feat(cma-report): 建账期初层 + 方向判定可信度 + 权益行科目集(M1/M2/M7,规则表 v1.1)
- M1 时点报表口径:余额 = 期初(report_opening_balances) + 累计发生额;期初只作用于取数层
(不生成凭证/不写 voucher_details/不改 kpi_values);basis=period 不套期初;受 effective_date 约束
- M2 期初未补 → direction_check_unreliable=true:报表/底稿带"期初未补,方向判定不可信"水印,
重分类触发只进底稿诊断(diagnostic_only),不落地对外行项目
- M7 权益行科目集 = {3001,3002,3101,3103,3104,3105}(含 4001/4002/4101/4103/4104 别名):补实收资本 3001
- 1901 待处理财产损溢过渡列报「其他流动资产」以保恒等式(M6 归因文案)
- 只读验收脚本 + 幂等导入器(dry-run 默认,--commit 门控)
This commit is contained in:
+46
-10
@@ -1410,12 +1410,19 @@ BALANCE_SHEET_SECTIONS = [
|
||||
"key": "equity",
|
||||
"name": "所有者权益",
|
||||
"category_label": "所有者权益",
|
||||
# M7(规则表 v1.1 §三):权益行科目集 = {3001,3002,3101,3103,3104,3105}
|
||||
# (含 4001/4002/4101/4103/4104 别名)。账套实际记账用小企业准则 3xxx 科目,
|
||||
# 别名只参与取数、不参与行 key(避免打破按 key 的预算映射)。缺 3001 会漏实收资本 2,000,000。
|
||||
"lines": [
|
||||
{"codes": [("4001", 1)], "name": "实收资本", "ns_category": "equity"},
|
||||
{"codes": [("4002", 1)], "name": "资本公积", "ns_category": "equity"},
|
||||
{"codes": [("4001", 1)], "alias_codes": [("3001", 1)],
|
||||
"name": "实收资本", "ns_category": "equity"},
|
||||
{"codes": [("4002", 1)], "alias_codes": [("3002", 1)],
|
||||
"name": "资本公积", "ns_category": "equity"},
|
||||
{"codes": [("4101", 1)], "alias_codes": [("3101", 1)],
|
||||
"name": "盈余公积", "ns_category": "equity"},
|
||||
# alias_codes:模板用 4103/4104,智享通凭证明细实际用 3103/3104(2026-09-16 补)
|
||||
{"codes": [("4103", 1), ("4104", 1)],
|
||||
"alias_codes": [("3103", 1), ("3104", 1)],
|
||||
"alias_codes": [("3103", 1), ("3104", 1), ("3105", 1)],
|
||||
"name": "未分配利润", "ns_category": "equity"},
|
||||
],
|
||||
},
|
||||
@@ -1549,20 +1556,26 @@ def get_balance_sheet(
|
||||
|
||||
plan = _bs_analysis(db, entity_id, period, use_basis)
|
||||
has_real = bool(plan["has_voucher_data"])
|
||||
# M2:期初未补 → 方向判定不可信 → 重分类结论只进底稿诊断,不落地到对外行项目
|
||||
diag_only = bool(plan.get("direction_check_unreliable"))
|
||||
plan_prev = _bs_analysis(db, entity_id, prev_period, use_basis, include_unmapped=False) if has_real else None
|
||||
|
||||
def _present_key(pl: Optional[dict]) -> str:
|
||||
"""M2:期初未补(方向判定不可信)时,对外行项目用**原始余额**(不呈现"重分类转正")"""
|
||||
return "raw" if (pl or {}).get("direction_check_unreliable") else "after"
|
||||
|
||||
def _line_value(pl: Optional[dict], key: str, col: str):
|
||||
if pl is None:
|
||||
return None
|
||||
return pl["after"].get(key)
|
||||
return pl[_present_key(pl)].get(key)
|
||||
|
||||
# 规则落位项目(模板未列报的目标项)→ 按板块挂载虚拟行
|
||||
virtual: dict = {}
|
||||
for key, item in plan["items"].items():
|
||||
if item["template"] or item.get("blocked"):
|
||||
continue
|
||||
end_v = plan["after"].get(key)
|
||||
begin_v = plan_prev["after"].get(key) if plan_prev else None
|
||||
end_v = plan[_present_key(plan)].get(key)
|
||||
begin_v = plan_prev[_present_key(plan_prev)].get(key) if plan_prev else None
|
||||
if not end_v and not begin_v:
|
||||
continue
|
||||
virtual.setdefault(item["section_key"], []).append({
|
||||
@@ -1604,6 +1617,8 @@ def get_balance_sheet(
|
||||
if begin["value"] is not None:
|
||||
sec_begin += begin["value"]
|
||||
raw_key = plan["raw"].get(key)
|
||||
diag_adj = None if raw_key is None else round(
|
||||
float(plan["after"].get(key) or 0) - float(raw_key), 2)
|
||||
lines.append({
|
||||
"name": line["name"],
|
||||
"item_key": key,
|
||||
@@ -1615,8 +1630,11 @@ def get_balance_sheet(
|
||||
"is_reclass_line": False,
|
||||
"template_gap": False,
|
||||
"raw_value": None if raw_key is None else round(float(raw_key), 2),
|
||||
# 对外调整额按呈现口径计算:期初未补(方向判定不可信)时不落地 → 0
|
||||
"reclass_adjustment": None if raw_key is None else round(
|
||||
float(plan["after"].get(key) or 0) - float(raw_key), 2),
|
||||
float(_line_value(plan, key, "end") or 0) - float(raw_key), 2),
|
||||
# 诊断用调整额(无论是否落地都给出,供底稿/审计追溯)
|
||||
"diagnostic_reclass_adjustment": diag_adj,
|
||||
})
|
||||
|
||||
# 规则落位项目(虚拟行)
|
||||
@@ -1674,7 +1692,12 @@ def get_balance_sheet(
|
||||
},
|
||||
# ── 重分类(调表不调账)──
|
||||
"reclass": {
|
||||
"applied": bool(plan["entries"]),
|
||||
# M2:期初未补 → 方向判定不可信 → 重分类结论只作底稿诊断,不落地到对外行项目
|
||||
"applied": bool(plan["entries"]) and not diag_only,
|
||||
"diagnostic_only": diag_only,
|
||||
"direction_check_unreliable": diag_only,
|
||||
"caliber_warning": plan.get("caliber_warning") or "",
|
||||
"opening": plan.get("opening"),
|
||||
"rule_version": plan["rule_version"],
|
||||
"basis": plan["basis"],
|
||||
"has_voucher_data": has_real,
|
||||
@@ -1682,6 +1705,7 @@ def get_balance_sheet(
|
||||
"invariants": inv,
|
||||
"negatives": plan["negatives"],
|
||||
"template_gaps": plan["template_gaps"],
|
||||
"residual_reported": plan.get("residual_reported") or [],
|
||||
"unmapped_subjects": plan["unmapped_subjects"],
|
||||
"pl_unclosed": plan["pl_unclosed"],
|
||||
"working_paper_url": f"/api/cma/reports/balance-sheet/reclass-working-paper?period={period}&basis={use_basis}",
|
||||
@@ -1709,10 +1733,16 @@ def get_reclass_working_paper(
|
||||
"rule_version": plan["rule_version"],
|
||||
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"accounting_method": "调表不调账(报表取数层重分类,不生成记账凭证)",
|
||||
# ── M1/M2:期初层 + 方向判定可信度(期初未补 → 只作诊断)──
|
||||
"direction_check_unreliable": plan.get("direction_check_unreliable", False),
|
||||
"caliber_warning": plan.get("caliber_warning") or "",
|
||||
"opening": plan.get("opening"),
|
||||
"diagnostic_only": bool(plan.get("direction_check_unreliable")),
|
||||
"rules": plan["rules"],
|
||||
"entries": plan["entries"],
|
||||
"invariants": plan["invariants"],
|
||||
"template_gaps": plan["template_gaps"],
|
||||
"residual_reported": plan.get("residual_reported") or [],
|
||||
"negatives": plan["negatives"],
|
||||
"unmapped_subjects": plan["unmapped_subjects"],
|
||||
"pl_unclosed": plan["pl_unclosed"],
|
||||
@@ -1759,13 +1789,15 @@ def export_reclass_working_paper(
|
||||
ws.column_dimensions[ws.cell(row=2, column=ci).column_letter].width = max(12, min(46, len(str(h)) * 2 + 10))
|
||||
|
||||
e_rows = report_reclass.entry_rows(plan)
|
||||
warn = (" ⚠️ 期初未补,方向判定不可信(重分类结论仅诊断,不落地)"
|
||||
if plan.get("direction_check_unreliable") else "")
|
||||
ws1 = wb.active
|
||||
ws1.title = "重分类底稿"
|
||||
if e_rows:
|
||||
_sheet(ws1, f"资产负债表重分类底稿(调表不调账) {period} 账套 entity_id={entity_id} 口径basis={use_basis} {plan['rule_version']}",
|
||||
_sheet(ws1, f"资产负债表重分类底稿(调表不调账) {period} 账套 entity_id={entity_id} 口径basis={use_basis} {plan['rule_version']}{warn}",
|
||||
list(e_rows[0].keys()), e_rows)
|
||||
else:
|
||||
ws1.cell(row=1, column=1, value=f"本期({period})无触发的重分类规则").font = Font(bold=True)
|
||||
ws1.cell(row=1, column=1, value=f"本期({period})无触发的重分类规则{warn}").font = Font(bold=True)
|
||||
|
||||
d_rows = report_reclass.diagnostic_rows(plan)
|
||||
ws2 = wb.create_sheet("不变量与诊断")
|
||||
@@ -2091,6 +2123,10 @@ def export_statutory_reports(
|
||||
"negatives": (bs.get("reclass") or {}).get("negatives") or {"before": [], "after": [], "after_in_scope": []},
|
||||
"template_gaps": (bs.get("reclass") or {}).get("template_gaps") or [],
|
||||
"unmapped_subjects": (bs.get("reclass") or {}).get("unmapped_subjects") or [],
|
||||
# M1/M2:期初层 + 方向判定可信度(底稿 Sheet 同源暴露)
|
||||
"residual_reported": (bs.get("reclass") or {}).get("residual_reported") or [],
|
||||
"direction_check_unreliable": (bs.get("reclass") or {}).get("direction_check_unreliable", False),
|
||||
"caliber_warning": (bs.get("reclass") or {}).get("caliber_warning", ""),
|
||||
}
|
||||
for row in report_reclass.diagnostic_rows(plan_like):
|
||||
r = write_row(ws4, r, [row.get("项目"), row.get("值"), row.get("说明")])
|
||||
|
||||
Reference in New Issue
Block a user