diff --git a/backend/app/api/dashboard.py b/backend/app/api/dashboard.py index eb8b2996..2674a906 100644 --- a/backend/app/api/dashboard.py +++ b/backend/app/api/dashboard.py @@ -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) diff --git a/backend/scripts/kpi_bep_deriver.py b/backend/scripts/kpi_bep_deriver.py index 6b5c15d1..57a73218 100644 --- a/backend/scripts/kpi_bep_deriver.py +++ b/backend/scripts/kpi_bep_deriver.py @@ -1,13 +1,16 @@ """保本点(盈亏平衡)派生KPI执行器 — 管理会计OS (cma-bep-kpi 2026-09-08) -按财务Bot权威口径(陕西酣客_保本点分析_2026年1-7月.md)自动计算并落库三个派生KPI: - F_BEP_REVENUE 保本销售额(万元) = 月均管理费 ÷ 边际贡献率 - F_BEP_COVER 保本覆盖率(%) = 月均销售 ÷ 保本销售额 × 100 (正向: >=100绿 >=85黄 <85红) - F_BEP_GAP 保本缺口(%) = (保本销售额-月均销售) ÷ 保本销售额 × 100 (反向: <=0绿 <=15黄 >15红) +按财务Bot权威口径(陕西酣客_保本点分析_2026年1-7月.md)自动计算并落库五个派生KPI: + F_BEP_REVENUE 保本销售额(万元) = 月均管理费 ÷ 边际贡献率 (账面口径,含折旧) + F_BEP_COVER 保本覆盖率(%) = 月均销售 ÷ 保本销售额 × 100 (正向: >=100绿 >=85黄 <85红) + F_BEP_GAP 保本缺口(%) = (保本销售额-月均销售) ÷ 保本销售额 × 100 (反向: <=0绿 <=15黄 >15红) + F_BEP_REVENUE_CASH 现金保本销售额(万元) = (月均管理费-月均折旧) ÷ 边际贡献率 (现金口径,剔除折旧560213,中性无阈值) + F_BEP_COVER_CASH 现金保本覆盖率(%) = 月均销售 ÷ 现金保本销售额 × 100 (正向: >=100绿 >=85黄 <85红) 口径(与财务Bot报告一致): - 销售收入: voucher_details.subject_code='5001'(主营业务收入) 贷方合计,当年1月至目标月 YTD 月均 - 管理费: subject_code LIKE '5602%'(管理费用,含560213折旧) 借方合计,YTD 月均 → 账面口径 +- 折旧: subject_code = '560213'(管理费用-折旧叶子) 借方合计,YTD 月均 → 现金口径剔除项(eq精确匹配,防5602130误含) - 渠补率: 厂家政策定死 84.61% (571103渠补支出/5001销售, 1-7月累计凭证验证≈84.0%~84.6%) - 边际贡献率 = 1 - 渠补率 = 15.39% - 目标月: --period 显式指定;默认自动选最近完整月(当月跳过;最近月销售额<近6月均30%视为未结账回退) @@ -45,10 +48,16 @@ TARGET_KPIS = [ ("F_BEP_REVENUE", "保本销售额"), ("F_BEP_COVER", "保本覆盖率"), ("F_BEP_GAP", "保本缺口"), + ("F_BEP_REVENUE_CASH", "现金口径保本销售额"), + ("F_BEP_COVER_CASH", "现金口径保本覆盖率"), ] +# 现金口径 KPI 编码(计算路径独立于账面,remark 文案用现金口径) +CASH_CODES = {"F_BEP_REVENUE_CASH", "F_BEP_COVER_CASH"} + SALES_CODE = "5001" # 主营业务收入 ADMIN_PREFIX = "5602" # 管理费用(含折旧叶子560213) +DEP_SUBJECT = "560213" # 管理费用-折旧(现金口径剔除项,eq 精确匹配防 5602130 误含) REBATE_CODE = "571103" # 渠补/捐赠支出(校验用,不直接进公式) @@ -160,7 +169,7 @@ def _eval_static_threshold(value: float, kpi) -> tuple: def run_derive(db, entity_id: int, period: str, rebate_rate_pct: float, write_kpi: bool = True) -> dict: - """计算并落库三个派生KPI。返回结果摘要。""" + """计算并落库五个派生KPI(账面3条+现金2条)。返回结果摘要。""" kpis = {k.kpi_code: k for k in db.query(KPIDefinition).filter( KPIDefinition.entity_id == entity_id, KPIDefinition.kpi_code.in_([c for c, _ in TARGET_KPIS]), @@ -179,9 +188,12 @@ def run_derive(db, entity_id: int, period: str, rebate_rate_pct: float, sales_sum_w = _sum_field(db, entity_id, period_from, period_to, "credit_amount", ("eq", SALES_CODE)) / 10000.0 admin_sum_w = _sum_field(db, entity_id, period_from, period_to, "debit_amount", ("like", ADMIN_PREFIX)) / 10000.0 rebate_sum_w = _sum_field(db, entity_id, period_from, period_to, "debit_amount", ("eq", REBATE_CODE)) / 10000.0 + dep_sum_w = _sum_field(db, entity_id, period_from, period_to, "debit_amount", ("eq", DEP_SUBJECT)) / 10000.0 sales_avg = sales_sum_w / months admin_avg = admin_sum_w / months + dep_avg = dep_sum_w / months + cash_admin_avg = admin_avg - dep_avg # 现金口径固定成本(剔除折旧纸面费用) # 2. 渠补率:厂家政策值优先,日志附凭证累计校验 calc_rebate = (rebate_sum_w / sales_sum_w * 100.0) if sales_sum_w > 0 else 0.0 @@ -189,15 +201,19 @@ def run_derive(db, entity_id: int, period: str, rebate_rate_pct: float, rebate_rate_pct = POLICY_REBATE_RATE_PCT cm_pct = 100.0 - rebate_rate_pct # 边际贡献率(%) - # 3. 派生计算(万元 / %) + # 3. 派生计算(万元 / %)——账面3条路径不变;现金口径在账面基础上剔除折旧 bep = admin_avg / (cm_pct / 100.0) if cm_pct > 0 else 0.0 cover = (sales_avg / bep * 100.0) if bep > 0 else 0.0 gap = ((bep - sales_avg) / bep * 100.0) if bep > 0 else 0.0 + cash_bep = cash_admin_avg / (cm_pct / 100.0) if cm_pct > 0 else 0.0 + cash_cover = (sales_avg / cash_bep * 100.0) if cash_bep > 0 else 0.0 values = { "F_BEP_REVENUE": round(bep, 1), "F_BEP_COVER": round(cover, 1), "F_BEP_GAP": round(gap, 1), + "F_BEP_REVENUE_CASH": round(cash_bep, 1), + "F_BEP_COVER_CASH": round(cash_cover, 1), } # 4. upsert kpi_values (source_type='derived',幂等) @@ -206,6 +222,11 @@ def run_derive(db, entity_id: int, period: str, rebate_rate_pct: float, value_rows = {} for code, _ in TARGET_KPIS: kpi = kpis[code] + if code in CASH_CODES: + remark = (f"现金口径: 管理费月均{admin_avg:.2f}万-折旧月均{dep_avg:.2f}万" + f"=固定现金{cash_admin_avg:.2f}万 ÷ 边际贡献率{cm_pct:.2f}% → 保本{cash_bep:.2f}万") + else: + remark = f"保本派生器: 销售月均{sales_avg:.1f}万/管理费月均{admin_avg:.1f}万/渠补率{rebate_rate_pct:.2f}%" existing = db.query(KPIValue).filter( KPIValue.kpi_id == kpi.id, KPIValue.period == period, @@ -216,7 +237,7 @@ def run_derive(db, entity_id: int, period: str, rebate_rate_pct: float, if existing: existing.actual_value = values[code] existing.source_batch = batch - existing.remark = f"保本派生器: 销售月均{sales_avg:.1f}万/管理费月均{admin_avg:.1f}万/渠补率{rebate_rate_pct:.2f}%" + existing.remark = remark existing.data_status = "pending" else: existing = KPIValue( @@ -227,7 +248,7 @@ def run_derive(db, entity_id: int, period: str, rebate_rate_pct: float, source_type="derived", source_batch=batch, data_status="pending", - remark=f"保本派生器: 销售月均{sales_avg:.1f}万/管理费月均{admin_avg:.1f}万/渠补率{rebate_rate_pct:.2f}%", + remark=remark, ) db.add(existing) db.flush() @@ -281,6 +302,9 @@ def run_derive(db, entity_id: int, period: str, rebate_rate_pct: float, "sales_avg": round(sales_avg, 2), "admin_sum_w": round(admin_sum_w, 2), "admin_avg": round(admin_avg, 2), + "dep_sum_w": round(dep_sum_w, 2), + "dep_avg": round(dep_avg, 2), + "cash_admin_avg": round(cash_admin_avg, 2), "rebate_sum_w": round(rebate_sum_w, 2), "rebate_policy_pct": rebate_rate_pct, "rebate_calc_pct": round(calc_rebate, 2), @@ -308,11 +332,15 @@ def main(): f"(YTD {r['months']}个月) mode={'dry-run' if args.dry_run else 'write'}") print(f" 销售收入累计 {r['sales_sum_w']:.2f}万 月均 {r['sales_avg']:.2f}万") print(f" 管理费累计 {r['admin_sum_w']:.2f}万 月均 {r['admin_avg']:.2f}万 (含折旧,账面口径)") + print(f" 折旧累计 {r['dep_sum_w']:.2f}万 月均 {r['dep_avg']:.2f}万 (560213) | " + f"现金固定成本月均 {r['cash_admin_avg']:.2f}万 (管理费-折旧)") print(f" 渠补支出累计 {r['rebate_sum_w']:.2f}万 | 渠补率: 政策{r['rebate_policy_pct']:.2f}% " f"(凭证累计校验 {r['rebate_calc_pct']:.2f}%) | 边际贡献率 {r['cm_pct']:.2f}%") print(f" F_BEP_REVENUE 保本销售额 = {r['values']['F_BEP_REVENUE']} 万元") print(f" F_BEP_COVER 保本覆盖率 = {r['values']['F_BEP_COVER']} %") print(f" F_BEP_GAP 保本缺口 = {r['values']['F_BEP_GAP']} %") + print(f" F_BEP_REVENUE_CASH 现金保本销售额 = {r['values']['F_BEP_REVENUE_CASH']} 万元") + print(f" F_BEP_COVER_CASH 现金保本覆盖率 = {r['values']['F_BEP_COVER_CASH']} %") print(f" 落库: {r['written']}") finally: db.close() diff --git a/backend/scripts/sql/bep_kpi_defs.sql b/backend/scripts/sql/bep_kpi_defs.sql index bc366fcc..8c17cbc2 100644 --- a/backend/scripts/sql/bep_kpi_defs.sql +++ b/backend/scripts/sql/bep_kpi_defs.sql @@ -3,8 +3,10 @@ -- 口径: 财务Bot权威文件 陕西酣客_保本点分析_2026年1-7月.md -- 渠补率=84.61%(厂家政策,571103/5001累计验证84.05%~84.61%区间) -- 边际贡献率=1-渠补率=15.39% --- 保本销售额=月均管理费(5602借方,含折旧) ÷ 15.39% → 26.2万/15.39% = 170.3万 +-- 保本销售额=月均管理费(5602借方,含折旧) ÷ 15.39% → 26.2万/15.39% = 170.3万 (账面口径) -- 覆盖率=月均销售(5001贷方)÷保本销售额 → 113.5/170.3 = 66.7% +-- 现金口径(剔除折旧560213): 现金保本销售额=(月均管理费-月均折旧) ÷ 15.39% (~121.4万) +-- 现金覆盖率=月均销售÷现金保本额 (~93.5%);两口径并列展示,账面红≠现金红 INSERT INTO kpi_definitions (entity_id, kpi_code, kpi_name, dimension, category, formula, formula_desc, @@ -31,4 +33,18 @@ VALUES '反向指标(缺口越小越好): <=0绿(已保本) <=15黄 >15红; 当前约33%=月销距保本差33%', 'derived', 'BEP派生器 kpi_bep_deriver.py(voucher_details 5001月均)', '财务部', 'monthly', '%', 0, 'ratio', '<=0', '<=15', '>15', + 'strategic', 'active', 'finance-bot', 'Epic2', 1), + +(1, 'F_BEP_REVENUE_CASH', '现金口径保本销售额', 'finance', 'profitability', + 'round((月管理费月均-月折旧月均)/(1-渠补率),1)', + '现金口径=月管理费(5602借方月均,剔除折旧560213)÷(1-渠补率84.61%); 中性参考值, 无阈值', + 'derived', 'BEP派生器 kpi_bep_deriver.py(voucher_details 5602/560213月均)', '财务部', 'monthly', '万元', 0, + 'ratio', NULL, NULL, NULL, + 'strategic', 'active', 'finance-bot', 'Epic2', 1), + +(1, 'F_BEP_COVER_CASH', '现金口径保本覆盖率', 'finance', 'profitability', + 'round(实际月销/现金口径保本额*100,1)', + '实际月销(5001贷方月均)÷现金口径保本额; 正向指标: >=100绿 >=85黄 <85红; 当前约93.5%(2026-07)', + 'derived', 'BEP派生器 kpi_bep_deriver.py(voucher_details 5001/5602/560213月均)', '财务部', 'monthly', '%', 100, + 'ratio', '>=100', '>=85', '<85', 'strategic', 'active', 'finance-bot', 'Epic2', 1);