run_uat_shipping_fact_seed.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. #!/usr/bin/env python3
  2. """Create tenant-scoped UAT sales shipment business facts.
  3. Facts are written to the normal ASN business tables. A subsequent S1 rebuild
  4. must pull them through mdp_stg_ship_trans -> mdp_std_ship_trans -> DWD -> KPI.
  5. """
  6. from __future__ import annotations
  7. import argparse
  8. import json
  9. from collections import defaultdict
  10. from datetime import datetime, timedelta
  11. from decimal import Decimal
  12. from typing import Any
  13. from apply_sql_file import connect
  14. TENANTS = {
  15. 838257186181189: "A",
  16. 838257212780613: "B",
  17. 838257237606469: "DEMO",
  18. }
  19. PREFIX = "UAT-SHIP-"
  20. def shipment_ratio(code: str, order_no: str, index: int) -> Decimal:
  21. if code == "A":
  22. scenario = next((x for x in ("A01", "A02", "A03", "A04", "A05") if x in order_no), "")
  23. return {
  24. "A01": Decimal("1.00"),
  25. "A02": Decimal("0.60"),
  26. "A03": Decimal("0.20"),
  27. "A04": Decimal("0.80"),
  28. "A05": Decimal("1.00"),
  29. }.get(scenario, Decimal("0.75"))
  30. if code == "B":
  31. return Decimal("0.65")
  32. return (Decimal("1.00"), Decimal("0.82"), Decimal("0.55"), Decimal("0.25"))[index % 4]
  33. def load_order_lines(cursor: Any) -> dict[int, list[dict[str, Any]]]:
  34. tenant_ids = ",".join(str(x) for x in TENANTS)
  35. cursor.execute(
  36. f"""
  37. SELECT tenant_id,order_no,order_line,item_code,item_name,order_qty
  38. FROM mdp_std_so
  39. WHERE tenant_id IN ({tenant_ids})
  40. AND IFNULL(order_no,'')<>''
  41. AND IFNULL(item_code,'')<>''
  42. AND IFNULL(order_qty,0)>0
  43. ORDER BY tenant_id,order_no,CAST(order_line AS UNSIGNED),item_code
  44. """
  45. )
  46. result: dict[int, list[dict[str, Any]]] = defaultdict(list)
  47. for row in cursor.fetchall():
  48. result[int(row["tenant_id"])].append(row)
  49. return result
  50. def apply() -> dict[str, Any]:
  51. conn = connect()
  52. summary: dict[str, Any] = {"masters": 0, "details": 0, "tenants": {}}
  53. try:
  54. conn.begin()
  55. with conn.cursor() as cursor:
  56. lines_by_tenant = load_order_lines(cursor)
  57. tenant_ids = tuple(TENANTS)
  58. placeholders = ",".join(["%s"] * len(tenant_ids))
  59. cursor.execute(
  60. f"DELETE FROM ASNBOLShipperDetail WHERE tenant_id IN ({placeholders}) AND Id LIKE %s",
  61. (*tenant_ids, f"{PREFIX}%"),
  62. )
  63. cursor.execute(
  64. f"DELETE FROM ASNBOLShipperMaster WHERE tenant_id IN ({placeholders}) AND Id LIKE %s",
  65. (*tenant_ids, f"{PREFIX}%"),
  66. )
  67. now = datetime.now().replace(microsecond=0)
  68. for tenant_id, code in TENANTS.items():
  69. grouped: dict[str, list[dict[str, Any]]] = defaultdict(list)
  70. for row in lines_by_tenant.get(tenant_id, []):
  71. grouped[str(row["order_no"])].append(row)
  72. tenant_summary = {"orders": 0, "lines": 0}
  73. for order_index, (order_no, order_lines) in enumerate(grouped.items()):
  74. shipper_id = f"{PREFIX}{code}-{order_index + 1:03d}"
  75. if code == "DEMO":
  76. ship_date = now - timedelta(days=(order_index * 2) % 90)
  77. else:
  78. ship_date = now - timedelta(days=order_index % 10)
  79. ratio = shipment_ratio(code, order_no, order_index)
  80. total_qty = sum(Decimal(str(row["order_qty"])) for row in order_lines)
  81. real_total = (total_qty * ratio).quantize(Decimal("0.0001"))
  82. cursor.execute(
  83. """
  84. INSERT INTO ASNBOLShipperMaster
  85. (Id,OrdNbr,QtyToShip,QtyShipped,ShipDate,Status,IsConfirm,
  86. SoldTo,Remark,CreateTime,UpdateTime,IsActive,tenant_id)
  87. VALUES
  88. (%s,%s,%s,%s,%s,'SHIPPED',1,'UAT-CUSTOMER',
  89. 'UAT_GENERATOR:销售发运事实',NOW(),NOW(),1,%s)
  90. """,
  91. (shipper_id, order_no, total_qty, real_total, ship_date, tenant_id),
  92. )
  93. master_rec_id = cursor.lastrowid
  94. summary["masters"] += 1
  95. tenant_summary["orders"] += 1
  96. for line_index, row in enumerate(order_lines, 1):
  97. qty = Decimal(str(row["order_qty"]))
  98. real_qty = (qty * ratio).quantize(Decimal("0.0001"))
  99. cursor.execute(
  100. """
  101. INSERT INTO ASNBOLShipperDetail
  102. (ASNBOLShipperRecID,Id,Line,OrdNbr,OrdLine,ContainerItem,
  103. Descr,QtyToShip,PickingQty,RealQty,QtyShipped,ShipDate,
  104. Status,IsConfirm,Remark,CreateTime,UpdateTime,IsActive,tenant_id)
  105. VALUES
  106. (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,
  107. 'SHIPPED',1,'UAT_GENERATOR:销售发运明细',NOW(),NOW(),1,%s)
  108. """,
  109. (
  110. master_rec_id,
  111. f"{shipper_id}-L{line_index:03d}",
  112. line_index,
  113. order_no,
  114. int(row["order_line"] or line_index),
  115. row["item_code"],
  116. row["item_name"] or row["item_code"],
  117. qty,
  118. real_qty,
  119. real_qty,
  120. real_qty,
  121. ship_date,
  122. tenant_id,
  123. ),
  124. )
  125. summary["details"] += 1
  126. tenant_summary["lines"] += 1
  127. summary["tenants"][code] = tenant_summary
  128. conn.commit()
  129. except Exception:
  130. conn.rollback()
  131. raise
  132. finally:
  133. conn.close()
  134. return summary
  135. def main() -> int:
  136. parser = argparse.ArgumentParser()
  137. parser.add_argument("--apply", action="store_true")
  138. args = parser.parse_args()
  139. if not args.apply:
  140. print(json.dumps({"mode": "dry-run", "tenants": TENANTS, "prefix": PREFIX}, ensure_ascii=False, indent=2))
  141. return 0
  142. result = apply()
  143. result["mode"] = "apply"
  144. print(json.dumps(result, ensure_ascii=False, indent=2, default=str))
  145. return 0
  146. if __name__ == "__main__":
  147. raise SystemExit(main())