1、调整导入运单
2、调整对账
This commit is contained in:
@@ -247,6 +247,9 @@
|
||||
>
|
||||
{{ row[column.prop] }}
|
||||
</el-link>
|
||||
<span v-else-if="column.money && column.prop === 'unitPrice'">
|
||||
{{ formatUnitPrice(row) }}
|
||||
</span>
|
||||
<span v-else-if="column.money">
|
||||
{{ formatMoney(row[column.prop], row.currency || form.currency) }}
|
||||
</span>
|
||||
@@ -259,6 +262,12 @@
|
||||
<span v-else-if="column.prop === 'transportType'">
|
||||
{{ transportTypeName(row[column.prop]) }}
|
||||
</span>
|
||||
<span v-else-if="column.prop === 'cargoName'">
|
||||
{{ formatCargoNames(row) }}
|
||||
</span>
|
||||
<span v-else-if="column.prop === 'cargoType'">
|
||||
{{ formatCargoTypes(row) }}
|
||||
</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -1078,6 +1087,7 @@ export default {
|
||||
adjustRows: [],
|
||||
pendingAdjustments: {},
|
||||
sourceFeeSnapshots: {},
|
||||
detailFeeCache: {}, // 缓存每个明细的费用明细数据(用于拼接货物名称和类型)
|
||||
adjustChangeRecordVisible: false,
|
||||
adjustChangeRecord: null,
|
||||
adjustChangeRecordDetailRows: [],
|
||||
@@ -1169,6 +1179,19 @@ export default {
|
||||
});
|
||||
return Array.from(names);
|
||||
},
|
||||
// 按 preSettlementDetailId 分组的明细数据(用于拼接货物名称和类型)
|
||||
groupedDetails() {
|
||||
const groups = {};
|
||||
this.details.forEach(detail => {
|
||||
// 优先使用 sourceDetailId,如果没有则使用 preSettlementDetailId
|
||||
const groupKey = detail.sourceDetailId || detail.preSettlementDetailId || detail.id;
|
||||
if (!groups[groupKey]) {
|
||||
groups[groupKey] = [];
|
||||
}
|
||||
groups[groupKey].push(detail);
|
||||
});
|
||||
return groups;
|
||||
},
|
||||
filteredDetails() {
|
||||
return this.details.filter(row =>
|
||||
Object.entries(this.appliedDetailQuery).every(([field, keyword]) => {
|
||||
@@ -1322,6 +1345,7 @@ export default {
|
||||
this.adjustRows = [];
|
||||
this.pendingAdjustments = {};
|
||||
this.sourceFeeSnapshots = {};
|
||||
this.detailFeeCache = {};
|
||||
this.adjustDialog.activeTab = 'adjust';
|
||||
this.adjustDialog.detailId = '';
|
||||
this.adjustDialog.sourceDetailId = '';
|
||||
@@ -1371,6 +1395,8 @@ export default {
|
||||
partyB: detail.payeeName,
|
||||
});
|
||||
}
|
||||
// 预加载所有明细的费用明细数据,用于拼接货物名称和类型
|
||||
await this.loadAllDetailFees();
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
@@ -1917,6 +1943,30 @@ export default {
|
||||
this.normalizeAdjustRow(item, data.feeItemNames || [])
|
||||
);
|
||||
},
|
||||
// 批量加载所有明细的费用明细数据,用于拼接货物名称和类型
|
||||
async loadAllDetailFees() {
|
||||
if (!this.details.length) return;
|
||||
|
||||
// 并发加载所有明细的费用数据
|
||||
await Promise.all(
|
||||
this.details.map(async detail => {
|
||||
if (!detail.id) return;
|
||||
|
||||
try {
|
||||
const response = await getDetailFees(detail.id);
|
||||
const feeRows = response.data?.data || response.data || [];
|
||||
|
||||
// 缓存费用明细数据
|
||||
if (Array.isArray(feeRows) && feeRows.length > 0) {
|
||||
this.detailFeeCache[detail.id] = feeRows;
|
||||
}
|
||||
} catch (error) {
|
||||
// 加载失败时使用明细本身的数据
|
||||
console.warn(`加载明细 ${detail.id} 的费用数据失败:`, error);
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
// 明细列表只有汇总金额,费用项级别的原始金额需要按来源明细单独拉取并缓存为调整基准。
|
||||
async ensureSourceFeeSnapshots() {
|
||||
const targets = this.details.filter(row => {
|
||||
@@ -2461,6 +2511,65 @@ export default {
|
||||
if (!Number.isFinite(quantity)) return '-';
|
||||
return `${quantity.toFixed(2)}${row.quantityUnit ? ` ${row.quantityUnit}` : ''}`;
|
||||
},
|
||||
// 拼接同一运单的货物名称
|
||||
formatCargoNames(row) {
|
||||
const detailId = row.id;
|
||||
const feeRows = this.detailFeeCache[detailId] || [];
|
||||
|
||||
// 如果没有缓存数据或只有一条,返回原始值
|
||||
if (feeRows.length <= 1) {
|
||||
return this.displayValue(row.cargoName);
|
||||
}
|
||||
|
||||
// 收集所有货物名称并去重
|
||||
const names = feeRows
|
||||
.map(item => item.cargoName)
|
||||
.filter(name => name && String(name).trim())
|
||||
.filter((name, index, arr) => arr.indexOf(name) === index);
|
||||
|
||||
return names.length > 0 ? names.join('、') : this.displayValue(row.cargoName);
|
||||
},
|
||||
// 拼接同一运单的货物类型
|
||||
formatCargoTypes(row) {
|
||||
const detailId = row.id;
|
||||
const feeRows = this.detailFeeCache[detailId] || [];
|
||||
|
||||
// 如果没有缓存数据或只有一条,返回原始值
|
||||
if (feeRows.length <= 1) {
|
||||
return this.displayValue(row.cargoType);
|
||||
}
|
||||
|
||||
// 收集所有货物类型并去重
|
||||
const types = feeRows
|
||||
.map(item => item.cargoType)
|
||||
.filter(type => type && String(type).trim())
|
||||
.filter((type, index, arr) => arr.indexOf(type) === index);
|
||||
|
||||
return types.length > 0 ? types.join('、') : this.displayValue(row.cargoType);
|
||||
},
|
||||
// 格式化运输单价,如果多个货物的单价不一样则显示'-'
|
||||
formatUnitPrice(row) {
|
||||
const detailId = row.id;
|
||||
const feeRows = this.detailFeeCache[detailId] || [];
|
||||
|
||||
// 如果没有缓存数据或只有一条,返回原始值
|
||||
if (feeRows.length <= 1) {
|
||||
return this.formatMoney(row.unitPrice, row.currency || this.form.currency);
|
||||
}
|
||||
|
||||
// 检查所有货物的运输单价是否一致
|
||||
const prices = feeRows
|
||||
.map(item => Number(item.unitPrice || 0))
|
||||
.filter((price, index, arr) => arr.indexOf(price) === index);
|
||||
|
||||
// 如果单价不一致,返回'-'
|
||||
if (prices.length > 1) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
// 单价一致,返回该单价
|
||||
return this.formatMoney(prices[0], row.currency || this.form.currency);
|
||||
},
|
||||
displayValue(value) {
|
||||
return value === undefined || value === null || value === '' ? '-' : value;
|
||||
},
|
||||
|
||||
@@ -523,23 +523,62 @@
|
||||
<el-button type="primary" plain @click="openFeeAdjustment('补款')">补款</el-button>
|
||||
<el-button type="primary" plain @click="openFeeAdjustment('扣款')">扣款</el-button>
|
||||
</div>
|
||||
<el-table :data="completionSummaryRows" border>
|
||||
<el-table
|
||||
:data="completionSummaryRows"
|
||||
border
|
||||
show-summary
|
||||
:summary-method="getCompletionSummarySums"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="72" align="center" />
|
||||
<el-table-column prop="name" label="费用项目" min-width="150" align="center" />
|
||||
<el-table-column prop="name" label="费用项目" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'completion-dialog__negative': row._feeKind === '扣款' }">{{ row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="原金额(元)" min-width="160" align="right">
|
||||
<template #default="{ row }">{{ formatCompletionMoney(row.originalAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="调整金额(元)" min-width="160" align="right">
|
||||
<template #default="{ row }"
|
||||
><span :class="{ 'completion-dialog__negative': row.adjustAmount < 0 }">{{
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-if="editable && row.manualFlag === 1"
|
||||
v-model="row.adjustAmount"
|
||||
:class="{ 'completion-dialog__negative-input': row._feeKind === '扣款' }"
|
||||
:precision="2"
|
||||
:step="0.01"
|
||||
:controls="false"
|
||||
:value-on-clear="0"
|
||||
@change="recalculateCompletionFee(row)"
|
||||
/>
|
||||
<span v-else :class="{ 'completion-dialog__negative': Number(row.adjustAmount) < 0 }">{{
|
||||
formatCompletionMoney(row.adjustAmount)
|
||||
}}</span></template
|
||||
>
|
||||
}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="调整后金额(元)" min-width="180" align="right">
|
||||
<template #default="{ row }">{{ formatCompletionMoney(row.afterAmount) }}</template>
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'completion-dialog__negative': Number(row.afterAmount) < 0 }">{{
|
||||
formatCompletionMoney(row.afterAmount)
|
||||
}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-if="editable"
|
||||
v-model="row.remark"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
<span v-else>{{ displayValue(row.remark) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="editable" label="操作" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="row.manualFlag === 1" type="danger" @click="removeCompletionFee(row)">删除</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="220" />
|
||||
</el-table>
|
||||
</section>
|
||||
</div>
|
||||
@@ -565,6 +604,7 @@ import * as api from '@/api/settlement/transportReconciliation';
|
||||
import * as formalSettlementApi from '@/api/settlement/formalSettlement';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { Search } from '@element-plus/icons-vue';
|
||||
import { h } from 'vue';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { transportReconciliationFormFields } from '@/option/settlement/transportReconciliationForm';
|
||||
import {
|
||||
@@ -610,6 +650,7 @@ export default {
|
||||
adjustDialog: { visible: false, saving: false, rows: [], reason: '' },
|
||||
manualMatchDialog: { visible: false, loading: false, row: null },
|
||||
completionDialog: { visible: false, loading: false },
|
||||
completionSummaryRowsData: null,
|
||||
externalEditing: {},
|
||||
externalEditSnapshots: {},
|
||||
matchingStarted: false,
|
||||
@@ -728,9 +769,17 @@ export default {
|
||||
return [...names];
|
||||
},
|
||||
duplicateRows() {
|
||||
return this.externalDetails.filter(
|
||||
row => row.suspectedDuplicate || row.matchStatus === 'suspected_duplicate'
|
||||
const duplicateKeys = new Set();
|
||||
const duplicateRows = [];
|
||||
this.externalDetails.forEach(row => {
|
||||
const key = this.externalDuplicateKey(row);
|
||||
if (duplicateKeys.has(key)) duplicateRows.push(row);
|
||||
else duplicateKeys.add(key);
|
||||
});
|
||||
const duplicateKeySet = new Set(
|
||||
duplicateRows.map(row => this.externalDuplicateKey(row))
|
||||
);
|
||||
return this.externalDetails.filter(row => duplicateKeySet.has(this.externalDuplicateKey(row)));
|
||||
},
|
||||
canStartMatch() {
|
||||
return Boolean(this.form.formalSettlementId && this.externalDetails.length);
|
||||
@@ -767,6 +816,11 @@ export default {
|
||||
return this.form.reconciliationModeName || labels[this.form.reconciliationMode] || '-';
|
||||
},
|
||||
completionSummaryRows() {
|
||||
if (Array.isArray(this.completionSummaryRowsData)) return this.completionSummaryRowsData;
|
||||
return this.buildCompletionSummaryRows;
|
||||
},
|
||||
|
||||
buildCompletionSummaryRows() {
|
||||
const source =
|
||||
this.form.feeSummary ||
|
||||
this.form.feeSummaries ||
|
||||
@@ -796,10 +850,14 @@ export default {
|
||||
: originalAmount;
|
||||
return {
|
||||
name,
|
||||
feeType: '',
|
||||
feeItem: name,
|
||||
originalAmount,
|
||||
adjustAmount: afterAmount - originalAmount,
|
||||
afterAmount,
|
||||
remark: '',
|
||||
manualFlag: 0,
|
||||
_feeKind: '',
|
||||
_index: index,
|
||||
};
|
||||
});
|
||||
@@ -851,6 +909,7 @@ export default {
|
||||
this.externalDetails = [];
|
||||
this.externalEditing = {};
|
||||
this.externalEditSnapshots = {};
|
||||
this.completionSummaryRowsData = null;
|
||||
this.matchingStarted = false;
|
||||
this.externalTab = 'all';
|
||||
this.internalQuery = { documentNo: '', vehicleNo: '', batchNo: '', cargoName: '' };
|
||||
@@ -880,6 +939,7 @@ export default {
|
||||
: data.payeeName) ||
|
||||
'',
|
||||
};
|
||||
this.completionSummaryRowsData = null;
|
||||
const internalRows = data.internalDetails || data.internalBillDetails || data.internals || [];
|
||||
this.internalDetails = internalRows.map(row => this.normalizeInternalRow(row));
|
||||
const externalRows = data.externalDetails || data.externalBillDetails || data.externals || [];
|
||||
@@ -908,6 +968,52 @@ export default {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
externalDuplicateKey(row) {
|
||||
const feeItems = row.feeItems || this.parseFeeItems(row.feeItemsJson);
|
||||
const normalized = value => {
|
||||
if (value === null || value === undefined || value === '') return '';
|
||||
if (typeof value === 'number') return Number(value).toString();
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(
|
||||
Object.keys(value)
|
||||
.sort()
|
||||
.reduce((result, key) => {
|
||||
result[key] = normalized(value[key]);
|
||||
return result;
|
||||
}, {})
|
||||
);
|
||||
}
|
||||
const text = String(value).trim().replace(/\s+/g, '').toLowerCase();
|
||||
return /^-?\d+(\.\d+)?$/.test(text) ? Number(text).toString() : text;
|
||||
};
|
||||
const normalizeDate = value => {
|
||||
if (!value) return '';
|
||||
const date = this.$dayjs(value);
|
||||
return date.isValid() ? date.format('YYYY-MM-DDTHH:mm:ss') : value;
|
||||
};
|
||||
return [
|
||||
row.vehicleNo,
|
||||
row.departureAddress,
|
||||
row.arrivalAddress,
|
||||
normalizeDate(row.actualDepartureTime),
|
||||
normalizeDate(row.actualCompletionTime),
|
||||
row.transportType,
|
||||
row.cargoName,
|
||||
row.cargoType,
|
||||
row.specification,
|
||||
row.model,
|
||||
row.transportQuantity,
|
||||
row.quantityUnit,
|
||||
row.mileage,
|
||||
row.batchNo,
|
||||
row.unitPrice,
|
||||
row.freightAmount,
|
||||
feeItems,
|
||||
row.settlementAmount,
|
||||
]
|
||||
.map(normalized)
|
||||
.join('|');
|
||||
},
|
||||
normalizeInternalRow(row) {
|
||||
const feeItems =
|
||||
row.feeItems && typeof row.feeItems === 'object' && Object.keys(row.feeItems).length
|
||||
@@ -1027,6 +1133,7 @@ export default {
|
||||
if (formalSettlementChanged) {
|
||||
this.externalDetails = [];
|
||||
this.matchingStarted = false;
|
||||
this.completionSummaryRowsData = null;
|
||||
}
|
||||
await this.loadFormalInternalPreview(selected.id);
|
||||
this.formalDialog.visible = false;
|
||||
@@ -1174,6 +1281,7 @@ export default {
|
||||
this.externalDetails = (data.externalDetails || []).map((row, index) =>
|
||||
this.normalizeExternalRow(row, index)
|
||||
);
|
||||
this.completionSummaryRowsData = null;
|
||||
this.matchingStarted = true;
|
||||
this.$message.success('匹配完成');
|
||||
} finally {
|
||||
@@ -1206,10 +1314,22 @@ export default {
|
||||
) {
|
||||
return this.$message.warning('差异单数、差异货量和差异金额必须全部为0才可完成对账');
|
||||
}
|
||||
this.completionSummaryRowsData = this.buildCompletionSummaryRows.map(row => ({ ...row }));
|
||||
this.completionDialog.visible = true;
|
||||
},
|
||||
async confirmCompletion() {
|
||||
if (this.completionDialog.loading) return;
|
||||
for (const row of this.completionSummaryRows) {
|
||||
const adjustAmount = Number(row.adjustAmount || 0);
|
||||
if (row.manualFlag === 1 && row._feeKind === '补款' && adjustAmount <= 0) {
|
||||
return this.$message.warning('补款调整金额必须大于0');
|
||||
}
|
||||
if (row.manualFlag === 1 && row._feeKind === '扣款' && adjustAmount >= 0) {
|
||||
return this.$message.warning('扣款调整金额必须小于0');
|
||||
}
|
||||
if (String(row.remark || '').length > 200) return this.$message.warning('备注不能超过200个字');
|
||||
this.recalculateCompletionFee(row);
|
||||
}
|
||||
this.completionDialog.loading = true;
|
||||
try {
|
||||
this.actionLoading = true;
|
||||
@@ -1220,6 +1340,16 @@ export default {
|
||||
remark: this.form.remark,
|
||||
internalDetails: this.serializeInternalRows(),
|
||||
externalDetails: this.serializeExternalRows(),
|
||||
feeSummary: this.completionSummaryRows.map(row => ({
|
||||
id: row.id || undefined,
|
||||
feeType: row.feeType,
|
||||
feeItem: row.feeItem || row.name,
|
||||
originalAmount: Number(row.originalAmount || 0),
|
||||
adjustAmount: Number(row.adjustAmount || 0),
|
||||
settlementAmount: Number(row.afterAmount || 0),
|
||||
remark: row.remark || '',
|
||||
manualFlag: Number(row.manualFlag || 0),
|
||||
})),
|
||||
})
|
||||
);
|
||||
this.form = { ...this.form, ...data };
|
||||
@@ -1237,7 +1367,55 @@ export default {
|
||||
}
|
||||
},
|
||||
openFeeAdjustment(type) {
|
||||
this.$message.info(`${type}请在结算明细调整中维护`);
|
||||
if (!Array.isArray(this.completionSummaryRowsData)) {
|
||||
this.completionSummaryRowsData = this.buildCompletionSummaryRows.map(row => ({ ...row }));
|
||||
}
|
||||
this.completionSummaryRowsData.push({
|
||||
id: null,
|
||||
name: type,
|
||||
feeType: '其他费用',
|
||||
feeItem: type,
|
||||
originalAmount: 0,
|
||||
adjustAmount: 0,
|
||||
afterAmount: 0,
|
||||
remark: '',
|
||||
manualFlag: 1,
|
||||
_feeKind: type,
|
||||
_index: this.completionSummaryRowsData.length,
|
||||
});
|
||||
},
|
||||
removeCompletionFee(row) {
|
||||
if (row.manualFlag !== 1 || !Array.isArray(this.completionSummaryRowsData)) return;
|
||||
const index = this.completionSummaryRowsData.indexOf(row);
|
||||
if (index >= 0) this.completionSummaryRowsData.splice(index, 1);
|
||||
},
|
||||
recalculateCompletionFee(row) {
|
||||
row.originalAmount = row.manualFlag === 1 ? 0 : Number(row.originalAmount || 0);
|
||||
row.afterAmount = Number(row.originalAmount || 0) + Number(row.adjustAmount || 0);
|
||||
},
|
||||
getCompletionSummarySums({ columns, data }) {
|
||||
return columns.map((column, index) => {
|
||||
if (index === 0) return '合计';
|
||||
const amountPropertyByIndex = {
|
||||
2: 'originalAmount',
|
||||
3: 'adjustAmount',
|
||||
4: 'afterAmount',
|
||||
};
|
||||
const property = column.property || amountPropertyByIndex[index];
|
||||
if (property === 'originalAmount' || property === 'adjustAmount' || property === 'afterAmount') {
|
||||
const total = data.reduce((sum, row) => {
|
||||
const value = property === 'afterAmount' ? row.afterAmount : row[property];
|
||||
return sum + Number(value || 0);
|
||||
}, 0);
|
||||
const text = this.formatCompletionMoney(total);
|
||||
const negativeAmount =
|
||||
(property === 'adjustAmount' || property === 'afterAmount') && total < 0;
|
||||
return negativeAmount
|
||||
? h('span', { class: 'completion-dialog__negative', style: { color: '#f56c6c' } }, text)
|
||||
: text;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
},
|
||||
sumFeeAmount(rows, name) {
|
||||
return rows.reduce((sum, row) => {
|
||||
@@ -1254,11 +1432,16 @@ export default {
|
||||
item.afterAmount ?? item.adjustedAmount ?? item.settlementAmount ?? item.totalAmount ?? originalAmount
|
||||
);
|
||||
return {
|
||||
id: item.id,
|
||||
feeType: item.feeType || '',
|
||||
feeItem: item.feeItem || item.name || item.feeItemName || '',
|
||||
name: item.name || item.feeItem || item.feeItemName || `费用${index + 1}`,
|
||||
originalAmount,
|
||||
adjustAmount: Number(item.adjustAmount ?? item.adjustmentAmount ?? afterAmount - originalAmount),
|
||||
afterAmount,
|
||||
remark: item.remark || item.adjustRemark || '',
|
||||
manualFlag: Number(item.manualFlag || 0),
|
||||
_feeKind: item.feeItem === '补款' || item.feeItem === '扣款' ? item.feeItem : '',
|
||||
_index: index,
|
||||
};
|
||||
},
|
||||
@@ -1781,6 +1964,9 @@ export default {
|
||||
.completion-dialog__negative {
|
||||
color: #f56c6c;
|
||||
}
|
||||
.completion-dialog__negative-input :deep(input) {
|
||||
color: #f56c6c;
|
||||
}
|
||||
.completion-dialog__footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
Reference in New Issue
Block a user