Mixin 与实用类

Odoo 实现了一些实用的类和 mixin,便于你为自己的对象添加常用行为。本指南将详细介绍其中的大部分,并附上示例与用例。

消息功能

消息集成

基本消息系统

为模型集成消息功能非常简单。只需继承 mail.thread mixin,并在表单视图中加入 chatter <chatter/> 元素,即可立刻用起来。chatter 元素支持一些用于控制表单行为的选项:

  • open_attachments:默认展开附件区域

  • reload_on_attachment:添加或移除附件时重新加载表单视图

  • reload_on_follower:关注者更新时重新加载表单视图

  • reload_on_post:发布新消息时重新加载表单视图

示例

我们来创建一个简单的模型,表示一次商务出差。由于组织这类出差通常涉及很多人和大量讨论,我们在该模型上添加消息交换支持。

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')

在表单视图中:

<record id="business_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 with options you might want to set -->
            <chatter open_attachments="True"/>
        </form>
    </field>
</record>

为模型添加 chatter 支持后,用户可以轻松地在模型的任意记录上添加消息或内部备注;每一条都会发送通知(消息通知所有关注者,内部备注通知员工(base.group_user)用户)。如果你的邮件网关和 catchall 地址配置正确,这些通知将通过电子邮件发送,并可直接在邮件客户端中回复;自动路由系统会把回复投递到正确的讨论串。

在服务端,提供了一些辅助函数,帮助你轻松发送消息并管理记录上的关注者:

发布消息

message_post(self, body='', subject=None, message_type='notification', subtype=None, parent_id=False, attachments=None, **kwargs)

在现有讨论串中发布一条新消息,返回新建的 mail.message ID。

参数
  • body (str | Markup) – 消息正文。如果为 str 类型,将被转义。HTML 内容请使用 Markup 对象。

  • message_type (str) – 参见 mail_message.message_type 字段

  • parent_id (int) – 处理对上一封消息的回复:在私密讨论时,把父级伙伴添加到消息中

  • attachments (list(tuple(str,str))) – 附件元组列表,格式为 (name,content),其中 content 未经 base64 编码

  • body_is_html (bool) – 指示是否应将 body 视为 HTML,即使它只是 str。

  • **kwargs – 额外的关键字参数将用作新 mail.message 记录的默认列值

退货

新建 mail.message 的 ID

返回类型

int

message_post_with_view(views_or_xmlid, **kwargs):

辅助方法:使用 view_id 通过 ir.qweb 引擎渲染,以发送邮件或发布消息。该方法是独立方法,因为模板和撰写器都不支持批量处理视图。当模板能够处理 ir UI 视图后,此方法很可能会消失。

参数

record (str or ir.ui.view) – 应发送的视图的外部标识或记录

message_post_with_template(template_id, **kwargs)

使用模板发送邮件的辅助方法

参数
  • template_id – 用于渲染并生成消息正文的模板 ID

  • **kwargs – 用于创建 mail.compose.message 向导的参数(该向导继承自 mail.message)

接收消息

当邮件网关处理新电子邮件时,会调用这些方法。这些邮件可能是新的讨论串(如果它们通过 别名 到达),也可能只是现有讨论串的回复。重写这些方法,可以依据邮件本身的一些值来设置讨论串记录上的值(例如更新日期或电子邮件地址、把抄送地址添加为关注者,等等)。

message_new(msg_dict, custom_values=None)

当给定讨论串模型收到新消息、且该消息不属于现有讨论串时,由 message_process 调用。

默认行为是创建对应模型的新记录(依据从消息中提取的一些非常基础的信息)。可通过重写此方法实现额外行为。

参数
  • msg_dict (dict) – 包含邮件详情与附件的映射。详情参见 message_process 与 mail.message.parse

  • custom_values (dict) – 可选字典,包含创建新讨论串记录时传给 create() 的额外字段值;注意,这些值可能会覆盖消息中传来的其他值

返回类型

int

退货

新建讨论串对象的 ID

message_update(msg_dict, update_vals=None)

