前言

在製造業專案中,發包期程管理是非常重要的功能。專案經理需要追蹤:

  • 哪些料件發包給哪個廠商?
  • 預計什麼時候交貨?
  • 實際到貨日期是什麼時候?
  • 目前狀態如何?

這篇介紹如何在 專案管理資料模型 的基礎上,實作發包期程的 MCP 工具,讓用戶可以透過對話管理發包進度。


資料表設計

CREATE TABLE project_delivery_schedules (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
    vendor VARCHAR(128) NOT NULL,          -- 廠商名稱
    item VARCHAR(256) NOT NULL,            -- 料件名稱
    quantity VARCHAR(64),                  -- 數量(含單位)
    order_date DATE,                       -- 發包日期
    expected_delivery_date DATE,           -- 預計交貨日期
    actual_delivery_date DATE,             -- 實際到貨日期
    status VARCHAR(32) DEFAULT 'pending',  -- 狀態
    notes TEXT,                            -- 備註
    created_by VARCHAR(64),
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_delivery_project ON project_delivery_schedules(project_id);
CREATE INDEX idx_delivery_status ON project_delivery_schedules(status);
CREATE INDEX idx_delivery_vendor ON project_delivery_schedules(vendor);

狀態定義

狀態 說明 使用時機
pending 待發包 尚未下單
ordered 已發包 已下單,等待交貨
delivered 已到貨 貨物已到
completed 已完成 驗收完成

Pydantic Model

class DeliveryScheduleBase(BaseModel):
    """發包/交貨期程基礎欄位"""
    vendor: str                                    # 廠商名稱
    item: str                                      # 料件名稱
    quantity: str | None = None                    # 數量(含單位)
    order_date: date | None = None                 # 發包日期
    expected_delivery_date: date | None = None     # 預計交貨日期
    actual_delivery_date: date | None = None       # 實際到貨日期
    status: str = "pending"                        # 狀態
    notes: str | None = None                       # 備註

MCP 工具實作

新增發包記錄

@mcp.tool()
async def add_delivery_schedule(
    project_id: str,
    vendor: str,
    item: str,
    quantity: str | None = None,
    order_date: str | None = None,
    expected_delivery_date: str | None = None,
    status: str = "pending",
    notes: str | None = None,
) -> str:
    """
    新增專案發包/交貨記錄

    Args:
        project_id: 專案 UUID
        vendor: 廠商名稱(必填)
        item: 料件名稱(必填)
        quantity: 數量(含單位,如「2 台」)
        order_date: 發包日期(格式: YYYY-MM-DD)
        expected_delivery_date: 預計交貨日期(格式: YYYY-MM-DD)
        status: 狀態(pending/ordered/delivered/completed)
        notes: 備註
    """
    await ensure_db_connection()

    async with get_connection() as conn:
        # 驗證專案存在
        project = await conn.fetchrow(
            "SELECT id, name FROM projects WHERE id = $1",
            project_id,
        )
        if not project:
            return f"錯誤:找不到專案 {project_id}"

        # 解析日期
        parsed_order_date = None
        parsed_expected_date = None

        if order_date:
            try:
                parsed_order_date = date.fromisoformat(order_date)
            except ValueError:
                return "錯誤:發包日期格式錯誤,請使用 YYYY-MM-DD 格式"

        if expected_delivery_date:
            try:
                parsed_expected_date = date.fromisoformat(expected_delivery_date)
            except ValueError:
                return "錯誤:預計交貨日期格式錯誤"

        # 驗證狀態
        valid_statuses = ["pending", "ordered", "delivered", "completed"]
        if status not in valid_statuses:
            return f"錯誤:狀態必須是 {', '.join(valid_statuses)} 其中之一"

        # 新增記錄
        row = await conn.fetchrow(
            """
            INSERT INTO project_delivery_schedules
                (project_id, vendor, item, quantity, order_date,
                 expected_delivery_date, status, notes, created_by)
            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'AI')
            RETURNING id, vendor, item
            """,
            project_id, vendor, item, quantity,
            parsed_order_date, parsed_expected_date, status, notes,
        )

        return f"✅ 已新增發包記錄:【{vendor}{item}"

使用情境

用戶:水切爐專案要發包一個加熱器給亦達,預計 1/20 交貨

AI:(調用 add_delivery_schedule)
AI:✅ 已新增發包記錄:【亦達】加熱器
    預計交貨:2026-01-20
    狀態:待發包

更新發包記錄

更新支援兩種匹配方式:直接用 ID,或用廠商 + 料件名稱模糊匹配。

@mcp.tool()
async def update_delivery_schedule(
    project_id: str,
    delivery_id: str | None = None,
    vendor: str | None = None,
    item: str | None = None,
    new_vendor: str | None = None,
    new_item: str | None = None,
    new_quantity: str | None = None,
    new_status: str | None = None,
    order_date: str | None = None,
    actual_delivery_date: str | None = None,
    expected_delivery_date: str | None = None,
    new_notes: str | None = None,
) -> str:
    """
    更新專案發包/交貨記錄

    Args:
        project_id: 專案 UUID
        delivery_id: 發包記錄 UUID(直接指定)
        vendor: 廠商名稱(用於匹配記錄)
        item: 料件名稱(用於匹配記錄)
        new_status: 新狀態
        actual_delivery_date: 實際到貨日期
        ... 其他更新欄位
    """
    await ensure_db_connection()

    async with get_connection() as conn:
        # 驗證專案存在
        project = await conn.fetchrow(
            "SELECT id, name FROM projects WHERE id = $1", project_id
        )
        if not project:
            return f"錯誤:找不到專案 {project_id}"

        # 找到目標記錄
        if delivery_id:
            # 直接用 ID
            row = await conn.fetchrow(
                "SELECT * FROM project_delivery_schedules WHERE id = $1 AND project_id = $2",
                delivery_id, project_id,
            )
            if not row:
                return f"錯誤:找不到發包記錄 {delivery_id}"
            matches = [row]

        elif vendor and item:
            # 用廠商 + 料件模糊匹配
            matches = await conn.fetch(
                """
                SELECT * FROM project_delivery_schedules
                WHERE project_id = $1 AND vendor ILIKE $2 AND item ILIKE $3
                """,
                project_id, f"%{vendor}%", f"%{item}%",
            )
            if not matches:
                return f"錯誤:找不到匹配的發包記錄(廠商:{vendor},料件:{item})"

            if len(matches) > 1:
                # 多筆匹配,列出供選擇
                result = f"找到 {len(matches)} 筆匹配記錄,請更精確指定:\n"
                for m in matches:
                    result += f"- {m['vendor']} - {m['item']}(ID: {m['id']}\n"
                return result
        else:
            return "錯誤:請提供 delivery_id 或 vendor + item"

        # 執行更新...
        target = matches[0]
        # 建立 UPDATE SQL 動態更新有值的欄位
        # ...

        return f"✅ 已更新發包記錄:【{target['vendor']}{target['item']}"

使用情境

用戶:亦達的加熱器今天到貨了

AI:(調用 update_delivery_schedule,vendor="亦達",item="加熱器")
AI:✅ 已更新發包記錄:【亦達】加熱器
    實際到貨:2026-01-18
    狀態:已到貨

查詢發包記錄

@mcp.tool()
async def get_delivery_schedules(
    project_id: str,
    status: str | None = None,
    vendor: str | None = None,
    limit: int = 20,
) -> str:
    """
    取得專案的發包/交貨記錄

    Args:
        project_id: 專案 UUID
        status: 狀態過濾(pending/ordered/delivered/completed)
        vendor: 廠商過濾
        limit: 最大數量,預設 20
    """
    await ensure_db_connection()

    async with get_connection() as conn:
        # 驗證專案存在
        project = await conn.fetchrow(
            "SELECT id, name FROM projects WHERE id = $1", project_id
        )
        if not project:
            return f"錯誤:找不到專案 {project_id}"

        # 建立查詢
        sql = "SELECT * FROM project_delivery_schedules WHERE project_id = $1"
        params = [project_id]

        if status:
            sql += " AND status = $2"
            params.append(status)

        if vendor:
            sql += f" AND vendor ILIKE ${len(params) + 1}"
            params.append(f"%{vendor}%")

        sql += " ORDER BY COALESCE(expected_delivery_date, '9999-12-31'), created_at"
        sql += f" LIMIT ${len(params) + 1}"
        params.append(limit)

        rows = await conn.fetch(sql, *params)

        if not rows:
            return f"專案「{project['name']}」目前沒有發包記錄"

        # 格式化輸出
        status_names = {
            "pending": "待發包",
            "ordered": "已發包",
            "delivered": "已到貨",
            "completed": "已完成",
        }

        result = f"📦 {project['name']} 的發包記錄(共 {len(rows)} 筆):\n\n"

        for r in rows:
            status_display = status_names.get(r["status"], r["status"])
            result += f"【{r['vendor']}{r['item']}\n"
            if r["quantity"]:
                result += f"  數量:{r['quantity']}\n"
            if r["order_date"]:
                result += f"  發包日:{r['order_date']}\n"
            if r["expected_delivery_date"]:
                result += f"  預計交貨:{r['expected_delivery_date']}\n"
            if r["actual_delivery_date"]:
                result += f"  實際到貨:{r['actual_delivery_date']}\n"
            result += f"  狀態:{status_display}\n\n"

        return result

使用情境

用戶:水切爐專案的發包進度如何?

AI:(調用 get_delivery_schedules)
AI:📦 水切爐改善 的發包記錄(共 3 筆):

    【亦達】加熱器
      數量:2 台
      預計交貨:2026-01-20
      狀態:待發包

    【金豐】鋼板
      數量:10 片
      發包日:2025-12-15
      預計交貨:2026-01-10
      狀態:已發包

    【東元】馬達
      數量:1 台
      發包日:2025-12-01
      預計交貨:2025-12-20
      實際到貨:2025-12-18
      狀態:已到貨

進階查詢

按狀態過濾

用戶:有哪些還沒到的?

AI:(調用 get_delivery_schedules,status="ordered")
AI:📦 水切爐改善 - 已發包待交貨(共 1 筆):

    【金豐】鋼板
      預計交貨:2026-01-10
      狀態:已發包

按廠商過濾

用戶:亦達的東西都發包了嗎?

AI:(調用 get_delivery_schedules,vendor="亦達")
AI:📦 水切爐改善 - 亦達相關(共 1 筆):

    【亦達】加熱器
      數量:2 台
      預計交貨:2026-01-20
      狀態:待發包

    還有 1 筆待發包

工作流程

典型的發包追蹤流程:

1. 待發包 (pending)
   ├── 新增發包記錄
   └── 填寫廠商、料件、預計交貨日

2. 已發包 (ordered)
   ├── 更新狀態為 ordered
   └── 填寫發包日期

3. 已到貨 (delivered)
   ├── 更新狀態為 delivered
   └── 填寫實際到貨日期

4. 已完成 (completed)
   └── 驗收完成,更新狀態

對話範例

用戶:金豐的鋼板今天下單了

AI:(調用 update_delivery_schedule)
AI:✅ 已更新發包記錄:【金豐】鋼板
    發包日:2026-01-05
    狀態:已發包

---

用戶:鋼板到了

AI:(調用 update_delivery_schedule)
AI:✅ 已更新發包記錄:【金豐】鋼板
    實際到貨:2026-01-09
    狀態:已到貨

小結

發包期程管理功能包含:

工具 功能
add_delivery_schedule 新增發包記錄
update_delivery_schedule 更新記錄(支援模糊匹配)
get_delivery_schedules 查詢記錄(支援過濾)

關鍵設計:

  • 模糊匹配:不需記住 UUID,用廠商 + 料件名稱即可更新
  • 狀態追蹤:pending → ordered → delivered → completed
  • 日期管理:發包日、預計交貨、實際到貨

下一篇 專案附件與連結管理 會介紹如何管理專案相關的檔案和連結。


參考資源