发布:将已验 TEST 功能更新至既有生产灰度(2026-10-23) #15
@@ -28,6 +28,7 @@ export interface RelabelDetail {
|
||||
export interface DiscardSummary { id: string; discardNo: string; outboundNo?: string; inboundCodes: string; overseasWarehouseId: string; overseasWarehouseName: string; totalPackages: number; totalFee: number; currency: string; status: string; createdAt: string; completedAt?: string }
|
||||
export interface DiscardCandidate { orderId: string; sourceType?: 'DROPSHIP' | 'RETURN_CARTON'; returnCartonId?: string; cartonMark?: string; inboundCode: string; inboundOrderNo: string; overseasWarehouseId: string; overseasWarehouseName: string; availablePackages: number; skus: Array<{ skuCode: string; productName: string; availablePackages: number }> }
|
||||
export interface RelabelInventoryCandidate {
|
||||
cartonSkuItems: Array<{ skuCode: string; quantity: number }>; cartonAvailablePackages: number | string
|
||||
skuConversions?: import('./returnInventory').ReturnSkuConversion[]
|
||||
stockQuantity?: number; inventoryStatus?: string; occupiedRelabelNo?: string; mergedIntoCartonMarks?: string[]; hasOutbound?: boolean
|
||||
inventoryBalanceId: string; inboundOrderId: string; inboundOrderNo?: string; inboundCode?: string
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import ts from 'typescript'
|
||||
|
||||
const source = readFileSync(new URL('./returnRelabelCandidates.ts', import.meta.url), 'utf8')
|
||||
const js = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 } }).outputText
|
||||
const { returnRelabelCandidate } = await import(`data:text/javascript;base64,${Buffer.from(js).toString('base64')}`)
|
||||
const converted = {
|
||||
returnCartonId: '20', cartonMark: 'NEW-BOX', availablePackages: 0, skuItems: [],
|
||||
cartonAvailablePackages: 1, cartonSkuItems: [{ skuCode: 'NEW-A', quantity: 10 }, { skuCode: 'NEW-B', quantity: 5 }],
|
||||
}
|
||||
|
||||
test('carton labeling selects the current box and both converted SKUs', () => {
|
||||
const candidate = returnRelabelCandidate(converted, 'carton')
|
||||
assert.equal(candidate.cartonMark, 'NEW-BOX')
|
||||
assert.equal(candidate.availablePackages, 1)
|
||||
assert.equal(candidate.skuCode, 'NEW-A,NEW-B')
|
||||
assert.deepEqual(candidate.skuItems, converted.cartonSkuItems)
|
||||
})
|
||||
|
||||
test('SKU and combined labeling still exclude completed SKU quantities', () => {
|
||||
for (const mode of ['sku', 'carton_and_sku']) {
|
||||
assert.equal(returnRelabelCandidate(converted, mode).availablePackages, 0)
|
||||
assert.deepEqual(returnRelabelCandidate(converted, mode).skuItems, [])
|
||||
}
|
||||
})
|
||||
|
||||
test('an occupied box cannot be selected for carton labeling', () => {
|
||||
assert.equal(returnRelabelCandidate({ ...converted, cartonAvailablePackages: 0 }, 'carton').availablePackages, 0)
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { RelabelInventoryCandidate } from '../api/warehouseServices'
|
||||
|
||||
export function returnRelabelCandidate(candidate: RelabelInventoryCandidate, mode: string): RelabelInventoryCandidate {
|
||||
if (!candidate.returnCartonId || mode !== 'carton') return candidate
|
||||
return {
|
||||
...candidate,
|
||||
availablePackages: candidate.cartonAvailablePackages,
|
||||
skuItems: candidate.cartonSkuItems,
|
||||
skuCode: candidate.cartonSkuItems.map(item => item.skuCode).join(','),
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import QueryToolbar, { type QueryField, type QueryModel } from '@/components/com
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
import type { TableAction } from '@/components/common/tableActions'
|
||||
import { useAuthStore } from '@/modules/auth/public'
|
||||
import { returnRelabelCandidate } from '../utils/returnRelabelCandidates'
|
||||
import {
|
||||
createDiscard, createRelabel, createTransfer, getDiscard, getRelabel, getTransfer, resubmitRelabel,
|
||||
getTransferCartonLabel, submitTransfer, updateTransfer,
|
||||
@@ -199,7 +200,8 @@ const selectedTransferInventory = ref('')
|
||||
const selectedTransferSourceOrderIds = ref<string[]>([])
|
||||
const isReturnRelabelSource = computed(() => props.service === 'labeling' && selectedRelabelInboundOrderId.value.startsWith('RETURN_ORDER:'))
|
||||
const selectedReturnOrderNo = computed(() => isReturnRelabelSource.value ? selectedRelabelInboundOrderId.value.slice('RETURN_ORDER:'.length) : '')
|
||||
const selectedReturnCandidates = computed(() => relabelCandidates.value.filter(item => Boolean(item.returnCartonId) && returnSourceNos(item).includes(selectedReturnOrderNo.value)))
|
||||
const currentRelabelCandidates = computed(() => relabelCandidates.value.map(item => returnRelabelCandidate(item, labelingMode.value)))
|
||||
const selectedReturnCandidates = computed(() => currentRelabelCandidates.value.filter(item => Boolean(item.returnCartonId) && returnSourceNos(item).includes(selectedReturnOrderNo.value)))
|
||||
const availableReturnCandidates = computed(() => selectedReturnCandidates.value.filter(item => inventoryQuantity(item.availablePackages) > 0).map(item => ({ ...item, skuItems: (item.skuItems || []).filter(sku => inventoryQuantity(sku.quantity) > 0) })))
|
||||
const returnBoxCandidates = computed(() => availableReturnCandidates.value.filter(item => item.sourceType === 'RETURN_CARTON'))
|
||||
const returnLooseCandidates = computed(() => availableReturnCandidates.value.filter(item => item.sourceType === 'RETURN_PARCEL'))
|
||||
@@ -222,7 +224,7 @@ function returnInventoryState(candidate: RelabelInventoryCandidate) {
|
||||
}
|
||||
const relabelInboundOptions = computed(() => {
|
||||
const grouped = new Map<string, { value: string; inboundOrderNo: string; inboundCode: string; availablePackages: number; quantityUnit: '箱' | '件'; stockQuantity: number | null; units: number; hasOutbound: boolean; reasons: string[] }>()
|
||||
for (const candidate of relabelCandidates.value) {
|
||||
for (const candidate of currentRelabelCandidates.value) {
|
||||
const sources = candidate.returnCartonId ? returnSourceNos(candidate) : [String(candidate.inboundOrderId)]
|
||||
for (const sourceNo of sources) {
|
||||
const value = candidate.returnCartonId ? 'RETURN_ORDER:' + sourceNo : sourceNo
|
||||
@@ -1419,11 +1421,10 @@ async function selectOperationWarehouse() {
|
||||
}
|
||||
function selectRelabelInventory(inboundOrderId: string) {
|
||||
const rows: CargoRow[] = []
|
||||
const selectedCandidates = relabelCandidates.value.filter((item) => {
|
||||
const selectedCandidates = currentRelabelCandidates.value.filter((item) => {
|
||||
return item.returnCartonId ? returnSourceNos(item).includes(inboundOrderId.slice('RETURN_ORDER:'.length)) : String(item.inboundOrderId) === inboundOrderId
|
||||
})
|
||||
if (inboundOrderId.startsWith('RETURN_ORDER:')) {
|
||||
labelingMode.value = 'sku'
|
||||
for (const candidate of selectedCandidates) rows.push({
|
||||
id: ++cargoSequence, sku: candidate.skuItems?.[0]?.skuCode || candidate.skuCode || '',
|
||||
name: candidate.productNameZh || '', included: false, quantity: 1, availableQuantity: 1,
|
||||
|
||||
@@ -7,7 +7,7 @@ const source = view.slice(view.indexOf('function returnSourceNos'), view.indexOf
|
||||
function setup(candidates) {
|
||||
const output = ts.transpileModule(source + '\nexports.options = relabelInboundOptions; exports.visible = visibleRelabelInboundOptions; exports.label = relabelOptionLabel', { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }).outputText
|
||||
const exports = {}, show = { value: false }, search = { value: '' }, selected = { value: '' }
|
||||
new Function('exports', 'computed', 'relabelCandidates', 'showOccupiedRelabelInventory', 'relabelInventorySearch', 'selectedRelabelInboundOrderId', 'inventoryQuantity', output)(exports, fn => ({ get value() { return fn() } }), { value: candidates }, show, search, selected, value => Math.max(0, Number(value) || 0))
|
||||
new Function('exports', 'computed', 'currentRelabelCandidates', 'showOccupiedRelabelInventory', 'relabelInventorySearch', 'selectedRelabelInboundOrderId', 'inventoryQuantity', output)(exports, fn => ({ get value() { return fn() } }), { value: candidates }, show, search, selected, value => Math.max(0, Number(value) || 0))
|
||||
return { ...exports, show, search, selected }
|
||||
}
|
||||
test('occupied return keeps stock facts but remains hidden when searched', () => {
|
||||
|
||||
Reference in New Issue
Block a user