run_wp_sd0_baseline.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. #!/usr/bin/env python3
  2. """WP-SD0 baseline inventory for Smart Diagnosis formalization.
  3. Reusable: rerun this script to regenerate the same evidence set.
  4. All bigint IDs are serialized as strings to avoid JSON precision loss.
  5. The script never marks empty modules as passed.
  6. """
  7. from __future__ import annotations
  8. import json
  9. import re
  10. from datetime import date, datetime
  11. from decimal import Decimal
  12. from pathlib import Path
  13. import pymysql
  14. from openpyxl import Workbook
  15. from openpyxl.styles import Alignment, Font, PatternFill
  16. from openpyxl.utils import get_column_letter
  17. ROOT = Path(__file__).resolve().parents[4]
  18. CONFIG = ROOT / "server" / "Admin.NET.Application" / "Configuration" / "Database.json"
  19. EVIDENCE = ROOT / "doc" / "plan" / "UAT留证" / "2026-08-17-智慧诊断正式落地"
  20. MODULES = ("S1", "S2", "S3", "S4", "S5", "S6", "S7", "S9")
  21. TENANTS = {
  22. "A": {"code": "A", "name": "UATTEST_CHL", "tenant_id": 838257186181189},
  23. "B": {"code": "B", "name": "UATTEST_CHLB", "tenant_id": 838257212780613},
  24. "DEMO": {"code": "DEMO", "name": "UATDEMO", "tenant_id": 838257237606469},
  25. }
  26. INVALID_TENANTS = (0, 1, 1300000000001)
  27. FACT_TABLES = (
  28. ("dwd_requirement_examine_detail", "tenant_id"),
  29. ("dwd_ship_trans", "tenant_id"),
  30. ("dwd_order_schedule_trans", "tenant_id"),
  31. ("dwd_supplier_delivery", "tenant_id"),
  32. ("dwd_s4_purchase_execution", "tenant_id"),
  33. ("dwd_material_readiness", "tenant_id"),
  34. ("dwd_material_shortage", "tenant_id"),
  35. ("dwd_supplier_risk", "tenant_id"),
  36. ("dwd_qc_trans", "tenant_id"),
  37. ("mdp_std_so", "tenant_id"),
  38. ("mdp_std_s6_report", "tenant_id"),
  39. ("mdp_std_fqc_result", "tenant_id"),
  40. ("mdp_std_ipqc_inspection", "tenant_id"),
  41. ("mdp_std_s4_iqc", "tenant_id"),
  42. ("qms_qcp_inspbill", "tenant_id"),
  43. ("qms_gcjyd", "tenant_id"),
  44. ("qms_qcpp_inspbill", "tenant_id"),
  45. ("ado_s8_exception", "tenant_id"),
  46. ("ado_smart_ops_kpi_atomic_day", "tenant_id"),
  47. ("ado_smart_ops_improvement_plan", "TenantId"),
  48. )
  49. HEADER_FILL = PatternFill("solid", fgColor="1F4E79")
  50. HEADER_FONT = Font(color="FFFFFF", bold=True)
  51. WARN_FILL = PatternFill("solid", fgColor="FFF2CC")
  52. EMPTY_FILL = PatternFill("solid", fgColor="F4CCCC")
  53. def connect() -> pymysql.Connection:
  54. raw = CONFIG.read_text(encoding="utf-8-sig")
  55. value = next(
  56. item
  57. for item in re.findall(r'(?m)^\s*"ConnectionString"\s*:\s*"([^"]+)"', raw)
  58. if "Database=aidopdev" in item
  59. )
  60. parts = {
  61. item.split("=", 1)[0].strip().lower(): item.split("=", 1)[1].strip()
  62. for item in value.split(";")
  63. if "=" in item
  64. }
  65. return pymysql.connect(
  66. host=parts["server"],
  67. port=int(parts["port"]),
  68. user=parts["uid"],
  69. password=parts["pwd"],
  70. database=parts["database"],
  71. charset="utf8mb4",
  72. cursorclass=pymysql.cursors.DictCursor,
  73. )
  74. def json_safe(value: object) -> object:
  75. if isinstance(value, datetime):
  76. return value.isoformat(timespec="seconds")
  77. if isinstance(value, date):
  78. return value.isoformat()
  79. if isinstance(value, Decimal):
  80. return str(value)
  81. if isinstance(value, bytes):
  82. return value.decode("utf-8", errors="replace")
  83. if isinstance(value, dict):
  84. return {str(key): json_safe(item) for key, item in value.items()}
  85. if isinstance(value, (list, tuple)):
  86. return [json_safe(item) for item in value]
  87. if isinstance(value, int) and abs(value) > 2**53:
  88. return str(value)
  89. return value
  90. def scalar(cur: pymysql.cursors.DictCursor, sql: str, params: tuple = ()) -> int:
  91. cur.execute(sql, params)
  92. row = cur.fetchone() or {}
  93. return int(next(iter(row.values())) or 0)
  94. def fetch_all(cur: pymysql.cursors.DictCursor, sql: str, params: tuple = ()) -> list[dict]:
  95. cur.execute(sql, params)
  96. return [json_safe(row) for row in cur.fetchall()]
  97. def tenant_meta(cur: pymysql.cursors.DictCursor, tenant_id: int) -> dict:
  98. cur.execute(
  99. """
  100. SELECT CAST(Id AS CHAR) id, Title title, CAST(OrgId AS CHAR) org_id, Status status
  101. FROM SysTenant
  102. WHERE Id=%s
  103. """,
  104. (tenant_id,),
  105. )
  106. row = cur.fetchone()
  107. return json_safe(row) if row else {"id": str(tenant_id), "missing": True}
  108. def kpi_master(cur: pymysql.cursors.DictCursor, tenant_id: int) -> dict:
  109. by_module = fetch_all(
  110. cur,
  111. """
  112. SELECT ModuleCode module_code, MetricLevel metric_level,
  113. COUNT(*) metric_count,
  114. SUM(CASE WHEN IFNULL(IsEnabled,1)=1 THEN 1 ELSE 0 END) enabled_count
  115. FROM ado_smart_ops_kpi_master
  116. WHERE TenantId=%s
  117. GROUP BY ModuleCode, MetricLevel
  118. ORDER BY ModuleCode, MetricLevel
  119. """,
  120. (tenant_id,),
  121. )
  122. trees = fetch_all(
  123. cur,
  124. """
  125. SELECT p.ModuleCode module_code,
  126. p.MetricCode l1_code,
  127. p.MetricName l1_name,
  128. CAST(p.Id AS CHAR) l1_id,
  129. SUM(CASE WHEN c.MetricLevel=2 THEN 1 ELSE 0 END) l2_count,
  130. SUM(CASE WHEN c.MetricLevel=3 THEN 1 ELSE 0 END) l3_count,
  131. SUM(CASE WHEN c.MetricLevel=4 THEN 1 ELSE 0 END) l4_count
  132. FROM ado_smart_ops_kpi_master p
  133. LEFT JOIN ado_smart_ops_kpi_master c
  134. ON c.TenantId=p.TenantId AND c.ParentId=p.Id
  135. WHERE p.TenantId=%s AND p.MetricLevel=1
  136. GROUP BY p.ModuleCode, p.MetricCode, p.MetricName, p.Id
  137. ORDER BY p.ModuleCode, p.MetricCode
  138. """,
  139. (tenant_id,),
  140. )
  141. missing_modules = [
  142. module
  143. for module in MODULES
  144. if not any(row["module_code"] == module for row in by_module)
  145. ]
  146. incomplete_l1 = [
  147. row
  148. for row in trees
  149. if int(row["l2_count"] or 0) == 0
  150. ]
  151. return {
  152. "by_module_level": by_module,
  153. "l1_trees": trees,
  154. "missing_modules": missing_modules,
  155. "l1_without_l2": incomplete_l1,
  156. "total": sum(int(row["metric_count"]) for row in by_module),
  157. }
  158. def daily_values(cur: pymysql.cursors.DictCursor, tenant_id: int) -> dict:
  159. result: dict[str, object] = {}
  160. for level, table in (
  161. ("L1", "ado_s9_kpi_value_l1_day"),
  162. ("L2", "ado_s9_kpi_value_l2_day"),
  163. ("L3", "ado_s9_kpi_value_l3_day"),
  164. ("L4", "ado_s9_kpi_value_l4_day"),
  165. ):
  166. result[level] = fetch_all(
  167. cur,
  168. f"""
  169. SELECT module_code,
  170. COUNT(*) row_count,
  171. COUNT(DISTINCT metric_code) metric_count,
  172. MIN(biz_date) min_date,
  173. MAX(biz_date) max_date
  174. FROM {table}
  175. WHERE tenant_id=%s AND IFNULL(is_deleted,0)=0
  176. GROUP BY module_code
  177. ORDER BY module_code
  178. """,
  179. (tenant_id,),
  180. )
  181. result["atomic"] = fetch_all(
  182. cur,
  183. """
  184. SELECT domain_code, metric_code,
  185. COUNT(*) row_count,
  186. MIN(stat_date) min_date,
  187. MAX(stat_date) max_date,
  188. SUM(CASE WHEN IFNULL(customer_code,'')<>''
  189. OR IFNULL(product_code,'')<>''
  190. OR IFNULL(order_no,'')<>''
  191. OR IFNULL(supplier_code,'')<>''
  192. OR IFNULL(material_code,'')<>''
  193. OR IFNULL(work_order_no,'')<>''
  194. THEN 1 ELSE 0 END) dimensioned_rows,
  195. SUM(CASE WHEN IFNULL(grain,'') IN ('module','') THEN 1 ELSE 0 END) summary_rows
  196. FROM ado_smart_ops_kpi_atomic_day
  197. WHERE tenant_id=%s AND IFNULL(is_deleted,0)=0
  198. GROUP BY domain_code, metric_code
  199. ORDER BY domain_code, metric_code
  200. """,
  201. (tenant_id,),
  202. )
  203. return result
  204. def scope_proxy(atomic_rows: list[dict]) -> dict:
  205. """dataSource.scope is computed at query time; persist a data-side proxy."""
  206. total = sum(int(row["row_count"]) for row in atomic_rows)
  207. dimensioned = sum(int(row["dimensioned_rows"] or 0) for row in atomic_rows)
  208. summary = sum(int(row["summary_rows"] or 0) for row in atomic_rows)
  209. return {
  210. "note": "scope is API-computed; this is a data-side proxy from atomic grain/dimensions",
  211. "atomic_total": total,
  212. "module_summary_proxy": summary,
  213. "filtered_atomic_proxy": dimensioned,
  214. "partial_filtered_proxy": max(total - summary - dimensioned, 0),
  215. "empty": total == 0,
  216. }
  217. def improvement_plans(cur: pymysql.cursors.DictCursor, tenant_id: int) -> dict:
  218. plans = fetch_all(
  219. cur,
  220. """
  221. SELECT CAST(Id AS CHAR) id,
  222. CAST(TenantId AS CHAR) tenant_id,
  223. CAST(FactoryId AS CHAR) factory_id,
  224. PlanNo plan_no,
  225. ModuleCode module_code,
  226. MetricCode metric_code,
  227. ProblemLevel problem_level,
  228. ProblemName problem_name,
  229. Status status,
  230. CAST(OwnerUserId AS CHAR) owner_user_id,
  231. CAST(FlowInstanceId AS CHAR) flow_instance_id,
  232. VerifyResult verify_result,
  233. CASE
  234. WHEN ActionItemsJson IS NULL OR ActionItemsJson='' THEN 0
  235. ELSE 1
  236. END has_action_json,
  237. CHAR_LENGTH(IFNULL(ActionItemsJson,'')) action_json_chars,
  238. CreateTime create_time,
  239. UpdateTime update_time
  240. FROM ado_smart_ops_improvement_plan
  241. WHERE TenantId=%s
  242. ORDER BY CreateTime
  243. """,
  244. (tenant_id,),
  245. )
  246. by_status: dict[str, int] = {}
  247. for plan in plans:
  248. status = str(plan.get("status") or "UNKNOWN")
  249. by_status[status] = by_status.get(status, 0) + 1
  250. flow_ids = [int(plan["flow_instance_id"]) for plan in plans if plan.get("flow_instance_id")]
  251. approval = []
  252. if flow_ids:
  253. placeholders = ",".join(["%s"] * len(flow_ids))
  254. approval = fetch_all(
  255. cur,
  256. f"""
  257. SELECT CAST(Id AS CHAR) id,
  258. CAST(BizId AS CHAR) biz_id,
  259. Status status,
  260. CurrentNodeId current_node_id
  261. FROM ApprovalFlowInstance
  262. WHERE Id IN ({placeholders})
  263. """,
  264. tuple(flow_ids),
  265. )
  266. return {
  267. "count": len(plans),
  268. "by_status": by_status,
  269. "with_action_json": sum(int(plan["has_action_json"]) for plan in plans),
  270. "with_flow": len(flow_ids),
  271. "plans": plans,
  272. "approval_instances": approval,
  273. }
  274. def pollution(cur: pymysql.cursors.DictCursor) -> dict:
  275. rows = []
  276. for table, tenant_col in FACT_TABLES + (
  277. ("ado_s9_kpi_value_l1_day", "tenant_id"),
  278. ("ado_s9_kpi_value_l2_day", "tenant_id"),
  279. ("ado_s9_kpi_value_l3_day", "tenant_id"),
  280. ("ado_s9_kpi_value_l4_day", "tenant_id"),
  281. ("ado_smart_ops_kpi_master", "TenantId"),
  282. ):
  283. factory_col = None
  284. cur.execute(
  285. """
  286. SELECT COLUMN_NAME
  287. FROM information_schema.COLUMNS
  288. WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=%s
  289. AND COLUMN_NAME IN ('factory_id','FactoryId')
  290. """,
  291. (table,),
  292. )
  293. found = cur.fetchone()
  294. if found:
  295. factory_col = found["COLUMN_NAME"]
  296. invalid_sql = f"""
  297. SELECT COUNT(*) n
  298. FROM `{table}`
  299. WHERE `{tenant_col}` IS NULL OR `{tenant_col}` IN %s
  300. """
  301. invalid = scalar(cur, invalid_sql, (INVALID_TENANTS,))
  302. same_scope = 0
  303. if factory_col:
  304. same_scope = scalar(
  305. cur,
  306. f"""
  307. SELECT COUNT(*) n FROM `{table}`
  308. WHERE `{factory_col}` IS NOT NULL
  309. AND `{factory_col}`=`{tenant_col}`
  310. """,
  311. )
  312. rows.append(
  313. {
  314. "table": table,
  315. "invalid_tenant_rows": invalid,
  316. "factory_eq_tenant_rows": same_scope,
  317. }
  318. )
  319. return {
  320. "invalid_tenant_total": sum(int(row["invalid_tenant_rows"]) for row in rows),
  321. "factory_eq_tenant_total": sum(int(row["factory_eq_tenant_rows"]) for row in rows),
  322. "tables": rows,
  323. }
  324. def fact_counts(cur: pymysql.cursors.DictCursor, tenant_id: int) -> list[dict]:
  325. result = []
  326. for table, tenant_col in FACT_TABLES:
  327. count = scalar(
  328. cur,
  329. f"SELECT COUNT(*) n FROM `{table}` WHERE `{tenant_col}`=%s",
  330. (tenant_id,),
  331. )
  332. result.append({"table": table, "row_count": count, "empty": count == 0})
  333. return result
  334. def collect_tenant(cur: pymysql.cursors.DictCursor, spec: dict) -> dict:
  335. tenant_id = spec["tenant_id"]
  336. master = kpi_master(cur, tenant_id)
  337. values = daily_values(cur, tenant_id)
  338. facts = fact_counts(cur, tenant_id)
  339. empty_facts = [row["table"] for row in facts if row["empty"]]
  340. empty_value_modules = {
  341. level: [module for module in MODULES if not any(row["module_code"] == module for row in values[level])]
  342. for level in ("L1", "L2", "L3", "L4")
  343. }
  344. return {
  345. "code": spec["code"],
  346. "name": spec["name"],
  347. "tenant_id": str(tenant_id),
  348. "sys_tenant": tenant_meta(cur, tenant_id),
  349. "kpi_master": master,
  350. "daily_values": values,
  351. "scope_proxy": scope_proxy(values["atomic"]),
  352. "improvement_plans": improvement_plans(cur, tenant_id),
  353. "fact_tables": facts,
  354. "gaps": {
  355. "missing_kpi_modules": master["missing_modules"],
  356. "l1_without_l2": [row["l1_code"] for row in master["l1_without_l2"]],
  357. "empty_value_modules": empty_value_modules,
  358. "empty_fact_tables": empty_facts,
  359. },
  360. }
  361. def autofit(ws) -> None:
  362. for column in ws.columns:
  363. letter = get_column_letter(column[0].column)
  364. width = 12
  365. for cell in column:
  366. width = max(width, min(len(str(cell.value or "")), 48))
  367. ws.column_dimensions[letter].width = width + 2
  368. def style_header(ws) -> None:
  369. for cell in ws[1]:
  370. cell.fill = HEADER_FILL
  371. cell.font = HEADER_FONT
  372. cell.alignment = Alignment(horizontal="center")
  373. def write_excel(path: Path, sheets: dict[str, list[list[object]]]) -> None:
  374. wb = Workbook()
  375. first = True
  376. for name, rows in sheets.items():
  377. ws = wb.active if first else wb.create_sheet(name)
  378. if first:
  379. ws.title = name
  380. first = False
  381. for row in rows:
  382. ws.append(row)
  383. if rows:
  384. style_header(ws)
  385. for excel_row in ws.iter_rows(min_row=2):
  386. if any(str(cell.value) in {"0", "[]", "MISSING", "EMPTY"} for cell in excel_row):
  387. for cell in excel_row:
  388. if str(cell.value) in {"0", "[]", "MISSING", "EMPTY"}:
  389. cell.fill = EMPTY_FILL
  390. autofit(ws)
  391. path.parent.mkdir(parents=True, exist_ok=True)
  392. wb.save(path)
  393. def main() -> None:
  394. EVIDENCE.mkdir(parents=True, exist_ok=True)
  395. conn = connect()
  396. payload: dict[str, object] = {
  397. "generated_at": datetime.now().isoformat(timespec="seconds"),
  398. "script": "doc/plan/sql/uat-data/run_wp_sd0_baseline.py",
  399. "gate": "WP-SD0 baseline only; empty modules are listed, not passed",
  400. "tenants": {},
  401. "pollution": {},
  402. "empty_tables": [],
  403. "notes": [
  404. "dataSource.scope is computed at query time; baseline stores an atomic-grain proxy.",
  405. "All bigint IDs are strings.",
  406. "Do not treat this file as acceptance pass.",
  407. ],
  408. }
  409. try:
  410. with conn.cursor() as cur:
  411. for spec in TENANTS.values():
  412. tenant = collect_tenant(cur, spec)
  413. tenant["gaps"]["no_improvement_plans"] = tenant["improvement_plans"]["count"] == 0
  414. payload["tenants"][spec["code"]] = tenant
  415. payload["pollution"] = pollution(cur)
  416. finally:
  417. conn.close()
  418. empty_tables = []
  419. for tenant in payload["tenants"].values():
  420. empty_tables.extend(
  421. {
  422. "tenant": tenant["code"],
  423. "table": table,
  424. }
  425. for table in tenant["gaps"]["empty_fact_tables"]
  426. )
  427. for module in tenant["kpi_master"]["missing_modules"]:
  428. empty_tables.append({"tenant": tenant["code"], "table": f"kpi_master:{module}"})
  429. payload["empty_tables"] = empty_tables
  430. baseline_path = EVIDENCE / "00-baseline.json"
  431. baseline_path.write_text(
  432. json.dumps(json_safe(payload), ensure_ascii=False, indent=2),
  433. encoding="utf-8",
  434. )
  435. kpi_rows = [["tenant", "tenant_id", "module", "level", "metric_count", "enabled_count"]]
  436. tree_rows = [["tenant", "module", "l1_code", "l1_name", "l2_count", "l3_count", "l4_count", "tree_status"]]
  437. value_rows = [["tenant", "level", "module", "row_count", "metric_count", "min_date", "max_date"]]
  438. atomic_rows = [["tenant", "domain", "metric", "row_count", "dimensioned", "summary", "min_date", "max_date"]]
  439. gap_rows = [["tenant", "gap_type", "detail"]]
  440. for tenant in payload["tenants"].values():
  441. for row in tenant["kpi_master"]["by_module_level"]:
  442. kpi_rows.append(
  443. [
  444. tenant["code"],
  445. tenant["tenant_id"],
  446. row["module_code"],
  447. row["metric_level"],
  448. row["metric_count"],
  449. row["enabled_count"],
  450. ]
  451. )
  452. for row in tenant["kpi_master"]["l1_trees"]:
  453. status = "OK" if int(row["l2_count"] or 0) > 0 else "NO_L2"
  454. tree_rows.append(
  455. [
  456. tenant["code"],
  457. row["module_code"],
  458. row["l1_code"],
  459. row["l1_name"],
  460. row["l2_count"],
  461. row["l3_count"],
  462. row["l4_count"],
  463. status,
  464. ]
  465. )
  466. for level in ("L1", "L2", "L3", "L4"):
  467. for row in tenant["daily_values"][level]:
  468. value_rows.append(
  469. [
  470. tenant["code"],
  471. level,
  472. row["module_code"],
  473. row["row_count"],
  474. row["metric_count"],
  475. row["min_date"],
  476. row["max_date"],
  477. ]
  478. )
  479. for module in tenant["gaps"]["empty_value_modules"][level]:
  480. value_rows.append([tenant["code"], level, module, 0, 0, "", "",])
  481. gap_rows.append([tenant["code"], f"empty_{level}", module])
  482. for row in tenant["daily_values"]["atomic"]:
  483. atomic_rows.append(
  484. [
  485. tenant["code"],
  486. row["domain_code"],
  487. row["metric_code"],
  488. row["row_count"],
  489. row["dimensioned_rows"],
  490. row["summary_rows"],
  491. row["min_date"],
  492. row["max_date"],
  493. ]
  494. )
  495. for module in tenant["kpi_master"]["missing_modules"]:
  496. gap_rows.append([tenant["code"], "missing_kpi_module", module])
  497. for code in tenant["gaps"]["l1_without_l2"]:
  498. gap_rows.append([tenant["code"], "l1_without_l2", code])
  499. for table in tenant["gaps"]["empty_fact_tables"]:
  500. gap_rows.append([tenant["code"], "empty_fact_table", table])
  501. write_excel(
  502. EVIDENCE / "01-kpi-tree-inventory.xlsx",
  503. {
  504. "master": kpi_rows,
  505. "l1_trees": tree_rows,
  506. "daily_values": value_rows,
  507. "atomic": atomic_rows,
  508. "gaps": gap_rows,
  509. },
  510. )
  511. plan_rows = [[
  512. "tenant", "plan_id", "plan_no", "module", "metric", "status",
  513. "factory_id", "owner_user_id", "flow_instance_id", "has_action_json",
  514. "action_json_chars", "verify_result", "create_time",
  515. ]]
  516. approval_rows = [["tenant", "flow_id", "biz_id", "status", "current_node_id"]]
  517. fact_rows = [["tenant", "table", "row_count", "empty"]]
  518. pollution_rows = [["table", "invalid_tenant_rows", "factory_eq_tenant_rows"]]
  519. for tenant in payload["tenants"].values():
  520. plans = tenant["improvement_plans"]
  521. if not plans["plans"]:
  522. plan_rows.append([tenant["code"], "EMPTY", "", "", "", "", "", "", "", 0, 0, "", ""])
  523. for plan in plans["plans"]:
  524. plan_rows.append(
  525. [
  526. tenant["code"],
  527. plan["id"],
  528. plan["plan_no"],
  529. plan["module_code"],
  530. plan["metric_code"],
  531. plan["status"],
  532. plan["factory_id"],
  533. plan["owner_user_id"],
  534. plan["flow_instance_id"],
  535. plan["has_action_json"],
  536. plan["action_json_chars"],
  537. plan["verify_result"],
  538. plan["create_time"],
  539. ]
  540. )
  541. for row in plans["approval_instances"]:
  542. approval_rows.append(
  543. [tenant["code"], row["id"], row["biz_id"], row["status"], row["current_node_id"]]
  544. )
  545. for row in tenant["fact_tables"]:
  546. fact_rows.append([tenant["code"], row["table"], row["row_count"], "EMPTY" if row["empty"] else "OK"])
  547. for row in payload["pollution"]["tables"]:
  548. pollution_rows.append(
  549. [row["table"], row["invalid_tenant_rows"], row["factory_eq_tenant_rows"]]
  550. )
  551. write_excel(
  552. EVIDENCE / "02-improvement-plan-inventory.xlsx",
  553. {
  554. "plans": plan_rows,
  555. "approvals": approval_rows,
  556. "fact_tables": fact_rows,
  557. "pollution": pollution_rows,
  558. },
  559. )
  560. log_path = EVIDENCE / "00-baseline-run.log"
  561. log_path.write_text(
  562. "\n".join(
  563. [
  564. f"generated_at={payload['generated_at']}",
  565. f"script={payload['script']}",
  566. f"invalid_tenant_total={payload['pollution']['invalid_tenant_total']}",
  567. f"factory_eq_tenant_total={payload['pollution']['factory_eq_tenant_total']}",
  568. *[
  569. f"{code}: kpi={tenant['kpi_master']['total']} plans={tenant['improvement_plans']['count']} empty_facts={len(tenant['gaps']['empty_fact_tables'])}"
  570. for code, tenant in payload["tenants"].items()
  571. ],
  572. f"empty_entries={len(empty_tables)}",
  573. ]
  574. )
  575. + "\n",
  576. encoding="utf-8",
  577. )
  578. print(log_path.read_text(encoding="utf-8"))
  579. if __name__ == "__main__":
  580. main()