调整收付款问题

This commit is contained in:
2026-08-31 12:40:35 +08:00
parent 2451efdfcb
commit dea760ea1c
12 changed files with 544 additions and 164 deletions
@@ -51,9 +51,9 @@
<vehicle-attachment-upload
:model-value="rows"
:file-types="attachmentFileTypes"
:max-size="500"
:max-size="maxSize"
:show-tip="true"
tip="支持pdfbmpjpegpngjpgdocdocxpptpptxxlsxxlsemlmsgzip的文件格式单个文件不超过500M"
:tip="`支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式单个文件不超过${maxSize}MB`"
:show-file-list="false"
button-text="上传附件"
@update:model-value="handleUploadChange"
@@ -149,6 +149,10 @@ export default {
type: Boolean,
default: false,
},
maxSize: {
type: Number,
default: 500,
},
},
emits: ['update:modelValue'],
data() {
+1 -1
View File
@@ -1,5 +1,5 @@
export const receiptFlowTableColumns = [
{ prop: 'receiptNoticeNo', label: '认领通知单', minWidth: 150 },
{ prop: 'receiptNoticeNo', label: '认领通知单', minWidth: 150, link: true },
{ prop: 'payerName', label: '付款人', minWidth: 130 },
{ prop: 'receiptAmount', label: '付款金额', minWidth: 130, money: true },
{ prop: 'counterpartyName', label: '对方户名', minWidth: 150 },
+12 -2
View File
@@ -10,8 +10,18 @@ export const preSettlementTableColumns = [
{ label: '原币结算金额', prop: 'settlementAmount', minWidth: 140, money: true, precision: 2 },
{ label: '本位币结算金额', prop: 'localSettlementAmount', minWidth: 150, money: true, precision: 2 },
{ label: '结算汇率', prop: 'exchangeRate', minWidth: 110, precision: 2 },
{ label: '申请预付金额', prop: 'advanceAppliedAmount', minWidth: 140, money: true },
{ label: '已付款金额', prop: 'advancePaidAmount', minWidth: 130, money: true },
{
label: '申请预付金额(审核中的)',
prop: 'advanceAppliedAmount',
minWidth: 190,
money: true,
},
{
label: '已付款金额(审核通过的)',
prop: 'advancePaidAmount',
minWidth: 200,
money: true,
},
{ label: '审核状态', prop: 'approvalStatusName', minWidth: 110, status: true },
{ label: '当前节点', prop: 'currentNode', minWidth: 130 },
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 130 },
+1 -1
View File
@@ -75,7 +75,7 @@
</section-card>
<section-card title="附件信息">
<vehicle-attachment-table v-model="form.attachments" :readonly="readonly" />
<vehicle-attachment-table v-model="form.attachments" :readonly="readonly" :max-size="10" />
</section-card>
<div class="bill-payment-form-page__actions">
+13
View File
@@ -15,6 +15,8 @@
@edit="openEdit"
@delete="handleDelete"
@flow="openFlow"
@approve="handleApprove"
@return="handleReturn"
@void="handleVoid"
@page-change="handlePageChange"
@size-change="handleSizeChange"
@@ -126,6 +128,17 @@ export default {
this.flowDialog.row = row;
this.flowDialog.visible = true;
},
async handleApprove(row) {
await api.approve({ id: row.id });
this.$message.success('审批通过');
this.loadTable();
},
async handleReturn(row) {
const { value } = await this.$prompt('请输入驳回原因', '审批驳回');
await api.returnBill({ id: row.id, reason: value });
this.$message.success('已驳回');
this.loadTable();
},
async handleVoid(row) {
const { value } = await this.$prompt('请输入作废原因', '作废汇票付款');
await api.voidBill({ id: row.id, reason: value });
@@ -22,7 +22,7 @@
<span v-else>{{ displayValue(row[column.prop]) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right" align="center">
<el-table-column label="操作" width="320" fixed="right" align="center">
<template #default="{ row }">
<div class="bill-payment-table__links">
<el-link
@@ -55,6 +55,18 @@
@click="$emit('flow', row)"
>流程</el-link
>
<el-link
v-if="row.approvalStatus === 'reviewing' && hasPermission('bill_payment_approve')"
type="primary"
@click="$emit('approve', row)"
>通过</el-link
>
<el-link
v-if="row.approvalStatus === 'reviewing' && hasPermission('bill_payment_approve')"
type="danger"
@click="$emit('return', row)"
>驳回</el-link
>
<el-link
v-if="row.approvalStatus === 'approved' && hasPermission('bill_payment_void')"
type="danger"
@@ -90,7 +102,18 @@ export default {
loading: { type: Boolean, default: false },
page: { type: Object, required: true },
},
emits: ['add', 'view', 'edit', 'delete', 'flow', 'void', 'page-change', 'size-change'],
emits: [
'add',
'view',
'edit',
'delete',
'flow',
'approve',
'return',
'void',
'page-change',
'size-change',
],
data() {
return { columns: billPaymentTableColumns };
},
+80 -32
View File
@@ -188,7 +188,10 @@
</el-row>
</section-card>
<section-card title="开票内容"><template #extra><el-button v-if="!readonly" type="primary" @click="addSheet">添加</el-button></template>
<section-card title="开票内容"
><template #extra
><el-button v-if="!readonly" type="primary" @click="addSheet">添加</el-button></template
>
<div v-for="(sheet, sheetIndex) in form.sheets" :key="sheet.localId" class="invoice-sheet">
<div class="invoice-sheet__header">
<span>第{{ sheetIndex + 1 }}张</span>
@@ -247,17 +250,6 @@
/>
</template>
</el-table-column>
<el-table-column label="不含税单价" min-width="130">
<template #default="{ row }">
<el-input-number
v-model="row.unitPriceNoTax"
:disabled="readonly"
:min="0"
:precision="2"
:controls="false"
/>
</template>
</el-table-column>
<el-table-column label="含税金额" min-width="130">
<template #default="{ row }">
<el-input-number
@@ -362,7 +354,11 @@
show-overflow-tooltip
>
<template #default="{ row }">
<el-link v-if="column.link && row[column.prop]" type="primary">
<el-link
v-if="column.link && row[column.prop]"
type="primary"
@click.stop="handleDetailLink(row, column)"
>
{{ row[column.prop] }}
</el-link>
<span
@@ -371,9 +367,13 @@
Number(row[column.prop]) === -1
"
></span>
<span v-else-if="column.feeItemKeys">{{ formatMoney(getFeeItemValue(row, column.feeItemKeys)) }}</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-if="column.prop === 'transportType'">{{
transportTypeLabel(row.transportType)
}}</span>
<span v-else>{{ displayValue(row[column.prop]) }}</span>
</template>
</el-table-column>
@@ -400,17 +400,15 @@
<div class="invoice-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" type="primary" plain @click="saveDraft"
>保存</el-button
>
<el-button
v-if="!readonly && canSave && hasPermission('invoice_application_submit')"
type="primary"
@click="submitForm"
>提交</el-button>
>提交</el-button
>
</div>
</el-form>
@@ -468,6 +466,8 @@
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/invoiceApplication';
import { getAll as getInvoiceItems } from '@/api/base/invoice-item';
import { getList as getWaybillList } from '@/api/business/waybill-manage';
import { getList as getReceivablePayableDetailList } from '@/api/settlement/receivable-payable-detail';
import { getDictionary } from '@/api/system/dictbiz';
import { invoiceApplicationDetailColumns } from '@/option/payment/invoiceApplication';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
@@ -662,7 +662,8 @@ export default {
if (this.settlementIds.length) {
await Promise.all([this.loadReceiverInformation(), this.loadSettlementDetails(false)]);
}
if (this.readonly) this.$nextTick(() => this.$refs.formRef?.clearValidate('departmentEmails'));
if (this.readonly)
this.$nextTick(() => this.$refs.formRef?.clearValidate('departmentEmails'));
this.$nextTick(this.restoreDetailSelection);
},
async loadInvoiceItems() {
@@ -683,8 +684,7 @@ export default {
transportTypeLabel(value) {
if (!value) return '-';
return (
this.transportTypeOptions.find(item => String(item.value) === String(value))?.label ||
value
this.transportTypeOptions.find(item => String(item.value) === String(value))?.label || value
);
},
invoiceItemNames(categoryName) {
@@ -840,7 +840,7 @@ export default {
...item,
formalSettlementId: item.id,
settlementId: item.id,
allocatedInvoiceAmount: 0,
allocatedInvoiceAmount: rows.length === 1 ? Number(item.availableInvoiceAmount || 0) : 0,
}));
this.form.projectName = first.projectName;
this.form.deptName = first.deptName;
@@ -906,12 +906,10 @@ export default {
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.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())
@@ -962,6 +960,56 @@ export default {
if (this.readonly) return;
this.selectedDetailIds = rows.map(item => item.formalSettlementDetailId || item.id);
},
handleDetailLink(row, column) {
if (column.prop === 'documentNo') {
this.openPayableDetail(row);
return;
}
if (column.prop === 'waybillNo') this.openWaybillDetail(row);
},
async openPayableDetail(row) {
let detailId = row.sourceDetailId || '';
if (!detailId && row.documentNo) {
try {
const data = this.unwrapData(
await getReceivablePayableDetailList(1, 1, {
documentNo: row.documentNo,
settlementType: 'payable',
})
);
detailId = (Array.isArray(data) ? data : data?.records || [])[0]?.id || '';
} catch {
detailId = '';
}
}
if (!detailId) {
this.$message.info('当前记录未找到对应的应付明细');
return;
}
this.$router.push({
path: '/settlement/payable-detail',
query: { detailId },
});
},
async openWaybillDetail(row) {
let waybillId = row.waybillId || '';
if (!waybillId && row.waybillNo) {
try {
const data = this.unwrapData(await getWaybillList(1, 1, { waybillNo: row.waybillNo }));
waybillId = (Array.isArray(data) ? data : data?.records || [])[0]?.id || '';
} catch {
waybillId = '';
}
}
if (!waybillId) {
this.$message.info('当前记录未找到对应的运单详情');
return;
}
this.$router.push({
path: '/business/waybill-manage',
query: { detailId: waybillId },
});
},
restoreDetailSelection() {
if (this.readonly || !this.$refs.detailTable) return;
const selected = new Set(this.selectedDetailIds.map(String));
+136 -85
View File
@@ -163,6 +163,7 @@
v-loading="invoiceDialog.loading"
:data="invoiceDialog.rows"
border
empty-text="暂无票据数据"
highlight-current-row
@current-change="invoiceDialog.current = $event"
@row-dblclick="confirmInvoice"
@@ -188,6 +189,17 @@
</template>
</el-table-column>
</el-table>
<div class="receipt-form-page__dialog-pagination">
<el-pagination
v-model:current-page="invoiceDialog.page.current"
v-model:page-size="invoiceDialog.page.size"
:total="invoiceDialog.page.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="refreshInvoicePoolPage"
@size-change="handleInvoicePoolSizeChange"
/>
</div>
<template #footer>
<el-button @click="invoiceDialog.visible = false">取消</el-button>
<el-button type="primary" @click="confirmInvoice()">确定</el-button>
@@ -247,67 +259,12 @@
import { Search } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/invoiceReceipt';
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
const invoicePoolMockTemplates = [
{
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 mockInvoiceTypes = ['增值税专用发票', '增值税普通发票'];
const mockTaxRates = [3, 6, 9, 13];
const mockBankNames = ['中国工商银行', '中国农业银行', '中国银行', '中国建设银行', '招商银行'];
const randomDigits = length =>
Array.from({ length }, () => Math.floor(Math.random() * 10)).join('');
@@ -319,28 +276,59 @@ const formatMockDate = date => {
return `${year}-${month}-${day}`;
};
const createInvoicePoolMockRows = () =>
invoicePoolMockTemplates.map((template, index) => {
const invoiceDateValue = new Date();
invoiceDateValue.setDate(invoiceDateValue.getDate() - Math.floor(Math.random() * 15));
const invoiceDate = formatMockDate(invoiceDateValue);
const invoiceAmount = Math.floor(10000 + Math.random() * 190000);
const taxAmount = Number(
(
(invoiceAmount * Number(template.taxRate || 0)) /
(100 + Number(template.taxRate || 0))
).toFixed(2)
);
return {
...template,
id: Date.now() + index * 100 + Math.floor(Math.random() * 100),
invoiceNo: `25${randomDigits(18)}`,
invoiceDate,
invoiceAmount,
taxAmount,
kingdeeBillNo: `KD-FP-${invoiceDate.replaceAll('-', '')}${randomDigits(3)}`,
};
const customerName = customer =>
customer.fullName || customer.shortName || customer.customerCode || `客商${customer.id}`;
const customerEmail = (customer, index) => {
const prefix = String(customer.customerCode || customer.id || `customer${index + 1}`)
.replace(/[^a-zA-Z0-9_-]/g, '')
.toLowerCase();
return `finance@${prefix || `customer${index + 1}`}.example.com`;
};
const createInvoicePoolMockRows = (customers, counterpartyName, departmentEmail) => {
const rows = [];
const baseId = Date.now();
customers.forEach((customer, customerIndex) => {
const name = customerName(customer);
['issuer', 'receiver'].forEach((role, roleIndex) => {
const count = 3 + Math.floor(Math.random() * 5);
for (let index = 0; index < count; index += 1) {
const rowIndex = rows.length;
const invoiceDateValue = new Date();
invoiceDateValue.setDate(invoiceDateValue.getDate() - Math.floor(Math.random() * 90));
const invoiceDate = formatMockDate(invoiceDateValue);
const taxRate = mockTaxRates[(customerIndex + roleIndex + index) % mockTaxRates.length];
const invoiceAmount = Math.floor(10000 + Math.random() * 190000);
const taxAmount = Number(
((invoiceAmount * Number(taxRate)) / (100 + Number(taxRate))).toFixed(2)
);
const bankName = mockBankNames[(customerIndex + index) % mockBankNames.length];
rows.push({
id: baseId + rowIndex,
invoiceNo: `25${randomDigits(18)}`,
invoiceDate,
invoiceType: mockInvoiceTypes[(customerIndex + index) % mockInvoiceTypes.length],
taxRate,
invoiceAmount,
taxAmount,
receiverName: role === 'receiver' ? name : counterpartyName,
issuerName: role === 'issuer' ? name : counterpartyName,
bankName,
bankAccount: randomDigits(19),
issuingBank: `${bankName}营业部`,
phone: customer.contactPhone || '',
customerEmails: customerEmail(customer, customerIndex),
departmentEmails: departmentEmail || '',
attachmentsJson: '[]',
kingdeeBillNo: `KD-FP-${invoiceDate.replaceAll('-', '')}${randomDigits(5)}`,
kingdeeStatus: 'unsynced',
});
}
});
});
return rows;
};
const emptyForm = () => ({
id: null,
@@ -380,9 +368,18 @@ export default {
Search,
form: emptyForm(),
invoicePoolMockRows: [],
invoicePoolMockLoaded: false,
customerEmailOptions: [],
departmentEmailOptions: [],
invoiceDialog: { visible: false, loading: false, keyword: '', rows: [], current: null },
invoiceDialog: {
visible: false,
loading: false,
keyword: '',
sourceRows: [],
rows: [],
current: null,
page: { current: 1, size: 10, total: 0 },
},
settlementDialog: {
visible: false,
loading: false,
@@ -447,7 +444,6 @@ export default {
.filter(Boolean);
},
async initialize() {
this.invoicePoolMockRows = createInvoicePoolMockRows();
const userInfo = this.$store.getters.userInfo || {};
const userEmail = userInfo.email || '';
this.departmentEmailOptions = userEmail ? [userEmail] : [];
@@ -499,22 +495,72 @@ export default {
async openInvoiceDialog() {
this.invoiceDialog.visible = true;
this.invoiceDialog.keyword = this.form.invoiceNo || '';
if (!this.recordId) this.invoicePoolMockRows = createInvoicePoolMockRows();
await this.loadInvoicePool();
},
async loadInvoicePool() {
this.invoiceDialog.loading = true;
this.invoiceDialog.page.current = 1;
this.invoiceDialog.current = null;
try {
if (!this.recordId) {
this.invoiceDialog.rows = this.filterInvoicePoolMockRows(this.invoiceDialog.keyword);
if (!this.invoicePoolMockLoaded) await this.loadInvoicePoolMockRows();
this.invoiceDialog.sourceRows = this.filterInvoicePoolMockRows(
this.invoiceDialog.keyword
);
this.refreshInvoicePoolPage();
return;
}
const data = this.unwrapData(await api.getInvoicePool(this.invoiceDialog.keyword));
this.invoiceDialog.rows = Array.isArray(data) ? data : data?.records || [];
this.invoiceDialog.sourceRows = Array.isArray(data) ? data : data?.records || [];
this.refreshInvoicePoolPage();
} finally {
this.invoiceDialog.loading = false;
}
},
async loadInvoicePoolMockRows() {
const pageSize = 500;
const firstData = this.unwrapData(await getCustomerList(1, pageSize, {}));
const firstRows = Array.isArray(firstData) ? firstData : firstData?.records || [];
const total = Number(firstData?.total || firstRows.length);
const pageCount = Math.ceil(total / pageSize);
const remainingPages = await Promise.all(
Array.from({ length: Math.max(pageCount - 1, 0) }, (_, index) =>
getCustomerList(index + 2, pageSize, {})
)
);
const customers = [
...firstRows,
...remainingPages.flatMap(response => {
const data = this.unwrapData(response);
return Array.isArray(data) ? data : data?.records || [];
}),
].filter(
(customer, index, rows) =>
rows.findIndex(item => String(item.id) === String(customer.id)) === index
);
const userInfo = this.$store.getters.userInfo || {};
const counterpartyName = userInfo.deptName || userInfo.dept_name || '本企业';
this.invoicePoolMockRows = createInvoicePoolMockRows(
customers,
counterpartyName,
userInfo.email || ''
);
this.invoicePoolMockLoaded = true;
},
refreshInvoicePoolPage() {
const page = this.invoiceDialog.page;
const total = this.invoiceDialog.sourceRows.length;
const maxPage = Math.max(Math.ceil(total / page.size), 1);
if (page.current > maxPage) page.current = maxPage;
const start = (page.current - 1) * page.size;
page.total = total;
this.invoiceDialog.current = null;
this.invoiceDialog.rows = this.invoiceDialog.sourceRows.slice(start, start + page.size);
},
handleInvoicePoolSizeChange() {
this.invoiceDialog.page.current = 1;
this.refreshInvoicePoolPage();
},
filterInvoicePoolMockRows(keyword) {
const normalizedKeyword = String(keyword || '')
.trim()
@@ -781,6 +827,11 @@ export default {
.receipt-form-page__dialog-search .el-input {
width: 360px;
}
.receipt-form-page__dialog-pagination {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
.receipt-form-page :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
+135 -32
View File
@@ -238,9 +238,19 @@
<el-table-column label="匹配金额(含税)" min-width="150">
<template #default="{ row }">{{ formatMoney(row.matchedAmount) }}</template>
</el-table-column>
<el-table-column v-if="!readonly" label="操作" width="100">
<template #default="{ $index }">
<el-link type="danger" @click="form.invoices.splice($index, 1)">删除</el-link>
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row, $index }">
<div class="payment-form-page__invoice-actions">
<el-link type="primary" @click="viewInvoiceAttachment(row)">查看</el-link>
<el-link type="primary" @click="downloadInvoiceAttachment(row)">下载</el-link>
<el-link
v-if="!readonly"
type="danger"
@click="form.invoices.splice($index, 1)"
>
删除
</el-link>
</div>
</template>
</el-table-column>
</el-table>
@@ -333,7 +343,7 @@
>
<div class="payment-form-page__attachment-upload">
<vehicle-attachment-upload
v-model="form.attachments"
:model-value="form.attachments"
:readonly="readonly"
:multiple="true"
:limit="20"
@@ -341,7 +351,7 @@
:file-types="attachmentFileTypes"
:show-file-list="false"
button-text="上传附件"
@change="normalizeAttachments"
@update:model-value="normalizeAttachments"
@success="handleManualAttachmentUpload"
/>
</div>
@@ -598,7 +608,7 @@ const emptyForm = () => ({
settlementAmount: 0,
payableAmount: 0,
billType: '',
paymentRatio: 50,
paymentRatio: null,
appliedAmount: 0,
paymentMethod: 'bank_transfer',
billLedgerId: null,
@@ -661,6 +671,8 @@ export default {
receiptAccountLoading: false,
firstContractPayment: false,
contractSignatureFileAvailable: false,
contractPaymentRatioLimit: null,
contractPaymentRatioExceeded: false,
payeeCustomerLevel: '',
attachmentRuleLoading: false,
attachmentImagePreviewVisible: false,
@@ -794,6 +806,7 @@ export default {
}
},
'form.paymentRatio'(value) {
this.checkContractPaymentRatio(value);
if (this.amountSyncing || !this.paymentAmountBase) return;
this.amountSyncing = true;
this.form.appliedAmount = Number(
@@ -1013,7 +1026,8 @@ export default {
);
},
isValidReferenceId(id) {
return id !== null && id !== undefined && String(id).trim() !== '-1';
const value = String(id ?? '').trim();
return value !== '' && value !== '-1' && value !== '0';
},
async loadTransferredPreSettlements() {
const fallbackRows = this.transferPayload?.sourcePreSettlements || [];
@@ -1246,6 +1260,44 @@ export default {
return [];
}
},
invoiceAttachment(row = {}) {
const value = row.attachmentJson;
if (!value) return {};
if (typeof value === 'object') return Array.isArray(value) ? value[0] || {} : value;
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed[0] || {} : parsed || {};
} catch {
return {};
}
},
invoiceAttachmentUrl(row) {
return this.attachmentFileUrl(this.invoiceAttachment(row));
},
invoiceAttachmentName(row) {
return (
this.attachmentFileName(this.invoiceAttachment(row)) || `${row.invoiceNo || '发票'}.pdf`
);
},
viewInvoiceAttachment(row) {
const attachment = this.invoiceAttachment(row);
if (!this.attachmentFileUrl(attachment)) {
this.$message.warning('当前发票暂无可查看附件');
return;
}
this.previewAttachment(
{ ...attachment, name: this.invoiceAttachmentName(row) },
[attachment]
);
},
downloadInvoiceAttachment(row) {
const url = this.invoiceAttachmentUrl(row);
if (!url) {
this.$message.warning('当前发票暂无可下载附件');
return;
}
downloadFileByUrl(url, this.invoiceAttachmentName(row));
},
attachmentFileName(file) {
return String(file?.originalName || file?.name || file?.fileName || '').trim();
},
@@ -1282,14 +1334,14 @@ export default {
this.attachmentFileExtension(file) === 'pdf'
);
},
previewAttachment(file) {
previewAttachment(file, files = this.form.attachments) {
const url = this.attachmentFileUrl(file);
if (!url) {
this.$message.warning('附件地址为空,无法预览');
return;
}
if (this.isAttachmentImage(file)) {
this.attachmentImagePreviewUrls = (this.form.attachments || [])
this.attachmentImagePreviewUrls = (files || [])
.filter(item => this.isAttachmentImage(item) && this.attachmentFileUrl(item))
.map(item => this.attachmentFileUrl(item));
this.attachmentImagePreviewIndex = Math.max(
@@ -1384,22 +1436,29 @@ export default {
if (imported.length) this.form.attachments = [...this.form.attachments, ...imported];
},
async loadCurrentSettlementDetails() {
if (this.form.paymentType === 'project_advance') return [];
const requests = [];
if (this.isValidReferenceId(this.form.preSettlementId)) {
requests.push(preApi.getDetail(this.form.preSettlementId));
if (
this.form.paymentType === 'progress_advance' &&
this.isValidReferenceId(this.form.preSettlementId)
) {
const response = await preApi.getDetail(this.form.preSettlementId);
const detail = this.unwrapData(response);
return detail?.id ? [detail] : [];
}
if (this.isValidReferenceId(this.form.settlementId)) {
requests.push(formalApi.getDetail(this.form.settlementId));
if (
this.form.paymentType === 'settlement_payment' &&
this.isValidReferenceId(this.form.settlementId)
) {
const response = await formalApi.getDetail(this.form.settlementId);
const detail = this.unwrapData(response);
return detail?.id ? [detail] : [];
}
if (!requests.length) return [];
const responses = await Promise.all(requests);
return responses.map(response => this.unwrapData(response)).filter(item => item?.id);
return [];
},
async loadAttachmentRuleData(settlementRows = []) {
this.attachmentRuleLoading = true;
this.firstContractPayment = false;
this.contractSignatureFileAvailable = false;
this.resetContractPaymentRatioLimit();
try {
const settlementMaterials = [
ATTACHMENT_MATERIALS.weighingSlip,
@@ -1417,6 +1476,7 @@ export default {
const contractResponse = await getContractDetail(this.form.contractId);
const contract = this.unwrapData(contractResponse) || {};
if (this.form.paymentType === 'project_advance') {
this.applyContractPaymentRatioLimit(contract);
const contractFiles = this.parse(contract.contractFileJson);
this.contractSignatureFileAvailable = contractFiles.some(file =>
Boolean(this.attachmentFileUrl(file))
@@ -1453,6 +1513,41 @@ export default {
this.attachmentRuleLoading = false;
}
},
resetContractPaymentRatioLimit() {
this.contractPaymentRatioLimit = null;
this.contractPaymentRatioExceeded = false;
},
applyContractPaymentRatioLimit(contract = {}) {
const [firstRatio] = this.parse(contract.paymentRatioJson);
const ratioLimitValue = firstRatio?.ratioLimit;
const ratioLimit = Number(ratioLimitValue);
this.contractPaymentRatioLimit =
ratioLimitValue === null ||
ratioLimitValue === undefined ||
String(ratioLimitValue).trim() === '' ||
!Number.isFinite(ratioLimit)
? null
: ratioLimit;
this.checkContractPaymentRatio(this.form.paymentRatio);
},
checkContractPaymentRatio(value) {
const paymentRatio = Number(value);
const hasPaymentRatio =
value !== null &&
value !== undefined &&
String(value).trim() !== '' &&
Number.isFinite(paymentRatio);
const exceeded =
this.form.paymentType === 'project_advance' &&
Boolean(this.form.contractId) &&
this.contractPaymentRatioLimit !== null &&
hasPaymentRatio &&
paymentRatio > this.contractPaymentRatioLimit;
if (exceeded && !this.contractPaymentRatioExceeded) {
this.$message.warning('已超合同配置比例');
}
this.contractPaymentRatioExceeded = exceeded;
},
validateRequiredAttachments() {
if (!this.missingAttachmentMaterials.length) return true;
this.$message.warning(
@@ -1949,20 +2044,22 @@ export default {
this.referenceVisible = false;
},
normalizeAttachments(files) {
this.form.attachments = (files || []).map(file => ({
...file,
attachmentType: (() => {
const matchedType = this.resolveAttachmentTypeByFileName(file);
const existingType = [
...Object.values(MATERIAL_TYPE_MAP),
'other',
].includes(file.attachmentType)
? file.attachmentType
: 'other';
return matchedType !== 'other' ? matchedType : existingType;
})(),
description: file.description || '',
}));
const existingAttachments = this.form.attachments || [];
this.form.attachments = (files || []).map(file => {
const fileUrl = this.attachmentFileUrl(file);
const existingFile = existingAttachments.find(item => {
const sameUid = file.uid && item.uid && String(file.uid) === String(item.uid);
const sameUrl = fileUrl && this.attachmentFileUrl(item) === fileUrl;
return sameUid || sameUrl;
});
return {
...existingFile,
...file,
attachmentType:
existingFile?.attachmentType || this.resolveAttachmentTypeByFileName(file),
description: file.description || existingFile?.description || '',
};
});
},
handleManualAttachmentUpload(uploadedFile) {
const uploadedUrl = this.attachmentFileUrl(uploadedFile);
@@ -2206,6 +2303,12 @@ export default {
.payment-form-page__attachment-download {
margin-left: auto;
}
.payment-form-page__invoice-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
}
.payment-form-page__attachment-file-name-cell {
display: block;
width: 100%;
+3
View File
@@ -371,6 +371,9 @@ export default {
</script>
<style scoped lang="scss">
.receipt-claim-form-page__form {
padding-bottom: 72px;
}
.receipt-claim-form-page__form :deep(.el-select),
.receipt-claim-form-page__form :deep(.el-input-number) {
width: 100%;
+98 -4
View File
@@ -71,10 +71,9 @@
show-overflow-tooltip
>
<template #default="{ row }">
<el-link v-if="column.link && canClaim(row)" type="primary" @click="openClaim(row)">
<el-link v-if="column.link" type="primary" @click="openDetail(row)">
{{ displayValue(row[column.prop]) }}
</el-link>
<span v-else-if="column.link">{{ displayValue(row[column.prop]) }}</span>
<el-tag
v-else-if="column.status"
:type="statusType(row.claimStatus)"
@@ -105,6 +104,74 @@
/>
</div>
</section>
<el-dialog
v-model="detailDialog.visible"
title="收款流水详情"
width="960px"
append-to-body
destroy-on-close
>
<div v-loading="detailDialog.loading">
<el-descriptions :column="3" border>
<el-descriptions-item label="认领通知单">
{{ displayValue(detailDialog.data.receiptNoticeNo) }}
</el-descriptions-item>
<el-descriptions-item label="付款人">
{{ displayValue(detailDialog.data.payerName) }}
</el-descriptions-item>
<el-descriptions-item label="付款金额">
{{ formatMoney(detailDialog.data.receiptAmount) }}
</el-descriptions-item>
<el-descriptions-item label="交易时间">
{{ displayValue(detailDialog.data.transactionTime) }}
</el-descriptions-item>
<el-descriptions-item label="对方户名">
{{ displayValue(detailDialog.data.counterpartyName) }}
</el-descriptions-item>
<el-descriptions-item label="对方开户行">
{{ displayValue(detailDialog.data.counterpartyBank) }}
</el-descriptions-item>
<el-descriptions-item label="对方账号">
{{ displayValue(detailDialog.data.counterpartyAccount) }}
</el-descriptions-item>
<el-descriptions-item label="明细流水号">
{{ displayValue(detailDialog.data.detailSerialNo) }}
</el-descriptions-item>
<el-descriptions-item label="认领状态">
<el-tag :type="statusType(detailDialog.data.claimStatus)">
{{ displayValue(detailDialog.data.claimStatusName) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="已认领金额">
{{ formatMoney(detailDialog.data.claimedAmount) }}
</el-descriptions-item>
<el-descriptions-item label="剩余可认领金额">
{{ formatMoney(detailDialog.data.remainingAmount) }}
</el-descriptions-item>
<el-descriptions-item label="摘要">
{{ displayValue(detailDialog.data.summary) }}
</el-descriptions-item>
</el-descriptions>
<div class="dialog-section-title receipt-flow-page__detail-title">认领记录</div>
<el-table :data="detailDialog.claimRecords" border empty-text="暂无认领记录">
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="createTime" label="认领时间" min-width="170" align="center" />
<el-table-column label="认领金额" min-width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.operationAmount) }}</template>
</el-table-column>
<el-table-column prop="operatorName" label="认领人" min-width="120" align="center" />
<el-table-column label="状态变化" min-width="170" align="center">
<template #default="{ row }">{{ claimStatusChange(row) }}</template>
</el-table-column>
<el-table-column prop="content" label="认领说明" min-width="220" />
</el-table>
</div>
<template #footer>
<el-button @click="detailDialog.visible = false">关闭</el-button>
</template>
</el-dialog>
</basic-container>
</template>
@@ -150,8 +217,10 @@ const formatMockDateTime = date => {
)}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
};
const createReceiptFlowMockRows = () =>
receiptFlowMockTemplates.map((template, index) => {
const createReceiptFlowMockRows = () => {
const count = 20 + Math.floor(Math.random() * 31);
return Array.from({ length: count }, (_, index) => {
const template = receiptFlowMockTemplates[index % receiptFlowMockTemplates.length];
const transactionDate = new Date();
transactionDate.setDate(transactionDate.getDate() - Math.floor(Math.random() * 7));
transactionDate.setHours(8 + Math.floor(Math.random() * 10), Math.floor(Math.random() * 60), 0);
@@ -167,6 +236,7 @@ const createReceiptFlowMockRows = () =>
sourceUpdatedTime: transactionTime,
};
});
};
const emptyQuery = () => ({
receiptNoticeNo: '',
@@ -190,6 +260,7 @@ export default {
syncing: false,
searchExpanded: false,
page: { current: 1, size: 10, total: 0 },
detailDialog: { visible: false, loading: false, data: {}, claimRecords: [] },
};
},
computed: {
@@ -250,6 +321,21 @@ export default {
this.syncing = false;
}
},
async openDetail(row) {
this.detailDialog = {
visible: true,
loading: true,
data: { ...row },
claimRecords: [],
};
try {
const data = this.unwrapData(await api.getDetail(row.id));
this.detailDialog.data = data || {};
this.detailDialog.claimRecords = data?.claimRecords || [];
} finally {
this.detailDialog.loading = false;
}
},
openClaim(row) {
this.$router.push({
path: '/payment/receipt-flow/form',
@@ -265,6 +351,11 @@ export default {
statusType(status) {
return { claimed: 'success', partial: 'warning', unclaimed: 'info' }[status] || '';
},
claimStatusChange(row) {
const label = status =>
({ claimed: '认领完成', partial: '部分认领', unclaimed: '未认领' }[status] || '-');
return `${label(row.fromStatus)}${label(row.toStatus)}`;
},
displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
@@ -314,6 +405,9 @@ export default {
justify-content: flex-end;
padding: 16px 0;
}
.receipt-flow-page__detail-title {
margin: 20px 0 12px;
}
.receipt-flow-page :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
@@ -34,7 +34,9 @@
<div class="settlement-detail-page__toolbar-left">
<el-button type="primary" @click="openGenerateDialog">生成费用</el-button>
<el-button type="primary" plain @click="openUpdateFeeDialog">更新费用</el-button>
<el-button type="primary" plain @click="openTransferDialog">批量转结算</el-button>
<el-button v-if="isPayable" type="primary" plain @click="openTransferDialog">
批量转结算
</el-button>
<el-button type="primary" plain @click="handleExport">导出</el-button>
</div>
<div class="settlement-detail-page__toolbar-right">
@@ -849,11 +851,17 @@ export default {
return this.cargoTypeOptions.filter(item => item.children?.length);
},
},
mounted() {
watch: {
'$route.fullPath'() {
this.openRouteDetail();
},
},
async mounted() {
this.loadTransportTypeOptions();
this.loadCargoTypeOptions();
this.loadPriceUnitOptions();
this.loadTable();
await this.loadTable();
await this.openRouteDetail();
},
beforeUnmount() {
this.clearAdjustCalculations();
@@ -1191,6 +1199,29 @@ export default {
}
this.$router.push({ path, query: { detailNo } });
},
async openRouteDetail(detailId = this.$route.query.detailId) {
if (
!detailId ||
this.settlementType !== 'payable' ||
this.$route.path !== '/settlement/payable-detail'
) {
return;
}
let row = this.rows.find(item => String(item.id) === String(detailId));
if (!row) {
const res = await api.getList(1, 1, {
ids: detailId,
settlementType: this.settlementType,
});
const data = this.unwrapPage(res);
if (data.records?.[0]) row = this.decorateRow(data.records[0]);
}
if (!row) {
this.$message.info('当前记录未找到对应的应付明细');
return;
}
await this.openDetailDialog(row);
},
openWaybillDetail(row) {
if (!row.waybillId) {
this.$message.info('当前记录未关联运单详情');