先说结论
如果你还把 Odoo 员工理解成“一个员工 = 一条固定记录 = 一份合同”,那已经太扁平了。
从源码看,Odoo 现在更接近这样建模:
hr.employee是员工主对象hr.version是员工在不同时间段的有效版本- 合同日期只是版本里的一个重要维度,不是全部
也就是说,员工不是只有一个静态档案,而是一条会随时间演化的版本时间线。
为什么要引入 hr.version
现实里的员工不会只变一次。
一个人可能会经历:
- 入职
- 试用期结束
- 岗位调整
- 部门调整
- 工时日历变化
- 合同续签
- 工资变化
- 离职或未来待生效变更
如果这些都直接覆盖在 hr.employee 当前字段上,系统会失去一个很关键的能力:
我想知道“某一天”这个员工到底处于什么状态。
而 hr.version 就是在解决这个问题。
date_version、contract_date_start、date_start 到底有什么区别
这是很多人第一次看源码时最容易混的地方。
1. date_version
表示这份版本从哪一天开始生效。
它更像“版本切换点”。
2. contract_date_start / contract_date_end
表示合同区间。
它回答的是:这个员工在合同意义上,从什么时候到什么时候处于合同内。
3. date_start / date_end
源码里 _compute_dates() 会根据:
- 当前版本的
date_version - 当前版本的合同日期
- 下一版本的
date_version
计算出这份版本真正对外生效的区间。
所以它不是手填,而是算出来的“有效时间窗”。
这点很重要:
版本切换点 和 合同边界 不是同一个概念,但会一起决定最终有效期。
为什么一个员工不能有重叠合同区间
hr.version 的约束里会检查:同一员工的合同时间不能互相重叠。
这背后的业务含义很直接:
- 你可以有多个连续版本
- 你也可以有未来版本
- 但不能让同一名员工在同一时期同时落入两段冲突合同里
否则后面的:
- work entries 生成
- 排班推导
- 薪资期间计算
- 出勤/请假归属
都会失真。
所以这个约束不是“表单校验太严格”,而是在保护整条时间线的可计算性。
is_current、is_past、is_future 为什么比看字段值更重要
源码里直接计算了:
is_currentis_pastis_future
这意味着 Odoo 很在意一个版本处在时间轴上的位置,而不只是字段内容本身。
比如同样一条调岗记录:
- 今天生效,就是当前版本
- 下个月生效,就是未来版本
- 去年那条,就是历史版本
系统后续很多判断,其实都依赖这个“时态”,而不仅仅是岗位名或工资金额。
为什么 create_version() 不是简单复制一行
源码里的 create_version() 做的事情比“复制旧记录”多得多:
- 找出指定日期应继承哪一个旧版本
- 判断那一天落在哪段合同区间中
- 必要时同步已有合同段的结束日期
- 复制能继承的字段
- 再写入新版本的差异值
所以版本不是“快照备份”,而是带时间语义的继承与切分。
这也解释了为什么做 HR 实施时,不能用“直接改当前员工字段”来理解所有业务变化。
create_contract() 暗示了什么
create_contract() 的逻辑也很有代表性:
- 如果同一天已有版本,就把合同日期写到该版本上
- 否则创建一个新的版本,把合同起点落进去
- 如果未来已有另一段合同,还会自动推导当前合同的结束边界
这说明在 Odoo 里,合同不是孤立漂浮的一张纸,而是嵌在员工版本时间线里的。
这套设计给实施带来的现实好处
1. 更容易表达“未来生效”
今天先录好下月调岗,不必立刻污染当前状态。
2. 更容易回答历史问题
比如:
- 上个月他的工时日历是什么?
- 那次请假时他属于哪个部门?
- 工资单期间到底挂哪份版本?
3. 更适合联动 work entry 和排班
因为系统能按时间取到正确版本,而不是只看员工“当前长什么样”。
实施时最危险的误解
最危险的一句往往是:
“反正员工就一条记录,直接改掉就好了。”
短期看很省事,长期会让历史失真。
更稳的理解应该是:
hr.employee是员工身份hr.version是员工在不同时间的状态片段- 合同只是这些状态片段中的关键边界之一
一句话记忆
Odoo 管的不是“一个静态员工”,而是一条随时间变化的员工版本时间线;看懂 hr.version,很多 HR 行为才真正说得通。
DISCUSSION
评论区