test_pl_plan_single_contract_rules.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  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. def test_assert_load_products_union_across_two_contracts():
  30. db = _FakeDb(["p1", "p2"])
  31. load = PlanLoadBody(
  32. customerId="cust-1",
  33. customerName="外部客户A",
  34. contracts=[
  35. PlanContractLink(contractId="c1", contractNo="N1"),
  36. PlanContractLink(contractId="c2", contractNo="N2"),
  37. ],
  38. products=[
  39. PlanStopProductBody(
  40. productId="p2",
  41. productNo="p2",
  42. productName="产品p2",
  43. unit="吨",
  44. planQty=1,
  45. )
  46. ],
  47. )
  48. assert_load_products_in_selected_contract(db, "shanxia2.0", load)
  49. class _FakeQuery:
  50. def __init__(self, rows):
  51. self._rows = rows
  52. def filter(self, *args, **kwargs):
  53. return self
  54. def all(self):
  55. return self._rows
  56. class _FakeDb:
  57. def __init__(self, product_ids):
  58. self._rows = [SimpleNamespace(product_id=pid) for pid in product_ids]
  59. def query(self, model):
  60. return _FakeQuery(self._rows)
  61. def _load_with_products(product_ids, contract_id="c1"):
  62. return PlanLoadBody(
  63. customerId="cust-1",
  64. customerName="外部客户A",
  65. contracts=[PlanContractLink(contractId=contract_id, contractNo="N1")],
  66. products=[
  67. PlanStopProductBody(
  68. productId=pid,
  69. productNo=pid,
  70. productName=f"产品{pid}",
  71. unit="吨",
  72. planQty=1,
  73. )
  74. for pid in product_ids
  75. ],
  76. )
  77. def test_assert_load_products_in_contract_passes_when_all_match():
  78. db = _FakeDb(["p1", "p2"])
  79. load = _load_with_products(["p1"])
  80. assert_load_products_in_selected_contract(db, "shanxia2.0", load)
  81. def test_assert_load_products_in_contract_rejects_outside():
  82. db = _FakeDb(["p1"])
  83. load = _load_with_products(["p2"])
  84. with pytest.raises(HTTPException) as exc:
  85. assert_load_products_in_selected_contract(db, "shanxia2.0", load)
  86. assert exc.value.status_code == 422
  87. assert "不在所选合同明细中" in str(exc.value.detail)
  88. def test_assert_load_products_skips_when_no_contract():
  89. db = _FakeDb([])
  90. load = PlanLoadBody(
  91. customerId="cust-1",
  92. customerName="外部客户A",
  93. contracts=[],
  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_load_products_skips_internal_customer():
  106. from app.services.pl_internal_customer import INTERNAL_CUSTOMER_NAMES
  107. internal = next(iter(INTERNAL_CUSTOMER_NAMES))
  108. # Empty/mismatched contract products — would 422 for external customer.
  109. db = _FakeDb([])
  110. load = PlanLoadBody(
  111. customerId="cust-1",
  112. customerName=internal,
  113. contracts=[PlanContractLink(contractId="c1", contractNo="N1")],
  114. products=[
  115. PlanStopProductBody(
  116. productId="p1",
  117. productNo="p1",
  118. productName="产品p1",
  119. unit="吨",
  120. planQty=1,
  121. )
  122. ],
  123. )
  124. assert_load_products_in_selected_contract(db, "shanxia2.0", load)
  125. def test_assert_plan_products_outside_contract_raises():
  126. db = _FakeDb(["p1"])
  127. load = _load_with_products(["p1"])
  128. plan_products = [
  129. SimpleNamespace(product_id="p2", product_name="计划外产品"),
  130. ]
  131. with pytest.raises(HTTPException) as exc:
  132. assert_load_products_in_selected_contract(
  133. db, "shanxia2.0", load, plan_products=plan_products
  134. )
  135. assert exc.value.status_code == 422
  136. assert "计划外产品" in str(exc.value.detail)
  137. assert "不在所选合同明细中" in str(exc.value.detail)
  138. def _delivery_with_products(product_ids, contract_id="c1"):
  139. return PlanDeliveryBody(
  140. seq=1,
  141. customerId="cust-2",
  142. customerName="外部客户B",
  143. contracts=[PlanContractLink(contractId=contract_id, contractNo="N2")],
  144. products=[
  145. PlanStopProductBody(
  146. productId=pid,
  147. productNo=pid,
  148. productName=f"产品{pid}",
  149. unit="吨",
  150. planQty=1,
  151. )
  152. for pid in product_ids
  153. ],
  154. )
  155. def test_assert_delivery_products_in_contract_passes_when_all_match():
  156. db = _FakeDb(["p1", "p2"])
  157. delivery = _delivery_with_products(["p1"])
  158. assert_delivery_products_in_selected_contract(db, "shanxia2.0", delivery, 1)
  159. def test_assert_delivery_products_in_contract_rejects_outside():
  160. db = _FakeDb(["p1"])
  161. delivery = _delivery_with_products(["p2"])
  162. with pytest.raises(HTTPException) as exc:
  163. assert_delivery_products_in_selected_contract(db, "shanxia2.0", delivery, 2)
  164. assert exc.value.status_code == 422
  165. assert "送货段#2产品" in str(exc.value.detail)
  166. assert "不在所选合同明细中" in str(exc.value.detail)
  167. def test_assert_delivery_products_skips_when_no_contract():
  168. db = _FakeDb([])
  169. delivery = PlanDeliveryBody(
  170. seq=1,
  171. customerId="cust-2",
  172. customerName="外部客户B",
  173. contracts=[],
  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_delivery_products_skips_internal_customer():
  186. from app.services.pl_internal_customer import INTERNAL_CUSTOMER_NAMES
  187. internal = next(iter(INTERNAL_CUSTOMER_NAMES))
  188. db = _FakeDb([])
  189. delivery = PlanDeliveryBody(
  190. seq=1,
  191. customerId="cust-2",
  192. customerName=internal,
  193. contracts=[PlanContractLink(contractId="c1", contractNo="N1")],
  194. products=[
  195. PlanStopProductBody(
  196. productId="p1",
  197. productNo="p1",
  198. productName="产品p1",
  199. unit="吨",
  200. planQty=1,
  201. )
  202. ],
  203. )
  204. assert_delivery_products_in_selected_contract(db, "shanxia2.0", delivery, 1)
  205. def test_assert_stop_contract_required_rejects_external_without_contract():
  206. with pytest.raises(HTTPException) as exc:
  207. assert_stop_contract_required("外部客户A", [], "装货客户")
  208. assert exc.value.status_code == 422
  209. assert exc.value.detail == "请为装货客户选择合同"
  210. def test_assert_stop_contract_required_allows_external_with_contract():
  211. links = [PlanContractLink(contractId="c1", contractNo="N1")]
  212. assert_stop_contract_required("外部客户A", links, "装货客户") is None
  213. def test_assert_stop_contract_required_skips_internal_customer():
  214. from app.services.pl_internal_customer import INTERNAL_CUSTOMER_NAMES
  215. internal = next(iter(INTERNAL_CUSTOMER_NAMES))
  216. assert_stop_contract_required(internal, [], "装货客户") is None