feat(dashboard): 经营看板「本量利·保本点」卡片 + 只读BEP概览端点 (cma-bep-kpi Phase2)

- 新增 GET /api/cma/dashboard/bep-card:账面/现金双口径只读数据
  (5001月销113.6/5602管理费26.1/折旧560213 7.51/渠补率84.61%)
  账面: 保本169.9万 覆盖率66.9%(黄) 缺口33.1%; 现金: 保本121.0万 覆盖率93.9%
- 新增 components/BEPBreakEvenCard.vue:主指标覆盖率红黄绿圆点+副指标
  并排+下钻面板(公式/输入KPI/双口径切换/解读),A区只读
- Dashboard.vue CEO/财务视图顶部引入卡片
This commit is contained in:
Hermes CI Fix
2026-09-08 13:58:01 +08:00
parent 1a5356a2d1
commit 9eb38cfa89
3 changed files with 357 additions and 1 deletions
+156 -1
View File
@@ -7,7 +7,7 @@ from typing import Optional
from app.database import get_db
from app.auth_middleware import require_auth, require_role
from app.deps import get_entity_id
from app.models import KPIDefinition, KPIValue, KPIAlert, User
from app.models import KPIDefinition, KPIValue, KPIAlert, User, VoucherDetail
from app.utils.cache import get as cache_get, set as cache_set
import json
import logging
@@ -18,6 +18,161 @@ router = APIRouter(prefix="/api/cma/dashboard", tags=["驾驶舱"],
dependencies=[Depends(require_role("ceo", "finance", "business", "it"))],
)
# ── 本量利·保本点 卡片只读数据(cma-bep-kpi Phase2 2026-09-08)──
# 口径与 backend/scripts/kpi_bep_deriver.py 完全一致:
# 月销 = voucher_details subject 5001(主营业务收入) 贷方 YTD 月均(万元)
# 月管理费 = 5602% 借方 YTD 月均(账面口径含折旧)
# 折旧 = 560213 借方 YTD 月均
# 渠补率 = 厂家政策 84.61%(财务Bot权威口径)
# 保本额 = 月固定成本 ÷ (1-渠补率)
# 账面口径(含折旧) / 现金口径(剔除折旧) 双口径只读返回,无写操作。
BEP_CODES = ("F_BEP_REVENUE", "F_BEP_COVER", "F_BEP_GAP")
BEP_SALES_SUBJECT = "5001"
BEP_ADMIN_PREFIX = "5602"
BEP_DEP_SUBJECT = "560213"
BEP_REBATE_POLICY_PCT = 84.61
def _bep_sum_field(db, entity_id: int, period_from: str, period_to: str,
field: str, subject_filter=None) -> float:
"""聚合 voucher_details 金额(元),subject_filter: ('eq', code) / ('like', prefix)"""
q = db.query(func.sum(getattr(VoucherDetail, field))).filter(
VoucherDetail.entity_id == entity_id,
VoucherDetail.carry_forward == 0,
VoucherDetail.period >= period_from,
VoucherDetail.period <= period_to,
)
if subject_filter:
kind, code = subject_filter
if kind == "eq":
q = q.filter(VoucherDetail.subject_code == code)
elif kind == "like":
q = q.filter(VoucherDetail.subject_code.like(code + "%"))
val = q.scalar()
return float(val or 0.0)
def _bep_level(value, kpi) -> str:
"""按 KPI 静态阈值文本判定级别(绿→黄→红,命中即止),无阈值返回 none。
与 alert_generator / kpis._static_threshold_level 语义一致。"""
import re as _re
for level, field in (("green", "threshold_green"), ("yellow", "threshold_yellow"),
("red", "threshold_red")):
expr = getattr(kpi, field, None)
if not expr:
continue
m = _re.match(r"(>=|<=|>|<|=|!=)\s*([\d.]+)", str(expr).strip())
if not m:
continue
op, val = m.group(1), float(m.group(2))
hit = {
">=": value >= val, "<=": value <= val,
">": value > val, "<": value < val,
"=": value == val, "!=": value != val,
}[op]
if hit:
return level
return "none"
@router.get("/bep-card")
def get_bep_card(
db: Session = Depends(get_db),
entity_id: int = Depends(get_entity_id),
):
"""本量利·保本点 卡片数据(只读,A区)——账面/现金双口径"""
kpis = {k.kpi_code: k for k in db.query(KPIDefinition).filter(
KPIDefinition.entity_id == entity_id,
KPIDefinition.status == "active",
KPIDefinition.kpi_code.in_(BEP_CODES),
).all()}
if len(kpis) != 3:
return {"available": False, "message": "BEP派生KPI未完整注册", "found": list(kpis.keys())}
# 最新派生期间与值(同派生器:source_type='derived')
latest = {}
for code in BEP_CODES:
v = db.query(KPIValue).filter(
KPIValue.kpi_id == kpis[code].id,
KPIValue.source_type == "derived",
KPIValue.actual_value.isnot(None),
).order_by(KPIValue.period.desc(), KPIValue.id.desc()).first()
latest[code] = {"period": v.period, "value": v.actual_value} if v else None
periods = [latest[c]["period"] for c in BEP_CODES if latest[c]]
if not periods:
return {"available": False, "message": "尚无派生值,请先运行 kpi_bep_deriver.py"}
period = max(periods)
year = period[:4]
months = int(period[5:7])
period_from = f"{year}-01"
period_to = period
# 输入口径(万元 / %)——与派生器一致
sales_avg = _bep_sum_field(db, entity_id, period_from, period_to,
"credit_amount", ("eq", BEP_SALES_SUBJECT)) / 10000.0 / months
admin_avg = _bep_sum_field(db, entity_id, period_from, period_to,
"debit_amount", ("like", BEP_ADMIN_PREFIX)) / 10000.0 / months
dep_avg = _bep_sum_field(db, entity_id, period_from, period_to,
"debit_amount", ("eq", BEP_DEP_SUBJECT)) / 10000.0 / months
rebate_pct = BEP_REBATE_POLICY_PCT
cm_pct = 100.0 - rebate_pct
# 账面口径:管理费含折旧(与派生落库值一致)
book_bep = round(admin_avg / (cm_pct / 100.0), 1)
book_cover = round(sales_avg / book_bep * 100.0, 1) if book_bep else 0.0
book_gap = round((book_bep - sales_avg) / book_bep * 100.0, 1) if book_bep else 0.0
# 现金口径:管理费-折旧
cash_admin = admin_avg - dep_avg
cash_bep = round(cash_admin / (cm_pct / 100.0), 1) if cash_admin > 0 else 0.0
cash_cover = round(sales_avg / cash_bep * 100.0, 1) if cash_bep else 0.0
cash_gap = round((cash_bep - sales_avg) / cash_bep * 100.0, 1) if cash_bep else 0.0
# 参考最新落库派生值(存在时使用,保证与 Phase1 KPI 一致)
derived = {}
for code in BEP_CODES:
lv = latest[code]
derived[code] = {"period": lv["period"], "value": lv["value"]} if lv else None
cover_kpi = kpis["F_BEP_COVER"]
book_level = _bep_level(book_cover, cover_kpi)
cash_level = _bep_level(cash_cover, cover_kpi)
gap_kpi = kpis["F_BEP_GAP"]
book_gap_level = _bep_level(book_gap, gap_kpi)
cash_gap_level = _bep_level(cash_gap, gap_kpi)
return {
"available": True,
"entity_id": entity_id,
"period": period,
"months": months,
"inputs": {
"sales_avg": round(sales_avg, 2),
"admin_avg": round(admin_avg, 2),
"dep_avg": round(dep_avg, 2),
"rebate_pct": rebate_pct,
"cm_pct": round(cm_pct, 2),
},
"book": {
"bep_revenue": book_bep,
"cover": book_cover,
"gap": book_gap,
"cover_level": book_level,
"gap_level": book_gap_level,
},
"cash": {
"admin_cash": round(cash_admin, 2),
"bep_revenue": cash_bep,
"cover": cash_cover,
"gap": cash_gap,
"cover_level": cash_level,
"gap_level": cash_gap_level,
},
"derived": derived,
"formula": "保本销售额 = 月固定成本 ÷ (1 − 渠补率84.61%)",
"readonly": True,
}
def parse_period(period_type: str, start_date: str = None, end_date: str = None):
"""解析时间区间"""
today = datetime.now()