feat(P1-2): 预算三法并存落地 — 增量/零基/弹性均可应用

按任总观点: 三个方法都体现功能, 具体用哪个取决于用户场景
- 后端: POST /budget/apply-method 把所选方法结果写入预算计划(version标注方法)
  增量95.5万(上月×1.05) / 零基71.4万(成本砍半压缩) / 弹性58.7万(收入联动)
- 前端: 确认选择改为真正调用apply-method(原来只弹提示不落地)
- 场景提示: 增量=稳定快速编 / 零基=成本优化 / 弹性=收入波动大
This commit is contained in:
Hermes CI Fix
2026-08-26 22:43:46 +08:00
parent e330545d59
commit 95b73c9b90
3 changed files with 114 additions and 3 deletions
+97
View File
@@ -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),可在版本管理中查看。",
}
# ============ 预算版本管理 API2026-08-25 补充,修复前端"加载版本失败" ============
@router.get("/versions")
+1
View File
@@ -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),
+16 -3
View File
@@ -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 || '应用失败')
}
}