Files
bj_power/e2e_mes_verify.py
T
SunYF b6799c9de4 feat: 完成产线与仓储系统多模块迭代升级
本次迭代覆盖MES与WMS核心业务:
1. 新增接驳台托盘传感器读取与AGV对接能力
2. 完善工单排产、备料流程与权限体系拆分
3. 优化看板接口与前端路由、样式
4. 新增操作日志、库存盘点与角色保护逻辑
5. 修复代理地址、BOM保存等已知问题
2026-09-04 14:18:53 +08:00

408 lines
22 KiB
Python
Raw 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.
# -*- 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)
# 修正 BOMSaveBom 为 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("全部通过")