test_pl_contract_plan_qty.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. """合同产品运输安排数量汇总单元测试。"""
  2. from types import SimpleNamespace
  3. from app.services.pl_contract_plan_qty import (
  4. _bucket_for_plan,
  5. accumulate_stop_products,
  6. merge_product_qty_maps,
  7. resolve_linked_plan_scopes,
  8. sum_qtys_for_scope,
  9. )
  10. from collections import defaultdict
  11. def test_bucket_archived():
  12. assert (
  13. _bucket_for_plan(
  14. archive_status="archived",
  15. follow_status="arranged",
  16. transport_status="arranged",
  17. )
  18. == "archived"
  19. )
  20. def test_bucket_arranged_unarchived_includes_draft_archive():
  21. assert (
  22. _bucket_for_plan(
  23. archive_status="draft",
  24. follow_status="arranged",
  25. transport_status="arranged",
  26. )
  27. == "arranged_unarchived"
  28. )
  29. def test_bucket_skips_pending_and_cancelled():
  30. assert (
  31. _bucket_for_plan(
  32. archive_status="unarchived",
  33. follow_status="pending_arrange",
  34. transport_status="pending",
  35. )
  36. is None
  37. )
  38. assert (
  39. _bucket_for_plan(
  40. archive_status="unarchived",
  41. follow_status="cancelled",
  42. transport_status="arranged",
  43. )
  44. is None
  45. )
  46. def test_merge_product_qty_maps_totals():
  47. out = merge_product_qty_maps(
  48. archived={"p1": 120.0, "p2": 50.0},
  49. arranged_unarchived={"p1": 80.0, "p3": 100.0},
  50. )
  51. assert out["p1"]["archivedQty"] == 120.0
  52. assert out["p1"]["arrangedUnarchivedQty"] == 80.0
  53. assert out["p1"]["arrangedTotalQty"] == 200.0
  54. assert out["p2"]["arrangedTotalQty"] == 50.0
  55. assert out["p3"]["arrangedTotalQty"] == 100.0
  56. def test_sum_qtys_explicit_delivery_stop_only():
  57. load_products = {
  58. "L1": [SimpleNamespace(product_id="p1", plan_qty=10)],
  59. }
  60. delivery_products = {
  61. "D1": [SimpleNamespace(product_id="p1", plan_qty=40)],
  62. "D2": [SimpleNamespace(product_id="p1", plan_qty=50)],
  63. }
  64. qtys = sum_qtys_for_scope(
  65. load_products_by_load=load_products,
  66. delivery_products_by_delivery=delivery_products,
  67. all_load_ids={"L1"},
  68. all_delivery_ids={"D1", "D2"},
  69. scope={"load_ids": set(), "delivery_ids": {"D1"}, "use_all_stops": False},
  70. )
  71. assert qtys == {"p1": 40.0}
  72. def test_legacy_use_all_stops_prefers_delivery():
  73. load_products = {
  74. "L1": [SimpleNamespace(product_id="p1", plan_qty=99)],
  75. }
  76. delivery_products = {
  77. "D1": [SimpleNamespace(product_id="p1", plan_qty=40)],
  78. }
  79. qtys = sum_qtys_for_scope(
  80. load_products_by_load=load_products,
  81. delivery_products_by_delivery=delivery_products,
  82. all_load_ids={"L1"},
  83. all_delivery_ids={"D1"},
  84. scope={"load_ids": set(), "delivery_ids": set(), "use_all_stops": True},
  85. )
  86. assert qtys == {"p1": 40.0}
  87. def test_resolve_scopes_marks_legacy():
  88. links = [
  89. SimpleNamespace(plan_id="A", side="delivery", stop_id="D1"),
  90. ]
  91. scopes = resolve_linked_plan_scopes(links, ["B"])
  92. assert scopes["A"]["delivery_ids"] == {"D1"}
  93. assert scopes["A"]["has_explicit"] is True
  94. assert scopes["B"]["use_all_stops"] is True
  95. def test_accumulate_stop_products():
  96. acc = defaultdict(float)
  97. accumulate_stop_products(
  98. acc,
  99. [
  100. SimpleNamespace(product_id="p1", plan_qty=DecimalLike(12.5)),
  101. SimpleNamespace(product_id="p1", plan_qty=7.5),
  102. SimpleNamespace(product_id="", plan_qty=9),
  103. ],
  104. )
  105. assert acc["p1"] == 20.0
  106. def test_accumulate_archive_actual_load_uses_inbound_qty():
  107. acc = defaultdict(float)
  108. accumulate_stop_products(
  109. acc,
  110. [
  111. SimpleNamespace(product_id="p1", plan_qty=100, inbound_qty=88),
  112. SimpleNamespace(product_id="p1", plan_qty=50, inbound_qty=None),
  113. ],
  114. stop_kind="load",
  115. qty_mode="archive_actual",
  116. )
  117. # 88 + 回退 plan_qty 50
  118. assert acc["p1"] == 138.0
  119. def test_accumulate_archive_actual_delivery_uses_outbound_qty():
  120. acc = defaultdict(float)
  121. accumulate_stop_products(
  122. acc,
  123. [SimpleNamespace(product_id="p1", plan_qty=100, outbound_qty=66)],
  124. stop_kind="delivery",
  125. qty_mode="archive_actual",
  126. )
  127. assert acc["p1"] == 66.0
  128. def test_accumulate_archive_actual_zero_does_not_fallback():
  129. acc = defaultdict(float)
  130. accumulate_stop_products(
  131. acc,
  132. [SimpleNamespace(product_id="p1", plan_qty=100, inbound_qty=0)],
  133. stop_kind="load",
  134. qty_mode="archive_actual",
  135. )
  136. assert acc["p1"] == 0.0
  137. def test_sum_qtys_archive_actual_mode_on_scoped_stops():
  138. load_products = {
  139. "L1": [SimpleNamespace(product_id="p1", plan_qty=10, inbound_qty=7)],
  140. }
  141. delivery_products = {
  142. "D1": [SimpleNamespace(product_id="p1", plan_qty=40, outbound_qty=33)],
  143. }
  144. qtys = sum_qtys_for_scope(
  145. load_products_by_load=load_products,
  146. delivery_products_by_delivery=delivery_products,
  147. all_load_ids={"L1"},
  148. all_delivery_ids={"D1"},
  149. scope={"load_ids": {"L1"}, "delivery_ids": {"D1"}, "use_all_stops": False},
  150. qty_mode="archive_actual",
  151. )
  152. assert qtys == {"p1": 40.0} # 7 + 33
  153. def test_sum_qtys_default_mode_ignores_actual_fields():
  154. load_products = {
  155. "L1": [SimpleNamespace(product_id="p1", plan_qty=10, inbound_qty=7)],
  156. }
  157. delivery_products = {
  158. "D1": [SimpleNamespace(product_id="p1", plan_qty=40, outbound_qty=33)],
  159. }
  160. qtys = sum_qtys_for_scope(
  161. load_products_by_load=load_products,
  162. delivery_products_by_delivery=delivery_products,
  163. all_load_ids={"L1"},
  164. all_delivery_ids={"D1"},
  165. scope={"load_ids": set(), "delivery_ids": {"D1"}, "use_all_stops": False},
  166. )
  167. assert qtys == {"p1": 40.0}
  168. class DecimalLike:
  169. def __init__(self, v):
  170. self.v = v
  171. def __float__(self):
  172. return float(self.v)