refactor(bom): 前端改为消费后端分配结果,删除本地分配与跨页数据污染
一、删除前端分配算法
Selection.vue 与 borrow/apply/index.vue 原先各自维护一套
rowsByBaseId 归并 + 跨行分配循环(近 80 行),现全部移除。
改为:构造 requirements → 调用 bomMatchStock → 直接 push 返回的 items。
二、缺料提示
后端返回 shortages 时,用 ElMessageBox.alert 逐项列出
「物料名:需 X,实配 Y,缺 Z」,替代原先笼统的「跳过 N 种缺货物料」。
全部满足则不弹窗,仅提示添加成功。
三、API 封装支持双签名(api/outbound.ts)
bomMatchStock(requirements | { requirements })
同时兼容旧的 bomMatchStock(childIds),未改造的调用方不受影响。
四、清除 loadStockForBom(两个页面)
该方法会把 BOM 匹配结果整体写入 stockList.value,而 stockList 同时是
「手动选单弹窗」的数据源 —— 操作过 BOM 后再打开手动选单,看到的会是
BOM 匹配结果而非库存列表。BOM 流程不再触碰 stockList 后,该交叉污染
一并消除,方法随之删除。
注:借库选单页存在逐字相同的 .find() + Math.min 缺陷,本次一并修复,
使两个 BOM 入口行为一致。
SFC 编译通过;vue-tsc 无新增错误(残留 6 处告警为既有问题)。
This commit is contained in:
@ -144,14 +144,30 @@ export function closeRequest(id: number) {
|
||||
})
|
||||
}
|
||||
|
||||
/** BOM 需求项:后端据此在 DB 层完成跨批次分配 */
|
||||
export interface BomRequirement {
|
||||
base_id: number
|
||||
required_qty: number
|
||||
name?: string
|
||||
spec_model?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* BOM 匹配库存(替代前端 while(true) 全量加载)
|
||||
* 根据 BOM 子件的 base_id 列表,服务端精确查询有库存的记录
|
||||
* BOM 库存匹配 / 分配
|
||||
*
|
||||
* 两种用法:
|
||||
* 1) 分配模式(推荐):传 { requirements },后端直接返回已分配好的库存行
|
||||
* (含 stock_id / source_table / allocated_qty)与 shortages 缺料明细,
|
||||
* 前端直接入购物车即可,不做任何分配运算。
|
||||
* 2) 查询模式(兼容旧调用):传 base_id 数组,返回全部匹配库存行。
|
||||
*/
|
||||
export function bomMatchStock(childIds: number[]) {
|
||||
export function bomMatchStock(payload: BomRequirement[] | { requirements: BomRequirement[] }) {
|
||||
const data = Array.isArray(payload)
|
||||
? { child_ids: payload } // 旧签名兼容
|
||||
: { requirements: payload.requirements || [] }
|
||||
return request({
|
||||
url: '/v1/outbound/bom-match-stock',
|
||||
method: 'post',
|
||||
data: { child_ids: childIds }
|
||||
data
|
||||
})
|
||||
}
|
||||
@ -708,24 +708,9 @@ const loadStockList = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// BOM 匹配用:服务端精确查询匹配库存(替代前端 while(true) 全量加载)
|
||||
const loadStockForBom = async (childIds: number[]) => {
|
||||
stockLoading.value = true
|
||||
try {
|
||||
const res: any = await bomMatchStock(childIds)
|
||||
const items = (res.data?.items || []).map((item: any) => ({
|
||||
...item,
|
||||
uniqueKey: item.uniqueKey || `${item.type}_${item.id}`,
|
||||
warehouse_location: item.warehouse_location || item.warehouse_loc || item.full_path || ''
|
||||
}))
|
||||
stockList.value = items
|
||||
stockTotal.value = items.length
|
||||
} catch (e) {
|
||||
ElMessage.error('加载 BOM 匹配库存失败')
|
||||
} finally {
|
||||
stockLoading.value = false
|
||||
}
|
||||
}
|
||||
// [已移除] loadStockForBom
|
||||
// BOM 加入清单已改为后端分配(传 requirements、收 items + shortages),
|
||||
// 不再把匹配库存灌进 stockList,避免干扰手动选择弹窗的数据源。
|
||||
|
||||
const openManualSelect = async () => {
|
||||
manualDialogVisible.value = true
|
||||
@ -855,48 +840,71 @@ const confirmBomAdd = async () => {
|
||||
|
||||
const bomRows = currentBomDetail.value
|
||||
|
||||
// ★ 服务端精确查询 BOM 子件匹配库存(替代前端 while(true) 全量加载)
|
||||
const childIds = bomRows.map((b: any) => b.child_id).filter(Boolean)
|
||||
await loadStockForBom(childIds)
|
||||
let addedCount = 0
|
||||
let skippedCount = 0
|
||||
|
||||
bomRows.forEach((bomItem: any) => {
|
||||
const dosage = parseFloat(bomItem.dosage) || 0
|
||||
const needQty = dosage * bomSets.value
|
||||
|
||||
const stockCandidate = stockList.value.find(s =>
|
||||
(s.base_id && s.base_id == bomItem.child_id)
|
||||
)
|
||||
|
||||
if (stockCandidate) {
|
||||
const availableQty = stockCandidate.available_quantity || 0
|
||||
const actualAddQty = Math.min(needQty, availableQty)
|
||||
|
||||
if (actualAddQty > 0) {
|
||||
const existing = selectedItems.value.find(e => e.uniqueKey === stockCandidate.uniqueKey)
|
||||
if (existing) {
|
||||
existing.export_quantity += actualAddQty
|
||||
} else {
|
||||
const newItem = JSON.parse(JSON.stringify(stockCandidate))
|
||||
if (bomItem.warehouse_location) {
|
||||
newItem.warehouse_location = bomItem.warehouse_location
|
||||
}
|
||||
newItem.export_quantity = actualAddQty
|
||||
selectedItems.value.push(newItem)
|
||||
}
|
||||
addedCount++
|
||||
} else {
|
||||
skippedCount++
|
||||
// ==========================================================================
|
||||
// ★ BOM 加入清单:分配完全交给后端
|
||||
//
|
||||
// 原实现用 stockList.find(s => s.base_id == child_id) 只取第一条库存行,
|
||||
// 多批次物料会因此只加到 1 件(与出库选单页此前的缺陷同源)。
|
||||
// 现改为传需求、收结果,分配在 DB 层完成,与出库/借库扣减同源。
|
||||
// ==========================================================================
|
||||
const requirements = bomRows
|
||||
.map((b: any) => {
|
||||
const dosage = parseFloat(b.dosage) || 0
|
||||
return {
|
||||
base_id: b.child_id,
|
||||
required_qty: dosage * bomSets.value,
|
||||
name: b.child_name || b.name || '',
|
||||
spec_model: b.child_spec || b.spec_model || ''
|
||||
}
|
||||
} else {
|
||||
skippedCount++
|
||||
}
|
||||
})
|
||||
})
|
||||
.filter((r: any) => r.base_id && r.required_qty > 0)
|
||||
|
||||
if (requirements.length === 0) {
|
||||
return ElMessage.warning('当前 BOM 无可借用的子件需求')
|
||||
}
|
||||
|
||||
let addedCount = 0
|
||||
let shortages: any[] = []
|
||||
|
||||
try {
|
||||
const res: any = await bomMatchStock({ requirements })
|
||||
const items = res?.data?.items || []
|
||||
shortages = res?.data?.shortages || []
|
||||
|
||||
items.forEach((item: any) => {
|
||||
const qty = Number(item.allocated_qty ?? item.export_quantity ?? 0)
|
||||
if (qty <= 0) return
|
||||
const existing = selectedItems.value.find(e => e.uniqueKey === item.uniqueKey)
|
||||
if (existing) {
|
||||
const cap = Number(item.available_quantity || 0)
|
||||
existing.export_quantity = Math.min(existing.export_quantity + qty, cap)
|
||||
} else {
|
||||
const newItem = JSON.parse(JSON.stringify(item))
|
||||
newItem.export_quantity = qty
|
||||
selectedItems.value.push(newItem)
|
||||
}
|
||||
addedCount++
|
||||
})
|
||||
} catch (e: any) {
|
||||
return ElMessage.error(e?.msg || 'BOM 物料分配失败,请重试')
|
||||
}
|
||||
|
||||
// ★ 缺料提示:逐项列出未能满足的物料及缺口
|
||||
if (shortages.length > 0) {
|
||||
const detail = shortages
|
||||
.map((s: any) => `${s.name || ('base_id ' + s.base_id)}:需 ${s.required_qty},实配 ${s.allocated_qty},缺 ${s.missing}`)
|
||||
.join('\n')
|
||||
ElMessageBox.alert(
|
||||
`以下 ${shortages.length} 项物料库存不足,已按最大可用量分配:\n\n${detail}`,
|
||||
'⚠️ 部分物料库存不足',
|
||||
{ type: 'warning', confirmButtonText: '知道了' }
|
||||
).catch(() => {})
|
||||
}
|
||||
|
||||
if (addedCount > 0) {
|
||||
const tip = skippedCount > 0 ? `(跳过 ${skippedCount} 种缺货物料)` : ''
|
||||
ElMessage.success(`成功添加 ${addedCount} 类物料${tip}`)
|
||||
if (shortages.length === 0) {
|
||||
ElMessage.success(`成功添加 ${addedCount} 行物料`)
|
||||
}
|
||||
bomSelectVisible.value = false
|
||||
} else {
|
||||
ElMessage.warning('该 BOM 所有物料库存均为 0')
|
||||
|
||||
@ -713,26 +713,12 @@ const loadStockList = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// BOM 匹配用:服务端精确查询匹配库存(替代前端 while(true) 全量加载)
|
||||
const loadStockForBom = async (childIds: number[]) => {
|
||||
stockLoading.value = true
|
||||
try {
|
||||
const res: any = await bomMatchStock(childIds)
|
||||
const items = (res.data?.items || []).map((item: any) => ({
|
||||
...item,
|
||||
uniqueKey: item.uniqueKey || `${item.type}_${item.id}`,
|
||||
warehouse_location: item.warehouse_location || item.warehouse_loc || item.full_path || ''
|
||||
}))
|
||||
stockList.value = items
|
||||
stockTotal.value = items.length
|
||||
} catch (e) {
|
||||
ElMessage.error('加载 BOM 匹配库存失败')
|
||||
} finally {
|
||||
stockLoading.value = false
|
||||
}
|
||||
}
|
||||
// [已移除] loadStockForBom
|
||||
// BOM 加入购物车已改为后端分配:直接把 requirements 传给 bomMatchStock,
|
||||
// 取回已分配好的库存行直接入车,无需再把匹配库存灌进 stockList
|
||||
// (避免 stockList 被 BOM 流程覆盖而干扰手动选单弹窗)。
|
||||
|
||||
// 手动选库存弹窗:加载服务端分页数据 + BOM 用全量数据
|
||||
// 手动选库存弹窗:加载服务端分页数据
|
||||
const openManualSelect = async () => {
|
||||
manualDialogVisible.value = true
|
||||
stockPage.value = 1
|
||||
@ -912,55 +898,82 @@ const confirmBomAdd = async () => {
|
||||
|
||||
const bomRows = currentBomDetail.value
|
||||
|
||||
// ★ 服务端精确查询 BOM 子件匹配库存(替代前端 while(true) 全量加载)
|
||||
const childIds = bomRows.map((b: any) => b.child_id).filter(Boolean)
|
||||
await loadStockForBom(childIds)
|
||||
let addedCount = 0
|
||||
let skippedCount = 0
|
||||
|
||||
bomRows.forEach((bomItem: any) => {
|
||||
const dosage = parseFloat(bomItem.dosage) || 0
|
||||
const needQty = dosage * bomSets.value
|
||||
|
||||
const stockCandidate = stockList.value.find(s =>
|
||||
(s.base_id && s.base_id == bomItem.child_id)
|
||||
)
|
||||
|
||||
if (stockCandidate) {
|
||||
const availableQty = stockCandidate.available_quantity || 0
|
||||
const actualAddQty = Math.min(needQty, availableQty)
|
||||
|
||||
if (actualAddQty > 0) {
|
||||
const existing = selectedItems.value.find(e => e.uniqueKey === stockCandidate.uniqueKey)
|
||||
if (existing) {
|
||||
existing.export_quantity += actualAddQty
|
||||
} else {
|
||||
const newItem = JSON.parse(JSON.stringify(stockCandidate))
|
||||
if (bomItem.warehouse_location) {
|
||||
newItem.warehouse_location = bomItem.warehouse_location
|
||||
}
|
||||
newItem.export_quantity = actualAddQty
|
||||
selectedItems.value.push(newItem)
|
||||
}
|
||||
addedCount++
|
||||
} else {
|
||||
skippedCount++
|
||||
// ==========================================================================
|
||||
// ★ BOM 加入购物车:分配完全交给后端
|
||||
//
|
||||
// 演进说明:
|
||||
// 最初前端用 .find() 取第一条库存行 → 遇到多批次物料只加到 1 件;
|
||||
// 改为前端跨行分配后仍有隐患 —— 前端 stockList 由多个入口写入,且
|
||||
// base_id/stock_id 的类型差异会让匹配静默落空,表现同样是"库存不足"。
|
||||
// 现在只负责「传需求」和「收结果」,分配在 DB 层完成,与出库扣减同源。
|
||||
// ==========================================================================
|
||||
const requirements = bomRows
|
||||
.map((b: any) => {
|
||||
const dosage = parseFloat(b.dosage) || 0
|
||||
return {
|
||||
base_id: b.child_id,
|
||||
required_qty: dosage * bomSets.value,
|
||||
name: b.child_name || b.name || '',
|
||||
spec_model: b.child_spec || b.spec_model || ''
|
||||
}
|
||||
} else {
|
||||
skippedCount++
|
||||
}
|
||||
})
|
||||
})
|
||||
.filter((r: any) => r.base_id && r.required_qty > 0)
|
||||
|
||||
if (requirements.length === 0) {
|
||||
return ElMessage.warning('当前 BOM 无可出库的子件需求')
|
||||
}
|
||||
|
||||
let addedCount = 0
|
||||
let shortages: any[] = []
|
||||
|
||||
try {
|
||||
// ★ 一次性把需求交给后端,取回已分配好的库存行
|
||||
const res: any = await bomMatchStock({ requirements })
|
||||
const items = res?.data?.items || []
|
||||
shortages = res?.data?.shortages || []
|
||||
|
||||
// 直接入购物车:每行已带 stock_id / source_table / allocated_qty,无需再算
|
||||
items.forEach((item: any) => {
|
||||
const qty = Number(item.allocated_qty ?? item.export_quantity ?? 0)
|
||||
if (qty <= 0) return
|
||||
|
||||
const existing = selectedItems.value.find(e => e.uniqueKey === item.uniqueKey)
|
||||
if (existing) {
|
||||
// 同款库存行已存在:累加,但仍以该行可用量为上限
|
||||
const cap = Number(item.available_quantity || 0)
|
||||
existing.export_quantity = Math.min(existing.export_quantity + qty, cap)
|
||||
} else {
|
||||
const newItem = JSON.parse(JSON.stringify(item))
|
||||
newItem.export_quantity = qty
|
||||
selectedItems.value.push(newItem)
|
||||
}
|
||||
addedCount++
|
||||
})
|
||||
} catch (e: any) {
|
||||
return ElMessage.error(e?.msg || 'BOM 物料分配失败,请重试')
|
||||
}
|
||||
|
||||
// ★ 缺料提示:逐项列出未能满足的物料及缺口
|
||||
if (shortages.length > 0) {
|
||||
const detail = shortages
|
||||
.map((s: any) => `${s.name || ('base_id ' + s.base_id)}:需 ${s.required_qty},实配 ${s.allocated_qty},缺 ${s.missing}`)
|
||||
.join('\n')
|
||||
ElMessageBox.alert(
|
||||
`以下 ${shortages.length} 项物料库存不足,已按最大可用量分配:\n\n${detail}`,
|
||||
'⚠️ 部分物料库存不足',
|
||||
{ type: 'warning', confirmButtonText: '知道了' }
|
||||
).catch(() => {})
|
||||
}
|
||||
|
||||
if (addedCount > 0) {
|
||||
const tip = skippedCount > 0 ? `(跳过 ${skippedCount} 种缺货物料)` : ''
|
||||
ElMessage.success(`成功添加 ${addedCount} 类物料${tip}`)
|
||||
|
||||
if (shortages.length === 0) {
|
||||
ElMessage.success(`成功添加 ${addedCount} 行物料`)
|
||||
}
|
||||
// ★ 记录本次缺货清单(持久化,防止下次重复出/漏出)
|
||||
saveBomShortage(selectedBomNo.value, bomDetailList.value.filter(i => i.shortage > 0))
|
||||
bomSelectVisible.value = false
|
||||
} else {
|
||||
ElMessage.warning('该 BOM 所有物料库存均为 0')
|
||||
// 全部缺货也要记录
|
||||
saveBomShortage(selectedBomNo.value, bomDetailList.value.filter(i => i.shortage > 0))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user