Files
PayrollManagementSystem/frontend/src/views/calc/ledger/index.vue
T
liuwenxinandClaude Code cb4f38dfd9 fix(m3): code-review P0 六项——算错钱与不可用
- 经理聚合分成放大 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>
2026-10-19 11:25:21 +08:00

187 lines
6.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>