Merge branch 'master' into feature/20261019-bfi-all-web
# Conflicts: # src/views/payment/invoice-application.vue # src/views/payment/invoice-receipt.vue # src/views/payment/payment-application.vue # src/views/settlement/formal-settlement.vue # src/views/settlement/pre-settlement.vue
This commit is contained in:
@@ -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,
|
||||
@@ -1373,16 +1388,12 @@ export default {
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
summaryTotal(value) {
|
||||
this.form.settlementAmount = Number(value || 0).toFixed(2);
|
||||
this.form.localSettlementAmount = (
|
||||
Number(value || 0) * Number(this.form.exchangeRate || 1)
|
||||
).toFixed(2);
|
||||
summaryTotal() {
|
||||
this.syncSettlementAmountsFromSummary();
|
||||
},
|
||||
'form.exchangeRate'(value) {
|
||||
this.form.localSettlementAmount = (
|
||||
Number(this.summaryTotal || 0) * Number(value || 1)
|
||||
).toFixed(2);
|
||||
'form.exchangeRate'() {
|
||||
if (this._skipExchangeRateRebuild) return;
|
||||
this.buildSummaryFeesFromDetails();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -1438,7 +1449,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 +1489,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 +1513,7 @@ export default {
|
||||
this.loadFeeOptions(),
|
||||
this.loadFeeCategoryOptions(),
|
||||
this.loadAttachmentTypeOptions(),
|
||||
this.loadBusinessModelOptions(),
|
||||
this.loadTransportTypeOptions(),
|
||||
]);
|
||||
}
|
||||
@@ -1664,8 +1679,9 @@ export default {
|
||||
current.settlementAmount = current.originalAmount;
|
||||
summaryMap.set(key, current);
|
||||
});
|
||||
// 先保留预结算手工费用行,再按明细本位币 + 结算汇率重算系统归集行
|
||||
this.summaryFees = [...summaryMap.values()];
|
||||
if (!this.summaryFees.length) await this.refreshSummaryFees();
|
||||
await this.refreshSummaryFees();
|
||||
},
|
||||
async loadAllContracts() {
|
||||
const response = await getContractOptions('');
|
||||
@@ -1767,13 +1783,19 @@ export default {
|
||||
},
|
||||
async fetchBfiExchangeRate() {
|
||||
const currency = this.form.currency;
|
||||
if (!currency || currency === 'RMB') return;
|
||||
if (!currency || currency === 'RMB') {
|
||||
this.buildSummaryFeesFromDetails();
|
||||
return;
|
||||
}
|
||||
if (!this.form.exchangeRateDate) return;
|
||||
try {
|
||||
const { data } = await getBfiExchangeRate(currency, this.form.exchangeRateDate);
|
||||
const rateData = data?.data;
|
||||
if (rateData?.excval != null) {
|
||||
this._skipExchangeRateRebuild = true;
|
||||
this.form.exchangeRate = Number(rateData.excval);
|
||||
this._skipExchangeRateRebuild = false;
|
||||
this.buildSummaryFeesFromDetails();
|
||||
} else {
|
||||
this.$message.warning(`未查询到 ${currency} 的BFI汇率数据`);
|
||||
}
|
||||
@@ -1781,6 +1803,37 @@ export default {
|
||||
this.$message.warning('查询BFI汇率失败,请手动输入结算汇率');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 明细金额按本位币理解;外币结算时结算合计原金额/结算金额 = 本位币金额 / 结算汇率。
|
||||
*/
|
||||
convertLocalToSettlementAmount(localAmount) {
|
||||
const amount = Number(localAmount || 0);
|
||||
if (!Number.isFinite(amount)) return 0;
|
||||
if (this.isLocalCurrencySettlement) {
|
||||
return Number(amount.toFixed(2));
|
||||
}
|
||||
const rate = Number(this.form.exchangeRate || 0);
|
||||
if (rate <= 0) return 0;
|
||||
return Number((amount / rate).toFixed(2));
|
||||
},
|
||||
/**
|
||||
* 结算金额取结算合计原币汇总值;本位币合计 = 结算金额 × 结算汇率。
|
||||
*/
|
||||
syncSettlementAmountsFromSummary() {
|
||||
const settlementTotal = this.summaryFees.reduce(
|
||||
(total, row) => total + Number(row.settlementAmount || 0),
|
||||
0
|
||||
);
|
||||
this.form.settlementAmount = Number(settlementTotal || 0).toFixed(2);
|
||||
if (this.isLocalCurrencySettlement) {
|
||||
this.form.localSettlementAmount = this.form.settlementAmount;
|
||||
this.form.exchangeRate = 1;
|
||||
return;
|
||||
}
|
||||
const rate = Number(this.form.exchangeRate || 0);
|
||||
this.form.localSettlementAmount =
|
||||
rate > 0 ? (Number(this.form.settlementAmount || 0) * rate).toFixed(2) : '0.00';
|
||||
},
|
||||
openCandidateDialog() {
|
||||
this.candidate.visible = true;
|
||||
this.candidate.page.current = 1;
|
||||
@@ -2002,8 +2055,9 @@ export default {
|
||||
.map(row => [`${row.feeType}\u0000${row.feeItem}`, row])
|
||||
);
|
||||
const append = (feeItem, originalValue, settlementValue, fallbackFeeType = '') => {
|
||||
const originalAmount = Number(originalValue || 0);
|
||||
const settlementAmount = Number(settlementValue || 0);
|
||||
// 明细费用按本位币归集,外币结算时再按汇率换算成结算原币金额
|
||||
const originalAmount = this.convertLocalToSettlementAmount(originalValue);
|
||||
const settlementAmount = this.convertLocalToSettlementAmount(settlementValue);
|
||||
if (!feeItem || !Number.isFinite(originalAmount) || !Number.isFinite(settlementAmount)) {
|
||||
return;
|
||||
}
|
||||
@@ -2041,6 +2095,7 @@ export default {
|
||||
});
|
||||
const manualRows = this.summaryFees.filter(row => Number(row.manualFlag) === 1);
|
||||
this.summaryFees = [...generatedMap.values(), ...manualRows];
|
||||
this.syncSettlementAmountsFromSummary();
|
||||
},
|
||||
// 每条明细对应的费用行:[调整前基准行, 调整后当前行],缺少费用快照时退化为明细自身的汇总行。
|
||||
resolveDetailFeePairs(detail) {
|
||||
@@ -2424,6 +2479,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 +2852,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 +2944,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 +3022,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 +3387,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: '',
|
||||
},
|
||||
@@ -1312,9 +1332,12 @@ export default {
|
||||
if (value) this.initialize();
|
||||
},
|
||||
},
|
||||
summaryTotal(value) {
|
||||
this.form.settlementAmount = Number(value || 0).toFixed(2);
|
||||
this.recalculateLocalAmount();
|
||||
summaryTotal() {
|
||||
this.syncSettlementAmountsFromSummary();
|
||||
},
|
||||
'form.exchangeRate'() {
|
||||
if (this._skipExchangeRateRebuild) return;
|
||||
this.buildSummaryFeesFromDetails();
|
||||
},
|
||||
filteredDetails: {
|
||||
handler(list) {
|
||||
@@ -1330,7 +1353,6 @@ export default {
|
||||
methods: {
|
||||
isTierUnitPrice,
|
||||
isUnitPriceAdjustable,
|
||||
formatContractOptionLabel,
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission?.[code], false);
|
||||
},
|
||||
@@ -1344,6 +1366,7 @@ export default {
|
||||
if (!this.publicMode) {
|
||||
await this.loadFeeOptions();
|
||||
await this.loadFeeCategoryOptions();
|
||||
await this.loadBusinessModelOptions();
|
||||
await this.loadTransportTypeOptions();
|
||||
await this.loadAttachmentTypeOptions();
|
||||
}
|
||||
@@ -1485,6 +1508,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 +1547,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 +1671,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 +1687,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 +1788,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 || []),
|
||||
@@ -1844,8 +1893,9 @@ export default {
|
||||
.map(row => [`${row.feeType}\u0000${row.feeItem}`, row])
|
||||
);
|
||||
const appendSummary = (feeItem, originalValue, settlementValue, fallbackFeeType = '') => {
|
||||
const originalAmount = Number(originalValue || 0);
|
||||
const settlementAmount = Number(settlementValue || 0);
|
||||
// 明细费用按本位币归集,外币结算时再按汇率换算成结算原币金额
|
||||
const originalAmount = this.convertLocalToSettlementAmount(originalValue);
|
||||
const settlementAmount = this.convertLocalToSettlementAmount(settlementValue);
|
||||
if (!feeItem || !Number.isFinite(originalAmount) || !Number.isFinite(settlementAmount)) {
|
||||
return;
|
||||
}
|
||||
@@ -1900,7 +1950,7 @@ export default {
|
||||
});
|
||||
nextFeeMap.forEach(row => orderedFees.push(row));
|
||||
this.summaryFees = orderedFees;
|
||||
this.recalculateLocalAmount();
|
||||
this.syncSettlementAmountsFromSummary();
|
||||
},
|
||||
// 每条明细对应的费用行:[调整前基准行, 调整后当前行],缺少来源费用快照时退化为明细自身的汇总行。
|
||||
resolveDetailFeePairs(detail) {
|
||||
@@ -1988,17 +2038,21 @@ export default {
|
||||
},
|
||||
handleExchangeRateDateChange() {
|
||||
this.fetchBfiExchangeRate();
|
||||
this.recalculateLocalAmount();
|
||||
},
|
||||
async fetchBfiExchangeRate() {
|
||||
if (!this.form.currency || this.form.currency === 'RMB') return;
|
||||
if (!this.form.currency || this.form.currency === 'RMB') {
|
||||
this.buildSummaryFeesFromDetails();
|
||||
return;
|
||||
}
|
||||
if (!this.form.exchangeRateDate) return;
|
||||
try {
|
||||
const { data } = await getBfiExchangeRate(this.form.currency, this.form.exchangeRateDate);
|
||||
const rateData = data?.data;
|
||||
if (rateData?.excval != null) {
|
||||
this._skipExchangeRateRebuild = true;
|
||||
this.form.exchangeRate = Number(rateData.excval);
|
||||
this.recalculateLocalAmount();
|
||||
this._skipExchangeRateRebuild = false;
|
||||
this.buildSummaryFeesFromDetails();
|
||||
} else {
|
||||
this.$message.warning(`未查询到 ${this.form.currency} 的BFI汇率数据`);
|
||||
}
|
||||
@@ -2006,9 +2060,39 @@ export default {
|
||||
this.$message.warning('查询BFI汇率失败,请手动输入结算汇率');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 明细金额按本位币理解;外币结算时结算合计原金额/结算金额 = 本位币金额 / 结算汇率。
|
||||
*/
|
||||
convertLocalToSettlementAmount(localAmount) {
|
||||
const amount = Number(localAmount || 0);
|
||||
if (!Number.isFinite(amount)) return 0;
|
||||
if (this.isLocalCurrencySettlement) {
|
||||
return Number(amount.toFixed(2));
|
||||
}
|
||||
const rate = Number(this.form.exchangeRate || 0);
|
||||
if (rate <= 0) return 0;
|
||||
return Number((amount / rate).toFixed(2));
|
||||
},
|
||||
/**
|
||||
* 结算金额取结算合计原币汇总值;本位币合计 = 结算金额 × 结算汇率。
|
||||
*/
|
||||
syncSettlementAmountsFromSummary() {
|
||||
const settlementTotal = this.summaryFees.reduce(
|
||||
(total, row) => total + Number(row.settlementAmount || 0),
|
||||
0
|
||||
);
|
||||
this.form.settlementAmount = Number(settlementTotal || 0).toFixed(2);
|
||||
if (this.isLocalCurrencySettlement) {
|
||||
this.form.localSettlementAmount = this.form.settlementAmount;
|
||||
this.form.exchangeRate = 1;
|
||||
return;
|
||||
}
|
||||
const rate = Number(this.form.exchangeRate || 0);
|
||||
this.form.localSettlementAmount =
|
||||
rate > 0 ? (Number(this.form.settlementAmount || 0) * rate).toFixed(2) : '0.00';
|
||||
},
|
||||
recalculateLocalAmount() {
|
||||
const rate = this.form.currency === 'RMB' ? 1 : Number(this.form.exchangeRate || 0);
|
||||
this.form.localSettlementAmount = (Number(this.form.settlementAmount || 0) * rate).toFixed(2);
|
||||
this.buildSummaryFeesFromDetails();
|
||||
},
|
||||
openCandidateDialog() {
|
||||
if (!this.form.contractId) {
|
||||
@@ -2251,17 +2335,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 +2366,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 +2643,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 +2659,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 +2686,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 +2702,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 } })),
|
||||
|
||||
@@ -380,7 +380,13 @@ export default {
|
||||
},
|
||||
async loadFormalSettlements(keyword = '') {
|
||||
const { data } = await api.getFormalSettlements(keyword);
|
||||
this.candidates = data?.data || data || [];
|
||||
const payload = data?.data ?? data;
|
||||
// 兼容误返回分页对象的情况,只取正式结算单列表
|
||||
this.candidates = Array.isArray(payload)
|
||||
? payload
|
||||
: Array.isArray(payload?.records)
|
||||
? payload.records
|
||||
: [];
|
||||
},
|
||||
async loadFeeOptions() {
|
||||
const { data } = await api.getFeeOptions();
|
||||
|
||||
@@ -926,14 +926,34 @@ const stripInternalRowFields = row => {
|
||||
};
|
||||
};
|
||||
|
||||
/** 合并费用列名与各行 feeItems 键,避免保存/回显时丢动态费用列 */
|
||||
const collectFeeItemNames = (preferred = [], detailRows = []) => {
|
||||
const names = [];
|
||||
const seen = new Set();
|
||||
const pushName = value => {
|
||||
const name = String(value || '').trim();
|
||||
if (!name || seen.has(name)) return;
|
||||
seen.add(name);
|
||||
names.push(name);
|
||||
};
|
||||
(preferred || []).forEach(pushName);
|
||||
(detailRows || []).forEach(row => {
|
||||
const feeItems = row?.feeItems;
|
||||
if (!feeItems || typeof feeItems !== 'object' || Array.isArray(feeItems)) return;
|
||||
Object.keys(feeItems).forEach(pushName);
|
||||
});
|
||||
return names;
|
||||
};
|
||||
|
||||
const buildPayload = () => {
|
||||
const { file, ...batch } = form;
|
||||
const payloadRows = rows.value.map(stripInternalRowFields);
|
||||
return {
|
||||
...batch,
|
||||
settlementBillType: form.settlementType === 'payable' ? form.settlementBillType || 'formal' : 'formal',
|
||||
waybillMode: form.waybillMode === 'history' ? 'history' : 'new',
|
||||
feeItemNames: [...feeItemNames.value],
|
||||
rows: rows.value.map(stripInternalRowFields),
|
||||
feeItemNames: collectFeeItemNames(feeItemNames.value, payloadRows),
|
||||
rows: payloadRows,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1436,9 +1456,13 @@ const loadDetail = async id => {
|
||||
if (detail.projectId) {
|
||||
await loadContractsByProject(detail.projectId, detail.projectName || '');
|
||||
}
|
||||
feeItemNames.value = Array.isArray(detail.feeItemNames) ? detail.feeItemNames : [];
|
||||
parsedHeaders.value = [];
|
||||
const detailRows = Array.isArray(detail.rows) ? detail.rows : [];
|
||||
// 先按库中列名与明细 feeItems 合并,再规范化行数据,保证草稿再打开仍展示全部费用列
|
||||
feeItemNames.value = collectFeeItemNames(
|
||||
Array.isArray(detail.feeItemNames) ? detail.feeItemNames : [],
|
||||
detailRows
|
||||
);
|
||||
rows.value = detailRows.map((item, index) => normalizeParsedRow(item, index));
|
||||
refreshDuplicateFlags();
|
||||
// 查看模式始终只读;编辑时仅草稿可改,已完成/已作废强制只读
|
||||
|
||||
@@ -125,15 +125,17 @@
|
||||
<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>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="!filteredInternalRows.length"
|
||||
@click="exportInternalDetails"
|
||||
>导出</el-button
|
||||
>
|
||||
<div class="reconciliation-editor__filter-actions">
|
||||
<el-button @click="resetInternalQuery">重置</el-button>
|
||||
<el-button type="primary" @click="internalFilterTick++">查询</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="!filteredInternalRows.length"
|
||||
@click="exportInternalDetails"
|
||||
>导出</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<el-table
|
||||
:data="filteredInternalRows"
|
||||
@@ -189,6 +191,9 @@
|
||||
"
|
||||
>{{ formatDate(row[column.prop]) }}</span
|
||||
>
|
||||
<span v-else-if="column.prop === 'unitPrice'">{{
|
||||
formatOptionalMoney(row.unitPrice)
|
||||
}}</span>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
@@ -263,7 +268,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 +276,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 +285,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 +293,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 +305,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) }}
|
||||
@@ -318,6 +323,9 @@
|
||||
"
|
||||
>{{ formatDate(row[column.prop]) }}</span
|
||||
>
|
||||
<span v-else-if="column.prop === 'unitPrice'">{{
|
||||
formatOptionalMoney(row.unitPrice)
|
||||
}}</span>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
@@ -476,6 +484,7 @@
|
||||
<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.prop === 'unitPrice'">{{ formatOptionalMoney(row.unitPrice) }}</span>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
@@ -630,6 +639,10 @@ import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { Search } from '@element-plus/icons-vue';
|
||||
import { h } from 'vue';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import {
|
||||
consumeImportResponse,
|
||||
resolveImportErrorMessage,
|
||||
} from '@/utils/import-excel';
|
||||
import { transportReconciliationFormFields } from '@/option/settlement/transportReconciliationForm';
|
||||
import {
|
||||
internalColumns,
|
||||
@@ -676,6 +689,8 @@ export default {
|
||||
completionDialog: { visible: false, loading: false },
|
||||
completionSummaryRowsData: null,
|
||||
externalEditing: {},
|
||||
// 调整中的草稿,输入只改草稿,点「完成」才写回列表
|
||||
externalEditDrafts: {},
|
||||
externalEditSnapshots: {},
|
||||
matchingStarted: false,
|
||||
rules: {
|
||||
@@ -932,6 +947,7 @@ export default {
|
||||
this.internalDetails = [];
|
||||
this.externalDetails = [];
|
||||
this.externalEditing = {};
|
||||
this.externalEditDrafts = {};
|
||||
this.externalEditSnapshots = {};
|
||||
this.completionSummaryRowsData = null;
|
||||
this.matchingStarted = false;
|
||||
@@ -1038,12 +1054,24 @@ export default {
|
||||
.map(normalized)
|
||||
.join('|');
|
||||
},
|
||||
blankOptionalNumber(value) {
|
||||
if (value === null || value === undefined || value === '' || Number(value) === -1) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
normalizeInternalRow(row) {
|
||||
const feeItems =
|
||||
row.feeItems && typeof row.feeItems === 'object' && Object.keys(row.feeItems).length
|
||||
? row.feeItems
|
||||
: this.parseFeeItems(row.feeItemsJson);
|
||||
return { ...row, feeItems };
|
||||
return {
|
||||
...row,
|
||||
feeItems,
|
||||
unitPrice: this.blankOptionalNumber(row.unitPrice),
|
||||
mileage: this.blankOptionalNumber(row.mileage),
|
||||
freightAmount: this.blankOptionalNumber(row.freightAmount),
|
||||
};
|
||||
},
|
||||
normalizeExternalRow(row, index) {
|
||||
const feeItems =
|
||||
@@ -1053,6 +1081,9 @@ export default {
|
||||
return {
|
||||
...row,
|
||||
feeItems,
|
||||
unitPrice: this.blankOptionalNumber(row.unitPrice),
|
||||
mileage: this.blankOptionalNumber(row.mileage),
|
||||
freightAmount: this.blankOptionalNumber(row.freightAmount),
|
||||
_externalKey: row.id || row.externalLineNo || `external-${index}`,
|
||||
};
|
||||
},
|
||||
@@ -1280,6 +1311,9 @@ export default {
|
||||
snapshot.feeItemsJson = row.feeItemsJson || JSON.stringify(row.feeItems || {});
|
||||
snapshot.actualDepartureTime = this.normalizeExternalDateTime(row.actualDepartureTime);
|
||||
snapshot.actualCompletionTime = this.normalizeExternalDateTime(row.actualCompletionTime);
|
||||
snapshot.unitPrice = this.blankOptionalNumber(row.unitPrice);
|
||||
snapshot.mileage = this.blankOptionalNumber(row.mileage);
|
||||
snapshot.freightAmount = this.blankOptionalNumber(row.freightAmount);
|
||||
return snapshot;
|
||||
});
|
||||
},
|
||||
@@ -1293,6 +1327,9 @@ export default {
|
||||
snapshot.feeItemsJson = row.feeItemsJson || JSON.stringify(row.feeItems || {});
|
||||
snapshot.actualDepartureTime = this.normalizeExternalDateTime(row.actualDepartureTime);
|
||||
snapshot.actualCompletionTime = this.normalizeExternalDateTime(row.actualCompletionTime);
|
||||
snapshot.unitPrice = this.blankOptionalNumber(row.unitPrice);
|
||||
snapshot.mileage = this.blankOptionalNumber(row.mileage);
|
||||
snapshot.freightAmount = this.blankOptionalNumber(row.freightAmount);
|
||||
snapshot.suspectedDuplicate = duplicateIds.has(String(row.id));
|
||||
return snapshot;
|
||||
});
|
||||
@@ -1322,7 +1359,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async handleUpdate() {
|
||||
await this.$confirm('将以外部账单数据更新运单及内部结算明细,是否继续?', '更新账单', {
|
||||
await this.$confirm('将以外部账单数据更新内部结算明细,是否继续?', '更新账单', {
|
||||
type: 'warning',
|
||||
});
|
||||
this.actionLoading = true;
|
||||
@@ -1503,23 +1540,24 @@ 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')) {
|
||||
downloadXls(
|
||||
response.data,
|
||||
`运输对账导入失败明细${this.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.xlsx`
|
||||
);
|
||||
// 成功时 axios 拦截器可能已把 JSON Blob 解析成对象,不能再调 .text()
|
||||
const consumed = await consumeImportResponse(response, {
|
||||
businessName: '运输对账',
|
||||
dayjsFormat: this.$dayjs().format('YYYY-MM-DD HH-mm-ss'),
|
||||
});
|
||||
if (consumed.kind === 'excel') {
|
||||
downloadXls(consumed.blob, consumed.fileName);
|
||||
this.$message.warning('部分数据导入失败,已下载失败明细');
|
||||
} else {
|
||||
const result = JSON.parse(await response.data.text());
|
||||
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 || '外部账单导入失败');
|
||||
if (!error?._notified) {
|
||||
this.$message.error(await resolveImportErrorMessage(error));
|
||||
}
|
||||
}
|
||||
},
|
||||
async downloadTemplate(mode) {
|
||||
@@ -1661,6 +1699,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 +1732,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 +1799,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 &&
|
||||
@@ -1808,7 +1865,10 @@ export default {
|
||||
: Number(value || 0).toFixed(2);
|
||||
},
|
||||
formatMileage(value) {
|
||||
return Number(value) === -1 ? '' : this.displayValue(value);
|
||||
return this.blankOptionalNumber(value) === null ? '' : this.displayValue(value);
|
||||
},
|
||||
formatOptionalMoney(value, currency = 'RMB') {
|
||||
return this.blankOptionalNumber(value) === null ? '-' : this.formatMoney(value, currency);
|
||||
},
|
||||
formatDate(value) {
|
||||
if (value === null || value === undefined || value === '') return '-';
|
||||
@@ -1929,15 +1989,26 @@ export default {
|
||||
}
|
||||
.reconciliation-editor__filter {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(160px, 1fr)) auto auto;
|
||||
grid-template-columns: repeat(4, minmax(160px, 1fr)) auto;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
.reconciliation-editor__filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.reconciliation-editor__filter-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.reconciliation-editor__filter-actions .el-button {
|
||||
min-width: 64px;
|
||||
margin-left: 0;
|
||||
}
|
||||
.reconciliation-editor__filter-label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 8px;
|
||||
|
||||
@@ -140,6 +140,7 @@ export default {
|
||||
rows: [],
|
||||
selection: [],
|
||||
loading: false,
|
||||
approveLoading: false,
|
||||
syncLoading: false,
|
||||
page: { currentPage: 1, pageSize: 10, pageSizes: [10, 20, 50, 100], total: 0 },
|
||||
editor: { visible: false, id: null, readonly: false },
|
||||
@@ -371,15 +372,21 @@ export default {
|
||||
});
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await this.$confirm(
|
||||
`审核通过后${this.billLabel}生效,金额只能通过结算调整单处理,确认通过?`,
|
||||
'提示',
|
||||
{ type: 'warning' }
|
||||
);
|
||||
// 防止重复点击:请求期间表格遮罩,后续点击直接忽略
|
||||
if (this.loading) return;
|
||||
this.loading = true;
|
||||
// 防止重复点击:请求进行中忽略后续点击,避免重复审批和重复提示
|
||||
if (this.approveLoading) return;
|
||||
this.approveLoading = true;
|
||||
let loading = null;
|
||||
try {
|
||||
await this.$confirm(
|
||||
`审核通过后${this.billLabel}生效,金额只能通过结算调整单处理,确认通过?`,
|
||||
'提示',
|
||||
{ type: 'warning' }
|
||||
);
|
||||
loading = this.$loading({
|
||||
lock: true,
|
||||
text: '审批处理中,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
});
|
||||
const res = await api.approve({ id: row.id });
|
||||
// data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因
|
||||
if (res.data.data === false) {
|
||||
@@ -387,11 +394,11 @@ export default {
|
||||
} else {
|
||||
this.$message.success(res.data.msg || '审批通过');
|
||||
}
|
||||
await this.loadTable();
|
||||
this.onLoad(this.page);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
loading?.close();
|
||||
this.approveLoading = false;
|
||||
}
|
||||
this.onLoad(this.page);
|
||||
},
|
||||
async handleReturn(row) {
|
||||
const { value } = await this.$prompt('请输入驳回原因', '审批驳回', {
|
||||
|
||||
@@ -334,6 +334,7 @@ export default {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
loading: false,
|
||||
approveLoading: false,
|
||||
query: emptyQuery(),
|
||||
searchFields: preSettlementSearchFields,
|
||||
searchExpanded: false,
|
||||
@@ -691,16 +692,23 @@ export default {
|
||||
});
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await this.$confirm('确认审批通过该预付结算单?', '提示', { type: 'warning' });
|
||||
// 防止重复点击:请求期间表格遮罩,后续点击直接忽略
|
||||
if (this.loading) return;
|
||||
this.loading = true;
|
||||
// 防止重复点击:请求进行中忽略后续点击,避免重复审批和重复提示
|
||||
if (this.approveLoading) return;
|
||||
this.approveLoading = true;
|
||||
let loading = null;
|
||||
try {
|
||||
await this.$confirm('确认审批通过该预付结算单?', '提示', { type: 'warning' });
|
||||
loading = this.$loading({
|
||||
lock: true,
|
||||
text: '审批处理中,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
});
|
||||
await approve({ id: row.id });
|
||||
this.$message.success('审批通过');
|
||||
await this.loadTable();
|
||||
this.loadTable();
|
||||
} finally {
|
||||
this.loading = false;
|
||||
loading?.close();
|
||||
this.approveLoading = false;
|
||||
}
|
||||
},
|
||||
async handleReturn(row) {
|
||||
|
||||
@@ -6,15 +6,33 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, getCurrentInstance, nextTick, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import SettlementImportPage from './components/settlement-import-page.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
const instance = getCurrentInstance();
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
if (route.query.mode === 'view') return '查看导入结算单';
|
||||
return route.query.id ? '编辑导入结算单' : '新增导入结算单';
|
||||
});
|
||||
|
||||
const syncTagTitle = () => {
|
||||
nextTick(() => {
|
||||
store.commit('SET_TAG', {
|
||||
fullPath: route.fullPath,
|
||||
name: pageTitle.value,
|
||||
});
|
||||
instance?.proxy?.$router?.$avueRouter?.setTitle?.(pageTitle.value);
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
syncTagTitle();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -239,19 +239,22 @@ export default {
|
||||
this.onLoad(this.page);
|
||||
},
|
||||
openCreate() {
|
||||
this.$router.push({ path: '/settlement/settlement-import/form' });
|
||||
this.$router.push({
|
||||
path: '/settlement/settlement-import/form',
|
||||
query: { name: '新增导入结算单' },
|
||||
});
|
||||
},
|
||||
openDetail(row) {
|
||||
if (!row?.id) return;
|
||||
this.$router.push({
|
||||
path: '/settlement/settlement-import/form',
|
||||
query: { id: row.id, mode: 'view' },
|
||||
query: { id: row.id, mode: 'view', name: '查看导入结算单' },
|
||||
});
|
||||
},
|
||||
editDraft(row) {
|
||||
this.$router.push({
|
||||
path: '/settlement/settlement-import/form',
|
||||
query: { id: row.id },
|
||||
query: { id: row.id, name: '编辑导入结算单' },
|
||||
});
|
||||
},
|
||||
async handleDelete(row) {
|
||||
|
||||
Reference in New Issue
Block a user