fix(web): 扫码页与备选库位带单据ID,按有效可用量限流
配合后端预占回加:扫码/借库/审批页面调用 /outbound/scan 与 /outbound/alternatives 时带上本单 id 与 biz_type,拿到的 available_quantity 即为「实时可用量 + 本单 预占」,本单锁定的行不会再因实时可用量为 0 而被拦住或从列表消失。 后端已在响应边界完成归一化,故前端模板与校验逻辑(:max、库存列展示、超量 警告、草稿存取)一律不动,只多传参数 —— 避免把同一语义散落到多个读写点。 - api/outbound.ts:getStockByBarcode / getStockAlternatives 加 requestId 与 bizType 参数,导出 ScanBizType,ScanResult 补 reserved_quantity 等字段 - api/transaction.ts:同名的历史副本同步签名(当前无调用方,加注释指向唯一来源) - views/outbound/create.vue、views/transaction/borrow.vue:各 3 处调用点带上 单据 id;refreshStockFromDraft 改为显式透传 id,不依赖 computed 已解析 - views/borrow/approval/index.vue:审批页也带上本单 id,否则待审批单 (预占已生效)锁定的行会因实时可用量为 0 而从备选列表消失 借库侧 request_id 是 BorrowApproval.id,靠 biz_type='borrow' 与出库单区分 —— 两张表 ID 空间独立,不可省。
This commit is contained in:
@ -31,6 +31,9 @@ export interface OutboundSubmitData {
|
|||||||
remark?: string
|
remark?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 单据类型:出库单与借库单是两张独立表、ID 空间独立,必须显式区分 */
|
||||||
|
export type ScanBizType = 'outbound' | 'borrow'
|
||||||
|
|
||||||
export interface ScanResult {
|
export interface ScanResult {
|
||||||
id: number
|
id: number
|
||||||
sku: string
|
sku: string
|
||||||
@ -38,7 +41,14 @@ export interface ScanResult {
|
|||||||
spec_model: string
|
spec_model: string
|
||||||
source_table: string // 'stock_buy' | 'stock_product' ...
|
source_table: string // 'stock_buy' | 'stock_product' ...
|
||||||
stock_quantity: number
|
stock_quantity: number
|
||||||
|
/** ★ 有效可用量 = 实时可用量 + 本单在该行的预占量(本单可扫的上限) */
|
||||||
available_quantity: number
|
available_quantity: number
|
||||||
|
/** 实时可用量原值(不含本单预占),备查/展示用 */
|
||||||
|
raw_available_quantity?: number
|
||||||
|
/** 本单在该库存行上的预占量 */
|
||||||
|
reserved_quantity?: number
|
||||||
|
/** 预占是否已生效;false 表示后端降级为实时可用量 */
|
||||||
|
reservation_applied?: boolean
|
||||||
batch_number?: string
|
batch_number?: string
|
||||||
warehouse_location?: string
|
warehouse_location?: string
|
||||||
barcode?: string
|
barcode?: string
|
||||||
@ -47,13 +57,24 @@ export interface ScanResult {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据条码获取库存物品详情
|
* 根据条码获取库存物品详情
|
||||||
|
*
|
||||||
|
* ★ 传 requestId + bizType 后,返回的 available_quantity 会回加本单在该行的
|
||||||
|
* 预占量。出库选单提交申请时已扣减 available_quantity,若扫码时不回加,
|
||||||
|
* 被本单占满的行会显示 0,工人根本扫不进去。
|
||||||
|
*
|
||||||
* @param barcode 扫描到的条码
|
* @param barcode 扫描到的条码
|
||||||
|
* @param requestId 本单 ID(出库审批单 / 借库审批单)
|
||||||
|
* @param bizType 单据类型,决定 requestId 查哪张表(默认 outbound)
|
||||||
*/
|
*/
|
||||||
export function getStockByBarcode(barcode: string) {
|
export function getStockByBarcode(
|
||||||
|
barcode: string,
|
||||||
|
requestId?: number | null,
|
||||||
|
bizType: ScanBizType = 'outbound'
|
||||||
|
) {
|
||||||
return request<any, ScanResult>({
|
return request<any, ScanResult>({
|
||||||
url: '/v1/outbound/scan',
|
url: '/v1/outbound/scan',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: { barcode }
|
params: { barcode, request_id: requestId, biz_type: bizType }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,15 +196,22 @@ export function bomMatchStock(payload: BomRequirement[] | { requirements: BomReq
|
|||||||
* 备选库位查询 —— 为「物理覆盖」提供可见性
|
* 备选库位查询 —— 为「物理覆盖」提供可见性
|
||||||
*
|
*
|
||||||
* 申请单把货预占在某个库位后,工人现场可能进不去该库位,需要改扫同物料的
|
* 申请单把货预占在某个库位后,工人现场可能进不去该库位,需要改扫同物料的
|
||||||
* 其它批次。本接口按 available_quantity > 0 返回**真正能拿**的库位,
|
* 其它批次。本接口返回**真正能拿**的库位(实时可用量 > 0,或本单已预占该行),
|
||||||
* 并标注哪一条是本单锁定的推荐行。
|
* 并标注哪一条是本单锁定的推荐行。
|
||||||
*
|
*
|
||||||
|
* ★ 传 requestId + bizType 后,available_quantity 会回加本单在该行的预占量;
|
||||||
|
* 否则被本单占满的行实时可用量为 0,会从列表里凭空消失。
|
||||||
|
*
|
||||||
* @param baseId 物料 ID(申请明细里的 base_id)
|
* @param baseId 物料 ID(申请明细里的 base_id)
|
||||||
* @param locked 本单锁定的批次(用于标注「推荐」),可选
|
* @param locked 本单锁定的批次(用于标注「推荐」),可选
|
||||||
|
* @param requestId 本单 ID(出库审批单 / 借库审批单),可选
|
||||||
|
* @param bizType 单据类型,决定 requestId 查哪张表(默认 outbound)
|
||||||
*/
|
*/
|
||||||
export function getStockAlternatives(
|
export function getStockAlternatives(
|
||||||
baseId: number,
|
baseId: number,
|
||||||
locked?: { stock_id?: number; source_table?: string }
|
locked?: { stock_id?: number; source_table?: string },
|
||||||
|
requestId?: number | null,
|
||||||
|
bizType: ScanBizType = 'outbound'
|
||||||
) {
|
) {
|
||||||
return request({
|
return request({
|
||||||
url: '/v1/outbound/alternatives',
|
url: '/v1/outbound/alternatives',
|
||||||
@ -191,7 +219,9 @@ export function getStockAlternatives(
|
|||||||
params: {
|
params: {
|
||||||
base_id: baseId,
|
base_id: baseId,
|
||||||
stock_id: locked?.stock_id,
|
stock_id: locked?.stock_id,
|
||||||
source_table: locked?.source_table
|
source_table: locked?.source_table,
|
||||||
|
request_id: requestId,
|
||||||
|
biz_type: bizType
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,13 +47,24 @@ export interface ScanResult {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据条码获取库存物品详情
|
* 根据条码获取库存物品详情
|
||||||
|
*
|
||||||
|
* ⚠ 本文件是与 `@/api/outbound` 同名的**历史副本**(同 URL、同返回形状),
|
||||||
|
* 当前无任何调用方 —— 借库扫码页 `views/transaction/borrow.vue` 是从
|
||||||
|
* `@/api/outbound` 导入的。改动请落到那边,这里同步仅为避免下次改漏。
|
||||||
|
*
|
||||||
* @param barcode 扫描到的条码
|
* @param barcode 扫描到的条码
|
||||||
|
* @param requestId 本单 ID(出库审批单 / 借库审批单)
|
||||||
|
* @param bizType 单据类型,决定 requestId 查哪张表(默认 outbound)
|
||||||
*/
|
*/
|
||||||
export function getStockByBarcode(barcode: string) {
|
export function getStockByBarcode(
|
||||||
|
barcode: string,
|
||||||
|
requestId?: number | null,
|
||||||
|
bizType: 'outbound' | 'borrow' = 'outbound'
|
||||||
|
) {
|
||||||
return request<any, ScanResult>({
|
return request<any, ScanResult>({
|
||||||
url: '/v1/outbound/scan',
|
url: '/v1/outbound/scan',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: { barcode }
|
params: { barcode, request_id: requestId, biz_type: bizType }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -62,7 +62,7 @@
|
|||||||
placement="right"
|
placement="right"
|
||||||
:width="340"
|
:width="340"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
@show="loadAlternatives(item)"
|
@show="loadAlternatives(item, row.id)"
|
||||||
>
|
>
|
||||||
<!-- ★ 整个库位格子都是点击热区,工人不必精准点图标 -->
|
<!-- ★ 整个库位格子都是点击热区,工人不必精准点图标 -->
|
||||||
<template #reference>
|
<template #reference>
|
||||||
@ -310,16 +310,22 @@ const altLoading = ref(false)
|
|||||||
const altItems = ref<any[]>([])
|
const altItems = ref<any[]>([])
|
||||||
const altTotal = ref(0)
|
const altTotal = ref(0)
|
||||||
|
|
||||||
const loadAlternatives = async (item: any) => {
|
const loadAlternatives = async (item: any, approvalId?: number) => {
|
||||||
if (!item?.base_id) return
|
if (!item?.base_id) return
|
||||||
altLoading.value = true
|
altLoading.value = true
|
||||||
altItems.value = []
|
altItems.value = []
|
||||||
altTotal.value = 0
|
altTotal.value = 0
|
||||||
try {
|
try {
|
||||||
const res: any = await getStockAlternatives(item.base_id, {
|
// ★ 带上本单 id 且 biz_type='borrow':
|
||||||
stock_id: item.stock_id,
|
// 审批阶段看到的多是 status=0(待审批)的单据,而预占在**提交申请时**
|
||||||
source_table: item.source_table,
|
// 就已冻结可用数。不回加的话,被本单占满的行实时可用量为 0,
|
||||||
})
|
// 恰好会从列表里消失 —— 审批人反而看不到本单锁定的批次。
|
||||||
|
const res: any = await getStockAlternatives(
|
||||||
|
item.base_id,
|
||||||
|
{ stock_id: item.stock_id, source_table: item.source_table },
|
||||||
|
approvalId,
|
||||||
|
'borrow'
|
||||||
|
)
|
||||||
altItems.value = res?.data?.items || []
|
altItems.value = res?.data?.items || []
|
||||||
altTotal.value = res?.data?.total_available || 0
|
altTotal.value = res?.data?.total_available || 0
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@ -469,10 +469,13 @@ const loadAlternatives = async (row: any) => {
|
|||||||
altItems.value = []
|
altItems.value = []
|
||||||
altTotal.value = 0
|
altTotal.value = 0
|
||||||
try {
|
try {
|
||||||
const res: any = await getStockAlternatives(row.base_id, {
|
// ★ 带上本单 id:本单锁定的批次实时可用量为 0,不回加的话会从列表里消失
|
||||||
stock_id: row.stock_id,
|
const res: any = await getStockAlternatives(
|
||||||
source_table: row.source_table,
|
row.base_id,
|
||||||
})
|
{ stock_id: row.stock_id, source_table: row.source_table },
|
||||||
|
selectedRequest.value?.id,
|
||||||
|
'outbound'
|
||||||
|
)
|
||||||
altItems.value = res?.data?.items || []
|
altItems.value = res?.data?.items || []
|
||||||
altTotal.value = res?.data?.total_available || 0
|
altTotal.value = res?.data?.total_available || 0
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -674,7 +677,7 @@ const restoreDraft = async (requestId: number) => {
|
|||||||
// ★ 刷新库存:草稿里的 available_quantity 是**扫描那一刻**的快照,
|
// ★ 刷新库存:草稿里的 available_quantity 是**扫描那一刻**的快照,
|
||||||
// 若期间别人出库了,界面会显示陈旧数字,工人扫满后到提交时才被
|
// 若期间别人出库了,界面会显示陈旧数字,工人扫满后到提交时才被
|
||||||
// 后端拒绝(白扫一场)。这里重新拉一次实时可用量。
|
// 后端拒绝(白扫一场)。这里重新拉一次实时可用量。
|
||||||
await refreshStockFromDraft()
|
await refreshStockFromDraft(requestId)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('恢复草稿失败', e)
|
console.warn('恢复草稿失败', e)
|
||||||
}
|
}
|
||||||
@ -684,20 +687,28 @@ const restoreDraft = async (requestId: number) => {
|
|||||||
* 用实时库存刷新购物车行的「库存」列。
|
* 用实时库存刷新购物车行的「库存」列。
|
||||||
*
|
*
|
||||||
* 草稿里的 available_quantity 是扫描那一刻的快照,恢复时可能已过期
|
* 草稿里的 available_quantity 是扫描那一刻的快照,恢复时可能已过期
|
||||||
* (期间别人出库/借出/报废都会消耗可用量)。这里按 base_id 查一次实时
|
* (期间别人出库/借出/报废都会消耗可用量)。这里按 base_id 查一次实时值,
|
||||||
* 可用量,避免工人基于陈旧数字扫满、到提交时才发现不够。
|
* 避免工人基于陈旧数字扫满、到提交时才发现不够。
|
||||||
*
|
*
|
||||||
* 复用 /alternatives 端点 —— 它本来就按 base_id 返回各库存行的实时可用量。
|
* 复用 /alternatives 端点 —— 它按 base_id 返回各库存行的实时可用量。
|
||||||
|
* ★ 传入本单 id 后,返回的 available_quantity 是**有效可用量**
|
||||||
|
* (实时值 + 本单预占),故本单锁定行不会因实时值为 0 而被过滤掉,
|
||||||
|
* 下面的「已扫数量 > 最新可用量」判定也不会误报。
|
||||||
* 失败不阻断:拿不到实时值就沿用草稿快照,至少不影响继续作业。
|
* 失败不阻断:拿不到实时值就沿用草稿快照,至少不影响继续作业。
|
||||||
*/
|
*/
|
||||||
const refreshStockFromDraft = async () => {
|
const refreshStockFromDraft = async (requestId?: number | null) => {
|
||||||
const plan = selectedRequest.value?.items || []
|
const plan = selectedRequest.value?.items || []
|
||||||
const baseIds = [...new Set(plan.map((p: any) => p.base_id).filter(Boolean))]
|
const baseIds = [...new Set(plan.map((p: any) => p.base_id).filter(Boolean))]
|
||||||
if (!baseIds.length) return
|
if (!baseIds.length) return
|
||||||
|
|
||||||
|
// ★ 显式透传单据 id(而非依赖 selectedRequest 已解析),保证预占回加一定生效
|
||||||
|
const rid = requestId ?? selectedRequest.value?.id
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
(baseIds as number[]).map(bid => getStockAlternatives(bid).catch(() => null))
|
(baseIds as number[]).map(bid =>
|
||||||
|
getStockAlternatives(bid, undefined, rid, 'outbound').catch(() => null)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
const latestByKey = new Map<string, number>()
|
const latestByKey = new Map<string, number>()
|
||||||
for (const r of results) {
|
for (const r of results) {
|
||||||
@ -874,7 +885,9 @@ const handleManualInput = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. 调用 API 查询
|
// 2. 调用 API 查询
|
||||||
const res = await getStockByBarcode(code)
|
// ★ 带上本单 id:申请阶段已把本单要的货从 available_quantity 扣掉(预占),
|
||||||
|
// 不回加的话,被本单占满的行会显示 0,工人根本扫不进去。
|
||||||
|
const res = await getStockByBarcode(code, selectedRequest.value?.id, 'outbound')
|
||||||
if (res.data) {
|
if (res.data) {
|
||||||
const item = res.data
|
const item = res.data
|
||||||
const availQty = parseFloat(item.available_quantity || 0)
|
const availQty = parseFloat(item.available_quantity || 0)
|
||||||
|
|||||||
@ -424,10 +424,13 @@ const loadAlternatives = async (row: any) => {
|
|||||||
altItems.value = []
|
altItems.value = []
|
||||||
altTotal.value = 0
|
altTotal.value = 0
|
||||||
try {
|
try {
|
||||||
const res: any = await getStockAlternatives(row.base_id, {
|
// ★ 带上本单 id:本单锁定的批次实时可用量为 0,不回加的话会从列表里消失
|
||||||
stock_id: row.stock_id,
|
const res: any = await getStockAlternatives(
|
||||||
source_table: row.source_table,
|
row.base_id,
|
||||||
})
|
{ stock_id: row.stock_id, source_table: row.source_table },
|
||||||
|
selectedApprovalId.value,
|
||||||
|
'borrow'
|
||||||
|
)
|
||||||
altItems.value = res?.data?.items || []
|
altItems.value = res?.data?.items || []
|
||||||
altTotal.value = res?.data?.total_available || 0
|
altTotal.value = res?.data?.total_available || 0
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -598,7 +601,7 @@ const restoreDraft = async (requestId: number) => {
|
|||||||
|
|
||||||
// ★ 刷新库存:草稿里的 available_quantity 是扫描那一刻的快照,
|
// ★ 刷新库存:草稿里的 available_quantity 是扫描那一刻的快照,
|
||||||
// 期间别人出库/借出会消耗可用量,需重新拉取实时值。
|
// 期间别人出库/借出会消耗可用量,需重新拉取实时值。
|
||||||
await refreshStockFromDraft()
|
await refreshStockFromDraft(requestId)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('恢复草稿失败', e)
|
console.warn('恢复草稿失败', e)
|
||||||
}
|
}
|
||||||
@ -607,15 +610,24 @@ const restoreDraft = async (requestId: number) => {
|
|||||||
/**
|
/**
|
||||||
* 用实时库存刷新购物车行的「库存」列(与扫码出库页同款)。
|
* 用实时库存刷新购物车行的「库存」列(与扫码出库页同款)。
|
||||||
* 复用 /alternatives 端点;失败不阻断,沿用草稿快照。
|
* 复用 /alternatives 端点;失败不阻断,沿用草稿快照。
|
||||||
|
*
|
||||||
|
* ★ 传入本单 id 后,返回的 available_quantity 是**有效可用量**
|
||||||
|
* (实时值 + 本单预占)。借库申请提交时已冻结可用数,不回加的话
|
||||||
|
* 被本单占满的行会显示 0,下面「已借数量 > 最新可用量」也会误报。
|
||||||
*/
|
*/
|
||||||
const refreshStockFromDraft = async () => {
|
const refreshStockFromDraft = async (requestId?: number | null) => {
|
||||||
const plan = selectedApproval.value?.items || []
|
const plan = selectedApproval.value?.items || []
|
||||||
const baseIds = [...new Set(plan.map((p: any) => p.base_id).filter(Boolean))]
|
const baseIds = [...new Set(plan.map((p: any) => p.base_id).filter(Boolean))]
|
||||||
if (!baseIds.length) return
|
if (!baseIds.length) return
|
||||||
|
|
||||||
|
// ★ 显式透传单据 id(而非依赖 selectedApproval 已解析)
|
||||||
|
const rid = requestId ?? selectedApproval.value?.id ?? selectedApprovalId.value
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
(baseIds as number[]).map(bid => getStockAlternatives(bid).catch(() => null))
|
(baseIds as number[]).map(bid =>
|
||||||
|
getStockAlternatives(bid, undefined, rid, 'borrow').catch(() => null)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
const latestByKey = new Map<string, number>()
|
const latestByKey = new Map<string, number>()
|
||||||
for (const r of results) {
|
for (const r of results) {
|
||||||
@ -793,7 +805,10 @@ const handleManualInput = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 查库
|
// 查库
|
||||||
const res = await getStockByBarcode(code)
|
// ★ 带上本单 id:借库申请提交时已冻结可用数(available_quantity 被扣减),
|
||||||
|
// 不回加的话,被本单占满的行会显示 0,工人根本扫不进去。
|
||||||
|
// biz_type='borrow' 不可省 —— 出库单与借库单是两张独立表、ID 空间独立。
|
||||||
|
const res = await getStockByBarcode(code, selectedApprovalId.value, 'borrow')
|
||||||
if (res.data) {
|
if (res.data) {
|
||||||
const item = res.data
|
const item = res.data
|
||||||
const availQty = parseFloat(item.available_quantity || 0)
|
const availQty = parseFloat(item.available_quantity || 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user