430 lines
20 KiB
Python
430 lines
20 KiB
Python
|
|
# ============================================================
|
|||
|
|
# 工单拖拽调程(moduleId: domain-adjust, 可重生 ✅)
|
|||
|
|
# EX-04:预览(沙盒/只读校验)+ 提交(新草稿版本,P2)
|
|||
|
|
# 本切片优先柔性轨;固定轨后续对称扩展。
|
|||
|
|
# ============================================================
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import copy
|
|||
|
|
from datetime import datetime, timedelta
|
|||
|
|
from typing import Any
|
|||
|
|
|
|||
|
|
from server.timeutil import add_minutes, fmt_date, fmt_dt, parse_dt
|
|||
|
|
|
|||
|
|
World = dict[str, Any]
|
|||
|
|
SNAP_MIN = 15
|
|||
|
|
|
|||
|
|
|
|||
|
|
def snap_dt(dt, snap: int = SNAP_MIN):
|
|||
|
|
"""吸附到 snap 分钟网格。"""
|
|||
|
|
total = dt.hour * 60 + dt.minute
|
|||
|
|
snapped = round(total / snap) * snap
|
|||
|
|
base = dt.replace(hour=0, minute=0, second=0, microsecond=0)
|
|||
|
|
return base + timedelta(minutes=snapped)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def preview_flex_adjust(world: World, wo_id: int, new_start: str) -> dict:
|
|||
|
|
"""预览柔性工单平移(P0):不改主干;校验设备占槽与工序先后。"""
|
|||
|
|
from server.state.seed import ensure_flex_seed
|
|||
|
|
ensure_flex_seed(world)
|
|||
|
|
versions = world.get("flexScheduleVersions", [])
|
|||
|
|
if not versions:
|
|||
|
|
raise ValueError("尚无柔性排产版本")
|
|||
|
|
latest_id = versions[-1]["id"]
|
|||
|
|
wos = [w for w in world.get("flexWorkOrders", []) if w.get("versionId") == latest_id]
|
|||
|
|
wo = next((w for w in wos if w["id"] == wo_id), None)
|
|||
|
|
if wo is None:
|
|||
|
|
raise ValueError(f"工单 id={wo_id} 不在最新版本")
|
|||
|
|
if wo.get("frozen"):
|
|||
|
|
return {"ok": False, "reasons": ["冻结工单不可拖拽"], "moves": [],
|
|||
|
|
"woId": wo_id, "newStart": None}
|
|||
|
|
|
|||
|
|
old_start = parse_dt(wo["plannedStartTime"])
|
|||
|
|
old_end = parse_dt(wo["plannedEndTime"])
|
|||
|
|
dur = (old_end - old_start).total_seconds() / 60
|
|||
|
|
target = snap_dt(parse_dt(new_start))
|
|||
|
|
delta = (target - old_start).total_seconds() / 60
|
|||
|
|
if abs(delta) < 1:
|
|||
|
|
return {"ok": True, "reasons": ["位置未变"], "moves": [],
|
|||
|
|
"woId": wo_id, "newStart": fmt_dt(old_start), "deltaMin": 0}
|
|||
|
|
|
|||
|
|
# 同虚拟产线:本工序及后续工序整体平移,保持工艺先后
|
|||
|
|
same_vl = sorted(
|
|||
|
|
[w for w in wos if w.get("vlId") == wo.get("vlId")],
|
|||
|
|
key=lambda w: w.get("seq") or 0)
|
|||
|
|
to_move = [w for w in same_vl if (w.get("seq") or 0) >= (wo.get("seq") or 0)]
|
|||
|
|
proposed = []
|
|||
|
|
for w in to_move:
|
|||
|
|
ns = add_minutes(parse_dt(w["plannedStartTime"]), delta)
|
|||
|
|
ne = add_minutes(parse_dt(w["plannedEndTime"]), delta)
|
|||
|
|
proposed.append({
|
|||
|
|
"woId": w["id"], "orderNo": w.get("flexOrderNo"), "seq": w.get("seq"),
|
|||
|
|
"equipmentId": w["equipmentId"], "equipmentCode": w.get("equipmentCode"),
|
|||
|
|
"oldStart": w["plannedStartTime"], "oldEnd": w["plannedEndTime"],
|
|||
|
|
"newStart": fmt_dt(ns), "newEnd": fmt_dt(ne),
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
reasons: list[str] = []
|
|||
|
|
# 设备占槽:与未移动工单、以及本批内其他设备冲突
|
|||
|
|
moved_ids = {p["woId"] for p in proposed}
|
|||
|
|
for p in proposed:
|
|||
|
|
ns, ne = parse_dt(p["newStart"]), parse_dt(p["newEnd"])
|
|||
|
|
for other in wos:
|
|||
|
|
if other["id"] in moved_ids:
|
|||
|
|
# 同批内不同设备可并行;同设备则互查
|
|||
|
|
op = next((x for x in proposed if x["woId"] == other["id"]), None)
|
|||
|
|
if op and op["equipmentId"] == p["equipmentId"] and op["woId"] != p["woId"]:
|
|||
|
|
os, oe = parse_dt(op["newStart"]), parse_dt(op["newEnd"])
|
|||
|
|
if ns < oe and ne > os:
|
|||
|
|
reasons.append(
|
|||
|
|
f"同设备冲突:{p['orderNo']}#{p['seq']} 与 "
|
|||
|
|
f"{op['orderNo']}#{op['seq']} @ {p['equipmentCode']}")
|
|||
|
|
continue
|
|||
|
|
if other["equipmentId"] != p["equipmentId"]:
|
|||
|
|
continue
|
|||
|
|
os, oe = parse_dt(other["plannedStartTime"]), parse_dt(other["plannedEndTime"])
|
|||
|
|
if ns < oe and ne > os:
|
|||
|
|
reasons.append(
|
|||
|
|
f"设备 {p['equipmentCode']} 时段被 {other.get('flexOrderNo')} 占用"
|
|||
|
|
f"({other['plannedStartTime']}~{other['plannedEndTime']})")
|
|||
|
|
|
|||
|
|
# 与前序工序:移动后仍须 ≥ 前序结束(前序若也在 to_move 则用新时间)
|
|||
|
|
by_id = {p["woId"]: p for p in proposed}
|
|||
|
|
for p in proposed:
|
|||
|
|
prev = next((w for w in same_vl
|
|||
|
|
if (w.get("seq") or 0) == (p["seq"] or 0) - 1), None)
|
|||
|
|
if not prev:
|
|||
|
|
continue
|
|||
|
|
if prev["id"] in by_id:
|
|||
|
|
prev_end = parse_dt(by_id[prev["id"]]["newEnd"])
|
|||
|
|
else:
|
|||
|
|
prev_end = parse_dt(prev["plannedEndTime"])
|
|||
|
|
if parse_dt(p["newStart"]) < prev_end:
|
|||
|
|
reasons.append(f"{p['orderNo']} 工序#{p['seq']} 早于前序完工 {fmt_dt(prev_end)}")
|
|||
|
|
|
|||
|
|
# 去重
|
|||
|
|
uniq = list(dict.fromkeys(reasons))
|
|||
|
|
return {
|
|||
|
|
"ok": len(uniq) == 0,
|
|||
|
|
"reasons": uniq,
|
|||
|
|
"moves": proposed,
|
|||
|
|
"woId": wo_id,
|
|||
|
|
"newStart": fmt_dt(target),
|
|||
|
|
"deltaMin": delta,
|
|||
|
|
"durationMin": dur,
|
|||
|
|
"versionId": latest_id,
|
|||
|
|
"versionNo": versions[-1].get("versionNo"),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def stage_flex_adjust(store, wo_id: int, new_start: str, session_id: str, actor: str) -> dict:
|
|||
|
|
"""暂存拖拽提交为 P2 确认卡。"""
|
|||
|
|
from server.agent_core import harness
|
|||
|
|
from server.agent_core.audit import write_audit
|
|||
|
|
preview = preview_flex_adjust(store.data, wo_id, new_start)
|
|||
|
|
if not preview["ok"]:
|
|||
|
|
raise ValueError(";".join(preview["reasons"]) or "调程不可行")
|
|||
|
|
if not preview["moves"]:
|
|||
|
|
return {"message": "位置未变,无需提交。", "preview": preview}
|
|||
|
|
lines = [
|
|||
|
|
f"工单 #{wo_id} 平移 {preview['deltaMin']:.0f} 分钟 → {preview['newStart']}",
|
|||
|
|
f"联动同产线后续工序共 {len(preview['moves'])} 条",
|
|||
|
|
"将生成新草稿版本(不改历史版本)",
|
|||
|
|
]
|
|||
|
|
for m in preview["moves"][:4]:
|
|||
|
|
lines.append(f" · {m['orderNo']}#{m['seq']} {m['oldStart']} → {m['newStart']}")
|
|||
|
|
block = harness.stage_confirmation(
|
|||
|
|
session_id, "flex.adjust.commit",
|
|||
|
|
{"woId": wo_id, "newStart": preview["newStart"], "moves": preview["moves"]},
|
|||
|
|
title="甘特调程确认", summary_lines=lines)
|
|||
|
|
write_audit(store.data, store.next_id, actor=actor, category="GATE",
|
|||
|
|
action="flex.adjust.stage", target={"type": "FLEX_WO", "id": wo_id},
|
|||
|
|
power="P2", rationale={"confirmId": block.props["confirmId"],
|
|||
|
|
"deltaMin": preview["deltaMin"]})
|
|||
|
|
store.save()
|
|||
|
|
return {"message": "调程已进入 P2 确认队列。", "block": block.model_dump(),
|
|||
|
|
"preview": preview}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def commit_flex_adjust(store, wo_id: int, new_start: str, actor: str = "web") -> dict:
|
|||
|
|
"""执行调程:复制最新版本为新草稿并写入平移后时间。"""
|
|||
|
|
from server.agent_core.audit import write_audit
|
|||
|
|
from server.state.seed import ensure_flex_seed
|
|||
|
|
ensure_flex_seed(store.data)
|
|||
|
|
preview = preview_flex_adjust(store.data, wo_id, new_start)
|
|||
|
|
if not preview["ok"]:
|
|||
|
|
raise ValueError(";".join(preview["reasons"]) or "调程不可行")
|
|||
|
|
if not preview["moves"]:
|
|||
|
|
return {"message": "位置未变", "versionId": preview["versionId"]}
|
|||
|
|
|
|||
|
|
world = store.data
|
|||
|
|
versions = world["flexScheduleVersions"]
|
|||
|
|
latest = versions[-1]
|
|||
|
|
old_vid = latest["id"]
|
|||
|
|
new_vid = store.next_id("flexScheduleVersion")
|
|||
|
|
now = datetime.now()
|
|||
|
|
ver_no = "FV" + fmt_date(now).replace("-", "") + f"-{len(versions) + 1:03d}"
|
|||
|
|
new_ver = {
|
|||
|
|
**{k: v for k, v in latest.items()
|
|||
|
|
if k not in ("id", "versionNo", "versionName", "createdAt", "createdBy",
|
|||
|
|
"parentVersionId", "adjustOf")},
|
|||
|
|
"id": new_vid, "versionNo": ver_no,
|
|||
|
|
"versionName": f"拖拽调程 #{wo_id}",
|
|||
|
|
"status": "DRAFT",
|
|||
|
|
"parentVersionId": old_vid,
|
|||
|
|
"adjustOf": wo_id,
|
|||
|
|
"createdBy": actor, "createdAt": fmt_dt(now),
|
|||
|
|
"conflictCount": 0,
|
|||
|
|
}
|
|||
|
|
# 复制 VL
|
|||
|
|
vl_map: dict[int, int] = {}
|
|||
|
|
for vl in world.get("flexVirtualLines", []):
|
|||
|
|
if vl.get("versionId") != old_vid:
|
|||
|
|
continue
|
|||
|
|
nid = store.next_id("flexVirtualLine")
|
|||
|
|
vl_map[vl["id"]] = nid
|
|||
|
|
world["flexVirtualLines"].append({**copy.deepcopy(vl), "id": nid, "versionId": new_vid})
|
|||
|
|
move_by_id = {m["woId"]: m for m in preview["moves"]}
|
|||
|
|
for wo in world.get("flexWorkOrders", []):
|
|||
|
|
if wo.get("versionId") != old_vid:
|
|||
|
|
continue
|
|||
|
|
nw = copy.deepcopy(wo)
|
|||
|
|
nw["id"] = store.next_id("flexWorkOrder")
|
|||
|
|
nw["versionId"] = new_vid
|
|||
|
|
nw["vlId"] = vl_map.get(wo["vlId"], wo["vlId"])
|
|||
|
|
mv = move_by_id.get(wo["id"])
|
|||
|
|
if mv:
|
|||
|
|
nw["plannedStartTime"] = mv["newStart"]
|
|||
|
|
nw["plannedEndTime"] = mv["newEnd"]
|
|||
|
|
nw["adjustedFrom"] = mv["oldStart"]
|
|||
|
|
world["flexWorkOrders"].append(nw)
|
|||
|
|
# 刷新 VL 起止
|
|||
|
|
for old_vl_id, new_vl_id in vl_map.items():
|
|||
|
|
vl_wos = [w for w in world["flexWorkOrders"] if w.get("vlId") == new_vl_id]
|
|||
|
|
if not vl_wos:
|
|||
|
|
continue
|
|||
|
|
vl = next(v for v in world["flexVirtualLines"] if v["id"] == new_vl_id)
|
|||
|
|
vl["plannedStart"] = min(w["plannedStartTime"] for w in vl_wos)
|
|||
|
|
vl["plannedEnd"] = max(w["plannedEndTime"] for w in vl_wos)
|
|||
|
|
new_ver["vlCount"] = len(vl_map)
|
|||
|
|
new_ver["woCount"] = sum(1 for w in world["flexWorkOrders"] if w["versionId"] == new_vid)
|
|||
|
|
new_ver["conflictCount"] = 0
|
|||
|
|
world["flexScheduleVersions"].append(new_ver)
|
|||
|
|
write_audit(world, store.next_id, actor=actor, category="WORLD_WRITE",
|
|||
|
|
action="flex.adjust.commit", target={"type": "FLEX_VERSION", "id": new_vid},
|
|||
|
|
power="P2", rationale={"woId": wo_id, "newStart": new_start,
|
|||
|
|
"moved": len(preview["moves"])})
|
|||
|
|
store.save()
|
|||
|
|
return {"message": f"调程完成 ✅ 新版本 {ver_no}(联动 {len(preview['moves'])} 条工单)。",
|
|||
|
|
"versionId": new_vid, "versionNo": ver_no, "preview": preview}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------- 固定轨(产线/工位甘特) ----------------
|
|||
|
|
|
|||
|
|
def _latest_fixed_wos(world: World) -> tuple[dict | None, list[dict], list[dict]]:
|
|||
|
|
versions = world.get("scheduleVersions", [])
|
|||
|
|
if not versions:
|
|||
|
|
return None, [], []
|
|||
|
|
latest = versions[-1]
|
|||
|
|
pos = [p for p in world.get("productionOrders", [])
|
|||
|
|
if p.get("schedulingVersionId") == latest["id"]]
|
|||
|
|
po_ids = {p["id"] for p in pos}
|
|||
|
|
wos = [w for w in world.get("workOrders", []) if w.get("productionOrderId") in po_ids]
|
|||
|
|
return latest, pos, wos
|
|||
|
|
|
|||
|
|
|
|||
|
|
def preview_fixed_adjust(world: World, wo_id: int, new_start: str) -> dict:
|
|||
|
|
"""预览固定轨工单平移(P0):工位占槽 + 同 PO 工序先后 + 维保窗口。"""
|
|||
|
|
latest, _pos, wos = _latest_fixed_wos(world)
|
|||
|
|
if latest is None:
|
|||
|
|
raise ValueError("尚无固定轨排产版本")
|
|||
|
|
wo = next((w for w in wos if w["id"] == wo_id), None)
|
|||
|
|
if wo is None:
|
|||
|
|
raise ValueError(f"工单 id={wo_id} 不在最新版本")
|
|||
|
|
if wo.get("isFrozen"):
|
|||
|
|
return {"ok": False, "reasons": ["冻结工单不可拖拽"], "moves": [],
|
|||
|
|
"woId": wo_id, "newStart": None}
|
|||
|
|
|
|||
|
|
old_start = parse_dt(wo["plannedStartTime"])
|
|||
|
|
target = snap_dt(parse_dt(new_start))
|
|||
|
|
delta = (target - old_start).total_seconds() / 60
|
|||
|
|
if abs(delta) < 1:
|
|||
|
|
return {"ok": True, "reasons": ["位置未变"], "moves": [],
|
|||
|
|
"woId": wo_id, "newStart": fmt_dt(old_start), "deltaMin": 0}
|
|||
|
|
|
|||
|
|
same_po = sorted(
|
|||
|
|
[w for w in wos if w.get("productionOrderId") == wo.get("productionOrderId")],
|
|||
|
|
key=lambda w: w.get("sequenceNo") or 0)
|
|||
|
|
to_move = [w for w in same_po if (w.get("sequenceNo") or 0) >= (wo.get("sequenceNo") or 0)]
|
|||
|
|
proposed = []
|
|||
|
|
for w in to_move:
|
|||
|
|
ns = add_minutes(parse_dt(w["plannedStartTime"]), delta)
|
|||
|
|
ne = add_minutes(parse_dt(w["plannedEndTime"]), delta)
|
|||
|
|
proposed.append({
|
|||
|
|
"woId": w["id"], "orderNo": w.get("orderNo"), "seq": w.get("sequenceNo"),
|
|||
|
|
"workstationId": w["workstationId"], "workstationName": w.get("workstationName"),
|
|||
|
|
"oldStart": w["plannedStartTime"], "oldEnd": w["plannedEndTime"],
|
|||
|
|
"newStart": fmt_dt(ns), "newEnd": fmt_dt(ne),
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
reasons: list[str] = []
|
|||
|
|
moved_ids = {p["woId"] for p in proposed}
|
|||
|
|
for p in proposed:
|
|||
|
|
ns, ne = parse_dt(p["newStart"]), parse_dt(p["newEnd"])
|
|||
|
|
for other in wos:
|
|||
|
|
if other["id"] in moved_ids:
|
|||
|
|
op = next((x for x in proposed if x["woId"] == other["id"]), None)
|
|||
|
|
if op and op["workstationId"] == p["workstationId"] and op["woId"] != p["woId"]:
|
|||
|
|
os, oe = parse_dt(op["newStart"]), parse_dt(op["newEnd"])
|
|||
|
|
if ns < oe and ne > os:
|
|||
|
|
reasons.append(
|
|||
|
|
f"同工位冲突:{p['orderNo']} 与 {op['orderNo']} @ {p['workstationName']}")
|
|||
|
|
continue
|
|||
|
|
if other["workstationId"] != p["workstationId"]:
|
|||
|
|
continue
|
|||
|
|
os, oe = parse_dt(other["plannedStartTime"]), parse_dt(other["plannedEndTime"])
|
|||
|
|
if ns < oe and ne > os:
|
|||
|
|
reasons.append(
|
|||
|
|
f"工位 {p['workstationName']} 被 {other.get('orderNo')} 占用"
|
|||
|
|
f"({other['plannedStartTime']}~{other['plannedEndTime']})")
|
|||
|
|
|
|||
|
|
by_id = {p["woId"]: p for p in proposed}
|
|||
|
|
for p in proposed:
|
|||
|
|
prev = next((w for w in same_po
|
|||
|
|
if (w.get("sequenceNo") or 0) == (p["seq"] or 0) - 1), None)
|
|||
|
|
if not prev:
|
|||
|
|
continue
|
|||
|
|
if prev["id"] in by_id:
|
|||
|
|
prev_end = parse_dt(by_id[prev["id"]]["newEnd"])
|
|||
|
|
else:
|
|||
|
|
prev_end = parse_dt(prev["plannedEndTime"])
|
|||
|
|
if parse_dt(p["newStart"]) < prev_end:
|
|||
|
|
reasons.append(f"{p['orderNo']} 早于前序完工 {fmt_dt(prev_end)}")
|
|||
|
|
|
|||
|
|
# 维保窗口:设备.workstationId ↔ 工单工位
|
|||
|
|
eq_by_ws = {e["workstationId"]: e for e in world.get("equipment", []) if e.get("workstationId")}
|
|||
|
|
for p in proposed:
|
|||
|
|
eq = eq_by_ws.get(p["workstationId"])
|
|||
|
|
if not eq:
|
|||
|
|
continue
|
|||
|
|
ns, ne = parse_dt(p["newStart"]), parse_dt(p["newEnd"])
|
|||
|
|
for m in world.get("maintenance", []):
|
|||
|
|
if m.get("status", "PLANNED") not in ("PLANNED", "IN_PROGRESS"):
|
|||
|
|
continue
|
|||
|
|
if m.get("equipmentId") != eq["id"]:
|
|||
|
|
continue
|
|||
|
|
ms, me = parse_dt(m["plannedStart"]), parse_dt(m["plannedEnd"])
|
|||
|
|
if ns < me and ne > ms:
|
|||
|
|
reasons.append(f"{p['orderNo']} 落入维保窗口({eq.get('name') or eq['id']})")
|
|||
|
|
|
|||
|
|
uniq = list(dict.fromkeys(reasons))
|
|||
|
|
return {
|
|||
|
|
"ok": len(uniq) == 0, "reasons": uniq, "moves": proposed,
|
|||
|
|
"woId": wo_id, "newStart": fmt_dt(target), "deltaMin": delta,
|
|||
|
|
"versionId": latest["id"], "versionNo": latest.get("versionNo"),
|
|||
|
|
"track": "fixed",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def stage_fixed_adjust(store, wo_id: int, new_start: str, session_id: str, actor: str) -> dict:
|
|||
|
|
from server.agent_core import harness
|
|||
|
|
from server.agent_core.audit import write_audit
|
|||
|
|
preview = preview_fixed_adjust(store.data, wo_id, new_start)
|
|||
|
|
if not preview["ok"]:
|
|||
|
|
raise ValueError(";".join(preview["reasons"]) or "调程不可行")
|
|||
|
|
if not preview["moves"]:
|
|||
|
|
return {"message": "位置未变,无需提交。", "preview": preview}
|
|||
|
|
lines = [
|
|||
|
|
f"工单 #{wo_id} 平移 {preview['deltaMin']:.0f} 分钟 → {preview['newStart']}",
|
|||
|
|
f"联动同 PO 后续工序共 {len(preview['moves'])} 条",
|
|||
|
|
"将生成新草稿版本(不改历史版本)",
|
|||
|
|
]
|
|||
|
|
for m in preview["moves"][:4]:
|
|||
|
|
lines.append(f" · {m['orderNo']} {m['oldStart']} → {m['newStart']}")
|
|||
|
|
block = harness.stage_confirmation(
|
|||
|
|
session_id, "schedule.adjust.commit",
|
|||
|
|
{"woId": wo_id, "newStart": preview["newStart"], "moves": preview["moves"]},
|
|||
|
|
title="固定甘特调程确认", summary_lines=lines)
|
|||
|
|
write_audit(store.data, store.next_id, actor=actor, category="GATE",
|
|||
|
|
action="schedule.adjust.stage", target={"type": "WORK_ORDER", "id": wo_id},
|
|||
|
|
power="P2", rationale={"confirmId": block.props["confirmId"],
|
|||
|
|
"deltaMin": preview["deltaMin"]})
|
|||
|
|
store.save()
|
|||
|
|
return {"message": "调程已进入 P2 确认队列。", "block": block.model_dump(),
|
|||
|
|
"preview": preview}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def commit_fixed_adjust(store, wo_id: int, new_start: str, actor: str = "web") -> dict:
|
|||
|
|
"""执行固定轨调程:复制最新版本 PO/WO 为新草稿。"""
|
|||
|
|
from server.agent_core.audit import write_audit
|
|||
|
|
preview = preview_fixed_adjust(store.data, wo_id, new_start)
|
|||
|
|
if not preview["ok"]:
|
|||
|
|
raise ValueError(";".join(preview["reasons"]) or "调程不可行")
|
|||
|
|
if not preview["moves"]:
|
|||
|
|
return {"message": "位置未变", "versionId": preview["versionId"]}
|
|||
|
|
|
|||
|
|
world = store.data
|
|||
|
|
versions = world["scheduleVersions"]
|
|||
|
|
latest = versions[-1]
|
|||
|
|
old_vid = latest["id"]
|
|||
|
|
new_vid = store.next_id("scheduleVersion")
|
|||
|
|
now = datetime.now()
|
|||
|
|
ver_no = "V" + fmt_date(now).replace("-", "") + f"-{len(versions) + 1:03d}"
|
|||
|
|
new_ver = {
|
|||
|
|
**{k: v for k, v in latest.items()
|
|||
|
|
if k not in ("id", "versionNo", "versionName", "createdAt", "createdBy",
|
|||
|
|
"publishedAt", "parentVersionId", "adjustOf")},
|
|||
|
|
"id": new_vid, "versionNo": ver_no,
|
|||
|
|
"versionName": f"拖拽调程 #{wo_id}",
|
|||
|
|
"status": "DRAFT", "parentVersionId": old_vid, "adjustOf": wo_id,
|
|||
|
|
"createdBy": actor, "createdAt": fmt_dt(now), "publishedAt": None,
|
|||
|
|
"conflictCount": 0, "resolvedCount": 0,
|
|||
|
|
}
|
|||
|
|
old_pos = [p for p in world.get("productionOrders", [])
|
|||
|
|
if p.get("schedulingVersionId") == old_vid]
|
|||
|
|
po_map: dict[int, int] = {}
|
|||
|
|
for po in old_pos:
|
|||
|
|
nid = store.next_id("productionOrder")
|
|||
|
|
po_map[po["id"]] = nid
|
|||
|
|
np = copy.deepcopy(po)
|
|||
|
|
np["id"] = nid
|
|||
|
|
np["schedulingVersionId"] = new_vid
|
|||
|
|
world["productionOrders"].append(np)
|
|||
|
|
|
|||
|
|
move_by_id = {m["woId"]: m for m in preview["moves"]}
|
|||
|
|
wo_count = 0
|
|||
|
|
for wo in world.get("workOrders", []):
|
|||
|
|
if wo.get("productionOrderId") not in po_map:
|
|||
|
|
continue
|
|||
|
|
nw = copy.deepcopy(wo)
|
|||
|
|
nw["id"] = store.next_id("workOrder")
|
|||
|
|
nw["productionOrderId"] = po_map[wo["productionOrderId"]]
|
|||
|
|
mv = move_by_id.get(wo["id"])
|
|||
|
|
if mv:
|
|||
|
|
nw["plannedStartTime"] = mv["newStart"]
|
|||
|
|
nw["plannedEndTime"] = mv["newEnd"]
|
|||
|
|
nw["adjustedFrom"] = mv["oldStart"]
|
|||
|
|
world["workOrders"].append(nw)
|
|||
|
|
wo_count += 1
|
|||
|
|
|
|||
|
|
# 回填 PO 起止
|
|||
|
|
for old_po_id, new_po_id in po_map.items():
|
|||
|
|
po_wos = [w for w in world["workOrders"] if w["productionOrderId"] == new_po_id]
|
|||
|
|
if not po_wos:
|
|||
|
|
continue
|
|||
|
|
po = next(p for p in world["productionOrders"] if p["id"] == new_po_id)
|
|||
|
|
po["plannedStartDate"] = min(w["plannedStartTime"] for w in po_wos)
|
|||
|
|
po["plannedEndDate"] = max(w["plannedEndTime"] for w in po_wos)
|
|||
|
|
|
|||
|
|
new_ver["poCount"] = len(po_map)
|
|||
|
|
new_ver["woCount"] = wo_count
|
|||
|
|
world["scheduleVersions"].append(new_ver)
|
|||
|
|
write_audit(world, store.next_id, actor=actor, category="WORLD_WRITE",
|
|||
|
|
action="schedule.adjust.commit",
|
|||
|
|
target={"type": "SCHEDULE_VERSION", "id": new_vid},
|
|||
|
|
power="P2", rationale={"woId": wo_id, "newStart": new_start,
|
|||
|
|
"moved": len(preview["moves"])})
|
|||
|
|
store.save()
|
|||
|
|
return {"message": f"调程完成 ✅ 新版本 {ver_no}(联动 {len(preview['moves'])} 条工单)。",
|
|||
|
|
"versionId": new_vid, "versionNo": ver_no, "preview": preview}
|