From a52faa90a6fc6bae47c79305bcb6a7805974870d Mon Sep 17 00:00:00 2001 From: Hermes Fullstack Date: Fri, 16 Oct 2026 18:22:39 +0800 Subject: [PATCH] =?UTF-8?q?fix(dashboard):=20bep-card=20terms=20=E5=88=86?= =?UTF-8?q?=E6=94=AF=E8=A1=A5=E5=AD=97=E6=AE=B5=E5=88=AB=E5=90=8D=E6=98=A0?= =?UTF-8?q?=E5=B0=84=EF=BC=88=E4=BF=AE=20289cc8f=20=E5=BC=95=E5=85=A5?= =?UTF-8?q?=E7=9A=84=20e2=20500=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 289cc8f(§4-1 博海固定成本仅借方)在 dashboard._bep_sum_field 新增 terms 分支, 把 bep_caliber 的 terms 记法字段名(debit/credit)原样传给 getattr(VoucherDetail, field) ⇒ AttributeError: type object 'VoucherDetail' has no attribute 'debit' ⇒ GET /api/cma/dashboard/bep-card(entity=2 博海)稳定 500(entity=1 走 filters 分支不受影响)。 修复:terms 分支内做字段名映射 debit→debit_amount / credit→credit_amount(与 profit_lines.FIELD_MAP、kpi_bep_deriver._TERM_FIELD 同式),再递归聚合。 回归测试:tests/test_profit_summary_real_caliber_20261014.py 新增 test_dashboard_card_e2_terms_mode_field_alias(修复前 RED:复现同一 AttributeError; 修复后 GREEN:e2 固定成本 98,000 元 / 卡面 9.80 万)。 --- backend/app/api/dashboard.py | 11 +++++++++- ...st_profit_summary_real_caliber_20261014.py | 21 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/backend/app/api/dashboard.py b/backend/app/api/dashboard.py index 3f491122..7df67c72 100644 --- a/backend/app/api/dashboard.py +++ b/backend/app/api/dashboard.py @@ -72,6 +72,13 @@ BEP_DEP_SUBJECT = "560213" # (酣客 e1 = 84.61;博海 e2 = None,因无渠补科目)。端点各处直接取口径值,不做回落。 +# bep_caliber 的带号科目项(terms)以 `debit`/`credit` 命名,本模块聚合用的是模型列名 +# `debit_amount`/`credit_amount` ⇒ 必须做字段名映射。 +# 2026-10-16 修复(P0):289cc8f2 引入的 terms 分支把 'debit' 原样传入 ⇒ +# `getattr(VoucherDetail, 'debit')` AttributeError ⇒ e2 博海 `GET /api/cma/dashboard/bep-card` 500。 +_BEP_FIELD_ALIAS = {"debit": "debit_amount", "credit": "credit_amount", "net": "net"} + + def _bep_sum_field(db, entity_id: int, period_from: str, period_to: str, field: str, subject_filter=None, *, exclude_closing: bool = False, subject_filters=None, terms=None) -> float: @@ -91,8 +98,10 @@ def _bep_sum_field(db, entity_id: int, period_from: str, period_to: str, if terms: total = 0.0 for sign, code, tfield in terms: + # terms 记法字段名 → 模型列名(缺此映射 ⇒ getattr(VoucherDetail,'debit') AttributeError) + col_field = _BEP_FIELD_ALIAS.get(str(tfield), str(tfield)) total += (1.0 if sign == "+" else -1.0) * _bep_sum_field( - db, entity_id, period_from, period_to, tfield, + db, entity_id, period_from, period_to, col_field, ("like", str(code)[:-1]) if str(code).endswith("%") else ("eq", code), exclude_closing=exclude_closing) return total diff --git a/backend/tests/test_profit_summary_real_caliber_20261014.py b/backend/tests/test_profit_summary_real_caliber_20261014.py index 6369a9ac..1c3e06dc 100644 --- a/backend/tests/test_profit_summary_real_caliber_20261014.py +++ b/backend/tests/test_profit_summary_real_caliber_20261014.py @@ -228,6 +228,27 @@ class TestCaliberDriven: D._sum_field(db, 1, PERIOD, PERIOD, "net", ("like", "5602")) == 120_000.0 assert PL.sum_field(db, 1, PERIOD, PERIOD, "credit_amount", ("eq", "5001")) == 2_634_900.0 + def test_dashboard_card_e2_terms_mode_field_alias(self, db: Session): + """289cc8f 回归(P0):e2 固定成本走 terms(仅借方)时,`dashboard._bep_sum_field` + 必须把 terms 记法字段名(debit/credit)映射到模型列名(debit_amount/credit_amount); + 否则 `getattr(VoucherDetail, 'debit')` AttributeError ⇒ `GET /dashboard/bep-card` 500。 + """ + seed_e2(db) + from app.api.dashboard import _bep_sum_field, _bep_inputs + from app.utils.bep_caliber import fixed_cost_spec + + cal2 = ENTITY_CALIBERS[2] + mode, spec = fixed_cost_spec(cal2) + assert mode == "terms", spec + raw = _bep_sum_field(db, 2, PERIOD, PERIOD, "net", terms=spec, + exclude_closing=cal2["exclude_closing"]) + # §4-1 仅借方:6601 60,000 + 6602 30,000 + 6603 8,000 = 98,000 元 + # (6602 贷 5,000 / 6603 贷 20,000 不进固定;summary 含「结转」腿被剔除) + assert raw == 98_000.0, raw + inp = _bep_inputs(db, 2, cal2, PERIOD, PERIOD, 1) + assert inp["sales_avg"] == 100.00, inp + assert inp["admin_avg"] == 9.80, inp + # ── L2 勾稽自检 ───────────────────────────────────────────────────────────