Files
tms-erp-web/src/views/settlement/formal-settlement.vue
T

899 lines
35 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<basic-container class="formal-page">
<el-tabs
v-if="!lockedSettlementType"
type="border-card"
v-model="activeSettlementType"
@tab-change="handleSettlementTypeChange"
>
<el-tab-pane label="应付" name="payable">
<formal-settlement-table-panel
v-if="activeSettlementType === 'payable'"
key="payable"
v-bind="tablePanelProps"
v-on="tablePanelListeners"
/>
</el-tab-pane>
<el-tab-pane label="应收" name="receivable">
<formal-settlement-table-panel
v-if="activeSettlementType === 'receivable'"
key="receivable"
v-bind="tablePanelProps"
v-on="tablePanelListeners"
/>
</el-tab-pane>
</el-tabs>
<formal-settlement-table-panel
v-else
key="locked"
v-bind="tablePanelProps"
v-on="tablePanelListeners"
/>
<formal-settlement-editor
v-model="editor.visible"
:record-id="editor.id"
:readonly="editor.readonly"
@success="onLoad(page)"
/>
<el-dialog
v-model="paymentDialog.visible"
title="付款申请"
width="520px"
append-to-body
class="formal-payment-dialog"
>
<section-card title="付款信息">
<template v-if="paymentDialog.rows.length > 1">
<div class="formal-payment-dialog__batch-tip">
已选择 {{ paymentDialog.rows.length }} 条同合同付款结算单,请确认各单申请金额。
</div>
<el-table :data="paymentDialog.rows" border>
<el-table-column prop="formalSettlementNo" label="结算单号" min-width="160" />
<el-table-column label="剩余可申请金额" min-width="150" align="right">
<template #default="{ row }">{{
formatMoney(paymentRemaining(row), row.currency)
}}</template>
</el-table-column>
<el-table-column label="申请付款金额" min-width="150" align="right">
<template #default="{ row }">
<el-input-number
v-model="row.appliedAmount"
:min="0.01"
:max="paymentRemaining(row)"
:precision="2"
:controls="false"
/>
</template>
</el-table-column>
</el-table>
</template>
<el-form v-else :model="paymentDialog.form" label-position="right" label-width="auto">
<el-form-item label="结算单号">{{
paymentDialog.row.formalSettlementNo || '-'
}}</el-form-item>
<el-form-item label="剩余可申请金额">{{
formatMoney(paymentAvailable, paymentDialog.row.currency)
}}</el-form-item>
<el-form-item label="申请付款金额" required
><el-input-number
v-model="paymentDialog.form.appliedAmount"
:min="0.01"
:max="paymentAvailable"
:precision="2"
:controls="false"
/></el-form-item>
<el-form-item label="备注"
><el-input
v-model="paymentDialog.form.remark"
type="textarea"
maxlength="200"
show-word-limit
/></el-form-item>
</el-form>
</section-card>
<template #footer
><el-button @click="paymentDialog.visible = false">取消</el-button
><el-button type="success" :loading="paymentDialog.loading" @click="submitPayment"
>提交</el-button
></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="项目名称">
{{ invoiceRegisterDialog.row.projectName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="所属组织">
{{ invoiceRegisterDialog.row.deptName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="合同编号">
{{ invoiceRegisterDialog.row.contractNo || '-' }}
</el-descriptions-item>
<el-descriptions-item label="付款方">
{{ invoiceRegisterDialog.row.payerName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="收款方">
{{ invoiceRegisterDialog.row.payeeName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="结算类型">
{{ settlementTypeName(invoiceRegisterDialog.row.settlementType) }}
</el-descriptions-item>
<el-descriptions-item label="业务模式">
{{ invoiceRegisterDialog.row.businessModelName || '-' }}
</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="创建人">
{{ invoiceRegisterDialog.row.createUserName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ invoiceRegisterDialog.row.createTime || '-' }}
</el-descriptions-item>
<el-descriptions-item label="金蝶同步状态" :span="2">
{{ kingdeeSyncName(invoiceRegisterDialog.row.kingdeeSyncStatus) }}
</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">
{{ invoiceRegisterDialog.row.remark || '-' }}
</el-descriptions-item>
</el-descriptions>
</section-card>
<section-card title="认领收票单">
<div class="invoice-register-dialog__actions">
<el-button
type="primary"
plain
:disabled="invoiceRegisterDialog.saving"
@click="candidatesDialog.visible = true"
>选择收票单</el-button
>
</div>
<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="160" align="right">
<template #default="{ row }">
<!-- 匹配金额支持手动修改(默认=发票金额全额),已认领回显行只读 -->
<el-input-number
v-if="!row.claimed"
v-model="row.matchedAmount"
:min="0"
:max="Number(row.invoiceAmount || 0)"
:precision="2"
:controls="false"
/>
<span v-else>{{ formatMoney(row.matchedAmount || row.invoiceAmount) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="90" fixed="right" align="center">
<template #default="{ row, $index }">
<!-- 已认领票回显:认领保存为增量逻辑不支持移除,仅新增票可移除 -->
<el-tag v-if="row.claimed" size="small">已认领</el-tag>
<el-link
v-else
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>
<script>
import { mapGetters } from 'vuex';
import { createSettlementTransfer } from '@/utils/settlement-transfer';
import {
hasSettlementPermission,
settlementBillLabel,
settlementListPath,
} from '@/utils/settlement-menu';
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 * 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, InvoiceReceiptCandidatesDialog },
mixins: [organizationSearch],
props: {
/** 菜单拆分后锁定结算类型:payable=付款结算单,receivable=收款结算单 */
lockedSettlementType: {
type: String,
default: '',
validator: value => ['', 'payable', 'receivable'].includes(value),
},
},
data() {
return {
query: {},
activeSettlementType: 'payable',
rows: [],
selection: [],
loading: false,
approveLoading: false,
syncLoading: false,
page: { currentPage: 1, pageSize: 10, pageSizes: [10, 20, 50, 100], total: 0 },
editor: { visible: false, id: null, readonly: false },
paymentDialog: {
visible: false,
loading: false,
row: {},
rows: [],
form: { appliedAmount: 0, remark: '' },
},
// 先票后款路径B:发票登记(关联收票单 + 确认推送指定应付)
invoiceRegisterDialog: {
visible: false,
saving: false,
row: {},
candidates: [],
},
candidatesDialog: {
visible: false,
},
mkEnabled: false,
// keep-alive 多标签下仅激活实例响应 detailNo,避免缓存页重复弹窗
isPageActive: true,
};
},
computed: {
...mapGetters(['permission', 'userInfo']),
// 列随结算类型切换重建:收款结算单不展示「已指定应付」
option() {
return formalSettlementOption(this.activeSettlementType);
},
isAdmin() {
return isAdministrator(this.userInfo);
},
billLabel() {
return settlementBillLabel(this.activeSettlementType);
},
hasPermissionFn() {
return actionOrCode => {
const action = String(actionOrCode || '').replace(
/^(payment_settlement_|receipt_settlement_|formal_settlement_)/,
''
);
return hasSettlementPermission(this, this.activeSettlementType, action);
};
},
tablePanelProps() {
return {
option: this.option,
rows: this.rows,
loading: this.loading,
page: this.page,
hasPermission: this.hasPermissionFn,
syncLoading: this.syncLoading,
mkEnabled: this.mkEnabled,
showPaymentAction: this.activeSettlementType === 'payable',
billLabel: this.billLabel,
};
},
tablePanelListeners() {
return {
create: this.openCreate,
payment: this.openPaymentDialog,
sync: this.handleSync,
export: this.handleExport,
refresh: () => this.onLoad(this.page),
load: this.onLoad,
action: this.onAction,
'search-change': this.searchChange,
'search-reset': this.searchReset,
'update:selection': selection => {
this.selection = selection;
},
};
},
paymentAvailable() {
return this.paymentRemaining(this.paymentDialog.row);
},
},
watch: {
lockedSettlementType: {
immediate: true,
handler(value) {
if (value === 'payable' || value === 'receivable') {
this.activeSettlementType = value;
}
},
},
activeSettlementType() {
// 切换结算类型后由新挂载的 avue-crud 触发 on-load,此处只重置分页,避免重复请求
this.page.currentPage = 1;
},
'$route.query.detailNo'(detailNo) {
if (detailNo) this.openRouteDetail(detailNo);
},
},
created() {
this.loadOrganizationOptions();
},
mounted() {
this.isPageActive = true;
this.openRouteDetail();
loadMkEnabled()
.then(enabled => {
this.mkEnabled = !!enabled;
})
.catch(() => {
this.mkEnabled = false;
});
},
activated() {
this.isPageActive = true;
if (sessionStorage.getItem('formal-settlement-list-refresh') === '1') {
sessionStorage.removeItem('formal-settlement-list-refresh');
this.onLoad(this.page);
}
// 从其他页带 detailNo 跳回时,激活态补开详情
if (this.$route.query.detailNo) {
this.openRouteDetail();
}
},
deactivated() {
this.isPageActive = false;
},
methods: {
hasPermission(actionOrCode) {
return this.hasPermissionFn(actionOrCode);
},
async onLoad(page) {
this.loading = true;
try {
const params = this.buildQueryParams();
const response = await api.getList(page.currentPage, page.pageSize, params);
const data = this.unwrapData(response);
this.rows = data.records || [];
this.page.total = data.total || 0;
} finally {
this.loading = false;
}
},
searchChange(params, done) {
// 所属组织级联返回 id 路径,转成组织名称传给后端
this.normalizeOrganizationSearch(params, 'deptName');
const range = params.createDateRange || [];
delete params.createDateRange;
this.query = {
...params,
createStartDate: range[0] || '',
createEndDate: range[1] || '',
};
this.page.currentPage = 1;
this.onLoad(this.page);
done();
},
searchReset() {
this.query = {};
this.page.currentPage = 1;
this.onLoad(this.page);
},
onAction({ type, row }) {
const handlers = {
view: () => this.openView(row),
edit: () => this.openEdit(row),
delete: () => this.handleDelete(row),
submit: () => this.handleSubmit(row),
mkAudit: () => this.handleMkAudit(row),
approve: () => this.handleApprove(row),
return: () => this.handleReturn(row),
void: () => this.handleVoid(row),
print: () => this.handlePrint(row),
invoiceRegister: () => this.openInvoiceRegisterDialog(row),
};
(handlers[type] || (() => {}))();
},
openCreate() {
// 按列表锁定的结算类型路由到对应表单入口,保证菜单高亮与关闭回落列表正确
this.$router.push({
path: settlementListPath(this.activeSettlementType) + '/form',
query: {
mode: 'add',
name: `新增${this.billLabel}`,
settlementType: this.activeSettlementType,
listPath: settlementListPath(this.activeSettlementType),
},
});
},
openEdit(row) {
const settlementType = row.settlementType || this.activeSettlementType;
// 行类型可能与列表锁定类型不同,按行类型路由
this.$router.push({
path: settlementListPath(settlementType) + '/form',
query: {
mode: 'edit',
id: row.id,
name: `编辑${settlementBillLabel(settlementType)}`,
settlementType,
listPath: settlementListPath(settlementType),
},
});
},
openView(row) {
this.editor = { visible: true, id: row.id, readonly: true };
},
async openRouteDetail(detailNo = this.$route.query.detailNo) {
const formalSettlementNo = String(detailNo || '').trim();
if (!formalSettlementNo || !this.isPageActive) return;
const response = await api.getList(1, 10, { formalSettlementNo });
const data = this.unwrapData(response);
const row = (data.records || []).find(item => item.formalSettlementNo === formalSettlementNo);
if (!row) {
this.$message.warning(`未找到对应的${this.billLabel}`);
return;
}
// 锁定菜单(付款/收款)下若单号属于另一类型,跳到对应列表页再打开
if (
this.lockedSettlementType &&
row.settlementType &&
row.settlementType !== this.lockedSettlementType
) {
await this.$router.replace({
path: settlementListPath(row.settlementType),
query: { detailNo: formalSettlementNo },
});
return;
}
if (!this.lockedSettlementType) {
this.activeSettlementType = row.settlementType || this.activeSettlementType;
}
this.openView(row);
},
async handleDelete(row) {
await this.$confirm(`确认删除该${this.billLabel}草稿?`, '提示', { type: 'warning' });
await api.remove(row.id);
this.$message.success('删除成功');
this.onLoad(this.page);
},
async handleSubmit(row) {
await this.$confirm('提交后来源预付结算单将保持锁定,确认提交?', '提示', { type: 'warning' });
const previousApprovalStatus = row.approvalStatus || '';
await api.submit({ id: row.id });
await submitMkApprovalFlow({
bizType: 'formal-settlement',
bizId: row.id,
subjectName: row.formalSettlementNo || '',
approvalStatus: previousApprovalStatus,
mkEnabled: this.mkEnabled,
});
this.$message.success('提交成功');
this.onLoad(this.page);
},
handleMkAudit(row) {
if (!row?.id) {
this.$message.warning(`${this.billLabel}ID为空,无法打开审核页`);
return;
}
getMKApprovalUrl({ bizId: String(row.id) })
.then(res => {
const url = res?.data?.data;
if (!url) {
this.$message.error('获取审核跳转地址失败');
return;
}
window.open(url, '_blank');
})
.catch(() => {
this.$message.error('获取审核跳转地址失败');
});
},
async handleApprove(row) {
// 防止重复点击:请求进行中忽略后续点击,避免重复审批和重复提示
if (this.approveLoading) return;
this.approveLoading = true;
let loading = null;
try {
await this.$confirm(
`审核通过后${this.billLabel}生效,金额只能通过结算调整单处理,确认通过?`,
'提示',
{ type: 'warning' }
);
loading = this.$loading({
lock: true,
text: '审批处理中,请稍候…',
background: 'rgba(255, 255, 255, 0.6)',
});
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.onLoad(this.page);
} finally {
loading?.close();
this.approveLoading = false;
}
},
async handleReturn(row) {
const { value } = await this.$prompt('请输入驳回原因', '审批驳回', {
inputValidator: value => Boolean(value?.trim()) || '请输入驳回原因',
});
await api.returnBill({ id: row.id, reason: value });
this.$message.success('已驳回');
this.onLoad(this.page);
},
async handleVoid(row) {
const { value } = await this.$prompt('请输入作废原因', `作废${this.billLabel}`, {
inputValidator: value => Boolean(value?.trim()) || '请输入作废原因',
});
await api.voidBill({ id: row.id, reason: value });
this.$message.success('已作废');
this.onLoad(this.page);
},
selectedOne(action) {
if (this.selection.length !== 1) {
this.$message.warning(`${action}需选择一条${this.billLabel}`);
return null;
}
return this.selection[0];
},
async handleSync() {
if (this.syncLoading) return;
const rows = (this.selection || []).filter(row => row.approvalStatus === 'approved');
if (!rows.length) {
this.$message.warning(`请选择至少一条审批通过的${this.billLabel}`);
return;
}
this.syncLoading = true;
try {
let successCount = 0;
let failCount = 0;
let lastFailReason = '';
for (const row of rows) {
try {
await api.syncKingdee(row.id, { meta: { suppressErrorMessage: true } });
successCount += 1;
} catch (error) {
failCount += 1;
lastFailReason = String(error?.message || error || '未知错误').trim() || '未知错误';
}
}
await this.onLoad(this.page);
if (rows.length === 1) {
if (failCount) {
await this.$alert(`同步失败,${lastFailReason}`, '同步结果', {
type: 'error',
confirmButtonText: '知道了',
});
} else {
let message = '同步成功';
const freshRow = (this.rows || []).find(item => String(item.id) === String(rows[0].id));
if (freshRow && ['pending', 'partial'].includes(freshRow.attachmentSyncStatus)) {
message += '\n该单附件未全部上传,可重推补传';
}
await this.$alert(message, '同步结果', {
type: 'success',
confirmButtonText: '知道了',
});
}
return;
}
let batchMessage = `完成同步${successCount}条,失败${failCount}条`;
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) {
batchMessage += `\n其中${pendingCount}单附件未全部上传,可重推补传`;
}
await this.$alert(batchMessage, '同步结果', {
type: failCount ? 'warning' : 'success',
confirmButtonText: '知道了',
});
} finally {
this.syncLoading = false;
}
},
async openPaymentDialog() {
if (!this.selection.length) return this.$message.warning(`请至少选择一条${this.billLabel}`);
const rows = this.selection;
if (rows.some(row => row.approvalStatus !== 'approved' || row.settlementType !== 'payable'))
return this.$message.warning('仅审批通过的付款结算单允许发起付款申请');
if (rows.some(row => row.adjustStatus === 'adjusted'))
return this.$message.warning('已调整的结算单不可再发起付款,请使用调整后的新单');
const contractIds = new Set(rows.map(row => String(row.contractId || '')));
if (contractIds.size !== 1 || contractIds.has(''))
return this.$message.warning('付款申请必须选择同一个合同的付款结算单');
const paymentRows = rows.map(row => ({ ...row, appliedAmount: this.paymentRemaining(row) }));
if (paymentRows.some(row => row.appliedAmount <= 0))
return this.$message.warning('所选付款结算单均须存在剩余可申请金额');
try {
const sourceFormalSettlements = await Promise.all(
paymentRows.map(async row => {
const [amountResponse, detailResponse] = await Promise.all([
paymentApi.getReferenceAmount('settlement_payment', row.id),
api.getDetail(row.id),
]);
const amount = this.unwrapData(amountResponse) || {};
const detail = this.unwrapData(detailResponse) || {};
const remainingAmount = Number(amount.payableAmount ?? this.paymentRemaining(row));
return {
...row,
id: row.id,
settlementId: row.id,
projectId: detail.projectId ?? row.projectId,
projectName: detail.projectName || row.projectName,
deptId: detail.deptId ?? row.deptId,
deptName: detail.deptName || row.deptName,
contractId: detail.contractId ?? row.contractId,
contractNo: detail.contractNo || row.contractNo,
contractName: detail.contractName || row.contractName,
payerName: detail.payerName || row.payerName,
payeeName: detail.payeeName || row.payeeName,
formalSettlementNo: detail.formalSettlementNo || row.formalSettlementNo,
settlementAmount: Number(
amount.settlementAmount ?? detail.settlementAmount ?? row.settlementAmount ?? 0
),
payableAmount: remainingAmount,
appliedAmount: remainingAmount,
invoices: detail.invoices || [],
paymentRecords: detail.paymentRecords || detail.paymentApplications || [],
};
})
);
if (sourceFormalSettlements.some(row => row.appliedAmount <= 0)) {
this.$message.warning('所选付款结算单均须存在剩余可申请金额');
return;
}
const transferToken = createSettlementTransfer({
sourceFormalSettlements,
settlementType: 'payable',
});
await this.$router.push({
path: '/payment/payment-application/form',
query: { mode: 'add', paymentType: 'settlement_payment', transferToken },
});
this.selection = [];
} catch (error) {
this.$message.error('结算信息加载失败,请稍后重试');
}
},
async submitPayment() {
const paymentRows = this.paymentDialog.rows;
if (paymentRows.length > 1) {
if (paymentRows.some(row => Number(row.appliedAmount || 0) <= 0))
return this.$message.warning('请输入各付款结算单的申请付款金额');
if (paymentRows.some(row => Number(row.appliedAmount) > this.paymentRemaining(row)))
return this.$message.warning('申请付款金额不能超过剩余可申请金额');
} else if (Number(this.paymentDialog.form.appliedAmount || 0) <= 0) {
return this.$message.warning('请输入申请付款金额');
}
this.paymentDialog.loading = true;
try {
const response =
paymentRows.length > 1
? await api.applyPayments({
items: paymentRows.map(row => ({ id: row.id, appliedAmount: row.appliedAmount })),
remark: this.paymentDialog.form.remark,
})
: await api.applyPayment({ id: this.paymentDialog.row.id, ...this.paymentDialog.form });
const data = this.unwrapData(response);
this.$message.success(
Array.isArray(data) ? `已生成 ${data.length} 条付款申请` : `付款申请已生成:${data}`
);
this.paymentDialog.visible = false;
this.selection = [];
this.onLoad(this.page);
} finally {
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('打印');
if (!row) return;
}
const win = window.open('', '_blank');
if (!win) return this.$message.warning('浏览器阻止了打印窗口,请允许弹窗后重试');
win.document.write(
`<!doctype html><html><head><title>${
row.formalSettlementNo
}</title><style>body{font-family:Arial,"Microsoft YaHei",sans-serif;padding:32px;color:#222}h1{text-align:center}.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px 28px;margin-top:30px}.item{border-bottom:1px solid #ddd;padding:8px 0}.actions{text-align:center;margin-top:36px}@media print{.actions{display:none}}</style></head><body><h1>${this.escapeHtml(
settlementBillLabel(row.settlementType || this.activeSettlementType)
)}</h1><div class="grid"><div class="item">结算单号:${this.escapeHtml(
row.formalSettlementNo
)}</div><div class="item">预付结算单号:${this.escapeHtml(
row.preSettlementNos
)}</div><div class="item">项目:${this.escapeHtml(
row.projectName
)}</div><div class="item">合同编号:${this.escapeHtml(
row.contractNo
)}</div><div class="item">合同名称:${this.escapeHtml(
row.contractName
)}</div><div class="item">所属组织:${this.escapeHtml(
row.deptName
)}</div><div class="item">付款方:${this.escapeHtml(
row.payerName
)}</div><div class="item">收款方:${this.escapeHtml(
row.payeeName
)}</div><div class="item">结算金额:${this.formatMoney(
row.settlementAmount,
row.currency
)}</div></div><div class="actions"><button onclick="window.print()">打印</button></div></body></html>`
);
win.document.close();
},
async handleExport() {
const params = this.buildQueryParams();
if (this.selection.length) params.ids = this.selection.map(item => item.id).join(',');
const { data } = await api.exportList(params);
downloadFile(
data,
`${this.billLabel}${this.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.xlsx`
);
},
buildQueryParams() {
const params = { ...this.query };
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;
},
handleSettlementTypeChange() {
// 分页重置与数据加载由 activeSettlementType watcher + 新挂载 crud 的 on-load 完成
this.selection = [];
},
escapeHtml(value) {
return String(value || '-').replace(
/[&<>"']/g,
char => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[char])
);
},
formatMoney(value, currency = 'RMB') {
return `${Number(value || 0).toFixed(2)} ${currency || 'RMB'}`;
},
paymentRemaining(row) {
const remaining = Number(row?.settlementAmount || 0) - Number(row?.paidAmount || 0);
return Math.max(0, Number(remaining || 0));
},
},
};
</script>
<style scoped lang="scss">
.formal-page {
:deep(.el-table) {
--el-table-border-color: #eff1f7;
}
:deep(.el-table__body tr:nth-child(even) > td.el-table__cell),
:deep(.el-table__body tr:nth-child(even) > td.el-table-fixed-column--left),
:deep(.el-table__body tr:nth-child(even) > td.el-table-fixed-column--right) {
background: #fafafa;
}
}
.formal-payment-dialog__batch-tip {
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;
}
</style>