This commit is contained in:
2026-08-28 15:30:24 +08:00
parent 17bbde81b2
commit 9a4e0f7c03
16 changed files with 1016 additions and 183 deletions
@@ -38,7 +38,11 @@
disabled
/><span v-else>{{
displayValue(
field.prop === 'settlementTypeName' ? settlementTypeName : form[field.prop]
field.prop === 'settlementTypeName'
? settlementTypeName
: field.prop === 'formalSettlementId'
? form.formalSettlementNo || form.formalSettlementId
: form[field.prop]
)
}}</span></el-form-item
></el-col
@@ -340,7 +344,8 @@ export default {
}
this.loading = true;
try {
const { data } = await api.getDetail(this.recordId);
const response = await api.getDetail(this.recordId);
const data = response?.data?.data || response?.data || response || {};
this.form = { ...createSettlementAdjustmentForm(), ...data };
this.details = (data.details || []).map(item => ({
...item,
@@ -124,6 +124,22 @@
class="status-text"
>{{ updateName(row.updateResult) }}</el-tag
>
<span v-else-if="column.prop === 'matchedExternalLineNo'">
{{ formatMatchedExternalLineNo(row.matchedExternalLineNo) }}
</span>
<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.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>
@@ -160,7 +176,9 @@
>下载货物明细对账模板</el-button
>
<el-button type="primary" plain @click="chooseImport">导入</el-button>
<el-button type="primary" @click="handleMatch">开始匹配内部账单</el-button>
<el-button type="primary" :disabled="!canStartMatch" @click="handleMatch"
>开始匹配内部账单</el-button
>
<input
ref="fileInput"
type="file"
@@ -186,6 +204,19 @@
<el-tag v-if="column.prop === 'matchStatus'" :type="matchTagType(row.matchStatus)" class="status-text">{{
matchName(row.matchStatus)
}}</el-tag>
<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.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>
@@ -202,27 +233,6 @@
</el-table-column>
</el-table>
</section-card>
<section-card title="操作说明">
<el-collapse>
<el-collapse-item title="操作说明" name="help">
<div class="reconciliation-editor__help">
<p>1. 新增对账单时选择一张已审批通过的正式结算单系统自动带出内部账单明细</p>
<p>
2. 导入对方 Excel
账单后点击开始匹配内部账单系统按车号货物地址批次发货时间运输量和金额匹配
</p>
<p>3. 外部账单存在重复候选时会标记为疑似重复需要人工选择唯一明细匹配</p>
<p>4. 只有所有内外部明细一一匹配且差异为 0才允许按匹配结果更新账单或完成对账</p>
<p>
5.
已付金额大于外部匹配金额时禁止更新整车模式一车多货会跳过自动更新可通过调整逐货物修改
</p>
<p>6. 按匹配结果更新成功后生成变更记录完成对账后单据不可再编辑和删除</p>
</div>
</el-collapse-item>
</el-collapse>
</section-card>
</div>
<template #footer>
@@ -263,6 +273,11 @@
formatMoney(row.settlementAmount, row.currency)
}}</template></el-table-column
>
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<el-link type="primary" @click="selectFormal(row)">选择</el-link>
</template>
</el-table-column>
</el-table>
<div class="reconciliation-editor__pagination">
<el-pagination
@@ -288,6 +303,7 @@
><el-input-number
v-model="row.transportQuantity"
:min="0"
:precision="2"
:controls="false" /></template
></el-table-column>
<el-table-column prop="unitPrice" label="运输单价" min-width="120"
@@ -325,7 +341,11 @@
<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" />
<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)
@@ -346,7 +366,10 @@
</template>
<script>
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 { downloadXls } from '@/utils/util';
import { transportReconciliationFormFields } from '@/option/settlement/transportReconciliationForm';
import {
@@ -373,6 +396,7 @@ export default {
form: this.emptyForm(),
formFields: transportReconciliationFormFields,
internalColumns,
transportTypeOptions: [],
internalDetails: [],
externalDetails: [],
internalQuery: { documentNo: '', vehicleNo: '', batchNo: '', cargoName: '' },
@@ -396,6 +420,7 @@ export default {
};
},
computed: {
...mapGetters(['userInfo']),
visible: {
get() {
return this.modelValue;
@@ -435,6 +460,9 @@ export default {
row => row.suspectedDuplicate || row.matchStatus === 'suspected_duplicate'
);
},
canStartMatch() {
return Boolean(this.form.formalSettlementId && this.externalDetails.length);
},
visibleExternalRows() {
return this.externalTab === 'duplicate' ? this.duplicateRows : this.externalDetails;
},
@@ -479,6 +507,7 @@ export default {
};
},
async initialize() {
this.loadTransportTypeOptions();
this.currentId = this.recordId;
this.form = this.emptyForm();
this.internalDetails = [];
@@ -487,7 +516,8 @@ export default {
this.internalQuery = { documentNo: '', vehicleNo: '', batchNo: '', cargoName: '' };
if (!this.currentId) {
this.form.reconciliationMode = 'vehicle';
this.form.reconcilerName = '当前用户';
this.form.reconcilerName =
this.userInfo?.realName || this.userInfo?.userName || this.userInfo?.account || '当前用户';
this.form.reconciliationDate = this.$dayjs().format('YYYY-MM-DD');
return;
}
@@ -499,10 +529,12 @@ export default {
}
},
async loadDetail() {
const { data } = await api.getDetail(this.currentId);
const data = this.unwrapData(await api.getDetail(this.currentId)) || {};
this.form = { ...this.emptyForm(), ...data };
this.internalDetails = data.internalDetails || [];
this.externalDetails = data.externalDetails || [];
this.internalDetails =
data.internalDetails || data.internalBillDetails || data.internals || [];
this.externalDetails =
data.externalDetails || data.externalBillDetails || data.externals || [];
},
openFormalDialog() {
if (!this.editable) return;
@@ -513,21 +545,42 @@ export default {
async loadFormalOptions() {
this.formalDialog.loading = true;
try {
const { data } = await api.getFormalOptions(
this.formalDialog.page.current,
this.formalDialog.page.size,
{ settlementType: this.settlementType, keyword: this.formalDialog.query.keyword }
const params = {
settlementType: this.settlementType,
keyword: this.formalDialog.query.keyword,
};
const data = this.unwrapData(
await api.getFormalOptions(
this.formalDialog.page.current,
this.formalDialog.page.size,
params
)
);
this.formalDialog.rows = data.records || [];
this.formalDialog.page.total = data.total || 0;
let rows = data.records || [];
let total = data.total || 0;
if (!rows.length) {
const fallback = await formalSettlementApi.getList(
this.formalDialog.page.current,
this.formalDialog.page.size,
{ ...params, approvalStatus: 'approved' }
);
const fallbackData = this.unwrapData(fallback);
rows = fallbackData.records || [];
total = fallbackData.total || 0;
}
this.formalDialog.rows = rows;
this.formalDialog.page.total = total;
} finally {
this.formalDialog.loading = false;
}
},
confirmFormal() {
async confirmFormal() {
if (this.formalDialog.selected.length !== 1)
return this.$message.warning('请选择一张正式结算单');
const selected = this.formalDialog.selected[0];
await this.selectFormal(this.formalDialog.selected[0]);
},
async selectFormal(selected) {
const formalSettlementChanged = this.form.formalSettlementId !== selected.id;
this.form = {
...this.form,
...selected,
@@ -535,33 +588,115 @@ export default {
formalSettlementNo: selected.formalSettlementNo,
reconciliationNo: this.form.reconciliationNo,
};
if (formalSettlementChanged) this.externalDetails = [];
await this.loadFormalInternalPreview(selected.id);
this.formalDialog.visible = false;
},
async handleSave() {
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',
});
if (this.form.reconciliationMode === 'cargo') {
const feeGroups = await Promise.all(
details.map(async detail => ({
detail,
fees: this.unwrapData(await formalSettlementApi.getDetailFees(detail.id)) || [],
}))
);
this.internalDetails = feeGroups.flatMap(({ detail, fees }) => {
if (!fees.length) return [buildInternalRow(detail, {}, 0)];
return fees.map((fee, index) =>
buildInternalRow(
detail,
{
...fee,
settlementAmount: fee.settlementAmountTax ?? 0,
},
index
)
);
});
} else {
this.internalDetails = details.map((detail, index) => buildInternalRow(detail, {}, index));
}
const internalQuantity = this.internalDetails.reduce(
(total, row) => total + Number(row.transportQuantity || 0),
0
);
const internalAmount = this.internalDetails.reduce(
(total, row) => total + Number(row.settlementAmount || 0),
0
);
this.form = {
...this.form,
internalBillCount: this.internalDetails.length,
internalQuantity,
internalAmount,
externalBillCount: 0,
externalQuantity: 0,
externalAmount: 0,
differenceCount: this.internalDetails.length,
differenceQuantity: internalQuantity,
differenceAmount: internalAmount,
matchedCount: 0,
unmatchedCount: this.internalDetails.length,
};
},
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
},
async loadTransportTypeOptions() {
try {
const data = this.unwrapData(await getDictionary({ code: 'transport_type' }));
const records = Array.isArray(data) ? data : data?.records || [];
this.transportTypeOptions = records.map(item => ({
label: item.dictValue || item.label || item.name,
value: item.dictKey || item.value || item.dictValue || item.name,
}));
} catch {
this.transportTypeOptions = [];
}
},
async handleSave(silent = false) {
const valid = await this.$refs.formRef.validate().catch(() => false);
if (!valid || !this.form.formalSettlementId) return this.$message.warning('请选择正式结算单');
if (!valid || !this.form.formalSettlementId) {
this.$message.warning('请选择正式结算单');
return null;
}
this.saving = true;
try {
const { data } = await api.save({
id: this.currentId,
formalSettlementId: this.form.formalSettlementId,
reconciliationMode: this.form.reconciliationMode,
reconciliationDate: this.form.reconciliationDate,
remark: this.form.remark,
});
const data = this.unwrapData(
await api.save({
id: this.currentId,
formalSettlementId: this.form.formalSettlementId,
reconciliationMode: this.form.reconciliationMode,
reconciliationDate: this.form.reconciliationDate,
remark: this.form.remark,
})
);
this.currentId = data;
await this.loadDetail();
this.$message.success('草稿保存成功');
this.$emit('success');
if (!silent) {
this.$message.success('草稿保存成功');
this.$emit('success');
}
return data;
} finally {
this.saving = false;
}
},
async handleMatch() {
if (!this.currentId) {
await this.handleSave();
if (!this.currentId) return;
}
if (!this.currentId) return this.$message.warning('请先保存草稿后再匹配');
this.actionLoading = true;
try {
await api.match(this.currentId);
@@ -572,10 +707,11 @@ export default {
}
},
async handleUpdate() {
if (!this.currentId) return this.$message.warning('请先保存对账单');
await this.$confirm('将以外部账单匹配金额更新内部结算明细,是否继续?', '更新账单', {
type: 'warning',
});
const savedId = await this.handleSave(true);
if (!savedId) return;
this.actionLoading = true;
try {
await api.updateByMatch(this.currentId);
@@ -586,22 +722,30 @@ export default {
}
},
async handleComplete() {
if (!this.currentId) return this.$message.warning('请先保存对账单');
if (
Number(this.form.differenceCount || 0) !== 0 ||
Number(this.form.differenceQuantity || 0) !== 0 ||
Number(this.form.differenceAmount || 0) !== 0
) {
return this.$message.warning('差异单数、差异货量和差异金额必须全部为0才可完成对账');
}
await this.$confirm('完成后对账单不可修改和删除,是否继续?', '完成对账', {
type: 'warning',
});
const savedId = await this.handleSave(true);
if (!savedId) return;
this.actionLoading = true;
try {
await api.complete(this.currentId);
await this.loadDetail();
this.$message.success('对账完成');
this.visible = false;
this.$emit('success');
} finally {
this.actionLoading = false;
}
},
chooseImport() {
if (!this.currentId) return this.$message.warning('请先保存对账单');
this.$refs.fileInput?.click();
},
async handleImport(event) {
@@ -609,6 +753,10 @@ export default {
event.target.value = '';
if (!file) return;
try {
if (!this.currentId) {
const savedId = await this.handleSave(true);
if (!savedId) return;
}
const response =
this.form.reconciliationMode === 'cargo'
? await api.importCargo(this.currentId, file)
@@ -731,6 +879,28 @@ export default {
displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
formatMatchedExternalLineNo(value) {
return Number(value) < 0 ? '' : this.displayValue(value);
},
formatTransportQuantity(value) {
return value === null || value === undefined || value === ''
? '-'
: Number(value || 0).toFixed(2);
},
formatMileage(value) {
return Number(value) === -1 ? '' : this.displayValue(value);
},
formatDate(value) {
if (value === null || value === undefined || value === '') return '-';
const date = this.$dayjs(value);
return date.isValid() ? date.format('YYYY-MM-DD') : this.displayValue(value);
},
transportTypeLabel(value) {
if (value === null || value === undefined || value === '') return '-';
return (
this.transportTypeOptions.find(item => String(item.value) === String(value))?.label || value
);
},
formatMoney(value, currency = 'RMB') {
return `${Number(value || 0).toFixed(2)} ${currency || 'RMB'}`;
},
+55 -13
View File
@@ -160,7 +160,9 @@
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import * as XLSX from 'xlsx';
import { createSettlementTransfer } from '@/utils/settlement-transfer';
import * as api from '@/api/settlement/formalSettlement';
import * as paymentApi from '@/api/payment/paymentApplication';
import {
formalSettlementSearchFields,
invoiceStatusOptions,
@@ -362,27 +364,67 @@ export default {
this.$message.success(`同步成功,金蝶单据号:${data}`);
this.loadTable();
},
openPaymentDialog() {
async openPaymentDialog() {
if (!this.selection.length) return this.$message.warning('请至少选择一条正式结算单');
const rows = this.selection;
if (rows.some(row => row.approvalStatus !== 'approved' || row.settlementType !== 'payable'))
return this.$message.warning('仅审批通过的应付正式结算单允许发起付款申请');
const contractIds = new Set(rows.map(row => String(row.contractId || '')));
if (rows.length > 1 && (contractIds.size !== 1 || contractIds.has('')))
return this.$message.warning('批量付款申请必须选择同一个合同的正式结算单');
if (contractIds.size !== 1 || contractIds.has(''))
return this.$message.warning('付款申请必须选择同一个合同的正式结算单');
const paymentRows = rows.map(row => ({ ...row, appliedAmount: this.paymentRemaining(row) }));
if (paymentRows.some(row => row.appliedAmount <= 0))
return this.$message.warning('所选正式结算单均须存在剩余可申请金额');
this.paymentDialog = {
visible: true,
loading: false,
row: paymentRows[0],
rows: paymentRows,
form: {
appliedAmount: paymentRows.length === 1 ? paymentRows[0].appliedAmount : 0,
remark: '',
},
};
try {
const sourceFormalSettlements = await Promise.all(
paymentRows.map(async row => {
const [amountResponse, detailResponse] = await Promise.all([
paymentApi.getReferenceAmount('settlement_payment', row.id),
api.getDetail(row.id),
]);
const amount = this.unwrapData(amountResponse) || {};
const detail = this.unwrapData(detailResponse) || {};
const remainingAmount = Number(amount.payableAmount ?? this.paymentRemaining(row));
return {
...row,
id: row.id,
settlementId: row.id,
projectId: detail.projectId ?? row.projectId,
projectName: detail.projectName || row.projectName,
deptId: detail.deptId ?? row.deptId,
deptName: detail.deptName || row.deptName,
contractId: detail.contractId ?? row.contractId,
contractNo: detail.contractNo || row.contractNo,
contractName: detail.contractName || row.contractName,
payerName: detail.payerName || row.payerName,
payeeName: detail.payeeName || row.payeeName,
formalSettlementNo: detail.formalSettlementNo || row.formalSettlementNo,
settlementAmount: Number(
amount.settlementAmount ?? detail.settlementAmount ?? row.settlementAmount ?? 0
),
payableAmount: remainingAmount,
appliedAmount: remainingAmount,
invoices: detail.invoices || [],
paymentRecords: detail.paymentRecords || detail.paymentApplications || [],
};
})
);
if (sourceFormalSettlements.some(row => row.appliedAmount <= 0)) {
this.$message.warning('所选正式结算单均须存在剩余可申请金额');
return;
}
const transferToken = createSettlementTransfer({
sourceFormalSettlements,
settlementType: 'payable',
});
await this.$router.push({
path: '/payment/payment-application/form',
query: { mode: 'add', paymentType: 'settlement_payment', transferToken },
});
this.selection = [];
} catch (error) {
this.$message.error('结算信息加载失败,请稍后重试');
}
},
async submitPayment() {
const paymentRows = this.paymentDialog.rows;
@@ -138,10 +138,11 @@ export default {
async loadTable() {
this.loading = true;
try {
const { data } = await api.getList(this.page.current, this.page.size, {
const response = await api.getList(this.page.current, this.page.size, {
...this.query,
settlementType: this.settlementType,
});
const data = response?.data?.data || response?.data || response || {};
this.rows = data.records || [];
this.page.total = data.total || 0;
} finally {
@@ -202,10 +203,11 @@ export default {
this.loadTable();
},
async handleExport() {
const { data } = await api.getList(1, 100000, {
const response = await api.getList(1, 100000, {
...this.query,
settlementType: this.settlementType,
});
const data = response?.data?.data || response?.data || response || {};
const exportRows = (data.records || []).map(item => ({
对账单号: item.reconciliationNo,
付款方: item.payerName,