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

685 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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="附件信息">
<!-- 金蝶票池认领的发票附件为 piaozone path 型,需带发票号走 A801-2 代理查看/下载 -->
<vehicle-attachment-table
v-model="form.attachments"
:readonly="readonly"
show-view-actions
:kingdee-invoice-no="form.invoiceNo"
/>
</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 v-if="!isPublicViewPage" 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"
row-key="id"
border
@selection-change="settlementDialog.selection = $event"
>
<el-table-column type="selection" width="52" align="center" reserve-selection />
<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">
确定{{ settlementDialog.selection.length ? `(${settlementDialog.selection.length})` : '' }}
</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';
import { getMkPublicDetail } from '@/api/mk-process';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
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' || this.isPublicViewPage;
},
isPublicViewPage() {
return this.$route.path === '/payment/invoice-receipt/public-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() {
if (this.isPublicViewPage) {
await this.loadPublicDetail();
return;
}
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.applyDetail(data);
if (this.settlementIds.length) await this.loadReferenceInformation();
},
async loadPublicDetail() {
if (!this.recordId) return;
const data = this.unwrapData(await getMkPublicDetail('invoice-receipt', this.recordId));
this.applyDetail(data);
},
applyDetail(data) {
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),
};
},
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;
this.settlementDialog.selection = [];
await this.loadSettlementCandidates();
await this.$nextTick();
const selectedIds = new Set(this.settlementIds.map(id => String(id)));
this.settlementDialog.rows.forEach(row => {
if (selectedIds.has(String(row.id))) {
this.$refs.settlementTable?.toggleRowSelection(row, true);
}
});
},
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;
}
const previousAmount = new Map(
this.form.settlements.map(item => [
String(item.formalSettlementId || item.settlementId),
item.allocatedInvoiceAmount,
])
);
this.form.settlements = rows.map(item => ({
...item,
formalSettlementId: item.id,
settlementId: item.id,
allocatedInvoiceAmount: previousAmount.has(String(item.id))
? Number(previousAmount.get(String(item.id)) || 0)
: 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(() => {});
},
selectSettlement(row) {
this.$refs.settlementTable?.toggleRowSelection(row);
},
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;
const previousApprovalStatus = this.form.approvalStatus || '';
await api.submit({ id: this.form.id });
await submitMkApprovalFlow({
bizType: 'invoice-receipt',
bizId: this.form.id,
subjectName: this.form.invoiceNo || '',
approvalStatus: previousApprovalStatus,
mkEnabled: await loadMkEnabled().catch(() => false),
});
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: 24px;
}
.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>