项目深度

Odoo 里的剩余工时为什么有两套:任务 `remaining_hours` 与销售订单行 `remaining_hours_so` 到底各管什么

很多人看到 Odoo 任务上既有剩余工时,又能看到销售订单行剩余工时,会觉得重复。其实这两者分别回答的是“任务预算还剩多少”和“合同还能消耗多少”,是执行视角与商业视角的两套余额系统。

项目
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 35 阅读

先说结论

很多人在 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.pysale_order_line.py,会发现这两套字段压根不是在回答同一个问题。

它们分别回答的是:

  1. 这张任务按执行预算还剩多少时间
  2. 这张任务背后的销售承诺还剩多少可消耗时间

前者是交付执行视角,后者是商业合同视角。

所以它们不是重复字段,而是 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_timesheethr_timesheet 都会在 certain context 下,把“剩余时间”拼到显示名里。

但要注意,Odoo 其实有两套显示逻辑:

  • 一套展示任务执行剩余时间
  • 一套展示销售订单行合同剩余时间

如果不理解底层语义,很容易把它们都看成“任务还剩多少小时”。

实际上一个是在说:

  • 这张任务的预算还剩多少

另一个是在说:

  • 这张任务默认绑定的合同余额还剩多少

它们看起来像近义词,业务含义却完全不同。


第七层:为什么这套设计对预付工时包特别重要

假设你卖给客户一包 100 小时顾问服务。

项目开始后,实际执行会出现至少三种层次:

1. 项目级

  • 合同总包还剩多少小时

2. 任务级

  • 某个实施任务当前预算还剩多少

3. 工时归属级

  • 某条 timesheet 到底算进哪条销售订单行

remaining_hoursremaining_hours_so 的并存,刚好分别覆盖了第 2 层和第 1 / 3 层之间的桥接。

没有这套区分,项目经理很容易在执行上“感觉还行”,但商业上已经超消耗了。


新手最容易误解的 5 件事

1. 以为 remaining_hoursremaining_hours_so 是同一个字段的不同展示

不对。它们属于两套不同业务语义。

2. 以为任务剩余时间一定等于合同剩余时间

不对。任务预算和销售承诺天然可能不一致。

不对。源码还考虑了 timesheet 对销售行归属的动态偏移。

4. 以为所有服务产品都应该有合同剩余额

不对。只有 prepaid / fixed price 且时间单位场景才特别适合这个概念。

5. 以为项目超时问题只要看任务 overtime 就够了

不对。很多项目真正先爆的是合同余额,而不是任务预算。


实战里最该注意什么

1. 管交付的人看 remaining_hours,管经营的人也要看 remaining_hours_so

只盯一边,都会漏风险。

2. 如果任务下工时经常改归属销售行,要特别警惕任务上显示的合同余额变化

这往往是“同一张任务在吃不同合同桶”的信号。

3. 做预付工时包项目时,最好把“预算余额”和“合同余额”当成两个独立看板指标

这是 Odoo 源码已经明确表达出来的运营现实。


一句话记忆法

Odoo 里的任务 remaining_hours 管的是执行预算,remaining_hours_so 管的是合同余额;一个回答“还能做多久”,另一个回答“还能按当前销售承诺计费多久”。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。