✨ 结算单业务模式运输方式与编辑体验优化

预结算/收付款结算支持业务模式与运输方式展示导出;完善结算调整、对账导入与变更记录;组织合同初始流水号未填不再显示-1。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-21 16:42:02 +08:00
parent 6a21b4ac8d
commit e7c76c803f
11 changed files with 310 additions and 93 deletions
@@ -64,6 +64,20 @@
:precision="2"
:controls="false"
/>
<el-select
v-else-if="field.type === 'select' && editable"
v-model="form[field.prop]"
filterable
clearable
placeholder="请选择"
>
<el-option
v-for="item in formSelectOptions(field)"
:key="item.dictKey"
:label="item.dictValue"
:value="item.dictKey"
/>
</el-select>
<span
v-else-if="field.prop === 'formalSettlementNo'"
:class="{ 'form-readonly--code': form.formalSettlementNo }"
@@ -71,6 +85,9 @@
{{ form.formalSettlementNo || (form.id ? '-' : '系统自动生成') }}
</span>
<span v-else-if="field.money">{{ formatMoney(form[field.prop]) }}</span>
<span v-else-if="field.type === 'select'">{{
formSelectLabel(field, form[field.prop])
}}</span>
<span v-else-if="field.type === 'contract'">{{
displayValue(form.contractName)
}}</span>
@@ -424,26 +441,7 @@
<section-card title="发票信息">
<div class="formal-editor__invoice-claim">
<el-form inline label-position="right" label-width="auto" @submit.prevent>
<el-form-item label="发票号码">
<el-input
v-model="invoiceClaimNo"
clearable
maxlength="32"
placeholder="请输入发票号码"
@keyup.enter="openInvoiceClaimDialog"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:loading="invoiceClaimLoading"
@click="openInvoiceClaimDialog"
>
认领查询
</el-button>
</el-form-item>
</el-form>
<el-button type="primary" @click="openInvoiceClaimDialog">认领查询</el-button>
</div>
<el-table :data="invoices" border>
<el-table-column type="index" label="序号" width="64" align="center" />
@@ -544,7 +542,6 @@
title="查询可认领发票"
confirm-text="确认认领"
multiple
:initial-invoice-no="String(invoiceClaimNo || '').trim()"
:query-api="invoicePoolQueryApi"
@confirm="handleInvoiceClaimConfirm"
/>
@@ -697,9 +694,19 @@
<el-table-column prop="changeTime" label="变更时间" min-width="170" align="center">
<template #default="{ row }">{{ displayValue(row.changeTime) }}</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center" fixed="right">
<template #default="{ row }">
<el-link type="primary" @click="openChangeRecordDetail(row)">详情</el-link>
</template>
</el-table-column>
</el-table>
</section-card>
</div>
<change-record-detail-dialog
v-model="changeRecordDetailVisible"
:rows="changeRecordDetailRows"
/>
<template v-if="!pageMode" #footer>
<el-button v-if="editable" @click="handleSyncPlaceholder">同步</el-button>
<el-button @click="visible = false">取消</el-button>
@@ -1071,6 +1078,7 @@ import { getDictionary } from '@/api/system/dictbiz';
import { h } from 'vue';
import { mapGetters } from 'vuex';
import { isTierUnitPrice, isUnitPriceAdjustable } from '@/utils/settlement-adjust-rule';
import { isApprovalSettlementChange } from '@/utils/settlement-change-record';
import { normalizeCurrencyCode } from '@/utils/currency';
import {
settlementRecordTypeName,
@@ -1083,6 +1091,7 @@ import {
viewKingdeeInvoiceFile,
} from '@/utils/kingdee-attachment';
import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/main.vue';
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
import ProjectAdvanceWriteOff from './project-advance-write-off.vue';
import * as XLSX from 'xlsx';
import { isAdministrator } from '@/utils/auth';
@@ -1093,7 +1102,12 @@ const LOCAL_CURRENCY_HIDDEN_FIELDS = ['exchangeRateDate', 'exchangeRate', 'local
export default {
name: 'FormalSettlementEditor',
components: { InfoFilled, KingdeeInvoicePoolDialog, ProjectAdvanceWriteOff },
components: {
InfoFilled,
KingdeeInvoicePoolDialog,
ChangeRecordDetailDialog,
ProjectAdvanceWriteOff,
},
props: {
modelValue: Boolean,
recordId: [String, Number],
@@ -1123,14 +1137,14 @@ export default {
receiptClaims: [],
adjustments: [],
changeRecords: [],
changeRecordDetailVisible: false,
changeRecordDetailRows: [],
projectAdvanceWriteOffs: [],
attachments: [],
selectedAttachmentRows: [],
invoices: [],
// 开票申请经金蝶回写的发票结果(只读展示,含板式文件/数电PDF下载地址)
issuedInvoices: [],
invoiceClaimNo: '',
invoiceClaimLoading: false,
// 金蝶侧发票附件查看/下载进行中(行组级 loading 守卫)
kingdeeAttachmentBusy: false,
invoiceClaimDialog: {
@@ -1162,6 +1176,7 @@ export default {
projects: [],
feeOptions: [],
feeCategoryOptions: [],
businessModelOptions: [],
transportTypeOptions: [],
fields: formalSettlementFormFields,
sourceTableColumns: sourceColumns,
@@ -1438,7 +1453,9 @@ export default {
this.receiptClaims = this.unwrapData(await getReceiptClaims(id)) || [];
}
this.adjustments = data.adjustments || [];
this.changeRecords = data.changeRecords || [];
this.changeRecords = (data.changeRecords || []).filter(
row => !isApprovalSettlementChange(row.changeContent)
);
this.projectAdvanceWriteOffs = (data.projectAdvanceWriteOffs || []).map(row => ({
...row,
writeOffAmount: Number(row.writeOffAmount || 0),
@@ -1476,13 +1493,14 @@ export default {
this.receiptClaims = [];
this.adjustments = [];
this.changeRecords = [];
this.changeRecordDetailVisible = false;
this.changeRecordDetailRows = [];
this.projectAdvanceWriteOffs = [];
this.advancePayments = [];
this.attachments = [];
this.selectedAttachmentRows = [];
this.invoices = [];
this.issuedInvoices = [];
this.invoiceClaimNo = '';
this.invoiceClaimDialog.visible = false;
this.invoiceClaimDialog.selected = [];
this.attachmentUploadFiles = [];
@@ -1499,6 +1517,7 @@ export default {
this.loadFeeOptions(),
this.loadFeeCategoryOptions(),
this.loadAttachmentTypeOptions(),
this.loadBusinessModelOptions(),
this.loadTransportTypeOptions(),
]);
}
@@ -2424,6 +2443,7 @@ export default {
row.settlementAmountTax = Number(data.afterAmount || 0);
} catch (error) {
row.calculateError = error.message || '费用试算失败';
this.$message.warning(row.calculateError);
} finally {
if (version === row.adjustCalculateVersion) row.calculating = false;
}
@@ -2796,6 +2816,8 @@ export default {
sourceDetailId: row.sourcePreSettlementId ? undefined : row.sourceDetailId || row.id,
adjustAmount: Number(row.adjustAmount || 0),
})),
businessModel: this.form.businessModel || undefined,
transportType: this.form.transportType || undefined,
exchangeRateDate: this.form.exchangeRateDate,
exchangeRate: this.form.exchangeRate,
summaryFees: this.summaryFees
@@ -2886,6 +2908,55 @@ export default {
const lineNo = row?.lineNo;
return lineNo === -1 || String(lineNo) === '-1' ? '' : this.displayValue(lineNo);
},
parseChangeRecordData(value) {
if (!value) return {};
if (typeof value === 'object') return value;
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
} catch (error) {
return {};
}
},
formatChangeRecordValue(value) {
if (value === undefined || value === null || value === '') return '空';
if (Array.isArray(value)) {
if (!value.length) return '空';
return value
.map(item => {
if (!item || typeof item !== 'object') return String(item);
return Object.entries(item)
.map(([key, itemValue]) => `${key}:${this.formatChangeRecordValue(itemValue)}`)
.join(',');
})
.join('\n');
}
if (typeof value === 'object') return JSON.stringify(value);
return String(value);
},
buildChangeRecordDetailRows(row = {}) {
const before = this.parseChangeRecordData(row.beforeData);
const after = this.parseChangeRecordData(row.afterData);
const fields = [...new Set([...Object.keys(before), ...Object.keys(after)])];
const rows = fields
.filter(field => JSON.stringify(before[field]) !== JSON.stringify(after[field]))
.map(field => ({
field,
before: this.formatChangeRecordValue(before[field]),
after: this.formatChangeRecordValue(after[field]),
}));
if (row.changeContent) {
rows.unshift({ field: '变更内容', before: '空', after: row.changeContent });
}
if (row.changeReason) {
rows.push({ field: '变更原因', before: '空', after: row.changeReason });
}
return rows;
},
openChangeRecordDetail(row) {
this.changeRecordDetailRows = this.buildChangeRecordDetailRows(row);
this.changeRecordDetailVisible = true;
},
paymentMethodName(value) {
const labels = {
bank_transfer: '银行转账',
@@ -2915,9 +2986,36 @@ export default {
formatDetailMileage(value) {
return this.readonly && Number(value) === -1 ? '-' : this.displayValue(value);
},
async loadBusinessModelOptions() {
const { data } = await getDictionary({ code: 'business_model' });
const records = Array.isArray(data?.data) ? data.data : data?.data?.records || [];
this.businessModelOptions = records.map(item => ({
dictKey: item.dictKey ?? item.value,
dictValue: item.dictValue ?? item.label ?? item.name,
}));
},
async loadTransportTypeOptions() {
const { data } = await getDictionary({ code: 'transport_type' });
this.transportTypeOptions = data?.data || [];
const records = Array.isArray(data?.data) ? data.data : data?.data?.records || [];
this.transportTypeOptions = records.map(item => ({
dictKey: item.dictKey ?? item.value,
dictValue: item.dictValue ?? item.label ?? item.name,
}));
},
formSelectOptions(field) {
const key = field?.optionsKey;
const options = key ? this[key] : [];
return Array.isArray(options) ? options : [];
},
formSelectLabel(field, value) {
if (value === undefined || value === null || value === '') return '-';
const normalized = String(value).trim();
const option = this.formSelectOptions(field).find(
item =>
String(item.dictKey ?? '').trim() === normalized ||
String(item.dictValue ?? '').trim() === normalized
);
return option?.dictValue || normalized;
},
transportTypeName(value) {
if (value === undefined || value === null || value === '') return '';
@@ -3253,21 +3351,9 @@ export default {
}
.formal-editor__invoice-claim {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
justify-content: flex-end;
margin-bottom: 12px;
}
.formal-editor__invoice-claim-hint {
color: var(--el-color-danger);
font-size: 13px;
}
.formal-editor__invoice-claim :deep(.el-form-item) {
margin-bottom: 0;
}
.formal-editor__invoice-claim :deep(.el-input) {
width: 360px;
}
.formal-editor__issued-invoices {
margin-top: 16px;
}