diff --git a/backend/app/api/budget.py b/backend/app/api/budget.py index 416c2dcd..6479f0e6 100644 --- a/backend/app/api/budget.py +++ b/backend/app/api/budget.py @@ -954,3 +954,160 @@ def budget_method_comparison(data: dict): "recommended": recommended, "recommended_name": "零基预算", } + + +# ============ 预算版本管理 API(2026-08-25 补充,修复前端"加载版本失败") ============ + +@router.get("/versions") +def list_budget_versions( + year: Optional[int] = Query(None, description="预算年份"), + db: Session = Depends(get_db), + entity_id: int = Depends(get_entity_id), +): + """预算版本列表 — 按版本聚合统计""" + query = db.query(BudgetPlan).filter(BudgetPlan.entity_id == entity_id) + if year: + query = query.filter(BudgetPlan.budget_year == year) + + rows = query.all() + # 按version+status聚合 + version_map = {} + for r in rows: + key = (r.version or "v1.0", r.status or "active") + if key not in version_map: + version_map[key] = { + "version": r.version or "v1.0", + "status": r.status or "active", + "kpi_count": 0, + "total_budget": 0.0, + "periods": set(), + "updated_at": r.updated_at, + } + v = version_map[key] + v["kpi_count"] += 1 + v["total_budget"] += r.budget_value or 0 + v["periods"].add(r.period) + if r.updated_at and (v["updated_at"] is None or r.updated_at > v["updated_at"]): + v["updated_at"] = r.updated_at + + result = [] + for key, v in version_map.items(): + result.append({ + "version": v["version"], + "status": v["status"], + "kpi_count": v["kpi_count"], + "total_budget": round(v["total_budget"], 2), + "period_count": len(v["periods"]), + "year": year, + "updated_at": v["updated_at"].strftime("%Y-%m-%d %H:%M") if v["updated_at"] else "", + }) + result.sort(key=lambda x: x["version"], reverse=True) + return result + + +@router.post("/versions/submit") +def submit_budget_version( + data: dict, + db: Session = Depends(get_db), + entity_id: int = Depends(get_entity_id), +): + """提交版本审批:active → submitted""" + version = data.get("version", "v1.0") + rows = db.query(BudgetPlan).filter( + BudgetPlan.entity_id == entity_id, + BudgetPlan.version == version, + ).all() + if not rows: + raise HTTPException(404, f"版本 {version} 不存在") + for r in rows: + r.status = "submitted" + db.commit() + return {"success": True, "version": version, "status": "submitted", "count": len(rows)} + + +@router.post("/versions/approve") +def approve_budget_version( + data: dict, + db: Session = Depends(get_db), + entity_id: int = Depends(get_entity_id), +): + """审批版本:submitted → approved/rejected""" + version = data.get("version", "v1.0") + action = data.get("action", "approved") + rows = db.query(BudgetPlan).filter( + BudgetPlan.entity_id == entity_id, + BudgetPlan.version == version, + ).all() + if not rows: + raise HTTPException(404, f"版本 {version} 不存在") + new_status = "approved" if action == "approved" else "rejected" + for r in rows: + r.status = new_status + db.commit() + return {"success": True, "version": version, "status": new_status, "count": len(rows)} + + +@router.post("/versions/diff") +def diff_budget_versions( + data: dict, + db: Session = Depends(get_db), + entity_id: int = Depends(get_entity_id), +): + """版本差异对比:v1.0 vs v2.0 逐KPI差异""" + version_a = data.get("version_a", "") + version_b = data.get("version_b", "") + year = data.get("year") + if not version_a or not version_b: + raise HTTPException(400, "需要 version_a 和 version_b") + + def load_version(ver: str): + query = db.query(BudgetPlan).filter( + BudgetPlan.entity_id == entity_id, + BudgetPlan.version == ver, + ) + if year: + query = query.filter(BudgetPlan.budget_year == year) + return {r.kpi_id: r for r in query.all()} + + va = load_version(version_a) + vb = load_version(version_b) + + kpi_ids = set(va.keys()) | set(vb.keys()) + kpis = db.query(KPIDefinition).filter(KPIDefinition.id.in_(kpi_ids)).all() + kpi_map = {k.id: k for k in kpis} + + diffs = [] + total_a = total_b = 0.0 + changed_count = 0 + for kid in sorted(kpi_ids): + ra = va.get(kid) + rb = vb.get(kid) + val_a = ra.budget_value if ra else 0 + val_b = rb.budget_value if rb else 0 + total_a += val_a + total_b += val_b + k = kpi_map.get(kid) + diff = val_b - val_a + if abs(diff) > 0.001: + changed_count += 1 + diffs.append({ + "kpi_code": k.kpi_code if k else "", + "kpi_name": k.kpi_name if k else f"KPI-{kid}", + "version_a": round(val_a, 2), + "version_b": round(val_b, 2), + "diff": round(diff, 2), + }) + + diffs.sort(key=lambda x: -abs(x["diff"])) + return { + "summary": { + "version_a": version_a, + "version_b": version_b, + "kpi_total": len(kpi_ids), + "changed_count": changed_count, + "total_a": round(total_a, 2), + "total_b": round(total_b, 2), + "total_diff": round(total_b - total_a, 2), + }, + "diffs": diffs, + } diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 8285a84f..2764a3a4 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -178,6 +178,11 @@ export const budgetApi = { autoDecompose: (data: any) => api.post('/budget/auto-decompose', data), deviationReport: (params?: any) => api.get('/budget/deviation-report', { params }), methodComparison: (data: any) => api.post('/budget/method-comparison', data), + // 版本管理 + versions: (params?: any) => api.get('/budget/versions', { params }), + versionSubmit: (data: any) => api.post('/budget/versions/submit', data), + versionApprove: (data: any) => api.post('/budget/versions/approve', data), + versionDiff: (data: any) => api.post('/budget/versions/diff', data), // 持续规划 getConfig: () => api.get('/budget/config'), setConfig: (data: any) => api.post('/budget/config', data),