This commit is contained in:
2026-08-31 10:38:21 +08:00
parent a10eea247f
commit a75ac1cfa4
13 changed files with 864 additions and 161 deletions
@@ -60,7 +60,7 @@
v-else-if="field.type === 'number' && editable"
v-model="form[field.prop]"
:min="0.000001"
:precision="6"
:precision="2"
:controls="false"
/>
<span v-else-if="field.money">{{ formatMoney(form[field.prop]) }}</span>
@@ -153,8 +153,7 @@
<span
v-else-if="isSummaryMoney(column.prop)"
:class="{
'formal-editor__negative-amount':
column.prop === 'adjustAmount' && Number(row[column.prop] || 0) < 0,
'formal-editor__negative-amount': isNegativeAdjustedAmount(row, column.prop),
}"
>
{{ formatMoney(row[column.prop]) }}
@@ -224,7 +223,10 @@
<template #default="{ row }">
<span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
<span v-else-if="column.prop === 'transportQuantity'">
{{ formatQuantity(row[column.prop]) }}
{{ formatDetailTransportQuantity(row[column.prop]) }}
</span>
<span v-else-if="column.prop === 'mileage'">
{{ formatDetailMileage(row[column.prop]) }}
</span>
<span v-else-if="column.prop === 'transportType'">
{{ transportTypeName(row[column.prop]) }}
@@ -235,11 +237,9 @@
<el-table-column v-if="editable" label="操作" width="150" fixed="right" align="center">
<template #default="{ row }">
<div class="formal-editor__links">
<el-link v-if="row.formalSettlementId" type="primary" @click="openAdjustDialog(row)"
>调整</el-link
>
<el-link type="primary" @click="openAdjustDialog(row)">调整</el-link>
<el-link v-if="!row.sourcePreSettlementId" type="danger" @click="removeDetail(row)"
>删除</el-link
>踢出</el-link
>
</div>
</template>
@@ -254,7 +254,13 @@
{{ missingAttachmentTypeText }}
</span>
</template>
<el-table :data="attachments" border>
<div class="formal-editor__attachment-actions">
<el-button type="primary" :disabled="!attachments.length" @click="downloadAttachments">
批量下载
</el-button>
</div>
<el-table :data="attachments" border @selection-change="handleAttachmentSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column label="类型" min-width="180" align="center">
<template #default="{ row }">
@@ -323,7 +329,7 @@
</section-card>
<section-card title="发票信息">
<div v-if="editable" class="formal-editor__invoice-claim">
<div class="formal-editor__invoice-claim">
<el-form inline label-position="right" label-width="auto" @submit.prevent>
<el-form-item label="发票号码">
<el-input
@@ -394,9 +400,7 @@
<div class="formal-editor__links">
<el-link type="primary" @click="viewInvoiceAttachment(row)">查看</el-link>
<el-link type="primary" @click="downloadInvoiceAttachment(row)">下载</el-link>
<el-link v-if="editable" type="danger" @click="removeInvoice($index)">
删除
</el-link>
<el-link type="danger" @click="removeInvoice($index)">删除</el-link>
</div>
</template>
</el-table-column>
@@ -626,6 +630,39 @@
>
</el-dialog>
<el-dialog
v-model="detailAmountAdjust.visible"
title="结算明细调整"
width="520px"
append-to-body
>
<el-form
:model="detailAmountAdjust"
label-position="right"
label-width="auto"
class="formal-editor__amount-adjust"
>
<el-form-item label="原金额">
<span>{{ formatMoney(detailAmountAdjust.originalAmount) }}</span>
</el-form-item>
<el-form-item label="调整金额" required>
<el-input-number
v-model="detailAmountAdjust.adjustAmount"
:precision="2"
:step="0.01"
:controls="false"
/>
</el-form-item>
<el-form-item label="结算金额">
<span>{{ formatMoney(detailAmountAdjustSettlementAmount) }}</span>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="detailAmountAdjust.visible = false">取消</el-button>
<el-button type="primary" @click="confirmDetailAmountAdjustment">提交</el-button>
</template>
</el-dialog>
<el-dialog v-model="adjust.visible" title="结算明细调整" width="92%" append-to-body>
<el-table v-loading="adjust.loading" :data="adjust.rows" border>
<el-table-column type="index" label="序号" width="64" align="center" />
@@ -671,14 +708,6 @@
:precision="2"
:controls="false" /></template
></el-table-column>
<el-table-column label="结算金额(不含税)" min-width="180" align="center"
><template #default="{ row }"
><el-input-number
v-model="row.settlementAmountNoTax"
:min="0"
:precision="2"
:controls="false" /></template
></el-table-column>
<el-table-column label="备注" min-width="180" align="center"
><template #default="{ row }"><el-input v-model="row.remark" maxlength="200" /></template
></el-table-column>
@@ -723,6 +752,7 @@ import {
summaryColumns,
} from '@/option/settlement/formalSettlementTable';
import { getDictionary } from '@/api/system/dictbiz';
import { h } from 'vue';
import { mapGetters } from 'vuex';
import { downloadFileByUrl } from '@/utils/util';
import * as XLSX from 'xlsx';
@@ -752,6 +782,7 @@ export default {
adjustments: [],
changeRecords: [],
attachments: [],
selectedAttachmentRows: [],
invoices: [],
invoiceClaimNo: '',
invoiceClaimLoading: false,
@@ -815,6 +846,12 @@ export default {
reason: '',
rows: [],
},
detailAmountAdjust: {
visible: false,
row: null,
originalAmount: 0,
adjustAmount: 0,
},
detailCollapsed: false,
detailQuery: {
documentNo: '',
@@ -865,6 +902,14 @@ export default {
summaryTotal() {
return this.summaryFees.reduce((total, row) => total + Number(row.settlementAmount || 0), 0);
},
detailAmountAdjustSettlementAmount() {
return Number(
(
Number(this.detailAmountAdjust.originalAmount || 0) +
Number(this.detailAmountAdjust.adjustAmount || 0)
).toFixed(2)
);
},
filteredDetails() {
return this.details.filter(row =>
Object.entries(this.appliedDetailQuery).every(([field, keyword]) => {
@@ -901,7 +946,14 @@ export default {
},
methods: {
async initialize() {
const newRecordAudit = this.recordId
? null
: {
createUserName: this.userInfo?.realName || this.userInfo?.userName || '',
createTime: this.$dayjs().format('YYYY-MM-DD HH:mm:ss'),
};
this.form = createFormalSettlementForm();
if (newRecordAudit) Object.assign(this.form, newRecordAudit);
this.sources = [];
this.details = [];
this.summaryFees = [];
@@ -909,9 +961,16 @@ export default {
this.adjustments = [];
this.changeRecords = [];
this.attachments = [];
this.selectedAttachmentRows = [];
this.invoices = [];
this.invoiceClaimNo = '';
this.attachmentUploadFiles = [];
this.detailAmountAdjust = {
visible: false,
row: null,
originalAmount: 0,
adjustAmount: 0,
};
this.detailCollapsed = false;
this.resetDetailQuery(false);
await Promise.all([
@@ -923,6 +982,7 @@ export default {
if (!this.recordId) {
this.form.exchangeRateDate = this.$dayjs().format('YYYY-MM-DD');
if (this.initialData) await this.applyInitialData();
Object.assign(this.form, newRecordAudit);
await this.refreshFormalSettlementNo();
return;
}
@@ -945,6 +1005,7 @@ export default {
this.adjustments = data.adjustments || [];
this.changeRecords = data.changeRecords || [];
this.attachments = this.parseAttachments(data.attachmentsJson);
this.selectedAttachmentRows = [];
this.invoices = data.invoices || [];
this.sortAttachments();
this.contracts = this.allContracts.filter(
@@ -1272,7 +1333,8 @@ export default {
this.buildSummaryFeesFromDetails();
this.detailCandidate.visible = false;
},
removeDetail(row) {
async removeDetail(row) {
await this.$confirm('确认将该明细踢出正式结算单?', '提示', { type: 'warning' });
const index = this.details.indexOf(row);
if (index < 0) return;
this.details.splice(index, 1);
@@ -1309,16 +1371,26 @@ export default {
isSummaryMoney(prop) {
return ['originalAmount', 'adjustAmount', 'settlementAmount'].includes(prop);
},
isNegativeAdjustedAmount(row, prop) {
return (
Number(row.adjustAmount || 0) < 0 && ['adjustAmount', 'settlementAmount'].includes(prop)
);
},
recalculateSummaryRow(row) {
row.settlementAmount = Number(row.originalAmount || 0) + Number(row.adjustAmount || 0);
},
getSummarySums({ columns, data }) {
const sumProps = ['originalAmount', 'adjustAmount', 'settlementAmount'];
const hasNegativeAdjustment = data.some(row => Number(row.adjustAmount || 0) < 0);
return columns.map((column, index) => {
if (index === 0) return '合计';
if (!sumProps.includes(column.property)) return '';
const total = data.reduce((sum, row) => sum + Number(row[column.property] || 0), 0);
return this.formatMoney(total);
const totalText = this.formatMoney(total);
if (column.property === 'settlementAmount' && hasNegativeAdjustment) {
return h('span', { style: { color: 'var(--el-color-danger)' } }, totalText);
}
return totalText;
});
},
buildSummaryFeesFromDetails() {
@@ -1406,6 +1478,20 @@ export default {
);
},
async openAdjustDialog(row) {
if (!row.formalSettlementId) {
const originalAmount = Number(
row.originalAmount ??
row.totalAmount ??
Number(row.settlementAmountTax || 0) - Number(row.adjustAmount || 0)
);
this.detailAmountAdjust = {
visible: true,
row,
originalAmount: Number(originalAmount.toFixed(2)),
adjustAmount: Number(row.adjustAmount || 0),
};
return;
}
this.adjust = {
visible: true,
loading: true,
@@ -1425,6 +1511,20 @@ export default {
this.adjust.loading = false;
}
},
confirmDetailAmountAdjustment() {
const settlementAmount = this.detailAmountAdjustSettlementAmount;
if (!Number.isFinite(settlementAmount) || settlementAmount < 0) {
return this.$message.warning('调整后的结算金额不能小于0');
}
const row = this.detailAmountAdjust.row;
if (!row) return;
row.originalAmount = this.detailAmountAdjust.originalAmount;
row.adjustAmount = Number(this.detailAmountAdjust.adjustAmount || 0);
row.settlementAmountTax = settlementAmount;
row.pendingDetailAdjustment = true;
this.buildSummaryFeesFromDetails();
this.detailAmountAdjust.visible = false;
},
async saveAdjustment() {
if (!this.adjust.reason.trim()) return this.$message.warning('请输入调整原因');
this.adjust.saving = true;
@@ -1586,6 +1686,17 @@ export default {
settlementType: this.form.settlementType,
sourcePreSettlementIds: this.form.sourcePreSettlementIds,
sourceDetailIds: this.form.sourceDetailIds,
detailAdjustments: this.details
.filter(row => row.pendingDetailAdjustment)
.map(row => ({
sourcePreSettlementDetailId:
row.sourcePreSettlementDetailId ||
(row.sourcePreSettlementId ? row.id : undefined),
sourceDetailId: row.sourcePreSettlementId
? undefined
: row.sourceDetailId || row.id,
adjustAmount: Number(row.adjustAmount || 0),
})),
exchangeRateDate: this.form.exchangeRateDate,
exchangeRate: this.form.exchangeRate,
summaryFees: this.summaryFees
@@ -1639,6 +1750,12 @@ export default {
const quantity = Number(value);
return Number.isFinite(quantity) ? quantity.toFixed(2) : '-';
},
formatDetailTransportQuantity(value) {
return this.readonly && Number(value) === -1 ? '-' : this.formatQuantity(value);
},
formatDetailMileage(value) {
return this.readonly && Number(value) === -1 ? '-' : this.displayValue(value);
},
async loadTransportTypeOptions() {
const { data } = await getDictionary({ code: 'transport_type' });
this.transportTypeOptions = data?.data || [];
@@ -1736,12 +1853,46 @@ export default {
});
this.sortAttachments();
},
handleAttachmentSelectionChange(rows) {
this.selectedAttachmentRows = rows || [];
},
removeAttachment(index) {
this.attachments.splice(index, 1);
this.selectedAttachmentRows = this.selectedAttachmentRows.filter(file =>
this.attachments.includes(file)
);
},
getAttachmentUrl(file = {}) {
return file.url || file.link || file.src || file.domain || '';
},
getAttachmentName(file = {}) {
return (
file.originalName || file.name || this.getAttachmentFileName(this.getAttachmentUrl(file))
);
},
downloadAttachment(file) {
const url = this.getAttachmentUrl(file);
if (!url) {
this.$message.warning('附件地址为空,无法下载');
return;
}
downloadFileByUrl(url, this.getAttachmentName(file) || '附件');
},
downloadAttachments() {
const files = this.selectedAttachmentRows.length
? this.selectedAttachmentRows
: this.attachments;
const downloadableFiles = files.filter(file => this.getAttachmentUrl(file));
if (!downloadableFiles.length) {
this.$message.warning(
this.selectedAttachmentRows.length ? '所选附件无法下载' : '暂无可下载附件'
);
return;
}
downloadableFiles.forEach((file, index) => {
window.setTimeout(() => this.downloadAttachment(file), index * 200);
});
},
previewAttachment(file) {
const url = this.getAttachmentUrl(file);
if (!url) return this.$message.warning('附件地址为空,无法预览');
@@ -1844,7 +1995,7 @@ export default {
}
.formal-editor__detail-filter-actions {
grid-column: 1 / -1;
justify-self: start;
justify-self: end;
}
.formal-editor__page-actions {
display: flex;
@@ -1892,12 +2043,20 @@ export default {
.formal-editor__adjust-reason {
margin-top: 16px;
}
.formal-editor__amount-adjust :deep(.el-input-number) {
width: 100%;
}
.formal-editor__attachment-missing {
margin-left: 12px;
color: var(--el-color-danger);
font-size: 13px;
font-weight: 400;
}
.formal-editor__attachment-actions {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
}
.formal-editor__attachment-upload {
margin-top: 12px;
}