先说结论
在 Odoo 里,Work Entry 从来不是“这个员工这个月有一条工时总账”那么粗。
源码真正做的是:
- 先找出员工在这个区间内有效的 contract version
- 再按每个 version 的合同日期、版本日期、日历/排班去生成工时
- 如果期间有请假,再让 leave 去覆盖或替换对应 work entries
- 如果月中换了合同、换了排班、从固定班变弹性班,系统就会把同一个工资期切成几段
所以很多人看到“同一个月为什么前半段 8 小时、后半段 3 小时”时,以为系统坏了。
其实这是合同时间线在工资工时里的自然投影。
为什么 work entry 不是挂在 employee 上直接一把算完
hr.work.entry 看起来是员工级对象,但生成逻辑真正依赖的是 hr.version。
这非常关键。
因为员工主对象只回答“这个人是谁”,而 work entry 需要回答:
- 这一天适用哪份合同
- 这一天适用哪个日历
- 这一天是不是已经切进未来版本
- 这一天的 work entry type 应该是什么
也就是说,work entry 需要时间切片后的雇佣事实,而不是一条静态员工档案。
contract_date_start、contract_date_end 和 date_version 的边界都要算进去
之前很多人只盯合同起止日期,但源码里 version 还同时有:
contract_date_startcontract_date_enddate_version
这些字段共同决定 version 的有效区间。
换句话说,不是“有合同就生效”,而是要同时满足:
- 这份 version 已经到了它的版本生效时间
- 它的合同日期区间覆盖当前生成窗口
因此月中调整合同,或者提前录入未来生效版本,都会影响 work entry 的切分方式。
多合同场景下,为什么同一个月天然会被拆成两半甚至更多
hr_work_entry 的测试里有非常典型的用例:
- 9 月 1 日到 15 日:一个日历 / 一份合同
- 9 月 16 日到 30 日:换成另一份日历 / 另一份合同
系统生成结果会明显分成两个区间:
- 前半月按第一份 version 的日历时长生成
- 后半月按第二份 version 的日历时长生成
而且这里不只是“合同名变了”,连每天生成几条、每条多少小时、周末算不算工作都可能不同。
测试甚至覆盖了这些切换:
- flexible → standard
- standard → flexible
- fully flexible → standard
- standard → fully flexible
说明 Odoo 真正在意的不是“有没有续约”,而是:
续约后适用的工作时间制度有没有变化。
为什么弹性班和标准班切换后,work entry 条数都会变
测试里很直观:
- 标准班那一半,没有周末工时,常见是每天 8 小时
- 弹性班或 fully flexible 那一半,可能周末也会生成,且时长可能按另一套逻辑,比如每天 3 小时
这代表 Work Entry 的职责并不是“忠实抄合同名称”,而是要把合同绑定的可结算工作时间结构落地。
所以续签合同时只改工资不改日历,和同时改日历,对 payroll side 的影响完全不同。
请假一旦跨合同边界,为什么更容易让人看不懂
hr_work_entry_holidays 的多合同测试也说明了一个关键点:
- 请假发生在第二份合同期间
- 生成 work entries 后,只过滤第二份 contract 对应的记录
- 最终工作工时和请假工时分别在这份合同下统计
这说明 Leave 并不会悬空地覆盖“员工整个月”,而是覆盖它所落入的有效 version 区间。
所以如果一个请假刚好跨:
- 合同结束日
- 新合同开始日
- version 切换日
那最终结果就可能出现:
- 一部分由旧合同语义解释
- 一部分由新合同语义解释
- leave work entry 只覆盖相应区间的原工作条目
这也是为什么多合同请假问题永远比单合同复杂。
为什么很多改动后都需要 regenerate,而不是手补几条工时
Work Entry 的生成逻辑跟 leave 覆盖逻辑一样,都高度依赖时间区间和 version 边界。
这意味着一旦你改了这些东西:
- 合同起止
- 版本日期
- 资源日历
- 请假批准状态
旧结果很可能已经不再可信。
源码之所以大量采用重建思路,是因为这里最重要的是区间一致性。
手工补一条,看似改对了当日,但常常会把:
- 相邻区间
- 覆盖关系
- payroll validation 前后的状态
一起弄乱。
所以这类问题我通常建议:
- 先定位当前版本边界
- 再看合同与日历变化
- 最后统一 regenerate
而不是直接在 work entry 表面修一条。
这对 payroll/HR 实施最大的提醒是什么
1)“续签合同”不是纯 HR 动作
只要合同带着日历和版本边界进入系统,它就会直接影响 payroll 输入层。
2)未来生效版本不是无害草稿
即使今天还没生效,它也已经在影响后续区间的切分逻辑。
3)请假问题不能只看 leave 表
尤其多合同场景,必须一起看 version、contract window 和生成后的 work entries。
4)先问“哪份 version 负责这一天”
这是排查所有 work entry 边界问题最快的入口。
我会给业务方的一句话解释
如果业务方问:“为什么同一个工资月会被系统拆成两套工时?”
我会说:
因为 Odoo 发薪前看的不是‘这个月有没有上班’,而是‘这个月每一天到底归哪份合同版本负责’。
只要这个问题的答案发生变化,work entry 就一定会分段。
一句话记忆
Odoo 的 work entry 是按合同版本时间线生成的;月中换合同、换日历、换请假状态,都会把同一工资期切成不同的责任区间。
DISCUSSION
评论区