pl_plan_stop_rules.py 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. """运输计划多段:内部客户地磅默认值与数量平衡校验"""
  2. from __future__ import annotations
  3. from decimal import Decimal
  4. from typing import Any, Iterable, Literal, Optional, Tuple
  5. from app.services.pl_internal_customer import is_internal_customer_name
  6. StopType = Literal["load", "delivery"]
  7. def resolve_stop_dibang_defaults(
  8. customer_name: Optional[str],
  9. stop_type: StopType,
  10. ) -> Tuple[bool, str]:
  11. """地磅默认勾选以本段客户是否为内部客户(山峡/云栋)为准。"""
  12. if not is_internal_customer_name(customer_name):
  13. return False, "none"
  14. if stop_type == "load":
  15. return True, "outbound"
  16. if stop_type == "delivery":
  17. return True, "inbound"
  18. return False, "none"
  19. def normalize_stop_dibang_fields(
  20. customer_name: Optional[str],
  21. stop_type: StopType,
  22. generate_dibang: Optional[int],
  23. dibang_direction: Optional[str],
  24. ) -> Tuple[int, str]:
  25. """归一化段次地磅勾选与方向。
  26. - 非内部客户:强制不生成地磅单
  27. - 内部客户:尊重显式勾选值(含可选车取消勾选传 0);仅 None 时用默认
  28. - 勾选但方向缺失/none 时按本段客户补默认方向
  29. """
  30. if not is_internal_customer_name(customer_name):
  31. return 0, "none"
  32. default_gen, default_dir = resolve_stop_dibang_defaults(customer_name, stop_type)
  33. if generate_dibang is None:
  34. gen = 1 if default_gen else 0
  35. else:
  36. gen = 1 if int(generate_dibang) == 1 else 0
  37. direction = (dibang_direction or "").strip()
  38. if gen:
  39. if direction not in ("outbound", "inbound"):
  40. direction = default_dir if default_dir != "none" else (
  41. "outbound" if stop_type == "load" else "inbound"
  42. )
  43. else:
  44. direction = "none"
  45. return gen, direction
  46. def _extract_qty(item: Any, *field_names: str) -> Decimal:
  47. for name in field_names:
  48. if isinstance(item, dict):
  49. value = item.get(name)
  50. else:
  51. value = getattr(item, name, None)
  52. if value is not None:
  53. return Decimal(str(value))
  54. return Decimal("0")
  55. def _sum_qty(items: Iterable[Any], *field_names: str) -> Decimal:
  56. total = Decimal("0")
  57. for item in items or []:
  58. total += _extract_qty(item, *field_names)
  59. return total
  60. def _fmt_qty(qty: Decimal) -> str:
  61. if qty == qty.to_integral_value():
  62. return str(int(qty))
  63. return format(qty.normalize(), "f")
  64. def warn_qty_imbalance(
  65. plan_products: Iterable[Any],
  66. load_products: Iterable[Any],
  67. delivery_products: Iterable[Any],
  68. ) -> list[str]:
  69. load_total = _sum_qty(load_products, "plan_qty")
  70. delivery_total = _sum_qty(delivery_products, "plan_qty")
  71. if load_total == delivery_total:
  72. return []
  73. return [
  74. f"装货合计({_fmt_qty(load_total)})与送货合计({_fmt_qty(delivery_total)})不一致,请核对"
  75. ]