from decimal import Decimal import pytest from app.services.pl_brand_archive import ( expand_export_rows_by_brand_qty, expand_export_rows_for_plan_brand, format_plan_product_constraint_brand, is_archived_for_export_brand, resolve_export_brand_qty_rows, validate_brand_qty_rows, ) from types import SimpleNamespace def test_resolve_export_brand_qty_rows_prefers_stop_level(): by_stop = { "sp1": [{"brandId": "b1", "brandName": "君正", "qty": Decimal("32")}], } by_plan = { "pp1": [{"brandId": "b2", "brandName": "红四方", "qty": Decimal("10")}], } rows = resolve_export_brand_qty_rows( stop_product_id="sp1", plan_product_id="pp1", by_stop_id=by_stop, by_plan_product_id=by_plan, ) assert len(rows) == 1 assert rows[0]["brandName"] == "君正" def test_resolve_export_brand_qty_rows_falls_back_to_plan_level(): rows = resolve_export_brand_qty_rows( stop_product_id="missing", plan_product_id="pp1", by_stop_id={}, by_plan_product_id={ "pp1": [{"brandId": "b1", "brandName": "君正", "qty": Decimal("32")}], }, ) assert len(rows) == 1 assert rows[0]["brandName"] == "君正" def test_resolve_export_brand_qty_rows_empty(): assert ( resolve_export_brand_qty_rows( stop_product_id=None, plan_product_id=None, by_stop_id={}, by_plan_product_id={}, ) == [] ) def test_expand_export_rows_by_brand_qty_splits_and_blanks(): base = {"productName": "片碱", "planQty": 34, "actualQty": 32, "brandName": "旧"} blank = expand_export_rows_by_brand_qty(base, []) assert len(blank) == 1 assert blank[0]["brandName"] == "" assert blank[0]["actualQty"] == 32 split = expand_export_rows_by_brand_qty( base, [ {"brandId": "b1", "brandName": "君正", "qty": Decimal("20")}, {"brandId": "b2", "brandName": "红四方", "qty": Decimal("12")}, ], ) assert [r["brandName"] for r in split] == ["君正", "红四方"] assert [r["actualQty"] for r in split] == [Decimal("20"), Decimal("12")] assert all(r["planQty"] == 34 for r in split) def test_archive_status_gate_for_export_brand(): assert is_archived_for_export_brand("archived") is True assert is_archived_for_export_brand("unarchived") is False assert is_archived_for_export_brand("draft") is False product = SimpleNamespace(brand_mode="required", brand_name="君正") assert format_plan_product_constraint_brand(product) == "君正" assert ( format_plan_product_constraint_brand( SimpleNamespace(brand_mode="any", brand_name="君正") ) == "" ) def test_expand_export_rows_for_plan_brand_draft_uses_product_not_qty(): base = {"productName": "片碱", "planQty": 34, "actualQty": 32, "brandName": ""} product = SimpleNamespace(id="pp1", brand_mode="required", brand_name="君正") rows = expand_export_rows_for_plan_brand( base, archive_status="draft", plan_product=product, stop_product_id="lp1", by_stop_id={ "lp1": [ {"brandId": "b1", "brandName": "红四方", "qty": Decimal("20")}, {"brandId": "b2", "brandName": "君正", "qty": Decimal("12")}, ] }, ) assert len(rows) == 1 assert rows[0]["brandName"] == "君正" assert rows[0]["actualQty"] == 32 def test_expand_export_rows_for_plan_brand_archived_splits_qty(): base = {"productName": "片碱", "planQty": 34, "actualQty": 32, "brandName": ""} product = SimpleNamespace(id="pp1", brand_mode="any", brand_name=None) rows = expand_export_rows_for_plan_brand( base, archive_status="archived", plan_product=product, stop_product_id="lp1", by_stop_id={ "lp1": [ {"brandId": "b1", "brandName": "君正", "qty": Decimal("20")}, {"brandId": "b2", "brandName": "红四方", "qty": Decimal("12")}, ] }, ) assert [r["brandName"] for r in rows] == ["君正", "红四方"] assert [r["actualQty"] for r in rows] == [Decimal("20"), Decimal("12")] def test_required_allows_other_brand_when_sum_matches(): validate_brand_qty_rows( brand_mode="required", required_brand_id="b1", actual_qty=Decimal("10"), rows=[{"brandId": "b2", "brandName": "红四方", "qty": Decimal("10")}], ) def test_required_allows_multi_brand_when_sum_matches(): validate_brand_qty_rows( brand_mode="required", required_brand_id="b1", actual_qty=Decimal("10"), rows=[ {"brandId": "b1", "brandName": "君正", "qty": Decimal("6")}, {"brandId": "b2", "brandName": "红四方", "qty": Decimal("4")}, ], ) def test_required_sum_must_match(): with pytest.raises(ValueError, match="合计"): validate_brand_qty_rows( brand_mode="required", required_brand_id="b1", actual_qty=Decimal("10"), rows=[ {"brandId": "b1", "brandName": "君正", "qty": Decimal("6")}, {"brandId": "b2", "brandName": "红四方", "qty": Decimal("3")}, ], ) def test_any_sum_must_match(): with pytest.raises(ValueError, match="合计"): validate_brand_qty_rows( brand_mode="any", required_brand_id=None, actual_qty=Decimal("10"), rows=[ {"brandId": "b1", "brandName": "君正", "qty": Decimal("6")}, {"brandId": "b2", "brandName": "红四方", "qty": Decimal("3")}, ], ) def test_any_ok(): validate_brand_qty_rows( brand_mode="any", required_brand_id=None, actual_qty=Decimal("10"), rows=[ {"brandId": "b1", "brandName": "君正", "qty": Decimal("6")}, {"brandId": "b2", "brandName": "红四方", "qty": Decimal("4")}, ], ) def test_no_brand_ignores_rows(): validate_brand_qty_rows( brand_mode=None, required_brand_id=None, actual_qty=Decimal("5"), rows=[], ) def test_delta_for_plan_type(): from app.services.pl_inventory_service import qty_delta_for_plan assert qty_delta_for_plan("inbound", Decimal("3")) == Decimal("3") assert qty_delta_for_plan("outbound", Decimal("3")) == Decimal("-3") assert qty_delta_for_plan("direct", Decimal("3")) == Decimal("0")