| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- """运输计划归档导出:按装货/送货段 × 段内产品展开,与列表展示规则一致。"""
- from __future__ import annotations
- from datetime import date, datetime
- from decimal import Decimal
- from typing import Optional, Union
- from sqlalchemy.orm import Session
- from app.models.pl_models import PlPlan, PlPlanProduct, PlTransport
- from app.schemas.pl.plan import PlanDeliveryRead, PlanLoadRead, PlanStopProductRead
- from app.services.pl_internal_customer import is_internal_customer_name
- from app.services.pl_plan_stop_service import load_plan_stops_grouped
- StopKind = str # "load" | "delivery"
- PLAN_TYPE_LABELS = {
- "direct": "直销单",
- "inbound": "入库单",
- "outbound": "出库单",
- }
- ARCHIVE_STATUS_LABELS = {
- "archived": "已归档",
- "unarchived": "未归档",
- "draft": "已暂存",
- }
- FOLLOW_STATUS_LABELS = {
- "wait_notice": "等通知",
- "pending_arrange": "待安排",
- "arranged": "已安排",
- "completed": "已完成",
- "cancelled": "已取消",
- "draft": "待安排",
- "processing": "进行中",
- }
- FREIGHT_TYPE_LABELS = {
- "by_weight": "按重量",
- "by_trip": "按趟",
- }
- def resolve_archive_list_io_column(kind: StopKind, customer_name: Optional[str]) -> str:
- """装货/送货段入库出库列方向(与前端 resolveArchiveListIoColumn 一致)。"""
- internal = is_internal_customer_name(customer_name)
- if kind == "load":
- return "outbound" if internal else "inbound"
- return "inbound" if internal else "outbound"
- def _format_date_value(value: Optional[Union[date, datetime, str]]) -> str:
- if value is None:
- return ""
- return str(value)[:10]
- def _format_plate_display(license_plate: Optional[str], trailer_no: Optional[str]) -> str:
- plate = (license_plate or "").strip()
- trailer = (trailer_no or "").strip()
- if not plate and not trailer:
- return ""
- if plate and trailer:
- return f"{plate}{trailer}"
- return plate or trailer
- def _get_stop_archive_actual_qty(product: PlanStopProductRead, kind: StopKind) -> Optional[Decimal]:
- return product.inbound_qty if kind == "load" else product.outbound_qty
- def _get_stop_archive_unit_price(product: PlanStopProductRead, kind: StopKind) -> Optional[Decimal]:
- return product.inbound_price if kind == "load" else product.outbound_price
- def _get_stop_archive_amount(product: PlanStopProductRead, kind: StopKind) -> Optional[Decimal]:
- stored = product.inbound_amount if kind == "load" else product.outbound_amount
- if stored is not None:
- return stored
- qty = _get_stop_archive_actual_qty(product, kind)
- price = _get_stop_archive_unit_price(product, kind)
- if qty is not None and price is not None:
- return qty * price
- return None
- def _stop_kind_label(kind: StopKind, delivery_seq: Optional[int] = None) -> str:
- if kind == "load":
- return "装货"
- return f"送货 #{delivery_seq or 1}"
- def _stop_time(kind: StopKind, stop: Union[PlanLoadRead, PlanDeliveryRead]) -> str:
- if kind == "load":
- return _format_date_value(getattr(stop, "load_time", None))
- return _format_date_value(getattr(stop, "delivery_time", None))
- def _load_legacy_products_by_plan(
- db: Session, tenant_no: str, plan_ids: list[str]
- ) -> dict[str, list[PlPlanProduct]]:
- if not plan_ids:
- return {}
- products = (
- db.query(PlPlanProduct)
- .filter(
- PlPlanProduct.tenant_no == tenant_no,
- PlPlanProduct.del_flag == 0,
- PlPlanProduct.plan_id.in_(plan_ids),
- )
- .order_by(PlPlanProduct.order_num)
- .all()
- )
- grouped: dict[str, list[PlPlanProduct]] = {}
- for product in products:
- grouped.setdefault(product.plan_id, []).append(product)
- return grouped
- def _plan_common_export_fields(
- plan: PlPlan,
- transport: Optional[PlTransport],
- ) -> dict:
- freight_type_raw = transport.freight_type if transport else "by_weight"
- freight_type_label = FREIGHT_TYPE_LABELS.get(freight_type_raw, freight_type_raw or "")
- freight_price = transport.freight_price if transport else None
- freight_amount = plan.freight_amount
- if freight_amount is None and transport is not None:
- freight_amount = transport.freight_amount
- plan_date = _format_date_value(plan.plan_date)
- return {
- "planNo": plan.plan_no or "",
- "planDate": plan_date,
- "archiveStatus": ARCHIVE_STATUS_LABELS.get(plan.archive_status or "", plan.archive_status or ""),
- "followStatus": FOLLOW_STATUS_LABELS.get(plan.follow_status or "", plan.follow_status or ""),
- "planType": PLAN_TYPE_LABELS.get(plan.plan_type or "", plan.plan_type or ""),
- "licensePlate": _format_plate_display(plan.license_plate, plan.trailer_no),
- "erpEntered": "是" if int(plan.erp_entered or 0) == 1 else "否",
- "freightType": freight_type_label,
- "freightPrice": freight_price,
- "freightAmount": freight_amount,
- "remark": plan.remark or "",
- }
- def _empty_product_fields() -> dict:
- return {
- "productName": "",
- "model": "",
- "packageSpec": "",
- "unit": "",
- "planQty": None,
- "actualQty": None,
- "inboundQty": None,
- "outboundQty": None,
- "unitPrice": None,
- "amount": None,
- }
- def _product_row_from_stop_product(
- product: PlanStopProductRead,
- kind: StopKind,
- customer_name: Optional[str],
- ) -> dict:
- actual_qty = _get_stop_archive_actual_qty(product, kind)
- unit_price = _get_stop_archive_unit_price(product, kind)
- amount = _get_stop_archive_amount(product, kind)
- io_col = resolve_archive_list_io_column(kind, customer_name)
- inbound_qty = actual_qty if io_col == "inbound" else None
- outbound_qty = actual_qty if io_col == "outbound" else None
- return {
- "productName": product.product_name or "",
- "model": product.model or "",
- "packageSpec": product.package_spec or "",
- "unit": product.unit or "",
- "planQty": product.plan_qty,
- "actualQty": actual_qty,
- "inboundQty": inbound_qty,
- "outboundQty": outbound_qty,
- "unitPrice": unit_price,
- "amount": amount,
- }
- def _append_stop_product_rows(
- rows: list[dict],
- row_no: int,
- plan_common: dict,
- kind: StopKind,
- stop: Union[PlanLoadRead, PlanDeliveryRead],
- delivery_seq: Optional[int],
- products: list[PlanStopProductRead],
- ) -> int:
- customer_name = stop.customer_name or ""
- stop_common = {
- **plan_common,
- "stopKind": _stop_kind_label(kind, delivery_seq),
- "stopCustomer": customer_name,
- "stopAddress": stop.address or "",
- "stopTime": _stop_time(kind, stop),
- }
- product_rows = products or [None]
- for product in product_rows:
- row_no += 1
- row = {
- **stop_common,
- "rowNo": row_no,
- **_empty_product_fields(),
- }
- if product is not None:
- row.update(_product_row_from_stop_product(product, kind, customer_name))
- rows.append(row)
- return row_no
- def _append_legacy_plan_product_rows(
- rows: list[dict],
- row_no: int,
- plan: PlPlan,
- plan_common: dict,
- products: list[PlPlanProduct],
- ) -> int:
- """无段表数据时回退计划产品级导出(兼容旧数据)。"""
- stop_common = {
- **plan_common,
- "stopKind": "",
- "stopCustomer": plan.customer_name or plan.supplier_name or "",
- "stopAddress": plan.destination_name or "",
- "stopTime": _format_date_value(plan.delivery_time),
- }
- product_rows = products or [None]
- for product in product_rows:
- row_no += 1
- row = {
- **stop_common,
- "rowNo": row_no,
- **_empty_product_fields(),
- }
- if product is not None:
- row.update(
- {
- "productName": product.product_name or "",
- "model": product.model or "",
- "packageSpec": product.package_spec or "",
- "unit": product.unit or "",
- "planQty": product.quantity,
- "actualQty": product.actual_qty,
- "inboundQty": product.inbound_qty,
- "outboundQty": product.outbound_qty,
- "unitPrice": product.inbound_price or product.outbound_price,
- "amount": product.inbound_amount or product.outbound_amount,
- }
- )
- rows.append(row)
- return row_no
- def build_plan_archive_export_rows(
- db: Session,
- tenant_no: str,
- plans: list[PlPlan],
- transports_by_id: dict[str, PlTransport],
- ) -> list[dict]:
- """构建归档导出数据行:每计划 × 每段次 × 每段产品一行。"""
- if not plans:
- return []
- plan_ids = [plan.id for plan in plans]
- stops_by_plan = load_plan_stops_grouped(db, tenant_no, plan_ids)
- legacy_products_by_plan = _load_legacy_products_by_plan(db, tenant_no, plan_ids)
- rows: list[dict] = []
- row_no = 0
- for plan in plans:
- transport = transports_by_id.get(plan.transport_id) if plan.transport_id else None
- plan_common = _plan_common_export_fields(plan, transport)
- load_read, delivery_reads = stops_by_plan.get(plan.id, (None, []))
- has_stops = load_read is not None or bool(delivery_reads)
- if not has_stops:
- row_no = _append_legacy_plan_product_rows(
- rows,
- row_no,
- plan,
- plan_common,
- legacy_products_by_plan.get(plan.id, []),
- )
- continue
- if load_read is not None:
- row_no = _append_stop_product_rows(
- rows,
- row_no,
- plan_common,
- "load",
- load_read,
- None,
- load_read.products or [],
- )
- for idx, delivery_read in enumerate(delivery_reads):
- delivery_seq = delivery_read.seq or (idx + 1)
- row_no = _append_stop_product_rows(
- rows,
- row_no,
- plan_common,
- "delivery",
- delivery_read,
- delivery_seq,
- delivery_read.products or [],
- )
- return rows
|