clear_pl_plan_instance_data.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. """
  2. 清空运输计划(pl_plan)及关联实例数据,便于测试环境重新走流程。
  3. 用法(在 backend 目录、已配置 .env DATABASE_URL):
  4. python scripts/clear_pl_plan_instance_data.py --yes
  5. """
  6. from __future__ import annotations
  7. import argparse
  8. import os
  9. import sys
  10. from urllib.parse import urlparse
  11. from sqlalchemy import create_engine, text
  12. from sqlalchemy.engine import Engine
  13. # 确保可加载 app 配置
  14. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  15. TENANT_NO = "shanxia2.0"
  16. def _engine_from_env() -> Engine:
  17. url = os.environ.get("DATABASE_URL")
  18. if not url:
  19. from app.core.config import settings
  20. url = settings.DATABASE_URL
  21. return create_engine(url, pool_pre_ping=True)
  22. def _table_exists(conn, table: str) -> bool:
  23. row = conn.execute(
  24. text(
  25. "SELECT COUNT(*) FROM information_schema.tables "
  26. "WHERE table_schema = DATABASE() AND table_name = :t"
  27. ),
  28. {"t": table},
  29. ).scalar()
  30. return int(row or 0) > 0
  31. def _count(conn, table: str, where: str = "", params: dict | None = None) -> int:
  32. if not _table_exists(conn, table):
  33. return 0
  34. sql = f"SELECT COUNT(*) FROM `{table}`"
  35. if where:
  36. sql += f" WHERE {where}"
  37. return int(conn.execute(text(sql), params or {}).scalar() or 0)
  38. def _delete(conn, table: str, where: str, params: dict | None = None) -> int:
  39. if not _table_exists(conn, table):
  40. return 0
  41. sql = f"DELETE FROM `{table}` WHERE {where}"
  42. result = conn.execute(text(sql), params or {})
  43. return int(result.rowcount or 0)
  44. def main() -> None:
  45. parser = argparse.ArgumentParser(description="清空运输计划实例数据")
  46. parser.add_argument(
  47. "--yes",
  48. action="store_true",
  49. help="确认执行删除(未带此参数仅预览计数)",
  50. )
  51. parser.add_argument("--tenant", default=TENANT_NO, help="租户编号,默认 shanxia2.0")
  52. args = parser.parse_args()
  53. engine = _engine_from_env()
  54. parsed = urlparse(str(engine.url))
  55. db_name = parsed.path.lstrip("/")
  56. print(f"目标库: {db_name} @ {parsed.hostname}:{parsed.port or 3306}")
  57. print(f"租户: {args.tenant}")
  58. print("---")
  59. with engine.connect() as conn:
  60. plan_count = _count(conn, "pl_plan", "tenant_no = :t", {"t": args.tenant})
  61. print(f"pl_plan 行数: {plan_count}")
  62. if not args.yes:
  63. print("预览模式,未删除。请加 --yes 执行。")
  64. return
  65. stats: dict[str, int] = {}
  66. with engine.begin() as conn:
  67. tenant = args.tenant
  68. def del_by_plan(table: str, extra: str = "") -> None:
  69. cond = f"plan_id IN (SELECT id FROM pl_plan WHERE tenant_no = :t)"
  70. if extra:
  71. cond = f"{cond} AND {extra}"
  72. n = _delete(conn, table, cond, {"t": tenant})
  73. if n:
  74. stats[table] = stats.get(table, 0) + n
  75. # 先收集运输单 id(删 pl_plan_transport 后无法再关联)
  76. transport_ids: list[str] = []
  77. if _table_exists(conn, "pl_plan_transport"):
  78. rows = conn.execute(
  79. text(
  80. "SELECT DISTINCT transport_id FROM pl_plan_transport "
  81. "WHERE tenant_no = :t AND plan_id IN (SELECT id FROM pl_plan WHERE tenant_no = :t)"
  82. ),
  83. {"t": tenant},
  84. ).fetchall()
  85. transport_ids = [str(r[0]) for r in rows if r[0]]
  86. def del_by_transport_ids(table: str, col: str = "transport_id") -> None:
  87. if not transport_ids:
  88. return
  89. placeholders = ",".join([f":tid{i}" for i in range(len(transport_ids))])
  90. params = {f"tid{i}": v for i, v in enumerate(transport_ids)}
  91. n = _delete(conn, table, f"{col} IN ({placeholders})", params)
  92. if n:
  93. stats[table] = stats.get(table, 0) + n
  94. def del_transports() -> None:
  95. if not transport_ids:
  96. return
  97. placeholders = ",".join([f":tid{i}" for i in range(len(transport_ids))])
  98. params = {f"tid{i}": v for i, v in enumerate(transport_ids)}
  99. n = _delete(conn, "pl_transport", f"id IN ({placeholders})", params)
  100. if n:
  101. stats["pl_transport"] = n
  102. def del_by_transport(table: str) -> None:
  103. del_by_transport_ids(table, "transport_id")
  104. def del_inbound_children() -> None:
  105. if not _table_exists(conn, "pl_inbound_order"):
  106. return
  107. cond = "order_id IN (SELECT id FROM pl_inbound_order WHERE plan_id IN (SELECT id FROM pl_plan WHERE tenant_no = :t))"
  108. n = _delete(
  109. conn,
  110. "pl_inbound_order_item_brand_qty",
  111. cond,
  112. {"t": tenant},
  113. )
  114. if n:
  115. stats["pl_inbound_order_item_brand_qty"] = n
  116. n = _delete(conn, "pl_inbound_order_item", cond, {"t": tenant})
  117. if n:
  118. stats["pl_inbound_order_item"] = n
  119. n = _delete(conn, "pl_inbound_order", "plan_id IN (SELECT id FROM pl_plan WHERE tenant_no = :t)", {"t": tenant})
  120. if n:
  121. stats["pl_inbound_order"] = n
  122. def del_outbound_children() -> None:
  123. if not _table_exists(conn, "pl_outbound_order"):
  124. return
  125. cond = "order_id IN (SELECT id FROM pl_outbound_order WHERE plan_id IN (SELECT id FROM pl_plan WHERE tenant_no = :t))"
  126. n = _delete(
  127. conn,
  128. "pl_outbound_order_item_brand_qty",
  129. cond,
  130. {"t": tenant},
  131. )
  132. if n:
  133. stats["pl_outbound_order_item_brand_qty"] = n
  134. n = _delete(conn, "pl_outbound_order_item", cond, {"t": tenant})
  135. if n:
  136. stats["pl_outbound_order_item"] = n
  137. n = _delete(conn, "pl_outbound_order", "plan_id IN (SELECT id FROM pl_plan WHERE tenant_no = :t)", {"t": tenant})
  138. if n:
  139. stats["pl_outbound_order"] = n
  140. # 地磅:先收集关联 id
  141. db_pp_ids: list[str] = []
  142. db_inv_ids: list[str] = []
  143. if _table_exists(conn, "pl_transport_db_link"):
  144. rows = conn.execute(
  145. text(
  146. "SELECT DISTINCT db_purchase_plan_id, db_inventory_id FROM pl_transport_db_link "
  147. "WHERE tenant_no = :t AND plan_id IN (SELECT id FROM pl_plan WHERE tenant_no = :t)"
  148. ),
  149. {"t": tenant},
  150. ).fetchall()
  151. for pp, inv in rows:
  152. if pp and str(pp).strip():
  153. db_pp_ids.append(str(pp))
  154. if inv and str(inv).strip():
  155. db_inv_ids.append(str(inv))
  156. # 运输单子表
  157. del_by_transport("pl_transport_product_brand_qty")
  158. del_by_transport("pl_transport_product_feedback")
  159. del_by_plan("pl_transport_db_link")
  160. del_by_plan("pl_transport_driver_stop_feedback")
  161. del_by_plan("pl_transport_site_stop_feedback")
  162. del_by_plan("pl_transport_site_link")
  163. del_inbound_children()
  164. del_outbound_children()
  165. del_by_transport("pl_transport_approval")
  166. del_by_transport("pl_freight_settlement_transport")
  167. del_by_plan("pl_plan_transport")
  168. del_transports()
  169. def del_orphan_transports() -> None:
  170. """计划关联已删后仍残留的运输单(租户级实例数据)"""
  171. if not _table_exists(conn, "pl_transport"):
  172. return
  173. n = _delete(conn, "pl_transport", "tenant_no = :t", {"t": tenant})
  174. if n:
  175. stats["pl_transport(orphan)"] = n
  176. def del_freight_settlements() -> None:
  177. if not _table_exists(conn, "pl_freight_settlement"):
  178. return
  179. sid_cond = "settlement_id IN (SELECT id FROM pl_freight_settlement WHERE tenant_no = :t)"
  180. n = _delete(conn, "pl_freight_settlement_approval", sid_cond, {"t": tenant})
  181. if n:
  182. stats["pl_freight_settlement_approval"] = n
  183. n = _delete(conn, "pl_freight_settlement_transport", sid_cond, {"t": tenant})
  184. if n:
  185. stats["pl_freight_settlement_transport(extra)"] = n
  186. n = _delete(conn, "pl_freight_settlement", "tenant_no = :t", {"t": tenant})
  187. if n:
  188. stats["pl_freight_settlement"] = n
  189. del_orphan_transports()
  190. del_freight_settlements()
  191. # 计划子表
  192. del_by_plan("pl_plan_load_product")
  193. del_by_plan("pl_plan_delivery_product")
  194. del_by_plan("pl_plan_load")
  195. del_by_plan("pl_plan_delivery")
  196. del_by_plan("pl_plan_contract")
  197. del_by_plan("pl_plan_product_brand_qty")
  198. del_by_plan("pl_plan_product")
  199. del_by_plan("pl_plan_change")
  200. # 地磅出入库与计划
  201. if _table_exists(conn, "db_inbound_outbound"):
  202. if db_inv_ids:
  203. placeholders = ",".join([":i" + str(i) for i in range(len(db_inv_ids))])
  204. params = {"t": tenant, **{f"i{i}": v for i, v in enumerate(db_inv_ids)}}
  205. n = conn.execute(
  206. text(f"DELETE FROM db_inbound_outbound WHERE id IN ({placeholders})"),
  207. params,
  208. ).rowcount
  209. if n:
  210. stats["db_inbound_outbound(by_inv)"] = int(n)
  211. if db_pp_ids:
  212. placeholders = ",".join([":p" + str(i) for i in range(len(db_pp_ids))])
  213. params = {f"p{i}": v for i, v in enumerate(db_pp_ids)}
  214. n = conn.execute(
  215. text(f"DELETE FROM db_inbound_outbound WHERE purchase_plan_id IN ({placeholders})"),
  216. params,
  217. ).rowcount
  218. if n:
  219. stats["db_inbound_outbound(by_pp)"] = int(n)
  220. n = _delete(
  221. conn,
  222. "db_inbound_outbound",
  223. "plan_no IN (SELECT plan_no FROM pl_plan WHERE tenant_no = :t)",
  224. {"t": tenant},
  225. )
  226. if n:
  227. stats["db_inbound_outbound(by_plan_no)"] = n
  228. if _table_exists(conn, "db_purchase_plan"):
  229. if db_pp_ids:
  230. placeholders = ",".join([":p" + str(i) for i in range(len(db_pp_ids))])
  231. params = {f"p{i}": v for i, v in enumerate(db_pp_ids)}
  232. n = conn.execute(
  233. text(f"DELETE FROM db_purchase_plan WHERE id IN ({placeholders})"),
  234. params,
  235. ).rowcount
  236. if n:
  237. stats["db_purchase_plan(by_link)"] = int(n)
  238. n = _delete(
  239. conn,
  240. "db_purchase_plan",
  241. "tenant_no = :t AND remark LIKE '来源运输计划%'",
  242. {"t": tenant},
  243. )
  244. if n:
  245. stats["db_purchase_plan(by_remark)"] = n
  246. n = _delete(
  247. conn,
  248. "db_purchase_plan",
  249. "tenant_no = :t AND plan_no IN (SELECT plan_no FROM pl_plan WHERE tenant_no = :t)",
  250. {"t": tenant},
  251. )
  252. if n:
  253. stats["db_purchase_plan(by_plan_no)"] = n
  254. n = _delete(conn, "pl_plan", "tenant_no = :t", {"t": tenant})
  255. stats["pl_plan"] = n
  256. remaining = _count(conn, "pl_plan", "tenant_no = :t", {"t": tenant})
  257. print("已删除行数:")
  258. for k, v in sorted(stats.items()):
  259. print(f" {k}: {v}")
  260. print(f"剩余 pl_plan: {remaining}")
  261. print("完成。")
  262. if __name__ == "__main__":
  263. main()