128 lines
5.2 KiB
Python
128 lines
5.2 KiB
Python
|
|
# ============================================================
|
|||
|
|
# OR-02 排产参数 / 客户等级权重黄金测试
|
|||
|
|
# ============================================================
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
from server.agent_core.intent import parse_fast
|
|||
|
|
from server.aps_domain.params import (
|
|||
|
|
apply_params_update, confirmation_for_params_update, get_schedule_params,
|
|||
|
|
)
|
|||
|
|
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, parse_dt, today0
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _next_id_factory():
|
|||
|
|
counters: dict[str, int] = {}
|
|||
|
|
|
|||
|
|
def next_id(kind: str) -> int:
|
|||
|
|
counters[kind] = counters.get(kind, 0) + 1
|
|||
|
|
return counters[kind]
|
|||
|
|
|
|||
|
|
return next_id
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _two_orders_world(vip_due_offset=10, c_due_offset=5):
|
|||
|
|
"""构造仅两单、同优先级的世界,便于观察等级权重对开工顺序的影响。"""
|
|||
|
|
world = seed_world()
|
|||
|
|
base = today0()
|
|||
|
|
due_vip = fmt_date(add_minutes(base, vip_due_offset * 24 * 60))
|
|||
|
|
due_c = fmt_date(add_minutes(base, c_due_offset * 24 * 60))
|
|||
|
|
order_date = fmt_date(base)
|
|||
|
|
product = next(m for m in world["materials"] if m["type"] == "FINISHED_PRODUCT")
|
|||
|
|
|
|||
|
|
def make(oid, no, level, due):
|
|||
|
|
return {
|
|||
|
|
"id": oid, "orderNo": no, "customerId": f"C{oid}", "customerName": f"客户{level}",
|
|||
|
|
"customerLevel": level, "orderDate": order_date, "deliveryDate": due,
|
|||
|
|
"priority": 5, "manualPriority": None, "status": "CONFIRMED",
|
|||
|
|
"source": "MANUAL", "specialRequirements": "", "totalAmount": 1000,
|
|||
|
|
"isRush": False, "rushStrategy": None, "changes": [],
|
|||
|
|
"createdBy": "test", "createdAt": order_date + " 09:00", "updatedAt": order_date + " 09:00",
|
|||
|
|
"items": [{
|
|||
|
|
"id": oid * 10, "orderId": oid, "lineNo": 1,
|
|||
|
|
"productId": product["id"], "productName": product["name"], "productCode": product["code"],
|
|||
|
|
"quantity": 100, "unit": "件", "bomVersion": "V1.0", "routingVersion": "V1.0",
|
|||
|
|
"status": "PENDING", "note": "",
|
|||
|
|
}],
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
world["salesOrders"] = [
|
|||
|
|
make(1, "SO-VIP-1", "VIP", due_vip),
|
|||
|
|
make(2, "SO-C-1", "C", due_c),
|
|||
|
|
]
|
|||
|
|
world["scheduleVersions"] = []
|
|||
|
|
world["productionOrders"] = []
|
|||
|
|
world["workOrders"] = []
|
|||
|
|
world["conflicts"] = []
|
|||
|
|
return world
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _run(world, strategy="COMPREHENSIVE"):
|
|||
|
|
start = fmt_date(add_minutes(today0(), 24 * 60))
|
|||
|
|
params = EngineParams(orderIds=[], engineType="RULE", strategyTemplate=strategy,
|
|||
|
|
planningHorizonDays=14, startDate=start)
|
|||
|
|
return get_engine("RULE").solve(world, params, _next_id_factory())
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _earliest_level(world) -> str:
|
|||
|
|
pos = world["productionOrders"]
|
|||
|
|
earliest = min(pos, key=lambda p: parse_dt(p["plannedStartDate"]))
|
|||
|
|
so = next(s for s in world["salesOrders"] if s["id"] == earliest["salesOrderId"])
|
|||
|
|
return so["customerLevel"]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_default_params_have_level_weights():
|
|||
|
|
p = get_schedule_params(seed_world())
|
|||
|
|
assert p["customerLevelWeights"]["VIP"] == 3.0
|
|||
|
|
assert p["customerLevelWeights"]["C"] == 1.0
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_vip_weight_pulls_order_earlier():
|
|||
|
|
"""提高 VIP 权重后,即便 VIP 交期更晚,综合策略下 VIP 也应更早开工。"""
|
|||
|
|
# 默认:VIP=3,C=1;VIP 交期更晚 → 仍可能 VIP 先(等级键优先)
|
|||
|
|
w1 = _two_orders_world(vip_due_offset=12, c_due_offset=5)
|
|||
|
|
_run(w1)
|
|||
|
|
assert _earliest_level(w1) == "VIP"
|
|||
|
|
|
|||
|
|
# 把 VIP 压到 1、C 提到 10 → C 应先开工
|
|||
|
|
w2 = _two_orders_world(vip_due_offset=12, c_due_offset=5)
|
|||
|
|
apply_params_update(w2, {"customerLevelWeights": {"VIP": 1, "C": 10}})
|
|||
|
|
_run(w2)
|
|||
|
|
assert _earliest_level(w2) == "C"
|
|||
|
|
|
|||
|
|
# 再把 VIP 提到 20 → VIP 又应先开工
|
|||
|
|
w3 = _two_orders_world(vip_due_offset=12, c_due_offset=5)
|
|||
|
|
apply_params_update(w3, {"customerLevelWeights": {"VIP": 20, "C": 1}})
|
|||
|
|
_run(w3)
|
|||
|
|
assert _earliest_level(w3) == "VIP"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_delivery_first_still_edd():
|
|||
|
|
"""交期优先仍以交期为主键(G5 不变式)。"""
|
|||
|
|
world = _two_orders_world(vip_due_offset=12, c_due_offset=5)
|
|||
|
|
apply_params_update(world, {"customerLevelWeights": {"VIP": 20, "C": 1}})
|
|||
|
|
_run(world, strategy="DELIVERY_FIRST")
|
|||
|
|
assert _earliest_level(world) == "C" # C 交期更早
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_params_update_roundtrip_and_confirm_text():
|
|||
|
|
world = seed_world()
|
|||
|
|
title, lines = confirmation_for_params_update(
|
|||
|
|
world, {"customerLevelWeights": {"VIP": 9}})
|
|||
|
|
assert "排产参数" in title
|
|||
|
|
assert any("VIP" in x for x in lines)
|
|||
|
|
applied = apply_params_update(world, {"customerLevelWeights": {"VIP": 9}})
|
|||
|
|
assert applied["after"]["customerLevelWeights"]["VIP"] == 9.0
|
|||
|
|
apply_params_update(world, {"resetDefaults": True})
|
|||
|
|
assert get_schedule_params(world)["customerLevelWeights"]["VIP"] == 3.0
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_intent_params():
|
|||
|
|
world = seed_world()
|
|||
|
|
assert parse_fast("查看排产参数", world).intent == "params.query"
|
|||
|
|
assert parse_fast("提高VIP权重到10", world).intent == "params.update"
|
|||
|
|
assert parse_fast("提高VIP权重到10", world).params.get("vipWeight") == 10.0
|
|||
|
|
assert parse_fast("恢复默认排产参数", world).params.get("resetDefaults") is True
|