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

本文为最好用的免费ERP系统Odoo 12开发手册系列文章第四篇。

Odoo 的一个强大功能是无需直接修改底层对象就可以添加功能。这是通过其继承机制来实现的,采取在已有对象之上修改层来完成。这种修改可以在不同层上进行-模型层、视图层和业务逻辑层。我们创建新的模块来做出所需修改而无需在原有模块中直接修改。

上一篇文章中我们从零开始创建了一个新应用,本文中我们学习如何通过继承已有的核心应用或第三方模块来创建新的模块。实现以上本文将主要涵盖:

  • 原模型扩展,为已有模型添加功能
  • 修改数据记录来继承视图,添加功能或修改数据来修改其它模块创建的数据记录
  • 其它模型继承机制,如代理继承和 mixin 类
  • 继承 Python 方法来为应用业务逻辑添加功能
  • 继承 Web 控制器和模板来为网页添加功能

开发准备

本文要求可通过命令行来启动 Odoo 服务。代码将在第三章 Odoo 12 开发之创建第一个 Odoo 应用的基础上进行修改。通过该文的学习现在我们已经有了library_app模块。本系列文章代码请参见 GitHub 仓库

学习项目-继承图书馆应用

在第三章 Odoo 12 开发之创建第一个 Odoo 应用中我们创建了一个图书应用的初始模块,可供查看图书目录。现在我们要创建一个library_member模块,来对图书应用进行扩展以让图书会员可以借书。它继承 Book 模型,并添加一个图书是否可借的标记。该信息将在图书表单和图书目录页显示。

应添加图书会员主数据模型Member,类似 Partner 来存储个人数据,如姓名、地址和 email,还有一些特殊字段,如图书会员卡号。最有效的方案是代理继承,自动创建图书会员记录并包含关联 Partner 记录。该方案使得所有的Partner 字段在 Member 中可用,没有任何数据结构上的重复。

我们还要在借书表单中为会员提供消息和社交功能,包括计划活动组件来实现更好地协作。我们还要添加会员从图书馆中借书的功能,但暂不涉及。以下是当前所要修改内容的总结:

  • 图书

    • 添加一个Is Available? 字段。现在通过手动管理,以后会自动化
    • 扩展 ISBN 验证逻辑来同时支持10位数的ISBN
    • 扩展图书目录页来分辨不可借阅图书并允许用户过滤出可借图书
  • 会员

    • 添加一个新模型来存储姓名、卡号和 Email、地址一类的联系信息
    • 添加社交讨论和计划活动功能

首先在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

Odoo 12图书项目is_available字段添加

修改已有字段

通过上面部分可以看到向已有模型添加新字段非常简单。有时还要对已有字段进行修改,也非常简单。在继承模型时,可对已有字段叠加修改,也就是说仅需定义要增加或修改的字段属性。

我们将对原来创建的library_app模块的 Book模型做两处简单修改:

  • 为isbn字段添加一条提示,说明同时支持10位数的 ISBN(稍后会实现该功能)
  • 为publisher_id字段添加数据库索引,以提升搜索效率

编辑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菜单下的字段定义中可进行查看。

Odoo 12图书项目 ISBN 提示

修改视图和数据

模块中视图和其它数据构件也可通过继承来修改。就视图而言,通常需要添加功能。视图的展示结构在 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
<?xml version="1.0"?>
<odoo>
<record id="view_form_book_extend" model="ir.ui.view">
<field name="name">Book: add Is Available? field</field>
<field name="model">library.book</field>
<field name="inherit_id" ref="library_app.view_form_book" />
<field name="arch" type="xml">
<field name="isbn" position="after">
<field name="is_available" />
</field>
</field>
</record>
</odoo>

以上代码中,我们高亮显示了继承相关的元素。inherit_id记录字段通过 ref 属性指向继承视图的外部标识符,我们将在第五章 Odoo 12开发之导入、导出以及模块数据讨论外部标识符详情。视图使用 XML 定义并存储在结构字段 arch 中。要继承一个视图,先定位要扩展的节点,然后执行要做的操作,如添加 XML 元素。

定位节点的最简单方法是使用唯一标识属性,通常是 name。然后添加定位属性,声明要做的修改。本例中继承节点是name=”isbn”元素,修改是在选定元素后加一段 XML:

1
2
3
<field name="isbn" position="after">
<!-- 此处添加修改内容 -->
</field>

