test_pl_plan_stop_rules.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. """运输计划多段:内部客户地磅默认值与数量平衡校验"""
  2. from decimal import Decimal
  3. from app.services.pl_internal_customer import INTERNAL_CUSTOMER_NAMES
  4. from app.services.pl_plan_stop_rules import (
  5. load_delivery_product_gap,
  6. normalize_stop_dibang_fields,
  7. resolve_stop_dibang_defaults,
  8. warn_qty_imbalance,
  9. )
  10. def test_internal_shanxia_load_outbound():
  11. name = "安庆市山峡化工有限公司"
  12. assert name in INTERNAL_CUSTOMER_NAMES
  13. assert resolve_stop_dibang_defaults(name, "load") == (True, "outbound")
  14. def test_internal_shanxia_delivery_inbound():
  15. name = "安庆市山峡化工有限公司"
  16. assert resolve_stop_dibang_defaults(name, "delivery") == (True, "inbound")
  17. def test_internal_yundong_load_outbound():
  18. name = "安徽云栋化工有限公司"
  19. assert resolve_stop_dibang_defaults(name, "load") == (True, "outbound")
  20. def test_internal_yundong_delivery_inbound():
  21. name = "安徽云栋化工有限公司"
  22. assert resolve_stop_dibang_defaults(name, "delivery") == (True, "inbound")
  23. def test_external_customer_no_dibang():
  24. assert resolve_stop_dibang_defaults("外部客户有限公司", "load") == (False, "none")
  25. assert resolve_stop_dibang_defaults("外部客户有限公司", "delivery") == (False, "none")
  26. def test_delivery_defaults_use_delivery_customer():
  27. """送货段地磅默认以本段客户是否为内部客户为准。"""
  28. internal = "安庆市山峡化工有限公司"
  29. assert resolve_stop_dibang_defaults(internal, "delivery") == (True, "inbound")
  30. assert resolve_stop_dibang_defaults("外部客户有限公司", "delivery") == (False, "none")
  31. def test_normalize_stop_dibang_fields_repairs_none_direction():
  32. gen, direction = normalize_stop_dibang_fields(
  33. "安庆市山峡化工有限公司",
  34. "delivery",
  35. 1,
  36. "none",
  37. )
  38. assert gen == 1
  39. assert direction == "inbound"
  40. def test_normalize_respects_explicit_uncheck_for_internal_customer():
  41. """内部客户显式传 0(如可选车皖H21522 取消勾选)时须保留,不得用默认值强制回 1。"""
  42. gen, direction = normalize_stop_dibang_fields(
  43. "安庆市山峡化工有限公司",
  44. "load",
  45. 0,
  46. "none",
  47. )
  48. assert gen == 0
  49. assert direction == "none"
  50. def test_normalize_respects_explicit_uncheck_for_internal_delivery():
  51. gen, direction = normalize_stop_dibang_fields(
  52. "安徽云栋化工有限公司",
  53. "delivery",
  54. 0,
  55. "inbound",
  56. )
  57. assert gen == 0
  58. assert direction == "none"
  59. def test_normalize_forces_off_for_external_even_when_checked():
  60. gen, direction = normalize_stop_dibang_fields(
  61. "外部客户有限公司",
  62. "load",
  63. 1,
  64. "outbound",
  65. )
  66. assert gen == 0
  67. assert direction == "none"
  68. def test_normalize_keeps_explicit_check_for_internal():
  69. gen, direction = normalize_stop_dibang_fields(
  70. "安庆市山峡化工有限公司",
  71. "load",
  72. 1,
  73. "outbound",
  74. )
  75. assert gen == 1
  76. assert direction == "outbound"
  77. def test_external_empty_name_no_dibang():
  78. assert resolve_stop_dibang_defaults(None, "load") == (False, "none")
  79. assert resolve_stop_dibang_defaults(" ", "delivery") == (False, "none")
  80. def test_warn_qty_balanced_returns_empty():
  81. plan = [{"quantity": Decimal("30")}]
  82. load = [{"plan_qty": Decimal("30")}]
  83. delivery = [{"plan_qty": Decimal("30")}]
  84. assert warn_qty_imbalance(plan, load, delivery) == []
  85. def test_warn_qty_uses_plan_qty_on_plan_products():
  86. plan = [{"plan_qty": Decimal("28")}]
  87. load = [{"plan_qty": Decimal("28")}]
  88. delivery = [{"plan_qty": Decimal("28")}]
  89. assert warn_qty_imbalance(plan, load, delivery) == []
  90. def test_warn_qty_load_vs_delivery_imbalance():
  91. plan = [{"quantity": Decimal("28")}]
  92. load = [{"plan_qty": Decimal("30")}]
  93. delivery = [{"plan_qty": Decimal("28")}]
  94. warnings = warn_qty_imbalance(plan, load, delivery)
  95. assert warnings == [
  96. "装货合计(30)与送货合计(28)不一致,请核对",
  97. ]
  98. def test_warn_qty_delivery_vs_load_imbalance():
  99. plan = [{"quantity": Decimal("30")}]
  100. load = [{"plan_qty": Decimal("30")}]
  101. delivery = [{"plan_qty": Decimal("25")}]
  102. warnings = warn_qty_imbalance(plan, load, delivery)
  103. assert warnings == [
  104. "装货合计(30)与送货合计(25)不一致,请核对",
  105. ]
  106. def test_warn_qty_load_delivery_decimal_imbalance():
  107. plan = [{"quantity": Decimal("30")}]
  108. load = [{"plan_qty": Decimal("30")}]
  109. delivery = [{"plan_qty": Decimal("30.31")}]
  110. warnings = warn_qty_imbalance(plan, load, delivery)
  111. assert warnings == [
  112. "装货合计(30)与送货合计(30.31)不一致,请核对",
  113. ]
  114. def test_warn_qty_multi_stop_30_equals_10_15_5_balanced():
  115. """验收场景:计划 30 = 送货 10+15+5,装货 30。"""
  116. plan = [{"quantity": Decimal("30")}]
  117. load = [{"plan_qty": Decimal("30")}]
  118. delivery = [
  119. {"plan_qty": Decimal("10")},
  120. {"plan_qty": Decimal("15")},
  121. {"plan_qty": Decimal("5")},
  122. ]
  123. assert warn_qty_imbalance(plan, load, delivery) == []
  124. def test_warn_qty_sums_multiple_rows_balanced_across_stops():
  125. plan = [{"quantity": Decimal("10")}, {"quantity": Decimal("20")}]
  126. load = [{"plan_qty": Decimal("10")}, {"plan_qty": Decimal("15")}]
  127. delivery = [{"plan_qty": Decimal("25")}]
  128. assert warn_qty_imbalance(plan, load, delivery) == []
  129. def test_warn_qty_sums_multiple_rows_imbalanced_across_stops():
  130. plan = [{"quantity": Decimal("10")}, {"quantity": Decimal("20")}]
  131. load = [{"plan_qty": Decimal("10")}, {"plan_qty": Decimal("15")}]
  132. delivery = [{"plan_qty": Decimal("24")}]
  133. warnings = warn_qty_imbalance(plan, load, delivery)
  134. assert warnings == [
  135. "装货合计(25)与送货合计(24)不一致,请核对",
  136. ]
  137. def test_same_product_quantities_sum_across_packages_and_delivery_stops():
  138. load = [
  139. {"product_id": "a", "product_name": "尿素", "plan_qty": Decimal("8")},
  140. {"product_id": "a", "product_name": "尿素", "plan_qty": Decimal("2")},
  141. ]
  142. delivery = [
  143. {"product_id": "a", "product_name": "尿素", "plan_qty": Decimal("6")},
  144. {"product_id": "a", "product_name": "尿素", "plan_qty": Decimal("4")},
  145. ]
  146. assert warn_qty_imbalance([], load, delivery) == []
  147. def test_warn_qty_names_each_product_when_sums_differ():
  148. load = [
  149. {"product_id": "a", "product_name": "尿素", "plan_qty": Decimal("10")},
  150. {"product_id": "b", "product_name": "甲醇", "plan_qty": Decimal("5")},
  151. ]
  152. delivery = [
  153. {"product_id": "a", "product_name": "尿素", "plan_qty": Decimal("6")},
  154. {"product_id": "b", "product_name": "甲醇", "plan_qty": Decimal("9")},
  155. ]
  156. assert warn_qty_imbalance([], load, delivery) == [
  157. "产品尿素:装货合计(10)与送货合计(6)不一致,请核对",
  158. "产品甲醇:装货合计(5)与送货合计(9)不一致,请核对",
  159. ]
  160. def test_product_set_mismatch_names_both_sides():
  161. load = {
  162. "products": [
  163. {"product_id": "a", "product_name": "A"},
  164. {"product_id": "d", "product_name": "D"},
  165. ]
  166. }
  167. deliveries = [
  168. {"products": [{"product_id": "a", "product_name": "A"}]},
  169. {
  170. "products": [
  171. {"product_id": "b", "product_name": "B"},
  172. {"product_id": "c", "product_name": "C"},
  173. ]
  174. },
  175. ]
  176. assert load_delivery_product_gap(load, deliveries) == (
  177. "装货段有产品 D,送货段没有;送货段有产品 B、C,装货段没有。两边产品必须一致,不能保存"
  178. )
  179. def test_same_product_with_different_packages_is_one_product():
  180. load = {
  181. "products": [
  182. {"product_id": "a", "product_name": "尿素", "package_spec": "桶"},
  183. {"product_id": "a", "product_name": "尿素", "package_spec": "散装"},
  184. ]
  185. }
  186. deliveries = [{"products": [{"product_id": "a", "product_name": "尿素", "package_spec": "袋"}]}]
  187. assert load_delivery_product_gap(load, deliveries) == ""
  188. def test_empty_product_rows_match():
  189. assert load_delivery_product_gap({"products": []}, [{"products": []}]) == ""