|
|
@@ -1,14 +1,17 @@
|
|
|
"""合同产品运输安排数量汇总单元测试。"""
|
|
|
from types import SimpleNamespace
|
|
|
+from collections import defaultdict
|
|
|
|
|
|
from app.services.pl_contract_plan_qty import (
|
|
|
_bucket_for_plan,
|
|
|
accumulate_stop_products,
|
|
|
+ attach_plan_qty_fields_to_products,
|
|
|
merge_product_qty_maps,
|
|
|
+ normalize_package_spec,
|
|
|
+ product_package_key,
|
|
|
resolve_linked_plan_scopes,
|
|
|
sum_qtys_for_scope,
|
|
|
)
|
|
|
-from collections import defaultdict
|
|
|
|
|
|
|
|
|
def test_bucket_archived():
|
|
|
@@ -52,16 +55,72 @@ def test_bucket_skips_pending_and_cancelled():
|
|
|
)
|
|
|
|
|
|
|
|
|
+def test_normalize_package_spec():
|
|
|
+ from app.services.pl_contract_plan_qty import normalize_package_spec
|
|
|
+
|
|
|
+ assert normalize_package_spec(None) == ""
|
|
|
+ assert normalize_package_spec(" ") == ""
|
|
|
+ assert normalize_package_spec(" 槽罐车 ") == "槽罐车"
|
|
|
+ assert normalize_package_spec("25kg/桶") != normalize_package_spec("25Kg/袋")
|
|
|
+
|
|
|
+
|
|
|
+def test_accumulate_splits_same_product_different_package():
|
|
|
+ acc = defaultdict(float)
|
|
|
+ accumulate_stop_products(
|
|
|
+ acc,
|
|
|
+ [
|
|
|
+ SimpleNamespace(product_id="SXWL007344", package_spec="25kg/桶", plan_qty=15),
|
|
|
+ SimpleNamespace(product_id="SXWL007344", package_spec="槽罐车", plan_qty=700),
|
|
|
+ SimpleNamespace(product_id="SXWL007344", package_spec=" 槽罐车 ", plan_qty=10),
|
|
|
+ ],
|
|
|
+ )
|
|
|
+ k_bucket = ("SXWL007344", "25kg/桶")
|
|
|
+ k_tank = ("SXWL007344", "槽罐车")
|
|
|
+ assert acc[k_bucket] == 15.0
|
|
|
+ assert acc[k_tank] == 710.0
|
|
|
+
|
|
|
+
|
|
|
+def test_merge_product_qty_maps_composite_keys():
|
|
|
+ out = merge_product_qty_maps(
|
|
|
+ archived={("p1", "A"): 10.0},
|
|
|
+ arranged_unarchived={("p1", "B"): 20.0, ("p1", "A"): 5.0},
|
|
|
+ )
|
|
|
+ assert out[("p1", "A")]["arrangedTotalQty"] == 15.0
|
|
|
+ assert out[("p1", "B")]["arrangedTotalQty"] == 20.0
|
|
|
+
|
|
|
+
|
|
|
+def test_attach_plan_qty_fields_matches_package():
|
|
|
+ qty_map = {
|
|
|
+ ("pid1", "25kg/桶"): {
|
|
|
+ "archivedQty": 1.0,
|
|
|
+ "arrangedUnarchivedQty": 2.0,
|
|
|
+ "arrangedTotalQty": 3.0,
|
|
|
+ },
|
|
|
+ ("pid1", "槽罐车"): {
|
|
|
+ "archivedQty": 10.0,
|
|
|
+ "arrangedUnarchivedQty": 20.0,
|
|
|
+ "arrangedTotalQty": 30.0,
|
|
|
+ },
|
|
|
+ }
|
|
|
+ products = [
|
|
|
+ {"productId": "pid1", "packageSpec": "25kg/桶"},
|
|
|
+ {"productId": "pid1", "packageSpec": "槽罐车"},
|
|
|
+ ]
|
|
|
+ attach_plan_qty_fields_to_products(products, qty_map)
|
|
|
+ assert products[0]["arrangedTotalQty"] == 3.0
|
|
|
+ assert products[1]["arrangedTotalQty"] == 30.0
|
|
|
+
|
|
|
+
|
|
|
def test_merge_product_qty_maps_totals():
|
|
|
out = merge_product_qty_maps(
|
|
|
- archived={"p1": 120.0, "p2": 50.0},
|
|
|
- arranged_unarchived={"p1": 80.0, "p3": 100.0},
|
|
|
+ archived={("p1", ""): 120.0, ("p2", ""): 50.0},
|
|
|
+ arranged_unarchived={("p1", ""): 80.0, ("p3", ""): 100.0},
|
|
|
)
|
|
|
- assert out["p1"]["archivedQty"] == 120.0
|
|
|
- assert out["p1"]["arrangedUnarchivedQty"] == 80.0
|
|
|
- assert out["p1"]["arrangedTotalQty"] == 200.0
|
|
|
- assert out["p2"]["arrangedTotalQty"] == 50.0
|
|
|
- assert out["p3"]["arrangedTotalQty"] == 100.0
|
|
|
+ assert out[("p1", "")]["archivedQty"] == 120.0
|
|
|
+ assert out[("p1", "")]["arrangedUnarchivedQty"] == 80.0
|
|
|
+ assert out[("p1", "")]["arrangedTotalQty"] == 200.0
|
|
|
+ assert out[("p2", "")]["arrangedTotalQty"] == 50.0
|
|
|
+ assert out[("p3", "")]["arrangedTotalQty"] == 100.0
|
|
|
|
|
|
|
|
|
def test_sum_qtys_explicit_delivery_stop_only():
|
|
|
@@ -79,7 +138,7 @@ def test_sum_qtys_explicit_delivery_stop_only():
|
|
|
all_delivery_ids={"D1", "D2"},
|
|
|
scope={"load_ids": set(), "delivery_ids": {"D1"}, "use_all_stops": False},
|
|
|
)
|
|
|
- assert qtys == {"p1": 40.0}
|
|
|
+ assert qtys == {("p1", ""): 40.0}
|
|
|
|
|
|
|
|
|
def test_legacy_use_all_stops_prefers_delivery():
|
|
|
@@ -96,7 +155,7 @@ def test_legacy_use_all_stops_prefers_delivery():
|
|
|
all_delivery_ids={"D1"},
|
|
|
scope={"load_ids": set(), "delivery_ids": set(), "use_all_stops": True},
|
|
|
)
|
|
|
- assert qtys == {"p1": 40.0}
|
|
|
+ assert qtys == {("p1", ""): 40.0}
|
|
|
|
|
|
|
|
|
def test_resolve_scopes_marks_legacy():
|
|
|
@@ -119,7 +178,7 @@ def test_accumulate_stop_products():
|
|
|
SimpleNamespace(product_id="", plan_qty=9),
|
|
|
],
|
|
|
)
|
|
|
- assert acc["p1"] == 20.0
|
|
|
+ assert acc[("p1", "")] == 20.0
|
|
|
|
|
|
|
|
|
def test_accumulate_archive_actual_load_uses_inbound_qty():
|
|
|
@@ -134,7 +193,7 @@ def test_accumulate_archive_actual_load_uses_inbound_qty():
|
|
|
qty_mode="archive_actual",
|
|
|
)
|
|
|
# 88 + 回退 plan_qty 50
|
|
|
- assert acc["p1"] == 138.0
|
|
|
+ assert acc[("p1", "")] == 138.0
|
|
|
|
|
|
|
|
|
def test_accumulate_archive_actual_delivery_uses_outbound_qty():
|
|
|
@@ -145,7 +204,7 @@ def test_accumulate_archive_actual_delivery_uses_outbound_qty():
|
|
|
stop_kind="delivery",
|
|
|
qty_mode="archive_actual",
|
|
|
)
|
|
|
- assert acc["p1"] == 66.0
|
|
|
+ assert acc[("p1", "")] == 66.0
|
|
|
|
|
|
|
|
|
def test_accumulate_archive_actual_zero_does_not_fallback():
|
|
|
@@ -156,7 +215,7 @@ def test_accumulate_archive_actual_zero_does_not_fallback():
|
|
|
stop_kind="load",
|
|
|
qty_mode="archive_actual",
|
|
|
)
|
|
|
- assert acc["p1"] == 0.0
|
|
|
+ assert acc[("p1", "")] == 0.0
|
|
|
|
|
|
|
|
|
def test_sum_qtys_archive_actual_mode_on_scoped_stops():
|
|
|
@@ -174,7 +233,7 @@ def test_sum_qtys_archive_actual_mode_on_scoped_stops():
|
|
|
scope={"load_ids": {"L1"}, "delivery_ids": {"D1"}, "use_all_stops": False},
|
|
|
qty_mode="archive_actual",
|
|
|
)
|
|
|
- assert qtys == {"p1": 40.0} # 7 + 33
|
|
|
+ assert qtys == {("p1", ""): 40.0} # 7 + 33
|
|
|
|
|
|
|
|
|
def test_sum_qtys_default_mode_ignores_actual_fields():
|
|
|
@@ -191,7 +250,7 @@ def test_sum_qtys_default_mode_ignores_actual_fields():
|
|
|
all_delivery_ids={"D1"},
|
|
|
scope={"load_ids": set(), "delivery_ids": {"D1"}, "use_all_stops": False},
|
|
|
)
|
|
|
- assert qtys == {"p1": 40.0}
|
|
|
+ assert qtys == {("p1", ""): 40.0}
|
|
|
|
|
|
|
|
|
class DecimalLike:
|