先说结论
Odoo 企业版 VoIP 的重点,不是“电话记录有没有存下来”,而是:
一通电话结束之后,系统能不能把它转成一个明确、可继续处理的协作动作。
所以源码里你会看到两条并行对象:
voip.call:描述电话事件本身;mail.activity:描述这通电话之后,谁还需要继续做什么。
这就是为什么在实际使用里,VoIP 看起来不像单纯通话工具,而像“电话 + 待办 + 回拨入口”的组合。
这篇主要看哪里
核心源码在:
enterprise/voip/models/voip_call.pyenterprise/voip/models/voip_queue_mixin.pyenterprise/voip/models/mail_activity.py
重点方法:
voip.call.start_call()voip.call.end_call()voip.call.miss_call()voip.queue.mixin.create_call_activity()voip.queue.mixin.delete_call_activity()mail.activity.get_today_call_activities()
第一层:voip.call 负责记录电话状态机,但它不负责“后续动作”
voip.call 这几个方法非常直接:
start_call():写入start_date,状态变ongoing;end_call():写入end_date,状态变terminated,必要时记录activity_name;miss_call():状态变missed;reject_call()/abort_call():处理其他结束语义。
它的职责很清楚:
- 描述电话何时开始、结束;
- 这通电话结果如何;
- 通过 Store 把状态推给前端。
但它并不直接回答另一个更重要的问题:
- 这通电话结束后,接下来谁去跟进?
这个问题交给 call activity 机制。
第二层:create_call_activity() 不是装饰功能,而是把电话接入待办系统
voip_queue_mixin.create_call_activity() 的设计非常有代表性。
它做的事情有三步:
1. 先保证系统里存在“phonecall”类型的 activity
如果 xmlid 不存在,就尝试找同类;还没有,就自动创建一个 mail.activity.type。
这说明 Odoo 假定:
VoIP 跟进不是私有模型行为,而应该复用全局活动体系。
2. 再为当前记录批量创建 mail.activity
活动字段里会带上:
activity_type_iddate_deadlineres_idres_model_iduser_id
于是“去回拨这个客户”就从一个模糊想法,变成了标准化待办。
3. 如果记录没有手机号,直接报错拒绝入队
这个细节非常关键。
源码不会因为你点了“加入拨号队列”就无脑建活动,而是检查 activity.phone。如果缺手机号,直接抛 UserError。
这背后的产品逻辑很合理:
- 没号就没法回拨;
- 这种 activity 建出来只会变成假待办;
- 不如在入队时就阻止。
第三层:为什么说它是 call queue,而不是普通活动列表
因为 VoIP 不是只想让你“看见一个待办”,而是想让这个待办能回到电话流程里。
mail_activity.get_today_call_activities() 与相关格式化逻辑,会把今天的电话活动聚出来,再喂给前端。
这意味着这些活动不是后台静态记录,而是:
- 能作为拨号入口;
- 能按今日范围聚合;
- 能和 VoIP 前端直接联动。
所以 call activity 在 Odoo 里并不是“顺手创建一条备注”,而是电话工作台的任务队列。
第四层:删除 call activity 为什么要发 bus 事件
delete_call_activity() 不只是 unlink()。
它会先找到:
- 当前用户;
- 当前模型;
- 电话类 activity;
- 截止日期不晚于今天;
- 且有 phone 的活动。
然后在删除前,对每条活动执行:
self.env.user._bus_send("delete_call_activity", {"id": activity.id})
这说明一件事:
call queue 的删除不是纯数据库操作,而是带前端同步语义。
也就是说,队列里的待办被移除时,前端拨号工作台也应该立即知道,不然列表会脏。
这和普通 mail.activity 的后台管理视角完全不同。
第五层:未接来电为什么适合落到 activity,而不是只留一条 call 记录
因为 voip.call 只能告诉你“刚才发生了什么”,却不能驱动“现在该做什么”。
未接来电最典型的后续动作是:
- 尽快回拨;
- 指定某个负责人跟进;
- 在今天的工作列表里显式可见。
这恰好是 mail.activity 最擅长表达的东西。
所以从协同设计上看:
voip.call.miss_call()保留事实;create_call_activity()生成下一步动作;get_today_call_activities()把动作带回前端队列。
这三者合起来,才构成“未接来电处理”。
第六层:这套设计解决了什么现实问题
1. 电话不再是瞬时事件
只记 call log,团队容易漏跟进。
2. 跟进动作可以落入统一协作体系
因为它最终变成 mail.activity,而不是 VoIP 私有待办。
3. 前端拨号台和后台活动表可以共用同一份事实
不是两套平行系统。
4. 数据质量问题在入口就被拦住
没电话号码就不允许加入 call queue,避免垃圾任务。
实战里最容易踩的坑
坑 1:只改 voip.call 状态,不生成 activity
这样你有通话记录,但没有可执行的回拨队列。
坑 2:把 call activity 当普通备注
它实际上是前端拨号工作台的一部分,删除、完成、今天范围筛选都可能影响 UI。
坑 3:忽略手机号校验
如果你手工补 activity,却不保证 phone 可用,前端回拨体验很容易炸。
调试顺序建议
遇到“未接来电为什么没出现在回拨列表”时,建议按这个顺序查:
voip.call是否已进入missed;- 是否有调用
create_call_activity(); - 相关记录是否真的有 phone;
mail.activity是否为phonecall类别;get_today_call_activities()的时间与用户范围是否命中;- 前端是否收到 bus 更新。
结论
Odoo 企业版 VoIP 的真正价值,不在于它把电话打进系统,而在于它把电话后的动作也纳入系统:
voip.call记录事实;mail.activity记录下一步;- call queue 把这些待办组织成可回拨工作台;
- bus 事件保证前后端列表同步。
一句话总结:
Odoo 把未接来电做成待办,不是附加功能,而是把“电话”真正接进协同流程的关键一步。
DISCUSSION
评论区