✨ 结算单业务模式运输方式与编辑体验优化
预结算/收付款结算支持业务模式与运输方式展示导出;完善结算调整、对账导入与变更记录;组织合同初始流水号未填不再显示-1。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -8,6 +8,8 @@ export const formalSettlementFormFields = [
|
||||
{ label: '收款方', prop: 'payeeName', readonly: true },
|
||||
{ label: '结算类型', prop: 'settlementTypeName', readonly: true },
|
||||
{ label: '结算金额', prop: 'settlementAmount', readonly: true, money: true },
|
||||
{ label: '业务模式', prop: 'businessModel', type: 'select', optionsKey: 'businessModelOptions' },
|
||||
{ label: '运输方式', prop: 'transportType', type: 'select', optionsKey: 'transportTypeOptions' },
|
||||
{ label: '汇率日期', prop: 'exchangeRateDate', type: 'date', required: true },
|
||||
{ label: '结算汇率', prop: 'exchangeRate', type: 'number', required: true },
|
||||
{ label: '本位币合计', prop: 'localSettlementAmount', readonly: true, money: true },
|
||||
@@ -25,6 +27,8 @@ export const createFormalSettlementForm = () => ({
|
||||
settlementType: '',
|
||||
sourcePreSettlementIds: [],
|
||||
sourceDetailIds: [],
|
||||
businessModel: '',
|
||||
transportType: '',
|
||||
exchangeRateDate: '',
|
||||
exchangeRate: 1,
|
||||
createUserName: '',
|
||||
|
||||
@@ -8,6 +8,8 @@ export const formalSettlementTableColumns = [
|
||||
{ label: '合同编号', prop: 'contractNo', minWidth: 150 },
|
||||
{ label: '合同名称', prop: 'contractName', minWidth: 170 },
|
||||
{ label: '原币结算金额', prop: 'settlementAmount', minWidth: 145, money: true },
|
||||
{ label: '业务模式', prop: 'businessModelName', minWidth: 120 },
|
||||
{ label: '运输方式', prop: 'transportTypeName', minWidth: 120 },
|
||||
{ label: '本位币结算金额', prop: 'localSettlementAmount', minWidth: 155, money: true },
|
||||
{ label: '结算汇率', prop: 'exchangeRate', minWidth: 110 },
|
||||
{ label: '申请付款金额(含预付)', prop: 'appliedPaymentAmount', minWidth: 185, money: true },
|
||||
|
||||
@@ -12,6 +12,8 @@ export const preSettlementFormFields = [
|
||||
{ label: '付款方', prop: 'payerName', readonly: true },
|
||||
{ label: '结算类型', prop: 'settlementType', readonly: true },
|
||||
{ label: '结算金额', prop: 'settlementAmount', readonly: true, money: true },
|
||||
{ label: '业务模式', prop: 'businessModel', type: 'select', optionsKey: 'businessModelOptions' },
|
||||
{ label: '运输方式', prop: 'transportType', type: 'select', optionsKey: 'transportTypeOptions' },
|
||||
{ label: '汇率日期', prop: 'exchangeRateDate', type: 'date' },
|
||||
{ label: '结算汇率', prop: 'exchangeRate', type: 'number', class: 'exchange-rate-input' },
|
||||
{ label: '本位币合计', prop: 'localSettlementAmount', readonly: true, money: true },
|
||||
@@ -64,6 +66,8 @@ export const emptyPreSettlementForm = () => ({
|
||||
settlementType: 'payable',
|
||||
currency: 'RMB',
|
||||
settlementAmount: 0,
|
||||
businessModel: '',
|
||||
transportType: '',
|
||||
localCurrency: 'RMB',
|
||||
localSettlementAmount: 0,
|
||||
exchangeRateDate: '',
|
||||
|
||||
@@ -8,6 +8,8 @@ export const preSettlementTableColumns = [
|
||||
{ label: '合同编号', prop: 'contractNo', minWidth: 140 },
|
||||
{ label: '合同名称', prop: 'contractName', minWidth: 160 },
|
||||
{ label: '原币结算金额', prop: 'settlementAmount', minWidth: 140, money: true, precision: 2 },
|
||||
{ label: '业务模式', prop: 'businessModelName', minWidth: 120 },
|
||||
{ label: '运输方式', prop: 'transportTypeName', minWidth: 120 },
|
||||
{ label: '本位币结算金额', prop: 'localSettlementAmount', minWidth: 150, money: true, precision: 2 },
|
||||
{ label: '结算汇率', prop: 'exchangeRate', minWidth: 110, precision: 2 },
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'nprogress/nprogress.css';
|
||||
const excelPattern = /\.(xls|xlsx)$/;
|
||||
|
||||
/** 是否为浏览器 Blob(含跨 realm 的 Blob-like) */
|
||||
const isBlobLike = value =>
|
||||
export const isBlobLike = value =>
|
||||
value != null &&
|
||||
typeof value === 'object' &&
|
||||
typeof value.size === 'number' &&
|
||||
@@ -15,14 +15,14 @@ const isBlobLike = value =>
|
||||
(typeof value.text === 'function' || typeof value.arrayBuffer === 'function');
|
||||
|
||||
/** axios 拦截器已把 JSON Blob 解析成的业务对象 */
|
||||
const isBusinessJson = value =>
|
||||
export const isBusinessJson = value =>
|
||||
value != null &&
|
||||
typeof value === 'object' &&
|
||||
!isBlobLike(value) &&
|
||||
!(value instanceof ArrayBuffer) &&
|
||||
('code' in value || 'success' in value || 'msg' in value);
|
||||
|
||||
const isExcelResponse = res => {
|
||||
export const isExcelResponse = res => {
|
||||
// 已是业务 JSON(导入成功/失败提示)时,绝不能当失败明细 Excel 处理
|
||||
if (isBusinessJson(res?.data)) {
|
||||
return false;
|
||||
@@ -54,7 +54,7 @@ const isExcelResponse = res => {
|
||||
* 提前解析成普通对象;此处需同时兼容 Blob / 字符串 / 已解析对象,避免再调
|
||||
* `blob.text()` 时出现「blob.text is not a function」。
|
||||
*/
|
||||
const parseBlobJson = async data => {
|
||||
export const parseBlobJson = async data => {
|
||||
if (data == null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -3,10 +3,27 @@ export function isFreightFeeName(name) {
|
||||
return text.includes('运费') || text.includes('运输费');
|
||||
}
|
||||
|
||||
function parseBillingRules(row = {}) {
|
||||
const rule = row.billingRule || {};
|
||||
const nested = Array.isArray(rule.rules) ? rule.rules : [];
|
||||
if (nested.length) return nested;
|
||||
const raw = row.billingRulesJson ?? row.billingRules;
|
||||
if (Array.isArray(raw)) return raw.filter(item => item && typeof item === 'object');
|
||||
if (typeof raw === 'string' && raw.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed.filter(item => item && typeof item === 'object') : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/** 运输单价对应的运费计费类型。 */
|
||||
export function freightBillingType(row = {}) {
|
||||
const rule = row.billingRule || {};
|
||||
const rules = Array.isArray(rule.rules) ? rule.rules : [];
|
||||
const rules = parseBillingRules(row);
|
||||
const freightRule = rules.find(
|
||||
item => isFreightFeeName(item?.feeItem) || isFreightFeeName(item?.feeType)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
/** 审批通过、驳回等审批流转类变更内容,不在结算单变更记录中展示。 */
|
||||
export function isApprovalSettlementChange(content) {
|
||||
const text = String(content || '').trim();
|
||||
if (!text) return false;
|
||||
return text.includes('审批通过') || text.includes('审批驳回') || text === '已驳回';
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<el-form-item :label="field.label" :prop="field.prop">
|
||||
<el-input
|
||||
v-if="field.type === 'contract'"
|
||||
:model-value="formatContractOptionLabel(form)"
|
||||
:model-value="form.contractName"
|
||||
readonly
|
||||
:disabled="!editable || details.length > 0"
|
||||
placeholder="请选择合同"
|
||||
@@ -53,6 +53,23 @@
|
||||
:disabled="!editable"
|
||||
@change="recalculateLocalAmount"
|
||||
/>
|
||||
<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.type === 'select'" class="form-readonly">
|
||||
{{ formSelectLabel(field, form[field.prop]) }}
|
||||
</span>
|
||||
<el-input
|
||||
v-else-if="field.type === 'textarea'"
|
||||
v-model="form[field.prop]"
|
||||
@@ -493,7 +510,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="openAdjustChangeRecord(row)">查看</el-link>
|
||||
<el-link type="primary" @click="openAdjustChangeRecord(row)">详情</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -999,8 +1016,8 @@ import {
|
||||
settlementDetailColumns,
|
||||
settlementSummaryColumns,
|
||||
} from '@/option/settlement/preSettlementTable';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { isTierUnitPrice, isUnitPriceAdjustable } from '@/utils/settlement-adjust-rule';
|
||||
import { isApprovalSettlementChange } from '@/utils/settlement-change-record';
|
||||
import { normalizeCurrencyCode } from '@/utils/currency';
|
||||
import { settlementStatusName } from '@/utils/settlementStatus';
|
||||
import { downloadFileByUrl } from '@/utils/util';
|
||||
@@ -1095,6 +1112,7 @@ export default {
|
||||
},
|
||||
feeOptions: [],
|
||||
feeCategoryOptions: [],
|
||||
businessModelOptions: [],
|
||||
transportTypeOptions: [],
|
||||
summaryFees: [],
|
||||
details: [],
|
||||
@@ -1144,6 +1162,8 @@ export default {
|
||||
readonly: false,
|
||||
detailId: '',
|
||||
sourceDetailId: '',
|
||||
// 未保存明细(id 仍等于来源应收应付明细)时先暂存,保存预结算单后再生效
|
||||
draftMode: false,
|
||||
detailLineNo: '',
|
||||
reason: '',
|
||||
},
|
||||
@@ -1330,7 +1350,6 @@ export default {
|
||||
methods: {
|
||||
isTierUnitPrice,
|
||||
isUnitPriceAdjustable,
|
||||
formatContractOptionLabel,
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission?.[code], false);
|
||||
},
|
||||
@@ -1344,6 +1363,7 @@ export default {
|
||||
if (!this.publicMode) {
|
||||
await this.loadFeeOptions();
|
||||
await this.loadFeeCategoryOptions();
|
||||
await this.loadBusinessModelOptions();
|
||||
await this.loadTransportTypeOptions();
|
||||
await this.loadAttachmentTypeOptions();
|
||||
}
|
||||
@@ -1485,6 +1505,7 @@ export default {
|
||||
this.detailFeeCache = {};
|
||||
this.adjustDialog.detailId = '';
|
||||
this.adjustDialog.sourceDetailId = '';
|
||||
this.adjustDialog.draftMode = false;
|
||||
this.adjustDialog.detailLineNo = '';
|
||||
this.adjustChangeRecordVisible = false;
|
||||
this.adjustChangeRecord = null;
|
||||
@@ -1523,7 +1544,7 @@ export default {
|
||||
createTime: row.paymentCreateTime || row.createTime,
|
||||
}));
|
||||
this.changeRecords = (detail.changeRecords || []).filter(
|
||||
row => row.changeContent !== '预结算审批通过'
|
||||
row => !isApprovalSettlementChange(row.changeContent)
|
||||
);
|
||||
this.attachments = this.parseAttachments(detail.attachmentsJson);
|
||||
this.sortAttachments();
|
||||
@@ -1647,6 +1668,14 @@ export default {
|
||||
const { data } = await getDictionary({ code: 'fee_category' });
|
||||
this.feeCategoryOptions = data?.data || [];
|
||||
},
|
||||
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' });
|
||||
const records = Array.isArray(data?.data) ? data.data : data?.data?.records || [];
|
||||
@@ -1655,6 +1684,21 @@ export default {
|
||||
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;
|
||||
},
|
||||
handleContractChange(id) {
|
||||
const contract = this.contractOptions.find(item => String(item.id) === String(id));
|
||||
if (!contract) {
|
||||
@@ -1741,6 +1785,8 @@ export default {
|
||||
return {
|
||||
id: this.form.id || undefined,
|
||||
contractId: this.form.contractId,
|
||||
businessModel: this.form.businessModel || undefined,
|
||||
transportType: this.form.transportType || undefined,
|
||||
exchangeRateDate: this.form.exchangeRateDate || undefined,
|
||||
exchangeRate: this.form.exchangeRate,
|
||||
attachmentsJson: JSON.stringify(this.attachments || []),
|
||||
@@ -2251,17 +2297,21 @@ export default {
|
||||
},
|
||||
async openAdjustDialog(row, readonly) {
|
||||
const detailRow = row;
|
||||
const sourceDetailId = detailRow.sourceDetailId || '';
|
||||
// 未保存明细 id 仍等于来源应收应付明细 id,调整结果先暂存
|
||||
const draftMode =
|
||||
!this.form.id ||
|
||||
!detailRow.id ||
|
||||
String(detailRow.id) === String(detailRow.sourceDetailId);
|
||||
this.adjustDialog.visible = true;
|
||||
this.adjustDialog.loading = true;
|
||||
this.adjustDialog.readonly = readonly;
|
||||
this.adjustDialog.draftMode = draftMode;
|
||||
this.adjustDialog.detailId = detailRow.id;
|
||||
this.adjustDialog.detailLineNo = detailRow.lineNo;
|
||||
this.adjustDialog.sourceDetailId =
|
||||
detailRow.id && String(detailRow.id) === String(detailRow.sourceDetailId)
|
||||
? detailRow.sourceDetailId
|
||||
: '';
|
||||
const sourceDetailId = String(this.adjustDialog.sourceDetailId || '');
|
||||
const pending = sourceDetailId ? this.pendingAdjustments[sourceDetailId] : null;
|
||||
this.adjustDialog.sourceDetailId = sourceDetailId;
|
||||
const pendingKey = String(sourceDetailId || '');
|
||||
const pending = pendingKey ? this.pendingAdjustments[pendingKey] : null;
|
||||
// 底部备注回显结算明细备注;暂存过的调整优先取暂存备注
|
||||
this.adjustDialog.reason =
|
||||
pending?.reason !== undefined && pending?.reason !== null
|
||||
@@ -2278,11 +2328,12 @@ export default {
|
||||
this.adjustRows = cachedFees.map(item => this.normalizeAdjustRow(item));
|
||||
return;
|
||||
}
|
||||
if (sourceDetailId) {
|
||||
if (!this.sourceFeeSnapshots[sourceDetailId]) {
|
||||
this.sourceFeeSnapshots[sourceDetailId] = await this.loadSourceFeeRows(sourceDetailId);
|
||||
if (draftMode && sourceDetailId) {
|
||||
if (!this.sourceFeeSnapshots[String(sourceDetailId)]) {
|
||||
this.sourceFeeSnapshots[String(sourceDetailId)] =
|
||||
await this.loadSourceFeeRows(sourceDetailId);
|
||||
}
|
||||
this.adjustRows = this.cloneAdjustRows(this.sourceFeeSnapshots[sourceDetailId]);
|
||||
this.adjustRows = this.cloneAdjustRows(this.sourceFeeSnapshots[String(sourceDetailId)]);
|
||||
return;
|
||||
}
|
||||
const cachedFees = this.detailFeeCache[detailRow.id];
|
||||
@@ -2554,7 +2605,7 @@ export default {
|
||||
if (
|
||||
['transportQuantity', 'mileage', 'unitPrice'].includes(changedField) &&
|
||||
this.adjustDialog.sourceDetailId &&
|
||||
row.id &&
|
||||
(row.sourceFeeId || row.id) &&
|
||||
(changedField !== 'unitPrice' || isUnitPriceAdjustable(row))
|
||||
) {
|
||||
this.scheduleSourceAdjustCalculation(row, changedField);
|
||||
@@ -2570,7 +2621,8 @@ export default {
|
||||
try {
|
||||
const response = await calculateAdjustedFee({
|
||||
detailId: this.adjustDialog.sourceDetailId,
|
||||
feeId: row.id,
|
||||
// 已保存预结算费用行需用源应收应付费用ID试算
|
||||
feeId: row.sourceFeeId || row.id,
|
||||
transportQuantity: Number(row.transportQuantity || 0),
|
||||
mileage: Number(row.mileage || 0),
|
||||
unitPrice: row.unitPrice === null || row.unitPrice === '' ? null : Number(row.unitPrice),
|
||||
@@ -2596,6 +2648,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;
|
||||
}
|
||||
@@ -2611,7 +2664,7 @@ export default {
|
||||
}
|
||||
this.adjustDialog.saving = true;
|
||||
try {
|
||||
if (this.adjustDialog.sourceDetailId) {
|
||||
if (this.adjustDialog.draftMode && this.adjustDialog.sourceDetailId) {
|
||||
this.pendingAdjustments[this.adjustDialog.sourceDetailId] = {
|
||||
reason: this.adjustDialog.reason,
|
||||
rows: this.adjustRows.map(row => ({ ...row, feeItems: { ...row.feeItems } })),
|
||||
|
||||
@@ -263,7 +263,7 @@
|
||||
<template v-else-if="isExternalRowEditing(row) && isExternalEditableColumn(column.prop)">
|
||||
<el-input-number
|
||||
v-if="column.feeItemName"
|
||||
v-model="row.feeItems[column.feeItemName]"
|
||||
v-model="externalEditTarget(row).feeItems[column.feeItemName]"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
@@ -271,7 +271,7 @@
|
||||
/>
|
||||
<el-date-picker
|
||||
v-else-if="isExternalDateColumn(column.prop)"
|
||||
v-model="row[column.prop]"
|
||||
v-model="externalEditTarget(row)[column.prop]"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
format="YYYY-MM-DD"
|
||||
@@ -280,7 +280,7 @@
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="isExternalNumberColumn(column.prop)"
|
||||
v-model="row[column.prop]"
|
||||
v-model="externalEditTarget(row)[column.prop]"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
@@ -288,7 +288,7 @@
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="column.prop === 'transportType'"
|
||||
v-model="row[column.prop]"
|
||||
v-model="externalEditTarget(row)[column.prop]"
|
||||
size="small"
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
@@ -300,7 +300,7 @@
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input v-else v-model="row[column.prop]" size="small" />
|
||||
<el-input v-else v-model="externalEditTarget(row)[column.prop]" size="small" />
|
||||
</template>
|
||||
<span v-else-if="column.prop === 'transportType'">
|
||||
{{ transportTypeLabel(row.transportType) }}
|
||||
@@ -630,6 +630,7 @@ import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { Search } from '@element-plus/icons-vue';
|
||||
import { h } from 'vue';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { isExcelResponse, parseBlobJson } from '@/utils/import-excel';
|
||||
import { transportReconciliationFormFields } from '@/option/settlement/transportReconciliationForm';
|
||||
import {
|
||||
internalColumns,
|
||||
@@ -676,6 +677,8 @@ export default {
|
||||
completionDialog: { visible: false, loading: false },
|
||||
completionSummaryRowsData: null,
|
||||
externalEditing: {},
|
||||
// 调整中的草稿,输入只改草稿,点「完成」才写回列表
|
||||
externalEditDrafts: {},
|
||||
externalEditSnapshots: {},
|
||||
matchingStarted: false,
|
||||
rules: {
|
||||
@@ -932,6 +935,7 @@ export default {
|
||||
this.internalDetails = [];
|
||||
this.externalDetails = [];
|
||||
this.externalEditing = {};
|
||||
this.externalEditDrafts = {};
|
||||
this.externalEditSnapshots = {};
|
||||
this.completionSummaryRowsData = null;
|
||||
this.matchingStarted = false;
|
||||
@@ -1503,16 +1507,16 @@ export default {
|
||||
this.form.reconciliationMode === 'cargo'
|
||||
? await api.importCargo(this.currentId, file)
|
||||
: await api.importVehicle(this.currentId, file);
|
||||
const contentType = response.headers?.['content-type'] || response.data?.type || '';
|
||||
if (contentType.includes('spreadsheetml') || contentType.includes('ms-excel')) {
|
||||
// 成功时 axios 拦截器可能已把 JSON Blob 解析成对象,不能再调 .text()
|
||||
if (isExcelResponse(response)) {
|
||||
downloadXls(
|
||||
response.data,
|
||||
`运输对账导入失败明细${this.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.xlsx`
|
||||
);
|
||||
this.$message.warning('部分数据导入失败,已下载失败明细');
|
||||
} else {
|
||||
const result = JSON.parse(await response.data.text());
|
||||
if (result.code !== 200) throw new Error(result.msg || '导入失败');
|
||||
const result = await parseBlobJson(response.data);
|
||||
if (Number(result.code) !== 200) throw new Error(result.msg || '导入失败');
|
||||
this.$message.success('外部账单导入成功');
|
||||
}
|
||||
this.matchingStarted = false;
|
||||
@@ -1661,6 +1665,28 @@ export default {
|
||||
isExternalRowEditing(row) {
|
||||
return Boolean(this.externalEditing[this.externalRowKey(row)]);
|
||||
},
|
||||
cloneExternalEditRow(row) {
|
||||
return {
|
||||
...row,
|
||||
feeItems: { ...(row.feeItems || this.parseFeeItems(row.feeItemsJson)) },
|
||||
};
|
||||
},
|
||||
/** 编辑态绑定草稿,避免输入时实时改写列表导致疑似重复 Tab 刷新 */
|
||||
externalEditTarget(row) {
|
||||
const key = this.externalRowKey(row);
|
||||
return this.externalEditDrafts[key] || row;
|
||||
},
|
||||
clearExternalEditState(key) {
|
||||
const editing = { ...this.externalEditing };
|
||||
const drafts = { ...this.externalEditDrafts };
|
||||
const snapshots = { ...this.externalEditSnapshots };
|
||||
delete editing[key];
|
||||
delete drafts[key];
|
||||
delete snapshots[key];
|
||||
this.externalEditing = editing;
|
||||
this.externalEditDrafts = drafts;
|
||||
this.externalEditSnapshots = snapshots;
|
||||
},
|
||||
isExternalEditableColumn(prop) {
|
||||
return !['externalLineNo', 'matchStatus'].includes(prop);
|
||||
},
|
||||
@@ -1672,34 +1698,30 @@ export default {
|
||||
},
|
||||
openExternalAdjust(row) {
|
||||
const key = this.externalRowKey(row);
|
||||
const draft = this.cloneExternalEditRow(row);
|
||||
this.externalEditSnapshots = {
|
||||
...this.externalEditSnapshots,
|
||||
[key]: { ...row },
|
||||
[key]: this.cloneExternalEditRow(row),
|
||||
};
|
||||
this.externalEditDrafts = { ...this.externalEditDrafts, [key]: draft };
|
||||
this.externalEditing = { ...this.externalEditing, [key]: true };
|
||||
},
|
||||
cancelExternalAdjust(row) {
|
||||
const key = this.externalRowKey(row);
|
||||
const snapshot = this.externalEditSnapshots[key];
|
||||
if (snapshot) Object.assign(row, snapshot);
|
||||
const editing = { ...this.externalEditing };
|
||||
const snapshots = { ...this.externalEditSnapshots };
|
||||
delete editing[key];
|
||||
delete snapshots[key];
|
||||
this.externalEditing = editing;
|
||||
this.externalEditSnapshots = snapshots;
|
||||
this.clearExternalEditState(this.externalRowKey(row));
|
||||
},
|
||||
async finishExternalAdjust(row) {
|
||||
row.feeItemsJson = JSON.stringify(row.feeItems || {});
|
||||
const key = this.externalRowKey(row);
|
||||
const editing = { ...this.externalEditing };
|
||||
const snapshots = { ...this.externalEditSnapshots };
|
||||
delete editing[key];
|
||||
delete snapshots[key];
|
||||
this.externalEditing = editing;
|
||||
this.externalEditSnapshots = snapshots;
|
||||
const draft = this.externalEditDrafts[key];
|
||||
if (draft) {
|
||||
const feeItems = { ...(draft.feeItems || {}) };
|
||||
Object.assign(row, draft, { feeItems });
|
||||
row.feeItemsJson = JSON.stringify(feeItems);
|
||||
} else {
|
||||
row.feeItemsJson = JSON.stringify(row.feeItems || {});
|
||||
}
|
||||
this.clearExternalEditState(key);
|
||||
this.refreshLocalStats();
|
||||
this.$message.success('调整已暂存');
|
||||
this.$message.success('调整已暂存,匹配或完成对账后生效');
|
||||
},
|
||||
normalizeExternalDateTime(value) {
|
||||
if (!value) return null;
|
||||
@@ -1743,7 +1765,8 @@ export default {
|
||||
differenceQuantity: Math.abs(internalQuantity - externalQuantity),
|
||||
differenceAmount: Math.abs(internalAmount - externalAmount),
|
||||
matchedCount,
|
||||
unmatchedCount: internalUnmatched + externalUnmatched,
|
||||
// 无法匹配按内部账单条数统计,不叠加外部未匹配数
|
||||
unmatchedCount: internalUnmatched,
|
||||
matchStatus:
|
||||
this.internalDetails.length > 0 &&
|
||||
this.internalDetails.length === this.externalDetails.length &&
|
||||
|
||||
@@ -429,7 +429,13 @@ export default {
|
||||
rules: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
// -1 为空占位,不填时按空处理
|
||||
if (
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
value === '' ||
|
||||
Number(value) === -1
|
||||
) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
@@ -1188,7 +1194,13 @@ export default {
|
||||
}
|
||||
},
|
||||
normalizeContractInitialSerial(value) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
// -1 为空占位,不填时不落库、不回显
|
||||
if (
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
value === '' ||
|
||||
Number(value) === -1
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const text = String(value).trim();
|
||||
@@ -1198,6 +1210,12 @@ export default {
|
||||
const serial = Number(text);
|
||||
return Number.isInteger(serial) ? serial : value;
|
||||
},
|
||||
clearEmptyContractInitialSerial(form = this.form) {
|
||||
if (!form) return;
|
||||
if (Number(form.contractInitialSerial) === -1) {
|
||||
form.contractInitialSerial = '';
|
||||
}
|
||||
},
|
||||
isRootDept(row) {
|
||||
return row && (row.parentId === 0 || String(row.parentId) === '0');
|
||||
},
|
||||
@@ -1502,6 +1520,7 @@ export default {
|
||||
if (this.form.isPlatformCompany === undefined || this.form.isPlatformCompany === null) {
|
||||
this.form.isPlatformCompany = 0;
|
||||
}
|
||||
this.clearEmptyContractInitialSerial(this.form);
|
||||
this.initData(this.form.tenantId);
|
||||
this.loadParentDeptCode(this.form.parentId);
|
||||
}
|
||||
@@ -1516,6 +1535,7 @@ export default {
|
||||
if (this.form.parentId === '0') {
|
||||
this.form.parentId = '';
|
||||
}
|
||||
this.clearEmptyContractInitialSerial(this.form);
|
||||
this.loadParentDeptCode(this.form.parentId);
|
||||
if (this.form.hasOwnProperty('leaderId')) {
|
||||
const splitResult = func.split(this.form.leaderId);
|
||||
|
||||
Reference in New Issue
Block a user