feat: P0/P1/P2全部功能 — 四层泳道/视角切换/KPI看板/预警/差异反打/预算/知识面板/回顾会/情景预测/Excel导入/角色权限
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
"""多情景预测模拟引擎 — 管理会计OS P2-1
|
||||
从战略地图KPI输入变量出发,按类别映射到财务影响,
|
||||
输出乐观/基准/保守三情景数值+曲线数据
|
||||
"""
|
||||
import math
|
||||
import logging
|
||||
from typing import List, Dict, Optional
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger("cma.scenario")
|
||||
|
||||
# KPI类别 → 财务影响映射系数
|
||||
# 降本类: 每变化1% → 成本节省系数
|
||||
# 增收类: 每变化1% → 收入增长系数
|
||||
KPI_CATEGORY_MAP = {
|
||||
# 增收类
|
||||
"revenue_growth": {"type": "revenue", "factor": 0.8, "desc": "收入增长"},
|
||||
"customer_scale": {"type": "revenue", "factor": 0.6, "desc": "客户规模→收入"},
|
||||
# 降本类
|
||||
"cost_control": {"type": "cost", "factor": -0.7, "desc": "成本节约"},
|
||||
"asset_efficiency": {"type": "cost", "factor": -0.3, "desc": "资产效率→成本"},
|
||||
# 利润类
|
||||
"profitability": {"type": "profit", "factor": 0.5, "desc": "直接利润影响"},
|
||||
# 现金流类
|
||||
"cash_risk": {"type": "cash", "factor": 0.4, "desc": "现金流影响"},
|
||||
# 客户类→收入
|
||||
"customer_satisfaction": {"type": "revenue", "factor": 0.3, "desc": "满意度→收入"},
|
||||
"customer_concentration": {"type": "revenue", "factor": -0.2, "desc": "集中度→风险"},
|
||||
# 流程类→成本
|
||||
"delivery_quality": {"type": "cost", "factor": -0.3, "desc": "交付质量→成本"},
|
||||
"supply_chain": {"type": "cost", "factor": -0.2, "desc": "供应链→成本"},
|
||||
# 学习类→长期收入
|
||||
"talent_pipeline": {"type": "revenue", "factor": 0.15, "desc": "人才→收入"},
|
||||
"employee_engagement": {"type": "cost", "factor": -0.1, "desc": "敬业度→成本"},
|
||||
"innovation": {"type": "revenue", "factor": 0.2, "desc": "创新→收入"},
|
||||
}
|
||||
|
||||
# 默认基准财务数据(万元/月)
|
||||
DEFAULT_BASE_REVENUE = 1000.0 # 基准收入
|
||||
DEFAULT_BASE_COST = 700.0 # 基准成本
|
||||
DEFAULT_BASE_PROFIT = 300.0 # 基准利润
|
||||
|
||||
|
||||
def calculate_scenario(
|
||||
variables: List[Dict],
|
||||
scenario_type: str = "base", # "optimistic" / "base" / "pessimistic"
|
||||
base_revenue: float = DEFAULT_BASE_REVENUE,
|
||||
base_cost: float = DEFAULT_BASE_COST,
|
||||
) -> Dict:
|
||||
"""根据KPI变量列表和三情景系数计算财务影响
|
||||
|
||||
Args:
|
||||
variables: [{"kpi_code", "kpi_name", "category", "value", "step_optimistic", "step_base", "step_pessimistic"}, ...]
|
||||
scenario_type: 情景类型
|
||||
base_revenue: 基准收入
|
||||
base_cost: 基准成本
|
||||
|
||||
Returns:
|
||||
{revenue, cost, profit, profit_margin, kpi_impacts, details}
|
||||
"""
|
||||
step_key = {
|
||||
"optimistic": "step_optimistic",
|
||||
"base": "step_base",
|
||||
"pessimistic": "step_pessimistic",
|
||||
}.get(scenario_type, "step_base")
|
||||
|
||||
total_revenue_impact = 0.0
|
||||
total_cost_impact = 0.0
|
||||
total_profit_impact = 0.0
|
||||
total_cash_impact = 0.0
|
||||
base_profit_val = base_revenue - base_cost
|
||||
details = []
|
||||
|
||||
for var in variables:
|
||||
kpi_code = var.get("kpi_code", "")
|
||||
kpi_name = var.get("kpi_name", "")
|
||||
category = var.get("category", "")
|
||||
current_value = var.get("value", 0)
|
||||
step_value = var.get(step_key, 0)
|
||||
|
||||
# 变化百分比 (当前值变化 / 当前值)
|
||||
if current_value and current_value != 0:
|
||||
change_pct = step_value / abs(current_value) * 100
|
||||
else:
|
||||
change_pct = 0
|
||||
|
||||
# 查找类别映射
|
||||
mapping = KPI_CATEGORY_MAP.get(category, {"type": "revenue", "factor": 0.5, "desc": "通用影响"})
|
||||
impact_type = mapping["type"]
|
||||
factor = mapping["factor"]
|
||||
impact_desc = mapping["desc"]
|
||||
|
||||
# 计算财务影响 = 变化率 × 系数 × 基准值
|
||||
financial_impact = change_pct / 100 * factor
|
||||
if impact_type == "revenue":
|
||||
impact_amount = financial_impact * base_revenue
|
||||
total_revenue_impact += impact_amount
|
||||
elif impact_type == "cost":
|
||||
impact_amount = financial_impact * base_cost
|
||||
total_cost_impact += impact_amount
|
||||
elif impact_type == "profit":
|
||||
impact_amount = financial_impact * base_profit_val
|
||||
total_profit_impact += impact_amount
|
||||
elif impact_type == "cash":
|
||||
impact_amount = financial_impact * base_profit_val
|
||||
total_cash_impact += impact_amount
|
||||
else:
|
||||
impact_amount = 0
|
||||
|
||||
details.append({
|
||||
"kpi_code": kpi_code,
|
||||
"kpi_name": kpi_name,
|
||||
"category": category,
|
||||
"current_value": current_value,
|
||||
"scenario_value": step_value,
|
||||
"change_pct": round(change_pct, 2),
|
||||
"impact_type": impact_type,
|
||||
"impact_desc": impact_desc,
|
||||
"impact_amount": round(impact_amount, 2),
|
||||
})
|
||||
|
||||
# 合成最终财务数据
|
||||
final_revenue = base_revenue + total_revenue_impact
|
||||
final_cost = base_cost + total_cost_impact
|
||||
# 重新计算利润(考虑所有影响)
|
||||
final_profit = (final_revenue - final_cost) + total_profit_impact
|
||||
profit_margin = round(final_profit / final_revenue * 100, 2) if final_revenue else 0
|
||||
|
||||
return {
|
||||
"scenario_type": scenario_type,
|
||||
"base_revenue": base_revenue,
|
||||
"base_cost": base_cost,
|
||||
"base_profit": base_revenue - base_cost,
|
||||
"revenue": round(final_revenue, 2),
|
||||
"cost": round(final_cost, 2),
|
||||
"profit": round(final_profit, 2),
|
||||
"profit_margin": profit_margin,
|
||||
"revenue_impact": round(total_revenue_impact, 2),
|
||||
"cost_impact": round(total_cost_impact, 2),
|
||||
"profit_impact": round(total_profit_impact, 2),
|
||||
"cash_impact": round(total_cash_impact, 2),
|
||||
"kpi_impacts": details,
|
||||
}
|
||||
|
||||
|
||||
def run_three_scenarios(
|
||||
variables: List[Dict],
|
||||
base_revenue: float = DEFAULT_BASE_REVENUE,
|
||||
base_cost: float = DEFAULT_BASE_COST,
|
||||
months: int = 12,
|
||||
) -> Dict:
|
||||
"""运行三情景模拟,生成曲线数据
|
||||
|
||||
Args:
|
||||
variables: KPI变量列表,每个包含step_optimistic/step_base/step_pessimistic
|
||||
base_revenue: 基准月度收入
|
||||
base_cost: 基准月度成本
|
||||
months: 预测月数
|
||||
|
||||
Returns:
|
||||
{scenarios: [...], chart_data: {months, optimistic, base, pessimistic}, summary}
|
||||
"""
|
||||
optimistic = calculate_scenario(variables, "optimistic", base_revenue, base_cost)
|
||||
base = calculate_scenario(variables, "base", base_revenue, base_cost)
|
||||
pessimistic = calculate_scenario(variables, "pessimistic", base_revenue, base_cost)
|
||||
|
||||
# 生成月度曲线数据(按月线性趋近情景值)
|
||||
start_revenue = base_revenue
|
||||
start_cost = base_cost
|
||||
start_profit = base_revenue - base_cost
|
||||
|
||||
chart_data = {
|
||||
"months": [],
|
||||
"optimistic": {"revenue": [], "cost": [], "profit": []},
|
||||
"base": {"revenue": [], "cost": [], "profit": []},
|
||||
"pessimistic": {"revenue": [], "cost": [], "profit": []},
|
||||
}
|
||||
|
||||
for m in range(1, months + 1):
|
||||
progress = m / months # 从0到1线性趋近
|
||||
label = f"第{m}月" if months <= 12 else f"M{m}"
|
||||
|
||||
for scenario_type, scenario_data in [
|
||||
("optimistic", optimistic),
|
||||
("base", base),
|
||||
("pessimistic", pessimistic),
|
||||
]:
|
||||
rev = start_revenue + (scenario_data["revenue"] - start_revenue) * progress
|
||||
cst = start_cost + (scenario_data["cost"] - start_cost) * progress
|
||||
prf = start_profit + (scenario_data["profit"] - start_profit) * progress
|
||||
chart_data[scenario_type]["revenue"].append(round(rev, 2))
|
||||
chart_data[scenario_type]["cost"].append(round(cst, 2))
|
||||
chart_data[scenario_type]["profit"].append(round(prf, 2))
|
||||
|
||||
chart_data["months"].append(label)
|
||||
|
||||
# 汇总
|
||||
base_profit_val = base["profit"]
|
||||
scenarios_list = []
|
||||
for label, data in [
|
||||
("乐观", optimistic),
|
||||
("基准", base),
|
||||
("保守", pessimistic),
|
||||
]:
|
||||
deviation = data["profit"] - base_profit_val
|
||||
scenarios_list.append({
|
||||
"scenario": label,
|
||||
"revenue": data["revenue"],
|
||||
"cost": data["cost"],
|
||||
"profit": data["profit"],
|
||||
"profit_margin": data["profit_margin"],
|
||||
"deviation_from_base": round(deviation, 2),
|
||||
"deviation_pct": round(deviation / base_profit_val * 100, 2) if base_profit_val else 0,
|
||||
"kpi_impacts": data["kpi_impacts"],
|
||||
})
|
||||
|
||||
summary = {
|
||||
"expected_profit": round((optimistic["profit"] + base["profit"] + pessimistic["profit"]) / 3, 2),
|
||||
"best_profit": optimistic["profit"],
|
||||
"worst_profit": pessimistic["profit"],
|
||||
"base_profit": base["profit"],
|
||||
"variance": round(
|
||||
((optimistic["profit"] - base["profit"]) ** 2 +
|
||||
(base["profit"] - base["profit"]) ** 2 +
|
||||
(pessimistic["profit"] - base["profit"]) ** 2) / 3, 2
|
||||
),
|
||||
"base_revenue": base_revenue,
|
||||
"base_cost": base_cost,
|
||||
"months": months,
|
||||
}
|
||||
|
||||
return {
|
||||
"scenarios": scenarios_list,
|
||||
"chart_data": chart_data,
|
||||
"summary": summary,
|
||||
}
|
||||
Reference in New Issue
Block a user