diff --git a/AGENTS.md b/AGENTS.md index 8316ba2..275c464 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,7 +28,7 @@ - 工作台定义:`src/data/overseas-workbench.js`。 - 登录与会话:`src/api/auth.js`、`src/stores/session.js`。 - 扫码设备层:`src/utils/scan-broadcast.js`;广播 Action=`android.intent.action.SCANRESULT`,Extra=`value`。 -- 入仓作业已接柜号级真实海外 API:账号锁柜、三类照片签收、统一箱码定位入仓码、FBA 按箱数收货、OVERSEAS 逐箱收货并逐入仓码核平;库内盘点、移库、托盘管理、换标收货/执行/复核以及库存/库位/托盘查询均已接入专用 PDA API。 +- 入仓作业已接柜号级真实海外 API:账号锁柜、三类照片签收、统一箱码定位入仓码、普通 FBA 按箱数收货、普通 OVERSEAS 逐箱收货、一件代发按国内封托来源托盘整托收货,并逐入仓码核平;库内盘点、移库、托盘管理、换标收货/执行/复核以及库存/库位/托盘查询均已接入专用 PDA API。 ## 页面与能力快速索引 @@ -55,7 +55,7 @@ ## 海外业务接口边界 -- `src/api/inbound.js` 已接入待卸柜候选、柜详情/prepare、current 恢复、三类照片签收、统一箱码定位入仓码、实物托盘解析、FBA bulk/OVERSEAS carton 收货及整柜完成;FBA bulk 不提交 `skuCode`,库存分配由服务端按计划明细完成;响应与 404/409 判定集中在 `src/api/receiving-adapter.js`,双 Token 刷新与海外 401 单次重放由 `src/api/overseas.js` 统一处理。 +- `src/api/inbound.js` 已接入待卸柜候选、柜详情/prepare、current 恢复、三类照片签收、统一箱码定位入仓码、实物托盘解析、普通 FBA bulk/普通 OVERSEAS carton、一件代发 source pallet 整托收货及整柜完成;FBA bulk 不提交 `skuCode`,库存分配由服务端按计划明细完成;一件代发只提交国内冻结的来源托盘号,不允许逐箱或拆托收货。响应与 404/409 判定集中在 `src/api/receiving-adapter.js`,双 Token 刷新与海外 401 单次重放由 `src/api/overseas.js` 统一处理。 - 入仓摘要、箱码解析和实收响应的 `assigned/mergeAllowed/urgent/mergeHint` 只消费 Lastleg 返回值,布尔标志只有 `true` 生效,`mergeAllowed` 缺省为 `false`;合托仅作提示,不能改变收货资格或触发分配。首扫实物托盘只确认服务端已有托盘、库位与资源锁预留,不在设备端重新分配托盘、库位或资源锁;只提交业务箱码、托盘码及当前收货数量,最终资格由服务端决定。 - `src/api/overseas.js` 的 `overseasUrl` 统一拼接普通请求、上传和下载地址:先移除运行时 `apiBase` 末尾斜杠;当基址已以 `/api` 或 `/overseas-api` 结尾时,再移除业务路径重复的 `/api` 前缀。各业务 API 不得自行拼接 `session.apiBase`,也不得依赖双斜杠或重复 `/api` 被网关容错。 - 待卸柜候选只消费服务端已按 Token 绑定仓过滤的结果,并严格校验每柜正式 `overseasWarehouseCode`、`overseasWarehouseName` 与 `warehouse-profile` 一致;字段缺失或仓不一致一律 fail-closed,不得从 FBA/业务目的仓或内部 ID 推导。候选保留服务端返回的未齐全柜并严格消费 `allPlansReady`、`operable`、`blockReason`、`plannedArrivalAt` 与柜级计划统计;`operable=false` 只能查看只读摘要,不得调用 prepare。候选顺序沿用服务端“可操作优先、同组计划到仓时间升序、时间为空最后”的排序,前端不得用入仓码/计划数组长度自行推导就绪或把预计到港冒充计划到仓。 diff --git a/src/api/inbound.js b/src/api/inbound.js index 307975d..9a45d8b 100644 --- a/src/api/inbound.js +++ b/src/api/inbound.js @@ -24,4 +24,5 @@ export const resolveCurrentCarton = async (cartonMark) => normalizeReceivingInbo export const resolveCurrentPallet = (palletCode, inboundCode) => overseasRequest('POST', current('/pallets/resolve'), { palletCode, inboundCode }) export const receiveCurrentBulk = async (request) => normalizeReceivingAssignment(await overseasRequest('POST', current('/receipts/bulk'), request)) export const receiveCurrentCarton = async (request) => normalizeReceivingAssignment(await overseasRequest('POST', current('/receipts/carton'), request)) +export const receiveCurrentWholePallet = async (request) => normalizeReceivingAssignment(await overseasRequest('POST', current('/receipts/pallet'), request)) export const completeCurrentReceiving = async (request) => normalizeReceivingSession(await overseasRequest('POST', current('/complete'), request)) diff --git a/src/api/receiving-adapter.js b/src/api/receiving-adapter.js index 20a304d..2b91063 100644 --- a/src/api/receiving-adapter.js +++ b/src/api/receiving-adapter.js @@ -39,6 +39,7 @@ export function assertReceivingWarehouse(value, profile, label = '当前柜') { export function normalizeReceivingInbound(value = {}) { return { ...normalizeReceivingAssignment(value), + wholePalletInbound: value.wholePalletInbound === true, plannedPalletCount: number(value.plannedPalletCount), totalPackages: number(value.totalPackages), receivedPackages: number(value.receivedPackages), diff --git a/src/pages/operation/operation.vue b/src/pages/operation/operation.vue index 5447603..ef133b8 100644 --- a/src/pages/operation/operation.vue +++ b/src/pages/operation/operation.vue @@ -66,6 +66,13 @@ + 本次已收 {{ lastReceipt.acceptedPackages }} 箱,{{ lastReceipt.inboundCode }} 剩余 {{ lastReceipt.pendingPackages }} 箱 @@ -122,7 +130,7 @@ import { idempotencyKey, receiving, receivingPhase, receivingProgress, releaseIdempotencyKey, resetReceivingDraft, setCurrentReceiving } from '@/stores/receiving.js' import { startScanBroadcast } from '@/utils/scan-broadcast.js' import { playErrorSound, playSuccessSound } from '@/utils/audio-feedback.js' - import { completeCurrentReceiving, downloadCurrentUnloadPhoto, getCurrentReceiving, getCurrentWarehouseProfile, getReceivingContainer, listReceivingCandidates, prepareReceivingContainer, receiveCurrentBulk, receiveCurrentCarton, resolveCurrentCarton, resolveCurrentPallet, signCurrentReceiving, uploadCurrentUnloadPhoto } from '@/api/inbound.js' + import { completeCurrentReceiving, downloadCurrentUnloadPhoto, getCurrentReceiving, getCurrentWarehouseProfile, getReceivingContainer, listReceivingCandidates, prepareReceivingContainer, receiveCurrentBulk, receiveCurrentCarton, receiveCurrentWholePallet, resolveCurrentCarton, resolveCurrentPallet, signCurrentReceiving, uploadCurrentUnloadPhoto } from '@/api/inbound.js' const MAX_UNLOAD_PHOTO_BYTES = 10 * 1024 * 1024 export default { @@ -134,9 +142,9 @@ candidates() { return receiving.candidates }, selected() { return receiving.selected }, current() { return receiving.current }, photoSlots() { return receiving.photoSlots }, resolvedCarton() { return receiving.resolvedCarton }, resolvedPallet() { return receiving.resolvedPallet }, lastReceipt() { return receiving.lastReceipt }, activeTarget: { get() { return receiving.activeScanTarget }, set(value) { receiving.activeScanTarget = value } }, - phase() { return this.detailOpen && !this.current ? 'detail' : receivingPhase(this.current) }, progress() { return receivingProgress(this.current) }, activeInbound() { return this.resolvedCarton }, + phase() { return this.detailOpen && !this.current ? 'detail' : receivingPhase(this.current) }, progress() { return receivingProgress(this.current) }, currentDropshipInbound() { return (this.current?.inbounds || []).find((item) => item.wholePalletInbound && Number(item.pendingPackages) > 0) || null }, activeInbound() { return this.resolvedCarton || this.currentDropshipInbound }, uploadedPhotoCount() { return this.photoSlots.filter((item) => item?.fileId).length }, photosReady() { return this.uploadedPhotoCount === 3 && !this.photoSlots.some((item) => item?.status === 'uploading') }, - canSubmitReceipt() { if (!this.activeInbound || !this.resolvedPallet?.compatible) return false; if (this.activeInbound.destinationType === 'FBA') { const count = Number(this.packageCount); return Number.isInteger(count) && count > 0 && count <= Number(this.activeInbound.pendingPackages) } return Boolean(this.activeInbound.cartonMark) }, + canSubmitReceipt() { if (!this.activeInbound) return false; if (this.activeInbound.wholePalletInbound) return Boolean(this.palletInput.trim()); if (!this.resolvedPallet?.compatible) return false; if (this.activeInbound.destinationType === 'FBA') { const count = Number(this.packageCount); return Number.isInteger(count) && count > 0 && count <= Number(this.activeInbound.pendingPackages) } return Boolean(this.activeInbound.cartonMark) }, allInboundsReconciled() { const values = this.current?.inbounds || []; return values.length > 0 && values.every((item) => Number(item.pendingPackages) === 0) }, boundWarehouseLabel() { const code = String(this.user?.warehouseCode || '').trim(); const name = String(this.user?.warehouseName || '').trim(); return code && name ? `${code} · ${name}` : '海外仓未关联' }, pageTitle() { return { candidates: '待卸货集装箱', detail: '柜号详情', sign: '拆柜拍照签收', receive: '当前柜收货', completed: '入库完成' }[this.phase] }, @@ -155,9 +163,9 @@ async refreshBoundWarehouse() { this.user = applyWarehouseProfile(await getCurrentWarehouseProfile()); return this.user }, photoStateLabel(photo) { if (photo?.status === 'uploading') return '正在上传…'; if (photo?.status === 'error') return photo.errorMessage || (photo.fileId ? '重拍失败,已保留原图' : '上传失败,请重试'); return photo?.fileId ? '点击左侧照片可查看大图' : '请拍摄现场照片' }, startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: (value) => this.handleHardwareScan(value), onError: (error) => { this.error = error; this.showToast('硬件扫码接收器异常,请检查扫描服务', 'warning') } }) }, stopScanner() { if (typeof this._stopScanner === 'function') this._stopScanner(); this._stopScanner = null }, - handleHardwareScan(value) { console.info('[Overseas PDA scan] dispatch', { value, phase: this.phase, target: this.activeTarget, matchedInbound: this.activeInbound?.inboundCode }); if (!this.inboundMode) return this.showToast('该功能缺少海外 PDA 专用接口,扫码未提交', 'warning'); if (this.phase === 'candidates') return this.showToast('请直接点击待卸货集装箱进入详情', 'warning'); if (this.phase !== 'receive') return; if (this.activeTarget === 'pallet' && this.activeInbound) { this.palletInput = value; return this.resolvePallet(value) } this.cartonInput = value; return this.resolveCarton(value) }, + handleHardwareScan(value) { console.info('[Overseas PDA scan] dispatch', { value, phase: this.phase, target: this.activeTarget, matchedInbound: this.activeInbound?.inboundCode }); if (!this.inboundMode) return this.showToast('该功能缺少海外 PDA 专用接口,扫码未提交', 'warning'); if (this.phase === 'candidates') return this.showToast('请直接点击待卸货集装箱进入详情', 'warning'); if (this.phase !== 'receive') return; if (this.currentDropshipInbound) { this.palletInput = value; return this.submitWholePallet(value) } if (this.activeTarget === 'pallet' && this.activeInbound) { this.palletInput = value; return this.resolvePallet(value) } this.cartonInput = value; return this.resolveCarton(value) }, async run(action, work) { this.busy = true; this.error = null; try { const result = await work(); this.showToast(`${action}成功`, 'success'); return result } catch (error) { const mappedError = toReceivingError(error, `${action}失败`); this.error = mappedError; playErrorSound(); this.showToast(mappedError.message, 'error'); throw mappedError } finally { this.busy = false } }, - setScanTargetForCurrent(value) { receiving.activeScanTarget = value?.status === 'WAIT_SIGN' ? 'container' : 'carton' }, + setScanTargetForCurrent(value) { receiving.activeScanTarget = value?.status === 'WAIT_SIGN' ? 'container' : (value?.inbounds || []).some((item) => item.wholePalletInbound && Number(item.pendingPackages) > 0) ? 'source-pallet' : 'carton' }, async restorePhotoPreviews() { await Promise.all(receiving.photoSlots.filter((photo) => photo?.downloadPath && !photo.localPath).map(async (photo) => { photo.status = 'restoring'; try { photo.localPath = await downloadCurrentUnloadPhoto(photo.downloadPath); photo.status = 'success' } catch (error) { photo.status = 'error'; photo.errorMessage = error.message || '照片加载失败'; this.error = error } })) }, async applyCurrentReceiving(value) { this.validateReceivingWarehouse(value, '当前接柜会话'); setCurrentReceiving(value); await this.restorePhotoPreviews(); this.setScanTargetForCurrent(value) }, async bootstrap() { if (this.loading) return; this.loading = true; this.error = null; this.detailOpen = false; try { await this.refreshBoundWarehouse(); try { const current = await getCurrentReceiving(); await this.applyCurrentReceiving(current); this.candidateError = null } catch (error) { if (!isNoCurrentReceiving(error)) throw error; resetReceivingDraft(); await this.loadCandidates(false, false) } this.bootstrapped = true } catch (error) { this.candidateError = error } finally { this.loading = false } }, @@ -170,10 +178,11 @@ async replacePhoto(index, path) { const previous = receiving.photoSlots[index]; receiving.photoSlots.splice(index, 1, { ...(previous || {}), role: this.photoSteps[index].role, status: 'uploading', errorMessage: '' }); this.busy = true; this.error = null; try { const file = await uploadCurrentUnloadPhoto(path); receiving.photoSlots.splice(index, 1, { ...file, fileId: String(file.fileId), role: this.photoSteps[index].role, localPath: path, uploaded: true, status: 'success' }); this.showToast(`${this.photoSteps[index].label}上传成功`, 'success') } catch (error) { receiving.photoSlots.splice(index, 1, previous?.fileId ? { ...previous, status: 'error', errorMessage: error.message || '重拍失败,已保留原图' } : { role: this.photoSteps[index].role, localPath: path, uploaded: false, status: 'error', errorMessage: error.message || '上传失败,请重试' }); this.error = error; this.showToast(error.message || '照片上传失败', 'error'); playErrorSound() } finally { this.busy = false } }, previewPhoto(index) { const path = this.photoSlots[index]?.localPath; if (!path) return; uni.previewImage({ urls: [path], current: path }) }, async signContainer() { if (!this.photosReady) return; const photos = this.photoSlots.map((item, index) => ({ role: this.photoSteps[index].role, fileId: String(item.fileId) })); const key = idempotencyKey('sign', { containerNo: this.current.containerNo, photos }); try { const value = await this.run('签收当前柜', async () => this.validateReceivingWarehouse(await signCurrentReceiving({ photos, idempotencyKey: key.value }), '当前接柜会话')); releaseIdempotencyKey(key.signature); await this.applyCurrentReceiving(value); this.clearReceiptContext() } catch (_) {} }, - clearReceiptContext() { receiving.resolvedCarton = null; receiving.resolvedPallet = null; this.cartonInput = ''; this.palletInput = ''; this.packageCount = 1; receiving.activeScanTarget = 'carton' }, + clearReceiptContext() { receiving.resolvedCarton = null; receiving.resolvedPallet = null; this.cartonInput = ''; this.palletInput = ''; this.packageCount = 1; receiving.activeScanTarget = this.currentDropshipInbound ? 'source-pallet' : 'carton' }, async resolveCarton(raw) { const cartonMark = String(raw?.detail?.value ?? raw ?? this.cartonInput).trim(); if (!cartonMark) return; if (this.busy) { console.warn('[Overseas PDA carton] ignored while busy', { cartonMark }); return this.showToast('正在处理上一笔扫描,请稍后重试', 'warning') } console.info('[Overseas PDA carton] resolving', { cartonMark, containerNo: this.current?.containerNo }); try { const value = await this.run('解析箱码', () => resolveCurrentCarton(cartonMark)); console.info('[Overseas PDA carton] resolved', { cartonMark: value.cartonMark, inboundCode: value.inboundCode, destinationType: value.destinationType, pendingPackages: value.pendingPackages }); receiving.resolvedCarton = value; receiving.resolvedPallet = null; this.cartonInput = value.cartonMark; this.palletInput = ''; this.packageCount = 1; playSuccessSound() } catch (error) { console.error('[Overseas PDA carton] failed', { cartonMark, message: error.message, code: error.code, requestId: error.requestId, traceId: error.traceId }) } }, async resolvePallet(raw) { const palletCode = String(raw?.detail?.value ?? raw ?? this.palletInput).trim(); if (!palletCode || !this.activeInbound || this.busy) return; try { const value = await this.run('校验托盘', () => resolveCurrentPallet(palletCode, this.activeInbound.inboundCode)); receiving.resolvedPallet = value; this.palletInput = value.palletCode; if (!value.compatible) throw Object.assign(new Error('托盘不匹配'), { code: 'PALLET_INCOMPATIBLE' }); playSuccessSound() } catch (error) { if (!this.error) this.error = error } }, async submitReceipt() { if (!this.canSubmitReceipt) return; const isFba = this.activeInbound.destinationType === 'FBA'; const payload = isFba ? { inboundCode: this.activeInbound.inboundCode, palletCode: this.resolvedPallet.palletCode, packageCount: Number(this.packageCount) } : { cartonMark: this.activeInbound.cartonMark, palletCode: this.resolvedPallet.palletCode }; const key = idempotencyKey(isFba ? 'bulk-receipt' : 'carton-receipt', payload); try { const result = await this.run('提交实收', () => isFba ? receiveCurrentBulk({ ...payload, idempotencyKey: key.value }) : receiveCurrentCarton({ ...payload, idempotencyKey: key.value })); const current = this.validateReceivingWarehouse(await getCurrentReceiving(), '当前接柜会话'); releaseIdempotencyKey(key.signature); receiving.lastReceipt = result; await this.applyCurrentReceiving(current); this.clearReceiptContext(); playSuccessSound() } catch (_) {} }, + async submitWholePallet(raw) { const sourcePalletNo = String(raw?.detail?.value ?? raw ?? this.palletInput).trim(); if (!sourcePalletNo || !this.currentDropshipInbound || this.busy) return; this.palletInput = sourcePalletNo; const payload = { sourcePalletNo }; const key = idempotencyKey('whole-pallet-receipt', payload); try { const result = await this.run('整托实收', () => receiveCurrentWholePallet({ ...payload, idempotencyKey: key.value })); const current = this.validateReceivingWarehouse(await getCurrentReceiving(), '当前接柜会话'); releaseIdempotencyKey(key.signature); receiving.lastReceipt = result; await this.applyCurrentReceiving(current); this.clearReceiptContext(); playSuccessSound() } catch (_) {} }, async completeContainer() { if (!this.allInboundsReconciled) return; const key = idempotencyKey('complete', { containerNo: this.current.containerNo }); try { const value = await this.run('完成入库', async () => this.validateReceivingWarehouse(await completeCurrentReceiving({ idempotencyKey: key.value }), '当前接柜会话')); releaseIdempotencyKey(key.signature); await this.applyCurrentReceiving(value) } catch (_) {} }, async finishAndReturn() { resetReceivingDraft(); await this.loadCandidates(false) }, showToast(message, type) { clearTimeout(this._toastTimer); this.toast = { visible: true, message, type }; this._toastTimer = setTimeout(() => { this.toast.visible = false }, 2600) } diff --git a/tests/project-contract.test.mjs b/tests/project-contract.test.mjs index 2f342d0..dbdff1f 100644 --- a/tests/project-contract.test.mjs +++ b/tests/project-contract.test.mjs @@ -53,8 +53,8 @@ test('login has no committed demo credentials and inbound operation implements t assert.match(runtime, /#ifdef H5[\s\S]*?VITE_H5_AUTH_API_BASE_URL/) assert.match(runtime, /#ifdef H5[\s\S]*?VITE_H5_OVERSEAS_API_BASE_URL/) assert.match(operation, /startScanBroadcast/) - for (const action of ['prepareReceivingContainer', 'getCurrentReceiving', 'resolveCurrentCarton', 'resolveCurrentPallet', 'receiveCurrentBulk', 'receiveCurrentCarton', 'completeCurrentReceiving']) assert.match(operation, new RegExp(action)) - for (const path of ['/candidates', '/prepare', '/unload-photos/files', '/sign', '/cartons/resolve', '/pallets/resolve', '/receipts/bulk', '/receipts/carton', '/complete']) assert.match(inbound, new RegExp(path.replaceAll('/', '\\/'))) + for (const action of ['prepareReceivingContainer', 'getCurrentReceiving', 'resolveCurrentCarton', 'resolveCurrentPallet', 'receiveCurrentBulk', 'receiveCurrentCarton', 'receiveCurrentWholePallet', 'completeCurrentReceiving']) assert.match(operation, new RegExp(action)) + for (const path of ['/candidates', '/prepare', '/unload-photos/files', '/sign', '/cartons/resolve', '/pallets/resolve', '/receipts/bulk', '/receipts/carton', '/receipts/pallet', '/complete']) assert.match(inbound, new RegExp(path.replaceAll('/', '\\/'))) assert.match(inbound, /current\('\/pallets\/resolve'\)/) assert.doesNotMatch(inbound, /platform\/v1\/warehouse-admin/, 'PDA只能调用current/pallets/resolve,不能访问Admin仓储资源接口') assert.doesNotMatch(operation, /resolveCurrentSku|resolvedSku|skuInput|selectedInboundCode|chooseInbound|class="inbound-tabs"/) @@ -63,7 +63,10 @@ test('login has no committed demo credentials and inbound operation implements t assert.doesNotMatch(operation, /\.split\([^\n]*['"]#['"]/, '前端必须原样提交完整箱码,禁止按 # 猜测入仓码或箱序号') assert.match(operation, /const payload = isFba \? \{ inboundCode: this\.activeInbound\.inboundCode, palletCode: this\.resolvedPallet\.palletCode, packageCount: Number\(this\.packageCount\) \}/) assert.doesNotMatch(operation, /skuCode/) - assert.match(operation, /clearReceiptContext\(\)[\s\S]*activeScanTarget = 'carton'/) + assert.match(operation, /currentDropshipInbound[\s\S]*wholePalletInbound/) + assert.match(operation, /name="source-pallet"[\s\S]*@submit="submitWholePallet"/) + assert.match(operation, /submitWholePallet[\s\S]*sourcePalletNo[\s\S]*receiveCurrentWholePallet/) + assert.match(operation, /clearReceiptContext\(\)[\s\S]*activeScanTarget = this\.currentDropshipInbound \? 'source-pallet' : 'carton'/) assert.match(operation, /name="pallet"[\s\S]*@activate="activeTarget = 'pallet'"/) assert.doesNotMatch(operation, /receiving\.activeScanTarget = 'pallet'/, '箱码解析成功后必须由作业员手动选择托盘扫码,不得自动跳转') assert.match(operation, /\[Overseas PDA scan\] dispatch/) diff --git a/tests/receiving-adapter.test.mjs b/tests/receiving-adapter.test.mjs index cc890c1..2034057 100644 --- a/tests/receiving-adapter.test.mjs +++ b/tests/receiving-adapter.test.mjs @@ -25,7 +25,7 @@ test('reservation mismatch asks for the reserved pallet and retains diagnostics' test('first scan and receipt requests confirm server reservations without resource assignment commands', async () => { const source = await readFile(new URL('../src/api/inbound.js', import.meta.url), 'utf8') const calls = [] - const api = new Function('overseasRequest', 'normalizeReceivingAssignment', 'normalizeReceivingInbound', source.replace(/^import .*$/gm, '').replace(/^export /gm, '') + '; return {resolveCurrentCarton,resolveCurrentPallet,receiveCurrentBulk,receiveCurrentCarton};')(async (...args) => { + const api = new Function('overseasRequest', 'normalizeReceivingAssignment', 'normalizeReceivingInbound', source.replace(/^import .*$/gm, '').replace(/^export /gm, '') + '; return {resolveCurrentCarton,resolveCurrentPallet,receiveCurrentBulk,receiveCurrentCarton,receiveCurrentWholePallet};')(async (...args) => { calls.push(args) return { compatible: true, assigned: true, mergeAllowed: 'true', urgent: true, mergeHint: 'unsafe hint' } }, normalizeReceivingAssignment, normalizeReceivingInbound) @@ -33,11 +33,13 @@ test('first scan and receipt requests confirm server reservations without resour assert.equal((await api.resolveCurrentPallet('P-1', 'IN-1')).compatible, true) assert.equal((await api.receiveCurrentBulk({ inboundCode: 'IN-1', palletCode: 'P-1', packageCount: 2 })).urgent, true) assert.equal((await api.receiveCurrentCarton({ cartonMark: 'BOX-1', palletCode: 'P-1' })).mergeHint, '') + assert.equal((await api.receiveCurrentWholePallet({ sourcePalletNo: 'DP-1' })).urgent, true) assert.deepEqual(calls, [ ['POST', '/pda/v1/last-leg/receiving-containers/current/cartons/resolve', { cartonMark: 'BOX-1' }], ['POST', '/pda/v1/last-leg/receiving-containers/current/pallets/resolve', { palletCode: 'P-1', inboundCode: 'IN-1' }], ['POST', '/pda/v1/last-leg/receiving-containers/current/receipts/bulk', { inboundCode: 'IN-1', palletCode: 'P-1', packageCount: 2 }], - ['POST', '/pda/v1/last-leg/receiving-containers/current/receipts/carton', { cartonMark: 'BOX-1', palletCode: 'P-1' }] + ['POST', '/pda/v1/last-leg/receiving-containers/current/receipts/carton', { cartonMark: 'BOX-1', palletCode: 'P-1' }], + ['POST', '/pda/v1/last-leg/receiving-containers/current/receipts/pallet', { sourcePalletNo: 'DP-1' }] ]) const page = await readFile(new URL('../src/pages/operation/operation.vue', import.meta.url), 'utf8') assert.match(page, /activeInbound\.assigned/) @@ -49,6 +51,13 @@ test('first scan and receipt requests confirm server reservations without resour assert.match(gate, /resolvedPallet\?\.compatible/) }) +test('one-click dropship inbound is marked only by an explicit server boolean', () => { + for (const value of [undefined, null, false, 1, 'true', 'false']) { + assert.equal(normalizeReceivingInbound({ wholePalletInbound: value }).wholePalletInbound, false) + } + assert.equal(normalizeReceivingInbound({ wholePalletInbound: true }).wholePalletInbound, true) +}) + test('receiving adapter preserves the three signed photo roles in workflow order', () => { const snowflakeFileId = '2092476334483931138' const session = normalizeReceivingSession({