Merge remote-tracking branch 'origin/master' into feature/20261016-settlement-adjustment-kingdee

This commit is contained in:
刘泉佳
2026-10-18 15:14:38 +08:00
40 changed files with 1511 additions and 537 deletions
@@ -733,7 +733,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"
@@ -742,7 +742,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="合同编号"
@@ -867,20 +867,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
@@ -918,11 +904,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"
@@ -936,11 +940,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"
@@ -948,8 +947,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>
@@ -970,51 +976,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="暂无计费规则详情" />
@@ -1026,6 +1026,7 @@
</template>
<script>
import { InfoFilled } from '@element-plus/icons-vue';
import { getMkPublicDetail } from '@/api/mk-process';
import {
adjustDetail,
@@ -1065,7 +1066,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';
@@ -1257,7 +1257,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();
@@ -1379,7 +1385,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 handleAdjustmentRepush(row) {
await this.$confirm(
@@ -2264,8 +2280,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);
@@ -2291,26 +2318,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 ||
@@ -2328,7 +2337,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,
},
};
@@ -2406,7 +2419,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,
};
@@ -2429,7 +2446,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('费用正在重新计算,请稍候');
}
@@ -2458,6 +2474,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));
@@ -2471,24 +2495,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();
@@ -2510,7 +2547,6 @@ export default {
feeItems: row.feeItems,
settlementAmountTax: row.settlementAmountTax,
settlementAmountNoTax: row.settlementAmountNoTax,
remark: row.remark,
})),
});
this.$message.success('调整保存成功');
@@ -2534,6 +2570,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('请至少选择一条发票');
@@ -2542,26 +2589,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);
@@ -2619,7 +2673,7 @@ export default {
}
matchedTotal += matchedAmount;
}
if (matchedTotal > Number(this.form.settlementAmount || 0)) {
if (matchedTotal > this.resolveInvoiceClaimSettlementAmount()) {
this.$message.warning('发票匹配结算单金额合计不能超过结算金额');
return false;
}
@@ -2727,7 +2781,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()
@@ -2738,7 +2792,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 => ({
@@ -3168,20 +3222,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);