diff --git a/.gitignore b/.gitignore index a148397..eb97814 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store node_modules +pnpm-lock.yaml /dist /tests/e2e/videos/ diff --git a/package.json b/package.json index f1ead31..ee42d0f 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "prod": "vite --mode production", "build": "vite build", "build:prod": "vite build --mode production", - "serve": "vite preview --host" + "serve": "vite preview --host", + "typecheck": "vue-tsc --noEmit" }, "dependencies": { "@element-plus/icons-vue": "^2.3.1", @@ -47,9 +48,11 @@ "prettier": "^2.8.7", "sass": "^1.85.1", "terser": "^5.31.1", + "typescript": "^7.0.2", "unplugin-auto-import": "^0.11.2", "vite": "^5.4.19", "vite-plugin-compression": "^0.5.1", - "vite-plugin-vue-setup-extend": "^0.4.0" + "vite-plugin-vue-setup-extend": "^0.4.0", + "vue-tsc": "^3.3.11" } } diff --git a/src/api/base/currency.js b/src/api/base/currency.js index adb7555..69fd94b 100644 --- a/src/api/base/currency.js +++ b/src/api/base/currency.js @@ -58,3 +58,14 @@ export const changeStatus = (id, status) => { }, }); }; + +export const getBfiExchangeRate = (currencyCode, effectdate) => { + return request({ + url: '/blade-transport/bfi/exchange-rate', + method: 'get', + params: { + currencyCode, + effectdate, + }, + }); +}; diff --git a/src/api/payment/invoiceApplication.js b/src/api/payment/invoiceApplication.js index 3e0433f..717703d 100644 --- a/src/api/payment/invoiceApplication.js +++ b/src/api/payment/invoiceApplication.js @@ -22,7 +22,7 @@ export const approve = data => request({ url: `${baseUrl}/approve`, method: 'pos export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data }); export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data }); export const syncKingdee = id => - request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } }); + request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } }); export const invoiceTypeOptions = [ { label: '电子专票', value: 'electronic_special' }, diff --git a/src/api/payment/invoiceReceipt.js b/src/api/payment/invoiceReceipt.js index d4cf692..071eff1 100644 --- a/src/api/payment/invoiceReceipt.js +++ b/src/api/payment/invoiceReceipt.js @@ -5,8 +5,17 @@ const baseUrl = '/blade-transport/invoice-receipt'; export const getList = (current, size, params) => request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } }); export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } }); -export const getInvoicePool = keyword => - request({ url: `${baseUrl}/invoice-pool`, method: 'get', params: { keyword } }); +export const queryInvoicePool = params => + request({ url: `${baseUrl}/invoice-pool`, method: 'get', params }); +export const downloadInvoicePoolAttachment = invoiceNo => + // 金蝶票池发票文件下载(后端代理 A801-2 流式回源),耗时较普通接口长,放宽到 120s + request({ + url: `${baseUrl}/invoice-pool/attachment/download`, + method: 'get', + params: { invoiceNo }, + responseType: 'blob', + timeout: 120000, + }); export const getSettlementCandidates = (keyword, receiptId) => request({ url: `${baseUrl}/settlement-candidates`, @@ -25,8 +34,8 @@ export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post' export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data }); export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data }); export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data }); -export const syncKingdee = id => - request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } }); +export const pushKingdee = ids => + request({ url: `${baseUrl}/push-kingdee`, method: 'post', timeout: 30000, data: ids }); export const approvalStatusOptions = [ { label: '草稿', value: 'draft' }, @@ -36,8 +45,9 @@ export const approvalStatusOptions = [ { label: '已作废', value: 'voided' }, ]; -export const kingdeeStatusOptions = [ - { label: '未同步', value: 'unsynced' }, - { label: '已同步', value: 'synced' }, - { label: '同步失败', value: 'failed' }, +export const kingdeePushStatusOptions = [ + { label: '未推送', value: 'unsynced' }, + { label: '已保存收票单', value: 'pushed' }, + { label: '已指定应付', value: 'assigned' }, + { label: '推送失败', value: 'failed' }, ]; diff --git a/src/api/payment/paymentApplication.js b/src/api/payment/paymentApplication.js index 43e50e1..3e4c802 100644 --- a/src/api/payment/paymentApplication.js +++ b/src/api/payment/paymentApplication.js @@ -16,10 +16,15 @@ export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post' export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data }); export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data }); export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data }); +// 同步金蝶涉及多次金蝶远程接口调用(A103-2 预检 + A103-1/A103-3 推送),单笔可达 20s+,覆盖全局 10s 超时 export const syncKingdee = id => - request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } }); + request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } }); +// 批量同步逐单单据级事务串行推送,耗时随单量线性增长 export const syncKingdeeBatch = ids => - request({ url: `${baseUrl}/sync-kingdee-batch`, method: 'post', data: ids }); + request({ url: `${baseUrl}/sync-kingdee-batch`, method: 'post', timeout: 30000, data: ids }); +// 回写任务按 paymentNo 分批远程查询 A103-2/A103-4,同样可能超过全局超时 +export const syncKingdeeResult = () => + request({ url: `${baseUrl}/sync-kingdee-result`, method: 'post', timeout: 30000 }); export const paymentTypeOptions = [ { label: '项目预付', value: 'project_advance' }, @@ -36,5 +41,8 @@ export const approvalStatusOptions = [ export const kingdeeStatusOptions = [ { label: '未生成', value: 'unsynced' }, { label: '已生成', value: 'synced' }, + { label: '付款中', value: 'paying' }, + { label: '已付款', value: 'paid' }, + { label: '已关闭', value: 'closed' }, { label: '生成失败', value: 'failed' }, ]; diff --git a/src/api/payment/receiptClaimRecord.js b/src/api/payment/receiptClaimRecord.js index 4bda63f..570888d 100644 --- a/src/api/payment/receiptClaimRecord.js +++ b/src/api/payment/receiptClaimRecord.js @@ -8,14 +8,24 @@ export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get' export const updateAttachments = data => request({ url: `${baseUrl}/attachments`, method: 'post', data }); export const voidClaim = id => request({ url: `${baseUrl}/void`, method: 'post', params: { id } }); +export const queryKingdeeResult = () => + request({ url: `${baseUrl}/query-kingdee-result`, method: 'post' }); export const claimStatusOptions = [ { label: '已认领', value: 'claimed' }, { label: '已作废', value: 'voided' }, ]; +export const pushStatusOptions = [ + { label: '未推送', value: 'none' }, + { label: '已推送', value: 'pushed' }, + { label: '推送失败', value: 'failed' }, +]; + export const kingdeeBillStatusOptions = [ { label: '未生成', value: 'none' }, + { label: '已推送', value: 'pushed' }, { label: '审核通过', value: 'approved' }, + { label: '已收款', value: 'received' }, { label: '处理失败', value: 'failed' }, ]; diff --git a/src/api/payment/receiptFlow.js b/src/api/payment/receiptFlow.js index 32ad53b..231e0ac 100644 --- a/src/api/payment/receiptFlow.js +++ b/src/api/payment/receiptFlow.js @@ -12,10 +12,25 @@ export const getSettlementCandidates = (keyword, flowId) => params: { keyword, flowId }, }); export const claim = data => request({ url: `${baseUrl}/claim`, method: 'post', data }); -export const sync = data => request({ url: `${baseUrl}/sync`, method: 'post', data }); +export const sync = () => request({ url: `${baseUrl}/sync`, method: 'post' }); +export const pushKingdee = flowId => + request({ url: `${baseUrl}/push-kingdee`, method: 'post', params: { flowId } }); export const claimStatusOptions = [ { label: '未认领', value: 'unclaimed' }, { label: '部分认领', value: 'partial' }, { label: '认领完成', value: 'claimed' }, ]; + +export const pushStatusOptions = [ + { label: '未推送', value: 'none' }, + { label: '已推送', value: 'pushed' }, + { label: '推送失败', value: 'failed' }, +]; + +export const kingdeeBillStatusOptions = [ + { label: '已推送', value: 'pushed' }, + { label: '审核通过', value: 'approved' }, + { label: '已收款', value: 'received' }, + { label: '处理失败', value: 'failed' }, +]; diff --git a/src/api/settlement/formalSettlement.js b/src/api/settlement/formalSettlement.js index 50e8810..92830f8 100644 --- a/src/api/settlement/formalSettlement.js +++ b/src/api/settlement/formalSettlement.js @@ -30,8 +30,9 @@ export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post' export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data }); export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data }); export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data }); +// 同步金蝶涉及多次金蝶远程接口调用(A102-4 预检 + A102-2 保存),可达 20s+,覆盖全局 10s 超时 export const syncKingdee = id => - request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } }); + request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } }); export const getDetailFees = detailId => request({ url: `${baseUrl}/detail-fees`, method: 'get', params: { detailId } }); export const getReceiptClaims = formalSettlementId => @@ -48,3 +49,5 @@ export const applyPayments = data => request({ url: `${baseUrl}/apply-payments`, method: 'post', data }); export const claimInvoices = data => request({ url: `${baseUrl}/claim-invoices`, method: 'post', data }); +export const queryInvoicePool = params => + request({ url: `${baseUrl}/invoice-pool`, method: 'get', params }); diff --git a/src/api/vehicle/customer-archive.js b/src/api/vehicle/customer-archive.js index f6266db..0814c2a 100644 --- a/src/api/vehicle/customer-archive.js +++ b/src/api/vehicle/customer-archive.js @@ -152,6 +152,28 @@ export const getScoreTemplate = quantificationId => { }); }; +export const queryKingdeeCustomer = creditCode => { + return request({ + url: '/blade-transport/bfi/customer/query-by-credit-code', + method: 'get', + timeout: 30000, + params: { + creditCode, + }, + }); +}; + +export const syncKingdee = id => { + return request({ + url: '/blade-transport/customer-archive/sync-kingdee', + method: 'post', + timeout: 30000, + params: { + id, + }, + }); +}; + export const recognizeBusinessLicenseOcr = imageUrl => { return request({ url: '/blade-transport/baidu-ocr/recognize-url', diff --git a/src/axios.js b/src/axios.js index 529a3f5..1d23232 100644 --- a/src/axios.js +++ b/src/axios.js @@ -271,10 +271,18 @@ axios.interceptors.response.use( }, error => { NProgress.done(); + // 网络级失败(HTTP 状态 >500、网关错误、超时、断网等 validateStatus 放行之外的响应) + // 原为静默 reject,接口报错页面无任何感知;统一补错误提示,取消请求与刷新令牌请求除外 + if (!axios.isCancel(error) && !isRefreshTokenRequest(error?.config)) { + ElMessage({ + message: error?.response?.data?.msg || '请求失败,请稍后重试', + type: 'error', + }); + } + return Promise.reject(new Error(error)); // 保留原始 axios 错误对象:下游(如导入失败明细解析)需要 error.response // 读取响应体。若包装成 new Error(error),response 会丢失、message 退化成 // "[object Object]",用户只能看到无意义的通用提示。 - return Promise.reject(error); } ); diff --git a/src/components/kingdee-invoice-pool-dialog/main.vue b/src/components/kingdee-invoice-pool-dialog/main.vue new file mode 100644 index 0000000..7b0449c --- /dev/null +++ b/src/components/kingdee-invoice-pool-dialog/main.vue @@ -0,0 +1,434 @@ + + + + + diff --git a/src/components/vehicle-attachment-table/main.vue b/src/components/vehicle-attachment-table/main.vue index 4ca1d88..15e7f1d 100644 --- a/src/components/vehicle-attachment-table/main.vue +++ b/src/components/vehicle-attachment-table/main.vue @@ -43,9 +43,23 @@ - - @@ -192,7 +199,9 @@ > row.approvalStatus === 'approved'); if (!rows.length) { this.$message.warning('请选择至少一条审批通过的开票申请'); return; } - await Promise.all(rows.map(row => api.syncKingdee(row.id))); - this.$message.success(`已同步${rows.length}条开票申请`); - this.loadTable(); + this.batchSyncing = true; + try { + await Promise.all(rows.map(row => api.syncKingdee(row.id))); + this.$message.success(`已同步${rows.length}条开票申请`); + await this.loadTable(); + // 附件联带同步提示:同步成功但附件未全部上传时追加 warning,重推即自动补传 + const idSet = new Set(rows.map(row => String(row.id))); + const pendingCount = (this.rows || []).filter(row => idSet.has(String(row.id)) + && ['pending', 'partial'].includes(row.attachmentSyncStatus)).length; + if (pendingCount) { + this.$message({ + type: 'warning', + message: `其中${pendingCount}单附件未全部上传,可重推补传`, + duration: 8000, + }); + } + } finally { + this.batchSyncing = false; + } }, async handleExport() { const data = this.unwrapData(await api.getList(1, 100000, this.query)); @@ -452,6 +484,28 @@ export default { '' ); }, + lockTag(row) { + if (row.kingdeeLockStatus === 'locked') { + return { show: true, type: 'danger', label: '金蝶锁定中', tip: '该单据已被金蝶锁定,不可编辑/作废' }; + } + if (row.kingdeeLockStatus === 'failed') { + if ((row.kingdeeTryCount || 0) >= 5) { + return { + show: true, + type: 'danger', + label: '锁定失败·已达上限', + tip: row.kingdeeLockFailReason || '自动补偿次数已用完,请重推「同步金蝶」立即补锁', + }; + } + return { + show: true, + type: 'warning', + label: '锁定失败·补偿中', + tip: row.kingdeeLockFailReason || '系统将自动补偿重锁', + }; + } + return { show: false }; + }, displayValue(value) { return value === null || value === undefined || value === '' ? '-' : value; }, diff --git a/src/views/payment/invoice-receipt-form.vue b/src/views/payment/invoice-receipt-form.vue index fd54230..ac424da 100644 --- a/src/views/payment/invoice-receipt-form.vue +++ b/src/views/payment/invoice-receipt-form.vue @@ -47,7 +47,7 @@ @@ -56,7 +56,7 @@ @@ -118,7 +118,13 @@ - + + @@ -149,62 +155,13 @@ - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
+ - Array.from({ length }, () => Math.floor(Math.random() * 10)).join(''); - -const formatMockDate = date => { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - return `${year}-${month}-${day}`; -}; - -const customerName = customer => - customer.fullName || customer.shortName || customer.customerCode || `客商${customer.id}`; - -const customerEmail = (customer, index) => { - const prefix = String(customer.customerCode || customer.id || `customer${index + 1}`) - .replace(/[^a-zA-Z0-9_-]/g, '') - .toLowerCase(); - return `finance@${prefix || `customer${index + 1}`}.example.com`; -}; - -const createInvoicePoolMockRows = (customers, counterpartyName, departmentEmail) => { - const rows = []; - const baseId = Date.now(); - customers.forEach((customer, customerIndex) => { - const name = customerName(customer); - ['issuer', 'receiver'].forEach((role, roleIndex) => { - const count = 3 + Math.floor(Math.random() * 5); - for (let index = 0; index < count; index += 1) { - const rowIndex = rows.length; - const invoiceDateValue = new Date(); - invoiceDateValue.setDate(invoiceDateValue.getDate() - Math.floor(Math.random() * 90)); - const invoiceDate = formatMockDate(invoiceDateValue); - const taxRate = mockTaxRates[(customerIndex + roleIndex + index) % mockTaxRates.length]; - const invoiceAmount = Math.floor(10000 + Math.random() * 190000); - const taxAmount = Number( - ((invoiceAmount * Number(taxRate)) / (100 + Number(taxRate))).toFixed(2) - ); - const bankName = mockBankNames[(customerIndex + index) % mockBankNames.length]; - rows.push({ - id: baseId + rowIndex, - invoiceNo: `25${randomDigits(18)}`, - invoiceDate, - invoiceType: mockInvoiceTypes[(customerIndex + index) % mockInvoiceTypes.length], - taxRate, - invoiceAmount, - taxAmount, - receiverName: role === 'receiver' ? name : counterpartyName, - issuerName: role === 'issuer' ? name : counterpartyName, - bankName, - bankAccount: randomDigits(19), - issuingBank: `${bankName}营业部`, - phone: customer.contactPhone || '', - customerEmails: customerEmail(customer, customerIndex), - departmentEmails: departmentEmail || '', - attachmentsJson: '[]', - kingdeeBillNo: `KD-FP-${invoiceDate.replaceAll('-', '')}${randomDigits(5)}`, - kingdeeStatus: 'unsynced', - }); - } - }); - }); - return rows; -}; - const emptyForm = () => ({ id: null, - kingdeeInvoicePoolId: null, invoiceNo: '', invoiceDate: '', invoiceType: '', @@ -346,8 +234,6 @@ const emptyForm = () => ({ bankName: '', bankAccount: '', issuingBank: '', - kingdeeBillNo: '', - kingdeeStatus: 'unsynced', phone: '', customerEmails: [], departmentEmails: [], @@ -363,24 +249,17 @@ const emptyForm = () => ({ export default { name: 'InvoiceReceiptForm', components: { + KingdeeInvoicePoolDialog, VehicleAttachmentTable, }, data() { return { Search, form: emptyForm(), - invoicePoolMockRows: [], - invoicePoolMockLoaded: false, customerEmailOptions: [], departmentEmailOptions: [], invoiceDialog: { visible: false, - loading: false, - keyword: '', - sourceRows: [], - rows: [], - current: null, - page: { current: 1, size: 10, total: 0 }, }, settlementDialog: { visible: false, @@ -426,6 +305,12 @@ export default { this.initialize(); }, methods: { + apiQueryInvoicePool(params) { + return api.queryInvoicePool(params); + }, + handleInvoicePicked(rows) { + this.confirmInvoice(rows[0]); + }, hasPermission(code) { return this.permission?.[code] !== false; }, @@ -483,7 +368,7 @@ export default { }; }, validateInvoice(rule, value, callback) { - if (!value || !this.form.kingdeeInvoicePoolId) { + if (!value) { callback(new Error('请选择金蝶票据池发票')); return; } @@ -509,99 +394,17 @@ export default { } callback(); }, - async openInvoiceDialog() { + openInvoiceDialog() { this.invoiceDialog.visible = true; - this.invoiceDialog.keyword = this.form.invoiceNo || ''; - await this.loadInvoicePool(); - }, - async loadInvoicePool() { - this.invoiceDialog.loading = true; - this.invoiceDialog.page.current = 1; - this.invoiceDialog.current = null; - try { - if (!this.recordId) { - if (!this.invoicePoolMockLoaded) await this.loadInvoicePoolMockRows(); - this.invoiceDialog.sourceRows = this.filterInvoicePoolMockRows( - this.invoiceDialog.keyword - ); - this.refreshInvoicePoolPage(); - return; - } - const data = this.unwrapData(await api.getInvoicePool(this.invoiceDialog.keyword)); - this.invoiceDialog.sourceRows = Array.isArray(data) ? data : data?.records || []; - this.refreshInvoicePoolPage(); - } finally { - this.invoiceDialog.loading = false; - } - }, - async loadInvoicePoolMockRows() { - const pageSize = 500; - const firstData = this.unwrapData(await getCustomerList(1, pageSize, {})); - const firstRows = Array.isArray(firstData) ? firstData : firstData?.records || []; - const total = Number(firstData?.total || firstRows.length); - const pageCount = Math.ceil(total / pageSize); - const remainingPages = await Promise.all( - Array.from({ length: Math.max(pageCount - 1, 0) }, (_, index) => - getCustomerList(index + 2, pageSize, {}) - ) - ); - const customers = [ - ...firstRows, - ...remainingPages.flatMap(response => { - const data = this.unwrapData(response); - return Array.isArray(data) ? data : data?.records || []; - }), - ].filter( - (customer, index, rows) => - rows.findIndex(item => String(item.id) === String(customer.id)) === index - ); - const userInfo = this.$store.getters.userInfo || {}; - const counterpartyName = userInfo.deptName || userInfo.dept_name || '本企业'; - this.invoicePoolMockRows = createInvoicePoolMockRows( - customers, - counterpartyName, - userInfo.email || '' - ); - this.invoicePoolMockLoaded = true; - }, - refreshInvoicePoolPage() { - const page = this.invoiceDialog.page; - const total = this.invoiceDialog.sourceRows.length; - const maxPage = Math.max(Math.ceil(total / page.size), 1); - if (page.current > maxPage) page.current = maxPage; - const start = (page.current - 1) * page.size; - page.total = total; - this.invoiceDialog.current = null; - this.invoiceDialog.rows = this.invoiceDialog.sourceRows.slice(start, start + page.size); - }, - handleInvoicePoolSizeChange() { - this.invoiceDialog.page.current = 1; - this.refreshInvoicePoolPage(); - }, - filterInvoicePoolMockRows(keyword) { - const normalizedKeyword = String(keyword || '') - .trim() - .toLowerCase(); - if (!normalizedKeyword) return this.invoicePoolMockRows.map(item => ({ ...item })); - return this.invoicePoolMockRows - .filter(item => - [item.invoiceNo, item.issuerName, item.receiverName].some(value => - String(value || '') - .toLowerCase() - .includes(normalizedKeyword) - ) - ) - .map(item => ({ ...item })); }, confirmInvoice(row, options = {}) { - const selected = row?.id ? row : this.invoiceDialog.current; + const selected = row; if (!selected) { this.$message.warning('请选择一张金蝶票据池发票'); return; } const preserveUserInput = options.preserveUserInput === true; Object.assign(this.form, { - kingdeeInvoicePoolId: selected.id, invoiceNo: selected.invoiceNo, invoiceDate: selected.invoiceDate, invoiceType: selected.invoiceType, @@ -613,8 +416,6 @@ export default { bankName: selected.bankName, bankAccount: selected.bankAccount, issuingBank: selected.issuingBank, - kingdeeBillNo: selected.kingdeeBillNo || '', - kingdeeStatus: selected.kingdeeStatus || 'unsynced', phone: preserveUserInput ? this.form.phone : selected.phone || '', customerEmails: preserveUserInput ? this.form.customerEmails @@ -725,7 +526,6 @@ export default { payload() { return { id: this.form.id, - kingdeeInvoicePoolId: this.form.kingdeeInvoicePoolId, invoiceNo: this.form.invoiceNo, invoiceDate: this.form.invoiceDate, invoiceType: this.form.invoiceType, @@ -737,8 +537,6 @@ export default { bankName: this.form.bankName, bankAccount: this.form.bankAccount, issuingBank: this.form.issuingBank, - kingdeeBillNo: this.form.kingdeeBillNo, - kingdeeStatus: this.form.kingdeeStatus, phone: this.form.phone, customerEmails: this.form.customerEmails.join(';'), departmentEmails: this.form.departmentEmails.join(';'), @@ -782,13 +580,12 @@ export default { this.$message.warning('请先选择金蝶票据池发票'); return; } - const data = this.recordId - ? this.unwrapData(await api.getInvoicePool(this.form.invoiceNo)) - : this.filterInvoicePoolMockRows(this.form.invoiceNo); - const rows = Array.isArray(data) ? data : data?.records || []; - const invoice = rows.find(item => item.invoiceNo === this.form.invoiceNo); + const data = this.unwrapData( + await api.queryInvoicePool({ current: 1, size: 1, invoiceNo: this.form.invoiceNo }) + ); + const invoice = (data.records || []).find(item => item.invoiceNo === this.form.invoiceNo); if (!invoice) { - this.$message.warning('金蝶票据池中未查询到该发票'); + this.$message.warning('金蝶票池未查询到该发票'); return; } if (!this.confirmInvoice(invoice, { preserveUserInput: true })) return; @@ -851,11 +648,6 @@ export default { .receipt-form-page__dialog-search .el-input { width: 360px; } -.receipt-form-page__dialog-pagination { - display: flex; - justify-content: flex-end; - margin-top: 12px; -} .receipt-form-page :deep(.el-table) { --el-table-border-color: #eff1f7; } diff --git a/src/views/payment/invoice-receipt.vue b/src/views/payment/invoice-receipt.vue index 6aa21c3..02b2ad1 100644 --- a/src/views/payment/invoice-receipt.vue +++ b/src/views/payment/invoice-receipt.vue @@ -40,10 +40,10 @@ />
- - + + 批量同步 ({ deptName: '', deptPath: [], approvalStatus: '', - kingdeeStatus: '', + kingdeePushStatus: '', }); export default { @@ -248,12 +249,13 @@ export default { searchVisible: true, tableSize: 'default', approvalStatusOptions: api.approvalStatusOptions, - kingdeeStatusOptions: api.kingdeeStatusOptions, + kingdeePushStatusOptions: api.kingdeePushStatusOptions, columns: invoiceReceiptTableColumns, visibleColumnKeys: invoiceReceiptTableColumns.map(column => column.prop), rows: [], selection: [], loading: false, + pushLoading: false, page: { current: 1, size: 10, total: 0 }, mkEnabled: false, }; @@ -363,8 +365,13 @@ export default { }, async handleApprove(row) { await this.$confirm('确认审批通过该收票登记?', '提示', { type: 'warning' }); - await api.approve({ id: row.id }); - this.$message.success('审批通过'); + const res = await api.approve({ id: row.id }); + // data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因 + if (res.data.data === false) { + this.$message.warning(res.data.msg); + } else { + this.$message.success(res.data.msg || '审批通过'); + } this.loadTable(); }, async handleReturn(row) { @@ -379,15 +386,29 @@ export default { this.$message.success('作废成功'); this.loadTable(); }, - async handleBatchSync() { + async handlePushKingdee() { + if (this.pushLoading) return; const rows = this.selection.filter(row => row.approvalStatus === 'approved'); if (!rows.length) { this.$message.warning('请选择至少一条审批通过的收票登记'); return; } - await Promise.all(rows.map(row => api.syncKingdee(row.id))); - this.$message.success(`已同步${rows.length}条收票登记`); - this.loadTable(); + await this.$confirm(`确认将选中的 ${rows.length} 条收票登记同步至金蝶?`, '提示', { + type: 'warning', + }); + this.pushLoading = true; + try { + const summary = this.unwrapData(await api.pushKingdee(rows.map(row => row.id))); + await this.loadTable(); + // 附件联带同步提示:推送成功但附件未全部上传时在结果弹窗追加说明,重推即自动补传 + const idSet = new Set(rows.map(row => String(row.id))); + const pendingCount = (this.rows || []).filter(row => idSet.has(String(row.id)) + && ['pending', 'partial'].includes(row.attachmentSyncStatus)).length; + const attachmentTip = pendingCount ? `\n其中${pendingCount}单附件未全部上传,可重推补传` : ''; + this.$alert((summary || '推送完成') + attachmentTip, '推送结果', { confirmButtonText: '知道了' }); + } finally { + this.pushLoading = false; + } }, async handleExport() { const data = this.unwrapData(await api.getList(1, 100000, this.query)); @@ -404,8 +425,9 @@ export default { 当前处理人: item.currentProcessor, 创建人: item.createUserName, 创建时间: item.createTime, - 金蝶单据号: item.kingdeeBillNo, - 金蝶单据状态: item.kingdeeStatusName, + 金蝶单据号: item.kingdeeReceivedInvoiceNo, + 金蝶状态: item.kingdeePushStatusName, + 失败原因: item.kingdeePushMsg, })); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(exportRows), '收票登记'); diff --git a/src/views/payment/payment-application-form.vue b/src/views/payment/payment-application-form.vue index 96e737a..00bfe18 100644 --- a/src/views/payment/payment-application-form.vue +++ b/src/views/payment/payment-application-form.vue @@ -283,8 +283,18 @@ + + - + + +
+
+ 开票发票(本结算单开票申请经金蝶开票回写,文件由金蝶发票云直链提供) +
+ + + + + + + + + + + + + +
- - - - - - - - - - - - - - - - -
- -
- -
+ confirm-text="确认认领" + multiple + :initial-invoice-no="String(invoiceClaimNo || '').trim()" + :query-api="invoicePoolQueryApi" + @confirm="handleInvoiceClaimConfirm" + /> total + Number(item.matchedAmount || 0), 0), - 0 - ); - const existingNumbers = new Set(this.invoices.map(item => String(item.invoiceNo))); - const mockRows = numbers - .map((invoiceNo, index) => { - const matchedAmount = Math.min( - preferredMatchedAmounts[index], - availableAmounts[index], - remainingAmount - ); - remainingAmount = Number((remainingAmount - matchedAmount).toFixed(2)); - return { - invoiceNo, - invoiceDate: this.$dayjs().subtract(index, 'day').format('YYYY-MM-DD'), - invoiceType: index === 1 ? '增值税普通发票' : '增值税专用发票', - taxRate: [3, 6, 9][index], - invoiceAmount: invoiceAmounts[index], - availableInvoiceAmount: availableAmounts[index], - matchedAmount, - attachmentJson: '', - mockData: true, - }; - }) - .filter(item => !existingNumbers.has(item.invoiceNo)); - if (!mockRows.length) { - this.$message.warning('当前发票号码的Mock数据已认领'); - return; - } - this.invoices = [...this.invoices, ...mockRows]; - this.$message.success(`已生成${mockRows.length}条Mock发票数据`); - } finally { - this.invoiceClaimLoading = false; - } - }, openInvoiceClaimDialog() { - const stamp = Date.now(); - this.invoiceClaimDialog.loading = true; - this.invoiceClaimDialog.rows = Array.from({ length: 80 }, (_, index) => { - const amount = Math.floor(1000 + Math.random() * 49000); - return { - invoiceNo: `MOCK${stamp}${String(index + 1).padStart(3, '0')}`.slice(0, 32), - invoiceDate: this.$dayjs() - .subtract(Math.floor(Math.random() * 90), 'day') - .format('YYYY-MM-DD'), - invoiceType: index % 2 ? '增值税普通发票' : '增值税专用发票', - issuerName: this.form.payerName || 'Mock开票单位', - receiverName: this.form.payeeName || 'Mock受票单位', - invoiceAmount: amount, - availableInvoiceAmount: amount, - matchedAmount: amount, - taxRate: [3, 6, 9, 13][index % 4], - attachmentJson: '', - mockData: true, - }; - }); - this.invoiceClaimDialog.selected = []; - this.invoiceClaimDialog.page.current = 1; - this.invoiceClaimDialog.page.total = 80; this.invoiceClaimDialog.visible = true; - this.invoiceClaimDialog.loading = false; + }, + invoicePoolQueryApi(params) { + return queryInvoicePool(params); + }, + async handleInvoiceClaimConfirm(rows) { + // 统一票池行附件字段为复数 attachmentsJson,认领请求契约是单数 attachmentJson,中转处归一 + this.invoiceClaimDialog.selected = rows.map(row => ({ + ...row, + attachmentJson: row.attachmentJson || row.attachmentsJson, + })); + await this.confirmInvoiceClaims(); }, async confirmInvoiceClaims() { if (!this.invoiceClaimDialog.selected.length) { @@ -2408,7 +2356,11 @@ export default { if (Number(this.form.settlementAmount || 0) > 0) { remaining = Math.max(0, remaining - matchedAmount); } - return { ...row, matchedAmount }; + return { + ...row, + invoiceType: row.invoiceTypeName || row.invoiceType || '', + matchedAmount, + }; }) .filter(row => row.matchedAmount > 0); if (!invoices.length) { @@ -2494,16 +2446,64 @@ export default { const attachment = this.invoiceAttachment(row); return attachment.originalName || attachment.name || `${row.invoiceNo || '发票'}.pdf`; }, - viewInvoiceAttachment(row) { + async viewInvoiceAttachment(row) { + const attachment = this.invoiceAttachment(row); + // 金蝶侧附件(path 型)经后端代理 A801-2 预览,OSS 直链维持 window.open + if (isKingdeePathAttachment(attachment)) { + if (this.kingdeeAttachmentBusy) return; + this.kingdeeAttachmentBusy = true; + try { + await viewKingdeeInvoiceFile(row.invoiceNo, this.invoiceAttachmentName(row)); + } finally { + this.kingdeeAttachmentBusy = false; + } + return; + } const url = this.invoiceAttachmentUrl(row); - if (!url) return this.$message.warning('当前Mock发票暂无可查看附件'); + if (!url) return this.$message.warning('当前发票暂无可查看附件'); window.open(url, '_blank'); }, - downloadInvoiceAttachment(row) { + async downloadInvoiceAttachment(row) { + const attachment = this.invoiceAttachment(row); + // 金蝶侧附件(path 型)经后端代理 A801-2 下载,OSS 直链维持前端直连下载 + if (isKingdeePathAttachment(attachment)) { + if (this.kingdeeAttachmentBusy) return; + this.kingdeeAttachmentBusy = true; + try { + await downloadKingdeeInvoiceFile(row.invoiceNo, this.invoiceAttachmentName(row)); + } finally { + this.kingdeeAttachmentBusy = false; + } + return; + } const url = this.invoiceAttachmentUrl(row); - if (!url) return this.$message.warning('当前Mock发票暂无可下载附件'); + if (!url) return this.$message.warning('当前发票暂无可下载附件'); downloadFileByUrl(url, this.invoiceAttachmentName(row)); }, + // 开票发票预览地址:数电PDF优先(OFD浏览器无法预览),退预览图、板式文件 + issuedInvoicePreviewUrl(row) { + return row.invoicePdfFileUrl || row.invoiceImageUrl || row.invoiceFileUrl || ''; + }, + // 开票发票下载地址:数电PDF优先,退板式文件、数电XML + issuedInvoiceDownloadUrl(row) { + return row.invoicePdfFileUrl || row.invoiceFileUrl || row.invoiceXmlFileUrl || ''; + }, + // 下载文件名按直链扩展名推断(数电票 .pdf / 板式文件 .ofd|.pdf),缺省 .pdf + issuedInvoiceFileName(row, url) { + const match = String(url || '').split('?')[0].match(/\.([a-zA-Z0-9]+)$/); + const extension = match ? `.${match[1].toLowerCase()}` : '.pdf'; + return `${row.invoiceNo || '发票'}${extension}`; + }, + viewIssuedInvoice(row) { + const url = this.issuedInvoicePreviewUrl(row); + if (!url) return this.$message.warning('当前发票暂无可查看文件,可稍后重试开票结果同步'); + window.open(url, '_blank'); + }, + downloadIssuedInvoice(row) { + const url = this.issuedInvoiceDownloadUrl(row); + if (!url) return this.$message.warning('当前发票暂无可下载文件,可稍后重试开票结果同步'); + downloadFileByUrl(url, this.issuedInvoiceFileName(row, url)); + }, buildSavePayload() { return { id: this.form.id, @@ -2993,6 +2993,14 @@ export default { .formal-editor__invoice-claim :deep(.el-input) { width: 360px; } +.formal-editor__issued-invoices { + margin-top: 16px; +} +.formal-editor__issued-title { + margin-bottom: 8px; + color: var(--el-text-color-secondary); + font-size: 13px; +} .formal-editor__negative-amount { color: var(--el-color-danger); } diff --git a/src/views/settlement/components/formal-settlement-table-panel.vue b/src/views/settlement/components/formal-settlement-table-panel.vue index eb830f3..8d0d028 100644 --- a/src/views/settlement/components/formal-settlement-table-panel.vue +++ b/src/views/settlement/components/formal-settlement-table-panel.vue @@ -21,6 +21,7 @@ type="primary" plain :disabled="selection.length !== 1" + :loading="syncLoading" @click="$emit('sync')" >同步金蝶
@@ -91,6 +92,12 @@ {{ kingdeeSyncName(row.kingdeeSyncStatus) }} + {{ displayValue(row[column.prop]) }}
@@ -137,6 +144,7 @@ export default { rows: { type: Array, default: () => [] }, columns: { type: Array, default: () => [] }, loading: { type: Boolean, default: false }, + syncLoading: { type: Boolean, default: false }, page: { type: Object, required: true }, hasPermission: { type: Function, required: true }, mkEnabled: { type: Boolean, default: false }, @@ -267,6 +275,28 @@ export default { kingdeeSyncName(value) { return kingdeeSyncStatusOptions.find(item => item.value === value)?.label || value || '-'; }, + lockTag(row) { + if (row.kingdeeLockStatus === 'locked') { + return { show: true, type: 'danger', label: '金蝶锁定中', tip: '该单据已被金蝶锁定,不可编辑/作废' }; + } + if (row.kingdeeLockStatus === 'failed') { + if ((row.kingdeeTryCount || 0) >= 5) { + return { + show: true, + type: 'danger', + label: '锁定失败·已达上限', + tip: row.kingdeeLockFailReason || '自动补偿次数已用完,请重推「同步金蝶」立即补锁', + }; + } + return { + show: true, + type: 'warning', + label: '锁定失败·补偿中', + tip: row.kingdeeLockFailReason || '系统将自动补偿重锁', + }; + } + return { show: false }; + }, displayValue(value) { return value === null || value === undefined || value === '' ? '-' : value; }, diff --git a/src/views/settlement/components/pre-settlement-editor.vue b/src/views/settlement/components/pre-settlement-editor.vue index 0ff982d..7a63786 100644 --- a/src/views/settlement/components/pre-settlement-editor.vue +++ b/src/views/settlement/components/pre-settlement-editor.vue @@ -42,7 +42,7 @@ format="YYYY-MM-DD" :disabled="!editable || form.currency === 'RMB'" placeholder="请选择" - @change="recalculateLocalAmount" + @change="handleExchangeRateDateChange" /> String(item.id) === String(row.id)); + if (freshRow && ['pending', 'partial'].includes(freshRow.attachmentSyncStatus)) { + this.$message({ + type: 'warning', + message: '该单附件未全部上传,可重推补传', + duration: 8000, + }); + } + } finally { + this.syncLoading = false; + } }, async openPaymentDialog() { if (!this.selection.length) return this.$message.warning('请至少选择一条正式结算单'); diff --git a/src/views/vehicle/customer-archive.vue b/src/views/vehicle/customer-archive.vue index daa7b9e..facbd05 100644 --- a/src/views/vehicle/customer-archive.vue +++ b/src/views/vehicle/customer-archive.vue @@ -109,6 +109,12 @@ {{ row.status === 1 ? '启用' : '停用' }} +