人力资源

Odoo 考勤里的加班为什么总“差几分钟”:看懂 Ruleset、公司宽容、员工宽容与跨天切分

很多团队以为 Odoo 的考勤加班就是下班晚几分钟就多几分钟。源码并不是这么算的。真正决定结果的是 ruleset、company/employee tolerance、按时段切分,以及跨午夜后重新分日的逻辑。

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

先说结论

在 Odoo 里,加班不是“晚走 12 分钟就一定算 12 分钟”

源码真正做的是一套分层判断:

  1. 先看这条 overtime rule 是按 数量 算,还是按 时段
  2. 再看它有没有 公司有利宽容区employer_tolerance
  3. 再看它有没有 员工有利宽容区employee_tolerance
  4. 最后还要看这段考勤是不是跨天、是不是落在工作日/非工作日/排班外

所以很多人觉得“系统怎么老是差 5 分钟、10 分钟、甚至跨天后变两条”,其实不是算错,而是规则的语义比表面看起来细得多


为什么官方要把 overtime 做成 ruleset,而不是一个固定公式

hr.attendance.overtime.rule 不是一个“倍率配置表”,而是一套识别额外工时的策略对象

源码里核心字段非常直白:

  • base_off = quantity | timing
  • timing_type = work_days | non_work_days | leave | schedule
  • expected_hours_from_contract
  • expected_hours
  • employee_tolerance
  • employer_tolerance
  • paid
  • amount_rate

这说明 Odoo 一开始就没想把“加班”理解成唯一公式。

同样是 extra hours,不同公司可能想表达的是:

  • 每天超出标准工时后才算
  • 只要落在夜班时间窗就算
  • 周末整段都算
  • 落在指定排班外才算
  • 不足工时也要算成负向差额

也就是说,ruleset 管的是“哪些时间被识别成额外工时”,不是简单的算术差值。


quantitytiming 的差别,比很多实施顾问想的更大

quantity:你多了多少,少了多少

如果规则基于 quantity,系统会先判断你相对“应工作时长”是多还是少。

这个“应工作时长”可能来自:

  • 员工合同/排班(expected_hours_from_contract = True
  • 规则手工指定的 usual work hours

这类规则最适合:

  • 标准工时超出后算加班
  • 一周累计超时后算加班
  • 缺勤管理开启时,把不足工时算成负向 extra hours

timing:不是多了多少,而是多出来的时间落在哪

如果规则基于 timing,系统更像在问:

这段 attendance 有没有落在某个被定义为“额外”的时间窗里?

源码支持的语义包括:

  • 任何工作日上的指定时段
  • 任何非工作日上的指定时段
  • 排班外(schedule
  • 员工 off / leave 期间

这意味着同样是 2 小时加班:

  • quantity 规则里,它可能只是“超出标准工时 2 小时”
  • timing 规则里,它可能只取晚 18:00 之后那一段,哪怕全天总工时没有超很多

很多项目把这两种语义混用,最后就会觉得 Odoo 的结果“不稳定”。其实只是规则基准不同


公司宽容区和员工宽容区,不是同一个方向的“免计分钟”

源码里 employer_toleranceemployee_tolerance 分开存在,这是很关键的设计。

employer_tolerance:公司有利的缓冲区

测试里有个很典型的例子:员工当天只比标准工时多了 10 分钟。

  • 如果 employer_tolerance = 10/60,系统可能直接判定 不生成 overtime line
  • 把阈值降低到 4 分钟,再触发 action_regenerate_overtimes(),那 10 分钟又会出现

也就是说,这个字段更像:

“少量超时先不认,超过阈值再算。”

它不是四舍五入,也不是展示层美化,而是是否入账的边界。

employee_tolerance:员工有利的缓冲区

反方向同样存在。

如果员工当天少上了 10 分钟,且系统开启了缺勤管理,源码允许把它识别成负向 overtime

但如果 employee_tolerance 足够大,这 10 分钟可以先被容忍,不生成负向差额;阈值缩小后,再重算就会出现 -10/60 的 overtime duration。

所以这不是一个对称的小功能,而是两条业务政策:

  • 公司愿意忽略多小的“多上班”
  • 公司愿意忽略多小的“少上班”

两边阈值可以完全不同。


为什么“中午不休息多做一小时”不一定算加班

测试里还有一个非常反直觉但很重要的案例:

  • 8:00 到 17:00 全程不断
  • 7:00 到 16:00 全程不断
  • 9:00 到 18:00 全程不断

这些情况下,系统可能都给出 0 extra hours

原因不是它忽略了总时长,而是它把午休视为排班结构的一部分,而不是“多做了就自动变加班”的奖励槽位。

换句话说,Odoo 并不是只看总秒数,还会看:

  • 这段时间是否占用了本应工作的区间
  • 额外部分是否真的落在规则认定的 overtime window 中

这就是为什么很多企业觉得“员工没休午饭,为什么系统不加一小时班”。

因为源码默认不把“穿透午休”直接等价成可认定加班。


跨午夜后,为什么一条 attendance 会长出两条 overtime

测试里还明确覆盖了跨天场景:

  • 周五 8:00 check in
  • 周六 3:00 check out

结果不是简单得到一条 10 小时 overtime,而是按天切分:

  • 周五晚上的一段
  • 周六凌晨的一段

这背后对应的是规则天然按“日期语义”运作:

  • 哪天是工作日
  • 哪天是非工作日
  • 哪天适用哪套 version / timezone / unusual days

所以跨午夜不是 UI 小细节,而是规则解释边界改变

如果你把跨天 attendance 粗暴当一整块处理,就会把工作日夜间和非工作日凌晨混成一笔,最后加班政策就失真了。


action_regenerate_overtimes() 暗示了什么

测试多次说明:只要调整 tolerance 或 rule,系统通常需要通过 action_regenerate_overtimes() 重建结果。

这和普通汇总字段非常不同。

它说明 overtime line 不是“写死的最终事实”,而是由 attendance + rule + company policy 推导出来的派生结果

这也解释了为什么我一直不建议在项目里直接手改 overtime 汇总值:

  • 你改的是结果,不是规则来源
  • 下一次重算,人工修补就会被覆盖
  • 源码本身就是按“删掉并重建”的思路工作

正确做法通常是:

  • 调整 ruleset
  • 明确 tolerance 政策
  • 再统一重算

而不是直接在员工当天记录上补一个数字。


实施时最容易踩的 4 个坑

坑一:把 tolerance 当成显示层四舍五入

不是。它会直接影响 overtime line 是否存在。

坑二:只配一条 quantity rule,想覆盖所有业务场景

现实里夜班、周末、排班外、非工作日常常需要 timing 规则配合。

坑三:忽略负向 overtime

如果启用了 absence management,少工时不只是“不算加班”,它可能成为真正的负向差额。

坑四:跨天后还按单日思维看结果

只要跨午夜,工作日/非工作日边界、时区边界、规则边界都可能重新生效。


我会怎么给业务方做一句话解释

如果业务方问:“为什么系统不是晚走几分钟就算几分钟?”

我会直接回答:

因为 Odoo 算的不是‘钟表上的多待了多久’,而是‘这些时间有没有跨过公司定义的加班识别边界’。

这句话比讲字段更容易让人理解。


一句话记忆

Odoo 的 overtime 不是简单的时长差,而是 ruleset、时段切分、双向 tolerance 和跨天语义共同决定的识别结果。

DISCUSSION

评论区

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