"""预算自动从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, }