This commit is contained in:
2026-08-31 10:38:21 +08:00
parent a10eea247f
commit a75ac1cfa4
13 changed files with 864 additions and 161 deletions
+6
View File
@@ -19,6 +19,12 @@ export const changeRoute = data =>
method: 'post', method: 'post',
data, data,
}); });
export const maintainMileage = data =>
request({
url: `${baseUrl}/maintain-mileage`,
method: 'post',
data,
});
export const roadLoading = ids => export const roadLoading = ids =>
request({ request({
url: `${baseUrl}/road-loading`, url: `${baseUrl}/road-loading`,
@@ -8,6 +8,8 @@ export const getFormalSettlements = keyword =>
request({ url: `${baseUrl}/candidate-formal-settlements`, method: 'get', params: { keyword } }); request({ url: `${baseUrl}/candidate-formal-settlements`, method: 'get', params: { keyword } });
export const getFormalDetails = formalSettlementId => export const getFormalDetails = formalSettlementId =>
request({ url: `${baseUrl}/formal-details`, method: 'get', params: { formalSettlementId } }); request({ url: `${baseUrl}/formal-details`, method: 'get', params: { formalSettlementId } });
export const getFeeOptions = () =>
request({ url: `${baseUrl}/fee-options`, method: 'get' });
export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data }); export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data });
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } }); export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
export const submit = id => request({ url: `${baseUrl}/submit`, method: 'post', data: { id } }); export const submit = id => request({ url: `${baseUrl}/submit`, method: 'post', data: { id } });
@@ -11,8 +11,8 @@ export const formalSettlementFormFields = [
{ label: '汇率日期', prop: 'exchangeRateDate', type: 'date', required: true }, { label: '汇率日期', prop: 'exchangeRateDate', type: 'date', required: true },
{ label: '结算汇率', prop: 'exchangeRate', type: 'number', required: true }, { label: '结算汇率', prop: 'exchangeRate', type: 'number', required: true },
{ label: '本位币合计', prop: 'localSettlementAmount', readonly: true, money: true }, { label: '本位币合计', prop: 'localSettlementAmount', readonly: true, money: true },
{ label: '申请付款金额(含预付)', prop: 'appliedPaymentAmount', readonly: true, money: true }, { label: '创建人', prop: 'createUserName', readonly: true },
{ label: '已收/已付合计', prop: 'paidAmount', readonly: true, money: true }, { label: '创建时间', prop: 'createTime', readonly: true },
]; ];
export const createFormalSettlementForm = () => ({ export const createFormalSettlementForm = () => ({
@@ -27,6 +27,8 @@ export const createFormalSettlementForm = () => ({
sourceDetailIds: [], sourceDetailIds: [],
exchangeRateDate: '', exchangeRateDate: '',
exchangeRate: 1, exchangeRate: 1,
createUserName: '',
createTime: '',
attachmentsJson: '[]', attachmentsJson: '[]',
remark: '', remark: '',
}); });
@@ -7,11 +7,21 @@ export const approvalStatusOptions = [
]; ];
export const invoiceStatusOptions = [ export const invoiceStatusOptions = [
{ label: '未收/开票', value: 'unreceived' }, { label: '未开/收票', value: 'unreceived' },
{ label: '部分收/开票', value: 'partial' }, { label: '部分开/收票', value: 'partial' },
{ label: '已收/开票', value: 'completed' }, { label: '已开/收票', value: 'completed' },
]; ];
export const invoiceStatusName = (value, settlementType) => {
const payable = settlementType === 'payable';
const labels = {
unreceived: payable ? '未收票' : '未开票',
partial: payable ? '部分收票' : '部分开票',
completed: payable ? '已收票' : '已开票',
};
return labels[value] || value || '-';
};
export const paymentStatusOptions = [ export const paymentStatusOptions = [
{ label: '未收/付款', value: 'unpaid' }, { label: '未收/付款', value: 'unpaid' },
{ label: '部分收/付款', value: 'partial' }, { label: '部分收/付款', value: 'partial' },
@@ -1,13 +1,15 @@
export const settlementAdjustmentFormFields = [ export const settlementAdjustmentFormFields = [
{ label: '单据号', prop: 'adjustmentNo' }, { label: '单据号', prop: 'adjustmentNo' },
{ label: '关联结算单号', prop: 'formalSettlementId' }, { label: '关联结算单号', prop: 'formalSettlementId' },
{ label: '客户/客商名称', prop: 'customerName' }, { label: '客户', prop: 'customerName' },
{ label: '结算单类型', prop: 'settlementTypeName' }, { label: '结算单类型', prop: 'settlementTypeName' },
{ label: '关联合同名称', prop: 'contractName' }, { label: '关联合同名称', prop: 'contractName' },
{ label: '关联项目', prop: 'projectName' }, { label: '关联项目', prop: 'projectName' },
{ label: '原结算金额', prop: 'originalSettlementAmount', money: true }, { label: '原结算金额', prop: 'originalSettlementAmount', money: true },
{ label: '调整金额', prop: 'adjustmentAmount', money: true }, { label: '调整金额', prop: 'adjustmentAmount', money: true },
{ label: '调整后结算金额', prop: 'adjustedSettlementAmount', money: true }, { label: '调整后结算金额', prop: 'adjustedSettlementAmount', money: true },
{ label: '创建日期', prop: 'createTime' },
{ label: '创建人', prop: 'createUserName' },
]; ];
export const createSettlementAdjustmentForm = () => ({ export const createSettlementAdjustmentForm = () => ({
@@ -24,6 +26,8 @@ export const createSettlementAdjustmentForm = () => ({
adjustmentAmount: 0, adjustmentAmount: 0,
originalSettlementAmount: 0, originalSettlementAmount: 0,
adjustedSettlementAmount: 0, adjustedSettlementAmount: 0,
createTime: '',
createUserName: '',
attachmentsJson: '[]', attachmentsJson: '[]',
remark: '', remark: '',
}); });
@@ -8,8 +8,6 @@ export const settlementAdjustmentTableColumns = [
{ label: '合同编号', prop: 'contractNo', minWidth: 150 }, { label: '合同编号', prop: 'contractNo', minWidth: 150 },
{ label: '合同名称', prop: 'contractName', minWidth: 170 }, { label: '合同名称', prop: 'contractName', minWidth: 170 },
{ label: '调整金额', prop: 'adjustmentAmount', minWidth: 130, money: true }, { label: '调整金额', prop: 'adjustmentAmount', minWidth: 130, money: true },
{ label: '原结算金额', prop: 'originalSettlementAmount', minWidth: 140, money: true },
{ label: '调整后结算金额', prop: 'adjustedSettlementAmount', minWidth: 160, money: true },
{ label: '审核状态', prop: 'approvalStatusName', minWidth: 110, status: true }, { label: '审核状态', prop: 'approvalStatusName', minWidth: 110, status: true },
{ label: '当前节点', prop: 'currentNode', minWidth: 120 }, { label: '当前节点', prop: 'currentNode', minWidth: 120 },
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 130 }, { label: '当前处理人', prop: 'currentProcessor', minWidth: 130 },
@@ -2143,6 +2143,9 @@
<el-link type="primary" v-if="canComplete(row)" @click="handleAction('complete', row)"> <el-link type="primary" v-if="canComplete(row)" @click="handleAction('complete', row)">
{{ completeActionLabel }} {{ completeActionLabel }}
</el-link> </el-link>
<el-link type="primary" v-if="canMaintainMileage(row)" @click="openMileageDialog(row)">
维护里程
</el-link>
<el-link <el-link
v-for="operation in customOperations(row)" v-for="operation in customOperations(row)"
:key="operation.action" :key="operation.action"
@@ -2156,6 +2159,59 @@
</template> </template>
</component> </component>
<el-dialog
v-model="mileageDialog.visible"
width="520px"
append-to-body
:close-on-click-modal="false"
class="business-crud-page__mileage-dialog"
@closed="resetMileageDialog"
>
<template #header>
<div class="dialog-section-title">维护里程</div>
</template>
<el-form
ref="mileageFormRef"
:model="mileageForm"
:rules="mileageRules"
label-position="right"
label-width="auto"
class="business-crud-page__mileage-form"
>
<el-form-item label="里程(公里)" prop="mileage">
<el-input
:model-value="mileageForm.mileage"
inputmode="numeric"
maxlength="10"
placeholder="请输入里程"
@input="handleMaintainMileageInput"
>
<template #append>公里</template>
</el-input>
</el-form-item>
<el-form-item label="备注" prop="mileageRemark">
<el-input
v-model="mileageForm.mileageRemark"
type="textarea"
:rows="3"
maxlength="200"
show-word-limit
placeholder="请输入里程维护备注"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="mileageDialog.visible = false">取消</el-button>
<el-button
type="primary"
:loading="mileageDialog.submitting"
@click="submitMileageMaintenance"
>
提交
</el-button>
</template>
</el-dialog>
<empty-pagination <empty-pagination
v-show="!isStandaloneFormPage" v-show="!isStandaloneFormPage"
:page="page" :page="page"
@@ -5507,6 +5563,26 @@ export default {
waybillRouteChangeRecords: [], waybillRouteChangeRecords: [],
waybillRouteChangeDragIndex: -1, waybillRouteChangeDragIndex: -1,
waybillRouteChangeSaving: false, waybillRouteChangeSaving: false,
mileageDialog: {
visible: false,
submitting: false,
row: null,
},
mileageForm: {
id: '',
mileage: '',
mileageRemark: '',
},
mileageRules: {
mileage: [
{ required: true, message: '请输入里程', trigger: 'blur' },
{
pattern: /^[1-9]\d{0,9}$/,
message: '里程必须为不超过10位的正整数',
trigger: 'blur',
},
],
},
waybillCommonAddressBox: false, waybillCommonAddressBox: false,
waybillCommonAddressLoading: false, waybillCommonAddressLoading: false,
waybillCommonAddressQuery: { waybillCommonAddressQuery: {
@@ -6717,6 +6793,15 @@ export default {
? status === 'dispatching' ? status === 'dispatching'
: status === 'processing'; : status === 'processing';
}, },
canMaintainMileage(row) {
return (
this.config.permission === 'waybill_manage' &&
this.hasPermission(`${this.config.permission}_mileage`) &&
typeof this.api.maintainMileage === 'function' &&
this.statusValue(row) === 'completed' &&
row.mileageMaintainable === true
);
},
canReassign(row) { canReassign(row) {
return ( return (
this.hasAction('reassign') && this.hasAction('reassign') &&
@@ -11572,6 +11657,46 @@ export default {
this.onLoad(this.page, this.query); this.onLoad(this.page, this.query);
}); });
}, },
openMileageDialog(row) {
this.mileageDialog = {
visible: true,
submitting: false,
row,
};
this.mileageForm = {
id: row.id,
mileage: row.mileage === null || row.mileage === undefined ? '' : String(row.mileage),
mileageRemark: row.mileageRemark || '',
};
this.$nextTick(() => this.$refs.mileageFormRef?.clearValidate());
},
handleMaintainMileageInput(value) {
this.mileageForm.mileage = String(value || '')
.replace(/\D/g, '')
.slice(0, 10);
},
async submitMileageMaintenance() {
const valid = await this.$refs.mileageFormRef?.validate().catch(() => false);
if (!valid || this.mileageDialog.submitting) return;
this.mileageDialog.submitting = true;
try {
await this.api.maintainMileage({
id: this.mileageForm.id,
mileage: Number(this.mileageForm.mileage),
mileageRemark: String(this.mileageForm.mileageRemark || '').trim(),
});
this.$message.success('里程维护成功');
this.mileageDialog.visible = false;
this.onLoad(this.page, this.query);
} finally {
this.mileageDialog.submitting = false;
}
},
resetMileageDialog() {
this.mileageDialog.row = null;
this.mileageForm = { id: '', mileage: '', mileageRemark: '' };
this.$refs.mileageFormRef?.clearValidate();
},
handleAction(action, row) { handleAction(action, row) {
const actionName = { const actionName = {
enable: '启用', enable: '启用',
@@ -15592,6 +15717,18 @@ export default {
top: 92px !important; top: 92px !important;
left: 0 !important; left: 0 !important;
} }
:global(.business-crud-page__mileage-dialog .business-crud-page__mileage-form) {
padding: 4px 20px 0;
}
:global(.business-crud-page__mileage-dialog .business-crud-page__mileage-form .el-form-item) {
margin-bottom: 16px;
}
:global(.business-crud-page__mileage-dialog .business-crud-page__mileage-form .el-input),
:global(.business-crud-page__mileage-dialog .business-crud-page__mileage-form .el-textarea) {
width: 100%;
}
.el-input__icon { .el-input__icon {
color: #1e90ff !important; color: #1e90ff !important;
} }
@@ -60,7 +60,7 @@
v-else-if="field.type === 'number' && editable" v-else-if="field.type === 'number' && editable"
v-model="form[field.prop]" v-model="form[field.prop]"
:min="0.000001" :min="0.000001"
:precision="6" :precision="2"
:controls="false" :controls="false"
/> />
<span v-else-if="field.money">{{ formatMoney(form[field.prop]) }}</span> <span v-else-if="field.money">{{ formatMoney(form[field.prop]) }}</span>
@@ -153,8 +153,7 @@
<span <span
v-else-if="isSummaryMoney(column.prop)" v-else-if="isSummaryMoney(column.prop)"
:class="{ :class="{
'formal-editor__negative-amount': 'formal-editor__negative-amount': isNegativeAdjustedAmount(row, column.prop),
column.prop === 'adjustAmount' && Number(row[column.prop] || 0) < 0,
}" }"
> >
{{ formatMoney(row[column.prop]) }} {{ formatMoney(row[column.prop]) }}
@@ -224,7 +223,10 @@
<template #default="{ row }"> <template #default="{ row }">
<span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span> <span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
<span v-else-if="column.prop === 'transportQuantity'"> <span v-else-if="column.prop === 'transportQuantity'">
{{ formatQuantity(row[column.prop]) }} {{ formatDetailTransportQuantity(row[column.prop]) }}
</span>
<span v-else-if="column.prop === 'mileage'">
{{ formatDetailMileage(row[column.prop]) }}
</span> </span>
<span v-else-if="column.prop === 'transportType'"> <span v-else-if="column.prop === 'transportType'">
{{ transportTypeName(row[column.prop]) }} {{ transportTypeName(row[column.prop]) }}
@@ -235,11 +237,9 @@
<el-table-column v-if="editable" label="操作" width="150" fixed="right" align="center"> <el-table-column v-if="editable" label="操作" width="150" fixed="right" align="center">
<template #default="{ row }"> <template #default="{ row }">
<div class="formal-editor__links"> <div class="formal-editor__links">
<el-link v-if="row.formalSettlementId" type="primary" @click="openAdjustDialog(row)" <el-link type="primary" @click="openAdjustDialog(row)">调整</el-link>
>调整</el-link
>
<el-link v-if="!row.sourcePreSettlementId" type="danger" @click="removeDetail(row)" <el-link v-if="!row.sourcePreSettlementId" type="danger" @click="removeDetail(row)"
>删除</el-link >踢出</el-link
> >
</div> </div>
</template> </template>
@@ -254,7 +254,13 @@
{{ missingAttachmentTypeText }} {{ missingAttachmentTypeText }}
</span> </span>
</template> </template>
<el-table :data="attachments" border> <div class="formal-editor__attachment-actions">
<el-button type="primary" :disabled="!attachments.length" @click="downloadAttachments">
批量下载
</el-button>
</div>
<el-table :data="attachments" border @selection-change="handleAttachmentSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="64" align="center" /> <el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column label="类型" min-width="180" align="center"> <el-table-column label="类型" min-width="180" align="center">
<template #default="{ row }"> <template #default="{ row }">
@@ -323,7 +329,7 @@
</section-card> </section-card>
<section-card title="发票信息"> <section-card title="发票信息">
<div v-if="editable" class="formal-editor__invoice-claim"> <div class="formal-editor__invoice-claim">
<el-form inline label-position="right" label-width="auto" @submit.prevent> <el-form inline label-position="right" label-width="auto" @submit.prevent>
<el-form-item label="发票号码"> <el-form-item label="发票号码">
<el-input <el-input
@@ -394,9 +400,7 @@
<div class="formal-editor__links"> <div class="formal-editor__links">
<el-link type="primary" @click="viewInvoiceAttachment(row)">查看</el-link> <el-link type="primary" @click="viewInvoiceAttachment(row)">查看</el-link>
<el-link type="primary" @click="downloadInvoiceAttachment(row)">下载</el-link> <el-link type="primary" @click="downloadInvoiceAttachment(row)">下载</el-link>
<el-link v-if="editable" type="danger" @click="removeInvoice($index)"> <el-link type="danger" @click="removeInvoice($index)">删除</el-link>
删除
</el-link>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
@@ -626,6 +630,39 @@
> >
</el-dialog> </el-dialog>
<el-dialog
v-model="detailAmountAdjust.visible"
title="结算明细调整"
width="520px"
append-to-body
>
<el-form
:model="detailAmountAdjust"
label-position="right"
label-width="auto"
class="formal-editor__amount-adjust"
>
<el-form-item label="原金额">
<span>{{ formatMoney(detailAmountAdjust.originalAmount) }}</span>
</el-form-item>
<el-form-item label="调整金额" required>
<el-input-number
v-model="detailAmountAdjust.adjustAmount"
:precision="2"
:step="0.01"
:controls="false"
/>
</el-form-item>
<el-form-item label="结算金额">
<span>{{ formatMoney(detailAmountAdjustSettlementAmount) }}</span>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="detailAmountAdjust.visible = false">取消</el-button>
<el-button type="primary" @click="confirmDetailAmountAdjustment">提交</el-button>
</template>
</el-dialog>
<el-dialog v-model="adjust.visible" title="结算明细调整" width="92%" append-to-body> <el-dialog v-model="adjust.visible" title="结算明细调整" width="92%" append-to-body>
<el-table v-loading="adjust.loading" :data="adjust.rows" border> <el-table v-loading="adjust.loading" :data="adjust.rows" border>
<el-table-column type="index" label="序号" width="64" align="center" /> <el-table-column type="index" label="序号" width="64" align="center" />
@@ -671,14 +708,6 @@
:precision="2" :precision="2"
:controls="false" /></template :controls="false" /></template
></el-table-column> ></el-table-column>
<el-table-column label="结算金额(不含税)" min-width="180" align="center"
><template #default="{ row }"
><el-input-number
v-model="row.settlementAmountNoTax"
:min="0"
:precision="2"
:controls="false" /></template
></el-table-column>
<el-table-column label="备注" min-width="180" align="center" <el-table-column label="备注" min-width="180" align="center"
><template #default="{ row }"><el-input v-model="row.remark" maxlength="200" /></template ><template #default="{ row }"><el-input v-model="row.remark" maxlength="200" /></template
></el-table-column> ></el-table-column>
@@ -723,6 +752,7 @@ import {
summaryColumns, summaryColumns,
} from '@/option/settlement/formalSettlementTable'; } from '@/option/settlement/formalSettlementTable';
import { getDictionary } from '@/api/system/dictbiz'; import { getDictionary } from '@/api/system/dictbiz';
import { h } from 'vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { downloadFileByUrl } from '@/utils/util'; import { downloadFileByUrl } from '@/utils/util';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
@@ -752,6 +782,7 @@ export default {
adjustments: [], adjustments: [],
changeRecords: [], changeRecords: [],
attachments: [], attachments: [],
selectedAttachmentRows: [],
invoices: [], invoices: [],
invoiceClaimNo: '', invoiceClaimNo: '',
invoiceClaimLoading: false, invoiceClaimLoading: false,
@@ -815,6 +846,12 @@ export default {
reason: '', reason: '',
rows: [], rows: [],
}, },
detailAmountAdjust: {
visible: false,
row: null,
originalAmount: 0,
adjustAmount: 0,
},
detailCollapsed: false, detailCollapsed: false,
detailQuery: { detailQuery: {
documentNo: '', documentNo: '',
@@ -865,6 +902,14 @@ export default {
summaryTotal() { summaryTotal() {
return this.summaryFees.reduce((total, row) => total + Number(row.settlementAmount || 0), 0); return this.summaryFees.reduce((total, row) => total + Number(row.settlementAmount || 0), 0);
}, },
detailAmountAdjustSettlementAmount() {
return Number(
(
Number(this.detailAmountAdjust.originalAmount || 0) +
Number(this.detailAmountAdjust.adjustAmount || 0)
).toFixed(2)
);
},
filteredDetails() { filteredDetails() {
return this.details.filter(row => return this.details.filter(row =>
Object.entries(this.appliedDetailQuery).every(([field, keyword]) => { Object.entries(this.appliedDetailQuery).every(([field, keyword]) => {
@@ -901,7 +946,14 @@ export default {
}, },
methods: { methods: {
async initialize() { async initialize() {
const newRecordAudit = this.recordId
? null
: {
createUserName: this.userInfo?.realName || this.userInfo?.userName || '',
createTime: this.$dayjs().format('YYYY-MM-DD HH:mm:ss'),
};
this.form = createFormalSettlementForm(); this.form = createFormalSettlementForm();
if (newRecordAudit) Object.assign(this.form, newRecordAudit);
this.sources = []; this.sources = [];
this.details = []; this.details = [];
this.summaryFees = []; this.summaryFees = [];
@@ -909,9 +961,16 @@ export default {
this.adjustments = []; this.adjustments = [];
this.changeRecords = []; this.changeRecords = [];
this.attachments = []; this.attachments = [];
this.selectedAttachmentRows = [];
this.invoices = []; this.invoices = [];
this.invoiceClaimNo = ''; this.invoiceClaimNo = '';
this.attachmentUploadFiles = []; this.attachmentUploadFiles = [];
this.detailAmountAdjust = {
visible: false,
row: null,
originalAmount: 0,
adjustAmount: 0,
};
this.detailCollapsed = false; this.detailCollapsed = false;
this.resetDetailQuery(false); this.resetDetailQuery(false);
await Promise.all([ await Promise.all([
@@ -923,6 +982,7 @@ export default {
if (!this.recordId) { if (!this.recordId) {
this.form.exchangeRateDate = this.$dayjs().format('YYYY-MM-DD'); this.form.exchangeRateDate = this.$dayjs().format('YYYY-MM-DD');
if (this.initialData) await this.applyInitialData(); if (this.initialData) await this.applyInitialData();
Object.assign(this.form, newRecordAudit);
await this.refreshFormalSettlementNo(); await this.refreshFormalSettlementNo();
return; return;
} }
@@ -945,6 +1005,7 @@ export default {
this.adjustments = data.adjustments || []; this.adjustments = data.adjustments || [];
this.changeRecords = data.changeRecords || []; this.changeRecords = data.changeRecords || [];
this.attachments = this.parseAttachments(data.attachmentsJson); this.attachments = this.parseAttachments(data.attachmentsJson);
this.selectedAttachmentRows = [];
this.invoices = data.invoices || []; this.invoices = data.invoices || [];
this.sortAttachments(); this.sortAttachments();
this.contracts = this.allContracts.filter( this.contracts = this.allContracts.filter(
@@ -1272,7 +1333,8 @@ export default {
this.buildSummaryFeesFromDetails(); this.buildSummaryFeesFromDetails();
this.detailCandidate.visible = false; this.detailCandidate.visible = false;
}, },
removeDetail(row) { async removeDetail(row) {
await this.$confirm('确认将该明细踢出正式结算单?', '提示', { type: 'warning' });
const index = this.details.indexOf(row); const index = this.details.indexOf(row);
if (index < 0) return; if (index < 0) return;
this.details.splice(index, 1); this.details.splice(index, 1);
@@ -1309,16 +1371,26 @@ export default {
isSummaryMoney(prop) { isSummaryMoney(prop) {
return ['originalAmount', 'adjustAmount', 'settlementAmount'].includes(prop); return ['originalAmount', 'adjustAmount', 'settlementAmount'].includes(prop);
}, },
isNegativeAdjustedAmount(row, prop) {
return (
Number(row.adjustAmount || 0) < 0 && ['adjustAmount', 'settlementAmount'].includes(prop)
);
},
recalculateSummaryRow(row) { recalculateSummaryRow(row) {
row.settlementAmount = Number(row.originalAmount || 0) + Number(row.adjustAmount || 0); row.settlementAmount = Number(row.originalAmount || 0) + Number(row.adjustAmount || 0);
}, },
getSummarySums({ columns, data }) { getSummarySums({ columns, data }) {
const sumProps = ['originalAmount', 'adjustAmount', 'settlementAmount']; const sumProps = ['originalAmount', 'adjustAmount', 'settlementAmount'];
const hasNegativeAdjustment = data.some(row => Number(row.adjustAmount || 0) < 0);
return columns.map((column, index) => { return columns.map((column, index) => {
if (index === 0) return '合计'; if (index === 0) return '合计';
if (!sumProps.includes(column.property)) return ''; if (!sumProps.includes(column.property)) return '';
const total = data.reduce((sum, row) => sum + Number(row[column.property] || 0), 0); const total = data.reduce((sum, row) => sum + Number(row[column.property] || 0), 0);
return this.formatMoney(total); const totalText = this.formatMoney(total);
if (column.property === 'settlementAmount' && hasNegativeAdjustment) {
return h('span', { style: { color: 'var(--el-color-danger)' } }, totalText);
}
return totalText;
}); });
}, },
buildSummaryFeesFromDetails() { buildSummaryFeesFromDetails() {
@@ -1406,6 +1478,20 @@ export default {
); );
}, },
async openAdjustDialog(row) { async openAdjustDialog(row) {
if (!row.formalSettlementId) {
const originalAmount = Number(
row.originalAmount ??
row.totalAmount ??
Number(row.settlementAmountTax || 0) - Number(row.adjustAmount || 0)
);
this.detailAmountAdjust = {
visible: true,
row,
originalAmount: Number(originalAmount.toFixed(2)),
adjustAmount: Number(row.adjustAmount || 0),
};
return;
}
this.adjust = { this.adjust = {
visible: true, visible: true,
loading: true, loading: true,
@@ -1425,6 +1511,20 @@ export default {
this.adjust.loading = false; this.adjust.loading = false;
} }
}, },
confirmDetailAmountAdjustment() {
const settlementAmount = this.detailAmountAdjustSettlementAmount;
if (!Number.isFinite(settlementAmount) || settlementAmount < 0) {
return this.$message.warning('调整后的结算金额不能小于0');
}
const row = this.detailAmountAdjust.row;
if (!row) return;
row.originalAmount = this.detailAmountAdjust.originalAmount;
row.adjustAmount = Number(this.detailAmountAdjust.adjustAmount || 0);
row.settlementAmountTax = settlementAmount;
row.pendingDetailAdjustment = true;
this.buildSummaryFeesFromDetails();
this.detailAmountAdjust.visible = false;
},
async saveAdjustment() { async saveAdjustment() {
if (!this.adjust.reason.trim()) return this.$message.warning('请输入调整原因'); if (!this.adjust.reason.trim()) return this.$message.warning('请输入调整原因');
this.adjust.saving = true; this.adjust.saving = true;
@@ -1586,6 +1686,17 @@ export default {
settlementType: this.form.settlementType, settlementType: this.form.settlementType,
sourcePreSettlementIds: this.form.sourcePreSettlementIds, sourcePreSettlementIds: this.form.sourcePreSettlementIds,
sourceDetailIds: this.form.sourceDetailIds, sourceDetailIds: this.form.sourceDetailIds,
detailAdjustments: this.details
.filter(row => row.pendingDetailAdjustment)
.map(row => ({
sourcePreSettlementDetailId:
row.sourcePreSettlementDetailId ||
(row.sourcePreSettlementId ? row.id : undefined),
sourceDetailId: row.sourcePreSettlementId
? undefined
: row.sourceDetailId || row.id,
adjustAmount: Number(row.adjustAmount || 0),
})),
exchangeRateDate: this.form.exchangeRateDate, exchangeRateDate: this.form.exchangeRateDate,
exchangeRate: this.form.exchangeRate, exchangeRate: this.form.exchangeRate,
summaryFees: this.summaryFees summaryFees: this.summaryFees
@@ -1639,6 +1750,12 @@ export default {
const quantity = Number(value); const quantity = Number(value);
return Number.isFinite(quantity) ? quantity.toFixed(2) : '-'; return Number.isFinite(quantity) ? quantity.toFixed(2) : '-';
}, },
formatDetailTransportQuantity(value) {
return this.readonly && Number(value) === -1 ? '-' : this.formatQuantity(value);
},
formatDetailMileage(value) {
return this.readonly && Number(value) === -1 ? '-' : this.displayValue(value);
},
async loadTransportTypeOptions() { async loadTransportTypeOptions() {
const { data } = await getDictionary({ code: 'transport_type' }); const { data } = await getDictionary({ code: 'transport_type' });
this.transportTypeOptions = data?.data || []; this.transportTypeOptions = data?.data || [];
@@ -1736,12 +1853,46 @@ export default {
}); });
this.sortAttachments(); this.sortAttachments();
}, },
handleAttachmentSelectionChange(rows) {
this.selectedAttachmentRows = rows || [];
},
removeAttachment(index) { removeAttachment(index) {
this.attachments.splice(index, 1); this.attachments.splice(index, 1);
this.selectedAttachmentRows = this.selectedAttachmentRows.filter(file =>
this.attachments.includes(file)
);
}, },
getAttachmentUrl(file = {}) { getAttachmentUrl(file = {}) {
return file.url || file.link || file.src || file.domain || ''; return file.url || file.link || file.src || file.domain || '';
}, },
getAttachmentName(file = {}) {
return (
file.originalName || file.name || this.getAttachmentFileName(this.getAttachmentUrl(file))
);
},
downloadAttachment(file) {
const url = this.getAttachmentUrl(file);
if (!url) {
this.$message.warning('附件地址为空,无法下载');
return;
}
downloadFileByUrl(url, this.getAttachmentName(file) || '附件');
},
downloadAttachments() {
const files = this.selectedAttachmentRows.length
? this.selectedAttachmentRows
: this.attachments;
const downloadableFiles = files.filter(file => this.getAttachmentUrl(file));
if (!downloadableFiles.length) {
this.$message.warning(
this.selectedAttachmentRows.length ? '所选附件无法下载' : '暂无可下载附件'
);
return;
}
downloadableFiles.forEach((file, index) => {
window.setTimeout(() => this.downloadAttachment(file), index * 200);
});
},
previewAttachment(file) { previewAttachment(file) {
const url = this.getAttachmentUrl(file); const url = this.getAttachmentUrl(file);
if (!url) return this.$message.warning('附件地址为空,无法预览'); if (!url) return this.$message.warning('附件地址为空,无法预览');
@@ -1844,7 +1995,7 @@ export default {
} }
.formal-editor__detail-filter-actions { .formal-editor__detail-filter-actions {
grid-column: 1 / -1; grid-column: 1 / -1;
justify-self: start; justify-self: end;
} }
.formal-editor__page-actions { .formal-editor__page-actions {
display: flex; display: flex;
@@ -1892,12 +2043,20 @@ export default {
.formal-editor__adjust-reason { .formal-editor__adjust-reason {
margin-top: 16px; margin-top: 16px;
} }
.formal-editor__amount-adjust :deep(.el-input-number) {
width: 100%;
}
.formal-editor__attachment-missing { .formal-editor__attachment-missing {
margin-left: 12px; margin-left: 12px;
color: var(--el-color-danger); color: var(--el-color-danger);
font-size: 13px; font-size: 13px;
font-weight: 400; font-weight: 400;
} }
.formal-editor__attachment-actions {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
}
.formal-editor__attachment-upload { .formal-editor__attachment-upload {
margin-top: 12px; margin-top: 12px;
} }
@@ -77,9 +77,10 @@
>{{ displayValue(row[column.prop]) }}</el-tag >{{ displayValue(row[column.prop]) }}</el-tag
> >
<span v-else-if="column.money">{{ formatMoney(row[column.prop], row.currency) }}</span> <span v-else-if="column.money">{{ formatMoney(row[column.prop], row.currency) }}</span>
<span v-else-if="column.prop === 'invoiceStatusName'">{{ <span
invoiceName(row.invoiceStatus) v-else-if="column.prop === 'invoiceStatusName'"
}}</span> :class="['fs-table-panel__invoice-status', `is-${row.invoiceStatus || 'unreceived'}`]"
>{{ invoiceName(row.invoiceStatus, row.settlementType) }}</span>
<span v-else-if="column.prop === 'paymentStatusName'">{{ <span v-else-if="column.prop === 'paymentStatusName'">{{
paymentName(row.paymentStatus) paymentName(row.paymentStatus)
}}</span> }}</span>
@@ -120,7 +121,7 @@
<script> <script>
import { Refresh } from '@element-plus/icons-vue'; import { Refresh } from '@element-plus/icons-vue';
import { import {
invoiceStatusOptions, invoiceStatusName,
kingdeeSyncStatusOptions, kingdeeSyncStatusOptions,
paymentStatusOptions, paymentStatusOptions,
} from '@/option/settlement/formalSettlementSearch'; } from '@/option/settlement/formalSettlementSearch';
@@ -210,8 +211,8 @@ export default {
'' ''
); );
}, },
invoiceName(value) { invoiceName(value, settlementType) {
return invoiceStatusOptions.find(item => item.value === value)?.label || value || '-'; return invoiceStatusName(value, settlementType);
}, },
paymentName(value) { paymentName(value) {
return paymentStatusOptions.find(item => item.value === value)?.label || value || '-'; return paymentStatusOptions.find(item => item.value === value)?.label || value || '-';
@@ -247,6 +248,17 @@ export default {
justify-content: center; justify-content: center;
gap: 8px; gap: 8px;
} }
.fs-table-panel__invoice-status {
&.is-unreceived {
color: #f56c6c;
}
&.is-partial {
color: #e6a23c;
}
&.is-completed {
color: #67c23a;
}
}
.fs-table-panel__pagination { .fs-table-panel__pagination {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
@@ -51,7 +51,7 @@
v-else-if="field.type === 'number'" v-else-if="field.type === 'number'"
v-model="form[field.prop]" v-model="form[field.prop]"
:min="0" :min="0"
:precision="6" :precision="2"
:controls="false" :controls="false"
:disabled="!editable || form.currency === 'RMB'" :disabled="!editable || form.currency === 'RMB'"
@change="recalculateLocalAmount" @change="recalculateLocalAmount"
@@ -104,10 +104,10 @@
@change="handleManualFeeTypeChange(row)" @change="handleManualFeeTypeChange(row)"
> >
<el-option <el-option
v-for="item in feeOptions" v-for="item in feeCategoryOptions"
:key="item.feeType" :key="item.id || item.dictValue"
:label="item.feeTypeName || item.feeType" :label="item.dictKey || item.dictValue"
:value="item.feeType" :value="item.dictValue"
/> />
</el-select> </el-select>
<el-select <el-select
@@ -899,14 +899,25 @@ export default {
currency: first.currency || 'RMB', currency: first.currency || 'RMB',
localCurrency: first.localCurrency || 'RMB', localCurrency: first.localCurrency || 'RMB',
}); });
this.details = rows.map(row => ({ this.details = rows.map(row => this.normalizeSourceDetail(row));
this.buildSummaryFeesFromDetails();
},
normalizeSourceDetail(row) {
const originalAmount =
row.originalAmount ?? row.totalAmount ?? row.afterAmount ?? row.settlementAmount ?? 0;
return {
...row, ...row,
sourceDetailId: row.sourceDetailId || row.id, sourceDetailId: row.sourceDetailId || row.id,
originalAmount,
adjustAmount: row.adjustAmount ?? 0,
settlementAmountTax: settlementAmountTax:
row.settlementAmountTax ?? row.totalAmount ?? row.afterAmount ?? row.settlementAmount, row.settlementAmountTax ??
row.totalAmount ??
row.afterAmount ??
row.settlementAmount ??
originalAmount,
feeItemsJson: row.feeItemsJson || JSON.stringify(row.feeItems || {}), feeItemsJson: row.feeItemsJson || JSON.stringify(row.feeItems || {}),
})); };
this.buildSummaryFeesFromDetails();
}, },
resetEditor() { resetEditor() {
this.form = emptyPreSettlementForm(); this.form = emptyPreSettlementForm();
@@ -1054,7 +1065,7 @@ export default {
sourceDetailIds: this.details.map(row => row.sourceDetailId || row.id), sourceDetailIds: this.details.map(row => row.sourceDetailId || row.id),
summaryFees: this.summaryFees.map(row => ({ summaryFees: this.summaryFees.map(row => ({
id: row.id || undefined, id: row.id || undefined,
feeType: row.feeType || '', feeType: this.feeCategoryValue(row.feeType),
feeItem: row.feeItem, feeItem: row.feeItem,
originalAmount: Number(row.originalAmount || 0), originalAmount: Number(row.originalAmount || 0),
adjustAmount: Number(row.adjustAmount || 0), adjustAmount: Number(row.adjustAmount || 0),
@@ -1079,14 +1090,25 @@ export default {
if (!this.manualFeeItems(row.feeType).includes(row.feeItem)) row.feeItem = ''; if (!this.manualFeeItems(row.feeType).includes(row.feeItem)) row.feeItem = '';
}, },
manualFeeItems(feeType) { manualFeeItems(feeType) {
return this.feeOptions.find(item => item.feeType === feeType)?.feeItems || []; return (
this.feeOptions.find(
item => this.feeCategoryValue(item.feeType) === this.feeCategoryValue(feeType)
)?.feeItems || []
);
},
feeCategoryValue(value) {
if (value === undefined || value === null || value === '') return '';
const option = this.feeCategoryOptions.find(
item => String(item.dictKey) === String(value) || String(item.dictValue) === String(value)
);
return option?.dictValue || value;
}, },
feeCategoryName(value) { feeCategoryName(value) {
if (value === undefined || value === null || value === '') return ''; if (value === undefined || value === null || value === '') return '';
const option = this.feeCategoryOptions.find( const option = this.feeCategoryOptions.find(
item => String(item.dictKey) === String(value) || String(item.dictValue) === String(value) item => String(item.dictKey) === String(value) || String(item.dictValue) === String(value)
); );
return option?.dictValue || value; return option?.dictKey || value;
}, },
transportTypeName(value) { transportTypeName(value) {
if (value === undefined || value === null || value === '') return ''; if (value === undefined || value === null || value === '') return '';
@@ -1124,7 +1146,7 @@ export default {
const feeOption = this.feeOptions.find(item => const feeOption = this.feeOptions.find(item =>
(item.feeItems || []).some(name => String(name) === String(feeItem)) (item.feeItems || []).some(name => String(name) === String(feeItem))
); );
const feeType = feeOption?.feeType || fallbackFeeType || ''; const feeType = this.feeCategoryValue(feeOption?.feeType || fallbackFeeType);
const key = `${feeType}\u0000${feeItem}`; const key = `${feeType}\u0000${feeItem}`;
const current = summaryMap.get(key) || { const current = summaryMap.get(key) || {
id: '', id: '',
@@ -1239,7 +1261,7 @@ export default {
const existingIds = new Set(this.details.map(row => String(row.sourceDetailId || row.id))); const existingIds = new Set(this.details.map(row => String(row.sourceDetailId || row.id)));
this.candidateSelection.forEach(row => { this.candidateSelection.forEach(row => {
if (!existingIds.has(String(row.id))) { if (!existingIds.has(String(row.id))) {
this.details.push({ ...row, sourceDetailId: row.id }); this.details.push(this.normalizeSourceDetail(row));
} }
}); });
this.candidateDialog.confirming = true; this.candidateDialog.confirming = true;
@@ -42,6 +42,8 @@
? settlementTypeName ? settlementTypeName
: field.prop === 'formalSettlementId' : field.prop === 'formalSettlementId'
? form.formalSettlementNo || form.formalSettlementId ? form.formalSettlementNo || form.formalSettlementId
: field.prop === 'createTime'
? formatCreateDate(form[field.prop])
: form[field.prop] : form[field.prop]
) )
}}</span></el-form-item }}</span></el-form-item
@@ -68,23 +70,55 @@
v-if="editable && form.formalSettlementId" v-if="editable && form.formalSettlementId"
type="primary" type="primary"
plain plain
@click="openFeeDialog" @click="addFeeRow"
>添加费用</el-button >添加费用</el-button
> >
</template> </template>
<el-table :data="details" border> <el-table :data="details" border>
<el-table-column type="index" label="序号" width="64" align="center" /><el-table-column <el-table-column type="index" label="序号" width="64" align="center" />
prop="feeType" <el-table-column prop="feeType" label="费用类型" min-width="180" align="center">
label="费用类型" <template #default="{ row }">
min-width="150" <el-select
align="center" v-if="editable && row.manualFlag === 1"
/><el-table-column v-model="row.feeType"
prop="feeItem" filterable
label="费用项目" allow-create
min-width="180" default-first-option
align="center" clearable
show-overflow-tooltip placeholder="请选择或输入"
/><el-table-column >
<el-option
v-for="item in feeOptions"
:key="item.feeType"
:label="item.feeTypeName || item.feeType"
:value="item.feeType"
/>
</el-select>
<span v-else>{{ displayValue(feeTypeName(row.feeType)) }}</span>
</template>
</el-table-column>
<el-table-column prop="feeItem" label="费用项目" min-width="200" align="center">
<template #default="{ row }">
<el-select
v-if="editable && row.manualFlag === 1"
v-model="row.feeItem"
filterable
allow-create
default-first-option
clearable
placeholder="请选择或输入"
>
<el-option
v-for="item in feeItemOptions(row.feeType)"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<span v-else>{{ displayValue(row.feeItem) }}</span>
</template>
</el-table-column>
<el-table-column
prop="originalAmountTax" prop="originalAmountTax"
label="原金额(含税)" label="原金额(含税)"
min-width="145" min-width="145"
@@ -98,7 +132,9 @@
><el-input-number ><el-input-number
v-if="editable" v-if="editable"
v-model="row.adjustmentAmountTax" v-model="row.adjustmentAmountTax"
:class="{ negative: Number(row.adjustmentAmountTax) < 0 }"
:precision="2" :precision="2"
:step="0.01"
:controls="false" :controls="false"
@change="recalculate" @change="recalculate"
/><span v-else :class="{ negative: Number(row.adjustmentAmountTax) < 0 }">{{ /><span v-else :class="{ negative: Number(row.adjustmentAmountTax) < 0 }">{{
@@ -134,7 +170,7 @@
></template ></template
></el-table-column ></el-table-column
> >
<template #empty><el-empty description="请选择调整费用明细" /></template> <template #empty><el-empty description="暂无调整费用" /></template>
</el-table> </el-table>
<div class="settlement-adjustment-editor__summary"> <div class="settlement-adjustment-editor__summary">
调整金额合计<strong :class="{ negative: Number(form.adjustmentAmount) < 0 }">{{ 调整金额合计<strong :class="{ negative: Number(form.adjustmentAmount) < 0 }">{{
@@ -210,53 +246,19 @@
</div> </div>
<template #footer <template #footer
><el-button @click="visible = false">取消</el-button ><el-button @click="visible = false">取消</el-button
><el-button
v-if="editable"
type="primary"
plain
:loading="syncing"
:disabled="!form.formalSettlementId"
@click="handleSync"
>同步</el-button
><el-button v-if="editable" type="primary" :loading="saving" @click="handleSave" ><el-button v-if="editable" type="primary" :loading="saving" @click="handleSave"
>保存</el-button >保存</el-button
></template ></template
> >
<el-dialog v-model="feeDialog.visible" title="选择调整费用" width="88%" append-to-body>
<el-table
ref="feeTable"
:data="feeRows"
border
@selection-change="feeDialog.selected = $event"
><el-table-column type="selection" width="52" align="center" /><el-table-column
type="index"
label="序号"
width="64"
align="center"
/><el-table-column
prop="documentNo"
label="单据号"
min-width="150"
align="center"
/><el-table-column
prop="feeType"
label="费用类型"
min-width="140"
align="center"
/><el-table-column
prop="feeItem"
label="费用项目"
min-width="180"
align="center"
show-overflow-tooltip
/><el-table-column
prop="originalAmountTax"
label="原金额(含税)"
min-width="145"
align="center"
><template #default="{ row }">{{
formatMoney(row.originalAmountTax)
}}</template></el-table-column
></el-table
>
<template #footer
><el-button @click="feeDialog.visible = false">取消</el-button
><el-button type="primary" @click="confirmFees">确定</el-button></template
>
</el-dialog>
</el-dialog> </el-dialog>
</template> </template>
@@ -274,6 +276,7 @@ export default {
data: () => ({ data: () => ({
loading: false, loading: false,
saving: false, saving: false,
syncing: false,
form: createSettlementAdjustmentForm(), form: createSettlementAdjustmentForm(),
fields: settlementAdjustmentFormFields, fields: settlementAdjustmentFormFields,
details: [], details: [],
@@ -296,11 +299,10 @@ export default {
'zip', 'zip',
], ],
candidates: [], candidates: [],
feeRows: [], feeOptions: [],
rules: { rules: {
formalSettlementId: [{ required: true, message: '请选择关联正式结算单', trigger: 'change' }], formalSettlementId: [{ required: true, message: '请选择关联正式结算单', trigger: 'change' }],
}, },
feeDialog: { visible: false, selected: [] },
}), }),
computed: { computed: {
visible: { visible: {
@@ -333,22 +335,29 @@ export default {
methods: { methods: {
async initialize() { async initialize() {
this.form = createSettlementAdjustmentForm(); this.form = createSettlementAdjustmentForm();
const userInfo = this.$store.getters.userInfo || {};
this.form.createTime = this.$dayjs().format('YYYY-MM-DD');
this.form.createUserName = userInfo.realName || userInfo.userName || userInfo.account || '';
this.details = []; this.details = [];
this.attachments = []; this.attachments = [];
this.selectedAttachments = []; this.selectedAttachments = [];
this.candidates = []; this.candidates = [];
this.feeRows = []; this.feeOptions = [];
if (!this.recordId) { if (!this.recordId) {
await this.loadFormalSettlements(''); await Promise.all([this.loadFormalSettlements(''), this.loadFeeOptions()]);
return; return;
} }
this.loading = true; this.loading = true;
try { try {
const response = await api.getDetail(this.recordId); const [response] = await Promise.all([api.getDetail(this.recordId), this.loadFeeOptions()]);
const data = response?.data?.data || response?.data || response || {}; const data = response?.data?.data || response?.data || response || {};
this.form = { ...createSettlementAdjustmentForm(), ...data }; this.form = { ...createSettlementAdjustmentForm(), ...data };
this.details = (data.details || []).map(item => ({ this.details = (data.details || []).map(item => ({
...item, ...item,
manualFlag:
item.formalSettlementDetailId == null && item.formalSettlementDetailFeeId == null
? 1
: 0,
adjustmentAmountTax: Number(item.adjustmentAmountTax || 0), adjustmentAmountTax: Number(item.adjustmentAmountTax || 0),
adjustmentAmountNoTax: adjustmentAmountNoTax:
item.adjustmentAmountNoTax == null ? null : Number(item.adjustmentAmountNoTax), item.adjustmentAmountNoTax == null ? null : Number(item.adjustmentAmountNoTax),
@@ -363,6 +372,10 @@ export default {
const { data } = await api.getFormalSettlements(keyword); const { data } = await api.getFormalSettlements(keyword);
this.candidates = data?.data || data || []; this.candidates = data?.data || data || [];
}, },
async loadFeeOptions() {
const { data } = await api.getFeeOptions();
this.feeOptions = data?.data || data || [];
},
async handleFormalChange(id) { async handleFormalChange(id) {
if (!id) { if (!id) {
Object.assign(this.form, { Object.assign(this.form, {
@@ -397,6 +410,7 @@ export default {
if (String(this.form.formalSettlementId) !== String(formalSettlementId)) return; if (String(this.form.formalSettlementId) !== String(formalSettlementId)) return;
this.details = (data?.data || data || []).map(fee => ({ this.details = (data?.data || data || []).map(fee => ({
...fee, ...fee,
manualFlag: 0,
originalAmountTax: Number(fee.originalAmountTax || 0), originalAmountTax: Number(fee.originalAmountTax || 0),
adjustmentAmountTax: 0, adjustmentAmountTax: 0,
adjustmentAmountNoTax: null, adjustmentAmountNoTax: null,
@@ -407,25 +421,28 @@ export default {
this.loading = false; this.loading = false;
} }
}, },
async openFeeDialog() { addFeeRow() {
const { data } = await api.getFormalDetails(this.form.formalSettlementId); this.details.push({
const used = new Set(this.details.map(item => String(item.formalSettlementDetailFeeId))); formalSettlementDetailId: null,
this.feeRows = (data?.data || data || []).filter( formalSettlementDetailFeeId: null,
item => !used.has(String(item.formalSettlementDetailFeeId)) feeType: '',
); feeItem: '',
this.feeDialog = { visible: true, selected: [] }; originalAmountTax: 0,
adjustmentAmountTax: 0,
adjustmentAmountNoTax: null,
remark: '',
manualFlag: 1,
});
}, },
confirmFees() { feeTypeName(value) {
this.feeDialog.selected.forEach(item => return (
this.details.push({ this.feeOptions.find(item => String(item.feeType) === String(value))?.feeTypeName || value
...item, );
adjustmentAmountTax: 0, },
adjustmentAmountNoTax: null, feeItemOptions(feeType) {
remark: '', return (
}) this.feeOptions.find(item => String(item.feeType) === String(feeType))?.feeItems || []
); );
this.recalculate();
this.feeDialog.visible = false;
}, },
recalculate() { recalculate() {
const total = this.details.reduce( const total = this.details.reduce(
@@ -437,13 +454,65 @@ export default {
(Number(this.form.originalSettlementAmount || 0) + total).toFixed(2) (Number(this.form.originalSettlementAmount || 0) + total).toFixed(2)
); );
}, },
async handleSync() {
const formalSettlementId = this.form.formalSettlementId;
if (!formalSettlementId) return this.$message.warning('请先选择正式结算单');
this.syncing = true;
try {
const [candidateResponse, detailResponse] = await Promise.all([
api.getFormalSettlements(this.form.formalSettlementNo || ''),
api.getFormalDetails(formalSettlementId),
]);
if (String(this.form.formalSettlementId) !== String(formalSettlementId)) return;
const candidateData = candidateResponse?.data?.data || candidateResponse?.data || [];
const formalSettlements = Array.isArray(candidateData) ? candidateData : [];
const formalSettlement = formalSettlements.find(
item => String(item.id) === String(formalSettlementId)
);
if (formalSettlement) {
const { id, ...formalInformation } = formalSettlement;
Object.assign(this.form, formalInformation, {
formalSettlementId: id,
formalSettlementNo: formalSettlement.formalSettlementNo,
originalSettlementAmount: Number(formalSettlement.settlementAmount || 0),
settlementTypeName: formalSettlement.settlementTypeName,
});
}
const existingDetailMap = new Map(
this.details
.filter(item => item.formalSettlementDetailFeeId != null)
.map(item => [String(item.formalSettlementDetailFeeId), item])
);
const manualDetails = this.details.filter(item => Number(item.manualFlag) === 1);
const detailData = detailResponse?.data?.data || detailResponse?.data || [];
const formalDetails = Array.isArray(detailData) ? detailData : [];
this.details = formalDetails
.map(fee => {
const existing = existingDetailMap.get(String(fee.formalSettlementDetailFeeId));
return {
...fee,
manualFlag: 0,
originalAmountTax: Number(fee.originalAmountTax || 0),
adjustmentAmountTax: Number(existing?.adjustmentAmountTax || 0),
adjustmentAmountNoTax:
existing?.adjustmentAmountNoTax == null
? null
: Number(existing.adjustmentAmountNoTax),
remark: existing?.remark || '',
};
})
.concat(manualDetails);
this.recalculate();
this.$message.success('同步成功');
} finally {
this.syncing = false;
}
},
async handleSave() { async handleSave() {
await this.$refs.formRef.validate();
if (!this.details.length) return this.$message.warning('请至少添加一条调整费用');
this.saving = true; this.saving = true;
try { try {
await api.save({ const { data } = await api.save({
...(this.recordId ? { id: this.form.id } : {}), id: this.form.id || undefined,
formalSettlementId: this.form.formalSettlementId, formalSettlementId: this.form.formalSettlementId,
attachmentsJson: JSON.stringify(this.attachments || []), attachmentsJson: JSON.stringify(this.attachments || []),
remark: this.form.remark, remark: this.form.remark,
@@ -452,11 +521,13 @@ export default {
formalSettlementDetailFeeId: item.formalSettlementDetailFeeId, formalSettlementDetailFeeId: item.formalSettlementDetailFeeId,
feeType: item.feeType, feeType: item.feeType,
feeItem: item.feeItem, feeItem: item.feeItem,
originalAmountTax: item.originalAmountTax,
adjustmentAmountTax: item.adjustmentAmountTax, adjustmentAmountTax: item.adjustmentAmountTax,
adjustmentAmountNoTax: item.adjustmentAmountNoTax, adjustmentAmountNoTax: item.adjustmentAmountNoTax,
remark: item.remark, remark: item.remark,
})), })),
}); });
this.form.id = data?.data || data || this.form.id;
this.$message.success('保存成功'); this.$message.success('保存成功');
this.visible = false; this.visible = false;
this.$emit('success'); this.$emit('success');
@@ -470,6 +541,11 @@ export default {
formatMoney(value) { formatMoney(value) {
return Number(value || 0).toFixed(2); return Number(value || 0).toFixed(2);
}, },
formatCreateDate(value) {
if (!value) return '';
const date = this.$dayjs(value);
return date.isValid() ? date.format('YYYY-MM-DD') : value;
},
handleAttachmentChange(files) { handleAttachmentChange(files) {
const userInfo = this.$store.getters.userInfo || {}; const userInfo = this.$store.getters.userInfo || {};
const uploadUserName = userInfo.realName || userInfo.userName || ''; const uploadUserName = userInfo.realName || userInfo.userName || '';
@@ -573,4 +649,7 @@ export default {
.negative { .negative {
color: #f56c6c; color: #f56c6c;
} }
.settlement-adjustment-editor :deep(.el-input-number.negative .el-input__inner) {
color: #f56c6c;
}
</style> </style>
+4 -4
View File
@@ -164,8 +164,8 @@ import { createSettlementTransfer } from '@/utils/settlement-transfer';
import * as api from '@/api/settlement/formalSettlement'; import * as api from '@/api/settlement/formalSettlement';
import * as paymentApi from '@/api/payment/paymentApplication'; import * as paymentApi from '@/api/payment/paymentApplication';
import { import {
invoiceStatusName,
formalSettlementSearchFields, formalSettlementSearchFields,
invoiceStatusOptions,
paymentStatusOptions, paymentStatusOptions,
} from '@/option/settlement/formalSettlementSearch'; } from '@/option/settlement/formalSettlementSearch';
import { formalSettlementTableColumns } from '@/option/settlement/formalSettlementTable'; import { formalSettlementTableColumns } from '@/option/settlement/formalSettlementTable';
@@ -505,7 +505,7 @@ export default {
原币结算金额: item.settlementAmount, 原币结算金额: item.settlementAmount,
本位币结算金额: item.localSettlementAmount, 本位币结算金额: item.localSettlementAmount,
结算汇率: item.exchangeRate, 结算汇率: item.exchangeRate,
发票状态: this.invoiceName(item.invoiceStatus), 发票状态: this.invoiceName(item.invoiceStatus, item.settlementType),
收付款状态: this.paymentName(item.paymentStatus), 收付款状态: this.paymentName(item.paymentStatus),
审核状态: item.approvalStatusName, 审核状态: item.approvalStatusName,
金蝶单据号: item.kingdeeBillNo, 金蝶单据号: item.kingdeeBillNo,
@@ -550,8 +550,8 @@ export default {
'' ''
); );
}, },
invoiceName(value) { invoiceName(value, settlementType) {
return invoiceStatusOptions.find(item => item.value === value)?.label || value || '-'; return invoiceStatusName(value, settlementType);
}, },
paymentName(value) { paymentName(value) {
return paymentStatusOptions.find(item => item.value === value)?.label || value || '-'; return paymentStatusOptions.find(item => item.value === value)?.label || value || '-';
@@ -190,7 +190,7 @@
<div <div
class="settlement-detail-page__detail-panel-body settlement-detail-page__adjust-panel-body" class="settlement-detail-page__detail-panel-body settlement-detail-page__adjust-panel-body"
> >
<div v-if="isReceivable" class="settlement-detail-page__adjust-toolbar"> <div v-if="isPayable" class="settlement-detail-page__adjust-toolbar">
<el-button type="primary" :disabled="adjustDialog.loading" @click="addAdjustFee"> <el-button type="primary" :disabled="adjustDialog.loading" @click="addAdjustFee">
新增费用 新增费用
</el-button> </el-button>
@@ -209,18 +209,26 @@
show-overflow-tooltip show-overflow-tooltip
> >
<template #default="{ row }"> <template #default="{ row }">
<span <el-select
v-if="row.manualFee && ['billingFactor', 'billingType'].includes(column.prop)" v-if="column.prop === 'cargoName' && row.manualFee"
>
-
</span>
<el-input
v-else-if="column.prop === 'cargoName' && row.manualFee"
v-model="row.cargoName" v-model="row.cargoName"
class="settlement-detail-page__adjust-control"
:loading="commonCargoLoading"
clearable clearable
maxlength="100" filterable
placeholder="请输入" allow-create
/> default-first-option
placeholder="请选择或输入"
@visible-change="visible => visible && loadCommonCargoOptions()"
@change="value => handleAdjustCargoNameChange(row, value)"
>
<el-option
v-for="item in manualCargoNameOptions(row)"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<span v-else-if="column.prop === 'cargoName'"> <span v-else-if="column.prop === 'cargoName'">
{{ formatDetailCell(row, column.prop) }} {{ formatDetailCell(row, column.prop) }}
</span> </span>
@@ -241,6 +249,56 @@
<span v-else-if="column.prop === 'cargoType'"> <span v-else-if="column.prop === 'cargoType'">
{{ formatDetailCell(row, column.prop) }} {{ formatDetailCell(row, column.prop) }}
</span> </span>
<el-select
v-else-if="column.prop === 'specification' && row.manualFee"
v-model="row.specification"
class="settlement-detail-page__adjust-control"
:loading="commonCargoLoading"
clearable
filterable
allow-create
default-first-option
placeholder="请选择或输入"
@visible-change="visible => visible && loadCommonCargoOptions()"
@change="value => handleAdjustSpecificationChange(row, value)"
>
<el-option
v-for="item in manualSpecificationOptions(row)"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-select
v-else-if="column.prop === 'billingFactor' && row.manualFee"
v-model="row.billingFactor"
class="settlement-detail-page__adjust-control"
clearable
placeholder="请选择"
@change="handleAdjustBillingFactorChange(row)"
>
<el-option
v-for="item in adjustBillingElements"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<el-select
v-else-if="column.prop === 'billingType' && row.manualFee"
v-model="row.billingType"
class="settlement-detail-page__adjust-control"
:disabled="!row.billingFactor"
clearable
placeholder="请选择"
>
<el-option
v-for="item in adjustBillingTypes(row)"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<el-select <el-select
v-else-if="column.prop === 'priceUnit'" v-else-if="column.prop === 'priceUnit'"
v-model="row.priceUnit" v-model="row.priceUnit"
@@ -327,7 +385,12 @@
</div> </div>
<footer class="settlement-detail-page__detail-panel-footer"> <footer class="settlement-detail-page__detail-panel-footer">
<el-button @click="closeAdjustPanel">取消</el-button> <el-button @click="closeAdjustPanel">取消</el-button>
<el-button type="primary" :loading="adjustDialog.submitting" @click="saveAdjustFee"> <el-button
type="primary"
:loading="adjustDialog.submitting"
:disabled="hasAdjustCalculationPending"
@click="saveAdjustFee"
>
保存 保存
</el-button> </el-button>
</footer> </footer>
@@ -392,7 +455,6 @@
v-model="transferForm.settlementBillType" v-model="transferForm.settlementBillType"
@change="handleTransferTypeChange" @change="handleTransferTypeChange"
> >
<el-radio label="pre">预结算单</el-radio>
<el-radio label="formal">正式结算单</el-radio> <el-radio label="formal">正式结算单</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
@@ -621,6 +683,7 @@ import {
} from '@/option/settlement/receivable-payable-detail'; } from '@/option/settlement/receivable-payable-detail';
import * as api from '@/api/settlement/receivable-payable-detail'; import * as api from '@/api/settlement/receivable-payable-detail';
import { getList as getContractList } from '@/api/business/contract-manage'; import { getList as getContractList } from '@/api/business/contract-manage';
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
import { getContractOptions as getSettlementContractOptions } from '@/api/settlement/preSettlement'; import { getContractOptions as getSettlementContractOptions } from '@/api/settlement/preSettlement';
import { getList as getCargoTypeList } from '@/api/base/cargo-type'; import { getList as getCargoTypeList } from '@/api/base/cargo-type';
import { exportBlob } from '@/api/common'; import { exportBlob } from '@/api/common';
@@ -628,6 +691,26 @@ import { getDictionary } from '@/api/system/dictbiz';
import { downloadXls } from '@/utils/util'; import { downloadXls } from '@/utils/util';
import { createSettlementTransfer } from '@/utils/settlement-transfer'; import { createSettlementTransfer } from '@/utils/settlement-transfer';
const ADJUST_BILLING_ELEMENTS = [
'按重量',
'按体积',
'按车辆',
'按里程',
'按吨·公里',
'固定金额(整单一口价)',
'按数量',
];
const ADJUST_BILLING_TYPE_MAP = {
按重量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
按体积: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
按车辆: ['固定单价'],
按里程: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
'按吨·公里': ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
'固定金额(整单一口价)': ['固定一口价'],
按数量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
};
export default { export default {
props: { props: {
settlementType: { settlementType: {
@@ -667,6 +750,10 @@ export default {
adjustRows: [], adjustRows: [],
adjustDynamicFeeColumns: [], adjustDynamicFeeColumns: [],
adjustTextProps: ['specification', 'model', 'billingFactor', 'billingType'], adjustTextProps: ['specification', 'model', 'billingFactor', 'billingType'],
adjustBillingElements: ADJUST_BILLING_ELEMENTS,
commonCargoOptions: [],
commonCargoLoading: false,
commonCargoRequest: null,
cargoTypeOptions: [], cargoTypeOptions: [],
cargoTypeFlatOptions: [], cargoTypeFlatOptions: [],
cargoTypeLoading: false, cargoTypeLoading: false,
@@ -718,6 +805,12 @@ export default {
isReceivable() { isReceivable() {
return this.settlementType === 'receivable'; return this.settlementType === 'receivable';
}, },
isPayable() {
return this.settlementType === 'payable';
},
hasAdjustCalculationPending() {
return this.adjustRows.some(row => row.calculating);
},
visibleSearchFields() { visibleSearchFields() {
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4); return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
}, },
@@ -725,7 +818,9 @@ export default {
return [...feeDetailBaseColumns, ...this.dynamicFeeColumns, ...feeDetailTailColumns]; return [...feeDetailBaseColumns, ...this.dynamicFeeColumns, ...feeDetailTailColumns];
}, },
displayTableColumns() { displayTableColumns() {
const columns = [...this.tableColumns]; const columns = this.tableColumns.filter(
column => this.settlementType !== 'receivable' || column.prop !== 'preSettlementNo'
);
const totalIndex = columns.findIndex(column => column.prop === 'totalAmountText'); const totalIndex = columns.findIndex(column => column.prop === 'totalAmountText');
const dynamicColumns = this.tableFeeItemNames.map((name, index) => ({ const dynamicColumns = this.tableFeeItemNames.map((name, index) => ({
label: name, label: name,
@@ -760,6 +855,9 @@ export default {
this.loadPriceUnitOptions(); this.loadPriceUnitOptions();
this.loadTable(); this.loadTable();
}, },
beforeUnmount() {
this.clearAdjustCalculations();
},
methods: { methods: {
async loadTransportTypeOptions() { async loadTransportTypeOptions() {
const res = await getDictionary({ code: 'transport_type' }); const res = await getDictionary({ code: 'transport_type' });
@@ -806,6 +904,82 @@ export default {
}); });
return this.priceUnitRequest; return this.priceUnitRequest;
}, },
loadCommonCargoOptions() {
if (this.commonCargoOptions.length) return Promise.resolve(this.commonCargoOptions);
if (this.commonCargoRequest) return this.commonCargoRequest;
this.commonCargoLoading = true;
this.commonCargoRequest = getCommonCargoList(1, 9999)
.then(res => {
this.commonCargoOptions = this.extractRecords(res);
return this.commonCargoOptions;
})
.finally(() => {
this.commonCargoLoading = false;
this.commonCargoRequest = null;
});
return this.commonCargoRequest;
},
commonCargoMatches(row) {
const cargoType = String(row.cargoType || '').trim();
if (!cargoType) return this.commonCargoOptions;
const matched = this.commonCargoOptions.filter(item =>
[item.secondCargoTypeName, item.firstCargoTypeName, item.cargoType].some(
value => String(value || '').trim() === cargoType
)
);
return matched.length ? matched : this.commonCargoOptions;
},
manualCargoNameOptions(row) {
const options = new Map();
this.commonCargoMatches(row).forEach(item => {
const value = String(item.cargoName || '').trim();
if (value && !options.has(value)) options.set(value, { label: value, value });
});
return Array.from(options.values());
},
manualSpecificationOptions(row) {
const cargoName = String(row.cargoName || '').trim();
const records = cargoName
? this.commonCargoMatches(row).filter(
item => String(item.cargoName || '').trim() === cargoName
)
: this.commonCargoMatches(row);
const options = new Map();
records.forEach(item => {
const value = String(item.specification || item.spec || '').trim();
if (value && !options.has(value)) options.set(value, { label: value, value });
});
return Array.from(options.values());
},
handleAdjustCargoNameChange(row, value) {
row.cargoName = String(value || '').trim();
const cargo = this.commonCargoMatches(row).find(
item => String(item.cargoName || '').trim() === row.cargoName
);
if (cargo) this.applyCommonCargoToAdjustRow(row, cargo);
},
handleAdjustSpecificationChange(row, value) {
row.specification = String(value || '').trim();
const cargo = this.commonCargoMatches(row).find(
item =>
String(item.cargoName || '').trim() === String(row.cargoName || '').trim() &&
String(item.specification || item.spec || '').trim() === row.specification
);
if (cargo) this.applyCommonCargoToAdjustRow(row, cargo);
},
applyCommonCargoToAdjustRow(row, cargo) {
row.cargoName = cargo.cargoName || row.cargoName || '';
row.specification = cargo.specification || cargo.spec || row.specification || '';
row.model = cargo.model || row.model || '';
row.priceUnit = cargo.priceUnit || row.priceUnit || '';
row.cargoType =
cargo.secondCargoTypeName ||
cargo.cargoType ||
cargo.firstCargoTypeName ||
row.cargoType ||
'';
row.cargoTypePath = this.resolveCargoTypePath(row.cargoType);
},
buildCargoTypeTree(cargoTypes = []) { buildCargoTypeTree(cargoTypes = []) {
const flatCargoTypes = []; const flatCargoTypes = [];
const collectCargoTypes = list => { const collectCargoTypes = list => {
@@ -920,6 +1094,8 @@ export default {
row.cargoTypePath = path; row.cargoTypePath = path;
row.cargoType = labels.length ? labels[labels.length - 1] : ''; row.cargoType = labels.length ? labels[labels.length - 1] : '';
row.cargoName = ''; row.cargoName = '';
row.specification = '';
row.model = '';
}, },
formatColumnValue(row, column) { formatColumnValue(row, column) {
if (column.prop === 'transportType') return this.transportTypeLabel(row[column.prop]); if (column.prop === 'transportType') return this.transportTypeLabel(row[column.prop]);
@@ -989,6 +1165,7 @@ export default {
this.detailDialog.row = null; this.detailDialog.row = null;
}, },
closeAdjustPanel() { closeAdjustPanel() {
this.clearAdjustCalculations();
this.adjustDialog.visible = false; this.adjustDialog.visible = false;
this.adjustDialog.row = null; this.adjustDialog.row = null;
}, },
@@ -1041,6 +1218,7 @@ export default {
try { try {
const [res] = await Promise.all([ const [res] = await Promise.all([
api.getFeeDetail(row.id), api.getFeeDetail(row.id),
this.loadCommonCargoOptions(),
this.loadCargoTypeOptions(), this.loadCargoTypeOptions(),
this.loadPriceUnitOptions(), this.loadPriceUnitOptions(),
]); ]);
@@ -1068,9 +1246,14 @@ export default {
originalAmount: Number(item.originalAmount || 0), originalAmount: Number(item.originalAmount || 0),
feeItems, feeItems,
dataSource: dataSource:
item.dataSource || (item.billingFactor === '手工调整' ? '手录入' : '自动生成'), item.dataSource || (item.billingFactor === '手工调整' ? '手录入' : '自动生成'),
manualFee: item.dataSource === '手工录入' || item.billingFactor === '手工调整', manualFee:
['手动录入', '手动添加', '手工录入'].includes(item.dataSource) ||
item.billingFactor === '手工调整',
cargoTypePath: this.resolveCargoTypePath(item.cargoType), cargoTypePath: this.resolveCargoTypePath(item.cargoType),
calculating: false,
adjustCalculateTimer: null,
adjustCalculateVersion: 0,
}; };
this.recalculateAdjustRow(adjusted); this.recalculateAdjustRow(adjusted);
return adjusted; return adjusted;
@@ -1080,20 +1263,20 @@ export default {
} }
}, },
addAdjustFee() { addAdjustFee() {
if (!this.isReceivable) return; if (!this.isPayable) return;
const feeItems = this.adjustDynamicFeeColumns.reduce((items, column) => { const feeItems = this.adjustDynamicFeeColumns.reduce((items, column) => {
items[column.feeItemName] = 0; items[column.feeItemName] = 0;
return items; return items;
}, {}); }, {});
this.adjustRows.push({ this.adjustRows.push({
dataSource: '手录入', dataSource: '手录入',
cargoName: '', cargoName: '',
cargoType: '', cargoType: '',
cargoTypePath: [], cargoTypePath: [],
specification: '', specification: '',
model: '', model: '',
billingFactor: '-', billingFactor: '',
billingType: '-', billingType: '',
transportQuantity: '', transportQuantity: '',
priceUnit: '', priceUnit: '',
unitPrice: '', unitPrice: '',
@@ -1107,10 +1290,19 @@ export default {
manualFee: true, manualFee: true,
remark: '', remark: '',
changeReason: '', changeReason: '',
calculating: false,
adjustCalculateTimer: null,
adjustCalculateVersion: 0,
}); });
}, },
feeSourceLabel(value) { feeSourceLabel(value) {
return value === '手工录入' ? '手录入' : '自动生成'; return ['手动录入', '手动添加', '手工录入'].includes(value) ? '手录入' : '自动生成';
},
adjustBillingTypes(row) {
return ADJUST_BILLING_TYPE_MAP[row.billingFactor] || [];
},
handleAdjustBillingFactorChange(row) {
if (!this.adjustBillingTypes(row).includes(row.billingType)) row.billingType = '';
}, },
adjustTextMaxlength(prop) { adjustTextMaxlength(prop) {
return ['billingFactor', 'billingType'].includes(prop) ? 100 : 255; return ['billingFactor', 'billingType'].includes(prop) ? 100 : 255;
@@ -1131,6 +1323,10 @@ export default {
}, },
handleAdjustDecimalInput(row, prop, value, changedField) { handleAdjustDecimalInput(row, prop, value, changedField) {
row[prop] = this.normalizeAdjustDecimal(value); row[prop] = this.normalizeAdjustDecimal(value);
if (!row.manualFee && row.id && ['transportQuantity', 'mileage'].includes(prop)) {
this.scheduleAdjustFeeCalculation(row);
return;
}
this.recalculateAdjustRow(row, changedField); this.recalculateAdjustRow(row, changedField);
}, },
handleAdjustFeeItemInput(row, feeItemName, value) { handleAdjustFeeItemInput(row, feeItemName, value) {
@@ -1155,6 +1351,50 @@ export default {
); );
row.adjustAmount = Number((row.afterAmount - Number(row.originalAmount || 0)).toFixed(2)); row.adjustAmount = Number((row.afterAmount - Number(row.originalAmount || 0)).toFixed(2));
}, },
scheduleAdjustFeeCalculation(row) {
if (row.adjustCalculateTimer) clearTimeout(row.adjustCalculateTimer);
row.adjustCalculateVersion += 1;
row.calculating = true;
const version = row.adjustCalculateVersion;
row.adjustCalculateTimer = setTimeout(() => {
row.adjustCalculateTimer = null;
this.calculateAdjustRow(row, version);
}, 300);
},
async calculateAdjustRow(row, version) {
try {
const res = await api.calculateAdjustedFee({
detailId: this.adjustDialog.row?.id,
feeId: row.id,
transportQuantity: Number(row.transportQuantity || 0),
mileage: Number(row.mileage || 0),
freightAmount: Number(row.freightAmount || 0),
feeItems: Object.fromEntries(
Object.entries(row.feeItems || {}).map(([name, amount]) => [name, Number(amount || 0)])
),
});
if (version !== row.adjustCalculateVersion || !this.adjustDialog.visible) return;
const data = res.data?.data || res.data || res || {};
row.freightAmount = Number(data.freightAmount || 0);
row.feeItems = Object.fromEntries(
Object.entries(data.feeItems || {}).map(([name, amount]) => [name, Number(amount || 0)])
);
row.adjustAmount = Number(data.adjustAmount || 0);
row.afterAmount = Number(data.afterAmount || 0);
} catch (error) {
row.calculateError = error.message;
} finally {
if (version === row.adjustCalculateVersion) row.calculating = false;
}
},
clearAdjustCalculations() {
this.adjustRows.forEach(row => {
if (row.adjustCalculateTimer) clearTimeout(row.adjustCalculateTimer);
row.adjustCalculateTimer = null;
row.adjustCalculateVersion += 1;
row.calculating = false;
});
},
isFreightFeeItem(name) { isFreightFeeItem(name) {
return String(name || '').includes('运费') || String(name || '').includes('运输费'); return String(name || '').includes('运费') || String(name || '').includes('运输费');
}, },
@@ -1163,15 +1403,20 @@ export default {
this.$message.warning('没有可调整的费用明细'); this.$message.warning('没有可调整的费用明细');
return; return;
} }
if (this.hasAdjustCalculationPending) {
this.$message.warning('费用正在重新计算,请稍候');
return;
}
if (!this.validateManualAdjustRows()) return;
this.adjustDialog.submitting = true; this.adjustDialog.submitting = true;
try { try {
await api.adjustFee({ await api.adjustFee({
detailId: this.adjustDialog.row.id, detailId: this.adjustDialog.row.id,
rows: this.adjustRows.map(row => ({ rows: this.adjustRows.map(row => ({
id: row.id, id: row.id,
cargoName: row.cargoName, cargoName: String(row.cargoName || '').trim(),
cargoType: row.cargoType, cargoType: row.cargoType,
specification: row.specification, specification: String(row.specification || '').trim(),
model: row.model, model: row.model,
billingFactor: row.billingFactor, billingFactor: row.billingFactor,
billingType: row.billingType, billingType: row.billingType,
@@ -1193,6 +1438,32 @@ export default {
this.adjustDialog.submitting = false; this.adjustDialog.submitting = false;
} }
}, },
validateManualAdjustRows() {
for (const [index, row] of this.adjustRows.entries()) {
if (!row.manualFee) continue;
if (!String(row.cargoName || '').trim()) {
this.$message.warning(`第${index + 1}行货物名称不能为空`);
return false;
}
if (String(row.cargoName).trim().length > 100) {
this.$message.warning(`第${index + 1}行货物名称不能超过100个字`);
return false;
}
if (String(row.specification || '').trim().length > 255) {
this.$message.warning(`第${index + 1}行规格不能超过255个字`);
return false;
}
if (!ADJUST_BILLING_ELEMENTS.includes(row.billingFactor)) {
this.$message.warning(`第${index + 1}行请选择计费要素`);
return false;
}
if (!this.adjustBillingTypes(row).includes(row.billingType)) {
this.$message.warning(`第${index + 1}行请选择计费要素对应的计费类型`);
return false;
}
}
return true;
},
async loadFeeDetail() { async loadFeeDetail() {
if (!this.detailDialog.row) return; if (!this.detailDialog.row) return;
this.detailDialog.loading = true; this.detailDialog.loading = true;
@@ -1841,6 +2112,7 @@ export default {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
gap: 8px;
} }
.settlement-detail-page__pagination { .settlement-detail-page__pagination {