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:
@@ -10,7 +10,7 @@ from datetime import datetime
|
|||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.deps import get_entity_id
|
from app.deps import get_entity_id
|
||||||
from app.auth_middleware import require_auth, require_role
|
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=["预算管理"],
|
router = APIRouter(prefix="/api/cma/budget", tags=["预算管理"],
|
||||||
dependencies=[Depends(require_role("ceo", "finance", "it"))],
|
dependencies=[Depends(require_role("ceo", "finance", "it"))],
|
||||||
@@ -171,15 +171,106 @@ def auto_decompose_budget(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user=Depends(require_auth),
|
current_user=Depends(require_auth),
|
||||||
):
|
):
|
||||||
"""自动分解年度预算到月度(均分或按历史权重)"""
|
"""自动分解年度预算到月度(均分或按历史权重)
|
||||||
|
支持两种模式:
|
||||||
|
1. 单KPI:传 kpi_id + annual_budget
|
||||||
|
2. 批量:不传 kpi_id,分解该年所有已有年度预算的KPI
|
||||||
|
"""
|
||||||
kpi_id = data.get("kpi_id")
|
kpi_id = data.get("kpi_id")
|
||||||
year = data.get("year", datetime.now().year)
|
year = data.get("year", datetime.now().year)
|
||||||
annual_budget = data.get("annual_budget")
|
annual_budget = data.get("annual_budget")
|
||||||
method = data.get("method", "equal") # equal / weighted
|
method = data.get("method", "equal") # equal / weighted
|
||||||
version = data.get("version", "v1.0")
|
version = data.get("version", "v1.0")
|
||||||
|
|
||||||
if not kpi_id or annual_budget is None:
|
# ── 批量模式:不传kpi_id → 分解该年所有有年度预算的KPI ──
|
||||||
raise HTTPException(400, "缺少必要参数: kpi_id, annual_budget")
|
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()
|
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
|
||||||
if not kpi:
|
if not kpi:
|
||||||
|
|||||||
Reference in New Issue
Block a user