完善付款管理

This commit is contained in:
2026-08-27 18:21:07 +08:00
parent a176608019
commit cec4c323a7
4 changed files with 1133 additions and 219 deletions
+6
View File
@@ -4,6 +4,12 @@ const baseUrl = '/blade-transport/payment-application';
export const getList = (current, size, params) => export const getList = (current, size, params) =>
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } }); request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } }); export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
export const getReferenceAmount = (paymentType, referenceId, excludeId) =>
request({
url: `${baseUrl}/reference-amount`,
method: 'get',
params: { paymentType, referenceId, excludeId: excludeId || undefined },
});
export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data }); export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data });
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } }); export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data }); export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
File diff suppressed because it is too large Load Diff
+17 -5
View File
@@ -87,7 +87,7 @@
v-if="hasPermission('payment_application_add')" v-if="hasPermission('payment_application_add')"
type="primary" type="primary"
plain plain
@click="openCreate" @click="openProjectAdvance"
>预付申请</el-button >预付申请</el-button
> >
<el-button <el-button
@@ -114,9 +114,12 @@
<el-link v-if="column.link" type="primary" @click="openView(row)">{{ <el-link v-if="column.link" type="primary" @click="openView(row)">{{
row[column.prop] row[column.prop]
}}</el-link> }}</el-link>
<el-tag v-else-if="column.status" :type="statusType(row.approvalStatus)" class="status-text">{{ <el-tag
row[column.prop] || '-' v-else-if="column.status"
}}</el-tag> :type="statusType(row.approvalStatus)"
class="status-text"
>{{ row[column.prop] || '-' }}</el-tag
>
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span> <span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
<span v-else>{{ displayValue(row[column.prop]) }}</span> <span v-else>{{ displayValue(row[column.prop]) }}</span>
</template> </template>
@@ -307,6 +310,12 @@ export default {
openCreate() { openCreate() {
this.$router.push({ path: '/payment/payment-application/form', query: { mode: 'add' } }); this.$router.push({ path: '/payment/payment-application/form', query: { mode: 'add' } });
}, },
openProjectAdvance() {
this.$router.push({
path: '/payment/payment-application/form',
query: { mode: 'add', paymentType: 'project_advance' },
});
},
openEdit(row) { openEdit(row) {
this.$router.push({ this.$router.push({
path: '/payment/payment-application/form', path: '/payment/payment-application/form',
@@ -320,7 +329,10 @@ export default {
}); });
}, },
openSettlementPayment() { openSettlementPayment() {
this.openCreate(); this.$router.push({
path: '/payment/payment-application/form',
query: { mode: 'add', paymentType: 'settlement_payment' },
});
}, },
selectedOne(action) { selectedOne(action) {
if (this.selection.length !== 1) { if (this.selection.length !== 1) {
@@ -4,7 +4,11 @@
v-bind="editorContainerProps" v-bind="editorContainerProps"
@update:model-value="visible = $event" @update:model-value="visible = $event"
> >
<div v-loading="loading" class="formal-editor"> <div
v-loading="loading"
class="formal-editor"
:class="{ 'formal-editor--page': pageMode }"
>
<section-card title="结算基本信息"> <section-card title="结算基本信息">
<el-form <el-form
ref="formRef" ref="formRef"
@@ -319,26 +323,164 @@
</div> </div>
</section-card> </section-card>
<section-card v-if="readonly && payments.length" title="付款信息"> <section-card title="发票信息">
<el-table :data="payments" border> <div v-if="editable" class="formal-editor__invoice-claim">
<el-form inline label-position="right" label-width="auto" @submit.prevent>
<el-form-item label="发票号码">
<el-input
v-model="invoiceClaimNo"
clearable
maxlength="32"
placeholder="请输入发票号码"
@keyup.enter="claimInvoices"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="invoiceClaimLoading" @click="claimInvoices">
认领查询
</el-button>
</el-form-item>
</el-form>
</div>
<el-table :data="invoices" border>
<el-table-column type="index" label="序号" width="64" align="center" /> <el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="invoiceNo" label="发票号" min-width="150" align="center" />
<el-table-column prop="invoiceDate" label="开票日期" min-width="130" align="center">
<template #default="{ row }">{{ displayValue(row.invoiceDate) }}</template>
</el-table-column>
<el-table-column prop="invoiceType" label="发票类型" min-width="150" align="center">
<template #default="{ row }">{{ displayValue(row.invoiceType) }}</template>
</el-table-column>
<el-table-column prop="taxRate" label="税率" min-width="100" align="center">
<template #default="{ row }">
{{ row.taxRate === null || row.taxRate === undefined ? '-' : `${row.taxRate}%` }}
</template>
</el-table-column>
<el-table-column <el-table-column
v-for="column in paymentTableColumns" prop="invoiceAmount"
:key="column.prop" label="发票金额(含税)"
v-bind="column" min-width="150"
align="center"
>
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
</el-table-column>
<el-table-column
prop="availableInvoiceAmount"
label="可匹配发票金额(含税)"
min-width="180"
align="center"
>
<template #default="{ row }">{{ formatMoney(row.availableInvoiceAmount) }}</template>
</el-table-column>
<el-table-column
prop="matchedAmount"
label="匹配结算单金额(含税)"
min-width="190"
align="center" align="center"
> >
<template #default="{ row }"> <template #default="{ row }">
<span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span> <el-input-number
<span v-else-if="column.prop === 'paymentTypeName'">{{ v-if="editable"
row.paymentType === 'final' ? '尾款' : displayValue(row.paymentType) v-model="row.matchedAmount"
}}</span> :min="0"
<span v-else-if="column.prop === 'billStatusName'">{{ :max="Number(row.availableInvoiceAmount || 0)"
row.billStatus === 'reviewing' ? '审批中' : displayValue(row.billStatus) :precision="2"
}}</span> :controls="false"
<span v-else>{{ displayValue(row[column.prop]) }}</span> />
<span v-else>{{ formatMoney(row.matchedAmount) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="220" align="center">
<template #default="{ row, $index }">
<div class="formal-editor__links">
<el-link type="primary" @click="viewInvoiceAttachment(row)">查看</el-link>
<el-link type="primary" @click="downloadInvoiceAttachment(row)">下载</el-link>
<el-link v-if="editable" type="danger" @click="removeInvoice($index)">
删除
</el-link>
</div>
</template>
</el-table-column>
</el-table>
</section-card>
<section-card v-if="readonly" title="付款信息">
<el-table :data="paymentApplications" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="paymentTypeName" label="付款类型" min-width="120" align="center">
<template #default="{ row }">{{ displayValue(row.paymentTypeName) }}</template>
</el-table-column>
<el-table-column prop="paymentNo" label="单据号" min-width="160" align="center" />
<el-table-column prop="appliedAmount" label="申请付款金额" min-width="145" align="center">
<template #default="{ row }">{{ formatMoney(row.appliedAmount) }}</template>
</el-table-column>
<el-table-column prop="paidAmount" label="已付款金额" min-width="130" align="center">
<template #default="{ row }">{{ formatMoney(row.paidAmount) }}</template>
</el-table-column>
<el-table-column prop="approvalStatusName" label="单据状态" min-width="120" align="center">
<template #default="{ row }">{{ displayValue(row.approvalStatusName) }}</template>
</el-table-column>
<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="150" align="center">
<template #default="{ row }">{{ displayValue(row.kingdeeBillNo) }}</template>
</el-table-column>
<el-table-column prop="createUserName" label="创建人" min-width="120" align="center">
<template #default="{ row }">{{ displayValue(row.createUserName) }}</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center">
<template #default="{ row }">{{ displayValue(row.createTime) }}</template>
</el-table-column>
</el-table>
</section-card>
<section-card v-if="readonly" title="结算调整单">
<el-table :data="adjustments" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="adjustmentNo" label="调整单号" min-width="170" align="center" />
<el-table-column prop="adjustmentAmount" label="调整金额" min-width="130" align="center">
<template #default="{ row }">{{ formatMoney(row.adjustmentAmount) }}</template>
</el-table-column>
<el-table-column prop="approvalStatusName" label="单据状态" min-width="120" align="center">
<template #default="{ row }">{{ displayValue(row.approvalStatusName) }}</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="220" align="center">
<template #default="{ row }">{{ displayValue(row.remark) }}</template>
</el-table-column>
<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="150" align="center">
<template #default="{ row }">{{ displayValue(row.kingdeeBillNo) }}</template>
</el-table-column>
<el-table-column prop="createUserName" label="创建人" min-width="120" align="center">
<template #default="{ row }">{{ displayValue(row.createUserName) }}</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center">
<template #default="{ row }">{{ displayValue(row.createTime) }}</template>
</el-table-column>
</el-table>
</section-card>
<section-card v-if="readonly" title="变更记录">
<el-table :data="changeRecords" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="changeType" label="变更类型" min-width="150" align="center">
<template #default="{ row }">{{ displayValue(row.changeType) }}</template>
</el-table-column>
<el-table-column prop="lineNo" label="行号" min-width="90" align="center">
<template #default="{ row }">{{ displayValue(row.lineNo) }}</template>
</el-table-column>
<el-table-column prop="operationType" label="类型" min-width="110" align="center">
<template #default="{ row }">{{ displayValue(row.operationType) }}</template>
</el-table-column>
<el-table-column prop="changeContent" label="变更内容" min-width="360" align="center">
<template #default="{ row }">{{ displayValue(row.changeContent) }}</template>
</el-table-column>
<el-table-column prop="changeReason" label="变更原因" min-width="220" align="center">
<template #default="{ row }">{{ displayValue(row.changeReason) }}</template>
</el-table-column>
<el-table-column prop="operatorName" label="操作人" min-width="120" align="center">
<template #default="{ row }">{{ displayValue(row.operatorName) }}</template>
</el-table-column>
<el-table-column prop="changeTime" label="变更时间" min-width="170" align="center">
<template #default="{ row }">{{ displayValue(row.changeTime) }}</template>
</el-table-column>
</el-table> </el-table>
</section-card> </section-card>
</div> </div>
@@ -568,12 +710,12 @@ import {
candidateColumns, candidateColumns,
candidateDetailColumns, candidateDetailColumns,
detailColumns, detailColumns,
paymentColumns,
sourceColumns, sourceColumns,
summaryColumns, summaryColumns,
} from '@/option/settlement/formalSettlementTable'; } from '@/option/settlement/formalSettlementTable';
import { getDictionary } from '@/api/system/dictbiz'; import { getDictionary } from '@/api/system/dictbiz';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { downloadFileByUrl } from '@/utils/util';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
export default { export default {
@@ -597,8 +739,13 @@ export default {
sources: [], sources: [],
details: [], details: [],
summaryFees: [], summaryFees: [],
payments: [], paymentApplications: [],
adjustments: [],
changeRecords: [],
attachments: [], attachments: [],
invoices: [],
invoiceClaimNo: '',
invoiceClaimLoading: false,
attachmentUploadFiles: [], attachmentUploadFiles: [],
attachmentTypeOptions: [], attachmentTypeOptions: [],
attachmentFileTypes: [ attachmentFileTypes: [
@@ -627,7 +774,6 @@ export default {
sourceTableColumns: sourceColumns, sourceTableColumns: sourceColumns,
summaryTableColumns: summaryColumns, summaryTableColumns: summaryColumns,
detailTableColumns: detailColumns, detailTableColumns: detailColumns,
paymentTableColumns: paymentColumns,
candidateTableColumns: candidateColumns, candidateTableColumns: candidateColumns,
candidateDetailTableColumns: candidateDetailColumns, candidateDetailTableColumns: candidateDetailColumns,
rules: { rules: {
@@ -750,8 +896,12 @@ export default {
this.sources = []; this.sources = [];
this.details = []; this.details = [];
this.summaryFees = []; this.summaryFees = [];
this.payments = []; this.paymentApplications = [];
this.adjustments = [];
this.changeRecords = [];
this.attachments = []; this.attachments = [];
this.invoices = [];
this.invoiceClaimNo = '';
this.attachmentUploadFiles = []; this.attachmentUploadFiles = [];
this.detailCollapsed = false; this.detailCollapsed = false;
this.resetDetailQuery(false); this.resetDetailQuery(false);
@@ -782,8 +932,11 @@ export default {
this.sources = data.sources || []; this.sources = data.sources || [];
this.details = data.details || []; this.details = data.details || [];
this.summaryFees = data.summaryFees || []; this.summaryFees = data.summaryFees || [];
this.payments = data.payments || []; this.paymentApplications = data.paymentApplications || [];
this.adjustments = data.adjustments || [];
this.changeRecords = data.changeRecords || [];
this.attachments = this.parseAttachments(data.attachmentsJson); this.attachments = this.parseAttachments(data.attachmentsJson);
this.invoices = data.invoices || [];
this.sortAttachments(); this.sortAttachments();
this.contracts = this.allContracts.filter( this.contracts = this.allContracts.filter(
item => String(item.projectId) === String(this.form.projectId) item => String(item.projectId) === String(this.form.projectId)
@@ -1279,6 +1432,130 @@ export default {
this.adjust.saving = false; this.adjust.saving = false;
} }
}, },
claimInvoices() {
this.invoiceClaimLoading = true;
try {
const keyword = String(this.invoiceClaimNo || '').trim();
const defaultNo = `MOCK${this.$dayjs().format('YYYYMMDD')}`;
const baseNo = (keyword || defaultNo).slice(0, 29);
const numbers = [baseNo, `${baseNo}-02`, `${baseNo}-03`];
const invoiceAmounts = [1000, 2000, 3000];
const availableAmounts = [1000, 1000, 3000];
const preferredMatchedAmounts = [1000, 1000, 2000];
let remainingAmount = Math.max(
Number(this.form.settlementAmount || this.summaryTotal || 0) -
this.invoices.reduce((total, item) => total + Number(item.matchedAmount || 0), 0),
0
);
const existingNumbers = new Set(this.invoices.map(item => String(item.invoiceNo)));
const mockRows = numbers
.map((invoiceNo, index) => {
const matchedAmount = Math.min(
preferredMatchedAmounts[index],
availableAmounts[index],
remainingAmount
);
remainingAmount = Number((remainingAmount - matchedAmount).toFixed(2));
return {
invoiceNo,
invoiceDate: this.$dayjs()
.subtract(index, 'day')
.format('YYYY-MM-DD'),
invoiceType: index === 1 ? '增值税普通发票' : '增值税专用发票',
taxRate: [3, 6, 9][index],
invoiceAmount: invoiceAmounts[index],
availableInvoiceAmount: availableAmounts[index],
matchedAmount,
attachmentJson: '',
mockData: true,
};
})
.filter(item => !existingNumbers.has(item.invoiceNo));
if (!mockRows.length) {
this.$message.warning('当前发票号码的Mock数据已认领');
return;
}
this.invoices = [...this.invoices, ...mockRows];
this.$message.success(`已生成${mockRows.length}条Mock发票数据`);
} finally {
this.invoiceClaimLoading = false;
}
},
validateInvoices() {
const invoiceNumbers = new Set();
let matchedTotal = 0;
for (const row of this.invoices) {
const invoiceNo = String(row.invoiceNo || '').trim();
const invoiceAmount = Number(row.invoiceAmount || 0);
const availableAmount = Number(row.availableInvoiceAmount || 0);
const matchedAmount = Number(row.matchedAmount || 0);
const taxRate = Number(row.taxRate || 0);
if (!invoiceNo) {
this.$message.warning('发票号不能为空');
return false;
}
if (invoiceNumbers.has(invoiceNo)) {
this.$message.warning(`发票号${invoiceNo}重复`);
return false;
}
invoiceNumbers.add(invoiceNo);
if ([invoiceAmount, availableAmount, matchedAmount].some(value => value < 0)) {
this.$message.warning(`发票${invoiceNo}的金额不能小于0`);
return false;
}
if (availableAmount > invoiceAmount) {
this.$message.warning(`发票${invoiceNo}的可匹配金额不能超过发票金额`);
return false;
}
if (matchedAmount > availableAmount) {
this.$message.warning(`发票${invoiceNo}的匹配金额不能超过可匹配金额`);
return false;
}
if (taxRate < 0 || taxRate > 100) {
this.$message.warning(`发票${invoiceNo}的税率必须在0-100之间`);
return false;
}
matchedTotal += matchedAmount;
}
if (matchedTotal > Number(this.form.settlementAmount || 0)) {
this.$message.warning('发票匹配结算单金额合计不能超过结算金额');
return false;
}
return true;
},
removeInvoice(index) {
this.invoices.splice(index, 1);
},
invoiceAttachment(row = {}) {
if (!row.attachmentJson) return {};
if (typeof row.attachmentJson === 'object') {
return Array.isArray(row.attachmentJson) ? row.attachmentJson[0] || {} : row.attachmentJson;
}
try {
const parsed = JSON.parse(row.attachmentJson);
return Array.isArray(parsed) ? parsed[0] || {} : parsed || {};
} catch {
return {};
}
},
invoiceAttachmentUrl(row) {
const attachment = this.invoiceAttachment(row);
return attachment.url || attachment.link || attachment.fileUrl || '';
},
invoiceAttachmentName(row) {
const attachment = this.invoiceAttachment(row);
return attachment.originalName || attachment.name || `${row.invoiceNo || '发票'}.pdf`;
},
viewInvoiceAttachment(row) {
const url = this.invoiceAttachmentUrl(row);
if (!url) return this.$message.warning('当前Mock发票暂无可查看附件');
window.open(url, '_blank');
},
downloadInvoiceAttachment(row) {
const url = this.invoiceAttachmentUrl(row);
if (!url) return this.$message.warning('当前Mock发票暂无可下载附件');
downloadFileByUrl(url, this.invoiceAttachmentName(row));
},
async handleSave() { async handleSave() {
await this.$refs.formRef.validate(); await this.$refs.formRef.validate();
if (!this.form.sourcePreSettlementIds.length && !this.form.sourceDetailIds.length) { if (!this.form.sourcePreSettlementIds.length && !this.form.sourceDetailIds.length) {
@@ -1292,6 +1569,7 @@ export default {
if (invalidManualFee) { if (invalidManualFee) {
return this.$message.warning('请完善手工费用的费用类型和费用项'); return this.$message.warning('请完善手工费用的费用类型和费用项');
} }
if (!this.validateInvoices()) return;
if (!this.validateAttachments()) return; if (!this.validateAttachments()) return;
this.saving = true; this.saving = true;
try { try {
@@ -1320,6 +1598,16 @@ export default {
manualFlag: row.manualFlag || 0, manualFlag: row.manualFlag || 0,
})), })),
attachmentsJson: this.stringifyAttachments(this.attachments), attachmentsJson: this.stringifyAttachments(this.attachments),
invoices: this.invoices.map(row => ({
invoiceNo: String(row.invoiceNo || '').trim(),
invoiceDate: row.invoiceDate || null,
invoiceType: row.invoiceType || '',
taxRate: Number(row.taxRate || 0),
invoiceAmount: Number(row.invoiceAmount || 0),
availableInvoiceAmount: Number(row.availableInvoiceAmount || 0),
matchedAmount: Number(row.matchedAmount || 0),
attachmentJson: row.attachmentJson || '',
})),
remark: this.form.remark, remark: this.form.remark,
}); });
this.$message.success('保存成功'); this.$message.success('保存成功');
@@ -1525,6 +1813,9 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.formal-editor--page {
padding-bottom: 72px;
}
.formal-editor__actions { .formal-editor__actions {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1598,6 +1889,23 @@ export default {
.formal-editor__attachment-upload { .formal-editor__attachment-upload {
margin-top: 12px; margin-top: 12px;
} }
.formal-editor__invoice-claim {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
margin-bottom: 12px;
}
.formal-editor__invoice-claim-hint {
color: var(--el-color-danger);
font-size: 13px;
}
.formal-editor__invoice-claim :deep(.el-form-item) {
margin-bottom: 0;
}
.formal-editor__invoice-claim :deep(.el-input) {
width: 360px;
}
.formal-editor__negative-amount { .formal-editor__negative-amount {
color: var(--el-color-danger); color: var(--el-color-danger);
} }