feat(P1-3): 相关成本决策模块 — 官方25%权重'商业决策分析'核心
新增 /predict/relevant-decision API + 前端Tab(3场景):
1. 自制vs外购: 相关成本比较+无差别点(增量成本视角)
2. 特殊订单: 剩余产能下价格>变动成本即接受(增量利润)
3. 产品组合(约束理论): 单位约束资源边际贡献排序
验证: 自制850k vs 外购900k→自制; 特殊订单单位贡献25→接受;
产品组合B产品单位约束贡献50最高→优先
This commit is contained in:
@@ -161,6 +161,97 @@ def api_cvp_detailed(data: dict):
|
||||
raise HTTPException(400, f"CVP详细分析失败: {str(e)}")
|
||||
|
||||
|
||||
# ── 相关成本决策(CMA P2商业决策分析25%权重核心) ──────────────────
|
||||
|
||||
|
||||
@router.post("/relevant-decision")
|
||||
def api_relevant_decision(data: dict):
|
||||
"""相关成本决策分析(CMA P2 商业决策分析核心内容)
|
||||
场景: make-or-buy自制外购 / special-order特殊订单 / product-mix产品组合
|
||||
"""
|
||||
try:
|
||||
decision_type = data.get("type", "make_or_buy")
|
||||
|
||||
if decision_type == "make_or_buy":
|
||||
# 自制vs外购决策
|
||||
# 相关成本 = 增量成本(只有随决策变化的成本才是相关的)
|
||||
make_var_cost = float(data.get("make_variable_cost", 0)) # 自制单位变动成本
|
||||
make_fixed = float(data.get("make_fixed_cost", 0)) # 自制新增固定成本
|
||||
buy_price = float(data.get("buy_price", 0)) # 外购单价
|
||||
demand = float(data.get("demand", 0)) # 需求量
|
||||
existing_fixed = float(data.get("existing_fixed_cost", 0)) # 现有固定成本(无关成本,自制不增加则忽略)
|
||||
|
||||
make_total = make_var_cost * demand + make_fixed
|
||||
buy_total = buy_price * demand
|
||||
diff = buy_total - make_total # >0自制省钱
|
||||
|
||||
return {
|
||||
"type": "自制vs外购",
|
||||
"make_total_cost": round(make_total, 2),
|
||||
"buy_total_cost": round(buy_total, 2),
|
||||
"difference": round(diff, 2),
|
||||
"recommendation": "自制" if diff > 0 else "外购",
|
||||
"reason": f"自制总成本{make_total:.2f} vs 外购总成本{buy_total:.2f},{'自制节省' + str(round(diff,2)) if diff > 0 else '外购节省' + str(round(-diff,2))}",
|
||||
"unit_make_cost": round(make_var_cost + (make_fixed / demand if demand else 0), 2),
|
||||
"unit_buy_price": buy_price,
|
||||
"indifferent_point": round(make_fixed / (buy_price - make_var_cost), 2) if buy_price > make_var_cost else None,
|
||||
"notes": "仅考虑相关成本(增量成本);现有固定成本若不受决策影响则无关",
|
||||
}
|
||||
|
||||
elif decision_type == "special_order":
|
||||
# 特殊订单决策(有剩余产能时,只要价格>单位变动成本即接受)
|
||||
normal_price = float(data.get("normal_price", 0))
|
||||
special_price = float(data.get("special_price", 0))
|
||||
var_cost = float(data.get("variable_cost", 0))
|
||||
order_qty = float(data.get("order_qty", 0))
|
||||
capacity_used = float(data.get("capacity_used", 0)) # 特殊订单占用产能%
|
||||
extra_fixed = float(data.get("extra_fixed_cost", 0)) # 一次性额外固定成本
|
||||
|
||||
contribution_per_unit = special_price - var_cost
|
||||
total_contribution = contribution_per_unit * order_qty - extra_fixed
|
||||
accept = total_contribution > 0 and capacity_used <= 100
|
||||
|
||||
return {
|
||||
"type": "特殊订单",
|
||||
"unit_contribution": round(contribution_per_unit, 2),
|
||||
"total_contribution": round(total_contribution, 2),
|
||||
"extra_fixed_cost": extra_fixed,
|
||||
"capacity_used_pct": capacity_used,
|
||||
"recommendation": "接受" if accept else "拒绝",
|
||||
"reason": f"单价{special_price} - 变动成本{var_cost} = 单位贡献{contribution_per_unit:.2f}" +
|
||||
(f",共{total_contribution:.2f} > 0 且产能{capacity_used}%够用 → 接受(增量利润)" if accept else
|
||||
f",总贡献{total_contribution:.2f} ≤ 0 或产能不足 → 拒绝"),
|
||||
"notes": "有剩余产能时,只要价格>变动成本且不冲击正常市场即可接受;固定成本无关",
|
||||
}
|
||||
|
||||
elif decision_type == "product_mix":
|
||||
# 产品组合决策(约束理论:单位约束资源的边际贡献最大者优先)
|
||||
products = data.get("products", []) # [{name, price, var_cost, constraint_usage, demand}]
|
||||
results = []
|
||||
for p in products:
|
||||
cm_per_unit = float(p.get("price", 0)) - float(p.get("var_cost", 0))
|
||||
cm_per_constraint = cm_per_unit / float(p.get("constraint_usage", 1))
|
||||
results.append({
|
||||
"name": p.get("name", ""),
|
||||
"unit_contribution": round(cm_per_unit, 2),
|
||||
"constraint_usage": float(p.get("constraint_usage", 1)),
|
||||
"contribution_per_constraint": round(cm_per_constraint, 2),
|
||||
"demand": float(p.get("demand", 0)),
|
||||
})
|
||||
# 按单位约束资源贡献排序(约束理论优先)
|
||||
results.sort(key=lambda x: x["contribution_per_constraint"], reverse=True)
|
||||
return {
|
||||
"type": "产品组合(约束理论)",
|
||||
"ranking": results,
|
||||
"recommendation": f"优先生产「{results[0]['name']}」(单位约束贡献{results[0]['contribution_per_constraint']}最高)",
|
||||
"notes": "瓶颈资源下,按单位约束资源的边际贡献排序,而非单位边际贡献",
|
||||
}
|
||||
|
||||
raise HTTPException(400, "未知决策类型: " + str(decision_type))
|
||||
except Exception as e:
|
||||
raise HTTPException(400, f"相关成本决策失败: {str(e)}")
|
||||
|
||||
|
||||
# ── 现金流预测(AI事前预警) ────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
@@ -218,6 +218,7 @@ export const predictApi = {
|
||||
investment: (data: any) => api.post('/predict/investment', data),
|
||||
sensitivity: (data: any) => api.post('/predict/sensitivity', data),
|
||||
scenario: (data: any) => api.post('/predict/scenario', data),
|
||||
relevantDecision: (data: any) => api.post('/predict/relevant-decision', data),
|
||||
growthQuality: (data: any) => api.post('/predict/growth-quality', data),
|
||||
// AI事前预警
|
||||
cashForecast: (data: any) => api.post('/predict/cash-forecast', data),
|
||||
|
||||
@@ -217,6 +217,112 @@
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab: 相关成本决策(CMA P2商业决策分析) -->
|
||||
<el-tab-pane label="相关成本决策" name="relevantDecision">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="24">
|
||||
<el-radio-group v-model="relDecisionType" style="margin-bottom:16px;">
|
||||
<el-radio-button value="make_or_buy">自制 vs 外购</el-radio-button>
|
||||
<el-radio-button value="special_order">特殊订单</el-radio-button>
|
||||
<el-radio-button value="product_mix">产品组合(约束理论)</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 自制vs外购 -->
|
||||
<el-row v-if="relDecisionType === 'make_or_buy'" :gutter="16">
|
||||
<el-col :span="10">
|
||||
<el-card>
|
||||
<template #header>输入参数</template>
|
||||
<el-form label-width="140px" size="small">
|
||||
<el-form-item label="需求量(件)"><el-input-number v-model="makeBuy.demand" :min="0" style="width:200px;" /></el-form-item>
|
||||
<el-form-item label="自制单位变动成本"><el-input-number v-model="makeBuy.make_variable_cost" :min="0" style="width:200px;" /></el-form-item>
|
||||
<el-form-item label="自制新增固定成本"><el-input-number v-model="makeBuy.make_fixed_cost" :min="0" style="width:200px;" /></el-form-item>
|
||||
<el-form-item label="外购单价"><el-input-number v-model="makeBuy.buy_price" :min="0" style="width:200px;" /></el-form-item>
|
||||
<el-button type="primary" size="small" @click="runRelevantDecision">计算决策</el-button>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<el-card v-if="relResult">
|
||||
<template #header>决策结果</template>
|
||||
<el-alert :title="'建议:' + relResult.recommendation" :type="relResult.recommendation === '自制' ? 'primary' : 'warning'" :description="relResult.reason" show-icon :closable="false" style="margin-bottom:12px;" />
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="自制总成本">{{ formatRelNum(relResult.make_total_cost) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="外购总成本">{{ formatRelNum(relResult.buy_total_cost) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="单位自制成本">{{ formatRelNum(relResult.unit_make_cost) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="外购单价">{{ formatRelNum(relResult.unit_buy_price) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="无差别点" :span="2">{{ relResult.indifferent_point != null ? formatRelNum(relResult.indifferent_point) + '件' : '—' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-text size="small" type="info" style="margin-top:8px;display:block;">{{ relResult.notes }}</el-text>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 特殊订单 -->
|
||||
<el-row v-if="relDecisionType === 'special_order'" :gutter="16">
|
||||
<el-col :span="10">
|
||||
<el-card>
|
||||
<template #header>输入参数</template>
|
||||
<el-form label-width="140px" size="small">
|
||||
<el-form-item label="正常售价"><el-input-number v-model="specialOrder.normal_price" :min="0" style="width:200px;" /></el-form-item>
|
||||
<el-form-item label="特殊订单价"><el-input-number v-model="specialOrder.special_price" :min="0" style="width:200px;" /></el-form-item>
|
||||
<el-form-item label="单位变动成本"><el-input-number v-model="specialOrder.variable_cost" :min="0" style="width:200px;" /></el-form-item>
|
||||
<el-form-item label="订单数量"><el-input-number v-model="specialOrder.order_qty" :min="0" style="width:200px;" /></el-form-item>
|
||||
<el-form-item label="产能占用%"><el-input-number v-model="specialOrder.capacity_used" :min="0" :max="100" style="width:200px;" /></el-form-item>
|
||||
<el-form-item label="额外固定成本"><el-input-number v-model="specialOrder.extra_fixed_cost" :min="0" style="width:200px;" /></el-form-item>
|
||||
<el-button type="primary" size="small" @click="runRelevantDecision">计算决策</el-button>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<el-card v-if="relResult">
|
||||
<template #header>决策结果</template>
|
||||
<el-alert :title="'建议:' + relResult.recommendation" :type="relResult.recommendation === '接受' ? 'success' : 'danger'" :description="relResult.reason" show-icon :closable="false" style="margin-bottom:12px;" />
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="单位贡献">{{ formatRelNum(relResult.unit_contribution) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="总贡献">{{ formatRelNum(relResult.total_contribution) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="产能占用">{{ relResult.capacity_used_pct }}%</el-descriptions-item>
|
||||
<el-descriptions-item label="额外固定成本">{{ formatRelNum(relResult.extra_fixed_cost) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-text size="small" type="info" style="margin-top:8px;display:block;">{{ relResult.notes }}</el-text>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 产品组合 -->
|
||||
<el-row v-if="relDecisionType === 'product_mix'" :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>产品参数(价格/变动成本/约束资源消耗/需求)</template>
|
||||
<div v-for="(p, idx) in productMix.products" :key="idx" style="display:flex;gap:8px;margin-bottom:8px;align-items:center;">
|
||||
<el-input v-model="p.name" placeholder="产品名" style="width:100px;" size="small" />
|
||||
<el-input-number v-model="p.price" :min="0" placeholder="价格" style="width:110px;" size="small" />
|
||||
<el-input-number v-model="p.var_cost" :min="0" placeholder="变动成本" style="width:110px;" size="small" />
|
||||
<el-input-number v-model="p.constraint_usage" :min="0.1" placeholder="约束消耗" style="width:100px;" size="small" />
|
||||
<el-input-number v-model="p.demand" :min="0" placeholder="需求" style="width:90px;" size="small" />
|
||||
</div>
|
||||
<el-button size="small" @click="productMix.products.push({ name: '新产品', price: 100, var_cost: 60, constraint_usage: 1, demand: 100 })">+ 添加产品</el-button>
|
||||
<el-button size="small" type="primary" @click="runRelevantDecision" style="margin-left:8px;">计算决策</el-button>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card v-if="relResult">
|
||||
<template #header>决策结果</template>
|
||||
<el-alert :title="relResult.recommendation" type="success" :closable="false" style="margin-bottom:12px;" />
|
||||
<el-table :data="relResult.ranking" border stripe size="small">
|
||||
<el-table-column prop="name" label="产品" />
|
||||
<el-table-column prop="unit_contribution" label="单位贡献" />
|
||||
<el-table-column prop="constraint_usage" label="约束消耗" />
|
||||
<el-table-column prop="contribution_per_constraint" label="单位约束贡献" />
|
||||
<el-table-column prop="demand" label="需求" />
|
||||
</el-table>
|
||||
<el-text size="small" type="info" style="margin-top:8px;display:block;">{{ relResult.notes }}</el-text>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab 5: 现金流预测(AI事前预警) -->
|
||||
<el-tab-pane label="现金流预测" name="cashFlow">
|
||||
<el-row :gutter="16">
|
||||
@@ -455,6 +561,35 @@ const scenarioForm = ref({
|
||||
})
|
||||
const scenarioResult = ref<any>({})
|
||||
|
||||
// ── 相关成本决策(CMA P2商业决策分析) ──
|
||||
const relDecisionType = ref('make_or_buy')
|
||||
const relResult = ref<any>(null)
|
||||
const makeBuy = ref({ demand: 10000, make_variable_cost: 80, make_fixed_cost: 50000, buy_price: 90 })
|
||||
const specialOrder = ref({ normal_price: 150, special_price: 120, variable_cost: 95, order_qty: 1000, capacity_used: 60, extra_fixed_cost: 0 })
|
||||
const productMix = ref({ products: [
|
||||
{ name: 'A产品', price: 100, var_cost: 60, constraint_usage: 2, demand: 1000 },
|
||||
{ name: 'B产品', price: 150, var_cost: 100, constraint_usage: 1, demand: 800 },
|
||||
{ name: 'C产品', price: 200, var_cost: 150, constraint_usage: 3, demand: 500 },
|
||||
]})
|
||||
|
||||
function formatRelNum(v: any) {
|
||||
if (v === null || v === undefined) return '--'
|
||||
return Number(v).toLocaleString('zh-CN', { maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
async function runRelevantDecision() {
|
||||
try {
|
||||
let payload: any = { type: relDecisionType.value }
|
||||
if (relDecisionType.value === 'make_or_buy') payload = { ...payload, ...makeBuy.value }
|
||||
else if (relDecisionType.value === 'special_order') payload = { ...payload, ...specialOrder.value }
|
||||
else payload = { ...payload, products: productMix.value.products }
|
||||
relResult.value = await predictApi.relevantDecision(payload) as any
|
||||
} catch (e: any) {
|
||||
relResult.value = null
|
||||
ElMessage.error(e?.response?.data?.detail || e?.message || '决策计算失败')
|
||||
}
|
||||
}
|
||||
|
||||
function fillScenarioExample() {
|
||||
scenarioForm.value = {
|
||||
optimistic: { revenue: 130, cost: 90 },
|
||||
|
||||
Reference in New Issue
Block a user