调整结算模块
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="title" width="98%" top="2vh" append-to-body destroy-on-close>
|
||||
<div v-loading="loading" class="reconciliation-editor">
|
||||
<component
|
||||
:is="editorContainer"
|
||||
v-bind="editorContainerProps"
|
||||
@update:model-value="visible = $event"
|
||||
>
|
||||
<div
|
||||
v-loading="loading"
|
||||
class="reconciliation-editor"
|
||||
:class="{ 'reconciliation-editor--page': pageMode }"
|
||||
>
|
||||
<section-card title="导入外部账单,与内部账单核对">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
@@ -98,17 +106,34 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="reconciliation-editor__filter">
|
||||
<el-input v-model="internalQuery.documentNo" placeholder="单据号" clearable />
|
||||
<el-input v-model="internalQuery.vehicleNo" placeholder="车号" clearable />
|
||||
<el-input v-model="internalQuery.batchNo" placeholder="批次号" clearable />
|
||||
<el-input v-model="internalQuery.cargoName" placeholder="货物名称" clearable />
|
||||
<div class="reconciliation-editor__filter-item">
|
||||
<span class="reconciliation-editor__filter-label">单据号</span>
|
||||
<el-input v-model="internalQuery.documentNo" placeholder="请输入" clearable />
|
||||
</div>
|
||||
<div class="reconciliation-editor__filter-item">
|
||||
<span class="reconciliation-editor__filter-label">车号</span>
|
||||
<el-input v-model="internalQuery.vehicleNo" placeholder="请输入" clearable />
|
||||
</div>
|
||||
<div class="reconciliation-editor__filter-item">
|
||||
<span class="reconciliation-editor__filter-label">批次号</span>
|
||||
<el-input v-model="internalQuery.batchNo" placeholder="请输入" clearable />
|
||||
</div>
|
||||
<div class="reconciliation-editor__filter-item">
|
||||
<span class="reconciliation-editor__filter-label">货物名称</span>
|
||||
<el-input v-model="internalQuery.cargoName" placeholder="请输入" clearable />
|
||||
</div>
|
||||
<el-button @click="resetInternalQuery">重置</el-button>
|
||||
<el-button type="primary" @click="internalFilterTick++">查询</el-button>
|
||||
</div>
|
||||
<el-table :data="filteredInternalRows" border max-height="420">
|
||||
<el-table
|
||||
:data="filteredInternalRows"
|
||||
border
|
||||
max-height="420"
|
||||
:row-class-name="internalRowClassName"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in internalColumns"
|
||||
v-for="column in internalTableColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
@@ -133,6 +158,9 @@
|
||||
<span v-else-if="column.prop === 'transportQuantity'">
|
||||
{{ formatTransportQuantity(row.transportQuantity) }}
|
||||
</span>
|
||||
<span v-else-if="column.feeItemName">
|
||||
{{ formatMoney(getFeeItemAmount(row, column.feeItemName)) }}
|
||||
</span>
|
||||
<span v-else-if="column.prop === 'mileage'">{{ formatMileage(row.mileage) }}</span>
|
||||
<span
|
||||
v-else-if="
|
||||
@@ -149,17 +177,11 @@
|
||||
<div class="reconciliation-editor__links">
|
||||
<el-link v-if="editable" type="primary" @click="openAdjust(row)">调整</el-link>
|
||||
<el-link
|
||||
v-if="editable && row.matchResult === 'matched'"
|
||||
v-if="editable && ['matched', 'partial'].includes(row.matchResult)"
|
||||
type="primary"
|
||||
@click="handleUnmatch(row)"
|
||||
>取消匹配</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="editable && row.matchResult !== 'matched'"
|
||||
type="primary"
|
||||
@click="openManualMatch(row)"
|
||||
>人工匹配</el-link
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -192,7 +214,12 @@
|
||||
<el-tab-pane :label="`导入明细(${externalDetails.length})`" name="all" />
|
||||
<el-tab-pane :label="`疑似重复(${duplicateRows.length})`" name="duplicate" />
|
||||
</el-tabs>
|
||||
<el-table :data="visibleExternalRows" border max-height="420">
|
||||
<el-table
|
||||
:data="visibleExternalRows"
|
||||
border
|
||||
max-height="420"
|
||||
:row-class-name="externalRowClassName"
|
||||
>
|
||||
<el-table-column
|
||||
v-for="column in externalColumns"
|
||||
:key="column.prop"
|
||||
@@ -204,12 +231,57 @@
|
||||
<el-tag v-if="column.prop === 'matchStatus'" :type="matchTagType(row.matchStatus)" class="status-text">{{
|
||||
matchName(row.matchStatus)
|
||||
}}</el-tag>
|
||||
<template v-else-if="isExternalRowEditing(row) && isExternalEditableColumn(column.prop)">
|
||||
<el-input-number
|
||||
v-if="column.feeItemName"
|
||||
v-model="row.feeItems[column.feeItemName]"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
size="small"
|
||||
/>
|
||||
<el-date-picker
|
||||
v-else-if="isExternalDateColumn(column.prop)"
|
||||
v-model="row[column.prop]"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
format="YYYY-MM-DD"
|
||||
size="small"
|
||||
placeholder="请选择"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="isExternalNumberColumn(column.prop)"
|
||||
v-model="row[column.prop]"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
size="small"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="column.prop === 'transportType'"
|
||||
v-model="row[column.prop]"
|
||||
size="small"
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in transportTypeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input v-else v-model="row[column.prop]" size="small" />
|
||||
</template>
|
||||
<span v-else-if="column.prop === 'transportType'">
|
||||
{{ transportTypeLabel(row.transportType) }}
|
||||
</span>
|
||||
<span v-else-if="column.prop === 'transportQuantity'">
|
||||
{{ formatTransportQuantity(row.transportQuantity) }}
|
||||
</span>
|
||||
<span v-else-if="column.feeItemName">
|
||||
{{ formatMoney(getFeeItemAmount(row, column.feeItemName)) }}
|
||||
</span>
|
||||
<span v-else-if="column.prop === 'mileage'">{{ formatMileage(row.mileage) }}</span>
|
||||
<span
|
||||
v-else-if="
|
||||
@@ -221,21 +293,24 @@
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" fixed="right" align="center">
|
||||
<template #default="{ row }"
|
||||
><el-link
|
||||
v-if="editable && row.matchStatus !== 'matched'"
|
||||
type="primary"
|
||||
@click="openManualMatchByExternal(row)"
|
||||
>匹配</el-link
|
||||
></template
|
||||
>
|
||||
<el-table-column label="操作" width="150" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="reconciliation-editor__links">
|
||||
<template v-if="editable && isExternalRowEditing(row)">
|
||||
<el-link type="primary" @click="finishExternalAdjust(row)">完成</el-link>
|
||||
<el-link type="primary" @click="cancelExternalAdjust(row)">取消</el-link>
|
||||
</template>
|
||||
<el-link v-else-if="editable" type="primary" @click="openExternalAdjust(row)">
|
||||
调整
|
||||
</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section-card>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<template v-if="!pageMode" #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<template v-if="editable">
|
||||
<el-button type="primary" plain :loading="saving" @click="handleSave">保存草稿</el-button>
|
||||
@@ -248,6 +323,18 @@
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<div v-if="pageMode" class="reconciliation-editor__page-actions">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<template v-if="editable">
|
||||
<el-button type="primary" plain :loading="actionLoading" @click="handleUpdate">
|
||||
按匹配结果更新账单
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="actionLoading" @click="handleComplete">
|
||||
完成对账
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="formalDialog.visible" title="选择正式结算单" width="84%" append-to-body>
|
||||
<el-form :model="formalDialog.query" inline label-position="right" label-width="88px">
|
||||
<el-form-item label="正式结算单号"
|
||||
@@ -335,34 +422,7 @@
|
||||
>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="manualDialog.visible" title="选择外部账单明细" width="86%" append-to-body>
|
||||
<el-table :data="unmatchedExternalRows" border @row-click="manualDialog.selected = $event">
|
||||
<el-table-column type="index" label="序号" width="64" />
|
||||
<el-table-column prop="externalLineNo" label="外部行号" width="90" />
|
||||
<el-table-column prop="vehicleNo" label="车牌号" width="110" />
|
||||
<el-table-column prop="cargoName" label="货物名称" min-width="140" />
|
||||
<el-table-column prop="transportQuantity" label="运输量" width="110"
|
||||
><template #default="{ row }">{{
|
||||
formatTransportQuantity(row.transportQuantity)
|
||||
}}</template></el-table-column
|
||||
>
|
||||
<el-table-column prop="settlementAmount" label="结算金额" width="130"
|
||||
><template #default="{ row }">{{
|
||||
formatMoney(row.settlementAmount)
|
||||
}}</template></el-table-column
|
||||
>
|
||||
<el-table-column label="选择" width="80"
|
||||
><template #default="{ row }"
|
||||
><el-radio v-model="manualDialog.selected" :label="row"> </el-radio></template
|
||||
></el-table-column
|
||||
>
|
||||
</el-table>
|
||||
<template #footer
|
||||
><el-button @click="manualDialog.visible = false">取消</el-button
|
||||
><el-button type="primary" @click="confirmManualMatch">确定</el-button></template
|
||||
>
|
||||
</el-dialog>
|
||||
</el-dialog>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -385,6 +445,7 @@ export default {
|
||||
recordId: [String, Number],
|
||||
settlementType: { type: String, default: 'payable' },
|
||||
readonly: Boolean,
|
||||
pageMode: Boolean,
|
||||
},
|
||||
emits: ['update:modelValue', 'success'],
|
||||
data() {
|
||||
@@ -411,7 +472,9 @@ export default {
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
},
|
||||
adjustDialog: { visible: false, saving: false, rows: [], reason: '' },
|
||||
manualDialog: { visible: false, internal: null, selected: null },
|
||||
externalEditing: {},
|
||||
externalEditSnapshots: {},
|
||||
matchingStarted: false,
|
||||
rules: {
|
||||
formalSettlementNo: [{ required: true, message: '请选择正式结算单', trigger: 'change' }],
|
||||
reconciliationMode: [{ required: true, message: '请选择对账模式', trigger: 'change' }],
|
||||
@@ -429,6 +492,22 @@ export default {
|
||||
this.$emit('update:modelValue', value);
|
||||
},
|
||||
},
|
||||
editorContainer() {
|
||||
return this.pageMode ? 'div' : 'el-dialog';
|
||||
},
|
||||
editorContainerProps() {
|
||||
if (this.pageMode) {
|
||||
return { class: 'reconciliation-editor-shell reconciliation-editor-shell--page' };
|
||||
}
|
||||
return {
|
||||
modelValue: this.visible,
|
||||
title: this.title,
|
||||
width: '98%',
|
||||
top: '2vh',
|
||||
appendToBody: true,
|
||||
destroyOnClose: true,
|
||||
};
|
||||
},
|
||||
editable() {
|
||||
return !this.readonly;
|
||||
},
|
||||
@@ -442,7 +521,7 @@ export default {
|
||||
filteredInternalRows() {
|
||||
void this.internalFilterTick;
|
||||
const query = this.internalQuery;
|
||||
return this.internalDetails.filter(
|
||||
return this.groupedInternalRows.filter(
|
||||
row =>
|
||||
(!query.documentNo || String(row.documentNo || '').includes(query.documentNo)) &&
|
||||
(!query.vehicleNo || String(row.vehicleNo || '').includes(query.vehicleNo)) &&
|
||||
@@ -450,10 +529,62 @@ export default {
|
||||
(!query.cargoName || String(row.cargoName || '').includes(query.cargoName))
|
||||
);
|
||||
},
|
||||
groupedInternalRows() {
|
||||
const groups = new Map();
|
||||
this.internalDetails.forEach((row, index) => {
|
||||
const key = row.documentNo ? String(row.documentNo) : `__row_${index}`;
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key).push(row);
|
||||
});
|
||||
return [...groups.values()].map(rows => this.mergeInternalRows(rows));
|
||||
},
|
||||
internalFeeItemNames() {
|
||||
const names = new Set();
|
||||
this.internalDetails.forEach(row => {
|
||||
Object.keys(row.feeItems || this.parseFeeItems(row.feeItemsJson)).forEach(name => {
|
||||
if (name) names.add(name);
|
||||
});
|
||||
});
|
||||
return [...names];
|
||||
},
|
||||
internalTableColumns() {
|
||||
const columns = internalColumns.filter(column => column.prop !== 'freightAmount');
|
||||
const settlementIndex = columns.findIndex(column => column.prop === 'settlementAmount');
|
||||
const feeColumns = this.internalFeeItemNames.map((name, index) => ({
|
||||
prop: `feeItem_${index}`,
|
||||
label: name,
|
||||
minWidth: 120,
|
||||
money: true,
|
||||
feeItemName: name,
|
||||
}));
|
||||
columns.splice(settlementIndex < 0 ? columns.length : settlementIndex, 0, ...feeColumns);
|
||||
return columns;
|
||||
},
|
||||
externalColumns() {
|
||||
return this.form.reconciliationMode === 'cargo'
|
||||
? externalCargoColumns
|
||||
: externalVehicleColumns;
|
||||
const columns = [
|
||||
...(this.form.reconciliationMode === 'cargo'
|
||||
? externalCargoColumns
|
||||
: externalVehicleColumns),
|
||||
];
|
||||
const settlementIndex = columns.findIndex(column => column.prop === 'settlementAmount');
|
||||
const feeColumns = this.externalFeeItemNames.map((name, index) => ({
|
||||
prop: `externalFeeItem_${index}`,
|
||||
label: name,
|
||||
minWidth: 120,
|
||||
money: true,
|
||||
feeItemName: name,
|
||||
}));
|
||||
columns.splice(settlementIndex < 0 ? columns.length : settlementIndex, 0, ...feeColumns);
|
||||
return columns;
|
||||
},
|
||||
externalFeeItemNames() {
|
||||
const names = new Set();
|
||||
this.externalDetails.forEach(row => {
|
||||
Object.keys(row.feeItems || this.parseFeeItems(row.feeItemsJson)).forEach(name => {
|
||||
if (name) names.add(name);
|
||||
});
|
||||
});
|
||||
return [...names];
|
||||
},
|
||||
duplicateRows() {
|
||||
return this.externalDetails.filter(
|
||||
@@ -466,15 +597,13 @@ export default {
|
||||
visibleExternalRows() {
|
||||
return this.externalTab === 'duplicate' ? this.duplicateRows : this.externalDetails;
|
||||
},
|
||||
unmatchedExternalRows() {
|
||||
return this.externalDetails.filter(
|
||||
row => row.matchStatus !== 'matched' && !row.suspectedDuplicate
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue(value) {
|
||||
if (value) this.initialize();
|
||||
modelValue: {
|
||||
immediate: true,
|
||||
handler(value) {
|
||||
if (value) this.initialize();
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -484,6 +613,7 @@ export default {
|
||||
reconciliationMode: 'vehicle',
|
||||
formalSettlementId: null,
|
||||
formalSettlementNo: '',
|
||||
customerName: '',
|
||||
payerName: '',
|
||||
payeeName: '',
|
||||
projectName: '',
|
||||
@@ -512,6 +642,9 @@ export default {
|
||||
this.form = this.emptyForm();
|
||||
this.internalDetails = [];
|
||||
this.externalDetails = [];
|
||||
this.externalEditing = {};
|
||||
this.externalEditSnapshots = {};
|
||||
this.matchingStarted = false;
|
||||
this.externalTab = 'all';
|
||||
this.internalQuery = { documentNo: '', vehicleNo: '', batchNo: '', cargoName: '' };
|
||||
if (!this.currentId) {
|
||||
@@ -530,11 +663,27 @@ export default {
|
||||
},
|
||||
async loadDetail() {
|
||||
const data = this.unwrapData(await api.getDetail(this.currentId)) || {};
|
||||
this.form = { ...this.emptyForm(), ...data };
|
||||
this.internalDetails =
|
||||
data.internalDetails || data.internalBillDetails || data.internals || [];
|
||||
this.externalDetails =
|
||||
data.externalDetails || data.externalBillDetails || data.externals || [];
|
||||
this.form = {
|
||||
...this.emptyForm(),
|
||||
...data,
|
||||
customerName:
|
||||
data.customerName ||
|
||||
((data.settlementType || this.settlementType) === 'receivable'
|
||||
? data.payerName
|
||||
: data.payeeName) ||
|
||||
'',
|
||||
};
|
||||
const internalRows = data.internalDetails || data.internalBillDetails || data.internals || [];
|
||||
this.internalDetails = internalRows.map(row => this.normalizeInternalRow(row));
|
||||
const externalRows = data.externalDetails || data.externalBillDetails || data.externals || [];
|
||||
this.externalDetails = externalRows.map((row, index) =>
|
||||
this.normalizeExternalRow(row, index)
|
||||
);
|
||||
this.matchingStarted =
|
||||
this.matchingStarted ||
|
||||
data.matchStatus === 'partial' ||
|
||||
this.internalDetails.some(row => row.matchResult === 'matched') ||
|
||||
this.externalDetails.some(row => row.matchStatus === 'matched');
|
||||
},
|
||||
openFormalDialog() {
|
||||
if (!this.editable) return;
|
||||
@@ -542,6 +691,80 @@ export default {
|
||||
this.formalDialog.page.current = 1;
|
||||
this.loadFormalOptions();
|
||||
},
|
||||
parseFeeItems(value) {
|
||||
if (!value) return {};
|
||||
if (typeof value === 'object' && !Array.isArray(value)) return value;
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
normalizeInternalRow(row) {
|
||||
const feeItems =
|
||||
row.feeItems && typeof row.feeItems === 'object' && Object.keys(row.feeItems).length
|
||||
? row.feeItems
|
||||
: this.parseFeeItems(row.feeItemsJson);
|
||||
return { ...row, feeItems };
|
||||
},
|
||||
normalizeExternalRow(row, index) {
|
||||
const feeItems =
|
||||
row.feeItems && typeof row.feeItems === 'object' && Object.keys(row.feeItems).length
|
||||
? row.feeItems
|
||||
: this.parseFeeItems(row.feeItemsJson);
|
||||
return {
|
||||
...row,
|
||||
feeItems,
|
||||
_externalKey: row.id || row.externalLineNo || `external-${index}`,
|
||||
};
|
||||
},
|
||||
mergeInternalRows(rows) {
|
||||
const firstRow = rows[0] || {};
|
||||
const feeItems = {};
|
||||
rows.forEach(row => {
|
||||
Object.entries(row.feeItems || this.parseFeeItems(row.feeItemsJson)).forEach(
|
||||
([name, amount]) => {
|
||||
feeItems[name] = Number(feeItems[name] || 0) + Number(amount || 0);
|
||||
}
|
||||
);
|
||||
});
|
||||
const matchResults = rows.map(row => row.matchResult);
|
||||
const updateResults = rows.map(row => row.updateResult);
|
||||
return {
|
||||
...firstRow,
|
||||
cargoName: this.joinInternalValues(rows, 'cargoName'),
|
||||
cargoType: this.joinInternalValues(rows, 'cargoType'),
|
||||
transportQuantity: this.sumInternalValues(rows, 'transportQuantity'),
|
||||
settlementAmount: this.sumInternalValues(rows, 'settlementAmount'),
|
||||
feeItems,
|
||||
matchedExternalLineNo: this.joinInternalValues(rows, 'matchedExternalLineNo'),
|
||||
matchResult: matchResults.every(value => value === 'matched')
|
||||
? 'matched'
|
||||
: matchResults.some(value => value === 'matched')
|
||||
? 'partial'
|
||||
: 'unmatched',
|
||||
updateResult: updateResults.every(value => value === updateResults[0])
|
||||
? updateResults[0]
|
||||
: 'partial_updated',
|
||||
_sourceRows: rows,
|
||||
};
|
||||
},
|
||||
joinInternalValues(rows, prop) {
|
||||
return [
|
||||
...new Set(
|
||||
rows
|
||||
.map(row => row[prop])
|
||||
.filter(value => value !== null && value !== undefined && value !== '')
|
||||
),
|
||||
].join('、');
|
||||
},
|
||||
sumInternalValues(rows, prop) {
|
||||
return rows.reduce((total, row) => total + Number(row[prop] || 0), 0);
|
||||
},
|
||||
getFeeItemAmount(row, name) {
|
||||
return (row.feeItems || this.parseFeeItems(row.feeItemsJson))[name] ?? 0;
|
||||
},
|
||||
async loadFormalOptions() {
|
||||
this.formalDialog.loading = true;
|
||||
try {
|
||||
@@ -586,25 +809,35 @@ export default {
|
||||
...selected,
|
||||
formalSettlementId: selected.id,
|
||||
formalSettlementNo: selected.formalSettlementNo,
|
||||
customerName:
|
||||
selected.customerName ||
|
||||
((selected.settlementType || this.settlementType) === 'receivable'
|
||||
? selected.payerName
|
||||
: selected.payeeName) ||
|
||||
'',
|
||||
reconciliationNo: this.form.reconciliationNo,
|
||||
};
|
||||
if (formalSettlementChanged) this.externalDetails = [];
|
||||
if (formalSettlementChanged) {
|
||||
this.externalDetails = [];
|
||||
this.matchingStarted = false;
|
||||
}
|
||||
await this.loadFormalInternalPreview(selected.id);
|
||||
this.formalDialog.visible = false;
|
||||
},
|
||||
async loadFormalInternalPreview(formalSettlementId) {
|
||||
const formal = this.unwrapData(await formalSettlementApi.getDetail(formalSettlementId));
|
||||
const details = formal.details || [];
|
||||
const buildInternalRow = (detail, overrides = {}, index = 0) => ({
|
||||
...detail,
|
||||
...overrides,
|
||||
id: null,
|
||||
lineNo: index + 1,
|
||||
formalSettlementDetailId: detail.id,
|
||||
settlementAmount: overrides.settlementAmount ?? detail.settlementAmountTax ?? 0,
|
||||
matchResult: 'unmatched',
|
||||
updateResult: 'not_updated',
|
||||
});
|
||||
const buildInternalRow = (detail, overrides = {}, index = 0) =>
|
||||
this.normalizeInternalRow({
|
||||
...detail,
|
||||
...overrides,
|
||||
id: null,
|
||||
lineNo: index + 1,
|
||||
formalSettlementDetailId: detail.id,
|
||||
settlementAmount: overrides.settlementAmount ?? detail.settlementAmountTax ?? 0,
|
||||
matchResult: 'unmatched',
|
||||
updateResult: 'not_updated',
|
||||
});
|
||||
if (this.form.reconciliationMode === 'cargo') {
|
||||
const feeGroups = await Promise.all(
|
||||
details.map(async detail => ({
|
||||
@@ -688,7 +921,7 @@ export default {
|
||||
await this.loadDetail();
|
||||
if (!silent) {
|
||||
this.$message.success('草稿保存成功');
|
||||
this.$emit('success');
|
||||
this.$emit('success', this.currentId);
|
||||
}
|
||||
return data;
|
||||
} finally {
|
||||
@@ -701,6 +934,7 @@ export default {
|
||||
try {
|
||||
await api.match(this.currentId);
|
||||
await this.loadDetail();
|
||||
this.matchingStarted = true;
|
||||
this.$message.success('匹配完成');
|
||||
} finally {
|
||||
this.actionLoading = false;
|
||||
@@ -740,7 +974,7 @@ export default {
|
||||
await this.loadDetail();
|
||||
this.$message.success('对账完成');
|
||||
this.visible = false;
|
||||
this.$emit('success');
|
||||
this.$emit('success', this.currentId);
|
||||
} finally {
|
||||
this.actionLoading = false;
|
||||
}
|
||||
@@ -773,7 +1007,9 @@ export default {
|
||||
if (result.code !== 200) throw new Error(result.msg || '导入失败');
|
||||
this.$message.success('外部账单导入成功');
|
||||
}
|
||||
this.matchingStarted = false;
|
||||
await this.loadDetail();
|
||||
this.matchingStarted = false;
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '外部账单导入失败');
|
||||
}
|
||||
@@ -794,9 +1030,9 @@ export default {
|
||||
visible: true,
|
||||
saving: false,
|
||||
reason: '',
|
||||
rows: this.internalDetails
|
||||
.filter(item => item.formalSettlementDetailId === row.formalSettlementDetailId)
|
||||
.map(item => ({ ...item })),
|
||||
rows: (row._sourceRows || this.internalDetails.filter(
|
||||
item => item.formalSettlementDetailId === row.formalSettlementDetailId
|
||||
)).map(item => ({ ...item })),
|
||||
};
|
||||
},
|
||||
async saveAdjust() {
|
||||
@@ -817,26 +1053,85 @@ export default {
|
||||
}
|
||||
},
|
||||
async handleUnmatch(row) {
|
||||
await api.unmatch(row.id);
|
||||
const matchedRows = (row._sourceRows || [row]).filter(item => item.matchResult === 'matched');
|
||||
for (const item of matchedRows) await api.unmatch(item.id);
|
||||
await this.loadDetail();
|
||||
},
|
||||
openManualMatch(row) {
|
||||
this.manualDialog = { visible: true, internal: row, selected: null };
|
||||
internalRowClassName({ row }) {
|
||||
return this.matchingStarted && row.matchResult !== 'matched'
|
||||
? 'reconciliation-editor__row--unmatched'
|
||||
: '';
|
||||
},
|
||||
openManualMatchByExternal(row) {
|
||||
this.manualDialog = { visible: true, internal: null, selected: row };
|
||||
externalRowClassName({ row }) {
|
||||
return this.matchingStarted && row.matchStatus !== 'matched'
|
||||
? 'reconciliation-editor__row--unmatched'
|
||||
: '';
|
||||
},
|
||||
async confirmManualMatch() {
|
||||
if (!this.manualDialog.selected) return this.$message.warning('请选择外部账单明细');
|
||||
if (!this.manualDialog.internal) return this.$message.warning('请从内部账单行发起人工匹配');
|
||||
await api.manualMatch({
|
||||
reconciliationId: this.currentId,
|
||||
internalId: this.manualDialog.internal.id,
|
||||
externalId: this.manualDialog.selected.id,
|
||||
});
|
||||
this.manualDialog.visible = false;
|
||||
await this.loadDetail();
|
||||
this.$message.success('人工匹配成功');
|
||||
externalRowKey(row) {
|
||||
return String(row.id || row.externalLineNo || row._externalKey || '');
|
||||
},
|
||||
isExternalRowEditing(row) {
|
||||
return Boolean(this.externalEditing[this.externalRowKey(row)]);
|
||||
},
|
||||
isExternalEditableColumn(prop) {
|
||||
return !['externalLineNo', 'matchStatus'].includes(prop);
|
||||
},
|
||||
isExternalDateColumn(prop) {
|
||||
return ['actualDepartureTime', 'actualCompletionTime'].includes(prop);
|
||||
},
|
||||
isExternalNumberColumn(prop) {
|
||||
return ['transportQuantity', 'mileage', 'unitPrice', 'freightAmount', 'settlementAmount'].includes(prop);
|
||||
},
|
||||
openExternalAdjust(row) {
|
||||
const key = this.externalRowKey(row);
|
||||
this.externalEditSnapshots = {
|
||||
...this.externalEditSnapshots,
|
||||
[key]: { ...row },
|
||||
};
|
||||
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;
|
||||
},
|
||||
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;
|
||||
this.refreshExternalStats();
|
||||
},
|
||||
refreshExternalStats() {
|
||||
const externalQuantity = this.externalDetails.reduce(
|
||||
(total, item) => total + Number(item.transportQuantity || 0),
|
||||
0
|
||||
);
|
||||
const externalAmount = this.externalDetails.reduce(
|
||||
(total, item) => total + Number(item.settlementAmount || 0),
|
||||
0
|
||||
);
|
||||
const matchedCount = this.externalDetails.filter(item => item.matchStatus === 'matched').length;
|
||||
this.form = {
|
||||
...this.form,
|
||||
externalBillCount: this.externalDetails.length,
|
||||
externalQuantity,
|
||||
externalAmount,
|
||||
differenceQuantity: Math.abs(Number(this.form.internalQuantity || 0) - externalQuantity),
|
||||
differenceAmount: Math.abs(Number(this.form.internalAmount || 0) - externalAmount),
|
||||
matchedCount,
|
||||
unmatchedCount: Math.max(Number(this.form.internalBillCount || 0), this.externalDetails.length) - matchedCount,
|
||||
};
|
||||
},
|
||||
matchName(value) {
|
||||
return (
|
||||
@@ -863,6 +1158,7 @@ export default {
|
||||
updated: '已更新',
|
||||
skipped_multi_cargo: '跳过更新',
|
||||
manually_adjusted: '手工调整',
|
||||
partial_updated: '部分更新',
|
||||
not_updated: '未更新',
|
||||
}[value] ||
|
||||
value ||
|
||||
@@ -872,7 +1168,7 @@ export default {
|
||||
updateTagType(value) {
|
||||
return value === 'updated' || value === 'manually_adjusted'
|
||||
? 'success'
|
||||
: value === 'skipped_multi_cargo'
|
||||
: value === 'skipped_multi_cargo' || value === 'partial_updated'
|
||||
? 'warning'
|
||||
: 'info';
|
||||
},
|
||||
@@ -956,11 +1252,50 @@ export default {
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.reconciliation-editor__filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.reconciliation-editor__filter-label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 8px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.reconciliation-editor__filter-item :deep(.el-input) {
|
||||
min-width: 0;
|
||||
}
|
||||
.reconciliation-editor__pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.reconciliation-editor-shell--page {
|
||||
display: block;
|
||||
}
|
||||
.reconciliation-editor--page {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
.reconciliation-editor__page-actions {
|
||||
position: fixed;
|
||||
z-index: 20;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 230px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
min-height: 64px;
|
||||
box-sizing: border-box;
|
||||
padding: 12px 24px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #eff1f7;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.reconciliation-editor__page-actions .el-button + .el-button {
|
||||
margin-left: 12px;
|
||||
}
|
||||
.reconciliation-editor__help {
|
||||
color: #606266;
|
||||
line-height: 1.8;
|
||||
@@ -980,6 +1315,20 @@ export default {
|
||||
.reconciliation-editor :deep(.el-table__body tr:nth-child(even) > td.el-table__cell) {
|
||||
background: #fafafa;
|
||||
}
|
||||
.reconciliation-editor :deep(.el-table__body tr.reconciliation-editor__row--unmatched > td.el-table__cell) {
|
||||
color: #f56c6c;
|
||||
background: #fff1f0 !important;
|
||||
}
|
||||
.reconciliation-editor
|
||||
:deep(.el-table__body tr.reconciliation-editor__row--unmatched > td.el-table__cell .cell) {
|
||||
color: #f56c6c;
|
||||
}
|
||||
.reconciliation-editor
|
||||
:deep(.el-table__body tr.reconciliation-editor__row--unmatched .status-text) {
|
||||
color: #f56c6c;
|
||||
background: #fde2e2;
|
||||
border-color: #fbc4c4;
|
||||
}
|
||||
@media (max-width: 1200px) {
|
||||
.reconciliation-editor__stats {
|
||||
grid-template-columns: repeat(3, minmax(150px, 1fr));
|
||||
|
||||
Reference in New Issue
Block a user