很多团队把 Planning 接工时条目时,直觉是“班次一发布,系统自动生成 work entry”。这句话只说对了一半。/home/ubuntu/odoo-temp/enterprise/hr_work_entry_planning 真正难的地方在于:班次会改、班次可能重叠、工时已经验证后又不能随便动。
参考入口:
enterprise/hr_work_entry_planning/models/planning_slot.pyenterprise/hr_work_entry_planning/models/hr_work_entry.pyenterprise/hr_work_entry_planning/wizard/hr_work_entry_regeneration_wizard.pyenterprise/hr_work_entry_planning/tests/test_work_entry_planning.py
一、发布班次后不是增量补一条,而是按受影响区间批量重生
_create_work_entries() 的注释已经说明:Planning 的情况比 leave 更复杂,因为 slot 没有 overlap constraint,也不能简单把某条 work entry 局部修掉。
官方的做法是:
- 找出当前 slot 触碰到的版本、已有 work entry 和时间区间;
- 先把受影响的 work entries 统一归档;
- 再根据版本批量
_get_work_entries_values()重算; - 最后统一 postprocess 并 create。
所以它本质上是按区间重生成,不是“来一条班次就插一条工时”。
二、为什么 published slot 一创建就可能触发生成
create() 里对 state 为 published 的 slot 直接调用 _create_work_entries()。这保证了正式发布动作一发生,薪资侧可用的工时事实就同步建立起来,而不是等某个异步任务补全。
反过来说,草稿排班不是工资事实;只有进入 published,才开始影响 work entry。
三、validated work entry 会反向锁住班次
write() 与 _unlink_except_validated_work_entries() 都会搜索 state = validated 的关联工时。一旦存在,修改 resource_id / start_datetime / end_datetime / allocated_hours 或删除 slot 都会抛 UserError。
这条边界特别重要:不是班次永远比工时“上游”,而是一旦工时已经被验证,它就反过来成为受保护事实,班次不能再随意改写历史。
四、删除 slot 为什么只把工时 active=False
unlink() 先把关联 hr.work.entry 归档,再删 slot。本质原因是工资和审计链路需要保留这些工时记录的存在痕迹,不能因为排班被删就让工时事实凭空消失。
这也是 Odoo 常见的做法:业务上想“移除”,技术上更倾向于归档而不是硬删除。
五、_get_planning_duration() 说明 allocated hours 不是绝对真相
如果查询区间和 slot 完全一致,直接返回 allocated_hours;如果只是 slot 的子区间,就新建一个内存态 slot 按 planning 规则重新算时长。也就是说,allocated hours 不是可任意切片复用的固定值,切到子区间时要重新经过 planning 规则。
六、实战建议
- 排班改动频繁的团队,要接受 work entry 会按受影响区间整段重算,不要指望轻量 patch。
- 出现“排班改不了”的投诉时,第一反应先查是否已经生成并验证了工时。
- 删除班次后看见历史工时还在,不一定是脏数据,可能只是被归档保留。
七、结论
hr_work_entry_planning 真正解决的是排班事实与薪资事实如何安全衔接。批量重算、validated 锁定和归档保留,都是为了让班次可运营、工时可结算、历史可审计。
DISCUSSION
评论区