Merge remote-tracking branch 'origin/release/test' into release/test
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { http } from '@/shared/api/http'
|
import { http } from '@/shared/api/http'
|
||||||
import { reviewCargoDiscard } from './lastMileOrders'
|
import { reviewCargoDiscard, updateLastMileOrderPalletMerge } from './lastMileOrders'
|
||||||
import { changeContainerReceivingWarehouse, convertLastMileContainerToInboundPlans, createManualInboundPlan, getContainerPlanProgress, getLastMileOrder, getLastMileOrderManualNodes, listContainerPlanProgress, listLastMileOrders, transferLastMileOrders, updateLastMileOrderHold, updateLastMileOrderManualNodes } from './lastMileOrders'
|
import { changeContainerReceivingWarehouse, convertLastMileContainerToInboundPlans, createManualInboundPlan, getContainerPlanProgress, getLastMileOrder, getLastMileOrderManualNodes, listContainerPlanProgress, listLastMileOrders, transferLastMileOrders, updateLastMileOrderHold, updateLastMileOrderManualNodes } from './lastMileOrders'
|
||||||
|
|
||||||
vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn(), put: vi.fn(), post: vi.fn() } }))
|
vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn(), put: vi.fn(), post: vi.fn() } }))
|
||||||
@@ -148,3 +148,12 @@ describe('last-mile order overseas API', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('sets and cancels a merge group with string IDs and explicit customer scope', async () => {
|
||||||
|
vi.mocked(http.put).mockResolvedValue({ code: '200', message: 'success', data: null })
|
||||||
|
for (const required of [true, false]) {
|
||||||
|
await updateLastMileOrderPalletMerge(['2092873463568596994'], '2092618717861048321', required)
|
||||||
|
expect(http.put).toHaveBeenLastCalledWith('/platform/v1/last-leg/orders/pallet-merge-required/batch',
|
||||||
|
{ orderIds: ['2092873463568596994'], palletMergeRequired: required }, { params: { customerId: '2092618717861048321' } })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -356,7 +356,8 @@ export interface ChangeContainerReceivingWarehousePayload {
|
|||||||
|
|
||||||
export interface LastMileOrderDetailDto {
|
export interface LastMileOrderDetailDto {
|
||||||
returnReceipt?: {
|
returnReceipt?: {
|
||||||
order: { returnNo: string; receivedQuantity: number; expectedQuantity: number }
|
order: { returnNo: string; status?: string; receivedQuantity: number; expectedQuantity: number }
|
||||||
|
items?: Array<{ skuCode: string; productNameZh?: string; expectedQuantity: number; receivedQuantity: number }>
|
||||||
receipts: Array<{ receiptNo: string; unitType: string; unitCode?: string; palletCode?: string; receivedQuantity: number; boxCount: number; signedAt: string }>
|
receipts: Array<{ receiptNo: string; unitType: string; unitCode?: string; palletCode?: string; receivedQuantity: number; boxCount: number; signedAt: string }>
|
||||||
cartons?: Array<{ receiptNo: string; palletCode?: string; cartonMark: string; countStatus: string; countedAt?: string; items: Array<{ skuCode: string; productNameZh?: string; quantity: number }> }>
|
cartons?: Array<{ receiptNo: string; palletCode?: string; cartonMark: string; countStatus: string; countedAt?: string; items: Array<{ skuCode: string; productNameZh?: string; quantity: number }> }>
|
||||||
} | null
|
} | null
|
||||||
@@ -426,6 +427,15 @@ export async function transferLastMileOrders(payload: TransferDestinationPayload
|
|||||||
return unwrapApiResponse(response)
|
return unwrapApiResponse(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateLastMileOrderPalletMerge(orderIds: string[], customerId: string, palletMergeRequired: boolean) {
|
||||||
|
const response = await http.put<ApiEnvelope<null>, ApiEnvelope<null>>(
|
||||||
|
'/platform/v1/last-leg/orders/pallet-merge-required/batch',
|
||||||
|
{ orderIds, palletMergeRequired },
|
||||||
|
{ params: { customerId } },
|
||||||
|
)
|
||||||
|
return unwrapApiResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
export async function listContainerPlanProgress(query: ContainerPlanProgressQuery): Promise<OverseasPageResult<ContainerPlanProgressSummaryDto>> {
|
export async function listContainerPlanProgress(query: ContainerPlanProgressQuery): Promise<OverseasPageResult<ContainerPlanProgressSummaryDto>> {
|
||||||
const response = await http.get<ApiEnvelope<OverseasPagePayload<ContainerPlanProgressSummaryDto>>, ApiEnvelope<OverseasPagePayload<ContainerPlanProgressSummaryDto>>>(
|
const response = await http.get<ApiEnvelope<OverseasPagePayload<ContainerPlanProgressSummaryDto>>, ApiEnvelope<OverseasPagePayload<ContainerPlanProgressSummaryDto>>>(
|
||||||
'/platform/v1/last-leg/orders/container-summaries',
|
'/platform/v1/last-leg/orders/container-summaries',
|
||||||
|
|||||||
@@ -13,6 +13,31 @@ vi.mock('../api/warehouseTransfers', () => ({ getWarehouseTransfer: vi.fn() }))
|
|||||||
enableAutoUnmount(afterEach)
|
enableAutoUnmount(afterEach)
|
||||||
|
|
||||||
describe('LastMileOrderDetailView domestic detail alignment', () => {
|
describe('LastMileOrderDetailView domestic detail alignment', () => {
|
||||||
|
it.each(['PENDING_REVIEW', 'PARTIALLY_RECEIVED'])('shows SKU forecast before receipt completion (%s) without forecast box counts', async status => {
|
||||||
|
vi.mocked(getLastMileOrder).mockResolvedValue({ order: { id: 'return-tail', customerId: 'C1', orderType: 'RETURN_RELABEL', orderNo: 'RT-1', currentStatus: status, packageCount: 30 },
|
||||||
|
cargo: [], fees: [], statusHistory: [], operationHistory: [],
|
||||||
|
returnReceipt: { order: { returnNo: 'RT-1', status, expectedQuantity: 30, receivedQuantity: 5 }, receipts: [],
|
||||||
|
items: [{ skuCode: 'SKU-FORECAST', productNameZh: '预报商品', expectedQuantity: 30, receivedQuantity: 5 }] } })
|
||||||
|
const wrapper = mount(LastMileOrderDetailView, { global: { plugins: [ElementPlus] } }); await flushPromises()
|
||||||
|
expect(wrapper.get('[aria-label="基本信息"]').text()).not.toContain('箱数')
|
||||||
|
await wrapper.findAll('[role="tab"]').find(tab => tab.text().includes('货物信息'))!.trigger('click'); await flushPromises()
|
||||||
|
const panel = wrapper.get('.table-panel')
|
||||||
|
expect(panel.text()).toContain('SKU-FORECAST')
|
||||||
|
expect(panel.text()).toContain('预报商品')
|
||||||
|
expect(panel.text()).toContain('预报件数')
|
||||||
|
expect(panel.text()).toContain('30')
|
||||||
|
expect(panel.text()).not.toContain('箱数')
|
||||||
|
})
|
||||||
|
it('uses actual cargo after return receipt completion', async () => {
|
||||||
|
vi.mocked(getLastMileOrder).mockResolvedValue({ order: { id: 'return-tail', customerId: 'C1', orderType: 'RETURN_RELABEL', orderNo: 'RT-1', currentStatus: 'INBOUND_COMPLETED' },
|
||||||
|
cargo: [{ id: 'actual', skuCode: 'SKU-ACTUAL', productNameZh: '实收商品', productQuantity: 25, packageCount: 2 }], fees: [], statusHistory: [], operationHistory: [],
|
||||||
|
returnReceipt: { order: { returnNo: 'RT-1', status: 'COMPLETED', expectedQuantity: 30, receivedQuantity: 25 }, receipts: [], items: [{ skuCode: 'SKU-FORECAST', expectedQuantity: 30, receivedQuantity: 25 }] } })
|
||||||
|
const wrapper = mount(LastMileOrderDetailView, { global: { plugins: [ElementPlus] } }); await flushPromises()
|
||||||
|
await wrapper.findAll('[role="tab"]').find(tab => tab.text().includes('货物信息'))!.trigger('click'); await flushPromises()
|
||||||
|
expect(wrapper.get('.table-panel').text()).toContain('SKU-ACTUAL')
|
||||||
|
expect(wrapper.get('.table-panel').text()).not.toContain('SKU-FORECAST')
|
||||||
|
expect(wrapper.get('.table-panel').text()).toContain('25')
|
||||||
|
})
|
||||||
it('shows signed box contents and actual quantities on return orders', async () => {
|
it('shows signed box contents and actual quantities on return orders', async () => {
|
||||||
vi.mocked(getLastMileOrder).mockResolvedValue({ order: { id: 'tail-return', customerId: 'customer-1', orderNo: 'RETU-1', orderType: 'RETURN_RELABEL', currentStatus: 'INBOUND_COMPLETED' }, cargo: [], fees: [], statusHistory: [], operationHistory: [],
|
vi.mocked(getLastMileOrder).mockResolvedValue({ order: { id: 'tail-return', customerId: 'customer-1', orderNo: 'RETU-1', orderType: 'RETURN_RELABEL', currentStatus: 'INBOUND_COMPLETED' }, cargo: [], fees: [], statusHistory: [], operationHistory: [],
|
||||||
returnReceipt: { order: { returnNo: 'RT-1', receivedQuantity: 10, expectedQuantity: 10 }, receipts: [{ receiptNo: 'RR-1', unitType: 'PALLET', unitCode: 'PLT-1', receivedQuantity: 10, boxCount: 2, signedAt: '' }],
|
returnReceipt: { order: { returnNo: 'RT-1', receivedQuantity: 10, expectedQuantity: 10 }, receipts: [{ receiptNo: 'RR-1', unitType: 'PALLET', unitCode: 'PLT-1', receivedQuantity: 10, boxCount: 2, signedAt: '' }],
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ function feeStatusLabel(value: string) { return ({ PENDING_REVIEW: '待审核',
|
|||||||
function isSingleLineBusinessField(field: DetailField) { return field.label === '入仓码' || field.label === '集装箱号' }
|
function isSingleLineBusinessField(field: DetailField) { return field.label === '入仓码' || field.label === '集装箱号' }
|
||||||
|
|
||||||
const referenceValue = computed(() => lastMileOrderReference(detail.value?.order.orderNo, '订单编码待补充'))
|
const referenceValue = computed(() => lastMileOrderReference(detail.value?.order.orderNo, '订单编码待补充'))
|
||||||
|
const isReturnOrder = computed(() => detail.value?.order.orderType === 'RETURN_RELABEL')
|
||||||
|
const showReturnForecast = computed(() => isReturnOrder.value && (detail.value?.returnReceipt?.order.status
|
||||||
|
? detail.value.returnReceipt.order.status !== 'COMPLETED'
|
||||||
|
: ['PENDING_REVIEW', 'PENDING_RECEIPT', 'PARTIALLY_RECEIVED', 'REJECTED', 'CANCELLED'].includes(detail.value?.order.currentStatus || '')))
|
||||||
const orderFields = computed<DetailField[]>(() => {
|
const orderFields = computed<DetailField[]>(() => {
|
||||||
const order = detail.value?.order
|
const order = detail.value?.order
|
||||||
const fields: DetailField[] = [
|
const fields: DetailField[] = [
|
||||||
@@ -93,7 +97,7 @@ const containerFields = computed<DetailField[]>(() => {
|
|||||||
{ label: '实际到港时间', value: formatOverseasDateTime(order?.actualArrivalAt) },
|
{ label: '实际到港时间', value: formatOverseasDateTime(order?.actualArrivalAt) },
|
||||||
{ label: '预计到仓时间', value: formatOverseasDateTime(order?.estimatedWarehouseAt) },
|
{ label: '预计到仓时间', value: formatOverseasDateTime(order?.estimatedWarehouseAt) },
|
||||||
{ label: '货柜节点', value: containerNodeLabel(order?.containerNode) },
|
{ label: '货柜节点', value: containerNodeLabel(order?.containerNode) },
|
||||||
]
|
].filter(field => !isReturnOrder.value || field.label !== '箱数')
|
||||||
})
|
})
|
||||||
const commercialLink = computed(() => businessOrderLink(detail.value?.order.businessSource?.businessType, detail.value?.order.businessSource?.businessOrderNo, access.can))
|
const commercialLink = computed(() => businessOrderLink(detail.value?.order.businessSource?.businessType, detail.value?.order.businessSource?.businessOrderNo, access.can))
|
||||||
const basicFields = computed(() => [...orderFields.value, ...containerFields.value])
|
const basicFields = computed(() => [...orderFields.value, ...containerFields.value])
|
||||||
@@ -104,7 +108,8 @@ const overviewFields = computed(() => {
|
|||||||
return [
|
return [
|
||||||
{ label: '客户', value: text(order?.customerName) },
|
{ label: '客户', value: text(order?.customerName) },
|
||||||
{ label: '海外仓', value: text(order?.overseasWarehouseName) },
|
{ label: '海外仓', value: text(order?.overseasWarehouseName) },
|
||||||
{ label: warehouseTransfer ? (hasReceivedPackages ? '实收总箱数' : '客户预报总箱数') : '总箱数', value: (hasReceivedPackages ? order?.receivedPackageCount : order?.packageCount) == null ? '—' : `${hasReceivedPackages ? order?.receivedPackageCount : order?.packageCount} 箱` },
|
...(isReturnOrder.value ? [{ label: showReturnForecast.value ? '预报总件数' : '实收总件数', value: `${showReturnForecast.value ? detail.value?.returnReceipt?.order.expectedQuantity ?? order?.productQuantity ?? 0 : detail.value?.returnReceipt?.order.receivedQuantity ?? order?.productQuantity ?? 0} 件` }]
|
||||||
|
: [{ label: warehouseTransfer ? (hasReceivedPackages ? '实收总箱数' : '客户预报总箱数') : '总箱数', value: (hasReceivedPackages ? order?.receivedPackageCount : order?.packageCount) == null ? '—' : `${hasReceivedPackages ? order?.receivedPackageCount : order?.packageCount} 箱` }]),
|
||||||
{ label: '总托数', value: order?.palletCount == null ? '—' : order.palletCount + ' 托' },
|
{ label: '总托数', value: order?.palletCount == null ? '—' : order.palletCount + ' 托' },
|
||||||
{ label: '货物毛重', value: order?.grossWeightKg == null ? '—' : order.grossWeightKg + ' kg' },
|
{ label: '货物毛重', value: order?.grossWeightKg == null ? '—' : order.grossWeightKg + ' kg' },
|
||||||
{ label: '货物净重', value: order?.netWeightKg == null ? '—' : order.netWeightKg + ' kg' },
|
{ label: '货物净重', value: order?.netWeightKg == null ? '—' : order.netWeightKg + ' kg' },
|
||||||
@@ -130,7 +135,10 @@ const documents = computed<DocumentRow[]>(() => {
|
|||||||
.filter((item) => item[1]).map(([type, number]) => ({ type: String(type), number: String(number), createdAt: '—', quote: '—' } as DocumentRow))
|
.filter((item) => item[1]).map(([type, number]) => ({ type: String(type), number: String(number), createdAt: '—', quote: '—' } as DocumentRow))
|
||||||
.concat((order.relabelApplications || []).map(application => ({ type: '换标单', number: application.relabelNo, createdAt: formatOverseasDateTime(application.createdAt), quote: '—', status: relabelApplicationStatusLabel(application.status), quantity: application.quantity })))
|
.concat((order.relabelApplications || []).map(application => ({ type: '换标单', number: application.relabelNo, createdAt: formatOverseasDateTime(application.createdAt), quote: '—', status: relabelApplicationStatusLabel(application.status), quantity: application.quantity })))
|
||||||
})
|
})
|
||||||
const cargo = computed<CargoRow[]>(() => (detail.value?.cargo || []).map((item) => ({
|
const cargo = computed<CargoRow[]>(() => showReturnForecast.value && detail.value?.returnReceipt?.items?.length
|
||||||
|
? detail.value.returnReceipt.items.map(item => ({ id: item.skuCode, sku: item.skuCode, productName: text(item.productNameZh),
|
||||||
|
quantity: item.expectedQuantity, pallets: 0, cartons: 0, weight: 0, volume: 0 }))
|
||||||
|
: (detail.value?.cargo || []).map((item) => ({
|
||||||
id: String(item.id), sku: text(item.skuCode), productName: text(item.productNameZh), pallets: Number(item.palletCount || 0),
|
id: String(item.id), sku: text(item.skuCode), productName: text(item.productNameZh), pallets: Number(item.palletCount || 0),
|
||||||
cartons: Number(item.packageCount || 0), quantity: Number(item.productQuantity || 0), weight: Number(item.grossWeightKg || 0), volume: Number(item.volumeCbm || 0),
|
cartons: Number(item.packageCount || 0), quantity: Number(item.productQuantity || 0), weight: Number(item.grossWeightKg || 0), volume: Number(item.volumeCbm || 0),
|
||||||
})))
|
})))
|
||||||
@@ -149,12 +157,16 @@ const logs = computed<LogRow[]>(() => {
|
|||||||
return [...statusRows, ...operationRows].sort((left, right) => right.sortAt.localeCompare(left.sortAt))
|
return [...statusRows, ...operationRows].sort((left, right) => right.sortAt.localeCompare(left.sortAt))
|
||||||
})
|
})
|
||||||
|
|
||||||
const cargoColumns: DataTableColumn[] = [
|
const cargoColumns = computed<DataTableColumn[]>(() => showReturnForecast.value ? [
|
||||||
|
{ key: 'sku', prop: 'sku', label: 'SKU', minWidth: 150 },
|
||||||
|
{ key: 'productName', prop: 'productName', label: '中文品名', minWidth: 180 },
|
||||||
|
{ key: 'quantity', prop: 'quantity', label: '预报件数', width: 120, align: 'right' },
|
||||||
|
] : [
|
||||||
{ key: 'sku', prop: 'sku', label: 'SKU', minWidth: 150 }, { key: 'productName', prop: 'productName', label: '中文品名', minWidth: 180 },
|
{ key: 'sku', prop: 'sku', label: 'SKU', minWidth: 150 }, { key: 'productName', prop: 'productName', label: '中文品名', minWidth: 180 },
|
||||||
{ key: 'pallets', prop: 'pallets', label: '托数', width: 100, align: 'right' }, { key: 'cartons', prop: 'cartons', label: '箱数', width: 100, align: 'right' },
|
{ key: 'pallets', prop: 'pallets', label: '托数', width: 100, align: 'right' }, { key: 'cartons', prop: 'cartons', label: '箱数', width: 100, align: 'right' },
|
||||||
{ key: 'quantity', prop: 'quantity', label: '件数', width: 100, align: 'right' }, { key: 'weight', prop: 'weight', label: '重量(kg)', width: 120, align: 'right' },
|
{ key: 'quantity', prop: 'quantity', label: '件数', width: 100, align: 'right' }, { key: 'weight', prop: 'weight', label: '重量(kg)', width: 120, align: 'right' },
|
||||||
{ key: 'volume', prop: 'volume', label: '体积(m³)', width: 120, align: 'right' },
|
{ key: 'volume', prop: 'volume', label: '体积(m³)', width: 120, align: 'right' },
|
||||||
]
|
])
|
||||||
const documentColumns: DataTableColumn[] = [
|
const documentColumns: DataTableColumn[] = [
|
||||||
{ key: 'status', prop: 'status', label: '状态', minWidth: 130 }, { key: 'quantity', prop: 'quantity', label: '换标数量(件)', minWidth: 130 },
|
{ key: 'status', prop: 'status', label: '状态', minWidth: 130 }, { key: 'quantity', prop: 'quantity', label: '换标数量(件)', minWidth: 130 },
|
||||||
{ key: 'type', prop: 'type', label: '单据类型', minWidth: 150 }, { key: 'number', prop: 'number', label: '单据编号', minWidth: 200 },
|
{ key: 'type', prop: 'type', label: '单据类型', minWidth: 150 }, { key: 'number', prop: 'number', label: '单据编号', minWidth: 200 },
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
listLastMileOrders,
|
listLastMileOrders,
|
||||||
reviewCargoDiscard,
|
reviewCargoDiscard,
|
||||||
transferLastMileOrders,
|
transferLastMileOrders,
|
||||||
|
updateLastMileOrderPalletMerge,
|
||||||
getLastMileOrderManualNodes,
|
getLastMileOrderManualNodes,
|
||||||
updateLastMileOrderHold,
|
updateLastMileOrderHold,
|
||||||
updateLastMileOrderManualNodes,
|
updateLastMileOrderManualNodes,
|
||||||
@@ -38,7 +39,7 @@ vi.mock('../api/lastMileOrders', () => ({
|
|||||||
changeContainerReceivingWarehouse: vi.fn(),
|
changeContainerReceivingWarehouse: vi.fn(),
|
||||||
createManualInboundPlan: vi.fn(), convertLastMileContainerToInboundPlans: vi.fn(), getContainerInboundPlanPreview: vi.fn(), transferLastMileOrders: vi.fn(),
|
createManualInboundPlan: vi.fn(), convertLastMileContainerToInboundPlans: vi.fn(), getContainerInboundPlanPreview: vi.fn(), transferLastMileOrders: vi.fn(),
|
||||||
getLastMileOrderManualNodes: vi.fn(), updateLastMileOrderHold: vi.fn(), updateLastMileOrderManualNodes: vi.fn(),
|
getLastMileOrderManualNodes: vi.fn(), updateLastMileOrderHold: vi.fn(), updateLastMileOrderManualNodes: vi.fn(),
|
||||||
reviewCargoDiscard: vi.fn(),
|
reviewCargoDiscard: vi.fn(), updateLastMileOrderPalletMerge: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
class ResizeObserverStub implements ResizeObserver { observe() {} unobserve() {} disconnect() {} }
|
class ResizeObserverStub implements ResizeObserver { observe() {} unobserve() {} disconnect() {} }
|
||||||
@@ -1189,3 +1190,40 @@ describe('LastMileOrdersView pending order and container conversion', () => {
|
|||||||
expect(routerPush).toHaveBeenCalledWith({ name: 'overseas-return-inventory', query: expect.objectContaining({ packing: '1', returnNos: 'RETURN-1' }) })
|
expect(routerPush).toHaveBeenCalledWith({ name: 'overseas-return-inventory', query: expect.objectContaining({ packing: '1', returnNos: 'RETURN-1' }) })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('tail-leg pallet merge toolbar', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
canAccess.mockImplementation(() => true)
|
||||||
|
vi.mocked(listLastMileOrders).mockResolvedValue({ records: [], page: 1, size: 10, total: 0, pages: 0 })
|
||||||
|
vi.mocked(updateLastMileOrderPalletMerge).mockReset().mockResolvedValue(null)
|
||||||
|
})
|
||||||
|
it('submits the selected group and cancellation with the customer scope', async () => {
|
||||||
|
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as Awaited<ReturnType<typeof ElMessageBox.confirm>>)
|
||||||
|
const wrapper = await mountView()
|
||||||
|
const row = { id: '2092873463568596994', customerId, currentStatusCode: 'PENDING_ASSIGNMENT', palletMergeFlag: '' }
|
||||||
|
expect(wrapper.findAll('button').find(b => b.text() === '合托')!.attributes('disabled')).toBeDefined()
|
||||||
|
await selectRows(wrapper, [row])
|
||||||
|
await wrapper.findAll('button').find(b => b.text() === '合托')!.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(updateLastMileOrderPalletMerge).toHaveBeenLastCalledWith([row.id], customerId, true)
|
||||||
|
await selectRows(wrapper, [{ ...row, palletMergeFlag: 'REQUIRED' }])
|
||||||
|
await wrapper.findAll('button').find(b => b.text() === '取消合托')!.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(updateLastMileOrderPalletMerge).toHaveBeenLastCalledWith([row.id], customerId, false)
|
||||||
|
})
|
||||||
|
it('blocks completed and mixed-customer selections and hides unauthorized actions', async () => {
|
||||||
|
const wrapper = await mountView()
|
||||||
|
for (const rows of [
|
||||||
|
[{ id: '1', customerId, currentStatusCode: 'INBOUND_COMPLETED', palletMergeFlag: 'REQUIRED' }],
|
||||||
|
[{ id: '1', customerId, palletMergeFlag: 'MERGED' }],
|
||||||
|
[{ id: '1', customerId }, { id: '2', customerId: 'other' }],
|
||||||
|
]) {
|
||||||
|
await selectRows(wrapper, rows)
|
||||||
|
expect(wrapper.findAll('button').find(b => b.text() === '合托')!.attributes('disabled')).toBeDefined()
|
||||||
|
expect(wrapper.findAll('button').find(b => b.text() === '取消合托')!.attributes('disabled')).toBeDefined()
|
||||||
|
}
|
||||||
|
canAccess.mockImplementation(() => false)
|
||||||
|
const denied = await mountView()
|
||||||
|
expect(denied.findAll('button').some(b => b.text() === '合托' || b.text() === '取消合托')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
|||||||
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
||||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||||
import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'
|
import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'
|
||||||
import { changeContainerReceivingWarehouse, convertLastMileContainerToInboundPlans, createManualInboundPlan, getContainerInboundPlanPreview, getContainerPlanProgress, getLastMileOrderManualNodes, listContainerPlanProgress, listLastMileOrders, reviewDirectRelabelOrder, reviewReturnOrder, transferLastMileOrders, updateLastMileOrderHold, updateLastMileOrderManualNodes, type ContainerInboundPlanPreviewDto, type ContainerInboundPlanPreviewGroupDto, type ContainerInboundPlanPreviewOrderDto, type ContainerPlanProgressDetailDto, type ContainerPlanProgressSummaryDto, type ManualNodeCode, type LastMileOrderQuery } from '../api/lastMileOrders'
|
import { changeContainerReceivingWarehouse, convertLastMileContainerToInboundPlans, createManualInboundPlan, getContainerInboundPlanPreview, getContainerPlanProgress, getLastMileOrderManualNodes, listContainerPlanProgress, listLastMileOrders, reviewDirectRelabelOrder, reviewReturnOrder, transferLastMileOrders, updateLastMileOrderPalletMerge, updateLastMileOrderHold, updateLastMileOrderManualNodes, type ContainerInboundPlanPreviewDto, type ContainerInboundPlanPreviewGroupDto, type ContainerInboundPlanPreviewOrderDto, type ContainerPlanProgressDetailDto, type ContainerPlanProgressSummaryDto, type ManualNodeCode, type LastMileOrderQuery } from '../api/lastMileOrders'
|
||||||
import { businessOrderLink } from '../businessOrderLink'
|
import { businessOrderLink } from '../businessOrderLink'
|
||||||
import OrderReferenceText from '../components/OrderReferenceText.vue'
|
import OrderReferenceText from '../components/OrderReferenceText.vue'
|
||||||
import { reviewCargoDiscard } from '../api/lastMileOrders'
|
import { reviewCargoDiscard } from '../api/lastMileOrders'
|
||||||
@@ -64,6 +64,33 @@ function openReturnPackingPlan() {
|
|||||||
const exportLoading = ref(false)
|
const exportLoading = ref(false)
|
||||||
const actionLoading = ref(false)
|
const actionLoading = ref(false)
|
||||||
async function exportOrders() { exportLoading.value = true; try { await submitExport('LAST_MILE_ORDER_LIST', '尾程订单', orderQuery()) } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '尾程订单导出失败,请重试').message) } finally { exportLoading.value = false } }
|
async function exportOrders() { exportLoading.value = true; try { await submitExport('LAST_MILE_ORDER_LIST', '尾程订单', orderQuery()) } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '尾程订单导出失败,请重试').message) } finally { exportLoading.value = false } }
|
||||||
|
const palletMergeSelectionEditable = computed(() => selectedRows.value.length > 0
|
||||||
|
&& selectedRows.value.every(row => row.customerId && row.currentStatusCode !== 'INBOUND_COMPLETED' && row.palletMergeFlag !== 'MERGED')
|
||||||
|
&& new Set(selectedRows.value.map(row => row.customerId)).size === 1)
|
||||||
|
const canCancelPalletMerge = computed(() => palletMergeSelectionEditable.value
|
||||||
|
&& selectedRows.value.every(row => row.palletMergeFlag === 'REQUIRED'))
|
||||||
|
async function submitPalletMerge(required: boolean) {
|
||||||
|
if (actionLoading.value || !access.can(permission.lastMileOrderPalletMerge)
|
||||||
|
|| !(required ? palletMergeSelectionEditable.value : canCancelPalletMerge.value)) return
|
||||||
|
const selection = [...selectedRows.value]
|
||||||
|
const label = required ? '合托' : '取消合托'
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(required
|
||||||
|
? `将勾选的 ${selection.length} 票订单标记为同一合托组?所选订单须属于同一客户、同一目的仓,PDA 将提示现场合托。`
|
||||||
|
: `取消勾选的 ${selection.length} 票订单的合托标记和 PDA 提示?`, label,
|
||||||
|
{ customClass: 'admin-density-confirm', confirmButtonText: `确认${label}`, cancelButtonText: '返回', type: 'warning' })
|
||||||
|
} catch { return }
|
||||||
|
actionLoading.value = true
|
||||||
|
actionError.value = null
|
||||||
|
try {
|
||||||
|
await updateLastMileOrderPalletMerge(selection.map(row => row.id), selection[0]!.customerId, required)
|
||||||
|
ElMessage.success(required ? '已标记合托' : '已取消合托标记')
|
||||||
|
selectedRows.value = []
|
||||||
|
await load()
|
||||||
|
} catch (cause) {
|
||||||
|
actionError.value = getApiErrorDetails(cause, `${label}失败,请重试`)
|
||||||
|
} finally { actionLoading.value = false }
|
||||||
|
}
|
||||||
const actionError = ref<ApiErrorDetails | null>(null)
|
const actionError = ref<ApiErrorDetails | null>(null)
|
||||||
const actionTarget = ref<LastMileOrderListItem | null>(null)
|
const actionTarget = ref<LastMileOrderListItem | null>(null)
|
||||||
const inboundPlanOpen = ref(false)
|
const inboundPlanOpen = ref(false)
|
||||||
@@ -1108,7 +1135,7 @@ onMounted(() => { void load() })
|
|||||||
</div>
|
</div>
|
||||||
</section></template>
|
</section></template>
|
||||||
|
|
||||||
<template v-if="statusTab !== '柜子'" #toolbar-actions><el-button v-if="selectedReturnRows.length" type="warning" @click="openReturnPackingPlan"><PackagePlus :size="16" />创建退货合箱计划({{ selectedReturnRows.length }})</el-button><el-button v-if="access.can(permission.lastMileOrderExport) && access.can('platform:download:list')" plain :loading="exportLoading" @click="exportOrders"><Download :size="16" />导出订单</el-button><el-button v-if="access.can(permission.lastMileOrderTransfer)" plain :disabled="!selectedRows.length || hasSelectedRejectedReturn() || actionLoading" @click="openTransfer">转仓</el-button><el-button v-if="access.can(permission.inboundOrderCreate) && selectedRows.length === 1 && canGenerateInboundPlan(selectedRows[0])" type="primary" plain :disabled="hasSelectedInboundPlan() || actionLoading" @click="openInboundPlan()">生成入库计划</el-button><el-button v-if="access.can(permission.inboundOrderCreate)" type="primary" :disabled="actionLoading" @click="openContainerConversion">按柜批量生成</el-button></template>
|
<template v-if="statusTab !== '柜子'" #toolbar-actions><el-button v-if="access.can(permission.lastMileOrderPalletMerge)" plain :disabled="!palletMergeSelectionEditable || actionLoading" @click="submitPalletMerge(true)">合托</el-button><el-button v-if="access.can(permission.lastMileOrderPalletMerge)" plain :disabled="!canCancelPalletMerge || actionLoading" @click="submitPalletMerge(false)">取消合托</el-button><el-button v-if="selectedReturnRows.length" type="warning" @click="openReturnPackingPlan"><PackagePlus :size="16" />创建退货合箱计划({{ selectedReturnRows.length }})</el-button><el-button v-if="access.can(permission.lastMileOrderExport) && access.can('platform:download:list')" plain :loading="exportLoading" @click="exportOrders"><Download :size="16" />导出订单</el-button><el-button v-if="access.can(permission.lastMileOrderTransfer)" plain :disabled="!selectedRows.length || hasSelectedRejectedReturn() || actionLoading" @click="openTransfer">转仓</el-button><el-button v-if="access.can(permission.inboundOrderCreate) && selectedRows.length === 1 && canGenerateInboundPlan(selectedRows[0])" type="primary" plain :disabled="hasSelectedInboundPlan() || actionLoading" @click="openInboundPlan()">生成入库计划</el-button><el-button v-if="access.can(permission.inboundOrderCreate)" type="primary" :disabled="actionLoading" @click="openContainerConversion">按柜批量生成</el-button></template>
|
||||||
<template #relabelOrderNo="{ row }"><div v-for="application in row.relabelApplications" :key="application.relabelNo" class="relabel-suborder">{{ application.relabelNo }}</div><span v-if="!row.relabelApplications?.length">{{ row.relabelOrderNo }}</span></template>
|
<template #relabelOrderNo="{ row }"><div v-for="application in row.relabelApplications" :key="application.relabelNo" class="relabel-suborder">{{ application.relabelNo }}</div><span v-if="!row.relabelApplications?.length">{{ row.relabelOrderNo }}</span></template>
|
||||||
<template #relabelStatus="{ row }"><div v-for="application in row.relabelApplications" :key="application.relabelNo" class="relabel-suborder">{{ application.statusLabel }} · {{ application.quantity }} 件</div><span v-if="!row.relabelApplications?.length">{{ row.relabelStatus }}</span></template>
|
<template #relabelStatus="{ row }"><div v-for="application in row.relabelApplications" :key="application.relabelNo" class="relabel-suborder">{{ application.statusLabel }} · {{ application.quantity }} 件</div><span v-if="!row.relabelApplications?.length">{{ row.relabelStatus }}</span></template>
|
||||||
<template #actions="{ row }"><RowActions text-actions :actions="lastMileRowActions(row as LastMileOrderListItem)" :more-label="`更多尾程订单操作 · ${(row as LastMileOrderListItem).orderNo || '当前记录'}`" @action="handleLastMileRowAction($event, row as LastMileOrderListItem)" /></template>
|
<template #actions="{ row }"><RowActions text-actions :actions="lastMileRowActions(row as LastMileOrderListItem)" :more-label="`更多尾程订单操作 · ${(row as LastMileOrderListItem).orderNo || '当前记录'}`" @action="handleLastMileRowAction($event, row as LastMileOrderListItem)" /></template>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export const overseasWarehousePermissions = {
|
|||||||
lastMileOrderList: 'platform:overseas-warehouse:last-mile-order:list',
|
lastMileOrderList: 'platform:overseas-warehouse:last-mile-order:list',
|
||||||
lastMileOrderExport: 'platform:overseas-warehouse:last-mile-order:export',
|
lastMileOrderExport: 'platform:overseas-warehouse:last-mile-order:export',
|
||||||
lastMileOrderTransfer: 'platform:overseas-warehouse:last-mile-order:edit',
|
lastMileOrderTransfer: 'platform:overseas-warehouse:last-mile-order:edit',
|
||||||
|
lastMileOrderPalletMerge: 'platform:overseas-warehouse:last-mile-order:edit',
|
||||||
warehouseTransferApprove: 'platform:overseas-warehouse:warehouse-transfer:approve',
|
warehouseTransferApprove: 'platform:overseas-warehouse:warehouse-transfer:approve',
|
||||||
cargoDiscardApprove: 'lastleg:cargo-discard:approve',
|
cargoDiscardApprove: 'lastleg:cargo-discard:approve',
|
||||||
inboundOrderList: 'platform:overseas-warehouse:inbound-order:list',
|
inboundOrderList: 'platform:overseas-warehouse:inbound-order:list',
|
||||||
|
|||||||
Reference in New Issue
Block a user