From 587d5b39832bde83ee8325800504b0e3a006af5d Mon Sep 17 00:00:00 2001 From: wecom-fullstack Date: Sun, 4 Oct 2026 19:41:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(cvp):=20BEP=20=E2=91=A4=20=E5=8F=A3?= =?UTF-8?q?=E5=BE=84=E7=BB=9F=E4=B8=80=E8=90=BD=E5=9C=B0=EF=BC=88=E5=8F=98?= =?UTF-8?q?=E5=8A=A8=E6=88=90=E6=9C=AC=E7=8E=87=3D=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E5=8F=A3=E5=BE=84=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 派生器/口径:F_BEP_VC_RATE 新增;卡面/公式行文案改「月固定成本 ÷ 边际贡献率」 - 保本额 = 月固定成本 ÷ (1 − 变动成本率);实测渠补率(F_BEP_REBATE)降为 e1 专用 chip - 报告卡口径说明文字同步(固定成本=固定费用合计) 任务: finance-20261003-cvp-fix-and-bohai-coverage-fullstack ⑤ --- backend/app/api/dashboard.py | 50 +++-- backend/app/utils/bep_caliber.py | 36 ++-- backend/scripts/kpi_bep_deriver.py | 177 +++++++++++++----- .../sql/bep_kpi_defs_20261003_vc_rate.sql | 39 ++++ frontend/src/components/BEPBreakEvenCard.vue | 15 +- 5 files changed, 240 insertions(+), 77 deletions(-) create mode 100644 backend/scripts/sql/bep_kpi_defs_20261003_vc_rate.sql diff --git a/backend/app/api/dashboard.py b/backend/app/api/dashboard.py index 391d8e96..ac6369f8 100644 --- a/backend/app/api/dashboard.py +++ b/backend/app/api/dashboard.py @@ -35,8 +35,10 @@ router = APIRouter(prefix="/api/cma/dashboard", tags=["驾驶舱"], # 月销 = voucher_details subject 5001(主营业务收入) 贷方 YTD 月均(万元) # 月管理费 = 5602% 借−贷 净额 YTD 月均(carry_forward=0 行域;账面口径含折旧) # 折旧 = 560213 借−贷 净额 YTD 月均(同上;口径4 见 _decisions/cma-expense-profit-net-caliber-20261002.md) -# 渠补率 = **实测累计**(Σ571103借方 ÷ Σ5001贷方,YTD)——取自派生落库值 F_BEP_REBATE(单一事实源) -# 保本额 = 月固定成本 ÷ (1-渠补率) +# 变动成本率 = **实测累计**(⑤ 口径统一:酣客=Σ(5401+5403+571103−530103)÷Σ5001贷; +# 博海=Σ6401借÷Σ6001贷)——取自派生落库值 F_BEP_VC_RATE(单一事实源) +# 实测渠补率 = Σ571103借 ÷ Σ5001贷(酣客专用,F_BEP_REBATE,仅作 chip 展示,**不再充当变动成本率**) +# 保本额 = 月固定成本 ÷ 边际贡献率(边际贡献率 = 1 − 变动成本率) # 2026-10-02 口径修订(任总 11:43 CST 放行「改,这样数据更加真实」): # ① 渠补率不再用政策常量 84.61 参与计算,改用派生落库的实测累计值; # 政策常量仅作对照返回(inputs.rebate_policy_pct),**不参与计算**; @@ -46,10 +48,16 @@ router = APIRouter(prefix="/api/cma/dashboard", tags=["驾驶舱"], # 不出保本值、不回落政策常量(不跨期回落,避免混期口径)。 # 账面口径(含折旧) / 现金口径(剔除折旧) 双口径只读返回,无写操作。 BEP_CODES = ("F_BEP_REVENUE", "F_BEP_COVER", "F_BEP_GAP", - "F_BEP_REVENUE_CASH", "F_BEP_COVER_CASH", "F_BEP_REBATE") + "F_BEP_REVENUE_CASH", "F_BEP_COVER_CASH", + "F_BEP_VC_RATE", "F_BEP_REBATE") BEP_BOOK_CODES = ("F_BEP_REVENUE", "F_BEP_COVER", "F_BEP_GAP") # 账面3条必须齐全才 available BEP_CASH_CODES = ("F_BEP_REVENUE_CASH", "F_BEP_COVER_CASH") -BEP_REBATE_CODE = "F_BEP_REBATE" # 变动/渠补率派生KPI编码(卡面单一事实源) +# 保本公式所用「变动成本率」编码(⑤ 口径统一 2026-10-03:边际贡献率 = 1 − 变动成本率) +BEP_VC_CODE = "F_BEP_VC_RATE" +# 实测渠补率编码(酣客 e1 专用、**可选**;⑤ 后**不再充当变动成本率**,仅作卡面 chip) +BEP_REBATE_CODE = "F_BEP_REBATE" +# fail-closed 必备六条:VC_RATE 必填;REBATE 因博海(e2)无渠补科目而不作必备 +BEP_REQUIRED_CODES = tuple(c for c in BEP_CODES if c != BEP_REBATE_CODE) # 展示用输入口径常量(**仅酣客(entity1)**;多实体一律走 app/utils/bep_caliber.py 的 ENTITY_CALIBERS, # 2026-10-03 扩面:博海 entity2 为 6001/6401/6601+6602+6603/1602) BEP_SALES_SUBJECT = "5001" @@ -153,6 +161,10 @@ def get_bep_card( · 渠补率 = 派生落库的**实测累计值**(F_BEP_REBATE,单一事实源);政策常量仅对照返回; · 账面 / 现金两条路径**统一读派生落库值**(不再实时复算 voucher); · fail-closed:六条派生值同期不齐(尤其缺渠补率)⇒ available=False,不出保本值、不回落常量。 + + 2026-10-03 口径统一(⑤,任总 18:00 CST):**边际贡献率 = 1 − 变动成本率**(F_BEP_VC_RATE, + 完整口径:销货成本+税金及附加+渠补−厂补 / 博海 6401÷6001),不再用渠补率充当变动成本率; + 实测渠补率 F_BEP_REBATE 降为 e1 专用展示 chip(e2 无渠补科目 ⇒ None,不再纳入必备集)。 """ cal = caliber_or_none(entity_id) if cal is None: @@ -199,20 +211,21 @@ def get_bep_card( months = int(period[5:7]) period_from = f"{year}-01" - # fail-closed:六条派生值(含变动成本率)本期必须齐全,缺一条即不出保本值 - missing_at_period = [c for c in BEP_CODES if by_code.get(c, {}).get(period) is None] + # fail-closed:必备派生值(含变动成本率)本期必须齐全,缺一条即不出保本值 + missing_at_period = [c for c in BEP_REQUIRED_CODES if by_code.get(c, {}).get(period) is None] if missing_at_period: # ① 不适用(派生器 fail-closed 落库:remark 已注明「保本点不适用」) # —— 与「缺数」区分:卡面显示「不适用」而非「数据不齐」,且不得显示 0。 na_remarks = [remark_map.get((c, period), "") for c in missing_at_period] na_hit = [r for r in na_remarks if "不适用" in r] - if na_hit and BEP_REBATE_CODE not in missing_at_period: + if na_hit and BEP_VC_CODE not in missing_at_period: reason = na_hit[0] label = ("边际贡献为负 · 保本点不适用" if "边际贡献非正" in reason else "固定成本退化 · 保本点不适用") return {"available": False, "not_applicable": True, "entity_id": entity_id, "period": period, "months": months, "label": label, "reason": reason, + "vc_pct": by_code.get(BEP_VC_CODE, {}).get(period), "rebate_pct": by_code.get(BEP_REBATE_CODE, {}).get(period), "rebate_policy_pct": (cal.get("policy_pct") if cal.get("policy_pct") is not None else BEP_REBATE_POLICY_PCT), @@ -221,8 +234,8 @@ def get_bep_card( "derived_periods": book_periods[:6], "data_source": "derived", "readonly": True} # ② 缺数(真不齐) - if BEP_REBATE_CODE in missing_at_period: - msg = (f"{cal['margin_label']}不可得:本期未落 F_BEP_REBATE 派生值 ⇒ 不出保本值" + if BEP_VC_CODE in missing_at_period: + msg = (f"{cal['margin_label']}不可得:本期未落 F_BEP_VC_RATE 派生值 ⇒ 不出保本值" "(fail-closed,不回落政策常量、不跨期回落)") else: msg = ("本期派生值不齐 %s ⇒ 不出保本值(fail-closed,不跨期回落)" @@ -233,16 +246,19 @@ def get_bep_card( else BEP_REBATE_POLICY_PCT), "derived_periods": book_periods[:6], "readonly": True} - # 六条派生落库值(同期)—— 账面 / 现金两条路径同一来源,卡面不做实时复算 - d = {c: by_code[c][period] for c in BEP_CODES} + # 派生落库值(同期)—— 账面 / 现金两条路径同一来源,卡面不做实时复算 + d = {c: by_code.get(c, {}).get(period) for c in BEP_CODES} book_bep = float(d["F_BEP_REVENUE"]) book_cover = float(d["F_BEP_COVER"]) book_gap = float(d["F_BEP_GAP"]) cash_bep = float(d["F_BEP_REVENUE_CASH"]) cash_cover = float(d["F_BEP_COVER_CASH"]) cash_gap = round(100.0 - cash_cover, 1) # 缺口 = 100 − 覆盖率(与派生器同口径) - rebate_pct = float(d[BEP_REBATE_CODE]) # 实测累计渠补率(单一事实源) - cm_pct = round(100.0 - rebate_pct, 2) + # ⑤ 口径统一(任总 2026-10-03 18:00「统一」):保本公式用**变动成本率**(F_BEP_VC_RATE), + # 边际贡献率 = 1 − 变动成本率;F_BEP_REBATE(实测渠补率)仅酣客(e1)有,**不再充当变动成本率**。 + vc_pct = float(d[BEP_VC_CODE]) + cm_pct = round(100.0 - vc_pct, 2) + rebate_pct = (float(d[BEP_REBATE_CODE]) if d.get(BEP_REBATE_CODE) is not None else None) # 输入口径(万元,展示用;数值口径一律以派生落库值为准)——按实体口径现算 inp = _bep_inputs(db, entity_id, cal, period_from, period, months) @@ -272,9 +288,12 @@ def get_bep_card( "sales_avg": round(sales_avg, 2), "admin_avg": round(admin_avg, 2), "dep_avg": round(dep_avg, 2), + "vc_pct": vc_pct, "rebate_pct": rebate_pct, "rebate_policy_pct": BEP_REBATE_POLICY_PCT, # 政策常量:仅作对照,不参与计算 - "rebate_source": "derived:F_BEP_REBATE(Σ571103借方÷Σ5001贷方, YTD累计)", + "rebate_source": ("derived:F_BEP_VC_RATE(⑤完整口径:销货成本+税金及附加+渠补−厂补)" + if entity_id == 1 + else "derived:F_BEP_VC_RATE(Σ6401借÷Σ6001贷)"), "cm_pct": cm_pct, }, "book": { @@ -294,7 +313,8 @@ def get_bep_card( }, "derived": derived, "data_source": "derived", # 账面/现金同源:均读派生落库值 - "formula": f"保本销售额 = 月固定成本 ÷ (1 − 实测渠补率{rebate_pct:.2f}%)", + "formula": (f"保本销售额 = 月固定成本 ÷ 边际贡献率{cm_pct:.2f}%" + f"(边际贡献率 = 1 − 变动成本率{vc_pct:.2f}%)"), "readonly": True, } diff --git a/backend/app/utils/bep_caliber.py b/backend/app/utils/bep_caliber.py index 89f4bfd0..a5579091 100644 --- a/backend/app/utils/bep_caliber.py +++ b/backend/app/utils/bep_caliber.py @@ -5,18 +5,24 @@ · 派生落库写入面:`backend/scripts/kpi_bep_deriver.py` · 卡面读取面 :`backend/app/api/dashboard.py::/bep-card` -口径(2026-10-03 财务裁定,任务 finance-20261003-cvp-fix-and-bohai-coverage-fullstack): +口径(2026-10-03 财务裁定 + 任总 18:00「统一」,任务 finance-20261003-cvp-fix-and-bohai-coverage-fullstack): | 项 | entity 1 陕西酣客 | entity 2 博海 | |---|---|---| -| 变动成本(分子) | Σ571103 借方(渠补/捐赠支出) | Σ6401 借方(主营业务成本) | +| 变动成本(分子) | **(Σ5401借 + Σ5403借 + Σ571103借 − Σ530103贷)**〔⑤口径统一〕 | Σ6401 借方(主营业务成本) | | 收入基数(分母) | Σ5001 贷方(主营业务收入) | Σ6001 贷方(主营业务收入) | -| 变动成本率 | Σ571103借 ÷ Σ5001贷(实测渠补率) | Σ6401借 ÷ Σ6001贷(主营业务成本率) | -| 固定成本 | `5602%`(管理费用,含折旧)借−贷净额 | `6601+6602+6603`(销售/管理/财务费用)借−贷净额 | +| 变动成本率 | 上式 ÷ Σ5001贷(**完整口径**,非渠补率) | Σ6401借 ÷ Σ6001贷(主营业务成本率) | +| 实测渠补率(F_BEP_REBATE) | Σ571103借 ÷ Σ5001贷(**保留**为渠补单一事实源,**不再**充当变动成本率) | 无(None) | +| 固定成本 | `5601%` + `5602%` + `5603%` 借−贷净额〔⑤扩容〕 | `6601+6602+6603`(销售/管理/财务费用)借−贷净额 | | 折旧剔除(现金口径) | `560213`(管理费用-折旧叶子) | `1602` 累计折旧**当期贷方发生额**(见下) | | 本期利润结转科目 | `3103` 本年利润 | `4103` 本年利润 | | 结账信号次选(落盘) | `/root/WB_WS/_data/zxt-hanke/{YYYY.MM}/check.md` | 无(只用 DB 信号) | +⑤ 口径统一依据(任总 2026-10-03 18:00 DM「统一」):完整口径唯一可勾稽至营业利润 + `808.80 − 672.65 − 224.16 = −88.01 ≈ −88.00` 既定营业利润。 +2026-08 锚点(YTD 月均):变动成本率 **83.17%**、边际贡献率 **16.83%**、固定月均 **28.02 万**、 + 保本 **166.45 万/月**、覆盖率 **60.74%**、缺口 **39.26%**、现金保本 **≈121.8 万/月**。 + 博海折旧口径说明:博海科目表只到 4 位一级科目(无费用类折旧叶子,**不存在** 酣客式 `560213`), 唯一含「折旧」的是资产类 `1602 累计折旧`;其**当期贷方发生额** = 当期计提折旧额(对方科目 计入 `6602 管理费用`),故取它作现金口径剔除项。实测 2026-01..07 每月 1,468.83 元, @@ -33,15 +39,22 @@ CLOSING_SUMMARY_LIKE = "%结转%" # 酣客结账信号次选来源(智享通落盘 check.md;与 app/api/data.py 的 ZXT_LANDING_DIR 同源) ZXT_LANDING_DIR_HANKE = "/root/WB_WS/_data/zxt-hanke" +# 变动成本「带号科目项」记法: terms = [(sign, subject_code, field), ...] +# sign='+' ⇒ 取该科目 field 的发生额相加;sign='-' ⇒ 取该科目 field 的发生额相减 +# field ∈ {'debit' → debit_amount, 'credit' → credit_amount, 'net' → 借−贷} ENTITY_CALIBERS = { 1: dict( name="陕西酣客", - margin_label="实测渠补率", - margin_desc="Σ571103借方 ÷ Σ5001贷方", + margin_label="变动成本率", + margin_desc="(Σ5401借+Σ5403借+Σ571103借−Σ530103贷) ÷ Σ5001贷", + # ⑤ 口径统一(2026-10-03 任总「统一」):变动成本 = 销货成本 + 税金及附加 + 渠补 − 厂补 + varcost_terms=[("+", "5401", "debit"), ("+", "5403", "debit"), + ("+", "571103", "debit"), ("-", "530103", "credit")], + # F_BEP_REBATE 实测渠补率专用(单一事实源保留;不再充当变动成本率) + rebate_terms=[("+", "571103", "debit")], revenue_field="credit_amount", revenue_filter=("eq", "5001"), - varcost_field="debit_amount", varcost_filter=("eq", "571103"), - fixed_filters=[("like", "5602")], - fixed_label="管理费", + fixed_filters=[("like", "5601"), ("like", "5602"), ("like", "5603")], + fixed_label="固定成本(5601+5602+5603)", dep_field="net", dep_filter=("eq", "560213"), closing_subject="3103", landing_dir=ZXT_LANDING_DIR_HANKE, @@ -52,15 +65,16 @@ ENTITY_CALIBERS = { name="博海", margin_label="变动成本率(主营业务成本率)", margin_desc="Σ6401借方 ÷ Σ6001贷方", + varcost_terms=[("+", "6401", "debit")], + rebate_terms=None, # 博海无渠补科目 revenue_field="credit_amount", revenue_filter=("eq", "6001"), - varcost_field="debit_amount", varcost_filter=("eq", "6401"), fixed_filters=[("like", "6601"), ("like", "6602"), ("like", "6603")], fixed_label="固定成本(6601+6602+6603)", dep_field="credit_amount", dep_filter=("eq", "1602"), closing_subject="4103", landing_dir=None, exclude_closing=True, - policy_pct=None, # 博海无渠补政策常量 + policy_pct=None, ), } diff --git a/backend/scripts/kpi_bep_deriver.py b/backend/scripts/kpi_bep_deriver.py index 9f77b29f..551a5409 100644 --- a/backend/scripts/kpi_bep_deriver.py +++ b/backend/scripts/kpi_bep_deriver.py @@ -6,11 +6,14 @@ F_BEP_GAP 保本缺口(%) = (保本销售额-月均销售) ÷ 保本销售额 × 100 (反向: <=0绿 <=15黄 >15红) F_BEP_REVENUE_CASH 现金保本销售额(万元) = (月均固定成本-月均折旧) ÷ 边际贡献率 (现金口径,中性无阈值) F_BEP_COVER_CASH 现金保本覆盖率(%) = 月均销售 ÷ 现金保本销售额 × 100 (正向: >=100绿 >=85黄 <85红) - F_BEP_REBATE 变动/渠补率(%) = 见下方「实体口径注册表」 (YTD累计; 卡面单一事实源,无阈值) + F_BEP_VC_RATE 变动成本率(%) = 见下方「实体口径注册表」 (YTD累计; 保本公式分母,无阈值) + F_BEP_REBATE 实测渠补率(%) = e1 专用展示 chip(Σ571103÷Σ5001,YTD; e2 无渠补科目 ⇒ NULL) 实体口径注册表(2026-10-03 新增,任务 finance-20261003-cvp-fix-and-bohai-coverage-fullstack): - · entity 1 陕西酣客:变动成本率 = Σ571103借方 ÷ Σ5001贷方(实测渠补率);固定成本 = 5602% 净额; - 折旧剔除 = 560213(管理费用-折旧叶子)。 + · entity 1 陕西酣客:变动成本率 = Σ(5401销货成本 + 5403税金及附加 + 571103渠补 − 530103厂补) ÷ Σ5001贷方 + 〔⑤口径统一 2026-10-03,任总 18:00「统一」;勾稽:808.80 − 672.65 − 224.16 = −88.01 ≈ −88.00 营业利润〕; + 固定成本 = 5601% + 5602% + 5603% 净额;折旧剔除 = 560213(管理费用-折旧叶子)。 + 实测渠补率 F_BEP_REBATE 保留(仍是渠补单一事实源),但**不再充当变动成本率**。 · entity 2 博海:**无渠补科目(571103)**,变动成本 = Σ6401借(主营业务成本), 变动成本率 = Σ6401借 ÷ Σ6001贷;固定成本 = (6601 销售费用 + 6602 管理费用 + 6603 财务费用) 净额; 折旧剔除 = 1602 累计折旧「当期贷方发生额」(博海科目表只到 4 位一级,无费用类折旧叶子; @@ -21,11 +24,11 @@ 口径(与财务Bot报告一致): - 销售收入: entity1 = subject_code='5001'(主营业务收入) 贷方合计;entity2 = '6001' 贷方合计。当年1月至目标月 YTD 月均 -- 固定成本: entity1 = subject_code LIKE '5602%'(管理费用,含560213折旧) 借−贷净额合计(carry_forward=0),YTD 月均 +- 固定成本: entity1 = subject_code LIKE '5601%'/'5602%'/'5603%'(销售/管理/财务费用,含560213折旧) 借−贷净额合计(carry_forward=0),YTD 月均〔⑤扩容〕 entity2 = 6601/6602/6603 借−贷净额合计(同上,但另需剔除结转腿) - 折旧: entity1 = '560213' 借−贷净额;entity2 = '1602' 贷方发生额(现金口径剔除项) -- 变动成本率: **实测累计口径**(2026-10-02 财务裁定,任总 11:43 CST 放行「改,这样数据更加真实」) - 酣客 渠补率(实测) = Σ(571103 借方) ÷ Σ(5001 贷方) × 100,期间 = 年初至当期(YTD),保留 2 位小数。 +- 变动成本率: **完整实测口径**(⑤口径统一 2026-10-03,任总 18:00「统一」;勾稽件 _reports/陕西酣客_边际贡献式损益表_20261003.md) + 酣客 = Σ(5401销货成本 + 5403税金及附加 + 571103渠补 − 530103厂补) ÷ Σ(5001 贷方) × 100,YTD,保留 2 位小数。 `POLICY_REBATE_RATE_PCT = 84.61` 政策常量**仅作对照展示**(写进返回值 rebate_policy_pct / remark), **不参与任何计算**;`--rebate-rate` 显式传值时视为人工诊断覆盖(正常调用不再使用)。 - 边际贡献率 = 100 − 变动成本率 @@ -85,13 +88,17 @@ TARGET_KPIS = [ ("F_BEP_GAP", "保本缺口"), ("F_BEP_REVENUE_CASH", "现金口径保本销售额"), ("F_BEP_COVER_CASH", "现金口径保本覆盖率"), + ("F_BEP_VC_RATE", "变动成本率"), ("F_BEP_REBATE", "实测渠补率"), ] -# 实测渠补率 KPI 编码(卡面单一事实源;无阈值,不参与预警级别判定) +# 变动成本率 KPI(⑤ 口径统一 2026-10-03:保本公式分子口径 = 销货成本+税金及附加+渠补−厂补) +VC_RATE_KPI = "F_BEP_VC_RATE" + +# 实测渠补率 KPI 编码(卡面单一事实源;无阈值,不参与预警级别判定;⑤ 后**不再充当变动成本率**) REBATE_KPI = "F_BEP_REBATE" -# 保本口径 KPI(缺 F_BEP_REBATE 定义时不阻断派生主流程,仅跳过渠补率落库并告警) +# 保本口径 KPI(缺 F_BEP_REBATE 定义时不阻断派生主流程,见 run_derive 告警分支) CORE_KPI_CODES = [c for c, _ in TARGET_KPIS if c != REBATE_KPI] # 现金口径 KPI 编码(计算路径独立于账面,remark 文案用现金口径) @@ -222,6 +229,42 @@ def _subject_clause(kind: str, code: str): raise ValueError(f"未知科目过滤类型: {kind}") +# 带号科目项的字段映射(terms 记法见 bep_caliber.ENTITY_CALIBERS) +_TERM_FIELD = { + "debit": lambda: VoucherDetail.debit_amount, + "credit": lambda: VoucherDetail.credit_amount, + "net": lambda: VoucherDetail.debit_amount - VoucherDetail.credit_amount, +} + + +def _sum_terms(db, entity_id: int, period_from: str, period_to: str, + terms, *, exclude_closing: bool = False) -> float: + """按「带号科目项」聚合金额(元)。terms = [(sign, code, field), ...] + + · sign='+' ⇒ 加该科目 field 发生额;sign='-' ⇒ 减之 + · field ∈ {debit, credit, net} + · code 以 '%' 结尾时按前缀(like)匹配,否则精确匹配 + ⑤ 口径统一(2026-10-03):酣客变动成本 = (Σ5401借 + Σ5403借 + Σ571103借 − Σ530103贷)。 + """ + total = 0.0 + for sign, code, field in terms: + expr = _TERM_FIELD[field]() + q = db.query(func.sum(expr)).filter( + VoucherDetail.entity_id == entity_id, + VoucherDetail.carry_forward == 0, + VoucherDetail.period >= period_from, + VoucherDetail.period <= period_to, + ) + q = (q.filter(VoucherDetail.subject_code.like(code)) if code.endswith("%") + else q.filter(VoucherDetail.subject_code == code)) + if exclude_closing: + q = q.filter(or_(VoucherDetail.summary.is_(None), + ~VoucherDetail.summary.like(CLOSING_SUMMARY_LIKE))) + v = float(q.scalar() or 0.0) + total += v if sign == "+" else -v + return total + + def compute_margin_rate(db, entity_id: int, period_from: str, period_to: str, caliber: dict = None) -> dict: """变动成本率(YTD 累计,2 位小数)—— 按实体口径现算。 @@ -236,9 +279,8 @@ def compute_margin_rate(db, entity_id: int, period_from: str, period_to: str, (调用方不得回落政策常量,须按「不可得」处理)。 """ cal = caliber or ENTITY_CALIBERS[entity_id] - num_raw = _sum_field(db, entity_id, period_from, period_to, - cal["varcost_field"], cal["varcost_filter"], - exclude_closing=cal["exclude_closing"]) + num_raw = _sum_terms(db, entity_id, period_from, period_to, + cal["varcost_terms"], exclude_closing=cal["exclude_closing"]) den_raw = _sum_field(db, entity_id, period_from, period_to, cal["revenue_field"], cal["revenue_filter"], exclude_closing=cal["exclude_closing"]) @@ -256,7 +298,7 @@ def compute_margin_rate(db, entity_id: int, period_from: str, period_to: str, info["reason"] = f"{cal['margin_label']}不可得:{cal['revenue_filter'][1]} 累计缺数或为 0" return info if num_w <= 0: - info["reason"] = f"{cal['margin_label']}不可得:{cal['varcost_filter'][1]} 累计缺数或为 0" + info["reason"] = f"{cal['margin_label']}不可得:变动成本累计缺数或为 0" return info info["available"] = True info["rebate_pct"] = round(num_w / den_w * 100.0, 2) @@ -264,17 +306,39 @@ def compute_margin_rate(db, entity_id: int, period_from: str, period_to: str, def compute_rebate_rate(db, entity_id: int, period_from: str, period_to: str) -> dict: - """实测累计渠补率(2026-10-02 财务裁定口径)—— 酣客(entity1) 专用薄封装。 + """实测累计**渠补率**(F_BEP_REBATE,2026-10-02 口径)—— 酣客(entity1) 专用。 渠补率(实测) = Σ(571103 借方) ÷ Σ(5001 贷方) × 100,期间 = 年初至当期(YTD),保留 2 位小数。 · 571103 = 渠补/捐赠支出(损益类费用科目)⇒ 本期发生额落**借方**; · 5001 = 主营业务收入 ⇒ 本期发生额落**贷方**; - · 行域 = carry_forward=0(与 _sum_field 一致,排除期间损益结转腿)。 + · 行域 = carry_forward=0(排除期间损益结转腿)。 - 保留本函数名/键名以兼容既有调用方与单测;通用实现见 compute_margin_rate。 + ⚠️ ⑤ 口径统一(2026-10-03)后:本函数只算**渠补口径**,是 F_BEP_REBATE 的单一事实源; + 保本公式所用的**变动成本率**(销货成本+税金及附加+渠补−厂补)见 compute_margin_rate。 + 博海(rebate_terms=None)无渠补科目 ⇒ 返回 available=False(F_BEP_REBATE=NULL)。 """ - return compute_margin_rate(db, entity_id, period_from, period_to, - ENTITY_CALIBERS.get(entity_id)) + cal = ENTITY_CALIBERS.get(entity_id) + if not cal or not cal.get("rebate_terms"): + return {"available": False, "rebate_pct": None, "rebate_sum_w": 0.0, + "sales_sum_w": 0.0, "label": "实测渠补率", + "reason": f"{cal['name'] if cal else entity_id} 无渠补科目"} + num_raw = _sum_terms(db, entity_id, period_from, period_to, + cal["rebate_terms"], exclude_closing=cal["exclude_closing"]) + den_raw = _sum_field(db, entity_id, period_from, period_to, + cal["revenue_field"], cal["revenue_filter"], + exclude_closing=cal["exclude_closing"]) + num_w, den_w = num_raw / 10000.0, den_raw / 10000.0 + info = {"rebate_sum_w": round(num_w, 2), "sales_sum_w": round(den_w, 2), + "rebate_pct": None, "available": False, "reason": "", "label": "实测渠补率"} + if den_w <= 0: + info["reason"] = "渠补率不可得:Σ5001贷累计缺数或为 0" + return info + if num_w <= 0: + info["reason"] = "渠补率不可得:Σ571103借累计缺数或为 0" + return info + info["available"] = True + info["rebate_pct"] = round(num_w / den_w * 100.0, 2) + return info def _month_sales(db, entity_id: int, period: str, caliber: dict = None) -> float: @@ -460,9 +524,11 @@ def run_derive(db, entity_id: int, period: str, rebate_rate_pct: float = None, dep_avg = dep_sum_w / months cash_admin_avg = admin_avg - dep_avg # 现金口径固定成本(剔除折旧纸面费用) - # 2. 变动成本率:默认逐期实测累计(不再取政策常量;常量仅对照,仅 entity1 有) + # 2. 变动成本率(⑤ 后=销货成本+税金及附加+渠补−厂补 完整口径):默认逐期实测累计 rebate_info = compute_margin_rate(db, entity_id, period_from, period_to, cal) calc_pct = rebate_info["rebate_pct"] + # ⑤:渠补率(e1 专用;e2 无 ⇒ available=False)。F_BEP_REBATE 专用,独立于上面变动成本率。 + rebate_kpi_info = compute_rebate_rate(db, entity_id, period_from, period_to) if rebate_rate_pct is not None: rebate_info = dict(rebate_info, available=True, rebate_pct=round(float(rebate_rate_pct), 2), reason="人工覆盖(--rebate-rate)") @@ -484,7 +550,8 @@ def run_derive(db, entity_id: int, period: str, rebate_rate_pct: float = None, period, cal["margin_label"], rebate_pct) return _fail_closed_cm_non_positive(db, entity_id, period, months, rebate_info, cal, kpis, write_kpi, sales_avg=sales_avg, - admin_avg=admin_avg, dep_avg=dep_avg) + admin_avg=admin_avg, dep_avg=dep_avg, + rebate_kpi_info=rebate_kpi_info) # 2c. fail-closed ③(2026-10-03,同族补充):固定成本退化 ⇒ 保本点不适用 # 边际贡献率 > 0 但分子(固定成本)退化时,公式给出 保本额≈0 / 覆盖率趋于无穷, @@ -498,7 +565,8 @@ def run_derive(db, entity_id: int, period: str, rebate_rate_pct: float = None, return _fail_closed_cm_non_positive(db, entity_id, period, months, rebate_info, cal, kpis, write_kpi, sales_avg=sales_avg, admin_avg=admin_avg, dep_avg=dep_avg, guard="fixed", - cash_admin_avg=cash_admin_avg) + cash_admin_avg=cash_admin_avg, + rebate_kpi_info=rebate_kpi_info) # 3. 派生计算(万元 / %)——账面3条路径不变;现金口径在账面基础上剔除折旧 bep = admin_avg / (cm_pct / 100.0) @@ -513,7 +581,9 @@ def run_derive(db, entity_id: int, period: str, rebate_rate_pct: float = None, "F_BEP_GAP": round(gap, 1), "F_BEP_REVENUE_CASH": round(cash_bep, 1), "F_BEP_COVER_CASH": round(cash_cover, 1), - "F_BEP_REBATE": round(rebate_pct, 2), + "F_BEP_VC_RATE": round(rebate_pct, 2), # 变动成本率(⑤ 保本分子口径) + "F_BEP_REBATE": (round(rebate_kpi_info["rebate_pct"], 2) + if rebate_kpi_info["available"] else None), # 实测渠补率(e1;e2=NULL) } # 4. upsert kpi_values (source_type='derived',幂等) @@ -526,15 +596,20 @@ def run_derive(db, entity_id: int, period: str, rebate_rate_pct: float = None, if kpi is None: # 定义缺失(仅可能为 F_BEP_REBATE,见上方告警) written[code] = "no-definition" continue - if code == REBATE_KPI: - if entity_id == 1: - remark = (f"实测渠补率(YTD累计): Σ571103借方 {rebate_info['rebate_sum_w']:.2f}万 ÷ " - f"Σ5001贷方 {rebate_info['sales_sum_w']:.2f}万 × 100 = {rebate_pct:.2f}%" - f"(政策常量 {POLICY_REBATE_RATE_PCT:.2f}% 仅供对照,不参与计算)") + if code == VC_RATE_KPI: + remark = (f"变动成本率(YTD累计): {cal['margin_desc']} = " + f"{rebate_info['rebate_sum_w']:.2f}万 ÷ {rebate_info['sales_sum_w']:.2f}万 " + f"× 100 = {rebate_pct:.2f}%(⑤口径统一:销货成本+税金及附加+渠补−厂补;" + f"保本额 = 固定成本月均 ÷ (1−变动成本率))") + elif code == REBATE_KPI: + if rebate_kpi_info["available"]: + rr = rebate_kpi_info["rebate_pct"] + remark = (f"实测渠补率(YTD累计): Σ571103借方 {rebate_kpi_info['rebate_sum_w']:.2f}万 ÷ " + f"Σ5001贷方 {rebate_kpi_info['sales_sum_w']:.2f}万 × 100 = {rr:.2f}%" + f"(政策常量 {POLICY_REBATE_RATE_PCT:.2f}% 仅供对照;⑤ 后**不再充当变动成本率**," + f"变动成本率见 F_BEP_VC_RATE)") else: - remark = (f"{cal['margin_label']}(YTD累计): {cal['margin_desc']} = " - f"{rebate_info['rebate_sum_w']:.2f}万 ÷ {rebate_info['sales_sum_w']:.2f}万 " - f"× 100 = {rebate_pct:.2f}%({cal['name']}口径,无渠补科目)") + remark = f"{cal['name']}无渠补科目(实测渠补率不适用);⑤ 后变动成本率见 F_BEP_VC_RATE" elif code in CASH_CODES: if entity_id == 1: remark = (f"现金口径: 管理费月均{admin_avg:.2f}万-折旧月均{dep_avg:.2f}万" @@ -560,9 +635,11 @@ def run_derive(db, entity_id: int, period: str, rebate_rate_pct: float = None, is_new = existing is None if existing: old_value = float(existing.actual_value) if existing.actual_value is not None else None - new_value = float(values[code]) - value_changed = old_value is None or abs(old_value - new_value) > 1e-9 - existing.actual_value = values[code] + nv = values[code] + new_value = float(nv) if nv is not None else None + value_changed = ((old_value is None) != (new_value is None) + or (old_value is not None and abs(old_value - new_value) > 1e-9)) + existing.actual_value = nv existing.source_batch = batch if value_changed: existing.remark = (f"{remark}|值已更新(batch={batch}, " @@ -651,8 +728,10 @@ def run_derive(db, entity_id: int, period: str, rebate_rate_pct: float = None, "rebate_sum_w": rebate_info["rebate_sum_w"], "rebate_sales_base_w": rebate_info["sales_sum_w"], "rebate_actual_pct": rebate_pct, + "vc_rate_pct": rebate_pct, + "rebate_kpi_pct": (rebate_kpi_info["rebate_pct"] if rebate_kpi_info["available"] else None), "rebate_policy_pct": POLICY_REBATE_RATE_PCT, - "rebate_source": rebate_info.get("reason") or "YTD实测累计(Σ571103借方/Σ5001贷方)", + "rebate_source": rebate_info.get("reason") or "YTD实测累计(⑤完整口径)", "rebate_calc_pct": round(calc_pct, 2) if calc_pct is not None else None, "cm_pct": round(cm_pct, 2), "values": values, @@ -734,7 +813,8 @@ def _fail_closed_cm_non_positive(db, entity_id: int, period: str, months: int, rebate_info: dict, cal: dict, kpis: dict, write_kpi: bool, sales_avg: float = None, admin_avg: float = None, dep_avg: float = None, guard: str = "cm", - cash_admin_avg: float = None) -> dict: + cash_admin_avg: float = None, + rebate_kpi_info: dict = None) -> dict: """fail-closed ② 落库:保本点**不适用**(2026-10-03 财务裁定)。 五条保本派生值(REVENUE/COVER/GAP/REVENUE_CASH/COVER_CASH)写 @@ -758,14 +838,16 @@ def _fail_closed_cm_non_positive(db, entity_id: int, period: str, months: int, detail = (f"边际贡献非正,保本点不适用({cal['margin_label']} {rebate_pct:.2f}% ≥ 100%," f"边际贡献率 {cm_pct:.2f}% ⇒ 每增一元销售不产生正贡献,保本点无经济意义;" f"fail-closed,禁写 0.0)") - if entity_id == 1: - rebate_remark = (f"实测渠补率(YTD累计): Σ571103借方 {rebate_info['rebate_sum_w']:.2f}万 ÷ " - f"Σ5001贷方 {rebate_info['sales_sum_w']:.2f}万 × 100 = {rebate_pct:.2f}%" - f"(政策常量 {POLICY_REBATE_RATE_PCT:.2f}% 仅供对照,不参与计算)") + vc_rate_remark = (f"变动成本率(YTD累计): {cal['margin_desc']} = " + f"{rebate_info['rebate_sum_w']:.2f}万 ÷ {rebate_info['sales_sum_w']:.2f}万 " + f"× 100 = {rebate_pct:.2f}%(⑤口径统一;保本点因边际贡献非正/固定成本退化不适用)") + if rebate_kpi_info and rebate_kpi_info.get("available"): + rebate_remark = (f"实测渠补率(YTD累计): Σ571103借方 {rebate_kpi_info['rebate_sum_w']:.2f}万 ÷ " + f"Σ5001贷方 {rebate_kpi_info['sales_sum_w']:.2f}万 × 100 = " + f"{rebate_kpi_info['rebate_pct']:.2f}%(政策常量 {POLICY_REBATE_RATE_PCT:.2f}% 仅供对照;" + f"⑤ 后不再充当变动成本率)") else: - rebate_remark = (f"{cal['margin_label']}(YTD累计): {cal['margin_desc']} = " - f"{rebate_info['rebate_sum_w']:.2f}万 ÷ {rebate_info['sales_sum_w']:.2f}万 " - f"× 100 = {rebate_pct:.2f}%({cal['name']}口径,无渠补科目)") + rebate_remark = f"{cal['name']}无渠补科目(实测渠补率不适用);变动成本率见 F_BEP_VC_RATE" batch = f"bep-{datetime.now().strftime('%Y%m%d%H%M%S')}" written = {} changed = [] @@ -775,9 +857,16 @@ def _fail_closed_cm_non_positive(db, entity_id: int, period: str, months: int, if kpi is None: written[code] = "no-definition" continue - is_rebate = (code == REBATE_KPI) - new_val = round(float(rebate_pct), 2) if is_rebate else None - rm = rebate_remark if is_rebate else detail + if code == VC_RATE_KPI: + new_val = round(float(rebate_pct), 2) # 变动成本率可测 ⇒ 照常落库 + rm = vc_rate_remark + elif code == REBATE_KPI: + new_val = (round(float(rebate_kpi_info["rebate_pct"]), 2) + if rebate_kpi_info and rebate_kpi_info.get("available") else None) + rm = rebate_remark + else: + new_val = None + rm = detail existing = db.query(KPIValue).filter( KPIValue.kpi_id == kpi.id, KPIValue.period == period, diff --git a/backend/scripts/sql/bep_kpi_defs_20261003_vc_rate.sql b/backend/scripts/sql/bep_kpi_defs_20261003_vc_rate.sql new file mode 100644 index 00000000..7bfc590d --- /dev/null +++ b/backend/scripts/sql/bep_kpi_defs_20261003_vc_rate.sql @@ -0,0 +1,39 @@ +-- CMA BEP 变动成本率派生KPI定义 — F_BEP_VC_RATE(2026-10-03 ⑤ 口径统一) +-- 依据: 任总 2026-10-03 18:00 DM「统一」+ 任务 finance-20261003-cvp-fix-and-bohai-coverage-fullstack §追加⑤ +-- 口径: 变动成本率 = 变动成本 ÷ 收入基数(YTD 累计,2位小数) +-- · 酣客(e1): 变动成本 = Σ5401借(销货成本)+Σ5403借(税金及附加)+Σ571103借(渠补)−Σ530103贷(厂补) +-- · 博海(e2): 变动成本 = Σ6401借(主营业务成本) +-- 保本额 = 固定成本月均 ÷ (1 − 变动成本率) +-- 说明: F_BEP_REBATE(实测渠补率/博海主营成本率) 保留,⑤ 后**不再充当变动成本率** +-- 幂等: NOT EXISTS 守卫,可安全重跑 +INSERT INTO kpi_definitions +(entity_id, kpi_code, kpi_name, dimension, category, formula, formula_desc, + data_source_type, data_source, data_owner, frequency, unit, target_value, + target_calc_type, threshold_green, threshold_yellow, threshold_red, + kpi_level, status, bot_source, epic, created_by) +SELECT 1, 'F_BEP_VC_RATE', '变动成本率', 'finance', 'profitability', + 'round((sum(5401借)+sum(5403借)+sum(571103借)-sum(530103贷))/sum(5001贷)*100,2)', + '变动成本率(YTD累计,⑤完整口径)=(Σ5401借+Σ5403借+Σ571103借−Σ530103贷)÷Σ5001贷; 供保本点卡面单一事实源; 缺数期不出值(fail-closed)', + 'derived', 'BEP派生器 kpi_bep_deriver.py(voucher_details 5401/5403/571103/530103/5001 累计)', '财务部', 'monthly', '%', 0.00, + 'ratio', NULL, NULL, NULL, + 'strategic', 'active', 'finance-bot', 'Epic2', 1 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM kpi_definitions WHERE entity_id = 1 AND kpi_code = 'F_BEP_VC_RATE' +); + +INSERT INTO kpi_definitions +(entity_id, kpi_code, kpi_name, dimension, category, formula, formula_desc, + data_source_type, data_source, data_owner, frequency, unit, target_value, + target_calc_type, threshold_green, threshold_yellow, threshold_red, + kpi_level, status, bot_source, epic, created_by) +SELECT 2, 'F_BEP_VC_RATE', '变动成本率(主营业务成本率)', 'finance', 'profitability', + 'round(sum(6401借)/sum(6001贷)*100,2)', + '变动成本率(实测,YTD累计)=Σ6401借方÷Σ6001贷方(剔结转账腿); 博海无渠补科目; 供保本点卡面单一事实源, 无阈值', + 'derived', 'BEP派生器 kpi_bep_deriver.py(voucher_details 6401/6001 累计)', '财务部', 'monthly', '%', 0, + 'ratio', NULL, NULL, NULL, + 'strategic', 'active', 'finance-bot', 'Epic2', 1 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM kpi_definitions WHERE entity_id = 2 AND kpi_code = 'F_BEP_VC_RATE' +); diff --git a/frontend/src/components/BEPBreakEvenCard.vue b/frontend/src/components/BEPBreakEvenCard.vue index 298b96b5..cabea065 100644 --- a/frontend/src/components/BEPBreakEvenCard.vue +++ b/frontend/src/components/BEPBreakEvenCard.vue @@ -81,10 +81,10 @@
📐 公式
{{ card.formula }}
- 账面:固定成本{{ fmt(inputs.admin_avg) }}万 ÷ {{ fmt(inputs.cm_pct, 2) }}% = 保本{{ fmt(current.bep_revenue) }}万(管理费含折旧) + 账面:固定成本{{ fmt(inputs.admin_avg) }}万 ÷ {{ fmt(inputs.cm_pct, 2) }}% = 保本{{ fmt(current.bep_revenue) }}万(固定成本含折旧)
- 现金:管理费{{ fmt(inputs.admin_avg) }} − 折旧{{ fmt(inputs.dep_avg) }} = {{ fmt(current.admin_avg) }}万 ÷ {{ fmt(inputs.cm_pct, 2) }}% = 保本{{ fmt(current.bep_revenue) }}万 + 现金:固定成本{{ fmt(inputs.admin_avg) }} − 折旧{{ fmt(inputs.dep_avg) }} = {{ fmt(current.admin_avg) }}万 ÷ {{ fmt(inputs.cm_pct, 2) }}% = 保本{{ fmt(current.bep_revenue) }}万
@@ -96,9 +96,10 @@
📊 输入KPI
- 月管理费 {{ fmt(inputs.admin_avg) }}万 - 月收入(5001) {{ fmt(inputs.sales_avg) }}万 - 渠补率 {{ fmt(inputs.rebate_pct, 2) }}% + 月固定成本 {{ fmt(inputs.admin_avg) }}万 + 月收入 {{ fmt(inputs.sales_avg) }}万 + 变动成本率 {{ fmt(inputs.vc_pct, 2) }}% + 渠补率 {{ fmt(inputs.rebate_pct, 2) }}% 折旧 {{ fmt(inputs.dep_avg) }}万/月
数据源:voucher_details 凭证聚合(YTD月均口径)· {{ card.period }} · 纯只读
@@ -149,8 +150,8 @@ const lvlText = computed(() => ({ green: '已保本', yellow: '未达保本', re const gapColor = computed(() => (current.value.gap <= 0 ? '#67c23a' : current.value.gap <= 50 ? '#e6a23c' : '#f56c6c')) const modeNote = computed(() => mode.value === 'cash' - ? '固定成本=管理费−折旧(现金视角)' - : '固定成本=管理费(含折旧,账面视角)' + ? '固定成本=固定费用合计−折旧(现金视角)' + : '固定成本=固定费用合计(含折旧,账面视角)' ) const insight = computed(() => {