先说结论
在 Odoo 里,加班不是“晚走 12 分钟就一定算 12 分钟”。
源码真正做的是一套分层判断:
- 先看这条 overtime rule 是按 数量 算,还是按 时段 算
- 再看它有没有 公司有利宽容区(
employer_tolerance) - 再看它有没有 员工有利宽容区(
employee_tolerance) - 最后还要看这段考勤是不是跨天、是不是落在工作日/非工作日/排班外
所以很多人觉得“系统怎么老是差 5 分钟、10 分钟、甚至跨天后变两条”,其实不是算错,而是规则的语义比表面看起来细得多。
为什么官方要把 overtime 做成 ruleset,而不是一个固定公式
hr.attendance.overtime.rule 不是一个“倍率配置表”,而是一套识别额外工时的策略对象。
源码里核心字段非常直白:
base_off = quantity | timingtiming_type = work_days | non_work_days | leave | scheduleexpected_hours_from_contractexpected_hoursemployee_toleranceemployer_tolerancepaidamount_rate
这说明 Odoo 一开始就没想把“加班”理解成唯一公式。
同样是 extra hours,不同公司可能想表达的是:
- 每天超出标准工时后才算
- 只要落在夜班时间窗就算
- 周末整段都算
- 落在指定排班外才算
- 不足工时也要算成负向差额
也就是说,ruleset 管的是“哪些时间被识别成额外工时”,不是简单的算术差值。
quantity 和 timing 的差别,比很多实施顾问想的更大
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_tolerance 和 employee_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
评论区