除string 属性外的任意 XML 元素和属性可作为继承节点,字符串属性会被翻译成用户所使用的语言,因此不能作为节点选择器。

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

一旦 XML 节点被选为继承点,需要指明要执行的继承操作。这通过 position 属性实现:

  • inside(默认值):在所选节点内添加内容,这一节点应是一类的容器
  • after:在选定节点之后向父节点添加内容
  • before:在选定节点之前向父节点添加内容
  • replace:替换所选节点。若使用空元素则会删除该元素。Odoo 之后还允许使用其它标记来包裹元素,通过在内容中使用$0来表示被替换的元素。
  • attributes:修改匹配元素属性值。内容中应包含带有一个或多个<attribute **name=”attr-name”** >value元素。如True,若不带内容,如则 attribute 会从所选元素中删除。

小贴士: 通过position=”replace”可删除 XML 元素,但应避免这么做。这么做会破坏其它依赖所删除节点、将其作为占位符添加元素的模块。一个替代方案是,让该元素不可见。

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

ℹ️Odoo 12中的修改
position=”move”子定位符是 Odoo 12中新增的,之前的版本中没有

例如:

1
2
3
<field name="target_field" position="after">
<field name="my_field" position="move"/>
</field>

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

在声明文件data 中加入该视图文件并更新模块即可:

Odoo 12图书项目添加 is_available

使用 XPath 选取继承点

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

在这些情况下我们就需要更高级的方式来定位待扩展 XML 元素。定位 XML 中元素的一种自然方式是 XPath 表达式。以上一篇文章中定义的 Book 表单视图为例,定位元素的 XPath 表达式是//field[@name]=’isbn’。该表达式查找 name 属性等于 isbn 的元素。

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

1
2
3
<xpath expr="//field[@name='isbn']" position="after">
<field name="is_available" />
</xpath>

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

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

修改数据

普通数据记录不同于视图,它没有 XML arch 结构,也不能使用 XPath 来进行扩展。但还是可以通过替换字段值来进行修改。

数据加载元素实际是对 y 模型进行插入或更新操作。若不存在记录 x,则被创建,否则被更新/覆盖。其它模块中的记录可通过.全局标识符访问,因此可以在我们的模块中重写其它模块中已写入的数据。

ℹ️点号是保留符号,用于分隔模块名和对象标识符,所以在标识符名中不要使用点号,而应使用下划线字符。

举个例子,我们将 User 安全组的名称修改为 Librarian,对应修改library_app.library_group_user记录。添加library_member/security/library_security.xml并加入如下代码:

1
2
3
4
5
6
<odoo>
<!-- Modify Group name -->
<record id="library_app.library_group_user" model="res.groups">
<field name="name">Librarian</field>
</record>
</odoo>

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

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

在声明文件data 中加入security/library_security.xml并更新模块即可看到效果。

Odoo 12图书项目 Librarian

其它模型继承机制

前面我们介绍了模型的基本继承,在官方文档中称为经典继承。这是最常用的继承方式,最容易想到的就是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)
  • 添加菜单项
  • 添加表单和列表视图
  • 更新manifest文件来声明这些新增数据文件

读者可以先尝试自己添加,再来看下面的详细步骤:

要创建安全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
<odoo>
<act_window id="action_library_member"
name="Library Members"
res_model="library.member"
view_mode="tree,form" />
<menuitem id="menu_library_member"
action="action_library_member"
parent="library_app.menu_library" />
</odoo>

要添加视图,创建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
<?xml version="1.0" ?>
<odoo>
<record id="view_form_member" model="ir.ui.view">
<field name="name">Library Member Form View</field>
<field name="model">library.member</field>
<field name="arch" type="xml">
<form>
<group>
<field name="name" />
<field name="email" />
<field name="card_number" />
</group>
</form>
</field>
</record>
<record id="view_tree_member" model="ir.ui.view">
<field name="name">Library Member List View</field>
<field name="model">library.member</field>
<field name="arch" type="xml">
<tree>
<field name="name" />
<field name="card_number" />
</tree>
</field>
</record>
</odoo>

最后,编辑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

Odoo 12图书项目图书会员

使用 mixin类继承模型

