run_uat_t8_fact_seed.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. #!/usr/bin/env python3
  2. """Seed tenant-scoped UAT T8 facts into the MDP staging layer.
  3. The script intentionally stops at ``mdp_stg_t8_*``. S5/S6/S7 module rebuilds
  4. must perform STG -> STD -> DWD -> KPI conversion afterwards.
  5. Default window is the **current calendar month** (unique ``YYYYMM`` row ids) so
  6. an August run writes August facts without touching July keys, and a September
  7. run can seed September the same way.
  8. """
  9. from __future__ import annotations
  10. import argparse
  11. import json
  12. from datetime import date, datetime, time, timedelta
  13. from typing import Any
  14. from apply_sql_file import connect
  15. TENANTS = (
  16. ("A", 838257186181189, 10, 3, 8, 0.90),
  17. ("B", 838257212780613, 6, 2, 5, 0.75),
  18. ("DEMO", 838257237606469, 30, 5, 16, 0.83),
  19. )
  20. SOURCE_SYSTEM = "UAT_GENERATOR"
  21. ZTID = "pbxfxp"
  22. TABLES = {
  23. "tz_head": "mdp_stg_t8_kc_tz_head",
  24. "tz_list": "mdp_stg_t8_kc_tz_list",
  25. "dd_head": "mdp_stg_t8_kc_dd_head",
  26. "dd_list": "mdp_stg_t8_kc_dd_list",
  27. "zj_list": "mdp_stg_t8_kc_zj_list",
  28. "pelist": "mdp_stg_t8_sys_pelist",
  29. "bg_head": "mdp_stg_t8_cj_bg_head_rep",
  30. "cllist": "mdp_stg_t8_kc_dd_list_cllist",
  31. }
  32. def current_month_window(today: date) -> tuple[date, date]:
  33. start = today.replace(day=1)
  34. if start.month == 12:
  35. nxt = start.replace(year=start.year + 1, month=1, day=1)
  36. else:
  37. nxt = start.replace(month=start.month + 1, day=1)
  38. return start, nxt - timedelta(days=1)
  39. def clamp_day(day: date, month_end: date) -> date:
  40. return day if day <= month_end else month_end
  41. def dt(day: date, hour: int = 8) -> str:
  42. return datetime.combine(day, time(hour=hour)).isoformat(timespec="seconds")
  43. def stage_row(
  44. rows: list[tuple[str, int, str, str, dict[str, Any]]],
  45. table_key: str,
  46. tenant_id: int,
  47. row_id: str,
  48. raw: dict[str, Any],
  49. ) -> None:
  50. rows.append((TABLES[table_key], tenant_id, row_id, row_id, raw))
  51. def seed_staff(
  52. rows: list[tuple[str, int, str, str, dict[str, Any]]],
  53. prefix: str,
  54. tenant_id: int,
  55. id_base: int,
  56. warehouse_staff: int,
  57. production_staff: int,
  58. ) -> None:
  59. for index in range(warehouse_staff):
  60. rid = f"{prefix}-PE-WH-{index + 1:03d}"
  61. stage_row(rows, "pelist", tenant_id, rid, {
  62. "Id": id_base + 1_000 + index,
  63. "ztid": ZTID,
  64. "zzzt": "在职",
  65. "gw": "仓管",
  66. })
  67. for index in range(production_staff):
  68. rid = f"{prefix}-PE-PROD-{index + 1:03d}"
  69. stage_row(rows, "pelist", tenant_id, rid, {
  70. "Id": id_base + 2_000 + index,
  71. "ztid": ZTID,
  72. "zzzt": "在职",
  73. "gw": "生产",
  74. })
  75. for index in range(2):
  76. rid = f"{prefix}-PE-PLAN-{index + 1:03d}"
  77. stage_row(rows, "pelist", tenant_id, rid, {
  78. "Id": id_base + 3_000 + index,
  79. "ztid": ZTID,
  80. "zzzt": "在职",
  81. "gw": "计划",
  82. })
  83. def seed_month(
  84. rows: list[tuple[str, int, str, str, dict[str, Any]]],
  85. *,
  86. code: str,
  87. prefix: str,
  88. tenant_id: int,
  89. id_base: int,
  90. order_count: int,
  91. on_time_ratio: float,
  92. month_start: date,
  93. month_end: date,
  94. ) -> None:
  95. month_tag = month_start.strftime("%Y%m")
  96. month_shift = ((month_start.year * 12 + month_start.month) % 100) * 500_000
  97. span = max(month_end.day - 8, 1)
  98. for index in range(order_count):
  99. seq = index + 1
  100. product = f"{prefix}-FG-{seq % 5 + 1:02d}"
  101. material = f"{prefix}-MAT-{seq % 8 + 1:02d}"
  102. work_order = f"{prefix}-WO-{month_tag}-{seq:03d}"
  103. task_no = f"{work_order}-TASK"
  104. sales_order = f"{prefix}-SO-{month_tag}-{seq:03d}"
  105. sales_line = f"{sales_order}-L1"
  106. qty = 80 + (seq % 6) * 20
  107. offset = min((seq * 2) % span, max(month_end.day - 9, 0))
  108. receipt_day = clamp_day(month_start + timedelta(days=offset), month_end)
  109. issue_day = clamp_day(receipt_day + timedelta(days=2), month_end)
  110. start_day = clamp_day(issue_day + timedelta(days=1), month_end)
  111. plan_finish = clamp_day(start_day + timedelta(days=4), month_end)
  112. is_on_time = (seq / order_count) <= on_time_ratio
  113. finish_day = clamp_day(
  114. plan_finish - timedelta(days=1) if is_on_time else plan_finish + timedelta(days=2),
  115. month_end,
  116. )
  117. fqc_day = finish_day
  118. ship_day = clamp_day(finish_day + timedelta(days=1), month_end)
  119. return_day = clamp_day(ship_day + timedelta(days=2), month_end)
  120. prod_head_id = id_base + month_shift + 10_000 + seq
  121. prod_line_id = id_base + month_shift + 20_000 + seq
  122. sales_head_id = id_base + month_shift + 30_000 + seq
  123. sales_line_id = id_base + month_shift + 40_000 + seq
  124. stage_row(rows, "dd_head", tenant_id, f"{prefix}-DDH-PROD-{month_tag}-{seq:03d}", {
  125. "Id": prod_head_id,
  126. "ztid": ZTID,
  127. "lbs": "生产任务",
  128. "zf": 0,
  129. "shyn": 1,
  130. "noid": work_order,
  131. })
  132. stage_row(rows, "dd_list", tenant_id, f"{prefix}-DDL-PROD-{month_tag}-{seq:03d}", {
  133. "Id": prod_line_id,
  134. "idid": prod_head_id,
  135. "rwnoid": task_no,
  136. "code": product,
  137. "sl": qty,
  138. "slzx": qty if is_on_time else round(qty * 0.72, 2),
  139. "jhdate": plan_finish.isoformat(),
  140. "gdyn": 1 if is_on_time else 0,
  141. "gdtime": dt(finish_day, 17),
  142. "addtime": dt(start_day, 8),
  143. })
  144. stage_row(rows, "dd_head", tenant_id, f"{prefix}-DDH-SALES-{month_tag}-{seq:03d}", {
  145. "Id": sales_head_id,
  146. "ztid": ZTID,
  147. "lbs": "销售订单",
  148. "zf": 0,
  149. "shyn": 1,
  150. "noid": sales_order,
  151. })
  152. stage_row(rows, "dd_list", tenant_id, f"{prefix}-DDL-SALES-{month_tag}-{seq:03d}", {
  153. "Id": sales_line_id,
  154. "idid": sales_head_id,
  155. "rwnoid": sales_line,
  156. "code": product,
  157. "sl": qty,
  158. "slzx": qty,
  159. "jhdate": (ship_day + timedelta(days=1 if is_on_time else -1)).isoformat(),
  160. "gdyn": 1,
  161. "gdtime": dt(ship_day, 17),
  162. "addtime": dt(start_day, 8),
  163. })
  164. stage_row(rows, "cllist", tenant_id, f"{prefix}-CL-{month_tag}-{seq:03d}", {
  165. "Id": id_base + month_shift + 50_000 + seq,
  166. "idid": prod_head_id,
  167. })
  168. stage_row(rows, "bg_head", tenant_id, f"{prefix}-BG-{month_tag}-{seq:03d}", {
  169. "Id": id_base + month_shift + 60_000 + seq,
  170. "noid": work_order,
  171. "kgdate": dt(start_day, 8),
  172. "ztid": ZTID,
  173. })
  174. stage_row(rows, "zj_list", tenant_id, f"{prefix}-FQC-{month_tag}-{seq:03d}", {
  175. "Id": id_base + month_shift + 70_000 + seq,
  176. "ztid": ZTID,
  177. "lyid": sales_line_id,
  178. "zjyn": 1,
  179. "shdate": dt(fqc_day, 9),
  180. })
  181. quality_return_ratio = (
  182. 0.003 if code == "A"
  183. else 0.008 if code == "B"
  184. else (0.002, 0.006, 0.012)[(seq - 1) % 3]
  185. )
  186. transactions = (
  187. ("采购入库", material, "", receipt_day, qty * 2),
  188. ("生产领料", material, work_order, issue_day, qty * 1.6),
  189. ("生产入库", product, task_no, finish_day, qty if is_on_time else qty * 0.72),
  190. ("销售出库", product, sales_line, ship_day, qty),
  191. ("销售退货", product, sales_line, return_day, qty * quality_return_ratio),
  192. )
  193. for tx_index, (tx_type, item, linkage, tx_day, actual_qty) in enumerate(transactions, 1):
  194. tx_head_id = id_base + month_shift + 100_000 + seq * 10 + tx_index
  195. tx_key = f"{prefix}-TZ-{month_tag}-{seq:03d}-{tx_index}"
  196. stage_row(rows, "tz_head", tenant_id, f"{tx_key}-H", {
  197. "Id": tx_head_id,
  198. "ztid": ZTID,
  199. "lbs": tx_type,
  200. "lynoid": work_order if tx_type == "生产领料" else "",
  201. "hzyn": 0,
  202. "zfyn": 0,
  203. "shyn": 1,
  204. "shtime": dt(tx_day, 16),
  205. "date0": tx_day.isoformat(),
  206. })
  207. stage_row(rows, "tz_list", tenant_id, f"{tx_key}-L", {
  208. "Id": id_base + month_shift + 200_000 + seq * 10 + tx_index,
  209. "idid": tx_head_id,
  210. "code": item,
  211. "lynoid": linkage,
  212. "slzx": round(actual_qty, 2),
  213. "sl": qty,
  214. "gdyn": 1 if actual_qty >= qty else 0,
  215. "gdtime": dt(tx_day, 16),
  216. "rwnoid": task_no,
  217. "jhdate": plan_finish.isoformat(),
  218. "addtime": dt(tx_day, 8),
  219. })
  220. def build_rows(today: date, tenant_codes: set[str] | None = None) -> list[tuple[str, int, str, str, dict[str, Any]]]:
  221. month_start, month_end = current_month_window(today)
  222. rows: list[tuple[str, int, str, str, dict[str, Any]]] = []
  223. selected = [
  224. item for item in TENANTS
  225. if not tenant_codes or item[0] in tenant_codes
  226. ]
  227. for code, tenant_id, order_count, warehouse_staff, production_staff, on_time_ratio in selected:
  228. prefix = f"UAT-T8-{code}"
  229. id_base = tenant_id % 1_000_000_000 * 100_000
  230. seed_staff(rows, prefix, tenant_id, id_base, warehouse_staff, production_staff)
  231. seed_month(
  232. rows,
  233. code=code,
  234. prefix=prefix,
  235. tenant_id=tenant_id,
  236. id_base=id_base,
  237. order_count=order_count,
  238. on_time_ratio=on_time_ratio,
  239. month_start=month_start,
  240. month_end=month_end,
  241. )
  242. return rows
  243. def apply(rows: list[tuple[str, int, str, str, dict[str, Any]]]) -> dict[str, int]:
  244. batch_id = f"UAT_T8_FACT_{datetime.now():%Y%m%d%H%M%S}"
  245. counts: dict[str, int] = {}
  246. conn = connect()
  247. try:
  248. conn.begin()
  249. with conn.cursor() as cursor:
  250. for table, tenant_id, row_id, biz_key, raw in rows:
  251. cursor.execute(
  252. f"""
  253. INSERT INTO {table}
  254. (tenant_id,source_system,source_table,source_row_id,source_biz_key,
  255. raw_data,sync_batch_id,sync_time,process_status,process_message)
  256. VALUES
  257. (%s,%s,%s,%s,%s,%s,%s,NOW(),'PENDING',NULL)
  258. ON DUPLICATE KEY UPDATE
  259. tenant_id=VALUES(tenant_id),
  260. source_row_id=VALUES(source_row_id),
  261. raw_data=VALUES(raw_data),
  262. sync_batch_id=VALUES(sync_batch_id),
  263. sync_time=NOW(),
  264. process_status='PENDING',
  265. process_message=NULL,
  266. update_time=NOW()
  267. """,
  268. (
  269. tenant_id,
  270. SOURCE_SYSTEM,
  271. table,
  272. row_id,
  273. biz_key,
  274. json.dumps(raw, ensure_ascii=False),
  275. batch_id,
  276. ),
  277. )
  278. counts[table] = counts.get(table, 0) + 1
  279. conn.commit()
  280. except Exception:
  281. conn.rollback()
  282. raise
  283. finally:
  284. conn.close()
  285. return counts
  286. def main() -> int:
  287. parser = argparse.ArgumentParser()
  288. parser.add_argument("--apply", action="store_true", help="write staging rows")
  289. parser.add_argument("--as-of", help="YYYY-MM-DD; default today. Controls which calendar month is seeded.")
  290. parser.add_argument("--tenants", default="A", help="comma-separated tenant codes: A,B,DEMO")
  291. args = parser.parse_args()
  292. as_of = date.fromisoformat(args.as_of) if args.as_of else date.today()
  293. tenant_codes = {x.strip().upper() for x in args.tenants.split(",") if x.strip()}
  294. rows = build_rows(as_of, tenant_codes)
  295. month_start, month_end = current_month_window(as_of)
  296. summary: dict[str, Any] = {
  297. "sourceSystem": SOURCE_SYSTEM,
  298. "asOf": as_of.isoformat(),
  299. "window": f"{month_start.isoformat()}~{month_end.isoformat()}",
  300. "tenantCodes": sorted(tenant_codes),
  301. "rowCount": len(rows),
  302. "tables": {},
  303. "mode": "apply" if args.apply else "dry-run",
  304. }
  305. for table, *_ in rows:
  306. summary["tables"][table] = summary["tables"].get(table, 0) + 1
  307. if args.apply:
  308. summary["applied"] = apply(rows)
  309. print(json.dumps(summary, ensure_ascii=False, indent=2))
  310. return 0
  311. if __name__ == "__main__":
  312. raise SystemExit(main())