Alan Hou的个人博客

第四章 Odoo 15开发之模块继承

Odoo 的一项强大之处是无需直接修改所扩展模块的代码即可添加功能。这都归功于与自身代码组件相独立的功能继承。对模块的扩展可通过继承机制实现,以已有对象的修改层的形式。这些修改可以发生在每个层面,包括模型、视图和业务逻辑层面。我们不是直接修改原有模块,而是新建一个模块,采用所要做的修改在已有模块上新增一层。

上一章讲解了如何从零开始创建应用。本章中我们将学习如何创建继承自已有应用或模块的模块,并使用原有的内核或社区功能。

为此,我们会讲解如下内容:

学习完本章后,读者可以对已有Odoo应用创建继承模块。可以对应用的任一组件做出修改:模型、视图、业务逻辑代码、网页控制器和网页模板。

开发准备

本文要求可通过命令行来启动 Odoo 服务。

代码将在第三章 Odoo 15开发之创建第一个 Odoo 应用的基础上进行修改。通过该篇的学习我们已在插件路径添加了代码并在数据安装了library_app模块。

本章对项目新增library_member插件模块。相应的代码请见GitHub 仓库ch04目录。

学习项目-扩展图书馆应用

第三章 Odoo 15开发之创建第一个 Odoo 应用中我们创建了一个图书应用的初始模块,可供查看图书清单。现在我们要对图书应用进行扩展添加图书会员并允许他们借书。这需要创建一个扩展模块library_member。

我们要提供如下的功能:

后面我们会引入一个功能让会员可从图书馆借书,但这不在当前的讨论范畴。在后面的几章中会逐步展开。

图书

以下是要对图书所要做的技术修改的汇总:

会员

以下是要对图书会员所要做的技术修改的汇总:

首先在library_app同级目录创建一个library_member目录作为扩展模块,并在其中添加两个文件,一个__init__.py空文件和一个包含如下内容的__manifest__.py文件:

接着我们就可以开发功能了。第一个任务是常用的简单需求:对已有模型新增字段。这正好是介绍Odoo继承机制的好机会。

对已有模型新增字段

第一步我们来为Book模型添加is_available布尔型字段。当前它只是一个简单的可编辑字段,但在之后我们会将其变成自动根据所借阅和归还的图书来赋值。

要继承已有模型,需要在 Python 类中添加一个_inherit 属性来标明所继承的模型。新类继承父 Odoo 模型的所有功能,仅需在其中声明要做的修改。可以认为这类继承是对已有模型的引用并在插入了一些修改。

通过继承为模型插入新字段

继承模型是通过 Python类以及 Odoo自有的继承机制,使用_inherit 类属性进行声明。_inherit属性标明所继承的模型。所声明的调用抓取父 Odoo 模型的所有功能,仅需声明要做修改的部分。

编码指南推荐为每个模型创建一个 Python 文件,因此我们添加library_member/models/library_book.py文件来继承原模型。首先创建__init__.py文件来导入该文件:

1、添加library_member/__init__.py文件来导入 models 子文件夹

2、添加library_member/models/__init__.py文件,导入models子文件夹中中的文件:

3、创建library_member/models/library_book.py文件来继承library.book模型:

此处我们使用了_inherit类属性来声明所继承模型。注意我们并没有使用到其它类属性,连_name 也没使用。除非想要做出修改,否则不需要使用这些属性。

小贴士:_name是模型标识符,如果修改会发生什么呢?其实你可以修改,这时它会创建所继承模型的拷贝,成为一个新模型。这叫作原型继承,本文后面通过原型拷贝模型一节会讨论。

可以把它看成是引用了中央仓库中的一个模型定义,然后在其内进行修改。修改包含添加字段、修改已有字段、修改模型类属性或添加带有新业务逻辑的方法。

要在数据表中添加新增模型字段,必须要先安装插件模块。如果一切顺利的话,就可以通过TechnicalDatabase StructureModels菜单查看到library.book模型中新增了这一字段。

对表单视图添加字段

