feat(核算)!: 添加 T+ 正式核算与采集托管

补齐源快照、资料预演、正式导入及工资核算的依赖链路。
新增版本化输入、逐段 FIFO、生成锁和结构化当月调整,
并提供默认关闭的周期采集、有限重试及 Windows 托管。

原始客户资料、私有配置及首页改动不纳入本次提交。

BREAKING CHANGE: 升级前需应用新增迁移,并通过
PAYROLL_MAKER_USER_ID 绑定实际工资制作人,否则业务只读。
旧单批次核算默认关闭;历史兼容需显式启用
PAYROLL_ALLOW_LEGACY_CALCULATION。生产启用和真实金额待验收。
这个提交包含在:
liuwenxin 于 2026-11-02 09:42:53 +08:00 提交
1 父节点 1b074576fe
当前提交 108bf87225
154 个文件已更改
+9515 -645

没有与搜索条件相匹配的文件

+9 -5
查看文件
@@ -26,7 +26,7 @@ from app.models import PerformanceLedger, Personnel, Receipt, SecondaryAllocatio
from app.models.receipt import SETTLEMENT_DONE
from tests.integration.factories import business_payload, customer
pytestmark = pytest.mark.integration
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("legacy_calculation")]
_CALC = "/api/v1/calc"
_BUSINESSES = "/api/v1/business/businesses"
@@ -338,8 +338,8 @@ class TestGuards:
assert float(ledger["converted_profit"]) == -2000.0
assert float(ledger["performance_amount"]) < 0
def test_准入_normal403_marketing_ops可用(
self, client: TestClient, admin: dict, normal: dict, make_user, auth_headers
def test_准入_未指定账号拒绝_指定制作人可用(
self, client: TestClient, admin: dict, normal: dict, make_user, auth_headers, monkeypatch
) -> None:
_biz, receipt = _create_full_chain(
client,
@@ -354,6 +354,9 @@ class TestGuards:
ops = make_user(["marketing_ops"])
ops["headers"] = auth_headers(ops)
from app.core.config import get_settings
assert _calc(client, ops["headers"], receipt["id"]).status_code == 403
monkeypatch.setattr(get_settings(), "payroll_maker_user_id", ops["id"])
resp = _calc(client, ops["headers"], receipt["id"])
assert resp.status_code == 200, resp.text
@@ -426,7 +429,8 @@ class TestRecalc:
)
assert _calc(client, admin["headers"], receipt["id"]).status_code == 200
fin = finance
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": "成本录入修正"},
@@ -726,7 +730,7 @@ class TestSecondaryAllocation:
resp = client.post(
f"{_CALC}/receipts/{receipt['id']}/recalc",
json={"reason": "用例:分摊重算"},
headers=finance["headers"],
headers=admin["headers"],
)
assert resp.status_code == 200, resp.text