feat(pda): receive dropship cartons by sealed pallet

This commit is contained in:
2026-09-09 18:33:17 +08:00
parent f6ee80d677
commit 1c01352a39
6 changed files with 36 additions and 13 deletions
+2 -2
View File
@@ -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。候选顺序沿用服务端“可操作优先、同组计划到仓时间升序、时间为空最后”的排序,前端不得用入仓码/计划数组长度自行推导就绪或把预计到港冒充计划到仓。
+1
View File
@@ -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))
+1
View File
@@ -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),
+15 -6
View File
@@ -66,6 +66,13 @@
<template v-else-if="phase === 'receive'">
<ReceivingSessionSummary :session="current" />
<view class="scan-card">
<template v-if="currentDropshipInbound">
<text class="section-label">扫描国内封托来源托盘</text>
<view class="matched-inbound-card"><view class="mode-banner overseas"><text>一件代发整托收货</text><text>{{ destinationLabel(currentDropshipInbound) }}</text></view><text class="matched-inbound-code">{{ currentDropshipInbound.inboundCode }}</text><view class="matched-inbound-progress"><text>总计 {{ currentDropshipInbound.totalPackages }} 箱</text><text>已收 {{ currentDropshipInbound.receivedPackages }} 箱</text><text>待收 {{ currentDropshipInbound.pendingPackages }} 箱</text></view></view>
<ScanTargetInput name="source-pallet" v-model="palletInput" label="来源托盘码" placeholder="扫描 DP 开头的国内封托托盘码" :active="activeTarget === 'source-pallet'" @activate="activeTarget = 'source-pallet'" @submit="submitWholePallet" />
<button class="primary-button receipt-button" :disabled="!canSubmitReceipt || busy" @tap="submitWholePallet(palletInput)">确认整托收货</button>
</template>
<template v-else>
<text class="section-label">1. 扫描箱码定位入仓码</text>
<ScanTargetInput name="carton" v-model="cartonInput" label="箱码" placeholder="NYEXX2608260001#4-1" :active="activeTarget === 'carton'" @activate="activeTarget = 'carton'" @submit="resolveCarton" />
<button class="secondary-button" :disabled="busy || !cartonInput.trim()" @tap="resolveCarton(cartonInput)">解析箱码</button>
@@ -89,6 +96,7 @@
</template>
<button class="primary-button receipt-button" :disabled="!canSubmitReceipt || busy" @tap="submitReceipt">{{ activeInbound.destinationType === 'FBA' ? '确认按箱数收货' : '确认本箱收货' }}</button>
</template>
</template>
</view>
<view v-if="lastReceipt" class="success-card">本次已收 {{ lastReceipt.acceptedPackages }} 箱,{{ lastReceipt.inboundCode }} 剩余 {{ lastReceipt.pendingPackages }} 箱</view>
<view v-if="allInboundsReconciled" class="sticky-action one"><button class="primary-button complete-button" :disabled="busy" @tap="completeContainer">完成整柜入库</button></view>
@@ -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) }
+6 -3
View File
@@ -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/)
+11 -2
View File
@@ -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({