人力资源

Odoo 年假计划改了,为什么老余额不会自动重写:Milestone、Carryover 与 Transition 只会沿时间线继续生效

很多 HR 看到累计计划配错了,会直觉觉得“把 accrual plan 改对,旧余额应该一起刷新”。但从 hr.leave.allocation 的累计处理逻辑能看出,milestone、carryover、nextcall、lastcall 和 expiring carryover days 都是在沿时间线推进,而不是每次全量回算整段历史。

人力资源
进阶 开发者 1 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

Odoo 里的 accrual plan 不是“一个公式”,而是一条正在运行的余额时间线。

这意味着当你中途修改:

  • milestone
  • carryover 规则
  • transition mode
  • cap
  • validity

系统并不会天然把整段过去历史全部重新改写。

hr.leave.allocation 的累计逻辑能看得很清楚:它关心的是一组正在推进的状态,例如:

  • lastcall
  • actual_lastcall
  • nextcall
  • last_executed_carryover_date
  • expiring_carryover_days
  • carried_over_days_expiration_date

这代表它更像一个沿时间滚动执行的状态机,而不是一个随时可无损重算的静态公式表。


为什么 HR 会直觉觉得“改计划就该刷新旧余额”

因为从界面视角看,accrual plan 很像一张配置表:

  • 每月加多少
  • 满几年切哪个 milestone
  • 年初怎么 carryover
  • 结转后多久失效

所以很容易脑补成:

  • 规则变了
  • 余额当然应该一起重算

但源码里的真实对象不是“规则表 + 即时总额”这么简单。

它管理的是:

  • 这个 allocation 从哪天开始跑
  • 跑到哪次 nextcall
  • 什么时候发生了 carryover
  • 有多少 carryover 额度会在未来某天失效
  • 当前是按哪个 level 在继续推进

一旦理解成这条“运行中的余额轨迹”,你就会明白为什么系统对历史改写会很保守。


_process_accrual_plans() 的心智:往前推进,不是反复重写过去

_process_accrual_plans() 做的核心事情是:

  • 从当前 allocation 的状态出发
  • 一步步推进到今天或目标日期
  • 在推进过程中处理 level transition、carryover、cap、有效期失效

它不是每次都从 allocation 起点无脑重算到今天,而是大量依赖现有状态字段作为“运行现场”。

这就意味着两件事:

1)历史不是免费的

你不是随便改个配置,系统就自动把每一个过去节点都重新解释。

2)当前余额带着历史痕迹

今天看到的余额,里面可能已经包含:

  • 之前某次 carryover 后留下的额度
  • 某个旧 milestone 下累计出来的值
  • 某次过期日已经扣掉的一批天数

这些都不是单靠“看现在这张 plan”就能瞬间逆推出完整历史的。


源码里甚至直接写明:已运行过的 allocation 不建议靠修改配置补救

hr.leave.allocation 里有一段提示非常直白,大意是:

  • 这条 allocation 已经跑过一次
  • 后续再改配置,不会影响已经分配给员工的天数
  • 如果你要改配置,应该删除并新建 allocation

这句话很重要。

它不是在说系统完全不能处理任何变化,而是在提醒你:

累计分配一旦进入运行态,系统优先保证时间线一致性,而不是替你无成本重写历史。

这跟很多 HR 的直觉正好相反。


为什么 Milestone / Transition 更不适合回头重写

我们前面那篇累计文章已经讲过,milestone 和 transition mode 决定的是:

  • 员工何时进入下一个 level
  • 到门槛当天立刻切,还是等当前累计周期跑完再切

这本身就是时间条件。

如果你后来把:

  • start_count
  • start_type
  • transition_mode

改掉,那就等于在问系统:

  • 过去几个月本来已经按旧 level 跑过的 nextcall,要不要重新定义?
  • 当时已经发生过的 carryover,要不要按新 level 重新裁剪?
  • 过期额度要不要重新复活?

这些问题每一个都会牵动历史状态,绝不是简单“重算一下 added_value”能解决的。


Carryover 更说明它是状态机,而不是普通公式

看 carryover 相关字段你就知道事情没那么简单:

  • last_executed_carryover_date
  • expiring_carryover_days
  • carried_over_days_expiration_date

这代表系统不仅知道“有没有结转”,还记得:

  • 上次结转是什么时候执行的
  • 结转后有多少额度在等待未来过期
  • 那批额度会在哪天失效

如果你此时把 carryover 规则从:

  • lost 改成 carried over
  • unlimited 改成 limited
  • 6 个月有效改成 12 个月有效

系统并不会默认替你把已经发生过的历史结转重新演算一遍。否则它还得同时回答:

  • 已经失效的额度要不要恢复
  • 已经被员工用掉的额度怎么回填来源
  • 历史工资 / 请假记录是否要连带修正

这就是为什么“改规则自动追写历史”看似方便,实则风险巨大。


真正该怎么改,才比较稳

场景一:未来政策变更

最稳的方式通常是:

  • 保留旧 allocation 的历史
  • 从新日期开始启用新 plan 或新 allocation

这样边界最清楚。

场景二:配置刚上线、还没真正跑几次

如果还处于很早期、影响范围可控,可以考虑重建相关 allocation,再让系统按新规则重新跑。

场景三:已经跑很久、员工还在持续用假

这时更应该谨慎。

因为你改的不是一个静态数字,而是一条已经参与过:

  • 请假可用余额
  • 结转失效
  • 未来累计预测

的运行时间线。


这对实施最大的提醒

很多团队把 accrual plan 当成“可随时回头修正的参数表”。

我更建议把它当成:

  • 制度定义 + 运行状态 的组合

一旦进入运行态,你要优先尊重历史边界,而不是期待系统帮你做魔法回写。

所以实施时,最好一开始就确认:

  • carryover 日期
  • milestone 门槛
  • transition mode
  • 有效期
  • cap

因为这些字段越晚改,历史包袱越重。


我会怎么跟 HR 解释

如果 HR 问:

“我把累计计划改对了,为什么员工旧余额没自动刷新?”

我会说:

因为 Odoo 的累计不是静态公式,而是沿 nextcall、carryover 和过期节点逐步跑出来的余额历史。你改的是未来制度,不一定是过去结果;历史若要重写,往往要靠重建 allocation,而不是指望计划字段自动追写。


一句话记忆

Odoo 的 accrual plan 修改,更像改变未来轨道,而不是重写过去轨迹;milestone、carryover 和 transition 一旦跑进历史,就不能把它们当普通公式字段随手回刷。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。