当现有讨论串收到新消息时,由 message_process 调用。默认行为是使用来件邮件中的 update_vals 更新记录。

可通过重写此方法实现额外行为。

参数
  • msg_dict (dict) – 包含邮件详情与附件的映射;详情参见 message_process 与 mail.message.parse()。

  • update_vals (dict) – 字典,包含按记录 ID 更新记录的值;如果该字典为 None 或为空,则不执行写入操作。

退货

True

关注者管理

message_subscribe(partner_ids=None, channel_ids=None, subtype_ids=None, force=True)

把伙伴添加到记录的关注者中。

参数
  • partner_ids (list(int)) – 将订阅该记录的伙伴 ID

  • channel_ids (list(int)) – 将订阅该记录的频道 ID

  • subtype_ids (list(int)) – 频道或伙伴将订阅的子类型 ID(若为 None,则默认为默认子类型)

  • force – 如果为 True,则在使用参数中给定的子类型创建新关注者之前,先删除现有关注者

退货

成功/失败

返回类型

bool

message_unsubscribe(partner_ids=None, channel_ids=None)

把伙伴从记录的关注者中移除。

参数
  • partner_ids (list(int)) – 将订阅该记录的伙伴 ID

  • channel_ids (list(int)) – 将订阅该记录的频道 ID

退货

True

返回类型

bool

message_unsubscribe_users(user_ids=None)

基于用户的 message_subscribe 包装方法。

参数

user_ids (list(int)) – 将取消订阅该记录的用户 ID;若为 None,则改为取消订阅当前用户。

退货

True

返回类型

bool

记录变更日志

mail 模块为字段增加了强大的跟踪系统,可让你在记录的 chatter 中记录对特定字段的变更。要为字段添加跟踪,只需把 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')

从现在起,对出差名称或负责人的每次变更都会在记录上记一条备注。name 字段也会显示在通知中,以便为通知提供更多上下文(即使名称没有变化)。

子类型

子类型让你能更精细地控制消息。子类型充当通知的分类体系,使文档的订阅者可以自定义希望接收的通知子类型。

子类型以数据形式在模块中创建;该模型具有以下字段:

name (必填)- Char

子类型名称,将显示在通知自定义弹窗中

description - Char

将添加到此子类型所发布消息中的描述。若为空,则改为添加名称

internal - Boolean

带有内部子类型的消息仅员工可见,即 base.group_user 用户组的成员

parent_id - Many2one

用于自动订阅的关联子类型;例如项目子类型通过此关联与任务子类型相连。当某人订阅了某个项目时,他将使用父级子类型订阅该项目的所有任务

relation_field - Char

例如,在关联项目和任务子类型时,关联字段是任务的 project_id 字段

res_model - Char

该子类型适用的模型;若为 False,则该子类型适用于所有模型

default - Boolean

订阅时是否默认启用该子类型

sequence - Integer

用于在通知自定义弹窗中对子类型排序

hidden - Boolean

该子类型是否在通知自定义弹窗中隐藏

将子类型与字段跟踪结合使用,可以依据用户可能关心的内容来订阅不同类别的通知。为此,可以重写 _track_log_get_default_subtype() 函数:

_track_log_get_default_subtype(track_init_values)

根据已更新的值,给出记录变更所触发的子类型。

参数

init_values (dict) – 记录的原始值;字典中仅包含被修改的字段

退货

子类型的完整外部标识;若未触发任何子类型,则为 False

示例

我们在示例类上添加一个 state 字段,并在该字段值变化时触发带有特定子类型的通知。

首先,定义我们的子类型:

<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>

