aps-agent/scripts/load_kangni.py

145 lines
5.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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