| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- #!/usr/bin/env python3
- """WP-SD7 module readiness inventory.
- Reusable. Never auto-passes a module.
- Checks L1→L2 (and L3/L4 when configured), operational-factory facts,
- KPI day coverage, S8 exception presence, and improvement-loop status.
- Live create→approve→execute→verify→close is recorded only when actually run.
- """
- from __future__ import annotations
- import json
- import re
- from datetime import date, datetime
- from decimal import Decimal
- from pathlib import Path
- import pymysql
- ROOT = Path(__file__).resolve().parents[4]
- CONFIG = ROOT / "server" / "Admin.NET.Application" / "Configuration" / "Database.json"
- EVIDENCE = ROOT / "doc" / "plan" / "UAT留证" / "2026-08-17-智慧诊断正式落地"
- MODULES = ("S1", "S2", "S3", "S4", "S5", "S6", "S7", "S9")
- TENANTS = {
- "A": {"code": "A", "name": "UATTEST_CHL", "tenant_id": 838257186181189},
- "B": {"code": "B", "name": "UATTEST_CHLB", "tenant_id": 838257212780613},
- "DEMO": {"code": "DEMO", "name": "UATDEMO", "tenant_id": 838257237606469},
- }
- MODULE_SOURCES = {
- "S1": ("order_schedule",),
- "S2": ("order_schedule",),
- "S3": ("supplier_delivery", "material_readiness"),
- "S4": ("supplier_delivery",),
- "S5": ("iqc_bill",),
- "S6": ("order_schedule", "s6_report"),
- "S7": ("fqc_result",),
- "S9": (
- "order_schedule",
- "supplier_delivery",
- "material_readiness",
- "s6_report",
- "iqc_bill",
- "fqc_result",
- ),
- }
- SOURCE_SQL = {
- "order_schedule": """
- SELECT COUNT(*) c FROM dwd_order_schedule_trans
- WHERE tenant_id=%s AND IFNULL(work_order,'')<>''
- AND (factory_id IS NULL OR factory_id=0 OR factory_id=1 OR factory_id=%s)
- AND (factory_id IS NULL OR factory_id<>%s)
- """,
- "supplier_delivery": """
- SELECT COUNT(*) c FROM dwd_supplier_delivery
- WHERE tenant_id=%s AND IFNULL(po_no,'')<>''
- AND (factory_id IS NULL OR factory_id=0 OR factory_id=1 OR factory_id=%s)
- AND (factory_id IS NULL OR factory_id<>%s)
- """,
- "material_readiness": """
- SELECT COUNT(*) c FROM dwd_material_readiness
- WHERE tenant_id=%s AND IFNULL(shortage_qty,0)>0
- AND (factory_id IS NULL OR factory_id=0 OR factory_id=1 OR factory_id=%s)
- AND (factory_id IS NULL OR factory_id<>%s)
- """,
- "s6_report": """
- SELECT COUNT(*) c FROM mdp_std_s6_report
- WHERE tenant_id=%s AND IFNULL(work_order_no,'')<>''
- AND (factory_id IS NULL OR factory_id=0 OR factory_id=1 OR factory_id=%s)
- AND (factory_id IS NULL OR factory_id<>%s)
- """,
- "iqc_bill": """
- SELECT COUNT(*) c FROM qms_qcp_inspbill
- WHERE tenant_id=%s AND IFNULL(FBILLNO,'')<>''
- """,
- "fqc_result": """
- SELECT COUNT(*) c FROM mdp_std_fqc_result
- WHERE tenant_id=%s AND IFNULL(bill_no,'')<>''
- """,
- }
- def connect() -> pymysql.Connection:
- raw = CONFIG.read_text(encoding="utf-8-sig")
- value = next(
- item
- for item in re.findall(r'(?m)^\s*"ConnectionString"\s*:\s*"([^"]+)"', raw)
- if "Database=aidopdev" in item
- )
- parts = {
- item.split("=", 1)[0].strip().lower(): item.split("=", 1)[1].strip()
- for item in value.split(";")
- if "=" in item
- }
- return pymysql.connect(
- host=parts["server"],
- port=int(parts["port"]),
- user=parts["uid"],
- password=parts["pwd"],
- database=parts["database"],
- charset="utf8mb4",
- cursorclass=pymysql.cursors.DictCursor,
- )
- def json_safe(value: object) -> object:
- if isinstance(value, datetime):
- return value.isoformat(timespec="seconds")
- if isinstance(value, date):
- return value.isoformat()
- if isinstance(value, Decimal):
- return str(value)
- if isinstance(value, dict):
- return {str(key): json_safe(item) for key, item in value.items()}
- if isinstance(value, (list, tuple)):
- return [json_safe(item) for item in value]
- if isinstance(value, int) and abs(value) > 2**53:
- return str(value)
- return value
- def scalar(cur: pymysql.cursors.DictCursor, sql: str, params: tuple = ()) -> int:
- cur.execute(sql, params)
- row = cur.fetchone() or {}
- return int(next(iter(row.values())) or 0)
- def fetch_all(cur: pymysql.cursors.DictCursor, sql: str, params: tuple = ()) -> list[dict]:
- cur.execute(sql, params)
- return [json_safe(row) for row in cur.fetchall()]
- def tenant_meta(cur: pymysql.cursors.DictCursor, tenant_id: int) -> dict:
- cur.execute(
- """
- SELECT CAST(Id AS CHAR) id, Title title, CAST(OrgId AS CHAR) org_id
- FROM SysTenant WHERE Id=%s
- """,
- (tenant_id,),
- )
- row = cur.fetchone()
- return json_safe(row) if row else {"id": str(tenant_id), "missing": True}
- def trees(cur: pymysql.cursors.DictCursor, tenant_id: int, module: str) -> list[dict]:
- return fetch_all(
- cur,
- """
- SELECT p.MetricCode l1_code, p.MetricName l1_name,
- COUNT(DISTINCT CASE WHEN c2.MetricLevel=2 THEN c2.Id END) l2_count,
- COUNT(DISTINCT CASE WHEN c3.MetricLevel=3 THEN c3.Id END) l3_count,
- COUNT(DISTINCT CASE WHEN c4.MetricLevel=4 THEN c4.Id END) l4_count
- FROM ado_smart_ops_kpi_master p
- LEFT JOIN ado_smart_ops_kpi_master c2
- ON c2.TenantId=p.TenantId AND c2.ParentId=p.Id AND c2.MetricLevel=2
- LEFT JOIN ado_smart_ops_kpi_master c3
- ON c3.TenantId=p.TenantId AND c3.ParentId=c2.Id AND c3.MetricLevel=3
- LEFT JOIN ado_smart_ops_kpi_master c4
- ON c4.TenantId=p.TenantId AND c4.ParentId=c3.Id AND c4.MetricLevel=4
- WHERE p.TenantId=%s AND p.ModuleCode=%s AND p.MetricLevel=1 AND IFNULL(p.IsEnabled,1)=1
- GROUP BY p.MetricCode, p.MetricName
- ORDER BY p.MetricCode
- """,
- (tenant_id, module),
- )
- def kpi_days(cur: pymysql.cursors.DictCursor, tenant_id: int, factory_id: int, module: str) -> dict:
- result = {}
- for level, table in (
- ("L1", "ado_s9_kpi_value_l1_day"),
- ("L2", "ado_s9_kpi_value_l2_day"),
- ("L3", "ado_s9_kpi_value_l3_day"),
- ("L4", "ado_s9_kpi_value_l4_day"),
- ):
- result[level] = fetch_all(
- cur,
- f"""
- SELECT COUNT(*) row_count, COUNT(DISTINCT metric_code) metric_count,
- MIN(biz_date) min_date, MAX(biz_date) max_date
- FROM {table}
- WHERE tenant_id=%s AND module_code=%s AND IFNULL(is_deleted,0)=0
- AND (factory_id IS NULL OR factory_id=0 OR factory_id=1 OR factory_id=%s)
- AND (factory_id IS NULL OR factory_id<>%s)
- """,
- (tenant_id, module, factory_id, tenant_id),
- )[0]
- return result
- def facts(cur: pymysql.cursors.DictCursor, tenant_id: int, factory_id: int, module: str) -> dict:
- out = {}
- for key in MODULE_SOURCES[module]:
- sql = SOURCE_SQL[key]
- if key in ("iqc_bill", "fqc_result"):
- out[key] = scalar(cur, sql, (tenant_id,))
- else:
- out[key] = scalar(cur, sql, (tenant_id, factory_id, tenant_id))
- return out
- def module_report(cur: pymysql.cursors.DictCursor, tenant: dict, factory_id: int, module: str) -> dict:
- tree = trees(cur, tenant["tenant_id"], module)
- complete = [row for row in tree if int(row["l2_count"] or 0) > 0]
- natural_stop = [row["l1_code"] for row in tree if int(row["l2_count"] or 0) == 0]
- with_l3 = [row["l1_code"] for row in tree if int(row["l3_count"] or 0) > 0]
- with_l4 = [row["l1_code"] for row in tree if int(row["l4_count"] or 0) > 0]
- day = kpi_days(cur, tenant["tenant_id"], factory_id, module)
- fact = facts(cur, tenant["tenant_id"], factory_id, module)
- fact_total = sum(fact.values())
- l1_rows = int(day["L1"]["row_count"] or 0)
- blockers = []
- if not complete:
- blockers.append("no_l1_l2_chain")
- if fact_total <= 0:
- blockers.append("no_registered_facts")
- if l1_rows <= 0 and int(day["L2"]["row_count"] or 0) <= 0:
- blockers.append("no_kpi_day_values")
- plans = fetch_all(
- cur,
- """
- SELECT CAST(Id AS CHAR) id, PlanNo plan_no, Status status, RootCause root_cause
- FROM ado_smart_ops_improvement_plan
- WHERE TenantId=%s AND ModuleCode=%s
- ORDER BY CreateTime DESC
- """,
- (tenant["tenant_id"], module),
- )
- closed_loop = any(
- row["status"] == "closed" and "[WP-SD7/" in str(row.get("root_cause") or "")
- for row in plans
- )
- if not closed_loop:
- blockers.append("improvement_loop_not_closed")
- return {
- "module": module,
- "l1_count": len(tree),
- "complete_l1_l2": [row["l1_code"] for row in complete],
- "natural_stop_no_l2": natural_stop,
- "l3_configured": with_l3,
- "l4_configured": with_l4,
- "trees": tree,
- "kpi_days": day,
- "facts": fact,
- "fact_total": fact_total,
- "plans": plans,
- "blockers": blockers,
- "verdict": "blocked" if blockers else "ready_for_signoff",
- "auto_passed": False,
- }
- def s8_report(cur: pymysql.cursors.DictCursor, tenant_id: int) -> dict:
- count = scalar(
- cur,
- "SELECT COUNT(*) c FROM ado_s8_exception WHERE tenant_id=%s AND IFNULL(is_deleted,0)=0",
- (tenant_id,),
- )
- linked = scalar(
- cur,
- """
- SELECT COUNT(*) c
- FROM ado_smart_ops_improvement_plan p
- WHERE p.TenantId=%s AND p.ModuleCode='S8'
- """,
- (tenant_id,),
- )
- blockers = []
- if count <= 0:
- blockers.append("no_s8_exception")
- if linked <= 0:
- blockers.append("no_s8_linked_plan")
- return {
- "exception_count": count,
- "linked_plan_count": linked,
- "blockers": blockers,
- "verdict": "blocked" if blockers else "ready_for_signoff",
- "note": "S8 does not copy KPI diagnosis tree",
- "auto_passed": False,
- }
- def isolation(cur: pymysql.cursors.DictCursor, tenant_ids: list[int]) -> dict:
- leaks = []
- for table, column in (
- ("dwd_order_schedule_trans", "tenant_id"),
- ("dwd_supplier_delivery", "tenant_id"),
- ("dwd_material_readiness", "tenant_id"),
- ("mdp_std_s6_report", "tenant_id"),
- ("qms_qcp_inspbill", "tenant_id"),
- ("mdp_std_fqc_result", "tenant_id"),
- ):
- for tenant_id in tenant_ids:
- other = [x for x in tenant_ids if x != tenant_id]
- placeholders = ",".join(["%s"] * len(other))
- count = scalar(
- cur,
- f"SELECT COUNT(*) c FROM {table} WHERE {column}=%s AND {column} IN ({placeholders})",
- (tenant_id, *other),
- )
- if count:
- leaks.append({"table": table, "tenant_id": str(tenant_id), "count": count})
- factory_eq_tenant = {}
- for table in (
- "dwd_order_schedule_trans",
- "dwd_supplier_delivery",
- "dwd_material_readiness",
- "mdp_std_s6_report",
- "ado_s9_kpi_value_l1_day",
- ):
- factory_eq_tenant[table] = fetch_all(
- cur,
- f"""
- SELECT CAST(tenant_id AS CHAR) tenant_id, COUNT(*) row_count
- FROM {table}
- WHERE tenant_id IN ({",".join(["%s"] * len(tenant_ids))})
- AND factory_id=tenant_id
- GROUP BY tenant_id
- """,
- tuple(tenant_ids),
- )
- return {"cross_tenant_impossible_leaks": leaks, "factory_eq_tenant": factory_eq_tenant}
- def main() -> None:
- EVIDENCE.mkdir(parents=True, exist_ok=True)
- conn = connect()
- try:
- cur = conn.cursor()
- tenants = []
- any_blocked = False
- for item in TENANTS.values():
- meta = tenant_meta(cur, item["tenant_id"])
- factory_id = int(meta.get("org_id") or 0)
- modules = [
- module_report(cur, item, factory_id, module) for module in MODULES
- ]
- s8 = s8_report(cur, item["tenant_id"])
- if any(row["verdict"] != "ready_for_signoff" for row in modules) or s8["verdict"] != "ready_for_signoff":
- any_blocked = True
- tenants.append(
- {
- "tenant": {**item, **meta, "factory_id": str(factory_id)},
- "modules": modules,
- "s8": s8,
- }
- )
- payload = {
- "work_package": "WP-SD7",
- "generated_at": datetime.now().isoformat(timespec="seconds"),
- "auto_passed": False,
- "overall_verdict": "blocked" if any_blocked else "ready_for_signoff",
- "must_not_pass": [
- "page_open_only",
- "kpi_without_facts",
- "other_tenant_facts",
- "hand_edited_status",
- "canned_root_cause",
- "s1_only_then_declare_all_done",
- ],
- "isolation": isolation(cur, [x["tenant_id"] for x in TENANTS.values()]),
- "tenants": tenants,
- }
- out = EVIDENCE / "07-module-readiness.json"
- out.write_text(json.dumps(json_safe(payload), ensure_ascii=False, indent=2), encoding="utf-8")
- print(out)
- print("overall_verdict=", payload["overall_verdict"])
- for tenant in tenants:
- print(tenant["tenant"]["name"], "S8", tenant["s8"]["verdict"])
- for module in tenant["modules"]:
- print(
- " ",
- module["module"],
- module["verdict"],
- "facts=",
- module["fact_total"],
- "blockers=",
- ",".join(module["blockers"]) or "-",
- )
- finally:
- conn.close()
- if __name__ == "__main__":
- main()
|