"""预演只分配资金,覆盖先旧后新、预收与缺条件的核算边界。""" from datetime import date from decimal import Decimal import pytest from app.services.tplus.fifo import money, preview_group def inputs(): """明确口径的简单业务,测试不依赖真实客户数据。""" group = { "key": "g", "scope_confirmed": True, "opening_confirmed": True, "opening_balance": "0", "historical_settlement_confirmed": True, } ship = { "source_id": "1", "line_id": "1", "date": "2026-03-10", "group_key": "g", "amount": "100", "cost_total": "50", "category": "municipal_water", "personnel_id": "001", "customer_type": "personal", "business_key": "business1", } cash = { "source_id": "2", "date": "2026-03-10", "group_key": "g", "amount": "100", "voucher_confirmed": True, } facts = {"personnel": {"001": {"role_code": "salesperson"}}} params = { "2026-03-10": { "thresholds": {"municipal_water": "30"}, "start": "10", "slope": "4/360", "floor": "4", "boundary": 360, "enterprise": "3", } } return group, ship, cash, facts, params def run(group, ships, cash, facts, params, until=date(2026, 3, 31)): return preview_group(group, ships, cash, facts, params, until) def test_跨月预演区分累计与当月计提(): g, s, c, f, p = inputs() result = run(g, [s], [c], f, p, date(2026, 4, 30)) assert Decimal(result["performance_total"]) > 0 assert result["month_performance_total"] == "0.00" assert result["lines"][0]["shipment_remaining_after"] == "0.00" def test_经理本人业务交接后仍排除(): g, s, c, f, p = inputs() f["personnel"]["001"]["role_code"] = "sales_manager" f["personnel"]["002"] = {"role_code": "salesperson"} f["handovers"] = [ { "business_key": "business1", "mode": "RESIGNATION", "effective_date": "2026-03-15", "from_personnel_id": "001", "to_personnel_id": "002", } ] line = run(g, [s], [c], f, p)["lines"][0] assert line["excluded_manager_own"] and line["performance"] == "0.00" def test_未知运费影响同组顺序时不猜零费用(): g, s, c, f, p = inputs() unknown = {**s, "pending": ["FREIGHT_PENDING"]} result = run(g, [unknown, {**s, "source_id": "2"}], [c], f, p) assert result["status"] == "PENDING" assert result["lines"] == [] and result["reasons"] == ["FREIGHT_PENDING"] def test_范围外的未来负向单据不阻塞本月(): g, s, c, f, p = inputs() future = {**s, "source_id": "99", "date": "2026-05-01", "amount": "-100"} result = run(g, [s, future], [c], f, p) assert result["status"] == "SUCCEEDED" assert result["receipt_total"] == "100.00" def test_一款多发货金额守恒且同日按源id排序(): g, s, c, f, p = inputs() result = run( g, [ {**s, "source_id": "9", "amount": "60", "cost_total": "30"}, {**s, "source_id": "3", "amount": "60", "cost_total": "30"}, ], [c], f, p, ) assert [r["shipment_id"] for r in result["lines"]] == ["3", "9"] assert result["receipt_total"] == result["allocated_total"] == "100.00" assert result["shipment_remaining"] == "20.00" def test_多款一发货和成本分摊尾差(): g, s, c, f, p = inputs() s = {**s, "amount": "3", "cost_total": "1"} cash = [{**c, "source_id": str(i), "amount": "1"} for i in range(3)] result = run(g, [s], cash, f, p) assert sum(Decimal(r["allocated_cost"]) for r in result["lines"]) == Decimal("1") assert result["shipment_remaining"] == "0.00" def test_旧应收优先冲平且不算新绩效(): g, s, c, f, p = inputs() g["opening_balance"] = "70" result = run(g, [s], [c], f, p) assert result["lines"][0]["kind"] == "OPENING" assert result["lines"][0]["performance"] is None assert result["lines"][1]["amount"] == "30.00" assert result["opening_remaining"] == "0.00" def test_明确未生成凭证的款排除且不阻塞合格回款(): g, s, c, f, p = inputs() excluded = { **c, "source_id": "99", "amount": "45", "voucher_confirmed": False, "exclusion": "NOT_GENERATED_VOUCHER", } result = run(g, [s], [excluded, c], f, p) assert result["status"] == "SUCCEEDED" assert result["receipt_total"] == result["allocated_total"] == "100.00" assert result["excluded_receipt_total"] == "45.00" line = next(row for row in result["lines"] if row["kind"] == "EXCLUDED") assert line["performance"] is None and line["reasons"] == ["NOT_GENERATED_VOUCHER"] def test_离职人员缺交接依据不猜绩效归属(): g, s, c, f, p = inputs() f["personnel"]["001"]["status"] = 0 line = run(g, [s], [c], f, p)["lines"][0] assert line["status"] == "PENDING" and line["performance"] is None assert "HANDOVER_PENDING" in line["reasons"] def test_预收参数取值时点不明确时不猜测(): g, s, c, f, p = inputs() c["date"] = "2026-03-01" line = run(g, [s], [c], f, p)["lines"][0] assert line["status"] == "PENDING" assert "PREPAYMENT_PARAMETERS_PENDING" in line["reasons"] def test_预收未发货不计提_发货后最高比例(): g, s, c, f, p = inputs() c["date"] = "2026-03-01" p["prepayment_parameter_basis"] = "SHIPMENT" before = run(g, [s], [c], f, p, date(2026, 3, 5)) assert before["prepayment_remaining"] == "100.00" after = run(g, [s], [c], f, p) assert after["lines"][0]["rate"] == "1/100" assert after["lines"][0]["effective_date"] == "2026-03-10" @pytest.mark.parametrize( "missing,reason", [("cost_total", "GUIDANCE_PENDING"), ("category", "PRODUCT_CATEGORY_PENDING")] ) def test_缺必要字段不按零计算(missing, reason): g, s, c, f, p = inputs() s.pop(missing) result = run(g, [s], [c], f, p) assert result["lines"][0]["performance"] is None assert reason in result["lines"][0]["reasons"] def test_凭证状态未确认阻止本组分配(): g, s, c, f, p = inputs() c["voucher_confirmed"] = False assert run(g, [s], [c], f, p)["reasons"] == ["VOUCHER_STATE_PENDING"] def test_经理本人业务排除(): g, s, c, f, p = inputs() f["personnel"]["001"]["role_code"] = "sales_manager" assert run(g, [s], [c], f, p)["lines"][0]["performance"] == "0.00" def test_负单据不进入普通fifo(): g, s, c, f, p = inputs() c["amount"] = "-1" assert run(g, [s], [c], f, p)["reasons"] == ["NEGATIVE_OR_ZERO_DOCUMENT"] def test_已核对更正发货保留源记录但排除绩效(): g, s, c, f, p = inputs() correction = { **s, "source_id": "99", "amount": "-100", "performance_excluded": True, "exclusion_reason": "CORRECTION_EXCLUDED", "correction_pair_key": "pair-1", } result = run(g, [correction, s], [c], f, p) excluded = next(row for row in result["lines"] if row["kind"] == "EXCLUDED") assert result["status"] == "SUCCEEDED" assert result["excluded_shipment_total"] == "-100.00" assert excluded["reasons"] == ["CORRECTION_EXCLUDED"] assert result["performance_total"] != "0.00" def test_禁止跨组及重复输入(): g, s, c, f, p = inputs() with pytest.raises(ValueError, match="跨"): run(g, [s], [{**c, "group_key": "other"}], f, p) with pytest.raises(ValueError, match="重复"): run(g, [s, s], [c], f, p) def test_离职交接转给新人员_在职交接保留发货人(): g, s, c, f, p = inputs() f["personnel"]["002"] = {"role_code": "salesperson"} handover = { "business_key": "business1", "mode": "RESIGNATION", "effective_date": "2026-03-05", "from_personnel_id": "001", "to_personnel_id": "002", } f["handovers"] = [handover] assert run(g, [s], [c], f, p)["lines"][0]["personnel_id"] == "002" handover["mode"] = "ACTIVE" handover["shipping_ownership_confirmed"] = True assert run(g, [s], [c], f, p)["lines"][0]["personnel_id"] == "001" def test_在职交接未确认发货归属时不生成绩效(): g, s, c, f, p = inputs() handover = {"business_key": "business1", "mode": "ACTIVE", "effective_date": "2026-03-05", "from_personnel_id": "001", "to_personnel_id": "002", "shipping_ownership_confirmed": False} f["handovers"] = [handover] result = run(g, [s], [c], f, p) assert result["status"] == "PARTIAL" assert "HANDOVER_SHIPMENTS_PENDING" in result["lines"][0]["reasons"] assert result["lines"][0]["performance"] is None handover["business_key"] = "another-business" assert run(g, [s], [c], f, p)["status"] == "SUCCEEDED" def test_输入金额拒绝浮点与无穷(): for value in [1.1, "NaN", "Infinity"]: with pytest.raises(ValueError): money(value)