|
|
<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 :model-value="getRowUnitName(row)" />
|
|
|
</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 getToWarehouseOptions(row)"
|
|
|
: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%"
|
|
|
@change="handleCountChange(row)"
|
|
|
/>
|
|
|
</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="getProductOptionRowKey"
|
|
|
: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="getProductOptionId(row)"> </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.Stock.stockDisplay')"
|
|
|
prop="stockDisplay"
|
|
|
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 { StockApi } from '@/api/erp/stock/stock'
|
|
|
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' },
|
|
|
{ validator: validateStockMoveCount, trigger: ['change', '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 | string>()
|
|
|
const productQueryParams = reactive({
|
|
|
pageNo: 1,
|
|
|
pageSize: 10,
|
|
|
categoryType: undefined as number | undefined,
|
|
|
stockNotZero: true,
|
|
|
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}`)
|
|
|
const getRowUnitName = (row) => row.productUnitName || '-'
|
|
|
|
|
|
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,
|
|
|
purchaseUnitName: undefined,
|
|
|
unitName: 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 getNumberValue(value) {
|
|
|
if (value === undefined || value === null || value === '') return undefined
|
|
|
const numberValue = Number(String(value).replace(/,/g, ''))
|
|
|
return Number.isFinite(numberValue) ? numberValue : undefined
|
|
|
}
|
|
|
|
|
|
function getCountExceededMessage(row) {
|
|
|
const stockCount = getNumberValue(row.stockCount)
|
|
|
return stockCount === undefined
|
|
|
? '调拨数量不能大于库存'
|
|
|
: `调拨数量不能大于库存(当前库存:${erpCountInputFormatter(stockCount)})`
|
|
|
}
|
|
|
|
|
|
function isCountExceeded(row) {
|
|
|
const count = getNumberValue(row.count)
|
|
|
const stockCount = getNumberValue(row.stockCount)
|
|
|
return count !== undefined && stockCount !== undefined && count > stockCount
|
|
|
}
|
|
|
|
|
|
function validateStockMoveCount(rule, _value, callback) {
|
|
|
const rowIndex = Number(String(rule.field || '').split('.')[0])
|
|
|
const row = formData.value[rowIndex]
|
|
|
if (row && isCountExceeded(row)) {
|
|
|
callback(new Error(getCountExceededMessage(row)))
|
|
|
return
|
|
|
}
|
|
|
callback()
|
|
|
}
|
|
|
|
|
|
function warnIfCountExceeded(row) {
|
|
|
if (isCountExceeded(row)) {
|
|
|
ElMessage.warning(getCountExceededMessage(row))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function validateCountField(row) {
|
|
|
const rowIndex = formData.value.indexOf(row)
|
|
|
if (rowIndex < 0) return
|
|
|
formRef.value?.validateField(`${rowIndex}.count`).catch(() => undefined)
|
|
|
}
|
|
|
|
|
|
function handleCountChange(row) {
|
|
|
warnIfCountExceeded(row)
|
|
|
validateCountField(row)
|
|
|
}
|
|
|
|
|
|
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`])
|
|
|
.catch(() => undefined)
|
|
|
}
|
|
|
|
|
|
async function handleFromAreaChange(row) {
|
|
|
await setStockCount(row)
|
|
|
warnIfSameArea(row)
|
|
|
validateAreaFields(row)
|
|
|
warnIfCountExceeded(row)
|
|
|
validateCountField(row)
|
|
|
}
|
|
|
|
|
|
function handleToAreaChange(row) {
|
|
|
warnIfSameArea(row)
|
|
|
validateAreaFields(row)
|
|
|
}
|
|
|
|
|
|
async function onChangeFromWarehouse(_warehouseId, row) {
|
|
|
const firstStock = getFromAreaOptions(row)[0]
|
|
|
row.fromAreaId = firstStock ? getStockAreaId(firstStock) : undefined
|
|
|
await setStockCount(row)
|
|
|
warnIfSameArea(row)
|
|
|
validateAreaFields(row)
|
|
|
warnIfCountExceeded(row)
|
|
|
validateCountField(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
|
|
|
productQueryParams.stockNotZero = true
|
|
|
const data = await StockApi.getStockPage(productQueryParams)
|
|
|
productDialogList.value = normalizeStockProductOptions(data?.list || [])
|
|
|
productDialogTotal.value = data?.total || 0
|
|
|
} finally {
|
|
|
productDialogLoading.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function handleProductDialogQuery() {
|
|
|
productQueryParams.pageNo = 1
|
|
|
getProductDialogList()
|
|
|
}
|
|
|
|
|
|
function handleProductDialogRowClick(row: any) {
|
|
|
selectedProductId.value = getProductOptionId(row)
|
|
|
}
|
|
|
|
|
|
async function confirmProductSelect() {
|
|
|
if (props.disabled) return
|
|
|
const product = productDialogList.value.find(
|
|
|
(item) => String(getProductOptionId(item)) === String(selectedProductId.value)
|
|
|
)
|
|
|
if (!product || !currentSelectRow.value) return
|
|
|
fillRowByProduct(currentSelectRow.value, product)
|
|
|
await loadProductStockList(currentSelectRow.value)
|
|
|
productDialogVisible.value = false
|
|
|
}
|
|
|
|
|
|
function fillRowByProduct(row, product) {
|
|
|
row.productId = getProductOptionId(product)
|
|
|
row.productName = product.name || product.productName
|
|
|
row.productUnitName = product.unitName || product.purchaseUnitName
|
|
|
row.purchaseUnitName = product.purchaseUnitName
|
|
|
row.unitName = product.unitName || product.purchaseUnitName
|
|
|
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.purchaseUnitName = undefined
|
|
|
row.unitName = 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 getProductOptionId(item) {
|
|
|
return item?.productId ?? item?.id
|
|
|
}
|
|
|
|
|
|
function getProductOptionRowKey(row) {
|
|
|
return [getProductOptionId(row), row.warehouseId, row.areaId, row.stockId || row.id]
|
|
|
.filter((item) => item !== undefined && item !== null && item !== '')
|
|
|
.join('-')
|
|
|
}
|
|
|
|
|
|
function normalizeStockProductOptions(list) {
|
|
|
const map = new Map()
|
|
|
;(list || []).forEach((item) => {
|
|
|
const productId = getProductOptionId(item)
|
|
|
if (!productId || map.has(String(productId))) return
|
|
|
map.set(String(productId), {
|
|
|
...item,
|
|
|
id: productId,
|
|
|
productId,
|
|
|
name: item.name || item.productName,
|
|
|
unitName: item.unitName || item.purchaseUnitName,
|
|
|
subCategoryName: item.subCategoryName || item.categoryName,
|
|
|
stockDisplay: item.stockDisplay || item.totalBaseCount || item.count || item.stockCount
|
|
|
})
|
|
|
})
|
|
|
return Array.from(map.values())
|
|
|
}
|
|
|
|
|
|
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
|
|
|
warnIfCountExceeded(row)
|
|
|
validateCountField(row)
|
|
|
}
|
|
|
|
|
|
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) => String(getStockWarehouseId(item)) === String(warehouseId))
|
|
|
.forEach((item) => {
|
|
|
const area = areaList.find(
|
|
|
(areaItem) => String(areaItem.id) === String(getStockAreaId(item))
|
|
|
)
|
|
|
if (!area) return
|
|
|
item.areaCode = area.areaCode
|
|
|
item.areaName = area.areaName
|
|
|
})
|
|
|
})
|
|
|
)
|
|
|
}
|
|
|
|
|
|
function appendOptionIfMissing(options, id, option, getId) {
|
|
|
if (id === undefined || id === null || !option) return options
|
|
|
const exists = options.some((item) => String(getId(item)) === String(id))
|
|
|
return exists ? options : [option, ...options]
|
|
|
}
|
|
|
|
|
|
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 appendOptionIfMissing(
|
|
|
Array.from(warehouseMap.values()),
|
|
|
row.fromWarehouseId,
|
|
|
row.fromWarehouseName
|
|
|
? { warehouseId: row.fromWarehouseId, warehouseName: row.fromWarehouseName }
|
|
|
: undefined,
|
|
|
getStockWarehouseId
|
|
|
)
|
|
|
}
|
|
|
|
|
|
function getFromAreaOptions(row) {
|
|
|
if (!row.fromWarehouseId) return []
|
|
|
const areaOptions = (row.productStockList || []).filter(
|
|
|
(item) => String(getStockWarehouseId(item)) === String(row.fromWarehouseId)
|
|
|
)
|
|
|
return appendOptionIfMissing(
|
|
|
areaOptions,
|
|
|
row.fromAreaId,
|
|
|
row.fromAreaName ? { areaId: row.fromAreaId, areaName: row.fromAreaName } : undefined,
|
|
|
getStockAreaId
|
|
|
)
|
|
|
}
|
|
|
|
|
|
function getStockWarehouseId(stock) {
|
|
|
return stock.warehouseId ?? stock.fromWarehouseId ?? stock.id
|
|
|
}
|
|
|
|
|
|
function getStockWarehouseName(stock) {
|
|
|
return stock.warehouseName || stock.fromWarehouseName || stock.name || getStockWarehouseId(stock)
|
|
|
}
|
|
|
|
|
|
function getStockAreaId(stock) {
|
|
|
return stock.areaId ?? stock.fromAreaId ?? stock.id
|
|
|
}
|
|
|
|
|
|
function getStockCount(stock) {
|
|
|
return stock.stockCount ?? stock.count
|
|
|
}
|
|
|
|
|
|
function getToWarehouseOptions(row) {
|
|
|
return appendOptionIfMissing(
|
|
|
warehouseList.value,
|
|
|
row.toWarehouseId,
|
|
|
row.toWarehouseName ? { id: row.toWarehouseId, name: row.toWarehouseName } : undefined,
|
|
|
(item) => item.id
|
|
|
)
|
|
|
}
|
|
|
|
|
|
function getToAreaOptions(row) {
|
|
|
const areaOptions =
|
|
|
Array.isArray(row.toAreaList) && row.toAreaList.length > 0
|
|
|
? row.toAreaList
|
|
|
: warehouseList.value.find((item) => String(item.id) === String(row.toWarehouseId))
|
|
|
?.areaList || []
|
|
|
return appendOptionIfMissing(
|
|
|
areaOptions,
|
|
|
row.toAreaId,
|
|
|
row.toAreaName ? { id: row.toAreaId, areaName: row.toAreaName } : undefined,
|
|
|
(item) => item.id
|
|
|
)
|
|
|
}
|
|
|
|
|
|
function getAreaLabel(item) {
|
|
|
const areaCode = item.areaCode || item.code
|
|
|
const areaName = item.areaName || item.fromAreaName || item.toAreaName || 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) =>
|
|
|
String(getStockWarehouseId(item)) === String(row.fromWarehouseId) &&
|
|
|
String(getStockAreaId(item)) === String(row.fromAreaId)
|
|
|
)
|
|
|
row.stockCount = stock ? getStockCount(stock) : 0
|
|
|
}
|
|
|
|
|
|
function validate() {
|
|
|
const exceededRow = formData.value.find(isCountExceeded)
|
|
|
if (exceededRow) {
|
|
|
ElMessage.warning(getCountExceededMessage(exceededRow))
|
|
|
}
|
|
|
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>
|