| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- """运输计划多段:内部客户地磅默认值与数量平衡校验"""
- from __future__ import annotations
- from decimal import Decimal
- from typing import Any, Iterable, Literal, Optional, Tuple
- from fastapi import HTTPException
- from app.services.pl_internal_customer import is_internal_customer_name
- StopType = Literal["load", "delivery"]
- def resolve_stop_dibang_defaults(
- customer_name: Optional[str],
- stop_type: StopType,
- ) -> Tuple[bool, str]:
- """地磅默认勾选以本段客户是否为内部客户(山峡/云栋)为准。"""
- if not is_internal_customer_name(customer_name):
- return False, "none"
- if stop_type == "load":
- return True, "outbound"
- if stop_type == "delivery":
- return True, "inbound"
- return False, "none"
- def normalize_stop_dibang_fields(
- customer_name: Optional[str],
- stop_type: StopType,
- generate_dibang: Optional[int],
- dibang_direction: Optional[str],
- ) -> Tuple[int, str]:
- """归一化段次地磅勾选与方向。
- - 非内部客户:强制不生成地磅单
- - 内部客户:尊重显式勾选值(含可选车取消勾选传 0);仅 None 时用默认
- - 勾选但方向缺失/none 时按本段客户补默认方向
- """
- if not is_internal_customer_name(customer_name):
- return 0, "none"
- default_gen, default_dir = resolve_stop_dibang_defaults(customer_name, stop_type)
- if generate_dibang is None:
- gen = 1 if default_gen else 0
- else:
- gen = 1 if int(generate_dibang) == 1 else 0
- direction = (dibang_direction or "").strip()
- if gen:
- if direction not in ("outbound", "inbound"):
- direction = default_dir if default_dir != "none" else (
- "outbound" if stop_type == "load" else "inbound"
- )
- else:
- direction = "none"
- return gen, direction
- def _extract_qty(item: Any, *field_names: str) -> Decimal:
- for name in field_names:
- if isinstance(item, dict):
- value = item.get(name)
- else:
- value = getattr(item, name, None)
- if value is not None:
- return Decimal(str(value))
- return Decimal("0")
- def _fmt_qty(qty: Decimal) -> str:
- if qty == qty.to_integral_value():
- return str(int(qty))
- return format(qty.normalize(), "f")
- def _row_text(item: Any, *field_names: str) -> str:
- for name in field_names:
- if isinstance(item, dict):
- value = item.get(name)
- else:
- value = getattr(item, name, None)
- text = str(value or "").strip()
- if text:
- return text
- return ""
- def _product_key(item: Any) -> str:
- product_id = _row_text(item, "product_id", "productId")
- if product_id:
- return f"id:{product_id}"
- product_no = _row_text(item, "product_no", "productNo")
- if product_no:
- return f"no:{product_no}"
- product_name = _row_text(item, "product_name", "productName")
- if product_name:
- return f"name:{product_name}"
- return ""
- def _product_label(item: Any) -> str:
- return _row_text(
- item, "product_name", "productName", "product_no", "productNo", "product_id", "productId"
- ) or "未命名产品"
- def _ordered_product_labels(items: Iterable[Any]) -> list[tuple[str, str]]:
- labels: dict[str, str] = {}
- order: list[str] = []
- for item in items or []:
- key = _product_key(item)
- if not key or key in labels:
- continue
- labels[key] = _product_label(item)
- order.append(key)
- return [(key, labels[key]) for key in order]
- def _delivery_product_rows(deliveries: Optional[Iterable[Any]]) -> list[Any]:
- rows: list[Any] = []
- for delivery in deliveries or []:
- if isinstance(delivery, dict):
- rows.extend(delivery.get("products") or [])
- else:
- rows.extend(getattr(delivery, "products", None) or [])
- return rows
- def load_delivery_product_gap(load: Any, deliveries: Optional[Iterable[Any]]) -> str:
- """装货段与全部送货段的产品种类必须一致。返回空字符串表示一致。"""
- load_rows = []
- if load is not None:
- load_rows = load.get("products") if isinstance(load, dict) else (getattr(load, "products", None) or [])
- load_labels = _ordered_product_labels(load_rows)
- delivery_labels = _ordered_product_labels(_delivery_product_rows(deliveries))
- delivery_keys = {key for key, _label in delivery_labels}
- load_keys = {key for key, _label in load_labels}
- only_load = [label for key, label in load_labels if key not in delivery_keys]
- only_delivery = [label for key, label in delivery_labels if key not in load_keys]
- parts: list[str] = []
- if only_load:
- parts.append(f"装货段有产品 {'、'.join(only_load)},送货段没有")
- if only_delivery:
- parts.append(f"送货段有产品 {'、'.join(only_delivery)},装货段没有")
- if not parts:
- return ""
- return f"{';'.join(parts)}。两边产品必须一致,不能保存"
- def assert_load_delivery_same_products(load: Any, deliveries: Optional[Iterable[Any]]) -> None:
- gap = load_delivery_product_gap(load, deliveries)
- if gap:
- raise HTTPException(status_code=422, detail=gap)
- def _group_plan_qty(items: Iterable[Any]) -> tuple[list[str], dict[str, tuple[Decimal, str]]]:
- groups: dict[str, tuple[Decimal, str]] = {}
- order: list[str] = []
- for item in items or []:
- key = _product_key(item)
- qty = _extract_qty(item, "plan_qty", "planQty")
- label = _product_label(item) if key else ""
- if key not in groups:
- groups[key] = (Decimal("0"), label)
- order.append(key)
- total, existing = groups[key]
- groups[key] = (total + qty, existing or label)
- return order, groups
- def warn_qty_imbalance(
- plan_products: Iterable[Any],
- load_products: Iterable[Any],
- delivery_products: Iterable[Any],
- ) -> list[str]:
- del plan_products
- load_order, load_groups = _group_plan_qty(load_products)
- delivery_order, delivery_groups = _group_plan_qty(delivery_products)
- warnings: list[str] = []
- seen: set[str] = set()
- for key in [*load_order, *delivery_order]:
- if key in seen:
- continue
- seen.add(key)
- load_qty, load_label = load_groups.get(key, (Decimal("0"), ""))
- delivery_qty, delivery_label = delivery_groups.get(key, (Decimal("0"), ""))
- if load_qty == delivery_qty:
- continue
- label = load_label or delivery_label
- text = f"装货合计({_fmt_qty(load_qty)})与送货合计({_fmt_qty(delivery_qty)})不一致,请核对"
- if label:
- text = f"产品{label}:{text}"
- warnings.append(text)
- return warnings
|