先说结论
Odoo 处理循环会议时,核心思想不是“已有 N 条会议记录,改一下字段然后批量写回去”,而是:
把 recurrence 当成一套生成规则,把当前事件当成分割点,把 future-only 修改当成“从这里开始生成一条新规则”。
这就是为什么你在界面里看到“只修改当前及未来事件”,最终效果常常像:
- 旧系列保留到当前节点之前;
- 当前节点以后挂到一条新的 recurrence;
- 未来实例按新规则重新生成;
- 参会人不是直接改
partner_ids完事,而是转成calendar.attendee的增删命令。
如果把它误解成“多条普通事件的批量更新”,很多现场现象都解释不通。
这篇主要看哪里
核心源码在:
addons/calendar/models/calendar_event.pyaddons/calendar/models/calendar_recurrence.py
重点方法:
calendar.event._attendees_values()calendar.event._apply_recurrence_values()calendar.event._get_recurrence_params()calendar.recurrence._split_from()calendar.recurrence._apply_recurrence()
这些方法组合起来,定义了“会议规则如何拆”“未来实例如何重建”“参会人如何同步”。
第一层:参会人不是 partner_ids 的镜像字段,而是独立 attendee 记录
很多开发者第一次看 Calendar 会误以为:
- 改了
partner_ids,参会人就只是 Many2many 跟着变一下。
但 _attendees_values() 说明事情没这么简单。
它做的是把 partner 变更命令翻译成 attendee 命令:
- 删除 partner → 查对应
calendar.attendee再删; - 替换整组 partner → 算出 removed 和 added;
- 新增 partner → 为每个新增人生成新的 attendee 创建命令。
也就是说:
Calendar 真正承载 RSVP、出席状态、邀请反馈的,是 attendee,而不是 partner_ids 本身。
所以“参会人列表”只是表层;底下真正有状态的是 attendee 记录。
第二层:future-only 修改的本质,是从当前节点切一条新 recurrence
_apply_recurrence_values() 的注释已经把核心意图写得很清楚:
- 如果事件还没有 recurrence,就新建一条;
- 如果已经有 recurrence 且只改未来,就调用
recurrence_id._split_from(event, values); - 然后统一
_apply_recurrence(),把缺失事件生成出来。
这段逻辑的关键是:
它不是原地改整条旧规则
而是把“当前事件”视为分割点。
这意味着:
- 历史实例不需要跟着新规则重写;
- 当前及未来可以切到一套新 recurrence;
- 旧 recurrence 仍然能保存此前的链路。
这正是业务上最合理的做法。
比如:
- 每周一例会原本在 10:00;
- 从下周开始改到 15:00;
- 过去开过的会议,显然不该被强行改成 15:00。
Odoo 就是按这个思路落地的。
第三层:为什么系统需要 _get_recurrence_params()
_get_recurrence_params() 看起来像个小工具,但它其实决定了 recurrence 的“锚点语义”。
它会从当前事件日期里推导:
- 星期几;
- 第几个星期几;
- 当月第几天。
这意味着 recurrence 不是只保留一个 RRULE 字符串就完事,而是要把“当前事件在时间轴上的位置”转换成结构化参数。
这在两类场景里尤其重要:
场景 1:按周重复
例如每周二。
场景 2:按月重复
例如“每月第三个周四”或“每月 18 号”。
如果没有这层参数抽取,你对基准事件做 future split 时,就无法稳定地从当前节点继续生成后续实例。
第四层:为什么你会觉得“只改一条,系统却动了很多条”
因为 Calendar 看的不是“单条 event 记录”,而是:
- 当前 event;
- 它所属的 recurrence;
- 以当前节点为边界的 future segment;
- 由 recurrence 重新推导出来的所有 future events。
所以只要你改的是影响规则的字段,比如:
- 重复频率;
- 间隔;
- 星期几;
- 时间段;
- 终止条件;
系统就不是在改单条记录,而是在改一段生成逻辑。
这就是“界面操作像改单条,结果却影响未来一串实例”的本质原因。
第五层:attendee 同步和 recurrence 拆分为什么要一起理解
很多问题都出在这里。
如果你只理解 recurrence,不理解 attendee,就会以为“未来实例重建后,参会人自然都对了”。
但真实情况是:
- recurrence 决定会生成哪些 event;
- attendee 命令决定每个 event 上有哪些参与者以及他们的状态承载体。
所以当你对 partner 做 set、link、unlink 时,系统不是偷懒直接覆写 partner_ids,而是先算出 attendee 的增删。
这让 Odoo 能保留正确的邀请语义:
- 谁被加进会议;
- 谁被移出;
- 哪些 attendee 应该删掉;
- 新 attendee 应该重新创建并走自己的 RSVP 生命周期。
第六层:这套设计解决了什么业务问题
它解决的不是“代码优雅”这么简单,而是现实里的协同办公约束:
1. 历史会议不应被未来规则污染
已经开完的例会,不该因为你今天改规则而回写成新时间。
2. 未来实例必须从正确边界继续生成
不是从第一条会议重新算,而是从当前拆分点往后算。
3. 参与人状态需要有独立生命周期
受邀、接受、拒绝、待确认,这些不是 partner_ids 能表达的,需要 attendee。
4. 邮件邀请与会议状态要可追踪
如果 attendee 只是列表影子,就很难稳妥支撑 RSVP、提醒和参与状态。
实战里最容易踩的坑
坑 1:直接批量改 recurrence 相关字段
如果你二开时直接对一串 event write(),却没有走相应 recurrence 语义,很容易造成:
- 历史和未来混在一起;
- UI 上看着像一组循环会议,底层却已经断链;
- 未来实例和规则不一致。
坑 2:只改 partner_ids,不理解 attendee 的存在
你会在后续邀请、参会状态、邮件提醒里遇到“为什么跟我想的不一样”。
因为真正的参会状态对象是 calendar.attendee。
坑 3:把“当前及未来”理解成 SQL 条件过滤
它不是单纯 start >= 当前日期 的批量更新,而是 recurrence split。
这两者完全不是一个层级。
调试这类问题时该怎么看
排查顺序建议是:
- 先看当前 event 有没有
recurrence_id; - 看这次修改是否走了
_apply_recurrence_values(); - 看是否触发
_split_from()生成新 recurrence; - 再看未来实例是不是由
_apply_recurrence()重建; - 最后检查 partner 变更是否正确生成 attendee 命令。
如果你一上来只看最终生成的 event 列表,通常很难倒推出问题发生在哪一步。
结论
Odoo Calendar 的循环会议机制,本质上是:
- 用 recurrence 保存规则;
- 用 event 表达实例;
- 用 attendee 表达参与人状态;
- 用 split 机制把“当前及未来”修改从历史里切出来。
所以一句话总结:
Odoo 改循环会议,不是在“修改一堆已有记录”,而是在“重新定义从当前节点开始的时间规则,并把参与人状态同步到新实例上”。
DISCUSSION
评论区