"""口径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