完善收付款管理
This commit is contained in:
@@ -93,9 +93,9 @@
|
||||
:disabled="readonly"
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
:multiple-limit="3"
|
||||
placeholder="请选择客商开票信息邮箱"
|
||||
no-data-text="当前客商开票信息未配置邮箱"
|
||||
>
|
||||
<el-option
|
||||
v-for="email in departmentEmailOptions"
|
||||
@@ -203,9 +203,14 @@
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
<el-table-column label="商品和服务分类" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.goodsCategory" :disabled="readonly">
|
||||
<el-select
|
||||
v-model="row.goodsCategory"
|
||||
:disabled="readonly"
|
||||
filterable
|
||||
@change="handleGoodsCategoryChange(row)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in goodsCategoryOptions"
|
||||
v-for="item in invoiceItemCategories"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
@@ -215,12 +220,17 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="货物或服务简称" min-width="170">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.goodsName" :disabled="readonly" filterable allow-create>
|
||||
<el-select
|
||||
v-model="row.goodsName"
|
||||
:disabled="readonly"
|
||||
filterable
|
||||
@change="handleGoodsNameChange(row)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in goodsNameOptions"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
v-for="item in invoiceItemNames(row.goodsCategory)"
|
||||
:key="item.shortName"
|
||||
:label="item.shortName"
|
||||
:value="item.shortName"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
@@ -358,7 +368,15 @@
|
||||
<el-link v-if="column.link && row[column.prop]" type="primary">
|
||||
{{ row[column.prop] }}
|
||||
</el-link>
|
||||
<span
|
||||
v-else-if="
|
||||
['transportQuantity', 'mileage'].includes(column.prop) &&
|
||||
Number(row[column.prop]) === -1
|
||||
"
|
||||
></span>
|
||||
<span v-else-if="column.feeItemKeys">{{ formatMoney(getFeeItemValue(row, column.feeItemKeys)) }}</span>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else-if="column.prop === 'transportType'">{{ transportTypeLabel(row.transportType) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -366,41 +384,7 @@
|
||||
</section-card>
|
||||
|
||||
<section-card title="附件信息">
|
||||
<el-table :data="form.attachments" border>
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
<el-table-column prop="originalName" label="文件名" min-width="220" />
|
||||
<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="110" align="center">
|
||||
<template #default="{ row, $index }">
|
||||
<div class="invoice-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>
|
||||
<div class="invoice-form-page__attachment-upload">
|
||||
<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="上传"
|
||||
@change="normalizeAttachments"
|
||||
/>
|
||||
</div>
|
||||
<vehicle-attachment-table v-model="form.attachments" :readonly="readonly" />
|
||||
</section-card>
|
||||
|
||||
<section-card title="备注">
|
||||
@@ -469,6 +453,11 @@
|
||||
<el-table-column label="可开票金额" min-width="140" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.availableInvoiceAmount) }}</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>
|
||||
@@ -481,13 +470,16 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import * as api from '@/api/payment/invoiceApplication';
|
||||
import { getAll as getInvoiceItems } from '@/api/base/invoice-item';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { invoiceApplicationDetailColumns } from '@/option/payment/invoiceApplication';
|
||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||
|
||||
let localId = 0;
|
||||
const emptyLine = () => ({
|
||||
localId: ++localId,
|
||||
goodsCategory: '交通运输服务',
|
||||
goodsName: '运费',
|
||||
goodsCategory: '',
|
||||
goodsName: '',
|
||||
unit: '吨',
|
||||
quantity: 0,
|
||||
unitPriceNoTax: 0,
|
||||
@@ -525,6 +517,9 @@ const emptyForm = () => ({
|
||||
|
||||
export default {
|
||||
name: 'InvoiceApplicationForm',
|
||||
components: {
|
||||
VehicleAttachmentTable,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: emptyForm(),
|
||||
@@ -533,29 +528,14 @@ export default {
|
||||
receiverInvoiceOptions: [],
|
||||
contactOptions: [],
|
||||
departmentEmailOptions: [],
|
||||
invoiceItems: [],
|
||||
transportTypeOptions: [],
|
||||
invoiceTypeOptions: api.invoiceTypeOptions,
|
||||
detailColumns: invoiceApplicationDetailColumns,
|
||||
goodsCategoryOptions: ['交通运输服务', '生产生活服务', '其他现代服务'],
|
||||
goodsNameOptions: ['运费', '装卸搬运服务', '过路费', '仓储服务', '代理服务费'],
|
||||
detailBaseColumns: invoiceApplicationDetailColumns.filter(
|
||||
column => !['feeItemsJson', 'freightAmount'].includes(column.prop)
|
||||
),
|
||||
feeItemColumns: [],
|
||||
unitOptions: ['吨', '车', '次', '项'],
|
||||
taxRateOptions: [0, 1, 3, 6, 9, 13],
|
||||
attachmentFileTypes: [
|
||||
'pdf',
|
||||
'bmp',
|
||||
'jpeg',
|
||||
'png',
|
||||
'jpg',
|
||||
'doc',
|
||||
'docx',
|
||||
'ppt',
|
||||
'pptx',
|
||||
'xlsx',
|
||||
'xls',
|
||||
'eml',
|
||||
'msg',
|
||||
'zip',
|
||||
'rar',
|
||||
],
|
||||
settlementDialog: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
@@ -566,9 +546,7 @@ export default {
|
||||
rules: {
|
||||
settlementIds: [{ validator: this.validateSettlements, trigger: 'change' }],
|
||||
invoiceType: [{ required: true, message: '请选择发票类型', trigger: 'change' }],
|
||||
departmentEmails: [
|
||||
{ type: 'array', required: true, min: 1, message: '请选择部门邮箱', trigger: 'change' },
|
||||
],
|
||||
departmentEmails: [{ validator: this.validateDepartmentEmails, trigger: 'change' }],
|
||||
receiverInvoiceInfoId: [{ required: true, message: '请选择受票方单位', trigger: 'change' }],
|
||||
contactPhone: [{ pattern: /^\d{11}$/, message: '联系电话必须为11位数字', trigger: 'blur' }],
|
||||
taxpayerNo: [{ validator: this.validateInvoiceInfoField, trigger: 'change' }],
|
||||
@@ -614,11 +592,19 @@ export default {
|
||||
0
|
||||
);
|
||||
},
|
||||
allocatedTotal() {
|
||||
return this.form.settlements.reduce(
|
||||
(total, item) => total + Number(item.allocatedInvoiceAmount || 0),
|
||||
0
|
||||
);
|
||||
detailColumns() {
|
||||
return [...this.detailBaseColumns, ...this.feeItemColumns];
|
||||
},
|
||||
invoiceItemCategories() {
|
||||
return [...new Set(this.invoiceItems.map(item => item.categoryName).filter(Boolean))];
|
||||
},
|
||||
taxRateOptions() {
|
||||
const rates = new Set([0, 1, 3, 6, 9, 13]);
|
||||
this.invoiceItems.forEach(item => {
|
||||
const rate = Number(item.defaultTaxRate);
|
||||
if (Number.isFinite(rate) && rate >= 0 && rate <= 100) rates.add(rate);
|
||||
});
|
||||
return [...rates].sort((left, right) => left - right);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
@@ -641,22 +627,25 @@ export default {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
parseEmails(value) {
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
return values
|
||||
.flatMap(item => String(item || '').split(/[;,,;]/))
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean);
|
||||
},
|
||||
async initialize() {
|
||||
await Promise.all([this.loadInvoiceItems(), this.loadTransportTypeOptions()]);
|
||||
const userInfo = this.$store.getters.userInfo || {};
|
||||
const userEmail = userInfo.email || '';
|
||||
this.departmentEmailOptions = userEmail ? [userEmail] : [];
|
||||
if (!this.recordId) {
|
||||
this.form.applicantName = userInfo.realName || userInfo.userName || '';
|
||||
if (userEmail) this.form.departmentEmails = [userEmail];
|
||||
return;
|
||||
}
|
||||
const data = this.unwrapData(await api.getDetail(this.recordId));
|
||||
this.form = {
|
||||
...emptyForm(),
|
||||
...data,
|
||||
departmentEmails: String(data.departmentEmails || '')
|
||||
.split(';')
|
||||
.filter(Boolean),
|
||||
departmentEmails: this.parseEmails(data.departmentEmails),
|
||||
settlements: (data.settlements || []).map(item => ({
|
||||
...item,
|
||||
settlementId: item.formalSettlementId,
|
||||
@@ -669,12 +658,76 @@ export default {
|
||||
attachments: this.parse(data.attachmentsJson),
|
||||
};
|
||||
if (!this.form.sheets.length) this.form.sheets = [emptySheet()];
|
||||
this.form.sheets.forEach(sheet =>
|
||||
sheet.lines.forEach(line => this.syncInvoiceItemLine(line, false))
|
||||
);
|
||||
this.selectedDetailIds = (data.details || []).map(item => item.formalSettlementDetailId);
|
||||
if (this.settlementIds.length) {
|
||||
await Promise.all([this.loadReceiverInformation(), this.loadSettlementDetails(false)]);
|
||||
}
|
||||
if (this.readonly) this.$nextTick(() => this.$refs.formRef?.clearValidate('departmentEmails'));
|
||||
this.$nextTick(this.restoreDetailSelection);
|
||||
},
|
||||
async loadInvoiceItems() {
|
||||
const data = this.unwrapData(await getInvoiceItems());
|
||||
this.invoiceItems = Array.isArray(data) ? data : data?.records || [];
|
||||
this.form.sheets.forEach(sheet =>
|
||||
sheet.lines.forEach(line => this.syncInvoiceItemLine(line, false))
|
||||
);
|
||||
},
|
||||
async loadTransportTypeOptions() {
|
||||
const data = this.unwrapData(await getDictionary({ code: 'transport_type' }));
|
||||
const records = Array.isArray(data) ? data : data?.records || [];
|
||||
this.transportTypeOptions = records.map(item => ({
|
||||
label: item.dictValue || item.label || item.name,
|
||||
value: item.dictKey || item.value || item.dictValue || item.name,
|
||||
}));
|
||||
},
|
||||
transportTypeLabel(value) {
|
||||
if (!value) return '-';
|
||||
return (
|
||||
this.transportTypeOptions.find(item => String(item.value) === String(value))?.label ||
|
||||
value
|
||||
);
|
||||
},
|
||||
invoiceItemNames(categoryName) {
|
||||
return this.invoiceItems.filter(item => item.categoryName === categoryName);
|
||||
},
|
||||
findInvoiceItem(line) {
|
||||
return this.invoiceItems.find(
|
||||
item => item.categoryName === line.goodsCategory && item.shortName === line.goodsName
|
||||
);
|
||||
},
|
||||
syncInvoiceItemLine(line, clearInvalid = true) {
|
||||
const categoryExists = this.invoiceItemCategories.includes(line.goodsCategory);
|
||||
if (!categoryExists && clearInvalid) line.goodsCategory = '';
|
||||
const item = this.findInvoiceItem(line);
|
||||
if (item) {
|
||||
line.taxRate = Number(item.defaultTaxRate || 0);
|
||||
this.recalculateLine(line);
|
||||
} else if (clearInvalid) {
|
||||
line.goodsName = '';
|
||||
line.taxRate = 0;
|
||||
}
|
||||
},
|
||||
handleGoodsCategoryChange(line) {
|
||||
const names = this.invoiceItemNames(line.goodsCategory);
|
||||
if (!names.some(item => item.shortName === line.goodsName)) line.goodsName = '';
|
||||
line.taxRate = 0;
|
||||
if (names.length === 1) {
|
||||
line.goodsName = names[0].shortName;
|
||||
this.handleGoodsNameChange(line);
|
||||
}
|
||||
},
|
||||
handleGoodsNameChange(line) {
|
||||
const item = this.findInvoiceItem(line);
|
||||
if (!item) {
|
||||
line.taxRate = 0;
|
||||
return;
|
||||
}
|
||||
line.taxRate = Number(item.defaultTaxRate || 0);
|
||||
this.recalculateLine(line);
|
||||
},
|
||||
validateSettlements(rule, value, callback) {
|
||||
if (!this.form.settlements.length) {
|
||||
callback(new Error('请选择正式结算单'));
|
||||
@@ -715,6 +768,27 @@ export default {
|
||||
}
|
||||
callback();
|
||||
},
|
||||
validateDepartmentEmails(rule, value, callback) {
|
||||
if (this.readonly) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
const emails = this.parseEmails(value);
|
||||
if (!emails.length) {
|
||||
callback(new Error('请选择部门邮箱'));
|
||||
return;
|
||||
}
|
||||
if (emails.length > 3) {
|
||||
callback(new Error('部门邮箱最多选择3个'));
|
||||
return;
|
||||
}
|
||||
const options = new Set(this.departmentEmailOptions.map(item => item.toLowerCase()));
|
||||
if (emails.some(email => !options.has(email.toLowerCase()))) {
|
||||
callback(new Error('部门邮箱必须选择当前客商开票信息中的邮箱'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
handleInvoiceTypeChange() {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.formRef?.validateField([
|
||||
@@ -746,6 +820,12 @@ export default {
|
||||
this.$message.warning('请至少选择一张应付正式结算单');
|
||||
return;
|
||||
}
|
||||
await this.applySettlements(rows);
|
||||
},
|
||||
async selectSettlement(row) {
|
||||
await this.applySettlements([row]);
|
||||
},
|
||||
async applySettlements(rows) {
|
||||
const first = rows[0];
|
||||
const incompatible = rows.some(
|
||||
item =>
|
||||
@@ -775,22 +855,83 @@ export default {
|
||||
async loadSettlementDetails(resetSelection = true) {
|
||||
this.detailRows =
|
||||
this.unwrapData(await api.getSettlementDetails(this.settlementIds.join(','))) || [];
|
||||
this.buildFeeItemColumns();
|
||||
if (resetSelection) this.selectedDetailIds = this.detailRows.map(item => item.id);
|
||||
this.$nextTick(this.restoreDetailSelection);
|
||||
},
|
||||
parseFeeItems(value) {
|
||||
if (!value) return {};
|
||||
if (typeof value === 'object' && !Array.isArray(value)) return value;
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
buildFeeItemColumns() {
|
||||
const groupedNames = new Map();
|
||||
this.detailRows.forEach(row => {
|
||||
Object.keys(this.parseFeeItems(row.feeItemsJson || row.feeItems)).forEach(name => {
|
||||
const normalizedName = this.normalizeFeeItemName(name);
|
||||
if (!normalizedName) return;
|
||||
const key = this.feeItemNameKey(name);
|
||||
const group = groupedNames.get(key) || { label: normalizedName, keys: [] };
|
||||
if (!group.keys.includes(name)) group.keys.push(name);
|
||||
groupedNames.set(key, group);
|
||||
});
|
||||
});
|
||||
this.feeItemColumns = [...groupedNames.values()].map((group, index) => ({
|
||||
prop: `feeItem_${index}`,
|
||||
label: group.label,
|
||||
minWidth: 130,
|
||||
money: true,
|
||||
feeItemKeys: group.keys,
|
||||
}));
|
||||
},
|
||||
normalizeFeeItemName(name) {
|
||||
return String(name || '')
|
||||
.replace(/[\s\u200b-\u200d\ufeff]/g, '')
|
||||
.trim();
|
||||
},
|
||||
feeItemNameKey(name) {
|
||||
return this.normalizeFeeItemName(name).toLowerCase();
|
||||
},
|
||||
getFeeItemValue(row, names) {
|
||||
const items = this.parseFeeItems(row.feeItemsJson || row.feeItems);
|
||||
return names.reduce((total, name) => total + Number(items[name] || 0), 0);
|
||||
},
|
||||
async loadReceiverInformation() {
|
||||
const data = this.unwrapData(await api.getReceiverInformation(this.settlementIds.join(',')));
|
||||
this.receiverInvoiceOptions = data.invoiceInfos || [];
|
||||
const invoiceInfos = data.invoices || data.invoiceInfos || data.customer?.invoices || [];
|
||||
this.receiverInvoiceOptions = Array.isArray(invoiceInfos) ? invoiceInfos : [];
|
||||
this.contactOptions = data.contacts || [];
|
||||
const invoiceInfoEmails = [
|
||||
...this.receiverInvoiceOptions.flatMap(item => this.parseEmails(item.email)),
|
||||
];
|
||||
this.departmentEmailOptions = invoiceInfoEmails
|
||||
.filter(Boolean)
|
||||
.reduce((emails, email) => {
|
||||
if (!emails.some(item => item.toLowerCase() === email.toLowerCase())) emails.push(email);
|
||||
return emails;
|
||||
}, []);
|
||||
this.form.departmentEmails = this.form.departmentEmails
|
||||
.filter(email =>
|
||||
this.departmentEmailOptions.some(option => option.toLowerCase() === email.toLowerCase())
|
||||
)
|
||||
.slice(0, 3);
|
||||
this.form.issuerName = data.issuerName || this.form.issuerName;
|
||||
if (!this.form.receiverInvoiceInfoId) {
|
||||
const invoiceInfo =
|
||||
let invoiceInfo = this.receiverInvoiceOptions.find(
|
||||
item => String(item.id) === String(this.form.receiverInvoiceInfoId)
|
||||
);
|
||||
if (!invoiceInfo) {
|
||||
invoiceInfo =
|
||||
this.receiverInvoiceOptions.find(item => item.isDefault === 1) ||
|
||||
this.receiverInvoiceOptions[0];
|
||||
if (invoiceInfo) {
|
||||
this.form.receiverInvoiceInfoId = invoiceInfo.id;
|
||||
this.handleReceiverChange(invoiceInfo.id);
|
||||
}
|
||||
}
|
||||
if (invoiceInfo) {
|
||||
this.form.receiverInvoiceInfoId = invoiceInfo.id;
|
||||
this.handleReceiverChange(invoiceInfo.id);
|
||||
}
|
||||
if (!this.form.contactName) {
|
||||
const contact =
|
||||
@@ -846,23 +987,9 @@ export default {
|
||||
const rate = Number(row.taxRate || 0) / 100;
|
||||
row.taxAmount = rate ? Number((amount - amount / (1 + rate)).toFixed(2)) : 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,
|
||||
}));
|
||||
},
|
||||
validateBusiness() {
|
||||
if (!this.selectedDetailIds.length) throw new Error('请至少选择一条开票明细');
|
||||
if (this.invoiceAmount <= 0) throw new Error('本次开票金额必须大于0');
|
||||
if (Math.abs(this.invoiceAmount - this.allocatedTotal) > 0.001) {
|
||||
throw new Error('结算单分摊金额合计必须等于本次开票金额');
|
||||
}
|
||||
for (const sheet of this.form.sheets) {
|
||||
if (!sheet.lines.length) throw new Error('每张发票至少需要一条商品行');
|
||||
const hasFreight = sheet.lines.some(line => line.goodsName === '运费');
|
||||
@@ -968,12 +1095,6 @@ export default {
|
||||
background: #fafafa;
|
||||
font-weight: 600;
|
||||
}
|
||||
.invoice-form-page__links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.invoice-form-page__remark :deep(.el-form-item__content) {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
@@ -997,17 +1118,6 @@ export default {
|
||||
:global(.avue-layout--horizontal .invoice-form-page__actions) {
|
||||
left: 0;
|
||||
}
|
||||
.invoice-form-page__attachment-upload {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.invoice-form-page__attachment-upload .vehicle-attachment-upload {
|
||||
width: auto;
|
||||
}
|
||||
.invoice-form-page__attachment-upload .el-upload {
|
||||
display: inline-flex;
|
||||
}
|
||||
.invoice-form-page__dialog-search {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
Reference in New Issue
Block a user