然后,我们需要重写 _track_log_get_default_subtype() 函数。跟踪系统会调用该函数,以根据当前正在应用的变更确定应使用哪个子类型。在我们的例子中,我们希望当 state 字段从 draft 变为 confirmed 时使用我们崭新的子类型:

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_log_get_default_subtype(self, track_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 cache
        self.ensure_one()
        if 'state' in init_values and self.state == 'confirmed':
            return self.env.ref('my_module.mt_state_change')
        return super()._track_log_get_default_subtype(track_init_values)

自定义通知

向关注者发送通知时,在模板中添加按钮以便直接从邮件中执行快捷操作会非常有用。哪怕只是一个直接链接到记录表单视图的简单按钮也很有用;不过多数情况下,你并不希望向门户用户显示这些按钮。

通知系统允许按以下方式自定义通知模板:

  • 显示 访问按钮:这些按钮显示在通知邮件顶部,允许收件人直接访问记录的表单视图

  • 显示 关注按钮:这些按钮允许收件人直接从记录中快速订阅

  • 显示 取消关注按钮:这些按钮允许收件人直接从记录中快速取消订阅

  • 显示 自定义操作按钮:这些按钮会调用特定路由,让你可以直接从邮件中执行一些实用操作(例如将线索转换为商机、为费用经理审批费用单等)

通过重写 _notify_get_groups 函数,可以把这些按钮设置应用到你自行定义的不同用户组。

_notify_get_groups(message, groups)

根据已更新的值,给出记录变更所触发的子类型。

参数
  • message (record) – 当前正在发送的 mail.message 记录

  • groups (list(tuple)) – 格式为 (group_name, group_func,group_data) 的元组列表,其中: group_name 是一个仅用于重写和操作分组的标识符。默认分组为 user(与员工用户关联的收件人)、portal(与门户用户关联的收件人)和 customer(未与任何用户关联的收件人)。重写用法的一个示例是添加一个与 res.groups(如 Hr Officers)关联的分组,以便为其设置特定的操作按钮。 group_func 是一个函数指针,接收伙伴记录作为参数。该方法会应用于收件人,以判断他们是否属于给定分组。只保留第一个匹配的分组。评估顺序即列表顺序。 group_data 是一个字典,包含通知邮件的参数,可能的键值如下: - has_button_access 是否在邮件中显示 Access <Document>。对于新分组默认为 True,对于 portal / customer 为 False。 - button_access 包含按钮 url 和 title 的字典 - has_button_follow 是否在邮件中显示 Follow(如果收件人当前未关注该讨论串)。对于新分组默认为 True,对于 portal / customer 为 False。 - button_follow 包含按钮 url 和 title 的字典 - has_button_unfollow 是否在邮件中显示 Unfollow(如果收件人当前正在关注该讨论串)。对于新分组默认为 True,对于 portal / customer 为 False。 - button_unfollow 包含按钮 url 和 title 的字典 - actions 要在通知邮件中显示的操作按钮列表。每个操作都是一个包含按钮 url 和 title 的字典。

退货

子类型的完整外部标识;若未触发任何子类型,则为 False

操作列表中的 url 可以通过调用 _notify_get_action_link() 函数自动生成:

为当前记录(或在设置了 model 和 res_id 关键字参数时,为特定记录)生成给定类型的链接。

参数

link_type (str) – 要生成的链接类型;可为以下任一值: view 指向记录表单视图的链接 assign 将已登录用户分配到记录的 user_id 字段(如果该字段存在) follow 顾名思义 unfollow 顾名思义 method 在记录上调用方法;方法名应通过关键字参数 method 提供 new 为新记录打开一个空白的表单视图;你可以通过在关键字参数 action_id 中提供动作的 id(数据库 id 或完整解析的外部标识)来指定特定动作

退货

针对记录所选定类型生成的链接

返回类型

str

示例

让我们为商务出差的状态变更通知添加一个自定义按钮;该按钮会将状态重置为草稿,并且仅对(虚构的)旅行管理员用户组(business.group_trip_manager)的成员可见

class BusinessTrip(models.Model):
    _name = 'business.trip'
    _inherit = ['mail.thread', 'mail.alias.mixin']
    _description = 'Business Trip'

    # Pevious code goes here

    def action_cancel(self):
        self.write({'state': 'draft'})

    def _notify_get_groups(self, message, groups):
        """ Handle Trip Manager recipients that can cancel the trip at the last
        minute and kill all the fun. """
        groups = super(BusinessTrip, self)._notify_get_groups(message, groups)

        self.ensure_one()
        if self.state == 'confirmed':
            app_action = self._notify_get_action_link('method',
                                method='action_cancel')
            trip_actions = [{'url': app_action, 'title': _('Cancel')}]

        new_group = (
            'group_trip_manager',
            lambda partner: any(
                user.sudo().has_group('business.group_trip_manager')
                for user in partner.user_ids
            ),
            {'actions': trip_actions},
        )

        return [new_group] + groups

请注意,我本可以在该方法之外定义求值函数,定义一个全局函数来替代 lambda,但为了让这些有时会略显枯燥的文档更简短、更不啰嗦,我选择了前者而非后者。

重写默认值

有多种方式可以自定义 mail.thread 模型的行为,包括(但不限于):

_mail_post_access - Model 属性

在该模型上发布消息所需的访问权限;默认需要 write 权限,也可以设置为 read

上下文键:

这些上下文键可用于在一定程度上控制 mail.thread 的功能,例如在调用 create() 或 write() (或任何其他可能有用的方法)期间进行自动订阅或字段跟踪。

  • mail_create_nosubscribe:在 create 或 message_post 时,不要将当前用户订阅到记录的讨论串

  • mail_create_nolog:在 create 时,不记录自动生成的 “<Document> created” 消息

  • mail_notrack:在 create 和 write 时,不执行会创建消息的值跟踪

  • tracking_disable:在 create 和 write 时,不执行任何 MailThread 功能(自动订阅、跟踪、发布……)

  • mail_auto_delete:自动删除邮件通知;默认为 True

  • mail_notify_force_send:如果待发送的邮件通知少于 50 封,则直接发送,而不使用队列;默认为 True

  • mail_notify_user_signature:在邮件通知中添加当前用户的签名;默认为 True

邮件别名

别名是可配置的电子邮件地址,它们关联到特定记录(该记录通常继承 mail.alias.mixin 模型),当有人通过电子邮件联系该地址时会创建新记录。这是一种让你的系统可从外部访问的简便方式,使用户或客户无需直接连接 Odoo,即可快速在你的数据库中创建记录。

别名与收件邮件网关

有些人出于同样的目的使用收件邮件网关。使用别名时你仍然需要一个配置正确的邮件网关,不过一个全收(catchall)域就足够了,因为所有路由都在 Odoo 内部完成。相比邮件网关,别名有几个优势:

  • 配置更简单
    • 多个别名可以共用一个收件网关;这样就不必在你的域名上配置多个电子邮件地址(所有配置都在 Odoo 内部完成)

    • 配置别名无需系统访问权限

  • 更一致
    • 可在相关记录上配置,而无需在设置子菜单中配置

  • 更容易在服务器端重写
    • 该 Mixin 模型从一开始就是为扩展而构建的,让你能比使用邮件网关更轻松地从收到的电子邮件中提取有用数据。

别名支持集成

别名通常配置在父模型上,当通过电子邮件联系该地址时,父模型会创建特定的记录。例如,项目应用有用于创建任务或问题的别名,销售团队有用于生成线索的别名。

注解

由别名创建的模型 必须 继承 mail_thread 模型。

通过继承 mail.alias.mixin 来添加别名支持;该 mixin 会为父类中创建的每条记录生成一条新的 mail.alias 记录(例如,每条 project.project 记录在创建时都会初始化其对应的 mail.alias 记录)。

注解

别名也可以手动创建,并通过一个简单的 Many2one 字段来支持。本指南假定你希望获得更完整的集成,包括自动创建别名、记录专用的默认值等。

与 mail.thread 继承不同,mail.alias.mixin 需要 一些特定的重写才能正确工作。这些重写会指定所创建别名的值,例如它必须创建哪类记录,以及这些记录可能根据父对象而具有的一些默认值:

_get_alias_model_name(vals)

返回该别名的模型名称。不是对现有记录的回复的收件邮件,将导致创建一条该别名模型的新记录。该值可能取决于 vals,即创建该模型的记录时传给 create 的值字典。

参数

dict (vals) – 将要持有该别名的、新建记录的值

退货

模型名称

返回类型

str

_get_alias_values()

返回用于创建别名、或在别名创建后写入别名的值。虽然并非完全强制,但通常需要通过别名的 alias_defaults 字段设置一个默认值字典,以确保新建的记录会关联到别名的父对象(即任务会在正确的项目中创建)。

退货

将要写入新别名的值字典

返回类型

dict

_get_alias_values() 的重写尤其值得关注,因为它让你可以轻松修改别名的行为。在可以在别名上设置的字段中,以下这些尤其值得关注:

alias_name - Char

电子邮件别名的名称,例如,如果你想接收发往 <jobs@example.odoo.com> 的邮件,就设为 ‘jobs’

alias_user_id - Many2one (res.users)

通过该别名接收邮件时所创建记录的所有者;如果未设置该字段,系统将尝试根据发件人(From)地址找到合适的所有者,如果找不到与该地址对应的系统用户,则使用管理员账户

alias_defaults - Text

一个 Python 字典,在为该别名创建新记录时会被求值,以提供默认值

alias_force_thread_id - Integer

可选的讨论串(记录)ID,所有收到的消息都会附加到该讨论串,即使它们并不是对它的回复;如果设置了该值,将完全禁用新记录的创建

alias_contact - Selection

使用邮件网关在文档上发布消息的策略

  • everyone:所有人都可以发布

  • partners:仅限已通过身份验证的合作伙伴

  • followers:仅限相关文档的关注者或所关注频道的成员

请注意,别名使用了 委托继承,这意味着虽然别名存储在另一张表中,你仍可以从父对象直接访问所有这些字段。这样你就能轻松地从记录的表单视图配置别名。

示例

让我们在商务出差类上添加别名,以便通过电子邮件即时创建费用。

class BusinessTrip(models.Model):
    _name = 'business.trip'
    _inherit = ['mail.thread', 'mail.alias.mixin']
    _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)
    expense_ids = fields.One2many('business.expense', 'trip_id', 'Expenses')
    alias_id = fields.Many2one('mail.alias', string='Alias', ondelete="restrict",
                               required=True)

    def _get_alias_model_name(self, vals):
    """ Specify the model that will get created when the alias receives a message """
        return 'business.expense'

    def _get_alias_values(self):
    """ Specify some default values that will be set in the alias at its creation """
        values = super(BusinessTrip, self)._get_alias_values()
        # alias_defaults holds a dictionary that will be written
        # to all records created by this alias
        #
        # in this case, we want all expense records sent to a trip alias
        # to be linked to the corresponding business trip
        values['alias_defaults'] = {'trip_id': self.id}
        # we only want followers of the trip to be able to post expenses
        # by default
        values['alias_contact'] = 'followers'
        return values

