feat: restore available qty color and add BOM shortage analysis (kitting) for outbound
This commit is contained in:
@ -235,7 +235,7 @@
|
|||||||
|
|
||||||
<el-table-column v-if="columns.available.visible" prop="availableCount" label="可用数" min-width="100" align="center" sortable="custom">
|
<el-table-column v-if="columns.available.visible" prop="availableCount" label="可用数" min-width="100" align="center" sortable="custom">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span>{{ row.availableCount }}</span>
|
<span :style="{ fontWeight: 'bold', color: row.availableCount > 0 ? '#409EFF' : 'inherit' }">{{ row.availableCount }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
|
|||||||
@ -150,10 +150,10 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="bomSelectVisible" title="按 BOM 套餐添加" width="600px" destroy-on-close :close-on-click-modal="false">
|
<el-dialog v-model="bomSelectVisible" title="按 BOM 套餐添加" width="700px" destroy-on-close :close-on-click-modal="false">
|
||||||
<el-form label-width="100px">
|
<el-form label-width="100px">
|
||||||
<el-form-item label="选择产品">
|
<el-form-item label="选择产品">
|
||||||
<el-select v-model="selectedBomNo" filterable placeholder="请选择启用状态的 BOM 配方" style="width: 100%">
|
<el-select v-model="selectedBomNo" filterable placeholder="请选择启用状态的 BOM 配方" style="width: 100%" @change="() => {}">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="b in bomOptions"
|
v-for="b in bomOptions"
|
||||||
:key="`${b.bom_no}_${b.version}`"
|
:key="`${b.bom_no}_${b.version}`"
|
||||||
@ -164,14 +164,51 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="生产套数">
|
<el-form-item label="生产套数">
|
||||||
<el-input-number v-model="bomSets" :min="1" label="套" style="width: 200px;" :disabled="!userStore.hasPermission('outbound_selection:operation')" />
|
<el-input-number v-model="bomSets" :min="1" label="套" style="width: 200px;" :disabled="!userStore.hasPermission('outbound_selection:operation')" />
|
||||||
|
<el-tag v-if="selectedBomNo && maxBuildableSets >= 0" type="success" style="margin-left: 16px;">
|
||||||
|
当前库存最多可成套出库: {{ maxBuildableSets }} 套
|
||||||
|
</el-tag>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 齐套性分析警告 -->
|
||||||
|
<el-alert
|
||||||
|
v-if="selectedBomNo && shortageList.length > 0 && bomSets > maxBuildableSets"
|
||||||
|
type="error"
|
||||||
|
:closable="false"
|
||||||
|
style="margin: 16px 0;"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<span style="font-weight: bold;">库存不足以组成 {{ bomSets }} 套,缺少以下物料:</span>
|
||||||
|
</template>
|
||||||
|
<el-table :data="shortageList" 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 label="需补足数量" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span style="color: #F56C6C; font-weight: bold;">{{ row.shortage }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="可用库存" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span>{{ row.available }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</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="userStore.hasPermission('outbound_selection:operation')" type="primary" @click="confirmBomAdd">一键计算并添加</el-button>
|
<el-button
|
||||||
|
v-if="userStore.hasPermission('outbound_selection:operation')"
|
||||||
|
type="primary"
|
||||||
|
@click="confirmBomAdd"
|
||||||
|
:disabled="hasShortage"
|
||||||
|
>
|
||||||
|
{{ hasShortage ? '库存不足,无法添加' : '一键计算并添加' }}
|
||||||
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
@ -280,7 +317,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { Printer, Search, Plus, Download, List } from '@element-plus/icons-vue'
|
import { Printer, Search, Plus, Download, List } from '@element-plus/icons-vue'
|
||||||
import { ElMessage, ElTable, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElTable, ElMessageBox } from 'element-plus'
|
||||||
import { getAllStock, printSelectionList } from '@/api/inbound/stock'
|
import { getAllStock, printSelectionList } from '@/api/inbound/stock'
|
||||||
@ -309,6 +346,7 @@ const manualTableRef = ref<InstanceType<typeof ElTable>>()
|
|||||||
const bomOptions = ref<any[]>([])
|
const bomOptions = ref<any[]>([])
|
||||||
const selectedBomNo = ref('')
|
const selectedBomNo = ref('')
|
||||||
const bomSets = ref(1)
|
const bomSets = ref(1)
|
||||||
|
const currentBomDetail = ref<any[]>([]) // 当前选中的BOM明细
|
||||||
|
|
||||||
// 打印相关
|
// 打印相关
|
||||||
const currentTime = ref('')
|
const currentTime = ref('')
|
||||||
@ -335,6 +373,47 @@ const totalExportCount = computed(() => {
|
|||||||
return validSelectedItems.value.reduce((sum, item) => sum + (item.export_quantity || 0), 0)
|
return validSelectedItems.value.reduce((sum, item) => sum + (item.export_quantity || 0), 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// --- BOM 齐套性分析计算属性 ---
|
||||||
|
const maxBuildableSets = computed(() => {
|
||||||
|
if (currentBomDetail.value.length === 0 || allStockData.value.length === 0) return 0
|
||||||
|
let minSets = Infinity
|
||||||
|
currentBomDetail.value.forEach((bomItem: any) => {
|
||||||
|
const dosage = parseFloat(bomItem.dosage) || 0 // 单套需求量
|
||||||
|
if (dosage <= 0) return
|
||||||
|
// 匹配库存中的可用数量
|
||||||
|
const stockItem = allStockData.value.find((s: any) => s.base_id && s.base_id == bomItem.child_id)
|
||||||
|
const available = stockItem ? (stockItem.availableCount || 0) : 0
|
||||||
|
const buildable = Math.floor(available / dosage)
|
||||||
|
if (buildable < minSets) minSets = buildable
|
||||||
|
})
|
||||||
|
return minSets === Infinity ? 0 : minSets
|
||||||
|
})
|
||||||
|
|
||||||
|
const shortageList = computed(() => {
|
||||||
|
if (currentBomDetail.value.length === 0 || allStockData.value.length === 0 || bomSets.value <= 0) return []
|
||||||
|
const target = bomSets.value
|
||||||
|
const shortages: any[] = []
|
||||||
|
currentBomDetail.value.forEach((bomItem: any) => {
|
||||||
|
const dosage = parseFloat(bomItem.dosage) || 0 // 单套需求量
|
||||||
|
const totalNeed = dosage * target
|
||||||
|
const stockItem = allStockData.value.find((s: any) => s.base_id && s.base_id == bomItem.child_id)
|
||||||
|
const available = stockItem ? (stockItem.availableCount || 0) : 0
|
||||||
|
const shortage = totalNeed - available
|
||||||
|
if (shortage > 0) {
|
||||||
|
shortages.push({
|
||||||
|
name: bomItem.child_name || bomItem.name || '未知物料',
|
||||||
|
sku: bomItem.child_sku || bomItem.sku || '-',
|
||||||
|
need: totalNeed,
|
||||||
|
available: available,
|
||||||
|
shortage: shortage
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return shortages
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasShortage = computed(() => shortageList.value.length > 0 && bomSets.value > maxBuildableSets.value)
|
||||||
|
|
||||||
// --- 辅助方法 ---
|
// --- 辅助方法 ---
|
||||||
const getTypeTag = (type: string) => {
|
const getTypeTag = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -466,6 +545,8 @@ const handleMainQuantityChange = (val: number | undefined, row: any) => {
|
|||||||
const openBomSelect = async () => {
|
const openBomSelect = async () => {
|
||||||
bomSelectVisible.value = true
|
bomSelectVisible.value = true
|
||||||
bomSets.value = 1
|
bomSets.value = 1
|
||||||
|
selectedBomNo.value = ''
|
||||||
|
currentBomDetail.value = []
|
||||||
try {
|
try {
|
||||||
const res = await getBomList({ active_only: true })
|
const res = await getBomList({ active_only: true })
|
||||||
bomOptions.value = res.data || []
|
bomOptions.value = res.data || []
|
||||||
@ -474,6 +555,21 @@ const openBomSelect = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 监听 BOM 选择变化,自动加载明细并计算齐套性
|
||||||
|
watch(selectedBomNo, async (newBomNo) => {
|
||||||
|
if (!newBomNo) {
|
||||||
|
currentBomDetail.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const detailRes = await getBomDetail(newBomNo)
|
||||||
|
currentBomDetail.value = detailRes.data?.children || []
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('加载 BOM 明细失败')
|
||||||
|
currentBomDetail.value = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const confirmBomAdd = async () => {
|
const confirmBomAdd = async () => {
|
||||||
if(!selectedBomNo.value) return ElMessage.warning('请选择 BOM');
|
if(!selectedBomNo.value) return ElMessage.warning('请选择 BOM');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user