fix: lock dropship inbound plan pallet count

This commit is contained in:
2026-09-14 16:38:51 +08:00
parent e3baf69db5
commit 33520e8fd3
3 changed files with 69 additions and 5 deletions
@@ -287,6 +287,40 @@ describe('LastMileOrdersView pending order and container conversion', () => {
expect(payload).not.toHaveProperty('plannedWarehouseName')
})
it('locks one-click dropship manual plans to the frozen source pallet count', async () => {
vi.mocked(getContainerInboundPlanPreview).mockResolvedValue({
...preview,
sourcePalletCount: 1,
groups: [{
...preview.groups[0], destinationType: 'ONE_CLICK_DROPSHIP', sourcePalletCount: 1,
orders: [{
...preview.groups[0].orders[0], orderType: 'ONE_CLICK_DROPSHIP',
destinationType: 'ONE_CLICK_DROPSHIP', sourcePalletCount: 1,
targetPalletCount: 1, supplementPalletCount: 0,
}],
}],
} as never)
const wrapper = await openPending()
await selectRows(wrapper, [selectedRow({ orderTypeCode: 'ONE_CLICK_DROPSHIP', pallets: 2 })])
await button(wrapper, '生成入库计划')!.trigger('click')
await flushPromises()
const palletInput = wrapper.find('.command-form').findComponent({ name: 'ElInputNumber' })
expect(palletInput.props('disabled')).toBe(true)
expect(palletInput.props('modelValue')).toBe(1)
expect(document.body.textContent).toContain('固定按头程已封托托盘数整体入库')
wrapper.find('.command-form').findComponent({ name: 'ElDatePicker' })
.vm.$emit('update:modelValue', '2026-09-08')
await wrapper.vm.$nextTick()
;([...document.body.querySelectorAll('button')]
.find((item) => item.textContent?.includes('保存并生成入库单')) as HTMLButtonElement).click()
await flushPromises()
expect(createManualInboundPlan).toHaveBeenCalledWith(expect.objectContaining({ palletCount: 1 }),
expect.stringContaining('manual-inbound-plan-'))
})
it('opens a cabinet selector without requiring order selection and loads server summaries only then', async () => {
const wrapper = await openPending()
await openContainerDialog(wrapper)
@@ -49,6 +49,13 @@ const inboundPlanWarehouseLabel = computed(() => [actionTarget.value?.overseasWa
.filter((value) => value && value !== '—')
.join(' · '))
const inboundPlanMinimumPalletCount = ref(1)
const inboundPlanSourcePalletCount = ref<number | null>(null)
const inboundPlanPreviewLoading = ref(false)
const inboundPlanPreviewError = ref('')
const inboundPlanIsDropship = computed(() => actionTarget.value?.orderTypeCode === 'ONE_CLICK_DROPSHIP')
const inboundPlanSubmitDisabled = computed(() => inboundPlanIsDropship.value
&& (inboundPlanPreviewLoading.value || inboundPlanSourcePalletCount.value == null
|| inboundPlanSourcePalletCount.value <= 0))
const inboundPlanForm = reactive({ plannedArrivalDate: '', inboundCode: '', palletCount: 1, palletType: 'WOOD' as 'STANDARD' | 'WOOD' | 'PAPER', assigned: false, mergeRequired: false, urgent: false, remark: '' })
const holdOpen = ref(false)
const holdForm = reactive({ held: true, chargeable: true, reason: '' })
@@ -481,6 +488,8 @@ function openInboundPlan(source?: object) {
const volumeMinimum = typeof target.volume === 'number' && target.volume > 0 ? Math.ceil(target.volume / 2) : 1
inboundPlanMinimumPalletCount.value = Math.max(1, volumeMinimum,
typeof target.pallets === 'number' ? target.pallets : 0)
inboundPlanSourcePalletCount.value = null
inboundPlanPreviewError.value = ''
Object.assign(inboundPlanForm, { plannedArrivalDate: '', inboundCode: target.inboundCode === '—' ? '' : target.inboundCode, palletCount: inboundPlanMinimumPalletCount.value, palletType: 'WOOD', assigned: false, mergeRequired: false, urgent: false, remark: '' })
actionError.value = null
inboundPlanOpen.value = true
@@ -488,20 +497,39 @@ function openInboundPlan(source?: object) {
}
async function refreshManualPalletMinimum(target: LastMileOrderListItem) {
if (!target.sourceContainerBookingId) return
inboundPlanPreviewLoading.value = true
inboundPlanPreviewError.value = ''
try {
const preview = await getContainerInboundPlanPreview(target.sourceContainerBookingId)
if (actionTarget.value?.id !== target.id || !inboundPlanOpen.value) return
const order = preview.groups.flatMap((group) => group.orders).find((item) => item.orderId === target.id)
if (target.orderTypeCode === 'ONE_CLICK_DROPSHIP') {
const sourcePalletCount = Number(order?.sourcePalletCount || 0)
if (sourcePalletCount <= 0) throw new Error('一件代发订单缺少头程封托托盘快照')
inboundPlanSourcePalletCount.value = sourcePalletCount
inboundPlanMinimumPalletCount.value = sourcePalletCount
inboundPlanForm.palletCount = sourcePalletCount
return
}
const suggested = Number(order?.suggestedPalletCount || 0)
inboundPlanMinimumPalletCount.value = Math.max(inboundPlanMinimumPalletCount.value, suggested)
inboundPlanForm.palletCount = Math.max(inboundPlanForm.palletCount, inboundPlanMinimumPalletCount.value)
} catch {
// The backend repeats the same calculation and remains authoritative on submit.
} catch (error) {
if (actionTarget.value?.id === target.id && inboundPlanOpen.value) {
inboundPlanPreviewError.value = getApiErrorDetails(error, '柜级托盘预览加载失败,请重试').message
}
} finally {
if (actionTarget.value?.id === target.id) inboundPlanPreviewLoading.value = false
}
}
async function submitInboundPlan() {
const row = actionTarget.value
const frozenInboundCode = String(row?.inboundCode || '').trim()
if (inboundPlanIsDropship.value && (inboundPlanSourcePalletCount.value == null
|| inboundPlanForm.palletCount !== inboundPlanSourcePalletCount.value)) {
ElMessage.warning('一件代发必须按头程已封托托盘数整体生成入库计划')
return
}
if (!row || !row.overseasWarehouseId || !inboundPlanForm.plannedArrivalDate || !frozenInboundCode || frozenInboundCode === '—' || inboundPlanForm.palletCount < inboundPlanMinimumPalletCount.value) {
ElMessage.warning('当前柜须已分配接柜仓,并完整填写预计到仓日期和托数')
return
@@ -719,6 +747,7 @@ function blockerText(blockers: string[]) {
PLAN_ALREADY_STARTED: '已有计划已开始作业,不能再补生成托盘',
PLAN_PALLET_BINDING_INCOMPLETE: '已有计划只绑定了部分真实托盘,请联系管理员处理',
DROPSHIP_PALLET_SNAPSHOT_MISSING: '缺少一件代发托盘快照',
DROPSHIP_PALLET_COUNT_MISMATCH: '一件代发计划托数与头程已封托托盘数不一致,请重新走单',
PALLET_COUNT_NOT_READY: '计划托数暂未就绪',
}
return [...new Set(blockers.map((code) => labels[code] || '计划数据暂未就绪,请刷新或联系管理员'))].join(';') || '—'
@@ -926,10 +955,11 @@ onMounted(() => { void load() })
</LoadErrorState>
</el-form>
</AppDialog>
<AppDialog v-model="inboundPlanOpen" title="入库计划" width="min(720px, calc(100vw - 28px))" confirm-text="保存并生成入库单" :confirm-loading="actionLoading" @confirm="submitInboundPlan">
<AppDialog v-model="inboundPlanOpen" title="入库计划" width="min(720px, calc(100vw - 28px))" confirm-text="保存并生成入库单" :loading="inboundPlanPreviewLoading" :confirm-loading="actionLoading" :confirm-disabled="inboundPlanSubmitDisabled" @confirm="submitInboundPlan">
<el-alert title="保存后生成入库单;清关完成后入库计划不可更改。" type="info" :closable="false" show-icon />
<el-alert v-if="inboundPlanPreviewError" class="container-convert-alert" :title="inboundPlanPreviewError" type="error" :closable="false" show-icon />
<el-form class="command-form" label-position="top">
<div class="command-form__grid"><el-form-item label="计划入库仓"><el-input :model-value="inboundPlanWarehouseLabel" disabled placeholder="当前柜接柜仓" /><small>接柜仓按柜统一;如需变更,请在柜子工作台执行“变更接柜仓”。</small></el-form-item><el-form-item label="预计到仓日期" required><el-date-picker v-model="inboundPlanForm.plannedArrivalDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择日期" /></el-form-item><el-form-item label="入仓码"><el-input :model-value="inboundPlanForm.inboundCode" disabled placeholder="订单冻结入仓码" /></el-form-item><el-form-item label="托数" required><el-input-number v-model="inboundPlanForm.palletCount" :min="inboundPlanMinimumPalletCount" :max="999" controls-position="right" placeholder="请输入托数" /><small>按柜级预览至少 {{ inboundPlanMinimumPalletCount }} 托,可增加,不能减少。</small></el-form-item><el-form-item label="托盘类型"><el-select v-model="inboundPlanForm.palletType" placeholder="请选择托盘类型"><el-option label="标准托盘" value="STANDARD" /><el-option label="木托盘" value="WOOD" /><el-option label="纸托盘" value="PAPER" /></el-select></el-form-item><el-form-item label="是否分配"><el-switch v-model="inboundPlanForm.assigned" inline-prompt active-text="是" inactive-text="否" /></el-form-item><el-form-item label="允许合托"><el-switch v-model="inboundPlanForm.mergeRequired" inline-prompt active-text="是" inactive-text="否" /><small>仅作为现场可选提示,不强制工人合托。</small></el-form-item><el-form-item label="是否加急"><el-switch v-model="inboundPlanForm.urgent" inline-prompt active-text="是" inactive-text="否" /></el-form-item></div>
<div class="command-form__grid"><el-form-item label="计划入库仓"><el-input :model-value="inboundPlanWarehouseLabel" disabled placeholder="当前柜接柜仓" /><small>接柜仓按柜统一;如需变更,请在柜子工作台执行“变更接柜仓”。</small></el-form-item><el-form-item label="预计到仓日期" required><el-date-picker v-model="inboundPlanForm.plannedArrivalDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择日期" /></el-form-item><el-form-item label="入仓码"><el-input :model-value="inboundPlanForm.inboundCode" disabled placeholder="订单冻结入仓码" /></el-form-item><el-form-item label="托数" required><el-input-number v-model="inboundPlanForm.palletCount" :min="inboundPlanMinimumPalletCount" :max="inboundPlanIsDropship ? inboundPlanMinimumPalletCount : 999" :disabled="inboundPlanIsDropship" controls-position="right" placeholder="请输入托数" /><small v-if="inboundPlanIsDropship">一件代发固定按头程已封托托盘数整体入库,不可增减。</small><small v-else>按柜级预览至少 {{ inboundPlanMinimumPalletCount }} 托,可增加,不能减少。</small></el-form-item><el-form-item label="托盘类型"><el-select v-model="inboundPlanForm.palletType" placeholder="请选择托盘类型"><el-option label="标准托盘" value="STANDARD" /><el-option label="木托盘" value="WOOD" /><el-option label="纸托盘" value="PAPER" /></el-select></el-form-item><el-form-item label="是否分配"><el-switch v-model="inboundPlanForm.assigned" inline-prompt active-text="是" inactive-text="否" /></el-form-item><el-form-item label="允许合托"><el-switch v-model="inboundPlanForm.mergeRequired" inline-prompt active-text="是" inactive-text="否" /><small>仅作为现场可选提示,不强制工人合托。</small></el-form-item><el-form-item label="是否加急"><el-switch v-model="inboundPlanForm.urgent" inline-prompt active-text="是" inactive-text="否" /></el-form-item></div>
<el-form-item label="备注"><el-input v-model="inboundPlanForm.remark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="请输入备注(选填)" /></el-form-item>
</el-form>
</AppDialog>