diff --git a/inventory-web/src/views/operation/scrap/create.vue b/inventory-web/src/views/operation/scrap/create.vue index a0fc30f..de44813 100644 --- a/inventory-web/src/views/operation/scrap/create.vue +++ b/inventory-web/src/views/operation/scrap/create.vue @@ -305,32 +305,80 @@ const scanScannedQty = computed(() => const scanScannedTypes = computed(() => cartItems.value.length) const scanTotalTypes = computed(() => plannedItems.value.length) -// --- 按 (source_table, stock_id) 匹配计划项 --- -const matchPlan = (sourceTable: string, stockId: any) => - plannedItems.value.find( - (p: any) => String(p.source_table) === String(sourceTable) && String(p.stock_id) === String(stockId) - ) +// ============================================================ +// ★ SKU 主键匹配(与后端 _match_key 完全同口径) +// +// SKU 在同一库存表内唯一、且不存在跨表重名,故以 SKU 作为扫码匹配的唯一标识。 +// 个别历史库存行 SKU 为空,这类行回退为 source_table + stock_id 复合键; +// 前缀区分(sku: / row:)确保空 SKU 行绝不会与正常 SKU 串键。 +// ============================================================ +const normSku = (sku: any) => String(sku ?? '').trim() -// 某计划项已扫数量 -const scannedQtyOf = (plan: any) => +const matchKey = (sourceTable: any, stockId: any, sku: any) => { + const s = normSku(sku) + return s ? `sku:${s}` : `row:${String(sourceTable)}#${String(stockId)}` +} + +// 按匹配键找批准明细行 +const findPlanByKey = (key: string) => + plannedItems.value.find((p: any) => matchKey(p.source_table, p.stock_id, p.sku) === key) + +// 某匹配键的批准总量(同一 SKU 在批准单中出现多行时累加) +const approvedQtyByKey = (key: string) => + plannedItems.value + .filter((p: any) => matchKey(p.source_table, p.stock_id, p.sku) === key) + .reduce((sum: number, p: any) => sum + (Number(p.scrap_qty) || 0), 0) + +// 某匹配键在购物车中的已扫总量 +const scannedQtyByKey = (key: string) => cartItems.value - .filter(ci => String(ci.source_table) === String(plan.source_table) - && String(ci.stock_id) === String(plan.stock_id)) - .reduce((sum, ci) => sum + (Number(ci.scrap_quantity) || 0), 0) + .filter((ci: any) => matchKey(ci.source_table, ci.id, ci.sku) === key) + .reduce((sum: number, ci: any) => sum + (Number(ci.scrap_quantity) || 0), 0) -// 购物车某行还能再报废多少(批准量 - 其余行已占用量) +// 展示用:计划行的已扫数量 +const scannedQtyOf = (plan: any) => + scannedQtyByKey(matchKey(plan.source_table, plan.stock_id, plan.sku)) + +// 购物车某行还能再报废多少(该 SKU 批准量 - 其余行已占用量,且不超当前可用库存) const maxScrapFor = (row: any) => { - const plan = matchPlan(row.source_table, row.stock_id) - const approved = Number(plan?.scrap_qty) || 0 + const key = matchKey(row.source_table, row.id, row.sku) + const approved = approvedQtyByKey(key) const others = cartItems.value - .filter(ci => ci !== row - && String(ci.source_table) === String(row.source_table) - && String(ci.stock_id) === String(row.stock_id)) - .reduce((sum, ci) => sum + (Number(ci.scrap_quantity) || 0), 0) + .filter((ci: any) => ci !== row && matchKey(ci.source_table, ci.id, ci.sku) === key) + .reduce((sum: number, ci: any) => sum + (Number(ci.scrap_quantity) || 0), 0) const avail = parseFloat(row.available_quantity) || 0 return Math.max(0, Math.min(approved - others, avail)) } +/** + * ★ 扫码校验(SKU 主键) + * + * 返回 null 表示放行;返回字符串表示拦截原因(调用方以红色 toast 提示并阻断入车)。 + * · 扫码 SKU 不在批准明细中 → 拦截 + * · 该 SKU 累计扫码量 + 本次数量 > 批准量 → 拦截 + */ +const validateAgainstPlan = (item: any, addQty: number): string | null => { + const key = matchKey(item.source_table, item.id, item.sku) + const plan = findPlanByKey(key) + + if (!plan) { + const s = normSku(item.sku) + return s + ? `SKU【${s}】不在该报废申请单的批准明细中,禁止报废` + : `该物料【${item.name || ''}】不在申请单的批准明细中,禁止报废` + } + + const approved = approvedQtyByKey(key) + const already = scannedQtyByKey(key) + + if (already + addQty > approved) { + const label = normSku(plan.sku) || plan.name || '' + return `SKU【${label}】超出批准数量(批准: ${approved},已扫: ${already},本次: ${addQty})` + } + + return null +} + // 未扫清单 const unscannedList = computed(() => plannedItems.value.map((plan: any) => { @@ -385,27 +433,6 @@ const handleRequestChange = (val: number | null) => { barcodeInput.value = '' } -/** - * 校验扫码物品是否属于本申请单的批准明细,且累计不超批准量。 - * 返回 null 表示通过,否则返回错误文案。 - */ -const validateAgainstPlan = (item: any, addQty: number): string | null => { - const plan = matchPlan(item.source_table, item.id) - - if (!plan) { - return `该物料【${item.name || item.sku}】不在申请单的批准明细中,禁止报废` - } - - const approved = Number(plan.scrap_qty) || 0 - const alreadyScanned = scannedQtyOf(plan) - - if (alreadyScanned + addQty > approved) { - return `【${plan.name || plan.sku}】超出批准数量(批准: ${approved},已扫: ${alreadyScanned},本次: ${addQty})` - } - - return null -} - // --- 扫码逻辑 --- const onScanSuccess = (code: string) => { if (!code) return @@ -444,10 +471,9 @@ const handleManualInput = async () => { try { loading.value = true - // 1. 购物车已有该库存行 → 累加(按 source_table + id 去重,与申请单明细同口径) + // 1. 购物车已有该 SKU → 累加(与批准明细同口径,按 SKU 去重) const existIndex = cartItems.value.findIndex( - item => String(item.source_table) === String(item.source_table) && - (item.barcode === code || item.sku === code || String(item.id) === String(code)) + item => normSku(item.sku) === code || item.barcode === code || String(item.id) === code ) if (existIndex > -1) { const item = cartItems.value[existIndex]