feat:出库单修改功能

This commit is contained in:
zhouz
2026-07-24 10:35:50 +08:00
parent 8a9d488ac2
commit ac19f00697
14 changed files with 257 additions and 100 deletions

View File

@@ -27,6 +27,7 @@ export namespace WmsIostorInvApi {
creator?: string; // 创建者标识
creatorName?: string; // 创建者名称
createTime?: Dayjs | string; // 创建时间
details?: OutboundDetail[]; // 出入库单明细
disOptid: string; // 分配人
disTime?: Dayjs | string; // 分配时间
confirmOptid: string; // 确认人
@@ -64,12 +65,18 @@ export namespace WmsIostorInvApi {
/** 出库单新增请求 */
export interface OutboundCreateReq {
billType: string; // 单据类型
billStatus: string; // 单据状态
storId: string; // 仓库标识
bizDate: number; // 业务日期(毫秒时间戳)
remark?: string; // 备注
details: OutboundDetail[]; // 出库单明细
}
/** 出库单修改请求 */
export interface OutboundUpdateReq extends OutboundCreateReq {
iostorinvId: string;
}
/** 可用库存 */
export interface AvailableInventory {
groupId: number | string; // 组盘记录标识
@@ -119,6 +126,13 @@ export function getIostorInv(id: string) {
);
}
/** 查询出入库单全部明细 */
export function getIostorInvDetails(iostorinvId: string) {
return requestClient.get<WmsIostorInvApi.OutboundDisplayDetail[]>(
`/wms/iostor-inv/details?iostorinvId=${encodeURIComponent(iostorinvId)}`,
);
}
/** 新增出入库单主表 */
export function createIostorInv(data: WmsIostorInvApi.IostorInv) {
return requestClient.post('/wms/iostor-inv/create', data);
@@ -129,6 +143,11 @@ export function updateIostorInv(data: WmsIostorInvApi.IostorInv) {
return requestClient.put('/wms/iostor-inv/update', data);
}
/** 修改出库单主表及明细 */
export function updateOutbound(data: WmsIostorInvApi.OutboundUpdateReq) {
return requestClient.put('/wms/iostor-inv/update', data);
}
/** 删除出入库单主表 */
export function deleteIostorInv(id: number) {
return requestClient.delete(`/wms/iostor-inv/delete?id=${id}`);

View File

@@ -9,37 +9,37 @@ import { Button, Input, InputNumber, message, Space, Table } from 'antdv-next';
import dayjs from 'dayjs';
import { useVbenForm } from '#/adapter/form';
import { getSimpleDictDataList } from '#/api/system/dict/data';
import { getBsrealStorAttrSimpleList } from '#/api/wms/bsrealstorattr';
import {
createOutbound,
getIostorInv,
updateIostorInv,
getIostorInvDetails,
updateOutbound,
} from '#/api/wms/iostorinv';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
import { summarizeDetails } from './detail-summary';
import InventorySelectComponent from './inventory-select.vue';
import ManualDetailComponent from './manual-detail.vue';
import {
buildOutboundPayload,
filterOutboundBillTypeOptions,
mergeOutboundInventory,
type OutboundFormDetail,
shouldApplyEditResult,
shouldClearDetailsForWarehouseChange,
} from './outbound-form';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
const emit = defineEmits(['success']);
const formData = ref<WmsIostorInvApi.IostorInv>();
const details = ref<OutboundFormDetail[]>([]);
const currentStorId = ref<string>();
let manualRowSequence = 0;
let billTypeRequest: null | ReturnType<typeof getSimpleDictDataList> = null;
let mainModalOpen = false;
let mainOpenToken = 0;
let activeEditId: string | undefined;
let suppressWarehouseChange = false;
const isEdit = computed(() => Boolean(formData.value?.iostorinvId));
const getTitle = computed(() =>
@@ -48,17 +48,6 @@ const getTitle = computed(() =>
: '出库单新增',
);
const [LegacyForm, legacyFormApi] = useVbenForm({
commonConfig: {
componentProps: { class: 'w-full' },
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
});
async function handleWarehouseChange(nextStorId?: string) {
const shouldClear = shouldClearDetailsForWarehouseChange(
currentStorId.value,
@@ -78,7 +67,7 @@ const [OutboundForm, outboundFormApi] = useVbenForm({
labelWidth: 88,
},
handleValuesChange: (values) => {
if (Object.hasOwn(values, 'storId')) {
if (!suppressWarehouseChange && Object.hasOwn(values, 'storId')) {
void handleWarehouseChange(values.storId);
}
},
@@ -108,7 +97,7 @@ const [OutboundForm, outboundFormApi] = useVbenForm({
component: 'Select',
componentProps: {
allowClear: true,
options: [],
options: getDictOptions(DICT_TYPE.WMS_OUT_BILL_TYPE),
placeholder: '请选择业务类型',
},
fieldName: 'billType',
@@ -116,11 +105,15 @@ const [OutboundForm, outboundFormApi] = useVbenForm({
rules: 'required',
},
{
component: 'Input',
componentProps: { disabled: true },
defaultValue: '生成',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.WMS_IO_BILL_STATUS),
placeholder: '请选择单据状态',
},
fieldName: 'billStatus',
label: '单据状态',
rules: 'required',
},
{
component: 'InputNumber',
@@ -164,28 +157,6 @@ const [ManualDetail, manualDetailApi] = useVbenModal({
destroyOnClose: true,
});
async function loadBillTypeOptions() {
billTypeRequest ??= getSimpleDictDataList();
const request = billTypeRequest;
try {
const dictItems = await request;
outboundFormApi.updateSchema([
{
componentProps: {
allowClear: true,
options: filterOutboundBillTypeOptions(dictItems),
placeholder: '请选择业务类型',
},
fieldName: 'billType',
},
]);
} catch {
message.error('出库业务类型加载失败,请稍后重试');
} finally {
if (billTypeRequest === request) billTypeRequest = null;
}
}
const columns = [
{ key: 'index', title: '序号', width: 60 },
{ dataIndex: 'materialCode', key: 'materialCode', title: '物料编码', width: 130 },
@@ -265,34 +236,23 @@ function validateDetails() {
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (isEdit.value) {
const { valid } = await legacyFormApi.validate();
if (!valid) return;
modalApi.lock();
try {
await updateIostorInv(
(await legacyFormApi.getValues()) as WmsIostorInvApi.IostorInv,
);
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
return;
}
const { valid } = await outboundFormApi.validate();
if (!valid || !validateDetails()) return;
const values = await outboundFormApi.getValues();
modalApi.lock();
try {
await createOutbound(
buildOutboundPayload(
{ ...values, bizDate: dayjs(values.bizDate).valueOf() },
details.value,
),
const payload = buildOutboundPayload(
{ ...values, bizDate: dayjs(values.bizDate).valueOf() },
details.value,
);
if (isEdit.value) {
await updateOutbound({
...payload,
iostorinvId: formData.value!.iostorinvId,
});
} else {
await createOutbound(payload);
}
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
@@ -318,17 +278,32 @@ const [Modal, modalApi] = useVbenModal({
formData.value = undefined;
details.value = [];
currentStorId.value = undefined;
await outboundFormApi.resetForm();
suppressWarehouseChange = true;
try {
await outboundFormApi.resetForm();
await outboundFormApi.updateSchema([
{
componentProps: {
allowClear: true,
disabled: false,
options: getDictOptions(DICT_TYPE.WMS_IO_BILL_STATUS),
placeholder: '请选择单据状态',
},
fieldName: 'billStatus',
},
]);
if (!mainModalOpen || mainOpenToken !== token || activeEditId) return;
await outboundFormApi.setValues({
billCode: '保存时自动生成',
billStatus: '10',
bizDate: dayjs(),
detailCount: 0,
totalWeight: '0.000',
});
} finally {
suppressWarehouseChange = false;
}
if (!mainModalOpen || mainOpenToken !== token || activeEditId) return;
await outboundFormApi.setValues({
billCode: '保存时自动生成',
billStatus: '生成',
bizDate: dayjs(),
detailCount: 0,
totalWeight: '0.000',
});
if (!mainModalOpen || mainOpenToken !== token || activeEditId) return;
await loadBillTypeOptions();
return;
}
const editId = data.iostorinvId;
@@ -336,7 +311,10 @@ const [Modal, modalApi] = useVbenModal({
formData.value = data;
modalApi.lock();
try {
const editResult = await getIostorInv(editId);
const [editResult, editDetails] = await Promise.all([
getIostorInv(editId),
getIostorInvDetails(editId),
]);
if (
!shouldApplyEditResult(
mainModalOpen,
@@ -349,7 +327,38 @@ const [Modal, modalApi] = useVbenModal({
return;
}
formData.value = editResult;
await legacyFormApi.setValues(editResult);
suppressWarehouseChange = true;
try {
await outboundFormApi.resetForm();
await outboundFormApi.updateSchema([
{
componentProps: {
allowClear: true,
disabled: true,
options: getDictOptions(DICT_TYPE.WMS_IO_BILL_STATUS),
placeholder: '请选择单据状态',
},
fieldName: 'billStatus',
},
]);
details.value = editDetails.map((detail, index) => ({
...detail,
rowKey: `edit-${editId}-${index}`,
}));
await outboundFormApi.setValues({
billCode: editResult.billCode,
billStatus: editResult.billStatus,
billType: editResult.billType,
bizDate: editResult.bizDate ? dayjs(editResult.bizDate) : undefined,
detailCount: details.value.length,
remark: editResult.remark,
storId: editResult.storId,
totalWeight: Number(editResult.totalWeight || 0).toFixed(3),
});
currentStorId.value = editResult.storId;
} finally {
suppressWarehouseChange = false;
}
} finally {
if (
shouldApplyEditResult(
@@ -369,8 +378,7 @@ const [Modal, modalApi] = useVbenModal({
<template>
<Modal class="w-[1400px] max-w-[96vw]" :title="getTitle">
<LegacyForm v-if="isEdit" class="mx-4" />
<div v-else class="px-4">
<div class="px-4">
<OutboundForm />
<div class="mb-3 mt-1 flex items-center justify-between">
<span class="text-base font-medium">出库明细</span>

View File

@@ -34,7 +34,13 @@ describe('buildOutboundPayload', () => {
it('将业务日期转换成毫秒,并剥离所有仅展示字段和汇总字段', () => {
const bizDate = new Date('2026-07-22T08:30:00+08:00');
const payload = buildOutboundPayload(
{ billType: 'SALE', bizDate, remark: '主备注', storId: 'S-1' },
{
billStatus: '10',
billType: 'SALE',
bizDate,
remark: '主备注',
storId: 'S-1',
},
[
{
groupId: 11,
@@ -79,7 +85,7 @@ describe('buildOutboundPayload', () => {
it('手工汇总行不附带库存引用字段', () => {
const payload = buildOutboundPayload(
{ billType: 'SALE', bizDate: 1, storId: 'S-1' },
{ billStatus: '10', billType: 'SALE', bizDate: 1, storId: 'S-1' },
[{ materialCode: 'M-1', planQty: 2 }],
);

View File

@@ -24,6 +24,7 @@ export function shouldApplyEditResult(
export interface OutboundFormValues {
billType?: string;
billStatus?: string;
bizDate?: number | string | { valueOf: () => number };
remark?: string;
storId?: string;
@@ -48,6 +49,7 @@ export function buildOutboundPayload(
): WmsIostorInvApi.OutboundCreateReq {
return {
billType: values.billType!,
billStatus: values.billStatus!,
bizDate: Number(values.bizDate?.valueOf()),
details: details.map((detail) => ({
groupId: detail.groupId,