先说结论
很多人在 Odoo 项目里会看到两类很像的字段:
- 任务自己的
remaining_hours - 任务 / 销售上下文里的
remaining_hours_so
于是第一反应通常是:
- 这不是重复了吗
- 为什么系统不保留一套就好
如果你去看 /home/ubuntu/odoo-temp/addons/hr_timesheet/models/project_task.py、/home/ubuntu/odoo-temp/addons/sale_timesheet/models/project_task.py 和 sale_order_line.py,会发现这两套字段压根不是在回答同一个问题。
它们分别回答的是:
- 这张任务按执行预算还剩多少时间
- 这张任务背后的销售承诺还剩多少可消耗时间
前者是交付执行视角,后者是商业合同视角。
所以它们不是重复字段,而是 Odoo 有意保留的两套余额系统。
第一层:任务 remaining_hours 关注的是执行预算
在 hr_timesheet/models/project_task.py 里,任务 remaining_hours 的计算逻辑非常直接:
allocated_hours- 减去
effective_hours - 再减去
subtask_effective_hours
也就是:
任务还剩多少计划工时可用。
这套逻辑有几个特点:
- 只从任务及其子任务的实际工时出发
- 不关心这批工时最后能不能计费
- 也不关心销售订单行还剩多少量
它关心的是项目经理最熟悉的那个问题:
- 这项工作还剩多少预算
- 是否已经 overtime
- 子任务是不是把父任务预算吃穿了
所以 remaining_hours 的世界是“执行管理世界”。
第二层:销售订单行 remaining_hours 关注的是合同余额
在 sale_timesheet/models/sale_order_line.py 里,销售订单行也会计算自己的 remaining_hours,但含义完全不同。
它大致等于:
- 订单行售出的数量
- 减去已交付数量
qty_delivered - 再统一换算成小时
前提还要求:
- 产品是 prepaid / fixed price
- 并且是时间类单位
所以销售订单行上的 remaining_hours 关注的是:
这条服务合同还剩多少可消耗时间。
这跟任务预算不是一回事。
因为现实里完全可能出现:
- 合同还剩 40 小时
- 但某张任务预算只剩 6 小时
也可能出现反过来:
- 任务预算还剩不少
- 但客户合同可计费时间已经快用完
这两种张力,恰恰是项目经营中最常见、也最容易被忽略的问题。
第三层:remaining_hours_so 是把合同余额投影回任务视角
sale_timesheet/models/project_task.py 里新增的 remaining_hours_so,本质上是在做一件桥接工作:
- 它不是重算任务预算
- 而是把任务默认销售行所对应的合同余额,投影到任务上展示出来
源码的计算思路很有意思。
它先取:
- 当前任务
sale_line_id.remaining_hours
再结合任务下 timesheet 的改动做一个 delta 调整,尤其处理这些情况:
- timesheet 原本挂在任务默认销售行上
- 但现在被改到了别的销售行
- 或者原来不在默认销售行上,现在又切回来了
这说明 remaining_hours_so 不是一个死板的 related 字段,而是一个考虑工时销售归属漂移后的动态合同余额。
官方显然知道:
- 一张任务底下的工时,不一定永远都走任务默认销售行
- 有些工时会被重分配到别的销售行
- 如果只做简单 related 展示,任务上的合同剩余额就会误导人
第四层:为什么 Odoo 要同时保留任务预算余额与合同余额
因为项目执行和合同消耗从来就不是同一条线。
执行侧关心
- 任务是否超预算
- 子任务是否把父任务时间吃掉
- 当前交付进度如何
商业侧关心
- 这张销售订单行还能不能继续吃工时
- 客户购买的服务包还有多少余额
- 当前任务继续做下去,会不会已经超出可计费范围
如果系统只留任务 remaining_hours,你会看不到合同余额风险。
如果系统只留 remaining_hours_so,你又会失去执行控制。
Odoo 选择两套并存,本质上是在承认:
项目管理不是只管做完,还要管这批工作还能不能按当前合同继续计费。
第五层:为什么 remaining_hours_so 只在某些销售策略下才有意义
sale_order_line.py 里有个 remaining_hours_available,只有满足这些条件时才成立:
service_policy == ordered_prepaid- 销售单位属于时间类单位
也就是说,合同剩余额这件事,并不是所有服务产品都适用。
比如:
- timesheet delivered 型产品,重点更偏已交付与待开票
- milestones 型产品,重点是 milestone 到没到
- manual delivered 型产品,重点是人工调整交付量
只有那种“客户预购了一包时间”的场景,合同剩余小时数才是核心控制指标。
这也是为什么 remaining_hours_so 不是 everywhere 都出现。它只在真正有合同余额语义时才值得强调。
第六层:任务名称里显示的“剩余时间”也可能不是你想的那个余额
sale_timesheet 和 hr_timesheet 都会在 certain context 下,把“剩余时间”拼到显示名里。
但要注意,Odoo 其实有两套显示逻辑:
- 一套展示任务执行剩余时间
- 一套展示销售订单行合同剩余时间
如果不理解底层语义,很容易把它们都看成“任务还剩多少小时”。
实际上一个是在说:
- 这张任务的预算还剩多少
另一个是在说:
- 这张任务默认绑定的合同余额还剩多少
它们看起来像近义词,业务含义却完全不同。
第七层:为什么这套设计对预付工时包特别重要
假设你卖给客户一包 100 小时顾问服务。
项目开始后,实际执行会出现至少三种层次:
1. 项目级
- 合同总包还剩多少小时
2. 任务级
- 某个实施任务当前预算还剩多少
3. 工时归属级
- 某条 timesheet 到底算进哪条销售订单行
remaining_hours 与 remaining_hours_so 的并存,刚好分别覆盖了第 2 层和第 1 / 3 层之间的桥接。
没有这套区分,项目经理很容易在执行上“感觉还行”,但商业上已经超消耗了。
新手最容易误解的 5 件事
1. 以为 remaining_hours 和 remaining_hours_so 是同一个字段的不同展示
不对。它们属于两套不同业务语义。
2. 以为任务剩余时间一定等于合同剩余时间
不对。任务预算和销售承诺天然可能不一致。
3. 以为 remaining_hours_so 只是 sale line 的 related 字段
不对。源码还考虑了 timesheet 对销售行归属的动态偏移。
4. 以为所有服务产品都应该有合同剩余额
不对。只有 prepaid / fixed price 且时间单位场景才特别适合这个概念。
5. 以为项目超时问题只要看任务 overtime 就够了
不对。很多项目真正先爆的是合同余额,而不是任务预算。
实战里最该注意什么
1. 管交付的人看 remaining_hours,管经营的人也要看 remaining_hours_so
只盯一边,都会漏风险。
2. 如果任务下工时经常改归属销售行,要特别警惕任务上显示的合同余额变化
这往往是“同一张任务在吃不同合同桶”的信号。
3. 做预付工时包项目时,最好把“预算余额”和“合同余额”当成两个独立看板指标
这是 Odoo 源码已经明确表达出来的运营现实。
一句话记忆法
Odoo 里的任务
remaining_hours管的是执行预算,remaining_hours_so管的是合同余额;一个回答“还能做多久”,另一个回答“还能按当前销售承诺计费多久”。
DISCUSSION
评论区