aps-agent/scripts/load_kangni.py

145 lines
5.7 KiB
Python
Raw Permalink Normal View History

"""加载康尼现场「完整生产路线」并跑柔性排产(替换 flex* 演示种子)。
用法:
python scripts/load_kangni.py
python scripts/load_kangni.py --route "<kangni-data-dir>/订单102285668_完整生产路线.xlsx"
python scripts/load_kangni.py --no-persist --no-siblings
python scripts/load_kangni.py --schedule-only # 已加载后只重排
"""
from __future__ import annotations
import argparse
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
from server.aps_domain.kangni_intake import ( # noqa: E402
DEFAULT_DATA_DIR,
DEFAULT_ROUTE_XLSX,
load_site_into_world,
)
from server.engines import PoolEngine # noqa: E402
from server.state.store import WorldStore # noqa: E402
from server.timeutil import add_minutes, fmt_date, today0 # noqa: E402
def _counter(store: WorldStore | None = None):
if store is not None:
return store.next_id
c: dict[str, int] = {}
def nid(kind: str) -> int:
c[kind] = c.get(kind, 0) + 1
return c[kind]
return nid
def main() -> int:
ap = argparse.ArgumentParser(description="康尼现场数据 → flex* → PoolEngine")
ap.add_argument("--route", default=str(DEFAULT_ROUTE_XLSX), help="完整生产路线 xlsx")
ap.add_argument("--data-dir", default=str(DEFAULT_DATA_DIR), help="源表目录(订单/BOM/工艺)")
ap.add_argument("--stations", type=int, default=4, help="合成装配工位数")
ap.add_argument("--with-siblings", action="store_true",
help="连带导入同源其它生产订单(默认只导入完整生产路线主单)")
ap.add_argument("--no-persist", action="store_true", help="不写 world.json(纯内存试跑)")
ap.add_argument("--no-schedule", action="store_true", help="只导入不排产")
ap.add_argument("--schedule-only", action="store_true", help="跳过导入,对当前 world 排产")
ap.add_argument("--sort", default="BOTTLENECK", choices=["ASC", "DESC", "BOTTLENECK"])
args = ap.parse_args()
include_siblings = bool(args.with_siblings)
store = WorldStore()
world = store.data
if not args.schedule_only:
meta = load_site_into_world(
world,
route_path=args.route,
data_dir=args.data_dir,
include_sibling_orders=include_siblings,
station_count=args.stations,
clear_all=True,
)
print("=" * 72)
print(" 演示数据已清空 · 现场主数据+订单+flex* 已写入")
print("=" * 72)
print(f" 主订单: {meta.get('primaryOrder')}")
print(f" 销售订单: {meta.get('salesOrders')} 物料: {meta.get('fixedMaterials')} "
f"工序库: {meta.get('fixedOperations')}")
print(f" 柔性订单: {meta.get('orderCount')} 工序种: {meta.get('operationCount')} "
f"BOM: {meta.get('bomCount')}")
print(f" 工厂: {[f.get('name') for f in world.get('factories', [])]}")
print(f" 来源: {meta.get('source')}")
for line in meta.get("inferences") or []:
print(f" · {line}")
if not args.no_persist:
store._reset_counters()
store.save()
print(f" 已落盘: {store.path}")
else:
print("跳过导入,使用当前 world 中的数据 …")
include_siblings = False
if args.no_schedule:
return 0
for k in ("flexScheduleVersions", "flexVirtualLines", "flexWorkOrders", "flexConflicts"):
world[k] = []
start = world.get("flexOrders", [{}])[0].get("dueDate")
# 排产起点:主单计划开始日或明天
plan = None
for o in world.get("flexOrders") or []:
plan = o.get("dueDate")
break
# 用今天相对更合理:现场交期在 2026-08,以计划窗起点
start_date = fmt_date(add_minutes(today0(), 24 * 60))
# 若订单有更早的合理窗,仍用明天起算(引擎默认)
_ = plan
result = PoolEngine().solve(
world, _counter(None if args.no_persist else store),
sort_mode=args.sort, start_date=start_date,
name=f"现场柔性排产 {args.sort}",
)
if not args.no_persist:
store.save()
print()
print("=" * 72)
print(f" 柔性排产完成 · {result.get('sortMode')} · {result.get('versionNo')}")
print("=" * 72)
print(f" 订单 {result.get('orderCount')} → VL {result.get('vlCount')} / WO {result.get('woCount')}")
print(f" 冲突 {result.get('conflictCount')} 总延期(h) {result.get('totalTardiness')} "
f"利用率 {result.get('avgUtilization')}")
wos = {(w["vlId"], w["seq"]): w for w in world.get("flexWorkOrders") or []}
for vl in world.get("flexVirtualLines") or []:
print(f"\n ▶ {vl['vlNo']} {vl['productCode']} ×{vl['quantity']} "
f"[{vl.get('plannedStart')} → {vl.get('plannedEnd')}] WBS={vl.get('wbs')}")
for a in vl.get("assignments") or []:
w = wos.get((vl["id"], a["seq"]))
print(f" seq={a['seq']:>4} {a['operationCode']:<14} "
f"{a['equipmentCode']:<10} {a['start']} → {a['end']}"
+ (" ★瓶颈" if w and w.get("isBottleneck") else ""))
conflicts = world.get("flexConflicts") or []
if conflicts:
print("\n 冲突摘要:")
for c in conflicts[:12]:
print(f" [{c.get('severity')}] {c.get('conflictType')}: {c.get('description')}")
if len(conflicts) > 12:
print(f" …共 {len(conflicts)} 条")
return 0
if __name__ == "__main__":
raise SystemExit(main())