feat(outbound): BOM 出库增加清单打印与缺货持久化提醒
- BOM 弹窗物料清单增加「本次结果」列(可出/缺货未出) - 增加「打印 BOM 清单」按钮,可打印当前 BOM 的完整出库清单 - 缺货物料持久化到 localStorage,下次打开同一 BOM 时自动提示历史缺货记录 防止重复出库或漏出 - 打印区域使用与 A4 出库单一致的 iframe 打印机制
This commit is contained in:
@ -239,14 +239,21 @@
|
|||||||
<span :style="{ color: row.shortage > 0 ? '#F56C6C' : '#67C23A', fontWeight: 'bold' }">{{ row.shortage > 0 ? row.shortage : '✓' }}</span>
|
<span :style="{ color: row.shortage > 0 ? '#F56C6C' : '#67C23A', fontWeight: 'bold' }">{{ row.shortage > 0 ? row.shortage : '✓' }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="本次结果" width="90" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.shortage > 0" type="danger" size="small">缺货未出</el-tag>
|
||||||
|
<el-tag v-else type="success" size="small">可出</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</el-alert>
|
</el-alert>
|
||||||
|
|
||||||
<div style="margin-left: 100px; color: #909399; font-size: 12px;">
|
<div style="margin-left: 100px; color: #909399; font-size: 12px;">
|
||||||
注意:系统将自动计算所需原料数量 ( 配方用量 × 套数 )。
|
注意:系统将自动计算所需原料数量 ( 配方用量 × 套数 )。缺货物料不会加入出库单,请先补货。
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="bomSelectVisible = false">取消</el-button>
|
<el-button @click="bomSelectVisible = false">取消</el-button>
|
||||||
|
<el-button v-if="bomDetailList.length > 0" :icon="Printer" @click="printBomChecklist">打印 BOM 清单</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="userStore.hasPermission('outbound_selection:operation')"
|
v-if="userStore.hasPermission('outbound_selection:operation')"
|
||||||
type="primary"
|
type="primary"
|
||||||
@ -257,6 +264,37 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- BOM 打印区域(隐藏,打印时克隆到 iframe) -->
|
||||||
|
<div id="bom-print-area" class="print-bom-area" style="display:none;">
|
||||||
|
<h2 style="text-align:center; margin:0 0 8px;">BOM 出库清单</h2>
|
||||||
|
<p style="text-align:center; margin:0 0 16px; font-size:13px;">
|
||||||
|
BOM: <b>{{ selectedBomNo }}</b> | 套数: <b>{{ bomSets }}</b> 套 | 生成时间: {{ bomPrintTime }}
|
||||||
|
</p>
|
||||||
|
<table class="print-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>序号</th><th>物料名称</th><th>SKU</th><th>需求量</th><th>可用库存</th><th>缺料</th><th>结果</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(row, idx) in bomDetailList" :key="idx">
|
||||||
|
<td>{{ idx + 1 }}</td>
|
||||||
|
<td>{{ row.name }}</td>
|
||||||
|
<td>{{ row.sku }}</td>
|
||||||
|
<td>{{ row.need }}</td>
|
||||||
|
<td>{{ row.available }}</td>
|
||||||
|
<td>{{ row.shortage > 0 ? row.shortage : '-' }}</td>
|
||||||
|
<td>{{ row.shortage > 0 ? '缺货未出' : '可出' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p style="margin-top:12px;">
|
||||||
|
可出 <b style="color:#67C23A;">{{ bomAvailableCount }}</b> 种 |
|
||||||
|
缺货 <b style="color:#F56C6C;">{{ bomShortageCount }}</b> 种
|
||||||
|
</p>
|
||||||
|
<p style="color:#909399; font-size:12px;">缺货物料不会加入本次出库单,请补货后重新按 BOM 出库。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="previewVisible"
|
v-model="previewVisible"
|
||||||
title="出库单核对与打印"
|
title="出库单核对与打印"
|
||||||
@ -569,6 +607,11 @@ const bomDetailList = computed(() => {
|
|||||||
|
|
||||||
const hasShortage = computed(() => bomDetailList.value.some((item: any) => item.shortage > 0) && bomSets.value > maxBuildableSets.value)
|
const hasShortage = computed(() => bomDetailList.value.some((item: any) => item.shortage > 0) && bomSets.value > maxBuildableSets.value)
|
||||||
|
|
||||||
|
// ★ BOM 打印统计
|
||||||
|
const bomPrintTime = ref('')
|
||||||
|
const bomAvailableCount = computed(() => bomDetailList.value.filter(i => i.shortage === 0).length)
|
||||||
|
const bomShortageCount = computed(() => bomDetailList.value.filter(i => i.shortage > 0).length)
|
||||||
|
|
||||||
// --- 辅助方法 ---
|
// --- 辅助方法 ---
|
||||||
const getTypeTag = (type: string) => {
|
const getTypeTag = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -741,6 +784,23 @@ const openBomSelect = async () => {
|
|||||||
bomOptions.value = Array.from(groupMap.entries()).map(([category, items]) => ({
|
bomOptions.value = Array.from(groupMap.entries()).map(([category, items]) => ({
|
||||||
category, count: items.length, items
|
category, count: items.length, items
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// ★ 提示有历史缺货记录的 BOM,防止下次重复出/漏出
|
||||||
|
const pendingBoms = Object.keys(bomShortageRecords.value).filter(no =>
|
||||||
|
flatItems.some(i => i.bom_no === no)
|
||||||
|
)
|
||||||
|
if (pendingBoms.length > 0) {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
`检测到以下 BOM 存在未完成的缺货记录(上次按 BOM 出库时缺货):\n\n` +
|
||||||
|
pendingBoms.map(no => {
|
||||||
|
const rec = bomShortageRecords.value[no]
|
||||||
|
return `• ${no}(${rec.items.length} 种缺货,${rec.time})`
|
||||||
|
}).join('\n') +
|
||||||
|
`\n\n建议先补货后再出库,避免漏出。是否查看?`,
|
||||||
|
'存在未完成出库',
|
||||||
|
{ confirmButtonText: '知道了', cancelButtonText: '忽略', type: 'warning' }
|
||||||
|
).catch(() => {})
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error('加载 BOM 列表失败')
|
ElMessage.error('加载 BOM 列表失败')
|
||||||
}
|
}
|
||||||
@ -818,12 +878,36 @@ const confirmBomAdd = async () => {
|
|||||||
if (addedCount > 0) {
|
if (addedCount > 0) {
|
||||||
const tip = skippedCount > 0 ? `(跳过 ${skippedCount} 种缺货物料)` : ''
|
const tip = skippedCount > 0 ? `(跳过 ${skippedCount} 种缺货物料)` : ''
|
||||||
ElMessage.success(`成功添加 ${addedCount} 类物料${tip}`)
|
ElMessage.success(`成功添加 ${addedCount} 类物料${tip}`)
|
||||||
|
|
||||||
|
// ★ 记录本次缺货清单(持久化,防止下次重复出/漏出)
|
||||||
|
saveBomShortage(selectedBomNo.value, bomDetailList.value.filter(i => i.shortage > 0))
|
||||||
bomSelectVisible.value = false
|
bomSelectVisible.value = false
|
||||||
} else {
|
} else {
|
||||||
ElMessage.warning('该 BOM 所有物料库存均为 0')
|
ElMessage.warning('该 BOM 所有物料库存均为 0')
|
||||||
|
// 全部缺货也要记录
|
||||||
|
saveBomShortage(selectedBomNo.value, bomDetailList.value.filter(i => i.shortage > 0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ★ 缺货记录持久化:按 BOM 编号保存到 localStorage
|
||||||
|
const BOM_SHORTAGE_KEY = 'MOM_BOM_SHORTAGE_RECORDS'
|
||||||
|
const bomShortageRecords = ref<Record<string, any[]>>(JSON.parse(localStorage.getItem(BOM_SHORTAGE_KEY) || '{}'))
|
||||||
|
|
||||||
|
const saveBomShortage = (bomNo: string, shortageList: any[]) => {
|
||||||
|
const records = { ...bomShortageRecords.value }
|
||||||
|
if (shortageList.length > 0) {
|
||||||
|
records[bomNo] = {
|
||||||
|
time: new Date().toLocaleString('zh-CN'),
|
||||||
|
items: shortageList.map(i => ({ name: i.name, sku: i.sku, need: i.need, available: i.available, shortage: i.shortage }))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 本次没有缺货 → 清除该 BOM 的历史缺货记录
|
||||||
|
delete records[bomNo]
|
||||||
|
}
|
||||||
|
bomShortageRecords.value = records
|
||||||
|
localStorage.setItem(BOM_SHORTAGE_KEY, JSON.stringify(records))
|
||||||
|
}
|
||||||
|
|
||||||
// --- 通用逻辑 ---
|
// --- 通用逻辑 ---
|
||||||
|
|
||||||
const handleSelectionChange = (val: any[]) => {
|
const handleSelectionChange = (val: any[]) => {
|
||||||
@ -963,6 +1047,51 @@ const confirmSubmitRequest = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ★ BOM 清单打印(复用与 confirmPrint 相同的 iframe 打印机制)
|
||||||
|
const printBomChecklist = () => {
|
||||||
|
if (bomDetailList.value.length === 0) return ElMessage.warning('请先选择 BOM 并加载清单')
|
||||||
|
bomPrintTime.value = new Date().toLocaleString('zh-CN')
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const printElement = document.getElementById('bom-print-area')
|
||||||
|
if (!printElement) return
|
||||||
|
|
||||||
|
const iframe = document.createElement('iframe')
|
||||||
|
iframe.style.position = 'fixed'
|
||||||
|
iframe.style.right = '0'
|
||||||
|
iframe.style.bottom = '0'
|
||||||
|
iframe.style.width = '0'
|
||||||
|
iframe.style.height = '0'
|
||||||
|
iframe.style.border = '0'
|
||||||
|
document.body.appendChild(iframe)
|
||||||
|
|
||||||
|
const iframeDoc = iframe.contentWindow?.document
|
||||||
|
if (!iframeDoc) return
|
||||||
|
iframeDoc.open()
|
||||||
|
iframeDoc.write('<html><head><title>BOM 出库清单</title></head><body></body></html>')
|
||||||
|
iframeDoc.close()
|
||||||
|
|
||||||
|
const style = `
|
||||||
|
body { font-family: 'Microsoft YaHei', sans-serif; }
|
||||||
|
.print-bom-area { display: block !important; padding: 20px; }
|
||||||
|
.print-table { width: 100%; border-collapse: collapse; }
|
||||||
|
.print-table th, .print-table td { border: 1px solid #000; padding: 6px 8px; font-size: 13px; text-align: center; }
|
||||||
|
.print-table th { background: #f0f0f0; }
|
||||||
|
`;
|
||||||
|
const styleEl = iframeDoc.createElement('style')
|
||||||
|
styleEl.textContent = style
|
||||||
|
iframeDoc.head.appendChild(styleEl)
|
||||||
|
|
||||||
|
iframeDoc.body.appendChild(printElement.cloneNode(true))
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
iframe.contentWindow?.focus()
|
||||||
|
iframe.contentWindow?.print()
|
||||||
|
setTimeout(() => document.body.removeChild(iframe), 1000)
|
||||||
|
}, 500)
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
const confirmPrint = async () => {
|
const confirmPrint = async () => {
|
||||||
previewVisible.value = false;
|
previewVisible.value = false;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user