feat: 付款结算单选收票单全额认领并新增发票登记推送指定应付

This commit is contained in:
刘泉佳
2026-10-31 16:07:12 +08:00
parent 441d62a874
commit 46e7952e49
5 changed files with 359 additions and 10 deletions
+8
View File
@@ -58,5 +58,13 @@ 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 },
});
export const queryInvoicePool = params =>
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params });
@@ -440,8 +440,15 @@
/>
<section-card title="发票信息">
<div class="formal-editor__invoice-claim">
<el-button type="primary" @click="openInvoiceClaimDialog">认领查询</el-button>
<div v-if="editable" class="formal-editor__invoice-claim">
<!-- 先票后款:付款结算单选审批通过的收票单(全额认领);收款结算单维持原票池认领 -->
<el-button
v-if="form.settlementType !== 'receivable'"
type="primary"
@click="openInvoiceReceiptDialog"
>选择收票单</el-button
>
<el-button v-else type="primary" @click="openInvoiceClaimDialog">认领查询</el-button>
</div>
<el-table :data="invoices" border>
<el-table-column type="index" label="序号" width="64" align="center" />
@@ -452,11 +459,6 @@
<el-table-column prop="invoiceType" label="发票类型" min-width="150" align="center">
<template #default="{ row }">{{ displayValue(row.invoiceType) }}</template>
</el-table-column>
<el-table-column prop="taxRate" label="税率" min-width="100" align="center">
<template #default="{ row }">
{{ row.taxRate === null || row.taxRate === undefined ? '-' : `${row.taxRate}%` }}
</template>
</el-table-column>
<el-table-column
prop="invoiceAmount"
label="发票金额(含税)"
@@ -480,8 +482,9 @@
align="center"
>
<template #default="{ row }">
<!-- 先票后款:收票单认领金额固定=发票金额(全额认领),不可改 -->
<el-input-number
v-if="editable"
v-if="editable && form.settlementType === 'receivable'"
v-model="row.matchedAmount"
:min="0"
:max="Number(row.availableInvoiceAmount || 0)"
@@ -546,6 +549,11 @@
@confirm="handleInvoiceClaimConfirm"
/>
<invoice-receipt-candidates-dialog
v-model="invoiceReceiptDialog.visible"
@confirm="handleInvoiceReceiptConfirm"
/>
<section-card
v-if="readonly"
:title="form.settlementType === 'receivable' ? '收款信息' : '付款信息'"
@@ -1068,6 +1076,7 @@ import {
settlementDetailPath,
} from '@/utils/settlement-detail-link';
import { getBfiExchangeRate } from '@/api/base/currency';
import InvoiceReceiptCandidatesDialog from './invoice-receipt-candidates-dialog.vue';
import {
createFormalSettlementForm,
formalSettlementFormFields,
@@ -1110,6 +1119,7 @@ export default {
components: {
InfoFilled,
KingdeeInvoicePoolDialog,
InvoiceReceiptCandidatesDialog,
ChangeRecordDetailDialog,
ProjectAdvanceWriteOff,
},
@@ -1158,6 +1168,10 @@ export default {
selected: [],
confirming: false,
},
// 先票后款:付款结算单选收票单(审批通过+未认领)弹窗
invoiceReceiptDialog: {
visible: false,
},
attachmentUploadFiles: [],
attachmentTypeOptions: [],
attachmentFileTypes: [
@@ -2651,6 +2665,39 @@ export default {
openInvoiceClaimDialog() {
this.invoiceClaimDialog.visible = true;
},
// 先票后款:选收票单(审批通过+未被认领),确认后按发票号全额认领
openInvoiceReceiptDialog() {
this.invoiceReceiptDialog.visible = true;
},
async handleInvoiceReceiptConfirm(rows) {
// 收票单行 → 结算单发票行:匹配金额固定=发票金额(全额认领),附件契约归一为单数 attachmentJson
const invoices = rows.map(row => ({
...row,
attachmentJson: row.attachmentJson || row.attachmentsJson,
invoiceType: row.invoiceType || '',
taxRate: row.taxRate ?? null,
availableInvoiceAmount: Number(row.invoiceAmount || 0),
matchedAmount: Number(row.invoiceAmount || 0),
}));
if (!this.form.id) {
const existingNumbers = new Set(this.invoices.map(item => String(item.invoiceNo)));
this.invoices = [
...this.invoices,
...invoices.filter(row => !existingNumbers.has(String(row.invoiceNo))),
];
this.$message.success(`已选择${invoices.length}条收票单,保存正式结算单后生效`);
return;
}
const existingNumbers = new Set(this.invoices.map(item => String(item.invoiceNo)));
const newInvoices = invoices.filter(row => !existingNumbers.has(String(row.invoiceNo)));
if (!newInvoices.length) {
this.$message.warning('所选收票单均已认领');
return;
}
await claimInvoicesApi({ formalSettlementId: this.form.id, invoices: newInvoices });
await this.loadFormalDetail(this.form.id);
this.$message.success('收票单认领成功,已同步正式结算单及关联付款单据');
},
invoicePoolQueryApi(params) {
return queryInvoicePool(params);
},
@@ -189,6 +189,13 @@ export default {
row.kingdeeSyncStatus !== 'synced'
)
actions.push({ type: 'void', label: '作废', danger: true });
// 先票后款:付款结算单审批通过后可发票登记(关联收票单并推送指定应付)
if (
this.showPaymentAction &&
this.hasPermission('invoice_register') &&
row.approvalStatus === 'approved'
)
actions.push({ type: 'invoiceRegister', label: '发票登记' });
return actions;
},
statusType(status) {
@@ -0,0 +1,150 @@
<template>
<el-dialog
v-model="visible"
title="选择收票单"
width="86%"
append-to-body
destroy-on-close
@open="handleOpen"
>
<div class="invoice-candidates-dialog__search">
<el-input
v-model="keyword"
clearable
placeholder="发票号 / 销方(开票单位) / 购方(受票单位)"
@keyup.enter="handleQuery"
/>
<el-button type="primary" :loading="loading" @click="handleQuery">查询</el-button>
</div>
<el-table
ref="candidatesTable"
v-loading="loading"
:data="rows"
row-key="id"
border
height="460"
@selection-change="selection = $event"
>
<el-table-column type="selection" width="52" fixed="left" align="center" reserve-selection />
<el-table-column prop="invoiceNo" label="发票号码" min-width="180" />
<el-table-column prop="invoiceDate" label="开票日期" min-width="110" align="center">
<template #default="{ row }">{{ row.invoiceDate || '-' }}</template>
</el-table-column>
<el-table-column prop="invoiceType" label="发票类型" min-width="140" />
<el-table-column prop="issuerName" label="开票单位(销方)" min-width="180" show-overflow-tooltip />
<el-table-column prop="receiverName" label="受票单位(购方)" min-width="180" show-overflow-tooltip />
<el-table-column label="发票金额(含税)" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
</el-table-column>
<el-table-column label="推送状态" min-width="120" align="center">
<template #default="{ row }">
<el-tag :type="pushStatusType(row.kingdeePushStatus)" size="small">
{{ pushStatusName(row.kingdeePushStatus) }}
</el-tag>
</template>
</el-table-column>
</el-table>
<div class="invoice-candidates-dialog__tip">
仅列出审批通过且未被其他付款结算单认领的收票单,最多 200 条
</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleConfirm">
确定{{ selection.length ? `(${selection.length})` : '' }}
</el-button>
</template>
</el-dialog>
</template>
<script>
import { claimCandidates } from '@/api/payment/invoiceReceipt';
export default {
name: 'InvoiceReceiptCandidatesDialog',
props: {
modelValue: { type: Boolean, required: true },
},
emits: ['update:modelValue', 'confirm'],
data() {
return {
loading: false,
keyword: '',
rows: [],
selection: [],
};
},
computed: {
visible: {
get() {
return this.modelValue;
},
set(value) {
this.$emit('update:modelValue', value);
},
},
},
methods: {
unwrap(response) {
const body = response?.data || response || {};
return body?.data || body;
},
handleOpen() {
this.keyword = '';
this.selection = [];
this.$refs.candidatesTable?.clearSelection?.();
this.load();
},
handleQuery() {
this.load();
},
async load() {
this.loading = true;
try {
this.rows = this.unwrap(await claimCandidates(this.keyword)) || [];
} finally {
this.loading = false;
}
},
handleConfirm() {
if (!this.selection.length) {
this.$message.warning('请至少选择一条收票单');
return;
}
this.$emit('confirm', this.selection.map(row => ({ ...row })));
this.visible = false;
},
pushStatusName(value) {
return (
{ pushed: '已保存收票单', assigned: '已指定应付', failed: '推送失败', unsynced: '未推送' }[
value
] || '未推送'
);
},
pushStatusType(value) {
return { pushed: 'warning', assigned: 'success', failed: 'danger', unsynced: 'info' }[value] || 'info';
},
formatMoney(value) {
const number = Number(value);
return Number.isFinite(number) ? number.toFixed(2) : '-';
},
},
};
</script>
<style lang="scss" scoped>
.invoice-candidates-dialog__search {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-bottom: 12px;
.el-input {
width: 360px;
}
}
.invoice-candidates-dialog__tip {
margin-top: 8px;
color: #909399;
font-size: 12px;
}
</style>
+139 -2
View File
@@ -98,6 +98,79 @@
></template
>
</el-dialog>
<!-- 先票后款路径B:结算单审批后发票登记(关联收票单 → 确认推送指定应付) -->
<el-dialog
v-model="invoiceRegisterDialog.visible"
title="发票登记"
width="86%"
append-to-body
class="invoice-register-dialog"
>
<section-card title="结算基本信息">
<el-descriptions :column="4" border size="small">
<el-descriptions-item label="结算单号">
{{ invoiceRegisterDialog.row.formalSettlementNo || '-' }}
</el-descriptions-item>
<el-descriptions-item label="结算总金额">
{{ formatMoney(invoiceRegisterDialog.row.settlementAmount, invoiceRegisterDialog.row.currency) }}
</el-descriptions-item>
<el-descriptions-item label="已匹配发票金额">
{{ formatMoney(invoiceRegisterDialog.row.invoiceAmount, invoiceRegisterDialog.row.currency) }}
</el-descriptions-item>
<el-descriptions-item label="金蝶同步状态">
{{ kingdeeSyncName(invoiceRegisterDialog.row.kingdeeSyncStatus) }}
</el-descriptions-item>
</el-descriptions>
</section-card>
<section-card title="认领收票单">
<el-button
type="primary"
plain
:disabled="invoiceRegisterDialog.saving"
@click="candidatesDialog.visible = true"
>选择收票单</el-button
>
<el-table :data="invoiceRegisterDialog.candidates" border class="invoice-register-dialog__table">
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="invoiceNo" label="发票号" min-width="160" align="center" />
<el-table-column prop="invoiceDate" label="开票日期" min-width="120" align="center" />
<el-table-column prop="invoiceType" label="发票类型" min-width="140" align="center" />
<el-table-column prop="issuerName" label="开票单位(销方)" min-width="170" show-overflow-tooltip />
<el-table-column label="发票金额(含税)" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
</el-table-column>
<el-table-column label="匹配结算单金额" min-width="150" align="right">
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
</el-table-column>
<el-table-column label="操作" width="90" fixed="right" align="center">
<template #default="{ $index }">
<el-link
type="danger"
:disabled="invoiceRegisterDialog.saving"
@click="invoiceRegisterDialog.candidates.splice($index, 1)"
>移除</el-link
>
</template>
</el-table-column>
</el-table>
</section-card>
<template #footer>
<el-button @click="invoiceRegisterDialog.visible = false">取消</el-button>
<el-button
type="primary"
:loading="invoiceRegisterDialog.saving"
:disabled="!invoiceRegisterDialog.candidates.length"
@click="submitInvoiceRegister"
>保存认领</el-button
>
</template>
</el-dialog>
<invoice-receipt-candidates-dialog
v-model="candidatesDialog.visible"
@confirm="addInvoiceRegisterCandidates"
/>
</basic-container>
</template>
@@ -113,16 +186,17 @@ 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 { 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=收款结算单 */
@@ -151,6 +225,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,
@@ -297,6 +381,7 @@ export default {
return: () => this.handleReturn(row),
void: () => this.handleVoid(row),
print: () => this.handlePrint(row),
invoiceRegister: () => this.openInvoiceRegisterDialog(row),
};
(handlers[type] || (() => {}))();
},
@@ -596,6 +681,58 @@ export default {
this.paymentDialog.loading = false;
}
},
// 先票后款路径B:发票登记——认领收票单(claim-invoices 走增量认领并同步收票侧关系)
openInvoiceRegisterDialog(row) {
this.invoiceRegisterDialog.row = row || {};
this.invoiceRegisterDialog.candidates = [];
this.invoiceRegisterDialog.visible = true;
},
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.invoiceAmount || 0),
}));
this.invoiceRegisterDialog.saving = true;
try {
await api.claimInvoices({ formalSettlementId: settlement.id, invoices });
this.invoiceRegisterDialog.visible = false;
this.onLoad(this.page);
try {
await this.$confirm('是否将发票匹配数据推送金蝶?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
} catch {
this.$message.info('已保存认领,未推送金蝶');
return;
}
const data = this.unwrapData(await api.invoiceAssignKingdee(settlement.id));
this.$message.success(data || '发票登记推送金蝶成功');
this.onLoad(this.page);
} finally {
this.invoiceRegisterDialog.saving = false;
}
},
handlePrint(row) {
if (!row) {
row = this.selectedOne('打印');