_verify_s1_s4_continue_e2e.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. #!/usr/bin/env python3
  2. """Continue unfinished S1-S4 tests: routing sync, reschedule, dispatch, S3/S4 smoke."""
  3. from __future__ import annotations
  4. import json
  5. import sys
  6. import urllib.error
  7. import urllib.request
  8. from datetime import date
  9. import pymysql
  10. from gmssl import sm2
  11. BASE = "http://127.0.0.1:5005"
  12. TENANT = 797403760988229
  13. DOMAIN = str(TENANT)
  14. BILL_NO = "SO202607120001"
  15. WORK_ORD = "M000000005"
  16. ORDER_ID = 825854621474885
  17. PK = "84C7466D950E120E5ECE5DD85D0C90EAA85081A3A2BD7C57AE6DC822EFCCBD66620C67B0103FC8DD280E36C3B282977B722AAEC3C56518EDCEBAFB72C5A05312"
  18. CONN = dict(
  19. host="123.60.180.165",
  20. port=3306,
  21. user="aidopremote",
  22. password="1234567890aiDOP#",
  23. database="aidopdev",
  24. charset="utf8mb4",
  25. connect_timeout=20,
  26. cursorclass=pymysql.cursors.DictCursor,
  27. )
  28. def http(method, path, body=None, token=None, timeout=180):
  29. headers = {"Content-Type": "application/json", "Accept": "application/json"}
  30. if token:
  31. headers["Authorization"] = f"Bearer {token}"
  32. data = None if body is None else json.dumps(body, ensure_ascii=False).encode("utf-8")
  33. req = urllib.request.Request(f"{BASE}{path}", data=data, headers=headers, method=method)
  34. try:
  35. with urllib.request.urlopen(req, timeout=timeout) as resp:
  36. raw = resp.read().decode("utf-8")
  37. return resp.status, json.loads(raw) if raw else {}
  38. except urllib.error.HTTPError as e:
  39. raw = e.read().decode("utf-8", errors="replace")
  40. try:
  41. return e.code, json.loads(raw)
  42. except json.JSONDecodeError:
  43. return e.code, {"raw": raw[:1500]}
  44. except Exception as e:
  45. return 0, {"error": str(e)}
  46. def unwrap(body):
  47. if isinstance(body, dict) and body.get("code") == 200 and "result" in body:
  48. return body["result"]
  49. return body
  50. def ok(status, body):
  51. if status != 200:
  52. return False
  53. if isinstance(body, dict) and body.get("error"):
  54. return False
  55. if isinstance(body, dict) and body.get("code") not in (None, 200):
  56. return False
  57. if isinstance(body, dict) and body.get("type") == "error":
  58. return False
  59. return True
  60. def login():
  61. crypt = sm2.CryptSM2(public_key=PK, private_key=None, mode=1)
  62. pwd = crypt.encrypt(b"1234567890dop").hex()
  63. st, body = http("POST", "/api/sysAuth/login", {"account": "AIDOPDemo", "password": pwd, "tenantId": DOMAIN})
  64. token = (body.get("result") or {}).get("accessToken") or body.get("accessToken")
  65. if not token:
  66. raise RuntimeError(f"login failed: {body}")
  67. return token
  68. def add(report, name, status, body, extra=None):
  69. item = {
  70. "step": name,
  71. "http": status,
  72. "ok": ok(status, body),
  73. "body": unwrap(body) if isinstance(body, dict) else body,
  74. }
  75. if isinstance(body, dict) and body.get("message"):
  76. item["message"] = body.get("message")
  77. if isinstance(body, dict) and body.get("raw"):
  78. item["raw"] = body.get("raw")[:300]
  79. if isinstance(body, dict) and body.get("error"):
  80. item["error"] = body.get("error")
  81. if extra:
  82. item.update(extra)
  83. # Truncate large bodies
  84. b = item.get("body")
  85. if isinstance(b, dict) and len(json.dumps(b, default=str)) > 2000:
  86. keep = {}
  87. for k in (
  88. "message",
  89. "Message",
  90. "total",
  91. "workOrderCount",
  92. "scheduleRowCount",
  93. "skippedWorkOrders",
  94. "shortageItemCount",
  95. "prCreatedCount",
  96. "prUpdatedCount",
  97. "createdCount",
  98. "candidateCount",
  99. "demandCount",
  100. "enabled",
  101. "batchId",
  102. "dwdCount",
  103. "shortageCount",
  104. "workOrdTempCount",
  105. "list",
  106. "actionCode",
  107. "warnings",
  108. "workOrders",
  109. ):
  110. if k in b:
  111. keep[k] = b[k] if k != "list" else (len(b[k]) if isinstance(b[k], list) else b[k])
  112. if not keep:
  113. keep = {"keys": list(b.keys())[:12]}
  114. item["body"] = keep
  115. report["steps"].append(item)
  116. return item
  117. def q(conn, sql, params=None):
  118. with conn.cursor() as cur:
  119. cur.execute(sql, params or ())
  120. return cur.fetchall()
  121. def q1(conn, sql, params=None):
  122. rows = q(conn, sql, params)
  123. return rows[0] if rows else {}
  124. def main():
  125. report = {
  126. "title": "S1-S4 continue unfinished tests",
  127. "bill_no": BILL_NO,
  128. "work_ord": WORK_ORD,
  129. "steps": [],
  130. "db": {},
  131. "checks": [],
  132. }
  133. st, body = http("GET", "/api/sysAuth/captcha")
  134. add(report, "backend_health", st, body, {"ok": st == 200})
  135. if st != 200:
  136. report["passed"] = False
  137. print(json.dumps(report, ensure_ascii=False, indent=2, default=str))
  138. sys.exit(1)
  139. token = login()
  140. report["login"] = "ok"
  141. with pymysql.connect(**CONN) as conn:
  142. report["db"]["before"] = {
  143. "wo": q1(
  144. conn,
  145. "SELECT WorkOrd, ItemNum, Status, BusinessID FROM WorkOrdMaster WHERE tenant_id=%s AND WorkOrd=%s",
  146. (TENANT, WORK_ORD),
  147. ),
  148. "routing_cnt": q1(
  149. conn,
  150. "SELECT COUNT(*) AS c FROM WorkOrdRouting WHERE tenant_id=%s AND WorkOrd=%s",
  151. (TENANT, WORK_ORD),
  152. ).get("c", 0),
  153. "detail_cnt": q1(
  154. conn,
  155. "SELECT COUNT(*) AS c FROM WorkOrdDetail WHERE tenant_id=%s AND WorkOrd=%s",
  156. (TENANT, WORK_ORD),
  157. ).get("c", 0),
  158. "demand": q1(conn, "SELECT COUNT(*) AS c FROM ic_demandschedule WHERE tenant_id=%s", (TENANT,)).get("c", 0),
  159. "pr": q1(
  160. conn,
  161. "SELECT COUNT(*) AS c FROM srm_pr_main WHERE tenant_id=%s AND IFNULL(IsDeleted,0)=0",
  162. (TENANT,),
  163. ).get("c", 0),
  164. "workordtemp_has_tenant": q1(
  165. conn,
  166. """
  167. SELECT COUNT(*) AS c FROM information_schema.COLUMNS
  168. WHERE TABLE_SCHEMA='aidopdev' AND TABLE_NAME='WorkOrdTemp' AND COLUMN_NAME='tenant_id'
  169. """,
  170. ).get("c", 0),
  171. }
  172. # S2: sync routing for unfinished schedule path
  173. st, body = http(
  174. "POST",
  175. "/api/Production/scheduling/sync-routing",
  176. {"workOrd": WORK_ORD, "domain": DOMAIN},
  177. token,
  178. )
  179. add(report, "s2_sync_routing", st, body)
  180. st, body = http("GET", f"/api/Production/scheduling/routings?workOrd={WORK_ORD}&domain={DOMAIN}", token=token)
  181. add(report, "s2_routings_list", st, body)
  182. st, body = http("GET", f"/api/Production/scheduling/materials?workOrd={WORK_ORD}&domain={DOMAIN}", token=token)
  183. add(report, "s2_materials_list", st, body)
  184. st, body = http("POST", f"/api/Production/scheduling/generate?domain={DOMAIN}", None, token, timeout=300)
  185. add(report, "s2_schedule_generate_retry", st, body)
  186. st, body = http("GET", f"/api/Production/scheduling/list?page=1&pageSize=10&domain={DOMAIN}&keyword={WORK_ORD}", token=token)
  187. add(report, "s2_scheduling_list_wo", st, body)
  188. st, body = http("GET", f"/api/Production/work-order-progress/list?page=1&pageSize=10&keyword={WORK_ORD}", token=token)
  189. add(report, "s2_progress_list", st, body)
  190. # S2 dispatch / kitting
  191. st, body = http(
  192. "POST",
  193. "/api/WorkOrder/dispatch/kitting-check",
  194. {"workOrd": WORK_ORD, "tenantId": TENANT},
  195. token,
  196. timeout=180,
  197. )
  198. add(report, "s2_dispatch_kitting_check", st, body)
  199. st, body = http(
  200. "POST",
  201. "/api/WorkOrder/dispatch/work-order-kitting-check",
  202. {"workOrd": WORK_ORD, "tenantId": TENANT},
  203. token,
  204. timeout=180,
  205. )
  206. add(report, "s2_work_order_kitting_check", st, body)
  207. st, body = http(
  208. "POST",
  209. "/api/WorkOrder/dispatch/release",
  210. {
  211. "workOrd": WORK_ORD,
  212. "tenantId": TENANT,
  213. "ordDate": date.today().isoformat(),
  214. "lotSerial": "E2E-CONT-001",
  215. },
  216. token,
  217. timeout=180,
  218. )
  219. add(report, "s2_dispatch_release", st, body)
  220. # S3
  221. for name, path, method, body in [
  222. ("s3_demand_schedule_list", "/api/Supply/demand-schedule/list?page=1&pageSize=10", "GET", None),
  223. ("s3_purchase_request_list", "/api/Supply/purchase-request/list?page=1&pageSize=10", "GET", None),
  224. ("s3_purchase_order_list", "/api/Supply/purchase-order/list?page=1&pageSize=10", "GET", None),
  225. ("s3_delivery_schedule_list", "/api/Supply/delivery-schedule/list?page=1&pageSize=10", "GET", None),
  226. ("s3_material_readiness_list", "/api/Supply/work-order-material-readiness/list?page=1&pageSize=10", "GET", None),
  227. ("s3_kitting_refresh", "/api/Supply/work-order-material-readiness/refresh", "POST", None),
  228. ("s3_delivery_generate", "/api/Supply/delivery-schedule/generate", "POST", None),
  229. ("s3_process_outsource_list", "/api/Supply/process-outsource-order/list?page=1&pageSize=5", "GET", None),
  230. ("s3_outsource_order_list", "/api/Supply/outsource-order/list?page=1&pageSize=5", "GET", None),
  231. ]:
  232. st, body = http(method, path, body, token, timeout=300)
  233. add(report, name, st, body)
  234. # S4
  235. for name, path in [
  236. ("s4_supplier_shipment_list", "/api/ProcurementExecution/supplier-shipment/list?page=1&pageSize=10"),
  237. ("s4_receiving_list", "/api/ProcurementExecution/receiving/list?page=1&pageSize=5"),
  238. ("s4_iqc_list", "/api/ProcurementExecution/iqc/list?page=1&pageSize=5"),
  239. ("s4_shortage_board", "/api/ProcurementExecution/shortage-board/list?page=1&pageSize=5"),
  240. ]:
  241. st, body = http("GET", path, token=token)
  242. add(report, name, st, body)
  243. st, body = http("POST", f"/api/WorkOrder/material-requirement/generate?domain={DOMAIN}", None, token, timeout=300)
  244. add(report, "s4_mrp_generate_again", st, body)
  245. # S1 lists for sample order
  246. st, body = http("GET", f"/api/Order/seorder/list?billNo={BILL_NO}&page=1&pageSize=5", token=token)
  247. add(report, "s1_order_list", st, body)
  248. st, body = http("GET", f"/api/Order/delivery/list?billNo={BILL_NO}&page=1&pageSize=10", token=token)
  249. add(report, "s1_delivery_list", st, body)
  250. for mod in ("S1", "S2", "S3", "S4"):
  251. st, body = http("GET", f"/api/AidopKanban/module-detail?moduleCode={mod}", token=token)
  252. add(report, f"kanban_{mod}", st, body)
  253. with pymysql.connect(**CONN) as conn:
  254. report["db"]["after"] = {
  255. "wo": q1(
  256. conn,
  257. "SELECT WorkOrd, ItemNum, Status, BusinessID FROM WorkOrdMaster WHERE tenant_id=%s AND WorkOrd=%s",
  258. (TENANT, WORK_ORD),
  259. ),
  260. "routing_cnt": q1(
  261. conn,
  262. "SELECT COUNT(*) AS c FROM WorkOrdRouting WHERE tenant_id=%s AND WorkOrd=%s",
  263. (TENANT, WORK_ORD),
  264. ).get("c", 0),
  265. "detail_cnt": q1(
  266. conn,
  267. "SELECT COUNT(*) AS c FROM WorkOrdDetail WHERE tenant_id=%s AND WorkOrd=%s",
  268. (TENANT, WORK_ORD),
  269. ).get("c", 0),
  270. "demand": q1(conn, "SELECT COUNT(*) AS c FROM ic_demandschedule WHERE tenant_id=%s", (TENANT,)).get("c", 0),
  271. "pr": q1(
  272. conn,
  273. "SELECT COUNT(*) AS c FROM srm_pr_main WHERE tenant_id=%s AND IFNULL(IsDeleted,0)=0",
  274. (TENANT,),
  275. ).get("c", 0),
  276. "po": q1(
  277. conn,
  278. "SELECT COUNT(*) AS c FROM srm_po_main WHERE tenant_id=%s AND IFNULL(IsDeleted,0)=0",
  279. (TENANT,),
  280. ).get("c", 0),
  281. "dwd_readiness": q1(
  282. conn,
  283. "SELECT COUNT(*) AS c FROM dwd_material_readiness WHERE tenant_id=%s AND work_order=%s",
  284. (TENANT, WORK_ORD),
  285. ).get("c", 0),
  286. "entries": q(
  287. conn,
  288. """
  289. SELECT e.entry_seq, e.item_number, e.progress, w.WorkOrd, w.Status
  290. FROM crm_seorderentry e
  291. LEFT JOIN WorkOrdMaster w ON w.BusinessID=e.Id AND w.tenant_id=e.tenant_id
  292. WHERE e.seorder_id=%s AND e.IsDeleted=0
  293. ORDER BY e.entry_seq
  294. """,
  295. (ORDER_ID,),
  296. ),
  297. }
  298. # Checks
  299. steps = {s["step"]: s for s in report["steps"]}
  300. after = report["db"]["after"]
  301. checks = [
  302. {"name": "sync_routing_api", "ok": steps["s2_sync_routing"]["ok"]},
  303. {
  304. "name": "routing_rows_exist",
  305. "ok": (after.get("routing_cnt") or 0) >= 1,
  306. "detail": after.get("routing_cnt"),
  307. },
  308. {"name": "schedule_generate_api", "ok": steps["s2_schedule_generate_retry"]["ok"]},
  309. {
  310. "name": "schedule_has_rows_or_explained",
  311. "ok": steps["s2_schedule_generate_retry"]["ok"],
  312. "detail": steps["s2_schedule_generate_retry"].get("body"),
  313. },
  314. {"name": "s2_progress_list", "ok": steps["s2_progress_list"]["ok"]},
  315. {"name": "s3_purchase_request_list", "ok": steps["s3_purchase_request_list"]["ok"]},
  316. {"name": "s3_purchase_order_list", "ok": steps["s3_purchase_order_list"]["ok"]},
  317. {
  318. "name": "s3_material_readiness_list",
  319. "ok": steps["s3_material_readiness_list"]["ok"],
  320. "detail": steps["s3_material_readiness_list"].get("message")
  321. or steps["s3_material_readiness_list"].get("raw")
  322. or steps["s3_material_readiness_list"].get("body"),
  323. },
  324. {"name": "s3_kitting_refresh", "ok": steps["s3_kitting_refresh"]["ok"]},
  325. {"name": "s4_supplier_shipment_list", "ok": steps["s4_supplier_shipment_list"]["ok"]},
  326. {"name": "s4_mrp_generate", "ok": steps["s4_mrp_generate_again"]["ok"]},
  327. {"name": "s1_order_list", "ok": steps["s1_order_list"]["ok"]},
  328. {"name": "kanban_all", "ok": all(steps[f"kanban_{m}"]["ok"] for m in ("S1", "S2", "S3", "S4"))},
  329. {
  330. "name": "dispatch_release_or_blocked",
  331. "ok": steps["s2_dispatch_release"]["ok"]
  332. or (
  333. isinstance(steps["s2_dispatch_release"].get("body"), dict)
  334. and "物料" in str(steps["s2_dispatch_release"].get("body"))
  335. )
  336. or bool(steps["s2_dispatch_release"].get("message")),
  337. "detail": steps["s2_dispatch_release"].get("message") or steps["s2_dispatch_release"].get("body"),
  338. "note": "允许因缺料/工艺阻塞失败,但需有明确返回",
  339. },
  340. ]
  341. report["checks"] = checks
  342. report["passed"] = all(c["ok"] for c in checks)
  343. report["failed"] = [c["name"] for c in checks if not c["ok"]]
  344. report["step_failures"] = [s["step"] for s in report["steps"] if not s.get("ok")]
  345. out = r"d:\Projects\Ai-DOP\SourceCode\ZZYDOP\doc\_verify_s1_s4_continue_e2e_result.json"
  346. with open(out, "w", encoding="utf-8") as f:
  347. json.dump(report, f, ensure_ascii=False, indent=2, default=str)
  348. summary = {
  349. "passed": report["passed"],
  350. "failed": report["failed"],
  351. "step_failures": report["step_failures"],
  352. "db_before": report["db"]["before"],
  353. "db_after": {
  354. "wo": after.get("wo"),
  355. "routing_cnt": after.get("routing_cnt"),
  356. "detail_cnt": after.get("detail_cnt"),
  357. "demand": after.get("demand"),
  358. "pr": after.get("pr"),
  359. "po": after.get("po"),
  360. "dwd_readiness": after.get("dwd_readiness"),
  361. "entries": after.get("entries"),
  362. },
  363. "key_steps": {
  364. k: {
  365. "ok": steps[k]["ok"],
  366. "http": steps[k]["http"],
  367. "body": steps[k].get("body"),
  368. "message": steps[k].get("message"),
  369. }
  370. for k in (
  371. "s2_sync_routing",
  372. "s2_schedule_generate_retry",
  373. "s2_dispatch_release",
  374. "s3_material_readiness_list",
  375. "s3_kitting_refresh",
  376. "s3_delivery_generate",
  377. "s4_mrp_generate_again",
  378. "s4_supplier_shipment_list",
  379. )
  380. if k in steps
  381. },
  382. }
  383. print(json.dumps(summary, ensure_ascii=False, indent=2, default=str))
  384. sys.exit(0 if report["passed"] else 5)
  385. if __name__ == "__main__":
  386. main()