From cbc5521958c370932c5665d028c78b9ea7ded0ef Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 8 Sep 2026 14:15:28 +0800 Subject: [PATCH] fix(overseas): separate receiving and FBA warehouses --- .../overseas-warehouse/api/inboundOrders.ts | 2 ++ .../overseas-warehouse/api/inboundPlans.ts | 3 ++ .../pages/InboundOrderDetailView.vue | 7 +++- .../pages/InboundOrdersView.vue | 11 ++++-- .../pages/realApiMount.test.ts | 35 +++++++++++++++++-- src/modules/overseas-warehouse/types.ts | 1 + 6 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/modules/overseas-warehouse/api/inboundOrders.ts b/src/modules/overseas-warehouse/api/inboundOrders.ts index 210c0cd1..37bee7ee 100644 --- a/src/modules/overseas-warehouse/api/inboundOrders.ts +++ b/src/modules/overseas-warehouse/api/inboundOrders.ts @@ -26,6 +26,8 @@ export interface InboundOrderSummaryDto { inboundCode?: string | null lxCode?: string | null orderType?: string | null + overseasWarehouseCode?: string | null + overseasWarehouseName?: string | null warehouseId?: string | null warehouseCode?: string | null warehouseName?: string | null diff --git a/src/modules/overseas-warehouse/api/inboundPlans.ts b/src/modules/overseas-warehouse/api/inboundPlans.ts index cdbcf131..375ce2ef 100644 --- a/src/modules/overseas-warehouse/api/inboundPlans.ts +++ b/src/modules/overseas-warehouse/api/inboundPlans.ts @@ -27,6 +27,7 @@ export interface InboundPlanSummaryDto { inboundCode?: string | null containerNo?: string | null overseasWarehouseId?: string | null + overseasWarehouseCode?: string | null overseasWarehouseName?: string | null destinationWarehouseId?: string | null destinationWarehouseCode?: string | null @@ -47,6 +48,8 @@ export interface InboundPlanDetailDto { customerId: string sourceOrderId: string containerNo?: string | null + overseasWarehouseCode?: string | null + overseasWarehouseName?: string | null status?: string | null lineCount: number palletCount: number diff --git a/src/modules/overseas-warehouse/pages/InboundOrderDetailView.vue b/src/modules/overseas-warehouse/pages/InboundOrderDetailView.vue index 570b5ab5..5867e666 100644 --- a/src/modules/overseas-warehouse/pages/InboundOrderDetailView.vue +++ b/src/modules/overseas-warehouse/pages/InboundOrderDetailView.vue @@ -13,11 +13,16 @@ const planNo = computed(() => String(route.params.orderId || '')) const customerId = computed(() => String(route.query.customerId || '')) const detail = ref(null); const loading = ref(false); const loadError = ref(null) function text(value: unknown) { return String(value ?? '').trim() || '—' } +function receivingWarehouse(code: unknown, name: unknown) { + const normalizedCode = String(code ?? '').trim() + const normalizedName = String(name ?? '').trim() + return normalizedCode && normalizedName ? `${normalizedCode} · ${normalizedName}` : '—' +} function status(value: unknown) { const labels: Record = { PENDING_ASSIGNMENT: '待入库', PDA_PENDING: '待入库', ASSIGNED: '待入库', PICKING: '拣货中', PARTIAL_INBOUND: '部分入库', COMPLETED: '全部入库' }; return labels[String(value || '')] || text(value) } const firstLine = computed(() => detail.value?.lines[0]) const fieldValues = computed>(() => ({ 入库单号: text(detail.value?.planNo), 入仓码: text(firstLine.value?.inboundCode), 类型: '—', 货主: '—', 关联订单: '—', - 海外仓: text(firstLine.value?.destinationWarehouseName), FBA仓库: text(firstLine.value?.fbaWarehouseCode), 总箱数: detail.value?.expectedPackageCount ?? detail.value?.lines.reduce((sum, line) => sum + line.requiredPackageCount, 0) ?? 0, + 海外仓: receivingWarehouse(detail.value?.overseasWarehouseCode, detail.value?.overseasWarehouseName), FBA仓库: text(firstLine.value?.fbaWarehouseCode), 总箱数: detail.value?.expectedPackageCount ?? detail.value?.lines.reduce((sum, line) => sum + line.requiredPackageCount, 0) ?? 0, 应拣货数: detail.value?.expectedPackageCount ?? detail.value?.lines.reduce((sum, line) => sum + line.requiredPackageCount, 0) ?? 0, 实际拣货数: detail.value?.pickedPackageCount ?? detail.value?.allocations.reduce((sum, item) => sum + item.pickedPackageCount, 0) ?? 0, '计费体积(m³)': Number(detail.value?.totalVolumeCbm || 0), 总托盘数: Number(detail.value?.palletCount || 0), 状态: status(detail.value?.status), 预约状态: '—', 预约送仓时间: '—', 预计装车时间: '—', 排车状态: '—', 创建时间: '—', 更新时间: '—', 备注: '—', diff --git a/src/modules/overseas-warehouse/pages/InboundOrdersView.vue b/src/modules/overseas-warehouse/pages/InboundOrdersView.vue index b7e046f2..bbd752fb 100644 --- a/src/modules/overseas-warehouse/pages/InboundOrdersView.vue +++ b/src/modules/overseas-warehouse/pages/InboundOrdersView.vue @@ -84,6 +84,11 @@ const columns: DataTableColumn[] = [ ] function text(value: unknown) { const normalized = String(value ?? '').trim(); return normalized || '—' } +function receivingWarehouse(code: unknown, name: unknown) { + const normalizedCode = String(code ?? '').trim() + const normalizedName = String(name ?? '').trim() + return normalizedCode && normalizedName ? `${normalizedCode} · ${normalizedName}` : '—' +} function number(value: unknown) { const normalized = Number(value); return Number.isFinite(normalized) ? normalized : 0 } function typeLabel(value: unknown) { return lastMileOrderTypeLabel(value) } function planStatusLabel(value: unknown) { return ({ PENDING_ASSIGNMENT: '待入库', ASSIGNED: '待入库', PDA_PENDING: '待入库', PICKING: '拣货中', COMPLETED: '全部入库', PARTIAL_INBOUND: '部分入库' } as Record)[String(value || '')] || text(value) } @@ -92,8 +97,8 @@ function appointmentStatusLabel(value: unknown) { return !value ? '未预约' : function toListItem(dto: InboundOrderSummaryDto): InboundOrderListItem { return { lineId: String(dto.lineId), planId: String(dto.planId), customerId: String(dto.customerId), orderId: String(dto.orderId), - warehouseId: text(dto.warehouseId) === '—' ? '' : String(dto.warehouseId), destinationWarehouseCode: text(dto.warehouseCode), - orderNo: text(dto.planNo), inboundCode: text(dto.inboundCode), lxCode: text(dto.lxCode), type: typeLabel(dto.orderType), warehouse: text(dto.warehouseName || dto.warehouseCode), + warehouseId: text(dto.warehouseId) === '—' ? '' : String(dto.warehouseId), destinationWarehouseCode: text(dto.warehouseCode), destinationWarehouseName: text(dto.warehouseName), + orderNo: text(dto.planNo), inboundCode: text(dto.inboundCode), lxCode: text(dto.lxCode), type: typeLabel(dto.orderType), warehouse: receivingWarehouse(dto.overseasWarehouseCode, dto.overseasWarehouseName), fbaWarehouse: text(dto.fbaWarehouseCode), owner: text(dto.customerName), cartons: number(dto.packageCount), pickedCartons: number(dto.pickedPackageCount), volume: number(dto.volumeCbm), pallets: number(dto.palletCount), status: planStatusLabel(dto.status), appointmentNo: text(dto.appointmentNo), appointmentStatus: appointmentStatusLabel(dto.appointmentStatus), appointmentAt: formatOverseasDateTime(dto.appointmentAt), expectedLoadingAt: formatOverseasDateTime(dto.expectedLoadingAt), dispatchStatus: dto.dispatchStatus === 'SCHEDULED' ? '已排车' : '未排车', @@ -172,7 +177,7 @@ async function load(model: QueryModel = filters.value) { function search(model: QueryModel) { page.value = 1; void load(model) } function reset(model: QueryModel) { page.value = 1; void load(model) } function openDetail(source: object) { const row = source as InboundOrderListItem; void router.push({ name: 'overseas-inbound-order-detail', params: { orderId: row.orderNo }, query: { customerId: row.customerId } }) } -async function openPlanEdit(source: object) { const row = source as InboundOrderListItem; planEditTarget.value = row; planEditOpen.value = true; planEditLoading.value = true; mutationError.value = null; try { Object.assign(planEditForm, { warehouseId: row.warehouseId, warehouseCode: row.destinationWarehouseCode === '—' ? '' : row.destinationWarehouseCode, warehouseName: row.warehouse, version: row.version }); if (!warehouses.value.length) warehouses.value = (await listReceivingOverseasWarehouses()).records } catch (error) { planEditOpen.value = false; mutationError.value = getApiErrorDetails(error, '海外仓列表加载失败,请重试') } finally { planEditLoading.value = false } } +async function openPlanEdit(source: object) { const row = source as InboundOrderListItem; planEditTarget.value = row; planEditOpen.value = true; planEditLoading.value = true; mutationError.value = null; try { Object.assign(planEditForm, { warehouseId: row.warehouseId, warehouseCode: row.destinationWarehouseCode === '—' ? '' : row.destinationWarehouseCode, warehouseName: row.destinationWarehouseName === '—' ? '' : row.destinationWarehouseName, version: row.version }); if (!warehouses.value.length) warehouses.value = (await listReceivingOverseasWarehouses()).records } catch (error) { planEditOpen.value = false; mutationError.value = getApiErrorDetails(error, '海外仓列表加载失败,请重试') } finally { planEditLoading.value = false } } async function submitPlanEdit() { const target = planEditTarget.value; if (!target || !planEditForm.warehouseId) { ElMessage.warning('请选择目的仓'); return } planEditSaving.value = true; mutationError.value = null; try { await updateInboundPlan(target.orderNo, { customerId: target.customerId, overseasWarehouseId: planEditForm.warehouseId, warehouseCode: planEditForm.warehouseCode, warehouseName: planEditForm.warehouseName, version: planEditForm.version }); planEditOpen.value = false; ElMessage.success('入库单已更新'); await load() } catch (error) { mutationError.value = getApiErrorDetails(error, '入库单更新失败,请重试') } finally { planEditSaving.value = false } } onMounted(() => { void load() }) diff --git a/src/modules/overseas-warehouse/pages/realApiMount.test.ts b/src/modules/overseas-warehouse/pages/realApiMount.test.ts index baa18ddc..959e4c4a 100644 --- a/src/modules/overseas-warehouse/pages/realApiMount.test.ts +++ b/src/modules/overseas-warehouse/pages/realApiMount.test.ts @@ -3,16 +3,23 @@ import { createPinia } from 'pinia' import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { listPlatformTenants } from '@/modules/iam' +import DataTable from '@/shared/components/DataTable.vue' +import { listInboundOrders } from '../api/inboundOrders' import { getInboundPlan } from '../api/inboundPlans' import { listInventoryLedger } from '../api/inventory' import { getLastMileOrder } from '../api/lastMileOrders' import InboundOrderDetailView from './InboundOrderDetailView.vue' +import InboundOrdersView from './InboundOrdersView.vue' import InventoryTransactionsView from './InventoryTransactionsView.vue' import LastMileOrderDetailView from './LastMileOrderDetailView.vue' const routing = vi.hoisted(() => ({ push: vi.fn(), route: { params: {} as Record, query: {} as Record } })) vi.mock('vue-router', () => ({ useRoute: () => routing.route, useRouter: () => ({ push: routing.push }) })) vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn() })) +vi.mock('../api/inboundOrders', () => ({ + listInboundOrders: vi.fn(), createFbaAppointment: vi.fn(), getFbaAppointment: vi.fn(), + updateFbaAppointment: vi.fn(), updateInboundPlan: vi.fn(), +})) vi.mock('../api/inboundPlans', () => ({ getInboundPlan: vi.fn() })) vi.mock('../api/inventory', () => ({ listInventoryLedger: vi.fn() })) vi.mock('../api/lastMileOrders', () => ({ getLastMileOrder: vi.fn(), createLastMileOrderFee: vi.fn() })) @@ -24,11 +31,35 @@ describe('overseas warehouse mounted real API pages', () => { beforeEach(() => { vi.stubGlobal('ResizeObserver', ResizeObserverStub); vi.stubGlobal('localStorage', memoryStorage()); routing.push.mockReset(); routing.route.params = {}; routing.route.query = {} vi.mocked(listPlatformTenants).mockReset().mockResolvedValue({ records: [{ id: '10001', tenantName: '客户甲', status: 'ACTIVE' } as never], page: 1, size: 200, total: 1, pages: 1 }) - vi.mocked(getInboundPlan).mockReset().mockResolvedValue({ id: '9007199254740993', planNo: 'IP-1', customerId: '10001', sourceOrderId: '9007199254740995', status: 'PENDING_ASSIGNMENT', lineCount: 1, palletCount: 1, totalVolumeCbm: 2, lines: [{ id: '1', orderId: '9007199254740995', inboundCode: 'IN-1', fbaWarehouseCode: 'ONT8', destinationWarehouseName: '洛杉矶仓', volumeCbm: 2, requiredPackageCount: 10, plannedPalletCount: 1 }], pallets: [], allocations: [] }) + vi.mocked(listInboundOrders).mockReset().mockResolvedValue({ records: [], page: 1, size: 10, total: 0, pages: 0 }) + vi.mocked(getInboundPlan).mockReset().mockResolvedValue({ id: '9007199254740993', planNo: 'IP-1', customerId: '10001', sourceOrderId: '9007199254740995', overseasWarehouseCode: 'US-LAX-01', overseasWarehouseName: '洛杉矶接柜仓', status: 'PENDING_ASSIGNMENT', lineCount: 1, palletCount: 1, totalVolumeCbm: 2, lines: [{ id: '1', orderId: '9007199254740995', inboundCode: 'IN-1', fbaWarehouseCode: 'ABE3', destinationWarehouseCode: 'ABE3', destinationWarehouseName: 'ABE3目的仓', volumeCbm: 2, requiredPackageCount: 10, plannedPalletCount: 1 }], pallets: [], allocations: [] }) vi.mocked(listInventoryLedger).mockReset().mockResolvedValue({ records: [{ id: '9007199254740997', customerId: '10001', customerName: '客户甲', destinationWarehouseCode: 'LAX', inboundCode: 'NYEXX2608300001', inboundOrderNo: 'IP-1', businessType: 'STOCK_IN', beforePackages: 0, deltaPackages: 10, afterPackages: 10, occurredAt: '2026-08-20T10:00:00' }], page: 1, size: 20, total: 1, pages: 1 }) vi.mocked(getLastMileOrder).mockReset().mockResolvedValue({ order: { id: '9007199254740999', customerId: '10001', customerName: '华远贸易', orderNo: 'LMO-260827-0001', inboundCode: 'NYEXX2608280033', containerNo: 'TSTU0000061', currentStatus: 'PENDING_ASSIGNMENT' }, cargo: [], statusHistory: [], fees: [], operationHistory: [] }) }) - it('mounts inbound detail with business references without exposing the internal source order id', async () => { routing.route.params = { orderId: 'IP-1' }; routing.route.query = { customerId: '10001' }; const wrapper = mount(InboundOrderDetailView, { global: { plugins: [ElementPlus] } }); await flushPromises(); expect(getInboundPlan).toHaveBeenCalledWith('IP-1', '10001'); expect(wrapper.text()).toContain('IP-1'); expect(wrapper.text()).toContain('IN-1'); expect(wrapper.text()).not.toContain('9007199254740995'); expect(wrapper.text()).toContain('洛杉矶仓') }) + it('keeps receiving warehouse and FBA warehouse independent in the inbound-plan list', async () => { + vi.mocked(listInboundOrders).mockResolvedValue({ + records: [ + { lineId: '1', planId: '11', planNo: 'IP-1', customerId: '10001', orderId: '21', inboundCode: 'IN-1', overseasWarehouseCode: 'US-LAX-01', overseasWarehouseName: '洛杉矶接柜仓', warehouseId: '31', warehouseCode: 'ABE3', warehouseName: 'ABE3目的仓', fbaWarehouseCode: 'ABE3' }, + { lineId: '2', planId: '12', planNo: 'IP-2', customerId: '10001', orderId: '22', inboundCode: 'IN-2', overseasWarehouseCode: null, overseasWarehouseName: '不充整接柜仓', warehouseId: '32', warehouseCode: 'ONT8', warehouseName: 'ONT8目的仓', fbaWarehouseCode: 'ONT8' }, + ], page: 1, size: 10, total: 2, pages: 1, + }) + const wrapper = mount(InboundOrdersView, { global: { plugins: [createPinia(), ElementPlus] } }) + await flushPromises() + const rows = wrapper.findComponent(DataTable).props('data') as Array<{ warehouse: string; fbaWarehouse: string; destinationWarehouseName: string }> + expect(rows[0]).toMatchObject({ warehouse: 'US-LAX-01 · 洛杉矶接柜仓', fbaWarehouse: 'ABE3', destinationWarehouseName: 'ABE3目的仓' }) + expect(rows[1]).toMatchObject({ warehouse: '—', fbaWarehouse: 'ONT8', destinationWarehouseName: 'ONT8目的仓' }) + }) + it('mounts inbound detail with independent receiving and FBA warehouse references', async () => { routing.route.params = { orderId: 'IP-1' }; routing.route.query = { customerId: '10001' }; const wrapper = mount(InboundOrderDetailView, { global: { plugins: [ElementPlus] } }); await flushPromises(); expect(getInboundPlan).toHaveBeenCalledWith('IP-1', '10001'); expect(wrapper.text()).toContain('IP-1'); expect(wrapper.text()).toContain('IN-1'); expect(wrapper.text()).not.toContain('9007199254740995'); expect(wrapper.text()).toContain('US-LAX-01 · 洛杉矶接柜仓'); expect(wrapper.text()).toContain('ABE3'); expect(wrapper.text()).not.toContain('ABE3目的仓') }) + it('shows an empty receiving warehouse when either frozen field is missing without falling back to destination data', async () => { + vi.mocked(getInboundPlan).mockResolvedValueOnce({ id: '1', planNo: 'IP-2', customerId: '10001', sourceOrderId: '2', overseasWarehouseCode: null, overseasWarehouseName: '不充整接柜仓', status: 'PENDING_ASSIGNMENT', lineCount: 1, palletCount: 0, totalVolumeCbm: 0, lines: [{ id: '3', orderId: '2', inboundCode: 'IN-2', fbaWarehouseCode: 'ABE3', destinationWarehouseCode: 'ABE3', destinationWarehouseName: 'ABE3目的仓', requiredPackageCount: 1, plannedPalletCount: 0 }], pallets: [], allocations: [] }) + routing.route.params = { orderId: 'IP-2' }; routing.route.query = { customerId: '10001' } + const wrapper = mount(InboundOrderDetailView, { global: { plugins: [ElementPlus] } }) + await flushPromises() + const field = (label: string) => wrapper.findAll('dt').find((item) => item.text() === label)?.element.parentElement?.querySelector('dd')?.textContent + expect(field('海外仓')).toBe('—') + expect(field('FBA仓库')).toBe('ABE3') + expect(wrapper.text()).not.toContain('ABE3目的仓') + }) it('mounts inventory ledger as localized read-only business facts without exposing internal ids', async () => { const wrapper = mount(InventoryTransactionsView, { global: { plugins: [createPinia(), ElementPlus] } }); await flushPromises(); expect(listInventoryLedger).toHaveBeenCalledWith(expect.objectContaining({ page: 1, size: 10 })); expect(listInventoryLedger).not.toHaveBeenCalledWith(expect.objectContaining({ customerId: expect.anything() })); expect(wrapper.text()).toContain('客户甲'); expect(wrapper.text()).toContain('NYEXX2608300001'); expect(wrapper.text()).not.toContain('10001'); expect(wrapper.text()).toContain('IP-1'); expect(wrapper.text()).toContain('10'); expect(wrapper.text()).toContain('入库'); expect(wrapper.text()).not.toContain('STOCK_IN'); expect(wrapper.text()).toContain('不提供出库单跳转'); expect(wrapper.text()).not.toContain('当前流水 DTO'); expect(wrapper.text()).not.toContain('操作人名称待补充') }) it('renders complete business references with single-line ellipsis and titles', async () => { routing.route.params = { orderId: '9007199254740999' }; const wrapper = mount(LastMileOrderDetailView, { global: { plugins: [createPinia(), ElementPlus] } }); await flushPromises(); expect(getLastMileOrder).toHaveBeenCalledWith('9007199254740999'); expect(wrapper.text()).toContain('华远贸易'); expect(wrapper.text()).toContain('LMO-260827-0001'); expect(wrapper.get('.order-reference-text').attributes('title')).toBe('LMO-260827-0001'); const businessValues = wrapper.findAll('.detail-field-value--business'); expect(businessValues.map((item) => item.attributes('title'))).toEqual(expect.arrayContaining(['NYEXX2608280033', 'TSTU0000061'])); expect(businessValues.every((item) => item.text().length > 0)).toBe(true) }) }) diff --git a/src/modules/overseas-warehouse/types.ts b/src/modules/overseas-warehouse/types.ts index 47146cb8..62c3c6fb 100644 --- a/src/modules/overseas-warehouse/types.ts +++ b/src/modules/overseas-warehouse/types.ts @@ -60,6 +60,7 @@ export interface InboundOrderListItem { orderId: string warehouseId: string destinationWarehouseCode: string + destinationWarehouseName: string orderNo: string inboundCode: string lxCode: string