213 lines
8.4 KiB
Python
213 lines
8.4 KiB
Python
"""
|
||
预编报表(预算版三张报表)测试 — P2 2026-08-30
|
||
|
||
覆盖:
|
||
1. 三张预算版报表接口 200
|
||
2. has_budget 标注正确(有预算行 true / 无预算映射行 false)
|
||
3. 差异计算与 budget-execution 一致(同 KPI 同 period 对比)
|
||
4. 无预算行显式标注(budget_source=none)
|
||
5. 比率型KPI单独标注(ratio_kpi=true,不计算金额差异)
|
||
"""
|
||
from fastapi.testclient import TestClient
|
||
from sqlalchemy.orm import Session
|
||
|
||
from tests.conftest import create_test_user, get_token_for_user, auth_header
|
||
from app.models import KPIDefinition, KPIValue, BudgetPlan
|
||
|
||
|
||
def _create_kpi(db: Session, code: str, name: str, entity_id: int = 1, frequency: str = "monthly", target_value=None) -> KPIDefinition:
|
||
kpi = KPIDefinition(
|
||
entity_id=entity_id,
|
||
kpi_code=code,
|
||
kpi_name=name,
|
||
dimension="finance",
|
||
category="financial_report",
|
||
formula="-",
|
||
data_source="测试",
|
||
data_owner="财务部",
|
||
frequency=frequency,
|
||
unit="元",
|
||
target_value=target_value,
|
||
kpi_level="operational",
|
||
status="active",
|
||
)
|
||
db.add(kpi)
|
||
db.commit()
|
||
db.refresh(kpi)
|
||
return kpi
|
||
|
||
|
||
def _create_budget(db: Session, kpi_id: int, period: str, value: float, version: str = "v1.0") -> BudgetPlan:
|
||
plan = BudgetPlan(
|
||
entity_id=1,
|
||
kpi_id=kpi_id,
|
||
period=period,
|
||
budget_value=value,
|
||
budget_year=int(period.split("-")[0]),
|
||
budget_month=int(period.split("-")[1]),
|
||
version=version,
|
||
status="active",
|
||
)
|
||
db.add(plan)
|
||
db.commit()
|
||
db.refresh(plan)
|
||
return plan
|
||
|
||
|
||
def _create_actual(db: Session, kpi_id: int, period: str, value: float) -> KPIValue:
|
||
v = KPIValue(kpi_id=kpi_id, period=period, actual_value=value, source_type="manual")
|
||
db.add(v)
|
||
db.commit()
|
||
db.refresh(v)
|
||
return v
|
||
|
||
|
||
class TestProformaProfitStatement:
|
||
BASE = "/api/cma/reports/proforma/profit-statement"
|
||
|
||
def _setup(self, db: Session):
|
||
"""F_REVENUE: 预算150 / 实际123.45;F_OP_CFLOW: 预算20"""
|
||
create_test_user(db)
|
||
rev = _create_kpi(db, "F_REVENUE", "营业收入", frequency="quarterly", target_value=1200)
|
||
op = _create_kpi(db, "F_OP_CFLOW", "经营性现金流")
|
||
_create_budget(db, rev.id, "2026-08", 150.0)
|
||
_create_budget(db, op.id, "2026-08", 20.0)
|
||
_create_actual(db, rev.id, "2026-08", 123.45)
|
||
|
||
def test_returns_200_and_budget_mapping(self, client: TestClient, db: Session):
|
||
self._setup(db)
|
||
token = get_token_for_user(client)
|
||
resp = client.get(f"{self.BASE}?period=2026-08", headers=auth_header(token))
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["period"] == "2026-08"
|
||
assert data["budget_version"] == "v1.0"
|
||
|
||
# 营业收入(6001) → F_REVENUE:预算150 实际123.45 差异-26.55/-17.7%
|
||
rev_line = None
|
||
for block in data["blocks"]:
|
||
for item in block["items"]:
|
||
if item["code"] == "6001":
|
||
rev_line = item
|
||
assert rev_line is not None, "利润表应含营业收入(6001)行"
|
||
assert rev_line["has_budget"] is True
|
||
assert rev_line["mapped_kpi_code"] == "F_REVENUE"
|
||
assert rev_line["budget_source"] == "budget_plan"
|
||
assert rev_line["budget_value"] == 150.0
|
||
assert rev_line["actual_value"] == 123.45
|
||
assert rev_line["deviation_amount"] == -26.55
|
||
assert rev_line["deviation_rate"] == -17.7
|
||
|
||
def test_no_budget_line_explicit(self, client: TestClient, db: Session):
|
||
"""无预算映射的行(如 6402 其他业务成本)显式 has_budget=false"""
|
||
self._setup(db)
|
||
token = get_token_for_user(client)
|
||
resp = client.get(f"{self.BASE}?period=2026-08", headers=auth_header(token))
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
line = None
|
||
for block in data["blocks"]:
|
||
for item in block["items"]:
|
||
if item["code"] == "6402":
|
||
line = item
|
||
assert line is not None
|
||
assert line["has_budget"] is False
|
||
assert line["budget_source"] == "none"
|
||
assert line["budget_value"] is None
|
||
|
||
def test_deviation_matches_budget_execution(self, client: TestClient, db: Session):
|
||
"""同 KPI 同 period:proforma 差异与 budget-execution 一致"""
|
||
self._setup(db)
|
||
token = get_token_for_user(client)
|
||
# budget-execution 里的 F_REVENUE
|
||
be = client.get("/api/cma/reports/budget-execution?period=2026-08", headers=auth_header(token))
|
||
assert be.status_code == 200
|
||
be_item = next(i for i in be.json()["items"] if i["kpi_code"] == "F_REVENUE")
|
||
# proforma 利润表 6001 行
|
||
pf = client.get(f"{self.BASE}?period=2026-08", headers=auth_header(token))
|
||
pf_item = None
|
||
for block in pf.json()["blocks"]:
|
||
for item in block["items"]:
|
||
if item["code"] == "6001":
|
||
pf_item = item
|
||
assert pf_item is not None
|
||
assert pf_item["actual_value"] == be_item["actual_value"]
|
||
assert pf_item["budget_value"] == be_item["budget_value"]
|
||
assert pf_item["deviation_amount"] == be_item["deviation_amount"]
|
||
assert pf_item["deviation_rate"] == be_item["deviation_rate"]
|
||
|
||
|
||
class TestProformaBalanceSheet:
|
||
BASE = "/api/cma/reports/proforma/balance-sheet"
|
||
|
||
def _setup(self, db: Session):
|
||
create_test_user(db)
|
||
op = _create_kpi(db, "F_OP_CFLOW", "经营性现金流")
|
||
ar = _create_kpi(db, "F_AR_DAYS", "应收账款周转天数", frequency="monthly", target_value=5)
|
||
_create_budget(db, op.id, "2026-08", 20.0)
|
||
_create_budget(db, ar.id, "2026-08", 5.0)
|
||
|
||
def test_returns_200_and_mappings(self, client: TestClient, db: Session):
|
||
self._setup(db)
|
||
token = get_token_for_user(client)
|
||
resp = client.get(f"{self.BASE}?period=2026-08", headers=auth_header(token))
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["budget_version"] == "v1.0"
|
||
|
||
lines = {}
|
||
for sec in data["sections"]:
|
||
for ln in sec["lines"]:
|
||
lines[ln["name"]] = ln
|
||
|
||
# 货币资金 → F_OP_CFLOW(有预算)
|
||
assert lines["货币资金"]["has_budget"] is True
|
||
assert lines["货币资金"]["mapped_kpi_code"] == "F_OP_CFLOW"
|
||
assert lines["货币资金"]["budget_value"] == 20.0
|
||
# 应收账款 → F_AR_DAYS(比率型,单独标注,不计算金额差异)
|
||
assert lines["应收账款"]["has_budget"] is True
|
||
assert lines["应收账款"]["ratio_kpi"] is True
|
||
assert lines["应收账款"]["mapped_kpi_code"] == "F_AR_DAYS"
|
||
assert lines["应收账款"]["deviation_amount"] is None
|
||
assert lines["应收账款"]["note"] is not None
|
||
# 无映射行(存货 1405)显式无预算
|
||
assert lines["存货"]["has_budget"] is False
|
||
assert lines["存货"]["budget_source"] == "none"
|
||
|
||
|
||
class TestProformaCashFlow:
|
||
BASE = "/api/cma/reports/proforma/cash-flow"
|
||
|
||
def _setup(self, db: Session):
|
||
create_test_user(db)
|
||
rev = _create_kpi(db, "F_REVENUE", "营业收入", frequency="quarterly")
|
||
op = _create_kpi(db, "F_OP_CFLOW", "经营性现金流")
|
||
_create_budget(db, rev.id, "2026-08", 150.0)
|
||
_create_budget(db, op.id, "2026-08", 20.0)
|
||
_create_actual(db, rev.id, "2026-08", 123.45)
|
||
|
||
def test_returns_200_and_mappings(self, client: TestClient, db: Session):
|
||
self._setup(db)
|
||
token = get_token_for_user(client)
|
||
resp = client.get(f"{self.BASE}?period=2026-08", headers=auth_header(token))
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["budget_version"] == "v1.0"
|
||
|
||
lines = {}
|
||
sections = {s["key"]: s for s in data["sections"]}
|
||
for sc in data["sections"]:
|
||
for ln in sc["lines"]:
|
||
lines[ln["code"]] = ln
|
||
|
||
# CF01 → F_REVENUE(有预算)
|
||
assert lines["CF01"]["has_budget"] is True
|
||
assert lines["CF01"]["mapped_kpi_code"] == "F_REVENUE"
|
||
assert lines["CF01"]["budget_value"] == 150.0
|
||
# CF02 无映射 → 显式无预算
|
||
assert lines["CF02"]["has_budget"] is False
|
||
assert lines["CF02"]["budget_source"] == "none"
|
||
# 经营净额 → F_OP_CFLOW 预算
|
||
assert sections["operating"]["net_budget"] == 20.0
|
||
assert sections["operating"]["has_budget"] is True
|