feat(overseas): manage last-mile order controls
This commit is contained in:
@@ -87,12 +87,15 @@ describe('last-mile order overseas API', () => {
|
||||
it('uses the manual inbound-plan endpoint with the complete form and idempotency evidence', async () => {
|
||||
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', message: 'success', data: {} })
|
||||
const payload = { orderId: '190000000000000099', customerId: '190000000000000001', expectedOrderVersion: 4,
|
||||
plannedWarehouseId: '193000000000200003', plannedWarehouseCode: 'US-LAX-01', plannedWarehouseName: '洛杉矶仓',
|
||||
plannedArrivalDate: '2026-09-08', inboundCode: 'NYEXX2609060063', palletCount: 8, palletType: 'STANDARD' as const,
|
||||
plannedArrivalDate: '2026-09-08', palletCount: 8, palletType: 'STANDARD' as const,
|
||||
assigned: false, mergeRequired: false, urgent: true, remark: '加急' }
|
||||
await createManualInboundPlan(payload, 'plan-key-1')
|
||||
expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans/manual', payload,
|
||||
{ headers: { 'Idempotency-Key': 'plan-key-1' } })
|
||||
expect(payload).not.toHaveProperty('inboundCode')
|
||||
expect(payload).not.toHaveProperty('plannedWarehouseId')
|
||||
expect(payload).not.toHaveProperty('plannedWarehouseCode')
|
||||
expect(payload).not.toHaveProperty('plannedWarehouseName')
|
||||
})
|
||||
|
||||
it('uses versioned idempotent order hold and complete manual-node snapshot endpoints', async () => {
|
||||
|
||||
@@ -52,6 +52,8 @@ export interface LastMileOrderSummaryDto {
|
||||
sourceOrderNo?: string | null
|
||||
sourceInboundId?: string | null
|
||||
customerName?: string | null
|
||||
overseasWarehouseId?: string | null
|
||||
overseasWarehouseCode?: string | null
|
||||
overseasWarehouseName?: string | null
|
||||
transferWarehouseName?: string | null
|
||||
transferWarehouseId?: string | null
|
||||
@@ -224,11 +226,7 @@ export interface ManualInboundPlanPayload {
|
||||
customerId: string
|
||||
orderId: string
|
||||
expectedOrderVersion: number
|
||||
plannedWarehouseId: string
|
||||
plannedWarehouseCode: string
|
||||
plannedWarehouseName: string
|
||||
plannedArrivalDate: string
|
||||
inboundCode: string
|
||||
palletCount: number
|
||||
palletType: 'STANDARD' | 'WOOD' | 'PAPER'
|
||||
assigned: boolean
|
||||
|
||||
@@ -52,6 +52,17 @@ describe('last-mile order presentation adapter', () => {
|
||||
}).owner).toBe('—')
|
||||
})
|
||||
|
||||
it('preserves the frozen overseas receiving warehouse id and code as strings', () => {
|
||||
const row = toLastMileOrderListItem({
|
||||
id: '190000000000000099', customerId: '190000000000000001', orderNo: 'LMO-260908-0001',
|
||||
overseasWarehouseId: '190000000000000123', overseasWarehouseCode: 'LAX01', overseasWarehouseName: '洛杉矶接柜仓',
|
||||
})
|
||||
|
||||
expect(row.overseasWarehouseId).toBe('190000000000000123')
|
||||
expect(row.overseasWarehouseCode).toBe('LAX01')
|
||||
expect(row.warehouse).toBe('洛杉矶接柜仓')
|
||||
})
|
||||
|
||||
it('preserves the exact server LMO short number without padding, prefixing or deriving it from id', () => {
|
||||
const serverOrderNo = 'LMO-9A7'
|
||||
const row = toLastMileOrderListItem({ id: '2092873463568596994', customerId: '100', orderNo: serverOrderNo })
|
||||
|
||||
@@ -106,6 +106,8 @@ export function toLastMileOrderListItem(dto: LastMileOrderSummaryDto): LastMileO
|
||||
inboundCode: text(dto.inboundCode),
|
||||
lxCode: text(dto.lxCode),
|
||||
owner: text(dto.customerName),
|
||||
overseasWarehouseId: String(dto.overseasWarehouseId ?? '').trim(),
|
||||
overseasWarehouseCode: String(dto.overseasWarehouseCode ?? '').trim(),
|
||||
warehouse: text(dto.overseasWarehouseName),
|
||||
transferWarehouse: text(dto.transferWarehouseName),
|
||||
fbaWarehouse: text(dto.fbaWarehouseCode),
|
||||
|
||||
@@ -73,7 +73,12 @@ const currentReceivingWarehouse = {
|
||||
id: summary.overseasWarehouseId, warehouseCode: summary.overseasWarehouseCode, warehouseName: summary.overseasWarehouseName,
|
||||
}
|
||||
function selectedRow(overrides: Record<string, unknown> = {}) {
|
||||
return { id: '2092873463568596994', customerId, sourceContainerBookingId: bookingId, orderNo: 'LMO-260827-0001', ...overrides }
|
||||
return {
|
||||
id: '2092873463568596994', customerId, sourceContainerBookingId: bookingId, orderNo: 'LMO-260827-0001',
|
||||
overseasWarehouseId: summary.overseasWarehouseId, overseasWarehouseCode: summary.overseasWarehouseCode,
|
||||
warehouse: summary.overseasWarehouseName, inboundCode: 'IN-LOCKED', version: 3, pallets: 2,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
enableAutoUnmount(afterEach)
|
||||
@@ -195,11 +200,44 @@ describe('LastMileOrdersView pending order and container conversion', () => {
|
||||
expect(document.body.textContent).toContain('计划入库仓')
|
||||
expect(document.body.textContent).toContain('预计到仓日期')
|
||||
expect(document.body.textContent).toContain('托盘类型')
|
||||
const inboundCodeInput = wrapper.findAllComponents({ name: 'ElInput' })
|
||||
.find((input) => input.props('placeholder') === '订单冻结入仓码')
|
||||
expect(inboundCodeInput?.props('disabled')).toBe(true)
|
||||
const receivingWarehouseInput = wrapper.findAllComponents({ name: 'ElInput' })
|
||||
.find((input) => input.props('placeholder') === '当前柜接柜仓')
|
||||
expect(receivingWarehouseInput?.props('disabled')).toBe(true)
|
||||
expect(receivingWarehouseInput?.props('modelValue')).toBe('LAX01 · 洛杉矶接柜仓')
|
||||
expect(document.body.textContent).toContain('接柜仓按柜统一')
|
||||
expect(listAllPlatformWarehouses).not.toHaveBeenCalled()
|
||||
expect(createManualInboundPlan).not.toHaveBeenCalled()
|
||||
expect(convertLastMileContainerToInboundPlans).not.toHaveBeenCalled()
|
||||
expect(listContainerPlanProgress).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows the order-frozen inbound code but never sends it or a per-order receiving warehouse override', async () => {
|
||||
const wrapper = await openPending()
|
||||
await selectRows(wrapper, [selectedRow()])
|
||||
await button(wrapper, '生成入库计划')!.trigger('click')
|
||||
await flushPromises()
|
||||
const datePicker = wrapper.find('.command-form').findComponent({ name: 'ElDatePicker' })
|
||||
datePicker.vm.$emit('update:modelValue', '2026-09-08')
|
||||
await wrapper.vm.$nextTick()
|
||||
const confirm = [...document.body.querySelectorAll('button')]
|
||||
.find((item) => item.textContent?.includes('保存并生成入库单')) as HTMLButtonElement
|
||||
confirm.click()
|
||||
await flushPromises()
|
||||
|
||||
expect(createManualInboundPlan).toHaveBeenCalledWith(expect.objectContaining({
|
||||
customerId, orderId: '2092873463568596994', expectedOrderVersion: 3,
|
||||
plannedArrivalDate: '2026-09-08', palletCount: 2,
|
||||
}), expect.stringContaining('manual-inbound-plan-'))
|
||||
const payload = vi.mocked(createManualInboundPlan).mock.calls[0][0] as unknown as Record<string, unknown>
|
||||
expect(payload).not.toHaveProperty('inboundCode')
|
||||
expect(payload).not.toHaveProperty('plannedWarehouseId')
|
||||
expect(payload).not.toHaveProperty('plannedWarehouseCode')
|
||||
expect(payload).not.toHaveProperty('plannedWarehouseName')
|
||||
})
|
||||
|
||||
it('opens a cabinet selector without requiring order selection and loads server summaries only then', async () => {
|
||||
const wrapper = await openPending()
|
||||
await openContainerDialog(wrapper)
|
||||
|
||||
@@ -42,9 +42,10 @@ async function exportOrders() { exportLoading.value = true; try { const { create
|
||||
const actionError = ref<ApiErrorDetails | null>(null)
|
||||
const actionTarget = ref<LastMileOrderListItem | null>(null)
|
||||
const inboundPlanOpen = ref(false)
|
||||
const inboundPlanWarehouseLoading = ref(false)
|
||||
const inboundPlanWarehouses = ref<TransferWarehouseOption[]>([])
|
||||
const inboundPlanForm = reactive({ warehouseId: '', plannedArrivalDate: '', inboundCode: '', palletCount: 1, palletType: 'STANDARD' as 'STANDARD' | 'WOOD' | 'PAPER', assigned: false, mergeRequired: false, urgent: false, remark: '' })
|
||||
const inboundPlanWarehouseLabel = computed(() => [actionTarget.value?.overseasWarehouseCode, actionTarget.value?.warehouse]
|
||||
.filter((value) => value && value !== '—')
|
||||
.join(' · '))
|
||||
const inboundPlanForm = reactive({ plannedArrivalDate: '', inboundCode: '', palletCount: 1, palletType: 'STANDARD' as 'STANDARD' | 'WOOD' | 'PAPER', assigned: false, mergeRequired: false, urgent: false, remark: '' })
|
||||
const holdOpen = ref(false)
|
||||
const holdForm = reactive({ held: true, chargeable: true, reason: '' })
|
||||
const manualNodesOpen = ref(false)
|
||||
@@ -436,43 +437,28 @@ async function submitTransfer() {
|
||||
function idempotencyKey(prefix: string) {
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
||||
}
|
||||
async function loadInboundPlanWarehouses() {
|
||||
inboundPlanWarehouseLoading.value = true
|
||||
try {
|
||||
inboundPlanWarehouses.value = (await listAllPlatformWarehouses({ warehouseType: 'OVERSEAS', status: 'ACTIVE' })).map(toTransferWarehouseOption)
|
||||
const targetWarehouse = inboundPlanWarehouses.value.find((item) => actionTarget.value?.warehouse.includes(item.warehouseName))
|
||||
if (targetWarehouse) inboundPlanForm.warehouseId = targetWarehouse.warehouseId
|
||||
} catch (error) {
|
||||
actionError.value = getApiErrorDetails(error, '入库仓候选加载失败,请重试')
|
||||
} finally {
|
||||
inboundPlanWarehouseLoading.value = false
|
||||
}
|
||||
}
|
||||
function openInboundPlan(source?: object) {
|
||||
const target = source ? source as LastMileOrderListItem : (selectedRows.value.length === 1 ? selectedRows.value[0] : null)
|
||||
if (!target) { ElMessage.warning('生成入库计划时请选择 1 条尾程订单'); return }
|
||||
if (target.detained) { ElMessage.warning('订单处于暂扣状态,解除暂扣后才能生成入库计划'); return }
|
||||
actionTarget.value = target
|
||||
Object.assign(inboundPlanForm, { warehouseId: '', plannedArrivalDate: '', inboundCode: target.inboundCode === '—' ? '' : target.inboundCode, palletCount: typeof target.pallets === 'number' && target.pallets > 0 ? target.pallets : 1, palletType: 'STANDARD', assigned: false, mergeRequired: false, urgent: false, remark: '' })
|
||||
inboundPlanWarehouses.value = []
|
||||
Object.assign(inboundPlanForm, { plannedArrivalDate: '', inboundCode: target.inboundCode === '—' ? '' : target.inboundCode, palletCount: typeof target.pallets === 'number' && target.pallets > 0 ? target.pallets : 1, palletType: 'STANDARD', assigned: false, mergeRequired: false, urgent: false, remark: '' })
|
||||
actionError.value = null
|
||||
inboundPlanOpen.value = true
|
||||
void loadInboundPlanWarehouses()
|
||||
}
|
||||
async function submitInboundPlan() {
|
||||
const row = actionTarget.value
|
||||
const warehouse = inboundPlanWarehouses.value.find((item) => item.warehouseId === inboundPlanForm.warehouseId)
|
||||
if (!row || !warehouse || !inboundPlanForm.plannedArrivalDate || !inboundPlanForm.inboundCode.trim() || inboundPlanForm.palletCount < 1) {
|
||||
ElMessage.warning('请完整填写计划入库仓、预计到仓日期、入仓码和托数')
|
||||
const frozenInboundCode = String(row?.inboundCode || '').trim()
|
||||
if (!row || !row.overseasWarehouseId || !inboundPlanForm.plannedArrivalDate || !frozenInboundCode || frozenInboundCode === '—' || inboundPlanForm.palletCount < 1) {
|
||||
ElMessage.warning('当前柜须已分配接柜仓,并完整填写预计到仓日期和托数')
|
||||
return
|
||||
}
|
||||
actionLoading.value = true; actionError.value = null
|
||||
try {
|
||||
await createManualInboundPlan({
|
||||
customerId: row.customerId, orderId: row.id, expectedOrderVersion: row.version,
|
||||
plannedWarehouseId: warehouse.warehouseId, plannedWarehouseCode: warehouse.warehouseCode,
|
||||
plannedWarehouseName: warehouse.warehouseName, plannedArrivalDate: inboundPlanForm.plannedArrivalDate,
|
||||
inboundCode: inboundPlanForm.inboundCode.trim(), palletCount: inboundPlanForm.palletCount,
|
||||
plannedArrivalDate: inboundPlanForm.plannedArrivalDate,
|
||||
palletCount: inboundPlanForm.palletCount,
|
||||
palletType: inboundPlanForm.palletType, assigned: inboundPlanForm.assigned,
|
||||
mergeRequired: inboundPlanForm.mergeRequired, urgent: inboundPlanForm.urgent,
|
||||
remark: inboundPlanForm.remark.trim() || undefined,
|
||||
@@ -715,10 +701,10 @@ onMounted(() => { void load() })
|
||||
</LoadErrorState>
|
||||
</el-form>
|
||||
</AppDialog>
|
||||
<AppDialog v-model="inboundPlanOpen" title="入库计划" width="min(720px, calc(100vw - 28px))" confirm-text="保存并生成入库单" :loading="inboundPlanWarehouseLoading" :confirm-loading="actionLoading" @confirm="submitInboundPlan">
|
||||
<AppDialog v-model="inboundPlanOpen" title="入库计划" width="min(720px, calc(100vw - 28px))" confirm-text="保存并生成入库单" :confirm-loading="actionLoading" @confirm="submitInboundPlan">
|
||||
<el-alert title="保存后生成入库单;清关完成后入库计划不可更改。" type="info" :closable="false" show-icon />
|
||||
<el-form class="command-form" label-position="top">
|
||||
<div class="command-form__grid"><el-form-item label="计划入库仓" required><el-select v-model="inboundPlanForm.warehouseId" filterable placeholder="请选择海外仓"><el-option v-for="warehouse in inboundPlanWarehouses" :key="warehouse.warehouseId" :label="warehouse.label" :value="warehouse.warehouseId" /></el-select></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="入仓码" required><el-input v-model="inboundPlanForm.inboundCode" maxlength="128" placeholder="请输入入仓码" /></el-form-item><el-form-item label="托数" required><el-input-number v-model="inboundPlanForm.palletCount" :min="1" :max="999" controls-position="right" placeholder="请输入托数" /></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="否" /></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="1" :max="999" controls-position="right" placeholder="请输入托数" /></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="否" /></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>
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface LastMileOrderListItem {
|
||||
inboundCode: string
|
||||
lxCode: string
|
||||
owner: string
|
||||
overseasWarehouseId: string
|
||||
overseasWarehouseCode: string
|
||||
warehouse: string
|
||||
transferWarehouse: string
|
||||
fbaWarehouse: string
|
||||
|
||||
Reference in New Issue
Block a user