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>
This commit is contained in:
1 parent
35fe37fb0c
commit
cb4f38dfd9
6 files changed
+274
-68
No files matched your search
@@ -39,30 +39,38 @@ _GOLD_ITEMS = [
|
||||
]
|
||||
|
||||
|
||||
def _person(client: TestClient, headers: dict) -> dict:
|
||||
def _role_id(role_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_id = int(
|
||||
return int(
|
||||
session.execute(
|
||||
select(BusinessRole.id).where(
|
||||
BusinessRole.role_code == BUSINESS_ROLE_SALESPERSON,
|
||||
BusinessRole.role_code == role_code,
|
||||
BusinessRole.status == 1,
|
||||
)
|
||||
).scalar_one()
|
||||
)
|
||||
|
||||
|
||||
def _person(
|
||||
client: TestClient,
|
||||
headers: dict,
|
||||
*,
|
||||
role_code: str = "salesperson",
|
||||
manager_id: int | None = None,
|
||||
) -> dict:
|
||||
resp = client.post(
|
||||
_PERSONNEL,
|
||||
json={
|
||||
"personnel_name": f"t_月结_{uuid.uuid4().hex[:8]}",
|
||||
"business_role_id": role_id,
|
||||
"business_role_id": _role_id(role_code),
|
||||
"salary_standard": "3000.00",
|
||||
**({"manager_id": manager_id} if manager_id is not None else {}),
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
@@ -70,6 +78,20 @@ def _person(client: TestClient, headers: dict) -> dict:
|
||||
return resp.json()["data"]
|
||||
|
||||
|
||||
def _set_switch(client: TestClient, headers: dict, code: str, value: str) -> None:
|
||||
"""显式设置审批开关(V8 起审批开关默认开启,用例不得依赖种子默认值)。"""
|
||||
resp = client.post(
|
||||
"/api/v1/system/params/changes",
|
||||
json={
|
||||
"change_type": 1,
|
||||
"remark": f"用例设置 {code}={value}",
|
||||
"items": [{"param_code": code, "dimension_value": "", "value": value}],
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
|
||||
|
||||
def _calc_one(client: TestClient, headers: dict) -> dict:
|
||||
"""完整造一笔已核算回款(回款日 2026-10-20 → 归月 2026-10)。"""
|
||||
cust = customer(client, headers)
|
||||
@@ -161,7 +183,8 @@ class TestGenerate:
|
||||
).status_code
|
||||
== 200
|
||||
)
|
||||
# 提交(开关默认关)→ 自动通过锁定
|
||||
# 显式关开关 → 提交即自动通过锁定(用例不依赖种子默认值)
|
||||
_set_switch(client, admin["headers"], "approval.monthly_settlement.enabled", "0")
|
||||
sid = client.get(f"{_SETTLEMENTS}", headers=admin["headers"]).json()["data"]["list"][0][
|
||||
"id"
|
||||
]
|
||||
@@ -172,8 +195,7 @@ class TestGenerate:
|
||||
f"{_SETTLEMENTS}/generate", json={"month": "2026-10"}, headers=admin["headers"]
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
# 已核算禁改(M2 防线)先于月结锁定拦截生效——两者均 400 拒改
|
||||
assert ("已核算" in resp.json()["message"]) or ("锁定" in resp.json()["message"])
|
||||
assert "已审定锁定" in resp.json()["message"]
|
||||
|
||||
|
||||
class TestSubmitAndLock:
|
||||
@@ -183,6 +205,8 @@ class TestSubmitAndLock:
|
||||
self, client: TestClient, admin: dict, finance: dict
|
||||
) -> None:
|
||||
_calc_one(client, admin["headers"])
|
||||
# 显式关开关(V8 起种子默认开启,用例不得依赖默认值)
|
||||
_set_switch(client, admin["headers"], "approval.monthly_settlement.enabled", "0")
|
||||
resp = client.post(
|
||||
f"{_SETTLEMENTS}/generate", json={"month": "2026-10"}, headers=admin["headers"]
|
||||
)
|
||||
@@ -322,6 +346,13 @@ class TestSwitchOn:
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
|
||||
# 财务节点通过只是流转到待领导——不得提前锁定月结(否则领导驳回后
|
||||
# 锁定字段残留、两级审批形同虚设)
|
||||
mid = client.get(f"{_SETTLEMENTS}/{sid}", headers=admin["headers"]).json()["data"]
|
||||
assert mid["status"] == 2, "财务通过后应仍为待审批,不得提前锁定"
|
||||
assert mid["locked_at"] is None
|
||||
assert mid["locked_by"] is None
|
||||
|
||||
# 领导通过 → 锁定
|
||||
resp = client.post(
|
||||
f"/api/v1/system/approvals/{order['id']}/action",
|
||||
@@ -333,3 +364,89 @@ class TestSwitchOn:
|
||||
assert final["status"] == 3
|
||||
assert final["locked_at"] is not None
|
||||
assert final["locked_by"] == leader["username"] # 审批通过人=领导账号
|
||||
|
||||
|
||||
class TestManagerAggregate:
|
||||
"""经理团队聚合:比例按参数实际值计算(守「百分数再乘 100」放大缺陷)。"""
|
||||
|
||||
def test_聚合金额按实际比例计入(self, client: TestClient, admin: dict) -> None:
|
||||
manager = _person(client, admin["headers"], role_code="sales_manager")
|
||||
sales = _person(client, admin["headers"], manager_id=manager["id"])
|
||||
|
||||
cust = customer(client, admin["headers"])
|
||||
resp = client.post(
|
||||
_BUSINESSES,
|
||||
json=business_payload(
|
||||
cust["id"],
|
||||
owner_personnel_id=sales["id"],
|
||||
items=[dict(_GOLD_ITEMS[0], product_name=f"t_聚合_{uuid.uuid4().hex[:6]}")],
|
||||
),
|
||||
headers=admin["headers"],
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
biz = resp.json()["data"]
|
||||
resp = client.post(
|
||||
_SHIPMENTS,
|
||||
json={
|
||||
"business_id": biz["id"],
|
||||
"ship_date": "2026-10-01",
|
||||
"items": [{"product_name": "t_发", "quantity": "1", "sale_price": "1.00"}],
|
||||
},
|
||||
headers=admin["headers"],
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
resp = client.post(
|
||||
_RECEIPTS,
|
||||
json={
|
||||
"business_id": biz["id"],
|
||||
"receipt_date": "2026-10-20",
|
||||
"amount": "20000.00",
|
||||
"shipment_id": resp.json()["data"]["id"],
|
||||
},
|
||||
headers=admin["headers"],
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
receipt = resp.json()["data"]
|
||||
assert (
|
||||
client.post(
|
||||
f"{_CALC}/receipts/{receipt['id']}/calc", headers=admin["headers"]
|
||||
).status_code
|
||||
== 200
|
||||
)
|
||||
|
||||
resp = client.post(
|
||||
f"{_SETTLEMENTS}/generate", json={"month": "2026-10"}, headers=admin["headers"]
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
items = resp.json()["data"]["items"]
|
||||
manager_items = [i for i in items if i["calc_type"] == 2]
|
||||
assert len(manager_items) == 1
|
||||
# 业务员绩效 39.11;经理 = 39.11×12.5% + 39.11×5% = 6.84425 → 6.84
|
||||
# (若调用方把百分数再 ×100,此处会是 684.43)
|
||||
assert manager_items[0]["personnel_id"] == manager["id"]
|
||||
assert manager_items[0]["performance_sum"] == "6.84"
|
||||
assert manager_items[0]["payable_sum"] == "6.84"
|
||||
|
||||
|
||||
class TestAdjustment:
|
||||
"""调整单:已锁定月份补发/扣减的唯一通道。"""
|
||||
|
||||
def test_新建调整单_开关关闭即生效(self, client: TestClient, admin: dict) -> None:
|
||||
# 显式关开关(用例不依赖种子默认值)
|
||||
_set_switch(client, admin["headers"], "approval.adjustment.enabled", "0")
|
||||
person = _person(client, admin["headers"])
|
||||
resp = client.post(
|
||||
f"{_SETTLEMENTS}/adjustments",
|
||||
json={
|
||||
"settlement_month": "2026-10",
|
||||
"personnel_id": person["id"],
|
||||
"direction": 1,
|
||||
"amount": "500.00",
|
||||
"reason": "补发差额",
|
||||
},
|
||||
headers=admin["headers"],
|
||||
)
|
||||
# 守 ENABLE_CODE_BY_TYPE 漏登记 type=3:修复前此处恒 400「未知的审批对象类型」
|
||||
assert resp.status_code == 200, resp.text
|
||||
out = resp.json()["data"]
|
||||
assert out["status"] == 2 # 已生效
|
||||
Reference in new issue
Block a user