# ============================================================ # 订单管理领域服务(moduleId: domain-orders, 可重生 ✅) # 职责:销售订单只读投影 + P2 订单写入动作的纯应用逻辑 # 规则:新增/编辑/取消/完成/删除订单均写主干世界,必须由调用方走 Harness 确认卡 # ============================================================ from __future__ import annotations # 前向类型引用 from datetime import datetime # 时间戳 from typing import Any # 类型标注 from server.timeutil import fmt_dt, today0 # 时间格式化 World = dict[str, Any] # 世界状态类型别名 # 当前数据模型的订单状态。后续订单池审批会扩展为更完整状态机。 ORDER_STATUSES = { "DRAFT", "SUBMITTED", "APPROVED", "REJECTED", "CHANGED", "CANCELLED", "COMPLETED", "CONFIRMED", # 遗留别名 ≡ APPROVED(排产资格相同) } # 正式排产默认纳入的状态(OR-03) SCHEDULABLE_STATUSES = {"APPROVED", "CONFIRMED"} # 可提交审核 SUBMITTABLE_STATUSES = {"DRAFT", "REJECTED", "CHANGED"} # 可批准/驳回 REVIEWABLE_STATUSES = {"SUBMITTED", "CHANGED"} CUSTOMER_LEVELS = {"VIP", "A", "B", "C"} ORDER_ACTIONS = ( "order.upsert", "order.cancel", "order.complete", "order.delete", "order.clear", "order.submit", "order.approve", "order.reject", ) POOL_LANES = { "pending": ("SUBMITTED",), "approved": ("APPROVED", "CONFIRMED"), "changed": ("CHANGED",), "draft": ("DRAFT", "REJECTED"), "closed": ("CANCELLED", "COMPLETED"), } def normalize_status(status: str | None, *, default: str = "DRAFT") -> str: s = str(status or default).upper() if s == "CONFIRMED": return "APPROVED" # 新写入统一为 APPROVED if s not in ORDER_STATUSES: raise ValueError( "订单状态必须是 DRAFT/SUBMITTED/APPROVED/REJECTED/CHANGED/CANCELLED/COMPLETED") return s def is_schedulable(status: str | None, *, include_unapproved: bool = False) -> bool: s = str(status or "") if s in SCHEDULABLE_STATUSES: return True if include_unapproved and s in ("DRAFT", "SUBMITTED", "CHANGED", "REJECTED"): return True return False def _now() -> str: """当前时间字符串(与世界状态 createdAt/updatedAt 口径一致)。""" return fmt_dt(datetime.now()) def _product(world: World, product_id: int) -> dict[str, Any]: """取成品物料;不存在则抛出 ValueError,阻止脏订单进入世界状态。""" product = next((m for m in world["materials"] if m["id"] == product_id and m["type"] == "FINISHED_PRODUCT"), None) if product is None: raise ValueError(f"产品不存在或不是成品:{product_id}") return product def _next_item_id(world: World) -> int: """销售订单明细发号:扫描现有 items,避免改 WorldStore 计数器契约。""" max_id = 0 for so in world.get("salesOrders", []): for item in so.get("items", []): if isinstance(item.get("id"), int): max_id = max(max_id, item["id"]) return max_id + 1 def list_products(world: World) -> list[dict[str, Any]]: """成品下拉选项(P0 只读)。""" return [{ "id": m["id"], "code": m["code"], "name": m["name"], "unit": m["unit"], } for m in world["materials"] if m["type"] == "FINISHED_PRODUCT"] def _flex_status_to_sales(status: str | None) -> str: """现场/SQL 柔性订单状态 → 销售订单状态机。""" s = str(status or "").upper() if s in ("RELEASED", "RELEASE", "OPEN", "IN_PROGRESS", "PRODUCING", "PLANNED"): return "APPROVED" if s in ("DONE", "COMPLETED", "FINISHED", "CLOSED"): return "COMPLETED" if s in ("CANCELLED", "CANCELED"): return "CANCELLED" if s in ("DRAFT", "CREATED"): return "DRAFT" if s in ORDER_STATUSES or s == "CONFIRMED": try: return normalize_status(s) except ValueError: return "APPROVED" return "APPROVED" def sync_flex_orders_to_sales(world: World) -> int: """把 flexOrders 投影为 salesOrders,供订单管理 / MRP 分解使用。 仅替换 source∈{SQL,FLEX,SITE} 的行,保留用户手工销售单。 跳过演示厂 FO-26* / 无经典物料 id 的行,避免污染固定轨 RULE 引擎。 """ demo_mat = { "HV-HARNESS", "PDU-UNIT", "CHARGE-GUN", "HV-CONN", "CTRL-A", "CTRL-B", "CTRL-C", } flex = [] for o in world.get("flexOrders") or []: if (o.get("status") or "") in ("DONE", "CANCELLED", "COMPLETED"): continue ono = str(o.get("orderNo") or "") pc = str(o.get("productCode") or "") if ono.startswith(("FO-26", "SO-DEMO")): continue if pc in demo_mat: continue flex.append(o) kept = [so for so in (world.get("salesOrders") or []) if str(so.get("source") or "MANUAL").upper() not in ("SQL", "FLEX", "SITE")] mats = {m.get("code"): m for m in (world.get("materials") or []) if m.get("code")} # 缺物料时从 flexMaterials 补进经典表,保证有正整数 id flex_mats = {m.get("code"): m for m in (world.get("flexMaterials") or []) if m.get("code")} next_mat = max((m.get("id", 0) for m in (world.get("materials") or []) if isinstance(m.get("id"), int)), default=0) + 1 next_so = max((so.get("id", 0) for so in kept if isinstance(so.get("id"), int)), default=0) + 1 next_item = _next_item_id({"salesOrders": kept}) today = fmt_dt(today0())[:10] added = 0 for fo in flex: ono = str(fo.get("orderNo") or "").strip() pc = str(fo.get("productCode") or "").strip() if not ono or not pc: continue if any(so.get("orderNo") == ono for so in kept): continue mat = mats.get(pc) if mat is None: fm = flex_mats.get(pc) or { "code": pc, "name": fo.get("productName") or pc, "type": "FINISHED_PRODUCT", "unit": "件", } mat = { "id": next_mat, "code": pc, "name": fm.get("name") or pc, "spec": fm.get("spec") or "", "type": fm.get("type") or "FINISHED_PRODUCT", "unit": fm.get("unit") or "件", "productFamily": "", "safetyStock": float(fm.get("safetyStock") or 0), "procurementLeadTime": float(fm.get("procurementLeadTime") or 0), "stock": float(fm.get("stock") or 0), "inTransit": float(fm.get("inTransit") or 0), "status": "ACTIVE", } world.setdefault("materials", []).append(mat) mats[pc] = mat next_mat += 1 pid = mat.get("id") if not isinstance(pid, int) or pid <= 0: continue st = _flex_status_to_sales(fo.get("status")) qty = float(fo.get("quantity") or 0) or 1 kept.append({ "id": next_so, "orderNo": ono, "customerName": fo.get("customerName") or "现场客户", "customerLevel": fo.get("customerLevel") or "B", "orderDate": str(fo.get("orderDate") or today)[:10], "deliveryDate": str(fo.get("dueDate") or today)[:10], "priority": int(fo.get("priority") or 5), "status": st, "source": "SQL" if fo.get("craftlCode") else "FLEX", "isRush": bool(fo.get("isRush")), "specialRequirements": fo.get("drawingNo") or "", "totalAmount": 0, "createdAt": _now(), "updatedAt": _now(), "items": [{ "id": next_item, "productId": pid, "productCode": pc, "productName": fo.get("productName") or mat.get("name") or pc, "quantity": qty, "unit": mat.get("unit") or "件", "status": "PENDING" if st == "APPROVED" else st, }], }) next_so += 1 next_item += 1 added += 1 world["salesOrders"] = kept return added def list_orders(world: World) -> list[dict[str, Any]]: """销售订单列表投影(含已投影的 SQL/柔性单;只读兜底不写盘)。""" rows: list[dict[str, Any]] = [] for so in world.get("salesOrders") or []: items = so.get("items", []) first = items[0] if items else {} rows.append({ "id": so["id"], "orderNo": so["orderNo"], "customerName": so["customerName"], "customerLevel": so.get("customerLevel") or "B", "orderDate": so.get("orderDate") or "", "deliveryDate": so.get("deliveryDate") or so.get("dueDate") or "", "priority": so.get("priority") or 5, "status": _flex_status_to_sales(so.get("status")), "source": so.get("source", "MANUAL"), "isRush": so.get("isRush", False), "rushStrategy": so.get("rushStrategy"), "specialRequirements": so.get("specialRequirements", ""), "totalAmount": so.get("totalAmount", 0), "itemCount": len(items), "productId": first.get("productId"), "productCode": first.get("productCode", ""), "productName": first.get("productName", ""), "quantity": first.get("quantity", 0), "unit": first.get("unit", "件"), "updatedAt": so.get("updatedAt") or so.get("createdAt"), "reviewNote": so.get("reviewNote") or "", "schedulable": is_schedulable(_flex_status_to_sales(so.get("status"))), }) # 兜底:销售表空时只读展柔性订单(不写 salesOrders,避免污染演示世界) if not rows: mats = {m.get("code"): m for m in (world.get("materials") or [])} for i, fo in enumerate(world.get("flexOrders") or []): if (fo.get("status") or "") in ("DONE", "CANCELLED", "COMPLETED"): continue ono = str(fo.get("orderNo") or "") if ono.startswith(("FO-26", "SO-DEMO")): continue pc = fo.get("productCode") or "" st = _flex_status_to_sales(fo.get("status")) rows.append({ "id": 10_000_000 + int(fo.get("id") or i), "orderNo": fo.get("orderNo"), "customerName": fo.get("customerName") or "现场客户", "customerLevel": "B", "orderDate": "", "deliveryDate": fo.get("dueDate") or "", "priority": fo.get("priority") or 5, "status": st, "source": "FLEX", "isRush": False, "itemCount": 1, "productId": (mats.get(pc) or {}).get("id"), "productCode": pc, "productName": fo.get("productName") or (mats.get(pc) or {}).get("name") or pc, "quantity": fo.get("quantity") or 0, "unit": "件", "schedulable": is_schedulable(st), }) rows.sort(key=lambda x: ( x["status"] in ("CANCELLED", "COMPLETED"), x["status"] not in ("SUBMITTED", "CHANGED", "APPROVED"), x["deliveryDate"], x["priority"], )) return rows def pool_summary(world: World) -> dict[str, Any]: """订单池看板计数(只读,不写盘)。""" counts = {k: 0 for k in POOL_LANES} counts["all"] = 0 counts["schedulable"] = 0 for row in list_orders(world): counts["all"] += 1 st = row.get("status") if is_schedulable(st): counts["schedulable"] += 1 for lane, statuses in POOL_LANES.items(): if st in statuses: counts[lane] += 1 break return counts def list_production_orders(world: World) -> list[dict[str, Any]]: """生产订单/虚拟产线投影:优先最新柔性排产版本,其次固定轨 productionOrders。""" rows: list[dict[str, Any]] = [] flex_vers = world.get("flexScheduleVersions") or [] if flex_vers: ver = flex_vers[-1] vid = ver.get("id") orders_by_no = {o.get("orderNo"): o for o in (world.get("flexOrders") or [])} for vl in world.get("flexVirtualLines") or []: if vl.get("versionId") != vid: continue ono = vl.get("orderNo") o = orders_by_no.get(ono) or {} wos = [w for w in (world.get("flexWorkOrders") or []) if w.get("versionId") == vid and w.get("vlId") == vl.get("id")] rows.append({ "id": vl.get("id"), "orderNo": ono, "vlNo": vl.get("vlNo"), "productCode": vl.get("productCode") or o.get("productCode"), "productName": o.get("productName") or vl.get("productCode"), "quantity": vl.get("quantity") or o.get("quantity"), "dueDate": o.get("dueDate"), "plannedStart": vl.get("plannedStart"), "plannedEnd": vl.get("plannedEnd"), "stepCount": len(wos) or vl.get("stepCount") or 0, "status": vl.get("status") or "PLANNED", "versionNo": ver.get("versionNo"), "track": "flex", }) return rows for po in world.get("productionOrders") or []: rows.append({ "id": po.get("id"), "orderNo": po.get("orderNo") or po.get("salesOrderNo"), "vlNo": po.get("poNo") or po.get("code"), "productCode": po.get("productCode"), "productName": po.get("productName"), "quantity": po.get("quantity"), "dueDate": po.get("dueDate"), "plannedStart": po.get("plannedStart"), "plannedEnd": po.get("plannedEnd"), "stepCount": po.get("woCount") or 0, "status": po.get("status") or "PLANNED", "versionNo": po.get("schedulingVersionId"), "track": "fixed", }) return rows def find_product_by_hint(world: World, hint: str) -> dict[str, Any] | None: """按编码/名称模糊解析成品(对话建单用):先精确编码,再名称包含匹配。""" h = (hint or "").strip() if not h: return None products = [m for m in world.get("materials") or [] if m.get("type") == "FINISHED_PRODUCT"] if not products: products = [m for m in world.get("flexMaterials") or [] if m.get("type") == "FINISHED_PRODUCT"] exact = next((m for m in products if str(m.get("code") or "").lower() == h.lower()), None) if exact: return exact return next( (m for m in products if h in str(m.get("name") or "") or str(m.get("name") or "") in h or h.lower() in str(m.get("code") or "").lower()), None, ) def find_order(world: World, *, order_id: int | None = None, order_no: str | None = None) -> dict[str, Any] | None: """按 ID 或订单号查订单(P0 只读;含柔性表只读回退)。""" if order_id is not None: hit = next((so for so in world.get("salesOrders") or [] if so["id"] == order_id), None) if hit: return hit if order_no: hit = next((so for so in world.get("salesOrders") or [] if str(so.get("orderNo") or "").lower() == order_no.lower()), None) if hit: return hit fo = next((o for o in world.get("flexOrders") or [] if str(o.get("orderNo") or "").lower() == order_no.lower()), None) if fo: return { "id": fo.get("id"), "orderNo": fo.get("orderNo"), "customerName": fo.get("customerName") or "现场客户", "customerLevel": "B", "deliveryDate": fo.get("dueDate"), "priority": fo.get("priority") or 5, "status": _flex_status_to_sales(fo.get("status")), "source": "FLEX", "items": [{ "productCode": fo.get("productCode"), "productName": fo.get("productName"), "quantity": fo.get("quantity"), "unit": "件", }], } return None def normalize_order_payload(world: World, payload: dict[str, Any]) -> dict[str, Any]: """订单写入载荷归一化与校验(P0 纯校验)。""" out = dict(payload) if out.get("id") in ("", None): out.pop("id", None) elif not isinstance(out["id"], int): out["id"] = int(out["id"]) out["customerName"] = str(out.get("customerName") or "").strip() if not out["customerName"]: raise ValueError("客户名称不能为空") out["customerLevel"] = str(out.get("customerLevel") or "A").upper() if out["customerLevel"] not in CUSTOMER_LEVELS: raise ValueError("客户等级必须是 VIP/A/B/C") out["deliveryDate"] = str(out.get("deliveryDate") or "").strip() if not out["deliveryDate"]: raise ValueError("交期不能为空") out["priority"] = max(1, min(9, int(out.get("priority") or 5))) out["productId"] = int(out.get("productId") or 0) product = _product(world, out["productId"]) out["productName"] = product["name"] out["productCode"] = product["code"] out["unit"] = product["unit"] out["quantity"] = max(1, int(float(out.get("quantity") or 1))) # 新建默认 DRAFT;遗留 CONFIRMED 写入时归一为 APPROVED default_status = "DRAFT" if not out.get("id") else "APPROVED" out["status"] = normalize_status(out.get("status"), default=default_status) out["isRush"] = bool(out.get("isRush", False)) out["rushStrategy"] = "STRATEGY_SHIFT" if out["isRush"] else None out["specialRequirements"] = str(out.get("specialRequirements") or "").strip() out["reviewNote"] = str(out.get("reviewNote") or "").strip() return out def confirmation_for_order_action(world: World, action: str, payload: dict[str, Any]) -> tuple[str, list[str]]: """生成订单 P2 确认卡标题与影响摘要。""" if action == "order.upsert": p = normalize_order_payload(world, payload) verb = "编辑" if p.get("id") else "新增" title = f"{verb}销售订单" lines = [ f"客户:{p['customerName']}({p['customerLevel']})", f"产品:{p['productName']} × {p['quantity']} {p['unit']},交期 {p['deliveryDate']}", "批准后写入主干订单池,并影响后续新排产版本(P2)", ] return title, lines if action == "order.clear": n = len(world.get("salesOrders", [])) po = len(world.get("purchaseOrders", [])) os = len(world.get("outsourceOrders", [])) if n == 0 and po == 0 and os == 0: raise ValueError("订单池已空,无需清理") return "一键清理全部订单", [ f"将清空销售订单 {n} 条", f"同步清空采购建议 {po} / 委外建议 {os} 条", "历史排产版本与工单保留不回写;后续试排将无订单可排", "执行前自动建档,可回滚(P2)", ] if action == "order.approve": ids = _resolve_order_ids(world, payload) if not ids: raise ValueError("没有可批准的订单(需 SUBMITTED/CHANGED)") nos = [find_order(world, order_id=i)["orderNo"] for i in ids] # type: ignore[index] return f"批准订单 {len(ids)} 条", [ "、".join(nos[:5]) + ("…" if len(nos) > 5 else ""), "批准后状态 → APPROVED,可进入正式排产", "执行前自动建档,可回滚(P2)", ] if action == "order.reject": ids = _resolve_order_ids(world, payload, for_reject=True) if not ids: raise ValueError("没有可驳回的订单(需 SUBMITTED/CHANGED)") note = str(payload.get("reviewNote") or "").strip() or "(未填写原因)" nos = [find_order(world, order_id=i)["orderNo"] for i in ids] # type: ignore[index] return f"驳回订单 {len(ids)} 条", [ "、".join(nos[:5]) + ("…" if len(nos) > 5 else ""), f"驳回原因:{note}", "驳回后状态 → REJECTED,不参与排产;可修改后重新提交", "执行前自动建档,可回滚(P2)", ] order = find_order(world, order_id=int(payload.get("id") or 0), order_no=payload.get("orderNo")) if order is None: raise ValueError("目标订单不存在") if action == "order.cancel": return f"取消订单 {order['orderNo']}", [ f"客户:{order['customerName']},当前状态 {order['status']}", "批准后该订单不再参与后续排产,历史版本不回写", "执行前自动建档,可回滚", ] if action == "order.complete": return f"完成订单 {order['orderNo']}", [ f"客户:{order['customerName']},当前状态 {order['status']}", "批准后订单及明细置为 COMPLETED,不再参与后续排产", "执行前自动建档,可回滚", ] if action == "order.delete": draft_po = sum(1 for p in world.get("purchaseOrders", []) if p.get("salesOrderId") == order["id"] and p.get("status") == "DRAFT") draft_os = sum(1 for o in world.get("outsourceOrders", []) if o.get("salesOrderId") == order["id"] and o.get("status") == "DRAFT") hist = sum(1 for po in world.get("productionOrders", []) if po.get("salesOrderId") == order["id"]) lines = [ f"客户:{order['customerName']} · {order['orderNo']}({order['status']})", "批准后从订单池物理删除,后续排产不再纳入", ] if draft_po or draft_os: lines.append(f"同步清除草稿采购 {draft_po} / 委外 {draft_os} 条") if hist: lines.append(f"历史生产订单 {hist} 条保留不回写(仅断联销售单)") lines.append("执行前自动建档,可回滚") return f"删除订单 {order['orderNo']}", lines raise ValueError(f"不支持的订单动作:{action}") def _resolve_order_ids(world: World, payload: dict[str, Any], *, for_reject: bool = False) -> list[int]: """解析单笔/批量订单 ID,并过滤到可审核状态。""" allowed = REVIEWABLE_STATUSES raw_ids = payload.get("orderIds") or payload.get("ids") or [] ids: list[int] = [] if raw_ids == "pending" or raw_ids == ["pending"]: return [so["id"] for so in world.get("salesOrders", []) if so.get("status") in allowed] if isinstance(raw_ids, list) and raw_ids: ids = [int(x) for x in raw_ids] elif payload.get("id"): ids = [int(payload["id"])] elif payload.get("orderNo"): o = find_order(world, order_no=str(payload["orderNo"])) if o: ids = [o["id"]] out = [] for oid in ids: o = find_order(world, order_id=oid) if o and o["status"] in allowed: out.append(oid) return out def apply_order_action(world: World, next_id, action: str, payload: dict[str, Any]) -> dict[str, Any]: """应用订单写入动作到内存世界(P2 动作的业务侧实现;调用方负责门禁/快照/审计/落盘)。""" if action == "order.clear": n = len(world.get("salesOrders", [])) po_n = len(world.get("purchaseOrders", [])) os_n = len(world.get("outsourceOrders", [])) world["salesOrders"] = [] world["purchaseOrders"] = [] world["outsourceOrders"] = [] return { "order": {"id": 0, "orderNo": "ALL", "status": "CLEARED"}, "created": False, "beforeStatus": None, "clearedOrders": n, "clearedPurchase": po_n, "clearedOutsource": os_n, } if action == "order.upsert": p = normalize_order_payload(world, payload) if p.get("id"): order = find_order(world, order_id=p["id"]) if order is None: raise ValueError("目标订单不存在") before_status = order["status"] # 已批准订单被改内容 → CHANGED,需重新审核(除非显式仍写 APPROVED 且字段未变) next_status = p["status"] content_changed = any([ order.get("customerName") != p["customerName"], order.get("customerLevel") != p["customerLevel"], order.get("deliveryDate") != p["deliveryDate"], order.get("priority") != p["priority"], (order.get("items") or [{}])[0].get("productId") != p["productId"], (order.get("items") or [{}])[0].get("quantity") != p["quantity"], bool(order.get("isRush")) != bool(p["isRush"]), ]) if before_status in SCHEDULABLE_STATUSES and content_changed and next_status in SCHEDULABLE_STATUSES: next_status = "CHANGED" order.update({ "customerName": p["customerName"], "customerLevel": p["customerLevel"], "deliveryDate": p["deliveryDate"], "priority": p["priority"], "status": next_status, "isRush": p["isRush"], "rushStrategy": p["rushStrategy"], "specialRequirements": p["specialRequirements"], "updatedAt": _now(), }) if next_status != before_status: order.setdefault("changes", []).append({ "at": _now(), "action": "order.upsert", "from": before_status, "to": next_status, }) if not order.get("items"): order["items"] = [] item = order["items"][0] if order["items"] else None if item is None: item = {"id": _next_item_id(world), "orderId": order["id"], "lineNo": 1} order["items"].append(item) item.update({ "productId": p["productId"], "productName": p["productName"], "productCode": p["productCode"], "quantity": p["quantity"], "unit": p["unit"], "bomVersion": item.get("bomVersion", "V1.0"), "routingVersion": item.get("routingVersion", "V1.0"), "status": "COMPLETED" if next_status == "COMPLETED" else "CANCELLED" if next_status == "CANCELLED" else "PENDING", "note": item.get("note", ""), }) return {"order": order, "created": False, "beforeStatus": before_status} order_id = next_id("salesOrder") order_date = today0().strftime("%Y-%m-%d") product = _product(world, p["productId"]) order = { "id": order_id, "orderNo": "SO" + today0().strftime("%Y%m%d") + f"{order_id:03d}", "customerId": f"CUST{order_id:03d}", "customerName": p["customerName"], "customerLevel": p["customerLevel"], "orderDate": order_date, "deliveryDate": p["deliveryDate"], "priority": p["priority"], "manualPriority": None, "status": p["status"], "source": "MANUAL", "specialRequirements": p["specialRequirements"], "totalAmount": p["quantity"] * 100, "isRush": p["isRush"], "rushStrategy": p["rushStrategy"], "changes": [], "createdBy": "planner", "createdAt": _now(), "updatedAt": _now(), "items": [{ "id": _next_item_id(world), "orderId": order_id, "lineNo": 1, "productId": product["id"], "productName": product["name"], "productCode": product["code"], "quantity": p["quantity"], "unit": product["unit"], "bomVersion": "V1.0", "routingVersion": "V1.0", "status": "PENDING", "note": "", }], } world["salesOrders"].append(order) return {"order": order, "created": True, "beforeStatus": None} # 批准/驳回支持 orderIds / pending,须在单笔 id 查找之前 if action == "order.approve": ids = _resolve_order_ids(world, payload) if not ids: raise ValueError("没有可批准的订单") updated = [] for oid in ids: o = find_order(world, order_id=oid) if not o: continue before = o["status"] o["status"] = "APPROVED" o["reviewNote"] = str(payload.get("reviewNote") or "").strip() o["updatedAt"] = _now() o.setdefault("changes", []).append({ "at": _now(), "action": "order.approve", "from": before, "to": "APPROVED", }) updated.append(o) first = updated[0] return { "order": first, "created": False, "beforeStatus": first.get("changes", [{}])[-1].get("from"), "approvedCount": len(updated), "orderNos": [o["orderNo"] for o in updated], } if action == "order.reject": ids = _resolve_order_ids(world, payload, for_reject=True) if not ids: raise ValueError("没有可驳回的订单") note = str(payload.get("reviewNote") or payload.get("reason") or "").strip() updated = [] for oid in ids: o = find_order(world, order_id=oid) if not o: continue before = o["status"] o["status"] = "REJECTED" o["reviewNote"] = note or "已驳回" o["updatedAt"] = _now() o.setdefault("changes", []).append({ "at": _now(), "action": "order.reject", "from": before, "to": "REJECTED", "note": note, }) updated.append(o) first = updated[0] return { "order": first, "created": False, "beforeStatus": "SUBMITTED", "rejectedCount": len(updated), "orderNos": [o["orderNo"] for o in updated], } order = find_order(world, order_id=int(payload.get("id") or 0), order_no=payload.get("orderNo")) if order is None: raise ValueError("目标订单不存在") before_status = order["status"] if action == "order.delete": oid = order["id"] order_no = order["orderNo"] # 清除该单关联的草稿 MRP;已下达建议保留但断开关联语义由业务自行处理 world["purchaseOrders"] = [ p for p in world.get("purchaseOrders", []) if not (p.get("salesOrderId") == oid and p.get("status") == "DRAFT") ] world["outsourceOrders"] = [ o for o in world.get("outsourceOrders", []) if not (o.get("salesOrderId") == oid and o.get("status") == "DRAFT") ] world["salesOrders"] = [so for so in world["salesOrders"] if so["id"] != oid] return {"order": {"id": oid, "orderNo": order_no, "status": "DELETED"}, "created": False, "beforeStatus": before_status} if action == "order.cancel": order["status"] = "CANCELLED" for item in order.get("items", []): item["status"] = "CANCELLED" elif action == "order.complete": order["status"] = "COMPLETED" for item in order.get("items", []): item["status"] = "COMPLETED" else: raise ValueError(f"不支持的订单动作:{action}") order["updatedAt"] = _now() order.setdefault("changes", []).append({ "at": _now(), "action": action, "from": before_status, "to": order["status"], }) return {"order": order, "created": False, "beforeStatus": before_status} def apply_order_submit(world: World, payload: dict[str, Any]) -> dict[str, Any]: """提交审核(P1):DRAFT/REJECTED/CHANGED → SUBMITTED。""" order = find_order(world, order_id=int(payload.get("id") or 0), order_no=payload.get("orderNo")) if order is None: raise ValueError("目标订单不存在") before = order["status"] if before not in SUBMITTABLE_STATUSES: raise ValueError(f"订单 {order['orderNo']} 状态为 {before},不能提交审核") order["status"] = "SUBMITTED" order["reviewNote"] = "" order["updatedAt"] = _now() order.setdefault("changes", []).append({ "at": _now(), "action": "order.submit", "from": before, "to": "SUBMITTED", }) return {"order": order, "created": False, "beforeStatus": before}