class BusinessExpense(models.Model):
    _name = 'business.expense'
    _inherit = ['mail.thread']
    _description = 'Business Expense'

    name = fields.Char()
    amount = fields.Float('Amount')
    trip_id = fields.Many2one('business.trip', 'Business Trip')
    partner_id = fields.Many2one('res.partner', 'Created by')

我们希望能在商务出差的表单视图中轻松配置别名,因此让我们在表单视图中添加以下内容:

<page string="Emails">
    <group name="group_alias">
        <label for="alias_name" string="Email Alias"/>
        <div name="alias_def">
            <!-- display a link while in view mode and a configurable field
            while in edit mode -->
            <field name="alias_id" class="oe_read_only oe_inline"
                    string="Email Alias" required="0"/>
            <div class="oe_edit_only oe_inline" name="edit_alias"
                 style="display: inline;" >
                <field name="alias_name" class="oe_inline"/>
                @
                <field name="alias_domain" class="oe_inline" readonly="1"/>
            </div>
        </div>
        <field name="alias_contact" class="oe_inline"
                string="Accept Emails From"/>
    </group>
</page>

现在我们可以直接从表单视图修改别名地址,并更改谁可以向该别名发送电子邮件。

然后我们可以在费用模型上重写 message_new(),以便在创建费用时从我们的电子邮件中获取值:

