feat(admin): manage customs receipt and final files
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
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 { completeAllBuyContainerCustoms, downloadContainerCustomsCompletion, downloadContainerCustomsDocument, getContainerCustoms, startContainerCustoms, uploadContainerCustomsCompletion } from './platformContainerCustoms'
|
import { completeAllBuyContainerCustoms, downloadContainerCustomsReceipt, downloadContainerCustomsDocument, getContainerCustoms, startContainerCustoms, uploadContainerCustomsReceipt } from './platformContainerCustoms'
|
||||||
|
|
||||||
const customs = {
|
const customs = {
|
||||||
aggregateId: 2092438843630391401n, loadingPlanId: 2092438843630391402n, containerBookingId: 2092438843630391403n,
|
aggregateId: 2092438843630391401n, loadingPlanId: 2092438843630391402n, containerBookingId: 2092438843630391403n,
|
||||||
planNo: 'LP-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', status: 'READY_TO_START', hasActualCargo: true, version: '3', taxRefundCount: 1, coveredTaxRefundCount: 1, actualBoxCount: 12,
|
planNo: 'LP-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', status: 'READY_TO_START', hasActualCargo: true, version: '3', taxRefundCount: 1, coveredTaxRefundCount: 1, actualBoxCount: 12,
|
||||||
inbounds: [{ inboundId: 2092438843630391404n, orderId: 2092438843630391405n, orderNo: 'O-1', inboundCode: 'RK-1', tenantId: 2092438843630391406n, customerName: '客户一', taxMode: 'TAX_REFUND', covered: true }],
|
inbounds: [{ inboundId: 2092438843630391404n, orderId: 2092438843630391405n, orderNo: 'O-1', inboundCode: 'RK-1', tenantId: 2092438843630391406n, customerName: '客户一', taxMode: 'TAX_REFUND', covered: true }],
|
||||||
documents: [{ documentId: 2092438843630391407n, tenantId: 2092438843630391406n, source: { fileId: '2092438843630391408', fileName: 'source.pdf', fileHash: 'hash', sizeBytes: 1, version: 2, uploadedAt: '2026-08-28T10:00:00' }, inboundIds: ['2092438843630391404'], inboundCodes: ['RK-1'], frozen: true, completion: null }],
|
documents: [{ documentId: 2092438843630391407n, tenantId: 2092438843630391406n, source: { fileId: '2092438843630391408', fileName: 'source.pdf', fileHash: 'hash', sizeBytes: 1, version: 2, uploadedAt: '2026-08-28T10:00:00' }, inboundIds: ['2092438843630391404'], inboundCodes: ['RK-1'], frozen: true, receipt: null, finalDocument: null }],
|
||||||
startedAt: null, completedAt: null,
|
startedAt: null, completedAt: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,8 +43,8 @@ describe('platform container customs API', () => {
|
|||||||
const document = { ...customs.documents[0], documentId: '2092438843630391407', tenantId: '2092438843630391406' }
|
const document = { ...customs.documents[0], documentId: '2092438843630391407', tenantId: '2092438843630391406' }
|
||||||
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: document })
|
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: document })
|
||||||
const completion = new File(['done'], 'done.pdf')
|
const completion = new File(['done'], 'done.pdf')
|
||||||
await uploadContainerCustomsCompletion('2092438843630391407', completion, 0)
|
await uploadContainerCustomsReceipt('2092438843630391407', completion, 0)
|
||||||
expect(post.mock.calls[0]?.[0]).toBe('/platform/v1/customs/loading-plans/documents/2092438843630391407/completion/upload')
|
expect(post.mock.calls[0]?.[0]).toBe('/platform/v1/customs/loading-plans/documents/2092438843630391407/receipt/upload')
|
||||||
expect((post.mock.calls[0]?.[1] as FormData).get('file')).toBe(completion)
|
expect((post.mock.calls[0]?.[1] as FormData).get('file')).toBe(completion)
|
||||||
expect(post.mock.calls[0]?.[2]).toMatchObject({ params: { expectedVersion: 0 }, headers: { 'Content-Type': 'multipart/form-data', 'Idempotency-Key': expect.any(String) } })
|
expect(post.mock.calls[0]?.[2]).toMatchObject({ params: { expectedVersion: 0 }, headers: { 'Content-Type': 'multipart/form-data', 'Idempotency-Key': expect.any(String) } })
|
||||||
})
|
})
|
||||||
@@ -56,11 +56,11 @@ describe('platform container customs API', () => {
|
|||||||
const documentId = '2092438843630391407'
|
const documentId = '2092438843630391407'
|
||||||
|
|
||||||
await expect(downloadContainerCustomsDocument(documentId)).resolves.toBe(source)
|
await expect(downloadContainerCustomsDocument(documentId)).resolves.toBe(source)
|
||||||
await expect(downloadContainerCustomsCompletion(documentId)).resolves.toBe(completion)
|
await expect(downloadContainerCustomsReceipt(documentId)).resolves.toBe(completion)
|
||||||
|
|
||||||
expect(get.mock.calls).toEqual([
|
expect(get.mock.calls).toEqual([
|
||||||
[`/platform/v1/customs/loading-plans/documents/${documentId}/download`, { responseType: 'blob' }],
|
[`/platform/v1/customs/loading-plans/documents/${documentId}/download`, { responseType: 'blob' }],
|
||||||
[`/platform/v1/customs/loading-plans/documents/${documentId}/completion/download`, { responseType: 'blob' }],
|
[`/platform/v1/customs/loading-plans/documents/${documentId}/receipt/download`, { responseType: 'blob' }],
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -30,9 +30,10 @@ export interface ContainerCustomsDocument {
|
|||||||
inboundIds: string[]
|
inboundIds: string[]
|
||||||
inboundCodes: string[]
|
inboundCodes: string[]
|
||||||
frozen: boolean
|
frozen: boolean
|
||||||
completion: ContainerCustomsFile | null
|
receipt: ContainerCustomsFile | null
|
||||||
receiptStatus?: 'PENDING' | 'CONFIRMED' | 'REJECTED' | null
|
receiptStatus?: 'PENDING' | 'CONFIRMED' | 'REJECTED' | null
|
||||||
receiptRejectReason?: string | null
|
receiptRejectReason?: string | null
|
||||||
|
finalDocument: ContainerCustomsFile | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContainerCustomsView {
|
export interface ContainerCustomsView {
|
||||||
@@ -60,7 +61,7 @@ interface RawContainerCustomsView extends Omit<ContainerCustomsView, 'aggregateI
|
|||||||
containerBookingId: string | number
|
containerBookingId: string | number
|
||||||
version: string | number
|
version: string | number
|
||||||
inbounds: Array<Omit<ContainerCustomsInbound, 'inboundId' | 'orderId' | 'tenantId'> & { inboundId: string | number; orderId: string | number; tenantId: string | number }>
|
inbounds: Array<Omit<ContainerCustomsInbound, 'inboundId' | 'orderId' | 'tenantId'> & { inboundId: string | number; orderId: string | number; tenantId: string | number }>
|
||||||
documents: Array<Omit<ContainerCustomsDocument, 'documentId' | 'tenantId' | 'source' | 'completion'> & { documentId: string | number; tenantId: string | number; source: ContainerCustomsFile; completion: ContainerCustomsFile | null }>
|
documents: Array<Omit<ContainerCustomsDocument, 'documentId' | 'tenantId' | 'source' | 'receipt' | 'finalDocument'> & { documentId: string | number; tenantId: string | number; source: ContainerCustomsFile; receipt: ContainerCustomsFile | null; finalDocument: ContainerCustomsFile | null }>
|
||||||
}
|
}
|
||||||
|
|
||||||
const basePath = '/platform/v1/customs/loading-plans'
|
const basePath = '/platform/v1/customs/loading-plans'
|
||||||
@@ -73,7 +74,7 @@ function normalize(value: RawContainerCustomsView): ContainerCustomsView {
|
|||||||
...value,
|
...value,
|
||||||
aggregateId: String(value.aggregateId), loadingPlanId: String(value.loadingPlanId), containerBookingId: String(value.containerBookingId), version: Number(value.version),
|
aggregateId: String(value.aggregateId), loadingPlanId: String(value.loadingPlanId), containerBookingId: String(value.containerBookingId), version: Number(value.version),
|
||||||
inbounds: value.inbounds.map((item) => ({ ...item, inboundId: String(item.inboundId), orderId: String(item.orderId), tenantId: String(item.tenantId) })),
|
inbounds: value.inbounds.map((item) => ({ ...item, inboundId: String(item.inboundId), orderId: String(item.orderId), tenantId: String(item.tenantId) })),
|
||||||
documents: value.documents.map((item) => ({ ...item, documentId: String(item.documentId), tenantId: String(item.tenantId), source: file(item.source)!, completion: file(item.completion) })),
|
documents: value.documents.map((item) => ({ ...item, documentId: String(item.documentId), tenantId: String(item.tenantId), source: file(item.source)!, receipt: file(item.receipt), finalDocument: file(item.finalDocument) })),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,11 +88,15 @@ export function downloadContainerCustomsDocument(documentId: string) {
|
|||||||
return http.get<Blob, Blob>(`${basePath}/documents/${encodeURIComponent(documentId)}/download`, { responseType: 'blob' })
|
return http.get<Blob, Blob>(`${basePath}/documents/${encodeURIComponent(documentId)}/download`, { responseType: 'blob' })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadContainerCustomsCompletion(documentId: string) {
|
export function downloadContainerCustomsReceipt(documentId: string) {
|
||||||
return http.get<Blob, Blob>(`${basePath}/documents/${encodeURIComponent(documentId)}/completion/download`, { responseType: 'blob' })
|
return http.get<Blob, Blob>(`${basePath}/documents/${encodeURIComponent(documentId)}/receipt/download`, { responseType: 'blob' })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function action(loadingPlanId: string, name: 'start' | 'complete-all-buy', expectedVersion: number, remark?: string) {
|
export function downloadContainerCustomsFinal(documentId: string) {
|
||||||
|
return http.get<Blob, Blob>(`${basePath}/documents/${encodeURIComponent(documentId)}/final/download`, { responseType: 'blob' })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function action(loadingPlanId: string, name: 'start' | 'submit-to-broker' | 'complete-all-buy', expectedVersion: number, remark?: string) {
|
||||||
const response = await http.post<ApiEnvelope<RawContainerCustomsView>, ApiEnvelope<RawContainerCustomsView>>(
|
const response = await http.post<ApiEnvelope<RawContainerCustomsView>, ApiEnvelope<RawContainerCustomsView>>(
|
||||||
`${basePath}/${encodeURIComponent(loadingPlanId)}/${name}`,
|
`${basePath}/${encodeURIComponent(loadingPlanId)}/${name}`,
|
||||||
{ expectedVersion, confirm: true, remark: remark?.trim() || null },
|
{ expectedVersion, confirm: true, remark: remark?.trim() || null },
|
||||||
@@ -108,14 +113,26 @@ export function completeAllBuyContainerCustoms(loadingPlanId: string, expectedVe
|
|||||||
return action(loadingPlanId, 'complete-all-buy', expectedVersion)
|
return action(loadingPlanId, 'complete-all-buy', expectedVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadContainerCustomsCompletion(documentId: string, fileValue: File, expectedVersion: number) {
|
export function submitContainerCustomsToBroker(loadingPlanId: string, expectedVersion: number) {
|
||||||
|
return action(loadingPlanId, 'submit-to-broker', expectedVersion, '已将客户确认回执发送给报关行')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadFile(documentId: string, fileValue: File, expectedVersion: number, kind: 'receipt' | 'final') {
|
||||||
const body = new FormData()
|
const body = new FormData()
|
||||||
body.append('file', fileValue)
|
body.append('file', fileValue)
|
||||||
const response = await http.post<ApiEnvelope<ContainerCustomsDocument>, ApiEnvelope<ContainerCustomsDocument>>(
|
const response = await http.post<ApiEnvelope<ContainerCustomsDocument>, ApiEnvelope<ContainerCustomsDocument>>(
|
||||||
`${basePath}/documents/${encodeURIComponent(documentId)}/completion/upload`,
|
`${basePath}/documents/${encodeURIComponent(documentId)}/${kind}/upload`,
|
||||||
body,
|
body,
|
||||||
{ params: { expectedVersion }, headers: { 'Content-Type': 'multipart/form-data', 'Idempotency-Key': operationId('container-customs-completion') } },
|
{ params: { expectedVersion }, headers: { 'Content-Type': 'multipart/form-data', 'Idempotency-Key': operationId(`container-customs-${kind}`) } },
|
||||||
)
|
)
|
||||||
const result = unwrapApiResponse(response)
|
const result = unwrapApiResponse(response)
|
||||||
return { ...result, documentId: String(result.documentId), tenantId: String(result.tenantId), source: file(result.source)!, completion: file(result.completion) }
|
return { ...result, documentId: String(result.documentId), tenantId: String(result.tenantId), source: file(result.source)!, receipt: file(result.receipt), finalDocument: file(result.finalDocument) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadContainerCustomsReceipt(documentId: string, fileValue: File, expectedVersion: number) {
|
||||||
|
return uploadFile(documentId, fileValue, expectedVersion, 'receipt')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadContainerCustomsFinal(documentId: string, fileValue: File, expectedVersion: number) {
|
||||||
|
return uploadFile(documentId, fileValue, expectedVersion, 'final')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,16 +10,19 @@ const api = vi.hoisted(() => ({
|
|||||||
completeAllBuy: vi.fn(),
|
completeAllBuy: vi.fn(),
|
||||||
upload: vi.fn(),
|
upload: vi.fn(),
|
||||||
downloadSource: vi.fn(),
|
downloadSource: vi.fn(),
|
||||||
downloadCompletion: vi.fn(),
|
downloadReceipt: vi.fn(),
|
||||||
|
downloadFinal: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../api/platformContainerCustoms', () => ({
|
vi.mock('../api/platformContainerCustoms', () => ({
|
||||||
getContainerCustoms: api.get,
|
getContainerCustoms: api.get,
|
||||||
startContainerCustoms: api.start,
|
startContainerCustoms: api.start,
|
||||||
completeAllBuyContainerCustoms: api.completeAllBuy,
|
completeAllBuyContainerCustoms: api.completeAllBuy,
|
||||||
uploadContainerCustomsCompletion: api.upload,
|
uploadContainerCustomsReceipt: api.upload,
|
||||||
downloadContainerCustomsDocument: api.downloadSource,
|
downloadContainerCustomsDocument: api.downloadSource,
|
||||||
downloadContainerCustomsCompletion: api.downloadCompletion,
|
downloadContainerCustomsReceipt: api.downloadReceipt,
|
||||||
|
downloadContainerCustomsFinal: api.downloadFinal,
|
||||||
|
submitContainerCustomsToBroker: vi.fn(),
|
||||||
}))
|
}))
|
||||||
vi.mock('@/modules/downloads', () => ({ createPlatformExportTask: vi.fn() }))
|
vi.mock('@/modules/downloads', () => ({ createPlatformExportTask: vi.fn() }))
|
||||||
|
|
||||||
@@ -34,7 +37,8 @@ function customs(status: ContainerCustomsStatus, hasCompletion = false): Contain
|
|||||||
documentId: '2092438843630391407', tenantId: '2092438843630391406',
|
documentId: '2092438843630391407', tenantId: '2092438843630391406',
|
||||||
source: { fileId: '2092438843630391408', fileName: '客户退税资料.pdf', fileHash: 'source-hash', sizeBytes: 12, version: 2, uploadedAt: '2026-08-29T10:00:00' },
|
source: { fileId: '2092438843630391408', fileName: '客户退税资料.pdf', fileHash: 'source-hash', sizeBytes: 12, version: 2, uploadedAt: '2026-08-29T10:00:00' },
|
||||||
inboundIds: ['2092438843630391404'], inboundCodes: ['RK-1'], frozen: true,
|
inboundIds: ['2092438843630391404'], inboundCodes: ['RK-1'], frozen: true,
|
||||||
completion: hasCompletion ? { fileId: '2092438843630391409', fileName: '报关完成文件.pdf', fileHash: 'completion-hash', sizeBytes: 13, version: 1, uploadedAt: '2026-08-29T11:00:00' } : null,
|
receipt: hasCompletion ? { fileId: '2092438843630391409', fileName: '报关行回执.pdf', fileHash: 'receipt-hash', sizeBytes: 13, version: 1, uploadedAt: '2026-08-29T11:00:00' } : null,
|
||||||
|
finalDocument: hasCompletion ? { fileId: '2092438843630391410', fileName: '真实报关完税文件.pdf', fileHash: 'final-hash', sizeBytes: 14, version: 1, uploadedAt: '2026-08-29T12:00:00' } : null,
|
||||||
}],
|
}],
|
||||||
startedAt: status === 'DECLARING' || status === 'COMPLETED' ? '2026-08-29T10:30:00' : null,
|
startedAt: status === 'DECLARING' || status === 'COMPLETED' ? '2026-08-29T10:30:00' : null,
|
||||||
completedAt: status === 'COMPLETED' ? '2026-08-29T11:30:00' : null,
|
completedAt: status === 'COMPLETED' ? '2026-08-29T11:30:00' : null,
|
||||||
@@ -59,7 +63,8 @@ describe('ContainerCustomsPanel', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
api.downloadSource.mockResolvedValue(new Blob(['source']))
|
api.downloadSource.mockResolvedValue(new Blob(['source']))
|
||||||
api.downloadCompletion.mockResolvedValue(new Blob(['completion']))
|
api.downloadReceipt.mockResolvedValue(new Blob(['receipt']))
|
||||||
|
api.downloadFinal.mockResolvedValue(new Blob(['final']))
|
||||||
Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: vi.fn(() => 'blob:customs-file') })
|
Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: vi.fn(() => 'blob:customs-file') })
|
||||||
Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: vi.fn() })
|
Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: vi.fn() })
|
||||||
vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => undefined)
|
vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => undefined)
|
||||||
@@ -113,7 +118,8 @@ describe('ContainerCustomsPanel', () => {
|
|||||||
|
|
||||||
wrapper = await render('DECLARING', true, true)
|
wrapper = await render('DECLARING', true, true)
|
||||||
expect(wrapper.text()).toContain('覆盖报关行回执')
|
expect(wrapper.text()).toContain('覆盖报关行回执')
|
||||||
expect(wrapper.text()).toContain('报关完成文件.pdf')
|
expect(wrapper.text()).toContain('报关行回执.pdf')
|
||||||
|
expect(wrapper.text()).toContain('真实报关完税文件.pdf')
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
|
|
||||||
wrapper = await render('DECLARING', false, true)
|
wrapper = await render('DECLARING', false, true)
|
||||||
@@ -126,7 +132,8 @@ describe('ContainerCustomsPanel', () => {
|
|||||||
it('keeps COMPLETED strictly read-only with both files visible and downloadable', async () => {
|
it('keeps COMPLETED strictly read-only with both files visible and downloadable', async () => {
|
||||||
const wrapper = await render('COMPLETED', true, true)
|
const wrapper = await render('COMPLETED', true, true)
|
||||||
expect(wrapper.text()).toContain('客户退税资料.pdf')
|
expect(wrapper.text()).toContain('客户退税资料.pdf')
|
||||||
expect(wrapper.text()).toContain('报关完成文件.pdf')
|
expect(wrapper.text()).toContain('报关行回执.pdf')
|
||||||
|
expect(wrapper.text()).toContain('真实报关完税文件.pdf')
|
||||||
expect(wrapper.text()).toContain('报关已完成,待其他离仓条件')
|
expect(wrapper.text()).toContain('报关已完成,待其他离仓条件')
|
||||||
expect(buttonTexts(wrapper)).not.toContain('上传报关行回执')
|
expect(buttonTexts(wrapper)).not.toContain('上传报关行回执')
|
||||||
expect(buttonTexts(wrapper)).not.toContain('覆盖报关行回执')
|
expect(buttonTexts(wrapper)).not.toContain('覆盖报关行回执')
|
||||||
@@ -138,13 +145,16 @@ describe('ContainerCustomsPanel', () => {
|
|||||||
await wrapper.get('[aria-label="下载客户退税资料 客户退税资料.pdf"]').trigger('click')
|
await wrapper.get('[aria-label="下载客户退税资料 客户退税资料.pdf"]').trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(api.downloadSource).toHaveBeenCalledWith('2092438843630391407')
|
expect(api.downloadSource).toHaveBeenCalledWith('2092438843630391407')
|
||||||
expect(api.downloadCompletion).not.toHaveBeenCalled()
|
expect(api.downloadReceipt).not.toHaveBeenCalled()
|
||||||
|
|
||||||
await wrapper.get('[aria-label="下载报关行回执 报关完成文件.pdf"]').trigger('click')
|
await wrapper.get('[aria-label="下载报关行回执 报关行回执.pdf"]').trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(api.downloadCompletion).toHaveBeenCalledWith('2092438843630391407')
|
expect(api.downloadReceipt).toHaveBeenCalledWith('2092438843630391407')
|
||||||
expect(HTMLAnchorElement.prototype.click).toHaveBeenCalledTimes(2)
|
await wrapper.get('[aria-label="下载报关完税文件 真实报关完税文件.pdf"]').trigger('click')
|
||||||
expect(URL.createObjectURL).toHaveBeenCalledTimes(2)
|
await flushPromises()
|
||||||
|
expect(api.downloadFinal).toHaveBeenCalledWith('2092438843630391407')
|
||||||
|
expect(HTMLAnchorElement.prototype.click).toHaveBeenCalledTimes(3)
|
||||||
|
expect(URL.createObjectURL).toHaveBeenCalledTimes(3)
|
||||||
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:customs-file')
|
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:customs-file')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -159,6 +169,6 @@ describe('ContainerCustomsPanel', () => {
|
|||||||
expect(wrapper.text()).toContain('CUSTOMS_FILE_404')
|
expect(wrapper.text()).toContain('CUSTOMS_FILE_404')
|
||||||
expect(wrapper.text()).toContain('request-customs-download')
|
expect(wrapper.text()).toContain('request-customs-download')
|
||||||
expect(wrapper.text()).toContain('trace-customs-download')
|
expect(wrapper.text()).toContain('trace-customs-download')
|
||||||
expect(api.downloadCompletion).not.toHaveBeenCalled()
|
expect(api.downloadReceipt).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,11 +9,14 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
|||||||
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
||||||
import {
|
import {
|
||||||
completeAllBuyContainerCustoms,
|
completeAllBuyContainerCustoms,
|
||||||
downloadContainerCustomsCompletion,
|
downloadContainerCustomsReceipt,
|
||||||
|
downloadContainerCustomsFinal,
|
||||||
downloadContainerCustomsDocument,
|
downloadContainerCustomsDocument,
|
||||||
getContainerCustoms,
|
getContainerCustoms,
|
||||||
startContainerCustoms,
|
startContainerCustoms,
|
||||||
uploadContainerCustomsCompletion,
|
submitContainerCustomsToBroker,
|
||||||
|
uploadContainerCustomsReceipt,
|
||||||
|
uploadContainerCustomsFinal,
|
||||||
type ContainerCustomsDocument,
|
type ContainerCustomsDocument,
|
||||||
type ContainerCustomsStatus,
|
type ContainerCustomsStatus,
|
||||||
type ContainerCustomsView,
|
type ContainerCustomsView,
|
||||||
@@ -33,7 +36,8 @@ const actionLoading = ref(false)
|
|||||||
const loadError = ref<ApiErrorDetails | null>(null)
|
const loadError = ref<ApiErrorDetails | null>(null)
|
||||||
const actionError = ref<ApiErrorDetails | null>(null)
|
const actionError = ref<ApiErrorDetails | null>(null)
|
||||||
const downloadingKey = ref('')
|
const downloadingKey = ref('')
|
||||||
const completionFiles = reactive<Record<string, File | null>>({})
|
const receiptFiles = reactive<Record<string, File | null>>({})
|
||||||
|
const finalFiles = reactive<Record<string, File | null>>({})
|
||||||
|
|
||||||
const missingInbounds = computed(() => customs.value?.inbounds.filter((item) => item.taxMode === 'TAX_REFUND' && !item.covered) || [])
|
const missingInbounds = computed(() => customs.value?.inbounds.filter((item) => item.taxMode === 'TAX_REFUND' && !item.covered) || [])
|
||||||
const missingCoverageCount = computed(() => Math.max(0, (customs.value?.taxRefundCount || 0) - (customs.value?.coveredTaxRefundCount || 0)))
|
const missingCoverageCount = computed(() => Math.max(0, (customs.value?.taxRefundCount || 0) - (customs.value?.coveredTaxRefundCount || 0)))
|
||||||
@@ -45,9 +49,14 @@ const statusText: Record<ContainerCustomsStatus, string> = {
|
|||||||
function documentCustomers(document: ContainerCustomsDocument) {
|
function documentCustomers(document: ContainerCustomsDocument) {
|
||||||
return [...new Set((customs.value?.inbounds || []).filter((item) => document.inboundIds.includes(item.inboundId)).map((item) => item.customerName))].join('、') || '—'
|
return [...new Set((customs.value?.inbounds || []).filter((item) => document.inboundIds.includes(item.inboundId)).map((item) => item.customerName))].join('、') || '—'
|
||||||
}
|
}
|
||||||
function selectCompletion(documentId: string, upload: UploadFile) {
|
function selectReceipt(documentId: string, upload: UploadFile) {
|
||||||
if (!props.canManage || customs.value?.status !== 'DECLARING') return
|
if (!props.canManage || customs.value?.status !== 'DECLARING') return
|
||||||
completionFiles[documentId] = upload.raw || null
|
receiptFiles[documentId] = upload.raw || null
|
||||||
|
actionError.value = null
|
||||||
|
}
|
||||||
|
function selectFinal(documentId: string, upload: UploadFile) {
|
||||||
|
if (!props.canManage || customs.value?.status !== 'DECLARING') return
|
||||||
|
finalFiles[documentId] = upload.raw || null
|
||||||
actionError.value = null
|
actionError.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,18 +80,31 @@ async function start() {
|
|||||||
finally { actionLoading.value = false }
|
finally { actionLoading.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadCompletion(document: ContainerCustomsDocument) {
|
async function uploadReceipt(document: ContainerCustomsDocument) {
|
||||||
if (!props.canManage || customs.value?.status !== 'DECLARING') return
|
if (!props.canManage || customs.value?.status !== 'DECLARING') return
|
||||||
const selected = completionFiles[document.documentId]
|
const selected = receiptFiles[document.documentId]
|
||||||
if (!selected) { ElMessage.warning('请选择对应的报关完成文件'); return }
|
if (!selected) { ElMessage.warning('请选择对应的报关行回执'); return }
|
||||||
actionLoading.value = true; actionError.value = null
|
actionLoading.value = true; actionError.value = null
|
||||||
try {
|
try {
|
||||||
await uploadContainerCustomsCompletion(document.documentId, selected, document.completion?.version || 0)
|
await uploadContainerCustomsReceipt(document.documentId, selected, document.receipt?.version || 0)
|
||||||
completionFiles[document.documentId] = null
|
receiptFiles[document.documentId] = null
|
||||||
await load()
|
await load()
|
||||||
emit('refreshed')
|
emit('refreshed')
|
||||||
ElMessage.success('报关行回执已上传,等待客户确认')
|
ElMessage.success('报关行回执已上传,等待客户确认')
|
||||||
} catch (error) { actionError.value = getApiErrorDetails(error, '报关完成文件上传失败') }
|
} catch (error) { actionError.value = getApiErrorDetails(error, '报关行回执上传失败') }
|
||||||
|
finally { actionLoading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadFinal(document: ContainerCustomsDocument) {
|
||||||
|
if (!props.canManage || customs.value?.status !== 'DECLARING') return
|
||||||
|
const selected = finalFiles[document.documentId]
|
||||||
|
if (!selected) { ElMessage.warning('请选择真实报关完税文件'); return }
|
||||||
|
actionLoading.value = true; actionError.value = null
|
||||||
|
try {
|
||||||
|
await uploadContainerCustomsFinal(document.documentId, selected, document.finalDocument?.version || 0)
|
||||||
|
finalFiles[document.documentId] = null
|
||||||
|
await load(); emit('refreshed'); ElMessage.success('真实报关完税文件已上传')
|
||||||
|
} catch (error) { actionError.value = getApiErrorDetails(error, '真实报关完税文件上传失败') }
|
||||||
finally { actionLoading.value = false }
|
finally { actionLoading.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,8 +117,8 @@ function saveBlob(blob: Blob, fileName: string) {
|
|||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadFile(document: ContainerCustomsDocument, kind: 'source' | 'completion') {
|
async function downloadFile(document: ContainerCustomsDocument, kind: 'source' | 'receipt' | 'final') {
|
||||||
const target = kind === 'source' ? document.source : document.completion
|
const target = kind === 'source' ? document.source : kind === 'receipt' ? document.receipt : document.finalDocument
|
||||||
if (!target) return
|
if (!target) return
|
||||||
const key = `${kind}:${document.documentId}`
|
const key = `${kind}:${document.documentId}`
|
||||||
downloadingKey.value = key
|
downloadingKey.value = key
|
||||||
@@ -104,9 +126,9 @@ async function downloadFile(document: ContainerCustomsDocument, kind: 'source' |
|
|||||||
try {
|
try {
|
||||||
const blob = kind === 'source'
|
const blob = kind === 'source'
|
||||||
? await downloadContainerCustomsDocument(document.documentId)
|
? await downloadContainerCustomsDocument(document.documentId)
|
||||||
: await downloadContainerCustomsCompletion(document.documentId)
|
: kind === 'receipt' ? await downloadContainerCustomsReceipt(document.documentId) : await downloadContainerCustomsFinal(document.documentId)
|
||||||
saveBlob(blob, target.fileName)
|
saveBlob(blob, target.fileName)
|
||||||
ElMessage.success(`${kind === 'source' ? '客户退税资料' : '报关完成文件'}下载已开始`)
|
ElMessage.success(`${kind === 'source' ? '客户退税资料' : kind === 'receipt' ? '报关行回执' : '报关完税文件'}下载已开始`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
actionError.value = getApiErrorDetails(error, `${kind === 'source' ? '客户退税资料' : '报关完成文件'}下载失败`)
|
actionError.value = getApiErrorDetails(error, `${kind === 'source' ? '客户退税资料' : '报关完成文件'}下载失败`)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -114,6 +136,15 @@ async function downloadFile(document: ContainerCustomsDocument, kind: 'source' |
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function submitToBroker() {
|
||||||
|
if (!customs.value || customs.value.status !== 'DECLARING') return
|
||||||
|
try { await ElMessageBox.confirm('确认已核对全部回执并把确认结果发送给报关行执行?', '发送报关行执行', { customClass: 'admin-density-confirm', type: 'warning', confirmButtonText: '确认发送' }) } catch { return }
|
||||||
|
actionLoading.value = true; actionError.value = null
|
||||||
|
try { customs.value = await submitContainerCustomsToBroker(props.loadingPlanId, customs.value.version); ElMessage.success('已发送给报关行执行') }
|
||||||
|
catch (error) { actionError.value = getApiErrorDetails(error, '发送报关行执行失败') }
|
||||||
|
finally { actionLoading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
async function completeAllBuy() {
|
async function completeAllBuy() {
|
||||||
if (!customs.value || customs.value.status !== 'DECLARING' || !allBuy.value) return
|
if (!customs.value || customs.value.status !== 'DECLARING' || !allBuy.value) return
|
||||||
try { await ElMessageBox.confirm('确认全买单柜报关已经完成?报关完成不等同于国内离仓。', '完成全买单柜报关', { customClass: 'admin-density-confirm', type: 'warning', confirmButtonText: '确认完成' }) }
|
try { await ElMessageBox.confirm('确认全买单柜报关已经完成?报关完成不等同于国内离仓。', '完成全买单柜报关', { customClass: 'admin-density-confirm', type: 'warning', confirmButtonText: '确认完成' }) }
|
||||||
@@ -152,6 +183,7 @@ onMounted(load)
|
|||||||
<el-tag :type="!customs.hasActualCargo ? 'info' : customs.status === 'COMPLETED' ? 'success' : customs.status === 'WAITING_CUSTOMER_DOCUMENTS' ? 'danger' : 'warning'">{{ customs.hasActualCargo ? statusText[customs.status] : '无需报关' }}</el-tag>
|
<el-tag :type="!customs.hasActualCargo ? 'info' : customs.status === 'COMPLETED' ? 'success' : customs.status === 'WAITING_CUSTOMER_DOCUMENTS' ? 'danger' : 'warning'">{{ customs.hasActualCargo ? statusText[customs.status] : '无需报关' }}</el-tag>
|
||||||
<el-button v-if="canManage && allBuy" plain :loading="actionLoading" @click="exportAllBuy">导出买单 XLSX</el-button>
|
<el-button v-if="canManage && allBuy" plain :loading="actionLoading" @click="exportAllBuy">导出买单 XLSX</el-button>
|
||||||
<el-button v-if="canManage && customs.hasActualCargo && customs.status === 'READY_TO_START'" type="primary" :loading="actionLoading" @click="start">开始报关</el-button>
|
<el-button v-if="canManage && customs.hasActualCargo && customs.status === 'READY_TO_START'" type="primary" :loading="actionLoading" @click="start">开始报关</el-button>
|
||||||
|
<el-button v-if="canManage && customs.hasActualCargo && customs.status === 'DECLARING'" type="primary" plain :loading="actionLoading" @click="submitToBroker">发送报关行执行</el-button>
|
||||||
<el-button v-if="canManage && allBuy && customs.status === 'DECLARING'" type="primary" :loading="actionLoading" @click="completeAllBuy">确认报关完成</el-button>
|
<el-button v-if="canManage && allBuy && customs.status === 'DECLARING'" type="primary" :loading="actionLoading" @click="completeAllBuy">确认报关完成</el-button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -167,13 +199,14 @@ onMounted(load)
|
|||||||
<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>
|
<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 v-if="customs.documents.length" class="compact-table-wrap">
|
<div v-if="customs.documents.length" class="compact-table-wrap">
|
||||||
<h3>已冻结退税资料与报关行回执</h3>
|
<h3>已冻结退税资料、报关行回执与完税文件</h3>
|
||||||
<table class="compact-table document-table density-table"><thead><tr><th>操作</th><th>客户退税资料</th><th>报关行回执</th><th>关联入仓码</th><th>版本</th><th>客户</th><th>回执情况</th></tr></thead><tbody>
|
<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 :auto-upload="false" :limit="1" :show-file-list="false" :on-change="(upload: UploadFile) => selectCompletion(document.documentId, upload)"><el-button class="density-action" size="small">{{ document.completion ? '覆盖报关行回执' : '上传报关行回执' }}</el-button></el-upload><el-button data-tone="primary" class="density-action" size="small" type="primary" :disabled="!completionFiles[document.documentId]" :loading="actionLoading" @click="uploadCompletion(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 :auto-upload="false" :limit="1" :show-file-list="false" :on-change="(upload: UploadFile) => selectReceipt(document.documentId, upload)"><el-button class="density-action" size="small">{{ document.receipt ? '覆盖报关行回执' : '上传报关行回执' }}</el-button></el-upload><el-button data-tone="primary" class="density-action" size="small" type="primary" :disabled="!receiptFiles[document.documentId]" :loading="actionLoading" @click="uploadReceipt(document)">提交</el-button><el-upload :auto-upload="false" :limit="1" :show-file-list="false" :on-change="(upload: UploadFile) => selectFinal(document.documentId, upload)"><el-button class="density-action" size="small">{{ document.finalDocument ? '覆盖完税文件' : '上传完税文件' }}</el-button></el-upload><el-button data-tone="primary" class="density-action" size="small" type="primary" :disabled="!finalFiles[document.documentId]" :loading="actionLoading" @click="uploadFinal(document)">提交</el-button></div><span v-else class="readonly-label">只读</span></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.completion" class="file-cell"><span :title="document.completion.fileName">{{ document.completion.fileName }}</span><el-button class="density-action" type="primary" :loading="downloadingKey === `completion:${document.documentId}`" :aria-label="`下载报关行回执 ${document.completion.fileName}`" @click="downloadFile(document, 'completion')">下载</el-button></div><span v-else>未上传</span></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>{{ document.inboundCodes.join('、') || '—' }}</td><td>退税资料 v{{ document.source.version }}<span v-if="document.completion"> / 回执 v{{ document.completion.version }}</span></td><td>{{ documentCustomers(document) }}</td><td>{{ document.completion ? '已上传报关行回执' : '待上传报关行回执' }}</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>{{ document.inboundCodes.join('、') || '—' }}</td><td>退税资料 v{{ document.source.version }}<span v-if="document.receipt"> / 回执 v{{ document.receipt.version }}</span><span v-if="document.finalDocument"> / 完税 v{{ document.finalDocument.version }}</span></td><td>{{ documentCustomers(document) }}</td><td>{{ document.receipt ? (document.receiptStatus === 'CONFIRMED' ? '客户已确认回执' : '已上传回执') : '待上传报关行回执' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody></table>
|
</tbody></table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user