feat: 预算版本管理API — 修复前端'加载版本失败'(budgetApi.versions未定义+后端无端点)
- 后端: GET /budget/versions(版本列表) POST /versions/submit /versions/approve /versions/diff - 前端: budgetApi.versions/versionSubmit/versionApprove/versionDiff - 数据: budget_plans已有v2.0 active 24条 - 验证: 版本列表200(v2.0 24KPI 预算730万) 版本对比200
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user