""" 预编报表(预算版三张报表)测试 — 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