项目更新

Odoo 项目更新为什么不是“自动日报”:project.update、状态快照与项目汇报边界讲透

很多人以为 Odoo 项目里的 Update 就是一个会自动刷新的状态面板,但源码真正做的是“带上下文的人工快照”。本文从 project.update、last_update_status、默认描述模板和 milestone 变化跟踪讲清它的设计边界。

Odoo 开发 项目
进阶 开发者 3 分钟阅读
0 评论 0 点赞 0 收藏 38 阅读

先说结论

很多人第一次看到 Odoo 项目里的 Update / Status Update,直觉会以为它是:

  • 任务变化后自动生成的一条日报
  • 或者项目主页上的一个实时状态字段
  • 再或者只是管理层写备注的富文本框

但源码真正表达的意思其实更清楚:

project.update 不是实时看板,也不是纯备注,而是一张“带上下文的项目快照”。

这个快照里既有人工填写的判断,也有系统在创建那一刻帮你抓取的上下文,比如:

  • 当前项目状态 status
  • 当前进度 progress
  • 当时的任务总数 / 已关闭任务数
  • 默认生成的描述模板
  • 里程碑变化摘要
  • 在有权限时可见的盈利摘要

所以它更像一次阶段性汇报,而不是持续自刷新的监控面板。


先看源码位置:这是一套独立机制

这条链路主要在这些文件里:

  • addons/project/models/project_update.py
  • addons/project/models/project_project.py
  • addons/project/views/project_update_views.xml
  • addons/project/views/project_update_templates.xml

从模型定义就能看出来,Odoo 没把“项目更新”塞进 project.project 的几行字段里,而是单独建了一个模型:

class ProjectUpdate(models.Model):
_name = 'project.update'
_description = 'Project Update'
_inherit = ['mail.thread.cc', 'mail.activity.mixin']

这有两个很重要的信号:

  1. 它是独立记录,不是项目表上的一段历史文本。
  2. 它带协同属性。 因为继承了 mail.thread.ccmail.activity.mixin,所以 update 本身是可以参与沟通、跟进、安排活动的。

这说明官方不是把它设计成“系统自动算一个状态”,而是把它当成项目沟通对象


last_update_status 为什么很容易让人误会

project.project 上,你会看到这些字段:

  • update_ids
  • last_update_id
  • last_update_status
  • last_update_color

很多人看到 last_update_status,就会以为:

这就是项目当前状态,而且系统会按任务变化自动重算。

但源码不是这样。

project_project.py 里有这一段:

@api.depends('last_update_id.status')
def _compute_last_update_status(self):
for project in self:
    project.last_update_status = project.last_update_id.status or 'to_define'

意思非常直接:

项目上的“当前状态”,本质上只是“最后一条 update 的状态”。

也就是说:

  • 它不是从任务完成率自动推出来的
  • 不是从盈利面板自动算出来的
  • 不是从 milestone 自动推出来的
  • 而是最后一次项目更新记录里,作者给出的状态判断

这就是第一个关键边界:

Odoo 把项目状态视为“管理判断的结果”,不是“底层数据自动算出来的唯一真相”。

这是一个很成熟的产品选择。

因为真实项目里,任务完成 80%,不代表项目一定 on track; 里程碑延期了,也不一定必须 off track; 盈利暂时难看,也可能只是结算还没到。

系统可以提供证据,但最终状态是汇报判断,不是机械结论


为什么改项目状态时,系统会顺手创建一条 project.update

还有一个特别值得讲的细节。

project.project.write() 里,Odoo 对 last_update_status 做了特殊处理:

if 'last_update_status' in vals and vals['last_update_status'] != 'to_define':
for project in self:
    self.env['project.update'].with_context(default_project_id=project.id).create({
        'name': _('Status Update - %(date)s', date=fields.Date.today().strftime(get_lang(self.env).date_format)),
        'status': vals.get('last_update_status'),
    })
vals.pop('last_update_status')

这段逻辑很有味道。

它不是简单把 project.last_update_status 写掉,而是:

  1. 发现你想改项目状态
  2. 先创建一条新的 project.update
  3. 再让项目去指向这条 update

这意味着:

在 Odoo 的设计里,“项目状态改变”本身就应该留下一个更新记录。

也就是:

  • 状态不是裸字段改动
  • 状态变化应该被历史化
  • 状态变化应该成为可追溯的汇报事件

这跟很多系统只在项目表上放一个红黄绿字段很不一样。

Odoo 更偏向这样的理念:

项目状态不只是“现在是什么颜色”,还要能回答“它是什么时候、由谁、基于什么上下文变成这个颜色的”。


新建 update 时,为什么默认会帮你填一部分内容

project.update.default_get() 是这套体验顺滑的关键。

源码里它会在创建 update 时,尽量从项目上带一些已有上下文:

  • project_id:来自当前上下文 active_id
  • progress:默认取 project.last_update_id.progress
  • description:默认用 _build_description(project) 生成
  • status:默认继承项目当前最后状态;若还是 to_define,则回落为 on_track

