完善收付款管理

This commit is contained in:
2026-08-27 23:52:17 +08:00
parent e98aa61cd2
commit b446e34c94
14 changed files with 783 additions and 629 deletions
+122 -181
View File
@@ -83,66 +83,6 @@
</el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="开户行">
<el-input v-model="form.bankName" disabled placeholder="从金蝶票据池带出" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="开户账号">
<el-input v-model="form.bankAccount" disabled placeholder="从金蝶票据池带出" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="开票行">
<el-input v-model="form.issuingBank" disabled placeholder="从金蝶票据池带出" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="电话">
<el-input v-model="form.phone" :disabled="readonly" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="客户邮箱" prop="customerEmails">
<el-select
v-model="form.customerEmails"
:disabled="readonly"
multiple
filterable
allow-create
default-first-option
:multiple-limit="3"
>
<el-option
v-for="email in customerEmailOptions"
:key="email"
:label="email"
:value="email"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="部门邮箱" prop="departmentEmails">
<el-select
v-model="form.departmentEmails"
:disabled="readonly"
multiple
filterable
allow-create
default-first-option
:multiple-limit="3"
>
<el-option
v-for="email in departmentEmailOptions"
:key="email"
:label="email"
:value="email"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
</section-card>
@@ -178,52 +118,7 @@
</section-card>
<section-card title="附件信息">
<template #extra>
<el-button
v-if="form.attachments.length"
type="primary"
plain
:icon="Download"
@click="downloadAttachments"
>批量下载</el-button
>
</template>
<el-table :data="form.attachments" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column label="文件名" min-width="220">
<template #default="{ row }">{{ row.originalName || row.name || '-' }}</template>
</el-table-column>
<el-table-column label="附件描述" min-width="240">
<template #default="{ row }">
<el-input v-model="row.description" :disabled="readonly" maxlength="200" />
</template>
</el-table-column>
<el-table-column prop="size" label="文件大小" width="120" />
<el-table-column prop="uploadUserName" label="上传人" width="140" />
<el-table-column prop="uploadTime" label="上传时间" width="170" />
<el-table-column label="操作" width="120" align="center">
<template #default="{ row, $index }">
<div class="receipt-form-page__links">
<el-link type="primary" :href="row.url || row.link" target="_blank">查看</el-link>
<el-link v-if="!readonly" type="danger" @click="form.attachments.splice($index, 1)"
>删除</el-link
>
</div>
</template>
</el-table-column>
</el-table>
<vehicle-attachment-upload
v-model="form.attachments"
:readonly="readonly"
:multiple="true"
:limit="20"
:max-size="500"
:file-types="attachmentFileTypes"
:show-file-list="false"
button-text="上传附件"
class="receipt-form-page__uploader"
@change="normalizeAttachments"
/>
<vehicle-attachment-table v-model="form.attachments" :readonly="readonly" />
</section-card>
<section-card title="备注">
@@ -289,6 +184,11 @@
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template>
</el-table-column>
<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="150" />
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<el-link type="primary" @click.stop="confirmInvoice(row)">选择</el-link>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="invoiceDialog.visible = false">取消</el-button>
@@ -331,6 +231,11 @@
<el-table-column label="已收票金额" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(row.receivedInvoiceAmount) }}</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<el-link type="primary" @click.stop="selectSettlement(row)">选择</el-link>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="settlementDialog.visible = false">取消</el-button>
@@ -341,9 +246,70 @@
</template>
<script>
import { Download, Search } from '@element-plus/icons-vue';
import { Search } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/invoiceReceipt';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
const invoicePoolMockRows = [
{
id: 90000001,
invoiceNo: '25312000000000000101',
invoiceDate: '2026-08-25',
invoiceType: '增值税专用发票',
taxRate: 9,
invoiceAmount: 109000,
taxAmount: 9000,
receiverName: '华东供应链管理有限公司',
issuerName: '上海安捷运输有限公司',
bankName: '中国工商银行上海分行',
bankAccount: '1001000100000000019',
issuingBank: '中国工商银行上海分行营业部',
phone: '021-66880001',
customerEmails: 'finance@huadong.example.com',
departmentEmails: 'transport@huadong.example.com',
attachmentsJson: '[]',
kingdeeBillNo: 'KD-FP-20260825001',
},
{
id: 90000002,
invoiceNo: '25312000000000000102',
invoiceDate: '2026-08-26',
invoiceType: '增值税普通发票',
taxRate: 6,
invoiceAmount: 53000,
taxAmount: 3000,
receiverName: '华东供应链管理有限公司',
issuerName: '江苏联运物流有限公司',
bankName: '中国建设银行南京分行',
bankAccount: '3201000100000000026',
issuingBank: '中国建设银行南京城南支行',
phone: '025-66880002',
customerEmails: 'finance@huadong.example.com',
departmentEmails: 'logistics@huadong.example.com',
attachmentsJson: '[]',
kingdeeBillNo: 'KD-FP-20260826002',
},
{
id: 90000003,
invoiceNo: '25312000000000000103',
invoiceDate: '2026-08-27',
invoiceType: '增值税专用发票',
taxRate: 3,
invoiceAmount: 20600,
taxAmount: 600,
receiverName: '华东供应链管理有限公司',
issuerName: '浙江通达物流有限公司',
bankName: '招商银行杭州分行',
bankAccount: '5719000100000000033',
issuingBank: '招商银行杭州高新支行',
phone: '0571-66880003',
customerEmails: 'finance@huadong.example.com',
departmentEmails: 'settlement@huadong.example.com',
attachmentsJson: '[]',
kingdeeBillNo: 'KD-FP-20260827003',
},
];
const emptyForm = () => ({
id: null,
@@ -359,6 +325,8 @@ const emptyForm = () => ({
bankName: '',
bankAccount: '',
issuingBank: '',
kingdeeBillNo: '',
kingdeeStatus: 'unsynced',
phone: '',
customerEmails: [],
departmentEmails: [],
@@ -373,29 +341,15 @@ const emptyForm = () => ({
export default {
name: 'InvoiceReceiptForm',
components: {
VehicleAttachmentTable,
},
data() {
return {
Download,
Search,
form: emptyForm(),
customerEmailOptions: [],
departmentEmailOptions: [],
attachmentFileTypes: [
'pdf',
'bmp',
'jpeg',
'png',
'jpg',
'doc',
'docx',
'ppt',
'pptx',
'xlsx',
'xls',
'eml',
'msg',
'zip',
],
invoiceDialog: { visible: false, loading: false, keyword: '', rows: [], current: null },
settlementDialog: {
visible: false,
@@ -433,12 +387,6 @@ export default {
settlementIds() {
return this.form.settlements.map(item => item.formalSettlementId || item.settlementId);
},
allocatedTotal() {
return this.form.settlements.reduce(
(total, item) => total + Number(item.allocatedInvoiceAmount || 0),
0
);
},
},
created() {
this.initialize();
@@ -523,25 +471,37 @@ export default {
async loadInvoicePool() {
this.invoiceDialog.loading = true;
try {
this.invoiceDialog.rows =
this.unwrapData(await api.getInvoicePool(this.invoiceDialog.keyword)) || [];
if (!this.recordId) {
this.invoiceDialog.rows = this.filterInvoicePoolMockRows(this.invoiceDialog.keyword);
return;
}
const data = this.unwrapData(await api.getInvoicePool(this.invoiceDialog.keyword));
this.invoiceDialog.rows = Array.isArray(data) ? data : data?.records || [];
} finally {
this.invoiceDialog.loading = false;
}
},
filterInvoicePoolMockRows(keyword) {
const normalizedKeyword = String(keyword || '')
.trim()
.toLowerCase();
if (!normalizedKeyword) return invoicePoolMockRows.map(item => ({ ...item }));
return invoicePoolMockRows
.filter(item =>
[item.invoiceNo, item.issuerName, item.receiverName].some(value =>
String(value || '')
.toLowerCase()
.includes(normalizedKeyword)
)
)
.map(item => ({ ...item }));
},
confirmInvoice(row, options = {}) {
const selected = row?.id ? row : this.invoiceDialog.current;
if (!selected) {
this.$message.warning('请选择一张金蝶票据池发票');
return;
}
if (this.form.settlements.length) {
const first = this.form.settlements[0];
if (selected.receiverName !== first.payerName || selected.issuerName !== first.payeeName) {
this.$message.warning('金蝶发票的开票单位、受票单位与结算单收付款方不一致');
return;
}
}
const preserveUserInput = options.preserveUserInput === true;
Object.assign(this.form, {
kingdeeInvoicePoolId: selected.id,
@@ -556,6 +516,8 @@ export default {
bankName: selected.bankName,
bankAccount: selected.bankAccount,
issuingBank: selected.issuingBank,
kingdeeBillNo: selected.kingdeeBillNo || '',
kingdeeStatus: selected.kingdeeStatus || 'unsynced',
phone: preserveUserInput ? this.form.phone : selected.phone || '',
customerEmails: preserveUserInput
? this.form.customerEmails
@@ -608,13 +570,6 @@ export default {
this.$message.warning('关联结算单必须属于同一项目、组织及收付款方');
return;
}
if (
this.form.kingdeeInvoicePoolId &&
(this.form.receiverName !== first.payerName || this.form.issuerName !== first.payeeName)
) {
this.$message.warning('金蝶发票的开票单位、受票单位与结算单收付款方不一致');
return;
}
this.form.settlements = rows.map(item => ({
...item,
formalSettlementId: item.id,
@@ -629,6 +584,10 @@ export default {
await this.loadReferenceInformation();
this.$refs.formRef?.validateField('settlementIds').catch(() => {});
},
async selectSettlement(row) {
this.settlementDialog.selection = [row];
await this.confirmSettlements();
},
async loadReferenceInformation() {
const data = this.unwrapData(await api.getReferenceInformation(this.settlementIds.join(',')));
this.customerEmailOptions = data.customerEmails || [];
@@ -645,28 +604,6 @@ export default {
0
);
},
normalizeAttachments(files) {
const userInfo = this.$store.getters.userInfo || {};
const userName = userInfo.realName || userInfo.userName || '';
const time = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
this.form.attachments = (files || []).map(file => ({
...file,
description: file.description || '',
uploadUserName: file.uploadUserName || userName,
uploadTime: file.uploadTime || time,
}));
},
downloadAttachments() {
this.form.attachments.forEach(file => {
const url = file.url || file.link;
if (!url) return;
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = file.originalName || file.name || '';
anchor.target = '_blank';
anchor.click();
});
},
openSettlementDetail(row) {
const id = row.formalSettlementId || row.settlementId || row.id;
if (!id) return;
@@ -678,9 +615,6 @@ export default {
validateBusiness() {
const invoiceAmount = Number(this.form.invoiceAmount || 0);
if (invoiceAmount <= 0) throw new Error('开票金额必须大于0');
if (Math.abs(this.allocatedTotal - invoiceAmount) > 0.001) {
throw new Error('分摊发票金额总和必须等于开票金额');
}
for (const row of this.form.settlements) {
const allocated = Number(row.allocatedInvoiceAmount);
if (!Number.isFinite(allocated) || allocated < 0) {
@@ -695,6 +629,19 @@ export default {
return {
id: this.form.id,
kingdeeInvoicePoolId: this.form.kingdeeInvoicePoolId,
invoiceNo: this.form.invoiceNo,
invoiceDate: this.form.invoiceDate,
invoiceType: this.form.invoiceType,
taxRate: this.form.taxRate,
invoiceAmount: this.form.invoiceAmount,
taxAmount: this.form.taxAmount,
receiverName: this.form.receiverName,
issuerName: this.form.issuerName,
bankName: this.form.bankName,
bankAccount: this.form.bankAccount,
issuingBank: this.form.issuingBank,
kingdeeBillNo: this.form.kingdeeBillNo,
kingdeeStatus: this.form.kingdeeStatus,
phone: this.form.phone,
customerEmails: this.form.customerEmails.join(';'),
departmentEmails: this.form.departmentEmails.join(';'),
@@ -731,7 +678,10 @@ export default {
this.$message.warning('请先选择金蝶票据池发票');
return;
}
const rows = this.unwrapData(await api.getInvoicePool(this.form.invoiceNo)) || [];
const data = this.recordId
? this.unwrapData(await api.getInvoicePool(this.form.invoiceNo))
: this.filterInvoicePoolMockRows(this.form.invoiceNo);
const rows = Array.isArray(data) ? data : data?.records || [];
const invoice = rows.find(item => item.invoiceNo === this.form.invoiceNo);
if (!invoice) {
this.$message.warning('金蝶票据池中未查询到该发票');
@@ -762,18 +712,9 @@ export default {
.receipt-form-page__form :deep(.el-input-number) {
width: 100%;
}
.receipt-form-page__links {
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
}
.receipt-form-page__remark :deep(.el-form-item__content) {
margin-left: 0 !important;
}
.receipt-form-page__uploader {
margin-top: 12px;
}
.receipt-form-page__actions {
display: flex;
justify-content: flex-end;