# ============================================================ # 订单管理领域服务(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 = {"SUBMITTED", "CONFIRMED", "CANCELLED", "COMPLETED"} CUSTOMER_LEVELS = {"VIP", "A", "B", "C"} 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 list_orders(world: World) -> list[dict[str, Any]]: """销售订单列表投影(P0 只读;管理页数据源)。""" rows: list[dict[str, Any]] = [] for so in world["salesOrders"]: items = so.get("items", []) first = items[0] if items else {} rows.append({ "id": so["id"], "orderNo": so["orderNo"], "customerName": so["customerName"], "customerLevel": so["customerLevel"], "orderDate": so["orderDate"], "deliveryDate": so["deliveryDate"], "priority": so["priority"], "status": so["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"), }) rows.sort(key=lambda x: (x["status"] in ("CANCELLED", "COMPLETED"), x["deliveryDate"], x["priority"])) return rows 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: return next((so for so in world["salesOrders"] if so["id"] == order_id), None) if order_no: return next((so for so in world["salesOrders"] if so["orderNo"].lower() == order_no.lower()), None) 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))) status = str(out.get("status") or "CONFIRMED").upper() if status not in ORDER_STATUSES: raise ValueError("订单状态必须是 SUBMITTED/CONFIRMED/CANCELLED/COMPLETED") out["status"] = 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() 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 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,不再参与后续排产", "执行前自动建档,可回滚", ] raise ValueError(f"不支持的订单动作:{action}") def apply_order_action(world: World, next_id, action: str, payload: dict[str, Any]) -> dict[str, Any]: """应用订单写入动作到内存世界(P2 动作的业务侧实现;调用方负责门禁/快照/审计/落盘)。""" 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"] order.update({ "customerName": p["customerName"], "customerLevel": p["customerLevel"], "deliveryDate": p["deliveryDate"], "priority": p["priority"], "status": p["status"], "isRush": p["isRush"], "rushStrategy": p["rushStrategy"], "specialRequirements": p["specialRequirements"], "updatedAt": _now(), }) 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 p["status"] == "COMPLETED" else "CANCELLED" if p["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} 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.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}