第 12 章:继承

Odoo 的一个强大之处在于其模块化。每个模块都专用于满足某项业务需求,但模块之间也可以相互交互。这对于扩展现有模块的功能非常有用。例如,在我们的房地产场景中,我们希望直接在常规用户视图中显示某位销售员的房产列表。

但在深入了解 Odoo 的模块继承之前,我们先看看如何修改标准 CRUD(创建、读取、更新、删除)方法的行为。

Python 继承

注解

目标:本节结束时:

  • 不应允许删除状态不是新建或已取消的房产。

删除
  • 创建报价后,房产状态应变为“已收到报价”

  • 不应允许创建价格低于现有报价的报价

创建

在我们的房地产模块中,我们从不需要专门开发任何东西就能执行标准 CRUD 操作。Odoo 框架提供了执行这些操作所需的工具。事实上,得益于经典的 Python 继承,这些操作已经包含在我们的模型中了:

from odoo import fields, models

class TestModel(models.Model):
    _name = "test_model"
    _description = "Test Model"

    ...

我们的 class TestModel 继承自 Model,该模型提供了 create()、read()、write() 和 unlink()。

这些方法(以及 Model 上定义的任何其他方法)都可以扩展,以添加特定的业务逻辑:

from odoo import fields, models

class TestModel(models.Model):
    _name = "test_model"
    _description = "Test Model"

    ...

    @api.model
    def create(self, vals_list):
        # Do some business logic, modify vals...
        ...
        # Then call super to execute the parent method
        return super().create(vals_list)

对于 create() 方法来说,装饰器 model() 是必需的,因为在创建的上下文中,记录集 self 的内容并无意义,但对其他 CRUD 方法则不需要。

同样需要注意的是,尽管我们可以直接重写 unlink() 方法,但几乎总是更希望改用装饰器 ondelete() 编写一个新方法。带有该装饰器的方法会在 unlink() 期间被调用,从而避免在直接重写 unlink() 时卸载该模型所属模块可能引发的一些问题。

在 Python 3 中,super() 等价于 super(TestModel, self)。当你需要用修改过的记录集调用父方法时,可能需要使用后一种写法。

危险

  • 始终 调用 super() 非常重要,否则会破坏流程。只有在极少数非常特殊的情况下才不需要调用它。

  • 请确保 始终 返回与父方法一致的数据。例如,如果父方法返回 dict(),你的重写方法也必须返回 dict()。

练习

为 CRUD 方法添加业务逻辑。

  • 如果房产状态不是“新建”或“已取消”,则禁止删除该房产

提示:使用 ondelete() 装饰器创建一个新方法,并记住 self 可能是包含多条记录的记录集。

  • 创建报价时,将房产状态设为“已收到报价”。如果用户试图创建金额低于现有报价的报价,还应抛出错误。

提示:property_id 字段在 vals 中可用,但它的类型是 int。要实例化 estate.property 对象,请使用 self.env[model_name].browse(value) (示例)

模型继承

参考:与本主题相关的文档可在 继承与扩展 中找到。

在我们的房地产模块中,我们希望直接在 设置 / 用户与公司 / 用户 表单视图中显示与某位销售员关联的房产列表。为此,我们需要向 res.users 模型添加一个字段,并调整其视图以显示该字段。

Odoo 提供了两种 继承 机制,以模块化的方式扩展现有模型。

第一种继承机制允许模块通过以下方式修改在另一个模块中定义的模型的行为:

  • 向模型添加字段,

  • 重写模型中字段的定义,

  • 向模型添加约束,

  • 向模型添加方法,

  • 重写模型中的现有方法。

第二种继承机制(委托)允许将模型的每条记录与父模型的记录关联起来,并提供对父记录字段的透明访问。

继承方法

在 Odoo 中,第一种机制是迄今为止最常用的。在我们的例子中,我们想向现有模型添加字段,这意味着我们将使用第一种机制。例如:

from odoo import fields, models

class InheritedModel(models.Model):
    _inherit = ["inherited.model"]

    new_field = fields.Char(string="New Field")

在 此处 可以找到一个向模型添加两个字段的实际示例。

按照惯例,每个被继承的模型都定义在各自的 Python 文件中。在我们的示例中,该文件是 models/inherited_model.py。

练习

向用户添加字段。

  • 向 res.users 添加以下字段:

字段

类型

property_ids

estate.property 中引用销售员的字段的 One2many 反向字段

  • 为该字段添加域,使其只列出可用的房产。

在下一节中,我们将把该字段添加到视图中,并检查一切是否正常!

视图继承

参考:与本主题相关的文档可在 继承 中找到。

注解

目标:本节结束时,应能在销售员的用户表单视图中显示与其关联的可用房产列表

用户

Odoo 不是就地修改现有视图(通过覆盖它们),而是提供视图继承:子级“扩展”视图会应用在根视图之上。这些扩展既可以向父视图添加内容,也可以从中移除内容。

扩展视图使用 inherit_id 字段引用其父视图。它的 arch 字段不是单个视图,而是包含若干 xpath 元素,用于选择并修改父视图的内容:

<record id="inherited_model_view_form" model="ir.ui.view">
    <field name="name">inherited.model.form.inherit.test</field>
    <field name="model">inherited.model</field>
    <field name="inherit_id" ref="inherited.inherited_model_view_form"/>
    <field name="arch" type="xml">
        <!-- find field description and add the field
             new_field after it -->
        <xpath expr="//field[@name='description']" position="after">
          <field name="new_field"/>
        </xpath>
    </field>
</record>
expr

一个 XPath 表达式,用于在父视图中选择单个元素。如果未匹配任何元素或匹配到多个元素,则引发错误

position

要对匹配元素执行的操作:

inside

将 xpath 的主体追加到匹配元素的末尾

replace

用 xpath 的主体替换匹配元素,并将新主体中出现的所有 $0 节点替换为原始元素

before

将 xpath 的主体作为同级元素插入到匹配元素之前

after

将 xpaths 的主体作为同级元素插入到匹配元素之后

attributes

使用 xpath 主体中的特殊 attribute 元素修改匹配元素的属性

当只匹配单个元素时,可以直接在要查找的元素上设置 position 属性。下面两种继承方式的结果相同。

<xpath expr="//field[@name='description']" position="after">
    <field name="idea_ids" />
</xpath>

<field name="description" position="after">
    <field name="idea_ids" />
</field>

在 此处 可以找到一个视图继承扩展示例。

练习

向用户视图添加字段。

在新的笔记本页面中,将 property_ids 字段添加到 base.view_users_form。

提示:在 此处 可以找到一个用户视图继承的示例。

得益于模块化理念,继承在 Odoo 中被广泛使用。别犹豫,阅读相应文档以获取更多信息!

在 下一章 中,我们将学习如何与其他模块交互。