test_pl_plan_single_contract_rules.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import pytest
  2. from fastapi import HTTPException
  3. from types import SimpleNamespace
  4. from app.schemas.pl.plan import (
  5. PlanContractLink,
  6. PlanDeliveryBody,
  7. PlanLoadBody,
  8. PlanStopProductBody,
  9. )
  10. from app.services.pl_plan_stop_service import (
  11. assert_at_most_one_contract,
  12. assert_delivery_products_in_selected_contract,
  13. assert_load_products_in_selected_contract,
  14. assert_stop_contract_required,
  15. )
  16. def test_assert_at_most_one_contract_allows_zero_or_one():
  17. assert assert_at_most_one_contract([], "装货合同") == []
  18. one = [PlanContractLink(contractId="c1", contractNo="N1")]
  19. assert assert_at_most_one_contract(one, "装货合同") == one
  20. def test_assert_at_most_one_contract_rejects_two():
  21. links = [
  22. PlanContractLink(contractId="c1", contractNo="N1"),
  23. PlanContractLink(contractId="c2", contractNo="N2"),
  24. ]
  25. with pytest.raises(HTTPException) as exc:
  26. assert_at_most_one_contract(links, "装货合同")
  27. assert exc.value.status_code == 422
  28. assert "只能选择 1 份" in str(exc.value.detail)
  29. class _FakeQuery:
  30. def __init__(self, rows):
  31. self._rows = rows
  32. def filter(self, *args, **kwargs):
  33. return self
  34. def all(self):
  35. return self._rows
  36. class _FakeDb:
  37. def __init__(self, product_ids):
  38. self._rows = [SimpleNamespace(product_id=pid) for pid in product_ids]
  39. def query(self, model):
  40. return _FakeQuery(self._rows)
  41. def _load_with_products(product_ids, contract_id="c1"):
  42. return PlanLoadBody(
  43. customerId="cust-1",
  44. customerName="外部客户A",
  45. contracts=[PlanContractLink(contractId=contract_id, contractNo="N1")],
  46. products=[
  47. PlanStopProductBody(
  48. productId=pid,
  49. productNo=pid,
  50. productName=f"产品{pid}",
  51. unit="吨",
  52. planQty=1,
  53. )
  54. for pid in product_ids
  55. ],
  56. )
  57. def test_assert_load_products_in_contract_passes_when_all_match():
  58. db = _FakeDb(["p1", "p2"])
  59. load = _load_with_products(["p1"])
  60. assert_load_products_in_selected_contract(db, "shanxia2.0", load)
  61. def test_assert_load_products_in_contract_rejects_outside():
  62. db = _FakeDb(["p1"])
  63. load = _load_with_products(["p2"])
  64. with pytest.raises(HTTPException) as exc:
  65. assert_load_products_in_selected_contract(db, "shanxia2.0", load)
  66. assert exc.value.status_code == 422
  67. assert "不在所选合同明细中" in str(exc.value.detail)
  68. def test_assert_load_products_skips_when_no_contract():
  69. db = _FakeDb([])
  70. load = PlanLoadBody(
  71. customerId="cust-1",
  72. customerName="外部客户A",
  73. contracts=[],
  74. products=[
  75. PlanStopProductBody(
  76. productId="p1",
  77. productNo="p1",
  78. productName="产品p1",
  79. unit="吨",
  80. planQty=1,
  81. )
  82. ],
  83. )
  84. assert_load_products_in_selected_contract(db, "shanxia2.0", load)
  85. def test_assert_load_products_skips_internal_customer():
  86. from app.services.pl_internal_customer import INTERNAL_CUSTOMER_NAMES
  87. internal = next(iter(INTERNAL_CUSTOMER_NAMES))
  88. # Empty/mismatched contract products — would 422 for external customer.
  89. db = _FakeDb([])
  90. load = PlanLoadBody(
  91. customerId="cust-1",
  92. customerName=internal,
  93. contracts=[PlanContractLink(contractId="c1", contractNo="N1")],
  94. products=[
  95. PlanStopProductBody(
  96. productId="p1",
  97. productNo="p1",
  98. productName="产品p1",
  99. unit="吨",
  100. planQty=1,
  101. )
  102. ],
  103. )
  104. assert_load_products_in_selected_contract(db, "shanxia2.0", load)
  105. def test_assert_plan_products_outside_contract_raises():
  106. db = _FakeDb(["p1"])
  107. load = _load_with_products(["p1"])
  108. plan_products = [
  109. SimpleNamespace(product_id="p2", product_name="计划外产品"),
  110. ]
  111. with pytest.raises(HTTPException) as exc:
  112. assert_load_products_in_selected_contract(
  113. db, "shanxia2.0", load, plan_products=plan_products
  114. )
  115. assert exc.value.status_code == 422
  116. assert "计划外产品" in str(exc.value.detail)
  117. assert "不在所选合同明细中" in str(exc.value.detail)
  118. def _delivery_with_products(product_ids, contract_id="c1"):
  119. return PlanDeliveryBody(
  120. seq=1,
  121. customerId="cust-2",
  122. customerName="外部客户B",
  123. contracts=[PlanContractLink(contractId=contract_id, contractNo="N2")],
  124. products=[
  125. PlanStopProductBody(
  126. productId=pid,
  127. productNo=pid,
  128. productName=f"产品{pid}",
  129. unit="吨",
  130. planQty=1,
  131. )
  132. for pid in product_ids
  133. ],
  134. )
  135. def test_assert_delivery_products_in_contract_passes_when_all_match():
  136. db = _FakeDb(["p1", "p2"])
  137. delivery = _delivery_with_products(["p1"])
  138. assert_delivery_products_in_selected_contract(db, "shanxia2.0", delivery, 1)
  139. def test_assert_delivery_products_in_contract_rejects_outside():
  140. db = _FakeDb(["p1"])
  141. delivery = _delivery_with_products(["p2"])
  142. with pytest.raises(HTTPException) as exc:
  143. assert_delivery_products_in_selected_contract(db, "shanxia2.0", delivery, 2)
  144. assert exc.value.status_code == 422
  145. assert "送货段#2产品" in str(exc.value.detail)
  146. assert "不在所选合同明细中" in str(exc.value.detail)
  147. def test_assert_delivery_products_skips_when_no_contract():
  148. db = _FakeDb([])
  149. delivery = PlanDeliveryBody(
  150. seq=1,
  151. customerId="cust-2",
  152. customerName="外部客户B",
  153. contracts=[],
  154. products=[
  155. PlanStopProductBody(
  156. productId="p1",
  157. productNo="p1",
  158. productName="产品p1",
  159. unit="吨",
  160. planQty=1,
  161. )
  162. ],
  163. )
  164. assert_delivery_products_in_selected_contract(db, "shanxia2.0", delivery, 1)
  165. def test_assert_delivery_products_skips_internal_customer():
  166. from app.services.pl_internal_customer import INTERNAL_CUSTOMER_NAMES
  167. internal = next(iter(INTERNAL_CUSTOMER_NAMES))
  168. db = _FakeDb([])
  169. delivery = PlanDeliveryBody(
  170. seq=1,
  171. customerId="cust-2",
  172. customerName=internal,
  173. contracts=[PlanContractLink(contractId="c1", contractNo="N1")],
  174. products=[
  175. PlanStopProductBody(
  176. productId="p1",
  177. productNo="p1",
  178. productName="产品p1",
  179. unit="吨",
  180. planQty=1,
  181. )
  182. ],
  183. )
  184. assert_delivery_products_in_selected_contract(db, "shanxia2.0", delivery, 1)
  185. def test_assert_stop_contract_required_rejects_external_without_contract():
  186. with pytest.raises(HTTPException) as exc:
  187. assert_stop_contract_required("外部客户A", [], "装货客户")
  188. assert exc.value.status_code == 422
  189. assert exc.value.detail == "请为装货客户选择合同"
  190. def test_assert_stop_contract_required_allows_external_with_contract():
  191. links = [PlanContractLink(contractId="c1", contractNo="N1")]
  192. assert_stop_contract_required("外部客户A", links, "装货客户") is None
  193. def test_assert_stop_contract_required_skips_internal_customer():
  194. from app.services.pl_internal_customer import INTERNAL_CUSTOMER_NAMES
  195. internal = next(iter(INTERNAL_CUSTOMER_NAMES))
  196. assert_stop_contract_required(internal, [], "装货客户") is None