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>
|
||||
</template>
|
||||
</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-alert>
|
||||
|
||||
<div style="margin-left: 100px; color: #909399; font-size: 12px;">
|
||||
注意:系统将自动计算所需原料数量 ( 配方用量 × 套数 )。
|
||||
注意:系统将自动计算所需原料数量 ( 配方用量 × 套数 )。缺货物料不会加入出库单,请先补货。
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="bomSelectVisible = false">取消</el-button>
|
||||
<el-button v-if="bomDetailList.length > 0" :icon="Printer" @click="printBomChecklist">打印 BOM 清单</el-button>
|
||||
<el-button
|
||||
v-if="userStore.hasPermission('outbound_selection:operation')"
|
||||
type="primary"
|
||||
@ -257,6 +264,37 @@
|
||||
</template>
|
||||
</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
|
||||
v-model="previewVisible"
|
||||
title="出库单核对与打印"
|
||||
@ -569,6 +607,11 @@ const bomDetailList = computed(() => {
|
||||
|
||||
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) => {
|
||||
switch (type) {
|
||||
@ -741,6 +784,23 @@ const openBomSelect = async () => {
|
||||
bomOptions.value = Array.from(groupMap.entries()).map(([category, 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) {
|
||||
ElMessage.error('加载 BOM 列表失败')
|
||||
}
|
||||
@ -818,12 +878,36 @@ const confirmBomAdd = async () => {
|
||||
if (addedCount > 0) {
|
||||
const tip = skippedCount > 0 ? `(跳过 ${skippedCount} 种缺货物料)` : ''
|
||||
ElMessage.success(`成功添加 ${addedCount} 类物料${tip}`)
|
||||
|
||||
// ★ 记录本次缺货清单(持久化,防止下次重复出/漏出)
|
||||
saveBomShortage(selectedBomNo.value, bomDetailList.value.filter(i => i.shortage > 0))
|
||||
bomSelectVisible.value = false
|
||||
} else {
|
||||
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[]) => {
|
||||
@ -963,9 +1047,54 @@ 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 () => {
|
||||
previewVisible.value = false;
|
||||
|
||||
|
||||
try {
|
||||
const payload = validSelectedItems.value.map(item => ({
|
||||
name: item.name, standard: item.standard, quantity: item.export_quantity
|
||||
|
||||
Reference in New Issue
Block a user