优化
This commit is contained in:
@@ -69,6 +69,22 @@ function buttonTexts(wrapper: Awaited<ReturnType<typeof render>>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('ContainerCustomsPanel', () => {
|
describe('ContainerCustomsPanel', () => {
|
||||||
|
it('locks pre-entry uploads after customer confirmation before broker execution', async () => {
|
||||||
|
const value = customs('DECLARING', true)
|
||||||
|
value.documents[0]!.receiptStatus = 'CONFIRMED'
|
||||||
|
api.get.mockResolvedValue(value)
|
||||||
|
const wrapper = mount(ContainerCustomsPanel, {
|
||||||
|
props: { loadingPlanId: value.loadingPlanId, canList: true, canManage: true, departureState: 'WAITING_OTHER_GATES' },
|
||||||
|
global: { plugins: [ElementPlus] },
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.text()).toContain('客户已确认预录单,待发送报关行执行')
|
||||||
|
for (const label of ['选择预录单', '提交预录单']) {
|
||||||
|
expect(wrapper.findAll('button').find(button => button.text() === label)?.attributes('disabled')).toBeDefined()
|
||||||
|
}
|
||||||
|
expect(wrapper.findAll('button').find(button => button.text() === '发送报关行执行')?.attributes('disabled')).toBeUndefined()
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
api.getDeclarationBatches.mockResolvedValue([])
|
api.getDeclarationBatches.mockResolvedValue([])
|
||||||
@@ -264,7 +280,7 @@ describe('ContainerCustomsPanel', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
await wrapper.findAll('button').find(button => button.text() === '提交预录单')!.trigger('click')
|
await wrapper.findAll('button').find(button => button.text() === '提交预录单')!.trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(api.upload).toHaveBeenCalledWith(document.documentId, selected, 7)
|
expect(api.upload).not.toHaveBeenCalled()
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ const allBuy = computed(() => customs.value?.hasActualCargo === true && customs.
|
|||||||
const brokerSubmission = computed(() => customs.value?.brokerSubmission || null)
|
const brokerSubmission = computed(() => customs.value?.brokerSubmission || null)
|
||||||
const brokerSubmitted = computed(() => Boolean(customs.value?.brokerSubmittedAt))
|
const brokerSubmitted = computed(() => Boolean(customs.value?.brokerSubmittedAt))
|
||||||
const allReceiptsConfirmed = computed(() => Boolean(customs.value?.documents.length) && customs.value?.documents.every((document) => document.receipt && document.receiptStatus === 'CONFIRMED') === true)
|
const allReceiptsConfirmed = computed(() => Boolean(customs.value?.documents.length) && customs.value?.documents.every((document) => document.receipt && document.receiptStatus === 'CONFIRMED') === true)
|
||||||
|
const receiptUploadLocked = computed(() => brokerSubmitted.value || customs.value?.documents.some((document) => document.receiptStatus === 'CONFIRMED') === true)
|
||||||
const openDeclarationBatch = computed(() => declarationBatches.value.find((item) => ['READY', 'SUBMITTED', 'ACKNOWLEDGED', 'RESULT_PENDING'].includes(item.status)) || null)
|
const openDeclarationBatch = computed(() => declarationBatches.value.find((item) => ['READY', 'SUBMITTED', 'ACKNOWLEDGED', 'RESULT_PENDING'].includes(item.status)) || null)
|
||||||
const statusText: Record<ContainerCustomsStatus, string> = {
|
const statusText: Record<ContainerCustomsStatus, string> = {
|
||||||
WAITING_CUSTOMER_DOCUMENTS: '待客户补齐退税资料', READY_TO_START: '可开始报关', DECLARING: '报关中', COMPLETED: '报关已完成',
|
WAITING_CUSTOMER_DOCUMENTS: '待客户补齐退税资料', READY_TO_START: '可开始报关', DECLARING: '报关中', COMPLETED: '报关已完成',
|
||||||
@@ -85,7 +86,7 @@ function formatDateTime(value: string | null | undefined) {
|
|||||||
return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN', { hour12: false })
|
return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN', { hour12: false })
|
||||||
}
|
}
|
||||||
function selectReceipt(upload: UploadFile) {
|
function selectReceipt(upload: UploadFile) {
|
||||||
if (!props.canManage || customs.value?.status !== 'DECLARING' || !brokerSubmission.value || brokerSubmitted.value) return
|
if (!props.canManage || customs.value?.status !== 'DECLARING' || !brokerSubmission.value || receiptUploadLocked.value || actionLoading.value) return
|
||||||
receiptFile.value = upload.raw || null
|
receiptFile.value = upload.raw || null
|
||||||
actionError.value = null
|
actionError.value = null
|
||||||
}
|
}
|
||||||
@@ -186,7 +187,7 @@ async function start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function uploadReceipt() {
|
async function uploadReceipt() {
|
||||||
if (!props.canManage || customs.value?.status !== 'DECLARING' || !brokerSubmission.value || brokerSubmitted.value) return
|
if (!props.canManage || customs.value?.status !== 'DECLARING' || !brokerSubmission.value || receiptUploadLocked.value || actionLoading.value) return
|
||||||
const selected = receiptFile.value
|
const selected = receiptFile.value
|
||||||
if (!selected) { ElMessage.warning('请选择对应的预录单'); return }
|
if (!selected) { ElMessage.warning('请选择对应的预录单'); return }
|
||||||
const documents = customs.value.documents
|
const documents = customs.value.documents
|
||||||
@@ -325,12 +326,12 @@ onMounted(load)
|
|||||||
<div class="workbench-evidence"><table class="compact-table density-table"><thead><tr><th>客户</th><th>订单号</th><th>入仓码</th></tr></thead><tbody><tr v-for="item in missingInbounds" :key="item.inboundId"><td>{{ item.customerName }}</td><td>{{ item.orderNo }}</td><td>{{ item.inboundCode }}</td></tr></tbody></table></div>
|
<div class="workbench-evidence"><table class="compact-table density-table"><thead><tr><th>客户</th><th>订单号</th><th>入仓码</th></tr></thead><tbody><tr v-for="item in missingInbounds" :key="item.inboundId"><td>{{ item.customerName }}</td><td>{{ item.orderNo }}</td><td>{{ item.inboundCode }}</td></tr></tbody></table></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="customs.documents.length" class="compact-table-wrap">
|
<div v-if="customs.documents.length" class="compact-table-wrap">
|
||||||
<div class="documents-heading"><h3>已冻结退税资料、预录单与完税文件</h3><div v-if="canManage && customs.status === 'DECLARING'" class="document-actions density-actions"><el-upload :disabled="!brokerSubmission || brokerSubmitted" :auto-upload="false" :limit="1" :show-file-list="false" :on-change="selectReceipt"><el-button class="density-action" size="small" :disabled="!brokerSubmission || brokerSubmitted">{{ !brokerSubmission ? '首次发送后上传预录单' : '选择预录单' }}</el-button></el-upload><span v-if="receiptFile" class="selected-receipt" :title="receiptFile.name">{{ receiptFile.name }}</span><el-button data-tone="primary" class="density-action" size="small" type="primary" :disabled="!brokerSubmission || brokerSubmitted || !receiptFile" :loading="actionLoading" @click="uploadReceipt">提交预录单</el-button></div></div>
|
<div class="documents-heading"><h3>已冻结退税资料、预录单与完税文件</h3><div v-if="canManage && customs.status === 'DECLARING'" class="document-actions density-actions"><el-upload :disabled="actionLoading || !brokerSubmission || receiptUploadLocked" :auto-upload="false" :limit="1" :show-file-list="false" :on-change="selectReceipt"><el-button class="density-action" size="small" :disabled="actionLoading || !brokerSubmission || receiptUploadLocked">{{ !brokerSubmission ? '首次发送后上传预录单' : '选择预录单' }}</el-button></el-upload><span v-if="receiptFile" class="selected-receipt" :title="receiptFile.name">{{ receiptFile.name }}</span><el-button data-tone="primary" class="density-action" size="small" type="primary" :disabled="actionLoading || !brokerSubmission || receiptUploadLocked || !receiptFile" :loading="actionLoading" @click="uploadReceipt">提交预录单</el-button></div></div>
|
||||||
<div class="workbench-evidence"><table class="compact-table document-table density-table"><thead><tr><th>操作</th><th>关联入仓码</th><th>报关行状态</th><th>客户退税资料</th><th>预录单</th><th>真实报关完税文件</th><th>客户</th><th>预录单情况</th></tr></thead><tbody>
|
<div class="workbench-evidence"><table class="compact-table document-table density-table"><thead><tr><th>操作</th><th>关联入仓码</th><th>报关行状态</th><th>客户退税资料</th><th>预录单</th><th>真实报关完税文件</th><th>客户</th><th>预录单情况</th></tr></thead><tbody>
|
||||||
<tr v-for="document in customs.documents" :key="document.documentId">
|
<tr v-for="document in customs.documents" :key="document.documentId">
|
||||||
<td><div v-if="canManage && customs.status === 'DECLARING'" class="document-actions density-actions"><el-upload :disabled="!brokerSubmitted" :auto-upload="false" :limit="1" :show-file-list="false" :on-change="(upload: UploadFile) => selectFinal(document.documentId, upload)"><el-button class="density-action" size="small" :disabled="!brokerSubmitted">{{ document.finalDocument ? '覆盖完税文件' : '上传完税文件' }}</el-button></el-upload><el-button data-tone="primary" class="density-action" size="small" type="primary" :disabled="!brokerSubmitted || !finalFiles[document.documentId]" :loading="actionLoading" @click="uploadFinal(document)">提交</el-button></div><span v-else class="readonly-label">只读</span></td>
|
<td><div v-if="canManage && customs.status === 'DECLARING'" class="document-actions density-actions"><el-upload :disabled="!brokerSubmitted" :auto-upload="false" :limit="1" :show-file-list="false" :on-change="(upload: UploadFile) => selectFinal(document.documentId, upload)"><el-button class="density-action" size="small" :disabled="!brokerSubmitted">{{ document.finalDocument ? '覆盖完税文件' : '上传完税文件' }}</el-button></el-upload><el-button data-tone="primary" class="density-action" size="small" type="primary" :disabled="!brokerSubmitted || !finalFiles[document.documentId]" :loading="actionLoading" @click="uploadFinal(document)">提交</el-button></div><span v-else class="readonly-label">只读</span></td>
|
||||||
<td>{{ document.inboundCodes.join('、') || '—' }}</td>
|
<td>{{ document.inboundCodes.join('、') || '—' }}</td>
|
||||||
<td>{{ brokerSubmitted ? '已发送报关行执行' : brokerSubmission ? '已首次发送,等待预录单确认' : '待首次发送报关行' }}</td>
|
<td>{{ brokerSubmitted ? '已发送报关行执行' : document.receiptStatus === 'CONFIRMED' ? '客户已确认预录单,待发送报关行执行' : brokerSubmission ? '已首次发送,等待预录单确认' : '待首次发送报关行' }}</td>
|
||||||
<td><div class="file-cell"><span :title="document.source.fileName">{{ document.source.fileName }}</span><el-button class="density-action" type="primary" :loading="downloadingKey === `source:${document.documentId}`" :aria-label="`下载客户退税资料 ${document.source.fileName}`" @click="downloadFile(document, 'source')">下载</el-button></div></td>
|
<td><div class="file-cell"><span :title="document.source.fileName">{{ document.source.fileName }}</span><el-button class="density-action" type="primary" :loading="downloadingKey === `source:${document.documentId}`" :aria-label="`下载客户退税资料 ${document.source.fileName}`" @click="downloadFile(document, 'source')">下载</el-button></div></td>
|
||||||
<td><div v-if="document.receipt" class="file-cell"><span :title="document.receipt.fileName">{{ document.receipt.fileName }}</span><el-button class="density-action" type="primary" :loading="downloadingKey === `receipt:${document.documentId}`" :aria-label="`下载预录单 ${document.receipt.fileName}`" @click="downloadFile(document, 'receipt')">下载</el-button></div><span v-else>未上传</span></td><td><div v-if="document.finalDocument" class="file-cell"><span :title="document.finalDocument.fileName">{{ document.finalDocument.fileName }}</span><el-button class="density-action" type="primary" :loading="downloadingKey === `final:${document.documentId}`" :aria-label="`下载报关完税文件 ${document.finalDocument.fileName}`" @click="downloadFile(document, 'final')">下载</el-button></div><span v-else>未上传</span></td><td>{{ documentCustomers(document) }}</td><td>{{ document.receipt ? (document.receiptStatus === 'CONFIRMED' ? '客户已确认预录单' : document.receiptStatus === 'REJECTED' ? '客户已打回预录单' : '待客户确认预录单') : '待上传预录单' }}</td>
|
<td><div v-if="document.receipt" class="file-cell"><span :title="document.receipt.fileName">{{ document.receipt.fileName }}</span><el-button class="density-action" type="primary" :loading="downloadingKey === `receipt:${document.documentId}`" :aria-label="`下载预录单 ${document.receipt.fileName}`" @click="downloadFile(document, 'receipt')">下载</el-button></div><span v-else>未上传</span></td><td><div v-if="document.finalDocument" class="file-cell"><span :title="document.finalDocument.fileName">{{ document.finalDocument.fileName }}</span><el-button class="density-action" type="primary" :loading="downloadingKey === `final:${document.documentId}`" :aria-label="`下载报关完税文件 ${document.finalDocument.fileName}`" @click="downloadFile(document, 'final')">下载</el-button></div><span v-else>未上传</span></td><td>{{ documentCustomers(document) }}</td><td>{{ document.receipt ? (document.receiptStatus === 'CONFIRMED' ? '客户已确认预录单' : document.receiptStatus === 'REJECTED' ? '客户已打回预录单' : '待客户确认预录单') : '待上传预录单' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -140,6 +140,13 @@ describe('inbound appointment native dialogs', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('inbound plan and pallet move native dialogs', () => {
|
describe('inbound plan and pallet move native dialogs', () => {
|
||||||
|
it('hides plan editing for first-leg generated inbound orders', async () => {
|
||||||
|
vi.mocked(listInboundOrders).mockResolvedValue({ records: [{ ...order, orderType: 'FIRST_LEG' }], total: 1, page: 1, size: 10, pages: 1 })
|
||||||
|
const wrapper = await listPage()
|
||||||
|
expect(wrapper.findAll('.el-table__body-wrapper button').map(button => button.text())).not.toContain('编辑')
|
||||||
|
expect(wrapper.findAll('.el-table__body-wrapper button').map(button => button.text())).toContain('查看')
|
||||||
|
expect(listReceivingOverseasWarehouses).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
it('shows plan identity and saves the selected master warehouse with the internal version through fullscreen', async () => {
|
it('shows plan identity and saves the selected master warehouse with the internal version through fullscreen', async () => {
|
||||||
const { dialog } = await openEditPlan()
|
const { dialog } = await openEditPlan()
|
||||||
expect(dialog.get('[aria-label="入库计划来源"]').findAll('input')).toHaveLength(0)
|
expect(dialog.get('[aria-label="入库计划来源"]').findAll('input')).toHaveLength(0)
|
||||||
|
|||||||
@@ -234,8 +234,9 @@ async function load(model: QueryModel = filters.value) {
|
|||||||
function search(model: QueryModel) { page.value = 1; void load(model) }
|
function search(model: QueryModel) { page.value = 1; void load(model) }
|
||||||
function reset(model: QueryModel) { page.value = 1; void load(model) }
|
function reset(model: QueryModel) { page.value = 1; void load(model) }
|
||||||
function openDetail(source: object) { const row = source as InboundOrderListItem; void router.push({ name: 'overseas-inbound-order-detail', params: { orderId: row.orderNo }, query: { customerId: row.customerId } }) }
|
function openDetail(source: object) { const row = source as InboundOrderListItem; void router.push({ name: 'overseas-inbound-order-detail', params: { orderId: row.orderNo }, query: { customerId: row.customerId } }) }
|
||||||
async function openPlanEdit(source: object) { const row = source as InboundOrderListItem; planEditTarget.value = row; planEditOpen.value = true; planEditLoading.value = true; mutationError.value = null; try { Object.assign(planEditForm, { warehouseId: row.warehouseId, warehouseCode: row.destinationWarehouseCode === '—' ? '' : row.destinationWarehouseCode, warehouseName: row.destinationWarehouseName === '—' ? '' : row.destinationWarehouseName, version: row.version }); if (!warehouses.value.length) warehouses.value = (await listReceivingOverseasWarehouses()).records } catch (error) { planEditOpen.value = false; mutationError.value = getApiErrorDetails(error, '海外仓列表加载失败,请重试') } finally { planEditLoading.value = false } }
|
function canEditPlan(row: InboundOrderListItem) { return access.can(permission.inboundOrderEdit) && row.status === '待入库' && row.type !== '头程订单' }
|
||||||
async function submitPlanEdit() { if (planEditSaving.value || planEditLoading.value || !access.can(permission.inboundOrderEdit)) return; const target = planEditTarget.value; if (!target || !planEditForm.warehouseId) { ElMessage.warning('请选择目的仓'); return } planEditSaving.value = true; mutationError.value = null; try { await updateInboundPlan(target.orderNo, { customerId: target.customerId, overseasWarehouseId: planEditForm.warehouseId, warehouseCode: planEditForm.warehouseCode, warehouseName: planEditForm.warehouseName, version: planEditForm.version }); planEditOpen.value = false; ElMessage.success('入库单已更新'); await load() } catch (error) { mutationError.value = getApiErrorDetails(error, '入库单更新失败,请重试') } finally { planEditSaving.value = false } }
|
async function openPlanEdit(source: object) { const row = source as InboundOrderListItem; if (!canEditPlan(row)) return; planEditTarget.value = row; planEditOpen.value = true; planEditLoading.value = true; mutationError.value = null; try { Object.assign(planEditForm, { warehouseId: row.warehouseId, warehouseCode: row.destinationWarehouseCode === '—' ? '' : row.destinationWarehouseCode, warehouseName: row.destinationWarehouseName === '—' ? '' : row.destinationWarehouseName, version: row.version }); if (!warehouses.value.length) warehouses.value = (await listReceivingOverseasWarehouses()).records } catch (error) { planEditOpen.value = false; mutationError.value = getApiErrorDetails(error, '海外仓列表加载失败,请重试') } finally { planEditLoading.value = false } }
|
||||||
|
async function submitPlanEdit() { if (planEditSaving.value || planEditLoading.value || !access.can(permission.inboundOrderEdit)) return; const target = planEditTarget.value; if (!target || !canEditPlan(target)) return; if (!planEditForm.warehouseId) { ElMessage.warning('请选择目的仓'); return } planEditSaving.value = true; mutationError.value = null; try { await updateInboundPlan(target.orderNo, { customerId: target.customerId, overseasWarehouseId: planEditForm.warehouseId, warehouseCode: planEditForm.warehouseCode, warehouseName: planEditForm.warehouseName, version: planEditForm.version }); planEditOpen.value = false; ElMessage.success('入库单已更新'); await load() } catch (error) { mutationError.value = getApiErrorDetails(error, '入库单更新失败,请重试') } finally { planEditSaving.value = false } }
|
||||||
onMounted(() => { void load() })
|
onMounted(() => { void load() })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -250,7 +251,7 @@ onMounted(() => { void load() })
|
|||||||
|
|
||||||
|
|
||||||
<template #toolbar-actions><el-button v-if="access.can(permission.inboundOrderAppointment)" plain :disabled="!selectedRows.length" @click="openAppointment"><CalendarCheck2 :size="16" />预约</el-button><el-button v-if="canCreateDispatch" type="primary" @click="openSmartDispatch"><Truck :size="16" />智能排车</el-button></template>
|
<template #toolbar-actions><el-button v-if="access.can(permission.inboundOrderAppointment)" plain :disabled="!selectedRows.length" @click="openAppointment"><CalendarCheck2 :size="16" />预约</el-button><el-button v-if="canCreateDispatch" type="primary" @click="openSmartDispatch"><Truck :size="16" />智能排车</el-button></template>
|
||||||
<template #actions="{ row }"><div class="row-actions density-actions admin-text-actions"><el-button class="density-action" data-tone="neutral" size="small" @click="openDetail(row)">查看</el-button><el-button class="density-action" data-tone="neutral" v-if="access.can(permission.inboundOrderEdit) && ['待入库'].includes((row as InboundOrderListItem).status)" size="small" @click="openPlanEdit(row)">编辑</el-button><el-button class="density-action" data-tone="neutral" v-if="(row as InboundOrderListItem).appointmentStatus !== '多批预约' && (row as InboundOrderListItem).appointmentNo !== '—' && access.can(permission.inboundOrderList)" size="small" @click="openAppointmentEdit(row, true)">查看预约</el-button><el-button class="density-action" data-tone="neutral" v-if="(row as InboundOrderListItem).appointmentStatus !== '多批预约' && (row as InboundOrderListItem).appointmentNo !== '—' && access.can(permission.inboundOrderAppointment)" size="small" @click="openAppointmentEdit(row)">预约</el-button><el-button v-if="(row as InboundOrderListItem).appointmentStatus === '多批预约' && access.can('platform:appointment:list')" class="density-action" @click="openMultipleAppointments(row as InboundOrderListItem)">管理多批预约</el-button></div></template>
|
<template #actions="{ row }"><div class="row-actions density-actions admin-text-actions"><el-button class="density-action" data-tone="neutral" size="small" @click="openDetail(row)">查看</el-button><el-button class="density-action" data-tone="neutral" v-if="canEditPlan(row as InboundOrderListItem)" size="small" @click="openPlanEdit(row)">编辑</el-button><el-button class="density-action" data-tone="neutral" v-if="(row as InboundOrderListItem).appointmentStatus !== '多批预约' && (row as InboundOrderListItem).appointmentNo !== '—' && access.can(permission.inboundOrderList)" size="small" @click="openAppointmentEdit(row, true)">查看预约</el-button><el-button class="density-action" data-tone="neutral" v-if="(row as InboundOrderListItem).appointmentStatus !== '多批预约' && (row as InboundOrderListItem).appointmentNo !== '—' && access.can(permission.inboundOrderAppointment)" size="small" @click="openAppointmentEdit(row)">预约</el-button><el-button v-if="(row as InboundOrderListItem).appointmentStatus === '多批预约' && access.can('platform:appointment:list')" class="density-action" @click="openMultipleAppointments(row as InboundOrderListItem)">管理多批预约</el-button></div></template>
|
||||||
<template #pagination-left>共 {{ total }} 条入库单 · 已选 {{ selectedRows.length }} 条</template>
|
<template #pagination-left>共 {{ total }} 条入库单 · 已选 {{ selectedRows.length }} 条</template>
|
||||||
</DataTable></div></section>
|
</DataTable></div></section>
|
||||||
</ListPageLayout>
|
</ListPageLayout>
|
||||||
|
|||||||
Reference in New Issue
Block a user