Files
shanxia2.0/backend/app/services/pl_plan_arrange_copy_flags.py
T
2026-10-02 09:28:00 +08:00

219 lines
7.4 KiB
Python

"""运输计划列表:批量计算「复制给司机 / 现场 / 磅单二维码」按钮显示标志。
语义与查看弹窗 / 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