表单、列表和搜索视图通过XML数据结构定义。需要一种修改 XML 的方式来继承视图。也即要定位到 XML 元素然后对该处进行修改。

所继承视图的 XML 数据记录和普通视图中相似,多了一个 inherit_id属性来引用所要继承的视图。

下面我们继承图书视图并添加is_available字段。

首先要查找待继承的视图的XML ID.通过Settings > Technical > User Interface > Views菜单来查看。图书表单的XML ID是library_app.view_form_book

然后还要找到要插入修改的XML元素。我们选择在ISBN字段之后添加Is Available?字段。通常通过name 属性定位元素。此处为<field name="isbn" />

我们添加XML文件,即views/book_view.xml来继承 Partner 视图,内容如下:

以上代码中,我们高亮显示了继承相关的元素。inherit_id记录字段通过 ref 属性指向视图的外部标识符定位所继承的视图。

arch包含所声明扩展点处使用的元素,一个带有name=”isbn”<field>元素,同时包含position=”after”来声明位置。在扩展元素内,使用XML来添加is_available字段。

创建完继承之后图书表单(在声明文件中添加该视图文件并升级插件)如下图:

图4.1:添加了Is Available?字段后的图书表单

我们学习了继承的基础知识,对模型层和视图层新增了一个字段。接下来,我们将学习我们所使用的模型继承方法,即经典继承。

使用经典的in-place继承来扩展模型

可以把经典模型继承看作是一个插入(in-place)扩展。在声明了具有_inherit属性的Python类时,它获取到了对相应模型定义的引用,然后对其添加扩展。模型定义存储在Odoo模型仓库中,我们可对其做进一步的修改。

下面我们学习如何在常用的继承用例中使用经典继承:修改已有字段的属性并扩展Python方法来添加或变更业务逻辑。

增量修改已有字段

继承模型时,可对已有字段做出增量修改。也就是只需要定义要修改或添加的属性。

我们对library_app模块中所创建的Book模型做两处修改:

编辑library_member/models/library_book.py文件,并在library.book 模型中添加如下代码:

这会对字段的指定属性作出修改,未指定的属性保持不变。

升级模块,进入图书表单,将鼠标悬停在 ISBN 字段上,就可以看到所添加的提示信息了。index=True这一修改的效果不太容易发现,通过开发者工具菜单的View Fields选项或Settings > Technical > Database Structure > Models菜单下的字段定义中可进行查看。

图4.2:出版社字段启用了索引

继承 Python 方法对业务逻辑添加功能

Python 方法中编写的业务逻辑也可以被继承。Odoo 借用了 Python 已有的父类行为的对象继承机制。

举个实际的例子,我们将扩展图书 ISBN 的验证逻辑。在图书应用中仅能验证现代的13位ISBN,但老一些的图书可能只有10位数的 ISBN。我们继承_check_isbn()方法来完成这种情况的验证。

library_member/models/library_book.py文件中添加如下代码:

在继承类中继承方法,我们要使用相同方法名重新定义该方法,本例中即为_check_isbn()。这个方法使用 super()来调用父类已实现的方法。本例中对应的代码为super()._check_isbn()

在方法继承中,我们在调用父类的super()的前添加了自己的逻辑。这个方法验证ISBN是否为10位数。若是则执行所添加的对10位ISBN的验证。否则进入原有的13位验证逻辑。

如果想要进行测试或是书写测试用例。这里有一个10位ISBN的示例:威廉·戈尔丁所著《蝇王》的原始ISBN为0-571-05686-5。

Odoo 11中的变化
在Odoo 11中,所使用的Python版本由2.7变为3.5 或更新版本。Python 3做出了很大的改版,不完全兼容Python 2。尤其是在Python 3中简化了super()的语法。之前使用Python 2的Odoo版本中,super() 需要传入两个参数:类名和self;例如super(Book, self)._check_isbn()

经典继承是最常用的继承机制。但Odoo还提供了其它的继承方式,用于别的场景。接下来我们一同学习。

其它模型继承机制

前面我们介绍了经典继承,可以看成是一种原地修改的扩展。这是最常用的一种方式,但Odoo框架还支持适用其它场景下几种继承机制。

