Files
pda-overseas-uniapp/tests/warehouse-transfer-counting.test.mjs

303 lines
19 KiB
JavaScript

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/warehouse-transfer/warehouse-transfer.vue',import.meta.url),'utf8')
const script = source.match(/<script>([\s\S]*?)<\/script>/)[1].replace(/^import .+$/gm,'').replace('export default','page =')
function setup() {
const storage = new Map()
const scope = { CartonCountFields:{},PortalHeader:{},ScanTargetInput:{},session:{apiBase:'LOCAL',currentUser:{id:'OP',warehouseId:'WH'}},uni:{showModal: options=>options.success({confirm:true}),getStorageSync:key=>storage.get(key),setStorageSync:(key,value)=>storage.set(key,JSON.parse(JSON.stringify(value))),removeStorageSync:key=>storage.delete(key)} }
runInNewContext(script,scope)
const value = scope.page.data()
for(const [key,method] of Object.entries(scope.page.methods)) value[key]=method.bind(value)
for(const [key,get] of Object.entries(scope.page.computed)) Object.defineProperty(value,key,{get:()=>get.call(value)})
value.selected={transferNo:'WT1',packageCount:1,status:'WAIT_PDA_RECEIVING',items:[{id:'I1',skuCode:'A',quantity:4}],cartons:[{cartonMark:'BOX1',transferItemId:'I1',status:'EXPECTED'}]}
value.pallets=[{key:'P1',draftId:'P1',palletNo:'PALLET1',generated:true,bound:false}];value.restoreCartons();Object.assign(value.cartonBoxes[0],{length:10,width:20,height:30,weight:2});value.cartonBoxes[0].items[0].quantity=4
return {scope,value,storage}
}
test('deletion needs confirmation and cancel preserves box and measurements',async()=>{
const {value,scope}=setup(); let warning;
scope.uni.showModal=options=>{warning=options;options.success({confirm:false})};
await value.removeCarton(0);
assert.equal(value.cartonBoxes.length,1);assert.equal(value.cartonBoxes[0].weight,2);
assert.match(warning.content,/不计入本次实收/);assert.equal(warning.confirmText,'确认删除');
assert.equal(value.restorableCartons.length,0);assert.equal(value.deletingCarton,false);
})
test('deleted box survives reload and restores original identity, SKU, measurements and pallet',async()=>{
const {value}=setup(); await value.removeCarton(0);
assert.equal(value.cartonBoxes.length,0);assert.equal(value.restorableCartons.length,1);
value.restoreCartons();await value.restoreDeletedCarton();
const box=value.cartonBoxes[0];
assert.equal(box.cartonMark,'BOX1');assert.equal(box.weight,2);assert.equal(box.items[0].quantity,4);
assert.equal(box.transferPalletId,'P1');assert.equal(value.restorableCartons.length,0);
assert.equal(value.selected.packageCount,1);assert.equal(value.cartonConfirmed(box),false);
})
test('saved missing box can be restored after reopening count without increasing forecast',async()=>{
const {value,scope}=setup();
value.selected.cartons[0].status='NOT_RECEIVED';value.selected.cartons[0].countedAt='2026-10-16';
value.restoreCartons();assert.equal(value.cartonBoxes.length,0);
scope.reopenWarehouseTransferMeasurement=async()=>({...value.selected,cartons:value.selected.cartons.map(box=>({...box,countedAt:null}))});
await value.restoreDeletedCarton();
assert.equal(value.cartonsCounted,false);assert.equal(value.cartonBoxes[0].cartonMark,'BOX1');
assert.equal(value.selected.packageCount,1);assert.equal(value.restorableCartons.length,0);
value.cartonBoxes[0].weight=9;value.saveCartonDraft();value.restoreCartons();
assert.equal(value.cartonBoxes[0].cartonMark,'BOX1');assert.equal(value.cartonBoxes[0].weight,9);
})
test('previously deleted forecast box without an archive can still be restored',async()=>{
const {value}=setup();value.cartonBoxes=[];value.saveCartonDraft();value.restoreCartons();
assert.equal(value.restorableCartons.length,1);
await value.restoreDeletedCarton();
assert.equal(value.cartonBoxes[0].cartonMark,'BOX1');assert.equal(value.selected.packageCount,1);
})
test('summary toggle preserves measurements and volume uses three decimals',()=>{
const {value}=setup()
assert.equal(value.summaryCollapsed,false)
value.summaryCollapsed=true
assert.equal(value.cartonBoxes[0].weight,2)
Object.assign(value.cartonBoxes[0],{length:10,width:10,height:6})
value.addExtraCarton()
Object.assign(value.cartonBoxes[1],{length:10,width:10,height:6,weight:2})
assert.equal(value.palletVolume(value.pallets[0]),'0.001')
assert.equal(value.totalVolume,'0.001')
assert.match(source,/aria-label="添加 SKU"/)
assert.match(source,/aria-label="删除箱子"/)
})
test('remeasure preserves saved measurements and unlocks carton editing',async()=>{
const {value,scope}=setup();
const box={...value.cartonBoxes[0],countedAt:'2026-10-16',status:'COUNTED'};
value.selected.cartons=[box]; value.cartonRequestKey='old-key';
scope.reopenWarehouseTransferMeasurement=async()=>({...value.selected,cartons:[{...box,countedAt:null}]});
await value.remeasure();
assert.equal(value.cartonsCounted,false); assert.equal(value.cartonBoxes[0].weight,2);
assert.equal(value.cartonBoxes[0].transferPalletId,'P1'); assert.equal(value.cartonRequestKey,'');
assert.equal(value.cartonCountReady,true);
})
test('bound pallet deletion retains box values and requires reassignment',async()=>{
const {value,scope}=setup();
scope.uni.showModal=options=>options.success({confirm:true});
value.pallets[0].bound=true;
scope.deleteWarehouseTransferPallet=async()=>({...value.selected,pallets:[]});
await value.removePallet(0);
assert.equal(value.cartonBoxes[0].transferPalletId,''); assert.equal(value.cartonBoxes[0].weight,2);
assert.equal(value.generatedPallets.length,0); assert.equal(value.pallets.length,1);
assert.equal(value.cartonCountReady,false); assert.equal(value.cartonPalletFilter,'');
})
test('failed pallet deletion retains binding and measurements',async()=>{
const {value,scope}=setup();
scope.uni.showModal=options=>options.success({confirm:true});
scope.deleteWarehouseTransferPallet=async()=>{throw new Error('offline')};
await value.removePallet(0);
assert.equal(value.cartonBoxes[0].transferPalletId,'P1'); assert.equal(value.pallets[0].busy,false);
assert.equal(value.error.message,'offline');
})
test('transfer counts require valid pieces and measurements and record variance reason',()=>{
const {value}=setup();assert.equal(value.cartonCountReady,true)
value.cartonBoxes[0].items[0].quantity=0;assert.equal(value.cartonCountReady,false)
value.cartonBoxes[0].items[0].quantity=1.5;assert.equal(value.cartonCountReady,false)
value.cartonBoxes[0].items[0].quantity=4;value.cartonBoxes[0].height=0;assert.equal(value.cartonCountReady,false)
value.cartonBoxes[0].height=30;value.addExtraCarton();Object.assign(value.cartonBoxes[1],{length:10,width:20,height:30,weight:2});value.cartonBoxes[1].items[0].quantity=5
assert.equal(value.cartonCountReady,true);value.countDiscrepancyReason='多一箱';assert.equal(value.cartonCountReady,true)
assert.equal(value.cartonQuantity,9);assert.equal(value.cartonVolume,'0.012')
})
test('failed count unlocks the draft and each explicit retry sends current contents with a fresh key',async()=>{
const {value,scope}=setup();const requests=[]
scope.countWarehouseTransferCartons=async(no,request)=>{requests.push(request);throw new Error('offline')}
await value.confirmCartons();await value.confirmCartons()
assert.equal(requests.length,2);assert.notEqual(requests[0].idempotencyKey,requests[1].idempotencyKey)
value.cartonBoxes[0].items[0].quantity=8;await value.confirmCartons();assert.notEqual(requests[1].idempotencyKey,requests[2].idempotencyKey)
assert.equal(requests[2].boxes[0].items[0].quantity,8);assert.equal(value.cartonBoxes[0].items[0].quantity,8)
assert.equal(value.countingCartons,false);assert.equal(value.error.message,'offline')
})
test('draft restores additional cartons and discrepancy reason without crossing tasks',()=>{
const {value}=setup();value.addExtraCarton();value.countDiscrepancyReason='超收';value.saveCartonDraft();value.restoreCartons()
assert.equal(value.cartonBoxes.length,2);assert.equal(value.countDiscrepancyReason,'超收')
value.selected={...value.selected,transferNo:'WT2'};value.restoreCartons();assert.equal(value.cartonBoxes.length,1)
})
test('received count gates pallet completion and saving cannot switch active order',()=>{
const {value}=setup();assert.match(value.validationMessage,/先完成全部箱子/)
value.selected.cartons[0].countedAt='2026-10-15';value.cartonBoxes[0].transferPalletId='FOREIGN';assert.match(value.validationMessage,/已清点实收箱数/)
value.countingCartons=true;value.backToCandidates();assert.equal(value.selected.transferNo,'WT1')
})
test('batch applies only selected received boxes of one SKU and preserves independent drafts',()=>{
const {value}=setup()
value.selected.items.push({id:'I2',skuCode:'B'})
value.addExtraCarton();value.addExtraCarton();value.addExtraCarton()
value.cartonBoxes[2].items[0].skuCode='B'
value.cartonBoxes[3].received=false
value.selectBatchCartons(true)
assert.equal(value.batchTargetBoxes.length,2)
value.toggleBatchCarton(0)
Object.assign(value.batchBox,{length:32,width:24,height:18,weight:3.2})
value.batchBox.items[0].quantity=6
assert.equal(value.batchTotalQuantity,6)
value.applyBatchCartons()
assert.equal(value.cartonBoxes[0].items[0].quantity,4)
assert.equal(value.cartonBoxes[1].items[0].quantity,6)
assert.equal(value.cartonBoxes[2].items[0].quantity,'')
assert.equal(value.cartonBoxes[3].items[0].quantity,'')
value.batchBox.items[0].quantity=8
assert.equal(value.cartonBoxes[1].items[0].quantity,6)
value.restoreCartons()
assert.equal(value.cartonBoxes[1].items[0].quantity,6)
assert.equal(value.batchTargetBoxes.length,0)
value.changeBatchSku(1)
assert.equal(value.batchSku,'B')
assert.equal(value.batchBox.items[0].quantity,'')
})
test('batch guards invalid quantities, busy state, submitted facts and stale mixed SKU selections',()=>{
const {value}=setup();value.selectBatchCartons(true)
Object.assign(value.batchBox,{length:32,width:24,height:18,weight:3.2})
for(const quantity of [0,1.5,2147483648]){
value.batchBox.items[0].quantity=quantity;value.applyBatchCartons()
assert.equal(value.cartonBoxes[0].items[0].quantity,4)
}
value.batchBox.items[0].quantity=6;value.countingCartons=true;value.applyBatchCartons()
assert.equal(value.cartonBoxes[0].items[0].quantity,4)
value.countingCartons=false;value.cartonBoxes[0].items[0].skuCode='B'
assert.equal(value.batchTargetBoxes.length,0);value.applyBatchCartons()
assert.equal(value.cartonBoxes[0].items[0].quantity,4)
value.cartonBoxes[0].items[0].skuCode='A';value.selected.cartons[0].countedAt='now';value.applyBatchCartons()
assert.equal(value.cartonBoxes[0].items[0].quantity,4)
})
test('copy uses preceding received same SKU without sharing quantity objects or changing box identity',()=>{
const {value}=setup();value.addExtraCarton();value.addExtraCarton()
value.cartonBoxes[1].items[0].skuCode='B'
value.copyPreviousCarton(1);assert.equal(value.cartonBoxes[1].items[0].quantity,'')
const identity=value.cartonBoxes[2].localKey
value.copyPreviousCarton(2)
assert.equal(value.cartonBoxes[2].length,10)
assert.equal(value.cartonBoxes[2].items[0].quantity,4)
assert.equal(value.cartonBoxes[2].localKey,identity)
value.cartonBoxes[2].items[0].quantity=9
assert.equal(value.cartonBoxes[0].items[0].quantity,4)
value.cartonBoxes[0].received=false;assert.equal(value.previousMatchingCarton(2),undefined)
})
test('pallet totals follow actual box assignments and exclude missing boxes',()=>{
const {value}=setup();value.pallets.push({key:'P2',draftId:'P2',palletNo:'PALLET2',generated:true})
value.addExtraCarton();Object.assign(value.cartonBoxes[1],{length:40,width:20,height:10,weight:3,transferPalletId:'P2'});value.cartonBoxes[1].items[0].quantity=6
assert.equal(value.palletSummary(value.pallets[0]).cartonCount,1)
assert.equal(value.palletSummary(value.pallets[1]).volume,0.008)
assert.equal(value.totalWeight,'5.00')
value.assignCartonPallet(value.cartonBoxes[1],0)
assert.equal(value.palletSummary(value.pallets[0]).cartonCount,2)
assert.equal(value.palletSummary(value.pallets[1]).cartonCount,0)
value.cartonBoxes[1].received=false
assert.equal(value.totalWeight,'2.00')
value.cartonBoxes[0].transferPalletId='FOREIGN';assert.equal(value.cartonCountReady,false)
})
test('pallet number can be created before measurements and new boxes receive that identity',async()=>{
const {value,scope}=setup();const requests=[]
value.pallets=[{key:'NEW',generated:false,busy:false}];value.cartonBoxes[0].transferPalletId=''
const pallet={id:'CREATED',sequenceNo:1,status:'GENERATED',palletNo:'NEWPALLET'}
scope.saveWarehouseTransferPalletDraft=async(no,request)=>{requests.push(request);return {pallets:[{...pallet,status:'DRAFT'}]}}
scope.generateWarehouseTransferPallet=async()=>({pallets:[pallet]})
await value.generatePallet(value.pallets[0])
assert.equal(requests.length,1);assert.equal('lengthCm' in requests[0],false);assert.equal('grossWeightKg' in requests[0],false)
assert.equal(value.cartonBoxes[0].transferPalletId,'CREATED')
assert.equal(value.pallets[0].palletNo,'NEWPALLET')
})
// 最终完成入口需要区分无差异、取消、空白原因和网络重试。
for (const answer of [{confirm:false}, {confirm:true,content:' '}, {confirm:true,content:'少到一箱'}]) {
test('completion handles discrepancy answer '+JSON.stringify(answer),async()=>{
const {value,scope}=setup();let requests=[];let prompts=0
value.selected.items[0].quantity=5;value.selected.cartons[0].countedAt='2026-10-16'
value.pallets[0].bound=true;value.photoFileIds=[91]
scope.uni.showModal=options=>{prompts++;options.success(answer)}
scope.palletizeWarehouseTransfer=async(no,payload)=>{requests.push(payload);throw new Error('offline')}
await value.submit()
assert.equal(prompts,1);assert.equal(value.submitting,false)
assert.equal(requests.length,answer.confirm && answer.content?.trim()?1:0)
if(requests.length){assert.equal(requests[0].discrepancyReason,'少到一箱');await value.submit();assert.notEqual(requests[0].idempotencyKey,requests[1].idempotencyKey)}
})
}
test('matching SKU counts with different carton count submit without prompting, and missing photos block before prompting',async()=>{
const {value,scope}=setup();let prompts=0;let requests=[]
value.selected.packageCount=9;value.selected.cartons[0].countedAt='2026-10-16';value.pallets[0].bound=true
scope.uni.showModal=()=>{prompts++}
scope.palletizeWarehouseTransfer=async(no,payload)=>{requests.push(payload);throw new Error('offline')}
await value.submit();assert.equal(requests.length,0);assert.equal(prompts,0)
value.photoFileIds=[91];await value.submit();assert.equal(prompts,0);assert.equal(requests.length,1);assert.equal(requests[0].discrepancyReason,null)
})
// 逐箱确认收起、再次编辑及草稿恢复必须保留其他箱的确认状态。
test('individual confirmation collapses only that box and resumes from draft',async()=>{
const {value,scope}=setup();value.addExtraCarton()
Object.assign(value.cartonBoxes[1],{length:10,width:20,height:30,weight:2});value.cartonBoxes[1].items[0].quantity=5
let saves=0;scope.countWarehouseTransferCartons=async()=>{saves++;throw new Error('offline')}
await value.confirmSingleCarton(value.cartonBoxes[0])
assert.equal(value.cartonConfirmed(value.cartonBoxes[0]),true);assert.equal(value.allCartonsConfirmed,false);assert.equal(saves,0)
value.restoreCartons();assert.equal(value.cartonConfirmed(value.cartonBoxes[0]),true)
await value.editCarton(value.cartonBoxes[0]);assert.equal(value.cartonConfirmed(value.cartonBoxes[0]),false)
await value.confirmSingleCarton(value.cartonBoxes[0]);await value.confirmSingleCarton(value.cartonBoxes[1])
assert.equal(value.allCartonsConfirmed,true);assert.equal(saves,1);assert.equal(value.error.message,'offline')
value.cartonBoxes[0].weight=3;assert.equal(value.cartonConfirmed(value.cartonBoxes[0]),false);assert.equal(value.allCartonsConfirmed,false)
})
test('saved boxes restore collapsed and editing reopens only selected box',async()=>{
const {value,scope}=setup()
const first={...value.cartonBoxes[0],quantity:4,countedAt:'2026-10-16',status:'COUNTED'}
const second={...first,cartonMark:'BOX2'}
value.selected.cartons=[first,second];value.restoreCartons()
assert.equal(value.cartonBoxes.every(box=>value.cartonConfirmed(box)),true)
scope.reopenWarehouseTransferMeasurement=async()=>({...value.selected,cartons:value.selected.cartons.map(box=>({...box,countedAt:null}))})
await value.editCarton(value.cartonBoxes[0])
assert.equal(value.cartonsCounted,false);assert.equal(value.cartonConfirmed(value.cartonBoxes[0]),false);assert.equal(value.cartonConfirmed(value.cartonBoxes[1]),true)
value.restoreCartons();assert.equal(value.cartonConfirmed(value.cartonBoxes[1]),true)
})
test('failed reopening preserves confirmed data and invalid box cannot confirm',async()=>{
const {value,scope}=setup();value.cartonBoxes[0].weight=0
await value.confirmSingleCarton(value.cartonBoxes[0]);assert.equal(value.cartonConfirmed(value.cartonBoxes[0]),false)
value.cartonBoxes[0].weight=2;value.selected.cartons=[{...value.cartonBoxes[0],countedAt:'2026-10-16',status:'COUNTED'}];value.restoreCartons()
scope.reopenWarehouseTransferMeasurement=async()=>{throw new Error('offline')}
await value.editCarton(value.cartonBoxes[0]);assert.equal(value.cartonsCounted,true);assert.equal(value.cartonConfirmed(value.cartonBoxes[0]),true)
})
// 箱数变化不影响 SKU 门禁,异 SKU 正负差异不能按总件数抵消。
test('SKU differences compare each SKU and ignore carton count',()=>{
const {value}=setup();value.selected.packageCount=9
assert.equal(value.hasSkuReceiptDifference,false)
value.selected.items.push({id:'I2',skuCode:'B',quantity:3})
value.cartonBoxes[0].items=[{skuCode:'A',quantity:3},{skuCode:'B',quantity:4}]
assert.equal(value.cartonQuantity,value.forecastSkuQuantity)
assert.equal(value.skuReceiptDifferences.length,2)
value.cartonBoxes[0].items=[{skuCode:'A',quantity:4},{skuCode:'B',quantity:3}]
assert.equal(value.hasSkuReceiptDifference,false)
})
// 客户补箱后,新增预报箱进入PDA,原本机草稿和删除标记不丢失。
test('forecast additions merge into existing draft without losing edited boxes',async()=>{
const {value}=setup();value.cartonBoxes[0].weight=7;value.saveCartonDraft()
value.selected.packageCount=2;value.selected.cartons.push({cartonMark:'BOX2',transferItemId:'I1',status:'EXPECTED'})
value.restoreCartons();assert.equal(value.cartonBoxes.length,2);assert.equal(value.cartonBoxes[0].weight,7);assert.equal(value.cartonBoxes[1].cartonMark,'BOX2')
await value.removeCarton(0);value.restoreCartons();assert.equal(value.cartonBoxes.length,1);assert.equal(value.cartonBoxes[0].cartonMark,'BOX2')
})
test('count payload carries forecast version for stale snapshot rejection',async()=>{
const {value,scope}=setup();let payload
scope.countWarehouseTransferCartons=async(no,request)=>{payload=request;throw new Error('offline')}
await value.confirmCartons();assert.equal(payload.forecastPackageCount,1);assert.equal(payload.boxes[0].cartonMark,'BOX1')
})
test('新增预报箱无SKU绑定时恢复为可编辑箱,保留已有草稿', () => {
const { value } = setup()
value.selected.cartons.push({ cartonMark: 'NEW-UNBOUND', cartonIndex: 2, status: 'EXPECTED', transferItemId: null })
value.selected.packageCount = 2
value.restoreCartons()
const added = value.cartonBoxes.find(box => box.cartonMark === 'NEW-UNBOUND')
assert.ok(added)
assert.ok(value.declaredSkuCodes.includes(added.items[0].skuCode))
})