From 1fc806ea7f67b33738156dcfcb4e3f9173bab056 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Wed, 28 Oct 2026 14:45:10 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/procurementWorkflow.ts | 3 +++ .../components/ProcurementWorkflowPanel.vue | 22 ++++++++++--------- .../api/ownedBusiness.test.ts | 9 ++++++++ .../overseas-master-data/api/ownedBusiness.ts | 6 ++++- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/modules/overseas-business/api/procurementWorkflow.ts b/src/modules/overseas-business/api/procurementWorkflow.ts index b1922c0d..86877e18 100644 --- a/src/modules/overseas-business/api/procurementWorkflow.ts +++ b/src/modules/overseas-business/api/procurementWorkflow.ts @@ -14,3 +14,6 @@ export async function getProcurementWorkflow(orderNo: string, customerId: string export async function submitProcurementWorkflow(orderNo: string, customerId: string, action: string, payload: WorkflowCommand) { return unwrapApiResponse(await http.post, ApiEnvelope>(`${base}/${encodeURIComponent(orderNo)}/${action}`, payload, { params: { customerId } })) } +export async function editProcurementOutbound(orderNo: string, customerId: string, batchId: string, payload: WorkflowCommand) { + return unwrapApiResponse(await http.put, ApiEnvelope>(`${base}/${encodeURIComponent(orderNo)}/outbounds/${encodeURIComponent(batchId)}`, payload, { params: { customerId } })) +} diff --git a/src/modules/overseas-business/components/ProcurementWorkflowPanel.vue b/src/modules/overseas-business/components/ProcurementWorkflowPanel.vue index b83a33f5..ff727cc6 100644 --- a/src/modules/overseas-business/components/ProcurementWorkflowPanel.vue +++ b/src/modules/overseas-business/components/ProcurementWorkflowPanel.vue @@ -9,7 +9,7 @@ import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration' import { listOverseasPurchaseWarehouseOptions } from '../api/platformOverseasBusiness' import { submitProcurementCommand, uploadProcurementFile } from '../api/procurementCommands' -import { submitProcurementWorkflow, type ProcurementWorkflow, type ProcurementOutbound, type ProcurementWorkflowReceipt } from '../api/procurementWorkflow' +import { editProcurementOutbound, submitProcurementWorkflow, type ProcurementWorkflow, type ProcurementOutbound, type ProcurementWorkflowReceipt } from '../api/procurementWorkflow' import { dateTime, statusLabel } from '../procurementPresentation' import type { PlatformProcurement } from '../procurementTypes' import type { OverseasPurchaseWarehouseOption } from '../types' @@ -21,7 +21,7 @@ const open = ref(false), saving = ref(false), uploading = ref(false), checkingFe const action = ref(''), batchId = ref(''), warehouses = ref([]), warehouseLoading = ref(false) const form = reactive({ quantity: 1, trackingNo: '', fileIds: [] as string[], warehouseId: '', decision: 'APPROVE' as 'APPROVE' | 'REJECT', reason: '' }) const uploadedNames = ref([]) -const title = computed(() => ({ review: '审核采购单', receipts: '登记收货', 'request-confirmation': '采购完成', outbounds: '创建出库单', dispatch: '开始派送', complete: '上传签收凭证' })[action.value] || '采购操作') +const title = computed(() => ({ review: '审核采购单', receipts: '登记收货', 'request-confirmation': '采购完成', outbounds: '创建出库单', 'edit-outbound': '编辑出库单', dispatch: '开始派送', cancel: '取消出库单', complete: '上传签收凭证' })[action.value] || '采购操作') const permission = computed(() => `platform:overseas-business:purchase:${action.value === 'review' ? 'review' : ['receipts', 'request-confirmation'].includes(action.value) ? 'receive-overseas' : 'ship-customer'}`) const can = (name: string) => access.can(`platform:overseas-business:purchase:${name}`) const permissionHint = computed(() => props.workflow.status === 'RECEIVING' && !can('receive-overseas') @@ -49,6 +49,7 @@ async function begin(name: string, batch?: ProcurementOutbound) { } action.value = name; batchId.value = batch?.id || ''; error.value = null; warehouseLoading.value = false Object.assign(form, { quantity: 1, trackingNo: '', fileIds: [], warehouseId: '', decision: 'APPROVE', reason: '' }); uploadedNames.value = [] + if (name === 'edit-outbound' && batch) Object.assign(form, { quantity: batch.quantity, trackingNo: batch.trackingNo }) fingerprint = ''; requestKey = ''; openedScope = scope(); open.value = true; captureBaseline() if (name === 'review') { warehouseLoading.value = true @@ -75,17 +76,18 @@ async function save() { if (saving.value || uploading.value || openedScope !== scope() || !access.can(permission.value)) return if (action.value === 'review' && form.decision === 'APPROVE' && !form.warehouseId) { ElMessage.warning('请选择收货仓'); return } if (action.value === 'review' && form.decision === 'REJECT' && !form.reason.trim()) { ElMessage.warning('请填写拒绝原因'); return } - if (['receipts', 'outbounds'].includes(action.value) && (!Number.isSafeInteger(form.quantity) || form.quantity < 1)) { ElMessage.warning('请输入有效数量'); return } + if (['receipts', 'outbounds', 'edit-outbound'].includes(action.value) && (!Number.isSafeInteger(form.quantity) || form.quantity < 1)) { ElMessage.warning('请输入有效数量'); return } if (action.value === 'receipts' && form.quantity > props.detail.customerView.order.purchaseQuantity - props.workflow.receivedQuantity) { ElMessage.warning('收货数量不能超过剩余采购数量'); return } - if (action.value === 'outbounds' && (!form.trackingNo.trim() || form.quantity > props.workflow.availableQuantity)) { ElMessage.warning('请填写物流跟踪号,数量不能超过可出库库存'); return } + if (['outbounds', 'edit-outbound'].includes(action.value) && (!form.trackingNo.trim() || form.quantity > props.workflow.availableQuantity + (action.value === 'edit-outbound' ? (props.workflow.outbounds.find(row => row.id === batchId.value)?.quantity || 0) : 0))) { ElMessage.warning('请填写物流跟踪号,数量不能超过可出库库存'); return } if (['receipts', 'complete'].includes(action.value) && !form.fileIds.length) { ElMessage.warning(action.value === 'receipts' ? '请上传收货照片' : '请上传签收文件或图片'); return } - const data = { version: props.workflow.version, ...(action.value === 'receipts' ? { quantity: form.quantity, fileIds: [...form.fileIds] } : action.value === 'outbounds' ? { quantity: form.quantity, trackingNo: form.trackingNo.trim() } : action.value === 'complete' ? { fileIds: [...form.fileIds] } : {}) } + const data = { version: props.workflow.version, ...(action.value === 'receipts' ? { quantity: form.quantity, fileIds: [...form.fileIds] } : ['outbounds', 'edit-outbound'].includes(action.value) ? { quantity: form.quantity, trackingNo: form.trackingNo.trim() } : action.value === 'complete' ? { fileIds: [...form.fileIds] } : {}) } const next = JSON.stringify([action.value, batchId.value, form.warehouseId, form.decision, form.reason, data]) if (fingerprint !== next || !requestKey) { fingerprint = next; requestKey = crypto.randomUUID() } const current = dialogGeneration saving.value = true; error.value = null try { if (action.value === 'review') await submitProcurementCommand(props.workflow.orderNo, props.customerId, 'review', { version: props.workflow.version, idempotencyKey: requestKey, decision: form.decision, purchaseWarehouseId: form.decision === 'APPROVE' ? form.warehouseId : undefined, reason: form.reason.trim() || undefined }) + else if (action.value === 'edit-outbound') await editProcurementOutbound(props.workflow.orderNo, props.customerId, batchId.value, { ...data, idempotencyKey: requestKey }) else await submitProcurementWorkflow(props.workflow.orderNo, props.customerId, batchId.value ? `outbounds/${encodeURIComponent(batchId.value)}/${action.value}` : action.value, { ...data, idempotencyKey: requestKey }) if (current !== dialogGeneration) return captureBaseline(); open.value = false; ElMessage.success('操作成功'); emit('changed') @@ -94,7 +96,7 @@ async function save() { } const receiptColumns: DataTableColumn[] = [{ prop: 'quantity', label: '收货数量', width: 140 }, { key: 'receivedAt', label: '收货时间', minWidth: 170, formatter: (row: ProcurementWorkflowReceipt) => dateTime(row.receivedAt) }, { key: 'files', label: '照片', minWidth: 180, slotName: 'files' }] const outboundColumns: DataTableColumn[] = [ - { key: 'actions', label: '操作', width: 180, slotName: 'actions', fixed: 'left' }, { prop: 'outboundNo', label: '出库单号', minWidth: 190 }, + { key: 'actions', label: '操作', width: 250, slotName: 'actions', fixed: 'left' }, { prop: 'outboundNo', label: '出库单号', minWidth: 190 }, { key: 'status', label: '状态', width: 120, formatter: (row: ProcurementOutbound) => statusLabel(row.status), afterColumn: 'outboundNo' }, { prop: 'quantity', label: '出库数量', width: 110 }, { prop: 'trackingNo', label: '物流跟踪号', minWidth: 190 }, { key: 'createdAt', label: '创建时间', minWidth: 170, formatter: (row: ProcurementOutbound) => dateTime(row.createdAt) }, @@ -114,14 +116,14 @@ const fileName = (id: string) => props.detail.files.find(file => file.fileId === {{ permissionHint }}
累计收货
{{ workflow.receivedQuantity }}
采购库存
{{ workflow.stockQuantity }}
出库处理中
{{ workflow.reservedQuantity }}
可出库
{{ workflow.availableQuantity }}

收货记录

-

出库单

+

出库单

- - + +
{{ name }}
-

确认采购完成并提交客户确认?

确认本批次开始派送?

+

确认采购完成并提交客户确认?

确认本批次开始派送?

确认取消本批出库单?取消后库存占用将释放。

diff --git a/src/modules/overseas-master-data/api/ownedBusiness.test.ts b/src/modules/overseas-master-data/api/ownedBusiness.test.ts index 112bb72e..75a91b8a 100644 --- a/src/modules/overseas-master-data/api/ownedBusiness.test.ts +++ b/src/modules/overseas-master-data/api/ownedBusiness.test.ts @@ -93,3 +93,12 @@ it('accepts unknown transport ownership without guessing current master data', a expect(result.row.vehicle.ownership).toBeNull() expect(result.row.actualPalletCount).toBeNull() }) + +it('accepts transport rows when nullable ownership is omitted by JSON serialization', async () => { + const detail = ownedBusinessFixture() + const { ownership: _omitted, ...vehicle } = detail.row.vehicle + vi.mocked(http.get).mockResolvedValue({ data: { ...detail, row: { ...detail.row, taskType: 'PICKUP', actualPalletCount: null, vehicle } } }) + const result = await getOwnedBusiness(detail.row.dispatchNo) + expect(result.row.taskType).toBe('PICKUP') + expect(result.row.vehicle.ownership).toBeUndefined() +}) diff --git a/src/modules/overseas-master-data/api/ownedBusiness.ts b/src/modules/overseas-master-data/api/ownedBusiness.ts index f6c4568b..3d61b932 100644 --- a/src/modules/overseas-master-data/api/ownedBusiness.ts +++ b/src/modules/overseas-master-data/api/ownedBusiness.ts @@ -16,7 +16,11 @@ function operations(value: OwnedBusinessOperations): OwnedBusinessOperations { return { ...value, money: value.money ? { ...value.money, amount: decimal(value.money.amount, 2, 18)! } : null, actualDistanceMiles: decimal(value.actualDistanceMiles,3,12), fuelConsumptionLitresPer100Km: decimal(value.fuelConsumptionLitresPer100Km,3,12) } } function row(value: OwnedBusinessRow): OwnedBusinessRow { - if (!value.dispatchNo || !['AFTER_LOADING','DELIVERING','DELIVERED'].includes(value.stage) || !(['OWNED','DISPATCHED'].includes(value.vehicle?.ownership || '') || (value.vehicle?.ownership === null && ['LOCAL_TRUCK','PICKUP'].includes(value.taskType || '')))) throw new Error('自有排车来源或阶段不完整,请重新读取') + // Jackson omits nullable ownership fields from transport rows. Treat both + // null and omitted values as the same transport snapshot state. + const ownership = value.vehicle?.ownership + const transportWithoutVehicleOwnership = ownership == null && ['LOCAL_TRUCK','PICKUP'].includes(value.taskType || '') + if (!value.dispatchNo || !['AFTER_LOADING','DELIVERING','DELIVERED'].includes(value.stage) || !(['OWNED','DISPATCHED'].includes(ownership || '') || transportWithoutVehicleOwnership)) throw new Error('自有排车来源或阶段不完整,请重新读取') return { ...value, operations: operations(value.operations), unitPrice: decimal(value.unitPrice,2,18), recordVersion: ownedBusinessVersion(value.recordVersion), dispatchRecordVersion: ownedBusinessVersion(value.dispatchRecordVersion), actualPalletCount: value.actualPalletCount == null ? null : count(value.actualPalletCount), photoSummary: { loadingCount: count(value.photoSummary.loadingCount), fuelCount: count(value.photoSummary.fuelCount) } } } function detail(value: OwnedBusinessDetail): OwnedBusinessDetail { if (value.row.dispatchNo !== value.dispatch.dispatchNo) throw new Error('运营资料与原排车单不一致'); return { ...value, row: row(value.row) } }