2026-07-23 13:38:43 +08:00
|
|
|
|
"""加载康尼现场「完整生产路线」并跑柔性排产(替换 flex* 演示种子)。
|
|
|
|
|
|
|
|
|
|
|
|
用法:
|
|
|
|
|
|
python scripts/load_kangni.py
|
2026-09-08 00:07:26 +08:00
|
|
|
|
python scripts/load_kangni.py --route "<kangni-data-dir>/订单102285668_完整生产路线.xlsx"
|
2026-07-23 13:38:43 +08:00
|
|
|
|
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())
|