This commit is contained in:
2026-10-08 15:34:47 +08:00
parent 23572c125f
commit c20d5c53e1
13 changed files with 515 additions and 39 deletions
+2 -2
View File
@@ -176,8 +176,8 @@ test('return packing is a separate same-customer inventory operation', async ()
])
assert.match(pages, /pages\/return-packing\/return-packing/)
assert.match(workbench, /key: 'returnPacking'[^\n]*available: true[^\n]*route: '\/pages\/return-packing\/return-packing'/)
for (const action of ['findPackingOrders', 'mergeReturnCartons']) assert.match(page, new RegExp(action))
for (const path of ['/packing/orders', '/cartons/merge', '/cartons/']) assert.match(api, new RegExp(path.replaceAll('/', '\\/')))
for (const action of ['findPackingOrders', 'packReturnSkus']) assert.match(page, new RegExp(action))
for (const path of ['/packing/orders', '/cartons/pack', '/cartons/']) assert.match(api, new RegExp(path.replaceAll('/', '\\/')))
assert.match(page, /startScanBroadcast/)
assert.match(page, /customerKey[\s\S]*不同客户的退货单不能合箱/)
assert.match(page, /idempotencyKey: key\.value/)
+70
View File
@@ -0,0 +1,70 @@
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/ReturnBoxMergeWork.vue'
const source = readFileSync(new URL('../' + filename, import.meta.url), 'utf8')
const { descriptor } = parse(source)
test('box merge component compiles both its script and template', () => {
compileScript(descriptor, { id: 'box-merge' })
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 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 () => {
const {work,component}=instance(); work.choose('NEW')
assert.equal(work.sources.length,2)
assert.equal(work.active,'')
await work.acceptScan('A')
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)
assert.equal(work.plannedItems().reduce((sum,item)=>sum+item.quantity,0),5)
assert.equal(component.computed.canComplete.call(work),true)
})
test('existing box uses another box as target and never removes target inventory', async () => {
const {work,component}=instance(); work.choose('EXISTING'); await work.scanSource('A'); await work.scanTarget('B')
assert.equal(work.target.cartonMark,'B'); assert.equal(work.sources.length,1)
work.setDeduction('A','SKU1',1)
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)
assert.equal(work.plannedItems().length,0)
})
test('existing source can be replaced before merging, echoing the mark and clearing old deductions and target', async () => {
const {work,component}=instance()
work.choose('EXISTING'); await work.scanSource('A'); await work.scanTarget('B')
work.setDeduction('A','SKU1',1)
assert.equal(work.sourceCode,'A')
work.sourceCode='B'
assert.equal(component.computed.canComplete.call(work),false)
await work.scanSource('B')
assert.equal(work.sources.length,1)
assert.equal(work.sources[0].cartonMark,'B')
assert.equal(work.sourceCode,'B')
assert.equal(work.target,null)
assert.equal(work.targetCode,'')
assert.equal(work.plannedItems().length,0)
await work.scanTarget('A'); work.setDeduction('B','SKU1',1)
assert.equal(component.computed.canComplete.call(work),true)
})
test('a directly scanned box survives switching between existing and new targets', async () => {
const {work}=instance()
const box=work.plan.sources[0];work.plan=null;work.initialSource=box
work.choose('EXISTING')
assert.equal(work.sources[0].cartonMark,'A');assert.equal(work.sourceCode,'A')
work.choose('NEW')
assert.equal(work.sources[0].items.length,2)
work.choose('EXISTING')
assert.equal(work.sourceCode,'A');assert.equal(work.active,'target')
})
+24
View File
@@ -0,0 +1,24 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { defaultPackingBoxes, packingBoxPayload, validPackingAllocation } from '../src/utils/return-packing-allocation.js'
const measured = box => ({ ...box, length: '20', width: '30', height: '40', weight: '5' })
test('default one box per SKU preserves all planned quantities', () => {
const boxes = defaultPackingBoxes([{ skuCode: 'A', quantity: 7 }, { skuCode: 'B', quantity: 3 }, { skuCode: 'C', quantity: 0 }]).map(measured)
assert.equal(boxes.length, 2)
assert.equal(validPackingAllocation(boxes, { A: 7, B: 3 }), true)
assert.deepEqual(packingBoxPayload(boxes[0]).items, [{ skuCode: 'A', quantity: 7 }])
})
test('split and mixed boxes must equal the complete locked plan', () => {
const boxes = [measured({ quantities: { A: 2, B: 3 } }), measured({ quantities: { A: 5 } })]
assert.equal(validPackingAllocation(boxes, { A: 7, B: 3 }), true)
assert.equal(validPackingAllocation(boxes, { A: 8, B: 3 }), false)
assert.equal(validPackingAllocation(boxes, { A: 6, B: 3 }), false)
assert.equal(validPackingAllocation(boxes, { A: 7 }), false)
})
test('empty boxes, decimal quantities and invalid measurements block completion', () => {
assert.equal(validPackingAllocation([], { A: 1 }), false)
assert.equal(validPackingAllocation([measured({ quantities: {} })], {}), false)
assert.equal(validPackingAllocation([measured({ quantities: { A: 1.5 } })], { A: 1.5 }), false)
assert.equal(validPackingAllocation([{ ...measured({ quantities: { A: 1 } }), weight: '0' }], { A: 1 }), false)
})
+5
View File
@@ -11,6 +11,11 @@ test('packing preview keeps customer scope internal and exposes stable order ref
assert.equal(value.skus[0].availableQuantity, 8)
})
test('legacy single-customer preview is normalized to all customer orders', () => {
const value = normalizePackingPreview({ customerId: '100', orders: [{ returnNo: 'RT-1' }, { returnNo: 'RT-2' }], skus: [{ skuCode: 'SKU-1', availableQuantity: 8 }] })
assert.deepEqual(value.customers[0].orders.map((item) => item.returnNo), ['RT-1', 'RT-2'])
})
test('return packing API uses exact PDA inventory routes without submitting customer or warehouse scope', async () => {
const calls = []
session.apiBase = '/api'; session.token = 'packing-token'
+62
View File
@@ -0,0 +1,62 @@
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/pages/return-packing/return-packing.vue'
const { descriptor } = parse(readFileSync(new URL('../' + filename, import.meta.url), 'utf8'))
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() {},
updateWarehouseWork(domain, patch) { saved = {...saved,...patch} }, resetWarehouseDomain() { reset = true }, ...api
})
const page = {...component.data(), activePlan:{planNo:'P1'}, busy:true}
for (const [key, method] of Object.entries(component.methods)) page[key] = method.bind(page)
page.loadPlans = () => { page.plans = [{planNo:'P2'}] }
return {page, saved:()=>saved, reset:()=>reset}
}
test('packing completion template compiles without a putaway prerequisite', () => {
compileScript(descriptor, {id:'packing'})
assert.deepEqual(compileTemplate({source:descriptor.template.content,filename,id:'packing'}).errors,[])
assert.equal(descriptor.template.content.includes('确认上架'),false)
})
test('unified scan resolves an actual carton and passes its SKU contents into box work', async () => {
let skuCalls=0
const carton={cartonMark:'BOX1',customerId:'C1',items:[{skuCode:'SKU1',quantity:2}]}
const {page,saved}=instance({scanReturnCarton:async()=>carton,findPackingOrders:async()=>{skuCalls++;return {}}})
page.activePlan=null;page.busy=false;page.selectedOrders=[]
await page.search('BOX1')
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 () => {
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}]}]}}})
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)
queryError={code:'A0409',message:'箱库存被占用'}
await page.search('LOCKEDBOX')
assert.equal(skuCalls,1);assert.equal(page.error.code,'A0409')
})
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'}]})
assert.equal(page.result,null)
assert.equal(page.activePlan,null)
assert.equal(reset(),true)
assert.equal(page.plans[0].planNo,'P2')
})
test('new target retains labels for printing and can return without putaway', () => {
const {page,saved,reset}=instance()
page.packingCompleted({targetMode:'NEW',labels:[{cartonMark:'NEW#1-1'}]})
assert.equal(saved().result.labels[0].cartonMark,'NEW#1-1')
assert.equal(reset(),false)
page.busy=false; page.nextPacking()
assert.equal(page.result,null)
assert.equal(reset(),true)
})