打通 T+ 快照、业务、发货、回款和核算准备链路,保留来源差异并提供待补状态。 新增业务自动接入任务、跨平台读取服务托管、权限复核、来源关联和前端核对页面。 补充迁移、隔离集成测试、浏览器模拟验收及角色授权边界修复。
141 lines
6.6 KiB
Python
141 lines
6.6 KiB
Python
"""隔离MySQL/API验证独立确认、默认门槛、凭证及内容变更,不写真实工资。"""
|
||
|
||
import copy
|
||
from decimal import Decimal
|
||
|
||
import pytest
|
||
from sqlalchemy import func, select
|
||
|
||
from app.core.db import get_session_factory
|
||
from app.models import OperationLog, PayrollInput, PerformanceLedger, Receipt
|
||
from tests.integration.test_formal_payroll import (
|
||
post,
|
||
unconfirmed_formal_chain as unconfirmed_formal_chain,
|
||
)
|
||
|
||
pytestmark = pytest.mark.integration
|
||
ROOT = "/api/v1/business/receipts"
|
||
|
||
|
||
def confirm(client, headers, receipt):
|
||
return client.post(f"{ROOT}/{receipt['id']}/confirm", headers=headers,
|
||
json={"expected_revision": receipt["confirmation_revision"]})
|
||
|
||
|
||
def calculate(client, headers, business):
|
||
return client.post(f"/api/v1/payroll/businesses/{business['id']}/calculate", headers=headers,
|
||
json={"month": "2026-03"})
|
||
|
||
|
||
def switch_off(client, headers):
|
||
post(client, "/api/v1/system/params/changes", headers, {
|
||
"change_type": 1, "remark": "t_独立确认关闭场景",
|
||
"items": [{"param_code": "receipt.confirmation.enabled", "dimension_value": "", "value": "0"}],
|
||
})
|
||
|
||
|
||
def test_默认开启_保存资料和凭证资格不能代替人工确认(client, admin, unconfirmed_formal_chain):
|
||
business, receipt, *_ = unconfirmed_formal_chain
|
||
headers = admin["headers"]
|
||
assert client.get(ROOT + "/confirmation-policy", headers=headers).json()["data"]["enabled"] is True
|
||
assert receipt["confirmation_status"] == 0
|
||
response = calculate(client, headers, business)
|
||
assert response.status_code == 400 and "人工确认" in response.text
|
||
with get_session_factory()() as s:
|
||
assert s.get(Receipt, receipt["id"]).confirmation_status == 0
|
||
assert s.scalar(select(func.count()).select_from(PerformanceLedger)) == 0
|
||
|
||
|
||
def test_独立确认后核算_重复确认幂等及审计(client, admin, unconfirmed_formal_chain):
|
||
business, receipt, *_ = unconfirmed_formal_chain
|
||
headers = admin["headers"]
|
||
response = confirm(client, headers, receipt)
|
||
assert response.status_code == 200, response.text
|
||
confirmed = response.json()["data"]
|
||
assert confirmed["confirmation_status"] == 1 and confirmed["confirmed_by"]
|
||
again = confirm(client, headers, receipt).json()["data"]
|
||
assert again["confirmed_at"] == confirmed["confirmed_at"]
|
||
result = calculate(client, headers, business)
|
||
assert result.status_code == 200, result.text
|
||
assert result.json()["data"]["performance"] == "0.20"
|
||
with get_session_factory()() as s:
|
||
assert s.get(Receipt, receipt["id"]).settlement_status == 1
|
||
assert s.scalar(select(func.count()).select_from(OperationLog).where(OperationLog.action == "人工确认")) >= 1
|
||
|
||
|
||
def test_关闭仅跳过人工确认_不产生确认状态(client, admin, unconfirmed_formal_chain):
|
||
business, receipt, *_ = unconfirmed_formal_chain
|
||
switch_off(client, admin["headers"])
|
||
result = calculate(client, admin["headers"], business)
|
||
assert result.status_code == 200, result.text
|
||
with get_session_factory()() as s:
|
||
assert s.get(Receipt, receipt["id"]).confirmation_status == 0
|
||
|
||
|
||
@pytest.mark.parametrize("manually_confirmed", [False, True])
|
||
def test_关闭开关或人工确认都不能豁免无效凭证(client, admin, unconfirmed_formal_chain, manually_confirmed):
|
||
business, receipt, *_ = unconfirmed_formal_chain
|
||
if manually_confirmed:
|
||
assert confirm(client, admin["headers"], receipt).status_code == 200
|
||
else:
|
||
switch_off(client, admin["headers"])
|
||
with get_session_factory()() as s:
|
||
record = s.scalar(select(PayrollInput).where(PayrollInput.business_id == business["id"]))
|
||
data = copy.deepcopy(record.data_json)
|
||
data["receipts"][0]["voucher_confirmed"] = False
|
||
record.data_json = data
|
||
s.commit()
|
||
result = calculate(client, admin["headers"], business)
|
||
assert result.status_code == 400 and "VOUCHER_STATE_PENDING" in result.text
|
||
|
||
|
||
def test_修改关键内容重置确认_旧页面不能误确认(client, admin, unconfirmed_formal_chain):
|
||
_, receipt, *_ = unconfirmed_formal_chain
|
||
headers = admin["headers"]
|
||
assert confirm(client, headers, receipt).status_code == 200
|
||
changed = client.put(f"{ROOT}/{receipt['id']}", headers=headers,
|
||
json={"receipt_date": receipt["receipt_date"], "amount": "101"})
|
||
assert changed.status_code == 200, changed.text
|
||
data = changed.json()["data"]
|
||
assert data["confirmation_status"] == 0 and data["confirmed_at"] is None
|
||
assert confirm(client, headers, receipt).status_code == 409
|
||
assert confirm(client, headers, data).status_code == 200
|
||
|
||
|
||
def test_只读拒绝确认及切换开关(client, admin, normal, unconfirmed_formal_chain):
|
||
_, receipt, *_ = unconfirmed_formal_chain
|
||
assert confirm(client, normal["headers"], receipt).status_code == 403
|
||
response = client.post("/api/v1/system/params/changes", headers=normal["headers"], json={
|
||
"change_type": 1, "items": [{"param_code": "receipt.confirmation.enabled", "value": "0"}],
|
||
})
|
||
assert response.status_code == 403
|
||
|
||
|
||
@pytest.mark.parametrize("unconfirmed_formal_chain,expected", [
|
||
({"unit_cost": "50"}, "0.00"), ({"unit_cost": "45"}, "-20.00"),
|
||
], indirect=["unconfirmed_formal_chain"])
|
||
def test_正式核算零毛利特例但低阈值仍为负(client, admin, unconfirmed_formal_chain, expected):
|
||
business, receipt, *_ = unconfirmed_formal_chain
|
||
assert confirm(client, admin["headers"], receipt).status_code == 200
|
||
result = calculate(client, admin["headers"], business)
|
||
assert result.status_code == 200, result.text
|
||
with get_session_factory()() as s:
|
||
ledger = s.scalar(select(PerformanceLedger).where(PerformanceLedger.business_id == business["id"]))
|
||
assert ledger.converted_profit == Decimal(expected)
|
||
assert ledger.performance_amount == Decimal(expected) * Decimal("0.01")
|
||
|
||
|
||
@pytest.mark.parametrize("role,profit,converted,performance", [
|
||
("salesperson", "0", "0", "0"),
|
||
("salesperson", "10", "-20", "-0.20"),
|
||
("info_officer", "0", "0", "0.10"),
|
||
])
|
||
def test_试算零毛利与信息员基数保持独立(client, admin, role, profit, converted, performance):
|
||
data = post(client, "/api/v1/system/params/preview", admin["headers"], {
|
||
"business_role": role, "customer_type": "personal", "product_category": "municipal_water",
|
||
"ship_date": "2026-03-10", "receipt_date": "2026-03-10", "revenue": "100",
|
||
"profit": profit, "received_amount": "100",
|
||
})
|
||
assert Decimal(data["converted_profit"]) == Decimal(converted)
|
||
assert Decimal(data["performance_amount"]) == Decimal(performance)
|