Odoo 的 Mail 就是那个 Discuss 应用,但同时也是一个基础模块。所谓基础模块,就是几乎所有的 Odoo 应用都依赖了 Mail 模块。
一般来说 打开 Odoo 默认应用就是 Discuss,在 Discuss 中可以收发邮件,建立 Channel,收发相关 Channel 的消息。所谓 Channel 就是一个对话过程,有一对一的,也有多人的(就是聊天群)。通过建立 Channel 可以和 Odoo 的用户进行实时消息通讯。
以前我曾经写过一个 Odoo 的登录用户是如何监听自己所关心的 Channel 上的消息的。
?
Discuss 主界面
Discuss 应用的界面非常特殊,它既不是 Odoo 支持的 View 的任何一种,Odoo 支持很多内置的 View,如 Tree、Form、Calendar、Graph、Pivot等等,也可以自己造一个新的 View。
除了创建定制 View,Odoo 还支持一种创建定制化前端界面的方法,就是 Client Action,客户端 Action,(这个 Action 翻译成啥呢,感觉怎么翻译都不太对劲)Action 就是通过用户交互操作导致 Odoo 产生的反应,比如用户点击了一个菜单,Odoo 要执行菜单对应的 Action,点击一个 Button,Odoo 要执行一个这个 Button 对应的 Action,点击一个上下文菜单(在显示表单信息时候,在控制栏中间显示的菜单),Odoo 要执行一个对应的 Action。所有 Action 必须提前在数据库中提前描述,就是 ir.actions.act_windows,ir.actions.client 和 ir.actions.server,这三个 actions 的表的记录都是描述 Action 的,windows 表中的记录是指那些使用了 Odoo View 的 Action,client 表中的记录是指在客户端(Javascript)中创建的 Action,server 表中的记录是指在服务器端执行的 Action (Python),Server Action 可以返回一个 windows 或者 client Action。关于 Action 可以单独开一个主题去讲。
Discuss 是一个 Client Action,需要在数据中创建相应的 Action 记录,需要在前端(Javascript)实现。
<record id="action_discuss" model="ir.actions.client"><field name="name">Discuss</field><field name="tag">mail.discuss</field><field name="res_model">mail.channel</field><field name="params" eval=""{
'default_active_id': 'mailbox_inbox'}""/></record>
这个 XML 数据在 Module 安装时转化成相应的数据库记录。
var Discuss = AbstractAction.extend({
contentTemplate: 'mail.discuss',custom_events: _.extend({
}, AbstractAction.prototype.custom_events, {
discard_extended_composer: '_onDiscardExtendedComposer',message_moderation: '_onMessageModeration',search: '_onSearch',update_moderation_buttons: '_onUpdateModerationButtons',}),events: {
'click .o_mail_sidebar_title .o_add': '_onAddThread','blur .o_mail_add_thread input': '_onAddThreadBlur','click .o_mail_channel_settings': '_onChannelSettingsClicked','click .o_mail_discuss_item': '_onDiscussItemClicked','keydown': '_onKeydown','click .o_mail_open_channels': '_onPublicChannelsClick','click .o_mail_partner_unpin': '_onUnpinChannel','input .o_discuss_sidebar_quick_search input': '_onInputSidebarQuickSearchInput',},hasControlPanel: true,loadControlPanel: true,withSearchBar: true,searchMenuTypes: ['filter', 'favorite'],/*** @override* @param {
Object} [options]* @param {
integer} [options.channelQuickSearchThreshold=20] amount of* channels (dm inluded) for which a quick search appears in the sidebar.*/init: function (parent, action, options) {
this._super.apply(this, arguments);this.action = action;this.context = action.context;this.action_manager = parent;this.domain = [];this.options = options || {
};if (!('channelQuickSearchThreshold' in this.options)) {
this.options.channelQuickSearchThreshold = 20;}this._isMessagingReady = this.call('mail_service', 'isReady');this._isStarted = false;this._threadsScrolltop = {
};this._composerStates = {
};this._defaultThreadID = this.options.active_id ||this.action.context.active_id ||this.action.params.default_active_id ||'mailbox_inbox';this._selectedMessage = null;this._throttledUpdateThreads = _.throttle(this._updateThreads.bind(this), 100, {
leading: false });this.controlPanelParams.modelName = 'mail.message';this.call('mail_service', 'getMailBus').on('messaging_ready', this, this._onMessagingReady);},/*** @override*/start: function () {
var self = this;this._isStarted = true;return this._super.apply(this, arguments).then(function () {
if (!self._isMessagingReady) {
return;}return self._initRender();});},/*** @override*/do_show: function () {
this._super.apply(this, arguments);this._updateControlPanel();this.action_manager.do_push_state({
action: this.action.id,active_id: this._thread.getID(),});},/*** @override*/destroy: function () {
if (this.$buttons) {
this.$buttons.off().remove();}this._super.apply(this, arguments);},/*** @override*/on_attach_callback: function () {
this.call('mail_service', 'getMailBus').trigger('discuss_open', true);if (this._thread) {
this._threadWidget.scrollToPosition(this._threadsScrolltop[this._thread.getID()]);this._loadEnoughMessages();}},/*** @override*/on_detach_callback: function () {
this.call('mail_service', 'getMailBus').trigger('discuss_open', false);this._threadsScrolltop[this._thread.getID()] = this._threadWidget.getScrolltop();},
core.action_registry.add('mail.discuss', Discuss);
贴出部分代码,在 mail/start/src/js/discuss.js 中,确实是在前端制造了Client Action数据库记录对应的 Action。
关键要加入到前端这个注册表中,不然 Odoo 没法看到。
对于任何一个 Odoo Module (或者叫做应用)来说,加载界面的抽象过程都是一致的,前端都会向后端请求 load 和 load views,load 能够让后端有机会告诉前端,应该执行什么 Action,在这里就是执行 Client Action,前端在知道了这个 Action 之后通过向后端发送 load views,从而知道自己应该加载什么 View,在这里只会返回 Search View 的相关信息。
Channel 列表
Discuss Action 初始化的时候会通过 Mail Manager (mail service,这又是一个前端概念,类似 AngularJS 的 service,一些通用的功能封装和访问后台数据功能,这些 service 很容易重用和测试)去初始化 message,init_messaging,一次性获取所有的 Channel 列表。
如果一个 Module 想返回不同类型的 Channel,需要在后台重载相关函数,比如 im_livechat 要返回所有 livechat 类型的 Channel,它需要这样做:
@api.model
def channel_fetch_slot(self):values = super(MailChannel, self).channel_fetch_slot()pinned_channels = self.env['mail.channel.partner'].search([('partner_id', '=', self.env.user.partner_id.id), ('is_pinned', '=', True)]).mapped('channel_id')values['channel_livechat'] = self.search([('channel_type', '=', 'livechat'), ('id', 'in', pinned_channels.ids)]).channel_info()return values
这里面有个问题,如果很多很多 Channel 怎么办,Odoo 也一次性全捞出来,可能是它赌你不会有很多 Channel,对于客服类的应用,从上边的代码可以看出它指抓 pinned partner,只抓那些活跃的partner对应的Channel,然后它会每天运行 cron,把超过 一天没有活动的 unpin。貌似也是一个解决办法。
Odoo 中的 cron 和 AutoVacuum (自动吸尘器)自己清理一些无用过期数据,只需要重载 AutoVacuum 的 power_on。
<?xml version="1.0" encoding="utf-8"?>
<odoo><record id="autovacuum_job" model="ir.cron"><field name="name">Base: Auto-vacuum internal data</field><field name="model_id" ref="model_ir_autovacuum"/><field name="state">code</field><field name="code">model.power_on()</field><field name='interval_number'>1</field><field name='interval_type'>days</field><field name="numbercall">-1</field></record>
</odoo>
class AutoVacuum(models.AbstractModel):_inherit = 'ir.autovacuum'@api.modeldef power_on(self, *args, **kwargs):self.env['mail.channel'].remove_empty_livechat_sessions()self.env['mail.channel.partner'].unpin_old_livechat_sessions()return super(AutoVacuum, self).power_on(*args, **kwargs)
因为 AutoVacuum 加了一个每日执行的 Cron,重载这个 Cron,就可以定期执行数据整理的任务。
Mail 混合物 (Mail Mixins)
mail.channel 继承了 mail.thread 这个抽象模型。抽象模型(AbstractModel)不创建真正的数据库表,存在的意义就是被其他 Model 继承。
mail.thread 这个抽象模型非常的基础,也非常重要,Odoo 的各个 Module 的 Model 大多数都继承了它。
在Model 中继承 mail.thread,也需要在 views 表单界面(form view)中显示相关的字段:
例如,让我们创建一个代表商务旅行的简单模型。由于组织此类旅行通常涉及很多人和很多讨论,因此让我们在模型上添加对消息的支持。
创建一个 Model,就是一个数据表。可见它继承了 mail.thread。
class BusinessTrip(models.Model):_name = 'business.trip'_inherit = ['mail.thread']_description = 'Business Trip'name = fields.Char()partner_id = fields.Many2one('res.partner', 'Responsible')guest_ids = fields.Many2many('res.partner', 'Participants')
创建这个表的一个 Form View。
<record id="businness_trip_form" model="ir.ui.view"><field name="name">business.trip.form</field><field name="model">business.trip</field><field name="arch" type="xml"><form string="Business Trip"><!-- Your usual form view goes here...Then comes chatter integration --><div class="oe_chatter"><field name="message_follower_ids" widget="mail_followers"/><field name="message_ids" widget="mail_thread"/></div></form></field>
</record>
class=“oe_chatter” 那个HTML元素就能够增加对Model的消息交互的支持了。是不是非常简单,这样Odoo用户就可以针对这个Model的数据进行评论,这个数据的变化也会以消息的形式体现。
名字稍微有点乱,Mail 是代码的模块名称,Discuss 是应用名称,Chatter 是一个功能名称。任何 Model 加了 Chatter 功能后,这个Model的表单界面就会有评论支持的功能。
在模型上添加了聊天功能后,用户可以在模型的任何记录上添加消息或内部注释。其中的每一个消息或者注释都会推送通知(向所有关注者发送消息,向员工(base.group_user)用户发送内部注释)。如果正确配置了您的邮件网关和 CATCHALL 地址,这些通知将通过电子邮件发送,并且可以直接从您的邮件客户端回复。Odoo 会自动路由,将回复的邮件路由到正确的上下文消息会话之中(这也是为啥叫做 Mail Thread 了,名字好多 )。
在服务器端(Python端),mail.thread 提供了一些程序功能可帮助您轻松发送消息并管理记录中的关注者:
发消息
message_post(self, body='', subject=None, message_type='notification', subtype=None, parent_id=False, attachments=None, **kwargs)
在当前会话中发送消息并且返回消息 ID。当前会话是指这个数据记录。
收消息
邮件网关处理新电子邮件时,将调用这些方法。这些电子邮件可以是新主题(如果它们通过alias到达),也可以只是来自现有主题的回复。
通过重载(Override)它们,您可以根据电子邮件本身的一些数据记录值在记录上设置值(如更新日期或电子邮件地址,将CC的地址添加为关注者,等等)。
message_new(msg_dict, custom_values=None)
如果一个消息不属于当前记录的会话,这个函数将会被调用,默认情况下会创建一个新的数据记录,重载这个函数可以实现一些其他的行为。
message_update(msg_dict, update_vals=None)
这个函数会在消息属于当前数据记录的情况下调用,一般是利用消息的信息来更新数据记录的值,重载这个函数可以实现一些其他行为。
订阅者管理(Followers management)
message_subscribe(partner_ids=None, channel_ids=None, subtype_ids=None, force=True)
为记录添加订阅者。
message_unsubscribe(partner_ids=None, channel_ids=None)
删除订阅者。
partner ids 好理解,就是用户。channel ids 是指这些 channel 也订阅了这个数据记录,因为 channel 算是聊天中的一个会话,往往表示在一个会话中的一组人。
记录数据的变更
Mail 模块在字段级别上提供了功能强大的跟踪系统,使您可以将更改记录到聊天记录中。要将跟踪添加到字段,只需将跟踪(tracking)属性设置为True。
class BusinessTrip(models.Model):_name = 'business.trip'_inherit = ['mail.thread']_description = 'Business Trip'name = fields.Char(tracking=True)partner_id = fields.Many2one('res.partner', 'Responsible',tracking=True)guest_ids = fields.Many2many('res.partner', 'Participants')
上面的 Model 中的一些字段的属性 tracking 设置为 True。那么这些字段的所有改变将被以聊天消息的形式日志记录。从现在开始,对旅行名称或负责人的任何更改都会在记录中记录注释。该name字段也将显示在通知中,以提供有关通知的更多上下文信息(即使名称未更改)。
子类型
子类型使您可以更精细地控制消息。子类型充当通知的分类系统,允许文档的订阅者自定义他们希望接收的通知的子类型。
可以通过在 Data 中创建子类型。一个模块中的 Data 都是 XML,会在 Odoo 启动或者模块安装的时候转化为数据记录。
<record id="mt_state_change" model="mail.message.subtype"><field name="name">Trip confirmed</field><field name="res_model">business.trip</field><field name="default" eval="True"/><field name="description">Business Trip confirmed!</field>
</record>
这个XML文档会被转化为 mail message subtype 的一条数据记录。
在对应的model 中重载 _track_subtype 这个函数,可以根据当前数据记录的 state 值的变化决定发送自定义的消息子类型。
class BusinessTrip(models.Model):_name = 'business.trip'_inherit = ['mail.thread']_description = 'Business Trip'name = fields.Char(tracking=True)partner_id = fields.Many2one('res.partner', 'Responsible',tracking=True)guest_ids = fields.Many2many('res.partner', 'Participants')state = fields.Selection([('draft', 'New'), ('confirmed', 'Confirmed')],tracking=True)def _track_subtype(self, init_values):# init_values contains the modified fields' values before the changes## the applied values can be accessed on the record as they are already# in cacheself.ensure_one()if 'state' in init_values and self.state == 'confirmed':return self.env.ref('my_module.mt_state_change')return super(BusinessTrip, self)._track_subtype(init_values)
可见 Odoo 将消息或者讨论的应用范围泛化了,应用到各个 Model 的数据上,当然也能通过 mail.channel实现最基本的实时消息的功能。网站在线客服,员工内部通讯,甚至可以扩展成电话呼叫中心系统。
转载:https://zhuanlan.zhihu.com/p/149864247