Files
tms-erp-web/src/views/payment/invoice-receipt-form.vue
T

634 lines
21 KiB
Vue

<template>
<basic-container class="receipt-form-page">
<div class="archive-page-form__title">
{{ readonly ? '查看收票登记' : recordId ? '编辑收票登记' : '新增收票登记' }}
</div>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="right"
label-width="auto"
class="receipt-form-page__form"
>
<section-card title="收票基本信息">
<el-row :gutter="32">
<el-col :span="6">
<el-form-item label="发票号码" prop="invoiceNo">
<el-input v-model="form.invoiceNo" readonly placeholder="请选择金蝶票据池发票">
<template v-if="!readonly" #append>
<el-tooltip content="查询金蝶票据池" placement="top">
<el-button :icon="Search" @click="openInvoiceDialog" />
</el-tooltip>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="开票日期">
<el-input v-model="form.invoiceDate" disabled placeholder="从金蝶票据池带出" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="发票类型">
<el-input v-model="form.invoiceType" disabled placeholder="从金蝶票据池带出" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="税率">
<el-input
:model-value="formatRate(form.taxRate)"
disabled
placeholder="从金蝶票据池带出"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="开票金额">
<el-input
:model-value="form.invoiceNo ? formatMoney(form.invoiceAmount) : ''"
disabled
placeholder="从金蝶票据池带出"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="税额">
<el-input
:model-value="form.invoiceNo ? formatMoney(form.taxAmount) : ''"
disabled
placeholder="从金蝶票据池带出"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="受票单位">
<el-input v-model="form.receiverName" disabled placeholder="从金蝶票据池带出" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="开票单位">
<el-input v-model="form.issuerName" disabled placeholder="从金蝶票据池带出" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="关联结算单" prop="settlementIds">
<el-input :model-value="settlementLabel" readonly placeholder="请选择应付正式结算单">
<template v-if="!readonly" #append>
<el-tooltip content="选择正式结算单" placement="top">
<el-button :icon="Search" @click="openSettlementDialog" />
</el-tooltip>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
</section-card>
<section-card title="结算信息">
<el-table :data="form.settlements" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column label="结算单号" min-width="180" align="center">
<template #default="{ row }">
<el-link type="primary" @click="openSettlementDetail(row)">
{{ row.formalSettlementNo }}
</el-link>
</template>
</el-table-column>
<el-table-column label="结算总金额" min-width="150" align="right">
<template #default="{ row }">{{ formatMoney(row.settlementAmount) }}</template>
</el-table-column>
<el-table-column label="已收票金额" min-width="150" align="right">
<template #default="{ row }">{{ formatMoney(row.receivedInvoiceAmount) }}</template>
</el-table-column>
<el-table-column label="分摊发票金额" min-width="180">
<template #default="{ row }">
<el-input-number
v-model="row.allocatedInvoiceAmount"
:disabled="readonly"
:min="0"
:max="remainingAmount(row)"
:precision="2"
:controls="false"
/>
</template>
</el-table-column>
</el-table>
</section-card>
<section-card title="附件信息">
<vehicle-attachment-table v-model="form.attachments" :readonly="readonly" />
</section-card>
<section-card title="备注">
<el-form-item class="receipt-form-page__remark">
<el-input
v-model="form.remark"
:disabled="readonly"
type="textarea"
:rows="2"
maxlength="200"
show-word-limit
/>
</el-form-item>
</section-card>
<div class="receipt-form-page__actions">
<el-button @click="goBack">取消</el-button>
<el-button v-if="!readonly" type="primary" plain @click="syncReferenceData">同步</el-button>
<el-button v-if="!readonly && canSave" type="primary" plain @click="saveDraft"
>保存</el-button
>
<el-button
v-if="!readonly && canSave && hasPermission('invoice_receipt_submit')"
type="primary"
@click="submitForm"
>提交</el-button
>
</div>
</el-form>
<kingdee-invoice-pool-dialog
v-model="invoiceDialog.visible"
title="查询金蝶票据池"
:initial-invoice-no="form.invoiceNo"
:query-api="apiQueryInvoicePool"
@confirm="handleInvoicePicked"
/>
<el-dialog
v-model="settlementDialog.visible"
title="选择应付正式结算单"
width="86%"
append-to-body
>
<div class="receipt-form-page__dialog-search">
<el-input
v-model="settlementDialog.keyword"
clearable
placeholder="结算单号、项目或合同"
@keyup.enter="loadSettlementCandidates"
/>
<el-button type="primary" @click="loadSettlementCandidates">查询</el-button>
</div>
<el-table
ref="settlementTable"
v-loading="settlementDialog.loading"
:data="settlementDialog.rows"
border
@selection-change="settlementDialog.selection = $event"
>
<el-table-column type="selection" width="52" align="center" />
<el-table-column prop="formalSettlementNo" label="结算单号" min-width="170" />
<el-table-column prop="projectName" label="所属项目" min-width="150" />
<el-table-column prop="deptName" label="所属组织" min-width="150" />
<el-table-column prop="contractNo" label="合同编号" min-width="150" />
<el-table-column prop="payerName" label="付款方" min-width="150" />
<el-table-column prop="payeeName" label="收款方" min-width="150" />
<el-table-column label="结算总金额" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(row.settlementAmount) }}</template>
</el-table-column>
<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>
<el-button type="primary" @click="confirmSettlements">确定</el-button>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import { Search } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/invoiceReceipt';
import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/main.vue';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
const emptyForm = () => ({
id: null,
invoiceNo: '',
invoiceDate: '',
invoiceType: '',
taxRate: null,
invoiceAmount: null,
taxAmount: null,
receiverName: '',
issuerName: '',
bankName: '',
bankAccount: '',
issuingBank: '',
phone: '',
customerEmails: [],
departmentEmails: [],
projectName: '',
deptName: '',
payerName: '',
payeeName: '',
settlements: [],
attachments: [],
remark: '',
});
export default {
name: 'InvoiceReceiptForm',
components: {
KingdeeInvoicePoolDialog,
VehicleAttachmentTable,
},
data() {
return {
Search,
form: emptyForm(),
customerEmailOptions: [],
departmentEmailOptions: [],
invoiceDialog: {
visible: false,
},
settlementDialog: {
visible: false,
loading: false,
keyword: '',
rows: [],
selection: [],
},
rules: {
invoiceNo: [{ required: true, validator: this.validateInvoice, trigger: 'change' }],
settlementIds: [{ required: true, validator: this.validateSettlements, trigger: 'change' }],
customerEmails: [{ validator: this.validateEmails, trigger: 'change' }],
departmentEmails: [{ validator: this.validateEmails, trigger: 'change' }],
},
};
},
computed: {
...mapGetters(['permission']),
recordId() {
return this.$route.query.id || '';
},
readonly() {
return this.$route.query.mode === 'view';
},
canSave() {
const code = this.recordId ? 'invoice_receipt_edit' : 'invoice_receipt_add';
return this.hasPermission(code);
},
settlementLabel() {
return this.form.settlements
.map(item => item.formalSettlementNo)
.filter(Boolean)
.join(',');
},
settlementIds() {
return this.form.settlements.map(item => item.formalSettlementId || item.settlementId);
},
},
created() {
this.initialize();
},
methods: {
apiQueryInvoicePool(params) {
return api.queryInvoicePool(params);
},
handleInvoicePicked(rows) {
this.confirmInvoice(rows[0]);
},
hasPermission(code) {
return this.permission?.[code] !== false;
},
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
},
parse(value) {
if (!value) return [];
if (Array.isArray(value)) return value;
try {
return JSON.parse(value) || [];
} catch {
return [];
}
},
parseEmails(value) {
return String(value || '')
.split(/[;,,;]/)
.map(item => item.trim())
.filter(Boolean);
},
async initialize() {
const userInfo = this.$store.getters.userInfo || {};
const userEmail = userInfo.email || '';
this.departmentEmailOptions = userEmail ? [userEmail] : [];
if (!this.recordId) {
if (userEmail) this.form.departmentEmails = [userEmail];
return;
}
const data = this.unwrapData(await api.getDetail(this.recordId));
this.form = {
...emptyForm(),
...data,
customerEmails: this.parseEmails(data.customerEmails),
departmentEmails: this.parseEmails(data.departmentEmails),
settlements: (data.settlements || []).map(item => ({
...item,
settlementId: item.formalSettlementId,
})),
attachments: this.parse(data.attachmentsJson),
};
if (this.settlementIds.length) await this.loadReferenceInformation();
},
validateInvoice(rule, value, callback) {
if (!value) {
callback(new Error('请选择金蝶票据池发票'));
return;
}
callback();
},
validateSettlements(rule, value, callback) {
if (!this.form.settlements.length) {
callback(new Error('请选择应付正式结算单'));
return;
}
callback();
},
validateEmails(rule, value, callback) {
const emails = Array.isArray(value) ? value : this.parseEmails(value);
const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (emails.length > 3) {
callback(new Error('最多填写3个邮箱'));
return;
}
if (emails.some(email => !valid.test(email))) {
callback(new Error('请输入正确的邮箱'));
return;
}
callback();
},
openInvoiceDialog() {
this.invoiceDialog.visible = true;
},
confirmInvoice(row, options = {}) {
const selected = row;
if (!selected) {
this.$message.warning('请选择一张金蝶票据池发票');
return;
}
const preserveUserInput = options.preserveUserInput === true;
Object.assign(this.form, {
invoiceNo: selected.invoiceNo,
invoiceDate: selected.invoiceDate,
invoiceType: selected.invoiceType,
taxRate: selected.taxRate,
invoiceAmount: selected.invoiceAmount,
taxAmount: selected.taxAmount,
receiverName: selected.receiverName,
issuerName: selected.issuerName,
bankName: selected.bankName,
bankAccount: selected.bankAccount,
issuingBank: selected.issuingBank,
phone: preserveUserInput ? this.form.phone : selected.phone || '',
customerEmails: preserveUserInput
? this.form.customerEmails
: this.parseEmails(selected.customerEmails),
departmentEmails: preserveUserInput
? this.form.departmentEmails
: this.parseEmails(selected.departmentEmails),
attachments: preserveUserInput
? this.form.attachments
: this.parse(selected.attachmentsJson),
});
this.customerEmailOptions = [...this.form.customerEmails];
this.departmentEmailOptions = [
...new Set([...this.departmentEmailOptions, ...this.form.departmentEmails]),
];
this.invoiceDialog.visible = false;
this.$refs.formRef?.validateField('invoiceNo').catch(() => {});
return true;
},
async openSettlementDialog() {
this.settlementDialog.visible = true;
await this.loadSettlementCandidates();
},
async loadSettlementCandidates() {
this.settlementDialog.loading = true;
try {
this.settlementDialog.rows =
this.unwrapData(
await api.getSettlementCandidates(this.settlementDialog.keyword, this.form.id)
) || [];
} finally {
this.settlementDialog.loading = false;
}
},
async confirmSettlements() {
const rows = this.settlementDialog.selection;
if (!rows.length) {
this.$message.warning('请至少选择一张应付正式结算单');
return;
}
const first = rows[0];
const incompatible = rows.some(
item =>
String(item.projectId) !== String(first.projectId) ||
String(item.deptId) !== String(first.deptId) ||
item.payerName !== first.payerName ||
item.payeeName !== first.payeeName
);
if (incompatible) {
this.$message.warning('关联结算单必须属于同一项目、组织及收付款方');
return;
}
this.form.settlements = rows.map(item => ({
...item,
formalSettlementId: item.id,
settlementId: item.id,
allocatedInvoiceAmount: rows.length === 1 ? Number(item.settlementAmount || 0) : 0,
}));
this.form.projectName = first.projectName;
this.form.deptName = first.deptName;
this.form.payerName = first.payerName;
this.form.payeeName = first.payeeName;
this.settlementDialog.visible = false;
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 || [];
this.departmentEmailOptions = [
...new Set([...this.departmentEmailOptions, ...(data.departmentEmails || [])]),
];
if (!this.form.customerEmails.length && this.customerEmailOptions.length) {
this.form.customerEmails = [this.customerEmailOptions[0]];
}
},
remainingAmount(row) {
return Math.max(
Number(row.settlementAmount || 0) - Number(row.receivedInvoiceAmount || 0),
0
);
},
openSettlementDetail(row) {
const id = row.formalSettlementId || row.settlementId || row.id;
if (!id) return;
this.$router.push({
path: '/settlement/formal-settlement/form',
query: { mode: 'view', id, name: '查看正式结算' },
});
},
validateBusiness() {
const invoiceAmount = Number(this.form.invoiceAmount || 0);
if (invoiceAmount <= 0) throw new Error('开票金额必须大于0');
for (const row of this.form.settlements) {
const allocated = Number(row.allocatedInvoiceAmount);
if (!Number.isFinite(allocated) || allocated < 0) {
throw new Error('分摊发票金额必须大于或等于0');
}
if (allocated - this.remainingAmount(row) > 0.001) {
throw new Error(`结算单${row.formalSettlementNo}的累计收票金额不能超过结算总金额`);
}
}
},
payload() {
return {
id: this.form.id,
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,
phone: this.form.phone,
customerEmails: this.form.customerEmails.join(';'),
departmentEmails: this.form.departmentEmails.join(';'),
attachmentsJson: JSON.stringify(this.form.attachments || []),
remark: this.form.remark,
settlements: this.form.settlements.map(item => ({
settlementId: item.formalSettlementId || item.settlementId,
allocatedInvoiceAmount: item.allocatedInvoiceAmount,
})),
};
},
async saveDraft() {
await this.$refs.formRef.validate();
try {
this.validateBusiness();
} catch (error) {
this.$message.warning(error.message);
return false;
}
const id = this.unwrapData(await api.save(this.payload()));
this.form.id = id;
this.$message.success('保存成功');
return true;
},
async submitForm() {
const saved = await this.saveDraft();
if (!saved) return;
await api.submit({ id: this.form.id });
this.$message.success('提交成功');
this.goBack();
},
async syncReferenceData() {
if (!this.form.invoiceNo) {
this.$message.warning('请先选择金蝶票据池发票');
return;
}
const data = this.unwrapData(
await api.queryInvoicePool({ current: 1, size: 1, invoiceNo: this.form.invoiceNo })
);
const invoice = (data.records || []).find(item => item.invoiceNo === this.form.invoiceNo);
if (!invoice) {
this.$message.warning('金蝶票池未查询到该发票');
return;
}
if (!this.confirmInvoice(invoice, { preserveUserInput: true })) return;
if (this.settlementIds.length) await this.loadReferenceInformation();
this.$message.success('同步成功');
},
goBack() {
this.$router.push('/payment/invoice-receipt');
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
formatRate(value) {
return value === null || value === undefined || value === '' ? '' : `${Number(value)}%`;
},
},
};
</script>
<style scoped lang="scss">
.receipt-form-page__form {
padding-bottom: 72px;
}
.receipt-form-page__form :deep(.el-form-item) {
margin-bottom: 14px;
}
.receipt-form-page__form :deep(.el-select),
.receipt-form-page__form :deep(.el-input-number) {
width: 100%;
}
.receipt-form-page__remark :deep(.el-form-item__content) {
margin-left: 0 !important;
}
.receipt-form-page__actions {
display: flex;
justify-content: flex-end;
padding: 12px 24px;
position: fixed;
right: 0;
left: 230px;
bottom: 0;
margin: 0;
z-index: 10;
background: #fff;
border-top: 1px solid #eff1f7;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
}
:global(.avue--collapse .receipt-form-page__actions) {
left: 60px;
}
:global(.avue-layout--horizontal .receipt-form-page__actions) {
left: 0;
}
.receipt-form-page__dialog-search {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-bottom: 12px;
}
.receipt-form-page__dialog-search .el-input {
width: 360px;
}
.receipt-form-page :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
.receipt-form-page :deep(.el-table__row:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
@media (max-width: 900px) {
.receipt-form-page__form :deep(.el-col-6) {
max-width: 100%;
flex: 0 0 100%;
}
}
</style>