Files
Hermes CI Fix 8631ce718f
ci/woodpecker/push/main Pipeline failed
feat(profitability): real ROI (op profit / invested capital), enable EVA, add RI
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).
2026-10-03 02:10:47 +08:00

95 lines
4.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""盈利能力三指标(ROI / EVA / RI)参数单一事实源 —— 口径裁定 2026-10-02
依据(唯一有效出处):财务Bot 2026-10-02 裁定(任总 11:59 CST DM「立;但①要补真ROI」)
任务件 `finance-20261002-profitability-indicators-roi-eva-ri-fullstack.md`
口径(万元):
· 真 ROI(IMA/CMA 口径) = 营业利润 ÷ 投入资本
投入资本 = 有息负债(2001 短期借款 + 2501 长期借款)+ 所有者权益(3xxx 一级科目净额)
· EVA = NOPAT − 投入资本 × WACC ,NOPAT = 经营口径营业利润 × (1 − 所得税率)
· RI = 营业利润 − 投入资本 × 最低报酬率
· 经营口径营业利润 = (5001 − 5401 − 5403 − 5601 − 5602 − 5603) + 厂补(530103) − 渠补(571103)
厂补/渠补**计入经营**(参数开关可回旧口径);571112 商品折扣(销售) 默认**不计入**
参数化纪律(任务单 §二-1「勿硬编码进公式文本」):
所有参数存放于 `kpi_definitions.data_source_config` 的 `params` 子对象,**单一事实源**:
· `F_ROI.data_source_config.params` —— 利润口径开关(三条指标共用)
· `F_EVA.data_source_config.params` —— tax_rate / wacc
· `F_RI.data_source_config.params` —— min_return_rate
本模块是**唯一读取入口**(派生器与只读端点共用),读不到即回落 DEFAULTS。
改参数 = 改 DB 的 JSON 后重跑派生器(无需改代码),见 `scripts/kpi_profitability_deriver.py`。
"""
from __future__ import annotations
import json
from typing import Any, Dict
from app.models import KPIDefinition
#: 三条指标的 kpi_code(与种子 SQL `scripts/sql/profitability_kpi_defs_20261003.sql` 一致)
ROI_KPI = "F_ROI"
EVA_KPI = "F_EVA"
RI_KPI = "F_RI"
PROFITABILITY_KPIS = (ROI_KPI, EVA_KPI, RI_KPI)
#: 出厂默认参数(DB 参数缺失/未解析时的兜底;与裁定 §一 一致)
DEFAULTS: Dict[str, Any] = {
# —— 利润口径开关(三条共用,落 F_ROI.params)——
"include_factory_subsidy": True, # 厂补 530103 计入经营
"include_channel_subsidy": True, # 渠补 571103 计入经营
"include_sales_discount": False, # 571112 商品折扣(销售) 默认不计入(未决开关,任总点头后置 true)
# —— 资本成本参数 ——
"tax_rate": 0.25, # 所得税率(落 F_EVA.params)
"wacc": 0.08, # 加权平均资本成本(落 F_EVA.params)
"min_return_rate": 0.08, # RI 最低报酬率(落 F_RI.params,默认同 WACC)
# —— 口径开关 ——
"annualize": False, # 是否年化(默认 False = YTD 累计口径,公式/备注已声明)
"capital_basis": "ending", # 投入资本口径:ending=期末 / average=平均((期初+期末)/2)
}
def parse_params(raw: Any) -> Dict[str, Any]:
"""把 `data_source_config`(dict / JSON 字符串 / None)解析成 params 子对象。
兼容形态:`{"params": {...}}`(本单约定) / 裸 `{...}`(历史行无 params 包装) / None。
"""
if raw is None:
return {}
if isinstance(raw, (bytes, bytearray)):
raw = raw.decode("utf-8", "replace")
if isinstance(raw, str):
raw = raw.strip()
if not raw:
return {}
try:
raw = json.loads(raw)
except (ValueError, TypeError):
return {}
if not isinstance(raw, dict):
return {}
params = raw.get("params", raw)
if not isinstance(params, dict):
return {}
return {k: v for k, v in params.items() if v is not None}
def load_profitability_params(db, entity_id: int) -> Dict[str, Any]:
"""读取并合并三条指标行的参数(F_ROI ← F_EVA ← F_RI 逐层覆盖 DEFAULTS)。
读不到定义/参数 ⇒ 对应层不覆盖(DEFAULTS 兜底)。返回的 dict 恒含 DEFAULTS 的全部键。
"""
out = dict(DEFAULTS)
rows = db.query(KPIDefinition.kpi_code, KPIDefinition.data_source_config).filter(
KPIDefinition.entity_id == entity_id,
KPIDefinition.kpi_code.in_(list(PROFITABILITY_KPIS)),
).all()
by_code = {}
for code, cfg in rows:
by_code[code] = parse_params(cfg)
for code in PROFITABILITY_KPIS:
for k, v in (by_code.get(code) or {}).items():
if k in out:
out[k] = v
return out