先说结论
在 Odoo 里,请假批准后发生的事情,绝不只是 hr.leave 状态从申请中变成已批准。
更深的一层是:
系统必须决定,这段时间里员工原本的工作安排要不要被请假覆盖,并且最终在
hr.work.entry里留下可结算、可校验的结果。
所以 Leave 和 Work Entry 的关系,本质上不是“一个请假单 + 一个标签”,而是时间占用权的重新分配。
为什么请假不能只存在于 hr.leave
如果请假只是单独一张单据,而不影响 work entries,会发生什么?
同一天可能同时出现:
- 正常出勤工时
- 请假工时
- 甚至薪资已验证的工时
那系统就没法回答一个很基础的问题:
这个人这一天到底是在上班,还是在请假?
因此 Odoo 在 hr_work_entry_holidays 里专门把请假接进了 work entry 体系。
批准请假时,系统会先创建“请假型 work entry”
源码里的 _cancel_work_entry_conflict() 名字很低调,实际做的事却很关键。
大致流程是:
- 找出请假期间与合同重叠的版本
- 为这些区间生成对应的 work entry 值
- 创建新的请假型
hr.work.entry - 再去对比同期间已存在的其他 work entries
也就是说,批准请假后,系统不是只说“这段时间有假”,而是会真正落一组可参与后续校验和结算的 work entries。
为什么有的旧工时会被归档,有的只会失去 leave 关联
源码里的处理非常细:
完全被请假覆盖的旧工时
会被标记为 active = False,也就是归档。
这代表:
- 这些旧工时已经不再应该参与当前结果
- 请假把它们完整替代了
与请假有交叉但不完全包含的工时
系统不会粗暴全删,而是把部分冲突对象挑出来处理。
对这些 overlapping entries,源码会把 leave_id 清掉(前提是不是 validated)。
这说明 Odoo 在做的不是“删除一切旧数据”,而是尽可能保留可解释的结构,只把应当让位的部分让位。
为什么 work entry 会进入 conflict
hr.work.entry 本身有独立的校验逻辑。
源码里至少会检查这些异常:
- 没有 work entry type
- 同一员工同一天总时长 <= 0 或 > 24 小时
- 请假类工时完全落在排班之外
- 已经 validated 的日期又被重新动到
这很能说明问题:
work entry 不是“展示层结果”,而是要被系统当真去校验的业务对象。
所以请假只要碰到:
- 排班边界
- 合同边界
- 已验证工资期间
- 与旧工时的重叠
就可能触发 conflict,而不是默默通过。
为什么“排班外请假”会报冲突
源码里的 _mark_leaves_outside_schedule() 专门检查:
- 请假型 work entries 是否落在理论排班之外
- 如果完全在排班外,就打成
conflict
这背后的业务逻辑很直白:
如果那天本来就不应该上班,你请掉的是谁的班?
当然,现实里有弹性工时、特殊制度,所以源码也对 flexible calendar 做了区别处理。也就是说,Odoo 并不是机械地说“排班外永远不准请”,而是在判断这段请假是否真的占用了应工作时间。
请假被拒绝或取消后,为什么还要“重建” work entries
很多系统做到批准就结束了,但 Odoo 更进一步:
action_refuse()_move_validate_leave_to_confirm()_action_user_cancel()
这些路径都会调用 _regen_work_entries()。
核心逻辑是:
- 找到与该请假关联的 work entries
- 先归档这些 leave entries
- 再按当天版本重新生成正常 attendance work entries
这说明 Odoo 的目标不是简单撤销请假单,而是把那段时间恢复成一个重新自洽的工时世界。
这对实施意味着什么
1. 不要把请假理解成纯审批表单
它其实会改写底层工时结果。
2. 不要忽视合同与版本区间
请假能不能正确映射成 work entries,取决于那段时间员工是否有有效合同版本。
3. 工资期已验证后,改请假会更敏感
因为系统会防止你悄悄修改已经进入薪资结算的工时。
最适合记住的一句话
Odoo 里的请假不是“贴在工时上面”的标签,而是会真正占用并重写那段 work entries;批准时覆盖,取消时重建。
DISCUSSION
评论区