diff --git a/src/api/settlement/settlementAdjustment.js b/src/api/settlement/settlementAdjustment.js index d809bf6..4c7b460 100644 --- a/src/api/settlement/settlementAdjustment.js +++ b/src/api/settlement/settlementAdjustment.js @@ -17,6 +17,10 @@ export const approve = id => request({ url: `${baseUrl}/approve`, method: 'post' export const returnBill = (id, reason) => request({ url: `${baseUrl}/return`, method: 'post', data: { id, reason } }); export const repush = id => request({ url: `${baseUrl}/repush`, method: 'post', params: { id } }); +export const syncKingdee = id => + request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } }); +export const kingdeeWriteOffPrecheck = formalSettlementId => + request({ url: `${baseUrl}/kingdee-writeoff-precheck`, method: 'get', params: { formalSettlementId } }); export const approvalStatusOptions = [ { label: '草稿', value: 'draft' }, diff --git a/src/option/settlement/formalSettlementTable.js b/src/option/settlement/formalSettlementTable.js index bf3f0c4..92713ca 100644 --- a/src/option/settlement/formalSettlementTable.js +++ b/src/option/settlement/formalSettlementTable.js @@ -19,6 +19,8 @@ export const formalSettlementTableColumns = [ { label: '审核状态', prop: 'approvalStatusName', minWidth: 110, status: true }, { label: '当前节点', prop: 'currentNode', minWidth: 130 }, { label: '当前处理人', prop: 'currentProcessor', minWidth: 130 }, + { label: '调整标识', prop: 'adjustFlag', minWidth: 110 }, + { label: '来源原单号', prop: 'originFormalSettlementNo', minWidth: 170 }, { label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 150 }, { label: '金蝶单据状态', prop: 'kingdeeSyncStatus', minWidth: 135 }, { label: '金蝶锁状态', prop: 'kingdeeLockStatus', minWidth: 150 }, diff --git a/src/option/settlement/settlementAdjustmentTable.js b/src/option/settlement/settlementAdjustmentTable.js index aaaa9fc..4919a1a 100644 --- a/src/option/settlement/settlementAdjustmentTable.js +++ b/src/option/settlement/settlementAdjustmentTable.js @@ -1,6 +1,6 @@ export const settlementAdjustmentTableColumns = [ { label: '单据号', prop: 'adjustmentNo', minWidth: 160, link: true, fixed: 'left' }, - { label: '结算单号', prop: 'formalSettlementNo', minWidth: 160 }, + { label: '结算单号', prop: 'formalSettlementNo', minWidth: 160, link: true }, { label: '结算单类型', prop: 'settlementTypeName', minWidth: 110 }, { label: '项目名称', prop: 'projectName', minWidth: 140 }, { label: '所属组织', prop: 'deptName', minWidth: 140 }, @@ -9,6 +9,8 @@ export const settlementAdjustmentTableColumns = [ { label: '合同名称', prop: 'contractName', minWidth: 170 }, { label: '调整金额', prop: 'adjustmentAmount', minWidth: 130, money: true }, { label: '审核状态', prop: 'approvalStatusName', minWidth: 110, status: true }, + { label: '金蝶单据状态', prop: 'kingdeeSyncStatus', minWidth: 130, kingdee: true }, + { label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 150 }, { label: '当前节点', prop: 'currentNode', minWidth: 120 }, { label: '当前处理人', prop: 'currentProcessor', minWidth: 130 }, { label: '创建人', prop: 'createUserName', minWidth: 110 }, diff --git a/src/utils/settlementStatus.js b/src/utils/settlementStatus.js new file mode 100644 index 0000000..4fdf2ec --- /dev/null +++ b/src/utils/settlementStatus.js @@ -0,0 +1,23 @@ +/** + * 结算单预付记录单据状态名(预结算编辑器与正式结算编辑器共用,避免两页状态名不一致) + */ +export function settlementStatusName(status, displayValue) { + const labels = { + reviewing: '审批中', + approved: '审批通过', + paid: '已付款', + returned: '已驳回', + voided: '已作废', + draft: '草稿', + }; + const name = labels[status]; + if (name) return name; + return typeof displayValue === 'function' ? displayValue(status) : status || '-'; +} + +/** + * 统一预付付款记录类型名 + */ +export function settlementRecordTypeName(recordType) { + return { progress_advance: '进度预付', project_advance: '项目预付核销' }[recordType] || ''; +} diff --git a/src/views/payment/payment-application.vue b/src/views/payment/payment-application.vue index f8c0915..baf454c 100644 --- a/src/views/payment/payment-application.vue +++ b/src/views/payment/payment-application.vue @@ -163,6 +163,7 @@ export default { rows: [], selection: [], loading: false, + approveLoading: false, syncLoading: false, resultSyncing: false, query: {}, @@ -327,14 +328,21 @@ export default { this.refreshChange(); }, async handleApprove(row) { - 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 || '审批通过'); + // 防止重复点击:请求进行中忽略后续点击 + if (this.approveLoading) return; + this.approveLoading = true; + try { + 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.refreshChange(); + } finally { + this.approveLoading = false; } - this.refreshChange(); }, handleMkAudit(row) { if (!row?.id) { diff --git a/src/views/settlement/components/formal-settlement-editor.vue b/src/views/settlement/components/formal-settlement-editor.vue index 04a82d3..e7d8339 100644 --- a/src/views/settlement/components/formal-settlement-editor.vue +++ b/src/views/settlement/components/formal-settlement-editor.vue @@ -177,6 +177,22 @@ + + + {{ formatMoney(form.settlementAmount) }} + + + {{ formatMoney(settlePaidTotal) }} + + + {{ formatMoney(settleRemainTotal) }} + + @@ -577,28 +593,27 @@ {{ displayValue(row.claimStatusName) }} - + - - {{ displayValue(row.paymentTypeName) }} + + {{ displayValue(row.typeName) }} - {{ paymentMethodName(row.paymentMethod) }} + {{ row.recordType ? '-' : paymentMethodName(row.paymentMethod) }} - + {{ formatMoney(row.appliedAmount) }} {{ formatMoney(row.paidAmount) }} - - {{ displayValue(row.approvalStatusName) }} + + + {{ row.recordType + ? settlementStatusName(row.billStatus) + : displayValue(row.approvalStatusName) }} + {{ displayValue(row.kingdeeBillNo) }} @@ -639,6 +654,22 @@ {{ displayValue(row.createTime) }} + + + 重新推送 + - + + @@ -1038,6 +1069,10 @@ import { h } from 'vue'; import { mapGetters } from 'vuex'; import { formatContractOptionLabel } from '@/utils/contract'; import { normalizeCurrencyCode } from '@/utils/currency'; +import { + settlementRecordTypeName, + settlementStatusName, +} from '@/utils/settlementStatus'; import { downloadFileByUrl } from '@/utils/util'; import { downloadKingdeeInvoiceFile, @@ -1048,6 +1083,7 @@ import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/m import ProjectAdvanceWriteOff from './project-advance-write-off.vue'; import * as XLSX from 'xlsx'; import { isAdministrator } from '@/utils/auth'; +import * as settlementAdjustmentApi from '@/api/settlement/settlementAdjustment'; // 结算币种为本位币(人民币)时汇率恒为1,汇率日期、结算汇率与按汇率换算的本位币合计都不展示 const LOCAL_CURRENCY_HIDDEN_FIELDS = ['exchangeRateDate', 'exchangeRate', 'localSettlementAmount']; @@ -1080,6 +1116,7 @@ export default { details: [], summaryFees: [], paymentApplications: [], + advancePayments: [], receiptClaims: [], adjustments: [], changeRecords: [], @@ -1259,6 +1296,34 @@ export default { summaryTotal() { return this.summaryFees.reduce((total, row) => total + Number(row.settlementAmount || 0), 0); }, + // 已付金额(含预付、核销):编辑态取来源预结算预付快照 + 当前核销行实时合计(草稿态无尾款申请);只读态用落库值 + settlePaidTotal() { + if (!this.editable) return Number(this.form.paidAmount || 0); + const advancePaid = this.sources.reduce( + (sum, item) => sum + Number(item.advancePaidAmount || 0), + 0 + ); + const writeOff = this.projectAdvanceWriteOffs.reduce( + (sum, row) => sum + Number(row.writeOffAmount || 0), + 0 + ); + return advancePaid + writeOff; + }, + settleRemainTotal() { + return Math.max(Number(this.form.settlementAmount || 0) - Number(this.settlePaidTotal || 0), 0); + }, + // 付款记录合并表:预付付款记录(进度预付/项目预付核销)+ 尾款付款申请 + paymentRecords() { + const advances = (this.advancePayments || []).map(row => ({ + ...row, + typeName: settlementRecordTypeName(row.recordType), + })); + const tails = (this.paymentApplications || []).map(row => ({ + ...row, + typeName: row.paymentTypeName, + })); + return [...advances, ...tails]; + }, filteredDetails() { return this.details.filter(row => Object.entries(this.appliedDetailQuery).every(([field, keyword]) => { @@ -1332,6 +1397,17 @@ export default { this.validData(this.permission?.[`formal_settlement_${action}`], false) ); }, + async handleAdjustmentRepush(row) { + await this.$confirm( + '请确认金蝶旧应付/应收单已由财务手工冲销,继续将生成全新财务应付/应收单。', + '高危操作确认', + { type: 'warning', confirmButtonText: '确认重新推送' } + ); + const res = await settlementAdjustmentApi.repush(row.id); + this.$message.success(`已生成新金蝶单据:${res.data?.data || res.data || '-'}`); + await this.loadFormalDetail(this.recordId); + this.$emit('success'); + }, async loadFormalDetail(id) { const response = this.publicMode ? await getMkPublicDetail('formal-settlement', id) @@ -1353,6 +1429,7 @@ export default { this.detailFeeSnapshots = {}; this.pendingAdjustments = {}; this.paymentApplications = data.paymentApplications || []; + this.advancePayments = data.advancePayments || []; if (this.readonly && this.form.settlementType === 'receivable' && !this.publicMode) { this.receiptClaims = this.unwrapData(await getReceiptClaims(id)) || []; } @@ -1396,6 +1473,7 @@ export default { this.adjustments = []; this.changeRecords = []; this.projectAdvanceWriteOffs = []; + this.advancePayments = []; this.attachments = []; this.selectedAttachmentRows = []; this.invoices = []; @@ -1544,6 +1622,19 @@ export default { this.sources.map(item => getPreSettlementDetail(item.preSettlementId)) ); const settlements = responses.map(response => this.unwrapData(response) || {}); + // 预结算转正式:带出来源预结算已保存的项目预付核销行(后端保存时转移源行) + const writeOffRows = settlements.flatMap(settlement => settlement.projectAdvanceWriteOffs || []); + const existingWriteOffIds = new Set( + this.projectAdvanceWriteOffs.map(row => String(row.paymentApplicationId)) + ); + writeOffRows.forEach(row => { + if (existingWriteOffIds.has(String(row.paymentApplicationId))) return; + existingWriteOffIds.add(String(row.paymentApplicationId)); + this.projectAdvanceWriteOffs.push({ + ...row, + writeOffAmount: Number(row.writeOffAmount || 0), + }); + }); this.details = settlements.flatMap(settlement => (settlement.details || []).map(detail => ({ ...detail, @@ -1714,7 +1805,7 @@ export default { this.candidate.page.current = 1; this.loadCandidates(); }, - confirmCandidates() { + async confirmCandidates() { const merged = [...this.sources, ...this.candidate.selected]; const map = new Map( merged.map(item => [ @@ -1722,6 +1813,7 @@ export default { { ...item, preSettlementId: item.preSettlementId || item.id }, ]) ); + const previousIds = new Set(this.sources.map(item => String(item.preSettlementId || item.id))); this.sources = [...map.values()]; this.form.sourcePreSettlementIds = this.sources.map(item => item.preSettlementId); if (this.sources.length) { @@ -1738,6 +1830,26 @@ export default { this.form.localSettlementAmount = this.form.settlementAmount * Number(this.form.exchangeRate || 1); this.candidate.visible = false; + // 预结算转正式:带出来源预结算已保存的项目预付核销行(后端保存时转移源行,额度单份占用) + const newIds = this.sources + .map(item => item.preSettlementId) + .filter(id => !previousIds.has(String(id))); + if (!newIds.length) return; + const responses = await Promise.all(newIds.map(id => getPreSettlementDetail(id))); + const rows = responses.flatMap(response => + this.unwrapData(response)?.projectAdvanceWriteOffs || [] + ); + const existing = new Set( + this.projectAdvanceWriteOffs.map(row => String(row.paymentApplicationId)) + ); + rows.forEach(row => { + if (existing.has(String(row.paymentApplicationId))) return; + existing.add(String(row.paymentApplicationId)); + this.projectAdvanceWriteOffs.push({ + ...row, + writeOffAmount: Number(row.writeOffAmount || 0), + }); + }); }, removeSource(index) { this.sources.splice(index, 1); @@ -2762,6 +2874,9 @@ export default { }; return this.displayValue(labels[value] || value); }, + settlementStatusName(status) { + return settlementStatusName(status, this.displayValue); + }, unwrapData(response) { const body = response?.data || response || {}; return body?.data || body; @@ -3144,6 +3259,9 @@ export default { .formal-editor__negative-amount { color: var(--el-color-danger); } +.formal-editor__settle-summary { + margin-top: 8px; +} .formal-editor :deep(.formal-editor__negative-amount .el-input__inner) { 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 b15b642..816b985 100644 --- a/src/views/settlement/components/formal-settlement-table-panel.vue +++ b/src/views/settlement/components/formal-settlement-table-panel.vue @@ -92,6 +92,7 @@ {{ kingdeeSyncName(row.kingdeeSyncStatus) }} + {{ adjustFlag(row) }} {{ lockTag(row).label }} @@ -277,6 +278,12 @@ export default { kingdeeSyncName(value) { return kingdeeSyncStatusOptions.find(item => item.value === value)?.label || value || '-'; }, + adjustFlag(row) { + // 原单已调整 / 重推生成的新单(显示来源原单号) + if (row.adjustStatusName) return row.adjustStatusName; + if (row.originFormalSettlementNo) return '调整后单'; + return '-'; + }, lockTag(row) { if (row.kingdeeLockStatus === 'locked') { return { show: true, type: 'danger', label: '金蝶锁定中', tip: '该单据已被金蝶锁定,不可编辑/作废' }; diff --git a/src/views/settlement/components/pre-settlement-editor.vue b/src/views/settlement/components/pre-settlement-editor.vue index ca9b5a6..0f015c1 100644 --- a/src/views/settlement/components/pre-settlement-editor.vue +++ b/src/views/settlement/components/pre-settlement-editor.vue @@ -987,6 +987,7 @@ import { } from '@/option/settlement/preSettlementTable'; import { formatContractOptionLabel } from '@/utils/contract'; import { normalizeCurrencyCode } from '@/utils/currency'; +import { settlementStatusName } from '@/utils/settlementStatus'; import { downloadFileByUrl } from '@/utils/util'; import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval'; import * as XLSX from 'xlsx'; @@ -2919,15 +2920,7 @@ export default { return value === undefined || value === null || value === '' ? '-' : value; }, advanceStatusName(status) { - return ( - { - reviewing: '审批中', - approved: '审批通过', - paid: '已付款', - returned: '已驳回', - voided: '已作废', - }[status] || this.displayValue(status) - ); + return settlementStatusName(status, this.displayValue); }, advanceCreateUserName(row) { return this.displayValue( diff --git a/src/views/settlement/components/settlement-adjustment-editor.vue b/src/views/settlement/components/settlement-adjustment-editor.vue index 0c646d3..0ee9530 100644 --- a/src/views/settlement/components/settlement-adjustment-editor.vue +++ b/src/views/settlement/components/settlement-adjustment-editor.vue @@ -414,6 +414,8 @@ export default { }); this.details = []; this.recalculate(); + // 金蝶冲销预检:软提示,不阻断建单(金蝶不可用时静默降级) + this.precheckWriteOff(formalSettlementId); this.loading = true; try { const { data } = await api.getFormalDetails(formalSettlementId); @@ -431,6 +433,19 @@ export default { this.loading = false; } }, + async precheckWriteOff(formalSettlementId) { + try { + const { data } = await api.kingdeeWriteOffPrecheck(formalSettlementId); + if (String(this.form.formalSettlementId) !== String(formalSettlementId)) return; + const result = data?.data || data || {}; + if (!result.message) return; + if (result.syncStatus === 'synced' && result.writtenOff === false) + this.$message.warning(result.message); + else this.$message.info(result.message); + } catch (error) { + // 预检软提示:金蝶查询失败静默降级,不阻断建单 + } + }, addFeeRow() { this.details.push({ formalSettlementDetailId: null, diff --git a/src/views/settlement/formal-settlement.vue b/src/views/settlement/formal-settlement.vue index 734e3a4..1033caa 100644 --- a/src/views/settlement/formal-settlement.vue +++ b/src/views/settlement/formal-settlement.vue @@ -297,6 +297,10 @@ export default { if (detailNo) this.openRouteDetail(detailNo); }, }, + created() { + // 标签机制按 fullPath 缓存同页多实例:仅当前路由实例应响应 detailNo 跳转,防止缓存旧实例也弹窗 + this.ownFullPath = this.$route.fullPath; + }, mounted() { if (this.lockedSettlementType === 'payable' || this.lockedSettlementType === 'receivable') { this.activeSettlementType = this.lockedSettlementType; @@ -397,7 +401,7 @@ export default { }, async openRouteDetail(detailNo = this.$route.query.detailNo) { const formalSettlementNo = String(detailNo || '').trim(); - if (!formalSettlementNo) return; + if (!formalSettlementNo || this.ownFullPath !== this.$route.fullPath) return; const response = await api.getList(1, 10, { formalSettlementNo }); const data = this.unwrapData(response); const row = (data.records || []).find(item => item.formalSettlementNo === formalSettlementNo); diff --git a/src/views/settlement/settlement-adjustment.vue b/src/views/settlement/settlement-adjustment.vue index c4bacd4..629f8c1 100644 --- a/src/views/settlement/settlement-adjustment.vue +++ b/src/views/settlement/settlement-adjustment.vue @@ -88,16 +88,21 @@ >{{ row[column.prop] }}{{ row[column.prop] || '-' }}{{ kingdeeSyncName(row.kingdeeSyncStatus) }}{{ formatMoney(row[column.prop]) }}{{ displayValue(row[column.prop]) }} - 驳回重新推送(高危){{ row.kingdeeSyncStatus === 'synced' ? '重新推送(高危)' : '推送金蝶' }} item.value === value)?.label || value || '-'; + }, displayValue(value) { return value === null || value === undefined || value === '' ? '-' : value; }, diff --git a/src/views/wel/components/rank-panel.vue b/src/views/wel/components/rank-panel.vue index 1ffaa6e..982968a 100644 --- a/src/views/wel/components/rank-panel.vue +++ b/src/views/wel/components/rank-panel.vue @@ -42,5 +42,5 @@ export default { diff --git a/src/views/wel/components/section-business.vue b/src/views/wel/components/section-business.vue index 9322f72..a0e3d8c 100644 --- a/src/views/wel/components/section-business.vue +++ b/src/views/wel/components/section-business.vue @@ -156,7 +156,7 @@ export default {