本文为最好用的免费ERP系统Odoo 12开发手册系列文章第四篇。
Odoo 的一个强大功能是无需直接修改底层对象就可以添加功能。这是通过其继承机制来实现的,采取在已有对象之上修改层来完成。这种修改可以在不同层上进行-模型层、视图层和业务逻辑层。我们创建新的模块来做出所需修改而无需在原有模块中直接修改。
上一篇文章中我们从零开始创建了一个新应用,本文中我们学习如何通过继承已有的核心应用或第三方模块来创建新的模块。实现以上本文将主要涵盖:
本文要求可通过命令行来启动 Odoo 服务。代码将在第三章 Odoo 12 开发之创建第一个 Odoo 应用的基础上进行修改。通过该文的学习现在我们已经有了library_app模块。本系列文章代码请参见 GitHub 仓库。
在第三章 Odoo 12 开发之创建第一个 Odoo 应用中我们创建了一个图书应用的初始模块,可供查看图书目录。现在我们要创建一个library_member模块,来对图书应用进行扩展以让图书会员可以借书。它继承 Book 模型,并添加一个图书是否可借的标记。该信息将在图书表单和图书目录页显示。
应添加图书会员主数据模型Member,类似 Partner 来存储个人数据,如姓名、地址和 email,还有一些特殊字段,如图书会员卡号。最有效的方案是代理继承,自动创建图书会员记录并包含关联 Partner 记录。该方案使得所有的Partner 字段在 Member 中可用,没有任何数据结构上的重复。
我们还要在借书表单中为会员提供消息和社交功能,包括计划活动组件来实现更好地协作。我们还要添加会员从图书馆中借书的功能,但暂不涉及。以下是当前所要修改内容的总结:
首先在library_app同级目录创建一个library_member目录来作为扩展模块,并在其中添加两个文件,一个__init__.py空文件和一个包含如下内容的__manifest__.py文件:
1 2 3 4 5 6 7 |
{ 'name': 'Library Members', 'description': 'Manage people who will be able to borrow books.', 'author': 'Alan Hou', 'depends': ['library_app'], 'application': False, } |
第一步我们来为Book模型添加is_available布尔型字段。这里使用经典的 in-place 模型继承。该字段值可通过图书借出和归还记录自动计算,但现在我们先使用普通字段。要继承已有模型,需要在 Python 类中添加一个_inherit 属性来标明所继承的模型。新类继承父 Odoo 模型的所有功能,仅需在其中声明要做的修改。在任何地方使用该模型修改都可用,可以认为这类继承是对已有模型的引用并在原处做了一些修改。
通过 Python 类来新建模型,继承模型同样是通过 Python 以及 Odoo 自有的继承机制,即_inherit 类属性。该属性标明所继承的模型。新的类继承父 Odoo 模型的所有功能,仅需声明要做修改的部分。编码指南推荐为每个模型创建一个 Python 文件,因此我们添加library_member/models/library_book.py文件来继承原模型,首先创建__init__.py文件来导入该文件:
1、添加library_member/__init__.py文件来导入 models 子文件夹
1 |
from . import models |
2、添加library_member/models/__init__.py文件子文件夹中的代码文件:
1 |
from . import library_book |
3、创建library_member/models/library_book.py文件来继承library.book模型:
1 2 3 4 5 |
from odoo import fields, models class Book(models.Model): _inherit = 'library.book' is_available = fields.Boolean('Is Available?') |
使用_inherit类属性来声明所继承模型。注意我们并没有使用到其它类属性,甚至是_name 也没使用。除非想要做出修改,否则不需要使用这些属性。
ℹ️_name是模型标识符,如果修改会发生什么呢?其实你可以修改,这时它会创建所继承模型的拷贝,成为一个新模型。这叫作原型继承,本文后面会讨论。
可以把这个想成是对模型定义的一个引用,在原处做了一个修改。可以添加字段、修改已有字段、修改模型类属性甚至是包含业务逻辑的方法。要在数据表中添加新的模型字段需要安装该模块。如果一切顺利,通过Settings > Technical > Database Structure > Models菜单查看library.book模型即可看到该字段。
1 |
~/odoo-dev/odoo/odoo-bin -d dev12 -i library_member |
通过上面部分可以看到向已有模型添加新字段非常简单。有时还要对已有字段进行修改,也非常简单。在继承模型时,可对已有字段叠加修改,也就是说仅需定义要增加或修改的字段属性。
我们将对原来创建的library_app模块的 Book模型做两处简单修改:
编辑library_member/models/library_book.py文件,并在library.book 模型中添加如下代码:
1 2 3 4 |
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这一修改不太容易发现,通过Settings > Technical > Database Structure > Models菜单下的字段定义中可进行查看。
模块中视图和其它数据构件也可通过继承来修改。就视图而言,通常需要添加功能。视图的展示结构在 arch 字段中使用 XML定义。这一 XML 数据可通过定位到所需修改的地方来进行继承,然后声明需执行的操作,如在该处添加 XML 元素。对于剩余的数据元素,它们代表写入数据库中的记录,继承模型可通过写操作来修改它们的值。
表单、列表和搜索视图通过arch 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 属性定位元素,此处为
我们添加views/book_view.xml文件来继承 Partner 视图,加入如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
以上代码中,我们高亮显示了继承相关的元素。inherit_id记录字段通过 ref 属性指向继承视图的外部标识符,我们将在第五章 Odoo 12开发之导入、导出以及模块数据讨论外部标识符详情。视图使用 XML 定义并存储在结构字段 arch 中。要继承一个视图,先定位要扩展的节点,然后执行要做的操作,如添加 XML 元素。
定位节点的最简单方法是使用唯一标识属性,通常是 name。然后添加定位属性,声明要做的修改。本例中继承节点是name=”isbn”元素,修改是在选定元素后加一段 XML:
1 2 3 |
|
除string 属性外的任意 XML 元素和属性可作为继承节点,字符串属性会被翻译成用户所使用的语言,因此不能作为节点选择器。
ℹ️在9.0以前,string 属性(显示标签文本)也可作为继承定位符。在9.0之后则不再允许。这一限制主要源自这些字符串的语言翻译机制。
一旦 XML 节点被选为继承点,需要指明要执行的继承操作。这通过 position 属性实现:
小贴士:通过position=”replace”可删除 XML 元素,但应避免这么做。这么做会破坏其它依赖所删除节点、将其作为占位符添加元素的模块。一个替代方案是,让该元素不可见。
除了attributes定位,上述定位符可与带position=”move”的子元素合并。效果是将子定位符目标节点移到父定位符目录位置。
ℹ️Odoo 12中的修改
position=”move”子定位符是 Odoo 12中新增的,之前的版本中没有
例如:
1 2 3 |
|
其它视图类型,如列表和搜索视图,也有 arch 字段,可以表单视图同样的方式被继承。
在声明文件data 中加入该视图文件并更新模块即可:
有时可能没有带唯一值的属性来用作 XML 节点选择器。在所选元素没有 name 属性时可能出现这一情况,如
在这些情况下我们就需要更高级的方式来定位待扩展 XML 元素。定位 XML 中元素的一种自然方式是 XPath 表达式。以上一篇文章中定义的 Book 表单视图为例,定位
前一部分对图书表单视图继承的 XPath 写法是:
1 2 3 |
|
XPath 语法的更多知识请见 Python 官方文档。
如果 XPath 表达式匹配到了多个元素,仅会选取第一个作为扩展目录。所以表达式应越精确越好,使用唯一属性。name 属性最易于确保找到精确元素作为扩展点,因此在创建视图 XML 元素时添加唯一标识符就非常重要。
普通数据记录不同于视图,它没有 XML arch 结构,也不能使用 XPath 来进行扩展。但还是可以通过替换字段值来进行修改。
ℹ️点号是保留符号,用于分隔模块名和对象标识符,所以在标识符名中不要使用点号,而应使用下划线字符。
举个例子,我们将 User 安全组的名称修改为 Librarian,对应修改library_app.library_group_user记录。添加library_member/security/library_security.xml并加入如下代码:
1 2 3 4 5 6 |
|
这里我们使用了一个
小贴士:使用
元素时,可以选择要执行写操作的字段,但对 shortcut 元素则并非如此,如
在声明文件data 中加入security/library_security.xml并更新模块即可看到效果。
前面我们介绍了模型的基本继承,在官方文档中称为经典继承。这是最常用的继承方式,最容易想到的就是in-place继承。获取模型并对其继承。添加的新功能会自动添加到已有模型中,而不会创建新模型。
可以为_inherit 属性传入多个值来继承多个父模型。大多数情况下这通过 mixin 类完成,mixin类是实现可复用的通用功能。也可以像普通模型那样独立使用,像是一个功能容器,可随时加到其它模型中。
如在使用_inherit 属性的同时还使用了与父模型不同的_name属性,此时会复用所继承并创建一个新的模型,并带有自己的数据表和数据。官方文档称其为原型(prototype)继承。下面我们会拿一个模型,并为其创建一个拷贝。在添加新功能时,只会被加到新模型中,而不会变更原模型。
此外还有代理(delegation)继承,通过_inherits 属性来使用(注意最后有一个 s)。这允许我们创建一个包含和继承已有模型的新模型。新模型创建新记录时,在原模型中也会被创建并使用many-to-one 字段关联。查看新模型的人可以看到所有原模型和新模型中的字段,但在后台两个模型分别处理各自的数据。
下面我们一起来了解详情。
前文我们继承模型时使用了_inherit 属性,创建一个类继承library.book 并添加了一些功能。类中没有使用_name属性,不指明即使用library.book。如果设置了不个不同值的_name 属性,会通过从所继承的模型拷贝功能创建新模型。
在实际开发中,这类继承一般通过抽象 mixin 类,很少这样直接继承普通模型,因为这样会创建冗余的数据结构。Odoo 还有一种代理继承机制可避免这类数据结构冗余,所以普通模型通常会使用这种方法来做继承。
使用代理继承无需复制数据即可在数据库中复用数据结构,这通过将一个模型嵌入另一个来实现。UML 中这种称作组合(composition)关系:父类无需子类即可存在,而子类必须要有父类才能存在。
比如,对于内核 User模型,每条记录包含一条 Partner 记录,因此包含 Partner 中的所有字段以及User自身的一些字段。
在图书项目中,我们要添加一个图书会员模型。会员有会员卡并通过会员卡借阅读书。我们要记录卡号,还要存储email 和地址这类个人信息。Partner 模型已包含联系和地址信息,所以最好是进行复用,而不去创建重复的数据结构。
为会员模型创建library_member/models/library_member.py文件并加入如下代码:
1 2 3 4 5 6 7 8 9 10 11 |
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字段引用。
ℹ️Odoo 8中的修改
在新的 API 中引入了delegate=True字段属性。在那之前,代理继承通过模型属性来定义,类似_inherits = {‘res.partner’: ‘partner_id’}。现在仍支持这一写法,官网中还有相应介绍,但delegate=True 字段属性可起到相同效果且使用更简单。
透过代理机制,嵌套模型的所有字段就像父模型字段一样自动可用。本例中,会员卡模型可使用 Partner 中的所有字段,如 name, address和 email,以及会员自身的独有字段,如card_number。在后台中,Partner 字段存储在关联的 Partner 记录,没有重复的数据结构。
ℹ️对于模型方法则并非如此,Partner 模型中的方法在 Member 模型中不可使用。
与原型继承相比,代理继承的好处在于无需跨表重复像地址这样的数据。任何需包含地址的新模型通过代理嵌入了 Partner 模型。如果在 Partner 中修改 address字段,在所有嵌入的模型中可以马上使用。
小贴士:代理继承可通过如下组合来进行替代:
- 父模型中的一个 many-to-one 字段
- 重载 create()方法自动创建并设置父级记录
- 父字段中希望暴露的特定字段的关联字段
有时这比完整的代理继承更为合适。例如res.company并没有继承res.partner,但使用到了其中好几个字段。
不要忘记在library_member/model/__init__.py文件中加入:
1 2 |
from . import library_book from . import library_member |
要使用我们创建的 Member 模型,还要完成以下步骤:
读者可以先尝试自己添加,再来看下面的详细步骤:
要创建安全ACL,创建library_member/security/ir.model.access.csv文件并加入如下代码:
1 2 3 |
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/views/library_menu.xml文件并加入如下代码:
1 2 3 4 5 6 7 8 9 |
name="Library Members" res_model="library.member" view_mode="tree,form" /> |
要添加视图,创建library_member/views/member_view.xml文件并加入如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
最后,编辑manifest文件来声明这三个新文件:
1 2 3 4 5 6 |
'data':[ ... 'security/ir.model.access.csv', 'views/library_menu.xml', 'views/member_view.xml', ] |
如果编写正确,在进行模型更新后即可使用新的图书会员模型了。
1 |
~/odoo-dev/odoo/odoo-bin -d dev12 -u library_member |
原型继承主要用于支持 mixin 类。mixin 是基于 models.Abstract 的抽象的模型(而不是models.Model),它在数据库中没有实际的体现,而是提供功能供其它模型复用(混合 mixed in)。Odoo 插件提供多种 mixin,最常的两种由 Discuss 应用(mail 模块)提供:
ℹ️Odoo 11中的修改
mail 模块现在通过mail.activity.mixin抽象模型提供Activities任务管理功能。该功能在 Odoo 11中才添加,此前的版本中没有。
我们一起来为 Member 模型添加上述两种 mixin。社交消息功能由 mail 模块的mail.thread模型提供,要将其加入自定义模型,应进行如下操作:
对于第一步扩展模型需要在__manifest__.py文件中添加对 mail 的依赖。
1 |
'depends': ['library_app', 'mail'], |
第二步中对 mixin 类的继承通过_inherit属性完成,应编辑library_member/models/library_member.py并添加如下代码:
1 2 3 4 5 |
class Member(models.Model): _name = 'library.member' _description = 'Library Member' _inherit = ['mail.thread', 'mail.activity.mixin'] ... |
通过添加额外的这行代码,我们的模型就会包含这些 mixin 的所有字段和方法。
第三步中向表单视图添加相关字段,编辑library_member/views/member_view.xml文件并在表单最后添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 |
...
|
mail 模块还为这些字段提供了一些特定的网页组件,以上代码中已使用到。在升级模块后会员表单将变成这样:
有时普通用户仅能访问正在 follow 的记录。在这些情况下我们应添加访问记录规则来让用户可以看到 follow 的记录。本例中用不到这一功能,但可通过[(‘message_partner_ids’, ‘in’, [user.partner_id.id])]或来进行添加。
Python 方法中编写的业务逻辑也可以被继承。Odoo 借用了 Python 已有的父类行为的对象继承机制。
作为一个实际的例子,我们将继承图书 ISBN 验证逻辑。在图书应用中仅能验证13位的 ISBN,但老一些的图书可能只有10位数的 ISBN。我们将继承_check_isbn()方法来完成这种情况的验证。在library_member/models/library_book.py文件中添加如下方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
from odoo import api, fields, models class Book(models.Model): ... @api.multi 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() |
要继承方法,我们要重新定义该方法,可以使用 super()来调用已实现的部分。在这个方法中我们验证是否为10位数 ISBN,然后插入遗失的验证逻辑。若不是10位,则进入原有的13位验证逻辑。
如果想要进行测试甚至是书写测试用例,可使用0-571-05686-5作为例子,该书是威廉·戈尔丁的《蝇王》。
ℹ️Odoo 11中的修改
从 Odoo 11开始,支持的主Python版本为 Python 3(Odoo 12中为 Python 3.5)。而此前的 Odoo 版本使用 Python 2,其中 super()需传入类名和 self 两个参数,那么,上例中的代码应修改为super(Book, self)._check_isbn()。
Odoo 中的所有功能都带有扩展性,web 功能也不例外,所以已有控制器和模块都能被继承。
作为示例,我们将继承图书目录网页,加入前面添加的图书可用性信息:
网页控制器不应包含实际业务逻辑,仅集中于展示逻辑。我们可能会需要添加对额外 URL 参数甚至是路由的支持,来改变网页的展示。我们将扩展/library/books来支持available=1参数,以过滤出可借阅图书。
要继承已有控制器,需导入对应对象,然后用方法新增逻辑来进行实现。下面新增ibrary_member/controllers/main.py文件并加入如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 |
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 |
我们要继承的Books控制器在library_app/controllers/main.py中定义。因此需要通过odoo.addons.library_app.controllers.main导入。这和模型不同,模型可以通过 env 对象中的central registry 来引用任意模型类,而无需了解实现它的文件。控制器没有这个,我们需要知道实现需继承控制器的模块和文件。
然后基于Books声明了一个BooksExtended类,类名不具关联性,仅用于继承和扩展原类中定义的方法。
再后我们(重)定义了一个控制器方法 list()。它至少需要一个简单的@http.route()装饰器来保持路径活跃。如果不带参数,将会保留父类中定义的路由。但也可以为@http.route() 装饰器添加参数,来重新定义或替换类路由。
在继承的 list()方法中,一开始使用了 super()来运行已有代码。处理结果返回一个 Response 对象,Response 带有模块要渲染的属性 template,以及渲染使用的上下文qcontext。但还需要生成 HTML,仅会在控制器结束运行时生成。这也让我们可以在最终渲染完成之前可以修改 Response 属性。
list()方法带有**kwargs参数,捕获所有kwargs字典中的参数。这些是 URL 中的参数,如?available=1。方法检测kwargs中available键的值,检测到后改变qcontext来获取仅为可借阅图书的图书记录集。
还要记得让模块知道这个新 Python 文件,需通过将 controllers 子文件夹中添加到library_member/__init__.py中:
1 2 |
from . import models from . import controllers |
在library_member/controllers/__init__.py文件中添加一行代码:
1 |
from . import main |
然后更新模板并访问http://
要修改网页的实际展示,就需要继承所使用的 QWeb 模板。我们将继承library_app.book_list_template来展示更多有关不可借阅图书的信息。添加library_member/views/book_list_template.xml文件并加入如下代码:
1 2 3 4 5 6 7 8 9 10 11 |
name="Extended Book List" inherit_id="library_app.book_list_template"> (Not Available) |
网页模板像其它 Odoo 视图类型一样是 XML 文件,同样也可以使用 XPath 来定位元素并对它们进行操作。所继承模型通过在元素中的inherit_id来指明。
小贴士:在前例中使用了灵活性很强的 XPath 标记,但这里也可以使用等价的简化标记:
然后在 library_member/__manifest__.py文件中加入该文件的声明:
1 2 3 4 |
'data':[ ... 'views/book_list_template.xml', ] |
然后访问http://
扩展性是 Odoo 框架的一个重要功能。我们可以创建插件来为需要实现功能的多个层的已有插件修改或添加功能。
模型层中,我们使用_inherit模型属性来引用已有模型,然后在原处执行修改。模型内的字段对象还支持叠加定义,这样可对已有字段重新声明,仅修改属性。
其它的模型继承机制允许我们利用数据结构和业务逻辑。代理继承通过多对一关联字段上的delegate=True属性(或老式的 inherits 模型属性),来让所有关联模块的所有字段可用,并复用它们的数据结构。原型继承使用_inherit属性,来复制其它模型的功能(数据结构定义和方法),并启用抽象 mixin 类,提供一系列像文档讨论消息和 follower 的可复用功能。
视图层中,视图结构通过 XML 定义,(使用 XPath 或 Odoo 简化语法)定位 XML 元素来进行继承及添加 XML 片断。其它由模块创建的记录已可由继承模块修改,仅需引用 对应的完整 XML ID 并在设想的字段上执行写操作。
业务逻辑层中,可使用模型继承相同的机制来进行继承,以及重新声明要继承的方法。在方法内,Python 的super()函数可用于调用所继承方法的代码,添加代码可在其之前或之后运行。
对于前端网页,控制器中的展示逻辑继承方式和模型方法相似,网页模板也是包含 XML 结构的视图,因此可以像其它视图类型一样的被继承。
下一篇文章中,我们将更深入学习模型,探索模型提供给我们的所有功能。
☞☞☞第五章 Odoo 12开发之导入、导出以及模块数据