这说明 Odoo 不是要求用户每次从零写一篇周报,而是:

给你一张预填好的更新草稿,你再在上面补判断与说明。

这里尤其值得注意两个点。

1)progress 默认继承上一条 update,而不是系统自动重算

这再次说明:

  • progress 更像“汇报口径”
  • 不是任务关闭率的强绑定映射

任务关闭率只是辅助背景; 你填 60%、70%、85%,是项目经理对整体推进程度的表达。

2)如果项目还没定过状态,默认不是 to_define,而是 on_track

源码注释写得很直白:

  • to_define 是项目初始状态占位
  • project.update.status 里没有这个选项
  • 所以第一次填更新时,默认会落到 on_track

产品层面很好理解:

“还没设置项目状态” 可以存在;但 “写 update 的时候还不给状态” 就不太成立了。


默认描述不是空白富文本,而是 QWeb 拼出来的“汇报模板”

很多人会忽略 description 的来源,以为只是一个 HTML 字段。

实际上 project.update._build_description(project) 会调用:

self.env['ir.qweb']._render(
'project.project_update_default_description',
self._get_template_values(project)
)

也就是说,默认描述来自 QWeb 模板,不是简单字符串拼接。

模板文件在:

  • addons/project/views/project_update_templates.xml

里面会动态渲染几块内容:

  • Summary
  • Activities / Milestones
  • Profitability

这就很关键了。

Odoo 不是让 update 只写“今天挺顺利”,而是把它引导成一份有结构的项目汇报。

这份模板默认想让你回答什么

从模板结构看,官方默认关心的是三类东西:

  1. 整体情况:How’s this project going?
  2. 里程碑进展:哪些 milestone 已到期、可标记完成、最近有无变化
  3. 经营情况:如果项目具备盈利分析条件,就带出收入 / 成本摘要

换句话说,这条 update 其实在帮项目经理回答:

  • 项目总体还稳不稳
  • 关键交付有没有偏移
  • 业务结果是否在朝预期推进

这比单纯一段随手备注有结构得多。


为什么说它是“快照”,不是“实时仪表盘”

这个判断最核心的证据,在 create() 里。

project.update.create() 在创建完成后,会把当下项目任务信息写入 update:

update.write({
'task_count': project.task_count,
'closed_task_count': project.task_count - project.open_task_count,
})

注意这里的语义:

  • 不是做关联字段
  • 不是实时 compute
  • 而是创建那一刻写死一份数值

这意味着:

一条 update 的任务统计,记录的是“写这条 update 时”的任务状态,不会跟着未来任务变化一起漂移。

这就是典型快照设计。

它的好处非常大:

  • 你下周回头看上周 update,不会被今天的数据覆盖
  • 你可以比较不同阶段的项目状态
  • 你能保留“当时的判断依据”

如果它做成实时字段,历史 update 就会失去意义,因为过去每一条都会被现在改写。

所以 Odoo 这里很明确:

update 的价值不在“实时”,而在“保留当时”。


里程碑部分为什么特别有意思:它不只看当前值,还看“自上次更新以来发生了什么”

这是这条链路最容易被低估、也最值得写文章的一点。

project.update._get_milestone_values(project) 不只是列出 milestone,它还区分了三层信息:

  • list:当前项目里相关 milestone 列表
  • updated:自上次 project update 以来,deadline 被改过的 milestone
  • created:自上次 project update 以来,新建的 milestone

这说明 Odoo 关心的不只是“现在有哪些 milestone”,还关心:

相对于上一次正式汇报,这次有哪些计划边界发生了变化。

这就是管理语义,不是简单数据展示。

它怎么知道 milestone 截止日期被改过

更妙的是 _get_last_updated_milestone()

这段代码没有直接看当前字段值,而是查:

  • mail_message
  • mail_tracking_value
  • ir_model_fields

并且只抓 project.milestone.deadline 的字段追踪记录。

也就是说,它不是问:

  • milestone 现在 deadline 是多少

而是在问:

  • 自上次 update 之后,谁的 deadline 被改过,从什么日期改成了什么日期

这是非常“汇报型”的设计。

因为管理层最关心的往往不是静态表,而是变化:

  • 哪个里程碑延期了
  • 哪个里程碑被重新承诺了
  • 哪个里程碑是刚新增进来的

所以这块设计的本质不是“列数据”,而是“列变化”。


为什么模板里会塞 profitability,但它又不等于“项目更新就是盈利报表”

_get_template_values(project) 里会调用:

profitability_values, show_profitability = project._get_profitability_values()

然后把结果喂给描述模板。

这让很多人误会:

project update 是不是本质上就是 profitability 的外壳?

不是。

更准确地说:

盈利摘要是 project update 的上下文材料之一,不是它的全部定义。

这篇和已有“项目盈利”文章最大的区别也在这里:

  • “项目盈利”那篇讲的是收入 / 成本 / billable 的经营逻辑
  • 这篇讲的是项目更新怎样把盈利信息包装进一次阶段性汇报

