admin-vue3/src/views/wms/order/movement/MovementOrderForm.vue

519 lines
15 KiB
Vue

<!-- WMS 移库单表单 -->
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="1280px">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="98px"
>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="移库单号" prop="no">
<el-input v-model="formData.no" maxlength="64" placeholder="请输入移库单号" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="来源仓库" prop="sourceWarehouseId">
<WarehouseSelect
v-model="formData.sourceWarehouseId"
@change="handleSourceWarehouseChange"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="目标仓库" prop="targetWarehouseId">
<WarehouseSelect
v-model="formData.targetWarehouseId"
@change="handleTargetWarehouseChange"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="单据日期" prop="orderTime">
<el-date-picker
v-model="formData.orderTime"
class="!w-1/1"
placeholder="请选择单据日期"
type="date"
value-format="x"
/>
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item label="备注" prop="remark">
<el-input
v-model="formData.remark"
maxlength="255"
placeholder="请输入备注"
:rows="3"
type="textarea"
/>
</el-form-item>
</el-col>
</el-row>
<div class="mb-12px flex items-center justify-between">
<span class="text-14px font-bold">移库明细</span>
<el-tooltip
content="请先选择来源仓库和目标仓库"
:disabled="canAddDetail"
placement="top"
>
<span>
<el-button
:disabled="!canAddDetail"
plain
type="primary"
@click="handleAddDetail"
>
<Icon class="mr-5px" icon="ep:plus" />
添加商品
</el-button>
</span>
</el-tooltip>
</div>
<el-table
:data="formData.details"
:summary-method="getDetailSummaries"
border
empty-text="暂无商品明细"
show-summary
>
<el-table-column label="商品信息" min-width="210">
<template #default="{ row }">
<div>{{ row.itemName || '-' }}</div>
<div v-if="row.itemCode" class="text-12px text-gray-500">
商品编号:{{ row.itemCode }}
</div>
</template>
</el-table-column>
<el-table-column label="规格信息" min-width="210">
<template #default="{ row }">
<div>{{ row.skuName || '-' }}</div>
<div v-if="row.skuCode" class="text-12px text-gray-500">
规格编号:{{ row.skuCode }}
</div>
</template>
</el-table-column>
<el-table-column align="right" label="可用库存" width="120">
<template #default="{ row }">{{ formatQuantity(row.availableQuantity) || '-' }}</template>
</el-table-column>
<el-table-column label="移库数量" prop="quantity" width="160">
<template #default="{ row }">
<el-input-number
v-model="row.quantity"
:controls="false"
:min="0"
:precision="QUANTITY_PRECISION"
class="!w-1/1"
placeholder="数量"
@change="handleDetailQuantityChange(row)"
/>
</template>
</el-table-column>
<el-table-column label="单价(元)" prop="price" width="160">
<template #default="{ row }">
<el-input-number
v-model="row.price"
:controls="false"
:min="0"
:precision="PRICE_PRECISION"
class="!w-1/1"
placeholder="单价"
@change="handleDetailPriceChange(row)"
/>
</template>
</el-table-column>
<el-table-column label="金额(元)" prop="totalPrice" width="160">
<template #default="{ row }">
<el-input-number
v-model="row.totalPrice"
:controls="false"
:min="0"
:precision="PRICE_PRECISION"
class="!w-1/1"
placeholder="金额"
@change="handleDetailTotalPriceChange(row)"
/>
</template>
</el-table-column>
<el-table-column align="center" label="操作" width="80">
<template #default="{ $index }">
<el-button link type="danger" @click="handleDeleteDetail($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<InventorySelect
ref="inventorySelectRef"
:warehouse-id="formData.sourceWarehouseId"
@change="handleSelectInventory"
/>
</el-form>
<template #footer>
<div class="flex items-center justify-between">
<div>
<el-button
v-if="isSavedPrepareOrder"
v-hasPermi="['wms:movement-order:complete']"
:disabled="formLoading"
type="success"
@click="handleComplete"
>
完成移库
</el-button>
<el-button
v-if="isSavedPrepareOrder"
v-hasPermi="['wms:movement-order:cancel']"
:disabled="formLoading"
type="danger"
@click="handleCancel"
>
作废
</el-button>
</div>
<div>
<el-button
v-if="isPrepareOrder"
:disabled="formLoading"
type="primary"
@click="submitForm"
>
保存
</el-button>
<el-button @click="dialogVisible = false"> </el-button>
</div>
</div>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { FormRules } from 'element-plus'
import { MovementOrderApi, MovementOrderVO } from '@/api/wms/order/movement'
import { MovementOrderDetailVO } from '@/api/wms/order/movement/detail'
import InventorySelect, {
InventorySelectRow
} from '@/views/wms/inventory/components/InventorySelect.vue'
import WarehouseSelect from '@/views/wms/md/warehouse/components/WarehouseSelect.vue'
import { OrderStatusEnum, OrderUpdateStatusList } from '@/views/wms/utils/constants'
import {
dividePrice,
formatQuantity,
formatSumPrice,
formatSumQuantity,
multiplyPrice,
PRICE_PRECISION,
QUANTITY_PRECISION
} from '@/views/wms/utils/format'
import { generateOrderNo } from '@/views/wms/utils/order'
/** WMS 移库单表单 */
defineOptions({ name: 'WmsMovementOrderForm' })
const { t } = useI18n()
const message = useMessage()
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref('')
const originalFormData = ref('')
const formData = ref<MovementOrderVO>({
id: undefined,
no: undefined,
orderTime: undefined,
status: OrderStatusEnum.PREPARE,
sourceWarehouseId: undefined,
targetWarehouseId: undefined,
remark: undefined,
details: []
})
const formRules = reactive<FormRules>({
no: [{ required: true, message: '移库单号不能为空', trigger: 'blur' }],
orderTime: [{ required: true, message: '单据日期不能为空', trigger: 'change' }],
sourceWarehouseId: [{ required: true, message: '来源仓库不能为空', trigger: 'change' }],
targetWarehouseId: [{ required: true, message: '目标仓库不能为空', trigger: 'change' }]
})
const formRef = ref()
const inventorySelectRef = ref()
const isPrepareOrder = computed(
() =>
!formData.value.id ||
(formData.value.status !== undefined && OrderUpdateStatusList.includes(formData.value.status))
)
const isSavedPrepareOrder = computed(
() =>
!!formData.value.id &&
formData.value.status !== undefined &&
OrderUpdateStatusList.includes(formData.value.status)
)
const canAddDetail = computed(
() => !!formData.value.sourceWarehouseId && !!formData.value.targetWarehouseId
)
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
if (id) {
formLoading.value = true
try {
const order = await MovementOrderApi.getMovementOrder(id)
formData.value = { ...order, details: normalizeDetails(order.details || []) }
} finally {
formLoading.value = false
}
}
originalFormData.value = JSON.stringify(buildSubmitData())
}
defineExpose({ open })
/** 构建移库明细 */
function buildDetail(inventory: InventorySelectRow): MovementOrderDetailVO {
return {
id: undefined,
itemId: inventory.itemId,
itemCode: inventory.itemCode,
itemName: inventory.itemName,
unit: inventory.unit,
skuId: inventory.skuId,
skuCode: inventory.skuCode,
skuName: inventory.skuName,
sourceWarehouseId: inventory.warehouseId,
sourceWarehouseName: inventory.warehouseName,
targetWarehouseId: formData.value.targetWarehouseId,
quantity: undefined,
availableQuantity: inventory.availableQuantity,
price: undefined,
totalPrice: undefined
}
}
function normalizeDetails(details: MovementOrderDetailVO[]) {
return details.map((detail) => ({
...detail,
totalPrice: detail.totalPrice ?? multiplyPrice(detail.quantity, detail.price)
}))
}
/** 打开库存选择弹窗 */
const handleAddDetail = () => {
if (!canAddDetail.value) {
return
}
inventorySelectRef.value?.open(getSelectedInventoryKeys())
}
/** 选择库存 */
const handleSelectInventory = (inventories: InventorySelectRow[]) => {
if (!inventories.length) {
return
}
formData.value.details = formData.value.details || []
inventories.forEach((inventory) => {
if (isInventorySelected(inventory)) {
return
}
formData.value.details!.push(buildDetail(inventory))
})
}
/** 判断库存是否已选择 */
function isInventorySelected(inventory: InventorySelectRow) {
return (formData.value.details || []).some((detail) => {
return detail.skuId === inventory.skuId && detail.sourceWarehouseId === inventory.warehouseId
})
}
/** 获得已选择的库存标识 */
function getSelectedInventoryKeys() {
return (formData.value.details || [])
.map((detail) =>
detail.skuId && detail.sourceWarehouseId
? `${detail.skuId}-${detail.sourceWarehouseId}`
: undefined
)
.filter((key): key is string => !!key)
}
const handleDeleteDetail = (index: number) => {
formData.value.details?.splice(index, 1)
}
const handleSourceWarehouseChange = () => {
formData.value.details = []
}
const handleTargetWarehouseChange = () => {
refreshTargetToDetails()
}
const refreshTargetToDetails = () => {
const details = formData.value.details || []
details.forEach((detail) => {
detail.targetWarehouseId = formData.value.targetWarehouseId
})
}
const handleDetailQuantityChange = (detail: MovementOrderDetailVO) => {
if (detail.price !== undefined && detail.price !== null) {
detail.totalPrice = multiplyPrice(detail.quantity, detail.price)
return
}
detail.price = dividePrice(detail.totalPrice, detail.quantity)
}
const handleDetailPriceChange = (detail: MovementOrderDetailVO) => {
detail.totalPrice = multiplyPrice(detail.quantity, detail.price)
}
const handleDetailTotalPriceChange = (detail: MovementOrderDetailVO) => {
detail.price = dividePrice(detail.totalPrice, detail.quantity)
}
/** 计算表格的合计行数据 */
function getDetailSummaries({
columns,
data
}: {
columns: any[]
data: MovementOrderDetailVO[]
}) {
return columns.map((column, index) => {
if (index === 0) {
return '合计'
}
if (column.property === 'quantity') {
return formatSumQuantity(data, (detail) => detail.quantity)
}
if (column.property === 'totalPrice') {
return formatSumPrice(data, (detail) => detail.totalPrice)
}
return ''
})
}
/** 校验明细 */
const validateDetails = (required: boolean) => {
if (formData.value.sourceWarehouseId === formData.value.targetWarehouseId) {
message.error('来源仓库和目标仓库不能相同')
return false
}
if (!formData.value.details?.length) {
if (required) {
message.error('至少包含一条移库明细')
return false
}
return true
}
for (let i = 0; i < formData.value.details.length; i++) {
const detail = formData.value.details[i]
if (!detail.quantity || detail.quantity <= 0) {
message.error(`${i + 1} 行明细移库数量必须大于 0`)
return false
}
if (detail.availableQuantity !== undefined && detail.quantity > detail.availableQuantity) {
message.error(`${i + 1} 行明细移库数量不能大于可用库存`)
return false
}
}
return true
}
/** 构建提交数据 */
const buildSubmitData = () => {
const {
totalQuantity: _totalQuantity,
totalPrice: _totalPrice,
details,
...order
} = formData.value
return {
...order,
details: details || []
} as MovementOrderVO
}
const emit = defineEmits(['success'])
const submitForm = async () => {
await formRef.value.validate()
if (!validateDetails(false)) {
return
}
formLoading.value = true
try {
const data = buildSubmitData()
if (formType.value === 'create') {
await MovementOrderApi.createMovementOrder(data)
message.success(t('common.createSuccess'))
} else {
await MovementOrderApi.updateMovementOrder(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
emit('success')
} finally {
formLoading.value = false
}
}
/** 完成移库:表单修改过则先保存,再完成 */
const handleComplete = async () => {
await formRef.value.validate()
if (!validateDetails(true)) {
return
}
try {
await message.confirm('确认完成移库?完成后将更新库存。')
formLoading.value = true
const data = buildSubmitData()
if (JSON.stringify(data) !== originalFormData.value) {
await MovementOrderApi.updateMovementOrder(data)
}
await MovementOrderApi.completeMovementOrder(formData.value.id!)
message.success('移库成功')
dialogVisible.value = false
emit('success')
} catch {
// 用户点击确认框取消按钮、或网络异常时跳过
} finally {
formLoading.value = false
}
}
/** 作废移库单 */
const handleCancel = async () => {
try {
await message.confirm('确认作废该移库单?作废后不可恢复。')
formLoading.value = true
await MovementOrderApi.cancelMovementOrder(formData.value.id!)
message.success('作废成功')
dialogVisible.value = false
emit('success')
} catch {
// 用户点击确认框取消按钮、或网络异常时跳过
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
no: generateOrderNo('YK'),
orderTime: undefined,
status: OrderStatusEnum.PREPARE,
sourceWarehouseId: undefined,
targetWarehouseId: undefined,
remark: undefined,
details: []
}
originalFormData.value = ''
nextTick(() => formRef.value?.clearValidate())
}
</script>