本次迭代覆盖MES与WMS核心业务: 1. 新增接驳台托盘传感器读取与AGV对接能力 2. 完善工单排产、备料流程与权限体系拆分 3. 优化看板接口与前端路由、样式 4. 新增操作日志、库存盘点与角色保护逻辑 5. 修复代理地址、BOM保存等已知问题
408 lines
22 KiB
Python
408 lines
22 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
MES 8 项优化端到端验证(2026-09-04)
|
||
覆盖:
|
||
① 工单状态机(流转白名单/原因/日志/终态联动)
|
||
② 日排产与工单状态联动
|
||
③ BOM 需求量自动计算(不手填)
|
||
④ 备料生成前 WMS 物料存在性校验(缺料阻断 + 降级)
|
||
⑤ 下发 SN 归属/进线校验 + 未完成信号握手
|
||
⑧ 流程卡打印预检 + 产品名称联查修复 + 拧紧数据渲染
|
||
⑦ 流程多工位绑定(一流程多工位 / 一工位一启用流程 / 停用可再绑)
|
||
依赖:本机 8888(MES) 与 8890(WMS) 已用新二进制启动。
|
||
"""
|
||
import json
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
import time
|
||
import urllib.request
|
||
import urllib.error
|
||
from datetime import date, timedelta
|
||
|
||
MES = "http://127.0.0.1:8888"
|
||
WMS = "http://127.0.0.1:8890"
|
||
PSQL = [r"C:\Program Files\PostgreSQL\17\bin\psql.exe", "-U", "postgres", "-h", "127.0.0.1"]
|
||
PSQL_ENV = dict(os.environ, PGPASSWORD="postgres")
|
||
|
||
PASS, FAIL = [], []
|
||
|
||
def report(name, ok, extra=""):
|
||
(PASS if ok else FAIL).append(name)
|
||
print(("PASS " if ok else "FAIL ") + name + ((" | " + extra) if extra else ""))
|
||
|
||
def api(method, path, body=None, token=None, base=MES, raw=False, xapi=None):
|
||
url = base + path
|
||
data = None
|
||
headers = {"Content-Type": "application/json"}
|
||
if body is not None:
|
||
data = json.dumps(body, ensure_ascii=False).encode()
|
||
if token:
|
||
headers["Authorization"] = "Bearer " + token
|
||
if xapi:
|
||
headers["X-API-TOKEN"] = xapi
|
||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||
try:
|
||
with urllib.request.urlopen(req, timeout=15) as r:
|
||
if raw:
|
||
return r.status, r.read().decode("utf-8", "replace")
|
||
return r.status, json.loads(r.read().decode("utf-8", "replace"))
|
||
except urllib.error.HTTPError as e:
|
||
try:
|
||
return e.code, json.loads(e.read().decode("utf-8", "replace"))
|
||
except Exception:
|
||
return e.code, {}
|
||
except Exception as e:
|
||
return 0, {"code": -1, "message": str(e)}
|
||
|
||
def q(sql, db="bj_power_mes"):
|
||
"""psql 只读/写查询,返回行文本"""
|
||
p = subprocess.run(PSQL + ["-d", db, "-t", "-A", "-c", sql],
|
||
capture_output=True, text=True, env=PSQL_ENV)
|
||
return p.stdout.strip()
|
||
|
||
def esc(s):
|
||
return s.replace("'", "''")
|
||
|
||
def today(offset=0):
|
||
return (date.today() + timedelta(days=offset)).isoformat()
|
||
|
||
# 预清理(脚本可重入):清掉上次运行残留,避免工单号唯一约束冲突 / 旧SENT干扰握手
|
||
CLEAN_SQL = """
|
||
DELETE FROM material_request WHERE order_no LIKE 'E2E-%';
|
||
DELETE FROM daily_plan WHERE order_no LIKE 'E2E-%';
|
||
DELETE FROM scan_record WHERE sn LIKE 'E2E-SN-%' OR sn LIKE 'SN-DBG-%';
|
||
DELETE FROM plc_send_log WHERE order_no LIKE 'E2E-%' OR status='SENT';
|
||
DELETE FROM torque_record WHERE sn LIKE 'E2E-SN-%' OR sn LIKE 'SN-DBG-%' OR work_order_no LIKE 'E2E-%';
|
||
DELETE FROM step_data WHERE sn LIKE 'E2E-SN-%' OR sn LIKE 'SN-DBG-%';
|
||
DELETE FROM workpiece_process WHERE sn LIKE 'E2E-SN-%' OR sn LIKE 'SN-DBG-%';
|
||
DELETE FROM association_trace WHERE finished_sn LIKE 'E2E-SN-%' OR finished_sn LIKE 'SN-DBG-%';
|
||
DELETE FROM workpiece WHERE sn LIKE 'E2E-SN-%' OR sn LIKE 'SN-DBG-%' OR order_no LIKE 'E2E-%';
|
||
DELETE FROM event_log WHERE work_order_no LIKE 'E2E-%';
|
||
DELETE FROM work_order WHERE work_order_no LIKE 'E2E-%';
|
||
DELETE FROM work_order_bom WHERE product_code LIKE 'E2E-PROD-%';
|
||
"""
|
||
q(CLEAN_SQL)
|
||
|
||
# ---------- 登录 ----------
|
||
st, login = api("POST", "/api/v1/login", {"username": "admin", "password": "123456"})
|
||
token = (login.get("data") or {}).get("accessToken", "")
|
||
report("登录 admin", st == 200 and token and login.get("code") == 0,
|
||
login.get("message", ""))
|
||
if not token:
|
||
print(json.dumps(login, ensure_ascii=False)[:300])
|
||
sys.exit(1)
|
||
|
||
st, pt = api("GET", "/api/v1/product-types", token=token)
|
||
ptypes = (pt.get("data") or [])
|
||
report("产品类型可查", pt.get("code") == 0 and len(ptypes) > 0)
|
||
ptype_id = ptypes[0]["id"] if ptypes else 0
|
||
|
||
def create_wo(no, qty=10, product_code="E2E-PROD-001", product_name="测试产品E2E", status="CREATED", seq=""):
|
||
body = {"workOrderNo": no, "productTypeId": ptype_id, "productCode": product_code,
|
||
"productName": product_name, "quantity": qty, "status": status, "processSeq": seq}
|
||
return api("POST", "/api/v1/work-orders", body, token)
|
||
|
||
def wo_by_no(no):
|
||
st, r = api("GET", "/api/v1/work-orders?orderNo=" + no, token=token)
|
||
rows = r.get("data") or []
|
||
return (rows[0] if rows else None)
|
||
|
||
def set_status(wo_id, status, reason=""):
|
||
return api("POST", "/api/v1/work-orders/status", {"id": wo_id, "status": status, "reason": reason}, token)
|
||
|
||
def get_wo(wo_id):
|
||
st, r = api("GET", "/api/v1/work-orders/%d" % wo_id, token=token)
|
||
return r.get("data")
|
||
|
||
D1 = today(1); D2 = today(2); D3 = today(3); TDY = today(0)
|
||
|
||
# ================= A. 工单状态机 =================
|
||
print("\n==== A. 工单状态机 ====")
|
||
noA = "E2E-WO-A-" + today().replace("-", "")
|
||
create_wo(noA, qty=10)
|
||
wo = wo_by_no(noA)
|
||
report("A1 创建工单(恒CREATED)", wo is not None and wo.get("status") == "CREATED",
|
||
"status=" + (wo or {}).get("status", "?"))
|
||
|
||
# 非法跳转 CREATED→DONE 应拒绝
|
||
code, r = set_status(wo["id"], "DONE")
|
||
report("A2 非法跳转 CREATED→DONE 被拒", code == 200 and r.get("code") != 0, r.get("message", ""))
|
||
# 合法 RELEASED
|
||
code, r = set_status(wo["id"], "RELEASED")
|
||
report("A3 CREATED→RELEASED 允许", r.get("code") == 0, r.get("message", ""))
|
||
# RELEASED→DONE 拒绝
|
||
code, r = set_status(wo["id"], "DONE")
|
||
report("A4 RELEASED→DONE 被拒(需先开工)", r.get("code") != 0, r.get("message", ""))
|
||
# RELEASED→IN_PROGRESS
|
||
code, r = set_status(wo["id"], "IN_PROGRESS")
|
||
report("A5 RELEASED→IN_PROGRESS 允许", r.get("code") == 0, r.get("message", ""))
|
||
# 暂停带原因
|
||
code, r = set_status(wo["id"], "PAUSED", "设备检修")
|
||
report("A6 IN_PROGRESS→PAUSED(带原因) 允许", r.get("code") == 0, r.get("message", ""))
|
||
# 暂停期间再完工应拒绝
|
||
code, r = set_status(wo["id"], "DONE")
|
||
report("A7 PAUSED→DONE 被拒", r.get("code") != 0, r.get("message", ""))
|
||
# 恢复
|
||
code, r = set_status(wo["id"], "IN_PROGRESS")
|
||
report("A8 PAUSED→IN_PROGRESS 允许", r.get("code") == 0, r.get("message", ""))
|
||
# 完工
|
||
code, r = set_status(wo["id"], "DONE")
|
||
report("A9 IN_PROGRESS→DONE 允许", r.get("code") == 0, r.get("message", ""))
|
||
woA = get_wo(wo["id"])
|
||
report("A10 DONE 记录 completedAt", (woA or {}).get("completedAt") is not None)
|
||
# 终态不可再流转
|
||
code, r = set_status(wo["id"], "CANCELLED")
|
||
report("A11 DONE→CANCELLED 被拒", r.get("code") != 0, r.get("message", ""))
|
||
|
||
# 取消带原因
|
||
noB = "E2E-WO-B-" + today().replace("-", "")
|
||
create_wo(noB, qty=5)
|
||
woB = wo_by_no(noB)
|
||
set_status(woB["id"], "CANCELLED", "计划取消")
|
||
# 取消后不可流转
|
||
code, r = set_status(woB["id"], "IN_PROGRESS")
|
||
report("A12 CANCELLED 不可再开工", r.get("code") != 0, r.get("message", ""))
|
||
|
||
# 状态日志 + 原因入库
|
||
logrows = q("SELECT description || '|' || COALESCE(payload->>'reason','') FROM event_log WHERE work_order_no='%s' AND event_type='work.order.status' ORDER BY id DESC" % esc(noA))
|
||
report("A13 状态流转写日志且带原因", "变更工单状态" in logrows and "设备检修" in logrows, logrows.replace("\n", " "))
|
||
logB = q("SELECT payload->>'reason' FROM event_log WHERE work_order_no='%s' AND event_type='work.order.status' ORDER BY id DESC LIMIT 1" % esc(noB))
|
||
report("A14 取消原因入库", logB == "计划取消", logB)
|
||
|
||
# 编辑/删除约束
|
||
noC1 = "E2E-WO-C1-" + today().replace("-", "")
|
||
create_wo(noC1, qty=6)
|
||
woC1 = wo_by_no(noC1)
|
||
code, r = api("PUT", "/api/v1/work-orders", {"id": woC1["id"], "workOrderNo": noC1, "productTypeId": ptype_id, "quantity": 7}, token)
|
||
report("A15 CREATED 可编辑", r.get("code") == 0, r.get("message", ""))
|
||
set_status(woC1["id"], "RELEASED")
|
||
code, r = api("PUT", "/api/v1/work-orders", {"id": woC1["id"], "workOrderNo": noC1, "productTypeId": ptype_id, "quantity": 8}, token)
|
||
report("A16 已下发不可编辑", r.get("code") != 0, r.get("message", ""))
|
||
code, r = api("DELETE", "/api/v1/work-orders/%d" % woC1["id"], token=token)
|
||
report("A17 已下发不可删除", r.get("code") != 0, r.get("message", ""))
|
||
woC1 = wo_by_no(noC1)
|
||
|
||
# ================= B. 日排产联动 =================
|
||
print("\n==== B. 日排产与工单状态联动 ====")
|
||
noC = "E2E-WO-C-" + today().replace("-", "")
|
||
create_wo(noC, qty=20)
|
||
woC = wo_by_no(noC)
|
||
def save_plan(order_no, d, qty, status=""):
|
||
return api("POST", "/api/v1/daily-plans", {"orderNo": order_no, "planDate": d, "planQty": qty, "status": status}, token)
|
||
code, r = save_plan(noC, D1, 8)
|
||
report("B1 保存日排产(PENDING)", r.get("code") == 0, r.get("message", ""))
|
||
code, r = save_plan(noC, D2, 15)
|
||
report("B2 排产合计超工单量被拒", r.get("code") != 0, r.get("message", ""))
|
||
code, r = save_plan(noC, D2, 10)
|
||
report("B3 合计未超可保存", r.get("code") == 0, r.get("message", ""))
|
||
# 工单开工 → 排产置 PROCESSING
|
||
set_status(woC["id"], "RELEASED")
|
||
set_status(woC["id"], "IN_PROGRESS")
|
||
p1 = q("SELECT status FROM daily_plan WHERE order_no='%s' AND plan_date='%s'" % (esc(noC), D1))
|
||
report("B4 工单执行中→排产置PROCESSING", p1 == "PROCESSING", p1)
|
||
# 暂停期间不可新增排产
|
||
code, r = set_status(woC["id"], "PAUSED", "待料")
|
||
code, r = save_plan(noC, D3, 2)
|
||
report("B5 工单暂停→新增排产被拒", r.get("code") != 0 and "暂停" in r.get("message", ""), r.get("message", ""))
|
||
code, r = set_status(woC["id"], "IN_PROGRESS")
|
||
code, r = save_plan(noC, D3, 2)
|
||
report("B6 恢复后可排产", r.get("code") == 0, r.get("message", ""))
|
||
# 工单完工 → 未足额排产置 CANCELLED
|
||
code, r = set_status(woC["id"], "DONE")
|
||
plans = q("SELECT plan_date||':'||status FROM daily_plan WHERE order_no='%s' ORDER BY plan_date" % esc(noC)).replace("\n", " ")
|
||
report("B7 工单DONE→未足额排产置CANCELLED", all(s in plans for s in [D1 + ":CANCELLED", D2 + ":CANCELLED", D3 + ":CANCELLED"]), plans)
|
||
# 已结束工单不可排产
|
||
code, r = save_plan(noC, D1, 1)
|
||
report("B8 DONE 工单不可排产", r.get("code") != 0, r.get("message", ""))
|
||
# 取消工单联动排产
|
||
noCC = "E2E-WO-CC-" + today().replace("-", "")
|
||
create_wo(noCC, qty=5)
|
||
woCC = wo_by_no(noCC)
|
||
save_plan(noCC, D1, 2)
|
||
set_status(woCC["id"], "CANCELLED", "紧急取消")
|
||
pcc = q("SELECT status FROM daily_plan WHERE order_no='%s'" % esc(noCC))
|
||
report("B9 工单取消→排产置CANCELLED", pcc == "CANCELLED", pcc)
|
||
|
||
# ================= C. 完工联动当日排产 completedQty =================
|
||
print("\n==== C. 完工联动当日排产 ====")
|
||
noF = "E2E-WO-F-" + today().replace("-", "")
|
||
create_wo(noF, qty=5)
|
||
woF = wo_by_no(noF)
|
||
save_plan(noF, TDY, 2)
|
||
def online(sn, order_no):
|
||
return api("POST", "/api/v1/workpiece/online", {"sn": sn, "orderNo": order_no}, token)
|
||
def report_proc(sn, code, station):
|
||
return api("POST", "/api/v1/workpiece/process/report", {"sn": sn, "processCode": code, "stationNo": station, "steps": []}, token)
|
||
def done(sn):
|
||
return api("POST", "/api/v1/workpiece/done", {"sn": sn, "batchItems": [], "serialItems": []}, token)
|
||
code, r = online("E2E-SN-F1", noF)
|
||
report("C1 工件进线", r.get("code") == 0, r.get("message", ""))
|
||
code, r = report_proc("E2E-SN-F1", 1, 1)
|
||
report("C2 工序报工", r.get("code") == 0, r.get("message", ""))
|
||
code, r = done("E2E-SN-F1")
|
||
report("C3 完工OK", r.get("code") == 0, r.get("message", ""))
|
||
pc = q("SELECT completed_qty||':'||status FROM daily_plan WHERE order_no='%s' AND plan_date='%s'" % (esc(noF), TDY))
|
||
report("C4 完工后排产 completedQty=1/置PROCESSING", pc == "1:PROCESSING", pc)
|
||
code, r = online("E2E-SN-F2", noF)
|
||
code2, r2 = done("E2E-SN-F2")
|
||
report("C5 第二件进线+完工", r.get("code") == 0 and r2.get("code") == 0, r.get("message", "") + " / " + r2.get("message", ""))
|
||
pc = q("SELECT completed_qty||':'||status FROM daily_plan WHERE order_no='%s' AND plan_date='%s'" % (esc(noF), TDY))
|
||
report("C6 足额后排产置DONE", pc == "2:DONE", pc)
|
||
|
||
# ================= D. BOM 算料 + WMS 校验 =================
|
||
print("\n==== D. BOM 自动算料 + WMS 存在性校验 ====")
|
||
noG = "E2E-WO-G-" + today().replace("-", "")
|
||
PROD = "E2E-PROD-001"
|
||
create_wo(noG, qty=5, product_code=PROD)
|
||
woG = wo_by_no(noG)
|
||
save_plan(noG, D3, 3)
|
||
|
||
def save_bom(items):
|
||
return api("PUT", "/api/v1/bom", {"productCode": PROD, "items": items}, token)
|
||
good_items = [
|
||
{"materialCode": "GJ-BAN-001", "materialName": "10mm钢板(结构件)", "spec": "", "unit": "件", "manageMode": "1", "unitQty": 2, "lossRate": 5},
|
||
{"materialCode": "JM-ZHOUCHENG-6020", "materialName": "精密轴承6020", "spec": "", "unit": "个", "manageMode": "2", "unitQty": 1, "lossRate": 0},
|
||
]
|
||
bom_with_bad = good_items + [{"materialCode": "NO-SUCH-CODE-9", "materialName": "幽灵料", "spec": "", "unit": "个", "manageMode": "2", "unitQty": 1, "lossRate": 0}]
|
||
code, r = save_bom(bom_with_bad)
|
||
report("D1 保存BOM(含缺失料) OK", r.get("code") == 0, r.get("message", ""))
|
||
code, r = api("POST", "/api/v1/material-requests/generate", {"planDate": D3}, token)
|
||
data = r.get("data") or {}
|
||
report("D2 生成备料单:缺料阻断不生成", r.get("code") == 0 and data.get("blocked") is True and "NO-SUCH-CODE-9" in data.get("missing", []),
|
||
json.dumps(data, ensure_ascii=False))
|
||
cnt = q("SELECT count(*) FROM material_request WHERE order_no='%s'" % esc(noG))
|
||
report("D3 阻断时零写入", cnt == "0", cnt)
|
||
# 修正 BOM(SaveBom 为 upsert 不删行,先移除缺料行)→ 生成成功,数量=3*2*1.05=6.3 与 3
|
||
q("DELETE FROM work_order_bom WHERE product_code='%s' AND material_code='NO-SUCH-CODE-9'" % esc(PROD))
|
||
code, r = save_bom(good_items)
|
||
code, r = api("POST", "/api/v1/material-requests/generate", {"planDate": D3}, token)
|
||
data = r.get("data") or {}
|
||
report("D4 修正BOM后生成成功", r.get("code") == 0 and data.get("count") == 2, json.dumps(data, ensure_ascii=False))
|
||
st, rows = api("GET", "/api/v1/material-requests?orderNo=" + noG, token=token)
|
||
mrs = rows.get("data") or []
|
||
rn = sorted(x.get("requestNo") for x in mrs)
|
||
report("D5 生成两条且单号唯一", len(mrs) == 2 and len(set(rn)) == 2, ";".join(rn))
|
||
qty_ok = sorted(round(x.get("reqQty"), 2) for x in mrs) == [3.0, 6.3]
|
||
report("D6 reqQty=planQty×unitQty×(1+loss) 自动计算", qty_ok, str([(x.get("materialCode"), x.get("reqQty")) for x in mrs]))
|
||
|
||
# WMS 直连接口(可选旁证)
|
||
st, r = api("POST", "/api/internal/material/exists", {"codes": ["GJ-BAN-001", "NO-SUCH-CODE-9"]},
|
||
base=WMS, xapi="Hardman_2026")
|
||
wdata = r.get("data") or {}
|
||
report("D7 WMS内部接口 missing 直查", st == 200 and r.get("code") == 0 and wdata.get("missing") == ["NO-SUCH-CODE-9"],
|
||
json.dumps(wdata, ensure_ascii=False))
|
||
|
||
# ================= E. 下发 SN 校验 + 握手 =================
|
||
print("\n==== E. PLC 下发 SN 校验 ====")
|
||
def plc_send(body):
|
||
return api("POST", "/api/v1/plc/send-process", body, token)
|
||
code, r = plc_send({"orderNo": noG, "sn": "E2E-SN-NOPE", "stationNo": 1, "processCombination": "1"})
|
||
report("E1 未进线SN被拒", r.get("code") != 0 and ("不属于" in r.get("message", "") or "未进线" in r.get("message", "")), r.get("message", ""))
|
||
code, r = online("E2E-SN-001", noG)
|
||
report("E2 进线E2E-SN-001", r.get("code") == 0, r.get("message", ""))
|
||
code, r = plc_send({"orderNo": noG, "sn": "E2E-SN-001", "stationNo": 1, "processCombination": "1"})
|
||
report("E3 合法SN下发成功", r.get("code") == 0, r.get("message", ""))
|
||
# 立即再发第二条 → 上一条未收到完成信号
|
||
code, r = plc_send({"orderNo": noG, "sn": "E2E-SN-001", "stationNo": 1, "processCombination": "1"})
|
||
report("E4 未收到完成信号被拒(握手)", r.get("code") != 0 and "完成信号" in r.get("message", ""), r.get("message", ""))
|
||
time.sleep(7)
|
||
code, r = plc_send({"orderNo": noG, "sn": "E2E-SN-001", "stationNo": 1, "processCombination": "1"})
|
||
report("E5 完成信号后再次下发成功", r.get("code") == 0, r.get("message", ""))
|
||
# DONE 工单不可下发:SN 属于该工单才先命中 DONE 校验
|
||
noH = "E2E-WO-H-" + today().replace("-", "")
|
||
create_wo(noH, qty=3)
|
||
woH = wo_by_no(noH)
|
||
set_status(woH["id"], "RELEASED"); set_status(woH["id"], "IN_PROGRESS"); set_status(woH["id"], "DONE")
|
||
online("E2E-SN-H1", noH)
|
||
code, r = plc_send({"orderNo": noH, "sn": "E2E-SN-H1", "stationNo": 1, "processCombination": "1"})
|
||
report("E6 DONE工单禁止下发", r.get("code") != 0 and "已完成" in r.get("message", ""), r.get("message", ""))
|
||
|
||
# ================= F. 流程多工位绑定 =================
|
||
print("\n==== F. 流程多工位绑定 ====")
|
||
def flows(station_no=None):
|
||
u = "/api/v1/process-flows" + (("?stationNo=" + str(station_no)) if station_no else "")
|
||
st, r = api("GET", u, token=token)
|
||
return r.get("data") or []
|
||
def save_flow(body):
|
||
return api("POST", "/api/v1/process-flows", body, token)
|
||
def flow_status(fid, status):
|
||
return api("POST", "/api/v1/process-flows/status", {"id": fid, "status": status}, token)
|
||
def stations():
|
||
st, r = api("GET", "/api/v1/stations", token=token)
|
||
return r.get("data") or []
|
||
STEP11 = [{"seq": 1, "name": "装配完成", "collectType": "NONE", "isTorque": False, "remark": "", "criteria": []}]
|
||
base_st = {x["stationNo"]: (x.get("flowId") or 0) for x in stations()}
|
||
base_flows = {x["id"]: x for x in flows()}
|
||
report("F1 列表含 stations 绑定字段", all(("stations" in x) for x in base_flows.values()),
|
||
str([(x["id"], x.get("stations")) for x in list(base_flows.values())[:3]]))
|
||
|
||
# 负向:启用流程占用冲突
|
||
code, r = save_flow({"name": "E2E-冲突流程", "stationNo": 5, "stations": [5], "steps": []})
|
||
report("F2 工位已被启用流程占用被拒", r.get("code") != 0 and "已被启用流程" in r.get("message", ""), r.get("message", ""))
|
||
# 停用 11/12 → 新建临时流程绑 12 → 扩到 11+12
|
||
flow_status(11, "INACTIVE"); flow_status(12, "INACTIVE")
|
||
code, r = save_flow({"name": "E2E-流程-双工位", "stationNo": 12, "stations": [12], "steps": STEP11})
|
||
fid = None
|
||
if r.get("code") == 0:
|
||
for f in flows():
|
||
if f["name"] == "E2E-流程-双工位":
|
||
fid = f["id"]
|
||
report("F3 停用后可新建流程绑定工位12", r.get("code") == 0 and fid, r.get("message", ""))
|
||
code, r = save_flow({"id": fid, "name": "E2E-流程-双工位", "stationNo": 12, "stations": [11, 12], "steps": STEP11})
|
||
report("F4 一流程绑多工位(11+12)", r.get("code") == 0, r.get("message", ""))
|
||
st_map = {x["stationNo"]: (x.get("flowId") or 0) for x in stations()}
|
||
report("F5 工位11/12均指向临时流程", st_map.get(11) == fid and st_map.get(12) == fid, str(st_map))
|
||
fl11 = flows(11)
|
||
report("F6 按工位号过滤命中临时流程", any(x.get("id") == fid for x in fl11), str([x.get("id") for x in fl11]))
|
||
# 恢复
|
||
code, r = api("DELETE", "/api/v1/process-flows/%d" % fid, token=token)
|
||
report("F7 删除流程自动解绑工位", r.get("code") == 0, r.get("message", ""))
|
||
st_map = {x["stationNo"]: (x.get("flowId") or 0) for x in stations()}
|
||
report("F8 删除后工位解绑", st_map.get(11) == 0 and st_map.get(12) == 0, str(st_map))
|
||
flow_status(11, "ACTIVE"); flow_status(12, "ACTIVE")
|
||
code, r = save_flow({"id": 11, "name": base_flows[11]["name"], "stationNo": 11, "stations": [11], "steps": STEP11})
|
||
code2, r2 = save_flow({"id": 12, "name": base_flows[12]["name"], "stationNo": 12, "stations": [12], "steps": STEP11})
|
||
report("F9 恢复工位11/12原绑定", r.get("code") == 0 and r2.get("code") == 0, r.get("message", "") + " / " + r2.get("message", ""))
|
||
st_map = {x["stationNo"]: (x.get("flowId") or 0) for x in stations()}
|
||
final_ok = st_map.get(11) == 11 and st_map.get(12) == 12 and base_st.get(11) == st_map.get(11) and base_st.get(12) == st_map.get(12)
|
||
report("F10 恢复后绑定与基线一致", final_ok, str({11: st_map.get(11), 12: st_map.get(12)}))
|
||
fl11_now = {x["id"]: x for x in flows()}
|
||
ok11 = len(fl11_now.get(11, {}).get("steps", [])) == len(base_flows.get(11, {}).get("steps", []))
|
||
ok12 = len(fl11_now.get(12, {}).get("steps", [])) == len(base_flows.get(12, {}).get("steps", []))
|
||
report("F11 恢复后11/12号流程步骤数一致", ok11 and ok12,
|
||
"11:" + str(len(fl11_now.get(11, {}).get("steps", []))) + "/" + str(len(base_flows.get(11, {}).get("steps", []))) +
|
||
" 12:" + str(len(fl11_now.get(12, {}).get("steps", []))) + "/" + str(len(base_flows.get(12, {}).get("steps", []))))
|
||
|
||
# ================= G. 流程卡预检与渲染 =================
|
||
print("\n==== G. 流程卡打印预检 ====")
|
||
st, r = api("GET", "/api/v1/process-card?sn=E2E-SN-001&check=1", token=token)
|
||
miss = (r.get("data") or {}).get("missing", []) if r.get("code") == 0 else None
|
||
report("G1 无任何工序记录时预检报缺失", miss is not None and any("工序" in m for m in miss), str(miss))
|
||
# 补 1、7 号工序 + 拧紧数据
|
||
report_proc("E2E-SN-001", 1, 1)
|
||
report_proc("E2E-SN-001", 7, 7)
|
||
code, r = api("POST", "/api/v1/torque/report", {"sn": "E2E-SN-001", "workOrder": noG, "stationNo": "7",
|
||
"screwNo": "S-001", "torque": 50.0, "angle": 90.0, "result": "OK"}, token)
|
||
report("G2 拧紧上报OK", r.get("code") == 0, r.get("message", ""))
|
||
st, r = api("GET", "/api/v1/process-card?sn=E2E-SN-001&check=1", token=token)
|
||
miss = (r.get("data") or {}).get("missing", [])
|
||
report("G3 补齐后预检缺失为空", r.get("code") == 0 and miss == [], str(miss))
|
||
st, html = api("GET", "/api/v1/process-card?sn=E2E-SN-001", token=token, raw=True)
|
||
report("G4 打印HTML含产品名/工序/拧紧数据",
|
||
st == 200 and "测试产品E2E" in html and "装配七" in html and "拧紧数据" in html and "S-001" in html,
|
||
("len=%d" % len(html)) if html else "")
|
||
|
||
# ================= 清理 =================
|
||
print("\n==== 清理测试数据 ====")
|
||
q(CLEAN_SQL)
|
||
left = q("SELECT count(*) FROM work_order WHERE work_order_no LIKE 'E2E-%'")
|
||
report("清理完成", left == "0", "剩余=" + left)
|
||
|
||
print("\n========== 汇总 ==========")
|
||
print("PASS %d / FAIL %d" % (len(PASS), len(FAIL)))
|
||
if FAIL:
|
||
print("FAILED: " + "; ".join(FAIL))
|
||
sys.exit(1)
|
||
print("全部通过")
|