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).
95 lines
4.4 KiB
Python
95 lines
4.4 KiB
Python
# -*- 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
|