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 勾稽自检 ───────────────────────────────────────────────────────────