feat(profitability): real ROI (op profit / invested capital), enable EVA, add RI
ci/woodpecker/push/main Pipeline failed
ci/woodpecker/push/main Pipeline failed
Kaliber: wecom-finance 2026-10-02 ruling (Boss 11:59 CST "li; but (1) add real ROI"). - kpi_definitions seed scripts/sql/profitability_kpi_defs_20261003.sql: F_ROI renamed to F_ROA (same kpi_id=20, all 8 historical values kept), new F_ROI (real IMA caliber = operating profit / invested capital), F_EVA activated with unit and thresholds aligned to 万元, new F_RI (residual income). - parameters are NOT hardcoded in formula text: they live in kpi_definitions.data_source_config.params (tax_rate / wacc / min_return_rate / include_factory_subsidy / include_channel_subsidy / include_sales_discount / annualize / capital_basis); single reader = app/utils/profitability_params.py. - deriver scripts/kpi_profitability_deriver.py: YTD operating profit from voucher_details (carry_forward=0 net), invested capital from subject_balances level-1 rows only, idempotent upsert into kpi_values (source_type=derived, no data_status downgrade, fail-closed when invested capital is 0). - read-only endpoint GET /reports/value-management reads the derived rows only (single source of truth, no recomputation) and exposes the parameter block. - DupontAnalysis.vue: value management section (ROI / ROA / ROE / EVA / RI). - caliber_units.py: RATIO_CALIBER_TABLE key F_ROI renamed to F_ROA, new F_ROI row. - tests: backend/tests/test_profitability_indicators.py (21 cases).
This commit is contained in:
@@ -21,6 +21,7 @@ from app.auth_middleware import require_role, require_auth
|
||||
from app.models import KPIDefinition, KPIValue, BudgetPlan, StrategicMap, KPIAlert, User, Subject, ActionPlan, OperationLog, ReportHistory
|
||||
from app.utils.deviation_engine import calc_period_deviation, calc_period_diff, calc_deviation, get_budget_for_kpi
|
||||
from app.utils.alert_entity_scope import month_period_clause
|
||||
from app.utils.profitability_params import load_profitability_params
|
||||
from app.utils.voucher_guard import apply_voucher_guard
|
||||
from app.services import report_reclass
|
||||
from app.deps import get_entity_id, assert_entity_slug_scope, resolve_session_slug
|
||||
@@ -2333,6 +2334,88 @@ def get_dupont_analysis(
|
||||
return {"error": "不支持的实体"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 价值管理区 — 真 ROI / ROA / EVA / RI(只读)
|
||||
# 口径: 财务Bot 2026-10-02 裁定(任总 11:59 CST DM「立;但①要补真ROI」)
|
||||
# ============================================================
|
||||
|
||||
@router.get("/value-management")
|
||||
def get_value_management(
|
||||
period: Optional[str] = Query(None, description="期间 YYYY-MM;缺省取该账套最新实测月"),
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""价值管理区 — 真 ROI / 总资产报酬率(ROA) / EVA / 剩余收益(RI)。
|
||||
|
||||
单一事实源:**只读 `kpi_values` 里 `source_type='derived'` 的派生行**(由
|
||||
`scripts/kpi_profitability_deriver.py` 落库),本端点**不复算**——避免「同一指标两份结果」。
|
||||
参数由 `app/utils/profitability_params.py` 从 `kpi_definitions.data_source_config.params` 读取。
|
||||
口径要点:YTD 累计(非年化);投入资本 = 有息负债(2001+2501) + 所有者权益(3xxx 一级科目);
|
||||
厂补 530103 / 渠补 571103 计入经营(开关可回旧口径)。
|
||||
"""
|
||||
codes = ("F_ROI", "F_ROA", "F_EVA", "F_RI", "F_ROE")
|
||||
defs = {d.kpi_code: d for d in db.query(KPIDefinition).filter(
|
||||
KPIDefinition.entity_id == entity_id,
|
||||
KPIDefinition.kpi_code.in_(codes),
|
||||
).all()}
|
||||
ids = [d.id for d in defs.values()]
|
||||
|
||||
if period is None and ids:
|
||||
latest = db.query(KPIValue.period).filter(
|
||||
KPIValue.kpi_id.in_(ids),
|
||||
KPIValue.actual_value.isnot(None),
|
||||
month_period_clause(KPIValue.period),
|
||||
).order_by(KPIValue.period.desc()).first()
|
||||
period = latest[0] if latest else None
|
||||
|
||||
rows = []
|
||||
if ids and period:
|
||||
rows = db.query(KPIValue).filter(
|
||||
KPIValue.kpi_id.in_(ids),
|
||||
KPIValue.period == period,
|
||||
KPIValue.actual_value.isnot(None),
|
||||
).order_by(KPIValue.id.desc()).all()
|
||||
|
||||
metric = {}
|
||||
for row in rows: # id desc ⇒ 每个 kpi 取最新一条(同指标多来源时显式暴露,不去重掩盖)
|
||||
kpi = next((d for d in defs.values() if d.id == row.kpi_id), None)
|
||||
if kpi is None or kpi.kpi_code in metric:
|
||||
continue
|
||||
metric[kpi.kpi_code] = {
|
||||
"value": row.actual_value,
|
||||
"unit": kpi.unit,
|
||||
"kpi_name": kpi.kpi_name,
|
||||
"source_type": row.source_type,
|
||||
"data_status": row.data_status,
|
||||
"source_batch": row.source_batch,
|
||||
"remark": row.remark,
|
||||
"formula_desc": kpi.formula_desc,
|
||||
}
|
||||
|
||||
params = load_profitability_params(db, entity_id)
|
||||
return {
|
||||
"entity_id": entity_id,
|
||||
"period": period,
|
||||
"roa": metric.get("F_ROA"),
|
||||
"roi": metric.get("F_ROI"),
|
||||
"eva": metric.get("F_EVA"),
|
||||
"ri": metric.get("F_RI"),
|
||||
"roe": metric.get("F_ROE"),
|
||||
"params": {
|
||||
"tax_rate": params["tax_rate"],
|
||||
"wacc": params["wacc"],
|
||||
"min_return_rate": params["min_return_rate"],
|
||||
"include_factory_subsidy": params["include_factory_subsidy"],
|
||||
"include_channel_subsidy": params["include_channel_subsidy"],
|
||||
"include_sales_discount": params["include_sales_discount"],
|
||||
"annualize": params["annualize"],
|
||||
"capital_basis": params["capital_basis"],
|
||||
},
|
||||
"readonly": True,
|
||||
"data_source": "derived",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 自动报告生成 — ChatBI优化P1
|
||||
# 支持周报/月报/专项报告,定时/事件/手动触发
|
||||
|
||||
Reference in New Issue
Block a user