PayrollManagementSystem/backend/tests/unit/test_tplus_fifo.py

267 lines
8.9 KiB
Python
Raw Normal View History

"""预演只分配资金,覆盖先旧后新、预收与缺条件的核算边界。"""
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)