feat: restore available qty color and add BOM shortage analysis (kitting) for outbound

This commit is contained in:
DXC
2026-03-19 09:33:15 +08:00
parent e6dafc7775
commit de887136a3
2 changed files with 101 additions and 5 deletions

View File

@ -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>

View File

@ -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');