Odoo15-开发精要-全-

Odoo15 开发精要(全)

原文:zh.annas-archive.org/md5/ffff8bd6d54e17cc8574eee77559e638

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:第三章:您的第一个 Odoo 应用程序

在 Odoo 中进行开发通常意味着创建我们自己的模块。在本章中,我们将创建我们的第一个 Odoo 应用程序,学习使其对 Odoo 可用所需的步骤,并安装它。

我们将通过学习开发工作流程的基础知识开始,我们将创建并安装一个新的模块,并在开发迭代过程中应用我们做出的更改。

Odoo 遵循类似于模型-视图-控制器MVC)的架构,我们将遍历不同的层来实现一个库应用程序。

在本章中,我们将涵盖以下主题:

  • 库项目的概述

  • 第 1 步 – 创建新的addon模块

  • 第 2 步 – 创建新应用程序

  • 第 3 步 – 添加自动化测试

  • 第 4 步 – 实现模型层

  • 第 5 步 – 设置访问安全

  • 第 6 步 – 实现后端视图层

  • 第 7 步 – 实现业务逻辑层

  • 第 8 步 – 实现网站用户界面UI

采用这种方法,您将能够逐步了解构成应用程序的基本构建块,并体验从头开始构建 Odoo 模块的迭代过程。

技术要求

本章要求您已安装 Odoo 服务器,并能够从命令行启动它以执行安装模块或运行测试等操作。如果您没有可用的 Odoo 开发环境,请确保您已审查第二章准备开发环境

在本章中,我们将从一张白纸开始创建我们的第一个 Odoo 应用程序,因此我们不需要任何额外的代码来开始。

本章的代码可以在本书的 GitHub 仓库中找到,网址为github.com/PacktPublishing/Odoo-15-Development-Essentials,在ch03目录下。

库项目的概述

我们将使用学习项目来更好地探索本章中解释的主题,并看到它们在实际中的应用。我们将创建一个新的 Odoo 应用程序来管理图书库。我们将在这个项目中使用所有后续章节,其中每个章节都是一个迭代,向应用程序添加功能。在这里,我们将创建图书馆应用程序的第一个迭代。

我们将实现的第一项功能将是图书目录。目录使我们能够记录图书馆中的书籍及其相关细节。我们还希望通过一个公开网站提供这个目录,以便可以看到可用的书籍。

图书馆书籍应具有以下数据:

  • 标题

  • 作者

  • 出版社

  • 发布日期

  • 封面图片

  • 国际标准书号ISBN),带有校验位验证

  • 激活标志,指示应在网站上公开可用的书籍

对于 Odoo 基础应用来说,图书馆应用将有两个用户组,图书馆用户和图书馆管理员。预期用户级别能够执行所有日常操作,而管理员级别则预期能够额外编辑应用配置。

对于图书目录功能,我们将继续编辑图书记录作为管理员保留的功能。以下应适用:

  • 图书馆管理员应该能够编辑书籍。

  • 图书馆用户和通过网站使用的公共用户应该只能查看书籍。

这个简单的项目将使我们能够涵盖构建 Odoo 应用所涉及的所有主要组件。第一步是创建一个模块目录,该目录将托管我们应用的代码和组件。

第一步 – 创建新的插件模块

插件模块是一个包含实现某些 Odoo 功能的文件的目录。它可以添加新功能或修改现有功能。插件模块目录必须包含一个名为__manifest__.py的清单文件或描述文件。

一些模块插件被作为应用。应用是 Odoo 中功能区域的顶级模块,我们期望我们的模块在顶级应用菜单中有所体现。基础 Odoo 中的应用示例包括CRM项目HR。非应用模块插件预期将依赖于一个应用,向其添加或扩展功能。

如果新模块为 Odoo 添加了新的或主要的功能,它可能应该是一个应用。如果模块只是对现有应用进行了更改,它可能应该是一个常规的插件模块。

要开发新模块,我们将执行以下操作:

  1. 确保我们将工作的目录位于 Odoo 服务器插件路径中。

  2. 创建模块的目录,包含清单文件。

  3. 如果我们打算分发模块,请选择模块的许可证。

  4. 添加模块描述。

  5. 可选地,添加一个图标以代表该模块。

在此之后,我们可以安装该模块以确认它对 Odoo 服务器可用并且安装正确。

准备插件路径

插件模块是一个包含 Odoo清单文件的目录,提供了诸如新应用或现有应用的附加功能等特性。插件目录包含多个插件模块。插件路径是 Odoo 配置的一部分,列出了 Odoo 服务器将查找可用插件的目录列表。

默认情况下,插件路径包括与 Odoo 捆绑的基础应用,位于odoo/addons目录中,以及提供核心功能的基础模块,位于odoo/odoo/addons目录中。插件路径通常修改为添加一个或多个我们想要使用的自定义开发和社区模块的目录。

图书馆项目将由几个模块组成。这样做是一个好习惯,因为它促进了更小、更专注的模块,有助于降低复杂性。我们将为项目的模块创建一个插件目录。

如果遵循了 第二章准备开发环境,的说明,Odoo 服务器代码应在 ~/work15/odoo/。自定义插件模块应保存在它们自己的目录中,与 Odoo 代码分开。

对于图书馆,我们将创建一个 ~/work15/library 目录,并将其包含在插件路径中。我们可以通过直接编辑配置文件或使用 Odoo 命令行界面CLI)来完成此操作。以下是后者的操作方法:

$ 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 选项用于设置要使用的配置文件。

  • -c 选项一起使用的 --save 选项将保存配置文件中使用的选项。

  • --stop 选项,即 --stop-after-init,在所有操作完成后停止 Odoo 服务器,并返回到命令行。

如果使用了相对路径作为插件路径选项,Odoo 将在将其存储到配置文件之前将它们转换为绝对路径。

Odoo 15 的变化

创建的配置文件将使用默认配置作为模板。在 Linux 系统中,默认配置文件是位于 ~/.odoorc 的文件。

Odoo 的 scaffold 命令提供了一种快速创建新模块骨架的方法。我们可以用它来填充 library 插件目录,使其包含一个有效的模块。要构建 library_app 模块目录,请执行以下代码:

(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 服务器能够找到其中的模块。

在上一节中,我们也使用了 Odoo scaffold命令来自动创建新library_app模块目录的骨架结构,其中已经放置了基本结构。记住scaffold命令,它看起来像这样:odoo scaffold <module> <addons-directory>。创建的模块目录看起来像这样:

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 标识符ID)——它应该以字母开头,并且只能包含字母、数字和下划线字符。

它包含几个子目录,用于模块的不同组件。这种子目录结构不是必需的,但它是一个广泛使用的约定。

一个有效的 Odoo 插件模块目录必须包含一个__manifest__.py描述文件。它还需要是 Python 可导入的,因此它还必须有一个__init__.py文件。这两个文件是我们首先在目录树中看到的。

小贴士

在较旧的 Odoo 版本中,模块描述文件被命名为__openerp__.py。这个文件名仍然被支持,但已弃用。

描述文件包含一个 Python 字典,其中包含描述模块的属性。scaffold 自动生成的描述文件应该类似于以下内容:

{
    '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文件:

from . import controllers
from . import models

在顶级没有 Python 文件,有两个包含 Python 文件的子目录,controllersmodels。查看模块树,我们可以看到这两个目录包含 Python 文件和各自的__init__.py文件。

创建描述文件

scaffold命令准备了一个描述文件,可以用作指南,或者我们可以从一个空文件创建描述文件。

描述文件应该是一个有效的 Python 文件,包含一个字典。没有可能的字典键是必需的,因此空字典{}将是文件的有效内容。在实践中,我们至少想要提供一些关于模块的基本描述,声明作者身份,并选择一个分发许可。

以下是一个良好的起点:

{
    "name": "Library Management",
    "summary": "Manage library catalog and book lending.",
    "author": "Daniel Reis",
    "license": "AGPL-3",
    "website": "https://github.com/PacktPublishing"
               "/Odoo-15-Development-Essentials",
    "version": "15.0.1.0.0",
    "depends": ["base"],
    "application": True,
}

这里使用的键提供了在应用表单主选项卡中展示的所有数据,如下面的截图所示:

图 3.1 – 图书馆管理模块应用表单

图 3.1 – 图书馆管理模块应用表单

我们使用了以下键:

  • name: 模块的标题。

  • summary: 模块目的的一行总结。

  • author: 版权作者的姓名。它是一个字符串,但可以包含以逗号分隔的姓名列表。

  • license: 这标识了作者允许模块以何种许可进行分发。AGPL-3LGPL-3 是流行的开源选择。通过 Odoo Apps Store 销售的专有模块通常使用 OPL-1 Odoo 专有许可。许可将在本章后面更详细地讨论。

  • website: 获取有关模块更多信息的一个 统一资源定位符 (URL)。这可以帮助人们找到更多文档或问题跟踪器来提交错误和建议。

  • version: 模块的版本。它应遵循语义版本控制规则(详情见 semver.org/)。使用 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,并用于 粘合 模块。一旦所有依赖项安装完毕,就会触发粘合模块的安装。例如,这可以用于在两个应用程序都安装在同一实例中时自动提供连接两个应用程序的功能。

设置模块类别

模块被分组到类别中,代表它们相关的功能区域。这些类别用于分组插件模块,以及安全组。

如果插件没有设置类别,将分配未分类值。目前,这是图书馆应用的类别。

我们可以在 Odoo 的应用菜单中看到几个类别,在左侧面板上。在那里,我们可以看到可以用于我们的模块的类别,如下面的屏幕截图所示:

图 3.2 – 带有类别窗格的应用列表

图 3.2 – 带有类别窗格的应用列表

类别可以有层次结构——例如,项目应用属于服务/项目类别。

如果在插件模块中使用了一个不存在的类别,Odoo 将自动创建它并使其可用。我们将利用这一点为图书馆应用创建一个新的类别:服务/图书馆

修改__manifest__.py文件,以便添加一个category键:

    "category": "Services/Library", 

类别对于组织安全组也很重要,并且要在可扩展标记语言XML)数据文件中引用它们,我们需要使用相应的 XML ID。

分配给模块类别的 XML ID 是从base.module_category_前缀加上类别名称自动生成的。例如,对于base.module_category_services_library

我们可以通过导航到相应的表单视图,然后在开发者菜单中使用查看元数据选项来确认应用类别的 XML ID。

没有应用类别的菜单项,但可以从安全表单访问类别表单,如下所示:

  1. 打开设置 | 用户 | 菜单选项,创建一个新的测试记录。

  2. 应用字段下拉列表中选择一个选项,并保存。这个过程在下面的屏幕截图中展示:图 3.3 – 应用选择列表,在用户组表单中

    图 3.3 – 应用选择列表,在用户组表单中

  3. 点击应用链接以打开所选类别的对应详情表单。

  4. 在类别表单中,在开发者菜单中选择查看元数据选项,以查看分配给它的 XML ID。

  5. 如果你不再需要测试组,你可以选择删除它。

或者,可以在 Odoo 源代码中找到内置类别列表及其 XML ID。GitHub URL 如下:github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml.

选择许可证

为你的作品选择一个许可证非常重要,你应该仔细考虑哪个选项最适合你,以及它的含义。

软件代码受版权法保护,保留作者使用或修改作品的权利。这通常意味着你个人或你所在的公司。为了使其他方能够安全地使用作品,他们必须与代码作者签订许可协议。

如果你希望你的代码可以自由获取,它需要携带一个许可证,说明其他人可以对你的代码做什么。不同的许可证将规定不同的条款。

Odoo 模块最常用的许可证是 GNU's Not UnixGNULesser General Public LicenseLGPL-3) 和 Affero General Public LicenseAGPL-3)。两者都允许你自由分发和修改作品,只要作者得到认可,并且派生作品在相同的许可证条件下分发。

AGPL 是一种强大的开源许可证,要求使用该代码的在线服务必须将源代码与用户共享。这种许可证在社区中很受欢迎,因为它强制派生作品也必须在 AGPL 条件下分发。因此,开源代码不能被纳入封闭的商业解决方案中,原始作者可以从其他人的改进中受益。

LGPL 比 AGPL 更为宽松,也允许进行商业修改,无需共享相应的源代码。这种许可证通常用于网页和系统集成组件,其中解决方案可能包含在私人许可证下或与 AGPL 不兼容的条款下的组件。

你可以在这里了解更多关于 GNU 许可证的信息:www.gnu.org/licenses/

虽然你可以出售 GPL 许可证的软件,但由于该许可证允许人们自由复制和分发代码,这并不是一个流行的商业模式。因此,Odoo 应用商店中出售的许多模块更喜欢使用专有许可证。为此,Odoo 提出了 Odoo 专有许可证 OPL-1

关于 Odoo 许可证的更多详细信息,请参阅 www.odoo.com/documentation/user/legal/licenses/licenses.html

添加描述

描述是一段长文本,用于介绍模块功能。描述文本支持 reStructuredTextRST) 格式,以生成丰富的文本文档。

你可以在这里了解更多关于 RST 的信息:docutils.sourceforge.io/rst.html。页面包括一个值得书签的快速参考链接:docutils.sourceforge.io/docs/user/rst/quickstart.html

这里是一个 RST 文档的简短示例:

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 文件,以便访客可以轻松找到模块的介绍。因此,而不是使用description清单密钥,Odoo 模块可以有一个README.rstREADME.md文件用于相同的目的。此文件应放置在模块目录的根目录中,与__manifest__.py文件并列。

另一个选择是提供一个位于static/description/模块子目录中的index.html HTML 文件。页面资源,如图像和层叠样式表CSS),也应位于同一目录。

注意

对于将application密钥设置为True的模块,仅使用index.html描述,并忽略描述密钥。

添加图标

模块可以可选地有一个代表它们的图标。在创建新应用的情况下,这一点尤为重要,因为应用预期将在应用菜单中有一个图标。

要添加图标,我们需要在模块中添加一个static/description/icon.png文件,其中包含要使用的图标。

对于图书馆应用项目,我们将重用library_app/static/description目录中的一个图标。

从命令行,我们会运行以下命令:

$ cd ~/work15/library/library_app
$ mkdir -p ./static/description
$ cp ~/work15/odoo/addons/note/static/description/\
icon.png ./static/description/

安装新模块

我们现在有一个最小的addon模块。它还没有实现任何功能,但我们可以安装它以确认它到目前为止运行正常。

要安装一个新的模块,我们应该使用 -d-i 选项启动服务器。-d--database 选项确保我们正在使用正确的 Odoo 数据库。-i--init 选项接受一个由逗号分隔的模块列表,用于安装。

Odoo 11 中的变化

当安装新模块时,Odoo 会自动从当前配置的插件路径中更新可用模块的列表。在 Odoo 10 之前并非如此,那时在安装新的插件模块之前需要手动更新模块列表。模块列表在 Web 客户端中更新,从应用列表中的菜单选项。

对于本章中较早准备好的 Odoo 环境,并且已经激活了 Python 虚拟环境,以下命令安装了library_app模块:

(env15)$ odoo -c ~/work15/library.conf -d library -i \
library_app

我们添加了 -d 库选项以确保为安装选择正确的数据库。可能的情况是,此选项已在配置文件中定义,因此是多余的。即使如此,最好还是谨慎行事,并在命令中明确声明要安装的数据库。

小贴士

仔细关注服务器日志消息以确认模块已被正确找到并安装。你应该看到一个odoo.addons.base.models.ir_module: ALLOW access to module.button_install消息,没有警告。

为了使模块安装成为可能,模块所在的 addons 目录应该为 Odoo 服务器所知。这可以通过停止和启动 Odoo 服务器,查看 Odoo 启动序列期间打印的 odoo: addons paths: 日志消息来确认。

如果找不到模块,通常是因为插件路径不正确。通过仔细检查正在使用的插件路径来双重确认这一点。

升级模块

开发模块是一个迭代过程,对源文件所做的更改然后将应用于 Odoo。

这可以通过 图形用户界面GUI)完成,通过在 应用 列表中查找模块并点击 升级 按钮来实现。这将重新加载数据文件,应用所做的更改,并更新数据库模式定义。然而,当更改仅限于 Python 逻辑时,升级可能不足以完成。可能需要重启 Odoo 服务器以重新加载更改后的 Python 代码。当模块更改涉及数据文件和 Python 逻辑时,可能需要执行这两个操作。

总结来说,以下适用:

  • 当修改模型或其字段时,需要升级以应用数据库模式更改。

  • 当更改 Python 代码的逻辑时,需要重启以重新加载代码文件。

  • 当更改 XML 或 逗号分隔值CSV)文件时,需要升级以重新应用文件中的数据。

为了避免将代码更改应用于 Odoo 相关的任何混淆或挫败感,最简单的解决方案是在代码更改完成后,使用模块 upgrade 命令重启 Odoo 服务。

在运行服务器实例的终端中,使用 Ctrl + C 停止它。然后,启动服务器并使用以下命令升级 library_app 模块:

(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 代码重新加载。有关此选项的更多详细信息,请参阅 第二章使用服务器开发选项 部分,准备开发环境

我们现在有一个模块目录,准备托管实现应用的组件。由于这是一个应用,而不是添加功能的纯技术模块,我们将首先添加一些应用预期的基本组件。

第 2 步 – 创建新应用

一些 Odoo 模块创建新的应用,而另一些则添加功能或修改现有应用。虽然涉及的技术组件大致相同,但一个应用应包括一些特征元素。由于图书馆模块是一个新应用,我们应该在我们的模块中包含它们。

一个应用应该具备以下功能:

  • 一个图标,用于在应用列表中展示

  • 一个顶级菜单项,所有应用菜单项都将放置于此之下

  • 为应用设置安全组,以便它只能对需要它的用户启用,并且在此处设置访问安全。

应用图标是模块的static/description/子目录中的一个icon.png文件。这已经在添加图标部分中完成。

接下来,我们将处理应用的高级菜单。

添加顶级菜单项

由于我们正在创建一个新应用,它应该有一个主菜单项。在社区版Community Edition (CE))中,这显示为右上角下拉菜单中的一个新条目。在企业版Enterprise Edition (EE))中,它显示为应用切换器主菜单中的一个附加图标。

菜单项是通过 XML 数据文件添加的视图组件。要定义一个菜单项,创建一个views/library_menu.xml文件,内容如下:

<odoo> 
  <!-- Library App Menu -->
  <menuitem id="menu_library" name="Library" /> 
</odoo> 

UI,包括菜单选项和操作,是数据库存储的记录,由 Web 客户端实时读取和解释。

所述文件描述了要加载到 Odoo 数据库中的记录。《元素是在ir.ui.menu`模型上写入记录的指令,其中 Odoo 菜单项被存储。

id属性也称为menu_library XML ID。

这里添加的菜单项非常简单,只使用了一个属性:name。还有其他可用的属性我们没有在这里使用。我们将在本章的实现后端视图层部分中了解更多关于它们的信息。

图书馆模块尚不了解这个新的 XML 数据文件。为了使其被识别并加载到 Odoo 实例中,需要在清单文件的data属性中声明。编辑__manifest__.py文件字典以添加此键,如下所示:

    "data": [
        "views/library_menu.xml",
    ],

data清单键是模块在安装或升级时需要加载的数据文件的列表。文件路径相对于清单文件所在的模块根目录。

要将这些菜单配置加载到我们的 Odoo 数据库中,我们需要升级模块。在此阶段这样做不会产生任何可见效果。此菜单项还没有可操作的子菜单,因此不会显示。一旦我们添加子菜单和相应的访问权限,它将变得可见。

小贴士

菜单树中的项目只有在存在可见的子菜单项时才会显示。打开视图的较低级菜单项只有在用户有权访问相应的模型时才会可见。

添加安全组

在常规用户可以使用功能之前,必须授予他们访问权限。在 Odoo 中,这是通过使用安全来完成的。访问权限授予给安全组,用户被分配到安全组。

Odoo 应用通常提供两个组,用于两个级别的访问,如下所示:

  • 用户访问级别,用于执行日常操作的用户

  • 管理员访问级别,具有对所有功能(包括配置)的完全访问权限

图书馆应用将具有这两个安全组。我们将在下一部分进行工作。

与安全相关的文件通常保存在一个security/模块子目录中,因此我们应该为这些定义创建一个security/library_security.xml文件。

安全组按照用于附加模块的相同类别组织。要将一个类别分配给安全组,我们应该找到相应的 XML ID。关于如何找到这个 XML ID,在本章的设置模块类别部分已经讨论过。在那里,我们可以了解到base.module_category_services_library的 XML ID。

接下来,我们将添加图书馆用户安全组。它属于module_library_category,并且将继承内部用户安全权限,在此基础上构建。如果我们打开该组的表单并使用开发者菜单base.group_user

现在,向security/library_security.xml文件添加以下 XML:

<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 正在向res.groups模型添加一条记录。这条记录有三个字段的值,如下所示:

  • name是组标题。这是一个简单的字符串值。

  • category_id是相关的应用程序。它是一个关系字段,因此使用ref属性将其链接到之前创建的类别,使用其 XML ID。

  • implied_ids是一个一对多关系字段,包含将也适用于属于此组的用户的组列表。多对多字段使用在第五章中详细说明的特殊语法,导入、导出和模块数据。在这种情况下,我们使用代码4来添加对现有内部用户 XML ID,base.group_user的链接。

    Odoo 12 中的变化

    用户表单有一个用户类型部分,仅在开发者模式启用时可见。它允许我们在互斥选项之间进行选择——内部用户门户(外部用户,如客户)和公共(网站匿名访客)。这是为了避免在之前的 Odoo 版本中发现的配置错误,其中内部用户可能会意外地包含在门户公共组中,从而实际上减少了他们的访问权限。

接下来,我们将创建一个管理组。它应该给我们用户组的所有权限,以及一些保留给管理员的额外访问权限。因此,我们希望它从library_group_user库用户继承。

编辑security/library_security.xml文件,在<odoo>元素内添加以下 XML:

    <!-- 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'))]"/>
    </record>

这里,我们同样看到了namecategory_idimplied_ids字段,就像之前一样。implied_ids字段被设置为与图书馆用户组的链接,以继承其权限。

这也在users字段上设置了值。这个组被分配给了管理员(admin)和 Odoobot 用户。

Odoo 12 的变化

自 Odoo 12 以来,我们有一个系统根用户,它不会显示在用户列表中,并在需要权限提升时(sudo)由框架内部使用。admin 用户可以用来登录服务器,并且应该能够访问所有功能,但与系统根用户一样,它绕过了访问安全。在 Odoo 11 版本之前,admin 用户也是内部根超级用户。

我们还需要在清单文件中添加这个额外的 XML 数据文件:

    "data": [
        "security/library_security.xml",
        "views/library_menu.xml",
    ], 

注意,library_security.xml文件是在library_menu.xml之前添加的。由于引用只能使用已定义的 ID,所以加载数据文件的顺序很重要。通常,菜单项会引用安全组,因此,在菜单和视图定义之前添加安全定义是一种良好的实践。

下一步是添加定义应用模型的 Python 代码。但在那之前,我们将添加一些测试用例,遵循测试驱动开发TDD)的方法。

第 3 步 – 添加自动化测试

编程最佳实践包括为你的代码编写自动化测试。这对于像 Python 这样的动态语言来说尤为重要——由于没有编译步骤,你无法确定在解释器运行代码之前是否存在语法错误。一个好的编辑器可以帮助我们提前检测到一些这些问题,但它不能像自动化测试那样帮助我们确保代码按预期执行。

TDD 方法指出,我们应该先编写测试,检查它们是否失败,然后开发代码,最终应该通过这些测试。受这种方法的启发,我们现在将在添加实际功能之前添加我们的模块测试。

Odoo 支持基于 Python 内置的unittest库的自动化测试。在这里,我们将简要介绍自动化测试,更详细的解释可以在第八章中找到,业务逻辑 – 支持业务流程

Odoo 12 的变化

直到 Odoo 11,测试也可以使用YAML Ain't Markup LanguageYAML)数据文件来描述。在 Odoo 12 中,YAML 数据文件支持被移除,因此这种测试方式不再可用。

测试需要满足一些要求,以便测试运行器能够找到并执行它们,如下所述:

  1. 测试被放置在tests/子目录中。与常规模块 Python 代码不同,这个目录不需要被导入到顶级__init__.py文件中。测试运行引擎将在模块中查找这些测试目录,然后运行它们。

  2. 测试代码文件应该以 test_ 开头,并从 tests/__init__.py 中导入。测试代码将位于从 odoo.tests.common 导入的 Odoo 框架中可用的几个测试对象之一派生的类中。最常用的测试类是 TransactionCase。测试对象使用 setUp() 方法来初始化测试用例所需的数据。

  3. 每个测试用例都是一个以 test_ 开头名称的方法。对于 TrasactionCase 测试对象,每个测试都是一个独立的交易,在开始前运行设置步骤,并在结束时回滚。因此,下一个步骤将看不到前一个测试所做的更改。

    小贴士

    测试可以使用演示数据来简化设置阶段,但这不是一种好做法,因为在这种情况下,测试用例只能在安装了演示数据的数据库中运行。如果所有测试数据都在测试设置中准备,那么测试可以在任何数据库中运行,包括空数据库或生产数据库的副本。

我们计划我们的应用程序有一个 library.book 模型。让我们添加一个简单的测试来确认新书是否正确创建。

添加测试用例

我们将添加一个简单的测试来检查书籍的创建。为此,我们需要添加一些设置数据并添加一个测试用例。测试用例将仅确认 active 字段具有预期的默认值,即 True

要完成这个任务,请按照以下步骤操作:

  1. 添加一个包含以下代码的 tests/__init__.py 文件:

    from . import test_book
    
  2. 然后,添加实际的测试代码,可在 tests/test_book.py 文件中找到,如下所示:

    from odoo.tests.common import TransactionCase 
    class 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 字段是否具有预期的默认值。在测试用例中而不是在设置方法中创建书籍是有意义的。我们选择不这样做的原因是我们还想使用这本书进行其他测试用例,而在设置中创建它避免了代码的重复。

运行测试

在安装或升级模块时,通过使用 --test-enable 选项启动服务器来运行测试,如下所示:

(env15) $ odoo -c ~/work15/library.conf -u library_app \
--test-enable

Odoo 服务器将在升级的模块中查找 tests/ 子目录,并运行它们。在这个阶段,预期测试将抛出错误,因此你应该在服务器日志中看到与测试相关的 ERROR 消息。这将在我们向模块添加书籍模型后改变。

现在,我们应该添加对业务逻辑的测试。理想情况下,我们希望每一行代码至少有一个测试用例覆盖。

测试业务逻辑

我们计划对有效的 ISBN 进行逻辑检查。因此,我们将添加一个测试用例来检查该方法是否正确验证了第一版 Odoo 开发基础 书籍的 ISBN。检查将通过一个 _check_isbn() 方法实现,返回 TrueFalse

tests/test_book.py 中,在 test_create() 方法之后添加几行代码,如下所示:

    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方法,如下所示:

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"})

我们在setUp方法中添加了两行。第一行使用其 XML ID 查找admin用户记录,第二行修改了用于运行测试的环境self.env,将活动用户更改为admin用户。

对于我们已编写的测试,不需要进一步更改。它们将以相同的方式运行,但现在使用admin用户,因为环境已修改。

图书馆应用程序现在有几个基本测试,但它们失败了。接下来,我们应该添加实现功能的代码,以便测试通过。

第 4 步 - 实现模型层

模型描述和存储业务对象数据,例如客户关系管理CRM)机会、销售订单或合作伙伴(客户、供应商等)。模型描述了一个字段列表,也可以附加特定的业务逻辑。

模型数据结构和附加的业务逻辑使用从 Odoo 模板类派生的对象类用 Python 代码描述。一个模型映射到一个数据库表,Odoo 框架负责所有数据库交互,包括保持数据库结构与对象同步,以及将所有事务转换为数据库指令。负责这一功能的框架组件是对象关系映射ORM)组件。

我们的应用程序将用于管理图书馆,我们需要一个用于图书目录的模型。

创建数据模型

遵循 Odoo 开发指南,模型对应的 Python 文件应放置在models子目录中,并且每个模型应该有一个文件。因此,我们将在library_app模块中创建一个models/library_book.py文件。

小贴士

Odoo 官方编码指南可以在www.odoo.com/documentation/15.0/reference/guidelines.html找到。另一个相关的编码标准文档是Odoo 社区协会OCA)的编码指南,可以在 https://odoo-community.org/page/contributing 找到。

第一件事是确保我们的模块使用models/目录。这意味着当 Odoo 加载模块时,Python 应该导入它。为此,编辑模块的主要__init__.py文件,使其包含以下行:

from . import models

类似地,models/子目录应该包含一个__init__.py文件,用于导入要使用的代码文件。添加一个包含以下代码的models/__init__.py文件:

from . import library_book

接下来,我们可以创建一个包含以下内容的models/library_book.py文件:

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 代码import语句,用于使modelsfields Odoo 核心对象可用。

第二行声明了新的library.book模型。这是一个从models.Model派生的 Python 类。

下面的行是缩进的。Python 代码块由缩进级别定义,这意味着这些下一行是Book类定义的一部分。类名使用驼峰式命名法,这是 Python 的常用约定。实际使用的 Python 类名对 Odoo 框架来说并不重要。与 Odoo 相关的模型 ID 是下一行中声明的_name属性。

接下来的两行以下划线开头,并声明了一些 Odoo 类属性。_name属性定义了模型名称,使用点(.)来分隔它们的键词。

提示

模型 ID 使用点分隔的单词。其余所有内容使用下划线(_),例如插件模块名称、XML ID、表名称等。

然后,我们有_description模型属性。这是模型记录的显示名称,可以在某些用户消息中用来引用记录。这不是强制性的,但如果缺失,服务器日志中会显示警告信息。

最后七行声明了模型字段。我们可以看到最常用字段类型的示例。对于标量值,我们可以看到使用了CharBooleanDateBinary字段类型。对于关系字段,我们可以看到Many2oneMany2many

name字段用于数据记录标题——在这种情况下,是书名。

active字段用于活动记录。默认情况下,只显示活动记录,非活动记录会自动隐藏。这在主数据模型中很有用,可以隐藏不再使用但出于历史原因需要保留在数据库中的记录。

提示

nameactive是特殊的字段名称。默认情况下,Odoo 框架会对它们进行特殊处理。name字段默认用于记录显示名称,即当从其他模型引用记录时显示的文本。active字段用于从 UI 中过滤掉非活动记录。

publisher_id是本例中的res.partner合作伙伴模型。它用于引用出版公司。对于多对一字段名称的约定是以_id结尾。

author_idsres.partner合作伙伴模型。在数据库级别,这些数据实际上并没有存储在表字段中,而是在一个辅助表中存储,该表是自动创建的,用于存储两个表中的记录之间的关系。对于多对多字段名称的约定是以_ids结尾。

这两种关系都是书和合作伙伴模型之间的关系。合作伙伴模型内置在 Odoo 框架中,是存储人员、公司和地址的地方。我们正在使用它来存储我们的出版商和作者。

现在,我们通过升级图书馆应用程序模块在 Odoo 中使这些更改生效。同样,这是我们可以运行的命令来更新library数据库上的library_app模块:

(env15)$ odoo -c ~/work15/library.conf -d library \
-u library_app

目前还没有菜单项可以访问书籍模型。这些将在本章后面添加。尽管如此,为了检查新创建的模型并确认它已在数据库中正确创建,我们可以使用library.book模型并点击它以查看其定义,如图下截图所示:

![图 3.4 – 技术菜单中的 library.book 模型视图

![img/Figure_3.4_B16119_B16119.jpg]

图 3.4 – 技术菜单中的 library.book 模型视图

我们应该能够看到列出的模型,并确认它包含我们在 Python 文件中定义的字段。如果您看不到这些,请尝试再次重启服务器并升级模块,并密切注意服务器日志,寻找加载图书馆应用程序的消息以及任何报告 Odoo 数据库问题的警告。

library.book字段列表中,我们可以看到一些我们没有声明的额外字段。这些是 Odoo 自动添加到每个模型中的特殊字段。如下所示:

  • id是每个记录的唯一数字数据库 ID。

  • create_datecreate_uid是记录创建的时间戳和创建记录的用户。

  • display_name为记录提供文本表示——例如,当它在其他记录中引用时。它是一个计算字段,默认情况下,如果可用,仅使用name字段中的文本。

  • write_datewrite_uid是记录的最后修改时间戳和执行该更新的用户。

  • __last_update是一个不存储在数据库中的计算字段,用于并发检查。

书籍模型现在已在数据库中创建,但尚未对用户可用。我们需要一个菜单项来实现这一点,但这还不够。为了使菜单项可见,用户首先需要被授予对新模型的访问权限。

第 5 步 – 设置访问安全

library.book模型已在数据库中创建,但您可能已经注意到,当它被加载时,它会将此警告消息打印到服务器日志:

 The model library.book has no access rules, consider adding one. 

消息非常明确——新的模型没有访问规则,因此目前任何人还不能使用它。之前,我们为这个应用程序创建了安全组,我们现在需要给他们提供对应用程序模型的访问权限。

Odoo 12 中的更改

admin用户遵循访问安全规则,就像任何其他用户一样,除了类似 root 的内部超级用户。在它可以使用之前,我们需要授予它对新模型的访问权限。在 Odoo 11 之前,这种情况并不存在。在这些早期的 Odoo 版本中,admin用户也是内部超级用户,并绕过了访问安全规则。这意味着新创建的模型自动对其可用并可使用。

添加访问控制安全

要了解添加访问规则到模型所需的信息,在 Web 客户端,导航到设置 | 技术 | 安全 | 访问权限,如图下所示:

![图 3.5 – 技术菜单中的访问权限列表]

图 3.5_B16119_B16119.jpg

图 3.5 – 技术菜单中的访问权限列表

这些访问权限也被称为访问控制列表ACL。在之前的屏幕截图中,我们可以看到一些模型的 ACL。它表示对于安全组,在记录上允许执行哪些类型的操作:读取、写入、创建和删除。

Odoo 14 中的更改

临时模型,用于交互式向导,现在也需要为用户组提供访问权限。在之前的 Odoo 版本中,情况并非如此,用户默认可以访问这些模型。建议授予读取、写入和创建权限,不授予删除/解除链接权限(CSV 文件上的1,1,1,0)。

对于图书馆应用程序,我们将授予图书馆用户读取、写入和创建书籍记录的访问权限,并授予图书馆管理员完全访问权限,包括删除记录。

这些数据可以由模块数据文件提供,将记录加载到ir.model.access模型中。CSV 数据文件的名称必须与我们加载数据的模型 ID 匹配。

因此,我们应该添加security/ir.model.access.csv文件,内容如下:

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是记录的外部 ID(也称为 XML ID)。在我们的模块中应该是唯一的。

  • name是一个描述性标题。它具有信息性,并且建议它是唯一的。

  • model_id是我们赋予访问权限的模型的 XML ID。模型由 ORM 自动生成 XML ID;对于library.book,ID 是model_library_book

  • group_id标识要赋予权限的安全组。我们授予了在之前创建的安全组:library_group_userlibrary_group_manager的访问权限。

  • perm_...字段授予对readwritecreateunlink(删除)操作的访问权限。我们使用1表示yes/true,使用0表示no/false

我们不要忘记在__manifest__.py文件中的data键中引用这个新文件。它应该看起来像这样:

    "data": [
        "security/library_security.xml",
        "security/ir.model.access.csv",
        "views/library_menu.xml",
    ],

如前所述,升级模块以使这些更改在 Odoo 数据库中生效。警告信息应该已经消失。

到目前为止,书籍模型是可用的,并且应该对 admin 用户可访问。因此,我们的第一个测试应该通过。现在让我们运行它,如下所示:

(env15) $ odoo -c ~/work15/library.conf -u library_app --test-enable

我们应该看到一个测试通过,一个测试失败。

ACL 访问权限 选项在模型级别授予权限,但 Odoo 还支持行级访问安全,通过 记录规则。此功能将在下一节中解释。

行级访问规则

记录规则定义了限制安全组可以访问的记录的过滤器。例如,销售人员可能只能看到他们自己的报价,或者会计用户可能只能看到他们被授予访问权限的公司会计分录。

为了展示这个功能,我们将限制图书馆用户不能查看非活动书籍。默认情况下,这些书籍是隐藏的,但如果我们使用 active 等于 True 的条件过滤记录,它们仍然可以访问。

ir.rule 模型。

需要在此概述的记录规则定义字段:

  • name:一个独特的标题,最好是唯一的。

  • model_id:规则应用的模型的引用。

  • groups:规则应用的权限组的引用。此字段是可选的,如果没有设置,则被视为全局规则(global 字段自动设置为 True)。全局规则的行为不同——它们施加的限制是非全局规则无法覆盖的。它使用特定的语法来写入多对多字段。

  • domain_force:用于访问限制的域过滤器,使用 Odoo 中用于过滤表达式的元组列表语法。

要向图书馆应用添加记录规则,编辑 security/library_security.xml 文件,在 </odoo> 最终标签之前添加第二个 <data> 部分,如下所示:

  <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"。第二种方法是在不升级的情况下重新安装模块。这在命令行中是可能的,使用 -i 而不是 -u 在已安装的模块上。

groups字段是多对多关系,并使用用于多对多字段的特殊语法。它是一个元组列表,其中每个元组是一个命令。在这种情况下,使用了(4, x)命令,代码4表示下一个引用的记录要追加到值中。引用的记录是library_group_user,图书馆用户组。多对多字段的写法在第六章中更详细地讨论,模型 – 结构化应用程序数据

域表达式也使用特殊的语法,包含一个三元组列表,每个三元组指定一个过滤条件。域过滤语法在第七章中解释,记录集 — 与模型数据交互

现在用户可以访问书籍模型,我们可以继续添加 UI,从菜单项开始。

第 6 步 – 实现后端视图层

视图层描述了 UI。视图使用 XML 定义,该 XML 由 Web 客户端框架用于动态生成数据感知的 HTML 视图。

菜单项可以执行窗口动作以渲染视图。例如,用户菜单项处理一个名为用户的窗口动作,进而渲染一个视图组合,包括列表表单

可用几种视图类型。最常用的三种视图是列表(有时由于历史原因称为),表单和位于右上角搜索框中的搜索选项。

在接下来的几节中,我们将逐步改进,并需要频繁地升级模块以使它们可用,或者我们可以使用--dev=all服务器选项,这使我们免于在开发时进行模块升级。使用它,视图定义直接从 XML 文件中读取,所做的更改立即对 Odoo 可用,无需进行模块升级。在第二章中,准备开发环境,提供了关于--dev服务器选项的更多详细信息。

小贴士

如果模块升级因 XML 错误而失败,不要慌张!仔细阅读服务器日志中的错误消息。它应该指向问题所在之处。如果你感到困惑,只需注释掉最后编辑的 XML 部分或从__manifest__.py中删除 XML 文件,然后重复升级。然后服务器应该正确启动。

遵循 Odoo 开发指南,UI 的 XML 文件应位于views/子目录中。

让我们开始为我们的待办事项应用创建 UI。

添加菜单项

图书馆应用现在有了存储书籍数据的模型,我们希望它在 UI 上可用。首先要做的事情是添加相应的菜单选项。

编辑views/library_menu.xml文件,并添加窗口动作和书籍模型菜单项的记录,如下所示:

  <!-- Action to open the Book list --> 
  <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>
  <!-- Menu item to open the Book list -->
  <menuitem id="menu_library_book" 
    name="Books" 
    parent="menu_library"
    action="action_library_book"
  />

此数据文件描述了两个要添加到 Odoo 中的记录,如下所示:

  • <record>元素定义了一个客户端窗口动作,以启用treeform视图的顺序打开library.book模型。

  • Books<menuitem>,运行之前定义的action_library_book动作。

现在升级图书馆应用将使这些更改可用。可能需要刷新浏览器页面才能看到新的菜单项。一旦完成,Odoo 中的图书馆顶级菜单应该可用,并具有书籍子菜单选项。

尽管我们尚未定义我们的 UI 视图,Odoo 提供了自动生成的视图,使我们能够立即开始浏览和编辑数据。

点击图书馆 | 书籍菜单项将显示基本列表视图,点击创建按钮将显示如下表单:

图 3.6 – 图书馆书籍自动生成的表单视图

图 3.6 – 图书馆书籍自动生成的表单视图

Odoo 为我们提供了自动生成的视图,但它们并不那么出色。我们可能需要自己动手创建视图,从书籍表单视图开始。

创建表单视图

视图是存储在数据库中的数据记录,在ir.ui.view模型中。因此,我们需要添加一个数据文件,其中包含一个<record>元素来描述视图。

将此新的views/book_view.xml文件添加到定义表单视图:

<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记录有一个记录id字段,它定义了一个 XML ID,可以供其他记录引用。视图记录为三个字段设置值:namemodelarch

该视图是library.book模型,命名为Book Form。名称仅用于信息目的。它不必是唯一的,但它应该允许你轻松识别它所引用的记录。实际上,名称可以完全省略;在这种情况下,它将自动从模型名称和视图类型生成。

最重要的字段是arch,因为它包含实际的视图定义,这需要更仔细的检查。

视图定义的第一个元素是<form>标签。它声明了我们正在定义的视图类型以及应该包含在其内的剩余元素。

接下来,我们使用<group>元素在表单内定义部分,这些部分可以包含<field>元素或其他元素,包括嵌套的组元素。一个组添加了一个带有两列的不可见网格,对于字段来说非常合适,因为默认情况下,它们占用两列,一列用于标签文本,另一列用于输入字段。

我们简单的表单包含一个单独的<group>元素,并在其中为要展示的每个字段添加了一个<field>元素。字段自动使用适当的默认小部件,例如日期字段使用日期选择小部件。在某些情况下,我们可能想使用特定的小部件,添加widget属性。这就是author_ids使用小部件以标签列表的形式显示作者,以及image字段使用处理图像的适当小部件的情况。视图元素的详细解释见第十章后端视图 - 设计用户界面

记得将这个新文件添加到清单文件中的data键;否则,我们的模块将不知道它,并且它不会被加载。以下是您需要执行此操作的代码:

    "data": [
        "security/library_security.xml",
        "security/ir.model.access.csv", 
        "views/book_view.xml",
        "views/library_menu.xml",
    ], 

视图通常位于安全文件之后,菜单文件之前。

记住,为了将更改加载到我们的 Odoo 数据库中,需要模块升级。要在 Web 客户端看到更改,需要重新加载表单。可以再次点击打开它的菜单选项,或者重新加载浏览器页面(在大多数浏览器中为F5)。

商业文档表单视图

前一节提供了一个基本表单视图,但我们可以对其进行一些改进。对于文档模型,Odoo 有一个模仿纸张页面的展示风格。此表单包含两个顶部元素:一个<header>元素,用于包含操作按钮,以及一个<sheet>元素,用于包含数据字段。

我们可以使用这个,并使用上一节中定义的基本<form>元素来修改它:

<form> 
  <header> 
    <!-- Buttons will go here --> 
  </header> 
  <sheet> 
    <!-- Content goes here: --> 
    <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> 
  </sheet> 
</form> 

表单可以包含按钮,用于执行操作。这些按钮可以运行窗口动作,通常打开另一个表单,或者运行 Python 类方法。按钮可以放置在顶部的<header>部分内,或者表单内的任何位置。让我们看看如何。

添加操作按钮

我们将在页眉中展示一个按钮,用于检查书籍 ISBN 是否有效。这个功能的代码将位于我们命名为button_check_isbn()的书籍模型的方法中。

我们还没有添加方法,但我们可以先在表单中添加相应的按钮,如下所示:

<header> 
  <button name="button_check_isbn" type="object" 
          string="Check ISBN" /> 
</header> 

按钮的基本属性如下列出:

  • string:按钮上显示的 UI 文本

  • type:它执行的动作类型,objectaction

  • name:这是运行动作的 ID。对于objecttype是方法名称;对于action,这是动作记录 ID。

  • class:这是一个可选属性,用于应用 CSS 样式,就像常规 HTML 一样。

使用组来组织表单

<group>标签允许我们组织表单内容。一个<group>元素创建一个包含两列的不可见网格。添加到其中的字段元素将垂直堆叠,因为每个字段占用两个单元格——一个用于标签,另一个用于输入框。在<group>元素内部添加两个<group>元素将创建一个包含字段两列的布局。

我们将使用这个来组织书籍表单。我们将更改<sheet>内容以匹配以下内容:

<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属性,为它们分配一个 ID。这不是必需的,但建议这样做,因为它使得它们更容易被扩展视图引用。

完整的表单视图

到目前为止,书籍表单视图的 XML 定义应该看起来像这样:

<form> 
  <header> 
    <button name="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>顶级元素应包含作为列呈现的字段。

我们可以将以下<tree>视图定义添加到book_view.xml中:

<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> 

这定义了一个包含四个列的列表:nameauthor_idspublisher_iddate_published

在列表的右上角,Odoo 显示一个搜索框。它搜索的字段和可用的过滤器由一个<search>视图定义。

如前所述,我们将将其添加到book_view.xml中,如下所示:

<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_inactive"
              string="Inactive" 
              domain="[('active','=',True)]"/>
      <filter name="filter_active"
              string="Active"
              domain="[('active','=',False)]"/> 
    </search> 
  </field> 
</record> 

这个搜索视图正在使用两个不同的元素,<field><filter>

<field>元素定义了当用户在搜索框中键入时自动搜索的字段。我们添加了publisher_id以自动显示出版商字段的搜索结果。《filter>`元素添加了预定义的过滤器条件,这些条件可以通过用户点击切换。过滤器条件使用 Odoo 域过滤器语法。域过滤器在第十章中更详细地介绍,后端视图——设计用户界面

Odoo 12 中的更改

<filter>元素现在需要有一个name="..."属性,唯一标识每个过滤器定义。如果它缺失,XML 验证将失败,模块将无法安装或升级。

现在我们已经将图书馆应用程序的基本组件放好了——模型和视图层。接下来,我们添加业务逻辑层,添加将使检查 ISBN按钮工作的代码。

第 7 步 – 实现业务逻辑层

业务逻辑层支持应用程序的业务规则,例如验证和自动化。我们现在将为library.book模型添加逻辑。

添加业务逻辑

现代 ISBN 有 13 位数字,最后一位是计算自前 12 位的校验位。如果digits包含前 12 位数字,这段 Python 代码将返回相应的校验位:

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,并返回TrueFalse,如下所示:

    def _check_isbn(self):
        self.ensure_one()
        digits = [int(x) for x in self.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 中的更改

@api.multi装饰器已从 Odoo 应用程序编程接口API)中移除,无法使用。请注意,对于之前的 Odoo 版本,此装饰器是可用的,但不是必需的。添加或不添加它会产生完全相同的效果。

为了向用户报告验证问题,我们将使用 Odoo 的ValidationError异常,因此首先要做的是通过导入使其可用。编辑models/library_book.pyPython 文件,在文件顶部添加以下内容,如下所示:

from odoo.exceptions import ValidationError

接下来,仍然在models/library_book.py文件中,向Book类添加以下代码:

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 ISBN is invalid" % 
              book.isbn) 
    return True 

在这里,self代表一个记录集,我们可以遍历每个记录并对每个执行检查。

此方法用于self以表示单个记录,并且不需要使用for循环。实际上,我们使用_check_isbn()辅助方法做了类似的事情。如果您选择这样做,建议在方法开始时添加self.ensure_one(),以便在self不是单个记录的情况下尽早失败。

但我们选择使用for循环来支持多个记录,使我们的代码能够在将来需要时执行批量验证。

代码遍历所有选定的书籍任务记录,并对每个记录,如果书籍 ISBN 有值,它会检查其是否有效。如果不是,则会向用户发出警告信息。

Model方法不需要返回任何内容,但我们至少应该让它返回一个True值。原因是并非所有客户端的 XML-远程过程调用RPC)协议实现都支持 None/Null 值,当方法返回此类值时可能会引发错误。

这是个升级模块并再次运行测试的好时机,添加--test-enable选项以确认测试现在正在通过。您也可以尝试实时操作,进入书籍表单并尝试带有正确和错误 ISBN 的按钮。

图书馆应用具有我们想要在其第一次迭代中添加的所有后端功能,我们在多个层(模型、视图和业务逻辑)中实现了 Odoo 组件。但 Odoo 还支持创建面向外部的 Web 页面。在下一节中,我们将创建我们的第一个 Odoo 网站页面。

第 8 步 – 实现网站 UI

Odoo 还提供了一个 Web 开发框架,用于开发与后端应用紧密集成的网站功能。我们将通过创建一个简单的网页来显示图书馆中活跃的书籍列表来迈出第一步。

图书目录页面将在http://my-server/library/books地址响应网络请求,因此/library/books是我们想要实现的 URL 端点。

Web http.Controller派生类。该方法通过@http.route控制器绑定到一个或多个 URL 端点。当访问这些 URL 端点中的任何一个时,控制器代码将执行并返回要向用户展示的 HTML。HTML 渲染通常使用 QWeb 模板引擎完成。

添加端点控制器

控制器的代码预期应位于 /controllers 子目录中。要添加控制器,首先编辑 library_app/__init__.py 文件,使其也能导入 controllers 子目录,如下所示:

from . import models
from . import controllers

然后,添加一个 library_app/controllers/__init__.py 文件,以便这个目录可以被 Python 导入,并添加一个导入语句到其中,用于我们将实现控制器的 main.py Python 文件,如下所示:

from . import main 

现在,添加控制器的实际文件 library_app/controllers/main.py,以下面的代码:

from odoo import http
class Books(http.Controller):
    @http.route("/library/books")
    def list(self, **kwargs):
        Book = http.request.env["library.book"]
        books = Book.search([])
        return http.request.render(
            "library_app.book_list_template",
            {"book"': books}
        )

第一行导入 odoo.http 模块,这是提供网络相关功能的框架核心组件。接下来,我们创建一个控制器对象类,它从 http.Controller 派生。

我们为类及其方法选择的特定 ID 名称并不重要。@http.route 装饰器很重要,因为它声明了要绑定的 URL 端点——在这种情况下是 /books。目前,网页正在使用默认的访问控制并需要用户登录。

在控制器方法内部,我们可以使用 http.request.env 访问运行环境。我们用它来获取目录中所有活跃书籍的记录集。

最后一步是使用 http.request.render() 处理 library_app.index_template QWeb 模板并生成输出 HTML。我们可以通过字典将值提供给模板,并且这是用来传递 books 记录集的。

如果我们现在重新启动 Odoo 服务器以重新加载 Python 代码并尝试访问 /library/books URL,我们应该在服务器日志中看到一个错误消息:ValueError: 外部 ID 在系统中未找到:library_app.book_list_template。这是预期的,因为我们还没有定义该模板。那应该是我们的下一步。

添加 QWeb 模板

QWeb 模板也与其他 Odoo 视图一起存储,对应的数据文件通常存储在 /views 子目录中。让我们添加 views/book_list_template.xml 文件,如下所示:

<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 模板。实际上,它是一个 ir.ui.view 记录的快捷方式,这是模板存储的基本模型。该模板包含要使用的 HTML 并使用 QWeb 特定的标签和属性。

t-foreach 属性用于遍历由控制器通过 http.request.render() 调用提供给模板的 books 变量中的项目。t-field 属性负责正确渲染 Odoo 记录字段的内容。

QWeb 模板数据文件需要在模块清单中声明,就像任何其他 XML 数据文件一样,以便它被加载并可供使用。因此,应该编辑 __manifest__.py 文件以添加它,如下所示:

    "data": [
        "security/library_security.xml",
        "security/ir.model.access.csv", 
        "views/book_view.xml",
        "views/library_menu.xml",
        "views/book_list_template.xml",
    ], 

在清单中声明 XML 文件并执行模块升级后,网页应该可以正常工作。使用有效的 Odoo 登录打开http://<my-server>:8069/library/books URL 应该会显示一个可用的书籍简单列表,如图下截图所示:

图 3.7 – 包含书籍列表的网页

图 3.7 – 包含书籍列表的网页

这是对 Odoo 网页功能的简要概述。这些功能在第十三章创建 Web 和门户前端功能中进行了更深入的讨论。

快速参考

大多数组件在其他章节中进行了更详细的讨论,并在那里提供了快速参考,如下所示:

  • 第二章, 准备开发环境, 用于 CLI installupgrade 模块

  • 第五章, 导入、导出和模块数据, 用于创建 XML 和 CSV 数据文件

  • 第六章, 模型 – 应用数据结构化, 用于模型层,定义模型和字段

  • 第七章, 记录集 – 模型数据操作, 用于域过滤语法和记录集操作

  • 第八章, 业务逻辑 – 支持业务流程, 用于 Python 方法业务逻辑

  • 第十章, 后端视图 – 设计用户界面, 用于视图,包括窗口操作、菜单项、表单、列表和搜索

  • 第十三章, 创建 Web 和门户前端功能, 用于 Web 控制器和 QWeb 语法

其他地方没有进一步解释的是访问安全,我们在此为这些组件提供快速参考。

访问安全

内部系统模型在此列出:

  • res.groups: name, implied_ids, users

  • res.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: 内部用户—任何后端用户

  • base.group_system: 设置—管理员属于此组

  • base.group_no_one: 技术特性,通常用于使功能对用户不可见

  • base.group_public: 公共,用于使功能对 Web 匿名用户可访问

Odoo 提供的默认用户 XML ID 在此列出:

  • base.user_root: 根系统超级用户,也称为OdooBot

  • base.user_admin: 默认用户,默认命名为Administrator

  • base.default_user: 用于新后端用户的模板。它是一个模板,且处于非活动状态,但可以被复制以创建新用户。

  • base.default_public user:用于创建新门户用户的模板。

摘要

我们从头开始创建了一个新模块,涵盖了模块中涉及的基本组件——模型、访问安全、菜单、三种基本视图类型(表单、列表和搜索)以及模型方法中的业务逻辑。我们还学习了如何使用 Web 控制器和 QWeb 模板创建网页。

在这个过程中,我们熟悉了模块开发过程,这包括模块升级和应用服务器重启,以便在 Odoo 中逐步实施变更。

总是记住,当添加模型字段时,需要进行升级。当更改 Python 代码,包括清单文件时,需要重启。当更改 XML 或 CSV 文件时,也需要进行升级;此外,如有疑问,请同时进行:重启服务器并升级模块。

我们已经了解了创建新 Odoo 应用的基本要素和步骤。但在大多数情况下,我们的模块将扩展现有应用以添加功能。这就是我们在下一章将要学习的内容。

进一步阅读

这里所展示的所有 Odoo 特定主题将在本书剩余章节中做更深入的探讨。

官方文档提供了一些相关的资源,这些资源可以作为良好的补充阅读材料,如下所示:

学习 Python 对于 Odoo 开发很重要。Packt目录中有一些好的 Python 书籍,例如Learn Python Programming – Second Editionwww.packtpub.com/application-development/learn-python-programming-second-edition

第二章:第四章:扩展模块

Odoo 最强大的功能之一是能够在不直接接触扩展模块的代码的情况下添加功能。这允许进行干净的特性扩展,这些扩展在它们自己的代码组件中是隔离的。扩展模块可以通过继承机制实现,这些机制作为现有对象上的修改层工作。这些修改可以在每个级别发生——包括模型、视图和业务逻辑级别。我们不会直接修改现有的模块,而是通过在现有模块上添加一个包含预期修改的层来创建一个新的模块。

上一章引导我们从头开始创建一个新应用。在本章中,我们将学习如何创建扩展现有应用或模块的模块,并使用现有的核心或社区功能。

为了实现这一点,我们将涵盖以下主题:

  • 学习项目 – 扩展图书馆应用

  • 向现有模型添加新字段

  • 使用经典就地扩展扩展模型

  • 更多的模型继承机制

  • 扩展视图和数据

  • 扩展网页

到本章结束时,你应该能够创建扩展现有应用的 Odoo 模块。你将能够向任何几个应用组件添加修改:模型、视图、业务逻辑代码、网页控制器和网页模板。

技术要求

对于本章,你需要一个可以从终端会话中控制的 Odoo 服务器。

本章中的代码依赖于我们在第三章您的第一个 Odoo 应用中创建的代码。你应该在你的附加组件路径中拥有那段代码,并且有一个安装了library_app模块的数据库。

本章将library_member附加模块添加到我们的项目中。相应的代码可以在本书的 GitHub 仓库中找到,github.com/PacktPublishing/Odoo-15-Development-Essentials,在ch04目录下。

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

第三章您的第一个 Odoo 应用中,我们为library_member创建了初始模块。

这些是我们必须提供的功能:

  • 图书馆书籍可以是可借阅的,也可以不是。这项信息应该在书籍表单和网站目录页面上显示。

  • 一些图书馆成员主数据,包括图书馆卡号,以及个人数据,如姓名、地址和电子邮件。

  • 我们希望向成员提供在借阅表上可用的消息和社交功能,包括计划活动小部件,以实现更好的协作。

之后,我们计划引入一个允许成员从图书馆借阅书籍的功能,但这个功能目前不在我们的范围内。这将在接下来的几章中逐步实现。

书籍

以下是我们必须引入到书籍中的技术变更总结:

  • 添加一个“是否可用?”字段。目前,它将由人工管理,但以后可以自动化。

  • 扩展 ISBN 验证逻辑以也支持旧的 10 位数字 ISBN 格式。

  • 扩展网络目录页面以识别不可用的书籍,并允许用户仅过滤可用的书籍。

会员

以下是必须引入到图书馆会员中的技术变更总结:

  • 添加一个新的模型来存储个人的姓名、卡号和联系信息,例如电子邮件和地址。

  • 添加社交讨论和计划活动功能。

要开始工作在这个扩展模块上,我们应该在library_app旁边创建library_member目录,并添加两个文件——一个空的__init__.py文件和一个包含以下内容的__manifest__.py文件:

{
    "name": "Library Members",
    "license": "AGPL-3",
    "description": "Manage members borrowing books.",
    "author": "Daniel Reis",
    "depends": ["library_app"],
    "application": False,
}

现在,我们准备好开始工作在功能上了。我们的首要任务是一个频繁且简单的请求——向现有模型添加新字段。这恰好是介绍 Odoo 继承机制的好方法。

向现有模型添加新字段

我们的首要任务是向书籍模型添加is_available布尔字段。目前,这将是一个简单的可编辑字段,但稍后我们可以想象将其改为自动,基于已借出和归还的书籍。

要扩展现有的模型,我们必须使用具有_inherit属性的 Python 类,以标识正在扩展的模型。新类继承父 Odoo 模型的所有功能,我们只需要声明要引入的修改。我们可以将这种继承视为获取现有模型的引用并在其上进行原地更改。

通过原地模型扩展添加新字段

通过使用 Odoo 特定的继承机制,通过 Python 类扩展模型,该机制通过_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文件,导入该子目录中使用的代码文件:

    from . import library_book
    
  3. 通过扩展library.book模型创建library_member/models/library_book.py文件:

    from odoo import fields, models
    
    class Book(models.Model): 
        _inherit = "library.book" 
        is_available = fields.Boolean("Is Available?")
    

在这里,我们使用了_inherit类属性来声明要扩展的模型。请注意,我们没有使用任何其他类属性,甚至没有使用_name。这不是必需的,除非我们想要对它们进行更改。

小贴士

_name 是模型标识符;如果我们尝试更改它会发生什么?这是允许的,这样做会创建一个新的模型,它是继承模型的副本。这被称为 原型继承,将在本章的 使用原型继承复制模型 部分中进一步讨论。

我们可以将其视为获取一个位于中央注册表中的模型定义的引用,并对其进行原地更改。这可以包括添加字段、修改现有字段、修改模型类属性或添加具有新业务逻辑的方法。

要将新的模型字段添加到数据库表中,我们必须安装附加模块。如果一切按预期进行,当我们转到 library.book 模型时,新添加的字段应该是可见的。

向表单视图添加字段

表单、列表和搜索视图是使用 XML 数据结构定义的。要扩展视图,我们需要一种修改 XML 的方法。这意味着定位 XML 元素,然后在那些点进行修改。

继承视图的 XML 数据记录与常规视图的类似,但有一个额外的 inherit_id 属性,用于引用要扩展的视图。

我们将扩展书籍视图以添加 is_available 字段。

我们需要做的第一件事是找到要扩展的视图的 XML ID。我们可以通过在 library_app.view_form_book 中查找视图来找到它。

当我们到达那里时,我们还应该定位要插入更改的 XML 元素。我们将选择在 ISBN 字段之后添加 Is Available? 字段。要使用的元素通常可以通过其 name 属性来识别。在这种情况下,它是 <field name="isbn" />

当将 XML 文件添加到扩展 Partner 视图时,views/book_view.xml,它应该具有以下内容:

<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 属性来引用其外部标识符。

arch 字段包含声明要使用的扩展点的元素,即具有 name="isbn"<field> 元素,以及要添加的新元素的位置,在这种情况下是 position="after"。在扩展元素内部,我们有要添加的 XML,在这种情况下是 is_available 字段。

创建此扩展后,书籍表单将看起来如下:

![Figure 4.1 – The book form with the "Is Available?" field added

![Figure 4.1 – The book form with the "Is Available?" field added

图 4.1 – 添加了 "Is Available?" 字段的书籍表单

我们刚刚了解了继承的基本知识,并为模型和视图层添加了新的字段。接下来,我们将学习更多关于我们使用的模型扩展方法;即,经典继承。

使用经典原地扩展扩展模型

我们可以将经典模型继承视为原地扩展。当声明具有 _inherit 属性的 Python 类时,它将获得对应模型定义的引用,然后向其中添加扩展。模型定义存储在 Odoo 模型注册表中,我们可以对其进行进一步修改。

现在,让我们学习如何使用它来处理频繁的扩展用例:修改现有字段的属性和扩展 Python 方法以添加或修改业务逻辑。

逐步修改现有字段

当我们扩展模型时,现有字段可以逐步修改。这意味着我们只需要定义要更改或添加的字段属性。

我们将对在 library_app 模块中创建的书籍字段进行两项更改:

  • isbn 字段上,添加一个帮助工具提示,说明我们支持 10 位和 13 位 ISBN,后者将在下一节中实现。

  • publisher_id 字段上,添加数据库索引以提高搜索效率。

我们应该编辑 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 的影响较难察觉,但可以在字段定义中看到,这可以通过选择 开发者工具 菜单中的 查看字段 选项或从 设置 | 技术 | 数据库结构 | 模型 菜单访问:

![Figure 4.2 – The Publisher field with the index enabled]

![img/Figure_4.2_B16119.jpg]

图 4.2 – 启用索引的出版商字段

扩展 Python 方法以添加功能到业务逻辑

编码在 Python 方法中的业务逻辑也可以扩展。为此,Odoo 使用 Python 对象继承机制来扩展继承类的行为。

作为实际示例,我们将扩展图书馆书籍 ISBN 验证逻辑。基础图书馆应用程序提供的逻辑验证现代 13 位 ISBN。但一些较老的标题可能带有 10 位 ISBN。_check_isbn() 方法应该扩展以验证这些情况。

通过添加以下代码来编辑 library_member/models/library_book.py 文件:

# class Book(models.Model):
    def _check_isbn(self):
        self.ensure_one()
        digits = [int(x) for x in self.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 位长。如果是这样,将执行添加的 ISBN-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 框架还支持一些其他情况下有用的扩展机制。

这些是委派继承、原型继承和混入的使用:

  • User记录嵌入一个Partner记录,因此User记录具有所有可用于Partner记录的字段,再加上特定于User记录的字段。它通过_inherits属性使用。

  • 使用_inherit指定要复制的模型和_name属性指定新模型的标识符。

  • mail附加模块提供的mail.thread模型。它实现了 Odoo 中多个模型中可用的聊天和消息功能,例如合作伙伴销售报价。从Models.abstract而不是Models.model构建了一个mixin类,并使用_inherit

接下来的几节将更详细地探讨这些可能性。

使用委派继承嵌入模型

委派继承允许我们重用数据结构,而无需在数据库中进行重复。它将委派模型的一个实例嵌入到继承模型中。

注意

为了在技术上精确,委派继承不是真正的对象继承;相反,它是对象组合,其中某些对象特性被委派给或由第二个对象提供。

关于委派,请注意以下内容:

  • 创建新的模型记录也会创建和链接一个委派模型记录。

  • 在继承模型中不存在的委派模型字段在读写操作中可用,表现得像相关计算字段。

例如,对于用户模型,每个记录都包含一个合作伙伴记录,因此你将在合作伙伴上找到的字段将可用,再加上一些特定于用户的字段。

对于图书馆项目,我们希望添加一个图书馆成员模型。成员将能够借阅书籍并拥有用于借阅的图书馆卡。成员主数据应包括卡号,以及一些个人信息,如电子邮件和地址。合作伙伴模型已经支持联系和地址信息,因此最好重用它,而不是复制数据结构。

要使用委托继承将合作伙伴字段添加到图书馆成员模型中,请按照以下步骤操作:

  1. 将用于实现继承的 Python 文件导入。通过添加以下高亮行编辑library_member/model/__init__.py

    from . import library_book
    from . import library_member
    
  2. 接下来,添加描述新图书馆成员模型的 Python 文件,library_member/models/library_member.py,其中包含以下代码:

    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",
    library.member model embeds the inherited model, res.partner, so that when a new Member record is created, a related Partner is automatically created and referenced in the partner_id field.Through the delegation mechanism, all the fields of the embedded model are automatically made available as if they were fields of the parent model fields. In this case, the Library Member model has all of the Partner fields available for use, such as `name`, `address`, and `email`, plus the ones specific to members, such as `card_number`. Behind the scenes, the Partner fields are stored in the linked Partner record, and no data structure duplication occurs.Delegation inheritance works only at the data level, not at the logic level. No methods from the inherited model are inherited. They are still accessible using the `partner_id.open_parent()` runs the `open_parent()` method of the embedded Partner record.There is an alternative syntax for delegation inheritance that's available through the `_inherits` model attribute. It comes from the pre-Odoo 8 old API, and it is still widely used. The Library Model example with the same effect as earlier looks like this:
    
    

    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)

    
    To finish adding this new model, a few additional steps are needed – add the security ACLs, a menu item, and some view3.
    
  3. 要添加安全 ACL,创建包含以下内容的library_member/security/ir.model.access.csv文件:

    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,4
    
  4. 要添加菜单项,创建包含以下代码的library_member/views/library_menu.xml文件:

    <odoo>
        <act_window id="action_library_member"
          name="Library Members"
          res_model="library.member"
          view_mode="tree,form" />
        <menuitem id="menu_library_member"
          name="Members"
          action="action_library_member"
          parent="library_app.menu_library" />
    </odoo5
    
  5. 要添加视图,创建包含以下代码的library_member/views/member_view.xml文件:

    <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>
    </odoo6
    
  6. 最后,我们应该编辑清单以声明这三个新文件:

    "data": [
        "security/ir.model.access.csv",
        "views/book_view.xml",
        "views/member_view.xml",
        "views/library_menu.xml",
    ],
    

如果一切输入正确,在模块升级后,我们应该能够使用新的图书馆成员模型。

复制具有原型继承的模型

经典继承使用_inherit属性来扩展模型。由于没有修改_name属性,它实际上在同一个模型上执行原地修改。

如果同时修改了_name属性和_inherit,我们将得到一个新的模型,它是继承模型的副本。然后,这个新模型可以添加一些特定于它的功能,而不会添加到父模型中。复制的模型独立于父模型,其修改不会影响父模型。它有自己的数据库表和数据。官方文档称这为原型继承

实际上,使用_inherit复制模型的好处很少。相反,首选委托继承,因为它重用数据结构而不复制它们。

当我们使用多父类继承时,事情变得更有趣。为此,_inherit将是一个模型名称列表,而不是单个名称。

这可以用来将多个模型混合到一个模型中。它允许我们有一个模型提出要重复使用的功能。这种模式通常与抽象混合类一起使用。这将在下一节中详细讨论。

使用混合类重用模型功能

使用模型名称列表设置_inherit属性将继承那些模型的功能。大多数情况下,这是为了利用混入类。

一个没有在数据库中实际表示的models.AbstractModel,而不是models.Model

Odoo 标准插件提出了几个有用的混入类。在代码中搜索models.AbstractModel将揭示它们。值得注意的是,可能是最广泛使用的两个混入类,这些混入类由 Discuss 应用(mail插件模块)提供:

  • mail.thread混入类为消息板(也称为聊天)提供功能,这在许多文档表单的底部或右侧都可以找到,包括有关消息和通知的逻辑。

  • mail.activity.mixin混入类提供活动,这些活动也通过聊天讨论小部件公开,用于定义和计划待办任务。

    Odoo 11 的变化

    活动混入类是 Odoo 11 中引入的新功能,在早期版本中不可用。

聊天和活动是广泛使用的功能,在下一节中,我们将花一点时间演示如何添加它们。

向模型添加消息聊天和活动

现在我们将向图书馆成员模型添加消息聊天和活动混入类。这是添加它们的所需步骤:

  1. 将依赖项添加到提供混入模型的插件模块;即mail

  2. 继承mail.threadmail.activity.mixin混入类。

  3. Form视图添加字段。

让我们详细检查前面的步骤:

  1. 要将依赖项添加到mail插件,编辑__manifest__.py文件:

        "depends": ["library_app", "mail"], 
    
  2. 要继承混入类,编辑library_member/models/library_member.py文件,添加以下高亮文本:

    class Member(models.Model): 
        _name = "library.member"
        _description = "Library Member"
        _inherits = {"res.partner": "partner_id"}
    _inherit = ["library.member", "mail.thread", "mail.activity.mixin"].
    
  3. 最后,我们必须将相关字段添加到Library Member Form。通过添加以下高亮代码编辑library_member/views/member_view.xml文件:

      <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>
            <!-- 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>
        </field>
      </record>
    

如我们所见,mail模块不仅为关注者、活动和消息提供字段,还提供特定的 Web 客户端小部件,所有这些都在这里被使用。

一旦模块升级,图书馆成员表单应该看起来像这样:

图 4.3 – 图书馆成员表单视图

图 4.3 – 图书馆成员表单视图

注意,混入类本身不会对访问安全造成任何变化,包括记录规则。在某些情况下,已经存在记录规则,限制了每个用户可访问的记录。例如,如果我们想用户只能查看他们是关注者的记录,必须显式添加该记录规则。

mail.thread模型包含一个用于列出关注者message_partner_ids的字段。为了实现关注者的访问规则,需要添加[('message_partner_ids', 'in', [user.partner_id.id])]

通过这些,我们已经看到了如何在模型和逻辑层扩展模块。下一步是扩展视图,以反映在模型层所做的更改。

扩展视图和数据

视图和其他数据组件也可以通过扩展模块进行修改。对于视图,通常的情况是添加功能。视图的表示结构是用 XML 定义的。要扩展此 XML,我们必须找到要扩展的节点,然后声明在那里执行的操作,例如插入额外的 XML 元素。

其他数据元素代表写入数据库的记录。扩展模块可以写入它们以更改某些值。

扩展视图

视图是用 XML 定义的,并存储在架构字段 arch 中。要扩展视图,我们必须找到扩展将发生的位置的节点,然后执行预期的更改,例如添加 XML 元素。

Odoo 提供了一种简化的表示法,通过使用我们想要匹配的 XML 标签——例如 <field> ——以及一个或多个具有匹配功能的独特属性,如 name,来扩展 XML。然后,我们必须添加 position 属性来声明要进行的修改类型。

恢复我们在本章前面使用的示例,要在 isbn 字段之后添加额外内容,我们可以使用以下代码:

      <field name="isbn" position="after">
        <!-- Changed content goes here -->
      </field>

任何 XML 元素和属性都可以用来选择作为扩展点的节点,除了 string 属性。字符串属性的值在视图生成期间被转换为用户的活跃语言,因此它们不能可靠地用作节点选择器。

要执行扩展操作是通过 position 属性声明的。允许执行以下几种操作:

  • inside(默认):在所选节点内追加内容。节点应该是容器,例如 <group><page>

  • after:在所选节点之后添加内容。

  • before:在所选节点之前添加内容。

  • replace:替换所选节点。如果它与空内容一起使用,则删除该元素。自 Odoo 10 以来,它还允许您使用内容中的 $0 来包装元素,以表示被替换的元素;例如,<field name="name" position="replace"><h1>$0</h1></field>

  • attributes:修改匹配元素的属性值。内容应包含一个或多个 <attribute name="attr-name">value<attribute> 元素,例如 <attribute name="invisible">True</attribute>。如果没有主体使用,例如 <attribute name="invisible"/>,则从所选元素中删除该属性。

    小贴士

    虽然 position="replace" 允许我们删除 XML 元素,但应避免使用。它可能会因为使用被删除节点作为扩展点来添加其他元素的模块而损坏。作为替代方案,考虑保留该元素并使其不可见。

将 XML 节点移动到不同的位置

除了attributes操作外,前面的定位器可以与具有position="move"的子元素组合。其效果是将子定位器目标节点移动到父定位器目标位置。

Odoo 12 中的更改

position="move"子定位器是 Odoo 12 中的新功能,在之前的版本中不可用。

以下是将my_field从当前位置移动到target_field之后位置的示例:

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

其他视图类型,如列表和搜索视图,也有arch字段,并且可以像表单视图一样进行扩展。

使用 XPath 选择 XML 扩展点

在某些情况下,我们可能没有具有唯一值的属性可以用来作为 XML 节点选择器。当要选择的元素没有name属性时,这种情况可能发生,例如对于<group><notebook><page>视图元素。另一种情况是当有多个元素具有相同的name属性时,例如在 Kanban QWeb 视图中,相同的字段可以在同一个 XML 模板中包含多次。

对于这些情况,我们需要一种更复杂的方法来定位要扩展的 XML 元素。作为 XML,XPath 表达式是定位元素的自然方式。

例如,以我们在上一章中定义的书籍表单视图为例,定位<field name="isbn">元素的 XPath 表达式是//field[@name]='isbn'。这个表达式找到具有name属性等于isbn<field>元素。

之前章节中创建的书籍表单视图扩展的 XPath 等价表达式如下:

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

更多关于支持的 XPath 语法的详细信息可以在官方 Python 文档中找到:docs.python.org/3/library/xml.etree.elementtree.html#supported-xpath-syntax

如果 XPath 表达式匹配多个元素,则只有第一个元素将被选为目标进行扩展。因此,应该尽可能使用唯一的属性使其尽可能具体。使用name属性是确保我们找到作为扩展点的元素的最简单方法。因此,在我们的创建的视图的 XML 元素中拥有这些唯一的标识符非常重要。

修改现有数据

常规数据记录也可以被扩展,在实践中,这意味着覆盖现有值。为此,我们只需要识别要写入的记录以及要更新的字段和值。由于我们不是像视图那样修改 XML arch结构,所以不需要 XPath 表达式。

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

可以使用<module>.<identifier>全局标识符访问其他模块中的记录,因此一个模块可以更新由另一个模块创建的记录。

小贴士

点号(.)被保留用于分隔模块名称和对象标识符。因此,它不能用于标识符名称。相反,使用下划线(_)字符。

例如,我们将把用户安全组的名称改为Librarian。要修改的记录是在library_app模块中创建的,具有library_app.library_group_user标识符。

要做到这一点,我们将添加library_member/security/library_security.xml文件,以及以下代码:

<odoo>
  <!-- Modify Group name -->
<record id="library_app.library_group_user" 
model="res.groups"> 
    <field name="name">Librarian</field>
  </record>
</odoo>

注意,我们使用了<record>元素,只写入name字段。你可以将这视为在这个字段上的写操作。

小贴士

当使用<record>元素时,我们可以选择我们想要写入的字段,但对于快捷元素,如<menuitem><act_window>,则不是这样。这些元素需要提供所有属性,缺少任何一个都会将相应的字段设置为空值。然而,你可以使用<record>来设置通过快捷元素创建的字段的值。

不要忘记将library_member/security/library_security.xml文件添加到清单文件的data键中。完成此操作并升级模块后,我们应该在用户组中看到名称更改。

扩展视图允许你在后端表示层引入修改。但同样也可以在前端 Web 表示层进行。这就是我们将在下一节中讨论的内容。

扩展 Web 页面

可扩展性是 Odoo 框架的关键设计选择,Odoo Web 组件也不例外。因此,Odoo Web 控制器和模板也可以扩展。

我们在上一章第三章您的第一个 Odoo 应用程序中创建的图书馆应用程序提供了一个需要改进的图书目录页面。

我们将扩展它以利用图书馆成员模块添加的图书可用性信息:

  • 在控制器方面,我们将添加对查询字符串参数的支持,以仅过滤可用的图书;即/library/books?available=1

  • 在模板方面,我们将指定不可用的图书。

让我们开始扩展 Web 控制器。

扩展 Web 控制器

Web 控制器负责处理 Web 请求并将页面渲染为响应。它们应该专注于表示逻辑,而不是处理业务逻辑,这些逻辑应该被整合到模型方法中。

支持额外的参数甚至 URL 路由是 Web 表示特定的,并且适合 Web 控制器处理。

这里将扩展/library/books端点以支持查询字符串参数available=1,我们将使用它来过滤图书目录,以便只显示可用的标题。

要扩展现有的控制器,我们需要导入创建它的原始对象,基于它声明一个 Python 类,然后实现包含额外逻辑的类方法。

扩展控制器的代码应该添加到library_member/controllers/main.py文件中,如下所示:

from odoo import http 
from odoo.addons.library_app.controllers.main import Books 
class BooksExtended(Books):
    @http.route()
    def list(self, **kwargs):
        response = super().list(**kwargs)
        if kwargs.get("available"):
            all_books = response.qcontext["books"]
            available_books = all_books.filtered(
              "is_available")
            response.qcontext["books"] = available_books
        return response

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

  1. 添加library_member/controllers/main.py文件,确保它包含前面的代码。

  2. 通过将控制器的子目录添加到library_member/__init__.py文件中,使这个新的 Python 文件为模块所知:

    from . import models
    from . import controllers
    
  3. 添加library_member/controllers/__init__.py文件,并包含以下代码行:

    from . import main 
    
  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 尚未生成。这只有在控制器运行完成后才会发生。因此,在最终渲染之前,可以更改Response的属性。

此方法检查 kwargsavailable 键的非空值。如果找到,则过滤掉不可用的书籍,并使用此记录集更新 qcontext。因此,当控制器处理完成后,将使用更新后的书籍记录集渲染 HTML,这将仅包括可用的书籍。

扩展 QWeb 模板

网页模板是 XML 文档,就像其他 Odoo 视图类型一样,可以使用选择器表达式,就像我们对其他视图类型(如表单)所做的那样。QWeb 模板通常更复杂,因为它们包含更多的 HTML 元素,所以大多数时候需要更通用的 XPath 表达式。

要修改网页的实际呈现方式,我们应该扩展正在使用的 QWeb 模板。作为一个例子,我们将扩展 library_app.book_list_template 以添加关于不可用书籍的视觉信息。

QWeb 扩展是一个使用额外的 inherit_id 属性来标识要扩展的 QWeb 模板的 <template> 元素。在这种情况下是 library_app.book_list_template

按照以下步骤操作:

  1. library_member/views/book_list_template.xml 文件以及以下代码添加进去:

    <odoo>
      <template id="book_list_extended"
                name="Extended Book List"
                inherit_id=
                  "library_app.book_list_template">
    xpath notation. Note that in this case, we could have also used the equivalent simplified notation; that is, <span t-field="book.publisher_id" position=after>.
    
  2. 在插件清单中声明此额外数据文件;即 library_member/__manifest__.py

    "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 应该会显示不可用书籍的额外(不可用)视觉信息。以下是网页将呈现的样子:

图 4.4 – 带有可用性信息的书籍列表网页

图 4.4 – 带有可用性信息的书籍列表网页

这完成了我们对如何扩展每种 Odoo 组件的审查,从数据模型到用户界面元素。

摘要

可扩展性是 Odoo 框架的关键特性。我们可以在 Odoo 中构建插件模块,这些模块可以在多个层上更改或添加功能,以实现 Odoo 中的功能。这样,我们的项目将能够以干净和模块化的方式重用和扩展第三方插件模块。

在模型层,我们使用 _inherit 模型属性来获取对现有模型的引用,然后对其进行原地修改。模型内的字段对象也支持增量定义,这样我们就可以重新声明一个现有字段,只需提供要更改的属性。

附加的模型继承机制允许您重用数据结构和业务逻辑。通过在多对一关系字段上的delegate=True属性(或旧式的inherits模型属性)激活的委托继承,使得相关模型的所有字段都可用,并重用其数据结构。原型继承,通过使用_inherit与附加模型,允许您从其他模型复制功能(数据结构定义和方法),并启用抽象混入类的使用,提供一系列可重用功能,例如文档讨论消息和关注者。

在视图层,视图结构使用 XML 定义,可以通过定位 XML 元素(使用 XPath 或 Odoo 简化的语法)并提供要添加的 XML 片段来进行扩展。由模块创建的其他数据记录也可以通过扩展模块通过简单地引用相应的完整 XML ID 并在目标字段上执行写操作来修改。

在业务逻辑层,可以通过与模型扩展和重新声明方法以扩展相同的方式添加扩展。在这些扩展中,使用super() Python 函数来调用继承方法的代码,并且我们的附加代码可以在那之前或之后运行。

对于前端网页,控制器中的表示逻辑可以以类似于模型方法的方式扩展,并且 Web 模板也是具有 XML 结构的视图,因此它们可以以与其他视图类型相同的方式进行扩展。

在下一章中,我们将更深入地探讨模型,并探索它们能为我们提供的一切。

进一步阅读

以下是一些指向官方文档的附加参考,这些文档可以提供有关模块扩展和继承机制的有用信息:

第二部分:模型

第二部分介绍了模型,这些模型负责构建应用程序的数据模型结构。与模型密切相关,数据加载技术和访问控制也在此讨论。

本节包含以下章节:

  • 第五章**,导入、导出和模块数据

  • 第六章**,模型 – 结构化应用程序数据

第三章:第五章:导入、导出和模块数据

大多数 Odoo 模块定义,如用户界面安全规则,是存储在特定数据库表中的数据记录。模块中找到的 XML 和 CSV 文件在 Odoo 应用程序运行时不会被使用。它们是加载这些定义到数据库表中的手段。

由于这个原因,Odoo 模块的一个重要部分是将数据表示在文件中,以便在模块安装时将其加载到数据库中。模块还可以包含初始数据和演示数据。数据文件允许我们将这些添加到我们的模块中。

此外,了解 Odoo 数据表示格式对于在项目实施过程中导出和导入业务数据也很重要。

本章将涵盖以下主题:

  • 理解外部标识符的概念

  • 导出和导入数据文件

  • 使用 CSV 文件

  • 添加模块数据

  • 使用 XML 数据文件

到本章结束时,您将能够执行数据导出和导入操作,以将初始数据填充到数据库中,并自动在创建的模块中创建默认和演示数据。

技术要求

本章要求您运行一个 Odoo 服务器,并安装了图书馆应用程序的基础模块。

本章的代码可以在本书的 GitHub 仓库中找到,位于github.com/PacktPublishing/Odoo-15-Development-Essentialsch05/目录下。它包含了我们创建在第三章,“您的第一个 Odoo 应用程序”,中的library_app原始副本,以及为本章添加的额外文件。

理解外部标识符的概念

外部标识符,也称为XML ID,是一个可读的字符串标识符,在 Odoo 中唯一标识一个特定的记录。它们对于将数据加载到 Odoo 中非常重要,允许我们修改现有数据记录或在其他数据记录中引用它。

首先,我们将介绍外部标识符的工作原理以及如何检查它们。然后,我们将学习如何使用 Web 客户端查找特定数据记录的外部标识符,因为这在创建附加模块、扩展现有功能时经常需要。

外部标识符的工作原理

让我们先了解标识符是如何工作的。记录的实际数据库标识符是一个自动分配的顺序号,在模块安装过程中无法预先知道将分配给每个记录的 ID。外部标识符允许我们引用相关记录,而无需知道分配给它的实际数据库 ID。XML ID 为数据库 ID 提供了一个方便的别名,这样我们就可以在需要引用特定记录时使用它。

在 Odoo 模块数据文件中定义的记录使用XML ID。一个原因是避免在模块升级时创建重复记录。模块升级将再次将数据文件加载到数据库中。我们希望它能够检测它们之前存在的记录以便更新,而不是创建重复的记录。

使用 XML ID 的另一个原因是支持相互关联的数据:需要引用其他数据记录的数据记录。由于我们无法知道实际的数据库 ID,我们可以使用 XML ID,这样翻译将由Odoo 框架透明地处理。

Odoo 负责将外部标识符名称转换为分配给它们的实际数据库 ID。背后的机制相当简单:Odoo 保留一个表,其中包含命名外部标识符与其对应的数字数据库 ID 之间的映射:ir.model.data模型。

我们必须启用开发者模式才能有可用的菜单选项。检查你是否有右上角的开发者模式bug 图标,紧挨着用户头像图标。如果没有,你现在应该在设置顶菜单中启用它。请参阅第一章使用开发者模式快速入门,获取更多详细信息。

我们可以使用library_app模块来检查现有的映射,我们会看到由我们创建的模块生成的外部标识符,如下面的截图所示:

图 5.1 – 由 library_app 模块生成的外部标识符

图 5.1 – 由 library_app 模块生成的外部标识符

在这里,我们可以看到外部标识符有library_app.action_library_book

外部标识符只需要在 Odoo 模块内部是唯一的,这样就没有两个模块因为意外选择相同的标识符名称而冲突的风险。全局唯一标识符是通过将模块名称与实际的外部标识符名称连接起来构建的。这就是你在完整 ID字段中可以看到的内容。

当在数据文件中使用外部标识符时,我们可以选择使用完整的标识符或仅使用外部标识符名称。通常,仅使用外部标识符名称更简单,但完整的标识符使我们能够引用来自其他模块的数据记录。在这样做的时候,请确保那些模块包含在模块依赖关系中,以确保那些记录在我们之前被加载。

在某些情况下,即使我们指的是同一模块中的 XML ID,也需要完整的 ID。

在列表顶部,我们可以看到 library_app.action_library_book 完整标识符。这是我们为该模块创建的菜单操作,它也在相应的菜单项中引用。点击它,我们将进入表单视图,其中包含其详细信息。在那里,我们可以看到 library_app 模块中的 action_library_book 外部标识符映射到 ir.actions.act_window 模型中的特定记录 ID,在这种情况下是 87

通过点击记录的行,可以在表单视图中看到信息,如下面的屏幕截图所示:

图 5.2 – library_app.action_library_book 外部标识符的表单视图

图 5.2 – library_app.action_library_book 外部标识符的表单视图

除了提供一种让记录轻松引用其他记录的方法外,外部标识符还允许您在重复导入时避免数据重复。如果外部标识符已经存在,现有记录将被更新,从而避免创建一个新的、重复的记录。

查找外部标识符

当我们为我们自己的模块编写数据记录时,我们经常需要查找现有的外部标识符以供参考。因此,了解如何查找这些标识符是很重要的。

做这件事的一种方法是通过使用设置 | 技术 | 序列与标识符 | 外部标识符菜单,这在之前的图 5.1中已经展示过。我们也可以使用开发者菜单来做这件事。如您在第一章中回忆的那样,使用开发者模式快速入门开发者菜单可以在设置仪表板的最右下角激活。

要查找数据记录的外部标识符,我们应该打开相应的表单视图,选择开发者菜单,然后选择查看元数据选项。这将显示包含记录的数据库 ID 和外部标识符(也称为 XML ID)的对话框。

例如,要查找 demo 用户 ID,我们应该导航到 demo 用户表单的用户表单视图,然后选择 base.user_demo 并确认数据库 ID 是 6

图 5.3 – 查看元数据对话框窗口

图 5.3 – 查看元数据对话框窗口

要查找视图元素(如表单搜索操作)的外部标识符,开发者菜单也是一个很好的帮助来源。为此,我们可以使用适当的编辑视图选项打开包含相应视图详细信息的表单。在那里,我们将找到一个外部 ID字段,它提供了我们所需的信息。

例如,在下面的屏幕截图中,我们可以看到 base.view_users_form

图 5.4 – 显示表单视图外部 ID 属性的编辑视图窗口

图 5.4 – 显示表单视图外部 ID 属性的编辑视图窗口

通过这些,我们已经了解了外部 ID及其如何用作别名来引用数据库记录。我们还探讨了查找在数据文件中引用记录所需的XML ID的几种方法。接下来,我们将学习如何创建这些XML ID将非常有用的数据文件。

导出和导入 CSV 数据文件

生成数据文件并了解文件应具有的结构的一个简单方法就是使用内置的导出功能。

通过生成的 CSV 文件,我们可以了解手动将数据导入系统所需的格式,编辑它们以执行批量更新,甚至使用它们来为我们的附加模块生成演示数据。

在本节中,我们将学习关于从 Odoo 用户界面导出和导入数据的基本知识。

导出数据

数据导出是任何列表视图中都有的标准功能。要使用它,我们必须通过选择最左侧的相应复选框来选择要导出的行,然后从列表顶部的操作按钮中选择导出选项。

首先,我们应该将几本 Odoo 书籍添加到Odoo Development Essentials 11Odoo 11 Development Cookbook

我们还需要安装联系人应用程序,这样我们就可以看到合作伙伴列表视图,并可以从那里导出这些记录。注意,联系卡片的默认视图是看板,因此我们需要切换到列表视图:

图 5.5 – 操作菜单中的导出选项

图 5.5 – 操作菜单中的导出选项

我们还可以在列标题中勾选复选框以选择所有符合当前搜索标准的可用记录。

导出选项将我们带到导出数据对话框表单,在那里我们可以选择导出什么以及如何导出。我们关心的是以允许我们稍后手动或作为附加模块的一部分导入文件的方式导出:

图 5.6 – 导出数据对话框窗口

图 5.6 – 导出数据对话框窗口

在对话框表单的顶部,我们有两个可用的选择:

  • 我想更新数据(兼容导入的导出):启用此复选框,以便以格式友好的方式导出数据,以便稍后导入。

  • 导出格式:您可以选择CSVXLSX。我们将选择 CSV 文件,以便更好地了解原始导出格式,该格式仍然被任何电子表格应用程序所理解。

接下来,选择要导出的列。在这个例子中,通过仅选择名称字段,执行了一个非常简单的导出。通过点击导出按钮,将可导出一个导出数据文件。导出的 CSV 文件应如下所示:

"id","name"
"__export__.res_partner_43_f82d2ecc","Alexandre Fayolle"
"__export__.res_partner_41_30a5bc3c","Daniel Reis"
"__export__.res_partner_44_6be5a130","Holger Brunn"
"__export__.res_partner_42_38b48275","Packt Publishing"

第一行包含字段名称,在导入过程中将用于自动将列映射到其目的地。

第一行有选定的name列,正如预期的那样。由于选择了导入兼容的导出选项,自动添加了一个初始 ID 列。

自动添加的id列分配了每个记录的外部 ID。这允许编辑导出的数据文件并在以后重新导入,以更新记录,而不是创建重复的记录。

缺失的外部标识符将自动使用__export__前缀生成,如前一个文件导出示例所示。

小贴士

由于自动生成的记录标识符,导出或导入功能可以用于批量编辑 Odoo 数据 – 将数据导出到 CSV,使用电子表格软件进行批量编辑,然后将其重新导入到 Odoo。

导入数据

一旦我们准备好格式正确的数据文件,我们希望将其导入到 Odoo 中。让我们学习如何通过网页客户端用户界面来完成这项操作。

首先,我们必须确保导入功能是启用的。它应该默认启用。如果不是,可以在设置应用中的常规设置菜单项下找到此选项。在权限部分,应勾选导入导出选项。

启用此选项后,列表视图搜索小部件将在收藏菜单中显示导入记录选项,位于筛选器分组依据菜单旁边。

注意

负责提供此功能的base_import模块。

让我们尝试对我们的联系人合作伙伴数据进行批量编辑。在电子表格或文本编辑器中打开我们刚刚下载的 CSV 文件,并更改一些值。我们还可以添加一些新行,将id列留空。

如我们之前提到的,第一列id为每一行提供唯一的标识符。这允许在将数据重新导入到 Odoo 时更新现有记录,而不是需要复制它们。如果我们编辑导出文件中的任何名称,则在导入文件时相应的记录将被更新。

对于已添加到 CSV 文件中的新行,我们可以选择提供我们选择的任何外部标识符,或者我们可以将id列留空。无论哪种方式,都会为它们创建一个新的记录。例如,我们添加了一条没有id且名称为菲利普·K·迪克的行以在数据库中创建:

,Phillip K. Dick

保存这些更改到CSV文件后,点击收藏菜单中的导入选项。下一页允许我们上传数据文件。然后,将显示导入助手:

图 5.7 – 导入文件助手

图 5.7 – 导入文件助手

在这里,我们应该选择 CSV 文件在磁盘上的位置,然后点击左上角的测试按钮,以检查其正确性。

由于要导入的文件基于 Odoo 导出,它很可能有效,并且列将自动映射到数据库中的正确目的地。根据用于编辑数据文件的程序,你可能需要调整分隔符和编码选项以获得最佳结果。

现在,点击 导入,然后你就会看到——修改和新记录应该已经被加载到 Odoo 中!

CSV 数据文件中的相关记录

上一节中的示例相当简单,但一旦我们开始使用关系字段,将多个表中的记录链接起来,数据文件可能会变得更加复杂。

之前,我们处理了在 Books 中使用的 Partner 记录。现在,我们将探讨如何在 CSV 文件中表示这些合作伙伴在书籍数据中的引用。特别是,我们有一个多对一(或外键)关系用于出版社(publisher_id 字段)和一个多对多关系用于作者(author_ids 字段)。

在 CSV 文件标题行中,关系列应该在其名称后附加 /id。它将使用外部标识符引用相关记录。在我们的例子中,我们将使用相关合作伙伴的外部 ID 作为值,将书籍出版商加载到 publisher_id/id 字段中。

可以使用 /.id 来代替,这样我们就可以使用实际的数据库 ID(已经分配的真实数字标识符),但这通常不是我们需要的。除非你有充分的理由这样做,否则始终使用外部 ID 而不是数据库 ID。此外,请记住,数据库 ID 是特定于特定 Odoo 数据库的,所以,大多数情况下,如果导入到原始数据库之外的数据库,它可能无法正确工作。

多对多字段也可以通过 CSV 数据文件导入。这就像提供一个由逗号分隔的外部 ID 列表,并用双引号包围一样简单。例如,为了加载书籍作者,我们会有一个 author_ids/id 列,我们会使用合作伙伴的外部 ID 列表作为值来链接。以下是一个 CSV 文件中多对多字段示例:

id, name, author_ids/id
book_odc11, "Odoo 11 Development Cookbook", "__export__.res_partner_43_f82d2ecc,__export__.res_partner_44_6be5a130"

一对多字段通常表示标题或行,或父或子关系,并且有特殊支持来导入这些类型的关联——对于同一父记录,我们可以有多个相关行。

这里,我们有一个 Partners 模型中一对一字段(多对一)的示例:一个公司合作伙伴可以有多个子联系人。如果我们从 Partner 模型导出数据并包括 Contacts/Name 字段,我们将看到可以用来导入此类数据的结构:

![图 5.8 – 数据文件示例导入多对多相关记录图片

![图 5.8 – 数据文件示例导入多对多相关记录

idname列用于父记录,而child_ids列用于子记录。注意,在第一个之后,父记录列在子记录中留空。

之前表示为 CSV 文件的表格如下所示:

"id","name","child_ids/id","child_ids/name"
"base.res_partner_12","Azure Interior","base.res_partner_address_15","Brandon Freeman"
"","","base.res_partner_address_28","Colleen Diaz"
"","","base.res_partner_address_16","Nicole Ford"

在这里,我们可以看到前两列,idname,在第一行中有值,在接下来的两行中为空。它们包含父记录的数据,即联系人的公司

另外两列都以前缀child_ids/开头,并在所有三行中都有值。它们包含与父公司相关的联系人的数据。第一行包含公司和第一个联系人的数据,而随后的行包含子联系人列的数据。

添加模块数据

模块使用数据文件将它们的默认数据、演示数据、用户界面定义和其他配置加载到数据库中。为此,我们可以使用 CSV 和 XML 文件。

Odoo 12 中的更改

YAML 文件格式直到 Odoo 11 都受到支持,但在 Odoo 12 中被移除。不过,为了使用示例,您可以查看 Odoo 11 中的l10n_be官方模块,有关 YAML 格式的信息,您可以访问yaml.org/

模块使用的 CSV 文件与我们看到的用于导入功能的文件相同。在模块中使用时,文件名必须与将要加载数据的模型名称匹配。例如,用于将数据加载到library.book模型的 CSV 文件必须命名为library.book.csv

数据 CSV 文件的一个常见用途是访问已加载到ir.model.access模型的安全定义。它们通常在security/子目录中使用 CSV 文件,命名为ir.model.access.csv

演示数据

Odoo 附加模块可以安装演示数据,这样做被认为是良好的实践。这对于提供模块的使用示例和测试中要使用的数据集很有用。模块的演示数据使用__manifest__.py清单文件的demo属性声明。就像data属性一样,它是一个包含模块内部相应相对路径的文件名列表。

应该向library.book模块添加一些演示数据。一种简单的方法是使用已安装模块的开发数据库导出一些数据。

习惯上,将数据文件放在data/子目录中。我们应该将这些数据文件保存在library_app附加模块中,命名为data/library.book.csv。由于这些数据将属于我们的模块,我们应该编辑id值,以从由导出功能生成的标识符中删除__export__前缀。

例如,我们的res.partner.csv数据文件可能如下所示:

id,name 
res_partner_alexandre,"Alexandre Fayolle" 
res_partner_daniel,"Daniel Reis" 
res_partner_holger,"Holger Brunn"
res_partner_packt,"Packt Publishing"

包含图书演示数据的library.book.csv数据文件如下所示:

"id","name","date_published","publisher_id/id","author_ids/id"
library_book_ode11,"Odoo Development Essentials 11","2018-03-01",res_partner_packt,res_partner_daniel
library_book_odc11,"Odoo 11 Development Cookbook","2018-01-01",res_partner_packt,"res_partner_alexandre,res_partner_holger"

不要忘记将这些数据文件添加到 __manifest__.py 清单的 demo 属性中:

"demo": [
    "data/res.partner.csv",
    "data/library.book.csv",
],

文件按声明的顺序加载。这很重要,因为文件中的记录不能引用尚未创建的其他记录。

下次模块更新时,只要安装了带有演示数据的模块,文件的内容就会被导入。

注意

虽然数据文件在模块升级时也会重新导入,但对于演示数据文件来说并非如此:这些文件仅在模块安装时导入。

当然,与普通的 CSV 文件相比,XML 文件可以用来加载或初始化数据,利用它们提供的额外功能。在下一节中,我们将讨论使用 XML 格式的数据文件。

使用 XML 数据文件

虽然 CSV 文件提供了一个简单且紧凑的格式来表示数据,但 XML 文件功能更强大,并提供了更多对加载过程的控制。例如,它们的文件名不需要与要加载的模型匹配。这是因为 XML 格式更加丰富,可以通过文件内的 XML 元素提供更多有关加载的信息。

我们在之前的章节中使用了 XML 数据文件。用户界面组件,如视图和菜单项,实际上是在系统模型中存储的记录。模块中的 XML 文件用于将这些记录加载到实例数据库中。

为了展示这一点,将在 library_app 模块中添加第二个数据文件,data/book_demo.xml,其内容如下:

<?xml version="1.0"?>
<odoo noupdate="1">
  <!-- Data to load -->
  <record model="res.partner" id="res_partner_huxley"> 
    <field name="name">Aldous Huxley</field> 
  </record> 
  <record model="library.book" id="library_book_bnw">
    <field name="name">Brave New World</field>
    <field name="author_ids"
           eval="[(4, ref('res_partner_huxley'))]" />
    <field name="date_published">1932-01-01</field>
  </record>
</odoo>

与往常一样,新的数据文件必须在 __manifest__.py 文件中声明:

"demo": [
    "data/res.partner.csv",
    "data/library.book.csv",
    "data/book_demo.xml",
],

与上一节中看到的 CSV 数据文件类似,此文件也将数据加载到 图书馆书籍 模型中。

XML 数据文件有一个 <odoo> 顶级元素,其中可以包含多个 <record> 元素,它们相当于 CSV 文件中的数据行。

注意

数据文件中的 <odoo> 顶级元素是在 9.0 版本中引入的,并取代了之前的 <openerp> 标签。顶级元素内的 <data> 部分仍然受支持,但现在它是可选的。实际上,现在 <odoo><data> 是等效的,因此我们可以使用任何一个作为我们的 XML 数据文件的顶级元素。

<record> 元素有两个强制属性,modelid,用于记录的外部标识符,并包含一个 <field> 标签,用于写入每个字段。

注意,字段名称中的斜杠表示法在这里不可用;我们无法使用 <field name="publisher_id/id">。相反,使用 ref 特殊属性来引用外部标识符。我们将在稍后讨论多对多关系字段的值。

你可能已经注意到了 <odoo> 元素顶部的 noupdate="1" 属性。这阻止了数据记录在模块升级时被加载,因此对它们的任何后续编辑都不会丢失。

noupdate 数据属性

当模块升级时,数据文件加载会重复,模块的记录会被重写。这意味着升级模块将覆盖可能对模块数据进行的任何手动更改。

小贴士

显然,如果视图被手动修改以添加快速自定义,这些更改将在下一个模块升级时丢失。为了避免这种情况,正确的方法是创建继承视图,以引入我们想要引入的更改。

此重写行为是默认的,但可以更改,以便某些数据仅在安装时导入,并在后续模块升级中忽略。这可以通过在<odoo><data>元素中使用noupdate="1"属性来完成。

这对于预期以后将进行自定义的初始配置数据很有用,因为这些手动进行的自定义将不会受到模块升级的影响。例如,它经常用于记录访问规则,允许它们适应特定实施的需求。

在同一个XML文件中可以有多个<data>部分。我们可以利用这一点来分离只导入一次的数据,使用noupdate="1",以及可以在每次升级时重新导入的数据,使用noupdate="0"noupdate="0"是默认值,因此如果我们愿意,可以省略它。请注意,我们需要有一个顶级 XML 元素,因此在这种情况下,我们将使用两个<data>部分。它们必须位于顶级<odoo><data>元素内。

小贴士

当我们开发模块时,noupdate属性可能会很棘手,因为对数据的后续更改将被忽略。一种解决方案是,而不是使用-u选项升级模块,使用-i选项重新安装它。使用命令行通过-i选项重新安装会忽略数据记录上的noupdate标志。

noupdate标志存储在每个记录的外部标识符信息中。可以直接使用外部标识符表单手动编辑它,该表单位于技术菜单中,通过使用不可更新复选框。

Odoo 12 中的更改

开发者菜单中,当访问查看元数据时,对话框现在也会显示无更新标志的值,以及记录的XML ID。此外,可以通过点击来更改无更新标志

在 XML 中定义记录

在一个XML数据文件中,每个<record>元素有两个基本属性,idmodel,并包含<field>元素,为每一列分配值。id属性对应于记录的外部标识符,而model属性对应于目标模型。《field》元素有几种不同的方式来分配值。让我们详细看看它们。

直接设置字段值

<field>元素的name属性标识要写入的字段。

要写入的值是元素的内容:字段打开和关闭标签之间的文本。对于日期和日期时间,返回 datedatetime 对象的表达式 eval 属性将有效。返回 "YYYY-mm-dd""YYYY-mm-dd HH:MM:SS" 的字符串将被正确转换。对于布尔字段,"0""False" 值被转换为 False,任何其他非空值将被转换为 True

Odoo 10 的变化

在 Odoo 10 中,从数据文件中读取布尔 False 值的方式已得到改进。在之前的版本中,任何非空值,包括 "0""False",都被转换为 True。直到 Odoo 9,布尔值应使用 eval 属性设置,例如 eval="False"

使用表达式设置值

设置字段值的另一种更详细的方法是使用 eval 属性。它评估一个 Python 表达式并将结果分配给字段。

表达式是在一个上下文中评估的,除了 Python 内置函数外,还有一些额外的标识符可用于构建要评估的表达式。

要处理日期,以下 Python 模块可用:timedatetimetimedeltarelativedelta。它们允许您计算日期值,这在演示和测试数据中经常使用,以便使用的日期接近模块安装日期。有关这些 Python 模块的更多信息,请参阅docs.python.org/3/library/datatypes.html中的文档。

例如,要将值设置为昨天,我们可以使用以下代码:

<field name="date_published"
       eval="(datetime.now() + timedelta(-1))" />

评估上下文中还可用 ref() 函数,它用于将外部标识符转换为相应的数据库 ID。这可以用于设置关系字段的值。以下是一个示例:

<field name="publisher_id" eval="ref('res_partner_packt')" />

此示例使用 eval 属性为 publisher_id 字段设置值。评估的表达式是使用特殊 ref() 函数的 Python 代码,该函数用于将 XML ID 转换为相应的数据库 ID。

在多对一关系字段上设置值

对于多对一关系字段,要写入的值是链接记录的数据库 ID。在 XML 文件中,我们通常知道记录的 XML ID,我们需要将其转换为实际的数据库 ID。

做这件事的一种方法是在 eval 属性中使用 ref() 函数,就像我们在上一节中做的那样。

一种更简单的方法是使用 ref 属性,该属性适用于 <field> 元素;例如:

<field name="publisher_id" ref="res_partner_packt" />

此示例为 publisher_id 多对一字段设置值,引用具有 res_partner_packt XML ID 的数据库记录。

在多对多关系字段上设置值

对于一对一和多对多字段,期望的是一个相关 ID 的列表,而不是单个 ID。此外,可以执行多个操作 - 我们可能想要用新的列表替换当前的相关记录列表,或者向其中添加一些记录,甚至解除一些记录的链接。

为了支持对多对字段的写操作,我们可以在 eval 属性中使用特殊语法。要写入多对字段,我们可以使用一个 三元组列表。每个 三元组 是一个 write 命令,根据第一个元素中使用的代码执行不同的操作。

要覆盖书籍作者列表,我们会使用以下代码:

<field name="author_ids"
       eval="[(6, 0, 
              [ref('res_partner_alexandre'), 
               ref('res_partner_holger')] 
              )]"
/>

要将链接记录追加到书籍作者的当前列表中,我们会使用以下代码:

<field name="author_ids" 
       eval="[(4, ref('res_partner_daniel'))]"
/>

上述示例是最常见的。在两种情况下,我们只使用了一个命令,但我们可以在外部列表中链式调用多个命令。append (4)replace (6) 命令是最常用的。在 append (4) 的情况下,三元组的最后一个值未使用且不需要,因此可以省略,就像我们在前面的代码示例中所做的那样。

可用的完整 多对写命令 列表如下:

  • (0, _, {'field': value}) 创建一个新记录并将其链接到当前记录。

  • (1, id, {'field': value}) 更新已链接记录上的值。

  • (2, id, _) 删除与 id 相关的记录的链接。

  • (3, id, _) 删除与 id 相关的记录的链接,但不删除该记录。这通常是你用于在多对多字段上删除相关记录时使用的方法。

  • (4, id, _) 链接一个已存在的记录。这只能用于多对多字段。

  • (5, _, _) 删除所有链接,但不删除链接的记录。

  • (6, _, [ids]) 用提供的列表替换了链接记录的列表。

在前面的列表中使用的 _ 下划线符号表示无关的值,通常用 0False 填充。

小贴士

可以安全地省略尾随的无关值。例如,(4, id, _) 可以用作 (4, id)

在本节中,我们学习了如何使用 <record> 标签将记录加载到数据库中。作为替代,有一些快捷标签可以用作常规 <record> 标签的替代。下一节将向我们介绍这些标签。

常用模型的快捷方式

如果我们回顾到 第三章您的第一个 Odoo 应用程序,我们将在 XML 文件中找到除了 <record> 之外的其他元素,例如 <menuitem>

这些是常用模型的便捷快捷方式,与常规 <record> 元素相比,具有更紧凑的表示法。它们用于将数据加载到支持用户界面的基础模型中,这些将在 第十章后端视图 - 设计用户界面 中更详细地探讨。

以下是可以用的快捷元素及其加载的数据对应的模型:

  • <menuitem> 用于菜单项模型,ir.ui.menu

  • <template> 用于存储在 ir.ui.view 模型中的 QWeb 模板。

    Odoo 14 的变化

    Odoo 的早期版本曾经支持额外的快捷标签,但这些标签现在不再受支持。曾经有一个 <act_window> 用于窗口动作模型,ir.actions.act_window,以及一个 <report> 用于报告动作模型,ir.actions.report.xml

重要的是要注意,当用于修改现有记录时,快捷元素会覆盖所有字段。这与 <record> 基本元素不同,后者只写入提供的字段。因此,对于需要修改用户界面元素特定字段的情况,我们应该使用 <record> 元素来操作。

在 XML 数据文件中使用其他操作

到目前为止,我们已经看到了如何使用 XML 文件添加或更新数据。但 XML 文件还允许您删除数据并执行任意模型方法。这对于更复杂的数据设置可能很有用。在接下来的章节中,我们将学习如何使用删除和函数调用 XML 功能。

删除记录

要删除数据记录,我们可以使用 <delete> 元素,并为其提供一个 ID 或搜索域以找到目标记录。

例如,使用搜索域查找要删除的记录如下所示:

<delete
  model="res.partner"
  search="[('id','=',ref(
    'library_app.res_partner_daniel'))]"
/>

如果我们知道要删除的特定 ID,我们可以使用它与 id 属性一起。这是上一个示例的情况,因此也可以这样写:

<delete model="res.partner" id="library_app.res_partner_daniel" />

这与上一个示例具有相同的效果。由于我们知道要查找的 ID,我们可以简单地使用 id 属性与 XML ID 一起,而不是使用带有域表达式的 search 属性。

调用模型方法

一个 XML 文件也可以通过 <function> 元素在其加载过程中执行任意方法。这可以用于设置演示和测试数据。

例如,Odoo 内置的 笔记 应用程序使用它来设置演示数据:

<data noupdate="1"> 
<function  
   model="res.users"  
   name="_init_data_user_note_stages"
   eval="[]" />
</data>

这调用 res.users 模型的 _init_data_user_note_stages 方法,不传递任何参数。参数列表由 eval 属性提供,在这种情况下是一个空列表。

这就完成了我们使用 XML 数据文件所需了解的所有内容。我们提供了 <data> 元素和 noupdate 标志的概述。然后我们学习了如何使用 <record> 元素来加载数据记录,以及如何设置相关字段的值。我们还了解了记录快捷方式,例如 <menuitem><template>。最后,我们学习了如何使用 <delete><function> 元素删除记录和执行任意函数调用。

这样,我们应该准备好使用 XML 数据文件来满足我们项目可能需要的任何数据需求。

摘要

在本章中,我们学习了如何在文本文件中表示数据。这些可以用于手动将数据导入 Odoo 或将其包含在附加模块中作为默认或 演示数据

到目前为止,我们应该能够从网络界面导出和导入 CSV 数据文件,并利用 外部 ID 来检测和更新数据库中已存在的记录。它们还可以用来对数据进行批量编辑,通过编辑并重新导入从 Odoo 导出的 CSV 文件。

我们还更详细地学习了 XML 数据文件的结构以及它们提供的所有功能。这些不仅包括字段上的设置值,还包括如删除记录和调用模型方法等操作。

在下一章中,我们将专注于如何使用 记录 来处理模型中包含的数据。这将为我们提供实施应用程序的业务逻辑和规则所必需的工具。

进一步阅读

官方的 Odoo 文档提供了关于数据文件的其他资源:www.odoo.com/documentation/15.0/developer/reference/backend/data.html

第四章:第六章:模型 – 结构化应用程序数据

在本章中,我们将更多地了解模型层以及如何使用模型来设计支持应用程序的数据结构。我们将探讨可用的模型类型,何时应该使用它们,以及如何定义强制数据验证的约束。

模型由支持多种数据类型的数据字段组成,某些字段类型支持定义模型之间的关系。字段的高级使用涉及使用特定的业务逻辑自动计算值。

本章将涵盖以下主题:

  • 学习项目 – 改进图书馆应用

  • 创建模型

  • 创建字段

  • 模型之间的关系

  • 计算字段

  • 模型约束

  • Odoo 基础模型的概述

在这些主题中,您将学习如何为您的 Odoo 项目创建非平凡的数据结构。到本章结束时,您应该对所有相关功能有一个清晰的概述,这些功能对于结构化数据模型是必需的。

技术要求

本章基于我们在第三章,“您的第一个 Odoo 应用程序”中创建的代码。此代码可在本书 GitHub 存储库的ch06/目录中找到,网址为github.com/PacktPublishing/Odoo-15-Development-Essentials

应将其放在您的附加组件路径中。请确保您已安装library_app模块。

学习项目 – 改进图书馆应用

第三章,“您的第一个 Odoo 应用程序”中,我们创建了library_app附加模块并实现了简单的library.book模型来表示图书目录。在本章中,我们将回顾该模块以丰富我们为每本书可以存储的数据。

我们将添加一个类别层次结构,用于使用以下结构进行图书分类:

  • 名称:类别标题

  • 父类别:它所属的父类别

  • 子类别:作为父类的这些类别

  • 特色图书或作者:代表此类别的选定图书或作者

将添加更多字段以展示 Odoo 字段可用的不同数据类型。我们还将使用模型约束在图书模型上实施一些验证:

  • 标题和出版日期应该是唯一的。

  • 输入的 ISBN 号应该是有效的。

我们将首先回顾 Odoo 模型,现在将更加深入,以了解我们可用的所有选项。

创建模型

模型是 Odoo 框架的核心。它们描述了应用程序数据结构,是应用程序服务器和数据库存储之间的桥梁。可以在模型周围实现业务逻辑以提供应用程序功能,并在其之上创建用户界面以提供用户体验。

在以下小节中,我们将了解模型的通用属性,这些属性用于影响其行为,以及我们可用的几种类型——常规模型临时模型抽象模型

模型属性

模型类可以使用额外的属性来控制某些行为。这些是最常用的属性:

  • _name:这是我们创建的 Odoo 模型的内部标识符。在创建新模型时这是必需的。

  • _description:这是一个用户友好的标题,可以用来引用单个 Model 记录,例如 Book。这是可选的但推荐使用。如果没有设置,则在加载序列期间将显示服务器日志警告。

  • _order:这设置了在浏览模型记录或以列表视图显示时使用的默认排序。它是一个用作 SQL 排序子句的文本字符串,因此可以是那里可以使用的任何内容,尽管它具有智能行为并支持可翻译的和多对一字段名称。

我们的 Book 模型已经使用了 _name_description 属性。以下代码添加了 _order 属性,以便默认按书名排序,然后按出版日期的逆序(从最新到最旧)排序:

class Book(models.Model):
    _name = "library.book"
    _description = "Book"
    _order = "name, date_published desc"

还有一些更高级的属性在复杂情况下可能很有帮助:

  • _rec_name:这设置了用于记录显示名称的字段。默认情况下,它是 name 字段,这就是为什么我们通常选择这个特定的字段名称作为记录标题字段的原因。

  • _table:这是支持模型的数据库表名称。通常,它由 ORM 自动设置,ORM 将使用模型名称,并将点替换为下划线。然而,我们可以自由选择要使用的特定数据库表名称。

  • _log_access=False:这可以用来防止自动创建审计跟踪字段;即 create_uidcreate_datewrite_uidwrite_date

  • _auto=False:这防止了底层数据库表被自动创建。在这种情况下,我们应该使用 init() 方法来提供我们创建支持数据库对象(表或视图)的特定逻辑。这通常用于支持只读报告的视图。

例如,以下代码在 library.book 模型上设置了默认值:

    _recname = "name"
    _table = "library_book"
    _log_access = True
    _auto = True

注意

此外,还有 _inherit_inherits 属性,它们用于模块扩展。这些在 第四章扩展模块 中有详细解释。

当使用 _auto = False 时,我们正在覆盖创建数据库对象的过程,因此我们应该提供相应的逻辑。这种应用的常见例子是基于数据库视图的模型,该视图收集了报告所需的所有数据。

下面是一个从 sale 核心模块中提取的示例,位于 sale/report/sale_report.py 文件中:

    def init(self):
        tools.drop_view_if_exists(self.env.cr, self._table)
        self.env.cr.execute(
            "CREATE or REPLACE VIEW %s as (%s)"
            % (self._table, self._query())
        )  

前面的代码使用了tools Python 模块,需要使用odoo import tools来导入。

模型和 Python 类

Odoo 模型使用 Python 类。在前面的代码中,我们可以看到一个基于models.Model类的 Python 类Book,它被用来定义一个名为library.book的 Odoo 模型。

Odoo 模型存储在一个中央注册表中,通过环境对象可访问,通常使用self.env来访问。中央注册表保存了对所有可用模型的引用,并且可以使用类似字典的语法来访问它们。

例如,要在方法内部获取图书馆书籍模型的引用,我们可以使用self.env["library.book"]self.env.get(["library.book"])

如你所见,模型名称很重要,是访问模型注册表的关键。

模型名称必须是全局唯一的。因此,一个好的做法是将模块所属的应用程序的第一词作为模型名称的第一词。例如,对于Library应用程序,所有模型名称都应该以library为前缀。核心模块的其他示例包括projectcrmsale

小贴士

模型名称应使用单数形式,如library.book,而不是library.books。约定是使用由点连接的单词列表。第一个单词应标识模型所属的主要应用程序,例如library.booklibrary.book.category。其他从官方插件中提取的示例包括project.projectproject.taskproject.task.type

另一方面,Python 类的标识符仅限于它们声明的 Python 文件中,并且与 Odoo 框架无关。用于它们的标识符仅对该文件中的代码有意义,并且很少相关。Python 类标识符的约定是使用驼峰式命名法,遵循 PEP8 编码约定定义的标准。

有几种类型的模型可供使用。最常用的一种是models.Model类,用于持久数据库存储模型。接下来,我们将了解其他可用的模型类型。

临时模型和抽象模型

对于大多数 Odoo 模型,Python 类基于models.Model。这种类型的模型具有永久数据库持久性,这意味着为它们创建了数据库表,并且它们的记录存储在它们被显式删除之前。大多数时候,这正是你所需要的。

但在某些情况下,我们不需要永久数据库持久性,因此这两种其他模型类型可能很有用:

  • models.TransientModel 用于向导风格的用户交互。它们的数据仍然存储在数据库中,但预期是临时的。定期清理这些表中的旧数据。例如,设置 | 翻译 | 导入翻译 菜单选项打开一个对话框窗口,该窗口使用临时模型来存储用户选择并实现向导逻辑。将在 第八章业务逻辑 – 支持业务流程 中讨论使用临时模型的示例。

  • models.AbstractModel 类没有附加数据存储。它们可以用作可重用的特征集,与其他模型一起使用 Odoo 的继承功能。例如,mail.thread 是之前提到的 mail.thread 示例中提供的抽象模型,在 第四章扩展模块 中进行了讨论。

检查现有模型

通过用户界面可以检查由 Python 类创建的模型和字段。启用 开发者模式,通过 设置 顶部菜单,导航到 技术 | 数据库结构 | 模型 菜单项。在这里,您将找到数据库中所有可用的模型列表。

在列表中单击模型将打开一个表单,显示其详细信息,如下面的截图所示:

图 6.1 – 从技术菜单检查书籍模型

图 6.1 – 从技术菜单检查书籍模型

这是一个检查模型的好工具,因为它显示了不同模块所做的所有修改的结果。在表单的右上角,library.booklibrary_applibrary_member 模块的影响。

小贴士

第一章使用开发者模式快速入门 所见,模型 表单是可编辑的!可以从这里创建和修改模型、字段和视图。您可以使用此功能构建原型,这些原型将作为附加模块稍后实现。

在下方区域,我们有一些带有附加信息的标签页:

  • 字段 列出了模型字段。

  • 访问权限 列出了授予安全组的访问控制规则。

  • 记录规则 列出了应用于记录的记录规则。

  • 说明 是模型定义的 docstring。

  • 视图 列出了模型的可用视图。

要找到模型的外部标识符或 XML ID,我们可以使用 model_。例如,由 library_app 模块创建的 library.book 模型生成的标识符是 library_app.model_library_book。这些 XML ID 通常用于定义安全 ACLs 的 CSV 文件。

我们现在熟悉了定义模型的可选方案。下一步是了解几个字段类型以及配置它们的选项。

创建字段

创建了新的模型后,下一步是向其中添加字段。Odoo 支持所有预期的基本数据类型,例如文本字符串、整数、浮点数、布尔值、日期和时间,以及图像或二进制数据。

让我们探索 Odoo 中可用的几种字段类型。

基本字段类型

我们将回到书籍模型,以展示可用的几种字段类型。

library_app/models/library_book.py 文件中,编辑 Book 类,将当前的字段定义替换为以下内容:

class Book(models.Model):
    _name = "library.book"
    _description = "Book"
    # String fields:
    name = fields.Char("Title")
    isbn = fields.Char("ISBN")
    book_type = fields.Selection(
        [("paper","Paperback"),
         ("hard","Hardcover"),
         ("electronic","Electronic"),
         ("other", "Other")],
        "Type")
    notes = fields.Text("Internal Notes") 
    descr = fields.Html("Description") 
    # Numeric fields:
    copies = fields.Integer(default=1)
    avg_rating = fields.Float("Average Rating", (3, 2))
    price = fields.Monetary("Price", "currency_id") 
    # price helper
    currency_id = fields.Many2one("res.currency")  
    # Date and time fields:
    date_published = fields.Date()
    last_borrow_date = fields.Datetime(
        "Last Borrowed On",
         default=lambda self: fields.Datetime.now()) 
    # Other fields:
    active = fields.Boolean("Active?")
    image = fields.Binary("Cover") 
    # Relational Fields
    publisher_id = fields.Many2one(
        "res.partner", string="Publisher")
    author_ids = fields.Many2many(
        "res.partner", string="Authors")

这些是 Odoo 中可用的非关系型字段类型示例,以及每个字段预期的定位参数。接下来,我们将解释所有这些字段类型和选项。

小贴士

Python 函数可以有两种类型的参数:定位参数和关键字参数。

fn(x, y) 应该是类似于 f(1, 2) 的形式。

f(x=1, y=2),或者甚至可以混合两种风格,例如 f(1, y=2)

然而,请注意,定位参数必须在关键字参数之前,因此不允许 f(x=1, 2)。有关关键字参数的更多信息,请参阅 Python 官方文档 docs.python.org/3/tutorial/controlflow.html#keyword-arguments

作为一般规则,第一个定位参数是字段标题,它对应于 string 关键字参数。此规则的例外是 Selection 字段和所有关系型字段。

string 属性用作用户界面标签的默认文本。如果没有提供 string 属性,它将自动从字段名称生成,将下划线替换为空格,并将每个单词的首字母大写。例如,date_published 的默认标签是 Date Published

作为参考,这是所有可用的非关系型字段类型列表,以及每个字段预期的定位参数:

  • Char(string) 是一个简单的文本字段。预期的唯一定位参数是字段标签。

  • Text(string) 是一个多行文本字段。唯一的定位参数也是字段标签。

  • Selection(selection, string) 是一个下拉选择列表。选择定位参数是一个 [("value", "Description"),] 的元组列表。对于每一对,第一个元素是存储在数据库中的值,第二个元素是在用户界面中展示的描述。扩展模块可以使用 selection_add 关键字参数向此列表添加选项。

  • Html(string) 被存储为文本字段,但具有针对 HTML 内容展示的用户界面特定处理。出于安全考虑,它默认被清理,但可以通过使用 sanitize=False 属性来覆盖此行为。

  • Integer(string) 用于整数数字,并期望一个字符串参数作为字段标签。

  • Float(string, digits) 存储浮点数,并有一个用于精度的第二个可选参数。这是一个 (n, d) 元组,其中 n 是总位数,d 是用于小数的位数。

  • Monetary(string, currency_field)float 字段类似,但具有对货币值的特定处理。currency_field 第二个参数是货币字段名称。默认情况下,它设置为 currency_field="currency_id"

  • Date(string)Datetime(string) 字段用于日期和日期时间值。它们只期望标签文本作为位置参数。

  • Boolean(string) 存储真或假值,并为标签文本提供一个位置参数。

  • Binary(string) 存储二进制数据,包括图像,并期望字符串标签位置参数。

这些字段定义提供了通常使用的参数。请注意,没有必需的参数,Odoo 将为缺失的参数使用合理的默认值。

Odoo 12 的变化

DateDatetime 字段现在在 ORM 中作为 Python 日期对象处理。在之前的版本中,它们被作为文本表示处理。因此,在操作时,需要显式转换为 Python 日期对象,之后还需要将其转换回文本字符串。

基于文本的字段,包括 CharTextHtml,有几个特定的属性:

  • size(仅适用于 Char 字段)设置最大允许大小。除非有很好的理由,否则建议不要使用它;例如,一个允许的最大长度为的社会安全号码。

  • translate=True 使字段内容可翻译,为不同语言持有不同的值。

  • trim 默认设置为 True 并自动修剪周围的空白符,这是由网络客户端执行的。这可以通过将 trim=False 显式禁用。

    Odoo 12 的变化

    trim 字段属性是在 Odoo 12 中引入的。在之前的版本中,文本字段会连同空白符一起保存。

此外,我们还有可用的关系字段类型。这些将在本章的 模型之间的关系 部分中稍后解释。

然而,在我们到达那里之前,还有更多关于基本字段类型属性的知识需要了解,如下一节所述。

常见字段属性

到目前为止,我们已经查看了几种基本字段类型的基本位置参数。然而,还有更多属性可供我们使用。

以下关键字参数属性通常对所有字段类型都可用:

  • string 是字段的默认标签,用于用户界面。除了 Selection 和关系字段外,它作为第一个位置参数可用,因此大多数情况下不作为关键字参数使用。如果没有提供,它将自动从字段名称生成。

  • default 为字段设置默认值。它可以是固定值(例如,active 字段中的 default=True),或可调用的引用,无论是命名的函数引用还是 lambda 匿名函数。

  • help 提供了当用户将鼠标悬停在 UI 字段上时显示的工具提示文本。

  • readonly=True 使字段在用户界面中默认不可编辑。这不在 API 层面上强制执行:模型方法中的代码仍然可以写入它,视图定义可以覆盖此设置。这仅是一个用户界面设置。

  • required=True 使字段在用户界面中默认为必填项。这在数据库级别通过向数据库列添加 NOT NULL 约束来强制执行。

  • index=True 在字段上添加数据库索引,以加快搜索操作,但会牺牲磁盘空间使用和较慢的写入操作。

  • copy=False 在通过 copy() ORM 方法复制记录时忽略字段。字段值默认会复制,除了多对多关系字段,它们默认不会复制。

  • deprecated=True 将字段标记为已弃用。它仍然会按常规工作,但任何对其的访问都会将警告消息写入服务器日志。

  • groups 允许您限制字段的访问和可见性,仅限于某些组。它期望一个以逗号分隔的安全组 XML ID 列表;例如,groups="base.group_user,base.group_system"

  • states 预期 UI 属性的字典映射值,这取决于 state 字段的值。可以使用的属性有 readonlyrequiredinvisible;例如,states={'done':[('readonly',True)]}

    小贴士

    注意,states 字段属性在视图中等同于 attrs 属性。此外,视图支持一个具有不同用途的 states 属性:它是一个逗号分隔的状态列表,其中视图元素应该是可见的。

下面是一个包含所有可用关键字参数的 name 字段示例:

    name = fields.Char(
        "Title",
        default=None,
        help="Book cover title.",
        readonly=False,
        required=True,
        index=True,
        copy=False,
        deprecated=True,
        groups="",
        states={},
    )

以前的 Odoo 版本支持 oldname="field" 属性,当字段在新版本中重命名时使用。它使得在模块升级过程中,旧字段中的数据可以自动复制到新字段中。

Odoo 13 的变化

oldname 字段属性已被移除,不再可用。替代方案是使用迁移脚本。

上述字段属性是通用的,适用于所有字段类型。接下来,我们将学习如何在字段上设置默认值。

设置默认值

如我们之前提到的,default 属性可以有一个固定值或对函数的引用,以动态计算默认值。

对于简单的计算,我们可以使用 lambda 函数来避免创建命名方法函数的开销。以下是一个使用当前日期和时间计算默认值的常见示例:

    last_borrow_date = fields.Datetime(
        "Last Borrowed On",
        default=lambda self: fields.Datetime.now(),
    )

default 值也可以是一个函数引用。这可以是一个名称引用或一个包含函数名称的字符串。

以下示例使用名称引用 _default_last_borrow_date 函数方法:

    def _default_last_borrow_date(self):
        return fields.Datetime.now()
    last_borrow_date = fields.Datetime(
        "Last Borrowed On",
        default=_default_last_borrow_date,
    )

此示例执行相同的操作,但使用包含函数名称的字符串:

    last_borrow_date = fields.Datetime(
        "Last Borrowed On",
        default="_default_last_borrow_date",
    ) 
    def _default_last_borrow_date(self):
        return fields.Datetime.now()

使用此方法,函数名称解析是在运行时延迟的,而不是在 Python 文件加载时。因此,在第二个示例中,我们可以引用代码中稍后声明的函数,而在第一个示例中,函数必须在函数声明之前声明。

然而,这里的通用代码约定是在字段定义之前定义默认值函数。支持静态代码分析的代码编辑器可以检测到输入错误,这也是首选第一种方法,即使用函数名称引用的另一个论据。

自动字段名称

一些字段名称是特殊的,要么是因为 ORM 为特殊目的保留了它们,要么是因为一些内置功能使用了某些默认字段名称。

id 字段被保留用于作为自动编号,唯一标识每条记录,并用作数据库的主键。它将自动添加到每个模型中。

除非设置了 _log_access=False 模型属性,否则以下字段将在新模型上自动创建:

  • create_uid 是创建记录的用户。

  • create_date 是记录创建的日期和时间。

  • write_uid 是用于记录最后修改用户。

  • write_date 是记录最后修改的日期和时间。

当在表单视图中查看时,这些字段的信息在 开发者模式 菜单中,然后点击 查看元数据 选项时在 Web 客户端可用。

前述字段名称对 Odoo 框架具有特殊意义。除此之外,还有一些字段名称被用作 Odoo 特性的默认值。下一节将描述它们。

保留字段名称

一些内置 API 功能默认期望特定的字段名称。这些被认为是保留字段名称,我们应该避免将它们用于预期之外的目的。

这些是保留字段:

  • Char 类型的 namex_name:这些默认用作记录的显示名称。但可以通过设置 _rec_name 模型属性来使用不同的字段作为显示名称。非字符字段类型也已知适用于此,并且将强制进行数字到文本的转换。

  • activex_activeBoolean 类型:这些允许您停用记录,使它们不可见。除非将 {'active_test': False} 键添加到环境上下文中,否则 active=False 的记录将自动排除在查询之外。它可以用作记录的 存档软删除 功能。

  • Selection 类型的 state:这代表记录生命周期的基本状态。它允许使用 states 字段属性动态设置 readonlyrequiredinvisible 属性;例如,states={'draft': [('readonly', False)]}

  • Many2one 类型的 parent_id:这个字段用于定义树状层次结构,并允许在域表达式中使用 child_ofparent_of 操作符。用作 parent_id 的字段可以通过 _parent_name 模型属性设置为不同的字段。

  • Char 类型的 parent_path:这可以用于优化域表达式中 child_ofparent_of 操作符的使用。为了正确操作,使用 add index=True 以使用数据库索引。我们将在本章的 层次关系 部分讨论层次关系。

  • Many2one 类型的 company_id:这个字段用于标识记录所属的公司。空值表示该记录在公司之间共享。它通过 _check_company 函数用于公司数据一致性的内部检查。

    Odoo 14 的变化

    x_active 现在被视为与 active 字段等效,可以用于相同的效果。这是为了更好地支持使用开发者模式Odoo Studio应用程序进行定制而引入的。

到目前为止,我们讨论了非关系型字段。但应用程序数据结构的大部分内容是关于描述实体之间的关系。现在让我们看看这一点。

模型之间的关系

非平凡的商务应用程序需要使用涉及的不同实体之间的关系。为此,我们需要使用关系型字段。

看一下 Library 应用程序,Book 模型有以下关系:

  • 每本书可以有一个出版者,每个出版者也可以有许多本书。从书籍的角度来看,这是一个多对一关系。它在数据库中通过一个整数字段实现,持有相关出版者记录的 ID,以及一个数据库外键,强制引用完整性。

  • 从出版者的角度来看,这是一对多关系,意味着每个出版者可以有许多本书。虽然这也在 Odoo 中是一个字段类型,但其数据库表示依赖于多对一关系。我们可以通过在书籍上运行查询并按出版者 ID 过滤来知道与出版者相关的书籍。

  • 每本书可以有多个作者,每个作者也可以有多个书。这是一个多对多关系。反向关系也是一个多对多关系。在关系型数据库中,多对多关系通过辅助数据库表来表示。Odoo 将自动处理这一点,尽管如果我们想的话,我们可以对技术细节进行一些控制。

我们将在接下来的章节中探讨这些关系的每一个。

特殊情况是层次关系,其中模型中的记录与同一模型中的其他记录相关联。我们将通过介绍一个图书分类模型来解释这一点。

最后,Odoo 框架还支持灵活的关系,其中同一字段能够表示与几个不同模型的关系。这些被称为Reference字段。

多对一关系

一个publisher_id字段代表对图书出版商的引用——合作伙伴模型中的一个记录。

提醒一下,这是仅使用位置参数的出版字段定义:

        publisher_id = fields.Many2one(
            "res.partner", "Publisher")

之前的Many2one字段定义使用了位置参数:

  • 第一个位置参数是相关模型,对应于comodel关键字参数,在这种情况下是res.partner

  • 第二个位置参数是字段标签,对应于string关键字参数。对于其他关系字段来说并非如此,因此首选选项是始终使用string作为关键字参数。

多对一模型字段在数据库表中创建一个列,有一个外键指向相关表,并持有相关记录的数据库 ID。

可以使用关键字参数代替或补充位置参数。这些是多对一字段支持的关键字参数:

  • ondelete: 这定义了当相关记录被删除时会发生什么。可能的行为如下:

    set null(默认值):当相关记录被删除时设置一个空值。

    restricted: 这将引发错误,防止删除。

    cascade: 当相关记录被删除时,这也会删除此记录。

  • context: 这是一个字典,包含对网络客户端视图有意义的用于在导航关系时携带信息的数据,例如设置默认值。这将在第八章中更详细地解释,业务逻辑 – 支持业务流程

  • domain: 这是一个域表达式——用于过滤在关系字段上可供选择的记录的元组列表。有关更多详细信息,请参阅第八章业务逻辑 – 支持业务流程

  • auto_join=True: 这允许 ORM 在搜索使用此关系时使用 SQL 连接。如果使用,将绕过访问安全规则,用户可能能够访问安全规则不允许的相关记录,但 SQL 查询将运行得更快。

  • delegate=True: 这将创建与相关模型相关的委托继承。当使用时,必须设置required=Trueondelete="cascade"属性。有关委托继承的更多信息,请参阅第四章扩展模块

一对多反向关系

一对多关系是多对一关系的逆关系。它列出与该记录有关系的记录。

例如,在图书馆书籍模型中,publisher_id字段与合作伙伴模型之间存在多对一关系。这意味着合作伙伴模型可以与书籍模型存在一对多反向关系,列出每个合作伙伴出版的书籍。

在创建一对一关系字段之前,应将反向多对一字段添加到相关模型中。为此,创建library_app/models/res_partner.py文件,并包含以下代码:

from odoo import fields, models
class Partner(models.Model):
    _inherit = "res.partner"
published_book_ids = fields.One2many(
        "library.book",
        "publisher_id",
        string="Published Books")

由于这是一个模块的新代码文件,它也必须添加到library_app/models/__init__.py文件中:

from . import library_book
from . import res_partner

One2many字段期望三个位置参数:

  • 相关模型,对应于comodel_name关键字参数

  • 用于引用此记录的相关模型字段,对应于inverse_name关键字参数

  • 字段标签,对应于string关键字参数

可用的附加关键字参数与多对一字段的相同:contextdomainauto_joinondelete(在这里,这些作用于关系的方)。

多对多关系

当两个实体之间都存在多对多关系时,使用多对多关系。以图书馆书籍为例,书籍和作者之间存在多对多关系:每本书可以有多个作者,每个作者也可以有多个书籍。

在书籍的一侧——即library.book模型——我们有以下内容:

class Book(models.Model)
    _name = "library.book"
    author_ids = fields.Many2many(
        "res.partner",
        string="Authors")

在作者的一侧,我们可以有res.partner模型的反向关系:

class Partner(models.Model): 
    _inherit = "res.partner"
    book_ids = fields.Many2many(
        "library.book",
        string="Authored Books")

Many2many最小签名期望一个位置参数用于相关模型——comodel_name关键字参数——并且建议也提供带有字段标签的string参数。

在数据库级别,多对多关系不会向现有表添加任何列。相反,自动创建一个特殊的关系表来存储记录之间的关系。这个特殊表只有两个 ID 字段,每个相关表都有一个外键。

默认情况下,关系表的名字是两个表名通过下划线连接,并在末尾附加_rel。在我们的书籍或作者关系的情况下,它应该命名为library_book_res_partner_rel

在某些情况下,我们可能需要覆盖这些自动默认值。这种情况之一是当相关模型具有长名称时,自动生成的关联表名称过长,超过了 63 个字符的 PostgreSQL 限制。在这些情况下,我们需要手动选择关系表名称以符合表名大小限制。

另一个情况是我们需要在同一模型之间建立第二个多对多关系。在这些情况下,必须手动提供关系表名称,以避免与第一个关系已使用的表名称冲突。

有两种方法可以手动覆盖这些值:要么使用位置参数,要么使用关键字参数。

当使用位置参数进行字段定义时,字段定义看起来是这样的:

# Book <-> Authors relation (using positional args)
author_ids = fields.Many2many( 
    "res.partner",
    "library_book_res_partner_rel",
    "a_id",
    "b_id",
    "Authors")

可以使用关键字参数代替,这可能会更易于阅读:

# Book <-> Authors relation (using keyword args)
author_ids = fields.Many2many(
    comodel_name="res.partner", 
    relation="library_book_res_partner_rel",
    column1="a_id",
    column2="b_id",
    string="Authors")

这里使用了以下参数:

  • comodel_name是相关模型的名称。

  • relation是支持关系数据的数据库表名称。

  • column1是引用模型记录的列名。

  • column2是引用相关模型记录的列名。

  • string是用户界面中的字段标签。

与一对一关系字段类似,多对多字段也可以使用contextdomainauto_join关键字参数。

小贴士

在抽象模型上,不要使用多对多字段column1column2属性。在 ORM 设计方面,抽象模型存在限制,当你强制设置关系列的名称时,它们将无法再干净地继承。

父子关系是一个值得详细研究的特殊情况。我们将在下一节中这样做。

层次关系

父子树关系使用与同一模型的多对一关系表示,其中每个记录都包含对其父记录的引用。反向的一对多关系表示记录的直接子项。

Odoo 提供了对这些层次数据结构的改进支持,使得child_ofparent_of运算符在域表达式中可用。只要模型有一个parent_id字段(或者模型有一个有效的_parent_name定义,设置一个用于此目的的替代字段名称),这些运算符都是可用的。

通过设置_parent_store=True模型属性并添加parent_path辅助字段,可以启用优化的层次树搜索。这个辅助字段存储有关层次树结构的额外信息,用于运行更快的查询。

Odoo 12 中的更改

parent_path层次辅助字段是在 Odoo 12 中引入的。之前的版本使用了parent_leftparent_right整数字段来达到相同的目的,但自 Odoo 12 起,这些字段已被弃用。

作为层次结构的示例,我们将在图书馆应用中添加一个分类树,用于对书籍进行分类。

让我们添加library_app/models/library_book_category.py文件,以及以下代码:

from odoo import api, fields, models
class BookCategory(models.Model):
    _name = "library.book.category"
    _description = "Book Category"
    _parent_store = True
    name = fields.Char(translate=True, required=True)
    # Hierarchy fields
    parent_id = fields.Many2one(
        "library.book.category",
        "Parent Category",
        ondelete="restrict")
    parent_path = fields.Char(index=True)
    # Optional, but nice to have:
    child_ids = fields.One2many(
        "library.book.category",
"parent_id",
        "Subcategories")

这里,我们有一个基本的模型,包含一个parent_id字段来引用父记录。

为了实现更快的树搜索,我们添加了 _parent_store=True 模型属性。这样做时,必须也添加 parent_path 字段,并且它必须被索引。用于引用父记录的字段预期命名为 parent_id,但可以使用任何其他字段名,只要我们在 _parent_name 可选模型属性中声明即可。

添加一个字段以列出直接子项通常很方便。这是前述代码中显示的一对多反向关系。

为了让我们的模块使用之前的代码,请记住在 library_app/models/__init__.py 中添加对其文件的引用:

from . import library_book_category
from . import library_book
from . import res_partner

注意,这些额外的操作会带来存储和执行时间上的惩罚,因此最好在您预期读取频率高于写入频率的情况下使用,例如在分类树的情况下。这仅在优化具有许多节点的深层层次结构时才是必要的;这可能会被用于小型或浅层层次结构。

使用引用字段实现灵活的关系

正规的关系字段只能引用一个固定的共同模型。Reference 字段类型没有这种限制,并支持灵活的关系,相同的字段可以引用来自不同目标模型的记录。

例如,我们将向图书分类模型添加一个 Reference 字段,以指示突出显示的图书或作者。此字段可以链接到图书或合作伙伴记录:

    highlighted_id = fields.Reference(
        [("library.book", "Book"), ("res.partner",
           "Author")],
        "Category Highlight",
    )

字段定义类似于 Selection 字段,但在这里,选择列表包含可用于字段的模型。在用户界面中,用户将从可用列表中选择一个模型,然后从该模型中选择一个特定记录。

引用字段以字符字段的形式存储在数据库中,包含一个 <model>,<id> 字符串。

Odoo 12 的变化

以前的 Odoo 版本具有可引用的模型配置,可用于从 Reference 字段选择在 Reference 字段中使用的模型,通过在模型选择列表中添加 odoo.addons.res.res_request.referenceable_models 函数来实现。此功能已在 Odoo 12 中删除。

通过这些,我们已经看到了 Odoo 支持的字段类型。字段不仅可以存储用户提供的数据,还可以呈现计算值。下一节将介绍这一功能。

计算字段

字段可以由一个函数自动计算其值,而不是简单地读取数据库存储的值。计算字段就像常规字段一样声明,但具有额外的 compute 参数来定义用于计算的函数。

计算字段涉及编写一些业务逻辑。因此,为了充分利用这一功能,我们应该熟悉将在 第八章 中解释的主题,即 业务逻辑 – 支持业务流程。计算字段仍将在此处解释,但我们将尽可能保持业务逻辑简单。

作为示例,我们将向 Books 模型添加一个计算字段,显示出版商的国家。这将允许在表单视图中显示国家。

找到所需值的代码很简单:如果 book 代表一本书的记录,我们可以使用对象点符号通过 book.publisher_id.country_id 获取出版商的国家。

通过在 library_app/models/library_book.py 文件中添加以下代码来编辑书籍模型:

    publisher_country_id = fields.Many2one(
        "res.country", string="Publisher Country",
        compute="_compute_publisher_country",
    )
    @api.depends("publisher_id.country_id")
    def _compute_publisher_country(self):
        for book in self:
book.publisher_country_id = 
              book.publisher_id.country_id

首先,此代码添加了 publisher_country_id 字段,并使用用于其计算的方法函数名称设置计算属性,即 _compute_publisher_country

函数名称作为字符串参数传递给了字段,但它也可以作为可调用的引用(函数标识符,不带引号)传递。在这种情况下,我们需要确保在字段之前在 Python 文件中定义了该函数。

计算方法名称的编码约定是在计算字段名称后附加 _compute_ 前缀。

_compute_publisher_country 方法接收一个 self 记录集,用于操作,并预期为这些记录设置计算字段值。代码应该在 self 记录集上迭代,以对每条记录进行操作。

计算值使用常规的赋值(写入)操作设置。在我们的例子中,计算相当简单:我们将其分配给当前书籍的 publisher_id.country_id 值。

小贴士

同样的计算方法可以用来计算两个或更多字段。在这种情况下,应该在计算字段的 compute 属性上使用该方法,计算方法应该为所有这些字段分配值。

计算函数必须始终为要计算的字段或字段分配一个值。如果你的计算方法有 if 条件,请确保所有运行路径都为计算字段分配值。如果遗漏了为某些计算字段分配值,计算方法将报错。

Odoo 13 的变化

Odoo 13 引入了 可计算的写入字段,旨在未来替代 onchange 机制。可计算的写入字段具有计算逻辑,由依赖关系的变化触发,并允许用户直接设置值。这个机制将与 onchange 一起在 第八章 中讨论,业务逻辑 – 支持业务流程

@api.depends 装饰器用于指定计算所依赖的字段。ORM 使用它来知道何时触发计算以更新存储或缓存的值。接受一个或多个字段名作为参数,并可以使用点符号来跟踪字段关系。在这个例子中,当 publisher_id.country_id 发生变化时,应该重新计算 publisher_country_id 字段。

警告

忘记添加@api.depends装饰器到计算方法中,或者添加了但未能添加所有用于计算的依赖字段,将阻止计算字段在应该重新计算时进行计算。这可能导致难以识别的错误。

我们可以通过将publisher_country_id字段添加到书籍表单视图(在library_app/views/library_book.xml文件中)来查看我们工作的结果。确保在尝试使用 Web 客户端时,所选的出版商已设置国家。

在计算字段上进行搜索和写入

我们创建的计算字段可以读取,但不能搜索或写入。默认情况下,计算字段值在读取时立即计算,其值不存储在数据库中。这就是为什么它们不能像常规存储字段那样进行搜索。

一种绕过此限制的方法是将计算值存储在数据库中,通过添加store = True属性。当任何依赖项更改时,它们将重新计算。由于值现在已存储,它们可以像常规字段一样进行搜索,因此不需要搜索函数。

计算字段也支持搜索和写入操作,而无需存储在数据库中。这可以通过实现这些操作的专用函数以及compute函数来实现:

  • 实现搜索逻辑的search函数

  • 实现写入逻辑的inverse函数

使用这些,我们的计算字段声明将如下所示:

    publisher_country_id = fields.Many2one(
        "res.country",
        string="Publisher Country",
        compute="_compute_publisher_country",
        inverse="_inverse_publisher_country",
        search="_search_publisher_country",
    )

要在计算字段上写入,我们必须实现值计算的逻辑。这就是为什么负责处理写入操作的功能被称为inverse

在此示例中,设置publisher_country_id的值预计会更改出版商的国家。

注意,这也会更改所有此出版商的书籍中看到的值。常规访问控制适用于这些写入操作,因此此操作只有在当前用户还需要对合作伙伴模型进行写入访问时才会成功。

此逆函数实现使用设置在计算字段上的值来执行实际写入操作,以使此更改持久化:

    def _inverse_publisher_country(self):
        for book in self:
            book.publisher_id.country_id = 
              book.publisher_country_id

原始值计算将book.publisher_id.country_id值复制到book.publisher_country_id字段。之前显示的逆实现执行相反的操作。它读取book.publisher_country_id上设置的值并将其写入book.publisher_id.country_id字段。

要启用计算字段的搜索操作,必须实现其search函数。search函数拦截在计算字段上操作的域表达式,然后使用仅包含常规存储字段的替代域表达式来替换它们。

publisher_country_id示例中,实际搜索应该在链接的publisher_id合作伙伴记录的country_id字段上执行。以下是此转换的函数实现:

    def _search_publisher_country(self, operator, value):
       return [
            ("publisher_id.country_id", operator, value)
        ]
"

当我们在模型上执行搜索时,会使用一个域表达式元组作为参数,给出操作符和域表达式中使用的值的详细信息。

当这个计算字段在域表达式条件中找到时,会触发 search 函数。它接收搜索的 operatorvalue,并期望将原始搜索元素转换为替代域搜索表达式。country_id 字段存储在相关合作伙伴模型中,因此我们的搜索实现只是修改了原始搜索表达式,使用 publisher_id.country_id 字段代替。

为了参考,域表达式将在第八章中更详细地解释,业务逻辑 – 支持业务流程

相关字段

在上一节中我们实现的计算字段只是简单地将一个值从一个相关记录复制到模型的一个字段。这是一个常见的用例,当我们想要从一个相关记录中展示一个字段时就需要它。Odoo 框架为此提供了一个快捷方式:相关字段功能。

相关字段使属于相关模型的字段在模型中可用,并且可以通过点符号链访问。这使得它们在点符号无法使用的情况下也变得可用,例如 UI 表单视图。

要创建一个相关字段,必须声明一个所需类型的字段,并使用 related 属性,需要点符号字段链来达到目标相关字段。

可以使用 related 字段来达到与之前 publisher_country_id 计算字段示例相同的效果。

这里是另一种实现方式,现在使用 related 字段:

    publisher_country_id = fields.Many2one(
        "res.country",
        string="Publisher Country",
        related="publisher_id.country_id",
    )

在幕后,相关字段只是计算字段,它们还方便地实现了 searchinverse 方法。因此,它们可以被搜索和写入。

默认情况下,相关字段是只读的,因此反向写操作将不可用。要启用它,设置 readonly=False 字段属性。

Odoo 12 的变化

在之前的 Odoo 版本中,相关字段默认是可写的,但已被证明这是一个危险的默认设置,因为它可能允许在不期望允许的情况下更改设置或主数据。因此,从 Odoo 12 开始,related 字段现在是默认只读的:readonly=True

值得注意的是,related 字段也可以使用 store=True 在数据库中存储,就像任何其他计算字段一样。

有了这些,我们已经了解了 Odoo 字段支持的功能,包括计算字段。关于数据结构的一个重要元素是强制数据质量和完整性的约束。这就是下一节将要讨论的内容。

模型约束

通常,应用程序需要确保数据完整性并执行验证以确保数据完整和正确。

PostgreSQL 数据库管理器支持许多有用的验证,例如避免重复或检查值是否满足某些简单条件。Odoo 模型可以使用 PostgreSQL 约束功能来实现这一点。

一些检查需要更复杂的逻辑,并且最好用 Python 代码实现。对于这些情况,我们可以使用特定的模型方法来实现该 Python 约束逻辑。

让我们更深入地了解这两种可能性。

SQL 模型约束

SQL 约束添加到数据库表定义中,并由 PostgreSQL 直接执行。它们使用 _sql_constraints 类属性声明。

它是一个元组列表,每个元组的格式为 (name, sql, message)

  • name 是约束标识符名称。

  • sql 是 PostgreSQL 约束的语法。

  • message 是当约束未验证时向用户展示的错误信息。

最常用的 SQL 约束是 UNIQUE 约束,用于防止数据重复,以及 CHECK 约束,用于在数据上测试 SQL 表达式。

例如,我们将向 Book 模型添加两个约束:

  • 确保存在具有相同标题和出版日期的重复书籍。

  • 确保出版日期不是未来的日期。

通过添加以下代码来编辑 library_app/models/library_book.py 文件,这些代码实现了这两个约束。通常,这应该在字段声明部分的代码之后:

    _sql_constraints = [
        ("library_book_name_date_uq",
         "UNIQUE (name, date_published)",
        "Title and publication date must be unique."),
        ("library_book_check_date",
         "CHECK (date_published <= current_date)",
         "Publication date must not be in the future."),
    ]

有关 PostgreSQL 约束语法的更多信息,请参阅官方文档:www.postgresql.org/docs/current/ddl-constraints.html

Python 模型约束

Python 约束可以使用任意代码来执行验证。验证函数应该用 @api.constrains 装饰,并包含在检查中涉及的字段列表。当这些字段中的任何一个被修改时,将触发验证,如果条件失败则应抛出异常——通常是 ValidationError

在图书馆应用的情况下,一个明显的例子是防止插入错误的 ISBN。我们已经在 _check_isbn() 方法中有了检查 ISBN 是否正确的逻辑。我们可以在模型约束中使用这个逻辑来防止保存错误的数据。

通过前往文件顶部并添加以下 import 语句来编辑 library_app/models/library_book.py 文件:

from odoo.exceptions import ValidationError

现在,在同一个文件中,向 Book 类添加以下代码:

    @api.constrains("isbn")
    def _constrain_isbn_valid(self):
        for book in self:
            if book.isbn and not book._check_isbn():
                raise ValidationError(
                    "%s is an invalid ISBN" % book.isbn)

Python SQL 约束通常添加在包含字段声明的代码部分之前。

Odoo 基础模型概述

在前面的章节中,我们有机会创建新的模型,例如 Book 模型,但我们还使用了 Odoo 基础模块提供的已存在的模型,例如 Partner 模型。在本节中,我们将对这些内置模型进行简要介绍。

Odoo 核心框架包括 base 扩展模块。它提供了 Odoo 应用程序运行所需的基本功能。它可以在 Odoo 仓库中找到,位于 ./odoo/addons/base 子目录下。

标准扩展模块,它们提供了 Odoo 官方应用程序和功能,依赖于并构建在 base 模块之上。标准扩展模块可以在 Odoo 仓库中找到,位于 ./addons 子目录下。

base 模块提供了两种类型的模型:

  • 信息仓库,ir.*,模型

  • 资源,res.*,模型

信息仓库模型用于存储 Odoo 框架所需的基本数据,如菜单、视图、模型和操作。我们在 技术 菜单中找到的数据通常存储在信息仓库模型中。

以下是一些相关示例:

  • ir.actions.act_window 用于 窗口操作

  • ir.config_parameter 用于全局配置选项

  • ir.ui.menu 用于 菜单项

  • ir.ui.view 用于 视图

  • ir.model 用于 模型

  • ir.model.fields 用于模型 字段

  • ir.model.data 用于 XML ID

资源模型存储了任何模块都可以使用的基本主数据。

这些是最重要的资源模型:

  • res.partner 用于商业伙伴,如客户和供应商,以及地址

  • res.company 用于公司数据

  • res.country 用于国家

  • res.country.state 用于国家内部的状态或地区

  • res.currency 用于货币

  • res.groups 用于应用程序安全组

  • res.users 用于应用程序用户

这应该提供了有用的背景信息,以帮助您了解这些模型的起源。

摘要

在本章中,我们学习了不同类型的模型,例如临时模型和抽象模型,以及为什么它们分别对用户界面向导和混入有用。其他相关的模型特性包括 Python 和 SQL 约束,这些可以用来防止数据输入错误。

我们还学习了可用的字段类型,以及它们支持的所有属性,以便尽可能准确地表示业务数据。我们还学习了关系字段,以及如何使用它们来创建应用程序中使用的不同实体之间的关系。

之后,我们了解到模型通常基于 models.Model 类,但我们也可以使用 models.Abstract 为可重用混入模型和 models.Transient 为向导或高级用户交互对话框。我们看到了可用的通用模型属性,例如 _order 用于默认排序顺序和 _rec_name 用于记录表示的默认字段。

模型中的字段定义了它们将存储的所有数据。我们还看到了可用的非关系字段类型及其支持的属性。我们还学习了多种关系字段类型——多对一、一对多和多对多——以及它们如何定义模型之间的关系,包括层次父子关系。

大多数领域将用户输入存储在数据库中,但字段可以通过 Python 代码自动计算值。我们看到了如何实现计算字段以及我们拥有的某些高级可能性,例如使它们可写和可搜索。

模型定义的一部分是约束,强制数据一致性,以及验证。这些可以使用 PostgreSQL 或 Python 代码实现。

现在我们已经创建了数据模型,我们应该用一些默认和演示数据来填充它。在下一章中,我们将学习如何使用数据文件通过我们的系统导出、导入和加载数据。

进一步阅读

模型的官方文档可以在www.odoo.com/documentation/15.0/developer/reference/backend/orm.html.找到。

第三部分:业务逻辑

在第三部分,我们解释了如何在模型周围编写业务逻辑层,对应于架构中的控制器组件。这包括用于操作模型数据的内置对象关系映射ORM)函数,以及用于消息和通知的社会功能。

在本节中,包括以下章节:

  • 第七章**,记录集 – 与模型数据交互

  • 第八章**,业务逻辑 – 支持业务流程

  • 第九章**,外部 API – 与其他系统集成

第五章:第七章:记录集 – 与模型数据交互

在前面的章节中,我们概述了模型创建和将数据加载到模型中的过程。现在我们已经有了数据模型和一些可以操作的数据,是时候学习如何以编程方式与之交互了。

一个商业应用程序需要业务逻辑来计算数据、执行验证或自动化操作。Odoo 框架 API 为开发者提供了实现这种业务逻辑的工具。大多数情况下,这意味着查询、转换和写入数据。

Odoo 在底层数据库之上实现了一个 对象关系映射ORM)层。ORM 对象提供了 应用程序编程接口API),用于与数据交互。此 API 提供了执行环境和创建 记录集,这些是用于操作数据库中存储的数据的对象。

本章解释了如何使用执行环境和记录集,以便你拥有实现业务流程所需的所有工具。

在本章中,我们将涵盖以下主题:

  • 使用 shell 命令交互式探索 ORM API

  • 理解执行环境和上下文

  • 使用记录集和域查询数据

  • 访问记录集中的数据

  • 向记录写入

  • 与日期和时间交互

  • 与记录集交互

  • 事务和低级 SQL

到本章结束时,你应该能够使用 Odoo 代码执行所有这些操作,并且你也将准备好使用这些工具来实现你自己的业务流程。

技术要求

本章中的代码示例将在交互式 shell 中执行,不需要前几章中的任何代码。代码的副本可以在 ch07/ch07_recorsets_code.py 文件中找到。

使用 shell 命令

shell 命令选项。这些命令可以交互式执行,以更好地理解它们的工作方式。

要使用它,请在启动 Odoo 时添加 shell 命令以及我们通常在启动 Odoo 时使用的任何 Odoo 选项:

(env15) $ odoo shell -c library.conf

这将在终端中启动通常的服务器启动序列,但不会启动一个监听请求的 HTTP 服务器,而是启动一个等待输入的 Python 提示符。

这个交互式命令接口模拟了在 class 方法内部找到的环境,在 OdooBot 超级用户下运行。self 变量可用,并设置为 OdooBot 超级用户记录对象。

例如,以下命令检查 self 记录集:

>>> self
res.users(1,)
>>> self._name
'res.users'
>>> self.name
'OdooBot'
>>> self.login
'__system__'

之前的命令打印出以下内容:

  • self 变量包含一个 res.users 记录集,其中包含一个 ID 1 的记录。

  • 检查 self._name 的记录集模型名称是 res.users,正如预期的那样。

  • 记录字段 name 的值为 OdooBot

  • 记录字段 login 的值为 __system__

    Odoo 12 的变化

    ID 1 超级用户从 admin 更改为内部的 __system__ 用户。admin 用户现在是 ID 2 用户,不再是超级用户,尽管 Odoo 标准应用程序会小心地自动授予它对它们的完全访问权限。进行这种更改的主要原因是为了避免用户使用超级用户账户执行日常活动。这样做是危险的,因为这种更改绕过了所有访问规则,可能会导致不一致的数据,例如跨公司关系。现在它仅用于故障排除或非常具体的跨公司操作。

与 Python 一样,要退出提示符,请按 Ctrl + D。这将关闭服务器进程并返回到系统外壳提示符。

我们现在知道了如何启动 Odoo 壳会话。这对于我们探索 Odoo API 功能非常重要。因此,让我们使用它来探索执行环境。

执行环境

Odoo 记录集在 环境 上下文中操作,提供有关触发操作上下文的相关信息。例如,正在使用的数据库游标、当前 Odoo 用户等。

在模型方法内部运行的 Python 代码可以访问 self 记录集变量,并且可以通过 self.env 访问局部环境。服务器外壳环境也以类似方式提供 self 引用,就像在方法内部找到的那样。

在本节中,我们将了解执行环境提供的属性以及如何使用它们。

环境属性

正如我们所见,self 是一个记录集。记录集携带环境信息,例如浏览数据的用户和额外的上下文相关信息(例如,活动语言和时间区域)。

可以使用记录集的 env 属性访问当前环境,如下例所示:

>>> self.env
<odoo.api.Environment object at 0x7f6882f7df40>

self.env 中的执行环境有以下属性可用:

  • env.cr 属性是正在使用的数据库游标。

  • env.user 属性是当前用户的记录。

  • env.uid 属性是会话用户的 ID。它与 env.user.id 相同。

  • env.context 属性是一个不可变的字典,包含会话上下文数据。

  • env.company 属性是活动公司。

  • env.companies 属性是用户允许的公司。

    Odoo 13 的变化

    env.companyenv.companies 属性是在 Odoo 13 中引入的。在之前的版本中,此信息是通过使用 env.user.company_idenv.user.company_ids 从用户记录中读取的。

环境还提供了对所有已安装模型可用的注册表的访问权限。例如,self.env["res.partner"] 返回对 partner 模型的引用。然后我们可以使用 search()browse() 在其上创建记录集:

>>> self.env["res.partner"].search([("display_name", "like", "Azure")])
res.partner(14, 26, 33, 27)

在这个例子中,返回给res.partner模型的记录集包含三个记录,ID 分别为14263327。记录集不是按 ID 排序的,因为使用了对应模型的默认顺序。在合作伙伴模型的情况下,默认对象_orderdisplay_name

环境上下文

context对象是一个字典,携带会话数据,可以在客户端用户界面和服务器端 ORM 及业务逻辑中使用。

从客户端来看,它可以携带信息从一个视图传递到下一个视图——例如,在跟随链接或按钮后,上一个视图上活动的记录 ID——或者它可以为下一个视图提供默认值。

在服务器端,某些记录集字段值可以依赖于上下文提供的区域设置。特别是,lang键影响可翻译字段的值。

上下文还可以为服务器端代码提供信号。例如,当active_test键设置为False时,它会改变 ORM search()方法的行为,使其不对非活动记录应用自动过滤器,忽略active记录字段。

来自 Web 客户端的初始上下文看起来像这样:

>>> self.env.context
{'lang': 'en_US', 'tz': 'Europe/Brussels'}

在这里,你可以看到带有用户语言的lang键和带有时区信息的tz。根据当前上下文,记录中的内容可能不同:

  • 翻译字段可以根据活动的lang语言有不同的值。

  • 日期时间字段在返回给客户端时,可以根据活动的tz时区显示不同的时间。

当从一个视图的链接或按钮打开视图时,Web 客户端将自动将一些键添加到上下文中,提供有关我们正在导航的记录的信息:

  • active_model是之前的模型名称。

  • active_id是用户定位的原始记录的 ID。

  • active_ids是在用户从列表视图导航时选择的 ID 列表。

向导助手经常使用这些键来找到他们预期要操作的记录。

可以使用具有这些特定前缀的键来使用上下文设置目标 Web 客户端视图的默认值并激活默认过滤器:

  • 添加到字段名称的default_前缀为该字段设置默认值。例如,{'default_user_id': uid}将当前用户设置为默认值。

  • 添加到过滤器名称的default_search_前缀将自动启用该过滤器。例如,{'default_search_filter_my_tasks': 1}激活名为filter_my_books的过滤器。

这些前缀经常用于<field context="{...}">元素中。

修改记录集执行环境和上下文

记录集执行上下文可以被修改以利用上一节中描述的行为,或者向调用该记录集的方法中添加信息。

可以通过以下方法修改环境和其上下文。这些方法中的每一个都返回一个新的记录集,以及一个带有修改后的环境的原始副本:

  • <recordset>.with_context(<dictionary>)方法用提供的字典中的上下文替换上下文。

  • <recordset>.with_context(key=value, ...)方法通过在它上设置提供的属性来修改上下文。

  • <recordset>.sudo([flag=True])方法启用或禁用超级用户模式,允许它绕过安全规则。上下文用户保持不变。

  • <recordset>.with_user(<user>)方法将用户修改为提供的用户,该用户可以是用户记录或 ID 号。

  • <recordset>.with_company(<company>)方法将公司修改为提供的公司,该公司可以是公司记录或 ID 号。

  • <recordset>.with_env(<env>)方法将记录集的完整环境修改为提供的环境。

    Odoo 13 中的更改

    with_user()with_company()方法是在 Odoo 13 中引入的。为了切换用户,旧版本使用sudo([<user>])方法,该方法可以提供给特定用户以切换到超级用户上下文。为了切换公司,旧版本使用with_context(force=company=<id>),设置一个在相关业务逻辑中检查的context键。

此外,环境对象提供了env.ref()函数,它接受一个带有外部标识符的字符串,并返回相应的记录,如下面的示例所示:

>>> self.env.ref('base.user_root')
res.users(1,)

如果外部标识符不存在,将引发一个ValueError异常。

当在 Odoo 服务器中运行 Python 代码时,我们学习了更多关于执行环境的知识。下一步是与数据交互。在这种情况下,首先要学习的是如何查询数据和创建记录集,这将在下一节中讨论。

使用记录集和域查询数据

Odoo 业务逻辑需要从数据库中读取数据以执行基于它的操作。这是通过记录集完成的,它查询原始数据并将其暴露为我们可以操作的 Python 对象。

Odoo Python 通常在类方法中运行,其中self代表要处理的记录集。在某些情况下,我们需要为其他模型创建记录集。为此,我们应该获取模型的引用,然后查询它以创建记录集。

环境对象,通常可通过self.env访问,持有对所有可用模型的引用,并且可以使用类似字典的语法访问它们。例如,要获取partner模型的引用,请使用self.env['res.partner']self.env.get('res.partner')。然后,可以使用此模型引用创建记录集,正如我们将在下面看到的那样。

创建记录集

search()方法接受一个域表达式,并返回一个与这些条件匹配的记录集。例如,[('name', 'like', 'Azure')]将返回所有包含name字段为Azure的记录。

如果模型有active特殊字段,则默认情况下,只有active=True的记录将被考虑。

以下关键字参数也可以使用:

  • order关键字是作为数据库查询中的ORDER BY子句使用的字符串。这通常是一个字段名的逗号分隔列表。每个字段名后面可以跟DESC关键字来表示降序。

  • limit关键字设置要检索的最大记录数。

  • offset关键字忽略前n个结果;它可以与limit一起使用,以每次查询记录块。

有时,我们只需要知道满足某些条件的记录数量。为此,我们可以使用search_count(),它以更有效的方式返回记录计数而不是记录集。

browse()方法接受一个 ID 列表或单个 ID,并返回包含这些记录的记录集。在已知我们想要的记录的 ID 的情况下,这可能是方便的。

例如,要获取所有显示名称中包含Lumber的合作伙伴记录,可以使用以下search()调用:

>>> self.env['res.partner'].search([('display_name', 'like', 'Lumber')])
res.partner(15, 34)

在已知要查询的 ID 的情况下,使用browse()调用,如下例所示:

>>> self.env['res.partner'].browse([15, 34]) 
res.partner(15, 34)

大多数情况下,ID 是未知的,所以search()方法比browse()方法使用得更频繁。

要充分利用search(),需要对域过滤器语法有良好的理解。因此,我们将在下一节中关注这一点。

域表达式

用于查询数据库的WHERE表达式。一个('<field>', '<operator>', <value>)元组。例如,以下是一个有效的域表达式,包含单个条件:[('is_done', '=', False)]。没有条件的域表达式也是允许的。这翻译为一个空列表([]),结果是返回所有记录的查询。

实际上,域有两个可能的评估上下文:客户端,例如在窗口操作和 Web 客户端视图中,以及服务器端,例如在安全记录规则和模型方法 Python 代码中。在<field><value>元素中可以使用的内容可能取决于评估上下文。

接下来,我们将详细解释域条件中的每个元素:字段运算符

域条件的字段元素

第一个条件元素是一个字符串,包含正在过滤的字段名称。当在服务器端使用域表达式时,字段元素可以使用点符号来访问相关模型的值。例如,我们可以使用类似'publisher_id.name'的东西,甚至'publisher_id.country_id.name'

在客户端,不允许使用点符号,只能使用简单的字段名。

小贴士

在需要客户端域表达式中的相关记录值,但点符号无法使用的情况下,解决方案是在模型中通过使用related=属性添加一个相关字段。这样,值就可以作为直接可访问的模型字段来访问。

域条件运算符元素

第二个条件元素是应用于被过滤字段的运算符。以下是允许的运算符列表:

图片

这些运算符应用于第一个元素提供的字段,使用第三个元素提供的值。例如,('shipping_address_id', 'child_of', partner_id)检查评估上下文中的partner_id变量并读取其值。数据库在shipping_address_id字段上查询,选择那些地址是partner_id值中标识的地址的子地址的记录。

域条件值元素

第三个元素被评估为 Python 表达式。它可以使用字面值,如数字、布尔值、字符串或列表,并且可以使用评估上下文中可用的字段和标识符。

记录对象不是接受值。相反,应使用相应的 ID 值。例如,不要使用[('user_id', '=', user)] - 而应使用[('user_id', '=', user)]

对于记录规则,评估上下文有以下名称可用:

  • user: 当前用户的记录(相当于self.env.user)。使用user.id获取相应的 ID。

  • company_id: 活动公司的记录 ID(相当于self.env.company.id)。

  • company_ids: 允许的公司 ID 列表(相当于self.env.companies.ids)。

  • time: Python 时间模块,提供日期和时间函数。官方参考可以在docs.python.org/3/library/time.html找到。

    Odoo 13 中的变更

    自 Odoo 13 以来,company_idcompany_ids上下文值可用于记录规则评估,并且不应再使用之前版本的方案,即使用user.company_id.id。例如,之前经常使用的['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])]域现在应写为[('company_id', 'in', company_ids)]

在多对多字段上搜索

当搜索字段是多对多时,运算符应用于每个字段值,如果任何字段值匹配域条件,则评估的记录将包含在结果中。

=in运算符的行为类似于包含操作。它们都检查字段值是否与搜索值列表中的任何值匹配。对称地,!=not in运算符检查字段值是否与搜索值列表中的任何值都不匹配。

使用多个条件组合域表达式

域表达式是一系列项目,可以包含多个条件元组。默认情况下,这些条件将隐式地使用 AND 逻辑运算符组合。这意味着它将只返回满足所有条件的记录。

也可以使用显式逻辑操作符——例如,使用&符号(默认)进行 AND 操作,使用|符号进行 OR 操作。这些将在下一个两个项上操作,以递归方式工作。我们稍后会更详细地讨论这一点。

对于一个稍微正式的定义,域表达式使用前缀表示法,也称为波兰表示法PN),其中操作符位于操作数之前。AND 和 OR 操作符是二元操作符,而 NOT 是一元操作符。

感叹号(!)代表 NOT 操作符,它作用于后续项。因此,它应该放在要取反的项之前。例如,['!', ('is_done','=',True)]表达式将过滤所有未完成的记录。

操作符项,例如(!)或(|),可以嵌套,允许定义AND/OR/NOT复杂条件。让我们用一个例子来说明这一点。

在服务器端记录规则中,我们可以找到类似于这个的域表达式:

['|',
    ('message_follower_ids', 'in', [user.partner_id.id]),
    '|',
        ('user_id', '=', user.id),
        ('user_id', '=', False)
]

此域过滤所有记录,其中:

  • 当前用户是关注者,或

  • 当前用户是记录的责任人(user_id),或

  • 记录没有设置责任用户。

以下图显示了之前域表达式示例的抽象语法树表示:

![图 7.1 – 一个说明组合域表达式的图]

](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/odoo15-dev-ess/img/Figure_7.1_B16119.jpg)

图 7.1 – 一个说明组合域表达式的图

第一个|(或OR)操作符作用于后续条件加上下一个条件的结果。下一个条件又是两个其他条件的并集——记录中用户 ID 设置为当前用户,或者用户 ID 未设置。

特殊域条件

对于需要始终为真或始终为假的表达式的场景,也支持一些特殊的域条件。

(1, "=", 1)条件代表一个始终为真的表达式。它可以用于记录规则,以赋予更高用户组对所有记录的访问权限,这些记录之前由较低用户组限制。例如,它用于User: All Documents组,以覆盖继承的User: Own Documents only组中的记录访问限制。有关此示例,请参阅 Odoo 源代码中的addons/sales_team/security/sales_team_security.xml

(0, "=", 1)条件也得到支持,它代表一个始终为假的表达式。

按字段和聚合数据分组

有时,我们需要根据数据字段对记录进行分组。Odoo 可以使用read_group()方法来完成这项工作。该方法参数如下:

  • domain参数是一个包含域表达式以过滤要检索的记录的列表。

  • fields参数是一个字段名称列表,以及要应用的聚合函数,格式为field:aggr。聚合函数是允许使用sumavgminmaxcountcount_distinct的函数。例如:["subtotal:sum"]

  • groupby参数是一个包含要按其分组的字段名称的列表。

  • limit参数是一个可选的最大组数返回数。

  • offset参数是一个可选的数字,表示要跳过的记录数。

  • orderby参数是一个可选的字符串,包含要应用于结果的排序子句(类似于search()支持的)。

  • 如果将lazy参数设置为True,则仅按第一个字段分组,并将剩余的分组字段添加到__context结果中。此参数默认为True,因此将其设置为False以立即应用所有分组字段

下面是一个按国家分组伙伴记录并计算找到的不同州数量的示例:

>>> self.env["res.partner"].read_group([("display_name", "like", "Azure")], fields=["state_id:count_distinct",], groupby=["country_id"], lazy=False)
[{'__count': 4, 'state_id': 1, 'country_id': (233, <odoo.tools.func.lazy object at 0x7f197b65fc00>), '__domain': ['&', ('country_id', '=', 233), ('display_name', 'like', 'Azure')]}]

这返回了一个包含单个组结果的列表,对应于233国家 ID。运行self.env["res.country"].browse(233).name,我们可以看到国家是United Sates__count键显示在233国家 ID 中有4个合作伙伴,state_id对象显示了count不同的聚合结果:这些合作伙伴使用了1个不同的州。

我们现在知道了如何创建记录集。接下来,我们希望读取它们中的数据。在许多情况下,这是一个简单的操作,但对于某些字段类型,有一些需要注意的细节。下一节将帮助我们了解这些。

访问记录集中的数据

一旦我们有一个记录集,我们希望检查其中包含的数据。因此,在以下章节中,我们将探讨如何访问记录集中的数据。

我们可以获取称为单例的个别记录的字段值。关系字段具有特殊属性,我们可以使用点符号遍历链接的记录。最后,我们将讨论在需要处理日期和时间记录并将它们转换为不同格式时的一些考虑因素。

访问单个记录数据

当记录集中只有一个记录时,它被称为单例。单例仍然是记录集,可以在需要记录集的任何地方使用。

但与多元素记录集不同,单例可以使用点符号访问其字段,如下所示:

>>> print(self.name)
OdooBot

在下一个示例中,我们可以看到相同的self单例记录集也表现得像一个记录集,我们可以迭代它。它只有一个记录,因此只打印出一个名称:

>>> for rec in self: print(rec.name)
...
OdooBot

尝试在包含多个记录的记录集中访问字段值将导致错误,因此这可能在不确定是否正在处理单例记录集的情况下成为一个问题。

小贴士

虽然使用点符号访问字段在多个记录上不会工作,但可以通过将值映射到记录集来批量访问它们。这是通过使用mapped()完成的。例如,rset.mapped("name")返回一个包含name值的列表。

对于仅设计用于与单例一起工作的方法,我们可以在开始时使用self.ensure_one()进行检查。如果self不是一个单例,它将引发错误。

小贴士

ensure_one() 函数如果记录为空也会引发错误。要检查 rset 是否有一个或零个记录,你可以使用 rset or rset.ensure_one()

空记录也是一个单例。这很方便,因为访问字段值将返回一个 None 值而不是引发错误。这也适用于关系字段,使用点符号访问相关记录不会引发错误。

因此,在实际操作中,在访问字段值之前没有必要检查空记录集。例如,而不是 if record: print(record.name),我们可以安全地写出更简单的 print(record.name) 方法。也可以通过使用 or 条件提供一个空值的默认值:print(record.name or '')

访问关系字段

如我们之前所见,模型可以有关系字段——多对一一对多多对多。这些字段类型具有记录集作为值。

在多对一字段的情况下,值可以是单例或空记录集。在这两种情况下,我们可以直接访问它们的字段值。例如,以下指令是正确且安全的:

>>> self.company_id
res.company(1,)
>>> self.company_id.name
'YourCompany'
>>> self.company_id.currency_id
res.currency(1,)
>>> self.company_id.currency_id.name
'EUR'

空记录集方便地也表现得像一个单例,访问其字段不会返回错误,而是只返回 False。正因为如此,我们可以使用点符号来遍历记录,而不用担心空值引起的错误,如下所示:

>>> self.company_id.parent_id
res.company()
>>> self.company_id.parent_id.name
False

访问日期和时间值

在记录集中,datedatetime 值被表示为原生的 Python 对象。例如,当我们查找 admin 用户的最后登录日期时:

>>> self.browse(2).login_date
datetime.datetime(2021, 11, 2, 16, 47, 57, 327756)

由于 datedatetime 值是 Python 对象,它们具有适用于这些对象的所有操作功能。

Odoo 12 的变化

datedatetime 字段值现在表示为 Python 对象,与之前的 Odoo 版本不同,那时 datedatetime 值被表示为文本字符串。这些字段类型值仍然可以使用文本表示来设置,就像之前的 Odoo 版本一样。

日期和时间存储在数据库中为原生的 datetime 值,在记录集中看到的也是 UTC。当网络客户端向用户展示时,datetime 值会通过使用存储在上下文 tz 键中的当前会话时区设置进行转换,例如,{'tz': 'Europe/Brussels'}。这种转换是网络客户端的责任,因为服务器没有执行这种转换。

例如,布鲁塞尔(UTC+1)用户输入的 11:00 AM 日期时间值在数据库中存储为 10:00 AM UTC,并被纽约(UTC-4)用户看到为 06:00 AM。Odoo 服务器日志消息的时间戳使用 UTC 时间而不是本地服务器时间。

相反的转换——从会话时区到 UTC——也需要在将用户的 datetime 输入发送回服务器时由网络客户端完成。

小贴士

记住,数据库中存储并由服务器代码处理的数据日期和时间始终以协调世界时(UTC)表示。即使是服务器日志消息的时间戳也以 UTC 表示。

我们现在已经回顾了如何访问记录数据的细节。然而,我们的应用程序将为业务流程提供一些自动化,因此不可避免地我们还需要写入记录集。让我们在下一节中详细探讨这一点。

写入记录

我们有两种不同的方式来写入记录:使用对象风格的直接赋值或使用write()方法。write()方法是负责执行写入操作的低级方法,当使用外部 API 或加载 XML 记录时,它仍然被直接使用。对象风格的直接赋值后来被添加到 ORM 模型中。它实现了活动记录模式,可以在 Python 代码逻辑中使用。

Odoo 13 的变更

在 Odoo 13 中,ORM 模型引入了一种新的数据库写入方法,称为flush()方法,它将自动调用以一次性执行相应的数据库操作。

接下来,我们将探讨这两种方法及其差异。

使用对象风格的值赋值

记录集实现了活动记录模式。这意味着我们可以向它们分配值,这些更改将持久保存在数据库中。这是一种直观且方便的数据操作方式。

Odoo 13 的变更

从 Odoo 13 开始,支持向包含多个记录的记录集分配值。在 Odoo 12 之前,仅支持写入单个记录的值,并且必须使用write()方法来写入多个记录。

这里有一个例子:

>>> root = self.env["res.users"].browse(1)
>>> print(root.name) 
System
>>> root.name = "Superuser"
>>> print(root.name) 
Superuser

当使用活动记录模式时,可以通过分配记录集来设置关系字段的值。

日期和时间字段可以分配 Python 原生对象或 Odoo 默认格式的字符串表示的值:

>>> from datetime import date
>>> self.date = date(2020, 12, 1)
>>> self.date
datetime.date(2020, 12, 1)
>>> self.date = "2020-12-02"
>>> self.date
datetime.date(2020, 12, 2)

二进制字段应分配base64编码的值。例如,当从文件中读取原始二进制数据时,该值必须在分配给字段之前使用base64.b64encode()进行转换:

>>> import base64
>>> blackdot_binary = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x04\x00\x00\x00\xb5\x1c\x0c\x02\x00\x00\x00\x0bIDATx\xdacd\xf8\x0f\x00\x01\x05\x01\x01'\x18\xe3f\x00\x00\x00\x00IEND\xaeB'\x82"
>>> self.image_1920 = base64.b64encode(blackdot_binary).decode("utf-8")

当在多对一字段上分配值时,分配的值必须是一个单一记录(即,一个单例记录集)。

对于多对多字段,值也可以使用记录集分配,用新的记录集替换(如果有的话)链接的记录列表。在这里,允许任何大小的记录集。

要在关系字段上设置空值,请使用NoneFalse

>>> self.child_ids = None
>>> self.child_ids
res.partner()

要在分配的列表中追加或删除记录,请使用记录操作。

例如,想象一家公司记录还有一个相关的合作伙伴记录,用于存储地址详情。假设我们想将当前用户添加为公司子联系人的联系人。这可以通过以下方式完成:

>>> mycompany_partner = self.company_id.partner_id
>>> myaddress = self.partner_id
>>> mycompany_partner.child_ids = mycompany_partner.child_ids | myaddress

在这里,管道运算符(|)被用来连接记录以获得更大的记录集。

紧凑的追加和赋值运算符(|=)可以用于达到相同的效果:

>>> mycompany_partner.child_ids |= myaddress

本章后面的 组合记录集 部分提供了关于记录操作操作的更多详细信息。

使用 write() 方法

write() 方法也可以用于更新记录中的数据。它接受一个包含要分配的字段名称和值的字典。在某些情况下,例如字典首先准备,然后执行分配,它可能更方便。在 Odoo 的较旧版本(直到 Odoo 12)中,对于无法直接分配的情况,它也很有用。

write() 方法接收一个包含字段和要分配的值的字典,并使用这些值更新记录集:

>>> Partner = self.env['res.partner']
>>> recs = Partner.search( [("name", "ilike", "Azure")])
>>> recs.write({"comment": "Hello!"})
True

日期和时间字段可以使用相应的 Python 对象的值或使用字符串文本表示来分配,就像使用对象样式赋值一样。

自 Odoo 13 以来,write() 可以使用记录集在单对一和多对多关系字段上设置值,就像使用对象样式赋值一样。

Odoo 13 的变化

write() 方法可以使用记录集在关系字段上分配值。在 Odoo 12 之前,多对一字段使用 ID 值设置,多对多字段使用特殊语法设置,例如,(4, <id>, _) 用于添加记录,(6, 0, [<ids>]) 用于设置完整的记录列表。此语法在 第五章 导入、导出和模块数据 中有更详细的讨论。

例如,假设我们有两个合作伙伴记录,address1address2,我们想在 self.child_ids 字段上设置它们。

使用 write() 方法,我们会这样做:

self.write({ 'child_ids': address1 | address2})

另一个选项(适用于 Odoo 13 之前的版本)如下:

self.write({ 'child_ids': [(6, 0, [address1.id, address2.id])]})

write() 方法用于在现有记录上写入日期。但我们也需要创建和删除记录,我们将在下一节中讨论。

创建和删除记录

create()unlink() 模型方法允许我们分别创建和删除现有记录。

create() 方法接受一个包含要创建的记录的字段和值的字典,使用与 write() 相同的语法。默认值会自动应用,正如本例所示:

>>> Partner = self.env['res.partner']
>>> new = Partner.create({'name': 'ACME', 'is_company': True})
>>> print(new)
res.partner(59,)

unlink() 方法删除记录集中的记录,如下例所示:

>>> rec = Partner.search([('name', '=', 'ACME')])
>>> rec.unlink()
2021-11-15 18:40:10,090 3756 INFO library odoo.models.unlink: User #1 deleted mail.message records with IDs: [20]
2021-11-15 18:40:10,146 3756 INFO library odoo.models.unlink: User #1 deleted res.partner records with IDs: [59]
2021-11-15 18:40:10,160 3756 INFO library odoo.models.unlink: User #1 deleted mail.followers records with IDs: [9]
True

unlink() 方法返回一个 True 值。此外,在 delete 操作期间,它会触发日志消息,通知相关记录的级联删除,例如聊天消息和关注者。

创建记录的另一种方法是复制现有的一个。为此,可以使用 copy() 模型方法。它接受一个可选的字典参数,其中包含在创建新记录时覆盖的值。

例如,要从 demo 用户创建一个新用户,我们可以使用以下方法:

>>> demo = self.env.ref("base.user_demo")
>>> new = demo.copy({"name": "John", "login": "john@example.com"})

带有 copy=False 属性的字段不会自动复制。多对多关系字段默认禁用此标志,因此它们不会复制。

在前面的章节中,我们已经学习了如何在记录集中访问数据以及如何创建和写入记录集。然而,有一些字段类型值得更多关注。在下一节中,我们将讨论处理日期和时间字段的具体技术。

与日期和时间字段一起工作

访问记录集中的数据部分,我们看到了如何从记录中读取日期和时间值。通常还需要执行日期计算以及将日期在它们的本地格式和字符串表示之间进行转换。在这里,我们将了解如何执行这些类型的操作。

Odoo 提供了一些有用的函数来创建新的日期和时间对象。

odoo.fields.Date对象提供以下辅助函数:

  • fields.Date.today()函数返回一个字符串,其中包含服务器期望的当前日期格式,使用 UTC 作为参考。这足以计算默认值。它可以直接在日期字段定义中使用default=fields.Date.today

  • fields.Date.context_today(record, timestamp=None)函数返回一个字符串,其中包含会话上下文中的当前日期。时区值取自记录的上下文。可选的timestamp参数是一个datetime对象,如果提供,将使用它而不是当前时间。

odoo.fields.Datetime对象提供以下日期时间创建函数:

  • fields.Datetime.now()函数返回一个字符串,其中包含服务器期望的当前datetime格式,使用 UTC 作为参考。这足以计算默认值。它可以直接在datetime字段定义中使用default=fields.Datetime.now

  • fields.Datetime.context_timestamp(record, timestamp)函数将一个无知的datetime值(没有时区)转换为时区感知的datetime值。时区是从记录的上下文中提取的,因此函数名为context_timestamp

添加和减去时间

日期对象可以进行比较和减法,以找到两个日期之间的时间差。这个时间差是一个timedelta对象。timedelta对象可以添加到或从datedatetime对象中减去,执行日期算术。

这些对象由 Python 标准库datetime模块提供。以下是我们可以用它们进行的必要操作的示例:

>>> from datetime import date
>>> date.today()
datetime.date(2021, 11, 3)
>>> from datetime import timedelta
>>> date(2021, 11, 3) + timedelta(days=7)
datetime.date(2021, 11, 10)

datedatetimetimedelta数据类型的完整参考可以在docs.python.org/3/library/datetime.html找到。

timedelta对象支持周、天、小时、秒等。但它不支持年或月。

要使用月份或年来执行日期算术,我们应该使用relativedelta对象。以下是一个添加一年一个月的示例:

>>> from dateutil.relativedelta import relativedelta
>>> date(2021, 11, 3) + relativedelta(years=1, months=1)
datetime.date(2022, 12, 3)

relativedelta对象支持高级日期算术,包括闰年和复活节计算。有关它的文档可以在dateutil.readthedocs.io找到。

Odoo 还在 odoo.tools.date_utils 模块中提供了一些额外的函数:

  • start_of(value, granularity) 函数返回具有指定粒度的时间段的开始,这是一个字符串值,可以是 yearquartermonthweekdayhour

  • end_of(value, granularity) 函数返回具有指定粒度的时间段的结束。

  • add(value, **kwargs) 函数向给定值添加时间间隔。**kwargs 参数应由 relativedelta 对象使用来定义时间间隔。这些参数可以是 yearsmonthsweeksdayshoursminutes 等。

  • subtract(value, **kwargs) 函数从给定值中减去时间间隔。

这些实用函数也暴露在 odoo.fields.Dateodoo.fields.Datetime 对象中。

这里有一些使用先前函数的示例:

>>> from odoo.tools import date_utils
>>> from datetime import datetime
>>> now = datetime(2020, 11, 3, 0, 0, 0)
>>> date_utils.start_of(now, 'week')
datetime.datetime(2020, 11, 2, 0, 0)
>>> date_utils.end_of(now, 'week')
datetime.datetime(2020, 11, 8, 23, 59, 59, 999999)
>>> today = date(2020, 11, 3)
>>> date_utils.add(today, months=2)
datetime.date(2021, 1, 3)
>>> date_utils.subtract(today, months=2)
datetime.date(2020, 9, 3)

将日期和时间对象转换为文本表示

有时会需要将 Python date 对象转换为文本表示。这可能需要,例如,准备用户消息或格式化数据以发送到另一个系统。

Odoo 字段对象提供辅助函数,用于将原生 Python 对象转换为字符串表示形式:

  • fields.Date.to_string(value) 函数将 date 对象转换为 Odoo 服务器期望的格式字符串。

  • fields.Datetime.to_string(value) 函数将 datetime 对象转换为 Odoo 服务器期望的格式字符串。

这些使用 Odoo 服务器预定义的默认值,这些默认值在以下常量中定义:

  • odoo.tools.DEFAULT_SERVER_DATE_FORMAT

  • odoo.tools.DEFAULT_SERVER_DATETIME_FORMAT

这些分别对应于 %Y-%m-%d%Y-%m-%d %H:%M:%S

date.strftimedatetime.strftime 函数接受一个格式字符串参数,可用于其他转换为文本。

例如,考虑以下内容:

>>> from datetime import date
>>> date(2020, 11, 3).strftime("%d/%m/%Y")
'03/11/2020'

可用格式代码的更多详细信息可以在 docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior 找到。

转换表示为文本的日期和时间

有时日期以文本字符串的格式到达,需要将其转换为 Python datedatetime 对象。在 Odoo 11 之前,这通常是必需的,其中存储的日期被读取为文本表示。提供了一些工具来帮助将文本转换为原生数据类型,然后再将其转换回文本。

为了便于在格式之间进行转换,fields.Datefields.Datetime 对象提供这些函数:

  • fields.Date.to_date 函数将字符串转换为 date 对象。

  • fields.Datetime.to_datetime(value) 函数将字符串转换为 datetime 对象。

to_datetime 函数的一个使用示例如下:

>>> from odoo import fields
>>> fields.Datetime.to_datetime("2020-11-21 23:11:55")
datetime.datetime(2020, 11, 21, 23, 11, 55)

上述示例使用 Odoo 内部日期格式解析提供的字符串,并将其转换为 Python datetime对象。

对于其他日期和时间格式,可以使用datedatetime对象的strptime方法:

>>> from datetime import datetime
>>> datetime.strptime("03/11/2020", "%d/%m/%Y")
datetime.datetime(2020, 11, 3, 0, 0)

在大多数情况下,文本表示的时间不会在 UTC 中,正如 Odoo 服务器所期望的那样。在将其存储在 Odoo 数据库之前,必须将时间转换为 UTC。

例如,如果用户位于欧洲/布鲁塞尔时区(UTC+1 小时),则2020-12-01 00:30:00的用户时间应存储为 UTC 的2020-11-30 23:30:00。以下是实现此功能的代码示例:

>>> from datetime import datetime
>>> import pytz
>>> naive_date = datetime(2020, 12, 1, 0, 30, 0)
>>> client_tz = self.env.context["tz"]
>>> client_date = pytz.timezone(client_tz).localize(naive_date)
>>> utc_date = client_date.astimezone(pytz.utc)
>>> print(utc_date)
2020-11-30 23:30:00+00:00

此代码从上下文中获取用户时区名称,然后使用它将原始日期转换为时区感知的日期。最后一步是通过使用astimezone(pytz.utc)将客户端时区日期转换为 UTC 日期。

我们现在已经学习了在 Odoo 中处理日期和时间的具体技术。还有专门的技术用于处理记录集和存储在关系字段中的值,我们将在下一节中讨论这些技术。

与记录集一起工作

记录集是一组记录,Python 业务逻辑经常需要使用它们。可以在记录集上执行几种操作,例如映射和过滤。我们还可以通过添加或删除记录来组合新的记录集。其他常见操作包括检查记录集的内容,以检查特定记录是否存在,例如。

Odoo 10 的变化

自 Odoo 10 以来,记录集操作保留了记录顺序。这与之前的 Odoo 版本不同,在之前的版本中,记录集操作不保证保留记录顺序,尽管添加和切片保持了记录顺序。

记录集操作

记录集有一些可用的函数,可以执行对它们有用的操作,例如排序过滤记录

这些是支持的函数和属性:

  • recordset.ids 属性返回一个包含记录集元素 ID 的列表。

  • recordset.ensure_one() 函数检查它是否是单个记录(即单例);如果不是,将引发ValueError异常。

  • recordset.filtered(<函数或字符串>) 函数返回一个过滤后的记录集,这是一个测试函数,用于过滤记录。参数也可以是一个包含点分隔的字段序列的字符串。评估为真值(truthy value)的记录将被选中。

  • recordset.mapped(<函数或字符串>) 函数返回一个值列表,并为每条记录返回一个值。参数也可以是一个包含点分隔的字段序列的字符串,用于评估以到达要返回的字段。在字段序列中,多对一关系是安全使用的。

  • recordset.sorted(<function ot str>) 函数返回具有特定元素顺序的记录集。该函数为每个记录返回一个值,这些值用于对记录集进行排序。参数可以是一个字符串,包含要排序的字段名称。请注意,不允许使用字段名称的点表示法序列。还有一个可选的 reverse=True 参数。

这里是这些函数的一些使用示例:

>>> rs0 = self.env["res.partner"].search([("display_name", "like", "Azure")])
>>> len(rs0)  # how many records?
4
>>> rs0.filtered(lambda r: r.name.startswith("Nicole"))
res.partner(27,)
>>> rs0.filtered("is_company")
res.partner(14,)
>>> rs0.mapped("name")
['Azure Interior', 'Brandon Freeman', 'Colleen Diaz', 'Nicole Ford']
>>> rs0.sorted("name", reverse=True).mapped("name")
['Nicole Ford', 'Colleen Diaz', 'Brandon Freeman', 'Azure Interior']
>>> rs0.mapped(lambda r: (r.id, r.name))
[(14, 'Azure Interior'), (26, 'Brandon Freeman'), (33, 'Colleen Diaz'), (27, 'Nicole Ford')]

记录集的组合

记录集是不可变的,这意味着它们的值不能直接修改。相反,我们可以根据现有的记录集组合一个新的记录集。切片表示法,通常与 Python 列表一起使用,可以用于记录集以提取记录的子集。以下是一些示例:

  • rs[0]rs[-1] 分别检索第一个元素和最后一个元素。

  • rs[1:] 结果是一个不包含第一个元素的记录集副本。

  • rs[:1] 返回记录集的第一个元素。

    小贴士

    为了安全地检索记录集的第一个元素,请使用 rs[:1] 而不是 rs[0]。后者如果 rs 为空,将导致错误,而前者在这种情况下将只返回一个空记录集。另一种选择是使用 odoo.fields 模块中的 first() 函数:fields.first(rs)

记录集还支持以下集合操作:

  • rs1 | rs2 操作是一个并集集操作,结果是一个包含两个记录集中所有元素的记录集。这是一个类似集合的操作,不会导致重复元素。

  • 例如,self.env.user | self.env.user 返回一个记录,例如 res.users(1,)

  • rs1 & rs2 操作是一个交集集操作,结果是一个只包含两个记录集中都存在的元素的记录集。

  • rs1 - rs2 操作是一个差集操作,结果是一个不包含在 rs2 中的 rs1 元素的记录集。

    小贴士

    记录集也支持加法操作(+),但是应该避免使用。它与并集操作(|)的行为不同,并允许记录集中有重复的元素。然而,这很少是我们想要的。例如,self.env.user + self.env.user 返回两个记录,例如 res.users(1, 1)

我们可以直接使用值赋值来使用这些操作,以获得更短的表示法:

  • self.author_ids |= author1 操作如果 author1 记录不在记录集中,则添加该记录。

  • self.author_ids &= author1 操作仅保留也存在于 author1 记录集中的记录。

  • self.author_ids -= author1 操作如果 author1 记录存在于记录集中,则删除该特定 author1 记录。

记录集累积

在某些情况下,我们希望遍历一些逻辑并累积循环每次迭代的记录。使用 ORM 累积记录集的方法是从一个空记录集开始,然后向其中添加记录。要获取一个空记录集,创建对模型的引用。例如,考虑以下内容:

Partner = self.env["res.partner"]
recs = self.env["res.partner"]
for i in range(3):
    rec = Partner.create({"name": "Partner %s" % i})
    recs |= rec

之前的代码循环三次,在每次循环中,在将记录累积到 recs 记录集之前,都会创建一个新的合作伙伴记录。由于它是一个记录集,recs 变量可以在需要记录集的情况下使用,例如,将值分配给多对一字段。

然而,累积记录集不是时间效率高的操作,应该避免在循环中使用。原因在于 Odoo 记录集是不可变对象,对记录集的任何操作都意味着复制它以获取修改后的版本。当向记录集中追加记录时,原始记录集不会被修改。相反,会创建一个带有追加记录的它的副本。这种复制操作消耗时间,记录集越大,所需时间越长。

因此,应该考虑其他替代方案。对于前面的例子,我们可以在 Python 列表中累积所有记录的数据字典,然后通过单个 create() 调用创建所有记录。这是可能的,因为 create() 方法可以接受字典列表。

因此,循环可能看起来像这样:

values = []
for i in range(3):
    value = {"name": "Partner %s" % i}
    values.append(value)
recs = self.env["res.partner"].create(values)

然而,这个解决方案并不适用于所有情况。另一种选择是使用 Python 列表来累积记录。Python 列表是可变对象,对于它们来说,追加元素是一个高效的操作。由于 Python 列表实际上不是记录集,因此这个选项不能用于需要记录集的地方,例如,对一个多对一字段进行赋值。

以下是将记录累积到 Python 列表的示例:

Partner = self.env["res.partner"]
recs = []
for i in range(3):
    rec = Partner.create({"name": "Partner %s" % i})
    recs.append(new_rec)

之前的例子说明了在循环中可以使用的几种技术,用于从单个元素构建记录集。然而,有许多情况下循环不是严格必要的,mapped()filtered() 等操作可以提供更有效的方法来实现目标。

记录集比较

有时候,我们需要比较记录集的内容以决定需要采取的进一步行动。记录集支持预期的比较操作。

要检查 <rec> 记录是否是 <my_recordset> 记录集中的元素,可以使用以下代码:

  • <rec> in <my_recordset>

  • <rec> not in <my_recordset>

记录集也可以进行比较,以检查一个是否包含在另一个中。要比较两个记录集,使用 set1set2

  • 使用 set1 <= set2set1 < set2 返回 True,如果 set1 中的所有元素也都在 set2 中。如果两个记录集具有相同的元素,< 操作符返回 False

  • 使用 set1 >= set2set1 > set2 返回 True,如果 set2 中的所有元素也都在 set1 中。如果两个记录集具有相同的元素,> 操作符返回 False

事务和低级 SQL

从客户端调用的 ORM 方法在事务中运行。事务确保在并发写入或失败的情况下正确性。在事务期间,使用的数据记录被锁定,保护它们免受其他并发事务的影响,并确保它们不会被意外更改。在发生故障的情况下,所有事务更改都会回滚,返回到初始状态。

事务支持由 PostgreSQL 数据库提供。当从客户端调用 ORM 方法时,会启动一个新的事务。如果在方法执行期间发生错误,所做的任何更改都会被撤销。如果方法执行完成后没有错误,则所做的更改会被提交,使其生效并对所有其他事务可见。

这会自动为我们处理,我们通常不需要担心。然而,在某些高级用例中,可能需要控制当前事务。

Odoo 13 的变化

自 Odoo 13 以来,数据库写入操作不是在方法运行时执行的。相反,它们累积在内存缓存中,实际的数据库写入被延迟到方法执行的末尾,这是通过在这一点上自动调用的flush()调用来完成的。

控制数据库事务

有一些情况下,控制事务可能是有用的,可以使用self.env.cr数据库游标来实现这一点。一个例子是遍历记录并对每个记录执行操作,我们希望跳过有操作错误的那些,而不影响其他记录。

为了这个,对象提供了以下:

  • self.env.cr.commit()提交事务的缓冲写入操作,使它们在数据库中生效。

  • self.env.cr.rollback()取消自上次提交以来的事务write操作,如果没有提交,则取消所有操作。

    小贴士

    Odoo shell会话模拟方法执行上下文。这意味着数据库写入不会在self.env.cr.commit()被调用之前执行。

执行原始 SQL

可以通过使用游标execute()方法直接在数据库中运行 SQL。这需要一个要运行的 SQL 语句的字符串和一个作为 SQL 参数值的第二个可选参数。

值参数可以是一个元组或一个字典。当使用元组时,参数被替换为%s,而当使用字典时,它们被替换为%(<name>)s。以下是两种方法的示例:

>>> self.env.cr.execute("SELECT id, login FROM res_users WHERE login=%s OR id=%s", ("demo", 1))
>>> self.env.cr.execute("SELECT id, login FROM res_users WHERE login=%(login)s OR id=%(id)s", {"login": "demo", "id": 1})

之前的任何指令都会运行 SQL,替换参数并准备一个需要检索结果的游标。更多详情可以在psycopg2文档的www.psycopg.org/docs/usage.html#query-parameters中找到。

注意!

使用 cr.execute() 时,我们不应直接通过连接参数来组合 SQL 查询。这样做是已知的,存在安全风险,可能会被 SQL 注入攻击利用。始终使用 %s 占位符与第二个参数一起传递值。

要获取结果,可以使用 fetchall() 函数,返回行的 元组

>>> self.env.cr.fetchall()
[(6, 'demo'), (1, '__system__')]

dictfetchall() 函数也可以用来检索记录作为字典:

>>> self.env.cr.dictfetchall()
[{'id': 6, 'login': 'demo'}, {'id': 1, 'login': '__system__'}]

提示

self.env.cr 数据库游标对象是围绕 PostgreSQL 库 psycopg2 的 Odoo 特定包装器。这意味着 psycopg2 文档对于理解如何完全使用该对象是有帮助的:

www.psycopg.org/docs/cursor.html

还可以运行 UPDATEINSERT。Odoo 环境依赖于数据缓存,当执行这些 DML 指令时,可能与数据库不一致。因此,在运行原始 DML 后,应使用 self.env.cache.invalidate(fnames=None, ids=None) 来使环境缓存失效。

fnames 是一个包含要使无效和刷新的字段名称的列表。如果没有提供,则所有字段都将使无效。

ids 是一个包含要使无效和刷新的记录 ID 的列表。如果没有提供,则所有记录都将使无效。

注意!

直接在数据库中执行 SQL 语句绕过了 ORM 验证和依赖,可能导致数据不一致。只有在你确定自己在做什么的情况下才应使用它。

摘要

在本章中,我们学习了如何使用模型数据来执行 CRUD 操作——即 创建读取更新删除 数据——以及使用和操作 记录集 所需的所有技术。这为我们实现业务逻辑和自动化代码提供了所需的基础。

要实验 ORM API,我们使用了 Odoo 交互式外壳。我们在通过 self.env 访问的环境中运行我们的命令。该环境类似于模型方法中提供的环境,因此它是一个探索 Odoo API 的有用游乐场。

环境允许我们从任何作为记录集提供的 Odoo 模型中查询数据。我们学习了创建记录集的不同方法,然后是如何读取提供的数据,包括日期、二进制值和关系字段等特殊数据类型。

Odoo 的另一个基本功能是写回数据。在本章中,我们还学习了如何创建新记录、写入现有记录以及删除记录。

我们还通过使用 Python 内置工具和 Odoo 框架中包含的一些辅助函数,研究了如何处理日期和时间值。

记录集可以被操作以添加元素、过滤记录、重新排序或累积值,以及比较它们或检查特定记录的包含情况。在实现业务逻辑时,可能需要这些操作中的任何一个,本章介绍了实现所有这些操作的基本技术。

最后,在某些情况下,我们可能需要跳过使用 ORM 模型,并使用低级 SQL 操作直接访问数据库或对事务有更精细的控制。这些操作允许我们解决偶尔出现的 ORM 模型不是最佳工具的情况。

在掌握所有这些工具后,我们为下一章做好了准备,下一章我们将为我们的模型添加业务逻辑层,并实现使用 ORM API 来自动化操作的模式方法。

进一步阅读

官方 Odoo 文档中的记录集信息可以在www.odoo.com/documentation/15.0/developer/reference/backend/orm.html找到。

第六章:第八章:业务逻辑 - 支持业务流程

在前面的章节中,我们学习了如何使用模型来构建应用程序数据结构,然后如何使用 ORM API 和记录集探索和交互数据。

在本章中,我们将把这些内容结合起来实现应用程序中常见的业务逻辑模式。我们将了解业务逻辑可以触发的几种方式,以及一些常用的支持模式。我们还将了解重要的开发技术,如日志记录、调试和测试。

本章我们将讨论以下主题:

  • 学习项目 - 书籍借阅模块

  • 触发业务逻辑的方法

  • 理解 ORM 方法装饰器用于记录集

  • 探索有用的数据模型模式

  • 使用 ORM 内置方法

  • 添加 onchange 用户界面逻辑

  • 消息和活动功能

  • 创建向导

  • 抛出异常

  • 编写单元测试

  • 使用日志消息

  • 了解可用的开发者工具

到本章结束时,你应该对设计和实现业务逻辑自动化充满信心,并知道如何测试和调试你的代码。

技术要求

在本章中,我们将创建一个新的library_checkout附加模块。它依赖于我们在前几章中创建的library_applibrary_member附加模块。

这些附加模块的代码可以在本书的 GitHub 仓库中找到,在github.com/PacktPublishing/Odoo-15-Development-Essentials-Fifth-Editionch08目录下。

这两个附加模块需要在 Odoo 附加模块路径中可用,以便它们可以被安装和使用。

学习项目 - 书籍借阅模块

图书馆应用程序的主数据结构已经就绪。现在,我们希望向我们的系统中添加交易。我们希望图书馆会员能够借阅书籍。这意味着我们应该跟踪书籍的可用性和归还情况。

每本书的借阅都有一个生命周期,从它们被创建的那一刻到书籍归还的那一刻。这是一个简单的流程,可以用看板(Kanban)板表示,其中几个阶段作为列呈现,来自左侧列的工作项被发送到右侧直到完成。

本章重点介绍支持此功能所需的数据模型和业务逻辑。

基本用户界面将在第十章,“后端视图 - 设计用户界面”中进行讨论,而看板视图将在第十一章,“看板视图和客户端 QWeb”中进行讨论。让我们快速了解一下数据模型。

准备数据模型

我们必须做的第一件事是为书籍借阅功能规划所需的数据模型。

图书借阅模型应具有以下字段:

  • 图书馆会员借阅图书(必需)

  • 借阅日期(默认为今天)

  • 负责人(默认为当前用户)负责结账

  • 借阅条目,请求的图书(一个或多个)

为了支持图书借阅生命周期,我们还将有以下内容:

  • 请求阶段—草稿、开放、借出、归还或取消

  • 到期日期,图书应归还的日期

  • 归还日期,图书归还的日期

我们将首先创建新的library_checkout模块并实现图书馆借阅模型的初始版本。与前面的章节相比,这不会引入任何新内容,但将为构建本章相关功能提供基础。

创建模块

library_checkout模块需要创建,类似于我们在前面的章节中所做的那样。按照以下步骤进行操作:

  1. 在与图书馆项目其他附加模块相同的目录中创建一个新的library_checkout目录。以下文件应添加到此目录。

  2. 添加__manifest__.py文件并确保其包含以下内容:

    { "name": "Library Book Checkout",
      "description": "Members can borrow books from the 
        library.",
      "author": "Daniel Reis",
      "depends": ["library_member"],
      "data": [
        "security/ir.model.access.csv",
        "views/library_menu.xml",
        "views/checkout_view.xml",
      ],
    }
    
  3. 添加主__init__.py文件,包含以下代码行:

    from . import models
    
  4. 添加models/__init__.py文件,包含以下代码行:

    from . import library_checkout
    
  5. 添加模型定义文件,models/library_checkout.py,如下所示:

    from odoo import fields, models
    class Checkout(models.Model):
        _name = "library.checkout"
        _description = "Checkout Request"
        member_id = fields.Many2one(
            "library.member",
            required=True,
        )
        user_id = fields.Many2one(
            "res.users",
            "Librarian",
            default=lambda s: s.env.user,
        )
        request_date = fields.Date(
            default=lambda s: fields.Date.today(),
        )
    

接下来,我们应该添加数据文件,包括访问规则、菜单项和一些基本视图,以便模块可以使用。

  1. 将访问安全配置添加到security/ir.model.access.csv文件中:

    id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
    checkout_user,Checkout User,model_library_checkout,library_app.library_group_user,1,1,1,1
    
  2. 接下来,需要添加views/library_menu.xml文件以实现菜单项:

    <odoo>
      <record id="action_library_checkout" 
              model="ir.actions.act_window">
        <field name="name">Checkouts</field>
        <field name="res_model">library.checkout</field>
        <field name="view_mode">tree,form</field>
      </record>
      <menuitem id="menu_library_checkout"
                name="Checkout"
                action="action_library_checkout"
                parent="library_app.menu_library"
      />
    </odoo>
    
  3. 视图在views/checkout_view.xml文件中实现:

    <odoo>
      <record id="view_tree_checkout" model="ir.ui.view">
        <field name="name">Checkout Tree</field>
        <field name="model">library.checkout</field>
        <field name="arch" type="xml">
            <tree>
                <field name="request_date" />
                <field name="member_id" />
            </tree>
        </field>
      </record>
      <record id="view_form_checkout" model="ir.ui.view">
        <field name="name">Checkout Form</field>
        <field name="model">library.checkout</field>
        <field name="arch" type="xml">
          <form>
            <sheet>
              <group>
                <field name="member_id" />
                <field name="request_date" />
                <field name="user_id" />
              </group>
            </sheet>
          </form>
        </field>
      </record>
    </odoo>
    

现在模块包含上述文件后,可以在我们的开发数据库中安装:

图 8.1 – 初始图书馆借阅功能

图 8.1 – 初始图书馆借阅功能

现在,我们可以开始添加更多有趣的功能。

在整个项目中,我们将向不同位置添加业务逻辑片段,以展示 Odoo 提供的多种可能性。下一节将讨论这些选项。

探索触发业务逻辑的方法

一旦数据模型就绪,就需要业务逻辑来执行一些自动操作。业务逻辑可以由用户直接启动,例如通过按钮点击,或者当发生事件时自动触发,例如在记录上写入。

这部分业务逻辑将涉及对记录集的读取和写入。这些细节和技术在第七章记录集 – 与模型数据交互中进行了讨论,我们提供了实际业务逻辑实现所需的工具。

下一个问题是如何触发业务逻辑。这取决于何时以及为什么应该触发业务逻辑。以下是几个选项的总结。

一些业务逻辑与模型字段定义紧密相关。以下是一些与模型定义相关的业务逻辑实例:

  • @api.constrains

  • @api.depends 和分配给 compute 字段属性。

  • @api.model 和分配给 default 字段属性。

这种模型定义逻辑在第六章中进行了详细讨论,模型 – 结构化应用程序数据。一些示例可以在数据模型模式部分找到。ORM 方法装饰器对记录集部分提供了此处提到的几个 ORM 装饰器的回顾。

我们还有与业务工作流相关的 model event-related business logic,它可以附加到以下记录相关事件:

  • 创建、写入和解除链接业务逻辑可以添加到这些事件,在无法使用其他更优雅的方法的情况下。

  • Onchange 逻辑可以应用于用户界面视图,以便我们有一些字段值会因其他字段的变化而改变。

对于直接由用户发起的操作,以下选项可用:

  • 一个用于调用对象方法的 button 视图元素。按钮可以位于看板视图的表单或树中。

  • 一个 server 动作,可以从菜单项或 Action 上下文菜单中访问。

  • 一个用于打开向导表单的 window 动作,用户可以从其中收集输入,按钮将调用业务逻辑。这允许更丰富的用户交互。

这些技术将在本章中介绍。辅助方法通常会使用 API 装饰器,因此理解不同的可用装饰器很重要。为了清晰起见,下一节提供了它们的概述。

理解 ORM 方法装饰器对记录集的影响

方法定义可以由一个 @ 开头,这将对它应用一个装饰器。这些装饰器为这些方法添加特定的行为,并且根据方法的目的,可以使用不同的装饰器。

计算字段和验证方法的装饰器

一些装饰器对验证逻辑和计算字段很有用。它们在此列出:

  • @api.depends(fld1,...) 用于计算字段函数,以确定应触发(重新)计算的更改。它必须在计算字段上设置值;否则,将显示错误。

  • @api.constrains(fld1,...) 用于模型验证函数,并在提到的任何字段发生变化时进行检查。它不应在数据中写入更改。如果检查失败,应引发异常。

这些内容在第六章中进行了详细讨论,模型 – 结构化应用程序数据

另一组装饰器会影响 self 记录集的行为,并且当你实现其他类型的业务逻辑时是相关的。

影响 self 记录集的装饰器

默认情况下,方法预期作用于由 self 的第一个参数提供的记录集。方法代码通常会包含一个 for 语句,该语句遍历 self 记录集中的每个记录。

Odoo 14 的变化

@api.multi 装饰器在 Odoo 14 中已被移除。在之前的 Odoo 版本中,它被用来明确表示被装饰的方法期望在 self 参数中有一个记录集。这对于方法来说已经是默认行为,因此它的使用只是为了清晰。@api.one 装饰器自 Odoo 9 起已被弃用,并在 Odoo 14 中被移除。它为你处理记录循环,使得方法代码对每个记录只调用一次,并且 self 参数始终是一个单例。从 Odoo 14 开始,这两个装饰器都必须从代码中移除,因为它们不再被支持。

在某些情况下,方法预期在类级别上工作,而不是在特定记录上,表现得像 @api.model,在这种情况下,self 方法参数应该用作模型的引用;它不期望包含记录。

例如,create() 方法使用 @api.model – 它不期望输入记录,只期望一个值字典,该字典将被用来创建并返回一个记录。用于计算默认值的方法也应该使用 @api.model 装饰器。

在我们可以深入到业务逻辑实现之前,我们必须使数据模型更加深入,在这个过程中,提供一些常见数据模型模式的示例。

探索有用的数据模型模式

对于表示业务文档的模型,通常需要一些数据结构。这些可以在几个 Odoo 应用程序中看到,例如 销售订单发票

一个常见的模式是表头/行数据结构。它将用于结账请求,以便你可以有多个书籍。另一个模式是使用状态或阶段。这两个有区别,我们将在稍后讨论它们并提供一个参考实现。

最后,ORM API 提供了一些与用户界面相关的几个方法。这些方法也将在本节中讨论。

使用表头和行模型

表单视图的一个常见需求是拥有表头行数据结构。例如,销售订单包括几个订单项的行。在结账功能的案例中,一个结账请求可以有多个请求行,每个请求行对应一个借出的物品。

使用 Odoo,实现这一点很简单。需要一个表头行表视图的两种模型 – 一个用于文档表头,另一个用于文档行。行模型有一个多对一字段来标识它所属的表头,而表头模型有一个一对多字段列出该文档中的行。

library_checkout模块已经添加到结账模型中,因此现在我们想要添加行。按照以下步骤进行操作:

  1. 编辑models/library_checkout.py文件,为结账行添加多对一字段:

        line_ids = fields.One2many(
            "library.checkout.line",
            "checkout_id",
            string="Borrowed Books",
        )
    
  2. 将新模型的文件添加到models/__init__.py中,如下所示:

    from . import library_checkout
    from . import library_checkout_line
    
  3. 接下来,添加声明结账行模型的 Python 文件,models/library_checkout_line.py,内容如下:

    from odoo import api, exceptions, fields, models
    class CheckoutLine(models.Model):
        _name = "library.checkout.line"
        _description = "Checkout Request Line"
        checkout_id = fields.Many2one(
            "library.checkout",
            required=True,
        )
        book_id = fields.Many2one("library.book", 
          required=True)
        note = fields.Char("Notes")
    
  4. 我们还必须添加访问安全配置。编辑security/ir.model.access.csv文件,并添加以下突出显示的行:

    id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
    checkout_user,Checkout User,model_library_checkout,library_app.library_group_user,1,1,1,1
    checkout_line_user,Checkout Line User,model_library_checkout,library_app.library_group_user,1,1,1,1
    
  5. 接下来,我们想要将结账行添加到表单中。我们将将其添加为笔记本小部件的第一页。编辑views/checkout_view.xml文件,并在</sheet>元素之前添加以下代码:

              <notebook>
                <page name="lines">
                  <field name="line_ids">
                    <tree editable="bottom">
                      <field name="book_id" />
                      <field name="note" />
                    </tree>
                  </field>
                </page>
              </notebook>
    

结账表单将如下所示:

图 8.2 – 带有笔记本小部件的结账表单

图 8.2 – 带有笔记本小部件的结账表单

该行的多对一字段显示一个嵌套在父表单视图中的列表视图。默认情况下,Odoo 将查找用于渲染的列表视图定义,这对任何列表视图都是典型的。如果没有找到,将自动生成一个默认视图。

也可以在<field>内部声明特定的视图。我们在前面的代码中就是这样做的。在line_ids字段元素内部,有一个嵌套的<tree>视图定义,将用于此表单。

使用阶段和状态进行以文档为中心的工作流程

在 Odoo 中,我们可以实现以文档为中心的工作流程。我们所说的文档可以是销售订单、项目任务或人力资源申请人等。所有这些在创建到完成的过程中都应遵循一定的生命周期。每个工作项都记录在将经过一系列可能阶段的文档中,直到完成。

如果我们将这些阶段作为看板中的列,并将文档作为这些列中的项目,我们就会得到一个看板,提供所有进行中工作的快速视图。

实现这些进度步骤有两种方法 – 状态阶段

  • state特殊字段名称,使其使用方便。关闭状态列表的缺点在于它不能轻易地容纳自定义流程步骤。

  • stage_id字段名称。可用的阶段列表容易修改,因为您可以删除、添加或重新排序它们。它的缺点是对于流程自动化来说不可靠。由于阶段列表可以更改,自动化规则不能依赖于特定的阶段 ID 或描述。

当我们设计数据模型时,我们需要决定是否应该使用阶段或状态。如果触发业务逻辑比配置流程步骤的能力更重要,则应优先选择状态;否则,阶段应该是首选选择。

如果您无法决定,有一种方法可以提供两种世界的最佳结合:我们可以使用阶段并将每个阶段映射到相应的状态。流程步骤的列表可以很容易地由用户进行配置,并且由于每个阶段都将与某些可靠的状态代码相关联,因此也可以自信地用于自动化业务逻辑。

这种结合的方法将被用于图书馆借阅功能。为了实现借阅阶段,我们将添加library.checkout.stage模型。描述阶段所需的字段如下:

  • 名称,或标题。

  • 顺序,用于对阶段列进行排序。

  • 折叠,用于 Kanban 视图决定默认应该折叠哪些列。我们通常希望将此设置在非活动项列上,例如完成取消

  • 激活,允许存档或不再使用的阶段,以防流程发生变化。

  • 状态,一个封闭的选择列表,用于将每个阶段映射到固定状态。

为了实现前面的字段,我们应该开始添加阶段模型,包括模型定义、视图、菜单和访问安全:

  1. 添加models/library_checkout_stage.py文件并确保它包含以下模型定义代码:

    from odoo import fields, models
    class CheckoutStage(models.Model):
        _name = "library.checkout.stage"
        _description = "Checkout Stage"
        _order = "sequence"
        name = fields.Char()
        sequence = fields.Integer(default=10)
        fold = fields.Boolean()
        active = fields.Boolean(default=True)
        state = fields.Selection(
            [("new","Requested"),
             ("open","Borrowed"),
             ("done","Returned"),
             ("cancel", "Canceled")],
            default="new",
        )
    

    上述代码不应让您感到惊讶。阶段有一个逻辑顺序,因此它们呈现的顺序很重要。这通过_order="sequence"得到保证。我们还可以看到state字段将每个阶段映射到基本状态,这可以安全地用于业务逻辑。

  2. 如同往常,新的代码文件必须添加到models/__init__.py文件中,该文件应如下所示:

    from . import library_checkout_stage
    from . import library_checkout
    from . import library_checkout_line
    
  3. 需要访问安全规则。阶段包含设置数据,并且只能通过security/ir.model.access.csv文件进行编辑:

    id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
    checkout_user,Checkout User,model_library_checkout,library_app.library_group_user,1,1,1,1
    checkout_line_user,Checkout Line
    User,model_library_checkout,library_app.library_group_user,1,1,1,1
    checkout_stage_user,Checkout Stage User,model_library_checkout_stage,library_app.library_group_user,1,0,0,0
    checkout_stage_manager,Checkout Stage Manager,model_library_checkout_stage,library_app.library_group_manager,1,1,1,1
    
  4. 接下来,需要一个菜单项,用于导航到阶段的设置。由于library_app模块尚未提供,让我们编辑它以添加此功能。编辑library_app/views/library_menu.xml文件并添加以下 XML:

      <menuitem id="menu_library_configuration"
                name="Configuration"
                parent="menu_library"
      />
    
  5. 现在,在library_checkout/views/library_menu.xml文件中添加以下 XML:

      <record id="action_library_stage" 
              model="ir.actions.act_window">
        <field name="name">Stages</field>
        <field name="res_model">
          library.checkout.stage</field>
        <field name="view_mode">tree,form</field>
      </record>
      <menuitem id="menu_library_stage"
                name="Stages"
                action="action_library_stage"
                parent=
                 "library_app.menu_library_configuration" 
      />
    
  6. 我们需要一些阶段来工作,因此让我们向模块添加一些默认数据。创建data/library_checkout_stage.xml文件,并包含以下代码:

    <odoo noupdate="1">
      <record id="stage_new" model=
        "library.checkout.stage">
          <field name="name">Draft</field>
          <field name="sequence">10</field>
          <field name="state">new</field>
      </record>
      <record id="stage_open" model=
        "library.checkout.stage">
          <field name="name">Borrowed</field>
          <field name="sequence">20</field>
          <field name="state">open</field>
      </record>
      <record id="stage_done" model=
        "library.checkout.stage">
          <field name="name">Completed</field>
          <field name="sequence">90</field>
          <field name="state">done</field>
      </record>
      <record id="stage_cancel" model=
        "library.checkout.stage">
          <field name="name">Canceled</field>
          <field name="sequence">95</field>
          <field name="state">cancel</field>
      </record>
    </odoo>
    
  7. 在此生效之前,需要将其添加到library_checkout/__manifest__.py文件中,如下所示:

      "data": [
        "security/ir.model.access.csv",
        "views/library_menu.xml",
        "views/checkout_view.xml",
        "data/library_checkout_stage.xml",
      ],
    

以下截图显示了预期的阶段列表视图:

图 8.3 – 阶段列表视图

图 8.3 – 阶段列表视图

这处理了添加阶段模型到library_checkout并允许用户配置它所需的所有组件。

向模型添加阶段工作流支持

接下来,应将阶段字段添加到图书馆借阅模型中。为了提供良好的用户体验,还应注意以下两点:

  • 应分配的默认阶段应该是具有new状态的第一个。

  • 当按阶段分组时,所有可用的阶段都应该存在,即使每个阶段都没有结账。

这些应该添加到library_checkout/models/library_checkout.py文件中的Checkout类。

查找默认阶段的函数应返回将用作默认值的记录:

    @api.model
    def _default_stage_id(self):
        Stage = self.env["library.checkout.stage"]
        return Stage.search([("state", "=", "new")], 
          limit=1)

这返回阶段模型中的第一条记录。由于阶段模型按顺序排序,它将返回具有最低序列号的记录。

当我们按阶段分组时,我们希望看到所有可能的阶段,而不仅仅是具有结账记录的阶段。用于此的方法应返回用于分组的记录集。在这种情况下,返回所有活动阶段是合适的:

    @api.model
    def _group_expand_stage_id(self, stages, domain, 
      order):
        return stages.search([], order=order)

最后,我们希望添加到结账模型的stage_id字段可以使用前面提到的defaultgroup_expand属性的方法:

    stage_id = fields.Many2one(
        "library.checkout.stage",
        default=_default_stage_id,
        group_expand="_group_expand_stage_id")
    state = fields.Selection(related="stage_id.state")

stage_id与阶段模型具有多对一的关系。默认值由_default_stage_id方法函数计算,stage_id上的 groupby 将使用_group_expand_stage_id方法函数的结果。

Odoo 10 的变化

group_expand字段属性是在 Odoo 10 中引入的,在之前的版本中不可用。

group_expand参数覆盖了字段上分组的方式。分组操作的默认行为是只看到正在使用的阶段;没有结账文档的阶段不会显示。但在stage_id字段的情况下,我们希望看到所有可用的阶段,即使其中一些没有任何项目。

_group_expand_stage_id()辅助函数返回分组操作应使用的分组记录列表。在这种情况下,它返回所有现有阶段,无论该阶段是否有图书馆结账。

注意

group_expand属性必须是一个包含方法名称的字符串。这与其他属性不同,例如default,它可以是要字符串或直接引用方法名称。

也添加了state字段。它只是使此模型中与阶段相关的state字段可用,以便可以在视图中使用。这将使用视图可用的特殊state支持。

支持用户界面的方法

以下方法主要用于网络客户端以渲染用户界面和执行基本交互:

  • name_get()计算(ID, name)元组,以及 ID。它是display_name值的默认计算,可以扩展以实现自定义显示表示,例如在记录名称旁边显示标识代码。

  • name_search(name="", args=None, operator="ilike", limit=100)在显示名称上执行搜索。当用户在关系字段中键入时用于视图,以生成包含与键入文本匹配的建议记录的列表。它返回(ID, name)元组的列表。

  • name_create(name)创建一个只接受名称作为输入的新记录。它在on_create="quick_create"的看板视图中使用,您只需提供其名称即可快速创建相关记录。它可以扩展以提供通过此功能创建的新记录的特定默认值。

  • default_get([fields])返回要创建的新记录的默认值,作为一个字典。默认值可能取决于变量,例如当前用户或会话上下文。这可以扩展以添加额外的默认值。

  • fields_get()用于描述模型的字段定义。

  • fields_view_get()由 Web 客户端用于检索 UI 视图的结构以进行渲染。它可以提供一个视图的 ID 作为参数,或者使用view_type="form"来指定我们想要的视图类型。例如,self.fields_view_get(view_type="tree")将返回用于渲染self模型的树视图 XML 架构。

这些内置 ORM 模型可以作为实现特定模型业务逻辑的扩展点。

下一个部分将讨论如何通过记录操作(如创建或写入记录)触发业务逻辑。

使用 ORM 内置方法

模型定义相关的方法可以执行许多操作,但某些业务逻辑无法通过它们实现,因此需要附加到 ORM 记录写操作上。

ORM 提供了执行创建读取更新删除CRUD)操作的方法,这些操作针对我们的模型数据。让我们探讨这些写操作以及如何扩展以支持自定义逻辑。

要读取数据,提供的主要方法是search()browse(),如在第第七章中所述,记录集 – 与模型数据交互

写入模型数据的方法

ORM 提供了三种基本写操作的方法,如下所示:

  • <Model>.create(values)在模型上创建一个新的记录。它返回创建的记录。values可以是一个字典或字典列表,用于批量创建记录。

  • <Recordset>.write(values)使用values字典更新记录集。它不返回任何内容。

  • <Recordset>.unlink()从数据库中删除记录。它不返回任何内容。

values参数是一个将字段名称映射到要写入的值的字典。这些方法除了create()方法外,都带有@api.multi装饰器,而create()方法则带有@api.model装饰器。

Odoo 12 的变化

在 Odoo 12 中引入了能够使用create()访问字典列表,而不是单个字典对象的能力。这也允许我们批量创建记录。这种能力是通过特殊的@api.model_create_multi装饰器支持的。

在某些情况下,这些方法需要扩展以在触发时运行一些特定的业务逻辑。这个业务逻辑可以在主方法操作执行前后运行。

扩展 create()的示例

让我们看看一个利用这个特性的例子。我们希望防止在BorrowedReturned状态下直接创建新的出借记录。通常,验证应该在用@api.constrains装饰的特定方法中实现。但这个特定的情况与创建记录事件相关联,并且很难作为一个常规验证来实现。

编辑library_checkout/models/library_checkout.py文件并添加create()扩展方法:

    @api.model 
    def create(self, vals):
        # Code before create: should use the 'vals' dict
        new_record = super().create(vals) 
        # Code after create: can use the 'new_record' 
        # created 
        if new_record.stage_id.state in ("open", "close"):
            raise exceptions.UserError(
                "State not allowed for new checkouts."
            )
        return new_record

新记录是通过super().create()调创建的。在此之前,新记录在业务逻辑中不可用 – 只能使用values字典,或者甚至可以更改它,以强制对即将创建的记录设置值。

super().create()之后的代码可以访问已创建的新记录,并可以使用记录功能,例如使用点符号链访问相关记录。前面的示例使用new_record.stage_id.state来访问与新记录阶段相对应的状态。状态不是用户可配置的,并提供了一组可靠的业务逻辑中使用的值。因此,我们可以查找opendone状态,并在发现任何这些状态时引发错误。

扩展 write()的示例

让我们看看另一个例子。Checkout模型应记录书籍被借阅的日期,即Checkout Date,以及它们被归还的日期,即Close Date。这不能使用计算字段来完成。相反,应扩展write()方法以检测出借状态的更改,并在适当的时候更新已记录的日期:当状态变为openclose时。

在我们实现这个逻辑之前,必须创建两个日期字段。编辑library_checkout/models/library_checkout.py文件并添加以下代码:

    checkout_date = fields.Date(readonly=True)
    close_date = fields.Date(readonly=True)

当一条记录被修改时,当出借记录进入适当的状态时,应设置checkout_dateclose_date字段。为此,我们将使用自定义的write()方法,如下所示:

    def write(self, vals):
        # Code before write: 'self' has the old values 
        if "stage_id" in vals:
            Stage = self.env["library.checkout.stage"]
old_state = self.stage_id.state
new_state = 
              Stage.browse(vals["stage_id"]).state
if new_state != old_state and new_state == 
              "open":
                vals['checkout_date'] = fields.Date.today()
if new_state != old_state and new_state == 
               "done":
                vals['close_date'] = fields.Date.today()
        super().write(vals)
        # Code after write: can use 'self' with the updated 
        # values
        return True

在前面的示例中,扩展代码是在super()调用之前添加的;因此,self记录执行写操作之前。为了知道将要对该记录进行什么更改,我们可以检查vals参数。vals字典中的stage_id值是一个 ID 号,而不是一个记录,因此需要浏览以获取相应的记录,然后读取相应的state

比较新旧状态以在适当的时候触发日期值的更新。尽可能的情况下,我们更喜欢在super().write()指令之前更改要写入的值,并修改vals字典,而不是直接设置字段值。我们将在下一节中看到原因。

扩展 write()的示例,设置字段值

之前的代码仅修改用于写入的值;它没有直接将值分配给模型字段。这样做是安全的,但在某些情况下可能不够。

write() 方法内部分配模型字段值会导致无限递归循环:赋值会再次触发写方法,然后重复赋值,触发另一个写调用。这将一直重复,直到 Python 返回递归错误。

有一种技术可以避免这种递归循环,使得 write() 方法能够在其记录字段上设置值。诀窍是在设置值之前在环境 context 中设置一个唯一的标记,并且仅在不存在该标记时运行设置值的代码。

一个例子将有助于使这一点更清楚。让我们重写前面的例子,以便在调用 super() 之后而不是之前执行更新:

    def write(self, vals):
        # Code before write: 'self' has the old values 
        old_state = self.stage_id.state
        super().write(vals)
        # Code after write: can use 'self' with the updated 
        # values
        new_state = self.stage_id.state 
        if not self.env.context.get("_checkout_write"):
            if new_state != old_state and new_state == "open":
                self.with_context(
                  _checkout_write=True).write(
                    {"checkout_date": fields.Date.today()})
            if new_state != old_state and new_state == 
              "done":
                self.with_context(
                  _checkout_write=True).write(
                    {"close_date": fields.Date.today()})
        return True

使用这种技术,扩展代码由一个 if 语句保护,并且仅在上下文中找不到特定标记时才运行。此外,额外的 self.write() 操作使用 with_context 方法在执行写操作之前设置该标记。这种组合确保 if 语句中的自定义登录只运行一次,并且不会在进一步的 write() 调用中触发,从而避免无限循环。

何时扩展 create() 和 write() 方法

扩展 create()write() 方法应仔细考虑。

在大多数情况下,必须在记录保存时执行某些验证,或者必须自动计算某些值。对于这些常见情况,这里列出了更好的选项:

  • 对于基于其他字段自动计算的字段值,请使用计算字段。例如,当行值更改时,应计算一个标题总计。

  • 对于非固定字段默认值,请使用函数作为默认字段值。它将被评估并用于分配默认值。

  • 要在其他字段值更改时更改某些字段的值,请使用 onchange 方法,如果预期在用户界面中执行此操作,或者使用新的 onchange 方法,这仅在表单视图交互中起作用,而不是在直接写调用中,尽管计算可写字段在这两种情况下都起作用。添加 onchange 用户界面逻辑部分将提供更多关于此的信息。

  • 对于验证,请使用 constraint 函数。这些函数在字段值更改时自动触发,如果验证条件失败,则预期会引发错误。

仍然存在一些情况,这些选项中的任何一个都无法工作,需要扩展 create()write() 方法,例如当设置的默认值依赖于正在创建的记录的其他字段时。在这种情况下,默认值函数将不起作用,因为它无法访问新记录的其他字段值。

数据导入和导出方法

如在第 5 章 中讨论的,数据导入和导出(Importing, Exporting, and Module Data),也通过以下方法从 ORM API 中提供:

  • load([fields], [data]) 用于导入数据,并在 Odoo 导入 CSV 或电子表格数据到 Odoo 时使用。第一个参数是要导入的字段列表,它直接映射到 CSV 的顶部行。第二个参数是记录列表,其中每个记录都是要解析和导入的字符串值列表。它直接映射到 CSV 数据的行和列,并实现了 CSV 数据导入的功能,如外部标识符支持。

  • export_data([fields]) 由网络客户端的 Export 函数使用。它返回一个包含 datas 键的字典,其中包含数据;即,行列表。字段名称可以使用在 CSV 文件中使用的 .id/id 后缀,数据格式与可导入的 CSV 文件兼容。

用户在编辑数据时,也可以在用户界面中实现自动化。我们将在下一节中了解这一点。

添加 onchange 用户界面逻辑

用户在编辑时,也可以更改网络客户端视图。这种机制被称为 @api.onchange,它们在用户界面视图触发时被触发,当用户在特定字段上编辑值时。

自 Odoo 13 以来,可以通过使用一种称为 计算可写字段 的特定计算字段形式来实现相同的效果。这种 ORM 改进旨在避免经典 onchange 机制的一些限制,从长远来看,它应该完全取代它。

经典的 onchange 方法

onchange 方法可以更改表单中的其他字段值,执行验证,向用户显示消息,或在相关字段中设置域过滤器,限制可用的选项。

onchange 方法是异步调用的,并返回数据,这些数据被网络客户端用于更新当前视图中的字段。

onchange 方法与触发字段相关联,这些字段作为 @api.onchange("fld1", "fld2", ...) 装饰器的参数传递。

注意

api.onchange 参数不支持点符号表示法;例如,"partner_id.name"。如果使用,它将被忽略。

在方法内部,self 参数是一个虚拟记录,它包含当前表单数据。它是虚拟的,因为它可以是一个新记录或更改记录,它仍在被编辑,尚未保存到数据库中。如果在这个 self 记录上设置了值,这些值将在用户界面表单上更改。请注意,它不会写入数据库记录;相反,它提供信息,以便您可以在 UI 表单中更改数据。

注意

onchange 方法有一些限制,如文档所述 www.odoo.com/documentation/15.0/developer/reference/backend/orm.html#odoo.api.onchange。计算可写字段可以用作 onchange 的功能齐全的替代品。有关更多信息,请参阅 新的 onchange,使用计算可写字段 部分。

不需要返回值,但可以返回一个dict结构,其中包含要在用户界面中显示的警告消息或要在表单字段上设置的域过滤器。

让我们用一个例子来说明。在结账表单上,当选择图书馆成员时,请求日期将被设置为今天。如果日期发生变化,将向用户显示一个警告消息,提醒他们这一点。

为了实现这一点,编辑library_checkout/models/library_checkout.py文件并添加以下方法:

    @api.onchange("member_id")
    def onchange_member_id(self):
        today_date = fields.Date.today()
        if self.request_date != today_date:
            self.request_date = today_date
            return {
                "warning": {
                    "title": "Changed Request Date",
                    "message": "Request date changed to 
                      today!",
                }
            }

之前的onchange方法在用户界面上的member_id字段设置时触发。实际的方法名称并不重要,但惯例是它的名称以onchange_前缀开始。

onchange方法内部,self代表一个包含所有当前在编辑的记录中已设置的字段的单个虚拟记录,并且我们可以与之交互。

方法代码检查当前的request_date是否需要更改。如果需要,则将request_date设置为今天,以便用户能在表单中看到这一变化。然后,返回一个非阻塞的警告信息给用户。

onchange方法不需要返回任何内容,但它们可以返回一个包含警告或域键的字典,如下所示:

  • 警告键应该描述一个要在对话框窗口中显示的消息,例如{"title": "消息标题", "message": "消息正文"}

  • 域键可以设置或更改其他字段的域属性。这允许你构建更用户友好的界面;仅当此时有意义的选项才可用。域键的值看起来像{"user_id": [("email", "!=", False)]}

新的onchange,包含计算可写字段

经典onchange机制在 Odoo 框架提供的用户体验中扮演着关键角色。然而,它有几个重要的限制。

其中之一是它从服务器端事件中独立工作。onchange仅在表单视图请求时播放,不会因为实际的write()值变化而调用。这迫使服务器端业务逻辑明确重新播放相关的onchange方法。

另一个限制是onchange附加到触发字段上,而不是受影响的更改字段上。在非平凡的情况下,这变得难以扩展,并使得追踪更改的来源变得困难。

为了解决这些问题,Odoo 框架扩展了计算字段的特性,使其也能处理onchange用例。我们将这种技术称为计算可写字段。经典onchange仍然被支持和使用,但预计将在未来的版本中被计算字段取代并成为弃用。

Odoo 13 中的更改

计算可写字段是在 Odoo 13 中引入的,并且适用于该版本及以后的版本。

计算可写字段分配了计算方法,必须存储,并且必须有readonly=False属性。

让我们使用这种技术来实现之前的 onchange。以下是request_date字段定义应该如何更改:

    request_date = fields.Date(
        default=lambda s: fields.Date.today(),
        compute="_compute_request_date_onchange",
        store=True,
        readonly=False,
    )

这是一个常规的存储和可写字段,但它附加了一个可以在特定条件下触发的计算方法。例如,当member_id字段发生变化时,应该触发计算方法。

这是计算方法的代码,_compute_request_date_onchange

    @api.depends("member_id")
    def _compute_request_date_onchange(self):
        today_date = fields.Date.today()
        if self.request_date != today_date:
            self.request_date = today_date
            return {
                "warning": {
                    "title": "Changed Request Date",
                    "message": "Request date changed to 
                      today!",
                }
            }

@api.depends对于计算字段来说与往常一样工作,并声明了要监视其变化的字段。实际提供的字段列表与经典@api.onchange使用的相同。

方法代码可以非常类似于等效的 onchange 方法。在这个特定情况下,它们是相同的。请注意,计算字段并不保证在每次方法调用时都会设置一个值。这只有在满足某些条件时才会发生。在这种情况下,原始请求日期与今天的日期不同。这违反了常规的计算字段规则,但对于可计算的可写字段是允许的。

特别与业务流程相关的是发送电子邮件或通知用户的能力。下一节将讨论 Odoo 为此提供的功能。

消息和活动功能

Odoo 提供了全局消息和活动计划功能,所有这些功能都是由mail技术名称提供的。

消息功能是通过mail.thread模型添加的,并在表单视图中提供了一个消息小部件,也称为 Chatter。这个小部件允许你记录笔记或向其他人发送消息。它还保存了已发送消息的历史记录,并且也被自动过程用于记录进度跟踪消息。

同样的应用程序还通过mail.activity.mixin模型提供活动管理功能。可以将活动小部件添加到表单视图中,以便用户安排和跟踪活动的历史记录。

添加消息和活动功能

邮件模块提供了mail.thread抽象类,用于将消息功能添加到任何模型中,以及mail.activity.mixin,它为计划活动功能做同样的事情。在第四章 扩展模块中,我们解释了如何使用从混合抽象类继承的方式来添加这些继承功能到模型中。

让我们通过必要的步骤来了解:

  1. 通过编辑library_checkout/__manifest__.py文件中的'depends'键,将mail模块依赖项添加到library_checkout附加模块中,如下所示:

      "depends": ["library_member", "mail"],
    
  2. 要使library.checkout模型继承消息和活动抽象类,编辑library_checkout/models/library_checkout.py文件,如下所示:

    class Checkout(models.Model): 
        _name = "library.checkout"
        _description = "Checkout Request"
        _inherit = ["mail.thread", "mail.activity.mixin"]
    
  3. 要将消息和活动字段添加到结账表单视图中,编辑library_checkout/views/checkout_view.xml文件:

      <record id="view_form_checkout" model="ir.ui.view">
        <field name="name">Checkout Form</field>
        <field name="model">library.checkout</field>
        <field name="arch" type="xml">
          <form>
            <sheet>
              <group>
                <field name="member_id" />
                <field name="request_date" />
                <field name="user_id" />
              </group>
              <notebook>
                <page name="lines">
                  <field name="line_ids">
                    <tree editable="bottom">
                      <field name="book_id" />
                      <field name="note" />
                    </tree>
                  </field>
                </page>
              </notebook>
            </sheet>
            <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>
        </field>
      </record>
    </odoo>
    

完成这些操作后,结账模型将具有消息和活动字段及其功能。

消息和活动字段和模型

消息和活动功能向继承mail.threadmail.activity.mixin类的模型添加了新字段,以及所有这些功能的支持模型。这些都是已添加的基本数据结构。

mail.thread混合类使两个新字段可用:

  • message_follower_idsmail.followers具有一对一关系,并存储应接收通知的消息跟随者。跟随者可以是合作伙伴或渠道。合作伙伴代表特定的人或组织。渠道不是一个特定的人,而是代表订阅列表。

  • message_idsmail.message记录具有一对一关系,并列出记录的消息历史。

mail.activity.mixin混合类添加了以下新字段:

  • activity_idsmail.activity具有一对一关系,并存储已完成或计划的活动。

消息子类型

消息可以分配一个子类型。子类型可以识别特定事件,例如任务的创建或关闭,并且对于微调应发送给谁的通知非常有用。

子类型存储在mail.message.subtype模型中,可以在设置 | 技术 | 电子邮件 | 子类型菜单中进行配置。

可用的基本消息子类型如下:

  • mail.mt_comment XML ID,用于通过消息小部件中的发送消息选项发送的消息。跟随者将收到有关此消息的通知。

  • mail.mt_note XML ID,用于使用日志笔记XML ID 创建的消息,这些消息不会发送通知。

  • mail.mt_activities XML ID,用于使用安排活动链接创建的消息。它不打算发送通知。

应用程序可以添加自己的子类型,这些子类型通常与相关事件相关联。例如,报价已发送销售订单已确认。这些在消息历史中记录这些事件时,由应用程序的业务逻辑使用。

子类型允许您确定何时发送通知以及发送给谁。消息小部件右上角的跟随者菜单允许您添加或删除跟随者,以及选择他们将接收通知的特定子类型。以下截图显示了特定跟随者(在这种情况下为Deco Addict)的子类型选择表单:

图 8.4 – 选择活动消息子类型的跟随者小部件

图 8.4 – 选择活动消息子类型的跟随者小部件

子类型订阅标志可以手动编辑,其默认值在编辑子类型定义时配置,以检查默认字段。当它被设置时,新记录的跟随者将默认接收通知。

除了内置的子类型外,附加模块还会添加它们自己的子类型。子类型可以是全局的或针对特定模型。在后一种情况下,子类型的 res_model 字段标识了它应用的模型。

发布消息

模块业务逻辑可以利用消息系统向用户发送通知。

message_post() 方法用于发布消息。以下是一个示例:

self.message_post("Hello!")

上述代码添加了一条简单的文本消息,但没有向关注者发送通知。这是因为,默认情况下,消息是通过使用 subtype="mail.mt_note" 参数发布的。

要使消息发送通知,应使用 mail.mt_comment 子类型,如下例所示:

self.message_post(
    "Hello again!",
    subject="Hello",
    subtype='mail.mt_comment",
)

消息体是 HTML 格式,因此我们可以包括用于文本效果的标记,例如 <b> 用于粗体文本或 <i> 用于斜体。

消息体将出于安全原因进行清理,因此某些特定的 HTML 元素可能无法出现在最终消息中。

添加关注者

从业务逻辑角度来看,自动将关注者添加到文档中以便他们可以接收相应的通知也非常有用。有一些方法可以添加关注者,如下所示:

  • message_subscribe(partner_ids=<list of int IDs>) 添加合作伙伴

  • message_subscribe(channel_ids=<list of int IDs>) 添加渠道

  • message_subscribe_users(user_ids=<list of int IDs>) 添加用户

默认子类型将应用于每个订阅者。要强制用户订阅特定的子类型列表,您可以添加 subtype_ids=<list of int IDs> 属性,该属性列出了要启用订阅的特定子类型。如果使用此属性,它还将重置现有的关注者订阅的子类型到指定的子类型。

创建向导

向导是用户界面模式,为用户提供丰富的交互,通常用于为自动化流程提供输入。

例如,checkout 模块将为图书馆用户提供向借阅者群发电子邮件的向导。例如,他们可以选择最旧的借阅记录和借阅的书籍,并向他们发送消息,要求归还书籍。

用户首先访问借阅列表视图,选择要使用的借阅记录,然后从操作上下文菜单中选择发送消息选项。这将打开向导表单,允许他们编写消息主题和正文。点击发送按钮将向每个借阅所选借阅记录的人发送电子邮件。

向导模型

向导向用户显示表单视图,通常是一个对话框窗口,其中包含一些需要填写的字段和按钮来触发某些业务逻辑。然后,这些将被用于向导的逻辑。

这是通过使用与常规视图相同的模型/视图架构实现的,但支持模型基于models.TransientModel而不是models.Model。此类模型也有数据库表示,用于存储向导的状态。向导数据是临时的,以便向导完成其工作。一个计划任务会定期清理向导数据库表中的旧数据。

library_checkout/wizard/library_checkout_massmessage.py文件将创建用户交互所需的数据结构模型:要通知的借阅记录列表、消息主题和消息正文。

按照以下步骤将向导添加到library_checkout模块中:

  1. 首先,编辑library_checkout/__init__.py文件,将代码导入到wizard/子目录中,如下所示:

    from . import models
    from . import wizard
    
  2. 添加wizard/__init__.py文件,包含以下代码行:

    from . import checkout_mass_message
    
  3. 然后,创建实际的wizard/checkout_mass_message.py文件,如下所示:

    from odoo import api, exceptions, fields, models
    class CheckoutMassMessage(models.TransientModel): 
        _name = "library.checkout.massmessage"
        _description = "Send Message to Borrowers"
        checkout_ids = fields.Many2many(
            "library.checkout",
            string="Checkouts",
        )
        message_subject = fields.Char()
        message_body = fields.Html()
    

这样,我们就准备好了向导所需的基本数据结构。

注意,常规模型不应有使用瞬态模型的关联字段。

这意味着瞬态模型不应与常规模型有一对多关系。原因是瞬态模型上的一对多关系将要求常规模型与瞬态模型有反向多对一关系,这会导致自动清理瞬态记录时出现问题。

这种方法的替代方案是使用多对多关系。多对多关系存储在专用表中,并且当关系任一侧被删除时,该表中的行会自动删除。

向导的访问安全

正如常规模型一样,瞬态模型也需要定义访问安全规则。这可以通过与常规模块相同的方式进行,通常在security/ir.model.access.csv文件中完成。

Odoo 13 的变化

直到 Odoo 12,瞬态模型不需要访问安全规则。这在 Odoo 13 中发生了变化,因此现在瞬态模型需要访问规则,就像常规模型一样。

要为向导模型添加 ACL,编辑security/ir.model.access.csv文件并添加以下突出显示的行:

id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
checkout_user,Checkout User,model_library_checkout,library_app.library_group_user,1,1,1,1
checkout_line_user,Checkout Line
checkout_user,Checkout User,model_library_checkout,library_app.library_group_user,1,1,1,1
checkout_stage_user,Checkout Stage User,model_library_checkout_stage,library_app.library_group_user,1,0,0,0
checkout_stage_manager,Checkout Stage Manager,model_library_checkout_stage,library_app.library_group_manager,1,1,1,1
checkout_massmessage_user,Checkout Mass Message User,model_library_checkout_massmessage,library_app.library_group_user,1,1,1,1

只需添加一行即可为图书馆用户组添加完全访问权限;对于图书馆管理员组不需要特定的访问权限。

向导表单

向导表单视图的定义方式与常规模型相同,除了两个特定元素:

  • 可以使用<footer>部分来替换操作按钮。

  • 有一个special="cancel"按钮可以用来中断向导而不执行任何操作。

以下为wizard/checkout_mass_message_wizard_view.xml文件的内容:

<odoo>
  <record id="view_form_checkout_message" 
    model="ir.ui.view">
    <field name="name">Library Checkout Mass Message 
      Wizard</field>
    <field name="model">
      library.checkout.massmessage</field>
    <field name="arch" type="xml">
      <form>
        <group>
          <field name="message_subject" />
          <field name="message_body" />
          <field name="checkout_ids" />
        </group>
        <footer>
          <button type="object"
            name="button_send"
            string="Send Messages" />
          <button special="cancel" 
            string="Cancel" 
            class="btn-secondary" />
        </footer>
      </form>
    </field>
  </record>
  <record id="action_checkout_message"
          model="ir.actions.act_window">
    <field name="name">Send Messages</field>
    <field name="res_model">
      library.checkout.massmessage</field>
    <field name="view_mode">form</field>
    <field name="binding_model_id"
           ref="model_library_checkout" />
    <field name="binding_view_types">form,list</field>
    <field name="target">new</field>
  </record>
</odoo>

之前的 XML 代码添加了两个数据记录——一个用于向导表单视图,另一个用于打开向导的操作。

ir.actions.act_window 窗口操作记录在 binding_model_id 字段值中可用。

记得将此文件添加到清单文件中:

  "data": [
    "security/ir.model.access.csv",
    "wizard/checkout_mass_message_wizard_view.xml",
    "views/library_menu.xml",
    "views/checkout_view.xml",
    "data/library_checkout_stage.xml",
  ],

向导表单将如下所示:

图 8.5 – 发送消息向导表单

图 8.5 – 发送消息向导表单

要打开向导,用户应在检查列表视图中选择一个或多个记录,并通过列表视图顶部的 操作 菜单选择 发送消息 选项。

向导业务逻辑

在这一点上,操作打开向导表单,但还不能对记录执行任何操作。首先,我们希望向导显示在检查列表视图中选择的记录列表。

当打开向导表单时,它显示一个空表单。它还不是一条记录;这只会发生在你点击一个调用方法的按钮时。

备注

当打开向导表单时,我们有一个空记录。create() 方法尚未被调用;这只会发生在我们按下按钮时。因此,它不能用来设置在向导表单中展示的初始值。

仍然可以通过在字段上设置默认值来在空表单上填充数据。default_get() 是一个 ORM API 方法,负责计算记录的默认值。它可以扩展以添加自定义逻辑,如下所示:

    @api.model
    def default_get(self, field_names):
        defaults_dict = super().default_get(field_names)
        # Add values to the defaults_dict here
        return defaults_dict

前面的方法函数可以用来为 checkout_ids 字段添加默认值。但我们仍然需要知道如何访问原始列表视图中将要选择的记录列表。

当从客户端窗口导航到下一个窗口时,Web 客户端会在环境的 context 中存储一些关于原始视图的数据。这些数据如下:

  • Active_model,这是模型的官方名称

  • Active_id,这是表单活动记录的 ID 或从列表导航时的树视图的第一个记录

  • active_ids,这是一个包含所选记录的列表,如果你是从表单导航,则可能只有一个元素

  • active_domain,如果操作是从表单视图中触发的

在这种情况下,可以使用 active_ids 获取在列表视图中选择的记录 ID,并在 checkout_ids 字段上设置默认值。default_get 方法看起来是这样的:

    @api.model
    def default_get(self, field_names):
        defaults_dict = super().default_get(field_names)
        checkout_ids = self.env.context["active_ids"]
        defaults_dict["checkout_ids"] = checkout_ids
        return defaults_dict

首先,使用 super() 调用框架的 default_get() 实现方法,该方法返回一个包含默认值的字典。然后,将 checkout_id 键添加到 defaults_dict 中,并从环境上下文中读取 active_ids 值。

这样一来,当打开向导表单时,checkout_ids 字段将自动填充已选择的记录。接下来,需要实现表单的 发送消息 按钮的逻辑。

在检查表单 XML 代码时,我们可以看到 button_send 是按钮调用的函数名称。它应该定义在 wizard/checkout_mass_message.py 文件中,如下面的代码所示:

    def button_send(self):
        self.ensure_one()
        for checkout in self.checkout_ids:
            checkout.message_post(
                body=self.message_body,
                subject=self.message_subject,
                subtype='mail.mt_comment',
            )
        return True

该方法设计为与单个记录一起工作,如果 self 是一个记录集而不是单例,则不会正确工作。为了明确这一点,使用了 self.ensure_one()

在这里,self 代表向导记录数据,它在按钮被按下时创建。它包含在向导表单上输入的数据。进行验证以确保用户提供了消息体文本。

访问 checkout_id 字段,并通过循环遍历其记录中的每一个。对于每个结账记录,使用 mail.thread API 发送消息。必须使用 mail.mt_comment 子类型来发送通知邮件给记录的跟踪者。消息的 bodysubject 来自 self 记录字段。

对于方法来说,始终返回一些内容是一个好习惯——至少是 True 值。这样做的原因仅仅是因为一些 XML-RPC 客户端不支持 None 值。当一个 Python 函数没有明确的 return 语句时,它隐式地返回 None 值。在实践中,你可能不会意识到这个问题,因为网络客户端使用的是 JSON-RPC,而不是 XML-RPC,但遵循这个好习惯仍然很重要。

向导是我们业务逻辑工具箱中最复杂的工具,也是本章将要介绍的技术列表的结束。

业务逻辑还涉及在运行某些操作之前或之后测试是否满足正确的条件。下一节将解释如何在这种情况下触发异常。

抛出异常

有时候,输入不适合执行的任务,代码需要警告用户并使用错误消息中断程序的执行。这是通过抛出异常来完成的。Odoo 提供了在这些情况下应该使用的异常类。

最有用的 Odoo 异常如下:

from odoo import exceptions
raise exceptions.ValidationError("Inconsistent data")
raise exceptions.UserError("Wrong input")

应该在 Python 代码中的验证中使用 ValidationError 异常,例如在 @api.constrains 装饰的方法中。

应该在所有其他情况下使用 UserError 异常,在这些情况下,某些操作不应该被允许,因为它违反了业务逻辑。

作为一般规则,在方法执行期间所做的所有数据操作都是在数据库事务中完成的,并在发生异常时回滚。这意味着,当抛出异常时,所有之前的数据更改都会被取消。

让我们看看使用向导的 button_send 方法的例子。如果我们仔细想想,如果没有选择任何结账文档,运行发送消息的逻辑就没有任何意义。同样,发送没有消息体的消息也没有意义。如果发生这些情况之一,我们应该警告用户。

要这样做,编辑 button_send() 方法并添加以下突出显示的代码:

    def button_send(self):
        self.ensure_one()
        if not self.checkout_ids:
            raise exceptions.UserError(
                "No Checkouts were selected."
            )
        if not self.message_body:
            raise exceptions.UserError(
                "A message body is required"
            )
        for checkout in self.checkout_ids:
            checkout.message_post(
                body=self.message_body,
                subject=self.message_subject,
                subtype='mail.mt_comment',
            )
        return True

当您使用异常时,请确保在代码文件顶部添加了from odoo import exceptions指令。添加验证就像检查某些条件是否满足,如果没有满足则抛出异常一样简单。

下一节将讨论每个 Odoo 开发者都应该熟悉的开发工具。我们将从自动化测试开始。

编写单元测试

自动化测试通常被视为软件开发的最佳实践。它们不仅有助于确保代码正确实现,更重要的是,它们为未来的代码更改或重写提供了一个安全网。

在动态编程语言(如 Python)的情况下,没有编译步骤,语法错误可能被忽略。确保有测试代码覆盖率对于检测代码编写错误(如误拼的标识符名称)尤为重要。

这两个目标为测试编写提供了指导。一个目标应该是测试覆盖率——编写运行所有代码行的测试用例。

仅此一项通常就能在第二个目标上取得良好进展,即验证代码的正确性。这是因为,在完成代码覆盖率测试后,我们将有一个很好的起点来构建非平凡用例的附加测试用例。

Odoo 12 中的更改

在较早的 Odoo 版本中,测试也可以使用 YAML 数据文件来描述。随着 Odoo 12 的发布,YAML 数据文件引擎已被移除,此类文件不再受支持。有关它的最后一份文档可在 https://doc.odoo.com/v6.0/contribute/15_guidelines/coding_guidelines_testing/找到。

接下来,我们将学习如何向模块中添加单元测试并运行它们。

添加单元测试

附加模块测试必须添加到tests/子目录中。测试运行器将自动发现具有此特定名称的子目录中的测试,并且模块的顶级__init__.py文件不应导入它们。

要为在library_checkout附加模块中创建的向导逻辑添加测试,我们首先需要创建tests/__init__.py文件并将要使用的测试文件导入。在这种情况下,它应该包含以下代码行:

from . import test_checkout_mass_message

然后,我们必须创建tests/test_checkout_mass_message.py文件并确保它具有单元测试代码的基本框架:

from odoo import exceptions
from odoo.tests import common
class TestWizard(common.SingleTransactionCase):
    def setUp(self, *args, **kwargs):
        super(TestWizard, self).setUp(*args, **kwargs)
        # Add test setup code here... 
    def test_01_button_send(self):
        """Send button should create messages on
           Checkouts"""
        # Add test code

Odoo 提供了一些用于测试的类,如下所示:

  • TransactionCase为每个测试使用不同的事务,并在测试结束时自动回滚。

  • SingleTransactionCase在一个事务中运行所有测试,这些测试只在最后一个测试结束时回滚。这可以显著加快测试速度,但单个测试需要以兼容的方式编写。

这些测试类是围绕 Python 标准库中的unittest测试用例的包装器。有关更多详细信息,您可以参考官方文档:https://docs.python.org/3/library/unittest.html。

setUp()方法是在这里准备测试数据的地方,通常存储为类属性,以便在测试方法中使用。

测试作为类方法实现,例如在前面代码中的test_01_button_send()示例。测试用例方法名称必须以test_前缀开头。这就是允许测试运行器发现它们的原因。测试方法按测试函数名称的顺序运行。

当运行测试时,docstring方法会被打印到服务器日志中,并应用来提供正在执行的测试的简短描述。

运行测试

一旦编写了测试,就是时候运行它们了。为此,您必须升级或安装要测试的模块(-I-u),并将-test-enable选项添加到 Odoo server命令中。

命令看起来像这样:

(env15) $ odoo -c library.conf --test-enable -u library_checkout --stop-after-init

只会测试已安装或升级的模块 - 这就是为什么使用了-u选项。如果需要安装某些依赖项,它们的测试也会运行。如果您不希望发生这种情况,那么请安装新模块,然后在升级(-u)测试模块时运行测试。

虽然该模块包含测试代码,但这些代码还没有进行任何测试,应该能够成功运行。如果我们仔细查看服务器日志,应该有报告测试运行的INFO消息,类似于以下内容:

INFO library odoo.modules.module: odoo.addons.library_checkout.tests.test_checkout_mass_message running tests.

测试代码框架已经准备好了。现在,需要添加实际的测试代码。我们应该从设置数据开始。

设置测试

编写测试的第一步是准备要使用的数据。这通常在setUp方法中完成。在我们的例子中,需要一个结账记录,以便在向导测试中使用。

将测试操作作为特定用户执行以测试访问控制是否已正确配置是很方便的。这可以通过使用sudo(<user>)模型方法来实现。Recordsets 会携带这些信息,因此在使用sudo()创建后,同一 recordset 中的后续操作将使用相同的上下文执行。

这是setUp方法的代码:

from odoo import exceptions
from odoo.tests import common
class TestWizard(common.SingleTransactionCase):
    def setUp(self, *args, **kwargs):
        super().setUp(*args, **kwargs)
        # Setup test data
        admin_user = self.env.ref('base.user_admin')
        self.Checkout = self.env['library.checkout']\
            .with_user(admin_user)
        self.Wizard = self.env[
          'library.checkout.massmessage']\
            .with_user(admin_user)
        a_member = self.env['library.member']\
            .create({'partner_id': 
               admin_user.partner_id.id})
        self.checkout0 = self.Checkout\
            .create({'member_id': a_member.id})

现在,我们可以使用self.checkout0记录和self.Wizard模型来进行我们的测试。

编写测试用例

现在,让我们扩展我们在初始框架中看到的test_button_test()方法,以实现测试。

基本测试会在测试对象上运行一些代码,得到一个结果,然后使用assert语句将其与预期结果进行比较。消息发布逻辑不会返回任何值以进行检查,因此需要不同的方法。

button_send()方法向消息历史记录中添加一条消息。检查这发生的一种方法是在运行方法前后计算消息的数量。测试代码可以在向导前后计算消息的数量。以下代码添加了这一点:

    def test_01_button_send(self):
        """Send button creates messages on Checkouts"""
        count_before = len(self.checkout0.message_ids)
        # TODO: run wizard
        count_after = len(self.checkout0.message_ids)
        self.assertEqual(
            count_before + 1,
            count_after,
"Expected one additional message in the 
             Checkout.",
        )

验证测试是否成功或失败的检查是self.assertEqual语句。它比较运行向导前后的消息数量。预期会发现比之前多一条消息。最后一个参数提供了一个可选但推荐的消息,当测试失败时打印出来。

assertEqual函数只是可用的断言方法之一。应根据要执行的检查选择适当的断言函数。《unittest》文档提供了所有方法的良好参考,可以在 https://docs.python.org/3/library/unittest.html#test-cases 找到。

运行向导并不简单,用户界面工作流程需要被模拟。回想一下,环境上下文被用来通过active_ids键向向导传递数据。我们必须创建一个带有消息主题和正文已填写值的向导记录,然后运行button_send方法。

完整的代码如下所示:

    def test_01_button_send(self):
        """Send button creates messages on Checkouts"""
        count_before = len(self.checkout0.message_ids)
        Wizard0 = self.Wizard\
            .with_context(active_ids=self.checkout0.ids)
        wizard0 = Wizard0.create({
            "message_subject": "Hello",
            "message_body": "This is a message.",
        })
        wizard0.button_send()
        count_after = len(self.checkout0.message_ids)
        self.assertEqual(
            count_before + 1,
            count_after,
            "Expected one additional message in the 
             Checkout.",
        )

使用with_context模型方法将active_ids添加到环境上下文中。然后,使用create()方法创建向导记录并添加用户输入的数据。最后,调用button_send方法。

为测试类添加更多测试用例,并使用额外的测试方法。记住,对于TransactionCase测试,每个测试结束时都会进行回滚,并且前一个测试中执行的操作会被撤销。对于SingleTransactionCase,测试会依次构建,测试运行顺序很重要。由于测试是按字母顺序运行的,因此为测试方法选择的名字是相关的。为了使这一点更清晰,一个好的做法是在测试方法名前添加一个数字,就像我们在前面的例子中所做的那样。

测试异常

在某些情况下,代码预期会引发异常,这也应该被测试。例如,我们可以测试验证是否被正确执行。

继续进行向导测试,执行验证以检查消息正文是否为空。可以添加一个测试来检查此验证是否正确执行。

要检查是否引发了异常,相应的代码必须放在with self.assertRaises()代码块内。

应该为这个测试添加另一种方法,如下所示:

    def test_02_button_send_empty_body(self):
        """Send button errors on empty body message"""
        Wizard0 = self.Wizard\
            .with_context(active_ids=self.checkout0.ids)
        wizard0 = Wizard0.create({})
        with self.assertRaises(exceptions.UserError) as e:
            wizard0.button_send()

如果button_send()方法没有引发UserException,则测试将失败。如果它确实引发了该异常,则测试将成功。引发的异常存储在e变量中,可以通过额外的方法命令进行检查——例如,以验证错误消息的内容。

使用日志消息

将消息写入日志文件对于监控和审计运行系统很有用。它还可以帮助代码维护,使得从运行过程中获取调试信息更容易,而无需更改代码。

要在 Odoo 代码中使用日志记录,首先,必须准备一个记录器对象。为此,在 library_checkout/wizard/checkout_mass_message.py 文件顶部添加以下代码行:

import logging
_logger = logging.getLogger(__name__)

这里使用的是 logging Python 标准库模块。使用当前代码文件的名称 __name__ 初始化 _logger 对象。这样,日志消息将包括生成它们的文件信息。

日志消息有几种可用级别。具体如下:

_logger.debug('A DEBUG message') 
_logger.info('An INFO message') 
_logger.warning('A WARNING message') 
_logger.error('An ERROR message')

我们现在可以使用记录器将消息写入 Odoo 服务器日志。

此日志可以添加到 button_send 向导方法中。在结束行之前添加以下指令;即,return True

        _logger.info(
            "Posted %d messages to the Checkouts: %s",
            len(self.checkout_ids),
            str(self.checkout_ids),
        )

使用此代码,当使用向导发送消息时,服务器日志将打印出类似以下的消息:

INFO library odoo.addons.library_checkout.wizard.checkout_mass_message: Posted 2 messages to the Checkouts: [3, 4]

注意,在日志消息中没有使用 Python 字符串插值 - 即,使用 % 操作符。更具体地说,而不是 _logger.info("Hello %s" % "World"),使用的是类似 _logger.info("Hello %s", "World") 的方式。不使用插值意味着代码在运行时有一个更少的任务要执行,这使得日志记录更高效。因此,变量应始终作为额外的日志参数提供。

服务器日志消息的时间戳始终使用 UTC。这可能会让人惊讶,这源于 Odoo 服务器内部处理所有日期都使用 UTC 的事实。

对于调试级别的日志消息,使用 _logger.debug()。例如,在 checkout.message_post() 指令之后立即添加以下调试日志消息:

            _logger.debug(
                "Message on %d to followers: %s",
                checkout.id,
                checkout.message_follower_ids)

默认情况下,这不会将任何内容打印到服务器日志中,因为默认日志级别是 INFO。需要将日志级别设置为 DEBUG 才能将调试消息打印到日志中。

Odoo 的 --log-level 命令选项设置了一般的日志级别。例如,将 --log-level=debug 添加到命令行中可以启用所有调试日志消息。

这可以通过微调并只为特定模块设置特定的日志级别来进行优化。要只为这个向导代码启用调试消息,使用 --log-handler 选项。这可以多次使用来设置多个模块的日志级别。

例如,向导的 Python 模块是 odoo.addons.library_checkout.wizard.checkout_mass_message,如 INFO 日志消息所示。要为其设置调试日志级别,使用以下命令行选项:

--log-handler=
odoo.addons.library_checkout.wizard.checkout_mass_message:DEBUG

Odoo 服务器日志选项的完整参考可以在官方文档中找到:www.odoo.com/documentation/15.0/developer/misc/other/cmdline.html

小贴士

如果你想深入了解 Python 日志记录,官方文档是一个很好的起点:docs.python.org/3/library/logging.html

记录日志是一个有用的工具,但在调试方面比较简短。有一些工具和技术可以帮助开发者完成工作。我们将在下一节中探讨这些工具。

了解可用的开发者工具

几种工具可以简化开发者的工作。我们在这本书中之前介绍过的网页界面的开发者模式就是其中之一。还有一个服务器开发者模式选项,它提供了一些对开发者友好的功能。接下来将详细介绍这些功能。之后,我们将讨论如何在服务器上调试代码。

服务器开发选项

Odoo 服务器提供了一个--dev选项,该选项启用开发者功能以加快开发周期,例如以下功能:

  • 在发现附加模块中的异常时进入调试器。这是通过设置调试器完成的。pdb是默认的。

  • 当 Python 代码文件保存时自动重新加载 Python 代码,避免手动重启服务器。这可以通过reload选项启用。

  • 直接从 XML 文件中读取视图定义,避免手动模块升级。这可以通过xml选项启用。

  • 直接在网页浏览器中使用 Python 调试接口。这可以通过werkzeug选项启用。

--dev选项接受一个以逗号分隔的选项列表。可以使用all选项,通过--dev=all方便地启用所有这些选项。

当你启用调试器时,Odoo 服务器默认使用pdb,但如果你的系统中已安装其他选项,也可以使用。支持的替代选项如下:

当你编辑 Python 代码时,每次代码更改都需要重新启动服务器,以便重新加载并使用 Odoo 的最新代码。--dev=reload选项可以自动化这一过程。启用后,Odoo 服务器会检测到对代码文件的更改,并自动触发代码重新加载,使代码更改立即生效。

为了使代码重新加载生效,需要watchdogPython 包。可以使用以下命令进行安装:

(env15) $ pip3 install watchdog

--dev=all服务器命令选项还启用了reload,这是最常用的方式:

(env15) $ odoo -c library.conf --dev=all

注意,这仅适用于 Python 代码更改。对于其他更改,例如更改模型的数据结构,需要模块升级;仅重新加载是不够的。

调试

开发者工作中的一大部分是调试代码。为此,能够设置断点和逐步运行代码是非常方便的。

Odoo 是一个运行 Python 代码的服务器,它等待客户端请求,这些请求由相关服务器代码处理,然后向客户端返回响应。这意味着 Python 代码的调试是在服务器端进行的。断点在服务器上激活,暂停代码执行到该行。因此,开发者需要访问运行服务器的终端窗口,以便设置断点和在触发断点时操作调试器。

Python 调试器

可用的最简单的调试工具是 Python 集成调试器 pdb。然而,还有其他选项提供更丰富的用户界面,更接近于复杂 IDE 通常提供的界面。

调试器提示符可以触发的有两种方式。

一种是当抛出一个未处理的异常并且启用了 --dev=all 选项时。调试器将在引发异常的指令处停止代码执行。开发者可以检查那一刻的变量和程序状态,以更好地理解导致异常的原因。

另一种方式是通过编辑代码并添加以下行来手动设置断点,以便在执行应暂停的位置:

import pdb; pdb.set_trace()

这不需要启用 –dev 模式。需要重新加载 Odoo 服务器以使用更改后的代码。当程序执行到达 pdb.set_trace() 命令时,服务器终端窗口将显示 (pdb) Python 提示符,等待输入。

(pdb) 提示符充当 Python shell,可以在当前执行上下文中运行任何表达式或命令。这意味着可以检查当前变量,甚至修改它们。

一些调试器特定的命令也是可用的。这些是最重要的可用命令:

  • h(帮助)显示可用 pdb 命令的摘要。

  • p(打印)评估并打印一个表达式。

  • pp(pretty-print)用于打印数据结构,如字典或列表。

  • l(列表)列出将要执行的下一条指令周围的代码。

  • n(下一步)跳过到下一条指令。

  • s(步进)进入当前指令。

  • c(继续)正常继续执行。

  • u(向上)在执行栈中向上移动。

  • d(向下)在执行栈中向下移动。

  • bt(backtrace)显示当前执行栈。

Python 官方文档包括 pdb 命令的完整描述:docs.python.org/3/library/pdb.html#debugger-commands

一个示例调试会话

要了解如何使用调试器的功能,让我们看看一个调试会话的样子。

首先在 button_send() 工作表方法的第 一行添加调试器断点,如下所示:

    def button_send(self):
        import pdb; pdb.set_trace()
        self.ensure_one()
        # ...

在执行服务器重新加载后,打开服务器上的 button_send() 方法,这将暂停在断点处。当它等待服务器的响应时,Web 客户端将保持在 加载中… 状态。

在那个时刻,运行服务器的终端窗口应该显示类似以下内容:

> /home/daniel/work15/library/library_checkout/wizard
/checkout_mass_message.py(29)button_send()
-> self.ensure_one()
(Pdb)

这是 pdb 调试器提示符,前两行提供了有关 Python 代码执行暂停位置的信息。第一行显示文件、行号和函数名,而第二行是下一行将要运行的代码。

提示

在调试会话期间,服务器日志消息可能会悄悄出现。其中大部分来自 werkzeug 模块。可以通过向 Odoo 命令添加 --log-handler=werkzeug:WARNING 选项来静音这些消息。另一种选择是使用 --log-level=warn 降低日志的一般详细程度。

输入 h 会显示可用命令的快速参考。输入 l 会显示当前代码行及其周围的代码行。

输入 n 会运行当前代码行并移动到下一行。按下 Enter 重复上一个命令。

p 调试命令会打印出一个表达式的结果,而 pp 命令则执行相同的操作,但将输出格式化得更加易读,特别是对于 dictlist 数据结构。例如,要打印用于向导中的 checkout_ids 字段值,请输入以下命令:

(pdb) self.checkout_ids
library.checkout(1,)
(Pdb)

调试提示符可以运行 Python 命令和表达式。允许任何 Python 表达式,甚至变量赋值。

当你完成调试会话后,输入 c 以继续正常程序执行。在某些情况下,你可能想要中断执行,此时可以使用 q 来退出。

提示

当你从调试器返回到终端提示符时,终端可能会看起来无响应,并且任何输入的文本都不会打印到终端。这可以通过使用 reset 命令来解决;即通过输入 <enter>reset<enter>

其他 Python 调试器

虽然 pdb 有现成可用的优势,但它可能相当简洁。幸运的是,还有一些更舒适的选项存在。

IronPython 调试器 ipdb 是一个流行的选择,它使用与 pdb 相同的命令,但增加了如自动补全和语法高亮等改进,使用起来更加舒适。可以使用以下命令进行安装:

$ pip3 install ipdb

要添加断点,请使用以下命令:

import ipdb; ipdb.set_trace()

另一个替代的调试器是 pudb。它也支持与 pdb 相同的命令,并在文本终端中工作,但它使用类似窗口的图形显示。有用的信息,如当前上下文中的变量及其值,可以在屏幕上的单独窗口中轻松获取。

它可以通过系统包管理器或通过 pip 安装,如下所示:

$ sudo apt-get install python-pudb  # using Debian OS packages
$ pip3 install pudb  # or using pip, possibly in a virtualenv

可以以类似于 pdb 的方式添加断点:

import pudb; pudb.set_trace()

也有简短版本可用:

import pudb; pu.db

上述代码可以更快地输入,同时也提供了预期的效果——添加代码执行断点。

注意

自 Python 3.7 以来,可以通过使用 breakpoint() 函数而不是 pdb.set_trace() 来简化断点的设置。调试库可以覆盖 breakpoint() 的行为,直接调用它们。然而,在撰写本文时,pudbipdb 并没有这样做,因此使用 breakpoint() 与它们一起没有好处。

打印消息和记录日志

有时,我们只需要检查一些变量的值或检查某些代码块是否正在执行。Python 的 print() 指令可以完美地完成这项工作,而不会停止执行流程。请注意,打印的文本被发送到标准输出,如果它被写入文件,则不会存储在服务器日志中。

print() 函数仅用作开发辅助工具,不应出现在最终代码中,准备部署。如果 print 语句还能帮助调查生产系统中的问题,考虑将它们转换为调试级别的日志消息。

检查和终止正在运行的进程

还有几个技巧可以让我们检查正在运行的 Odoo 进程。

首先,找到服务器实例的 进程 IDPID)。这个数字在每个日志消息中打印出来,紧随时间戳之后。另一种找到 PID 的方法是,在另一个终端窗口中运行以下命令:

$ ps ax | grep odoo

下面是一个示例输出:

 2650 pts/5  S+   0:00 grep --color=auto odoo
21688 pts/4  Sl+  0:05 python3 /home/daniel/work15/env15/bin/odoo

输出的第一列是进程的 PID。在这个例子中,21688 是 Odoo 进程的 PID。

现在我们知道了进程的 PID,可以向该 Odoo 服务器进程发送信号。kill 命令用于发送这些信号。默认情况下,kill 发送一个信号来终止进程,但它也可以发送其他更友好的信号。

如果向 Odoo 服务器发送 SIGQUIT-3 信号,Odoo 服务器将打印出当前正在执行的代码的堆栈跟踪:

$ kill -3 <PID>

发送 SIGQUIT 后,Odoo 服务器日志将显示堆栈跟踪。这有助于了解当时正在执行哪些代码。这些信息为每个正在使用的线程打印出来。

它被一些代码分析方法用来跟踪服务器在哪里花费时间,并分析代码的执行。有关代码分析的有用信息可以在官方文档中找到,见 www.odoo.com/documentation/15.0/howtos/profilecode.html

我们可以向 Odoo 服务器进程发送的其他信号包括 HUP,用于重新加载服务器,以及 INTTERM,用于强制服务器关闭,如下所示:

$ kill -HUP <PID>
$ kill -TERM <PID>

HUP 信号在无需停止服务器的情况下重新加载 Odoo 配置时特别有用。

摘要

在本章中,我们探讨了 ORM API 的各种功能以及如何使用它们来创建对用户做出反应的动态应用程序,这有助于他们避免错误并自动化繁琐的任务。

模型验证和计算字段可以覆盖很多用例,但并非全部。我们学习了如何扩展 API 的创建、写入和解除链接方法以覆盖更多用例。

为了丰富的用户交互,我们使用了 mail 核心插件混入来添加用户关于文档的通信和在其上计划活动的功能。向导允许应用程序与用户进行对话并收集运行特定流程所需的数据。异常允许应用程序中止不正确的操作,通知用户问题并回滚中间更改,保持系统一致性。

我们还讨论了开发者创建和维护应用程序可用的工具:日志消息、调试工具和单元测试。

在下一章中,我们仍然会使用 ORM,但我们将从外部应用的角度来审视它:我们将使用 Odoo 服务器作为后端来存储数据和运行业务流程。

进一步阅读

以下是在本章讨论的主题中最相关的参考资料:

第七章:第九章:外部 API – 与其他系统集成

Odoo 服务器提供了一个外部 API,该 API 被其 Web 客户端使用,并且也适用于其他客户端应用程序。在本章中,我们将学习如何使用 Odoo 外部 API 通过将其作为后端来实现与 Odoo 服务器交互的外部应用程序。

这可以用来编写脚本以加载或修改 Odoo 数据,或者与现有的 Odoo 业务应用程序集成,这互补且不能被 Odoo 应用程序取代。

我们将描述如何使用 OdooRPC 调用,然后利用这些知识使用 Python 构建一个简单的命令行应用程序,用于图书馆Odoo 应用程序。

本章将涵盖以下主题:

  • 介绍学习项目 – 一个用于编目书籍的客户端应用程序

  • 在客户端机器上设置 Python

  • 探索 Odoo 外部 API

  • 实现客户端应用程序的 XML-RPC 接口

  • 实现客户端应用程序的用户界面

  • 使用 OdooRPC 库

到本章结束时,您应该已经创建了一个简单的 Python 应用程序,该应用程序可以使用 Odoo 作为后端来查询和存储数据。

技术要求

本章中的代码需要我们在第三章您的第一个 Odoo 应用程序中创建的library_app Odoo 模块。相应的代码可以在此书的 GitHub 仓库中找到:github.com/PacktPublishing/Odoo-15-Development-Essentials

Git 克隆仓库的路径应该在 Odoo 插件路径中,并且应该安装library_app模块。代码示例将假设您正在使用的 Odoo 数据库是library,以与提供的安装说明保持一致,见第二章准备开发环境

本章中的代码可以在同一仓库中的ch09/client_app/目录中找到。

介绍学习项目 – 一个用于编目书籍的客户端应用程序

在本章中,我们将开发一个简单的客户端应用程序来管理图书馆书籍编目。这是一个命令行界面(CLI)应用程序,使用 Odoo 作为其后端。我们将实现的功能将是基本的,以保持对与 Odoo 服务器交互所使用技术的关注。

这个简单的命令行应用程序应该能够完成以下操作:

  • 通过标题搜索和列出书籍。

  • 向编目中添加新书。

  • 编辑书籍标题。

目标是专注于如何使用 Odoo 外部 API,因此我们希望避免引入您可能不熟悉的额外编程语言。通过引入这个限制,最合理的选择是使用 Python 来实现客户端应用程序。然而,一旦我们理解了特定语言的 XML-RPC 库,处理 RPC 调用的技术也将适用。

应用程序将是一个 Python 脚本,它期望执行特定命令。以下是一个示例:

$ python3 library.py add "Moby-Dick"
$ python3 library.py list "moby"
3 Moby-Dick
$ python3 library.py set-title 3 "Moby Dick"

此示例会话演示了使用客户端应用程序添加、列出和修改书名。

此客户端应用程序将使用 Python 运行。在我们开始查看客户端应用程序的代码之前,我们必须确保 Python 已安装在客户端机器上。

在客户端机器上设置 Python

Odoo API 可以通过两种不同的协议从外部访问:XML-RPC 和 JSON-RPC。任何能够实现这些协议之一客户端的外部程序都将能够与 Odoo 服务器交互。为了避免引入额外的编程语言,我们将使用 Python 来探索外部 API。

到目前为止,Python 代码仅在服务器端使用。对于客户端应用程序,Python 代码将在客户端运行,因此工作站可能需要额外的设置。

要遵循本章中的示例,您所使用的系统需要能够运行 Python 3 代码。如果您已经遵循了本书其他章节中使用的相同开发环境,这可能已经实现。然而,如果尚未实现,我们应该确保 Python 已安装。

要确保开发工作站上已安装 Python 3,请在终端窗口中运行python3 --version命令。如果没有安装,请参考官方网站以找到适用于您系统的安装包,网址为www.python.org/downloads/

对于 Ubuntu,有很大可能性它已经预安装在您的系统上。如果没有,可以使用以下命令安装:

$ sudo apt-get install python3 python3-pip

对于 Windows 10,可以从 Microsoft Store 安装。

在 PowerShell 中运行python3将引导您到相应的下载页面。

如果您是 Windows 用户并且已使用一站式安装程序安装了 Odoo,您可能会想知道为什么 Python 解释器尚未对您可用。在这种情况下,您需要额外的安装。简短的回答是,Odoo 一站式安装程序包含一个嵌入的 Python 解释器,它不会直接提供给通用系统。

现在 Python 已经安装并可用,它可以用来探索 Odoo 外部 API。

探索 Odoo 外部 API

在我们实现客户端应用程序之前,应该先熟悉 Odoo 外部 API。以下章节将使用Python 解释器探索 XML-RPC API。

使用 XML-RPC 连接到 Odoo 外部 API

访问 Odoo 服务器的最简单方法是使用 XML-RPC。Python 标准库中的xmlrpc库可以用于此目的。

请记住,正在开发的应用程序是一个连接到服务器的客户端。因此,需要一个正在运行的 Odoo 服务器实例供客户端连接。代码示例将假设 Odoo 服务器实例在同一台机器上运行,http://localhost:8069,但如果您要使用的服务器在不同的机器上运行,则可以使用任何可到达的 URL。

Odoo xmlrpc/2/common 端点公开了公共方法,并且可以在不登录的情况下访问这些方法。这些方法可以用来检查服务器版本和验证登录凭证。让我们使用 xmlrpc 库来探索公开的 common Odoo API。

首先,启动 Python 3 控制台并输入以下内容:

>>> from xmlrpc import client
>>> srv = "http://localhost:8069"
>>> common = client.ServerProxy("%s/xmlrpc/2/common" % srv)
>>> common.version()
{'server_version': '15.0', 'server_version_info': [15, 0, 0, 'final', 0, ''], 'server_serie': '15.0', 'protocol_version': 1}

之前的代码导入了 xmlrpc 库,并设置了一个包含服务器地址和监听端口的变量。这可以根据要连接的 Odoo 服务器的特定 URL 进行调整。

接下来,创建一个 XML-RPC 客户端对象以访问在 /xmlrpc/2/common 端点公开的服务器公共服务。您不需要登录。那里可用的方法之一是 version(),它用于检查 Odoo 服务器版本。这是一种简单的方法来确认与服务器通信是否正常。

另一个有用的公共方法是 authenticate()。此方法确认用户名和密码被接受,并返回在请求中应使用的用户 ID。以下是一个示例:

>>> db, user, password = "library", "admin", "admin"
>>> uid = common.authenticate(db, user, password, {})
>>> print(uid)
2

authenticate() 方法期望四个参数:数据库名称、用户名、密码和用户代理。之前的代码使用变量来存储这些信息,然后将这些变量作为参数传递。

Odoo 14 的变化

Odoo 14 支持使用 API 密钥,这可能对于外部访问 Odoo API 是必需的。API 密钥可以在用户的偏好表单中设置,在账户安全选项卡中。XML-RPC 的使用方式相同,只是应该使用 API 密钥作为密码。更多详细信息请参阅官方文档www.odoo.com/documentation/15.0/developer/misc/api/odoo.html#api-keys

应使用用户代理环境来提供有关客户端的一些元数据。这是强制性的,至少应该是一个空字典 {}

如果身份验证失败,将返回 False 值。

common 公共端点相当有限,因此要访问 ORM API 或其他端点,需要使用所需的身份验证。

使用 XML-RPC 运行服务器方法

要访问 Odoo 模型和它们的方法,需要使用 xmlrpc/2/object 端点。对该端点的请求需要登录详细信息。

此端点公开了一个通用的 execute_kw 方法,并接收模型名称、要调用的方法以及一个包含传递给该方法的参数列表。

下面是一个 execute_kw 的工作示例。它调用 search_count 方法,该方法返回与域过滤器匹配的记录数:

>>> api = xmlrpc.client.ServerProxy('%s/xmlrpc/2/object' % srv)
>>> api.execute_kw(db, uid, password, "res.users", "search_count", [[]])
3

此代码使用 xmlrpc/2/endpoint 对象来访问服务器 API。使用以下参数调用 execute_kw() 方法:

  • 要连接到的数据库名称

  • 连接用户 ID

  • 用户密码(或 API 密钥)

  • 目标模型标识符

  • 要调用的方法

  • 位置参数列表

  • 可选的包含关键字参数的字典(在此示例中未使用)

可以调用所有模型方法,除了以下划线 (_) 开头的,这些被认为是私有的。某些方法可能不适用于 XML-RPC 协议,如果它们返回的值无法通过 XML-RPC 协议发送。这是 browse() 的情况,它返回一个记录集对象。尝试通过 XML-RPC 使用 browse() 会返回 TypeError: cannot marshal objects 错误。而不是 browse(),XML-RPC 调用应使用 readsearch_read,它们返回的数据格式是 XML-RPC 协议可以发送到客户端的格式。

现在,让我们看看如何使用 searchread 来查询 Odoo 数据。

使用搜索和读取 API 方法

Odoo 服务器端代码使用 browse 来查询记录。RPC 客户端不能使用它,因为记录集对象不能通过 RPC 协议传输。相反,应使用 read 方法。

read([<ids>, [<fields>])browse 方法类似,但它返回的是记录列表,而不是记录集。每个记录都是一个字典,包含请求的字段及其数据。

让我们看看如何使用 read() 方法从 Odoo 中检索数据:

>>> api = xmlrpc.client.ServerProxy("%s/xmlrpc/2/object" % srv)
>>> api.execute_kw(db, uid, password, "res.users", "read", [2, ["login", "name", "company_id"]])
[{'id': 2, 'login': 'admin', 'name': 'Mitchell Admin', 'company_id': [1, 'YourCompany']}]

上述示例调用 res.users 模型的 read 方法,带有两个位置参数——记录 ID 2(也可以使用 ID 列表)和要检索的字段列表 ["login", "name", "company_id"],以及没有关键字参数。

结果是一个字典列表,其中每个字典都是一个记录。多对多字段的值遵循特定的表示。它们是一对值,包含记录 ID 和记录显示名称。例如,之前返回的 company_id 值是 [1, 'YourCompany']

记录 ID 可能未知,在这种情况下,需要搜索调用以找到匹配域过滤器的记录 ID。

例如,如果我们想找到管理员用户,我们可以使用 [("login", "=", "admin")]。这个 RPC 调用如下所示:

>>> domain = [("login", "=", "admin")]
>>> api.execute_kw(db, uid, password, "res.users", "search", [domain])
[2]

结果是一个只有一个元素的列表,2,这是 admin 用户的 ID。

常见的操作是使用 searchread 方法的组合来查找符合域过滤器的记录 ID,然后检索它们的数据。对于客户端应用程序来说,这意味着对服务器进行两次往返。为了简化这个过程,search_read 方法可用,它可以在单步中执行这两个操作。

这里有一个使用 search_read 来查找管理员用户并返回其名称的示例:

>>> api.execute_kw(db, uid, password, "res.users", "search_read", [domain, ["login", "name"]])
[{'id': 2, 'login': 'admin', 'name': 'Mitchell Admin'}]

search_read方法使用了两个位置参数:一个包含域过滤器的列表,以及一个包含要检索的字段的第二个列表。

search_read的参数如下:

  • domain:一个包含域过滤表达式的列表

  • fields:一个包含要检索的字段名称的列表

  • offset:要跳过的记录数或用于记录分页的记录数

  • limit:要返回的最大记录数

  • order:用于数据库的ORDER BY子句的字符串

fields参数对于readsearch_read都是可选的。如果没有提供,将检索所有模型字段。但这可能会导致昂贵的函数字段计算和检索大量可能不需要的数据。因此,建议提供显式的字段列表。

execute_kw调用可以使用位置参数和关键字参数。以下是在使用关键字参数而不是位置参数时,相同的调用看起来是什么样子:

>>> api.execute_kw(db, uid, password, "res.users", "search_read", [], {"domain": domain, "fields": ["login", "name"]})

search_read是检索数据最常用的方法,但还有更多方法可用于写入数据或触发其他业务逻辑。

调用其他 API 方法

除了前缀为下划线的那些方法被认为是私有的之外,所有其他模型方法都通过 RPC 公开。这意味着createwriteunlink可以调用以在服务器上修改数据。

让我们看看一个例子。以下代码创建了一个新的合作伙伴记录,修改了它,读取以确认修改已被写入,并最终删除它:

>>> x = api.execute_kw(db, uid, password, "res.partner", "create", 
[{'name': 'Packt Pub'}])
>>> print(x)
49
>>> api.execute_kw(db, uid, password, "res.partner", "write", 
[[x], {'name': 'Packt Publishing'}]) 
True
>>> api.execute_kw(db, uid, password, "res.partner", "read", 
[[x], ["name"]])
[{'id': 49, 'name': 'Packt Publishing'}]
>>> api.execute_kw(db, uid, password, "res.partner", "unlink", [[x]])
True
>>> api.execute_kw(db, uid, password, "res.partner", "read", [[x]])
[]

XML-RPC 协议的一个限制是它不支持None值。有一个支持None值的 XML-RPC 扩展,但这是否可用将取决于客户端应用程序使用的特定 XML-RPC 库。不返回任何内容的方法可能无法通过 XML-RPC 使用,因为它们隐式返回None。这就是为什么方法始终返回某些内容,如True值是一个好习惯。另一个选择是使用 JSON-RPC。OdooRPC库支持此协议,它将在本章的“使用 OdooRPC 库”部分中使用。

前缀为下划线的Model方法被认为是私有的,并且不会通过 XML-RPC 公开。

小贴士

通常,客户端应用程序希望在一个 Odoo 表单上复制手动用户输入。调用create()方法可能不足以完成这项任务,因为表单可以使用onchange方法来自动化一些字段,这些方法是由表单的交互触发的,而不是由create()触发的。解决方案是在 Odoo 服务器上创建一个自定义方法,该方法使用create()然后运行所需的onchange方法。

值得重复的是,Odoo 外部 API 可以被大多数编程语言使用。官方文档提供了 Ruby、PHP 和 Java 的示例。这些信息可在www.odoo.com/documentation/15.0/webservices/odoo.html找到。

到目前为止,我们已经看到了如何使用 XML-RPC 协议调用 Odoo 方法。现在,我们可以使用这个来构建书籍目录客户端应用程序。

实现客户端应用程序 XML-RPC 接口

让我们先从实现图书馆书籍目录客户端应用程序开始。

这可以分成两个文件:一个包含服务器后端library_xmlrpc.py的 Odoo 后端接口,另一个是用户界面library.py。这将允许我们为后端接口使用替代实现。

从 Odoo 后端组件开始,将使用LibraryAPI类来设置与支持与 Odoo 交互所需方法的 Odoo 服务器的连接。要实现的方法如下:

  • search_read(<title>)用于通过标题搜索书籍数据

  • create(<title>)用于创建具有特定标题的书

  • write(<id>, <title>)用于使用书籍 ID 更新书籍标题

  • unlink(<id>)用于使用 ID 删除一本书

选择一个目录来存放应用程序文件,并创建library_xmlrpc.py文件。首先添加类构造函数,如下所示:

import xmlrpc.client
class LibraryAPI(): 
    def __init__(self, host, port, db, user, pwd):
        common = xmlrpc.client.ServerProxy(
            "http://%s:%d/xmlrpc/2/common" % (host, port))
        self.api = xmlrpc.client.ServerProxy(
            "http://%s:%d/xmlrpc/2/object" % (host, port))
        self.uid = common.authenticate(db, user, pwd, {})
        self.pwd = pwd
        self.db = db
        self.model = "library.book"

这个类存储了执行目标模型调用所需的所有信息:API XML-RPC 引用、uid、密码、数据库名和模型名。

对 Odoo 的 RPC 调用都将使用相同的execute_kw RPC 方法。在它周围添加了一个薄薄的包装器,在_execute()私有方法中。这利用了存储在对象中的数据,提供了一个更小的函数签名,如下面的代码块所示:

    def _execute(self, method, arg_list, kwarg_dict=None): 
        return self.api.execute_kw( 
            self.db, self.uid, self.pwd, self.model,
            method, arg_list, kwarg_dict or {})

这个_execute()私有方法现在可以用于更简洁的高层方法实现。

第一个公共方法是search_read()方法。它将接受一个可选的字符串,用于搜索书籍标题。如果没有提供标题,将返回所有记录。这是相应的实现:

    def search_read(self, title=None):
        domain = [("name", "ilike", title)] if title else 
                   [] 
        fields = ["id", "name"]
        return self._execute("search_read", [domain, 
          fields])

create()方法将创建一个具有给定标题的新书,并返回创建记录的 ID:

    def create(self, title):
        vals = {"name": title}
        return self._execute("create", [vals])

write()方法将接受新的标题和书籍 ID 作为参数,并在此书籍上执行写操作:

    def write(self, id, title): 
        vals = {"name": title} 
        return self._execute("write", [[id], vals])

最后,使用unlink()方法根据相应的 ID 删除一本书:

    def unlink(self, id): 
        return self._execute("unlink", [[id]])

我们在文件末尾添加一小段测试代码,如果运行 Python 文件,将执行这些代码,有助于测试已实现的方法,如下所示:

if __name__ == "__main__": 
    # Sample test configurations 
    host, port, db = "localhost", 8069, "library" 
    user, pwd = "admin", "admin"
    api = LibraryAPI(host, port, db, user, pwd) 
    from pprint import pprint 
    pprint(api.search_read())

如果我们运行这个 Python 脚本,我们应该看到我们的图书馆书籍内容被打印出来:

$ python3 library_xmlrpc.py
[{'id': 1, 'name': 'Odoo Development Essentials 11'},
 {'id': 2, 'name': 'Odoo 11 Development Cookbook'},
 {'id': 3, 'name': 'Brave New World'}]

现在我们已经围绕我们的 Odoo 后端有一个简单的包装器,让我们处理命令行用户界面。

实现客户端应用程序用户界面

我们的目标是学习如何编写外部应用程序和 Odoo 服务器之间的接口,我们已经在上一节中做到了这一点。但让我们更进一步,为这个简约客户端应用程序构建用户界面。

为了尽可能保持简单,我们将使用简单的命令行用户界面,并避免使用额外的依赖。这使我们能够利用 Python 的内置功能来实现命令行应用程序,以及ArgumentParser库。

现在,在library_xmlrpc.py文件旁边,创建一个新的library.py文件。这个文件将导入 Python 的命令行参数解析器和LibraryAPI类,如下面的代码所示:

from argparse import ArgumentParser
from library_xmlrpc import LibraryAPI

接下来,我们必须描述参数解析器期望的命令。有四个命令:

  • list用于搜索和列出书籍

  • add用于添加一本书

  • set用于更新书籍标题

  • del用于删除一本书

实现上述命令的命令行解析器代码如下:

parser = ArgumentParser()
parser.add_argument(
    "command",
    choices=["list", "add", "set", "del"])
parser.add_argument("params", nargs="*")  # optional args
args = parser.parse_args()

args对象代表用户给出的命令行选项。args.command是正在使用的命令,而args.params包含用于命令的附加参数,如果提供了任何参数。

如果没有给出或给出了错误的命令,参数解析器将处理这种情况,并将向用户显示预期的输入。argparse的完整参考可以在官方文档中找到,网址为docs.python.org/3/library/argparse.html

下一步是执行与user命令相对应的操作。我们将首先创建一个LibraryAPI实例。这需要 Odoo 连接细节,在这个简单的实现中,这些细节将被硬编码,如下所示:

host, port, db = "localhost", 8069, "library"
user, pwd = "admin", "admin"
api = LibraryAPI(host, port, db, user, pwd)

第一行设置了服务器实例和要连接的数据库的一些固定参数。在这种情况下,连接到的是本地 Odoo 服务器,监听默认的8069端口,连接到library数据库。要连接到不同的服务器和数据库,这些参数应相应地进行调整。

必须添加新的特定代码来处理每个命令。我们将从list命令开始,它返回书籍列表:

if args.command == "list":
    title = args.params[:1]
    books = api.search_read(title)
    for book in books:
        print("%(id)d %(name)s" % book)

在前面的代码中使用了LibraryAPI.search_read()方法来检索书籍记录的列表。然后迭代返回的列表以打印出每个元素。

接下来是add命令:

if args.command == "add":
    title = args.params[0]
    book_id = api.create(title)
    print("Book added with ID %d for title %s." % (
      book_id, title))

由于在LibraryAPI对象中已经完成了艰苦的工作,实现只需要调用create()方法并向最终用户显示结果。

set命令允许我们更改现有书籍的标题。它应该有两个参数——书籍的 ID 和新的标题:

if args.command == "set":
    if len(args.params) != 2:
        print("set command requires a Title and ID.")
    else:
        book_id, title = int(args.params[0]), 
          args.params[1]
        api.write(book_id, title)
        print("Title of Book ID %d set to %s." % (book_id, 
          title))

最后,是del命令的实现,用于删除书籍记录。这与之前的命令没有太大区别:

if args.command == "del":
    book_id = int(args.params[0])
    api.unlink(book_id)
    print("Book with ID %s was deleted." % book_id)

客户端应用程序已完成,您可以使用您选择的命令尝试它。特别是,我们应该能够运行本章开头所示的示例命令。

小贴士

在 Linux 系统上,可以通过运行 chmod +x library.py 命令并将 #!/usr/bin/env python3 添加到文件的第一行来使 library.py 可执行。之后,在命令行中运行 ./library.py 应该可以工作。

这是一个相当基础的应用程序,很容易想到几种改进它的方法。这里的目的是使用 Odoo RPC API 构建一个最小可行应用。

使用 OdooRPC 库

另一个需要考虑的相关客户端库是 OdooRPC。它是一个完整的客户端库,使用 JSON-RPC 协议而不是 XML-RPC。尽管 XML-RPC 仍然得到支持,但 Odoo 官方网页客户端也使用 JSON-RPC。

OdooRPC 库现在在 Odoo 社区协会的伞下维护。源代码仓库可以在 github.com/OCA/odoorpc 找到。

可以使用以下命令从 PyPI 安装 OdooRPC 库:

$ pip3 install odoorpc

当创建一个新的 odoorpc.ODOO 对象时,OdooRPC 库会设置一个服务器连接。在这个时候,我们应该使用 ODOO.login() 方法来创建一个用户会话。就像在服务器端一样,会话有一个 env 属性,包含会话的环境,包括用户 ID、uidcontext

OdooRPC 库可以用来为服务器提供 library_xmlrpc.py 接口的替代实现。它应该提供相同的功能,但使用 JSON-RPC 而不是 XML-RPC 来实现。

为了实现这一点,将创建一个名为 library_odoorpc.py 的 Python 模块,它为 library_xmlrpc.py 模块提供了一个即插即用的替代品。为此,创建一个名为 library_odoorpc.py 的新文件,并将其放在旁边,该文件包含以下代码:

import odoorpc
class LibraryAPI():
    def __init__(self, host, port, db, user, pwd):
        self.api = odoorpc.ODOO(host, port=port)
        self.api.login(db, user, pwd)
        self.uid = self.api.env.uid
        self.model = "library.book"
        self.Model = self.api.env[self.model]
    def _execute(self, method, arg_list, kwarg_dict=None):
        return self.api.execute(
            self.model,
            method, *arg_list, **kwarg_dict)

OdooRPC 库实现了 ModelRecordset 对象,它们模仿了服务器端对应对象的行为。目标是使使用此库的代码与 Odoo 服务器端使用的代码相似。客户端使用的方法利用这一点,并在 self.Model 属性中存储对 library.book 模型对象的引用,该属性由 OdooRPC 的 env["library.book"] 调用提供。

_execute() 方法也在这里实现;它允许我们将其与普通的 XML-RPC 版本进行比较。OdooRPC 库有一个 execute() 方法来运行任意的 Odoo 模型方法。

接下来是 search_read()create()write()unlink() 客户端方法的实现。在同一个文件中,将这些方法添加到 LibraryAPI() 类内部:

    def search_read(self, title=None):
        domain = [("name", "ilike", title)] if title else 
                  []
        fields = ["id", "name"]
        return self.Model.search_read(domain, fields)
    def create(self, title):
        vals = {"name": title}
        return self.Model.create(vals)
    def write(self, id, title):
        vals = {"name": title}
        self.Model.write(id, vals)
    def unlink(self, id):
        return self.Model.unlink(id)

注意这个客户端代码与 Odoo 服务器端代码的相似之处。

这个 LibraryAPI 对象可以用作 library_xmlrpc.py 的即插即用替代品。可以通过编辑 library.py 文件并将 from library_xmlrpc import LibraryAPI 行更改为 from library_odoorpc import LibraryAPI 来用作 RPC 连接层。现在,测试驱动 library.py 客户端应用程序;它应该表现得和以前一样!

摘要

本章的目标是了解外部 API 的工作原理及其功能。我们首先通过使用 Python XML-RPC 客户端编写简单脚本来探索它,尽管外部 API 可以从任何编程语言中使用。官方文档提供了 Java、PHP 和 Ruby 的代码示例。

然后,我们学习了如何使用 XML-RPC 调用来搜索和读取数据,以及如何调用任何其他方法。例如,我们可以创建、更新和删除记录。

接下来,我们介绍了 OdooRPC 库。它提供了一个在 RPC 基础库(XML-RPC 或 JSON-RPC)之上的层,以提供类似于服务器端可找到的 API 的本地 API。这降低了学习曲线,减少了编程错误,并使得在服务器端和客户端代码之间复制代码变得更加容易。

通过这些,我们已经完成了关于编程 API 和业务逻辑的章节。现在,是时候看看视图和用户界面了。在下一章中,我们将更详细地探讨后端视图以及网络客户端可以提供的开箱即用的用户体验。

进一步阅读

以下附加参考资料可能有助于补充本章中描述的主题:

第四部分:视图

接下来是视图层。我们将详细讨论使用模型和业务逻辑的图形用户界面(GUI)。Odoo 网络客户端提供了一套丰富的组件来设计 GUI,但同时也提供了一个灵活的网站开发框架。QWeb 模板在高级网络客户端视图、报告和网站页面上扮演着重要角色,在此进行介绍。

在本节中,包括以下章节:

  • 第十章**,后端视图 – 设计用户界面

  • 第十一章**,看板视图和客户端 QWeb

  • 第十二章**,使用服务器端 QWeb 创建可打印的 PDF 报告

  • 第十三章**,创建 Web 和门户前端功能

第八章:第十章:后端视图 – 设计用户界面

本章描述了如何创建视图以实现业务应用的用户界面。Odoo 用户界面从菜单项和菜单点击时执行的各种动作开始,因此这些是我们首先将学习的组件。

最常用的视图类型是表单视图,我们需要了解一些元素,从组织视图中的元素布局到理解字段和按钮的所有可用选项。

一些其他常用的视图包括列表视图和搜索视图。最后,还有其他一些针对特定目的有用的视图类型,例如数据透视表视图和图形视图。这些视图类型将在本章末尾进行概述。

本章将涵盖以下主题:

  • 添加菜单项

  • 理解窗口动作

  • 探索表单视图的结构

  • 使用字段

  • 使用按钮

  • 添加动态视图元素

  • 探索列表视图

  • 探索搜索视图

  • 理解其他可用的视图类型

到本章结束时,你应该熟悉所有 Odoo 视图类型,并拥有使用它们的资源。特别是,你将能够自信地设计非平凡的表单视图,并提供良好的用户体验。

技术要求

我们将继续使用library_checkout附加模块。它的模型层已经完成;现在,它需要用户界面的视图层。

本章中的代码基于我们在第八章中创建的代码,业务逻辑 – 支持业务流程。必要的代码可以在此书的 GitHub 仓库中找到,位于github.com/PacktPublishing/Odoo-15-Development-Essentialsch10目录下。

添加菜单项

菜单项是用户界面导航的起点。它们形成一个层次结构,其中顶级项代表应用程序,下一级是应用程序主菜单。可以添加更多子菜单级别。

没有子菜单的菜单项是可操作的,可以触发一个动作,告诉 Web 客户端要做什么,例如打开一个视图。

菜单项存储在ir.ui.menu模型中,可以通过设置 | 技术 | 用户界面 | 菜单项菜单进行浏览。

library_app附加模块为图书馆书籍创建了一个顶级菜单,而library_checkout附加模块添加了借阅和借阅阶段的菜单项。这两个模块都在library_checkout/views/library_menu.xml中实现。

这是借阅菜单项的 XML:

    <menuitem id="menu_library_checkout"
              name="Checkout"
              action="action_library_checkout"
              parent="library_app.library_menu"
    />

上一段代码使用了<menuitem>快捷元素,这是一种创建菜单记录的简写方式,比<record model="ir.ui.menu">元素更方便。

最常用的 <menuitem> 属性如下:

  • name 是菜单项的标题,并在用户界面中显示。

  • action 是点击菜单项时运行的动作的 XML ID。

  • parent 是父菜单项的 XML ID。在这种情况下,父菜单项是在另一个模块中创建的,因此需要使用完整的 XML ID 进行引用;即 <module>.<XML ID>

还有一些其他属性也是可用的:

  • sequence 设置一个数字以对菜单项的展示顺序进行排序;例如,sequence="10"

  • groups 是具有访问菜单项的安全组 XML ID 的逗号分隔列表;例如,groups="library_app.library_group_user,library_app.library_group_manager"

  • web_icon 是要使用的图标的路径。它仅适用于企业版的顶级菜单项。路径值应遵循 web_icon="library_app,static/description/icon.png" 格式。

菜单项可以运行由 action 属性标识的动作,在大多数情况下,这将是一个 窗口动作。下一节将解释如何创建动作以及它们能够做什么。

理解窗口动作

菜单上的 窗口动作 给予网络客户端执行指令,例如打开一个视图,并且可以在视图中的菜单项或按钮中使用。

窗口动作用于标识在用户界面中使用的模型和要展示的视图。它们还可以使用 domain 过滤器来过滤可用的记录,并可以使用 context 属性设置默认值和过滤器。

窗口动作存储在 ir.actions.act_window 模型中,可以通过转到 设置 | 技术 | 动作 | 窗口动作 菜单进行浏览。

library_checkout/views/library_menu.xml 文件包含用于结账菜单项的窗口动作定义:

    <record id="action_library_checkout" 
            model="ir.actions.act_window">
        <field name="name">Checkouts</field>
        <field name="res_model">library.checkout</field>
        <field name="view_mode">tree,form</field>
    </record>

窗口动作是一个 ir.actions.act_window 记录。最重要的字段如下:

  • name 是通过动作打开的视图上显示的标题。

  • res_model 是目标模型的标识符。

  • view_mode 是要提供的视图类型的逗号分隔列表。列表中的第一个是默认打开的。

其他相关的窗口动作字段如下:

  • target 默认为 current,在主内容区域内联打开视图。如果设置为 new,则将在弹出对话框窗口中打开视图;例如,target="new"

  • context 在目标视图中设置上下文信息,可以设置默认值或激活过滤器等;例如,<field name="context">{'default_user_id': uid}</field>

  • domain 是一个域表达式,它强制对在打开的视图中可浏览的记录进行过滤;例如,domain="[('user_id', '=', uid)]"

  • limit 是列表视图中每页的记录数;例如,limit="80"

  • view_id是对要使用的特定视图的引用。它不能与view_mode一起使用。它通常与target="new"一起使用,以弹出方式打开特定表单。

    Odoo 13 中的更改

    直到 Odoo 12,可以使用<act_window>快捷元素来创建窗口操作。这在 Odoo 13 中被删除了。现在,必须使用<record model="ir.actions.act_window">元素来创建窗口操作。

在本章中,我们将为library.checkout模型添加视图类型。通过这样做,我们将展示除了表单和树/列表视图之外的其他可用视图类型。

要提供的视图类型必须由窗口操作指示。因此,让我们编辑library_checkout/views/library_menu.xml文件以添加新的视图类型,如下面的代码所示:

    <record id="action_library_checkout" 
            model="ir.actions.act_window">
        <field name="name">Checkouts</field>
        <field name="res_model">library.checkout</field>
        <field name="view_mode"
            >tree,form,activity, calendar,graph,pivot</field>
    </record>

这些更改目前还不能进行。在将它们添加到窗口操作的view_mode之前,应该实现相应视图类型的定义。

除了菜单项或视图按钮之外,操作还可以在可用的操作上下文菜单中使用,该菜单位于搜索框附近。下一节将详细介绍这一点。

向操作上下文菜单添加选项

窗口操作也可以从位于表单视图顶部的操作菜单按钮使用,也可以在记录被选中时在列表视图中使用:

图 10.1 – 操作上下文菜单

图 10.1 – 操作上下文菜单

此菜单是上下文相关的,因为操作将应用于当前选定的记录或记录。

要在操作菜单中提供操作,必须在窗口操作上设置两个更多字段:

  • binding_model_id是对要使用该操作的模型的引用;例如,<field name="binding_model_id" ref="model_library_checkout" />

  • binding_view_types可用于限制选项的可见性,使其仅对特定视图类型可见,例如formlist;例如,<field name="binding_view_types">form,list</field>

library_checkout模块的wizard/checkout_mass_message_wizard_view.xml文件中已经实现了这个示例。这里复制出来供参考:

  <record id="action_checkout_message"
          model="ir.actions.act_window">
    <field name="name">Send Messages</field>
    <field name="res_model">
      library.checkout.massmessage</field>
    <field name="view_mode">form</field>
    <field name="binding_model_id" 
      ref="model_library_checkout" />
    <field name="binding_view_types">form,list</field>
    <field name="target">new</field>
  </record>

与绑定到操作菜单相关的设置在上一段代码中被突出显示。

以下截图说明了相应的操作菜单项:

图 10.2 – 发送消息操作菜单选项

图 10.2 – 发送消息操作菜单选项

Odoo 13 中的更改

在 Odoo 13 中,操作绑定字段发生了变化。直到 Odoo 12,src_model设置绑定并使用模型标识符,例如library.checkout。它可以在表单视图中使用,也可以通过将multi设置为true使其在列表视图中可用。

一旦触发窗口操作,就会打开相应的视图。最常用的视图类型是表单和列表。下一节将详细介绍如何创建表单视图。

探索表单视图结构

表单视图是用户与数据记录交互的主要方式。表单视图可以采用简单的布局或业务文档布局,类似于纸质文档。在本节中,我们将学习如何设计这些业务文档视图以及如何使用可用的元素和组件。

第八章业务逻辑 – 支持业务流程中,我们创建了一个图书馆借阅模型并为其准备了一个基本表单。我们将在本节中重新访问并增强它。

以下截图显示了完成后的表单视图将看起来像什么:

![Figure 10.3 – 增强的结账表单视图

![img/Figure_10.3_B16119.jpg]

图 10.3 – 增强的结账表单视图

在我们逐渐在本章中添加不同元素的同时,您可以参考此截图。

使用业务文档视图

从历史上看,组织使用纸质表单来支持其内部流程。业务应用模型支持这些纸质表单的数字版本,在这个过程中,它们可以添加自动化并使操作更高效。

为了获得更直观的用户界面,表单视图可以模仿这些纸质文档,帮助用户可视化他们在纸质表单上习惯运行的过程。

例如,对于图书馆应用,图书借阅可能是一个需要填写纸质表单的过程。让结账表单具有类似纸质文档的布局可能是一个好主意。

一个业务文档包含两个元素:一个<head>部分和一个<sheet>部分。head包含控制文档业务工作流的按钮和组件,而sheet包含实际的文档内容。在sheet部分之后,我们还可以有消息和活动组件。

要将此结构添加到结账表单中,首先编辑library_checkout/views/checkout_view.xml文件,并将表单视图记录更改为以下基本框架:

<record id="view_form_checkout" model="ir.ui.view"> 
  <field name="model">library.checkout</field> 
  <field name="arch" type="xml"> 
    <form> 
      <header>  
        <!-- To add buttons and status widget -->
      </header>
      <sheet> 
        <!-- To add form content -->
      </sheet> 
      <!-- Discuss widgets -->
      <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>
  </field> 
</record>

视图名称是可选的,如果缺失则自动生成。因此,为了简单起见,前一个视图记录中省略了<field name="name">元素。

<head><sheet>部分目前为空,将在下一部分中扩展。

底部的消息部分使用了由mail附加模块提供的组件,具体说明见第八章业务逻辑 – 支持业务流程

首先要检查的部分是表单标题。

添加标题部分

顶部的标题通常包含文档在其生命周期中将经过的步骤以及相关的操作按钮。这些操作按钮是常规的表单按钮,而用于前进的按钮通常会被突出显示,以帮助用户。

添加标题按钮

让我们从向当前空白的标题部分添加一个按钮开始。

在编辑表单视图中的<header>部分时,添加一个按钮将结账移动到完成状态:

<header> 
  <field name="state" invisible="True" />
  <button name="button_done"
    type="object"
    string="Return Books"
    attrs="{'invisible':
      [('state', 'in', ['new', 'done', 'cancel'])]}"
    class="oe_highlight"
  />
</header>

通过使用前面的代码,将Return Books按钮添加到标题中,并设置type="object",表示调用模型方法。name="button_done"声明了要调用的方法名称。

使用class="oe_highlight"来突出显示按钮。当我们有多个按钮可供选择时,可以突出显示主要或更常见的操作,以帮助用户。

使用attrs属性使按钮仅在有意义的状态下可见。它应在open状态下可见,因此应将newdonecancel状态设置为不可见。

用于执行此操作的条件使用state字段,否则在表单上不使用。为了使attrs条件工作,需要将state字段加载到 Web 客户端。为此,它被添加为一个不可见字段。

在这个特定情况下,使用了特殊的state字段名,并且可以使用更简单的states属性来实现使用attrs实现的可见性条件。states属性列出了元素将可见的状态。

通过使用states而不是attrs,按钮仅在open状态下可见,看起来如下:

  <button name="button_done" 
    type="object" 
    string="Return Books"
    states="open"
    class="oe_highlight"
  />

attrsstates元素可见性功能也可以用于其他视图元素,例如字段。我们将在本章后面更详细地探讨它们。

为了使此按钮工作,被调用的方法必须实现。为此,在library_checkout/models/library_checkout.py文件中,向checkout类添加以下代码:

    def button_done(self):
        Stage = self.env["library.checkout.stage"]
        done_stage = Stage.search([("state", "=", "done")], 
          limit=1)
        for checkout in self:
            checkout.stage_id = done_stage
        return True

首先,代码查找完成状态。它将用于将记录设置为该阶段。

self记录集通常是一个单独的记录,但 API 允许它对多记录记录集进行调用,因此应该处理这种可能性。这可以通过对self上的for循环来完成。然后,对于self记录集中的每个记录,必须将stage_id字段设置为完成阶段。

除了按钮外,标题还可以包含状态栏小部件,以展示可用的阶段状态

添加状态栏管道

标题中另一个有用的元素是流程图,展示流程步骤以及当前文档的位置。这可以基于阶段状态列表。此管道小部件可以是可点击的,也可以不是,以防我们只想通过按钮进行更改。

使用statusbar小部件通过<field>元素添加状态栏小部件。结账模型有stage_id字段,我们将使用它:

<header>  
  <field name="state" invisible="True" />
  <button name="do_clear_done" type="object" 
    string="Clear Done"
    states="open,cancel"
    class="oe_highlight" />
  <field name="stage_id"
    widget="statusbar"
options="{'clickable': True, 'fold_field': 'fold'}" />
</header>

statusbar小部件可以使用状态选择字段或阶段多对一字段。这两种字段可以在多个 Odoo 核心模块中找到。

clickable 选项允许用户通过点击状态栏来更改文档阶段。启用它为用户提供灵活性。但也有一些情况下,我们需要对工作流程有更多的控制,并要求用户仅通过可用的操作按钮来通过阶段。

Odoo 12 的变化

直到 Odoo 11,可点击的选项是一个字段属性,<field widget="statusbar" clickable="True" />。在 Odoo 12 中,它被转换为一个小部件选项,<field widget="statusbar" options="{'clickable': True}" />

fold_field 选项用于允许不太重要的阶段,如 已取消,在设置 fold_field 时被隐藏(折叠),该字段名用于此。在这种情况下,它被命名为 fold

使用状态而不是阶段

阶段是一个多对一的字段,它使用支持模型来设置流程的步骤。它是灵活的,可以被最终用户配置以适应他们特定的业务流程,并且非常适合支持看板。图书馆借阅模型正在使用它。

状态是一个包含固定流程步骤的封闭选择列表,例如 新建进行中完成。由于可用的状态不能更改,因此可以在业务逻辑中使用它。但它不能由最终用户配置。

每种方法都有其优缺点。通过使用阶段并将每个阶段映射到状态,可以从中受益于两种选项的最佳之处。借阅模型实现了这一点,在借阅阶段模型中添加了一个状态字段,该字段也通过相关字段直接在借阅模型中可用。

如果一个模型只使用状态,则可以使用带有 statusbar 小部件的状态栏管道。然而,fold_field 选项不可用;相反,可以使用 statusbar_visible 属性,列出要显示的状态。

使用带有状态 field 的状态栏看起来是这样的:

<field name="state"
  widget="statusbar"
  options="{'clickable': True}"
  statusbar_visible="draft,open,done"
/>

注意,之前的代码在 library_checkout 模块中没有使用。由于它支持更灵活的阶段,我们更愿意在用户界面中使用它们。

现在我们已经完成了标题部分,让我们来看看主要表单部分。

设计文档表格

表格画布是表单的主要区域,实际数据元素放置于此。它被设计得像实际的纸质文档。

通常,一个文档表格结构将包含以下区域:

  • 顶部的一个文档标题

  • 顶部右角的一个按钮框

  • 文档标题数据字段

  • 底部的一个笔记本,用于可以组织成标签或页面的附加字段

文档通常会包含详细的代码行。这些通常在笔记本的第一页上展示。

这里是预期的 XML 结构:

      <sheet>
        <!-- Button box -->
        <div class="oe_button_box" name="button_box" />
        <!-- Header title -->
        <div class="oe_title" />
        <!-- Header fields -->
        <group />
        <!-- Notebook -->
        <notebook />
      </sheet>

在表格之后,我们通常会有聊天小部件,其中包含文档关注者、讨论消息和计划活动。

让我们逐一讨论这些区域。按钮框将在稍后讨论,所以接下来,我们将讨论标题头。

添加标题头

标题头通常会用大号字母显示文档的标题。它可能后面跟着一个副标题,也可能旁边有一个图像。

首先,需要向结账模型添加几个字段。需要一个字段用作标题,还需要一个图像来表示借阅者。编辑 library_checkout/models/library_checkout.py 文件并添加以下代码:

    name = fields.Char(string="Title")
    member_image = fields.Binary(related=
      "member_id.image_128")

标题头位于 <div class="oe_title"> 元素内。可以使用常规 HTML 元素,如 divspanh1h3

在下面的代码中,<sheet> 元素已经扩展以包含标题,以及一些作为副标题的附加字段:

<sheet> 
  <div name="button_box" class="oe_button_box" />
  <field name="member_image" widget="image" 
    class="oe_avatar" />
  <div class="oe_title"> 
    <label for="name" class="oe_edit_only"/> 
    <h1><field name="name"/></h1> 
    <h3> 
      <span class="oe_read_only">By </span> 
      <label for="member_id" class="oe_edit_only"/> 
      <field name="member_id" class="oe_inline" /> 
    </h3> 
  </div> 
  <!-- More elements will be added from here... --> 
</sheet>

前面的 XML 渲染包括以下内容:

  • 一个按钮框 <div> 元素。现在它是空的,但可以用来添加智能按钮。

  • 一个图像字段,用于 member_image,使用类似头像的图像小部件。

  • 包含文档标题元素的 <div> 元素。在标题内,有如下内容:

    • name 字段的 <label>,仅在编辑模式下可见。

    • name 字段,它被渲染为 HTML <h1> 标题。

    • 一个包含 member_id 字段的 <h3> 副标题头。这仅在阅读模式下可见。<field> 标签使用 oe_inline 让 HTML 元素管理文本流。

<group> 元素之外的字段不会为其渲染标签。前面的 XML 没有包含 <group> 元素,因此需要显式添加标签。

标题元素之后,通常会有标题字段,组织成组。

使用组组织表单内容

表单的主要内容应该使用 <group> 标签组织。

<group> 标签在画布中插入两列。添加到组内的字段使用这些两列——第一列用于字段标签,第二列用于字段值小部件。向组中添加更多字段将按垂直堆叠,新字段添加在新的一行中。

一个常见的模式是拥有两列字段,并排排列。你可以通过在顶层组中添加两个嵌套的 <group> 标签来实现这一点。

继续我们的表单视图,我们将使用这个来添加主要内容,在标题的 <div> 部分之后:

<!-- More elements will be added from here... --> 
<group name="group_top"> 
  <group name="group_col1"> 
    <field name="request_date" /> 
  </group> 
  <group name="group_col2"> 
    <field name="close_date" /> 
    <field name="user_id" /> 
  </group> 
</group>

顶部的 <group> 元素在画布中创建两列。每个嵌套的 <group> 元素使用这些列中的一列。第一个嵌套组使用左列,而第二个组使用右列。

<group> 元素被分配了一个 name。这不是必需的,但建议这样做,以便模块更容易扩展。

<group> 元素也可以有一个 string 属性,它用于显示其标题文本。

Odoo 11 的变化

string 属性不能再用作继承的锚点。这是因为相应的文本可以被翻译,这可能会破坏继承/扩展视图。应使用 name 属性代替。

可以使用以下元素来调整视图布局:

  • 可以使用<newline>元素来强制换行,以便下一个元素在下一行的第一列中渲染。

  • 可以添加<separator>元素来添加部分标题。可以使用string属性设置标题文本。

colcolspan属性提供了对网格布局的额外控制:

  • col属性用于<group>元素上,以自定义它包含的列数。默认情况下,一个<group>元素包含两列,但可以更改为任何其他数字。偶数更合适,因为默认情况下,添加的每个字段都占用两列——一列用于标签,一列用于值。

  • 可以在组内元素上使用colspan属性来设置它们应该占据的特定列数。默认情况下,一个字段占用两列。

以下代码显示了顶部分组元素的另一种版本,并使用col="4"来在两列中展示四个字段:

<group name="group_top" col="4"> 
  <field name="request_date" />
  <field name="user_id" /> 
  <span colspan="2" />
  <field name="close_date" /> 
</group>

注意到字段的顺序不同,因为字段是从左到右,然后从上到下放置的。使用了<span colspan="2">元素来占据第二行的前两列,以便close_date字段占据最后两列。

一些表单还包含笔记本部分,用于在不同页面上组织额外的字段。

添加标签页笔记本

笔记本元素是组织表单内容的另一种方式。它是一个包含多个标签页的容器。这些可以用来将不常用的数据隐藏起来,直到需要时才显示,或者按主题组织大量字段。

结账表单将包含一个笔记本元素,第一页将包含借阅书籍的列表。为此,在上一节中添加的<group name="group_top">元素之后,包含以下 XML:

<notebook> 
  <page name="page_lines" string="Borrowed Books">
    <field name="line_ids" />
  </page>
</notebook>

这个笔记本只包含一个页面。要添加更多,只需在<notebook>元素内包含更多的<page>元素。默认情况下,页面画布不会渲染字段标签。为了实现这一点,字段应该放在一个<group>部分内,就像表单主画布一样。

在这种情况下,在页面内部添加了一个多对一line_ids字段,没有<group>元素,因此不会为其渲染标签。

page元素支持以下属性:

  • string,用于页面标题。这是必需的。

  • attrs是一个字典,用于将invisiblerequired属性值映射到域表达式的结果。

  • accesskey,一个 HTML 访问键。

本节讨论了表单视图的典型布局,以及使用此视图时最重要的元素。最重要的元素是数据字段。下一节将详细讨论它们。

使用字段

在表单或列表视图中,字段小部件是展示和编辑模型字段数据的方式。

视图字段有一些可用的属性。大多数这些属性值默认来自模型定义,但在视图中可以覆盖这些值。

这里是常见字段属性的快速参考:

  • name 是模型中的字段名称,并标识了由该元素渲染的字段。

  • string 是要使用的标签文本。它覆盖了模型定义。

  • help 提供了一些当鼠标悬停在字段上时显示的工具提示帮助文本。

  • placeholder 提供了在字段内显示的建议文本。

  • widget 设置用于渲染字段的特定小部件。可用的小部件将在本节后面讨论。

  • options 是一个 JSON 数据结构,用于向小部件传递额外的选项。要使用的值取决于所使用的小部件。

  • Class 是用于字段 HTML 渲染过程的 CSS 类的逗号分隔列表。

  • nolabel="True" 阻止自动字段标签的展示。这对于 <group> 元素内的字段是有意义的,并且经常与 <label for="..."> 元素一起使用。

  • invisible="True" 使得字段不可见,但它的数据是从服务器获取的,并在表单上可用。请注意,表单不能在不可见字段上写入。

  • readonly="True" 使得表单上的字段为只读。

  • required="True" 使得字段在表单上为必填项。

以下特殊属性仅由特定字段类型支持:

  • password="True" 用于文本字段。它显示为密码字段,会隐藏输入的字符。

  • filename 用于二进制字段,是用于上传文件名称的模型字段名称。

有两个话题值得进一步讨论。一个是如何更好地控制字段标签的展示,另一个是关于使用不同的网页客户端小部件以获得更好的用户体验。

修改字段标签

字段不会自动渲染标签,除非它们在 <group> 元素内。在这种情况下,标签将使用 nolabel="True" 显式抑制。

可以使用 <label for="..."/> 元素显式添加标签。这使您能够更好地控制字段标签的显示位置。以下代码用于表单标题:

<label for="name" class="oe_edit_only" />

for 属性标识了我们应该从其中获取标签文本的字段。可选的 string 属性可以设置标签的特定文本。也可以使用 CSS 类。之前的代码使用了以下内容:

  • class="oe_edit_only" 使得元素仅在编辑模式下可见。

  • class="oe_read_only" 使得元素仅在只读模式下可见。

这可以用来控制字段标签的展示方式。字段数据的展示也可以通过不同的小部件进行调整。

选择字段小部件

字段内容是通过网页客户端小部件展示的。这可能会影响数据向用户展示的方式,以及设置字段值时的交互。

每种字段类型都使用适当默认小部件显示。然而,可能还有其他可选小部件。

文本字段小部件

对于文本字段,可以使用以下小部件:

  • email 渲染为可操作的 mailto HTML 链接。

  • phone 渲染为可操作的电话 HTML 链接。

  • url 用于将文本格式化为可点击的 URL。

  • html 用于将文本渲染为 HTML 内容。在编辑模式下,它具有所见即所得的编辑器,允许您格式化内容而无需使用 HTML 语法。

数值字段小部件

对于数值字段,以下小部件可用:

  • handle 专门为列表视图中的序列字段设计,显示一个可拖动的手柄以重新组织行顺序。

  • float_timefloat 字段格式化为小时和分钟。

  • monetaryfloat 字段显示为货币金额。它期望使用的货币在 currency_id 伴随字段中。如果 currency 字段有不同的名称,可以通过 options="{'currency_field': '<field name>'}" 设置。

  • progressbarfloat 表示为百分比进度条,这对于表示完成率的字段很有用。

  • percentagepercentpie 是可以与浮点字段一起使用的其他小部件。

关联和选择字段小部件

对于关联和选择字段,以下小部件可用:

  • many2many_tags 将值显示为类似按钮的标签列表。

  • many2many_checkboxes 将可选择的值显示为复选框列表。

  • selection 使用 selection 字段小部件处理多对一字段。

  • radio 使用单选按钮显示 selection 字段选项。

  • priorityselection 字段表示为可点击的星级列表。选择选项通常是数字。

  • state_selection 显示交通灯按钮,通常用于看板状态选择列表。normal 状态为灰色,done 为绿色,任何其他状态均以红色表示。

    Odoo 11 的变化

    state_selection 小部件在 Odoo 11 中引入,并取代了之前的 kanban_state_selection,后者已被弃用。

二进制字段小部件

对于二进制字段,以下小部件可用:

  • image 将二进制数据呈现为图像。

  • pdf_viewer 以 PDF 预览小部件的形式呈现二进制数据(自 Odoo 12 引入)。

关联字段

关联字段小部件允许您搜索并选择相关记录。

它还允许您打开相关记录的表单或导航到相应的表单,并即时创建新记录,也称为 快速创建

可以使用 options 字段属性禁用这些功能:

options="{'no_open': True, 'no_create': True}"

contextdomain 字段属性在关联字段中特别有用:

  • context 可以设置从字段创建的相关记录的默认值。

  • domain 限制可选择的记录。一个常见示例是字段的选项取决于表中的另一个字段的值。

多对多字段也可以使用 mode 属性来设置用于显示记录的视图类型。默认使用 tree 视图,但其他选项包括 formkanbangraph。它可以是逗号分隔的视图模式列表。

关联字段可以包含内联特定视图定义以使用。这些定义在 <field> 元素内部作为嵌套视图定义。例如,line_ids 检查点可以定义特定列表和表单视图:

<notebook> 
  <page name="page_lines" string="Borrowed Books" > 
    <field name="line_ids"> 
      <tree>
        <field name="book_id" />
      </tree>
      <form>
        <field name="book_id" />
      </form>
    </field>
  </page> 
</notebook>

行列表将使用内联 <tree> 定义。当您点击一行时,将出现一个表单对话框并使用内联 <form> 定义的结构。

我们已经看到了可以使用字段完成的所有事情。下一个最重要的视图元素是按钮,用于运行操作。

使用按钮

按钮允许用户触发操作,例如打开另一个视图或在服务器函数中运行业务逻辑。它们在讨论表头时被引入,但也可以在表单和列表视图的任何位置添加。

按钮支持以下属性:

  • string 是按钮文本标签,或当使用图标时 HTML 的 alt 文本。

  • type 是要执行的操作类型。可能的值包括 object,用于调用 Python 方法,或 action,用于运行窗口操作。

  • name 根据所选类型标识要执行的具体操作:要么是模型方法名称,要么是要运行的窗口操作的数据库 ID。可以使用 %(<xmlid>)d 公式在视图加载时将 XML ID 转换为必要的数据库 ID。

  • argstype="object" 时使用,用于将额外参数传递给方法调用。

  • context 设置上下文的值。这可以在调用的方法中使用或影响由窗口操作打开的视图。

  • confirm 是当按钮被点击时确认消息框的文本。这将在执行操作之前显示。

  • special="cancel" 在向导表单中使用,用于添加 取消 按钮,用于关闭表单而不执行任何操作。

  • icon 是要在按钮中显示的图标图像。可用的图标来自 Font Awesome 集合,应使用相应的 CSS 类指定,例如 icon="fa-question"。有关图标参考,请查看 fontawesome.com/

    Odoo 11 的变化

    在 Odoo 11 之前,按钮图标是从 GTK 客户端库中起源的图像,并且限于 addons/web/static/src/img/icons 中可用的那些。

    工作流引擎在 Odoo 11 中已被弃用并移除。在之前支持工作流的版本中,按钮可以使用 type="workflow" 触发工作流引擎信号。在这种情况下,name 属性应包含工作流信号名称。

在某些表单的右上角区域找到的一种特定类型的按钮称为 智能按钮。让我们更仔细地看看它。

使用智能按钮

文档表单在右上角区域有一个智能按钮区域是很常见的。智能按钮显示为带有统计指示器的矩形,点击时可以追踪。

Odoo UI 模式通常有一个用于智能按钮的不可见框。这个按钮框通常是 <sheet> 中的第一个元素,看起来像这样:

<div name="button_box" class="oe_button_box">
 <!-- Smart buttons will go here... -->
</div>

按钮的容器只是一个具有 oe_button_box 类的 div 元素。在 Odoo 11.0 之前的版本中,可能还需要 oe_right 类来确保按钮框保持与表单右侧对齐。

对于图书馆借阅模块,将为该图书馆成员正在进行的未完成借阅添加一个智能按钮。按钮应显示这些借阅的统计信息,并且点击时应该打开一个包含这些项目的借阅列表。

对于按钮统计,需要在 library.checkout 模型中创建一个计算字段,在 library_checkout/models/library_checkout.py 文件中:

    count_checkouts = fields.Integer(
        compute="_compute_count_checkouts")
    def _compute_count_checkouts(self):
        for checkout in self:
            domain = [
                ("member_id", "=", checkout.member_id.id),
                ("state", "not in", ["done", "cancel"]),
            ]
            checkout.count_checkouts = 
              self.search_count(domain)

前面的计算会遍历每个借阅记录来计算并运行该成员的搜索查询,统计未完成借阅的数量。

小贴士

前面的实现违反了一个性能原则:不要在循环中执行记录搜索操作。

为了性能优化,应该在循环之前批量执行搜索操作,并将结果用于循环内部。下面是一个这种实现示例。这涉及到一些非平凡的代码,所以如果你觉得现在理解起来太难,可以自由跳过。

read_group() 方法可以用来获取分组后的数据。它返回一个包含 dict 类型行的列表,例如 [{'member_id_count': 1, 'member_id': (1, 'John Doe'), …), …]。在这个数据结构中查找 member_id 是比较困难的。如果将行列表转换成一个将 member_id 映射到记录计数的字典,这个查找操作就可以变得非常简单。

这里是使用这些技术的替代实现:

    def _compute_count_checkouts(self):
        members = self.mapped("member_id")
        domain = [
            ("member_id", "in", members.ids),
            ("state", "not in ", ["done", "cancel"]),
        ]
        raw = self.read_group(domain, ["id:count"], 
          ["member_id"])
        data = {
            x["member_id"][0]: x["member_id_count"] for 
              x in raw
        }
        for checkout in self:
            checkout.count_checkouts = data.get(
                checkout.member_id.id, 0)

现在有了计算显示数量的字段,智能按钮可以添加到视图中。在 <sheet> 部分的顶部,用以下代码替换我们之前添加的按钮框占位符:

<div name="button_box" class="oe_button_box">
  <button type="action"
    name="%(action_library_checkout)d"
    class="oe_stat_button"
    icon="fa-book"
    domain="[('member_id', '=', member_id)]"
    context="{'default_member_id': member_id}"
  >
    <field name="count_checkouts"
      string="Checkouts" 
      widget="statinfo" />
  </button>
</div>

button 元素本身是一个容器,用于显示统计信息的字段应该添加到其中。这些统计信息是使用特定 statinfo 小部件的常规字段。未完成借阅的数量通过 count_checkouts 字段在按钮定义中展示。

智能按钮必须具有 class="oe_stat_button" CSS 样式,并且应该使用 icon 属性设置图标集。

在这种情况下,它包含 type="action",这意味着按钮运行一个 窗口操作,如 name 属性所标识。%(action_library_checkout)d 表达式返回要运行的操作的数据库 ID。此 窗口操作 打开结账列表。为了确保只显示相关的记录,使用了 domain 属性。如果在那个视图中创建了一个新记录,将当前成员设置为默认值将非常方便。这可以通过在 context 属性中使用 default_member_id 键来完成。

作为参考,以下是可用于智能按钮的属性:

  • class="oe_stat_button" 将渲染一个矩形而不是常规按钮。

  • icon 设置要使用的图标,从 Font Awesome 集合中选择。访问 fontawesome.com 浏览可用的图标。

  • typename 分别是按钮类型和要触发的操作的名称。对于智能按钮,类型通常为 action 用于 窗口操作,而 name 将是执行操作的 ID。可以使用 "%(action-xmlid)d" 将 XML ID 转换为所需的数据库 ID。

  • string 向按钮添加标签文本。在先前的代码示例中没有使用它,因为字段提供了文本标签。

  • context 可以用于在目标视图中设置默认值,对于从按钮导航的视图中创建的新记录。

  • help 添加了一个当鼠标指针悬停在按钮上时显示的帮助提示。

除了按钮和智能按钮之外,还可以将动态元素添加到视图中,以更改元素的值或可见性。这将在下一节中讨论。

添加动态视图元素

视图元素可以根据字段值动态更改其外观或行为。字段值可以通过 onchange 机制动态设置为其他表单字段上的域筛选器的值。这些功能将在下一节中讨论。

使用 onchange 事件

onchange 机制允许我们在用户在未保存的表单上修改数据时触发服务器逻辑。例如,当设置产品字段时,同一表单上的单价可以自动设置。

在较旧的 Odoo 版本中,onchange 事件在视图级别定义,但自 Odoo 8 以来,它们直接在模型层声明,无需任何特定的视图标记。这可以通过使用 @api.onchange('field1', 'field2', ...) 装饰器的方法来完成。它将 onchange 逻辑绑定到声明的字段。在第八章第八章中详细讨论了 onchange 模型方法,业务逻辑 - 支持业务流程,并在那里讨论了一个示例。

onchange 机制还会自动重新计算计算字段,立即响应用户输入。继续使用之前的例子,如果价格字段发生变化,包含总金额的计算字段也会自动使用新的价格信息进行更新。

使用动态属性

视图元素可以有一些属性对字段值的变化做出反应;例如,变为可见或必填。

以下属性可以用来控制视图元素的可见性:

  • groups根据当前用户所属的安全组使元素可见。只有指定组的成员才能看到它。它期望一个以逗号分隔的组 XML ID 列表。

  • states根据记录的state字段使元素可见。它期望一个以逗号分隔的状态值列表。当然,模型必须有一个state选择字段。

  • attrs可以根据某些条件设置不可见和必填属性。它使用一个字典,其中invisiblereadonlyrequired是可能的键。这些键映射到评估为真或假的域表达式。

这里是使用attrs的一个示例。为了使closed_date字段仅在done状态下可见,可以使用以下代码:

<field name="closed_date"
       attrs="{'invisible':[('state', 'not in', 
         ['done'])]}"
/>

invisible属性在任何元素中都可用,而不仅仅是字段。例如,它也可以用于笔记本页面和group元素。

readonlyrequired属性仅适用于数据字段,允许我们实现基本的客户端逻辑,例如,在依赖于其他记录值(如状态)的情况下使字段成为必填。

这结束了我们对表单视图的讨论。然而,还有几种视图类型需要探索。接下来,我们将讨论列表/树视图。

探索列表视图

列表视图可能是最常用的视图类型,紧随其后的是表单视图。列表视图将记录呈现为行,数据字段呈现为列。默认情况下,它们是只读的,但也可以设置为可编辑。

列表视图的基本定义很简单。它是一个包含在<tree>元素内的字段元素的序列。library_checkout已经在views/checkout_view.xml文件中包含了一个简单的列表视图,看起来是这样的:

  <record id="view_tree_checkout" model="ir.ui.view">
    <field name="name">Checkout Tree</field>
    <field name="model">library.checkout</field>
    <field name="arch" type="xml">
      <tree>
        <field name="request_date" />
        <field name="member_id" />
      </tree>
    </field>
  </record>

列表视图可以包含字段和按钮,为表单描述的属性在列表视图中同样有效。

在了解基础知识之后,可以在列表视图中使用一些附加功能。在下一节中,我们将介绍新的列表标题部分。

添加列表视图标题部分

与表单视图类似,列表视图也可以有标题部分,其中可以添加按钮以在模型上执行操作。语法与视图相同。

例如,在操作菜单中有一个发送消息选项可用。这并不直接对用户可见,并且可以将其作为一个标题按钮使其更加可见。

编辑树视图以添加此按钮看起来是这样的:

      <tree>
        <header>
          <button type="action"
            name="%(action_checkout_messag)d"
            string="Send Messages"
          />
        <header>
        <field name="request_date" />
        <field name="member_id" />
      </tree>

按钮操作与操作菜单选项类似。按钮仅在选中列表记录时可见。

新增于 Odoo 14

列表视图中的<header>元素是在 Odoo 14 中引入的。此功能在之前的版本中不可用。

在列表视图的内容方面,行可以使用不同的颜色来突出显示特定的条件,例如用红色突出显示延迟的活动。下一节将解释如何使用此类装饰。

使用行装饰

以下列表的扩展版本添加了一些额外的字段以及一些装饰属性到<tree>根元素:

      <tree
        decoration-muted="state in ['done', 'cancel']"
        decoration-bf="state=='open'"
      >
        <header>
          <button type="action"
            name="%(action_checkout_messag)d"
            string="Send Messages"
          />
        <header>
        <field name="state" invisible="True" />
        <field name="name" />
        <field name="request_date" />
        <field name="member_id" />
        <field name="stage_id" />
      </tree>

树元素通过使用state字段的表达式来使用两个装饰属性。decoration-muted使用灰色线条来显示完成或取消状态。decoration-bf使用粗体线条突出显示打开状态。

在这些表达式中使用的字段必须在视图的<field>元素中声明,以确保从服务器检索到必要的数据。如果不需要显示,可以设置其invisible="1"属性。

行的文本颜色和字体可以根据 Python 表达式的评估而改变。这可以通过设置用于评估的表达式的decoration–NAME属性来完成。可用的属性如下:

  • decoration-bf将字体设置为粗体。

  • decoration-it将字体设置为斜体。

  • decoration-muted将文本颜色设置为灰色。

  • decoration-primary将文本颜色设置为深蓝色。

  • decoration-success将文本颜色设置为浅蓝色。

  • decoration-warning将文本颜色设置为黄色。

  • decoration-danger将文本颜色设置为红色。

上述装饰名称基于 Bootstrap 库。有关更多详细信息,请参阅getbootstrap.com/docs/3.3/css/#helper-classes

除了装饰属性之外,还有一些其他属性可用于控制列表视图的行为。

其他列表视图属性

树元素的一些其他相关属性如下:

  • default_order用于设置行的特定排序顺序。其值是一个以逗号分隔的字段名列表,与 SQL 的ORDER BY子句兼容。

  • createdeleteedit,如果设置为false(小写),将禁用在列表视图上的相应操作。

  • editable使记录可以直接在列表视图中编辑。可能的值包括topbottom;即新记录将被添加的位置。

这些属性允许您控制默认行顺序以及记录是否可以直接在视图中编辑。

另一个相关功能是能够计算列表视图列的总计和子总计,如下一节所示。

添加列总计

列表视图也支持数值字段的列总计。可以使用可用的聚合属性之一显示汇总值——sumavgminmax

应该使用标签文本设置用于摘要值的聚合属性。

例如,让我们考虑借出模型已添加一个表示借书数量的字段,num_books。为了在列表视图中查看相应的总金额,应添加以下字段元素:

    <field name="num_books" sum="Num. Books" />

num_books 字段计算每个借出检查的借书数量。它是一个计算字段,我们需要将其添加到模型中:

    num_books = fields.Integer(compute=
      "_compute_num_books")
    @api.depends("line_ids")
    def _compute_num_books(self):
        for book in self:
            book.num_books = len(book.line_ids)

累计小计仅适用于存储字段。因此,在之前的示例中,如果对图书馆应用程序用户来说累计小计是一个重要功能,则需要添加 store=True

在表单和列表视图之后,下一个最重要的 UI 元素是搜索视图,它允许我们执行默认搜索并根据筛选器进行分组。

探索搜索视图

在视图的右上角有一个搜索框,下面有一些按钮,包括 筛选分组。当你输入搜索框时,你会看到有关要搜索的字段的建议。

提出的搜索选项是在 搜索视图 中配置的。当前搜索视图可以通过开发者菜单并选择 编辑 ControlPanelView 选项来检查。

搜索视图是通过 <search> 视图类型定义的。它可以提供以下类型的元素:

  • <field>元素用于在搜索框中输入时添加筛选选项。

  • <filter> 元素用于在 筛选分组 按钮下添加预定义的筛选器。

  • 一个 <searchpanel> 元素,用于在用户界面的左侧包含一个导航树。

    Odoo 13 的变化

    列表和看板视图的 <searchpanel> 小部件是在 Odoo 13 中引入的,在早期版本中不可用。

要将这些搜索选项添加到 library_checkout 模块,编辑 views/checkout_view.xml 文件并添加以下记录:

<record id="view_filter_checkout" model="ir.ui.view"> 
  <field name="model">library.checkout</field> 
  <field name="arch" type="xml"> 
    <search>
      <!-- Add content here -->
      <field name="name" />
    </search>
  </field>
</record>

现在,让我们逐一介绍可以添加的元素类型。接下来将解释 <field> 元素。

理解 <field> 元素

当在搜索框中输入时,用户将看到建议,使他们可以将此搜索应用于特定字段。这些选项是通过 <field> 元素定义的。

例如,在 <search> 元素内添加以下 XML 将会提出在附加字段中搜索文本:

      <field name="name"/>
      <field name="member_id"/>
      <field name="user_id"/>

此代码为 titlememberuser 字段添加搜索结果建议。

搜索 <field> 元素可以使用以下属性:

  • name 是要搜索的字段名称。

  • string 是要使用的文本标签。

  • operator 可以用作与默认值不同的比较运算符;即,对于数值字段是 =,对于其他字段类型是 ilike

  • filter_domain 设置用于搜索的特定域表达式,提供对运算符属性的灵活替代方案。搜索的文本字符串在表达式中称为 self。一个简单的例子是 filter_domain="[('name', 'ilike', self)]"

  • groups 使在字段上的搜索仅对属于某些安全组的用户可用。它期望一个以逗号分隔的 XML IDs 列表。

这些过滤器可以独立激活,并将通过 OR 逻辑运算符连接。用 <separator/> 元素分隔的过滤块将通过 AND 逻辑运算符连接。

本节提供了关于如何使用 <field> 元素的良好总结。现在,让我们了解可用的 <filter> 元素。

理解 <filter> 元素

点击搜索框下的 过滤按组 按钮,可以获取预定义选项。用户可以点击这些选项来应用他们的过滤条件。

提示

过滤元素也可以通过窗口动作使用,通过添加 search_default_<filter name>: True 键到上下文中来激活它们。

可以通过 <filter> 元素添加过滤选项,并通过域过滤器设置特定搜索条件,通过域属性设置。以下是一个示例:

      <filter name="filter_not_done"
              string="To Return"
              domain="[('state','=','open')]"/>
      <filter name="filter_my_checkouts"
              string="My Checkouts"
              domain="[('user_id','=',uid)]"/>

这添加了两个可选的过滤器。它们将在 open 状态下可供选择。第二个过滤器根据当前用户是负责的图书管理员来过滤借阅,通过当前用户过滤 user_id。这可以通过上下文 uid 键获得。

过滤元素还用于向 按组 按钮添加选项。以下是一个示例:

      <filter name="group_user"
              string="By Member"
              context="{'group_by': 'member_id'}"/>

此过滤器设置一个以字段名称为键的 group by 上下文键,以进行分组。在这种情况下,它将按 member_id 进行分组。

对于 <filter> 元素,以下属性可用:

  • name 是一个标识符,用于后续的继承/扩展或使用窗口动作上下文键启用。这不是强制性的,但始终提供它是良好的实践。

  • string 是要显示的过滤器的标签文本。这是强制性的。

  • domain 是要添加到当前域的域表达式。

  • context 是要添加到当前上下文中的上下文字典。它通常用于设置 group_by 键,以指定要按字段名称分组的字段。

  • groups 使此元素字段仅对一组安全组(XML IDs)可用。

将前面的代码添加到 library_checkout 模块后,模块将被升级。这些过滤器和按组选项将在搜索框附近的按钮中可用。

另一个可用的搜索视图元素是搜索面板。我们将在下一节中查看它。

添加搜索面板

搜索视图还可以添加搜索面板,它将在所选视图的左侧可见。它列出了一个字段中的可用值。点击一个值将按该值过滤记录。默认情况下,此搜索面板仅在列表和看板视图中可见,尽管这可以更改。

以下代码将搜索面板添加到图书馆借阅视图中。在 <search> 视图元素内添加以下 XML:

      <searchpanel>
        <field name="member_id" enable_counters="1" />
        <field name="stage_id" select="multi" />
      </searchpanel>

上一段代码向搜索面板添加了两个字段,称为成员和阶段。每个字段都列出了几个可用值,点击这些值将应用相应的过滤器。

<searchpanel> 元素有一个可用的属性 view_type,可以设置面板可见的视图类型。默认值是 view_type="tree,kanban"

<searchpanel> 内部的 <field> 元素支持一些属性。以下是其中最重要的选择:

  • string 设置要使用的特定标签文本。

  • icon 设置要呈现的图标。

  • color 设置图标的颜色。它使用 HTML 十六进制代码,例如 #8F3A84

  • select="multi" 添加选择复选框,允许用户选择多个值。这仅适用于多对一和多对多字段。

  • groups 设置可以查看搜索面板的安全组列表 XML ID。

  • enable_counters="1" 在每个值旁边添加记录编号计数器。请注意,这可能会对视图的性能产生影响。

  • limit 设置允许选择的值的数量。默认值为 200,可以设置为 0 以无限制。

    Odoo 13 的变化

    搜索面板元素是在 Odoo 13 中引入的,在之前的版本中不可用。

经过这些更改后,这是带有搜索面板的列表视图的样子:

Figure 10.4 – 带有搜索面板的列表视图

图 10.4 – 带有搜索面板的列表视图

表单、列表和搜索视图是最常用的视图类型。但还有几种其他视图类型可用于设计我们的用户界面。我们将在下一节中查看这些内容。

理解其他可用的视图类型

表单视图和列表视图是基本用户界面组件,但除此之外,还可以使用一些其他特定的视图类型。

我们已经熟悉了三个基本视图:formtreesearch。在这些之外,Odoo 社区版还提供了以下视图类型:

  • kanban 以卡片的形式呈现记录,这些卡片可以组织在列中创建看板。

  • activity 呈现计划活动的摘要。

  • calendar 以日历格式呈现记录。

  • graph 以图表的形式呈现数据。

  • pivot 以交互式数据透视表的形式呈现数据。

  • qweb 用于声明在报告、看板视图或网页中使用的 QWeb 模板。然而,这并不是像表单和列表那样的受网络客户端支持的视图类型。

看板视图将在第十一章中详细介绍,看板视图和客户端 QWeb,因此这里不会涉及。

Odoo 14 的变化

diagram 视图类型,可用于展示记录之间的关系,在 Odoo 14 中已被移除。关于此的最后一部分文档,针对 Odoo 13,可以在 www.odoo.com/documentation/13.0/developer/reference/addons/views.html#diagram 找到。

Odoo 企业版支持更多视图类型:

  • dashboard 通过子视图(如交叉表和图表)展示汇总数据。

  • cohort 用于显示数据在一定时期内的变化。

  • map 以地图形式展示记录,并可以显示它们之间的路线。

  • Gantt 以甘特图的形式展示日期调度信息。这在项目管理中常用。

  • grid 以行和列的形式展示组织好的数据。

官方文档提供了所有视图及其可用属性的良参考:https://www.odoo.com/documentation/15.0/developer/reference/backend/views.html#view-types。

提示

其他视图类型可以在社区扩展模块中找到。在 Odoo 社区协会的旗下,包括视图类型和小部件在内的网络客户端扩展可以在 github.com/OCA/web GitHub 仓库中找到。例如,web_timeline 扩展模块提供了一个 timeline 视图类型,它也能够以甘特图的形式展示调度信息。这是社区版对 gantt 视图类型的替代。

以下部分提供了 Odoo 社区版中可用的附加视图类型的简要说明。

探索活动视图

活动视图提供了预定活动的摘要。它由 mail 扩展模块提供,因此需要安装此视图类型才能可用。

可以通过将 activity 视图类型添加到窗口操作的 view_mode 字段中启用它。从 action_library_checkout 窗口操作:

        <field name="view_mode">tree,form,activity</field>

如果没有视图定义存在,将自动生成一个。

这是一个简单的定义,等同于默认生成的定义:

<record id="view_activity_checkout" model="ir.ui.view">
  <field name="model">library.checkout</field>
  <field name="arch" type="xml">
     <activity string="Checkouts">
       <templates>
         <div t-name="activity-box">
           <div>
             <field name="ntame" />
           </div>
</div>
       </templates>
     </activity>
  </field>
</record>

<templates> 元素中的 HTML 用于描述记录信息。

探索日历视图

此视图类型以日历形式展示记录,可以使用不同的时间段进行查看:按年、月、周或日。

这是一个图书馆借阅的日历视图,根据请求日期在日历上显示项目:

<record id="view_calendar_checkout" model="ir.ui.view">
  <field name="model">library.checkout</field>
  <field name="arch" type="xml">
    <calendar date_start="request_date"
color="user_id">
      <field name="member_id" />
      <field name="stage_id" />
    </calendar>
  </field>
</record>

以下属性由日历视图支持:

  • date_start 是开始日期的字段(必需)。

  • date_stop 是结束日期的字段(可选)。

  • date_delay 在包含天数的字段中。它将替代 date_end 使用。

  • all_day 提供了一个布尔字段的名称,用于表示全天事件。在这些事件中,持续时间被忽略。

  • color 是用于为日历条目分组着色的字段。在此字段中的每个不同值都将分配一个颜色,并且所有条目都将具有相同的颜色。

  • mode 是日历的默认显示模式。它可以是 dayweekmonthyear

  • scales 是可用模式的逗号分隔列表。默认情况下,它们都是可用的。

  • form_view_id 可以提供在从日历视图打开记录时要使用的特定表单视图的标识符。

  • event_open_popup="True" 将表单视图作为对话框窗口打开。

  • quick_add 允许您快速创建新记录。用户只需提供描述即可。

    Odoo 11 的变化

    在 Odoo 11 中已移除 display 日历属性。在之前的版本中,它可以用来自定义日历条目标题文本的格式;例如,display="[name], Stage [stage_id]"

为了使此视图在具有 action_library_checkout 标识符的相应窗口的 view_mode 区域中可用:

<field name="view_mode">tree,form,calendar</field>

在进行此模块升级并重新加载页面后,日历视图应该可用。

探索交叉视图

数据也可以在交叉表中看到;即,一个动态分析矩阵。为此,我们有了交叉视图。

num_books 字段将在交叉视图中用于添加借阅模型。数据聚合仅适用于数据库存储的字段;对于 num_books 字段则不是这样。因此,需要修改以添加 store=True 属性:

    num_books = fields.Integer(
        compute="_compute_num_books",
        store=True)

要将交叉表添加到图书馆借阅中,请使用以下代码:

<record id="view_pivot_checkout" model="ir.ui.view">
  <field name="model">library.checkout</field>
  <field name="arch" type="xml">
    <pivot>
      <field name="stage_id" type="col" />
      <field name="member_id" />
      <field name="request_date" interval="week" />
      <field name="num_books" type="measure" />
    </pivot>
  </field>
</record>

图表和交叉视图应包含描述要使用的轴和度量的字段元素。大多数可用的属性对两种视图类型都是通用的:

  • name 用于标识在图表中使用的字段,就像在其他视图中一样。

  • type 是字段将如何被使用;即,作为一个 row 分组(默认),measurecol(仅适用于交叉表;它用于列分组)。

  • interval 对于日期字段是有意义的,并且是用于按 dayweekmonthquarteryear 分组时间数据的间隔。

除了这些基本属性之外,还有更多属性可用,并在 www.odoo.com/documentation/15.0/developer/reference/backend/views.html#pivot 中进行了文档说明。

为了使此视图在 action_library_checkout 窗口操作的 view_mode 区域中可用:

<field name="view_mode">tree,form,pivot</field>

在进行此模块升级并重新加载页面后,日历视图应该可用。

探索图表视图

图表视图展示了具有数据聚合的图表。可用的图表包括柱状图、折线图和饼图。

这是借阅模型的图表视图示例:

<record id="view_graph_checkout" model="ir.ui.view">
  <field name="model">library.checkout</field>
  <field name="arch" type="xml">
    <graph type="bar">
      <field name="stage_id" />
      <field name="num_books" type="measure" />
    </graph>
  </field>
</record>

graph 视图元素可以有一个 type 属性,可以设置为 bar(默认),pieline。在 bar 的情况下,可以使用额外的 stacked="True" 元素使其成为堆叠柱状图。

图表使用两种类型的字段:

  • type="row"是默认设置,用于设置聚合值的条件。

  • type="measure"用于用作度量标准的字段——即实际聚合的值。

大多数可用的图表视图属性与透视表视图类型相同。官方文档提供了良好的参考:www.odoo.com/documentation/15.0/developer/reference/backend/views.html#reference-views-graph

为了使此视图在action_library_checkout窗口操作的view_mode区域中可用:

<field name="view_mode">tree,form,graph</field>

在进行此模块升级并重新加载页面后,应可使用日历视图。

摘要

设计良好的视图对于良好的用户体验至关重要。应用程序需要支持业务逻辑,但易于使用的用户界面也同样重要,以帮助用户高效地导航业务流程并最小化错误。

Odoo Web 客户端提供了一套丰富的工具来构建此类用户界面。这包括菜单系统、几种视图类型以及可供选择的不同的字段小部件。

添加菜单项是第一步,这些使用窗口操作让 Web 客户端知道应该展示哪些视图。

大多数用户交互将在表单视图中发生,了解那里可以使用的所有元素非常重要。我们首先介绍了表单视图预期遵循的一般结构,以及要添加到每个视图中的元素。

这包括标题部分、标题字段、其他表单字段、可能的笔记本部分及其页面,以及最后的消息区域。

记录数据通过字段元素进行展示和修改。详细介绍了如何使用它们以及可以用来调整其展示的几个选项。

我们讨论的下一个视图类型是列表视图。虽然比表单视图简单,但它是一个重要的记录导航工具。搜索视图也被讨论,并可用于向搜索框区域添加预定义的过滤和分组选项。这对于用户快速访问他们日常操作所需的数据非常重要。

最后,概述了其他可用的视图类型,如透视表、图表和日历视图。虽然使用频率较低,但它们在特定情况下仍然发挥着重要作用。

在下一章中,我们将学习本章未涉及的具体视图类型:看板视图及其使用的模板语法 QWeb。

进一步阅读

以下参考材料补充了本章中描述的主题:

第九章:第十一章:看板视图和客户端 QWeb

看板视图支持精益流程,提供工作进度和每个工作项状态的视觉表示。这可以是一个重要的工具,用于简化业务流程。

本章介绍了看板板的概念,以及它们如何通过使用看板视图类型、阶段列和看板状态在Odoo中实现。

看板视图由QWeb提供支持——Odoo 使用的模板引擎。它是基于XML的,用于生成HTML片段和页面。它还用于报告和网站页面,因此是 Odoo 的重要组成部分,开发者应该熟悉。

在本章中,我们将展示如何在多个区域组织看板视图,例如标题和主要内容,以及如何使用 QWeb 语法应用可用的小部件和效果。

将详细描述 QWeb 模板语言,以提供对其功能的完整理解。

后续章节将解释如何扩展看板视图中使用的 QWeb 模板,并介绍一些有用的技术。在这里,你将学习如何添加打算在这些视图中使用的网络资源,例如CSSJavaScript

本章将涵盖以下主题:

  • 介绍看板板

  • 设计看板视图

  • 设计看板卡片

  • 探索 QWeb 模板语言

  • 扩展看板视图

  • 添加 CSS 和 JavaScript 资源

到本章结束时,你将了解看板板,并能够设计自己的看板视图。

技术要求

本章将继续增强来自第十章library_checkout附加模块,后端视图 – 设计用户界面。相应的代码可以在GitHub仓库的ch11/目录中找到,网址为github.com/PacktPublishing/Odoo-15-Development-Essentials.

介绍看板板

看板是一个日本词汇,字面意思是看板,与精益制造相关。最近,随着敏捷方法的采用,看板板在软件行业中变得流行。

看板板提供了一个工作队列的视觉表示。板子组织成列,代表工作流程的阶段。工作项由放置在板子适当列上的卡片表示。新的工作项从最左边的列开始,穿过板子,直到达到最右边的列,代表已完成的工作。

看板板的简洁性和视觉冲击力使其成为支持简单业务流程的好工具。以下是一个基本的看板板示例,有三个列:待办进行中完成,如图所示:

`

图 11.1 – 看板板示例

图 11.1 – 看板看板的一个例子

在许多情况下,与更复杂的流程引擎相比,看板看板是管理流程的一种更有效的方式。

Odoo 支持看板视图 – 与经典列表和表单视图一起 – 以支持看板看板。现在我们知道了什么是看板看板,让我们学习如何使用它。

在 Odoo 中支持看板看板

浏览 Odoo 应用,我们可以看到两种不同的使用看板视图的方式。一种是一个简单的卡片列表,用于联系人、产品、员工和应用程序等地方。另一种是看板看板,它按流程步骤组织在列中。

对于简单的卡片列表,联系人看板视图是一个很好的例子。联系人卡片左侧有一个图片,主区域有一个粗体标题,后面跟着一系列值:

图 11.2 – 联系人看板视图

图 11.2 – 联系人看板视图

虽然这个联系人视图使用的是看板视图,但它不是一个看板看板

看板看板的例子可以在CRM应用的管道页面或项目任务页面找到。管道页面的一个例子在图 11.3中展示:

图 11.3 – CRM 管道看板看板

图 11.3 – CRM 管道看板看板

联系人看板视图相比,最重要的区别在于卡片在列中的组织方式。这是通过分组功能实现的,这与列表视图使用的类似。通常,分组是在阶段字段中进行的。看板视图的一个非常有用的功能是它们支持在列之间拖放卡片,这会自动将相应的值分配给视图分组的字段。

CRM 管道页面的卡片结构稍微复杂一些。主要卡片区域也有一个标题,后面跟着相关信息列表,以及页脚区域。在这个页脚区域,我们可以看到左侧有一个优先级小部件,接着是一个活动指示器,右侧则可以看到负责用户的缩略图。

在本章展示的图中看不到,但卡片在右上角还有一个选项菜单,当鼠标悬停在其上时显示。此菜单允许我们更改卡片的颜色指示器,例如。

观察这两个例子中的卡片,我们可以看到一些差异。实际上,它们的设计非常灵活,没有一种设计看板卡片的方法。但这两个例子为您的设计提供了一个起点。

我们将使用更复杂的结构作为我们检查站看板卡片的标准。

理解看板状态

在看板板上,工作项从最左边的列开始,在工作进行过程中,它们会穿过各个列,直到到达最右边的列,这显示了已完成的项目。这暗示了一种推策略,这意味着当一个列的工作完成时,工作项会被到下一个列。

推策略往往会引起进行中的工作项的积累,这可能会效率低下。精益方法建议使用拉策略代替。在这里,每个阶段会在准备好开始下一个工作项时,从上一个阶段取工作。

Odoo 通过使用看板状态支持拉策略。每个记录工作项都有一个看板状态字段,表示其流程状态:进行中(灰色)、阻塞(红色)或就绪(绿色)。

当一个阶段所需的工作完成时,不是将卡片移动到下一个列,而是将其标记为Ready。这提供了一个视觉指示,表明工作项已准备好被下一个阶段取。此外,如果有什么阻碍工作前进,它可以被标记为Blocked,这提供了一个视觉指示,表明需要帮助来解锁这个工作项。

例如,看板状态在项目任务看板视图中被使用。在下面的屏幕截图中,我们可以看到每个卡片右下角的看板状态灰色-红色-绿色指示器。还要注意每个列顶部的进度条,它提供了每个状态的项目的视觉指示:

![图 11.4 – 项目任务看板视图与看板状态图片

图 11.4 – 带有看板状态的项目任务看板视图

看板状态在每个阶段都是有意义的,因此当项目移动到另一个阶段时,应该重置它。

现在,你已经了解了看板板上的不同视图及其外观。现在,我们将继续学习如何设计它们。

设计看板视图

书籍借阅流程可以使用看板视图来可视化进行中的工作。在这种情况下,看板板的列可以代表借阅阶段,每个借阅可以由一张卡片表示。

这就是图书馆借阅看板视图完成后的样子:

![图 11.5 – 图书馆借阅看板视图图片

图 11.5 – 图书馆借阅看板视图

表单视图主要使用 Odoo 特定的 XML 元素,如<field><group>。它们还使用一些 HTML 元素,如<h1><div>,但它们的使用是有限的。看板视图则恰恰相反。它们基于 HTML,并且还支持两个 Odoo 特定的元素:<field><button>

使用看板视图,最终在 Web 客户端呈现的 HTML 是动态从 QWeb 模板生成的。QWeb 引擎处理模板中的特殊 XML 标签和属性,以生成最终的 HTML。这允许对内容的渲染方式有更多的控制,但也使得视图设计更加复杂。

由于看板视图设计非常灵活,可以使用不同的设计结构。一个好的方法是找到一个与当前用例很好地匹配的现有看板视图,检查它,并用作参考。

创建一个最小可行看板视图

看板视图允许丰富的用户界面,但可能会迅速变得复杂。学习如何设计看板视图的第一步是创建一个最小可行视图。

要将看板视图添加到library_checkout模块,请按照以下步骤操作:

  1. 在窗口操作的view_mode中添加kanban。为此,编辑views/library_menu.xml文件,并更新view_mode字段中的值以匹配以下:

      <record id="action_library_checkout"
              model="ir.actions.act_window">
        <field name="name">Checkouts</field>
        <field name="res_model">library.checkout</field>
        <field name="view_mode">
          kanban was added at the beginning of the list to have it as the default view type.
    
  2. 新的看板视图将添加到一个新的 XML 文件中,views/checkout_kanban_view.xml。因此,将此文件添加到__manifest__.py模块的data键中:

        "data": [
            "security/ir.model.access.csv",
            "views/library_menu.xml",
            "views/checkout_view.xml",
            "views/checkout_kanban_view.xml",
            "wizard/checkout_mass_message_wizard_view.xml"
             ,
            "data/stage_data.xml",
        ],
    
  3. 最后,通过以下代码在views/checkout_kanban_view.xml文件中添加一个最小看板视图的 XML 代码:

    <odoo>
      <record id="library_checkout_kanban" 
        model="ir.ui.view">
        <field name="model">library.checkout</field>
        <field name="arch" type="xml">
    <kanban>
      <templates>
        <t t-name="kanban-box">
          <div>
            <field name="name" />
          </div>
        </t>
      </templates>
    </kanban>
        </field>
      </record>
    </odoo>
    

在前面的代码中,一个看板视图在<kanban>元素内部声明。看板视图使用 QWeb 模板语言描述。模板添加在<templates>子元素内部。

每个看板卡片的主模板在<t t-name="kanban-box">元素中描述。这是一个 QWeb 模板,是最小的。它是一个包含 Odoo 特定<field>小部件的 HTML<div>元素,该小部件也用于表单和树视图。

这提供了一个相当基本的看板视图 XML 结构,可以从它开始构建。要成为一个看板板,它需要包含每个流程阶段的列。

展示看板板列

看板板以列的形式呈现组织好的工作项,其中每个列是流程中的一个阶段。新的工作项从左侧列开始,然后通过列移动,直到到达右侧,完成。

看板视图在按字段分组时以列的形式呈现项目。对于看板板,视图应按阶段或状态字段分组 - 通常,使用stage_id

default_group_by属性为看板视图设置默认列组。要有一个用于图书借阅的看板板,编辑<kanban>元素,使其看起来像这样:

<kanban default_group_by="stage_id">

当打开此视图时,它将默认按阶段分组(类似于图 11.4)。用户仍然可以更改用于列表视图的group_by选项。

理解看板视图属性和元素

看板视图支持一些额外的属性来微调其行为。

<kanban>顶级元素支持以下属性:

  • default_group_by: 这将设置用于默认列分组的字段。

  • default_order: 这将设置用于看板项目的默认排序。

  • quick_create="false": 这将禁用通过仅提供标题描述来创建新项目的quick_create选项,使用每个列标题右侧的加号。false值是一个 JavaScript 字面量,必须小写。

  • quick_create_view:可以可选地用于设置用于quick_create函数的特定表单视图。它应该使用表单视图的 XML ID 设置。

  • class: 这会给渲染的看板视图的根元素添加一个 CSS 类。一个相关的类是o_kanban_small_column,它使得列比默认值更紧凑。可能通过模块提供的 CSS 资产提供额外的类。

  • group_creategroup_editgroup_deletequick_create_view:这些可以设置为false以禁用在看板列上的相应操作。例如,group_create="false"将移除屏幕右侧的垂直添加新列栏。

  • records_draggable="false": 这将禁用拖动记录在列之间的功能。

<kanban>元素可以包含以下元素:

  • <field>: 这用于声明需要从服务器检索的、由 QWeb 模板使用的字段。当这些字段用于 QWeb 评估表达式时,这是必要的。对于在模板 <field> 元素中使用的字段,则不需要。

  • <progressbar>: 此元素在组列标题上添加进度条小部件。

  • <templates>: 在声明看板卡片 QWeb 模板的地方需要此元素。

<templates>元素的一个示例可以在之前提供的最小看板视图中看到。下面提供了一个<progressbar>元素的示例。

向组列添加进度条

进度条可以显示列的总数以及表示列记录子状态的彩色条。CRM 流程页面使用它来提供从计划逾期的潜在活动摘要。另一个例子是项目任务看板中使用的看板状态。

为了做到这一点,首先需要将kanban_state添加到模型中,然后可以在视图中使用它。为此,执行以下步骤:

  1. 将字段添加到library.checkout模型中,按照以下方式编辑models/library_checkout.py文件:

    # class Checkout(models.Model):
        kanban_state = fields.Selection(
            [("normal", "In Progress"),
             ("blocked", "Blocked"),
             ("done", "Ready for next stage")],
            "Kanban State",
            default="normal")
    
  2. 当在write()方法的开头更改同一文件中的阶段时,添加以下业务逻辑以重置kanban_state

        def write(self, vals):
            # reset kanban state when changing stage
    if "stage_id" in vals and "kanban_state" 
    not in vals:
                vals["kanban_state"] = "normal"
            # Code before write ...
            # ...
            return True
    

这完成了目前所需的模型更改。在这里,我们专注于添加进度条 - 看板状态小部件将在稍后的部分添加。

<progressbar>元素是允许在<kanban>标签内使用的三种元素类型之一,与<field><templates>一起。

要将其添加到<kanban>视图定义中,编辑元素并添加以下高亮代码:

<kanban>
<progressbar field="kanban_state" 
   colors='{
     "done": "success",
     "blocked": "danger",
     "normal": "muted"}'
sum_fields="num_books" 
  />
  <templates>
    <t t-name="kanban-box">
      <div>
        <field name="name" />
      </div>
    </t>
  </templates>
</kanban>

上述代码添加了进度条小部件。field属性设置了要使用的模型字段,而colors属性将字段值映射到"danger""warning""success""muted"颜色。

默认情况下,列总计指示器计算每个列中的项目数量。这可以更改为模型字段中值的总和。在之前的代码中,添加了可选的 sum_fields 属性以显示每个列请求中的书籍总数。

到目前为止,我们已经有一个功能齐全的看板视图。然而,看板卡片可以显示更丰富的功能。下一节将专注于这一点,我们将进一步扩展用于渲染看板卡片内容的模板。

设计看板卡片

看板卡片的布局相当灵活,并使用由 <templates> 元素中声明的 QWeb 模板生成的 HTML。

内容区域通常会包含几个其他区域。以 CRM 流程为蓝图,可以找到以下部分:

  • 一个标题区域,包含潜在客户的简要概述

  • 一个内容区域,包含数量、客户名称和潜在客户标签

  • 一个左侧页脚区域,包含优先级和活动小部件

  • 一个右侧页脚区域,包含销售人员头像

  • 一个右上角菜单按钮,在这种情况下,在鼠标悬停时可见

这个部分实现了之前的看板卡片结构,并为每个部分填充内容以展示最重要的功能。设计看板卡片的第一个步骤是布局看板卡片框架,这将在下面描述。

注意

提出的看板框架以及使用的某些 CSS 类基于 CRM 流程看板视图。Odoo 模块可以提供特定的 CSS 类并在看板卡片设计中使用它们。因此,当检查来自不同模块的看板视图模板时,这些可能会有所不同。

组织看板卡片布局

看板卡片的最小设计现在将扩展到包括几个区域的框架,我们将现在描述这些。

看板卡片定义在 <templates> 部分的一个具有 t-name="kanban-box" 的元素中。这可以是一个 HTML 元素或一个 QWeb t- 指令。本章前面创建的定义使用了中性的 <t> QWeb 元素:<t t-name="kanban-box">

继续前进,看板视图模板和 QWeb 模板应该被编辑以标记要工作的区域,如下面的代码所示:

<kanban>
  <!-- Field list to ensure is loaded ... -->
  <templates>
    <t t-name="kanban-box">
      <div class="oe_kanban_global_click">
        <div class="o_dropdown_kanban dropdown">
          <!-- Top-right drop down menu ... -->
        </div>
        <div class="oe_kanban_content">
          <div class="o_kanban_record_title">
            <!-- Title area ... -->
            <field name="name" />
          </div>
          <div class="o_kanban_record_body">
            <!-- Other content area  ... -->
          </div>
          <div class="o_kanban_record_bottom">
            <div class="oe_kanban_bottom_left">
              <!-- Left side footer... -->
            </div>
            <div class="oe_kanban_bottom_right">
                <!-- Right side footer... -->
            </div>
          </div> <!-- o_kanban_record_bottom -->
          <div class="oe_clear"/>
        </div> <!-- oe_kanban_content -->
      </div> <!-- oe_kanban_global_click -->
</t>

之前的 QWeb 模板代码为看板卡片中通常看到的各个区域提供了一个框架。

当在 <t> 元素中使用 t-name QWeb 属性时,此元素只能有一个子元素。这在前面代码中就是这样,并且 <div> 子元素必须包含所有其他看板视图元素。

值得注意的是,这个总的 <div> 元素使用了 class="oe_kanban_global_click" 属性。这使得卡片可点击,当用户这样做时,相应的表单视图将以类似列表视图的方式打开。

下一个任务是专注于每个突出显示的区域,并向其中添加内容。

添加标题和其他内容字段

现在我们已经有一个基本的看板卡片框架,可以添加标题和附加数据。

这些将放在 <div class="oe_kanban_content"> 元素内部。所使用的骨架为这些部分提供了区域:<div class="o_kanban_record_title"><div class="o_kanban_record_body"> 元素。

以下代码扩展了这个部分,以突出显示卡片标题并添加检查请求日期和请求的图书馆成员 ID:

          <div class="o_kanban_record_title">
            <!-- Title area ... -->
            <strong><field name="name" /></strong>
          </div>
          <div class="o_kanban_record_body">
            <!-- Other content area ... -->
            <div><fields name="request_date" /></div>
            <div>
<field name="member_id" 
                widget="many2one_avatar"/>
            </div>
          </div>

在这种情况下,可以使用常规 HTML 元素。例如,使用 <strong> 元素来突出标题。此外,可以使用 <field> 元素来渲染字段值,这些值将以与表单视图类似的方式使用适当的格式进行渲染。在之前的代码中,request_date 使用了 <field> 元素,因此其内容将使用 Odoo 配置的日期格式进行渲染。它被包裹在一个 <div> 元素中,以便在多个字段之间有一个换行。

通过使用特定的小部件来添加 member_id 多对一对象,该小部件展示了相应的头像图像以及名称,widget="many2one_avatar"

现在我们已经为卡片添加了一些基本数据元素,让我们看看下拉菜单区域。

添加下拉选项菜单

�看板卡片可以在右上角有一个选项菜单。常见选项包括能够编辑或删除记录、为卡片设置颜色或运行任何可以从按钮调用的操作。

以下是为 oe_kanban_content 元素顶部添加的选项菜单的基本 HTML 代码:

        <div class="o_dropdown_kanban dropdown">
          <!-- Top-right drop down menu ... -->
          <a class="dropdown-toggle btn"
             role="button" data-toggle="dropdown"
             title="Dropdown menu" href="#">
            <span class="fa fa-ellipsis-v" />
          </a>
          <div class="dropdown-menu" role="menu">
            <!-- Edit menu option -->
            <t t-if="widget.editable">
              <a role="menuitem" type="edit"
                 class="dropdown-item">Edit</a>
            </t>
            <!-- Delete menu option -->
<t t-if="widget.deletable">
              <a role="menuitem" type="delete"
                 class="dropdown-item">Delete</a>
            </t>
            <!-- Separator line -->
            <div role="separator" class=
              "dropdown-divider"/>
            <!-- Color picker option: -->
<ul class="oe_kanban_colorpicker" 
data-field="color" />
            <!-- Set as Done menu option -->
<a t-if="record.state != 'done'" 
               role="menuitem" class="dropdown-item"
name="button_done" type="object">Set 
               as Done</a>
          </div>
        </div>

在这里,有一些使用可能未加载到视图中的字段的 QWeb 表达式。特别是,最后的 t-if 表达式使用了记录的 state 字段。为了确保这个字段在表单中可用,它应该紧接在 <kanban> 元素之后添加:

<kanban>
  <!-- Field list to ensure is loaded ... -->
  <field name="state" />

让我们分解下拉菜单代码并查看添加的关键元素:

  • 在 HTML 锚点 (<a>) 元素中的省略号图标,用于显示菜单按钮。

  • 一个 <div class="dropdown-menu" role="menu"> 元素,包含菜单选项。

  • 带有 type="edit"<a> 元素。

  • 带有 type="delete"<a> 元素。

  • 使用 <div role="separator" class="dropdown-divider"/> 创建的分隔线。

  • 使用 <ul class="oe_kanban_colorpicker" /> 元素添加的颜色选择器菜单选项。data-field 属性设置了用于存储选择的颜色的字段。这个功能将在下一节中实现,所以现在它不会正常工作。

  • 一个与按钮点击等效的菜单项,通过 <a> 元素添加,具有与常规按钮相同的 nametype 属性。这个特定的一个使用 name="button_done" type="object"

一些菜单项,如 t-if QWeb 指令。这个和其他 QWeb 指令将在本章的 探索 QWeb 模板语言 部分中更详细地解释。

widget 全局变量代表一个 KanbanRecord() JavaScript 对象,它负责渲染当前看板卡片。两个特别有用的属性是 widget.editablewidget.deletable,这允许我们检查相应的操作是否可用。

可以通过添加类似 设置为完成 选项的额外 <a> 元素以类似的方式添加菜单项。

可以使用可以记录字段值的 JavaScript 表达式来显示或隐藏菜单项。例如,state 字段未设置为 done

颜色选择器菜单选项使用一个特殊的 widget,该 widget 使用 color 模型字段来存储选择的颜色。虽然颜色选择器可用,但我们尚未添加设置卡片的功能。让我们在下节中完成这个操作。

添加看板卡片颜色指示器

看板卡片可以设置用户选择的颜色。这会在卡片左侧绘制一个条形,并且可以方便地定位项目。

要应用的颜色是通过卡片菜单上的颜色选择器选项选择的。这通过一个 <ul class="oe_kanban_colorpicker" data-field="color"/> 元素添加,如前节所示。data-field 属性设置了要使用的字段,在这种情况下是 color

要添加看板颜色卡片指示器,完成以下步骤:

  1. 通过编辑 models/library_checkout.py 文件,按以下方式在 library.checkout 模型中添加颜色字段:

    # class Checkout(models.Model):
    # ...
        color = fields.Integer()
    

    这是一个常规的整数字段。颜色选择器 widget 将可选颜色映射到数字。

  2. 现在,可以通过 QWeb 在看板卡片上使用颜色字段设置动态 CSS 样式。首先,通过添加以下代码将其添加到要加载的字段中:

    <kanban>
      <!-- Field list to ensure is loaded ... -->
      <field name="color" />
      <field name="state" />
    
  3. 最后,编辑看板卡片顶部的 <div> 元素以添加动态颜色样式,如下面的代码所示:

        <t t-name="kanban-box">
          <div t-attf-class="oe_kanban_global_click
             {{!selection_mode ? 'oe_kanban_color_' +
    kanban_getcolor(record.color.raw_value) : 
            ''}}">
    

上述代码使用 t-attf-class 动态计算要应用的 CSS 类。在 {{ }} 块中声明了一个 JavaScript 表达式,用于评估并返回要使用样式,这取决于 color 字段值。这完成了添加看板颜色卡片指示器的步骤。

看板卡片还有更多小部件可供选择。接下来的几节将展示如何使用它们,我们将把它们添加到卡片页脚部分。

添加优先级和活动小部件

优先级小部件显示为可以点击以选择优先级级别的星号列表。这个小部件是一个 widget="priority"<field> 元素。优先级字段是一个 Selection 字段,声明了可用的几个优先级级别。

需要修改 library.checkout 模型以添加优先级字段。为此,完成以下步骤:

  1. 按以下方式编辑 models/library_checkout.py 文件:

    # class Checkout(models.Model):
    # ...
    <field name="activity_ids">) with widget="kanban_activity".
    
  2. 现在,需要在左侧的看板模板中添加相应的 <field> 元素。因此,插入优先级小部件:

    <div class="oe_kanban_footer_left"> 
      <!-- Left side footer... --> 
    <field name="priority" widget="priority"/> 
    <field name="activity_ids" 
        widget="kanban_activity"/>
    </div>
    

看板卡片现在已在页脚左侧添加了优先级和活动小部件。接下来,我们将在右侧页脚添加更多小部件。

添加看板状态和小部件

看板状态小部件为项目呈现交通灯颜色。它是一个使用widget="kanban_state_selection"<field>元素。

对于相关的用户记录,有一个特定的小部件可供使用:widget="many2one_avatar_user"

这两个示例都将添加到看板卡片右下角,如下面的代码所示:

<div class="oe_kanban_footer_right">
  <!-- Right side footer... -->
  <field name="kanban_state"
         widget="kanban_state_selection" />
  <field name="user_id"
         widget="many2one_avatar_user" />
</div>

使用<field>元素和kanban_state_selection小部件添加看板状态。

使用user_id字段和widget="many2one_avatar_user"小部件添加用户头像图像。

另一个重要的话题是在看板卡片上使用动作,我们将在下一节中讨论。

在看板视图元素中使用动作

在 QWeb 模板中,链接的<a>标签可以有一个type属性。这设置了链接将执行的动作类型,以便链接可以像常规表单中的按钮一样操作。因此,除了<button>元素外,<a>标签也可以用来运行 Odoo 动作。

就像表单视图一样,动作类型可以设置为actionobject,并且应该有一个name属性来标识要执行的具体动作。此外,以下动作类型也是可用的:

  • open:这会打开相应的表单视图。

  • edit:这会直接以编辑模式打开相应的表单视图。

  • delete:这会删除记录并从看板视图中移除项目。

这完成了我们对设计看板视图的概述。看板视图使用 QWeb 模板语言,这里使用了几个示例。下一节将深入探讨 QWeb。

探索 QWeb 模板语言

QWeb 解析器在模板中寻找特殊指令,并用动态生成的 HTML 替换它们。这些指令是 XML 元素属性,可以在任何有效的标签或元素中使用——例如,<div><span><field>

有时,需要使用 QWeb 指令,但我们不希望在模板中的任何 XML 元素中放置它。对于这些情况,可以使用<t>特殊元素。它可以有 QWeb 指令,如t-ift-foreach,但它是无声的,并且不会对最终生成的 XML/HTML 产生任何影响。

QWeb 指令经常使用评估表达式来产生依赖于记录值的不同效果。用于评估这些表达式的语言取决于 QWeb 执行的环境。有两种不同的 QWeb 实现:客户端 JavaScript服务器端 Python。报告和网站页面使用 QWeb 的服务器端 Python 实现。

�看板视图使用客户端 JavaScript 实现。这意味着看板视图中使用的 QWeb 表达式应该使用 JavaScript 语法编写,而不是 Python。

当显示看板视图时,内部步骤大致如下:

  1. 获取用于渲染模板的 XML。

  2. 调用服务器read()方法以获取模板中使用的字段数据。

  3. 定位到kanban-box模板,并使用 QWeb 进行解析以输出最终的 HTML 片段。

  4. 在浏览器显示中注入 HTML(文档对象模型DOM))。

这并不是要精确的技术描述。它只是一个思维导图,有助于理解看板视图中事物的工作方式。

接下来,我们将学习 QWeb 表达式评估,并探索可用的 QWeb 指令,使用示例来增强结账看板卡片。

理解 QWeb JavaScript 评估上下文

许多 QWeb 指令使用的是用于产生某些结果的计算表达式。当在客户端使用(如看板视图的情况)时,这些表达式用 JavaScript 编写。它们在一个包含一些有用变量的上下文中进行评估。

可用record对象表示当前记录,包含从服务器请求的字段。字段值可以通过raw_valuevalue属性访问:

  • raw_value:这是read()服务器方法返回的值,因此更适合用于条件表达式。

  • value:这是根据用户设置格式化的,并旨在在用户界面中显示。这通常对日期、日期时间、浮点数、货币和关系字段很有用。

QWeb 评估上下文也可以引用 JavaScript 网络客户端实例。为了利用这一点,需要对网络客户端架构有良好的理解。在本章中,我们无法对此进行详细说明。然而,出于参考目的,以下标识符在 QWeb 表达式评估中可用:

  • widget:这是对当前KanbanRecord()小部件对象的引用,负责将当前记录渲染为看板卡片。它公开了一些我们可以使用的辅助函数。

  • record:这是widget.record的快捷方式,提供使用点符号访问可用字段。

  • read_only_mode:这表示当前视图是否处于只读模式(而不是编辑模式)。它是widget.view.options.read_only_mode的快捷方式。

  • instance:这是对完整网络客户端实例的引用。

由于 QWeb 模板是写在 XML 文件中的,因此对某些不被 XML 格式接受的字符(如小于号<)的使用有限制。当需要这些字符时——例如,用于描述 JavaScript 表达式——需要使用转义替代字符。

这些是可用于不等式操作的替代符号:

  • &lt;表示小于(<)。

  • &lt;=表示小于或等于(<=)。

  • &gt;表示大于(>)。

  • &gt;=表示大于或等于(>=)。

前面的比较符号不是 Odoo 特有的,而是 XML 格式标准的一部分。

之前的符号可以在 QWeb 评估表达式中使用,并且它们通常用于计算t-out指令要渲染的文本,我们将在下一节中描述。

使用t-out指令渲染值

<field>元素可用于渲染字段值,其优点是 Odoo 会为我们正确格式化输出。但这个限制是只能显示字段内容。

然而,t-out指令可以渲染代码表达式的结果作为 HTML 转义值:

<t t-out="'Requested on ${record.request_date.value}'" />

之前的代码渲染了 JavaScript 表达式的结果。record代表从 Odoo 服务器检索的记录,并提供对字段的访问。value属性返回正确格式化的内容,就像<field>元素返回的那样。raw_value属性返回未格式化的原生值。

Odoo 15 的变化

t-out指令是在t-esc指令中引入的,直到t-raw指令也被弃用。它之前被用来渲染原始值而不进行 HTML 转义,使用它存在安全风险。

使用t-set指令为变量赋值

对于更复杂的逻辑,表达式的结果可以存储到变量中,以便在模板中稍后使用。这需要使用t-set指令来设置变量名称,然后使用t-value指令来计算要分配的值。

例如,以下代码在请求还没有行时将标题渲染为红色。它使用red_or_black变量作为要使用的 CSS 类,如下所示:

<t t-set="red_or_black"
t-value="record.num_books == 0 ? '' : 
    'oe_kanban_text_red'"
/>
<strong t-att-class="red_or_black">
  <field name="name" />
</strong>

之前的示例中有一个使用num_books字段的代码表达式,因此我们需要确保它通过在<kanban>顶部元素内添加<field name="num_books" />元素来加载。

变量也可以分配 HTML 内容,如下例所示:

<t t-set="calendar_sign">
  <i class="fa fa-calendar" title="Calendar" />
</t>
<t t-out="calendar_sign" />

之前的代码将 HTML 内容分配给calendar_sign变量,然后使用t-out指令进行渲染。

使用t-attf-进行动态属性字符串替换

我们的看板卡片使用t-attf- QWeb 指令在顶部的<div>元素中动态设置一个类,这样卡片颜色就取决于color字段值。为此,使用了t-attf- QWeb 指令。

t-attf-指令通过字符串替换动态生成标签属性。这允许动态生成较大字符串的部分,例如 URL 或 CSS 类名。

指令寻找将被评估并替换为结果的代码块。这些块由{{}}#{}分隔。块的内容可以是任何有效的 JavaScript 表达式,并且可以使用 QWeb 表达式可用的任何变量,例如recordwidget

在这种情况下,使用了kanban_color() JavaScript 函数。这是专门提供的,用于将颜色索引数字映射到 CSS 类颜色名称。

作为详细示例,这个指令将被用来动态改变请求日期的颜色,如果优先级高,则显示为红色字母。为此,看板卡片中的<field name="request_date"/>元素应替换为以下内容:

<div t-attf-class="oe_kanban_text_{{
  record.priority.raw_value &lt; '2'
  ? 'black' : 'red' }}">
  <field name="request_date"/>
</div>

这将导致class="oe_kanban_text_red"class="oe_kanban_text_black",具体取决于优先级值。这是动态评估的——这意味着当用户点击优先级小部件更改它时,日期颜色会立即改变。

使用 t-att-动态属性计算表达式

t-att- QWeb 指令可以从表达式评估中动态生成一个属性值。

例如,之前章节中使用的t-attf-属性产生的格式化效果,也可以使用t-att-来实现。以下代码展示了这种替代实现:

<div t-att-class="record.priority.raw_value &lt; '2'
  ? 'oe_kanban_text_black' : 'oe_kanban_text_red'">
  <field name="request_date"/>
</div>

当表达式评估为假等价值时,属性根本不会渲染。这对于特殊的 HTML 属性,如checked输入字段非常重要。

使用 t-foreach 循环

遍历循环对于重复特定的 HTML 块很有用。为此,使用t-foreach指令与返回可迭代值的表达式一起使用。它需要伴随一个t-as指令,该指令设置迭代值的变量名。

这可以用来展示在结账时请求的书籍标题。这需要在lines_ids字段上循环。

注意,line_ids元素的可用值是数据库 ID,而不是记录对象。这可以通过在<!-- 其他内容区域 -->区域添加以下代码来确认:

<div>
  <t t-foreach="record.line_ids.raw_value" t-as="line">
    <t t-out="line" />;
  </t>
</div>

t-foreach指令接受一个评估为集合的 JavaScript 表达式,用于迭代。record.<field>.value返回字段值的字符串表示,而record.<field>.raw_value返回数据库存储的值。对于多对多字段,这是一个 ID 列表:

  • t-as指令设置用于引用每个迭代值的变量名。

  • t-out指令评估提供的表达式——在这种情况下,只是line变量名——并安全地渲染转义后的 HTML。

展示记录 ID 并不很有趣。然而,我们有一个 JavaScript 函数可以用来获取 ID 的图片:kanban_image()

要使用这个功能,首先,结账行需要支持图片。为此,需要编辑models/library_checkout_line.py文件,添加一个用于书封图片的字段:

    book_cover = fields.Binary(related="book_id.image")

现在,这个字段可以在看板卡片中使用:

<div>
  <t t-foreach="record.line_ids.raw_value" t-as="line">
    <t t-out="line" />;
    <img t-att-src="kanban_image(
      'library.checkout.line', 'book_cover', line)" 
      class="oe_avatar" height="60" alt="Cover" /> 
  </t>
</div>

上述代码在每个结账行中渲染书籍标题的图片。

如果有很多行,这可能对于看板卡片来说内容太多。由于t-foreach对象是一个 JavaScript 表达式,它可以使用额外的语法来限制允许的封面缩略图的数目。JavaScript 数组有一个slice()方法来提取元素子集。

这可以通过以下 for 循环的变体来限制数量为前五个元素:

<t t-foreach="record.line_ids.raw_value.slice(0, 5)" t-as="line>

for 循环有一些辅助变量可用。这些变量是自动生成的,并且以前面定义的变量名作为前缀。

如果使用 t-as="rec",其中 rec 被设置为变量名,辅助变量如下:

  • rec_index:这是迭代索引,从零开始。

  • rec_size:这是集合中元素的数量。

  • rec_first:在迭代的第一个元素上为 true

  • rec_last:在迭代的最后一个元素上为 true

  • rec_even:在偶数索引上为 true

  • rec_odd:在奇数索引上为 true

  • rec_parity:这可以是 oddeven,具体取决于当前索引。

  • rec_all:这代表正在迭代的对象。

  • rec_value:在迭代 {key:value} 字典(rec 保留键名)时持有值。

例如,当呈现一个以逗号分隔的值列表时,我们希望避免在最后一个迭代中出现尾随逗号。借助 _last 循环变量,避免在最后一个迭代中渲染它很容易。以下是一个示例:

<t t-foreach="record.line_ids.raw_value" t-as="rec">
  <t t-out="rec" />
  <t t-if="!rec_last">;</t>
</t>

rec_last 变量在最后一个记录上为 true。通过使用 !rec_last 取反,可以在除了最后一个迭代之外的所有迭代中打印逗号。

使用 t-if 应用条件

t-if 指令期望在客户端渲染看板视图时,在 JavaScript 中评估一个表达式。只有当条件评估为 true 时,标签及其内容才会被渲染。

在我们的示例中,它在结账看板视图中被用来根据某些条件提供菜单选项。

再举一个例子,我们可以显示借阅的书籍的结账编号,但仅当视图有任何行时。这可以通过在 <!-- 其他内容区域 --> 区域添加以下代码来确认:

<div> t-if="record.num_books.raw_value &gt; 0"> 
  <field name="num_books"/> books 
</div>>

在这里,我们使用了一个 t-if="<expression>"> 属性来渲染一个元素及其内容,仅当使用的表达式评估为 true 时。注意,条件表达式使用 &gt; 符号而不是 > 来表示大于操作。

else ifelse 条件也支持使用 t-elift-else 指令。以下是一个使用示例:

<div t-if="record.num_books.raw_value == 0">
  No books!
</div>
<div t-elif="record.num_books.raw_value == 1">
  One book
</div>
<div t-else="">
  <field name="num_books"/> books
</div>

这些条件对于在特定情况下渲染特定元素非常有用。

另一个有用的功能是能够将模板分解成更小的可重用片段,这些片段可以使用 t-call 包含。以下部分解释了这是如何工作的。

使用 t-call 调用和重用模板

而不是反复重复相同的 HTML 块,可以使用构建块来组合更复杂的用户界面视图。QWeb 模板可以用作可重用的 HTML 片段,这些片段可以插入到其他模板中。

可重用模板定义在 <templates> 标签内,并由具有 t-name 属性且不同于 kanban-box 的顶级元素标识。然后可以使用 t-call 指令包含这些其他模板。这适用于在同一看板视图中声明的模板,在同一插件模块的其他地方,甚至在不同的插件中。

例如,可以将封面列表隔离在可重用的片段中。为此,可以在 <t t-name="kanban-box"> 节点之后在 <templates> 元素中添加另一个模板,如下面的示例所示:

<t t-name="book_covers">
  <div>
    <t t-foreach="record.line_ids.raw_value" t-as="line">
      <t t-out="line" />;
      <img t-att-src="kanban_image(
        'library.checkout.line', 'book_cover', line)"
        class="oe_avatar" height="60" alt="Cover" />
    </t>
  </div>
</t>

然后,可以使用 t-call 指令在 kanban-box 主模板中调用此模板:

<t t-call="book_covers" />

要调用在其他插件模块中定义的模板,必须使用 module.name 完整标识符,类似于其他视图发生的方式。例如,此片段可以使用 library_checkout.book_covers 完整标识符在其他模块中引用。

被调用的模板在相同的上下文中运行,因此调用者中可用的任何变量名在处理被调用的模板时也是可用的。

一个更优雅的替代方案是将参数传递给被调用的模板。这是通过在 t-call 标签内设置变量来完成的。这些变量将仅在子模板上下文中评估和可用,而在调用者上下文中不存在。

例如,books_cover 模板可以有一个参数来设置要显示的最大封面数,而不是在子模板中硬编码。首先,应该编辑 book_covers 模板,将固定限制替换为变量,例如 limit

<t t-name="book_covers">
  <div>
    <t t-foreach="record.line_ids.raw_value.slice(0, 
      limit)"
       t-as="line">
      <t t-out="line" />;
      <img t-att-src="kanban_image(
        'library.checkout.line', 'book_cover', line)"
        class="oe_avatar" height="60" alt="Cover" />
    </t>
  </div>
</t>

现在,t-call 必须使用嵌套的 t-set 指令来设置此变量,如下面的代码所示:

<t t-call="book_covers"> 
  <t t-set="limit" t-value="3" /> 
</t>

t-call 元素内的整个内容也通过 0 魔术变量对子模板可用。而不是参数变量,可以在 t-call 元素内添加 HTML 代码片段,然后可以在被调用的模板中使用 <t t-out="0" /> 来使用它。这对于构建布局以及以模块化方式组合/嵌套 QWeb 模板特别有用。

使用字典和列表动态设置属性

我们已经介绍了最重要的 QWeb 指令,但还有一些需要了解。现在,我们将简要解释它们。

在这里,引入了 t-att-NAMEt-attf-NAME 风格的动态标签属性。此外,还可以使用固定的 t-att 指令。它接受键值映射字典或一对(即,一个两元素列表)。

例如,考虑以下映射:

<p t-att="{'class': 'oe_bold', 'name': 'Hello'}" />

上述代码产生以下结果:

<p class="oe_bold" name="Hello" />

t-att 也可以与列表或值对一起工作。例如,考虑以下:

<p t-att="['class', 'oe_bold']" />

上述代码产生以下结果:

<p class="oe_bold" />

这些特殊方式为元素分配属性在存在一些服务器端处理的情况下可能很有用,并且可以使用结果字典或列表在单个 t-att 元素上应用模板元素。

这完成了对 QWeb 模板语言的合理概述,特别关注看板视图应用,尽管 QWeb 语言也用于服务器端 – 例如,它可以用于报告和网站页面。

毫不奇怪,QWeb 模板提供了一个扩展机制。我们将在下一节中探讨这一点。

扩展看板视图

用于看板视图和报告的模板可以像其他视图类型一样扩展:即声明要匹配的元素,可能使用 XPath 表达式,并使用位置属性设置扩展应该做什么(例如,在匹配元素之后或之前添加新元素)。这些技术将在第四章 扩展模块中详细解释。

在实践中,看板视图和 QWeb 模板比常规表单视图更复杂,匹配要扩展的元素可能很棘手。

使用 <field> 元素作为选择器可能很困难。在看板视图中,相同的字段名可能被包含多次:在开始时,在要加载的字段列表中,然后在看板框模板内部再次出现。由于选择器将匹配找到的第一个字段元素,因此修改不会应用于模板内部,正如预期的那样。

例如,XPath 表达式 //t[@t-name='kanban-box']//field[@name='name'] 定位到匹配 <t t-name="kanban-box"> 的任何子元素,然后找到匹配 <field name="name"> 的任何进一步子元素。

另一个挑战是频繁使用没有明确标识符的 HTML 元素,例如 <div><span>。在这些情况下,需要使用非平凡匹配条件的 XPath 表达式。例如,//div/t/img XPath 表达式匹配一个 <div><t><img> 嵌套元素序列。

以下是一个扩展 Contacts �看板视图的示例:

<record id="res_partner_kanban_inherit" model="ir.ui.view"> 
  <field name="name">Contact Kanban modification</field> 
  <field name="model">res.partner</field> 
  <field name="inherit_id" 
    ref="base.res_partner_kanban_view" /> 
  <field name="arch" type="xml"> 
    <xpath
     expr="//t[@t-name=
       'kanban-box']//field[@name='display_name']" 
     position="before"> 
     <span>Name:</span> 
    </xpath> 
  </field> 
</record>

在上一个示例中,XPath 在 <t t-name="kanban-box"> 元素内部查找 <field name="display_name"> 元素。这排除了 <templates> 部分之外的相同字段元素。

对于复杂的 XPath 表达式,一些命令行工具可以帮助探索正确的语法。

xmllint 命令行实用程序 – 来自 libxml2-utils--xpath 选项,用于对 XML 文件进行查询。以下是如何使用它的示例:

$ xmllint --xpath "//templates//field[@name='name']" library_checkout/views/checkout_view.xml

另一个选项是 xpath 命令,来自 Debian/Ubuntu 软件包的 libxml-xpath-perl。以下是如何使用它的示例:

$ xpath -e "//templates//field[@name='name']" library_checkout/views/checkout_view.xml

这些工具可以用来快速尝试和测试 XML 文件上的 XPath 表达式。

到目前为止,你已经看到了如何创建和扩展看板视图。然而,这些可以利用额外的 JavaScript 和 CSS 资产来实现效果。下一节将解释如何添加这些组件。

添加 CSS 和 JavaScript 资产

看板视图主要是 HTML,并大量使用 CSS 类。在本章中,代码示例中介绍了一些标准 CSS 类,但模块也可以提供自己的 CSS。

通常使用的约定是将资产文件放在/static/src子目录中。

模块 Web 资源在assets键中的manifest文件中声明。此文件使用一个字典设置,将资产包映射到要扩展的资产和要添加到其中的资产列表。

这提供了向 Odoo 模块添加 Web 资产(如 CSS 和 JavaScript 资产)的工具。这些 Web 资产文件提供了一种结构化的方式,以更好地提供用户界面元素,从而提供更丰富的用户体验。

然后,它们可以在本章前几节讨论的模块的 QWeb 模板中使用。

这里是library_checkout附加模块的一个示例。编辑__manifest__.py文件,添加以下内容:

    "assets": {
        "web.assets_backend": {
            "library_checkout/static/src/css/checkout.css",
            "library_checkout/static/src/js/checkout.js",
        }
    }

以下代码向web.assets_backend资产包添加了 CSS 和 JavaScript 文件。

可用的主要资产包如下:

  • web.assets_common:这包含 Web 客户端、网站以及销售点的通用资产。

  • web.assets_backend:这包含特定于后端 Web 客户端的资产。

  • web.assets_frontend:这包含可供公共网站使用的资产。

assets清单键是在 Odoo 15 中引入的。对于之前的 Odoo 版本,资产是通过 XML 模板继承声明的。我们将在下一节中解释这一点。

在 Odoo 15 之前添加资产

在之前的 Odoo 版本中,资产是通过扩展资产包的 XML 文件添加的。执行此操作的 XML 文件通常放置在views/模块子目录中。

以下示例向library_checkout模块添加了 CSS 和 JavaScript 文件。添加views/assets.xml文件,并包含以下代码:

<odoo> 
  <template id="assets_backend" 
    inherit_id="web.assets_backend"
    name="Library Checkout Kanban Assets" >
    <xpath expr="." position="inside">
      <link rel="stylesheet"
       href="/library_checkout/static/src/css/checkout.css"
      />
      <script type="text/javascript"
       src="img/checkout.js">
      </script>
    </xpath>
  </template>
</odoo>

如同往常,此代码也应添加到__manifest__.py描述符文件的data键中。

摘要

本章涵盖了看板视图,并展示了它们如何作为强大的用户界面工具。到现在为止,你应该理解了看板板,并且你拥有了设计看板视图所需的技术。

在本章中,你还探索了驱动看板视图的 QWeb 模板语言。通过本章中的示例,你现在应该知道如何使用其功能。

如 Odoo 所预期,看板视图和 QWeb 模板也可以以与其他视图类型类似的方式由其他模块扩展。阅读完本章后,你应该知道如何使用此功能在看板视图中使用额外的技术。

最后,我们还讨论了在高级看板视图中使用 CSS 和 JavaScript 资产。我们还探讨了这些资产必须由模块提供,并且必须添加到后端资产中。你现在知道如何实现这一点。

下一章将继续探索 QWeb,但这次我们将关注服务器端,并了解如何设计可打印的报告。

进一步阅读

以下参考资料补充了本章讨论的主题:

第十章:第十二章:使用服务器端 QWeb 创建可打印的 PDF 报告

虽然常规视图可以为用户提供有价值的信息,但有时需要打印输出。可能是一个要发送给客户的 PDF 文档,或者是一个需要支持物理过程的纸质文档。为了解决这些问题,Odoo 应用程序支持打印业务报告。这些报告使用 QWeb 生成,然后导出为 PDF 文档,之后可以打印、发送电子邮件或简单地存储。

基于 QWeb 意味着可以重复使用用于看板视图和网页的相同技能来设计报告。除了 QWeb 之外,还使用特定的机制,例如报告操作、纸张格式以及可用于 QWeb 报告渲染的变量。

在本章中,将通过示例说明如何构建和向报告添加内容。通常的报告结构包括页眉详细信息页脚部分。可以添加的内容包括字段数据,包括图像等特定小部件。在报告中,通常还需要展示总计。所有这些内容将在本章中详细解释。

本章将涵盖以下主题:

  • 安装 wkhtmltopdf

  • 创建业务报告

  • 设计报告内容

  • 创建自定义报告

到本章结束时,您将熟悉创建 Odoo 报告所需的所有步骤,从报告操作到可以在 QWeb 模板上使用的特定技术。

技术要求

本章扩展了基于在第三章中首先创建的代码的现有library_app附加模块,即您的第一个 Odoo 应用程序。本章的代码可以在本书的 GitHub 存储库中找到,位于github.com/PacktPublishing/Odoo-15-Development-Essentialsch12/子目录中。

安装 wkhtmltopdf

Odoo 报告只是被转换为 PDF 文件的 HTML 页面。为此转换,使用了wkhtmltopdf命令行工具。其名称代表Webkit HTML to PDF

为了正确生成报告,需要安装推荐的wkhtmltopdf实用程序版本。已知某些版本的wkhtmltopdf库存在问题,例如无法打印页面页眉和页脚,因此我们需要对我们使用的版本进行挑剔。

自 Odoo 10 以来,版本 0.12.5 是官方推荐的版本。有关wkhtmltopdf的最新 Odoo 信息,可以在github.com/odoo/odoo/wiki/Wkhtmltopdf找到。

Debian 或 Ubuntu 提供的打包版本可能不合适。因此,建议直接下载并安装正确的包。下载链接可以在github.com/wkhtmltopdf/wkhtmltopdf/releases/tag/0.12.5找到。

要安装正确的wkhtmltopdf版本,请按照以下步骤操作:

  1. 首先,确保系统上没有安装不正确的版本,请输入以下命令:

    $ wkhtmltopdf --version
    
  2. 如果前面的命令报告的版本不是推荐的版本,则应卸载它。要在 Debian/Ubuntu 系统上这样做,请输入以下命令:

    $ sudo apt-get remove --purge wkhtmltopdf
    
  3. 接下来,您需要下载适合您系统的适当包并安装它。检查发布页面以获取正确的下载链接。在 Odoo 15 发布时,Ubuntu 20.04 LTS Focal Fossa 是最新的长期支持版本。对于 64 位架构,安装wkhtmltox_0.12.5-1.focal_amd64.deb包。在这种情况下要使用的下载命令如下:

    $ wget "https://github.com/wkhtmltopdf/wkhtmltopdf/releases/download/0.12.5/wkhtmltox_0.12.5-1.focal_amd64.deb" -O /tmp/wkhtml.deb
    
  4. 接下来,使用以下命令安装下载的包:

    $ sudo dpkg -i /tmp/wkhtml.deb
    

    这可能因为缺少依赖项而显示错误。在这种情况下,可以使用以下命令来修复:

    $ sudo apt-get -f install
    
  5. 最后,使用以下命令验证wkhtmltopdf库是否已正确安装并带有预期的版本号:

    $ wkhtmltopdf --version
    wkhtmltopdf 0.12.5 (with patched qt)
    

通过这样,您已成功安装了正确的wkhtmltopdf版本,并且现在 Odoo 服务器日志在启动过程中不会显示您需要 Wkhtmltopdf 以打印报告的 PDF 版本的信息消息。

现在您已经知道了如何下载和安装适合的wkhtmltopdf工具版本,让我们看看如何创建商业报告。

创建商业报告

对于图书馆应用来说,打印包含图书目录的报告将非常有用。这份报告应列出书名,以及如出版社出版日期作者等详细信息。

我们将在本章中实现这一点,并在过程中展示实现 Odoo 报告所涉及的几种技术。报告将被添加到现有的library_app模块中。

习惯上,报告文件应放在/reports子目录中,因此将添加一个reports/library_book_report.xml数据文件。像往常一样,在添加数据文件时,请记住在__manifest__.py文件的data键中声明它们。

要运行报告,我们首先必须添加报告操作。

添加报告操作

ir.actions.report XML 模型,可以通过使用设置 | 技术 | 操作 | 报告菜单选项进行检查。

Odoo 14 中的变化

Odoo 14 已弃用用于报告操作的<report>快捷标签。应使用<record model=""ir.actions.report">元素代替。

要添加报告操作并触发报告的执行,请编辑reports/library_book_report.xml文件,如下所示:

<odoo>
  <record id="action_library_book_report"
          model="ir.actions.report">
    <field name="name">Book Catalog</field>
    <field name="model">library.book</field>
    <field name="report_type">qweb-pdf</field>
    <field name="report_name">
      library_app.book_catalog</field>
    <field name="binding_model_id" 
      ref="model_library_book" />
    <field name="binding_type">report</field>
  </record>
</odoo>

此报告操作使此报告在 图书馆书籍 视图的顶部可用,在 打印 按钮旁边,紧挨着 操作 按钮:

图 12.1 - 打印上下文按钮

图 12.1 – 打印上下文按钮

这标志着向用户提供报告的第一步。

在之前的代码中使用的必要字段如下:

  • name 是报告操作的标题。

  • model 是报告基础模型的名称。

  • report_type 是要生成的文档类型。选项有 qweb-pdfqweb-htmlqweb-text

  • report_name 是用于生成报告内容的 QWeb 模板的 XML ID。与其他标识符引用不同,它必须是一个完整的引用,包括模块名称;即 <module_name>.<identifier_name>

    小贴士

    在报告开发过程中,将 report_type 设置为 qweb-html 允许您检查由 QWeb 模板生成的 HTML 结果,并且它还使解决问题更容易。完成此操作后,可以将其更改为 qweb-pdf

以下字段不是添加报告操作所必需的,但需要报告在 打印 菜单中显示,紧挨着 操作 菜单:

  • binding_model_id 是用于标识报告打印选项应可用的模型的单对多字段。

  • binding_type 应设置为 report

其他可选字段如下:

  • print_report_name 是一个 Python 表达式,用于提供报告的标题和文件名。object 变量可用,代表当前记录。

  • attachment 是一个 Python 表达式,您必须生成附件文件名。object 变量可用,代表当前记录。当设置时,生成的报告将作为附件存储。

  • attachment_use 当设置为 True 时,表示新的报告生成将重新打开存储的原始报告而不是重新生成它。

  • paperformat_id 是用于纸张格式的多对一字段。纸张格式包括页面大小和纵向或横向方向。

  • groups_id 是一个多对多字段,与可以使用报告的安全组相关联。

  • multi 当设置为 True 时,表示报告在表单视图中不可用。

由于引用的 QWeb 模板缺失,以下操作现在无法正常工作。我们将在以下章节中处理这个问题。

使用 QWeb 报告模板为每条记录的文档

Odoo 报表使用 QWeb 模板生成。QWeb 生成 HTML,然后可以将其转换为 PDF 文档。QWeb 指令和流程控制可以像往常一样使用,但应使用特定的容器以确保正确的页面格式。

以下示例提供了一个 QWeb 报告的最小可行模板。将以下代码添加到 reports/library_book_report.xml 文件中,紧接在我们之前添加的报告操作元素之后:

<template id="book_catalog"> 
  <t t-call="web.html_container">
    <t t-call="web.external_layout">
<t t-foreach="docs" t-as="o"> 
        <div class="page">
          <!-- Report content --> 
        </div>
      </t>
</t>
  </t>
</template>

这里最重要的元素是使用标准报告结构的t-call指令。web.html_container模板执行基本的设置以支持 HTML 文档。web.external_layout模板使用相应的公司设置处理报告的页眉和页脚。web.internal_layout模板可以作为替代使用,它只包含基本的页眉;更适合内部使用报告。

自 Odoo 11 以来的更改

在 Odoo 11 中,报告布局从report模块移动到了web模块。之前的 Odoo 版本使用report.external_layoutreport.internal_layout引用。从 Odoo 11 开始,这些需要更改为web.<...>引用。

docs变量代表用于生成报告的基本记录集。报告通常使用t-foreach QWeb 指令遍历每个记录。之前的报告模板为每个记录生成报告页眉和页脚。

注意,由于报告只是 QWeb 模板,因此可以应用继承,就像在其他视图中一样。用于报告的 QWeb 模板可以使用常规模板继承进行扩展——即使用XPath表达式——我们将在下一节讨论。

使用 QWeb 报告模板进行记录列表

在图书目录的情况下,有一个单独的报告文档,包含页眉和页脚,每个记录都有一个行或部分。

因此,报告模板需要根据以下代码进行调整:

<template id="book_catalog"> 
  <t t-call="web.html_container">
    <t t-call="web.external_layout">
      <div class="page">
        <!-- Report header content --> 
<t t-foreach="docs" t-as="o"> 
          <!-- Report row content --> 
        </t>
        <!-- Report footer content --> 
      </div> <!-- page -->
    </t>
  </t>
</template>

在之前的代码中,<div class="page">元素被移动到<t t-foreach="docs">之前,以便打印单个报告页眉和页脚,而单个记录将在同一文档内打印额外的内容。

现在我们有了基本的报告模板,我们可以自定义报告布局,这将在下一步进行。

选择报告布局

报告布局可以由用户自定义。只要它使用external_layout,这个设置就会应用于报告。

这些选项可以从设置 | 常规设置菜单中的公司 | 文档布局部分获得,如下面的截图所示:

图片

图 12.2 – 文档布局配置选项

在这里,配置文档布局按钮打开报告模板配置器,提供一些布局选项,并允许您选择公司标志、颜色或文本字体。

所选布局可以在设置中的布局字段进行设置,编辑布局将打开相应的视图表单,允许您直接自定义布局的 QWeb XML 定义。

现在您已经知道了如何设置通用报告布局,让我们看看如何处理页面格式。

设置纸张格式

Odoo 默认提供了一些页面格式,包括欧洲A4US Letter。还可以添加额外的页面格式,包括特定页面方向的格式。

纸张格式存储在 report.paperformat 模型中。可以使用 设置 | 技术 | 报告 | 纸张格式 菜单选项检查现有格式。

对于图书目录报告,将使用横版方向,并为此添加新的页面格式。

要添加 reports/library_book_report.xml 文件:

<record id="paperformat_euro_landscape" model="report.paperformat">
  <field name="name">A4 Landscape</field>
  <field name="format">A4</field>
  <field name="orientation">Landscape</field>
  <field name="margin_top">40</field>
  <field name="margin_bottom">32</field>
  <field name="margin_left">7</field>
  <field name="margin_right">7</field>
  <field name="header_line" eval="False" />
  <field name="header_spacing">35</field>
  <field name="dpi">90</field>
</record> 

这是欧洲 A4 格式的副本,由 base 模块定义,在 data/report_paperformat_data.xml 文件中,方向已从纵向改为横版。

现在可以使用此纸张格式进行报告。默认纸张格式在公司设置中定义,但报告可以设置特定的纸张格式以使用。这可以通过报告操作中的 paperfomat_id 字段来完成。

可以编辑报告操作以添加此字段:

  <record id="action_library_book_report"
          model="ir.actions.report">
    <field name="name">Book Catalog</field>
    <field name="model">library.book</field>
    <field name="report_type">qweb-pdf</field>
    <field name="report_name">
      library_app.book_catalog</field>
<field name="paperformat_id" 
      ref="paperformat_euro_landscape" />
  </record>

在报告的基本框架就绪后,现在是时候开始设计报告内容了。

设计报告内容

报告内容是用 HTML 编写的,并使用 Bootstrap 4 来帮助设计报告的布局。Bootstrap 在网络开发中被广泛使用。

小贴士

完整的参考可以在 getbootstrap.com 找到。

与看板视图不同,报告 QWeb 模板在服务器端渲染,并使用 Python QWeb 实现。因此,与 JavaScript QWeb 实现相比,有一些需要注意的差异。QWeb 表达式使用 Python 语法而不是 JavaScript 语法进行评估。

理解报告渲染上下文

表达式评估的服务端上下文与用于看板视图的客户端上下文不同。在报告模板中,以下变量可用:

  • docs 是一个可迭代的记录集合,用于渲染报告。

  • doc_ids 是要渲染报告的记录的 ID 列表。

  • doc_model 识别记录的模型;例如,library.book

  • user 是运行报告的用户记录。

  • res_company 是当前用户公司的记录。

  • website 是当前网站(如果有)的记录。这可能为 None

  • web_base_url 是 Odoo 服务器的基址。

  • time 是对 Python 的 time 库的引用。

  • context_timestamp 是一个函数,它接受一个 UTC 中的 datetime 对象并将其转换为用户的时区。

  • 这些值和 Python 库可以在模板内的代码表达式中使用。例如,要打印当前用户,我们可以使用以下命令:

    <span t-out="user.name" />
    

docs 值特别重要,因为它包含用于报告的数据。

现在你已经知道了如何访问报告的数据,下一步是添加报告内容。

添加报告内容

在基本 QWeb 模板就绪后,包括其标题、细节和页脚,你现在可以向其中添加内容。

这是渲染报告标题必须使用的 XML。它应该放在 <div class="page"> 节点内,并在 <t t-foreach=...> 元素之前:

<div class="page">
  <!-- Report header content -->
  <div class="container">
    <div class="row bg-primary">
      <div class="col-3">Title</div>
      <div class="col-2">Publisher</div>
      <div class="col-2">Date</div>
      <div class="col-3">Publisher Address</div>
      <div class="col-2">Authors</div>
</div>
    <t t-foreach="docs" t-as="o">
      <div class="row">
        <!-- Report row content -->
      </div>
    </t>
    <!-- Report footer content -->
  </div> <!-- container -->
</div> <!-- page -->

此内容布局使用 Bootstrap 4 网格系统,它是通过<div class="container">元素添加的。Bootstrap 有一个 12 列可用的网格布局。更多关于 Bootstrap 的详细信息可以在getbootstrap.com/docs/4.1/layout/grid找到。

Odoo 12 中的更改

Odoo 在 Odoo 11 之前使用 Bootstrap 3,并从 Odoo 12 开始使用 Bootstrap 4。Bootstrap 4 与 Bootstrap 3 不向后兼容。有关从 Bootstrap 3 到 Bootstrap 4 的更改提示,请参阅 Odoo 关于此主题的 wiki 页面:github.com/odoo/odoo/wiki/Tips-and-tricks:-BS3-to-BS4

之前的代码添加了一个带有列标题的表头行。之后,有一个t-foreach循环来遍历每条记录并为每条记录渲染一行。

接下来,重点将放在为每条记录渲染行上——在这种情况下,为目录中的每本书渲染一行。

使用<div class="row">元素添加行。行包含单元格,每个单元格可以跨越多个列,使行占据 12 列。每个单元格使用<div class="col-N">元素添加,其中N是它跨越的列数。例如,<div class="col-3">标题</div>是一个跨越三列的单元格。

QWeb 模板的渲染是在服务器端完成的,并使用记录集对象。因此,o.nameo记录的name字段获取值。并且很容易跟踪关系字段以访问其数据。例如,o.publisher_id.email从由publisher_id字段引用的合作伙伴记录中获取email字段。请注意,在客户端渲染的 QWeb 视图中,例如 Web 客户端看板视图,这是不可能的。

要为每条记录行添加内容,请在<div class="row">元素内添加以下 XML:

<!-- Report Row Content -->
<div class="row">
  <div class="col-3">
    <h4><span t-field="o.name" /></h4>
  </div>
  <div class="col-2">
    <span t-field="o.publisher_id" />
  </div>
  <div class="col-2">
    <span t-field="o.date_published"
          t-options="{'widget': 'date'}" />
  </div>
  <div class="col-3">
    <div t-field="o.publisher_id"
         t-options='{
           "widget": "contact",
           "fields": ["address", "email", "phone", 
             "website"], "no_marker": true}' />
  </div>
  <div class="col-2">
    <!-- Render Authors -->
  </div>
</div>

在前面的代码中,t-field属性被用来渲染字段数据。

t-options属性也可以用来为字段渲染提供额外的选项,例如要使用的小部件。

让我们更详细地看看字段小部件及其选项。

使用字段小部件

在模板中,字段值使用t-field属性进行渲染。这可以通过t-options属性来补充,以便您可以使用特定的小部件来渲染字段内容。

t-options使用类似于字典的数据结构设置。小部件键可以用来表示字段数据。

在前面的示例代码中,"widget": "contact"用于展示地址。它被用来渲染出版公司的地址o.publisher_idno_marker="true"选项被用来禁用一些默认显示的图标和contact小部件。

Odoo 11 中的更改

t-options属性是在 Odoo 11 中引入的,取代了之前 Odoo 版本中使用的t-field-options属性。

例如,假设doc代表一个特定的记录,渲染日期字段值看起来像这样:

<span t-field="doc.date_published" t-options="{'widget': 'date'}" />

支持的控件和选项的参考文档可以在www.odoo.com/documentation/15.0/developer/reference/frontend/javascript_reference.html#field-widgets找到。

小贴士

文档并不总是最新的,有关相应的源代码的更多详细信息可能可以在github.com/odoo/odoo/blob/15.0/odoo/addons/base/models/ir_qweb_fields.py找到。寻找继承自ir.qweb.field的类。get_available_options()方法提供了对支持选项的见解。

这样,我们已经添加了 QWeb XML 代码来渲染每本书的行。然而,authors列缺失。下一节将添加作者姓名,以及他们的图像,展示如何向报告中添加图像内容。

渲染图像

报告特征的最后一列应展示作者列表,以及他们的头像。头像图像可以使用t-field属性和image小部件来展示。

在最后一列中,添加以下代码:

<!-- Render authors -->
<ul class="list-unstyled">
  <t t-foreach="o.author_ids" t-as="author">
    <span t-field="author.image_128"
      t-options="{'widget': 'image', 
        'style': 'max-width: 32px'}" />
    <span t-field="author.name" />
  </t>
</ul>

在前面的代码中,有一个循环遍历author_ids多对多字段中的值。对于每个作者,你必须使用image小部件在image_128合作伙伴字段中渲染图像。

这样,你已经添加了标题和详情行。接下来的几节将处理报告页脚,它位于报告的末尾,在这个过程中将介绍报告总计。

计算总计

报告中常见的需求是提供总计。在某些情况下,模型中有字段计算这些总计,报告只需使用它们。在其他情况下,总计可能需要由报告来计算。

以为例,图书目录报告将在最后一行展示图书和作者的总数。

为了实现这一点,应在<t t-foreach="docs">元素的结束标签之后添加最后一行,以展示报告总计。

要做到这一点,请使用以下 XML 添加页脚内容:

<!-- Report footer content -->
<div class="row">
  <div class="col-3">
    <t t-out="len(docs)" /> Books
  </div>
  <div class="col-7" />
  <div class="col-2">
    <t t-out="len(docs.mapped('author_ids'))" /> Authors
  <div>
</div>

Python 函数len()用于计算集合中元素的数量。同样,总计也可以通过在值列表上使用sum()来计算。例如,以下列表推导式计算了一个总计金额:

<t t-out="sum([x.amount for x in docs])" />

这个列表推导式是对docs变量的循环,并返回一个包含每个记录的amount值的值列表。

你已经创建了报告的最后一个总计行。然而,在某些情况下,总计数不足以满足需求,需要累计总计。下一节将展示如何累计这些累计总计的值。

计算累计总计

在某些情况下,报告需要在迭代过程中执行计算——例如,为了保持累计总和,累计总和达到当前记录的总数。这种逻辑可以使用 QWeb 中的变量在每条记录迭代中累积值来实现。

为了说明这一点,您可以计算累积的作者数量。首先,在 docs 记录集的 t-foreach 循环之前初始化变量,使用以下代码:

<!-- Running total: initialize variable -->
<t t-set="missing_count" t-value="0" />

然后,在循环内部,将记录的作者数量添加到变量中。在展示作者列表之后立即这样做,并在每一行打印出当前的总数:

<!-- Running total: increment and present -->
<t t-set="missing_count"
   t-value=" missing_count + int(not o.publisher_id)" />
<p>(accum. <t t-out="missing_count"/>)</p>

上述代码可以添加到任何报告单元格中——例如,在出版社列单元格中。

这样,您已经添加了所有报告内容,包括报告总计。您还可以在报告中使用的一个功能是多语言支持。Odoo 支持此功能,下一节将解释如何使用它。

在报告中启用语言翻译

Odoo 用户界面使用当前用户选择的语言。在某些情况下,报告可能需要将其更改为特定语言。例如,使用客户语言打印文档可能比使用用户选择的语言更好。

在 QWeb 中,用于渲染模板的 t-call 指令后面可以跟一个 t-lang 属性,该属性对要使用的语言进行表达式评估。它应该评估为一个语言代码,例如 esen_US,通常是一个字段的表达式,其中可以找到要使用的语言。

为了展示这一点,图书馆应用将包括一个使用图书馆基础语言的图书目录报告版本,而不是用户语言。图书馆语言将是设置在公司合作伙伴记录上的语言。

对于这一点,现有的 book_catalog 模板可以被重用。它应该从另一个模板中调用,并且这个调用可以设置用于渲染过程的语言。

reports/library_book_report.xml 文件中,添加以下两个记录元素:

<record id="action_library_book_report_native"
        model="ir.actions.report">
  <field name="name">Native Language Book Catalog</field>
  <field name="model">library.book</field>
  <field name="report_type">qweb-pdf</field>
  <field name="report_name">
    library_app.book_catalog_native</field>
  <field name="binding_model_id" 
    ref="model_library_book" />
  <field name="binding_type">report</field>
  <field name="paperformat_id" 
    ref="paperformat_euro_landscape" />
</record>
<template id="book_catalog_native">
  <t t-call="library_app.book_catalog"
     t-lang="res_company.parter_id.lang" />
</template>

第一个记录添加了原生语言图书目录报告操作,该操作使用 library_app.book_catalog_native 模板来渲染报告。

第二个记录添加了报告模板。它是一个使用 t-call 渲染 book_catalog 模板并使用 t-lang 设置要使用语言的单一 QWeb 元素。

用于查找语言值的表达式是 res_company.parter_id.langres_company 变量是任何报告中可用的许多变量之一,是活动公司。公司有一个相关的合作伙伴记录 partner_id,合作伙伴有一个字段来存储语言,称为 lang

正在处理的报告基于一个记录集,例如 Books。但在某些情况下,需要使用的数据需要进行特定的计算。下一节将描述处理这些情况的选择。

完成此步骤后,最终的图书目录报告示例应如下所示:

图 12.3 – 最终的图书目录报告

本节涵盖了在 Odoo 中构建可打印报告的基本元素。进一步来说,高级报告可以使用特定逻辑来构建报告中使用的数据。下一节将讨论如何做到这一点。

创建自定义报告

默认情况下,将为所选记录生成报告,并通过docs变量在渲染上下文中可用。在某些情况下,准备任意数据结构以供报告使用是有用的。这可以通过自定义报告来实现。

自定义报告可以将所需的数据添加到报告渲染上下文中。这是通过一个具有特定名称的抽象模型来完成的,遵循report.<module>.<report-name>的命名约定。

此模型应实现一个_get_report_values()方法,该方法返回一个字典,其中包含要添加到渲染上下文中的变量。

例如,将一个按出版商的图书自定义报告添加到图书馆应用中。它将显示每个出版商出版的书籍。下面的截图显示了报告输出的示例:

图 12.4 – 按出版商的图书自定义报告示例

报告将在联系人列表中可用。可以选择一个或多个合作伙伴,如果有的话,报告将展示他们出版的标题。它也可以从出版者表单中运行,如下面的截图所示:

图 12.5 – 图书按出版商报告的打印菜单选项

此报告实现可以分为两个步骤。第一步是为报告准备数据的业务逻辑,第二步是报告布局的 QWeb 模板。

下一节将解释如何准备报告数据。

准备自定义报告数据

自定义报告可以使用由特定业务逻辑准备的数据,而不是简单地使用用户选择的记录集。

这可以通过使用抽象模型并遵循特定的命名约定来完成,该约定实现了一个_get_report_values()方法,用于返回一个包含报告模板将使用的变量的字典。

要将其实现为自定义报告,请添加reports/library_publisher_report.py文件,并包含以下代码:

from odoo import api, models
class PublisherReport(models.AbstractModel):
    _name = "report.library_app.publisher_report"
    @api.model
    def _get_report_values(self, docids, data=None):
        domain = [("publisher_id", "in", docids)]
        books = self.env["library.book"].search(domain)
        publishers = books.mapped("publisher_id")
        publisher_books = [
            (pub,
             books.filtered(lambda book: 
               book.publisher_id == pub))
            for pub in publishers
        ]
        docargs = {
            "publisher_books": publisher_books,
        }
        return docargs

为了使此文件被模块加载,还必须执行以下操作:

  • 添加reports/__init__.py文件,并包含from . import library_publisher_report行。

  • __init__.py文件的顶部添加from . import reports行。

该模型是一个AbstractModel,这意味着它没有数据库表示,也不存储数据。用于渲染的数据将由特定的业务逻辑计算得出。

报告模板标识名称将是publisher_report,因此模型名称应该是report.library_app.publisher_report

模型有一个名为_get_report_values@api.model装饰方法。docids参数是要打印报告的所选数字 ID 的列表。运行报告的基本模型是res.partner,因此这些将是合作伙伴 ID。

此方法使用特定的业务逻辑从所选的出版商中查找书籍并将它们按出版商分组。结果存储在publisher_books变量中,这是一个包含出版商记录和书籍记录集的成对列表;即[(res.partner(1), library.book(1, 2, 3))]

_get_report_values返回一个包含publisher_books键的字典,该键返回此数据结构。此键将在报告模板中作为变量可用,并可以在循环中进行迭代。

现在已经准备好了自定义报告数据,下一步是添加 QWeb 报告模板。

添加报告模板

下一步是创建用于渲染报告的 QWeb 模板。此模板与常规报告所做的工作类似。需要一个 XML 文件,以及报告操作和报告 QWeb 模板。唯一的不同是,此模板将使用_get_report_values方法返回的键/值对作为上下文变量,而不是使用docs上下文变量。

要实现报告操作和模板,请添加包含以下代码的reports/library_publisher_report.xml文件:

<odoo>
  <record id="action_publisher_report" model=
    "ir.actions.report">
    <field name="name">Books by Publisher</field>
    <field name="model">res.partner</field>
    <field name="report_type">qweb-pdf</field>
    <field name="report_name">
      library_app.publisher_report</field>
    <field name="binding_model_id" 
      ref="base.model_res_partner" />
    <field name="binding_type">report</field>
  </record>
  <template id="publisher_report">
    <t t-call="web.html_container">
      <t t-call="web.external_layout">
        <div class="page">
          <div class="container">
            <h1>Books by Publisher</h1>
            <t t-out="res_company" />
            <t t-foreach="publisher_books" t-as="group">
                <h2 t-field="group[0].name" />
                <ul>
                  <t t-foreach="group[1]" t-as="book">
                    <li>
                      <b><span t-field="book.name" /></b>
                      <span t-field="book.author_ids" />
                    </li>
                  </t>
                </ul>
            </t>
          </div>
        </div>
      </t>
    </t>
  </template>
</odoo>

之前的 XML 包括两个记录 - 一个用于添加“按出版商书籍”报告操作,另一个用于添加publisher_report报告模板。

当运行此报告时,报告引擎将尝试查找report.library_app. publisher_report模型。如果存在,就像这里的情况一样,将使用_get_report_values()方法向渲染上下文添加变量。

然后,QWeb 模板可以使用publisher_books变量来访问添加的数据。它是一个包含group[0]的列表,是用于组标题的出版商记录,而第二个元组元素group[1]是包含已发布书籍的记录集,使用第二个 for 循环展示。

记得还要在__manifest__.py模块中引用此 XML 文件。完成此操作后,一旦library_app模块已升级并且 Odoo 网络浏览器页面已重新加载,你将在打印上下文菜单中找到“按出版商书籍”报告,当在“联系人”列表中选择记录时。

摘要

在本章中,你学习了创建和添加自定义 Odoo 报告的基本技术。安装推荐的wkhtmltopdf实用程序版本对于确保报告正确渲染非常重要。你了解到报告是通过报告操作运行的,这些操作提供了渲染它们所需的基本信息。这可能包括要使用的纸张格式,你现在知道如何做到这一点。

你接下来学习的是报告设计,这可以通过 QWeb 模板来实现。正如你所意识到的,为此需要了解 QWeb、HTML 和 Bootstrap。在某些情况下,报告需要特定的业务逻辑来准备使用的数据。为此,你学习了如何创建自定义报告模型,以及如何使用它们的技巧。

打印报告可以作为商业应用的重要部分,因为它们通常需要作为向外部发送信息或支持仓库或商店地面上的物理流程的简单方式。本章为你提供了实现此类要求所需的工具和技术。现在,你可以确保你的商业应用不会辜负用户的需求。

在下一章中,我们将继续使用 QWeb,这次是用来构建网站页面。同时,也会解释 Web 控制器,它允许在 Odoo 网页上使用更丰富的功能。

进一步阅读

这额外的参考资料补充了本章中描述的主题。

相关的 Odoo 官方文档:

其他相关资源:

第十一章:第十三章:创建 Web 和门户前端功能

Odoo 是一个业务应用程序框架,提供快速构建应用程序所需的所有工具。统一的 Web 客户端提供了业务用户界面。但组织不能与世界隔离。能够与外部用户交互以支持高效流程是必要的。为此,Odoo 支持 Web 界面。

内部用户 Web 客户端有时被称为 后端,外部用户界面被称为 前端。前端提供 门户功能,可供门户用户登录访问。它还提供公共功能,无需登录即可访问,称为 网站功能

门户补充了后端应用程序,为外部用户提供自助服务功能,例如查看和支付他们的订单,或提交支持工单。

网站功能建立在 Odoo 内容管理系统CMS)之上,该系统允许您构建网页,包括易于使用的 拖放 网页设计工具。此外,网站功能还提供作为 模块 的附加功能,例如博客、在线招聘或电子商务。

在本章中,您将学习如何开发前端附加模块,利用 Odoo 提供的网站功能,同时讨论以下主题:

  • 介绍图书馆门户学习项目

  • 创建前端网页

  • 了解 Web 控制器

  • 添加门户功能

到本章结束时,您将学会如何使用 Web 控制器和 QWeb 模板创建动态网页,并将其集成到 Odoo 前端。此外,您还将学习如何利用 Odoo 门户模块,将其功能添加到其中。

技术要求

本章中的工作需要library_checkout附加模块,最后编辑于第十一章看板视图和客户端 QWeb。附加模块及其依赖代码可以在 Git 仓库 https://github.com/PacktPublishing/Odoo-15-Development-Essentials 中找到。本章中的代码可以在同一仓库中找到。

介绍图书馆门户学习项目

为了了解 Odoo 网页开发,将使用一个新的项目。图书馆应用程序可以使用自助服务功能为图书馆会员提供服务。会员可以被分配一个用户登录名以访问他们的图书借阅请求。

将为这些门户自助服务功能创建名为library_portal的附加模块。

首先要添加的文件是清单文件,library_portal/__manifest__.py,您可以使用以下代码创建它:

{
  "name": "Library Portal",
  "description": "Portal for library members",
  "author": "Daniel Reis",
  "license": "AGPL-3",
  "depends": [
      "library_checkout", "portal"
  ],
  "data": [
    "security/library_security.xml",
    "security/ir.model.access.csv",
    "views/checkout_portal_templates.xml",
  ],
}

该模块依赖于 library_checkout 来扩展其功能。它还依赖于 portal 模块,为门户功能提供基础。website 模块提供 CMS 功能,也可以用于网页开发。然而,portal 模块可以在不安装 Website 应用程序的情况下提供必要的前端功能。

data 键列出了三个要使用的 XML 文件。前两个与安全相关,为门户用户提供查看借阅请求所需的访问权限。最后一个 XML 文件将包含门户用户界面的 QWeb 模板。

为了使模块目录成为一个有效的 Python 模块,根据 Odoo 框架的要求,还需要一个空的 library_portal/__init__.py 文件。

现在新模块已经包含了必要的文件,下一步是添加使网页能够正常运行的必备组件。

创建前端网页

要开始学习 Odoo 网络开发的基础,将创建一个简单的网页。为此,需要两个组件:一个 网页控制器,当访问特定的 URL 时被触发,以及一个 QWeb 模板,用于生成由该 URL 展示的 HTML。

展示这个功能的网页是一个图书目录,是图书馆中图书的简单列表。图书目录页面将在 http://localhost:8069/library/catalog 可访问。

以下截图提供了一个示例,展示了应该看到的内容:

图 13.1 – 图书目录前端网页

图 13.1 – 图书目录前端网页

第一步是添加网页控制器,我们将在下一节中完成。

添加网页控制器

网页控制器是 Python 对象,用于实现网页功能。它们可以将 URL 路径链接到对象方法,这样当访问该 URL 时,就会执行该方法。

例如,对于 http://localhost:8069/library/catalog URL,访问的路径是 /library/catalog

一个 URL 路径,有时也称为 http.Controller 对象中的 @http.route 方法装饰器。

要为 /library/catalog 创建路由,执行以下步骤:

  1. 控制器 Python 代码将被添加到 controllers 子目录中。在 library_portal 模块目录中,编辑 __init__.py 文件以导入该子目录:

    from . import controllers
    
  2. controllers/__init__.py 文件添加到导入包含控制器代码的 Python 文件中,该文件将位于 main.py 文件中:

    from . import main
    
  3. 添加实际的控制器文件,controllers/main.py,包含以下代码:

    from odoo import http
    class Main(http.Controller): 
        @http.route("/library/catalog", 
          auth="public", website=True)
        def catalog(self, **kwargs):
            Book = http.request.env["library.book"]
            books = Book.sudo().search([])
            return http.request.render(
                "library_portal.book_catalog",
                  {"books": books},
            )
    

完成这些步骤后,控制器组件就完成了,并且能够处理 /library/catalog 路由的请求。

odoo.http 模块提供了 Odoo 网络相关的功能。负责页面渲染的网页控制器应该是继承自 odoo.http.Controller 类的对象。类名实际上并不重要。在之前的代码中,控制器类名为 Main()

Main() 类中的 catalog() 方法用 @http.route 装饰,将其绑定到一个或多个 URL 路由。在这里,catalog() 方法由 /library/catalog 路由触发。它还使用了 auth="public" 参数,这意味着此路由无需身份验证即可访问。website=true 参数意味着此页面将使用网页前端布局,并确保提供一些需要的附加变量。

注意

使用 website=True 不需要安装 网站 应用。它也适用于基本的 Odoo 前端网页。

这些 catalog() 路由方法预期会进行一些处理,然后返回 HTML 页面给用户的网络浏览器。

http.request 对象会自动设置与网络请求,并具有 .env 属性,用于访问 Odoo 环境。这可以用来实例化 Odoo 模型。示例代码就是这样来访问 library.book 模型,然后构建包含所有可用图书的记录集。

路由方法以登录用户或作为未登录时的公共特殊用户运行。由于公共用户访问权限非常有限,可能需要 sudo() 来确保可以检索要展示的数据。

最后一行返回 http.request.render() 的结果。这准备了一个要渲染的 QWeb 模板。两个参数是模板 XML ID,在本例中为 library_portal.book_catalog,以及一个字典,包含要提供给 QWeb 渲染上下文的变量。在这种情况下,一个 books 变量被提供,并设置为图书记录集。

注意

http.request.render() 函数返回一个 Odoo http.response 对象,包含有关如何渲染的指令。实际的 QWeb 模板到 HTML 的处理会延迟到所有网络控制器代码运行且响应准备好发送给客户端时。这允许扩展路由方法,例如,修改 qcontext 属性,该属性持有用于 QWeb 渲染的字典。

控制器已就绪,但所使用的 QWeb 模板需要在它能够工作之前创建。下一节将处理这一点。

添加 QWeb 模板

QWeb 模板是包含 HTML 代码和 QWeb 指令的 XML 片段,可以根据条件动态修改输出。图书目录网页需要一个 QWeb 模板来渲染要展示的 HTML。

要添加 library_portal.book_catalog QWeb 模板,执行以下步骤:

  1. 将使用一个新的 XML 数据文件,views/main_templates.xml,来声明模板。将其添加到 __manifest__.py 模块文件中的 data 键:

      "data": [
        "views/main_templates.xml",
      ]
    
  2. 添加包含 QWeb 模板的 XML 数据文件,views/main_templates.xml

    <odoo>
    <template id="book_catalog" name="Book List">
      <t t-call="web.frontend_layout">
        <t t-set="title">Book Catalog</t>
          <div class="oe_structure">
            <div class="container">
              <h1 class="h1-book-catalog">
                Book Catalog</h1>
              <table class="table">
                <thead>
                  <tr>
                    <th scope="col">Title</th>
                    <th scope="col">Published</th>
                    <th scope="col">Publisher</th>
                  </tr>
                </thead>
                <tbody>
          <t t-foreach="books" t-as="book">
            <tr scope="row">
              <td><span t-field="book.name" /></td>
              <td><span t-field="book.date_published" 
                  /></td>
              <td><span t-field="book.publisher_id" 
                  /></td>
            </tr>
          </t>
                </tbody>
              </table>
          </div>
        </div>
      </t>
    </template>
    </odoo>
    

这完成了使 QWeb 模板准备就绪所需的步骤。

之前的代码声明了book_catalog模板。它是一个 Bootstrap 表格,有三列。<thead>部分声明了列标题,而<t t-foreach> QWeb 指令为books记录集中的每本书渲染一个表格行。

注意

QWeb 模板是 XML。XML 语言比常规 HTML 有更严格的规则,例如,HTML 可以容忍未关闭的打开标签。在 XML 中,以及在 QWeb 模板中,这是不允许的。更准确地说,QWeb 模板遵循 XHTML 的要求。

在此模板中重要的是第一个指令<t t-call="web.frontend_layout">。这使得模板 HTML 被渲染为 Odoo 前端网页,包括页眉和页脚。为此布局被使用,控制器路由必须包含website=True参数。

提示

传入 QWeb 评估上下文的网站数据是由ir.ui.view模型的_prepare_qcontext方法设置的。例如,website模块在models/ir_ui_view.py文件中向其中添加变量。

<t t-set="title">也值得关注。它被前端布局用于设置浏览器标签页标题。

当我们有了控制器和 QWeb 模板,一旦library_portal模块安装或升级,使用 Web 浏览器打开http://localhost:8069/library/catalog应该会显示图书馆的书籍表格。

这些是实现前端网页的关键组件。请注意,网站应用程序可以用来提供更多前端功能,但不是必需的。

作为网页,它可能还需要使用额外的资产。下一节将解释这一点。

添加 CSS 和 JavaScript 资产

在设计网页时,HTML 代码通常辅以 CSS 或 JavaScript,这些最好作为额外的资产提供。

需要加载的资产在页面的头部部分声明。Odoo 有负责加载资产的特定 QWeb 模板。特别是,web.assets_backendweb.assets_frontend提供了专门用于后端 Web 客户端和前端网页的资产。web.assets_common提供了两者共有的资产。

要加载额外的资产,需要扩展适当的模板。

例如,在图书目录页面上,标题可以使用更大的字体大小来呈现。这可以通过在 CSS 文件中声明一个样式,然后在<h1>元素中使用它来实现。事实上,图书目录 QWeb 模板已经使用了<h1 class="h1-book-catalog">,应用了自定义样式。

要添加此自定义样式,请执行以下步骤:

  1. 使用以下内容创建static/src/css/library.css文件:

    .h1-book-catalog {
        font-size: 62px;
    }
    
  2. 此 CSS 必须由前端网页加载。为此,应扩展web.assets_frontend模板。在__manifest__.py文件中添加以下代码:

        "assets": {
            "web.assets_backend": {
                "library_portal/static/src/css/
                  library.css",
            }
        }
    

这描述了模块如何添加 Web 资产。这些资产通常是.js.css.scss文件。

Odoo 15 中的更改

Web 资源之前是通过 XML 文件添加的,扩展 QWeb 模板,如web.assets_backendweb.assets_frontend。在第十一章,“看板视图和客户端 QWeb”,在“添加 CSS 和 JavaScript 资源”部分提供了一个示例。

创建前端网页的基本方法已描述,涉及三个关键组件:Web 控制器、QWeb 模板和 Web 资源。

QWeb 模板及其语法已在第十一章,“看板视图和客户端 QWeb”,和第十二章,“使用服务器端 QWeb 创建可打印的 PDF 报告”中进行了详细描述。

但 Web 控制器值得更多关注,并且需要更深入地描述其功能。下一节将提供这些信息。

理解 Web 控制器

Web 控制器是服务器端组件,负责在访问 Odoo 网络路径时做出响应,通常触发网页的渲染。

网络路径,例如/library/catalog,被分配给一个路由,触发一个request对象,结果是一个包含返回给客户端的详细信息的response对象。

声明路由

使用http.route装饰器将方法分配给网络路径。以下是可用的参数:

  • route通常作为位置参数提供,是一个字符串,或字符串列表,包含映射的路径。方法参数可以从路径中提取。这些参数的表达式语法将在下一节中详细说明。

  • type,用于指定请求类型。默认为http,也可以设置为json

  • auth是所需的认证类型。它可以是一组中的userpublicnoneuser选项需要登录才能允许访问,public允许通过公共用户匿名访问,而none在特殊情况下很有用,例如不需要 Odoo 数据库的认证端点。

这些是可以在route装饰器上使用的参数。下一节将解释从主要参数中提取值以传递给装饰方法的语法。

从路由字符串中提取参数值

<type:name>。例如,<int:partner_id>提取一个整数值,并将其作为partner_id关键字参数传递给方法。也支持记录实例,使用model(<模型名称>)语法。例如,<model('res.partner'):partner>提取一个合作伙伴记录,并将其作为partner关键字参数传递给方法。

注意

更多关于路由路径格式的信息可以在官方 Werkzeug 文档中找到,网址为werkzeug.palletsprojects.com/routing/

URL 参数作为关键字参数传递给装饰方法。这些参数位于GET请求中的?字符之后,或通过POST请求提交。例如,将http://localhost:8069/mypage中的x设置为1y设置为2

小贴士

在路由方法中添加**kw通用关键字参数捕获可以防止在 URL 中添加意外参数时出错。例如,如果没有它,在方法参数中访问http://localhost:8069/library/catalogkw,它将被捕获在kw变量中,并且可以被方法代码忽略。

路由方法返回值可以是以下任何一种:

  • 一个假值,导致返回204 No Content HTTP 代码响应。

  • 一个文本字符串,用于返回包含该文本作为 HTML 内容的响应。

  • response对象通常使用render()方法创建。

接下来,让我们学习如何在路由方法中使用request对象。

使用请求对象

当客户端向 Odoo 服务器发出 Web 请求时,会自动实例化一个request对象。它通过导入odoo.http.request来提供。

以下是由此对象提供的最重要的属性:

  • env是 OdooEnvironment对象,类似于常规模型方法中的self.env提供的内容。

  • context是一个具有执行上下文的字典样式的Mapping对象,类似于模型方法上下文。

  • cr是 Odoo 数据库的 PostgreSQL 游标对象。

  • db是数据库名称。

  • session是一个存储会话详情的对象,包括身份验证。

  • params存储请求参数。通常它没有用,因为参数已经作为方法的参数提供。

  • csrf_token(time_limit=None)是一个用于生成当前会话 CSRF 令牌的方法。time_limit是令牌有效期的秒数。默认值None使其在整个会话期间有效。此属性用于设置 HTML 表单的 CSRF 令牌。

对于http类型请求,以下方法也是可用的:

  • make_response(data, headers=None, cookies=None)可以用来制作非 HTML 响应。

  • not_found(description=None)返回一个404 Not Found HTTP 代码。

  • render(template, qcontext=None, lazy=True, **kw)返回一个用于渲染的 QWeb 模板。实际的模板渲染被延迟到最终发送给客户端,因此可以通过继承方法进行修改。

请求对象提供了一种访问 Odoo 环境和客户端请求的所有信息的方式。接下来需要理解的相关对象是response,它将被发送回发起请求的客户端。

使用响应对象

response对象用于将最终的 HTTP 消息发送给客户端。当扩展路由方法时,可能需要修改由父super()方法返回的response

以下是在response对象上可用的内容:

  • template是用于渲染的模板名称。

  • qcontext是一个字典,包含用于模板渲染的数据。

  • uid是一个包含渲染模板的用户 ID 的整数。如果没有设置,则使用运行方法代码的当前用户。

  • render()是也在request对象中可用的相同渲染方法。

  • flatten()强制渲染模板。

响应对象也支持由父库werkzeug.wrappers.Response提供的参数。相应的文档可以在werkzeug.palletsprojects.com/wrappers/#werkzeug.wrappers.Response找到。

您现在对 Web 开发组件有了很好的了解。Odoo 还提供了一个用于与外部用户交互的门户,下一节将解释如何向其中添加功能。

添加门户功能

Odoo 门户功能使信息可供与外部用户交互。不同的应用程序可以为门户添加功能。例如,销售应用程序为顾客提供了检查订单甚至支付订单的能力。

需要创建门户用户,以提供对门户的访问权限。这可以在相应的联系记录的操作上下文菜单中完成,使用授予门户访问权限选项,如图图 13.2所示:

![图 13.2 – 联系记录上的“授予门户访问权限”选项]

![img/Figure_13.2_B16119.jpg]

图 13.2 – 联系记录上的“授予门户访问权限”选项

用户完成注册过程后,他们可以登录到 Odoo,并在点击右上角的用户名时看到我的账户选项。此选项打开门户主页,展示用户可用的所有文档的摘要。

可用的文档取决于已安装的应用程序。图 13.3显示了门户主页的示例外观:

![图 13.3 – 具有图书借阅功能的门户页面]

![img/Figure_13.3_B16119.jpg]

图 13.3 – 具有图书借阅功能的门户页面

library_portal模块将图书借阅项目添加到门户文档中,如图图 13.3所示。这是本节将要实施的结果。

这项工作将分为三个部分:访问安全、控制器和 QWeb 模板。以下每个部分都将解决这些步骤中的一个。您将首先设置门户访问安全配置。

配置门户用户的访问安全

在门户用户可以访问应用程序数据之前,必须向门户用户组base.group_portal group授予必要的访问权限。

在图书馆应用程序的情况下,门户用户应被授予对图书、成员、借阅和阶段模型的只读访问权限。此外,每个门户用户只能看到他们自己的成员记录和借阅。为此,需要添加访问权限和记录规则。

要配置门户用户的访问安全,请执行以下步骤:

  1. 创建security/ir.model.access.csv文件,向库模型添加读取访问权限,内容如下:

    id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
    access_book_portal,Book Portal Access,library_app.model_library_book,base.group_portal,1,0,0,0
    access_member_portal,Member Portal Access,library_member.model_library_member,base.group_portal,1,0,0,0
    access_stage_portal,Checkout Stage Portal Access,library_checkout.model_library_checkout_stage,base.group_portal,1,0,0,0
    access_checkout_portal,Checkout Portal Access,library_checkout.model_library_checkout,base.group_portal,1,0,0,0
    access_checkout_portal_line,Checkout Portal Line Access,library_checkout.model_library_checkout_line,base.group_portal,1,0,0,0
    
  2. 创建security/library_security.xml文件,使用记录规则限制用户可以访问的记录,内容如下:

    <odoo>
      <data noupdate="1">
        <record id="member_portal_rule" model="ir.rule">
          <field name="name">
            Library Member Portal Access</field>
          <field name="model_id" 
                 ref=
                   "library_member.model_library_member"/>
          <field name="domain_force"
            >[('partner_id', '=', 
             user.partner_id.id)]</field>
          <field name="groups" 
            eval="[(4,ref('base.group_portal'))]"/>
        </record>
        <record id="checkout_portal_rule" model="ir.rule">
          <field name="name">
            Library Checkout Portal Access</field>
          <field name="model_id"
             ref=
               "library_checkout.model_library_checkout"/>
          <field name="domain_force"
            >[('member_id.partner_id', '=', 
               user.partner_id.id)]</field>
          <field name="groups"
                 eval="[(4,ref('base.group_portal'))]"/>
        </record>
      </data>
    </odoo>
    
  3. 最后,将这些数据文件添加到模块__manifest__.py文件中的data键下:

      "data": [
        "security/ir.model.access.csv",
        "security/library_security.xml",
        "views/assets.xml",
        "views/main_templates.xml",
      ],
    

创建的记录规则根据当前用户合作伙伴记录user.partner_id.id应用过滤器。成员使用partner_id字段进行过滤,借阅使用member_id.partner_id字段进行过滤。

之后,以及模块升级后,门户用户将拥有使用图书馆门户页面的所需访问权限。

小贴士

通常情况下,Web 控制器通过使用sudo()来获取提升权限,从而避免添加访问权限的需求,这肯定能访问到数据。虽然方便,但sudo()的使用应谨慎考虑,并在可能的情况下避免使用。在模型层上实现访问安全,使用 ACLs 和记录规则,而不是依赖于控制器逻辑,更为安全。

配置必要的访问权限后,下一步是将结账项目添加到门户主列表中。

添加门户文档类型到主列表

访问“我的账户”页面会显示几种可用的文档类型,例如销售订单发票,以及每种类型的物品数量。

library_portal模块应将图书借阅选项添加到我的账户页面。为此,请执行以下步骤:

  1. 编辑controllers/__init__.py文件以导入包含控制器代码的 Python 文件,该文件位于portal.py文件中:

    from . import main
    from . import portal
    
  2. 添加控制器文件controllers/portal.py,包含以下代码:

    from odoo.http import route, request
    from odoo.addons.portal.controllers import portal
    class CustomerPortal(portal.CustomerPortal):
        def _prepare_home_portal_values(self, counters):
            values = super()
              ._prepare_home_portal_values(counters)
            if "book_checkout_count" in counters:
                count = 
               request.env[
                "library.checkout"].search_count([])
                values["book_checkout_count"] = count
            return values
    

    这扩展了由portal Odoo 模块提供的CustomerPortal控制器。之前的代码扩展了_prepare_home_portal_values()方法,负责计算文档计数器。它向结果值中添加了book_checkout_count键,并设置为结账计数。

  3. 添加 QWeb 模板文件views/portal_templates.py,包含以下代码:

    <odoo>
        <template id="portal_my_home" 
          inherit_id="portal.portal_my_home"
            name="Show Book Checkouts" priority="100"
            customize_show="True">
            <xpath expr="//div[hasclass('o_portal_docs')]"
                position="inside">
                <t t-call="portal.portal_docs_entry">
                    <t t-set="title">Book Checkouts</t>
                    <t t-set="url" 
                      t-value="'/my/book-checkouts'"/>
                    <t t-set="placeholder_count"
                       t-value="'book_checkout_count'"/>
                </t>
            </xpath>
        </template>
    </odoo>
    

    这扩展了portal.portal_my_home模板,负责渲染portal.portal_docs_entry模板,应使用以渲染每个文档项。它使用三个变量:title(标题),点击时导航到的url,以及placeholder_count(占位符计数),由_prepare_home_portal_values函数提供的计数器标识符。

  4. 最后,将新的数据文件添加到__manifest__.py

      "data": [
        "security/library_security.xml",
        "security/ir.model.access.csv",
        "views/assets.xml",
        "views/main_templates.xml",
        "views/portal_templates.xml",
      ],
    

之前的步骤添加了/my/book-checkouts页面,但尚未实现。下一节将以门户友好的方式完成此操作。

添加门户文档列表页面

“我的账户”主页列出了可用的各种文档类型。点击文档类型链接应打开可用的文档列表。

Figure 13.4展示了文档列表页面应该的样子:

Figure 13.4 – Portal document list page for book checkouts

Figure 13.4_B16119.jpg

Figure 13.4 – Portal document list page for book checkouts

门户提供了用于这些文档列表页面的基础功能,例如记录分页、过滤器以及排序选项。

之前的示例展示了如何在门户主页上添加文档类型。接下来,需要实现文档列表。继续使用上一节中的代码,需要进行以下两个步骤:

  1. 编辑控制器文件controllers/portal.py,添加/my/book-checkouts路由的代码,这将渲染my_book_checkouts模板。

  2. 编辑 QWeb 模板文件views/portal_templates.py,为图书借阅列表页面添加my_book_checkouts模板。

要添加到controllers/portal.py的代码如下:

    @route(
      ["/my/book-checkouts", "/my/book-checkouts/
          page/<int:page>"],
      auth="user",
      website=True,
    )
    def my_book_checkouts(self, page=1, **kw):
        Checkout = request.env["library.checkout"]
        domain = []
        # Prepare pager data
        checkout_count = Checkout.search_count(domain)
        pager_data = portal.pager(
            url="/my/book_checkouts",
            total=checkout_count,
            page=page,
            step=self._items_per_page,
        )
        # Recordset according to pager and domain filter
        checkouts = Checkout.search(
            domain,
            limit=self._items_per_page,
            offset=pager_data["offset"],
        )
        # Prepare template values and render
        values = self._prepare_portal_layout_values()
        values.update(
            {
                "checkouts": checkouts,
                "page_name": "book-checkouts",
                "default_url": "/my/book-checkouts",
                "pager": pager_data,
            }
        )
        return request.render(
            "library_portal.my_book_checkouts",
            values
        )

之前的代码为/my/book-checkouts/my/book-checkouts/page/路径添加了路由。第一个是默认使用的路由,第二个允许通过记录页面进行导航。

方法代码分为三个部分:

  • 第一段代码准备pager_data变量,该变量由模板用于渲染页面导航链接。它使用来自门户模块的pager()函数,负责准备这些数据。

  • 第二段代码创建了一个要使用的记录集,名为checkouts。它是通过使用之前定义的域过滤器和数据集来实现的。

  • 第三和最后一个代码部分准备values字典并渲染 QWeb 模板。值使用门户提供的_prepare_portal_layout_values()函数初始化,然后设置额外的数据键,包括分页数据。在checkouts数据键中设置要使用的记录集。

    提示

    门户页面也可以支持用户选择的排序顺序和过滤器。一个很好的例子是项目应用中实现的门户任务。检查相应的控制器和 QWeb 模板可以提供进一步指导,以将此功能添加到其他门户页面。

您已经添加了控制器代码,现在让我们添加以下代码的 QWeb 模板:

    <template id="my_book_checkouts" name=
      "My Book Checkouts">
      <t t-call="portal.portal_layout">
        <t t-if="checkouts" t-call="portal.portal_table">
          <thead>
            <tr>
              <th>Title</th>
              <th>Request Date</th>
              <th>Stage</th>
            </tr>
          </thead>
          <tbody>
            <tr t-foreach="checkouts" t-as="doc">
              <td>
                <a t-attf-href=
                  "/my/book-checkout/{{slug(doc)}}">
                  <span t-field="doc.name"/>
                </a>
              </td>
              <td>
                <span t-field="doc.request_date"/>
              </td>
              <td>
                <span t-field="doc.stage_id.name"
                      class="badge badge-pill badge-info"/>
              </td>
            </tr>
          </tbody>
        </t>
        <t t-else="">
          <div class="alert alert-warning" role="alert">
            There are no book checkouts.
          </div>
        </t>
      </t>
    </template>

之前的代码声明了my_book_checkouts QWeb 模板。它首先调用门户页面模板portal.portal_layout

然后,如果有记录需要渲染,它将准备一个 HTML 表格,调用portal.portal_table模板。

接下来,模板添加了表格的表头和主体。表格主体使用checkouts记录集的 for 循环来渲染每一行。

值得注意的是,每个记录名称上的<a>链接。在渲染借阅标题时,使用t-attf指令生成打开相应详情的链接。特殊的slug()函数用于为每个记录生成一个可读的标识符。

由于文档详情页面尚未实现,链接目前无法使用。下一节将完成这一工作。

添加门户文档详情页面

门户有一个主页,用户可以从主页导航到文档列表,然后打开特定的文档。可以通过/my/book-checkout/<id>路径访问特定的书籍借阅。

之前的章节实现了主页和文档列表功能。为了完成门户,应该实现文档详情页面。继续使用上一节的代码,需要两个步骤:

  1. 编辑控制器文件controllers/portal.py,为/my/book-checkout路由添加代码,渲染book_checkout模板。

  2. 编辑 QWeb 模板文件views/portal_templates.py,为书籍借阅列表页面添加book_checkout模板。

书籍借阅页面控制器的代码简单直接,没有带来任何新内容。如下所示:

    @route(
        ["/my/book-checkout/
           <model('library.checkout'):doc>"],
        auth="user",
        website=True,
    )
    def portal_my_project(self, doc=None, **kw):
        return request.render(
            "library_portal.book_checkout",
            {"doc": doc},
        )

之前的代码为/my/book-checkout/<id>路径添加了一个路由,该路由将<id>转换为library.checkout记录。此记录用作方法参数,通过doc变量名称捕获。

由于doc变量包含要使用的借阅记录,因此该方法只需要为它渲染 QWeb 模板,library_portal.book_checkout

用于 QWeb 模板的代码如下:

    <template id="book_checkout" name="Checkout Form">
      <t t-call="portal.portal_layout">
        <t t-call="portal.portal_record_layout">
          <t t-set="card_header">
            <div class="row">
              <div class="col">
                <h5 class="text-truncate" 
                  t-field="doc.name" />
              </div>
              <div class="col text-right">
                <span t-field="doc.stage_id.name"
                      class="badge badge-pill badge-info"
                      title="Current stage"/>
              </div>
            </div>
          </t>
          <t t-set="card_body">
            <!-- Member details -->
            <div class="row">
              <strong>Member</strong>
            </div>
            <div class="row">
              <div t-if="doc.member_id.image_1024"
                   class="col flex-grow-0">
<img class="rounded-circle mt-1 o_portal_contact_img"
  t-att-src="img/image_data_uri(doc.member_id.image_1024)"
  alt="Contact"/>
              </div>
              <div class="col pl-sm-0">
                <address t-field="doc.member_id"
                         t-options='{
                           "widget": "contact",
                           "fields": ["name", "email", 
                             "phone"]
                         }' />
              </div>
            </div>
            <!-- Checkout books -->
            <div class="row">
              <strong>Borrowed books</strong>
            </div>
            <div class="row">
              <div class="col">
                <ul>
                  <li t-foreach="doc.line_ids" t-as="line">
                    <span t-field=
                      "line.book_id.display_name" />
                  </li>
                </ul>
              </div>
            </div>
          </t>
        </t>
      </t>
    </template>

之前的代码创建了book_checkout QWeb 模板。再次强调,它首先调用门户页面模板,portal.portal_layout

然后,调用文档详细信息模板portal.portal_record_layout来准备详细内容。它使用以下两个 QWeb 变量,这些变量应该被设置:

  • card_header 设置用于页眉的 HTML。

  • card_body 设置用于文档详细信息的 HTML。

这个 HTML 添加了带有内容的行。有两个特别元素值得关注:

  • <img>元素,从数据字段添加图片

  • <address>元素,用于渲染合作伙伴记录的地址

当前实现缺少一个良好的可用性功能,即面包屑,允许用户通过链接导航回门户主页。下一节将展示如何添加此功能。

添加门户面包屑

门户页面支持面包屑,位于页面的顶部区域。默认情况下,有一个主页图标,允许用户快速导航回主页。当用户导航到文档列表,然后到特定文档时,这些选择可以添加到面包屑中。

Odoo 门户面包屑是通过portal.portal_breadcrumbs模板添加的。它应该扩展以添加特定文档类型的特定导航步骤。

要添加书籍借阅面包屑,编辑views/portal_templates.py文件,添加以下模板:

    <template id="portal_layout"
              name="Portal breadcrumb: book checkout"
              inherit_id="portal.portal_breadcrumbs">
      <xpath expr="//ol[hasclass('o_portal_submenu')]"
             position="inside">
        <li t-if="page_name == 'book-checkouts' or doc"
            class="col-lg-2"
            t-attf-class="breadcrumb-item
                         #{'active ' if not doc else ''}">
          <a t-if="doc"
             t-attf-href="/my/book-checkouts?{{ 
               keep_query() }}">
             Checkouts
           </a>
           <t t-else="">Checkouts</t>
        </li>
        <li t-if="doc" class="breadcrumb-item 
          active text-truncate
                              col-8 col-lg-10">
          <t t-esc="doc.name"/>
        </li>
      </xpath>
    </template>

之前的代码中的模板扩展了portal.portal_breadcrumbs模板。它通过添加带有o_portal_submenu类的<ol>元素,向其中添加面包屑<li>元素。

扩展添加了两个可能元素:一个用于结账文档列表,另一个用于特定的书籍结账。面包屑包含在所有门户页面上,这些添加的元素应该有条件地渲染,只有当它们对当前页面有意义时才渲染。

前面的章节指导你完成了向 Odoo 门户添加新功能的各个步骤,使外部用户能够与 Odoo 交互。

摘要

前端网页允许 Odoo 也为外部用户提供功能。这可以用来向公众展示通用信息,或者向门户用户提供个性化信息。前端网页特性是 Odoo CMS 的基础,由网站应用程序提供,以及用于电子商务等前端特性。

在本章中,你了解了构成前端网页特性、网页控制器和 QWeb 模板的核心技术组件。网页控制器实现路由,当访问特定 URL 路径(称为路由)时触发,并运行任何特定的业务逻辑。QWeb 模板接收由网页控制器准备的数据,并在 QWeb 模板引擎的帮助下渲染 HTML 输出。

你现在知道如何使用这些组件来实现一个与 Odoo 前端集成的公共网页,包括使用你自己的网页资源。你还知道如何利用 Odoo 门户的基本要素为外部用户提供自助服务功能。

本章完成了你在 Odoo 框架中各个组件的旅程。模型是其他组件围绕构建的中心元素。Odoo 基础模块提供了一些开发人员应该熟悉的必要模型。下一章将承担提供这些模型概述的任务。

进一步阅读

这些是补充本章讨论主题的额外参考资料,可在官方 Odoo 文档中找到:

你可以在 Packt Publishing 技术页面上找到额外的 Bootstrap 学习资源:www.packtpub.com/tech/Bootstrap

第五部分:部署和维护

最后,第五部分涵盖了部署和维护实践。在生产部署时,需要考虑一些特殊因素,例如在 Odoo 服务和网络之间配置反向代理。还包括一个附加的参考章节,概述了 Odoo 基础关键模型。

在本节中,包括以下章节:

  • 第十四章**,理解 Odoo 内置模型

  • 第十五章**,部署和维护生产实例

第十二章:第十四章:理解 Odoo 内置模型

当创建一个新的数据库时,会填充一个初始数据模型,提供可用于Odoo 应用的基本实体。本章确定了最相关的基本实体,并解释了如何从用户界面UI)检查它们,以及它们的作用。

虽然这种理解对于您能够开发 Odoo 应用不是必需的,但它将为理解 Odoo 框架的核心概念提供一个坚实的基础,并帮助利用技术菜单来解决更复杂的需求或问题。

本章讨论以下主题:

  • 理解联系人数据模型

  • 理解用户和公司数据模型

  • 理解安全相关信息库

  • 理解数据库结构模型

  • 理解 UI 相关的信息库

  • 理解配置属性和公司参数

  • 理解消息数据模型

到本章结束时,您将能够使用技术菜单来检查 Odoo 框架中最相关的内部数据记录,帮助您进行问题分析和解决。

在本章中,展示了简化的实体-关系图ERDs),让您能够可视化核心模型之间的关系,从而更深入地理解如何在您的业务应用中使用这些模型。

技术要求

要跟随本章内容,您只需要对 Odoo 15 实例拥有管理员访问权限,并在设置 | 技术菜单中启用开发者模式。要跟随联系人数据模型部分,必须安装联系人应用,要跟随消息数据模型部分,必须安装讨论应用。

理解联系人数据模型

资源模型在其技术标识符ID)上携带 res. 前缀。它们包含 Odoo 的基本主数据,如用户、公司、货币。

Odoo 的一个中心模型是 res.partner。它用于任何需要表示地址、个人或组织的地方。例如,客户、供应商、联系人、开票或发货地址、员工和申请人。它还用于补充用户和配置公司的联系数据。

虽然 res.partner 模型由 Odoo 基础模块提供,无需安装特定应用,但要使相应的菜单可用,必须安装联系人应用。这些是与联系人相关的模型:

  • res.bank 存储银行识别数据,因为很明显,没有银行参与很难进行商业活动。银行数据可以从联系人 | 配置 | 银行账户 | 银行菜单选项进行浏览。

  • res.partner.bank,包含银行账户详情。银行账户与res.partner相关联,不出所料,也引用了它们相关的res.bank银行。银行账户可以在联系人 | 配置 | 银行账户 | 银行账户中浏览。

  • res.partner.industry,是一个高级经济活动列表。它用 NACE 代码填充。NACE,即经济活动命名法,是欧洲经济活动的统计分类。该列表可以在联系人 | 配置 | 行业中找到。

  • res.country,列出世界国家,并包括有用的数据,如两位数的res.partner模型。国家列表可以在联系人 | 配置 | 本地化 | 国家中浏览。

  • res.country.state,列出国家州和类似的行政区域。该列表默认填充,数据可以在联系人 | 配置 | 本地化 | 二月州中查看。

  • res.country.group,允许我们定义国家组。Odoo 提供的默认组包括欧洲、单一欧元支付区SEPA)国家,和南美洲。根据需要,可以在联系人 | 配置 | 本地化 | 国家组中添加其他组。

  • res.currency,包含货币列表,当启用多货币时相关。该列表由 Odoo 预填充,相关货币应设置为active。访问该列表的菜单选项位于发票/会计应用中(如果启用了多货币),在相应的配置 | 会计 | 货币菜单选项中。

下面的图表提供了这些模型及其关系的概述:

图 14.1 – 联系人数据模型

图 14.1 – 联系人数据模型

备注

这里提供的数据模型图是简化的实体关系图(ERD)。简化的一部分是用简单的箭头替换了可能对许多人来说不熟悉的鸟脚符号。箭头代表多对一关系。双向箭头代表多对多关系。虚线代表软关系,这些关系不使用数据库 ID 或数据库外键FK)。模型名称使用单数形式,这是 ERD 的惯例,即使 Odoo 模型的名称可能使用复数形式。

其他资源模型是用户和公司,将在下一节中描述。

理解用户和公司数据模型

用户和公司是 Odoo 数据模型的核心元素。它们可以在设置 | 用户与公司菜单中访问。可用的菜单选项在此列出:

  • res.users,存储系统用户。这些用户在partner_id字段中有一个隐式的合作伙伴记录,其中存储了名称、电子邮件、图像和其他联系详情。

  • res.group,存储安全访问组。此菜单仅在启用开发者模式时可用。属于某个组的用户将获得该组的权限。组可以继承其他组,这意味着它们也将提供这些继承组的权限。

  • res.company,存储组织的详细信息以及特定公司的配置。它有一个隐式的合作伙伴记录,包含地址和联系详情,存储在partner_id字段中。新数据库提供了一个默认公司,其base.main_company可扩展标记语言XML)ID。

以下图表提供了这些模型之间关系的概览:

图 14.2 – 用户和公司数据模型

图 14.2 – 用户和公司数据模型

用户和访问组模型是 Odoo 访问安全定义的基础。下一节将详细介绍这些模型,可通过技术菜单访问。

理解安全相关信息存储库

Odoo 用户通过访问组获得对功能的访问权限。这些访问组包含它们提供的权限的定义。最相关的访问模型在此列出:

  • res.users,是 Odoo 系统用户。

  • res.group,是访问组。用户属于一个或多个组,每个组授予一定的权限。

  • ir.model.access,授予一个模型创建-读取-更新-删除CRUD)权限。

  • ir.rule,授予一个组在模型记录子集上的 CRUD 权限,这些记录由域表达式定义。例如,使用常规访问权限,您可以授予写入权限,然后记录规则可以限制某些记录为只读。

以下图表提供了数据模型这一部分的简化视图:

图 14.3 – 安全相关数据模型

图 14.3 – 安全相关数据模型

您已经了解了用户、访问组、合作伙伴和访问权限的数据模型,它们之间都有紧密的联系。在下一节中,您将继续进一步了解数据库结构定义,例如模型和字段。

理解数据库结构模型

信息存储库(ir.)模型描述了 Odoo 内部配置,如模型、字段和 UI。这些定义可以在设置 | 技术菜单下访问。

可以通过设置 | 技术 | 数据库结构菜单找到与数据模型相关的信息存储库。在该菜单中最相关的选项之后,我们有以下设置:

  • decimal.precision,用于配置不同用例的精度数字位数,例如产品价格。

  • ir.model,描述了 Odoo 安装的数据模型,这些模型大多数情况下映射到存储数据的数据库表。使用开发者菜单中的查看元数据选项查找模型的 XML ID 很有用。在应用中字段也有助于找出参与模型数据结构定义的模块。

  • ir.model.field,存储在数据库中定义的模型字段。此列表可以通过设置 | 技术 | 数据库结构 | 字段菜单访问,或通过开发者菜单中的查看字段选项访问。

  • ir.attachment,是用于存储附件文件的模型。它是 Odoo 跨应用使用的单一存储位置。

设置 | 技术 | 序列与标识符菜单包括与数据记录 ID 相关的模型,并包含以下设置:

  • ir.model.data,是存储外部 ID 的地方,也称为 XML ID。它们将数据库实例无关的 ID 名称映射到数据库实例特定的 ID 键。它们可在设置 | 技术 | 序列与标识符 | 外部标识符中访问。

  • ir.sequence,描述了用于自动编号分配的序列,例如在销售订单库存转移上。

以下图表展示了这些模型之间的高层次关系:

![图 14.4 – 数据库结构数据模型图片

图 14.4 – 数据库结构数据模型

您已经了解了用于保持 Odoo 模型定义的关键模型。接下来,我们将讨论下一节中的表示层定义。

理解与 UI 相关的信息存储库

UI 元素,如菜单和视图,存储在信息存储库模型中。对应的数据可以通过设置 | 技术 | 用户界面菜单访问。其中最相关的选项如下列出:

  • ir.ui.menu,定义菜单选项。这些形成一个层次树,叶项可以触发动作,然后通常提供显示视图组合的说明。

  • ir.ui.view,存储视图定义及其扩展。视图类型包括表单、列表、看板和 QWeb(用于报告和网页模板)。

设置 | 技术 | 动作菜单下,您可以找到这些 UI 元素的定义。其中最相关的选项如下列出:

  • ir.actions.actions,是其他动作类型从中派生的基本模型。通常,您不需要直接处理它。

  • ir.actions.report,是打印报告的动作。它们将具有相关的 QWeb 视图,提供报告定义,用于生成可以转换为便携式桌面格式PDF)格式的超文本标记语言HTML)报告。

  • ir.actions.act_window,用于展示视图的组成,可能是最常用的动作类型。最简单的视图组合是列表视图和表单视图。

  • ir.actions.server 用于运行服务器进程,例如创建或修改记录、发送电子邮件,甚至运行 Python 代码。

以下图表提供了先前模型及其关系的简化视图:

![图 14.5 – 操作和 UI 数据模型图 14.5 – B16119.jpg

图 14.5 – 操作和 UI 数据模型

通过本节,你应该对定义 Odoo UI 相关的几个元素以及如何使用技术菜单来检查它们有了更好的理解。在下一节中,你将介绍用于全局配置参数和公司相关数据的技術模型。

理解配置属性和公司参数

技术选项中的另一个重要菜单是 设置 | 技术 | 用户参数。在那里你可以找到两个选项:系统参数公司属性

ir.config_parameter 存储全局配置选项。其中一些是默认值,可以调整,而其他一些则在 web.base.url 选项中选择某些选项时设置。该选项存储 Odoo 服务器的 统一资源定位符URL)并可用于在电子邮件模板中创建链接。

ir.property 是存储多公司字段数据的地方。某些字段可以根据活动公司具有不同的值。这些也被称为 属性字段

例如,对于客户相关的合作伙伴字段 property_account_receivable_id) 和对于供应商相关的 property_account_payable_id),它们都是属性字段。

由于相同的字段名可以持有不同的值,这取决于活动公司,它可以是常规数据库文件。这就是 ir.property 模型作为存储这些值的地方出现。

此模型具有以下字段:

  • property_account_receivable_id

  • ir.model.fields 记录。

  • FloatMany2one

  • res.partner,62 表示数据库 ID 为 62 的 res.partner 记录的引用。

  • 公司:此值有效的公司。

  • account.account,813 表示 ID 为 813 的会计科目表。

    小贴士

    资源字段是可选的。如果为空,它将用作该公司的新的记录的默认值。这用于此处作为示例的 应收账款应付账款 字段。

理解这些公司属性和参数的相关性对于高级配置调整可能很有用,例如调整用于公共 Web URL 的默认值或定义多公司字段的默认值。下一节将继续技术菜单探索之旅,这次将涵盖与消息相关的模型。

理解消息数据模型

你可能需要与之合作的相关技术领域是许多表单中发现的 Chatter 小部件所使用的消息相关模型。这些功能由 mail 提供,因此需要在以下菜单项可用之前安装。

相关的技术模型可以在设置 | 技术 | 讨论菜单中找到。那里找到的最重要选项如下列所示:

  • mail.message存储每个消息。它通过邮件线程抽象模型与一个资源(模型中的一个特定记录)相关联。

  • mail.message.subtype用于每个消息。基本子类型包括笔记,用于内部讨论,讨论,用于外部消息,以及活动,用于计划的活动。这些子类型适用于任何模型。其他子类型,通常是模型特定的,可以添加以识别不同的事件。这允许配置默认订阅,决定哪些事件应该触发向哪些订阅者发送通知。

  • mail.tracking.value存储跟踪字段的字段值变更日志。要跟踪一个字段,请检查tracked=True字段属性。这些变更日志在聊天消息中呈现,因此跟踪值与聊天消息相关联。

  • mail.activity存储记录的单独活动。活动混合抽象增加了其他模型链接到活动的能力,类似于邮件线程对消息所做的那样。

  • mail.activity.type是可配置的活动类型,例如电子邮件电话会议待办事项

  • mail.followers存储每个消息线程的订阅者列表。每个订阅者记录还有一个它已订阅的子类型列表。每当添加包含这些子类型之一的任何新消息时,订阅者将收到通知。

    小贴士

    在一些具有严格控制策略的环境中,数据访问和变更日志是重要功能。作为开箱即用的跟踪功能的替代方案,可以使用审计日志社区模块。您可以在odoo-community.org/shop/product/audit-log-533找到它。

以下图表提供了这些模型及其关系的概览:

图 14.6 – 消息和活动数据模型

图 14.6 – 消息和活动数据模型

大多数时候,你会使用一些精选的应用程序编程接口API)方法来创建消息和活动,而不需要深入了解相应数据是如何存储的。

对于一些复杂情况,对底层数据模型有良好的理解可能很有价值。特别是,了解消息子类型和它们对订阅者的订阅情况,对于精细控制通知非常有用。这完成了我们对 Odoo 框架最重要的技术模型的概述。

摘要

在本章中,你了解了 Odoo 框架的内部结构,这些结构由信息仓库(ir)和资源(res)模型提供。

Contacts 模型对于在 Odoo 中存储所有人员和地址数据至关重要,安装 Contacts 应用程序为该模型和相关数据添加了用户界面。了解公司联系人可以有子联系人和地址对于有效地使用 Odoo 至关重要。Settings 应用程序中的 Users & Companies 菜单也被讨论,以介绍 UsersAccess GroupsCompaniesAccess Groups 在此处的作用是授予 Users 访问权限是一个关键思想。剩余的相关元素在 Settings 应用程序的 Technical 菜单中公开。现在让我们回顾一下本章中提到的关键思想。

从菜单的顶部开始,Discuss 子菜单包含消息和活动数据模型,一个关键思想是使用子类型来控制自动通知。Actions 菜单暴露了在菜单项和上下文菜单中使用的操作,并用于展示视图、打印报告或在服务器上执行代码。User Interface 菜单介绍了利用 ActionsMenu Items,以及用于存储后端视图和前端 HTML 模板的 Views 菜单选项。菜单中的下一个是 Database Structure 子菜单。在这里,用于描述所有 Odoo 数据结构的模型都是可用的。这些模型在应用程序开发过程中的多个地方被引用,例如在视图定义或模型扩展中。与模型密切相关的是 Security 定义,它授予访问组成员读取或写入模型或模型中特定记录域的访问权限。

虽然没有审查每个技术菜单选项,但最相关的选项已被展示,并应提供对 basemail 模块下数据结构的坚实基础理解。您在 Odoo 开发领域的旅程即将结束。现在您已经拥有了开发业务应用程序所需的所有工具和技能,所缺少的最后一部分是将它们部署并使其对最终用户可用。

Odoo 项目的最后一英里是将我们的工作部署到实际使用中。与开发安装相比,为生产环境安装 Odoo 有额外的要求。下一章将指导您设置 Odoo 生产安装,避免最常见的陷阱。

第十三章:第十五章:部署和维护生产实例

在本章中,您将学习如何为生产环境准备 Odoo 服务器的基础知识。

设置和维护服务器本身就是一个非平凡的话题,应由专业人士完成。这里提供的信息不足以确保普通用户能够创建一个能够承载敏感数据和服务的弹性且安全的环境。

本章的目标是介绍最重要的配置方面和针对 Odoo 部署的最佳实践。这将帮助系统管理员为他们的 Odoo 服务器主机做好准备。

您将首先设置主机系统,然后安装 Odoo 的先决条件和 Odoo 本身。Ubuntu 是云服务器的流行选择,这里将使用它。然后,需要准备 Odoo 配置文件。到目前为止,设置与开发环境使用的设置类似。

接下来,需要将 Odoo 配置为系统服务,以便在服务器启动时自动启动。

对于托管在公共云上的服务器,Odoo 应通过 HTTPS 提供服务。为此,您将学习如何使用自签名证书安装和配置 Nginx 反向代理。

最后一个部分讨论了如何执行服务器升级并准备一个预演环境,以便在实际更新应用之前进行测试运行。

本章讨论的主题如下:

  • 准备主机系统

  • 从源代码安装 Odoo

  • 配置 Odoo

  • 将 Odoo 配置为系统服务

  • 设置 Nginx 反向代理

  • 配置和强制执行 HTTPS

  • 维护 Odoo 服务和模块

到本章结束时,您将能够设置一个相当安全的 Odoo 服务器,这对于低调的生产使用已经足够好。然而,本章中给出的食谱并不是部署 Odoo 的唯一有效方法——还有其他方法也是可能的。

技术要求

要跟随本章,您需要一个干净的 Ubuntu 20.04 服务器——例如,一个在云上托管的 虚拟专用服务器VPS)。

本章中使用的代码和脚本可以在 github.com/PacktPublishing/Odoo-15-Development-EssentialsGitHub 仓库的 ch15/ 目录中找到。

准备主机系统

Odoo 通常部署在基于 DebianLinux 系统上。Ubuntu 是一个流行的选择,最新的 长期支持LTS)版本是 20.04 Focal Fossa

其他 Linux 发行版也可以使用。在商业领域,CentOS/Red Hat Enterprise LinuxRHEL)系统也很受欢迎。

安装过程需要提升访问权限,使用root超级用户或sudo命令。在 Debian 发行版中,默认登录是root,它具有管理访问权限,命令提示符显示#。在 Ubuntu 系统上,root账户被禁用。相反,在安装过程中配置了初始用户,并且是sudo命令来提升访问权限并使用root权限运行命令。

在开始 Odoo 安装之前,必须在主机系统上安装系统依赖,并创建一个特定的用户来运行 Odoo 服务。

下一个部分解释了在 Debian 系统上所需的系统依赖。

安装系统依赖

当从源运行 Odoo 时,需要安装一些系统依赖。

在开始之前,更新软件包索引并执行升级以确保所有已安装的程序都是最新的,这是一个好习惯,如下所示:

$ sudo apt update
$ sudo apt upgrade -y

接下来,可以安装PostgreSQL数据库。我们的用户应该被设置为数据库超级用户,以便他们能够获得对数据库的管理访问权限。以下是这些命令:

$ sudo apt install postgresql -y
$ sudo su -c "createuser -s $USER" postgres

注意

Odoo 可以使用安装在它自己的服务器上的现有 PostgreSQL 数据库。如果是这种情况,则不需要在 Odoo 服务器上安装 PostgreSQL 服务,并且应在 Odoo 配置文件中设置相应的连接详情。

这些是运行 Odoo 所需的 Debian 依赖项:

$ sudo apt install git python3-dev python3-pip \
python3-wheel python3-venv -y
$ sudo apt install build-essential libpq-dev libxslt-dev \
libzip-dev libldap2-dev libsasl2-dev libssl-dev

为了拥有报表打印功能,必须安装wkhtmltox。对于 Odoo 10 及以后的版本,推荐的版本是 0.12.5-1。下载链接可以在github.com/wkhtmltopdf/wkhtmltopdf/releases/tag/0.12.5找到。Ubuntu 的代号对于 18.04 版本是bionic,对于 20.04 版本是focal

以下命令为 Ubuntu 20.04 Focal 版本执行此安装:

$ wget "https://github.com/wkhtmltopdf/wkhtmltopdf\
/releases""/download/0.12.5/\
wkhtmltox_0.12.5-1.focal_amd64.deb" \
-O /tmp/wkhtml.deb
$ sudo dpkg -i /tmp/wkhtml.deb
$ sudo apt-get -fy install  # Fix dependency errors

软件包安装可能会报告缺少依赖项错误。在这种情况下,最后一个命令将强制安装这些依赖项并正确完成安装。

接下来,你将创建一个系统用户用于 Odoo 进程。

准备专用系统用户

一个好的安全实践是使用一个专用用户来运行 Odoo,该用户在主机系统上没有特殊权限。

对于用户名的一个流行选择是odoo。这是创建它的命令:

$ sudo adduser --home=/opt/odoo --disabled-password \
--gecos "Odoo" odoo

Linux 系统用户可以有一个目录。对于 Odoo 用户来说,这是一个方便的地方来存储 Odoo 文件。这个选择的流行选项是/opt/odoo。自动使用的--home选项会创建这个目录并将其设置为odoo用户的家目录。

此用户目前还没有访问 PostgreSQL 数据库的权限。以下命令添加了这种访问权限并为它创建数据库以初始化 Odoo 生产环境:

$ sudo su -c "createuser odoo" postgres
$ createdb --owner=odoo odoo-prod

在这里,odoo 是用户名,odoo-prod 是支持我们的 Odoo 实例的数据库名称。odoo 用户被设置为 odoo-prod 数据库的所有者。这意味着它对该数据库具有 创建和删除 权限,包括删除它的能力。

小贴士

要运行,Odoo 不需要使用数据库的特权权限。这些权限可能仅在某些维护操作中需要,例如安装或升级模块。因此,为了提高安全性,Odoo 系统用户可以是非所有者数据库用户。请注意,在这种情况下,维护应使用与数据库所有者不同的用户运行 Odoo。

要使用 Odoo 系统用户启动会话,请使用以下命令:

$ sudo su - odoo
$ exit

这将用于以 Odoo 用户身份运行安装步骤。完成后,exit 命令将终止该会话并返回到原始用户。

在下一节中,我们将继续安装 Odoo 代码和 /opt/odoo 目录。

从源代码安装 Odoo

虽然 Odoo 提供了 Debian/Ubuntu 和 CentOS/RHEL 系统包,但由于其提供的灵活性和控制,从源代码安装是一个流行的选项。

使用源代码可以更好地控制部署的内容,并在生产环境中更容易地管理更改和修复。例如,它允许我们将部署过程与 Git 工作流程相关联。

到目前为止,Odoo 的系统依赖项已经安装,数据库已准备好使用。现在,可以下载并安装 Odoo 源代码,以及所需的 Python 依赖项。

让我们看看如何下载 Odoo 源代码。

下载 Odoo 源代码

总有一天,您的服务器将需要升级和补丁。在需要的时候,版本控制仓库可以提供极大的帮助。我们使用 git 从仓库获取代码,就像我们在安装开发环境时做的那样。

接下来,我们将模拟 odoo 用户,并将代码下载到其主目录中,如下所示:

$ sudo su - odoo
$ git clone https://github.com/odoo/odoo.git \
/opt/odoo/odoo15 \
-b 15.0 --depth=1

-b 选项确保我们获取正确的分支,而 --depth=1 选项仅检索最新的代码修订版,忽略(长)变更历史,使下载更小、更快。

小贴士

Git 是管理 Odoo 部署代码版本的重要工具。如果您不熟悉 Git,值得了解更多关于它的信息。一个好的起点是 git-scm.com/doc

自定义模块通常也会使用 Git 管理,并且也应该克隆到生产服务器上。例如,以下代码将库自定义模块添加到 /opt/odoo/odoo15/library 目录:

$ git clone https://github.com/PacktPublishing/Odoo-15-Development-Essentials/opt/odoo/library

Odoo 源代码已位于服务器上,但还不能运行,因为所需的 Python 依赖项尚未安装。让我们在下一节中安装这些依赖项。

安装 Python 依赖项

下载 Odoo 源代码后,应安装 Odoo 所需的 Python 包。

其中许多也都有 Debian 或 Ubuntu 系统包。官方 Odoo Debian 安装包使用它们,依赖包的名称可以在 Odoo 源代码的 debian/control 文件中找到:github.com/odoo/odoo/blob/15.0/debian/control

这些 Python 依赖项也可以直接从 Python 包索引PyPI)安装。使用 Python 虚拟环境来做这件事可以更好地保护主机系统免受更改的影响。

以下命令创建一个虚拟环境,激活它,然后从源代码安装 Odoo 以及所有必需的 Python 依赖项:

$ python3 -m venv /opt/odoo/env15
$ source /opt/odoo/env15/bin/activate
(env15) $ pip install -r /opt/odoo/odoo15/requirements.txt
(env15) $ pip install -e /opt/odoo/odoo15

现在,Odoo 应该已经准备好了。可以使用以下任何命令来确认这一点:

(env15) $ odoo --version
Odoo Server 15.0
(env15) $ /opt/odoo/odoo15/odoo-bin --version
Odoo Server 15.0
$ /opt/odoo/env15/bin/python3 /opt/odoo/odoo15/odoo-bin --version
Odoo Server 15.0
$ /opt/odoo/env15/bin/odoo --version
Odoo Server 15.0

让我们逐个理解这些命令:

  • 第一个命令依赖于由 pip install -e /opt/odoo/odoo15 提供的 odoo 命令。

  • 第二个命令不依赖于 odoo 命令,它直接调用 Odoo 启动脚本,/opt/odoo/odoo15/odoo-bin

  • 第三个命令不需要事先激活虚拟环境,因为它直接使用相应的 Python 可执行文件,这具有相同的效果。

  • 最后一个命令以更紧凑的方式执行相同的操作。它直接使用该虚拟环境中可用的 odoo 命令。这对某些脚本可能很有用。

现在 Odoo 已经准备好运行了。下一步是注意要使用的配置文件,我们将在下一节中解释。

配置 Odoo

一旦安装了 Odoo,就需要准备用于生产服务的配置文件。

下一个子节提供了如何做到这一点的指导。

设置配置文件

预期配置文件位于 /etc 系统目录中。因此,Odoo 生产配置文件将存储在 /etc/odoo/odoo.conf

为了更容易地看到所有可用的选项,可以生成一个默认配置文件。这应该由将运行该服务的用户来完成。

如果尚未完成,为 odoo 用户创建一个会话并激活虚拟环境:

$ sudo su - odoo
$ python3 -m venv /opt/odoo/env15

现在,可以使用以下命令创建一个默认配置文件:

(env15) $ odoo -c /opt/odoo/odoo.conf --save --stop-after-init

在上一个命令中,-c 选项设置配置文件的位置。如果没有提供,则默认为 ~/.odoorc--save 选项将选项写入其中。如果文件不存在,它将使用所有默认选项创建。如果它已经存在,它将使用命令中使用的选项进行更新。

以下命令设置了该文件的一些重要选项:

(env15) $ odoo -c /opt/odoo/odoo.conf --save \
--stop-after-init \
-d odoo-prod --db-filter="^odoo-prod$" \
--without-demo=all --proxy-mode

设置的选项如下:

  • -d: 这是默认要使用的数据库。

  • --db-filter: 这是一个正则表达式,用于过滤 Odoo 服务可用的数据库。使用的表达式仅使 odoo-prod 数据库可用。

  • --without-demo=all: 这将禁用演示数据,以便 Odoo 初始化数据库从零开始。

  • --proxy-mode:这启用了代理模式,意味着 Odoo 应该期望来自反向代理的请求。

下一步是将此默认文件复制到 /etc 目录,并设置必要的访问权限,以便 Odoo 用户可以读取它:

$ exit  # exit from the odoo user session
$ sudo mkdir /etc/odoo
$ sudo cp /opt/odoo/odoo.conf /etc/odoo/odoo.conf
$ sudo chown -R odoo /etc/odoo
$ sudo chmod u=r,g=rw,o=r /etc/odoo/odoo.conf  # for extra hardening

最后一条命令确保运行 Odoo 进程的用户可以读取但不能更改配置文件,从而提供更好的安全性。

还需要创建 Odoo 日志文件目录,并授予 odoo 用户访问权限。这应该在 /var/log 目录内完成。以下命令可以完成此操作:

$ sudo mkdir /var/log/odoo
$ sudo chown odoo /var/log/odoo

最后,应该编辑 Odoo 配置文件,以确保一些重要的参数被正确配置。例如,以下命令使用 nano 编辑器打开文件:

$ sudo nano /etc/odoo/odoo.conf

这些是一些最重要的参数的建议值:

[options]
addons_path = /opt/odoo/odoo15/odoo/addons,/opt/odoo/odoo15/addons,/opt/odoo/library
admin_passwd = StrongRandomPassword
db_name = odoo-prod
dbfilter = ^odoo-prod$
http_interface = 127.0.0.1
http_port = 8069
limit_time_cpu = 600
limit_time_real = 1200
list_db = False
logfile = /var/log/odoo/odoo-server.log
proxy_mode = True
without_demo = all
workers = 6

让我们详细解释一下:

  • addons_path:这是一个逗号分隔的路径列表,其中将查找附加模块。它从左到右读取,最左边的目录被视为优先级更高。

  • admin_passwd:这是用于访问网络客户端数据库管理功能的密码。使用强密码设置此密码至关重要,或者更好的是将其设置为 False 以禁用此功能。

  • db_name:这是在服务器启动序列中初始化的数据库实例。

  • dbfilter:这是用于使数据库可访问的过滤器。它是一个 Python 解释的正则表达式表达式。为了用户不被提示选择数据库,并且未认证的 URL 能够正常工作,它应该设置为 ^dbname$,例如,dbfilter=^odoo-prod$。它支持 %h%d 占位符,它们将被 HTTP 请求的主机名和子域名名称替换。

  • http_interface:这是 Odoo 将监听的 TCP/IP 地址。默认情况下,它是 0.0.0.0,意味着所有地址。对于位于反向代理后面的部署,可以将它设置为反向代理地址,以便只考虑来自那里的请求。如果反向代理与 Odoo 服务在同一服务器上,请使用 127.0.0.1

  • http_port:这是服务器将监听的端口号。默认情况下,使用端口号 8069

  • limit_time_cpu / limit_time_real:这为工作者设置了 CPU 时间限制。默认设置 60120 可能太低,可能需要将它们提高。

  • list_db = False:这阻止了数据库列表,无论是在远程过程调用(RPC)级别还是在 UI 中,它还阻止了数据库管理屏幕和底层的 RPC 函数。

  • logfile:这是服务器日志应该写入的位置。对于系统服务,预期的位置是在 /var/log 内的某个地方。如果为空,则日志将打印到标准输出。

  • proxy_mode:当 Odoo 通过反向代理访问时,应该将其设置为 True,正如我们将要做的。

  • without_demo:在生产环境中,此选项应设置为all,以便新数据库上不包含演示数据。

  • workers:此选项,当值为两个或更多时,启用多进程模式。我们将在稍后详细讨论这一点。

从安全角度考虑,admin_passwdlist_db=False选项尤为重要。它们阻止对数据库管理功能的 Web 访问,并且应在任何生产或面向互联网的 Odoo 服务器上设置。

小贴士

可以使用openssl rand -base64 32命令在命令行中生成随机密码。将32数字更改为您喜欢的密码大小。

以下参数也可能很有帮助:

  • data_dir:这是会话数据和附件文件存储的路径;请记住备份此目录。

  • http_interface:此选项设置将监听的地址。默认情况下,它监听0.0.0.0,但在使用反向代理时,可以将其设置为127.0.0.1以仅响应本地请求。

我们可以通过以下方式运行 Odoo 手动检查配置的效果:

$ sudo su - odoo
$ source /opt/odoo/env15/bin/activate
$ odoo -c /etc/odoo/odoo.conf

最后一条命令不会在控制台显示任何输出,因为日志消息正在写入日志文件而不是标准输出。

要跟踪运行中的 Odoo 服务器的日志,可以使用tail命令:

$ tail -f /var/log/odoo/odoo-server.log

这可以在运行手动命令的原终端窗口之外的不同终端窗口中完成。

要在同一终端窗口中运行多个终端会话,可以使用tmuxscreen。Ubuntu 还提供了tmuxscreen。有关更多详细信息,请参阅help.ubuntu.com/community/Byobu

注意

不幸的是,无法直接从 Odoo 命令中取消logfile配置选项。如果我们想暂时将日志输出发送回标准输出,最佳解决方案是使用未设置logfile选项的配置文件副本。

可能的情况是odoo-prod数据库尚未由 Odoo 初始化,这需要手动完成。在这种情况下,可以通过安装base模块来完成初始化:

$ /opt/odoo/env15/bin/odoo -c /etc/odoo/odoo.conf -i base \
--stop-after-init

到目前为止,Odoo 配置应该已经准备好了。在继续之前,值得了解更多关于 Odoo 中的多进程工作者的信息。

理解多进程工作者

预期的生产实例应处理大量的工作负载。由于 Python 语言的全局解释器锁GIL),默认情况下,服务器运行一个进程,并且只能使用一个 CPU 核心进行处理。然而,有一个多进程模式可供使用,以便可以处理并发请求,从而让我们可以利用多个核心。

workers=N 选项设置要使用的工人数。作为一个指导原则,它可以设置为 1+2*P,其中 P 是处理器核心数。找到最佳设置可能需要通过使用不同的数字并检查服务器处理器的繁忙程度进行一些实验。在同一台机器上运行 PostgreSQL 也会对此产生影响,这将减少应该启用的工人数。

对于负载过高的情况,设置过多的工人数比设置过少的情况更好。最小值应该是六个,因为大多数浏览器使用的并行连接数。最大值通常由机器上的 RAM 量限制,因为每个工人都将消耗一些服务器内存。对于正常的使用模式,Odoo 服务器应该能够处理 (1+2*P)*6 个并发用户,其中 P 是处理器的数量。

有几个 limit- 配置参数可以用来调整工作参数。当工人数达到这些限制时,会回收工人数,相应的进程将被停止并启动一个新的进程。这可以保护服务器免受内存泄漏和特定进程过载服务器资源的影响。

官方文档提供了关于如何调整工作参数的额外建议。可以在 www.odoo.com/documentation/15.0/setup/deploy.html#builtin-server 找到。

到目前为止,Odoo 已经安装、配置并准备好运行。下一步是让它作为无人值守的系统服务运行。我们将在下一节中详细探讨这一点。

设置 Odoo 为系统服务

Odoo 应该作为系统服务运行,以便在系统启动时自动启动并无人值守运行,不需要用户会话。

在 Debian/Ubuntu 系统中,init 系统负责启动服务。历史上,Debian 及其衍生操作系统使用 sysvinit。但现在已经改变,最近的 Debian/Ubuntu 系统使用 systemd。这同样适用于 Ubuntu 16.04 及以后的版本。

要确认您的系统中使用的是 systemd,请尝试以下命令:

$ man init

此命令将打开当前正在使用的 init 系统的文档,以便您可以检查正在使用的内容。在手册页的顶部,您应该看到提到了 SYSTEMD

让我们继续配置 systemd 服务。

创建 systemd 服务

如果操作系统较新,例如 Debian 8 和 Ubuntu 16.04 或更新的版本,systemd 应该是正在使用的 init 系统。

要向系统中添加新的服务,只需创建一个描述它的文件。创建一个包含以下内容的 /lib/systemd/system/odoo.service 文件:

[Unit]
Description=Odoo Open Source ERP and CRM
After=network.target
[Service]
Type=simple
User=odoo
Group=odoo
ExecStart=/opt/odoo/env/bin/odoo -c /etc/odoo/odoo.conf --log-file=/var/log/odoo/odoo-server.log
KillMode=mixed
[Install]
WantedBy=multi-user.target

此服务配置文件基于 Odoo 源代码中提供的示例,可以在 github.com/odoo/odoo/blob/15.0/debian/odoo.service 找到。ExecStart 选项应调整为此系统要使用的特定路径。

接下来,可以使用以下命令将新服务注册:

$ sudo systemctl enable odoo.service

要启动这个新服务,请运行以下命令:

$ sudo systemctl start odoo

要检查其状态,请使用以下命令:

$ sudo systemctl status odoo

可以使用以下命令停止它:

$ sudo systemctl stop odoo

当以系统服务运行 Odoo 时,确认客户端可以访问它是有用的。让我们看看如何在命令行中做到这一点。

从命令行检查 Odoo 服务

要确认 Odoo 服务运行良好且响应迅速,我们可以检查它是否正在响应请求。我们应该能够从它那里获得响应,并在日志文件中看不到错误。

我们可以使用以下命令检查 Odoo 是否在服务器内部响应 HTTP 请求:

$ curl http://localhost:8069
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to target URL: <a href="/web">/web</a>.  If not click the link.

此外,要查看log文件中的内容,请使用以下命令:

$ less /var/log/odoo/odoo-server.log

要实时跟踪添加到日志文件的内容,可以使用tail -f,如下所示:

$ tail -f /var/log/odoo/odoo-server.log

Odoo 现在已安装并作为服务运行。接下来,可以通过添加反向代理来改进设置。下一节将解释这一点。

设置 Nginx 反向代理

虽然 Odoo 本身可以提供网页服务,但建议在其前面设置一个反向代理。反向代理接收来自客户端的流量,然后将它转发到响应客户端的 Odoo 服务器。这样做有几个好处。

在安全方面,它可以提供以下功能:

  • 处理(并强制执行)HTTPS 协议以加密流量。

  • 隐藏内部网络特征。

  • 充当应用程序防火墙,限制接受处理的 URL。

在性能方面,它可以提供以下功能:

  • 缓存静态内容,避免让 Odoo 服务承受这些请求的负担,从而减少其负载。

  • 压缩内容以加快加载时间。

  • 充当负载均衡器,在多个 Odoo 服务之间分配负载。

有几种选项可以作为反向代理使用。历史上,Apache是一个流行的选择。近年来,Nginx 已被广泛使用,并在 Odoo 官方文档中提到。在我们的示例中,将使用 Nginx 进行反向代理,并使用它实现所提供的安全和性能功能。

首先,Nginx 应该被安装并设置为监听默认的 HTTP 端口。可能这个端口已经被另一个已安装的服务占用。为确保端口空闲且可用,请使用以下命令,它应该导致错误:

$ curl http://localhost
curl: (7) Failed to connect to localhost port 80: Connection refused

如果它没有返回之前的错误消息,则已安装的服务正在使用端口80,应该被禁用或卸载。

例如,如果已安装 Apache 服务器,请使用sudo service apache2 stop命令停止它,或者甚至使用sudo apt remove apache2命令将其卸载。

在端口80空闲的情况下,可以安装和配置 Nginx。以下命令安装 Nginx:

$ sudo apt-get install nginx
$ sudo service nginx start  # start nginx, if not already started

要确认nginx运行正确,请使用浏览器访问服务器地址或使用服务器上的curl http://localhost命令。这应该返回一个欢迎来到 nginx页面。

Nginx 配置文件存储在/etc/nginx/available-sites/,通过将它们添加到/etc/nginx/enabled-sites/来激活,这通常是通过在可用站点目录中的文件创建符号链接来完成的。

为了准备 Odoo Nginx 配置,应删除默认配置并添加 Odoo 配置文件,如下所示:

$ sudo rm /etc/nginx/sites-enabled/default
$ sudo touch /etc/nginx/sites-available/odoo
$ sudo ln -s /etc/nginx/sites-available/odoo \
/etc/nginx/sites-enabled/odoo

接下来,使用nanovi等编辑器,按照以下方式编辑配置文件:

$ sudo nano /etc/nginx/sites-available/odoo

以下示例提供了一个基本的 Nginx 配置,用于 Odoo:

upstream odoo {
  server 127.0.0.1:8069;
}
upstream odoochat {
  server 127.0.0.1:8072;
}
server {
  listen 80;
  server_name odoo.mycompany.com;
  proxy_read_timeout 720s;
  proxy_connect_timeout 720s;
  proxy_send_timeout 720s;
  # Add Headers for odoo proxy mode
  proxy_set_header X-Forwarded-Host  $host;
  proxy_set_header X-Forwarded-For   $proxy_add_x_
    forwarded_for;
  proxy_set_header X-Forwarded-Proto $scheme;
  proxy_set_header X-Real-IP         $remote_addr;
  # log
  access_log /var/log/nginx/odoo.access.log;
  error_log /var/log/nginx/odoo.error.log;
  # Redirect longpoll requests to odoo longpolling port
  location /longpolling {
    proxy_pass http://odoochat;
  }
  # Redirect requests to odoo backend server
  location / {
    proxy_redirect off;
    proxy_pass http://odoo;
  }
  # common gzip
  gzip_types text/css text/scss text/plain text/xml 
   application/xml application/json application/javascript;
  gzip on;
}

在配置文件顶部,有upstream配置部分。这些指向默认监听端口80698072的 Odoo 服务。8069端口服务于 Web 客户端和 RPC 请求,而8072服务于即时消息功能使用的长轮询请求。

server配置部分定义了在80默认 HTTP 端口上接收到的流量将发生什么。在这里,它通过proxy_pass配置指令重定向到上游 Odoo 服务。任何针对/longpolling地址的流量都会传递给odoochat上游,而剩余的/流量会传递给odoo上游。

几个proxy_set_header指令向请求头添加信息,以便让 Odoo 后端服务知道它正在被代理。

小贴士

由于安全原因,确保 Odoo 的proxy_mode参数设置为True非常重要。这样做的原因是,在 Nginx 中,所有击中 Odoo 的请求都来自 Nginx 服务器,而不是原始的远程 IP 地址。在代理中设置X-Forwarded-For头并启用--proxy-mode允许 Odoo 了解请求的原始来源。请注意,在没有在代理级别强制设置头的情况下启用--proxy-mode允许恶意客户端伪造其请求地址。

在配置文件的末尾,可以找到一些与gzip相关的指令。这些指令启用了某些文件的压缩,从而提高了性能。

一旦编辑并保存,可以使用以下命令验证 Nginx 配置的正确性:

$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

现在,可以使用以下命令之一重新加载 Nginx 服务的新配置,具体取决于使用的init系统:

$ sudo /etc/init.d/nginx reload
$ sudo systemctl reload nginx  # using systemd
$ sudo service nginx reload  # on Ubuntu systems

这将使 Nginx 在不中断服务的情况下重新加载配置,而如果使用restart而不是reload,则可能会发生中断。

为了确保安全,应通过 HTTPS 访问 Odoo。下一节将讨论这个问题。

配置和强制执行 HTTPS

网络流量不应以纯文本形式通过互联网传输。当在网络上公开 Odoo 服务器时,应使用 HTTPS 加密流量。

在某些情况下,使用自签名证书可能是可接受的。请记住,使用自签名证书提供有限的安全性。虽然它允许加密流量,但它有一些安全限制,例如无法防止中间人攻击,或者无法在最新的网页浏览器上显示安全警告。

一个更稳健的解决方案是使用由认可机构签发的证书。这在运行电子商务网站时尤为重要。另一个选项是使用 Let's Encrypt 证书,Certbot 程序可以自动化获取该证书的 SSL 证书。有关更多信息,请参阅 certbot.eff.org/instructions

接下来,我们将看到如何创建自签名证书,以防这是首选的选择。

创建自签名 SSL 证书

Nginx 需要安装一个证书来启用 SSL。我们可以选择使用证书机构提供的证书,或者生成一个自签名的证书。

要创建自签名证书,请使用以下命令:

$ sudo mkdir /etc/ssl/nginx && cd /etc/ssl/nginx
$ sudo openssl req -x509 -newkey rsa:2048 \
-keyout server.key -out server.crt -days 365 -nodes
$ sudo chmod a-wx *            # make files read only
$ sudo chown www-data:root *   # access only to www-data group

上述代码创建了一个 /etc/ssl/nginx 目录和一个无密码的 SSL 证书。当运行 openssl 命令时,用户将被要求提供一些额外的信息,然后生成证书和密钥文件。最后,这些文件的拥有权被赋予 www-data 用户,该用户用于运行网页服务器。

准备好要使用的 SSL 证书后,下一步是将它安装到 Nginx 服务上。

在 Nginx 上配置 HTTPS 访问

为了强制使用 HTTPS,需要一个 SSL 证书。Nginx 服务将使用它来加密服务器和网页浏览器之间的流量。

对于这一点,需要重新检查 Odoo Nginx 的配置文件。编辑它,将 server 指令替换为以下内容:

server {
  listen 80;
  rewrite ^(.*) https://$host$1 permanent;
}

通过这个更改,对 http:// 地址的请求将被转换为 https:// 相应的地址,确保不会意外地使用非安全传输。

HTTPS 服务仍然需要配置。这可以通过向配置中添加以下 server 指令来完成:

# odoo server
upstream odoo {
  server 127.0.0.1:8069;
}
upstream odoochat {
  server 127.0.0.1:8072;
}
# http -> https
server {
  listen 80;
  server_name odoo.mycompany.com;
  rewrite ^(.*) https://$host$1 permanent;
}
server {
  listen 443;
  server_name odoo.mycompany.com;
  proxy_read_timeout 720s;
  proxy_connect_timeout 720s;
  proxy_send_timeout 720s;
  # Add Headers for odoo proxy mode
  proxy_set_header X-Forwarded-Host $host;
  proxy_set_header X-Forwarded-For $proxy_add_x_for
    warded_for;
  proxy_set_header X-Forwarded-Proto $scheme;
  proxy_set_header X-Real-IP $remote_addr;
  # SSL parameters
  ssl on;
  ssl_certificate /etc/ssl/nginx/server.crt;
  ssl_certificate_key /etc/ssl/nginx/server.key;
  ssl_session_timeout 30m;
  ssl_protocols TLSv1.2;
  ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-
    AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-
    RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-
    POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-
    GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
  ssl_prefer_server_ciphers off;
  # log
  access_log /var/log/nginx/odoo.access.log;
  error_log /var/log/nginx/odoo.error.log;
  # Redirect longpoll requests to odoo longpolling port
  location /longpolling {
    proxy_pass http://odoochat;
  }
  # Redirect requests to odoo backend server
  location / {
    proxy_redirect off;
    proxy_pass http://odoo;
  }
  # common gzip
  gzip_types text/css text/scss text/plain text/xml 
   application/xml application/json application/javascript;
  gzip on;
}

这个额外的 server 指令监听 HTTPS 端口,并使用 /etc/ssl/nginx/ 下的证书文件来加密流量。

注意

这里提出的 Nginx 配置基于在 www.odoo.com/documentation/15.0/administration/install/deploy.html#https 找到的官方文档。

一旦重新加载此配置,Odoo 应该只通过 HTTPS 运行,如下面的命令所示:

$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
$ sudo service nginx reload  # or: sudo systemctl reload nginx
* Reloading nginx configuration nginx
...done.
$ curl -k https://localhost

加密网络流量并不是 Nginx 为我们做的唯一事情。它还可以帮助我们减少 Odoo 上游服务的负载。让我们在下一节中详细探讨这一点。

缓存静态内容

Nginx 可以缓存提供的静态文件——这意味着后续对缓存文件的请求将直接由 Nginx 提供,无需上游 Odoo 服务请求。

这不仅提高了响应时间,还提高了 Odoo 服务能力,以服务更多用户,因为它现在专注于响应动态请求。

要启用静态内容缓存,请在 Nginx 配置文件中# comming gzip指令之后添加以下部分:

  # cache static data
  location ~* /web/static/ {
    proxy_cache_valid 200 60m;
    proxy_buffering on;
    expires 864000;
    proxy_pass http://odoo;
  }

使用此配置,静态数据将缓存 60 分钟。Odoo 静态内容定义为从/web/static路径提供的任何文件。

到这一点,服务器应该完全功能,Nginx 通过 HTTPS 处理请求,然后将它们传递给 Odoo 服务进行处理。

Odoo 服务需要维护和更新,所以下一节将讨论如何进行此操作。

维护 Odoo 服务和模块

一旦 Odoo 服务器启动并运行,预计需要一些维护工作——例如,安装或更新模块。

这些操作涉及生产系统的一些风险,因此在生产环境中应用之前,最好在预发布环境中进行测试。让我们从一个基本的配方开始,创建一个预发布环境。

创建预发布环境

预发布环境应该是生产系统的副本,理想情况下应该有自己的专用服务器。

一种简化方法,对于大多数情况来说足够安全,是将预发布环境放在与生产系统相同的服务器上。

要将odoo-prod生产数据库的副本作为odoo-stage数据库创建,请使用以下命令:

$ dropdb odoo-stage
$ createdb --owner=odoo odoo-stage
$ pg_dump odoo-prod | psql -d odoo-stage
$ sudo su - odoo
$ cd ~/.local/share/Odoo/filestore/
$ cp -r odoo-prod odoo-stage
$ exit

注意,一些配置被复制过来,例如连接到电子邮件服务器,您可能希望有额外的命令来禁用它们。具体需要采取的操作取决于数据库设置,但很可能可以通过脚本自动化。为此,了解psql命令可以直接从命令行运行 SQL 很有用,例如,psql -d odoo-stage -c "<SQL command>"

小贴士

可以使用以下命令以更快的速度创建数据库副本:

$ createdb --owner=odoo --template=odoo-prod odoo-stage

这里需要注意的是,为了使其运行,不能有任何对odoo-prod数据库的开放连接,因此在使用命令之前,需要停止 Odoo 生产服务器。

现在我们已经有了生产数据库的副本用于预发布,下一步是创建要使用的源副本。例如,这可以放在名为/opt/odoo/stage的子目录中。

以下 shell 命令复制相关文件并创建预发布环境:

$ sudo su - odoo
$ mkdir /opt/odoo/stage
$ cp -r /opt/odoo/odoo15/ /opt/odoo/stage/
$ cp -r /opt/odoo/library/ /opt/odoo/stage/  # custom code
$ python3 -m venv /opt/odoo/env-stage
$ source /opt/odoo/env-stage/bin/activate
(env-stage) $ pip install -r \
/opt/odoo/stage/odoo15/requirements.txt
(env-stage) $ pip install -e /opt/odoo/stage/odoo15
(env-stage) $ exit

最后,应该为预发布环境准备一个特定的 Odoo 配置文件,因为文件使用的路径不同。使用的 HTTP 端口也应更改,以便预发布环境可以与主生产服务同时运行。

现在,这个预演环境可以用于测试目的。因此,下一节将描述如何应用生产更新。

更新 Odoo 源代码

Odoo 和自定义模块的代码通常通过 Git 进行版本管理。

要从 GitHub 仓库获取最新的 Odoo 源代码,请使用git pull命令。在此之前,可以使用git tag命令为当前使用的提交创建一个标签,以便更容易回滚代码更新,如下所示:

$ sudo su - odoo
$ cd /opt/odoo/odoo15
$ git tag --force 15-last-prod
$ git pull
$ exit

要使代码更改生效,应重启 Odoo 服务。要使数据文件更改生效,需要升级模块。

小贴士

作为一般规则,对 Odoo 稳定版本的更改被认为是代码修复,因此通常不值得冒进行模块升级的风险。然而,如果你需要执行模块升级,可以使用-u <module>附加选项(或-u base),这将升级所有模块。

在将操作应用到生产数据库之前,我们可以使用预演数据库来测试这些操作,如下所示:

$ source /opt/odoo/env15/bin/activate
(env15) $ odoo -c /etc/odoo/odoo.conf -d odoo-stage \
--http-port=8080 -u library  # modules to updgrade
(env15) $ exit

这个 Odoo 预演服务器被配置为监听端口8080。我们可以用我们的网络浏览器导航到那里,检查升级后的代码是否正确工作。

如果出现问题,可以使用以下命令将代码回滚到早期版本:

$ sudo su - odoo
$ cd /opt/odoo/odoo15
$ git checkout 15-last-prod
$ exit

如果一切按预期进行,那么在生产服务上执行升级应该是安全的,这通常是通过重启来完成的。如果你想执行实际的模块升级,建议的方法是停止服务器,运行升级,然后重启服务,如下所示:

$ sudo service odoo stop
$ sudo su -c "/opt/odoo/env15/bin/odoo -c /etc/odoo/odoo.conf" \
" -u base --stop-after-init" odoo
$ sudo service odoo start

在运行升级之前备份数据库也是建议的。

在本节中,你学习了如何创建一个与主 Odoo 环境并行的预演环境,用于测试。在将更新应用到生产系统之前,可以在预演环境中尝试对 Odoo 代码或自定义模块的更新。这使我们能够在升级前识别并纠正可能发现的问题。

摘要

在本章中,我们学习了在基于 Debian 的生产服务器上设置和运行 Odoo 所需的额外步骤。我们查看配置文件中的最重要的设置,并学习了如何利用多进程模式。

为了提高安全性和可扩展性,我们还学习了如何在前端 Odoo 服务器进程前使用 Nginx 作为反向代理,以及如何配置它以使用 HTTPS 加密流量。

最后,提供了一些关于如何创建预演环境以及如何对 Odoo 代码或自定义模块进行更新的建议。

这涵盖了运行 Odoo 服务器和为用户提供一个相对稳定和安全的服务所需的基本要素。现在我们可以用它来托管我们的图书馆管理系统!

进一步阅读

要了解更多关于 Odoo 的信息,你应该查看官方文档 www.odoo.com/documentation。那里有更详细的某些主题,你还会发现本书未涉及的主题。

此外,还有一些关于 Odoo 的已出版书籍可能对你有帮助。Packt Publishing 在其目录中收录了一些,特别是 Odoo 开发食谱 提供了更多关于本书未讨论主题的进阶材料。在撰写本文时,可用的最后一版是为 Odoo 14 定制的,可在 www.packtpub.com/product/odoo-14-development-cookbook-fourth-edition/9781800200319 获取。

最后,Odoo 是一个具有活跃社区的开源产品。参与其中,提问和贡献是一个不仅能够学习,还能建立商业网络的绝佳方式。考虑到这一点,我们还应该提到 Odoo 社区协会OCA),它促进协作和高质量的开源代码。你可以在 odoo-community.org/github.com/OCA 上了解更多信息。

享受你的 Odoo 之旅!

Packt.com

订阅我们的在线数字图书馆,全面访问超过 7,000 本书和视频,以及行业领先的工具,帮助你规划个人发展并提升职业生涯。更多信息,请访问我们的网站。

第十四章:为什么订阅?

  • 通过来自 4,000 多位行业专业人士的实用电子书和视频,节省学习时间,增加编码时间

  • 通过为你量身定制的技能计划提高你的学习效果

  • 每月免费获得一本电子书或视频

  • 全文搜索,便于快速访问关键信息

  • 复制粘贴、打印和收藏内容

你知道 Packt 为每本书都提供电子书版本,包括 PDF 和 ePub 文件吗?你可以在 packt.com 升级到电子书版本,作为印刷书客户,你有权获得电子书副本的折扣。如需了解更多详情,请联系我们 customercare@packtpub.com

www.packt.com,你还可以阅读一系列免费的技术文章,注册各种免费通讯,并享受 Packt 书籍和电子书的独家折扣和优惠。

你可能还会喜欢的其他书籍

如果你喜欢这本书,你可能还会对 Packt 的其他书籍感兴趣:

![精通 Adobe Photoshop Elements

](https://www.packtpub.com/product/odoo-14-development-cookbook-fourth-edition/9781800200319)

Odoo 14 开发食谱 - 第四版

Parth Gajjar, Alexandre Fayolle, Holger Brunn, Daniel Reis

ISBN: 9781800200319

  • 使用 Odoo CMS 的动态构建块构建美丽的网站

  • 掌握高级概念,如缓存、预取、调试

  • 使用新的 OWL 框架修改后端 JavaScript 组件和 POS 应用程序

  • 通过远程过程调用(RPC)连接并访问 Odoo 中的任何对象

  • 使用 Odoo.sh 管理、部署和测试 Odoo 实例

  • 配置物联网盒以添加和升级销售点(POS)硬件

  • 了解如何实现应用内购买服务

![精通 Adobe Photoshop Elements

](https://packt.link/9781801078122)

使用 Odoo 网站构建器设计专业网站

Sainu Nannat

ISBN: 9781801078122

  • 了解在开发网站时如何实现结构块

  • 在 Odoo 网站构建器中使用动态内容块和内部内容块

  • 在 Odoo 网站构建器中使用 HTML、CSS 或 JS 编辑器自定义应用程序

  • 使用 Odoo 网站构建器创建和设计博客

  • 使用 Odoo 网站构建器构建一个功能齐全的电子商务网站和讨论论坛

  • 跟踪网站访客并了解实时聊天工具及其功能

Packt 正在寻找像你这样的作者

如果您有兴趣成为 Packt 的作者,请访问authors.packtpub.com并今天申请。我们已与成千上万的开发者和技术专业人士合作,就像您一样,帮助他们将见解分享给全球科技社区。您可以提交一般申请,申请我们正在招募作者的特定热门话题,或者提交您自己的想法。

分享您的想法

现在您已经完成了《Odoo 15 开发精华》,我们非常想听听您的想法!如果您在亚马逊购买了这本书,请点击此处直接跳转到该书的亚马逊评论页面并分享您的反馈或在该购买网站上留下评论。

您的评论对我们和科技社区都非常重要,它将帮助我们确保我们提供的是高质量的内容。

您可能还会喜欢的其他书籍

您可能还会喜欢的其他书籍

posted @ 2025-09-18 14:36  绝不原创的飞龙  阅读(24)  评论(0)    收藏  举报