打通 T+ 快照、业务、发货、回款和核算准备链路,保留来源差异并提供待补状态。 新增业务自动接入任务、跨平台读取服务托管、权限复核、来源关联和前端核对页面。 补充迁移、隔离集成测试、浏览器模拟验收及角色授权边界修复。
267 lines
8.9 KiB
Python
267 lines
8.9 KiB
Python
"""预演只分配资金,覆盖先旧后新、预收与缺条件的核算边界。"""
|
|
|
|
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)
|