分别是代理继承、型继承以及使用mixin:

下面几节会进行深入的讲解。

使用代理继承内嵌模型

使用代理继承无需复制数据即可在数据库中复用数据结构。它在继承模型中嵌入所代理模型实例。

注:从技术角度严格地说,代理继承并不是真的对象继承,而是一种对象组合,将一个对象的一些功能代理至另一个对象,或由另一个对象提供一些功能。

关于代理继承的要点:

举个例子,对于内核 User模型,每条记录包含一条 Partner 记录,因此包含 Partner 中的所有字段以及User自身的一些字段。

在图书项目中,我们要添加一个图书会员模型。会员有会员卡并通过会员卡借阅读书。会员主数据应包含卡号,以及一些个信息,如email和地址。Partner 模型已包含联系和地址信息,所以最好是复用,而不去创建重复的数据结构。

按如下步骤使用代理继承在图书会员模型中加入Partner字段:

  1. 需要导入实现进程的Python文件。编辑 library_member/model/__init__.py添加如下高亮的代码:
  2. 然后添加描述新的图书会员模型的Python文件:library_member/models/library_member.py,其中包含如下代码:

    通过代理继承,library.member 中嵌入了所继承的模型:res.partner,因此在新建会员记录时,会自动创建一个关联的 Partner并通过partner_id字段引用。

    透过代理机制,嵌套模型的所有字段像父模型字段一样自动可用。本例中,会员模型可使用 Partner 中的所有字段,如 name, addressemail,以及会员自身的独有字段,如card_number。底层Partner 字段存储于关联的 Partner 记录中,没有重复的数据结构。

    代理继承仅用在数据层面,不适用于逻辑层。没有继承所继承模型的任意方法。但仍可使用点号运算符来访问,也称为点号标记,用于访问对象属性。例如,会员模型中partner_id.open_parent() 运行嵌套Partner记录的open_parent()方法。

    代理继承还有一种替代语法,使用_inherits模型属性。这来自Odoo 8之前的老API,但仍在广泛使用。和上述代码相同效果的图书模型代码如下:

    完成新模型的添加,还需要完成几步:添加权限ACL、菜单和一些视图。

  3. 添加权限ACL,创建library_member/security/ir.model.access.csv文件并加入如下代码来:

  4. 添加菜单项,创建library_member/views/library_menu.xml文件并加入如下代码:

  5. 添加视图,创建library_member/views/member_view.xml文件并加入如下代码:

  6. 最后,编辑manifest文件来声明这三个新文件:

如果代码编写正确,升级模块后即可使用新的图书会员模型了。

使用原型继承拷贝功能

经典继承使用_inherit属性扩展模型。因其未修改_name属性,可对该模型执行有效的原地变更。

如果使用_inherit的同时修改了_name属性,就会获得一具所继承模型的副本。这时新模型就会获得仅针对其自身的功能,不会添加到父模型中。副本模型与父模型相独立,不受父模型修改的影响,它有自有的数据表和数据。官方文档将这种继承称为原型继承

在实际开发中,使用_inherit进行模型拷贝没太大用处。一般会更偏好代理继承,因为不拷贝就可复用数据结构。

在对多个父模型继承时,_inherit的值就不单个名称,而是一个模型名列表。

这可用于将多个模型混合加入一个模型。这样我们多次使用同一模型的功能。抽象mixin类广泛使用了这种模式。在下一节中进行讨论。

使用 mixin类复用模型功能

_inherit属性赋值为一个模型名列表,会继承这些模型的功能。这大多时候使用的是mixin类。

mixin类像是一些功能的容器,可供复用。它们实现通用功能,可添加至其它模型中。一般不直接单独使用。因此mixin类是基于models.AbstractModel的抽象模型,不像models.Model那样有实际数据表。

Odoo标准插件提供了一些有用的mixin。在代码中搜索models.AbstractModel可以找到它们。值的一提的,也可能最常用的是以下两个mixin,由讨论(Discuss:mail插件模型)应用提供:

