feat: 借库归还日期贯穿申请/审批/扫码借库;BOM 套餐添加与打印、缺货写入申请原因

This commit is contained in:
yueli
2026-09-02 18:39:17 +08:00
parent bf7b2cc6d3
commit ec9aafbe51
3 changed files with 194 additions and 8 deletions

View File

@ -247,6 +247,7 @@
</div> </div>
<template #footer> <template #footer>
<el-button @click="bomSelectVisible = false">取消</el-button> <el-button @click="bomSelectVisible = false">取消</el-button>
<el-button :icon="Printer" :disabled="bomDetailList.length === 0" @click="printBomChecklist" title="先选择 BOM 配方后即可打印清单">打印 BOM 清单</el-button>
<el-button <el-button
v-if="userStore.hasPermission('op_borrow_apply:operation')" v-if="userStore.hasPermission('op_borrow_apply:operation')"
type="primary" type="primary"
@ -257,6 +258,62 @@
</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>{{ selectedBomLabel }}</b> 套数: <b>{{ bomSets }}</b> 生成时间: {{ bomPrintTime }}
</p>
<table class="print-table">
<thead>
<tr>
<th>序号</th><th>物料名称</th><th>规格型号</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 v-if="bomShortageCount > 0" style="margin-top: 20px; border-top: 2px dashed #000; padding-top: 10px;">
<p style="text-align:center; margin:0 0 8px; font-size:13px;">
BOM: <b>{{ selectedBomLabel }}</b> 套数: <b>{{ bomSets }}</b> 生成时间: {{ bomPrintTime }}
</p>
<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 <el-dialog
v-model="previewVisible" v-model="previewVisible"
title="借库单核对与打印" title="借库单核对与打印"
@ -347,6 +404,19 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="预计归还日期">
<el-date-picker
v-model="requestExpectedReturn"
type="date"
placeholder="请选择日期"
style="width: 100%"
value-format="YYYY-MM-DD"
:disabled="requestIndefinite"
/>
<el-checkbox v-model="requestIndefinite" @change="handleRequestIndefiniteChange" style="margin-left: 12px;">
长期借用
</el-checkbox>
</el-form-item>
<el-form-item label="申请原因"> <el-form-item label="申请原因">
<el-input <el-input
v-model="requestRemark" v-model="requestRemark"
@ -473,6 +543,8 @@ const printLoading = ref(false)
const requestDialogVisible = ref(false) const requestDialogVisible = ref(false)
const borrowerName = ref('') const borrowerName = ref('')
const requestRemark = ref('') const requestRemark = ref('')
const requestExpectedReturn = ref('')
const requestIndefinite = ref(false)
const requestApproverId = ref<number | null>(null) const requestApproverId = ref<number | null>(null)
const approvers = ref<any[]>([]) const approvers = ref<any[]>([])
const requestSubmitting = ref(false) const requestSubmitting = ref(false)
@ -495,6 +567,7 @@ const bomOptions = ref<any[]>([])
const selectedBomNo = ref('') const selectedBomNo = ref('')
const bomSets = ref(1) const bomSets = ref(1)
const currentBomDetail = ref<any[]>([]) // 当前选中的BOM明细 const currentBomDetail = ref<any[]>([]) // 当前选中的BOM明细
const bomPrintTime = ref('')
// BOM 树形数据(将分组数据映射为 el-tree-select 需要的结构) // BOM 树形数据(将分组数据映射为 el-tree-select 需要的结构)
const treeData = computed(() => { const treeData = computed(() => {
@ -503,7 +576,8 @@ const treeData = computed(() => {
label: `${group.category} (${group.count})`, label: `${group.category} (${group.count})`,
disabled: true, // 禁止选中分类本身 disabled: true, // 禁止选中分类本身
children: (group.items || []).map((b: any) => ({ children: (group.items || []).map((b: any) => ({
value: b.bom_no, // ★ 用 bom_no + version 组合作为唯一 value避免同一产品多版本时回显错乱
value: `${b.bom_no}###${b.version}`,
label: `${b.bom_no} - ${b.parent_name} - ${b.version}` label: `${b.bom_no} - ${b.parent_name} - ${b.version}`
})) }))
})) }))
@ -546,6 +620,14 @@ 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)
const bomAvailableCount = computed(() => bomDetailList.value.filter(i => i.shortage === 0).length)
const bomShortageCount = computed(() => bomDetailList.value.filter(i => i.shortage > 0).length)
// BOM 选择显示标签bom_no###version → "bom_no (version)"
const selectedBomLabel = computed(() => {
const [no, ver] = (selectedBomNo.value || '').split('###')
return ver ? `${no} (${ver})` : no
})
// 打印相关 // 打印相关
const currentTime = ref('') const currentTime = ref('')
@ -715,13 +797,15 @@ const openBomSelect = async () => {
} }
// 监听 BOM 选择变化,自动加载明细(含库存)并计算齐套性 // 监听 BOM 选择变化,自动加载明细(含库存)并计算齐套性
watch(selectedBomNo, async (newBomNo) => { watch(selectedBomNo, async (newKey) => {
if (!newBomNo) { if (!newKey) {
currentBomDetail.value = [] currentBomDetail.value = []
return return
} }
// ★ 组合 key 拆解为 bom_no + version按选中版本精确拉取明细
const [bomNo, version] = newKey.split('###')
try { try {
const detailRes: any = await getBomWithStock(newBomNo) const detailRes: any = await getBomWithStock(bomNo, version)
currentBomDetail.value = detailRes.data?.children || [] currentBomDetail.value = detailRes.data?.children || []
} catch (e) { } catch (e) {
ElMessage.error('加载 BOM 明细失败') ElMessage.error('加载 BOM 明细失败')
@ -734,7 +818,9 @@ const confirmBomAdd = async () => {
if (currentBomDetail.value.length === 0) { if (currentBomDetail.value.length === 0) {
try { try {
const detailRes: any = await getBomWithStock(selectedBomNo.value) // ★ 组合 key 拆解为 bom_no + version按选中版本精确拉取明细
const [bomNo, version] = (selectedBomNo.value || '').split('###')
const detailRes: any = await getBomWithStock(bomNo, version)
currentBomDetail.value = detailRes.data?.children || [] currentBomDetail.value = detailRes.data?.children || []
} catch (e) { } catch (e) {
ElMessage.error('获取 BOM 详情失败') ElMessage.error('获取 BOM 详情失败')
@ -792,6 +878,51 @@ const confirmBomAdd = 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 handleMainQuantityChange = (val: number | undefined, row: any) => { const handleMainQuantityChange = (val: number | undefined, row: any) => {
if (val === 0) { if (val === 0) {
@ -900,6 +1031,8 @@ const openRequestDialog = () => {
borrowerName.value = userStore.user?.username || '' borrowerName.value = userStore.user?.username || ''
requestRemark.value = '' requestRemark.value = ''
requestApproverId.value = null requestApproverId.value = null
requestExpectedReturn.value = ''
requestIndefinite.value = false
loadApprovers() loadApprovers()
requestDialogVisible.value = true requestDialogVisible.value = true
} }
@ -915,7 +1048,27 @@ const loadApprovers = async () => {
} }
} }
// ★ 长期借用勾选:勾选时清空已选的归还日期
const handleRequestIndefiniteChange = (val: boolean) => {
if (val) requestExpectedReturn.value = ''
}
// ★ 确认提交借库申请 // ★ 确认提交借库申请
// ★ 将 BOM 缺货清单拼接进申请原因(与出库选单一致)
const buildRemarkWithShortage = (baseRemark: string): string => {
const parts: string[] = []
if (baseRemark) parts.push(baseRemark)
// ★ 用当前实时计算的缺货清单(需量 - 当前库存),而非 localStorage 历史记录
const shortageItems = bomDetailList.value.filter((i: any) => i.shortage > 0)
if (shortageItems.length > 0) {
const items = shortageItems.map((i: any) => `${i.name}(${i.shortage}个)`).join('、')
parts.push(`[BOM欠料待补] ${items}`)
}
return parts.join(' | ')
}
const confirmSubmitRequest = async () => { const confirmSubmitRequest = async () => {
requestSubmitting.value = true requestSubmitting.value = true
try { try {
@ -924,9 +1077,11 @@ const confirmSubmitRequest = async () => {
name: item.name || '', name: item.name || '',
spec_model: item.standard || '', spec_model: item.standard || '',
warehouse_location: item.warehouse_location || '', warehouse_location: item.warehouse_location || '',
quantity: item.export_quantity || 0 quantity: item.export_quantity || 0,
expected_return_time: requestIndefinite.value ? null : requestExpectedReturn.value,
is_indefinite: requestIndefinite.value
})), })),
remark: requestRemark.value.trim(), remark: buildRemarkWithShortage(requestRemark.value.trim()),
approver_id: requestApproverId.value approver_id: requestApproverId.value
} }

View File

@ -76,6 +76,15 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="归还期限" width="130" align="center">
<template #default="{ row }">
<span v-if="getReturnDeadline(row)" :style="row?.items?.[0]?.is_indefinite ? 'color:#E6A23C;font-weight:bold;' : ''">
{{ getReturnDeadline(row) }}
</span>
<span v-else style="color: #909399;">未填写</span>
</template>
</el-table-column>
<el-table-column prop="created_at" label="申请时间" width="170" /> <el-table-column prop="created_at" label="申请时间" width="170" />
<el-table-column label="状态" width="100" align="center"> <el-table-column label="状态" width="100" align="center">
@ -219,6 +228,14 @@ const getApplicantName = (id: number | null) => {
return userNameCache.value[id] ?? `用户 #${id}` return userNameCache.value[id] ?? `用户 #${id}`
} }
// 从申请单明细快照中读取归还期限(旧申请单无此字段时返回空串)
const getReturnDeadline = (row: any) => {
const first = row?.items?.[0]
if (first?.is_indefinite) return '长期借用'
if (first?.expected_return_time) return first.expected_return_time
return ''
}
const getApproverName = (id: number | null) => { const getApproverName = (id: number | null) => {
if (!id) return '-' if (!id) return '-'
return userNameCache.value[id] ?? `用户 #${id}` return userNameCache.value[id] ?? `用户 #${id}`

View File

@ -416,11 +416,25 @@ const loadApprovalRequests = async () => {
} }
} }
// ★ 切换审批单时:清空购物车和签名,防止跨单据污染 // ★ 切换审批单时:带出申请时填写的归还日期/长期借用,并清空购物车和签名,防止跨单据污染
const handleApprovalChange = (val: number | null) => { const handleApprovalChange = (val: number | null) => {
if (!val) { if (!val) {
selectedApprovalId.value = null selectedApprovalId.value = null
} }
// 从申请单明细快照中带出"预计归还日期"(申请时通过 items_json 存储,无需改数据库)
const req = approvalRequests.value.find(r => r.id === val)
const firstItem = req?.items?.[0]
if (firstItem?.is_indefinite) {
isIndefinite.value = true
form.expected_return_time = ''
} else if (firstItem?.expected_return_time) {
isIndefinite.value = false
form.expected_return_time = firstItem.expected_return_time
} else {
// 旧申请单/未填时间:保持手动选择
isIndefinite.value = false
form.expected_return_time = ''
}
cartItems.value = [] cartItems.value = []
signatureFile.value = null signatureFile.value = null
signaturePreviewUrl.value = '' signaturePreviewUrl.value = ''