调整业务模块

This commit is contained in:
2026-09-05 23:26:37 +08:00
parent 3bac9b6472
commit 6a6d10dc79
6 changed files with 437 additions and 138 deletions
@@ -337,11 +337,15 @@
clearable
maxlength="32"
placeholder="请输入发票号码"
@keyup.enter="claimInvoices"
@keyup.enter="openInvoiceClaimDialog"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="invoiceClaimLoading" @click="claimInvoices">
<el-button
type="primary"
:loading="invoiceClaimLoading"
@click="openInvoiceClaimDialog"
>
认领查询
</el-button>
</el-form-item>
@@ -407,6 +411,67 @@
</el-table>
</section-card>
<el-dialog
v-model="invoiceClaimDialog.visible"
title="查询可认领发票"
width="92%"
append-to-body
destroy-on-close
>
<el-table
ref="invoiceClaimTable"
v-loading="invoiceClaimDialog.loading"
:data="invoiceClaimPagedRows"
row-key="invoiceNo"
border
height="520"
@selection-change="invoiceClaimDialog.selected = $event"
>
<el-table-column
type="selection"
width="52"
fixed="left"
align="center"
reserve-selection
/>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="invoiceNo" label="发票号" min-width="180" align="center" />
<el-table-column prop="invoiceDate" label="开票日期" min-width="130" align="center" />
<el-table-column prop="invoiceType" label="发票类型" min-width="150" align="center" />
<el-table-column prop="issuerName" label="开票单位" min-width="180" align="center" />
<el-table-column prop="receiverName" label="受票单位" min-width="180" align="center" />
<el-table-column
prop="invoiceAmount"
label="发票金额(含税)"
min-width="160"
align="right"
>
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
</el-table-column>
<el-table-column prop="matchedAmount" label="认领金额" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(row.matchedAmount) }}</template>
</el-table-column>
</el-table>
<div class="formal-editor__pagination">
<el-pagination
v-model:current-page="invoiceClaimDialog.page.current"
v-model:page-size="invoiceClaimDialog.page.size"
:total="invoiceClaimDialog.page.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
/>
</div>
<template #footer>
<el-button @click="invoiceClaimDialog.visible = false">取消</el-button>
<el-button
type="primary"
:loading="invoiceClaimDialog.confirming"
@click="confirmInvoiceClaims"
>确认认领</el-button
>
</template>
</el-dialog>
<section-card
v-if="readonly"
:title="form.settlementType === 'receivable' ? '收款信息' : '付款信息'"
@@ -523,7 +588,7 @@
<template #default="{ row }">{{ displayValue(row.changeType) }}</template>
</el-table-column>
<el-table-column prop="lineNo" label="行号" min-width="90" align="center">
<template #default="{ row }">{{ displayValue(row.lineNo) }}</template>
<template #default="{ row }">{{ changeRecordLineNo(row) }}</template>
</el-table-column>
<el-table-column prop="operationType" label="类型" min-width="110" align="center">
<template #default="{ row }">{{ displayValue(row.operationType) }}</template>
@@ -843,6 +908,7 @@ import {
getFeeOptions,
getNextNo,
getReceiptClaims,
claimInvoices as claimInvoicesApi,
save,
} from '@/api/settlement/formalSettlement';
import {
@@ -898,6 +964,14 @@ export default {
invoices: [],
invoiceClaimNo: '',
invoiceClaimLoading: false,
invoiceClaimDialog: {
visible: false,
loading: false,
confirming: false,
rows: [],
selected: [],
page: { current: 1, size: 10, total: 0 },
},
attachmentUploadFiles: [],
attachmentTypeOptions: [],
attachmentFileTypes: [
@@ -1034,6 +1108,11 @@ export default {
});
return Array.from(names);
},
invoiceClaimPagedRows() {
const { current, size } = this.invoiceClaimDialog.page;
const start = (current - 1) * size;
return this.invoiceClaimDialog.rows.slice(start, start + size);
},
},
watch: {
modelValue: {
@@ -1103,6 +1182,9 @@ export default {
this.selectedAttachmentRows = [];
this.invoices = [];
this.invoiceClaimNo = '';
this.invoiceClaimDialog.visible = false;
this.invoiceClaimDialog.rows = [];
this.invoiceClaimDialog.selected = [];
this.attachmentUploadFiles = [];
this.billingRuleDialog = {
visible: false,
@@ -1864,6 +1946,78 @@ export default {
this.invoiceClaimLoading = false;
}
},
openInvoiceClaimDialog() {
const stamp = Date.now();
this.invoiceClaimDialog.loading = true;
this.invoiceClaimDialog.rows = Array.from({ length: 80 }, (_, index) => {
const amount = Math.floor(1000 + Math.random() * 49000);
return {
invoiceNo: `MOCK${stamp}${String(index + 1).padStart(3, '0')}`.slice(0, 32),
invoiceDate: this.$dayjs()
.subtract(Math.floor(Math.random() * 90), 'day')
.format('YYYY-MM-DD'),
invoiceType: index % 2 ? '增值税普通发票' : '增值税专用发票',
issuerName: this.form.payerName || 'Mock开票单位',
receiverName: this.form.payeeName || 'Mock受票单位',
invoiceAmount: amount,
availableInvoiceAmount: amount,
matchedAmount: amount,
taxRate: [3, 6, 9, 13][index % 4],
attachmentJson: '',
mockData: true,
};
});
this.invoiceClaimDialog.selected = [];
this.invoiceClaimDialog.page.current = 1;
this.invoiceClaimDialog.page.total = 80;
this.invoiceClaimDialog.visible = true;
this.invoiceClaimDialog.loading = false;
},
async confirmInvoiceClaims() {
if (!this.invoiceClaimDialog.selected.length) {
this.$message.warning('请至少选择一条发票');
return;
}
this.invoiceClaimDialog.confirming = true;
try {
const existingNumbers = new Set(this.invoices.map(item => String(item.invoiceNo)));
let remaining = Math.max(
Number(this.form.settlementAmount || 0) -
this.invoices.reduce((total, item) => total + Number(item.matchedAmount || 0), 0),
0
);
const invoices = this.invoiceClaimDialog.selected
.filter(row => !existingNumbers.has(String(row.invoiceNo)))
.map(row => {
const sourceAmount = Number(row.matchedAmount || 0);
const matchedAmount =
Number(this.form.settlementAmount || 0) > 0
? Math.min(sourceAmount, remaining)
: sourceAmount;
if (Number(this.form.settlementAmount || 0) > 0) {
remaining = Math.max(0, remaining - matchedAmount);
}
return { ...row, matchedAmount };
})
.filter(row => row.matchedAmount > 0);
if (!invoices.length) {
this.$message.warning('当前正式结算单没有可认领的剩余金额');
return;
}
if (!this.form.id) {
this.invoices = [...this.invoices, ...invoices];
this.invoiceClaimDialog.visible = false;
this.$message.success(`已选择${invoices.length}条发票,保存正式结算单后生效`);
return;
}
await claimInvoicesApi({ formalSettlementId: this.form.id, invoices });
await this.loadFormalDetail(this.form.id);
this.invoiceClaimDialog.visible = false;
this.$message.success('发票认领成功,已同步正式结算单及关联付款单据');
} finally {
this.invoiceClaimDialog.confirming = false;
}
},
validateInvoices() {
const invoiceNumbers = new Set();
let matchedTotal = 0;
@@ -2000,7 +2154,8 @@ export default {
return this.$message.warning('请完善手工费用的费用类型和费用项');
}
if (!this.validateInvoices()) return;
if (!this.validateAttachments()) return;
// 附件类型仅做提示,不阻断正式结算单新增/保存提交。
this.validateAttachments();
this.saving = true;
try {
await save(this.buildSavePayload());
@@ -2014,6 +2169,10 @@ export default {
displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
changeRecordLineNo(row) {
const lineNo = row?.lineNo;
return lineNo === -1 || String(lineNo) === '-1' ? '' : this.displayValue(lineNo);
},
paymentMethodName(value) {
const labels = {
bank_transfer: '银行转账',
@@ -82,12 +82,7 @@
添加费用
</el-button>
</template>
<el-table
:data="summaryFees"
border
show-summary
:summary-method="getSummarySums"
>
<el-table :data="summaryFees" border show-summary :summary-method="getSummarySums">
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column
v-for="column in summaryColumns"
@@ -261,7 +256,7 @@
<span v-else-if="pageMode && column.prop === 'mileage'">
{{ formatDetailMileage(row[column.prop]) }}
</span>
<span v-else-if="pageMode && column.prop === 'transportType'">
<span v-else-if="column.prop === 'transportType'">
{{ transportTypeName(row[column.prop]) }}
</span>
<span v-else>{{ displayValue(row[column.prop]) }}</span>
@@ -374,6 +369,12 @@
<span v-else-if="column.prop === 'billStatus'">
{{ advanceStatusName(row.billStatus) }}
</span>
<span v-else-if="column.prop === 'createUserName'">
{{ advanceCreateUserName(row) }}
</span>
<span v-else-if="column.prop === 'createTime'">
{{ advanceCreateTime(row) }}
</span>
<span v-else>{{ displayValue(row[column.prop]) }}</span>
</template>
</el-table-column>
@@ -381,7 +382,7 @@
</section-card>
<section-card v-if="form.id && readonly" title="变更记录">
<el-table :data="changeRecords" border>
<el-table :data="adjustChangeRecords" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column
v-for="column in changeTableColumns"
@@ -671,103 +672,131 @@
append-to-body
destroy-on-close
>
<el-table v-loading="adjustDialog.loading" :data="adjustRows" border>
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
<el-table-column prop="cargoName" label="货物名称" min-width="140" align="center" />
<el-table-column prop="cargoType" label="货物类型" min-width="130" align="center" />
<el-table-column label="计费规则" min-width="190" align="center">
<template #default="{ row }">
<div class="pre-settlement-editor__billing-rule-cell">
<span>{{ displayValue(billingRuleName(row)) }}</span>
<el-icon
class="pre-settlement-editor__billing-rule-icon"
title="查看计费规则详情"
@click="openBillingRuleInfo(row)"
>
<InfoFilled />
</el-icon>
</div>
</template>
</el-table-column>
<el-table-column label="运输总量" min-width="150" align="center">
<template #default="{ row }">
<span v-if="adjustDialog.readonly">{{ formatQuantity(row) }}</span>
<el-input-number
v-else
v-model="row.transportQuantity"
:min="0"
:precision="2"
:controls="false"
<el-tabs v-model="adjustDialog.activeTab" class="pre-settlement-editor__adjust-tabs">
<el-tab-pane label="结算明细调整" name="adjust">
<el-table v-loading="adjustDialog.loading" :data="adjustRows" border>
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
<el-table-column prop="cargoName" label="货物名称" min-width="140" align="center" />
<el-table-column prop="cargoType" label="货物类型" min-width="130" align="center" />
<el-table-column label="计费规则" min-width="190" align="center">
<template #default="{ row }">
<div class="pre-settlement-editor__billing-rule-cell">
<span>{{ displayValue(billingRuleName(row)) }}</span>
<el-icon
class="pre-settlement-editor__billing-rule-icon"
title="查看计费规则详情"
@click="openBillingRuleInfo(row)"
>
<InfoFilled />
</el-icon>
</div>
</template>
</el-table-column>
<el-table-column label="运输总量" min-width="150" align="center">
<template #default="{ row }">
<span v-if="adjustDialog.readonly">{{ formatQuantity(row) }}</span>
<el-input-number
v-else
v-model="row.transportQuantity"
:min="0"
:precision="2"
:controls="false"
/>
</template>
</el-table-column>
<el-table-column label="里程(KM" min-width="140" align="center">
<template #default="{ row }">
<el-input-number
v-model="row.mileage"
:min="0"
:precision="2"
:controls="false"
:disabled="adjustDialog.readonly"
/>
</template>
</el-table-column>
<el-table-column label="运输单价" min-width="140" align="center">
<template #default="{ row }">
<el-input-number
v-model="row.unitPrice"
:min="0"
:precision="2"
:controls="false"
:disabled="adjustDialog.readonly"
/>
</template>
</el-table-column>
<el-table-column
v-for="feeItem in adjustFeeItemNames"
:key="feeItem"
:label="feeItem"
min-width="140"
align="center"
>
<template #default="{ row }">
<el-input-number
v-model="row.feeItems[feeItem]"
:min="0"
:precision="2"
:controls="false"
:disabled="adjustDialog.readonly"
@change="recalculateAdjustRow(row, feeItem)"
/>
</template>
</el-table-column>
<el-table-column label="结算金额(含税)" min-width="170" align="center">
<template #default="{ row }">
<el-input-number
v-model="row.settlementAmountTax"
:precision="2"
:controls="false"
:disabled="adjustDialog.readonly"
/>
</template>
</el-table-column>
<el-table-column label="备注" min-width="200" align="center">
<template #default="{ row }">
<el-input v-model="row.remark" maxlength="200" :disabled="adjustDialog.readonly" />
</template>
</el-table-column>
</el-table>
<el-form
v-if="!adjustDialog.readonly"
:model="adjustDialog"
label-position="right"
label-width="auto"
class="pre-settlement-editor__adjust-reason"
>
<el-form-item label="调整原因">
<el-input v-model="adjustDialog.reason" maxlength="200" show-word-limit />
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="变更记录" name="records">
<el-table :data="changeRecords" border>
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="changeTime" label="变更日期" min-width="170" align="center" />
<el-table-column prop="operatorName" label="经办人" min-width="140" align="center" />
<el-table-column prop="changeType" label="变更类型" min-width="160" align="center" />
<el-table-column
prop="changeReason"
label="变更原因"
min-width="240"
align="center"
show-overflow-tooltip
/>
</template>
</el-table-column>
<el-table-column label="里程(KM" min-width="140" align="center">
<template #default="{ row }">
<el-input-number
v-model="row.mileage"
:min="0"
:precision="2"
:controls="false"
:disabled="adjustDialog.readonly"
/>
</template>
</el-table-column>
<el-table-column label="运输单价" min-width="140" align="center">
<template #default="{ row }">
<el-input-number
v-model="row.unitPrice"
:min="0"
:precision="2"
:controls="false"
:disabled="adjustDialog.readonly"
/>
</template>
</el-table-column>
<el-table-column
v-for="feeItem in adjustFeeItemNames"
:key="feeItem"
:label="feeItem"
min-width="140"
align="center"
>
<template #default="{ row }">
<el-input-number
v-model="row.feeItems[feeItem]"
:min="0"
:precision="2"
:controls="false"
:disabled="adjustDialog.readonly"
@change="recalculateAdjustRow(row, feeItem)"
/>
</template>
</el-table-column>
<el-table-column label="结算金额(含税)" min-width="170" align="center">
<template #default="{ row }">
<el-input-number
v-model="row.settlementAmountTax"
:precision="2"
:controls="false"
:disabled="adjustDialog.readonly"
/>
</template>
</el-table-column>
<el-table-column label="备注" min-width="200" align="center">
<template #default="{ row }">
<el-input v-model="row.remark" maxlength="200" :disabled="adjustDialog.readonly" />
</template>
</el-table-column>
</el-table>
<el-form
v-if="!adjustDialog.readonly"
:model="adjustDialog"
label-position="right"
label-width="auto"
class="pre-settlement-editor__adjust-reason"
>
<el-form-item label="调整原因">
<el-input v-model="adjustDialog.reason" maxlength="200" show-word-limit />
</el-form-item>
</el-form>
<el-table-column label="状态" min-width="120" align="center">
<template #default>已生效</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="{ row }">
<el-link type="primary" @click="openAdjustChangeRecord(row)">查看详情</el-link>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!adjustChangeRecords.length" description="暂无变更记录" :image-size="60" />
</el-tab-pane>
</el-tabs>
<template #footer>
<el-button @click="adjustDialog.visible = false">取消</el-button>
<el-button
@@ -781,6 +810,36 @@
</template>
</el-dialog>
<el-dialog
v-model="adjustChangeRecordVisible"
title="变更记录详情"
append-to-body
destroy-on-close
width="1100px"
top="10px"
class="pre-settlement-change-record-detail-dialog"
>
<div v-if="adjustChangeRecord" class="pre-settlement-change-record-detail-meta">
<span>变更日期:{{ adjustChangeRecord.changeTime || '-' }}</span>
<span>经办人:{{ adjustChangeRecord.operatorName || '-' }}</span>
<span>变更类型:{{ adjustChangeRecord.changeType || '-' }}</span>
<span>状态:已生效</span>
</div>
<el-table :data="adjustChangeRecordDetailRows" border :show-overflow-tooltip="false">
<el-table-column prop="field" label="变更字段" min-width="220" />
<el-table-column prop="before" label="变更前" min-width="330" />
<el-table-column prop="after" label="变更后" min-width="420" />
</el-table>
<el-empty
v-if="!adjustChangeRecordDetailRows.length"
description="暂无变更内容"
:image-size="60"
/>
<template #footer>
<el-button type="primary" @click="adjustChangeRecordVisible = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog
v-model="billingRuleDialog.visible"
title="计费规则详情"
@@ -1010,10 +1069,15 @@ export default {
loading: false,
saving: false,
readonly: false,
activeTab: 'adjust',
detailId: '',
detailLineNo: '',
reason: '',
},
adjustRows: [],
adjustChangeRecordVisible: false,
adjustChangeRecord: null,
adjustChangeRecordDetailRows: [],
billingRuleDialog: {
visible: false,
rule: null,
@@ -1105,7 +1169,7 @@ export default {
);
},
visibleDetailColumns() {
if (!this.pageMode) return this.detailColumns;
if (!this.readonly && !this.pageMode) return this.detailColumns;
return this.detailColumns.filter(column => column.prop !== 'settlementAmountNoTax');
},
detailFeeItemNames() {
@@ -1132,6 +1196,13 @@ export default {
});
return Array.from(names);
},
adjustChangeRecords() {
return this.changeRecords.filter(
row =>
row.changeType === '结算明细项' &&
String(row.lineNo ?? '') === String(this.adjustDialog.detailLineNo ?? '')
);
},
},
watch: {
modelValue: {
@@ -1229,6 +1300,12 @@ export default {
this.attachments = [];
this.selectedAttachmentRows = [];
this.adjustRows = [];
this.adjustDialog.activeTab = 'adjust';
this.adjustDialog.detailId = '';
this.adjustDialog.detailLineNo = '';
this.adjustChangeRecordVisible = false;
this.adjustChangeRecord = null;
this.adjustChangeRecordDetailRows = [];
this.billingRuleDialog.visible = false;
this.billingRuleDialog.rule = null;
this.contractOptions = [];
@@ -1247,7 +1324,11 @@ export default {
this.form = { ...emptyPreSettlementForm(), ...detail };
this.summaryFees = (detail.summaryFees || []).map(row => ({ ...row }));
this.details = (detail.details || []).map(row => ({ ...row }));
this.advances = detail.advances || [];
this.advances = (detail.advances || []).map(row => ({
...row,
createUserName: row.createUserName || detail.createUserName,
createTime: row.createTime || detail.createTime,
}));
this.changeRecords = detail.changeRecords || [];
this.attachments = this.parseAttachments(detail.attachmentsJson);
this.selectedAttachmentRows = [];
@@ -1360,7 +1441,11 @@ export default {
},
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,
}));
},
handleContractChange(id) {
const contract = this.contractOptions.find(item => String(item.id) === String(id));
@@ -1720,7 +1805,9 @@ export default {
this.adjustDialog.visible = true;
this.adjustDialog.loading = true;
this.adjustDialog.readonly = readonly;
this.adjustDialog.activeTab = 'adjust';
this.adjustDialog.detailId = detailRow.id;
this.adjustDialog.detailLineNo = detailRow.lineNo;
this.adjustDialog.reason = '';
try {
const { data } = await getDetailFees(detailRow.id);
@@ -1733,6 +1820,42 @@ export default {
this.adjustDialog.loading = false;
}
},
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 {};
}
},
formatAdjustChangeValue(value) {
if (value === undefined || value === null || value === '') return '空';
if (typeof value === 'object') return JSON.stringify(value);
return String(value);
},
buildAdjustChangeRecordRows(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.formatAdjustChangeValue(before[field]),
after: this.formatAdjustChangeValue(after[field]),
}));
if (row.changeContent)
rows.unshift({ field: '变更内容', before: '空', after: row.changeContent });
if (row.changeReason) rows.push({ field: '变更原因', before: '空', after: row.changeReason });
return rows;
},
openAdjustChangeRecord(row) {
this.adjustChangeRecord = row;
this.adjustChangeRecordDetailRows = this.buildAdjustChangeRecordRows(row);
this.adjustChangeRecordVisible = true;
},
billingRuleName(row) {
return this.normalizeBillingRule(row).name;
},
@@ -2035,6 +2158,14 @@ export default {
}[status] || this.displayValue(status)
);
},
advanceCreateUserName(row) {
return this.displayValue(
row?.createUserName || row?.advanceCreateUserName || row?.applyUserName
);
},
advanceCreateTime(row) {
return this.displayValue(row?.createTime || row?.advanceCreateTime || row?.applyTime);
},
changeRecordLineNo(row) {
if (row.changeType !== '结算明细项') return '';
if (row.lineNo !== undefined && row.lineNo !== null && row.lineNo !== '') return row.lineNo;
@@ -2225,6 +2356,23 @@ export default {
}
}
.pre-settlement-change-record-detail-meta {
display: flex;
flex-wrap: wrap;
gap: 8px 32px;
margin-bottom: 16px;
color: #606266;
}
:deep(.pre-settlement-change-record-detail-dialog .el-dialog__body) {
padding-top: 12px;
}
:deep(.pre-settlement-change-record-detail-dialog .el-table .cell) {
white-space: pre-wrap;
word-break: break-all;
}
:deep(.pre-settlement-editor .el-dialog__body) {
padding: 12px 16px;
}
+1 -3
View File
@@ -577,9 +577,7 @@ export default {
return `${Number(value || 0).toFixed(2)} ${currency || 'RMB'}`;
},
paymentRemaining(row) {
const remaining =
row?.remainingPayableAmount ??
Number(row?.settlementAmount || 0) - Number(row?.appliedPaymentAmount || 0);
const remaining = Number(row?.settlementAmount || 0) - Number(row?.paidAmount || 0);
return Math.max(0, Number(remaining || 0));
},
},