✨ 完善应收账单毛利分析与提成展示

This commit is contained in:
caojia
2026-11-10 17:25:21 +08:00
parent 9004a1b921
commit 3e5ef25153
10 changed files with 215 additions and 31 deletions
+65 -14
View File
@@ -36,6 +36,12 @@
<div>销售价 <b>{{ money(summary.salesAmount) }}</b></div><div>产品价 <b>{{ money(summary.productAmount) }}</b></div>
<div>销售毛利 <b>{{ money(summary.salesGrossProfit) }}</b></div>
<div>销售毛利率 <b>{{ rate(summary.salesGrossMarginRate) }}</b></div>
<div>
<el-tooltip content="仅统计已全部核销、提成计算成功且未冲回的订单提成;未全部核销或提成异常的订单暂不计入,无已计算提成时显示 -。" placement="top">
<span>正式提成 ⓘ</span>
</el-tooltip>
<b>{{ money(summary.commissionAmount) }}</b>
</div>
<div>账单已核销 <b>{{ money(summary.writtenOffAmount) }}</b></div><div>账单未核销 <b>{{ money(summary.unwrittenAmount) }}</b></div>
</div>
</el-card>
@@ -50,6 +56,7 @@
</div>
<el-table v-loading="loading" :data="rows" border stripe>
<el-table-column prop="billNo" label="账单号" min-width="170" show-overflow-tooltip />
<el-table-column prop="billName" label="账单名称" min-width="170" show-overflow-tooltip />
<el-table-column prop="billDate" label="账单日期" width="115" />
<el-table-column prop="customerCode" label="客户代码" width="110" />
<el-table-column prop="customerName" label="客户名称" min-width="160" show-overflow-tooltip />
@@ -62,7 +69,15 @@
<el-table-column label="已核销" min-width="110" align="right"><template #default="{ row }">{{ money(row.writtenOffAmount) }}</template></el-table-column>
<el-table-column label="未核销" min-width="110" align="right"><template #default="{ row }">{{ money(row.unwrittenAmount) }}</template></el-table-column>
<el-table-column label="状态" width="110"><template #default="{ row }">{{ statusLabel(row.status) }}</template></el-table-column>
<el-table-column label="操作" width="220" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="openBillDetails(row)">账单明细</el-button><el-button v-hasPermi="['report:arProfit:commission']" link type="primary" @click="openBillCommission(row)">查看提成</el-button><el-button v-hasPermi="['report:arProfit:commission']" link type="primary" @click="openWriteoffRates(row)">核销比例</el-button></template></el-table-column>
<el-table-column label="操作" width="250" fixed="right">
<template #default="{ row }">
<div class="ar-profit-actions">
<el-button link type="primary" @click="openBillDetails(row)">账单明细</el-button>
<el-button v-hasPermi="['report:arProfit:list']" link type="primary" @click="openBillCommission(row)">查看提成</el-button>
<el-button v-hasPermi="['report:arProfit:commission']" link type="primary" @click="openWriteoffRates(row)">核销比例</el-button>
</div>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" v-model:page="pageNum" v-model:limit="pageSize" :total="total" @pagination="loadRows" />
</el-card>
@@ -87,15 +102,11 @@
<el-dialog v-model="commissionVisible" title="提成管理" width="1100px" append-to-body>
<el-tabs v-model="commissionTab" @tab-change="loadCommissionTab">
<el-tab-pane label="正式提成" name="results">
<el-alert v-if="commissionBillId" :title="'当前查看账单:' + commissionBillNo" type="info" :closable="false" show-icon>
<el-button link type="primary" @click="clearCommissionBill">查看全部账单</el-button>
</el-alert>
<div v-if="commissionBillId" class="summary-note">账单全部核销后生成正式提成;存在待处理明细时不展示整单提成合计。</div>
<el-button icon="Download" style="margin-bottom: 12px" @click="exportCommissions">导出提成明细</el-button>
<el-checkbox v-model="includeReversed" style="margin: 0 0 12px 12px" @change="loadCommissionTab">显示已冲回记录</el-checkbox>
<el-table :data="commissionSummary" border size="small" style="margin-bottom: 16px">
<el-table-column prop="salesUserName" label="销售员" /><el-table-column prop="currency" label="币种" width="80" />
<el-table-column prop="orderCount" label="订单明细数" /><el-table-column prop="pendingCount" label="待处理订单数" />
<el-table-column prop="orderCount" label="订单明细数" /><el-table-column prop="pendingCount" label="异常订单数" />
<el-table-column label="正式提成合计" align="right"><template #default="{ row }">{{ money(row.commissionAmount) }}</template></el-table-column>
</el-table>
<el-table :data="results" border size="small" v-loading="commissionLoading">
@@ -131,6 +142,21 @@
</el-tab-pane>
</el-tabs>
</el-dialog>
<el-dialog v-model="billCommissionVisible" :title="'账单提成明细:' + billCommissionNo" width="1000px" append-to-body>
<el-table :data="billCommissionSummary" border size="small" style="margin-bottom: 16px">
<el-table-column prop="salesUserName" label="销售员" /><el-table-column prop="currency" label="币种" width="80" />
<el-table-column prop="orderCount" label="订单明细数" /><el-table-column prop="pendingCount" label="异常订单数" />
<el-table-column label="正式提成合计" align="right"><template #default="{ row }">{{ money(row.commissionAmount) }}</template></el-table-column>
</el-table>
<el-table :data="billCommissionResults" border size="small" v-loading="billCommissionLoading">
<el-table-column prop="orderNo" label="订单号" min-width="170" show-overflow-tooltip />
<el-table-column prop="salesUserName" label="销售员" width="110" /><el-table-column prop="currency" label="币种" width="80" />
<el-table-column label="销售毛利" width="120" align="right"><template #default="{ row }">{{ money(row.grossProfit) }}</template></el-table-column>
<el-table-column label="正式提成" width="120" align="right"><template #default="{ row }">{{ money(row.commissionAmount) }}</template></el-table-column>
<el-table-column label="状态" min-width="130"><template #default="{ row }">{{ resultLabel(row.resultStatus) }}</template></el-table-column>
</el-table>
<pagination v-show="billCommissionTotal > 0" v-model:page="billCommissionPage" v-model:limit="billCommissionSize" :total="billCommissionTotal" @pagination="loadBillCommission" />
</el-dialog>
<el-dialog v-model="confirmVisible" title="确认历史核销比例" width="480px" append-to-body>
<el-form label-width="100px"><el-form-item label="账单号">{{ confirmForm.billNo }}</el-form-item>
<el-form-item label="比例(%)"><el-input-number v-model="confirmForm.rate" :min="0" :max="100" :precision="4" /></el-form-item>
@@ -172,9 +198,13 @@ const summaryCards = computed(() => !summaryLoaded.value ? [] : summaries.value.
let requestVersion = 0
let commissionVersion = 0
const commissionVisible = ref(false), commissionTab = ref('results'), commissionLoading = ref(false)
const commissionBillId = ref(null), commissionBillNo = ref('')
const results = ref([]), commissionTotal = ref(0), commissionPage = ref(1), commissionSize = ref(10)
const commissionSummary = ref([])
const billCommissionVisible = ref(false), billCommissionLoading = ref(false), billCommissionNo = ref('')
const billCommissionId = ref(null)
const billCommissionResults = ref([]), billCommissionTotal = ref(0), billCommissionPage = ref(1), billCommissionSize = ref(10)
const billCommissionSummary = ref([])
let billCommissionVersion = 0
const includeReversed = ref(false)
const rates = ref([]), rateForm = reactive({ salesUserId: null, rate: null })
const missingRates = ref([]), missingTotal = ref(0), missingPage = ref(1), missingSize = ref(10)
@@ -227,7 +257,7 @@ async function loadBillDetails() {
}
async function openFees(row) { feeVisible.value = true; feeLoading.value = true; fees.value = []; try { fees.value = (await listArProfitFees(row.detailId)).data || [] } finally { feeLoading.value = false } }
function exportRows() { proxy.download('/report/ar-profit/export', params(), `应收账单毛利分析_${Date.now()}.xlsx`) }
function exportCommissions() { proxy.download('/report/ar-profit/commissions/export', { billId: commissionBillId.value }, `应收账单提成明细_${Date.now()}.xlsx`) }
function exportCommissions() { proxy.download('/report/ar-profit/commissions/export', {}, `应收账单提成明细_${Date.now()}.xlsx`) }
function money(v) { return v === null || v === undefined || v === '' || !Number.isFinite(Number(v)) ? '-' : Number(v).toFixed(2) }
function rate(v) { return v === null || v === undefined || v === '' || !Number.isFinite(Number(v)) ? '-' : Number(v).toFixed(2) + '%' }
function feeProductAmount(row) {
@@ -237,19 +267,39 @@ function feeProductAmount(row) {
}
function statusLabel(v) { return ({ CONFIRMED: '已确认', PARTIAL_WRITTEN_OFF: '部分核销', WRITTEN_OFF: '已核销' })[v] || v || '-' }
function resultLabel(v) { return ({ READY: '已计算', MISSING_RATE: '比例待确认', MISSING_PRICE: '价格缺失', MISSING_SNAPSHOT: '归属或区域缺失', WRITEOFF_MISMATCH: '核销金额不一致', REVERSED: '已冲回' })[v] || v || '-' }
function openCommission() { commissionBillId.value = null; commissionBillNo.value = ''; commissionPage.value = 1; commissionVisible.value = true; loadCommissionTab() }
function openBillCommission(row) { commissionBillId.value = row.arBillId; commissionBillNo.value = row.billNo; commissionPage.value = 1; commissionTab.value = 'results'; commissionVisible.value = true; loadCommissionTab() }
function clearCommissionBill() { commissionBillId.value = null; commissionBillNo.value = ''; commissionPage.value = 1; loadCommissionTab() }
function openCommission() { commissionPage.value = 1; commissionVisible.value = true; loadCommissionTab() }
function openBillCommission(row) { billCommissionId.value = row.arBillId; billCommissionNo.value = row.billNo; billCommissionPage.value = 1; billCommissionResults.value = []; billCommissionSummary.value = []; billCommissionTotal.value = 0; billCommissionVisible.value = true; loadBillCommission() }
async function loadBillCommission() {
const version = ++billCommissionVersion
const currentBillId = billCommissionId.value
if (!currentBillId) return
billCommissionLoading.value = true
billCommissionResults.value = []
billCommissionSummary.value = []
billCommissionTotal.value = 0
try {
const [result, summary] = await Promise.all([
listArProfitCommissions({ billId: currentBillId, pageNum: billCommissionPage.value, pageSize: billCommissionSize.value }),
listArProfitCommissionSummary({ billId: currentBillId })
])
if (version === billCommissionVersion) {
billCommissionResults.value = result.rows || []
billCommissionTotal.value = result.total || 0
billCommissionSummary.value = summary.data || []
}
} finally {
if (version === billCommissionVersion) billCommissionLoading.value = false
}
}
async function loadCommissionTab() {
const version = ++commissionVersion
const tab = commissionTab.value
const billId = commissionBillId.value
commissionLoading.value = true
if (tab === 'results') { results.value = []; commissionSummary.value = []; commissionTotal.value = 0 }
try {
if (tab === 'rates') { const r = await listArProfitRates(); if (version === commissionVersion) rates.value = r.data || [] }
else if (tab === 'missing') { const r = await listArProfitMissingRates({ pageNum: missingPage.value, pageSize: missingSize.value }); if (version === commissionVersion) { missingRates.value = r.rows || []; missingTotal.value = r.total || 0 } }
else { const [r, summary] = await Promise.all([listArProfitCommissions({ billId, pageNum: commissionPage.value, pageSize: commissionSize.value, includeReversed: includeReversed.value }), listArProfitCommissionSummary({ billId })]); if (version === commissionVersion) { results.value = r.rows || []; commissionTotal.value = r.total || 0; commissionSummary.value = summary.data || [] } }
else { const [r, summary] = await Promise.all([listArProfitCommissions({ pageNum: commissionPage.value, pageSize: commissionSize.value, includeReversed: includeReversed.value }), listArProfitCommissionSummary()]); if (version === commissionVersion) { results.value = r.rows || []; commissionTotal.value = r.total || 0; commissionSummary.value = summary.data || [] } }
} finally { if (version === commissionVersion) commissionLoading.value = false }
}
async function saveRate() {
@@ -271,8 +321,9 @@ search()
.metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr)); gap: 14px; }
.metrics > div { min-width: 0; padding: 10px; background: var(--el-fill-color-lighter); color: var(--el-text-color-secondary); }
.metrics b { display: block; margin-top: 5px; color: var(--el-text-color-primary); }
.summary-note { margin-top: 10px; color: var(--el-text-color-secondary); font-size: 12px; }
.heading { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; }
.table-title { font-size: 16px; font-weight: 500; }
.sales-code { float: right; margin-left: 16px; color: var(--el-text-color-secondary); font-size: 12px; }
.ar-profit-actions { display: flex; align-items: center; justify-content: center; gap: 2px; white-space: nowrap; }
.ar-profit-actions .el-button { margin: 0; padding: 0 2px; }
</style>