class BusinessExpense(models.Model):
    # Previous code goes here
    # ...

    def message_new(self, msg, custom_values=None):
        """ Override to set values according to the email.

        In this simple example, we simply use the email title as the name
        of the expense, try to find a partner with this email address and
        do a regex match to find the amount of the expense."""
        name = msg_dict.get('subject', 'New Expense')
        # Match the last occurrence of a float in the string
        # Example: '50.3 bar 34.5' becomes '34.5'. This is potentially the price
        # to encode on the expense. If not, take 1.0 instead
        amount_pattern = '(\d+(\.\d*)?|\.\d+)'
        expense_price = re.findall(amount_pattern, name)
        price = expense_price and float(expense_price[-1][0]) or 1.0
        # find the partner by looking for it's email
        partner = self.env['res.partner'].search([('email', 'ilike', email_address)],
                                                 limit=1)
        defaults = {
            'name': name,
            'amount': price,
            'partner_id': partner.id
        }
        defaults.update(custom_values or {})
        res = super(BusinessExpense, self).message_new(msg, custom_values=defaults)
        return res

活动跟踪

活动是用户必须对文档执行的操作,例如打电话或安排会议。活动随 mail 模块提供,因为它们集成在 Chatter 中,但 并不随 mail.thread 一起提供。活动是 mail.activity 类的记录,具有类型(mail.activity.type)、名称、描述、计划时间等属性。待处理的活动显示在 chatter 小部件的消息历史之上。

