很多实施把项目预测容量理解成一个很粗的公式:计划工时 = 排班小时,员工请假时再手动减掉。但 Odoo 企业版不是这样设计的。
在企业版里,请假和公共假日并不是“报表层扣一笔”,而是会直接进入 planning.slot 的底层工时计算,让 slot 自己的 allocated_hours 发生变化;而项目预测页面、项目看板上的 forecast 小时,本质上只是把这些已经被重算过的 slot 再聚合一遍。
这条链的关键代码主要在:
enterprise/planning_holidays/models/resource_calendar_leave.pyenterprise/planning_holidays/models/resource_calendar.pyenterprise/planning_holidays/models/planning_slot.pyenterprise/project_forecast/models/project_project.py
一、为什么请假不是“最后展示时减掉”
planning_holidays.models.resource_calendar_leave.ResourceCalendarLeaves 里最关键的方法是:
_process_shifts_domain()_recompute_shifts_in_leave_periods()
它的思路不是去改 project 报表,而是先找出在请假区间内、且受该资源或全局假日影响的 planning slots,然后把这些 slot 加入 allocated_hours 的重算队列。
也就是说:
- 新建请假 / 公共假日;
- 系统圈出受影响的 shifts;
- 这些 shifts 的
allocated_hours重新计算; - 项目预测再读取这些被重算后的 slot。
这就是为什么你会看到:项目 forecast 的数字变化,往往不是 project 模块自己扣的,而是 planning 层先变了。
二、公共假日和员工个人请假,作用层级并不一样
_process_shifts_domain() 很值得细看。
它把假期分成两类:
- 有
resource_id的:员工/资源级请假; - 没有
resource_id的:全局公共假日。
因此它生成 domain 时,会同时考虑两种覆盖方式:
- 某个具体资源在某时间段不可用;
- 整个公司/日历层在某时间段不可用。
这意味着项目经理常见的一个误解是错的:
“我只给员工批了假,不会影响 forecast 模板本身。”
不一定。只要该 slot 绑定了资源,而且时间段被覆盖,allocated_hours 就要重算;如果是公共假日,则可能影响更广的一批 slots。
三、半天假为什么不是简单砍成 4 小时
planning_holidays.models.resource_calendar.ResourceCalendarInherit._handle_flexible_leave_interval() 专门处理灵活假期与半天假。
这里不是写死“半天 = 4 小时”,而是按请假的请求粒度去改区间边界:
- 上午半天:从当天 00:00 起算到中午;
- 下午半天:从中午开始到当天结束;
- 不是半天假时,再交给上层逻辑处理。
这说明 Odoo 的判断口径是日历区间边界,不是写死一个经验工时值。真正扣掉多少小时,仍然依赖该员工/资源日历里的工作时间结构。
所以同样叫“半天假”,在不同工作日历下:
- 可能不是整齐的 4 小时;
- 也可能影响上午/下午不同班段;
- 对跨时区或弹性班表资源,结果更不应手算。
四、slot 上到底会看到什么变化
planning_holidays.models.planning_slot.PlanningSlot 里有两个常被忽略的信号:
leave_warningis_absent
_compute_leave_warning() 会调用 hr.leave._get_leave_interval() 和 _get_leave_warning(),把请假提示直接挂到 shift 上。也就是说,系统不仅改工时,还会在 UI 上告诉你:这个人这段时间其实在请假。
但更关键的是,真正影响容量的并不是 warning,而是前面提到的 allocated_hours 重算。
换句话说:
leave_warning是给人看的;allocated_hours是给容量模型和聚合结果看的。
很多人只盯 warning,不盯工时字段本身,所以才会误以为“系统只是提示冲突,没有真的改预测口径”。
五、项目 forecast 为什么会跟着变
project_forecast.models.project_project.ProjectProject._compute_total_forecast_time() 非常直接:
- 对
planning.slot按project_id做_read_group - 聚合
allocated_hours:sum - 再写入
total_forecast_time
这说明项目页面上的 forecast 总工时,本质不是另一套算法,而是:
项目 = 该项目下所有 planning slots 的 allocated_hours 汇总
因此只要 leave / holiday 机制把 slot 的 allocated_hours 改掉,项目 forecast 就会同步变化。
这也是为什么有时用户会困惑:
- 我没改 project;
- 我只是批了个假;
- 为什么项目预测小时数变了?
答案是:因为 project 读的是 planning 的结果,不是 project 自己保存了一份独立容量值。
六、真正该关注的是“工时口径是否已经进 slot”
实施里要避免三类错误判断。
1. 以为批准请假后,只会影响 HR,不会影响项目容量
错。resource.calendar.leaves 的 create / write / unlink 都会触发 _recompute_shifts_in_leave_periods(),只要时间段命中,slot 工时就会被重算。
2. 以为 forecast 页面上的小时数可以和原始 slot 时长简单对比
也错。slot 的显示时长、开始结束时间,与最终被聚合的 allocated_hours 并不总是同一个数字。公共假日、半天假、资源日历都会改变后者。
3. 以为半天假就是“人工减 0.5 天”
依旧不对。企业版按区间边界和资源日历去算,不是按管理口语去算。
七、实战建议
- 做项目预测实施时,优先核对员工的
resource.calendar,不要只测 project 视图。 - 如果客户常抱怨 forecast 不准,先检查是否启用了公共假日、半天假和弹性日历,而不是先怀疑项目模块。
- 自定义报表时,尽量直接读取
planning.slot.allocated_hours的最终值,不要自己用开始结束时间重算一次。 - 遇到“批假后项目 forecast 变了”的工单,先解释这是设计预期,不是异常数据漂移。
八、结论
Odoo 企业版的项目容量预测,核心不是“项目模块里有个减法公式”,而是请假和公共假日先改 planning slot 的可计入工时,再由 project forecast 做聚合。所以真正决定结果的,不是你在项目里看到了多少小时,而是底层 allocated_hours 在资源日历与假期规则作用后,还剩多少可分配容量。
DISCUSSION
评论区