feat(new30-p1): 追溯调整数据准备+KPI口径更新

- 后端新增 GET /api/cma/reports/restatement 端点
  - 旧口径vs新口径双列对比
  - 自动标记需调整项(管理费用剥离研发费用、财务费用拆解)
  - 返回净利润对比汇总
- 前端报表中心新增"追溯调整"Tab
  - 双列表格展示(旧准则/新准则)
  - 差异自动高亮标记
  - 调整原因列、汇总统计
- KPI口径更新(entity_id=1)
  - F_COST_RATIO 费用率: (销售费用+管理费用-研发费用)/营业收入×100%, target=15%
  - 新增 F_OP_PROFIT_MARGIN 经营利润率: 经营利润/营业收入×100%, target=5%
- 构建部署验证通过
This commit is contained in:
Hermes CI Fix
2026-07-22 00:01:20 +08:00
parent dd105efee3
commit ec6af751a5
2 changed files with 460 additions and 1 deletions
+313
View File
@@ -650,6 +650,319 @@ def _get_demo_block_total(block_key: str) -> float:
return demo.get(block_key, 0)
# ============================================================
# 追溯调整: 2026年数据按新30号准则重述 (P1)
# ============================================================
@router.get("/restatement")
def get_restatement(
period: str = Query(None, description="格式 YYYY-MM"),
db: Session = Depends(get_db),
):
"""2026年数据按新准则重述 — 旧口径vs新口径双列对比,自动标记调整项"""
if period is None:
period = datetime.now().strftime("%Y-%m")
# 旧口径数据 (传统利润表项目)
def _old_kpi_val(code: str):
kpi = db.query(KPIDefinition).filter(KPIDefinition.kpi_code == code).first()
if not kpi:
return None
v = db.query(KPIValue).filter(
KPIValue.kpi_id == kpi.id, KPIValue.period == period
).order_by(KPIValue.id.desc()).first()
return float(v.actual_value) if v and v.actual_value is not None else None
old_revenue = _old_kpi_val("F_REVENUE")
old_revenue_other = _old_kpi_val("F_REVENUE_OTHER")
old_cost = _old_kpi_val("F_COST")
old_cost_other = _old_kpi_val("F_COST_OTHER")
old_selling_exp = _old_kpi_val("F_SELLING_EXP")
old_admin_exp = _old_kpi_val("F_ADMIN_EXP")
old_finance_exp = _old_kpi_val("F_FINANCE_EXP")
old_rd_exp = _old_kpi_val("F_RD_EXP")
# 新口径数据 (从科目映射或kpi_values获取)
new_revenue = _get_subject_amount(db, "6001", period)
new_revenue_other = _get_subject_amount(db, "6051", period)
new_cost = _get_subject_amount(db, "6401", period)
new_cost_other = _get_subject_amount(db, "6402", period)
new_selling = _get_subject_amount(db, "6601", period)
new_admin = _get_subject_amount(db, "6602", period)
new_rd = _get_subject_amount(db, "660204", period)
new_interest_income = _get_subject_amount(db, "6011", period)
new_interest_exp = _get_subject_amount(db, "660301", period)
new_fx = _get_subject_amount(db, "6603", period)
new_fx_financing = _get_subject_amount(db, "660302", period)
new_invest_income = _get_subject_amount(db, "6111", period)
new_impairment = _get_subject_amount(db, "6701", period)
new_invest_impairment = _get_subject_amount(db, "670101", period)
new_tax = _get_subject_amount(db, "6801", period)
new_discontinued = _get_subject_amount(db, "6901", period)
# 旧口径汇总计算
old_operating_items = [
{"name": "营业收入", "old_value": old_revenue, "category": "operating"},
{"name": "其他业务收入", "old_value": old_revenue_other, "category": "operating"},
{"name": "减:营业成本", "old_value": old_cost, "category": "operating"},
{"name": "减:其他业务成本", "old_value": old_cost_other, "category": "operating"},
{"name": "减:销售费用", "old_value": old_selling_exp, "category": "operating"},
{"name": "减:管理费用", "old_value": old_admin_exp, "category": "operating"},
{"name": "减:财务费用", "old_value": old_finance_exp, "category": "financing"},
]
# 新口径项目明细
new_items_map = [
# 经营类
{"name": "营业收入", "new_value": new_revenue, "category": "operating"},
{"name": "其他业务收入", "new_value": new_revenue_other, "category": "operating"},
{"name": "减:营业成本", "new_value": new_cost, "category": "operating"},
{"name": "减:其他业务成本", "new_value": new_cost_other, "category": "operating"},
{"name": "减:销售费用", "new_value": new_selling, "category": "operating"},
{"name": "减:管理费用(不含研发)", "new_value": new_admin, "category": "operating"},
{"name": "减:研发费用", "new_value": new_rd, "category": "operating"},
{"name": "减:经营资产减值损失", "new_value": new_impairment, "category": "operating"},
{"name": "经营汇兑损益", "new_value": new_fx, "category": "operating"},
# 投资类
{"name": "利息收入", "new_value": new_interest_income, "category": "investing"},
{"name": "投资收益", "new_value": new_invest_income, "category": "investing"},
{"name": "减:投资类资产减值", "new_value": new_invest_impairment, "category": "investing"},
# 筹资类
{"name": "减:利息支出", "new_value": new_interest_exp, "category": "financing"},
{"name": "筹资汇兑损益", "new_value": new_fx_financing, "category": "financing"},
# 所得税
{"name": "减:所得税费用", "new_value": new_tax, "category": "tax"},
# 终止经营
{"name": "终止经营损益", "new_value": new_discontinued, "category": "discontinued"},
]
# 构建对比行
items = []
adjusted_count = 0
new_count = 0
unchanged_count = 0
# 先处理旧口径中存在的项目,匹配新口径
old_new_mapping = {
"营业收入": "营业收入",
"其他业务收入": "其他业务收入",
"减:营业成本": "减:营业成本",
"减:其他业务成本": "减:其他业务成本",
"减:销售费用": "减:销售费用",
"减:管理费用": "减:管理费用(不含研发)",
}
for old_item in old_operating_items:
name = old_item["name"]
old_val = old_item["old_value"]
new_name = old_new_mapping.get(name, name)
new_item = next((n for n in new_items_map if n["name"] == new_name), None)
new_val = new_item["new_value"] if new_item else None
# 管理费用:旧口径含研发,新口径不含 → 自动调整
if name == "减:管理费用":
# 旧管理费 - 旧研发费 = 新管理费(不含研发)
computed_new = old_val
if old_admin_exp is not None and old_rd_exp is not None:
# 如果新口径取不到值,用旧口径推算
if new_val is None:
new_val = old_admin_exp - old_rd_exp if old_rd_exp else old_admin_exp
diff = round(new_val - old_val, 2) if old_val is not None and new_val is not None else None
needs_adj = diff is not None and abs(diff) > 0.01
if needs_adj:
adjusted_count += 1
items.append({
"item_name": name,
"old_value": old_val,
"new_value": new_val,
"difference": diff,
"needs_adjustment": needs_adj,
"adjustment_reason": "研发费用剥离" if needs_adj else None,
"category": old_item["category"],
})
# 自动带上研发费用行
rd_old = None # 旧口径无单独研发费用
rd_new = new_rd or old_rd_exp
if rd_new is not None:
adjusted_count += 1
items.append({
"item_name": "减:研发费用(单独列示)",
"old_value": rd_old,
"new_value": rd_new,
"difference": rd_new if rd_new is not None else None,
"needs_adjustment": rd_new is not None,
"adjustment_reason": "新30号准则单独列示",
"category": "operating",
})
continue
# 财务费用:旧口径一行汇总,新口径拆解为利息收入(投资类)+利息支出(筹资类)
if name == "减:财务费用":
fin_old = old_val
fin_new_total = 0
fin_new_breakdown = []
# 利息支出(筹资类)
int_exp_val = new_interest_exp
if int_exp_val is not None:
fin_new_total += -int_exp_val
fin_new_breakdown.append({
"item_name": "减:利息支出(筹资类)",
"old_value": None,
"new_value": int_exp_val,
"difference": None,
"needs_adjustment": True,
"adjustment_reason": "财务费用拆解",
"category": "financing",
})
adjusted_count += 1
# 筹资汇兑损益
fx_fin_val = new_fx_financing
if fx_fin_val is not None:
fin_new_total += fx_fin_val
fin_new_breakdown.append({
"item_name": "筹资汇兑损益",
"old_value": None,
"new_value": fx_fin_val,
"difference": None,
"needs_adjustment": True,
"adjustment_reason": "财务费用拆解",
"category": "financing",
})
adjusted_count += 1
# 利息收入(投资类)
int_inc_val = new_interest_income
if int_inc_val is not None:
fin_new_breakdown.append({
"item_name": "利息收入(投资类)",
"old_value": None,
"new_value": int_inc_val,
"difference": None,
"needs_adjustment": True,
"adjustment_reason": "财务费用拆解",
"category": "investing",
})
adjusted_count += 1
# 经营汇兑损益(经营类)
fx_op_val = new_fx
if fx_op_val is not None:
fin_new_breakdown.append({
"item_name": "经营汇兑损益(经营类)",
"old_value": None,
"new_value": fx_op_val,
"difference": None,
"needs_adjustment": True,
"adjustment_reason": "财务费用拆解",
"category": "operating",
})
adjusted_count += 1
items.append({
"item_name": "减:财务费用",
"old_value": fin_old,
"new_value": None,
"difference": None,
"needs_adjustment": True,
"adjustment_reason": "财务费用拆解为投资类利息收入和筹资类利息支出",
"category": "financing",
"breakdown": fin_new_breakdown,
})
# 把拆解项加到主列表
for brk in fin_new_breakdown:
items.append(brk)
continue
# 普通项目直接对比
diff = round(new_val - old_val, 2) if old_val is not None and new_val is not None else None
needs_adj = diff is not None and abs(diff) > 0.01
if needs_adj:
adjusted_count += 1
else:
unchanged_count += 1
items.append({
"item_name": name,
"old_value": old_val,
"new_value": new_val,
"difference": diff,
"needs_adjustment": needs_adj,
"adjustment_reason": None,
"category": old_item["category"],
})
# 新增项目(只有新口径有)
existing_names = [i["item_name"] for i in items]
for new_item in new_items_map:
if new_item["name"] not in existing_names and new_item["new_value"] is not None:
items.append({
"item_name": new_item["name"],
"old_value": None,
"new_value": new_item["new_value"],
"difference": None,
"needs_adjustment": True,
"adjustment_reason": "新30号准则新增项目",
"category": new_item["category"],
})
new_count += 1
adjusted_count += 1
# 计算旧口径净利润和新口径净利润
def _calc_net_profit_old():
"""旧口径净利润 (简化)"""
rev = old_revenue or 0
rev_other = old_revenue_other or 0
c = old_cost or 0
c_other = old_cost_other or 0
sell = old_selling_exp or 0
admin = old_admin_exp or 0
fin = old_finance_exp or 0
return rev + rev_other - c - c_other - sell - admin - fin
def _calc_net_profit_new():
"""新口径净利润 = 经营利润 + 投资净收益 + 筹资净费用 + 所得税 + 终止经营"""
op_items = ["6001", "6051", "6401", "6402", "6601", "6602", "660204", "6603", "6701"]
inv_items = ["6011", "6111", "670101"]
fin_items = ["660301", "660302"]
tax_items = ["6801"]
dis_items = ["6901"]
total = 0
for codes in [op_items, inv_items, fin_items, tax_items, dis_items]:
for code in codes:
amt = _get_subject_amount(db, code, period)
if amt is not None:
# 根据BLOCK_INFO中的sign处理
for bk in BLOCK_INFO.values():
for ic in bk["items"]:
if ic["code"] == code:
total += amt * ic["sign"]
return round(total, 2)
old_net = _calc_net_profit_old()
new_net = _calc_net_profit_new()
return {
"period": period,
"items": items,
"summary": {
"total_items": len(items),
"adjusted_items": adjusted_count,
"new_items": new_count,
"unchanged_items": unchanged_count,
},
"net_profit_comparison": {
"old_net_profit": round(old_net, 2),
"new_net_profit": new_net,
"difference": round(new_net - old_net, 2),
},
}
@router.get("/category-map")
def get_category_map(
db: Session = Depends(get_db),