聊天窗口和活动都是广泛使用的功能,在下一节中,我们会演示如何进行添加。

对模型添加消息聊天窗口和计划活动

我们来为图书会员模型添加消息聊天和活动mixin。操作步骤如下:

  1. 添加提供mixin的插件模型依赖,即mail
  2. 继承mail.threadmail.activity.mixin这两个mixin类
  3. 在表单视图中添加字段。

我们来详细操作以上步骤:

  1. 编辑__manifest__.py文件添加对mail插件的依赖:
  2. 编辑library_member/models/library_member.py 文件继承mixin类,添加如下高亮的代码:

    通过添加这行代码,我们的模型就会包含这些 mixin 的所有字段和方法。

    小贴士:本例中,mixin添加到了新创建的模型中。如果要将它们添加到在其它模块中创建的已有模型中,那么父模型也应出现在继承列表中,如:_inherit = [“library.member”, “mail.thread”, “mail.activity.mixin”]

  3. 最后要在图书会员表单视图中添加相关字段。编辑library_member/views/member_view.xml文件添加如下高亮代码:

可以看到,mail模块不仅提供了关注者、计划活动和消息的字段,还为它们提供了具体的网页客户端微件,这里都使用了。

升级模块后,图书会员视图应当如下所示:

图4.3:图书会员表单视图

注意mixin本身不会对访问权限包括记录规则造成任何修改,有内置的记录规则 ,限制每个用户访问的记录。举个例子,如果希望用户仅浏览关注的人的记录,必须明确添加这条记录规则。

mail.thread模型包含显示关注者Partner的字段,名为message_partner_ids。实现关注者访问规则需要添加一条记录规则 ,加上类似 [(‘message_partner_ids’, ‘in’, [user.partner_id.id])]条件的作用域表达式。

至此,我们学习了如何在模型和逻辑层扩展模块。下一步学习视图的继承来展示模型层的修改。

视图和数据继承

视图和其它数据组件也可通过模块继承来修改。就视图而言,通常是添加功能。视图的展示结构通过XML定义。对XML的继承,我们需要定位到所要继承的节点,然后声明在该处执行的操作,如插入XML元素。

其它的数据元素表现为数据库中写入的记录。继承模块对在其上写入,来修改一些值。

视图继承

视图使用XML定义,存储在结构字段arch中。继承视图,我们需要定位到所要继承的节点,然后声明所做的修改,如添加XML元素。

Odoo自带继承XML的简化标记,使用希望匹配的XML标签,如<field>,借由一个或多个独特属性进行匹配,如name。然后必须要添加position属性来声明修改的类型。

回到本章之前在isbn字段后添加内容的例子,可以使用如下代码:

string 属性外的任意 XML 元素和属性均可用于选取继承点使用的节点,字符串属性会在生成视图期间翻译成用户所使用的语言,因此不能作为节点选择器。

ℹ️在9.0以前,string 属性(显示标签文本)也可作为继承定位符。在9.0之后则不再允许。这一限制主要源自这些字符串的语言翻译机制。

使用position属性声明继承操作。可允许多种操作,如下:

将XML节点迁移到其它地方

除了attributes操作,上述定位符可与带position=”move”的子元素合并。效果是将子定位符目标节点移到父定位符的目标位置。

Odoo 12中的变化
position=”move”子定位符是 Odoo 12中新增的,之前的版本中没有。

下例为将my_field从当前位置移动到target_field之后。

其它视图类型,如列表和搜索视图,也有 arch 字段,可以表单视图同样的方式进行继承。

使用 XPath 选取继承点

有时可能没有带唯一值的属性来用作 XML 节点选择器。在所选元素没有 name 属性时可能出现这一情况,如<group><notebook><page>视图元素。另外就是有多个带有相同 name 属性的元素,比如在看板 QWeb 视图中相同字段可能在同一 XML 模板中被多次包含。

在这些情况下我们就需要更高级的方式来定位待扩展 XML 元素。定位 XML 中元素的一种自然方式是 XPath 表达式。

