302 lines
10 KiB
Python
302 lines
10 KiB
Python
"""预算自动从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,
|
||
}
|