feat: 新30号准则P2 — MPM计算器+旧格式双列对比

This commit is contained in:
Hermes CI Fix
2026-07-22 12:23:40 +08:00
parent 748c2da43f
commit cbcc0d0a28
5 changed files with 705 additions and 14 deletions
+238 -2
View File
@@ -9,6 +9,7 @@ CMA管理报表中心 — 管理会计OS
4. 四维度绩效评分卡 — BSC健康度雷达图
"""
from fastapi import APIRouter, Depends, Query, HTTPException
from pydantic import BaseModel
from sqlalchemy.orm import Session
from sqlalchemy import func
from typing import Optional
@@ -571,10 +572,10 @@ def _get_subject_amount(db: Session, code: str, period: str) -> Optional[float]:
@router.get("/profit-statement")
def get_profit_statement(
period: str = Query(None, description="格式 YYYY-MM"),
format: str = Query("old", description="old/new"),
format: str = Query("old", description="old/new/dual"),
db: Session = Depends(get_db),
):
"""利润表 — 支持旧格式新30号准则五板块格式"""
"""利润表 — 支持旧格式新30号准则五板块格式、双列对比"""
if period is None:
period = datetime.now().strftime("%Y-%m")
@@ -582,7 +583,24 @@ def get_profit_statement(
# 旧30号准则格式(保留兼容)
return get_profit_summary(period=period, db=db)
if format == "dual":
# 双列对比:旧准则 vs 新准则
old_data = get_profit_summary(period=period, db=db)
new_data = _build_new_format_profit(db, period)
return {
"period": period,
"format": "dual",
"title": f"利润表双列对比({period}",
"old_format": old_data,
"new_format": new_data,
}
# === 新30号准则:五板块结构 ===
return _build_new_format_profit(db, period)
def _build_new_format_profit(db: Session, period: str) -> dict:
"""构建新30号准则五板块利润表"""
blocks = []
total_net_profit = 0
all_items_have_data = True
@@ -638,6 +656,224 @@ def get_profit_statement(
}
# ============================================================
# MPM管理层指标计算器 (P2)
# ============================================================
# MPM指标类型定义
MPM_INDICATOR_TYPES = {
"ebitda": {
"name": "EBITDA",
"description": "息税折旧摊销前利润",
"base_label": "净利润",
"default_adjustments": [
{"code": "tax", "name": "加:所得税费用", "sign": 1, "checked": True},
{"code": "interest", "name": "加:利息支出", "sign": 1, "checked": True},
{"code": "depreciation", "name": "加:折旧与摊销", "sign": 1, "checked": True},
{"code": "impairment", "name": "加:资产减值损失", "sign": 1, "checked": False},
],
},
"adjusted_net_profit": {
"name": "调整后净利润",
"description": "剔除非经常性项目后的可持续净利润",
"base_label": "净利润(准则)",
"default_adjustments": [
{"code": "impairment", "name": "加:资产减值损失", "sign": 1, "checked": True},
{"code": "equity_incentive", "name": "加:股权激励费用", "sign": 1, "checked": True},
{"code": "ma_cost", "name": "加:并购相关费用", "sign": 1, "checked": False},
{"code": "nonrecurring_income", "name": "减:非经常性投资收益", "sign": -1, "checked": True},
{"code": "asset_disposal", "name": "减:资产处置收益", "sign": -1, "checked": False},
{"code": "government_grant", "name": "减:政府补助", "sign": -1, "checked": False},
],
},
"free_cash_flow": {
"name": "自由现金流",
"description": "经营现金流扣除资本支出后的可自由支配现金流",
"base_label": "经营现金流",
"default_adjustments": [
{"code": "capex", "name": "减:资本支出", "sign": -1, "checked": True},
{"code": "working_capital", "name": "减:营运资本增加", "sign": -1, "checked": True},
{"code": "maintenance_capex", "name": "减:维护性资本支出", "sign": -1, "checked": False},
{"code": "dividend", "name": "加:股息收入", "sign": 1, "checked": False},
],
},
"custom": {
"name": "自定义指标",
"description": "自定义管理层指标",
"base_label": "净利润",
"default_adjustments": [
{"code": "adjustment_1", "name": "调整项目1", "sign": 1, "checked": False, "amount": None},
{"code": "adjustment_2", "name": "调整项目2", "sign": -1, "checked": False, "amount": None},
{"code": "adjustment_3", "name": "调整项目3", "sign": 1, "checked": False, "amount": None},
],
},
}
# 调整项默认金额(从利润表自动取值映射)
ADJUSTMENT_VALUE_MAP = {
"tax": {"code": "6801", "sign_inverse": True}, # 所得税费用,加回需取绝对值
"interest": {"code": "660301", "sign_inverse": True},
"impairment": {"code": "6701", "sign_inverse": False}, # 资产减值损失本身是费用
"equity_incentive": None, # 无映射,需用户输入
"ma_cost": None,
"nonrecurring_income": {"code": "6111", "sign_inverse": False},
"asset_disposal": None,
"government_grant": None,
"capex": None,
"working_capital": None,
"maintenance_capex": None,
"dividend": None,
}
class MpmCalculateRequest(BaseModel):
indicator_type: str = "adjusted_net_profit"
period: str = None
adjustments: list[dict] = None # [{code, name, sign, checked, amount}]
@router.post("/mpm-calculate")
def mpm_calculate(
req: MpmCalculateRequest,
db: Session = Depends(get_db),
):
"""MPM管理层指标计算器 — 生成合规调节表"""
if req.period is None:
req.period = datetime.now().strftime("%Y-%m")
indicator_cfg = MPM_INDICATOR_TYPES.get(req.indicator_type)
if not indicator_cfg:
raise HTTPException(status_code=400, detail=f"不支持的指标类型: {req.indicator_type}")
# 获取基准值:净利润
net_profit = _calc_new_net_profit(db, req.period)
if net_profit is None:
net_profit = 0
# 经营现金流(自由现金流的基准)
operating_cf = _get_kpi_val(db, "F_OPERATING_CF", req.period)
# 确定基准值
if req.indicator_type == "free_cash_flow":
base_value = operating_cf or net_profit # fallback
base_label = "经营现金流"
else:
base_value = net_profit
base_label = indicator_cfg["base_label"]
# 获取调整项(来自请求或默认)
adjustments = req.adjustments if req.adjustments else indicator_cfg["default_adjustments"]
# 自动填充调整项金额
reconciliation_items = []
running_total = base_value
# 第一步:基准值
reconciliation_items.append({
"step": 0,
"code": "_base",
"name": base_label,
"sign": 1,
"amount": round(base_value, 2),
"effective": round(base_value, 2),
"is_base": True,
"running_total": round(base_value, 2),
})
for adj in adjustments:
code = adj.get("code", "")
checked = adj.get("checked", False)
sign = adj.get("sign", 1)
name = adj.get("name", "")
amount = adj.get("amount")
# 尝试自动取值
if amount is None and checked:
amount = _get_adjustment_value(db, code, req.period)
effective = round(amount * sign, 2) if amount is not None else None
item = {
"step": len(reconciliation_items),
"code": code,
"name": name,
"sign": sign,
"amount": round(amount, 2) if amount is not None else None,
"effective": effective,
"checked": checked,
"is_base": False,
"running_total": None,
}
if checked and effective is not None:
running_total += effective
item["running_total"] = round(running_total, 2)
reconciliation_items.append(item)
# 最终结果
final_value = round(running_total, 2)
return {
"indicator_type": req.indicator_type,
"indicator_name": indicator_cfg["name"],
"indicator_desc": indicator_cfg["description"],
"period": req.period,
"base_value": round(base_value, 2),
"base_label": base_label,
"final_value": final_value,
"final_label": indicator_cfg["name"],
"adjustment_count": sum(1 for a in adjustments if a.get("checked", False)),
"total_adjustments": len(adjustments),
"reconciliation_items": reconciliation_items,
"has_real_data": net_profit != 0,
}
def _get_kpi_val(db: Session, code: str, period: str) -> Optional[float]:
"""从KPI定义+值获取数值"""
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
def _calc_new_net_profit(db: Session, period: str) -> Optional[float]:
"""计算新30号准则下的净利润"""
total = 0
has_data = False
for block_key in ["operating", "investing", "financing", "tax", "discontinued"]:
block_cfg = BLOCK_INFO[block_key]
for item_cfg in block_cfg["items"]:
amount = _get_subject_amount(db, item_cfg["code"], period)
if amount is not None:
total += amount * item_cfg["sign"]
has_data = True
if not has_data:
return None
return round(total, 2)
def _get_adjustment_value(db: Session, adj_code: str, period: str) -> Optional[float]:
"""获取调整项的自动取值"""
mapping = ADJUSTMENT_VALUE_MAP.get(adj_code)
if mapping is None:
return None # 需要用户输入
code = mapping["code"]
amount = _get_subject_amount(db, code, period)
if amount is None:
return None
# sign_inverse: 如果调整项是"加回"费用,费用本身的sign是负的(在利润表中是减项)
# 但在MPM调节中,加回费用取绝对值
return abs(amount) if mapping.get("sign_inverse", False) else amount
def _get_demo_block_total(block_key: str) -> float:
"""PRD示例数据 fallback"""
demo = {