先说结论
很多人看到 Odoo 的周期会议,会下意识理解成:
- 把一条会议复制很多份
- 以后分别改就行
这只是表面。
从 addons/calendar/models/calendar_event.py 看,Odoo 真正在维护的是:
- 一条
calendar.recurrence规则 - 一组跟随这条规则的事件
- 一批脱离规则的例外事件
- 与
partner_ids对应的attendee_ids - 会议提醒的重建
- 必要时共享的视频会议频道
一句话说:
Odoo 的周期会议不是“复制卡片”,而是“规则 + 实例 + 例外 + 通知同步”的协同模型。
为什么 calendar.event 里同时有 partner_ids 和 attendee_ids
这点很容易让人疑惑。
源码里既有:
partner_ids:参会人集合attendee_ids:参会状态对象
为什么不只保留一个?
因为“谁被邀请了”和“这个人是否接受 / 拒绝 / 待确认”不是同一层信息。
Odoo 的做法是:
partner_ids表达名单attendee_ids表达名单上每个人的响应状态
在 create / write 里,只要 partner_ids 改了,系统就会通过 _attendees_values() 自动生成或重建 attendee_ids。
这意味着:
参会人名单是静态集合,attendee 才是协同状态。
周期会议为什么不是简单复制
源码里,周期会议的核心字段有:
recurrencyrecurrence_idfollow_recurrencerecurrence_update
这些字段一起说明:
- 一条会议是否进入周期模式
- 它跟随哪条 recurrence 规则
- 当前这条是否仍然“跟着规则走”
- 修改时到底影响“这一次 / 之后的 / 全部”
如果只是简单复制,根本不需要这些语义层。
Odoo 明显想保留的是更高级的操作:
- 改这一场
- 改这一场及之后
- 改整个系列
这跟真正办公日历里的用户心智是一致的。
为什么会有 follow_recurrence
follow_recurrence 很值得注意。
它表示当前事件是不是还继续服从原来的周期规则。
一旦你对某一场会议做了足够“局部化”的修改,比如单独改时间,源码就可能把它从原规则里剥离,让它变成例外事件。
这就是为什么你会看到:
- 周期系列还在
- 但某一场已经不再完全跟着系列走
这不是 bug,而是官方明确支持的“例外事件”机制。
为什么修改周期会议时会出现 “This event / future events / all events”
这不是纯前端文案,而是后端模型真的按这三种语义处理。
在 write() 里,Odoo 会根据 recurrence_update 判断:
self_onlyfuture_eventsall_events
然后走不同链路:
改这一场
保留整个系列,只把当前事件局部剥离出来。
改这场及之后
通过 _update_future_events():
- 先把后续事件从旧 recurrence 裁开
- 当前事件更新为新起点
- 再生成一套新的 recurrence 规则
改整个系列
通过 _rewrite_recurrence():
- 归档旧系列
- 以 base event 为基础重写 recurrence
- 重新生成整套后续事件
所以这套行为绝不是“多条记录一起 update 一下”。
它是对时间序列规则的重写。
为什么参会人状态会被重置
源码里 _reset_attendees_status() 很说明问题。
当时间类字段变化时,Odoo 会把:
- 当前用户的 attendee 设为
accepted - 其他人的状态重置为
needsAction
这背后的协同逻辑很合理:
会议时间被改了,别人之前的接受,不应自动等同于“仍然接受新时间”。
这点如果不做,日历系统就会在最关键的地方失真。
所以 Odoo 不是只改会议日期,还会改参与者对这次邀请的有效状态。
为什么提醒也要重建
write() 里只要涉及:
partner_ids- 时间字段
alarm_ids
就会触发提醒相关重建。
这是因为会议提醒不是一个孤立功能,它依赖:
- 谁参加
- 什么时候开
- 周期规则是否变化
一旦这些条件改变,旧提醒的触发时间可能已经失效了。
所以官方实现会:
- 重新 setup alarms
- 对 recurrence 场景按系列维护下一次提醒
这说明在 Odoo 看来,日历不是“把会开出来”,而是“确保会前通知链条也成立”。
为什么周期会议的视频会议链接也不是随便复制
源码里还有一个很容易被忽略的点:
videocall_locationvideocall_channel_idaccess_token
尤其 _set_discuss_videocall_location() 注释写得很清楚:
- 周期事件的各次会议会有不同
access_token - 这是有意为之,避免 base event 删除后导致无法进入视频会议
同时,_create_videocall_channel() 又会在 recurrence 场景下,让同一系列的事件共享或继承视频频道。
这说明 Odoo 在做的是一种平衡:
- 链接 token 不完全复用,避免脆弱依赖
- 频道语义又尽量保持系列一致
这比“复制一个会议链接到所有记录”成熟得多。
为什么私密会议只显示 Busy
calendar.event 还继承了 mail.thread,所以它天然带有消息和协同属性。
但 Odoo 同时又做了隐私控制:
- 对非参与者的 private event,名称等敏感字段会被替换成
Busy
这说明 Calendar 在官方设计里,不只是排程工具,也是一个需要做权限切面的协同空间。
换句话说:
- 你可以知道别人忙不忙
- 但未必能知道别人忙什么
这正是企业日历很重要的边界。
做周期会议定制时最容易踩的坑
1. 把 recurrence 当成“批量复制模板”
结果一到“未来事件”修改就逻辑崩掉。
2. 只改 partner_ids,不理解 attendee_ids
结果参会状态、邮件通知和 RSVP 语义都不对。
3. 时间改了却不重置 attendee 状态
别人会被系统错误地视为已经接受新会议时间。
4. 忽略 alarm 重建
提醒时间会漂。
5. 视频会议链接简单复用
系列被拆分或删除后,容易留下脆弱引用。
一句话记忆法
Odoo 的周期会议不是“复制多条日程”,而是用 recurrence 规则去管理一串事件,并允许在中途拆出例外、重置参会状态、重建提醒和维护视频会议链路。
理解这一句,Calendar 的很多“为什么它这样改”都会变得很顺。
DISCUSSION
评论区