feat: P0/P1/P2全部功能 — 四层泳道/视角切换/KPI看板/预警/差异反打/预算/知识面板/回顾会/情景预测/Excel导入/角色权限
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
"""预算自动从KPI推算 API — P1-2
|
||||
|
||||
根据KPI的目标值自动生成预算建议。
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.auth_middleware import require_auth, require_role
|
||||
from app.models import KPIDefinition, BudgetPlan, KPIValue, OperationLog
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger("cma.budget_gen")
|
||||
|
||||
router = APIRouter(prefix="/api/cma/budget", tags=["KPI→预算"],
|
||||
dependencies=[Depends(require_role("ceo", "finance"))],
|
||||
)
|
||||
|
||||
|
||||
def _calc_budget(kpi: KPIDefinition) -> dict:
|
||||
"""根据KPI类型推算预算
|
||||
|
||||
算法:
|
||||
- 降本类: (当前值-目标值)×0.3
|
||||
- 增收类: 目标增收额×0.2
|
||||
- 能力类: 人均培训成本×人数
|
||||
- 系统类: 按模块开发费估算
|
||||
"""
|
||||
category = kpi.category or ""
|
||||
target = kpi.target_value or 0
|
||||
|
||||
result = {
|
||||
"suggested_budget": 0,
|
||||
"calc_logic": "",
|
||||
"calc_type": "未知",
|
||||
}
|
||||
|
||||
# 降本类: cost_control, cash_risk
|
||||
if category in ("cost_control", "cash_risk", "asset_efficiency"):
|
||||
result["calc_type"] = "降本类"
|
||||
# 当前值需要从最新的KPIValue获取
|
||||
# 这里返回算法描述,前端传入当前值
|
||||
result["calc_type_desc"] = "(当前值-目标值)×0.3"
|
||||
result["suggested_budget"] = 0 # 需要前端传当前值
|
||||
|
||||
# 增收类: revenue_growth, profitability
|
||||
elif category in ("revenue_growth", "profitability", "customer_scale"):
|
||||
result["calc_type"] = "增收类"
|
||||
result["calc_type_desc"] = "目标增收额×0.2"
|
||||
result["suggested_budget"] = round(target * 0.2, 2)
|
||||
|
||||
# 能力类: talent_pipeline, employee_engagement, innovation
|
||||
elif category in ("talent_pipeline", "employee_engagement", "innovation"):
|
||||
result["calc_type"] = "能力类"
|
||||
result["calc_type_desc"] = "人均培训成本×人数"
|
||||
result["suggested_budget"] = 0 # 需要外部参数
|
||||
|
||||
# 系统类: 默认为系统类
|
||||
elif category in ("supply_chain", "delivery_quality", "customer_concentration", "customer_satisfaction"):
|
||||
result["calc_type"] = "系统类"
|
||||
result["calc_type_desc"] = "按功能模块开发费估算"
|
||||
result["suggested_budget"] = round(target * 0.15, 2)
|
||||
|
||||
# 其他未分类
|
||||
else:
|
||||
result["calc_type"] = "系统类"
|
||||
result["calc_type_desc"] = "按功能模块开发费估算"
|
||||
result["suggested_budget"] = round(target * 0.15, 2)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/kpi-budget-candidates")
|
||||
def get_kpi_budget_candidates(
|
||||
year: int = None,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取可用于生成预算的KPI列表,按类型分类"""
|
||||
if not year:
|
||||
year = datetime.now().year
|
||||
|
||||
kpis = db.query(KPIDefinition).filter(KPIDefinition.status == "active").all()
|
||||
|
||||
# 获取每个KPI的最新实际值
|
||||
latest_values = {}
|
||||
for kpi in kpis:
|
||||
v = db.query(KPIValue).filter(
|
||||
KPIValue.kpi_id == kpi.id
|
||||
).order_by(KPIValue.calculated_at.desc()).first()
|
||||
if v:
|
||||
latest_values[kpi.id] = v.actual_value
|
||||
|
||||
# 分类
|
||||
categorized = {
|
||||
"cost_reduction": [], # 降本类
|
||||
"revenue_growth": [], # 增收类
|
||||
"capability": [], # 能力类
|
||||
"system": [], # 系统类
|
||||
}
|
||||
|
||||
for kpi in kpis:
|
||||
calc_info = _calc_budget(kpi)
|
||||
current_val = latest_values.get(kpi.id)
|
||||
|
||||
# 降本类: 需要当前值
|
||||
if calc_info["calc_type"] == "降本类":
|
||||
if current_val is not None and kpi.target_value:
|
||||
diff = current_val - kpi.target_value
|
||||
suggested = round(max(diff, 0) * 0.3, 2)
|
||||
calc_logic = f"当前值{current_val}-目标值{kpi.target_value}={diff:.2f},×0.3={suggested:.2f}"
|
||||
else:
|
||||
suggested = 0
|
||||
calc_logic = "缺少当前值或目标值,无法计算"
|
||||
item = {
|
||||
"id": kpi.id,
|
||||
"kpi_code": kpi.kpi_code,
|
||||
"kpi_name": kpi.kpi_name,
|
||||
"dimension": kpi.dimension,
|
||||
"category": kpi.category,
|
||||
"calc_type": "降本类",
|
||||
"target_value": kpi.target_value,
|
||||
"current_value": current_val,
|
||||
"suggested_budget": suggested,
|
||||
"calc_logic": calc_logic,
|
||||
}
|
||||
categorized["cost_reduction"].append(item)
|
||||
|
||||
elif calc_info["calc_type"] == "增收类":
|
||||
suggested = round((kpi.target_value or 0) * 0.2, 2)
|
||||
calc_logic = f"目标增收额{kpi.target_value}×0.2={suggested:.2f}"
|
||||
item = {
|
||||
"id": kpi.id,
|
||||
"kpi_code": kpi.kpi_code,
|
||||
"kpi_name": kpi.kpi_name,
|
||||
"dimension": kpi.dimension,
|
||||
"category": kpi.category,
|
||||
"calc_type": "增收类",
|
||||
"target_value": kpi.target_value,
|
||||
"current_value": current_val,
|
||||
"suggested_budget": suggested,
|
||||
"calc_logic": calc_logic,
|
||||
}
|
||||
categorized["revenue_growth"].append(item)
|
||||
|
||||
elif calc_info["calc_type"] == "能力类":
|
||||
# 假设人均培训成本2000元, 默认10人
|
||||
suggested = round(2000 * 10, 2)
|
||||
calc_logic = f"人均培训成本2000元×10人={suggested:.2f}(可调整人数和单价)"
|
||||
item = {
|
||||
"id": kpi.id,
|
||||
"kpi_code": kpi.kpi_code,
|
||||
"kpi_name": kpi.kpi_name,
|
||||
"dimension": kpi.dimension,
|
||||
"category": kpi.category,
|
||||
"calc_type": "能力类",
|
||||
"target_value": kpi.target_value,
|
||||
"current_value": current_val,
|
||||
"suggested_budget": suggested,
|
||||
"calc_logic": calc_logic,
|
||||
"per_head_cost": 2000,
|
||||
"head_count": 10,
|
||||
}
|
||||
categorized["capability"].append(item)
|
||||
|
||||
else: # 系统类
|
||||
suggested = round((kpi.target_value or 0) * 0.15, 2)
|
||||
if suggested <= 0:
|
||||
suggested = 30000 # 默认3万
|
||||
calc_logic = "按模块开发费估算: 默认30000元(可调整)"
|
||||
else:
|
||||
calc_logic = f"目标值{kpi.target_value}×0.15={suggested:.2f}"
|
||||
item = {
|
||||
"id": kpi.id,
|
||||
"kpi_code": kpi.kpi_code,
|
||||
"kpi_name": kpi.kpi_name,
|
||||
"dimension": kpi.dimension,
|
||||
"category": kpi.category,
|
||||
"calc_type": "系统类",
|
||||
"target_value": kpi.target_value,
|
||||
"current_value": current_val,
|
||||
"suggested_budget": suggested,
|
||||
"calc_logic": calc_logic,
|
||||
}
|
||||
categorized["system"].append(item)
|
||||
|
||||
return {"data": categorized}
|
||||
|
||||
|
||||
@router.post("/generate-from-kpis")
|
||||
def generate_budget_from_kpis(
|
||||
data: dict,
|
||||
db: Session = Depends(get_db),
|
||||
current_user=Depends(require_auth),
|
||||
):
|
||||
"""从选中的KPI生成预算科目
|
||||
|
||||
Body: {
|
||||
year: int,
|
||||
month: int,
|
||||
version: string,
|
||||
items: [
|
||||
{
|
||||
kpi_id: int,
|
||||
budget_amount: float, // 用户可编辑
|
||||
calc_logic: string,
|
||||
calc_type: string,
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
year = data.get("year", datetime.now().year)
|
||||
month = data.get("month", datetime.now().month + 1)
|
||||
version = data.get("version", "v1.0")
|
||||
items = data.get("items", [])
|
||||
|
||||
if not items:
|
||||
raise HTTPException(400, "请至少选择一个KPI")
|
||||
|
||||
period = f"{year}-{month:02d}"
|
||||
results = []
|
||||
total_amount = 0
|
||||
|
||||
for item in items:
|
||||
kpi_id = item.get("kpi_id")
|
||||
budget_amount = item.get("budget_amount")
|
||||
calc_logic = item.get("calc_logic", "")
|
||||
calc_type = item.get("calc_type", "")
|
||||
|
||||
if not kpi_id or budget_amount is None:
|
||||
continue
|
||||
|
||||
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
|
||||
if not kpi:
|
||||
continue
|
||||
|
||||
# 检查是否已有记录
|
||||
existing = db.query(BudgetPlan).filter(
|
||||
BudgetPlan.kpi_id == kpi_id,
|
||||
BudgetPlan.period == period,
|
||||
BudgetPlan.version == version,
|
||||
BudgetPlan.status == "active",
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
existing.budget_value = budget_amount
|
||||
existing.source_type = "kpi_generated"
|
||||
existing.source_kpi_id = kpi_id
|
||||
existing.calc_logic = calc_logic
|
||||
existing.remark = f"KPI推算({calc_type}): {calc_logic}"
|
||||
plan_id = existing.id
|
||||
else:
|
||||
plan = BudgetPlan(
|
||||
kpi_id=kpi_id,
|
||||
period=period,
|
||||
budget_value=budget_amount,
|
||||
budget_year=year,
|
||||
budget_month=month,
|
||||
version=version,
|
||||
status="active",
|
||||
source_type="kpi_generated",
|
||||
source_kpi_id=kpi_id,
|
||||
calc_logic=calc_logic,
|
||||
remark=f"KPI推算({calc_type}): {calc_logic}",
|
||||
created_by=current_user.name if hasattr(current_user, "name") else "",
|
||||
)
|
||||
db.add(plan)
|
||||
db.flush()
|
||||
plan_id = plan.id
|
||||
|
||||
total_amount += budget_amount
|
||||
results.append({
|
||||
"kpi_id": kpi_id,
|
||||
"kpi_code": kpi.kpi_code,
|
||||
"kpi_name": kpi.kpi_name,
|
||||
"budget_amount": budget_amount,
|
||||
"calc_logic": calc_logic,
|
||||
"plan_id": plan_id,
|
||||
})
|
||||
|
||||
# 操作日志
|
||||
log = OperationLog(
|
||||
user_id=getattr(current_user, "id", None),
|
||||
action="kpi_generate_budget",
|
||||
target_type="budget",
|
||||
detail=json.dumps({
|
||||
"year": year,
|
||||
"month": month,
|
||||
"version": version,
|
||||
"item_count": len(results),
|
||||
"total_amount": total_amount,
|
||||
}, ensure_ascii=False),
|
||||
)
|
||||
db.add(log)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"message": f"已从{len(results)}个KPI生成预算,合计¥{total_amount:,.2f}",
|
||||
"total_amount": total_amount,
|
||||
"items": results,
|
||||
}
|
||||
Reference in New Issue
Block a user