1、调整凭证
2、调整运单
This commit is contained in:
@@ -610,12 +610,30 @@
|
||||
</div>
|
||||
<template v-if="!pageMode" #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button
|
||||
v-if="editable && !recordId"
|
||||
type="primary"
|
||||
plain
|
||||
:loading="draftSaving"
|
||||
@click="handleDraftSave"
|
||||
>
|
||||
保存
|
||||
</el-button>
|
||||
<el-button v-if="editable" type="primary" :loading="saving" @click="handleSave"
|
||||
>提交</el-button
|
||||
>
|
||||
</template>
|
||||
<div v-if="pageMode" class="formal-editor__page-actions">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button
|
||||
v-if="editable && !recordId"
|
||||
type="primary"
|
||||
plain
|
||||
:loading="draftSaving"
|
||||
@click="handleDraftSave"
|
||||
>
|
||||
保存
|
||||
</el-button>
|
||||
<el-button v-if="editable" type="primary" :loading="saving" @click="handleSave">
|
||||
提交
|
||||
</el-button>
|
||||
@@ -964,6 +982,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
draftSaving: false,
|
||||
saving: false,
|
||||
form: createFormalSettlementForm(),
|
||||
sources: [],
|
||||
@@ -2396,6 +2415,20 @@ export default {
|
||||
remark: this.form.remark,
|
||||
};
|
||||
},
|
||||
async handleDraftSave() {
|
||||
this.draftSaving = true;
|
||||
try {
|
||||
await save({
|
||||
...this.buildSavePayload(),
|
||||
approvalStatus: 'draft',
|
||||
});
|
||||
this.$message.success('保存成功');
|
||||
this.visible = false;
|
||||
this.$emit('success');
|
||||
} finally {
|
||||
this.draftSaving = false;
|
||||
}
|
||||
},
|
||||
async handleSave() {
|
||||
await this.$refs.formRef.validate();
|
||||
if (!this.form.sourcePreSettlementIds.length && !this.form.sourceDetailIds.length) {
|
||||
|
||||
@@ -150,7 +150,18 @@
|
||||
>{{ updateName(row.updateResult) }}</el-tag
|
||||
>
|
||||
<span v-else-if="column.prop === 'matchedExternalLineNo'">
|
||||
{{ formatMatchedExternalLineNo(row.matchedExternalLineNo) }}
|
||||
<el-input
|
||||
:model-value="
|
||||
hasMatchedExternal(row) ? formatMatchedExternalLineNo(row.matchedExternalLineNo) : ''
|
||||
"
|
||||
readonly
|
||||
size="small"
|
||||
:placeholder="hasMatchedExternal(row) ? '' : '请选择匹配账单'"
|
||||
>
|
||||
<template #append>
|
||||
<el-button :icon="Search" :disabled="!editable" @click="openExternalMatchDialog(row)" />
|
||||
</template>
|
||||
</el-input>
|
||||
</span>
|
||||
<span v-else-if="column.prop === 'transportType'">
|
||||
{{ transportTypeLabel(row.transportType) }}
|
||||
@@ -422,6 +433,129 @@
|
||||
>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="manualMatchDialog.visible"
|
||||
title="选择未匹配外部账单"
|
||||
width="92%"
|
||||
append-to-body
|
||||
>
|
||||
<el-table v-loading="manualMatchDialog.loading" :data="unmatchedExternalRows" border max-height="520">
|
||||
<el-table-column
|
||||
v-for="column in externalColumns"
|
||||
:key="`manual-${column.prop}`"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span v-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="column.prop === 'actualDepartureTime' || column.prop === 'actualCompletionTime'">{{ formatDate(row[column.prop]) }}</span>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="handleManualMatch(row)">选择</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="manualMatchDialog.visible = false">取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="completionDialog.visible"
|
||||
title="汇总对账结果费用"
|
||||
width="92%"
|
||||
top="4vh"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
class="completion-dialog"
|
||||
>
|
||||
<div class="completion-dialog__content">
|
||||
<section class="completion-dialog__section">
|
||||
<div class="completion-dialog__grid">
|
||||
<div class="completion-dialog__field">
|
||||
<span class="completion-dialog__label">对账单号</span>
|
||||
<span class="completion-dialog__value">{{ displayValue(form.reconciliationNo) }}</span>
|
||||
</div>
|
||||
<div class="completion-dialog__field">
|
||||
<span class="completion-dialog__label">预结算单号</span>
|
||||
<span class="completion-dialog__value">{{
|
||||
displayValue(form.preSettlementNo || form.formalSettlementNo)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="completion-dialog__field">
|
||||
<span class="completion-dialog__label">合同编号</span>
|
||||
<span class="completion-dialog__value">{{ displayValue(form.contractNo) }}</span>
|
||||
</div>
|
||||
<div class="completion-dialog__field">
|
||||
<span class="completion-dialog__label">单据周期</span>
|
||||
<span class="completion-dialog__value">{{ completionPeriod }}</span>
|
||||
</div>
|
||||
<div class="completion-dialog__field">
|
||||
<span class="completion-dialog__label">客户/承运商</span>
|
||||
<span class="completion-dialog__value">{{ displayValue(form.customerName) }}</span>
|
||||
</div>
|
||||
<div class="completion-dialog__field">
|
||||
<span class="completion-dialog__label">项目</span>
|
||||
<span class="completion-dialog__value">{{ displayValue(form.projectName) }}</span>
|
||||
</div>
|
||||
<div class="completion-dialog__field">
|
||||
<span class="completion-dialog__label">对账模式</span>
|
||||
<span class="completion-dialog__value">{{ reconciliationModeLabel }}</span>
|
||||
</div>
|
||||
<div class="completion-dialog__field completion-dialog__field--remark">
|
||||
<span class="completion-dialog__label">备注</span>
|
||||
<span class="completion-dialog__value">{{ displayValue(form.remark) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="completion-dialog__section completion-dialog__section--fees">
|
||||
<div class="completion-dialog__section-title">费用合计信息</div>
|
||||
<div class="completion-dialog__fee-actions">
|
||||
<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-column type="index" label="序号" width="72" align="center" />
|
||||
<el-table-column prop="name" label="费用项目" min-width="150" align="center" />
|
||||
<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 }">{{
|
||||
formatCompletionMoney(row.adjustAmount)
|
||||
}}</span></template
|
||||
>
|
||||
</el-table-column>
|
||||
<el-table-column label="调整后金额(元)" min-width="180" align="right">
|
||||
<template #default="{ row }">{{ formatCompletionMoney(row.afterAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="220" />
|
||||
</el-table>
|
||||
</section>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="completion-dialog__footer">
|
||||
<el-button type="primary" plain @click="completionDialog.visible = false"
|
||||
>上一步</el-button
|
||||
>
|
||||
<el-button type="primary" :loading="completionDialog.loading" @click="confirmCompletion"
|
||||
>确认</el-button
|
||||
>
|
||||
<el-button @click="completionDialog.visible = false">取消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</component>
|
||||
</template>
|
||||
|
||||
@@ -430,6 +564,7 @@ import { mapGetters } from 'vuex';
|
||||
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 { downloadXls } from '@/utils/util';
|
||||
import { transportReconciliationFormFields } from '@/option/settlement/transportReconciliationForm';
|
||||
import {
|
||||
@@ -450,6 +585,7 @@ export default {
|
||||
emits: ['update:modelValue', 'success'],
|
||||
data() {
|
||||
return {
|
||||
Search,
|
||||
loading: false,
|
||||
saving: false,
|
||||
actionLoading: false,
|
||||
@@ -472,6 +608,8 @@ export default {
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
},
|
||||
adjustDialog: { visible: false, saving: false, rows: [], reason: '' },
|
||||
manualMatchDialog: { visible: false, loading: false, row: null },
|
||||
completionDialog: { visible: false, loading: false },
|
||||
externalEditing: {},
|
||||
externalEditSnapshots: {},
|
||||
matchingStarted: false,
|
||||
@@ -530,6 +668,9 @@ export default {
|
||||
);
|
||||
},
|
||||
groupedInternalRows() {
|
||||
if (this.form.reconciliationMode === 'cargo') {
|
||||
return this.internalDetails.map(row => ({ ...row, _sourceRows: [row] }));
|
||||
}
|
||||
const groups = new Map();
|
||||
this.internalDetails.forEach((row, index) => {
|
||||
const key = row.documentNo ? String(row.documentNo) : `__row_${index}`;
|
||||
@@ -597,6 +738,72 @@ export default {
|
||||
visibleExternalRows() {
|
||||
return this.externalTab === 'duplicate' ? this.duplicateRows : this.externalDetails;
|
||||
},
|
||||
unmatchedExternalRows() {
|
||||
return this.externalDetails.filter(row => row.matchStatus !== 'matched');
|
||||
},
|
||||
completionPeriod() {
|
||||
const period = this.form.period || this.form.documentPeriod || this.form.settlementPeriod;
|
||||
if (Array.isArray(period) && period.length) return period.filter(Boolean).join(' 至 ');
|
||||
if (typeof period === 'string' && period.trim()) return period;
|
||||
const start =
|
||||
this.form.periodStart ||
|
||||
this.form.periodStartDate ||
|
||||
this.form.startDate ||
|
||||
this.form.billStartDate ||
|
||||
this.form.reconciliationStartDate ||
|
||||
this.form.settlementStartDate;
|
||||
const end =
|
||||
this.form.periodEnd ||
|
||||
this.form.periodEndDate ||
|
||||
this.form.endDate ||
|
||||
this.form.billEndDate ||
|
||||
this.form.reconciliationEndDate ||
|
||||
this.form.settlementEndDate;
|
||||
if (start || end) return [start, end].filter(Boolean).join(' 至 ');
|
||||
return '-';
|
||||
},
|
||||
reconciliationModeLabel() {
|
||||
const labels = { vehicle: '整车总额对账', cargo: '货物明细对账' };
|
||||
return this.form.reconciliationModeName || labels[this.form.reconciliationMode] || '-';
|
||||
},
|
||||
completionSummaryRows() {
|
||||
const source =
|
||||
this.form.feeSummary ||
|
||||
this.form.feeSummaries ||
|
||||
this.form.feeDetails ||
|
||||
this.form.summaryFees;
|
||||
if (Array.isArray(source) && source.length) {
|
||||
return source.map((item, index) => this.normalizeCompletionFee(item, index));
|
||||
}
|
||||
if (source && typeof source === 'object') {
|
||||
return Object.entries(source).map(([name, item], index) =>
|
||||
this.normalizeCompletionFee(
|
||||
typeof item === 'object' ? { ...item, name: item.name || name } : { name, afterAmount: item },
|
||||
index
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const names = new Set([...this.internalFeeItemNames, ...this.externalFeeItemNames]);
|
||||
if (!names.size) names.add('运费');
|
||||
return [...names].map((name, index) => {
|
||||
const originalAmount = this.sumFeeAmount(this.internalDetails, name);
|
||||
const externalAmount = this.sumFeeAmount(this.externalDetails, name);
|
||||
const afterAmount = this.externalDetails.length
|
||||
? externalAmount
|
||||
: name === '运费'
|
||||
? Number(this.form.internalAmount || 0)
|
||||
: originalAmount;
|
||||
return {
|
||||
name,
|
||||
originalAmount,
|
||||
adjustAmount: afterAmount - originalAmount,
|
||||
afterAmount,
|
||||
remark: '',
|
||||
_index: index,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue: {
|
||||
@@ -928,12 +1135,45 @@ export default {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
serializeInternalRows() {
|
||||
return this.internalDetails.map(row => {
|
||||
const snapshot = { ...row };
|
||||
delete snapshot._sourceRows;
|
||||
delete snapshot._externalKey;
|
||||
delete snapshot.feeItems;
|
||||
snapshot.feeItemsJson = row.feeItemsJson || JSON.stringify(row.feeItems || {});
|
||||
return snapshot;
|
||||
});
|
||||
},
|
||||
serializeExternalRows() {
|
||||
return this.externalDetails.map(row => {
|
||||
const snapshot = { ...row };
|
||||
delete snapshot._sourceRows;
|
||||
delete snapshot._externalKey;
|
||||
delete snapshot.feeItems;
|
||||
snapshot.feeItemsJson = row.feeItemsJson || JSON.stringify(row.feeItems || {});
|
||||
snapshot.actualDepartureTime = this.normalizeExternalDateTime(row.actualDepartureTime);
|
||||
snapshot.actualCompletionTime = this.normalizeExternalDateTime(row.actualCompletionTime);
|
||||
return snapshot;
|
||||
});
|
||||
},
|
||||
async handleMatch() {
|
||||
if (!this.currentId) return this.$message.warning('请先保存草稿后再匹配');
|
||||
this.actionLoading = true;
|
||||
try {
|
||||
await api.match(this.currentId);
|
||||
await this.loadDetail();
|
||||
const data = this.unwrapData(
|
||||
await api.matchPreview({
|
||||
id: this.currentId,
|
||||
reconciliationMode: this.form.reconciliationMode,
|
||||
internalDetails: this.serializeInternalRows(),
|
||||
externalDetails: this.serializeExternalRows(),
|
||||
})
|
||||
);
|
||||
this.form = { ...this.form, ...data };
|
||||
this.internalDetails = (data.internalDetails || []).map(row => this.normalizeInternalRow(row));
|
||||
this.externalDetails = (data.externalDetails || []).map((row, index) =>
|
||||
this.normalizeExternalRow(row, index)
|
||||
);
|
||||
this.matchingStarted = true;
|
||||
this.$message.success('匹配完成');
|
||||
} finally {
|
||||
@@ -944,8 +1184,6 @@ export default {
|
||||
await this.$confirm('将以外部账单匹配金额更新内部结算明细,是否继续?', '更新账单', {
|
||||
type: 'warning',
|
||||
});
|
||||
const savedId = await this.handleSave(true);
|
||||
if (!savedId) return;
|
||||
this.actionLoading = true;
|
||||
try {
|
||||
await api.updateByMatch(this.currentId);
|
||||
@@ -963,22 +1201,68 @@ export default {
|
||||
) {
|
||||
return this.$message.warning('差异单数、差异货量和差异金额必须全部为0才可完成对账');
|
||||
}
|
||||
await this.$confirm('完成后对账单不可修改和删除,是否继续?', '完成对账', {
|
||||
type: 'warning',
|
||||
});
|
||||
const savedId = await this.handleSave(true);
|
||||
if (!savedId) return;
|
||||
this.actionLoading = true;
|
||||
this.completionDialog.visible = true;
|
||||
},
|
||||
async confirmCompletion() {
|
||||
if (this.completionDialog.loading) return;
|
||||
this.completionDialog.loading = true;
|
||||
try {
|
||||
await api.complete(this.currentId);
|
||||
await this.loadDetail();
|
||||
this.actionLoading = true;
|
||||
const data = this.unwrapData(
|
||||
await api.completeWithData({
|
||||
id: this.currentId,
|
||||
reconciliationDate: this.form.reconciliationDate,
|
||||
remark: this.form.remark,
|
||||
internalDetails: this.serializeInternalRows(),
|
||||
externalDetails: this.serializeExternalRows(),
|
||||
})
|
||||
);
|
||||
this.form = { ...this.form, ...data };
|
||||
this.internalDetails = (data.internalDetails || []).map(row => this.normalizeInternalRow(row));
|
||||
this.externalDetails = (data.externalDetails || []).map((row, index) =>
|
||||
this.normalizeExternalRow(row, index)
|
||||
);
|
||||
this.$message.success('对账完成');
|
||||
this.completionDialog.visible = false;
|
||||
this.visible = false;
|
||||
this.$emit('success', this.currentId);
|
||||
} finally {
|
||||
this.completionDialog.loading = false;
|
||||
this.actionLoading = false;
|
||||
}
|
||||
},
|
||||
openFeeAdjustment(type) {
|
||||
this.$message.info(`${type}请在结算明细调整中维护`);
|
||||
},
|
||||
sumFeeAmount(rows, name) {
|
||||
return rows.reduce((sum, row) => {
|
||||
const feeItems = row.feeItems || this.parseFeeItems(row.feeItemsJson);
|
||||
const fallback = name === '运费' ? row.freightAmount ?? row.settlementAmount : 0;
|
||||
return sum + Number(feeItems[name] ?? fallback ?? 0);
|
||||
}, 0);
|
||||
},
|
||||
normalizeCompletionFee(item, index) {
|
||||
const originalAmount = Number(
|
||||
item.originalAmount ?? item.original ?? item.originalSettlementAmount ?? 0
|
||||
);
|
||||
const afterAmount = Number(
|
||||
item.afterAmount ?? item.adjustedAmount ?? item.settlementAmount ?? item.totalAmount ?? originalAmount
|
||||
);
|
||||
return {
|
||||
name: item.name || item.feeItem || item.feeItemName || `费用${index + 1}`,
|
||||
originalAmount,
|
||||
adjustAmount: Number(item.adjustAmount ?? item.adjustmentAmount ?? afterAmount - originalAmount),
|
||||
afterAmount,
|
||||
remark: item.remark || item.adjustRemark || '',
|
||||
_index: index,
|
||||
};
|
||||
},
|
||||
formatCompletionMoney(value) {
|
||||
return Number(value || 0).toLocaleString('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
},
|
||||
chooseImport() {
|
||||
this.$refs.fileInput?.click();
|
||||
},
|
||||
@@ -987,10 +1271,8 @@ export default {
|
||||
event.target.value = '';
|
||||
if (!file) return;
|
||||
try {
|
||||
if (!this.currentId) {
|
||||
const savedId = await this.handleSave(true);
|
||||
if (!savedId) return;
|
||||
}
|
||||
const savedId = await this.handleSave(true);
|
||||
if (!savedId) return;
|
||||
const response =
|
||||
this.form.reconciliationMode === 'cargo'
|
||||
? await api.importCargo(this.currentId, file)
|
||||
@@ -1039,23 +1321,96 @@ export default {
|
||||
if (!this.adjustDialog.reason.trim()) return this.$message.warning('请输入调整原因');
|
||||
this.adjustDialog.saving = true;
|
||||
try {
|
||||
for (const row of this.adjustDialog.rows)
|
||||
await api.adjust({
|
||||
const rowsById = new Map(this.adjustDialog.rows.map(row => [row.id, row]));
|
||||
this.internalDetails = this.internalDetails.map(row => {
|
||||
const adjusted = rowsById.get(row.id);
|
||||
if (!adjusted) return row;
|
||||
return {
|
||||
...row,
|
||||
reconciliationId: this.currentId,
|
||||
updateMessage: this.adjustDialog.reason,
|
||||
});
|
||||
transportQuantity: adjusted.transportQuantity,
|
||||
unitPrice: adjusted.unitPrice,
|
||||
mileage: adjusted.mileage,
|
||||
freightAmount: adjusted.freightAmount,
|
||||
feeItems: adjusted.feeItems || this.parseFeeItems(adjusted.feeItemsJson),
|
||||
feeItemsJson: JSON.stringify(adjusted.feeItems || this.parseFeeItems(adjusted.feeItemsJson)),
|
||||
settlementAmount: adjusted.settlementAmount,
|
||||
updateResult: 'manually_adjusted',
|
||||
updateMessage: this.adjustDialog.reason.trim(),
|
||||
};
|
||||
});
|
||||
this.adjustDialog.visible = false;
|
||||
await this.loadDetail();
|
||||
this.$message.success('调整保存成功');
|
||||
this.refreshLocalStats();
|
||||
this.$message.success('调整已暂存');
|
||||
} finally {
|
||||
this.adjustDialog.saving = false;
|
||||
}
|
||||
},
|
||||
async handleUnmatch(row) {
|
||||
const matchedRows = (row._sourceRows || [row]).filter(item => item.matchResult === 'matched');
|
||||
for (const item of matchedRows) await api.unmatch(item.id);
|
||||
await this.loadDetail();
|
||||
matchedRows.forEach(item => {
|
||||
const external = this.externalDetails.find(candidate => candidate.id === item.matchedExternalId);
|
||||
if (external) {
|
||||
external.matchedInternalId = null;
|
||||
external.matchStatus = 'unmatched';
|
||||
external.suspectedDuplicate = false;
|
||||
}
|
||||
item.matchedExternalId = null;
|
||||
item.matchedExternalLineNo = null;
|
||||
item.matchResult = 'unmatched';
|
||||
});
|
||||
this.refreshLocalStats();
|
||||
this.$message.success('取消匹配已暂存');
|
||||
},
|
||||
hasMatchedExternal(row) {
|
||||
return row.matchedExternalLineNo !== null && row.matchedExternalLineNo !== undefined && row.matchedExternalLineNo !== '';
|
||||
},
|
||||
getInternalMatchTarget(row) {
|
||||
const sourceRows = row._sourceRows || [row];
|
||||
return sourceRows.find(item => item.id && item.matchResult !== 'matched') || sourceRows.find(item => item.id);
|
||||
},
|
||||
openExternalMatchDialog(row) {
|
||||
if (!this.editable) return;
|
||||
if (!this.getInternalMatchTarget(row)) return this.$message.warning('当前内部账单明细无法匹配');
|
||||
this.manualMatchDialog.row = row;
|
||||
this.manualMatchDialog.visible = true;
|
||||
},
|
||||
async handleManualMatch(externalRow) {
|
||||
const internalRow = this.getInternalMatchTarget(this.manualMatchDialog.row);
|
||||
if (!internalRow) return this.$message.warning('当前内部账单明细无法匹配');
|
||||
this.manualMatchDialog.loading = true;
|
||||
try {
|
||||
const internal = this.internalDetails.find(item => item.id === internalRow.id);
|
||||
const external = this.externalDetails.find(item => item.id === externalRow.id);
|
||||
if (!internal || !external) return this.$message.warning('匹配明细不存在');
|
||||
if (external.matchedInternalId && external.matchedInternalId !== internal.id) {
|
||||
const oldInternal = this.internalDetails.find(item => item.id === external.matchedInternalId);
|
||||
if (oldInternal) {
|
||||
oldInternal.matchedExternalId = null;
|
||||
oldInternal.matchedExternalLineNo = null;
|
||||
oldInternal.matchResult = 'unmatched';
|
||||
}
|
||||
}
|
||||
if (internal.matchedExternalId && internal.matchedExternalId !== external.id) {
|
||||
const oldExternal = this.externalDetails.find(item => item.id === internal.matchedExternalId);
|
||||
if (oldExternal) {
|
||||
oldExternal.matchedInternalId = null;
|
||||
oldExternal.matchStatus = 'unmatched';
|
||||
oldExternal.suspectedDuplicate = false;
|
||||
}
|
||||
}
|
||||
internal.matchedExternalId = external.id;
|
||||
internal.matchedExternalLineNo = external.externalLineNo;
|
||||
internal.matchResult = 'matched';
|
||||
external.matchedInternalId = internal.id;
|
||||
external.matchStatus = 'matched';
|
||||
external.suspectedDuplicate = false;
|
||||
this.manualMatchDialog.visible = false;
|
||||
this.matchingStarted = true;
|
||||
this.refreshLocalStats();
|
||||
this.$message.success('匹配成功');
|
||||
} finally {
|
||||
this.manualMatchDialog.loading = false;
|
||||
}
|
||||
},
|
||||
internalRowClassName({ row }) {
|
||||
return this.matchingStarted && row.matchResult !== 'matched'
|
||||
@@ -1101,7 +1456,7 @@ export default {
|
||||
this.externalEditing = editing;
|
||||
this.externalEditSnapshots = snapshots;
|
||||
},
|
||||
finishExternalAdjust(row) {
|
||||
async finishExternalAdjust(row) {
|
||||
row.feeItemsJson = JSON.stringify(row.feeItems || {});
|
||||
const key = this.externalRowKey(row);
|
||||
const editing = { ...this.externalEditing };
|
||||
@@ -1110,9 +1465,23 @@ export default {
|
||||
delete snapshots[key];
|
||||
this.externalEditing = editing;
|
||||
this.externalEditSnapshots = snapshots;
|
||||
this.refreshExternalStats();
|
||||
this.refreshLocalStats();
|
||||
this.$message.success('调整已暂存');
|
||||
},
|
||||
refreshExternalStats() {
|
||||
normalizeExternalDateTime(value) {
|
||||
if (!value) return null;
|
||||
const text = String(value);
|
||||
return text.length === 10 ? `${text}T00:00:00` : text;
|
||||
},
|
||||
refreshLocalStats() {
|
||||
const internalQuantity = this.internalDetails.reduce(
|
||||
(total, item) => total + Number(item.transportQuantity || 0),
|
||||
0
|
||||
);
|
||||
const internalAmount = this.internalDetails.reduce(
|
||||
(total, item) => total + Number(item.settlementAmount || 0),
|
||||
0
|
||||
);
|
||||
const externalQuantity = this.externalDetails.reduce(
|
||||
(total, item) => total + Number(item.transportQuantity || 0),
|
||||
0
|
||||
@@ -1121,18 +1490,37 @@ export default {
|
||||
(total, item) => total + Number(item.settlementAmount || 0),
|
||||
0
|
||||
);
|
||||
const matchedCount = this.externalDetails.filter(item => item.matchStatus === 'matched').length;
|
||||
const matchedCount = this.internalDetails.filter(item => item.matchResult === 'matched').length;
|
||||
const internalUnmatched = this.internalDetails.filter(item => item.matchResult !== 'matched').length;
|
||||
const externalUnmatched = this.externalDetails.filter(item => item.matchStatus !== 'matched').length;
|
||||
this.form = {
|
||||
...this.form,
|
||||
internalBillCount: this.internalDetails.length,
|
||||
externalBillCount: this.externalDetails.length,
|
||||
internalQuantity,
|
||||
internalAmount,
|
||||
externalQuantity,
|
||||
externalAmount,
|
||||
differenceQuantity: Math.abs(Number(this.form.internalQuantity || 0) - externalQuantity),
|
||||
differenceAmount: Math.abs(Number(this.form.internalAmount || 0) - externalAmount),
|
||||
differenceCount:
|
||||
Math.abs(this.internalDetails.length - this.externalDetails.length) +
|
||||
Math.min(internalUnmatched, externalUnmatched),
|
||||
differenceQuantity: Math.abs(internalQuantity - externalQuantity),
|
||||
differenceAmount: Math.abs(internalAmount - externalAmount),
|
||||
matchedCount,
|
||||
unmatchedCount: Math.max(Number(this.form.internalBillCount || 0), this.externalDetails.length) - matchedCount,
|
||||
unmatchedCount: internalUnmatched + externalUnmatched,
|
||||
matchStatus:
|
||||
this.internalDetails.length > 0 &&
|
||||
this.internalDetails.length === this.externalDetails.length &&
|
||||
matchedCount === this.internalDetails.length
|
||||
? 'matched'
|
||||
: matchedCount > 0
|
||||
? 'partial'
|
||||
: 'unmatched',
|
||||
};
|
||||
},
|
||||
refreshExternalStats() {
|
||||
this.refreshLocalStats();
|
||||
},
|
||||
matchName(value) {
|
||||
return (
|
||||
{
|
||||
@@ -1303,6 +1691,99 @@ export default {
|
||||
.dialog-form {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.completion-dialog__content {
|
||||
padding: 4px 8px 0;
|
||||
}
|
||||
.completion-dialog__section {
|
||||
padding: 20px 24px;
|
||||
border: 1px solid #eff1f7;
|
||||
background: #fff;
|
||||
}
|
||||
.completion-dialog__section + .completion-dialog__section {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.completion-dialog__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 18px 32px;
|
||||
}
|
||||
.completion-dialog__field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.completion-dialog__field--remark {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.completion-dialog__label {
|
||||
flex: 0 0 auto;
|
||||
min-width: 88px;
|
||||
margin-right: 12px;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.completion-dialog__value {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
min-height: 36px;
|
||||
padding: 8px 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
background: #f5f7fa;
|
||||
color: #606266;
|
||||
line-height: 18px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.completion-dialog__field--remark .completion-dialog__value {
|
||||
white-space: normal;
|
||||
}
|
||||
.completion-dialog__section--fees {
|
||||
position: relative;
|
||||
padding-top: 48px;
|
||||
}
|
||||
.completion-dialog__section-title {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #303133;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.completion-dialog__section-title::before {
|
||||
width: 4px;
|
||||
height: 22px;
|
||||
margin-right: 8px;
|
||||
background: #409eff;
|
||||
content: '';
|
||||
}
|
||||
.completion-dialog__fee-actions {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 24px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.completion-dialog__fee-actions .el-button + .el-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
.completion-dialog__negative {
|
||||
color: #f56c6c;
|
||||
}
|
||||
.completion-dialog__footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.completion-dialog__footer .el-button + .el-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
.hidden-file {
|
||||
display: none;
|
||||
}
|
||||
@@ -1336,5 +1817,26 @@ export default {
|
||||
.reconciliation-editor__filter {
|
||||
grid-template-columns: repeat(2, minmax(160px, 1fr));
|
||||
}
|
||||
.completion-dialog__grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@media (max-width: 680px) {
|
||||
.completion-dialog__grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
.completion-dialog__field--remark {
|
||||
grid-column: auto;
|
||||
}
|
||||
.completion-dialog__section {
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
.completion-dialog__section-title {
|
||||
left: 12px;
|
||||
}
|
||||
.completion-dialog__fee-actions {
|
||||
right: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1694,8 +1694,8 @@ export default {
|
||||
return;
|
||||
}
|
||||
this.$router.push({
|
||||
path: '/business/waybill-manage',
|
||||
query: { detailId: row.waybillId },
|
||||
path: '/business/waybill-manage/detail',
|
||||
query: { id: row.waybillId },
|
||||
});
|
||||
},
|
||||
async openDetailDialog(row) {
|
||||
|
||||
Reference in New Issue
Block a user