"""运输计划列表:批量计算「复制给司机 / 现场 / 磅单二维码」按钮显示标志。 语义与查看弹窗 / usePlPlanArrangeActions / build_site_records_for_arranged_plan 对齐: - 已安排且未取消,且存在司机链接或现场记录或地磅出入库关联时,显示「复制给司机」 - 有现场链接记录(含运输单按段 site_access_token、出入库单现场令牌回退)时显示「复制给现场」 - 有勾选生成地磅单的出入库关联(db_purchase_plan_id 非空)时显示「复制磅单二维码」 """ from __future__ import annotations from collections import defaultdict from dataclasses import dataclass from typing import Iterable from sqlalchemy.orm import Session from app.models.pl_models import ( PlInboundOrder, PlOutboundOrder, PlPlan, PlPlanDelivery, PlPlanLoad, PlTransport, PlTransportDbLink, PlTransportSiteLink, ) from app.services.pl_internal_customer import is_internal_customer_name @dataclass(frozen=True) class PlanArrangeCopyFlags: can_copy_to_driver: bool = False can_copy_to_site: bool = False can_copy_pound_qrcode: bool = False def _plan_requires_site_link_cached( plan: PlPlan, *, load_by_plan: dict[str, PlPlanLoad], deliveries_by_plan: dict[str, list[PlPlanDelivery]], ) -> bool: """与 plan_requires_site_link 语义一致,使用已批量加载的装/送货段。""" load = load_by_plan.get(plan.id) if load is not None: if is_internal_customer_name(load.customer_name): return True elif is_internal_customer_name(plan.supplier_name): return True deliveries = deliveries_by_plan.get(plan.id) or [] if deliveries: return any(is_internal_customer_name(row.customer_name) for row in deliveries) return is_internal_customer_name(plan.customer_name) or is_internal_customer_name( plan.destination_name ) def load_plan_arrange_copy_flags( db: Session, tenant_no: str, plans: Iterable[PlPlan], ) -> dict[str, PlanArrangeCopyFlags]: plan_list = list(plans) empty = PlanArrangeCopyFlags() result: dict[str, PlanArrangeCopyFlags] = {plan.id: empty for plan in plan_list} candidates = [ plan for plan in plan_list if plan.transport_status == "arranged" and plan.follow_status != "cancelled" and (plan.transport_id or "").strip() ] if not candidates: return result plan_ids = [plan.id for plan in candidates] transport_ids = list({plan.transport_id for plan in candidates if plan.transport_id}) transports = ( db.query(PlTransport) .filter( PlTransport.id.in_(transport_ids), PlTransport.tenant_no == tenant_no, PlTransport.del_flag == 0, ) .all() ) transport_by_id = { row.id: row for row in transports if row.transport_status != "cancelled" } site_plan_ids: set[str] = { plan_id for (plan_id,) in db.query(PlTransportSiteLink.plan_id) .filter( PlTransportSiteLink.tenant_no == tenant_no, PlTransportSiteLink.plan_id.in_(plan_ids), PlTransportSiteLink.del_flag == 0, ) .distinct() .all() } inventory_plan_ids: set[str] = set() dibang_plan_ids: set[str] = set() for plan_id, db_purchase_plan_id in ( db.query(PlTransportDbLink.plan_id, PlTransportDbLink.db_purchase_plan_id) .filter( PlTransportDbLink.tenant_no == tenant_no, PlTransportDbLink.plan_id.in_(plan_ids), PlTransportDbLink.del_flag == 0, ) .all() ): inventory_plan_ids.add(plan_id) if (db_purchase_plan_id or "").strip(): dibang_plan_ids.add(plan_id) # 与 get_plan_arrange_info 一致:无 site_link 时,用未作废出入库单的现场令牌回退 inbound_ids = [ plan.id for plan in candidates if plan.plan_type == "inbound" and plan.id not in site_plan_ids ] if inbound_ids: for plan_id, site_access_token, status in ( db.query(PlInboundOrder.plan_id, PlInboundOrder.site_access_token, PlInboundOrder.status) .filter( PlInboundOrder.tenant_no == tenant_no, PlInboundOrder.plan_id.in_(inbound_ids), PlInboundOrder.del_flag == 0, ) .all() ): if status != "voided" and (site_access_token or "").strip(): site_plan_ids.add(plan_id) outbound_ids = [ plan.id for plan in candidates if plan.plan_type == "outbound" and plan.id not in site_plan_ids ] if outbound_ids: for plan_id, site_access_token, status in ( db.query( PlOutboundOrder.plan_id, PlOutboundOrder.site_access_token, PlOutboundOrder.status ) .filter( PlOutboundOrder.tenant_no == tenant_no, PlOutboundOrder.plan_id.in_(outbound_ids), PlOutboundOrder.del_flag == 0, ) .all() ): if status != "voided" and (site_access_token or "").strip(): site_plan_ids.add(plan_id) # 批量装/送货段,避免 plan_requires_site_link / plan_has_load_stop 逐计划打库 need_site_fallback = [plan for plan in candidates if plan.id not in site_plan_ids] load_by_plan: dict[str, PlPlanLoad] = {} deliveries_by_plan: dict[str, list[PlPlanDelivery]] = defaultdict(list) if need_site_fallback: fallback_ids = [plan.id for plan in need_site_fallback] for row in ( db.query(PlPlanLoad) .filter( PlPlanLoad.plan_id.in_(fallback_ids), PlPlanLoad.tenant_no == tenant_no, PlPlanLoad.del_flag == 0, ) .all() ): load_by_plan[row.plan_id] = row for row in ( db.query(PlPlanDelivery) .filter( PlPlanDelivery.plan_id.in_(fallback_ids), PlPlanDelivery.tenant_no == tenant_no, PlPlanDelivery.del_flag == 0, ) .all() ): deliveries_by_plan[row.plan_id].append(row) # 与 build_site_records_for_arranged_plan 一致:无 site_link 行时,装货段 + 运输单 site_access_token for plan in need_site_fallback: if not _plan_requires_site_link_cached( plan, load_by_plan=load_by_plan, deliveries_by_plan=deliveries_by_plan, ): continue if plan.id not in load_by_plan: continue transport = transport_by_id.get(plan.transport_id or "") if not transport: continue if (transport.site_access_token or "").strip(): site_plan_ids.add(plan.id) for plan in candidates: transport = transport_by_id.get(plan.transport_id or "") if not transport: continue has_driver = bool((transport.driver_access_token or "").strip()) has_site = plan.id in site_plan_ids has_inventory = plan.id in inventory_plan_ids has_dibang = plan.id in dibang_plan_ids if not (has_driver or has_site or has_inventory): continue result[plan.id] = PlanArrangeCopyFlags( can_copy_to_driver=True, can_copy_to_site=has_site, can_copy_pound_qrcode=has_dibang, ) return result