diff --git a/backend/app/api/dashboard.py b/backend/app/api/dashboard.py index 3c67c34b..3f491122 100644 --- a/backend/app/api/dashboard.py +++ b/backend/app/api/dashboard.py @@ -13,7 +13,7 @@ from app.utils.voucher_guard import apply_voucher_guard from app.utils.threshold_parser import eval_threshold from app.utils.alert_entity_scope import month_period_clause # BEP 实体口径单一事实源(2026-10-03 扩面:酣客 entity1 / 博海 entity2 同表驱动派生器与卡面) -from app.utils.bep_caliber import CLOSING_SUMMARY_LIKE, caliber_or_none +from app.utils.bep_caliber import CLOSING_SUMMARY_LIKE, caliber_or_none, fixed_cost_spec # 「实测区 / 预估区」分区口径单一事实源(财务裁定 2026-10-02,任总「把 H2 预测行与实际分离」) # —— 预估行(data_status='estimated' 或 source_type='forecast')禁进实测汇总 / 趋势序列 / # 预警依据;仅 include_forecast=true 时以独立字段(forecast_value / is_forecast)单列返回。 @@ -74,7 +74,7 @@ BEP_DEP_SUBJECT = "560213" 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) -> float: + subject_filters=None, terms=None) -> float: """聚合 voucher_details 金额(元),subject_filter: ('eq', code) / ('like', prefix) field="net" = 借−贷 净额(口径4:费用/损益一律净额)。 @@ -82,9 +82,20 @@ def _bep_sum_field(db, entity_id: int, period_from: str, period_to: str, —— 见 /srv/hermes/wisdom/_decisions/cma-expense-profit-net-caliber-20261002.md。 exclude_closing(2026-10-03):该账套结转腿未标 carry_forward 时(博海:全账套 cf 恒 0), 须按 summary NOT LIKE '%结转%' 显式剔除,否则已结账期间净额恒 0。 - subject_filters:多条 OR(固定成本 6601+6602+6603)。 + subject_filters:多条 OR(固定成本前缀口径;酣客 5601/5602/5603 借−贷净额)。 + terms(2026-10-15,ASK-20261014-04 §4-1):**带号科目项** [(sign, code, field), ...] + —— `bep_caliber.fixed_cost_spec()` 解析出的博海形态(6601/6602/6603 **仅借方**); + 每项独立聚合后按符号相加,行域(账套/cf=0/剔结转腿/剔作废)与上面完全一致。 作废/撤单标记过滤(2026-09-14 收口): 已作废凭证不参与 BEP 均值。 """ + if terms: + total = 0.0 + for sign, code, tfield in terms: + total += (1.0 if sign == "+" else -1.0) * _bep_sum_field( + db, entity_id, period_from, period_to, tfield, + ("like", str(code)[:-1]) if str(code).endswith("%") else ("eq", code), + exclude_closing=exclude_closing) + return total expr = (VoucherDetail.debit_amount - VoucherDetail.credit_amount if field == "net" else getattr(VoucherDetail, field)) q = db.query(func.sum(expr)).filter( @@ -115,12 +126,21 @@ def _bep_sum_field(db, entity_id: int, period_from: str, period_to: str, def _bep_inputs(db, entity_id: int, cal: dict, period_from: str, period: str, months: int) -> dict: - """卡面展示用输入(万元/百分比)——按实体口径现算,与派生器同源同式。""" + """卡面展示用输入(万元/百分比)——按实体口径现算,与派生器同源同式。 + + 固定成本(admin):经 `bep_caliber.fixed_cost_spec()` **唯一解析口**取科目构成 —— + e1 酣客 = 5601%/5602%/5603% 借−贷净额;e2 博海 = 6601/6602/6603 **仅借方**(§4-1 批后口径)。 + """ + fix_mode, fix_spec = fixed_cost_spec(cal) sales = _bep_sum_field(db, entity_id, period_from, period, cal["revenue_field"], cal["revenue_filter"], exclude_closing=cal["exclude_closing"]) / 10000.0 - admin = _bep_sum_field(db, entity_id, period_from, period, "net", - subject_filters=cal["fixed_filters"], - exclude_closing=cal["exclude_closing"]) / 10000.0 + if fix_mode == "terms": + admin = _bep_sum_field(db, entity_id, period_from, period, "net", + terms=fix_spec, exclude_closing=cal["exclude_closing"]) / 10000.0 + else: + admin = _bep_sum_field(db, entity_id, period_from, period, "net", + subject_filters=fix_spec, + exclude_closing=cal["exclude_closing"]) / 10000.0 dep = _bep_sum_field(db, entity_id, period_from, period, cal["dep_field"], cal["dep_filter"], exclude_closing=cal["exclude_closing"]) / 10000.0 return { diff --git a/backend/app/api/reports.py b/backend/app/api/reports.py index 5d91d350..30da41fe 100644 --- a/backend/app/api/reports.py +++ b/backend/app/api/reports.py @@ -29,7 +29,7 @@ from app.utils.dupont_basis import ( ) from app.utils.voucher_guard import apply_voucher_guard from app.utils.profit_lines import (BOTTOM_LABEL, UNIT, build_items, compute_lines, - prev_period_of, reconcile) + import_batches, prev_period_of, reconcile) from app.services import report_reclass from app.deps import get_entity_id, assert_entity_slug_scope, resolve_session_slug import logging @@ -77,6 +77,8 @@ def get_profit_summary( "entity_id": entity_id, "unit": UNIT, "bottom_label": BOTTOM_LABEL, + "desc": ("基于管理会计视角的利润结构分析:营业收入 → 变动成本 → 边际贡献 → 固定成本 → " + f"{BOTTOM_LABEL}(真实科目取数,按账套口径现算)"), "source": "voucher_details 真实凭证明细发生额(按 entity 隔离;剔结转腿 + 作废/撤单行)", "available": cur["available"], "missing": cur["missing"], @@ -84,6 +86,10 @@ def get_profit_summary( "prev_missing": prev["missing"], "caliber": cur.get("caliber"), "items": build_items(cur, prev), + # §4-1:财务收益(博海 6603 贷方现金折扣收益等)单列备查 —— 不进固定成本、不参与勾稽 + "finance_income": cur.get("finance_income"), + # 4-5 备注(**纯展示**:数据批次 / 导入时刻,取 import_logs)—— 不参与任何计算 + "data_batches": import_batches(db, entity_id, period), "reconciliation": reconcile(values.get("revenue"), values.get("variable_cost"), values.get("fixed_cost"), values.get("result")), } diff --git a/backend/app/utils/bep_caliber.py b/backend/app/utils/bep_caliber.py index a5579091..3cd564be 100644 --- a/backend/app/utils/bep_caliber.py +++ b/backend/app/utils/bep_caliber.py @@ -9,11 +9,12 @@ | 项 | entity 1 陕西酣客 | entity 2 博海 | |---|---|---| -| 变动成本(分子) | **(Σ5401借 + Σ5403借 + Σ571103借 − Σ530103贷)**〔⑤口径统一〕 | Σ6401 借方(主营业务成本) | -| 收入基数(分母) | Σ5001 贷方(主营业务收入) | Σ6001 贷方(主营业务收入) | -| 变动成本率 | 上式 ÷ Σ5001贷(**完整口径**,非渠补率) | Σ6401借 ÷ Σ6001贷(主营业务成本率) | +| 变动成本(分子) | **(Σ5401借 + Σ5403借 + Σ571103借 − Σ530103贷)**〔⑤口径统一〕 | **Σ6401借 + Σ6403借**〔ASK-20261014-04 §4-2 批后口径〕 | +| 收入基数(分母) | Σ5001 贷方(主营业务收入) | Σ6001 贷方(主营业务收入,**不含** 6051)〔§4-4 维持现行〕 | +| 变动成本率 | 上式 ÷ Σ5001贷(**完整口径**,非渠补率) | 上式 ÷ Σ6001贷(主营业务成本率 + 税金及附加) | | 实测渠补率(F_BEP_REBATE) | Σ571103借 ÷ Σ5001贷(**保留**为渠补单一事实源,**不再**充当变动成本率) | 无(None) | -| 固定成本 | `5601%` + `5602%` + `5603%` 借−贷净额〔⑤扩容〕 | `6601+6602+6603`(销售/管理/财务费用)借−贷净额 | +| 固定成本 | `5601%` + `5602%` + `5603%` 借−贷净额〔⑤扩容〕 | `6601` + `6602` + `6603` **仅借方**〔ASK-20261014-04 §4-1 批后口径〕 | +| 财务收益(单列备查,**不进**固定成本) | 无(`560303` 利息收入 2026 零发生) | `6603` **贷方**(现金折扣收益等 64.40 万)〔§4-1〕 | | 折旧剔除(现金口径) | `560213`(管理费用-折旧叶子) | `1602` 累计折旧**当期贷方发生额**(见下) | | 本期利润结转科目 | `3103` 本年利润 | `4103` 本年利润 | | 结账信号次选(落盘) | `/root/WB_WS/_data/zxt-hanke/{YYYY.MM}/check.md` | 无(只用 DB 信号) | @@ -33,6 +34,17 @@ 损益类科目「借−贷」净额在已结账月份恒为 0,会把指标静默清零 (口径见 `/srv/hermes/wisdom/_decisions/cma-expense-profit-net-caliber-20261002.md`)。 故博海口径 `exclude_closing=True`。 + +⑥ ASK-20261014-04 批后口径(任总 2026-10-14 11:20「放行4」= 放行 4-1 / 4-2 / 4-5;4-3 / 4-4 维持现行): + · §4-1(放行)博海固定成本**弃「借−贷净额」**:`6601` / `6602` / `6603` **仅借方**进固定成本 + (2026-01..09 = 86.95 + 39.09 + 7.95 = **133.98 万**;净额口径为 69.58 万); + `6603` **贷方**(现金折扣收益等 64.40 万)**作收益项单列**,`finance_income_terms`,**不再冲减固定成本** + —— 净额口径的病灶是 2026-03 单月固定成本被折扣收益打成 **−9.02 万**(负值),批后为正值。 + · §4-2(放行)博海变动成本追加 `6403 营业税金及附加`(2026-01..09 = 0.37 万)⇒ 530.17 万。 + · §4-3 / §4-4(维持现行)`571112 商品折扣` 不冲减酣客收入;边际贡献收入基数维持 e1=`5001` / + e2=`6001`(**不含** e2 `6051`)。 + · 落点唯一:本表 `ENTITY_CALIBERS`(含本头注);报表层 / 卡面 / 派生器**一律**经 + `fixed_cost_spec()` 解析,不得另写科目集。酣客(e1)本次**未改**,仍走 `fixed_filters` 净额。 """ CLOSING_SUMMARY_LIKE = "%结转%" @@ -54,7 +66,10 @@ ENTITY_CALIBERS = { rebate_terms=[("+", "571103", "debit")], revenue_field="credit_amount", revenue_filter=("eq", "5001"), fixed_filters=[("like", "5601"), ("like", "5602"), ("like", "5603")], - fixed_label="固定成本(5601+5602+5603)", + # §4-1 未改酣客:仍走「借−贷净额」前缀口径 ⇒ fixed_terms 显式 None(不隐式继承) + fixed_terms=None, + finance_income_terms=None, # 酣客 560303 利息收入 2026 零发生 ⇒ 无单列财务收益项 + fixed_label="固定成本(5601+5602+5603 借−贷净额)", dep_field="net", dep_filter=("eq", "560213"), closing_subject="3103", landing_dir=ZXT_LANDING_DIR_HANKE, @@ -63,13 +78,18 @@ ENTITY_CALIBERS = { ), 2: dict( name="博海", - margin_label="变动成本率(主营业务成本率)", - margin_desc="Σ6401借方 ÷ Σ6001贷方", - varcost_terms=[("+", "6401", "debit")], + margin_label="变动成本率(主营成本率+税金及附加)", + margin_desc="(Σ6401借+Σ6403借) ÷ Σ6001贷", + # §4-2(放行):追加 6403 营业税金及附加(与 e1 含 5403 对齐) + varcost_terms=[("+", "6401", "debit"), ("+", "6403", "debit")], rebate_terms=None, # 博海无渠补科目 revenue_field="credit_amount", revenue_filter=("eq", "6001"), - fixed_filters=[("like", "6601"), ("like", "6602"), ("like", "6603")], - fixed_label="固定成本(6601+6602+6603)", + # §4-1(放行):弃「借−贷净额」⇒ 带号科目项 + field=debit(仅借方); + # 6603 贷方(现金折扣收益)改走 finance_income_terms 单列,不再冲减固定成本 + fixed_terms=[("+", "6601", "debit"), ("+", "6602", "debit"), ("+", "6603", "debit")], + fixed_filters=None, # 显式置空:禁止两套口径并存(fixed_cost_spec 以 terms 优先) + finance_income_terms=[("+", "6603", "credit")], + fixed_label="固定成本(6601+6602+6603 借方)", dep_field="credit_amount", dep_filter=("eq", "1602"), closing_subject="4103", landing_dir=None, @@ -90,3 +110,33 @@ def caliber_for(entity_id: int) -> dict: if cal is None: raise KeyError(f"entity={entity_id} 未注册 BEP 口径(见 app/utils/bep_caliber.py)") return cal + + +# ── 固定成本 / 财务收益:**唯一解析口** ────────────────────────────────────── +# 报表层(app/utils/profit_lines.py)、卡面(app/api/dashboard.py)、派生器 +# (scripts/kpi_bep_deriver.py)三处一律经本函数取科目构成,禁各自写一套(防「卡面新口径 / +# KPI 旧口径」)。§4-1 后:e2 走 terms(`6601/6602/6603` 仅借方),e1 走 filters(借−贷净额)。 + +def fixed_cost_spec(cal: dict): + """固定成本取数规格 → (mode, spec)。 + + mode='terms' ⇒ spec = [(sign, code, field), ...](带号科目项;§4-1 后博海形态) + mode='filters' ⇒ spec = [(kind, code), ...](前缀/精确 OR + 借−贷净额;酣客形态) + """ + terms = cal.get("fixed_terms") + if terms: + return "terms", [tuple(t) for t in terms] + return "filters", [tuple(f) for f in (cal.get("fixed_filters") or [])] + + +def finance_income_terms(cal: dict) -> list: + """财务收益(单列备查,**不进**固定成本)带号科目项;无则空列表(§4-1 只涉博海)。""" + return [tuple(t) for t in (cal.get("finance_income_terms") or [])] + + +def caliber_digest() -> str: + """口径指纹(证据用):ENTITY_CALIBERS 的 md5 —— 口径一改指纹即变,便于「改前/改后」取证。""" + import hashlib + import json + blob = json.dumps(ENTITY_CALIBERS, sort_keys=True, ensure_ascii=False, default=str) + return hashlib.md5(blob.encode("utf-8")).hexdigest() diff --git a/backend/app/utils/profit_lines.py b/backend/app/utils/profit_lines.py index ff43be00..6faa9578 100644 --- a/backend/app/utils/profit_lines.py +++ b/backend/app/utils/profit_lines.py @@ -5,7 +5,11 @@ · 收入 :`caliber['revenue_field']` + `caliber['revenue_filter']`(e1=5001 贷方 / e2=6001 贷方) · 变动成本 :`caliber['varcost_terms']`(带号科目项;e1 = 5401借 + 5403借 + 571103借 − 530103贷) · 固定成本 :`caliber['fixed_terms']`(带号科目项,**优先**)否则 `caliber['fixed_filters']` - (前缀元组 → 派生为 `('+', '<前缀>%', 'net')` 借−贷净额;口径升级为 `fixed_terms` 后本层零改动自动跟随) + (前缀元组 → 派生为 `('+', '<前缀>%', 'net')` 借−贷净额);两者一律经 + `bep_caliber.fixed_cost_spec()` **唯一解析口**解析(报表/卡面/派生器同源)。 + §4-1 批后:博海走 terms(`6601/6602/6603` **仅借方**),酣客仍走 filters(净额)。 + · 财务收益 :`caliber['finance_income_terms']`(§4-1:博海 `6603` **贷方**)—— **单列备查**, + **不进**固定成本、不进末行、不参与勾稽(纯展示项)。 · 期间行域 :`carry_forward == 0` + `caliber['exclude_closing']`(结转腿未标账套须显式剔 `summary LIKE '%结转%'`) · 作废行域 :`apply_voucher_guard`(`voucher_void_marks`,全库唯一实现) @@ -28,8 +32,10 @@ from typing import Dict, List, Optional, Tuple from sqlalchemy import func, or_ -from app.models import VoucherDetail -from app.utils.bep_caliber import CLOSING_SUMMARY_LIKE, caliber_or_none +from app.models import ImportLog, VoucherDetail +from app.utils.bep_caliber import (CLOSING_SUMMARY_LIKE, caliber_or_none, + finance_income_terms as cal_finance_income_terms, + fixed_cost_spec) from app.utils.voucher_guard import apply_voucher_guard # ── 展示口径 ──────────────────────────────────────────────────────────────── @@ -166,14 +172,16 @@ def revenue_terms(cal: dict) -> list: def fixed_cost_terms(cal: dict) -> Tuple[list, str]: - """固定成本科目项:优先 `fixed_terms`(带号项),否则由 `fixed_filters` 派生(前缀 + 借−贷净额)。 + """固定成本科目项:**经 `bep_caliber.fixed_cost_spec()` 唯一解析口**。 + 优先 `fixed_terms`(带号项,§4-1 后博海形态:仅借方),否则由 `fixed_filters` 派生 + (前缀 + 借−贷净额,酣客形态)。 返回 (terms, mode);mode 供报表标注当前口径形态(`fixed_terms` / `fixed_filters(net)`)。 """ - terms = cal.get("fixed_terms") - if terms: - return [tuple(t) for t in terms], "fixed_terms" - derived = [("+", str(code) + "%", "net") for _kind, code in (cal.get("fixed_filters") or [])] + mode, spec = fixed_cost_spec(cal) + if mode == "terms": + return [tuple(t) for t in spec], "fixed_terms" + derived = [("+", str(code) + "%", "net") for _kind, code in spec] return derived, "fixed_filters(net)" @@ -224,6 +232,7 @@ def compute_lines(db, entity_id: int, period_from: str, period_to: str) -> dict: exclude_closing = bool(cal.get("exclude_closing")) fix_terms, fix_mode = fixed_cost_terms(cal) rev_terms = revenue_terms(cal) + fin_terms = cal_finance_income_terms(cal) out["caliber"] = { "entity_id": entity_id, "name": cal.get("name"), @@ -233,6 +242,7 @@ def compute_lines(db, entity_id: int, period_from: str, period_to: str) -> dict: "varcost_terms": [list(t) for t in (cal.get("varcost_terms") or [])], "fixed_cost_terms": [list(t) for t in fix_terms], "fixed_mode": fix_mode, + "finance_income_terms": [list(t) for t in fin_terms], "exclude_closing": exclude_closing, "closing_subject": cal.get("closing_subject"), "digest": caliber_digest(cal), @@ -248,6 +258,10 @@ def compute_lines(db, entity_id: int, period_from: str, period_to: str) -> dict: var = sum_terms(db, entity_id, period_from, period_to, cal.get("varcost_terms") or [], exclude_closing=exclude_closing) fix = sum_terms(db, entity_id, period_from, period_to, fix_terms, exclude_closing=exclude_closing) + # 财务收益(§4-1:博海 6603 贷方)—— 单列备查,**不进**固定成本/末行,不参与勾稽 + fin = (sum_terms(db, entity_id, period_from, period_to, fin_terms, + exclude_closing=exclude_closing) if fin_terms + else {"total_yuan": 0.0, "parts": []}) rev_y = rev["total_yuan"] var_y = var["total_yuan"] @@ -255,7 +269,8 @@ def compute_lines(db, entity_id: int, period_from: str, period_to: str) -> dict: con_y = rev_y - var_y # 边际贡献 res_y = con_y - fix_y # 末行(营业利润) - out["rows"] = {"revenue": rev, "variable_cost": var, "fixed_cost": fix} + out["rows"] = {"revenue": rev, "variable_cost": var, "fixed_cost": fix, + "finance_income": fin} out["raw_yuan"] = {"revenue": round(rev_y, 2), "variable_cost": round(var_y, 2), "contribution": round(con_y, 2), "fixed_cost": round(fix_y, 2), "result": round(res_y, 2)} @@ -265,10 +280,39 @@ def compute_lines(db, entity_id: int, period_from: str, period_to: str) -> dict: "contribution": round(con_y / YUAN_PER_WAN, ROUND_DP), "fixed_cost": round(fix_y / YUAN_PER_WAN, ROUND_DP), "result": round(res_y / YUAN_PER_WAN, ROUND_DP)} + out["finance_income"] = { + "value": round(fin["total_yuan"] / YUAN_PER_WAN, ROUND_DP), + "raw_yuan": round(fin["total_yuan"], ROUND_DP), + "terms": fin["parts"], "unit": UNIT, + "name": "财务收益(单列备查)", + "basis": ("ENTITY_CALIBERS[entity]['finance_income_terms'](§4-1:6603 贷方 = 现金折扣收益等)" + ";单列备查,不进固定成本、不参与勾稽"), + } out["available"] = True return out +def import_batches(db, entity_id: int, period: str, limit: int = 5) -> list: + """数据批次 / 导入时刻(**纯展示备注**,4-5)—— 不参与任何计算。 + + 取自 `import_logs`(该账套 + 该期间),倒序最多 `limit` 条:批次号 / 行数 / 导入时刻。 + 无记录 ⇒ 空列表(页面不显示备注,不编造批次)。 + """ + try: + rows = (db.query(ImportLog) + .filter(ImportLog.entity_id == entity_id, ImportLog.period == period) + .order_by(ImportLog.id.desc()).limit(limit).all()) + except Exception: # noqa: BLE001 —— 展示项:取数失败不得影响报表主体 + return [] + return [{"batch": r.batch, + "rows": int(r.success_rows if r.success_rows is not None else (r.total_rows or 0)), + "total_rows": int(r.total_rows or 0), + "failed_rows": int(r.failed_rows or 0), + "import_type": r.import_type, + "imported_at": r.created_at.strftime("%Y-%m-%d %H:%M") if r.created_at else None, + "filename": r.filename} for r in rows] + + def reconcile(revenue: Optional[float], variable_cost: Optional[float], fixed_cost: Optional[float], result: Optional[float], tol: float = RECON_TOL_WAN) -> dict: @@ -358,7 +402,10 @@ def build_items(cur: dict, prev: dict, bottom_label: str = BOTTOM_LABEL) -> list "ratio": ratio_of("fixed_cost", cv), "raw_yuan": raw("fixed_cost"), "prev_raw_yuan": raw_prev("fixed_cost"), "terms": terms_of("fixed_cost", cur), "prev_terms": terms_of("fixed_cost", prev), - "unit": UNIT, "basis": "ENTITY_CALIBERS[entity]['fixed_terms'|'fixed_filters'](借−贷净额)"}, + "unit": UNIT, + "basis": ("ENTITY_CALIBERS[entity] 经 bep_caliber.fixed_cost_spec() 解析:" + "fixed_terms=带号科目项(§4-1 博海 6601/6602/6603 仅借方)/ " + "fixed_filters=前缀借−贷净额(酣客 5601/5602/5603)")}, {"name": f"= {bottom_label}", "line_key": "operating_profit", "is_total": True, "value": cv.get("result"), "prev_value": pv.get("result"), "change_rate": _calc_chg(cv.get("result"), pv.get("result")), diff --git a/backend/scripts/kpi_bep_deriver.py b/backend/scripts/kpi_bep_deriver.py index 551a5409..d3a0fe17 100644 --- a/backend/scripts/kpi_bep_deriver.py +++ b/backend/scripts/kpi_bep_deriver.py @@ -118,7 +118,7 @@ CARRY_SUBJECT = "3103" # 本年利润(carry_forward=1 且该科目 = 该 # (博海账套的结转行 carry_forward 仍是 0,不剔除则已结账期间净额恒 0 ⇒ 指标被清零) # 同表被 backend/app/api/dashboard.py::/bep-card 共用(口径不漂移)。 from app.utils.bep_caliber import ( # noqa: E402 (紧随 sys.path 之后导入) - CLOSING_SUMMARY_LIKE, ENTITY_CALIBERS, caliber_for, + CLOSING_SUMMARY_LIKE, ENTITY_CALIBERS, caliber_for, fixed_cost_spec, ) # 智享通落盘目录(与 app/api/data.py 的 ZXT_LANDING_DIR 一致)——结账信号次选来源 @@ -509,12 +509,18 @@ def run_derive(db, entity_id: int, period: str, rebate_rate_pct: float = None, months = int(period[5:7]) # YTD 月数(1月=1 ... 7月=7) # 1. 取数(元 → 万元)——按实体口径 + # 固定成本经 bep_caliber.fixed_cost_spec() 唯一解析口:e1 前缀净额 / e2 带号科目项仅借方(§4-1) + fix_mode, fix_spec = fixed_cost_spec(cal) sales_sum_w = _sum_field(db, entity_id, period_from, period_to, cal["revenue_field"], cal["revenue_filter"], exclude_closing=cal["exclude_closing"]) / 10000.0 - admin_sum_w = _sum_field(db, entity_id, period_from, period_to, "net", - subject_filters=cal["fixed_filters"], - exclude_closing=cal["exclude_closing"]) / 10000.0 + if fix_mode == "terms": + admin_sum_w = _sum_terms(db, entity_id, period_from, period_to, fix_spec, + exclude_closing=cal["exclude_closing"]) / 10000.0 + else: + admin_sum_w = _sum_field(db, entity_id, period_from, period_to, "net", + subject_filters=fix_spec, + exclude_closing=cal["exclude_closing"]) / 10000.0 dep_sum_w = _sum_field(db, entity_id, period_from, period_to, cal["dep_field"], cal["dep_filter"], exclude_closing=cal["exclude_closing"]) / 10000.0 diff --git a/backend/tests/test_profit_summary_real_caliber_20261014.py b/backend/tests/test_profit_summary_real_caliber_20261014.py index 0570e08d..6369a9ac 100644 --- a/backend/tests/test_profit_summary_real_caliber_20261014.py +++ b/backend/tests/test_profit_summary_real_caliber_20261014.py @@ -63,18 +63,20 @@ def seed_e1(db: Session, period: str = PERIOD): def seed_e2(db: Session, period: str = PERIOD): - """博海口径真账行: - 收入 6001 贷 1,000,000;变动 = 6401借 800,000; - 固定 = 6601借 60,000 + (6602借 30,000 − 贷 5,000) + (6603借 8,000 − 贷 20,000) = 73,000(净额口径)。 + """博海口径真账行(**ASK-20261014-04 §4-1/§4-2 批后口径**): + 收入 6001 贷 1,000,000;变动 = 6401借 800,000 + 6403借 20,000 = 820,000; + 固定(**仅借方**)= 6601借 60,000 + 6602借 30,000 + 6603借 8,000 = 98,000; + 财务收益(单列,不进固定)= 6603 贷 20,000。 + 一条 summary='结转期间损溢' 的结转腿(e2 账套 carry_forward 恒 0,必须按 summary 显式剔除)。 """ _row(db, 2, 1, "6001", credit=1_000_000.0, period=period) _row(db, 2, 2, "6401", debit=800_000.0, period=period) + _row(db, 2, 2, "6403", debit=20_000.0, period=period) # §4-2 追加营业税金及附加 _row(db, 2, 3, "6601", debit=60_000.0, period=period) _row(db, 2, 4, "6602", debit=30_000.0, period=period) - _row(db, 2, 5, "6602", credit=5_000.0, period=period) + _row(db, 2, 5, "6602", credit=5_000.0, period=period) # §4-1 后不进固定(仅借方) _row(db, 2, 6, "6603", debit=8_000.0, period=period) - _row(db, 2, 7, "6603", credit=20_000.0, period=period) + _row(db, 2, 7, "6603", credit=20_000.0, period=period) # §4-1 现金折扣收益 ⇒ 单列财务收益 _row(db, 2, 8, "6602", debit=999_999.0, summary="结转期间损溢", period=period) db.commit() @@ -122,14 +124,20 @@ class TestLinesFromRealSubjects: assert r["available"] is True v = r["values"] assert v["revenue"] == 100.00, v - assert v["variable_cost"] == 80.00, v - assert v["contribution"] == 20.00, v - assert v["fixed_cost"] == 7.30, v # 60,000 + 25,000 − 12,000 = 73,000 元 - assert v["result"] == 12.70, v + assert v["variable_cost"] == 82.00, v # 6401 800,000 + 6403 20,000(§4-2) + assert v["contribution"] == 18.00, v + assert v["fixed_cost"] == 9.80, v # §4-1 仅借方:60,000 + 30,000 + 8,000 = 98,000 元 + assert v["result"] == 8.20, v + # §4-1:6603 贷方现金折扣收益**不进固定成本**,改为单列财务收益(2.00 万) + assert r["finance_income"]["value"] == 2.00, r["finance_income"] + assert r["raw_yuan"]["fixed_cost"] == 98_000.0, r["raw_yuan"] # 结转腿(summary 含「结转」)不得计入 6602 fparts = {p["code"]: p for p in r["rows"]["fixed_cost"]["parts"]} - assert fparts["6602%"]["total_yuan"] == 25_000.0, fparts["6602%"] + assert fparts["6602"]["total_yuan"] == 30_000.0, fparts["6602"] + assert fparts["6602"]["field"] == "debit" and fparts["6602"]["match"] == "eq" assert r["caliber"]["exclude_closing"] is True + assert r["caliber"]["fixed_mode"] == "fixed_terms" + assert r["caliber"]["finance_income_terms"] == [["+", "6603", "credit"]] # ── L3 按 entity 隔离 ────────────────────────────────────────────────────── @@ -145,15 +153,20 @@ class TestEntityIsolation: assert r1["raw_yuan"]["revenue"] == 2_634_900.0 assert r2["raw_yuan"]["revenue"] == 1_000_000.0 - # 变动成本科目集不同:e1 含 5401+5403+571103−530103;e2 只有 6401 + # 变动成本科目集不同:e1 含 5401+5403+571103−530103;e2 含 6401+6403(§4-2) assert {p["code"] for p in r1["rows"]["variable_cost"]["parts"]} == {"5401", "5403", "571103", "530103"} - assert {p["code"] for p in r2["rows"]["variable_cost"]["parts"]} == {"6401"} + assert {p["code"] for p in r2["rows"]["variable_cost"]["parts"]} == {"6401", "6403"} # e2 的 6401 不得被算进 e1 assert r1["rows"]["variable_cost"]["parts"][0]["by_subject"] == {"5401": 2_000_000.0} - # 固定成本科目集不同:e1=5601/5602/5603;e2=6601/6602/6603 + # 固定成本科目集不同:e1=5601%/5602%/5603%(净额前缀);e2=6601/6602/6603(§4-1 仅借方带号项) assert {p["code"] for p in r1["rows"]["fixed_cost"]["parts"]} == {"5601%", "5602%", "5603%"} - assert {p["code"] for p in r2["rows"]["fixed_cost"]["parts"]} == {"6601%", "6602%", "6603%"} + assert {p["code"] for p in r2["rows"]["fixed_cost"]["parts"]} == {"6601", "6602", "6603"} + assert r1["caliber"]["fixed_mode"] == "fixed_filters(net)" + assert r2["caliber"]["fixed_mode"] == "fixed_terms" + # 财务收益单列项只有 e2 有(e1 无 6603 贷方项) + assert r1["finance_income"]["value"] == 0.0 + assert r2["finance_income"]["value"] == 2.00 # 口径快照不同(digest 异:口径表按账套而异) assert r1["caliber"]["digest"] != r2["caliber"]["digest"] @@ -384,7 +397,7 @@ class TestEndpoint: seed_e1(db) seed_e2(db) r2 = PL.compute_lines(db, 2, PERIOD, PERIOD) - assert r2["values"]["revenue"] == 100.00 and r2["values"]["fixed_cost"] == 7.30 + assert r2["values"]["revenue"] == 100.00 and r2["values"]["fixed_cost"] == 9.80 # entity1 的 token ⇒ entity1 账套(e2 行不进入) d = client.get(f"{BASE}?period={PERIOD}", headers=auth_header(token)).json() assert d["entity_id"] == 1 diff --git a/frontend/src/views/ReportCenter.vue b/frontend/src/views/ReportCenter.vue index 9bfc1acc..441bbcbf 100644 --- a/frontend/src/views/ReportCenter.vue +++ b/frontend/src/views/ReportCenter.vue @@ -27,7 +27,7 @@
-
基于管理会计视角的利润结构分析:收入 → 变动成本 → 边际贡献 → 固定成本 → 息税前利润
+
{{ profitDesc || '基于管理会计视角的利润结构分析:营业收入 → 变动成本 → 边际贡献 → 固定成本 → 营业利润(真实科目取数)' }}
+ +
+ 数据批次 / 导入时刻: + + + {{ b.batch }} | {{ b.rows }} 行 | 导入 {{ b.imported_at }} + + +
+ +
+ {{ profitFinanceIncome.name }}(备查,不计入上行):¥ {{ Number(profitFinanceIncome.value).toLocaleString() }} 万元 +
+ +
+
{{ profitChecks.note || '⚠️ 勾稽自检未通过:收入 − 变动成本 − 固定成本 ≠ 末行' }}
+
@@ -649,6 +668,10 @@ function getEntityId() { // ── 报表1:管理利润表 ── const profitItems = ref([]) +const profitDesc = ref('') +const profitBatches = ref([]) // 4-5 备注(纯展示:数据批次/导入时刻) +const profitChecks = ref(null) // 勾稽自检(reconciliation:balanced/diff/note) +const profitFinanceIncome = ref(null) // 财务收益(§4-1 单列备查) function profitSummary(param: any) { const sums: string[] = ['合计'] @@ -859,8 +882,21 @@ function onPeriodChange() { async function loadProfit() { try { const r = await api.get('/reports/profit-summary', { params: { period: reportPeriod.value, entity_id: getEntityId() } }) - profitItems.value = (r as any)?.items || [] - } catch { profitItems.value = [] } + const d: any = (r as any) || {} + profitItems.value = d.items || [] + profitDesc.value = d.desc || '' + // 以下三项均**纯展示**,不参与任何计算: + // 4-5 数据批次 / 导入时刻备注 · 勾稽自检(不平显式标红)· 财务收益单列备查 + profitBatches.value = d.data_batches || [] + profitChecks.value = d.reconciliation || null + profitFinanceIncome.value = d.finance_income || null + } catch { + profitItems.value = [] + profitDesc.value = '' + profitBatches.value = [] + profitChecks.value = null + profitFinanceIncome.value = null + } } async function loadBudget() {