2026-10-16 08:47:29 -04:00
|
|
|
|
"""单笔回款核算集成测试(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 计、负绩效照算、
|
|
|
|
|
|
准入(normal 403 / marketing_ops 可用)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
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, Receipt
|
|
|
|
|
|
from app.models.receipt import SETTLEMENT_DONE
|
|
|
|
|
|
from tests.integration.factories import business_payload, customer
|
|
|
|
|
|
|
|
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
|
|
|
|
|
|
|
|
_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)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-16 09:39:40 -04:00
|
|
|
|
@pytest.fixture
|
|
|
|
|
|
def finance(make_user, auth_headers) -> dict:
|
|
|
|
|
|
"""财务角色账号(重算授权角色之一)。"""
|
|
|
|
|
|
user = make_user(["finance"])
|
|
|
|
|
|
user["headers"] = auth_headers(user)
|
|
|
|
|
|
return user
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-16 08:47:29 -04:00
|
|
|
|
_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:
|
2026-10-16 09:39:40 -04:00
|
|
|
|
_biz, receipt = _create_full_chain(
|
2026-10-16 08:47:29 -04:00
|
|
|
|
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:
|
2026-10-16 09:39:40 -04:00
|
|
|
|
_biz, receipt = _create_full_chain(
|
2026-10-16 08:47:29 -04:00
|
|
|
|
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,
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
2026-10-16 09:39:40 -04:00
|
|
|
|
_biz, receipt = _create_full_chain(
|
2026-10-16 08:47:29 -04:00
|
|
|
|
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",
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
2026-10-16 09:39:40 -04:00
|
|
|
|
_biz, receipt = _create_full_chain(
|
2026-10-16 08:47:29 -04:00
|
|
|
|
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_准入_normal403_marketing_ops可用(
|
|
|
|
|
|
self, client: TestClient, admin: dict, normal: dict, make_user, auth_headers
|
|
|
|
|
|
) -> None:
|
2026-10-16 09:39:40 -04:00
|
|
|
|
_biz, receipt = _create_full_chain(
|
2026-10-16 08:47:29 -04:00
|
|
|
|
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)
|
|
|
|
|
|
resp = _calc(client, ops["headers"], receipt["id"])
|
|
|
|
|
|
assert resp.status_code == 200, resp.text
|
|
|
|
|
|
|
|
|
|
|
|
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"]
|
2026-10-16 09:39:40 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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"]
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
2026-10-16 09:39:40 -04:00
|
|
|
|
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
|