feat(calc): 重算授权留痕与批量核算
- calc_receipt 双模式:recalc_reason 非 None=重算(audit action=重算/ reason 留痕、作废旧版本行、calc_version 自动递增、CAS 仅首算执行—— 重算状态本就是已核算,误跑 CAS 会误报并发撞车) - recalc_receipt 薄委托(reason 必填、未核算过 400);重算授权 require_roles(admin, finance),营销后勤不可重算 - batch_calc:逐笔独立事务不开外层 audit(嵌套禁令),逐笔成败清单 (同业务第二笔回款走 #25 重复分支 ok=True/repeated=True) - 测试 +5:重算版本对/原因缺失 400/未核算重算 400/越权 403/批量清单 - 修 next_version 空表 max NULL → int(None) TypeError Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
df1a3839a1
commit
22a2872100
4 files changed
+288
-24
No files matched your search
@@ -119,6 +119,14 @@ 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_市政_金标",
|
||||
@@ -141,7 +149,7 @@ class TestGoldenStandard:
|
||||
"""金标准:双类别混合业务复算到分(Decimal 精确断言)。"""
|
||||
|
||||
def test_复算到分_全字段固化(self, client: TestClient, admin: dict) -> None:
|
||||
biz, receipt = _create_full_chain(
|
||||
_biz, receipt = _create_full_chain(
|
||||
client,
|
||||
admin["headers"],
|
||||
items=_GOLD_ITEMS,
|
||||
@@ -185,7 +193,7 @@ class TestGoldenStandard:
|
||||
assert detail["settlement_status"] == 1
|
||||
|
||||
def test_已核算回款再核算400(self, client: TestClient, admin: dict) -> None:
|
||||
biz, receipt = _create_full_chain(
|
||||
_biz, receipt = _create_full_chain(
|
||||
client,
|
||||
admin["headers"],
|
||||
items=_GOLD_ITEMS,
|
||||
@@ -288,7 +296,7 @@ class TestGuards:
|
||||
"cost_amount": None,
|
||||
}
|
||||
]
|
||||
biz, receipt = _create_full_chain(
|
||||
_biz, receipt = _create_full_chain(
|
||||
client,
|
||||
admin["headers"],
|
||||
items=items,
|
||||
@@ -315,7 +323,7 @@ class TestGuards:
|
||||
"cost_amount": "9000.00",
|
||||
}
|
||||
]
|
||||
biz, receipt = _create_full_chain(
|
||||
_biz, receipt = _create_full_chain(
|
||||
client,
|
||||
admin["headers"],
|
||||
items=items,
|
||||
@@ -333,7 +341,7 @@ class TestGuards:
|
||||
def test_准入_normal403_marketing_ops可用(
|
||||
self, client: TestClient, admin: dict, normal: dict, make_user, auth_headers
|
||||
) -> None:
|
||||
biz, receipt = _create_full_chain(
|
||||
_biz, receipt = _create_full_chain(
|
||||
client,
|
||||
admin["headers"],
|
||||
items=_GOLD_ITEMS,
|
||||
@@ -368,3 +376,132 @@ class TestGuards:
|
||||
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
|
||||
|
||||
fin = finance
|
||||
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"]
|
||||
|
||||
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
|
||||
Reference in new issue
Block a user