test_pl_plan_single_contract_rules.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  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_internal_customer import INTERNAL_CUSTOMER_NAMES
  11. from app.services.pl_plan_stop_service import (
  12. assert_at_most_one_contract,
  13. assert_delivery_products_in_selected_contract,
  14. assert_external_stop_products_have_contract_line,
  15. assert_load_products_in_selected_contract,
  16. assert_stop_contract_required,
  17. )
  18. def test_assert_at_most_one_contract_allows_zero_or_one():
  19. assert assert_at_most_one_contract([], "装货合同") == []
  20. one = [PlanContractLink(contractId="c1", contractNo="N1")]
  21. assert assert_at_most_one_contract(one, "装货合同") == one
  22. def test_assert_at_most_one_contract_rejects_two():
  23. links = [
  24. PlanContractLink(contractId="c1", contractNo="N1"),
  25. PlanContractLink(contractId="c2", contractNo="N2"),
  26. ]
  27. with pytest.raises(HTTPException) as exc:
  28. assert_at_most_one_contract(links, "供方合同")
  29. assert exc.value.status_code == 422
  30. assert "只能选择 1 份" in str(exc.value.detail)
  31. def test_assert_load_products_union_across_two_contracts():
  32. db = _FakeDb(["p1", "p2"])
  33. load = PlanLoadBody(
  34. customerId="cust-1",
  35. customerName="外部客户A",
  36. contracts=[
  37. PlanContractLink(contractId="c1", contractNo="N1"),
  38. PlanContractLink(contractId="c2", contractNo="N2"),
  39. ],
  40. products=[
  41. PlanStopProductBody(
  42. productId="p2",
  43. productNo="p2",
  44. productName="产品p2",
  45. unit="吨",
  46. planQty=1,
  47. )
  48. ],
  49. )
  50. assert_load_products_in_selected_contract(db, "shanxia2.0", load)
  51. class _FakeQuery:
  52. def __init__(self, rows):
  53. self._rows = rows
  54. def filter(self, *args, **kwargs):
  55. return self
  56. def all(self):
  57. return self._rows
  58. class _FakeDb:
  59. def __init__(self, product_ids):
  60. self._rows = [SimpleNamespace(product_id=pid) for pid in product_ids]
  61. def query(self, model):
  62. return _FakeQuery(self._rows)
  63. def _load_with_products(product_ids, contract_id="c1"):
  64. return PlanLoadBody(
  65. customerId="cust-1",
  66. customerName="外部客户A",
  67. contracts=[PlanContractLink(contractId=contract_id, contractNo="N1")],
  68. products=[
  69. PlanStopProductBody(
  70. productId=pid,
  71. productNo=pid,
  72. productName=f"产品{pid}",
  73. unit="吨",
  74. planQty=1,
  75. )
  76. for pid in product_ids
  77. ],
  78. )
  79. def test_assert_load_products_in_contract_passes_when_all_match():
  80. db = _FakeDb(["p1", "p2"])
  81. load = _load_with_products(["p1"])
  82. assert_load_products_in_selected_contract(db, "shanxia2.0", load)
  83. def test_assert_load_products_in_contract_rejects_outside():
  84. db = _FakeDb(["p1"])
  85. load = _load_with_products(["p2"])
  86. with pytest.raises(HTTPException) as exc:
  87. assert_load_products_in_selected_contract(db, "shanxia2.0", load)
  88. assert exc.value.status_code == 422
  89. assert "不在所选合同明细中" in str(exc.value.detail)
  90. def test_assert_load_products_skips_when_no_contract():
  91. db = _FakeDb([])
  92. load = PlanLoadBody(
  93. customerId="cust-1",
  94. customerName="外部客户A",
  95. contracts=[],
  96. products=[
  97. PlanStopProductBody(
  98. productId="p1",
  99. productNo="p1",
  100. productName="产品p1",
  101. unit="吨",
  102. planQty=1,
  103. )
  104. ],
  105. )
  106. assert_load_products_in_selected_contract(db, "shanxia2.0", load)
  107. def test_assert_load_products_skips_internal_customer():
  108. from app.services.pl_internal_customer import INTERNAL_CUSTOMER_NAMES
  109. internal = next(iter(INTERNAL_CUSTOMER_NAMES))
  110. # Empty/mismatched contract products — would 422 for external customer.
  111. db = _FakeDb([])
  112. load = PlanLoadBody(
  113. customerId="cust-1",
  114. customerName=internal,
  115. contracts=[PlanContractLink(contractId="c1", contractNo="N1")],
  116. products=[
  117. PlanStopProductBody(
  118. productId="p1",
  119. productNo="p1",
  120. productName="产品p1",
  121. unit="吨",
  122. planQty=1,
  123. )
  124. ],
  125. )
  126. assert_load_products_in_selected_contract(db, "shanxia2.0", load)
  127. def test_assert_plan_products_outside_contract_raises():
  128. db = _FakeDb(["p1"])
  129. load = _load_with_products(["p1"])
  130. plan_products = [
  131. SimpleNamespace(product_id="p2", product_name="计划外产品"),
  132. ]
  133. with pytest.raises(HTTPException) as exc:
  134. assert_load_products_in_selected_contract(
  135. db, "shanxia2.0", load, plan_products=plan_products
  136. )
  137. assert exc.value.status_code == 422
  138. assert "计划外产品" in str(exc.value.detail)
  139. assert "不在所选合同明细中" in str(exc.value.detail)
  140. def _delivery_with_products(product_ids, contract_id="c1"):
  141. return PlanDeliveryBody(
  142. seq=1,
  143. customerId="cust-2",
  144. customerName="外部客户B",
  145. contracts=[PlanContractLink(contractId=contract_id, contractNo="N2")],
  146. products=[
  147. PlanStopProductBody(
  148. productId=pid,
  149. productNo=pid,
  150. productName=f"产品{pid}",
  151. unit="吨",
  152. planQty=1,
  153. )
  154. for pid in product_ids
  155. ],
  156. )
  157. def test_assert_delivery_products_in_contract_passes_when_all_match():
  158. db = _FakeDb(["p1", "p2"])
  159. delivery = _delivery_with_products(["p1"])
  160. assert_delivery_products_in_selected_contract(db, "shanxia2.0", delivery, 1)
  161. def test_assert_delivery_products_in_contract_rejects_outside():
  162. db = _FakeDb(["p1"])
  163. delivery = _delivery_with_products(["p2"])
  164. with pytest.raises(HTTPException) as exc:
  165. assert_delivery_products_in_selected_contract(db, "shanxia2.0", delivery, 2)
  166. assert exc.value.status_code == 422
  167. assert "送货段#2产品" in str(exc.value.detail)
  168. assert "不在所选合同明细中" in str(exc.value.detail)
  169. def test_assert_delivery_products_skips_when_no_contract():
  170. db = _FakeDb([])
  171. delivery = PlanDeliveryBody(
  172. seq=1,
  173. customerId="cust-2",
  174. customerName="外部客户B",
  175. contracts=[],
  176. products=[
  177. PlanStopProductBody(
  178. productId="p1",
  179. productNo="p1",
  180. productName="产品p1",
  181. unit="吨",
  182. planQty=1,
  183. )
  184. ],
  185. )
  186. assert_delivery_products_in_selected_contract(db, "shanxia2.0", delivery, 1)
  187. def test_assert_delivery_products_skips_internal_customer():
  188. from app.services.pl_internal_customer import INTERNAL_CUSTOMER_NAMES
  189. internal = next(iter(INTERNAL_CUSTOMER_NAMES))
  190. db = _FakeDb([])
  191. delivery = PlanDeliveryBody(
  192. seq=1,
  193. customerId="cust-2",
  194. customerName=internal,
  195. contracts=[PlanContractLink(contractId="c1", contractNo="N1")],
  196. products=[
  197. PlanStopProductBody(
  198. productId="p1",
  199. productNo="p1",
  200. productName="产品p1",
  201. unit="吨",
  202. planQty=1,
  203. )
  204. ],
  205. )
  206. assert_delivery_products_in_selected_contract(db, "shanxia2.0", delivery, 1)
  207. def test_formal_save_rejects_external_product_without_contract_line():
  208. load = _load_with_products(["p1"])
  209. with pytest.raises(HTTPException) as exc:
  210. assert_external_stop_products_have_contract_line(load, [])
  211. assert exc.value.status_code == 422
  212. assert exc.value.detail == "装货产品「产品p1」没有合同编号,请从当前合同补到原来的产品行后再保存"
  213. def test_formal_save_accepts_external_product_bound_on_the_same_row():
  214. load = _load_with_products(["p1"])
  215. load.products[0].contract_id = "c1"
  216. load.products[0].contract_product_id = "line-1"
  217. assert assert_external_stop_products_have_contract_line(load, []) is None
  218. def test_formal_save_skips_internal_product_without_contract_line():
  219. internal = next(iter(INTERNAL_CUSTOMER_NAMES))
  220. load = _load_with_products(["p1"])
  221. load.customer_name = internal
  222. assert assert_external_stop_products_have_contract_line(load, []) is None
  223. def test_formal_save_names_the_delivery_segment_for_an_untagged_product():
  224. delivery = _delivery_with_products(["p1"])
  225. with pytest.raises(HTTPException) as exc:
  226. assert_external_stop_products_have_contract_line(
  227. _load_with_products([]),
  228. [delivery],
  229. )
  230. assert exc.value.status_code == 422
  231. assert "送货段#1产品" in str(exc.value.detail)
  232. def test_assert_stop_contract_required_rejects_external_without_contract():
  233. with pytest.raises(HTTPException) as exc:
  234. assert_stop_contract_required("外部客户A", [], "装货客户")
  235. assert exc.value.status_code == 422
  236. assert exc.value.detail == "请为装货客户选择合同"
  237. def test_assert_stop_contract_required_allows_external_with_contract():
  238. links = [PlanContractLink(contractId="c1", contractNo="N1")]
  239. assert_stop_contract_required("外部客户A", links, "装货客户") is None
  240. def test_assert_stop_contract_required_skips_internal_customer():
  241. from app.services.pl_internal_customer import INTERNAL_CUSTOMER_NAMES
  242. internal = next(iter(INTERNAL_CUSTOMER_NAMES))
  243. assert_stop_contract_required(internal, [], "装货客户") is None