899 lines
35 KiB
Vue
899 lines
35 KiB
Vue
<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 => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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>
|