feat: P0/P1/P2全部功能 — 四层泳道/视角切换/KPI看板/预警/差异反打/预算/知识面板/回顾会/情景预测/Excel导入/角色权限
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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