- 经理聚合分成放大 100 倍:参数按百分数存储(12.5=12.5%),parse_fraction 后调用方又 ×100、函数内部再 ÷100,净效应「绩效×12.5」;去掉调用方 ×100,rate_text 改用实际参数原文(原硬编码 12.5%+5% 参数调整后失真) - 审批联动只认终审:财务节点通过(1→2)不再提前 mark_locked/调整单生效, 否则领导驳回后 locked_at/locked_by 残留、两级审批形同虚设;驳回在任何 节点即时联动;mark_rejected 补清锁定字段 - 调整单新建不可用:ENABLE_CODE_BY_TYPE 未登记 type=3 → is_enabled 恒 400; 补登记 approval.adjustment.enabled。另修复叠加缺陷——开关关闭分支手工 ApprovalStep(order_id=调整单id),而该外键指向 approval_orders,必然 errno 1452;统一改走 submit_order(自动通过留痕由它负责) - 重算双重计提:业务级一次性计提的「进度登记」回款无生效台账行,重算会 凭空落新行;无旧行一律 400 - 前端重算传错 id:台账行主键当 receipt_id 传给重算接口,会作废无关回款 的台账;改传 row.receipt_id(聚合行防御) - quantize 显式 ROUND_HALF_UP:9 处金额 quantize 缺省银行家舍入, x.xx5 与四舍五入口径差 1 分(docstring 却声称 HALF_UP) 测试:显式控制审批开关(V8 起种子默认开启,用例不再依赖默认值); 新增回归 4 例(经理聚合比例/调整单即生效/进度登记重算 400/财务节点不提前 锁定),test_calc+test_settlements 22 passed Co-Authored-By: Claude Code <noreply@anthropic.com>
187 lines
6.0 KiB
Vue
187 lines
6.0 KiB
Vue
<script setup lang="ts">
|
||
import { reactive, ref } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
|
||
import * as calcApi from '@/api/calc'
|
||
import type { LedgerRow } from '@/api/calc'
|
||
import AppPagination from '@/components/table/AppPagination.vue'
|
||
import { usePagedList } from '@/composables/usePagedList'
|
||
import { handledError } from '@/utils/error'
|
||
|
||
/**
|
||
* 绩效核算台账(M3 ④):核算事实的固化点。
|
||
*
|
||
* - 核算=把回款固化为台账行(每笔回款仅一次;同业务重复回款仅登记进度)
|
||
* - 重算=作废旧版本、按当前参数落新版本(管理员/财务授权,原因留痕)
|
||
* - 行展开看计算快照(所用参数版本与行级折算明细)
|
||
*/
|
||
|
||
const filters = reactive({
|
||
settlement_month: '',
|
||
is_active: 1 as number | undefined,
|
||
})
|
||
|
||
const { records, total, pageNum, pageSize, loading, load, search } = usePagedList<
|
||
LedgerRow,
|
||
{ settlement_month?: string; is_active?: number }
|
||
>(calcApi.listLedger, () => ({
|
||
settlement_month: filters.settlement_month.trim() || undefined,
|
||
is_active: filters.is_active,
|
||
}))
|
||
|
||
function resetFilters(): void {
|
||
filters.settlement_month = ''
|
||
filters.is_active = 1
|
||
search()
|
||
}
|
||
|
||
const recalcVisible = ref(false)
|
||
const recalcTarget = ref<LedgerRow | null>(null)
|
||
const recalcReason = ref('')
|
||
|
||
function openRecalc(row: LedgerRow): void {
|
||
// 聚合行(calc_type=2)receipt_id 为 NULL,无回款可重算(按钮已限量,防御)
|
||
if (row.receipt_id == null) return
|
||
recalcTarget.value = row
|
||
recalcReason.value = ''
|
||
recalcVisible.value = true
|
||
}
|
||
|
||
async function submitRecalc(): Promise<void> {
|
||
const row = recalcTarget.value
|
||
if (!row || row.receipt_id == null) return
|
||
if (!recalcReason.value.trim()) {
|
||
ElMessage.warning('重算必须填写原因')
|
||
return
|
||
}
|
||
try {
|
||
// 重算接口按回款 ID 定位(台账行主键 ≠ 回款 ID:传 row.id 会重算到错误回款)
|
||
await calcApi.recalcReceipt(row.receipt_id, recalcReason.value.trim())
|
||
ElMessage.success('重算完成(新版本已生效)')
|
||
recalcVisible.value = false
|
||
await load()
|
||
} catch (error) {
|
||
handledError(error)
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="page">
|
||
<el-form inline class="filters" @submit.prevent>
|
||
<el-form-item label="归属月">
|
||
<el-input
|
||
v-model="filters.settlement_month"
|
||
placeholder="YYYY-MM"
|
||
clearable
|
||
style="width: 140px"
|
||
@keyup.enter="search"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="版本">
|
||
<!-- 可清空:清空=不加 is_active 过滤(axios 自动剔除 undefined),
|
||
即「全部版本」;0 精确匹配历史行,不能用来表达「含历史」 -->
|
||
<el-select
|
||
v-model="filters.is_active"
|
||
placeholder="全部版本"
|
||
clearable
|
||
style="width: 130px"
|
||
>
|
||
<el-option label="仅当前生效" :value="1" />
|
||
<el-option label="仅历史版本" :value="0" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" @click="search">查询</el-button>
|
||
<el-button @click="resetFilters">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<el-table v-loading="loading" :data="records" stripe>
|
||
<el-table-column type="expand">
|
||
<template #default="{ row }">
|
||
<pre class="snapshot">{{ JSON.stringify(row.snapshot_json ?? {}, null, 2) }}</pre>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="settlement_month" label="归属月" width="90" />
|
||
<el-table-column prop="business_no" label="业务编号" min-width="150" />
|
||
<el-table-column prop="personnel_name" label="归属人员" width="100" />
|
||
<el-table-column prop="role_code" label="岗位" width="110" />
|
||
<el-table-column prop="receipt_date" label="回款日" width="100" />
|
||
<el-table-column prop="payment_days" label="天数" width="70" />
|
||
<el-table-column prop="rate_text" label="计提比例" min-width="90" />
|
||
<el-table-column prop="converted_profit" label="折算毛利" min-width="100" />
|
||
<el-table-column prop="performance_amount" label="绩效金额" min-width="100" />
|
||
<el-table-column prop="payable_amount" label="应付绩效" min-width="100" />
|
||
<el-table-column label="版本" width="80">
|
||
<template #default="{ row }">
|
||
v{{ row.calc_version }}
|
||
<el-tag v-if="row.is_active === 0" size="small" type="info" disable-transitions>
|
||
已替代
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="90" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button
|
||
v-if="row.is_active === 1 && row.calc_type === 1"
|
||
link
|
||
type="primary"
|
||
@click="openRecalc(row)"
|
||
>
|
||
重算
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<AppPagination
|
||
v-model:page-num="pageNum"
|
||
v-model:page-size="pageSize"
|
||
:total="total"
|
||
@change="load"
|
||
/>
|
||
|
||
<el-dialog v-model="recalcVisible" title="重算确认" width="460px">
|
||
<p class="recalc-warn">
|
||
重算会作废当前版本(v{{ recalcTarget?.calc_version }})并按当前参数落新版本,
|
||
历史版本可追溯。请填写重算原因。
|
||
</p>
|
||
<el-input
|
||
v-model="recalcReason"
|
||
type="textarea"
|
||
:rows="2"
|
||
maxlength="255"
|
||
placeholder="重算原因(必填,留痕)"
|
||
/>
|
||
<template #footer>
|
||
<el-button @click="recalcVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="submitRecalc">确认重算</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.filters {
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.snapshot {
|
||
margin: 0;
|
||
padding: 10px;
|
||
font-size: 12px;
|
||
line-height: 1.6;
|
||
background: #f4f5f8;
|
||
border-radius: 6px;
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.recalc-warn {
|
||
margin: 0 0 10px;
|
||
font-size: 12px;
|
||
color: var(--ys-ink-3);
|
||
}
|
||
</style>
|