✨ 结算付款体验优化与合同/运单导入校验
付款申请汇票筛选与提示去重、结算菜单拆分与金蝶同步弹框、应收应付详情高度、合同计费与半月结调整,以及导入运单批量删除校验。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -140,7 +140,8 @@
|
||||
:step="0.01"
|
||||
:controls="false"
|
||||
style="width: 100%"
|
||||
value-on-clear="0"
|
||||
:value-on-clear="null"
|
||||
placeholder="请输入申请付款金额"
|
||||
/></el-form-item
|
||||
></el-col>
|
||||
<el-col :span="6"
|
||||
@@ -241,11 +242,22 @@
|
||||
<template #default="{ row }">{{ formatMoney(row.payableAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="paymentRatio" label="付款比例(%)" min-width="110">
|
||||
<template #default="{ row }">{{ formatMoney(row.paymentRatio) }}</template>
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'payment-form-page__ratio-exceeded': row.ratioExceeded }">
|
||||
{{ formatMoney(row.paymentRatio) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="appliedAmount" label="付款金额" min-width="130">
|
||||
<template #default="{ row }">{{ formatMoney(row.appliedAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="ratioRemark" label="备注" min-width="220" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'payment-form-page__ratio-exceeded': row.ratioExceeded }">
|
||||
{{ row.ratioRemark || '-' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section-card>
|
||||
<section-card v-if="showInvoiceSection" title="发票信息">
|
||||
@@ -409,7 +421,7 @@
|
||||
<el-button v-if="!readonly" type="primary" plain @click="syncPaymentRecords"
|
||||
>同步</el-button
|
||||
>
|
||||
<el-button v-if="!readonly && canSave" type="primary" plain @click="saveDraft()"
|
||||
<el-button v-if="!readonly && canSave" type="primary" plain @click="handleSave"
|
||||
>保存</el-button
|
||||
>
|
||||
<el-button
|
||||
@@ -483,7 +495,7 @@
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-tabs v-model="referenceTab" @tab-change="handleReferenceTabChange">
|
||||
<el-tab-pane v-if="canSelectFormalSettlement" label="正式结算单" name="formal">
|
||||
<el-tab-pane v-if="canSelectFormalSettlement" label="付款结算单" name="formal">
|
||||
<el-table
|
||||
v-loading="formalPage.loading"
|
||||
:data="formalRows"
|
||||
@@ -491,12 +503,19 @@
|
||||
@selection-change="formalSelection = $event"
|
||||
>
|
||||
<el-table-column type="selection" width="50" />
|
||||
<el-table-column prop="formalSettlementNo" label="结算单号" />
|
||||
<el-table-column prop="projectName" label="所属项目" />
|
||||
<el-table-column prop="contractName" label="合同名称" />
|
||||
<el-table-column prop="settlementAmount" label="结算金额">
|
||||
<el-table-column prop="formalSettlementNo" label="结算单号" min-width="150" />
|
||||
<el-table-column prop="projectName" label="所属项目" min-width="140" />
|
||||
<el-table-column prop="contractName" label="合同名称" min-width="160" />
|
||||
<el-table-column prop="settlementAmount" label="结算金额" min-width="130">
|
||||
<template #default="{ row }">{{ formatMoney(row.settlementAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="payableAmount" label="可付款金额" min-width="130">
|
||||
<template #default="{ row }">{{
|
||||
row.payableAmount === undefined || row.payableAmount === null
|
||||
? '-'
|
||||
: formatMoney(row.payableAmount)
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click.stop="selectFormal(row)">选择</el-link>
|
||||
@@ -515,7 +534,7 @@
|
||||
/>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane v-if="canSelectPreSettlement" label="预结算单" name="pre">
|
||||
<el-tab-pane v-if="canSelectPreSettlement" label="预付结算单" name="pre">
|
||||
<el-table
|
||||
v-loading="prePage.loading"
|
||||
:data="preRows"
|
||||
@@ -523,12 +542,19 @@
|
||||
@selection-change="preSelection = $event"
|
||||
>
|
||||
<el-table-column type="selection" width="50" />
|
||||
<el-table-column prop="preSettlementNo" label="结算单号" />
|
||||
<el-table-column prop="projectName" label="所属项目" />
|
||||
<el-table-column prop="contractName" label="合同名称" />
|
||||
<el-table-column prop="settlementAmount" label="结算金额">
|
||||
<el-table-column prop="preSettlementNo" label="结算单号" min-width="150" />
|
||||
<el-table-column prop="projectName" label="所属项目" min-width="140" />
|
||||
<el-table-column prop="contractName" label="合同名称" min-width="160" />
|
||||
<el-table-column prop="settlementAmount" label="结算金额" min-width="130">
|
||||
<template #default="{ row }">{{ formatMoney(row.settlementAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="payableAmount" label="可付款金额" min-width="130">
|
||||
<template #default="{ row }">{{
|
||||
row.payableAmount === undefined || row.payableAmount === null
|
||||
? '-'
|
||||
: formatMoney(row.payableAmount)
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click.stop="selectPre(row)">选择</el-link>
|
||||
@@ -562,22 +588,27 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="billDialogVisible" title="选择汇票" width="80%" append-to-body>
|
||||
<div class="payment-form-page__reference-search">
|
||||
<el-form :model="billQuery" label-position="right" label-width="160px" @submit.prevent>
|
||||
<div class="payment-form-page__reference-search-fields">
|
||||
<el-form-item label="汇票筛选">
|
||||
<el-input
|
||||
v-model="billQuery.keyword"
|
||||
clearable
|
||||
placeholder="请输入汇票号、出票单位或收票单位"
|
||||
@keyup.enter="handleBillSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="payment-form-page__reference-search-actions">
|
||||
<el-button type="primary" @click="handleBillSearch">搜索</el-button>
|
||||
<el-button @click="resetBillSearch">清空</el-button>
|
||||
</div>
|
||||
<div class="payment-form-page__reference-search payment-form-page__bill-search">
|
||||
<el-form
|
||||
:model="billQuery"
|
||||
inline
|
||||
label-position="right"
|
||||
label-width="88px"
|
||||
@submit.prevent
|
||||
>
|
||||
<el-form-item label="汇票筛选">
|
||||
<el-input
|
||||
v-model="billQuery.keyword"
|
||||
clearable
|
||||
style="width: 320px"
|
||||
placeholder="请输入汇票号、出票单位或收票单位"
|
||||
@keyup.enter="handleBillSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item class="payment-form-page__bill-search-actions">
|
||||
<el-button type="primary" @click="handleBillSearch">查询</el-button>
|
||||
<el-button @click="resetBillSearch">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-table v-loading="billPage.loading" :data="billRows" border>
|
||||
@@ -652,7 +683,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ElImageViewer } from 'element-plus';
|
||||
import { ElImageViewer, ElMessage } from 'element-plus';
|
||||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||
import { fallbackPlugin, imagePlugin, officePlugin, textPlugin } from '@open-file-viewer/core';
|
||||
import '@open-file-viewer/core/style.css';
|
||||
@@ -814,8 +845,12 @@ export default {
|
||||
contractSignatureFileAvailable: false,
|
||||
contractPaymentRatioLimit: null,
|
||||
contractPaymentRatioExceeded: false,
|
||||
/** 合同付款比例设置(按第1笔、第2笔…顺序) */
|
||||
contractPaymentRatioRows: [],
|
||||
payeeCustomerLevel: '',
|
||||
attachmentRuleLoading: false,
|
||||
// 最近一次从结算单同步过来的附件 URL,切换结算单时用于整批替换
|
||||
linkedSettlementAttachmentUrls: [],
|
||||
attachmentImagePreviewVisible: false,
|
||||
attachmentImagePreviewUrls: [],
|
||||
attachmentImagePreviewIndex: 0,
|
||||
@@ -860,7 +895,7 @@ export default {
|
||||
billLedgerId: [{ validator: this.validateBillLedger, trigger: 'change' }],
|
||||
settlementAmount: [{ validator: this.validateSettlementAmount, trigger: 'change' }],
|
||||
appliedAmount: [
|
||||
{ required: true, validator: this.validateAppliedAmount, trigger: 'change' },
|
||||
{ required: true, validator: this.validateAppliedAmount, trigger: ['change', 'blur'] },
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -905,8 +940,8 @@ export default {
|
||||
referenceDialogTitle() {
|
||||
if (this.form.paymentType === 'project_advance') return '选择项目合同';
|
||||
if (this.canSelectPreSettlement && this.canSelectFormalSettlement) return '选择结算单';
|
||||
if (this.canSelectPreSettlement) return '选择预结算单';
|
||||
if (this.canSelectFormalSettlement) return '选择正式结算单';
|
||||
if (this.canSelectPreSettlement) return '选择预付结算单';
|
||||
if (this.canSelectFormalSettlement) return '选择付款结算单';
|
||||
return '选择结算单';
|
||||
},
|
||||
billPayment() {
|
||||
@@ -960,10 +995,8 @@ export default {
|
||||
},
|
||||
settlementRows() {
|
||||
if (this.referenceRows.length) {
|
||||
return this.referenceRows.map(item => ({
|
||||
...item,
|
||||
paymentRatio: this.form.paymentRatio,
|
||||
appliedAmount:
|
||||
return this.referenceRows.map(item => {
|
||||
const appliedAmount =
|
||||
item.fixedAppliedAmount === true
|
||||
? Number(item.appliedAmount || 0)
|
||||
: Number(
|
||||
@@ -971,8 +1004,14 @@ export default {
|
||||
(Number(item.settlementAmount || 0) * Number(this.form.paymentRatio || 0)) /
|
||||
100
|
||||
).toFixed(2)
|
||||
),
|
||||
}));
|
||||
);
|
||||
return {
|
||||
...item,
|
||||
paymentRatio: this.form.paymentRatio,
|
||||
appliedAmount,
|
||||
...this.buildSettlementRatioHint(item),
|
||||
};
|
||||
});
|
||||
}
|
||||
if (!this.referenceLabel) return [];
|
||||
return [
|
||||
@@ -982,6 +1021,8 @@ export default {
|
||||
payableAmount: this.form.payableAmount,
|
||||
paymentRatio: this.form.paymentRatio,
|
||||
appliedAmount: this.form.appliedAmount,
|
||||
paymentSequence: 1,
|
||||
...this.buildSettlementRatioHint({ paymentSequence: 1 }),
|
||||
},
|
||||
];
|
||||
},
|
||||
@@ -1071,7 +1112,7 @@ export default {
|
||||
!this.form.preSettlementId &&
|
||||
!this.form.preSettlementIds?.length
|
||||
) {
|
||||
callback(new Error('请选择预结算单'));
|
||||
callback(new Error('请选择预付结算单'));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
@@ -1102,14 +1143,18 @@ export default {
|
||||
callback();
|
||||
},
|
||||
validateAppliedAmount(rule, value, callback) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
callback(new Error('请填写申请付款金额'));
|
||||
return;
|
||||
}
|
||||
const amount = Number(value);
|
||||
const payableAmount = Number(this.form.payableAmount || 0);
|
||||
if (value === null || value === undefined || value === '' || !Number.isFinite(amount)) {
|
||||
if (!Number.isFinite(amount)) {
|
||||
callback(new Error('请填写申请付款金额'));
|
||||
return;
|
||||
}
|
||||
if (amount <= 0) {
|
||||
callback(new Error('申请付款金额必须大于0'));
|
||||
callback(new Error('申请付款金额必须大于0,不能为0元'));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
@@ -1132,10 +1177,23 @@ export default {
|
||||
return;
|
||||
}
|
||||
const selected = this.billOptions.find(item => String(item.id) === String(value));
|
||||
if (
|
||||
selected &&
|
||||
Number(this.form.appliedAmount || 0) > Number(selected.availableBalance || 0)
|
||||
) {
|
||||
if (!selected) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (selected.billType && selected.billType !== 'received') {
|
||||
callback(new Error('汇票付款仅可选择收票类型的汇票'));
|
||||
return;
|
||||
}
|
||||
if (selected.maturityDate) {
|
||||
const maturity = this.$dayjs(selected.maturityDate).startOf('day');
|
||||
const today = this.$dayjs().startOf('day');
|
||||
if (maturity.isValid() && maturity.isBefore(today)) {
|
||||
callback(new Error('所选汇票已到期,不可选择'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (Number(this.form.appliedAmount || 0) > Number(selected.availableBalance || 0)) {
|
||||
callback(new Error('申请付款金额不能超过汇票可用余额'));
|
||||
return;
|
||||
}
|
||||
@@ -1263,6 +1321,7 @@ export default {
|
||||
await this.loadContractOptions(this.form.projectId);
|
||||
}
|
||||
await this.fillBillTypeFromContract(this.form.contractId);
|
||||
await this.refreshSettlementPaymentSequences();
|
||||
await this.loadAttachmentRuleData(await this.loadCurrentSettlementDetails());
|
||||
if (this.billPayment) await this.loadBillOptions('', data.billLedgerId);
|
||||
} else {
|
||||
@@ -1290,6 +1349,7 @@ export default {
|
||||
this.form.billType = '';
|
||||
this.contractPartyBName = '';
|
||||
this.receiptAccountOptions = [];
|
||||
this.resetContractPaymentRatioLimit();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -1301,11 +1361,13 @@ export default {
|
||||
this.form.payerName = partyA;
|
||||
}
|
||||
this.contractPartyBName = String(data?.partyB || data?.payeeName || '').trim();
|
||||
this.applyContractPaymentRatioLimit(data || {});
|
||||
await this.loadReceiptAccountOptions(autoSelectDefault);
|
||||
} catch (error) {
|
||||
this.form.billType = '';
|
||||
this.contractPartyBName = '';
|
||||
this.receiptAccountOptions = [];
|
||||
this.resetContractPaymentRatioLimit();
|
||||
}
|
||||
},
|
||||
async loadPaymentMethodOptions() {
|
||||
@@ -1400,16 +1462,33 @@ export default {
|
||||
if (!rows.length) return;
|
||||
const contractIds = new Set(rows.map(item => String(item.contractId || '')));
|
||||
if (contractIds.size !== 1 || contractIds.has('')) {
|
||||
this.$message.warning('所选正式结算单合同信息不一致,请返回后重新选择');
|
||||
this.$message.warning('所选付款结算单合同信息不一致,请返回后重新选择');
|
||||
return;
|
||||
}
|
||||
const first = rows[0];
|
||||
const referenceRows = rows.map(item => ({
|
||||
...this.createReferenceRow(item),
|
||||
settlementNo: item.formalSettlementNo || item.settlementNo || '',
|
||||
appliedAmount: Number(item.appliedAmount || 0),
|
||||
fixedAppliedAmount: true,
|
||||
}));
|
||||
const amountResponses = await Promise.all(
|
||||
rows.map(item => {
|
||||
const referenceId = item.settlementId || item.id;
|
||||
if (!this.isValidReferenceId(referenceId)) return Promise.resolve(null);
|
||||
return api
|
||||
.getReferenceAmount('settlement_payment', referenceId, this.form.id)
|
||||
.catch(() => null);
|
||||
})
|
||||
);
|
||||
const referenceRows = rows.map((item, index) => {
|
||||
const amount = this.unwrapData(amountResponses[index]) || {};
|
||||
return {
|
||||
...this.createReferenceRow({
|
||||
...item,
|
||||
settlementAmount: amount.settlementAmount ?? item.settlementAmount,
|
||||
payableAmount: amount.payableAmount ?? item.payableAmount,
|
||||
paymentSequence: amount.paymentSequence,
|
||||
}),
|
||||
settlementNo: item.formalSettlementNo || item.settlementNo || '',
|
||||
appliedAmount: Number(item.appliedAmount || 0),
|
||||
fixedAppliedAmount: true,
|
||||
};
|
||||
});
|
||||
const settlementAmount = referenceRows.reduce(
|
||||
(total, item) => total + Number(item.settlementAmount || 0),
|
||||
0
|
||||
@@ -1503,21 +1582,76 @@ export default {
|
||||
},
|
||||
createReferenceRow(row) {
|
||||
const settlementAmount = Number(row.settlementAmount || 0);
|
||||
const appliedAmount = Number(row.advanceAppliedAmount || row.appliedPaymentAmount || 0);
|
||||
const isPre = Boolean(row.preSettlementNo || row.preSettlementId || row.referenceType === 'pre');
|
||||
const paymentSequence = Number(row.paymentSequence);
|
||||
// 可付款金额必须来自 referenceAmount 接口(已扣减占用中的付款申请);缺失时置 0,避免误用未扣减结算金额
|
||||
const payableAmount =
|
||||
row.payableAmount === undefined || row.payableAmount === null
|
||||
? 0
|
||||
: Number(row.payableAmount || 0);
|
||||
return {
|
||||
id: row.preSettlementId || row.settlementId || row.id,
|
||||
settlementNo: row.preSettlementNo || row.formalSettlementNo || row.settlementNo || '',
|
||||
referenceType: isPre ? 'pre' : 'formal',
|
||||
settlementAmount: Number(Number(settlementAmount || 0).toFixed(2)),
|
||||
payableAmount: Number(
|
||||
(row.payableAmount === undefined || row.payableAmount === null
|
||||
? Math.max(0, settlementAmount - appliedAmount)
|
||||
: Number(row.payableAmount || 0)
|
||||
).toFixed(2)
|
||||
),
|
||||
payableAmount: Number(Math.max(0, payableAmount).toFixed(2)),
|
||||
paymentSequence: Number.isFinite(paymentSequence) && paymentSequence > 0 ? paymentSequence : 1,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* 按合同付款比例配置,判断当前付款次序是否超上限。
|
||||
* @param {Object} row 结算信息行(含 paymentSequence)
|
||||
*/
|
||||
buildSettlementRatioHint(row = {}) {
|
||||
const sequence = Number(row.paymentSequence || 1);
|
||||
const paymentRatio = Number(this.form.paymentRatio);
|
||||
const ratioConfig = this.contractPaymentRatioRows[sequence - 1];
|
||||
const ratioLimitValue = ratioConfig?.ratioLimit;
|
||||
const ratioLimit = Number(ratioLimitValue);
|
||||
const hasLimit =
|
||||
ratioLimitValue !== null &&
|
||||
ratioLimitValue !== undefined &&
|
||||
String(ratioLimitValue).trim() !== '' &&
|
||||
Number.isFinite(ratioLimit);
|
||||
const hasPaymentRatio =
|
||||
this.form.paymentRatio !== null &&
|
||||
this.form.paymentRatio !== undefined &&
|
||||
String(this.form.paymentRatio).trim() !== '' &&
|
||||
Number.isFinite(paymentRatio);
|
||||
const ratioExceeded = hasLimit && hasPaymentRatio && paymentRatio > ratioLimit;
|
||||
return {
|
||||
paymentSequence: Number.isFinite(sequence) && sequence > 0 ? sequence : 1,
|
||||
contractRatioLimit: hasLimit ? ratioLimit : null,
|
||||
ratioExceeded,
|
||||
ratioRemark: ratioExceeded
|
||||
? `合同约定第${sequence}笔付款比例上限为${Number(ratioLimit).toFixed(2)}%`
|
||||
: '',
|
||||
};
|
||||
},
|
||||
/** 为结算信息行回填付款次序(第几次付款) */
|
||||
async refreshSettlementPaymentSequences() {
|
||||
if (!this.isSettlementRelatedType || !this.referenceRows.length) {
|
||||
return;
|
||||
}
|
||||
const paymentType =
|
||||
this.form.paymentType === 'settlement_payment' ? 'settlement_payment' : 'progress_advance';
|
||||
const amountResponses = await Promise.all(
|
||||
this.referenceRows.map(item => {
|
||||
if (!this.isValidReferenceId(item.id)) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return api.getReferenceAmount(paymentType, item.id, this.form.id).catch(() => null);
|
||||
})
|
||||
);
|
||||
this.referenceRows = this.referenceRows.map((item, index) => {
|
||||
const amount = this.unwrapData(amountResponses[index]) || {};
|
||||
const paymentSequence = Number(amount.paymentSequence || item.paymentSequence || 1);
|
||||
return {
|
||||
...item,
|
||||
paymentSequence: Number.isFinite(paymentSequence) && paymentSequence > 0 ? paymentSequence : 1,
|
||||
};
|
||||
});
|
||||
},
|
||||
createInvoiceRows(invoices = [], settlementNo = '') {
|
||||
return (invoices || []).map(invoice => ({
|
||||
settlementNo,
|
||||
@@ -1568,29 +1702,41 @@ export default {
|
||||
if (!rows.length) return;
|
||||
const contractIds = new Set(rows.map(item => String(item.contractId || '')));
|
||||
if (contractIds.size > 1 || contractIds.has('')) {
|
||||
this.$message.warning('所选预结算单合同信息不一致,请返回后重新选择');
|
||||
this.$message.warning('所选预付结算单合同信息不一致,请返回后重新选择');
|
||||
return;
|
||||
}
|
||||
const first = rows[0];
|
||||
this.form.invoices = [];
|
||||
const paymentType =
|
||||
this.form.paymentType === 'settlement_payment' ? 'settlement_payment' : 'progress_advance';
|
||||
const amountResponses = await Promise.all(
|
||||
rows.map(item => {
|
||||
const referenceId = item.preSettlementId || item.id;
|
||||
if (!this.isValidReferenceId(referenceId)) return Promise.resolve(null);
|
||||
return api.getReferenceAmount(paymentType, referenceId, this.form.id).catch(() => null);
|
||||
})
|
||||
);
|
||||
this.referenceRows = rows.map((item, index) => {
|
||||
const amount = this.unwrapData(amountResponses[index]) || {};
|
||||
return this.createReferenceRow({
|
||||
...item,
|
||||
referenceType: 'pre',
|
||||
settlementAmount: amount.settlementAmount ?? item.settlementAmount,
|
||||
payableAmount: amount.payableAmount,
|
||||
try {
|
||||
const amountResponses = await Promise.all(
|
||||
rows.map(item => {
|
||||
const referenceId = item.preSettlementId || item.id;
|
||||
if (!this.isValidReferenceId(referenceId)) {
|
||||
return Promise.reject(new Error('结算单无效'));
|
||||
}
|
||||
return api.getReferenceAmount(paymentType, referenceId, this.form.id);
|
||||
})
|
||||
);
|
||||
this.referenceRows = rows.map((item, index) => {
|
||||
const amount = this.unwrapData(amountResponses[index]) || {};
|
||||
const settlementAmount = Number(amount.settlementAmount ?? item.settlementAmount ?? 0);
|
||||
if (amount.payableAmount === undefined || amount.payableAmount === null) {
|
||||
throw new Error('可付款金额获取失败,请重试');
|
||||
}
|
||||
return this.createReferenceRow({
|
||||
...item,
|
||||
referenceType: 'pre',
|
||||
settlementAmount,
|
||||
payableAmount: Number(amount.payableAmount || 0),
|
||||
paymentSequence: amount.paymentSequence,
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
this.$message.error(error?.message || '可付款金额获取失败,请重试');
|
||||
return;
|
||||
}
|
||||
const settlementAmount = this.referenceRows.reduce(
|
||||
(total, item) => total + Number(item.settlementAmount || 0),
|
||||
0
|
||||
@@ -1600,7 +1746,7 @@ export default {
|
||||
0
|
||||
);
|
||||
const paymentRatio = Number(this.form.paymentRatio || 0);
|
||||
// 尾款付款场景下选择预结算单时保留尾款付款类型,不强制改为进度预付
|
||||
// 尾款付款场景下选择预付结算单时保留尾款付款类型,不强制改为进度预付
|
||||
this.amountSyncing = true;
|
||||
Object.assign(this.form, {
|
||||
paymentType,
|
||||
@@ -1625,7 +1771,8 @@ export default {
|
||||
settlementAmount: Number(settlementAmount.toFixed(2)),
|
||||
payableAmount: Number(payableAmount.toFixed(2)),
|
||||
billType: '',
|
||||
appliedAmount: Number(((settlementAmount * paymentRatio) / 100).toFixed(2)),
|
||||
// 申请金额按可付款金额×比例,避免未扣减已申请额度
|
||||
appliedAmount: Number(((payableAmount * paymentRatio) / 100).toFixed(2)),
|
||||
});
|
||||
this.paymentRecords = [];
|
||||
this.clearReceiptAccount();
|
||||
@@ -1827,7 +1974,7 @@ export default {
|
||||
);
|
||||
return typeMatched || this.matchAttachmentMaterialByFileName(file, materials);
|
||||
},
|
||||
mergeAutomaticAttachments(files, materials, sourceName, defaultMaterial = null) {
|
||||
mergeAutomaticAttachments(files, materials, sourceName, defaultMaterial = null, sourceKey = '') {
|
||||
const existingUrls = new Set(
|
||||
(this.form.attachments || []).map(file => this.attachmentFileUrl(file)).filter(Boolean)
|
||||
);
|
||||
@@ -1848,26 +1995,95 @@ export default {
|
||||
attachmentType: MATERIAL_TYPE_MAP[material.key],
|
||||
description: file.description || `自动取自${sourceName}`,
|
||||
sourceImported: true,
|
||||
sourceKey: sourceKey || undefined,
|
||||
};
|
||||
});
|
||||
if (imported.length) this.form.attachments = [...this.form.attachments, ...imported];
|
||||
return imported;
|
||||
},
|
||||
/** 移除指定来源的自动关联附件(切换结算单/合同时整批替换) */
|
||||
removeAutomaticAttachments(sourceKey, sourceName = '') {
|
||||
const previousUrls = new Set(
|
||||
sourceKey === 'settlement' ? this.linkedSettlementAttachmentUrls || [] : []
|
||||
);
|
||||
this.form.attachments = (this.form.attachments || []).filter(file => {
|
||||
const url = this.attachmentFileUrl(file);
|
||||
if (sourceKey && file.sourceKey === sourceKey) return false;
|
||||
if (
|
||||
sourceName &&
|
||||
file.sourceImported &&
|
||||
String(file.description || '').includes(`自动取自${sourceName}`)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (previousUrls.size && url && previousUrls.has(url)) return false;
|
||||
return true;
|
||||
});
|
||||
if (sourceKey === 'settlement') {
|
||||
this.linkedSettlementAttachmentUrls = [];
|
||||
}
|
||||
},
|
||||
/** 按当前结算单附件整批同步:先清旧关联,再写入新结算单上传的附件 */
|
||||
syncSettlementAttachments(settlementRows = []) {
|
||||
const settlementMaterials = [
|
||||
ATTACHMENT_MATERIALS.weighingSlip,
|
||||
ATTACHMENT_MATERIALS.entrustOrder,
|
||||
ATTACHMENT_MATERIALS.settlement,
|
||||
];
|
||||
const settlementFiles = (settlementRows || []).flatMap(row => this.parse(row.attachmentsJson));
|
||||
const nextUrlSet = new Set(
|
||||
settlementFiles.map(file => this.attachmentFileUrl(file)).filter(Boolean)
|
||||
);
|
||||
// 编辑回显时草稿里已有结算单附件但无 sourceKey:用 URL 交集补齐待替换清单
|
||||
if (!(this.linkedSettlementAttachmentUrls || []).length && nextUrlSet.size) {
|
||||
this.linkedSettlementAttachmentUrls = (this.form.attachments || [])
|
||||
.map(file => this.attachmentFileUrl(file))
|
||||
.filter(url => url && nextUrlSet.has(url));
|
||||
}
|
||||
this.removeAutomaticAttachments('settlement', '结算单');
|
||||
const imported = this.mergeAutomaticAttachments(
|
||||
settlementFiles,
|
||||
settlementMaterials,
|
||||
'结算单',
|
||||
null,
|
||||
'settlement'
|
||||
);
|
||||
this.linkedSettlementAttachmentUrls = imported
|
||||
.map(file => this.attachmentFileUrl(file))
|
||||
.filter(Boolean);
|
||||
},
|
||||
async loadCurrentSettlementDetails() {
|
||||
const preIds = [
|
||||
...(this.form.preSettlementIds || []),
|
||||
this.form.preSettlementId,
|
||||
...(this.referenceRows || [])
|
||||
.filter(item => item.referenceType === 'pre')
|
||||
.map(item => item.id),
|
||||
]
|
||||
.map(id => String(id ?? '').trim())
|
||||
.filter(id => this.isValidReferenceId(id));
|
||||
const formalIds = [
|
||||
...(this.form.settlementIds || []),
|
||||
this.form.settlementId,
|
||||
...(this.referenceRows || [])
|
||||
.filter(item => item.referenceType !== 'pre')
|
||||
.map(item => item.id),
|
||||
]
|
||||
.map(id => String(id ?? '').trim())
|
||||
.filter(id => this.isValidReferenceId(id));
|
||||
const uniquePreIds = [...new Set(preIds)];
|
||||
const uniqueFormalIds = [...new Set(formalIds)];
|
||||
if (
|
||||
this.form.paymentType === 'progress_advance' &&
|
||||
this.isValidReferenceId(this.form.preSettlementId)
|
||||
(this.form.paymentType === 'progress_advance' ||
|
||||
this.form.paymentType === 'settlement_payment') &&
|
||||
uniquePreIds.length
|
||||
) {
|
||||
const response = await preApi.getDetail(this.form.preSettlementId);
|
||||
const detail = this.unwrapData(response);
|
||||
return detail?.id ? [detail] : [];
|
||||
const responses = await Promise.all(uniquePreIds.map(id => preApi.getDetail(id)));
|
||||
return responses.map(response => this.unwrapData(response)).filter(item => item?.id);
|
||||
}
|
||||
if (
|
||||
this.form.paymentType === 'settlement_payment' &&
|
||||
this.isValidReferenceId(this.form.settlementId)
|
||||
) {
|
||||
const response = await formalApi.getDetail(this.form.settlementId);
|
||||
const detail = this.unwrapData(response);
|
||||
return detail?.id ? [detail] : [];
|
||||
if (this.form.paymentType === 'settlement_payment' && uniqueFormalIds.length) {
|
||||
const responses = await Promise.all(uniqueFormalIds.map(id => formalApi.getDetail(id)));
|
||||
return responses.map(response => this.unwrapData(response)).filter(item => item?.id);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
@@ -1877,32 +2093,29 @@ export default {
|
||||
this.contractSignatureFileAvailable = false;
|
||||
this.resetContractPaymentRatioLimit();
|
||||
try {
|
||||
const settlementMaterials = [
|
||||
ATTACHMENT_MATERIALS.weighingSlip,
|
||||
ATTACHMENT_MATERIALS.entrustOrder,
|
||||
ATTACHMENT_MATERIALS.settlement,
|
||||
];
|
||||
const settlementFiles = (settlementRows || []).flatMap(row =>
|
||||
this.parse(row.attachmentsJson)
|
||||
);
|
||||
this.mergeAutomaticAttachments(settlementFiles, settlementMaterials, '结算单');
|
||||
// 切换结算单时,附件区同步替换为当前结算单已上传附件
|
||||
this.syncSettlementAttachments(settlementRows);
|
||||
if (!this.form.contractId) {
|
||||
this.firstContractPayment = false;
|
||||
this.removeAutomaticAttachments('contract', '合同');
|
||||
return;
|
||||
}
|
||||
const contractResponse = await getContractDetail(this.form.contractId);
|
||||
const contract = this.unwrapData(contractResponse) || {};
|
||||
// 结算信息需用合同全部付款比例配置判断第几笔是否超限
|
||||
this.applyContractPaymentRatioLimit(contract);
|
||||
if (this.form.paymentType === 'project_advance') {
|
||||
this.applyContractPaymentRatioLimit(contract);
|
||||
const contractFiles = this.parse(contract.contractFileJson);
|
||||
this.contractSignatureFileAvailable = contractFiles.some(file =>
|
||||
Boolean(this.attachmentFileUrl(file))
|
||||
);
|
||||
this.removeAutomaticAttachments('contract', '合同');
|
||||
this.mergeAutomaticAttachments(
|
||||
contractFiles,
|
||||
[ATTACHMENT_MATERIALS.contract],
|
||||
'合同',
|
||||
ATTACHMENT_MATERIALS.contract
|
||||
ATTACHMENT_MATERIALS.contract,
|
||||
'contract'
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -1916,12 +2129,14 @@ export default {
|
||||
String(item.id || '') !== String(this.form.id || '') &&
|
||||
item.approvalStatus !== 'voided'
|
||||
);
|
||||
this.removeAutomaticAttachments('contract', '合同');
|
||||
if (this.firstContractPayment) {
|
||||
this.mergeAutomaticAttachments(
|
||||
this.parse(contract.contractFileJson),
|
||||
[ATTACHMENT_MATERIALS.contract],
|
||||
'合同',
|
||||
ATTACHMENT_MATERIALS.contract
|
||||
ATTACHMENT_MATERIALS.contract,
|
||||
'contract'
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -1933,9 +2148,12 @@ export default {
|
||||
resetContractPaymentRatioLimit() {
|
||||
this.contractPaymentRatioLimit = null;
|
||||
this.contractPaymentRatioExceeded = false;
|
||||
this.contractPaymentRatioRows = [];
|
||||
},
|
||||
applyContractPaymentRatioLimit(contract = {}) {
|
||||
const [firstRatio] = this.parse(contract.paymentRatioJson);
|
||||
const ratioRows = this.parse(contract.paymentRatioJson).filter(item => item && typeof item === 'object');
|
||||
this.contractPaymentRatioRows = ratioRows;
|
||||
const [firstRatio] = ratioRows;
|
||||
const ratioLimitValue = firstRatio?.ratioLimit;
|
||||
const ratioLimit = Number(ratioLimitValue);
|
||||
this.contractPaymentRatioLimit =
|
||||
@@ -2279,6 +2497,18 @@ export default {
|
||||
this.loadBillPage();
|
||||
},
|
||||
selectBill(row) {
|
||||
if (row?.billType && row.billType !== 'received') {
|
||||
ElMessage.warning('汇票付款仅可选择收票类型的汇票');
|
||||
return;
|
||||
}
|
||||
if (row?.maturityDate) {
|
||||
const maturity = this.$dayjs(row.maturityDate).startOf('day');
|
||||
const today = this.$dayjs().startOf('day');
|
||||
if (maturity.isValid() && maturity.isBefore(today)) {
|
||||
ElMessage.warning('所选汇票已到期,不可选择');
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.form.billLedgerId = row.id;
|
||||
this.form.billNo = row.billNo || '';
|
||||
this.billOptions = [row];
|
||||
@@ -2296,6 +2526,33 @@ export default {
|
||||
contractName: String(contractName || '').trim() || undefined,
|
||||
};
|
||||
},
|
||||
async fillReferencePayableAmounts(rows, paymentType) {
|
||||
if (!rows?.length) return rows || [];
|
||||
const amountResponses = await Promise.all(
|
||||
rows.map(row => {
|
||||
const referenceId = row.id;
|
||||
if (!this.isValidReferenceId(referenceId)) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return api
|
||||
.getReferenceAmount(paymentType, referenceId, this.form.id)
|
||||
.catch(() => null);
|
||||
})
|
||||
);
|
||||
return rows.map((row, index) => {
|
||||
const amount = this.unwrapData(amountResponses[index]) || {};
|
||||
const settlementAmount = Number(amount.settlementAmount ?? row.settlementAmount ?? 0);
|
||||
const payableAmount =
|
||||
amount.payableAmount === undefined || amount.payableAmount === null
|
||||
? null
|
||||
: Number(amount.payableAmount || 0);
|
||||
return {
|
||||
...row,
|
||||
settlementAmount,
|
||||
payableAmount,
|
||||
};
|
||||
});
|
||||
},
|
||||
async loadFormalReferences() {
|
||||
this.formalPage.loading = true;
|
||||
try {
|
||||
@@ -2305,7 +2562,8 @@ export default {
|
||||
this.referenceParams('formal')
|
||||
);
|
||||
const data = this.unwrapData(response) || {};
|
||||
this.formalRows = data.records || [];
|
||||
const records = data.records || [];
|
||||
this.formalRows = await this.fillReferencePayableAmounts(records, 'settlement_payment');
|
||||
this.formalPage.total = Number(data.total || 0);
|
||||
} finally {
|
||||
this.formalPage.loading = false;
|
||||
@@ -2320,7 +2578,10 @@ export default {
|
||||
this.referenceParams('pre')
|
||||
);
|
||||
const data = this.unwrapData(response) || {};
|
||||
this.preRows = data.records || [];
|
||||
const records = data.records || [];
|
||||
const paymentType =
|
||||
this.form.paymentType === 'settlement_payment' ? 'settlement_payment' : 'progress_advance';
|
||||
this.preRows = await this.fillReferencePayableAmounts(records, paymentType);
|
||||
this.prePage.total = Number(data.total || 0);
|
||||
} finally {
|
||||
this.prePage.loading = false;
|
||||
@@ -2413,7 +2674,7 @@ export default {
|
||||
}
|
||||
}
|
||||
if (!id) {
|
||||
this.$message.warning(isPre ? '未找到对应的预结算单' : '未找到对应的正式结算单');
|
||||
this.$message.warning(isPre ? '未找到对应的预付结算单' : '未找到对应的付款结算单');
|
||||
return;
|
||||
}
|
||||
this.$router.push({
|
||||
@@ -2421,7 +2682,7 @@ export default {
|
||||
query: {
|
||||
mode: 'view',
|
||||
id,
|
||||
name: isPre ? '查看预结算' : '查看正式结算',
|
||||
name: isPre ? '查看预付结算单' : '查看付款结算单',
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -2463,7 +2724,14 @@ export default {
|
||||
const amount = this.unwrapData(amountResponse) || {};
|
||||
const settlementAmount = Number(amount.settlementAmount ?? row.settlementAmount ?? 0);
|
||||
const payableAmount = Number(amount.payableAmount ?? 0);
|
||||
this.referenceRows = [this.createReferenceRow({ ...row, settlementAmount, payableAmount })];
|
||||
this.referenceRows = [
|
||||
this.createReferenceRow({
|
||||
...row,
|
||||
settlementAmount,
|
||||
payableAmount,
|
||||
paymentSequence: amount.paymentSequence,
|
||||
}),
|
||||
];
|
||||
this.clearReceiptAccount();
|
||||
Object.assign(this.form, {
|
||||
paymentType: 'settlement_payment',
|
||||
@@ -2508,14 +2776,17 @@ export default {
|
||||
const rows = this.formalSelection || [];
|
||||
if (!rows.length) return;
|
||||
if (new Set(rows.map(item => String(item.contractId || ''))).size !== 1) {
|
||||
this.$message.warning('所选正式结算单必须属于同一合同');
|
||||
this.$message.warning('所选付款结算单必须属于同一合同');
|
||||
return;
|
||||
}
|
||||
const validRows = rows.filter(row => this.isValidReferenceId(row.id));
|
||||
const details = await Promise.all(validRows.map(row => formalApi.getDetail(row.id)));
|
||||
await this.applyTransferredFormalSettlements(
|
||||
details.map((response, index) => ({ ...validRows[index], ...this.unwrapData(response) }))
|
||||
);
|
||||
const detailRows = details.map((response, index) => ({
|
||||
...validRows[index],
|
||||
...this.unwrapData(response),
|
||||
}));
|
||||
await this.applyTransferredFormalSettlements(detailRows);
|
||||
await this.loadAttachmentRuleData(detailRows);
|
||||
this.referenceVisible = false;
|
||||
},
|
||||
async selectPre(row) {
|
||||
@@ -2533,7 +2804,13 @@ export default {
|
||||
const settlementAmount = Number(amount.settlementAmount ?? row.settlementAmount ?? 0);
|
||||
const payableAmount = Number(amount.payableAmount ?? 0);
|
||||
this.referenceRows = [
|
||||
this.createReferenceRow({ ...row, settlementAmount, payableAmount, referenceType: 'pre' }),
|
||||
this.createReferenceRow({
|
||||
...row,
|
||||
settlementAmount,
|
||||
payableAmount,
|
||||
referenceType: 'pre',
|
||||
paymentSequence: amount.paymentSequence,
|
||||
}),
|
||||
];
|
||||
this.form.invoices = [];
|
||||
this.paymentRecords = [];
|
||||
@@ -2572,14 +2849,17 @@ export default {
|
||||
const rows = this.preSelection || [];
|
||||
if (!rows.length) return;
|
||||
if (new Set(rows.map(item => String(item.contractId || ''))).size !== 1) {
|
||||
this.$message.warning('所选预结算单必须属于同一合同');
|
||||
this.$message.warning('所选预付结算单必须属于同一合同');
|
||||
return;
|
||||
}
|
||||
const validRows = rows.filter(row => this.isValidReferenceId(row.id));
|
||||
const details = await Promise.all(validRows.map(row => preApi.getDetail(row.id)));
|
||||
await this.applyTransferredPreSettlements(
|
||||
details.map((response, index) => ({ ...validRows[index], ...this.unwrapData(response) }))
|
||||
);
|
||||
const detailRows = details.map((response, index) => ({
|
||||
...validRows[index],
|
||||
...this.unwrapData(response),
|
||||
}));
|
||||
await this.applyTransferredPreSettlements(detailRows);
|
||||
await this.loadAttachmentRuleData(detailRows);
|
||||
this.referenceVisible = false;
|
||||
},
|
||||
normalizeAttachments(files) {
|
||||
@@ -2722,30 +3002,60 @@ export default {
|
||||
})),
|
||||
};
|
||||
},
|
||||
/** 顶部「保存」:保存成功后关闭表单页并回到列表 */
|
||||
async handleSave() {
|
||||
await this.saveDraft({ showSuccess: true, closePage: true });
|
||||
},
|
||||
clearMessages() {
|
||||
ElMessage.closeAll();
|
||||
this.$message?.closeAll?.();
|
||||
},
|
||||
async saveDraft(options = {}) {
|
||||
const showSuccess = options.showSuccess !== false;
|
||||
// 仅显式 showSuccess:true 时提示;提交前静默保存默认不弹「保存成功」
|
||||
const showSuccess = options.showSuccess === true;
|
||||
const closePage = options.closePage === true;
|
||||
await this.$refs.formRef.validate();
|
||||
const appliedAmount = Number(this.form.appliedAmount);
|
||||
if (
|
||||
this.form.appliedAmount === null ||
|
||||
this.form.appliedAmount === undefined ||
|
||||
this.form.appliedAmount === '' ||
|
||||
!Number.isFinite(appliedAmount) ||
|
||||
appliedAmount <= 0
|
||||
) {
|
||||
this.clearMessages();
|
||||
ElMessage.warning('申请付款金额必填且必须大于0');
|
||||
return false;
|
||||
}
|
||||
if (this.form.paymentType === 'project_advance' && !this.form.projectId) {
|
||||
this.$message.warning('请选择所属项目');
|
||||
this.clearMessages();
|
||||
ElMessage.warning('请选择所属项目');
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
this.validateInvoices();
|
||||
this.validatePaymentRecords();
|
||||
} catch (error) {
|
||||
this.$message.warning(error.message);
|
||||
this.clearMessages();
|
||||
ElMessage.warning(error.message);
|
||||
return false;
|
||||
}
|
||||
const data = this.unwrapData(await api.save(this.payload()));
|
||||
this.form.id = data;
|
||||
if (showSuccess) {
|
||||
this.$message.success('保存成功');
|
||||
this.clearMessages();
|
||||
ElMessage.success('保存成功');
|
||||
}
|
||||
if (closePage) {
|
||||
sessionStorage.setItem(paymentApplicationListRefreshKey, '1');
|
||||
this.goBack();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
async submitForm() {
|
||||
// 提交前静默保存,避免同时弹出「保存成功」与材料缺失 toast
|
||||
const saved = await this.saveDraft({ showSuccess: false });
|
||||
// 提交前静默保存;不校验/提示附件未上传;全程只保留一条「提交成功」
|
||||
this.clearMessages();
|
||||
const saved = await this.saveDraft({ showSuccess: false, closePage: false });
|
||||
if (!saved) return;
|
||||
const previousApprovalStatus = this.form.approvalStatus || '';
|
||||
await api.submit({ id: this.form.id });
|
||||
@@ -2756,8 +3066,9 @@ export default {
|
||||
approvalStatus: previousApprovalStatus,
|
||||
mkEnabled: await loadMkEnabled().catch(() => false),
|
||||
});
|
||||
this.$message.success('提交成功');
|
||||
// 通知列表页(keep-alive)重新拉取数据
|
||||
this.clearMessages();
|
||||
ElMessage.success('提交成功');
|
||||
// 通知列表页(keep-alive)重新拉取列表
|
||||
sessionStorage.setItem(paymentApplicationListRefreshKey, '1');
|
||||
this.goBack();
|
||||
},
|
||||
@@ -2820,6 +3131,19 @@ export default {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.payment-form-page__bill-search :deep(.el-form--inline) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0 8px;
|
||||
}
|
||||
.payment-form-page__bill-search :deep(.el-form-item) {
|
||||
margin-right: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.payment-form-page__bill-search-actions {
|
||||
margin-left: 0;
|
||||
}
|
||||
.payment-form-page__reference-pagination {
|
||||
margin-top: 12px;
|
||||
}
|
||||
@@ -2854,6 +3178,9 @@ export default {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.payment-form-page__ratio-exceeded {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
.payment-form-page__attachment-checklist {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
Reference in New Issue
Block a user