From a1d014258f6e92255ea9ed61cd9d2ea652504653 Mon Sep 17 00:00:00 2001 From: Hermes CI Fix Date: Tue, 25 Aug 2026 17:42:40 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=89=A7=E8=A1=8C=E5=88=86=E8=A7=A3400?= =?UTF-8?q?=E9=94=99=E8=AF=AF=20=E2=80=94=20auto-decompose=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=89=B9=E9=87=8F=E6=A8=A1=E5=BC=8F(=E4=B8=8D?= =?UTF-8?q?=E4=BC=A0kpi=5Fid=E5=88=86=E8=A7=A3=E8=AF=A5=E5=B9=B4=E6=89=80?= =?UTF-8?q?=E6=9C=89=E9=A2=84=E7=AE=97KPI)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 原API只支持单KPI(kpi_id+annual_budget),前端UI按整体分解调用 → 400 - 新增批量模式:聚合该年所有KPI年度预算,逐KPI均分/按历史权重分解12个月 - 验证:批量分解4个KPI(收入536万/净利-48万等),v1.0月度48条落库 --- backend/app/api/budget.py | 99 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 4 deletions(-) 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: