152 lines
5.2 KiB
Python
152 lines
5.2 KiB
Python
|
|
# ============================================================
|
|||
|
|
# OR-03 订单池 / 审核黄金测试
|
|||
|
|
# 固化:未批准不进排产;submit → approve 后可排;编辑已批→CHANGED
|
|||
|
|
# ============================================================
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
from server.aps_domain.orders import (
|
|||
|
|
apply_order_action,
|
|||
|
|
apply_order_submit,
|
|||
|
|
is_schedulable,
|
|||
|
|
pool_summary,
|
|||
|
|
)
|
|||
|
|
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):
|
|||
|
|
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, *, include_unapproved: bool = False):
|
|||
|
|
params = EngineParams(
|
|||
|
|
orderIds=[],
|
|||
|
|
engineType="RULE",
|
|||
|
|
strategyTemplate="COMPREHENSIVE",
|
|||
|
|
planningHorizonDays=14,
|
|||
|
|
startDate=fmt_date(add_minutes(today0(), 24 * 60)),
|
|||
|
|
includeUnapproved=include_unapproved,
|
|||
|
|
)
|
|||
|
|
return get_engine("RULE").solve(world, params, _next_id_factory(world))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_draft_excluded_from_schedule_run():
|
|||
|
|
"""DRAFT 订单默认不进入 schedule.run;includeUnapproved 时可试排。"""
|
|||
|
|
world = seed_world()
|
|||
|
|
applied = apply_order_action(world, _next_id_factory(world), "order.upsert", {
|
|||
|
|
"customerName": "待审客户",
|
|||
|
|
"customerLevel": "A",
|
|||
|
|
"deliveryDate": fmt_date(add_minutes(today0(), 9 * 24 * 60)),
|
|||
|
|
"priority": 2,
|
|||
|
|
"status": "DRAFT",
|
|||
|
|
"productId": 1,
|
|||
|
|
"quantity": 50,
|
|||
|
|
})
|
|||
|
|
draft = applied["order"]
|
|||
|
|
assert draft["status"] == "DRAFT"
|
|||
|
|
assert not is_schedulable(draft["status"])
|
|||
|
|
|
|||
|
|
result = _run(world)
|
|||
|
|
assert result.orderCount == 7
|
|||
|
|
assert all(po["salesOrderId"] != draft["id"] for po in world["productionOrders"])
|
|||
|
|
|
|||
|
|
result2 = _run(world, include_unapproved=True)
|
|||
|
|
assert result2.orderCount == 8
|
|||
|
|
assert any(po["salesOrderId"] == draft["id"] for po in world["productionOrders"])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_submit_approve_enters_scheduling():
|
|||
|
|
"""DRAFT → submit → approve 后纳入正式排产。"""
|
|||
|
|
world = seed_world()
|
|||
|
|
applied = apply_order_action(world, _next_id_factory(world), "order.upsert", {
|
|||
|
|
"customerName": "审核流客户",
|
|||
|
|
"customerLevel": "VIP",
|
|||
|
|
"deliveryDate": fmt_date(add_minutes(today0(), 10 * 24 * 60)),
|
|||
|
|
"priority": 1,
|
|||
|
|
"status": "DRAFT",
|
|||
|
|
"productId": 1,
|
|||
|
|
"quantity": 80,
|
|||
|
|
})
|
|||
|
|
order = applied["order"]
|
|||
|
|
|
|||
|
|
submitted = apply_order_submit(world, {"id": order["id"]})
|
|||
|
|
assert submitted["order"]["status"] == "SUBMITTED"
|
|||
|
|
assert _run(world).orderCount == 7
|
|||
|
|
|
|||
|
|
approved = apply_order_action(world, _next_id_factory(world), "order.approve", {
|
|||
|
|
"orderIds": [order["id"]],
|
|||
|
|
})
|
|||
|
|
assert approved["order"]["status"] == "APPROVED"
|
|||
|
|
assert approved["approvedCount"] == 1
|
|||
|
|
|
|||
|
|
result = _run(world)
|
|||
|
|
assert result.orderCount == 8
|
|||
|
|
assert any(po["salesOrderId"] == order["id"] for po in world["productionOrders"])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_edit_approved_becomes_changed():
|
|||
|
|
"""编辑已批准订单内容 → CHANGED,暂时退出可排池。"""
|
|||
|
|
world = seed_world()
|
|||
|
|
target = world["salesOrders"][0]
|
|||
|
|
assert target["status"] == "APPROVED"
|
|||
|
|
|
|||
|
|
apply_order_action(world, _next_id_factory(world), "order.upsert", {
|
|||
|
|
"id": target["id"],
|
|||
|
|
"customerName": target["customerName"],
|
|||
|
|
"customerLevel": target["customerLevel"],
|
|||
|
|
"deliveryDate": target["deliveryDate"],
|
|||
|
|
"priority": target["priority"],
|
|||
|
|
"productId": target["items"][0]["productId"],
|
|||
|
|
"quantity": target["items"][0]["quantity"] + 10,
|
|||
|
|
"status": "APPROVED",
|
|||
|
|
})
|
|||
|
|
assert target["status"] == "CHANGED"
|
|||
|
|
assert not is_schedulable(target["status"])
|
|||
|
|
assert pool_summary(world)["changed"] >= 1
|
|||
|
|
assert _run(world).orderCount == 6
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_batch_approve_pending():
|
|||
|
|
"""orderIds=pending 批量批准所有 SUBMITTED。"""
|
|||
|
|
world = seed_world()
|
|||
|
|
for name in ("批甲", "批乙"):
|
|||
|
|
o = apply_order_action(world, _next_id_factory(world), "order.upsert", {
|
|||
|
|
"customerName": name,
|
|||
|
|
"customerLevel": "B",
|
|||
|
|
"deliveryDate": fmt_date(add_minutes(today0(), 11 * 24 * 60)),
|
|||
|
|
"priority": 5,
|
|||
|
|
"status": "DRAFT",
|
|||
|
|
"productId": 2,
|
|||
|
|
"quantity": 40,
|
|||
|
|
})["order"]
|
|||
|
|
apply_order_submit(world, {"id": o["id"]})
|
|||
|
|
|
|||
|
|
sm = pool_summary(world)
|
|||
|
|
assert sm["pending"] == 2
|
|||
|
|
applied = apply_order_action(world, _next_id_factory(world), "order.approve", {
|
|||
|
|
"orderIds": "pending",
|
|||
|
|
})
|
|||
|
|
assert applied["approvedCount"] == 2
|
|||
|
|
assert pool_summary(world)["pending"] == 0
|
|||
|
|
assert _run(world).orderCount == 9
|