项目深度

Odoo 项目阶段耗时为什么不是“卡片在哪一列待了多久”:duration_tracking、状态切换与工作日历口径讲透

很多团队以为 Odoo 项目里的阶段耗时,就是任务在某一列里停留了多久。但源码真正实现的是“阶段跟踪 + 打开/关闭工时 + 资源日历”三套口径并行。本文把 `duration_tracking`、`mail.tracking.duration.mixin`、`working_hours_open/close` 和 stage onchange 的关系一次讲清。

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

先说结论

在 Odoo 项目里,“阶段耗时”不是单一指标,而是至少三层并行口径

  • duration_tracking:记录任务在各阶段累计待了多久
  • working_hours_open / working_days_open:从创建到首次被接手的工作时长
  • working_hours_close / working_days_close:从创建到关闭的工作时长

如果你直接把它理解成“任务在某一列待了多久”,实施时就很容易踩坑:

  • 看板拖列看到的是 stage_id 变化
  • 状态栏和 chatter 跟踪用的是 mail.tracking.duration.mixin
  • 工时口径还要再叠加 project.resource_calendar_id
  • 表单里只是 onchange 预演,也会影响前端看到的即时值

所以正确理解应该是:Odoo 同时维护“阶段停留历史”和“按工作日历折算的生命周期时长”,二者用途完全不同。


1. 阶段耗时的第一层:duration_tracking 不是报表装饰,而是 mixin 级机制

/home/ubuntu/odoo-temp/addons/project/models/project_task.py 里,project.task 继承了 mail.tracking.duration.mixin,并显式声明:

  • _track_duration_field = 'stage_id'
  • 模型字段里有 duration_tracking

这意味着项目任务的阶段耗时,不是项目模块单独手搓的一段统计 SQL,而是 mail tracking 框架在监听 stage_id 变化

它背后的含义有两个:

  1. 只要阶段发生切换,系统就会把旧阶段的累计时长写回 duration_tracking
  2. 这个结果本质是“阶段历史账本”,不是“当前 UI 上肉眼看到的停留感受”

也正因为是 tracking 机制,所以它特别适合回答这类问题:

  • 这个任务一共在“开发中”待过多少时间?
  • 它是不是在“待确认”与“返工”之间来回震荡过?
  • 批量改阶段时,时长统计会不会炸?

测试 /home/ubuntu/odoo-temp/addons/project/tests/test_project_task_mail_tracking_duration.py 也专门验证了三件事:

  • 单条任务阶段耗时可跟踪
  • 批量场景下也能跟踪
  • onchange 预演时,旧阶段累计值不会被莫名覆盖,新阶段会从 0 开始

这说明 Odoo 对这个字段的定位非常明确:它是可累计、可批处理、可在 UI 中即时反馈的阶段时间账本。


2. 第二层:为什么 working_hours_open / working_hours_closeduration_tracking 不是一回事

同一个模型里,_compute_elapsed 又维护了另一组字段:

  • working_hours_open
  • working_days_open
  • working_hours_close
  • working_days_close

源码逻辑很直接:

  • 只对“项目上配置了 resource_calendar_id 且任务有 create_date”的记录计算
  • date_assign 存在时,计算“创建 → 首次分派”的工作时长
  • date_end 存在时,计算“创建 → 结束”的工作时长
  • 调用的是 resource_calendar_id.get_work_duration_data(..., compute_leaves=True)

这跟 duration_tracking 的区别非常大:

duration_tracking 回答的是

任务在各阶段累计待了多久。

working_hours_open/close 回答的是

按项目工作日历折算后,这个任务从创建到被接手、从创建到关闭,跨过了多少“有效工作时间”。

所以你会看到一个常见现象:

  • 某任务在自然时间上跨了 5 天
  • working_hours_close 可能只有 16 小时

原因不是统计错了,而是因为源码明确把请假、非工作时段、工作日历都纳进了折算逻辑。

换句话说,一个是阶段历史,一个是生命周期 SLA;一个偏过程分析,一个偏交付管理。


3. 第三层:为什么表单里拖阶段时,数字会“当场变化”

test_task_mail_tracking_duration_during_onchange_stage 这个测试非常关键。

它模拟的不是数据库里已经提交的阶段变更,而是:

  • 打开表单
  • 在 Form/onchange 环境里切换 stage_id
  • 检查 duration_tracking 的前后值

断言结果是:

  • 旧阶段累计值保持不变
  • 新阶段条目即时出现,并且初值为 0

这透露了一个实现上的重要设计:

Odoo 希望用户在表单里切阶段时,就看到“现在这次切换会如何影响阶段时长结构”,而不是等保存后再回头猜。

这对实施很重要,因为很多顾问会把前端即时显示误判成“已经写库”。

实际上它更接近:

  • UI 侧先拿到 tracking 结果的预期形态
  • 真正保存后才落成正式历史

所以如果你在调试时发现“表单看到的值”和“最终保存后的统计”有短暂差异,先别急着怪报表,先区分 onchange 预演持久化结果


4. 为什么 Odoo 要把这三套时间口径拆开

从产品设计上看,这种拆法其实非常合理。

阶段时长用于流程优化

你要回答的是:

  • 返工是不是集中发生在测试列?
  • 哪个审批阶段最容易拖延?
  • 团队是不是老把任务挂在“待客户确认”?

这时候你关心的是 duration_tracking

打开时长用于接单响应

你要回答的是:

  • 任务从创建到有人接手花了多久?
  • 资源分派是不是太慢?

这时候你关心的是 working_hours_open

关闭时长用于交付 SLA

你要回答的是:

  • 项目交付是否按工作日历内承诺完成?
  • 某类型任务平均关闭耗时是多少?

这时候你关心的是 working_hours_close

如果只保留“某列停留多久”这一种指标,很多管理问题根本分不开。


5. 实施与二开时最容易踩的坑

坑一:把阶段列名当成唯一时长维度

很多人会说“我们只看开发中列待了多久”。

问题是:

  • 列可能改名
  • 列可能跨项目复用
  • 列只是流程位置,不等于接单或关闭语义

坑二:忽略工作日历

如果项目上没配置 resource_calendar_id_compute_elapsed 那组字段会直接给 0

这不是 bug,而是源码明确要求:没有工作日历,就不计算工作口径时长。

坑三:把 onchange 结果当作最终审计结果

表单切列时看到的新阶段 0 值,是预演能力的一部分,不代表审计历史已经永久写入。

坑四:混淆“阶段耗时”和“工时录入”

duration_tracking 统计的是阶段停留;timesheet 统计的是人实际录入的 unit_amount。两者可以相关,但绝对不是同一件事。


6. 一句落地建议

如果你是实施顾问,最稳妥的做法不是问客户“你想看哪个列待了多久”,而是先把问题拆成三类:

  • 你要看流程瓶颈?用 duration_tracking
  • 你要看响应速度?用 working_hours_open
  • 你要看交付 SLA?用 working_hours_close

这才符合 Odoo 源码本来的建模方式。


参考源码

  • /home/ubuntu/odoo-temp/addons/project/models/project_task.py
  • mail.tracking.duration.mixin
  • _track_duration_field = 'stage_id'
  • _compute_elapsed
  • /home/ubuntu/odoo-temp/addons/project/tests/test_project_task_mail_tracking_duration.py
  • test_project_task_mail_tracking_duration
  • test_project_task_mail_tracking_duration_batch
  • test_task_mail_tracking_duration_during_onchange_stage

DISCUSSION

评论区

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