fix: treat missing return records as empty results
This commit is contained in:
@@ -4,6 +4,11 @@ const BASE = '/pda/v1/last-leg/return-inventory'
|
||||
const encoded = (value) => encodeURIComponent(String(value || '').trim())
|
||||
const count = (value) => Number.isFinite(Number(value)) ? Math.max(0, Number(value)) : 0
|
||||
|
||||
function isNotFound(error) {
|
||||
const code = String(error?.code || '').toUpperCase()
|
||||
return code === 'A0404' || code === '404'
|
||||
}
|
||||
|
||||
function normalizeItems(items) {
|
||||
return Array.isArray(items) ? items.map((item) => ({
|
||||
skuCode: String(item.skuCode || ''), productNameZh: String(item.productNameZh || ''),
|
||||
@@ -34,7 +39,12 @@ export function normalizeReturnLabel(value = {}) {
|
||||
|
||||
export async function findPackingOrders(returnNo = '') {
|
||||
const query = String(returnNo || '').trim()
|
||||
return normalizePackingPreview(await overseasRequest('GET', `${BASE}/packing/orders${query ? `?returnNo=${encoded(query)}` : ''}`))
|
||||
try {
|
||||
return normalizePackingPreview(await overseasRequest('GET', `${BASE}/packing/orders${query ? `?returnNo=${encoded(query)}` : ''}`))
|
||||
} catch (error) {
|
||||
if (isNotFound(error)) return normalizePackingPreview()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function mergeReturnCartons(request) {
|
||||
|
||||
@@ -16,6 +16,11 @@ function normalizeItem(item = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function isNotFound(error) {
|
||||
const code = String(error?.code || '').toUpperCase()
|
||||
return code === 'A0404' || code === '404'
|
||||
}
|
||||
|
||||
export function normalizeReturnReceiptStart(value = {}) {
|
||||
return {
|
||||
returnNo: String(value.returnNo || ''),
|
||||
@@ -54,11 +59,22 @@ export function normalizeReturnReceiptResult(value = {}) {
|
||||
}
|
||||
|
||||
export async function startReturnReceipt(returnNo) {
|
||||
return normalizeReturnReceiptStart(await overseasRequest('POST', `${BASE}/start`, { returnNo: String(returnNo || '').trim() }))
|
||||
try {
|
||||
return normalizeReturnReceiptStart(await overseasRequest('POST', `${BASE}/start`, { returnNo: String(returnNo || '').trim() }))
|
||||
} catch (error) {
|
||||
if (isNotFound(error)) return null
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function findReturnSkuCandidates(skuCode) {
|
||||
return normalizeReturnSkuCandidates(await overseasRequest('POST', `${BASE}/sku-candidates`, { skuCode: String(skuCode || '').trim() }))
|
||||
const normalizedSkuCode = String(skuCode || '').trim()
|
||||
try {
|
||||
return normalizeReturnSkuCandidates(await overseasRequest('POST', `${BASE}/sku-candidates`, { skuCode: normalizedSkuCode }))
|
||||
} catch (error) {
|
||||
if (isNotFound(error)) return normalizeReturnSkuCandidates({ skuCode: normalizedSkuCode, candidates: [] })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function confirmReturnReceipt(request) {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<view v-if="user" class="page-shell portal-page work-page">
|
||||
<PortalHeader title="退货合箱" :user="user" fallback-url="/pages/module/module?key=inbound" />
|
||||
<scroll-view class="work-scroll" scroll-y><view class="portal-content work-content">
|
||||
<view class="work-heading"><text class="work-title">退货合箱</text><text class="work-subtitle">仅可合并当前仓、同一客户、已完成签收的退货单库存</text></view>
|
||||
<view v-if="!result" class="work-card"><text class="card-title">添加退货单</text><ScanTargetInput name="returnNo" v-model="searchCode" label="退货单号" placeholder="扫描或输入完整退货单号" :active="true" @submit="search" /><button class="primary" :disabled="!searchCode.trim() || busy" @tap="search(searchCode)">{{ busy ? '查询中…' : '查询可合箱库存' }}</button></view>
|
||||
|
||||
<view v-if="candidates !== null && !result" class="section-gap"><view v-if="!candidates.length" class="empty-box"><text class="empty-copy">没有匹配的已完成退货单或可用库存</text></view><view v-else class="task-list"><button v-for="item in candidates" :key="item.returnNo" class="task-card candidate-card" @tap="addOrder(item)"><view class="row-head"><view><text class="business-code">{{ item.returnNo }}</text><text class="meta-line">客户参考号:{{ item.customerReferenceNo || '—' }}</text></view><text class="select-link">添加 ›</text></view></button></view></view>
|
||||
@@ -46,7 +45,7 @@
|
||||
methods: {
|
||||
startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: (value) => { if (!this.busy) { this.searchCode = value; this.search(value) } }, onError: (error) => this.setError(error, '扫码广播注册失败') }) },
|
||||
stopScanner() { if (this._stopScanner) this._stopScanner(); this._stopScanner = null },
|
||||
async search(raw) { const value = String(raw?.detail?.value ?? raw ?? this.searchCode).trim(); if (!value || this.busy) return; this.searchCode = value; await this.run(async () => { const preview = await findPackingOrders(value); this.candidates = preview.orders.filter((item) => !this.selectedOrders.some((selected) => selected.returnNo === item.returnNo)); if (!this.candidates.length) playErrorSound(); else playSuccessSound() }, '查询可合箱退货单失败') },
|
||||
async search(raw) { const value = String(raw?.detail?.value ?? raw ?? this.searchCode).trim(); if (!value || this.busy) return; this.searchCode = value; await this.run(async () => { const preview = await findPackingOrders(value); this.candidates = preview.orders.filter((item) => !this.selectedOrders.some((selected) => selected.returnNo === item.returnNo)); if (this.candidates.length) playSuccessSound() }, '查询可合箱退货单失败') },
|
||||
async addOrder(item) { if (this.busy || !item?.returnNo) return; await this.run(async () => { const preview = await findPackingOrders(item.returnNo); const exact = preview.orders.filter((order) => order.returnNo === item.returnNo); if (exact.length !== 1 || !preview.customerKey) throw new Error('请扫描完整退货单号后重试'); const customerKey = this.selectedOrders[0]?.customerKey; if (customerKey && customerKey !== preview.customerKey) throw new Error('不同客户的退货单不能合箱'); const selected = [...this.selectedOrders, { ...exact[0], customerKey: preview.customerKey }]; this.previews = { ...this.previews, [item.returnNo]: preview }; updateWarehouseWork('returnPacking', { selectedOrders: selected, searchCode: '', quantities: this.validQuantities(selected, this.previews) }); this.candidates = null; playSuccessSound() }, '添加退货单失败') },
|
||||
removeOrder(returnNo) { const selected = this.selectedOrders.filter((item) => item.returnNo !== returnNo); const previews = { ...this.previews }; delete previews[returnNo]; this.previews = previews; updateWarehouseWork('returnPacking', { selectedOrders: selected, quantities: this.validQuantities(selected, previews) }) },
|
||||
validQuantities(selected, previews) { const available = new Map(); for (const order of selected) for (const item of previews[order.returnNo]?.skus || []) available.set(item.skuCode, (available.get(item.skuCode) || 0) + Number(item.availableQuantity || 0)); return Object.fromEntries([...available].map(([sku, maximum]) => [sku, Math.min(maximum, this.quantity(sku))])) },
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
<view v-if="user" class="page-shell portal-page work-page">
|
||||
<PortalHeader title="退货入库" :user="user" fallback-url="/pages/module/module?key=inbound" />
|
||||
<scroll-view class="work-scroll" scroll-y><view class="portal-content work-content">
|
||||
<view class="work-heading"><text class="work-title">退货入库</text><text class="work-subtitle">当前仓:{{ warehouseName }};每次提交生成一个签收箱和仓储计费批次</text></view>
|
||||
|
||||
<view v-if="!current && !result" class="work-card">
|
||||
<text class="card-title">选择查询方式</text>
|
||||
<view class="mode-row"><button class="mode-button" :class="{ active: entryMode === 'RETURN_NO' }" @tap="switchMode('RETURN_NO')">退货单号</button><button class="mode-button" :class="{ active: entryMode === 'SKU' }" @tap="switchMode('SKU')">SKU</button></view>
|
||||
@@ -13,7 +11,7 @@
|
||||
|
||||
<view v-if="!current && candidates !== null && !result" class="section-gap">
|
||||
<view class="row-head candidate-heading"><text class="card-title">匹配退货单</text><text class="meta-line">{{ candidates.length }} 个候选</text></view>
|
||||
<view v-if="!candidates.length" class="empty-box"><text class="empty-copy">当前绑定仓没有包含该 SKU 的待签收退货单</text></view>
|
||||
<view v-if="!candidates.length" class="empty-box"><text class="empty-copy">{{ entryMode === 'SKU' ? '当前绑定仓没有包含该 SKU 的待签收退货单' : '当前绑定仓没有匹配的待签收或签收中退货单' }}</text></view>
|
||||
<view v-else class="task-list"><button v-for="item in candidates" :key="item.returnNo" class="task-card candidate-card" @tap="selectCandidate(item)"><view class="row-head"><text class="business-code">{{ item.returnNo }}</text><text class="status-pill">{{ statusLabel(item.status) }}</text></view><text class="meta-line">客户参考号:{{ item.customerReferenceNo || '—' }}</text><view class="metric-row"><view class="metric"><strong>{{ item.skuReceivedQuantity }}/{{ item.skuExpectedQuantity }}</strong><text>该 SKU</text></view><view class="metric"><strong>{{ item.orderReceivedQuantity }}/{{ item.orderExpectedQuantity }}</strong><text>整单进度</text></view><text class="select-link">选择 ›</text></view></button></view>
|
||||
</view>
|
||||
|
||||
@@ -52,7 +50,6 @@
|
||||
components: { PortalHeader, ScanTargetInput },
|
||||
data() { return { user: session.currentUser, current: null, candidates: null, result: null, activeTarget: 'lookup', busy: false, error: null } },
|
||||
computed: {
|
||||
warehouseName() { return String(this.user?.warehouseName || '').trim() || '未关联海外仓' },
|
||||
entryMode: { get() { return warehouseWork.returnReceipt.entryMode }, set(value) { updateWarehouseWork('returnReceipt', { entryMode: value }) } },
|
||||
lookupCode: { get() { return warehouseWork.returnReceipt.lookupCode }, set(value) { updateWarehouseWork('returnReceipt', { lookupCode: value }) } },
|
||||
startedBy: { get() { return warehouseWork.returnReceipt.startedBy }, set(value) { updateWarehouseWork('returnReceipt', { startedBy: value }) } },
|
||||
@@ -68,9 +65,9 @@
|
||||
stopScanner() { if (this._stopScanner) this._stopScanner(); this._stopScanner = null },
|
||||
handleScan(value) { if (this.busy) return; if (!this.current) { this.lookupCode = value; this.lookup(value); return } if (this.activeTarget === 'carton') this.acceptCarton(value); else this.acceptTracking(value) },
|
||||
switchMode(mode) { if (this.busy) return; this.entryMode = mode; this.lookupCode = ''; this.candidates = null; this.error = null; this.activeTarget = 'lookup' },
|
||||
async lookup(raw) { const value = String(raw?.detail?.value ?? raw ?? this.lookupCode).trim(); if (!value || this.busy) return; this.lookupCode = value; this.error = null; this.result = null; if (this.entryMode === 'SKU') await this.run(async () => { const response = await findReturnSkuCandidates(value); this.candidates = response.candidates; playSuccessSound() }, '查询 SKU 候选失败'); else await this.openOrder(value, 'RETURN_NO') },
|
||||
async lookup(raw) { const value = String(raw?.detail?.value ?? raw ?? this.lookupCode).trim(); if (!value || this.busy) return; this.lookupCode = value; this.error = null; this.result = null; if (this.entryMode === 'SKU') await this.run(async () => { const response = await findReturnSkuCandidates(value); this.candidates = response.candidates; if (this.candidates.length) playSuccessSound() }, '查询 SKU 候选失败'); else await this.openOrder(value, 'RETURN_NO') },
|
||||
async selectCandidate(item) { if (!item?.returnNo || this.busy) return; await this.openOrder(item.returnNo, 'SKU') },
|
||||
async openOrder(returnNo, startedBy, feedback = true) { await this.run(async () => { const value = await startReturnReceipt(returnNo); this.current = value; this.candidates = null; this.startedBy = startedBy; updateWarehouseWork('returnReceipt', { currentReturnNo: value.returnNo, startedBy, lookupCode: value.returnNo, quantities: this.validQuantities(value) }); this.activeTarget = 'tracking'; if (feedback) playSuccessSound() }, '读取退货单失败') },
|
||||
async openOrder(returnNo, startedBy, feedback = true) { await this.run(async () => { const value = await startReturnReceipt(returnNo); if (!value) { this.current = null; this.candidates = []; updateWarehouseWork('returnReceipt', { currentReturnNo: '', quantities: {} }); this.activeTarget = 'lookup'; return } this.current = value; this.candidates = null; this.startedBy = startedBy; updateWarehouseWork('returnReceipt', { currentReturnNo: value.returnNo, startedBy, lookupCode: value.returnNo, quantities: this.validQuantities(value) }); this.activeTarget = 'tracking'; if (feedback) playSuccessSound() }, '读取退货单失败') },
|
||||
validQuantities(order) { const source = warehouseWork.returnReceipt.quantities || {}; return Object.fromEntries(order.expectedItems.map((item) => [item.skuCode, Math.min(Math.max(0, Number(source[item.skuCode]) || 0), this.remaining(item))])) },
|
||||
quantity(skuCode) { return Math.max(0, Number(warehouseWork.returnReceipt.quantities?.[skuCode]) || 0) },
|
||||
remaining(item) { return Math.max(0, Number(item.expectedQuantity || 0) - Number(item.receivedQuantity || 0)) },
|
||||
|
||||
@@ -28,3 +28,15 @@ test('return packing API uses exact PDA inventory routes without submitting cust
|
||||
assert.ok(calls.every((item) => item.authorization === 'Bearer packing-token'))
|
||||
assert.ok(calls.every((item) => !Object.hasOwn(item.data || {}, 'customerId') && !Object.hasOwn(item.data || {}, 'warehouseId')))
|
||||
})
|
||||
|
||||
test('return packing lookup treats not found as an empty preview', async () => {
|
||||
session.apiBase = '/overseas-api'; session.token = 'packing-token'
|
||||
globalThis.uni = { request(options) { options.success({ statusCode: 404, data: { code: 'A0404', message: '没有匹配的可合箱库存' }, header: {} }) } }
|
||||
assert.deepEqual(await findPackingOrders('MISSING-RETURN'), { customerKey: '', orders: [], skus: [] })
|
||||
})
|
||||
|
||||
test('return packing lookup still rejects real service errors', async () => {
|
||||
session.apiBase = '/overseas-api'; session.token = 'packing-token'
|
||||
globalThis.uni = { request(options) { options.success({ statusCode: 500, data: { code: 'B0500', message: '服务暂不可用' }, header: {} }) } }
|
||||
await assert.rejects(() => findPackingOrders('RT-1'), (error) => error.code === 'B0500')
|
||||
})
|
||||
|
||||
@@ -50,3 +50,27 @@ test('return receipt API calls only the dedicated PDA routes and preserves the r
|
||||
assert.ok(calls.every((item) => item.authorization === 'Bearer return-token'))
|
||||
assert.ok(calls.every((item) => !Object.hasOwn(item.data || {}, 'customerId') && !Object.hasOwn(item.data || {}, 'warehouseId')))
|
||||
})
|
||||
|
||||
test('return receipt lookup treats A0404 and HTTP 404 as empty results', async () => {
|
||||
session.apiBase = '/overseas-api'; session.token = 'return-token'
|
||||
let requestCount = 0
|
||||
globalThis.uni = {
|
||||
request(options) {
|
||||
requestCount += 1
|
||||
if (requestCount === 1) options.success({ statusCode: 404, data: { code: 'A0404', message: '当前仓没有待签收或签收中的退货单' }, header: {} })
|
||||
else options.success({ statusCode: 404, data: {}, header: {} })
|
||||
}
|
||||
}
|
||||
assert.equal(await startReturnReceipt('MISSING-RETURN'), null)
|
||||
assert.deepEqual(await findReturnSkuCandidates('MISSING-SKU'), { skuCode: 'MISSING-SKU', candidates: [] })
|
||||
})
|
||||
|
||||
test('return receipt lookup still rejects real service errors', async () => {
|
||||
session.apiBase = '/overseas-api'; session.token = 'return-token'
|
||||
globalThis.uni = {
|
||||
request(options) {
|
||||
options.success({ statusCode: 500, data: { code: 'B0500', message: '服务暂不可用' }, header: {} })
|
||||
}
|
||||
}
|
||||
await assert.rejects(() => startReturnReceipt('RT-001'), (error) => error.code === 'B0500')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user