feat: 借库归还日期贯穿申请/审批/扫码借库;BOM 套餐添加与打印、缺货写入申请原因
This commit is contained in:
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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}`
|
||||||
|
|||||||
@ -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 = ''
|
||||||
|
|||||||
Reference in New Issue
Block a user