124 lines
4.4 KiB
Python
124 lines
4.4 KiB
Python
# ============================================================
|
||
# MES 集成桩(moduleId: integ-mes-stub, 可重生 ✅)
|
||
# EX-05/09 最小切片:进程内 Mock MES,无真实车间接口。
|
||
# 下发:创建外部工单(幂等);报工:进度/完工回写镜像。
|
||
# ============================================================
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
_MIRROR_PATH = Path(__file__).resolve().parents[1] / "data" / "mes_mirror.json"
|
||
|
||
|
||
def default_mirror() -> dict[str, Any]:
|
||
return {
|
||
"system": "MES-MOCK",
|
||
"plant": "CNWH",
|
||
"updatedAt": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||
"workOrders": [], # 外部工单
|
||
"reports": [], # 报工流水
|
||
"idempotency": {}, # key → externalWoId
|
||
}
|
||
|
||
|
||
class MockMesClient:
|
||
"""进程内 Mock MES:读写本地镜像,模拟下发与报工。"""
|
||
|
||
def __init__(self, path: Path | None = None):
|
||
self.path = path or _MIRROR_PATH
|
||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||
if not self.path.exists():
|
||
self._save(default_mirror())
|
||
|
||
def _load(self) -> dict:
|
||
try:
|
||
return json.loads(self.path.read_text(encoding="utf-8"))
|
||
except (OSError, json.JSONDecodeError):
|
||
data = default_mirror()
|
||
self._save(data)
|
||
return data
|
||
|
||
def _save(self, data: dict) -> None:
|
||
data["updatedAt"] = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||
self.path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
|
||
def status(self) -> dict:
|
||
m = self._load()
|
||
wos = m.get("workOrders") or []
|
||
return {
|
||
"connected": True,
|
||
"mode": "stub",
|
||
"system": m.get("system"),
|
||
"plant": m.get("plant"),
|
||
"updatedAt": m.get("updatedAt"),
|
||
"woCount": len(wos),
|
||
"openCount": sum(1 for w in wos if w.get("status") != "COMPLETED"),
|
||
"reportCount": len(m.get("reports") or []),
|
||
}
|
||
|
||
def create_work_order(self, payload: dict, idem_key: str) -> dict:
|
||
"""幂等下发:相同 idem_key 返回原外部工单。"""
|
||
m = self._load()
|
||
existing = (m.get("idempotency") or {}).get(idem_key)
|
||
if existing:
|
||
wo = next((w for w in m.get("workOrders", []) if w["id"] == existing), None)
|
||
return {"duplicate": True, "externalWo": wo or {"id": existing, "idemKey": idem_key}}
|
||
eid = f"MES-WO-{len(m.get('workOrders') or []) + 1:04d}"
|
||
external = {
|
||
"id": eid, "idemKey": idem_key, "plant": m.get("plant"),
|
||
"at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||
"status": "RELEASED", "qtyDone": 0, "progressPct": 0,
|
||
**payload,
|
||
}
|
||
m.setdefault("workOrders", []).append(external)
|
||
m.setdefault("idempotency", {})[idem_key] = eid
|
||
self._save(m)
|
||
return {"duplicate": False, "externalWo": external}
|
||
|
||
def post_report(self, external_wo_id: str, payload: dict) -> dict:
|
||
"""写入报工流水并更新外部工单进度。"""
|
||
m = self._load()
|
||
wo = next((w for w in m.get("workOrders", []) if w["id"] == external_wo_id), None)
|
||
if not wo:
|
||
raise ValueError(f"MES 无外部工单 {external_wo_id}")
|
||
rid = f"MES-RPT-{len(m.get('reports') or []) + 1:04d}"
|
||
report = {
|
||
"id": rid, "externalWoId": external_wo_id,
|
||
"at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||
**payload,
|
||
}
|
||
m.setdefault("reports", []).append(report)
|
||
if "qtyDone" in payload:
|
||
wo["qtyDone"] = payload["qtyDone"]
|
||
if "progressPct" in payload:
|
||
wo["progressPct"] = payload["progressPct"]
|
||
if payload.get("status"):
|
||
wo["status"] = payload["status"]
|
||
self._save(m)
|
||
return {"report": report, "externalWo": wo}
|
||
|
||
def reset(self) -> dict:
|
||
data = default_mirror()
|
||
self._save(data)
|
||
return self.status()
|
||
|
||
|
||
_client: MockMesClient | None = None
|
||
|
||
|
||
def get_mes_client() -> MockMesClient:
|
||
global _client
|
||
if _client is None:
|
||
_client = MockMesClient()
|
||
return _client
|
||
|
||
|
||
def reset_mes_client(path: Path | None = None) -> MockMesClient:
|
||
global _client
|
||
_client = MockMesClient(path)
|
||
_client.reset()
|
||
return _client
|