发布:将已验 TEST 功能更新至既有生产灰度(2026-10-23) #15

Merged
admin merged 69 commits from release/test into main 2026-10-23 10:51:31 +08:00
5 changed files with 49 additions and 5 deletions
Showing only changes of commit 4db8700817 - Show all commits
@@ -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', () => {