Odoo 开发通常都需要创建自己的插件模块。本文中我们通过创建首个Odoo应用,学习在 Odoo 中显示、安装应用的步骤。
我们将从开发工作流的基础学起,即创建、安装新模块,然后升级开发迭代中更新的代码。
Odoo 采用类 MVC(Model-View-Controller)的架构,我们会深入到各层来实现一个图书应用。
本文主要包含以下内容:
- 图书项目总览
- 步骤1:新建插件模块
- 步骤2:新建应用
- 步骤3:添加自动化测试
- 步骤4:实现模型层
- 步骤5:配置访问权限
- 步骤6:实现后台视图层
- 步骤7:实现业务逻辑层
- 步骤8:实现网站用户界面(UI)
通过这种方法,我们会渐进学习组成应用的基本组成,并体验从零构建Odoo模块的迭代流程。
技术准备
学习本文需安装 Odoo 服务端,并可通过命令行启动服务来进行模块安装和运行测试之类的操作。如果还没有可用的Odoo开发环境,请参照第二章 Odoo 15开发之开发环境准备。
本章中我们从零开始创建第一个 Odoo 应用,无需任务初始代码。
本章的代码可通过 GitHub 仓库的ch03目录进行查看。
注:本章示例代码中使用的数据库名为 library,如你使用了其它数据库名称,请自行修改
图书项目总览
为更好地讲解本章知识点,我们通过案例进行学习。一起来创建一个管理图书的 Odoo 应用。该项目会在后续章节中持续使用,每篇文章都会进行一次迭代,对应用添加新的功能。本文中创建图书应用的第一个版本。
实现的第一个功能是图书目录。目录让我们可以在图书馆中保存图书的记录和图书详情。我们希望这个目录是可以对外访问的,可以查到可借阅的图书。
图书应包含如下数据:
- 标题
- 作者
- 出版社
- 出版日期
- 封面图
- ISBN(国际标准书号):包含ISBN的有效性检查
- 上架标记;标识图书是否可公开发布
按Odoo基本应用的惯例,图书应用有两个用户有组:图书用户和图书管理员。用户可执行日常操作,管理员还可以编辑应用的配置。
图书目录部分中,我们把图书记录编辑的功能预留给管理员。如下:
- 图书管理员可编辑图书
- 图书用户和网站访客仅能浏览图书
这一简单的项目包含构建Odoo应用的所有主要组件。第一步是要创建一个模块目录,用于放置应用的代码和组件。
步骤1:新建插件模块
插件模块是包含实现Odoo功能文件的目录。可以新增功能或修改已有的功能。插件目录必须含有一个声明或描述文件__manifest__.py
。
一部分模块插件在 Odoo 中以应用(app)的形式出现。应用是Odoo功能区中的那些顶级模块,我们希望这一模块出现在Apps菜单的顶级。Odoo的基本应用有CRM、Project和 HR。非应用模块插件一般依赖于某个应用,为其添加或扩展功能。
如果新模块为 Odoo 添加新的或重要的功能,一般应归类为应用。而如果模块仅修改已有应用的功能,那就是一个普通的插件模块。
开发新模块,我们需要:
- 确保操作的目录在 Odoo 的 addons 路径中
- 创建模块的目录,并包含声明文件
- 如打算对外发布,为模块选择一个证书
- 添加模块描述信息
- 为模块添加一个图标,此为可选
然后我们就可以安装模块了,确定模块在 Odoo 服务中可见并正确安装。
准备 addons 路径
一个插件模块是一个含有 Odoo 声明文件的目录,它创建一个新应用或为已有应用添加功能。addons目录中可包含多个插件模块。addons路径是Odoo的一项配置,包含一系列目录,Odoo服务端可在其中查找插件。
默认addons路径包含odoo/addons,其中存放 Odoo 自带的官方应用,以及odoo/odoo/addons目录,其中为提供核心功能的 base 模块。通常修改addons添加一个或多个目录,用于有些话自定义开发的及所希望使用的社区模块
图书应用包含多个模块。这是一种良好实践,因其采用更小更精细的模块,降低了复杂度。我们为项目模块创建一个插件目录。
读者如果学习了第二章 Odoo 15开发之开发环境准备,Odoo服务端代码应该存放于~/work15/odoo/。自定义插件模块就放在自己的目录中,独立于Odoo代码之外 。
我们为图书应用创建一个~/work15/library 目录,将该目录添加至添加路径中。可通过直接编辑配置文件或Odoo的命令行来实现。操作如下:
1 2 3 4 5 |
$ mkdir ~/work15/library $ source ~/work15/env15/bin/activate (env15) $ odoo \ --addons-path="~/work15/library,~/work15/odoo/addons" \ -d library -c ~/work15/library.conf --save --stop |
这时Odoo命令行会返回这样的错误:odoo: error: option –addons-path: no such directory: ‘/home/daniel/work15/library’。这是因为该目录仍是空的,Odoo无法在其中查找到插件模块。在搭建好首个图书应用模块的骨架后就不会存在这一问题了。
下面是关于Odoo命令行参数的说明:
--addons-path
参数设置所有Odoo模块所在的目录。-d
或--database
参数设置所使用的数据库名。如果数据库不存在,会使用Odoo的基本数据库模块创建并初始化。-c
或--config
参数设置使用的配置文件。--save
参数配合-c
参数使用将参数保存到配置文件中。--stop
是--stop-after-init
的短格式,它会在所有启动序列中的操作完成后停止Odoo服务,回到命令行。
如果插件路径参数使用的是相对路径,Odoo会将其转化为绝对路径再存储到配置文件中。
ODOO 15中的变化
所创建的配置文件会使用默认配置作为模板。在Linux系统中,默认配置文件为~/.odoorc。
Odoo 自带一个scaffold命令可快速创建新的模块骨架。可以使用它为图书插件目录填充有效的模块。执行如下命令初始化library_app模块目录:
1 |
(env15) $ odoo scaffold library_app ~/work15/library |
scaffold命令需要两个参数:模块目录名及创建位置的路径。有关scaffold命令的详情比较偏,可以执行odoo scaffold --help
查看。
现在再次执行保存配置文件的命令,带上插件目录~/work15/library/,就可以成功运行了。
启动序列的前几行日志消息包含所使用的配置。其中包含INFO ? odoo: Using configuration file at…用于标识所使用的配置文件,以及INFO ? odoo: addons paths: […] 列出所使用的插件目录。在排查Odoo为什么没能找到自定义模块时首先应该查这里。
创建模块目录
接上一节,我们有 ~/work15/library目录用于存放Odoo模块,将其添加到了插件路径中,这样Odoo服务就可以找到其中的模块了。
上一节中我们还使用了Odoo的 scaffold 命令自动为library_app模块目录创建了一个骨架结构,其中包含了基础的结构。scaffold命令的用法为:odoo scaffold <module> <addons-directory>
。所创建的模块目录如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
library_app/ ├── __init__.py ├── __manifest__.py ├── controllers │ ├── __init__.py │ └── controllers.py ├── demo │ └── demo.xml ├── models │ ├── __init__.py │ └── models.py ├── security │ └── ir.model.access.csv └── views ├── templates.xml └── views.xml |
模块目录名是其技术名称,本例中使用library_app。技术名称应为有效 Python 标识符,即以字母开头且仅能包含字母、数字和下划线。
它包含多个子目录,用作模块的不同组件。这种子目录结构并非强制,但普遍这样用。
有效的Odoo插件目录并包含一个__manifest__.py
描述文件。模块还应是可导入的,因此必须包含一个__init__.py
文件。在目录树结构中可以看到这正是前两个文件。
小贴士:在Odoo的老版本中,模块描述文件的名称为为__openerp__.py。 这一文件已废弃但系统仍支持。
描述文件包含一个Python字典,其中的属性对模块进行描述。脚手架所自动生成的文件如下:
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 27 28 29 30 31 32 33 34 35 |
# -*- coding: utf-8 -*- { 'name': "library_app", 'summary': """ Short (1 phrase/line) summary of the module's purpose, used as subtitle on modules listing or apps.openerp.com""", 'description': """ Long description of module's purpose """, 'author': "My Company", 'website': "http://www.yourcompany.com", # Categories can be used to filter modules in modules listing # Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml # for the full list 'category': 'Uncategorized', 'version': '0.1', # any module necessary for this one to work correctly 'depends': ['base'], # always loaded 'data': [ # 'security/ir.model.access.csv', 'views/views.xml', 'views/templates.xml', ], # only loaded in demonstration mode 'demo': [ 'demo/demo.xml', ], } |
下一节中我们会详细讨论声明文件。
模块文件__init__.py
应触发模块所有Python文件的导入。更具体来说,它应导入模块顶层的所有Python文件以及包含Python文件的子目录。类似地,这些子目录也应包含一个__init__.py
文件,导入子目录中的Python资源。
scaffold命令所生成的顶层__init__.py
文件内容如下:
1 2 |
from . import controllers from . import models |
在顶层中没有Python文件,只有两个包含Python文件的子目录:controllers和models。查看模块的树结构,可以看到这两个目录均包含Python文件以及__init__.py
文件。
创建声明文件
scaffold命令所准备的声明文件可作为指南,或者我们也可以创建一个空的声明文件。
声明文件应为一个包含字典的Python文件。不强制包含任何键,所以一个空字典{}就可作为文件的有效内容。实践中,我们希望为模块提供一些基本信息、声明作者并选择分发证书。
以下可作为初始内容:
1 2 3 4 5 6 7 8 9 10 |
{ "name": "Library Management", "summary": "Manage library catalog and book lending.", "author": "Alan Hou", "license": "AGPL-3", "website": "https://alanhou.org", "version": "15.0.1.0.0", "depends": ["base"], "application": True, } |
这里用到的键为应用表单的主标签中显示的信息,如下图所示:
我们使用了如下的描述键:
- name:模块标题
- summary:对模块做的单行综述
- author:版权作者姓名。本处为一个字符串,但可以是逗号分隔的一系列姓名
- license::标示作者授权模块进行分发的证书。AGPL-3和LGPL-3都是知名的开源选项。通过Odoo应用商店销售的自有模块应当使用Odoo自家的证书OPL-1。在本章的后面会对证书做更深入讨论。
- website:了解模块更多信息的 URL(统一资源定位符),可以帮助人们查看更多文档或提供文件 bug 和建议的跟踪
- version::模块的版本号。应遵守版本号规则。建议在模块版本号前加上 Odoo 版本,这样有助于确定模块所针对的Odoo版本。例如,针对Odoo 15.0的1.0.0模块可使用版本号15.0.1.0.0。
- depends:插件模块的依赖列表。安装模块时会触发对这些模块的安装。如果模块没有具体的依赖,通常会让其依赖base模块,但这并非强制要求。
- application:一个布尔型标记,代表模块是否在应用列表中以应用展示。大多数扩展模块是对已有应用添加功能,会将此值设置为False。图书管理模块是一个新应用,因为设置为True。
对依赖列表应保持谨慎。应确保在这里显式设置所有的依赖,否则,在新数据库中安装模块时可能会失败,因为缺失依赖或由于在Odoo启动序列中所需的模块加载时间晚于当前模块产生加载错误。在其它机器上部署时这两种情况都可能出现 ,定位、解决问题会非常耗时。
图3.1中的<div class="document">
一行用于模块的长描述,目前为空。这在添加描述一节中会进行讨论。
还有一些其它的描述键,用得不那么频繁:
- installable:表明模块是否可安装。默认值为True,因而无需显示设置。如果出于某种原因希望在插件目录中保留文件却禁用模块可以将其设置为False。
- auto_install:可设置为True,用于胶水模块。胶水模块的安装在安装所有依赖时进行一次触发。例如,它可用于在实例中同时安装了两个应用时,自动为其提供桥接的功能。
设置模块分类
模块可进行分类,表示相关的功能区。这些分类用于插件模块的分组,以及安全组。
如果插件未设置分类,默认使用Uncategorized。当前图书应用正是这一分类。
我们可以在Apps菜单的左边栏中看到一些Odoo的分类。在那里可以看到模块所能使用的分类,如下图所示:
分类可进行分组,如Project应用属于Services/Project分类。
如果对模块使用了不存在的分类,Odoo会自动创建。我们利用这一特性为图书应用新建 一个分类:Services/Library。
编辑__manifest__.py
文件新增category:
1 |
"category": "Services/Library", |
分类也可用于组织安全组,需要相应的XML ID来在XML数据文件中引用它们。
分配给模块分类的XML ID由base.module_category_加上分类名自动生成。例如,Services/Library所生成的XML ID为base.module_category_services_library。
我们可以访问相应的表单视图然后使用开发者菜单中的View Metadata选项来查看应用分类的XML ID 。
应用分类没有菜单项,但可能过安全组表单访问分类表单,如下:
- 打开Settings > User > Groups 菜单选项,新建一条测试记录。
- 在Application字段下拉列表中选择中选择一个选项,点击保存。操作流程如下图所示:
图3.3:用户组表单的应用选择列表 - 点击Application链接打开所选的分类对应详情表单。
- 位于分类表单时,在开发者菜单中选择View Metadata 查看所分配的XML ID。
- 此时不打算使用就可以删除测试分组了。
此外,可在Odoo的源代码中查看内置的分类及它们的XML ID。对应的GitHub链接为https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml。
选取证书
选取模块的证书非常重要,应当仔细考虑哪个是最佳选项,以及其潜在含义。
软件代码受版权法保护,保留作者使用和修改的权利。这通常是指你个人或你所在的公司。而其它主体如需安全使用你的代码,必须获取代码作者的证书授权。
如果希望代码自由免费传播,需要包含一个证书,表明其他有权使用你的代码。不同的证书条款不同。
Odoo模块最常使用的证书是LGPL-3(GNU’s Not Unix (GNU) Lesser General Public License)及AGPL-3(Affero General Public License)。两者都允许自由发布并进行修改,要求是应携带作者的署名以及衍生的代码要使用相同的证书。
AGPL是一种强力的开源证书,要求使用代码的在线服务对用户共享源代码。在社区中流行这种证书是因为它强制衍生的代码也使用AGPL发布。因而开源代码无法内置到商业解决方案中,而原作者可以因其他的改进而受益。
LGPL比AGPL更具许可性,它允许进行商业修改,而无需分享相应的源代码。网络和系统集成组件通常选择这种证书,其解决方案可能包含私有证书或与AGPL相兼容的组件。
进一步了解GNU 证书请访问GNU官网。
虽然可以销售GPL证书的应用,这却不是一种常用的商业模式,因为其他人可以自由拷贝、发布该代码。因此,Odoo应用商店中销售的很多模块使用自有证书。Odoo推荐使用其自有证书OPL-1。
进一步了解Odoo 证书请访问https://www.odoo.com/documentation/user/legal/licenses/licenses.html。
添加描述
描述(description)是一段展现模块功能的长文本。描述文本支持通过RST(reStructuredText)格式生成富文本文档。
可通过https://docutils.sourceforge.io/rst.html学习RST知识。该页面包含一个快捷手册链接,值得收藏:https://docutils.sourceforge.io/docs/user/rst/quickstart.html。
以下是一个RST文档的简短示例:
1 2 3 4 5 6 7 8 9 |
Title ===== Subtitle -------- This is *emphasis*, rendered in italics. This is **strong emphasis**, rendered in bold. This is a bullet list: - Item one. - Item two. |
添加描述的一种方式是在模块声明文件中使用description键。因为描述很可能会跨多行,最好在三引号“””中添加内容,这属于Python多行字符串的语法。
在GitHub等网站上发布的源代码应包含README文件,供访客轻松查看模块的简介。因此除了声明文件中的描述,Odoo还可以使用README.rst或README.md文件。文件应放在模块的根目录下,与__manifest__.py
文件同级。
另一种方案是使用HTML(超文本标记语言)文档描述文件。很多Odoo应用商店中发布的模块使用这种方式,对应用功能做更丰富的视觉展示。index.html文件应放在static/description/模块子目录下。页面资源文件,如图片和CSS,应当放在同一目录中。
注:对于将application设置为True的模块,只使用index.html描述,description键会遭到忽略。
添加图标
模块可以选择一个自己的图标。对于创建为应用的模块,这尤为重要,因为应用会在Apps菜单下显示图标。
需要对模块添加static/description/icon.png 文件进行使用。
对于图书应用项目,我们复用已有Odoo应用Notes的图标,将其拷贝至library_app/static/description目录中。
在命令行中使用如下命令:
1 2 3 |
$ cd ~/work15/library/library_app $ mkdir -p ./static/description $ cp ~/work15/odoo/addons/note/static/description/icon.png ./static/description/ |
安装新模块
现在我们已经有了一个精简的插件模块。尚未实现任何功能,但我们可以通过安装它来检查是否正常运行。
安装新模块,应在启动服务时使用-d和-i参数。-d或--database
参数确保我们使用正确的Odoo数据库。-i或--init
参数接收一个待安装的逗号分隔模块列表。
ODOO 11中的变化
安装新模块时,Odoo会自动当前配置的插件路径中更新可用模块列表。截至Odoo 10还不是如此,彼时需要在安装插件模块前手动更新模块列表。模块列表在网页客户端通过Apps列表中的菜单项更新。
使用本章所准备的Odoo开发环境并激活Python虚拟环境,可通过如下命令安装library_app模块:
1 |
(env15)$ odoo -c ~/work15/library.conf -d library -i library_app |
我们添加了-d library参数来指定正确的数据库进行安装。如果在配置文件中已进行指定,这个参数就是多余的。尽管如此,出于安全保障,最好在命令行中声明安装的数据库。
小贴士:仔细看服务端日志消息,确定可正确找到模块并安装。应该会在日志中看到odoo.addons.base.models.ir_module: ALLOW access to module.button_install并且无警告信息。
为让模块可安装,应将模块所在的插件目录告知Odoo。可通过重启Odoo服务端,并查找启动时所打印的odoo: addons paths:日志消息来进行确认。
如果找不到该模块,通常是因为插件路径错误。仔细检查使用的插件路径。
升级模块
开发模块是一个不断迭代的过程,对源代码不断地修改并在 Odoo 中应用。
可以在图形界面(GUI) 的Apps列表中查找模块并点击 Upgrade 按钮。这会重新加载数据文件、应用所做的修改并升级数据库模式定义。但如果修改的是 Python 逻辑,点击升级还不够。需要重启Odoo服务来重新加载修改后的Python代码。有时,模块中既修改了数据文件又修改了 Python 逻辑,那么就需要同时进行如上两种操作。
总结起来如下:
- 修改模型或其字段时,需要进行升级来应用数据库模式的修改。
- 修改Python逻辑代码时,需要重启来重新加载代码文件。
- 修改XML或CSV文件时,需要进行升级来重新应用文件中的数据。
为避免修改Odoo代码时所产生的困惑和阻力,最简单的方案是在修改代码后通过升级命令重启Odoo服务。
在服务端实例的终端中通过Ctrl + C停止服务。然后通过如下命令启动服务并升级library_app模块:
1 |
(env15)$ odoo -c ~/work15/library.conf -d library -u library_app |
-u参数(或全称–update)要求使用-d 参数并接收一个逗号分隔的待升级模块集。例如可以使用-u library_app,mail。模块升级后,所有依赖该模块的模块也会被升级。
按下上方向键会调出之前使用的命令。因此大部情况下,会使用Ctrl + C、上方向键和Enter键的组合来重复这一操作。
在近几个Odoo版本中,可使用对开发者友好的--dev=all
参数,来自动化这一工作流。使用该参数后,数据文件的修改会即时体现在运行的Odoo服务中,Python代码的修改会触发Odoo代码重新加载。有关这一参数的详情,请参见第二章 Odoo 15开发之开发环境准备的使用服务端开发模式一节。
现在模块目录已就绪,可托管实现应用的所有组件。因为这是一个应用,而非添加功能的技术模块,我们会开始添加一些应用所需的基础组件。
步骤2:新建应用
一些 Odoo 模块创建新应用,而另一些则对已有应用添加功能或作出修改。虽然两者的技术组件基本相同,但应用通常包含一些特征性元素。我们创建的是一个图书应用,所以应在模块中包含这些元素。
应用应包含的元素如下:
- 图标:用于在应用列表中展示
- 顶级菜单项:其下放置应用的所有菜单项
- 应用安全组:通过访问权限仅对指定用户开放
应用的图标是放置在static/description/子文件夹中的icon.png文件。在添加图标一节中已经添加过了。
下面我们来添加应用的顶级菜单。
添加应用顶级菜单项
我们创建的是一个新应用,因此应包含主菜单项,在社区版中,位于左上角的下拉菜单中,而在企业版中,则作为附加图标显示在应用切换器主界面中。
菜单项是使用 XML 数据文件添加的视图组件。通过创建views/library_menu.xml文件添加以下内容来定义菜单项:
1 2 3 4 |
<odoo> <!-- Library App Menu --> <menuitem id="menu_library" name="Library" /> </odoo> |
用户界面,包含菜单项和操作,均存储于数据表中供客户端实时读取解释,
上面的文件描述了要载入 Odoo 数据库的记录。<menuitem>
元素指示在存储Odoo菜单项的ir.ui.menu模型上写入一条记录。
id 属性也称作XML ID,用于唯一标识每个数据元素,以供其它元素引用。例如在添加图书子菜单时,就需要引用父级菜单项的XML ID,即menu_library。
此处添加的菜单项非常简单,仅用到了一个属性name。其它常用的属性这里没有使用。在本章稍后的实现后台视图层部分中会深入学习。
图书模块还不知道 XML 数据文件的存在。我们需要在声明中使用 data 属性来添加、载入到Odoo实例中。编辑_manifest__.py
文件中的字典,添加如下键:
1 2 3 |
'data': [ 'views/library_menu.xml', ], |
data声明键是一个在安装或升级模块时加载的数据文件列表。文件路径为声明文件所丰的根目录的相对路径。
要向Odoo数据库中加载这些菜单设置,需要升级模块。此时还不会有什么显式的效果。因菜单项还不包含可操作子菜单,所以不会显示。在添加好子菜单及相应的访问权限时即可显示。
小贴士:菜单树中的项目仅在含有可见子菜单项时才会显示。打开视图的底层菜单项仅对拥有访问相应模型的权限的用户可见。
添加权限组
普通用户在使用功能前需获得相应的权限。Odoo 中使用权限组来实现。权限授予组,组中再分配用户。
Odoo 应用通常有两个组,访问权限不同:
- 用户访问权限,用于执行日常操作的用户
- 管理员访问权限,包含配置等所有功能的访问权限
图书应用也包含这两个权限组。下面我们会实现。
访问权限相关的文件通常放在security/模块子目录中,因此我们需要创建一个security/library_security.xml文件用于权限定义。
权限组以插件模块使用的相同分类进行分组。需要找到相应的XML ID来为权限组设置分类。查找XML ID的方法在本章的设置模块分类一节中已进行过讨论。通过那部分的学习,我们知道Services/Library的XML ID 为base.module_category_services_library.。
下面我们就来添加这图书用户权限组。它属于前面所定义的Library分类,XML ID为module_library_category,还会继承内部用户权限,在其基础上实现。如若在用户组表单中打开开发菜单的View Metadata选项,会看到其XML ID为base.group_user。
现在对security/library_security.xml文件添加如下XML:
1 2 3 4 5 6 7 8 9 10 |
<odoo> <data> <!-- Library User Group --> <record id="library_group_user" model="res.groups"> <field name="name">User</field> <field name="category_id" ref="base.module_category_services_library" /> <field name="implied_ids" eval="[(4, ref('base.group_user'))]" /> </record> </data> </odoo> |
这里的知识量比较大,所以我们慢慢讲解下每个元素。这段XML是对组模型es.groups添加一条记录。记录有三个字段,分别是:
- name:组名。这是一个普通的字符串。
- category_id:关联应用,这是一个关联字段,因此使用了 ref 属性来通过 XML ID 连接此前创建的分类。
- implied_ids:这是一个一对多(one-to-many)关联字段,包含一个组列表来涵盖对组内的用户。对多字段使用了一个特殊语法,在本书第五章 Odoo 15开发之导入、导出以及模块数据中会进行介绍。我们使用了编号4来连接已有的内部用户组XML ID,base.group_user。
ODOO 12中的变化
User表单拥有一个User Type版块,仅在开启了开发者模式才能看到。这样我们可以选择互斥选项中的一个:Internal User、Portal (外部用户,如客户)和Public (网站匿名访客)。这一修改是为了避免此前版本中存在的错误配置,那时内部用户可能不小心加入到Portal和Public组中,从而降低了访问权限。
接下来我们创建管理员组。授予用户组的所有权限以并为应用管理员保留其它的权限。因此我们要继承图书用户library_group_user。
编辑security/library_security.xml文件,在<odoo>
元素中添加如下XML:
1 2 3 4 5 6 7 8 |
<!-- Library Manager Group --> <record id="library_group_manager" model="res.groups"> <field name="name">Manager</field> <field name="category_id" ref="base.module_category_services_library" /> <field name="implied_ids" eval="[(4, ref('library_group_user'))]" /> <field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]" /> |
这里也像之前一样出现了name, category_id, and implied_ids字段。implied_ids字段设置为链接向图书用户组,继承其权限。
同时还设置了users字段。将该授权给了管理员(admin)和Odoobot用户。
ODOO 12中的变化
Odoo 12开始,有了系统root用户,在用户列表中不显示,由框架在需要提权操作(sudo)时在内部使用。admin可以登入系统并应拥有所有功能的访问权限,但不再像系统 root 用户那样可以绕过访问限制。在Odoo 11及之前的版本中,admin用户同时也是内部root超级用户。
同样需要在声明文件中添加该 XML 文件:
1 2 3 4 |
"data": [ "security/library_security.xml", "views/library_menu.xml", ], |
注意library_security.xml 加在library_menu.xml文件之前。数据文件的加载顺序非常重要,因为我们只能引用已经定义过的标识符。菜单项经常引用到安全组,所以建议将安全组定义文件放到菜单和视图文件之前。
下一步是添加定义应用模型的Python代码。但在那之前我们按照测试驱动开发(TDD)的理论先添加一些测试用例。
步骤3:添加自动化测试
编程的最佳实践包含代码的自动化测试,对于像 Python 这样的动态语言尤为重要,因为它没有编译这一步,只有在解释器实际运行代码时才会报语法错误。好的编辑器可以让我们提前发现问题,但无法像自动化测试这样帮助我们确定代码是否可如预期运行。
TDD理论让我们先写测试,检查错误,然后开发代码直至通过测试。受此方法启示,在添加实际功能前我们先添加模块测试:
Odoo支持基于Python内置unittest库的自动化测试。这里我们快速介绍下自动化测试,在第八章 Odoo 15开发之业务逻辑 – 业务流程的支持有更为详尽的讲解。
Odoo 12中的变化
截至Odoo 11,测试可使用YAML(YAML Ain’t Markup Language)文件来进行表述。但 Odoo 12中移除了对YAML文件的支持,所以不能再使用这种方法。
测试需要满足一条件才能被发现,并由测试运行器执行,条件如下:
- 测试放在tests/子目录中。不同于常规的Python代码,这一目录不需要在顶层的
__init__.py
中导入。测试引擎会查找模块中的测试目录,然后运行。 - 测试代码文件名应以test_开头,并通过
tests/__init__.py
导入。测试代码放在Odoo框架几个可用测试对象派生出的类中,由odoo.tests.common导入。最常用的测试类为TransactionCase。测试对象使用setUp()方法初始化测试用例所使用的数据。 - 每个测试用例都是以test_打头的方法。对于TrasactionCase测试对象,每个测试都是独立的事务,开始时运行setup步骤,结束时回滚。因此,下面的步骤不会知道前一个测试所做的修改。
小贴士:测试可使用演示数据更简便地完成配置阶段,但不是一种良好实践,因为那样测试用例只能在安装了演示数据的数据库中运行。如果在测试配置中准备所有测试数据,那么该测试可在任意数据库中运行,包括空数据库或线上数据库的备份库中。
我们计划在应用中添加 library.book模型。下面添加一个简单测试,用于确定新书是否正确创建。
添加测试用例
我们添加一个简单测试,检测书的创建。这要求添加一些配置数据并添加一个测试用例。测试用例仅用于确定active字段的值是否为预期的默认值True。
按照如下步骤操作:
- 在
tests/__init__.py
中添加如下代码:
1from . import test_book - 然后添加实际的测试代码,位于tests/test_book.py文件中,内容如下:
123456789101112from odoo.tests.common import TransactionCaseclass TestBook(TransactionCase):def setUp(self, *args, **kwargs):super().setUp(*args, **kwargs)self.Book = self.env['library.book']self.book1 = self.Book.create({"name": "Odoo Development Essentials","isbn": "879-1-78439-279-6"})def test_book_create(self):"New Books are active by default"self.assertEqual(self.book1.active, True)
setUp()方法获取了一个Book模型对象的指针,然后使用它新建一本书。
test_book_create测试用例添加了一个简单的测试,检查所创建的书active字段的默认值。完全可以在测试用例中创建这本书,但我们选择了初始化方法。原因是我们打算在其它测试用例中使用这本书,在setup中进行创建可以减少重复代码。
运行测试用例
在安装或升级模块时使用--test-enable
参数可在启动服务时运行测试,如下:
1 |
(env15) $ odoo -c ~/work15/library.conf -u library_app --test-enable |
Odoo服务会在升级的模块中查找tests/子目录,然后运行测试。现在测试会抛出错误,可以在服务日志中看到ERROR消息与测试相关。在模块中添加图书模型后就可以解决这一问题。
现在应对测试添加业务逻辑。理想情况下,代码的每一行都应对应至少一个测试用例。
测试业务逻辑
测试我们打算添加检测ISBN有效性的逻辑。所以添加一个测试用例看能否正确检测出 《Odoo 开发手册第一版》ISBN的有效性。检测方法由check_isbn()方法实现,返回值为True 或 False。
在tests/test_book.py文件test_book_create() 方法后再加几行代码:
1 2 3 |
def test_check_isbn(self): "Check valid ISBN" self.assertTrue(self.book1._check_isbn) |
推荐为每个需检查的操作添加一个测试用例。别忘了在使用TransactionCase测试时,每条测试都会独立运行,测试用例创建或修改的数据在测试结束时会进行回滚。
当然,现在运行测试还是会失败,因为还没有实现所测试的功能。
测试访问权限
也可以对访问权限进行检测,确定是否对用户进行了正确的授权。
Odoo 中默认测试由不受权限控制的内部用户__system__
执行。所以我们要改变执行测试的用户,来检测是否授予了正确的访问权限。这通过在self.env中修改执行环境来实现,只需把 user 属性修改为希望运行测试的用户即可。
我们可以修改测试在实现。编辑tests/test_book.py中的setUp方法如下:
1 2 3 4 5 6 7 8 |
def setUp(self, *args, **kwargs): super().setUp(*args, **kwargs) user_admin = self.env.ref('base.user_admin') self.env = self.env(user=user_admin) self.Book = self.env['library.book'] self.book_ode = self.Book.create({ 'name': 'Odoo Development Essentials', 'isbn': '879-1-78439-279-6'})<br> |
我们在setUp方法中添加了两行。第一条使用XML ID查找到admin用户记录。第二行修改用于运行测试的环境self.env,将活跃用户修改为admin用户。
不需要对所编写的测试做其它的修改了。运行的方式不变,但使用的是admin用户,因为更改了环境。
图书应用现在有了两具基本测试,但运行会失败。接下来我们应添加实现功能的代码,以让测试可通过。
步骤4:实现模型层
模型描述并存储业务对象数据,如客户关系管理(CRM) 机会、销售订单或伙伴(客户、供应商等)。模型描述一组字段,也可添加具体的业务逻辑。
模型数据结构及关联的业务逻辑以Python代码呈献 ,使用由Odoo模板类派生出来的对象类。模型与数据表有映射关系,Odoo框架处理所有的数据库交互 ,不仅保持数据库结构与对象的同步,还将所有事务转译为数据库指令。负责的框架组件为对象关系映射 (ORM) 组件。
我们的应用用于管理图书,所以需要一个图书目录模型。
创建数据模型
Odoo 开发指南中提到模型的 Python 文件应放在models子目录中,每个模型有一个对应文件。因此我们在library_app模块目录下创建models/library_book.py文件。
首先应让模块使用的models/目录。这表示在Odoo加载模块时应由Python对其进行导入。为此,编辑模块的主__init__.py
文件添加如下内容:
1 |
from . import models |
同样models/子目录应包含一个__init__.py
文件导入要使用的代码文件。添加models/__init__.py文件,内容如下:
1 |
from . import library_book |
现在我们可以创建models/library_book.py加入如下内容:
1 2 3 4 5 6 7 8 9 10 11 |
from odoo import fields, models class Book(models.Model): _name = 'library.book' _description = 'Book' name = fields.Char('Title', required=True) isbn = fields.Char('ISBN') active = fields.Boolean('Active?', default=True) date_published = fields.Date() image = fields.Binary('Cover') publisher_id = fields.Many2one('res.partner', string='Publisher') author_ids = fields.Many2many('res.partner', string='Authors') |
第一行是 Python 代码导入语句,让 Odoo 内核的models和fields对象在这里可用。
第二行声明了新的library.book模型。这是一个继承自models.Model的类。
接下来的几行进行了缩进,Python的代码按缩进层次进行定义,也就是这些行是对Book类的定义。类名使用驼峰式命令,这是Python一种惯例。实际使用的Python类名与Odoo框架无关。与Odoo相关的模型ID是_name属性,在类中定义。
接着类名的两行使用下划线开头,声明了Odoo类的属性。_name定义了唯一ID (UID),在Odoo中使用它来引用该模型。模型ID使用点号(.)来分隔关键词。
小贴士:仅有模型名使用点号(.) 来分隔关键字。其它如模块、XML 标识符、数据表名等都使用下划线(_)。
紧接着的是_description模型属性。这是模型记录的显示名,在涉及模型记录的用户消息中会用到。不强制有这个字段,但没有的话会在服务端日志中显示警告消息。
最后的7行声明了模型字。我们可以看最常用的几种字段类型。标量值可以使用Char, Boolean, Date和Binary类型。关联字段可以使用Many2one 和 Many2many。
name字段用作数据模型标题,这里为书名。
active 字段用于有效记录,默认仅有效的记录会显示,无效记录会隐藏。适用于需要隐藏不再使用的记录但由于历史原因又要在数据库中保留的情况。
小贴士:name和active均为特殊字段名。默认对Odoo有特殊用途。name默认用作记录显示名,在另一个模型引用它时显示。active字段用于在用户界面中过滤掉无效记录。
publisher_id是一个多对一关联字段,在数据库中称为外键。它存储对另一个模型记录的关联关系,本例中为res.partner模型。用于关联出版公司。通常多对一字段的名称以_id结尾。
author_ids是一个多对多关联字段。可存储其它模型一条或多条记录的关联关系。本例用于图书的作者,可关联res.partner模型中的多条记录。在数据库层面,这种数据并不是存储在一个表格字段中,而是放在一个自动创建的辅助数据表中,用于存储两张表之间的关联。通常对多字段的名称以_ids结尾。
图书和伙伴模型之间有两种关联。伙伴模型是Odoo框架中内置的,用于存储人、公司和地址。我们使用它来存储出版社和作者。
现在通过升级图书应用来使用这些修改生效。以下是在library数据库中升级library_app模块的命令:
1 |
(env15)$ odoo -c ~/work15/library.conf -d library -u library_app |
现在还没访问图书模型的菜单。在本章稍后会添加。那么查看新创建的模型看是否在数据库中创建正确,可以通过Technical菜单。在Settings顶级菜单中,进入Technical > Database Structure > Models,搜索library.book模型、点击查看其定义内容,如下图所示:
我们应该可以看到所列的模型,并确认它包含Python文件中所定义的字段。如果你看不到,请升级重启,仔细查看服务端日志,看有没有加载图书应用的消息,以及有没有Odoo数据库的警告消息。
在library.book字段列表中,我们会看到一些并未声明的其它字段。这些特殊字段由Odoo自动为某个模型添加。它们是:
- id是模型中每条记录的唯一数据库标识符
- create_date和create_uid分别为记录创建时间和创建者
- display_name为所使用的记录提供文本显示,如其它记录引用它,它就会被计算并默认使用 name 字段中的文本
- write_date和write_uid分别表示最近修改时间和修改者
- __last_update是一个计算字段 ,它不在数据库中存储,用于做并发检测
现在图书模型在数据库中进行了创建,但用户仍无法访问。我们需要添加菜单,但光加菜单也不行。要显示菜单,首先需要授权新模型的访问。
步骤5:配置访问权限
library.book模型已在数据库进行了创建,便在加载服务时,你可能会注意到输出日志中有一条警告信息:
1 |
The model library.book has no access rules, consider adding one. |
提示消息已经很明确了,我们的新模型没有权限规则,所以没人能使用。我们已为应用添加了权限组,现在需要授权他们访问应用模型。
ODOO 12中的变化
admin和其它用户一样遵守访问权限规则,只有像root这样的超级用户才不受限。在访问新模型之前需要先授权。在Odoo 11及之前则并非如此。在这些较早的Odoo版本中,admin用户也是内部超级用户,不受权限规则的限制。也就说admin自动可以使用新模型。
添加访问控制权限
要了解需要哪些信息来为模型添加权限,可访问后台Settings > Technical > Security > Access Rights,如下图所示:
这些访问权限也称称作访问控制列表(ACL)。上图中可以看到一些模型的ACL。表明权限组可以对记录执行哪些操作:读、写、创建和删除。
Odoo 14中的变化
用于交互向导的临时模型,现在也需要向用户组提供访问权限。此前的Odoo版本没这个要求,用户默认可访问这些模型。推荐授予读、写和创建的权限,而不给删除权限(在CSV文件中为1,1,1,0)。
我们的图书应用会给用户组授予写、读和创建图书记录的权限,而管理员拥有所有权限,包含删除记录的权限。
这一数据可通过模块数据文件提供,将记录加载到ir.model.access模型中。CSV数据文件的名称必须与所要加载数据的模型ID相匹配。
所以要新增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_book_user,BookUser,model_library_book,library_group_user,1,1,1,0 access_book_manager,BookManager,model_library_book,library_group_manager,1,1,1,1 |
文件的第一行为字段名。CSV 文件中有如下列:
- id是记录的外部标识符(也称为XML ID),需在模块中唯一。
- name是描述性标题。可提供信息,推荐使用唯一的名称。
- model_id是授权模型的外部标识符。模型有ORM自动生成的XML ID,对于library.book,标识符为model_library_book。
- group_id指明授权的权限组,我们给前文创建的权限组授权:library_group_user和library_group_manager。
- perm_…字段授权read读, write写, create创建, 或unlink删除操作。使用1表示yes/true,0表示no/false
别忘了在__manifest__.py
的 data 属性中添加对新文件的导入。修改后如下:
1 2 3 4 5 |
'data': [ 'security/library_security.xml', 'security/ir.model.access.csv', 'views/library_menu.xml', ], |
老规矩升级模块使修改在Odoo数据库中生效。此时警告信息就不见了。
这时,admin用户就可以访问图书模型。所以每一条测试应该可通过。我们来运行一下:
1 |
(env15) $ odoo -c ~/work15/library.conf -u library_app --test-enable |
应该可以看到一条测试通过,一条失败。
ACL访问权限在模型层授权,但Odoo还通过记录规则支持等级访问权限。在下一小节中讲解。
行级权限规则
权限规则定义过滤器限定权限组可访问的记录。例如,限定销售员仅能查看自己的报价,或是会计仅能查看所授权公司的会计账目。
为展示这个功能,我们限定图书用户无法访问无效图书。虽然默认这些书是隐藏的,但通过active等于True的条件进行过滤时还是会访问这些记录。
记录规则位于Technical菜单下,与Access Rights同级。存储于ir.rule模型中。
定义记录规则所需的字段如下:
- name: 独特的标题,最好是唯一的。
- model_id: 对应用规则的模型的引用。
- groups: 对应用规则的权限组的引用。这是一个可选项,如未设置则被视为全局规则(global字段自动设置为True)。全局规则运行机制不同,其所加的限制非全局规则无法覆盖。使用特定的语法写入对多字段中。
- domain_force: 用于访问限制的域过滤器,采取由Odoo所使用的域过滤表达式元组列表语法。
要对图书应用添加记录规则,需编辑security/library_security.xml文件在结束标签</odoo>
前再添加一段<data>
:
1 2 3 4 5 6 7 8 9 10 |
<data noupdate="1"> <record id="book_user_rule" model="ir.rule"> <field name="name">Library Book User Access</field> <field name="model_id" ref="model_library_book" /> <field name="domain_force"> [('active','=',True)] </field> <field name="groups" eval="[(4,ref('library_group_user'))]" /> </record> </data> |
记录规则位于<data noupdate="1">
元素中,表示这些记录在安装模块时会被创建,但在模块更新时不会重写。这么做是允许对规则在后面做自定义但避免在执行模型升级时自定义内容丢失。
小贴士:开发过程中noupdate=”1″会带来麻烦,因为要修复和修改在模块升级时都不会更新。有两种处理方法。一种是在开发时临时使用noupdate=”0″,完成后修改为noupdate=”1″。另一种是不进行升级而是重新安装模块。在命令行中对已安装模块将-u换成-i即可实现。
不会在数据库中重写数据。所以在开发时可以修改为noupdate=”0″来让数据达到预期结果。
groups字段是多对多关联,使用对多字段所需要的特定语法。它是一个元组列表,每个元组都是一条命令。本例中使用了(4, x) ,4表示接下来引用的记录会附加到值之后。所引用的记录为library_group_user,即图书用户组。第六章 Odoo 15开发之模型 – 结构化应用数据中会讨论到对多字段写入的语法。
作用域表达式也使用特殊的语法,一个三元的列表,每个三元元组指定一个过滤条件。第七章 Odoo 15开发之记录集 – 使用模型数据中讲解作用哉过滤器语法。
现在用户已可访问图书模型,我们接下来添加用户界面,先从菜单开始。
步骤6:实现后台视图层
视图层为用户界面的描述,视图用 XML 定义,由网页客户端框架生成数据可知的 HTML 视图。
菜单项可执行窗口动作渲染视图的。比如,Users 菜单项处理一个同样名为 Users 的操作,然后使用列表和表单一个视图组合。
有多种视图类型可供使用。3种最常用的视图为列表视图(因历史原因也称为树状视图)、表单视图以及在搜索框右上角的搜索视图。
在接下来的小节中,我们会逐步进行改进,需要频繁地升级模块来使用修改生效,也可使用--dev=all
服务端参数,这样在开发时就无需升级模块。使用该参数时,视图定义会直接从XML文件中读取,所做的修改无需升级模块即可在 Odoo 中即刻生效。在第二章 Odoo 15开发之开发环境准备中详细地讲解了--dev
参数。
小贴士:如果因 XML 错误导致模块升级失败,不必惊慌!仔细阅读服务端日志的错误信息,就可以找到问题所在。如果觉得麻烦,注释掉最近编辑的 XML 内容或在
__manifest__.py
中删除该XML 文件,重新更新,服务应该就可正确启动了。
按照Odoo的开发者指南,用户界面的XML文件应放在views/子目录中。
接下来就创建我们图书应用的界面吧。
添加菜单项
我们的应用现在有了存储图书数据的模型,接下来希望在用户界面中访问它。首先要做的就是添加相应菜单项。
编辑views/library_menu.xml文件,添加如下的窗口动作和模块菜单项记录:
1 2 3 4 5 6 7 8 9 10 11 |
<!-- 打开图书列表的动作 --> <record id="action_library_book" model="ir.actions.act_window"> <field name="name">Library Books</field> <field name="res_model">library.book</field> <field name="view_mode">tree,form</field> </record> <!-- 打开图书列表的菜单 --> <menuitem id="menu_library_book" name="Books" parent="menu_library" action="action_library_book" /> |
这个数据文件包含两条添加到 Odoo 的记录:
<record>
元素定义了一个客户端窗口动作,按顺序在打开library.book模型时启用列表视图和表单视图。- 图书的
<menuitem>
,运行此前定义的action_library_book动作。
现在再次升级模块来让修改生效。可能需要刷新浏览器页面来查看新菜单。完成后就可以看到Library顶级菜单,并包含一个子菜单Books。
虽然尚未定义界面视图,Odoo会自动生成视图,让我们马上就可以查看、编辑数据。
点击 Library > Books菜单会显示一个基础列表视图,点击Create按钮会显示如下的表单视图:
Odoo自动为我们生成了视图,但不够完美。我们可能希望自己着手创建视图,先从图书表单视图开始。
创建表单视图
视图存储在数据库的ir.ui.view模型中的数据记录。因此我们需要添加数据文件,其中包含描述视图的<record>
元素。
新增views/book_view.xml文件来定义表单视图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<odoo> <record id="view_form_book" model="ir.ui.view"> <field name="name">Book Form</field> <field name="model">library.book</field> <field name="arch" type="xml"> <form string="Book"> <group> <field name="name" /> <field name="author_ids" widget="many2many_tags" /> <field name="publisher_id" /> <field name="date_published" /> <field name="isbn" /> <field name="active" /> <field name="image" widget="image" /> </group> </form> </field> </record> </odoo> |
ir.ui.view记录有一个定义XML ID的记录id字段,在其它记录引用它时使用。视图记录设置了三个字段的值:name, model和 arch。
这是library.book模型的视图,名为Book Form。这个名称仅用于提供信息。无需唯一,但应易于分辨所指向的记录。其实可以完全省略 name,那样会按模型名和视图类型来自动生成。
最重要的字段是arch,因其包含了视图的定义,需要仔细讲解。
其中每一个元素是<form>
标签。它声明了所定义的视图类型,其它元素因由其包裹。
接着,我们在表单中使用<group>
元素定义了分组。它可包含<field>
元素及其它元素,包括内嵌group元素。group添加一个两列的隐形风格,很适合字段,因为其占据的正是两列,一列为标签文件,另一列为输入框。
我们的表单仅包含一个<group>
元素,我们为每个字段添加一个<field>
元素以进行显示。字段会自动使用相应的默认微件,比如日期字段使用日期选择微件。在某些情况下,我们可能会添加widget属性来使用指定的微件。author_ids字段就是这么做的,使用一个将作者显示为标签列表的我邮件,还有image字段,使用处理图片的相应我邮件。第十章 Odoo 15开发之后台视图 – 设计用户界面中会详细讲解视图元素。
不要忘记在声明文件的 data 中加入新建文件,否则我们的模块将无法识别并加载该文件。代码如下:
1 2 3 4 5 6 |
'data': [ 'security/library_security.xml', 'security/ir.model.access.csv', 'views/library_menu.xml', 'views/book_view.xml', ], |
视图文件通常在权限文件之后、菜单文件之前。
要使修改载入 Odoo 数据库,就要更新模块。还需要重新加载页面来查看修改效果,可以再次点击菜单项或刷新网页(大多数浏览器中快捷键为 F5)。
业务文档表单视图
上面的部分创建了一个基础表单视图,还可以做一些改进。对于文档模型,Odoo 有一个模拟纸张的展示样式。表单包含两个顶级元素:包含操作按钮的<header>
元素和包含数据字段的<sheet>
元素。
可以使用它修改上一节中定义的基础<form>
元素:
添加动作按钮
我们将演示在头部添加一个按钮检测图书的ISBN是否有效。使用的代码为图书模型中的方法,名为button_check_isbn()。
我们尚未添加该方法,但可以先在表单中添加相应的按钮,代码如下:
1 2 3 4 |
<header> <button name="button_check_isbn" type="object" string="Check ISBN" /> </header> |
一个按钮的基本属性有:
- string:定义按钮显示文本
- type:执行的动作类型,object或action
- name:所运行动作的ID。对于object,name为方法名,而action使用动作的记录 ID
- class:应用 CSS 样式的可选属性,与HTML中用法相同
使用组来组织表单
<group>
标签可用于组织表单内容。<group>
元素创建一个两栏的隐形网格。其中添加的字段元素会在垂直方向上叠加,因为每个字段占据两个单元格:一个用作标签,另一个用作输入框。在<group>
元素内添加两个<group>
元素会生成一个两列字段的布局。
我们会使用它来组织图书表单。修改<sheet>
中的内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<sheet> <group name="group_top"> <group name="group_left"> <field name="name" /> <field name="author_ids" widget="many2many_tags" /> <field name="publisher_id" /> <field name="date_published" /> </group> <group name="group_right"> <field name="isbn" /> <field name="active" /> <field name="image" widget="image" /> </group> </group> </sheet> |
这里的<group>
元素添加了name属性,为其赋值了标识符。这不是强制的,但建议这么做,因为更易于扩展视图引用它们。
完整的表单视图
此时图书表单视图的XML定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<form> <header> <button name="button_check_isbn" type="object" string="Check ISBN" /> </header> <sheet> <group name="group_top"> <group name="group_left"> <field name="name" /> <field name="author_ids" widget="many2many_tags" /> <field name="publisher_id" /> <field name="date_published" /> </group> <group name="group_right"> <field name="isbn" /> <field name="active" /> <field name="image" widget="image" /> </group> </group> </sheet> </form> |
动作按钮还无法使用,因为需要添加业务逻辑。在本章的后续会添加。
添加列表视图和搜索视图
定义列表视图使用<tree>
视图类型。其结构非常直白。<tree>
顶级元素应包含以列形式展示的字段。
我们可以在book_view.xml文件中添加<tree>
视图的定义:
1 2 3 4 5 6 7 8 9 10 11 12 |
<record id="view_tree_book" model="ir.ui.view"> <field name="name">Book List</field> <field name="model">library.book</field> <field name="arch" type="xml"> <tree> <field name="name" /> <field name="author_ids" widget="many2many_tags" /> <field name="publisher_id" /> <field name="date_published" /> </tree> </field> </record> |
以上定义了一个含有四列的列表:name, author_ids, publisher_id和 date_published。
在该列表的右上角,Odoo 显示了一个搜索框。搜索的字段和可用过滤器由<search>
视图定义。
同样还在book_view.xml文件中添加:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<record id="view_search_book" model="ir.ui.view"> <field name="name">Book Filters</field> <field name="model">library.book</field> <field name="arch" type="xml"> <search> <field name="publisher_id" /> <filter name="filter_active" string="Active" domain="[('active','=',True)]" /> <filter name="filter_inactive" string="Inactive" domain="[('active','=',False)]" /> </search> </field> </record> |
搜索视图使用了两种元素定义:<field>
和<filter>
。
<field>
元素定义在搜索框中输入自动搜索的字段。这里添加了publisher_id自动显示出版商字段的搜索结果。<filter>
元素添加预定义的过滤条件,用户通过点击来切换。过滤条件使用了Odoo的作用域过滤语法。在第十章 Odoo 15开发之后台视图 – 设计用户界面中将会进一步介绍。
Odoo 12中的变化
<filter>
现在要求包含name=”…”属性,唯一标识每个定义的过滤器。如果不写,XML验证会失败,模块将无法安装或升级。
现在图书应用的基本组件(模型和视图层)都有了。接下我们添加业务逻辑层,添加让Check ISBN 按钮生效的代码。
步骤7:实现业务逻辑层
业务逻辑层编写应用的业务规则,如验证和自动化操作。现在我们来为Check ISBN按钮添加逻辑。通过在library.book模型的 Python 类中编写方法来实现。
添加业务逻辑
现代 ISBN 包含13位数字,最后一位是由前12位计算所得的检查位。如果digits包含了前12位,Python代码返回相应的检查位:
1 2 3 4 5 |
ponderations = [1, 3] * 6 terms = [a * b for a, b in zip(digits, ponderations)] remain = sum(terms) % 10 check = 10 - remain if remain != 0 else 0 return digits[-1] |
对上面的代码进行些许调整,就成为验证函数的核心代码。它应该为class Book(…)对象中的一个方法。我们会添加方法检测记录的ISBN,返回True或False,如下:
1 2 3 4 5 6 7 8 9 10 |
def _check_isbn(self): self.ensure_one() isbn = self.isbn.replace('-', '') # 为保持兼容性 Alan 自行添加 digits = [int(x) for x in isbn if x.isdigit()] if len(digits) == 13: ponderations = [1, 3] * 6 terms = [a * b for a,b in zip(digits[:12], ponderations)] remain = sum(terms) % 10 check = 10 - remain if remain !=0 else 0 return digits[-1] == check |
注意这个方法不能直接在表单按钮中直接使用,因为它没有提供结果的视图线索。下面我们会添加另一个方法。
ODOO 13中的变化
移除了Odoo应用程序接口(API)中的@api.multi装饰器,无法再使用。注意在此前的Odoo版本可使用这个闭包器,但其实不必。不论加或不加效果一样。
我们使用Odoo的ValidationError异常告知用户验证的结果,首先需要导入异常类。编辑models/library_book.py Python文件在文件顶部添加,如下:
1 |
from odoo.exceptions import ValidationError |
然后还是在models/library_book.py文件的Book 类中加入如下代码:
1 2 3 4 5 6 7 |
def button_check_isbn(self): for book in self: if not book.isbn: raise ValidationError('Please provide an ISBN for %s' % book.name) if book.isbn and not book._check_isbn(): raise ValidationError('%s is an invalid ISBN' % book.isbn) return True |
这里的self表示一个记录集,我们可以遍历每条记录,执行检测。
这个方法是用于表单按钮,所以理论上self为单条记录,不需要使用for循环。其实我们在辅助方法_check_isbn() 做了类似的事。如果使用这种方法,推荐在方法的起始处添加self.ensure_one(),在self不是单条记录时迟早报错。
但我们选择了for循环来支持多条记录,让代码可以执行之后可能希望有的多验证功能。
代码遍历每本选定的图书记录,如果图书的ISBN有值,会检测其有效性。如无效,则向用户抛出警告信息。
模型方法无需返回任何值,便我们应至少让其返回True。因为并非所有实现了XML远程过程调用(RPC)的客户端都支持None/Null值,那样在方法未返回值时可能会抛出错误。
此时可更新模块并再次运行测试,添加--test-enable
参数来确定测试是否通过。也可以在线测试,进入图书表单分别使用正确和错误的 ISBN点击按钮进行测试。
图书应用已包含所有首次迭代所需的后台功能了,我在实现了Odoo多层的组件:模型、视图和业务逻辑。但Odoo还支持创建面向外部的页面。下一节中,我们会创建首个Odoo网页。
步骤8:实现网站用户界面(UI)
1 2 |
from . import models from . import controllers |
然后添加library_app/controllers/__init__.py文件来让目录可被 Python 导入,并添加一条import语句导入稍后实现控制器代码的main.py Python文件,如下:
1 |
from . import main |
接下来创建实际的控制器文件library_app/controllers/main.py,并添加如下代码:
1 2 3 4 5 6 7 8 |
from odoo import http class Books(http.Controller): @http.route('/library/books', auth='user') def list(self, **kwargs): Book = http.request.env['library.book'] books = Book.search([]) return http.request.render( 'library_app.book_list_template', {'books':books}) |
第一行导入了odoo.http模块,是提供网页相关功能的核心组件。接着我们创建了一个控制器对象类,继承自http.Controller。
我们为类及其方法选择的名称并无关联。@http.route装饰器才是重要的部分,它声明了所绑定的URL端点,本例为/books。当前网页使用默认的权限控制,需要用户登录。
在控制器方法内,我们可以使用http.request.env访问运行环境。我们使用它来获取目录中所有有效图书的记录集。
最后一步是使用http.request.render() 来处理 library_app.index_template Qweb 模板并生成输出 HTML。可通过字典向模板传值,这里传递了books记录集。
这时如果重启 Odoo 服务来重载 Python 代码,并访问/library/books会得到一条服务端错误日志:ValueError: External ID not found in the system: library_app.book_list_template。这是因为我们还没有定义模板。下面就一起来定义模板。
添加QWeb模板
QWeb模板和其它视图类型一并存储,相应的数据库文件通常放在/views子目录下。我们创建views/book_list_template.xml文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?xml version="1.0" encoding="utf-8"?> <odoo> <template id="book_list_template" name="Book List"> <div id="wrap" class="container"> <h1>Books</h1> <t t-foreach="books" t-as="book"> <div class="row"> <span t-field="book.name" />, <span t-field="book.date_published" />, <span t-field="book.publisher_id" /> </div> </t> </div> </template> </odoo> |
<template>
元素用于声明 QWeb 模板,它事实上是一个存储模板的 base 模型 – ir.ui.view记录的快捷方式。模板中包含要使用的 HTML,并使用 Qweb 的特定属性。
t-foreach用于遍历变量 books变量中的每一项,通过控制器的http.request.render()调用来获取。t-field属性用于渲染Odoo记录字段的内容。
和其它XML数据文件一样,QWeb模板数据文件需要在模块声明文件中进行声明,这样才会加载并被使用。因此需要编辑__manifest__.py
文件,添加如下内容:
1 2 3 4 5 6 7 |
"data": [ "security/library_security.xml", "security/ir.model.access.csv", "views/book_view.xml", "views/library_menu.xml", "views/book_list_template.xml", ], |
在声明文件中添加了XML文件的声明后,执行模块升级,网页应该就可用了。使用登录了的用户打开http://<my-server>:8069/library/books
URL,应该就会显示可用的图书的简易列表了,如下图所示:
这是对Odoo网页功能的快速总览。这些功能会在第十三章 Odoo 15开发之创建网站及门户前端功能中深入讨论。
快速参考
这里讨论的大部分组件都会在其它章节中讨论,这里提供一个快速参考列表:
- 第二章 Odoo 15开发之开发环境准备:有关命令行安装和升级模块
- 第五章 Odoo 15开发之导入、导出以及模块数据:有关创建XML和CSV数据文件
- 第六章 Odoo 15开发之模型 – 结构化应用数据:有关模型层,定义模型及字段
- 第七章 Odoo 15开发之记录集 – 使用模型数据:有关作用域过滤器语法和记录集的操作
- 第八章 Odoo 15开发之业务逻辑 – 业务流程的支持:有关Python方法业务逻辑
- 第十章 Odoo 15开发之后台视图 – 设计用户界面:有关视图,包含窗口动作、菜单项、表单、列表和搜索
- 第十三章 Odoo 15开发之创建网站及门户前端功能:有关网页控制器和QWeb语法
其它地方没讲解的是访问权限,这里我们对这些组件提供一个快速参考。
访问权限
内部的系统模型列举如下:
- res.groups: groups相关字段: name, implied_ids, users
- res.users: users相关字段:name, groups_id
- ir.model.access: 访问控制相关字段: name, model_id, group_id, perm_read, perm_write, perm_create, perm_unlink
- ir.access.rule: 记录规则相关字段: name, model_id, groups, domain_force
大部分相关权限组的XML ID列举如下:
- base.group_user: internal user—任意后台用户
- base.group_system: Settings—管理员属于这个分组
- base.group_no_one: technical feature, 通常用于对用户隐藏功能
- base.group_public: Public, 用于让对网站匿名用户开放
由Odoo提供的默认用户的XML ID列举如下:
- base.user_root: 系统超级用户,也称为OdooBot
- base.user_admin: 默认用户,默认名为Administrator
- base.default_user: 后台新用户使用的模板。这是一个模板并且不可使用,但可以复制它来新建用户
- base.default_public user: 用于新建门户用户的模板
小结
本文中我们从0开始创建了一个新模块,了解了模块中常用的组件:模型、访问权限、菜单、三个基础视图类型(表单视图、列表视图和搜索视图)以及模型方法中的业务逻辑。我们还学习了如何使用网页控制器和QWeb模板创建网页。
在学习过程中,我们熟悉了模块开发流程,包括模块升级和应用服务重启来使修改在 Odoo 中生效。
不要忘记在添加模型字段时需要进行升级。修改含声明在内的 Python 文件需要重启服务。修改XML或CSV文件需进行升级;一旦不确定,同时进行重启服务和升级模块操作。
我们已经学习创建 Odoo 应用的基本元素和步骤,但大多数情况下,我们的模块都是扩展已有应用添加功能,我们将在下一篇文章中一起学习。
扩展阅读
本文中涉及到的所有课题在后续章节都会深入介绍。
官方文档中的相关资源可以作为补充阅读:
- 创建模块课程
- Odoo 指南中的一系列编码规范和模块开发指南
- Odoo 社区联盟(OCA)指南是指导 Odoo 开发最佳实践很好的资源
学习 Python 对 Odoo 开发来说也非常重要,在Packt 书录中有一些很好的 Python 图书,如Learn Python Programming – Second Edition。
注:本博客新增精通Python自动化脚本-运维人员宝典可用于深入Python 脚本的学习。