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:
@@ -10,6 +10,41 @@
|
||||
<el-button type="primary" size="small" @click="fetchDupont" :loading="loading">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 价值管理区:真 ROI / ROA / EVA / RI(口径:财务裁定 2026-10-02,任总 11:59「立;但①要补真ROI」) -->
|
||||
<div class="value-mgmt" v-if="vm && vm.period">
|
||||
<div class="vm-title">
|
||||
<svg class="vm-icon" viewBox="0 0 24 24" width="16" height="16" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 17l6-6 4 4 8-8"/><path d="M21 7v10H11"/>
|
||||
</svg>
|
||||
<span>价值管理({{ vm.period }})</span>
|
||||
<span class="vm-sub">YTD 累计口径 · 派生值单源(source_type=derived)</span>
|
||||
</div>
|
||||
<div class="vm-cards">
|
||||
<div class="vm-card" v-for="m in vmCards" :key="m.key">
|
||||
<div class="vm-value" :class="{ neg: m.raw !== null && m.raw !== undefined && m.raw < 0 }">
|
||||
{{ m.text }}<span class="vm-unit">{{ m.unit }}</span>
|
||||
</div>
|
||||
<div class="vm-label">{{ m.label }}</div>
|
||||
<div class="vm-desc">{{ m.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vm-params" v-if="vm.params">
|
||||
<span class="vm-params-title">参数(kpi_definitions.data_source_config)</span>
|
||||
所得税率 {{ pct(vm.params.tax_rate) }} · WACC {{ pct(vm.params.wacc) }} ·
|
||||
最低报酬率 {{ pct(vm.params.min_return_rate) }} ·
|
||||
厂补{{ vm.params.include_factory_subsidy ? '计入' : '不计入' }} ·
|
||||
渠补{{ vm.params.include_channel_subsidy ? '计入' : '不计入' }} ·
|
||||
商品折扣{{ vm.params.include_sales_discount ? '计入' : '不计入' }} ·
|
||||
{{ vm.params.annualize ? '年化(×12/月数)' : 'YTD 非年化' }} ·
|
||||
投入资本{{ vm.params.capital_basis === 'average' ? '平均' : '期末' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="value-mgmt empty" v-else-if="vmLoaded && !loading">
|
||||
<div class="vm-title"><span>价值管理</span></div>
|
||||
<div class="vm-empty">ROI / ROA / EVA / RI 尚无派生值(请先运行 kpi_profitability_deriver.py)</div>
|
||||
</div>
|
||||
|
||||
<template v-if="data.roe !== null && data.roe !== undefined">
|
||||
<!-- 1. ROE大数字 -->
|
||||
<div class="roe-hero">
|
||||
@@ -111,7 +146,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { sessionEntityName } from '../utils/entity'
|
||||
// 统一走 api/index.ts 的 axios 实例(拦截器自动带 Bearer / 401 跳登录;语义同为「已剥层」)
|
||||
import api from '../api/index'
|
||||
@@ -119,6 +154,36 @@ import api from '../api/index'
|
||||
const loading = ref(false)
|
||||
const entityLabel = ref(sessionEntityName())
|
||||
const data = ref<any>({})
|
||||
// 价值管理区(真 ROI / ROA / EVA / RI)——只读 /reports/value-management(派生值单源)
|
||||
const vm = ref<any>({})
|
||||
const vmLoaded = ref(false)
|
||||
|
||||
function fmtVal(v: any): string {
|
||||
if (v === null || v === undefined) return '-'
|
||||
const n = Number(v)
|
||||
return Number.isFinite(n) ? n.toFixed(2) : '-'
|
||||
}
|
||||
|
||||
function pct(v: any): string {
|
||||
const n = Number(v)
|
||||
return Number.isFinite(n) ? (n * 100).toFixed(2).replace(/\.00$/, '') + '%' : '-'
|
||||
}
|
||||
|
||||
const vmCards = computed(() => {
|
||||
const d = vm.value || {}
|
||||
return [
|
||||
{ key: 'roi', label: d.roi?.kpi_name || '投入资本回报率(ROI)', raw: d.roi?.value,
|
||||
unit: d.roi?.unit || '%', text: fmtVal(d.roi?.value), desc: '营业利润 ÷ 投入资本(真 ROI)' },
|
||||
{ key: 'roa', label: d.roa?.kpi_name || '总资产报酬率(ROA)', raw: d.roa?.value,
|
||||
unit: d.roa?.unit || '%', text: fmtVal(d.roa?.value), desc: '净利润 ÷ 平均总资产(原 F_ROI 正名)' },
|
||||
{ key: 'roe', label: d.roe?.kpi_name || '净资产收益率(ROE)', raw: d.roe?.value,
|
||||
unit: d.roe?.unit || '%', text: fmtVal(d.roe?.value), desc: '净利润 ÷ 平均净资产' },
|
||||
{ key: 'eva', label: d.eva?.kpi_name || '经济增加值(EVA)', raw: d.eva?.value,
|
||||
unit: d.eva?.unit || '万元', text: fmtVal(d.eva?.value), desc: 'NOPAT − 投入资本 × WACC' },
|
||||
{ key: 'ri', label: d.ri?.kpi_name || '剩余收益(RI)', raw: d.ri?.value,
|
||||
unit: d.ri?.unit || '万元', text: fmtVal(d.ri?.value), desc: '营业利润 − 投入资本 × 最低报酬率' },
|
||||
].filter(m => m.raw !== null && m.raw !== undefined)
|
||||
})
|
||||
|
||||
function formatPct(val: number): string {
|
||||
if (val === null || val === undefined) return '-'
|
||||
@@ -153,7 +218,22 @@ async function fetchDupont() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchDupont)
|
||||
async function fetchValueMgmt() {
|
||||
try {
|
||||
const r: any = await api.get('/reports/value-management')
|
||||
vm.value = r.data || r
|
||||
} catch (e) {
|
||||
console.error('价值管理加载失败:', e)
|
||||
vm.value = {}
|
||||
} finally {
|
||||
vmLoaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDupont()
|
||||
fetchValueMgmt()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -305,4 +385,41 @@ onMounted(fetchDupont)
|
||||
.insight-label { font-size: 14px; font-weight: 600; color: #d48806; margin-bottom: 8px; }
|
||||
.insight-text { font-size: 14px; color: #5a4e2b; line-height: 1.7; }
|
||||
.insight-improvement { font-size: 14px; color: #2c3e50; font-weight: 600; margin-top: 8px; padding-top: 8px; border-top: 1px solid #ffe58f; }
|
||||
|
||||
/* 价值管理区(真 ROI / ROA / EVA / RI) */
|
||||
.value-mgmt {
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.value-mgmt.empty { padding: 16px 20px; }
|
||||
.vm-title { display: flex; align-items: center; gap: 8px; font-size: 16px; font-weight: 600; color: #2c3e50; }
|
||||
.vm-icon { color: #667eea; }
|
||||
.vm-sub { font-size: 12px; font-weight: 400; color: #95a5a6; margin-left: 4px; }
|
||||
.vm-empty { font-size: 13px; color: #95a5a6; margin-top: 8px; }
|
||||
.vm-cards { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 16px; }
|
||||
.vm-card {
|
||||
flex: 1 1 150px;
|
||||
min-width: 150px;
|
||||
background: #f8f9ff;
|
||||
border: 1px solid #e4e8f7;
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
.vm-value { font-size: 24px; font-weight: 700; color: #2c3e50; }
|
||||
.vm-value.neg { color: #e74c3c; }
|
||||
.vm-unit { font-size: 12px; font-weight: 400; color: #95a5a6; margin-left: 4px; }
|
||||
.vm-label { font-size: 13px; color: #5a6b7f; margin-top: 4px; }
|
||||
.vm-desc { font-size: 11px; color: #b0b0b0; margin-top: 6px; }
|
||||
.vm-params {
|
||||
margin-top: 14px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px dashed #e8e8e8;
|
||||
font-size: 12px;
|
||||
color: #7f8c8d;
|
||||
line-height: 1.9;
|
||||
}
|
||||
.vm-params-title { font-weight: 600; color: #5a6b7f; margin-right: 6px; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user