可以使用对象上的 mail.activity.mixin 类来集成活动。此时 Chatter 会在表单视图中显示这些活动,而 kanban_activity 小部件可以在记录的看板视图中显示它们(通过 activity_ids 字段)。

示例

组织商务出差是一个繁琐的过程,跟踪诸如预订机票或前往机场的出租车等所需活动会很有用。为此,我们将在模型上添加活动 mixin,并在出差的邮件历史中显示接下来的计划活动。

class BusinessTrip(models.Model):
    _name = 'business.trip'
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _description = 'Business Trip'

    name = fields.Char()
    # [...]

我们修改出差的表单视图,以显示其接下来的活动:

<record id="business_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 -->
            <chatter/>
        </form>
    </field>
</record>

你可以在以下模型中找到具体的集成示例:

  • CRM(crm)应用中的 crm.lead

  • 销售(sale)应用中的 sale.order

  • 项目(project)应用中的 project.task

网站功能

访客跟踪

utm.mixin 类可用于通过指向特定资源的链接中的参数来跟踪在线营销/传播活动。该 mixin 会向你的模型添加 3 个字段:

  • campaign_id:指向 utm.campaign 对象的 Many2one 字段(即 Christmas_Special、Fall_Collection 等)

  • source_id:指向 utm.source 对象的 Many2one 字段(即 Search Engine、邮件列表等)

  • medium_id:指向 utm.medium 对象的 Many2one 字段(即 Snail Mail、e-Mail、社交网络更新等)

这些模型只有一个 name 字段(即它们只是用来区分活动,并没有任何特定行为)。

