diff --git a/backend/app/api/budget.py b/backend/app/api/budget.py index fa6c2c6b..0c1bd227 100644 --- a/backend/app/api/budget.py +++ b/backend/app/api/budget.py @@ -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: