diff --git a/backend/app/api/budget.py b/backend/app/api/budget.py index 2147c359..243f9fa4 100644 --- a/backend/app/api/budget.py +++ b/backend/app/api/budget.py @@ -1056,6 +1056,103 @@ def budget_method_comparison(data: dict): } +@router.post("/apply-method") +def apply_budget_method( + data: dict, + db: Session = Depends(get_db), + current_user=Depends(require_auth), +): + """应用所选预算编制方法到预算计划(2026-08-26:三法并存,按用户场景选择后落地) + 接收: { method: 'incremental'|'zero_based'|'flexible', year: 2026, entity: 'hanke', ... } + 说明: 方法计算结果 → 写入/更新预算计划(version标注方法名,便于追溯) + """ + method = data.get("method", "zero_based") + year = data.get("year", datetime.now().year) + entity = data.get("entity", "hanke") + entity_id = data.get("entity_id", 1) + + # 复用method-comparison计算(获得三法结果) + comp = budget_method_comparison({ + "entity": entity, + "last_month_budget": data.get("last_month_budget", 91), + "current_revenue": data.get("current_revenue", 122), + "fixed_costs": data.get("fixed_costs", { + "rent": 15, "labor": 40, "entertainment": 16, "misc": 12, + }), + "variable_cost_rate": data.get("variable_cost_rate", 0.4862), + "increment_rate": data.get("increment_rate", 0.05), + }) + + # 找所选方法的结果 + selected = None + for m in comp["methods"]: + if m["id"] == method: + selected = m + break + if not selected: + raise HTTPException(400, "未知预算方法: " + method) + + # 找到该年的核心KPI(营业收入/净利润/费用率等) + # 取该年已有预算的KPI,或默认核心4个 + kpis = db.query(KPIDefinition).filter( + KPIDefinition.entity_id == entity_id, + KPIDefinition.status == "active", + KPIDefinition.kpi_code.in_(["F_REVENUE", "F_NET_PROFIT", "F_COST_RATIO", "F_GROSS_MARGIN"]), + ).all() + if not kpis: + raise HTTPException(400, "未找到可应用的KPI") + + # 方法结果解释为收入预算(核心KPI应用) + # incremental/flexible/zero_based 的 result_value 均为"预算总额(万)" + # 写入F_REVENUE年度预算(period=YYYY-00 表示年度) + version = f"{method}-{datetime.now().strftime('%Y%m%d')}" + applied = [] + for kpi in kpis: + period = f"{year}-00" + # 删除旧版本的同KPI年度预算 + db.query(BudgetPlan).filter( + BudgetPlan.kpi_id == kpi.id, + BudgetPlan.period == period, + BudgetPlan.version.like(f"{method}-%"), + ).delete() + + # 各KPI的应用值(简化:收入用方法结果,其他按比例) + if kpi.kpi_code == "F_REVENUE": + budget_val = selected["result_value"] + elif kpi.kpi_code == "F_NET_PROFIT": + budget_val = round(selected["result_value"] * 0.02, 1) # 净利率约2% + elif kpi.kpi_code == "F_COST_RATIO": + budget_val = round(selected["result_value"] * 0.22, 1) # 费用率约22% + else: # F_GROSS_MARGIN + budget_val = round(selected["result_value"] * 0.18, 1) # 毛利率约18% + + bp = BudgetPlan( + entity_id=entity_id, + kpi_id=kpi.id, + period=period, + budget_value=budget_val, + budget_year=year, + budget_month=0, + version=version, + status="active", + remark=f"{selected['name']}应用({selected['result_value']}万) 来源{method}", + ) + db.add(bp) + applied.append({"kpi_code": kpi.kpi_code, "budget_value": budget_val}) + + db.commit() + return { + "message": f"已应用「{selected['name']}」到预算计划", + "method": method, + "method_name": selected["name"], + "version": version, + "total_budget": selected["result_value"], + "detail": selected["detail"], + "applied": applied, + "note": "选择哪种方法取决于场景:增量=稳定业务快速编;零基=成本优化专项;弹性=收入波动大。方法结果写入年度预算(period=YYYY-00),可在版本管理中查看。", + } + + # ============ 预算版本管理 API(2026-08-25 补充,修复前端"加载版本失败") ============ @router.get("/versions") diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index e4dca5b4..a7696b66 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -178,6 +178,7 @@ 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), + applyMethod: (data: any) => api.post('/budget/apply-method', data), // 版本管理 versions: (params?: any) => api.get('/budget/versions', { params }), versionSubmit: (data: any) => api.post('/budget/versions/submit', data), diff --git a/frontend/src/views/BudgetManagement.vue b/frontend/src/views/BudgetManagement.vue index eb02aca8..52fa980d 100644 --- a/frontend/src/views/BudgetManagement.vue +++ b/frontend/src/views/BudgetManagement.vue @@ -669,10 +669,23 @@ async function refreshMethodComparison() { } catch { ElMessage.error('加载预算方法对比失败') } } -function confirmMethod() { +async function confirmMethod() { const m = selectedMethodDetail.value - if (m) { - ElMessage.success(`已选择「${m.name}」,预算结果:${m.result_value} 万`) + if (!m) return + try { + const r: any = await budgetApi.applyMethod({ + method: m.id, + year: currentYear, + entity: 'hanke', + entity_id: getEntityId(), + last_month_budget: 91, + current_revenue: 122, + }) + ElMessage.success(r.message || `已应用「${m.name}」`) + // 刷新预算列表 + loadBudget() + } catch (e: any) { + ElMessage.error(e?.response?.data?.detail || e?.message || '应用失败') } }