This commit is contained in:
2026-08-14 19:02:34 +08:00
parent 44ee2be246
commit 4dec54feef
14 changed files with 215 additions and 54 deletions
@@ -0,0 +1,30 @@
<script setup lang="ts">
import AppDialog from '@/shared/components/AppDialog.vue'
import GlobalSmartLoadingView from '../pages/GlobalSmartLoadingView.vue'
const open = defineModel<boolean>({ default: false })
</script>
<template>
<AppDialog
v-model="open"
class="smart-loading-dialog"
title="智能装柜"
width="min(1600px, 98vw)"
cancel-text="关闭"
:show-confirm="false"
destroy-on-close
>
<GlobalSmartLoadingView embedded />
</AppDialog>
</template>
<style scoped>
:global(.smart-loading-dialog.app-dialog.el-dialog) { width: min(1600px, 98vw); max-height: 94vh; margin-block: 3vh !important; }
:global(.smart-loading-dialog .el-dialog__body) { padding: 0 16px 16px; }
:global(.smart-loading-dialog .app-dialog__body) { min-width: 0; }
@media (max-width: 720px) {
:global(.smart-loading-dialog.app-dialog.el-dialog) { width: calc(100vw - 12px); max-height: calc(100svh - 12px); margin-block: 6px !important; }
:global(.smart-loading-dialog .el-dialog__body) { padding: 0 8px 8px; }
}
</style>
@@ -35,8 +35,8 @@ describe('ContainersView', () => {
vi.mocked(listAllPlatformWarehouses).mockResolvedValue([])
vi.mocked(listPlatformPartners).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 1 })
vi.mocked(listOceanBookings).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 1 })
vi.mocked(listContainers).mockResolvedValue({ records: [{ id: '9001', planId: null, bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', warehouseId: '8', warehouseName: '义乌仓', containerType: '40HQ', planStatus: 'WAIT_LOADING', version: 0, billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null, departurePortCode: null, departurePortName: '宁波', destinationPortCode: null, destinationPortName: '长滩', destinationWarehouseCode: null, destinationWarehouseName: null, capacityCbm: 68, maxWeightKg: 28000, terminalTruckingCompany: null, loadingAt: null, estimatedSailingAt: null, actualSailingAt: null, estimatedPortArrivalAt: null, actualPortArrivalAt: null, estimatedWarehouseArrivalAt: null, actualWarehouseArrivalAt: null }], page: 1, size: 20, total: 1, pages: 1 })
vi.mocked(getContainer).mockResolvedValue({ id: '9001', planId: null, bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', warehouseId: '8', warehouseName: '义乌仓', containerType: '40HQ', planStatus: 'WAIT_LOADING', version: 0, billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null, departurePortCode: null, departurePortName: '宁波', destinationPortCode: null, destinationPortName: '长滩', destinationWarehouseCode: null, destinationWarehouseName: null, capacityCbm: 68, maxWeightKg: 28000, terminalTruckingCompany: null, loadingAt: null, estimatedSailingAt: null, actualSailingAt: null, estimatedPortArrivalAt: null, actualPortArrivalAt: null, estimatedWarehouseArrivalAt: null, actualWarehouseArrivalAt: null })
vi.mocked(listContainers).mockResolvedValue({ records: [{ id: '9001', planId: null, bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', warehouseId: '8', warehouseName: '义乌仓', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', containerType: '40HQ', planStatus: 'WAIT_LOADING', version: 0, billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null, departurePortCode: null, departurePortName: '宁波', destinationPortCode: null, destinationPortName: '长滩', destinationWarehouseCode: null, destinationWarehouseName: null, capacityCbm: 68, maxWeightKg: 28000, terminalTruckingCompany: null, loadingAt: null, estimatedSailingAt: null, actualSailingAt: null, estimatedPortArrivalAt: null, actualPortArrivalAt: null, estimatedWarehouseArrivalAt: null, actualWarehouseArrivalAt: null }], page: 1, size: 20, total: 1, pages: 1 })
vi.mocked(getContainer).mockResolvedValue({ id: '9001', planId: null, bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', warehouseId: '8', warehouseName: '义乌仓', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', containerType: '40HQ', planStatus: 'WAIT_LOADING', version: 0, billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null, departurePortCode: null, departurePortName: '宁波', destinationPortCode: null, destinationPortName: '长滩', destinationWarehouseCode: null, destinationWarehouseName: null, capacityCbm: 68, maxWeightKg: 28000, terminalTruckingCompany: null, loadingAt: null, estimatedSailingAt: null, actualSailingAt: null, estimatedPortArrivalAt: null, actualPortArrivalAt: null, estimatedWarehouseArrivalAt: null, actualWarehouseArrivalAt: null })
vi.mocked(getContainerOverview).mockResolvedValue({ container: { id: '9001', bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', status: 'WAIT_LOADING', warehouseId: '8', warehouseName: '义乌仓', voyageNo: null, vesselName: null, version: 0 }, loadingPlanId: null, pda: null, documents: [], exception: { active: false, reason: null, markedBy: null, markedByName: null, markedAt: null }, exceptionHistory: [], timeline: [] })
})
@@ -96,7 +96,7 @@ describe('ContainersView', () => {
it('routes each plan status to its owning workflow without reopening pushed drafts', async () => {
const base = {
id: '9001', planId: '9101', bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', warehouseId: '8', warehouseName: '义乌仓', containerType: '40HQ', version: 0,
id: '9001', planId: '9101', bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', warehouseId: '8', warehouseName: '义乌仓', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', containerType: '40HQ', version: 0,
billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null,
departurePortCode: null, departurePortName: '宁波', destinationPortCode: null, destinationPortName: '长滩',
destinationWarehouseCode: null, destinationWarehouseName: null, capacityCbm: 68, maxWeightKg: 28000,
@@ -41,7 +41,7 @@ vi.mock('../api/platformContainerLoading', () => ({
const container = {
id: '9007199254740993', planId: '9007199254740994', bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1',
warehouseId: '8', warehouseName: '义乌仓', containerType: '40HQ', planStatus: 'EDITING' as const, version: 1,
warehouseId: '8', warehouseName: '义乌仓', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', containerType: '40HQ', planStatus: 'EDITING' as const, version: 1,
billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null,
departurePortCode: null, departurePortName: null, destinationPortCode: null, destinationPortName: null,
destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', capacityCbm: 68, maxWeightKg: 28000,
@@ -59,7 +59,7 @@ describe('GlobalSmartLoadingView', () => {
vi.clearAllMocks()
vi.mocked(listContainers).mockResolvedValue({ records: [container], page: 1, size: 100, total: 1, pages: 1 })
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue(plan)
vi.mocked(listSmartLoadingCandidates).mockResolvedValue([{ accountId: '1', inboundId: '5', inboundDetailId: '6', orderId: '7', orderNo: 'ORD-2', inboundCode: 'IN-2', warehouseId: '8', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '另一企业', goodsName: '鞋类', availablePackageCount: 12, unitWeightKg: 10, unitVolumeCbm: 0.2 }])
vi.mocked(listSmartLoadingCandidates).mockResolvedValue([{ accountId: '1', inboundId: '5', inboundDetailId: '6', orderId: '7', orderNo: 'ORD-2', inboundCode: 'IN-2', warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '另一企业', goodsName: '鞋类', availablePackageCount: 12, unitWeightKg: 10, unitVolumeCbm: 0.2 }])
})
it('loads real containers, candidates and the selected plan into the three-column workbench', async () => {
@@ -69,13 +69,43 @@ describe('GlobalSmartLoadingView', () => {
expect(listContainers).toHaveBeenCalledWith({ page: 1, size: 100 })
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(container.id)
expect(listSmartLoadingCandidates).toHaveBeenCalledWith(container.id, container.warehouseId)
expect(wrapper.text()).toContain('候选订单与货物')
expect(wrapper.text()).toContain('候选入库单与货物')
expect(wrapper.text()).toContain('美森正班海卡包税')
expect(wrapper.text()).toContain('ORD-2')
expect(wrapper.text()).toContain('CB-1 装柜方案')
expect(wrapper.text()).toContain('LP-99')
expect(wrapper.text()).toContain('ORD-1')
})
it('groups containers by channel and reloads candidates when the channel changes', async () => {
const other = {
...container,
id: '9007199254740998',
planId: '9007199254740999',
cabinetNo: 'CB-2',
channelCode: 'HDHK',
channelName: '合德海卡包税',
}
vi.mocked(listContainers).mockResolvedValue({ records: [container, other], page: 1, size: 100, total: 2, pages: 1 })
vi.mocked(getSmartLoadingPlanByBooking).mockImplementation(async (bookingId) => ({ ...plan, bookingId }))
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.findAll('.container-option')).toHaveLength(1)
expect(wrapper.text()).toContain('CB-1')
expect(wrapper.text()).not.toContain('CB-2')
const tab = wrapper.findAll('[role="tab"]').find((item) => item.text().includes('合德海卡包税'))
expect(tab).toBeDefined()
await tab!.trigger('click')
await flushPromises()
expect(wrapper.findAll('.container-option')).toHaveLength(1)
expect(wrapper.text()).toContain('CB-2')
expect(wrapper.text()).not.toContain('CB-1')
expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith(other.id, other.warehouseId)
})
it('filters candidates by an active name-only customer selector and accountId', () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
expect(source).toContain("listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' })")
@@ -113,7 +143,7 @@ describe('GlobalSmartLoadingView', () => {
expect(wrapper.text()).not.toContain('税率')
expect(wrapper.text()).not.toContain('是否退税')
expect(wrapper.text()).not.toContain('美森正班')
expect(wrapper.text()).not.toContain('税率')
})
it('prefers an editable container on first load so pushed plans do not show a false editing error', async () => {
@@ -31,12 +31,15 @@ import type {
SmartLoadingPlan,
} from '../types'
const props = withDefaults(defineProps<{ embedded?: boolean }>(), { embedded: false })
const access = useAccessPreviewStore()
const router = useRouter()
const containers = ref<ContainerBookingResource[]>([])
const customers = ref<PlatformTenantResource[]>([])
const customersLoading = ref(false)
const selectedContainerId = ref('')
const activeChannelCode = ref('')
const plan = ref<SmartLoadingPlan | null>(null)
const candidates = ref<SmartLoadingCandidate[]>([])
const loading = ref(false)
@@ -59,6 +62,21 @@ const canAssign = computed(() => access.can('platform:firstleg:smart-loading:ass
const canDelete = computed(() => access.can('platform:firstleg:smart-loading:delete'))
const canPush = computed(() => access.can('platform:firstleg:smart-loading:push'))
const selectedContainer = computed(() => containers.value.find((item) => item.id === selectedContainerId.value) || null)
const channelTabs = computed(() => {
const channels = new Map<string, { code: string; name: string; containerCount: number }>()
for (const item of containers.value) {
if (item.planStatus === 'COMPLETED' || !item.channelCode) continue
const current = channels.get(item.channelCode)
if (current) current.containerCount += 1
else channels.set(item.channelCode, {
code: item.channelCode,
name: item.channelName || item.channelCode,
containerCount: 1,
})
}
return [...channels.values()]
})
const activeChannel = computed(() => channelTabs.value.find((item) => item.code === activeChannelCode.value) || null)
const editable = computed(() => plan.value?.status === 'EDITING' || plan.value?.status === 'LOADING')
const selectedPlanStatus = computed(() => selectedContainer.value?.planStatus || null)
const assignedDetailIds = computed(() => new Set((plan.value?.items || []).map((item) => item.inboundDetailId)))
@@ -88,6 +106,7 @@ const filteredContainers = computed(() => {
const status = String(appliedQuery.value.status || '')
return containers.value.filter((item) => {
if (item.planStatus === 'COMPLETED') return false
if (!activeChannelCode.value || item.channelCode !== activeChannelCode.value) return false
if (status && item.planStatus !== status) return false
if (!keyword) return true
return [item.cabinetNo, item.containerNo, item.bookingNo, item.destinationWarehouseCode, item.destinationWarehouseName]
@@ -215,6 +234,9 @@ async function loadAll(preserved: Record<string, number> = {}) {
? await Promise.all(Array.from({ length: firstPage.pages - 1 }, (_, index) => listContainers({ page: index + 2, size: 100 })))
: []
containers.value = [firstPage, ...remainingPages].flatMap((page) => page.records)
if (!channelTabs.value.some((item) => item.code === activeChannelCode.value)) {
activeChannelCode.value = channelTabs.value[0]?.code || ''
}
if (!filteredContainers.value.some((item) => item.id === selectedContainerId.value)) selectedContainerId.value = preferredContainerId()
await loadSelectedData(preserved)
} catch (error) {
@@ -241,6 +263,13 @@ async function selectContainer(containerId: string) {
mutationError.value = null
await loadSelectedData()
}
async function selectChannel(channelCode: string) {
if (activeChannelCode.value === channelCode) return
activeChannelCode.value = channelCode
selectedContainerId.value = preferredContainerId()
mutationError.value = null
await loadSelectedData()
}
async function applyFilters(model: QueryModel) {
appliedQuery.value = { ...model }
if (!filteredContainers.value.some((item) => item.id === selectedContainerId.value)) {
@@ -347,8 +376,23 @@ onMounted(() => { void load(); void loadCustomers() })
</script>
<template>
<AdminPage>
<component :is="props.embedded ? 'div' : AdminPage" :class="{ 'embedded-smart-loading': props.embedded }">
<section class="global-loading-page">
<nav v-if="channelTabs.length" class="channel-tabs" aria-label="物流渠道" role="tablist">
<button
v-for="channel in channelTabs"
:key="channel.code"
type="button"
role="tab"
:aria-selected="channel.code === activeChannelCode"
:class="{ active: channel.code === activeChannelCode }"
@click="selectChannel(channel.code)"
>
<span>{{ channel.name }}</span>
<small>{{ channel.containerCount }} 个柜次</small>
</button>
</nav>
<el-alert v-else-if="containers.length" type="warning" :closable="false" show-icon title="当前柜次缺少渠道快照,暂时不能参与智能装柜" />
<QueryToolbar
v-model="query"
ariaLabel="智能装柜筛选"
@@ -376,7 +420,7 @@ onMounted(() => { void load(); void loadCustomers() })
<section class="dispatch-grid" aria-label="智能装柜调度台">
<section class="dispatch-pane candidate-pane">
<header class="pane-header"><div><strong>候选订单与货物</strong><small>{{ selectedContainer?.warehouseName || '请先选择柜次' }}</small></div></header>
<header class="pane-header"><div><strong>候选入库单与货物</strong><small>{{ activeChannel?.name || '未选择渠道' }} · {{ selectedContainer?.warehouseName || '请先选择柜次' }}</small></div></header>
<DataTable
:data="filteredCandidates"
:columns="candidateColumns"
@@ -407,7 +451,7 @@ onMounted(() => { void load(); void loadCustomers() })
</section>
<aside class="dispatch-pane container-pane" aria-label="货柜列表">
<header class="pane-header"><div><strong>货柜</strong><small>待装柜、预装柜中与拣货中</small></div></header>
<header class="pane-header"><div><strong>当前渠道柜次</strong><small>待装柜、预装柜中与拣货中</small></div></header>
<div v-loading="loading" class="container-list">
<el-empty v-if="!filteredContainers.length" description="暂无符合条件的货柜" :image-size="72" />
<button
@@ -420,6 +464,7 @@ onMounted(() => { void load(); void loadCustomers() })
>
<span class="container-option__top"><strong>{{ item.cabinetNo }}</strong><el-tag size="small" :type="statusType(item.planStatus)">{{ statusLabel(item.planStatus) }}</el-tag></span>
<span>{{ item.containerNo || '集装箱号待录入' }} · {{ item.containerType }}</span>
<span>{{ item.channelName || item.channelCode || '渠道未设置' }}</span>
<span>{{ item.warehouseName || '仓库未设置' }}</span>
<span>目的仓 {{ item.destinationWarehouseName || item.destinationWarehouseCode || '不限' }}</span>
<span v-if="item.id === selectedContainerId && plan">体积 {{ formatNumber(totalVolume) }} / {{ formatNumber(volumeLimit) }} m³</span>
@@ -478,16 +523,23 @@ onMounted(() => { void load(); void loadCustomers() })
</AppDialog>
</template>
</section>
</AdminPage>
</component>
</template>
<style scoped>
.global-loading-page { display: grid; gap: 16px; min-width: 0; }
.embedded-smart-loading { min-width: 0; }
.channel-tabs { display: flex; min-height: 48px; overflow-x: auto; border-bottom: 2px solid var(--cc-border); background: #fff; }
.channel-tabs button { display: flex; min-width: 150px; min-height: 48px; align-items: center; justify-content: center; gap: 7px; padding: 8px 18px; border: 0; border-bottom: 2px solid transparent; background: transparent; color: var(--cc-text); cursor: pointer; }
.channel-tabs button.active { margin-bottom: -2px; border-bottom-color: var(--el-color-primary); color: var(--el-color-primary); font-weight: 600; }
.channel-tabs button:focus-visible { outline: 2px solid var(--el-color-primary); outline-offset: -3px; }
.channel-tabs small { color: var(--cc-muted); font-weight: 400; white-space: nowrap; }
.summary-band { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 1px; border: 1px solid var(--cc-border); background: var(--cc-border); }
.summary-band > div { display: grid; gap: 4px; padding: 12px 14px; background: #fff; }
.summary-band span,.pane-header small,.container-option span,.global-loading-page small { color: var(--cc-muted); font-size: 12px; }
.summary-band strong { font-size: 16px; }
.dispatch-grid { display: grid; grid-template-columns: minmax(390px, 1.1fr) 270px minmax(460px, 1.35fr); min-height: 620px; overflow-x: auto; border: 1px solid var(--cc-border); background: #fff; }
.embedded-smart-loading .dispatch-grid { min-height: 520px; height: min(62vh, 700px); }
.dispatch-pane { display: flex; min-width: 0; flex-direction: column; }
.dispatch-pane + .dispatch-pane { border-left: 1px solid var(--cc-border); }
.pane-header { display: flex; min-height: 58px; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 12px; border-bottom: 1px solid var(--cc-border); background: #fff; }
@@ -522,5 +574,6 @@ onMounted(() => { void load(); void loadCustomers() })
.pane-header,.plan-actions,.meter-title { align-items: stretch; flex-direction: column; }
.plan-actions :deep(.el-button) { width: 100%; min-height: 40px; margin-left: 0; }
.container-list { grid-template-columns: 1fr; }
.channel-tabs button { min-width: 132px; padding-inline: 12px; }
}
</style>
@@ -18,7 +18,7 @@ vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({
const container = {
id: '9007199254740993', planId: '9007199254740994', bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1',
warehouseId: '8', warehouseName: '义乌仓', containerType: '40HQ', planStatus: 'EDITING' as const, version: 1,
warehouseId: '8', warehouseName: '义乌仓', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', containerType: '40HQ', planStatus: 'EDITING' as const, version: 1,
billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null,
departurePortCode: null, departurePortName: null, destinationPortCode: null, destinationPortName: null,
destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', capacityCbm: 68, maxWeightKg: 28000,
@@ -32,7 +32,7 @@ describe('SmartLoadingWorkbenchView', () => {
vi.clearAllMocks()
vi.mocked(getContainer).mockResolvedValue(container)
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({ id: '99', bookingId: container.id, planNo: 'LP-99', status: 'EDITING', containerNo: 'MSCU-1', voyageNo: null, version: 2, items: [] })
vi.mocked(listSmartLoadingCandidates).mockResolvedValue([{ accountId: '1', inboundId: '2', inboundDetailId: '3', orderId: '4', orderNo: 'ORD-1', inboundCode: 'IN-1', warehouseId: '8', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '示例企业', goodsName: '服装', availablePackageCount: 12, unitWeightKg: 10, unitVolumeCbm: 0.2 }])
vi.mocked(listSmartLoadingCandidates).mockResolvedValue([{ accountId: '1', inboundId: '2', inboundDetailId: '3', orderId: '4', orderNo: 'ORD-1', inboundCode: 'IN-1', warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '示例企业', goodsName: '服装', availablePackageCount: 12, unitWeightKg: 10, unitVolumeCbm: 0.2 }])
})
it('restores the real draft and explains candidate capacity', async () => {
-6
View File
@@ -7,12 +7,6 @@ export const containerLoadingRoutes = [
component: () => import('./pages/ContainersView.vue'),
meta: { title: '货柜与排柜', description: '维护货柜资料并进入智能排柜工作台', permission: 'platform:firstleg:container:list' },
},
{
path: 'container-loading/smart-loading',
name: 'global-smart-loading-workbench',
component: () => import('./pages/GlobalSmartLoadingView.vue'),
meta: { title: '智能装柜', description: '统一调度候选货物、货柜与装柜方案', permission: 'platform:firstleg:smart-loading:list' },
},
{
path: 'container-loading/:containerId/smart-loading',
name: 'smart-loading-workbench',
+4
View File
@@ -8,6 +8,8 @@ export interface ContainerBookingResource {
cabinetNo: string
warehouseId: string
warehouseName: string | null
channelCode: string | null
channelName: string | null
billOfLadingNo: string | null
vesselName: string | null
voyageNo: string | null
@@ -188,6 +190,8 @@ export interface SmartLoadingCandidate {
orderNo: string | null
inboundCode: string | null
warehouseId: string
channelCode: string | null
channelName: string | null
destinationWarehouseId: string | null
destinationWarehouseCode: string | null
destinationWarehouseName: string | null