feat:库存调拨模块修改

main
黄伟杰 1 month ago
parent 90093ac0c5
commit 5858aef52a

@ -1,39 +1,39 @@
import request from '@/config/axios' import request from '@/config/axios'
// ERP 库存调度单 VO
export interface StockMoveVO { export interface StockMoveVO {
id: number // 出库编号 id: number
no: string // 出库单号 no: string
outTime: Date // 出库时间 categoryType?: number
totalCount: number // 合计数量 outTime: Date
totalPrice: number // 合计金额,单位:元 totalCount: number
status: number // 状态 totalPrice: number
remark: string // 备注 status: number
auditUserId?: number
auditUserName?: string
remark: string
} }
// ERP 库存调度单 API
export const StockMoveApi = { export const StockMoveApi = {
// 查询库存调度单分页
getStockMovePage: async (params: any) => { getStockMovePage: async (params: any) => {
return await request.get({ url: `/erp/stock-move/page`, params }) return await request.get({ url: `/erp/stock-move/page`, params })
}, },
// 查询库存调度单详情
getStockMove: async (id: number) => { getStockMove: async (id: number) => {
return await request.get({ url: `/erp/stock-move/get?id=` + id }) 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) => { createStockMove: async (data: StockMoveVO) => {
return await request.post({ url: `/erp/stock-move/create`, data }) return await request.post({ url: `/erp/stock-move/create`, data })
}, },
// 修改库存调度单
updateStockMove: async (data: StockMoveVO) => { updateStockMove: async (data: StockMoveVO) => {
return await request.put({ url: `/erp/stock-move/update`, data }) return await request.put({ url: `/erp/stock-move/update`, data })
}, },
// 更新库存调度单的状态
updateStockMoveStatus: async (id: number, status: number) => { updateStockMoveStatus: async (id: number, status: number) => {
return await request.put({ return await request.put({
url: `/erp/stock-move/update-status`, 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[]) => { deleteStockMove: async (ids: number[]) => {
return await request.delete({ return await request.delete({
url: `/erp/stock-move/delete`, url: `/erp/stock-move/delete`,
@ -54,8 +61,7 @@ export const StockMoveApi = {
}) })
}, },
// 导出库存调度单 Excel
exportStockMove: async (params) => { exportStockMove: async (params) => {
return await request.download({ url: `/erp/stock-move/export-excel`, 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?', confirmReverseApprove: 'Are you sure to reverse approve this transfer order?',
approveSuccess: 'Approve success', approveSuccess: 'Approve success',
reverseApproveSuccess: 'Reverse 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', exportName: 'Stock Transfer Order.xls',
list: 'Transfer Product List', list: 'Transfer Product List',
addItem: 'Add Transfer Product', addItem: 'Add Transfer Product',

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

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

@ -10,6 +10,20 @@
> >
<el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px"> <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="序号" 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"> <el-table-column label="调出仓库" min-width="125">
<template #default="{ row, $index }"> <template #default="{ row, $index }">
<el-form-item <el-form-item
@ -21,57 +35,36 @@
v-model="row.fromWarehouseId" v-model="row.fromWarehouseId"
clearable clearable
filterable filterable
:disabled="!row.productId"
placeholder="请选择调出仓库" placeholder="请选择调出仓库"
@change="onChangeWarehouse($event, row)" @change="onChangeFromWarehouse($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="请选择调入仓库"
> >
<el-option <el-option
v-for="item in warehouseList" v-for="item in getFromWarehouseOptions(row)"
:key="item.id" :key="getStockWarehouseId(item)"
:label="item.name" :label="getStockWarehouseName(item)"
:value="item.id" :value="getStockWarehouseId(item)"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="产品名称" min-width="180"> <el-table-column label="调出库区" min-width="125">
<template #default="{ row, $index }"> <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 <el-select
v-model="row.productId" v-model="row.fromAreaId"
clearable clearable
filterable filterable
@change="onChangeProduct($event, row)" :disabled="!row.fromWarehouseId"
placeholder="请选择产品" placeholder="请选择调出库区"
@change="handleFromAreaChange(row)"
> >
<el-option <el-option
v-for="item in productList" v-for="item in getFromAreaOptions(row)"
:key="item.id" :key="getStockAreaId(item)"
:label="item.name" :label="getAreaLabel(item)"
:value="item.id" :value="getStockAreaId(item)"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
@ -90,7 +83,7 @@
<el-input disabled v-model="row.stockCount" :formatter="erpCountInputFormatter" /> <el-input disabled v-model="row.stockCount" :formatter="erpCountInputFormatter" />
</el-form-item> </el-form-item>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="单位" min-width="80"> <el-table-column label="单位" min-width="80">
<template #default="{ row }"> <template #default="{ row }">
<el-form-item class="mb-0px!"> <el-form-item class="mb-0px!">
@ -98,6 +91,47 @@
</el-form-item> </el-form-item>
</template> </template>
</el-table-column> </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"> <el-table-column label="数量" prop="count" fixed="right" min-width="140">
<template #default="{ row, $index }"> <template #default="{ row, $index }">
<el-form-item :prop="`${$index}.count`" :rules="formRules.count" class="mb-0px!"> <el-form-item :prop="`${$index}.count`" :rules="formRules.count" class="mb-0px!">
@ -111,26 +145,6 @@
</el-form-item> </el-form-item>
</template> </template>
</el-table-column> </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"> <el-table-column label="备注" min-width="150">
<template #default="{ row, $index }"> <template #default="{ row, $index }">
<el-form-item :prop="`${$index}.remark`" class="mb-0px!"> <el-form-item :prop="`${$index}.remark`" class="mb-0px!">
@ -138,74 +152,160 @@
</el-form-item> </el-form-item>
</template> </template>
</el-table-column> </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 }"> <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> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-form> </el-form>
<el-row justify="center" class="mt-3" v-if="!disabled"> <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> </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> </template>
<script setup lang="ts"> <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 { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
import { StockApi } from '@/api/erp/stock/stock' import { StockMoveApi } from '@/api/erp/stock/move'
import { import { erpCountInputFormatter, getSumValue } from '@/utils'
erpCountInputFormatter, import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
erpPriceInputFormatter,
erpPriceMultiply,
getSumValue
} from '@/utils'
const { t } = useI18n() // const { t } = useI18n()
const props = defineProps<{ const props = defineProps<{
items: undefined items?: any[]
disabled: false categoryType?: number
disabled?: boolean
}>() }>()
const formLoading = ref(false) //
const formData = ref([]) const formLoading = ref(false)
const formData = ref<any[]>([])
const sameAreaMessage = '调出库区和调入库区不能相同'
const formRules = reactive({ const formRules = reactive({
inId: [{ required: true, message: '调度编号不能为空', trigger: 'blur' }], inId: [{ required: true, message: '调度编号不能为空', trigger: 'submit' }],
fromWarehouseId: [{ required: true, message: t('ErpStock.Move.item.validatorFromWarehouseRequired'), trigger: 'blur' }], fromWarehouseId: [{ required: true, message: t('ErpStock.Move.item.validatorFromWarehouseRequired'), trigger: 'submit' }],
toWarehouseId: [{ required: true, message: t('ErpStock.Move.item.validatorToWarehouseRequired'), trigger: 'blur' }], fromAreaId: [
productId: [{ required: true, message: t('ErpStock.Item.validatorProductRequired'), trigger: 'blur' }], { required: true, message: t('ErpStock.Item.validatorAreaRequired'), trigger: 'submit' },
count: [{ required: true, message: t('ErpStock.Item.validatorCountRequired'), trigger: 'blur' }] { 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( watch(
() => props.items, () => props.items,
async (val) => { (val) => {
formData.value = val formData.value = val || []
ensureDefaultRow()
}, },
{ immediate: true } { immediate: true }
) )
/** 监听合同产品变化,计算合同产品总价 */
watch( watch(
() => formData.value, () => props.categoryType,
(val) => { async (val, oldVal) => {
if (!val || val.length === 0) { await loadOptions()
return if (oldVal !== undefined && val !== oldVal) {
formData.value.forEach(clearRowSelection)
} }
// ensureDefaultRow()
val.forEach((item) => {
item.totalPrice = erpPriceMultiply(item.productPrice, item.count)
})
}, },
{ deep: true } { immediate: true }
) )
/** 合计 */ function getSummaries(param: SummaryMethodProps) {
const getSummaries = (param: SummaryMethodProps) => {
const { columns, data } = param const { columns, data } = param
const sums: string[] = [] const sums: string[] = []
columns.forEach((column, index) => { columns.forEach((column, index) => {
@ -213,82 +313,324 @@ const getSummaries = (param: SummaryMethodProps) => {
sums[index] = t('common.total') sums[index] = t('common.total')
return return
} }
if (['count', 'totalPrice'].includes(column.property)) { if (column.property === 'count') {
const sum = getSumValue(data.map((item) => Number(item[column.property]))) sums[index] = erpCountInputFormatter(getSumValue(data.map((item) => Number(item[column.property]))))
sums[index] =
column.property === 'count' ? erpCountInputFormatter(sum) : erpPriceInputFormatter(sum)
} else { } else {
sums[index] = '' sums[index] = ''
} }
}) })
return sums return sums
} }
/** 新增按钮操作 */ function ensureDefaultRow() {
const handleAdd = () => { if (!props.disabled && formData.value.length === 0) {
const row = { handleAdd()
}
}
function handleAdd() {
formData.value.push({
id: undefined, id: undefined,
fromWarehouseId: defaultWarehouse.value?.id, fromWarehouseId: undefined,
fromAreaId: undefined,
toWarehouseId: undefined, toWarehouseId: undefined,
toAreaId: undefined,
toAreaList: [],
productId: undefined, productId: undefined,
productUnitName: undefined, // productName: undefined,
productBarCode: undefined, // productUnitName: undefined,
productPrice: undefined, productBarCode: undefined,
stockCount: undefined, stockCount: undefined,
productStockList: [],
count: 1, count: 1,
totalPrice: undefined,
remark: undefined remark: undefined
} })
formData.value.push(row)
} }
/** 删除按钮操作 */ function handleDelete(index) {
const handleDelete = (index) => {
formData.value.splice(index, 1) formData.value.splice(index, 1)
} }
/** 处理仓库变更 */ function isSameMoveArea(row) {
const onChangeWarehouse = (warehouseId, row) => { return row.fromAreaId !== undefined && row.toAreaId !== undefined && String(row.fromAreaId) === String(row.toAreaId)
// }
setStockCount(row)
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) {
const onChangeProduct = (productId, row) => { if (isSameMoveArea(row)) {
const product = productList.value.find((item) => item.id === productId) ElMessage.warning(sameAreaMessage)
if (product) {
row.productUnitName = product.unitName
row.productBarCode = product.barCode
row.productPrice = product.minPrice
} }
// }
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) 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 setStockCount = async (row) => { const params = props.categoryType ? { categoryType: props.categoryType } : undefined
if (!row.productId || !row.fromWarehouseId) { 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 return
} }
const stock = await StockApi.getStock2(row.productId, row.fromWarehouseId) const data = await StockMoveApi.getProductStockList(row.productId)
row.stockCount = stock ? stock.count : 0 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 validate = () => { const warehouseIds = Array.from(
return formRef.value.validate() 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 })
/** 初始化 */ function getFromWarehouseOptions(row) {
onMounted(async () => { const stockList = row.productStockList || []
productList.value = await ProductApi.getProductSimpleList() const warehouseMap = new Map()
warehouseList.value = await WarehouseApi.getWarehouseSimpleList() stockList.forEach((item) => {
defaultWarehouse.value = warehouseList.value.find((item) => item.defaultStatus) const warehouseId = getStockWarehouseId(item)
// if (warehouseId !== undefined && !warehouseMap.has(warehouseId)) {
if (formData.value.length === 0) { warehouseMap.set(warehouseId, item)
handleAdd() }
})
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> </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" /> <dict-tag :type="DICT_TYPE.ERP_AUDIT_STATUS" :value="scope.row.status" />
</template> </template>
</el-table-column> </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"> <template #default="scope">
<el-button <el-button
link link
@ -182,33 +182,43 @@
type="primary" type="primary"
@click="openForm('update', scope.row.id)" @click="openForm('update', scope.row.id)"
v-hasPermi="['erp:stock-move:update']" v-hasPermi="['erp:stock-move:update']"
:disabled="scope.row.status === 20" :disabled="Number(scope.row.status) === 20"
> >
{{ t('action.edit') }} {{ t('action.edit') }}
</el-button> </el-button>
<el-button <el-button
v-if="[0, 1].includes(Number(scope.row.status))"
link link
type="primary" type="primary"
@click="handleUpdateStatus(scope.row.id, 20)" @click="openSubmitDialog(scope.row)"
v-hasPermi="['erp:stock-move:update-status']" 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>
<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 link
type="danger" type="danger"
@click="handleUpdateStatus(scope.row.id, 10)" @click="openAuditDialog(scope.row, 1)"
v-hasPermi="['erp:stock-move:update-status']" v-hasPermi="['erp:stock-move:update-status']"
v-else
> >
{{ t('ErpStock.Move.confirmReverseApprove').replace('确定', '').replace('该调拨单吗?', '') }} {{ t('ErpStock.Move.auditReject') }}
</el-button> </el-button>
<el-button <el-button
link link
type="danger" type="danger"
@click="handleDelete([scope.row.id])" @click="handleDelete([scope.row.id])"
v-hasPermi="['erp:stock-move:delete']" v-hasPermi="['erp:stock-move:delete']"
:disabled="Number(scope.row.status) === 20"
> >
{{ t('action.del') }} {{ t('action.del') }}
</el-button> </el-button>
@ -224,6 +234,59 @@
/> />
</ContentWrap> </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" /> <StockMoveForm ref="formRef" @success="getList" />
</template> </template>
@ -265,6 +328,27 @@ const exportLoading = ref(false) // 导出的加载中
const productList = ref<ProductVO[]>([]) // const productList = ref<ProductVO[]>([]) //
const warehouseList = ref<WarehouseVO[]>([]) // const warehouseList = ref<WarehouseVO[]>([]) //
const userList = ref<UserVO[]>([]) // 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 () => { const getList = async () => {
@ -310,17 +394,58 @@ const handleDelete = async (ids: number[]) => {
} catch {} } catch {}
} }
/** 审批/反审批操作 */ const openSubmitDialog = (row: StockMoveVO) => {
const handleUpdateStatus = async (id: number, status: number) => { 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 { try {
// await StockMoveApi.submitStockMove({
await message.confirm(status === 20 ? t('ErpStock.Move.confirmApprove') : t('ErpStock.Move.confirmReverseApprove')) id: submitForm.id,
// auditUserId: submitForm.auditUserId,
await StockMoveApi.updateStockMoveStatus(id, status) remark: submitForm.remark
message.success(status === 20 ? t('ErpStock.Move.approveSuccess') : t('ErpStock.Move.reverseApproveSuccess')) })
// message.success(t('ErpStock.Move.submitSuccess'))
submitDialogVisible.value = false
await getList() 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