先说结论
在 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 变化。
它背后的含义有两个:
- 只要阶段发生切换,系统就会把旧阶段的累计时长写回
duration_tracking - 这个结果本质是“阶段历史账本”,不是“当前 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_close 跟 duration_tracking 不是一回事
同一个模型里,_compute_elapsed 又维护了另一组字段:
working_hours_openworking_days_openworking_hours_closeworking_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.pymail.tracking.duration.mixin_track_duration_field = 'stage_id'_compute_elapsed/home/ubuntu/odoo-temp/addons/project/tests/test_project_task_mail_tracking_duration.pytest_project_task_mail_tracking_durationtest_project_task_mail_tracking_duration_batchtest_task_mail_tracking_duration_during_onchange_stage
DISCUSSION
评论区