feat: 新30号准则P2 — MPM计算器+旧格式双列对比
This commit is contained in:
+238
-2
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user