| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520 |
- from decimal import Decimal
- from unittest.mock import MagicMock, patch
- from app.schemas.pl.plan import PlanDeliveryRead, PlanLoadRead, PlanStopProductRead
- from app.services.pl_internal_customer import INTERNAL_CUSTOMER_NAMES
- from app.services.pl_plan_archive_export import (
- ARCHIVE_MERGED_SHEET_TITLE,
- _plan_common_export_fields,
- append_archive_list_merge_sheet,
- build_plan_archive_export_rows,
- prepare_archive_list_display_rows,
- resolve_archive_list_io_column,
- )
- from app.utils.excel_export import ExportColumn
- def _stop_product(**kwargs) -> PlanStopProductRead:
- defaults = {
- "id": "sp1",
- "planId": "p1",
- "tenantNo": "t1",
- "productId": "prod1",
- "productNo": "P001",
- "productName": "甲醇",
- "model": "99%",
- "unit": "吨",
- "packageSpec": "散装",
- "planQty": Decimal("10"),
- "createTime": "2026-08-27T00:00:00",
- "updateTime": "2026-08-27T00:00:00",
- }
- defaults.update(kwargs)
- return PlanStopProductRead(**defaults)
- def test_resolve_archive_list_io_column():
- internal = next(iter(INTERNAL_CUSTOMER_NAMES))
- assert resolve_archive_list_io_column("load", internal) == "outbound"
- assert resolve_archive_list_io_column("load", "外部客户") == "inbound"
- assert resolve_archive_list_io_column("delivery", internal) == "inbound"
- assert resolve_archive_list_io_column("delivery", "外部客户") == "outbound"
- # 配对宽表 helper(_append_paired_product_rows / _append_stop_product_rows)单测已删除:
- # 归档导出改为段次×产品多行,不再断言同排 loadPlanQty+deliveryPlanQty / deliveryStopKind。
- # 品牌别名与内部客户合同占位改由 build_plan_archive_export_rows 覆盖。
- def _bare_plan(**overrides):
- plan = MagicMock()
- plan.id = "p1"
- plan.plan_no = "PL001"
- plan.contract_no = "C1"
- plan.plan_date = "2026-08-27"
- plan.archive_status = "archived"
- plan.follow_status = "arranged"
- plan.plan_type = "direct"
- plan.license_plate = "皖A12345"
- plan.trailer_no = None
- plan.erp_entered = 0
- plan.freight_amount = None
- plan.remark = ""
- plan.transport_id = None
- plan.customer_name = "外部客户"
- plan.supplier_name = None
- plan.destination_name = None
- plan.delivery_time = None
- for key, value in overrides.items():
- setattr(plan, key, value)
- return plan
- def _archive_rows(
- plan,
- load,
- deliveries,
- *,
- stop_kind=None,
- legacy_products=None,
- alias_maps=None,
- brand_by_stop=None,
- brand_by_plan_product=None,
- ):
- if legacy_products is None:
- legacy_products = {}
- if alias_maps is None:
- alias_maps = ({}, {})
- if brand_by_stop is None:
- brand_by_stop = {}
- if brand_by_plan_product is None:
- brand_by_plan_product = {}
- with patch(
- "app.services.pl_plan_archive_export.load_plan_stops_grouped",
- return_value={plan.id: (load, deliveries)},
- ), patch(
- "app.services.pl_plan_archive_export._load_legacy_products_by_plan",
- return_value=legacy_products,
- ), patch(
- "app.services.pl_plan_archive_export._load_contract_nos_by_plan",
- return_value={plan.id: []},
- ), patch(
- "app.services.pl_plan_archive_export.load_crm_product_alias_maps",
- return_value=alias_maps,
- ), patch(
- "app.services.pl_plan_archive_export.load_brand_qtys_by_stop_product_ids",
- return_value=brand_by_stop,
- ), patch(
- "app.services.pl_plan_archive_export.load_brand_qtys_by_plan_product_ids",
- return_value=brand_by_plan_product,
- ):
- return build_plan_archive_export_rows(
- MagicMock(), "t1", [plan], {}, stop_kind=stop_kind
- )
- def test_build_plan_archive_export_rows_segment_rows_and_single_side():
- """默认按段次×产品展开:1 个装货产品 + 1 个送货产品 = 2 行;单侧筛选只留对应段。"""
- internal = next(iter(INTERNAL_CUSTOMER_NAMES))
- load_product = _stop_product(
- planQty=Decimal("10"),
- inboundQty=Decimal("10"),
- inboundPrice=Decimal("100"),
- )
- delivery_product = _stop_product(
- id="dp1",
- planQty=Decimal("12"),
- outboundQty=Decimal("9"),
- outboundPrice=Decimal("110"),
- outboundAmount=Decimal("990"),
- )
- load = PlanLoadRead(
- id="load1",
- planId="p1",
- tenantNo="t1",
- customerName=internal,
- address="装货地址",
- loadTime="2026-08-27T08:00:00",
- products=[load_product],
- createTime="2026-08-27T00:00:00",
- updateTime="2026-08-27T00:00:00",
- )
- delivery = PlanDeliveryRead(
- id="del1",
- planId="p1",
- tenantNo="t1",
- seq=2,
- customerName="外部客户",
- address="送货地址",
- deliveryTime="2026-08-28T10:00:00",
- products=[delivery_product],
- createTime="2026-08-27T00:00:00",
- updateTime="2026-08-27T00:00:00",
- )
- plan = _bare_plan()
- rows = _archive_rows(plan, load, [delivery], stop_kind=None)
- load_only = _archive_rows(plan, load, [delivery], stop_kind="load")
- delivery_only = _archive_rows(plan, load, [delivery], stop_kind="delivery")
- assert len(rows) == 2
- assert [row["stopSegment"] for row in rows] == ["装货", "送货 #2"]
- assert rows[0]["productName"] == "甲醇"
- assert rows[1]["productName"] == "甲醇"
- assert rows[0]["loadCustomer"] == internal
- assert rows[0]["deliveryCustomer"] == ""
- assert rows[0]["loadAddress"] == "装货地址"
- assert rows[0]["loadTime"] == "2026-08-27"
- assert rows[0]["deliveryAddress"] == ""
- assert rows[0]["deliveryTime"] == ""
- assert rows[0]["planQty"] == Decimal("10")
- assert rows[0]["actualQty"] == Decimal("10")
- assert rows[0]["unitPrice"] == Decimal("100")
- assert rows[0]["amount"] == Decimal("1000")
- assert rows[1]["loadCustomer"] == ""
- assert rows[1]["deliveryCustomer"] == "外部客户"
- assert rows[1]["loadAddress"] == ""
- assert rows[1]["loadTime"] == ""
- assert rows[1]["deliveryAddress"] == "送货地址"
- assert rows[1]["deliveryTime"] == "2026-08-28"
- assert rows[1]["planQty"] == Decimal("12")
- assert rows[1]["actualQty"] == Decimal("9")
- assert rows[1]["unitPrice"] == Decimal("110")
- assert rows[1]["amount"] == Decimal("990")
- assert rows[0].get("loadPlanQty") is None
- assert rows[0].get("deliveryPlanQty") is None
- assert rows[1].get("loadPlanQty") is None
- assert rows[1].get("deliveryPlanQty") is None
- assert not rows[0].get("deliveryStopKind")
- assert not rows[1].get("deliveryStopKind")
- assert len(load_only) == 1
- assert load_only[0]["stopSegment"] == "装货"
- assert load_only[0]["loadCustomer"] == internal
- assert load_only[0]["deliveryCustomer"] == ""
- assert load_only[0]["planQty"] == Decimal("10")
- assert load_only[0]["actualQty"] == Decimal("10")
- assert load_only[0]["unitPrice"] == Decimal("100")
- assert load_only[0]["amount"] == Decimal("1000")
- assert load_only[0].get("loadPlanQty") is None
- assert len(delivery_only) == 1
- assert delivery_only[0]["stopSegment"] == "送货 #2"
- assert delivery_only[0]["deliveryCustomer"] == "外部客户"
- assert delivery_only[0]["loadCustomer"] == ""
- assert delivery_only[0]["planQty"] == Decimal("12")
- assert delivery_only[0]["actualQty"] == Decimal("9")
- assert delivery_only[0]["unitPrice"] == Decimal("110")
- assert delivery_only[0]["amount"] == Decimal("990")
- assert delivery_only[0].get("deliveryPlanQty") is None
- class _FakePlan:
- id = "p1"
- plan_no = "PL001"
- contract_no = "SX/X-JT20260827001"
- plan_date = "2026-08-27"
- archive_status = "archived"
- follow_status = "arranged"
- plan_type = "direct"
- license_plate = "皖A12345"
- trailer_no = "挂001"
- erp_entered = 1
- freight_amount = Decimal("500")
- remark = "测试"
- def test_plan_common_export_fields():
- fields = _plan_common_export_fields(_FakePlan(), None)
- assert fields["planId"] == "p1"
- assert fields["planNo"] == "PL001"
- assert fields["contractNo"] == "SX/X-JT20260827001"
- assert fields["archiveStatus"] == "已归档"
- assert fields["followStatus"] == "已安排"
- assert fields["planType"] == "直销单"
- assert fields["licensePlate"] == "皖A12345挂001"
- assert fields["erpEntered"] == "是"
- def test_build_plan_archive_export_rows_includes_brand_and_alias():
- plan_product = MagicMock()
- plan_product.id = "pp1"
- plan_product.product_id = "prod1"
- plan_product.product_name = "甲醇别名"
- plan_product.brand_mode = "any"
- plan_product.brand_name = None
- load_product = _stop_product(id="lp1", planProductId="pp1", productName="甲醇别名")
- delivery_product = _stop_product(
- id="dp1",
- planProductId="pp1",
- productName="甲醇别名",
- outboundQty=Decimal("9"),
- outboundPrice=Decimal("110"),
- )
- load = PlanLoadRead(
- id="load1",
- planId="p1",
- tenantNo="t1",
- customerName="装货客户",
- products=[load_product],
- createTime="2026-08-27T00:00:00",
- updateTime="2026-08-27T00:00:00",
- )
- delivery = PlanDeliveryRead(
- id="del1",
- planId="p1",
- tenantNo="t1",
- seq=1,
- customerName="送货客户",
- products=[delivery_product],
- createTime="2026-08-27T00:00:00",
- updateTime="2026-08-27T00:00:00",
- )
- rows = _archive_rows(
- _bare_plan(),
- load,
- [delivery],
- legacy_products={"p1": [plan_product]},
- alias_maps=({"prod1": "甲醇"}, {"prod1": ["甲醇别名"]}),
- brand_by_stop={
- "lp1": [{"brandId": "b1", "brandName": "君正", "qty": Decimal("10")}],
- "dp1": [{"brandId": "b1", "brandName": "君正", "qty": Decimal("9")}],
- },
- )
- assert len(rows) == 2
- assert [row["stopSegment"] for row in rows] == ["装货", "送货 #1"]
- assert rows[0]["loadCustomer"] == "装货客户"
- assert rows[0]["deliveryCustomer"] == ""
- assert rows[1]["loadCustomer"] == ""
- assert rows[1]["deliveryCustomer"] == "送货客户"
- for row in rows:
- assert row["productName"] == "甲醇"
- assert row["brandName"] == "君正"
- assert row["aliasName"] == "甲醇别名"
- assert rows[0]["actualQty"] == Decimal("10")
- assert rows[1]["actualQty"] == Decimal("9")
- def test_build_plan_archive_export_rows_splits_multi_brand():
- plan_product = MagicMock()
- plan_product.id = "pp1"
- plan_product.product_id = "prod1"
- plan_product.product_name = "片碱"
- plan_product.brand_mode = "any"
- plan_product.brand_name = None
- load_product = _stop_product(
- id="lp1",
- planProductId="pp1",
- productName="片碱",
- planQty=Decimal("34"),
- inboundQty=Decimal("32"),
- inboundPrice=Decimal("2820"),
- )
- load = PlanLoadRead(
- id="load1",
- planId="p1",
- tenantNo="t1",
- customerName="装货客户",
- products=[load_product],
- createTime="2026-08-27T00:00:00",
- updateTime="2026-08-27T00:00:00",
- )
- rows = _archive_rows(
- _bare_plan(),
- load,
- [],
- legacy_products={"p1": [plan_product]},
- alias_maps=({"prod1": "片碱"}, {"prod1": []}),
- brand_by_stop={
- "lp1": [
- {
- "brandId": "b1",
- "brandName": "君正",
- "qty": Decimal("20"),
- "unitPrice": Decimal("2820"),
- },
- {
- "brandId": "b2",
- "brandName": "红四方",
- "qty": Decimal("12"),
- "unitPrice": Decimal("2820"),
- },
- ]
- },
- )
- load_rows = [r for r in rows if r.get("_stopKind") == "load"]
- assert len(load_rows) == 2
- assert [r["brandName"] for r in load_rows] == ["君正", "红四方"]
- assert [r["actualQty"] for r in load_rows] == [Decimal("20"), Decimal("12")]
- assert all(r["planQty"] == Decimal("34") for r in load_rows)
- assert load_rows[0]["amount"] == Decimal("20") * Decimal("2820")
- def test_plan_common_export_fields_merges_link_contract_nos():
- fields = _plan_common_export_fields(
- _FakePlan(),
- None,
- contract_nos=["SX/X-JT20260827001", "SX/X-JT20260827002"],
- )
- assert fields["contractNo"] == "SX/X-JT20260827001、SX/X-JT20260827002"
- def test_build_plan_archive_export_rows_internal_customer_contract_per_segment():
- """内部客户缺合同:装货行合同为出库,送货行为入库,不再拼在同一行。"""
- internal = next(iter(INTERNAL_CUSTOMER_NAMES))
- load_product = _stop_product(id="lp1", inboundQty=Decimal("10"))
- delivery_product = _stop_product(id="dp1", outboundQty=Decimal("10"))
- load = PlanLoadRead(
- id="load1",
- planId="p1",
- tenantNo="t1",
- customerName=internal,
- products=[load_product],
- contracts=[],
- createTime="2026-08-27T00:00:00",
- updateTime="2026-08-27T00:00:00",
- )
- delivery = PlanDeliveryRead(
- id="del1",
- planId="p1",
- tenantNo="t1",
- seq=1,
- customerName=internal,
- products=[delivery_product],
- contracts=[],
- createTime="2026-08-27T00:00:00",
- updateTime="2026-08-27T00:00:00",
- )
- rows = _archive_rows(_bare_plan(contract_no=""), load, [delivery])
- assert len(rows) == 2
- assert [row["stopSegment"] for row in rows] == ["装货", "送货 #1"]
- assert rows[0]["contractNo"] == "出库"
- assert rows[0]["loadCustomer"] == internal
- assert rows[0]["deliveryCustomer"] == ""
- assert rows[1]["contractNo"] == "入库"
- assert rows[1]["deliveryCustomer"] == internal
- assert rows[1]["loadCustomer"] == ""
- class _CellResolver:
- def row_to_cells(self, row, columns):
- return ["" if row.get(column.field) is None else row.get(column.field) for column in columns]
- _MERGE_COLUMNS = [
- ExportColumn(title="序号", field="rowNo"),
- ExportColumn(title="计划编号", field="planNo"),
- ExportColumn(title="装货客户", field="loadCustomer"),
- ExportColumn(title="送货客户", field="deliveryCustomer"),
- ExportColumn(title="合同编号", field="contractNo"),
- ExportColumn(title="产品名称", field="productName"),
- ExportColumn(title="计划量", field="planQty"),
- ]
- def _merged_ranges(sheet):
- return {str(item) for item in sheet.merged_cells.ranges}
- def test_prepare_single_delivery_puts_delivery_customer_on_first_row():
- internal = next(iter(INTERNAL_CUSTOMER_NAMES))
- load = PlanLoadRead(
- id="load1",
- planId="p1",
- tenantNo="t1",
- customerName="合肥玉坤",
- address="装货地址",
- loadTime="2026-10-09T00:00:00",
- products=[
- _stop_product(id="lp1", productName="片碱", planQty=Decimal("0.9")),
- _stop_product(id="lp2", productName="熟石灰", planQty=Decimal("1.75")),
- ],
- contracts=[],
- createTime="2026-10-09T00:00:00",
- updateTime="2026-10-09T00:00:00",
- )
- delivery = PlanDeliveryRead(
- id="del1",
- planId="p1",
- tenantNo="t1",
- seq=1,
- customerName="凤阳京环",
- address="送货地址",
- deliveryTime="2026-10-09T00:00:00",
- products=[
- _stop_product(id="dp1", productName="片碱", planQty=Decimal("0.9")),
- _stop_product(id="dp2", productName="熟石灰", planQty=Decimal("1.75")),
- ],
- contracts=[],
- createTime="2026-10-09T00:00:00",
- updateTime="2026-10-09T00:00:00",
- )
- rows = _archive_rows(_bare_plan(plan_type="direct", customer_name=internal), load, [delivery])
- display = prepare_archive_list_display_rows(rows)
- assert [row["rowNo"] for row in display] == [1, 1, 1, 1]
- assert display[0]["deliveryCustomer"] == "凤阳京环"
- assert display[0]["loadCustomer"] == "合肥玉坤"
- assert [row["productName"] for row in display] == ["片碱", "熟石灰", "片碱", "熟石灰"]
- assert display[0]["_contractMergeKey"] != display[2]["_contractMergeKey"]
- def test_merge_sheet_matches_list_rowspan():
- from openpyxl import Workbook
- internal = next(iter(INTERNAL_CUSTOMER_NAMES))
- load = PlanLoadRead(
- id="load1",
- planId="p1",
- tenantNo="t1",
- customerName=internal,
- products=[
- _stop_product(id="lp1", productName="氯化钠", planQty=Decimal("0.45")),
- _stop_product(id="lp2", productName="次氯酸钠", planQty=Decimal("1.17")),
- ],
- contracts=[],
- createTime="2026-10-07T00:00:00",
- updateTime="2026-10-07T00:00:00",
- )
- delivery_a = PlanDeliveryRead(
- id="del1",
- planId="p1",
- tenantNo="t1",
- seq=1,
- customerName="安庆奇创",
- deliveryTime="2026-10-07T00:00:00",
- products=[_stop_product(id="dp1", productName="次氯酸钠", planQty=Decimal("1.17"))],
- contracts=[],
- createTime="2026-10-07T00:00:00",
- updateTime="2026-10-07T00:00:00",
- )
- delivery_b = PlanDeliveryRead(
- id="del2",
- planId="p1",
- tenantNo="t1",
- seq=2,
- customerName="安庆奇创",
- deliveryTime="2026-10-07T00:00:00",
- products=[_stop_product(id="dp2", productName="氯化钠", planQty=Decimal("0.45"))],
- contracts=[],
- createTime="2026-10-07T00:00:00",
- updateTime="2026-10-07T00:00:00",
- )
- rows = _archive_rows(_bare_plan(plan_type="outbound"), load, [delivery_a, delivery_b])
- workbook = Workbook()
- workbook.active.title = "计划归档"
- append_archive_list_merge_sheet(workbook, _MERGE_COLUMNS, rows, _CellResolver())
- sheet = workbook[ARCHIVE_MERGED_SHEET_TITLE]
- assert [cell.value for cell in sheet[1]] == [column.title for column in _MERGE_COLUMNS]
- assert sheet["F2"].value == "氯化钠"
- assert sheet["F5"].value == "氯化钠"
- ranges = _merged_ranges(sheet)
- assert "A2:A5" in ranges
- assert "B2:B5" in ranges
- assert "C2:C5" in ranges
- assert "D2:D3" in ranges
- assert "D4:D5" not in ranges
- assert sheet["D2"].value == "—"
- assert sheet["D4"].value == "安庆奇创"
- assert sheet["D5"].value == "安庆奇创"
- assert "F2:F5" not in ranges
|