diff --git a/src/api/return-counting.js b/src/api/return-counting.js new file mode 100644 index 0000000..93705e4 --- /dev/null +++ b/src/api/return-counting.js @@ -0,0 +1,20 @@ +import { overseasRequest } from './overseas.js' +const BASE = '/pda/v1/last-leg/return-inventory/counting' +export function getCurrentReturnCountTask() { + return overseasRequest('GET', `${BASE}/current`) +} +export function beginReturnCountTask(receiptNo) { + return overseasRequest('POST', `${BASE}/receipts/${encodeURIComponent(receiptNo)}/begin`) +} +export function scanReturnCountTasks(code) { + return overseasRequest('GET', `${BASE}/scan/${encodeURIComponent(String(code).trim())}`) +} +export function listReturnCountTasks({ page = 1, size = 10, keyword = '' } = {}) { + return overseasRequest('GET', `${BASE}/tasks?page=${page}&size=${size}&keyword=${encodeURIComponent(keyword.trim())}`) +} +export function scanReturnCountCarton(mark) { + return overseasRequest('GET', `${BASE}/cartons/${encodeURIComponent(String(mark).trim())}`) +} +export function confirmReturnCount(receiptNo, request) { + return overseasRequest('POST', `${BASE}/receipts/${encodeURIComponent(receiptNo)}`, request) +} diff --git a/src/data/overseas-workbench.js b/src/data/overseas-workbench.js index d99c613..ec649be 100644 --- a/src/data/overseas-workbench.js +++ b/src/data/overseas-workbench.js @@ -4,7 +4,8 @@ export const MODULES = [ { key: 'warehouseTransferList', name: '仓储转运测量列表', nameKey: 'modules.warehouseTransferList.name', icon: '列', iconType: 'list', description: '查看待测量的仓储转运单', descriptionKey: 'modules.warehouseTransferList.description', available: true, route: '/pages/warehouse-transfer/warehouse-transfer?mode=list' }, { key: 'warehouseTransferWork', name: '转运测量作业', nameKey: 'modules.warehouseTransferWork.name', icon: '转', iconType: 'compose', description: '进入转运单详情完成测量、打托和拍照', descriptionKey: 'modules.warehouseTransferWork.description', available: true, route: '/pages/warehouse-transfer/warehouse-transfer?mode=work' }, { key: 'returnReceipt', name: '退货入库', nameKey: 'modules.returnReceipt.name', icon: '退', iconType: 'undo', description: '扫描退货单或 SKU,按本包实收数量生成退货库存', descriptionKey: 'modules.returnReceipt.description', available: true, route: '/pages/return-receipt/return-receipt' }, - { key: 'returnPacking', name: '退货合箱', nameKey: 'modules.returnPacking.name', icon: '合', iconType: 'folder-add', description: '选择同客户已签收退货单,按 SKU 数量生成新箱', descriptionKey: 'modules.returnPacking.description', available: true, route: '/pages/return-packing/return-packing' } + { key: 'returnPacking', name: '退货合箱', nameKey: 'modules.returnPacking.name', icon: '合', iconType: 'folder-add', description: '选择同客户已签收退货单,按 SKU 数量生成新箱', descriptionKey: 'modules.returnPacking.description', available: true, route: '/pages/return-packing/return-packing' }, + { key: 'returnCounting', name: '退货箱内清点', icon: '清', iconType: 'list', description: '逐箱清点托盘签收货物,核对 SKU 和数量', available: true, route: '/pages/return-counting/return-counting' } ] }, { key: 'outbound', name: '出库履约', nameKey: 'modules.outbound.name', icon: '出', description: '排车拣货、出库任务与发车闭环', descriptionKey: 'modules.outbound.description', operations: [ { key: 'dispatchPicking', name: '排车拣货', nameKey: 'modules.dispatchPicking.name', icon: '车', iconType: 'paperplane', description: '按排车任务扫描托盘拣货,完成门禁后执行发车', descriptionKey: 'modules.dispatchPicking.description', available: true, route: '/pages/dispatch/dispatch' }, diff --git a/src/pages.json b/src/pages.json index 31803c6..efd3a9a 100644 --- a/src/pages.json +++ b/src/pages.json @@ -6,6 +6,7 @@ { "path": "pages/operation/operation", "style": { "navigationStyle": "custom", "navigationBarTitleText": "柜号入库与现场作业", "backgroundColor": "#F5F7FA" } }, { "path": "pages/warehouse-transfer/warehouse-transfer", "style": { "navigationStyle": "custom", "navigationBarTitleText": "仓储转运测量", "backgroundColor": "#F5F7FA" } }, { "path": "pages/return-receipt/return-receipt", "style": { "navigationStyle": "custom", "navigationBarTitleText": "退货入库", "backgroundColor": "#F5F7FA" } }, + { "path": "pages/return-counting/return-counting", "style": { "navigationStyle": "custom", "navigationBarTitleText": "退货箱内清点", "backgroundColor": "#F5F7FA" } }, { "path": "pages/return-packing/return-packing", "style": { "navigationStyle": "custom", "navigationBarTitleText": "退货合箱", "backgroundColor": "#F5F7FA" } }, { "path": "pages/pallet/pallet", "style": { "navigationStyle": "custom", "navigationBarTitleText": "托盘管理", "backgroundColor": "#F5F7FA" } }, { "path": "pages/stocktake/stocktake", "style": { "navigationStyle": "custom", "navigationBarTitleText": "库存盘点", "backgroundColor": "#F5F7FA" } }, diff --git a/src/pages/return-counting/return-counting.vue b/src/pages/return-counting/return-counting.vue new file mode 100644 index 0000000..cc28006 --- /dev/null +++ b/src/pages/return-counting/return-counting.vue @@ -0,0 +1,79 @@ + + + diff --git a/src/pages/return-receipt/return-receipt.vue b/src/pages/return-receipt/return-receipt.vue index 2fd2f72..73f81d5 100644 --- a/src/pages/return-receipt/return-receipt.vue +++ b/src/pages/return-receipt/return-receipt.vue @@ -36,7 +36,7 @@ 本单已完成打托{{ pallet.palletCode }}{{ pallet.skuCode }} · {{ pallet.quantity }} 件签收批次:{{ pallet.receiptBatchNo }} - 退货签收成功第 {{ index + 1 }} 箱 · {{ label.cartonMark }}{{ result.unitType === 'PALLET' ? '托盘标签' : '箱唛' }} · {{ result.cartonMark }}托盘:{{ result.palletCode }}签收单:{{ result.receiptNo }}计费批次:{{ result.receiptBatchNo }}{{ result.receivedQuantity }}/{{ result.expectedQuantity }}整单累计{{ result.fullyMatched ? '已完成' : '签收中' }}退货单状态 + 退货签收成功第 {{ index + 1 }} 箱 · {{ label.cartonMark }}{{ result.unitType === 'PALLET' ? '托盘标签' : '箱唛' }} · {{ result.cartonMark }}托盘:{{ result.palletCode }}签收单:{{ result.receiptNo }}计费批次:{{ result.receiptBatchNo }}{{ result.receivedQuantity }}/{{ result.expectedQuantity }}整单累计{{ result.fullyMatched ? '已完成' : '签收中' }}退货单状态 {{ error.message }}错误码:{{ error.code }}requestId:{{ error.requestId }}traceId:{{ error.traceId }} @@ -74,6 +74,7 @@ onShow() { if (!requireSession()) return; this.user = session.currentUser; restoreWarehouseWork(`${this.user.id}:${this.user.warehouseId}`); this.entryMode = 'SKU'; this.startScanner(); if (!this.receiptBatch && warehouseWork.returnReceipt.currentReturnNo && !this.current && !this.busy) this.openOrder(warehouseWork.returnReceipt.currentReturnNo, 'SKU', false) }, onHide() { this.stopScanner() }, onUnload() { this.stopScanner() }, beforeUnmount() { this.stopScanner() }, methods: { + goCount() { const mark = this.result?.labels?.[0]?.cartonMark; if (mark) uni.navigateTo({ url: '/pages/return-counting/return-counting?cartonMark=' + encodeURIComponent(mark) }) }, selectUnitType(type) { if (this.busy) return; this.unitType = type; this.unitCode = ''; this.boxRows = [{ items: {} }]; this.palletCheck = null; this.activeTarget = type === 'PALLET' ? 'pallet' : 'unit' }, resizeBoxRows(raw) { const count = Math.floor(Number(raw) || 0); if (count < 1 || count > 100) return; this.boxRows = Array.from({ length: count }, (_, index) => this.boxRows[index] || { items: {} }) }, setBoxQuantity(index, skuCode, raw) { const value = Math.max(0, Math.floor(Number(raw) || 0)); this.boxRows[index].items = { ...this.boxRows[index].items, [skuCode]: value } }, diff --git a/tests/return-counting-api.test.mjs b/tests/return-counting-api.test.mjs new file mode 100644 index 0000000..a3946c0 --- /dev/null +++ b/tests/return-counting-api.test.mjs @@ -0,0 +1,43 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { listReturnCountTasks, scanReturnCountCarton, scanReturnCountTasks, getCurrentReturnCountTask, beginReturnCountTask, confirmReturnCount } from '../src/api/return-counting.js' +import { session } from '../src/stores/session.js' + +test('counting preserves box contents and retry key without accepting client warehouse scope', async () => { + session.apiBase = '/api'; session.token = 'counting-test' + const calls = [] + globalThis.uni = { request(options) { calls.push(options); options.success({ statusCode: 200, data: { code: '200', data: { status: 'PENDING' } }, header: {} }) } } + await listReturnCountTasks({ page: 2, keyword: ' RC#2-1 ' }) + await scanReturnCountCarton('RC#2-1') + const request = { idempotencyKey: 'count-retry', boxes: [{ cartonMark: 'RC#2-1', items: [{ skuCode: 'SKU-1', quantity: 3 }] }, { cartonMark: 'RC#2-2', items: [{ skuCode: 'SKU-1', quantity: 7 }] }] } + await confirmReturnCount('RR/1', request) + assert.deepEqual(calls.map(row => [row.method, row.url]), [ + ['GET', '/api/pda/v1/last-leg/return-inventory/counting/tasks?page=2&size=10&keyword=RC%232-1'], + ['GET', '/api/pda/v1/last-leg/return-inventory/counting/cartons/RC%232-1'], + ['POST', '/api/pda/v1/last-leg/return-inventory/counting/receipts/RR%2F1'] + ]) + assert.deepEqual(calls[2].data, request) + assert.ok(calls.every(row => row.header.Authorization === 'Bearer counting-test')) +}) + +test('unified scan supports box and pallet codes and preserves the no-count empty result', async () => { + session.apiBase = '/api'; session.token = 'counting-test' + const urls = [] + globalThis.uni = { request(options) { urls.push(options.url); options.success({ statusCode: 200, data: { code: '200', data: [] }, header: {} }) } } + assert.deepEqual(await scanReturnCountTasks(' RC#2-1 '), []) + assert.deepEqual(await scanReturnCountTasks('PLT/1'), []) + assert.deepEqual(urls, ['/api/pda/v1/last-leg/return-inventory/counting/scan/RC%232-1', '/api/pda/v1/last-leg/return-inventory/counting/scan/PLT%2F1']) +}) + +test('current task preserves null and begin uses the operator-scoped claim route', async () => { + session.apiBase = '/api'; session.token = 'counting-test' + const calls = [] + globalThis.uni = { request(options) { calls.push(options); options.success({ statusCode: 200, data: { code: '200', data: options.method === 'GET' ? null : { receiptNo: 'RR/1', status: 'COUNTING' } }, header: {} }) } } + assert.equal(await getCurrentReturnCountTask(), null) + assert.equal((await beginReturnCountTask('RR/1')).status, 'COUNTING') + assert.deepEqual(calls.map(row => [row.method, row.url]), [ + ['GET', '/api/pda/v1/last-leg/return-inventory/counting/current'], + ['POST', '/api/pda/v1/last-leg/return-inventory/counting/receipts/RR%2F1/begin'] + ]) + assert.equal(calls[1].data, undefined) +}) diff --git a/tests/return-counting-page.test.mjs b/tests/return-counting-page.test.mjs new file mode 100644 index 0000000..f00a954 --- /dev/null +++ b/tests/return-counting-page.test.mjs @@ -0,0 +1,157 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { runInNewContext } from 'node:vm' + +const source = readFileSync(new URL('../src/pages/return-counting/return-counting.vue', import.meta.url), 'utf8') +const script = source.match(/