以上一章中定义的图书表单视图为例,定位<field name="isbn">元素的 XPath 表达式是//field[@name]=’isbn’。该表达式查找 name 属性等于 isbn<field>元素。

前一部分对图书表单视图继承的 XPath 写法是:

XPath 语法的更多知识请见 Python 官方文档

如果 XPath 表达式匹配到了多个元素,仅会选取第一个作为扩展目标。所以表达式应越精确越好,使用唯一属性。name 属性最易于确保找到精确元素作为扩展点。因此在创建视图 XML 元素时添加唯一标识符就非常重要。

修改已有数据

普通数据记录也可被继承,在实际应用,通常是重写已有值。这时我们只需定位到需写入的记录,以及更新的字段和值。无需使用XPath表达式,因为我们并不是像对视图那样修改XML arch结构。

<record id="x" model="y">数据加载元素执行对 y 模型的插入或更新操作:若不存在记录 x,则创建,否则被更新/覆盖。

其它模块中的记录可通过<module>.<identifier>全局标识符访问,因此一个模块可以更新其它模块创建的记录。

小贴士:点号(.)是保留符号,用于分隔模块名和对象标识符。所以在标识符名中不能使用点号,而应使用下划线(_) 字符。

举个例子,我们将 User 安全组的名称修改为 Librarian。对应修改library_app模块中创建的记录,使用的是library_app.library_group_user标识符。

添加library_member/security/library_security.xml并加入如下代码:

这里我们使用了一个<record>元素,仅写了 name 字段。可以认为这是对该字段的一次写操作。

小贴士:使用<record>元素时,可以选择要执行写操作的字段,但对简写元素则并非如此,如<menuitem><act_window>。它们需要提供所有的属性,漏写任何一个都会将对应字段置为空值。但可使用<record>为原本通过简写元素创建的字段设置值。

刻在声明文件data 中加入security/library_security.xml。然后更新模块即可看到用户组名称的修改。

继承视图让我们可以对后台展示层做出修改。但对前台网页也可做同样的操作。在下一节中进行讲解。

网页继承

可扩展性是Odoo框架的一个关键设计选择,Odoo的网页组件同样可进行继承。所以可对Odoo网页控制器和模板进行扩展。

第三章 Odoo 15开发之创建第一个 Odoo 应用中所创建的图书应用中,有一个图书目录页面,可进行改进。

我们会对其进行扩展来使用在图书会员模块中添加的图书可用性:

先来继承网页控制器。

继承网页控制器

网页控制器处理网页请求并渲染页面返回响应。应关注展示逻辑,不处理业务逻辑,业务逻辑在模型方法中处理。

支持参数或URL路由属于网页展示部分,适合用网页控制器处理。

这里会扩展/library/books端点来支持查询字符串参数available=1,稍后用于过滤图书目录来仅显示可借阅的图书。

要继承已有控制器,需导入创建它的原始对象,基于它声明一个Python类,然后实现包含新增逻辑的类方法,

library_member/controllers/main.py文件中添加继承控制器的代码如下:

按如下步骤添加控制器代码:

  1. 添加 library_member/controllers/main.py文件,确保其包含上面的代码。
  2. 在控制器子目录中添加library_member/__init__.py文件让新增的Python文件在模块中可导入:
  3. library_member/controllers/__init__.py中添加如下代码:
  4. 之后,访问http://localhost:8069/library/books?available=1,应该会只展示勾选了Is Available?字段的图书。

下面我们来回顾控制器扩展代码,理解其实现原理。

所要继承的控制器Books,最初在library_app模块的controllers/main.py文件中声明。因此需要导入odoo.addons.library_app.controllers.main来引用该文件。

这与模型不同,模型有一个中央仓库可以获取任意模型类的引用,如self.env[‘library.book’],无需知识具体实现它的文件。控制器没有这样的仓库,需要知道是哪个模块和文件实现了控制器,方可对其扩展。

然后基于原来的Books声明了一个BooksExtended类。类名不具关联性,仅是继承和扩展原类的一个载体。

