✨ 结算付款体验优化与合同/运单导入校验
付款申请汇票筛选与提示去重、结算菜单拆分与金蝶同步弹框、应收应付详情高度、合同计费与半月结调整,以及导入运单批量删除校验。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -702,7 +702,7 @@
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="candidate.visible" title="选择预结算单" width="88%" append-to-body>
|
||||
<el-dialog v-model="candidate.visible" title="选择预付结算单" width="88%" append-to-body>
|
||||
<el-card>
|
||||
<el-form
|
||||
:model="candidate.query"
|
||||
@@ -711,7 +711,7 @@
|
||||
label-width="88px"
|
||||
class="formal-editor__candidate-search"
|
||||
>
|
||||
<el-form-item label="预结算单号"
|
||||
<el-form-item label="预付结算单号"
|
||||
><el-input v-model="candidate.query.preSettlementNo" clearable
|
||||
/></el-form-item>
|
||||
<el-form-item label="合同编号"
|
||||
@@ -836,20 +836,6 @@
|
||||
<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="formal-editor__billing-rule-cell">
|
||||
<span>{{ displayValue(billingRuleName(row)) }}</span>
|
||||
<el-icon
|
||||
class="formal-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 }">
|
||||
<el-input-number
|
||||
@@ -887,11 +873,29 @@
|
||||
v-for="feeItem in adjustFeeItemNames"
|
||||
:key="feeItem"
|
||||
:label="feeItem"
|
||||
min-width="140"
|
||||
min-width="160"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div v-if="isFreightFeeItem(feeItem)" class="formal-editor__billing-rule-cell">
|
||||
<el-input-number
|
||||
v-model="row.feeItems[feeItem]"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
@change="recalculateAdjustRow(row, feeItem)"
|
||||
/>
|
||||
<el-icon
|
||||
v-if="hasBillingRule(row)"
|
||||
class="formal-editor__billing-rule-icon"
|
||||
title="查看计费规则详情"
|
||||
@click="openBillingRuleInfo(row)"
|
||||
>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</div>
|
||||
<el-input-number
|
||||
v-else
|
||||
v-model="row.feeItems[feeItem]"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
@@ -905,11 +909,6 @@
|
||||
<el-input-number v-model="row.settlementAmountTax" :precision="2" :controls="false" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" min-width="200" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.remark" maxlength="200" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-form
|
||||
:model="adjust"
|
||||
@@ -917,8 +916,15 @@
|
||||
label-width="auto"
|
||||
class="formal-editor__adjust-reason"
|
||||
>
|
||||
<el-form-item label="调整原因" required>
|
||||
<el-input v-model="adjust.reason" maxlength="200" show-word-limit />
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="adjust.reason"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -939,51 +945,45 @@
|
||||
<template v-if="billingRuleDialog.rule">
|
||||
<div class="dialog-section-title">计费规则</div>
|
||||
<el-descriptions :column="2" border class="formal-editor__billing-rule-detail">
|
||||
<el-descriptions-item label="规则名称">{{
|
||||
displayValue(billingRuleDialog.rule.name)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="费用类型">{{
|
||||
displayValue(billingRuleDialog.rule.feeType)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="费用项">{{
|
||||
displayValue(billingRuleDialog.rule.feeItem)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="计费要素">{{
|
||||
displayValue(billingRuleDialog.rule.billingElement)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="计费类型">{{
|
||||
displayValue(billingRuleDialog.rule.billingType)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="计费单位">{{
|
||||
displayValue(billingRuleDialog.rule.billingUnit)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="单价">{{
|
||||
displayValue(billingRuleDialog.rule.unitPrice)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="最低计费量">{{
|
||||
displayValue(billingRuleDialog.rule.minimumBillingWeight)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="计费区间">{{
|
||||
billingRuleRanges(billingRuleDialog.rule)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{
|
||||
displayValue(billingRuleDialog.rule.remark)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="费用项">
|
||||
{{ displayValue(billingRuleDialog.rule.feeItem) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="计费要素">
|
||||
{{ displayValue(billingRuleDialog.rule.billingElement) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="计费类型">
|
||||
{{ displayValue(billingRuleDialog.rule.billingType) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="计费单位">
|
||||
{{ displayValue(billingRuleDialog.rule.billingUnit) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="单价">
|
||||
{{ displayValue(billingRuleDialog.rule.unitPrice) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="最低计费量">
|
||||
{{ displayValue(billingRuleDialog.rule.minimumBillingWeight) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="计费区间">
|
||||
{{ billingRuleRanges(billingRuleDialog.rule) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">
|
||||
{{ displayValue(billingRuleDialog.rule.remark) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="dialog-section-title formal-editor__billing-rule-match-title">匹配条件</div>
|
||||
<el-descriptions :column="2" border class="formal-editor__billing-rule-detail">
|
||||
<el-descriptions-item label="起运地">{{
|
||||
displayValue(billingRuleDialog.rule.matchCondition.origin)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="目的地">{{
|
||||
displayValue(billingRuleDialog.rule.matchCondition.destination)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="运输方式">{{
|
||||
displayValue(billingRuleDialog.rule.matchCondition.transportMode)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="货物类型">{{
|
||||
displayValue(billingRuleDialog.rule.matchCondition.cargoType)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="起运地">
|
||||
{{ displayValue(billingRuleDialog.rule.matchCondition.origin) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="目的地">
|
||||
{{ displayValue(billingRuleDialog.rule.matchCondition.destination) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="货物名称">
|
||||
{{ displayValue(billingRuleDialog.rule.matchCondition.cargoName) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="货物类型">
|
||||
{{ displayValue(billingRuleDialog.rule.matchCondition.cargoType) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
<el-empty v-else description="暂无计费规则详情" />
|
||||
@@ -995,6 +995,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import { getMkPublicDetail } from '@/api/mk-process';
|
||||
import {
|
||||
adjustDetail,
|
||||
@@ -1034,7 +1035,6 @@ import {
|
||||
} from '@/option/settlement/formalSettlementTable';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { h } from 'vue';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { normalizeCurrencyCode } from '@/utils/currency';
|
||||
@@ -1220,7 +1220,13 @@ export default {
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
title() {
|
||||
return this.readonly ? '查看正式结算单' : this.recordId ? '编辑正式结算单' : '新增正式结算单';
|
||||
const billLabel =
|
||||
this.form.settlementType === 'receivable' ? '收款结算单' : '付款结算单';
|
||||
return this.readonly
|
||||
? `查看${billLabel}`
|
||||
: this.recordId
|
||||
? `编辑${billLabel}`
|
||||
: `新增${billLabel}`;
|
||||
},
|
||||
detailFeeItemNames() {
|
||||
const names = new Set();
|
||||
@@ -1314,7 +1320,17 @@ export default {
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission?.[code], false);
|
||||
if (this.isAdmin) return true;
|
||||
const action = String(code || '').replace(
|
||||
/^(payment_settlement_|receipt_settlement_|formal_settlement_)/,
|
||||
''
|
||||
);
|
||||
const prefix =
|
||||
this.form.settlementType === 'receivable' ? 'receipt_settlement' : 'payment_settlement';
|
||||
return (
|
||||
this.validData(this.permission?.[`${prefix}_${action}`], false) ||
|
||||
this.validData(this.permission?.[`formal_settlement_${action}`], false)
|
||||
);
|
||||
},
|
||||
async loadFormalDetail(id) {
|
||||
const response = this.publicMode
|
||||
@@ -2152,8 +2168,19 @@ export default {
|
||||
)}.xlsx`
|
||||
);
|
||||
},
|
||||
billingRuleName(row) {
|
||||
return this.normalizeBillingRule(row).name;
|
||||
hasBillingRule(row) {
|
||||
const rule = this.normalizeBillingRule(row);
|
||||
return Boolean(
|
||||
rule.feeItem ||
|
||||
rule.billingElement ||
|
||||
rule.billingType ||
|
||||
rule.unitPrice ||
|
||||
rule.rules?.length ||
|
||||
rule.matchCondition?.origin ||
|
||||
rule.matchCondition?.destination ||
|
||||
rule.matchCondition?.cargoName ||
|
||||
rule.matchCondition?.cargoType
|
||||
);
|
||||
},
|
||||
openBillingRuleInfo(row) {
|
||||
this.billingRuleDialog.rule = this.normalizeBillingRule(row);
|
||||
@@ -2179,26 +2206,8 @@ export default {
|
||||
const matchCondition = this.parseObject(
|
||||
source.matchCondition ?? source.billingMatchCondition ?? rule.matchCondition
|
||||
);
|
||||
const ruleNames = matchedRules
|
||||
.map(item => item.ruleName || item.name || item.billingRuleName || item.feeItem)
|
||||
.filter(Boolean);
|
||||
return {
|
||||
...rule,
|
||||
name:
|
||||
source.billingRuleName ||
|
||||
source.feeRuleName ||
|
||||
source.ruleName ||
|
||||
source.billingPlanRuleName ||
|
||||
source.billingPlanName ||
|
||||
rule.name ||
|
||||
rule.ruleName ||
|
||||
rule.billingRuleName ||
|
||||
rule.planName ||
|
||||
rule.billingPlanName ||
|
||||
ruleNames.join('、') ||
|
||||
rule.feeItem ||
|
||||
'',
|
||||
feeType: source.billingFeeType || source.feeType || rule.feeType,
|
||||
feeItem: source.billingFeeItem || source.feeItem || rule.feeItem,
|
||||
billingElement:
|
||||
source.billingElement ||
|
||||
@@ -2216,7 +2225,11 @@ export default {
|
||||
...matchCondition,
|
||||
origin: matchCondition.origin || matchCondition.originName,
|
||||
destination: matchCondition.destination || matchCondition.destinationName,
|
||||
transportMode: matchCondition.transportMode || matchCondition.transportType,
|
||||
cargoName:
|
||||
matchCondition.cargoName ||
|
||||
matchCondition.cargoNameLabel ||
|
||||
source.cargoName ||
|
||||
rule.cargoName,
|
||||
cargoType: matchCondition.cargoType || matchCondition.cargoTypeName,
|
||||
},
|
||||
};
|
||||
@@ -2294,7 +2307,11 @@ export default {
|
||||
detailId: row.formalSettlementId ? row.id : null,
|
||||
sourceDetailId: row.sourceDetailId || null,
|
||||
feeKey,
|
||||
reason: pending?.reason || '',
|
||||
// 底部备注回显结算明细备注;暂存过的调整优先取暂存备注
|
||||
reason:
|
||||
pending?.reason !== undefined && pending?.reason !== null
|
||||
? pending.reason
|
||||
: row.remark || '',
|
||||
rows: [],
|
||||
targetRow: row.formalSettlementId ? null : row,
|
||||
};
|
||||
@@ -2317,7 +2334,6 @@ export default {
|
||||
}
|
||||
},
|
||||
async saveAdjustment() {
|
||||
if (!this.adjust.reason.trim()) return this.$message.warning('请输入调整原因');
|
||||
if (this.adjust.rows.some(row => row.calculating)) {
|
||||
return this.$message.warning('费用正在重新计算,请稍候');
|
||||
}
|
||||
@@ -2346,6 +2362,14 @@ export default {
|
||||
.reduce((total, item) => total + Number(item.settlementAmountTax || 0), 0)
|
||||
.toFixed(2)
|
||||
);
|
||||
const settlementAmountNoTax = Number(
|
||||
this.adjust.rows
|
||||
.reduce((total, item) => total + Number(item.settlementAmountNoTax || 0), 0)
|
||||
.toFixed(2)
|
||||
);
|
||||
const hasSettlementAmountNoTax = this.adjust.rows.some(
|
||||
item => item.settlementAmountNoTax !== undefined && item.settlementAmountNoTax !== null
|
||||
);
|
||||
const feeItems = this.adjust.rows.reduce((result, item) => {
|
||||
Object.entries(item.feeItems || {}).forEach(([name, amount]) => {
|
||||
result[name] = Number((Number(result[name] || 0) + Number(amount || 0)).toFixed(2));
|
||||
@@ -2359,24 +2383,37 @@ export default {
|
||||
originalAmount,
|
||||
adjustAmount: Number((settlementAmount - originalAmount).toFixed(2)),
|
||||
settlementAmountTax: settlementAmount,
|
||||
settlementAmountNoTax: hasSettlementAmountNoTax
|
||||
? settlementAmountNoTax
|
||||
: target.settlementAmountNoTax,
|
||||
feeItemsJson: JSON.stringify(feeItems),
|
||||
freightAmount: this.adjust.rows.reduce(
|
||||
(total, item) => total + Number(item.freightAmount || 0),
|
||||
0
|
||||
freightAmount: Number(
|
||||
this.adjust.rows
|
||||
.reduce((total, item) => total + Number(item.freightAmount || 0), 0)
|
||||
.toFixed(2)
|
||||
),
|
||||
transportQuantity: this.adjust.rows.reduce(
|
||||
(total, item) => total + Number(item.transportQuantity || 0),
|
||||
0
|
||||
transportQuantity: Number(
|
||||
this.adjust.rows
|
||||
.reduce((total, item) => total + Number(item.transportQuantity || 0), 0)
|
||||
.toFixed(2)
|
||||
),
|
||||
mileage: Number(
|
||||
this.adjust.rows
|
||||
.reduce((total, item) => total + Number(item.mileage || 0), 0)
|
||||
.toFixed(2)
|
||||
),
|
||||
mileage: this.adjust.rows.reduce((total, item) => total + Number(item.mileage || 0), 0),
|
||||
unitPrice: edited.unitPrice,
|
||||
remark: edited.remark,
|
||||
// 底部备注写入结算明细备注
|
||||
remark: this.adjust.reason || '',
|
||||
pendingDetailAdjustment: true,
|
||||
});
|
||||
if (this.adjust.feeKey) {
|
||||
const clonedRows = this.cloneAdjustRows(this.adjust.rows);
|
||||
// 暂存后同步费用快照,避免再次打开调整弹窗或汇总时仍用调整前数据。
|
||||
this.detailFeeSnapshots[this.adjust.feeKey] = clonedRows;
|
||||
this.pendingAdjustments[this.adjust.feeKey] = {
|
||||
reason: this.adjust.reason,
|
||||
rows: this.cloneAdjustRows(this.adjust.rows),
|
||||
rows: clonedRows,
|
||||
};
|
||||
}
|
||||
await this.refreshSummaryFees();
|
||||
@@ -2398,7 +2435,6 @@ export default {
|
||||
feeItems: row.feeItems,
|
||||
settlementAmountTax: row.settlementAmountTax,
|
||||
settlementAmountNoTax: row.settlementAmountNoTax,
|
||||
remark: row.remark,
|
||||
})),
|
||||
});
|
||||
this.$message.success('调整保存成功');
|
||||
@@ -2422,6 +2458,17 @@ export default {
|
||||
}));
|
||||
await this.confirmInvoiceClaims();
|
||||
},
|
||||
// 发票认领默认金额以当前结算金额为准:优先表单结算金额,其次结算合计,再次明细含税合计。
|
||||
resolveInvoiceClaimSettlementAmount() {
|
||||
const formAmount = Number(this.form.settlementAmount || 0);
|
||||
if (formAmount > 0) return formAmount;
|
||||
const summaryAmount = Number(this.summaryTotal || 0);
|
||||
if (summaryAmount > 0) return summaryAmount;
|
||||
return this.details.reduce(
|
||||
(total, row) => total + Number(row.settlementAmountTax || row.settlementAmount || 0),
|
||||
0
|
||||
);
|
||||
},
|
||||
async confirmInvoiceClaims() {
|
||||
if (!this.invoiceClaimDialog.selected.length) {
|
||||
this.$message.warning('请至少选择一条发票');
|
||||
@@ -2430,26 +2477,33 @@ export default {
|
||||
this.invoiceClaimDialog.confirming = true;
|
||||
try {
|
||||
const existingNumbers = new Set(this.invoices.map(item => String(item.invoiceNo)));
|
||||
const settlementAmount = this.resolveInvoiceClaimSettlementAmount();
|
||||
if (settlementAmount > 0 && Number(this.form.settlementAmount || 0) <= 0) {
|
||||
this.form.settlementAmount = Number(settlementAmount).toFixed(2);
|
||||
}
|
||||
let remaining = Math.max(
|
||||
Number(this.form.settlementAmount || 0) -
|
||||
settlementAmount -
|
||||
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);
|
||||
// 票池 matchedAmount 默认等于整张发票金额;可匹配口径优先取 availableInvoiceAmount
|
||||
const sourceAmount = Number(
|
||||
row.availableInvoiceAmount || row.matchedAmount || row.invoiceAmount || 0
|
||||
);
|
||||
// 可匹配金额大于剩余结算金额时,默认匹配金额取剩余结算金额(单张发票常见场景)
|
||||
const matchedAmount =
|
||||
Number(this.form.settlementAmount || 0) > 0
|
||||
? Math.min(sourceAmount, remaining)
|
||||
: sourceAmount;
|
||||
if (Number(this.form.settlementAmount || 0) > 0) {
|
||||
settlementAmount > 0 ? Math.min(sourceAmount, remaining) : sourceAmount;
|
||||
if (settlementAmount > 0) {
|
||||
remaining = Math.max(0, remaining - matchedAmount);
|
||||
}
|
||||
return {
|
||||
...row,
|
||||
invoiceType: row.invoiceTypeName || row.invoiceType || '',
|
||||
matchedAmount,
|
||||
availableInvoiceAmount: sourceAmount,
|
||||
matchedAmount: Number(matchedAmount.toFixed(2)),
|
||||
};
|
||||
})
|
||||
.filter(row => row.matchedAmount > 0);
|
||||
@@ -2507,7 +2561,7 @@ export default {
|
||||
}
|
||||
matchedTotal += matchedAmount;
|
||||
}
|
||||
if (matchedTotal > Number(this.form.settlementAmount || 0)) {
|
||||
if (matchedTotal > this.resolveInvoiceClaimSettlementAmount()) {
|
||||
this.$message.warning('发票匹配结算单金额合计不能超过结算金额');
|
||||
return false;
|
||||
}
|
||||
@@ -2615,7 +2669,7 @@ export default {
|
||||
summaryFees: this.summaryFees
|
||||
.filter(
|
||||
row =>
|
||||
row.manualFlag === 1 ||
|
||||
Number(row.manualFlag) === 1 ||
|
||||
row.id ||
|
||||
Number(row.adjustAmount || 0) !== 0 ||
|
||||
String(row.remark || '').trim()
|
||||
@@ -2626,7 +2680,7 @@ export default {
|
||||
feeItem: row.feeItem,
|
||||
adjustAmount: Number(row.adjustAmount || 0),
|
||||
remark: row.remark,
|
||||
manualFlag: row.manualFlag || 0,
|
||||
manualFlag: Number(row.manualFlag) === 1 ? 1 : 0,
|
||||
})),
|
||||
attachmentsJson: this.stringifyAttachments(this.attachments),
|
||||
invoices: this.invoices.map(row => ({
|
||||
@@ -3053,20 +3107,6 @@ export default {
|
||||
.formal-editor__billing-rule-match-title {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.dialog-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 22px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.dialog-section-title::before {
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
margin-right: 8px;
|
||||
background: #409eff;
|
||||
content: '';
|
||||
}
|
||||
.formal-editor__attachment-missing {
|
||||
margin-left: 12px;
|
||||
color: var(--el-color-danger);
|
||||
|
||||
Reference in New Issue
Block a user