fix: 执行分解400错误 — auto-decompose支持批量模式(不传kpi_id分解该年所有预算KPI)

- 原API只支持单KPI(kpi_id+annual_budget),前端UI按整体分解调用 → 400
- 新增批量模式:聚合该年所有KPI年度预算,逐KPI均分/按历史权重分解12个月
- 验证:批量分解4个KPI(收入536万/净利-48万等),v1.0月度48条落库
This commit is contained in:
Hermes CI Fix
2026-08-25 17:42:40 +08:00
parent 863514fec0
commit a1d014258f
+95 -4
View File
@@ -10,7 +10,7 @@ from datetime import datetime
from app.database import get_db
from app.deps import get_entity_id
from app.auth_middleware import require_auth, require_role
from app.models import BudgetPlan, KPIDefinition, OperationLog
from app.models import BudgetPlan, KPIDefinition, OperationLog, KPIValue
router = APIRouter(prefix="/api/cma/budget", tags=["预算管理"],
dependencies=[Depends(require_role("ceo", "finance", "it"))],
@@ -171,15 +171,106 @@ def auto_decompose_budget(
db: Session = Depends(get_db),
current_user=Depends(require_auth),
):
"""自动分解年度预算到月度(均分或按历史权重)"""
"""自动分解年度预算到月度(均分或按历史权重)
支持两种模式:
1. 单KPI:传 kpi_id + annual_budget
2. 批量:不传 kpi_id,分解该年所有已有年度预算的KPI
"""
kpi_id = data.get("kpi_id")
year = data.get("year", datetime.now().year)
annual_budget = data.get("annual_budget")
method = data.get("method", "equal") # equal / weighted
version = data.get("version", "v1.0")
if not kpi_id or annual_budget is None:
raise HTTPException(400, "缺少必要参数: kpi_id, annual_budget")
# ── 批量模式:不传kpi_id → 分解该年所有有年度预算的KPI ──
if not kpi_id:
# 找该年已存在的年度预算(period=YYYY-00 或已按月填的KPI汇总)
# 优先用 budget_plans 中该年的预算作为年度总额
year_budget_rows = db.query(BudgetPlan).filter(
BudgetPlan.entity_id == 1,
BudgetPlan.budget_year == year,
BudgetPlan.status == "active",
).all()
# 按KPI聚合年度预算总额
kpi_annual = {}
for r in year_budget_rows:
kpi_annual[r.kpi_id] = kpi_annual.get(r.kpi_id, 0) + (r.budget_value or 0)
if not kpi_annual:
raise HTTPException(400, "该年度没有可分解的预算,请先在预算执行中录入年度预算")
results = []
created_count = 0
for kid, annual in kpi_annual.items():
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kid).first()
if not kpi:
continue
# 计算各月权重
if method == "weighted":
last_year = year - 1
values = db.query(KPIValue).filter(
KPIValue.kpi_id == kid,
KPIValue.period.like(f"{last_year}-%"),
KPIValue.actual_value.isnot(None),
).order_by(KPIValue.period.asc()).all()
total = sum(v.actual_value for v in values)
weights = {v.period: v.actual_value / total for v in values} if total > 0 else {}
else:
weights = {}
monthly = []
for m in range(1, 13):
period = f"{year}-{m:02d}"
weight = weights.get(period, 1 / 12) if method == "weighted" and weights else 1 / 12
monthly_value = round(annual * weight, 2)
existing = db.query(BudgetPlan).filter(
BudgetPlan.kpi_id == kid,
BudgetPlan.period == period,
BudgetPlan.version == version,
BudgetPlan.status == "active",
).first()
if existing:
existing.budget_value = monthly_value
existing.updated_at = datetime.now()
else:
db.add(BudgetPlan(
entity_id=1,
kpi_id=kid,
period=period,
budget_value=monthly_value,
budget_year=year,
budget_month=m,
version=version,
status="active",
))
created_count += 1
monthly.append(monthly_value)
results.append({
"kpi_id": kid,
"kpi_code": kpi.kpi_code,
"kpi_name": kpi.kpi_name,
"annual_budget": round(annual, 2),
"method": "equal" if not weights else "weighted",
"monthly": monthly,
"monthly_count": 12,
})
db.commit()
return {
"message": f"批量分解完成:{len(results)}个KPI",
"count": len(results),
"results": results,
"created": created_count,
}
# ── 单KPI模式(原有逻辑)──
if annual_budget is None:
raise HTTPException(400, "缺少必要参数: annual_budget")
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
if not kpi: