先说结论
很多人看项目任务状态时,会把它理解成一件纯数据层的事:
state改了- 看板列或按钮变了
- 任务显示成已完成 / 已批准 / 待修改
但 addons/project/models/project_task.py 与 addons/project/data/mail_message_subtype_data.xml 结合起来看,
Odoo 真正关心的并不只是“状态值改没改”,而是:
这个状态变化应该如何被翻译成协作语义,并让任务层与项目层的参与者都在正确粒度上感知到它。
因此 approved、changes requested、waiting 并不只是状态码, 它们还是 subtype、通知语义,以及父子对象传播链的一部分。
一、为什么任务状态不能只当成字段枚举
如果任务状态只是界面显示值, 那做法会很简单:
- 改
state - 页面展示新状态
- 结束
但真实项目协作不是这样。
因为状态变化往往意味着责任关系改变:
approved说明这项工作已经通过某种确认changes requested说明执行者需要返工waiting说明当前卡在依赖或外部阻塞上done说明这项协作可以往后流转
这些都不是“字段改了”那么轻。
它们是团队对下一步协作关系的理解。
所以 Odoo 需要一套机制,把状态变化从数据库事件升级成沟通事件。
二、_track_subtype() 才是状态语义翻译器
在 project.task 里,最关键的方法之一就是 _track_subtype()。
源码里它建立了一张非常明确的映射:
1_done→project.mt_task_done1_canceled→project.mt_task_canceled01_in_progress→project.mt_task_in_progress03_approved→project.mt_task_approved02_changes_requested→project.mt_task_changes_requested04_waiting_normal→project.mt_task_waiting
而当 stage_id 改变时,又会走 project.mt_task_stage。
这说明 Odoo 的项目任务不是简单记录“哪个字段变了”, 而是在主动把变化翻译成:
- 完成
- 取消
- 进行中
- 已批准
- 请求修改
- 等待中
- 阶段变化
这些词本质上就是协作语言。
所以 _track_subtype() 可以理解成:
任务状态变化通往协作通知的翻译器。
三、为什么 approved / changes requested 特别像“审批协作”,却又不是重型工作流
用户要求里特别提到“审批与消息协同”。
项目任务这里就是一个很好的例子。
从源码看,Odoo 确实给任务准备了:
approvedchanges requested
这样的状态语义, 并且为它们配了独立 subtype。
这意味着系统承认:
- 任务推进有时会经过一种轻审批 / 评审动作
- 这个动作不应该只体现在字段上
- 它还应该成为一条明确的消息语义
但另一方面,Odoo 又没有把它做成一套沉重的 BPM 引擎。
它更像是:
- 用状态表达流程节点
- 用 subtype 表达沟通语义
- 用 Chatter 把评审结果留痕并扩散给相关人
这正好体现了 Odoo 在协同办公上的风格:
- 不一定重流程
- 但一定要让协作信号可见、可传递、可回顾
四、为什么 waiting subtype 还要配合权限和依赖边界
project.task._mail_get_message_subtypes() 里还有一段很容易被忽略的逻辑:
mt_task_waiting这个 subtype 不总是可用- 如果项目不允许任务依赖,或者用户没有依赖相关权限,它会被剔除
这件事很有代表性。
因为“等待中”不是一个中性的装饰状态, 它常常在表达:
- 我被别的任务卡住了
- 我在等上游
- 我在等批准或输入
如果当前项目根本没打开依赖管理能力, 或者当前用户没有这方面语义, 那把 waiting 当成一个普适通知入口反而会制造误导。
所以 Odoo 没有强行把所有 subtype 暴露给所有人, 而是让通知语义和业务能力边界保持一致。
五、为什么项目层还要再定义一套 parent subtype
在 mail_message_subtype_data.xml 里,
除了任务自己的 subtype,
还有一套项目层 subtype:
mt_project_task_newmt_project_task_stagemt_project_task_in_progressmt_project_task_changes_requestedmt_project_task_approvedmt_project_task_done- 等等
这些记录通过:
parent_idrelation_field=project_id
与任务 subtype 关联。
这背后解决的是一个很现实的协作问题:
- 任务参与者要看任务细节
- 但项目参与者未必跟每条任务
- 他们仍然需要在项目层感知关键任务状态信号
于是 Odoo 选择的不是“把项目成员全关注到每条任务”, 而是:
- 在任务层定义细粒度语义
- 在项目层定义这些语义的投影版本
这样通知既能上浮,又不必把所有人都卷进任务细节。
六、为什么 relation_field=project_id 这么关键
很多人第一次看到 relation_field,会觉得只是技术连接字段。
但在这里它其实表达的是:
- 当任务上发生某类 subtype 事件时
- 去哪个父对象上投影这个消息语义
在项目任务场景里,答案就是 project_id。
也就是说, 任务状态通知之所以能向项目层传播, 不是因为系统“顺手群发一下”, 而是因为模型层明确声明了:
- 这类子对象事件与哪个父对象存在协作关系
这让通知传播变成一种有结构的设计,而不是临时广播。
七、这套设计为什么特别适合多角色协作
一个项目里,常见会同时存在几类人:
- 任务执行者
- 项目经理
- 评审人 / 批准人
- 只关心项目整体进度的干系人
如果系统只有任务状态字段, 这些人就会面临两个问题:
- 要么必须进任务里自己看
- 要么完全不知道发生了什么
而有了 subtype + parent subtype 传播后, 通知就能更贴近角色分层:
- 执行者看任务级变化
- 项目关注者看项目级投影
- 评审类状态又能作为清晰的协作语义被记录下来
这就是为什么 approved / changes requested 这类状态特别适合用 subtype 表达。
它们天然是多人协作语义,不只是单人字段状态。
八、这对审批与消息协同的启发
很多团队一说审批,就立刻想到:
- 单独审批模块
- 审批单
- 完整流程引擎
但 Odoo 任务这里提供了另一种更轻的路径:
- 先让任务状态表达评审节点
- 再让 subtype 把节点变化翻译成消息语义
- 最后通过任务层与项目层的传播,让相关角色都在正确层级收到信号
这很适合那些不需要独立审批单、 但又确实存在评审 / 打回 / 等待 / 通过等协作动作的场景。
换句话说:
不是所有审批都要做成独立引擎,有些审批更像协作状态,而 subtype 正好适合承载这层表达。
九、实战里最容易踩的坑
1. 只改 state,不关心 _track_subtype()
结果状态有了,协作语义却没有。
2. 把 approved / changes requested 当成本地 UI 标签
忽略它们其实应该成为通知事件。
3. 不理解 waiting subtype 与依赖能力绑定
会误以为系统“有时有、有时没有”是 bug。
4. 想让项目感知任务变化,就把项目成员全部订阅到任务
这通常比 parent subtype 投影更吵。
5. 设计了很多状态,但没有对应通知语义
最后团队还是只能进记录里自己猜发生了什么。
一句话记忆法
Odoo 项目任务里的 approved、changes requested、waiting 等状态,不只是字段值,而是会被
_track_subtype()翻译成协作事件,再通过 parent subtype 向项目层传播的通知语义。
DISCUSSION
评论区