feat(outbound): 出库流程强化(缺料防漏 + 完全移除直接出库)
① BOM 清单 SKU 改为规格型号(getBomWithStock 返回 children 无 sku,只有 child_spec) ② 缺货清单自动写入出库申请备注(借鸡生蛋:历史欠料永久留存在申请单) ③ 打印单底部增加欠料待补签收区(补料人签字/日期) ④ 有缺料时阻断式二次确认,防手快漏看 ⑤ 完全移除直接出库模式,所有出库必须按单出库 - create.vue 删除 radio 切换、direct 相关逻辑 - 提交前强制校验已选择审批申请单
This commit is contained in:
@ -223,7 +223,7 @@
|
||||
</template>
|
||||
<el-table :data="bomDetailList" size="small" border style="margin-top: 8px; width: 100%;">
|
||||
<el-table-column prop="name" label="物料名称" min-width="120" />
|
||||
<el-table-column prop="sku" label="SKU" width="100" />
|
||||
<el-table-column prop="sku" label="规格型号" width="120" />
|
||||
<el-table-column label="需求量" width="80">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.need }}</span>
|
||||
@ -273,7 +273,7 @@
|
||||
<table class="print-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>序号</th><th>物料名称</th><th>SKU</th><th>需求量</th><th>可用库存</th><th>缺料</th><th>结果</th>
|
||||
<th>序号</th><th>物料名称</th><th>规格型号</th><th>需求量</th><th>可用库存</th><th>缺料</th><th>结果</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -293,6 +293,28 @@
|
||||
缺货 <b style="color:#F56C6C;">{{ bomShortageCount }}</b> 种
|
||||
</p>
|
||||
<p style="color:#909399; font-size:12px;">缺货物料不会加入本次出库单,请补货后重新按 BOM 出库。</p>
|
||||
|
||||
<!-- ★ 欠料待补签收区(仅当有缺货时打印此联) -->
|
||||
<div v-if="bomShortageCount > 0" style="margin-top: 20px; border-top: 2px dashed #000; padding-top: 10px;">
|
||||
<p style="font-weight: bold; font-size: 14px;">⚠️ 欠料待补清单(请补料后在此签字)</p>
|
||||
<table class="print-table" style="margin-top: 6px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>序号</th><th>物料名称</th><th>规格型号</th><th>需补数量</th><th>补领人签字</th><th>日期</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, idx) in bomDetailList.filter(i => i.shortage > 0)" :key="idx">
|
||||
<td>{{ idx + 1 }}</td>
|
||||
<td>{{ row.name }}</td>
|
||||
<td>{{ row.sku }}</td>
|
||||
<td>{{ row.shortage }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
@ -592,7 +614,8 @@ const bomDetailList = computed(() => {
|
||||
const shortage = Math.max(0, totalNeed - stock)
|
||||
return {
|
||||
name: bomItem.child_name || bomItem.name || '未知物料',
|
||||
sku: bomItem.child_sku || bomItem.sku || '-',
|
||||
// ★ 用规格型号替代 SKU(getBomWithStock 返回的 children 无 sku,只有 child_spec)
|
||||
sku: bomItem.child_spec || bomItem.spec_model || '-',
|
||||
need: totalNeed,
|
||||
available: stock,
|
||||
shortage
|
||||
@ -834,6 +857,27 @@ const confirmBomAdd = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 阻断式二次确认:有缺料时强制确认,防止手快漏看
|
||||
const shortageItems = bomDetailList.value.filter(i => i.shortage > 0)
|
||||
if (shortageItems.length > 0) {
|
||||
const shortageText = shortageItems.map(i => `• ${i.name}(${i.sku})需 ${i.need} 个,缺 ${i.shortage} 个`).join('\n')
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`当前 BOM 有 ${shortageItems.length} 项物料库存不足:\n\n${shortageText}\n\n` +
|
||||
`继续出库将【只扣减可用库存】,缺货物料不会加入本次出库单。\n是否确认【部分出库】?`,
|
||||
'⚠️ 存在缺料,请确认',
|
||||
{
|
||||
confirmButtonText: '确认部分出库',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
distinguishCancelAndClose: true
|
||||
}
|
||||
)
|
||||
} catch (e) {
|
||||
return // 用户取消
|
||||
}
|
||||
}
|
||||
|
||||
const bomRows = currentBomDetail.value
|
||||
|
||||
// ★ 服务端精确查询 BOM 子件匹配库存(替代前端 while(true) 全量加载)
|
||||
@ -891,7 +935,29 @@ const confirmBomAdd = async () => {
|
||||
|
||||
// ★ 缺货记录持久化:按 BOM 编号保存到 localStorage
|
||||
const BOM_SHORTAGE_KEY = 'MOM_BOM_SHORTAGE_RECORDS'
|
||||
const bomShortageRecords = ref<Record<string, any[]>>(JSON.parse(localStorage.getItem(BOM_SHORTAGE_KEY) || '{}'))
|
||||
const bomShortageRecords = ref<Record<string, any>>(JSON.parse(localStorage.getItem(BOM_SHORTAGE_KEY) || '{}'))
|
||||
|
||||
// ★ 将缺货清单拼接进备注(借鸡生蛋:历史缺货记录永久留存在出库申请单备注)
|
||||
const buildRemarkWithShortage = (baseRemark: string): string => {
|
||||
const parts: string[] = []
|
||||
if (baseRemark) parts.push(baseRemark)
|
||||
|
||||
const shortageBoms = Object.keys(bomShortageRecords.value)
|
||||
if (shortageBoms.length > 0) {
|
||||
const detail: string[] = []
|
||||
for (const no of shortageBoms) {
|
||||
const rec = bomShortageRecords.value[no]
|
||||
if (!rec?.items?.length) continue
|
||||
const items = rec.items.map((i: any) => `${i.name}(${i.shortage}个)`).join('、')
|
||||
detail.push(`${no}: ${items}`)
|
||||
}
|
||||
if (detail.length > 0) {
|
||||
parts.push(`[BOM欠料待补] ${detail.join(';')}`)
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join(' | ')
|
||||
}
|
||||
|
||||
const saveBomShortage = (bomNo: string, shortageList: any[]) => {
|
||||
const records = { ...bomShortageRecords.value }
|
||||
@ -1030,7 +1096,7 @@ const confirmSubmitRequest = async () => {
|
||||
warehouse_location: item.warehouse_location || '',
|
||||
quantity: item.export_quantity || 0
|
||||
})),
|
||||
remark: trimmed,
|
||||
remark: buildRemarkWithShortage(trimmed),
|
||||
approver_id: requestApproverId.value
|
||||
}
|
||||
|
||||
|
||||
@ -15,19 +15,14 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ★ 出库模式切换 -->
|
||||
<!-- ★ 出库模式:仅支持按单出库(已移除直接出库) -->
|
||||
<div class="mode-switch-bar">
|
||||
<el-radio-group v-model="outboundMode" size="default" @change="handleModeChange">
|
||||
<el-radio-button value="by-request">按单出库</el-radio-button>
|
||||
<el-radio-button value="direct">直接出库</el-radio-button>
|
||||
</el-radio-group>
|
||||
<span class="mode-hint">
|
||||
{{ outboundMode === 'by-request' ? '需先选择已审批通过的申请单' : '无需审批单,自由扫码出库' }}
|
||||
</span>
|
||||
<el-tag type="primary" size="large" style="font-size: 14px;">按单出库</el-tag>
|
||||
<span class="mode-hint">请先选择已审批通过的申请单,再扫码出库</span>
|
||||
</div>
|
||||
|
||||
<!-- ★ 按单出库:审批单选择 -->
|
||||
<div v-if="outboundMode === 'by-request'" class="approval-request-select">
|
||||
<div class="approval-request-select">
|
||||
<el-select
|
||||
v-model="selectedRequestId"
|
||||
placeholder="请选择已审批通过的出库申请单"
|
||||
@ -54,7 +49,7 @@
|
||||
</div>
|
||||
|
||||
<!-- ★ 按单出库:计划清单预览 -->
|
||||
<div v-if="outboundMode === 'by-request' && selectedRequest" class="planned-items-section">
|
||||
<div v-if="selectedRequest" class="planned-items-section">
|
||||
<div class="planned-header">
|
||||
<span class="planned-title">计划出库清单</span>
|
||||
<el-tag type="success" size="small">{{ selectedRequest.items?.length || 0 }} 种</el-tag>
|
||||
@ -79,7 +74,7 @@
|
||||
|
||||
<div class="scan-section">
|
||||
|
||||
<template v-if="outboundMode === 'by-request' && !selectedRequestId">
|
||||
<template v-if="!selectedRequestId">
|
||||
<div class="camera-placeholder" style="background-color: #f5f5f5; cursor: not-allowed;">
|
||||
<el-icon :size="40" color="#909399"><CameraFilled /></el-icon>
|
||||
<span class="text">请先选择审批单</span>
|
||||
@ -304,7 +299,7 @@ const barcodeRef = ref()
|
||||
const formRef = ref()
|
||||
|
||||
// ★ 双轨制模式
|
||||
const outboundMode = ref<'by-request' | 'direct'>('by-request') // 'by-request' | 'direct'
|
||||
// ★ 已移除直接出库模式,所有出库必须按单出库(通过 selectedRequest 校验)
|
||||
const approvalRequests = ref<any[]>([])
|
||||
const selectedRequest = ref<any>(null)
|
||||
const requestsLoading = ref(false)
|
||||
@ -355,18 +350,6 @@ const selectedRequestId = computed({
|
||||
|
||||
const plannedItems = computed(() => selectedRequest.value?.items ?? [])
|
||||
|
||||
// ★ 模式切换
|
||||
const handleModeChange = () => {
|
||||
selectedRequest.value = null
|
||||
selectedRequestId.value = null
|
||||
cartItems.value = []
|
||||
form.consumer_name = ''
|
||||
form.remark = ''
|
||||
signatureFile.value = null
|
||||
signaturePreviewUrl.value = ''
|
||||
barcodeInput.value = ''
|
||||
}
|
||||
|
||||
// ★ 加载已审批通过的申请单
|
||||
const loadApprovalRequests = async () => {
|
||||
requestsLoading.value = true
|
||||
@ -489,7 +472,7 @@ const handleManualInput = async () => {
|
||||
if (!code) return
|
||||
|
||||
// ★ 按单出库模式:必须先选择申请单
|
||||
if (outboundMode.value === 'by-request' && !selectedRequest.value) {
|
||||
if (!selectedRequest.value) {
|
||||
ElMessage.warning('请先选择要出库的审批申请单')
|
||||
return
|
||||
}
|
||||
@ -503,14 +486,12 @@ const handleManualInput = async () => {
|
||||
const item = cartItems.value[existIndex]
|
||||
|
||||
// ★ 按单模式:追加时仍需校验计划数量
|
||||
if (outboundMode.value === 'by-request') {
|
||||
const err = validateAgainstPlan(item.name, item.spec_model, 1)
|
||||
if (err) {
|
||||
ElMessage.error(err)
|
||||
if (navigator.vibrate) navigator.vibrate([200, 100, 200])
|
||||
barcodeInput.value = ''
|
||||
return
|
||||
}
|
||||
const err = validateAgainstPlan(item.name, item.spec_model, 1)
|
||||
if (err) {
|
||||
ElMessage.error(err)
|
||||
if (navigator.vibrate) navigator.vibrate([200, 100, 200])
|
||||
barcodeInput.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
const maxQty = parseFloat(item.available_quantity)
|
||||
@ -540,14 +521,12 @@ const handleManualInput = async () => {
|
||||
}
|
||||
|
||||
// ★ 按单模式:扫码加入前校验是否在计划清单内
|
||||
if (outboundMode.value === 'by-request') {
|
||||
const err = validateAgainstPlan(item.name, item.spec_model, 1)
|
||||
if (err) {
|
||||
ElMessage.error(err)
|
||||
if (navigator.vibrate) navigator.vibrate([200, 100, 200])
|
||||
barcodeInput.value = ''
|
||||
return
|
||||
}
|
||||
const err = validateAgainstPlan(item.name, item.spec_model, 1)
|
||||
if (err) {
|
||||
ElMessage.error(err)
|
||||
if (navigator.vibrate) navigator.vibrate([200, 100, 200])
|
||||
barcodeInput.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
// 加入购物车
|
||||
@ -610,6 +589,11 @@ const submitForm = async () => {
|
||||
}
|
||||
if (!formRef.value) return
|
||||
if (cartItems.value.length === 0) return ElMessage.warning('请先添加商品')
|
||||
// ★ 按单出库:提交前必须已选择审批申请单
|
||||
if (!selectedRequest.value) {
|
||||
ElMessage.warning('请先选择要出库的审批申请单')
|
||||
return
|
||||
}
|
||||
|
||||
await formRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
@ -653,7 +637,7 @@ const submitForm = async () => {
|
||||
// 3. 组装发给后端的包
|
||||
const submitPayload: any = {
|
||||
outbound_type: form.outbound_type,
|
||||
request_id: outboundMode.value === 'by-request' && selectedRequest.value ? selectedRequest.value.id : null,
|
||||
request_id: selectedRequest.value ? selectedRequest.value.id : null,
|
||||
consumer_name: form.consumer_name,
|
||||
operator_name: form.operator_name,
|
||||
remark: form.remark,
|
||||
|
||||
Reference in New Issue
Block a user