From 081f79a9a1a764ec265759064d095a5de66a0d8c Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Sat, 10 Oct 2026 16:12:38 +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 --- .../pages/OutboundsView.returnSku.test.ts | 28 +++++++------ .../pages/OutboundsView.selection.test.ts | 39 ++++++++++++++++++- .../pages/OutboundsView.vue | 6 ++- 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/src/modules/overseas-warehouse/pages/OutboundsView.returnSku.test.ts b/src/modules/overseas-warehouse/pages/OutboundsView.returnSku.test.ts index 84a31009..d91791da 100644 --- a/src/modules/overseas-warehouse/pages/OutboundsView.returnSku.test.ts +++ b/src/modules/overseas-warehouse/pages/OutboundsView.returnSku.test.ts @@ -10,11 +10,12 @@ vi.mock('@/modules/warehouses', () => ({ listPlatformWarehouses: vi.fn().mockRes vi.mock('../api/outbounds', () => ({ listOutbounds: vi.fn().mockResolvedValue({ records: [], total: 0 }), listOneClickOutboundCandidates: vi.fn().mockResolvedValue([]), listReturnOutboundCandidates: vi.fn().mockResolvedValue([]), createReturnOutbound: api.create })) vi.mock('../api/returnInventory', () => ({ listReturnStock: api.stock, listReturnCartons: api.cartons, getReturnCarton: api.detail })) const sources = [ - { cartonMark: 'BOX-A', sourceUnitType: 'BOX', sourceReturnNo: 'RT-1', status: 'AVAILABLE', items: [{ skuCode: 'SKU-A', quantity: 5, availableQuantity: 5 }] }, - { cartonMark: 'BOX-B', sourceUnitType: 'BOX', sourceReturnNo: 'RT-1', status: 'AVAILABLE', items: [{ skuCode: 'SKU-A', quantity: 5, availableQuantity: 5 }] }, + { cartonMark: 'LOOSE-A', sourceUnitType: 'PARCEL', sourceReturnNo: 'RT-1', status: 'AVAILABLE', items: [{ skuCode: 'SKU-A', quantity: 5, availableQuantity: 5 }] }, + { cartonMark: 'LOOSE-B', sourceUnitType: 'PARCEL', sourceReturnNo: 'RT-1', status: 'AVAILABLE', items: [{ skuCode: 'SKU-A', quantity: 5, availableQuantity: 5 }] }, { cartonMark: 'PARCEL', sourceUnitType: 'PARCEL', sourceReturnNo: 'RT-1', status: 'AVAILABLE', items: [{ skuCode: 'SKU-B', quantity: 6, availableQuantity: 6 }] }, + { cartonMark: 'PACKED', sourceUnitType: 'BOX', sourceReturnNo: 'RT-1', status: 'AVAILABLE', items: [{ skuCode: 'SKU-A', quantity: 5, availableQuantity: 5 }] }, { cartonMark: 'MIXED', sourceUnitType: 'BOX', sourceReturnNo: 'RT-1', status: 'AVAILABLE', items: [{ skuCode: 'SKU-A', quantity: 3 }, { skuCode: 'SKU-B', quantity: 2 }] }, - { cartonMark: 'OTHER-ORDER', sourceUnitType: 'BOX', sourceReturnNo: 'RT-2', status: 'AVAILABLE', items: [{ skuCode: 'SKU-A', quantity: 100 }] }, + { cartonMark: 'OTHER-ORDER', sourceUnitType: 'PARCEL', sourceReturnNo: 'RT-2', status: 'AVAILABLE', items: [{ skuCode: 'SKU-A', quantity: 100 }] }, ] let wrapper: VueWrapper beforeEach(() => { @@ -44,21 +45,24 @@ async function open(sku: string) { } function right() { return wrapper.findAllComponents({ name: 'ElTable' }).filter(table => table.classes().includes('return-result-table'))[1]! } async function selectMarks(marks: string[]) { - for (const mark of marks) right().findAllComponents(ElCheckbox).find(check => check.element.closest('tr')?.textContent?.includes(mark === 'PARCEL' ? '包裹散件(按 SKU 拣货)' : mark))!.vm.$emit('change', true) + for (const mark of marks) { + const rows = right().props('data') as Array<{ cartonMark: string }> + right().findAllComponents(ElCheckbox)[rows.findIndex(row => row.cartonMark === mark)]!.vm.$emit('change', true) + } await flushPromises() } describe('single SKU outbound source and piece quantity', () => { - it('selects packed single-SKU boxes, scopes quantities to the return order and excludes mixed boxes', async () => { + it('selects only loose SKU sources scoped to the return order and excludes packed boxes', async () => { await open('SKU-A') - expect(right().props('data').map((row: { cartonMark: string }) => row.cartonMark)).toEqual(['BOX-A', 'BOX-B']) + expect(right().props('data').map((row: { cartonMark: string }) => row.cartonMark)).toEqual(['LOOSE-A', 'LOOSE-B']) expect(wrapper.text()).not.toContain('OTHER-ORDER') - await selectMarks(['BOX-A', 'BOX-B']) + await selectMarks(['LOOSE-A', 'LOOSE-B']) expect(wrapper.findComponent(ElInputNumber).props('modelValue')).toBe(10) await wrapper.get('[data-test="submit"]').trigger('click'); await flushPromises() - expect(api.create).toHaveBeenCalledWith('C1', expect.objectContaining({ skuCode: 'SKU-A', quantity: 10, cartonMarks: ['BOX-A', 'BOX-B'] })) + expect(api.create).toHaveBeenCalledWith('C1', expect.objectContaining({ skuCode: 'SKU-A', quantity: 10, cartonMarks: ['LOOSE-A', 'LOOSE-B'] })) }) - it('submits six pieces from two five-piece boxes', async () => { - await open('SKU-A'); await selectMarks(['BOX-A', 'BOX-B']) + it('submits six pieces from two five-piece loose sources', async () => { + await open('SKU-A'); await selectMarks(['LOOSE-A', 'LOOSE-B']) wrapper.findComponent(ElInputNumber).vm.$emit('update:modelValue', 6); await flushPromises() await wrapper.get('[data-test="submit"]').trigger('click'); await flushPromises() expect(api.create).toHaveBeenCalledWith('C1', expect.objectContaining({ quantity: 6 })) @@ -77,13 +81,13 @@ describe('single SKU outbound source and piece quantity', () => { expect(right().text()).not.toContain('包裹散件') }) it.each([[0,1],[11,10]])('clamps input %s to the allowed bound %s', async (quantity, expected) => { - await open('SKU-A'); await selectMarks(['BOX-A', 'BOX-B']) + await open('SKU-A'); await selectMarks(['LOOSE-A', 'LOOSE-B']) wrapper.findComponent(ElInputNumber).vm.$emit('update:modelValue', quantity); await flushPromises() await wrapper.get('[data-test="submit"]').trigger('click'); await flushPromises() expect(api.create).toHaveBeenCalledWith('C1', expect.objectContaining({quantity: expected})) }) it.each([1.5])('blocks invalid quantity %s before creating an order', async quantity => { - await open('SKU-A'); await selectMarks(['BOX-A', 'BOX-B']) + await open('SKU-A'); await selectMarks(['LOOSE-A', 'LOOSE-B']) wrapper.findComponent(ElInputNumber).vm.$emit('update:modelValue', quantity); await flushPromises() await wrapper.get('[data-test="submit"]').trigger('click'); await flushPromises() expect(api.create).not.toHaveBeenCalled() diff --git a/src/modules/overseas-warehouse/pages/OutboundsView.selection.test.ts b/src/modules/overseas-warehouse/pages/OutboundsView.selection.test.ts index 99e785b4..308067a5 100644 --- a/src/modules/overseas-warehouse/pages/OutboundsView.selection.test.ts +++ b/src/modules/overseas-warehouse/pages/OutboundsView.selection.test.ts @@ -4,7 +4,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import OutboundsView from './OutboundsView.vue' import { listPlatformTenants } from '@/modules/iam' -const api = vi.hoisted(() => ({ candidates: vi.fn(), create: vi.fn(), warehouses: vi.fn() })) +const api = vi.hoisted(() => ({ candidates: vi.fn(), create: vi.fn(), warehouses: vi.fn(), returnCandidates: vi.fn(), stock: vi.fn(), cartons: vi.fn(), carton: vi.fn() })) +vi.mock('../api/returnInventory', () => ({ listReturnStock: api.stock, listReturnCartons: api.cartons, getReturnCarton: api.carton })) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) })) vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) })) vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn().mockResolvedValue({ records: [] }) })) @@ -13,7 +14,7 @@ vi.mock('../api/outbounds', () => ({ listOutbounds: vi.fn().mockResolvedValue({ records: [], total: 0 }), listOneClickOutboundCandidates: api.candidates, createOneClickDropshipOutbound: api.create, - cancelOutbound: vi.fn(), createReturnOutbound: vi.fn(), listReturnOutboundCandidates: vi.fn(), + cancelOutbound: vi.fn(), createReturnOutbound: vi.fn(), listReturnOutboundCandidates: api.returnCandidates, })) const fixtures = Array.from({ length: 7 }, (_, index) => ({ @@ -48,6 +49,40 @@ async function selectBothPages() { } describe('outbound candidate search and selection', () => { + it('limits single SKU quantities and selectable sources to loose stock of the selected return, excluding pallet and box contents', async () => { + vi.mocked(listPlatformTenants).mockResolvedValue({ records: [{ id: 'customer-a', tenantName: '客户甲', status: 'ACTIVE' }], page: 1, size: 200, total: 1, pages: 1 } as Awaited>) + const sources = [ + { cartonMark: 'P-A', sourceUnitType: 'PALLET', skuCode: 'SKU-A', quantity: 5 }, + { cartonMark: 'P-B', sourceUnitType: 'PALLET', skuCode: 'SKU-B', quantity: 6 }, + { cartonMark: 'BOX-A', sourceUnitType: 'BOX', skuCode: 'SKU-A', quantity: 3 }, + { cartonMark: 'SP-A', sourceUnitType: 'PARCEL', skuCode: 'SKU-A', quantity: 1 }, + { cartonMark: 'SP-B', sourceUnitType: 'PARCEL', skuCode: 'SKU-B', quantity: 5 }, + { cartonMark: 'SP-OTHER', sourceUnitType: 'PARCEL', skuCode: 'SKU-A', quantity: 20 }, + ].map(row => ({ ...row, status: 'AVAILABLE', sourceReturnNo: row.cartonMark === 'SP-OTHER' ? 'RT-OTHER' : 'RT-MIXED' })) + api.returnCandidates.mockResolvedValue([]) + api.stock.mockResolvedValue({ records: [{ skuCode: 'SKU-A', availableQuantity: 29 }, { skuCode: 'SKU-B', availableQuantity: 11 }] }) + api.cartons.mockResolvedValue({ records: sources }) + api.carton.mockImplementation(async (mark: string) => { const row = sources.find(row => row.cartonMark === mark)!; return { ...row, items: [{ skuCode: row.skuCode, quantity: row.quantity, availableQuantity: row.quantity }] } }) + await openCreate() + const type = wrapper.findComponent({ name: 'ElRadioGroup' }) + type.vm.$emit('update:modelValue', 'RETURN_OUTBOUND'); type.vm.$emit('change'); await flushPromises() + const customer = wrapper.findAllComponents(ElSelect).find(row => row.props('placeholder') === '请选择客户')! + customer.vm.$emit('update:modelValue', 'customer-a'); customer.vm.$emit('change'); await flushPromises() + const order = wrapper.findAllComponents(ElSelect).find(row => row.props('placeholder') === '选择已签收入库的退货单')! + order.vm.$emit('update:modelValue', 'RT-MIXED'); order.vm.$emit('change'); await flushPromises() + const mode = wrapper.findAllComponents({ name: 'ElRadioGroup' }).find(row => row.text().includes('单 SKU'))! + mode.vm.$emit('update:modelValue', 'SKU'); mode.vm.$emit('change'); await flushPromises() + const stock = wrapper.findAll('.return-list-card')[0]! + expect(stock.findAll('tbody tr').map(row => row.text().replace(/\s/g, ''))).toEqual(['SKU-A1', 'SKU-B5']) + stock.findAllComponents(ElCheckbox)[0]!.vm.$emit('change', true); await flushPromises() + const sourceTable = wrapper.findAll('.return-list-card')[1]! + expect(sourceTable.findAll('tbody tr')).toHaveLength(1) + expect(sourceTable.text()).toContain('SKU-A × 1') + sourceTable.findAllComponents(ElCheckbox)[0]!.vm.$emit('change', true); await flushPromises() + expect(wrapper.text()).toContain('所选散件来源可用 1 件') + mode.vm.$emit('update:modelValue', 'BOX'); mode.vm.$emit('change'); await flushPromises() + expect(wrapper.findAll('.return-list-card')[1]!.findAll('tbody tr')).toHaveLength(3) + }) beforeEach(() => { vi.mocked(listPlatformTenants).mockReset().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }) vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} }) diff --git a/src/modules/overseas-warehouse/pages/OutboundsView.vue b/src/modules/overseas-warehouse/pages/OutboundsView.vue index 2985199d..e7e21a5e 100644 --- a/src/modules/overseas-warehouse/pages/OutboundsView.vue +++ b/src/modules/overseas-warehouse/pages/OutboundsView.vue @@ -68,6 +68,7 @@ function transferCustomerName(row: WarehouseTransferOutboundCandidate) { const selectedReturnStock = computed(() => returnStockRows.value.map((item) => { if (returnPickMode.value !== 'SKU') return item const quantity = selectedReturnCartons.value.reduce((sum, carton) => { + if (carton.sourceUnitType !== 'PARCEL') return sum if (selectedReturnNo.value && !(carton.sourceReturnNo || '').split(',').includes(selectedReturnNo.value)) return sum const items = returnCartonDetails.value[carton.cartonMark]?.items return items?.length === 1 && items[0]?.skuCode === item.skuCode ? sum + Number(items[0].availableQuantity ?? items[0].quantity) : sum @@ -88,6 +89,7 @@ const filteredReturnCartons = computed(() => selectedReturnCartons.value.filter( if (returnPickMode.value === 'BOX' && item.sourceUnitType === 'PARCEL') return false if (selectedReturnNo.value && !(item.sourceReturnNo || returnCandidateByMark.value[item.cartonMark]?.returnNo || '').split(',').includes(selectedReturnNo.value)) return false if (returnPickMode.value === 'SKU') { + if (item.sourceUnitType !== 'PARCEL') return false const detail = returnCartonDetails.value[item.cartonMark] if (!selectedReturnSkus.value.length || detail?.items?.length !== 1 || detail.items[0]?.skuCode !== selectedReturnSkus.value[0]) return false } @@ -222,7 +224,7 @@ async function submit() { } if (!selectedReturnMarks.value.length) { ElMessage.warning(returnPickMode.value === 'SKU' - ? filteredReturnCartons.value.length ? '请在右侧选择该 SKU 的来源箱或散件记录' : '该退货单没有此 SKU 的可用单 SKU 箱或散件库存;混合箱请先拆箱' + ? filteredReturnCartons.value.length ? '请在右侧选择该 SKU 的散件来源记录' : '该退货单没有此 SKU 的可用散件库存;箱内库存请按箱/托出库' : '请选择出库箱号或托盘') return } @@ -346,7 +348,7 @@ onMounted(load) -

单 SKU 模式可填写全部或部分件数;已装箱货物由 PDA 扫箱唛按计划件数拣货,剩余数量保留在原箱。

所选来源可用 {{ selectedReturnQuantity }} 件;按来源箱号顺序分配箱 / 托单 SKU +

单 SKU 只展示该退货单的可用散件库存,可填写全部或部分件数,由 PDA 扫描 SKU 拣货。

所选散件来源可用 {{ selectedReturnQuantity }} 件箱 / 托单 SKU 按箱号出库按托盘出库
{{ code }}