原型继承主要用于支持 mixin 类。mixin 是基于 models.Abstract 的抽象的模型(而不是models.Model),它在数据库中没有实际的体现,而是提供功能供其它模型复用(混合 mixed in)。Odoo 插件提供多种 mixin,最常的两种由 Discuss 应用(mail 模块)提供:

  • mail.thread提供在许多文档表单下方或右侧的消息面板功能,以及消息和通知相关逻辑。这在我们自己的模型中将经常会添加,下面就来一起学习下。
  • mail.activity.mixin模型提供待办任务计划。

ℹ️Odoo 11中的修改
mail 模块现在通过mail.activity.mixin抽象模型提供Activities任务管理功能。该功能在 Odoo 11中才添加,此前的版本中没有。

我们一起来为 Member 模型添加上述两种 mixin。社交消息功能由 mail 模块的mail.thread模型提供,要将其加入自定义模型,应进行如下操作:

  1. 通过 mixin 模型 mail 为插件模块添加依赖
  2. 让类继承mail.thread和mail.activity.mixin两个 mixin 类
  3. 将message_follower_ids, message_ids和activity_id这些 mixin 的数据字段添加到表单视图

对于第一步扩展模型需要在__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
<odoo>
...
<form>
...
<!-- mail mixin fields -->
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers" />
<field name="activity_ids" widget="mail_activity" />
<field name="message_ids" widget="mail_thread" />
</div>
</form>

mail 模块还为这些字段提供了一些特定的网页组件,以上代码中已使用到。在升级模块后会员表单将变成这样:

Odoo 12图书项目使用 Mixin 类继承

有时普通用户仅能访问正在 follow 的记录。在这些情况下我们应添加访问记录规则来让用户可以看到 follow 的记录。本例中用不到这一功能,但可通过[(‘message_partner_ids’, ‘in’, [user.partner_id.id])]或来进行添加。

继承 Python 方法

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 12图书项目10位 ISBN 验证

继承 Web 控制器和模板

Odoo 中的所有功能都带有扩展性,web 功能也不例外,所以已有控制器和模块都能被继承。

作为示例,我们将继承图书目录网页,加入前面添加的图书可用性信息:

  • 在控制器端添加对查询参数的支持,访问/library/books?available=1过滤出可借阅图书
  • 在模板端,添加一个图书不可用的表示

继承网页控制器

网页控制器不应包含实际业务逻辑,仅集中于展示逻辑。我们可能会需要添加对额外 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://:8069/library/books?available=1 将仅显示勾选了Is Available? 的图书

Odoo 12图书项目可借阅图书

继承 QWeb 模板

要修改网页的实际展示,就需要继承所使用的 QWeb 模板。我们将继承library_app.book_list_template来展示更多有关不可借阅图书的信息。添加library_member/views/book_list_template.xml文件并加入如下代码:

1
2
3
4
5
6
7
8
9
10
11
<odoo>
<template id="book_list_extended"
name="Extended Book List"
inherit_id="library_app.book_list_template">
<xpath expr="//span[@t-field='book.publisher_id']" position="after">
<t t-if="not book.is_available">
<b>(Not Available)</b>
</t>
</xpath>
</template>
</odoo>

网页模板像其它 Odoo 视图类型一样是 XML 文件,同样也可以使用 XPath 来定位元素并对它们进行操作。所继承模型通过在元素中的inherit_id来指明。

小贴士: 在前例中使用了灵活性很强的 XPath 标记,但这里也可以使用等价的简化标记:

然后在 library_member/manifest.py文件中加入该文件的声明:

1
2
3
4
    'data':[
...
'views/book_list_template.xml',
]

然后访问http://:8069/library/books即可对不可借阅图书展示额外的(Not Available)信息。

Odoo 12图书项目不可借阅图书

总结

扩展性是 Odoo 框架的一个重要功能。我们可以创建插件来为需要实现功能的多个层的已有插件修改或添加功能。

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

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

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

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

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

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

☞☞☞第五章 Odoo 12开发之导入、导出以及模块数据

学霸专区

  1. 如何继承已有模型来添加 mixin,如mail.thread?
  2. 要在会员表单视图中添加Phone字段需要做哪些修改?
  3. 如果创建一个与继承属性的属性名不同的模型类会发生什么(例如_name=’y’ and _inherit=’x’)?
  4. XPath是否可用于修改其它模块的数据记录?
  5. 继承一个模型时,是否可扩展其方法但不使用 super()调用所继承的原始代码?
  6. 如何在不引用任何特定字段名的情况下继承图书目录页并在末行添加 ISBN 字段?

扩展阅读

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

本文首发地址:Alan Hou 的个人博客