feat:库存调拨模块修改

main
黄伟杰 2 weeks ago
parent 90093ac0c5
commit 5858aef52a

@ -1,39 +1,39 @@
import request from '@/config/axios'
// ERP 库存调度单 VO
export interface StockMoveVO {
id: number // 出库编号
no: string // 出库单号
outTime: Date // 出库时间
totalCount: number // 合计数量
totalPrice: number // 合计金额,单位:元
status: number // 状态
remark: string // 备注
id: number
no: string
categoryType?: number
outTime: Date
totalCount: number
totalPrice: number
status: number
auditUserId?: number
auditUserName?: string
remark: string
}
// ERP 库存调度单 API
export const StockMoveApi = {
// 查询库存调度单分页
getStockMovePage: async (params: any) => {
return await request.get({ url: `/erp/stock-move/page`, params })
},
// 查询库存调度单详情
getStockMove: async (id: number) => {
return await request.get({ url: `/erp/stock-move/get?id=` + id })
},
// 新增库存调度单
getProductStockList: async (productId: number) => {
return await request.get({ url: `/erp/stock-move/product-stock-list`, params: { productId } })
},
createStockMove: async (data: StockMoveVO) => {
return await request.post({ url: `/erp/stock-move/create`, data })
},
// 修改库存调度单
updateStockMove: async (data: StockMoveVO) => {
return await request.put({ url: `/erp/stock-move/update`, data })
},
// 更新库存调度单的状态
updateStockMoveStatus: async (id: number, status: number) => {
return await request.put({
url: `/erp/stock-move/update-status`,
@ -44,7 +44,14 @@ export const StockMoveApi = {
})
},
// 删除库存调度单
submitStockMove: async (data: { id: number; auditUserId?: number; remark?: string }) => {
return await request.put({ url: `/erp/stock-move/submit`, data })
},
auditStockMove: async (data: { id: number; status: number; remark?: string }) => {
return await request.put({ url: `/erp/stock-move/audit`, data })
},
deleteStockMove: async (ids: number[]) => {
return await request.delete({
url: `/erp/stock-move/delete`,
@ -54,8 +61,7 @@ export const StockMoveApi = {
})
},
// 导出库存调度单 Excel
exportStockMove: async (params) => {
return await request.download({ url: `/erp/stock-move/export-excel`, params })
}
}
}

@ -799,6 +799,19 @@ export default {
confirmReverseApprove: 'Are you sure to reverse approve this transfer order?',
approveSuccess: 'Approve success',
reverseApproveSuccess: 'Reverse approve success',
submit: 'Submit',
submitSuccess: 'Submitted successfully',
auditUserName: 'Approver',
placeholderAuditUser: 'Please select approver',
validatorAuditUserRequired: 'Approver is required',
submitRemark: 'Submit Remark',
placeholderSubmitRemark: 'Please enter submit remark',
auditApprove: 'Approve',
auditReject: 'Reject',
auditRemark: 'Audit Remark',
placeholderAuditRemark: 'Please enter audit remark',
auditApproveSuccess: 'Approved successfully',
auditRejectSuccess: 'Rejected successfully',
exportName: 'Stock Transfer Order.xls',
list: 'Transfer Product List',
addItem: 'Add Transfer Product',

@ -799,6 +799,19 @@ export default {
confirmReverseApprove: '确定反审批该调拨单吗?',
approveSuccess: '审批成功',
reverseApproveSuccess: '反审批成功',
submit: '提交',
submitSuccess: '提交成功',
auditUserName: '审批人',
placeholderAuditUser: '请选择审批人',
validatorAuditUserRequired: '审批人不能为空',
submitRemark: '提交备注',
placeholderSubmitRemark: '请输入提交备注',
auditApprove: '通过',
auditReject: '驳回',
auditRemark: '审批备注',
placeholderAuditRemark: '请输入审批备注',
auditApproveSuccess: '通过成功',
auditRejectSuccess: '驳回成功',
exportName: '库存调拨单.xls',
list: '调度产品清单',
addItem: '添加调度产品',

@ -25,6 +25,19 @@
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="t('ErpStock.Warehouse.categoryType')" prop="categoryType">
<el-radio-group v-model="formData.categoryType" @change="handleCategoryTypeChange">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.MATERIAL_CLASSIFICATION_TYPE)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item :label="t('ErpStock.Move.remark')" prop="remark">
<el-input
@ -46,7 +59,12 @@
<ContentWrap>
<el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
<el-tab-pane :label="t('ErpStock.Move.list')" name="item">
<StockMoveItemForm ref="itemFormRef" :items="formData.items" :disabled="disabled" />
<StockMoveItemForm
ref="itemFormRef"
:items="formData.items"
:category-type="formData.categoryType"
:disabled="disabled"
/>
</el-tab-pane>
</el-tabs>
</ContentWrap>
@ -60,6 +78,7 @@
</template>
<script setup lang="ts">
import { StockMoveApi, StockMoveVO } from '@/api/erp/stock/move'
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import StockMoveItemForm from './components/StockMoveItemForm.vue'
/** ERP 库存调度单表单 */
@ -76,12 +95,14 @@ const formData = ref({
id: undefined,
customerId: undefined,
moveTime: undefined,
categoryType: undefined,
remark: undefined,
fileUrl: '',
items: []
})
const formRules = reactive({
moveTime: [{ required: true, message: t('ErpStock.Move.validatorMoveTimeRequired'), trigger: 'blur' }]
moveTime: [{ required: true, message: t('ErpStock.Move.validatorMoveTimeRequired'), trigger: 'blur' }],
categoryType: [{ required: true, message: t('ErpStock.Warehouse.validatorCategoryTypeRequired'), trigger: 'change' }]
})
const disabled = computed(() => formType.value === 'detail')
const formRef = ref() // Ref
@ -100,7 +121,11 @@ const open = async (type: string, id?: number) => {
if (id) {
formLoading.value = true
try {
formData.value = await StockMoveApi.getStockMove(id)
const data = await StockMoveApi.getStockMove(id)
formData.value = {
...data,
categoryType: data.categoryType ?? getDefaultCategoryType()
}
} finally {
formLoading.value = false
}
@ -133,12 +158,19 @@ const submitForm = async () => {
}
}
const getDefaultCategoryType = () => getIntDictOptions(DICT_TYPE.MATERIAL_CLASSIFICATION_TYPE)[0]?.value
const handleCategoryTypeChange = () => {
formData.value.items = []
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
customerId: undefined,
moveTime: undefined,
categoryType: getDefaultCategoryType(),
remark: undefined,
fileUrl: undefined,
items: []

@ -10,6 +10,20 @@
>
<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
@ -21,57 +35,36 @@
v-model="row.fromWarehouseId"
clearable
filterable
:disabled="!row.productId"
placeholder="请选择调出仓库"
@change="onChangeWarehouse($event, 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}.toWarehouseId`"
:rules="formRules.toWarehouseId"
class="mb-0px!"
>
<el-select
v-model="row.toWarehouseId"
clearable
filterable
placeholder="请选择调入仓库"
@change="onChangeFromWarehouse($event, row)"
>
<el-option
v-for="item in warehouseList"
:key="item.id"
:label="item.name"
:value="item.id"
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="180">
<el-table-column label="调出库区" min-width="125">
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.productId`" :rules="formRules.productId" class="mb-0px!">
<el-form-item :prop="`${$index}.fromAreaId`" :rules="formRules.fromAreaId" class="mb-0px!">
<el-select
v-model="row.productId"
v-model="row.fromAreaId"
clearable
filterable
@change="onChangeProduct($event, row)"
placeholder="请选择产品"
:disabled="!row.fromWarehouseId"
placeholder="请选择调出库区"
@change="handleFromAreaChange(row)"
>
<el-option
v-for="item in productList"
:key="item.id"
:label="item.name"
:value="item.id"
v-for="item in getFromAreaOptions(row)"
:key="getStockAreaId(item)"
:label="getAreaLabel(item)"
:value="getStockAreaId(item)"
/>
</el-select>
</el-form-item>
@ -90,7 +83,7 @@
<el-input disabled v-model="row.stockCount" :formatter="erpCountInputFormatter" />
</el-form-item>
</template>
</el-table-column>
</el-table-column>
<el-table-column label="单位" min-width="80">
<template #default="{ row }">
<el-form-item class="mb-0px!">
@ -98,6 +91,47 @@
</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!">
@ -111,26 +145,6 @@
</el-form-item>
</template>
</el-table-column>
<el-table-column label="产品单价" fixed="right" min-width="120">
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.productPrice`" class="mb-0px!">
<el-input-number
v-model="row.productPrice"
controls-position="right"
:min="0.00"
:precision="2"
class="!w-100%"
/>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="合计金额" prop="totalPrice" fixed="right" min-width="100">
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.totalPrice`" class="mb-0px!">
<el-input disabled v-model="row.totalPrice" :formatter="erpPriceInputFormatter" />
</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!">
@ -138,74 +152,160 @@
</el-form-item>
</template>
</el-table-column>
<el-table-column align="center" fixed="right" label="操作" width="60">
<el-table-column v-if="!disabled" align="center" fixed="right" label="操作" width="60">
<template #default="{ $index }">
<el-button @click="handleDelete($index)" link></el-button>
<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-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, ProductVO } from '@/api/erp/product/product'
import { ProductApi } from '@/api/erp/product/product'
import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
import { StockApi } from '@/api/erp/stock/stock'
import {
erpCountInputFormatter,
erpPriceInputFormatter,
erpPriceMultiply,
getSumValue
} from '@/utils'
import { StockMoveApi } from '@/api/erp/stock/move'
import { erpCountInputFormatter, getSumValue } from '@/utils'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
const { t } = useI18n() //
const { t } = useI18n()
const props = defineProps<{
items: undefined
disabled: false
items?: any[]
categoryType?: number
disabled?: boolean
}>()
const formLoading = ref(false) //
const formData = ref([])
const formLoading = ref(false)
const formData = ref<any[]>([])
const sameAreaMessage = '调出库区和调入库区不能相同'
const formRules = reactive({
inId: [{ required: true, message: '调度编号不能为空', trigger: 'blur' }],
fromWarehouseId: [{ required: true, message: t('ErpStock.Move.item.validatorFromWarehouseRequired'), trigger: 'blur' }],
toWarehouseId: [{ required: true, message: t('ErpStock.Move.item.validatorToWarehouseRequired'), trigger: 'blur' }],
productId: [{ required: true, message: t('ErpStock.Item.validatorProductRequired'), trigger: 'blur' }],
count: [{ required: true, message: t('ErpStock.Item.validatorCountRequired'), trigger: 'blur' }]
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 formRef = ref([]) // Ref
const productList = ref<ProductVO[]>([]) //
const warehouseList = ref<WarehouseVO[]>([]) //
const defaultWarehouse = ref<WarehouseVO>(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,
async (val) => {
formData.value = val
(val) => {
formData.value = val || []
ensureDefaultRow()
},
{ immediate: true }
)
/** 监听合同产品变化,计算合同产品总价 */
watch(
() => formData.value,
(val) => {
if (!val || val.length === 0) {
return
() => props.categoryType,
async (val, oldVal) => {
await loadOptions()
if (oldVal !== undefined && val !== oldVal) {
formData.value.forEach(clearRowSelection)
}
//
val.forEach((item) => {
item.totalPrice = erpPriceMultiply(item.productPrice, item.count)
})
ensureDefaultRow()
},
{ deep: true }
{ immediate: true }
)
/** 合计 */
const getSummaries = (param: SummaryMethodProps) => {
function getSummaries(param: SummaryMethodProps) {
const { columns, data } = param
const sums: string[] = []
columns.forEach((column, index) => {
@ -213,82 +313,324 @@ const getSummaries = (param: SummaryMethodProps) => {
sums[index] = t('common.total')
return
}
if (['count', 'totalPrice'].includes(column.property)) {
const sum = getSumValue(data.map((item) => Number(item[column.property])))
sums[index] =
column.property === 'count' ? erpCountInputFormatter(sum) : erpPriceInputFormatter(sum)
if (column.property === 'count') {
sums[index] = erpCountInputFormatter(getSumValue(data.map((item) => Number(item[column.property]))))
} else {
sums[index] = ''
}
})
return sums
}
/** 新增按钮操作 */
const handleAdd = () => {
const row = {
function ensureDefaultRow() {
if (!props.disabled && formData.value.length === 0) {
handleAdd()
}
}
function handleAdd() {
formData.value.push({
id: undefined,
fromWarehouseId: defaultWarehouse.value?.id,
fromWarehouseId: undefined,
fromAreaId: undefined,
toWarehouseId: undefined,
toAreaId: undefined,
toAreaList: [],
productId: undefined,
productUnitName: undefined, //
productBarCode: undefined, //
productPrice: undefined,
productName: undefined,
productUnitName: undefined,
productBarCode: undefined,
stockCount: undefined,
productStockList: [],
count: 1,
totalPrice: undefined,
remark: undefined
}
formData.value.push(row)
})
}
/** 删除按钮操作 */
const handleDelete = (index) => {
function handleDelete(index) {
formData.value.splice(index, 1)
}
/** 处理仓库变更 */
const onChangeWarehouse = (warehouseId, row) => {
//
setStockCount(row)
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()
}
/** 处理产品变更 */
const onChangeProduct = (productId, row) => {
const product = productList.value.find((item) => item.id === productId)
if (product) {
row.productUnitName = product.unitName
row.productBarCode = product.barCode
row.productPrice = product.minPrice
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 = []
}
/** 加载库存 */
const setStockCount = async (row) => {
if (!row.productId || !row.fromWarehouseId) {
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 stock = await StockApi.getStock2(row.productId, row.fromWarehouseId)
row.stockCount = stock ? stock.count : 0
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
}
/** 表单校验 */
const validate = () => {
return formRef.value.validate()
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
})
})
)
}
defineExpose({ validate })
/** 初始化 */
onMounted(async () => {
productList.value = await ProductApi.getProductSimpleList()
warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
defaultWarehouse.value = warehouseList.value.find((item) => item.defaultStatus)
//
if (formData.value.length === 0) {
handleAdd()
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>

@ -168,7 +168,7 @@
<dict-tag :type="DICT_TYPE.ERP_AUDIT_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column :label="t('common.operate')" align="center" fixed="right">
<el-table-column :label="t('common.operate')" align="center" fixed="right" width="320">
<template #default="scope">
<el-button
link
@ -182,33 +182,43 @@
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['erp:stock-move:update']"
:disabled="scope.row.status === 20"
:disabled="Number(scope.row.status) === 20"
>
{{ t('action.edit') }}
</el-button>
<el-button
v-if="[0, 1].includes(Number(scope.row.status))"
link
type="primary"
@click="handleUpdateStatus(scope.row.id, 20)"
@click="openSubmitDialog(scope.row)"
v-hasPermi="['erp:stock-move:update-status']"
v-if="scope.row.status === 10"
>
{{ t('ErpStock.Move.confirmApprove').replace('确定', '').replace('该调拨单吗?', '') }}
{{ t('ErpStock.Move.submit') }}
</el-button>
<el-button
v-if="Number(scope.row.status) === 10"
link
type="primary"
@click="openAuditDialog(scope.row, 20)"
v-hasPermi="['erp:stock-move:update-status']"
>
{{ t('ErpStock.Move.auditApprove') }}
</el-button>
<el-button
v-if="Number(scope.row.status) === 10"
link
type="danger"
@click="handleUpdateStatus(scope.row.id, 10)"
@click="openAuditDialog(scope.row, 1)"
v-hasPermi="['erp:stock-move:update-status']"
v-else
>
{{ t('ErpStock.Move.confirmReverseApprove').replace('确定', '').replace('该调拨单吗?', '') }}
{{ t('ErpStock.Move.auditReject') }}
</el-button>
<el-button
link
type="danger"
@click="handleDelete([scope.row.id])"
v-hasPermi="['erp:stock-move:delete']"
:disabled="Number(scope.row.status) === 20"
>
{{ t('action.del') }}
</el-button>
@ -224,6 +234,59 @@
/>
</ContentWrap>
<Dialog :title="t('ErpStock.Move.submit')" v-model="submitDialogVisible" width="500">
<el-form ref="submitFormRef" :model="submitForm" :rules="submitRules" label-width="auto">
<el-form-item :label="t('ErpStock.Move.auditUserName')" prop="auditUserId">
<el-select
v-model="submitForm.auditUserId"
filterable
:placeholder="t('ErpStock.Move.placeholderAuditUser')"
class="!w-1/1"
>
<el-option
v-for="item in userList"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('ErpStock.Move.submitRemark')" prop="remark">
<el-input
v-model="submitForm.remark"
type="textarea"
:rows="3"
:placeholder="t('ErpStock.Move.placeholderSubmitRemark')"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" :disabled="submitLoading" @click="handleSubmit">
{{ t('common.ok') }}
</el-button>
<el-button @click="submitDialogVisible = false">{{ t('common.cancel') }}</el-button>
</template>
</Dialog>
<Dialog :title="auditDialogTitle" v-model="auditDialogVisible" width="500">
<el-form :model="auditForm" label-width="auto">
<el-form-item :label="t('ErpStock.Move.auditRemark')" prop="remark">
<el-input
v-model="auditForm.remark"
type="textarea"
:rows="3"
:placeholder="t('ErpStock.Move.placeholderAuditRemark')"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" :disabled="auditLoading" @click="handleAudit">
{{ t('common.ok') }}
</el-button>
<el-button @click="auditDialogVisible = false">{{ t('common.cancel') }}</el-button>
</template>
</Dialog>
<!-- 表单弹窗添加/修改 -->
<StockMoveForm ref="formRef" @success="getList" />
</template>
@ -265,6 +328,27 @@ const exportLoading = ref(false) // 导出的加载中
const productList = ref<ProductVO[]>([]) //
const warehouseList = ref<WarehouseVO[]>([]) //
const userList = ref<UserVO[]>([]) //
const submitFormRef = ref()
const submitDialogVisible = ref(false)
const submitLoading = ref(false)
const submitForm = reactive({
id: undefined as number | undefined,
auditUserId: undefined as number | undefined,
remark: undefined as string | undefined
})
const submitRules = {
auditUserId: [{ required: true, message: t('ErpStock.Move.validatorAuditUserRequired'), trigger: 'change' }]
}
const auditDialogVisible = ref(false)
const auditLoading = ref(false)
const auditForm = reactive({
id: undefined as number | undefined,
status: undefined as number | undefined,
remark: undefined as string | undefined
})
const auditDialogTitle = computed(() =>
auditForm.status === 20 ? t('ErpStock.Move.auditApprove') : t('ErpStock.Move.auditReject')
)
/** 查询列表 */
const getList = async () => {
@ -310,17 +394,58 @@ const handleDelete = async (ids: number[]) => {
} catch {}
}
/** 审批/反审批操作 */
const handleUpdateStatus = async (id: number, status: number) => {
const openSubmitDialog = (row: StockMoveVO) => {
submitForm.id = row.id
submitForm.auditUserId = row.auditUserId
submitForm.remark = undefined
submitDialogVisible.value = true
nextTick(() => submitFormRef.value?.clearValidate?.())
}
const handleSubmit = async () => {
await submitFormRef.value.validate()
if (!submitForm.id || !submitForm.auditUserId) return
submitLoading.value = true
try {
//
await message.confirm(status === 20 ? t('ErpStock.Move.confirmApprove') : t('ErpStock.Move.confirmReverseApprove'))
//
await StockMoveApi.updateStockMoveStatus(id, status)
message.success(status === 20 ? t('ErpStock.Move.approveSuccess') : t('ErpStock.Move.reverseApproveSuccess'))
//
await StockMoveApi.submitStockMove({
id: submitForm.id,
auditUserId: submitForm.auditUserId,
remark: submitForm.remark
})
message.success(t('ErpStock.Move.submitSuccess'))
submitDialogVisible.value = false
await getList()
} catch {}
} finally {
submitLoading.value = false
}
}
const openAuditDialog = (row: StockMoveVO, status: number) => {
auditForm.id = row.id
auditForm.status = status
auditForm.remark = undefined
auditDialogVisible.value = true
}
const handleAudit = async () => {
if (!auditForm.id || !auditForm.status) return
auditLoading.value = true
try {
await StockMoveApi.auditStockMove({
id: auditForm.id,
status: auditForm.status,
remark: auditForm.remark
})
message.success(
auditForm.status === 20
? t('ErpStock.Move.auditApproveSuccess')
: t('ErpStock.Move.auditRejectSuccess')
)
auditDialogVisible.value = false
await getList()
} finally {
auditLoading.value = false
}
}
/** 导出按钮操作 */

Loading…
Cancel
Save