优化
This commit is contained in:
@@ -7,7 +7,7 @@ const body = source.split('<script>')[1].split('</script>')[0].replace(/^\s*impo
|
||||
function setup(confirm) {
|
||||
const calls = []
|
||||
const uni = { showModal(options) { calls.push(options); options.success({ confirm }) }, redirectTo(options) { calls.push(options) } }
|
||||
const component = new Function('PortalHeader', 'RelabelPalletWork', 'ScanTargetInput', 'uni', body)({}, {}, {}, uni)
|
||||
const component = new Function('PortalHeader', 'RelabelPalletWork', 'ScanTargetInput', 'ReturnRelabelPackWork', 'uni', body)({}, {}, {}, {}, uni)
|
||||
const context = { busy: false, stage: 'receive', taskCode: 'PREVIOUS', error: null, candidates: [{ relabelNo: 'RL-ACTIVE', taskStatus: 'PROCESSING', claimedByCurrentOperator: true }] }
|
||||
context.promptActiveTask = component.methods.promptActiveTask.bind(context)
|
||||
return { calls, context, open: () => component.methods.openExecution.call(context, { relabelNo: 'RL-OTHER', taskStatus: 'AVAILABLE' }) }
|
||||
@@ -34,7 +34,7 @@ test('cancel stays on list and preserves existing task selection', async () => {
|
||||
test('new relabel task asks whether to start before it can be claimed', async () => {
|
||||
const calls = []
|
||||
const uni = { showModal(options) { calls.push(options); options.success({ confirm: false }) } }
|
||||
const component = new Function('PortalHeader', 'RelabelPalletWork', 'ScanTargetInput', 'uni', body)({}, {}, {}, uni)
|
||||
const component = new Function('PortalHeader', 'RelabelPalletWork', 'ScanTargetInput', 'ReturnRelabelPackWork', 'uni', body)({}, {}, {}, {}, uni)
|
||||
const context = { busy: false }
|
||||
const confirmed = await component.methods.confirmStartTask.call(context, { relabelNo: 'RL-NEW' })
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
const source = await readFile(new URL('../src/pages/relabel/relabel.vue', import.meta.url), 'utf8')
|
||||
const body = source.split('<script>')[1].split('</script>')[0].replace(/^\s*import .*$/gm, '').replace('export default', 'return')
|
||||
const component = new Function('PortalHeader', 'RelabelPalletWork', 'ScanTargetInput', body)({}, {}, {})
|
||||
const component = new Function('PortalHeader', 'RelabelPalletWork', 'ScanTargetInput', 'ReturnRelabelPackWork', body)({}, {}, {}, {})
|
||||
function context() {
|
||||
const value = { current: { reviewedAt: null, history: [], items: [{ itemId: '1', operationType: 'CARTON_AND_SKU', expectedPackages: 1, skippedPackages: 0, exceptionPackages: 0, skuProcessedPackages: 0, cartonProcessedPackages: 1, skuItems: [{ skuCode: 'OLD', newSkuCode: 'NEW', quantity: 3 }, { skuCode: 'OTHER', newSkuCode: 'NEW2', quantity: 2 }] }] }, taskTab: 'sku' }
|
||||
for (const name of ['skuDone', 'skuDetails', 'remaining']) value[name] = component.methods[name].bind(value)
|
||||
|
||||
@@ -1,49 +1,75 @@
|
||||
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
|
||||
const api = await readFile(new URL('../src/api/relabel.js', import.meta.url), 'utf8')
|
||||
const page = await readFile(new URL('../src/pages/relabel/relabel.vue', import.meta.url), 'utf8')
|
||||
const palletWork = await readFile(new URL('../src/components/RelabelPalletWork.vue', import.meta.url), 'utf8')
|
||||
|
||||
test('PDA exposes the approved return bulk SKU completion endpoint', () => {
|
||||
assert.match(api, /completeReturnRelabelSkuTotal/)
|
||||
assert.match(api, /\/return-skus\/complete-total/)
|
||||
const source = await readFile(new URL('../src/pages/relabel/relabel.vue', import.meta.url), 'utf8')
|
||||
const helperSource = await readFile(new URL('../src/utils/return-relabel.js', import.meta.url), 'utf8')
|
||||
const helpers = await import('data:text/javascript;base64,' + Buffer.from(helperSource).toString('base64'))
|
||||
const body = source.split('<script>')[1].split('</script>')[0].replace(/^\s*import .*$/gm, '').replace('export default', 'return')
|
||||
function setup(confirm = false) {
|
||||
const calls = [], errors = []
|
||||
const uni = { showModal(options) { options.success({ confirm }) } }
|
||||
const component = new Function('PortalHeader', 'RelabelPalletWork', 'ScanTargetInput', 'ReturnRelabelPackWork', 'requiresReturnPack', 'returnPackFacts', 'completeReturnRelabelSku', 'warehouseIdempotencyKey', 'releaseWarehouseKey', 'uni', body)({}, {}, {}, {}, helpers.requiresReturnPack, helpers.returnPackFacts, async (no, payload) => { calls.push(payload); return value.current }, () => ({ value: 'key', signature: 'signature' }), () => {}, uni)
|
||||
const item = (id, mark) => ({ itemId: id, returnCartonId: id, cartonMark: mark, returnQuantity: 10, operationType: 'RETURN_BULK_SKU_LABEL', skuCode: 'SKU', newSkuCode: 'TARGET', expectedPackages: 4, relabelledPackages: 0, skippedPackages: 0, exceptionPackages: 0 })
|
||||
const value = { current: { relabelNo: 'RL', version: 1, items: [item('1', 'BOX-A'), item('2', 'BOX-B')], history: [] }, busy: false, allProcessed: true, remark: '', photoFileIds: ['photo'], returnCartonCode: '', verifiedReturnCarton: '', returnSkuCode: '', bulkQuantities: {}, returnTargetLabels: {}, setError(error) { errors.push(error.message) }, applyCurrent(result) { this.current = result }, async run(action, work) { await work() } }
|
||||
for (const [name, fn] of Object.entries(component.methods)) if (!value[name]) value[name] = fn.bind(value)
|
||||
for (const name of ['isReturnTask', 'isReturnBulk', 'returnBulkSources', 'returnBulkSkuGroups', 'requiresPalletBinding', 'palletTask', 'completionBlockReason']) Object.defineProperty(value, name, { get: component.computed[name].bind(value) })
|
||||
return { value, calls, errors }
|
||||
}
|
||||
test('SKU alone cannot register or allocate unscanned sources', async () => {
|
||||
const { value, calls, errors } = setup()
|
||||
value.verifyReturnSku('SKU'); assert.match(errors.pop(), /先确认来源/)
|
||||
value.verifyReturnCarton('BOX-A'); value.verifyReturnSku('SKU')
|
||||
assert.deepEqual(value.returnBulkSources.map(item => item.cartonMark), ['BOX-A'])
|
||||
value.returnTargetLabels['2'] = 'TARGET'; value.bulkQuantities['2'] = '4'
|
||||
await value.completeBulkSku(value.current.items[1]); assert.equal(calls.length, 0)
|
||||
})
|
||||
test('quantity is explicit, integral and source bounded; wrong target cannot write', async () => {
|
||||
const { value, calls } = setup(); value.verifyReturnCarton('BOX-A'); value.verifyReturnSku('SKU')
|
||||
const item = value.current.items[0]; value.returnTargetLabels['1'] = 'TARGET'
|
||||
for (const quantity of ['', '1.5', '5', '0']) { value.bulkQuantities['1'] = quantity; await value.completeBulkSku(item) }
|
||||
assert.equal(calls.length, 0)
|
||||
value.bulkQuantities['1'] = '2'; value.returnTargetLabels['1'] = 'WRONG'; await value.completeBulkSku(item); assert.equal(calls.length, 0)
|
||||
value.returnTargetLabels['1'] = 'TARGET'; await value.completeBulkSku(item)
|
||||
assert.deepEqual(calls, [{ itemId: '1', cartonMark: 'BOX-A', skuCode: 'SKU', quantity: 2, expectedVersion: 1, idempotencyKey: 'key' }])
|
||||
})
|
||||
test('all action needs confirmation and rescanning clears quantity and target', async () => {
|
||||
for (const confirm of [false, true]) { const { value } = setup(confirm); value.verifyReturnCarton('BOX-A'); await value.fillAllReturn(value.current.items[0]); assert.equal(value.bulkQuantities['1'], confirm ? '4' : undefined); value.verifyReturnCarton('BOX-B'); assert.deepEqual(value.bulkQuantities, {}); assert.deepEqual(value.returnTargetLabels, {}) }
|
||||
})
|
||||
test('normal return SKU requires source and explicit SKU pieces before server scan', async () => {
|
||||
const { value, errors } = setup(); const item = { ...value.current.items[0], operationType: 'SKU_LABEL', expectedPackages: 1, skuItems: [{ skuCode: 'SKU', newSkuCode: 'TARGET', quantity: 3 }] }
|
||||
value.current.items = [item]; value.pendingSkuRows = [{ item, sku: item.skuItems[0], index: 0 }]
|
||||
await value.submitSku('SKU'); assert.match(errors.pop(), /先扫描/)
|
||||
value.verifyReturnCarton('BOX-A'); value.quickSku = 'SKU'; await value.commitSku(value.pendingSkuRows[0]); assert.match(errors.pop(), /实际件数/)
|
||||
})
|
||||
test('packing uses SKU pieces instead of box count and preserves both sources', () => {
|
||||
const task = { items: [{ returnCartonId: '1', cartonMark: 'A', operationType: 'SKU_LABEL', expectedPackages: 1, returnQuantity: 8, skuItems: [{ skuCode: 'SKU', quantity: 3 }] }, { returnCartonId: '2', cartonMark: 'B', operationType: 'SKU_LABEL', expectedPackages: 1, returnQuantity: 2, skuItems: [{ skuCode: 'SKU', quantity: 2 }] }] }
|
||||
assert.deepEqual(helpers.returnDeductions(task), [{ cartonMark: 'A', skuCode: 'SKU', quantity: 3 }, { cartonMark: 'B', skuCode: 'SKU', quantity: 2 }]); assert.equal(helpers.requiresReturnPack(task), true)
|
||||
task.items[0].returnQuantity = 3; assert.equal(helpers.requiresReturnPack(task), false)
|
||||
})
|
||||
test('only truck requires return pallet binding and counts physical new boxes', () => {
|
||||
const { value } = setup(); for (const method of [null, 'EXPRESS', 'EXPRESS_PICKUP', 'TRUCK']) { value.current.deliveryMethod = method; assert.equal(value.requiresPalletBinding, method === 'TRUCK') }
|
||||
value.current.items.forEach(item => { item.cartonMark = 'NEW'; item.relabelledPackages = item.expectedPackages })
|
||||
assert.equal(value.palletTask.items.length, 1); assert.equal(value.palletTask.items[0].expectedPackages, 1)
|
||||
})
|
||||
test('pack recovery ignores previous rework round', () => {
|
||||
const task = { reviewedAt: 'ROUND2', history: [{ action: 'RETURN_PACK', detail: JSON.stringify({ round: 'INITIAL', cartonMark: 'OLD' }) }, { action: 'RETURN_PACK', detail: JSON.stringify({ round: 'ROUND2', cartonMark: 'NEW' }) }] }
|
||||
assert.deepEqual(helpers.returnPackFacts(task).map(row => row.cartonMark), ['NEW'])
|
||||
})
|
||||
|
||||
test('return relabel pallet binding validates pieces while binding physical carton marks', () => {
|
||||
assert.match(palletWork, /isReturnBulk\(\).*RETURN_BULK_SKU_LABEL/)
|
||||
assert.match(palletWork, /count === packages && this\.completedItems\(\)\.length === this\.eligibleItems\.length && this\.marks\.length === this\.eligibleItems\.length/)
|
||||
assert.match(palletWork, /this\.isReturnBulk \? this\.eligibleItems : this\.eligibleItems\.slice\(0, count\)/)
|
||||
assert.match(page, /requiredMarks[\s\S]*some\(\(mark\) => !marks\.has\(mark\)\)/)
|
||||
})
|
||||
|
||||
test('return relabel mode works by SKU remaining quantity without carton-mark scanning', () => {
|
||||
assert.match(page, /current\?\.operationType === 'RETURN_BULK_SKU_LABEL'/)
|
||||
assert.match(page, /退货贴标 SKU 明细/)
|
||||
assert.match(page, /name="returnSku"[\s\S]*@submit="verifyReturnSku"/)
|
||||
assert.match(page, /activeTarget === 'return-sku'[\s\S]*this\.verifyReturnSku\(value\)/)
|
||||
assert.match(page, /verifiedReturnSkuKey !== group\.key/)
|
||||
assert.match(page, /Number\.isSafeInteger\(quantity\)[\s\S]*quantity > group\.remaining/)
|
||||
assert.match(page, /returnBulkSkuGroups/)
|
||||
assert.match(page, /来源托盘拆箱数量/)
|
||||
assert.match(page, /登记本次贴标.*group\.remaining/)
|
||||
assert.match(page, /completeReturnRelabelSkuTotal/)
|
||||
assert.match(page, /taskTab === 'carton' && !isReturnBulk/)
|
||||
})
|
||||
|
||||
test('new relabel pallet is immediately copied into the target scan field', () => {
|
||||
assert.match(palletWork, /this\.targetCode = this\.newPallet\.palletCode \|\| ''/)
|
||||
assert.match(palletWork, /退货换标无需扫描箱唛/)
|
||||
})
|
||||
|
||||
test('return relabel distinguishes a whole pallet from a partial unpack', () => {
|
||||
assert.match(palletWork, /wholeSource\(\).*plannedPackages === Number\(this\.selectedSource\.stockPackages/)
|
||||
assert.match(palletWork, /eligibleItems\(\).*this\.isReturnBulk \? this\.sourceItems\(\) : this\.completedItems\(\)/)
|
||||
assert.match(palletWork, /this\.selectedDestination = this\.sourceItems\(\)\[0\]/)
|
||||
assert.match(palletWork, /this\.completedItems\(\)\.length !== this\.eligibleItems\.length/)
|
||||
assert.match(palletWork, /贴标数量等于本托盘全部/)
|
||||
assert.match(palletWork, /贴标数量少于本托盘库存/)
|
||||
assert.match(palletWork, /v-if="!isReturnBulk"[\s\S]*name="relabel-source"/)
|
||||
assert.match(palletWork, /await this\.run\(async \(\) => \{ const value = await resolvePallet\(row\.palletCode\)/)
|
||||
test('partial return completion requires current-round independent box then truck binding and photo', () => {
|
||||
const { value } = setup()
|
||||
value.current.items.forEach(item => { item.relabelledPackages = item.expectedPackages })
|
||||
value.current.relabelledPackages = 8
|
||||
value.current.deliveryMethod = 'EXPRESS'
|
||||
assert.match(value.completionBlockReason, /独立新箱/)
|
||||
value.current.history.push({ action: 'RETURN_PACK', detail: JSON.stringify({ round: 'INITIAL', cartonMark: 'NEW' }) })
|
||||
value.current.items.forEach(item => { item.cartonMark = 'NEW'; item.returnQuantity = 8 })
|
||||
assert.equal(value.completionBlockReason, '')
|
||||
value.current.deliveryMethod = 'TRUCK'
|
||||
assert.match(value.completionBlockReason, /组托/)
|
||||
value.current.history.push({ action: 'PALLET_BIND', detail: JSON.stringify({ round: 'INITIAL', cartonMarks: ['NEW'] }) })
|
||||
assert.equal(value.completionBlockReason, '')
|
||||
value.photoFileIds = []
|
||||
assert.match(value.completionBlockReason, /照片/)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
const page = await readFile(new URL('../src/pages/relabel/relabel.vue', import.meta.url), 'utf8')
|
||||
const helper = await readFile(new URL('../src/utils/return-relabel.js', import.meta.url), 'utf8')
|
||||
const { returnDeductions } = await import('data:text/javascript;base64,' + Buffer.from(helper).toString('base64'))
|
||||
test('return carton label path requires source confirmation before carton scan', () => {
|
||||
assert.match(page, /退货箱唛换标必须先确认同一来源包裹\/箱/)
|
||||
assert.match(page, /this\.taskTab === 'carton' \? 'quick-carton' : 'quick-sku'/)
|
||||
})
|
||||
test('mixed return carton keeps one physical package while SKU deductions use pieces', () => {
|
||||
const task={items:[{returnCartonId:'1',cartonMark:'P1',operationType:'CARTON_AND_SKU',returnQuantity:5,skuItems:[{skuCode:'S',quantity:5}]}]}
|
||||
assert.deepEqual(returnDeductions(task),[{cartonMark:'P1',skuCode:'S',quantity:5}])
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
const helper = await readFile(new URL('../src/utils/return-relabel.js', import.meta.url), 'utf8')
|
||||
const { requiresReturnPack, returnDeductions, returnPackFacts, returnSelections } = await import('data:text/javascript;base64,' + Buffer.from(helper).toString('base64'))
|
||||
const source = await readFile(new URL('../src/components/ReturnRelabelPackWork.vue', import.meta.url), 'utf8')
|
||||
const body = source.split('<script>')[1].split('</script>')[0].replace(/^\s*import .*$/gm, '').replace('export default', 'return')
|
||||
function setup() {
|
||||
const calls = [], events = []
|
||||
const component = new Function('requiresReturnPack','returnDeductions','returnPackFacts','returnSelections','packReturnRelabelCartons','warehouseIdempotencyKey','releaseWarehouseKey',body)(requiresReturnPack,returnDeductions,returnPackFacts,returnSelections,async(no,payload)=>{calls.push(payload);return {relabelNo:no}},()=>({value:'stable',signature:'s'}),()=>{})
|
||||
const item = (mark,done) => ({returnCartonId:mark,cartonMark:mark,operationType:'SKU_LABEL',expectedPackages:1,relabelledPackages:done,skippedPackages:0,exceptionPackages:0,skuItems:[{skuCode:'SKU',quantity:3}]})
|
||||
const value = {...component.data(),task:{relabelNo:'RL',items:[item('A',1),item('B',0)],history:[]},$emit:(...args)=>events.push(args)}
|
||||
for(const[name,fn]of Object.entries(component.methods))value[name]=fn.bind(value)
|
||||
for(const[name,fn]of Object.entries(component.computed))Object.defineProperty(value,name,{get:fn.bind(value)})
|
||||
return {value,calls,events}
|
||||
}
|
||||
test('packing cannot run with an unfinished source, missing measurement, skipped or exception fact',async()=>{
|
||||
const {value,calls}=setup(); await value.pack();assert.equal(calls.length,0)
|
||||
value.dimensions={length:'20',width:'30',height:'40',weight:'2'}
|
||||
assert.equal(value.ready,false);value.task.items[1].relabelledPackages=1;assert.equal(value.ready,true)
|
||||
value.task.items[0].skippedPackages=1;assert.equal(value.ready,false);value.task.items[0].skippedPackages=0
|
||||
value.task.items[0].exceptionPackages=1;assert.equal(value.ready,false)
|
||||
})
|
||||
test('pack sends every source with exact SKU piece deductions and generated target; saved history prevents repeat',async()=>{
|
||||
const {value,calls,events}=setup();value.task.items[1].relabelledPackages=1;value.dimensions={length:'20',width:'30',height:'40',weight:'2'}
|
||||
await value.pack()
|
||||
assert.deepEqual(calls,[{sourceCartonMarks:['A','B'],targetCartonMark:null,sourceDeductions:[{cartonMark:'A',skuCode:'SKU',quantity:3},{cartonMark:'B',skuCode:'SKU',quantity:3}],length:20,width:30,height:40,weight:2,idempotencyKey:'stable'}])
|
||||
assert.ok(events.some(row=>row[0]==='saved'))
|
||||
value.task.history=[{action:'RETURN_PACK',detail:JSON.stringify({round:'INITIAL',cartonMark:'NEW',quantity:6,label:{printableHtml:'<html/>'}})}]
|
||||
assert.equal(value.ready,false);assert.equal(value.facts[0].cartonMark,'NEW')
|
||||
})
|
||||
@@ -12,17 +12,17 @@ test('box merge component compiles both its script and template', () => {
|
||||
assert.deepEqual(compileTemplate({ source: descriptor.template.content, filename, id: 'box-merge' }).errors, [])
|
||||
})
|
||||
function instance() {
|
||||
const component = vm.runInNewContext(descriptor.script.content.replace(/^import .*$/gm, '').replace('export default', 'component ='), { ScanTargetInput: {}, component: null })
|
||||
const component = vm.runInNewContext(descriptor.script.content.replace(/^import .*$/gm, '').replace('export default', 'component ='), { ScanTargetInput: {}, ReturnLabelConfirmation: {}, component: null })
|
||||
const work = { ...component.data(), plan: {customerId:'C1',sources:[{cartonMark:'A',items:[{skuCode:'SKU1',quantity:2},{skuCode:'SKU2',quantity:3}]},{cartonMark:'B',items:[{skuCode:'SKU1',quantity:2}]}]}, $emit(){} }
|
||||
for (const [name, method] of Object.entries(component.methods)) work[name] = method.bind(work)
|
||||
work.save = () => {}
|
||||
return {work,component}
|
||||
}
|
||||
test('new plan box displays all sources without scanning and keeps independent SKU deductions', async () => {
|
||||
test('new plan requires scanning each source and keeps independent SKU deductions', async () => {
|
||||
const {work,component}=instance(); work.choose('NEW')
|
||||
assert.equal(work.sources.length,2)
|
||||
assert.equal(work.active,'')
|
||||
await work.acceptScan('A')
|
||||
assert.equal(work.sources.length,0)
|
||||
assert.equal(work.active,'source')
|
||||
await work.acceptScan('A'); await work.acceptScan('B')
|
||||
work.setDeduction('A','SKU1',1); work.setDeduction('A','SKU2',2); work.setDeduction('B','SKU1',2)
|
||||
work.dimensions={length:10,width:20,height:30,weight:1}
|
||||
assert.equal(work.sources.length,2)
|
||||
@@ -36,7 +36,7 @@ test('existing box uses another box as target and never removes target inventory
|
||||
assert.equal(work.plannedItems().length,1)
|
||||
assert.equal(work.target.items[0].quantity,2)
|
||||
assert.equal(component.computed.canComplete.call(work),true)
|
||||
work.choose('NEW'); assert.equal(work.target,null); assert.equal(work.sources.length,2)
|
||||
work.choose('NEW'); assert.equal(work.target,null); assert.equal(work.sources.length,0)
|
||||
assert.equal(work.plannedItems().length,0)
|
||||
})
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ function scanState() {
|
||||
open(row) { this.task = row }, $nextTick(callback) { callback() } }
|
||||
value.run = scope.page.methods.run.bind(value)
|
||||
value.enterTask = scope.page.methods.enterTask.bind(value)
|
||||
value.offerNextPalletTask = scope.page.methods.offerNextPalletTask.bind(value)
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -63,6 +64,35 @@ test('box and single-batch pallet scans claim and open counting directly without
|
||||
}
|
||||
})
|
||||
|
||||
test('completed pallet offers remaining batches and starts only after confirmation', async () => {
|
||||
for (const confirm of [false, true]) {
|
||||
const value = scanState(); value.task = null
|
||||
const next = { receiptNo: 'RR-NEXT', palletCode: 'PLT-1', status: 'PENDING' }
|
||||
scope.scanReturnCountTasks = async code => { assert.equal(code, 'PLT-1'); return [next, { ...next, receiptNo: 'RR-DONE', status: 'CONFIRMED' }, { ...next, palletCode: 'PLT-2' }] }
|
||||
scope.getCurrentReturnCountTask = async () => null
|
||||
let claimed = 0
|
||||
scope.beginReturnCountTask = async receiptNo => { claimed++; assert.equal(receiptNo, 'RR-NEXT'); return { ...next, status: 'COUNTING' } }
|
||||
value.ask = async options => { assert.match(options.content, /还有 1 项/); assert.equal(options.cancelText, '暂不清点'); return { confirm } }
|
||||
await value.offerNextPalletTask({ receiptNo: 'RR-DONE', palletCode: 'PLT-1' })
|
||||
assert.equal(claimed, confirm ? 1 : 0)
|
||||
assert.equal(value.task?.status || null, confirm ? 'COUNTING' : null)
|
||||
}
|
||||
})
|
||||
|
||||
test('no remaining pallet task skips prompt and claim conflicts remain visible', async () => {
|
||||
const value = scanState(); value.task = null
|
||||
value.ask = async () => assert.fail('no remaining task should not prompt')
|
||||
scope.scanReturnCountTasks = async () => []
|
||||
await value.offerNextPalletTask({ receiptNo: 'RR-DONE', palletCode: 'PLT-1' })
|
||||
scope.scanReturnCountTasks = async () => [{ receiptNo: 'RR-NEXT', palletCode: 'PLT-1', status: 'COUNTING' }]
|
||||
value.ask = async () => ({ confirm: true })
|
||||
scope.getCurrentReturnCountTask = async () => null
|
||||
scope.beginReturnCountTask = async () => { throw Object.assign(new Error('该任务正在由其他人员清点'), { code: 'A0409' }) }
|
||||
await value.run(() => value.offerNextPalletTask({ receiptNo: 'RR-DONE', palletCode: 'PLT-1' }))
|
||||
assert.equal(value.error.code, 'A0409')
|
||||
assert.equal(value.task, null)
|
||||
})
|
||||
|
||||
test('direct scan cannot claim a different task while the operator has unfinished work', async () => {
|
||||
const value = scanState()
|
||||
scope.scanReturnCountTasks = async () => [{ receiptNo: 'RR-NEW', returnNo: 'RT-NEW' }]
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import vm from 'node:vm'
|
||||
import { parse, compileScript, compileTemplate } from 'vue/compiler-sfc'
|
||||
const filename = 'src/components/ReturnLabelConfirmation.vue'
|
||||
const { descriptor } = parse(readFileSync(new URL('../' + filename, import.meta.url), 'utf8'))
|
||||
test('physical label confirmation compiles and requires its own barcode before calling the service', async () => {
|
||||
compileScript(descriptor, { id: 'return-label' })
|
||||
assert.deepEqual(compileTemplate({ source: descriptor.template.content, filename, id: 'return-label' }).errors, [])
|
||||
const calls = []
|
||||
const component = vm.runInNewContext(descriptor.script.content.replace(/^import .*$/gm, '').replace('export default', 'component ='), {
|
||||
component: null, ScanTargetInput: {}, confirmReturnLabel: async mark => calls.push(mark)
|
||||
})
|
||||
const label = { ...component.data(), cartonMark: 'RP-1' }
|
||||
for (const [name, method] of Object.entries(component.methods)) label[name] = method.bind(label)
|
||||
await label.confirm('RP-OTHER'); assert.equal(calls.length, 0); assert.equal(label.confirmed, false)
|
||||
await label.acceptScan('RP-1'); assert.deepEqual(calls, ['RP-1']); assert.equal(label.confirmed, true)
|
||||
await label.confirm('RP-1'); assert.equal(calls.length, 1)
|
||||
})
|
||||
@@ -9,7 +9,7 @@ const { descriptor } = parse(readFileSync(new URL('../' + filename, import.meta.
|
||||
function instance(api = {}) {
|
||||
let saved = {}, reset = false
|
||||
const component = vm.runInNewContext(descriptor.script.content.replace(/^\s*import .*$/gm, '').replace('export default', 'component ='), {
|
||||
component: null, PortalHeader: {}, ScanTargetInput: {}, ReturnBoxMergeWork: {}, session: {currentUser:{}}, playSuccessSound() {},
|
||||
component: null, ReturnLabelConfirmation: {}, PortalHeader: {}, ScanTargetInput: {}, ReturnBoxMergeWork: {}, session: {currentUser:{}}, playSuccessSound() {},
|
||||
updateWarehouseWork(domain, patch) { saved = {...saved,...patch} }, resetWarehouseDomain() { reset = true }, ...api
|
||||
})
|
||||
const page = {...component.data(), activePlan:{planNo:'P1'}, busy:true}
|
||||
@@ -32,17 +32,19 @@ test('unified scan resolves an actual carton and passes its SKU contents into bo
|
||||
assert.equal(page.mode,'BOX');assert.equal(page.initialSource,carton)
|
||||
assert.equal(saved().initialSource.items[0].quantity,2);assert.equal(skuCalls,0)
|
||||
})
|
||||
test('only carton not-found falls back to customer SKU stock; conflicts do not fall back', async () => {
|
||||
test('unknown SKU cannot silently choose parcel sources; occupied sources retain the conflict', async () => {
|
||||
let skuCalls=0, queryError={code:'A0404'}
|
||||
const {page}=instance({scanReturnCarton:async()=>{throw queryError},findPackingOrders:async()=>{skuCalls++;return {customers:[{customerId:'C1',orders:[{returnNo:'R1'}],skus:[{skuCode:'SKU1',availableQuantity:3}]}]}}})
|
||||
const {page}=instance({scanReturnCarton:async()=>{throw queryError},findPackingOrders:async()=>{skuCalls++;return {}}})
|
||||
page.activePlan=null;page.busy=false;page.selectedOrders=[];page.searchCode=''
|
||||
page.setError=error=>{page.error=error}
|
||||
await page.search('SKU1')
|
||||
assert.equal(page.mode,'SKU');assert.equal(page.selectedCustomer.skus[0].availableQuantity,3)
|
||||
assert.match(page.error.message,/扫描来源包裹/)
|
||||
assert.equal(skuCalls,0)
|
||||
queryError={code:'A0409',message:'箱库存被占用'}
|
||||
await page.search('LOCKEDBOX')
|
||||
assert.equal(skuCalls,1);assert.equal(page.error.code,'A0409')
|
||||
await page.search('BOX1')
|
||||
assert.equal(page.error.message,'箱库存被占用');assert.equal(skuCalls,0)
|
||||
})
|
||||
|
||||
test('existing target immediately clears work and returns to refreshed plans even during completion', () => {
|
||||
const {page,reset}=instance()
|
||||
page.packingCompleted({targetMode:'EXISTING',labels:[{cartonMark:'B'}]})
|
||||
|
||||
@@ -4,7 +4,7 @@ import { readFileSync } from 'node:fs'
|
||||
import vm from 'node:vm'
|
||||
|
||||
function setup(confirm, saved = new Map(), scope = 'operator:warehouse') {
|
||||
const context = vm.createContext({ reactive: value => value, PortalHeader: {}, ScanTargetInput: {},
|
||||
const context = vm.createContext({ reactive: value => value, ReturnLabelConfirmation: {}, PortalHeader: {}, ScanTargetInput: {},
|
||||
playSuccessSound() {}, playErrorSound() {}, confirmReturnReceipt: confirm,
|
||||
uni: { setStorageSync: (key, value) => saved.set(key, structuredClone(value)), getStorageSync: key => saved.get(key), removeStorageSync: key => saved.delete(key) } })
|
||||
const store = readFileSync(new URL('../src/stores/warehouse-work.js', import.meta.url), 'utf8')
|
||||
@@ -20,13 +20,14 @@ function setup(confirm, saved = new Map(), scope = 'operator:warehouse') {
|
||||
palletCode: ' PLT-TEST ', palletLength: 10, palletWidth: 10, palletHeight: 10, palletWeight: 1,
|
||||
quantity: () => 2, boxCount: () => 1
|
||||
})
|
||||
Object.defineProperty(instance, 'receiptBatch', { get: () => vm.runInContext('warehouseWork.returnReceipt.pendingBatch', context) })
|
||||
return instance
|
||||
}
|
||||
|
||||
test('SKU lookup displays all pending order lines while retaining scanned SKU for pallet generation', () => {
|
||||
const source = readFileSync(new URL('../src/pages/return-receipt/return-receipt.vue', import.meta.url), 'utf8')
|
||||
const script = source.match(/<script[^>]*>([\s\S]*?)<\/script>/)[1]
|
||||
const component = vm.runInNewContext(script.replace(/^\s*import .*$/mg, '').replace('export default', 'globalThis.component ='), { PortalHeader: {}, ScanTargetInput: {} })
|
||||
const component = vm.runInNewContext(script.replace(/^\s*import .*$/mg, '').replace('export default', 'globalThis.component ='), { ReturnLabelConfirmation: {}, PortalHeader: {}, ScanTargetInput: {} })
|
||||
const lines = [{ skuCode: 'SKU-A', expectedQuantity: 6 }, { skuCode: 'SKU-B', expectedQuantity: 4 }]
|
||||
const instance = { current: { expectedItems: lines }, selectedSku: 'SKU-A' }
|
||||
instance.selectedItems = component.computed.selectedItems.call(instance)
|
||||
@@ -59,14 +60,14 @@ test('PALLET page submits the required palletCode through the actual confirmatio
|
||||
|
||||
test('parcel signing allows an empty code but still rejects quantities above the remaining amount', async () => {
|
||||
const source = readFileSync(new URL('../src/pages/return-receipt/return-receipt.vue', import.meta.url), 'utf8')
|
||||
const component = vm.runInNewContext(source.match(/<script[^>]*>([\s\S]*?)<\/script>/)[1].replace(/^\s*import .*$/mg, '').replace('export default', 'globalThis.component ='), { PortalHeader: {}, ScanTargetInput: {} })
|
||||
const state = { current: {}, selectedItems: [{ skuCode: 'A' }], receiptQuantity: 2, unitType: 'PARCEL', unitCode: '', quantity: () => 2, remaining: () => 3 }
|
||||
const component = vm.runInNewContext(source.match(/<script[^>]*>([\s\S]*?)<\/script>/)[1].replace(/^\s*import .*$/mg, '').replace('export default', 'globalThis.component ='), { ReturnLabelConfirmation: {}, PortalHeader: {}, ScanTargetInput: {} })
|
||||
const state = { current: {}, selectedItems: [{ skuCode: 'A' }], receiptQuantity: 2, unitType: 'PARCEL', boxRows: [{ items: { A: 2 } }], remaining: () => 3 }
|
||||
assert.equal(component.computed.canConfirm.call(state), true)
|
||||
state.quantity = () => 4
|
||||
state.boxRows[0].items.A = 4
|
||||
assert.equal(component.computed.canConfirm.call(state), false)
|
||||
const calls = []
|
||||
const page = setup(async payload => { calls.push(payload); return { unitType: 'PARCEL' } })
|
||||
page.unitType = 'PARCEL'; page.unitCode = ''; page.trackingNo = ''
|
||||
page.unitType = 'PARCEL'; page.unitCode = ''; page.trackingNo = ''; page.boxRows = [{ items: { 'SKU-TEST': 2 } }]
|
||||
await page.confirmReceipt()
|
||||
assert.equal(calls.length, 1)
|
||||
assert.equal(calls[0].trackingNo, undefined)
|
||||
@@ -77,13 +78,72 @@ test('parcel signing allows an empty code but still rejects quantities above the
|
||||
test('parcel waybill is submitted as trackingNo and leaves unitCode for the server', async () => {
|
||||
const calls = []
|
||||
const page = setup(async payload => { calls.push(payload); return { unitType: 'PARCEL' } })
|
||||
page.unitType = 'PARCEL'; page.unitCode = ' sf561626 '; page.trackingNo = 'stale-from-pallet'
|
||||
page.unitType = 'PARCEL'; page.unitCode = ' sf561626 '; page.trackingNo = 'stale-from-pallet'; page.boxRows = [{ items: { 'SKU-TEST': 2 } }]
|
||||
await page.confirmReceipt()
|
||||
assert.equal(calls[0].trackingNo, 'sf561626')
|
||||
assert.equal(calls[0].unitCode, undefined)
|
||||
assert.equal(calls[0].palletCode, undefined)
|
||||
})
|
||||
|
||||
test('batch waybill scans stay on the single parcel waybill input', () => {
|
||||
const page = setup(async () => ({}))
|
||||
page.unitType = 'PARCEL'; page.activeTarget = 'unit'; page.palletCode = ''
|
||||
page.handleScan('WAYBILL-ONE')
|
||||
assert.equal(page.unitCode, 'WAYBILL-ONE'); assert.equal(page.palletCode, '')
|
||||
assert.equal(page.activeTarget, 'unit')
|
||||
page.handleScan('WAYBILL-CORRECTED')
|
||||
assert.equal(page.unitCode, 'WAYBILL-CORRECTED')
|
||||
})
|
||||
|
||||
test('parcel rows must be nonempty and summed quantities cannot exceed the order remainder', () => {
|
||||
const source = readFileSync(new URL('../src/pages/return-receipt/return-receipt.vue', import.meta.url), 'utf8')
|
||||
const component = vm.runInNewContext(source.match(/<script[^>]*>([\s\S]*?)<\/script>/)[1].replace(/^\s*import .*$/mg, '').replace('export default', 'globalThis.component ='), { ReturnLabelConfirmation: {}, PortalHeader: {}, ScanTargetInput: {} })
|
||||
const state = { current: {}, selectedItems: [{ skuCode: 'A' }], receiptQuantity: 4, unitType: 'PARCEL', boxRows: [{ items: { A: 2 } }, { items: { A: 2 } }], remaining: () => 3 }
|
||||
assert.equal(component.computed.canConfirm.call(state), false)
|
||||
state.remaining = () => 5
|
||||
assert.equal(component.computed.canConfirm.call(state), true)
|
||||
state.boxRows[1].items = {}
|
||||
assert.equal(component.computed.canConfirm.call(state), false)
|
||||
})
|
||||
|
||||
test('one submission signs multiple mixed-SKU parcels sharing one waybill and independent labels', async () => {
|
||||
const calls = []
|
||||
const page = setup(async request => {
|
||||
calls.push(JSON.parse(JSON.stringify(request)))
|
||||
return { unitType: 'PARCEL', labels: [{ cartonMark: `RP-${calls.length}`, printableHtml: 'label' }], fullyMatched: false }
|
||||
})
|
||||
page.unitType = 'PARCEL'; page.unitCode = ' WAYBILL-BATCH '; page.trackingNo = 'stale'; page.selectedItems = [{ skuCode: 'SKU-TEST' }, { skuCode: 'SKU-B' }]
|
||||
page.boxRows = [{ items: { 'SKU-TEST': 1, 'SKU-B': 2 } }, { items: { 'SKU-TEST': 3 } }]
|
||||
await page.confirmReceipt()
|
||||
assert.equal(calls.length, 2)
|
||||
assert.deepEqual(calls[0].items, [{ skuCode: 'SKU-TEST', receivedQuantity: 1 }, { skuCode: 'SKU-B', receivedQuantity: 2 }])
|
||||
assert.deepEqual(calls[1].items, [{ skuCode: 'SKU-TEST', receivedQuantity: 3 }])
|
||||
assert.equal(calls[0].trackingNo, 'WAYBILL-BATCH'); assert.equal(calls[1].trackingNo, 'WAYBILL-BATCH')
|
||||
assert.notEqual(calls[0].idempotencyKey, calls[1].idempotencyKey)
|
||||
assert.deepEqual(Array.from(page.result.labels, row => row.cartonMark), ['RP-1', 'RP-2'])
|
||||
})
|
||||
|
||||
test('failed multi-parcel batch resumes immutable requests without re-signing a successful parcel', async () => {
|
||||
const saved = new Map(), calls = []
|
||||
let failSecond = true
|
||||
const confirm = async request => {
|
||||
calls.push(JSON.parse(JSON.stringify(request)))
|
||||
if (request.items[0].receivedQuantity === 2 && failSecond) throw new Error('response lost')
|
||||
return { unitType: 'PARCEL', labels: [{ cartonMark: `RP-${request.items[0].receivedQuantity}`, printableHtml: 'label' }], fullyMatched: false }
|
||||
}
|
||||
const page = setup(confirm, saved)
|
||||
page.unitType = 'PARCEL'; page.unitCode = 'WAYBILL-BATCH'; page.boxRows = [{ items: { 'SKU-TEST': 1 } }, { items: { 'SKU-TEST': 2 } }]
|
||||
await page.confirmReceipt()
|
||||
assert.equal(page.receiptBatch.entries[0].result.labels[0].cartonMark, 'RP-1')
|
||||
failSecond = false
|
||||
const restored = setup(confirm, saved)
|
||||
await restored.confirmReceipt()
|
||||
assert.equal(calls.length, 3)
|
||||
assert.equal(calls[1].idempotencyKey, calls[2].idempotencyKey)
|
||||
assert.deepEqual(calls[1], calls[2])
|
||||
assert.equal(restored.result.labels.length, 2)
|
||||
})
|
||||
|
||||
test('partial receipt continues the same order with fresh pallet measurements', async () => {
|
||||
const page = setup(async () => ({}))
|
||||
page.finishReceipt([{ fullyMatched: false }])
|
||||
|
||||
@@ -91,7 +91,7 @@ test('carton relabel uses one scan input and shows the confirmed FBA mapping aft
|
||||
assert.match(scan, /confirmedItem\?\.fbaCartonMark/)
|
||||
})
|
||||
|
||||
test('mixed relabel keeps SKU work open and return bulk also mounts pallet work', async () => {
|
||||
test('mixed relabel keeps SKU work open and return transport gates pallet work', async () => {
|
||||
const page = await read('../src/pages/relabel/relabel.vue')
|
||||
const body = page.match(/remaining\(item\) \{([^\n]+)\},/)[1]
|
||||
const remaining = new Function('item', body)
|
||||
@@ -99,7 +99,7 @@ test('mixed relabel keeps SKU work open and return bulk also mounts pallet work'
|
||||
cartonProcessedPackages: 1, skuProcessedPackages: 0, skippedPackages: 0, exceptionPackages: 0 }
|
||||
assert.equal(remaining.call({ taskTab: 'sku' }, mixed), 1)
|
||||
assert.equal(remaining.call({ taskTab: 'carton' }, mixed), 0)
|
||||
assert.match(page, /<RelabelPalletWork v-if="taskTab === 'carton' \|\| isReturnBulk"/)
|
||||
assert.match(page, /<RelabelPalletWork v-if="requiresPalletBinding && \(taskTab === 'carton' \|\| isReturnTask\)"/)
|
||||
})
|
||||
|
||||
test('relabel list confirms before starting a new task and completion returns to the list', async () => {
|
||||
|
||||
Reference in New Issue
Block a user