From f2ca479565f6ef62ea773b4521f873af9fb6b7f7 Mon Sep 17 00:00:00 2001 From: mpmedia Date: Fri, 29 May 2026 18:04:37 -0500 Subject: [PATCH] v0.4.1: Add parent_id to create_project; link_drive_document_to_project; 4 Cybrosys checklist tools (97 total) --- server/odoo_mcp.py | 132 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 2 deletions(-) diff --git a/server/odoo_mcp.py b/server/odoo_mcp.py index 51c2c3e..534ce1e 100644 --- a/server/odoo_mcp.py +++ b/server/odoo_mcp.py @@ -714,16 +714,20 @@ def list_task_stages(project_id: int = None) -> list: @mcp.tool() def create_project(name: str, description: str = "", user_id: int = None, date_start: str = "", date: str = "", - privacy_visibility: str = "employees") -> int: + privacy_visibility: str = "employees", + parent_id: int = None) -> int: """Create a new project. Returns the new project ID. + parent_id: ID of the parent project.project — use to create sub-projects within a programme. 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".""" + Trigger phrases: "create project", "new project", "add a project", "start a project", + "create sub-project", "create child 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 + if parent_id: vals["parent_id"] = parent_id vals["privacy_visibility"] = privacy_visibility or "employees" return _create("project.project", vals) @@ -937,6 +941,130 @@ def update_task_stage(stage_id: int, name: str = "", sequence: int = None, return _write("project.task.type", stage_id, vals) if vals else False +# ── Link Drive document to project ─────────────────────────────────────────── + +@mcp.tool() +def link_drive_document_to_project(project_id: int, drive_url: str, + name: str = "Google Drive Folder") -> int: + """Attach a Google Drive folder or document URL to a project.project record + as an ir.attachment of type 'url'. The link appears in the project's + Attachments panel in Odoo. Returns the new attachment ID. + Trigger phrases: "link drive folder to project", "attach drive to project", + "add drive link to project", "connect drive folder".""" + return _create("ir.attachment", { + "name": name, + "type": "url", + "url": drive_url, + "res_model": "project.project", + "res_id": project_id, + }) + + +# ── Task Checklists (projects_task_checklists — Cybrosys) ───────────────────── +# Models: task.checklist (templates), checklist.item (template items), +# checklist.item.line (per-task instances). State: todo/in_progress/done/cancel. +# project.task extended with: checklist_id (Many2one), checklists_ids (One2many via projects_id). + +@mcp.tool() +def list_checklist_templates(query: str = "", limit: int = 30) -> list: + """List available task.checklist templates with their items. + Trigger phrases: "checklist templates", "list checklists", "available checklists", + "show checklist templates".""" + domain = [["name", "ilike", query]] if query else [] + templates = _search_read("task.checklist", domain, + ["name", "description", "checklist_ids"], limit=limit) + for t in templates: + if t.get("checklist_ids"): + t["items"] = _search_read( + "checklist.item", + [["id", "in", t["checklist_ids"]]], + ["name", "sequence", "description"], + limit=200, + ) + return templates + + +@mcp.tool() +def get_task_checklists(task_id: int) -> list: + """Get all checklist item lines (checklist.item.line) attached to a task, + with state and item name. State values: 'todo', 'in_progress', 'done', 'cancel'. + Task progress % is recomputed automatically from these states in Odoo. + Trigger phrases: "task checklist", "checklist items", "checklist status", + "acceptance criteria", "phase checklist", "what's checked off".""" + return _search_read( + "checklist.item.line", + [["projects_id", "=", task_id]], + ["check_list_item_id", "description", "checklist_id", "state"], + limit=200, + ) + + +@mcp.tool() +def add_checklist_to_task(task_id: int, checklist_id: int) -> int: + """Attach a task.checklist template to a task by creating checklist.item.line records. + Idempotent — items already added for this checklist+task combination are skipped. + Also sets checklist_id on the task to the given template. + Returns count of new lines created. + Use list_checklist_templates to find valid checklist_id values. + NOTE: Replicates the UI onchange since that only fires client-side. + Trigger phrases: "add checklist to task", "attach checklist", "apply checklist", + "add acceptance criteria", "apply phase checklist".""" + # Items already present for this checklist on this task + existing = _search_read( + "checklist.item.line", + [["projects_id", "=", task_id], ["checklist_id", "=", checklist_id]], + ["check_list_item_id"], + limit=200, + ) + existing_item_ids = { + r["check_list_item_id"][0] + for r in existing + if isinstance(r.get("check_list_item_id"), list) + } + # Template items not yet added + items = _search_read( + "checklist.item", + [["checklist_id", "=", checklist_id]], + ["id"], + limit=200, + ) + new_items = [it for it in items if it["id"] not in existing_item_ids] + if not new_items: + return 0 + commands = [ + (0, 0, {"check_list_item_id": it["id"], "checklist_id": checklist_id, "state": "todo"}) + for it in new_items + ] + _write("project.task", task_id, { + "checklist_id": checklist_id, + "checklists_ids": commands, + }) + return len(new_items) + + +@mcp.tool() +def update_checklist_item(line_id: int, state: str) -> bool: + """Update the state of a checklist.item.line. + state: 'todo' | 'in_progress' | 'done' | 'cancel'. + Uses Odoo action methods so task progress % and chatter are updated automatically. + Use get_task_checklists to find valid line_id values. + Trigger phrases: "mark checklist item done", "check off item", "mark in progress", + "cancel checklist item", "complete acceptance criteria", "mark phase complete".""" + if state == "in_progress": + _call("checklist.item.line", "action_approve_and_next", [[line_id]], {}) + elif state == "done": + _call("checklist.item.line", "action_mark_completed", [[line_id]], {}) + elif state == "cancel": + _call("checklist.item.line", "action_mark_canceled", [[line_id]], {}) + elif state == "todo": + _write("checklist.item.line", line_id, {"state": "todo"}) + else: + raise ValueError( + f"Invalid state '{state}'. Valid values: 'todo', 'in_progress', 'done', 'cancel'." + ) + return True + + # ════════════════════════════════════════════════════════════════════════════ # HELPDESK # ════════════════════════════════════════════════════════════════════════════