_verify_s1_s4_full_e2e.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. #!/usr/bin/env python3
  2. """Full E2E: S1 confirm/refresh + S2 release success + S4 MRP PR + delivery schedule."""
  3. import json
  4. import subprocess
  5. import sys
  6. import urllib.error
  7. import urllib.request
  8. from datetime import date
  9. import pymysql
  10. BASE = "http://127.0.0.1:5005"
  11. TENANT = 797403760988229
  12. DOMAIN = "8010"
  13. ORDER_ID = 9106000400000002
  14. BILL_NO = "MPO482024102300001"
  15. WORK_ORDS = ["M500130841", "M500130842"]
  16. RELEASE_WO = "M500130841"
  17. SHORTAGE_ITEM = "034DD002"
  18. PO_SAMPLE = "PO-UAT-20260604-01"
  19. CONN = dict(
  20. host="123.60.180.165",
  21. port=3306,
  22. user="aidopremote",
  23. password="1234567890aiDOP#",
  24. database="aidopdev",
  25. charset="utf8mb4",
  26. connect_timeout=20,
  27. cursorclass=pymysql.cursors.DictCursor,
  28. )
  29. def http_json(method, path, body=None, token=None):
  30. headers = {"Content-Type": "application/json", "Accept": "application/json"}
  31. if token:
  32. headers["Authorization"] = f"Bearer {token}"
  33. data = None if body is None else json.dumps(body).encode("utf-8")
  34. req = urllib.request.Request(f"{BASE}{path}", data=data, headers=headers, method=method)
  35. try:
  36. with urllib.request.urlopen(req, timeout=120) as resp:
  37. raw = resp.read().decode("utf-8")
  38. try:
  39. return resp.status, json.loads(raw)
  40. except json.JSONDecodeError:
  41. return resp.status, {"raw": raw[:2000]}
  42. except urllib.error.HTTPError as e:
  43. raw = e.read().decode("utf-8", errors="replace")
  44. try:
  45. return e.code, json.loads(raw)
  46. except json.JSONDecodeError:
  47. return e.code, {"raw": raw[:2000]}
  48. def login():
  49. node_script = r"""
  50. const { sm2 } = require('sm-crypto-v2');
  51. const PK = '0484C7466D950E120E5ECE5DD85D0C90EAA85081A3A2BD7C57AE6DC822EFCCBD66620C67B0103FC8DD280E36C3B282977B722AAEC3C56518EDCEBAFB72C5A05312';
  52. console.log(JSON.stringify({ account: 'AIDOPDemo', password: sm2.doEncrypt('1234567890dop', PK, 1), tenantId: '797403760988229' }));
  53. """
  54. proc = subprocess.run(
  55. ["node", "-e", node_script],
  56. cwd=r"d:\Projects\Ai-DOP\SourceCode\ZZYDOP\Web",
  57. capture_output=True,
  58. text=True,
  59. timeout=30,
  60. )
  61. if proc.returncode != 0:
  62. raise RuntimeError(proc.stderr)
  63. status, body = http_json("POST", "/api/sysAuth/login", json.loads(proc.stdout.strip()))
  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 unwrap(body):
  69. if isinstance(body, dict) and body.get("code") == 200 and "result" in body:
  70. return body["result"]
  71. return body
  72. def pick_int(obj, *keys, default=0):
  73. if not isinstance(obj, dict):
  74. return default
  75. for k in keys:
  76. if k in obj and obj[k] is not None:
  77. try:
  78. return int(obj[k])
  79. except (TypeError, ValueError):
  80. pass
  81. return default
  82. def ensure_supplier_for_shortage_item(conn):
  83. """E2E 数据准备:缺料外购件补 srm_purchase 供应商(仅测试租户)。"""
  84. with conn.cursor() as cur:
  85. cur.execute(
  86. "SELECT COUNT(*) AS c FROM srm_purchase WHERE tenant_id=%s AND number=%s AND IFNULL(IsDeleted,0)=0",
  87. (TENANT, SHORTAGE_ITEM),
  88. )
  89. if cur.fetchone()["c"] > 0:
  90. return "already_exists"
  91. cur.execute(
  92. "SELECT Id, name FROM ic_item WHERE tenant_id=%s AND number=%s AND IsDeleted=0 LIMIT 1",
  93. (TENANT, SHORTAGE_ITEM),
  94. )
  95. ic = cur.fetchone()
  96. if not ic:
  97. return "no_ic_item"
  98. cur.execute(
  99. "SELECT * FROM srm_purchase WHERE tenant_id=%s AND IFNULL(IsDeleted,0)=0 AND supplier_id>0 LIMIT 1",
  100. (TENANT,),
  101. )
  102. tpl = cur.fetchone()
  103. if not tpl:
  104. return "no_template"
  105. cur.execute("SELECT IFNULL(MAX(Id),0)+1 AS nid FROM srm_purchase")
  106. new_id = cur.fetchone()["nid"]
  107. cur.execute(
  108. """
  109. INSERT INTO srm_purchase (
  110. Id, icitem_id, icitem_name, number, supplier_id, supplier_name, supplier_number,
  111. supplier_type, currency_type, lead_time, quota_rate, purchase_unit, IsRequireGoods,
  112. tenant_id, company_id, factory_id, is_active, effective_date, expiring_date,
  113. quota_priority, create_time, update_time, IsDeleted
  114. ) VALUES (
  115. %s, %s, %s, %s, %s, %s, %s,
  116. %s, %s, %s, %s, %s, %s,
  117. %s, %s, %s, %s, %s, %s,
  118. %s, NOW(), NOW(), 0
  119. )
  120. """,
  121. (
  122. new_id,
  123. ic["Id"],
  124. ic.get("name") or SHORTAGE_ITEM,
  125. SHORTAGE_ITEM,
  126. tpl["supplier_id"],
  127. tpl["supplier_name"],
  128. tpl["supplier_number"],
  129. tpl.get("supplier_type") or "标准",
  130. tpl.get("currency_type") or 1,
  131. tpl.get("lead_time") or 7,
  132. tpl.get("quota_rate") or 100,
  133. tpl.get("purchase_unit") or "PCS",
  134. tpl.get("IsRequireGoods") or 0,
  135. TENANT,
  136. tpl.get("company_id") or 1000,
  137. tpl.get("factory_id") or 2400,
  138. tpl.get("is_active") or "是",
  139. tpl.get("effective_date"),
  140. tpl.get("expiring_date"),
  141. tpl.get("quota_priority") or 0,
  142. ),
  143. )
  144. conn.commit()
  145. return "inserted"
  146. def prepare_e2e_state(conn):
  147. """E2E 前置:交期确认需 progress=2;下达需 Status=p 且无缺料。"""
  148. with conn.cursor() as cur:
  149. cur.execute(
  150. """
  151. UPDATE crm_seorderentry
  152. SET progress = '2', update_time = NOW()
  153. WHERE seorder_id = %s AND IsDeleted = 0
  154. """,
  155. (ORDER_ID,),
  156. )
  157. entry_rows = cur.rowcount
  158. cur.execute(
  159. """
  160. UPDATE WorkOrdMaster
  161. SET Status = 'p', ReleaseDate = NULL, LotSerial = NULL, UpdateTime = NOW()
  162. WHERE tenant_id = %s AND WorkOrd = %s
  163. """,
  164. (TENANT, RELEASE_WO),
  165. )
  166. wo_reset = cur.rowcount
  167. cur.execute(
  168. """
  169. UPDATE WorkOrdRouting
  170. SET Status = 'p', UpdateTime = NOW()
  171. WHERE tenant_id = %s AND WorkOrd = %s
  172. """,
  173. (TENANT, RELEASE_WO),
  174. )
  175. cur.execute(
  176. """
  177. DELETE FROM NbrMaster
  178. WHERE tenant_id = %s AND WorkOrd = %s AND Type = 'SM'
  179. """,
  180. (TENANT, RELEASE_WO),
  181. )
  182. pick_deleted = cur.rowcount
  183. conn.commit()
  184. return {"entry_progress_set": entry_rows, "wo_reset": wo_reset, "pick_bills_deleted": pick_deleted}
  185. def clear_shortage_for_release(conn, work_ord):
  186. """E2E:清零指定工单资源检查缺料,验证下达成功路径。"""
  187. with conn.cursor() as cur:
  188. cur.execute(
  189. """
  190. UPDATE b_bom_child_examine bce
  191. INNER JOIN b_examine_result ber ON ber.Id = bce.examine_id
  192. SET bce.lack_qty = 0, bce.self_lack_qty = 0, bce.update_time = NOW()
  193. WHERE ber.tenant_id = %s AND ber.morder_no = %s AND ber.IsDeleted = 0 AND bce.is_use = 1
  194. """,
  195. (TENANT, work_ord),
  196. )
  197. n = cur.rowcount
  198. cur.execute(
  199. """
  200. UPDATE mes_morder SET MaterialSituation = '齐套', update_time = NOW()
  201. WHERE tenant_id = %s AND morder_no = %s AND IsDeleted = 0
  202. """,
  203. (TENANT, work_ord),
  204. )
  205. conn.commit()
  206. return n
  207. def snapshot(conn):
  208. snap = {}
  209. with conn.cursor() as cur:
  210. cur.execute(
  211. "SELECT entry_seq, progress FROM crm_seorderentry WHERE seorder_id=%s ORDER BY entry_seq",
  212. (ORDER_ID,),
  213. )
  214. snap["entries"] = cur.fetchall()
  215. cur.execute(
  216. "SELECT WorkOrd, Status FROM WorkOrdMaster WHERE WorkOrd=%s AND tenant_id=%s",
  217. (RELEASE_WO, TENANT),
  218. )
  219. snap["release_wo"] = cur.fetchone()
  220. cur.execute(
  221. "SELECT COUNT(*) AS c FROM NbrMaster WHERE tenant_id=%s AND WorkOrd=%s AND Type='SM'",
  222. (TENANT, RELEASE_WO),
  223. )
  224. snap["pick_bills"] = cur.fetchone()["c"]
  225. cur.execute(
  226. """
  227. SELECT COUNT(*) AS c FROM srm_pr_main
  228. WHERE tenant_id=%s AND create_time >= DATE_SUB(NOW(), INTERVAL 2 HOUR)
  229. """,
  230. (TENANT,),
  231. )
  232. snap["recent_pr"] = cur.fetchone()["c"]
  233. cur.execute(
  234. """
  235. SELECT id, job_code, status, batch_id, start_time
  236. FROM mdp_transform_run_log
  237. WHERE job_code='S1_MDP_SYNC_TRANSFORM' AND start_time >= DATE_SUB(NOW(), INTERVAL 6 HOUR)
  238. ORDER BY start_time DESC LIMIT 3
  239. """
  240. )
  241. snap["s1_mdp_runs"] = cur.fetchall()
  242. cur.execute(
  243. "SELECT COUNT(*) AS c FROM dwd_ship_trans WHERE tenant_id=%s AND order_no=%s",
  244. (TENANT, BILL_NO),
  245. )
  246. snap["dwd_ship_rows"] = cur.fetchone()["c"]
  247. return snap
  248. def main():
  249. report = {"steps": [], "checks": []}
  250. token = login()
  251. report["login"] = "ok"
  252. with pymysql.connect(**CONN) as conn:
  253. report["supplier_seed"] = ensure_supplier_for_shortage_item(conn)
  254. report["e2e_prep"] = prepare_e2e_state(conn)
  255. report["before"] = snapshot(conn)
  256. # S1 交期确认
  257. st, body = http_json("POST", "/api/Order/seorder/confirm-delivery", {"ids": [ORDER_ID]}, token)
  258. report["steps"].append({"step": "confirm_delivery", "http": st, "body": unwrap(body)})
  259. # S4 MRP(缺料→PR)
  260. st, body = http_json("POST", f"/api/WorkOrder/material-requirement/generate?domain={TENANT}", None, token)
  261. mrp = unwrap(body)
  262. report["steps"].append({"step": "mrp_with_pr", "http": st, "body": mrp})
  263. # S2 下达成功路径(先清零缺料)
  264. with pymysql.connect(**CONN) as conn:
  265. cleared = clear_shortage_for_release(conn, RELEASE_WO)
  266. report["shortage_cleared_rows"] = cleared
  267. st, body = http_json(
  268. "POST",
  269. "/api/WorkOrder/dispatch/release",
  270. {
  271. "workOrd": RELEASE_WO,
  272. "tenantId": TENANT,
  273. "ordDate": date.today().isoformat(),
  274. "lotSerial": "E2E-FULL-001",
  275. },
  276. token,
  277. )
  278. report["steps"].append({"step": "release_success", "http": st, "body": unwrap(body)})
  279. # S3 交货计划生成(需求路径;无候选时回退 PO 批量)
  280. st, body = http_json("POST", "/api/Supply/delivery-schedule/generate", None, token)
  281. ds_body = unwrap(body)
  282. report["steps"].append({"step": "delivery_schedule_generate", "http": st, "body": ds_body})
  283. created = pick_int(ds_body, "createdCount", "CreatedCount")
  284. if created <= 0:
  285. st_b, body_b = http_json(
  286. "POST",
  287. "/api/Supply/delivery-schedule/batch-generate",
  288. {"poNumber": PO_SAMPLE},
  289. token,
  290. )
  291. report["steps"].append({"step": "delivery_schedule_batch_po", "http": st_b, "body": unwrap(body_b)})
  292. # S1 3级计划重排
  293. st, body = http_json(
  294. "POST",
  295. f"/api/Order/seorder/{ORDER_ID}/refresh-plan",
  296. {"reason": "E2E full chain refresh"},
  297. token,
  298. )
  299. report["steps"].append({"step": "refresh_plan", "http": st, "body": unwrap(body)})
  300. with pymysql.connect(**CONN) as conn:
  301. report["after"] = snapshot(conn)
  302. def ok(step, code=200):
  303. s = next((x for x in report["steps"] if x["step"] == step), None)
  304. return s is not None and s["http"] == code
  305. pr_created = pick_int(mrp, "prCreatedCount", "PrCreatedCount")
  306. recent_pr = report["after"].get("recent_pr") or 0
  307. report["checks"] = [
  308. {"name": "confirm_delivery", "ok": ok("confirm_delivery")},
  309. {
  310. "name": "mrp_pr_created",
  311. "ok": pr_created > 0 or recent_pr > 0,
  312. "detail": f"prCreatedCount={pr_created}, recent_pr={recent_pr}",
  313. },
  314. {"name": "release_success", "ok": ok("release_success")},
  315. {
  316. "name": "pick_bill_created",
  317. "ok": (report["after"].get("pick_bills") or 0) > 0,
  318. "detail": report["after"].get("pick_bills"),
  319. },
  320. {
  321. "name": "work_order_status_r",
  322. "ok": (report["after"].get("release_wo") or {}).get("Status") == "r",
  323. "detail": report["after"].get("release_wo"),
  324. },
  325. {
  326. "name": "work_order_was_reset_to_p",
  327. "ok": (report["before"].get("release_wo") or {}).get("Status") == "p",
  328. "detail": report["before"].get("release_wo"),
  329. },
  330. {"name": "delivery_schedule", "ok": ok("delivery_schedule_generate")},
  331. {
  332. "name": "delivery_schedule_or_batch",
  333. "ok": ok("delivery_schedule_generate")
  334. or ok("delivery_schedule_batch_po"),
  335. "detail": "需求生成或 PO 批量生成至少一路 HTTP 200",
  336. },
  337. {"name": "refresh_plan", "ok": ok("refresh_plan")},
  338. {
  339. "name": "s1_mdp_after_chain",
  340. "ok": len((report["after"].get("s1_mdp_runs") or [])) >= 1,
  341. "detail": report["after"].get("s1_mdp_runs"),
  342. },
  343. {
  344. "name": "dwd_ship_trans_main",
  345. "ok": (report["after"].get("dwd_ship_rows") or 0) >= 2,
  346. "detail": f"order_no={BILL_NO}, rows={report['after'].get('dwd_ship_rows')}",
  347. },
  348. ]
  349. report["passed"] = all(c["ok"] for c in report["checks"])
  350. out = r"d:\Projects\Ai-DOP\SourceCode\ZZYDOP\doc\_verify_s1_s4_full_e2e_result.json"
  351. with open(out, "w", encoding="utf-8") as f:
  352. json.dump(report, f, ensure_ascii=False, indent=2, default=str)
  353. print(json.dumps(report, ensure_ascii=False, indent=2, default=str))
  354. sys.exit(0 if report["passed"] else 5)
  355. if __name__ == "__main__":
  356. main()