也就是说,盈利在这里扮演的是:

  • 证明项目经营情况的一个维度
  • 而不是替代项目状态判断

更重要的是,project_project.py_get_profitability_values() 一开始就做了权限判断:

if not self.env.user.has_group('project.group_project_manager'):
return {}, False

这说明:

不是所有看 update 的人都能看到盈利摘要。

这也再次证明 update 不是简单导出一张固定报表,而是一个会随角色、权限、模块能力而变化的汇报容器。


project.update 里真正被“冻结”的,和真正会“继承”的,分别是什么

如果你想读懂这条机制,最有用的方法不是记字段,而是把字段分成两类。

一类:创建时冻结的快照

例如:

  • task_count
  • closed_task_count
  • 当次生成的 description
  • 该次选择的 status
  • 该次填写的 progress
  • 当时的 date

这类信息的核心目的是:

保存“那次汇报”的样子。

一类:下一次创建时会继承的上下文

例如:

  • 上一条 update 的 progress
  • 上一条 update 的 status
  • 基于最新项目情况重新生成的默认描述

这类信息的核心目的是:

降低下一次写更新的成本。

所以这套机制不是“全冻结”,也不是“全实时”,而是:

  • 历史记录要稳定
  • 新一轮编辑要顺手

这是非常成熟的“汇报对象”设计。


删除最后一条 update 会怎样:项目状态会回退

project.update.unlink() 也很值得一提:

for project in projects:
project.last_update_id = self.search([('project_id', '=', project.id)], order='date desc', limit=1)

这意味着如果你删掉某项目最近一条 update:

  • 项目的 last_update_id 会重新指向上一条
  • last_update_status 也会跟着回退到上一条状态

这再次说明:

项目当前状态没有单独维护一套“真值系统”,它就是对最后一条 update 的引用。

这也提醒开发者:

  • 如果你在二开里把 update 当普通日志随便删
  • 你可能会不小心把项目当前状态一起改掉

所以对 project.update 的删除、批量清理、迁移,都不能只当历史备注处理。


常见误解,逐个拆开

误解 1:项目更新会随着任务变化自动刷新

不会。

新建 update 时抓一份任务统计快照,之后历史 update 不会自动同步成最新任务数据。

误解 2:项目状态就是任务完成率

不是。

任务完成率是证据之一;最终 status 仍是人工汇报判断。

误解 3:有 profitability 就等于有 update

也不是。

盈利面板是一个上下文来源,但 update 是独立的沟通对象。

误解 4:里程碑列表只是当前清单

不止。

它还会强调“自上次 update 以来发生的变化”,尤其是 deadline 的改动与新建 milestone。

误解 5:改项目状态只是改一个字段

源码上并不是。

last_update_status 时,Odoo 倾向于先生成一条新的 project.update,把这次状态变化历史化。


对实施和二开来说,这条机制最值得抓住的设计思想是什么

我觉得有三条。

1)把“状态”视为管理判断,不是机械计算

底层数据可以支持判断,但不要假设系统能替管理者做最后裁决。

2)把“汇报”设计成快照,而不是实时投影

历史汇报的价值,在于保留当时的上下文。 如果全做成实时字段,历史就会失真。

3)重点不是列出全部对象,而是提炼“相对上次发生了什么变化”

里程碑变更追踪就是最好的例子。 管理真正关心的是变化,不是静态存量。


如果你要二开,最容易踩的坑有哪些

坑一:把 update 当普通 note,忽略它和项目当前状态的绑定

一旦删除、覆盖或乱同步 update,last_update_status 可能会跟着失真。

坑二:把 progress 自动绑成任务关闭率

表面省事,实际会把“管理判断”错误地降格成“机械百分比”。

坑三:重写默认描述时,只留一个大文本框

这样会把 Odoo 原来那种“结构化项目汇报”能力削弱掉。 更好的做法通常是:

  • 保留 summary
  • 保留 milestone 变化摘要
  • 按角色决定是否展示盈利摘要
  • 再补你自己的业务模块区块

坑四:忽略 mail.tracking,导致无法识别“自上次 update 以来的变化”

如果你做了自定义 milestone 字段、计划日期字段、阶段承诺字段,也可以借鉴 Odoo 这套思路:

  • 不是只展示当前值
  • 而是展示“上次汇报之后发生的变化”

这会让你的项目汇报质量立刻上一个层级。


最后再用一句话收住

Odoo 的 project.update 最值得学的地方,不是它做了一个好看的更新页面,而是它把项目汇报拆成了三层:

  • 人工判断的状态
  • 创建当下的业务快照
  • 相对上次汇报的关键变化

所以它不是自动日报,也不是实时仪表盘。

它更像是:

一张可追溯、可协作、带业务上下文的项目阶段快照。

如果你把这层想通了,就会明白为什么 Odoo 没把项目状态做成一个自动计算出来的“唯一真值”,而是选择了更贴近真实管理场景的设计。

DISCUSSION

评论区

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