migrate_20260808_pl_plan_legacy_party_contracts.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. #!/usr/bin/env python3
  2. """将历史运输计划的单一合同/客户,按单据类型落到供方或需方。
  3. 规则(与产品确认一致):
  4. - 出库单 outbound / 直销单 direct / 其它或空:旧合同 + 客户 → 需方
  5. - 入库单 inbound:旧合同 + 客户 → 供方(customer_* 清空,写入 supplier_*)
  6. 处理范围:
  7. 1) 仍有 pl_plan.contract_id,且尚无 pl_plan_contract 有效关联 → 写入分侧合同
  8. 2) 无合同但有客户:按单据类型把客户落到供方/需方(便于用户继续完善)
  9. 用法(在 backend 目录、已激活 venv):
  10. ENV_FILE=.env.production python scripts/migrate_20260808_pl_plan_legacy_party_contracts.py --dry-run
  11. ENV_FILE=.env.production python scripts/migrate_20260808_pl_plan_legacy_party_contracts.py
  12. """
  13. from __future__ import annotations
  14. import argparse
  15. import sys
  16. from datetime import datetime
  17. from pathlib import Path
  18. ROOT = Path(__file__).resolve().parents[1]
  19. if str(ROOT) not in sys.path:
  20. sys.path.insert(0, str(ROOT))
  21. from app.core.database import SessionLocal # noqa: E402
  22. from app.core.utils import generate_uuid # noqa: E402
  23. from app.models.pl_models import PlPlan, PlPlanContract # noqa: E402
  24. def _resolve_side(plan_type: str) -> str:
  25. return "supplier" if (plan_type or "").strip() == "inbound" else "demander"
  26. def _has_text(value) -> bool:
  27. return bool(str(value or "").strip())
  28. def main() -> int:
  29. parser = argparse.ArgumentParser()
  30. parser.add_argument("--dry-run", action="store_true", help="只统计不落库")
  31. args = parser.parse_args()
  32. db = SessionLocal()
  33. now = datetime.now()
  34. linked = 0
  35. party_updated = 0
  36. skipped = 0
  37. try:
  38. plans = db.query(PlPlan).filter(PlPlan.del_flag == 0).all()
  39. for plan in plans:
  40. side = _resolve_side(plan.plan_type or "")
  41. has_contract = _has_text(plan.contract_id)
  42. has_customer = _has_text(plan.customer_id) or _has_text(plan.customer_name)
  43. has_supplier = _has_text(plan.supplier_id) or _has_text(plan.supplier_name)
  44. exists_link = (
  45. db.query(PlPlanContract.id)
  46. .filter(
  47. PlPlanContract.plan_id == plan.id,
  48. PlPlanContract.del_flag == 0,
  49. )
  50. .first()
  51. )
  52. changed = False
  53. # 1) 合同落到分侧关联表
  54. if has_contract and not exists_link:
  55. if not args.dry_run:
  56. db.add(
  57. PlPlanContract(
  58. id=generate_uuid(),
  59. plan_id=plan.id,
  60. side=side,
  61. contract_id=plan.contract_id,
  62. contract_no=plan.contract_no,
  63. tenant_no=plan.tenant_no,
  64. order_num=1,
  65. create_by=plan.update_by or plan.create_by,
  66. create_dept_id=plan.create_dept_id or "",
  67. create_time=now,
  68. update_by=plan.update_by or plan.create_by,
  69. update_time=now,
  70. del_flag=0,
  71. )
  72. )
  73. linked += 1
  74. changed = True
  75. # 2) 客户落到供方/需方
  76. if side == "supplier":
  77. # 入库:客户 → 供方;清空需方客户
  78. # - 供方为空:整段迁移
  79. # - 供方与客户同名:清掉重复的需方客户
  80. # - 供方与客户不同:保留两边,留给人工确认(不覆盖供方)
  81. if has_customer and not has_supplier:
  82. if not args.dry_run:
  83. plan.supplier_id = plan.customer_id
  84. plan.supplier_name = plan.customer_name
  85. plan.customer_id = None
  86. plan.customer_name = None
  87. plan.destination_id = None
  88. plan.destination_name = None
  89. plan.update_time = now
  90. party_updated += 1
  91. changed = True
  92. elif (
  93. has_customer
  94. and has_supplier
  95. and str(plan.customer_name or "").strip()
  96. == str(plan.supplier_name or "").strip()
  97. ):
  98. if not args.dry_run:
  99. plan.customer_id = None
  100. plan.customer_name = None
  101. plan.destination_id = None
  102. plan.destination_name = None
  103. plan.update_time = now
  104. party_updated += 1
  105. changed = True
  106. else:
  107. # 出库/直销:客户保留在需方;补齐 destination
  108. if has_customer and not _has_text(plan.destination_name):
  109. if not args.dry_run:
  110. plan.destination_id = plan.customer_id
  111. plan.destination_name = plan.customer_name
  112. plan.update_time = now
  113. party_updated += 1
  114. changed = True
  115. if not changed:
  116. skipped += 1
  117. if args.dry_run:
  118. db.rollback()
  119. print(
  120. f"[dry-run] would link contracts={linked}, "
  121. f"update party fields={party_updated}, skipped={skipped}"
  122. )
  123. else:
  124. db.commit()
  125. print(
  126. f"ok linked contracts={linked}, "
  127. f"updated party fields={party_updated}, skipped={skipped}"
  128. )
  129. return 0
  130. except Exception as exc:
  131. db.rollback()
  132. print(f"ERR {type(exc).__name__}: {exc}")
  133. return 1
  134. finally:
  135. db.close()
  136. if __name__ == "__main__":
  137. raise SystemExit(main())