Merge remote-tracking branch 'origin/master' into feature/20261016-settlement-adjustment-kingdee
This commit is contained in:
@@ -52,53 +52,32 @@
|
||||
|
||||
<section class="formal-page__table-panel">
|
||||
<el-tabs
|
||||
v-if="!lockedSettlementType"
|
||||
type="border-card"
|
||||
v-model="activeSettlementType"
|
||||
@tab-change="handleSettlementTypeChange"
|
||||
>
|
||||
<el-tab-pane label="应付" name="payable">
|
||||
<formal-settlement-table-panel
|
||||
:key="activeSettlementType"
|
||||
:rows="rows"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:page="page"
|
||||
:has-permission="hasPermissionFn"
|
||||
:sync-loading="syncLoading"
|
||||
:mk-enabled="mkEnabled"
|
||||
v-model:search-visible="searchVisible"
|
||||
@create="openCreate"
|
||||
@payment="openPaymentDialog"
|
||||
@sync="handleSync"
|
||||
@export="handleExport"
|
||||
@refresh="loadTable"
|
||||
@page-change="onPageChange"
|
||||
@action="onAction"
|
||||
@update:selection="selection = $event"
|
||||
:key="'payable'"
|
||||
v-bind="tablePanelProps"
|
||||
v-on="tablePanelListeners"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="应收" name="receivable">
|
||||
<formal-settlement-table-panel
|
||||
:key="activeSettlementType"
|
||||
:rows="rows"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:page="page"
|
||||
:has-permission="hasPermissionFn"
|
||||
:sync-loading="syncLoading"
|
||||
:mk-enabled="mkEnabled"
|
||||
v-model:search-visible="searchVisible"
|
||||
@create="openCreate"
|
||||
@payment="openPaymentDialog"
|
||||
@sync="handleSync"
|
||||
@export="handleExport"
|
||||
@refresh="loadTable"
|
||||
@page-change="onPageChange"
|
||||
@action="onAction"
|
||||
@update:selection="selection = $event"
|
||||
:key="'receivable'"
|
||||
v-bind="tablePanelProps"
|
||||
v-on="tablePanelListeners"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<formal-settlement-table-panel
|
||||
v-else
|
||||
:key="activeSettlementType"
|
||||
v-bind="tablePanelProps"
|
||||
v-on="tablePanelListeners"
|
||||
/>
|
||||
</section>
|
||||
<formal-settlement-editor
|
||||
v-model="editor.visible"
|
||||
@@ -116,7 +95,7 @@
|
||||
<section-card title="付款信息">
|
||||
<template v-if="paymentDialog.rows.length > 1">
|
||||
<div class="formal-payment-dialog__batch-tip">
|
||||
已选择 {{ paymentDialog.rows.length }} 条同合同应付正式结算单,请确认各单申请金额。
|
||||
已选择 {{ paymentDialog.rows.length }} 条同合同付款结算单,请确认各单申请金额。
|
||||
</div>
|
||||
<el-table :data="paymentDialog.rows" border>
|
||||
<el-table-column prop="formalSettlementNo" label="结算单号" min-width="160" />
|
||||
@@ -176,6 +155,12 @@
|
||||
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
|
||||
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';
|
||||
@@ -210,6 +195,14 @@ export default {
|
||||
name: 'FormalSettlement',
|
||||
components: { FormalSettlementEditor, FormalSettlementTablePanel },
|
||||
mixins: [organizationSearch],
|
||||
props: {
|
||||
/** 菜单拆分后锁定结算类型:payable=付款结算单,receivable=收款结算单 */
|
||||
lockedSettlementType: {
|
||||
type: String,
|
||||
default: '',
|
||||
validator: value => ['', 'payable', 'receivable'].includes(value),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ArrowDown,
|
||||
@@ -237,9 +230,52 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission']),
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
billLabel() {
|
||||
return settlementBillLabel(this.activeSettlementType);
|
||||
},
|
||||
hasPermissionFn() {
|
||||
return code => this.permission?.[code] !== false;
|
||||
return actionOrCode => {
|
||||
const action = String(actionOrCode || '').replace(
|
||||
/^(payment_settlement_|receipt_settlement_|formal_settlement_)/,
|
||||
''
|
||||
);
|
||||
return hasSettlementPermission(this, this.activeSettlementType, action);
|
||||
};
|
||||
},
|
||||
tablePanelProps() {
|
||||
return {
|
||||
rows: this.rows,
|
||||
columns: this.columns,
|
||||
loading: this.loading,
|
||||
page: this.page,
|
||||
hasPermission: this.hasPermissionFn,
|
||||
syncLoading: this.syncLoading,
|
||||
mkEnabled: this.mkEnabled,
|
||||
searchVisible: this.searchVisible,
|
||||
showPaymentAction: this.activeSettlementType === 'payable',
|
||||
billLabel: this.billLabel,
|
||||
};
|
||||
},
|
||||
tablePanelListeners() {
|
||||
return {
|
||||
create: this.openCreate,
|
||||
payment: this.openPaymentDialog,
|
||||
sync: this.handleSync,
|
||||
export: this.handleExport,
|
||||
refresh: this.loadTable,
|
||||
'page-change': this.onPageChange,
|
||||
action: this.onAction,
|
||||
'update:selection': selection => {
|
||||
this.selection = selection;
|
||||
},
|
||||
'update:searchVisible': visible => {
|
||||
this.searchVisible = visible;
|
||||
},
|
||||
};
|
||||
},
|
||||
visibleSearchFields() {
|
||||
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
|
||||
@@ -249,6 +285,14 @@ export default {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
lockedSettlementType: {
|
||||
immediate: true,
|
||||
handler(value) {
|
||||
if (value === 'payable' || value === 'receivable') {
|
||||
this.activeSettlementType = value;
|
||||
}
|
||||
},
|
||||
},
|
||||
'$route.query.detailNo'(detailNo) {
|
||||
if (detailNo) this.openRouteDetail(detailNo);
|
||||
},
|
||||
@@ -258,6 +302,9 @@ export default {
|
||||
this.ownFullPath = this.$route.fullPath;
|
||||
},
|
||||
mounted() {
|
||||
if (this.lockedSettlementType === 'payable' || this.lockedSettlementType === 'receivable') {
|
||||
this.activeSettlementType = this.lockedSettlementType;
|
||||
}
|
||||
this.loadOrganizationOptions();
|
||||
this.loadTable();
|
||||
this.openRouteDetail();
|
||||
@@ -276,8 +323,8 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hasPermission(code) {
|
||||
return this.permission?.[code] !== false;
|
||||
hasPermission(actionOrCode) {
|
||||
return this.hasPermissionFn(actionOrCode);
|
||||
},
|
||||
async loadTable() {
|
||||
this.loading = true;
|
||||
@@ -330,15 +377,23 @@ export default {
|
||||
path: '/settlement/formal-settlement/form',
|
||||
query: {
|
||||
mode: 'add',
|
||||
name: '新增正式结算',
|
||||
name: `新增${this.billLabel}`,
|
||||
settlementType: this.activeSettlementType,
|
||||
listPath: settlementListPath(this.activeSettlementType),
|
||||
},
|
||||
});
|
||||
},
|
||||
openEdit(row) {
|
||||
const settlementType = row.settlementType || this.activeSettlementType;
|
||||
this.$router.push({
|
||||
path: '/settlement/formal-settlement/form',
|
||||
query: { mode: 'edit', id: row.id, name: '编辑正式结算' },
|
||||
query: {
|
||||
mode: 'edit',
|
||||
id: row.id,
|
||||
name: `编辑${settlementBillLabel(settlementType)}`,
|
||||
settlementType,
|
||||
listPath: settlementListPath(settlementType),
|
||||
},
|
||||
});
|
||||
},
|
||||
openView(row) {
|
||||
@@ -351,20 +406,22 @@ export default {
|
||||
const data = this.unwrapData(response);
|
||||
const row = (data.records || []).find(item => item.formalSettlementNo === formalSettlementNo);
|
||||
if (!row) {
|
||||
this.$message.warning('未找到对应的正式结算单');
|
||||
this.$message.warning(`未找到对应的${this.billLabel}`);
|
||||
return;
|
||||
}
|
||||
this.activeSettlementType = row.settlementType || this.activeSettlementType;
|
||||
if (!this.lockedSettlementType) {
|
||||
this.activeSettlementType = row.settlementType || this.activeSettlementType;
|
||||
}
|
||||
this.openView(row);
|
||||
},
|
||||
async handleDelete(row) {
|
||||
await this.$confirm('确认删除该正式结算草稿?', '提示', { type: 'warning' });
|
||||
await this.$confirm(`确认删除该${this.billLabel}草稿?`, '提示', { type: 'warning' });
|
||||
await api.remove(row.id);
|
||||
this.$message.success('删除成功');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleSubmit(row) {
|
||||
await this.$confirm('提交后来源预结算将保持锁定,确认提交?', '提示', { type: 'warning' });
|
||||
await this.$confirm('提交后来源预付结算单将保持锁定,确认提交?', '提示', { type: 'warning' });
|
||||
const previousApprovalStatus = row.approvalStatus || '';
|
||||
await api.submit({ id: row.id });
|
||||
await submitMkApprovalFlow({
|
||||
@@ -379,7 +436,7 @@ export default {
|
||||
},
|
||||
handleMkAudit(row) {
|
||||
if (!row?.id) {
|
||||
this.$message.warning('正式结算单ID为空,无法打开审核页');
|
||||
this.$message.warning(`${this.billLabel}ID为空,无法打开审核页`);
|
||||
return;
|
||||
}
|
||||
getMKApprovalUrl({ bizId: String(row.id) })
|
||||
@@ -397,7 +454,7 @@ export default {
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await this.$confirm(
|
||||
'审核通过后正式结算生效,金额只能通过结算调整单处理,确认通过?',
|
||||
`审核通过后${this.billLabel}生效,金额只能通过结算调整单处理,确认通过?`,
|
||||
'提示',
|
||||
{ type: 'warning' }
|
||||
);
|
||||
@@ -419,7 +476,7 @@ export default {
|
||||
this.loadTable();
|
||||
},
|
||||
async handleVoid(row) {
|
||||
const { value } = await this.$prompt('请输入作废原因', '作废正式结算单', {
|
||||
const { value } = await this.$prompt('请输入作废原因', `作废${this.billLabel}`, {
|
||||
inputValidator: value => Boolean(value?.trim()) || '请输入作废原因',
|
||||
});
|
||||
await api.voidBill({ id: row.id, reason: value });
|
||||
@@ -428,46 +485,80 @@ export default {
|
||||
},
|
||||
selectedOne(action) {
|
||||
if (this.selection.length !== 1) {
|
||||
this.$message.warning(`${action}需选择一条正式结算单`);
|
||||
this.$message.warning(`${action}需选择一条${this.billLabel}`);
|
||||
return null;
|
||||
}
|
||||
return this.selection[0];
|
||||
},
|
||||
async handleSync() {
|
||||
if (this.syncLoading) return;
|
||||
const row = this.selectedOne('同步金蝶');
|
||||
if (!row) return;
|
||||
|
||||
const rows = (this.selection || []).filter(row => row.approvalStatus === 'approved');
|
||||
if (!rows.length) {
|
||||
this.$message.warning(`请选择至少一条审批通过的${this.billLabel}`);
|
||||
return;
|
||||
}
|
||||
this.syncLoading = true;
|
||||
try {
|
||||
const response = await api.syncKingdee(row.id);
|
||||
const data = this.unwrapData(response);
|
||||
this.$message.success(`同步成功,金蝶单据号:${data}`);
|
||||
await this.loadTable();
|
||||
// 附件联带同步提示:同步成功但附件未全部上传时追加 warning,重推即自动补传
|
||||
const freshRow = (this.rows || []).find(item => String(item.id) === String(row.id));
|
||||
if (freshRow && ['pending', 'partial'].includes(freshRow.attachmentSyncStatus)) {
|
||||
this.$message({
|
||||
type: 'warning',
|
||||
message: '该单附件未全部上传,可重推补传',
|
||||
duration: 8000,
|
||||
});
|
||||
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.loadTable();
|
||||
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('请至少选择一条正式结算单');
|
||||
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('仅审批通过的应付正式结算单允许发起付款申请');
|
||||
return this.$message.warning('仅审批通过的付款结算单允许发起付款申请');
|
||||
const contractIds = new Set(rows.map(row => String(row.contractId || '')));
|
||||
if (contractIds.size !== 1 || contractIds.has(''))
|
||||
return this.$message.warning('付款申请必须选择同一个合同的正式结算单');
|
||||
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('所选正式结算单均须存在剩余可申请金额');
|
||||
return this.$message.warning('所选付款结算单均须存在剩余可申请金额');
|
||||
try {
|
||||
const sourceFormalSettlements = await Promise.all(
|
||||
paymentRows.map(async row => {
|
||||
@@ -503,7 +594,7 @@ export default {
|
||||
})
|
||||
);
|
||||
if (sourceFormalSettlements.some(row => row.appliedAmount <= 0)) {
|
||||
this.$message.warning('所选正式结算单均须存在剩余可申请金额');
|
||||
this.$message.warning('所选付款结算单均须存在剩余可申请金额');
|
||||
return;
|
||||
}
|
||||
const transferToken = createSettlementTransfer({
|
||||
@@ -523,7 +614,7 @@ export default {
|
||||
const paymentRows = this.paymentDialog.rows;
|
||||
if (paymentRows.length > 1) {
|
||||
if (paymentRows.some(row => Number(row.appliedAmount || 0) <= 0))
|
||||
return this.$message.warning('请输入各正式结算单的申请付款金额');
|
||||
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) {
|
||||
@@ -559,9 +650,11 @@ export default {
|
||||
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>正式结算单</h1><div class="grid"><div class="item">结算单号:${this.escapeHtml(
|
||||
}</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(
|
||||
)}</div><div class="item">预付结算单号:${this.escapeHtml(
|
||||
row.preSettlementNos
|
||||
)}</div><div class="item">项目:${this.escapeHtml(
|
||||
row.projectName
|
||||
@@ -586,12 +679,15 @@ export default {
|
||||
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.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.xlsx`);
|
||||
downloadFile(
|
||||
data,
|
||||
`${this.billLabel}${this.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.xlsx`
|
||||
);
|
||||
},
|
||||
buildQueryParams() {
|
||||
// 所属组织级联返回 id 路径,转成组织名称传给后端(用副本,避免污染搜索框回显)
|
||||
const params = this.normalizeOrganizationSearch({ ...this.query }, 'deptName');
|
||||
params.settlementType = this.activeSettlementType;
|
||||
params.settlementType = this.lockedSettlementType || this.activeSettlementType;
|
||||
const range = params.createDateRange || [];
|
||||
delete params.createDateRange;
|
||||
if (range.length === 2) {
|
||||
|
||||
Reference in New Issue
Block a user