test_pl_brand_archive.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. from decimal import Decimal
  2. import pytest
  3. from app.services.pl_brand_archive import (
  4. expand_export_rows_by_brand_qty,
  5. expand_export_rows_for_plan_brand,
  6. format_plan_product_constraint_brand,
  7. is_archived_for_export_brand,
  8. resolve_export_brand_qty_rows,
  9. validate_brand_qty_rows,
  10. )
  11. from types import SimpleNamespace
  12. def test_resolve_export_brand_qty_rows_prefers_stop_level():
  13. by_stop = {
  14. "sp1": [{"brandId": "b1", "brandName": "君正", "qty": Decimal("32")}],
  15. }
  16. by_plan = {
  17. "pp1": [{"brandId": "b2", "brandName": "红四方", "qty": Decimal("10")}],
  18. }
  19. rows = resolve_export_brand_qty_rows(
  20. stop_product_id="sp1",
  21. plan_product_id="pp1",
  22. by_stop_id=by_stop,
  23. by_plan_product_id=by_plan,
  24. )
  25. assert len(rows) == 1
  26. assert rows[0]["brandName"] == "君正"
  27. def test_resolve_export_brand_qty_rows_falls_back_to_plan_level():
  28. rows = resolve_export_brand_qty_rows(
  29. stop_product_id="missing",
  30. plan_product_id="pp1",
  31. by_stop_id={},
  32. by_plan_product_id={
  33. "pp1": [{"brandId": "b1", "brandName": "君正", "qty": Decimal("32")}],
  34. },
  35. )
  36. assert len(rows) == 1
  37. assert rows[0]["brandName"] == "君正"
  38. def test_resolve_export_brand_qty_rows_empty():
  39. assert (
  40. resolve_export_brand_qty_rows(
  41. stop_product_id=None,
  42. plan_product_id=None,
  43. by_stop_id={},
  44. by_plan_product_id={},
  45. )
  46. == []
  47. )
  48. def test_expand_export_rows_by_brand_qty_splits_and_blanks():
  49. base = {"productName": "片碱", "planQty": 34, "actualQty": 32, "brandName": "旧"}
  50. blank = expand_export_rows_by_brand_qty(base, [])
  51. assert len(blank) == 1
  52. assert blank[0]["brandName"] == ""
  53. assert blank[0]["actualQty"] == 32
  54. split = expand_export_rows_by_brand_qty(
  55. base,
  56. [
  57. {"brandId": "b1", "brandName": "君正", "qty": Decimal("20")},
  58. {"brandId": "b2", "brandName": "红四方", "qty": Decimal("12")},
  59. ],
  60. )
  61. assert [r["brandName"] for r in split] == ["君正", "红四方"]
  62. assert [r["actualQty"] for r in split] == [Decimal("20"), Decimal("12")]
  63. assert all(r["planQty"] == 34 for r in split)
  64. def test_archive_status_gate_for_export_brand():
  65. assert is_archived_for_export_brand("archived") is True
  66. assert is_archived_for_export_brand("unarchived") is False
  67. assert is_archived_for_export_brand("draft") is False
  68. product = SimpleNamespace(brand_mode="required", brand_name="君正")
  69. assert format_plan_product_constraint_brand(product) == "君正"
  70. assert (
  71. format_plan_product_constraint_brand(
  72. SimpleNamespace(brand_mode="any", brand_name="君正")
  73. )
  74. == ""
  75. )
  76. def test_expand_export_rows_for_plan_brand_draft_uses_product_not_qty():
  77. base = {"productName": "片碱", "planQty": 34, "actualQty": 32, "brandName": ""}
  78. product = SimpleNamespace(id="pp1", brand_mode="required", brand_name="君正")
  79. rows = expand_export_rows_for_plan_brand(
  80. base,
  81. archive_status="draft",
  82. plan_product=product,
  83. stop_product_id="lp1",
  84. by_stop_id={
  85. "lp1": [
  86. {"brandId": "b1", "brandName": "红四方", "qty": Decimal("20")},
  87. {"brandId": "b2", "brandName": "君正", "qty": Decimal("12")},
  88. ]
  89. },
  90. )
  91. assert len(rows) == 1
  92. assert rows[0]["brandName"] == "君正"
  93. assert rows[0]["actualQty"] == 32
  94. def test_expand_export_rows_for_plan_brand_archived_splits_qty():
  95. base = {"productName": "片碱", "planQty": 34, "actualQty": 32, "brandName": ""}
  96. product = SimpleNamespace(id="pp1", brand_mode="any", brand_name=None)
  97. rows = expand_export_rows_for_plan_brand(
  98. base,
  99. archive_status="archived",
  100. plan_product=product,
  101. stop_product_id="lp1",
  102. by_stop_id={
  103. "lp1": [
  104. {"brandId": "b1", "brandName": "君正", "qty": Decimal("20")},
  105. {"brandId": "b2", "brandName": "红四方", "qty": Decimal("12")},
  106. ]
  107. },
  108. )
  109. assert [r["brandName"] for r in rows] == ["君正", "红四方"]
  110. assert [r["actualQty"] for r in rows] == [Decimal("20"), Decimal("12")]
  111. def test_required_allows_other_brand_when_sum_matches():
  112. validate_brand_qty_rows(
  113. brand_mode="required",
  114. required_brand_id="b1",
  115. actual_qty=Decimal("10"),
  116. rows=[{"brandId": "b2", "brandName": "红四方", "qty": Decimal("10")}],
  117. )
  118. def test_required_allows_multi_brand_when_sum_matches():
  119. validate_brand_qty_rows(
  120. brand_mode="required",
  121. required_brand_id="b1",
  122. actual_qty=Decimal("10"),
  123. rows=[
  124. {"brandId": "b1", "brandName": "君正", "qty": Decimal("6")},
  125. {"brandId": "b2", "brandName": "红四方", "qty": Decimal("4")},
  126. ],
  127. )
  128. def test_required_sum_must_match():
  129. with pytest.raises(ValueError, match="合计"):
  130. validate_brand_qty_rows(
  131. brand_mode="required",
  132. required_brand_id="b1",
  133. actual_qty=Decimal("10"),
  134. rows=[
  135. {"brandId": "b1", "brandName": "君正", "qty": Decimal("6")},
  136. {"brandId": "b2", "brandName": "红四方", "qty": Decimal("3")},
  137. ],
  138. )
  139. def test_any_sum_must_match():
  140. with pytest.raises(ValueError, match="合计"):
  141. validate_brand_qty_rows(
  142. brand_mode="any",
  143. required_brand_id=None,
  144. actual_qty=Decimal("10"),
  145. rows=[
  146. {"brandId": "b1", "brandName": "君正", "qty": Decimal("6")},
  147. {"brandId": "b2", "brandName": "红四方", "qty": Decimal("3")},
  148. ],
  149. )
  150. def test_any_ok():
  151. validate_brand_qty_rows(
  152. brand_mode="any",
  153. required_brand_id=None,
  154. actual_qty=Decimal("10"),
  155. rows=[
  156. {"brandId": "b1", "brandName": "君正", "qty": Decimal("6")},
  157. {"brandId": "b2", "brandName": "红四方", "qty": Decimal("4")},
  158. ],
  159. )
  160. def test_no_brand_ignores_rows():
  161. validate_brand_qty_rows(
  162. brand_mode=None,
  163. required_brand_id=None,
  164. actual_qty=Decimal("5"),
  165. rows=[],
  166. )
  167. def test_delta_for_plan_type():
  168. from app.services.pl_inventory_service import qty_delta_for_plan
  169. assert qty_delta_for_plan("inbound", Decimal("3")) == Decimal("3")
  170. assert qty_delta_for_plan("outbound", Decimal("3")) == Decimal("-3")
  171. assert qty_delta_for_plan("direct", Decimal("3")) == Decimal("0")