diff --git a/src/api/payment/invoiceReceipt.js b/src/api/payment/invoiceReceipt.js index 8db2dfb..5ae7cbc 100644 --- a/src/api/payment/invoiceReceipt.js +++ b/src/api/payment/invoiceReceipt.js @@ -38,6 +38,15 @@ export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'p export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data }); export const pushKingdee = ids => request({ url: `${baseUrl}/push-kingdee`, method: 'post', timeout: 30000, data: ids }); +// 先票后款:发票明细查询(收票登记明细区块按票号带出发票明细行) +export const invoiceDetailQuery = invoiceNo => + request({ url: `${baseUrl}/invoice-detail-query`, method: 'get', params: { invoiceNo } }); +// 先票后款:结算单认领收票单候选分页(审批通过且未被有效应付结算单认领) +export const claimCandidates = params => + request({ url: `${baseUrl}/claim-candidates`, method: 'get', params }); +// 先票后款:结算单已认领收票登记列表(发票登记对话框回显已认领票) +export const claimedReceipts = formalSettlementId => + request({ url: `${baseUrl}/claimed-list`, method: 'get', params: { formalSettlementId } }); export const approvalStatusOptions = [ { label: '草稿', value: 'draft' }, diff --git a/src/api/payment/paymentApplication.js b/src/api/payment/paymentApplication.js index e464552..4a64416 100644 --- a/src/api/payment/paymentApplication.js +++ b/src/api/payment/paymentApplication.js @@ -27,13 +27,13 @@ export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'p 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', timeout: 30000, params: { id } }); + request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: APPROVE_TIMEOUT, params: { id } }); // 批量同步逐单单据级事务串行推送,耗时随单量线性增长 export const syncKingdeeBatch = ids => - request({ url: `${baseUrl}/sync-kingdee-batch`, method: 'post', timeout: 30000, data: ids }); + request({ url: `${baseUrl}/sync-kingdee-batch`, method: 'post', timeout: APPROVE_TIMEOUT, data: ids }); // 回写任务按 paymentNo 分批远程查询 A103-2/A103-4,同样可能超过全局超时 export const syncKingdeeResult = () => - request({ url: `${baseUrl}/sync-kingdee-result`, method: 'post', timeout: 30000 }); + request({ url: `${baseUrl}/sync-kingdee-result`, method: 'post', timeout: APPROVE_TIMEOUT }); export const paymentTypeOptions = [ { label: '项目预付', value: 'project_advance' }, diff --git a/src/api/settlement/formalSettlement.js b/src/api/settlement/formalSettlement.js index 2d980d4..8769744 100644 --- a/src/api/settlement/formalSettlement.js +++ b/src/api/settlement/formalSettlement.js @@ -58,5 +58,21 @@ export const applyPayments = data => request({ url: `${baseUrl}/apply-payments`, method: 'post', data }); export const claimInvoices = data => request({ url: `${baseUrl}/claim-invoices`, method: 'post', data }); +// 先票后款:发票登记确认推送(认领收票单逐单 A302-6 指定应付),涉及多次金蝶远程调用,覆盖全局超时 +export const invoiceAssignKingdee = id => + request({ + url: `${baseUrl}/invoice-assign-kingdee`, + method: 'post', + timeout: 30000, + params: { id }, + }); +// 先票后款:查看页重推认领收票单(收票单A303-2+指定应付A302-6 一步式),涉及多次金蝶远程调用,覆盖全局超时 +export const repushClaimedInvoice = (id, invoiceNo, matchedAmount) => + request({ + url: `${baseUrl}/repush-claimed-invoice`, + method: 'post', + timeout: 30000, + params: { id, invoiceNo, matchedAmount }, + }); export const queryInvoicePool = params => request({ url: `${baseUrl}/invoice-pool`, method: 'get', params }); diff --git a/src/api/transportCapacity/driver.js b/src/api/transportCapacity/driver.js index bbab4f1..d95d82d 100644 --- a/src/api/transportCapacity/driver.js +++ b/src/api/transportCapacity/driver.js @@ -91,3 +91,14 @@ export const recognizeBaiduOcr = (imageUrl, type, side = '') => { }, }); }; + +export const recognizeQualificationCertificateOcr = imageUrl => { + return request({ + url: '/blade-transport/baidu-ocr/qualification-certificate', + method: 'post', + timeout: 60000, + params: { + imageUrl, + }, + }); +}; diff --git a/src/api/vehicle/customer-archive.js b/src/api/vehicle/customer-archive.js index 150ec85..6c9107d 100644 --- a/src/api/vehicle/customer-archive.js +++ b/src/api/vehicle/customer-archive.js @@ -152,6 +152,34 @@ export const getScoreTemplate = quantificationId => { }); }; +/** 线下评分 Excel 模板下载 */ +export const downloadScoreExcelTemplate = quantificationId => { + return request({ + url: '/blade-transport/customer-archive/score-excel-template', + method: 'get', + responseType: 'blob', + params: { + quantificationId, + }, + }); +}; + +/** 线下评分 Excel 导入(成功返回评分明细 JSON;失败返回失败明细 Excel 流) */ +export const importScoreExcel = (quantificationId, file) => { + const formData = new FormData(); + formData.append('file', file); + return request({ + url: '/blade-transport/customer-archive/score-import', + method: 'post', + data: formData, + responseType: 'blob', + timeout: 60000, + params: { + quantificationId, + }, + }); +}; + export const queryKingdeeCustomer = creditCode => { return request({ url: '/blade-transport/bfi/customer/query-by-credit-code', diff --git a/src/option/payment/paymentApplication.js b/src/option/payment/paymentApplication.js index 9a74a22..6e5559a 100644 --- a/src/option/payment/paymentApplication.js +++ b/src/option/payment/paymentApplication.js @@ -170,6 +170,7 @@ export const paymentApplicationOption = ({ { label: '当前处理人', prop: 'currentProcessor', minWidth: 120, formatter: textFormatter }, { label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 130, formatter: textFormatter }, { label: '金蝶单据状态', prop: 'kingdeeStatusName', slot: true, minWidth: 130 }, + { label: '已指定应付', prop: 'kingdeeAssignStatus', slot: true, minWidth: 120 }, { label: '金蝶锁状态', prop: 'kingdeeLockStatus', slot: true, minWidth: 150 }, { label: '附件同步', prop: 'attachmentSyncStatusName', minWidth: 100, formatter: textFormatter }, ], diff --git a/src/option/settlement/formalSettlement.js b/src/option/settlement/formalSettlement.js index 6a0e213..6e335c0 100644 --- a/src/option/settlement/formalSettlement.js +++ b/src/option/settlement/formalSettlement.js @@ -89,24 +89,28 @@ const COLUMN_EXTRA = { }, kingdeeLockStatus: { slot: true }, kingdeeWriteOffStatus: { slot: true }, + kingdeeAssignStatus: { slot: true }, createTime: { width: 180 }, }; -const buildTableColumns = () => - formalSettlementTableColumns.map(column => { - const extra = { ...(COLUMN_EXTRA[column.prop] || {}) }; - if (column.money) { - extra.formatter = moneyFormatter; - extra.align = 'right'; - } - // link / status 标记的列统一走单元格插槽 - if (column.link || column.status) extra.slot = true; - // 插槽列不挂兜底 formatter:avue 中 formatter 会覆盖单元格插槽渲染 - if (!extra.slot && !extra.formatter) extra.formatter = textFormatter; - return { ...column, ...extra }; - }); +// 已指定应付仅付款结算单(先票后款 A302-6)有,收款结算单不展示 +const buildTableColumns = settlementType => + formalSettlementTableColumns + .filter(column => !(settlementType === 'receivable' && column.prop === 'kingdeeAssignStatus')) + .map(column => { + const extra = { ...(COLUMN_EXTRA[column.prop] || {}) }; + if (column.money) { + extra.formatter = moneyFormatter; + extra.align = 'right'; + } + // link / status 标记的列统一走单元格插槽 + if (column.link || column.status) extra.slot = true; + // 插槽列不挂兜底 formatter:avue 中 formatter 会覆盖单元格插槽渲染 + if (!extra.slot && !extra.formatter) extra.formatter = textFormatter; + return { ...column, ...extra }; + }); -export const formalSettlementOption = () => ({ +export const formalSettlementOption = (settlementType = '') => ({ height: 'auto', calcHeight: 32, tip: false, @@ -114,7 +118,7 @@ export const formalSettlementOption = () => ({ emptyBtnText: '重置', searchShow: true, searchIcon: true, - searchLabelWidth: 88, + searchLabelWidth: 100, // 查询/重置紧跟首行 4 个条件(4×5 + 菜单 4 = 24),不再独占下一整行 searchMenuSpan: 4, searchIndex: 4, @@ -168,7 +172,7 @@ export const formalSettlementOption = () => ({ type: 'daterange', }), // ---------- 表格列(部分兼搜索项) ---------- - ...buildTableColumns(), + ...buildTableColumns(settlementType), ], }); diff --git a/src/option/settlement/formalSettlementTable.js b/src/option/settlement/formalSettlementTable.js index 117ae86..9360d50 100644 --- a/src/option/settlement/formalSettlementTable.js +++ b/src/option/settlement/formalSettlementTable.js @@ -27,6 +27,7 @@ export const formalSettlementTableColumns = [ { label: '金蝶单据状态', prop: 'kingdeeSyncStatus', minWidth: 135 }, { label: '金蝶冲销状态', prop: 'kingdeeWriteOffStatus', minWidth: 135 }, { label: '金蝶锁状态', prop: 'kingdeeLockStatus', minWidth: 150 }, + { label: '已指定应付', prop: 'kingdeeAssignStatus', minWidth: 120 }, { label: '附件同步', prop: 'attachmentSyncStatusName', minWidth: 100 }, { label: '创建人', prop: 'createUserName', minWidth: 110 }, { label: '创建时间', prop: 'createTime', minWidth: 170 }, diff --git a/src/option/settlement/preSettlementForm.js b/src/option/settlement/preSettlementForm.js index 64754ba..f5d00ec 100644 --- a/src/option/settlement/preSettlementForm.js +++ b/src/option/settlement/preSettlementForm.js @@ -8,8 +8,20 @@ export const preSettlementFormFields = [ { label: '合同名称', prop: 'contractId', type: 'contract', required: true }, { label: '合同编号', prop: 'contractNo', readonly: true }, { label: '项目名称', prop: 'projectName', readonly: true }, - { label: '收款方', prop: 'payeeName', readonly: true }, - { label: '付款方', prop: 'payerName', readonly: true }, + { + label: '收款方', + prop: 'payeeName', + type: 'select', + optionsKey: 'payeeOptions', + required: true, + }, + { + label: '付款方', + prop: 'payerName', + type: 'select', + optionsKey: 'payerOptions', + required: true, + }, { label: '结算类型', prop: 'settlementType', readonly: true }, { label: '结算金额', prop: 'settlementAmount', readonly: true, money: true }, { label: '业务模式', prop: 'businessModel', type: 'select', optionsKey: 'businessModelOptions' }, @@ -24,6 +36,8 @@ export const preSettlementFormFields = [ export const preSettlementFormRules = { contractId: [{ required: true, message: '请选择合同名称', trigger: 'change' }], + payeeName: [{ required: true, message: '请选择收款方', trigger: 'change' }], + payerName: [{ required: true, message: '请选择付款方', trigger: 'change' }], exchangeRateDate: [ { validator: (_rule, value, callback, source) => { diff --git a/src/router/views/index.js b/src/router/views/index.js index e90da64..02c3516 100644 --- a/src/router/views/index.js +++ b/src/router/views/index.js @@ -129,7 +129,39 @@ export default [ name: '新增编辑结算单', meta: { keepAlive: false, - // 付款/收款结算单共用表单页,具体高亮菜单由页面按 settlementType 回写 + // 付款结算单表单入口,高亮付款结算单菜单 + activeMenu: '/settlement/payment-settlement', + }, + component: loadView(() => import('@/views/settlement/formal-settlement-form.vue')), + }, + ], + }, + { + // 收款结算单表单入口:与付款结算单共用表单组件,仅高亮菜单不同 + path: '/settlement/receipt-settlement/form', + component: Layout, + children: [ + { + path: '', + name: '新增编辑收款结算单', + meta: { + keepAlive: false, + activeMenu: '/settlement/receipt-settlement', + }, + component: loadView(() => import('@/views/settlement/formal-settlement-form.vue')), + }, + ], + }, + { + // 付款结算单表单入口:与 settlementListPath('payable') + '/form' 对应 + path: '/settlement/payment-settlement/form', + component: Layout, + children: [ + { + path: '', + name: '新增编辑付款结算单', + meta: { + keepAlive: false, activeMenu: '/settlement/payment-settlement', }, component: loadView(() => import('@/views/settlement/formal-settlement-form.vue')), diff --git a/src/views/business/contract-manage.vue b/src/views/business/contract-manage.vue index a21f689..4b56676 100644 --- a/src/views/business/contract-manage.vue +++ b/src/views/business/contract-manage.vue @@ -3084,7 +3084,7 @@ export default { const message = error?.response?.data?.msg || error?.message || - '部门合同前缀未配置,无法自动生成,请手动输入合同编号'; + '业务部门配置中未维护该平台公司与业务部门的合同编码前缀,无法自动生成,请先在业务部门配置中维护或手动输入合同编号'; this.$message.warning(message); }); }, diff --git a/src/views/business/project-apply.vue b/src/views/business/project-apply.vue index 2dfa40b..ced8d78 100644 --- a/src/views/business/project-apply.vue +++ b/src/views/business/project-apply.vue @@ -1082,6 +1082,7 @@ import { defineAsyncComponent } from 'vue'; import * as api from '@/api/business/project-apply'; import { getList as getContractManageList } from '@/api/business/contract-manage'; import { + getRisk, loadHcsImmediateEnabled, repush as hcsRepush, repushBatch as hcsRepushBatch, diff --git a/src/views/payment/invoice-application-form.vue b/src/views/payment/invoice-application-form.vue index 9e91a2c..b4897ea 100644 --- a/src/views/payment/invoice-application-form.vue +++ b/src/views/payment/invoice-application-form.vue @@ -494,6 +494,9 @@ import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.v import { getMkPublicDetail } from '@/api/mk-process'; import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval'; +// 提交成功后带标记返回,列表页(keep-alive)据此重新拉取 +const invoiceApplicationListRefreshKey = 'invoice-application-list-refresh'; + let localId = 0; const emptyLine = () => ({ localId: ++localId, @@ -1227,6 +1230,7 @@ export default { mkEnabled: await loadMkEnabled().catch(() => false), }); this.$message.success('提交成功'); + sessionStorage.setItem(invoiceApplicationListRefreshKey, '1'); this.goBack(); }, async syncReferenceData() { diff --git a/src/views/payment/invoice-application.vue b/src/views/payment/invoice-application.vue index 27075a7..39c40a6 100644 --- a/src/views/payment/invoice-application.vue +++ b/src/views/payment/invoice-application.vue @@ -250,6 +250,11 @@ import organizationSearch from '@/mixins/organization-search'; import { getMKApprovalUrl } from '@/api/system/business-process'; import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval'; import { kingdeeStatusType } from '@/utils/kingdee'; +import { settlementListPath } from '@/utils/settlement-menu'; + +// 表单提交成功后带标记返回,keep-alive 缓存的列表页据此重新拉取 +const invoiceApplicationListRefreshKey = 'invoice-application-list-refresh'; +const invoiceApplicationListRoute = '/payment/invoice-application'; const emptyQuery = () => ({ applicationNo: '', @@ -308,7 +313,28 @@ export default { this.mkEnabled = false; }); }, + activated() { + this.consumeListRefreshFlag(); + }, + watch: { + $route(to, from) { + if ( + to.path === invoiceApplicationListRoute && + from?.path?.includes('/invoice-application/form') + ) { + this.consumeListRefreshFlag(); + } + }, + }, methods: { + /** 表单提交成功后带标记返回时,重新拉取列表 */ + consumeListRefreshFlag() { + if (!sessionStorage.getItem(invoiceApplicationListRefreshKey)) { + return; + } + sessionStorage.removeItem(invoiceApplicationListRefreshKey); + this.loadTable(); + }, hasPermission(code) { return this.permission?.[code] !== false; }, @@ -372,12 +398,18 @@ export default { item => String(item.formalSettlementNo || '').trim() === normalizedNo ); if (!row?.id) { - this.$message.warning('未找到对应的付款结算单'); + this.$message.warning('未找到对应的结算单'); return; } + // 开票申请关联的多为收款结算单,按单据类型路由到对应表单入口,避免误落付款结算单 this.$router.push({ - path: '/settlement/formal-settlement/form', - query: { mode: 'view', id: row.id, name: '查看正式结算' }, + path: settlementListPath(row.settlementType) + '/form', + query: { + mode: 'view', + id: row.id, + settlementType: row.settlementType, + name: '查看正式结算', + }, }); } catch (error) { this.$message.error('正式结算单详情加载失败'); diff --git a/src/views/payment/invoice-receipt-form.vue b/src/views/payment/invoice-receipt-form.vue index b798e98..50cbfb8 100644 --- a/src/views/payment/invoice-receipt-form.vue +++ b/src/views/payment/invoice-receipt-form.vue @@ -53,15 +53,6 @@ - - - - - - +
- + @@ -132,11 +119,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + @@ -217,69 +225,46 @@ @confirm="handleInvoicePicked" /> - - - - - - - - - - - - - - - - - + + + + + + + + + + 查询 + 重置 + + + + + + + - + @@ -288,11 +273,15 @@ import { Search } from '@element-plus/icons-vue'; import { mapGetters } from 'vuex'; import * as api from '@/api/payment/invoiceReceipt'; +import { getList as getContractList } from '@/api/business/contract-manage'; import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/main.vue'; import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue'; import { getMkPublicDetail } from '@/api/mk-process'; import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval'; +// 提交成功后带标记返回,列表页(keep-alive)据此重新拉取 +const invoiceReceiptListRefreshKey = 'invoice-receipt-list-refresh'; + const emptyForm = () => ({ id: null, invoiceNo: '', @@ -310,6 +299,9 @@ const emptyForm = () => ({ customerEmails: [], departmentEmails: [], projectName: '', + contractNo: '', + contractName: '', + deptId: null, deptName: '', payerName: '', payeeName: '', @@ -330,26 +322,27 @@ export default { form: emptyForm(), customerEmailOptions: [], departmentEmailOptions: [], + invoiceItems: [], + contractDialog: { + visible: false, + loading: false, + rows: [], + page: { current: 1, size: 10, total: 0 }, + query: { contractNo: '', contractName: '' }, + }, invoiceDialog: { visible: false, }, - settlementDialog: { - visible: false, - loading: false, - keyword: '', - rows: [], - selection: [], - }, rules: { invoiceNo: [{ required: true, validator: this.validateInvoice, trigger: 'change' }], - settlementIds: [{ required: true, validator: this.validateSettlements, trigger: 'change' }], + contractNo: [{ required: true, validator: this.validateContract, trigger: 'change' }], customerEmails: [{ validator: this.validateEmails, trigger: 'change' }], departmentEmails: [{ validator: this.validateEmails, trigger: 'change' }], }, }; }, computed: { - ...mapGetters(['permission']), + ...mapGetters(['permission', 'userInfo']), recordId() { return this.$route.query.id || ''; }, @@ -363,15 +356,6 @@ export default { const code = this.recordId ? 'invoice_receipt_edit' : 'invoice_receipt_add'; return this.hasPermission(code); }, - settlementLabel() { - return this.form.settlements - .map(item => item.formalSettlementNo) - .filter(Boolean) - .join(','); - }, - settlementIds() { - return this.form.settlements.map(item => item.formalSettlementId || item.settlementId); - }, }, created() { this.initialize(); @@ -419,14 +403,13 @@ export default { } const data = this.unwrapData(await api.getDetail(this.recordId)); this.applyDetail(data); - if (this.settlementIds.length) await this.loadReferenceInformation(); }, async loadPublicDetail() { if (!this.recordId) return; const data = this.unwrapData(await getMkPublicDetail('invoice-receipt', this.recordId)); this.applyDetail(data); }, - applyDetail(data) { + async applyDetail(data) { this.form = { ...emptyForm(), ...data, @@ -438,6 +421,7 @@ export default { })), attachments: this.parse(data.attachmentsJson), }; + await this.loadInvoiceDetail(); }, validateInvoice(rule, value, callback) { if (!value) { @@ -446,9 +430,9 @@ export default { } callback(); }, - validateSettlements(rule, value, callback) { - if (!this.form.settlements.length) { - callback(new Error('请选择应付正式结算单')); + validateContract(rule, value, callback) { + if (!this.form.contractNo) { + callback(new Error('请选择所属合同')); return; } callback(); @@ -469,7 +453,7 @@ export default { openInvoiceDialog() { this.invoiceDialog.visible = true; }, - confirmInvoice(row, options = {}) { + async confirmInvoice(row, options = {}) { const selected = row; if (!selected) { this.$message.warning('请选择一张金蝶票据池发票'); @@ -504,92 +488,59 @@ export default { ...new Set([...this.departmentEmailOptions, ...this.form.departmentEmails]), ]; this.invoiceDialog.visible = false; + await this.loadInvoiceDetail(); this.$refs.formRef?.validateField('invoiceNo').catch(() => {}); return true; }, - async openSettlementDialog() { - this.settlementDialog.visible = true; - this.settlementDialog.selection = []; - await this.loadSettlementCandidates(); - await this.$nextTick(); - const selectedIds = new Set(this.settlementIds.map(id => String(id))); - this.settlementDialog.rows.forEach(row => { - if (selectedIds.has(String(row.id))) { - this.$refs.settlementTable?.toggleRowSelection(row, true); - } - }); - }, - async loadSettlementCandidates() { - this.settlementDialog.loading = true; + // 发票明细区块:按票号带出金蝶发票明细行(A801-1 镜像,推送 A303-2 分录同源) + async loadInvoiceDetail() { + if (!this.form.invoiceNo) { + this.invoiceItems = []; + return; + } try { - this.settlementDialog.rows = - this.unwrapData( - await api.getSettlementCandidates(this.settlementDialog.keyword, this.form.id) - ) || []; + this.invoiceItems = this.unwrapData(await api.invoiceDetailQuery(this.form.invoiceNo)) || []; + } catch { + this.invoiceItems = []; + } + }, + // 所属合同=承运商合同:选择后带出承办部门(结算组织=承办部门对应平台公司金蝶编码,后端派生) + openContractDialog() { + this.contractDialog.visible = true; + this.searchContracts(); + }, + searchContracts() { + this.contractDialog.page.current = 1; + this.loadContracts(); + }, + resetContractQuery() { + this.contractDialog.query = { contractNo: '', contractName: '' }; + this.searchContracts(); + }, + async loadContracts() { + this.contractDialog.loading = true; + try { + const res = await getContractList(this.contractDialog.page.current, this.contractDialog.page.size, { + ...this.contractDialog.query, + contractCategory: '承运商合同', + approvalStatuses: 'approved,change_approved', + }); + const data = this.unwrapData(res) || {}; + this.contractDialog.rows = data.records || []; + this.contractDialog.page.total = Number(data.total || 0); } finally { - this.settlementDialog.loading = false; + this.contractDialog.loading = false; } }, - async confirmSettlements() { - const rows = this.settlementDialog.selection; - if (!rows.length) { - this.$message.warning('请至少选择一张应付正式结算单'); - return; - } - const first = rows[0]; - const incompatible = rows.some( - item => - String(item.projectId) !== String(first.projectId) || - String(item.deptId) !== String(first.deptId) || - item.payerName !== first.payerName || - item.payeeName !== first.payeeName - ); - if (incompatible) { - this.$message.warning('关联结算单必须属于同一项目、组织及收付款方'); - return; - } - const previousAmount = new Map( - this.form.settlements.map(item => [ - String(item.formalSettlementId || item.settlementId), - item.allocatedInvoiceAmount, - ]) - ); - this.form.settlements = rows.map(item => ({ - ...item, - formalSettlementId: item.id, - settlementId: item.id, - allocatedInvoiceAmount: previousAmount.has(String(item.id)) - ? Number(previousAmount.get(String(item.id)) || 0) - : rows.length === 1 - ? Number(item.settlementAmount || 0) - : 0, - })); - this.form.projectName = first.projectName; - this.form.deptName = first.deptName; - this.form.payerName = first.payerName; - this.form.payeeName = first.payeeName; - this.settlementDialog.visible = false; - await this.loadReferenceInformation(); - this.$refs.formRef?.validateField('settlementIds').catch(() => {}); - }, - selectSettlement(row) { - this.$refs.settlementTable?.toggleRowSelection(row); - }, - async loadReferenceInformation() { - const data = this.unwrapData(await api.getReferenceInformation(this.settlementIds.join(','))); - this.customerEmailOptions = data.customerEmails || []; - this.departmentEmailOptions = [ - ...new Set([...this.departmentEmailOptions, ...(data.departmentEmails || [])]), - ]; - if (!this.form.customerEmails.length && this.customerEmailOptions.length) { - this.form.customerEmails = [this.customerEmailOptions[0]]; - } - }, - remainingAmount(row) { - return Math.max( - Number(row.settlementAmount || 0) - Number(row.receivedInvoiceAmount || 0), - 0 - ); + pickContract(row) { + if (!row) return; + Object.assign(this.form, { + contractNo: row.contractNo, + contractName: row.contractName, + deptName: row.organizationName, + }); + this.contractDialog.visible = false; + this.$refs.formRef?.validateField('contractNo').catch(() => {}); }, openSettlementDetail(row) { const id = row.formalSettlementId || row.settlementId || row.id; @@ -602,15 +553,6 @@ export default { validateBusiness() { const invoiceAmount = Number(this.form.invoiceAmount || 0); if (invoiceAmount <= 0) throw new Error('开票金额必须大于0'); - for (const row of this.form.settlements) { - const allocated = Number(row.allocatedInvoiceAmount); - if (!Number.isFinite(allocated) || allocated < 0) { - throw new Error('分摊发票金额必须大于或等于0'); - } - if (allocated - this.remainingAmount(row) > 0.001) { - throw new Error(`结算单${row.formalSettlementNo}的累计收票金额不能超过结算总金额`); - } - } }, payload() { return { @@ -631,10 +573,8 @@ export default { departmentEmails: this.form.departmentEmails.join(';'), attachmentsJson: JSON.stringify(this.form.attachments || []), remark: this.form.remark, - settlements: this.form.settlements.map(item => ({ - settlementId: item.formalSettlementId || item.settlementId, - allocatedInvoiceAmount: item.allocatedInvoiceAmount, - })), + // 所属合同仅传编号,合同名称/承办部门由后端按编号带出(结算组织=合同承办部门) + contractNo: this.form.contractNo, }; }, async saveDraft() { @@ -663,6 +603,7 @@ export default { mkEnabled: await loadMkEnabled().catch(() => false), }); this.$message.success('提交成功'); + sessionStorage.setItem(invoiceReceiptListRefreshKey, '1'); this.goBack(); }, async syncReferenceData() { @@ -678,8 +619,7 @@ export default { this.$message.warning('金蝶票池未查询到该发票'); return; } - if (!this.confirmInvoice(invoice, { preserveUserInput: true })) return; - if (this.settlementIds.length) await this.loadReferenceInformation(); + if (!(await this.confirmInvoice(invoice, { preserveUserInput: true }))) return; this.$message.success('同步成功'); }, goBack() { @@ -688,8 +628,13 @@ export default { formatMoney(value) { return Number(value || 0).toFixed(2); }, - formatRate(value) { - return value === null || value === undefined || value === '' ? '' : `${Number(value)}%`; + formatQuantity(value) { + const number = Number(value); + return Number.isFinite(number) ? String(number) : '-'; + }, + formatRateText(value) { + if (value === null || value === undefined || value === '') return '-'; + return `${value}%`; }, }, }; @@ -738,15 +683,6 @@ export default { :global(.avue-layout--horizontal .receipt-form-page__actions) { left: 0; } -.receipt-form-page__dialog-search { - display: flex; - justify-content: flex-end; - gap: 8px; - margin-bottom: 12px; -} -.receipt-form-page__dialog-search .el-input { - width: 360px; -} .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 aed1115..0a15cd8 100644 --- a/src/views/payment/invoice-receipt.vue +++ b/src/views/payment/invoice-receipt.vue @@ -223,6 +223,10 @@ import organizationSearch from '@/mixins/organization-search'; import { getMKApprovalUrl } from '@/api/system/business-process'; import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval'; +// 表单提交成功后带标记返回,keep-alive 缓存的列表页据此重新拉取 +const invoiceReceiptListRefreshKey = 'invoice-receipt-list-refresh'; +const invoiceReceiptListRoute = '/payment/invoice-receipt'; + const emptyQuery = () => ({ invoiceNo: '', invoiceDate: '', @@ -281,7 +285,28 @@ export default { this.mkEnabled = false; }); }, + activated() { + this.consumeListRefreshFlag(); + }, + watch: { + $route(to, from) { + if ( + to.path === invoiceReceiptListRoute && + from?.path?.includes('/invoice-receipt/form') + ) { + this.consumeListRefreshFlag(); + } + }, + }, methods: { + /** 表单提交成功后带标记返回时,重新拉取列表 */ + consumeListRefreshFlag() { + if (!sessionStorage.getItem(invoiceReceiptListRefreshKey)) { + return; + } + sessionStorage.removeItem(invoiceReceiptListRefreshKey); + this.loadTable(); + }, hasPermission(code) { return this.permission?.[code] !== false; }, diff --git a/src/views/payment/payment-application-form.vue b/src/views/payment/payment-application-form.vue index 0a01f1c..f93c25f 100644 --- a/src/views/payment/payment-application-form.vue +++ b/src/views/payment/payment-application-form.vue @@ -1296,9 +1296,11 @@ export default { this.form = { ...emptyForm(), ...data, + // 后端可能返回 -1 占位预付 id,统一归一为 null,防止点击结算单号误跳预付 + preSettlementId: this.isValidReferenceId(data.preSettlementId) ? data.preSettlementId : null, preSettlementIds: data.preSettlementIds?.length ? data.preSettlementIds - : data.preSettlementId + : this.isValidReferenceId(data.preSettlementId) ? [data.preSettlementId] : [], attachments: this.parse(data.attachmentsJson), @@ -1344,9 +1346,13 @@ export default { this.form = { ...emptyForm(), ...data, + // 后端可能返回 -1 占位预付 id,统一归一为 null,防止点击结算单号误跳预付 + preSettlementId: this.isValidReferenceId(data.preSettlementId) + ? data.preSettlementId + : null, preSettlementIds: data.preSettlementIds?.length ? data.preSettlementIds - : data.preSettlementId + : this.isValidReferenceId(data.preSettlementId) ? [data.preSettlementId] : [], attachments: this.parse(data.attachmentsJson), @@ -1587,17 +1593,22 @@ export default { }); }, clearFormalReferenceWhenPreOnly() { + // 纯预付引用才收敛为预付;-1 占位 id 不得充当预付关联 const progressAdvance = this.form.paymentType === 'progress_advance'; const onlyPre = progressAdvance || (this.referenceRows.length > 0 && this.referenceRows.every(item => item.referenceType === 'pre')); if (!onlyPre) return; - if (!this.form.preSettlementId) { + if (!this.isValidReferenceId(this.form.preSettlementId)) { const preId = - this.referenceRows.find(item => item.referenceType === 'pre')?.id || - (progressAdvance ? this.form.settlementId : null); - if (preId) { + this.referenceRows.find( + item => item.referenceType === 'pre' && this.isValidReferenceId(item.id) + )?.id || + (progressAdvance && this.isValidReferenceId(this.form.settlementId) + ? this.form.settlementId + : null); + if (this.isValidReferenceId(preId)) { this.form.preSettlementId = preId; this.form.preSettlementIds = [preId]; } @@ -1608,13 +1619,16 @@ export default { }, restoreReferenceRows(data, fallbackRow) { const relations = Array.isArray(data?.settlements) ? data.settlements : []; + // 行级预付/正式类型以过滤占位 id 后的关联为准,决定点击结算单号的跳转目标 const rows = relations .map(item => { - const isPre = Boolean(item.preSettlementId || item.preSettlementNo); + // 后端用 -1 占位空 id,必须先过滤再判预付,否则正式结算行会被误标为预付 + const preId = this.isValidReferenceId(item.preSettlementId) ? item.preSettlementId : null; + const isPre = Boolean(preId || item.preSettlementNo); const settlementNo = item.preSettlementNo || item.formalSettlementNo || ''; if (!settlementNo) return null; return { - id: isPre ? item.preSettlementId || item.formalSettlementId : item.formalSettlementId, + id: isPre ? preId || item.formalSettlementId : item.formalSettlementId, settlementNo, referenceType: isPre ? 'pre' : 'formal', settlementAmount: Number(Number(item.settlementAmount || 0).toFixed(2)), @@ -1642,9 +1656,10 @@ export default { }, createReferenceRow(row) { const settlementAmount = Number(row.settlementAmount || 0); + // 后端以 -1 占位空预付 id,必须先校验否则正式结算行会被误判为预付 const isPre = Boolean( row.preSettlementNo || - row.preSettlementId || + this.isValidReferenceId(row.preSettlementId) || row.referenceType === 'pre' || row.paymentType === 'progress_advance' ); @@ -2770,23 +2785,26 @@ export default { }, async openSettlementDetail(row = {}) { const settlementNo = String(row.settlementNo || '').trim(); + // 行上 referenceType 优先;发票行等只带单号的,按单号回查引用行;都缺省才回落表单级推断 + const matchedReference = this.referenceRows.find( + item => String(item.settlementNo || '').trim() === settlementNo + ); + const referenceType = row.referenceType || matchedReference?.referenceType; const isPre = - row.referenceType === 'pre' || - this.form.paymentType === 'progress_advance' || - Boolean(this.form.preSettlementId || this.form.preSettlementNo); + referenceType === 'pre' || + (!referenceType && + (this.form.paymentType === 'progress_advance' || + Boolean( + this.isValidReferenceId(this.form.preSettlementId) || this.form.preSettlementNo + ))); let id = row.id || row.preSettlementId || row.settlementId || + matchedReference?.id || (isPre ? this.form.preSettlementId || this.form.preSettlementIds?.[0] : this.form.settlementId || this.form.settlementIds?.[0]); - if (!id && settlementNo) { - const matched = this.referenceRows.find( - item => String(item.settlementNo || '').trim() === settlementNo - ); - id = matched?.id; - } if (!id && settlementNo) { try { const listApi = isPre ? preApi.getList : formalApi.getList; diff --git a/src/views/payment/payment-application.vue b/src/views/payment/payment-application.vue index 7135c3a..a66fb04 100644 --- a/src/views/payment/payment-application.vue +++ b/src/views/payment/payment-application.vue @@ -80,6 +80,23 @@ + + + + + + + + diff --git a/src/views/settlement/components/pre-settlement-editor.vue b/src/views/settlement/components/pre-settlement-editor.vue index 5dfb517..f45102d 100644 --- a/src/views/settlement/components/pre-settlement-editor.vue +++ b/src/views/settlement/components/pre-settlement-editor.vue @@ -989,6 +989,7 @@ import { save, submit, } from '@/api/settlement/preSettlement'; +import { getDetail as getContractDetail } from '@/api/business/contract-manage'; import { calculateAdjustedFee, getFeeDetail } from '@/api/settlement/receivable-payable-detail'; import { resolveReceivablePayableDetailId, @@ -1072,6 +1073,8 @@ export default { changeTableColumns: changeRecordColumns, candidateColumns: candidateDetailColumns, contractOptions: [], + payerOptions: [], + payeeOptions: [], contractCategoryOptions, signTypeOptions, effectiveTypeOptions, @@ -1391,18 +1394,34 @@ export default { const first = rows[0]; const settlementType = this.initialData.settlementType || first.settlementType || 'payable'; const contractId = first.contractId || ''; - let contractPartyA = first.payerName || ''; - let contractPartyB = first.payeeName || ''; - if (contractId && (!contractPartyA || !contractPartyB)) { + let contractPartyA = ''; + let contractPartyB = ''; + let contractPartyAJson = ''; + let contractPartyBJson = ''; + if (contractId) { try { - const { data } = await getContractOptions(first.contractNo || ''); - const matched = (data?.data || []).find(item => String(item.id) === String(contractId)); - contractPartyA = contractPartyA || matched?.partyA || ''; - contractPartyB = contractPartyB || matched?.partyB || ''; + const contractDetail = await this.fetchContractPartyDetail(contractId); + if (contractDetail) { + contractPartyA = contractDetail.partyA || ''; + contractPartyB = contractDetail.partyB || ''; + contractPartyAJson = contractDetail.partyAJson || ''; + contractPartyBJson = contractDetail.partyBJson || ''; + } } catch (error) { contractPartyA = ''; contractPartyB = ''; } + if (!contractPartyA && !contractPartyB) { + try { + const { data } = await getContractOptions(first.contractNo || ''); + const matched = (data?.data || []).find(item => String(item.id) === String(contractId)); + contractPartyA = matched?.partyA || ''; + contractPartyB = matched?.partyB || ''; + } catch (error) { + contractPartyA = first.payerName || ''; + contractPartyB = first.payeeName || ''; + } + } } if (!this.contractOptions.some(item => String(item.id) === String(contractId))) { this.contractOptions.push({ @@ -1416,6 +1435,8 @@ export default { settlementType, partyA: contractPartyA, partyB: contractPartyB, + partyAJson: contractPartyAJson, + partyBJson: contractPartyBJson, }); } this.handleContractChange(contractId); @@ -1427,12 +1448,12 @@ export default { projectName: first.projectName || this.form.projectName, deptId: first.deptId || this.form.deptId, deptName: first.deptName || this.form.deptName, - payerName: this.form.payerName || contractPartyA, - payeeName: this.form.payeeName || contractPartyB, settlementType, currency: first.currency || 'RMB', localCurrency: first.localCurrency || 'RMB', }); + // 明细带入的收付方若为单值且在选项中,优先保留;多方拼接值由下拉默认首项承接 + this.applyPartySelection(first.payerName, first.payeeName); this.details = rows.map(row => this.normalizeSourceDetail(row)); this.detailPage.current = 1; this.loading = true; @@ -1516,6 +1537,8 @@ export default { this.billingRuleDialog.visible = false; this.billingRuleDialog.rule = null; this.contractOptions = []; + this.payerOptions = []; + this.payeeOptions = []; this.contractDialog.visible = false; this.contractDialog.expanded = false; this.contractDialog.rows = []; @@ -1552,21 +1575,20 @@ export default { this.attachments = this.parseAttachments(detail.attachmentsJson); this.sortAttachments(); this.selectedAttachmentRows = []; - if ( - detail.contractId && - !this.contractOptions.some(item => item.id === detail.contractId) - ) { - this.contractOptions.push({ - id: detail.contractId, + if (detail.contractId) { + await this.ensureContractPartyOptions(detail.contractId, { contractNo: detail.contractNo, contractName: detail.contractName, projectId: detail.projectId, projectName: detail.projectName, deptId: detail.deptId, deptName: detail.deptName, - partyA: detail.payerName, - partyB: detail.payeeName, + settlementType: detail.settlementType, }); + this.applyPartySelection(detail.payerName, detail.payeeName); + } else { + this.payerOptions = []; + this.payeeOptions = []; } if (this.publicMode && detail.detailFees) { Object.entries(detail.detailFees).forEach(([detailId, feeRows]) => { @@ -1640,6 +1662,8 @@ export default { deptName: row.deptName || row.organizationName, partyA: row.partyA || row.payerName, partyB: row.partyB || row.payeeName, + partyAJson: row.partyAJson || '', + partyBJson: row.partyBJson || '', settlementType: row.settlementType || (row.contractCategory === '客户合同' ? 'receivable' : 'payable'), }; @@ -1649,7 +1673,7 @@ export default { this.handleContractChange(contract.id); this.contractDialog.visible = false; this.contractDialog.current = null; - this.$refs.formRef?.clearValidate('contractId'); + this.$refs.formRef?.clearValidate(['contractId', 'payerName', 'payeeName']); }, contractCategoryName(value) { return ( @@ -1714,6 +1738,8 @@ export default { this.form.deptName = ''; this.form.payerName = ''; this.form.payeeName = ''; + this.payerOptions = []; + this.payeeOptions = []; this.summaryFees = []; this.projectAdvanceWriteOffs = []; return; @@ -1726,9 +1752,8 @@ export default { this.form.deptId = contract.deptId; this.form.deptName = contract.deptName; this.form.settlementType = contract.settlementType || 'payable'; - // 部分候选合同只带 id/名称,缺甲方乙方;此时保留已带出的收付方,不要清成 undefined - if (contract.partyA) this.form.payerName = contract.partyA; - if (contract.partyB) this.form.payeeName = contract.partyB; + this.syncPartyOptionsFromContract(contract); + this.applyPartySelection('', ''); this.form.currency = contract.settlementCurrency || 'RMB'; this.summaryFees = []; this.projectAdvanceWriteOffs = []; @@ -1737,6 +1762,152 @@ export default { this.fetchBfiExchangeRate(); } }, + parsePartyNames(partyJson, fallbackName) { + try { + const parsed = partyJson ? JSON.parse(partyJson) : []; + if (Array.isArray(parsed) && parsed.length) { + const names = parsed + .map(item => String(item?.customerName || '').trim()) + .filter(Boolean); + if (names.length) return [...new Set(names)]; + } + } catch (error) { + // JSON 解析失败时回退到名称拆分 + } + return [ + ...new Set( + String(fallbackName || '') + .split(/[、,,]/) + .map(name => name.trim()) + .filter(Boolean) + ), + ]; + }, + buildPartySelectOptions(names) { + return (names || []).map(name => ({ dictKey: name, dictValue: name })); + }, + resolveContractPartyNames(contract) { + const settlementType = contract?.settlementType || this.form.settlementType || 'payable'; + const partyANames = this.parsePartyNames(contract?.partyAJson, contract?.partyA); + const partyBNames = this.parsePartyNames(contract?.partyBJson, contract?.partyB); + if (settlementType === 'receivable') { + return { payerNames: partyBNames, payeeNames: partyANames }; + } + return { payerNames: partyANames, payeeNames: partyBNames }; + }, + syncPartyOptionsFromContract(contract) { + const { payerNames, payeeNames } = this.resolveContractPartyNames(contract || {}); + this.payerOptions = this.buildPartySelectOptions(payerNames); + this.payeeOptions = this.buildPartySelectOptions(payeeNames); + }, + isJoinedPartyName(value) { + return /[、,,]/.test(String(value || '')); + }, + pickPartyValue(names, currentValue) { + const options = Array.isArray(names) ? names : []; + const current = String(currentValue || '').trim(); + if (!options.length) { + // 无线上选项时(如公开查看拉不到合同),保留已有单值,丢弃多方拼接脏值 + return this.isJoinedPartyName(current) ? '' : current; + } + if (current && !this.isJoinedPartyName(current) && options.includes(current)) { + return current; + } + return options[0] || ''; + }, + applyPartySelection(payerName, payeeName) { + const payerNames = this.payerOptions.map(item => item.dictKey); + const payeeNames = this.payeeOptions.map(item => item.dictKey); + this.form.payerName = this.pickPartyValue(payerNames, payerName); + this.form.payeeName = this.pickPartyValue(payeeNames, payeeName); + // 编辑历史单值不在合同多方中时,仍保留展示,避免下拉空白 + if ( + this.form.payerName && + !this.payerOptions.some(item => item.dictKey === this.form.payerName) + ) { + this.payerOptions = [ + ...this.payerOptions, + { dictKey: this.form.payerName, dictValue: this.form.payerName }, + ]; + } + if ( + this.form.payeeName && + !this.payeeOptions.some(item => item.dictKey === this.form.payeeName) + ) { + this.payeeOptions = [ + ...this.payeeOptions, + { dictKey: this.form.payeeName, dictValue: this.form.payeeName }, + ]; + } + }, + async fetchContractPartyDetail(contractId) { + if (!contractId) return null; + const { data } = await getContractDetail(contractId); + return data?.data || null; + }, + async ensureContractPartyOptions(contractId, fallback = {}) { + if (!contractId) { + this.payerOptions = []; + this.payeeOptions = []; + return null; + } + let contract = this.contractOptions.find(item => String(item.id) === String(contractId)); + const needDetail = + !contract || + !(contract.partyAJson || contract.partyBJson) || + this.isJoinedPartyName(contract.partyA) || + this.isJoinedPartyName(contract.partyB) || + (!contract.partyA && !contract.partyB); + if (needDetail) { + try { + const detail = await this.fetchContractPartyDetail(contractId); + if (detail) { + contract = { + ...(contract || {}), + id: contractId, + contractNo: detail.contractNo || fallback.contractNo || contract?.contractNo, + contractName: detail.contractName || fallback.contractName || contract?.contractName, + projectId: detail.projectId || fallback.projectId || contract?.projectId, + projectName: detail.projectName || fallback.projectName || contract?.projectName, + deptId: + detail.organizationId || + detail.deptId || + fallback.deptId || + contract?.deptId, + deptName: + detail.organizationName || + detail.deptName || + fallback.deptName || + contract?.deptName, + partyA: detail.partyA || contract?.partyA || '', + partyB: detail.partyB || contract?.partyB || '', + partyAJson: detail.partyAJson || contract?.partyAJson || '', + partyBJson: detail.partyBJson || contract?.partyBJson || '', + settlementType: + fallback.settlementType || + contract?.settlementType || + (detail.contractCategory === '客户合同' ? 'receivable' : 'payable'), + settlementCurrency: detail.settlementCurrency || contract?.settlementCurrency, + }; + } + } catch (error) { + // 合同详情失败时仍用现有快照拆分多方名称 + } + } + if (!contract) { + contract = { + id: contractId, + ...fallback, + partyA: fallback.partyA || '', + partyB: fallback.partyB || '', + }; + } + const index = this.contractOptions.findIndex(item => String(item.id) === String(contractId)); + if (index >= 0) this.contractOptions.splice(index, 1, contract); + else this.contractOptions.push(contract); + this.syncPartyOptionsFromContract(contract); + return contract; + }, async saveDraft(shouldSubmit) { await this.$refs.formRef?.validate(); if (shouldSubmit && !this.details.length) { diff --git a/src/views/settlement/formal-settlement.vue b/src/views/settlement/formal-settlement.vue index b25d8e5..b2e5905 100644 --- a/src/views/settlement/formal-settlement.vue +++ b/src/views/settlement/formal-settlement.vue @@ -98,6 +98,125 @@ > + + + + + + + {{ invoiceRegisterDialog.row.formalSettlementNo || '-' }} + + + {{ invoiceRegisterDialog.row.projectName || '-' }} + + + {{ invoiceRegisterDialog.row.deptName || '-' }} + + + {{ invoiceRegisterDialog.row.contractNo || '-' }} + + + {{ invoiceRegisterDialog.row.payerName || '-' }} + + + {{ invoiceRegisterDialog.row.payeeName || '-' }} + + + {{ settlementTypeName(invoiceRegisterDialog.row.settlementType) }} + + + {{ invoiceRegisterDialog.row.businessModelName || '-' }} + + + {{ formatMoney(invoiceRegisterDialog.row.settlementAmount, invoiceRegisterDialog.row.currency) }} + + + {{ formatMoney(invoiceRegisterDialog.row.invoiceAmount, invoiceRegisterDialog.row.currency) }} + + + {{ invoiceRegisterDialog.row.createUserName || '-' }} + + + {{ invoiceRegisterDialog.row.createTime || '-' }} + + + {{ kingdeeSyncName(invoiceRegisterDialog.row.kingdeeSyncStatus) }} + + + {{ invoiceRegisterDialog.row.remark || '-' }} + + + + +
+ 选择收票单 +
+ + + + + + + + + + + + + + + + +
+ +
+ + @@ -113,16 +232,18 @@ import { isAdministrator } from '@/utils/auth'; import { downloadFile } from '@/utils/util'; import * as api from '@/api/settlement/formalSettlement'; import * as paymentApi from '@/api/payment/paymentApplication'; -import { formalSettlementOption } from '@/option/settlement/formalSettlement'; +import * as invoiceReceiptApi from '@/api/payment/invoiceReceipt'; +import { formalSettlementOption, kingdeeSyncName } from '@/option/settlement/formalSettlement'; import organizationSearch from '@/mixins/organization-search'; import FormalSettlementEditor from './components/formal-settlement-editor.vue'; import FormalSettlementTablePanel from './components/formal-settlement-table-panel.vue'; +import InvoiceReceiptCandidatesDialog from './components/invoice-receipt-candidates-dialog.vue'; import { submitMkApprovalFlow, loadMkEnabled } from '@/utils/mk-approval'; import { getMKApprovalUrl } from '@/api/system/business-process'; export default { name: 'FormalSettlement', - components: { FormalSettlementEditor, FormalSettlementTablePanel }, + components: { FormalSettlementEditor, FormalSettlementTablePanel, InvoiceReceiptCandidatesDialog }, mixins: [organizationSearch], props: { /** 菜单拆分后锁定结算类型:payable=付款结算单,receivable=收款结算单 */ @@ -134,7 +255,6 @@ export default { }, data() { return { - option: formalSettlementOption(), query: {}, activeSettlementType: 'payable', rows: [], @@ -151,6 +271,16 @@ export default { rows: [], form: { appliedAmount: 0, remark: '' }, }, + // 先票后款路径B:发票登记(关联收票单 + 确认推送指定应付) + invoiceRegisterDialog: { + visible: false, + saving: false, + row: {}, + candidates: [], + }, + candidatesDialog: { + visible: false, + }, mkEnabled: false, // keep-alive 多标签下仅激活实例响应 detailNo,避免缓存页重复弹窗 isPageActive: true, @@ -158,6 +288,10 @@ export default { }, computed: { ...mapGetters(['permission', 'userInfo']), + // 列随结算类型切换重建:收款结算单不展示「已指定应付」 + option() { + return formalSettlementOption(this.activeSettlementType); + }, isAdmin() { return isAdministrator(this.userInfo); }, @@ -297,12 +431,14 @@ export default { return: () => this.handleReturn(row), void: () => this.handleVoid(row), print: () => this.handlePrint(row), + invoiceRegister: () => this.openInvoiceRegisterDialog(row), }; (handlers[type] || (() => {}))(); }, openCreate() { + // 按列表锁定的结算类型路由到对应表单入口,保证菜单高亮与关闭回落列表正确 this.$router.push({ - path: '/settlement/formal-settlement/form', + path: settlementListPath(this.activeSettlementType) + '/form', query: { mode: 'add', name: `新增${this.billLabel}`, @@ -313,8 +449,9 @@ export default { }, openEdit(row) { const settlementType = row.settlementType || this.activeSettlementType; + // 行类型可能与列表锁定类型不同,按行类型路由 this.$router.push({ - path: '/settlement/formal-settlement/form', + path: settlementListPath(settlementType) + '/form', query: { mode: 'edit', id: row.id, @@ -596,6 +733,68 @@ export default { this.paymentDialog.loading = false; } }, + // 先票后款路径B:发票登记——认领收票单(claim-invoices 走增量认领并同步收票侧关系) + // 打开时回显本单已认领收票单(claimed 标记禁止移除),已认领过票的可直接保存推送补指定应付 + async openInvoiceRegisterDialog(row) { + if ((row?.approvalStatus || '') !== 'approved') { + this.$message.warning('仅审批通过的付款结算单允许发票登记'); + return; + } + this.invoiceRegisterDialog.row = row || {}; + this.invoiceRegisterDialog.candidates = []; + this.invoiceRegisterDialog.visible = true; + try { + const claimed = this.unwrapData(await invoiceReceiptApi.claimedReceipts(row.id)) || []; + this.invoiceRegisterDialog.candidates = claimed.map(item => ({ ...item, claimed: true })); + } catch (error) { + this.invoiceRegisterDialog.candidates = []; + } + }, + addInvoiceRegisterCandidates(rows) { + const existing = new Set( + this.invoiceRegisterDialog.candidates.map(item => String(item.invoiceNo)) + ); + const added = rows.filter(row => !existing.has(String(row.invoiceNo))); + if (!added.length) { + this.$message.warning('所选收票单均已登记'); + return; + } + // 全额认领:匹配金额固定=发票金额(仅暂存展示,点保存认领确认后才落库) + this.invoiceRegisterDialog.candidates.push( + ...added.map(row => ({ ...row, matchedAmount: Number(row.invoiceAmount || 0) })) + ); + }, + async submitInvoiceRegister() { + const settlement = this.invoiceRegisterDialog.row; + const invoices = this.invoiceRegisterDialog.candidates.map(row => ({ + ...row, + attachmentJson: row.attachmentJson || row.attachmentsJson, + invoiceType: row.invoiceType || '', + taxRate: row.taxRate ?? null, + availableInvoiceAmount: Number(row.invoiceAmount || 0), + matchedAmount: Number(row.matchedAmount || row.invoiceAmount || 0), + })); + try { + // 先确认后保存:确定才认领并推送指定应付,取消不保存任何数据 + await this.$confirm('确认认领并推送金蝶指定应付?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning', + }); + } catch { + return; + } + this.invoiceRegisterDialog.saving = true; + try { + await api.claimInvoices({ formalSettlementId: settlement.id, invoices }); + const data = this.unwrapData(await api.invoiceAssignKingdee(settlement.id)); + this.$message.success(data || '发票登记推送金蝶成功'); + this.invoiceRegisterDialog.visible = false; + this.onLoad(this.page); + } finally { + this.invoiceRegisterDialog.saving = false; + } + }, handlePrint(row) { if (!row) { row = this.selectedOne('打印'); @@ -645,6 +844,9 @@ export default { params.settlementType = this.lockedSettlementType || this.activeSettlementType; return params; }, + settlementTypeName(value) { + return { payable: '付款', receivable: '收款' }[value] || '-'; + }, unwrapData(response) { const body = response?.data || response || {}; return body?.data || body; @@ -685,6 +887,11 @@ export default { margin-bottom: 12px; color: #606266; } +.invoice-register-dialog__actions { + display: flex; + justify-content: flex-end; + margin-bottom: 8px; +} :deep(.formal-page.basic-container .basic-container__card > .el-card__body) { padding: 0; } diff --git a/src/views/settlement/pre-settlement.vue b/src/views/settlement/pre-settlement.vue index 8648db4..a36e4b4 100644 --- a/src/views/settlement/pre-settlement.vue +++ b/src/views/settlement/pre-settlement.vue @@ -1,7 +1,7 @@