315 lines
10 KiB
Python
315 lines
10 KiB
Python
"""运输计划归档导出:按装货/送货段 × 段内产品展开,与列表展示规则一致。"""
|
|
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
|