再后我们(重)定义了一个待继承的控制器方法,本例为list()。它至少需要一个简单的@http.route()装饰器来保持路由为活跃状态。如果不带参数,将会保留父类中定义的路由。但也可以为@http.route() 装饰器添加参数,来重新定义或替换类路由。

list()方法带有**kwargs参数,捕获所有kwargs字典中的参数。这些是 URL 中给定的参数,如?available=1

小贴士**kwargs参数纳入所有可能无需使用的给定参数,但会让我们的URL可以兼容预期外的URL参数。如若选择指定具体参数,在设置了其它参数时,在调用相应控制器时会立刻失败,返回一条内部错误。

list()方法的代码一开始使用了 super()来调用相应父类方法。返回由父类方法计算的Response对象,包括待渲染的属性和模块,template,以及渲染时使用的上下文qcontext。但HTML尚待生成。仅在控制器完成运行时才生成HTML。因此在完成最终渲染前还可以修改Response属性。

该方法检测kwargsavailable键的非空值。如果找到,会过滤掉不可借阅图书,在记录集中更新qcontext。因此,在控制器处理完成时,HTML会使用更新后的图书记录进行渲染,仅包含可借阅图书。

继承 QWeb 模板

网页模板为XML文档,和其它Odoo视图类型一样可以使用选择器表达式,像我们在其实视图类型如表单中使用那样。QWeb模板通常更为复杂,因糨会包含更多的HTML元素,因此大多数据时候会使用更多样的XPath表达式。

要修改网页的实际展示,就需要继承所使用的 QWeb 模板。我们将继承library_app.book_list_template来展示更多有关不可借阅图书的信息。

QWeb继承是一个<template>元素,使用额外inherit_id属性来标识待继承的QWeb模板。本例中为library_app.book_list_template

执行如下步骤:

  1. 添加library_member/views/book_list_template.xml文件并加入如下代码:

    以下的示例使用了xpath标记。注意在本例我们也可以使用等效的简化标记,即<span t-field="book.publisher_id" position=after>
  2. 在插件声明文件(即library_member/__manifest__.py)中声明新增的数据文件:

此时访问http://localhost:8069/library/books应该会对不可借阅图书显示额外的视觉信息(not available)。网页长下面这样:

图4.4:包含可借阅信息的图书列表网页

至此完结了如何继承从数据模型至用户界面元素各种类型Odoo组件的回顾。

小结

扩展性是 Odoo 框架的一个重要功能。我们可以构建插件模块,对Odoo中需要在不同层实现功能的已有插件修改或添加功能。通过继承,我们的项目可以按整洁、模块化的方式复用和扩展第三方插件模块。

模型层中,我们使用_inherit模型属性来引用已有模型,然后在原处执行修改。模型内的字段对象还支持增量定义,这样可对已有字段重新声明,仅修改属性。

其它的模型继承机制允许我们复用数据结构和业务逻辑。代理继承通过多对一关联字段上的delegate=True属性(或老式的 inherits 模型属性),来让关联模型的所有字段可用,并复用它们的数据结构。原型继承使用_inherit属性加其它模型,来复制这些模型的功能(数据结构定义和方法),并可使用抽象 mixin 类,提供一系列像文档讨论消息和关注者的可复用功能。

视图层中,视图结构通过 XML 定义,(使用 XPath 或 Odoo 简化语法)定位 XML 元素来进行继承及添加 XML代码段。其它由模块创建的记录也可由继承模块修改,仅需引用对应的完整 XML ID 并在相应的字段上执行写操作。

业务逻辑层中,可使用模型继承相同的机制来进行继承,以及重新声明要继承的方法。在方法内,Python 的super()函数可用于调用所继承方法的代码,添加代码可在其之前或之后运行。

对于前端网页,控制器中的展示逻辑继承方式和模型方法相似,网页模板也是包含 XML 结构的视图,因此可以像其它视图类型一样的被继承。

下一章中,我们将更深入学习模型,探索模型提供给我们的所有功能。

扩展阅读

以下是官方文档的其它参考,可对模块扩展和继承机制的知识进行补充:

退出移动版