test_pl_plan_archive_export.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  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. ), patch(
  272. "app.services.pl_plan_archive_export.load_crm_product_alias_maps",
  273. return_value=({}, {}),
  274. ):
  275. paired = build_plan_archive_export_rows(
  276. MagicMock(), "t1", [plan], {}, stop_kind=None
  277. )
  278. load_only = build_plan_archive_export_rows(
  279. MagicMock(), "t1", [plan], {}, stop_kind="load"
  280. )
  281. delivery_only = build_plan_archive_export_rows(
  282. MagicMock(), "t1", [plan], {}, stop_kind="delivery"
  283. )
  284. assert len(paired) == 1
  285. assert paired[0]["productName"] == "甲醇"
  286. assert paired[0]["loadCustomer"] == internal
  287. assert paired[0]["deliveryCustomer"] == "外部客户"
  288. assert paired[0]["deliveryStopKind"] == "送货 #2"
  289. assert "stopKind" not in paired[0]
  290. assert len(load_only) == 1
  291. assert load_only[0]["stopKind"] == "装货"
  292. assert load_only[0]["loadCustomer"] == internal
  293. assert load_only[0]["deliveryCustomer"] == ""
  294. assert len(delivery_only) == 1
  295. assert delivery_only[0]["stopKind"] == "送货 #2"
  296. assert delivery_only[0]["deliveryCustomer"] == "外部客户"
  297. assert delivery_only[0]["loadCustomer"] == ""
  298. class _FakePlan:
  299. id = "p1"
  300. plan_no = "PL001"
  301. contract_no = "SX/X-JT20260827001"
  302. plan_date = "2026-08-27"
  303. archive_status = "archived"
  304. follow_status = "arranged"
  305. plan_type = "direct"
  306. license_plate = "皖A12345"
  307. trailer_no = "挂001"
  308. erp_entered = 1
  309. freight_amount = Decimal("500")
  310. remark = "测试"
  311. def test_plan_common_export_fields():
  312. fields = _plan_common_export_fields(_FakePlan(), None)
  313. assert fields["planId"] == "p1"
  314. assert fields["planNo"] == "PL001"
  315. assert fields["contractNo"] == "SX/X-JT20260827001"
  316. assert fields["archiveStatus"] == "已归档"
  317. assert fields["followStatus"] == "已安排"
  318. assert fields["planType"] == "直销单"
  319. assert fields["licensePlate"] == "皖A12345挂001"
  320. assert fields["erpEntered"] == "是"
  321. def test_append_paired_product_rows_includes_brand_and_alias():
  322. plan_product = MagicMock()
  323. plan_product.id = "pp1"
  324. plan_product.product_id = "prod1"
  325. plan_product.product_name = "甲醇别名"
  326. plan_product.brand_mode = "required"
  327. plan_product.brand_name = "君正"
  328. load_product = _stop_product(planProductId="pp1", productName="甲醇别名")
  329. delivery_product = _stop_product(
  330. id="dp1",
  331. planProductId="pp1",
  332. productName="甲醇别名",
  333. outboundQty=Decimal("9"),
  334. outboundPrice=Decimal("110"),
  335. )
  336. load = PlanLoadRead(
  337. id="load1",
  338. planId="p1",
  339. tenantNo="t1",
  340. customerName="装货客户",
  341. products=[load_product],
  342. createTime="2026-08-27T00:00:00",
  343. updateTime="2026-08-27T00:00:00",
  344. )
  345. delivery = PlanDeliveryRead(
  346. id="del1",
  347. planId="p1",
  348. tenantNo="t1",
  349. seq=1,
  350. customerName="送货客户",
  351. products=[delivery_product],
  352. createTime="2026-08-27T00:00:00",
  353. updateTime="2026-08-27T00:00:00",
  354. )
  355. rows: list[dict] = []
  356. _append_paired_product_rows(
  357. rows,
  358. 0,
  359. {"planNo": "PL001"},
  360. load,
  361. delivery,
  362. 1,
  363. plan_products_by_id={"pp1": plan_product},
  364. plan_products_by_product_id={"prod1": plan_product},
  365. standard_by_id={"prod1": "甲醇"},
  366. alias_options_by_id={"prod1": ["甲醇别名"]},
  367. )
  368. assert len(rows) == 1
  369. assert rows[0]["productName"] == "甲醇"
  370. assert rows[0]["brandName"] == "君正"
  371. assert rows[0]["aliasName"] == "甲醇别名"
  372. def test_plan_common_export_fields_merges_link_contract_nos():
  373. fields = _plan_common_export_fields(
  374. _FakePlan(),
  375. None,
  376. contract_nos=["SX/X-JT20260827001", "SX/X-JT20260827002"],
  377. )
  378. assert fields["contractNo"] == "SX/X-JT20260827001、SX/X-JT20260827002"
  379. def test_append_paired_rows_internal_customer_contract_placeholder():
  380. from app.services.pl_plan_archive_export import format_row_contract_export_label
  381. internal = next(iter(INTERNAL_CUSTOMER_NAMES))
  382. load_product = _stop_product(id="lp1", inboundQty=Decimal("10"))
  383. delivery_product = _stop_product(id="dp1", outboundQty=Decimal("10"))
  384. load = PlanLoadRead(
  385. id="load1",
  386. planId="p1",
  387. tenantNo="t1",
  388. customerName=internal,
  389. products=[load_product],
  390. contracts=[],
  391. createTime="2026-08-27T00:00:00",
  392. updateTime="2026-08-27T00:00:00",
  393. )
  394. delivery = PlanDeliveryRead(
  395. id="del1",
  396. planId="p1",
  397. tenantNo="t1",
  398. seq=1,
  399. customerName=internal,
  400. products=[delivery_product],
  401. contracts=[],
  402. createTime="2026-08-27T00:00:00",
  403. updateTime="2026-08-27T00:00:00",
  404. )
  405. assert format_row_contract_export_label(load=load, delivery=delivery) == "出库、入库"
  406. rows: list[dict] = []
  407. _append_paired_product_rows(
  408. rows,
  409. 0,
  410. {"planNo": "PL001", "contractNo": ""},
  411. load,
  412. delivery,
  413. 1,
  414. )
  415. assert rows[0]["contractNo"] == "出库、入库"
  416. load_only_rows: list[dict] = []
  417. _append_stop_product_rows(
  418. load_only_rows,
  419. 0,
  420. {"planNo": "PL001", "contractNo": ""},
  421. "load",
  422. load,
  423. None,
  424. load.products,
  425. )
  426. assert load_only_rows[0]["contractNo"] == "出库"
  427. delivery_only_rows: list[dict] = []
  428. _append_stop_product_rows(
  429. delivery_only_rows,
  430. 0,
  431. {"planNo": "PL001", "contractNo": ""},
  432. "delivery",
  433. delivery,
  434. 1,
  435. delivery.products,
  436. )
  437. assert delivery_only_rows[0]["contractNo"] == "入库"