aps-agent/server/integrations/sap_stub.py

129 lines
4.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.

# ============================================================
# SAP 集成桩(moduleId: integ-sap-stub, 可重生 ✅)
# MD-05 最小切片:进程内 Mock SAP,无真实 RFC/OData。
# 入站:生产订单 / 库存;出站:开完工、产量、异常(幂等键防重复)。
# ============================================================
from __future__ import annotations
import json
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any
_MIRROR_PATH = Path(__file__).resolve().parents[1] / "data" / "sap_mirror.json"
def _today() -> datetime:
return datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
def default_mirror() -> dict[str, Any]:
"""演示用 SAP 镜像:含待同步生产订单与库存。"""
base = _today()
due1 = (base + timedelta(days=7)).strftime("%Y-%m-%d")
due2 = (base + timedelta(days=5)).strftime("%Y-%m-%d")
return {
"system": "SAP-MOCK",
"plant": "CNWH",
"updatedAt": datetime.now().strftime("%Y-%m-%d %H:%M"),
"productionOrders": [
{"aufnr": "1000123456", "matnr": "HV-HARNESS", "maktx": "高压线束总成",
"gamng": 120, "gstrp": due1, "priority": 3, "status": "REL",
"kitOk": True},
{"aufnr": "1000123457", "matnr": "PDU-UNIT", "maktx": "PDU配电单元",
"gamng": 80, "gstrp": due2, "priority": 2, "status": "REL",
"kitOk": False},
{"aufnr": "1000123458", "matnr": "CHARGE-GUN", "maktx": "充电枪总成",
"gamng": 60, "gstrp": due1, "priority": 4, "status": "REL",
"kitOk": True},
],
"stocks": [
{"matnr": "WIRE-HV", "labst": 15000, "inTransit": 0},
{"matnr": "TERM-HV", "labst": 8000, "inTransit": 500},
{"matnr": "BUSBAR", "labst": 2000, "inTransit": 0},
],
"receipts": [], # APS→SAP 回写流水
"idempotency": {}, # key → receipt id
}
class MockSapClient:
"""进程内 Mock SAP:读写本地镜像文件,模拟外部系统。"""
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()
return {
"connected": True,
"mode": "stub",
"system": m.get("system"),
"plant": m.get("plant"),
"updatedAt": m.get("updatedAt"),
"openOrders": len(m.get("productionOrders") or []),
"receiptCount": len(m.get("receipts") or []),
}
def pull_orders(self) -> list[dict]:
return list(self._load().get("productionOrders") or [])
def pull_stocks(self) -> list[dict]:
return list(self._load().get("stocks") or [])
def push_receipt(self, payload: dict, idem_key: str) -> dict:
"""幂等回写:相同 idem_key 返回原回执,不重复建单。"""
m = self._load()
existing = (m.get("idempotency") or {}).get(idem_key)
if existing:
receipt = next((r for r in m.get("receipts", []) if r["id"] == existing), None)
return {"duplicate": True, "receipt": receipt or {"id": existing, "idemKey": idem_key}}
rid = f"SAP-RCPT-{len(m.get('receipts') or []) + 1:04d}"
receipt = {
"id": rid, "idemKey": idem_key, "plant": m.get("plant"),
"at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
**payload,
}
m.setdefault("receipts", []).append(receipt)
m.setdefault("idempotency", {})[idem_key] = rid
self._save(m)
return {"duplicate": False, "receipt": receipt}
def reset(self) -> dict:
data = default_mirror()
self._save(data)
return self.status()
_client: MockSapClient | None = None
def get_sap_client() -> MockSapClient:
global _client
if _client is None:
_client = MockSapClient()
return _client
def reset_sap_client(path: Path | None = None) -> MockSapClient:
"""测试/演示:重置单例(可选临时镜像路径)。"""
global _client
_client = MockSapClient(path)
_client.reset()
return _client