fix(pda): enforce cabinet evidence contract
This commit is contained in:
+15
-4
@@ -121,7 +121,7 @@ export async function submitContainerPickingBatch(task, actions, { warehouseId,
|
||||
const results = []
|
||||
let version = task.version ?? task.pickingVersion
|
||||
for (const action of actions || []) {
|
||||
const common = { warehouseId, version: action.version ?? version, operationId: `${submitOperationId}-${action.itemId}` }
|
||||
const common = { warehouseId, locationIds: action.locationIds || [], version: action.version ?? version, operationId: `${submitOperationId}-${action.itemId}` }
|
||||
let result
|
||||
if (action.type === 'PICK') result = await pickContainerItem(task.taskId, action.itemId, { ...common, pickedPackageCount: action.pickedPackageCount })
|
||||
else if (action.type === 'PARTIAL') result = await partialContainerItem(task.taskId, action.itemId, { ...common, finalPickedPackageCount: action.finalPickedPackageCount })
|
||||
@@ -190,7 +190,7 @@ export function completePicking(inboundId, payload, tenantId) {
|
||||
|
||||
export function getEvidence(inboundId, tenantId) {
|
||||
return request({
|
||||
url: `/inbounds/${encodeURIComponent(inboundId)}/loading/evidence?tenantId=${encodeURIComponent(tenantQuery(tenantId))}`,
|
||||
url: `/inbounds/${encodeURIComponent(inboundId)}/loading/evidence/pda?tenantId=${encodeURIComponent(tenantQuery(tenantId))}`,
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
@@ -198,9 +198,20 @@ export function getEvidence(inboundId, tenantId) {
|
||||
export function upsertEvidence(inboundId, payload, tenantId) {
|
||||
if (!EVIDENCE_TYPES.includes(payload.evidenceType)) throw new Error('不支持的装柜证据类型')
|
||||
return request({
|
||||
url: `/inbounds/${encodeURIComponent(inboundId)}/loading/evidence?tenantId=${encodeURIComponent(tenantQuery(tenantId))}`,
|
||||
url: `/inbounds/${encodeURIComponent(inboundId)}/loading/evidence/pda?tenantId=${encodeURIComponent(tenantQuery(tenantId))}`,
|
||||
method: 'POST',
|
||||
header: { 'Idempotency-Key': idempotencyKey(`evidence-${payload.evidenceType}`, inboundId) },
|
||||
header: { 'Idempotency-Key': stableIdempotencyKey(payload.operationId || `evidence-${inboundId}-${payload.evidenceType}-${payload.fileId || 'seal'}`) },
|
||||
data: payload
|
||||
})
|
||||
}
|
||||
|
||||
export function voidEvidence(inboundId, evidenceId, payload = {}, tenantId) {
|
||||
if (!inboundId || !evidenceId) throw new Error('缺少装柜证据标识,无法作废')
|
||||
if (!String(payload.reason || '').trim()) throw new Error('作废原因不能为空')
|
||||
return request({
|
||||
url: `/inbounds/${encodeURIComponent(inboundId)}/loading/evidence/${encodeURIComponent(evidenceId)}/void/pda?tenantId=${encodeURIComponent(tenantQuery(tenantId))}`,
|
||||
method: 'POST',
|
||||
header: { 'Idempotency-Key': stableIdempotencyKey(payload.operationId || `void-evidence-${inboundId}-${evidenceId}-${payload.expectedVersion ?? 0}`) },
|
||||
data: payload
|
||||
})
|
||||
}
|
||||
|
||||
+67
-21
@@ -123,9 +123,12 @@
|
||||
<text class="sync-state" :class="remoteSyncState.tone">{{ remoteSyncState.label }}</text>
|
||||
</view>
|
||||
<view class="evidence-grid">
|
||||
<view v-for="slot in evidenceSlots" :key="slot.type" class="evidence-card" :class="{ filled: evidenceEntry(slot.type) }">
|
||||
<text class="evidence-label">{{ slot.label }}</text>
|
||||
<text v-if="evidenceEntry(slot.type)" class="evidence-file">{{ evidenceEntry(slot.type).fileName || '已记录' }}</text>
|
||||
<view v-for="slot in evidenceSlots" :key="slot.type" class="evidence-card" :class="{ filled: evidenceEntries(slot.type).length }">
|
||||
<text class="evidence-label">{{ slot.label }}</text>
|
||||
<view v-for="photo in evidenceEntries(slot.type)" :key="photo.id || photo.fileId || photo.localPath" class="evidence-file-row">
|
||||
<text class="evidence-file">{{ photo.fileName || '已记录' }}</text>
|
||||
<button v-if="photo.id && photo.status !== 'VOID' && photo.status !== 'VOIDED'" class="evidence-retry-btn" @tap="voidEvidencePhoto(photo)">作废</button>
|
||||
</view>
|
||||
<text v-if="evidenceQueueEntry(slot.type)" class="evidence-upload-state" :class="evidenceQueueEntry(slot.type).status.toLowerCase()">
|
||||
{{ evidenceUploadState(slot.type) }}
|
||||
</text>
|
||||
@@ -135,10 +138,10 @@
|
||||
</view>
|
||||
<view class="seal-row">
|
||||
<input v-model="sealNo" class="seal-input" placeholder="输入封签号(必填)" maxlength="64" />
|
||||
<button class="evidence-btn seal-btn" @tap="saveEvidence('SEAL')">保存封签</button>
|
||||
<button class="evidence-btn seal-btn" @tap="saveSealNo">保存封签</button>
|
||||
</view>
|
||||
<text v-if="remoteError" class="remote-error" role="alert">{{ remoteError }}</text>
|
||||
<button class="loading-complete-btn" :disabled="submitting || loadingVersion === null" @tap="completeLoadingTask">确认装柜完成</button>
|
||||
<button class="loading-complete-btn" :disabled="submitting || loadingVersion === null || !completedBoxes[currentBoxIndex] || !evidenceComplete" @tap="completeLoadingTask">确认装柜完成</button>
|
||||
<text v-if="loadingVersion === null" class="version-hint">等待统一订单详情提供装柜版本号后可提交完成</text>
|
||||
</view>
|
||||
|
||||
@@ -321,7 +324,7 @@
|
||||
import { navigateBackOr } from '@/utils/navigation.js'
|
||||
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
|
||||
import { playErrorSound, playSuccessSound } from '@/utils/audio-feedback.js'
|
||||
import { claimContainerPickingTask, completeLoading, completePicking, EVIDENCE_SLOTS, getContainerPickingTask, getEvidence, getPickingTask, isRemoteReady, normalizePickingTask, submitContainerPickingBatch, uploadEvidenceFile, upsertEvidence } from '@/api/pda-loading.js'
|
||||
import { claimContainerPickingTask, completeLoading, completePicking, EVIDENCE_SLOTS, getContainerPickingTask, getEvidence, getPickingTask, isRemoteReady, normalizePickingTask, submitContainerPickingBatch, uploadEvidenceFile, upsertEvidence, voidEvidence } from '@/api/pda-loading.js'
|
||||
import { createEvidenceUploadQueue } from '@/utils/evidence-upload-queue.js'
|
||||
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
||||
import PopupHeader from '@/components/PopupHeader.vue'
|
||||
@@ -398,13 +401,16 @@
|
||||
partialInputValid() {
|
||||
if (!this.partialTargetItem) return false
|
||||
const value = parseInt(this.partialInput, 10)
|
||||
return Number.isInteger(value) && value > 0 && value < this.partialTargetItem.qty
|
||||
const available = this.availablePickQuantity(this.partialTargetItem)
|
||||
return Number.isInteger(value) && value > 0 && value < this.partialTargetItem.qty && (available === null || value <= available)
|
||||
},
|
||||
partialValidationMessage() {
|
||||
if (!this.partialInput || !this.partialTargetItem) return ''
|
||||
const value = parseInt(this.partialInput, 10)
|
||||
if (!Number.isInteger(value) || value <= 0) return '请输入大于 0 的整数'
|
||||
if (value >= this.partialTargetItem.qty) return `部分拣货必须少于 ${this.partialTargetItem.qty},整单请返回后点击“确认拣出”`
|
||||
const available = this.availablePickQuantity(this.partialTargetItem)
|
||||
if (available !== null && value > available) return `超过库存/库位可用量,可拣 ${available}`
|
||||
return ''
|
||||
},
|
||||
submitButtonText() {
|
||||
@@ -432,6 +438,9 @@
|
||||
if (this.remoteLoading) return { label: '同步中', tone: 'loading' }
|
||||
if (this.remoteError) return { label: '同步失败', tone: 'error' }
|
||||
return { label: this.remotePick ? '已同步' : '待同步', tone: this.remotePick ? 'ready' : 'local' }
|
||||
},
|
||||
evidenceComplete() {
|
||||
return this.evidenceSlots.every((slot) => this.validEvidenceEntries(slot.type).length > 0) && Boolean(String(this.sealNo || '').trim())
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -616,7 +625,9 @@
|
||||
playSuccessSound()
|
||||
this.pickItem(result.item.id, 'scan')
|
||||
},
|
||||
evidenceEntry(type) { return this.evidenceFiles[type] || null },
|
||||
evidenceEntries(type) { return Array.isArray(this.evidenceFiles[type]) ? this.evidenceFiles[type].filter((item) => item.status !== 'VOID' && item.status !== 'VOIDED') : (this.evidenceFiles[type] ? [this.evidenceFiles[type]] : []) },
|
||||
validEvidenceEntries(type) { return this.evidenceEntries(type).filter((item) => item.id && item.fileId && item.uploadStatus !== 'PENDING') },
|
||||
evidenceEntry(type) { const list = this.evidenceEntries(type); return list.length ? list[list.length - 1] : null },
|
||||
evidenceQueueEntry(type) {
|
||||
return [...this.evidenceQueueItems].reverse().find((item) => item.evidenceType === type) || null
|
||||
},
|
||||
@@ -649,6 +660,10 @@
|
||||
qty: Number(item.qty ?? item.packageCount ?? item.requiredPackageCount ?? 0),
|
||||
unit: item.unit ?? '件',
|
||||
location: item.location ?? item.locationCode ?? '',
|
||||
availablePackageCount: item.availablePackageCount ?? item.locationAvailablePackageCount ?? item.availableQty ?? item.stockAvailableQty ?? null,
|
||||
locationIds: item.locationIds ?? item.reservationIds ?? [],
|
||||
locationAvailable: item.locationAvailable ?? item.locationUsable ?? null,
|
||||
locationStatus: item.locationStatus ?? null,
|
||||
skipReason: item.skipReason || '', finalState: item.finalState || '', finalPartialQuantity: item.finalPartialQuantity || 0
|
||||
}))
|
||||
this.itemStates = createItemStates(this.currentBox.items)
|
||||
@@ -666,8 +681,8 @@
|
||||
this.restoreRemoteDraft()
|
||||
if (this.currentBox.inboundId) {
|
||||
const evidence = await getEvidence(this.currentBox.inboundId)
|
||||
this.evidenceFiles = (evidence && evidence.evidences ? evidence.evidences : []).reduce((all, item) => { all[item.evidenceType] = item; return all }, {})
|
||||
const seal = this.evidenceFiles.SEAL
|
||||
this.evidenceFiles = (evidence && evidence.evidences ? evidence.evidences : []).reduce((all, item) => { (all[item.evidenceType] ||= []).push(item); return all }, {})
|
||||
const seal = this.evidenceEntry('SEAL')
|
||||
if (seal) this.sealNo = seal.sealNo || ''
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -681,10 +696,11 @@
|
||||
},
|
||||
chooseEvidence(slot) {
|
||||
if (typeof uni.chooseImage !== 'function') { this.showToast('当前环境不支持选择照片', 'warning'); return }
|
||||
uni.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['camera', 'album'], success: (result) => {
|
||||
uni.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['camera'], success: (result) => {
|
||||
const path = result.tempFilePaths && result.tempFilePaths[0]
|
||||
if (!path) return
|
||||
this.evidenceFiles = { ...this.evidenceFiles, [slot.type]: { ...(this.evidenceFiles[slot.type] || {}), evidenceType: slot.type, fileName: path.split('/').pop() || path, localPath: path, uploadStatus: 'PENDING' } }
|
||||
const next = { evidenceType: slot.type, fileName: path.split('/').pop() || path, localPath: path, uploadStatus: 'PENDING' }
|
||||
this.evidenceFiles = { ...this.evidenceFiles, [slot.type]: [...(this.evidenceFiles[slot.type] || []), next] }
|
||||
this.queueEvidenceUpload(slot.type, path)
|
||||
}, fail: () => this.showToast('未选择照片,可稍后重试', 'warning') })
|
||||
},
|
||||
@@ -694,7 +710,8 @@
|
||||
await this.evidenceQueue.flush()
|
||||
const latest = this.evidenceQueue.snapshot().find((item) => item.id === entry.id)
|
||||
if (latest?.status !== 'SUCCEEDED') { this.remoteError = latest?.error || '文件上传失败'; return }
|
||||
this.evidenceFiles = { ...this.evidenceFiles, [type]: { ...this.evidenceFiles[type], fileId: latest.result?.fileId, fileName: latest.result?.fileName || this.evidenceFiles[type].fileName, uploadStatus: 'SUCCEEDED' } }
|
||||
const current = this.evidenceEntry(type) || {}
|
||||
this.evidenceFiles = { ...this.evidenceFiles, [type]: [...(this.evidenceFiles[type] || []).slice(0, -1), { ...current, fileId: latest.result?.fileId, fileName: latest.result?.fileName || current.fileName, uploadStatus: 'SUCCEEDED' }] }
|
||||
await this.saveEvidence(type)
|
||||
},
|
||||
async retryEvidence(type) {
|
||||
@@ -704,23 +721,42 @@
|
||||
await this.evidenceQueue.flush()
|
||||
const latest = this.evidenceQueue.latest(type)
|
||||
if (latest?.status !== 'SUCCEEDED') { this.remoteError = latest?.error || '文件上传失败'; return }
|
||||
this.evidenceFiles = { ...this.evidenceFiles, [type]: { ...this.evidenceFiles[type], fileId: latest.result?.fileId, fileName: latest.result?.fileName || this.evidenceFiles[type]?.fileName, uploadStatus: 'SUCCEEDED' } }
|
||||
const current = this.evidenceEntry(type) || {}
|
||||
this.evidenceFiles = { ...this.evidenceFiles, [type]: [...(this.evidenceFiles[type] || []).slice(0, -1), { ...current, fileId: latest.result?.fileId, fileName: latest.result?.fileName || current.fileName, uploadStatus: 'SUCCEEDED' }] }
|
||||
await this.saveEvidence(type)
|
||||
},
|
||||
async saveEvidence(type) {
|
||||
async saveEvidence(type, operationId = null) {
|
||||
if (!this.currentBox.inboundId) { this.showToast('演示任务已记录本地证据', 'success'); return }
|
||||
if (!isRemoteReady(this.currentBox.inboundId)) { this.showToast('当前会话未连接服务端,证据保留本地', 'warning'); return }
|
||||
const current = this.evidenceFiles[type] || {}
|
||||
if (type !== 'SEAL' && !current.fileId) { this.showToast('照片尚未上传完成', 'warning'); return }
|
||||
const payload = { expectedVersion: current.version || 0, evidenceType: type, fileId: current.fileId || null, fileName: current.fileName || null, sealNo: type === 'SEAL' ? this.sealNo : null, remark: null }
|
||||
const current = this.evidenceEntry(type) || {}
|
||||
if (!current.fileId) { this.showToast('照片尚未上传完成', 'warning'); return }
|
||||
const payload = { expectedVersion: current.version || 0, evidenceType: type, fileId: current.fileId || null, fileName: current.fileName || null, sealNo: type === 'SEAL' ? this.sealNo : null, remark: null, operationId: operationId || `evidence-${this.remoteTaskId || this.currentBox.inboundId}-${type}-${current.fileId || 'seal'}` }
|
||||
try {
|
||||
const result = await upsertEvidence(this.currentBox.inboundId, payload)
|
||||
this.evidenceFiles = (result && result.evidences ? result.evidences : []).reduce((all, item) => { all[item.evidenceType] = item; return all }, this.evidenceFiles)
|
||||
this.evidenceFiles = (result && result.evidences ? result.evidences : []).reduce((all, item) => { const list = (all[item.evidenceType] || []).filter((entry) => entry.id || entry.fileId !== item.fileId); const index = list.findIndex((entry) => entry.id === item.id); all[item.evidenceType] = index >= 0 ? list.map((entry, i) => i === index ? item : entry) : [...list, item]; return all }, this.evidenceFiles)
|
||||
this.showToast(`${type === 'SEAL' ? '封签' : '照片'}已保存`, 'success')
|
||||
} catch (error) { this.remoteError = error.network ? '网络失败,证据未提交,请重试' : (error.message || '证据保存失败'); this.showToast('证据保存失败', 'error') }
|
||||
},
|
||||
async saveSealNo() {
|
||||
if (!String(this.sealNo || '').trim()) { this.showToast('请输入封签号', 'warning'); return }
|
||||
await this.saveEvidence('SEAL', `seal-no-${this.remoteTaskId || this.currentBox.inboundId}-${Date.now()}`)
|
||||
},
|
||||
async voidEvidencePhoto(photo) {
|
||||
if (!photo?.id || !this.currentBox.inboundId) return
|
||||
const reason = 'PDA现场标记错图'
|
||||
try {
|
||||
await voidEvidence(this.currentBox.inboundId, photo.id, { expectedVersion: photo.version || 0, reason, operationId: `void-${this.currentBox.inboundId}-${photo.id}-${photo.version || 0}` })
|
||||
this.evidenceFiles = { ...this.evidenceFiles, [photo.evidenceType]: (this.evidenceFiles[photo.evidenceType] || []).map((item) => item.id === photo.id ? { ...item, status: 'VOID', voidReason: reason } : item) }
|
||||
this.showToast('照片已作废,可重新拍摄', 'success')
|
||||
} catch (error) {
|
||||
this.remoteError = error.network ? '网络失败,照片未作废,请重试' : (error.message || '照片作废失败')
|
||||
this.showToast('照片作废失败', 'error')
|
||||
}
|
||||
},
|
||||
async completeLoadingTask() {
|
||||
if (!isRemoteReady(this.currentBox.inboundId) || this.loadingVersion === null) return
|
||||
if (!this.completedBoxes[this.currentBoxIndex]) { this.showToast('请先提交拣货完成', 'warning'); return }
|
||||
if (!this.evidenceComplete) { this.showToast('五类有效照片和封签号均为必填', 'warning'); return }
|
||||
this.submitting = true
|
||||
try { await completeLoading(this.currentBox.inboundId, { expectedVersion: this.loadingVersion, remark: 'PDA确认装柜完成' }); this.showToast('已确认装柜完成', 'success') }
|
||||
catch (error) { this.remoteError = error.network ? '网络失败,装柜完成未提交' : (error.message || '装柜完成失败'); this.showToast('装柜完成失败', 'error') }
|
||||
@@ -728,6 +764,8 @@
|
||||
},
|
||||
pickItem(id, source = 'manual') {
|
||||
const item = this.currentBox.items.find((entry) => entry.id === id)
|
||||
const available = this.availablePickQuantity(item)
|
||||
if (available !== null && available < Number(item.qty || 0)) { this.showToast(`库存/库位可用量不足:可拣 ${available}`, 'error'); return }
|
||||
this.itemStates = { ...this.itemStates, [id]: ITEM_STATUS.PICKED }
|
||||
if (source === 'scan' && item) {
|
||||
this.recordScanResult('success', '扫描成功', item.sku, `${item.location} · 已拣 ${item.qty} ${item.unit}`)
|
||||
@@ -787,12 +825,20 @@
|
||||
if (!item) return
|
||||
if (!this.partialInputValid) return
|
||||
const value = parseInt(this.partialInput, 10)
|
||||
const available = this.availablePickQuantity(item)
|
||||
if (available !== null && value > available) { this.showToast(`超过库存/库位可用量:可拣 ${available}`, 'error'); return }
|
||||
this.itemStates = { ...this.itemStates, [item.id]: ITEM_STATUS.PARTIAL }
|
||||
this.partialQuantities = { ...this.partialQuantities, [item.id]: value }
|
||||
this.closeOverlay(false)
|
||||
this.showToast(`已部分拣货:${value}/${item.qty} ${item.unit}`, 'success')
|
||||
this.persistSession()
|
||||
},
|
||||
availablePickQuantity(item) {
|
||||
if (!item) return null
|
||||
if (item.locationAvailable === false || item.locationStatus === 'DISABLED') return 0
|
||||
const raw = item.locationAvailablePackageCount ?? item.availablePackageCount ?? item.availableQty ?? item.stockAvailableQty
|
||||
return raw === null || raw === undefined || raw === '' ? null : Math.max(0, Number(raw))
|
||||
},
|
||||
openBoxList(type) {
|
||||
this.boxListType = type
|
||||
this.boxDropdownOpen = false
|
||||
@@ -846,8 +892,8 @@
|
||||
const actions = this.currentBox.items.map((item) => {
|
||||
const status = this.itemState(item)
|
||||
const itemId = item.taskItemId ?? item.remoteItemId ?? item.id
|
||||
if (status === ITEM_STATUS.PICKED) return { type: 'PICK', itemId, pickedPackageCount: Number(item.qty || 0) }
|
||||
if (status === ITEM_STATUS.PARTIAL) return { type: 'PARTIAL', itemId, finalPickedPackageCount: Number(this.partialQuantities[item.id] || 0) }
|
||||
if (status === ITEM_STATUS.PICKED) return { type: 'PICK', itemId, pickedPackageCount: Number(item.qty || 0), locationIds: item.locationIds || [] }
|
||||
if (status === ITEM_STATUS.PARTIAL) return { type: 'PARTIAL', itemId, finalPickedPackageCount: Number(this.partialQuantities[item.id] || 0), locationIds: item.locationIds || [] }
|
||||
if (status === ITEM_STATUS.SKIPPED) return { type: 'SKIP', itemId, reason: item.skipReason || incompleteReason || '现场跳拣' }
|
||||
if (incompleteReason) return { type: 'SKIP', itemId, reason: `${incompleteReason}(提交时确认未拣)` }
|
||||
return null
|
||||
|
||||
@@ -11,7 +11,7 @@ test('PDA loading adapter keeps C04 read and write paths', () => {
|
||||
'/loading/picking?tenantId=',
|
||||
'/loading/picking/start?tenantId=',
|
||||
'/loading/picking/complete?tenantId=',
|
||||
'/loading/evidence?tenantId=',
|
||||
'/loading/evidence/pda?tenantId=',
|
||||
'/loading/complete?tenantId='
|
||||
]) assert.match(source, new RegExp(path.replace(/[?]/g, '\\?')))
|
||||
assert.match(source, /'Idempotency-Key'/)
|
||||
@@ -19,6 +19,8 @@ test('PDA loading adapter keeps C04 read and write paths', () => {
|
||||
for (const label of ['空仓照片', '半仓照片', '满仓照片', '锁仓照片', '带锁码照']) assert.match(source, new RegExp(label))
|
||||
assert.match(source, /uni\.uploadFile/)
|
||||
assert.match(source, /\/files\/loading-evidence\//)
|
||||
assert.match(source, /loading\/evidence\/pda\?tenantId=/)
|
||||
assert.match(source, /loading\/evidence\/\$\{encodeURIComponent\(evidenceId\)\}\/void\/pda\?tenantId=/)
|
||||
})
|
||||
|
||||
test('PDA adapter does not treat non-200 response bodies or network failures as success', () => {
|
||||
@@ -46,6 +48,11 @@ test('picking page exposes five evidence slots, retry and remote completion stat
|
||||
assert.match(page, /uploadEvidenceFile/)
|
||||
assert.match(page, /submitting \|\| completedBoxes\[currentBoxIndex\]/)
|
||||
assert.match(page, /completePicking\(/)
|
||||
assert.match(page, /sourceType: \['camera'\]/)
|
||||
assert.doesNotMatch(page, /sourceType: \['camera', 'album'\]/)
|
||||
assert.match(page, /evidenceEntries\(slot\.type\)/)
|
||||
assert.match(page, /voidEvidencePhoto\(/)
|
||||
assert.match(page, /evidenceComplete/)
|
||||
})
|
||||
|
||||
test('PDA cabinet picking adapter exposes online claim and item action contracts', () => {
|
||||
@@ -59,4 +66,7 @@ test('PDA cabinet picking adapter exposes online claim and item action contracts
|
||||
assert.match(page, /claimContainerPickingTask\(/)
|
||||
assert.match(page, /submitContainerPickingBatch\(/)
|
||||
assert.match(page, /任务版本已变化,请刷新后重试/)
|
||||
assert.match(source, /locationIds: action\.locationIds \|\| \[\]/)
|
||||
assert.match(page, /availablePickQuantity/)
|
||||
assert.match(page, /库存\/库位可用量不足/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user