项目深度

Odoo 周期任务为什么不会“无限复制到失控”:下一次生成、子任务复制与停止规则讲透

很多人以为 Odoo 的周期任务只是“关掉一张,系统再复制一张”。但源码里真正决定会不会继续生成、复制哪些字段、截止日期怎么顺延、子任务会不会一起带过去的,是 project.task 与 project.task.recurrence 的一整套协作逻辑。本文把这条链路拆开讲清。

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

先说结论

Odoo 的周期任务并不是“到了时间自动再复制一份”那么粗糙。

/home/ubuntu/odoo-temp/addons/project/models/project_task.pyaddons/project/models/project_task_recurrence.py 来看,真正的逻辑是:

  • 任务先挂上 recurrence_id
  • 当前这张任务进入 closed state 时,_inverse_state 才会检查是否该生成下一张
  • 只有这一组周期里最新的一张 被关闭,才会触发下一次生成
  • 生成时不是无脑整条记录克隆,而是只保留部分字段、顺延部分字段、重置部分状态
  • 如果设置了 repeat_type = until,还要判断下一次截止日期是否仍落在结束日期之内

所以它更像一个“关闭驱动的下一次实例生成器”,而不是定时器。


第一层:触发点不是 cron,而是“最后一张任务被关闭”

很多实施顾问第一次看到周期任务,直觉会以为后台有个定时作业按天生成。

但这里最关键的触发点,其实在 project.task_inverse_state

  • 先根据 recurrence_id 找出每个周期组里 id:max 的任务
  • 再过滤出那些已经进入 CLOSED_STATES 的任务
  • 只有“当前周期组里最新那张,而且已经关闭”的任务,才会传给 project.task.recurrence._create_next_occurrences()

这意味着两个实现细节非常重要:

1. 不是任意一张历史周期任务关闭都会继续生成

如果一个周期组里已经有多张历史任务,你再去补关旧任务,不会继续往后生。

2. 不是靠日期到点自动长出来

真正的业务语义是:

这次工作真正完成了,系统才创建下一次要做的工作。

这很符合项目任务场景。因为很多重复性任务并不是“时间到了就出现”,而是“上一轮收口后,下一轮才有意义”。


第二层:Odoo 为什么一定要先找“这一组里最新的一张”

project.task.recurrence 里有一个 _get_last_task_id_per_recurrence_id(),它通过 _read_group 为每个 recurrence_idid:max

这个设计非常聪明,因为它解决了周期任务最容易失控的两个问题:

防止重复生成

如果用户多次编辑状态、多人并发操作,系统至少先把“谁才是当前尾部实例”这件事统一下来。

防止历史任务补操作把链条重启

历史任务只是历史记录。只有尾部节点有资格决定要不要长出下一张。

从建模上看,这其实是在把周期任务看成一条链,而不是一堆彼此平行的复制品。


第三层:下一张任务到底复制什么,不复制什么

_create_next_occurrences_values() 比很多人想象得细。

它不是直接 copy() 完事,而是先:

  • copy_data() 拿一份基础拷贝值
  • _get_recurring_fields_to_copy() 明确保留哪些周期字段
  • _get_recurring_fields_to_postpone() 明确哪些日期类字段需要整体顺延
  • 再手工补一些“新实例默认值”

源码里默认要复制的关键字段里,最重要的是:

  • recurrence_id

默认要顺延的字段里,最重要的是:

  • date_deadline

除此之外,Odoo 还会显式改掉几项内容:

  • priority 被重置成 '0'
  • stage_id 会尽量回到项目的第一个阶段,而不是沿用上一张关闭时的阶段
  • child_ids 会递归生成新的子任务

这几个点加起来,就说明 Odoo 的目标不是“复制一个历史快照”,而是“生成一个新的待办实例”。

也正因为这样,如果你把很多一次性业务字段、审批痕迹、聊天痕迹都指望自动延续到下一轮,通常会失望。周期任务的默认设计,更偏向保留结构,重置执行态


第四层:为什么子任务也会一起复制

很多团队第一次用 recurring task,会惊讶:

为什么主任务下一轮生成时,子任务结构也一起被带出来了?

原因直接写在 _create_next_occurrences_values()

  • child_ids 不是简单引用旧子任务
  • 而是对每个 task.child_ids 再递归调用 _create_next_occurrences_values()
  • 然后用 Command.create(vals) 生成一套全新的子任务树

这意味着 Odoo 把“任务模板结构”也视为周期的一部分。

适合的场景是:

  • 每周巡检固定就要做 3 个子步骤
  • 每月对账固定就要做 5 个检查点
  • 每季度复盘总是有一组稳定 checklist

但它也提醒你:如果上一轮子任务里塞了很多临时脏数据,下一轮并不会继承执行结果,只会继承结构轮廓。


第五层:until 为什么不是“只看今天有没有到结束日”

should_create_occurrence() 这一段很容易被误读。

如果 repeat_type != 'until',当然可以一直生成。

但如果是 until,逻辑不是简单判断“今天是否小于结束日”,而是更细:

  • 当前任务必须有 date_deadline
  • 系统先拿当前任务截止日期加上 recurrence delta
  • 再判断“下一次截止日期 是否 <= repeat_until

也就是说,决定能不能继续生成的,不是当前实例本身,而是下一次实例的截止日期是否还合法。

这就解释了很多现场疑问:

  • 明明当前任务还在结束日期前,为什么关掉后没有下一张?
  • 因为下一张的 date_deadline 推过去以后,已经越界了。

这是一种很合理的边界控制。因为系统关心的是“下一轮是否还在计划范围内”,而不是“这一轮是否曾经在范围内”。


第六层:为什么有的周期任务看起来配置正常,却不再生成

实战里最常见的排错顺序,我建议按下面来:

1. 先看是不是最新一张

旧任务关闭,不会触发下一张。先确认你操作的是这一组里最后一张。

2. 再看状态是不是真的进了 closed set

不是所有“看起来完成”的 UI 状态都等于 closed semantics。要确认任务真正进入 CLOSED_STATES

3. 看 repeat_untildate_deadline

如果当前任务没有截止日期,或者推算后的下一次截止日期超出 repeat_until,就不会生成。

4. 看项目阶段是否让你误以为“复制失败”

新任务生成后,stage_id 往往会落到项目初始阶段,而不是你刚关掉的 Done 列。很多人其实不是没生成,而是去错列找了。

5. 看是不是有人把周期关掉了

_is_recurrence_valid() 会校验 repeat_interval > 0,并检查 until 日期是否仍然有效。配置非法时,这条链本来就不会健康运行。


第七层:实施上最该注意的设计边界

不要把 recurring task 当完整模板引擎

它适合重复任务,不适合复杂流程模板编排。特别是需要审批流、负责人矩阵、动态依赖时,最好另做更明确的建模。

要给周期任务设清晰的截止日期策略

因为 until 的停止判断直接依赖 date_deadline 顺延,没有 deadline 的周期任务,很容易让用户对停止边界产生误解。

子任务结构要稳定,不要混入大量一次性字段

否则每一轮都会复制出一套“看起来像模板,实际上夹着历史包袱”的结构。


最后一句

Odoo 周期任务最值得理解的,不是“它会复制”,而是:

  • 什么时候复制:关闭最新实例时
  • 复制谁:当前周期组尾部实例
  • 复制什么:结构和必要字段
  • 何时停止:下一次截止日期越过 repeat_until

把它理解成“关闭驱动的下一轮任务生成机制”,你就会发现它既不神秘,也不失控。

DISCUSSION

评论区

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