fix(bep-caliber): expense and profit sums use net (debit minus credit) under carry_forward=0
ci/woodpecker/push/main Pipeline failed
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:
@@ -27,8 +27,8 @@ router = APIRouter(prefix="/api/cma/dashboard", tags=["驾驶舱"],
|
|||||||
# ── 本量利·保本点 卡片只读数据(cma-bep-kpi Phase2 2026-09-08)──
|
# ── 本量利·保本点 卡片只读数据(cma-bep-kpi Phase2 2026-09-08)──
|
||||||
# 口径与 backend/scripts/kpi_bep_deriver.py 完全一致:
|
# 口径与 backend/scripts/kpi_bep_deriver.py 完全一致:
|
||||||
# 月销 = voucher_details subject 5001(主营业务收入) 贷方 YTD 月均(万元)
|
# 月销 = voucher_details subject 5001(主营业务收入) 贷方 YTD 月均(万元)
|
||||||
# 月管理费 = 5602% 借方 YTD 月均(账面口径含折旧)
|
# 月管理费 = 5602% 借−贷 净额 YTD 月均(carry_forward=0 行域;账面口径含折旧)
|
||||||
# 折旧 = 560213 借方 YTD 月均
|
# 折旧 = 560213 借−贷 净额 YTD 月均(同上;口径4 见 _decisions/cma-expense-profit-net-caliber-20261002.md)
|
||||||
# 渠补率 = 厂家政策 84.61%(财务Bot权威口径)
|
# 渠补率 = 厂家政策 84.61%(财务Bot权威口径)
|
||||||
# 保本额 = 月固定成本 ÷ (1-渠补率)
|
# 保本额 = 月固定成本 ÷ (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:
|
field: str, subject_filter=None) -> float:
|
||||||
"""聚合 voucher_details 金额(元),subject_filter: ('eq', code) / ('like', prefix)
|
"""聚合 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 均值。
|
作废/撤单标记过滤(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.entity_id == entity_id,
|
||||||
VoucherDetail.carry_forward == 0,
|
VoucherDetail.carry_forward == 0,
|
||||||
VoucherDetail.period >= period_from,
|
VoucherDetail.period >= period_from,
|
||||||
@@ -131,9 +136,9 @@ def get_bep_card(
|
|||||||
sales_avg = _bep_sum_field(db, entity_id, period_from, period_to,
|
sales_avg = _bep_sum_field(db, entity_id, period_from, period_to,
|
||||||
"credit_amount", ("eq", BEP_SALES_SUBJECT)) / 10000.0 / months
|
"credit_amount", ("eq", BEP_SALES_SUBJECT)) / 10000.0 / months
|
||||||
admin_avg = _bep_sum_field(db, entity_id, period_from, period_to,
|
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,
|
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
|
rebate_pct = BEP_REBATE_POLICY_PCT
|
||||||
cm_pct = 100.0 - rebate_pct
|
cm_pct = 100.0 - rebate_pct
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
|
|
||||||
口径(与财务Bot报告一致):
|
口径(与财务Bot报告一致):
|
||||||
- 销售收入: voucher_details.subject_code='5001'(主营业务收入) 贷方合计,当年1月至目标月 YTD 月均
|
- 销售收入: voucher_details.subject_code='5001'(主营业务收入) 贷方合计,当年1月至目标月 YTD 月均
|
||||||
- 管理费: subject_code LIKE '5602%'(管理费用,含560213折旧) 借方合计,YTD 月均 → 账面口径
|
- 管理费: subject_code LIKE '5602%'(管理费用,含560213折旧) 借−贷净额合计(carry_forward=0),YTD 月均 → 账面口径
|
||||||
- 折旧: subject_code = '560213'(管理费用-折旧叶子) 借方合计,YTD 月均 → 现金口径剔除项(eq精确匹配,防5602130误含)
|
- 折旧: subject_code = '560213'(管理费用-折旧叶子) 借−贷净额合计(carry_forward=0),YTD 月均 → 现金口径剔除项(eq精确匹配,防5602130误含)
|
||||||
- 渠补率: 厂家政策定死 84.61% (571103渠补支出/5001销售, 1-7月累计凭证验证≈84.0%~84.6%)
|
- 渠补率: 厂家政策定死 84.61% (571103渠补支出/5001销售, 1-7月累计凭证验证≈84.0%~84.6%)
|
||||||
- 边际贡献率 = 1 - 渠补率 = 15.39%
|
- 边际贡献率 = 1 - 渠补率 = 15.39%
|
||||||
- 目标月: --period 显式指定;默认自动选最近「已结账」完整月(当月跳过)。
|
- 目标月: --period 显式指定;默认自动选最近「已结账」完整月(当月跳过)。
|
||||||
@@ -137,8 +137,15 @@ def _month_range(period: str):
|
|||||||
|
|
||||||
def _sum_field(db, entity_id: int, period_from: str, period_to: str,
|
def _sum_field(db, entity_id: int, period_from: str, period_to: str,
|
||||||
field: str, subject_filter=None) -> float:
|
field: str, subject_filter=None) -> float:
|
||||||
"""聚合 voucher_details 金额(元)。subject_filter: ('eq', code) / ('like', prefix)"""
|
"""聚合 voucher_details 金额(元)。subject_filter: ('eq', code) / ('like', prefix)
|
||||||
q = db.query(func.sum(getattr(VoucherDetail, field))).filter(
|
|
||||||
|
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.entity_id == entity_id,
|
||||||
VoucherDetail.carry_forward == 0,
|
VoucherDetail.carry_forward == 0,
|
||||||
VoucherDetail.period >= period_from,
|
VoucherDetail.period >= period_from,
|
||||||
@@ -299,9 +306,9 @@ def run_derive(db, entity_id: int, period: str, rebate_rate_pct: float,
|
|||||||
|
|
||||||
# 1. 取数(元 → 万元)
|
# 1. 取数(元 → 万元)
|
||||||
sales_sum_w = _sum_field(db, entity_id, period_from, period_to, "credit_amount", ("eq", SALES_CODE)) / 10000.0
|
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
|
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, "debit_amount", ("eq", REBATE_CODE)) / 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, "debit_amount", ("eq", DEP_SUBJECT)) / 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
|
sales_avg = sales_sum_w / months
|
||||||
admin_avg = admin_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
|
||||||
Reference in New Issue
Block a user