refactor(scrap): 扫码校验改用 SKU 主键,与后端同口径
前端 validateAgainstPlan 由 (source_table, stock_id) 改为 SKU 主键:
· normSku() 去首尾空白,与后端 _norm_sku 一致;
· matchKey() 与后端 _match_key 完全同构(sku: 前缀 / row: 回退);
· approvedQtyByKey() 聚合同一 SKU 的批准总量,兼容批准单同 SKU 多行;
· 扫码不在批准明细内、或该 SKU 累计量超批准量时,红色 toast + 震动阻断,
不入购物车;
· 购物车去重与 maxScrapFor 上限同步改为按 SKU 计算。
已通过 SFC 编译与 vue-tsc(无新增类型错误)。
This commit is contained in:
@ -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]
|
||||
|
||||
Reference in New Issue
Block a user