fix(kpi): BEP保本KPI阈值按>=85口径修正 + 新增现金口径派生KPI(剔除折旧560213) + bep-card现金读派生单一事实源 (fin-fix-bep-threshold-20260908)

This commit is contained in:
Hermes CI Fix
2026-09-08 15:57:47 +08:00
parent cf81ee0ccd
commit 5bcc65b47f
3 changed files with 78 additions and 19 deletions
+25 -10
View File
@@ -26,7 +26,10 @@ router = APIRouter(prefix="/api/cma/dashboard", tags=["驾驶舱"],
# 渠补率 = 厂家政策 84.61%(财务Bot权威口径)
# 保本额 = 月固定成本 ÷ (1-渠补率)
# 账面口径(含折旧) / 现金口径(剔除折旧) 双口径只读返回,无写操作。
BEP_CODES = ("F_BEP_REVENUE", "F_BEP_COVER", "F_BEP_GAP")
# 现金口径数值优先读派生KPI(F_BEP_REVENUE_CASH/F_BEP_COVER_CASH)最新落库值,无派生值时回退现算。
BEP_CODES = ("F_BEP_REVENUE", "F_BEP_COVER", "F_BEP_GAP",
"F_BEP_REVENUE_CASH", "F_BEP_COVER_CASH")
BEP_BOOK_CODES = ("F_BEP_REVENUE", "F_BEP_COVER", "F_BEP_GAP") # 账面3条必须齐全才 available
BEP_SALES_SUBJECT = "5001"
BEP_ADMIN_PREFIX = "5602"
BEP_DEP_SUBJECT = "560213"
@@ -86,12 +89,17 @@ def get_bep_card(
KPIDefinition.status == "active",
KPIDefinition.kpi_code.in_(BEP_CODES),
).all()}
if len(kpis) != 3:
return {"available": False, "message": "BEP派生KPI未完整注册", "found": list(kpis.keys())}
missing_book = [c for c in BEP_BOOK_CODES if c not in kpis]
if missing_book:
return {"available": False, "message": "BEP账面口径派生KPI未完整注册",
"found": list(kpis.keys()), "missing": missing_book}
# 最新派生期间与值(同派生器:source_type='derived')
# 最新派生期间与值(同派生器:source_type='derived');现金2条缺失不阻塞整卡
latest = {}
for code in BEP_CODES:
if code not in kpis:
latest[code] = None
continue
v = db.query(KPIValue).filter(
KPIValue.kpi_id == kpis[code].id,
KPIValue.source_type == "derived",
@@ -99,7 +107,7 @@ def get_bep_card(
).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]]
periods = [latest[c]["period"] for c in BEP_BOOK_CODES if latest[c]]
if not periods:
return {"available": False, "message": "尚无派生值,请先运行 kpi_bep_deriver.py"}
period = max(periods)
@@ -122,13 +130,19 @@ def get_bep_card(
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
# 现金口径:管理费-折旧
# 现金口径:管理费-折旧;优先读派生KPI最新落库值(单一事实源),缺失时回退现算
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_derived_bep = latest["F_BEP_REVENUE_CASH"]["value"] if latest.get("F_BEP_REVENUE_CASH") else None
cash_derived_cover = latest["F_BEP_COVER_CASH"]["value"] if latest.get("F_BEP_COVER_CASH") else None
if cash_derived_bep is not None and cash_derived_cover is not None:
cash_bep = cash_derived_bep
cash_cover = cash_derived_cover
else:
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 一致)
# 回显最新落库派生值(账面3条+现金2条,存在时输出)
derived = {}
for code in BEP_CODES:
lv = latest[code]
@@ -136,7 +150,8 @@ def get_bep_card(
cover_kpi = kpis["F_BEP_COVER"]
book_level = _bep_level(book_cover, cover_kpi)
cash_level = _bep_level(cash_cover, cover_kpi)
cash_cover_kpi = kpis.get("F_BEP_COVER_CASH") or cover_kpi
cash_level = _bep_level(cash_cover, cash_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)