"""加载康尼现场「完整生产路线」并跑柔性排产(替换 flex* 演示种子)。 用法: python scripts/load_kangni.py python scripts/load_kangni.py --route "D:/.../订单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())