pl_plan_arrange_copy_flags.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. """运输计划列表:批量计算「复制给司机 / 现场 / 磅单二维码」按钮显示标志。
  2. 语义与查看弹窗 / usePlPlanArrangeActions / build_site_records_for_arranged_plan 对齐:
  3. - 已安排且未取消,且存在司机链接或现场记录或地磅出入库关联时,显示「复制给司机」
  4. - 有现场链接记录(含运输单按段 site_access_token、出入库单现场令牌回退)时显示「复制给现场」
  5. - 有勾选生成地磅单的出入库关联(db_purchase_plan_id 非空)时显示「复制磅单二维码」
  6. """
  7. from __future__ import annotations
  8. from collections import defaultdict
  9. from dataclasses import dataclass
  10. from typing import Iterable
  11. from sqlalchemy.orm import Session
  12. from app.models.pl_models import (
  13. PlInboundOrder,
  14. PlOutboundOrder,
  15. PlPlan,
  16. PlPlanDelivery,
  17. PlPlanLoad,
  18. PlTransport,
  19. PlTransportDbLink,
  20. PlTransportSiteLink,
  21. )
  22. from app.services.pl_internal_customer import is_internal_customer_name
  23. @dataclass(frozen=True)
  24. class PlanArrangeCopyFlags:
  25. can_copy_to_driver: bool = False
  26. can_copy_to_site: bool = False
  27. can_copy_pound_qrcode: bool = False
  28. def _plan_requires_site_link_cached(
  29. plan: PlPlan,
  30. *,
  31. load_by_plan: dict[str, PlPlanLoad],
  32. deliveries_by_plan: dict[str, list[PlPlanDelivery]],
  33. ) -> bool:
  34. """与 plan_requires_site_link 语义一致,使用已批量加载的装/送货段。"""
  35. load = load_by_plan.get(plan.id)
  36. if load is not None:
  37. if is_internal_customer_name(load.customer_name):
  38. return True
  39. elif is_internal_customer_name(plan.supplier_name):
  40. return True
  41. deliveries = deliveries_by_plan.get(plan.id) or []
  42. if deliveries:
  43. return any(is_internal_customer_name(row.customer_name) for row in deliveries)
  44. return is_internal_customer_name(plan.customer_name) or is_internal_customer_name(
  45. plan.destination_name
  46. )
  47. def load_plan_arrange_copy_flags(
  48. db: Session,
  49. tenant_no: str,
  50. plans: Iterable[PlPlan],
  51. ) -> dict[str, PlanArrangeCopyFlags]:
  52. plan_list = list(plans)
  53. empty = PlanArrangeCopyFlags()
  54. result: dict[str, PlanArrangeCopyFlags] = {plan.id: empty for plan in plan_list}
  55. candidates = [
  56. plan
  57. for plan in plan_list
  58. if plan.transport_status == "arranged"
  59. and plan.follow_status != "cancelled"
  60. and (plan.transport_id or "").strip()
  61. ]
  62. if not candidates:
  63. return result
  64. plan_ids = [plan.id for plan in candidates]
  65. transport_ids = list({plan.transport_id for plan in candidates if plan.transport_id})
  66. transports = (
  67. db.query(PlTransport)
  68. .filter(
  69. PlTransport.id.in_(transport_ids),
  70. PlTransport.tenant_no == tenant_no,
  71. PlTransport.del_flag == 0,
  72. )
  73. .all()
  74. )
  75. transport_by_id = {
  76. row.id: row for row in transports if row.transport_status != "cancelled"
  77. }
  78. site_plan_ids: set[str] = {
  79. plan_id
  80. for (plan_id,) in db.query(PlTransportSiteLink.plan_id)
  81. .filter(
  82. PlTransportSiteLink.tenant_no == tenant_no,
  83. PlTransportSiteLink.plan_id.in_(plan_ids),
  84. PlTransportSiteLink.del_flag == 0,
  85. )
  86. .distinct()
  87. .all()
  88. }
  89. inventory_plan_ids: set[str] = set()
  90. dibang_plan_ids: set[str] = set()
  91. for plan_id, db_purchase_plan_id in (
  92. db.query(PlTransportDbLink.plan_id, PlTransportDbLink.db_purchase_plan_id)
  93. .filter(
  94. PlTransportDbLink.tenant_no == tenant_no,
  95. PlTransportDbLink.plan_id.in_(plan_ids),
  96. PlTransportDbLink.del_flag == 0,
  97. )
  98. .all()
  99. ):
  100. inventory_plan_ids.add(plan_id)
  101. if (db_purchase_plan_id or "").strip():
  102. dibang_plan_ids.add(plan_id)
  103. # 与 get_plan_arrange_info 一致:无 site_link 时,用未作废出入库单的现场令牌回退
  104. inbound_ids = [
  105. plan.id
  106. for plan in candidates
  107. if plan.plan_type == "inbound" and plan.id not in site_plan_ids
  108. ]
  109. if inbound_ids:
  110. for plan_id, site_access_token, status in (
  111. db.query(PlInboundOrder.plan_id, PlInboundOrder.site_access_token, PlInboundOrder.status)
  112. .filter(
  113. PlInboundOrder.tenant_no == tenant_no,
  114. PlInboundOrder.plan_id.in_(inbound_ids),
  115. PlInboundOrder.del_flag == 0,
  116. )
  117. .all()
  118. ):
  119. if status != "voided" and (site_access_token or "").strip():
  120. site_plan_ids.add(plan_id)
  121. outbound_ids = [
  122. plan.id
  123. for plan in candidates
  124. if plan.plan_type == "outbound" and plan.id not in site_plan_ids
  125. ]
  126. if outbound_ids:
  127. for plan_id, site_access_token, status in (
  128. db.query(
  129. PlOutboundOrder.plan_id, PlOutboundOrder.site_access_token, PlOutboundOrder.status
  130. )
  131. .filter(
  132. PlOutboundOrder.tenant_no == tenant_no,
  133. PlOutboundOrder.plan_id.in_(outbound_ids),
  134. PlOutboundOrder.del_flag == 0,
  135. )
  136. .all()
  137. ):
  138. if status != "voided" and (site_access_token or "").strip():
  139. site_plan_ids.add(plan_id)
  140. # 批量装/送货段,避免 plan_requires_site_link / plan_has_load_stop 逐计划打库
  141. need_site_fallback = [plan for plan in candidates if plan.id not in site_plan_ids]
  142. load_by_plan: dict[str, PlPlanLoad] = {}
  143. deliveries_by_plan: dict[str, list[PlPlanDelivery]] = defaultdict(list)
  144. if need_site_fallback:
  145. fallback_ids = [plan.id for plan in need_site_fallback]
  146. for row in (
  147. db.query(PlPlanLoad)
  148. .filter(
  149. PlPlanLoad.plan_id.in_(fallback_ids),
  150. PlPlanLoad.tenant_no == tenant_no,
  151. PlPlanLoad.del_flag == 0,
  152. )
  153. .all()
  154. ):
  155. load_by_plan[row.plan_id] = row
  156. for row in (
  157. db.query(PlPlanDelivery)
  158. .filter(
  159. PlPlanDelivery.plan_id.in_(fallback_ids),
  160. PlPlanDelivery.tenant_no == tenant_no,
  161. PlPlanDelivery.del_flag == 0,
  162. )
  163. .all()
  164. ):
  165. deliveries_by_plan[row.plan_id].append(row)
  166. # 与 build_site_records_for_arranged_plan 一致:无 site_link 行时,装货段 + 运输单 site_access_token
  167. for plan in need_site_fallback:
  168. if not _plan_requires_site_link_cached(
  169. plan,
  170. load_by_plan=load_by_plan,
  171. deliveries_by_plan=deliveries_by_plan,
  172. ):
  173. continue
  174. if plan.id not in load_by_plan:
  175. continue
  176. transport = transport_by_id.get(plan.transport_id or "")
  177. if not transport:
  178. continue
  179. if (transport.site_access_token or "").strip():
  180. site_plan_ids.add(plan.id)
  181. for plan in candidates:
  182. transport = transport_by_id.get(plan.transport_id or "")
  183. if not transport:
  184. continue
  185. has_driver = bool((transport.driver_access_token or "").strip())
  186. has_site = plan.id in site_plan_ids
  187. has_inventory = plan.id in inventory_plan_ids
  188. has_dibang = plan.id in dibang_plan_ids
  189. if not (has_driver or has_site or has_inventory):
  190. continue
  191. result[plan.id] = PlanArrangeCopyFlags(
  192. can_copy_to_driver=True,
  193. can_copy_to_site=has_site,
  194. can_copy_pound_qrcode=has_dibang,
  195. )
  196. return result