fix(bep-caliber): expense and profit sums use net (debit minus credit) under carry_forward=0
ci/woodpecker/push/main Pipeline failed

ASK-20261002-06 fix04u: admin(5602%) and depreciation(560213) take net amount in both the dashboard card helper and the BEP deriver; sales (5001) keeps credit_amount. Verbatim per proposal section 2. No DB writes, no kpi_values backfill.
This commit is contained in:
Hermes CI Fix
2026-10-02 11:22:38 +08:00
parent 6c1ea85d71
commit 6be903fbc9
3 changed files with 121 additions and 12 deletions
+10 -5
View File
@@ -27,8 +27,8 @@ router = APIRouter(prefix="/api/cma/dashboard", tags=["驾驶舱"],
# ── 本量利·保本点 卡片只读数据(cma-bep-kpi Phase2 2026-09-08)──
# 口径与 backend/scripts/kpi_bep_deriver.py 完全一致:
# 月销 = voucher_details subject 5001(主营业务收入) 贷方 YTD 月均(万元)
# 月管理费 = 5602% 借方 YTD 月均(账面口径含折旧)
# 折旧 = 560213 借方 YTD 月均
# 月管理费 = 5602% 借−贷 净额 YTD 月均(carry_forward=0 行域;账面口径含折旧)
# 折旧 = 560213 借−贷 净额 YTD 月均(同上;口径4 见 _decisions/cma-expense-profit-net-caliber-20261002.md)
# 渠补率 = 厂家政策 84.61%(财务Bot权威口径)
# 保本额 = 月固定成本 ÷ (1-渠补率)
# 账面口径(含折旧) / 现金口径(剔除折旧) 双口径只读返回,无写操作。
@@ -46,9 +46,14 @@ 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)
field="net" = 借−贷 净额(口径4:费用/损益一律净额)。
⚠️ 净额只在 carry_forward==0 行域成立(含结转腿时损益类净额恒 0)
—— 见 /srv/hermes/wisdom/_decisions/cma-expense-profit-net-caliber-20261002.md。
作废/撤单标记过滤(2026-09-14 收口): 已作废凭证不参与 BEP 均值。
"""
q = db.query(func.sum(getattr(VoucherDetail, field))).filter(
expr = (VoucherDetail.debit_amount - VoucherDetail.credit_amount
if field == "net" else getattr(VoucherDetail, field))
q = db.query(func.sum(expr)).filter(
VoucherDetail.entity_id == entity_id,
VoucherDetail.carry_forward == 0,
VoucherDetail.period >= period_from,
@@ -131,9 +136,9 @@ def get_bep_card(
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
"net", ("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
"net", ("eq", BEP_DEP_SUBJECT)) / 10000.0 / months
rebate_pct = BEP_REBATE_POLICY_PCT
cm_pct = 100.0 - rebate_pct
+14 -7
View File
@@ -9,8 +9,8 @@
口径(与财务Bot报告一致):
- 销售收入: voucher_details.subject_code='5001'(主营业务收入) 贷方合计,当年1月至目标月 YTD 月均
- 管理费: subject_code LIKE '5602%'(管理费用,含560213折旧) 借方合计,YTD 月均 → 账面口径
- 折旧: subject_code = '560213'(管理费用-折旧叶子) 借方合计,YTD 月均 → 现金口径剔除项(eq精确匹配,防5602130误含)
- 管理费: subject_code LIKE '5602%'(管理费用,含560213折旧) 借−贷净额合计(carry_forward=0),YTD 月均 → 账面口径
- 折旧: subject_code = '560213'(管理费用-折旧叶子) 借−贷净额合计(carry_forward=0),YTD 月均 → 现金口径剔除项(eq精确匹配,防5602130误含)
- 渠补率: 厂家政策定死 84.61% (571103渠补支出/5001销售, 1-7月累计凭证验证≈84.0%~84.6%)
- 边际贡献率 = 1 - 渠补率 = 15.39%
- 目标月: --period 显式指定;默认自动选最近「已结账」完整月(当月跳过)。
@@ -137,8 +137,15 @@ def _month_range(period: str):
def _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(
"""聚合 voucher_details 金额(元)。subject_filter: ('eq', code) / ('like', prefix)
field="net" = 借−贷 净额(口径4:费用/损益一律净额)。
⚠️ 净额只在 carry_forward==0 行域成立(含结转腿时损益类净额恒 0)
—— 见 /srv/hermes/wisdom/_decisions/cma-expense-profit-net-caliber-20261002.md。
"""
expr = (VoucherDetail.debit_amount - VoucherDetail.credit_amount
if field == "net" else getattr(VoucherDetail, field))
q = db.query(func.sum(expr)).filter(
VoucherDetail.entity_id == entity_id,
VoucherDetail.carry_forward == 0,
VoucherDetail.period >= period_from,
@@ -299,9 +306,9 @@ def run_derive(db, entity_id: int, period: str, rebate_rate_pct: float,
# 1. 取数(元 → 万元)
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
admin_sum_w = _sum_field(db, entity_id, period_from, period_to, "net", ("like", ADMIN_PREFIX)) / 10000.0
rebate_sum_w = _sum_field(db, entity_id, period_from, period_to, "net", ("eq", REBATE_CODE)) / 10000.0
dep_sum_w = _sum_field(db, entity_id, period_from, period_to, "net", ("eq", DEP_SUBJECT)) / 10000.0
sales_avg = sales_sum_w / months
admin_avg = admin_sum_w / months
@@ -0,0 +1,97 @@
"""口径4「费用取净额」单测 — ASK-20261002-06(2026-10-02)
口径依据: /srv/hermes/wisdom/_decisions/cma-expense-profit-net-caliber-20261002.md
「任何费用/利润取数一律净额(借−贷)」
覆盖两条实现面取数助手(改前均只认 debit/credit 单边字段):
· app/api/dashboard.py::_bep_sum_field(卡片现算面)
· scripts/kpi_bep_deriver.py::_sum_field(派生落库写入面)
断言要点(含负对照,退回 debit_amount 口径时本文件必红):
1. field="net" = 借−贷(行域上存在贷方冲销行 ⇒ 净额 ≠ 借方合计);
2. carry_forward==0 过滤仍在位(结转腿行不参与);
3. 收入侧 sales 仍取 credit_amount,未改为净额(改净额会变负)。
4. `5602%` LIKE 前缀天然包含 `560213`(账面口径含折旧),故 admin 净额 = 两科目之和。
"""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import datetime # noqa: E402
from app.models import VoucherDetail # noqa: E402
from app.api.dashboard import _bep_sum_field # noqa: E402
from scripts import kpi_bep_deriver as D # noqa: E402
D_DATE = datetime.datetime(2026, 8, 31)
PERIOD = "2026-08"
def _row(db, no, code, debit=0.0, credit=0.0, cf=0):
db.add(VoucherDetail(entity_id=1, voucher_no=str(no), voucher_date=D_DATE,
subject_code=code, subject_name="单测科目",
debit_amount=debit, credit_amount=credit,
summary="口径4 单测", carry_forward=cf, period=PERIOD))
db.flush()
def _fixture(db):
"""借贷双侧行域 + 一条结转腿(carry_forward=1)行。
560202 : 借 1000 / 贷 200 → 净额 800(借方合计 1000)
560213 : 借 500 / 贷 100 → 净额 400(借方合计 500)
5001 : 贷 8000 / 借 0 → 收入仍取贷方 8000(取净额 = -8000)
560202 cf=1 : 借 9999 → 结转腿,任何口径下都不得计入
LIKE '5602%' 命中 560202 + 560213(账面口径含折旧)⇒ admin 净额 = 1200。
"""
_row(db, 91001, "560202", debit=1000.0)
_row(db, 91002, "560202", credit=200.0)
_row(db, 91003, "560213", debit=500.0)
_row(db, 91004, "560213", credit=100.0)
_row(db, 91005, "5001", credit=8000.0)
_row(db, 91006, "560202", debit=9999.0, cf=1)
def test_dashboard_net_caliber_admin_and_dep(db):
"""改后:dashboard._bep_sum_field 支持 field='net'=借−贷;借方口径仍可用(负对照)。"""
_fixture(db)
net_admin = _bep_sum_field(db, 1, PERIOD, PERIOD, "net", ("like", "5602"))
debit_admin = _bep_sum_field(db, 1, PERIOD, PERIOD, "debit_amount", ("like", "5602"))
net_dep = _bep_sum_field(db, 1, PERIOD, PERIOD, "net", ("eq", "560213"))
debit_dep = _bep_sum_field(db, 1, PERIOD, PERIOD, "debit_amount", ("eq", "560213"))
assert abs(net_admin - 1200.0) < 1e-6, net_admin # (1000-200) + (500-100)
assert abs(net_dep - 400.0) < 1e-6, net_dep # 500 - 100
# 负对照:退回 debit_amount 口径时上面的 net 断言必红(1500/500 ≠ 1200/400)
assert abs(debit_admin - 1500.0) < 1e-6, debit_admin
assert abs(debit_dep - 500.0) < 1e-6, debit_dep
def test_dashboard_net_excludes_carry_forward_rows(db):
"""carry_forward==0 过滤在位:结转腿行(cf=1,借 9999)不参与。"""
_fixture(db)
net_admin = _bep_sum_field(db, 1, PERIOD, PERIOD, "net", ("like", "5602"))
assert abs(net_admin - 1200.0) < 1e-6, net_admin # 含 cf=1 行则会是 1200+9999
def test_dashboard_sales_still_credit_amount(db):
"""收入侧未改:5001 仍取 credit_amount(若改净额 → 负数)。"""
_fixture(db)
sales_credit = _bep_sum_field(db, 1, PERIOD, PERIOD, "credit_amount", ("eq", "5001"))
sales_net = _bep_sum_field(db, 1, PERIOD, PERIOD, "net", ("eq", "5001"))
assert abs(sales_credit - 8000.0) < 1e-6, sales_credit
assert sales_net < 0, sales_net # 这正是「收入不得取净额」的反证
def test_deriver_net_caliber_parity(db):
"""派生器 _sum_field 与卡片助手同口径(两文件同批改的目的:避免同页两口径)。"""
_fixture(db)
d_net_admin = D._sum_field(db, 1, PERIOD, PERIOD, "net", ("like", "5602"))
d_net_dep = D._sum_field(db, 1, PERIOD, PERIOD, "net", ("eq", "560213"))
d_net_rebate = D._sum_field(db, 1, PERIOD, PERIOD, "net", ("eq", "571103"))
assert abs(d_net_admin - 1200.0) < 1e-6, d_net_admin
assert abs(d_net_dep - 400.0) < 1e-6, d_net_dep
assert abs(d_net_rebate - 0.0) < 1e-6, d_net_rebate # 本夹具无 571103 行
assert abs(d_net_admin - _bep_sum_field(db, 1, PERIOD, PERIOD, "net", ("like", "5602"))) < 1e-6