feat: add Projects module tools (v0.4.0)
This commit is contained in:
@@ -709,6 +709,234 @@ def list_task_stages(project_id: int = None) -> list:
|
||||
["id", "name", "sequence"], limit=50, order="sequence asc")
|
||||
|
||||
|
||||
# ── Project CRUD ──────────────────────────────────────────────────────────────
|
||||
|
||||
@mcp.tool()
|
||||
def create_project(name: str, description: str = "", user_id: int = None,
|
||||
date_start: str = "", date: str = "",
|
||||
privacy_visibility: str = "employees") -> int:
|
||||
"""Create a new project. Returns the new project ID.
|
||||
privacy_visibility: 'employees' (all employees), 'portal' (employees + invited portal users),
|
||||
'followers' (invited internal users only).
|
||||
Trigger phrases: "create project", "new project", "add a project", "start a project"."""
|
||||
vals: dict = {"name": name}
|
||||
if description: vals["description"] = description
|
||||
if user_id: vals["user_id"] = user_id
|
||||
if date_start: vals["date_start"] = date_start
|
||||
if date: vals["date"] = date
|
||||
vals["privacy_visibility"] = privacy_visibility or "employees"
|
||||
return _create("project.project", vals)
|
||||
|
||||
@mcp.tool()
|
||||
def update_project(project_id: int, name: str = "", description: str = "",
|
||||
user_id: int = None, date_start: str = "", date: str = "",
|
||||
privacy_visibility: str = "") -> bool:
|
||||
"""Update an existing project's metadata.
|
||||
privacy_visibility: 'employees', 'portal', 'followers'.
|
||||
Trigger phrases: "update project", "rename project", "change project owner",
|
||||
"set project deadline", "edit project"."""
|
||||
vals: dict = {}
|
||||
if name: vals["name"] = name
|
||||
if description: vals["description"] = description
|
||||
if user_id: vals["user_id"] = user_id
|
||||
if date_start: vals["date_start"] = date_start
|
||||
if date: vals["date"] = date
|
||||
if privacy_visibility: vals["privacy_visibility"] = privacy_visibility
|
||||
return _write("project.project", project_id, vals) if vals else False
|
||||
|
||||
@mcp.tool()
|
||||
def archive_project(project_id: int, archive: bool = True) -> bool:
|
||||
"""Archive or unarchive a project (soft delete). archive=True to archive, False to restore.
|
||||
Trigger phrases: "archive project", "close project", "deactivate project",
|
||||
"restore project", "reopen project"."""
|
||||
return _write("project.project", project_id, {"active": not archive})
|
||||
|
||||
|
||||
# ── Milestones ────────────────────────────────────────────────────────────────
|
||||
|
||||
@mcp.tool()
|
||||
def list_milestones(project_id: int, include_reached: bool = False) -> list:
|
||||
"""List milestones for a project.
|
||||
Set include_reached=True to also include already-completed milestones.
|
||||
Trigger phrases: "list milestones", "project milestones", "show milestones",
|
||||
"what milestones exist"."""
|
||||
domain = [["project_id", "=", project_id]]
|
||||
if not include_reached:
|
||||
domain.append(["is_reached", "=", False])
|
||||
return _search_read("project.milestone", domain,
|
||||
["id", "name", "project_id", "deadline", "is_reached", "reached_date"],
|
||||
limit=100, order="deadline asc")
|
||||
|
||||
@mcp.tool()
|
||||
def create_milestone(project_id: int, name: str, deadline: str = "") -> int:
|
||||
"""Create a milestone for a project. deadline format: YYYY-MM-DD. Returns milestone ID.
|
||||
Trigger phrases: "create milestone", "add milestone", "new milestone"."""
|
||||
vals: dict = {"project_id": project_id, "name": name}
|
||||
if deadline: vals["deadline"] = deadline
|
||||
return _create("project.milestone", vals)
|
||||
|
||||
@mcp.tool()
|
||||
def update_milestone(milestone_id: int, name: str = "", deadline: str = "",
|
||||
is_reached: bool = None) -> bool:
|
||||
"""Update a milestone's name, deadline, or completion status.
|
||||
Set is_reached=True to mark it complete, False to reopen it.
|
||||
Trigger phrases: "update milestone", "mark milestone complete", "change milestone deadline",
|
||||
"close milestone"."""
|
||||
vals: dict = {}
|
||||
if name: vals["name"] = name
|
||||
if deadline: vals["deadline"] = deadline
|
||||
if is_reached is not None: vals["is_reached"] = is_reached
|
||||
return _write("project.milestone", milestone_id, vals) if vals else False
|
||||
|
||||
|
||||
# ── Task chatter / notes ──────────────────────────────────────────────────────
|
||||
|
||||
@mcp.tool()
|
||||
def post_task_message(task_id: int, body: str,
|
||||
message_type: str = "comment") -> int:
|
||||
"""Post a message or internal note to a task's chatter thread.
|
||||
message_type: 'comment' (visible to all followers) or 'internal' (internal note only).
|
||||
Returns the new mail.message ID.
|
||||
Trigger phrases: "post a note", "log a message", "add a comment to the task",
|
||||
"internal note on task", "update task thread", "leave a comment"."""
|
||||
subtype_xmlid = "mail.mt_note" if message_type == "internal" else "mail.mt_comment"
|
||||
result = _call("project.task", "message_post", [[task_id]], {
|
||||
"body": body,
|
||||
"message_type": "comment",
|
||||
"subtype_xmlid": subtype_xmlid,
|
||||
})
|
||||
if isinstance(result, int):
|
||||
return result
|
||||
if isinstance(result, dict):
|
||||
return result.get("id", 0)
|
||||
return 0
|
||||
|
||||
@mcp.tool()
|
||||
def get_task_messages(task_id: int, limit: int = 20) -> list:
|
||||
"""Get the chatter message history for a task (most recent first).
|
||||
Trigger phrases: "task messages", "task history", "chatter", "task notes",
|
||||
"task comments", "what was said on this task"."""
|
||||
return _search_read("mail.message",
|
||||
[["model", "=", "project.task"], ["res_id", "=", task_id],
|
||||
["message_type", "in", ["comment", "email"]]],
|
||||
["id", "author_id", "date", "body", "subtype_id", "message_type"],
|
||||
limit=limit, order="date desc")
|
||||
|
||||
|
||||
# ── Subtasks ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@mcp.tool()
|
||||
def get_task_subtasks(task_id: int) -> list:
|
||||
"""Get all direct subtasks (child tasks) of a task.
|
||||
Trigger phrases: "subtasks", "child tasks", "get subtasks", "list subtasks",
|
||||
"what are the subtasks"."""
|
||||
return _search_read("project.task", [["parent_id", "=", task_id]],
|
||||
["id", "name", "stage_id", "user_ids", "date_deadline",
|
||||
"priority", "kanban_state", "active"],
|
||||
limit=100, order="sequence asc")
|
||||
|
||||
@mcp.tool()
|
||||
def create_subtask(name: str, parent_task_id: int, project_id: int = None,
|
||||
description: str = "", date_deadline: str = "",
|
||||
user_ids: list = None) -> int:
|
||||
"""Create a subtask linked to a parent task.
|
||||
Inherits the parent's project if project_id is omitted. Returns the new subtask ID.
|
||||
Trigger phrases: "create subtask", "add subtask", "new subtask", "child task"."""
|
||||
vals: dict = {"name": name, "parent_id": parent_task_id}
|
||||
if project_id: vals["project_id"] = project_id
|
||||
if description: vals["description"] = description
|
||||
if date_deadline: vals["date_deadline"] = date_deadline
|
||||
if user_ids: vals["user_ids"] = [(6, 0, user_ids)]
|
||||
return _create("project.task", vals)
|
||||
|
||||
|
||||
# ── Archive task ──────────────────────────────────────────────────────────────
|
||||
|
||||
@mcp.tool()
|
||||
def archive_task(task_id: int, archive: bool = True) -> bool:
|
||||
"""Archive or unarchive a task (soft delete). archive=True to archive, False to restore.
|
||||
Trigger phrases: "archive task", "close task", "remove task", "restore task",
|
||||
"deactivate task", "mark task done and archive"."""
|
||||
return _write("project.task", task_id, {"active": not archive})
|
||||
|
||||
|
||||
# ── Followers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@mcp.tool()
|
||||
def add_task_follower(task_id: int, partner_ids: list = None,
|
||||
user_ids: list = None) -> bool:
|
||||
"""Subscribe followers to a task so they receive update notifications.
|
||||
Pass partner_ids (res.partner record IDs) or user_ids (res.users record IDs).
|
||||
Trigger phrases: "add follower", "subscribe to task", "watch task",
|
||||
"notify user on task", "follow this task"."""
|
||||
if user_ids and not partner_ids:
|
||||
users = _search_read("res.users", [["id", "in", user_ids]],
|
||||
["partner_id"], limit=len(user_ids) + 5)
|
||||
partner_ids = [u["partner_id"][0] for u in users if u.get("partner_id")]
|
||||
if not partner_ids:
|
||||
return False
|
||||
_call("project.task", "message_subscribe", [[task_id]], {"partner_ids": partner_ids})
|
||||
return True
|
||||
|
||||
@mcp.tool()
|
||||
def remove_task_follower(task_id: int, partner_ids: list = None,
|
||||
user_ids: list = None) -> bool:
|
||||
"""Unsubscribe followers from a task.
|
||||
Pass partner_ids (res.partner record IDs) or user_ids (res.users record IDs).
|
||||
Trigger phrases: "remove follower", "unsubscribe from task", "stop following task",
|
||||
"mute task notifications"."""
|
||||
if user_ids and not partner_ids:
|
||||
users = _search_read("res.users", [["id", "in", user_ids]],
|
||||
["partner_id"], limit=len(user_ids) + 5)
|
||||
partner_ids = [u["partner_id"][0] for u in users if u.get("partner_id")]
|
||||
if not partner_ids:
|
||||
return False
|
||||
_call("project.task", "message_unsubscribe", [[task_id]], {"partner_ids": partner_ids})
|
||||
return True
|
||||
|
||||
|
||||
# ── Stage management ──────────────────────────────────────────────────────────
|
||||
|
||||
@mcp.tool()
|
||||
def create_task_stage(name: str, project_ids: list, sequence: int = 10,
|
||||
description: str = "") -> int:
|
||||
"""Create a new task stage and link it to one or more projects.
|
||||
project_ids: list of project.project IDs to attach this stage to.
|
||||
Returns the new stage ID.
|
||||
Trigger phrases: "create stage", "add stage", "new task stage", "add column",
|
||||
"add kanban column"."""
|
||||
vals: dict = {
|
||||
"name": name,
|
||||
"project_ids": [(6, 0, project_ids)],
|
||||
"sequence": sequence,
|
||||
}
|
||||
if description: vals["description"] = description
|
||||
return _create("project.task.type", vals)
|
||||
|
||||
@mcp.tool()
|
||||
def update_task_stage(stage_id: int, name: str = "", sequence: int = None,
|
||||
add_project_ids: list = None,
|
||||
remove_project_ids: list = None) -> bool:
|
||||
"""Update a task stage's name, sequence order, or project associations.
|
||||
add_project_ids: project IDs to link this stage to (appends, does not replace).
|
||||
remove_project_ids: project IDs to detach this stage from.
|
||||
Trigger phrases: "rename stage", "reorder stage", "update stage", "move stage",
|
||||
"change stage order"."""
|
||||
vals: dict = {}
|
||||
if name: vals["name"] = name
|
||||
if sequence is not None: vals["sequence"] = sequence
|
||||
commands = []
|
||||
if add_project_ids:
|
||||
for pid in add_project_ids:
|
||||
commands.append((4, pid)) # link without replacing others
|
||||
if remove_project_ids:
|
||||
for pid in remove_project_ids:
|
||||
commands.append((3, pid)) # unlink without deleting
|
||||
if commands:
|
||||
vals["project_ids"] = commands
|
||||
return _write("project.task.type", stage_id, vals) if vals else False
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# HELPDESK
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user