Files
tms-erp-web/src/views/payment/payment-application-form.vue
T
b2894lxlx 5fcb82c0c0 1、调整配载、总单
2、新增收付款模块
2026-08-22 21:11:48 +08:00

853 lines
29 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="payment-form-page">
<div class="payment-form-page__title">
{{ readonly ? '查看付款申请' : recordId ? '编辑付款申请' : '新增付款申请' }}
</div>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="right"
label-width="auto"
class="payment-form-page__form"
>
<section class="payment-form-page__section">
<div class="payment-form-page__section-title">付款基本信息</div>
<el-row :gutter="24">
<el-col :span="12"
><el-form-item label="单据号"
><el-input
v-model="form.paymentNo"
disabled
placeholder="系统自动生成" /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="付款类型" prop="paymentType"
><el-select v-model="form.paymentType" :disabled="readonly" @change="handleTypeChange"
><el-option
v-for="item in paymentTypeOptions"
:key="item.value"
v-bind="item" /></el-select></el-form-item
></el-col>
<el-col v-if="form.paymentType !== 'project_advance'" :span="12"
><el-form-item label="结算单号" prop="referenceId"
><el-input
v-model="referenceLabel"
readonly
:disabled="readonly"
placeholder="请选择预结算/正式结算单"
><template #append
><el-button :disabled="readonly" @click="openReference">选择</el-button></template
></el-input
></el-form-item
></el-col
>
<el-col :span="12"
><el-form-item label="所属项目"
><el-input v-model="form.projectName" disabled
><template v-if="form.paymentType === 'project_advance'" #append
><el-button :disabled="readonly" @click="openReference">选择</el-button></template
></el-input
></el-form-item
></el-col
>
<el-col :span="12"
><el-form-item label="合同名称"
><el-input v-model="form.contractName" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="结算金额"
><el-input v-model="form.settlementAmount" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="可付款金额"
><el-input v-model="form.payableAmount" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="票款类型"
><el-input v-model="form.billType" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="付款比例" prop="paymentRatio"
><el-input-number
v-model="form.paymentRatio"
:disabled="readonly"
:min="0"
:max="100"
:precision="2"
:controls="false"
style="width: 100%" /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="申请付款金额" prop="appliedAmount"
><el-input-number
v-model="form.appliedAmount"
:disabled="readonly"
:min="0"
:precision="2"
:controls="false"
style="width: 100%" /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="付款方式" prop="paymentMethod"
><el-select
v-model="form.paymentMethod"
:disabled="readonly"
@change="handlePaymentMethodChange"
><el-option
v-for="item in paymentMethodOptions"
:key="item.value"
v-bind="item" /></el-select></el-form-item
></el-col>
<el-col v-if="billPayment" :span="12"
><el-form-item label="汇票票据" prop="billLedgerId"
><el-select
v-model="form.billLedgerId"
:disabled="readonly"
filterable
remote
reserve-keyword
:remote-method="loadBillOptions"
:loading="billLoading"
placeholder="请选择可用汇票"
@change="handleBillChange"
><el-option
v-for="item in billOptions"
:key="item.id"
:label="`${item.billNo}|余额${formatMoney(item.availableBalance)}${
item.maturityDate
}`"
:value="item.id" /></el-select></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="收款方"
><el-input v-model="form.payeeName" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="收款账号"
><el-input v-model="form.bankAccount" :disabled="readonly" /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="开户银行"
><el-input v-model="form.bankName" :disabled="readonly" /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="申请人"
><el-input v-model="form.applicantName" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="申请日期"
><el-input v-model="form.applyDate" disabled /></el-form-item
></el-col>
<el-col :span="24"
><el-form-item label="备注"
><el-input
v-model="form.remark"
:disabled="readonly"
type="textarea"
maxlength="200"
show-word-limit /></el-form-item
></el-col>
</el-row>
</section>
<section class="payment-form-page__section">
<div class="payment-form-page__section-title">结算信息</div>
<el-table :data="settlementRows" border
><el-table-column type="index" label="序号" width="70" /><el-table-column
prop="settlementNo"
label="结算单号"
min-width="150" /><el-table-column
prop="settlementAmount"
label="结算总金额"
min-width="130" /><el-table-column
prop="payableAmount"
label="可付款金额"
min-width="130" /><el-table-column
prop="paymentRatio"
label="付款比例"
min-width="110" /><el-table-column
prop="appliedAmount"
label="付款金额"
min-width="130"
/></el-table>
</section>
<section class="payment-form-page__section">
<div class="payment-form-page__section-title">发票信息</div>
<div class="payment-form-page__section-actions">
<el-button type="primary" plain :disabled="readonly" @click="addInvoice"
>新增发票</el-button
>
</div>
<el-table :data="form.invoices" border
><el-table-column type="index" label="序号" width="70" /><el-table-column
prop="settlementNo"
label="结算单号"
min-width="120"
/><el-table-column label="发票号" min-width="140"
><template #default="{ row }"
><el-input v-model="row.invoiceNo" :disabled="readonly" /></template></el-table-column
><el-table-column label="开票日期" min-width="150"
><template #default="{ row }"
><el-date-picker
v-model="row.invoiceDate"
value-format="YYYY-MM-DD"
:disabled="readonly" /></template></el-table-column
><el-table-column label="发票类型" min-width="130"
><template #default="{ row }"
><el-input
v-model="row.invoiceType"
:disabled="readonly" /></template></el-table-column
><el-table-column label="税率" min-width="100"
><template #default="{ row }"
><el-input-number
v-model="row.taxRate"
:disabled="readonly"
:min="0"
:max="100"
:controls="false" /></template></el-table-column
><el-table-column label="发票金额(含税)" min-width="150"
><template #default="{ row }"
><el-input-number
v-model="row.invoiceAmount"
:disabled="readonly"
:min="0"
:precision="2"
:controls="false" /></template></el-table-column
><el-table-column label="匹配金额(含税)" min-width="150"
><template #default="{ row }"
><el-input-number
v-model="row.matchedAmount"
:disabled="readonly"
:min="0"
:precision="2"
:controls="false" /></template></el-table-column
><el-table-column label="操作" width="100"
><template #default="{ $index }"
><el-link v-if="!readonly" type="danger" @click="form.invoices.splice($index, 1)"
>删除</el-link
></template
></el-table-column
></el-table
>
</section>
<section class="payment-form-page__section">
<div class="payment-form-page__section-title">付款记录</div>
<el-table :data="paymentRecords" border
><el-table-column type="index" label="序号" width="70" /><el-table-column
prop="paidAmount"
label="付款金额"
min-width="160" /><el-table-column
prop="paidDate"
label="付款日期"
min-width="150" /><el-table-column
prop="paymentNo"
label="付款单号"
min-width="160" /><el-table-column
prop="kingdeeBillNo"
label="付款凭证"
min-width="160"
/></el-table>
</section>
<section class="payment-form-page__section">
<div class="payment-form-page__section-title">附件</div>
<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"
/><el-table :data="form.attachments" border
><el-table-column type="index" label="序号" width="70" /><el-table-column
label="附件类型"
width="160"
><template #default="{ row }"
><el-select v-model="row.attachmentType" :disabled="readonly"
><el-option label="磅单" value="weighing_slip" /><el-option
label="结算单"
value="settlement" /><el-option label="合同签章文件" value="contract" /><el-option
label="特批附件"
value="special_approval" /><el-option
label="其他"
value="other" /></el-select></template></el-table-column
><el-table-column prop="originalName" label="文件名" min-width="220" /><el-table-column
label="附件描述"
min-width="220"
><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="130"
/><el-table-column prop="uploadTime" label="上传时间" width="170" /><el-table-column
label="操作"
width="100"
><template #default="{ row, $index }"
><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
></template
></el-table-column
></el-table
>
</section>
<div class="payment-form-page__actions">
<el-button @click="goBack">返回</el-button
><el-button v-if="!readonly && canSave" @click="saveDraft">保存</el-button
><el-button
v-if="!readonly && canSave && hasPermission('payment_application_submit')"
type="primary"
@click="submitForm"
>提交</el-button
>
</div>
</el-form>
<el-dialog
v-model="referenceVisible"
:title="form.paymentType === 'project_advance' ? '选择项目合同' : '选择结算单'"
width="80%"
><el-table
v-if="form.paymentType === 'project_advance'"
:data="contractRows"
border
@row-click="selectContract"
><el-table-column prop="projectName" label="所属项目" /><el-table-column
prop="contractNo"
label="合同编号" /><el-table-column
prop="contractName"
label="合同名称" /><el-table-column prop="deptName" label="所属组织" /></el-table
><el-tabs v-else v-model="referenceTab"
><el-tab-pane label="正式结算单" name="formal"
><el-table :data="formalRows" border @row-click="selectFormal"
><el-table-column prop="formalSettlementNo" label="结算单号" /><el-table-column
prop="projectName"
label="所属项目" /><el-table-column
prop="contractName"
label="合同名称" /><el-table-column
prop="settlementAmount"
label="结算金额" /></el-table></el-tab-pane
><el-tab-pane label="预结算单" name="pre"
><el-table :data="preRows" border @row-click="selectPre"
><el-table-column prop="preSettlementNo" label="结算单号" /><el-table-column
prop="projectName"
label="所属项目" /><el-table-column
prop="contractName"
label="合同名称" /><el-table-column
prop="settlementAmount"
label="结算金额" /></el-table></el-tab-pane></el-tabs
></el-dialog>
</basic-container>
</template>
<script>
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/paymentApplication';
import * as billLedgerApi from '@/api/payment/billLedger';
import * as formalApi from '@/api/settlement/formalSettlement';
import * as preApi from '@/api/settlement/preSettlement';
const emptyForm = () => ({
id: null,
paymentNo: '',
paymentType: 'project_advance',
settlementId: null,
preSettlementId: null,
projectId: null,
projectName: '',
deptId: null,
deptName: '',
contractId: null,
contractNo: '',
contractName: '',
payerName: '',
payeeName: '',
settlementAmount: 0,
payableAmount: 0,
billType: '',
paymentRatio: 50,
appliedAmount: 0,
paymentMethod: 'bank_transfer',
billLedgerId: null,
billNo: '',
receiptAccountId: null,
receiptAccountName: '',
bankName: '',
bankAccount: '',
applicantName: '',
applyDate: '',
remark: '',
attachments: [],
invoices: [],
});
export default {
name: 'PaymentApplicationForm',
data() {
return {
form: emptyForm(),
amountSyncing: false,
paymentRecords: [],
referenceVisible: false,
referenceTab: 'formal',
formalRows: [],
preRows: [],
contractRows: [],
billOptions: [],
billLoading: false,
paymentTypeOptions: api.paymentTypeOptions,
paymentMethodOptions: api.paymentMethodOptions,
attachmentFileTypes: [
'pdf',
'bmp',
'jpeg',
'png',
'jpg',
'doc',
'docx',
'ppt',
'pptx',
'xlsx',
'xls',
'eml',
'msg',
'zip',
'rar',
],
rules: {
paymentType: [{ required: true, message: '请选择付款类型' }],
referenceId: [{ validator: this.validateReference, trigger: 'change' }],
paymentMethod: [{ required: true, message: '请选择付款方式' }],
billLedgerId: [{ validator: this.validateBillLedger, trigger: 'change' }],
appliedAmount: [{ validator: this.validateAppliedAmount, trigger: 'change' }],
},
};
},
computed: {
...mapGetters(['permission']),
recordId() {
return this.$route.query.id || '';
},
readonly() {
return this.$route.query.mode === 'view';
},
canSave() {
const code = this.recordId ? 'payment_application_edit' : 'payment_application_add';
return this.hasPermission(code);
},
billPayment() {
return ['bank_draft', 'commercial_draft'].includes(this.form.paymentMethod);
},
referenceLabel() {
return this.form.settlementNo || this.form.preSettlementNo || '';
},
settlementRows() {
if (!this.referenceLabel) return [];
return [
{
settlementNo: this.referenceLabel,
settlementAmount: this.form.settlementAmount,
payableAmount: this.form.payableAmount,
paymentRatio: this.form.paymentRatio,
appliedAmount: this.form.appliedAmount,
},
];
},
},
watch: {
'form.paymentRatio'(value) {
if (this.amountSyncing || !Number(this.form.payableAmount)) return;
this.amountSyncing = true;
this.form.appliedAmount = Number(
((Number(this.form.payableAmount) * Number(value || 0)) / 100).toFixed(2)
);
this.$nextTick(() => {
this.amountSyncing = false;
});
},
'form.appliedAmount'(value) {
if (this.amountSyncing || !Number(this.form.payableAmount)) return;
this.amountSyncing = true;
this.form.paymentRatio = Number(
((Number(value || 0) / Number(this.form.payableAmount)) * 100).toFixed(2)
);
this.$nextTick(() => {
this.amountSyncing = false;
});
},
},
created() {
this.initialize();
},
methods: {
hasPermission(code) {
return this.permission?.[code] !== false;
},
validateReference(rule, value, callback) {
if (this.form.paymentType === 'progress_advance' && !this.form.preSettlementId) {
callback(new Error('请选择预结算单'));
return;
}
if (this.form.paymentType === 'settlement_payment' && !this.form.settlementId) {
callback(new Error('请选择正式结算单'));
return;
}
callback();
},
validateAppliedAmount(rule, value, callback) {
const amount = Number(value);
const payableAmount = Number(this.form.payableAmount || 0);
if (!Number.isFinite(amount) || amount < 0) {
callback(new Error('申请付款金额不能小于0'));
return;
}
if (payableAmount > 0 && amount > payableAmount) {
callback(new Error('申请付款金额不能超过可付款金额'));
return;
}
callback();
},
validateBillLedger(rule, value, callback) {
if (!this.billPayment) {
callback();
return;
}
if (!value) {
callback(new Error('请选择可用汇票'));
return;
}
const selected = this.billOptions.find(item => String(item.id) === String(value));
if (
selected &&
Number(this.form.appliedAmount || 0) > Number(selected.availableBalance || 0)
) {
callback(new Error('申请付款金额不能超过汇票可用余额'));
return;
}
callback();
},
validateInvoices() {
const appliedAmount = Number(this.form.appliedAmount || 0);
let matchedTotal = 0;
for (const invoice of this.form.invoices || []) {
const invoiceAmount = Number(invoice.invoiceAmount || 0);
const matchedAmount = Number(invoice.matchedAmount || 0);
if (invoiceAmount < 0 || matchedAmount < 0) {
throw new Error('发票金额和匹配金额不能小于0');
}
if (matchedAmount > invoiceAmount) {
throw new Error('单张发票匹配金额不能超过发票金额');
}
matchedTotal += matchedAmount;
}
if (matchedTotal > appliedAmount) {
throw new Error('发票匹配金额合计不能超过申请付款金额');
}
},
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
},
async initialize() {
if (this.recordId) {
const data = this.unwrapData(await api.getDetail(this.recordId));
this.form = {
...emptyForm(),
...data,
attachments: this.parse(data.attachmentsJson),
invoices: data.invoices || [],
};
this.paymentRecords = data.paymentRecords || [];
if (this.billPayment) await this.loadBillOptions('', data.billLedgerId);
} else {
this.form.applyDate = this.$dayjs().format('YYYY-MM-DD');
this.form.applicantName =
this.$store.getters.userInfo?.realName || this.$store.getters.userInfo?.userName || '';
}
},
parse(value) {
if (!value) return [];
if (Array.isArray(value)) return value;
try {
return JSON.parse(value) || [];
} catch {
return [];
}
},
handleTypeChange() {
if (this.form.paymentType === 'project_advance') {
this.form.settlementId = null;
this.form.preSettlementId = null;
}
},
async handlePaymentMethodChange() {
if (!this.billPayment) {
this.form.billLedgerId = null;
this.form.billNo = '';
this.billOptions = [];
return;
}
await this.loadBillOptions();
},
async loadBillOptions(keyword = '', selectedId = this.form.billLedgerId) {
if (!this.billPayment) return;
this.billLoading = true;
try {
this.billOptions =
this.unwrapData(
await billLedgerApi.getAvailableOptions(keyword, this.form.deptId, selectedId)
) || [];
} finally {
this.billLoading = false;
}
},
handleBillChange(id) {
const selected = this.billOptions.find(item => String(item.id) === String(id));
this.form.billNo = selected?.billNo || '';
this.$refs.formRef?.validateField('billLedgerId').catch(() => {});
},
async loadReferences() {
const [formalResponse, preResponse, contractResponse] = await Promise.all([
formalApi.getList(1, 100, { settlementType: 'payable', approvalStatus: 'approved' }),
preApi.getList(1, 100, { settlementType: 'payable', approvalStatus: 'approved' }),
formalApi.getContractOptions(''),
]);
const formal = this.unwrapData(formalResponse);
const pre = this.unwrapData(preResponse);
this.formalRows = formal.records || [];
this.preRows = pre.records || [];
this.contractRows = this.unwrapData(contractResponse) || [];
},
async openReference() {
await this.loadReferences();
this.referenceVisible = true;
},
selectContract(row) {
Object.assign(this.form, {
projectId: row.projectId,
projectName: row.projectName,
deptId: row.deptId,
deptName: row.deptName,
contractId: row.id,
contractNo: row.contractNo,
contractName: row.contractName,
payerName: row.payerName,
payeeName: row.payeeName,
billType: '项目预付',
});
this.referenceVisible = false;
if (this.billPayment) this.loadBillOptions();
},
selectFormal(row) {
Object.assign(this.form, {
settlementId: row.id,
preSettlementId: null,
settlementNo: row.formalSettlementNo,
projectName: row.projectName,
deptId: row.deptId,
deptName: row.deptName,
contractName: row.contractName,
settlementAmount: row.settlementAmount,
payableAmount: Math.max(
0,
Number(row.settlementAmount || 0) - Number(row.appliedPaymentAmount || 0)
),
billType: '正式结算单',
payeeName: row.payeeName,
});
this.referenceVisible = false;
if (this.billPayment) this.loadBillOptions();
},
selectPre(row) {
Object.assign(this.form, {
preSettlementId: row.id,
settlementId: null,
preSettlementNo: row.preSettlementNo,
projectName: row.projectName,
deptId: row.deptId,
deptName: row.deptName,
contractName: row.contractName,
settlementAmount: row.settlementAmount,
payableAmount: Math.max(
0,
Number(row.settlementAmount || 0) - Number(row.advanceAppliedAmount || 0)
),
billType: '预结算单',
payeeName: row.payeeName,
});
this.referenceVisible = false;
if (this.billPayment) this.loadBillOptions();
},
addInvoice() {
this.form.invoices.push({
settlementNo: this.referenceLabel,
invoiceNo: '',
invoiceDate: '',
invoiceType: '',
taxRate: 3,
invoiceAmount: 0,
matchedAmount: 0,
attachmentJson: '',
});
},
normalizeAttachments(files) {
const userName =
this.$store.getters.userInfo?.realName || this.$store.getters.userInfo?.userName || '';
const time = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
this.form.attachments = (files || []).map(file => ({
...file,
attachmentType: file.attachmentType || 'other',
description: file.description || '',
uploadUserName: file.uploadUserName || userName,
uploadTime: file.uploadTime || time,
}));
},
payload() {
const {
id,
paymentType,
settlementId,
preSettlementId,
projectId,
projectName,
deptId,
deptName,
contractId,
contractNo,
contractName,
payerName,
payeeName,
settlementAmount,
payableAmount,
billType,
paymentRatio,
appliedAmount,
paymentMethod,
billLedgerId,
billNo,
receiptAccountId,
receiptAccountName,
bankName,
bankAccount,
attachments,
remark,
invoices,
} = this.form;
return {
id,
paymentType,
settlementId,
preSettlementId,
projectId,
projectName,
deptId,
deptName,
contractId,
contractNo,
contractName,
payerName,
payeeName,
settlementAmount,
payableAmount,
billType,
paymentRatio,
appliedAmount,
paymentMethod,
billLedgerId,
billNo,
receiptAccountId,
receiptAccountName,
bankName,
bankAccount,
attachmentsJson: JSON.stringify(attachments || []),
remark,
invoices,
};
},
async saveDraft() {
await this.$refs.formRef.validate();
if (this.form.paymentType === 'project_advance' && !this.form.projectId) {
this.$message.warning('请选择所属项目');
return false;
}
try {
this.validateInvoices();
} catch (error) {
this.$message.warning(error.message);
return false;
}
const data = this.unwrapData(await api.save(this.payload()));
this.form.id = data;
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();
},
goBack() {
this.$router.push('/payment/payment-application');
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
},
};
</script>
<style scoped lang="scss">
.payment-form-page__title,
.payment-form-page__section-title {
display: flex;
align-items: center;
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
}
.payment-form-page__title::before,
.payment-form-page__section-title::before {
width: 4px;
height: 20px;
margin-right: 8px;
background: #409eff;
content: '';
}
.payment-form-page__section {
margin-bottom: 24px;
}
.payment-form-page__section-title {
font-size: 16px;
}
.payment-form-page__section-actions {
display: flex;
justify-content: flex-end;
margin-bottom: 8px;
}
.payment-form-page__actions {
display: flex;
justify-content: center;
gap: 16px;
padding: 24px 0;
border-top: 1px solid #eff1f7;
}
.payment-form-page :deep(.el-form-item) {
margin-bottom: 16px;
}
.payment-form-page :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
.payment-form-page :deep(.el-table__body tr:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
:deep(.payment-form-page.basic-container .basic-container__card > .el-card__body) {
padding: 0;
}
</style>