diff --git a/backend/app/api/cashflow_structure.py b/backend/app/api/cashflow_structure.py new file mode 100644 index 00000000..6525f825 --- /dev/null +++ b/backend/app/api/cashflow_structure.py @@ -0,0 +1,322 @@ +""" +现金流量表结构分析 — 管理会计OS (CMA P2 · 财务报表分析) +规范命名:现金流量表结构分析(不使用"现金流肖像"等民间说法) + +核心方法:经营(O)/投资(I)/筹资(F) 三大活动净额正负组合 → 8类企业定性分析 + O 造血能力 / I 成长代价 / F 输血管道 + 长期黄金法则:经营现金流必须为正,且应大于净利润(利润质量)。 + +数据模型:cash_flow_structure_values — 官方现金流量表三大活动净额按期间注入 + - 未注入期间:无判定(不拿演示/示例数据冒充真实类型) + - 官方净额入库后类型判定自动生效(博海2026官方表注入路径) +""" +from fastapi import APIRouter, Depends, Query, HTTPException +from pydantic import BaseModel +from sqlalchemy.orm import Session +from sqlalchemy import func +from typing import Optional, List +from datetime import datetime + +from app.database import get_db +from app.auth_middleware import require_role +from app.deps import get_entity_id +from app.models import CashFlowStructureValue + +router = APIRouter( + prefix="/api/cma/cashflow-structure", + tags=["现金流量表结构分析"], + dependencies=[Depends(require_role("ceo", "finance", "it"))], +) + + +# ============================================================ +# 八类企业定性分析规则表(O/I/F 符号组合 → 类型) +# ============================================================ +CASHFLOW_TYPE_RULES = [ + { + "type_no": 1, "o_sign": "+", "i_sign": "-", "f_sign": "-", + "name": "成熟健康型", "health_emoji": "🟢", "health_label": "最优", + "lifecycle": "成熟期", + "profile": "主业盈利,自给自足;仍在投入扩张且有余力还债/分红。最健康形态(如成熟消费龙头)。", + "risk": "注意扩张效率与分红可持续性;经营现金流应大于净利润,否则利润质量存疑。", + }, + { + "type_no": 2, "o_sign": "+", "i_sign": "+", "f_sign": "-", + "name": "成熟收割型", "health_emoji": "🟢", "health_label": "良好", + "lifecycle": "成熟期后期/集团收割期", + "profile": "主业赚钱+过去投资进入回报期(处置/收益回笼)+还在还债分红。", + "risk": "若投资净流入来自大量处置核心资产,需警惕「卖资产过日子」。", + }, + { + "type_no": 3, "o_sign": "+", "i_sign": "-", "f_sign": "+", + "name": "高速扩张型", "health_emoji": "🟡", "health_label": "看增速", + "lifecycle": "成长期", + "profile": "主业能赚钱,但扩张更快、现金不够花,需要融资支撑资本开支。成长质量取决于经营造血增速能否追上扩张。", + "risk": "扩张节奏失控 → 现金消耗加速 → 滑向类型6/7。", + }, + { + "type_no": 4, "o_sign": "+", "i_sign": "+", "f_sign": "+", + "name": "全面回笼型", "health_emoji": "🟡", "health_label": "需辨析", + "lifecycle": "重组/收缩盘点期", + "profile": "三类活动都进钱,少见。多为经营好转+资产处置+新增融资并举(可能正在重组)。", + "risk": "需具体辨析筹资来源(股权/债权)与投资流入是否靠卖资产。", + }, + { + "type_no": 5, "o_sign": "-", "i_sign": "+", "f_sign": "-", + "name": "衰退变卖型", "health_emoji": "🔴", "health_label": "高危", + "lifecycle": "衰退期/清盘前奏", + "profile": "主业亏损,靠卖资产/处置投资回笼现金,同时还在还债。濒危信号。", + "risk": "资产卖完即无以为继;除非经营反转否则难持续。", + }, + { + "type_no": 6, "o_sign": "-", "i_sign": "-", "f_sign": "+", + "name": "初创烧钱型", "health_emoji": "🟠", "health_label": "高风险", + "lifecycle": "初创期/高增长投入期", + "profile": "主业烧钱+持续投入,全靠融资吊命。常见于初创/高增长科技企业;也是庞氏结构的表面特征。", + "risk": "融资链条断裂即死;须区分「真成长」(收入/用户高速增长)与「假烧钱」。", + }, + { + "type_no": 7, "o_sign": "-", "i_sign": "+", "f_sign": "+", + "name": "输血生存型", "health_emoji": "🔴", "health_label": "高危", + "lifecycle": "挣扎期", + "profile": "经营亏损,靠卖资产+借款输血双管齐下维持。压力极大。", + "risk": "短期可活,长期必须扭转经营现金流,否则资金链断裂。", + }, + { + "type_no": 8, "o_sign": "-", "i_sign": "-", "f_sign": "-", + "name": "全面失血型", "health_emoji": "🔴", "health_label": "极危", + "lifecycle": "深度危机期", + "profile": "经营/投资/筹资三个引擎全熄火,现金只出不进。最危险状态。", + "risk": "极可能需要紧急债务重组/股东注资/断臂求生。", + }, +] + +# 类型 → 规则 快速索引 +TYPE_RULE_MAP = {r["type_no"]: r for r in CASHFLOW_TYPE_RULES} + + +def _sign(value: Optional[float]) -> Optional[str]: + """净额符号:>0 → +,<0 → -,==0 → '0'(无法落入8类组合),None → None""" + if value is None: + return None + if value > 0: + return "+" + if value < 0: + return "-" + return "0" + + +def classify_enterprise(operating_net: Optional[float], + investing_net: Optional[float], + financing_net: Optional[float]) -> dict: + """O/I/F 三净额符号 → 8类企业定性判定 + + 返回: + {classified, type_no, name, health_emoji, health_label, risk, profile, lifecycle, o_sign, i_sign, f_sign, reason} + classified=False 时 reason 说明未判定的原因(任一净额缺失或为0) + """ + o, i, f = _sign(operating_net), _sign(investing_net), _sign(financing_net) + if o is None or i is None or f is None: + missing = [name for sign, name in [(o, "经营"), (i, "投资"), (f, "筹资")] if sign is None] + return {"classified": False, "reason": "以下活动净额缺失:" + "、".join(missing) + "(需官方现金流量表数据)"} + if "0" in (o, i, f): + zero = [name for sign, name in [(o, "经营"), (i, "投资"), (f, "筹资")] if sign == "0"] + return {"classified": False, "reason": "以下活动净额为0(恰好为0无法落入8类正负组合):" + "、".join(zero) + ",请按业务实质人工判定"} + + for rule in CASHFLOW_TYPE_RULES: + if rule["o_sign"] == o and rule["i_sign"] == i and rule["f_sign"] == f: + return { + "classified": True, + "type_no": rule["type_no"], + "name": rule["name"], + "health_emoji": rule["health_emoji"], + "health_label": rule["health_label"], + "lifecycle": rule["lifecycle"], + "profile": rule["profile"], + "risk": rule["risk"], + "o_sign": o, "i_sign": i, "f_sign": f, + "reason": "经营(%s) 投资(%s) 筹资(%s) → 类型%d %s" % (o, i, f, rule["type_no"], rule["name"]), + } + return {"classified": False, "reason": "未知组合,请检查数据"} + + +class CashFlowValueIn(BaseModel): + """三大活动净额录入(官方现金流量表 / 人工)""" + period: str + operating_net: float + investing_net: float + financing_net: float + begin_cash: Optional[float] = None + end_cash: Optional[float] = None + source: str = "official" + remark: Optional[str] = None + + +def _validate_period(period: str) -> str: + """期间格式校验 YYYY-MM""" + period = (period or "").strip() + try: + datetime.strptime(period, "%Y-%m") + except Exception: + raise HTTPException(status_code=422, detail="期间格式须为 YYYY-MM,如 2026-08") + return period + + +# ============================================================ +# 接口 +# ============================================================ + +@router.get("/rules") +def get_rules(): + """八类企业定性判定规则表(O/I/F 符号组合速查)""" + return {"rules": CASHFLOW_TYPE_RULES} + + +@router.get("/periods") +def list_periods( + db: Session = Depends(get_db), + entity_id: int = Depends(get_entity_id), +): + """已注入净额的期间列表(倒序,供按期间查询)""" + rows = db.query(CashFlowStructureValue.period).filter( + CashFlowStructureValue.entity_id == entity_id + ).order_by(CashFlowStructureValue.period.desc()).all() + return {"periods": [r[0] for r in rows]} + + +@router.get("/values") +def list_values( + db: Session = Depends(get_db), + entity_id: int = Depends(get_entity_id), +): + """已注入的三大活动净额记录(按期间倒序)""" + rows = db.query(CashFlowStructureValue).filter( + CashFlowStructureValue.entity_id == entity_id + ).order_by(CashFlowStructureValue.period.desc()).all() + return {"items": [ + { + "id": r.id, "period": r.period, + "operating_net": r.operating_net, + "investing_net": r.investing_net, + "financing_net": r.financing_net, + "begin_cash": r.begin_cash, "end_cash": r.end_cash, + "source": r.source, "remark": r.remark, + "updated_at": r.updated_at.strftime("%Y-%m-%d %H:%M") if r.updated_at else None, + } for r in rows + ]} + + +@router.get("/analysis") +def get_analysis( + period: str = Query(None, description="格式 YYYY-MM,缺省取最近已注入期间"), + db: Session = Depends(get_db), + entity_id: int = Depends(get_entity_id), +): + """现金流量表结构分析 — 三净额 → 八类企业判定 + 瀑布图数据""" + if period: + period = _validate_period(period) + + # 1. 取官方/人工注入的净额(唯一权威判定数据) + q = db.query(CashFlowStructureValue).filter( + CashFlowStructureValue.entity_id == entity_id + ) + if period: + rec = q.filter(CashFlowStructureValue.period == period).first() + else: + rec = q.order_by(CashFlowStructureValue.period.desc()).first() + if rec: + period = rec.period + + if rec is None: + return { + "period": period or datetime.now().strftime("%Y-%m"), + "data_available": False, + "message": "该期间暂无官方现金流量表净额数据,请先录入三大活动净额(官方现金流量表注入后自动生效)。", + "nets": None, + "classification": None, + "waterfall": None, + } + + nets = { + "operating": {"key": "operating", "name": "经营活动", "value": rec.operating_net, + "sign": _sign(rec.operating_net), "unit": "万元"}, + "investing": {"key": "investing", "name": "投资活动", "value": rec.investing_net, + "sign": _sign(rec.investing_net), "unit": "万元"}, + "financing": {"key": "financing", "name": "筹资活动", "value": rec.financing_net, + "sign": _sign(rec.financing_net), "unit": "万元"}, + } + + classification = classify_enterprise(rec.operating_net, rec.investing_net, rec.financing_net) + + # 2. 瀑布图:期初现金 → +经营 → +投资 → +筹资 → 期末现金 + waterfall = None + if rec.begin_cash is not None or rec.end_cash is not None: + begin = rec.begin_cash + end = rec.end_cash + steps = [] + cum = begin if begin is not None else 0.0 + if begin is not None: + steps.append({"step": "期初现金", "value": begin, "type": "begin"}) + for net_key, net_name in [("operating", "经营活动"), ("investing", "投资活动"), ("financing", "筹资活动")]: + val = getattr(rec, net_key + "_net") or 0.0 + cum += val + steps.append({"step": net_name, "value": round(val, 2), "type": "flow"}) + if end is not None: + steps.append({"step": "期末现金", "value": end, "type": "end"}) + elif begin is not None: + end = round(cum, 2) + steps.append({"step": "期末现金(推算)", "value": end, "type": "end"}) + waterfall = { + "begin_cash": begin, "end_cash": end, + "net_increase": round((end - begin), 2) if (begin is not None and end is not None) else None, + "steps": steps, + } + + return { + "period": rec.period, + "data_available": True, + "source": rec.source, + "remark": rec.remark, + "nets": nets, + "classification": classification, + "waterfall": waterfall, + } + + +@router.post("/values", dependencies=[Depends(require_role("ceo", "finance"))]) +def upsert_value( + payload: CashFlowValueIn, + db: Session = Depends(get_db), + entity_id: int = Depends(get_entity_id), +): + """录入/更新某期间三大活动净额(官方现金流量表注入;同期间重复提交=覆盖更新)""" + period = _validate_period(payload.period) + if payload.source not in ("official", "manual"): + raise HTTPException(status_code=422, detail="source 仅支持 official(官方报表)/manual(人工录入)") + + rec = db.query(CashFlowStructureValue).filter( + CashFlowStructureValue.entity_id == entity_id, + CashFlowStructureValue.period == period, + ).first() + created = rec is None + if rec is None: + rec = CashFlowStructureValue(entity_id=entity_id, period=period) + db.add(rec) + + rec.operating_net = round(payload.operating_net, 2) + rec.investing_net = round(payload.investing_net, 2) + rec.financing_net = round(payload.financing_net, 2) + rec.begin_cash = round(payload.begin_cash, 2) if payload.begin_cash is not None else rec.begin_cash + rec.end_cash = round(payload.end_cash, 2) if payload.end_cash is not None else rec.end_cash + rec.source = payload.source + rec.remark = payload.remark + db.commit() + + classification = classify_enterprise(rec.operating_net, rec.investing_net, rec.financing_net) + return { + "ok": True, + "id": rec.id, + "period": rec.period, + "classification": classification, + "message": "三大活动净额已%s" % ("入库" if created else "更新"), + } diff --git a/backend/app/main.py b/backend/app/main.py index 4da84608..00b170a3 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -5,7 +5,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from dotenv import load_dotenv from app.database import init_db -from app.api import auth, kpis, kpi_governance, templates, maps, dashboard, data, alerts, ai_analysis, ai_suggestions, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, bot_bridge_v2, lead, tenant, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers, okr, okr_templates, subjects, driver_budget, bot_kpis, ontology, bot_iron_law, analysis_results, expenses, cash, tax_compliance, verify, growth_quality, products, data_classification, value_sources, zero_based, derivation_rules, cash_classify +from app.api import auth, kpis, kpi_governance, templates, maps, dashboard, data, alerts, ai_analysis, ai_suggestions, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, bot_bridge_v2, lead, tenant, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers, okr, okr_templates, subjects, driver_budget, bot_kpis, ontology, bot_iron_law, analysis_results, expenses, cash, tax_compliance, verify, growth_quality, products, data_classification, value_sources, zero_based, derivation_rules, cash_classify, cashflow_structure from app.utils.cache import clear_all as clear_cache, delete as delete_cache from scripts.erp_sync import run_sync as run_erp_sync from app.auth_middleware import require_auth @@ -59,6 +59,7 @@ app.include_router(value_sources.router) app.include_router(zero_based.router) app.include_router(derivation_rules.router) app.include_router(cash_classify.router) +app.include_router(cashflow_structure.router) app.include_router(cost.router) app.include_router(predict.router) app.include_router(growth_quality.router) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 8137fd31..ce9e8758 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -940,3 +940,24 @@ class AISuggestion(Base): apply_detail = Column(JSON, nullable=True, comment="应用结果明细: [{target_type,target_id,action,before,after}]") created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + + +class CashFlowStructureValue(Base): + """现金流量表结构分析 — 三大活动净额 (P1 2026-09-03 八类企业定性分析) + 官方现金流量表三大活动净额按期间注入,支撑 O/I/F 符号组合 → 8类企业判定 + 单位统一为 万元(与 BH_CASH_BALANCE / ledger 口径一致) + """ + __tablename__ = "cash_flow_structure_values" + id = Column(Integer, primary_key=True, index=True) + entity_id = Column(Integer, nullable=False, default=1, comment="企业ID(账套,多租户隔离)") + period = Column(String(20), nullable=False, comment="期间 YYYY-MM") + operating_net = Column(Float, nullable=False, comment="经营活动现金流量净额(万元)") + investing_net = Column(Float, nullable=False, comment="投资活动现金流量净额(万元)") + financing_net = Column(Float, nullable=False, comment="筹资活动现金流量净额(万元)") + begin_cash = Column(Float, nullable=True, comment="期初现金及现金等价物余额(万元)") + end_cash = Column(Float, nullable=True, comment="期末现金及现金等价物余额(万元)") + source = Column(String(50), default="official", comment="official官方报表/manual人工录入") + remark = Column(String(500), nullable=True, comment="备注/口径说明") + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + __table_args__ = (UniqueConstraint("entity_id", "period", name="uq_cf_struct_entity_period"),) diff --git a/backend/tests/test_cashflow_structure.py b/backend/tests/test_cashflow_structure.py new file mode 100644 index 00000000..8f9caaac --- /dev/null +++ b/backend/tests/test_cashflow_structure.py @@ -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 diff --git a/frontend/src/permission.ts b/frontend/src/permission.ts index b28545b1..05c6becb 100644 --- a/frontend/src/permission.ts +++ b/frontend/src/permission.ts @@ -9,10 +9,10 @@ interface MenuItem { // ── 角色路由映射 ── export const ROLE_ROUTES: Record = { - ceo: ['/my-dashboard', '/dashboard', '/ai-suggestions', '/kpis', '/maps', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/notifications', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/cost-intelligence', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/analysis-confidence', '/expenses', '/cash-plan', '/receivables', '/growth-quality', '/product-matrix', '/tax-compliance', '/data-classification'], - finance: ['/my-dashboard', '/dashboard', '/ai-suggestions', '/kpis', '/maps', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/data', '/budget', '/deviations', '/cost', '/cost-intelligence', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/analysis-confidence', '/expenses', '/cash-plan', '/receivables', '/growth-quality', '/product-matrix', '/tax-compliance', '/data-classification'], + ceo: ['/my-dashboard', '/dashboard', '/ai-suggestions', '/kpis', '/maps', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/notifications', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/cost-intelligence', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/analysis-confidence', '/expenses', '/cash-plan', '/receivables', '/growth-quality', '/product-matrix', '/tax-compliance', '/data-classification', '/cashflow-structure'], + finance: ['/my-dashboard', '/dashboard', '/ai-suggestions', '/kpis', '/maps', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/data', '/budget', '/deviations', '/cost', '/cost-intelligence', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/analysis-confidence', '/expenses', '/cash-plan', '/receivables', '/growth-quality', '/product-matrix', '/tax-compliance', '/data-classification', '/cashflow-structure'], business: ['/my-dashboard', '/dashboard', '/ai-suggestions', '/kpis', '/alerts', '/budget', '/deviations', '/action-plans', '/knowledge', '/guide', '/customer', '/expenses', '/cash-plan', '/growth-quality', '/product-matrix', '/tax-compliance', '/data-classification'], - it: ['/my-dashboard', '/dashboard', '/ai-suggestions', '/kpis', '/alerts', '/data', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/cost-intelligence', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/analysis-confidence', '/expenses', '/cash-plan', '/product-matrix', '/tax-compliance', '/data-classification'], + it: ['/my-dashboard', '/dashboard', '/ai-suggestions', '/kpis', '/alerts', '/data', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/cost-intelligence', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/analysis-confidence', '/expenses', '/cash-plan', '/product-matrix', '/tax-compliance', '/data-classification', '/cashflow-structure'], } export const ROLE_ACTIONS: Record = { @@ -45,6 +45,8 @@ export const MENU_ITEMS: MenuItem[] = [ { path: '/ai-suggestions', label: 'AI建议中心', icon: 'Opportunity', roles: ['ceo', 'finance', 'business', 'it'], group: '🟡 C 监控与评价' }, { path: '/reports', label: '管理报表', icon: 'DataAnalysis', roles: ['ceo', 'finance', 'business'], group: '🟡 C 监控与评价' }, { path: '/dupont-analysis', label: '杜邦分析', icon: 'TrendCharts', roles: ['ceo', 'finance', 'it'], group: '🟡 C 监控与评价' }, + // ── GROUP: 财务报表分析 ── + { path: '/cashflow-structure', label: '现金流量表分析', icon: 'DataAnalysis', roles: ['ceo', 'finance', 'it'], group: '📊 财务报表分析' }, { path: '/customer', label: '客户维度', icon: 'User', roles: ['ceo', 'finance', 'business', 'it'], group: '🟡 C 监控与评价' }, { path: '/learning-dashboard', label: '学习成长看板', icon: 'Reading', roles: ['ceo', 'finance', 'it'], group: '🟡 C 监控与评价' }, { path: '/alerts', label: '预警中心', icon: 'WarningFilled', roles: ['ceo', 'finance', 'business', 'it'], group: '🟡 C 监控与评价' }, diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 15d2834e..3523a8a3 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -35,6 +35,7 @@ const routes = [ { path: 'reports', name: 'ReportCenter', component: () => import('@/views/ReportCenter.vue'), meta: { title: '管理报表', roles: ['ceo', 'finance', 'business'] } }, { path: 'alignment', name: 'KPIAlignment', component: () => import('@/views/KPIAlignment.vue'), meta: { title: '战略执行看板', roles: ['ceo', 'finance', 'business', 'it'], editable: true } }, { path: 'dupont-analysis', name: 'DupontAnalysis', component: () => import('@/views/DupontAnalysis.vue'), meta: { title: '杜邦分析', roles: ['ceo', 'finance', 'it'] } }, + { path: 'cashflow-structure', name: 'CashFlowStructure', component: () => import('@/views/CashFlowStructure.vue'), meta: { title: '现金流量表结构分析', roles: ['ceo', 'finance', 'it'] } }, { path: 'data-quality', name: 'DataQuality', component: () => import('@/views/DataQuality.vue'), meta: { title: '数据质量', roles: ['ceo', 'finance', 'it'] } }, { path: 'bi-reports', name: 'BiReports', component: () => import('@/views/BIReportCenter.vue'), meta: { title: 'BI报表', roles: ['ceo', 'finance', 'business'] } }, { path: 'okr-templates', name: 'OKRTemplates', component: () => import('@/views/OKRTemplates.vue'), meta: { title: 'OKR模板库', roles: ['ceo', 'finance', 'it'], editable: true } }, diff --git a/frontend/src/views/CashFlowStructure.vue b/frontend/src/views/CashFlowStructure.vue new file mode 100644 index 00000000..deadfaaa --- /dev/null +++ b/frontend/src/views/CashFlowStructure.vue @@ -0,0 +1,576 @@ + + + + +