修复智能装柜入仓码重复展示
This commit is contained in:
@@ -80,7 +80,7 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(wrapper.text()).not.toContain('服装')
|
||||
expect(wrapper.text()).not.toContain('鞋类')
|
||||
expect(wrapper.text()).not.toContain('SKU-1')
|
||||
expect(wrapper.text()).not.toContain('IN-1')
|
||||
expect(wrapper.text()).toContain('IN-1')
|
||||
expect(wrapper.text()).toContain('IN-2')
|
||||
expect(wrapper.text()).not.toContain('ORD-1')
|
||||
expect(wrapper.text()).not.toContain('ORD-2')
|
||||
@@ -378,6 +378,34 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(wrapper.text()).toContain('当前柜次暂无装柜明细')
|
||||
})
|
||||
|
||||
it('shows one row per inbound code and removes all internal specification rows', async () => {
|
||||
const secondDetail = {
|
||||
...plan.items[0], id: '102', inboundDetailId: '4', skuCode: 'SKU-2', productNameZh: '鞋类',
|
||||
packageCount: 4, grossWeightKg: 40, volumeCbm: 0.8, loadSequence: 2,
|
||||
}
|
||||
const groupedPlan = { ...plan, items: [plan.items[0], secondDetail] }
|
||||
const afterFirst = { ...plan, version: 3, items: [secondDetail] }
|
||||
const emptyPlan = { ...plan, version: 4, items: [] }
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValueOnce(groupedPlan).mockResolvedValue(emptyPlan)
|
||||
vi.mocked(removeSmartLoadingItem).mockResolvedValueOnce(afterFirst).mockResolvedValueOnce(emptyPlan)
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never)
|
||||
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
const inboundRows = wrapper.findAll('tr').filter((row) => row.text().includes('IN-1'))
|
||||
expect(inboundRows).toHaveLength(1)
|
||||
expect(inboundRows[0].text()).toContain('多规格;改箱数请移除后重加')
|
||||
expect(inboundRows[0].text()).toContain('100 kg')
|
||||
expect(inboundRows[0].text()).toContain('2 m³')
|
||||
|
||||
await inboundRows[0].find('.el-button--danger').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(removeSmartLoadingItem).toHaveBeenNthCalledWith(1, plan.id, plan.items[0].id, 2, 1)
|
||||
expect(removeSmartLoadingItem).toHaveBeenNthCalledWith(2, plan.id, secondDetail.id, 3, 1)
|
||||
})
|
||||
|
||||
it('uses item capabilities to protect picked quantities and removal', async () => {
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({
|
||||
...plan,
|
||||
|
||||
@@ -26,9 +26,9 @@ import {
|
||||
import type {
|
||||
ContainerBookingResource,
|
||||
SmartLoadingCandidate,
|
||||
SmartLoadingItem,
|
||||
SmartLoadingPlan,
|
||||
} from '../types'
|
||||
import { groupSmartLoadingItems, type SmartLoadingInboundItem } from '../smartLoadingItems'
|
||||
|
||||
const props = withDefaults(defineProps<{ embedded?: boolean }>(), { embedded: false })
|
||||
|
||||
@@ -79,6 +79,7 @@ const editable = computed(() => plan.value?.canEdit === true)
|
||||
const assignable = computed(() => plan.value?.canAssign === true)
|
||||
const selectedPlanStatus = computed(() => selectedContainer.value?.planStatus || null)
|
||||
const assignedInboundIds = computed(() => new Set((plan.value?.items || []).map((item) => item.inboundId)))
|
||||
const displayedPlanItems = computed(() => groupSmartLoadingItems(plan.value?.items || []))
|
||||
const totalPackages = computed(() => (plan.value?.items || []).reduce((sum, item) => sum + Number(item.packageCount || 0), 0))
|
||||
const totalVolume = computed(() => (plan.value?.items || []).reduce((sum, item) => sum + Number(item.volumeCbm || 0), 0))
|
||||
const totalWeight = computed(() => (plan.value?.items || []).reduce((sum, item) => sum + Number(item.grossWeightKg || 0), 0))
|
||||
@@ -127,6 +128,7 @@ const candidateColumns: DataTableColumn[] = [
|
||||
]
|
||||
const itemColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 170, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 190, fixed: 'left', showOverflowTooltip: true },
|
||||
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 140 },
|
||||
{ key: 'packageCount', label: '分配箱数', width: 140, slotName: 'packageCount' },
|
||||
{ key: 'grossWeightKg', label: '毛重', width: 110, slotName: 'weight' },
|
||||
@@ -138,7 +140,7 @@ function formatNumber(value: number, digits = 3) {
|
||||
}
|
||||
function percentage(value: number) { return Number.isFinite(value) ? Number(Math.max(value, 0).toFixed(2)) : 0 }
|
||||
function candidateRow(source: object) { return source as SmartLoadingCandidate }
|
||||
function itemRow(source: object) { return source as SmartLoadingItem }
|
||||
function itemRow(source: object) { return source as SmartLoadingInboundItem }
|
||||
function adjustmentMax(source: object) {
|
||||
const item = itemRow(source)
|
||||
const remaining = candidates.value.find((candidate) => candidate.inboundId === item.inboundId)?.availablePackageCount || 0
|
||||
@@ -159,7 +161,7 @@ function displayActualContainerNo(container: ContainerBookingResource | null | u
|
||||
}
|
||||
function syncDrafts(next: SmartLoadingPlan, preserved: Record<string, number> = {}) {
|
||||
for (const key of Object.keys(quantityDrafts)) delete quantityDrafts[key]
|
||||
for (const item of next.items || []) quantityDrafts[item.id] = preserved[item.id] ?? item.packageCount
|
||||
for (const item of groupSmartLoadingItems(next.items || [])) quantityDrafts[item.id] = preserved[item.id] ?? item.packageCount
|
||||
}
|
||||
function acceptPlan(next: SmartLoadingPlan, preserved: Record<string, number> = {}, syncContainerStatus = true) {
|
||||
plan.value = next
|
||||
@@ -328,34 +330,42 @@ async function adjust(source: object) {
|
||||
async function remove(source: object) {
|
||||
const item = itemRow(source)
|
||||
if (!plan.value || !item.canRemove) return
|
||||
const confirmed = await ElMessageBox.confirm(`确认从当前柜次移除“${item.customerName || '当前客户'}”的 ${item.packageCount} 箱明细吗?`, '移除装柜明细', {
|
||||
const confirmed = await ElMessageBox.confirm(`确认从当前柜次移除入仓码“${item.inboundCode || '—'}”的 ${item.packageCount} 箱吗?`, '移除装柜明细', {
|
||||
type: 'warning', confirmButtonText: '移除', cancelButtonText: '取消',
|
||||
}).catch(() => false)
|
||||
if (!confirmed || !plan.value) return
|
||||
await mutate(async () => {
|
||||
acceptPlan(await removeSmartLoadingItem(plan.value!.id, item.id, plan.value!.version, item.version))
|
||||
let current = plan.value!
|
||||
for (const original of item.sourceItems) {
|
||||
const currentItem = current.items.find((value) => value.id === original.id)
|
||||
if (!currentItem) continue
|
||||
current = await removeSmartLoadingItem(current.id, currentItem.id, current.version, currentItem.version)
|
||||
acceptPlan(current)
|
||||
}
|
||||
await loadSelectedData()
|
||||
ElMessage.success('装柜明细已移除')
|
||||
ElMessage.success('该入仓码已从当前柜次移除')
|
||||
}, '移除装柜明细失败', { pickingConflict: true })
|
||||
}
|
||||
async function removeAll() {
|
||||
const removableItems = plan.value?.items.filter((item) => item.canRemove) || []
|
||||
const removableItems = displayedPlanItems.value.filter((item) => item.canRemove)
|
||||
if (!removableItems.length) return
|
||||
const itemCount = removableItems.length
|
||||
const confirmed = await ElMessageBox.confirm(`确认移除当前方案的 ${itemCount} 条未拣明细吗?已拣明细会保留。`, '移除未拣明细', {
|
||||
const confirmed = await ElMessageBox.confirm(`确认移除当前方案的 ${itemCount} 个未拣入仓码吗?已拣入仓码会保留。`, '移除未拣明细', {
|
||||
type: 'warning', confirmButtonText: '确认移除', cancelButtonText: '取消',
|
||||
}).catch(() => false)
|
||||
if (!confirmed || !plan.value) return
|
||||
await mutate(async () => {
|
||||
let current = plan.value!
|
||||
for (const original of removableItems) {
|
||||
for (const inboundItem of removableItems) {
|
||||
for (const original of inboundItem.sourceItems) {
|
||||
const item = current.items.find((value) => value.id === original.id)
|
||||
if (!item) continue
|
||||
current = await removeSmartLoadingItem(current.id, item.id, current.version, item.version)
|
||||
acceptPlan(current)
|
||||
}
|
||||
}
|
||||
await loadSelectedData()
|
||||
ElMessage.success(`已移除 ${itemCount} 条未拣明细`)
|
||||
ElMessage.success(`已移除 ${itemCount} 个未拣入仓码`)
|
||||
}, '全部移除失败', { pickingConflict: true })
|
||||
}
|
||||
async function pushPreload() {
|
||||
@@ -528,8 +538,8 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
<div><div class="meter-title"><span>体积</span><strong>{{ formatNumber(totalVolume) }} / {{ formatNumber(volumeLimit) }} m³</strong></div><el-progress :percentage="Math.min(displayedVolumePercent, 100)" :status="volumePercent > 100 ? 'exception' : undefined" /></div>
|
||||
<div><div class="meter-title"><span>重量</span><strong>{{ formatNumber(totalWeight, 1) }} / {{ formatNumber(weightLimit, 1) }} kg</strong></div><el-progress :percentage="Math.min(displayedWeightPercent, 100)" :status="weightPercent > 100 ? 'exception' : undefined" /></div>
|
||||
</div>
|
||||
<DataTable :data="plan.items" :columns="itemColumns" :loading="selectionLoading || mutating" stripe preference-key="global-smart-loading-items" column-settings-mode="hide" empty-text="当前柜次暂无装柜明细">
|
||||
<template #actions="{ row }"><div class="row-actions"><el-button v-if="canAssign && editable && row.canAdjust" size="small" type="primary" plain :disabled="quantityDrafts[row.id] === row.packageCount" @click="adjust(row)"><Save :size="15" />保存</el-button><el-button v-if="canDelete && editable && row.canRemove" size="small" type="danger" plain @click="remove(row)"><Trash2 :size="15" />移除</el-button><small v-if="row.pickedPackageCount > 0">已拣 {{ row.pickedPackageCount }} 箱</small></div></template>
|
||||
<DataTable :data="displayedPlanItems" :columns="itemColumns" :loading="selectionLoading || mutating" stripe preference-key="global-smart-loading-items" column-settings-mode="hide" empty-text="当前柜次暂无装柜明细">
|
||||
<template #actions="{ row }"><div class="row-actions"><el-button v-if="canAssign && editable && row.canAdjust" size="small" type="primary" plain :disabled="quantityDrafts[row.id] === row.packageCount" @click="adjust(row)"><Save :size="15" />保存</el-button><el-button v-if="canDelete && editable && row.canRemove" size="small" type="danger" plain @click="remove(row)"><Trash2 :size="15" />移除整票</el-button><small v-if="row.detailCount > 1 && editable">多规格;改箱数请移除后重加</small><small v-if="row.pickedPackageCount > 0">已拣 {{ row.pickedPackageCount }} 箱</small></div></template>
|
||||
<template #packageCount="{ row }"><el-input-number v-model="quantityDrafts[row.id]" :min="row.minPackageCount" :max="adjustmentMax(row)" :step="1" :disabled="!canAssign || !editable || !row.canAdjust" controls-position="right" /></template>
|
||||
<template #weight="{ row }">{{ formatNumber(Number(row.grossWeightKg || 0), 1) }} kg</template>
|
||||
<template #volume="{ row }">{{ formatNumber(Number(row.volumeCbm || 0)) }} m³</template>
|
||||
|
||||
@@ -15,7 +15,8 @@ import {
|
||||
getSmartLoadingPlanByBooking, listSmartLoadingCandidates, pushSmartLoadingPreload,
|
||||
recommendSmartLoading, removeSmartLoadingItem,
|
||||
} from '../api/platformContainerLoading'
|
||||
import type { ContainerBookingResource, SmartLoadingCandidate, SmartLoadingItem, SmartLoadingPlan } from '../types'
|
||||
import type { ContainerBookingResource, SmartLoadingCandidate, SmartLoadingPlan } from '../types'
|
||||
import { groupSmartLoadingItems, type SmartLoadingInboundItem } from '../smartLoadingItems'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -42,6 +43,7 @@ const canViewLoadingList = computed(() => access.can('platform:firstleg:loading-
|
||||
const editable = computed(() => plan.value?.canEdit === true)
|
||||
const assignable = computed(() => plan.value?.canAssign === true)
|
||||
const assignedInboundIds = computed(() => new Set((plan.value?.items || []).map((item) => item.inboundId)))
|
||||
const displayedPlanItems = computed(() => groupSmartLoadingItems(plan.value?.items || []))
|
||||
const totalVolume = computed(() => (plan.value?.items || []).reduce((sum, item) => sum + Number(item.volumeCbm || 0), 0))
|
||||
const totalWeight = computed(() => (plan.value?.items || []).reduce((sum, item) => sum + Number(item.grossWeightKg || 0), 0))
|
||||
const volumeLimit = computed(() => Number(container.value?.capacityCbm || 0) * 1.1)
|
||||
@@ -75,7 +77,7 @@ const candidateColumns: DataTableColumn[] = [
|
||||
function formatNumber(value: number, digits = 3) { return Number.isFinite(value) ? value.toLocaleString('zh-CN', { maximumFractionDigits: digits }) : '0' }
|
||||
function percentage(value: number) { return Number.isFinite(value) ? Number(Math.max(value, 0).toFixed(2)) : 0 }
|
||||
function candidateRow(source: object) { return source as SmartLoadingCandidate }
|
||||
function itemRow(source: object) { return source as SmartLoadingItem }
|
||||
function itemRow(source: object) { return source as SmartLoadingInboundItem }
|
||||
function explainCandidate(source: object) {
|
||||
const candidate = candidateRow(source)
|
||||
const volume = Number(candidate.averageUnitVolumeCbm || 0)
|
||||
@@ -95,7 +97,7 @@ function displayContainerNo(value: ContainerBookingResource | null | undefined)
|
||||
}
|
||||
function syncDrafts(preserved: Record<string, number> = {}) {
|
||||
for (const key of Object.keys(quantityDrafts)) delete quantityDrafts[key]
|
||||
for (const item of plan.value?.items || []) quantityDrafts[item.id] = preserved[item.id] ?? item.packageCount
|
||||
for (const item of displayedPlanItems.value) quantityDrafts[item.id] = preserved[item.id] ?? item.packageCount
|
||||
}
|
||||
function acceptPlan(next: SmartLoadingPlan, preserved: Record<string, number> = {}) { plan.value = next; noDraft.value = false; syncDrafts(preserved) }
|
||||
|
||||
@@ -178,11 +180,21 @@ async function adjust(source: object) {
|
||||
async function remove(source: object) {
|
||||
const item = itemRow(source)
|
||||
if (!plan.value || !item.canRemove) return
|
||||
await ElMessageBox.confirm(`确认从计划移除“${item.productNameZh || item.inboundCode || '该货物'}”吗?`, '移除装柜明细', { type: 'warning', confirmButtonText: '移除', cancelButtonText: '取消' }).catch(() => false).then(async (confirmed) => {
|
||||
await ElMessageBox.confirm(`确认从计划移除入仓码“${item.inboundCode || '—'}”的 ${item.packageCount} 箱吗?`, '移除装柜明细', { type: 'warning', confirmButtonText: '移除', cancelButtonText: '取消' }).catch(() => false).then(async (confirmed) => {
|
||||
if (!confirmed || !plan.value) return
|
||||
mutating.value = true
|
||||
mutationError.value = null
|
||||
try { acceptPlan(await removeSmartLoadingItem(plan.value.id, item.id, plan.value.version, item.version)); ElMessage.success('装柜明细已移除') }
|
||||
try {
|
||||
let current = plan.value!
|
||||
for (const original of item.sourceItems) {
|
||||
const currentItem = current.items.find((value) => value.id === original.id)
|
||||
if (!currentItem) continue
|
||||
current = await removeSmartLoadingItem(current.id, currentItem.id, current.version, currentItem.version)
|
||||
acceptPlan(current)
|
||||
}
|
||||
await load()
|
||||
ElMessage.success('该入仓码已从装柜方案移除')
|
||||
}
|
||||
catch (error) { await handleMutationError(error, '移除失败', true) }
|
||||
finally { mutating.value = false }
|
||||
})
|
||||
@@ -251,9 +263,9 @@ onMounted(load)
|
||||
<template v-else-if="plan">
|
||||
<el-alert v-if="plan.status === 'LOADING' && plan.canEdit" type="warning" :closable="false" show-icon title="已下发 PDA,仍可维护未完成的装柜明细" description="新增、增加或移除未拣明细会刷新 PDA 明细与版本;PDA 端需要刷新后继续操作。" />
|
||||
<el-alert v-else-if="!plan.canEdit" :type="plan.finalCompletionStatus === 'COMPLETED' ? 'success' : 'info'" :closable="false" show-icon :title="plan.finalCompletionStatus === 'IN_PROGRESS' ? '正在最终确认,装柜方案暂时只读' : plan.finalCompletionStatus === 'COMPLETED' ? '装柜已完成,方案永久只读' : '当前装柜方案只读'" :description="plan.blockedReason || '请刷新计划获取最新作业状态。'" />
|
||||
<DataTable :data="plan.items" :columns="itemColumns" :loading="loading || mutating" stripe preference-key="smart-loading-items" column-settings-mode="hide" empty-text="暂无已分配货物">
|
||||
<DataTable :data="displayedPlanItems" :columns="itemColumns" :loading="loading || mutating" stripe preference-key="smart-loading-items" column-settings-mode="hide" empty-text="暂无已分配货物">
|
||||
<template #toolbar-start><div><h2>{{ displayContainerNo(container) }} 装柜方案</h2><p>智能建议必须经人工核对。</p></div></template>
|
||||
<template #actions="{ row }"><div class="row-actions"><el-button v-if="canAssign && editable && row.canAdjust" size="small" type="primary" plain :disabled="quantityDrafts[row.id] === row.packageCount" @click="adjust(row)"><Save :size="15" />保存箱数</el-button><el-button v-if="canDelete && editable && row.canRemove" size="small" type="danger" plain @click="remove(row)"><Trash2 :size="15" />移除</el-button><small v-if="row.pickedPackageCount > 0">已拣 {{ row.pickedPackageCount }} 箱</small></div></template>
|
||||
<template #actions="{ row }"><div class="row-actions"><el-button v-if="canAssign && editable && row.canAdjust" size="small" type="primary" plain :disabled="quantityDrafts[row.id] === row.packageCount" @click="adjust(row)"><Save :size="15" />保存箱数</el-button><el-button v-if="canDelete && editable && row.canRemove" size="small" type="danger" plain @click="remove(row)"><Trash2 :size="15" />移除整票</el-button><small v-if="row.detailCount > 1 && editable">含 {{ row.detailCount }} 个规格;改箱数请移除后重加</small><small v-if="row.pickedPackageCount > 0">已拣 {{ row.pickedPackageCount }} 箱</small></div></template>
|
||||
<template #order="{ row }"><strong>{{ row.inboundCode || '—' }}</strong></template>
|
||||
<template #goods="{ row }"><strong>{{ row.productNameZh || '未命名货物' }}</strong><small>{{ row.skuCode || '无 SKU' }}</small></template>
|
||||
<template #packageCount="{ row }"><el-input-number v-model="quantityDrafts[row.id]" :min="row.minPackageCount" :step="1" :disabled="!canAssign || !editable || !row.canAdjust" controls-position="right" /></template>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { SmartLoadingItem } from './types'
|
||||
import { groupSmartLoadingItems } from './smartLoadingItems'
|
||||
|
||||
function item(overrides: Partial<SmartLoadingItem> = {}): SmartLoadingItem {
|
||||
return {
|
||||
id: '1', loadingPlanId: '10', accountId: '20', inboundId: '30', inboundDetailId: '40',
|
||||
orderId: '50', orderNo: 'FL-1', inboundCode: 'NYEXX2608180003', customerName: '柏悦物流',
|
||||
destinationWarehouseId: '60', destinationWarehouseCode: 'LAX', destinationWarehouseName: '洛杉矶仓',
|
||||
skuCode: 'SKU-1', productNameZh: '货物一', packageCount: 2, productQuantity: 20,
|
||||
grossWeightKg: 662.8, volumeCbm: 0.334, loadSequence: 2, version: 1,
|
||||
pickedPackageCount: 0, minPackageCount: 1, canAdjust: true, canRemove: true,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('groupSmartLoadingItems', () => {
|
||||
it('shows multiple specification rows as one inbound-code row with summed quantities', () => {
|
||||
const grouped = groupSmartLoadingItems([
|
||||
item(),
|
||||
item({ id: '2', inboundDetailId: '41', skuCode: 'SKU-2', productNameZh: '货物二', packageCount: 2, grossWeightKg: 662.8, volumeCbm: 0.334, loadSequence: 3 }),
|
||||
])
|
||||
|
||||
expect(grouped).toHaveLength(1)
|
||||
expect(grouped[0]).toMatchObject({
|
||||
inboundCode: 'NYEXX2608180003', packageCount: 4, grossWeightKg: 1325.6,
|
||||
volumeCbm: 0.668, detailCount: 2, canAdjust: false, canRemove: true,
|
||||
skuCode: 'SKU-1、SKU-2', productNameZh: '货物一、货物二', loadSequence: 2,
|
||||
})
|
||||
expect(grouped[0].sourceItems.map((source) => source.id)).toEqual(['1', '2'])
|
||||
})
|
||||
|
||||
it('does not merge different inbound codes and keeps single-detail adjustment', () => {
|
||||
const grouped = groupSmartLoadingItems([
|
||||
item(),
|
||||
item({ id: '2', inboundId: '31', inboundCode: 'NYEXX2608180004' }),
|
||||
])
|
||||
|
||||
expect(grouped).toHaveLength(2)
|
||||
expect(grouped.every((row) => row.detailCount === 1 && row.canAdjust)).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { SmartLoadingItem } from './types'
|
||||
|
||||
export interface SmartLoadingInboundItem extends SmartLoadingItem {
|
||||
sourceItems: SmartLoadingItem[]
|
||||
detailCount: number
|
||||
}
|
||||
|
||||
function sumNullable(items: SmartLoadingItem[], value: (item: SmartLoadingItem) => number | null) {
|
||||
const values = items.map(value).filter((item): item is number => item != null)
|
||||
return values.length ? values.reduce((sum, item) => sum + Number(item || 0), 0) : null
|
||||
}
|
||||
|
||||
function joinDistinct(items: SmartLoadingItem[], value: (item: SmartLoadingItem) => string | null) {
|
||||
const values = [...new Set(items.map(value).map((item) => String(item || '').trim()).filter(Boolean))]
|
||||
return values.length ? values.join('、') : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 后端按规格保存装柜明细以保证重量、体积计算准确;作业页面按入仓码汇总展示。
|
||||
*/
|
||||
export function groupSmartLoadingItems(items: SmartLoadingItem[]): SmartLoadingInboundItem[] {
|
||||
const groups = new Map<string, SmartLoadingItem[]>()
|
||||
for (const item of items) {
|
||||
const key = `${item.accountId}:${item.inboundId}`
|
||||
groups.set(key, [...(groups.get(key) || []), item])
|
||||
}
|
||||
|
||||
return [...groups.values()].map((sourceItems) => {
|
||||
const first = sourceItems[0]
|
||||
const pickedPackageCount = sourceItems.reduce((sum, item) => sum + Number(item.pickedPackageCount || 0), 0)
|
||||
const loadSequences = sourceItems.map((item) => item.loadSequence).filter((item): item is number => item != null)
|
||||
return {
|
||||
...first,
|
||||
id: sourceItems.length === 1 ? first.id : `inbound:${first.accountId}:${first.inboundId}`,
|
||||
inboundDetailId: sourceItems.length === 1 ? first.inboundDetailId : '',
|
||||
skuCode: joinDistinct(sourceItems, (item) => item.skuCode),
|
||||
productNameZh: joinDistinct(sourceItems, (item) => item.productNameZh),
|
||||
packageCount: sourceItems.reduce((sum, item) => sum + Number(item.packageCount || 0), 0),
|
||||
productQuantity: sumNullable(sourceItems, (item) => item.productQuantity),
|
||||
grossWeightKg: sumNullable(sourceItems, (item) => item.grossWeightKg),
|
||||
volumeCbm: sumNullable(sourceItems, (item) => item.volumeCbm),
|
||||
loadSequence: loadSequences.length ? Math.min(...loadSequences) : null,
|
||||
pickedPackageCount,
|
||||
minPackageCount: sourceItems.reduce((sum, item) => sum + Number(item.minPackageCount || 0), 0),
|
||||
canAdjust: sourceItems.length === 1 && first.canAdjust,
|
||||
canRemove: sourceItems.every((item) => item.canRemove),
|
||||
sourceItems,
|
||||
detailCount: sourceItems.length,
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user