test_pl_plan_archive_export.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. from decimal import Decimal
  2. from unittest.mock import MagicMock, patch
  3. from app.schemas.pl.plan import PlanDeliveryRead, PlanLoadRead, PlanStopProductRead
  4. from app.services.pl_internal_customer import INTERNAL_CUSTOMER_NAMES
  5. from app.services.pl_plan_archive_export import (
  6. _append_paired_product_rows,
  7. _append_stop_product_rows,
  8. _plan_common_export_fields,
  9. build_plan_archive_export_rows,
  10. resolve_archive_list_io_column,
  11. )
  12. def _stop_product(**kwargs) -> PlanStopProductRead:
  13. defaults = {
  14. "id": "sp1",
  15. "planId": "p1",
  16. "tenantNo": "t1",
  17. "productId": "prod1",
  18. "productNo": "P001",
  19. "productName": "甲醇",
  20. "model": "99%",
  21. "unit": "吨",
  22. "packageSpec": "散装",
  23. "planQty": Decimal("10"),
  24. "createTime": "2026-08-27T00:00:00",
  25. "updateTime": "2026-08-27T00:00:00",
  26. }
  27. defaults.update(kwargs)
  28. return PlanStopProductRead(**defaults)
  29. def test_resolve_archive_list_io_column():
  30. internal = next(iter(INTERNAL_CUSTOMER_NAMES))
  31. assert resolve_archive_list_io_column("load", internal) == "outbound"
  32. assert resolve_archive_list_io_column("load", "外部客户") == "inbound"
  33. assert resolve_archive_list_io_column("delivery", internal) == "inbound"
  34. assert resolve_archive_list_io_column("delivery", "外部客户") == "outbound"
  35. def test_append_stop_product_rows_io_and_amount():
  36. internal = next(iter(INTERNAL_CUSTOMER_NAMES))
  37. product = _stop_product(
  38. inboundQty=Decimal("9.5"),
  39. inboundPrice=Decimal("100"),
  40. inboundAmount=None,
  41. )
  42. load = PlanLoadRead(
  43. id="load1",
  44. planId="p1",
  45. tenantNo="t1",
  46. customerName=internal,
  47. address="装货地址",
  48. loadTime="2026-08-27T08:00:00",
  49. products=[product],
  50. createTime="2026-08-27T00:00:00",
  51. updateTime="2026-08-27T00:00:00",
  52. )
  53. rows: list[dict] = []
  54. plan_common = {
  55. "planNo": "PL20260827001",
  56. "planDate": "2026-08-27",
  57. }
  58. row_no = _append_stop_product_rows(rows, 0, plan_common, "load", load, None, load.products)
  59. assert row_no == 1
  60. assert len(rows) == 1
  61. row = rows[0]
  62. assert row["stopKind"] == "装货"
  63. assert row["stopCustomer"] == internal
  64. assert row["loadCustomer"] == internal
  65. assert row["actualQty"] == Decimal("9.5")
  66. assert row["outboundQty"] == Decimal("9.5")
  67. assert row["loadOutboundQty"] == Decimal("9.5")
  68. assert row["inboundQty"] is None
  69. assert row["unitPrice"] == Decimal("100")
  70. assert row["loadUnitPrice"] == Decimal("100")
  71. assert row["amount"] == Decimal("950")
  72. assert row["loadAmount"] == Decimal("950")
  73. def test_append_stop_product_rows_delivery_external_outbound():
  74. product = _stop_product(
  75. id="sp2",
  76. outboundQty=Decimal("8"),
  77. outboundPrice=Decimal("120"),
  78. outboundAmount=Decimal("960"),
  79. )
  80. delivery = PlanDeliveryRead(
  81. id="del1",
  82. planId="p1",
  83. tenantNo="t1",
  84. seq=1,
  85. customerName="外部客户",
  86. address="送货地址",
  87. deliveryTime="2026-08-28T10:00:00",
  88. products=[product],
  89. createTime="2026-08-27T00:00:00",
  90. updateTime="2026-08-27T00:00:00",
  91. )
  92. rows: list[dict] = []
  93. row_no = _append_stop_product_rows(rows, 0, {"planNo": "P1"}, "delivery", delivery, 1, delivery.products)
  94. row = rows[0]
  95. assert row_no == 1
  96. assert row["stopKind"] == "送货 #1"
  97. assert row["deliveryCustomer"] == "外部客户"
  98. assert row["outboundQty"] == Decimal("8")
  99. assert row["deliveryOutboundQty"] == Decimal("8")
  100. assert row["inboundQty"] is None
  101. assert row["amount"] == Decimal("960")
  102. assert row["deliveryAmount"] == Decimal("960")
  103. def test_append_paired_product_rows_inner_join_one_row_per_product():
  104. internal = next(iter(INTERNAL_CUSTOMER_NAMES))
  105. load_a = _stop_product(
  106. id="lp1",
  107. productId="prod-a",
  108. productName="甲醇",
  109. packageSpec="散装",
  110. inboundQty=Decimal("10"),
  111. inboundPrice=Decimal("100"),
  112. )
  113. load_b = _stop_product(
  114. id="lp2",
  115. productId="prod-b",
  116. productName="乙醇",
  117. packageSpec="桶装",
  118. inboundQty=Decimal("5"),
  119. inboundPrice=Decimal("80"),
  120. )
  121. # 装货独有,送货无匹配 → 不出行
  122. load_only = _stop_product(
  123. id="lp3",
  124. productId="prod-c",
  125. productName="仅装货",
  126. packageSpec="散装",
  127. inboundQty=Decimal("1"),
  128. )
  129. delivery_a = _stop_product(
  130. id="dp1",
  131. productId="prod-a",
  132. productName="甲醇",
  133. packageSpec="散装",
  134. outboundQty=Decimal("9.5"),
  135. outboundPrice=Decimal("120"),
  136. outboundAmount=Decimal("1140"),
  137. )
  138. # 同产品不同包装 → 不匹配
  139. delivery_a_other_pkg = _stop_product(
  140. id="dp1b",
  141. productId="prod-a",
  142. productName="甲醇",
  143. packageSpec="桶装",
  144. outboundQty=Decimal("1"),
  145. outboundPrice=Decimal("1"),
  146. )
  147. delivery_b = _stop_product(
  148. id="dp2",
  149. productId="prod-b",
  150. productName="乙醇",
  151. packageSpec="桶装",
  152. outboundQty=Decimal("4"),
  153. outboundPrice=Decimal("90"),
  154. )
  155. # 送货独有 → 不出行
  156. delivery_only = _stop_product(
  157. id="dp3",
  158. productId="prod-d",
  159. productName="仅送货",
  160. packageSpec="散装",
  161. outboundQty=Decimal("2"),
  162. )
  163. load = PlanLoadRead(
  164. id="load1",
  165. planId="p1",
  166. tenantNo="t1",
  167. customerName=internal,
  168. address="装货地址",
  169. loadTime="2026-08-27T08:00:00",
  170. products=[load_a, load_b, load_only],
  171. createTime="2026-08-27T00:00:00",
  172. updateTime="2026-08-27T00:00:00",
  173. )
  174. delivery = PlanDeliveryRead(
  175. id="del1",
  176. planId="p1",
  177. tenantNo="t1",
  178. seq=1,
  179. customerName="外部客户",
  180. address="送货地址",
  181. deliveryTime="2026-08-28T10:00:00",
  182. products=[delivery_a, delivery_a_other_pkg, delivery_b, delivery_only],
  183. createTime="2026-08-27T00:00:00",
  184. updateTime="2026-08-27T00:00:00",
  185. )
  186. rows: list[dict] = []
  187. row_no = _append_paired_product_rows(rows, 0, {"planNo": "PL001"}, load, delivery, 1)
  188. assert row_no == 2
  189. assert len(rows) == 2
  190. by_name = {r["productName"]: r for r in rows}
  191. assert set(by_name) == {"甲醇", "乙醇"}
  192. methanol = by_name["甲醇"]
  193. assert methanol["loadCustomer"] == internal
  194. assert methanol["deliveryCustomer"] == "外部客户"
  195. assert methanol["deliveryStopKind"] == "送货 #1"
  196. assert methanol["packageSpec"] == "散装"
  197. assert methanol["loadOutboundQty"] == Decimal("10") # 内部客户装货 → 出库
  198. assert methanol["loadInboundQty"] is None
  199. assert methanol["loadUnitPrice"] == Decimal("100")
  200. assert methanol["loadAmount"] == Decimal("1000")
  201. assert methanol["deliveryOutboundQty"] == Decimal("9.5")
  202. assert methanol["deliveryInboundQty"] is None
  203. assert methanol["deliveryUnitPrice"] == Decimal("120")
  204. assert methanol["deliveryAmount"] == Decimal("1140")
  205. ethanol = by_name["乙醇"]
  206. assert ethanol["loadOutboundQty"] == Decimal("5")
  207. assert ethanol["deliveryOutboundQty"] == Decimal("4")
  208. assert ethanol["deliveryAmount"] == Decimal("360")
  209. def test_build_plan_archive_export_rows_pair_mode_and_single_side():
  210. internal = next(iter(INTERNAL_CUSTOMER_NAMES))
  211. load_product = _stop_product(
  212. inboundQty=Decimal("10"),
  213. inboundPrice=Decimal("100"),
  214. )
  215. delivery_product = _stop_product(
  216. id="dp1",
  217. outboundQty=Decimal("9"),
  218. outboundPrice=Decimal("110"),
  219. outboundAmount=Decimal("990"),
  220. )
  221. load = PlanLoadRead(
  222. id="load1",
  223. planId="p1",
  224. tenantNo="t1",
  225. customerName=internal,
  226. address="装货地址",
  227. loadTime="2026-08-27T08:00:00",
  228. products=[load_product],
  229. createTime="2026-08-27T00:00:00",
  230. updateTime="2026-08-27T00:00:00",
  231. )
  232. delivery = PlanDeliveryRead(
  233. id="del1",
  234. planId="p1",
  235. tenantNo="t1",
  236. seq=2,
  237. customerName="外部客户",
  238. address="送货地址",
  239. deliveryTime="2026-08-28T10:00:00",
  240. products=[delivery_product],
  241. createTime="2026-08-27T00:00:00",
  242. updateTime="2026-08-27T00:00:00",
  243. )
  244. plan = MagicMock()
  245. plan.id = "p1"
  246. plan.plan_no = "PL001"
  247. plan.contract_no = "C1"
  248. plan.plan_date = "2026-08-27"
  249. plan.archive_status = "archived"
  250. plan.follow_status = "arranged"
  251. plan.plan_type = "direct"
  252. plan.license_plate = "皖A12345"
  253. plan.trailer_no = None
  254. plan.erp_entered = 0
  255. plan.freight_amount = None
  256. plan.remark = ""
  257. plan.transport_id = None
  258. plan.customer_name = "外部客户"
  259. plan.supplier_name = None
  260. plan.destination_name = None
  261. plan.delivery_time = None
  262. with patch(
  263. "app.services.pl_plan_archive_export.load_plan_stops_grouped",
  264. return_value={"p1": (load, [delivery])},
  265. ), patch(
  266. "app.services.pl_plan_archive_export._load_legacy_products_by_plan",
  267. return_value={},
  268. ), patch(
  269. "app.services.pl_plan_archive_export._load_contract_nos_by_plan",
  270. return_value={"p1": []},
  271. ):
  272. paired = build_plan_archive_export_rows(
  273. MagicMock(), "t1", [plan], {}, stop_kind=None
  274. )
  275. load_only = build_plan_archive_export_rows(
  276. MagicMock(), "t1", [plan], {}, stop_kind="load"
  277. )
  278. delivery_only = build_plan_archive_export_rows(
  279. MagicMock(), "t1", [plan], {}, stop_kind="delivery"
  280. )
  281. assert len(paired) == 1
  282. assert paired[0]["productName"] == "甲醇"
  283. assert paired[0]["loadCustomer"] == internal
  284. assert paired[0]["deliveryCustomer"] == "外部客户"
  285. assert paired[0]["deliveryStopKind"] == "送货 #2"
  286. assert "stopKind" not in paired[0]
  287. assert len(load_only) == 1
  288. assert load_only[0]["stopKind"] == "装货"
  289. assert load_only[0]["loadCustomer"] == internal
  290. assert load_only[0]["deliveryCustomer"] == ""
  291. assert len(delivery_only) == 1
  292. assert delivery_only[0]["stopKind"] == "送货 #2"
  293. assert delivery_only[0]["deliveryCustomer"] == "外部客户"
  294. assert delivery_only[0]["loadCustomer"] == ""
  295. class _FakePlan:
  296. id = "p1"
  297. plan_no = "PL001"
  298. contract_no = "SX/X-JT20260827001"
  299. plan_date = "2026-08-27"
  300. archive_status = "archived"
  301. follow_status = "arranged"
  302. plan_type = "direct"
  303. license_plate = "皖A12345"
  304. trailer_no = "挂001"
  305. erp_entered = 1
  306. freight_amount = Decimal("500")
  307. remark = "测试"
  308. def test_plan_common_export_fields():
  309. fields = _plan_common_export_fields(_FakePlan(), None)
  310. assert fields["planId"] == "p1"
  311. assert fields["planNo"] == "PL001"
  312. assert fields["contractNo"] == "SX/X-JT20260827001"
  313. assert fields["archiveStatus"] == "已归档"
  314. assert fields["followStatus"] == "已安排"
  315. assert fields["planType"] == "直销单"
  316. assert fields["licensePlate"] == "皖A12345挂001"
  317. assert fields["erpEntered"] == "是"
  318. def test_plan_common_export_fields_merges_link_contract_nos():
  319. fields = _plan_common_export_fields(
  320. _FakePlan(),
  321. None,
  322. contract_nos=["SX/X-JT20260827001", "SX/X-JT20260827002"],
  323. )
  324. assert fields["contractNo"] == "SX/X-JT20260827001、SX/X-JT20260827002"