一旦有客户带着在 url 中设置的这些参数访问你的网站(即 https://www.odoo.com/?campaign_id=mixin_talk&source_id=www.odoo.com&medium_id=website),系统就会在访客的浏览器中为这些参数设置三个 cookie。一旦从网站创建了继承 utm.mixin 的对象(即线索表单、职位申请等),utm.mixin 的代码就会介入,并从 cookie 中获取值并设置到新记录中。完成之后,你就可以像使用其他字段一样,在定义报表和视图(分组等)时使用活动/来源/媒介字段。

要扩展此行为,只需向一个简单模型添加一个关系字段(该模型应支持 快速创建 (即使用单个 name 值调用 create())),并扩展 tracking_fields() 函数:

class UtmMyTrack(models.Model):
    _name = 'my_module.my_track'
    _description = 'My Tracking Object'

    name = fields.Char(string='Name', required=True)


class MyModel(models.Models):
    _name = 'my_module.my_model'
    _inherit = ['utm.mixin']
    _description = 'My Tracked Object'

    my_field = fields.Many2one('my_module.my_track', 'My Field')

    @api.model
    def tracking_fields(self):
        result = super(MyModel, self).tracking_fields()
        result.append([
        # ("URL_PARAMETER", "FIELD_NAME_MIXIN", "NAME_IN_COOKIES")
            ('my_field', 'my_field', 'odoo_utm_my_field')
        ])
        return result

这会告诉系统创建一个名为 odoo_utm_my_field 的 cookie,其值取自 url 参数 my_field;一旦通过网站表单的调用创建了该模型的新记录,utm.mixin 对 create() 方法的通用重写就会从 cookie 中获取该字段的默认值(如果 my_module.my_track 记录尚不存在,也会即时创建它)。

你可以在以下模型中找到具体的集成示例:

  • CRM(crm)应用中的 crm.lead

  • 招聘流程(hr_recruitment)应用中的 hr.applicant

  • 帮助台(helpdesk - 仅 Odoo 企业版)应用中的 helpdesk.ticket

网站可见性

你可以相当轻松地为任意记录添加网站可见性开关。虽然这个 mixin 手动实现起来也很容易,但它是继 mail.thread 继承之后最常用的 mixin;这也证明了它的实用性。该 mixin 的典型用例是任何拥有前端页面的对象;能够控制页面的可见性,让你可以从容地编辑页面,并在满意后才发布。

要包含该功能,你只需继承 website.published.mixin:

class BlogPost(models.Model):
    _name = "blog.post"
    _description = "Blog Post"
    _inherit = ['website.published.mixin']

该 mixin 会向你的模型添加 2 个字段:

  • website_published:表示发布状态的 Boolean 字段

  • website_url:表示访问该对象所用 URL 的 Char 字段

请注意,最后一个字段是计算字段,必须为你的类实现:

def _compute_website_url(self):
    for blog_post in self:
        blog_post.website_url = "/blog/%s" % (log_post.blog_id)

机制就位后,你只需调整前端和后端视图使其可用。在后端,通常的做法是在按钮框中添加一个按钮:

<button class="oe_stat_button" name="website_publish_button"
    type="object" icon="fa-globe">
    <field name="website_published" widget="website_button"/>
</button>

在前端,需要进行一些安全检查,以避免向网站访问者显示“编辑”按钮:

<div id="website_published_button" class="float-right"
     groups="base.group_website_publisher"> <!-- or any other meaningful group -->
    <t t-call="website.publish_management">
      <t t-set="object" t-value="blog_post"/>
      <t t-set="publish_edit" t-value="True"/>
      <t t-set="action" t-value="'blog.blog_post_action'"/>
    </t>
</div>

请注意,你必须将对象作为变量 object 传给模板;在此示例中,blog.post 记录作为 blog_post 变量传给了 qweb 渲染引擎,因此需要向发布管理模板指明这一点。publish_edit 变量允许前端按钮链接到后端(让你可以轻松地在前端与后端之间切换);如果设置了该变量,你必须在 action 变量中指定要在后端调用的动作的完整外部标识(注意,该模型必须存在表单视图)。

动作 website_publish_button 定义在该 mixin 中,并会根据你的对象调整其行为:如果该类具有有效的 website_url 计算函数,用户点击按钮时会被重定向到前端;随后用户可以直接从前端发布页面。这可确保不会意外地在线发布。如果没有计算函数,则只是切换布尔字段 website_published。

网站元数据

这个简单的 mixin 让你可以轻松地在前端页面中注入元数据。

class BlogPost(models.Model):
    _name = "blog.post"
    _description = "Blog Post"
    _inherit = ['website.seo.metadata', 'website.published.mixin']

该 mixin 会向你的模型添加 3 个字段:

  • website_meta_title:用于为页面设置额外标题的 Char 字段

  • website_meta_description:包含页面简短描述的 Char 字段(有时用于搜索结果中)

  • website_meta_keywords:包含一些关键字的 Char 字段,可帮助搜索引擎更精确地对你的页面进行分类;“推广”工具会帮助你轻松选出词汇相关的关键字

这些字段可以在前端使用编辑器工具栏中的“推广”工具进行编辑。设置这些字段有助于搜索引擎更好地索引你的页面。请注意,搜索引擎并非仅根据这些元数据来生成结果;最佳的 SEO 实践仍然是获得可靠来源的引用。

其它

客户评分

评分 mixin 允许发送电子邮件征求客户评分、在看板流程中自动转换状态,以及汇总你的评分统计数据。

为模型添加评分

要添加评分支持,只需继承 rating.mixin 模型:

class MyModel(models.Models):
    _name = 'my_module.my_model'
    _inherit = ['rating.mixin', 'mail.thread']

    user_id = fields.Many2one('res.users', 'Responsible')
    partner_id = fields.Many2one('res.partner', 'Customer')

该 mixin 的行为会根据你的模型进行调整:

  • rating.rating 记录将关联到你模型的 partner_id 字段(如果该字段存在)。

    • 如果你使用 partner_id 以外的字段,可以通过 rating_get_partner_id() 函数重写此行为

  • rating.rating 记录将关联到你模型 user_id 字段对应的合作伙伴(如果该字段存在)(即被评分的合作伙伴)

    • 如果你使用 user_id 以外的字段,可以通过 rating_get_rated_partner_id() 函数重写此行为(注意,该函数必须返回一个 res.partner;对于 user_id,系统会自动获取该用户的合作伙伴)

  • chatter 历史记录中会显示评分事件(如果你的模型继承自 mail.thread)

通过电子邮件发送评分请求

如果希望通过电子邮件请求评分,只需生成一封包含指向评分对象链接的电子邮件即可。一个非常基础的电子邮件模板可以如下所示:

<record id="rating_my_model_email_template" model="mail.template">
            <field name="name">My Model: Rating Request</field>
            <field name="email_from">${object.rating_get_rated_partner_id().email or '' | safe}</field>
            <field name="subject">Service Rating Request</field>
            <field name="model_id" ref="my_module.model_my_model"/>
            <field name="partner_to" >${object.rating_get_partner_id().id}</field>
            <field name="auto_delete" eval="True"/>
            <field name="body_html"><![CDATA[
% set access_token = object.rating_get_access_token()
<p>Hi,</p>
<p>How satsified are you?</p>
<ul>
    <li><a href="/rate/${access_token}/5">Satisfied</a></li>
    <li><a href="/rate/${access_token}/3">Okay</a></li>
    <li><a href="/rate/${access_token}/1">Dissatisfied</a></li>
</ul>
]]></field>
</record>

随后,您的客户会收到一封电子邮件,其中的链接指向一个简单网页,让他们可以针对与您用户的互动提供反馈(包括自由文本形式的反馈信息)。

然后,您可以通过为评分定义动作,非常轻松地将评分集成到您的表单视图中:

<record id="rating_rating_action_my_model" model="ir.actions.act_window">
    <field name="name">Customer Ratings</field>
    <field name="res_model">rating.rating</field>
    <field name="view_mode">kanban,pivot,graph</field>
    <field name="domain">[('res_model', '=', 'my_module.my_model'), ('res_id', '=', active_id), ('consumed', '=', True)]</field>
</record>

<record id="my_module_my_model_view_form_inherit_rating" model="ir.ui.view">
    <field name="name">my_module.my_model.view.form.inherit.rating</field>
    <field name="model">my_module.my_model</field>
    <field name="inherit_id" ref="my_module.my_model_view_form"/>
    <field name="arch" type="xml">
        <xpath expr="//div[@name='button_box']" position="inside">
            <button name="%(rating_rating_action_my_model)d" type="action"
                    class="oe_stat_button" icon="fa-smile-o">
                <field name="rating_count" string="Rating" widget="statinfo"/>
            </button>
        </xpath>
    </field>
</record>

请注意,评分提供了默认视图(看板、数据透视、图表),可让您快速鸟瞰客户评分。

你可以在以下模型中找到具体的集成示例:

  • Project(rating_project)应用中的 project.task

  • 帮助台(helpdesk - 仅 Odoo 企业版)应用中的 helpdesk.ticket