PayrollManagementSystem/backend/tests/integration/test_calc.py

799 lines
30 KiB
Python
Raw Normal View History

"""单笔回款核算集成测试(M3 ④:金标准复算到分)。
金标准场景(双类别混合业务,覆盖行级折算与汇总):
- 行1 市政水利:100×100.00 = 销售 10000,成本 3000 → 毛利率 0.7,阈值 30%
→ 折算 10000×(0.7−0.3) = 4000
- 行2 轨道交通:50×200.00 = 销售 10000,成本 8000 → 毛利率 0.2,阈值 20%
→ 折算 10000×(0.2−0.2) = 0
- 折算合计 4000;发货 10-01 → 回款 10-20,days=20
- 率 = (10 − 20×4/360)‰ = 88/9000;绩效 = 4000×88/9000 ≈ 39.11
- 回款 20000(=销售额 100%)→ R=100、权重 1 → 应付 = 39.11
守的还有:核算粒度 #25(业务级一次性计提——第二笔回款仅登记进度)、
已核算再核算 400、未指定批次 400、成本缺失行毛利按 0 计、负绩效照算、
准入(普通账号和仅营销后勤角色拒绝,分配工资角色后可用)。
"""
import uuid
from decimal import Decimal
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import select
from app.core.db import get_session_factory
from app.models import PerformanceLedger, Personnel, Receipt, SecondaryAllocation
from app.models.receipt import SETTLEMENT_DONE
from tests.integration.factories import business_payload, customer
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("legacy_calculation")]
_CALC = "/api/v1/calc"
_BUSINESSES = "/api/v1/business/businesses"
_SHIPMENTS = "/api/v1/business/shipments"
_RECEIPTS = "/api/v1/business/receipts"
_PERSONNEL = "/api/v1/base/personnel"
def _person(client: TestClient, headers: dict) -> dict:
resp = client.post(
_PERSONNEL,
json={
"personnel_name": f"t_核算_{uuid.uuid4().hex[:8]}",
"business_role_id": _role_id("salesperson"),
"salary_standard": "3000.00",
},
headers=headers,
)
assert resp.status_code == 200, resp.text
return resp.json()["data"]
def _role_id(code: str) -> int:
from sqlalchemy import select
from app.core.constants import BUSINESS_ROLE_SALESPERSON
from app.core.db import get_session_factory
from app.models import BusinessRole
factory = get_session_factory()
with factory() as session:
role_code = BUSINESS_ROLE_SALESPERSON if code == "salesperson" else code
return int(
session.execute(
select(BusinessRole.id).where(
BusinessRole.role_code == role_code, BusinessRole.status == 1
)
).scalar_one()
)
def _create_full_chain(
client: TestClient,
headers: dict,
*,
items: list[dict],
ship_date: str,
receipt_date: str,
amount: str,
) -> tuple[dict, dict]:
"""客户 → 人员 → 业务(双行明细+归属)→ 发货 → 回款(指定批次)。"""
cust = customer(client, headers)
person = _person(client, headers)
resp = client.post(
_BUSINESSES,
json=business_payload(cust["id"], owner_personnel_id=person["id"], items=items),
headers=headers,
)
assert resp.status_code == 200, resp.text
biz = resp.json()["data"]
resp = client.post(
_SHIPMENTS,
json={
"business_id": biz["id"],
"ship_date": ship_date,
"items": [{"product_name": "t_发货物", "quantity": "1", "sale_price": "1.00"}],
},
headers=headers,
)
assert resp.status_code == 200, resp.text
shipment = resp.json()["data"]
resp = client.post(
_RECEIPTS,
json={
"business_id": biz["id"],
"receipt_date": receipt_date,
"amount": amount,
"shipment_id": shipment["id"],
},
headers=headers,
)
assert resp.status_code == 200, resp.text
receipt = resp.json()["data"]
return biz, receipt
def _calc(client: TestClient, headers: dict, receipt_id: int):
return client.post(f"{_CALC}/receipts/{receipt_id}/calc", headers=headers)
@pytest.fixture
def finance(make_user, auth_headers) -> dict:
"""财务角色账号(重算授权角色之一)。"""
user = make_user(["finance"])
user["headers"] = auth_headers(user)
return user
_GOLD_ITEMS = [
{
"product_name": "t_市政_金标",
"product_category": "municipal_water",
"quantity": "100",
"sale_price": "100.00",
"cost_amount": "3000.00",
},
{
"product_name": "t_轨交_金标",
"product_category": "rail_transit",
"quantity": "50",
"sale_price": "200.00",
"cost_amount": "8000.00",
},
]
class TestGoldenStandard:
"""金标准:双类别混合业务复算到分(Decimal 精确断言)。"""
def test_复算到分_全字段固化(self, client: TestClient, admin: dict) -> None:
_biz, receipt = _create_full_chain(
client,
admin["headers"],
items=_GOLD_ITEMS,
ship_date="2026-10-01",
receipt_date="2026-10-20",
amount="20000.00",
)
resp = _calc(client, admin["headers"], receipt["id"])
assert resp.status_code == 200, resp.text
out = resp.json()["data"]
assert out["repeated"] is False
ledger = out["ledger"]
assert ledger["payment_days"] == 20
assert ledger["settlement_month"] == "2026-10"
# 率 = (10 − 20×4/360)‰ = 88/9000,Fraction 约分原文 11/1125
assert ledger["rate_text"] == "11/1125"
assert ledger["weight_r"] == 100
assert ledger["missing_cost"] == 0
assert ledger["snapshot_json"]["line_details"][0]["converted"] == "4000.00"
# 金额锚定 DB 事实源(NUMERIC(14,2) 读回恒两位;响应 JSON 的 Decimal
# 序列化 scale 不作断言——归前端展示问题)
factory = get_session_factory()
with factory() as session:
row = session.execute(
select(PerformanceLedger).where(
PerformanceLedger.receipt_id == receipt["id"],
PerformanceLedger.is_active == 1,
)
).scalar_one()
assert row.converted_profit == Decimal("4000.00")
# 绩效 = 4000 × 88/9000 = 39.111… → 落库两位 39.11
assert row.performance_amount == Decimal("39.11")
# 应付 = 绩效 × 权重 1 = 39.11
assert row.payable_amount == Decimal("39.11")
# 回款状态被 CAS 置已核算
detail = client.get(f"{_RECEIPTS}/{receipt['id']}", headers=admin["headers"]).json()["data"]
assert detail["settlement_status"] == 1
def test_已核算回款再核算400(self, client: TestClient, admin: dict) -> None:
_biz, receipt = _create_full_chain(
client,
admin["headers"],
items=_GOLD_ITEMS,
ship_date="2026-10-01",
receipt_date="2026-10-20",
amount="100.00",
)
assert _calc(client, admin["headers"], receipt["id"]).status_code == 200
resp = _calc(client, admin["headers"], receipt["id"])
assert resp.status_code == 400
assert "已核算" in resp.json()["message"]
def test_业务级一次性计提_第二笔仅登记进度(self, client: TestClient, admin: dict) -> None:
"""核算粒度 #25 一期口径:同业务第二笔回款不重复计提。"""
biz, first = _create_full_chain(
client,
admin["headers"],
items=_GOLD_ITEMS,
ship_date="2026-10-01",
receipt_date="2026-10-20",
amount="10000.00",
)
assert _calc(client, admin["headers"], first["id"]).status_code == 200
resp = client.post(
_RECEIPTS,
json={
"business_id": biz["id"],
"receipt_date": "2026-10-25",
"amount": "5000.00",
"shipment_id": None,
},
headers=admin["headers"],
)
assert resp.status_code == 200, resp.text
second = resp.json()["data"]
resp = _calc(client, admin["headers"], second["id"])
assert resp.status_code == 200, resp.text
out = resp.json()["data"]
assert out["repeated"] is True
factory = get_session_factory()
with factory() as session:
rows = (
session.execute(
select(PerformanceLedger).where(
PerformanceLedger.business_id == biz["id"],
PerformanceLedger.is_active == 1,
)
)
.scalars()
.all()
)
assert len(rows) == 1, "第二笔回款不得新增台账行(一次性计提)"
receipts = (
session.execute(select(Receipt).where(Receipt.business_id == biz["id"]))
.scalars()
.all()
)
assert all(r.settlement_status == SETTLEMENT_DONE for r in receipts)
class TestGuards:
"""校验链与策略点。"""
def test_未指定批次400(self, client: TestClient, admin: dict) -> None:
cust = customer(client, admin["headers"])
person = _person(client, admin["headers"])
resp = client.post(
_BUSINESSES,
json=business_payload(cust["id"], owner_personnel_id=person["id"]),
headers=admin["headers"],
)
assert resp.status_code == 200
biz = resp.json()["data"]
resp = client.post(
_RECEIPTS,
json={
"business_id": biz["id"],
"receipt_date": "2026-10-20",
"amount": "100.00",
},
headers=admin["headers"],
)
assert resp.status_code == 200
receipt = resp.json()["data"]
resp = _calc(client, admin["headers"], receipt["id"])
assert resp.status_code == 400
assert "发货批次" in resp.json()["message"]
def test_成本缺失行_毛利按0计并标记(self, client: TestClient, admin: dict) -> None:
items = [
{
"product_name": "t_无成本_市政",
"product_category": "municipal_water",
"quantity": "100",
"sale_price": "100.00",
"cost_amount": None,
}
]
_biz, receipt = _create_full_chain(
client,
admin["headers"],
items=items,
ship_date="2026-10-01",
receipt_date="2026-10-20",
amount="10000.00",
)
resp = _calc(client, admin["headers"], receipt["id"])
assert resp.status_code == 200, resp.text
ledger = resp.json()["data"]["ledger"]
# 成本缺失 → 毛利按 0 → 折算 = 销售额×(0−30%) = **−3000**(保守少算
# 方向:缺成本的行宁少算不多发),missing_cost=1
assert ledger["missing_cost"] == 1
assert float(ledger["converted_profit"]) == -3000.0
assert float(ledger["performance_amount"]) == pytest.approx(-29.33, abs=0.01)
def test_负绩效照算不归零(self, client: TestClient, admin: dict) -> None:
items = [
{
"product_name": "t_亏损_市政",
"product_category": "municipal_water",
"quantity": "100",
"sale_price": "100.00",
"cost_amount": "9000.00",
}
]
_biz, receipt = _create_full_chain(
client,
admin["headers"],
items=items,
ship_date="2026-10-01",
receipt_date="2026-10-20",
amount="10000.00",
)
resp = _calc(client, admin["headers"], receipt["id"])
assert resp.status_code == 200, resp.text
ledger = resp.json()["data"]["ledger"]
# 毛利 1000、率 0.1 < 阈值 30% → 折算 10000×(0.1−0.3) = −2000 → 负绩效
assert float(ledger["converted_profit"]) == -2000.0
assert float(ledger["performance_amount"]) < 0
def test_准入_未授权账号拒绝_分配工资角色后可用且无需绑定(
self, client: TestClient, admin: dict, normal: dict, make_user, auth_headers
) -> None:
_biz, receipt = _create_full_chain(
client,
admin["headers"],
items=_GOLD_ITEMS,
ship_date="2026-10-01",
receipt_date="2026-10-20",
amount="100.00",
)
resp = _calc(client, normal["headers"], receipt["id"])
assert resp.status_code == 403
ops = make_user(["marketing_ops"])
ops["headers"] = auth_headers(ops)
assert _calc(client, ops["headers"], receipt["id"]).status_code == 403
from app.models import AuthorizationSetting, Role, UserRole
with get_session_factory()() as session:
session.get(AuthorizationSetting, 1).maker_user_id = None
role = session.scalar(select(Role).where(Role.role_code == "payroll_maker"))
session.add(UserRole(user_id=ops["id"], role_id=role.id))
session.commit()
resp = _calc(client, ops["headers"], receipt["id"])
assert resp.status_code == 200, resp.text
def test_分界天数参数小数原文_核算不500(self, client: TestClient, admin: dict) -> None:
"""守 int(参数原文) 缺陷:INT 参数入库允许 '360.0'(Fraction 分母为 1),
直接 int(str) 抛 ValueError 会让整月核算 500。"""
resp = client.post(
"/api/v1/system/params/changes",
json={
"change_type": 1,
"remark": "用例:INT 参数小数原文",
"items": [
{
"param_code": "salesperson.days.boundary",
"dimension_value": "salesperson",
"value": "360.0",
}
],
},
headers=admin["headers"],
)
assert resp.status_code == 200, resp.text
_biz, receipt = _create_full_chain(
client,
admin["headers"],
items=_GOLD_ITEMS,
ship_date="2026-10-01",
receipt_date="2026-10-20",
amount="20000.00",
)
resp = _calc(client, admin["headers"], receipt["id"])
assert resp.status_code == 200, resp.text
# 分界取整无损:20 天 < 360 天 → 同一递减公式
assert resp.json()["data"]["ledger"]["rate_text"] == "11/1125"
def test_台账列表与筛选(self, client: TestClient, admin: dict) -> None:
biz, receipt = _create_full_chain(
client,
admin["headers"],
items=_GOLD_ITEMS,
ship_date="2026-10-01",
receipt_date="2026-10-20",
amount="100.00",
)
_calc(client, admin["headers"], receipt["id"])
resp = client.get(
f"{_CALC}/ledger",
params={"settlement_month": "2026-10", "is_active": 1},
headers=admin["headers"],
)
assert resp.status_code == 200
rows = resp.json()["data"]["list"]
assert rows and rows[0]["business_no"] == biz["business_no"]
class TestRecalc:
"""重算:授权动作,作废旧版本、落新版本并留痕。"""
def test_重算_新版本生效_旧版本保留(
self, client: TestClient, admin: dict, finance: dict
) -> None:
_biz, receipt = _create_full_chain(
client,
admin["headers"],
items=_GOLD_ITEMS,
ship_date="2026-10-01",
receipt_date="2026-10-20",
amount="20000.00",
)
assert _calc(client, admin["headers"], receipt["id"]).status_code == 200
assert client.post(f"{_CALC}/receipts/{receipt['id']}/recalc", json={"reason": "非制作人拒绝"}, headers=finance["headers"]).status_code == 403
fin = admin
resp = client.post(
f"{_CALC}/receipts/{receipt['id']}/recalc",
json={"reason": "成本录入修正"},
headers=fin["headers"],
)
assert resp.status_code == 200, resp.text
factory = get_session_factory()
with factory() as session:
rows = (
session.execute(
select(PerformanceLedger)
.where(PerformanceLedger.receipt_id == receipt["id"])
.order_by(PerformanceLedger.calc_version)
)
.scalars()
.all()
)
assert [(r.calc_version, r.is_active) for r in rows] == [(1, 0), (2, 1)]
assert rows[1].calc_reason == "成本录入修正"
# 详情回显的是新版本
assert (
client.get(
f"{_CALC}/ledger",
params={"receipt_id": receipt["id"], "is_active": 1},
headers=admin["headers"],
).json()["data"]["list"][0]["calc_version"]
== 2
)
def test_重算_原因缺失400(self, client: TestClient, admin: dict) -> None:
_biz, receipt = _create_full_chain(
client,
admin["headers"],
items=_GOLD_ITEMS,
ship_date="2026-10-01",
receipt_date="2026-10-20",
amount="100.00",
)
assert _calc(client, admin["headers"], receipt["id"]).status_code == 200
resp = client.post(
f"{_CALC}/receipts/{receipt['id']}/recalc",
json={"reason": " "},
headers=admin["headers"],
)
assert resp.status_code == 400
def test_未核算过_重算400(self, client: TestClient, admin: dict) -> None:
_biz, receipt = _create_full_chain(
client,
admin["headers"],
items=_GOLD_ITEMS,
ship_date="2026-10-01",
receipt_date="2026-10-20",
amount="100.00",
)
resp = client.post(
f"{_CALC}/receipts/{receipt['id']}/recalc",
json={"reason": "无旧版本"},
headers=admin["headers"],
)
assert resp.status_code == 400
assert "尚未核算" in resp.json()["message"]
fix(m3): code-review P0 六项——算错钱与不可用 - 经理聚合分成放大 100 倍:参数按百分数存储(12.5=12.5%),parse_fraction 后调用方又 ×100、函数内部再 ÷100,净效应「绩效×12.5」;去掉调用方 ×100,rate_text 改用实际参数原文(原硬编码 12.5%+5% 参数调整后失真) - 审批联动只认终审:财务节点通过(1→2)不再提前 mark_locked/调整单生效, 否则领导驳回后 locked_at/locked_by 残留、两级审批形同虚设;驳回在任何 节点即时联动;mark_rejected 补清锁定字段 - 调整单新建不可用:ENABLE_CODE_BY_TYPE 未登记 type=3 → is_enabled 恒 400; 补登记 approval.adjustment.enabled。另修复叠加缺陷——开关关闭分支手工 ApprovalStep(order_id=调整单id),而该外键指向 approval_orders,必然 errno 1452;统一改走 submit_order(自动通过留痕由它负责) - 重算双重计提:业务级一次性计提的「进度登记」回款无生效台账行,重算会 凭空落新行;无旧行一律 400 - 前端重算传错 id:台账行主键当 receipt_id 传给重算接口,会作废无关回款 的台账;改传 row.receipt_id(聚合行防御) - quantize 显式 ROUND_HALF_UP:9 处金额 quantize 缺省银行家舍入, x.xx5 与四舍五入口径差 1 分(docstring 却声称 HALF_UP) 测试:显式控制审批开关(V8 起种子默认开启,用例不再依赖默认值); 新增回归 4 例(经理聚合比例/调整单即生效/进度登记重算 400/财务节点不提前 锁定),test_calc+test_settlements 22 passed Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-18 23:25:21 -04:00
def test_进度登记回款重算400_不得凭空落行(self, client: TestClient, admin: dict) -> None:
"""业务级一次性计提的第二笔回款无台账行;重算若落新行会双重计提。"""
biz, first = _create_full_chain(
client,
admin["headers"],
items=_GOLD_ITEMS,
ship_date="2026-10-01",
receipt_date="2026-10-20",
amount="20000.00",
)
assert _calc(client, admin["headers"], first["id"]).status_code == 200
resp = client.post(
_RECEIPTS,
json={
"business_id": biz["id"],
"receipt_date": "2026-10-25",
"amount": "100.00",
},
headers=admin["headers"],
)
assert resp.status_code == 200, resp.text
second = resp.json()["data"]
assert _calc(client, admin["headers"], second["id"]).json()["data"]["repeated"] is True
resp = client.post(
f"{_CALC}/receipts/{second['id']}/recalc",
json={"reason": "尝试重算进度登记"},
headers=admin["headers"],
)
assert resp.status_code == 400, resp.text
assert "进度登记" in resp.json()["message"]
# 同业务仍只有一行生效台账(未凭空新增)
factory = get_session_factory()
with factory() as session:
rows = (
session.execute(
select(PerformanceLedger).where(
PerformanceLedger.business_id == biz["id"],
PerformanceLedger.is_active == 1,
)
)
.scalars()
.all()
)
assert len(rows) == 1
def test_重算_普通用户与营销后勤403(
self, client: TestClient, normal: dict, make_user, auth_headers
) -> None:
ops = make_user(["marketing_ops"])
ops["headers"] = auth_headers(ops)
for headers in (normal["headers"], ops["headers"]):
resp = client.post(f"{_CALC}/receipts/1/recalc", json={"reason": "x"}, headers=headers)
assert resp.status_code == 403
class TestBatch:
"""批量核算:逐笔独立事务,失败不中断。"""
def test_批量_逐笔成败清单(self, client: TestClient, admin: dict) -> None:
biz, r1 = _create_full_chain(
client,
admin["headers"],
items=_GOLD_ITEMS,
ship_date="2026-10-01",
receipt_date="2026-10-20",
amount="20000.00",
)
# 同业务第二笔回款:核算走 #25 重复分支(ok=True、repeated=True)
resp = client.post(
_RECEIPTS,
json={
"business_id": biz["id"],
"receipt_date": "2026-10-25",
"amount": "100.00",
},
headers=admin["headers"],
)
assert resp.status_code == 200, resp.text
r2 = resp.json()["data"]
resp = client.post(
f"{_CALC}/batch",
json={"receipt_ids": [r1["id"], r2["id"], 999999]},
headers=admin["headers"],
)
assert resp.status_code == 200, resp.text
results = resp.json()["data"]
by_id = {r["receipt_id"]: r for r in results}
assert by_id[r1["id"]]["ok"] is True and by_id[r1["id"]]["repeated"] is False
assert by_id[r2["id"]]["ok"] is True and by_id[r2["id"]]["repeated"] is True
assert by_id[999999]["ok"] is False
class TestSecondaryAllocation:
"""二次分摊拆行(澄清 #1 答复 2026-10-19:分配绩效——算后分)。
方案行直插入库(跳过保存侧校验,聚焦核算引擎;保存侧校验用例在
test_businesses)。金标准:双类别混合业务绩效 39.11 / 折算 4000.00,
拆 30%+40% 后参与人 11.73/15.64、负责人余额 11.74,行合计守恒。
"""
def _add_plan(self, business_id: int, rows: list[tuple[int, str]]) -> None:
"""直插方案行(测试隔离造数;核算引擎按「当前落库方案」消费)。"""
factory = get_session_factory()
with factory() as session:
for pid, ratio in rows:
person = session.get(Personnel, pid)
session.add(
SecondaryAllocation(
business_id=business_id,
personnel_id=pid,
personnel_name=person.personnel_name,
ratio=Decimal(ratio),
)
)
session.commit()
def test_分摊拆行_参与人比例与负责人余额守恒(self, client: TestClient, admin: dict) -> None:
biz, receipt = _create_full_chain(
client,
admin["headers"],
items=_GOLD_ITEMS,
ship_date="2026-10-01",
receipt_date="2026-10-20",
amount="20000.00",
)
parts = [_person(client, admin["headers"]) for _ in range(2)]
self._add_plan(biz["id"], [(parts[0]["id"], "30"), (parts[1]["id"], "40")])
resp = _calc(client, admin["headers"], receipt["id"])
assert resp.status_code == 200, resp.text
factory = get_session_factory()
with factory() as session:
rows = (
session.execute(
select(PerformanceLedger).where(
PerformanceLedger.receipt_id == receipt["id"],
PerformanceLedger.is_active == 1,
)
)
.scalars()
.all()
)
assert len(rows) == 3, "负责人+两参与人各一行"
by_pid = {r.personnel_id: r for r in rows}
a, b, o = (
by_pid[parts[0]["id"]],
by_pid[parts[1]["id"]],
by_pid[biz["owner_personnel_id"]],
)
# 绩效份额:A=39.11×30%=11.73、B=39.11×40%=15.64、负责人=守恒余额 11.74
assert (a.performance_amount, b.performance_amount, o.performance_amount) == (
Decimal("11.73"),
Decimal("15.64"),
Decimal("11.74"),
)
# 行合计 == 业务级全额(守恒是分摊红线:多一分少一分都是错账)
assert a.payable_amount + b.payable_amount + o.payable_amount == Decimal("39.11")
# 折算额同比例拆分:4000 的 30%/40%/余额
assert (a.converted_profit, b.converted_profit, o.converted_profit) == (
Decimal("1200.00"),
Decimal("1600.00"),
Decimal("1200.00"),
)
# 行级身份固化:参与人各自岗位;快照带方案与角色标签
# (ratio 为 NUMERIC(5,2) 读回两位 scale,断言用数值比较)
assert a.role_code == b.role_code == "salesperson"
assert a.snapshot_json["alloc_role"] == "participant"
assert Decimal(a.snapshot_json["alloc_ratio"]) == Decimal("30")
assert o.snapshot_json["alloc_role"] == "owner"
assert Decimal(o.snapshot_json["alloc_plan"][0]["ratio"]) == Decimal("30")
def test_分摊后第二笔回款仅登记进度(self, client: TestClient, admin: dict) -> None:
"""一次性计提(#25)与分摊叠加:第二笔不新增行、不重复拆分。"""
biz, first = _create_full_chain(
client,
admin["headers"],
items=_GOLD_ITEMS,
ship_date="2026-10-01",
receipt_date="2026-10-20",
amount="10000.00",
)
part = _person(client, admin["headers"])
self._add_plan(biz["id"], [(part["id"], "50")])
assert _calc(client, admin["headers"], first["id"]).status_code == 200
resp = client.post(
_RECEIPTS,
json={
"business_id": biz["id"],
"receipt_date": "2026-10-25",
"amount": "5000.00",
},
headers=admin["headers"],
)
assert resp.status_code == 200, resp.text
second = resp.json()["data"]
out = _calc(client, admin["headers"], second["id"]).json()["data"]
assert out["repeated"] is True
factory = get_session_factory()
with factory() as session:
rows = (
session.execute(
select(PerformanceLedger).where(
PerformanceLedger.business_id == biz["id"],
PerformanceLedger.is_active == 1,
)
)
.scalars()
.all()
)
assert len(rows) == 2, "第二笔回款不得新增拆分行(一次性计提)"
def test_重算作废全部拆分行并落新版本(
self, client: TestClient, admin: dict, finance: dict
) -> None:
biz, receipt = _create_full_chain(
client,
admin["headers"],
items=_GOLD_ITEMS,
ship_date="2026-10-01",
receipt_date="2026-10-20",
amount="20000.00",
)
parts = [_person(client, admin["headers"]) for _ in range(2)]
self._add_plan(biz["id"], [(parts[0]["id"], "30"), (parts[1]["id"], "40")])
assert _calc(client, admin["headers"], receipt["id"]).status_code == 200
resp = client.post(
f"{_CALC}/receipts/{receipt['id']}/recalc",
json={"reason": "用例:分摊重算"},
headers=admin["headers"],
)
assert resp.status_code == 200, resp.text
factory = get_session_factory()
with factory() as session:
rows = (
session.execute(
select(PerformanceLedger).where(PerformanceLedger.receipt_id == receipt["id"])
)
.scalars()
.all()
)
active = [r for r in rows if r.is_active == 1]
inactive = [r for r in rows if r.is_active == 0]
assert len(active) == 3 and len(inactive) == 3
assert {r.calc_version for r in active} == {2}, "新版本三行"
assert {r.calc_version for r in inactive} == {1}, "旧版本三行全部作废"
def test_合计超100核算侧拦截(self, client: TestClient, admin: dict) -> None:
biz, receipt = _create_full_chain(
client,
admin["headers"],
items=_GOLD_ITEMS,
ship_date="2026-10-01",
receipt_date="2026-10-20",
amount="100.00",
)
part = _person(client, admin["headers"])
self._add_plan(biz["id"], [(part["id"], "60")])
# 再直插另一行绕过保存校验(合计 110),核算侧防御必须拦
other = _person(client, admin["headers"])
factory = get_session_factory()
with factory() as session:
person = session.get(Personnel, other["id"])
session.add(
SecondaryAllocation(
business_id=biz["id"],
personnel_id=other["id"],
personnel_name=person.personnel_name,
ratio=Decimal("50"),
)
)
session.commit()
resp = _calc(client, admin["headers"], receipt["id"])
assert resp.status_code == 400
assert "超过 100%" in resp.json()["message"]
def test_参与人含负责人核算侧拦截(self, client: TestClient, admin: dict) -> None:
biz, receipt = _create_full_chain(
client,
admin["headers"],
items=_GOLD_ITEMS,
ship_date="2026-10-01",
receipt_date="2026-10-20",
amount="100.00",
)
self._add_plan(biz["id"], [(biz["owner_personnel_id"], "30")])
resp = _calc(client, admin["headers"], receipt["id"])
assert resp.status_code == 400
assert "含合同负责营销人员" in resp.json()["message"]