145 lines
5.7 KiB
Python
145 lines
5.7 KiB
Python
"""加载康尼现场「完整生产路线」并跑柔性排产(替换 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())
|