feat(cashflow): 现金流量表结构分析——三大活动净额八类企业定性判定 (P1)
- 后端: cashflow_structure.py 模块 (GET rules/periods/values/analysis + POST values upsert) - 判定: O/I/F 三净额符号 → 8类企业定性 (CMA P2 现金流量表分析, 无'现金流肖像'字样) - 数据: cash_flow_structure_values 表 (entity_id+period 唯一), 官方现金流量表净额按期间注入 - 前端: CashFlowStructure.vue 判定横幅/瀑布图/规则表/入库记录 + MyDialog 录入弹窗 - 菜单: 财务报表分析 → 现金流量表分析 (路由 /cashflow-structure, 角色 ceo/finance/it) - 测试: tests/test_cashflow_structure.py 7 条通过 (规则8类/判定/覆盖/博海边界6-7/非法期间)
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
"""现金流量表结构分析模块测试 — 八类企业定性判定 (P1 2026-09-03)
|
||||
|
||||
覆盖 cashflow_structure.py:
|
||||
GET /api/cma/cashflow-structure/rules 八类规则表
|
||||
GET /api/cma/cashflow-structure/periods 已注入期间
|
||||
GET /api/cma/cashflow-structure/analysis 结构分析+类型判定+瀑布数据
|
||||
POST /api/cma/cashflow-structure/values 三大活动净额注入(upsert)
|
||||
"""
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from tests.conftest import create_test_user, get_token_for_user, auth_header
|
||||
|
||||
BASE = "/api/cma/cashflow-structure"
|
||||
|
||||
|
||||
class TestRules:
|
||||
def test_rules_8_types(self, client: TestClient, db: Session):
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
resp = client.get(f"{BASE}/rules", headers=auth_header(token))
|
||||
assert resp.status_code == 200
|
||||
rules = resp.json()["rules"]
|
||||
assert len(rules) == 8
|
||||
# 判定组合必须恰好 8 种且无重复
|
||||
combos = [(r["o_sign"], r["i_sign"], r["f_sign"]) for r in rules]
|
||||
assert len(set(combos)) == 8
|
||||
# 规范命名:不含民间叫法
|
||||
assert all("现金流肖像" not in r["name"] for r in rules)
|
||||
|
||||
def test_rules_unauthorized(self, client: TestClient):
|
||||
resp = client.get(f"{BASE}/rules")
|
||||
assert resp.status_code in (401, 403)
|
||||
|
||||
|
||||
class TestStructureAnalysis:
|
||||
def _post_value(self, client, token, **kw):
|
||||
payload = {
|
||||
"period": "2026-08",
|
||||
"operating_net": -10.0,
|
||||
"investing_net": 3.0,
|
||||
"financing_net": 12.0, # - + + → 类型7 输血生存型
|
||||
"source": "official",
|
||||
"remark": "pytest 临时数据",
|
||||
}
|
||||
payload.update(kw)
|
||||
return client.post(f"{BASE}/values", headers=auth_header(token), json=payload)
|
||||
|
||||
def test_empty_analysis(self, client: TestClient, db: Session):
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
resp = client.get(f"{BASE}/analysis", headers=auth_header(token))
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["data_available"] is False
|
||||
assert body["classification"] is None
|
||||
|
||||
def test_upsert_and_classify_type7(self, client: TestClient, db: Session):
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
resp = self._post_value(client, token)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["ok"] is True
|
||||
assert body["classification"]["classified"] is True
|
||||
assert body["classification"]["type_no"] == 7
|
||||
assert body["classification"]["name"] == "输血生存型"
|
||||
|
||||
# 分析接口自动命中
|
||||
r2 = client.get(f"{BASE}/analysis", headers=auth_header(token), params={"period": "2026-08"})
|
||||
assert r2.status_code == 200
|
||||
a = r2.json()
|
||||
assert a["data_available"] is True
|
||||
assert a["classification"]["type_no"] == 7
|
||||
assert a["nets"]["operating"]["sign"] == "-"
|
||||
assert a["nets"]["investing"]["sign"] == "+"
|
||||
assert a["nets"]["financing"]["sign"] == "+"
|
||||
|
||||
# periods / values 可查
|
||||
r3 = client.get(f"{BASE}/periods", headers=auth_header(token))
|
||||
assert "2026-08" in r3.json()["periods"]
|
||||
|
||||
def test_upsert_overwrite_same_period(self, client: TestClient, db: Session):
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
self._post_value(client, token)
|
||||
resp2 = self._post_value(client, token, operating_net=50.0, investing_net=-20.0, financing_net=-10.0)
|
||||
assert resp2.status_code == 200
|
||||
assert resp2.json()["classification"]["type_no"] == 1 # + - - → 成熟健康型
|
||||
rows = client.get(f"{BASE}/values", headers=auth_header(token)).json()["items"]
|
||||
assert len(rows) == 1 # 同期间覆盖不重复插入
|
||||
|
||||
def test_type6_7_boundary_like_bohai(self, client: TestClient, db: Session):
|
||||
"""博海结构判定回归:经营弱/负 + 筹资依赖 → 类型6(- - +) / 类型7(- + +)"""
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
resp = self._post_value(client, token, operating_net=-5.0, investing_net=-2.0, financing_net=9.0)
|
||||
assert resp.json()["classification"]["type_no"] == 6
|
||||
resp = self._post_value(client, token, operating_net=-5.0, investing_net=2.0, financing_net=9.0)
|
||||
assert resp.json()["classification"]["type_no"] == 7
|
||||
|
||||
def test_invalid_period(self, client: TestClient, db: Session):
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
resp = self._post_value(client, token, period="2026/08")
|
||||
assert resp.status_code == 422
|
||||
Reference in New Issue
Block a user