From 84dd9dcbb4ac6de39f5eb4683a032e17bda0c4e5 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Wed, 4 Nov 2026 10:39:15 +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 --- .../container-loading/pages/ContainerDetailView.vue | 6 ++++-- .../finance/pages/FinanceFormalSettlementPage.vue | 2 +- src/modules/finance/pages/FinanceG3G4Views.test.ts | 9 +++++++++ src/modules/overseas-business/fleetArrangement.ts | 2 +- .../pages/TransportOrdersView.test.ts | 12 +++++++++++- .../overseas-business/pages/TransportOrdersView.vue | 4 ++-- src/modules/overseas-business/truckWorkflow.ts | 2 +- .../pages/LastMileOrderDetailView.test.ts | 4 ++-- .../pages/LastMileOrderDetailView.vue | 5 +++-- 9 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/modules/container-loading/pages/ContainerDetailView.vue b/src/modules/container-loading/pages/ContainerDetailView.vue index 15f1253f..2aa81799 100644 --- a/src/modules/container-loading/pages/ContainerDetailView.vue +++ b/src/modules/container-loading/pages/ContainerDetailView.vue @@ -498,6 +498,7 @@ async function saveDocument() { } catch (cause) { const details = getApiErrorDetails(cause, `${documentTitle.value}上传失败`) if (details.code === 'A0409') try { overview.value = await getContainerOverview(containerId.value) } catch { /* 保留原并发错误证据 */ } + console.error('[Container document upload] failed', details) documentError.value = details } finally { documentSaving.value = false } @@ -578,6 +579,7 @@ async function uploadClearance() { if (details.code === 'A0409') { try { await refreshOverviewDocument('DESTINATION_CLEARANCE') } catch { /* 保留原并发错误证据 */ } } + console.error('[Destination clearance upload] failed', details) clearanceError.value = details } finally { clearanceSaving.value = false } @@ -686,9 +688,9 @@ onMounted(async () => { - 选择文件上传新文件将替换当前资料并保留历史。 + 选择文件上传新文件将替换当前资料并保留历史。 - 选择文件 + 选择文件 diff --git a/src/modules/finance/pages/FinanceFormalSettlementPage.vue b/src/modules/finance/pages/FinanceFormalSettlementPage.vue index 3e6a1024..159acdb4 100644 --- a/src/modules/finance/pages/FinanceFormalSettlementPage.vue +++ b/src/modules/finance/pages/FinanceFormalSettlementPage.vue @@ -131,7 +131,7 @@ const paginatedRows = computed(() => { const start = (page.value - 1) * pageSize.value return filteredRows.value.slice(start, start + pageSize.value) }) -const totals = computed(() => filteredRows.value.reduce((result, row) => ({ +const totals = computed(() => filteredRows.value.filter(row => row.status !== 'VOIDED').reduce((result, row) => ({ amount: result.amount + totalAmount(row), paid: result.paid + paidAmount(row), outstanding: result.outstanding + row.outstandingAmount, diff --git a/src/modules/finance/pages/FinanceG3G4Views.test.ts b/src/modules/finance/pages/FinanceG3G4Views.test.ts index 9f4bc0bf..3f4691d4 100644 --- a/src/modules/finance/pages/FinanceG3G4Views.test.ts +++ b/src/modules/finance/pages/FinanceG3G4Views.test.ts @@ -72,6 +72,15 @@ enableAutoUnmount(afterEach) afterAll(() => vi.unstubAllGlobals()) describe('Finance G3/G4 views', () => { + it('excludes voided settlements from totals while retaining them in the list', async () => { + vi.mocked(listFinancePayableSettlements).mockResolvedValue([ + payable, + { ...payable, id: '702', settlementNo: 'AP-VOIDED', status: 'VOIDED', payableAmount: 50, outstandingAmount: 50 }, + ]) + const wrapper = await mountWithAccess(FinanceFormalSettlementPage, ['platform:finance:settlement-ap:list'], { kind: 'payable' }) + expect(wrapper.findAll('.workbench-metric-value strong').map(item => item.text())).toEqual(['¥200.00', '¥0.00', '¥200.00']) + expect(wrapper.text()).toContain('AP-VOIDED') + }) beforeEach(() => { vi.stubGlobal('ResizeObserver', ResizeObserverStub) vi.stubGlobal('localStorage', memoryStorage()) diff --git a/src/modules/overseas-business/fleetArrangement.ts b/src/modules/overseas-business/fleetArrangement.ts index 9d93fe12..a54861ac 100644 --- a/src/modules/overseas-business/fleetArrangement.ts +++ b/src/modules/overseas-business/fleetArrangement.ts @@ -25,5 +25,5 @@ export interface FleetArrangement { export const poolOrderStatusLabels: Record = { WAITING_INBOUND: '待入仓', RECEIVING: '收货中', WAITING_CONSOLIDATION: '待合托', - PENDING_DISPATCH: '待派车', IN_TRANSIT: '运输中', COMPLETED: '已完成', CANCELLED: '已取消', + PENDING_DISPATCH: '待派车', IN_TRANSIT: '派送中', COMPLETED: '已完成', CANCELLED: '已取消', } diff --git a/src/modules/overseas-business/pages/TransportOrdersView.test.ts b/src/modules/overseas-business/pages/TransportOrdersView.test.ts index 69e41f4e..2e1f9629 100644 --- a/src/modules/overseas-business/pages/TransportOrdersView.test.ts +++ b/src/modules/overseas-business/pages/TransportOrdersView.test.ts @@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useAuthStore } from '@/core/auth/auth' import { useAccessPreviewStore } from '@/core/access/accessPreview' import RowActions from '@/shared/components/RowActions.vue' +import QueryToolbar from '@/shared/components/QueryToolbar.vue' import type { DataTableColumn } from '@/shared/components/DataTable.vue' import { listLocalLooseboardOrders, listUsTruckOrders } from '../api/platformOverseasBusiness' import { overseasBusinessPermissions as permissions } from '../permissions' @@ -31,7 +32,7 @@ function mountList(domain: 'truck' | 'looseboard', edit = false) { auth.permissions = [permissions.truckList, permissions.looseboardList, ...(edit ? [permissions.looseboardEdit] : [])] useAccessPreviewStore().initializeAfterLogin() return mount(TransportOrdersView, { props: { domain }, global: { plugins: [pinia, ElementPlus], stubs: { - DataTable: { name: 'DataTable', props: ['data', 'columns'], template: '{{ column.prop ? row[column.prop] : \'\' }}' }, + DataTable: { name: 'DataTable', props: ['data', 'columns'], template: '{{ column.prop ? row[column.prop] : \'\' }}' }, AppDialog: true, } } }) } @@ -44,6 +45,15 @@ beforeEach(() => { }) describe('transport document entry', () => { + it.each(['truck', 'looseboard'] as const)('limits retired status filters only in truck mode (%s)', async domain => { + const wrapper = mountList(domain); await flushPromises() + const status = wrapper.findComponent(QueryToolbar).props('fields').find(field => field.key === 'status')! + const values = status.options!.map(option => option.value) + expect(values.includes('WAITING_CONSOLIDATION')).toBe(domain === 'looseboard') + if (domain === 'truck') for (const value of ['WAITING_INBOUND', 'RECEIVING', 'PALLET_OVERAGE_REVIEW', 'PARTIALLY_ALLOCATED', 'ALLOCATED', 'PENDING_DISPATCH']) expect(values).not.toContain(value) + else expect(values).toContain('WAITING_INBOUND') + expect(status.options!.find(option => option.value === 'IN_TRANSIT')?.label).toBe('派送中') + }) it('lets list-only users open looseboard details without edit permission', async () => { const wrapper = mountList('looseboard'); await flushPromises() const actions = wrapper.findComponent(RowActions) diff --git a/src/modules/overseas-business/pages/TransportOrdersView.vue b/src/modules/overseas-business/pages/TransportOrdersView.vue index 74d5cb93..baa1c3ad 100644 --- a/src/modules/overseas-business/pages/TransportOrdersView.vue +++ b/src/modules/overseas-business/pages/TransportOrdersView.vue @@ -42,7 +42,7 @@ const page = ref(1); const pageSize = ref(20); const total = ref(0) const arrangementOpen = ref(false), arrangement = ref(null) function showArrangement(row: LocalTransportOrder | LocalLooseboardOrder) { arrangement.value = row.fleetArrangement; arrangementOpen.value = true } const filters = ref({ orderNo: String(route.query.orderNo || ''), status: '', routeKeyword: '' }); const applied = ref({ ...filters.value }) -const looseboardStatusLabels: Record = { PENDING_ACCEPTANCE: '待接单', WAITING_INBOUND: '待入仓', RECEIVING: '收货中', WAITING_CONSOLIDATION: '待合托', PENDING_DISPATCH: '待派车', DISPATCHED: '已派车', IN_TRANSIT: '运输中', COMPLETED: '已完成', CANCELLED: '已取消', ...poolOrderStatusLabels } +const looseboardStatusLabels: Record = { PENDING_ACCEPTANCE: '待接单', WAITING_INBOUND: '待入仓', RECEIVING: '收货中', WAITING_CONSOLIDATION: '待合托', PENDING_DISPATCH: '待派车', DISPATCHED: '已派车', IN_TRANSIT: '派送中', COMPLETED: '已完成', CANCELLED: '已取消', ...poolOrderStatusLabels } const transportTypeLabels: Record = { PORT_PICKUP: '港口提柜', WAREHOUSE_TO_WAREHOUSE: '仓到仓', WAREHOUSE_TO_DOOR: '仓到门' } const statusLabels = computed>(() => isLooseboard.value ? looseboardStatusLabels : truckStatusLabels) const statusTones: Record = { @@ -53,7 +53,7 @@ const statusTones: Record(() => [ { key: 'orderNo', type: 'input', label: '商业单号', placeholder: '请输入商业单号', width: 230 }, - { key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 170, options: Object.entries(isLooseboard.value ? looseboardStatusLabels : truckStatusLabels).map(([value, label]) => ({ value, label })) }, + { key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 170, options: Object.entries(isLooseboard.value ? looseboardStatusLabels : truckStatusLabels).filter(([value]) => isLooseboard.value || !['WAITING_INBOUND', 'RECEIVING', 'WAITING_CONSOLIDATION', 'PALLET_OVERAGE_REVIEW', 'PARTIALLY_ALLOCATED', 'ALLOCATED', 'PENDING_DISPATCH'].includes(value)).map(([value, label]) => ({ value, label })) }, { key: 'routeKeyword', type: 'input', label: '运输路线', placeholder: '起运地/目的地', width: 240 }, ]) const truckColumns: DataTableColumn[] = [ diff --git a/src/modules/overseas-business/truckWorkflow.ts b/src/modules/overseas-business/truckWorkflow.ts index e9c0d795..f7aae6b3 100644 --- a/src/modules/overseas-business/truckWorkflow.ts +++ b/src/modules/overseas-business/truckWorkflow.ts @@ -35,7 +35,7 @@ export const truckCommandHints: Record = { 'allocate-pallets': '选择本单已实收且未分配的托盘,可分批分配;全部已接收托盘分配完成后才能进入待派车。', 'prepare-dispatch': '确认全部已接收托盘已分配完成,将本单交由调度安排车辆。', dispatch: '请在车辆安排已实际完成后确认,本单将进入已派车。', - 'start-transit': '请在车辆已实际发运后确认,本单将进入运输中。', + 'start-transit': '请在车辆已实际发运后确认,本单将进入派送中。', complete: '请在运输及交付已实际完成后确认。确认完成后不能撤回。', cancel: '取消后本单不能继续履约,请填写取消原因。', } diff --git a/src/modules/overseas-warehouse/pages/LastMileOrderDetailView.test.ts b/src/modules/overseas-warehouse/pages/LastMileOrderDetailView.test.ts index 45e03690..d3fc4e48 100644 --- a/src/modules/overseas-warehouse/pages/LastMileOrderDetailView.test.ts +++ b/src/modules/overseas-warehouse/pages/LastMileOrderDetailView.test.ts @@ -156,8 +156,8 @@ describe('LastMileOrderDetailView domestic detail alignment', () => { expect(createLastMileOrderFee).toHaveBeenCalledWith('internal-order-id', expect.objectContaining({ feeItemCode: 'FEE_TYPE_077', amount: 80 })) }) - it('creates a return order fee without charging quantity or unit inputs', async () => { - vi.mocked(getLastMileOrder).mockResolvedValue({ order: { id: 'internal-order-id', customerId: 'customer-1', orderNo: 'RT-1', orderType: 'RETURN_RELABEL', currentStatus: 'INBOUND_COMPLETED' }, cargo: [], fees: [], statusHistory: [], operationHistory: [] }) + it.each(['RETURN_RELABEL', 'WAREHOUSE_TRANSFER'])('creates a %s fee without charging quantity or unit inputs', async orderType => { + vi.mocked(getLastMileOrder).mockResolvedValue({ order: { id: 'internal-order-id', customerId: 'customer-1', orderNo: 'RT-1', orderType, currentStatus: 'INBOUND_COMPLETED' }, cargo: [], fees: [], statusHistory: [], operationHistory: [] }) const { dialog } = await openFeeEditor() expect(dialog.text()).not.toContain('计费数量') expect(dialog.text()).not.toContain('计费单位') diff --git a/src/modules/overseas-warehouse/pages/LastMileOrderDetailView.vue b/src/modules/overseas-warehouse/pages/LastMileOrderDetailView.vue index 62104d80..8de94ada 100644 --- a/src/modules/overseas-warehouse/pages/LastMileOrderDetailView.vue +++ b/src/modules/overseas-warehouse/pages/LastMileOrderDetailView.vue @@ -93,6 +93,7 @@ function isSingleLineBusinessField(field: DetailField) { return field.label === const referenceValue = computed(() => lastMileOrderReference(detail.value?.order.orderNo, '订单编码待补充')) const isReturnOrder = computed(() => detail.value?.order.orderType === 'RETURN_RELABEL') +const showFeeQuantity = computed(() => !isReturnOrder.value && detail.value?.order.orderType !== 'WAREHOUSE_TRANSFER') const returnReceiptUnits = computed(() => { const receiptTypes = new Map((detail.value?.returnReceipt?.receipts || []).map(receipt => [receipt.receiptNo, receipt.unitType])) const counts = { parcels: 0, boxes: 0, unknown: 0 } @@ -324,7 +325,7 @@ async function saveFee() { if (feeForm.direction === 'PAYABLE' && !supplier) { ElMessage.warning('应付费用必须选择供应商'); return } saving.value = true; mutationError.value = null try { - await createLastMileOrderFee(orderId.value, { feeItemCode: selectedItem.feeCode, feeItemName: selectedItem.feeTypeName, feeRemark: feeForm.feeRemark.trim() || undefined, direction: feeForm.direction, currency: feeForm.currency, amount: feeForm.amount, businessDate: feeForm.businessDate, supplierId: supplier ? String(supplier.id) : undefined, supplierName: supplier?.name, supplierCategory: supplier?.category, settlementMode: feeForm.direction === 'PAYABLE' ? 'ON_ACCOUNT' : (feeForm.settlementMode || undefined), ...(!isReturnOrder.value ? { chargeableQuantity: feeForm.quantity, quantityUnit: feeForm.quantity == null ? undefined : feeForm.quantityUnit.trim(), unitPrice: feeForm.quantity ? feeForm.amount / feeForm.quantity : undefined } : {}) }) + await createLastMileOrderFee(orderId.value, { feeItemCode: selectedItem.feeCode, feeItemName: selectedItem.feeTypeName, feeRemark: feeForm.feeRemark.trim() || undefined, direction: feeForm.direction, currency: feeForm.currency, amount: feeForm.amount, businessDate: feeForm.businessDate, supplierId: supplier ? String(supplier.id) : undefined, supplierName: supplier?.name, supplierCategory: supplier?.category, settlementMode: feeForm.direction === 'PAYABLE' ? 'ON_ACCOUNT' : (feeForm.settlementMode || undefined), ...(showFeeQuantity.value ? { chargeableQuantity: feeForm.quantity, quantityUnit: feeForm.quantity == null ? undefined : feeForm.quantityUnit.trim(), unitPrice: feeForm.quantity ? feeForm.amount / feeForm.quantity : undefined } : {}) }) feeDialogOpen.value = false; ElMessage.success('费用已提交审核'); await load() await Promise.all([feePanel.value?.refresh(), payableFeePanel.value?.refresh()]); activeTab.value = 'fees' } catch (error) { mutationError.value = getApiErrorDetails(error, '费用新增失败,请重试') } @@ -443,7 +444,7 @@ onMounted(load) - + 此单已有相同费用,确认本次为新增作业后可继续添加。{{ sameProjectFeeSummary(row) }} {{ existingFeesError }} 重新加载