_verify_s1_s4_business_chain_e2e.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. #!/usr/bin/env python3
  2. """S1-S4 business chain E2E.
  3. S1 example path:
  4. 新建订单 -> 订单评审(含齐套/资源检查) -> 生成物料需求 -> 生产排程 -> 同步物料需求
  5. Then smoke S2/S3/S4 list + key actions.
  6. """
  7. from __future__ import annotations
  8. import json
  9. import sys
  10. import urllib.error
  11. import urllib.request
  12. from datetime import date, timedelta
  13. import pymysql
  14. BASE = "http://127.0.0.1:5005"
  15. TENANT = 797403760988229
  16. CONN = dict(
  17. host="123.60.180.165",
  18. port=3306,
  19. user="aidopremote",
  20. password="1234567890aiDOP#",
  21. database="aidopdev",
  22. charset="utf8mb4",
  23. connect_timeout=20,
  24. cursorclass=pymysql.cursors.DictCursor,
  25. )
  26. def http_json(method, path, body=None, token=None, timeout=180):
  27. headers = {"Content-Type": "application/json", "Accept": "application/json"}
  28. if token:
  29. headers["Authorization"] = f"Bearer {token}"
  30. data = None if body is None else json.dumps(body, ensure_ascii=False).encode("utf-8")
  31. req = urllib.request.Request(f"{BASE}{path}", data=data, headers=headers, method=method)
  32. try:
  33. with urllib.request.urlopen(req, timeout=timeout) as resp:
  34. raw = resp.read().decode("utf-8")
  35. try:
  36. return resp.status, json.loads(raw) if raw else {}
  37. except json.JSONDecodeError:
  38. return resp.status, {"raw": raw[:2000]}
  39. except urllib.error.HTTPError as e:
  40. raw = e.read().decode("utf-8", errors="replace")
  41. try:
  42. return e.code, json.loads(raw)
  43. except json.JSONDecodeError:
  44. return e.code, {"raw": raw[:2000]}
  45. except Exception as e:
  46. return 0, {"error": str(e)}
  47. def sm2_encrypt_password(plain: str) -> str:
  48. """Match frontend sm-crypto-v2 mode=1 (C1C3C2) + Admin.NET GMUtil.SM2Decrypt."""
  49. from gmssl import sm2
  50. pk = "0484C7466D950E120E5ECE5DD85D0C90EAA85081A3A2BD7C57AE6DC822EFCCBD66620C67B0103FC8DD280E36C3B282977B722AAEC3C56518EDCEBAFB72C5A05312"
  51. pub = pk[2:] if pk.startswith("04") else pk
  52. crypt = sm2.CryptSM2(public_key=pub, private_key=None, mode=1)
  53. cipher = crypt.encrypt(plain.encode("utf-8"))
  54. if isinstance(cipher, (bytes, bytearray)):
  55. hex_s = cipher.hex()
  56. else:
  57. hex_s = str(cipher)
  58. # gmssl usually returns without leading 04; backend accepts both
  59. return hex_s
  60. def login():
  61. payload = {
  62. "account": "AIDOPDemo",
  63. "password": sm2_encrypt_password("1234567890dop"),
  64. "tenantId": str(TENANT),
  65. }
  66. st, body = http_json("POST", "/api/sysAuth/login", payload)
  67. token = (body.get("result") or {}).get("accessToken") or body.get("accessToken")
  68. if not token:
  69. raise RuntimeError(f"login failed http={st}: {body}")
  70. return token
  71. def unwrap(body):
  72. if isinstance(body, dict) and body.get("code") == 200 and "result" in body:
  73. return body["result"]
  74. return body
  75. def api_ok(status, body):
  76. if status != 200:
  77. return False
  78. if isinstance(body, dict) and body.get("error"):
  79. return False
  80. if body.get("code") in (200, None) and body.get("type") in ("success", None):
  81. # Furion unify: code=200 type=success; NonUnify may omit
  82. if body.get("code") == 200 and body.get("type") == "success":
  83. return True
  84. if "result" in body or "message" in body or "Message" in body:
  85. return True
  86. # NonUnify plain object
  87. if body.get("code") is None and body.get("type") is None and not body.get("error"):
  88. return True
  89. # NonUnify services often return plain DTO
  90. if status == 200 and isinstance(body, dict) and "code" not in body:
  91. return True
  92. return False
  93. def step(report, name, status, body, extra=None):
  94. item = {
  95. "step": name,
  96. "http": status,
  97. "ok": api_ok(status, body) if status else False,
  98. "body": unwrap(body) if isinstance(body, dict) else body,
  99. }
  100. if extra:
  101. item.update(extra)
  102. if isinstance(body, dict) and body.get("message"):
  103. item["message"] = body.get("message")
  104. report["steps"].append(item)
  105. return item
  106. def q(conn, sql, params=None):
  107. with conn.cursor() as cur:
  108. cur.execute(sql, params or ())
  109. return cur.fetchall()
  110. def q1(conn, sql, params=None):
  111. rows = q(conn, sql, params)
  112. return rows[0] if rows else {}
  113. def snapshot(conn, order_id=None, bill_no=None):
  114. snap = {}
  115. if order_id:
  116. snap["order"] = q1(
  117. conn,
  118. "SELECT Id, bill_no, status FROM crm_seorder WHERE Id=%s AND tenant_id=%s AND IsDeleted=0",
  119. (order_id, TENANT),
  120. )
  121. snap["entries"] = q(
  122. conn,
  123. """
  124. SELECT Id, entry_seq, item_number, qty, progress, plan_date, sys_capacity_date
  125. FROM crm_seorderentry
  126. WHERE seorder_id=%s AND tenant_id=%s AND IsDeleted=0 ORDER BY entry_seq
  127. """,
  128. (order_id, TENANT),
  129. )
  130. entry_ids = [r["Id"] for r in snap["entries"]]
  131. if entry_ids:
  132. fmt = ",".join(["%s"] * len(entry_ids))
  133. snap["work_orders"] = q(
  134. conn,
  135. f"""
  136. SELECT RecID, WorkOrd, ItemNum, Status, BusinessID
  137. FROM WorkOrdMaster
  138. WHERE tenant_id=%s AND BusinessID IN ({fmt})
  139. ORDER BY WorkOrd
  140. """,
  141. [TENANT, *entry_ids],
  142. )
  143. snap["examine"] = q(
  144. conn,
  145. f"""
  146. SELECT Id, sentry_id, morder_no, need_qty, kitting_times, create_time
  147. FROM b_examine_result
  148. WHERE tenant_id=%s AND sentry_id IN ({fmt}) AND IFNULL(IsDeleted,0)=0
  149. ORDER BY create_time DESC LIMIT 10
  150. """,
  151. [TENANT, *entry_ids],
  152. )
  153. snap["examine_lines"] = q1(
  154. conn,
  155. f"""
  156. SELECT COUNT(*) AS c FROM b_bom_child_examine b
  157. INNER JOIN b_examine_result e ON e.Id=b.examine_id
  158. WHERE e.tenant_id=%s AND e.sentry_id IN ({fmt})
  159. AND IFNULL(e.IsDeleted,0)=0 AND IFNULL(b.is_use,1)=1
  160. """,
  161. [TENANT, *entry_ids],
  162. ).get("c", 0)
  163. else:
  164. snap["work_orders"] = []
  165. snap["examine"] = []
  166. snap["examine_lines"] = 0
  167. snap["demand"] = q1(
  168. conn, "SELECT COUNT(*) AS c FROM ic_demandschedule WHERE tenant_id=%s", (TENANT,)
  169. ).get("c", 0)
  170. snap["pr"] = q1(
  171. conn,
  172. "SELECT COUNT(*) AS c FROM srm_pr_main WHERE tenant_id=%s AND IFNULL(IsDeleted,0)=0",
  173. (TENANT,),
  174. ).get("c", 0)
  175. snap["po"] = q1(
  176. conn,
  177. "SELECT COUNT(*) AS c FROM srm_po_main WHERE tenant_id=%s AND IFNULL(IsDeleted,0)=0",
  178. (TENANT,),
  179. ).get("c", 0)
  180. snap["schedule_active"] = q1(
  181. conn,
  182. """
  183. SELECT COUNT(*) AS c FROM WorkOrdMaster
  184. WHERE tenant_id=%s AND Status IN ('p','r','w','s')
  185. """,
  186. (TENANT,),
  187. ).get("c", 0)
  188. if bill_no:
  189. snap["action_logs"] = q(
  190. conn,
  191. """
  192. SELECT id, action_code, biz_no, status, message, start_time
  193. FROM aidop_action_run_log
  194. WHERE tenant_id=%s AND (biz_no=%s OR biz_no LIKE %s OR action_code LIKE 'S1_%%' OR action_code LIKE 'S4_%%')
  195. ORDER BY start_time DESC LIMIT 8
  196. """,
  197. (TENANT, bill_no, f"%{bill_no}%"),
  198. )
  199. return snap
  200. def main():
  201. report = {
  202. "title": "S1-S4 business chain E2E",
  203. "base": BASE,
  204. "tenant_id": TENANT,
  205. "steps": [],
  206. "checks": [],
  207. }
  208. st, body = http_json("GET", "/api/sysAuth/captcha")
  209. step(report, "backend_health", st, body, {"ok": st == 200})
  210. if st != 200:
  211. report["passed"] = False
  212. print(json.dumps(report, ensure_ascii=False, indent=2, default=str))
  213. sys.exit(1)
  214. token = login()
  215. report["login"] = "ok"
  216. domain = str(TENANT)
  217. plan_date = (date.today() + timedelta(days=30)).isoformat()
  218. bill_suffix = date.today().strftime("%Y%m%d")
  219. # ── S1-1 新建订单 ──
  220. save_body = {
  221. "billNo": None,
  222. "orderType": 2,
  223. "customNo": "10000000",
  224. "customName": "瑞贝德",
  225. "date": date.today().isoformat(),
  226. "urgent": 0,
  227. "country": "中国",
  228. "entries": [
  229. {
  230. "entrySeq": 1,
  231. "itemNumber": "91H0DE3",
  232. "itemName": "通用内镜直线切割吻合器弯转型钉匣-ENDORMC6035R-中国中文-0",
  233. "qty": 10,
  234. "planDate": plan_date,
  235. "unit": "PCS",
  236. },
  237. {
  238. "entrySeq": 2,
  239. "itemNumber": "1A0049H",
  240. "itemName": "一次性使用RCS系列端端吻合器-RCS25D-英文包装前",
  241. "qty": 5,
  242. "planDate": plan_date,
  243. "unit": "PCS",
  244. },
  245. ],
  246. }
  247. st, body = http_json("POST", "/api/Order/seorder/save", save_body, token)
  248. save_res = unwrap(body) if isinstance(body, dict) else {}
  249. order_id = (save_res or {}).get("id") or (save_res or {}).get("Id")
  250. step(report, "s1_create_order", st, body, {"order_id": order_id})
  251. if not order_id:
  252. report["passed"] = False
  253. print(json.dumps(report, ensure_ascii=False, indent=2, default=str))
  254. sys.exit(2)
  255. with pymysql.connect(**CONN) as conn:
  256. order_row = q1(
  257. conn,
  258. "SELECT Id, bill_no FROM crm_seorder WHERE Id=%s AND tenant_id=%s",
  259. (order_id, TENANT),
  260. )
  261. bill_no = order_row.get("bill_no")
  262. report["bill_no"] = bill_no
  263. report["order_id"] = order_id
  264. report["before_review"] = snapshot(conn, order_id, bill_no)
  265. # ── S1-2 订单评审(资源检查/齐套 + 预备工单) ──
  266. st, body = http_json("POST", "/api/Order/seorder/review", {"ids": [order_id]}, token, timeout=300)
  267. review_res = unwrap(body) if isinstance(body, dict) else {}
  268. step(report, "s1_order_review", st, body)
  269. with pymysql.connect(**CONN) as conn:
  270. report["after_review"] = snapshot(conn, order_id, bill_no)
  271. # ── S1-3 物料齐套看板刷新 + 生成物料需求 ──
  272. st, body = http_json(
  273. "POST",
  274. "/api/Supply/work-order-material-readiness/refresh",
  275. None,
  276. token,
  277. timeout=300,
  278. )
  279. step(report, "s3_kitting_refresh", st, body)
  280. st, body = http_json(
  281. "POST",
  282. f"/api/WorkOrder/material-requirement/generate?domain={domain}",
  283. None,
  284. token,
  285. timeout=300,
  286. )
  287. step(report, "s1_generate_material_requirement", st, body)
  288. # ── S1-4 生产排程 + 同步物料需求 ──
  289. st, body = http_json(
  290. "POST",
  291. f"/api/Production/scheduling/generate?domain={domain}",
  292. None,
  293. token,
  294. timeout=300,
  295. )
  296. step(report, "s2_production_schedule", st, body)
  297. st, body = http_json(
  298. "POST",
  299. f"/api/WorkOrder/material-requirement/generate?domain={domain}",
  300. None,
  301. token,
  302. timeout=300,
  303. )
  304. step(report, "s2_sync_material_requirement", st, body)
  305. # ── S1 交期确认(若评审后 progress 已到 2/3 可继续) ──
  306. st, body = http_json("POST", "/api/Order/seorder/confirm-delivery", {"ids": [order_id]}, token)
  307. step(report, "s1_confirm_delivery", st, body)
  308. # ── S2 列表冒烟 ──
  309. st, body = http_json(
  310. "GET",
  311. f"/api/Production/scheduling/list?page=1&pageSize=10&domain={domain}",
  312. token=token,
  313. )
  314. step(report, "s2_scheduling_list", st, body)
  315. st, body = http_json(
  316. "GET",
  317. "/api/Production/work-order-progress/list?page=1&pageSize=10",
  318. token=token,
  319. )
  320. step(report, "s2_work_order_progress_list", st, body)
  321. # ── S3 列表冒烟 ──
  322. st, body = http_json("GET", "/api/Supply/purchase-request/list?page=1&pageSize=10", token=token)
  323. step(report, "s3_purchase_request_list", st, body)
  324. st, body = http_json("GET", "/api/Supply/purchase-order/list?page=1&pageSize=10", token=token)
  325. step(report, "s3_purchase_order_list", st, body)
  326. st, body = http_json(
  327. "GET",
  328. "/api/Supply/work-order-material-readiness/list?page=1&pageSize=10",
  329. token=token,
  330. )
  331. step(report, "s3_material_readiness_list", st, body)
  332. st, body = http_json("POST", "/api/Supply/delivery-schedule/generate", None, token, timeout=180)
  333. step(report, "s3_delivery_schedule_generate", st, body)
  334. # ── S4 列表冒烟 ──
  335. st, body = http_json(
  336. "GET",
  337. "/api/ProcurementExecution/supplier-shipment/list?page=1&pageSize=10",
  338. token=token,
  339. )
  340. step(report, "s4_supplier_shipment_list", st, body)
  341. # 看板 KPI
  342. for mod in ("S1", "S2", "S3", "S4"):
  343. st, body = http_json("GET", f"/api/AidopKanban/module-detail?moduleCode={mod}", token=token)
  344. step(report, f"kanban_{mod}", st, body)
  345. with pymysql.connect(**CONN) as conn:
  346. report["after"] = snapshot(conn, order_id, bill_no)
  347. after = report["after"]
  348. after_review = report["after_review"]
  349. checks = [
  350. {"name": "create_order", "ok": bool(order_id), "detail": bill_no},
  351. {
  352. "name": "review_api",
  353. "ok": next(s["ok"] for s in report["steps"] if s["step"] == "s1_order_review"),
  354. "detail": review_res.get("message") if isinstance(review_res, dict) else review_res,
  355. },
  356. {
  357. "name": "work_orders_after_review",
  358. "ok": len(after_review.get("work_orders") or []) >= 1,
  359. "detail": [w.get("WorkOrd") for w in (after_review.get("work_orders") or [])],
  360. },
  361. {
  362. "name": "resource_check_examine",
  363. "ok": len(after_review.get("examine") or []) >= 1
  364. or (after_review.get("examine_lines") or 0) >= 1
  365. or (isinstance(review_res, dict) and (review_res.get("resourceCheckCount") or 0) >= 1),
  366. "detail": {
  367. "examine": len(after_review.get("examine") or []),
  368. "lines": after_review.get("examine_lines"),
  369. "api": (review_res or {}).get("resourceCheckCount") if isinstance(review_res, dict) else None,
  370. },
  371. },
  372. {
  373. "name": "generate_material_requirement",
  374. "ok": next(s["ok"] for s in report["steps"] if s["step"] == "s1_generate_material_requirement"),
  375. },
  376. {
  377. "name": "production_schedule",
  378. "ok": next(s["ok"] for s in report["steps"] if s["step"] == "s2_production_schedule"),
  379. },
  380. {
  381. "name": "sync_material_requirement",
  382. "ok": next(s["ok"] for s in report["steps"] if s["step"] == "s2_sync_material_requirement"),
  383. },
  384. {
  385. "name": "s2_scheduling_list",
  386. "ok": next(s["ok"] for s in report["steps"] if s["step"] == "s2_scheduling_list"),
  387. },
  388. {
  389. "name": "s3_purchase_request_list",
  390. "ok": next(s["ok"] for s in report["steps"] if s["step"] == "s3_purchase_request_list"),
  391. },
  392. {
  393. "name": "s3_material_readiness_list",
  394. "ok": next(s["ok"] for s in report["steps"] if s["step"] == "s3_material_readiness_list"),
  395. },
  396. {
  397. "name": "s4_supplier_shipment_list",
  398. "ok": next(s["ok"] for s in report["steps"] if s["step"] == "s4_supplier_shipment_list"),
  399. },
  400. {
  401. "name": "demand_or_pr_after_mrp",
  402. "ok": (after.get("demand") or 0) > 0 or (after.get("pr") or 0) > 0,
  403. "detail": {"demand": after.get("demand"), "pr": after.get("pr"), "po": after.get("po")},
  404. },
  405. ]
  406. report["checks"] = checks
  407. report["passed"] = all(c["ok"] for c in checks)
  408. report["failed"] = [c["name"] for c in checks if not c["ok"]]
  409. report["step_failures"] = [s["step"] for s in report["steps"] if not s.get("ok")]
  410. out = r"d:\Projects\Ai-DOP\SourceCode\ZZYDOP\doc\_verify_s1_s4_business_chain_e2e_result.json"
  411. with open(out, "w", encoding="utf-8") as f:
  412. json.dump(report, f, ensure_ascii=False, indent=2, default=str)
  413. # compact console summary
  414. summary = {
  415. "passed": report["passed"],
  416. "bill_no": bill_no,
  417. "order_id": order_id,
  418. "failed": report["failed"],
  419. "step_failures": report["step_failures"],
  420. "checks": checks,
  421. "after": {
  422. "work_orders": [w.get("WorkOrd") for w in (after.get("work_orders") or [])],
  423. "entry_progress": [e.get("progress") for e in (after.get("entries") or [])],
  424. "demand": after.get("demand"),
  425. "pr": after.get("pr"),
  426. "po": after.get("po"),
  427. "examine_lines": after.get("examine_lines"),
  428. },
  429. }
  430. print(json.dumps(summary, ensure_ascii=False, indent=2, default=str))
  431. sys.exit(0 if report["passed"] else 5)
  432. if __name__ == "__main__":
  433. main()