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
文件:
{
'name': 'Library Members',
'description': 'Manage people who will be able to borrow books.',
'author': 'Alan Hou',
'depends': ['library_app'],
'application': False,
}
接着我们就可以开发功能了。第一个任务是常用的简单需求:对已有模型新增字段。这正好是介绍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 子文件夹
from . import models
2、添加library_member/models/__init__.py
文件,导入models子文件夹中中的文件:
from . import library_book
3、创建library_member/models/library_book.py
文件来继承library.book模型:
from odoo import fields, models
class Book(models.Model):
_inherit = 'library.book'
is_available = fields.Boolean('Is Available?')
此处我们使用了_inherit类属性来声明所继承模型。注意我们并没有使用到其它类属性,连_name 也没使用。除非想要做出修改,否则不需要使用这些属性。
小贴士:_name是模型标识符,如果修改会发生什么呢?其实你可以修改,这时它会创建所继承模型的拷贝,成为一个新模型。这叫作原型继承,本文后面通过原型拷贝模型一节会讨论。
可以把它看成是引用了中央仓库中的一个模型定义,然后在其内进行修改。修改包含添加字段、修改已有字段、修改模型类属性或添加带有新业务逻辑的方法。
要在数据表中添加新增模型字段,必须要先安装插件模块。如果一切顺利的话,就可以通过Technical > Database Structure > Models菜单查看到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 属性定位元素。此处为
。
我们添加XML文件,即views/book_view.xml来继承 Partner 视图,内容如下:
Book: add Is Available? field
library.book
以上代码中,我们高亮显示了继承相关的元素。inherit_id记录字段通过 ref 属性指向视图的外部标识符定位所继承的视图。
arch包含所声明扩展点处使用的元素,一个带有name="isbn"的
元素,同时包含position="after"来声明位置。在扩展元素内,使用XML来添加is_available字段。
创建完继承之后图书表单(在声明文件中添加该视图文件并升级插件)如下图:
图4.1:添加了Is Available?字段后的图书表单
我们学习了继承的基础知识,对模型层和视图层新增了一个字段。接下来,我们将学习我们所使用的模型继承方法,即经典继承。
可以把经典模型继承看作是一个插入(in-place)扩展。在声明了具有_inherit属性的Python类时,它获取到了对相应模型定义的引用,然后对其添加扩展。模型定义存储在Odoo模型仓库中,我们可对其做进一步的修改。
下面我们学习如何在常用的继承用例中使用经典继承:修改已有字段的属性并扩展Python方法来添加或变更业务逻辑。
继承模型时,可对已有字段做出增量修改。也就是只需要定义要修改或添加的属性。
我们对library_app模块中所创建的Book模型做两处修改:
编辑library_member/models/library_book.py文件,并在library.book 模型中添加如下代码:
class Book(models.Model):
...
isbn = fields.Char(help="Use a valid ISBN-13 or ISBN-10.")
publisher_id = fields.Many2one(index=True)
这会对字段的指定属性作出修改,未指定的属性保持不变。
升级模块,进入图书表单,将鼠标悬停在 ISBN 字段上,就可以看到所添加的提示信息了。index=True这一修改的效果不太容易发现,通过开发者工具菜单的View Fields选项或Settings > Technical > Database Structure > Models菜单下的字段定义中可进行查看。
图4.2:出版社字段启用了索引
Python 方法中编写的业务逻辑也可以被继承。Odoo 借用了 Python 已有的父类行为的对象继承机制。
举个实际的例子,我们将扩展图书 ISBN 的验证逻辑。在图书应用中仅能验证现代的13位ISBN,但老一些的图书可能只有10位数的 ISBN。我们继承_check_isbn()方法来完成这种情况的验证。
在library_member/models/library_book.py文件中添加如下代码:
from odoo import api, fields, models
class Book(models.Model):
...
def _check_isbn(self):
self.ensure_one()
isbn = self.isbn.replace('-', '')
digits = [int(x) for x in isbn if x.isdigit()]
if len(digits) == 10:
ponderators = [1, 2, 3, 4, 5, 6, 7, 8, 9]
total = sum(a * b for a, b in zip(digits[:9], ponderators))
check = total % 11
return digits[-1] == check
else:
return super()._check_isbn()
在继承类中继承方法,我们要使用相同方法名重新定义该方法,本例中即为_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字段:
from . import library_book
from . import library_member
from odoo import fields, models
class Member(models.Model):
_name = 'library.member'
_description = 'Library Member'
card_number = fields.Char()
partner_id = fields.Many2one(
'res.partner',
delegate=True,
ondelete='cascade',
required=True)
通过代理继承,library.member 中嵌入了所继承的模型:res.partner,因此在新建会员记录时,会自动创建一个关联的 Partner并通过partner_id字段引用。
透过代理机制,嵌套模型的所有字段像父模型字段一样自动可用。本例中,会员模型可使用 Partner 中的所有字段,如 name, address和 email,以及会员自身的独有字段,如card_number。底层Partner 字段存储于关联的 Partner 记录中,没有重复的数据结构。
代理继承仅用在数据层面,不适用于逻辑层。没有继承所继承模型的任意方法。但仍可使用点号运算符来访问,也称为点号标记,用于访问对象属性。例如,会员模型中partner_id.open_parent() 运行嵌套Partner记录的open_parent()方法。
代理继承还有一种替代语法,使用_inherits模型属性。这来自Odoo 8之前的老API,但仍在广泛使用。和上述代码相同效果的图书模型代码如下:
from odoo import fields, models
class Member(models.Model):
_name = "library.member"
_description = "Library Member"
_inherits = {"res.partner": "partner_id"}
card_number = fields.Char()
partner_id = fields.Many2one(
"res.partner",
ondelete="cascade",
required=True)
完成新模型的添加,还需要完成几步:添加权限ACL、菜单和一些视图。
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_member_user,Member User Access,model_library_member,library_app.library_group_user,1,1,1,0
access_member_manager,Member Manager Access,model_library_member,library_app.library_group_manager,1,1,1,1
Library Member Form View
library.member
Library Member List View
library.member
"data": [
"security/ir.model.access.csv",
"views/book_view.xml",
"views/member_view.xml",
"views/library_menu.xml"
],
如果代码编写正确,升级模块后即可使用新的图书会员模型了。
经典继承使用_inherit属性扩展模型。因其未修改_name属性,可对该模型执行有效的原地变更。
如果使用_inherit的同时修改了_name属性,就会获得一具所继承模型的副本。这时新模型就会获得仅针对其自身的功能,不会添加到父模型中。副本模型与父模型相独立,不受父模型修改的影响,它有自有的数据表和数据。官方文档将这种继承称为原型继承。
在实际开发中,使用_inherit进行模型拷贝没太大用处。一般会更偏好代理继承,因为不拷贝就可复用数据结构。
在对多个父模型继承时,_inherit的值就不单个名称,而是一个模型名列表。
这可用于将多个模型混合加入一个模型。这样我们多次使用同一模型的功能。抽象mixin类广泛使用了这种模式。在下一节中进行讨论。
将_inherit属性赋值为一个模型名列表,会继承这些模型的功能。这大多时候使用的是mixin类。
mixin类像是一些功能的容器,可供复用。它们实现通用功能,可添加至其它模型中。一般不直接单独使用。因此mixin类是基于models.AbstractModel的抽象模型,不像models.Model那样有实际数据表。
Odoo标准插件提供了一些有用的mixin。在代码中搜索models.AbstractModel可以找到它们。值的一提的,也可能最常用的是以下两个mixin,由讨论(Discuss:mail插件模型)应用提供:
Odoo 11中的变化
activities mixin是在Odoo 11中引入的功能,在更早版本中无法使用。
聊天窗口和活动都是广泛使用的功能,在下一节中,我们会演示如何进行添加。
我们来为图书会员模型添加消息聊天和活动mixin。操作步骤如下:
我们来详细操作以上步骤:
__manifest__.py
文件添加对mail插件的依赖:"depends": ["library_app", "mail"],
class Member(models.Model):
_name = 'library.member'
_description = 'Library Member'
_inherit = ["mail.thread", "mail.activity.mixin"]
通过添加这行代码,我们的模型就会包含这些 mixin 的所有字段和方法。
小贴士:本例中,mixin添加到了新创建的模型中。如果要将它们添加到在其它模块中创建的已有模型中,那么父模型也应出现在继承列表中,如:_inherit = ["library.member", "mail.thread", "mail.activity.mixin"]。
Library Member Form View
library.member
可以看到,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标签,如
,借由一个或多个独特属性进行匹配,如name。然后必须要添加position属性来声明修改的类型。
回到本章之前在isbn字段后添加内容的例子,可以使用如下代码:
除string 属性外的任意 XML 元素和属性均可用于选取继承点使用的节点,字符串属性会在生成视图期间翻译成用户所使用的语言,因此不能作为节点选择器。
ℹ️在9.0以前,string 属性(显示标签文本)也可作为继承定位符。在9.0之后则不再允许。这一限制主要源自这些字符串的语言翻译机制。
使用position属性声明继承操作。可允许多种操作,如下:
或
一类的容器$0
。value
元素。如True
。若不带内容体,如
,则会从所选元素中删除属性。小贴士:虽然position="replace"可删除 XML 元素,但应避免这么做。这么做会其它其它依赖使用所删除节点作为扩展点插件元素产生崩溃。一个替代方案是,保留元素让其不可见。
除了attributes操作,上述定位符可与带position="move"的子元素合并。效果是将子定位符目标节点移到父定位符的目标位置。
Odoo 12中的变化
position="move"子定位符是 Odoo 12中新增的,之前的版本中没有。
下例为将my_field从当前位置移动到target_field之后。
其它视图类型,如列表和搜索视图,也有 arch 字段,可以表单视图同样的方式进行继承。
有时可能没有带唯一值的属性来用作 XML 节点选择器。在所选元素没有 name 属性时可能出现这一情况,如
、
或
视图元素。另外就是有多个带有相同 name 属性的元素,比如在看板 QWeb 视图中相同字段可能在同一 XML 模板中被多次包含。
在这些情况下我们就需要更高级的方式来定位待扩展 XML 元素。定位 XML 中元素的一种自然方式是 XPath 表达式。
以上一章中定义的图书表单视图为例,定位
元素的 XPath 表达式是//field[@name]='isbn'。该表达式查找 name 属性等于 isbn 的
元素。
前一部分对图书表单视图继承的 XPath 写法是:
XPath 语法的更多知识请见 Python 官方文档。
如果 XPath 表达式匹配到了多个元素,仅会选取第一个作为扩展目标。所以表达式应越精确越好,使用唯一属性。name 属性最易于确保找到精确元素作为扩展点。因此在创建视图 XML 元素时添加唯一标识符就非常重要。
普通数据记录也可被继承,在实际应用,通常是重写已有值。这时我们只需定位到需写入的记录,以及更新的字段和值。无需使用XPath表达式,因为我们并不是像对视图那样修改XML arch结构。
数据加载元素执行对 y 模型的插入或更新操作:若不存在记录 x,则创建,否则被更新/覆盖。
其它模块中的记录可通过
全局标识符访问,因此一个模块可以更新其它模块创建的记录。
小贴士:点号(.)是保留符号,用于分隔模块名和对象标识符。所以在标识符名中不能使用点号,而应使用下划线(_) 字符。
举个例子,我们将 User 安全组的名称修改为 Librarian。对应修改library_app模块中创建的记录,使用的是library_app.library_group_user标识符。
添加library_member/security/library_security.xml并加入如下代码:
Librarian
这里我们使用了一个
元素,仅写了 name 字段。可以认为这是对该字段的一次写操作。
小贴士:使用
元素时,可以选择要执行写操作的字段,但对简写元素则并非如此,如
和
。它们需要提供所有的属性,漏写任何一个都会将对应字段置为空值。但可使用
为原本通过简写元素创建的字段设置值。
刻在声明文件data 中加入security/library_security.xml。然后更新模块即可看到用户组名称的修改。
继承视图让我们可以对后台展示层做出修改。但对前台网页也可做同样的操作。在下一节中进行讲解。
可扩展性是Odoo框架的一个关键设计选择,Odoo的网页组件同样可进行继承。所以可对Odoo网页控制器和模板进行扩展。
第三章 Odoo 15开发之创建第一个 Odoo 应用中所创建的图书应用中,有一个图书目录页面,可进行改进。
我们会对其进行扩展来使用在图书会员模块中添加的图书可用性:
先来继承网页控制器。
网页控制器处理网页请求并渲染页面返回响应。应关注展示逻辑,不处理业务逻辑,业务逻辑在模型方法中处理。
支持参数或URL路由属于网页展示部分,适合用网页控制器处理。
这里会扩展/library/books端点来支持查询字符串参数available=1,稍后用于过滤图书目录来仅显示可借阅的图书。
要继承已有控制器,需导入创建它的原始对象,基于它声明一个Python类,然后实现包含新增逻辑的类方法,
在library_member/controllers/main.py文件中添加继承控制器的代码如下:
from odoo import http
from odoo.addons.library_app.controllers.main import Books
class BookExtended(Books):
@http.route()
def list(self, **kwargs):
response = super().list(**kwargs)
if kwargs.get('available'):
Book = http.request.env['library.book']
books = Book.search([('is_available', '=', True)])
response.qcontext['books'] = books
return response
按如下步骤添加控制器代码:
from . import models
from . import controllers
from . import main
下面我们来回顾控制器扩展代码,理解其实现原理。
所要继承的控制器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属性。
该方法检测kwargs中available键的非空值。如果找到,会过滤掉不可借阅图书,在记录集中更新qcontext。因此,在控制器处理完成时,HTML会使用更新后的图书记录进行渲染,仅包含可借阅图书。
网页模板为XML文档,和其它Odoo视图类型一样可以使用选择器表达式,像我们在其实视图类型如表单中使用那样。QWeb模板通常更为复杂,因糨会包含更多的HTML元素,因此大多数据时候会使用更多样的XPath表达式。
要修改网页的实际展示,就需要继承所使用的 QWeb 模板。我们将继承library_app.book_list_template来展示更多有关不可借阅图书的信息。
QWeb继承是一个
元素,使用额外inherit_id属性来标识待继承的QWeb模板。本例中为library_app.book_list_template。
执行如下步骤:
(Not Available)
以下的示例使用了xpath标记。注意在本例我们也可以使用等效的简化标记,即
"data": [
"security/library_security.xml",
"security/ir.model.access.csv",
"views/book_view.xml",
"views/member_view.xml",
"views/library_menu.xml",
"views/book_list_template.xml",
],
此时访问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 结构的视图,因此可以像其它视图类型一样的被继承。
下一章中,我们将更深入学习模型,探索模型提供给我们的所有功能。
以下是官方文档的其它参考,可对模块扩展和继承机制的知识进行补充: