140 lines
4.8 KiB
Python
140 lines
4.8 KiB
Python
|
|
# ============================================================
|
|||
|
|
# OR-01 订单管理黄金测试(moduleId: golden-order-management, 发版门禁 §14.3)
|
|||
|
|
# 固化订单池写入的最小闭环:
|
|||
|
|
# O1 新增订单进入后续排产输入
|
|||
|
|
# O2 取消订单从后续排产输入排除
|
|||
|
|
# O3 P2 确认卡摘要可生成并校验成品物料
|
|||
|
|
# O4 审计事件同时满足 at/ts 契约且哈希链可校验
|
|||
|
|
# ============================================================
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import pytest
|
|||
|
|
|
|||
|
|
from server.agent_core.audit import write_audit
|
|||
|
|
from server.agent_core.registry import verify_audit_chain
|
|||
|
|
from server.aps_domain.orders import apply_order_action, confirmation_for_order_action
|
|||
|
|
from server.engines import get_engine
|
|||
|
|
from server.engines.base import EngineParams
|
|||
|
|
from server.state.seed import seed_world
|
|||
|
|
from server.timeutil import add_minutes, fmt_date, today0
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _next_id_factory(world):
|
|||
|
|
"""按当前世界最大 ID 起号,避免新增订单与种子 ID 冲突。"""
|
|||
|
|
tables = {
|
|||
|
|
"audit": "auditEvents",
|
|||
|
|
"conflict": "conflicts",
|
|||
|
|
"log": "logs",
|
|||
|
|
"productionOrder": "productionOrders",
|
|||
|
|
"salesOrder": "salesOrders",
|
|||
|
|
"scheduleVersion": "scheduleVersions",
|
|||
|
|
"workOrder": "workOrders",
|
|||
|
|
}
|
|||
|
|
counters: dict[str, int] = {}
|
|||
|
|
|
|||
|
|
def next_id(kind: str) -> int:
|
|||
|
|
if kind not in counters:
|
|||
|
|
rows = world.get(tables.get(kind, kind + "s"), [])
|
|||
|
|
counters[kind] = max((r.get("id", 0) for r in rows if isinstance(r, dict)), default=0)
|
|||
|
|
counters[kind] += 1
|
|||
|
|
return counters[kind]
|
|||
|
|
|
|||
|
|
return next_id
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _run(world):
|
|||
|
|
"""执行一次规则引擎排产。"""
|
|||
|
|
params = EngineParams(
|
|||
|
|
orderIds=[],
|
|||
|
|
engineType="RULE",
|
|||
|
|
strategyTemplate="COMPREHENSIVE",
|
|||
|
|
planningHorizonDays=14,
|
|||
|
|
startDate=fmt_date(add_minutes(today0(), 24 * 60)),
|
|||
|
|
)
|
|||
|
|
return get_engine("RULE").solve(world, params, _next_id_factory(world))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_order_cancel_excludes_from_scheduling():
|
|||
|
|
"""取消订单后,该订单不再生成 PO,参与排产订单数从 7 降为 6。"""
|
|||
|
|
world = seed_world()
|
|||
|
|
target = world["salesOrders"][0]
|
|||
|
|
apply_order_action(world, _next_id_factory(world), "order.cancel", {"id": target["id"]})
|
|||
|
|
|
|||
|
|
result = _run(world)
|
|||
|
|
|
|||
|
|
assert result.orderCount == 6
|
|||
|
|
assert all(po["salesOrderId"] != target["id"] for po in world["productionOrders"])
|
|||
|
|
assert target["status"] == "CANCELLED"
|
|||
|
|
assert all(item["status"] == "CANCELLED" for item in target["items"])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_order_upsert_create_enters_scheduling():
|
|||
|
|
"""新增已确认订单后,下一版排产应把它纳入订单池。"""
|
|||
|
|
world = seed_world()
|
|||
|
|
payload = {
|
|||
|
|
"customerName": "试点客户",
|
|||
|
|
"customerLevel": "A",
|
|||
|
|
"deliveryDate": fmt_date(add_minutes(today0(), 9 * 24 * 60)),
|
|||
|
|
"priority": 2,
|
|||
|
|
"status": "CONFIRMED",
|
|||
|
|
"productId": 1,
|
|||
|
|
"quantity": 120,
|
|||
|
|
"isRush": False,
|
|||
|
|
"specialRequirements": "OR-01 golden",
|
|||
|
|
}
|
|||
|
|
applied = apply_order_action(world, _next_id_factory(world), "order.upsert", payload)
|
|||
|
|
order = applied["order"]
|
|||
|
|
|
|||
|
|
result = _run(world)
|
|||
|
|
|
|||
|
|
assert applied["created"] is True
|
|||
|
|
assert len(world["salesOrders"]) == 8
|
|||
|
|
assert result.orderCount == 8
|
|||
|
|
assert any(po["salesOrderId"] == order["id"] for po in world["productionOrders"])
|
|||
|
|
assert order["orderNo"].startswith("SO")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_order_confirmation_summary_validates_product():
|
|||
|
|
"""订单确认卡必须可解释影响范围,并拒绝非成品物料。"""
|
|||
|
|
world = seed_world()
|
|||
|
|
title, lines = confirmation_for_order_action(world, "order.upsert", {
|
|||
|
|
"customerName": "试点客户",
|
|||
|
|
"customerLevel": "VIP",
|
|||
|
|
"deliveryDate": fmt_date(add_minutes(today0(), 8 * 24 * 60)),
|
|||
|
|
"priority": 1,
|
|||
|
|
"status": "CONFIRMED",
|
|||
|
|
"productId": 1,
|
|||
|
|
"quantity": 80,
|
|||
|
|
})
|
|||
|
|
summary = "\n".join(lines)
|
|||
|
|
|
|||
|
|
assert "新增" in title
|
|||
|
|
assert "智能控制器A型" in summary
|
|||
|
|
assert "P2" in summary
|
|||
|
|
with pytest.raises(ValueError):
|
|||
|
|
confirmation_for_order_action(world, "order.upsert", {
|
|||
|
|
"customerName": "错误物料客户",
|
|||
|
|
"deliveryDate": fmt_date(add_minutes(today0(), 8 * 24 * 60)),
|
|||
|
|
"productId": 10,
|
|||
|
|
"quantity": 1,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_audit_event_exposes_at_and_ts_contract():
|
|||
|
|
"""新增审计事件应同时提供 at/ts,前端治理页与旧数据均可读取。"""
|
|||
|
|
world = seed_world()
|
|||
|
|
event = write_audit(
|
|||
|
|
world,
|
|||
|
|
_next_id_factory(world),
|
|||
|
|
actor="tester",
|
|||
|
|
category="GATE",
|
|||
|
|
action="order.upsert.stage",
|
|||
|
|
target={"type": "SALES_ORDER", "id": None},
|
|||
|
|
power="P2",
|
|||
|
|
rationale={"confirmId": "unit-test"},
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
assert event["at"] == event["ts"]
|
|||
|
|
assert verify_audit_chain(world["auditEvents"])["ok"] is True
|
|||
|
|
|