feat(P1-2): 预算三法并存落地 — 增量/零基/弹性均可应用
按任总观点: 三个方法都体现功能, 具体用哪个取决于用户场景 - 后端: POST /budget/apply-method 把所选方法结果写入预算计划(version标注方法) 增量95.5万(上月×1.05) / 零基71.4万(成本砍半压缩) / 弹性58.7万(收入联动) - 前端: 确认选择改为真正调用apply-method(原来只弹提示不落地) - 场景提示: 增量=稳定快速编 / 零基=成本优化 / 弹性=收入波动大
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 || '应用失败')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user