261 lines
11 KiB
Python
261 lines
11 KiB
Python
|
|
# ============================================================
|
|||
|
|
# 订单管理领域服务(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}
|