You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
besure_web/src/views/erp/stock/move/components/StockMoveItemForm.vue

637 lines
20 KiB
Vue

<template>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
v-loading="formLoading"
label-width="0px"
:inline-message="true"
:disabled="disabled"
>
<el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px">
<el-table-column label="序号" type="index" align="center" width="60" />
<el-table-column :label="productColumnLabel" min-width="180">
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.productId`" :rules="formRules.productId" class="mb-0px!">
<el-input
:model-value="getProductDisplayName(row)"
readonly
:clearable="!disabled"
:placeholder="productPlaceholder"
@click="handleProductInputClick(row)"
@clear="handleProductClear(row)"
/>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="调出仓库" min-width="125">
<template #default="{ row, $index }">
<el-form-item
:prop="`${$index}.fromWarehouseId`"
:rules="formRules.fromWarehouseId"
class="mb-0px!"
>
<el-select
v-model="row.fromWarehouseId"
clearable
filterable
:disabled="!row.productId"
placeholder="请选择调出仓库"
@change="onChangeFromWarehouse($event, row)"
>
<el-option
v-for="item in getFromWarehouseOptions(row)"
:key="getStockWarehouseId(item)"
:label="getStockWarehouseName(item)"
:value="getStockWarehouseId(item)"
/>
</el-select>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="调出库区" min-width="125">
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.fromAreaId`" :rules="formRules.fromAreaId" class="mb-0px!">
<el-select
v-model="row.fromAreaId"
clearable
filterable
:disabled="!row.fromWarehouseId"
placeholder="请选择调出库区"
@change="handleFromAreaChange(row)"
>
<el-option
v-for="item in getFromAreaOptions(row)"
:key="getStockAreaId(item)"
:label="getAreaLabel(item)"
:value="getStockAreaId(item)"
/>
</el-select>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="编码" min-width="150">
<template #default="{ row }">
<el-form-item class="mb-0px!">
<el-input disabled v-model="row.productBarCode" />
</el-form-item>
</template>
</el-table-column>
<el-table-column label="库存" min-width="100">
<template #default="{ row }">
<el-form-item class="mb-0px!">
<el-input disabled v-model="row.stockCount" :formatter="erpCountInputFormatter" />
</el-form-item>
</template>
</el-table-column>
<el-table-column label="单位" min-width="80">
<template #default="{ row }">
<el-form-item class="mb-0px!">
<el-input disabled v-model="row.productUnitName" />
</el-form-item>
</template>
</el-table-column>
<el-table-column label="调入仓库" min-width="125">
<template #default="{ row, $index }">
<el-form-item
:prop="`${$index}.toWarehouseId`"
:rules="formRules.toWarehouseId"
class="mb-0px!"
>
<el-select
v-model="row.toWarehouseId"
clearable
filterable
:disabled="!row.productId"
placeholder="请选择调入仓库"
@change="onChangeToWarehouse(row)"
>
<el-option v-for="item in warehouseList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="调入库区" min-width="125">
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.toAreaId`" :rules="formRules.toAreaId" class="mb-0px!">
<el-select
v-model="row.toAreaId"
clearable
filterable
:disabled="!row.toWarehouseId"
placeholder="请选择调入库区"
@change="handleToAreaChange(row)"
>
<el-option
v-for="item in getToAreaOptions(row)"
:key="item.id"
:label="getAreaLabel(item)"
:value="item.id"
/>
</el-select>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="数量" prop="count" fixed="right" min-width="140">
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.count`" :rules="formRules.count" class="mb-0px!">
<el-input-number
v-model="row.count"
controls-position="right"
:min="0.001"
:precision="3"
class="!w-100%"
/>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="备注" min-width="150">
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.remark`" class="mb-0px!">
<el-input v-model="row.remark" placeholder="请输入备注" />
</el-form-item>
</template>
</el-table-column>
<el-table-column v-if="!disabled" align="center" fixed="right" label="操作" width="60">
<template #default="{ $index }">
<el-button type="danger" @click="handleDelete($index)" link>
<Icon icon="ep:delete" />
</el-button>
</template>
</el-table-column>
</el-table>
</el-form>
<el-row justify="center" class="mt-3" v-if="!disabled">
<el-button @click="handleAdd" round>+ 添加调拨产品</el-button>
</el-row>
<Dialog :title="productSelectTitle" v-model="productDialogVisible" width="900px" class="stock-move-product-dialog">
<el-form :model="productQueryParams" :inline="true" class="-mb-15px">
<el-form-item :label="t('ErpStock.Item.code')">
<el-input
v-model="productQueryParams.barCode"
clearable
:placeholder="t('ErpStock.Item.placeholderCode')"
@keyup.enter="handleProductDialogQuery"
/>
</el-form-item>
<el-form-item :label="t('ErpStock.Item.name')">
<el-input
v-model="productQueryParams.name"
clearable
:placeholder="t('ErpStock.Item.placeholderName')"
@keyup.enter="handleProductDialogQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleProductDialogQuery">
<Icon icon="ep:search" class="mr-5px" /> {{ t('common.query') }}
</el-button>
</el-form-item>
</el-form>
<el-table
v-loading="productDialogLoading"
:data="productDialogList"
row-key="id"
:stripe="true"
:show-overflow-tooltip="true"
@row-click="handleProductDialogRowClick"
>
<el-table-column width="55" align="center">
<template #default="{ row }">
<el-radio v-model="selectedProductId" :label="row.id">&nbsp;</el-radio>
</template>
</el-table-column>
<el-table-column :label="t('ErpStock.Item.code')" prop="barCode" min-width="160" />
<el-table-column :label="productColumnLabel" prop="name" min-width="160" />
<el-table-column :label="t('ErpStock.Item.spec')" prop="standard" min-width="120" />
<el-table-column :label="t('ErpStock.Item.category')" prop="subCategoryName" min-width="120" />
<el-table-column :label="t('ErpStock.Item.unit')" prop="unitName" min-width="80" />
</el-table>
<div class="product-dialog-pagination">
<Pagination
:total="productDialogTotal"
v-model:page="productQueryParams.pageNo"
v-model:limit="productQueryParams.pageSize"
@pagination="getProductDialogList"
/>
</div>
<template #footer>
<el-button type="primary" :disabled="!selectedProductId" @click="confirmProductSelect">
{{ t('common.ok') }}
</el-button>
<el-button @click="productDialogVisible = false">{{ t('common.cancel') }}</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { ProductApi } from '@/api/erp/product/product'
import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
import { StockMoveApi } from '@/api/erp/stock/move'
import { erpCountInputFormatter, getSumValue } from '@/utils'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
const { t } = useI18n()
const props = defineProps<{
items?: any[]
categoryType?: number
disabled?: boolean
}>()
const formLoading = ref(false)
const formData = ref<any[]>([])
const sameAreaMessage = '调出库区和调入库区不能相同'
const formRules = reactive({
inId: [{ required: true, message: '调度编号不能为空', trigger: 'submit' }],
fromWarehouseId: [{ required: true, message: t('ErpStock.Move.item.validatorFromWarehouseRequired'), trigger: 'submit' }],
fromAreaId: [
{ required: true, message: t('ErpStock.Item.validatorAreaRequired'), trigger: 'submit' },
{ validator: validateDifferentArea, trigger: ['change', 'submit'] }
],
toWarehouseId: [{ required: true, message: t('ErpStock.Move.item.validatorToWarehouseRequired'), trigger: 'submit' }],
toAreaId: [
{ required: true, message: t('ErpStock.Item.validatorAreaRequired'), trigger: 'submit' },
{ validator: validateDifferentArea, trigger: ['change', 'submit'] }
],
productId: [{ required: true, message: t('ErpStock.Item.validatorProductRequired'), trigger: 'submit' }],
count: [{ required: true, message: t('ErpStock.Item.validatorCountRequired'), trigger: 'submit' }]
})
const formRef = ref()
const warehouseList = ref<WarehouseVO[]>([])
const productList = ref<any[]>([])
const currentSelectRow = ref<any>()
const productDialogVisible = ref(false)
const productDialogLoading = ref(false)
const productDialogList = ref<any[]>([])
const productDialogTotal = ref(0)
const selectedProductId = ref<number>()
const productQueryParams = reactive({
pageNo: 1,
pageSize: 10,
categoryType: undefined as number | undefined,
barCode: undefined as string | undefined,
name: undefined as string | undefined
})
const categoryTypeLabel = computed(() => {
const option = getIntDictOptions(DICT_TYPE.MATERIAL_CLASSIFICATION_TYPE).find(
(item) => Number(item.value) === Number(props.categoryType)
)
return option?.label || t('ErpStock.Item.productName')
})
const productColumnLabel = computed(() => categoryTypeLabel.value)
const productPlaceholder = computed(() => `请选择${productColumnLabel.value}`)
const productSelectTitle = computed(() => `选择${productColumnLabel.value}`)
watch(
() => props.items,
(val) => {
formData.value = val || []
ensureDefaultRow()
},
{ immediate: true }
)
watch(
() => props.categoryType,
async (val, oldVal) => {
await loadOptions()
if (oldVal !== undefined && val !== oldVal) {
formData.value.forEach(clearRowSelection)
}
ensureDefaultRow()
},
{ immediate: true }
)
function getSummaries(param: SummaryMethodProps) {
const { columns, data } = param
const sums: string[] = []
columns.forEach((column, index) => {
if (index === 0) {
sums[index] = t('common.total')
return
}
if (column.property === 'count') {
sums[index] = erpCountInputFormatter(getSumValue(data.map((item) => Number(item[column.property]))))
} else {
sums[index] = ''
}
})
return sums
}
function ensureDefaultRow() {
if (!props.disabled && formData.value.length === 0) {
handleAdd()
}
}
function handleAdd() {
formData.value.push({
id: undefined,
fromWarehouseId: undefined,
fromAreaId: undefined,
toWarehouseId: undefined,
toAreaId: undefined,
toAreaList: [],
productId: undefined,
productName: undefined,
productUnitName: undefined,
productBarCode: undefined,
stockCount: undefined,
productStockList: [],
count: 1,
remark: undefined
})
}
function handleDelete(index) {
formData.value.splice(index, 1)
}
function isSameMoveArea(row) {
return row.fromAreaId !== undefined && row.toAreaId !== undefined && String(row.fromAreaId) === String(row.toAreaId)
}
function validateDifferentArea(rule, _value, callback) {
const rowIndex = Number(String(rule.field || '').split('.')[0])
const row = formData.value[rowIndex]
if (row && isSameMoveArea(row)) {
callback(new Error(sameAreaMessage))
return
}
callback()
}
function warnIfSameArea(row) {
if (isSameMoveArea(row)) {
ElMessage.warning(sameAreaMessage)
}
}
function validateAreaFields(row) {
const rowIndex = formData.value.indexOf(row)
if (rowIndex < 0) return
formRef.value?.validateField([`${rowIndex}.fromAreaId`, `${rowIndex}.toAreaId`])
}
async function handleFromAreaChange(row) {
await setStockCount(row)
warnIfSameArea(row)
validateAreaFields(row)
}
function handleToAreaChange(row) {
warnIfSameArea(row)
validateAreaFields(row)
}
function onChangeFromWarehouse(_warehouseId, row) {
const firstStock = getFromAreaOptions(row)[0]
row.fromAreaId = firstStock ? getStockAreaId(firstStock) : undefined
setStockCount(row)
warnIfSameArea(row)
validateAreaFields(row)
}
async function onChangeToWarehouse(row) {
row.toAreaId = undefined
row.toAreaList = []
if (!row.toWarehouseId) return
const warehouse = await WarehouseApi.getWarehouse(row.toWarehouseId)
row.toAreaList = warehouse?.areaList || []
}
function handleProductInputClick(row) {
if (props.disabled) return
openProductSelectDialog(row)
}
function handleProductClear(row) {
if (props.disabled) return
clearRowSelection(row)
}
function getProductDisplayName(row) {
return row.productName || productList.value.find((item) => item.id === row.productId)?.name || ''
}
async function openProductSelectDialog(row) {
if (!props.categoryType) return
currentSelectRow.value = row
selectedProductId.value = row.productId
productQueryParams.pageNo = 1
productQueryParams.categoryType = props.categoryType
productQueryParams.barCode = undefined
productQueryParams.name = undefined
productDialogVisible.value = true
await getProductDialogList()
}
async function getProductDialogList() {
productDialogLoading.value = true
try {
productQueryParams.categoryType = props.categoryType
const data = await ProductApi.getProductPage(productQueryParams)
productDialogList.value = data?.list || []
productDialogTotal.value = data?.total || 0
} finally {
productDialogLoading.value = false
}
}
function handleProductDialogQuery() {
productQueryParams.pageNo = 1
getProductDialogList()
}
function handleProductDialogRowClick(row: any) {
selectedProductId.value = row.id
}
async function confirmProductSelect() {
if (props.disabled) return
const product = productDialogList.value.find((item) => item.id === selectedProductId.value)
if (!product || !currentSelectRow.value) return
fillRowByProduct(currentSelectRow.value, product)
await loadProductStockList(currentSelectRow.value)
productDialogVisible.value = false
}
function fillRowByProduct(row, product) {
row.productId = product.id
row.productName = product.name
row.productUnitName = product.unitName
row.productBarCode = product.barCode
row.fromWarehouseId = undefined
row.fromAreaId = undefined
row.toWarehouseId = undefined
row.toAreaId = undefined
row.toAreaList = []
row.stockCount = undefined
row.productStockList = []
}
function clearRowSelection(row) {
row.productId = undefined
row.productName = undefined
row.productUnitName = undefined
row.productBarCode = undefined
row.stockCount = undefined
row.productStockList = []
row.fromWarehouseId = undefined
row.fromAreaId = undefined
row.toWarehouseId = undefined
row.toAreaId = undefined
row.toAreaList = []
}
async function loadOptions() {
const params = props.categoryType ? { categoryType: props.categoryType } : undefined
formLoading.value = true
try {
const [products, warehouses] = await Promise.all([
ProductApi.getProductSimpleList(params),
WarehouseApi.getWarehouseSimpleList(params)
])
productList.value = products || []
warehouseList.value = warehouses || []
} finally {
formLoading.value = false
}
}
function normalizeProductStockList(data) {
if (Array.isArray(data)) return data
if (Array.isArray(data?.list)) return data.list
return []
}
async function loadProductStockList(row) {
if (!row.productId) {
row.productStockList = []
return
}
const data = await StockMoveApi.getProductStockList(row.productId)
row.productStockList = normalizeProductStockList(data)
await fillProductStockAreaInfo(row.productStockList)
const firstStock = row.productStockList[0]
row.fromWarehouseId = firstStock ? getStockWarehouseId(firstStock) : undefined
row.fromAreaId = firstStock ? getStockAreaId(firstStock) : undefined
row.stockCount = firstStock ? getStockCount(firstStock) : undefined
}
async function fillProductStockAreaInfo(stockList) {
const warehouseIds = Array.from(
new Set((stockList || []).map((item) => getStockWarehouseId(item)).filter(Boolean))
)
await Promise.all(
warehouseIds.map(async (warehouseId) => {
const warehouse = await WarehouseApi.getWarehouse(warehouseId)
const areaList = warehouse?.areaList || []
stockList
.filter((item) => Number(getStockWarehouseId(item)) === Number(warehouseId))
.forEach((item) => {
const area = areaList.find((areaItem) => Number(areaItem.id) === Number(getStockAreaId(item)))
if (!area) return
item.areaCode = area.areaCode
item.areaName = area.areaName
})
})
)
}
function getFromWarehouseOptions(row) {
const stockList = row.productStockList || []
const warehouseMap = new Map()
stockList.forEach((item) => {
const warehouseId = getStockWarehouseId(item)
if (warehouseId !== undefined && !warehouseMap.has(warehouseId)) {
warehouseMap.set(warehouseId, item)
}
})
return Array.from(warehouseMap.values())
}
function getFromAreaOptions(row) {
if (!row.fromWarehouseId) return []
return (row.productStockList || []).filter(
(item) => Number(getStockWarehouseId(item)) === Number(row.fromWarehouseId)
)
}
function getStockWarehouseId(stock) {
return stock.warehouseId
}
function getStockWarehouseName(stock) {
return stock.warehouseName || stock.name || stock.warehouseId
}
function getStockAreaId(stock) {
return stock.areaId
}
function getStockCount(stock) {
return stock.stockCount ?? stock.count
}
function getToAreaOptions(row) {
if (Array.isArray(row.toAreaList) && row.toAreaList.length > 0) return row.toAreaList
return warehouseList.value.find((item) => Number(item.id) === Number(row.toWarehouseId))?.areaList || []
}
function getAreaLabel(item) {
const areaCode = item.areaCode || item.code
const areaName = item.areaName || item.name
return areaCode ? `${areaCode} - ${areaName || ''}` : areaName
}
async function setStockCount(row) {
if (!row.productId || !row.fromWarehouseId || !row.fromAreaId) {
row.stockCount = undefined
return
}
if (!Array.isArray(row.productStockList) || row.productStockList.length === 0) {
await loadProductStockList(row)
}
const stock = row.productStockList.find(
(item) =>
Number(getStockWarehouseId(item)) === Number(row.fromWarehouseId) &&
Number(getStockAreaId(item)) === Number(row.fromAreaId)
)
row.stockCount = stock ? getStockCount(stock) : 0
}
function validate() {
return formRef.value.validate()
}
defineExpose({ validate })
</script>
<style scoped>
.stock-move-product-dialog :deep(.el-dialog__body) {
padding-bottom: 8px;
}
.stock-move-product-dialog :deep(.el-dialog__footer) {
border-top: 1px solid var(--el-border-color-lighter);
padding-top: 16px;
}
.product-dialog-pagination {
display: flex;
justify-content: flex-end;
width: 100%;
margin-top: 16px;
padding-bottom: 12px;
overflow-x: auto;
}
.product-dialog-pagination :deep(.pagination-container) {
margin: 0;
}
</style>