Odoo-开发秘籍第五版-全-
Odoo 开发秘籍第五版(全)
原文:
zh.annas-archive.org/md5/eff006ebae40d33cb748201c68a424ff译者:飞龙
前言
当你阅读这段文字时,你已经接触到了 Odoo,这是增长最快的开源 ERP 商业套件之一。Odoo 是一个功能齐全的开源平台,它帮助构建适用于各种行业的解决方案。如果你是开发者,你正坐在一座金矿上。如果你是最终用户,你将获得一个惊人的工具来简化你的业务流程,从售前到销售、库存和会计,一应俱全。
除了 Odoo 中可用的广泛应用程序列表,它就像一块美味的面团(嗯,这让你想起了令人垂涎的披萨吗?)可以根据你的需求进行塑形。技术上讲,它是一个非常灵活的 ORM 控制器(对象关系映射)驱动的应用程序开发框架,考虑到可扩展性而构建。遵循继承规则,功能/扩展和修改可以作为模块实现,这些模块被分类为应用程序。在这里提到 ORM,Odoo 展示了单体架构。
Odoo 17 开发食谱为开发者提供了一个坚实的平台,无论他们是初学者还是熟练者。代码片段涵盖了大多数问题和用例,解释的字段有助于准确开发模块,同时保持代码质量和可用性。作为额外的好处,第二十五章是一个特别的章节,它帮助开发者和非开发者快速生成原型。
这本书由整个 Serpent Consulting Services Pvt Ltd 团队编写和支持,每个人都贡献了自己的时间和精力,让梦想成真。
这本书面向的对象
这本书面向所有层次的开发者,需要至少了解面向对象编程,Python 是必备技能。即使是 Python 编程的新手也可以找到这本书适合。它旨在适应那些编程知识有限但强烈渴望学习的开发者。
偏好的开发编辑器是 PyCharm、Eclipse 或 Sublime,但预计大多数开发者将在基于 Ubuntu/Debian 的操作系统上运行 Odoo。代码示例有意保持简单和清晰,并配有详尽的解释,以促进理解。新手将从基础知识掌握概念,确保学习之旅愉快。
已经熟悉 Odoo 的资深开发者也应该在这本书中找到价值。它不仅增强了他们现有的知识,还提供了一个简单的方法来保持对最新 Odoo 版本的更新,其中显著的变化被突出显示。
最终,这本书旨在作为新人和资深开发者日常使用的坚实参考。此外,不同 Odoo 版本之间差异的文档将是对同时处理不同版本或移植模块的开发者有价值的资源。
这本书涵盖的内容
第一章**, 安装 Odoo 开发环境,解释了如何创建 Odoo 开发环境、启动 Odoo、创建配置文件以及激活 Odoo 的开发者工具。
第二章**, 管理 Odoo 服务器实例,提供了与从 GitHub 安装的附加组件一起工作的有用提示,以及如何组织实例的源代码。
第三章**, 创建 Odoo 附加模块,解释了 Odoo 附加模块的结构,并提供了从头开始创建简单模块的逐步指南。
第四章**, 应用模型,专注于 Odoo 模型结构,并解释了所有类型的字段及其属性。它还涵盖了通过扩展模块扩展现有数据库结构的技术。
第五章**, 基本服务器端开发,解释了在 Odoo 中执行 CRUD 操作的各种框架方法。本章还包括继承和扩展现有方法的不同方式。
第六章**, 管理模块数据,展示了如何将数据与您的模块代码一起打包。它还解释了当附加组件提供的数据模型在新版本中修改时,如何编写迁移脚本。
第七章**, 调试模块,提出了一些服务器端调试的策略,以及 Python 调试器的介绍。它还涵盖了在开发者模式下运行 Odoo 的技术。
第八章**, 高级服务器端开发技术,涵盖了 ORM 框架的更高级主题。这对于开发向导、SQL 视图、安装钩子、变更方法等非常有用。本章还解释了如何在数据库中执行原始 SQL 查询。
第九章**, 后端视图,解释了如何为您的数据模型编写业务视图以及如何从这些视图中调用服务器端方法。它涵盖了常规视图(列表视图、表单视图和搜索视图),以及一些复杂视图(看板、图形、日历、交叉表等)。
第十章**, 安全访问,解释了如何通过创建安全组、编写访问控制列表来定义给定模型上每个组可用的操作,以及在必要时编写记录级规则,来控制谁可以访问您的 Odoo 实例中的什么内容。
第十一章**, 国际化,展示了 Odoo 中语言翻译的工作方式。它展示了如何安装多种语言以及如何导入/导出翻译术语。
第十二章**, 自动化、工作流、电子邮件和打印,说明了 Odoo 中可用于实现记录业务流程的不同工具。它还展示了如何使用服务器操作和自动规则来支持业务规则。这还包括 QWeb 报告以生成动态 PDF 文档。
第十三章**, Web 服务器开发,涵盖了 Odoo Web 服务器的核心。它展示了如何创建自定义 URL 路由以在给定 URL 上提供数据,同时也展示了如何控制对这些 URL 的访问。
第十四章**, CMS 网站开发,展示了如何使用 Odoo 管理网站。它还展示了如何创建和修改美观的网页和 QWeb 模板。本章还包括如何创建具有选项的动态构建块。它包括一些专门用于管理 SEO、用户表单、UTM 跟踪、网站地图和获取访客位置信息的食谱。本章还突出了 Odoo 中多站点的最新概念。
第十五章**, Web 客户端开发,深入探讨了 Odoo 的 JavaScript 部分。它涵盖了如何创建新的字段小部件并调用服务器的 RPC。这还包括如何从头开始创建全新的视图。你还将学习如何创建入职导览。
第十六章**, Odoo Web 库(OWL),介绍了名为 OWL 的新客户端框架。它涵盖了 OWL 组件的生命周期。它还包括从头创建字段小部件的食谱。
第十七章**, 使用 Odoo 进行应用内购买,涵盖了与 Odoo 中最新 IAP 概念相关的所有内容。在本章中,你将学习如何创建 IAP 的客户和服务模块。你还将学习如何创建 IAP 账户并从最终用户那里提取 IAP 积分。
第十八章**, 自动化测试用例,包括如何编写和执行自动化测试用例。这包括服务器端和客户端测试用例。本章还涵盖了导览测试用例和设置无头 Chrome 以获取失败的测试用例的视频。
第十九章**, 使用 Odoo.sh 进行管理、部署和测试,解释了如何使用 PaaS 平台 Odoo.sh 来管理、部署和测试 Odoo 实例。它涵盖了如何管理不同类型的实例,例如生产、预发布和开发。本章还涵盖了 Odoo.sh 的各种配置选项。
第二十章**, Odoo 中的远程过程调用,涵盖了从外部应用程序连接 Odoo 实例的不同方法。本章教你如何通过 XML-RPC、JSON-RPC 和 oodoorpc 库连接到并访问 Odoo 实例的数据。
第二十一章**, 性能优化,解释了在 Odoo 中用于获得性能改进的不同概念和模式。本章包括预取、ORM 缓存和代码分析以检测性能问题。
第二十二章**, 销售点,涵盖了 PoS 应用程序中的定制。这包括用户界面的定制、添加新的操作按钮、修改业务流程和扩展客户食谱。
第二十三章**, 管理 Odoo 中的电子邮件,解释了如何在 Odoo 中管理电子邮件和聊天。它从配置邮件服务器开始,然后转向 Odoo 框架的邮件 API。本章还涵盖了 Jinja2 和 QWeb 邮件模板、表单视图中的聊天、字段日志和活动。
第二十四章**, 管理物联网盒,为您展示了物联网盒的最新硬件亮点。本章涵盖了如何配置、访问和调试物联网盒,还包括了将物联网盒与您的自定义附加组件集成的食谱。
第二十五章**, 深入探讨了模块开发的替代方法。虽然这通常不是最佳实现建议,但分析师可以使用本模块中概述的技术快速创建可能的设计、原型、报告或视图。
为了从这本书中获得最大收益
我们的主要且最有价值的建议仅仅是“实践”!每一章都提供了对开发方面的深入见解,因此应用您所学的内容至关重要。
为了充分利用这本书,我们建议您补充阅读有关 Python 编程语言、Ubuntu/Debian Linux 操作系统和 PostgreSQL 数据库的额外资源。
本书包含了 Odoo 的安装说明,因此您只需要 Ubuntu 20.04 或更高版本,或者任何其他基于 Linux 的操作系统。对于其他操作系统,您可以通过虚拟机使用它。如果您使用的是 Windows,您还可以将 Ubuntu 作为子系统安装:
| 本书涵盖的软件/硬件 | 操作系统要求 |
|---|---|
| Odoo 17 + Python 3.6 及以上 | Ubuntu 20.04 及以上 |
本书面向对 Python 编程语言有基本知识的开发者,因为 Odoo 后端运行在 Python 上。在 Odoo 中,数据文件是用 XML 创建的,因此需要具备基本的 XML 知识。
本书还涵盖了后端 JavaScript 框架、PoS 应用程序和网站构建器,这需要具备基本的 JavaScript、jQuery 和 Bootstrap 4 知识。Odoo 社区版是开源的,可以免费获取,但包括物联网、队列和仪表板在内的几个功能仅在企业版中可用,因此要跟随该食谱,您需要企业版。
要遵循第二十四章,“管理物联网盒”,您将需要 Raspberry Pi 3 Model B+,可在www.raspberrypi.org/products/raspberry-pi-3-model-b-plus/找到。
如果您正在使用本书的数字版,我们建议您亲自输入代码或从书的 GitHub 仓库(下一节中有一个链接)获取代码。这样做将帮助您避免与代码的复制和粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub(github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main)下载本书的示例代码文件。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有来自我们丰富的书籍和视频目录中的其他代码包,可在github.com/PacktPublishing/找到。查看它们吧!
使用的约定
本书使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“鉴于book是一个浏览记录,我们可以通过将book.id作为book_id参数传递来简单地回收第一个示例中的函数,以提供相同的内容。”
代码块设置如下:
@http.route('/my_library/books/json', type='json', auth='none')
def books_json(self):
records = request.env['library.book'].sudo().search([]) return records.read(['name'])
任何命令行输入或输出都按以下方式编写:
$ ./odoo-bin -d mydb --i18n-export=mail.po --modules=mail
$ mv mail.po ./addons/mail/i18n/mail.pot
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“另一个重要用途是提供演示数据,当数据库创建时,如果选中了加载演示数据复选框,则会加载这些数据。”
小贴士或重要注意事项
看起来像这样。
部分
在本书中,您将找到几个频繁出现的标题(准备就绪、如何做…、它是如何工作的…、更多内容…和另请参阅)。
为了清楚地说明如何完成一个菜谱,请按照以下方式使用这些部分:
准备就绪
本节告诉您在菜谱中可以期待什么,并描述了如何设置任何软件或任何为菜谱所需的初步设置。
如何做…
本节包含遵循菜谱所需的步骤。
它是如何工作的…
本节通常包含对上一节发生情况的详细解释。
更多内容…
本节包含有关菜谱的附加信息,以便您对菜谱有更深入的了解。
另请参阅
本节提供对其他有用信息的有用链接。
联系我们
我们欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件发送给我们 customercare@packtpub.com,并在邮件主题中提及书名。
勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告,我们将非常感谢。请访问 www.packtpub.com/support/errata 并填写表格。
盗版:如果您在互联网上发现任何形式的我们作品的非法副本,我们将不胜感激,如果您能提供位置地址或网站名称,我们将非常感谢。请通过电子邮件发送给我们 copyright@packt.com 并附上材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com.
分享您的想法
一旦您阅读了《Odoo 开发食谱》,我们非常乐意听到您的想法!请 点击此处直接进入此书的 Amazon 评论页面 并分享您的反馈。
您的评论对我们和科技社区都非常重要,并将帮助我们确保我们提供高质量的内容。
下载本书的免费 PDF 副本
感谢您购买本书!
您喜欢随时随地阅读,但无法携带您的印刷书籍到处走吗?
您选择的设备是否与您的电子书购买不兼容?
不要担心,现在,每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠远不止这些,您还可以获得独家折扣、时事通讯和每日免费内容的每日电子邮件。
按照以下简单步骤获取福利:
- 扫描二维码或访问下面的链接

packt.link/free-ebook/9781805124276
-
提交您的购买证明
-
就这些!我们将直接将您的免费 PDF 和其他福利发送到您的电子邮件地址。
第一章:安装 Odoo 开发环境
要开始我们的 Odoo 开发之旅,我们必须通过安装源代码来设置我们的开发环境,这些源代码可以帮助我们增强、调试和改进我们的开发技能。设置 Odoo 开发环境有几种方法,但本章提出了其中最好的方法。你可以在网上找到解释其他方法的几个教程。请记住,本章是关于设置一个与生产环境有不同的要求的开发环境;生产环境需要根据系统中的数据量和用户数量设置不同的参数。我们将在本章中介绍配置文件参数及其用法。
如果你刚开始接触 Odoo 开发,你必须了解 Odoo 生态系统的某些方面。第一个食谱将为你简要介绍 Odoo 生态系统,之后我们将为开发目的安装 Odoo。
在本章中,我们将涵盖以下食谱:
-
理解 Odoo 生态系统
-
从源代码安装 Odoo
-
管理 Odoo 服务器数据库
-
将实例配置存储在文件中
-
激活 Odoo 开发者工具
-
更新附加模块列表
技术要求
本章中使用的所有代码都可以从本书的 GitHub 仓库下载,网址为 github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter01。
理解 Odoo 生态系统
Odoo 为开发者提供了开箱即用的模块化和其强大的框架帮助他们快速构建项目。在开始成为成功的 Odoo 开发者的旅程之前,你应该熟悉 Odoo 生态系统中的各种角色。
假设你有一个具有 4 个 CPU 核心、8 GB RAM 和 30 个并发 Odoo 用户的系统。
要确定所需的工作进程数,将用户数除以 6。在这种情况下,30 个用户除以 6 等于 5,这是理论上的所需工作进程数。
要计算理论上的最大工作进程数,将 CPU 核心数乘以 2 并加 1。对于 4 个 CPU 核心,(4 * 2) + 1 等于 9,这是理论上的最大工作进程数。
根据这些计算,你可以为 Odoo 用户使用 5 个工作进程,并为 cron 工作进程额外使用一个工作进程,总共 6 个工作进程。
要估计 RAM 消耗,使用以下公式:
RAM = 工作进程数 * ((0.8 * 150) + (0.2 * 1024))
在这种情况下,6 个工作进程乘以 ((0.8 * 150) + (0.2 * 1024)) 大约等于 2 GB 的 RAM。
因此,根据这些计算,Odoo 安装将需要大约 2 GB 的 RAM。
Odoo 版本
Odoo 有两个不同的版本。第一个是社区版,它是开源的,而第二个是企业版,它需要付费许可。与其他软件供应商不同,Odoo 企业版只是一个包含额外功能或新应用程序的额外应用程序包,这些应用程序添加到社区版中。企业版运行在社区版之上。社区版受通用公共许可证 v3.0(LGPLv3)的约束,并包含所有基本的企业资源规划(ERP)应用程序,如销售、客户关系管理(CRM)、发票、采购和网站构建器。或者,企业版附带 Odoo 企业版许可证,这是一个专有许可证。Odoo 企业版具有几个高级功能,如全面会计、工作室、互联网协议语音(VoIP)、移动响应式设计、电子签名、营销自动化、交付和银行集成、物联网(IoT)等。企业版还为您提供无限的错误修复支持。以下图表显示企业版依赖于社区版,这也是为什么您需要后者才能使用前者:

图 1.1 – 社区版和企业版之间的差异
您可以在此处找到两个版本的完整比较:www.odoo.com/page/editions。
注意
Odoo 在市场上所有开源 ERP 中拥有最多的社区开发者,GitHub 上有 20K+ 的分支,因此您会在应用商店中找到大量的第三方应用程序(模块)。其中一些免费应用程序使用Affero 通用公共许可证版本 3(AGPLv3)。如果您的应用程序依赖于此类应用程序,则不能在您的应用程序中使用专有许可证。具有 Odoo 专有许可证的应用程序只能在具有 LGPL 或其他专有许可证的模块上开发。
Git 仓库
Odoo 的整个代码库托管在 GitHub 上。您可以在这里发布稳定版本的错误/问题。您也可以通过提交拉取请求(PR)来提议一个新功能。Odoo 有多个仓库。以下表格提供了更多信息:
| 仓库 | 用途 |
|---|---|
github.com/odoo/odoo |
这是 Odoo 的社区版。它对公众开放。 |
github.com/odoo/enterprise |
这是 Odoo 的企业版。它仅对官方 Odoo 合作伙伴开放。 |
github.com/odoo-dev/odoo |
这是一个持续开发的仓库。它对公众开放。 |
表 1.1 – Odoo git 仓库
每年,Odoo 发布一个主要版本,这是一个为期 3 年的长期支持版本,以及几个小版本。小版本主要用于 Odoo 的在线 master 分支正在开发中且不稳定,因此建议不要用于生产,因为它可能会破坏您的数据库。
Runbot
Runbot 是 Odoo 的自动化测试环境。每当 Odoo 的 GitHub 分支有新的提交时,Runbot 会拉取这些最新的更改,并为最后四个提交创建构建。在这里,您可以测试所有稳定和开发中的分支。您甚至可以尝试企业版及其开发分支。
每个构建都有一个不同的背景颜色,这表示测试用例的状态。绿色背景颜色表示所有测试用例都成功运行,您可以测试该分支,而红色背景颜色表示该分支上某些测试用例失败,某些功能可能在构建中损坏。您可以查看所有测试用例的日志,这些日志显示了安装过程中确切发生的事情。每个构建都有两个数据库。all 数据库安装了所有模块,而 base 数据库只安装了基础 Odoo 模块。每个构建都安装了基本的演示数据,因此您可以快速测试它,无需额外配置。
备注
您可以通过访问 runbot.odoo.com/runbot 来访问 Runbot。
以下凭证可用于访问任何 Runbot 构建:
-
登录 ID:admin 密码:admin
-
登录 ID:demo 密码:demo
-
登录 ID:portal 密码:portal
备注
这是一个公共测试环境,因此其他用户可能会使用/测试您正在测试的相同分支。
Odoo 应用商店
Odoo 几年前推出了应用商店,并立即受到欢迎。在撰写本文时,已有超过 39,000+ 个不同的应用程序托管在那里。您将在这里找到许多免费和付费应用程序,适用于不同版本,包括针对不同商业领域的特定解决方案,如教育、食品工业和医药。还包括扩展或为现有 Odoo 应用程序添加新功能的程序。应用商店还提供了许多美观的主题,适用于 Odoo 网站构建器。在 第三章,“创建 Odoo 扩展模块”,您将学习如何为您的自定义模块设置定价和货币。
您可以通过访问 www.odoo.com/apps 来访问 Odoo 应用商店。
您可以通过访问 www.odoo.com/apps/themes 来访问 Odoo 的主题。
备注
Odoo 在 13 版本之后开源了几个主题,现在使用了一个高级 JavaScript 脚本 OWL。我们将在 第十六章 中介绍这一点。请注意,这些在之前的版本中是付费主题。这意味着在 Odoo 的 15 和 16 版本中,您可以免费下载和使用这些美观的主题。
Odoo 社区协会
Odoo 社区协会(OCA)是一个非营利组织,负责开发和维护基于社区的 Odoo 模块。所有 OCA 模块都是开源的,并由 Odoo 社区成员维护。OCA 的 GitHub 账户包含多个用于不同 Odoo 应用的仓库。除了 Odoo 模块外,它还包含各种工具、迁移库、会计本地化等。
这里是 OCA 官方 GitHub 账户的 URL:github.com/OCA.
官方 Odoo 帮助论坛
Odoo 拥有一个非常强大的框架,只需通过使用/激活选项或遵循特定模式,就能实现许多事情。因此,如果您遇到一些技术问题或者对某些复杂案例不确定,您可以在 Odoo 官方帮助论坛上发布您的疑问。许多开发者活跃在这个论坛上,包括一些官方 Odoo 员工。
您可以通过访问www.odoo.com/forum/help-1来搜索问题或发布您的新问题。
Odoo 电子学习平台
最近,Odoo 推出了一款新的电子学习平台。该平台提供了许多视频,解释如何使用不同的 Odoo 应用。在撰写本文时,该平台没有技术视频,只有功能视频。
这里是 Odoo 电子学习平台的 URL:www.odoo.com/slides.
从源代码安装 Odoo
强烈建议您使用Linux Ubuntu操作系统来安装 Odoo,因为这是 Odoo 用于所有测试、调试和 Odoo 企业安装的操作系统。此外,大多数 Odoo 开发者使用 GNU/Linux 发行版,因此他们更有可能从 Odoo 社区获得对在GNU/Linux上发生的操作系统级别问题的支持,而不是Windows或macOS。
还建议使用与生产环境中将使用的相同环境(相同的发行版和相同的版本)来开发 Odoo 附加模块。这将避免一些令人不快的惊喜,例如在部署当天发现库的版本与预期不同,具有略微不同且不兼容的行为。如果您的工作站使用的是不同的操作系统,一个很好的方法是在工作站上设置一个虚拟机(VM),并在虚拟机中安装 GNU/Linux 发行版。
注意
Ubuntu 可以作为应用程序在Microsoft Store中获取,所以如果您不想切换到 Ubuntu,可以使用它。
对于这本书,我们将使用 Ubuntu Server 22.04 LTS,但您可以使用任何其他 Debian GNU/Linux 操作系统。无论您选择哪个 Linux 发行版,您都应该对如何从命令行使用它有所了解,并且了解系统管理肯定不会有害。
准备工作
我们假设您已经安装并运行了 Ubuntu 22.04,并且您有一个具有 root 访问权限的账户或已配置 sudo。在以下部分中,我们将安装 Odoo 的依赖项并从 GitHub 下载 Odoo 的源代码。
注意
一些配置需要系统登录用户名,因此当命令行中需要登录用户名时,我们将使用 $(whoami)。这是一个 shell 命令,它将在您输入的命令中替换您的登录名。
如果您拥有 GitHub 账户,某些操作将更容易。如果您还没有,请访问 github.com 并创建一个。
如何操作...
要从源安装 Odoo,请执行以下步骤:
-
运行以下命令以安装主要依赖项:
$ sudo apt-get update $ sudo apt install openssh-server fail2ban python3-pip python3-dev libxml2-dev libxslt1-dev zlib1g-dev libsasl2-dev libldap2-dev build-essential libssl-dev libffi-dev libmysqlclient-dev libpq-dev libjpeg8-dev liblcms2-dev libblas-dev libatlas-base-dev git curl python3-venv python3.10-venv fontconfig libxrender1 xfonts-75dpi xfonts-base -y -
下载并安装 wkhtmltopdf:
$ wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb $ sudo dpkg -i wkhtmltox_0.12.6.1-2.jammy_amd64.deb如果在运行上一条命令后遇到任何错误,请使用以下命令强制安装依赖项:
$ sudo apt-get install -f -
现在,安装 PostgreSQL 数据库:
$ sudo apt install postgresql -y -
配置 PostgreSQL:
$ sudo -i -u postgres createuser -s $(whoami) $ sudo su postgres $ psql alter user $(whoami) with password 'your_password'; \q git:$ git config --global user.name "Your Name"
$ git config --global user.email youremail@example.com
-
克隆 Odoo 代码库:
$ mkdir ~/odoo-dev $ cd ~/odoo-dev odoo-17.0 virtual environment and activate it:$ python3 -m venv ~/venv-odoo-17.0
venv:
$ cd ~/odoo-dev/odoo/ $ pip3 install -r requirements.txt -
创建并启动您的第一个 Odoo 实例:
$ createdb odoo-test http://localhost:8069 and authenticate it by using the admin account and using admin as the password.
注意
如果您需要 RTL 支持,请运行以下命令安装 node 和 rtlcss:sudo apt-get install nodejs npm -y sudo npm install -``g rtlcss。
它是如何工作的...
在 步骤 1 中,我们安装了几个核心依赖项。这些依赖项包括各种工具,如 git、pip3、wget、Python 安装工具等。这些核心工具将帮助我们使用简单命令安装其他 Odoo 依赖项。
在 步骤 2 中,我们下载并安装了 wkhtmltopdf 包,该包用于 Odoo 打印 PDF 文档,如销售订单、发票和其他报告。Odoo 17.0 需要 wkhtmltopdf 的 0.12.6.1 版本,而这个确切版本可能不包括在当前的 Linux 发行版中。幸运的是,wkhtmltopdf 的维护者为各种发行版提供了预构建的包,在 wkhtmltopdf.org/downloads.html 上,我们已经从该 URL 下载并安装了它。
此后,我们配置了用于 Odoo 数据库管理的 PostgreSQL。
PostgreSQL 配置
在 步骤 3 中,我们安装了 PostgreSQL 数据库。
在 步骤 4 中,我们创建了一个新的数据库用户,登录名为我们的系统用户名。$(whoami) 用于获取您的登录名,-s 选项用于赋予超级用户权限。让我们看看为什么需要这些配置。
Odoo 使用 psycopg2 Python 库与 PostgreSQL 数据库连接。要使用 psycopg2 库访问 PostgreSQL 数据库,Odoo 默认使用以下值:
-
默认情况下,
psycopg2尝试连接到与本地连接上当前用户名相同的数据库,这实现了无密码认证(这对于开发环境来说很好) -
本地连接使用 Unix 域套接字
-
数据库服务器监听端口
5432
就这样!你的 PostgreSQL 数据库现在已准备好与 Odoo 连接。
由于这是一个开发服务器,我们已经给用户赋予了--superuser权限。在开发实例中,给 PostgreSQL 用户更多权限是可以的,因为这将是你开发实例。对于生产实例,你可以使用--createdb命令行代替--superuser来限制权限。在生产服务器上,--superuser权限将给攻击者利用已部署代码中某些部分的漏洞提供额外的优势。
如果你想要使用不同登录的用户数据库用户,你需要为该用户提供密码。这是通过在创建用户时在命令行上传递--pwprompt标志来完成的,在这种情况下,命令将提示你输入密码。
如果用户已经创建,并且你想设置密码(或修改忘记的密码),你可以使用以下命令:
$ psql -c "alter role $(whoami) with password 'newpassword'"
如果这个命令失败,并显示错误信息说数据库不存在,那是因为你没有在食谱的第 4 步中创建一个以你的登录名命名的数据库。没关系;只需添加--dbname选项并指定一个现有数据库的名称,例如--dbname template1。
Git 配置
对于开发环境,我们使用 GitHub 上的 Odoo。使用git,你可以轻松地在不同的 Odoo 版本之间切换。请注意,你可以使用git的pull命令获取最新的更改。
在第 5 步中,我们配置了我们的git用户。
在第 6 步中,我们从 Odoo 的官方 GitHub 仓库下载了源代码。我们使用git clone命令下载 Odoo 的源代码。我们使用单个分支,因为我们只想为 17.0 版本创建一个分支。我们还使用了--depth 1来避免下载分支的完整提交历史。这些选项将非常快速地下载源代码,但如果你愿意,可以省略这些选项。
Odoo 开发者还提出了夜间构建,这些构建作为 tar 包和分发包提供。使用git clone的主要优势是,你将能够在源树中提交新的错误修复时更新你的仓库。你还可以轻松测试任何提议的修复,并跟踪回归,以便你可以使你的错误报告更加精确和有助于开发者。
注意
如果你可以访问企业版源代码,你可以在~/odoo-dev目录下的单独文件夹中下载它。
虚拟环境
Python 虚拟环境,或简称为venvs,是隔离的 Python 工作空间。这对于 Python 开发者非常有用,因为它们允许安装不同版本的 Python 库的不同工作空间,可能是在不同的 Python 解释器版本上。
你可以使用python3 -m venv ~/newvenv命令创建任意数量的环境。这将创建一个newvenv目录在指定位置,包含一个bin/子目录和一个lib/python3.10子目录。
在第 7 步中,我们在~/venv-odoo-17.0目录中创建了一个新的虚拟环境。这将是我们 Odoo 的独立 Python 环境,Odoo 的所有 Python 依赖都将安装在这个环境中。
要激活虚拟环境,我们需要使用source命令。我们使用了source ~/venv-odoo-17.0/bin/activate命令来激活虚拟环境。
安装 Python 包
Odoo 的源代码在requirements.txt中有一个 Python 依赖列表。在第 8 步中,我们通过pip3 install命令安装了所有这些依赖项。
就这些了。现在,你可以运行 Odoo 实例。
启动实例
现在是你一直等待的时刻。为了启动我们的第一个实例,在第 9 步中,我们创建了一个新的空数据库,使用了odoo-bin脚本,然后使用以下命令启动了 Odoo 实例:
python3 odoo-bin -d odoo-test -i base --addons-path=addons --db-filter=odoo-test$
你也可以在odoo-bin之前使用./来省略python3,因为它是可执行的 Python 脚本:
./odoo-bin -d odoo-test -i base --addons-path=addons --db-filter=odoo-test$
使用odoo-bin,使用以下命令行参数的脚本:
-
-d database_name: 默认使用此数据库。 -
--db-filter=database_name$: 只尝试连接与提供的正则表达式匹配的数据库。一个 Odoo 安装可以服务于多个实例,这些实例位于不同的数据库中,此参数限制了可用的数据库。尾随的$很重要,因为正则表达式用于匹配模式。这使你能够避免选择以指定字符串开头的名称。 -
--addons-path=directory1,directory2,...: 这是一个逗号分隔的目录列表,Odoo 将在此目录中查找附加组件。在实例创建时扫描此列表以填充实例中可用的附加模块列表。如果你想使用 Odoo 的企业版,则可以使用此选项添加其目录。 -
-i base: 这用于安装基本模块。当你通过命令行创建数据库时,这是必需的。
如果你使用的是与 Linux 登录不同的数据库用户,你需要传递以下附加参数:
-
--db_host=localhost: 使用数据库服务器的 TCP 连接 -
--db_user=database_username: 使用指定的数据库登录 -
--db_password=database_password: 这是用于验证 PostgreSQL 服务器的密码
要获取所有可用选项的概述,请使用--help参数。我们将在本章后面看到更多关于odoo-bin脚本的内容。
当 Odoo 在一个空数据库上启动时,它将创建支持其操作所需的数据库结构。它还会扫描附加路径以查找可用的附加模块,并将一些模块插入到数据库中的初始记录中。这包括具有默认admin密码的admin用户,您将使用它进行身份验证。
将您的网络浏览器指向http://localhost:8069/将带您到您新创建实例的登录页面,如下截图所示:

图 1.2 – Odoo 实例的登录屏幕
这是因为 Odoo 包含一个 HTTP 服务器。默认情况下,它监听 TCP 端口8069上的所有本地网络接口。
管理 Odoo 服务器数据库
当使用 Odoo 时,您实例中的所有数据都存储在 PostgreSQL 数据库中。所有您熟悉的标准化数据库管理工具都可用,但 Odoo 还提供了一些常见操作的 Web 界面。
准备中
我们假设您的工作环境已经设置好,并且您有一个正在运行的实例。
如何操作...
Odoo 数据库管理界面提供了创建、复制、删除、备份和恢复数据库的工具。还有更改主密码的方法,该密码用于保护对数据库管理界面的访问。
访问数据库管理界面
要访问数据库,请执行以下步骤:
-
前往您实例的登录屏幕(如果您已认证,请注销)。
-
点击管理数据库。这将带您导航到
localhost:8069/web/database/manager(您也可以直接将浏览器指向该 URL):

图 1.3 – 数据库管理器
设置或更改主密码
如果您使用默认值设置了实例并且尚未对其进行修改,正如我们将在下一节中解释的,数据库管理屏幕将显示一个警告,告诉您master password实例尚未设置,并建议您通过直接链接设置一个:

图 1.4 – 主密码警告
要设置主密码,请执行以下步骤:
- 点击设置主密码按钮。您将得到一个对话框,要求您填写新主密码字段:

图 1.5 – 设置新的主密码
- 输入一个复杂的新的密码,然后点击继续。
如果主密码已经设置,请点击屏幕底部的设置主密码按钮来更改它。在打开的对话框中,输入旧的主密码和新密码,然后点击继续:

图 1.6 – 修改主密码
注意
主密码是服务器配置文件中的admin_passwd键。如果服务器在没有指定配置文件的情况下启动,将在~/.odoorc中生成一个新的配置文件。有关配置文件的更多信息,请参考下一道菜谱。
创建新数据库
此对话框可用于创建由当前 Odoo 服务器处理的新数据库实例。按照以下步骤操作:
- 在数据库管理屏幕上,点击屏幕底部的创建数据库按钮。这将弹出一个以下对话框:

图 1.7 – 创建数据库对话框
-
按照以下方式填写表格:
-
主密码:这是此实例的主密码。
-
数据库名称:输入您希望创建的数据库名称。
-
电子邮件:在此处添加您的电子邮件地址;这将是您稍后的用户名。
-
密码:输入您想为新实例管理员用户设置的密码。
-
电话号码:设置您的电话号码(可选)。
-
语言:在下拉列表中选择您希望在新数据库中默认安装的语言。Odoo 将自动加载所选语言的翻译。
-
国家:在下拉列表中选择主要公司的国家。选择此选项将自动配置一些事情,包括公司的货币。
-
演示数据:勾选此框以获取演示数据。这对于运行交互式测试或为客户设置演示很有用,但不应用于设计用于包含生产数据的数据库。
-
注意
如果您希望使用数据库来运行模块的自动化测试(参考第七章,调试模块),您需要演示数据,因为 Odoo 中的绝大多数自动化测试都依赖于这些记录才能成功运行。
- 点击继续并等待新数据库初始化。之后,您将被重定向到实例,并以管理员身份连接。
故障排除
如果您被重定向到登录屏幕,这可能是由于filter选项。请注意,odoo-bin start命令会静默执行,只提供当前数据库。为了解决这个问题,只需像在从源安装 Odoo菜谱中所示的那样重新启动 Odoo 而不使用start命令。如果您有一个配置文件(请参阅本章后面的将实例配置存储在文件中菜谱),请检查db_filter选项是否未设置或设置为与新数据库名称匹配的值。
复制数据库
通常,您将有一个现有的数据库,并且您可能想对其进行实验以尝试一个程序或运行一个测试,但又不希望修改现有数据。这里的解决方案很简单:复制数据库并在副本上运行测试。根据需要重复此操作:
- 在数据库管理屏幕上,点击你想要克隆的数据库旁边的重复数据库链接:

图 1.8 – 重复数据库对话框
-
按照以下方式填写表格:
-
主密码:这是 Odoo 服务器的主密码
-
新名称:你想要给副本的名称
-
-
点击继续。
-
你可以点击数据库管理屏幕上新创建的数据库的名称,以访问该数据库的登录屏幕。
删除数据库
当你完成测试后,你将想要清理重复的数据库。为此,执行以下步骤:
- 在数据库管理屏幕上,你将在数据库名称旁边找到删除按钮。点击它将弹出以下对话框:

图 1.9 – 删除数据库对话框
-
填写表格,以及主密码字段,这是 Odoo 服务器的主密码。
-
点击删除。
警告!可能数据丢失!
如果你选择了错误的数据库并且没有备份,将无法恢复丢失的数据。
备份数据库
要创建备份,执行以下步骤:
- 在数据库管理屏幕上,你将在数据库名称旁边找到备份按钮。点击它将弹出以下对话框:

图 1.10 – 备份数据库对话框
-
按照以下方式填写表格:
zip用于生产数据库,因为这是唯一的真实完整备份格式。只有在你不关心文件存储的情况下,才使用pg_dump格式进行开发数据库备份。
-
点击备份。备份文件将被下载到你的浏览器。
还原数据库备份
如果你需要恢复备份,你需要这样做:
- 在数据库管理屏幕上,你将在屏幕底部找到还原数据库按钮。点击它将弹出以下对话框:

图 1.11 – 还原数据库对话框
-
按照以下方式填写表格:
-
主密码:这是 Odoo 服务器的主密码。
-
文件:这是一个之前下载的 Odoo 备份。
-
数据库名称:提供要恢复备份的数据库的名称。该数据库必须在服务器上不存在。
-
此数据库可能已被移动或复制:如果原始数据库在另一台服务器上或已被从当前服务器删除,请选择此数据库已移动。否则,选择此数据库是副本,这是安全的默认选项。
-
-
点击继续。
注意
无法在数据库之上还原数据库。如果你尝试这样做,你会得到一个错误消息(数据库还原错误:数据库已存在)。你需要先删除数据库。
它是如何工作的...
除了更改主密码屏幕外,这些功能在服务器上运行 PostgreSQL 管理命令并通过 Web 界面返回报告。
主密码是仅存在于 Odoo 服务器配置文件中的一条非常重要的信息,永远不会存储在数据库中。曾经有一个默认值是admin,但使用此值是一个安全风险。在 Odoo v9 及以后的版本中,这被标识为未设置的主密码,并且当您访问数据库管理界面时,强烈建议您更改它。即使它存储在配置文件下的admin_passwd条目中,这也不等同于admin用户的密码;这两个密码是独立的。主密码是为 Odoo 服务器进程设置的,它可以处理多个数据库实例,每个实例都有一个独立的admin用户及其自己的密码。
安全考虑
记住,在本章中,我们考虑的是开发环境。当您在生产服务器上工作时,Odoo 数据库管理界面是需要被保护的东西,因为它可以访问大量敏感信息,尤其是如果服务器托管了多个不同客户的 Odoo 实例。
要创建一个新的数据库,Odoo 使用 PostgreSQL 的createdb工具,并调用内部 Odoo 函数以与您在空数据库上启动 Odoo 时相同的方式初始化新数据库。
要复制数据库,Odoo 使用createdb的--template选项,将原始数据库作为参数传递。这使用内部和优化的 PostgreSQL 例程在新数据库中复制模板数据库的结构,这比创建备份然后恢复要快得多(尤其是在使用需要您下载备份文件并再次上传的 Web 界面时)。
备份和恢复操作分别使用pg_dump和pg_restore工具。当使用zip格式时,备份还将包括一个文件存储的副本,其中包含当您配置 Odoo 使其不将这些文件保存在数据库中时的文档副本;这是 14.0 版本中的默认选项。除非您更改它,否则这些文件将驻留在~/.local/share/Odoo/filestore。
如果备份变得过大,下载它可能会失败。这可能是由于 Odoo 服务器本身无法在内存中处理大文件,或者因为服务器运行在反向代理后面,因为代理中设置了 HTTP 响应大小的限制。相反,由于相同的原因,您可能会在数据库恢复操作中遇到问题。当您开始遇到这些问题时,是时候投资一个更健壮的外部备份解决方案了。
还有更多...
经验丰富的 Odoo 开发者通常不使用数据库管理界面,而是从命令行执行操作。例如,要使用演示数据初始化新数据库,可以使用以下单行命令:
$ createdb testdb && odoo-bin -d testdb
使用此命令行的优点是,您可以在使用它时请求安装附加组件 – 例如,-i sale,purchase,stock。
要复制数据库,停止服务器并运行以下命令:
$ createdb -T dbname newdbname
$ cd ~/.local/share/Odoo/filestore # adapt if you have changed the data_dir
$ cp -r dbname newdbname
$ cd -
注意,在开发环境中,通常省略文件存储。
注意
使用createdb -T仅在数据库上没有活动会话时才有效,这意味着在从命令行复制数据库之前,您必须关闭您的 Odoo 服务器。
要删除实例,请运行以下命令:
$ dropdb dbname
$ rm -rf ~/.local/share/Odoo/filestore/dbname
要创建备份(假设 PostgreSQL 服务器在本地运行),请使用以下命令:
$ pg_dump -Fc -f dbname.dump dbname
$ tar cjf dbname.tgz dbname.dump ~/.local/share/Odoo/filestore/dbname
要恢复备份,请运行以下命令:
$ tar xf dbname.tgz
$ pg_restore -C -d dbname dbname.dump
警告!
如果您的 Odoo 实例使用不同的用户连接到数据库,您需要传递-U username以确保正确的用户是恢复的数据库的所有者。
将实例配置存储在文件中
odoo-bin脚本有数十个选项,记住它们所有以及如何在启动服务器时正确设置它们是很繁琐的。幸运的是,可以将它们全部存储在配置文件中,并且只需指定您想要在开发中更改的选项。
如何完成...
对于此配方,请执行以下步骤:
-
要为您的 Odoo 实例生成配置文件,请运行以下命令:
$ ./odoo-bin --save --config myodoo.cfg --stop-after-init -
您可以添加额外的选项,它们的值将保存在生成的文件中。所有未设置选项都将使用默认值保存。要获取可能的选项列表,请使用以下命令:
--without-demo will become without_demo. This works for most options, but there are a few exceptions, all of which are listed in the following section. -
编辑
myodoo.cfg文件(使用以下章节中的表格来更改您可能想要更改的一些参数)。然后,要使用保存的选项启动服务器,请运行以下命令:$ ./odoo-bin -c myodoo.cfg
注意
--config选项通常缩写为-c。
它是如何工作的...
启动时,Odoo 通过三次遍历加载其配置。首先,从源代码初始化所有选项的默认值集。然后解析配置,然后任何在文件中定义的值将覆盖默认值。最后,分析命令行选项,它们的值将覆盖之前遍历中获得的配置。
如我们之前提到的,可以通过查看命令行选项的名称来找到配置变量的名称,方法是去除前导破折号并将中间破折号转换为下划线。这里有几个例外,特别是以下这些:

表 1.1 – Odoo 参数在命令行和配置文件中的差异
这里是一个通常通过配置文件设置的选项列表:

表 1.2 – Odoo 参数及其用法
这里是一个与数据库相关的配置选项列表:

表 1.3 – Odoo 参数及其用法
Odoo 使用 Python 的 ConfigParser 模块解析配置文件。然而,Odoo 11.0 中的实现已经改变,不再可能使用变量插值。所以,如果你习惯于使用 %(section.variable)s 语法从其他变量的值定义变量的值,你需要改变你的习惯,并回到显式值。
一些选项在配置文件中未使用,但在开发过程中广泛使用:

表 1.4 – Odoo 参数及其用法
激活 Odoo 开发者工具
当作为开发者使用 Odoo 时,你需要知道如何在网页界面中激活开发者模式,以便你可以访问技术设置菜单和开发者信息。启用调试模式将暴露几个高级配置选项和字段。这些选项和字段在 Odoo 中被隐藏,以提供更好的可用性,因为它们不是每天都会使用。
如何做到这一点...
在网页界面中激活开发者模式,请执行以下步骤:
-
连接到你的实例并以
admin身份进行认证。 -
前往设置菜单。
-
滚动到页面底部并找到开发者工具部分:

图 1.12 – 激活不同开发者模式的链接
-
点击激活 开发者模式。
-
等待用户界面重新加载。
替代方法
也可以通过编辑 URL 来激活开发者模式。在 # 符号之前插入 ?debug=1。例如,如果你的当前 URL 是 http://localhost:8069/web#menu_id=102&action=94 并且你想启用开发者模式,那么你需要将那个 URL 更改为 http://localhost:8069/web?debug=1#menu_id=102&action=94。此外,如果你想要使用带有资源的调试模式,那么将 URL 更改为 http://localhost:8069/web?debug=assets#menu_id=102&action=94。
要退出开发者模式,你可以执行以下任何一个操作:
-
编辑 URL 并在查询字符串中写入
?debug=0。 -
从设置菜单中的相同位置使用停用开发者模式。
-
在顶部菜单中点击虫子图标,然后点击离开开发者工具选项。
许多开发者都在使用浏览器扩展来切换调试模式。通过这样做,你可以快速切换调试模式,而无需访问设置菜单。这些扩展适用于 Firefox 和 Chrome。以下截图显示了你可以使用并从 Chrome 商店找到的一个插件:

图 1.13 – 调试模式的浏览器扩展
注意
自 Odoo v13 以来,调试模式的行为已发生变化。从 v13 开始,调试模式的状态存储在会话中,这意味着即使你从 URL 中移除了?debug,调试模式仍然会保持激活状态。
它是如何工作的...
在开发模式下,会发生两件事:
-
当你在表单视图中的一个字段上或列表视图中的一个列上悬停时,你会得到工具提示。这些提供了有关字段的技术信息(内部名称、类型等)。
-
在右上角用户菜单旁边显示一个带有虫子图标的下拉菜单,让你可以访问显示的模型的技术信息、各种相关视图定义、工作流程、自定义过滤器管理等等。
存在一种名为开发模式(带资产)的开发模式变体。这种模式的行为类似于正常的开发模式,但发送到浏览器的 JavaScript 和 CSS 代码没有被压缩,这意味着你可以轻松地使用浏览器中的 Web 开发工具来调试 JavaScript 代码(更多内容请参阅第十五章,Web 客户端开发)。
谨慎!
在开发模式下和不带开发模式的情况下测试你的附加组件,因为 JavaScript 库的非压缩版本可能会隐藏只有在你压缩版本中才会出现的错误。
更新附加模块列表
当添加新的附加模块时,你需要运行更新模块列表向导,以便将你的新应用程序添加到应用程序列表中。在这个配方中,你将学习如何更新应用程序列表。
准备工作
使用你的管理员账户启动你的实例并连接到它。完成此操作后,激活开发模式(如果你不知道如何激活开发模式,请参阅激活 Odoo 开发 工具配方)。
如何操作…
要更新你的实例中可用的附加模块列表,你需要执行以下步骤:
-
打开应用程序菜单。
-
点击更新 应用程序列表:

图 1.14 – 更新应用程序列表
- 在出现的对话框中,点击更新:

图 1.15 – 更新应用程序列表的对话框
- 更新完成后,你可以点击应用程序条目来查看可用的附加模块的更新列表。你需要在搜索框中移除应用程序上的默认过滤器才能看到所有这些。
它是如何工作的…
当 Odoo 读取存储在附加模块目录中的__manifest__.py时。它期望找到一个 Python 字典。除非清单中包含一个设置为False的installable键,否则附加模块元数据将被记录在数据库中。如果模块已经存在,信息将被更新。如果没有,将创建一个新的记录。如果之前可用的附加模块未找到,则记录不会被从列表中删除。
注意
只有在你初始化数据库后添加新的附加路径时,才需要更新应用程序列表。如果你在初始化数据库之前将新的附加路径添加到配置文件中,那么就无需手动更新模块列表。
总结到目前为止我们所学的,安装完成后,你可以通过以下命令行启动 Odoo 服务器(如果你使用的是虚拟环境,那么你首先需要激活它):
python3 odoo-bin -d odoo-test -i base --addons-path=addons --db-filter=odoo-test
一旦运行了模块,你就可以通过 http://localhost:8069 访问 Odoo。
你也可以使用配置文件来运行 Odoo,如下所示:
./odoo-bin -c myodoo.cfg
一旦启动了 Odoo 服务器,你就可以从 应用 菜单中安装/更新模块。
第二章:管理 Odoo 服务器实例
在第一章中,安装 Odoo 开发环境,我们探讨了如何仅使用源代码中提供的标准核心附加模块来设置 Odoo 实例。作为自定义 Odoo 默认功能的常规做法,我们创建一个单独的模块并将其保存在不同的存储库中,以便您可以稍后升级 Odoo 默认功能和您的存储库以保持其整洁。本章重点介绍向 Odoo 实例添加非核心或自定义附加模块。在 Odoo 中,您可以从多个目录加载附加模块。此外,建议您从单独的文件夹中加载第三方附加模块或您自己的自定义附加模块,以避免与 Odoo 核心模块冲突。即使是 Odoo 企业版也是一个附加模块目录,您需要像正常附加模块目录一样加载它。
在本章中,我们将涵盖以下食谱:
-
配置附加模块路径
-
标准化实例目录布局
-
安装和升级本地附加模块
-
从 GitHub 安装附加模块
-
应用附加模块的更改
-
应用和尝试提出的拉取请求(PRs)
关于术语
在本书中,我们将使用模块一词互换。所有这些都指的是可以从用户界面安装到 Odoo 的应用程序或扩展应用程序。
配置附加模块路径
通过addons_path参数的帮助,您可以将自己的附加模块加载到 Odoo 中。当 Odoo 初始化一个新的数据库时,它将在addons_path配置参数中提供的目录内搜索附加模块。Odoo 将在这些目录中搜索潜在的附加模块。
在addons_path中列出的目录应包含子目录,每个子目录都是一个附加模块。在数据库初始化之后,您将能够安装这些目录中给出的模块。
准备工作
本食谱假设您已经准备好一个实例,并生成了一个配置文件,如第一章中“将实例配置存储在文件中”食谱所述,安装 Odoo 开发环境。请注意,Odoo 的源代码位于~/odoo-dev/odoo,配置文件位于~/odoo-dev/odoo/myodoo.cfg。
如何操作…
要将~/odoo-dev/local-addons目录添加到实例的addons_path参数中,请执行以下步骤:
-
编辑您实例的配置文件;即
~/odoo-dev/myodoo.cfg。 -
定位以
addons_path=开头的行。默认情况下,它应该看起来像以下这样:addons_path = ~/odoo-dev/odoo/odoo/addons,~/odoo-dev/odoo/addons -
通过在后面添加逗号,然后是您想要添加到
addons_path中的目录名称,来修改该行,如下面的代码所示:addons_path = ~odoo-dev/odoo/odoo/addons,~odoo-dev/odoo/addons,~/odoo-dev/local-addons -
从终端重启您的实例:
$ ~/odoo-dev/odoo/odoo-bin -c myodoo.cfg
它是如何工作的…
当 Odoo 重新启动时,会读取配置文件。期望addons_path变量的值是一个逗号分隔的目录列表。接受相对路径,但它们相对于当前工作目录,因此在配置文件中应避免使用。
到目前为止,我们只在 Odoo 中列出了附加组件目录,但在~/odoo-dev/local-addons中没有任何附加组件模块。即使您将新的附加组件模块添加到此目录,Odoo 也不会在用户界面中显示此模块。为此,您需要执行额外操作,如前一章中“更新附加组件模块列表”食谱中所述。
注意
原因在于,当您初始化新数据库时,Odoo 会自动将您的自定义模块列在可用模块中,但如果您在数据库初始化后添加新模块,那么您就需要手动更新可用模块列表,如第一章,“安装 Odoo 开发环境”中“更新附加组件模块列表”食谱所示。
还有更多...
当您第一次调用odoo-bin脚本以初始化新数据库时,您可以使用逗号分隔的目录列表传递--addons-path命令行参数。这将初始化包含在提供的附加组件路径中找到的所有附加组件的可用模块列表。当您这样做时,您必须明确包含基本附加组件目录(odoo/odoo/addons)以及核心附加组件目录(odoo/addons)。与前面的食谱略有不同的是,本地附加组件不能为空;它们必须包含至少一个子目录,该子目录具有附加模块的最小结构。
在第三章,“创建 Odoo 附加组件模块”中,我们将探讨如何编写您自己的模块。在此期间,这里有一个快速技巧来生成 Odoo 会高兴的东西:
$ mkdir -p ~/odoo-dev/local-addons/dummy
$ touch ~/odoo-dev/local-addons/dummy/__init__.py
$ echo '{"name": "dummy", "installable": False}' > \
~/odoo-dev/local-addons/dummy/__manifest__.py
您可以使用--save选项来保存配置文件的路径:
$ odoo/odoo-bin -d odoo-test \
--addons-path="odoo/odoo/addons,odoo/addons,~/odoo-dev/local-addons" \
--save -c ~/odoo-dev/myodoo.cfg --stop-after-init
在这种情况下,使用相对路径是可以的,因为它们将在配置文件中转换为绝对路径。
注意
由于 Odoo 仅在从命令行设置路径时检查附加路径中的目录以查找附加组件,而不是在从配置文件加载路径时检查,因此虚拟模块不再必要。因此,您可以将其删除(或者保留,直到您确信您不需要创建新的配置文件)。
标准化实例目录布局
我们建议您的开发和生产环境都使用类似的目录布局。这种标准化将在您必须执行维护操作时非常有用,它还将使您日常工作更加轻松。
此食谱创建了一个目录结构,将具有相似生命周期或类似目的的文件分组到标准子目录中。
注意
此配方仅在你想要管理类似的结构化开发和生产环境时有用。如果你不希望这样做,你可以跳过此配方。
此外,没有必要遵循此配方中相同的文件夹结构。请随意更改此结构以适应你的需求。
我们生成一个干净的目录结构,具有清晰标记的目录和专用角色。我们使用不同的目录来存储以下内容:
-
由其他人维护的代码(在
src/中) -
本地特定代码
-
实例的文件存储
如何做到这一点…
要创建建议的实例布局,你需要执行以下步骤:
-
为每个实例创建一个目录:
$ mkdir ~/odoo-dev/projectname virtualenv object in a subdirectory called env/:$ python3 -m venv env
-
创建一些子目录,如下所示:
src/: This contains the clone of Odoo itself, as well as various third-party add-on projects (we have added Odoo source code to the next step in this recipe) -
local/:这用于保存你的实例特定附加组件 -
bin/:这包括各种辅助可执行 shell 脚本 -
filestore/:这被用作文件存储 -
logs/(可选):这用于存储服务器日志文件 -
克隆 Odoo 并安装需求(有关此操作的详细信息,请参阅第一章,安装 Odoo 开发环境):
$ git clone -b 17.0 --single-branch --depth 1 https://github.com/odoo/odoo.git src/odoo bin/odoo:!/bin/sh ROOT=$(dirname $0)/..
PYTHON=\(ROOT/env/bin/python3 ODOO=\)ROOT/src/odoo/odoo-bin
$PYTHON $ODOO -c \(ROOT/projectname.cfg "\)@" exit $?
-
使脚本可执行:
$ chmod +x bin/odoo -
创建一个空的虚拟本地模块:
$ mkdir -p local/dummy $ touch local/dummy/ init .py $ echo '{"name": "dummy", "installable": False}' >\ local/dummy/ manifest .py -
为你的实例生成一个配置文件:
$ bin/odoo --stop-after-init --save \ --addons-path src/odoo/odoo/addons,src/odoo/addons,local \ .gitignore file, which is used to tell GitHub to exclude given directories so that Git will ignore these directories when you commit the code; for example, filestore/, env/, logs/, and src/:dotfiles,但有例外:
.*
!.gitignore
python 编译文件
*.py[co]
emacs 备份文件
*~
未跟踪的子目录
/env/
/src/
/filestore/
/logs/
-
为此实例创建一个 Git 仓库,并将你添加到 Git 的文件:
$ git init $ git add . $ git commit -m "initial version of projectname"
它是如何工作的…
通过为每个项目使用一个 virtualenv 环境,我们确保项目的依赖项不会与其他项目的依赖项冲突,这些项目可能运行不同版本的 Odoo 或将使用需要不同 Python 依赖项版本的第三方附加模块。这以牺牲少量磁盘空间为代价。
以类似的方式,通过使用针对我们不同项目的单独克隆的 Odoo 和第三方附加模块,我们能够让每个项目独立发展,并且只在需要它们的实例上安装更新,从而降低引入回归的风险。
bin/odoo 脚本允许我们运行服务器而无需记住各种路径或激活 virtualenv 环境。这也为我们设置了配置文件。你可以在其中添加额外的脚本以帮助你在日常工作中。例如,你可以添加一个脚本来检出你需要运行实例的不同第三方项目。
关于配置文件,我们在这里只演示了设置的基本选项,但你可以显然设置更多,例如数据库名称、数据库过滤器或项目监听的端口。有关此主题的更多信息,请参阅第一章,安装 Odoo 开发环境。
最后,通过在 Git 仓库中管理所有这些,使得在不同的计算机上复制设置变得非常容易,并且可以在团队间共享开发。
加速技巧
为了方便项目创建,你可以创建一个包含空结构的模板仓库,并为每个新项目分叉该仓库。这将帮助你避免重复输入bin/odoo脚本、.gitignore文件以及任何其他需要的模板文件(如README.md、变更日志等)。
更多...
复杂模块的开发需要各种配置选项,这导致每次想要尝试任何配置选项时都需要更新配置文件。频繁更新配置文件可能会让人头疼,为了避免这种情况,一种替代方法是直接从命令行传递所有配置选项,如下所示:
-
手动激活
virtualenv:$ source env/bin/activate -
进入 Odoo 源目录:
$ cd src/odoo -
运行服务器:
./odoo-bin --addons-path=addons,../../local -d test-16 -i account,sale,purchase --log-level=debug
在步骤 3中,我们直接从命令行传递了一些配置选项。第一个是--addons-path,它加载 Odoo 的核心附加模块目录addons和你的附加模块目录local,你将在其中放置自己的附加模块。-d选项将使用test-16数据库或如果没有则创建一个新的数据库。-i选项将安装account、sale和purchase模块。接下来,我们传递了log-level选项并将日志级别增加到debug,以便在日志中显示更多信息。
注意
通过使用命令行,你可以快速更改配置选项。你还可以在终端中查看实时日志。有关所有可用选项,请参阅第一章,安装 Odoo 开发环境,或使用--help命令查看所有选项及其描述。
安装和升级本地附加模块
Odoo 的核心功能来自于其附加模块。Odoo 本身提供了丰富的附加模块,同时你也可以从应用商店下载附加模块,或者自己编写附加模块。
在本食谱中,我们将演示如何通过 Web 界面和命令行安装和升级附加模块。
使用命令行进行这些操作的主要好处包括能够同时操作多个附加模块,以及在安装或更新过程中清晰地查看服务器日志,这在开发模式或编写实例安装脚本时非常有用。
准备工作
确保您有一个正在运行的 Odoo 实例,其数据库已初始化,并且插件路径已正确设置。在本食谱中,我们将安装/升级几个插件模块。
如何操作...
安装或更新插件有两种可能的方法——您可以使用 Web 界面或命令行。
从 Web 界面
要使用 Web 界面在您的数据库中安装新的插件模块,请执行以下步骤:
- 使用管理员账户连接到实例并打开应用菜单:

图 2.1 – Odoo 应用列表
-
使用搜索框定位您想要安装的插件。以下是一些帮助您完成此任务的说明:
-
激活未安装过滤器。
-
如果您在寻找特定功能的插件而不是广泛功能的插件,请移除应用过滤器。
-
在搜索框中输入模块名称的一部分,并将其用作模块过滤器。
-
您可能会发现使用列表视图更易于阅读。
-
-
在卡片下模块名称下点击安装按钮。
注意,一些 Odoo 插件模块有外部 Python 依赖项。如果系统上未安装 Python 依赖项,则 Odoo 将中止安装,并显示以下对话框:

图 2.2 – 外部库依赖警告
要解决这个问题,只需在您的系统上安装相关的 Python 依赖项。
要更新数据库中预安装的模块,请执行以下步骤:
-
使用管理员账户连接到实例。
-
打开应用菜单。
-
点击应用:

图 2.3 – Odoo 应用列表
-
使用搜索框定位您想要安装的插件。以下是一些建议:
-
激活
crm并按Enter键搜索 CRM 应用。 -
您可能会发现使用列表视图更易于阅读。
-
-
在卡片右上角的三点处点击,然后点击升级选项:

图 2.4 – 升级模块的下拉链接
激活开发者模式以查看模块的技术名称。如果您不知道如何激活开发者模式,请参阅第一章,安装 Odoo 开发环境:

图 2.5 – 应用程序的技术名称
激活开发者模式后,它将以红色显示模块的技术名称。如果您使用的是 Odoo 社区版,您将看到一些带有升级按钮的额外应用。这些应用是 Odoo 企业版应用,为了安装/使用它们,您需要购买许可证。
从命令行
在您的数据库中安装新插件,请执行以下步骤:
-
找到扩展的名称。这是包含
__manifest__.py文件的目录名称,不包括前导路径。 -
停止实例。如果您正在处理生产数据库,请进行备份。
-
运行以下命令:
$ odoo/odoo-bin -c instance.cfg -d dbname -i addon1,addon2 \ --stop-after-init
如果在配置文件中已设置,则可以省略-d dbname。
- 重新启动实例。
要更新数据库中已安装的扩展模块,请执行以下步骤:
-
找到要更新的扩展模块名称;这是包含
__manifest__.py文件的目录名称,不包括前导路径。 -
停止实例。如果您正在处理生产数据库,请进行备份。
-
运行以下命令:
$ odoo/odoo-bin -c instance.cfg -d dbname -u addon1 \ --stop-after-init
如果在配置文件中已设置,则可以省略-d dbname。
- 重新启动实例。
它是如何工作的…
扩展模块的安装和更新是两个密切相关的过程,但有一些重要区别,如下两个部分所强调的。
扩展安装
当您安装扩展时,Odoo 会检查其可用的扩展列表,以查找具有提供名称未安装的扩展。它还会检查该扩展的依赖项,如果有,它会在安装扩展之前递归地安装它们。
单个模块的安装过程包括以下步骤:
-
如果有任何依赖项,运行
preinit扩展钩子。 -
从 Python 源代码加载模型定义,并根据需要更新数据库结构(有关详细信息,请参阅第四章,应用模型)。
-
加载扩展的数据文件,并根据需要更新数据库内容(有关详细信息,请参阅第六章,管理模块数据)。
-
如果在实例中启用了演示数据,则安装扩展的演示数据。
-
如果有任何依赖项,运行扩展的
postinit钩子。 -
运行扩展视图定义的验证。
-
如果启用了演示数据和测试,则运行扩展的测试(有关详细信息,请参阅第十八章,自动化测试用例)。
-
更新数据库中的模块状态。
-
从扩展的翻译中更新数据库中的翻译(有关详细信息,请参阅第十一章,国际化)。
注意
preinit和postinit钩子分别使用pre_init_hook和post_init_hook键在__manifest__.py文件中定义。这些钩子用于在安装扩展模块之前和之后调用 Python 函数。要了解更多关于init钩子的信息,请参阅第三章,创建 Odoo 扩展模块。
扩展更新
当您更新扩展时,Odoo 会检查其可用的扩展模块列表,以查找具有给定名称已安装的扩展。它还会检查该扩展的逆向依赖项(这些是依赖于更新扩展的扩展)。如果有,它也会递归地更新它们。
单个附加组件模块的更新过程包括以下步骤:
-
如果有,运行附加组件模块的预迁移步骤(有关详细信息,请参阅第六章,管理模块数据)。
-
从 Python 源代码加载模型定义,并在必要时更新数据库结构(有关详细信息,请参阅第四章,应用模型)。
-
加载附加组件的数据文件,并在必要时更新数据库内容(有关详细信息,请参阅第六章,管理模块数据)。
-
如果实例中启用了演示数据,则更新附加组件的演示数据。
-
如果你的模块有任何迁移方法,运行附加组件的后期迁移步骤(有关详细信息,请参阅第六章,管理模块数据)。
-
运行附加组件视图定义的验证。
-
如果启用了演示数据和测试,则运行附加组件的测试(有关详细信息,请参阅第十八章,自动化测试用例)。
-
更新数据库中的模块状态。
-
从附加组件的翻译中更新数据库中的翻译(有关详细信息,请参阅第十一章,国际化)。
注意
注意,更新尚未安装的附加组件模块没有任何作用。然而,安装已安装的附加组件模块会重新安装该附加组件,这可能会对某些数据文件产生一些意外影响,这些数据文件包含应由用户更新但在正常模块更新过程中未更新的数据(请参阅第六章,管理模块数据中的使用 noupdate 和 forcecreate 标志配方)。用户界面没有错误风险,但这种情况可能会在命令行中发生。
更多内容...
在处理依赖关系时要小心。考虑这样一个例子,你想要安装sale、sale_stock和sale_specific附加组件,其中sale_specific依赖于sale_stock,而sale_stock依赖于sale。要安装所有三个,你只需要安装sale_specific,因为它会递归地安装sale_stock和sale依赖项。要更新所有三个,你需要更新sale,因为这会递归地更新反向依赖项sale_stock和sale_specific。
在管理依赖关系时,一个棘手的部分是当你向已经安装了版本的插件添加依赖项时。让我们通过继续之前的例子来理解这一点。想象一下,你在sale_specific中添加了对stock_dropshipping的依赖。更新sale_specific插件不会自动安装新的依赖项,请求安装sale_specific也不会。在这种情况下,你可能会收到非常糟糕的错误消息,因为插件的 Python 代码没有成功加载,但插件的数据和数据库中模型的表是存在的。为了解决这个问题,你需要停止实例并手动安装新的依赖项。
从 GitHub 安装插件模块
GitHub 是第三方插件的绝佳来源。许多 Odoo 合作伙伴使用 GitHub 来共享他们内部维护的插件,Odoo 社区协会(OCA)在 GitHub 上共同维护了数百个插件。在你开始编写自己的插件之前,请确保检查是否已经存在你可以直接使用或作为起点的东西。
本食谱将向您展示如何从 GitHub 克隆 OCA 的partner-contact项目,并使其包含的插件模块在您的实例中可用。
准备工作
假设你想要向客户(合作伙伴)表单添加新字段。默认情况下,Odoo 客户模型没有gender字段。如果你想添加一个gender字段,你需要创建一个新的模块。幸运的是,邮件列表上的某个人告诉你关于partner_contact_gender插件模块的信息,该模块由 OCA 作为partner-contact项目的一部分维护。
本食谱中使用的路径反映了在标准化实例目录布局食谱中提出的布局。
如何操作...
要安装partner_contact_gender,请执行以下步骤:
-
进入你的项目目录:
17.0 branch of the partner-contact project in the src/ directory:$ git clone --branch 17.0 \
实例.cfg 中的 add-ons_path 行应如下所示:
addons_path = ~/odoo-dev/my-odoo/src/odoo/odoo/addons, \ ~/odoo-dev/my-odoo/src/odoo/addons, \ ~/odoo-dev/my-odoo/src/partner-contact, \ ~/odoo-dev/local-addons -
安装
partner_contact_gender插件(如果你不知道如何安装模块,请查看之前的食谱,安装和升级本地插件模块)。
它是如何工作的...
所有 OCA 代码存储库都将它们的插件包含在单独的子目录中,这与 Odoo 对插件路径中目录的期望是一致的。因此,只需在某个位置克隆存储库并将其添加到插件路径中就足够了。
更多内容...
一些维护者采用不同的方法,每个存储库有一个插件模块,位于存储库的根目录。在这种情况下,你需要创建一个新的目录,将其添加到插件路径中,并在该目录中克隆你需要的所有插件。记住,每次添加新的存储库克隆时,都要更新插件模块列表。
应用更改以添加插件
大多数在 GitHub 上可用的附加模块都可能发生变化,并且不遵循 Odoo 对其稳定版本施加的规则。它们可能收到错误修复或增强,包括您提交的问题或功能请求,这些更改可能引入数据库模式更改或数据文件和视图的更新。本食谱解释了如何安装更新版本。
准备工作
假设您报告了partner_contact_gender的问题,并收到通知称该问题已在partner-contact项目的17.0分支的最新修订版中解决。在这种情况下,您可能希望使用这个最新版本更新您的实例。
如何操作…
要将 GitHub 上的源代码修改应用到您的附加模块,您需要执行以下步骤:
-
停止使用该附加模块的实例。
-
如果是生产实例,请备份(参考第一章中的管理 Odoo 服务器数据库食谱,安装 Odoo 开发环境)。
-
前往
partner-contact克隆的目录:$ cd ~/odoo-dev/my-odoo/src/partner-contact -
为项目创建一个本地标签,以便在出现问题的情况下回滚到该版本:
$ git checkout 17.0 $ git tag 17.0-before-update-$(date --iso) -
获取源代码的最新版本:
partner_address_street3 add-on in your databases (refer to the *Installing and upgrading local add-on* *modules* recipe). -
重新启动实例。
它是如何工作的…
通常,附加模块的开发者偶尔会发布附加模块的最新版本。此更新通常包含错误修复和新功能。在这里,我们将获取附加模块的新版本并在我们的实例中更新它。
如果git pull --ff-only失败,您可以使用以下命令回滚到上一个版本:
$ git reset --hard 17.0-before-update-$(date --iso)
然后,您可以尝试不带--ff-only的git pull,这将导致合并,但这意味着您在附加模块上有本地更改。
参见
如果更新步骤失败,请参考第一章中的从源更新 Odoo食谱,安装 Odoo 开发环境,以获取恢复说明。请记住,始终在数据库生产副本上测试更新。
应用和尝试提议的 PR
在 GitHub 的世界里,PR 是由开发者提出的一个请求,以便项目的维护者可以包含一些新的开发成果。这样的 PR 可能包含错误修复或新功能。这些请求在合并到main分支之前会经过审查和测试。
本食谱解释了如何将 PR 应用到您的 Odoo 项目中以测试改进或错误修复。
准备工作
如前一个食谱中所述,假设您报告了partner_address_street3的问题,并收到通知称该问题已在项目的17.0分支的一个未合并的 PR 中解决。开发者要求您在 PR #123 中验证修复。您需要使用这个分支更新测试实例。
你不应该直接在生产数据库上尝试这样的分支,所以首先创建一个包含生产数据库副本的测试环境(请参阅第一章,安装 Odoo 开发环境)。
如何做到这一点…
要应用和尝试 GitHub PR 的附加模块,你需要执行以下步骤:
-
停止实例。
-
前往
partner-contact被克隆的目录:$ cd ~/odoo-dev/my-odoo/src/partner-contact -
为项目创建一个本地标签,以便在事情出错时可以回滚到该版本:
$ git checkout 17.0 123:在你的数据库中添加
partner_contact_gender1附加模块并重启实例(如果你不知道如何更新模块,请参阅安装和升级本地附加模块配方)。 -
测试更新—尝试重现你的问题,或者尝试你想要的特性。
如果这不起作用,请在 GitHub 的 PR 页面上评论,解释你做了什么以及什么没有起作用,以便开发者可以更新 PR。
如果它有效,请在 PR 页面上说明;这是 PR 验证过程的重要组成部分,并将加快main分支的合并。
它是如何工作的…
我们正在使用一个 GitHub 功能,该功能允许使用pull/nnnn/head分支名称通过数字拉取 PR,其中nnnn是 PR 的编号。git pull命令将合并远程分支到我们的分支中,在我们的代码库中应用更改。之后,我们更新附加模块,测试它,并向更改的作者报告任何失败或成功。
更多内容…
如果你想要同时测试同一存储库中的不同 PR,你可以重复此配方中的第 4 步:
$ git checkout -b 17.0-custom
使用不同的分支将帮助你记住你使用的是来自 GitHub 的版本,而是一个自定义版本。
注意
可以使用git branch命令列出你仓库中所有的本地分支。
从那时起,如果你需要应用来自 GitHub 的17.0分支的最新修订版,你将需要在不使用--ff-only的情况下拉取它:
$ git pull origin 17.0
第三章:创建 Odoo 附加模块
现在我们已经拥有了开发环境,并且知道如何管理 Odoo 服务器实例和数据库,我们将学习如何创建 Odoo 附加模块。
本章的主要目标是理解附加模块的结构以及向其中添加组件的典型增量工作流程。本章食谱名称中提到的各种组件将在后续章节中详细介绍。
Odoo 模块可以包含多个元素:
-
业务对象:
- 声明为 Python 类,这些资源根据其配置自动由 Odoo 持久化
-
对象视图:
- 业务对象 UI 显示的定义
-
数据文件(声明模型元数据的 XML 或 CSV 文件):
-
视图或报告
-
配置数据(模块参数化和安全规则)
-
示例数据和更多内容
-
-
Web 控制器:
- 处理来自网络浏览器、静态网络数据图像、或由 Web 界面或网站使用的 CSS 或 JavaScript 文件的请求
本章中,我们将涵盖以下食谱:
-
创建和安装新的附加模块
-
完成附加模块清单
-
组织附加模块的文件结构
-
添加模型
-
添加菜单项和视图
-
添加访问安全
-
使用
scaffold命令创建模块
技术要求
对于本章,你应已安装 Odoo,并且应已遵循第一章,安装 Odoo 开发环境中的食谱。你还应熟悉发现和安装额外附加模块,如第二章,管理 Odoo 服务器实例中所述。
本章中使用的所有代码都可以从 GitHub 仓库github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter03下载。
什么是 Odoo 附加模块?
除了框架代码外,Odoo 的所有代码库都以模块的形式打包。这些模块可以从数据库中随时安装或卸载。这些模块有两个主要目的。你可以添加新的应用程序/业务逻辑,或者你可以修改现有的应用程序。简单来说,在 Odoo 中,一切从模块开始,到模块结束。
Odoo 提供各种业务解决方案,如销售、采购、POS、会计、制造、项目和库存。创建新模块涉及向业务添加新功能或升级现有功能。
Odoo 的最新版本在社区和商业版中都引入了众多新模块。这些包括会议室、待办事项以及几个与 WhatsApp 相关的集成模块。
此外,这个版本还包含了许多令人兴奋的新功能,如重新设计的用户界面、改进的搜索功能以及 CRM、制造和电子商务的新功能。新版本还包括其他一些改进,如增强的性能、改进的安全性和更多集成。
Odoo 被各种规模的公司使用;每个公司都有不同的业务流程和需求。为了处理这个问题,Odoo 将应用程序的功能划分为不同的模块。这些模块可以根据需要加载到数据库中。基本上,管理员可以在任何时候启用/禁用这些功能。因此,相同的软件可以根据不同的需求进行调整。查看以下 Odoo 模块的截图;列中的第一个模块是主应用程序,其他模块旨在为该应用程序添加额外功能。要按应用程序的类别分组获取模块列表,请转到应用菜单并按类别分组:

图 3.1 – 按类别分组应用程序
如果您计划在 Odoo 中开发新的应用程序,您应该为各种功能创建边界。这将非常有帮助,可以将您的应用程序划分为不同的附加模块。现在您已经知道了 Odoo 中附加模块的目的,我们可以开始构建自己的模块。
创建和安装新的附加模块
在这个食谱中,我们将创建一个新的模块,使其在我们的 Odoo 实例中可用,并安装它。
准备工作
首先,我们需要一个准备就绪的 Odoo 实例。
如果您遵循了第一章中“从源代码轻松安装 Odoo”食谱的安装 Odoo 开发环境,Odoo 应该位于~/odoo-dev/odoo。为了解释的目的,我们将假设这个位置是 Odoo,尽管您可以使用任何其他您偏好的位置。
我们还需要一个位置来添加我们自己的 Odoo 模块。为了本食谱的目的,我们将在odoo目录旁边使用local-addons目录,位于~/odoo-dev/local-addons。
您可以在 GitHub 上上传自己的 Odoo 模块,并在本地系统上克隆它们以进行开发。
如何操作...
作为示例,我们将为这一章创建一个小型附加模块来管理宿舍。
以下步骤将创建并安装一个新的附加模块:
-
更改我们将工作的工作目录,并创建放置自定义模块的附加目录:
$ cd ~/odoo-dev my_hostel:init.py 文件:
my_hostel folder, create an __manifest__.py file with this line:{'name': 'Hostel Management'}
-
在附加路径中启动您的 Odoo 实例,包括模块目录:
--save option is added to the Odoo command, the add-ons path will be saved in the configuration file. The next time you start the server, if no add-on path option is provided, this will be used. -
在您的 Odoo 实例中启用新模块。使用admin登录 Odoo,在关于框中启用开发者模式,然后在应用顶部菜单中选择更新应用列表。现在,Odoo 应该知道我们的 Odoo 模块:

图 3.2 – 更新应用列表的对话框
- 选择
my_hostel。点击激活按钮,安装将完成。
它是如何工作的...
Odoo 模块是一个包含代码文件和其他资产的目录。所使用的目录名是该模块的技术名称。模块清单中的name键是其标题。
__manifest__.py文件是模块清单。它包含一个包含模块元数据的 Python 字典,包括类别、版本、它所依赖的模块以及它将加载的数据文件列表。它包含有关附加模块的重要元数据并声明应加载的数据文件。
在这个食谱中,我们使用了最小的清单文件,但在实际模块中,我们需要其他重要的键。这些将在下一个食谱完成附加模块清单中讨论。
模块目录必须是 Python 可导入的,因此它还需要有一个__init__.py文件,即使它是空的。要加载一个模块,Odoo 服务器将导入它。这将导致__init__.py文件中的代码被执行,因此它作为运行模块 Python 代码的入口点。因此,它通常包含导入语句来加载模块 Python 文件和子模块。
已知的模块可以直接从命令行使用--init或my_hostel应用程序安装,你可以使用my_hostel。此列表是在你从当时提供的附加路径上找到的模块创建新数据库时设置的。它可以通过更新模块 列表菜单在现有数据库中更新。
完成附加模块清单
清单是 Odoo 模块的一个重要组成部分。
准备工作
我们应该有一个模块来工作,它已经包含一个__manifest__.py清单文件。你可能想遵循前面的食谱来提供这样一个模块来工作。
如何做...
我们将在我们的附加模块中添加一个清单文件和一个图标:
-
要创建包含最相关键的清单文件,编辑模块的
__manifest__.py文件,使其看起来像这样:{ 'name': "Hostel Management", 'summary': "Manage Hostel easily", 'description': "Efficiently manage the entire residential facility in the school.", # Supports reStructuredText(RST) format (description is Deprecated), 'author': "Your name", 'website': "http://www.example.com", 'category': 'Uncategorized', 'version': '17.0.1.0.0', 'depends': ['base'], 'data': ['views/hostel.xml'], 'assets': { 'web.assets_backend': [ 'web/static/src/xml/**/*', ], }, 'demo': ['demo.xml'], } -
要为模块添加图标,选择一个 PNG 图像并复制到
static/description/icon.png。
它是如何工作的...
清单文件中的内容是一个常规 Python 字典,具有键和值。我们使用的示例清单包含最相关的键:
-
name:这是模块的标题。 -
summary:这是带有单行描述的副标题。 -
description:这是一个以纯文本或ReStructuredText (RST)格式编写的长描述。它通常被三重引号包围,并在 Python 中用于界定多行文本。有关 RST 快速入门参考,请访问docutils.sourceforge.net/docs/user/rst/quickstart.html。 -
author:这是一个包含作者名称的字符串。当有多个作者时,通常使用逗号分隔他们的名字,但请注意,它仍然是一个字符串,而不是 Python 列表。 -
website:这是人们应该访问的 URL,以了解更多关于模块或作者的信息。 -
category:这是根据兴趣领域组织模块。可以在github.com/odoo/odoo/blob/17.0/odoo/addons/base/data/ir_module_category_data.xml中看到可用的标准类别名称列表。然而,也可以在这里定义其他新的类别名称。 -
version:这是模块的版本号。Odoo 应用商店可以使用它来检测已安装模块的新版本。如果版本号不以 Odoo 目标版本开头(例如,17.0),它将被自动添加。不过,如果您明确地声明 Odoo 目标版本会更具有信息量——例如,使用17.0.1.0.0或17.0.1.0,而不是分别使用1.0.0或1.0。 -
depends:这是一个包含它直接依赖的模块的技术名称列表。如果您的模块不依赖于任何其他附加模块,那么您至少应该添加一个base模块。不要忘记包括任何定义 XML IDs、视图或模型并在此模块中引用的模块。这将确保它们按正确顺序加载,避免难以调试的错误。 -
data:这是在模块安装或升级期间要加载的数据文件的相对路径列表。路径相对于模块root目录。通常,这些是 XML 和 CSV 文件,但也可能有 YAML 数据文件。这些在 第六章,管理模块数据 中进行了深入讨论。 -
demo:这是包含演示数据的文件的相对路径列表,用于加载。只有在数据库创建时启用了Demo Data标志的情况下,才会加载这些文件。
用作模块图标的图像是位于 static/description/icon.png 的 PNG 文件。
Odoo 预计在主要版本之间会有显著的变化,因此为某个主要版本构建的模块很可能在没有转换和迁移工作的情况下与下一个版本不兼容。因此,在安装模块之前,确保了解模块的 Odoo 目标版本非常重要。
为了确保兼容性,我们需要遵循以下步骤:
-
首先,检查安装是否成功。如果成功了,然后继续检查模块的功能是否正常工作。
-
然而,如果安装不成功,您将需要根据您收到的错误调整代码和功能逻辑。
还有更多...
在模块清单中,除了有长描述外,还可以有单独的描述文件。自 8.0 版本以来,它可以由一个 README 文件替换,该文件具有 .txt、.rst 或 .md(Markdown)扩展名。否则,在模块中包含一个 description/index.html 文件。
此 HTML 描述将覆盖在清单文件中定义的描述。
还有几个常用的键:
-
licence:默认值是LGPL-3。此标识符用于在模块中提供的许可证下。其他许可证可能性包括AGPL-3、Odoo Proprietary License v1.0(主要用于付费应用)和Other OSI Approved Licence。 -
application:如果这是True,则模块被列为应用程序。通常,这用于功能区域的中心模块。 -
auto_install:如果这是True,则表示这是一个粘合模块,当所有依赖项都安装时,它会自动安装。 -
installable:如果这是True(默认值),则表示该模块可供安装。 -
external_dependencies:一些 Odoo 模块内部使用Python/bin库。如果您的模块使用此类库,您需要将它们放在这里。这将阻止用户在主机机器上未安装列出的模块时安装模块。 -
{pre_init, post_init, uninstall}_hook:这是一个在安装/卸载期间调用的 Python 函数钩子。有关更详细的示例,请参阅第八章,高级服务器端开发技术。 -
Assets:定义了所有静态文件如何在各种资产包中加载。Odoo 资产按包分组。每个包(特定类型的文件路径列表 -xml、js、css或scss)在模块清单中列出。
有许多特殊键用于应用商店列表:
-
price:此键用于设置您的附加模块的价格。此键的值应该是一个整数值。如果没有设置价格,这意味着您的应用是免费的。 -
currency:这是价格所用的货币。可能的值是USD和EUR。此键的默认值是EUR。 -
live_test_url:如果您想为您的应用提供一个实时测试 URL,您可以使用此键在应用商店上显示“实时预览”按钮。 -
iap:如果模块用于提供 IAP 服务,请设置您的 IAP 开发者密钥。 -
images:这给出了图像的路径。这张图片将被用作 Odoo 应用商店的封面图片。
组织附加模块文件结构
附加模块包含代码文件和其他资产,如 XML 文件和图像。对于这些文件中的大多数,我们可以在模块目录内部自由选择放置位置。
然而,Odoo 在模块结构上使用了一些约定,因此建议遵循它们。适当的代码提高了可读性,简化了维护,有助于调试,降低了复杂性,并促进了可靠性。这些适用于每个新模块和所有新开发。
准备工作
我们期望有一个附加模块目录,其中只包含__init__.py和__manifest__.py文件。在这个菜谱中,我们假设这是local-addons/my_hostel。
如何操作...
要为附加模块创建基本骨架,请执行以下步骤:
-
为代码文件创建目录:
$ cd local-addons/my_hostel $ mkdir models $ touch models/__init__.py $ mkdir controllers $ touch controllers/__init__.py $ mkdir views $ touch views/views.xml $ mkdir security $ mkdir wizards $ touch wizards/__init__.py $ mkdir reports $ mkdir data $ mkdir demo __init__.py file so that the code in the subdirectories is loaded:从
. import models导入从
controllers模块导入从
. import wizards导入
这应该会让我们开始一个包含最常用目录的结构,类似于这个:
my_hostel
├── __init__.py
├── __manifest__.py
├── controllers
│ └── __init__.py
├── data
├── demo
├── i18n
├── models
│ └── __init__.py
├── security
├── static
│ ├── description
│ └── src
│ ├─ js
│ ├─ scss
│ ├─ css
│ └ xml
├── reports
├── wizards
│ └── __init__.py
└──views
└── __init__.py
它是如何工作的...
为了提供一些背景信息,一个 Odoo 扩展模块可以有三类文件:
-
Python 代码由
__init__.py文件加载,其中.py文件和代码子目录被导入。包含 Python 代码的子目录,反过来,需要它们自己的__init__.py文件。 -
需要在
__manifest__.py模块清单的data和demo键中声明的数据文件,以便加载,通常是用于用户界面的 XML 和 CSV 文件、固定数据以及演示数据。也可能有 YAML 文件,这些文件可以包含一些在模块加载时运行的程序性指令——例如,用于在 XML 文件中以编程方式生成或更新记录,而不是静态地。 -
Web 资源,例如 JavaScript 代码和库、CSS、SASS 以及 QWeb/HTML 模板,是用于构建 UI 部分和管理这些 UI 元素中用户动作的文件。这些文件通过在
assets键上的清单声明,包括新文件和现有文件,将这些资源添加到 Web 客户端、小部件或网站页面上。
扩展文件组织在以下目录中:
-
models/目录包含后端代码文件,从而创建模型及其业务逻辑。建议每个模型一个文件,文件名与模型同名——例如,hostel.py用于hostel.hostel模型。这些在第四章,应用模型中有详细说明。 -
views/目录包含用户界面的 XML 文件,包括动作、表单、列表等。与模型一样,建议每个模型一个文件。网站模板的文件名预期以_template后缀结尾。后端视图在第九章,后端视图中有所解释,网站视图在第十四章,CMS 网站开发中有所说明。 -
data/目录包含包含模块初始数据的其他数据文件。数据文件在第六章,管理模块数据中有所解释。 -
demo/目录包含带有演示数据的文件,这对于测试、培训或模块评估很有用。 -
i18n/是 Odoo 查找翻译.pot和.po文件的位置。有关更多详细信息,请参阅第十一章,国际化。这些文件不需要在清单文件中提及。 -
security/目录包含定义访问控制列表的数据文件,通常是一个ir.model.access.csv文件,以及可能的一个 XML 文件,用于定义行级安全的访问组和记录规则。更多关于这方面的信息,请参阅第十章,安全访问。 -
controllers/包含网站控制器和提供此类功能的模块的代码文件。网络控制器在 第十三章 网络服务器开发 中有介绍。 -
static/是所有网络资产预期放置的位置。与其他目录不同,此目录名称不仅仅是一个约定。此目录中的文件是公开的,并且可以在不登录用户的情况下访问。此目录主要包含 JavaScript、样式表和图像等文件。它们不需要在模块清单中提及,但必须在网络模板中引用。这在 第十四章 CMS 网站开发 中有详细讨论。 -
wizards/包含所有与向导相关的文件。在 Odoo 中,向导用于存储中间数据。我们可以在 第八章 高级服务器端开发技术 中了解更多关于向导的信息。 -
reports/:Odoo 提供了一个生成 PDF 文档的功能,例如销售订单和发票。此目录包含所有与 PDF 报告相关的文件。我们将在 第十二章 自动化、工作流程、电子邮件和打印 中了解更多关于 PDF 报告的信息。
当向模块添加新文件时,不要忘记在 __manifest__.py 文件(对于数据文件)或 __init__.py 文件(对于代码文件)中声明它们;否则,这些文件将被忽略且不会被加载。
添加模型
模型定义了我们的业务应用程序将使用的数据结构。这个食谱展示了如何向模块添加一个基本模型。模型决定了数据库的逻辑结构和数据如何存储、组织和管理。换句话说,模型是一个可以与其他表链接的信息表。模型通常代表一个业务概念,例如销售订单、联系人或产品。
模块包含各种元素,例如模型、视图、数据文件、网络控制器和静态网络数据。
要创建一个宿舍模块,我们需要开发一个代表宿舍的模型。
准备工作
我们应该有一个模块来工作。如果你遵循了本章的第一个食谱,创建和安装新的附加模块,你将有一个名为 my_hostel 的空模块。我们将用它来解释。
如何做到这一点...
要添加一个新的 模型,我们需要添加一个描述它的 Python 文件,然后升级附加模块(或者如果尚未完成,则安装它)。所使用的路径相对于我们的附加模块位置(例如,~/odoo-dev/local-addons/my_hostel/):
-
在
models/hostel.py模块中添加一个 Python 文件,以下代码:from odoo import fields, models class Hostel(models.Model): _name = 'hostel.hostel' _description = "Information about hostel" name = fields.Char(string="Hostel Name", required=True) hostel_code = fields.Char(string="Code", required=True) street = fields.Char('Street') street2 = fields.Char('Street2') state_id = fields.Many2one("res.country.state", string="State") -
添加一个 Python 初始化文件,包含要由
models/__init__.py模块加载的代码文件,以下代码:from . import hostel -
编辑模块的 Python 初始化文件,以便模块加载
models/目录:from . import models -
通过命令行或用户界面中的应用菜单升级 Odoo 模块。在升级模块时,如果你仔细查看服务器日志,你应该会看到以下行:
odoo.modules.registry: module my_hostel: creating or updating database table
之后,新的hostel.hostel模型应该在我们的 Odoo 实例中可用。有两种方法可以检查我们的模型是否已添加到数据库中。
首先,你可以在 Odoo 用户界面中检查它。激活开发者工具并在这里打开hostel.hostel模型菜单。
第二种方法是检查你的 PostgreSQL 数据库中的表条目。你可以在数据库中搜索hostel_hostel表。在以下代码示例中,我们使用了test-17.0作为我们的数据库。然而,你可以用以下命令替换你的数据库名称:
$ psql test-17.0
test-17.0# \d hostel_hostel;
它是如何工作的...
我们的第一步是创建一个 Python 文件,在其中创建我们的新模块。
Odoo 框架有自己的ORM框架,它提供了对 PostgreSQL 数据库的抽象。通过继承 Odoo Python Model类,我们可以创建自己的模型(表)。当定义一个新的模型时,它也会被添加到一个中央模型注册表中。这使得其他模块稍后对其进行修改变得更容易。
模型有一些以下划线为前缀的通用属性。其中最重要的是_name,它提供了一个在整个 Odoo 实例中使用的唯一内部标识符。ORM 框架将根据此属性生成数据库表。在我们的配方中,我们使用了_name = 'hostel.hostel'。基于此属性,ORM 框架将创建一个名为hostel_hostel的新表。请注意,ORM 框架将通过替换_来创建表名。_description提供了模型的非正式名称,我们使用了_name = 'hostel.hostel'和_description='Information about hostel',并且_description='Information about hostel'只能以字母字符开头,我们不能以数字或特殊符号字符开头。
model字段被定义为类属性。我们首先定义了name字段,它是Char类型。对于模型来说,有这个字段很方便,因为默认情况下,当其他模型引用它时,它被用作记录描述。
我们还使用了一个关系字段的例子——state_id。这定义了Hostel和State之间的多对一关系。
关于模型还有很多要说的,它们将在第四章,应用模型中深入探讨。
接下来,我们必须让我们的模块知道这个新的 Python 文件。这是通过__init__.py文件来完成的。由于我们将代码放在了models/子目录中,我们需要上一个__init__文件来导入该目录,该目录应该反过来包含另一个__init__文件,导入那里的每个代码文件(在我们的情况下只有一个)。
通过升级模块激活 Odoo 模型的变化。Odoo 服务器将处理将model类转换为数据库结构变化。
虽然这里没有提供示例,但也可以将这些 Python 文件添加业务逻辑,无论是通过向模型的类中添加新方法,还是通过扩展现有方法,如create()或write()。这将在第五章,基本 服务器端开发中讨论。
添加访问安全
当添加新的数据模型时,您需要定义谁可以创建、读取、更新和删除记录。当创建全新的应用程序时,这可能涉及定义新的用户组。因此,如果用户没有这些访问权限,那么 Odoo 将不会显示您的菜单和视图。在前一个配方中,我们通过将admin用户转换为超级用户来访问我们的菜单。完成此配方后,您将能够直接作为admin用户访问Hostel模块的菜单和视图。
此配方基于前一个配方中的Hostel模型,并定义了一个新的用户安全组,以控制谁可以访问或修改Hostel的记录。
准备工作
实现了hostel.hostel模型的附加模块,在先前的配方中提供,在本配方中,我们将为它添加安全规则。所使用的路径相对于我们的附加模块位置(例如,~/odoo-dev/local-addons/my_hostel/)。
如何操作...
我们想要添加到这个配方中的安全规则如下:
-
每个人都将能够阅读宿舍记录。
-
一个名为宿舍管理员的新用户组将有权创建、读取、更新和删除宿舍记录。
要实现这一点,您需要执行以下步骤:
-
创建一个名为
security/hostel_security.xml的文件,内容如下:<?xml version="1.0" encoding="utf-8"?> <odoo> <record id="module_category_hostel" model="ir.module.category"> <field name="name">Hostel Management</field> <field name="sequence">31</field> </record> <record id="group_hostel_manager" model="res.groups"> <field name="name">Hostel Manager</field> <field name="category_id" ref="module_category_hostel"/> <field name="users" eval="[(4, ref('base.user_root')),(4, ref('base.user_admin'))]"/> </record> <record id="group_hostel_user" model="res.groups"> <field name="name">Hostel User</field> <field name="category_id" ref="module_category_hostel"/> </record> </odoo> -
添加一个名为
security/ir.model.access.csv的文件,内容如下:id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_hostel_manager_id,access.hostel.manager,my_hostel.model_hostel_hostel,my_hostel.group_hostel_manager,1,1,1,1 access_hostel_user_id,access.hostel.user,my_hostel.model_hostel_hostel,my_hostel.group_hostel_user,1,0,0,0 -
将这两个文件添加到
__manifest__.py的data条目中:# ... "data": [ "security/hostel_security.xml", "security/ir.model.access.csv", "views/hostel.xml", ], # ...
一旦您更新实例中的附加组件,新定义的安全规则将生效。
它是如何工作的...
我们提供了两个新的数据文件,我们将它们添加到附加模块的清单中,以便安装或更新模块时在数据库中加载它们:
-
security/hostel_security.xml文件通过创建一个res.groups记录来定义一个新的安全组。我们还通过使用其引用 ID,base.user_admin,赋予宿舍管理员对admin用户的权限,这样管理员用户将有权访问hostel.hostel模型。 -
ir.model.access.csv文件将模型上的权限与组关联。第一行有一个空的group_id:id列,这意味着该规则适用于所有人。最后一行将所有权限授予我们刚刚创建的组的成员。
清单文件数据部分的文件顺序很重要。创建安全组的文件必须在列出访问权限的文件之前加载,因为访问权限定义依赖于组的存在。由于视图可以是特定于安全组的,我们建议将组的定义文件放在列表中以确保安全。
相关内容
这本书有一个章节专门介绍安全。有关安全的信息,请参阅第十章,安全访问。
添加菜单项和视图
一旦我们有了满足数据结构需求的数据模型,我们希望有一个用户界面,以便我们的用户可以与之交互。菜单和视图在结构化和增强用户体验方面发挥着至关重要的作用。从技术角度来看,菜单是动态的用户界面组件,它呈现一组结构化的选项或链接,通常允许用户访问应用程序中的各种功能、功能或内容区域。这个菜谱基于前一个菜谱中的 Hostel 模型,并添加了一个菜单项来显示用户界面,包括列表和表单视图。
准备工作
实现之前菜谱中提供的 hostel.hostel 模型的附加模块是必需的。我们将使用的路径相对于我们的附加模块位置(例如,~/odoo-dev/local-addons/my_hostel/)。
如何操作...
要添加一个视图,我们将添加一个包含其定义的 XML 文件到模块中。由于这是一个新模型,我们还必须添加一个菜单选项,以便用户能够访问它。
对于模型,XML 文件添加视图文件夹以创建视图、操作和菜单项。
注意以下步骤的顺序很重要,因为其中一些步骤使用了对前一步骤中定义的 ID 的引用:
-
创建 XML 文件以添加描述用户界面的数据记录,
views/hostel.xml:<?xml version="1.0" encoding="utf-8"?> <odoo> <!-- Data records go here --> </odoo> -
将新的数据文件添加到附加模块的清单文件
__manifest__.py中,通过将其添加到views/hostel.xml:{ "name": "Hostel Management", "summary": "Manage Hostel easily", "depends": ["base"], "data": ["views/hostel.xml"], } -
在
hostel.xml文件中添加打开视图的操作:<record id="action_hostel" model="ir.actions.act_window"> <field name="name">Hostel</field> <field name="type">ir.actions.act_window</field> <field name="res_model">hostel.hostel</field> <field name="view_mode">tree,form</field> <field name="help" type="html"> <p class="oe_view_nocontent_create"> Create Hostel. </p> </field> </record> -
将菜单项添加到
hostel.xml文件中,使其对用户可见:<menuitem id="hostel_main_menu" name="Hostel" sequence="1"/> <menuitem id="hostel_type_menu" name="Hostel" parent="hostel_main_menu" action="my_hostel.action_hostel" groups="my_hostel.group_hostel_manager" sequence="1"/> -
将自定义表单视图添加到
hostel.xml文件中:<record id="view_hostel_form_view" model="ir.ui.view"> <field name="name">hostel.hostel.form.view</field> <field name="model">hostel.hostel</field> <field name="arch" type="xml"> <form string="Hostel"> <sheet> <div class="oe_title"> <h3> <table> <tr> <td style="padding- right:10px;"> <field name="name" required="1" placeholder="Name" /> </td> <td style="padding- right:10px;"> <field name="hostel_code" placeholder="Code" /> </td> </tr> </table> </h3> </div> <group> <group> <label for="street" string="Address"/> <div class="o_address_format"> <field name="street" placeholder="Street..." class="o_address_street"/> <field name="street2" placeholder="Street 2..." class="o_address_street"/> </div> </group> </group> </sheet> </form> </field> </record> -
将自定义树(列表)视图添加到
hostel.xml文件中:<record id="view_hostel_tree_view" model="ir.ui.view"> <field name="name">hostel.hostel.tree.view</field> <field name="model">hostel.hostel</field> <field name="arch" type="xml"> <tree> <field name="name"/> <field name="hostel_code"/> </tree> </field> </record> -
添加自定义的
hostel.xml文件:<record id="view_hostel_search_view" model="ir.ui.view"> <field name="name">Hostel Search</field> <field name="model">hostel.hostel</field> <field name="arch" type="xml"> <search> <field name="name"/> <field name="hostel_code"/> </search> </field> </record>
当在 Odoo 中添加新模型时,用户默认没有任何访问权限。我们必须为新模型定义访问权限才能获得访问权限。在我们的例子中,我们没有定义任何访问权限,因此用户无法访问我们的新模型。没有访问权限,我们的菜单和视图也不会可见。幸运的是,有一个快捷方式!通过切换到超级用户模式,您可以在没有访问权限的情况下查看我们的应用程序的菜单。
以超级用户身份访问 Odoo
通过将admin用户转换为superuser类型,您可以绕过访问权限,因此可以在不提供默认访问权限的情况下访问菜单和视图。要将admin用户转换为超级用户,请激活开发者模式。完成此操作后,从开发者工具选项中,点击成为超级用户选项。
作为开发者偏好,尝试在不成为超级用户的情况下做所有事情;这将非常有助于深入学习 Odoo。通过成为超级用户,所有安全访问和记录规则检查都将被绕过。
以下截图已提供作为参考:

图 3.3 – 激活超级用户模式的选项
成为超级用户后,您的菜单将具有条纹背景,如下面的截图所示:

图 3.4 – 激活超级用户模式
如果您现在尝试升级模块,您应该能够看到一个新的菜单选项(您可能需要刷新您的网络浏览器)。点击宿舍菜单将打开宿舍模型的列表视图,如下面的截图所示:

图 3.5 – 访问宿舍的菜单
它是如何工作的...
在较低级别,用户界面由存储在特殊模型中的记录定义。前两个步骤创建了一个空的 XML 文件来定义要加载的记录,然后我们将它们添加到模块的数据文件列表中,以便安装。
数据文件可以放置在模块目录的任何位置,但惯例是在views/子目录内定义用户界面。通常,这些文件的名称基于模型的名称。在我们的例子中,我们为hostel.hostel模型创建了用户界面,因此我们创建了views/hostel.xml文件。
下一步是定义一个窗口操作,以在 Web 客户端的主区域显示用户界面。该操作由res_model定义的目标模型,并且name属性用于在用户打开操作时向用户显示标题。这些只是基本属性。窗口操作支持其他属性,提供了更多控制视图渲染方式的能力,例如显示哪些视图,对可用的记录添加过滤器,或设置默认值。这些内容在第九章,后端视图中详细讨论。
通常,数据记录使用<record>标签定义,我们在我们的例子中为ir.actions.act_window模型创建了一个记录。这将创建窗口操作。
同样,菜单项存储在ir.ui.menu模型中,我们可以使用<record>标签创建这些。然而,Odoo 中有一个名为<menuitem>的快捷标签,因此我们在我们的例子中使用了这个标签。
这些是菜单项的主要属性:
-
name:这是要显示的菜单项文本。 -
action:这是要执行的操作的标识符。我们使用上一步中创建的窗口操作的 ID。 -
sequence:用于设置同一级别菜单项的显示顺序。 -
parent:这是父菜单项的标识符。在我们的示例菜单项中没有父项,这意味着它将显示在菜单的顶部。 -
web_icon:此属性用于显示菜单的图标。此图标仅在 Odoo 企业版中显示。
在这个阶段,我们还没有在我们的模块中定义任何视图。然而,如果你在这个阶段升级你的模块,Odoo 将会自动动态创建它们。尽管如此,我们肯定希望控制我们的视图外观,所以接下来两个步骤将创建一个表单视图和一个树形视图。
这两个视图都是通过 ir.ui.view 模型上的记录定义的。我们使用的属性如下:
-
name:这是一个标识视图的标题。在 Odoo 的源代码中,你将在这里找到重复的 XML ID,但如果你愿意,你可以添加一个更易读的标题作为名称。 -
如果省略了
name字段,Odoo 将使用模型名称和视图类型生成一个。这对于新模型的常规视图来说完全没问题。当你扩展视图时,建议有一个更明确的名称,因为这将在你在 Odoo 用户界面中查找特定视图时使你的生活更容易。 -
model:这是目标模型的内部标识符,如在其_name属性中定义的那样。 -
arch:这是视图架构,其中其结构实际上被定义。这是不同类型的视图彼此区分的地方。
表单视图通过顶部的 <form> 元素定义,其画布是一个两列网格。在表单内部,使用 <group> 元素垂直组合字段。两个组产生两个带有字段的列,这些字段是通过 <field> 元素添加的。字段使用默认的小部件根据其数据类型,但可以使用 widget 属性使用特定的小部件。
树形视图更简单;它们通过包含要显示的列的 <field> 元素的顶部 <tree> 元素定义。
最后,我们添加了一个 <search> 最高级标签,我们可以有 <field> 和 <filter> 元素。字段元素是可以从搜索视图中输入的额外字段,而过滤器元素是预定义的过滤器条件,可以通过点击激活。这些主题在第九章,“后端视图”中详细讨论。
使用 scaffold 命令创建模块
当创建一个新的 Odoo 模块时,有一些样板代码需要设置。为了帮助快速启动新模块,Odoo 提供了 scaffold 命令。
这个示例展示了如何使用 scaffold 命令创建一个新的模块,这将为目录放置文件框架。
准备工作
我们将在自定义模块目录中创建新的附加模块,因此我们需要安装 Odoo 并创建一个自定义模块的目录。我们将假设 Odoo 安装于~/odoo-dev/odoo,并且我们的自定义模块将被放置在~/odoo-dev/local-addons目录中。
如何操作...
我们将使用scaffold命令来创建样板代码。按照以下步骤使用scaffold命令创建新模块:
-
将工作目录更改为我们希望模块所在的位置。这可以是您选择的任何目录,但它需要位于附加路径内才有用。根据我们在上一个菜谱中使用的目录选择,这应该是以下内容:
scaffold command to create it. For our example, we will choose my_module:__manifest__.py默认模块清单提供并更改相关值。您肯定至少想要更改名称键中的模块标题。
这就是生成的附加模块应该看起来像什么:
$ tree my_module
my_module/
├── __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
5 directories, 10 files
您现在应该编辑各种生成的文件,并将它们适应您新模块的目的。
它是如何工作的...
scaffold命令根据模板创建新模块的骨架。
默认情况下,新模块将在当前工作目录中创建,但我们可以提供一个特定的目录来创建模块,通过传递一个额外的参数来实现。
考虑以下示例:
$ ~/odoo-dev/odoo/odoo-bin scaffold my_module ~/odoo-dev/local-addons
使用了一个default模板,但还有一个用于网站主题编写的theme模板可用。要选择特定的模板,可以使用-t选项。我们还可以使用一个包含模板的目录的路径。
这意味着我们可以使用自己的模板与scaffold命令一起使用。内置模板可以在/odoo/cli/templates Odoo 子目录中找到。要使用自己的模板,我们可以使用以下类似命令:
$ ~/odoo-dev/odoo/odoo-bin scaffold -t path/to/template my_module
默认情况下,Odoo 在/odoo/cli/templates目录中有两个模板。一个是default模板,另一个是theme模板。然而,您可以创建自己的模板或使用-t选项,如前述命令所示。
第四章:应用模型
本章将指导您对现有扩展模块进行一些小的改进。您已经在 第三章,创建 Odoo 扩展模块 中注册了您的扩展模块。现在,您将更深入地探索模块的数据库方面。您将学习如何创建新的模型(数据库表),添加新的字段,并应用约束。您还将发现如何使用 Odoo 中的继承来修改现有模型。在本章中,您将使用上一章中创建的相同模块。
本章涵盖了以下主题:
-
定义模型表示和顺序
-
向模型添加数据字段
-
添加可配置精度的浮点字段
-
向模型添加货币字段
-
向模型添加关系字段
-
向模型添加层次结构
-
向模型添加约束验证
-
向模型添加计算字段
-
暴露存储在其他模型中的相关字段
-
使用引用字段添加动态关系
-
使用继承向模型添加功能
-
使用抽象模型实现可重用模型功能
-
使用继承复制模型定义
技术要求
在继续本章的示例之前,请确保您已安装并配置了我们在 第三章,创建 Odoo 扩展模块 中开发的模块。
定义模型表示和顺序
模型指的是数据库表的表示。模型定义了数据库表的结构和行为,包括字段、关系和多种方法。模型使用 Odoo 的 对象关系映射(ORM)系统在 Python 代码中定义。ORM 允许开发人员使用 Python 类和方法与数据库交互,而不是编写原始 SQL 查询。
模型属性是在创建新模型时将要定义的模型特征;否则,我们使用已存在的模型的属性。模型使用带下划线前缀的结构属性来定义其行为。
准备工作
my_hostel 实例应该已经包含一个名为 models/hostel.py 的 Python 文件,该文件定义了一个基本模型。我们将编辑它以添加新的类级别属性。
如何操作...
通过有效地利用这些属性,开发人员可以在 Odoo 中创建组织良好、可重用和可维护的代码,从而实现更高效和健壮的应用程序。以下是可以用于模型上的属性:
-
_name:name属性是最重要的一个,因为它决定了内部全局标识符和数据库表名。模型名在模块命名空间内以点表示法表达。例如,name="hostel.hostel"将在数据库中创建hostel_hostel表:_name = 'hostel.hostel' -
_table:如果启用了‘_auto’,我们可以定义模型使用的 SQL 表名:_name = 'project.task.stage.personal' _table = 'project_task_user_rel' -
_description:为了给模型分配一个反映其目的和功能的描述性标题,请插入以下代码片段:_description = 'Information about hostel'
注意
如果您没有为您的模型使用_description,Odoo 将在日志中显示警告。
-
_order:默认的搜索结果排序字段是'id'。但是,可以通过提供一个包含逗号分隔的字段名称列表的字符串的_order属性来更改它,以便我们可以使用我们选择的字段。字段名称后可以跟desc关键字以降序排序。要按id降序排序,然后按名称升序排序记录,请使用以下代码语法:_order = "id desc, name"
注意
只能使用存储在数据库中的字段。无法使用非存储的计算字段对记录进行排序。_order字符串的语法类似于SQL ORDER BY子句,尽管它被简化了。例如,不允许使用特殊子句,如NULLS FIRST。
-
_rec_name:此属性用于设置用作记录表示或标题的字段。rec_name的默认字段是名称字段。_rec_name是 Odoo 的rec_name使用的记录显示名称,并将hostel_code设置为模型的代表,使用以下代码语法:_rec_name = 'hostel_code' hostel_code = fields.Char(string="Code", required=True)
注意
如果您的模型没有名称字段,并且您也没有指定_rec_name,则您的显示名称将是模型名称和记录 ID 的组合,如下所示 - (``hostel.hostel, 1)。
-
_rec_names_search:此属性用于通过提到的字段值搜索特定记录。它与使用name_search函数类似。您可以直接使用此属性而不是使用name_search方法。为此,请使用以下代码语法:_rec_names_search = ['name', 'code']
更多内容...
所有模型都有一个display_name字段,它以人类可读的格式显示记录表示,自 8.0 版本以来已自动添加到所有模型中。默认的_compute_display_name()方法使用_rec_name属性来确定哪个字段包含显示名称的数据。要自定义显示名称,您可以重写_compute_display_name()方法并提供您的逻辑。该方法应返回一个包含记录 ID 和 Unicode 字符串表示的元组的列表。
例如,为了在表示中包含宿舍名称和宿舍代码,例如Youth Hostel (YHG015),我们可以定义以下内容:
看看以下示例。这将向记录的名称添加一个发布日期:
@api.depends('hostel_code')
def _compute_display_name(self):
for record in self:
name = record.name
if record.hostel_code:
name = f'{name} ({record.hostel_code})'
record.display_name = name
在添加前面的代码后,您的display_name记录将被更新。假设您有一个名为Bell House Hostel的记录,其代码为BHH101;那么,前面的_compute_display_name()方法将生成一个如Bell House Hostel (BHH101)的名称。
当我们完成时,我们的hostel.py文件应如下所示:
from odoo import fields, models
class Hostel(models.Model):
_name = 'hostel.hostel'
_description = "Information about hostel"
_order = "id desc, name"
_rec_name = 'hostel_code'
name = fields.Char(string="hostel Name", required=True)
hostel_code = fields.Char(string="Code", required=True)
street = fields.Char('Street')
street2 = fields.Char('Street2')
zip = fields.Char('Zip', change_default=True)
city = fields.Char('City')
state_id = fields.Many2one("res.country.state", string='State')
country_id = fields.Many2one('res.country', string='Country')
phone = fields.Char('Phone',required=True)
mobile = fields.Char('Mobile',required=True)
email = fields.Char('Email')
@api.depends('hostel_code')
def _compute_display_name(self):
for record in self:
name = record.name
if record.hostel_code:
name = f'{name} ({record.hostel_code})'
record.display_name = name
您的hostel.xml文件中的<form>视图将如下所示:
<form string="Hostel">
<sheet>
<div class="oe_title">
<h3>
<table>
<tr>
<td style="padding-right:10px;">
<field name="name" required="1"
placeholder="Name" /></td>
<td style="padding-right:10px;">
<field name="hostel_code" placeholder="Code"
/></td>
</tr>
</table>
</h3>
</div>
<group>
<group>
<label for="street" string="Address"/>
<div class="o_address_format">
<field name="street" placeholder="Street..."
class="o_address_street"/>
<field name="street2" placeholder="Street 2..."
class="o_address_street"/>
<field name="city" placeholder="City"
class="o_address_city"/>
<field name="state_id" class="o_address_state"
placeholder="State" options='{"no_open":
True}'/>
<field name="zip" placeholder="ZIP"
class="o_address_zip"/>
<field name="country_id" placeholder="Country"
class="o_address_country" options='{"no_open":
True, "no_create": True}'/>
</div>
</group>
<group>
<field name="phone" widget="phone"/>
<field name="mobile" widget="phone"/>
<field name="email" widget="email"
context="{'gravatar_image': True}"/>
</group>
</group>
</sheet>
</form>
我们应该升级模块以在 Odoo 中激活这些更改。
要更新模块,执行以下操作:
Activate developer mode ->Apps -> Update App List
然后,搜索my_hostel模块,并通过下拉菜单升级它,如图下所示:

图 4.1 – 更新模块的选项
或者,您也可以在命令行中使用-u my_hostel命令。
向模型添加数据字段
字段代表数据库表中的一列,并定义了可以存储在该列中的数据结构。Odoo 模型中的字段用于指定模型将存储的数据的属性和特征。每个字段都有一个数据类型(例如,Char、Integer、Float或Date)以及确定字段行为的各种属性。
在本节中,您将探索字段可以支持的各种数据类型以及如何将它们添加到模型中。
准备工作
本食谱假设您已经有一个带有my_hostel附加模块的实例,如第三章,创建 Odoo 附加模块中所述。
如何操作...
my_hostel附加模块应该已经包含models/hostel.py,定义了一个基本模型。我们将编辑它以添加新字段:
-
使用最小语法向
Hostel模型添加字段:from odoo import models, fields class Hostel(models.Model): # … email = fields.Char('Email') hostel_floors = fields.Integer(string="Total Floors") image = fields.Binary('Hostel Image') active = fields.Boolean("Active", default=True, help="Activate/Deactivate hostel record") type = fields.Selection([("male", "Boys"), ("female", "Girls"), ("common", "Common")], "Type", help="Type of Hostel", required=True, default="common") other_info = fields.Text("Other Information", help="Enter more information") description = fields.Html('Description') hostel_rating = fields.Float('Hostel Average Rating', digits=(14, 4))我们已经向模型添加了新字段。我们还需要将这些字段添加到表单视图中,以便在用户界面中反映这些更改。请参考以下代码以向表单视图添加字段:
<field name="image" widget="image" class="oe_avatar"/> <group> <group> <label for="street" string="Address"/> <div class="o_address_format"> <field name="street" placeholder="Street..." class="o_address_street"/> <field name="street2" placeholder="Street 2..." class="o_address_street"/> <field name="city" placeholder="City" class="o_address_city"/> <field name="state_id" class="o_address_state" placeholder="State" options='{"no_open": True}'/> <field name="zip" placeholder="ZIP" class="o_address_zip"/> <field name="country_id" placeholder="Country" class="o_address_country" options='{"no_open": True, "no_create": True}'/> </div> <field name="phone" widget="phone"/> <field name="mobile" widget="phone"/> <field name="email" widget="email" context="{'gravatar_image': True}"/> </group> <group> <field name="hostel_floors"/> <field name="active"/> <field name="type"/> <field name="hostel_rating"/> <field name="other_info"/> </group> </group> <group> <field name="description"/> </group>
升级模块将使这些更改在 Odoo 模型中生效。
它是如何工作的...
要向模型添加字段,您需要在它们的 Python 类中定义相应类型的属性。非关系字段的可用类型如下:
-
字符:存储字符串值。
-
文本:存储多行字符串值。
-
选择:存储从预定义值和描述的列表中选择的一个值。它包含值和描述对的列表。所选的值将存储在数据库中,可以是字符串或整数。描述是自动可翻译的。
注意
如果整数字段的值为零,Odoo 不会显示描述。选择字段也接受函数引用作为其选择属性,而不是列表。这允许您动态生成选项列表。您可以在本章的使用引用字段添加动态关系食谱中找到一个相关示例,其中也使用了选择属性。
-
HTML:以 HTML 格式存储富文本。
-
二进制:存储二进制文件,如图像或文档。
-
True/False值。 -
使用
fields.Date.today()将默认值设置为当前日期。 -
将
datetime值作为 UTC 时间的 Python datetime 对象。使用fields.Date.now()将默认值设置为当前时间。 -
整数:存储整数值。
-
浮点数:存储具有可选精度的数值(总位数和小数位数)。
-
货币:以特定货币存储金额。这将在本章的 将货币字段添加到模型 菜谱中进一步解释。
步骤 1 显示了添加到每种字段类型的最小语法。字段定义可以扩展以添加其他可选属性,如 步骤 2 所示。以下是已使用字段属性的说明:
-
string是字段的标题,并用于 UI 视图标签。它是可选的。如果没有设置,将根据字段名称推导标签,通过添加标题大小写并替换下划线为空格。 -
当设置为
True时,translate使得字段可翻译。它的值可能因用户界面语言而异。 -
default是默认值。它也可以是一个用于计算默认值的函数——例如,default=_compute_default,其中_compute_default是在字段定义之前在模型上定义的方法。 -
help是在 UI 工具提示中显示的解释文本。 -
groups使得字段仅对某些安全组可用。它是一个包含以逗号分隔的安全组 XML ID 列表的字符串。这将在第十章 安全访问中更详细地说明。 -
copy标志表示在记录复制时是否复制字段值。默认情况下,对于非关系和Many2one字段为True,对于One2many和计算字段为False。 -
当设置为
True时,index为字段创建数据库索引,这有时可以允许更快的搜索。它替换了已弃用的select=1属性。 -
readonly标志使得字段在用户界面中默认为只读。 -
required标志使得字段在用户界面中默认为必填项。 -
这里提到的各种白名单在
odoo/fields.py中定义。 -
company_dependent标志使得字段为每个公司存储不同的值。它替换了已弃用的Property字段类型。 -
值不存储在模型表上。它注册为
ir.property。当需要company_dependent字段的值时,将搜索ir.property并将其链接到当前公司(如果存在一个属性,则还包括当前记录)。如果记录上的值被更改,它将修改当前记录的现有属性(如果存在)或为当前公司和res_id创建一个新的属性。如果公司侧的值被更改,它将影响所有尚未更改值的记录。 -
group_operator是一个聚合函数,用于在按组模式显示结果。此属性的可能的值包括
count、count_distinct、array_agg、bool_and、bool_or、max、min、avg和sum。整数、浮点数和货币字段类型对此属性的默认值为sum。此字段由:meth:~odoo.models.Model.read_group方法用于根据此字段分组行。支持的聚合函数如下:
-
array_agg:将所有值(包括空值)连接到一个数组中 -
count:计算行数 -
count_distinct:计算不同行数 -
bool_and:如果所有值都是true,则返回true,否则返回false -
bool_or:如果至少有一个值是true,则返回true,否则返回false -
max:返回所有值的最大值 -
min:返回所有值的最小值 -
avg:返回所有值的平均值 -
sum:返回所有值的总和
-
-
Store:用于确定字段是否存储在数据库中(默认为True,对于计算字段为False)。 -
group_expand:此函数用于在按当前字段分组时扩展read_group结果:.. code-block:: python @api.model def _read_group_selection_field(self, values, domain, order): return ['choice1', 'choice2', ...] # available selection choices. @api.model def _read_group_many2one_field(self, records, domain, order): return records + self.search([custom_domain]) -
在 HTML 字段中使用
sanitize标志来系统地从其内容中移除可能不安全的标签。激活此标志会导致对输入进行彻底的清洁。对于寻求对 HTML 清洁有更细致控制的用户,还有其他一些属性可用。重要的是要注意,这些属性仅在启用sanitize标志时有效。
如果您需要在 HTML 清洁中实现更精细的控制,可以使用一些额外的属性,但这些属性仅在启用 sanitize 时有效:
-
sanitize_tags=True,用于移除不属于白名单的标签(这是默认设置) -
sanitize_attributes=True,用于移除不属于白名单的标签属性 -
sanitize_style=True,用于移除不属于白名单的样式属性 -
strip_style=True,用于移除所有样式元素 -
strip_class=True,用于移除类属性
最后,我们根据模型中新添加的字段更新了表单视图。我们将所有字段放置在表单视图中,但您可以将它们放置在任何您想要的位置。表单视图在第九章,后端视图中有更详细的解释。
还有更多...
Date 和 Datetime 字段对象公开了一些实用方法,这些方法对于日期和 datetime 非常方便:
对于 Date,我们有以下内容:
-
fields.Date.to_date(string_value)将字符串解析为日期对象。 -
fields.Date.to_string(date_value)将 Python 日期对象转换为字符串。 -
fields.Date.today()以字符串格式返回当前日期。这适用于使用默认值。 -
fields.Date.context_today(record, timestamp)以字符串格式返回时间戳的日期(如果省略时间戳,则为当前日期),根据记录(或记录集)的上下文时区。
对于 Datetime,我们有以下内容:
-
fields.Datetime.to_datetime(string_value)将字符串解析为 datetime 对象。 -
fields.Datetime.to_string(datetime_value)将 datetime 对象转换为字符串。 -
fields.Datetime.now()以字符串格式返回当前日期和时间。这适用于使用默认值。 -
fields.Datetime.context_timestamp(record, timestamp)将一个无时区信息的 datetime 对象转换为具有时区信息的 datetime 对象。使用记录上下文中的时区。这不适合作为默认值,但可以在向外部系统发送数据时使用。
除了基本字段外,还有一些关系字段,如 Many2one、One2many 和 Many2many。这些内容在本章的 向模型添加关系字段 菜谱中有详细说明。
你也可以通过使用 compute 字段属性来定义计算函数,从而创建具有自动计算值的字段。这在本章的 向模型添加计算字段 菜谱中有详细说明。
Odoo 模型中默认添加了一些字段,因此你应该避免使用这些名称作为你的字段名称。具体如下:
-
id(记录自动生成的标识符) -
create_date(记录创建的时间戳) -
create_uid(创建记录的用户) -
write_date(最后记录的时间戳编辑) -
write_uid(最后编辑记录的用户)
可以通过设置 _log_access=False 模型属性来禁用这些日志字段的自动创建。
可以添加到模型中的另一个特殊列是 active。它必须是一个 Boolean 字段,允许用户将记录标记为非活动状态。它用于在记录上启用 archive/unarchive 功能。其定义如下:
active = fields.Boolean('Active', default=True)
默认情况下,只有将 active 设置为 True 的记录是可见的。要检索它们,我们需要使用 [('active', '=', False)] 域过滤器。或者,如果将 'active_test': False 值添加到环境上下文中,ORM 不会过滤掉非活动记录。
在某些情况下,你可能无法修改上下文以获取活动和非活动记录。如果是这样,你可以使用 ['|', ('active', '=', True), ('active', '=', False)] 域。
小贴士
[('active', 'in' (True, False))] 并不会像你预期的那样工作。Odoo 明确在域中寻找 ('active', '=', False) 条件。它将默认只限制搜索到活动记录。
添加具有可配置精度的浮点字段
当使用 float 字段时,我们可能希望让最终用户配置将要使用的十进制精度。在本菜谱中,我们将向 hostel 模型添加一个 hostel_rating 字段,并允许用户配置十进制精度。
准备工作
我们将继续使用前一个菜谱中的 my_hostel 扩展模块。
如何做到这一点...
执行以下步骤以将动态十进制精度应用于模型的 hostel_rating 字段:
-
创建一个数据文件夹并添加一个
data.xml文件。在此文件中,为十进制精度模型添加以下记录。这将添加一个新的配置。<record forcecreate="True" id="decimal_point" model="decimal.precision"> <field name="name">Rating Value</field> <field name="digits">3</field> </record> -
从设置菜单中的链接激活开发者模式(参考第一章,安装 Odoo 开发环境中的激活 Odoo 开发者工具配方)。这将启用设置 | 技术菜单。
-
访问小数精度配置。为此,打开设置顶部菜单并选择技术 | 数据库结构 | 小数精度。我们应该能看到当前定义的设置列表。

图 4.2 – 创建新的小数精度
-
要使用此小数精度设置添加
model字段,请通过编辑models/hostel.py文件并添加以下代码:class Hostel(models.Model): hostel_rating = fields.Float('Hostel Average Rating', # digits=(14, 4) # Method 1: Optional precision (total, decimals), digits='Rating Value' # Method 2 )
它是如何工作的...
当您向字段的digits属性添加一个字符串值时,Odoo 会在小数精度模型的Usage字段中查找该字符串,并返回一个元组,具有 16 位精度和配置中定义的小数位数。使用字段定义,而不是将其硬编码,我们允许最终用户根据其需求进行配置。
向模型添加货币字段
要在模型中处理货币值和货币,我们可以使用 Odoo 通过使用特定的字段类型和功能来提供对货币值和货币的特殊支持。Odoo 对货币值和货币的特殊支持简化了财务数据的处理,确保准确性、一致性和符合货币相关要求。
准备工作
我们将使用之前配方中的相同my_hostel附加模块。
如何操作…
我们需要添加一个货币字段以及一个货币字段来存储金额的货币。
我们将添加models/hostel_room.py,以添加必要的字段:
-
创建字段以存储金额的货币:
class HostelRoom(models.Model): _name = "hostel.room" …# currency_id = fields.Many2one('res.currency', string='Currency') -
添加货币字段以存储金额:
class HostelRoom(models.Model): _name = "hostel.room" …# rent_amount = fields.Monetary('Rent Amount', help="Enter rent amount per month") # optional attribute: currency_field='currency_id' incase currency field have another name then 'currency_id'为新模型创建一个安全文件和一个表单视图以在 UI 中显示它。升级附加模块以应用更改。货币字段将显示如下:

图 4.3 – 货币字段中的货币符号
它是如何工作的…
Odoo 可以正确地在用户界面中显示货币字段,因为它们有一个表示其货币的第二个字段。这个字段类似于一个浮点字段。
货币字段通常命名为currency_id,但我们可以使用任何其他名称,只要我们使用可选的currency_field参数指定它。
如果您的货币信息存储在名为currency_id的字段中,您不需要为货币字段指定currency_field属性。
当您需要在同一记录中存储不同货币的金额时,这很有用。例如,如果您想有销售订单和公司的货币,您可以创建两个字段作为fields.Many2one(res.currency),并为每个金额使用一个。
货币定义(res.currency model 的 decimal_precision 字段)决定了金额的小数精度。
向模型添加关系字段
关系字段用于表示 Odoo 模型之间的关系。有三种类型的关系:
-
多对一,或简称为m2o -
一对一,或简称为o2m -
多对多,或简称为m2m
为了说明这一点,让我们考虑宿舍房间模型。一个房间属于一个单独的宿舍,因此宿舍和房间之间的关系是 m2o。然而,一个宿舍可以有多个房间,所以相反的关系是 o2m。
我们还可以有一个 m2m 关系。例如,一个房间可以提供各种便利设施,便利设施可以在不同的房间中可用。这是一个双向的 m2m 关系。
准备工作
我们将继续使用之前菜谱中的 my_hostel 附加模块。
如何做到这一点...
我们将编辑 models/hostel_room.py 文件以添加这些字段:
-
在
Hostel Room中添加m2o字段:class HostelRoom(models.Model): # ... hostel_id = fields.Many2one("hostel.hostel", "hostel", help="Name of hostel") -
我们想为一名学生创建一个链接到房间的
o2m字段。 -
首先,我们需要一个新的宿舍学生模型。我们将创建一个
hostel_student.py文件,并向宿舍学生模型添加一些基本字段。然后,我们将添加一个room_idm2o字段来连接学生和房间模型。 -
最后,我们将向
hostel.room模型添加一个o2m字段,student_ids,来自hostel.student模型:class HostelStudent(models.Model): _name = "hostel.student" name = fields.Char("Student Name") gender = fields.Selection([("male", "Male"), ("female", "Female"), ("other", "Other")], string="Gender", help="Student gender") active = fields.Boolean("Active", default=True, help="Activate/Deactivate hostel record") room_id = fields.Many2one("hostel.room", "Room", help="Select hostel room") class HostelRoom(models.Model): _name = "hostel.room" # ... student_ids = fields.One2many("hostel.student", "room_id", string="Students", help="Enter students") -
我们将创建一个新的文件,
hostel_amenities.py。将以下代码添加到该文件中:class HostelAmenities(models.Model): _name = "hostel.amenities" _description = "Hostel Amenities" name = fields.Char("Name", help="Provided Hostel Amenity") active = fields.Boolean("Active", help="Activate/Deactivate whether the amenity should be given or not")现在,我们将向
hostel.room模型添加一个便利设施的m2m字段。将以下代码添加到hostel_room.py文件中:class HostelRoom(models.Model): _name = "hostel.room" # ... hostel_amenities_ids = fields.Many2many("hostel.amenities", "hostel_room_amenities_rel", "room_id", "amenitiy_id", string="Amenities", domain="[('active', '=', True)]", help="Select hostel room amenities")
现在,升级附加模块,新字段应该可以在模型中找到。除非将它们添加到视图中,否则它们在视图中是不可见的。我们将向 hostel_room.xml 文件中添加新字段。
我们可以通过检查 设置 | 技术 | 数据库结构 | 模型 中的 开发者 模式下的 model 字段来确认它们的添加。
它是如何工作的…
m2o 字段存储模型表列中另一个记录的数据库 ID。这将在数据库中创建一个外键约束,确保存储的 ID 是对另一个表中记录的有效引用。默认情况下,这些关系字段没有数据库索引,但您可以通过设置 index=True 属性来添加一个。
您还可以指定当引用 m2o 字段的记录被删除时会发生什么。ondelete 属性控制这种行为。例如,当学生的房间记录被删除时,应该发生什么?默认选项是 'set null',这意味着字段将具有空值。另一个选项是 'restrict',这意味着相关的记录不能被删除。第三个选项是 'cascade',这意味着链接的记录也将被删除。
您也可以为其他关系字段使用 context 和 domain。这些属性主要在客户端有用,并为通过字段访问的相关记录视图提供默认值:
-
context在您点击字段以查看相关记录视图时,在客户端上下文中设置一些变量。例如,您可以使用它为此视图中创建的新记录设置默认值。 -
domain是一个过滤器,限制了您可以从中选择的关联记录列表。
您可以在第九章 后端视图中了解更多关于 context 和 domain 的信息。
o2m 字段是 m2o 字段的相反,它允许您从模型访问相关记录列表。与其他字段不同,它不在数据库表中具有列。它只是方便地在视图中显示这些相关记录的一种方式。要使用 o2m 字段,您需要在其他模型中有一个相应的 m2o 字段。在我们的示例中,我们在房间模型中添加了一个 o2m 字段。student_ids o2m 字段引用了 hostel.room 模型的 room_id 字段。
m2m 字段在模型的表中没有列。相反,它使用数据库中的另一个表来存储两个模型之间的关系。此表有两个列,用于存储相关记录的 ID。当您使用 m2m 字段将房间及其设施链接起来时,在此表中创建一个新记录,包含房间的 ID 和设施的 ID。
Odoo 会为您创建关系表。默认情况下,关系表的名称是由两个模型的名称组成,按字母顺序排序,并带有 _rel 后缀。您可以使用 relation 属性更改此名称。
当两个模型的名称对于默认名称来说太长时,您应该使用 relation 属性。PostgreSQL 对数据库标识符的长度限制为 63 个字符。因此,如果两个模型的名称每个都超过 23 个字符,您应该使用 relation 属性设置一个较短的名称。我们将在下一节中进一步解释这一点。
还有更多...
您还可以为 m2o 字段使用 auto_join 属性。此属性允许 ORM 在此字段上使用 SQL 连接。这意味着 ORM 不会检查此字段的用户访问控制和记录访问规则。在某些情况下,这可以帮助解决性能问题,但最好避免这样做。
我们已经看到了定义关系字段的最简单方法。现在,让我们看看这些字段特有的属性。
这些是 o2m 字段的属性:
-
comodel_name:这是字段所关联的模型的名称。您需要此属性用于所有关系字段。您可以不带关键字作为第一个参数来编写它。 -
inverse_name:这仅适用于o2m字段。它是其他模型中链接回此模型的m2o字段的名称。 -
limit:这是针对o2m和m2m字段的。它设置用户界面中读取和显示的最大记录数。
这些是m2m字段的属性:
-
comodel_name:这是字段关联的模型的名称。它与o2m字段相同。 -
relation:这是数据库中存储关系的表的名称。您可以使用此属性来更改默认名称。 -
column1:这是关系表中链接到此模型的第 1 列的名称。 -
column2:这是关系表中链接到其他模型的第 2 列的名称。
Odoo 通常自动处理这些属性的创建和管理。它可以识别并利用现有关系表来处理逆m2m字段。然而,在某些特定场景中需要手动干预。
当处理同一两个模型之间的多个m2m字段时,有必要为每个字段分配不同的关系表名称。
在两个模型的名称超过 PostgreSQL 对数据库对象名称的 63 个字符限制的情况下,您必须自己设置这些属性。默认的关系表名称通常是<model1>_<model2>rel。然而,此表包含一个较长的名称的索引(<model1><model2>rel<model1>id<model2>_id_key),这也需要遵守 63 个字符的限制。因此,如果两个模型的组合名称超过此限制,您必须选择较短的关系表名称。
为模型添加层次结构
您可以使用m2o字段来表示层次结构,其中每个记录都有一个父记录和同一模型中的多个子记录。然而,Odoo 还通过使用嵌套集模型(en.wikipedia.org/wiki/Nested_set_model)来提供对此类字段的支持。当激活时,在域过滤器中使用child_of运算符的查询将显著加快。
以“宿舍”为例,我们将构建一个可以用于分类宿舍的层次结构分类树。
准备工作
我们将继续使用前一个菜谱中的my_hostel附加模块。
如何操作...
我们将为分类树添加一个新的 Python 文件,models/hostel_categ.py,如下所示:
-
要加载新的 Python 代码文件,请将以下行添加到
models/__init__.py:from . import Hostel Category model with the parent and child relationships, create the models/hostel_categ.py file with the following code:from odoo import models, fields, api
class HostelCategory(models.Model):
_name = "hostel.category"
name = fields.Char('类别')
parent_id = fields.Many2one(
'hostel.category',
string='父类别',
ondelete='restrict',
index=True)
parent_path = fields.Char(index=True)
child_ids = fields.One2many(
'hostel.category', 'parent_id',
string='子类别')
-
为了启用特殊的层次结构支持,还需要添加以下代码:
_parent_store = True _parent_name = "parent_id" # optional if field is 'parent_id' parent_path = fields.Char(index=True, unaccent=False) -
要添加检查以防止循环关系,请将以下行添加到模型中:
from odoo.exceptions import ValidationError ... @api.constrains('parent_id') def _check_hierarchy(self): if not self._check_recursion(): raise models.ValidationError( 'Error! You cannot create recursive categories.') -
现在,我们需要为一家旅舍分配一个类别。为此,我们将在
hostel.hostel模型中添加一个新的m2o字段:category_id = fields.Many2one('hostel.category')
最后,模块升级将使这些更改生效。
要在用户界面中显示hostel.category模型,您需要添加菜单、视图和安全性规则。有关更多详细信息,请参阅第三章,创建 Odoo 附加模块。或者,您也可以访问所有代码github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter04。
它是如何工作的...
我们想创建一个新的具有层次关系的模型。这意味着每个记录都可以在同一模型中有一个父记录和多个子记录。以下是完成此操作的步骤:
-
我们创建了一个
m2o字段来引用父记录。我们使用index=True来使此字段在数据库中索引,以便更快地进行查询。我们还使用ondelete='cascade'或ondelete='restrict'来控制当父记录被删除时会发生什么。 -
我们创建一个
o2m字段来访问一个记录的所有子记录。这个字段不会向数据库添加任何内容,但它是一种方便获取子记录的方法。我们通过在模型属性中使用parent_store=True来添加对层次结构的特殊支持。这使得使用child_of运算符的查询更快,但同时也使得写操作变慢。我们还添加了一个名为parent_path的辅助字段来存储用于层次结构搜索的数据。如果我们为父字段使用与parent_id不同的名称,我们需要在模型属性中使用parent_name来指定它。 -
我们通过使用
models.Model中的_check_recursion方法来防止层次结构中的循环依赖。这避免了我们有一个既是另一个记录的祖先又是后代的记录,这可能导致无限循环。 -
我们在
hostel.hostel模型中添加了一个category_id字段,类型为Many2one,以便我们可以为每个旅舍分配一个类别。这只是为了完成我们的示例。
还有更多...
您应该使用这种技术来处理层次结构变化不大但读取和查询频繁的情况。这是因为数据库中的嵌套集模型需要在添加、删除或移动类别时更新所有记录的parent_path列(和相关数据库索引)。这可能会很慢且成本高昂,尤其是在有大量并发事务的情况下。
如果您的层次结构经常变化,您可能会通过使用标准的parent_id和child_ids关系获得更好的性能。这样,您可以避免表级锁定。
向模型添加约束验证
我们想确保我们的模型没有无效或不一致的数据。Odoo 有两种类型的约束来完成这项工作:
-
数据库级别的约束:这是 PostgreSQL 支持的约束。最常见的是UNIQUE约束,它防止重复值。我们还可以使用CHECK和EXCLUDE约束来满足其他条件。这些约束快速且可靠,但它们受限于 PostgreSQL 能做什么。 -
服务器级别的约束:这是我们编写的 Python 代码中的约束。当数据库级别的约束不足以满足我们的需求时,我们可以使用这些约束。这些约束更灵活且功能强大,但它们较慢且更复杂。
准备工作
我们将继续使用之前配方中的my_hostel附加模块。我们将使用宿舍房间模型并向其添加一些约束。我们将使用来自第三章的宿舍房间模型,创建 Odoo 附加模块,并向其添加一些约束。
我们将使用一个UNIQUE约束来确保房间号码不会重复。我们还将添加一个 Python 模型约束来检查租金金额是否为正。
如何操作...
-
SQL 约束是通过
_sql_constraints模型属性定义的。此属性被分配一个包含字符串(name,sql_definition,message)的三元组列表,其中name是有效的 SQL 约束名称,sql_definition是table_constraint表达式,message是错误消息。我们可以在hostel.room模型中添加以下代码:_sql_constraints = [ ("room_no_unique", "unique(room_no)", "Room number must be unique!")] -
一个检查记录集中条件的方法。我们使用
constrains()装饰器标记方法为约束,并指示哪些字段参与了条件。当这些字段中的任何一个被更改时,将自动检查约束。如果条件不满足,方法应抛出异常:from odoo.exceptions import ValidationError ... @api.constrains("rent_amount") def _check_rent_amount(self): """Constraint on negative rent amount""" if self.rent_amount < 0: raise ValidationError(_("Rent Amount Per Month should not be a negative value!"))在对代码文件进行这些更改后,您需要升级附加模块并重新启动服务器。
注意
如果你通过模型继承将 SQL 约束添加到现有模型中,请确保没有违反约束的行。如果你有此类行,则不会添加 SQL 约束,并在日志中生成错误。
有关 PostgreSQL 约束的一般信息和特定于表的约束的更多信息,请参阅www.postgresql.org/docs/current/static/ddl-constraints.html。
它是如何工作的...
我们可以使用 Python 代码来验证我们的模型并防止无效数据。为此,我们使用两个东西:
一个检查记录集中条件的方法。我们使用constrains()装饰器标记方法为约束,并指示哪些字段参与了条件。当这些字段中的任何一个被更改时,将自动检查约束。
当条件不满足时,我们抛出的ValidationError异常。此异常向用户显示错误消息并停止操作。
向模型添加计算字段
我们可能想要创建一个字段,该字段依赖于同一记录或相关记录中其他字段的值。例如,我们可以通过将单价乘以数量来计算总额。在 Odoo 模型中,我们可以使用计算字段来完成这项工作。
为了演示计算字段的工作原理,我们将在宿舍房间模型中添加一个计算字段,该字段根据学生入住情况计算房间可用性。
我们还可以使计算字段可编辑和可搜索。我们将在我们的示例中向您展示如何做到这一点。
准备工作
我们将继续使用之前菜谱中的my_hostel附加模块。
如何做到这一点...
我们将修改models/hostel_room.py代码文件,以包含一个新字段及其实现逻辑的方法:
-
计算字段的值通常依赖于同一记录中其他字段的值。ORM 要求开发者在
compute方法中使用depends()装饰器声明这些依赖关系。ORM 使用给定的依赖关系,在任何一个依赖关系发生变化时重新计算字段。首先,将新字段添加到HostelRooms模型中:student_per_room = fields.Integer("Student Per Room", required=True,help="Students allocated per room")' availability = fields.Float(compute="_compute_check_availability",string="Availability", help="Room availability in hostel") @api.depends("student_per_room", "student_ids") def _compute_check_availability(self): """Method to check room availability""" for rec in self: rec.availability = rec.student_per_room - len(rec.student_ids.ids) -
默认情况下,计算字段是只读的,因为用户不应输入值。
然而,在某些情况下,允许用户直接设置值可能是有帮助的。例如,在我们的宿舍学生场景中,我们将添加入学日期、出院日期和持续时间。我们希望用户能够输入持续时间或出院日期,其他值将相应更新:
admission_date = fields.Date("Admission Date", help="Date of admission in hostel", default=fields.Datetime.today) discharge_date = fields.Date("Discharge Date", help="Date on which student discharge") duration = fields.Integer("Duration", compute="_compute_check_duration", inverse="_inverse_duration", help="Enter duration of living") @api.depends("admission_date", "discharge_date") def _compute_check_duration(self): """Method to check duration""" for rec in self: if rec.discharge_date and rec.admission_date: rec.duration = (rec.discharge_date - rec.admission_date).days def _inverse_duration(self): for stu in self: if stu.discharge_date and stu.admission_date: duration = (stu.discharge_date - stu.admission_date).days if duration != stu.duration: stu.discharge_date = (stu.admission_date + timedelta(days=stu.duration)).strftime('%Y-%m-%d')计算方法将值分配给字段,而逆方法将值分配给字段的依赖关系。
注意,当记录保存时调用逆方法,而计算方法在其依赖关系中的任何一个发生变化时都会被调用。
-
计算字段默认情况下不存储在数据库中。一种解决方案是将字段存储为具有
store=True属性:availability = fields.Float(compute="_compute_check_availability", store=True, string="Availability", help="Room availability in hostel")由于计算字段默认情况下不存储在数据库中,除非我们使用
store=True属性或添加一个search方法,否则无法对计算字段进行搜索。
它是如何工作的...
计算字段看起来像一个普通字段,但它有一个compute属性,该属性指定了计算其值的方法的名称。
然而,计算字段在内部并不等同于普通字段。计算字段在运行时即时计算,因此它们不存储在数据库中,所以默认情况下无法对它们进行搜索或写入。您需要做一些额外的工作来启用对它们的写入和搜索支持。让我们看看如何做。
计算方法在运行时即时计算,但 ORM 使用缓存来避免每次访问其值时都无必要地重新计算。因此,它需要知道它依赖于哪些其他字段。它使用@depends装饰器来确定何时应该使缓存的值无效并重新计算。
确保计算方法始终为计算字段分配一个值。否则,将发生错误。这可能会发生在您的代码中有条件有时未能为计算字段分配值的情况下。这可能很难调试。
通过实现inverse方法可以添加写入支持。这使用分配给计算字段的值来更新源字段。当然,这仅适用于简单计算。然而,仍然有一些情况下它可能很有帮助。在我们的例子中,我们通过编辑持续时间天数来使设置排放日期成为可能,因为Duration是一个计算字段。
inverse属性是可选的;如果您不想使计算字段可编辑,可以跳过它。
还可以通过将search属性设置为方法名(类似于compute和inverse)来使非存储计算字段可搜索。与inverse一样,search也是可选的;如果您不想使计算字段可搜索,可以跳过它。
然而,这种方法不应该执行实际的搜索。相反,它接收用于在字段上搜索的操作符和值作为参数,并应该返回一个域,其中包含要使用的替代搜索条件。
可选的store=True标志将字段存储在数据库中。在这种情况下,计算后,字段值存储在数据库中,并且从那时起,它们以与常规字段相同的方式检索,而不是在运行时重新计算。多亏了@api.depends装饰器,ORM 将知道何时需要重新计算和更新这些存储值。您可以将它视为持久缓存。它还有使字段可用于搜索条件的好处,包括排序和按操作分组。如果您在计算字段中使用store=True,则不再需要实现search方法,因为该字段存储在数据库中,并且可以基于它进行搜索/排序。
compute_sudo=True标志用于需要以更高权限执行计算的情况。这可能是在计算需要使用可能对最终用户不可访问的数据时所需的。
注意
在 Odoo v13 中,compute_sudo的默认值发生了变化。在 Odoo v13 之前,compute_sudo的值是False,但在 v13 中,compute_sudo的默认值取决于store属性。如果store属性是True,则compute_sudo是True;否则,它是False。但是,您可以通过在字段定义中显式设置compute_sudo来始终覆盖它。
还有更多...
Odoo v13 为 ORM 引入了一种新的缓存机制。以前,缓存是基于环境的,但现在,在 Odoo v13 中,有一个全局缓存。因此,如果您有一个依赖于上下文值的计算字段,有时您可能会得到错误值。为了解决这个问题,您需要使用@api.depends_context装饰器。请参考以下示例:
@api.depends('price')
@api.depends_context('company_id')
def _compute_value(self):
company_id = self.env.context.get('company_id')
...
# other computation
你可以在前面的例子中看到,我们的计算使用了上下文中的company_id。通过在depends_context装饰器中使用company_id,我们确保字段值将根据上下文中company_id的值重新计算。
暴露存储在其他模型中的相关字段
Odoo 客户端只能读取他们查询的模型所属的字段数据。他们不能像服务器端代码那样使用点符号访问相关表中的数据。
然而,我们可以通过添加相关字段来使相关表中的数据对客户端可用。这就是我们将如何获取学生模型中房间的宿舍。
准备工作
我们将继续使用之前菜谱中的my_hostel附加模块。
如何操作...
编辑models/hostel_student.py文件以添加新的related字段。
确保我们有一个宿舍房间字段,然后,我们添加一个新的关联字段来将学生与他们的宿舍联系起来:
class HostelStudent(models.Model):
_name = "hostel.student"
# ...
hostel_id = fields.Many2one("hostel.hostel", related='room_id.hostel_id')
最后,我们需要升级附加模块,以便新字段可以在模型中使用。
它是如何工作的...
相关字段是一种特殊类型的字段,它引用来自不同记录的另一个字段。要创建相关字段,我们需要指定related属性,并给出一个显示要跟随的字段路径的字符串。例如,我们可以创建一个显示学生房间宿舍的相关字段,通过遵循room_id.hostel_id路径。
更多内容...
相关字段实际上是计算字段。它们只是提供了一个方便的快捷语法来从相关模型中读取字段值。由于它们是计算字段,这意味着store属性也是可用的。作为快捷方式,它们还具有所需的所有引用字段属性,例如name和translatable。
此外,它们支持一个related_sudo标志,类似于compute_sudo;当设置为True时,字段链在遍历时不检查用户访问权限。
在create()方法中使用相关字段可能会影响性能,因为这些字段的计算被延迟到它们创建的末尾。因此,如果你有一个o2m关系,例如在sale.order和sale.order.line模型中,并且你在行模型上有一个指向订单模型字段的关联字段,你应该在记录创建期间显式地读取订单模型上的字段,而不是使用关联字段快捷方式,尤其是当有很多行时。
使用引用字段添加动态关系
使用关系字段,我们需要事先决定关系的目标模型(或共同模型)。然而,有时我们可能需要将此决定留给用户,首先选择我们想要的模型,然后选择我们想要链接的记录。
使用 Odoo,这可以通过引用字段实现。
准备工作
我们将继续使用之前菜谱中的my_hostel附加模块。
如何操作...
编辑models/hostel.py文件以添加新的相关字段:
-
首先,我们需要添加一个辅助方法来动态构建可选择的目标模型列表:
from odoo import models, fields, api class Hostel(models.Model): _name = 'hostel.hostel' # ... @api.model def _referencable_models(self): models = self.env['ir.model'].search([ ('field_id.name', '=', 'message_ids')]) return [(x.model, x.name) for x in models] -
然后,我们需要添加引用字段并使用之前的函数提供可选择的模型列表:
ref_doc_id = fields.Reference( selection='_referencable_models', string='Reference Document')
由于我们正在更改模型的结构,因此需要模块升级以激活这些更改。
它是如何工作的...
引用字段类似于m2o字段,但它们允许用户选择要链接到的模型。
目标模型可以从由selection属性提供的列表中选择。selection属性必须是一个包含两个元素的元组的列表,其中第一个是模型的内部标识符,第二个是对它的文本描述。
这里有一个例子:
[('res.users', 'User'), ('res.partner', 'Partner')]
然而,而不是提供一个固定的列表,我们可以使用最常用的模型。为了简单起见,我们使用了具有消息功能的全部模型。使用_referencable_models方法,我们动态地提供了一个模型列表。
我们的配方从提供一个函数开始,该函数可以浏览所有可引用的模型记录,以动态构建一个将提供给selection属性的列表。尽管两种形式都是允许的,但我们声明了带引号的函数名,而不是直接引用不带引号的函数。这更加灵活,并允许在代码中稍后定义引用的函数,例如,这是在使用直接引用时不可能做到的。
该函数需要@api.model装饰器,因为它在模型级别上操作,而不是在记录集级别上。
虽然这个功能看起来不错,但它带来了显著的执行开销。在大量记录(例如,在列表视图中)显示引用字段可能会创建沉重的数据库负载,因为每个值都需要在单独的查询中进行查找。与常规关系字段不同,它也无法利用数据库引用完整性。
使用继承向模型添加功能
Odoo 拥有一个强大的功能,可以显著增强其灵活性和功能性,这对于寻求定制解决方案的企业尤其有益。此功能允许集成模块附加组件,使它们能够增强现有模块的功能,而无需修改其底层代码库。这是通过添加或修改字段和方法,以及通过扩展当前方法并添加补充逻辑来实现的。这种模块化方法不仅促进了可定制和可扩展的系统,而且还确保了升级和维护保持流畅,防止了与自定义修改通常相关的复杂性。
官方文档描述了 Odoo 中的三种继承方式:
-
类继承(扩展)
-
原型继承
-
委托继承
我们将在单独的配方中看到这些内容。在这个配方中,我们将看到类继承(扩展)。这是用来向现有模型添加新字段或方法的。
我们将扩展现有的合作伙伴模型 res.partner,使其包含一个计算每个用户分配了多少宿舍房间的计算字段。这将有助于确定每个房间分配给哪个部分以及哪个用户占用它。
准备工作
我们将继续使用前一个菜谱中的 my_hostel 附加模块。
如何做...
我们将扩展内置的合作伙伴模型。如果您记得,我们已经在本章的 向模型添加关系字段 菜谱中继承了 res.parnter 模型。为了使解释尽可能简单,我们将在 models/hostel_book.py 代码文件中重用 res.partner 模型:
-
首先,我们将确保
authored_book_ids反向关系在合作伙伴模型中,并添加计算字段:class ResPartner(models.Model): _inherit = "res.partner" is_hostel_rector = fields.Boolean("Hostel Rector", help="Activate if the following person is hostel rector") assign_room_ids = fields.Many2many('library.book',string='Authored Books') count_assign_room = fields.Integer( 'Number of Authored Books', compute="_compute_count_room") -
接下来,添加计算书籍数量的所需方法:
@api.depends('assign_room_ids') def _compute_count_room(self): for partner in self: partner.count_assign_room = len(partner.assign_room_ids) -
最后,我们需要升级附加模块以使修改生效。
它是如何工作的...
当使用 _inherit 属性定义模型类时,它向继承的模型添加修改,而不是替换它。
这意味着在继承类中定义的字段被添加或更改到父模型中。在数据库层,ORM 将字段添加到同一数据库表中。
字段也逐步修改。这意味着如果字段已经在超类中存在,则仅修改继承类中声明的属性;其他属性保持与父类中相同。
继承类中定义的方法将替换父类中的方法。如果您不使用 super 调用父方法,则父方法的版本将不会执行,我们将失去功能。因此,每次您通过继承现有方法添加新逻辑时,都应该包含一个带有 super 的语句来调用其父类中的版本。这将在 第五章 的 基本 服务器端开发 中更详细地讨论。
此菜谱将为现有模型添加新字段。如果您还希望将这些新字段添加到现有视图中(用户界面),请参阅 第九章 的 更改现有视图 – 视图继承 菜谱,后端视图。
使用继承复制模型定义
我们在前一个菜谱中看到了类继承(扩展)。现在,我们将看到 hostel.room 模型。
准备工作
我们将继续使用前一个菜谱中的 my_hostel 附加模块。
如何做...
原型继承是通过同时使用 _name 和 _inherit 类属性来执行的。执行以下步骤以生成 hotel.room 模型的副本:
-
在
/my_hostel/models/目录下添加一个名为hostel_room_copy.py的新文件。 -
将以下内容添加到
hostel_room_copy.py文件中:from odoo import fields, models, api, _ class HostelRoomCopy(models.Model): _name = "hostel.room.copy" _inherit="hostel.room" _description = "Hostel Room Information Copy" -
将新文件引用导入
/my_library/models/__init__.py文件。在更改之后,您的__init__.py文件将看起来像这样:from . import hostel_room from . import hostel_room_copy -
最后,我们需要升级附加模块以使修改生效。
-
要检查新模型的定义,请转到这里的
hostel.room.copy模型。
小贴士
为了查看新模型的菜单和视图,你需要添加视图和菜单的 XML 定义。要了解更多关于视图和菜单的信息,请参考 第三章 中的 添加菜单项和视图食谱,创建 Odoo 附加模块。
它是如何工作的...
通过同时使用 _name 和 _inherit 类属性,你可以复制模型的定义。当你在这两个属性中使用模型时,Odoo 将复制 _inherit 的模型定义,并创建一个具有 _name 属性的新模型。
在我们的例子中,Odoo 将复制 Hostel.room 模型的定义并创建一个新的模型,名为 hostel.room.copy。新的 hostel.room.copy 模型拥有自己的数据库表和独立于 hostel.room 父模型的自己的数据。由于它仍然继承自合作伙伴模型,对它的任何后续修改也将影响新的模型。
原型继承复制父类的所有属性。它复制字段、属性和方法。如果你想在子类中修改它们,你只需在子类中添加一个新的定义即可。例如,hostel.room 模型有 _name_get 方法。如果你想在子类中使用不同的 _name_get 版本,你需要重新定义 hostel.room.copy 模型中的方法。
注意
如果你在 _inherit 和 _name 属性中使用相同的模型名称,原型继承将不起作用。如果你确实在 _inherit 和 _name 属性中使用相同的模型名称,它将表现得像正常的扩展继承。
还有更多...
在官方文档中,这被称为原型继承,但在实践中,它很少被使用。原因是委托继承通常以更有效的方式满足这种需求,而不需要复制数据结构。有关更多信息,请参考下一道食谱,使用委托继承将功能复制到另一个模型。
使用委托继承将功能复制到另一个模型
第三种继承类型是委托继承。它不使用 _inherit,而是使用 _inherits 类属性。有些情况下,我们不想修改现有的模型,而是想基于现有的模型创建一个新的模型来使用它已有的功能。我们可以使用原型继承复制模型定义,但这将生成重复的数据结构。如果你想在复制模型定义时不重复数据结构,那么答案就在 Odoo 的委托继承中,它使用 _inherits 模型属性(注意额外的 s)。
传统继承与面向对象编程中同名概念的差异很大。委托继承,反过来,在创建一个新模型以包含父模型的功能方面是相似的。它还支持多态继承,其中我们可以从两个或更多其他模型继承。
我们运营一个既提供房间又容纳学生的宿舍。为了更好地管理我们的住宿,将学生相关信息整合到我们的系统中至关重要。具体来说,对于每个学生,我们需要全面的身份和地址信息,类似于合作伙伴模型中捕获的信息。此外,维护与房间分配相关的记录也非常关键,包括每个学生入住的开始和结束日期以及他们的卡号。
直接将这些字段添加到现有的合作伙伴模型中并不是一个理想的方法,因为这会在模型中无谓地添加与学生特定的数据,这些数据对于非学生合作伙伴来说是不相关的。一个更有效的解决方案是通过创建一个新的模型来增强合作伙伴模型,该模型从它继承并引入了管理学生信息所需的所有额外字段。这种方法确保了一个更干净、更有组织、功能更高效的系统,以满足我们宿舍的独特需求。
准备工作
我们将继续使用之前菜谱中的 my_hostel 附加模块。
如何操作...
新的图书馆会员模型应该有自己的 Python 代码文件,但为了尽可能简化说明,我们将重用 models/hostel_student.py 文件:
-
添加继承自
res.partner的新模型:class HostelStudent(models.Model): _name = "hostel.student" _inherits = {'res.partner': 'partner_id'} _description = "Hostel Student Information" ……… partner_id = fields.Many2one('res.partner', ondelete='cascade') -
接下来,我们将添加每个学生特有的字段:
gender = fields.Selection([("male", "Male"), ("female", "Female"), ("other", "Other")], string="Gender", help="Student gender") room_id = fields.Many2one("hostel.room", "Room", help="Select hostel room")
现在,我们将升级附加模块以激活更改。
它是如何工作的...
_inherits 模型属性设置了我们想要继承的父模型。在这种情况下,我们只有一个——res.partner。其值是一个键值字典,其中键是继承的模型,值是用于链接它们的字段名称。这些是必须也在模型中定义的 m2o 字段。在我们的例子中,partner_id 是将用于与 Partner 父模型链接的字段。
为了更好地理解它是如何工作的,让我们看看当我们创建一个新会员时在数据库级别上会发生什么:
-
在
res_partner表中创建了一个新记录。 -
在
hostel_student表中创建了一个新记录。 -
hostel_student表的partner_id字段被设置为为其创建的res_partner记录的 ID。
会员记录自动链接到一个新的合作伙伴记录。这只是一个 m2o 关系,但委托机制添加了一些魔法,使得合作伙伴的字段看起来就像属于会员记录一样,并且也会自动创建一个新的合作伙伴记录。
你可能想知道,这个自动创建的合作伙伴记录并没有什么特别之处。它是一个普通的合作伙伴,如果你浏览合作伙伴模型,你将能够找到那条记录(当然,不包括额外的成员数据)。所有成员都是合作伙伴,但只有一些合作伙伴也是成员。那么,如果你删除了一个也是成员的合作伙伴记录会发生什么呢?你可以通过为关系字段选择ondelete值来决定。对于partner_id,我们使用了cascade。这意味着删除合作伙伴也会删除相应的成员。我们本来可以使用更保守的设置restrict来禁止在合作伙伴有链接成员的情况下删除合作伙伴。在这种情况下,只有删除成员才会生效。
重要的是要注意,委托继承仅适用于字段,不适用于方法。所以,如果合作伙伴模型有一个do_something()方法,成员模型将不会自动继承它。
还有更多...
对于这种继承委托有一个快捷方式。你不需要创建一个_inherits字典,你可以在m2o字段定义中使用delegate=True属性。这将与_inherits选项完全一样。主要优势是这更简单。在给定的例子中,我们执行了与上一个例子相同的继承委托,但在这个例子中,我们不是创建一个_inherits字典,而是在partner_id字段中使用了delegate=True选项:
class HostelStudent(models.Model):
_name = "hostel.student"
_description = "Hostel Student Information"
partner_id = fields.Many2one('res.partner', ondelete='cascade', delegate=True)
委托继承的一个值得注意的案例是用户模型,res.users。它从合作伙伴(res.partner)继承。这意味着用户上可以看到的一些字段实际上存储在合作伙伴模型中(特别是name字段)。当创建新用户时,我们也会得到一个新的、自动创建的合作伙伴。
我们还应该提到,使用_inherit的传统继承也可以将功能复制到新模型中,尽管效率较低。这在使用继承向模型添加功能的菜谱中已经讨论过。
使用抽象模型实现可重用模型功能
有时,我们希望能够在几个不同的模型中添加特定的功能。在不同的文件中重复相同的代码是一种不良的编程实践;最好是实现一次并重用它。
抽象模型允许我们创建一个通用的模型,该模型实现了一些功能,然后这些功能可以被常规模型继承,以便使该功能可用。
作为例子,我们将实现一个简单的存档功能。这将在模型中添加active字段(如果尚未存在)并使存档方法可用以切换active标志。这是因为active是一个魔法字段。如果它默认存在于模型中,则带有active=False的记录将被从查询中过滤出来。然后我们将它添加到hostel room模型中。
准备工作
我们将继续使用上一个菜谱中的my_hostel附加模块。
如何做到这一点...
存档功能确实值得拥有自己的附加模块,或者至少是自己的 Python 代码文件。然而,为了使解释尽可能简单,我们将它塞入models/hostel_room.py文件中:
-
添加存档功能的抽象模型。它必须在库书籍模型中定义,在那里它将被使用:
class BaseArchive(models.AbstractModel): _name = 'base.archive' active = fields.Boolean(default=True) def do_archive(self): for record in self: record.active = not record.active -
现在,我们将编辑宿舍房间模型以继承存档模型:
class HostelRoom(models.Model): _name = "hostel.room" _inherit = ['base.archive']
为了激活更改,需要升级附加模块。
它是如何工作的...
抽象模型是通过基于models.AbstractModel的类创建的,而不是通常的models.Model。它具有常规模型的所有属性和能力;区别在于 ORM 不会在数据库中为它创建实际表示。这意味着它不能存储任何数据。它仅作为将添加到常规模型的可重复使用功能的模板。
我们的存档抽象模型相当简单。它只是添加了active字段和一个用于切换active标志值的方法,我们预计将来将通过用户界面上的按钮使用它。当一个模型类使用_inherit属性定义时,它将继承这些类的属性方法,而当前类中定义的属性方法将对继承的功能进行修改。
这里所涉及的机制与常规模型扩展(如根据使用继承向模型添加功能的配方)相同。你可能已经注意到_inherit使用的是模型标识符的列表,而不是单个模型标识符的字符串。实际上,_inherit可以同时采用这两种形式。使用列表形式允许我们从多个(通常是Abstract)类中继承。在这种情况下,我们只继承了一个,所以使用文本字符串就足够了。这里使用列表是为了说明。
还有更多...
值得注意的是,内置的抽象模型之一是mail.thread,它由mail (Discuss)附加模块提供。在模型上,它启用了许多表单底部看到的消息墙的讨论功能。
除了AbstractModel之外,还有一种第三种模型类型可用——models.TransientModel。它具有与models.Model类似的数据库表示,但创建在该处的记录应该是临时的,并且由服务器计划的任务定期清除。除此之外,临时模型的工作方式与常规模型相同。
models.TransientModel对于更复杂的用户交互非常有用,这种交互被称为向导。向导用于从用户那里获取输入。在第八章 高级服务器端开发技术中,我们将探讨如何使用这些技术进行高级用户交互。
第五章:基本服务器端开发
我们在第四章 应用模型中学习了如何在自定义模块中声明或扩展业务模型。该章节的教程中介绍了为计算字段编写方法和限制字段值的方法。本章重点介绍 Odoo 方法声明、记录集操作和扩展继承方法的服务器端编程基础。您可以使用这些知识在 Odoo 模块中创建或修改业务登录。
本章将涵盖以下教程:
-
指定模型方法和实现 API 装饰器
-
通知用户错误
-
为不同的模型获取一个空白记录集
-
创建新记录
-
更新记录集记录的值
-
搜索记录
-
合并记录集
-
过滤记录集
-
遍历记录集关系
-
排序记录集
-
扩展模型已建立的业务逻辑
-
扩展
write()和create() -
定制记录搜索方式
-
使用
read_group()按组获取数据
技术要求
Odoo 的在线平台是本章的一个先决条件。
您可以从以下 GitHub 仓库获取本章中使用的所有代码:github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter05
指定模型方法和使用 API 装饰器
Odoo 模型中的类既包含业务逻辑方法,也包含字段声明。我们在第四章 应用模型中学习了如何向模型添加字段。现在我们将看到如何在模型中包含业务逻辑和方法。
在本教程中,我们将学习如何创建一个函数,该函数可能被我们的应用程序的用户界面按钮或其他代码片段使用。此方法将在HostelRoom上操作,并采取必要的步骤来修改多个房间的状态。
准备工作
本教程假设您已准备好一个实例,其中包含my_hostel附加模块,如第三章 创建 Odoo 附加模块中所述。您需要向HostelRoom模型添加一个state字段,该字段定义如下:
from odoo import api, fields, models
class HostelRoom(models.Model):
# [...]
state = fields.Selection([
('draft', 'Unavailable'),
('available', 'Available'),
('closed', 'Closed')],
'State', default="draft")
有关更多信息,请参阅第三章 创建 Odoo 附加模块中的添加模型教程。
如何做到这一点...
要为宿舍房间定义一个方法,以更改所选房间的状态,您需要将以下代码添加到模型定义中:
-
添加一个辅助方法来检查状态转换是否允许:
@api.model def is_allowed_transition(self, old_state, new_state): allowed = [('draft', 'available'), ('available', 'closed'), ('closed', 'draft')] return (old_state, new_state) in allowed -
添加一个方法,将房间状态更改为通过参数传递的新状态:
def change_state(self, new_state): for room in self: if room.is_allowed_transition(room.state,\ new_state): room.state = new_state else: continue -
添加一个方法,通过调用
change_state方法来更改房间状态:def make_available(self): self.change_state('available') def make_closed(self): self.change_state('closed') -
在
<form>视图中添加一个按钮和状态栏。这将帮助我们通过用户界面触发这些方法:<form> ... <button name="make_available" string="Make Available" type="object"/> <button name="make_closed" string="Make Borrowed" type="object"/> <field name="state" widget="statusbar"/> ... </form>
要访问这些更新,您必须更新模块或安装它。
它是如何工作的...
教程代码中定义了几个方法。它们是典型的 Python 方法,以 self 作为它们的第一个参数,并可以选择接收额外的参数。odoo.api 模块中的 装饰器 用于装饰一些方法。
小贴士
在 Odoo 9.0 中,API 装饰器首先被添加以支持旧框架和新框架。从 Odoo 10.0 开始,之前的 API 已不再支持,然而,一些装饰器,如 @api.model,仍然在使用中。
在编写新方法时,如果您不使用装饰器,则该方法将在记录集上执行。在这样的方法中,self 是一个可以引用任意数量数据库记录的记录集(这包括空记录集),代码通常会遍历 self 中的记录以对每个单独的记录执行某些操作。
@api.model 装饰器类似,但它用于只关注模型本身的方法,而不是记录集的内容,该方法不会对记录集的内容进行操作。这个概念类似于 Python 的 @classmethod 装饰器。
在 步骤 1 中,我们创建了 is_allowed_transition() 方法。这个方法的目的在于验证从一个状态到另一个状态的转换是否有效。allowed 列表中的元组是可用的转换。例如,我们不希望允许从 closed 到 available 的转换,这就是为什么我们没有将 ('closed', 'available') 放入其中。
在 步骤 2 中,我们创建了 change_state() 方法。这个方法的目的在于改变房间的状态。当这个方法被调用时,它将根据 new_state 参数改变房间的状态。只有当转换被允许时,它才会改变房间状态。我们在这里使用了一个 for 循环,因为 self 可以包含多个记录集。
在 步骤 3 中,我们创建了通过调用 change_state() 方法来改变房间状态的方法。在我们的案例中,这个方法将由添加到用户界面的按钮触发。
在 步骤 4 中,我们在 <form> 视图中添加了 <button>。点击此按钮后,Odoo 网页客户端将调用 name 属性中提到的 Python 函数。请参考 第九章 的 添加按钮到表单 教程,了解如何从用户界面调用此类方法。我们还添加了 state 字段和 statusbar 小部件,以在 <form> 视图中显示房间的状态。
当用户从用户界面点击按钮时,将调用 步骤 3 中的某个方法。在这里,self 将是包含 hostel.room 模型记录的记录集。之后,我们调用 change_state() 方法,并根据点击的按钮传递适当的参数。
当 change_state() 被调用时,self 是 hostel.room 模型的相同记录集。change_state() 方法的主体遍历 self 以处理记录集中的每个房间。一开始在 self 上循环看起来很奇怪,但你会很快习惯这种模式。
在循环内部,change_state() 方法调用 is_allowed_transition()。调用是通过 room 本地变量进行的,但也可以对 hostel.room 模型的任何记录集进行调用,包括例如 self,因为 is_allowed_transition() 被装饰为 @api.model。如果转换被允许,change_state() 通过给记录集的属性赋值来将新状态分配给房间。这仅在长度为 1 的记录集上有效,这在遍历 self 时是保证的。
向用户报告错误
有时,在方法执行过程中停止处理是必要的,因为用户的活动无效或满足了一个错误条件。通过显示一个信息性错误消息,本教程演示了如何处理这些情况。
UserError 异常通常用于通知用户有关错误或异常情况。它通常在用户的输入未能满足预期标准或由于特定条件无法执行特定操作时使用。
准备工作
本教程要求您根据之前的说明设置一个实例,并安装了 my_hostel 扩展模块。
如何做到这一点...
我们将对前一个教程中的 change_state 方法进行修改,并在用户尝试更改 is_allowed_transition 方法不允许的状态时显示一条有用的消息。要开始,请执行以下步骤:
-
在 Python 文件的开头添加以下导入:
from odoo.exceptions import UserError from odoo.tools.translate import _ -
修改
change_state方法并在else部分抛出UserError异常:def change_state(self, new_state): for room in self: if room.is_allowed_transition(room.state, new_state): room.state = new_state else: msg = _('Moving from %s to %s is not allowed') % (room.state, new_state) raise UserError(msg)
它是如何工作的…
当 Python 中抛出异常时,它会在调用栈中向上传播,直到被处理。在 Odoo 中,响应网络客户端调用的 远程过程调用 (RPC) 层会捕获所有异常,并根据异常类触发对网络客户端的不同可能行为。
在 odoo.exceptions 中未定义的任何异常都将被处理为内部服务器错误(UserError 将在用户界面中显示错误消息。教程的代码通过抛出 UserError 来确保消息以用户友好的方式显示。在所有情况下,当前数据库事务都会回滚)。
我们正在使用一个名字奇怪的函数 _(),该函数在 odoo.tools.translate 中定义。此函数用于标记字符串为可翻译的,并在运行时检索翻译字符串,给定执行上下文中找到的最终用户的语言。更多关于此的信息可以在 第十一章,国际化 中找到。
重要提示
当使用 _() 函数时,确保你只传递带有插值占位符的字符串,而不是整个插值字符串。例如,_('Warning: could not find %s') % value 是正确的,但 _('Warning: could not find %s' % value) 是错误的,因为前者不会在翻译数据库中找到替换值后的字符串。
更多内容…
有时,你正在处理容易出错的代码,这意味着你正在执行的操作可能会生成错误。Odoo 会捕获此错误并向用户显示跟踪回溯。如果你不想向用户显示完整的错误日志,你可以捕获错误并引发一个带有有意义信息的自定义异常。在提供的示例中,我们从 try...catch 块生成 UserError,这样 Odoo 现在将显示一个带有有意义信息的警告,而不是显示完整的错误日志:
def post_to_webservice(self, data):
try:
req = requests.post('http://my-test-service.com', data=data, timeout=10)
content = req.json()
except IOError:
error_msg = _("Something went wrong during data submission")
raise UserError(error_msg)
return content
odoo.exceptions 中定义了几个异常类,所有这些类都派生自基类 except_orm 异常。其中大多数仅用于内部,除了以下内容:
-
ValidationError: 当一个字段的 Python 约束不被遵守时,会引发此异常。在第四章,“应用模型”,参考“向模型添加约束验证”教程以获取更多信息。 -
AccessError: 此错误通常在用户尝试访问不允许的内容时自动生成。如果你想从你的代码中显示访问错误,你可以手动引发此错误。 -
RedirectWarning: 使用此错误,你可以显示带有错误信息的重定向按钮。你需要向此异常传递两个参数:第一个参数是操作 ID,第二个参数是错误信息。 -
Warning: 在 Odoo 8.0 中,odoo.exceptions.Warning在 9.0 及以后的版本中扮演了与UserError相同的角色。现在它已被弃用,因为其名称具有误导性(它是一个错误,而不是警告)并且与 Python 内置的Warning类冲突。它仅为了向后兼容而保留,你应该在你的代码中使用UserError。
获取不同模型的空记录集
在创建 Odoo 代码时,当前模型的方法可以通过 self 访问。通过简单地实例化其类来开始对不同的模型进行工作是不可行的;你必须首先获取该模型的记录集。
本教程向您展示如何在模型方法中为任何在 Odoo 中注册的模型获取空记录集。
准备工作
本教程将重用 my_hostel 扩展模块中库示例的设置。
我们将在 hostel.room 模型中编写一个小的方法,并搜索所有 hostel.room.members。为此,我们需要为 hostel.room.members 获取一个空记录集。确保你已经添加了 hostel.room.members 模型和该模型的访问权限。
如何操作…
要在 hostel.room 的一个方法中获取 hostel.room.members 的记录集,你需要执行以下步骤:

图 5.1 – log_all_room_members
-
在
HostelRoom类中,编写一个名为log_all_room_members的方法:class HostelRoom(models.Model): # ... def log_all_room_members(self): # This is an empty recordset of model hostel.room.member hostel_room_obj = self.env['hostel.room.member'] all_members = hostel_room_obj.search([]) print("ALL MEMBERS:", all_members) return True -
在
<form>视图中添加一个按钮以调用我们的方法:<button name="log_all_room_members" string="Log Members" type="object"/>
更新模块以应用更改。之后,你将看到 <form> 视图。你可以通过点击该按钮在服务器日志中查看成员的记录集。
它是如何工作的…
在启动时,Odoo 加载所有模块,并将从 Model 派生的各种类组合在一起,并定义或扩展给定的模型。这些类存储在任何记录集的 env 属性中,作为 self.env 可用,是 odoo.api 模块中定义的 Environment 类的实例。
Environment 类在 Odoo 开发中扮演着核心角色:
-
它通过模拟 Python 字典提供对注册表的快捷访问。如果你知道你要查找的模型名称,
self.env[model_name]将为你提供该模型的空记录集。此外,记录集将共享self的环境。 -
它有一个
cr属性,这是一个你可以用来传递原始 SQL 查询的数据库游标。有关更多信息,请参阅 第八章,高级服务器端开发技术 中的 执行原始 SQL 查询 教程。 -
它有一个
user属性,它是当前执行调用的用户的引用。有关更多信息,请参阅 第八章,高级服务器端开发技术 和 更改执行动作的用户 教程。 -
它有一个
context属性,它是一个包含调用上下文的字典。这包括有关用户语言、时区和当前记录选择的信息。有关更多信息,请参阅 第八章,高级服务器端开发技术 中的 使用修改后的上下文调用方法 教程。
search() 调用将在后面的 搜索记录 教程中解释。
参见
有时,你可能想使用修改后的环境版本。一个例子是你可能需要一个具有不同用户和语言的环境。在 第八章,高级服务器端开发技术 中,你将学习如何在运行时修改环境。
创建新记录
在将业务逻辑流程付诸实践时,创建新记录是一个常规需求。如何为 hostel.room.category 模型构建记录包括在本教程中。我们将向 hostel.room.category 模型添加一个函数,用于生成示例目的的虚拟类别。我们将添加 <form> 视图以激活此方法。
准备工作
你需要理解你想要创建记录的模型的结构,特别是它们的名称和类型,以及这些字段上存在的任何约束(例如,是否其中一些是必填的)。
对于本教程,我们将重用第四章,应用模型中的my_hostel模块。请查看以下示例,快速回忆hostel.room.category模型:
class RoomCategory(models.Model):
_name = 'hostel.room.category'
_description = 'Hostel Room Category'
name = fields.Char('Category')
description = fields.Text('Description')
parent_id = fields.Many2one(
'hostel.room.category',
string='Parent Category',
ondelete='restrict',
index=True
)
child_ids = fields.One2many(
'hostel.room.category', 'parent_id',
string='Child Categories')
确保你已经为hostel.room.category模型添加了菜单、视图和访问权限。
如何做到这一点…
要创建一个包含一些子类别的类别,你需要执行以下步骤:

图 5.2 – 创建类别
-
在
hostel.room.category模型中创建一个名为create_categories的方法:def create_categories(self): ...... -
在此方法的主体内部,为第一个子类别的字段准备一个值字典:
categ1 = { 'name': 'Child category 1', 'description': 'Description for child 1' } -
准备第二个类别字段的值字典:
categ2 = { 'name': 'Child category 2', 'description': 'Description for child 2' } -
为父类别的字段准备一个值字典:
parent_category_val = { 'name': 'Parent category', 'description': 'Description for parent category', 'child_ids': [ (0, 0, categ1), (0, 0, categ2), ] } -
调用
create()方法来创建新的记录:record = self.env['hostel.room.category'].create(parent_category_val) -
在
<form>视图中添加一个按钮,从用户界面触发create_categories方法:<button name="create_categories" string="Create Categories" type="object"/>
它是如何工作的…
要为模型添加新记录,我们可以在与模型相关的任何记录集上调用create(values)方法。此方法返回一个长度为1的新记录集,其中包含具有在values字典中指定的字段值的记录。
字典中的键通过名称标识字段,而伴随的值反映了字段的值。根据字段类型,你需要为值传递不同的 Python 类型:
-
Text字段的值是用 Python 字符串给出的。 -
Float和Integer字段的值使用 Python 浮点数或整数给出。 -
boolean字段的值最好使用 Python 布尔值或整数给出。 -
Date字段的值是用 Python 的datetime.date对象给出的。 -
Datetime字段的值是用 Python 的datetime.datetime对象给出的。 -
Binary字段的值作为 Base64 编码的字符串传递。Python 标准库中的base64模块提供了如encodebytes(bytestring)等方法来对字符串进行 Base64 编码。 -
Many2one字段的值是用整数给出的,这个整数必须是相关记录的数据库 ID。 -
One2many和Many2many字段使用特殊的语法。值是一个包含三个元素元组的列表,如下所示:

表 5.1 – 关联字段写入
在本教程中,我们为想要创建的宿舍房间中的两个类别创建字典,然后我们使用这些字典在创建宿舍房间类别时,通过之前解释过的(0, 0, dict_val)语法,在child_ids条目中使用这些字典。
当在步骤 5中调用create()时,将创建三个记录:
-
一个用于父房间类别,由
create返回 -
在
record.child_ids中有两个属于儿童房间类别的记录。
还有更多…
如果模型为某些字段定义了一些默认值,则不需要进行特殊操作。create() 方法将负责计算在提供的字典中不存在的字段的默认值。
create() 方法还支持批量创建记录。要批量创建多个记录,您需要将多个值的列表传递给 create() 方法,如下例所示:
categ1 = {
'name': 'Category 1',
'description': 'Description for Category 1'
}
categ2 = {
'name': 'Category 2',
'description': 'Description for Category 2'
}
multiple_records = self.env['hostel.room.category'].create([categ1, categ2])
此代码将返回已创建的宿舍房间类别的记录集。
更新记录集记录的值
业务逻辑通常要求我们通过更改某些字段的值来更新记录。本教程展示了如何在我们进行过程中修改合作伙伴的 room_no 字段。
准备工作
本教程将使用与创建新记录教程相同的简化版 hostel.room 定义。您可以参考这个简化定义来了解字段信息。
我们在 hostel.room 模型中有一个 room_no 字段。为了说明目的,我们将通过点击按钮来写入此字段。
如何操作…
-
要更新房间的
room_no字段,您可以编写一个名为update_room_no()的新方法,其定义如下:def update_room_no(self): self.ensure_one() self.room_no = "RM002" -
然后,您可以在
xml中为房间的<form>视图添加一个按钮,如下所示:<button name="update_room_no" string="Update Room No" type="object"/> -
重新启动服务器并更新
my_hostel模块以查看更改。点击room_no后,将更改房间号。
它是如何工作的…
方法首先通过调用 ensure_one() 检查作为 self 传递的房间记录集是否恰好包含一个记录。如果不是这种情况,此过程将生成异常,并停止处理。这是必要的,因为我们不希望更改多个记录的房间号。如果您想更新多个值,可以移除 ensure_one() 并使用记录集上的循环来更新属性。
最后,该方法修改房间记录的属性值。它使用定义的房间号更新 room_no 字段。只需修改记录集的字段属性,就可以执行写操作。
还有更多…
如果您想向记录的字段添加新值,有三种选择:
-
第一种选择是本教程中解释过的。它通过直接分配值给表示记录字段的属性来工作,在所有上下文中都有效。一次无法分配值给所有记录集元素,因此,除非您确定您只处理单个记录,否则您需要遍历记录集。
-
第二种选择是使用
update()方法,通过传递字典映射字段名到您想要设置的值。这也仅适用于长度为1的记录集。当您需要一次性在同一记录上更新多个字段的值时,这可以节省一些打字。以下是教程的 步骤 2,重写为使用此选项:def change_room_no(self): self.ensure_one() self.update({ 'room_no': "RM002", 'another_field': 'value' ... }) -
第三个选项是调用
write()方法,传递一个将字段名映射到要设置的值的字典。此方法适用于任意大小的记录集,并且当前两个选项在每个记录和每个字段上执行一个数据库调用时,它将在单个数据库操作中更新所有具有指定值的记录。然而,它有一些限制:如果记录尚未存在于数据库中,则不起作用(有关更多信息,请参阅 第八章 中的 在更改时写入方法 教程,高级服务器端开发技术)。此外,在写入关系字段时需要特殊的格式,类似于create()方法使用的格式。请查看以下表格,了解用于生成关系字段不同值的格式:

表 5.2 – 关系字段更新
重要提示
1、2、3 和 5 操作类型不能与 create() 方法一起使用。
搜索记录
在业务逻辑方法中搜索记录也是一个常见的操作。有许多情况下,我们需要根据不同的标准搜索数据。本教程演示了通过名称和类别查找房间。
准备工作
本教程将使用与 创建新记录 教程相同的 hostel.room 定义。我们将在名为 find_room(self) 的方法中编写代码。
如何操作…
要查找房间,你需要执行以下步骤:
-
将
find_room方法添加到hostel.room模型中:def find_room(self): ... -
为你的标准编写搜索域:
domain = [ '|', '&', ('name', 'ilike', 'Room Name'), ('category_id.name', 'ilike', 'Category Name'), '&', ('name', 'ilike', 'Second Room Name 2'), ('category_id.name', 'ilike', 'SecondCategory Name 2') ] -
使用域调用
search()方法,这将返回记录集:rooms = self.search(domain)
rooms 变量将包含搜索到的房间记录集。你可以打印或记录该变量,以在服务器日志中查看结果。
它是如何工作的…
第一步 定义了以 def 关键字为前缀的方法名。
第二步 在局部变量中创建一个搜索域。通常,你会在调用搜索时看到这个创建过程,但对于复杂的域,将其单独定义是一种良好的实践。
要全面了解搜索域语法,请参阅 第九章 中的 在记录列表上定义过滤器 – 域 教程,后端视图。
第三步 使用域调用 search() 方法。该方法返回一个包含所有匹配域的记录集,然后可以进一步处理。在本教程中,我们仅使用域调用该方法,但以下关键字参数也受支持:
-
offset=N:这用于跳过与查询匹配的前N条记录。这可以与limit一起使用来实现分页,或者在处理大量记录时减少内存消耗。默认值为0。 -
limit=N:这表示最多应返回N条记录。默认情况下,没有限制。 -
order=sort_specification:此参数用于强制返回记录集中的顺序。默认情况下,顺序由模型类的_order属性决定。 -
count=boolean:如果为True,则返回记录数而不是记录集。默认为False。
重要提示
我们建议使用search_count(domain)方法而不是search(domain, count=True),因为方法名称以更清晰的方式传达了行为。两者将给出相同的结果。
有时,您需要从另一个模型进行搜索,以便搜索self返回当前模型的记录集。要从另一个模型进行搜索,我们需要获取该模型的空记录集。例如,假设我们想要搜索一些联系人。为此,我们需要在res.partner模型上使用search()方法。请参考以下代码。在这里,我们获取res.partner的空记录集以搜索联系人:
def find_partner(self):
PartnerObj = self.env['res.partner']
domain = [
'&', ('name', 'ilike', 'SerpentCS'),
('company_id.name', '=', 'SCS')
]
partner = PartnerObj.search(domain)
在前面的代码中,我们在域中有两个条件。当您有两个条件进行比较时,可以省略域中的'&',因为当您没有指定域时,Odoo 将'&'作为默认值。
还有更多...
我们之前提到,search()方法返回所有匹配域的记录。这实际上并不完全正确。安全规则确保用户只能获取他们具有read访问权限的记录。此外,如果模型有一个名为active的布尔字段,并且搜索域的任何术语都没有指定该字段的条件,那么搜索将自动添加一个隐式条件,只返回active=True的记录。因此,如果您期望搜索返回某些内容,但只得到空记录集,请确保检查active字段的值(如果存在),以检查记录规则。
请参考第八章中的调用具有不同上下文的方法教程,高级服务器端开发技术,了解如何不添加隐式的active=True条件。请查看第十章中的使用记录规则限制记录访问教程,安全访问,以获取有关记录级访问规则的信息。
如果出于某种原因,您发现自己正在编写原始 SQL 查询以查找记录 ID,确保在检索 ID 后使用self.env['record.model'].search([('id', 'in', tuple(ids))]).ids来确保应用安全规则。这在多公司Odoo 实例中尤为重要,因为记录规则用于确保公司之间的适当区分。
合并记录集
有时,您会发现您获得的记录集并不是您需要的。本教程展示了合并它们的多种方法。
准备工作
使用本教程,您需要为同一模型拥有两个或更多记录集。
如何操作...
按照以下步骤执行记录集上的常见操作:
-
要合并两个记录集到一个中,同时保留它们的顺序,请使用以下操作:
result = recordset1 + recordset2 -
要合并两个记录集到一个中,同时确保结果中没有重复,请使用以下操作:
result = recordset1 | recordset2 -
要找到两个记录集中共有的记录,请使用以下操作:
result = recordset1 & recordset2
它是如何工作的…
记录集类的实现为各种 Python 操作符重定义,这些操作符在此处被使用。以下是记录集上可用的最有用的 Python 操作符的总结表:

表 5.3 – 与域一起使用的操作符
此外,还有就地操作符 +=、-=、&= 和 |=,它们修改左侧操作数而不是创建一个新的记录集。这些操作符在更新记录的 One2many 或 Many2many 字段时非常有用。有关此示例,请参阅 更新记录集记录的值 教程。
过滤记录集
有时,你已经有了一个记录集,但只需要处理这些记录的子集。当然,你可以遍历记录集,每次检查条件并根据检查结果采取行动。构建仅包含感兴趣记录的新记录集以及在该记录集上使用单一操作可能更简单,在某些情况下,更有效。
本教程展示了如何使用 filter() 方法根据条件提取记录集的子集。
准备工作
我们将重用 创建新记录 教程中展示的简化 hostel.room 模型。本教程定义了一个方法,用于从提供的记录集中提取具有多个成员的房间。
如何做到这一点…
要从记录集中提取具有多个成员的记录,你需要执行以下步骤:
-
定义过滤记录集的方法:
def filter_members(room): all_rooms = self.search([]) filtered_rooms = self.rooms_with_multiple_members(all_rooms) -
定义接受原始记录集的方法:
@api.model def room_with_multiple_members(self, all_rooms): -
定义一个内部
predicate函数:def predicate(room): if len(room.member_ids) > 1: return True return False -
按如下方式调用
filter():return all_room.filter (predicate)
此过程的输出可以打印或记录,以便服务器日志可以包含它。有关更多信息,请参阅教程的示例代码。
它是如何工作的…
由 filter() 方法实现创建的记录集是空的。这个空记录集接收所有谓词函数评估为 True 的记录。最后,返回一个新的记录集。原始记录集中的记录仍然保持相同的顺序。
在上一个教程中使用了命名内部函数。你将经常看到匿名 Lambda 函数被用于这样的简单谓词:
@api.model
def room_with_multiple_rooms(self, all_rooms):
return all_rooms.filter(lambda b: len(b.member_ids) > 1)
实际上,你需要根据字段值在 Python 中的 truthy(非空字符串、非零数字、非空容器等)来过滤记录集。因此,如果你想过滤具有类别设置的记录,你可以像这样传递字段名进行过滤:all_rooms.filter('category_id')。
更多内容…
记住filter()使用内存来工作。使用搜索域或甚至切换到 SQL 以提高关键路径上方法的速度,但会牺牲可读性。
遍历记录集关系
当与长度为1的记录集一起工作时,各种字段都可作为记录属性。关系属性(One2many、Many2one和Many2many)也有值,这些值也是记录集。例如,假设我们想从hostel.room模型的记录集中访问类别名称。你可以通过以下方式通过Many2one字段的category_id遍历来访问类别名称:room.category_id.name。然而,当与包含多个记录的记录集一起工作时,不能使用属性。
本教程演示了如何使用mapped()函数导航记录集关系。我们将创建一个函数,从提供的房间列表中提取成员的名称。
准备工作
我们将重用本章中创建新记录教程中展示的hostel.room模型。
如何操作…
为了从房间记录集中检索成员名称,你必须执行以下操作:
-
定义一个名为
get_members_names()的方法:@api.model def get_members_names(self, rooms): -
调用
mapped()以获取成员的联系人名称:return rooms.mapped('member_ids.name')
它是如何工作的…
简单定义方法是第一步。通过调用mapped(path)函数遍历记录集字段是第二步;path是一个由点分隔的字段名字符串。路由中的下一个元素应用于mapped()为路径中的每个字段创建的新记录集。这个新记录集包含通过该字段与当前记录集中的每个元素连接的所有记录。如果路由中的最后一个字段是关系字段,则mapped()返回一个记录集;否则,返回一个 Python 列表。
mapped()方法有两个有用的属性:
-
当路由是一个单个标量字段名时,返回的列表与处理过的记录集的时序相同
-
如果路由中存在关系字段,则结果顺序不保留,但会消除重复项
重要提示
当你想对self中所有记录的Many2many字段指向的所有记录执行操作时,这个第二个属性非常有用,但你需要确保操作只执行一次(即使self的两个记录共享同一个目标记录)。
更多内容…
当使用mapped()时,请记住它是在 Odoo 服务器内部通过重复遍历关系并在内存中操作的,因此进行 SQL 查询,这可能不是高效的。然而,代码简洁且表达性强。如果你试图优化实例性能关键路径上的方法,你可能想要重写对mapped()的调用,并以适当的域将其表达为search(),甚至切换到 SQL(以牺牲可读性为代价)。
mapped() 方法也可以用一个函数作为参数进行调用。在这种情况下,它返回一个包含对 self 的每个记录应用函数的结果的列表,或者如果函数返回一个记录集,则返回由函数返回的记录集的并集。
相关教程
更多信息,请参考以下内容:
-
本章的 搜索记录 教程
-
在 第八章,高级服务器端开发技术 的 执行原始 SQL 查询 教程
排序记录集
当你使用 search() 方法获取记录集时,你可以传递一个可选的排序参数来获取特定顺序的记录集。如果你已经从一个之前的代码段中获取了一个记录集并且想要对其进行排序,这将非常有用。如果你使用集合操作来合并两个记录集,例如,这可能会导致顺序丢失,这也可能很有用。
本教程将向您展示如何使用 sorted() 方法对现有记录集进行排序。我们将按评级对房间进行排序。
准备工作
我们将重用本章中在 创建新记录 教程中展示的 hostel.room 模型。
如何操作…
为了根据 rating 获取排序后的房间记录集,你需要执行以下步骤:
-
定义一个名为
sort_rooms_by_rating()的方法:@api.model def sort_rooms_by_rating(self, rooms): -
使用
sorted()方法,如给定示例所示,根据room_rating字段对房间记录进行排序:return rooms.sorted(key='room_rating')
它是如何工作的…
简单定义方法是 第一步。在 第二步 中,我们使用房间的 sorted() 函数的记录集。作为键参数提供的字段将由 sorted() 函数内部获取其数据。然后,它使用 Python 的本地排序方法返回一个排序后的记录集。
它还有一个可选参数,reverse=True,它返回一个逆序的记录集。reverse 的使用方式如下:
rooms.sorted(key='room_rating', reverse=True)
更多内容…
sorted() 方法将对记录集中的记录进行排序。如果没有参数调用,将使用模型 _order 属性。否则,可以传递一个函数来计算比较键,其方式与 Python 内置的 sorted (sequence, key) 函数相同。
重要提示
当使用模型的默认 _order 参数时,排序被委托给数据库,并执行一个新的 SELECT 函数来获取排序。否则,排序由 Odoo 执行。根据所操作的内容以及记录集的大小,可能会有一些重要的性能差异。
扩展模型中定义的业务逻辑
将应用程序功能划分为各种模块是 Odoo 中的一种流行做法。您可以通过安装或卸载应用程序轻松实现这一点,这将启用或禁用功能。此外,当您向原始应用程序添加新功能时,您必须修改一些在原始应用程序中预定义的方法的行为。旧模型偶尔会从添加新字段中受益。这是底层框架最有用的功能之一,在 Odoo 中这个过程相当简单。
在本教程中,我们将了解如何从另一个模块中的方法扩展一个方法的方法的业务逻辑。此外,我们还将使用新模块向现有模块添加新字段。
准备工作
对于本教程,我们将继续使用上一教程中的my_hostel模块。请确保您在my_hostel模块中拥有hostel.room.category模型。
对于本教程,我们将创建一个名为my_hostel_terminate的新模块,该模块依赖于my_ hostel模块。在这个模块中,我们将管理宿舍的终止日期。我们还将根据类别自动计算退宿日期。
在第四章的如何使用继承向模型添加功能教程中,我们了解了如何向现有模型添加字段。在本模块中,我们将扩展hostel.room模型如下:
class HostelRoom(models.Model):
_inherit = 'hostel.room'
date_terminate = fields.Date('Date of Termination')
然后,按照以下方式扩展hostel.room.category模型:
class RoomCategory(models.Model):
_inherit = 'hostel.room.category'
max_allow_days = fields.Integer(
'Maximum allows days',
help="For how many days room can be borrowed",
default=365)
要在视图中添加此字段,您需要遵循更改现有视图 - 视图继承教程,该教程位于第九章的后端视图部分。您可以在github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition找到完整的代码示例。
如何做到这一点…
要扩展hostel.room模型中的业务逻辑,您需要执行以下步骤:
-
从
my_hostel_terminate模块中,我们希望在将房间状态更改为Closed时,在rooms记录中设置date_terminate。为此,我们将覆盖my_hostel_terminate模块中的make_closed方法:def make_closed(self): day_to_allocate = self.category_id.max_allow_days or 10 self.date_return = fields.Date.today() + timedelta(days=day_to_allocate) return super(HostelRoom, self).make_closed() -
我们还希望在房间归还并可供借用时重置
date_terminate,因此我们将覆盖make_available方法来重置日期:def make_available(self): self.date_terminate = False return super(HostelRoom, self).make_available()
它是如何工作的…
步骤 1和步骤 2,在前一节中执行业务逻辑的扩展。我们定义了一个扩展hostel.room的模型,并重新定义了make_closed()和make_available()方法。在两个方法的最后一行,返回了父类实现的结果:
return super(HostelRoom, self).make_closed()
在 Odoo 模型的情况下,父类不是通过查看 Python 类定义所期望的。框架已经为我们记录集动态生成了一个类层次结构,父类是我们所依赖的模块中模型的定义。因此,调用 super() 会从 my_hostel 中返回 hostel.room 的实现。在这个实现中,make_closed() 将房间状态改为 Closed。因此,调用 super() 将调用父方法,并将房间状态设置为 Closed。
还有更多...
在本教程中,我们选择扩展方法的默认实现。在 make_closed() 和 make_available() 方法中,我们在 super() 调用之前修改了返回的结果。请注意,当你调用 super() 时,它将执行默认实现。当然,你还可以在 super() 调用之后执行一些操作。当然,我们也可以同时做这两件事。
然而,在方法执行过程中改变其行为更具挑战性。为了做到这一点,我们必须重构代码以提取一个扩展点到一个不同的函数,然后我们可以在扩展模块中重写这个函数。
你可能会受到重写函数的启发。始终要极端小心。如果你不使用你方法中的 super() 实现而重写函数,扩展机制以及可能扩展该方法的附加组件将会损坏,这意味着扩展方法将永远不会被调用。除非你在一个受控环境中工作,你确定安装了哪些附加组件,并且你已经验证你没有破坏它们,否则请避免这样做。另外,如果需要,请确保清楚地记录你做的所有事情。
在调用原始方法实现之前和之后你能做什么?有很多事情,包括但不限于以下内容:
-
改变发送给初始实现的参数(过去)
-
修改之前提供给原始实现的上下文
-
改变初始实现返回的结果(之后)
-
调用另一个方法(在之前和之后)
-
创建记录(在之前和之后)
-
在禁止的情况下抛出
UserError错误以取消执行(在之前和之后) -
将
self分割成更小的记录集,并以不同的方式对每个子集调用原始实现(之前)
扩展 write() 和 create()
从本章的模型教程中扩展定义在模型中的业务逻辑展示了如何扩展定义在模型类上的方法。如果你这么想,定义在模型父类上的方法也是模型的一部分。这意味着所有定义在 models.Model(实际上,在 models.BaseModel 上,它是 models.Model 的父类)上的基方法都是可用的,并且可以被扩展。
本教程展示了如何扩展 create() 和 write() 以控制对记录某些字段的访问。
准备工作
我们将扩展来自 my_hostel 扩展模块的库示例,该模块位于 第三章,创建 Odoo 扩展模块。
将 remarks 字段添加到 hostel.room 模型中。我们只想让 Hostel Managers 组的成员能够写入该字段:
from odoo import models, api, exceptions
class HostelRoom(models.Model):
_name = 'hostel.room'
remarks = fields.Text('Remarks')
将 remarks 字段添加到 view/hostel_room.xml 文件的 <form> 视图中,以便从用户界面访问此字段:
<field name="remarks"/>
修改 security/ir.model.access.csv 文件以授予库用户写入权限:
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hostel,hostel.room.user,model_hostel_room,base.group_user,1,1,0,0
如何操作...
为了防止非经理组成员修改 remarks 的值,您需要执行以下步骤:
-
按如下方式扩展
create()方法:@api.model def create(self, values): if not self.user_has_groups('my_hostel.group_hostel_manager'): if values.get('remarks'): raise UserError( 'You are not allowed to modify ' 'remarks' ) return super(HostelRoom, self).create(values) -
按如下方式扩展
write()方法:def write(self, values): if not self.user_has_groups('my_hostel.group_hostel_manager'): if values.get('remarks'): raise UserError( 'You are not allowed to modify ' 'manager_remarks' ) return super(HostelRoom, self).write(values)
安装模块以查看代码的实际效果。现在,只有经理类型的用户可以修改 remarks 字段。为了测试此实现,您可以登录为演示用户或从当前用户撤销经理访问权限。
工作原理...
步骤 1 在上一节中重新定义了 create() 方法。在调用 create() 的基类实现之前,我们的方法使用 user_has_groups() 方法来检查用户是否属于 my_hostel.group_hostel_manager 组(这是该组的 XML ID)。如果不是这种情况,并且为 remarks 传递了值,则会引发 UserError 异常,从而阻止记录的创建。这个检查是在调用基类实现之前执行的。
步骤 2 对 write() 方法执行同样的操作。在写入之前,我们检查组和值中字段的 presence,以便在出现问题时进行写入并引发 UserError 异常。
重要提示
在网络客户端将字段设置为只读并不能阻止 RPC 调用写入它。这就是为什么我们扩展了 create() 和 write()。
在本教程中,您已经看到了如何覆盖 create() 和 write() 方法。然而,请注意,这不仅仅限于 create() 和 write() 方法。您可以覆盖任何模型方法。例如,假设您想在记录被删除时执行某些操作。为此,您需要覆盖 unlink() 方法(当记录被删除时将调用 unlink() 方法)。以下是覆盖 unlink() 方法的简短代码片段:
def unlink(self):
# your logic
return super(HostelRoom, self).unlink()
警告
super(…).unlink(), records would not be deleted.
更多内容...
在扩展 write() 时,请注意,在调用 super() 实现的 write() 之前,self 仍然是未修改的。您可以使用这一点来比较字段当前值和 values 字典中的值。
在本教程中,我们选择引发异常,但我们也可以选择从 values 字典中删除受影响的字段并静默跳过记录中该字段的更新:
def write(self, values):
if not self.user_has_groups('my_hostel.group_hostel_manager'):
if values.get('remarks'):
del values['remarks']
return super(HostelRoom, self).write(values)
在调用super().write()之后,如果你想执行额外的操作,你必须小心任何可能导致再次调用write()的事情,否则你会创建一个无限递归循环。解决方案是在上下文中放置一个标记,以便检查以中断递归:
class MyModel(models.Model):
def write(self, values):
sup = super(MyModel, self).write(values)
if self.env.context.get('MyModelLoopBreaker'):
return
self = self.with_context(MyModelLoopBreaker=True)
self.compute_things() # can cause calls to writes
return sup
在前面的示例中,我们在调用compute_things()方法之前添加了MyModelLoopBreaker键。因此,如果再次调用write()方法,它不会进入无限循环。
自定义如何搜索记录
在第三章的定义模型表示和顺序教程中,创建 Odoo 附加模块介绍了name_get()方法,该方法用于计算记录在各种地方的表现,包括在用于在 Web 客户端显示Many2one关系的控件中。
本教程将向您展示如何通过重新定义name_search来通过房间号和名称在Many2one控件中搜索房间。
准备工作
对于这个教程,我们将使用以下模型定义:
class HostelRoom(models.Model):
def name_get(self):
result = []
for room in self:
member = room.member_ids.mapped('name')
name = '%s (%s)' % (room.name, ', '.join(member))
result.append((room.id, name))
return result
当使用此模型时,Many2one控件中的房间仅通过name_search属性显示,该属性引用模型类的_rec_name属性,在我们的情况下是'name'。我们还想允许通过房间号进行过滤。
如何操作…
为了执行此教程,你需要执行以下步骤:
-
要能够通过房间名称、成员之一或房间号来搜索
hostel.room,你需要在HostelRoom类中定义_name_search()方法,如下所示:@api.model def _name_search(self, name='', args=None, operator='ilike', limit=100, name_get_uid=None): args = [] if args is None else args.copy() if not(name == '' and operator == 'ilike'): args += ['|', '|', ('name', operator, name), ('isbn', operator, name), ('author_ids.name', operator, name) ] return super(HostelRoom, self)._name_search( name=name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid) -
在
hostel.room模型中添加previous_room_idMany2one字段以测试_name_search实现:previous_room = fields.Many2one('hostel.room', string='Previous Room') -
将以下字段添加到用户界面:
<field name="previous_room_id" /> -
重新启动并更新模块以反映这些更改。
你可以通过在previous_room_id Many2one字段中搜索来调用_name_search方法。
它是如何工作的…
name_search()方法的默认实现实际上只调用_name_search()方法,该方法执行实际工作。此_name_search()方法有一个额外的参数,name_get_uid,用于一些边缘情况,例如如果你想要使用sudo()或不同的用户来计算结果。
我们将接收到的大多数参数不变地传递给方法的super()实现:
-
name是一个包含用户已输入的值的字符串。 -
args可以是None或用作可能记录的前过滤器的搜索域。(它可以来自Many2one关系的域参数,例如。) -
operator是一个包含匹配操作符的字符串。通常,你会有'ilike'或'='。 -
limit是要检索的最大行数。 -
name_get_uid可以在调用name_get()时用来指定不同的用户,以计算在控件中显示的字符串。
我们对该方法实现的实现如下:
-
如果
args是None,则生成一个新的空列表,否则复制args。我们复制列表是为了避免我们的修改对调用者产生副作用。 -
然后,我们检查
name是否不是空字符串,或者operator是否不是'ilike'。这是为了避免生成一个愚蠢的域,例如[('name', ilike, '')],它不会过滤任何内容。在这种情况下,我们直接跳到super()调用实现。 -
如果我们有
name,或者operator不是'ilike',那么我们将一些过滤条件添加到args中。在我们的例子中,我们添加了将搜索提供的名称在房间标题、房间号或成员姓名中的子句。 -
最后,我们调用带有修改后的域的
super()实现并在args中强制name为''和operator为ilike。我们这样做是为了强制默认实现_name_search()不更改它接收的域,因此我们将使用我们指定的域。
更多内容…
我们在介绍中提到,此方法用于 Many2one 小部件。为了完整性,它也用于 Odoo 的以下部分:
-
当在域中使用
One2many和Many2many字段上的in操作符时 -
要在
many2many_tags小部件中搜索记录 -
要在 CSV 文件中搜索记录,请导入
参见
在 第三章 的 创建 Odoo 扩展模块 中,定义记录列表的过滤器 - 范围 教程演示了如何定义 name_get() 方法,该方法用于创建记录的文本表示。
在 第九章 的 后端视图 中的 定义记录列表的过滤器 - 范围 教程提供了更多关于搜索域语法的详细信息。
使用 read_group() 函数按组获取数据
在之前的教程中,我们看到了如何从数据库中搜索和获取数据。然而,有时你希望通过聚合记录来获取结果,例如上个月销售订单的平均成本。通常,我们使用 SQL 查询中的 group by 和 aggregate 函数来获取此类结果。幸运的是,在 Odoo 中,我们有 read_group() 方法。在本教程中,你将学习如何使用 read_group() 方法来获取聚合结果。
准备工作
在本教程中,我们将使用来自 第三章 的 my_hostel 扩展模块,创建 Odoo 扩展模块。
修改 hostel.room 模型,如下所示模型定义:
class HostelRoom(models.Model):
_name = 'hostel.room'
name = fields.Char('Name', required=True)
cost_price = fields.Float('Room Cost')
category_id = fields.Many2one('hostel.room.category')
添加 hostel.room.category 模型。为了简单起见,我们将其添加到相同的 hostel_room.py 文件中:
class HostelCategory(models.Model):
_name = 'hostel.room.category'
name = fields.Char('Category')
description = fields.Text('Description')
我们将使用 hostel.room 模型,并获取每个类别的平均成本价格。
如何做到这一点…
要提取分组结果,我们将向 hostel.room 模型添加 _get_average_cost 方法,它将使用 read_group() 方法按组获取数据:
@api.model
def _get_average_cost(self):
grouped_result = self.read_group(
[('cost_price', "!=", False)], # Domain
['category_id', 'cost_price:avg'], # Fields to access
['category_id'] # group_by
)
return grouped_result
要测试此实现,你需要在用户界面中添加一个按钮来触发此方法。然后,你可以在服务器日志中打印结果。
它是如何工作的…
read_group() 方法内部使用 SQL 的 groupby 和 aggregate 函数来获取数据。传递给 read_group() 方法的最常见参数如下:
-
domain: 这用于过滤分组记录。有关domain的更多信息,请参阅 第九章 中的 搜索视图 教程,后端视图。 -
fields: 这个参数传递你想要与分组数据一起获取的字段名称。此参数的可能值如下:-
field name: 你可以将字段名称传递给fields参数,但如果你使用此选项,则必须将此字段名称传递给groupby参数,否则将生成错误。 -
field_name:agg: 你可以使用aggregate函数传递字段名称。例如,在cost_price:avg中,avg是一个 SQL 聚合函数。PostgreSQL 聚合函数的列表可以在www.postgresql.org/docs/current/static/functions-aggregate.html找到。 -
name:agg(field_name): 这与之前的一个相同,但使用这种语法,你可以提供列别名,例如average_price:avg(cost_price)。
-
-
groupby: 此参数接受一个字段描述列表。记录将根据这些字段进行分组。对于date和datetime列,你可以传递groupby_function来根据不同的时间间隔应用日期分组。你可以对日期类型的字段按月份进行分组。 -
read_group()也支持一些可选参数,如下所示:-
offset: 这表示可选的跳过记录数。 -
limit: 这表示可选的最大返回记录数。 -
orderby: 如果传递此选项,则结果将根据给定的字段排序。 -
lazy: 此参数接受布尔值,默认为True。如果传递True,则结果仅按第一个groupby分组,其余的groupby参数放在__context键中。如果为False,则所有groupby函数在一个调用中完成。
-
性能提示
read_group() 比从记录集读取和处理值要快得多。因此,对于 KPI 或图表,你应该始终使用 read_group()。
第六章:管理模块数据
在 Odoo 中,管理模块数据涉及各种任务,例如在安装、升级或删除模块时在数据库中创建、更新和删除记录。这通常是通过称为数据文件的 XML 文件来完成的。
本章将研究添加组件模块在安装期间可能提供数据的情况。这有助于我们在提供元数据,如视图描述、菜单或操作,或提供默认设置时。另一个重要用途是提供演示数据,当创建数据库时,如果勾选了加载演示数据复选框,则会加载这些数据。
在本章中,我们将涵盖以下主题:
-
使用外部 ID 和命名空间
-
使用 XML 文件加载数据
-
使用
noupdate和forcecreate标志 -
使用 CSV 文件加载数据
-
添加组件更新和数据迁移
-
从 XML 文件中删除记录
-
从 XML 文件中调用函数
技术要求
本章的技术要求包括在线 Odoo 平台。
本章中使用的所有代码都可以从以下 GitHub 仓库下载:github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter06。
为了避免重复大量代码,我们将利用在第四章、“应用模型”中定义的模型。为了遵循这些示例,请确保从Chapter05/my_hostel模块中获取my_hostel模块的代码。
使用外部 ID 和命名空间
Odoo 中的记录使用外部 ID 或 XML ID 进行标识。到目前为止,我们在本书的视图、菜单和操作等区域使用了 XML ID,但我们仍然不知道 XML ID 是什么。本食谱将为您提供更多关于它的清晰度。
如何操作...
我们将向已存在的记录中写入以演示如何使用跨模块引用:
-
通过注册如下数据文件来更新
my_hostel模块的清单文件:'data': [ 'data/data.xml', ], -
在
hostel.room模型中创建一个新的房间:<record id="hostel_room" model="hostel.room"> <field name="name"> Hostel Room 01 </field> </record> -
更改主公司的名称:
<record id="base.main_company" model="res.company"> <field name="name">Packt Publishing</field> </record>
安装模块以应用更改。安装后,将创建Hostel Room 01房间的新记录,并将公司重命名为Packt Publishing。
它是如何工作的...
XML ID 是一个字符串,它指向数据库中的一个记录。这些 ID 本身是ir.model.data模型的记录。该模型包括诸如声明 XML ID 的模块名称、ID 字符串、引用模型和引用 ID 等信息。
每次我们在 <record> 标签上使用 XML ID 时,Odoo 都会检查字符串是否命名空间化(即是否恰好包含一个点),如果没有,它会将当前模块名称作为命名空间添加。然后,它会查找是否在 ir.model.data 中存在具有指定名称的记录。如果是,则执行列出的字段的 UPDATE 语句;如果不是,则执行 CREATE 语句。这就是你可以在记录已存在时提供部分数据的方式,就像我们之前所做的那样。
在本教程的第一个示例中,记录的 ID 是 hostel_room_1。由于它没有命名空间,最终的扩展 ID 将具有类似这样的模块名称 – my_hostel.hostel_room_1。然后,Odoo 将尝试查找 my_hostel.hostel_room_1 的记录。由于 Odoo 还没有为该扩展 ID 创建记录,它将在 hostel.room 模型中生成一个新的记录。
在第二个示例中,我们使用了主要公司的扩展 ID,即 base.main_company。正如其命名空间所暗示的,它是从基础模块加载的。由于扩展 ID 已经存在,Odoo 将执行写入(UPDATE)操作,以便公司名称更改为 Packt Publishing。
重要提示
除了更改其他模块定义的记录之外,部分数据的一个广泛应用是使用快捷元素方便地创建记录,并在该记录上写入快捷元素不支持的字段 – <act_window id="my_action" name="My action" model="res.partner" /><record id="my_action" model="ir.actions.act_window"> <field name="auto_search" eval="False" /></record>。
在 Odoo 中,ref 函数用于在系统内部的不同记录之间建立关系。它允许你从一个记录创建到另一个记录的引用,通常使用 多对一 关系。
ref 函数,如本章“使用 XML 文件加载数据”教程中所述,如果适当,也会将当前模块作为命名空间添加,但如果结果 XML ID 已经存在,则会引发错误。这也适用于尚未命名空间的 id 属性。
如果你想查看所有外部标识符的列表,请启动开发者模式并打开 设置 | 技术 | 序列与标识符 | 外部标识符 菜单。
还有更多…
你迟早需要从你的 Python 代码中访问具有 XML ID 的记录。在这些情况下,使用 self.env.ref() 函数。这返回引用记录的浏览记录(recordset)。请注意,在这里,你始终必须传递完整的 XML ID。以下是一个完整 XML ID 的示例 – <``module_name>.<record_id>.
总有一天,你可能需要使用 Python 代码来检索具有 XML ID 的记录。在这些情况下,使用 self.env.ref() 方法。这让你可以访问链接记录的浏览记录(recordset)。请注意,你必须始终在这里传递完整的 XML ID。
你可以从用户界面中看到任何记录的 XML ID。为此,你需要在 Odoo 中激活开发者模式;请参考 第一章,安装 Odoo 开发环境,以进行操作。激活开发者模式后,打开你想要查找 XML ID 的记录的 表单 视图。你将在顶部栏看到一个错误图标。从该菜单中,点击 查看元数据 选项。参见以下截图以供参考:

图 6.1 – 打开记录元数据的菜单
参见
咨询本章的 使用 noupdate 和 forcecreate 标志 教程,以了解为什么公司的名称仅在模块安装期间更改。
使用 XML 文件加载数据
在上一个教程中,我们使用 hostel_room_1 外部标识符创建了新的房间记录。在本教程中,我们将从 XML 文件添加不同类型的数据。我们将添加一个房间和一个作者作为演示数据。我们还将添加一个知名的出版商作为正常数据到我们的模块中。
如何操作...
按照以下步骤创建两个数据 XML 文件,并在 your__manifest__.py 文件中链接它们:
-
将名为
data/demo.xml的文件添加到你的清单中,在demo部分:'demo': [ 'data/demo.xml', ], -
将以下内容添加到该文件中:
<odoo> <record id="member_hda" model="res.partner"> <field name="name">Husen Daudi</field> </record> <record id="member_jvo" model="res.partner"> <field name="name">Jay Vora</field> </record> <record id="hostel_room_1" model="hostel.room"> <field name="name">Hostel Room 01</field> <field name="room_no">HR001</field> <field name="author_ids" eval="[(6, 0, [ref('author_hda'), ref('author_jvo')])]" /> </record> </odoo> -
将名为
data/data.xml的文件添加到你的清单中,在data部分:'data': [ 'data/data.xml', ... ], -
将以下 XML 内容添加到
data/data.xml文件中:<odoo> <record id="res_partner_packt" model="res.partner"> <field name="name">Packt Publishing</field> <field name="city">Birmingham</field> <field name="country_id" ref="base.uk" /> </record> </odoo>
当你现在更新你的模块时,你会看到我们创建的出版商,如果你的数据库已启用演示数据,如第三章中所述,创建 Odoo 扩展模块,你也会找到这个房间及其成员。
它是如何工作的...
数据 XML 文件使用 <record> 标签在数据库表中创建一行。<record> 标签有两个强制属性,id 和 model。对于 id 属性,请参考 使用外部 ID 和命名空间 教程;model 属性指的是模型 _name 属性。然后,我们使用 <field> 元素填充数据库中的列,正如你命名的模型所定义的那样。模型还决定哪些字段是必须填充的,并定义默认值。在这种情况下,你不需要明确为这些字段赋值。
在模块清单中注册数据 XML 文件有两种方式。第一种是使用 data 键,第二种是使用 demo 键。data 键中的 XML 文件在每次安装或更新模块时都会被加载,而带有 demo 键的 XML 文件只有在启用了数据库的演示数据时才会被加载。
在 步骤 1 中,我们在清单中注册了一个带有 demo 键的 data XML 文件。因为我们使用了 demo 键,所以只有当你为数据库启用了演示数据时,XML 文件才会被加载。
在步骤 2中,<field>元素可以包含一个简单的文本值,例如标量值的情况。如果你需要传递文件的内容(例如设置图像),请在<field>元素上使用file属性,并传递相对于附加程序路径的文件名。
要设置引用,有两种可能性。最简单的是使用ref属性,它适用于many2one字段,并且只包含要引用的记录的 XML ID。对于one2many和many2many字段,我们需要使用eval属性。在 XML 中使用eval属性来动态评估表达式。这是一个通用属性,可以用来评估 Python 代码作为字段的值。通常,<field>标签内的内容被视为字符串——例如,<field name="value">4.5</field>。这将评估为字符串4.5而不是浮点数。如果你想将值评估为浮点数、布尔值或其他类型(除了字符串),你需要使用eval属性,例如<field name="value" eval="4.5" /> <field name="value" eval="False" />。
这里还有一个例子——将strftime('%Y-01-01')视为填充date字段的一种方式。X2many字段期望由一个包含三个元组的列表填充,其中元组的第一个值确定要执行的操作。在eval属性中,我们可以访问一个名为ref的函数,该函数返回作为字符串给出的 XML ID 的数据库 ID。这允许我们引用一个记录,而无需知道其具体的 ID,这个 ID 在不同的数据库中可能不同,如下所示:
-
(2, id, False): 这从数据库中删除了具有id的链接记录。元组的第三个元素被忽略。 -
(3, id, False): 这将id记录从one2many字段中分离出来。请注意,此操作不会删除记录——它只是保留现有的记录不变。元组的最后一个元素也被忽略。 -
(4, id, False): 这为现有的id记录添加了一个链接,元组的最后一个元素被忽略。这应该是你大多数时候使用的方法,通常伴随着ref函数来获取已知其 XML ID 的记录的数据库 ID。 -
(5, False, False): 这切断了所有链接,但保留了链接的记录。 -
(6, False, [id, ...]): 这清除当前引用的记录,用列表中提到的 ID 替换它们。元组的第二个元素被忽略。
重要提示
注意,在数据文件中顺序很重要,并且数据文件中的记录只能引用列表中先定义的数据文件中的记录。这就是为什么你应该始终检查你的模块是否可以在空数据库中安装的原因,因为在开发过程中,你经常会在各个地方添加记录,而之后定义的记录已经存在于数据库中,这是从较早的更新中来的。
演示数据总是在data键的文件之后加载,这就是为什么这个例子中的引用可以工作。
还有更多...
虽然您可以使用record元素做几乎所有事情,但有一些快捷元素使开发人员创建某些类型的记录更加方便。这些包括菜单项、模板和动作窗口。有关这些内容的更多信息,请参阅第九章,后端视图,和第十四章,CMS 网站开发。
field元素也可以包含function元素,该元素调用在模型上定义的函数以提供字段的值。请参阅从 XML 文件调用函数教程,了解我们如何简单地调用函数直接写入数据库,绕过加载机制。
前面的列表遗漏了0和1的条目,因为它们在加载数据时不是非常有用。为了完整性,它们如下所示输入:
-
(0, False, {'key': value}):这会创建一个引用模型的新的记录,其字段从位置三的字典中填充。元组的第二个元素被忽略。由于这些记录没有 XML ID,并且每次模块更新时都会进行评估,导致重复条目,因此最好避免这种做法。相反,应在自己的记录元素中创建记录,并如本教程中如何工作…部分所述进行链接。 -
(1, id, {'key': value}):这可以用来写入现有的链接记录。出于我们之前提到的原因,您应该避免在 XML 文件中使用此语法。
这些语法与我们在第五章,基本 服务器端开发中创建新记录和更新记录值教程中解释的语法相同。
使用 noupdate 和 forcecreate 标志
大多数附加模块有不同的数据类型。有些数据只需存在,模块才能正常工作,其他数据不应由用户更改,而大多数数据可以根据用户的意愿更改,并且仅作为便利提供。本教程将详细介绍如何处理不同类型。首先,我们将在已存在的记录中编写一个字段,然后我们将创建一个在模块更新期间应该被重新创建的记录。
如何做到这一点...
我们可以通过在包含的<odoo>元素或<record>元素上设置某些属性来强制执行在加载数据时从 Odoo 不同的行为:
-
添加一个在安装时创建但后续更新不会更新的发布者。然而,如果用户删除它,它将被重新创建:
<odoo noupdate="1"> <record id="res_partner_packt" model="res.partner"> <field name="name">Packt Publishing</field> <field name="city">Birmingham</field> <field name="country_id" ref="base.uk"/> </record> </odoo> -
添加一个
room类别,在附加组件更新期间不会更改,并且如果用户删除它则不会重新创建:<odoo noupdate="1"> <record id="room_category_all" model="hostel.room.category" forcecreate="false"> <field name="name">All rooms</field> </record> </odoo>
如何工作...
<odoo>元素可以有一个noupdate属性,该属性在首次读取包含的数据记录时传播到创建的ir.model.data记录,从而成为此表中的一列。
当 Odoo 安装附加组件(称为init模式)时,无论noupdate是true还是false,都会写入所有记录。当你更新附加组件(称为update模式)时,会检查现有的 XML ID,以查看它们是否设置了noupdate标志,如果是,则忽略尝试写入此 XML ID 的元素。如果用户删除了相关的记录,则不会发生这种情况,这就是为什么你可以在update模式下通过将记录上的forcecreate标志设置为false来强制notrecreate noupdate记录。
重要提示
在旧版附加组件(包括版本 8.0 之前)中,你通常会找到一个包含<record>和其他元素的<data>元素的<openerp>元素。这仍然是可能的,但已弃用。现在,<odoo>、<openerp>和<data>具有完全相同的语义;它们被用作括号来包围 XML 数据。
更多内容...
如果你想要即使带有noupdate标志也能加载数据记录,你可以使用带有--init=your_addon或-i your_addon参数的 Odoo 服务器运行。这将强制 Odoo 重新加载你的记录。然而,这也会导致已删除的记录被重新创建。请注意,如果模块绕过 XML ID 机制(例如,通过在由<function>标签调用的 Python 代码中创建记录)——这可能会导致双重记录和相关安装错误。
使用此代码,你可以绕过任何noupdate标志,但首先,请确保这真的是你想要的。解决此处场景的另一种选项是编写迁移脚本,如附加组件更新和数据迁移教程中概述的那样。
参见
Odoo 还使用 XML ID 来跟踪在附加组件更新后要删除哪些数据。如果在更新之前记录具有来自模块命名空间的 XML ID,但在更新期间未重新设置 XML ID,则该记录及其 XML ID 将从数据库中删除,因为它们被认为是过时的。有关此机制的更深入讨论,请参阅附加组件更新和数据迁移教程。
使用 CSV 文件加载数据
虽然你可以使用 XML 文件完成所有需要的功能,但当需要提供大量数据时,这种格式并不方便,尤其是考虑到许多人更习惯在 Calc 或其他电子表格软件中预处理数据。CSV 格式的另一个优点是,当你使用标准的export功能时,你得到的就是 CSV 格式。在本教程中,我们将探讨如何导入类似表格的数据。
如何操作...
传统上,Odoo 中的访问控制列表(ACLs)用于管理记录和操作访问权限。ACLs 通过预定义的规则指定谁可以在指定的条目上执行特定操作(如读取、写入、创建和删除)。ACLs 通常通过 XML 文件在 Odoo 模块中定义。有关 ACLs 的更多详细信息,请参阅第十章的 ACLs 教程,安全访问。
将 security/ir.model.access.csv 添加到你的数据文件中:
'data': [
...
'security/ir.model.access.csv',
],
-
在
ir.model.dataCSV 文件中为模块添加访问安全设置:id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_hostel_manager,hostel.room.manager,model_hostel_room,group_hostel_manager,1,1,1,1
我们现在有一个 ACL,允许宿舍管理员读取图书记录,并且还允许他们编辑、创建或删除它们。
它是如何工作的...
你只需将所有数据文件放入你的清单的 data 列表中。Odoo 会根据文件扩展名来决定文件类型。CSV 文件的一个特点是它们的文件名必须与要导入的模型名称匹配——在我们的例子中是 ir.model.access。第一行需要是一个包含与模型字段名称完全匹配的列名的标题。
对于标量值,你可以使用带引号(如果必要,因为字符串本身包含引号或逗号)或不带引号字符串。
当使用 CSV 文件编写 many2one 字段时,Odoo 首先尝试将列值解释为 XML ID。如果没有点,Odoo 会添加当前模块名称作为命名空间,并在 ir.model.data 中查找结果。如果这失败了,就会调用模型的 name_search 函数,并将列的值作为参数传递,第一个返回的结果获胜。如果这也失败了,该行将被视为无效,Odoo 会引发错误。
添加插件更新和数据迁移
当编写插件模块时,你选择的数据库模型可能存在一些弱点,因此在插件模块的生命周期中你可能需要调整它。为了在不进行大量修改的情况下允许这样做,Odoo 支持在插件模块中进行版本控制和必要时运行迁移。
如何做到这一点...
我们假设在我们模块的早期版本中,allocation_date 字段是一个字符字段,人们可以写下他们认为合适的日期。我们现在意识到我们需要这个字段来进行比较和聚合,这就是为什么我们想将其类型更改为 Date。
Odoo 在类型转换方面做得很好,但在这个案例中,我们得自己动手,这就是为什么我们需要提供如何将安装了模块早期版本的数据库转换为当前版本可以运行的指导。让我们按照以下步骤尝试:
-
在你的
__manifest__.py文件中增加版本号:'version': '17.0.2.0.1', -
在
migrations/17.0.1.0.1/pre-migrate.py中提供迁移前的代码:def migrate(cr, version): cr.execute('ALTER TABLE hostel_room RENAME COLUMN allocation_date TO allocation_date_char') -
在
migrations/17.0.1.0.1/post-migrate.py中提供迁移后的代码:from odoo import fields from datetime import date def migrate(cr, version): cr.execute('SELECT id, allocation_date_char FROM hostel_room') for record_id, old_date in cr.fetchall(): # check if the field happens to be set in Odoo's internal # format new_date = None try: new_date = fields.Date.to_date(old_date) except ValueError: if len(old_date) == 4 and old_date.isdigit(): # probably a year new_date = date(int(old_date), 1, 1) if new_date: cr.execute('UPDATE hostel_room SET allocation_date=%s WHERE id=2', (new_date,))
如果没有这段代码,Odoo 会将旧的 allocation_date 列重命名为 allocation_date_moved 并创建一个新的列,因为没有从字符字段到日期字段的自动转换。从用户的角度来看,allocation_date 中的数据就消失了。
它是如何工作的...
第一个关键点是增加您附加组件的版本号,因为迁移仅在不同版本之间运行。在每次更新期间,Odoo 都会将更新时清单中的版本号写入ir_module_module表。如果版本号有三个或更少的组件,则版本号前面会加上 Odoo 的主版本和次版本。在前面的例子中,我们明确地命名了 Odoo 的主版本和次版本,这是一个好习惯,但1.0.1的值也会有相同的效果,因为内部 Odoo 会为附加组件的短版本号添加其自己的主版本和次版本号。通常,使用长表示法是一个好主意,因为您可以一眼看出附加组件是为哪个版本的 Odoo 准备的。
这两个迁移文件仅仅是代码文件,不需要在任何地方注册。当更新附加组件时,Odoo 会将ir_module_module中记录的附加组件版本与附加组件清单中的版本进行比较。如果清单的版本更高(在添加 Odoo 的主版本和次版本之后),则将在migrations文件夹中搜索,看是否包含包含版本(s)的文件夹,包括当前更新的版本。
然后,在找到的文件夹中,Odoo 会搜索以pre-开头的 Python 文件,加载它们,并期望它们定义一个名为migrate的函数,该函数有两个参数。这个函数以数据库游标作为第一个参数,当前安装的版本作为第二个参数调用。这发生在 Odoo 甚至查看附加组件定义的其余代码之前,因此您可以假设与上一个版本相比,您的数据库布局没有发生变化。
在所有pre-migrate函数成功运行之后,Odoo 会加载附加组件中声明的模型和数据,这可能会导致数据库布局发生变化。鉴于我们在pre-migrate.py中重命名了date_release,Odoo 将仅创建一个具有该名称的新列,但具有正确的数据类型。
之后,使用相同的搜索算法,将搜索并执行找到的post-migrate文件。在我们的例子中,我们需要查看每个值,看看我们是否可以从中提取出有用的东西;否则,我们将数据保留为NULL。如果不是绝对必要,不要编写遍历整个表的脚本;在这种情况下,我们会编写一个非常大、难以阅读的 SQL 开关。
重要提示
如果您只想重命名一个列,您不需要迁移脚本。在这种情况下,您可以将字段的oldname参数设置为字段的原始列名;然后 Odoo 会自行处理重命名。
还有更多...
在迁移前和迁移后的步骤中,你只能访问到一个游标,如果你习惯了 Odoo 环境,这并不太方便。在这个阶段使用模型可能会导致意外结果,因为在迁移前步骤中,附加组件的模型尚未加载,而且在迁移后步骤中,依赖于当前附加组件的附加组件定义的模型也尚未加载。然而,如果你不介意这个问题,要么是因为你想使用你的附加组件没有触及到的模型,要么是因为你知道这个问题不会成为问题,你可以通过编写以下内容来创建你习惯的环境:
from odoo import api, SUPERUSER_ID
def migrate(cr, version):
env = api.Environment(cr, SUPERUSER_ID, {})
# env holds all currently loaded models
参见
在编写迁移脚本时,你经常会遇到重复的任务,例如检查列或表是否存在,重命名事物,或将一些旧值映射到新值。在这里重新发明轮子既令人沮丧又容易出错;如果你能承担额外的依赖,可以考虑使用 github.com/OCA/openupgradelib。
从 XML 文件中删除记录
在之前的教程中,我们学习了如何从 XML 文件中生成或更改记录。你可能会偶尔希望删除由依赖模块创建的记录。可以使用 <delete> 标签。
准备工作
在本教程中,我们将从 XML 文件中添加一些类别,然后删除它们。在实际情况中,你将从这个模块创建此记录。但为了简单起见,我们将在同一个 XML 文件中添加一些类别,如下所示:
<record id="room_category_to_remove" model="hostel.room.category">
<field name="name">Single sharing</field>
</record>
<record id="room_category_not_remove" model="hostel.room.category">
<field name="name">Double Sharing</field>
</record>
如何操作...
从 XML 文件中删除记录有两种方法:
-
使用之前创建的记录的 XML ID:
<delete model="hostel.room.category" id="room_category_to_remove"/> -
使用搜索域:
<delete model="hostel.room.category" search="[('name', 'ilike', 'Single Room Category')]"/>
它是如何工作的...
你将需要使用 <delete> 标签。要从模型中删除记录,你需要提供模型名称作为 model 属性的值。这是一个必填属性。
必须在第一种方法中提供由另一个模块的数据文件生成的记录的 XML ID。Odoo 在安装模块时会查找记录。如果指定的 XML ID 与记录匹配,则该记录将被删除;否则,将引发错误。只有从 XML 文件(或具有 XML ID 的记录)生成的记录才能被删除。
在第二种方法中,你需要通过 domain 属性传递域。在模块安装期间,Odoo 将根据此域搜索记录。如果找到记录,则将其删除。如果没有记录匹配给定的域,则此选项不会引发错误。使用此选项时要极其小心,因为它可能会删除用户的数据,因为搜索选项会删除所有匹配域的记录。
警告
<delete> 在 Odoo 中很少使用,因为它很危险。如果你不小心,可能会破坏系统。如果可能的话,尽量避免使用它。
从 XML 文件中调用函数
你可以从 XML 文件创建所有类型的记录,但有时,生成包含一些业务逻辑的数据可能很困难。你可能想在生产环境中安装依赖模块时修改记录。在这种情况下,你可以通过<function>标签调用model方法。
如何操作...
对于这个教程,我们将使用上一个教程中的代码。作为一个例子,我们将增加现有房间的价格 10 美元。请注意,你可能根据公司配置使用其他货币。
按照以下步骤从 XML 文件中调用 Python 方法:
-
将
_update_room_price()方法添加到hostel.room模型中:@api.model def _update_room_price(self): all_rooms = self.search([]) for room in all_rooms: room.cost_price += 10 -
将
<function>添加到数据 XML 文件中:<function model="hostel.room" name="_update_room_price"/>
它是如何工作的...
在步骤 1中,我们添加了_update_room_price()方法,该方法搜索所有书籍并将价格增加 10 美元。我们以_开始方法名,因为在 ORM 中这被认为是私有的,不能通过 RPC 调用。
在步骤 2中,我们使用了带有两个属性的<function>标签:
-
model:声明方法的模型名 -
name:你想调用的方法名
当你安装这个模块时,_update_room_price()将被调用,并且书籍的价格将增加 10 美元。
重要提示
总是使用这个功能时带上noupdate选项。否则,每次你更新你的模块时它都会被调用。
更多内容...
使用<function>,你也可以向函数发送参数。比如说,你只想增加特定类别的房间价格,并且你想将这个金额作为参数发送。
要做到这一点,你需要创建一个接受类别作为参数的方法,如下所示:
@api.model
def update_room_price(self, category, amount_to_increase):
category_rooms = self.search([('category_id', '=', category.id)])
for room in category_rooms:
room.cost_price += amount_to_increase
要将类别和金额作为参数传递,你需要使用eval属性,如下所示:
<function model="hostel.room"
name="update_room_price"
eval="(ref('category_xml_id'), 20)"/>
当你安装模块时,它将增加指定类别的房间价格 20 美元。
第七章:调试模块
在 第五章,基本服务器端开发,我们看到了如何编写模型方法来实现我们模块的逻辑。然而,当我们遇到错误或逻辑问题时,我们可能会陷入困境。为了解决这些错误,我们需要进行详细的检查,这可能会花费一些时间。幸运的是,Odoo 为你提供了一些调试工具,可以帮助你找到各种问题的根本原因。在本章中,我们将详细探讨各种调试工具和技术。
在本章中,我们将介绍以下食谱:
-
自动重新加载和
--dev选项 -
生成服务器日志以帮助调试方法
-
使用 Odoo 壳交互式调用方法
-
使用 Python 调试器跟踪方法执行
-
理解调试模式选项
自动重新加载和 --dev 选项
在前面的章节中,我们看到了如何添加模型、字段和视图。每次我们更改 Python 文件时,我们都需要重新启动服务器以应用这些更改。如果我们更改 XML 文件,我们需要重新启动服务器并更新模块以在用户界面中反映这些更改。如果你正在开发大型应用程序,这可能会很耗时且令人沮丧。Odoo 提供了一个命令行选项 --dev 来克服这些问题。--dev 选项有几种可能的值,在本食谱中,我们将看到每个值。
准备工作
在你的开发环境中使用以下命令安装 inotify or watchdog。没有 inotify 或 watchdog,自动重新加载功能将不会工作:
$ pip3 install inotify
$ pip3 install watchdog
如何做...
要启用 dev 选项,你需要从命令行使用 --dev=value。此选项的可能值是 all、reload、pudb|wdb|ipdb|pdb、qweb、werkzeug 和 xml。查看以下食谱以获取更多信息。
它是如何工作的...
检查以下列表以了解所有 --dev 选项及其用途:
-
reload: 每当你对 Python 进行更改时,你需要重新启动服务器以在 Odoo 中反映这些更改。--dev=reload选项会在你对任何 Python 文件进行更改时自动重新加载 Odoo 服务器。如果你没有安装 Python 的inotify包,这个功能将不会工作。当你使用此选项运行 Odoo 服务器时,你会看到这样的日志:AutoReload watcher runningwith inotify。 -
qweb: 你可以使用 QWeb 模板在 Odoo 中创建动态的网站页面。在 第十四章,CMS 网站开发,我们将看到如何使用 QWeb 模板开发网页。你可以使用t-debug属性在 QWeb 模板中调试问题。只有当你使用--dev=qweb启用dev模式时,t-debug选项才会工作。 -
werkzeug: Odoo 使用werkzeug来处理 HTTP 请求。内部,Odoo 会捕获并抑制由werkzeug产生的所有异常。如果你使用--dev=werkzeug,当产生异常时,werkzeug 的交互式调试器将在网页上显示。 -
xml: 每次你在视图结构中做出更改时,都需要重新加载服务器并更新模块以应用这些更改。使用--dev=xml选项,你只需从浏览器重新加载 Odoo 即可。无需重新启动服务器或更新模块。 -
pudb|wdb|ipdb|pdb: 你可以使用--dev=pdb选项,它将在 Odoo 中生成异常时激活 PDB。Odoo 支持四个 Python 调试器:pudb、wdb、ipdb和pdb。 -
all: 如果你使用--dev=all,所有前面的选项都将被启用。
$ odoo/odoo-bin -c ~/odoo-dev/my-instance.cfg --dev=all
如果你只想启用几个选项,可以使用逗号分隔的值,如下所示:
$ odoo/odoo-bin -c ~/odoo-dev/my-instance.cfg --dev=reload,qweb
重要提示
如果你已经更改了数据库结构,例如添加了新字段,--dev=reload 选项将不会在数据库模式中反映这些更改。你需要手动更新模块;它仅适用于 Python 业务逻辑。如果你添加了新的视图或菜单,--dev=xml 选项将不会在用户界面中反映这一点。你需要手动更新模块。这在设计视图或网站页面结构时非常有用。如果用户从 GUI 中更改了视图,那么 --dev=xml 将不会从文件中加载 XML。Odoo 将使用用户更改的视图结构。
生成服务器日志以帮助调试方法
服务器日志在尝试了解崩溃前运行时发生了什么时非常有用。它们还可以添加以在调试问题时提供更多信息。这个食谱展示了如何将日志记录添加到现有方法中。
准备工作
我们将在以下方法中添加一些日志语句,该方法将产品的库存水平保存到文件中(你还需要将 product 和 stock 模块的依赖项添加到清单中):
from os.path import join as opj
from odoo import models, api, exceptions
EXPORTS_DIR = '/srv/exports'
class ProductProduct(models.Model):
_inherit = 'product.product'
@api.model
def export_stock_level(self, stock_location):
products = self.with_context(
location=stock_location.id
).search([])
products = products.filtered('qty_available')
fname = opj(EXPORTS_DIR, 'stock_level.txt')
try:
with open(fname, 'w') as fobj:
for prod in products:
fobj.write('%s\t%f\n' % (prod.name,
prod.qty_available))
except IOError:
raise exceptions.UserError('unable to save file')
如何操作...
为了在执行此方法时获取一些日志,执行以下步骤:
-
在代码开头,导入
logging模块:import logging -
在模型类定义之前,为模块获取一个记录器:
_logger = logging.getLogger(__name__) -
修改
export_stock_level()方法的代码,如下所示:@api.model def export_stock_level(self, stock_location): _logger.info('export stock level for %s', stock_location.name) products = self.with_context( location=stock_location.id).search([]) products = products.filtered('qty_available') _logger.debug('%d products in the location', len(products)) fname = join(EXPORTS_DIR, 'stock_level.txt') try: with open(fname, 'w') as fobj: for prod in products: fobj.write('%s\t%f\n' % ( prod.name, prod.qty_available)) except IOError: _logger.exception( 'Error while writing to %s in %s', 'stock_level.txt', EXPORTS_DIR) raise exceptions.UserError('unable to save file')
它是如何工作的...
步骤 1 从 Python 标准库中导入 logging 模块。Odoo 使用此模块来管理其日志。
步骤 2 为 Python 模块设置一个记录器。我们在 Odoo 中使用常见的 __name__ 习惯用法作为记录器名称的自动变量,并通过 _logger 调用记录器。
重要提示
__name__ 变量由 Python 解释器在模块导入时自动设置,其值为模块的完整名称。由于 Odoo 对导入做了一些小技巧,附加模块在 Python 中被视为属于 odoo.addons Python 包。因此,如果食谱的代码位于 my_hostel/models/hostel.py,则 __name__ 将为 odoo.addons.my_hostel.models.hostel。
通过这样做,我们得到两个好处:
-
由于 logging 模块中日志记录器的层次结构,设置在 odoo 日志记录器上的全局日志配置应用于我们的日志记录器
-
日志将带有完整的模块路径前缀,这在尝试找到给定日志行产生的地方时非常有帮助
步骤 3 使用日志记录器生成日志消息。可用于此的方法有(按日志级别递增)% 替换和要插入到消息中的附加参数。您不需要自己处理 % 替换;如果需要生成日志,日志模块足够智能,可以执行此操作。如果您正在以 INFO 级别运行,那么 DEBUG 日志将避免替换,这将在长期运行中消耗 CPU 资源。
本食谱中展示的另一种有用方法是 _logger.exception(),它可以在异常处理程序中使用。消息将以 ERROR 级别记录,并且堆栈跟踪也会打印在应用程序日志中。
还有更多...
您可以从命令行或配置文件中控制应用程序的 日志级别。主要有两种方法来做这件事:
第一种方法是使用 --log-handler 选项。其基本语法如下:--log-handler=prefix:level。在这种情况下,前缀是日志记录器名称路径的一部分,级别是 my_hostel 日志记录器设置为 DEBUG,并为其他附加组件保留默认日志级别,你可以按照以下方式启动 Odoo:
$ python odoo.py --log-handler=odoo.addons.my_hostel:DEBUG
在命令行上可以多次指定 --log-handler。您还可以配置 odoo.service.server,我们保留信息级别消息,包括服务器启动通知:
log_handler = :ERROR,werkzeug:CRITICAL,odoo.service.server:INFO
第二种方法是使用 --log-level 选项。要全局控制日志级别,可以使用 --log-level 作为命令行选项。此选项的可能值有 critical、error、warn、debug、debug_rpc、debug_rpc_answer、debug_sql 和 test。
设置日志级别的快捷方式有一些。以下是一份列表:
-
--log-request是--log-handler=odoo.http.rpc.request:DEBUG的快捷方式 -
--log-response是--log-handler=odoo.http.rpc.response:DEBUG的快捷方式 -
--log-web是--log-handler=odoo.http:DEBUG的快捷方式 -
--log-sql是--log-handler=odoo.sql_db:DEBUG的快捷方式
使用 Odoo 壳来交互式调用方法
Odoo 网络界面是为最终用户设计的,尽管开发者模式解锁了许多强大的功能。然而,通过网络界面进行测试和调试并不是最容易的方法,因为您需要手动准备数据,在菜单中进行导航以执行操作等。Odoo 壳是一个 命令行界面,您可以使用它来发出调用。本食谱展示了如何启动 Odoo 壳并执行如调用壳内方法之类的操作。
准备工作
我们将重用之前配方中的相同代码来生成服务器日志以帮助调试方法。这允许 product.product 模型添加一个新方法。我们假设你有一个已安装并可供使用的附加组件的实例。在这个配方中,我们期望你有一个名为 project.conf 的 Odoo 配置文件。
如何做到这一点...
为了从 Odoo 壳中调用 export_stock_level() 方法,执行以下步骤:
-
启动 Odoo 壳并指定你的项目配置文件:
$ ./odoo-bin shell -c project.conf --log-level=error -
检查错误消息并阅读在常规 Python 命令行提示符之前显示的信息文本:
env: <odoo.api.Environment object at 0x7f48cc0868c0> odoo: <module 'odoo' from '/home/serpentcs/workspace/17.0/odoo/__init__.py'> openerp: <module 'odoo' from '/home/serpentcs/workspace/17.0/odoo/__init__.py'> self: res.users(1,) Python 3.10.13 (main, Aug 25 2023, 13:20:03) [GCC 9.4.0] Type 'copyright', 'credits' or 'license' for more information product.product:product = env['product.product']
-
获取主要库存位置记录:
>>> location_stock = env.ref('stock.stock_location_stock') -
调用
export_stock_level()方法:>>> product.export_stock_level(location_stock) -
在退出前提交事务:
>>> env.cr.commit() -
通过按 Ctrl + D 退出壳。
它是如何工作的...
步骤 1 使用 odoo-bin shell 启动 Odoo 壳。所有常规命令行参数都是可用的。我们使用 -c 指定项目配置文件,并使用 --log-level 减少日志的冗余。在调试时,你可能希望为某些特定的插件设置日志级别为 DEBUG。
在提供 Python 命令行提示符之前,odoo-bin shell 启动一个不监听网络的 Odoo 实例并初始化一些全局变量,这些变量在输出中提到:
-
env是一个连接到数据库的环境,并在命令行或配置文件中指定。 -
odoo是为你导入的odoo包。你可以访问该包内的所有 Python 模块以执行你想要的操作。 -
openerp是odoo包的别名,用于向后兼容。 -
self是res.users的记录集,包含一个 Odoo 超用户(管理员)的单一记录,该记录与env环境相关联。
步骤 3 和 步骤 4 使用 env 获取一个空记录集并根据 XML ID 查找记录。步骤 5 调用 product.product 记录集上的方法。这些操作与你在方法内部使用的是相同的,唯一的区别是我们使用 env 而不是 self.env(尽管我们可以两者都有,因为它们是相同的)。有关可用的更多信息,请参阅 第五章,基本服务器端开发。
步骤 6 提交数据库事务。在这里这并不是严格必要的,因为我们没有修改数据库中的任何记录,但如果我们已经这样做并且希望这些更改持久化,这是必要的;当您通过 Web 界面使用 Odoo 时,每个 RPC 调用都在自己的数据库事务中运行,Odoo 会为您管理这些事务。当在 shell 模式下运行时,这种情况不再发生,您必须自己调用 env.cr.commit() 或 env.cr.rollback()。否则,当您退出 shell 时,任何正在进行的交易都会自动回滚。在测试时,这是可以的,但如果您使用 shell,例如,来脚本化实例的配置,别忘了提交您的工作!
还有更多...
在 shell 模式下,默认情况下,Odoo 会打开 Python 的 REPL 命令行界面。您可以使用 --shell-interface 选项使用您选择的 REPL。支持的 REPL 有 ipython、ptpython、bpython 和 python:
$ ./odoo-bin shell -c project.conf --shell-interface=ptpython
使用 Python 调试器跟踪方法执行
有时候,应用程序日志不足以找出问题所在。幸运的是,我们还有 Python 调试器。这个食谱展示了我们如何在一个方法中插入断点并通过手动跟踪执行过程。
准备工作
我们将重用本章 使用 Odoo 命令行界面交互式调用方法 食谱中展示的 export_stock_level() 方法。请确保您手头有该方法的副本。
如何操作...
要使用 pdb 跟踪 export_stock_level() 的执行,请执行以下步骤:
-
编辑方法的代码,并插入此处突出显示的行:
def export_stock_level(self, stock_location): import pdb; pdb.set_trace() products = self.with_context( location=stock_location.id ).search([]) fname = join(EXPORTS_DIR, 'stock_level.txt') try: with open(fname, 'w') as fobj: for prod in products.filtered('qty_available'): fobj.write('%s\t%f\n' % (prod.name, prod.qty_available)) except IOError: raise exceptions.UserError('unable to save file') -
运行该方法。我们将使用 Odoo 命令行界面,正如在 *使用 Odoo 命令行界面交互式调用 方法 的食谱中所述:
$ ./odoo-bin shell -c project.cfg --log-level=error [...] >>> product = env['product.product'] >>> location_stock = env.ref('stock.stock_location_stock') >>> product.export_stock_level(location_stock) > /home/cookbook/stock_level/models.py(18)export_stock_level() -> products = self.with_context( (Pdb) -
在
(Pdb)提示符下,发出args命令(其快捷键为a)以获取传递给方法的参数值:(Pdb) a self = product.product() stock_location = stock.location(14,) -
输入
list命令以检查您在代码中的位置:(Pdb) list 13 @api.model 14 def export_stock_level(self, stock_location): 15 _logger.info('export stock level for %s', 16 stock_location.name) 17 import pdb; pdb.set_trace() 18 -> products = self.with_context( 19 location=stock_location.id).search([]) 20 products = products.filtered('qty_available') 21 _logger.debug('%d products in the location', 22 len(products)) 23 fname = join(EXPORTS_DIR, 'stock_level.txt') (Pdb) -
输入
next命令三次以遍历方法的第一行。您也可以使用n,这是一个快捷键:(Pdb) next > /home/cookbook/stock_level/models.py(19)export_stock_level() -> location=stock_location.id).search([]) (Pdb) n > /home/cookbook/stock_level/models.py(20)export_stock_level() -> products = products.filtered('qty_available') (Pdb) n > /home/cookbook/stock_level/models.py(21)export_stock_level() -> _logger.debug('%d products in the location', (Pdb) n > /home/cookbook/stock_level/models.py(22)export_stock_level() -> len(products)) (Pdb) n > /home/cookbook/stock_level/models.py(23)export_stock_level() -> fname = join(EXPORTS_DIR, 'stock_level.txt') (Pdb) n > /home/cookbook/stock_level/models.py(24)export_stock_level() -> try: -
使用
p命令显示products和fname变量的值:(Pdb) p products product.product(32, 14, 17, 19, 21, 22, 23, 29, 34, 33, 26, 27, 42) (Pdb) p fname '/srv/exports/stock_level.txt' -
将
fname的值更改为指向/tmp目录:(Pdb) !fname = '/tmp/stock_level.txt' -
使用
return(快捷键:r)命令执行当前函数:(Pdb) return --Return-- > /home/cookbook/stock_level/models.py(26)export_stock_level()->None -> for product in products: -
使用
cont(快捷键:c)命令恢复程序的执行:(Pdb) c >>>
它是如何工作的...
在 步骤 1 中,我们通过从 Python 标准库中的 pdb 模块调用 set_trace() 方法在方法的源代码中硬编码了一个断点。当此方法执行时,程序的正常流程会停止,您会得到一个 (Pdb) 提示符,您可以在其中输入 pdb 命令。
步骤 2 使用 shell 模式调用 stock_level_export() 方法。您也可以正常重启服务器并使用 Web 界面通过点击用户界面的适当元素来生成对您需要跟踪的方法的调用。
当你需要使用 Python 调试器手动逐步执行一些代码时,以下是一些会使你的生活变得更简单的提示:
-
将日志级别降低以避免产生过多的日志行,这会污染调试器的输出。从
ERROR级别开始通常是合适的。你可能想启用一些具有更高详细度的特定日志记录器,你可以使用--log-handler命令行选项来实现(参考生成服务器日志以帮助调试方法)。 -
使用
--workers=0运行服务器以避免任何可能导致两个不同进程两次达到相同断点的多进程问题。 -
使用
--max-cron-threads=0运行服务器以禁用ir.cron周期性任务的处理,否则在逐步执行方法时可能会触发,这会产生不想要的日志和副作用。
步骤 3到8使用几个pdb命令来逐步执行方法的执行。以下是pdb的主要命令的摘要。其中大部分也可以使用首字母作为快捷键。我们在以下列表中通过在括号中包含可选字母来表示这一点:
-
h(elp): 这将显示pdb命令的帮助信息。 -
a(rgs): 这显示了当前函数/方法的参数值。 -
l(ist): 这将以 11 行为单位分块显示正在执行的源代码,最初集中在当前行。连续调用将移动到源代码文件的更远位置。你可以选择性地在开始和结束处传递两个整数,以指定要显示的区域。 -
p: 这将打印一个变量。 -
pp: 这将美化打印一个变量(对于列表和字典很有用)。 -
w(here): 这显示了调用堆栈,当前行在底部,Python 解释器在顶部。 -
u(p): 这将在调用堆栈中向上移动一级。 -
d(own): 这将在调用堆栈中向下移动一级。 -
n(ext): 这将执行当前代码行,然后停止。 -
s(tep): 这是为了进入方法调用的执行。 -
r(eturn): 这将恢复当前方法的执行,直到它返回。 -
c(ont(inue)): 这将恢复程序的执行,直到遇到下一个断点。 -
b(reak) <args>: 这将创建一个新的断点并显示其标识符;args可以是以下之一:-
<empty>: 这将列出所有断点。 -
line_number: 这将在当前文件中指定的行处中断。 -
filename:line_number: 这将在指定的文件中指定的行处中断(该文件将在sys.path的目录中搜索)。 -
function_name: 这将在指定函数的第一行处中断。 -
tbreak <args>: 这与break类似,但断点在达到后将被取消,因此后续执行该行不会触发它两次。 -
disable bp_id: 这通过 ID 禁用断点。 -
enable bl_id: 这通过 ID 启用已禁用的断点。
-
-
j(ump) lineno: 下一条要执行的行将是指定的行。这可以用来重新运行或跳过某些行。 -
(!) statement: 这将执行一个 Python 语句。如果命令看起来不像pdb命令,则可以省略!字符。例如,如果你想设置名为a的变量的值,因为a是args命令的快捷方式,你需要它。
还有更多...
在配方中,我们插入了一个pdb.set_trace()语句来中断到pdb进行调试。我们也可以直接从 Odoo shell 中启动pdb,这在无法使用pdb.runcall()轻松修改项目代码时非常有用。这个函数将方法作为第一个参数,将传递给函数的参数作为下一个参数。因此,在 Odoo shell 中,你将执行以下操作:
>>> import pdb
>>> product = env['product.product']
>>> location_stock = env.ref('stock.stock_location_stock')
>>> pdb.runcall(product.export_stock_level, location_stock)
> /home/cookbook/stock_level/models.py(16)export_stock_level()
-> products = self.with_context((Pdb)
在这个配方中,我们专注于 Python 标准库中的 Python 调试器pdb。了解这个工具非常有用,因为它保证在任何 Python 发行版中都可用。还有其他 Python 调试器可用,如ipdb(pypi.python.org/pypi/ipdb)和pudb(pypi.python.org/pypi/pudb),它们可以用作pdb的替代品。它们共享相同的 API,并且在这个配方中看到的绝大多数命令都没有改变。当然,如果你使用 Python IDE 为 Odoo 开发,你将能够访问与之集成的调试器。
相关内容
如果你想了解更多关于pdb调试器的信息,请参阅pdb的完整文档,网址为docs.python.org/3.9/library/pdb.html。
理解调试模式选项
在第一章“安装 Odoo 开发环境”中,我们看到了如何在 Odoo 中启用调试/开发者选项。这些选项在调试中非常有用,并揭示了更多技术信息。在本配方中,我们将详细查看这些选项。
如何操作...
检查第一章中“激活 Odoo 开发者工具”配方,安装 Odoo 开发环境,并激活开发者模式。激活开发者模式后,你将在顶部栏看到一个带有 bug 图标的下拉菜单,如图所示:

图 7.1 – 激活调试模式后的可用选项
在这个菜单中,你会看到各种选项。尝试它们以查看它们的作用。下一节将更详细地解释这些选项。
它是如何工作的...
让我们更深入地了解以下选项:
- 运行 JS 测试:此选项将带您转到 JavaScript QUnit 测试用例页面,如图下所示。它将逐个运行所有测试用例。在此,您可以查看测试用例的进度和状态。在第十八章,自动化测试用例中,我们将了解如何创建我们自己的 QUnit JavaScript 测试用例:

图 7.2 – QUnit 测试用例结果屏幕
-
运行 JS 移动测试:与前面的选项类似,但此选项为移动环境运行 QUnit 测试用例。
-
运行任何地方点击测试:此选项将逐个点击所有菜单。它将在所有视图和搜索过滤器中进行点击。如果出现问题或出现任何回归,它将显示回溯。要停止此测试,您需要重新加载页面。
-
打开视图:此选项将打开所有可用视图的列表。通过选择其中任何一个,您可以在不定义任何菜单或操作的情况下打开该视图。
-
禁用导游:Odoo 使用导游来改善新用户的入门体验。如果您想禁用它,您可以使用此选项。
-
开始导游:Odoo 还使用导游进行自动化测试。我们将在第十五章,Web 客户端开发中创建一个自定义入门导游。此选项将打开一个包含所有导游列表的对话框,如图下所示。通过点击导游旁边的播放按钮,Odoo 将自动执行导游的所有步骤:

图 7.3 – 手动启动导游的对话框
-
编辑操作:在第三章,创建 Odoo 附加模块的添加菜单项和视图配方中,我们添加了一个菜单项和一个操作来在 Odoo 中打开视图。这些操作的详细信息也存储在数据库中作为记录。此选项将打开我们打开以显示当前视图的操作的记录详情。
-
hostel.hostel模型,此选项将显示hostel.hostel模型字段的列表。 -
管理过滤器:在 Odoo 中,用户可以从搜索视图中创建自定义过滤器。此选项将打开当前模型的自定义过滤器列表。在此,您可以修改自定义过滤器。
-
技术翻译:此选项将打开当前模型翻译术语的列表。您可以从这里修改您模型的翻译术语。您可以参考第十一章,国际化以了解更多关于翻译的信息。
-
查看访问权限:此选项将显示当前模型的安全访问权限列表。
-
查看记录规则:此选项将显示当前模型的安全记录规则列表。
-
fields_view_get()方法。 -
ir.ui.view记录当前视图。此选项是动态的,它将根据当前打开的视图显示选项。这意味着如果您打开看板视图,您将获得编辑视图:看板选项,如果您打开表单视图,您将获得编辑视图:****表单选项。
重要提示
您可以从编辑视图选项修改视图定义。此更新定义将适用于当前数据库,并且当您更新模块时,这些更改将被删除。因此,最好从模块中修改视图。
-
ir.ui.view记录当前模型的搜索视图。 -
激活资产调试模式:Odoo 提供两种开发者模式:开发者模式和带资产的开发者模式。使用此选项,您可以从开发者模式切换到带资产的开发者模式。有关更多详细信息,请参阅第一章,安装 Odoo 开发环境中的激活 Odoo 开发工具配方。
-
激活测试资产调试模式:正如我们所知,Odoo 使用导游进行测试。启用此模式将在 Odoo 中加载测试资产。此选项将在开始****导游对话框中显示更多导游。
-
重新生成资产包:Odoo 通过资产包管理所有 CSS 和 JavaScript。此选项删除旧的 JavaScript 和 CSS 资产并生成新的。当您遇到由于资产缓存引起的问题时,此选项非常有用。我们将在第十四章,CMS 网站开发中了解更多关于资产包的信息。
-
成为超级用户:这是从版本 12 开始添加的新选项。通过激活此选项,您将切换到超级用户。即使您没有访问权限,您也可以访问记录。此选项并非对所有用户都可用;它仅适用于具有管理:设置访问权限的用户。激活此模式后,您将看到一条带条纹的顶部菜单,如图所示:


图 7.4 – 激活超级用户后的菜单
- 离开开发者工具:此选项允许您离开开发者模式。
我们已经看到了调试菜单下所有可用的选项。这些选项可以用于多种方式,例如调试、测试和修复问题。它们还可以用于探索视图的源代码。
第八章:高级服务器端开发技术
在第五章,基本服务器端开发中,你学习了如何为模型类编写方法,如何扩展继承模型的方法,以及如何处理记录集。本章将涉及更高级的主题,例如处理记录集的环境、在按钮点击时调用方法,以及处理onchange方法。本章中的食谱将帮助你管理更复杂的企业问题。你将学习如何通过结合视觉元素和阐明在 Odoo 应用程序开发过程中创建交互式功能的过程来建立理解。
在本章中,我们将探讨以下食谱:
-
更改执行操作的当前用户
-
以修改后的上下文调用方法
-
执行原始 SQL 查询
-
编写向导以引导用户
-
定义
onchange方法 -
在服务器端调用
onchange方法 -
使用
compute方法定义onchange -
基于 SQL 视图定义模型
-
添加自定义设置选项
-
实现
init钩子
技术要求
对于本章,你需要 Odoo 在线平台。
本章中使用的所有代码都可以从本书的 GitHub 仓库中下载,网址为github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter08。
更改执行操作的当前用户
在编写业务逻辑代码时,你可能需要以不同的安全上下文执行一些操作。一个典型的例子是使用superuser权限执行操作,绕过安全检查。当业务需求需要操作用户没有安全访问权限的记录时,就会出现这种需求。
这个食谱将向你展示如何通过使用sudo()允许普通用户创建room记录。简单来说,我们将允许用户自己创建room,即使他们没有创建或分配room记录的权限。
准备工作
为了更容易理解,我们将添加一个新的模型来管理宿舍房间。我们将添加一个名为hostel.student的新模型。你可以参考以下定义来添加此模型:
class HostelStudent(models.Model):
_name = "hostel.student"
_description = "Hostel Student Information"
name = fields.Char("Student Name")
gender = fields.Selection([("male", "Male"),
("female", "Female"), ("other", "Other")],
string="Gender", help="Student gender")
active = fields.Boolean("Active", default=True,
help="Activate/Deactivate hostel record")
hostel_id = fields.Many2one("hostel.hostel", "hostel", help="Name of hostel")
room_id = fields.Many2one("hostel.room", "Room",
help="Select hostel room")
status = fields.Selection([("draft", "Draft"),
("reservation", "Reservation"), ("pending", "Pending"),
("paid", "Done"),("discharge", "Discharge"), ("cancel", "Cancel")],
string="Status", copy=False, default="draft",
help="State of the student hostel")
admission_date = fields.Date("Admission Date",
help="Date of admission in hostel",
default=fields.Datetime.today)
discharge_date = fields.Date("Discharge Date",
help="Date on which student discharge")
duration = fields.Integer("Duration", compute="_compute_check_duration", inverse="_inverse_duration",
help="Enter duration of living")
你需要添加一个表单视图、一个动作和一个菜单项,以便从用户界面中查看这个新模型。你还需要为宿舍添加安全规则,以便它们可以发布宿舍学生。如果你不知道如何添加这些内容,请参阅第三章,创建 Odoo 附加模块。
或者,您可以使用我们从 GitHub 代码示例中提供的现成初始模块来节省时间。此模块位于Chapter08/00_initial_module文件夹中。GitHub 代码示例可在github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter08/00_initial_module找到。
如何操作...
如果你已经测试了该模块,你会发现只有具有hostel.room访问权限的用户才能将房间标记为管理员。非宿舍用户不能自己创建房间;他们需要请求管理员用户:
- 此用户具有宿舍管理员访问权限,这意味着他们可以创建宿舍 房间记录:

图 8.1 – 此用户具有宿舍管理员访问权限
如以下截图所示,宿舍管理员也可以创建房间记录:

图 8.2 – 宿舍管理员可以创建房间记录
- 此用户具有宿舍用户访问权限:

图 8.3 – 此用户具有宿舍用户访问权限
他们只能看到宿舍 房间记录:

图 8.4 – 宿舍用户只能看到宿舍房间记录
假设我们想要添加一个新功能,以便非宿舍用户可以自己创建房间。我们将这样做,而不会给他们hostel.room模型的访问权限。
因此,让我们学习如何让普通宿舍用户学生。
-
将
action_assign_room()方法添加到hostel.room模型中:class HostelStudent(models.Model): _name = "hostel.student" ... def action_assign_room(self): -
在方法中,确保我们正在对一个单一记录进行操作:
self.ensure_one() -
如果学生未付款,则发出警告(确保您已在顶部导入
UserError):if self.status != "paid": raise UserError(_("You can't assign a room if it's not paid.")) -
以超级用户身份获取
hostel.room的空记录集:room_as_superuser = self.env['hostel.room'].sudo() -
创建一个带有适当值的
room记录:room_rec = room_as_superuser.create({ "name": "Room A-103", "room_no": "A-103", "floor_no": 1, "room_category_id": self.env.ref("my_hostel.single_room_categ").id, "hostel_id": self.hostel_id.id, }) -
要从用户界面触发此方法,请将按钮添加到学生表单视图中:
<button name="action_assign_room" string="Assign Room" type="object" class="btn-primary" /> -
重新启动服务器并将
my_hostel更新以应用给定更改。更新后,你将在学生表单视图中看到分配房间按钮,如图所示:

图 8.5 – 学生表单视图中的分配房间按钮
当你点击它时,将创建一个新的房间记录。这对非宿舍用户也适用。你可以通过以演示用户身份访问 Odoo 来测试这一点。
工作原理...
在前三个步骤中,我们添加了一个名为action_assign_room()的新方法。当用户在学生表单视图中点击分配房间按钮时,将调用此方法。
在步骤 4中,我们使用了 sudo()。此方法返回一个新的记录集,其中包含修改后的 environment,用户在该环境中拥有 superuser 权限。当使用 sudo() 调用 recordset 时,环境将修改 environment 属性为 su,这表示环境的 superuser 状态。您可以通过 recordset.env.su 访问其状态。所有通过此 sudo 记录集进行的调用都带有超级用户权限。为了更好地理解这一点,请从方法中移除 .sudo(),然后点击 Access Error,用户将不再有权访问该模型。仅使用 sudo() 就会绕过所有安全规则。
如果您需要一个特定的用户,您可以传递一个包含该用户或该用户数据库 ID 的记录集,如下所示:
public_user = self.env.ref('base.public_user')
hostel_room = self.env['hostel.room'].with_user(public_user)
hostel_room.search([('name', 'ilike', 'Room 101')])
这段代码片段允许您使用 public 用户搜索可见的房间。
更多内容...
使用 sudo(),您可以绕过访问权限和安全记录规则。有时,您可以访问本应隔离的多个记录,例如在多公司环境中来自不同公司的记录。sudo() 记录集绕过了 Odoo 的所有安全规则。
如果您不小心,在此环境中搜索的记录可能会与数据库中存在的任何公司相关联,这意味着您可能会向用户泄露信息;更糟糕的是,您可能会通过将属于不同公司的记录链接起来,在不知不觉中破坏数据库。
当使用 sudo() 时,请谨慎行事,以避免意外后果,例如无意中将不同公司的记录链接起来。确保适当的数据隔离,并在绕过访问权限之前考虑对数据完整性和安全规则可能产生的影响。
重要提示
当使用 sudo() 时,始终要仔细检查您的 search() 调用是否依赖于标准记录规则来过滤结果。
不使用 sudo(),search() 调用将尊重标准记录规则,可能会根据用户权限限制对记录的访问。这可能导致搜索结果不完整或不准确,影响数据可见性和应用程序功能。
参考以下内容
查阅以下参考资料以获取更多信息:
-
如果您想了解更多关于环境的信息,请参阅第五章,基本 服务器端开发中的为模型获取空记录集配方。
-
有关访问控制列表和记录规则的更多信息,请参阅第十章,安全访问
调用一个修改过的上下文的方法
context 是记录集环境的一部分。它用于传递额外的信息,例如用户的时区和语言,从用户界面传递。你也可以使用上下文来传递动作中指定的参数。标准 Odoo 扩展中的几个方法使用上下文根据这些上下文值调整其业务逻辑。有时需要修改 recordset 中的上下文值,以从方法调用中获得所需的结果或计算字段的所需值。
这个菜谱将向你展示如何根据环境上下文中的值更改方法的行为。
准备工作
对于这个菜谱,我们将使用之前菜谱中的 my_hostel 模块。在 hostel.room 模型的表单视图中,我们将添加一个按钮来移除房间成员。如果宿舍的普通居民未经许可或授权从其分配的房间中移除其他居住者,可能会在住宿中造成混乱和问题。请注意,我们已经在房间表单视图中有了相同的按钮,但在这里,我们将探讨 Odoo 中的上下文使用,深入了解它如何影响系统操作和结果。
如何操作...
要添加一个按钮,你需要执行以下步骤:
-
添加
hostel.room:<button name="action_remove_room_members" string="Remove Room Members" type="object" class="btn-primary" /> -
将
action_remove_room_members()方法添加到hostel.room模型中:def action_remove_room_members(self): ... -
将以下代码添加到方法中,以更改环境上下文并调用移除房间成员的方法:
student.with_context(is_hostel_room=True).action_remove_room() -
更新
hostel.student模型的action_remove_room()方法,以便展示不同的行为:def action_remove_room(self): if self.env.context.get("is_hostel_room"): self.room_id = False
它是如何工作的…
在 Odoo 中,为了修改受上下文影响的操作行为,我们做了以下操作:
-
确定了目标行为。
-
定义了上下文参数。
-
修改了相关的代码部分。
-
仔细测试了更改。
-
确保模块间的兼容性。
在 步骤 1 中,我们移除了房间成员。
在 步骤 2 中,我们添加了一个新的按钮,移除房间成员。用户将使用此按钮来 移除 成员。
在 步骤 3 和 步骤 4 中,我们添加了一个方法,当用户点击 移除房间成员 按钮时会被调用。
在 步骤 5 中,我们使用一些关键字参数调用了 student.with_context()。这返回了一个带有更新上下文的新版本的 room_id 记录集。我们在这里添加了一个键到上下文中,is_hostel_room=True,但如果你想的话,可以添加多个键。在这里我们使用了 sudo()。
在 步骤 6 中,我们检查了上下文中 is_hostel_room 键的值是否为正。
现在,当宿舍房间在学生表单视图中移除房间成员时,room 记录集为 False。
这只是一个修改过的上下文示例,但你可以使用任何方法,例如 create()、write()、unlink() 等。你也可以根据需求创建任何自定义方法。
更多...
也可以向with_context()传递一个字典。在这种情况下,该字典用作新的上下文,它将覆盖当前的上下文。因此,步骤 5也可以写成以下形式:
new_context = self.env.context.copy()
new_context.update({'is_hostel_room': True})
student.with_context(new_context)
参考信息
参考以下配方来了解 Odoo 中上下文的更多信息:
-
在第五章的获取空记录集的模型配方中,基本服务器端开发解释了环境是什么
-
在第九章的传递参数到表单和动作 – 上下文配方中,后端视图解释了如何在动作定义中修改上下文
-
在第五章的搜索记录配方中,基本服务器端开发解释了活动记录
执行原始 SQL 查询
大多数情况下,你可以通过使用 Odoo 的 ORM 来执行你想要的操作——例如,你可以使用search()方法来获取记录。然而,有时你需要更多;要么你不能使用域语法来表达你想要的(对于某些操作来说可能很棘手,甚至根本不可能),或者你的查询需要多次调用search(),这最终变得效率低下。
此配方展示了如何使用原始 SQL 查询来获取用户在特定房间中保存的名称和数量。
准备工作
对于这个配方,我们将使用前一个配方中的my_hostel模块。为了简单起见,我们将在日志中打印结果,但在实际场景中,你需要在你的业务逻辑中使用查询结果。在第九章的后端视图中,我们将显示此查询的结果。
如何做到这一点...
要获取用户在特定房间中保存的名称和数量信息,你需要执行以下步骤:
-
将
action_category_with_amount()方法添加到hostel.room:def action_category_with_amount(self): ... -
在方法中,编写以下 SQL 查询:
""" SELECT hrc.name, hrc.amount FROM hostel_room AS hostel_room JOIN hostel_room_category as hrc ON hrc.id = hostel_room.room_category_id WHERE hostel_room.room_category_id = %(cate_id)s;""", {'cate_id': self.room_category_id.id} -
执行查询:
self.env.cr.execute(""" SELECT hrc.name, hrc.amount FROM hostel_room AS hostel_room JOIN hostel_room_category as hrc ON hrc.id = hostel_room.room_category_id WHERE hostel_room.room_category_id = %(cate_id)s;""", {'cate_id': self.room_category_id.id}) -
获取结果并记录它(确保你已经导入了
logger):result = self.env.cr.fetchall() _logger.warning("Hostel Room With Amount: %s", result) -
在
hostel.room模式的表单视图中添加一个按钮来触发我们的方法:<button name="action_category_with_amount" string="Log Category With Amount" type="object" class="btn-primary"/>
不要忘记在此文件中导入logger。然后,重新启动并更新my_hostel模块。
它是如何工作的...
在步骤 1中,我们添加了action_category_with_amount()方法,当用户点击带有金额的日志类别按钮时将被调用。
在步骤 2中,我们声明了一个 SQL SELECT查询。这将返回表示宿舍房间中数量的类别。如果你在 PostgreSQL CLI 中运行此查询,你将根据你的房间数据得到一个结果。以下是基于我的数据库的示例数据:
+---------------------------------------+-------+
| name | amount|
|---------------------------------------+-------|
| Single Room | 3000 |
+---------------------------------------+-------+
在步骤 4中,我们在self.env.cr中存储的数据库游标上调用execute()方法。这将查询发送到 PostgreSQL 并执行它。
在 步骤 5 中,我们使用了游标的 fetchall() 方法来检索查询选择的行列表。此方法返回行列表。在我的情况下,这是 [('Single Room', 3000)]。从我们执行的查询形式来看,我们知道每一行将恰好有两个值,第一个是 name,另一个是用户在特定房间中的金额。然后,我们简单地记录下来。
在 步骤 6 中,我们添加了一个 添加 按钮来处理用户操作。
重要提示
如果你正在执行 UPDATE 查询,你需要手动使缓存无效,因为 Odoo ORM 的缓存不知道你用 UPDATE 查询所做的更改。要使缓存无效,你可以使用 self.invalidate_cache()。
还有更多...
self.env.cr 中的对象是围绕 psycopg2 游标的一个薄包装。以下是你大部分时间会想使用的方法:
execute(query, params): 这将使用在查询中标记为%s的参数执行 SQL 查询,其中params是一个元组
警告
永远不要自己进行替换;始终使用格式化选项,如 %s。如果你使用字符串连接等技术,可能会使代码容易受到 SQL 注入攻击。
-
fetchone(): 这将返回数据库中的一行,封装在元组中(即使查询只选择了一个列) -
fetchall(): 这将返回数据库中的所有行,作为一个元组的列表 -
dictfetchall(): 这将返回数据库中的所有行,作为一个字典列表,映射列名到值
在处理原始 SQL 查询时要非常小心:
-
你正在绕过应用程序的所有安全性。确保你使用任何你检索的 ID 列表调用
search([('id', 'in', tuple(ids)])来过滤掉用户无权访问的记录。 -
你所做的任何修改都会绕过附加模块设置的约束,除了
NOT NULL、UNIQUE和FOREIGN KEY约束,这些约束在数据库级别强制执行。这也适用于任何计算字段重新计算触发器,因此你可能会损坏数据库。 -
避免使用
INSERT/UPDATE查询 – 通过查询插入或更新记录不会运行通过重写create()和write()方法编写的任何业务逻辑。它不会更新存储的计算字段,并且也会绕过 ORM 约束。
参见
对于访问权限管理,请参阅 第十章,安全访问。
编写向导以引导用户
在 第四章 的 使用抽象模型实现可重用模型功能 菜谱中,介绍了 models.TransientModel 基类。这个类与普通模型有很多共同之处,除了瞬态模型的记录在数据库中定期清理,因此得名瞬态。这些用于创建向导或对话框,用户在用户界面中填写,通常用于对数据库的持久记录执行操作。
准备工作
对于这个菜谱,我们将使用之前菜谱中的 my_hostel 模块。这个菜谱将添加一个新的向导。使用这个向导,用户将被分配房间。
如何做到这一点...
按照以下步骤添加一个新的向导以更新分配房间和宿舍记录:
-
使用以下定义向模块添加一个新的瞬态模型:
class AssignRoomStudentWizard(models.TransientModel): _name = 'assign.room.student.wizard' room_id = fields.Many2one("hostel.room", "Room", required=True) -
添加执行在瞬态模型上操作的
callback方法。将以下代码添加到AssignRoomStudentWizard类中:def add_room_in_student(self): hostel_room_student = self.env['hostel.student'].browse( self.env.context.get('active_id')) if hostel_room_student: hostel_room_student.update({ 'hostel_id': self.room_id.hostel_id.id, 'room_id': self.room_id.id, 'admission_date': datetime.today(), }) -
为模型创建一个表单视图。将以下视图定义添加到模块视图:
<record id='assign_room_student_wizard_form' model='ir.ui.view'> <field name='name'>assign room student wizard form view</field> <field name='model'>assign.room.student.wizard</field> <field name='arch' type='xml'> <form string="Assign Room"> <sheet> <group> <field name='room_id'/> </group> </sheet> <footer> <button string='Update' name='add_room_in_student' class='btn-primary' type='object'/> <button string='Cancel' class='btn-default' special='cancel'/> </footer> </form> </field> </record> -
创建一个动作和一个菜单项以显示向导。将以下声明添加到模块菜单文件中:
<record model="ir.actions.act_window" id="action_assign_room_student_wizard"> <field name="name">Assign Room</field> <field name="res_model">assign.room.student.wizard</field> <field name="view_mode">form</field> <field name="target">new</field> </record> -
在
ir.model.access.csv文件中为assign.room.student.wizard添加访问权限:access_assign_room_student_wizard_manager,access.assign.room.student.wizard.manager,model_assign_room_student_wizard,my_hostel.group_hostel_manager,1,1,1,1 -
将
my_hostel模块更新以应用更改。
它是如何工作的...
在 第一步 中,我们定义了一个新的模型。它与其他模型没有区别,除了基类是 TransientModel 而不是 Model。TransientModel 和 Model 都有一个共同的基类,称为 BaseModel,如果您检查 Odoo 的源代码,您会看到 99% 的工作都在 BaseModel 中,而 Model 和 TransientModel 几乎都是空的。
对于 TransientModel 记录,唯一发生变化的是以下内容:
-
记录会定期从数据库中删除,以便瞬态模型的表不会随着时间的推移而增长
-
您不允许在指向普通模型的
TransientModel实例上定义one2many字段,因为这将在持久模型上添加一个指向瞬态数据的列
在此情况下使用 many2many 关系。当然,如果 one2many 中的相关模型也是 TransientModel,您也可以使用 one2many 字段。
我们在模型中定义一个字段用于存储房间。我们可以添加其他标量字段,以便我们可以记录一个计划返回日期,例如。
在 第二步 中,我们向向导类中添加了代码,当在 第三步 中点击定义的按钮时将被调用。此代码读取向导的值并更新 hostel.student 记录。
在步骤 3中,我们为我们的向导定义了一个视图。有关详细信息,请参阅第九章中的文档式表单食谱,后端视图。这里的重要点是页脚中的按钮;type属性设置为'object',这意味着当用户点击按钮时,将调用按钮的name属性指定的方法。
在步骤 4中,我们确保在我们的应用程序菜单中有一个向导的入口点。我们在动作中使用target='new',这样表单视图就会以对话框的形式显示在当前表单之上。有关详细信息,请参阅第九章中的添加菜单项和窗口动作食谱,后端视图:

图 8.6 – 为学生分配房间的向导
在步骤 5中,我们为assign.room.student.wizard模型添加了访问权限。有了这个,管理员用户将获得对assign.room.student.wizard模型的完全权限。
注意
在 Odoo v14 之前,TransientModel不需要任何访问规则。任何人都可以创建记录,并且他们只能访问自己创建的记录。从 Odoo v14 开始,TransientModel需要访问权限。
还有更多...
这里有一些提高你的向导的技巧。
使用上下文来计算默认值
我们展示的向导需要用户在表单中填写成员的名称。我们可以使用 Web 客户端的一个功能来节省一些输入。当执行动作时,context会更新一些可以由向导使用的值:
-
active_model:这是与动作相关的模型名称。这通常是屏幕上显示的模型。 -
active_id:这表示有一个单个记录处于活动状态,并提供该记录的 ID。 -
active_ids:如果选择了多个记录,这将是一个包含 ID 的列表。当在树视图中选择多个项目并触发动作时会发生这种情况。在表单视图中,你得到[active_id]。 -
active_domain:这是向导将操作的一个附加域。
这些值可以用来计算模型的默认值,甚至可以直接用于按钮调用的方法中。为了改进本食谱中的示例,如果我们有一个在hostel.room模型表单视图中显示的按钮来启动向导,那么向导创建的上下文将包含{'active_model': 'hostel.room', 'active_id': <hostel_room_id>}。在这种情况下,你可以定义room_id字段来获取以下方法计算出的默认值:
def _default_member(self):
if self.context.get('active_model') == 'hostel.room':
return self.context.get('active_id', False)
向导和代码复用
在步骤 2中,我们可以在方法的开头添加self.ensure_one(),如下所示:
def add_room_in_student(self):
hostel_room_student = self.env['hostel.student'].browse(
self.env.context.get('active_id'))
if hostel_room_student:
hostel_room_student.update({
'hostel_id': self.room_id.hostel_id.id,
'room_id': self.room_id.id,
'admission_date': datetime.today(),
})
我们建议在此食谱中使用 v17。它将允许我们通过为向导创建记录并将它们放入单个记录集中来重用向导的其他部分(有关如何做到这一点,请参阅 第五章 中的 结合记录集 食谱,基本服务器端开发),然后在记录集上调用 add_room_in_student()。在这里,代码很简单,你不需要跳过所有那些环来记录不同学生分配了哪些房间。然而,在 Odoo 实例中,某些操作要复杂得多,有一个能够正确执行操作的向导总是很方便。当使用这些向导时,请确保检查源代码中任何可能的 active_model/active_id/active_ids 键的上下文使用。如果是这种情况,您需要传递一个自定义上下文(请参阅 使用修改后的上下文调用方法 食谱)。
重定向用户
步骤 2 中的方法不返回任何内容。这将在执行操作后关闭向导对话框。另一种可能性是让该方法返回一个包含 ir.action 字段的字典。在这种情况下,Web 客户端将处理操作,就像用户点击了菜单项一样。可以在 BaseModel 类中定义的 get_formview_action() 方法用来实现这一点。例如,如果我们想显示宿舍房间的表单视图,我们可以编写以下代码:
def add_room_in_student(self):
hostel_room_student = self.env['hostel.student'].browse(
self.env.context.get('active_id'))
if hostel_room_student:
hostel_room_student.update({
'hostel_id': self.room_id.hostel_id.id,
'room_id': self.room_id.id,
'admission_date': datetime.today(),
})
rooms = self.mapped('room_id')
action = rooms.get_formview_action()
if len(rooms.ids) > 1:
action['domain'] = [('id', 'in', tuple(rooms.ids))]
action['view_mode'] = 'tree,form'
return action
这将构建一个包含来自此向导的房间列表(实际上,当从用户界面调用向导时,将只有一个这样的房间)并创建一个动态操作,显示具有指定 ID 的房间。
重定向用户 技术可用于创建必须依次执行多个步骤的向导。向导中的每个步骤都可以通过提供一个 下一步 按钮来使用前一个步骤的值。这将调用向导上定义的方法,更新向导上的某些字段,返回一个将重新显示相同更新后的向导的操作,并为下一个步骤做好准备。
参考以下内容
请参考以下食谱以获取更多详细信息:
-
有关为向导定义视图的更多详细信息,请参阅 第九章 中的 文档样式表单 食谱,后端视图。
-
要了解有关视图和调用服务器端方法的更多信息,请参阅 第九章 中的 添加菜单项和窗口操作 食谱,后端视图。
-
有关为向导创建记录并将它们放入单个记录集中的更多详细信息,请参阅 第五章 中的 结合记录集 食谱,基本服务器端开发。
定义 on_change 方法
在编写业务逻辑时,通常某些字段是相互关联的。我们在第四章的向模型添加约束验证配方中探讨了如何指定字段之间的约束,应用模型。这个配方展示了一个稍微不同的概念。在这里,当用户界面中的字段被修改时,会调用onchange方法来更新客户端记录中其他字段的值,通常是在表单视图中。
我们将通过提供一个类似于在编写一个向导以引导用户配方中定义的向导来演示这一点,但可以用来记录持续时间返回。当在表单视图中设置日期时,学生的持续时间将被更新。虽然我们在Model上演示了onchange方法,但这些功能也适用于正常的Transient模型。
准备工作
对于这个配方,我们将使用本章编写一个表单以引导用户配方中的my_hostel模块。我们将创建一个宿舍学生并添加一个onchange方法,当用户选择出院日期或入院日期字段时,将自动填充持续时间。
您还希望通过定义以下模型来准备您的工作:
class HostelStudent(models.Model):
_name = "hostel.student"
_description = "Hostel Student Information"
admission_date = fields.Date("Admission Date",
help="Date of admission in hostel",
default=fields.Datetime.today)
discharge_date = fields.Date("Discharge Date",
help="Date on which student discharge")
duration = fields.Integer("Duration", inverse="_inverse_duration",help="Enter duration of living")
最后,您需要定义一个视图。这些步骤将留作练习,由您来完成。
如何操作...
为了在用户更改时自动填充返回的持续时间,您需要在HostelStudent步骤中添加一个onchange方法,其定义如下:
@api.onchange('admission_date', 'discharge_date')
def onchange_duration(self):
if self.discharge_date and self.admission_date:
self.duration = (self.discharge_date.year - \
self.admission_date.year) * 12 + \
(self.discharge_date.month - \
self.admission_date.month)
它是如何工作的...
onchange方法使用@api.onchange装饰器,它传递了更改的字段名称,因此将触发对该方法的调用。在我们的情况下,我们说每当用户界面中的admission_date或discharge_date被修改时,必须调用该方法。
在方法体内,我们计算了持续时间,并使用属性赋值来更新表单视图的duration属性。
更多...
正如我们在本配方中看到的,onchange方法的基本用途是在用户界面中更改某些其他字段时为字段计算新值。
在方法体内,您可以访问当前记录视图中的字段,但不一定是模型的所有字段。这是因为onchange方法可以在记录在用户界面中创建并在数据库中存储之前被调用!在onchange方法内部,self处于特殊状态,这由self.id不是整数,而是一个odoo.models.NewId实例的事实表示。因此,您不得在onchange方法中对数据库进行任何更改,因为用户最终可能会取消记录的创建,这将不会回滚在编辑过程中由onchange方法所做的任何更改。
在服务器端调用onchange方法
onchange方法有一个限制:当你执行服务器端操作时,它不会被调用。onchange仅在通过 Odoo 用户界面执行相关操作时自动调用。然而,在几种情况下,必须调用这些onchange方法,因为它们更新了创建或更新记录中的重要字段。当然,你可以自己进行所需的计算,但这并不总是可能的,因为onchange方法可以被安装在你不知道的实例上的第三方附加模块添加或修改。
这个配方解释了如何通过手动调用在创建记录之前调用onchange方法来对一个记录调用onchange方法。
准备工作
在更改执行动作的用户配方中,我们添加了一个返回房间按钮,以便用户可以自行更新房间和宿舍。现在我们想要为返回房间和宿舍做同样的事情;我们只需使用分配房间返回向导。
如何操作...
在这个配方中,我们将手动更新hostel.room模型的记录。为此,你需要执行以下步骤:
-
从
hostel.student.py文件中的tests实用程序导入Form:from odoo.tests.common import Form -
在
hostel.room模型中创建return_room方法:def return_room(self): self.ensure_one() -
获取
assign.room.student.wizard的空记录集:wizard = self.env['assign.room.student.wizard'] -
创建一个向导
Form块,如下所示:with Form(wizard) as return_form: -
通过分配房间并返回更新后的
room_id值来触发onchange:return_form.room_id = self.env.ref('my_hostel.101_room') record = return_form.save() record.with_context(active_id=self.id).add_room_in_student()
它是如何工作的...
关于步骤 1到步骤 3的解释,请参考第五章中的创建新记录配方,基本 服务器端开发。
在步骤 4中,我们创建了一个虚拟表单来处理 onchange 规范,例如 GUI。
步骤 5包含了返回房间和宿舍的完整逻辑。在第一行,我们在向导中分配了room_id。然后,我们调用了表单的save()方法,它返回了一个向导记录。之后,我们调用了add_room_in_student()方法来执行返回更新后的房间和宿舍的逻辑。
onchange方法通常从用户界面调用。但在这个配方中,我们学习了如何在服务器端使用/触发onchange方法的业务逻辑。这样,我们可以在不绕过任何业务逻辑的情况下创建记录。
参见
如果你想要了解更多关于创建和更新记录的信息,请参考第五章中的创建新记录和更新记录集记录的值配方,基本 服务器端开发。
使用计算方法定义onchange
在最后两个配方中,我们看到了如何定义和调用onchange方法。我们还看到了它的局限性,即它只能从用户界面自动调用。为了解决这个问题,Odoo v13 引入了一种新的定义onchange行为的方法。在这个配方中,我们将学习如何使用compute方法产生类似于onchange方法的行为。
准备工作
对于这个配方,我们将使用之前配方中的my_hostel模块。我们将用compute方法替换hostel.student的onchange方法。
如何操作...
按照以下步骤修改onchange方法以使用compute方法:
-
在
onchange_duration()方法中将api.onchange替换为depends,如下所示:@api.depends('admission_date', 'discharge_date') def onchange_duration(self): ... -
在字段的定义中添加
compute参数,如下所示:duration = fields.Integer("Duration", compute="onchange_duration", inverse="_inverse_duration", help="Enter duration of living")
升级my_hostel模块以应用代码,然后测试返回持续时间表以查看更改。
它是如何工作的...
功能上,我们的计算onchange与正常的onchange方法类似。唯一的区别是现在,onchange也会在后台更改时触发。
在步骤 1中,我们将@api.onchange替换为@api.depends。这是在字段值发生变化时重新计算方法所必需的。
在步骤 2中,我们将compute方法与字段注册。如您所注意到的,我们在compute字段定义中使用了readonly=False。默认情况下,compute方法是只读的,但通过设置readonly=False,我们确保字段是可编辑的并且可以被存储。
参考以下内容
要了解更多关于计算字段的信息,请参阅第四章中“向模型添加计算字段”的配方,应用模型。
基于 SQL 视图定义模型
当在设计一个附加模块时,我们使用类来建模数据,然后通过 Odoo 的 ORM 将它们映射到数据库表中。我们应用一些众所周知的设计原则,例如关注点分离和数据规范化。然而,在模块设计的后期阶段,从多个模型中聚合数据到一个单独的表,并在它们上面执行一些操作可能是有用的,特别是对于报告或生成仪表板。为了使这更容易,并利用 Odoo 底层PostgreSQL数据库引擎的全部功能,可以定义一个基于 PostgreSQL 视图的只读模型,而不是表。
在这个配方中,我们将重用本章中“编写向导以引导用户”配方中的房间模型,并创建一个新的模型以更容易地收集关于房间和作者的可访问性信息。
准备工作
对于这个配方,我们将使用之前配方中的my_hostel模块。我们将创建一个新的模型,名为hostel.room.availability,用于存储可用性数据。
如何操作...
要创建一个基于 PostgreSQL 视图的新模型,请按照以下步骤操作:
-
创建一个新的模型,将
_auto类属性设置为False:class HostelRoomAvailability(models.Model): _name = 'hostel.room.availability' _auto = False -
声明您希望在模型中看到的字段,并将它们设置为
readonly:room_id = fields.Many2one('hostel.room', 'Room', readonly=True) student_per_room = fields.Integer(string="Student Per Rooom", readonly=True) availability = fields.Integer(string="Availability",readonly=True) amount = fields.Integer(string="Amount", readonly=True) -
定义
init()方法以创建视图:def init(self): tools.drop_view_if_exists(self.env.cr, self._table) query = """ CREATE OR REPLACE VIEW hostel_room_availability AS ( SELECT min(h_room.id) as id, h_room.id as room_id, h_room.student_per_room as student_per_room, h_room.availability as availability, h_room.rent_amount as amount FROM hostel_room AS h_room GROUP BY h_room.id ); """ self.env.cr.execute(query) -
您现在可以定义新模型的视图。交叉视图特别有用于探索数据(参考第九章,后端视图)。
-
不要忘记为新模型定义一些访问规则(查看第十章,安全访问)。
它是如何工作的...
通常,Odoo 将使用列的字段定义为您定义的模型创建一个新的表。这是因为,在BaseModel类中,_auto属性默认为True。在步骤 1中,通过将此类属性设置为False,我们告诉 Odoo 我们将自行管理。
在步骤 2中,我们定义了一些将被 Odoo 用于生成表的字段。我们注意将它们标记为readonly=True,这样视图就不会启用您无法保存的修改,因为 PostgreSQL 视图是只读的。
在步骤 3中,我们定义了init()方法。此方法通常不执行任何操作;它在_auto_init()之后被调用(当_auto = True时负责创建表,否则不执行任何操作),我们使用它来创建一个新的 SQL 视图(或在模块升级的情况下更新现有视图)。视图创建查询必须创建一个具有与模型字段名称匹配的列名称的视图。
重要提示
忘记在视图定义查询中重命名列是一个常见的错误。这将导致当 Odoo 找不到列时出现错误消息。
注意,我们还需要提供一个名为ID的整数列值,它包含唯一的值。
还有更多...
在这样的模型上也可以有一些计算和关联字段。唯一的限制是字段不能被存储(因此,您不能使用它们来分组记录或搜索)。
如果您需要按基础用户分组,您需要通过将其添加到视图定义中来存储该字段,而不是使用相关字段。
参见
要了解更多信息,请查看以下食谱:
-
要了解更多关于用户操作 UI 视图的信息,请参考第九章,后端视图。
-
为了更好地理解访问控制和记录规则,请查看第十章,安全访问。
添加自定义设置选项
在 Odoo 中,您可以通过设置选项提供可选功能。用户可以在任何时候启用或禁用此选项。我们将在这个食谱中说明如何创建设置选项。
准备工作
在之前的食谱中,我们添加了按钮,以便宿舍用户可以点击并返回房间。并非每个宿舍都是这种情况;然而,我们将从之前的食谱创建一个my_hostel模块。
如何操作...
要创建自定义设置选项,请按照以下步骤操作:
-
通过继承
res.config.settings模型添加新字段:class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' group_hostel_user = fields.Boolean(string="Hostel User", implied_group='my_hostel.group_hostel_user') -
将此字段添加到现有的
xpath(更多详情请参阅第九章,后端视图):<record id="res_config_settings_view_form" model="ir.ui.view"> <field name="name">res.config.settings.view.form.inherit.hostel</field> <field name="model">res.config.settings</field> <field name="priority" eval="5"/> <field name="inherit_id" ref="base.res_config_settings_view_form"/> <field name="arch" type="xml"> <xpath expr="//div[hasclass('settings')]" position="inside"> <div class="app_settings_block" data-string="Hostel" string="Hostel" data-key="my_hostel" groups="my_hostel.group_hostel_manager"> <h2>Hostel</h2> <div class="row mt16 o_settings_container"> <div class="col-12 col-lg-6 o_setting_box" id="hostel"> <div class="o_setting_left_pane"> <field name="group_hostel_user"/> </div> <div class="o_setting_right_pane"> <label for="group_hostel_user"/> <div class="text-muted"> Allow users to hostel user </div> </div> </div> </div> </div> </xpath> </field> </record> -
添加一些操作和菜单以进行设置:
<record id="hostel_config_settings_action" model="ir.actions.act_window"> <field name="name">Settings</field> <field name="type">ir.actions.act_window</field> <field name="res_model">res.config.settings</field> <field name="view_id" ref="res_config_settings_view_form"/> <field name="view_mode">form</field> <field name="target">inline</field> <field name="context">{'module' : 'my_hostel'}</field> </record> <menuitem name="Settings" id="hostel_setting_menu" parent="hostel_main_menu" action="hostel_config_settings_action" sequence="50"/> -
重新启动服务器并更新
my_hostel模块以应用更改,如下所示:

图 8.7 – 宿舍用户访问权限设置选项以启用和禁用此功能
它是如何工作的...
在 Odoo 中,所有设置选项都是在res.config.settings模型中添加的。res.config.settings是一个临时模型。在步骤 1中,我们创建了一个新的安全组。我们将使用此组来创建隐藏和显示按钮。
在步骤 2中,我们通过继承res.config.settings模型添加了一个新的布尔字段。我们添加了一个implied_group属性,其值为my_hostel.group_hostel_user。当管理员启用或禁用带有布尔字段的选项时,此组将被分配给所有odoo用户。
base.res_config_settings_view_form。
在步骤 3中,我们通过继承此设置将其添加到用户界面。我们使用xpath添加了我们的设置选项。我们将在第九章,后端视图中更详细地介绍这一点。在表定义中,您会发现此选项的属性数据键值将是您的模块名称。这仅在您在xpath中添加整个新标签时才需要。
在步骤 4中,我们添加了一个操作和一个菜单,以便从用户界面访问配置选项。您需要从操作传递{'module': 'my_hostel'}上下文,以便在点击菜单时默认打开my_hostel模块。
在步骤 5中,我们将my_hostel.group_hostel_user组添加到按钮中。由于这个组,宿舍用户和返回按钮将根据设置选项被隐藏或显示。
之后,您将看到一个单独的布尔字段来启用或禁用对所有odoo用户的implied_group。由于我们添加了组到按钮,如果用户有组,按钮将显示,如果没有组,按钮将隐藏。我们将在第十章,安全访问中详细探讨安全组。
还有更多...
有几种其他方法可以通过各种选项来管理安装或卸载。为此,您需要添加一个名为module_加上模块名称的布尔字段。例如,如果我们创建一个名为my_hostel_extras的新模块,您需要添加一个布尔字段,如下所示:
module_my_hostel_extras = fields.Boolean(
string='Hostel Extra Features')
当您启用或禁用此选项时,odoo将安装或卸载``my_hostel_extras模块。
另一种管理设置的方法是使用系统参数。此类数据存储在ir.config_parameter模型中。以下是如何创建系统级全局参数的方法:
digest_emails = fields.Boolean(
string="Digest Emails",
config_parameter='digest.default_digest_emails')
字段中的config_parameter属性将确保用户数据存储在digest.default_digest_emails键中。
设置选项用于使你的应用程序通用。这些选项给用户提供了自由度,并允许他们在运行时启用或禁用功能。当你将功能转换为选项时,你可以用一个模块服务更多客户,并且你的客户可以在他们喜欢的时候启用该功能。
实现 init 钩子
在第六章 管理模块数据中,你学习了如何从 XML 或 CSV 文件中添加、更新和删除记录。然而,有时业务案例很复杂,无法使用数据文件解决。在这种情况下,你可以使用清单文件中的init钩子来执行你想要的操作。
复杂的业务案例可能需要超出标准 XML 或 CSV 文件的数据动态初始化。例如,包括与外部系统集成、执行复杂计算或根据运行时条件配置记录,这些都可以通过清单文件中的init钩子来实现。
准备工作
我们将使用之前菜谱中的相同my_hostel模块。为了简单起见,在这个菜谱中,我们只需通过post_init_hook创建一些房间记录。
如何操作...
要添加post_init_hook,请按照以下步骤操作:
-
使用
post_init_hook键在__manifest__.py文件中注册钩子:... 'post_init_hook': 'add_room_hook', ... -
将
add_room_hook()方法添加到__init__.py文件中:from odoo import api, SUPERUSER_ID def add_room_hook(cr, registry): env = api.Environment(cr, SUPERUSER_ID, {}) room_data1 = {'name': 'Room 1', 'room_no': '01'} room_data2 = {'name': 'Room 2', 'room_no': '02'} env['hostel.room'].create([room_data1, room_data2])
它是如何工作的...
在步骤 1中,我们在清单文件中使用add_room_hook值注册了post_init_hook。这意味着在模块安装后,Odoo 将在__init__.py中查找add_room_hook方法。post_init_hook值接收环境作为参数,展示了在模块安装后执行的add_room_hook函数的实例。
在步骤 2中,我们声明了add_room_hook()方法,该方法将在模块安装后调用。我们从这个方法创建了两个记录。在实际场景中,你可以在那里编写复杂业务逻辑。
在这个例子中,我们看了post_init_hook,但 Odoo 支持另外两个钩子:
-
pre_init_hook:当你开始安装模块时,这个钩子将被调用。它与post_init_hook相反;它将在安装当前模块后调用:- 使用
pre_init_hook键在__manifest__.py文件中注册钩子:
... 'pre_init_hook': 'pre_init_hook_hostel', ... - 使用
- 将
pre_init_hook_hostel()方法添加到__init__.py文件中:
def pre_init_hook_hostel(env):
env['ir.model.data'].search([
('model', 'like', 'hostel.hostel'),
]).unlink()
uninstall_hook:当你卸载模块时,这个钩子将被调用。这通常用于你的模块需要垃圾回收机制时:
- 使用
uninstall_hook键在__manifest__.py文件中注册钩子:
...
'uninstall_hook': 'uninstall_hook_user',
...
- 将
uninstall_hook_user()方法添加到__init__.py文件中:
def uninstall_hook_user(env):
hostel = env['res.users'].search([])
hostel.write({'active': False})
钩子是运行在现有代码之前、之后或替代现有代码的函数。钩子——以字符串形式显示的函数——包含在 Odoo 模块的__init__.py文件中。
第九章:后端视图
在所有前面的章节中,你已经看到了 Odoo 的服务器和数据库方面。在本章中,你将看到 Odoo 的用户界面(UI)方面。你将学习如何创建不同类型的视图。除了视图之外,本章还涵盖了其他组件,如操作按钮、菜单和小部件,这些将帮助你使你的应用程序更加用户友好。完成本章后,你将能够设计 Odoo 后端的 UI。请注意,本章不涵盖 Odoo 的网站部分;我们有一个单独的章节(14)来介绍这部分内容。
在本章中,我们将涵盖以下食谱:
-
添加菜单项和窗口操作
-
通过操作打开特定视图
-
将内容和小部件添加到表视图中
-
在表单中添加按钮
-
将参数传递给表单和操作 – 上下文
-
在记录列表上定义过滤器 – 域
-
定义列表视图
-
定义搜索视图
-
添加搜索过滤器侧边栏
-
修改现有视图 – 视图继承
-
定义文档样式表单
-
使用属性动态表单元素
-
定义嵌入式视图
-
在表视图的侧边显示附件
-
定义看板视图
-
根据其状态在列中显示看板卡片
-
定义日历视图
-
定义图形视图和交叉视图
-
定义群体视图
-
定义甘特图视图
-
定义活动视图
-
定义地图视图
技术要求
在本章的整个过程中,我们将假设你有一个安装了基本附加组件的数据库和一个空的 Odoo 附加模块,你可以将食谱中的 XML 代码添加到附加模块清单中引用的数据文件中。有关如何在你的附加组件中激活更改的更多信息,请参阅第三章,创建 Odoo 附加模块。
本章的技术要求包括一个在线 Odoo 平台。
本章中使用的所有代码都可以从 GitHub 存储库中下载,网址为github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter09。
添加菜单项和窗口操作
向用户提供新功能的明显方式是通过添加菜单项。当你点击一个菜单项时,会发生某些事情。这个食谱将指导你如何定义这件事。
我们将创建一个顶级菜单及其子菜单,该菜单将打开所有宿舍房间列表。
这也可以通过Web 用户界面通过设置菜单来完成,但我们更喜欢使用 XML 数据文件,因为这是我们创建我们的附加模块时必须使用的。
准备工作
在这个菜谱中,我们需要一个依赖于base模块的模块,因为my_hostel模块向hostel.room添加了新的模型。所以,如果你正在使用现有的模块,请在清单中添加base依赖。或者,你可以从github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter09/00_initial_module获取初始模块。
如何操作...
在我们的附加模块的 XML 数据文件中执行以下步骤:
-
定义要执行的操作:
<record id="action_hostel_room" model="ir.actions.act_window"> <field name="name">All Hostel Room</field> <field name="res_model">hostel.room</field> <field name="view_mode">tree,form</field> </record> -
创建顶级菜单,如下所示:
<menuitem id="menu_custom_hostel_room" name="Hostel Room" web_icon="my_hostel,static/description/icon.png"/> -
在菜单中引用我们的操作:
<menuitem id="menu_all_hostel_room" parent="menu_custom_hostel_room" action="action_hostel_room" sequence="10" groups="" />
如果我们现在升级模块,我们将看到一个带有Hostel Room标签的顶级菜单,它打开一个名为All Hostel Room的子菜单。点击该菜单项将打开所有宿舍房间的列表。
它是如何工作的...
第一个 XML 元素record model="ir.actions.act_window"声明了一个窗口操作,用于显示包含所有宿舍房间的列表视图。我们使用了最重要的属性:
-
name: 用来作为由动作打开的视图的标题。 -
res_model:这是要使用的模型。我们使用hostel.room,这是 Odoo 存储所有hostel room的地方。 -
view_mode:这列出了要提供的视图类型。它是一个以逗号分隔的视图类型文件。默认值是tree, form,这使得列表视图和表单视图可用。如果你只想显示日历和表单视图,那么view_mode的值应该是calendar, form。其他可能的视图选择是kanban、graph、pivot、calendar和cohort。你将在接下来的菜谱中了解更多关于这些视图的信息。 -
domain: 这是可选的,允许你设置一个过滤器,以在视图中提供可用的记录。我们将在本章的在记录列表上定义过滤器 – Domain菜谱中更详细地了解所有这些视图。 -
context: 这可以设置提供给打开的视图的值,影响它们的行为。在我们的例子中,对于新记录,我们希望房间等级的默认值为1。这将在本章的向表单和动作传递参数 – Context菜谱中更深入地讨论。 -
limit:这设置了在列表视图中可以看到的默认记录数量。在我们的例子中,我们给出了20的限制,但如果你不提供limit值,Odoo 将使用默认值80。
接下来,我们从顶级菜单创建到可点击的末级菜单项的菜单项层次结构。menuitem元素最重要的属性如下:
-
name:这用于显示菜单项的文本。如果你的菜单项链接到操作,你可以省略它,因为在这种情况下将使用操作名称。 -
parent(parent_id如果使用record元素):这是引用父菜单项的 XML ID。没有父项的项目是顶级菜单。 -
action: 这是引用要调用的操作的 XML ID。 -
sequence: 这用于对同级菜单项进行排序。 -
groups(groups_id与record标签): 这是一个可选的用户组列表,可以访问菜单项。如果为空,则对所有用户可用。 -
web_icon: 此选项仅在顶级菜单上工作。它将在企业版中显示您的应用程序的图标。
窗口操作会自动确定要使用的视图,通过查找目标模型的视图(form、tree等)并选择序列号最低的一个。ir.actions.act_window和menuitem是方便的快捷 XML 标签,隐藏了您实际正在做的事情。如果您不想使用快捷 XML 标签,则可以通过<record>标签创建ir.actions.act_window和ir.ui.menu模型的记录。例如,如果您想通过<record>加载act_window,可以这样做:
<record id="action_hostel_room" model="ir.actions.act_window">
<field name="name">All Hostel Room</field>
<field name="res_model">hostel.room</field>
<field name="view_mode">tree,form</field>
<field name="context">{'default_room_rating': 1.0}</field>
<field name="domain">[('state', '=', 'draft')]</field>
</record>
以同样的方式,您可以通过<record>创建一个menuitem实例。
重要提示
注意,使用menuitem快捷方式时使用的名称可能不会映射到使用record元素时使用的字段名称;parent应该是parent_id,而groups应该是groups_id。
为了构建菜单,Web 客户端读取ir.ui.menu中的所有记录,并从parent_id字段推断它们的层次结构。菜单也会根据用户对模型和分配给菜单和操作的组的权限进行过滤。当用户点击菜单项时,其action将被执行。
还有更多...
窗口操作还支持一个target属性来指定视图的展示方式。可能的选项如下:
-
current: 这是默认选项,在 Web 客户端的主要内容区域打开视图。
-
new: 这将在弹出窗口中打开视图。
-
current,但它以编辑模式打开表单并禁用操作菜单。 -
全屏: 该操作将覆盖整个浏览器窗口,因此也会覆盖菜单。有时,这被称为平板模式。
-
main: 这类似于current,但它还会清除面包屑。
对于窗口操作,还有一些额外的属性,这些属性不支持ir.actions.act_window快捷标签。要使用它们,我们必须使用具有以下字段的record元素:
-
res_id: 如果打开表单,您可以通过在此处设置其 ID 来打开特定的记录。这在多步骤向导或您需要频繁查看或编辑特定记录的情况下非常有用。 -
search_view_id: 这指定了用于树形视图和图形视图的特定搜索视图。
请记住,左上角的菜单(或企业版本中的应用程序图标)和顶部栏中的菜单都是由菜单项组成的。唯一的区别是左上角的菜单项没有父菜单,而顶部栏上的菜单项以顶部栏中的相应菜单项作为父菜单。在左侧栏中,层次结构更为明显。
此外,请记住,出于设计原因,如果您的二级菜单有子菜单,则一级菜单将打开下拉菜单。在任何情况下,Odoo 都将根据子菜单项的顺序打开第一个菜单项的动作。
参考以下内容以了解更多关于菜单和视图的信息:
-
ir.actions.act_window动作类型是最常见的动作类型,但菜单可以引用任何类型的动作。技术上,如果你链接到客户端动作、服务器动作或ir.actions.*命名空间中定义的任何其他模型,都是相同的。它只是在后端对动作的处理上有所不同。 -
如果你只需要在具体要调用的动作中获得一点更多的灵活性,请查看返回窗口动作的服务器动作。如果你需要完全的灵活性,请查看客户端动作(
ir.actions.client),它允许你拥有完全定制的用户界面。然而,只有作为最后的手段才这样做,因为当你使用它们时,你会失去 Odoo 的许多方便的助手。
参见
- 要详细了解所有视图中的过滤器,请参阅本章中的在记录列表上定义过滤器 – 域配方。
通过动作打开特定视图
窗口动作会自动确定要使用的视图,如果没有给出,但有时我们希望动作打开一个特定的视图。
我们将为hostel.room模型创建一个基本表单视图,然后我们将创建一个新的窗口动作,专门用于打开该表单视图。
如何操作...
-
定义
hostel room的最小树形和表单视图:<record id="hostel_room_view_tree" model="ir.ui.view"> <field name="name">Hostel Room List</field> <field name="model">hostel.room</field> <field name="arch" type="xml"> <tree> <field name="name"/> <field name="room_no"/> <field name="state"/> </tree> </field> </record> <record id="hostel_room_view_form" model="ir.ui.view"> <field name="name">Hostel Room Form</field> <field name="model">hostel.room</field> <field name="arch" type="xml"> <form> <header> <button name="make_available" string="Make Available" type="object"/> <button name="make_closed" string="Make Closed" type="object"/> <field name="state" widget="statusbar"/> </header> <group> <group> <field name="name"/> <field name="room_no"/> </group> <group> <field name="description"/> </group> </group> </form> </field> </record> -
将从添加菜单项和窗口动作配方中的动作更新为使用新的表单视图:
<record id="action_hostel_room_tree" model="ir.actions.act_window.view"> <field name="act_window_id" ref="action_hostel_room" /> <field name="view_id" ref="hostel_room_view_tree" /> <field name="view_mode">tree</field> <field name="sequence" eval="1"/> </record> <record id="action_hostel_room_form" model="ir.actions.act_window.view"> <field name="act_window_id" ref="action_hostel_room" /> <field name="view_id" ref="hostel_room_view_form" /> <field name="view_mode">form</field> <field name="sequence" eval="2"/> </record>
现在,如果你打开你的菜单并点击列表中的合作伙伴,你应该看到我们刚刚定义的非常简单的表单和树形视图。
它是如何工作的...
这次,我们使用了适用于任何类型记录的通用 XML 代码,即具有所需id和model属性的record元素。record元素上的id属性是一个任意字符串,它必须对于你的附加组件是唯一的。model属性指的是你想要创建的模型名称。鉴于我们想要创建一个视图,我们需要创建ir.ui.view模型的记录。在此元素内,你通过model属性设置模型中定义的字段。对于ir.ui.view,关键字段是model和arch。model字段包含你想要定义视图的模型,而arch字段包含视图本身的定义。我们稍后会介绍其内容。
name字段虽然不是必需的,但在调试视图问题时很有帮助。因此,将其设置为字符串,说明这个视图打算做什么。此字段的内容不会显示给用户,因此你可以填写任何你认为合理的技术提示。如果你在这里不设置任何内容,你将获得一个默认名称,其中包含模型名称和视图类型。
ir.actions.act_window.view
我们定义的第二个记录与我们在添加菜单项和窗口动作菜谱中定义的act_window协同工作。我们已经知道,通过设置那里的view_id字段,我们可以选择用于第一个视图模式哪个视图。然而,鉴于我们设置了view_mode字段为tree, form视图,view_id必须选择树视图,但我们想设置的是第二个的表单视图。
如果你发现自己处于这种情况,请使用ir.actions.act_window.view模型,它允许你精细控制为哪种视图类型加载哪些视图。这里定义的前两个字段是引用其他对象的通用方式的示例;你保持元素的主体为空,但添加一个名为ref的属性,其中包含你要引用的对象的 XML ID。因此,这里发生的情况是我们从上一个菜谱中的act_window_id字段引用我们的动作,并在view_id字段中引用我们刚刚创建的视图。然后,尽管不是必需的,我们添加一个序列号来定位这个视图分配相对于同一动作的其他视图分配的位置。这仅在你通过创建多个ir.actions.act_window.view记录为不同的视图模式分配视图时才有意义。
重要提示
一旦你定义了ir.actions.act_window.view记录,它们将优先于你在动作的view_mode字段中填写的内容。因此,使用前面的记录,你将看不到任何列表,而只有表单。你应该添加另一个指向hostel.room模型列表视图的ir.actions.act_window.view记录。
更多内容...
正如我们在添加菜单项和窗口动作菜谱中看到的那样,我们可以用<record>替换act_window。如果你想使用自定义视图,你可以遵循给定的语法:
<record id="action_hostel_room" model="ir.actions.act_window">
<field name="name">All Hostel Room</field>
<field name="res_model">hostel.room</field>
<field name="view_id" ref="hostel_room_view_tree"/>
<field name="view_mode">tree,form</field>
</record>
这个例子只是act_window的另一种选择。在 Odoo 的代码库中,你可以找到这两种动作类型。
在表单视图中添加内容和小部件
前面的菜谱展示了如何为动作选择特定的视图。现在,我们将演示如何使表单视图更有用。在这个菜谱中,我们将使用我们在有一个动作打开特定视图菜谱中定义的表单视图。在表单视图中,我们将添加小部件和内容。
如何操作...
-
定义表单视图的基本结构:
<record id="hostel_room_view_form" model="ir.ui.view"> <field name="name">Hostel Room Form</field> <field name="model">hostel.room</field> <field name="arch" type="xml"> <form> <!--form content goes here --> </form> </record> -
要添加标题栏,通常用于动作按钮和阶段流程,请在表单内添加以下内容:
<header> <button name="make_available" string="Make Available" type="object"/> <button name="make_closed" string="Make Closed" type="object"/> <field name="state" widget="statusbar"/> </header> -
使用
group标签组织表单字段:<group string="Content" name="my_content"> <field name="name"/> <field name="room_no"/> </group> <group> <field name="description"/> </group> <notebook> <page string="Other Information" name="other_information"> <field name="other_info" widget="html"/> </page> </notebook>
现在,表单应该显示一个带有按钮和两个垂直对齐字段的顶部栏,如下面的截图所示:

图 9.1 – 表单视图的截图
它是如何工作的...
我们首先来看 ir.ui.view 模型的 arch 字段。首先,请注意,视图是在 XML 中定义的,因此你需要为 arch 字段传递 type="xml" 属性;否则,解析器会感到困惑。此外,你的视图定义必须包含格式良好的 XML;否则,当你升级/安装模块时,你会得到一个错误,例如“元素 odoo 有额外内容”。
现在,我们将遍历我们之前使用的标签,并总结其他可用的标签。
form
当你定义一个表单视图时,arch 字段中的第一个元素必须是 form 元素。这在内部用于推导记录的 type 字段。
除了以下元素之外,你还可以在表单标签中使用任意 HTML。算法规定,Odoo 未知的所有元素都被视为纯 HTML,并简单地传递给浏览器。请注意,你填入的 HTML 可以与 Odoo 元素生成的 HTML 代码交互,这可能会扭曲渲染。
header
此元素是用于在表单的标题中显示的元素的容器,标题以白色栏的形式渲染。通常,如本例所示,你在这里放置操作按钮。或者,如果你的模型有一个 state 字段,你也可以选择一个 状态栏。
button
button 元素用于允许用户触发一个动作。有关详细信息,请参阅 向表单添加按钮 烹饪配方。
<group> 元素是 Odoo 的主要元素,用于组织内容。放置在 <group> 元素内的字段将带有它们的标题,并且同一组内的所有字段都将对齐,以便也有一个视觉指示器表明它们属于一起。你还可以嵌套 <group> 元素;这将导致 Odoo 在相邻的列中渲染包含的字段。
通常情况下,你应该使用 <group> 机制来在表单视图中显示所有字段,只有在必要时才退回到其他元素,例如 <notebook>、<label>、<newline> 以及更多。
如果你将 string 属性分配给一个组,其内容将被渲染为该组的标题。
你还应该养成给每个字段的逻辑组命名的好习惯。这个名称对用户是不可见的,但在我们接下来的配方中覆盖视图时非常有帮助。在表单定义中保持名称唯一,以避免混淆你指的是哪个组。不要使用 string 属性,因为字符串的值最终会因为翻译而改变。
field
为了实际显示和操作数据,你的表单视图应该包含一些 field 元素。以下是一个示例:
<field name="other_info" widget="html"/>
这些有一个必填属性,称为name,它指的是模型中字段的名称。早些时候,我们向用户提供了编辑合作伙伴类别的功能。如果我们只想禁用字段的编辑功能,我们可以将readonly属性设置为1或True。此属性实际上可能包含一小部分 Python 代码,因此readonly="2>1"也会使字段变为只读。这也适用于invisible属性,您曾使用它来获取从数据库读取但不会显示给用户的值。稍后,我们将探讨这种用法可能适用的场景。
您一定注意到了categories字段中的widget属性。这定义了字段中的数据应该如何呈现给用户。每种类型的字段都有一个标准小部件,因此您不必明确选择小部件。然而,几种类型提供了多种表示方式,因此您可能会选择除默认之外的其他方式。由于完整的可用小部件列表超出了本菜谱的范围,请查阅Odoo 的源代码以尝试它们。查看第十四章,CMS 网站开发,以了解如何制作自己的。
<notebook>和<page>
如果您的模型字段太多,则可以使用<notebook>和<page>标签来创建标签页。<notebook>标签中的每个<page>将创建一个新的标签页,页面内的内容将是标签页的内容。以下示例将创建两个标签页,每个标签页有三个字段:
<notebook>
<page string="Tab 1">
<field name="field1"/>
<field name="field2"/>
<field name="field3"/>
</page>
<page string="Tab 2">
<field name="field4"/>
<field name="field5"/>
<field name="field6"/>
</page>
</notebook>
<page>标签中的string属性将是标签页的名称。您只能在<notebook>标签中使用<page>标签,但在<page>标签中,您可以使用任何其他元素。
通用属性
在大多数元素上(这包括group、field和button),您可以设置attributes和groups属性。以下是一个小示例:
<field name="other_info"
readonly="state == 'available'"
groups="base.group_no_one"/>
虽然attributes在使用属性动态表单元素的菜谱中进行了讨论,但groups属性为您提供了仅向某些组的成员显示某些元素的可能性。简单来说,组的完整 XML ID(多个组之间用逗号分隔)是该属性,并且对于不是至少属于上述提到的组之一的任何成员,该元素都将被隐藏。
其他标签
有时候您可能希望偏离规定的严格布局组。例如,如果您想将记录的name字段渲染为标题,则字段的标签将干扰外观。在这种情况下,不要将字段放入group元素中,而是将其放入普通的 HTMLh1元素中。然后,在h1元素之前,放置一个label元素,并将for属性设置为您的字段名称:
<label for="name" />
<h1><field name="name" /></h1>
这将以字段内容作为大标题进行渲染,但字段名称将写在大标题上方的小字体中。这基本上是标准合作伙伴表单所做的事情。
如果您需要在组内添加换行,请使用 newline 元素。它始终为空:
<newline />
另一个有用的元素是 footer。当您将表单作为弹出窗口打开时,这是一个放置操作按钮的好地方。它也将被渲染为一个单独的栏,类似于 header 元素。
形式视图也包含特殊的部件,例如 web_ribbon。您可以使用 <widget> 标签如下使用它:
<widget name="web_ribbon" title="Archived" bg_color="bg-danger"
invisible="active"/>
您可以使用 attributes 根据条件隐藏和显示功能区。如果您不了解 attributes,请不要担心。它将在本章的 使用属性动态创建表单元素 食谱中介绍。
重要提示
不要指定 string 属性(或任何其他翻译属性),因为您的视图覆盖将因其他语言而中断,因为视图是在应用继承之前进行翻译的。
还有更多…
由于表单视图基本上是带有一些扩展的 HTML,Odoo 也广泛使用了 CSS 类。其中两个非常有用的是 oe_read_only 和 oe_edit_only。具有这些类的元素将仅在 只读模式 或 编辑模式 中可见。例如,要使标签仅在编辑模式下可见,请使用以下代码:
<label f"r="n"me" cla"s="oe_edit_o"ly" />
另一个非常有用的类是 oe_inline,您可以在字段上使用它,使它们渲染为内联元素,以避免造成不想要的换行。当您将字段嵌入文本或其他标记标签时,请使用此类。
此外,form 元素可以有 create、edit 和 delete 属性。如果您将这些属性之一设置为 false,则相应的操作将不会对此表单可用。如果没有明确设置,操作的可用性将根据用户的权限推断。请注意,这纯粹是为了整理用户界面;不要用于安全。
参见
现有的部件和视图已经提供了很多功能,但迟早您会有一些无法用现有部件和视图满足的需求。请参考以下食谱来创建您自己的视图和部件:
-
请参考本章中关于使用
button元素触发操作的 添加按钮到表单 食谱以获取更多详细信息。 -
要定义您自己的部件,请参考 第十五章 的 创建自定义部件 食谱,Web 客户端开发。
-
请参考 第十五章 的 创建新视图 食谱,Web 客户端开发,以创建您自己的视图。
添加按钮到表单
header 元素。
如何操作...
添加一个指向操作的按钮:
<button type="action" name="%(my_hostel.hostel_room_category_action)d" string="Open Hotel Room Category" />
它是如何工作的...
按钮的 type 属性决定了其他字段的语义,因此我们首先看看可能的值:
-
action:这将使按钮调用在ir.actions.*命名空间中定义的操作。name属性需要包含操作的数据库 ID,您可以使用包含操作的 XML ID 的 Python 格式字符串方便地让 Odoo 查找。 -
object:这调用当前模型中的一个方法。name属性包含函数的名称。 -
string:string属性用于分配用户看到的文本。
还有更多...
使用btn-primary CSS 类来渲染高亮按钮,使用btn-default来渲染普通按钮。这通常用于向导中的取消按钮或以视觉上不引人注目的方式提供次要操作。设置oe_link类会使按钮看起来像链接。您还可以使用其他 Bootstrap 按钮类来获取不同的按钮颜色。
一个带有object类型按钮的调用可以返回一个描述动作的字典,然后将在客户端执行。这样,您可以实现多屏幕向导或只是打开另一个记录。
重要提示
注意,点击按钮总是导致客户端在运行方法之前发出write或create调用。
您还可以通过替换string属性在button标签内添加内容。这通常用于按钮框,如文档风格 表单菜谱中所述。
向表单和动作传递参数 - 上下文
在 Odoo 内部,每个方法都可以访问一个名为context的字典,这个字典从每个动作传播到执行该动作涉及的方法。UI 也可以访问它,并且可以通过在上下文中设置值以各种方式修改它。在这个菜谱中,我们将通过玩弄语言、默认值和隐式过滤器来探索这个机制的一些应用。
准备工作
虽然不是严格必要的,但如果您还没有安装法语,安装法语会使这个菜谱更有趣。请参阅第十一章,国际化,了解如何进行此操作。如果您有法语数据库,将fr_FR更改为其他语言,例如,en_US适用于英语。此外,点击激活按钮(当您悬停时变为存档),以便存档一个宿舍房间并验证此合作伙伴不再出现在列表中。
如何做...
-
创建一个新的动作,非常类似于添加菜单项和窗口动作菜谱中的动作:
<record id="action_hostel_room" model="ir.actions.act_window"> <field name="name">All Hostel Room</field> <field name="res_model">hostel.room</field> <field name="view_id" ref="hostel_room_view_tree"/> <field name="view_mode">tree,form</field> <field name="context">{'lang': 'fr_FR','default_lang': 'fr_FR', 'active_test': False, 'default_room_rating': 1.0}</field> </record> -
添加一个调用此动作的菜单。这留给读者作为练习。
当您打开此菜单时,视图将以法语显示,如果您创建一个新的合作伙伴,它们将法语作为预选语言。一个不那么明显的变化是,您还将看到已停用(存档)的合作伙伴记录。
它是如何工作的...
上下文字典是从几个来源填充的。首先,读取当前用户记录的一些值(用户的语言和用户时区分别为lang和tz)。然后,我们有一些附加组件,它们为了自己的目的添加了键。此外,UI 添加了关于我们目前正在使用的模型和记录的键(active_id、active_ids、active_model)。此外,如通过打开特定视图执行操作配方中所示,我们可以在操作中添加自己的键。这些被合并在一起,并传递给底层服务器函数和客户端 UI。
因此,通过设置lang上下文键,我们强制显示语言为法语。你会注意到这不会改变整个 UI 语言;这是因为只有我们打开的列表视图位于这个上下文范围内。其余的 UI 已经用包含用户原始语言的另一个上下文加载。然而,如果你在这个列表视图中打开一个记录,它也会以法语呈现,如果你在表单上打开一个链接记录或按下执行操作的按钮,语言也会传播。
通过设置default_lang,我们为在这个上下文范围内创建的每个记录设置一个默认值。一般模式是default_$fieldname: my_default_value,这允许你为在这种情况下新创建的合作伙伴设置默认值。鉴于我们的菜单是关于旅舍房间,我们默认为Hostel Average Rating字段添加了default_room_rating: 1作为值。然而,这是一个针对hostel.room的全局默认值,所以这并没有改变任何事情。对于标量字段,语法与你在 Python 代码中写的相同:string字段用引号括起来,number字段保持原样,而Boolean字段是True或False。对于关系字段,语法稍微复杂一些;请参阅第六章,管理模块数据,了解如何编写它们。
重要提示
注意,上下文中设置的默认值会覆盖模型定义中设置的默认值,因此你可以在不同情况下有不同的默认值。
最后一个键是active_test,它具有非常特殊的语义。对于每个具有名为active的字段的模型,Odoo 会自动过滤掉该字段为False的记录。这就是为什么你没有勾选此字段的合作伙伴从列表中消失的原因。通过设置此键,我们可以抑制这种行为。
重要提示
这对于 UI 本身很有用,但在你需要确保操作应用于所有记录,而不仅仅是活动记录时,在你的 Python 代码中更是非常有用。
还有更多...
当定义上下文时,你可以访问一些变量,其中最重要的一个是uid,它评估当前用户的 ID。你需要这个来设置默认过滤器(请参考下一配方,在记录列表上定义过滤器 – 域)。此外,你可以访问context_today函数和current_date变量,前者是一个date对象,代表从用户时区看当前日期,后者是 UTC 时间中的当前日期,格式为YYYY-MM-DD。要将date字段的默认值设置为当前日期,使用current_date,对于默认过滤器,使用context_today()。
此外,你可以使用 Python 的datetime、time和relativedelta类的一个子集进行一些日期计算。
重要提示
大多数域都是在客户端进行评估的。出于安全考虑,服务器端的域评估受到限制。当引入客户端评估时,为了避免整个系统崩溃,最佳选择是将 Python 的一部分实现为 JavaScript。Odoo 内置了一个小的 JavaScript Python 解释器,它对简单的表达式工作得很好,这通常就足够了。
警惕在<record id="action_name" model="ir.actions.act_window.view">快捷方式中使用context变量。这些是在安装时评估的,这几乎从来不是你想要的。如果你需要在你的上下文中使用变量,请使用<record />语法。
我们还可以为按钮添加不同的上下文。这和我们将上下文键添加到我们的操作的方式一样。这会导致按钮调用的函数或操作在给定的上下文中运行。
大多数作为 Python 评估的表单元素属性也都可以访问上下文字典。invisible和readonly属性就是这些属性的例子。因此,在你希望某个元素在某些时候显示在表单中,而在其他时候不显示的情况下,将invisible属性设置为context.get('my_key')。对于导致字段应该不可见的字段的情况,将上下文键设置为my_key: True。这种策略使你能够适应你的表单,而无需为不同场合重写它。
你还可以为关系字段设置上下文,这会影响字段如何加载。通过将form_view_ref或tree_view_ref键设置为视图的完整 XML ID,你可以为这个字段选择一个特定的视图。当你对同一对象有多个相同类型的视图时,这是必要的。如果没有这个键,你会得到序列号最低的视图,这可能并不总是理想的。
参见
-
上下文还用于设置默认搜索过滤器。你可以通过本章的定义搜索视图配方了解更多关于默认搜索过滤器的内容。
-
关于设置默认配方的更多细节,请参考下一配方,在记录列表上定义过滤器 – 域。
-
要了解如何安装法语,请参阅第十一章,国际化。
-
您可以参考第六章,管理模块数据来学习如何编写关系字段的语法。
在记录列表上定义过滤器 – 域
我们在本章的第一个菜谱中已经看到了一个域的例子,它是[('state', '=', 'draft')]。通常,您需要从操作中显示所有可用记录的子集,或者只允许可能的记录子集成为many2one关系的目标。在 Odoo 中描述这些过滤器的方法是使用域。本菜谱说明了如何使用域来显示合作伙伴的选择。
如何做到这一点...
要显示您操作中的合作伙伴子集,您需要执行以下步骤:
-
当“状态”设置为“草稿”时创建一个操作:
<record id="action_hostel_room" model="ir.actions.act_window"> <field name="name">All Hostel Room</field> <field name="res_model">hostel.room</field> <field name="view_id" ref="hostel_room_view_tree"/> <field name="view_mode">tree,form</field> <field name="context">{'lang': 'fr_FR','default_lang': 'fr_FR', 'active_test': False, 'default_room_rating': 1.0}</field> <field name="domain">[('state', '=', 'draft'), ('room_rating', '>', '0.0')]</field> </record> -
添加调用这些操作的菜单。这留给读者作为练习。
它是如何工作的...
域的最简单形式是由三个元组组成的列表,其中包含一个字段名(问题模型中的)作为第一个元素中的string,一个运算符作为第二个元素中的string,以及要检查的字段值作为第三个元素。这是我们之前所做的那样,这被解释为,“所有这些条件都必须适用于我们感兴趣的记录。”这实际上是一个快捷方式,因为域知道两个前缀运算符——&和|——其中&是默认的。因此,在规范形式中,第一个域将如下所示:
['&',('state', '=', 'draft'), ('room_rating', '>', '0.0')]
虽然对于更大的表达式来说这可能有点难以阅读,但前缀运算符的优势在于它们的范围是严格定义的,这可以节省您不必担心运算符优先级和括号。它总是两个表达式:第一个&应用于'&',('state', '=', 'draft'),其中('room_rating', '>', '0.0')作为第一个操作数,('room_rating', '>', '0.0')作为第二个操作数。然后,我们有一个第一个操作数和('room_rating', '>', '0.0')作为第二个操作数。
在第二步,我们必须写出完整形式,因为我们需要|运算符。
例如,假设我们有一个复杂的域,如下所示:['|', ('user_id', '=', uid), '&', ('lang', '!=', 'fr_FR'), '|', ('phone', '=', False), ('email', '=', False)]。参见以下图例了解该域的评估方法:

图 9.2 – 域的评估
此外,还有一个!运算符用于否定,但考虑到逻辑等价和否定比较运算符(如!=和not in),它实际上并不是必需的。
重要提示
注意,这是一个一元前缀运算符,因此它只适用于域中的后续表达式,而不是所有后续的内容。
注意,当你为窗口操作或其他客户端域编写域时,右操作数不需要是固定值。你可以使用与传递参数到表单和操作 – 上下文配方中使用的相同的 Python 最小化版本,因此你可以编写如上周更改或我的合作伙伴之类的过滤器。
更多内容...
上述域仅适用于模型本身的字段,而我们在很多时候需要根据链接记录的属性进行过滤。为此,你可以使用在@api.depends定义或相关字段中使用的相同表示法:从当前模型创建一个点路径到你想过滤的模型。为了搜索那些销售员是字母G开头的组成员的合作伙伴,你会使用[('user_id.groups_id.name', '=like', 'G%')]域。路径可能很长,所以你只需确保当前模型和你想过滤的模型之间存在关系字段。
操作符
以下表格列出了可用的操作符及其语义:

表 9.1 – 操作符及其语义
注意,一些操作符仅与某些字段和值一起工作。例如,域[('category_id', 'in', 1)]是无效的,将生成错误,而域[('category_id', 'in', [1])]是有效的。
使用域进行搜索的陷阱
这一切对于传统字段来说都很好,但一个臭名昭著的问题是搜索非存储函数字段的值。人们经常省略搜索函数。这很简单,可以通过在您的代码中提供搜索函数来修复,如第四章,应用模型中所述。
另一个问题可能会让开发者感到困惑,那就是 Odoo 在通过带有负操作符的one2many或many2many字段进行搜索时的行为。想象一下,你有一个带有A标签的合作伙伴,你搜索[('category_id.name', '!=', 'B')]。你的合作伙伴出现在结果中,这正是你所期望的,但如果你给这个合作伙伴添加B标签,它仍然出现在你的结果中,因为对于搜索算法来说,只要有一个链接记录(在这种情况下是A)不满足条件就足够了。现在,如果你移除A标签,使B成为唯一的标签,合作伙伴将被过滤掉。如果你也移除B标签,使合作伙伴没有任何标签,它仍然会被过滤掉,因为链接记录的条件假设了该记录的存在。然而,在其他情况下,这正是你想要的行为,因此改变标准行为并不是一个真正的选项。如果你在这里需要不同的行为,请提供一个解释你所需否定方式的搜索函数。
重要提示
人们经常忘记在处理域时他们正在编写 XML 文件。您需要转义小于运算符。搜索在当前日期之前创建的记录将必须以[('create_date', '<', current_date)]的形式在 XML 中编写。
域在 Odoo 中被广泛使用。您将在 Odoo 的每个地方找到它们;它们用于搜索、过滤、安全规则、搜索视图、用户动作等。
如果您需要操作您没有以编程方式创建的域,请使用odoo.osv.expression中提供的实用函数。is_leaf、normalize_domain、AND和OR函数将允许您以 Odoo 的方式精确组合域。不要自己这样做,因为有许多您必须考虑的角落案例,您很可能会忽略其中一个。
参见
- 对于域的标准应用,请参阅定义搜索视图的食谱。
定义列表视图
在花费了大量时间在表单视图之后,我们现在将快速看一下如何定义列表视图。在内部,这些在某些地方被称为树视图,在其他地方被称为列表视图,但鉴于 Odoo 视图框架中还有一个称为tree的结构,我们将坚持使用列表。
如何做...
-
定义您的列表视图:
<record id="hostel_room_view_tree" model="ir.ui.view"> <field name="name">Hostel Room List</field> <field name="model">hostel.room</field> <field name="arch" type="xml"> <tree> <field name="name"/> <field name="room_no"/> <field name="state"/> </tree> </field> </record> -
在本章添加菜单项和窗口动作食谱中创建的动作中注册树视图:
<record id="action_hostel_room" model="ir.actions.act_window"> <field name="name">All Hostel Room</field> <field name="res_model">hostel.room</field> <field name="view_id" ref="hostel_room_view_tree"/> <field name="view_mode">tree,form</field> <field name="context">{'tree_view_ref': 'my_hostel.hostel_room_view_tree', 'lang': 'fr_FR','default_lang': 'fr_FR', 'active_test': False, 'default_room_rating': 1.0}</field> <field name="domain">[('state', '=', 'draft')]</field> </record> -
添加调用这些动作的菜单。这留给读者作为练习。
安装/升级模块。之后,您将看到我们为宿舍房间创建的树视图,如果您检查它,它将根据我们的条件显示不同的行样式。
它是如何工作的...
您已经了解这里发生的大部分内容。我们定义了一个视图,这次使用的是tree类型,并将其与ir.actions.act_window.view元素关联起来。所以,唯一需要讨论的就是tree元素及其语义。在列表中,您没有太多设计选择,因此此元素的唯一有效子元素是field和button元素。您还可以在列表视图中使用一些小部件;在我们的例子中,我们使用了many2one_avatar_user小部件。树视图支持一个特殊的小部件,称为handle。这是针对列表视图的。它用于整数字段,并渲染一个用户可以用来将行拖动到列表中不同位置的拖动把手,从而更新字段的值。这对于序列或优先级字段非常有用。
通过使用optional属性,您可以可选地显示字段。将optional属性添加到字段将允许用户在任何时候从 UI 中隐藏和显示列。在我们的例子中,我们使用了它来为country和state字段。
在tree元素中,这里的新特性是decoration属性。这包含了关于选择哪一种字体和/或颜色的规则,以decoration-$name="Python code"的形式给出。我们将其设置为不可见,因为我们只需要数据,不想让用户被额外的两列所打扰。可能的类包括decoration-bf(粗体)和decoration-it(斜体),以及语义化的 Bootstrap 类decoration-danger、decoration-info、decoration-muted、decoration-primary、decoration-success和decoration-warning。
还有更多...
对于数值字段,你可以添加一个sum属性,使得这一列会与你在属性中设置的文本一起作为工具提示进行求和。较少使用的是avg、min和max属性,它们分别显示平均值、最小值和最大值。请注意,这四个属性只对当前可见的记录有效,因此你可能需要调整动作的limit(在添加菜单项和窗口动作配方中已介绍过),以便用户能够立即看到所有记录。
对于tree元素,有一个非常有趣的属性是editable。如果你将其设置为顶部或底部,列表的行为将完全不同。如果没有设置,点击行会打开行的表单视图。设置了之后,点击行会使其可在线编辑,可见字段被渲染为表单字段。这在嵌入列表视图中尤其有用,这将在本章后面的定义嵌入视图配方中讨论。顶部或底部的选择与是否将在列表的顶部或底部添加新行有关。
默认情况下,记录是按照显示模型的_order属性进行排序的。用户可以通过点击列标题来改变排序,但你也可以通过在tree元素中设置default_order属性来设置不同的初始排序。语法与_order相同。
重要提示
排序常常是新开发者的一个烦恼来源。由于 Odoo 让 PostgreSQL 在这里完成工作,你只能根据 PostgreSQL 所知的字段进行排序,并且只能是对同一数据库表中的字段进行排序。因此,如果你想根据函数或相关字段进行排序,确保你设置了store=True。如果你需要根据从另一个模型继承的字段进行排序,声明一个存储的相关字段。
tree元素的create、edit和delete属性与我们在本章添加内容和小部件到表单视图配方中描述的form元素的工作方式相同。如果设置了editable属性,它们也决定了可用的控件。
定义搜索视图
当你打开列表视图时,你会在右上角注意到搜索字段。如果你在那里输入一些内容,你会收到关于搜索建议,同时还有一个预定义的过滤器组可供选择。本教程将指导你如何定义这些建议和选项。
如何操作...
-
定义你的搜索视图:
<record id="hostel_room_view_search" model="ir.ui.view"> <field name="model">hostel.room</field> <field name="arch" type="xml"> <search> <field name="name"/> <field name="room_no"/> <group expand="0" string="Group By"> <filter string="State" name="state" context="{'group_by':'state'}"/> </group> </search> </field> </record> -
告诉你的动作使用它:
<record id="action_hostel_room" model="ir.actions.act_window"> <field name="name">All Hostel Room</field> <field name="res_model">hostel.room</field> <field name="search_view_id" ref="hostel_room_view_search" /> <field name="view_mode">tree,form</field> <field name="context">{'tree_view_ref': 'my_hostel.hostel_room_view_tree', 'lang': 'fr_FR','default_lang': 'fr_FR', 'active_test': False, 'default_room_rating': 1.0}</field> <field name="domain">[('state', '=', 'draft')]</field> </record>
现在你可以在搜索栏中输入内容,系统会提供在 name、room no 和 state 字段中搜索此术语的能力。如果你的术语恰好是你系统中银行账户号码的子串,你甚至可以选择精确搜索这个银行账户。
它是如何工作的...
在 name 的情况下,我们只是将字段列为提供给用户搜索的字段。我们保留了默认的语义,即字符字段的子串搜索。
对于类别,我们做了更有趣的事情。默认情况下,你的搜索术语应用于一个名为 name_search 的 many2many 字段触发器,在这种情况下将是对类别名称的子串搜索。然而,根据你的类别结构,搜索具有你感兴趣或其子类别的合作伙伴可能非常方便。想象一下主类别 Newsletter Subscribers,其子类别有 Weekly Newsletter、Monthly Newsletter 和几种其他新闻类型。使用前面的搜索视图定义搜索 Newsletter Subscribers 将一次性给出所有订阅了这些新闻的人,这比逐个搜索每种类型并合并结果要方便得多。
filter_domain 属性可以包含任意域,因此你不仅限于在 name 属性中命名的相同字段进行搜索,也不限于只使用一个术语。self 变量是用户填入的内容,也是你在这里唯一可以使用的变量。
这里有一个来自默认搜索视图的更详细的示例,针对宿舍房间:
<field name="name"
filter_domain="[
'|',
('display_name', 'ilike', self),
('room_no', '=', self)]"/>
这意味着用户不需要考虑要搜索什么。他们只需要输入一些字母,按 Enter 键,如果运气好的话,其中一个字段包含我们正在寻找的字符串。
对于 child_ids 字段,我们使用了另一个技巧。字段的类型不仅决定了搜索用户输入的默认方式,还定义了 Odoo 展示建议的方式。鉴于 many2one 字段是唯一提供自动完成的字段,我们通过设置 widget 属性强制 Odoo 进行自动完成,即使 child_ids 是一个 one2many 字段。如果没有这个设置,我们将不得不在这个字段中进行搜索,而没有完成建议。同样的情况也适用于 many2many 字段。
重要提示
注意,每个设置了 many2one 小部件的字段都会在用户的每个按键时触发其模型上的搜索;不要使用太多。
你还应该将最常用的字段放在顶部,因为如果用户只是键入一些内容并按下 Enter 键,搜索到的将是第一个字段。搜索栏也可以使用键盘操作;通过按下向下箭头选择建议,通过按下右箭头打开 many2one 的完成建议。如果你教育用户了解这一点,并注意搜索视图中字段的合理排序,这将比先键入内容、然后抓取鼠标并选择选项要高效得多。
filter 元素创建了一个按钮,将过滤器的 domain 属性内容添加到搜索域中。你应该添加一个逻辑内部 name 和一个 string 属性来描述过滤器,以便用户了解。
<group> 标签用于在 country_id 字段下提供分组选项。
还有更多...
你可以使用 group 标签来分组过滤器,这使得它们与其他过滤器相比渲染得更靠近一些,但这也有语义上的含义。如果你将多个过滤器放在同一个组中并激活其中多个,它们的域将使用 | 运算符合并,而同一组之外的过滤器和字段将使用 & 运算符合并。有时,你可能希望你的过滤器具有析取性,即它们为互斥集进行过滤,在这种情况下,选择其中两个都会导致一个空的结果集。在同一个组内,你可以通过使用 separator 元素达到相同的效果。
重要提示
注意,如果用户为同一字段填写多个查询,它们也会使用 | 运算符合并,所以你不需要担心这一点。
除了 field 属性外,filter 元素还可以有一个 context 属性,其内容将与当前上下文合并,并最终与其他搜索视图中的上下文属性合并。这对于支持分组的视图至关重要(参考 定义看板视图 和 定义图形视图 菜谱)。因为结果上下文决定了使用 group_by 键进行分组的字段。我们将在适当的菜谱中探讨分组的细节,但上下文还有其他用途。例如,你可以编写一个函数字段,它根据上下文返回不同的值,然后你可以通过激活过滤器来更改这些值。
搜索视图本身也响应上下文键。在创建记录时,与默认值非常类似,你可以通过上下文传递搜索视图的默认值。如果我们之前操作中设置了上下文 {'search_default_room_rating': 1},那么 room_rating 过滤器就会在搜索视图中预先选中。不过,这仅当过滤器有名称时才有效,这就是为什么你应该始终设置它。要在搜索视图中为字段设置默认值,使用 search_default_$fieldname。
此外,field和filter元素可以有一个与表单视图中相同的语义的groups属性,以便使元素只对某些组可见。
相关内容
-
有关操作上下文的更多详细信息,请参阅向表单和操作传递参数 – 上下文食谱。
-
使用大量使用重音符号的语言的用户可能希望在填写
e字符时让 Odoo 搜索e、è、é和ê。这是PostgreSQL 服务器的一个名为unaccent的配置,Odoo 有特殊支持,但这本书的范围之外。有关无重音的更多信息,请参阅www.postgresql.org/docs/10/unaccent.html。
添加搜索过滤器侧面板
Odoo 提供了一种显示搜索过滤器的方法,即搜索过滤器侧面板。此面板在视图的侧边显示过滤器列表。当最终用户频繁使用搜索过滤器时,搜索面板非常有用。
准备工作
搜索面板是搜索视图的一部分。因此,对于这个食谱,我们将继续使用之前食谱中的my_module附加组件。我们将把我们的搜索面板添加到之前设计的搜索视图中。
如何操作...
如下所示,在搜索视图中添加<searchpanel>:
<record id="hostel_room_view_search" model="ir.ui.view">
<field name="model">hostel.room</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="room_no"/>
<field name="state"/>
<searchpanel>
<field name="state" expand="1" select="multi" icon="fa-check-square-o" enable_counters="1"/>
</searchpanel>
</search>
</field>
</record>
更新模块以应用修改。更新后,您将在视图的左侧看到搜索面板。
它是如何工作的...
要添加搜索面板,您需要在搜索视图中使用<searchpanel>标签。要添加您的过滤器,您需要在搜索面板中添加一个字段。
在我们的例子中,首先,我们添加了一个state字段。您还需要将该字段的icon属性添加到其中。这个图标将在过滤器标题之前显示。一旦您将字段添加到搜索面板中,它将显示带有图标的标题,以及下面的所有用户列表。点击用户后,列表视图中的记录将被筛选,您将只能看到所选用户的联系信息。在这个过滤器中,只能有一个活动项,这意味着一旦您点击另一个用户的过滤器,之前的用户过滤器将被移除。如果您想激活多用户过滤器,可以使用select="multi"属性。如果您使用该属性,您将找到每个过滤器选项的复选框,并且您将能够一次激活多个过滤器。我们在state过滤器上使用了select="multi"属性。这将允许我们一次选择和筛选多个类别。
重要提示
当您在many2one或many2many上使用侧面板过滤器时,请小心。如果关系模型有太多记录,为了避免性能问题,只会显示前 200 条记录。
更多内容...
如果你想按组显示搜索面板项,你可以在字段上使用groupby属性。例如,如果你想根据其父层次结构对类别进行分组,你可以添加带有parent_id字段的groupby属性,如下所示:
<field name="state"
icon="fa-check-square-o"
select="multi"
groupby="parent_id"/>
这将根据记录的父类别对类别过滤器进行分组。
修改现有视图 - 视图继承
到目前为止,我们已忽略现有视图并声明了全新的视图。虽然这在教学上是有意义的,但你很少会处于需要为现有模型定义新视图的情况。相反,你可能会想稍微修改现有视图,无论是为了简单地显示你添加到附加模型中的字段,还是根据你的需求或客户的需求进行定制。
在这个菜谱中,我们将更改默认的合作伙伴表单,通过修改搜索视图来显示记录的最后修改日期,并使mobile字段可搜索。然后,我们将更改合作伙伴列表视图中一列的位置。
如何操作...
-
将字段注入到默认表单视图:
<record id="hostel_room_view_form_inherit" model="ir.ui.view"> <field name="name">Hostel Room Form Inherit</field> <field name="model">hostel.room</field> <field name="inherit_id" ref="my_hostel.hostel_room_view_form" /> <field name="arch" type="xml"> <xpath expr="//group[@name='my_content']/group" position="inside"> <field name="room_no"/> </xpath> <xpath expr="//group[@name='my_content']/group" position="after"> <group> <field name="description"/> <field name="room_rating"/> </group> </xpath> </field> </record> -
将字段添加到默认搜索视图:
<record id="hostel_room_view_search_inherit" model="ir.ui.view"> <field name="name">Hostel Room Search inherit</field> <field name="model">hostel.room</field> <field name="inherit_id" ref="my_hostel.hostel_room_view_search" /> <field name="arch" type="xml"> <xpath expr="." position="inside"> <field name="room_rating"></field> </xpath> </field> </record> -
将字段添加到默认列表视图:
<record id="hostel_room_view_tree_inherit" model="ir.ui.view"> <field name="name">Hostel Room List Inherit</field> <field name="model">hostel.room</field> <field name="inherit_id" ref="my_hostel.hostel_room_view_tree" /> <field name="arch" type="xml"> <xpath expr="//field[@name='name']" position="after"> <field name="room_no"/> </xpath> </field> </record>
更新你的模块后,你应该在合作伙伴表单的网站字段下方看到最后更新于字段。当你输入某些内容到搜索框时,它应该建议你在移动字段中搜索合作伙伴,在合作伙伴列表视图中,你会看到电话号码和电子邮件的顺序已经改变。
它是如何工作的...
在步骤 1中,我们为表单继承添加了一个基本结构。这里的关键字段,正如你可能猜到的,是inherit_id。你需要将你要修改的视图(继承自)的 XML ID 传递给它。arch字段包含有关如何修改你继承的视图中的现有 XML 节点的说明。实际上,你应该将整个过程视为简单的 XML 处理,因为所有语义部分都将在之后才出现。
在继承视图的arch字段中最经典的指令是field元素,它具有所需的属性:name和position。由于你只能让每个字段在表单中只出现一次,因此名称已经唯一地标识了一个字段。通过position属性,我们可以将我们放在字段元素中的任何内容放置在命名字段之前、内部或之后。默认情况下是inside,但为了可读性,你应该始终命名你需要的位置。记住,我们这里不是在谈论语义;这是关于我们命名的字段在 XML 树中的位置。它之后如何渲染是另一回事。
步骤 2演示了不同的方法。xpath元素选择与expr属性中命名的 XPath 表达式匹配的第一个元素。在这里,position属性告诉处理器将xpath元素的内容放置在哪里。
重要提示
如果你想要基于 CSS 类创建一个 XPath 表达式,Odoo 提供了一个名为hasclass的特殊函数。例如,如果你想选择具有test_class CSS 类的<div>元素,那么表达式将是expr="//div[hasclass('test_class')]"。
步骤 3 展示了如何更改元素的位置。此选项是在phone字段中引入的,以便使用position=move选项在email字段之后出现。
XPath 可能看起来有些吓人,但它是一种非常高效的选择所需操作节点的手段。花点时间查看一些简单的表达式;这是值得的。你可能会遇到术语上下文节点,某些表达式相对于它。在 Odoo 的视图继承系统中,这始终是你正在继承的视图的根元素。
对于在继承视图的arch字段中找到的所有其他元素,处理器会寻找具有相同节点名称和匹配属性(排除属性位置,因为这属于指令的一部分)的第一个元素。仅在非常不可能出现这种组合不唯一的情况下使用此功能,例如将组元素与name属性组合。
重要提示
注意,你可以在arch字段中包含尽可能多的指令元素。我们只为每个继承视图使用了一个,因为我们目前没有其他想要更改的内容。
还有更多...
position属性有两个其他可能的值:replace和attributes。使用replace会导致选定的元素被指令元素的 内容替换。因此,如果你没有任何内容,选定的元素可以简单地被移除。前面的列表或表单视图会导致state字段被移除:
<field name="state" position="replace" />
重要提示
移除字段可能会导致其他继承视图损坏,并产生几个其他不希望出现的副作用,因此如果可能的话,请避免这样做。如果你确实需要移除字段,请在评估顺序较晚的视图中进行(有关更多信息,请参阅下一节,视图继承中的评估顺序)。
attributes与前面的示例具有非常不同的语义。处理器期望元素包含具有name属性的attribute元素。然后,这些元素将被用于设置选定元素的属性。如果你想注意之前的警告,你应该将state字段的invisible属性设置为1:
<field name="state" position="attributes">
<attribute name="invisible">1</attribute>
</field>
一个attribute节点可以有add和remove属性,这些属性反过来应该包含要从或添加到空格分隔列表中的值。这对于class属性非常有用,你可以通过以下方式添加一个类(而不是覆盖整个属性):
<field name="description" position="attributes">
<attribute name="class" add="oe_inline" separator=" "/>
</field>
此代码将oe_inline类添加到description字段。如果该字段已经存在类属性,Odoo 会将该值与separator属性的值合并。
视图继承中的评估顺序
由于我们目前只有一个父视图和一个继承视图,因此不会遇到任何视图覆盖冲突的问题。当你安装了几个模块后,你会发现有很多针对合作伙伴表单的覆盖。只要它们在视图中更改不同的内容,这就可以了,但在某些情况下,了解覆盖的工作原理以避免冲突是很重要的。
视图的直接后代按其 priority 字段的升序评估,因此优先级较低的视图首先应用。继承的每一步都应用于第一步的结果,所以如果一个优先级为 3 的视图更改了一个字段,而另一个优先级为 5 的视图删除了它,这是可以的。然而,如果优先级相反,则不起作用。
你还可以从视图继承一个继承视图本身。在这种情况下,第二级继承视图应用于它继承的视图的结果。所以,如果你有四个视图,A、B、C 和 D,其中 A 是一个独立的表单,B 和 C 从 A 继承,而 D 从 B 继承,评估的顺序是 A、B、D 和 C。使用这一点来强制执行顺序,而无需依赖于优先级;这在一般情况下更安全。如果一个继承视图添加了一个字段,你需要对此字段应用更改,那么应该从继承视图继承,而不是从独立视图继承。
重要提示
这种继承始终在原始视图的完整 XML 树上工作,并应用了之前继承视图的修改。
以下要点提供了关于一些用于调整视图继承行为的先进技巧的信息:
-
对于继承视图,有一个非常有用但不太为人所知的字段是
groups_id。该字段仅在请求父视图的用户是那里提到的某个组的成员时才会发生继承。这可以在为不同级别的访问调整用户界面时节省你大量的工作,因为,通过继承,你可以执行比仅基于组成员显示或隐藏元素更复杂的操作,就像表单元素的groups属性所做的那样。 -
例如,如果用户是某个组的成员,你可以删除元素(这是
groups属性的相反操作)。你还可以执行一些复杂的技巧,例如根据组成员添加属性。考虑一些简单的事情,比如使某些组的字段只读,或者更有趣的概念,比如为不同的组使用不同的小部件。 -
本菜谱中描述的内容与原始视图的
mode字段设置为primary有关,而继承视图有模式扩展,这是默认设置。我们将在稍后研究继承视图的mode设置为primary的情况,那里的规则略有不同。
定义文档样式表单
在这个菜谱中,我们将回顾一些设计指南,以便提供一致的用户体验。
如何做到这一点...
-
使用
header元素开始你的表单:<header> <button name="make_available" string="Make Available" type="object"/> <button name="make_closed" string="Make Closed" type="object"/> <button type="action" name="%(my_hostel.hostel_room_category_action)d" string="Open Hotel Room Category" /> <field name="state" widget="statusbar"/> sheet element for content:状态按钮,将用于显示宿舍房间总数,并将重定向到宿舍房间:
<div class="oe_button_box" name="button_box"> <button type="object" class="oe_stat_button" icon="fa-pencil-square-o" name="action_open_related_hostel_room"> <div class="o_form_field o_stat_info"> <span class="o_stat_value"> <field name="related_hostel_room"/> </span> <span class="o_stat_text">Hostel Room</span> </div> </button> </div> -
添加一些突出的字段:
<div class="oe_title"> <h1> <field name="name"/> </h1> </div> -
添加你的内容;如果有许多字段,可以使用笔记本:
<group> <field name="child_ids"/> <field name="hoste_room_ids" widget="many2many_tags"/> chatter widget (if applicable):
让我们看看这个菜谱是如何工作的。
它是如何工作的...
标题应包含执行用户当前看到对象操作的按钮。使用btn-primary类使按钮在视觉上突出(在撰写时为紫色),这是一种指导用户关于此刻最合理的操作的好方法。尽量将所有突出显示的按钮放在非突出显示按钮的左侧,并隐藏当前状态下不相关的按钮(如果适用)。如果模型有状态,请使用statusbar小部件在标题中显示它。这将在标题中右对齐渲染。
sheet元素被渲染为风格化的表格,最重要的字段应该是用户查看时的第一件事。使用oe_title类使它们在显眼的位置渲染(在撰写时浮动在左侧,字体大小略有调整)。
如果有其他与用户当前查看的记录相关的记录(例如合作伙伴表单上的合作伙伴发票),请将它们放在具有oe_right和oe_button_box类的元素中;这将使其中的按钮右对齐。在按钮本身上,使用oe_stat_button类强制执行按钮的统一渲染。根据惯例,还从icon属性分配一个图标类。你可以在fontawesome.com/v4.7.0/icons/上了解更多关于 Font Awesome 的信息。
你可以使用oe_chatter类和mail.thread混入。我们将在第二十三章,Odoo 中的电子邮件管理中详细说明。
重要提示
即使你不喜欢这个布局,也要坚持使用这里描述的元素和类名,并使用 CSS 和可能的 JavaScript 调整你需要的内容。这将使用户界面与现有插件更加兼容,并允许你更好地与核心插件集成。
参见
-
要了解更多关于 Font Awesome 的信息,请访问
fontawesome.com/v4.7.0/icons/。 -
要了解更多关于
mail.thread混入的详细信息,请参阅第二十三章,Odoo 中的电子邮件管理。
使用属性动态表单元素
到目前为止,我们只研究了根据用户的组(元素的groups属性和继承视图的groups_id字段)更改表单,没有更多。这个配方将向你展示如何根据表单中字段的值修改表单视图。
如何操作...
-
在表单元素上定义一个名为
attributes的属性:<field name="child_ids" invisible="not parent_id" required="parent_id"/> -
确保你引用的所有字段都在你的表单中可用:
<field name="parent_id"/>
如果parent_id不是“宿舍房间类别”,这将使child_ids字段不可见,如果是宿舍房间类别,则将是必填项。
它是如何工作的...
invisible、required和readonly键(所有这些都是可选的)。值是可能引用表单上存在的字段(实际上只有那些,所以没有点路径),整个字典根据客户端 Python 的规则进行评估,如本章中将参数传递给表单和操作 - 上下文配方中所述。因此,例如,你可以在右手操作数中访问上下文。
还有更多...
虽然对于标量字段,这个机制相当直接,但对于one2many和many2many字段,处理起来就不那么明显了。实际上,在标准的 Odoo 中,你无法在[[6, False, []]](你的右手操作数)中做太多关于那些字段的事情。
定义嵌入视图
当你在表单上显示one2many或many2many字段时,如果你没有使用专门的控件,你将无法控制其渲染方式。此外,在many2one字段的情况下,有时可能希望能够影响打开链接记录的方式。在这个配方中,我们将探讨如何为这些字段定义私有视图。
如何操作...
-
按照惯例定义你的领域,但不要关闭标签:
<field name="hostel_room_ids"> -
将视图定义写入标签:
<tree> <field name="name"/> <field name="room_no"/> </tree> <form> <sheet> <group> <field name="name"/> <field name="room_no"/> </group> </sheet> </form> -
关闭标签:
</field>
它是如何工作的...
当 Odoo 加载表单视图时,它首先检查字段中是否有嵌入视图的relational类型字段,如前所述。这些嵌入视图可以具有与我们之前定义的视图完全相同的元素。只有当 Odoo 找不到某种类型的嵌入视图时,它才会使用该类型的模型默认视图。
还有更多...
虽然嵌入视图可能看起来是一个很好的功能,但它们极大地复杂化了视图继承。例如,一旦涉及到嵌入视图,字段名称就不保证是唯一的,你通常必须使用一些复杂的 XPath 来选择嵌入视图内的元素。
因此,总的来说,你最好定义独立的视图,并使用前面在本章中让操作打开特定视图配方中描述的form_view_ref和tree_view_ref键。
在表单视图的侧边显示附件
在某些应用程序中,例如开票,你需要根据文档填写数据。为了简化数据填写过程,Odoo 12 版本中添加了一个新功能,在表单视图的侧边显示文档。
在本教程中,我们将学习如何并排显示表单视图和文档视图:

图 9.3 – 级联附件和表单视图
重要提示
此功能仅适用于大屏幕(>1534px),因此如果您有较小的视口,此功能将被隐藏。内部,此功能使用一些响应式工具,因此此功能仅在企业版中工作。然而,您仍然可以在您的模块中使用此代码。Odoo 将自动处理此操作,因此如果模块安装在企业版中,它将显示文档,而在社区版中,它将隐藏所有内容而不会产生任何副作用。
如何操作...
我们将启用此功能以修改hostel.room.category模型的表单视图,如下所示:
<record id="hostel_room_category_view_form" model="ir.ui.view">
<field name="name">Hostel Room Categories Form</field>
<field name="model">hostel.room.category</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_button_box" name="button_box">
<button type="object" class="oe_stat_button" icon="fa-pencil-square-o" name="action_open_related_hostel_room">
<div class="o_form_field o_stat_info">
<span class="o_stat_value">
<field name="related_hostel_room"/>
</span>
<span class="o_stat_text">Hostel Room</span>
</div>
</button>
</div>
<div class="oe_title">
<h1>
<field name="name"/>
</h1>
</div>
<group>
<group>
<field name="description"/>
</group>
<group>
<field name="parent_id"/>
</group>
</group>
<group>
<field name="child_ids"
invisible="not parent_id"
required="parent_id"/>
<field name="hoste_room_ids">
<tree>
<field name="name"/>
<field name="room_no"/>
</tree>
<form>
<sheet>
<group>
<field name="name" />
<field name="room_no"/>
</group>
</sheet>
</form>
</field>
</group>
</sheet>
<div class="o_attachment_preview" options="{'types': ['image', 'pdf'], 'order': 'desc'}"/>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
<field name="activity_ids" widget="mail_activity"/>
</div>
</form>
</field>
</record>
更新模块以应用更改。您需要通过记录聊天上传 PDF 或图像。当您上传时,Odoo 将在侧边显示附件。
工作原理...
此功能仅在您的模型继承了mail.thread模型时才有效。要显示任何表单视图侧边的文档,您需要在聊天元素之前添加一个带有o_attachment_preview类的空<div>。就是这样;聊天中附加的文档将在表单视图的侧边显示。
默认情况下,pdf和image文档将按日期升序显示。您可以通过提供额外的选项来更改此行为,以下是一些选项:
-
type:您需要传递您想要允许的文档类型列表。只有两个可能的值:pdf和image。例如,如果您只想显示pdf类型的图像,您可以传递{'type': ['pdf']}。 -
order:可能的值是asc和desc。这些选项允许您按文档创建日期的升序或降序显示文档。
还有更多...
在大多数情况下,您希望在记录的初始状态下显示文档。如果您想根据领域隐藏附件预览,您可以使用<div>标签上的attributes来隐藏预览。
看以下示例:如果state字段的值不是draft,它将隐藏 PDF 预览:
<div class="o_attachment_preview"
invisible="state != 'draft'"/>
这就是您可以在不需要时隐藏附件的方法。通常,此功能用于从 PDF 中填充数据,并且仅在草稿模式下激活。
定义看板视图
到目前为止,我们已经向您展示了一系列可以打开以显示表单的记录。虽然这些列表在展示大量信息时效率很高,但由于缺乏设计可能性,它们往往显得有些单调。在本教程中,我们将探讨看板视图,它允许我们以更具吸引力的方式展示记录列表。
如何操作...
-
定义一个
kanban类型的视图:<record id="hostel_room_category_view_kanban" model="ir.ui.view"> <field name="name">Hostel Room Categories kanban</field> <field name="model">hostel.room.category</field> <field name="arch" type="xml"> <kanban class="o_kanban_mobile" sample="1"> -
列出您在视图中将使用的字段:
<field name="name"/> <field name="description"/> <field name="parent_id"/> -
实施设计:
<templates> <t t-name="kanban-box"> <div t-attf-class="oe_kanban_global_click"> <div class="row mb4"> <div class="col-6 o_kanban_record_headings"> <strong> <span> <field name="name"/> </span> </strong> </div> <div class="col-6 text-end"> <strong><i role="img" title="description"/> <t t-esc="record.description.value"/></strong> </div> </div> <div class="row"> <div class="col-12"> <span><field name="parent_id"/></span> </div> </div> </div> </t> </templates> -
关闭所有标签:
</kanban> </field> </record> -
将此视图添加到您的某个操作中。这留作读者的练习。您可以在 GitHub 示例文件中找到一个完整的示例:
github.com/PacktPublishing/Odoo-13-Development-Cookbook-Fourth-Edition/tree/master/Chapter09/15_kanban_view/my_module。
它是如何工作的...
为了以后能够访问它们,我们需要在步骤 2中提供一个要加载的字段列表。templates元素的内容必须是一个具有t-name属性设置为kanban-box的单个t元素。
您在这个元素内部写入的内容将为每个记录重复,对于t元素和t-*属性有特殊的语义。有关详细信息,请参阅第十五章的使用客户端 QWeb 模板配方,Web 客户端开发,因为从技术上讲,看板视图只是 QWeb 模板的应用。
对于看板视图,有一些特定的修改。在评估期间,您可以访问read_only_mode、record和widget变量。字段可以通过record.fieldname访问,这是一个具有value和raw_value属性的对象,其中value是已格式化以供用户展示的字段值,而raw_value是来自数据库的字段值。
重要提示
many2many字段在这里是个例外。您只能通过record变量获取一个 ID 列表。对于用户可读的表示,您必须使用field元素。
注意模板顶部的type属性。此属性使 Odoo 生成一个以查看模式打开记录的链接(type属性也可以是object或action,这将渲染从模型或操作调用的函数的链接。在两种情况下,您都需要补充表单视图中按钮的属性,如本章中向表单添加按钮配方中概述的那样。您也可以使用button元素;这里的type属性具有相同的语义。
还有更多...
还有几个值得注意的辅助函数。如果您需要为元素生成伪随机颜色,请使用kanban_color(some_variable)函数,它将返回一个设置background和color属性的 CSS 类。这通常用于t-att-class元素。
如果您想显示存储在二进制字段中的图像,请使用kanban_image(modelname, fieldname, record.id.raw_value),如果您在字段列表中包含了该字段,则它返回一个数据 URI;如果字段已设置,则为占位符;如果没有设置,则为 URL,如果未在字段列表中包含该字段,则 Odoo 会流式传输字段的内容。如果您需要同时显示大量记录或预期非常大的图像,请不要在字段列表中包含该字段。通常,您会在img元素的t-att-src属性中使用此功能。
重要提示
在看板视图中进行设计可能会有些棘手。通常更好的方法是使用 HTML 类型的函数字段生成 HTML,并从 Qweb 视图中生成这个 HTML。这样,你仍然在使用 QWeb,但是在服务器端进行,当你需要处理大量数据时,这会变得更加方便。
相关内容
- 要了解更多关于模板元素的信息,请参阅第十五章的使用客户端 QWeb 模板配方,Web 客户端开发。
根据状态在列中显示看板卡片
此配方向您展示了如何设置一个看板视图,用户可以从一列拖放记录到另一列,从而将相关记录推入另一个状态。
准备工作
从现在起,我们将在这里使用宿舍模块,因为它定义了比基础模块中定义的更适合基于日期和状态的视图的模型。因此,在继续之前,将base添加到您的附加组件的依赖项列表中。
如何操作...
-
定义宿舍房间类别的看板视图:
<record id="hostel_room_category_view_kanban" model="ir.ui.view"> <field name="name">Hostel Room Categories kanban</field> <field name="model">hostel.room.category</field> <field name="arch" type="xml"> <kanban class="o_kanban_mobile" sample="1" default_group_by="parent_id"> <field name="name"/> <field name="description"/> <templates> <t t-name="kanban-box"> <div t-attf-class="oe_kanban_global_click"> <div class="row mb4"> <div class="col-6 o_kanban_record_headings"> <strong> <span> <field name="name"/> </span> </strong> </div> <div class="col-6 text-end"> <strong><i role="img" title="description"/> <t t-esc="record.description.value"/></strong> </div> </div> <div class="row"> <div class="col-12"> <span><field name="parent_id"/></span> </div> </div> </div> </t> </templates> </kanban> </field> </record> -
使用此视图添加一个菜单和一个动作。这留给读者作为练习。
它是如何工作的...
看板视图支持分组,这允许你在同一列中显示具有共同分组字段的记录。这通常用于父酒店房间类别或parent_id字段,因为它允许用户通过简单地将其拖入另一列来更改记录的此字段值。将default_group_by属性设置在kanban元素上,以使用你想要分组的字段名称来利用此功能。
要控制看板分组的操作,Odoo 中提供了一些选项:
-
group_create:此选项用于隐藏或显示true。 -
group_delete:此选项启用或禁用true。 -
group_edit:此选项启用或禁用true。 -
archivable:此选项启用或禁用从看板分组上下文菜单中存档和恢复记录的选项。这仅在您的模型中存在active布尔字段时才有效。 -
quick_create:使用此选项,您可以直接从看板视图中创建记录。 -
quick_create_view:默认情况下,quick_create选项仅在看板中显示名称字段。然而,使用quick_create_view选项,你可以提供最小表单视图的引用,以便在看板中显示它。 -
on_create:如果你在创建新记录时不希望使用quick_create,也不希望将用户重定向到表单视图,你可以提供向导的引用,以便在点击创建按钮时打开向导。
还有更多...
如果未在专用属性中定义,任何搜索过滤器都可以通过将名为group_by的上下文字符串设置为要按其分组的字段名称(名称)来添加分组。
定义日历视图
这个菜谱将指导你如何以可视化的方式显示和编辑记录中关于日期和持续时间的详细信息。
如何操作...
按以下步骤为hostel.room.category模型添加calendar视图:
-
定义
calendar视图:<record id="hostel_room_category_view_calendar" model="ir.ui.view"> <field name="name">Hostel Room Categories Calendar</field> <field name="model">hostel.room.category</field> <field name="arch" type="xml"> <calendar date_start="date_assign" date_stop="date_end" color="parent_id"> <field name="name" /> <field name="parent_id" /> </calendar> </field> </record> -
使用此视图添加菜单和操作。这留给读者作为练习。
它是如何工作的...
calendar视图需要通过date_start和date_stop属性传递字段名称,以指示在构建视觉表示时查看哪些字段。仅使用具有Datetime或Date类型的字段;其他类型的字段将不起作用,并会生成错误。虽然date_start是必需的,但你可以选择省略date_stop,并设置date_delay属性,它预期是一个表示持续时间的Float字段。
calendar视图允许你为具有相同字段值的记录分配相同的(任意指定的)颜色。要使用此功能,将color属性设置为所需的字段名称。在我们的例子中,我们可以一眼看出哪些宿舍房间类别属于同一宿舍房间类别,因为我们把parent_id作为字段来决定颜色组。
在calendar元素的主体中命名的字段将在表示覆盖时间间隔的块中显示,字段之间用逗号分隔。
还有更多...
calendar视图还有一些其他有用的属性。如果你想通过弹出窗口而不是标准表单视图打开日历条目,将event_open_popup设置为1。默认情况下,你只需填写一些文本即可创建新条目,这实际上会调用模型的name_create函数来创建记录。如果你想禁用此行为,将quick_add设置为0。
如果你的模型覆盖了整整一天,将all_day设置为字段名称,如果记录覆盖了整整一天则为true,否则为false。
定义图形视图和交叉视图
在这个菜谱中,我们将查看 Odoo 的商业智能视图。这些是只读视图,旨在展示数据。
准备工作
我们在这里仍然使用 hostel 模块。你可以配置图表和交叉视图以获取不同的统计数据。在我们的例子中,我们将专注于分配的用户。我们将生成一个图表和交叉视图来查看宿舍房间类别的用户。顺便说一句,最终用户可以通过修改视图选项来生成他们选择的统计数据。
如何操作...
-
使用条形图定义一个图表视图:
<record id="hostel_room_category_view_graph" model="ir.ui.view"> <field name="name">Hostel Room Categories Graph</field> <field name="model">hostel.room.category</field> <field name="arch" type="xml"> <graph type="bar"> <field name="parent_id"/> <field name="child_ids"/> </graph> </field> </record> -
定义一个交叉视图:
<record id="hostel_room_category_view_pivot" model="ir.ui.view"> <field name="name">Hostel Room Categories Pivot</field> <field name="model">hostel.room.category</field> <field name="arch" type="xml"> <pivot> <field name="parent_id" type="row"/> <field name="name" type="col"/> </pivot> </field> </record> -
使用此视图添加菜单和操作。这留给读者作为练习。
如果一切顺利,你应该会看到图表显示分配给哪些宿舍房间类别的父宿舍房间类别以及这些宿舍房间类别的状态。
它是如何工作的...
图表视图通过根元素 graph 声明。graph 元素上的 type 属性决定了图表视图的初始模式。可能的值是 bar、line 和 chart,但默认是 bar。图表视图高度交互式,因此用户可以在不同的模式之间切换,也可以添加和删除字段。如果你使用 type="bar",你还可以使用 stacked="1" 在分组时显示堆叠条形图。
field 元素告诉 Odoo 在哪个轴上显示什么。对于所有图表模式,你需要至少一个 row 类型的字段和一个 measure 类型的字段才能看到有用的内容。row 类型的字段确定分组,而 measure 类型的字段代表要显示的值。折线图只支持每种类型的一个字段,而图表和条形图可以很好地处理一个度量值和两个分组字段。
交叉视图有其自己的根元素,pivot。交叉视图支持任意数量的分组和度量字段。如果你切换到一个不支持你定义的分组和度量数量的模式,不会发生任何问题;一些字段将被忽略,结果可能不如预期那么有趣。
还有更多...
对于所有图表类型,Datetime 字段在分组时很棘手,因为你很少会遇到相同的字段值。所以,如果你有一个 row 类型的 Datetime 字段,也请指定以下值之一的 interval 属性:day、week、month、quarter 或 year。这将导致分组在给定的间隔内进行。
重要提示
分组,就像排序一样,高度依赖于 PostgreSQL。因此,这里的规则也适用,即一个字段必须存在于数据库和当前表中才能被使用。
定义数据库视图以收集所需的所有数据,并在该视图之上定义一个模型,以便所有必要的字段都可用,这是一种常见的做法。
根据视图的复杂性和分组,构建图表可能是一项相当昂贵的操作。在这种情况下,考虑将 auto_search 属性设置为 False,以便用户可以先调整所有参数,然后再触发搜索。
交叉表也支持按列分组。使用col类型为要添加的字段。
定义队列视图
对于记录的队列分析,新的队列视图是在 Odoo 版本 12 中添加的。队列视图用于找出记录在特定时间跨度内的生命周期。使用队列视图,您可以查看任何对象在特定时间内的流失和留存率。
准备工作
cohort视图是模块清单文件中的web_cohort的一部分。在我们的示例中,我们将创建一个视图来查看宿舍房间类别的队列分析。
如何操作...
按照以下步骤为hostel.room.category模型添加cohort视图:
-
定义一个
cohort视图:<record id="hostel_room_category_view_cohort" model="ir.ui.view"> <field name="name">Hostel Room Categories Cohort</field> <field name="model">hostel.room.category</field> <field name="arch" type="xml"> <cohort date_start="date_assign" date_stop="date_end" interval="month" string="Categories Cohort" /> </field> </record> -
使用此视图添加菜单和操作。这留给读者作为练习。
它是如何工作的...
要创建队列视图,您需要提供date_start和date_stop。这些将在视图中用于确定任何记录的时间跨度。例如,如果您正在管理一项服务的订阅,则订阅的开始日期将是date_start,而订阅即将到期的日期将是date_stop。
默认情况下,cohort视图将以每月的间隔在retention模式下显示。您可以使用提供的选项在cohort视图中获得不同的行为:
-
mode: 您可以使用两种模式使用队列:retention (默认)或churn。retention模式从 100%开始并随时间递减,而churn模式从 0%开始并随时间递增。 -
timeline: 此选项接受两个值:forward (默认)或backward。在大多数情况下,您需要使用前向时间线。然而,如果date_start在将来,您将需要使用后向时间线。我们使用后向时间线的例子可能是为未来日期的事件注册参会者,而注册日期在过去的场景。 -
interval: 默认情况下,队列按月分组,但您可以在间隔选项中更改此设置。除了月份外,队列还支持按日、周和年间隔分组。 -
measure: 就像图形和交叉表一样,度量用于显示给定字段的聚合值。如果没有提供选项,队列将显示记录数。
定义甘特图视图
Odoo 版本 13 添加了一个新的带有新选项的gantt视图。gantt视图对于查看整体进度和调度业务流程非常有用。在本菜谱中,我们将创建一个新的gantt视图并查看其选项。
准备工作
gantt视图是 Odoo 企业版的组成部分,因此您不能在社区版中使用它。如果您使用的是企业版,您需要在模块的清单文件中添加web_gantt依赖项。
在我们的示例中,我们将继续使用之前菜谱中的my_hostel模块。我们将为宿舍房间类别创建一个新的gantt视图。
如何操作...
-
按如下方式为宿舍房间类别模型定义一个
gantt视图:<record id="hostel_room_category_view_gantt" model="ir.ui.view"> <field name="name">Hostel Room Categories Gantt</field> <field name="model">hostel.room.category</field> <field name="arch" type="xml"> <gantt date_start="date_assign" date_stop="date_end" string="Hostel Room Category" default_group_by="parent_id" color="parent_id"> <field name="name"/> <field name="parent_id"/> <templates> <div t-name="gantt-popover" > <ul class="pl-1 mb-0 list-unstyled"> <li> <strong>Name: </strong> <t t-esc="name"/> </li> <li> <strong>Parent Category: </strong> <t t-esc="parent_id[1]"/> </li> </ul> </div> </templates> </gantt> </field> </record> -
使用此视图添加菜单和操作。这留作读者的练习。
安装并更新模块以应用更改;更新后,您将在宿舍房间类别中看到 gantt 视图。
它是如何工作的...
使用 gantt 视图,您可以在一个屏幕上显示整体进度表。在我们的示例中,我们为按父类别分组的宿舍房间类别创建了一个 gantt 视图。通常,您需要两个属性来创建 gantt 视图,即 start_date 和 stop_date,但还有一些其他属性可以扩展 gantt 视图的功能。让我们看看所有选项:
-
start_date: 定义gantt项的起始时间。它必须是日期或日期时间字段。 -
default_group_by: 如果您想根据字段对gantt项进行分组,请使用此属性。 -
color: 此属性用于决定gantt项的颜色。 -
progress: 此属性用于指示gantt项目的进度。 -
decoration-*: 装饰属性用于根据条件决定gantt项的颜色。它可以这样使用:decoration-danger="state == 'lost'"。它的其他值是decoration-success、decoration-info、decoration-warning和decoration-secondary。 -
scales: 如果您只想为少数几个刻度启用gantt视图,请使用scales属性。例如,如果您只想使用日和周刻度,则可以使用scales="day,week"。 -
默认情况下,
gantt视图项可调整大小和可拖动,但如果您想禁用此功能,则可以使用edit="0"属性。
更多...
当您悬停在 gantt 视图项上时,您将看到该项的名称和日期。如果您想自定义该弹出窗口,您可以定义一个如下所示的 gantt 视图定义:
<gantt date_start="date_assign" date_stop="date_end" string="Hostel Room Category" default_group_by="parent_id" color="parent_id">
<field name="name"/>
<field name="parent_id"/>
<templates>
<div t-name="gantt-popover" >
<ul class="pl-1 mb-0 list-unstyled">
<li>
<strong>Name: </strong>
<t t-esc="name"/>
</li>
<li>
<strong>Parent Category: </strong>
<t t-esc="parent_id[1]"/>
</li>
</ul>
</div>
</templates>
</gantt>
注意,您需要通过 <field> 标签添加您想在模板中使用的字段。
定义活动视图
活动是 Odoo 应用程序的重要组成部分。它们用于为不同的业务对象安排待办事项。activity 视图帮助您查看模型上所有活动的状态和进度。
准备工作
在我们的示例中,我们将继续使用前一个菜谱中的 my_hostel 模块。我们将为宿舍房间类别创建一个新的 activity 视图。
如何做...
-
按如下方式为
hostel room category模型定义一个activity视图:<record id="hostel_room_category_view_activity" model="ir.ui.view"> <field name="name">Hostel Room Categories Activity</field> <field name="model">hostel.room.category</field> <field name="arch" type="xml"> <activity string="Hostel Room Category"> <templates> <div t-name="activity-box"> <div> <field name="name" display="full"/> <field name="parent_id" muted="1" display="full"/> </div> </div> </templates> </activity> </field> </record> -
使用此视图添加菜单和操作。这留作读者的练习。
它是如何工作的...
activity 视图很简单;大多数事情都是自动管理的。您只有自定义第一列的选项。要显示第一列中的数据,您需要创建一个名为 activity-box 的 QWeb 模板,然后就可以了;Odoo 将管理其余部分。
activity 视图将在第一列显示您的模板,其他列将显示按活动类型分组的预定活动。
定义地图视图
Odoo 版本 13 增加了一个名为“地图”的新视图。正如其名所示,它用于显示带有标记的地图。它们对于现场服务非常有用。
准备工作
在我们的例子中,我们将继续使用之前菜谱中的my_hostel模块。我们将为宿舍房间类别创建新的map视图。map视图是模块清单文件中web_map依赖的一部分。
Odoo 使用来自www.mapbox.com/的 API 在视图中显示地图。为了在 Odoo 中查看地图,您需要从mapbox生成访问令牌。请确保您已生成访问令牌并将其设置在 Odoo 配置中。
如何操作…
-
按照以下方式为宿舍房间类别模型定义一个
map视图:<record id="hostel_room_category_view_map" model="ir.ui.view"> <field name="name">Hostel Room Categories Map</field> <field name="model">hostel.room.category</field> <field name="arch" type="xml"> <map> <field name="name" string="Title "/> <field name="parent_id" string="Hostel Room Category "/> </map> </field> ss</record> -
使用此视图添加菜单和操作。这留作读者的练习。
它是如何工作的...
创建地图视图相当简单;您只需要一个many2one字段,该字段引用hostel.room.category模型。hostel.room.category模型有address字段,这些字段被地图视图用于显示地址的标记。您需要使用res_partner属性将地址映射到map视图。在我们的例子中,我们使用了parent_id字段作为在parent_id字段中设置的宿舍房间类别父记录集。
第十章:安全访问
Odoo 通常被多用户组织使用。每个用户在每一个组织中都有一个独特的职位,并且根据他们的职能有不同的访问权限。例如,人力资源经理没有访问公司会计信息的权限。您可以使用访问权限和记录规则来确定用户在 Odoo 中可以访问哪些信息。在本章中,我们将学习如何设置访问权限规则和记录规则。
这种访问和安全的细分要求我们根据其权限级别提供对角色的访问。我们将在本章中了解这一点。
在本章中,我们将涵盖以下食谱:
-
创建安全组并将它们分配给用户
-
向模型添加安全访问
-
限制模型中字段的访问
-
使用记录规则来限制记录访问
-
使用安全组激活功能
-
以超级用户身份访问记录集
-
根据组隐藏视图元素和菜单
为了简洁地传达要点,本章中的食谱对现有示例模块进行了小的补充。
技术要求
本章的技术要求包括使用我们根据第三章,创建 Odoo 附加模块中的教程创建的模块。为了遵循这里的示例,您应该已经创建了该模块并准备好使用。
本章中将要使用的所有代码都可以从本书的 GitHub 仓库中下载,网址为github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter10。
创建安全组并将它们分配给用户
Odoo 中的安全访问通过安全组进行配置:权限被授予组,然后组被分配给用户。每个功能区域都有一个由中央应用程序提供的基本安全组。
当附加模块增强现有应用程序时,它们应该向相应的组添加权限,如向模型添加安全访问食谱中所述。
当附加模块引入一个尚未被现有核心应用程序覆盖的新功能区域时,应添加相关的安全组。我们通常至少应该有用户和管理角色。
以我们介绍的宿舍示例第三章,创建 Odoo 附加模块为例——它并不适合任何 Odoo 核心应用程序,因此我们将为它添加安全组。
准备工作
本教程假设您已经准备好一个 Odoo 实例,其中包含my_hostel,如第三章中所述,创建 Odoo 附加模块。
如何做...
要向模块添加新的访问安全组,请执行以下步骤:
-
确保附加模块的
__manifest__.py声明文件中定义了category键:'category': Hostel, -
将新的
security/groups.xml文件添加到清单的data键:'data': [ 'security/groups.xml', ], -
将数据记录的新 XML 文件添加到
security/groups.xml文件中,从空结构开始:<?xml version="1.0" encoding="utf-8"?> <odoo> <!-- Add step 4 goes here --> </odoo> -
在数据 XML 元素内部添加两个新组的
<record>标签:<record id="group_hostel_user" model="res.groups"> <field name="name">User</field> <field name="category_id" ref="base.module_category_hostel"/> <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/> </record> <record id="group_hostel_manager" model="res.groups"> <field name="name">Manager</field> <field name="category_id" ref="base.module_category_hostel"/> <field name="implied_ids" eval="[(4, ref('group_hostel_user'))]"/> <field name="users" eval="[(4, ref('base.user_admin'))]"/> </record>
如果我们升级附加模块,这两个记录将被加载。要在 UI 中看到这些组,您需要激活开发者模式。然后您可以通过设置 | 用户 | 组菜单选项看到它们,如下所示:

图 10.1 – 新增的安全组
重要信息
当您添加一个新的模型时,管理员用户没有对该模型的访问权限。这意味着管理员用户无法看到为该模型添加的菜单和视图。要显示它,您必须首先向该模型添加访问规则,我们将在“向模型添加安全访问”配方中完成此操作。请注意,您可以用超级用户身份访问新添加的模型;有关更多信息,请参阅第三章,“以超级用户身份访问 Odoo”配方,创建 Odoo 附加模块。
它是如何工作的...
附加模块被组织成功能区域,或主要的应用程序,例如会计和财务、销售或人力资源。这些由清单文件中的category键定义。
如果类别名称尚不存在,它将被自动创建。为了方便,还会为新的类别名称生成一个base.module_category_<category_name_in_manifest> XML ID,将空格替换为下划线。这对于将安全组与应用程序类别相关联很有用。
在我们的示例中,我们使用了base.module_category_hostel XML 标识符。
按照惯例,包含安全相关元素的数据文件应放置在security子目录中。
清单文件还必须用于注册安全文件。在模块清单的data键中指定文件的顺序至关重要,因为您不能在其他视图或ACL文件中使用对安全组的引用,直到该组被创建。建议将安全数据文件放在第一位,然后是 ACL 文件和其他用户界面数据文件。
在我们的示例中,我们使用<record>标签创建了组,这将创建res.groups模型的记录。res.group模型最重要的列如下:
-
name: 这是该组的显示名称。 -
category_id: 这是对应用程序类别的引用,并用于在用户表单中组织组。 -
implied_ids: 这些是从中继承权限的其他组。 -
users: 这是属于此组的用户列表。在新增的附加模块中,我们通常希望管理员用户属于应用程序的管理员组。
第一个安全组使用implied_ids作为base.group_user组。这是Employee用户组,并且是所有后端用户预期共享的基本安全组。
第二个安全组在users字段上设置值,将其分配给具有base.user_admin XML ID 的管理员用户。
属于安全组的用户将自动属于其暗示的组。例如,如果您将宿舍管理员组分配给任何用户,该用户也将包括在用户组中。这是因为宿舍管理员组在其implied_ids列中包含用户组。
此外,安全组的访问权限是累积的。如果用户所属的任何组(直接或间接)授予他们权限,则用户具有权限。
一些安全组在用户表单中以选择框的形式显示,而不是单独的复选框。这种情况发生在涉及到的组属于同一应用类别,并且通过implied_ids线性互联时。例如,组 A 暗示组 B,而组 B 暗示组 C。如果一个组没有通过implied_ids与其他组关联,则会出现复选框而不是选择框。
注意
注意,前面字段中定义的关系也有反向关系,可以在相关模型中编辑,例如安全组和用户。
使用相关记录的 XML ID 和一些特殊语法可以在引用字段上设置值,例如category_id和implied_ids。这种语法在第六章,管理模块数据中详细解释。
更多...
特殊的base.group_no_one安全组标志Technical Features被激活。从版本 9.0 开始,这一变化使得只要开发者模式处于激活状态,这些功能就会可见。
安全组只提供累积的访问权限。没有方法可以拒绝一个组的访问。这意味着用于自定义权限的手动建立的组应继承自权限更少的最近组(如果有),然后添加所有剩余的所需权限。
组还有以下这些额外的字段可用:
-
menu_access字段):这些是组可以访问的菜单项 -
view_access字段):这些是组可以访问的 UI 视图 -
model_access字段):这是对模型的访问权限,如向模型添加安全访问权限配方中详细说明 -
rule_groups字段):这些是应用于组的记录级访问规则,如使用记录规则限制记录访问配方中详细说明 -
comment字段):这是对组的描述或注释文本
有了这些,我们已经学会了如何构建安全组并通过 GUI 分配它们。在接下来的几个配方中,我们将利用这些组来建立访问控制列表和记录规则。
参见
要了解如何通过 超级用户 访问新添加的模型,请参阅 第三章 中的 作为超级用户访问 Odoo 菜谱,创建 Odoo 附加模块。
为模型添加安全访问权限
对于附加模块添加新模型是很常见的。例如,在 第三章 的 创建 Odoo 附加模块 中,我们添加了一个新的宿舍模型。在开发过程中,很容易忽略为新模型创建安全访问权限,你可能会发现很难看到创建的菜单和视图。这是因为,从 Odoo 版本 12 开始,管理员用户默认没有对新模型的访问权限。要查看新模型的视图和菜单,你需要添加安全 ACLs。
然而,没有 ACLs 的模型在加载时将触发一个警告日志消息,通知用户缺少 ACL 定义:
WARNING The model hostel.hostel has no access rules, consider adding one example, access_hostel_hostel, access_hostel_hostel, model_hostel_hostel, base.group_user,1,0,0,0
你也可以以超级用户身份访问新上传的模型,这绕过了所有安全要求。有关更多信息,请参阅 第三章 中的 作为超级用户访问 Odoo 菜谱,创建 Odoo 附加模块。管理员可以访问超级用户功能。因此,为了使非管理员用户可以使用新模型,我们必须建立它们的访问控制列表,以便 Odoo 了解如何访问它们以及每个用户组被允许执行的活动。
准备工作
我们将继续使用之前教程中的 my_hostel 模块,并为其添加缺失的访问控制列表(ACLs)。
如何操作...
my_hostel 应该已经包含创建 hostel.hostel 模型的 models/hostel.py Python 文件。现在我们将通过以下步骤添加一个描述此模型安全访问控制的数据文件:
-
编辑
__manifest__.py文件以声明一个新的数据文件:data: [ # ...Security Groups 'security/ir.model.access.csv', # ...Other data files ] -
在模块中添加一个新的
security/ir.model.access.csv文件,包含以下行:id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink acl_hostel,hostel_hostel_default,model_hostel_hostel,base_group_user,1,0,0,0 acl_hostel_manager,hostel_manager,model_hostel_hostel,group_hostel_manager,1,1,1,1
然后,我们应该升级模块,以便将这些 ACL 记录添加到我们的 Odoo 数据库中。更重要的是,如果我们使用 demo 用户登录演示数据库,我们应该能够访问 我的宿舍 菜单选项而不会收到任何安全错误。
它是如何工作的...
安全 ACLs 存储在核心 ir.model.access 模型中。我们只需要添加描述每个用户组预期访问权限的记录。
任何类型的数据文件都可以,尽管最流行的是 CSV 文件。文件可以放置在附加模块目录中的任何位置;然而,通常会将所有与 安全 相关的文件保存在一个安全子文件夹下。
这个新的数据文件在教程的第一阶段添加到清单中。下一步是包含解释安全访问控制规则的文件。CSV 文件必须以将要导入条目的模型命名,所以我们选择的名称不仅仅是一种约定;这是必需的。有关更多信息,请参阅第六章,管理模块数据。
如果模块还创建了新的安全组,其数据文件应在 ACLs 数据文件之前在清单中声明,因为您可能希望将其用于 ACLs。它们必须在处理 ACL 文件之前已经创建。
CSV 文件中的列如下:
-
id:这是此规则的内部 XML ID 标识。模块内的任何唯一名称都是可接受的,但最佳实践是使用access_<model>_<group>。 -
name:这是访问规则的标题。使用access.<model>.<group>名称是一种常见的做法。 -
model_id:id:这是模型的 XML ID。Odoo 自动为具有model_<name>格式的模型分配此类 ID,使用模型的重音符_name而不是点。如果模型是在不同的附加模块中创建的,则需要一个包含模块名称的完全限定 XML ID。 -
group_id:id:这是用户组的 XML ID。如果为空,则适用于所有用户。基础模块提供了一些基本组,例如base.group_user适用于所有员工和base.group_system适用于管理员用户。其他应用程序可以添加它们自己的用户组。 -
perm_read:前一个组的成员可以读取模型的记录。它接受两个值:0或1。使用0来限制模型上的读访问,使用1来提供读访问。 -
perm_write:前一个组的成员可以更新模型的记录。它接受两个值:0或1。使用0来限制模型上的写访问,使用1来提供写访问。 -
perm_create:前一个组的成员可以添加此模型的新记录。它接受两个值:0或1。使用0来限制模型上的创建访问,使用1来提供创建访问。 -
perm_unlink:前一个组的成员可以删除此模型的记录。它接受两个值:0或1。使用0来限制模型上的解除链接访问,使用1来提供解除链接访问。
我们使用的 CSV 文件为员工 | Employee标准安全组添加了只读访问权限,并为管理 | 设置组提供了完全写访问权限。
base.group_user 特别重要,因为 Odoo 标准应用添加的用户组都继承自它。这意味着,如果我们需要一个新的模型对所有后端用户都可用,无论他们使用的是哪个具体的应用程序,我们应该将这个权限添加到员工组。
base.group_user尤其重要,因为它继承自 Odoo 标准应用引入的用户组。这意味着,如果我们想让新的模型对所有后端用户可访问,而不管他们使用的是哪个应用,我们需要将其添加到员工组。
在调试模式下,您可以通过导航到设置 | 技术 | 安全 | 访问控制列表来查看生成的 ACLs,如下面的截图所示:

图 10.2 – ACL 列表视图
有些人发现使用此用户界面定义 ACLs 然后使用导出功能生成 CSV 文件更容易。
更多...
提供这种访问权限给在创建安全组和将它们分配给用户配方中指定的 Hostel 用户和 Hostel Manager 组似乎是合理的。如果您已经完成了那个课程,那么在将组身份更改为 Hostel 身份的同时完成这个练习将是一个很好的练习。
记住,附加模块提供的访问列表不应直接进行自定义,因为它们将在下一个模块更新时重新加载,从而擦除通过 GUI 所做的任何修改。
有两种方法可以自定义 ACLs。一种选项是构建新的继承自模块的安全组并添加额外的权限,但这只能让我们添加权限而不能删除它们。更灵活的方法是取消勾选<field name="active" />列。我们还可以创建新的 ACL 行来添加或修改权限。在模块更新后,停用的 ACLs 将不会被恢复,新插入的 ACL 行也不会受到影响。
值得注意的是,访问控制列表(ACLs)仅适用于传统模型,对于抽象或瞬态模型则不是必需的。如果这些模型被定义,它们将被忽略,并在服务器日志中记录一条警告信息。
参见
由于绕过了所有安全规则,您也可以通过超级用户访问新添加的模型。有关更多信息,请参阅第三章中的作为超级用户访问 Odoo配方,创建 Odoo 附加模块。
限制模型中字段的访问
在其他情况下,我们可能需要额外的细粒度访问控制,以及限制对模型中单个字段访问的能力。
使用groups属性,可以限制对字段的访问,使其仅限于特定的安全组。本配方将演示如何向 Hostels 模型添加具有受限访问权限的字段。
准备工作
我们将继续使用前一个教程中的my_hostel模块。
如何操作...
要添加一个仅限于特定安全组访问权限的字段,请执行以下步骤:
-
编辑模型文件以添加字段:
is_public = fields.Boolean(groups='my_hostel.group_hostel_manager') notes = fields.Text(groups='my_hostel.group_hostel_manager') -
在 XML 文件中编辑视图以添加字段:
<field name="is_public" /> <field name="notes" />
就这样。现在,升级附加模块以使模型中的更改生效。如果你使用没有系统配置访问权限的用户登录,例如在包含演示数据的数据库中使用demo,宿舍表单将不会显示该字段。
它是如何工作的...
包含groups属性的域在确定用户是否属于属性中指定的任何安全组时会被以不同的方式处理。如果一个用户不是某个特定组的成员,Odoo 将会从 UI 中移除该字段,并限制对该字段的 ORM 操作。
注意,这种安全性并非表面化。字段不仅在 UI 中被隐藏,在read和write等其他 ORM 操作中也不对用户可用。对于XML-RPC或JSON-RPC调用也是如此。
在使用这些字段在业务逻辑或 UI 事件变更(@api.onchange方法)时请小心;它们可能会对没有访问权限的用户引发错误。一个解决方案是使用权限提升,例如sudo()模型方法或计算字段的compute_sudo字段属性。
groups值是一个包含逗号分隔的有效 XML ID 列表的字符串,用于安全组。找到特定组的 XML ID 的最简单方法是激活开发者模式,导航到该组的表单,在设置 | 用户 | 组,然后从调试菜单中访问查看元数据选项,如图下所示:

图 10.3 – 查看组 XML ID 的菜单
你也可以通过利用形成组的<record>标签通过代码查看安全组的 XML ID。然而,查看信息,如图下所示,是找出组 XML ID 的最简单方法。
更多...
在某些情况下,我们希望字段根据特定要求(如字段中的值,如stage_id或state)可用或不可用。通常,这通过利用状态或属性等特性在视图级别处理,根据特定标准动态显示或隐藏字段。对于更完整的解释,请参阅第九章,后端视图。
注意,这些技术仅在用户界面级别上工作,并不提供实际的安全访问。为了做到这一点,你应该在业务逻辑层添加检查。要么添加带有@constrains装饰器的模型方法,实现特定的验证,要么扩展create、write或unlink方法来添加验证逻辑。你可以通过回到第五章,基本服务器端开发,来获取更多关于如何做到这一点的见解。
参见
请参阅第九章,后端视图,以获取有关如何使用标准隐藏和显示字段的更多信息。
要深入了解业务逻辑层,请参阅第五章,基本 服务器端开发。
使用记录规则限制记录访问
每个应用程序的基本要求是能够限制在特定模型上向每个用户暴露哪些记录。
这是通过使用记录规则来实现的。记录规则是在模型上指定的一个域过滤器表达式,随后应用于受影响的用户执行的所有数据查询。
例如,我们将向Hostel模型添加一个记录规则,以便Employee组中的用户只能访问公共宿舍。
准备工作
我们将继续使用之前配方中的my_hostel模块。
如何实现...
可以通过使用数据 XML 文件来添加记录规则。为此,请执行以下步骤:
-
确保由清单
data键引用security/security_rules.xml文件:'data': [ 'security/security_rules.xml', # ... ], -
我们应该有一个包含创建安全组的
<odoo>部分的security/security_rules.xml数据文件:

图 10.4 – 宿舍用户的记录规则
升级附加模块将在 Odoo 实例中加载记录规则。如果你正在使用演示数据,你可以通过默认的demo用户来测试它,为演示用户赋予宿舍用户权限。如果你没有使用演示数据,你可以创建一个新的用户并赋予其宿舍用户权限。
工作原理...
记录规则只是放置在ir.rule核心模型中的数据记录。虽然包含它们的文件可以位于模块中的任何位置,但security子文件夹是首选位置。通常包含安全组和记录规则的单一 XML 文件。
与组不同,标准模块中的记录规则被导入具有noupdate="1"属性的odoo部分。因为某些记录在模块更新后不会被重新加载,所以手动自定义它们是安全的,并且可以在进一步的升级中幸存。
为了与标准模块保持一致,我们的记录规则也应该包含在<odoo> noupdate="1">部分中。
记录规则可以通过设置| 技术 | 安全 | 记录规则菜单选项在 GUI 中查看,如下面的截图所示:

图 10.5 – 宿舍模型的 ACLs
在此示例中使用了以下最重要的记录规则字段:
-
name): 规则的描述性标题。 -
model_id): 规则应用的模型的引用。 -
groups): 受规则影响的权限组。如果没有提及权限组,则规则被认为是全局的,并且执行方式不同(继续阅读以了解更多关于这些组的信息)。 -
domain): 用于过滤记录的域表达式。规则仅适用于这些过滤记录。
我们创建的第一个记录规则是为Hostel User安全组。它使用[('is_public', '=', True)]域表达式来选择仅公开可用的宿舍。因此,具有Hostel User安全组的用户将只能看到公共宿舍。
注意
记录规则中使用的域表达式是在服务器上使用 ORM 对象执行的。因此,可以在左侧的字段(第一个元组成员)上使用点表示法。例如,[('country_id.code', '=', 'IN')]域表达式将仅返回包含印度国家的条目。
由于记录规则主要基于当前用户,您可以在域的右侧(第三个元组元素)使用user记录集。因此,如果您想显示当前用户的公司的记录,您可以使用[('company_id', '=', user.company_id.id)]域。或者,如果您想显示由当前用户创建的记录,您可以使用[('user_id', '=', user.id)]域。
我们希望Hostel Manager安全组能够访问所有宿舍,无论它们是公开的还是私人的。因为它是从Hostel User组派生的,所以它将只能看到公共宿舍,直到我们干预。
非全局记录规则使用OR逻辑运算符连接;每个规则添加访问权限,永远不会移除此访问权限。为了使Hostel Manager安全组能够访问所有宿舍,我们必须向其中添加一个记录规则,以便它可以添加对所有宿舍的访问权限,如下所示:
[('is_public', 'in', [True, False])]
我们选择在这里以不同的方式操作,并使用[(1, '=', 1)]特殊规则来无条件地给予对所有宿舍记录的访问权限。虽然这看起来可能是多余的,但请记住,如果我们不这样做,Hostel User规则可以被定制,从而让某些宿舍对设置用户不可达。域是特殊的,因为域元组的第一个元素必须是字段名;这种情况是两种情况之一,其中这不是真的。[(1, '=', 0)]的特殊域永远不会为真,但在记录规则的情况下也不是非常有用。这是因为此类规则用于限制对所有记录的访问。同样的事情也可以通过访问列表实现。
重要信息
如果您已激活SUPERUSER模式,则记录规则将被忽略。在测试您的记录规则时,请确保您使用另一个用户进行测试。
还有更多...
当记录规则未分配给安全组时,它被标记为全局,并且与其他规则的处理方式不同。
AND运算符。它们用于标准模块中创建多公司安全访问,以便每个用户只能看到他们自己业务的数据。
总结来说,标准非全局记录规则与 OR 运算符结合,如果任何规则授予访问权限,则记录可访问。当使用 AND 运算符时,全局记录规则会对传统记录规则提供的访问权限添加限制。常规记录规则不能覆盖全局记录规则施加的限制。
使用安全组激活功能
一些功能可以通过安全组进行限制,以便只有属于这些组的人才能访问。安全组可以继承其他组,从而授予它们权限。
这两个功能用于在 Odoo 中提供功能切换功能。安全组也可以用来为某些用户或整个 Odoo 实例激活或禁用功能。
本食谱演示了如何向配置设置中添加选项,并展示了启用额外功能的两种方法:通过安全组使其可见或通过安装附加模块添加它们。
对于第一种情况,我们将使宿舍开始日期成为一个可选的附加功能;对于第二种情况,例如,我们将提供一个安装 笔记 模块的选项。
准备工作
本教程使用 my_hostel 模块,该模块在 第三章,创建 Odoo 附加模块 中进行了描述。我们将需要安全组来工作,因此您也需要遵循本章中的 向模型添加安全访问 食谱。
在本食谱中,一些标识符需要引用附加模块的技术名称。我们将假设这是 my_hostel。如果您使用的是不同的名称,请将 my_hostel 替换为您的附加模块的实际技术名称。
如何操作...
要添加配置选项,请按照以下步骤操作:
-
要添加必要的依赖项和新 XML 数据文件,请像这样编辑
__manifest__.py文件并确保它依赖于base_setup:{ 'name': 'Cookbook code', 'category': 'Hostel', 'depends': ['base_setup'], 'data': [ 'security/ir.model.access.csv', 'security/groups.xml', 'views/hostel_hostel.xml', 'views/res_config_settings.xml', ], } -
要添加用于功能激活的新安全组,请编辑
security/groups.xml文件并添加以下记录:<record id="group_start_date" model="res.groups"> <field name="name">Hostel: Start date feature</field> <field name="category_id" ref="base.module_category_hidden" /> </record> -
要使宿舍开始日期仅在启用此选项时可见,请编辑
models/hostel.py文件中的字段定义:class HostelHostel(models.Model): # ... date_start = fields.Date( 'Start Date', groups='my_hostel.group_start_date', ) -
编辑
models/__init__.py文件以添加一个新的 Python 文件用于配置设置模型:from . import hostel from . import res_config_settings -
要通过添加新选项来扩展核心配置向导,请添加包含以下代码的
models/res_config_settings.py文件:from odoo import models, fields class ConfigSettings(models.TransientModel): _inherit = 'res.config.settings' group_start_date = fields.Boolean( "Manage Hostel Start dates", group='base.group_user', implied_group='my_hostel.group_start_dates', ) module_note = fields.Boolean("Install Notes app") -
要在用户界面中提供这些选项,请添加
views/res_config_settings.xml,它扩展了设置表单视图:<?xml version="1.0" encoding="utf-8"?> <odoo> <record id="view_general_config_hostel" model="ir.ui.view"> <field name="name">Configuration: add Hostel options</field> <field name="model">res.config.settings</field> <field name="inherit_id" ref="base_setup.res_config_settings_view_form" /> <field name="arch" type="xml"> <div id="business_documents" position="before"> <h2>Hostel</h2> <div class="row mt16 o_settings_container"> <!-- Add Step 7 and 8 goes here --> </div> </div> </field> </record> </odoo> -
在设置表单视图中,添加添加开始日期功能的选项:
<!-- Start Dates option --> <div class="col-12 col-lg-6 o_setting_box"> <div class="o_setting_left_pane"> <field name="group_start_date" class="oe_inline"/> </div> <div class="o_setting_right_pane"> <label for="group_start_date"/> <div class="text-muted"> Enable Start date feature on hostels </div> </div> </div> -
在设置表单视图中,添加安装笔记模块的选项:
<!-- Note module option --> <div class="col-12 col-lg-6 o_setting_box"> <div class="o_setting_left_pane"> <field name="module_note" class="oe_inline"/> </div> <div class="o_setting_right_pane"> <label for="module_note"/> <div class="text-muted"> Install note module </div> </div> </div>
在升级附加模块后,两个新的配置选项应在 设置 | 常规设置 下可用。屏幕应如下所示:

图 10.6 – 常规设置中的宿舍配置
如前一个截图所示,您将在宿舍部分看到新的设置。第一个选项,管理宿舍开始日期,将为宿舍记录启用开始日期功能。第二个选项,安装笔记应用,将安装 Odoo 的笔记应用。
它是如何工作的...
核心的base模块提供了res.config.settings模型,该模型提供了激活选项背后的业务逻辑。base_setup附加模块使用res.config.settings模型提供一些基本配置选项,这些选项可以在新数据库中提供。它还使设置 | 常规设置菜单可用。
base_setup模块将res.config.settings适配到中央管理仪表板,因此我们需要扩展它以添加配置设置。
如果我们决定为宿舍应用创建一个特定的设置表单,我们仍然可以从res.config.settings模型继承,使用不同的_name,然后为新的模型提供菜单选项和表单视图,仅针对这些设置。我们已经在第八章的添加自己的设置选项食谱中看到了这种方法,高级服务器端开发技术。
我们通过两种方式激活了这些功能:通过激活一个安全组并使功能对用户可见,以及通过安装一个提供此功能的附加模块。基本的res.config.settings模型提供了处理这两种情况所需的逻辑。
本教程的第一步是将base_setup附加模块添加到依赖项中,因为它为我们想要使用的res.config.settings模型提供了扩展。它还添加了一个额外的 XML 数据文件,我们将需要将其添加到常规 设置表单中。
在第二步中,我们创建了一个新的安全组,宿舍:开始日期功能。需要激活的功能应该只对该组可见,因此它将在该组启用之前隐藏。
在我们的示例中,我们希望宿舍开始日期仅在相应的配置选项启用时才可用。为了实现这一点,我们可以使用字段的groups属性,使其仅对这一安全组可用。我们在模型级别做了这件事,以便它自动应用于所有使用该字段的 UI 视图。
最后,我们扩展了res.config.settings模型以添加新的选项。每个选项都是一个布尔字段,其名称必须以group_或module_开头,根据我们希望它执行的操作。
group_选项字段应该有一个implied_group属性,并且应该是一个包含逗号分隔的安全组 XML ID 列表的字符串,当它启用时将激活这些安全组。XML ID 必须是完整的,包括模块名称、点以及标识符名称;例如,module_name.identifier。
我们还可以提供一个 group 属性来指定哪些安全组将启用该功能。如果没有定义任何组,它将为所有基于员工的组启用。因此,相关的组不会应用于门户安全组,因为这些组不像其他常规安全组那样从员工基本安全组继承。
激活背后的机制相当简单:它将 group 属性中的安全组添加到 implied_group 中,从而使相关的功能对相应的用户可见。
module_ 选项字段不需要任何额外的属性。字段名剩余部分标识了当此选项被激活时将安装的模块。在我们的示例中,module_note 将安装 Note 模块。
重要信息
取消勾选将不会警告即卸载模块,这可能导致数据丢失(模型、字段和模块数据将被删除作为后果)。为了避免意外取消勾选,secure_uninstall 社区模块(来自 github.com/OCA/server-tools)在用户卸载附加模块之前会提示用户输入密码。
还有更多...
配置设置也可以有以 default_ 前缀命名的字段。当其中之一有值时,ORM 将将其设置为全局默认值。settings 字段应该有一个 default_model 属性来标识受影响的模型,并且 default_ 前缀后面的字段名标识了将设置默认值的 model 字段。
此外,没有提到这三个前缀之一的字段可以用于其他设置,但您需要实现填充它们值的逻辑,使用以 get_default_ 命名的前缀方法,并使用以 set_ 命名的前缀方法在它们的值被编辑时执行操作。
对于那些想深入了解配置设置细节的人来说,请查看 Odoo 的源代码在 ./odoo/addons/base/models/res_config.py,那里有大量的注释。
以超级用户身份访问记录集
在之前的菜谱中,我们探讨了包括访问规则、安全组和记录规则在内的安全策略。您可以通过这些方法避免未经授权的访问。然而,在某些复杂的企业场景中,您可能需要查看或编辑记录,即使用户没有访问权限。例如,假设公共用户没有访问线索记录的权限,但用户可能通过提交网站表单在后台生成线索记录。
您可以通过使用 sudo() 以超级用户身份访问记录集。我们在 第八章 的 高级服务器端开发技术 菜谱中介绍了 sudo()。在这里,我们将看到即使您已设置 ACL 规则或将安全组分配给字段,您仍然可以使用 sudo() 获取访问权限。
如何操作...
我们将使用之前教程中的相同my_hostel模块。我们已经有了一个只读访问权限的 ALC 规则,适用于普通用户。我们将添加一个新字段,并使用安全组,以便只有管理员用户可以访问它。之后,我们将修改普通用户的字段值。按照以下步骤实现:
-
将新字段添加到
hostel.hostel模型:details_added = fields.Text( string="Details", groups='my_hostel.group_hostel_manager') -
将字段添加到表单视图:
<field name="details_added"/> -
将
add_details()方法添加到hostel.hostel模型:def add_details(self): self.ensure_one() message = "Details are(added by: %s)" % self.env.user.name self.sudo().write({ 'details_added': message }) -
将按钮添加到表单视图,以便我们可以从用户界面触发我们的方法。这应该放在
<header>标签内部:<button name="add_details" string="Add Details" type="object"/>
重新启动服务器并更新模块以应用这些更改。
它是如何工作的...
在步骤 1和步骤 2中,我们向模型和表单视图添加了一个名为details_added的新字段。请注意,我们在 Python 中将my_hostel.group_hostel_manager组放在字段上,因此此字段只能由管理员用户访问。
在下一步中,我们添加了add_details()方法。我们更新了此方法体内details_added()字段的值。请注意,我们在调用写入方法之前使用了sudo()。
最后,我们在表单视图中添加了一个按钮,用于从用户界面触发该方法。
为了测试这个实现,您需要使用非管理员用户登录。如果您已经用演示数据加载了数据库,您可以使用演示用户登录,然后点击add_details()方法将被调用,这将把消息写入details_added字段,即使用户没有适当的权限。您可以通过管理员用户检查字段的值,因为此字段将从演示用户那里隐藏。
当点击add_details()方法作为参数时,使用self。在我们将值写入宿舍记录集之前,我们使用了self.sudo()。这返回相同的记录集,但具有超级用户权限。这个记录集将具有su=True环境属性,并且将绕过所有访问规则和记录规则。正因为如此,非管理员用户将能够写入宿舍记录。
更多...
当您使用sudo()时需要格外小心,因为它绕过了所有访问权限。如果您想以其他用户的身份访问记录集,可以在sudo内部传递该用户的 ID – 例如,self.sudo(uid)。这将返回包含该用户环境的记录集。这样,它将不会绕过所有访问规则和记录规则,但您可以执行该用户允许的所有操作。
根据组隐藏视图元素和菜单
在之前的菜谱中,我们介绍了如何使用 Python 字段声明中的组参数来隐藏某些用户的字段。在用户界面中隐藏字段的另一种方法是向视图规范中的 XML 元素添加安全组。您还可以通过使用菜单来隐藏特定用户的安全组。
准备工作
对于这个菜谱,我们将重用前一个菜谱中的 my_hostel 扩展模块。在前一个菜谱中,我们向 <header> 标签添加了一个按钮。我们将通过向其添加组属性来隐藏几个用户的整个头部。
添加 hostel.room.category 模型的模型、视图和菜单。我们将隐藏对用户的类别菜单。请参阅 第四章 应用模型,了解如何添加模型视图和菜单。
如何操作...
按照以下步骤根据安全组隐藏元素:
-
向
<header>标签添加groups属性以隐藏它对其他用户:... <header groups="my_hostel.group_hostel_manager"> ... -
向
<menuitem>宿舍类别添加groups属性,以便它仅对图书管理员用户显示:<menuitem name="Hostel Room Categories" id="hostel_room_category_menu" parent="hostel_base_menu" action="hostel_room_category_action" groups="my_hostel.group_hostel_manager"/>
重新启动服务器并更新模块以应用这些更改。
它是如何工作的...
在 步骤 1 中,我们将 groups="my_hostel.group_hostel_manager" 添加到 <header> 标签中。这意味着整个头部部分将仅对宿舍用户和宿舍管理员可见。没有 group_hostel_manager 的普通后端用户将看不到头部部分。
在 步骤 2 中,我们将 groups="my_hostel.group_hostel_manager" 属性添加到 menuitem。这意味着此菜单仅对宿舍用户可见。
您几乎可以在任何地方使用 groups 属性,包括 <field>、<notebook>、<group> 和 <menuitems>,或者在任何视图架构的标签上。如果用户没有那个组,Odoo 将隐藏这些元素。您可以在网页和 QWeb 报告 中使用相同的组属性,这些将在 第十二章 自动化、工作流、电子邮件和打印 和 第十四章 CMS 网站开发 中介绍。
如我们在本章的 以超级用户访问记录集 菜谱中看到的,我们可以通过在 Python 字段定义中使用 groups 参数来隐藏某些用户的部分字段。请注意,在字段上使用安全组和在视图中使用 Python 安全组之间存在很大差异。Python 中的安全组提供真正的安全性;未经授权的用户甚至无法通过 ORM 或 RPC 调用来访问字段。然而,视图中的组只是为了提高可用性。通过 XML 文件中的组隐藏的字段仍然可以通过 RPC 或 ORM 访问。
参见
请参阅 第四章 应用模型,了解如何添加模型视图和菜单。
第十一章:国际化
Odoo 支持多种语言,并允许用户使用他们最舒适的语言。内置的 Odoo i18n 功能有助于实现这一点。通过字符串翻译,Odoo 还支持日期和时间格式。
在本章中,您将了解如何将翻译文件上传到您的模块并启用各种语言。由于国家多样性和本地语言的普遍性,用户通常发现当系统以他们的母语呈现时更容易与之连接。为了适应这一点,Odoo 提供了一种功能,可以将软件文本翻译成用户的偏好语言。这一功能通过确保界面对各种语言背景的个人都是可访问和可理解的,从而提高了软件在不同地区和人口统计学中的采用率和可用性。利用这些新功能将增强 Odoo 用户体验。
本章将涵盖以下食谱:
-
设置语言安装和用户偏好设置
-
设置与语言相关的选项
-
使用网络客户端用户界面进行文本翻译
-
将翻译导出为文件
-
使用
gettext工具使翻译更容易 -
将翻译文件导入 Odoo
-
修改网站的定制语言 URL 代码
许多这些食谱可以从网络客户端用户界面或从命令行完成。 wherever possible,我们将了解如何使用这两种选项。Odoo 使用 Transifex(Odoo)和 Weblate (OCA)翻译平台。
设置语言安装和用户偏好设置
Odoo 可以本地化以适应各种语言和地区设置,包括日期和数字格式。
初始安装的唯一语言是标准英语语言。我们需要安装各种地区和语言,以便人们可以使用它们。本食谱描述了如何实现用户偏好,以及如何设置它们。
如何操作...
激活开发者模式并按照以下步骤在 Odoo 实例中安装新语言:
- 前往设置 | 通用设置 | 语言。在这里,您将看到添加语言链接,如以下截图所示。点击该链接;将打开一个对话框,您可以在其中加载语言:

图 11.1 – 通用设置中的语言选项
- 选择您想要加载的语言:

图 11.2 – 加载语言的对话框
- 点击添加将加载所选语言,并将确认对话框打开,如下所示:

图 11.3 – 显示已加载语言的对话框
-
新语言也可以从命令行安装。前述步骤的等效命令如下:
$ ./odoo-bin -d mydb --load-language=es_ES -
要设置用户使用的语言,请转到设置 | 用户与公司 | 用户,然后在用户表单的偏好选项卡中设置语言字段值:

图 11.4 – 设置语言的用户表单
通过偏好菜单项,用户可以轻松地自行更改这些变量。他们可以通过点击网页客户端窗口右上角的用户名来访问它:

图 11.5 – 设置语言的偏好选项
它是如何工作的...
用户可以有自己的语言和时区偏好。语言设置用于将用户界面文本翻译成所选语言,并应用于浮点数和货币字段的地方惯例。
在用户可以选择语言之前,必须使用添加语言选项安装该语言。可以通过转到开发者模式下的设置 | 翻译 | 语言菜单选项来查看可用的语言列表。带有活动标志设置的语言已安装。
每个 Odoo 附加模块都负责提供翻译资源,这些资源应放置在i18n子目录中。每种语言的数据应在一个.po文件中。在我们的例子中,西班牙语的翻译数据是从es_ES.po数据文件中加载的。
Odoo 还支持es.po文件的概念,用于西班牙语,以及es_MX.po文件,用于墨西哥西班牙语,然后es.po被检测为es_MX.po的基础语言。当安装墨西哥西班牙语时,将加载两个数据文件;首先加载基础语言的一个,然后是特定语言的一个。因此,在我们的情况下,墨西哥西班牙语的翻译文件只需包含该语言变体独有的字符串。
i18n子目录还应该有一个<module_name>.pot文件,提供翻译模板并包含所有可翻译的字符串。本章的将翻译字符串导出到文件配方解释了如何导出可翻译字符串以生成此文件。
在 Odoo 的早期版本中,当安装了额外的语言时,相应的资源会从所有已安装的附加模块中加载,并存储在翻译术语模型中。其数据可以在设置 | 翻译 | 应用程序术语 | 翻译术语菜单选项中查看(并编辑)(注意,此菜单仅在开发者模式下可见)。
从 Odoo 版本 17 开始,您将无法找到此菜单,因为翻译术语现在作为本地术语存储。现在任何可翻译的字段都存储表示所有翻译语言值的 JSON 数据。例如,产品名称的翻译现在直接存储在name字段中。翻译过程没有改变——您只是无法看到带有所有已翻译术语列表的设置 | 翻译 | 应用程序术语 | 已翻译术语菜单项。
当安装新的附加模块或升级现有附加模块时,也会加载已安装语言的翻译文件。
还有更多...
通过再次选择语言旁边的刷新符号,可以在不升级附加模块的情况下刷新翻译文件。如果您更改了翻译文件,但不想处理更新模块(及其所有依赖项),则可以这样做。
如果覆盖现有术语复选框留空,则只会加载新翻译的字符串。因此,更改后的翻译字符串不会被加载。如果您想同时加载现有翻译并覆盖当前加载的翻译,请勾选该框。请注意,如果有人通过界面手动更改翻译,这可能会引起潜在问题。
覆盖现有术语复选框存在,因为我们可以通过转到设置 | 翻译 | 应用程序术语 | 已翻译术语菜单项或使用调试菜单中的技术翻译快捷选项来编辑特定的翻译。以这种方式添加或修改的翻译不会覆盖,除非通过启用覆盖现有术语复选框重新加载语言。
了解附加模块也可以有一个i18n_extra子目录,其中包含额外的翻译,这可能很有用。首先,下载i18n子目录中的.po文件。然后,Odoo ORM 下载基本语言的文件,然后是语言变体的文件。随后,下载i18n_extra子目录中的.po文件,首先是基本语言,然后是语言变体。最终加载的字符串翻译是最终具有优先级的翻译。
设置与语言相关的选项
只要用户使用正确的语言,区域设置就应该正确,因为它们带有合适的默认值。
尽管如此,您可能仍然想要更改语言的设置。例如,您可以选择在更改美国日期和数字格式以更好地满足您的需求的同时,使用用户界面默认的英语语言设置。
此外,日期和数字格式等区域设置由语言及其变体(例如es_MX代表墨西哥西班牙语)提供。
准备工作
我们需要开启开发者模式。如果之前尚未启用,请按照第一章中安装 Odoo 开发环境的激活 Odoo 开发者工具配方中描述的方式进行操作。
如何操作...
按照以下步骤更改语言的区域设置:
- 选择设置 | 翻译 | 语言菜单选项以查看已安装的语言及其选项。当您点击一个已安装的语言时,将打开一个包含必要选项的表单:

图 11.6 – 配置语言设置的表单
- 编辑语言设置。要将日期更改为 ISO 格式,将
%Y-%m-%d更改为。要将数字格式更改为使用逗号作为小数分隔符,相应地修改小数分隔符和千位分隔符字段。
它是如何工作的...
用户语言在用户偏好设置中选中,并在登录和启动新的 Odoo 用户会话时放置在lang上下文键中。通过将源文本翻译成用户语言,并根据语言当前的区域设置格式化日期和数字,相应地准备输出。
更多...
服务器端进程可以修改运行动作的上下文。例如,为了获取按照美国英语格式格式化的日期的记录,独立于当前用户的语言偏好,您可以执行以下操作:
en_records = self.with_context(lang='en_US').search([])
更多详情,请参阅第八章中的调用修改上下文的方法配方,高级服务器端开发技术。
使用 Web 客户端用户界面进行文本翻译
翻译的最简单方法是使用 Web 客户端提供的翻译功能。这些翻译字符串存储在数据库中,以后可以导出为.po文件,既可以包含在附加模块中,也可以手动导入。
文本字段可以有可翻译的内容,这意味着它们的值将取决于当前用户的语言。我们还将了解如何设置这些字段的语言相关值。
准备工作
我们需要启用开发者模式。如果没有启用,请按照第一章中安装 Odoo 开发环境的激活 Odoo 开发者工具配方所示进行操作。
如何操作...
我们将通过使用用户组功能作为示例来演示如何通过 Web 客户端翻译术语:
- 导航到您想要翻译的屏幕。例如,我们将通过设置 | 用户和公司 | 组菜单项打开组视图:

图 11.7 – 组的翻译
- 在表单视图中打开其中一个组记录,然后点击编辑:

图 11.8 – 字段值的翻译
- 注意,名字 字段在右侧有一个特殊图标。这表示它是一个可翻译字段。点击此图标将打开一个包含不同已安装语言的 翻译 列表。这允许我们为这些语言中的每一个设置翻译:

图 11.9 – 字段值的翻译
工作原理...
所有翻译术语都保存在任何模式/表的名字字段中。在我们的例子中,res_groups 表;当你检查名字字段中存储的信息时,它将被保存为一个字典,其中键是语言代码,值是翻译短语:

图 11.10 – 字段值的翻译
将翻译字符串导出到文件
翻译字符串可以带或不带所选语言的翻译文本进行导出。这可以是将 i18n 数据包含在模块中,或者稍后使用文本编辑器或可能使用专用工具进行翻译。
我们将通过我们的自定义 My Hostel 模块演示如何进行此操作,因此请随意将 My Hostel 替换为您自己的模块。
准备工作
我们需要启用开发者模式。如果尚未启用,请按照在 第一章 中演示的 激活 Odoo 开发者工具 菜谱进行操作,安装 Odoo 开发环境。
如何操作...
要导出 my_hostel 模块的翻译术语,请按照以下步骤操作:
-
在网络客户端用户界面中,从 设置 顶部菜单中选择 翻译 | 导入/导出 | 导出翻译 菜单选项。
-
在
.po格式下,并且一次导出单个扩展模块 –my_hostel是 Discuss 应用程序的技术名称),在我们的例子中:

图 11.11 – 导出翻译术语的对话框
-
在 Odoo 版本 17 中,你将在导出设置中找到一个名为 导出类型 的新选项,它包含两个选项:模块 和 模型。
-
将模块类型设置为模型将提供新的选项来选择具有筛选选项的特定模型,用户可以使用该选项仅导出基于特定筛选的记录:

图 11.12 – 导出翻译术语的对话框
-
一旦导出过程完成,将显示一个新窗口,其中包含下载文件的链接和一些额外的建议。
-
要从 Odoo 命令行界面导出
my_hostel扩展模块的翻译模板文件,请输入以下命令:$ ./odoo-bin -d mydb --i18n-export=my_hostel.pot --modules=my_hostel es_ES for Spanish, for example – from the Odoo command-line interface, enter the following command:$ ./odoo-bin -d mydb --i18n-export=es_ES.po --modules=my_hostel
--language=es_ES
$ mv es_ES.po ./addons/my_hostel/i18n
工作原理...
导出翻译功能从目标模块中提取可翻译的字符串,然后创建一个包含翻译术语的文件。这可以从 Web 客户端和命令行界面完成。
当从 Web 客户端导出时,我们可以选择导出空翻译模板——即包含要翻译的字符串和空翻译的文件,或者导出一个语言,结果是一个包含要翻译的字符串和所选语言的翻译的文件。
可用的文件格式是 CSV、PO 和 TGZ。TGZ 文件格式导出一个包含<name>/i18n/目录结构的压缩文件,其中包含 PO 或 POT 文件。
CSV 格式在通过电子表格进行翻译时很有用,但在附加模块中使用的格式是 PO 文件。这些文件应放置在i18n子目录中。一旦安装了相应的语言,它们就会自动加载。在导出这些 PO 文件时,我们应该一次只导出一个模块。PO 文件也是翻译工具(如 Poedit)支持的流行格式之一。
翻译也可以通过使用--i18n-export选项直接从命令行导出。这个示例展示了如何提取模板文件和翻译语言文件。
在这个示例的步骤 4中,我们导出了一个模板文件。--i18n-export选项期望导出路径和文件名。请注意,文件扩展名必须是 CSV、PO 或 TGZ。此选项需要-d选项,它指定要使用的数据库。还需要--modules选项来指示要导出的附加模块。请注意,--stop-after-init选项不是必需的,因为export命令在完成后会自动返回到命令行。
这将导出一个模板文件。Odoo 模块期望在i18n文件夹中找到具有.pot扩展名的导出模板。在处理模块时,导出操作完成后,我们通常希望将导出的 PO 文件移动到模块的i18n目录下,并命名为<模块>.pot。
在步骤 5中,也使用了–language选项。使用它,除了空翻译文件外,还会导出所选语言的翻译术语。这个用例之一是通过使用技术翻译功能通过 Web 客户端用户界面进行一些翻译,然后导出并包含在模块中。
还有更多...
视图和模型定义中的文本字符串会自动提取以进行翻译。对于模型,提取了_description属性、字段名称(string属性)、帮助文本以及选择字段选项,以及模型约束的用户文本(_constraints和_sql_constraints)。
在 Python 或 JavaScript 代码中需要翻译的文本字符串无法自动检测,因此代码应识别这些字符串,并将它们包裹在下划线函数中。
在 Python 的模块文件中,我们应该确保文件以以下方式导入:
from odoo import _
此文件可以在任何需要可翻译文本的地方使用,如下所示:
_('Hello World')
对于使用额外上下文信息的字符串,我们应该使用 Python 字符串插值,如下所示:
_('Hello %s') % 'World'
注意,插值应该放在翻译函数外部。例如,_("Hello %s" % 'World')是错误的。字符串插值也应优先于字符串连接,以便每个界面文本都只是一个翻译字符串。
请小心处理选择字段!如果你向字段定义传递一个显式的值列表,显示的字符串将自动标记为需要翻译。另一方面,如果你传递一个返回值列表的方法,显示字符串必须显式标记为需要翻译。
关于手动翻译工作,任何文本文件编辑器都可以使用,但使用专门支持 PO 文件语法的编辑器可以简化这项工作,减少格式错误的风险。此类编辑器包括以下列表:
- POEDIT:
poedit.net/
)
-
Emacs (PO 模式):
www.gnu.org/software/gettext/manual/html_node/PO-Mode.html -
Lokalize:
l10n.kde.org/tools/ -
Gtranslator:
wiki.gnome.org/Apps/Gtranslator
)
使用 gettext 工具使翻译更容易
PO 文件格式是 Unix-like 系统中常用的gettext i18n 和本地化系统的一部分。此系统包括用于简化翻译工作的工具。
这个配方演示了如何使用这些工具来帮助我们翻译我们的附加模块。我们希望在自定义模块上使用它,所以我们在第三章,创建 Odoo 附加模块中创建的my_hostel模块是一个很好的候选者。然而,你也可以自由地用你手头的其他自定义模块替换它,并相应地替换教程中的my_hostel引用。
如何做到这一点...
假设你的 Odoo 安装位于~/odoo-work/odoo,要使用命令行管理翻译,请按照以下步骤操作:
-
为目标语言创建一个翻译术语汇编——例如,西班牙语。如果我们命名我们的汇编文件为
odoo_es.po,我们应该编写以下代码:$ cd ~/odoo-work/odoo # Use the path to your Odoo installation $ find ./ -name es_ES.po | xargs msgcat --use-first | msgattrib --translated --no-fuzzy \ -o ./odoo_es.po -
从 Odoo 命令行界面导出附加模块的翻译模板文件,并将其放置在模块预期的位置:
$ ./odoo-bin -d mydb --i18n-export=my_module.po --modules=my_module $ mv my_module.po ./addons/my_module/i18n/my_module.pot -
如果目标语言还没有可用的翻译文件,创建 PO 翻译文件,重复使用在汇编中已找到和翻译的术语:
$ msgmerge --compendium ./odoo_es.po -o ./addons/my_module/i18n/es_ES.po \ /dev/null ./addons/my_module/i18n/my_module.pot -
如果存在翻译文件,请添加汇编中可以找到的翻译:
$ mv ./addons/my_module/i18n/es_ES.po /tmp/my_module_es_old.po $ msgmerge --compendium ./odoo_es.po -o./addons/my_module/i18n/es_ES.po \ /tmp/my_module_es_old.po ./addons/my_module/i18n/my_module.pot $ rm /tmp/my_module_es_old.po -
要查看 PO 文件中的未翻译术语,请使用以下命令:
$ msgattrib --untranslated ./addons/my_module/i18n/es_ES.po -
使用你喜欢的编辑器来完成翻译。
它是如何工作的...
步骤 1 使用gettext工具箱中的命令为所选语言创建翻译汇编——在我们的例子中是西班牙语。它通过在 Odoo 代码库中查找所有es_ES.po文件,并将它们传递给msgcat命令来实现。我们使用--use-first标志来避免冲突翻译(Odoo 代码库中有一些)。结果传递给msgattrib过滤器。我们使用--translated选项来过滤未翻译条目,并使用--no-fuzzy选项来删除模糊翻译。然后我们将结果保存在odoo_es.po中。
步骤 2 使用--i18n-export选项调用odoo.py。即使配置文件和--modules选项中已指定,您仍需要在命令行上指定一个数据库,并使用逗号分隔的模块列表来导出翻译。
在gettext世界中,模糊翻译是由msgmerge命令(或其他工具)使用源字符串的邻近匹配自动创建的。我们希望在汇编中避免这些。
步骤 3 通过使用在汇编中找到的现有翻译值来创建一个新的翻译文件。使用带有--compendium选项的msgmerge命令来查找汇编文件中的msgid行,匹配在步骤 2中生成的翻译模板文件中的那些。结果保存在es_ES.po文件中。
如果您有一个包含您想要保留的翻译的现有.po文件,您应该将其重命名,并将/dev/null参数替换为该文件。重命名过程是必要的,以避免使用相同的文件作为输入和输出。
更多...
本教程仅简要介绍了 GNU gettext工具箱中可用的丰富工具。全面覆盖超出了本书的范围。如果您感兴趣,GNU gettext文档包含大量关于 PO 文件操作的有价值信息,可在www.gnu.org/software/gettext/manual/gettext.html找到。
将翻译文件导入 Odoo
加载翻译的标准方法是存储 PO 文件在模块的i18n子文件夹中。每当附加模块安装或更新时,翻译文件就会被加载,并添加额外的翻译字符串。
然而,可能存在我们想要直接导入翻译文件的情况。在本教程中,我们将学习如何从网络客户端或命令行加载翻译文件。
准备工作
我们需要激活开发者模式。如果尚未激活,请按照第一章中“激活 Odoo 开发者工具”食谱中的说明进行激活,即安装 Odoo 开发环境。我们还需要一个翻译po文件,我们将在本教程中导入它——例如,myfile.po文件。
如何操作...
要导入翻译术语,请按照以下步骤操作:
-
在网络客户端用户界面中,从设置顶部菜单,选择翻译 | 导入/导出 | 导入翻译菜单选项。
-
在导入翻译对话框中,填写语言名称和语言代码,并选择要导入的文件。最后,点击导入按钮:

图 11.13 – 导入翻译文件的对话框
-
要从 Odoo 命令行界面导入翻译文件,我们必须将其放置在服务器附加路径中,然后执行导入:
$ mv myfile.po ./addons/ $ ./odoo.py -d mydb --i18n-import="myfile.po" --lang=fr_BE
它是如何工作的...
ir.translation 表,但在 Odoo 的新版本中,该表不再存在。因此,所有针对 arch_db 的视图级别翻译,例如按钮字符串和选择字段值,都将存储在 ir_ui_view 表中,而所有字段级别翻译,例如字段标签,都将存储在 ir_model_fields 表的 field_description 字段下。
例如,我们的宿舍表有一个 room_number 模型。其“房间号”字段翻译将作为 {"en_US": "Room Number", "fr_BE": "Numéro de chambre"} 存储在数据库级别。
网络客户端功能会要求输入语言名称,但在导入过程中并不使用它。它还有一个覆盖选项。如果选中,它将强制导入所有翻译字符串,即使它们已经存在,也会在过程中覆盖它们。
在命令行中,可以使用 --i18n-import 选项进行导入。它必须提供相对于附加路径目录的文件路径;-d 和 --language(或 -l)是必需的。通过添加 --i18n-overwrite 选项到命令中,也可以实现覆盖。请注意,我们在这里没有使用 --stop-after-init 选项。因为它不是必需的,因为导入操作在完成后会停止服务器。
修改网站的定制语言 URL 代码
Odoo 还支持网站的多语言。在网站上,当前语言被识别为语言字符串。在本食谱中,你将学习如何更改 URL 中的语言代码。
准备工作
在遵循此食谱之前,请确保你已经安装了 website 模块并启用了网站的多语言功能。
如何操作...
要修改语言的 URL 代码,请按照以下步骤操作:
- 从设置 | 翻译 | 语言菜单选项打开语言列表。点击其中一个已安装的语言将打开一个看起来像这样的表单:

图 11.14 – 网站的语言 URL 代码
- 这里,你会看到URL 代码字段。设置你想要的值。确保你在这里不要添加空格或特殊字符。
配置完成后,你可以在网站上测试结果。打开主页并更改语言;你将看到 URL 中的自定义语言代码。
它是如何工作的...
Odoo 通过 URL 路径识别网站的语种。例如,www.odoo.com/fr_FR 用于法语,www.odoo.com/es_ES 用于西班牙语。在这里,URL 中的 fr_FR 和 es_ES 部分是语言 ISO 代码,Odoo 使用这些代码来检测请求的语言。但有时,你可能希望以更用户友好的方式设置语言。在这种情况下,你可以更新法语对应的 fr。在这种情况下,www.odoo.com/fr_FR 将会被转换为 www.odoo.com/fr。
注意
在生产环境中更改 URL 代码不是问题;Odoo 会自动将包含语言 ISO 代码的 URL 重定向到你的自定义 URL。
第十二章:自动化、工作流程、电子邮件和打印
预期业务应用不仅存储记录,还要管理业务工作流程。一些对象,如潜在客户或项目任务,有很多并行运行的记录。一个对象有太多的记录会使业务情况难以清晰。Odoo 有几种技术可以处理这个问题。在本章中,我们将探讨如何设置具有动态阶段和看板组的业务工作流程。这将帮助用户了解他们的业务是如何运行的。
我们还将探讨如服务器操作和自动化操作等技术,这些技术可以被高级用户或功能顾问使用,以添加更简单的流程自动化,而无需创建自定义插件。最后,我们将创建基于 QWeb 的 PDF 报告并打印出来。
在本章中,我们将涵盖以下菜谱:
-
管理动态记录阶段
-
管理看板阶段
-
向看板卡片添加快速创建表单
-
创建交互式看板卡片
-
向看板视图中添加进度条
-
创建服务器操作
-
使用 Python 代码服务器操作
-
在时间条件下使用自动化操作
-
在事件条件下使用自动化操作
-
创建基于 QWeb 的 PDF 报告
-
从看板卡片管理活动
-
向表单视图添加状态按钮
-
启用记录的存档选项
技术要求
本章的技术要求是拥有在线 Odoo 平台。
本章将使用的所有代码都可以从本书的 GitHub 仓库下载:github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter12。
管理动态记录阶段
在my_hostel中,我们有一个state字段来指示宿舍房间记录的当前状态。此state字段限制为草稿或可用状态,并且无法向业务流程中添加新状态。为了避免这种情况,我们可以使用many2one字段在用户选择看板工作流程设计时提供灵活性,并且您可以在任何时候添加/删除新状态。
准备工作
对于这个菜谱,我们将使用来自第八章,高级服务器端开发技术的my_hostel模块。此模块管理宿舍和学生。它还记录房间。我们为此书添加了一个初始模块,Chapter12/00_initial_module/my_hostel,到 GitHub 仓库以帮助您开始:github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter12。
如何操作...
按照以下简单步骤向hostel.room模型添加阶段:
-
添加一个名为
hostel.room.stage的新模型,如下所示:class HostelRoomStage(models.Model): _name = 'hostel.room.stage' _order = 'sequence,name' name = fields.Char("Name") sequence = fields.Integer("Sequence") security/ir.model.access.csv file, as follows:从
hostel.room模型中替换状态字段,并替换为新的阶段 _id 字段,这是一个 many2one 字段,以及其方法,如下面的示例所示:@api.model def _default_room_stage(self): Stage = self.env['hostel.room.stage'] state field in the form view with the stage_id field, as shown in the following example:<field name="stage_id" widget="statusbar"
options="{'clickable': '1', 'fold_field': 'fold'}"/>
在树视图中的状态字段与阶段 _id 字段一起,如下所示:
<tree string="Room"> <field name="name"/> <field name="room_no"/> <field name="floor_no"/> <field name="stage_id"/> data/room_stages.xml file. Don’t forget to add this file to the manifest, as shown in the following example:草稿 1 可用 15 预留 5 True
安装模块后,您将在表单视图中看到阶段,如下面的截图所示:

图 12.1 – 表单视图中的阶段选择器
这里,您会注意到在宿舍记录上勾画出的阶段。这些阶段是可点击的,因此您可以通过点击来更改阶段。折叠阶段将在更多下拉菜单下显示。
工作原理...
由于我们想要动态管理记录阶段,我们需要创建一个新的模型。在步骤 1中,我们创建了一个名为hostel.room.stage的新模型来存储动态阶段。在这个模型中,我们添加了一些字段。其中之一是sequence字段,用于确定阶段的顺序。我们还添加了fold布尔字段,用于折叠阶段并将它们放入下拉列表中。当您的业务流程有很多阶段时,这非常有用,因为它意味着您可以通过设置此字段来在下拉菜单中隐藏不重要的阶段。
fold字段也用于看板视图中显示折叠的看板列。通常,预留项目预计处于未展开阶段,标记为完成或取消的项目应处于折叠阶段。
默认情况下,fold是用于存储阶段折叠值的字段名称。您可以通过添加_fold_name = 'is_fold'类属性来更改此名称。
在步骤 2中,我们为新的模型添加了基本访问权限规则。
在步骤 3中,我们将stage_id many2one字段添加到hostel.room模型中。在创建新的房间记录时,我们希望将默认阶段值设置为草稿。为了完成这个任务,我们添加了_default_room_stage()方法。此方法将检索具有最低序列号的hostel.room.stage模型记录,因此,在创建新记录时,序列号最低的阶段将在表单视图中显示为活动状态。
在第 4 步中,我们将stage_id字段添加到表单视图中。通过添加clickable选项,我们使状态栏可点击。我们还添加了fold字段的选项,这将允许我们在下拉菜单中显示不重要的阶段。
在第 5 步中,我们将stage_id添加到树视图中。
在第 6 步中,我们为各个阶段添加了默认数据。用户安装我们的模块后将会看到这些基本阶段。如果您想了解更多关于 XML 数据语法的知识,请参考第六章中的使用 XML 文件加载数据配方,管理模块数据。
重要提示
在此实现中,用户可以即时定义新的阶段。您需要为hostel.room.stage添加视图和菜单,以便可以从用户界面添加新阶段。如果您不知道如何添加视图和菜单,请参考第九章,后端视图。
如果您不想这样做,Kanban 视图提供了内置功能,可以直接从 Kanban 视图中添加、删除或修改阶段。我们将在下一个配方中查看这一点。
参见
- 要了解如何添加视图和菜单,请参考第九章,后端视图。
管理 Kanban 阶段
使用Kanban 板是管理工作流程的简单方法。它组织成列,每列对应一个阶段,工作项从左到右移动,直到完成。具有这些阶段的 Kanban 视图提供了灵活性,因为它允许用户选择自己的工作流程。它在一个屏幕上提供了记录的全面概述。
开始使用
在此配方中,我们将使用前一个配方中的my_hostel模块。我们将向hostel.room模型添加 Kanban,并将 Kanban 卡片按阶段分组。
如何操作...
执行以下步骤以启用hostel.room模型的 Kanban 工作流程:
-
按如下方式为
hostel.room添加 Kanban 视图:<record id="hostel_room_view_kanban" model="ir.ui.view"> <field name="name">Hostel room Kanban</field> <field name="model">hostel.room</field> <field name="arch" type="xml"> <kanban default_group_by="stage_id"> <field name="stage_id" /> <templates> <t t-name="kanban-box"> <div class="oe_kanban_global_click"> <div class="oe_kanban_content"> <div class="oe_kanban_card"> <div> <b> <field name="name" /> </b> </div> <div class="text-muted"> <i class="fa fa-building"/> <field name="hostel_id" /> </div> </div> </div> </div> </t> </templates> </kanban> </field> action_hostel_room action, as follows:...
kanban,tree,form _group_expand_stages()方法和将 group_expand 属性添加到 stage_id 字段,如下所示:
@api.model def _group_expand_stages(self, stages, domain, order): return stages.search([], order=order) stage_id = fields.Many2one( 'hostel.room.stage', default=_default_room_stage, group_expand='_group_expand_stages' )
重新启动服务器并更新模块以应用更改。这将启用 Kanban 板,如下截图所示:

图 12.2 – 按阶段分组的 Kanban 视图
如前一张截图所示,Kanban 视图将按阶段分组显示房间记录。您可以将卡片拖放到另一个阶段列。将卡片移动到另一个列会更改数据库中的阶段值。
它是如何工作的...
在第 1 步中,我们为hostel.room.stage模型添加了 Kanban 视图。请注意,我们使用stage_id作为 Kanban 的默认分组,这样当用户打开 Kanban 时,Kanban 卡片将按阶段分组。要了解更多关于 Kanban 的信息,请参考第九章,后端视图。
在步骤 2中,我们将kanban关键字添加到现有的操作中。
在步骤 3中,我们将group_expand属性添加到stage_id字段。我们还添加了一个新的_group_expand_stages()方法。group_expand改变了字段的分组行为。默认情况下,字段分组显示正在使用的阶段。例如,如果没有房间记录有Reserved阶段,分组将不会返回该阶段,因此看板将不会显示Reserved列。但在这个例子中,我们希望显示所有阶段,无论它们是否正在使用。
_group_expand_stages()函数用于返回所有阶段的记录。因此,看板视图将显示所有阶段,您将能够通过拖放使用工作流。
更多...
如果您在这个菜谱中创建的看板周围进行尝试,您会发现许多不同的功能。以下是一些:
-
您可以通过点击
group_create选项来创建一个新的阶段,该选项可以用来从看板中禁用添加列选项。 -
您可以通过拖动它们的标题来以不同的顺序排列列。这将更新
hostel.room.stage模型的序列字段。 -
您可以使用看板列标题上的齿轮图标编辑或删除列。
group_edit和group_delete选项可以用来禁用此功能。 -
在
fold字段中具有true值的阶段将折叠,列将显示为细长的条形。如果您点击这个细长的条形,它将展开并显示看板卡片。 -
如果模型有一个
active布尔字段,它将在看板列中显示存档和取消存档记录的选项。archivable选项可以用来禁用此功能。 -
看板列上的加号图标可以用来直接从看板视图中创建记录。
quick_create选项可以用来禁用此功能。目前,这个功能在我们的例子中不会工作。这将在下一个菜谱中解决。
参见
- 想了解更多关于看板的信息,请参阅第九章,后端视图。
向看板卡片添加快速创建表单
分组看板视图提供了快速创建功能,允许我们直接从看板视图中生成记录。列上的加号图标将显示一个可编辑的看板卡片,使用它可以创建记录。在这个菜谱中,我们将学习如何设计我们选择的快速创建看板表单。
入门
对于这个菜谱,我们将使用前一个菜谱中的my_hostel模块。我们将为hostel.room模型使用看板的快速创建选项。
如何做...
按照以下步骤为看板添加自定义快速创建表单:
-
为
hostel.room模型创建一个新的最小表单视图,如下所示:<record id="hostel_room_view_form_minimal" model="ir.ui.view"> <field name="name">Hostel room Form</field> <field name="model">hostel.room</field> <field name="arch" type="xml"> <form> <group> <field name="name"/> <field name="room_no"/> <field name="hostel_id" required="1"/> <field name="floor_no"/> <field name="student_per_room"/> </group> </form> </field> <kanban> tag, as follows:<kanban default_group_by="stage_id" on_create="quick_create" quick_create_view="my_hostel.hostel_room_view_form_minimal"> -
重新启动服务器并更新模块以应用更改。然后,点击列中的加号图标。这将启用看板表单,如图下所示:

图 12.3 – 从看板视图直接创建记录
当你在看板视图中点击创建按钮时,你会看到一个带有输入的小卡片,而不是被重定向到表单视图。你可以填写值并点击添加,这将创建一个房间记录。
工作原理...
要创建自定义快速创建选项,我们需要创建一个最小化表单视图。我们在步骤 1中这样做。我们添加了两个必填字段,因为不填写必填字段你无法创建记录。如果你这样做,Odoo 将生成错误并打开默认表单视图在对话框中,以便你可以输入所有必填值。
在步骤 2中,我们将这个新的表单视图添加到看板视图中。使用quick_create_view选项,你可以将自定义表单视图映射到看板视图中。我们还添加了一个额外的选项 – on_create="quick_create"。此选项将在你点击控制面板中的创建按钮时,在第一列显示快速创建表单。如果没有此选项,创建按钮将打开表单视图以编辑模式。
你可以通过在看板标签中添加quick_create="false"来禁用快速创建功能。
创建交互式看板卡片
�看板卡片支持所有 HTML 标签,这意味着你可以按自己的喜好设计它们。Odoo 提供了一些内置方式来使看板卡片更加互动。在本教程中,我们将添加颜色选项、星形小部件和many2many标签到看板卡片中。
开始使用
对于这个教程,我们将使用上一教程中的my_hostel模块。
如何操作...
按照以下步骤创建一个吸引人的看板卡片:
-
添加一个新模型来管理
hostel.room模型的标签,如下所示:class HostelAmenities(models.Model): _name = "hostel.amenities" _description = "Hostel Amenities" name = fields.Char("Name", help="Provided Hostel Amenity") active = fields.Boolean("Active", default=True, help="Activate/Deactivate whether the amenity should be given or not") hostel.amenities model, as follows:access_hostel_amenities_manager_id,access.hostel.amenities.manager,my_hostel.model_hostel_amenities,my_hostel.group_hostel_manager,1,1,1,1
hostel.room 模型,如下所示:
color = fields.Integer() popularity = fields.Selection([('no', 'No Demand'), ('low', 'Low Demand'), ('medium', 'Average Demand'), ('high', 'High Demand'),]) hostel_amenities_ids = fields.Many2many( "hostel.amenities", "hostel_room_amenities_rel", "room_id", "amenitiy_id", string="Amenities", domain="[('active', '=', True)]", help="Select hostel room amenities") -
按照以下方式将字段添加到表单视图中:
<field name="popularity" widget="priority"/> <field name="hostel_amenities_ids" widget="many2many_tags" color field to the Kanban view: -
在看板视图中添加一个下拉菜单来选择颜色:
<t t-name="kanban-box"> <div t-attf-class="#{kanban_color(record.color)} oe_kanban_global_click"> <div class="o_dropdown_kanban dropdown"> <a class="dropdown-toggle o-no-caret btn" role="button" data-toggle="dropdown"> <span class="fa fa-ellipsis-v"> </span> </a> <div class="dropdown-menu" role="menu"> <t t-if="widget.editable"> <a role="menuitem" type="edit" class="dropdown-item">Edit</a> </t> <t t-if="widget.deletable"> <a role="menuitem" type="delete" class="dropdown-item">Delete</a> </t> <ul class="oe_kanban_colorpicker" data-field="color"/> </div> </div> <div class="oe_kanban_content"> <div class="oe_kanban_card oe_kanban_global_click"> <div> <i class="fa fa-bed"> </i> <b> <field name="name" /> </b> </div> <div class="text-muted"> <i class="fa fa-building"> </i> <field name="hostel_id" /> </div> <span class="oe_kanban_list_many2many"> <field name="hostel_amenities_ids" widget="many2many_tags" options="{'color_field': 'color'}"/> </span> <div> <field name="popularity" widget="priority"/> </div> </div> </div> </div> </t> popularity field to the Kanban view:...
重要提示
应该添加到现有看板视图中的粗体代码。
重新启动服务器并更新模块以应用更改。然后,点击列上的加号图标。它将显示看板,如图下所示:

图 12.4 – 带有新选项的看板卡片
我们对看板结构的更改将使看板卡片启用额外选项。现在,您将能够在看板上选择颜色。您还可以使用星标对卡片进行优先级排序。
它是如何工作的...
在步骤 1和步骤 2中,我们为标签添加了新的模型和安全规则。在步骤 3中,我们在房间模型中添加了一些字段。
在步骤 4中,我们将这些字段添加到表单视图中。请注意,我们在popularity字段上使用了priority小部件,它显示带有星标图标的选项字段。在hostel_amenities_ids字段中,我们使用了many2many_tags小部件,它以标签的形式显示many2many字段。color_field选项用于在标签上启用颜色功能。此选项的值是存储颜色索引的字段名称。no_create_edit选项将禁用通过表单视图创建新标签的功能。
在步骤 5中,我们改进了很多东西。首先,我们在看板卡片上添加了t-attf-class="#{kanban_color(record.color.raw_value)}。这将用于显示看板卡片的颜色。它使用color字段的值并基于该值生成一个类。例如,如果看板记录在color字段中的值为2,它将向类中添加kanban_color_2。之后,我们添加了一个下拉菜单来添加例如编辑、删除和看板颜色选择器的选项。编辑和删除选项仅在用户具有适当的访问权限时显示。
最后,我们在看板卡片上添加了标签和优先级。添加所有这些后,看板卡片将如下所示:

图 12.5 – 看板卡片选项
使用这种卡片设计,您可以直接从看板卡片设置流行度星标和颜色。
向看板视图中添加进度条
有时,您在列中有大量记录,很难清楚地了解特定阶段。可以使用进度条来显示任何列的状态。在本菜谱中,我们将根据popularity字段在基于看板的视图中显示进度条。
开始使用
对于这个菜谱,我们将使用前一个菜谱中的my_hostel模块。
如何操作...
要向看板列添加进度条,您需要在看板视图定义中添加一个progressbar标签,如下所示:
<progressbar
field="popularity"
colors='{"low": "success", "medium": "warning", "high": "danger"}'/>
注意,看板列进度条是在 Odoo 版本 11 中引入的。在此之前的版本将不会显示列进度条。
重新启动服务器并更新模块以应用更改。然后,点击列上的加号图标。这将显示看板列上的进度条,如下面的截图所示:

图 12.6 – 带进度条的看板视图
更新模块后,您将在看板列中添加一个进度条。进度条的颜色显示基于记录状态的记录数。您可以通过点击进度条中的一个来根据该状态过滤记录。
它是如何工作的...
看板列上的进度条是根据字段值显示的。进度条支持四种颜色,因此您不能显示超过四种状态。可用的颜色是绿色(成功)、蓝色(信息)、红色(危险)和黄色(警告)。然后,您需要将颜色映射到字段状态。在我们的例子中,我们映射了priority字段的三个状态,因为我们不希望为需求不高的房间显示进度条。
默认情况下,进度条在旁边显示记录数。您可以通过点击进度条来查看特定状态的总量。点击进度条还会突出显示该状态的业务卡片。除了记录数,您还可以显示整数或浮点字段的和。为此,您需要添加sum_field属性以及字段值,例如sum_field="field_name"。
创建服务器动作
服务器动作是 Odoo 自动化工具的基础。它们允许我们描述要执行的动作。然后,这些动作可以通过事件触发器调用,或者在某些时间条件满足时自动触发。
最简单的情况是让最终用户通过从更多按钮中选择来对文档执行操作。我们将为项目任务创建此类动作,以便我们可以通过将当前选定的任务加星并为其设置 3 天后的截止日期来设置优先级。
准备中
我们需要一个安装了项目应用的 Odoo 实例。我们还需要激活开发者模式。如果尚未激活,请在 Odoo设置仪表板中激活它。
如何操作...
要创建服务器动作并从更多菜单使用它,请按照以下步骤操作:
- 从设置顶部菜单,选择技术 | 动作 | 服务器动作菜单项,然后在记录列表顶部点击创建上下文动作按钮,如图下截图所示:

图 12.7 – 服务器动作表视图
-
使用这些值填写服务器动作表单:
-
动作名称:设置为 优先级
-
模型:任务
-
类型:更新记录
-
-
在服务器动作中,在
优先级 -
值 -
低
下面的截图显示了输入的值:

图 12.8 – 设置要写入的行
-
保存服务器动作,然后点击左上角的创建上下文动作按钮,使其在项目任务的更多按钮下可用。
-
要尝试它,请转到项目顶层菜单,打开项目,然后打开一个随机任务。通过点击操作,我们应该看到设置优先级选项,如图下所示。选择此选项将标记任务并将截止日期更改为现在起 3 天:

图 12.9 – 设置优先级的服务器操作
一旦您添加了服务器操作,您将在任务上设置优先级选项。点击它后,服务器操作星号将变为黄色,表示任务的优先级已提高。此外,服务器操作将更改截止日期。
它是如何工作的...
服务器操作在模型上工作,因此我们首先必须做的第一件事是选择我们想要与之一起工作的模型。在我们的示例中,我们使用了项目任务。
接下来,我们应该选择要执行的操作类型。有几个选项可供选择:
-
更新记录 允许您设置当前记录或另一条记录的值。
-
创建活动 允许您在所选记录上创建活动
-
执行代码 允许您编写任意代码,在没有任何其他选项足够灵活以满足我们的需求时执行。
-
创建记录 允许您在当前模型或另一个模型上创建新记录。
-
发送电子邮件 允许您选择电子邮件模板。当操作被触发时,将使用此模板发送电子邮件。
-
执行现有操作 可以用来触发客户端或窗口操作,就像点击菜单项时一样。
-
添加关注者 允许用户或频道订阅记录。
-
创建下一个活动 允许您创建一个新的活动。这将显示在聊天中。
-
发送短信 允许您发送短信。您需要选择短信模板。
注意
发送短信文本消息 是 Odoo 的一项收费服务。如果您想发送短信,则需要购买短信信用。
在我们的示例中,我们使用了1来标记任务,并在datetime Python 模块(docs.python.org/2/library/datetime.html)上设置值以计算从今天起 3 天的日期。
可以使用任意 Python 表达式,以及其他几个可用的操作类型。出于安全原因,代码由odoo/tools/safe_eval.py文件中实现的safe_eval函数检查。这意味着某些 Python 操作可能不允许,但这很少成为问题。
当您向服务器操作添加下拉选项时,通常,它对所有内部用户都可用。但如果您只想向选定用户显示此选项,您可以将一个组分配给服务器操作。这在服务器操作表单视图的安全选项卡下可用。
还有更多...
Python 代码在受限环境中评估,以下对象可用于使用:
-
env:这是对Environment对象的引用,就像类方法中的self.env一样。 -
model:这是对服务器操作所作用的model类的引用。在我们的示例中,它等同于self.env['project.task']。 -
ValidationError:这是对from odoo.exceptions import ValidationError的引用,允许阻止不希望的操作的验证。它可以用作raise Warning('Message!')。 -
Record或records:这提供了对当前记录或记录的引用,允许你访问它们的字段值和方法。 -
log:这是一个用于在ir.logging模型中记录消息的函数,允许在数据库端记录操作。 -
datetime、dateutil和time:这些提供了对 Python 库的访问。
使用 Python 代码服务器操作
服务器操作有几种类型可供选择,但执行任意 Python 代码是最灵活的选项。当明智地使用时,它赋予用户从用户界面实施高级业务规则的能力,而无需创建特定的附加模块来安装该代码。
我们将通过实现一个向项目任务关注者发送提醒通知的服务器操作来演示这种类型的服务器操作。
准备工作
我们需要一个安装了项目应用程序的 Odoo 实例。
如何操作...
要创建 Python 代码服务器操作,请按照以下步骤操作:
-
创建一个新的服务器操作。在 设置 菜单中,选择 技术 | 操作 | 服务器操作 菜单项,然后在记录列表的顶部点击 创建 按钮。
-
使用以下值填写 服务器操作 表单:
-
操作名称:发送提醒
-
基础模型:任务
-
要执行的操作:执行代码
-
-
在 Python 代码 文本区域中,删除默认文本,并替换为以下代码:
if not record.date_deadline: raise ValidationError('Task has no deadline!') delta = record.date_deadline - datetime.date.today() days = delta.days if days==0: msg = 'Task is due today.' elif days < 0: msg = 'Task is %d day(s) late.' % abs(days) else: msg = 'Task will be due in %d day(s).' % days record.message_post(body=msg, subject='Reminder', subtype_xmlid='mail.mt_comment')以下截图显示了输入的值:

图 12.10 – 输入值的 Python 代码
-
保存服务器操作,然后点击左上角的 创建上下文操作 以使其在项目任务的 更多 按钮下可用。
-
现在,点击顶部的 项目 菜单,并选择 搜索 | 任务 菜单项。选择一个随机任务,为其设置截止日期,然后尝试在 更多 按钮下的 发送提醒 选项。
这与之前的菜谱工作方式相同;唯一的区别是,这个服务器操作将运行你的 Python 代码。一旦你在任务上运行了服务器操作,它将在聊天中放置一条消息。
它是如何工作的...
本章的 创建服务器操作 菜单提供了如何创建一般服务器操作的详细说明。对于这种特定类型的操作,我们需要选择 执行代码 选项,然后编写运行文本区域的代码。
代码可以有多个行,就像我们的食谱中那样,并且它在具有对当前记录对象或会话用户等对象的引用的上下文中运行。可用的引用在创建服务器 操作食谱中已描述。
我们使用的代码计算从当前日期到截止日期的天数,并使用这个数字来准备一个合适的通知消息。最后一行在任务的留言墙上实际发布消息。subtype='mt_comment'参数对于发送电子邮件通知给关注者来说是必需的,就像我们默认使用mt_note时,发布一个内部笔记而不发送通知,就像我们使用了记录内部笔记按钮一样。请参考第二十三章,在 Odoo 中管理电子邮件,了解更多关于 Odoo 中邮件的信息。
更多内容...
Python 代码服务器操作是一个强大且灵活的资源,但与自定义附加模块相比,它们确实有一些限制。
由于 Python 代码是在运行时评估的,如果发生错误,堆栈跟踪可能不那么有信息性,并且可能更难调试。使用第七章,调试模块中展示的技术,在服务器操作的代码中插入断点也是不可能的,因此调试需要使用日志语句来完成。另一个担忧是,在尝试追踪模块代码中的行为原因时,可能找不到任何相关的内容。在这种情况下,这可能是由于服务器操作引起的。
在执行更密集的服务器操作使用时,交互可能相当复杂,因此建议妥善规划并保持其组织。
相关内容
- 参考第第二十三章,在 Odoo 中管理电子邮件,了解更多关于 Odoo 中邮件的信息。
使用基于时间条件的自动化操作
自动化操作可以用于根据时间条件自动触发操作。我们可以使用它们来自动对满足某些标准和时间条件的记录执行某些操作。
例如,我们可以为具有截止日期的项目任务提前一天触发提醒通知,如果有的话。让我们看看如何做到这一点。
准备工作
要遵循这个食谱,我们需要安装好项目管理应用(其技术名称为project)和base_automation,并且激活开发者模式。我们还需要本章“使用 Python 代码服务器操作”食谱中创建的服务器操作。
如何操作...
要在任务上创建具有定时条件的自动化操作,请按照以下步骤操作:
-
在设置菜单中,选择技术 | 自动化 | 自动化操作菜单项,然后点击创建按钮。
-
在
临近截止日期发送通知中填写基本信息 -
模型:任务
-
在触发器字段中选择基于时间条件
-
对于待执行的操作,选择执行 现有操作
-
要设置记录标准,点击
["&",["date_deadline","!=",False],["stage_id.fold","=",False]],然后点击保存按钮。当更改到另一个字段时,满足条件的记录数量信息将更新并显示记录(s)按钮。通过点击记录按钮,我们可以检查满足域表达式的记录列表。 -
设置时间条件为
-``1天。 -
在操作选项卡下,在要运行的服务器操作中,点击添加项目,从列表中选择发送提醒;这应该之前已经创建。请参考以下截图:

图 12.11 – 自动化操作表单视图
如果没有,我们仍然可以使用创建按钮创建要运行的服务器操作。
-
点击保存以保存自动化操作。
-
执行以下步骤以尝试它:
-
前往项目菜单,选择搜索 | 任务,并在带有过去日期的任务上设置截止日期。
-
前往设置菜单,点击技术 | 自动化 | 计划操作菜单项,在列表中找到基本操作规则:检查并执行操作,打开其表单视图,然后在左上角点击手动运行按钮。这强制检查定时自动化操作。以下截图显示了这一点。请注意,这应该在新建的演示数据库上工作,但在现有数据库中可能不会这样工作:
-

图 12.12 – 运行自动化操作(测试用)
-
再次,前往项目菜单并打开您之前设置的带有截止日期的任务。检查消息板;您应该看到由服务器操作触发的自动化操作生成的通知。
在添加基于时间的自动化操作以设置截止日期后,将在截止日期前 1 天向任务添加提醒消息。
它是如何工作的...
自动化操作作用于模型,可以通过事件或时间条件触发。首先,我们必须设置模型和何时运行的值。
两种方法都可以使用过滤器来缩小我们可以执行操作的记录。我们可以使用域表达式来实现这一点。您可以在第九章,后端视图中找到有关编写域表达式的更多信息。或者,您可以通过使用用户界面功能创建并保存项目任务的过滤器,然后复制自动生成的域表达式,从基于搜索过滤器列表的设置选择中选中它。
我们使用的领域表达式选择所有具有非空Fold标志的记录并未进行检查。没有Fold标志的阶段被视为正在进行中。这样,我们避免了在完成、取消或关闭阶段触发通知。
然后,我们应该定义时间条件——要使用日期字段以及何时触发操作。时间段可以是分钟、小时、天或月,为时间段设置的数字可以是正数,表示日期之后的时段,或负数,表示日期之前的时段。当使用以天为单位的时间段时,我们可以提供一个定义工作日并可用于日计数的资源日历。
这些操作由检查操作规则计划作业进行检查。请注意,默认情况下,这是每 4 小时运行一次。这对于在日或月尺度上工作的操作是合适的,但如果你需要在小时间尺度上工作的操作,你需要将运行间隔更改为更小的值。
对于符合所有标准且触发日期条件(字段日期加上间隔)在最后一次操作执行之后记录,将触发操作。这是为了避免重复触发相同的操作。这也是为什么在计划操作尚未触发的数据库中手动运行前面的操作将起作用,但在已经由调度器运行的数据库中可能不会立即起作用的原因。
一旦自动化操作被触发,操作选项卡会告诉你应该发生什么。这可能是一系列服务器操作,例如更改记录上的值、发布通知或发送电子邮件。
更多...
这些类型的自动化操作在达到一定的时间条件时触发。这不同于在条件仍然为真时定期重复执行操作。例如,自动化操作无法在截止日期过后每天发布提醒。
此类操作可以由计划操作执行,这些操作存储在ir.cron模型中。然而,计划操作不支持服务器操作;它们只能调用模型对象的现有方法。因此,要实现自定义操作,我们需要编写一个附加模块,添加底层 Python 方法。
作为参考,该模型的名称为base.action.rule。
参见
- 有关编写领域表达式的更多详细信息,请参阅第九章,后端视图。
在事件条件下使用自动化操作
商业应用为业务操作提供记录系统,但也预期支持特定于组织用例的动态业务规则。
将这些规则雕刻到自定义附加模块中可能不够灵活,并且功能用户难以触及。由事件条件触发的自动操作可以弥合这一差距,并为自动化或强制执行组织的程序提供强大的工具。例如,我们将对项目任务进行验证,以确保只有项目经理可以更改任务到完成阶段。
准备工作
要遵循此配方,您需要已经安装了项目管理应用程序。您还需要激活开发者模式。如果尚未激活,请在 Odoo 的关于对话框中激活它。
如何操作...
要创建一个在任务上有事件条件的自动操作,请按照以下步骤操作:
-
在设置菜单中,选择技术 | 自动化 | 自动操作菜单项,然后点击创建按钮。
-
在
ValidateClosing Tasks中填写基本信息 -
阶段 ID -
在代码编辑器中的
[('stage_id.name', '!=', 'Done')]– 保存 -
对于代码编辑器中的
[('stage_id.name', '=', 'Done')]域,保存,如下所示截图:

图 12.13 – 自动操作表单视图
-
在Actions标签页中,点击Add an item。在列表对话框中,点击Create按钮以创建一个新的服务器操作。
-
使用以下值填写服务器操作表单,然后点击
ValidateClosing tasks -
模型: 任务
-
要执行的操作: 执行代码
-
Python Code: 输入以下代码:
if user != record.project_id.user_id: raise Warning('Only the Project Manager can close Tasks')
以下截图显示了输入的值:

图 12.14 – 添加子操作
- 点击
Demo用户,我们正在使用Administrator用户,我们的自动操作应该被触发,并且我们的警告消息应该阻止更改。
它是如何工作的...
我们首先为我们的自动操作命名,并设置它应该与之一起工作的模型。对于我们需要的行为类型,我们应该选择On Save,但On Creation、On Creation & Update、On Deletion和基于表单修改选项也是可用的。
接下来,我们定义过滤器以确定我们的操作何时应该被触发。On Save操作允许我们定义两个过滤器——一个用于更改记录之前检查,另一个用于更改记录之后检查。这可以用来表达转换——检测记录从状态 A变为状态 B。在我们的例子中,我们希望在未完成的任务变为完成阶段时触发操作。On Save操作是唯一允许这两个过滤器的操作;其他操作类型只允许一个过滤器。
重要提示
需要注意的是,我们的示例条件仅适用于英语语言用户。这是因为阶段名称是一个可翻译字段,不同语言可能有不同的值。因此,应避免或谨慎使用可翻译字段的过滤器。
最后,我们创建并添加一个(或多个)服务器操作,以便在自动化操作被触发时执行我们想要的任何操作。在这种情况下,我们选择演示如何实现自定义验证,利用 Python 代码服务器操作使用Warning异常来阻止用户的更改。
更多内容...
在第五章中,基本服务器端开发,我们看到了如何重新定义模型的write()方法以在记录更新时执行操作。记录更新上的自动化操作提供了另一种实现方式,具有一些优点和缺点。
在众多好处中,定义一个由存储计算字段的更新触发的操作很容易,这在纯代码中很难实现。还可能定义记录的过滤器,并为不同的记录或满足不同条件的记录定义不同的规则,这些条件可以用搜索域表示。
然而,与模块内 Python 业务逻辑代码相比,自动化操作可能存在一些缺点。如果规划不当,提供的灵活性可能导致复杂的交互,难以维护和调试。此外,写操作的前后过滤操作带来了一些开销,如果在执行敏感操作时可能会成为问题。
创建基于 QWeb 的 PDF 报告
当与外界沟通时,通常需要从数据库中的记录生成 PDF 文档。Odoo 使用与表单视图相同的模板语言:QWeb。
在本食谱中,我们将创建一个 QWeb 报告来打印关于当前被学生借用的房间信息。本食谱将重用本章前面提到的在 Kanban 视图中添加进度条食谱中展示的模型。
准备工作
如果您还没有这样做,请按照第一章中描述的步骤安装wkhtmltopdf,安装 Odoo 开发环境;否则,您将无法得到您努力工作后的闪亮 PDF。
此外,请确保web.base.url配置参数(或report.url)是一个可以从您的 Odoo 实例访问的 URL;否则,报告将需要很长时间才能生成,结果看起来也会很奇怪。
如何操作...
请按照以下步骤操作:
-
在本食谱中,我们将向
hostel.student添加一个报告,打印出学生所借的学生名单。我们需要向学生模型添加一个one2many字段,与hostel.room模型相关联,如下例所示:class HostelStudent(models.Model): _name = "hostel.student" _description = "Hostel Student Information" name = fields.Char("Student Name") gender = fields.Selection([("male", "Male"), ("female", "Female"), ("other", "Other")], string="Gender", help="Student gender") active = fields.Boolean("Active", default=True, reports/hostel_room_detail_report_template.xml, as follows:房间名称:
房间号:
学生姓名 性别 reports/hostel_room_detail_report.xml,如下例所示:
<?xml version="1.0" encoding="utf-8"?> <odoo> <record id="report_hostel_room_detail" model="ir.actions.report"> <field name="name">Room detail report</field> <field name="model">hostel.room</field> <field name="report_type">qweb-pdf</field> <field name="binding_model_id" ref="model_hostel_room"/> <field name="report_name">my_hostel.hostel_room_detail_reports_template</field> <field name="report_file">my_hostel.hostel_room_detail_reports_template</field> </record> </odoo> -
将这两个文件添加到附加组件的清单中,如下例所示:
... "depends": ["base"], "data": [ "security/hostel_security.xml", "security/ir.model.access.csv", "data/room_stages.xml", "views/hostel.xml", "views/hostel_amenities.xml", "views/hostel_room.xml", "views/hostel_room_stages_views.xml", "views/hostel_student.xml", "views/hostel_categ.xml", "reports/hostel_room_detail_report_template.xml", "reports/hostel_room_detail_report.xml", ],
现在,当打开房间表单视图或从列表视图中选择学生时,您应该可以在下拉菜单中找到打印房间详细报告的选项,如下面的截图所示:

图 12.15 – 报告的打印操作
它是如何工作的...
在步骤 1中,我们添加了一个one2many hostel_student_ids字段。该字段将包含学生的房间记录。我们将在 QWeb 报告中使用它来列出学生已预订的房间。
在步骤 2中,我们定义了 QWeb 模板。模板的内容将用于生成 PDF。在我们的例子中,我们使用了一些基本的 HTML 结构。我们还使用了t-esc和t-foreach等属性,这些属性用于在报告中生成动态内容。现在不必担心template元素内的这种语法。这个主题将在第十四章的创建或修改模板 – QWeb菜谱中详细讨论,CMS 网站开发。在模板中需要注意的另一件重要事情是布局。在我们的例子中,我们在模板中使用了web.internal_layout,这将生成具有最小页眉和页脚的最终 PDF。如果您想要使用公司标志和公司信息的 informative 页眉和页脚,请使用web.external_layout布局。我们还向docs参数添加了一个for循环,当用户从列表视图打印时,它将用于为多个记录生成报告。
在步骤 3中,我们通过<record>标签在另一个 XML 文件中声明了报告。它将注册报告的ir.actions.report模型。这里的关键部分是您将report_name字段设置为定义的模板的完整 XML ID(即modulename.record_id);否则,报告生成过程将失败。model字段确定报告操作的记录类型,而name字段是在打印菜单中显示给用户的名称。
注意
在 Odoo 的先前版本中,使用<report>标签来注册报告。但从版本 v14 开始,它已被弃用,您需要使用<record>标签创建ir.actions.report记录。<report>标签在 Odoo v14 中仍然支持以实现向后兼容,但使用它将在日志中显示警告。
通过将 report_type 设置为 qweb-pdf,我们请求我们的视图生成的 HTML 通过 wkhtmltopdf 运行,向用户交付 PDF。在某些情况下,你可能想使用 qweb-html 在浏览器中渲染 HTML。
更多...
报告的 HTML 中存在一些对布局至关重要的标记类。确保将所有内容包裹在一个设置了 page 类的元素中。如果你忘记了这一点,你将什么也看不到。要为你的记录添加标题或页脚,请使用 header 或 footer 类。
此外,记得这是 HTML,所以请充分利用 CSS 属性,如 page-break-before、page-break-after 和 page-break-inside。
你会注意到,我们所有的模板主体都被包裹在两个设置了 t-call 属性的元素中。我们将在第十四章 CMS 网站开发 中稍后探讨这个属性的机制,但你必须在你的报告中做同样的事情。这些元素确保 HTML 生成指向所有必要 CSS 文件的链接,并包含一些用于报告生成的其他数据。虽然 web.html_container 没有替代品,第二个 t-call 可以是 web.external_layout。区别在于外部布局已经包含显示公司标志、公司名称和一些你期望从公司外部沟通中获得的其他信息的标题和页脚,而内部布局只提供带有分页、打印日期和公司名称的标题。为了保持一致性,始终使用这两个之一。
重要提示
注意,web.internal_layout、web.external_layout、web.external_layout_header 和 web.external_layout_footer(后两个由外部布局调用)本身只是视图,你已经知道如何通过继承来更改它们。要使用模板元素进行继承,请使用 inherit_id 属性。
从看板卡片管理活动
Odoo 使用活动来对记录上的操作进行调度。这些活动可以在表单视图和看板视图中进行管理。在本菜谱中,我们将学习如何从看板视图卡片中管理活动。我们将向房间看板卡片添加活动小部件。
开始
对于这个菜谱,我们将使用之前菜谱中的 my_hostel 模块。
如何操作...
按照以下步骤从看板视图添加和管理活动:
-
将邮件依赖项添加到
manifest文件中:'depends': ['base', 'mail'], -
在
hostel.room模型中继承活动混入:class HostelRoom(models.Model): _name = "hostel.room" _description = "Hostel Room Information" _rec_name = "room_no" activity_state field to the Kanban view under the color field:<field name="color" />在看板模板中的
activity_ids字段。将此字段添加到流行度字段下,如图所示:<div> <field name="popularity" widget="priority"/> </div> <div> <field name="activity_ids" widget="kanban_activity"/> my_hostel module to apply the change. Open the Rooms Kanban view; you will see the activity manager on the Kanban card, as shown in the following screenshot:

图 12.16 – 卡片中的活动管理器
如你所见,在应用本菜谱中的代码后,你将能够从看板卡片管理活动。你现在也可以从看板卡片处理或创建活动。
它是如何工作的...
在 步骤 1 中,我们将依赖项添加到我们模块的清单中。我们这样做是因为与活动相关的所有实现都是 mail 模块的一部分。如果没有安装 mail,我们无法在我们的模型中使用活动。
在 步骤 2 中,我们将 activity mixin 添加到 hostel.room 模型中。这将启用房间记录的活动。添加 mail.activity.mixin 将添加活动所需的所有字段和方法。我们还添加了 mail.thread 混合,因为活动在用户处理活动时记录消息。如果您想了解更多关于此活动的信息,请参阅 第二十三章 的 在 Odoo 文档上管理活动 菜谱,在 Odoo 中管理电子邮件。
在 步骤 3 中,我们将 activity_state 字段添加到看板视图中。此字段由活动小部件用于显示颜色小部件。颜色将代表即将到来的活动的当前状态。
在 步骤 4 中,我们添加了活动小部件本身。它使用 activity_ids 字段。在我们的示例中,我们在一个单独的 <div> 标签中添加了活动小部件,但您可以根据您的设计要求将其放在任何位置。有了活动小部件,您可以直接从看板卡片中安排、编辑和处理活动。
更多内容...
在本章的 在 Kanban 视图中添加进度条 菜谱中,我们根据 popularity 字段显示了一个 Kanban 进度条。但我们也可以根据即将到来的活动的状态显示进度条:
<progressbar field="activity_state"
colors='{"planned": "success",
"today": "warning",
"overdue": "danger"}'/>
这将根据即将到来的活动的状态显示进度条。基于状态的进度条在 Odoo 的多个视图中使用。
相关内容
-
如果您想了解更多关于邮件线程的信息,请参阅 第二十三章 的 在 Odoo 文档上管理聊天 菜谱,在 Odoo 中管理电子邮件。
-
如果您想了解更多关于活动的信息,请参阅 第二十三章 的 在 Odoo 文档上管理活动 菜谱,在 Odoo 中管理电子邮件。
向表单视图中添加状态按钮
Odoo 使用状态按钮在表单视图中直观地关联两个不同的对象。它用于显示相关记录的一些基本关键绩效指标(KPIs)。它还用于重定向并打开另一个视图。在本菜谱中,我们将向房间的表单视图中添加一个状态按钮。此状态按钮将显示房间记录的数量,并且点击它时,我们将被重定向到看板视图列表。
开始
对于这个菜谱,我们将使用前一个菜谱中的 my_hostel 模块。
如何做到这一点...
按照以下步骤向宿舍的表单视图中添加状态按钮:
-
将
rooms_count计算字段添加到hostel.hostel模型中。此字段将计算宿舍中活跃房间的数量:rooms_count = fields.Integer(compute="_compute_rooms_count") def _compute_rooms_count(self): room_obj = self.env['hostel.room'] for hostel in self: hostel.hostel model. Prepend it just inside the <sheet> tag:<div class="oe_button_box" name="button_box"><button class="oe_stat_button" name="%(action_hostel_room)d" type="action" icon="fa-building" context="{'search_default_hostel_id': active_id}"><field string="房间" name="rooms_count" widget="statinfo"/></button>在
my_hostel模块中应用更改。打开任何宿舍的表单视图;您将找到状态按钮,如下面的截图所示:

图 12.17 – 宿舍表单视图中的状态按钮
点击状态按钮后,您将被重定向到房间看板视图。在这里,您将只看到当前宿舍的订单。
它是如何工作的...
在步骤 1中,我们添加了一个计算字段,用于计算当前宿舍的房间记录数量。此字段的值将用于状态按钮以显示计数。如果您想了解更多关于计算的信息,请参阅第四章中的向模型添加计算字段配方,应用程序模型。
在步骤 2中,我们在hostel.hostel模型的表单视图中添加了状态按钮。状态按钮有一个特定的语法和位置。所有状态按钮需要做的就是将其包裹在具有oe_button_box类的<div>标签下。状态按钮框需要放置在<sheet>标签内。请注意,我们在按钮框上使用了name属性。当您想添加一个新的状态时,这个name属性非常有用,但您将需要添加一个带有<button>标签和oe_stat_button类的状态按钮。内部,状态按钮只是一个具有不同用户界面的表单视图按钮。这意味着它支持所有由正常按钮支持的属性,例如操作、图标和上下文。
在我们的例子中,我们使用了房间订单的动作,这意味着当用户点击状态按钮时,他们将被重定向到房间记录,但它将显示所有房间的记录。我们只想显示当前房间的房间记录。为此,我们必须传递search_default_hostel_id。这将应用当前房间的默认过滤器。请注意,hostel_id是hostel.room模型上的many2one字段。如果您想根据另一个字段进行筛选,请在context中使用它,并在其前加上search_default_前缀。
状态按钮经常被使用,因为它们非常有用,并显示与记录相关的总体统计信息。您可以使用它们来显示与当前记录相关的所有信息。例如,在联系记录中,Odoo 显示状态按钮,显示与当前联系发票总数、潜在客户数量、订单数量等相关的信息。
参见
-
要了解更多关于按钮的信息,请参阅第九章中的在表单中添加按钮配方,后端视图。
-
要了解更多关于操作的信息,请参阅第九章中的添加菜单项和窗口操作配方,后端视图。
启用记录的存档选项
Odoo 提供了内置功能,以启用记录的存档和取消存档选项。这将帮助用户隐藏不再重要的记录。在这个配方中,我们将为房间添加一个存档/取消存档选项。一旦房间不可用,我们就可以存档房间。
入门指南
对于这个配方,我们将使用前一个配方中的my_hostel模块。
如何操作...
存档和取消存档主要自动工作。如果模型有一个名为active的布尔字段,则记录上的选项可用。我们已经在hostel.room模型中有一个active字段。但如果你还没有添加它,请按照以下步骤添加active字段:
-
在
hostel.room模型中添加一个名为active的布尔字段,如下所示:active field to the form view:<field name="active" invisible="1"/>
更新my_hostel模块以应用更改。现在,你将能够存档房间。存档选项在操作下拉菜单中可用,如下面的截图所示:

图 12.18 – 表单视图上的存档选项
一旦你存档了一个记录,你希望在 Odoo 的任何地方都能看到该记录。要查看它,你需要从搜索视图中应用一个过滤器。
它是如何工作的...
在 Odoo 中,名为active的布尔字段具有特殊用途。如果你在你的模型中添加了一个active字段,那么在active字段中值为false的记录将不会在 Odoo 的任何地方显示。
在步骤 1中,我们在hostel.room模型中添加了一个active字段。请注意,我们在这里保留了默认值True。如果我们不添加这个默认值,新创建的记录将默认以存档模式创建,即使在最近创建的情况下,也不会在视图中显示。
在步骤 2中,我们在表单视图中添加了active字段。如果你不在表单视图中添加active字段,存档/取消存档选项将不会在invisible属性中显示,从而在表单视图中隐藏它。
在我们的例子中,一旦你存档了一个房间,该房间将不会在树形视图或任何其他视图中显示。在宿舍记录的many2one下拉菜单中,该房间也不会显示。如果你想取消存档该房间,那么你需要从搜索视图中应用一个过滤器来显示存档记录,然后恢复该房间。
还有更多...
如果你的模型有一个名为active的布尔字段,search方法将不会返回存档记录。如果你想搜索所有记录,无论它们是否存档,那么在上下文中传递active_test,如下所示:
self.env['hostel.room'].with_context(active_test=False).search([])
注意,如果存档记录与另一个记录相关联,它将在相关表单视图中显示。例如,假设你有“房间 1”。然后,你存档“房间 1”,这意味着从现在起,你无法在房间中选择“房间 1”。但是如果你打开“订单 1”,你会看到存档的“房间 1”。
第十三章:网络服务器开发
在本章中,我们将介绍 Odoo 网络服务器部分的基础知识。请注意,这将涵盖基本方面;对于高级功能,您应参考 第十四章,CMS 网站开发。
Odoo 网络服务器是 Odoo 框架的一个关键组件,负责处理网络请求并向用户提供服务网络界面。
以下是 Odoo 网络服务器的关键方面:
-
网络界面和模块: 网络服务器提供了一个用户友好的网络界面,用于访问和交互 Odoo 应用程序。用户可以通过此界面浏览不同的模块,访问数据,并执行各种业务操作。
-
HTTP 服务器: Odoo 使用 HTTP 服务器来处理网络请求。它可以配置为与流行的网络服务器(如 Nginx 或 Apache)一起工作,或者可以运行其内置的 HTTP 服务器。
-
Werkzeug: Werkzeug 是一个 Python 的 WSGI(Web Server Gateway Interface)库,Odoo 使用它来处理 HTTP 请求和响应。Werkzeug 帮助路由请求、处理会话以及管理其他与网络相关的任务。
-
控制器和路由: Odoo 使用控制器来处理不同的网络请求,并将它们路由到适当的控制器和方法。路由机制确保请求被导向正确的模块和功能。
-
视图和模板: Odoo 使用视图和模板来定义数据在网页界面中的呈现方式。视图确定页面的结构,模板提供 HTML 和呈现逻辑以渲染数据。
-
业务逻辑: 网络服务器与 Odoo 的业务逻辑紧密集成。它与后端通信以获取和更新数据,确保网络界面反映了业务应用程序的最新状态。
-
安全性: 安全性是 Odoo 网络服务器的一个关键方面。它包括认证、授权和会话管理等功能,以确保用户具有适当的访问级别,并且他们的系统交互是安全的。
-
JavaScript 和 CSS: Odoo 网络界面依赖于 JavaScript 和 CSS 来增强用户体验并提供动态和响应式功能。这包括表单验证、交互式元素和实时更新。
-
RESTful API: 网络服务器还提供了一个 RESTful API,允许外部应用程序以编程方式与 Odoo 交互。这可以实现与第三方系统的集成以及自定义应用程序的开发。
-
定制和扩展: 开发者可以扩展和定制 Odoo 网络服务器以满足特定的业务需求。这包括创建自定义模块、视图和控制器。
理解 Odoo 网络服务器对于与 Odoo 一起工作的开发者和管理员来说至关重要,他们需要根据业务独特的需求部署、配置和定制系统。
Werkzeug (werkzeug.palletsprojects.com/en/2.3.x) 是一个用于 Python 的 WSGI 库,Odoo 使用它来处理 HTTP 请求和响应。WSGI 是一个规范,用于描述 Python 中 Web 服务器和 Web 应用程序之间的通信。Werkzeug 提供了一套实用工具和类,使得与 WSGI 应用程序一起工作变得更加容易。以下是 Werkzeug 在 Odoo 上下文中使用的一些详细信息:
-
代表传入 HTTP 请求的
Request对象。在 Odoo 中,此对象用于从传入的 HTTP 请求中提取信息,例如表单数据、查询参数和头部信息。 -
Werkzeug 中的
Response对象用于创建 HTTP 响应。Odoo 利用此功能构建并发送响应回客户端,包括渲染网页或响应 AJAX 请求提供数据。 -
路由:Werkzeug 实现了简单的 URL 路由。在 Odoo 中,路由机制用于将传入的请求映射到适当的控制器方法或视图。这有助于将请求定向到 Odoo 应用程序中的正确功能。
-
中间件:可以使用 Werkzeug 将中间件组件添加到 Odoo 应用程序中。中间件位于 Web 服务器和 Odoo 应用程序之间,可以执行诸如身份验证、日志记录或修改请求和响应等任务。
-
URL 构建:Werkzeug 提供了一个 URL 构建功能,有助于在 Odoo 应用程序的不同路由中生成 URL。这对于在 Web 界面中动态创建链接和重定向至关重要。
-
会话管理:Werkzeug 支持会话管理,Odoo 利用它来处理用户会话。这对于在多个请求之间维护用户状态以及确保用户身份验证等安全功能非常重要。
-
常用任务的实用工具:Werkzeug 包含各种实用工具,简化了常见的 Web 开发任务。Odoo 利用这些实用工具执行诸如解析表单数据、处理文件上传和管理 Cookie 等任务。
-
错误处理:Werkzeug 提供了处理错误(包括 HTTP 错误响应)的机制。Odoo 使用此机制确保在需要时向客户端返回适当的错误消息。
在 Odoo 的上下文中使用 Werkzeug,开发者通常通过 Odoo 模块中定义的控制器和视图与这些功能交互。了解 Werkzeug 对希望扩展或自定义 Odoo 的开发者有益,因为它提供了处理应用程序内 HTTP 请求和响应的底层机制的见解。然而,在日常的 Odoo 开发中,开发者通常在更高的层次上使用 Odoo 框架本身,而不直接与 Werkzeug 交互。
在本章中,我们将涵盖以下主题:
-
从网络中访问路径
-
限制对可网络访问路径的访问
-
消费传递给您的处理器的参数
-
修改现有的处理器
-
提供静态内容
技术要求
本章的技术要求包括在线 Odoo 平台。
本章中使用的所有代码都可以从 GitHub 仓库github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter13下载。
使一个路径可通过网络访问
使一个路径可通过网络访问意味着定义入口点或 URL,用户可以通过这些 URL 访问应用程序。这对于任何 Web 开发项目都是基本的,因为它决定了用户如何与系统交互。在这个菜谱中,我们将探讨如何使类似yourserver/path1/path2的 URL 对用户可访问。这可以是网页或返回任意数据以供其他程序消费的路径。在后一种情况下,你通常会使用 JSON 格式来消费参数并提供你的数据。
准备工作
我们将使用hostel.student模型,这是我们之前在第四章“应用模型”中讨论过的;因此,如果你还没有这样做,请从github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter13/00_initial_module获取初始模块,这样你才能跟随示例。
我们希望允许任何用户查询宿舍中所有学生的完整列表。此外,我们还想通过 JSON 请求向程序提供相同的信息。让我们看看如何实现它。
如何做...
我们需要添加控制器,这些控制器按照惯例放入一个名为controllers的文件夹中:
-
添加一个
controllers/main.py文件,其中包含我们页面的 HTML 版本,如下所示:from odoo import http from odoo.http import request class Main(http.Controller): @http.route('/my_hostel/students', type='http', auth='none') def students(self): students = request.env['hostel.student'].sudo().search([]) html_result = '<html><body><ul>' for student in students: html_result += "<li> %s </li>" % student.name html_result += '</ul></body></html>' return html_result -
添加一个函数以 JSON 格式提供相同的信息,如下例所示:
@http.route('/my_hostel/students/json', type='json', auth='none') def students_json(self): records = request.env['hostel.student'].sudo().search([]) return records.read(['name']) -
添加
controllers/__init__.py文件,如下所示:from . import main -
将
controllers导入到你的my_hostel/__init__.py文件中,如下所示:from . import controllers
重新启动你的服务器后,你可以在浏览器中访问/my_hostel/students,你将看到一个学生姓名的平面列表。

图 13.1 – 学生列表
要测试 JSON-RPC 部分,你必须构建一个 JSON 请求。一个简单的方法是使用以下命令在命令行接收输出:
curl -i -X POST -H "Content-Type: application/json" -d "{}"
localhost:8069/my_hostel/students/json
如果你此时遇到404错误,可能是因为你的实例上有多个数据库可用。如果是这样,Odoo 无法确定哪个数据库是用来处理请求的。
使用--db-filter='^yourdatabasename$'参数强制 Odoo 使用你安装模块的确切数据库。路径现在应该是可访问的。
它是如何工作的...
这里有两个关键点:我们的控制器是从 odoo.http.Controller 派生的,而我们用来服务内容的函数被 odoo.http.route 装饰。以与模型注册类似的方式,通过从 odoo.models.Model 继承来注册控制器。此外,Controller 有一个元类来处理这个问题。

图 13.2 – 控制器图
通常,您附加的路径应该以您的附加组件的名称开头,以避免名称冲突。当然,如果您扩展了附加组件的一些功能,您将使用这个附加组件的名称。
odoo.http.route
route 装饰器允许我们告诉 Odoo,一个方法首先应该是网络可访问的,第一个参数决定了它在哪个路径上是可访问的。如果您使用同一个函数来服务多个路径,您也可以传递一个字符串列表,而不是单个字符串。
type 参数默认为 http,它决定了将服务哪种类型的请求。虽然严格来说 JSON 是 HTTP,但将第二个函数声明为 type='json' 会让事情变得容易得多,因为 Odoo 会为我们处理类型转换。
目前不必担心 auth 参数;它将在本章的 限制对网络可访问路径的访问 菜单中讨论。
返回值
Odoo 对函数返回值的处理由 route 装饰器的 type 参数决定。对于 type='http',我们通常希望提供一些 HTML,所以第一个函数简单地返回一个包含它的字符串。另一种选择是使用 request.make_response(),这让您可以控制发送到响应中的头部。因此,为了指示我们的页面最后一次更新时间,我们可能将 students() 中的最后一行代码更改为以下代码:
return request.make_response(
html_result, headers=[
('Last-modified', email.utils.formatdate(
(
fields.Datetime.from_string(
request.env['hostel.student'].sudo()
.search([], order='write_date desc', limit=1)
.write_date) -
datetime.datetime(1970, 1, 1)
).total_seconds(),
usegmt=True)),
])
这段代码在生成的 HTML 中发送一个 Last-modified 头部,告诉浏览器列表最后一次修改的时间。我们可以从 hostel.student 模型的 write_date 字段中提取这个信息。
为了使前面的代码片段正常工作,您需要在文件顶部添加一些导入,如下所示:
import email
import datetime
from odoo import fields
您也可以手动创建一个 werkzeug 的 Response 对象并返回它,但这样做所获得的收益很小。
重要信息
生成 HTML 手动对于演示目的来说很棒,但在生产代码中你永远不应该这样做。始终使用模板,如在第十五章中演示的创建或修改模板 – QWeb食谱所示,Web 客户端开发,并通过调用request.render()返回它们。这将免费提供本地化,并通过将业务逻辑与表示层分离来使你的代码更好。此外,模板为你提供了在输出 HTML 之前转义数据的函数。前面的代码容易受到跨站脚本攻击(如果用户设法将script标签滑入书名,例如)。
对于 JSON 请求,只需返回你想要传递给客户端的数据结构;Odoo 会负责序列化。为了使其工作,你应该限制自己使用 JSON 可序列化的数据类型,这通常意味着字典、列表、字符串、浮点数和整数。
odoo.http.request
request对象是一个静态对象,它引用当前处理的请求,其中包含你执行操作所需的一切。这里最重要的方面是request.env属性,它包含一个Environment对象,这与模型的self.env完全相同。此环境绑定到当前用户,在先前的示例中不是这样,因为我们使用了auth='none'。没有用户也是为什么在示例代码中我们必须使用sudo()来调用模型方法的原因。
如果你习惯了 Web 开发,你会期望会话处理,这是完全正确的。使用request.session来获取OpenERPSession对象(这是对werkzeug的Session对象的薄包装)和request.session.sid来访问会话 ID。要存储会话值,只需将request.session当作一个字典来处理,如下面的示例所示:
request.session['hello'] = 'world'
request.session.get('hello')
重要注意事项
注意,在会话中存储数据与使用全局变量没有区别。只有在必须的情况下才这样做。这通常适用于多请求操作,例如在website_sale模块中的结账操作。
还有更多...
route装饰器可以有一些额外的参数,以便进一步自定义其行为。默认情况下,所有 HTTP 方法都是允许的,Odoo 会混合传递的参数。使用methods参数,你可以传递一个要接受的方法列表,这通常会是['GET']或['POST']之一。
要允许跨源请求(出于安全和隐私原因,浏览器会阻止来自加载脚本以外的域的 AJAX 和其他类型的请求),将cors参数设置为*以允许来自所有源的请求,或者设置为 URI 以限制请求只能来自该 URI。如果此参数未设置,默认情况下,Access-Control-Allow-Origin头不会设置,这将使你保持浏览器的标准行为。在我们的示例中,我们可能想要在
/my_module/students/json以允许从其他网站拉取的脚本访问学生列表。
默认情况下,Odoo 通过在每个请求中传递一个令牌来保护某些类型的请求免受跨站请求伪造攻击。如果你想关闭这个功能,将csrf参数设置为False,但请注意,这通常不是一个好主意。
参见
参考以下要点了解有关 HTTP 路由的更多信息:
-
如果你在一个实例上托管多个 Odoo 数据库,那么不同的数据库可能运行在不同的域上。如果是这样,你可以使用
--db-filter选项,或者你可以使用来自github.com/OCA/server-tools的dbfilter_from_header模块,该模块可以帮助你根据域过滤数据库。 -
要了解使用模板如何使模块化成为可能,请查看本章后面的修改现有处理器食谱。
限制对网络访问路径的访问
作为 Odoo 开发者,你主要关心的问题之一是确保应用程序的安全性。限制对可网络访问路径的访问是访问控制的一个关键方面。这涉及到确定谁可以或不能访问 Odoo 应用程序中的特定路由或功能。Odoo 提供了不同的认证机制来控制用户访问。理解和实施这些机制对于确保只有授权用户可以与应用程序的敏感或受保护部分交互至关重要。例如,你可能只想限制某些路由对已认证用户开放。
在本食谱中,我们将探讨 Odoo 提供的三种认证机制。我们将使用不同的认证机制定义路由,以展示它们之间的差异。
准备工作
在扩展前一个食谱中的代码时,我们还将依赖于第四章,应用程序模型中的hostel.student模型,因此你应该检索其代码以便继续。
如何操作...
在controllers/main.py中定义处理器:
-
添加一个路径,显示所有学生,如下所示:
@http.route('/my_hostel/all-students', type='http', auth='none') def all_students(self): students = request.env['hostel.student'].sudo().search([]) html_result = '<html><body><ul>' for student in students: html_result += "<li> %s </li>" % student.name html_result += '</ul></body></html>' return html_result -
添加一个路径,显示所有学生,并指明哪些属于当前用户(如果有)。这如下面的示例所示:
@http.route('/my_hostel/all-students/mark-mine', type='http', auth='public') def all_students_mark_mine(self): students = request.env['hostel.student'].sudo().search([]) hostels = request.env['hostel.hostel'].sudo().search([('rector', '=', request.env.user.partner_id.id)]) hostel_rooms = request.env['hostel.room'].sudo().search([('hostel_id', 'in', hostels.ids)]) html_result = '<html><body><ul>' for student in students: if student.id in hostel_rooms.student_ids.ids: html_result += "<li> <b>%s</b> </li>" % student.name else: html_result += "<li> %s </li>" % student.name html_result += '</ul></body></html>' return html_result -
添加一个路径,显示当前用户的学生,如下所示:
@http.route('/my_hostel/all-students/mine', type='http', auth='user') def all_students_mine(self): hostels = request.env['hostel.hostel'].sudo().search([('rector', '=', request.env.user.partner_id.id)]) students = request.env['hostel.room'].sudo().search([('hostel_id', 'in', hostels.ids)]).student_ids html_result = '<html><body><ul>' for student in students: html_result += "<li> %s </li>" % student.name html_result += '</ul></body></html>' return html_result
使用此代码,/my_hostel/all-students和/my_hostel/all-students/mark-mine路径对于未认证用户看起来相同,而登录用户在后者路径上会看到他们的学生以粗体显示。

图 13.3 – 标记为我的学生 – 已登录
以下截图显示了未登录时的结果:

图 13.4 – 标记为我的学生 – 未登录
/my_hostel/all-students/mine路径对未经身份验证的用户完全不可访问。如果您未经身份验证尝试访问它,您将被重定向到登录屏幕。

图 13.5 – 通过未经身份验证的用户访问
它是如何工作的...
身份验证方法之间的区别基本上就是您可以从request.env.user的内容中期待什么。
对于auth='none',用户记录始终为空,即使经过身份验证的用户访问该路径也是如此。如果您想提供不依赖于用户的资源,或者想在服务器范围内的模块中提供数据库无关的功能,请使用此选项。
auth='public'值将用户记录设置为具有 XML ID base.public_user的特殊用户,对于未经身份验证的用户,以及对于经过身份验证的用户。如果您想同时向未经身份验证的和经过身份验证的用户提供功能,而经过身份验证的用户还能获得一些额外功能,如前述代码所示,这是一个正确的选择。
使用auth='user'确保只有经过身份验证的用户才能访问您提供的内容。使用这种方法,您可以确保request.env.user指向一个现有用户。
还有更多...
身份验证方法的魔力发生在基础附加组件的ir.http模型中。无论您在路由中传递给auth参数的值是什么,Odoo 都会在这个模型上搜索一个名为_auth_method_<yourvalue>的函数,这样您就可以通过继承它并声明一个处理您选择的身份验证方法的方法来轻松地自定义它。
例如,我们将提供一个名为base_group_user的认证方法,它将仅在当前登录用户是base.group_user组的一部分时授权用户,如下例所示:
from odoo import exceptions, http, models
from odoo.http import request
class IrHttp(models.Model):
_inherit = 'ir.http'
def _auth_method_base_group_user(self):
self._auth_method_user()
if not request.env.user.has_group('base.group_user'):
raise exceptions.AccessDenied()
现在,您可以在装饰器中使用auth='base_group_user',并确信运行此路由处理器的用户是该组的成员。通过一点技巧,您可以将其扩展到auth='groups(xmlid1,...)';其实现留作练习,但包含在 GitHub 仓库示例代码的Chapter13/02_paths_auth/my_hostel/models/sample_auth_http.py中。
消费传递给处理器的参数
能够展示内容是件好事,但将内容作为用户输入的结果展示会更好。本食谱将演示接收这种输入并对其做出反应的不同方式。与先前的食谱一样,我们将利用hostel.student模型。
如何实现...
首先,我们将添加一个期望传统参数(学生 ID)以显示一些详细信息的路由。然后,我们将再次这样做,但我们将参数纳入路径本身:
-
添加一个期望学生 ID 作为参数的路径,如下例所示:
@http.route('/my_hostel/student_details', type='http', auth='none') def student_details(self, student_id): record = request.env['hostel.student'].sudo().browse(int(student_id)) return u'<html><body><h1>%s</h1>Room No: %s' % ( record.name, str(record.room_id.room_no) or 'none') -
添加一个可以在路径中传递学生 ID 的路径,如下所示:
@http.route("/my_hostel/student_details/<model('hostel.student'):student>", type='http', auth='none') def student_details_in_path(self, book): return self.student_details(student.id)
如果您将浏览器指向/my_hostel/student_details?student_id=1,您应该看到 ID 为 1 的学生的详细页面。

图 13.6 – 学生详情网页
如果这个文件不存在,您将收到一个错误页面。

图 13.7 – 学生未找到:错误页面
它是如何工作的...
默认情况下,Odoo(实际上,werkzeug)将GET和POST参数混合在一起,并将它们作为关键字参数传递给您的处理程序。因此,通过简单地将您的函数声明为期望一个名为student_id的参数,您就引入了这个参数,无论是作为GET(URL 中的参数)还是POST(通常通过具有您的处理程序作为action属性的<form>元素传递)。鉴于我们没有为这个参数添加默认值,如果在未设置参数的情况下尝试访问这个路径,运行时将引发错误。
第二个示例利用了在werkzeug环境中,大多数路径实际上都是虚拟的这一事实。因此,我们可以简单地定义我们的路径为包含一些输入。在这种情况下,我们说我们期望hostel.student实例的 ID 作为路径的最后一个组成部分。冒号后面的名称是关键字参数的名称。我们的函数将被调用,并将此参数作为关键字参数传递。在这里,Odoo 负责查找此 ID 并传递一个浏览记录,当然,这只有在访问此路径的用户具有适当的权限时才有效。鉴于student是一个浏览记录,我们可以通过传递student.id作为student_id参数来简单地重用第一个示例中的函数,输出相同的内容。
还有更多...
在路径内定义参数是werkzeug提供的一项功能,称为converters。model转换器是由 Odoo 添加的,它还定义了接受以逗号分隔的 ID 列表的转换器模型,并将包含这些 ID 的记录集传递给您的处理程序。
转换器的美妙之处在于,运行时会将参数强制转换为期望的类型,而您在使用常规关键字参数时则要自己处理。这些参数作为字符串提供,您必须自己处理必要的类型转换,如第一个示例所示。
内置的werkzeug转换器不仅包括int、float和string,还包括更复杂的类型,如path、any和uuid。您可以在werkzeug.palletsprojects.com/en/2.3.x/中查找它们的语义。
参见
如果您想了解更多关于 HTTP 路由的信息,请参考以下要点:
-
Odoo 的自定义转换器在基础模块中的
ir_http.py中定义,并在ir.http的_get_converters类方法中注册 -
如果您想了解更多关于路由上表单提交的信息,请参考第十四章中的从用户获取输入配方,CMS 网站开发
修改现有的处理器
当你安装网站模块时,/website/info 路径会显示有关你的 Odoo 实例的一些信息。在这个食谱中,我们将覆盖它以更改这个信息页面的布局,以及更改显示的内容。
准备工作
安装 website 模块并检查 /website/info 路径。在这个食谱中,我们将更新 /website/info 路由以提供更多信息。
如何操作...
我们将不得不调整现有的模板并覆盖现有的处理器。我们可以这样做:
-
在名为
views/templates.xml的文件中覆盖qweb模板,如下所示:<?xml version="1.0"?> <odoo> <template id="show_website_info" inherit_id="website.show_website_info"> <xpath expr="//dl[@t-foreach='apps']" position="replace"> <table class="table"> <tr t-foreach="apps" t-as="app"> <th> <a t-att-href="app.website" groups='base.group_no_one'> <t t-out="app.name" /> </a> </th> <td> <span t-out="app.summary" /> </td> </tr> </table> </xpath> </template> </odoo> -
在名为
controllers/main.py的文件中覆盖处理器,如下例所示:from odoo import http from odoo.addons.website.controllers.main import Website class WebsiteInfo(Website): @http.route() def website_info(self): result = super(WebsiteInfo, self).website_info() result.qcontext['apps'] = result.qcontext['apps'].filtered( lambda x: x.name != 'website' ) return result -
现在,当访问信息页面时,我们只会看到一个表格中过滤后的已安装应用程序列表,而不是原始的定义列表。

图 13.8 – 网站信息页面(原始)
以下截图显示了自定义页面:

图 13.9 – 网站信息页面(自定义)
工作原理…
在第一步中,我们覆盖了一个现有的 QWeb 模板。为了找出它是哪一个,你将不得不查阅原始处理器的代码。通常,这会给你类似以下这样的行,告诉你需要覆盖 template.name:
return request.render('template.name', values)
在我们的案例中,使用的处理器是一个名为 website_info 的模板,但这个模板立即被另一个名为 website.show_website_info 的模板扩展,所以覆盖这个模板更方便。在这里,我们用表格替换了显示已安装应用程序的定义列表。有关 QWeb 继承如何工作的详细信息,请参阅第十五章,Web 客户端开发。
为了覆盖处理器方法,我们必须识别定义处理器的类,在这个例子中是 odoo.addons.website.controllers.main.Website。我们需要导入这个类以便能够从它继承。现在,我们可以覆盖这个方法并更改传递给响应的数据。请注意,这里覆盖的处理器返回的是一个 Response 对象,而不是像之前的食谱那样返回一个 HTML 字符串,为了简洁起见。这个对象包含要使用的模板的引用以及模板可访问的值,但它仅在请求的末尾进行评估。
通常,有三种方法可以更改现有的处理器:
-
如果它使用 QWeb 模板,最简单的方法是覆盖模板。这对于布局更改和小的逻辑更改是正确的选择。
-
QWeb 模板接收一个上下文传递,这个上下文在响应中作为
qcontext成员可用。这通常是一个字典,你可以添加或删除值以满足你的需求。在先前的例子中,我们过滤了应用程序列表,只显示网站上的应用程序。 -
如果处理程序接收参数,你还可以预处理这些参数,以便覆盖的处理程序按你想要的方式行为。
更多内容...
如前所述,与控制器一起使用的继承与模型继承略有不同;实际上你需要对基类有一个引用,并对其使用 Python 继承。
不要忘记用@http.route装饰器装饰你的新处理程序;Odoo 将其用作标记,用于确定哪些方法暴露给网络层。如果你省略了装饰器,实际上会使处理程序的路径不可访问。
@http.route装饰器本身的行为类似于字段声明 – 你没有设置的每个值都将从你正在覆盖的函数的装饰器中继承,所以我们不需要重复我们不想更改的值。
在从你覆盖的函数接收response对象后,你可以做很多不仅仅是改变 QWeb 上下文的事情:
-
你可以通过操作
response.headers来添加或删除 HTTP 头。 -
如果你想要渲染一个完全不同的模板,你可以覆盖
response.template。 -
要检测响应是否最初基于 QWeb,请查询
response.is_qweb。 -
通过调用
response.render()可以获得生成的 HTML 代码。
相关内容
- 关于 QWeb 模板的详细信息将在第十五章,Web 客户端开发中给出。
提供静态资源
网页包含多种类型的静态资源,例如图片、视频、CSS 等。在本食谱中,我们将了解如何管理模块中的此类静态资源。
准备工作
对于这个食谱,我们将在页面上显示一个图片。从上一个食谱中获取my_hostel模块。从你的系统中选择任何图片,并将该图片放入/my_hostel/static/src/img目录中。
如何操作...
按照以下步骤在网页上显示图片:
-
将你的图片添加到
/my_hostel/static/src/img目录中。 -
在
controller中定义新的路由。在代码中,将图片 URL 替换为你的图片 URL:@http.route('/demo_page', type='http', auth='none') def students(self): image_url = '/my_hostel/static/src/image/odoo.png' html_result = """<html> <body> <img src="img/%s"/> </body> </html>""" % image_url return html_result -
重新启动服务器并更新模块以应用更改。现在,访问
/demo_page以查看页面上的图片。![图 13.10 – 网页上的静态图片]()
图 13.10 – 网页上的静态图片
它是如何工作的…
所有放置在/static文件夹下的文件都被视为静态资源,并且是公开可访问的。在我们的例子中,我们将图片放在了/static/src/img目录中。你可以在static目录下的任何位置放置静态资源,但基于文件类型,有一个推荐的目录结构:
-
/static/src/img是用于图片的目录 -
/static/src/css是用于 CSS 文件的目录 -
/static/src/scss是用于 SCSS 文件的目录 -
/static/src/fonts是用于字体文件的目录 -
/static/src/js是用于 JavaScript 文件的目录 -
/static/src/xml是用于客户端 QWeb 模板的 XML 文件的目录 -
/static/lib是用于外部库文件的目录
在我们的示例中,我们在页面上展示了一张图片。您也可以直接从 /my_hostel/static/src/image/odoo.png 访问该图片。
在这个示例中,我们在网页上展示了一个静态资源(一张图片),并看到了不同静态资源推荐使用的目录。接下来章节中,我们将看到更多简单的方式来展示页面内容和静态资源。
第十四章:CMS 网站开发
Odoo 内置了一个名为网站构建器的功能,这是一个强大的工具,允许您在 Odoo ERP 生态系统中创建和管理网站。它提供了一种用户友好且直观的网页设计方法,使得没有广泛技术知识的使用者也能轻松使用。
以下是 Odoo 网站构建器的关键功能和方面:
-
拖放界面:网站构建器提供了一个拖放界面,允许您轻松地在网页上添加和排列各种内容元素。这包括文本、图片、视频、表单、按钮等等。
-
预设计模板:Odoo 提供了一系列预设计的网站模板,您可以用作起点。这些模板是可定制的,并且可以根据您的品牌形象进行调整。
-
响应式设计:使用 Odoo 创建的网站设计为响应式,这意味着它们会自动适应不同的屏幕尺寸和设备,确保在桌面、平板电脑和智能手机上提供一致的用户体验。
-
内容管理:您可以轻松创建和管理网页、博客、产品列表以及其他类型的内容。网站构建器提供了一个内容管理系统(CMS)来组织和更新您的内容。
-
搜索引擎优化(SEO):Odoo 包含了 SEO 工具,允许您设置元数据、定义 SEO 友好的 URL 并管理网站地图,以提高您网站在搜索引擎中的可见性。
-
多语言支持:Odoo 支持多种语言,使其适合拥有国际受众的企业。您可以翻译内容并适应不同地区。
-
与其他 Odoo 模块集成:使用 Odoo 网站构建器的优势之一是它与其他 Odoo 模块(如 CRM、销售、库存等)的无缝集成。这意味着您可以在一个统一的系统中管理您业务的各个方面。
-
分析和报告:Odoo 提供了内置的分析和报告工具,以跟踪您网站的绩效,包括访客统计、转化率等等。
-
定制开发:对于有独特需求的企业,Odoo 的模块化架构允许进行定制开发以扩展平台的功能。
在本章中,您将探索 Odoo 网站的定制功能发展,并学习如何创建网页。您还将学习如何创建用户可以在页面上拖放的基本构建块。本章还涵盖了高级功能,如Urchin 跟踪模块(UTMs)、SEO、多网站、GeoIP 和网站地图。
在本章中,我们将介绍以下菜谱:
-
管理资产
-
为网站添加 CSS 和 JavaScript
-
创建或修改模板
-
管理动态路由
-
向用户提供静态片段
-
向用户提供动态片段
-
从网站用户那里获取输入
-
管理网站 SEO 选项
-
管理网站地图
-
获取访客的国家信息
-
跟踪营销活动
-
管理多个网站
-
重定向旧 URL
-
发布网站相关记录的管理
管理资源
在 Odoo 的上下文中,资源指的是各种类型的资源,例如 Cascading Style Sheets(CSS)、JavaScript 文件、字体和图像,这些资源被用来增强网站的外观和功能。在 Odoo 中管理资源对于维护一个结构良好且高效的网站非常重要。当浏览器中加载一个页面时,这些静态文件会向服务器发出单独的请求。请求的数量越多,网站的速度就越低。为了避免这个问题,大多数网站通过合并多个文件来提供静态资源。市场上有一些工具用于管理这类事情,但 Odoo 有自己的静态资源管理实现。
Odoo 中的资源包和不同资源是什么?
在 Odoo 中,资源包是不同资源的集合,例如 CSS、JavaScript 文件和其他资源,它们被分组在一起以便在您的网站上高效且有序地加载。资源包通过允许您定义哪些资源应该一起加载来帮助管理这些资源的加载,从而提高性能并确保网站正常工作。资源包的工作是将所有 JavaScript 和 CSS 合并到一个文件中,并通过最小化来减小其大小。
下面是 Odoo 中使用的不同资源包:
-
web._assets_primary_variables -
web._assets_secondary_variables -
web.assets_backend -
web.assets_frontend -
web.assets_frontend_minimal -
web.assets_frontend_lazy -
web.report_assets_common -
web.report_assets_pdf -
web.assets_web_dark -
web._assets_frontend_helpers -
web_editor.assets_wysiwyg -
website.assets_wysiwyg -
website.assets_editor
重要信息
对于某些特定应用,还有一些其他资源包被使用;
例如,point_of_sale.assets、survey.survey_assets、mass_mailing.layout 和 website_slides.slide_embed_assets。
Odoo 通过位于 /odoo/addons/base/models/assetsbundle.py 的 AssetBundle 类来管理其静态资源。
现在,AssetBundle 不仅合并多个文件,还包含更多功能。以下是它提供的功能列表:
-
在 Odoo 网站的上下文中,资源指的是各种类型的资源,例如层叠样式表(CSS)、JavaScript 文件、字体和图像,这些资源被用来增强网站的外观和功能。在 Odoo 中管理资源对于维护一个结构良好且高效的网站非常重要。当浏览器中加载一个页面时,这些静态文件会向服务器发出单独的请求。请求的数量越多,网站的速度就越低。为了避免这个问题,大多数网站通过合并多个文件来提供静态资源。市场上有一些工具用于管理这类事情,但 Odoo 有自己的静态资源管理实现。
-
它通过从文件内容中移除注释、额外空格和换行符来最小化 JavaScript 和 CSS 文件。移除这些额外数据将减小静态资源的大小并提高页面加载速度。
-
它内置了对 CSS 预处理器,如 Sassy CSS(SCSS)和 Leaner Style Sheets(LESS)的支持。这意味着您可以添加 SCSS 和 LESS 文件,并且它们将自动被编译并添加到包中。
自定义资源
正如我们所见,Odoo 为不同的代码库提供了不同的资产。为了得到正确的结果,您需要选择正确的资产包来放置您的自定义 JavaScript 和 CSS 文件。例如,如果您正在设计一个网站,您需要将文件放入 web.assets_frontend。尽管这种情况很少见,但有时您需要创建一个全新的资产包。您可以在下一节中了解到如何创建自己的资产包。
如何做到这一点...
要加载资产,您可以在模块的 __manifest__.py 文件中使用 web.assets_frontend 模板;例如:
'assets': {
'web.assets_backend': [
'my_hostel/static/src/xml/**/*',
],
'web.assets_frontend: [
'my_hostel/static/lib/bootstrap/**/*',
'my_hostel/static/src/js/**',
'my_hostel/static/src/scss/**',
],
},
这里是一些最重要的包:
-
web.assets_common -
web.assets_backend -
web.assets_frontend -
web.qunit_suite_tests -
web.qunit_mobile_suite_tests
操作
这里是针对特定资产文件的所有指令:
-
before -
after -
replace -
remove
append
追加资产操作是指向由其他模块或 Odoo 核心提供的现有包或模板中添加额外的 CSS 或 JavaScript 文件。这允许您扩展功能或外观,而无需直接修改原始代码;例如:
'web.assets_common': [
'my_hostel/static/src/js/**/*',
],
总是考虑您的资产加载顺序。如果您的代码依赖于其他资产中定义的任何特定库或功能,请确保它们以正确的顺序加载,以避免冲突或错误。
prepend
在 Odoo 中,将资产(如 CSS 或 JavaScript 文件)添加到现有包或模板(由其他模块或 Odoo 核心提供)的开头,涉及到添加您自己的 CSS 或 JavaScript 文件。这有助于确保您的自定义更改优先于现有的样式或脚本;例如:
'web.assets_common': [
('prepend','my_hostel/static/src/css/bootstrap_overridden.scss'),
],
确定您的资产加载顺序。追加资产意味着它们将在其他样式或脚本之前加载,可能会影响功能或设计。在覆盖核心功能时要谨慎。
before
在 Odoo 中,将资产(如 CSS 或 JavaScript 文件)组织在其他模块的资产之前,涉及到控制资源加载顺序以确保您的模块文件在其他模块的文件之前加载;例如:
'web.assets_common': [
('before', 'web/static/src/css/bootstrap_overridden.scss', 'my_hostel/static/src/css/bootstrap_overridden.scss'),
],
确保您在加载资源之前引用的是您想要加载的模块的正确资产或模板。错误的引用可能会导致错误或意外的行为。
after
在 Odoo 中,将 CSS 或 JavaScript 文件等资产组织在其他模块的资产之后,涉及到控制加载顺序以确保您的模块文件在其他模块的文件之后加载。这在您需要您的资产依赖于或覆盖其他模块的样式或脚本时很有用;例如:
'web.assets_common': [
('after', 'web/static/src/css/list_view.scss', 'my_hostel/static/src/css/list_view.scss'),
],
使用 after 属性或 Python 代码控制加载顺序有助于确保您的模块资产在其他模块之后加载,从而有效地管理依赖项和自定义。
include
在 Odoo 中,包括 CSS 或 JavaScript 文件等资产涉及将这些资源链接到您的模块或主题以增强其功能或外观;例如:
'web.assets_common': [
('include', 'web._primary_variables'),
],
在 Odoo 中包括资产允许您通过添加自定义样式或脚本扩展模块的功能,增强用户体验和功能。
删除
删除一个或多个文件。
删除资产,例如 CSS 或 JavaScript 文件,在 Odoo 中涉及从您的模块资产中排除它们;例如:
'web.assets_common': [
('remove', 'web/static/src/js/boot.js'),
],
在 Odoo 中删除资产允许您通过排除不需要或与您的模块功能冲突的特定样式或脚本来自定义您的模块。
替换
在 Odoo 中,替换资产涉及在您的模块或主题中用新 CSS 或 JavaScript 文件替换现有文件;例如:
'web.assets_common': [
('replace', 'web/static/src/js/boot.js', 'my_addon/static/src/js/
boot.js'),
],
在 Odoo 中替换资产允许您通过用新文件替换现有文件来更新和自定义模块的外观或功能。替换资产时要谨慎,以保持应用程序的稳定性和功能。
加载顺序
在 Odoo 中,管理资产的加载顺序(CSS、JavaScript 等)对于确保依赖关系正确解决和用户界面正确渲染至关重要。加载顺序可以控制以确定哪些资产首先加载或在其他资产之后加载;例如:
'web.assets_common': [
'my_addon/static/lib/jquery/jquery.js',
'my_addon/static/lib/jquery/**/*',
],
当调用资产包时(例如,t-call-assets="web.assets_common"),会生成一个空的资产列表。
所有与该包匹配的类型为ir.asset的记录都会被检索并按序列号排序。然后,所有序列严格小于 16 的记录都会被处理并应用于当前资产列表。
所有在其清单中声明该包资产的模块都会将它们的资产操作应用于此列表。这是按照模块依赖顺序进行的(例如,网站资产在网站之前处理)。如果指令尝试添加列表中已存在的文件,则对该文件不进行任何操作。换句话说,列表中只保留文件的第一种出现。
然后,处理并应用剩余的ir.asset记录(序列号大于或等于 16 的记录)。
在清单中声明的资产可能需要按特定顺序加载;例如,在加载lib文件夹时,jquery.js必须先于所有其他jquery脚本加载。一个解决方案是创建一个序列号较低的ir.asset记录或prepend指令,但还有另一种更简单的方法。
更多...
如果您在 Odoo 中处理资产,以下是一些您需要了解的事项。
在 Odoo 中调试 JavaScript 可能非常困难,因为AssetBundle将多个 JavaScript 文件合并成一个文件,并且还会对其进行压缩。通过启用带有资产的开发者模式,您可以跳过资产打包,页面将单独加载静态资产,以便您可以轻松调试。
合并资源一次生成并存储在ir.attachment模型中。之后,它们从附件中提供。如果您想重新生成资源,可以从调试选项中进行,如下面的截图所示:

图 14.1 – 资源激活选项的截图
小贴士
如您所知,Odoo 只会生成一次资源。在开发过程中,这种行为可能会引起头痛,因为它需要频繁重启服务器。为了克服这个问题,您可以在命令行中使用dev=xml,这将直接加载资源,因此不需要重启服务器。
懒加载
懒加载是一种技术,它将非关键资源的加载推迟到需要时,通常用于图像、脚本或其他资源以提高性能:
await loadAssets({
jsLibs: ["/web/static/lib/stacktrace-js/stacktrace.js"],
});
然而,在 Odoo 中实现特定资源或组件的懒加载可以通过自定义开发或利用第三方库来实现。以下是一些您可能考虑的方法。
懒加载图像
您可以使用如 Intersection Observer 这样的 JavaScript 库来实现图像的懒加载。这个库允许您在图像进入用户的视口时才加载图像。
-
使用 Intersection Observer,JavaScript 代码可能如下所示:
document.addEventListener("DOMContentLoaded", function () { var lazyImages = [].slice.call(document.querySelectorAll("img.lazy")); if ("IntersectionObserver" in window) { let lazyImageObserver = new \ IntersectionObserver(function (entries, \ observer) { entries.forEach(function (entry) { if (entry.isIntersecting) { let lazyImage = entry.target; lazyImage.src = \ lazyImage.dataset.src; lazyImage.classList.remove("lazy"); lazyImageObserver.unobserve(lazyImage); } }); }); lazyImages.forEach(function (lazyImage) { lazyImageObserver.observe(lazyImage); }); } });然后,您需要将
lazy类分配给您的<img>标签,并使用data-src属性作为实际图像源。 -
将
lazy类和data-src属性添加到图像标签中。
为网站添加 CSS 和 JavaScript
通过模块的资产管理系统管理如 CSS、JavaScript 和其他静态文件。您可以通过在清单文件中定义它们并将它们链接到视图或模板来控制这些资源在您的模块中的加载。
这是一份关于如何在 Odoo 中管理 CSS 和 JavaScript 的概述。
在模块清单中定义资源(manifest.py)
在清单文件中,指定模块所需的资源:
'assets': {
'web.assets_frontend': [
'my_hostel/static/src/scss/hostel.scss',
'my_hostel/static/src/js/hostel.js',
],
},
我们将添加 CSS、SCSS 和 JavaScript 文件,这些文件将修改网站。由于我们在修改网站,我们需要将网站作为依赖项添加。修改清单文件如下:
'depends': ['base', 'website'],
将一些 SCSS 代码添加到static/src/scss/hostel.scss中,如下所示:
$my-bg-color: #1C2529;
$my-text-color: #D3F4FF;
nav.navbar {
background-color: $my-bg-color !important;
.navbar-nav .nav-link span{
color: darken($my-text-color, 15);
font-weight: 600;
}
}
footer.o_footer {
background-color: $my-bg-color !important;
color: $my-text-color;
}
将一些 JavaScript 代码添加到static/src/js/my_library.js中,如下所示:
/** @odoo-module **/
import { _t } from "@web/core/l10n/translation";
import publicWidget from "@web/legacy/js/public/public_widget";
publicWidget.registry.MyHostel = publicWidget.Widget.extend({
selector: '#wrapwrap',
init() {
this._super(...arguments);
this.orm = this.bindService("orm");
alert(_t('Hello world'));
},
});
更新您的模块后,您应该会看到 Odoo 网站在菜单、主体和页脚中具有自定义颜色,并且在每次页面加载时都会出现一个有点令人烦恼的 Hello World 弹出窗口,如下面的截图所示:

图 14.2 – JavaScript 代码中 Hello World 弹出窗口的截图
小贴士
对于 CSS/SCSS 文件,有时顺序很重要。因此,如果你需要覆盖另一个插件中定义的样式,你必须确保你的文件在加载原始文件之后。这可以通过调整你的视图的优先级字段或直接从注入 CSS 文件引用的插件视图继承来实现。
我们添加了基本的 SCSS。Odoo 内置了对 SCSS 预处理器的支持。Odoo 将自动将 SCSS 文件编译成 CSS。在我们的示例中,我们使用了带有一些变量和 darken 函数的基本 SCSS,将 $my-text-color 的颜色加深 15%。SCSS 预处理器有众多其他功能;如果你想了解更多关于 SCSS 的信息,请参阅 sass-lang.com/。
创建或修改模板
网站模板使用 QWeb 创建,这是一种与 Odoo 框架无缝集成的模板语言。这些模板用于在 Odoo 网站模块中定义网页的结构和外观。
下面是关于如何在 Odoo 中处理网站模板的概述。
理解 QWeb 模板
Odoo 中的 QWeb 模板允许你使用类似于 XML 的语法,结合控制结构和占位符,创建动态网页。它们使你能够定义网页的结构、内容和展示。
创建基本的网站模板
要创建一个简单的网站模板,请按照以下步骤操作:
views 目录:
<!-- Example: custom_template.xml -->
<template id="custom_template" name="Custom Template">
<t t-call="website.layout">
<t t-set="page_title">Custom Page</t>
<!-- Your content here -->
<div class="custom-content">
<h1>Welcome to my custom page!</h1>
<p>This is a custom template created in Odoo.</p>
</div>
</t>
</template>
下面是对代码的解释:
-
<template>: 定义 QWeb 模板 -
id: 模板的唯一标识符 -
name: 模板的名称 -
<t t-call="website.layout">: 表示此模板继承自网站的主要布局 -
<t t-set="page_title">: 动态设置页面标题 -
<div class="custom-content">: 模板内的内容示例
包含在清单文件中:将你的视图文件包含在模块的清单文件中:
{
# Other manifest information
'data': [
'views/custom_template.xml',
# Other XML or CSV files
],
# Other manifest information
}
使用 Odoo 网站构建器
你还可以使用 Odoo 网站构建器界面,通过预定义的块和模板创建和定制网页。这允许以更直观和互动的方式设计网页,而无需直接编辑 XML 模板。
样式和定制
对于样式和定制,你可以使用 CSS,它可以在你的 QWeb 模板内包含或作为链接到模板的单独文件。
记住——你的网站模板的结构和样式可以根据你的具体需求和创建的网页的复杂性而变化。此外,考虑探索现有的 Odoo 网站模块和官方文档,以获取更多关于 Odoo 框架内 QWeb 模板的详细和高级用法。
循环
要处理记录集或可迭代的数据类型,你需要一个结构来遍历列表。在 QWeb 模板中,这可以通过 t-foreach 元素来完成。迭代可以在 t 元素中发生,在这种情况下,其内容会为 t-foreach 属性中传递的每个可迭代成员重复,如下所示:
<t t-foreach="[1, 2, 3, 4, 5]" t-as="num">
<p><t t-esc="num"/></p>
</t>
这将被渲染如下:
<p>1</p>
<p>2</p>
<p>3</p>
<p>4</p>
<p>5</p>
你也可以将 t-foreach 和 t-as 属性放置在某个任意元素中,此时该元素及其内容将重复迭代器中的每个项目。看看下面的代码块。这将生成与上一个示例完全相同的结果:
<p t-foreach="[1, 2, 3, 4, 5]" t-as="num">
<t t-esc="num"/>
</p>
在我们的例子中,看看 t-call 元素内部,那里实际的内容生成发生。模板期望在具有名为 hostel 的变量的上下文中渲染,该变量在 t-foreach 元素中迭代。t-as 属性是必需的,并将用作迭代变量名以访问迭代数据。虽然这种构造最常用的用途是迭代记录集,但你可以在任何可迭代的 Python 对象上使用它。
动态属性
QWeb 模板可以动态设置属性值。这可以通过以下三种方式实现。
第一种方式是通过 t-att-$attr_name。在模板渲染时,创建一个名为 $attr_name 的属性;它的值可以是任何有效的 Python 表达式。这是使用当前上下文计算得出的,结果被设置为属性的值,如下所示:
<div t-att-total="10 + 5 + 5"/>
它将被渲染如下:
<div total="20"></div>
第二种方式是通过 t-attf-$attr_name。这与前面的选项类似。唯一的区别是只有 {{ ..}} 和 #{..} 之间的字符串会被评估。这在值与字符串混合时很有用。它主要用于评估类,例如在这个例子中:
<t t-foreach="['info', 'danger', 'warning']" t-as="color">
<div t-attf-class="alert alert-#{color}">
Simple bootstrap alert
</div>
</t>
它将被渲染如下:
<div class="alert alert-info">
Simple bootstrap alert
</div>
<div class="alert alert-danger">
Simple bootstrap alert
</div>
<div class="alert alert-warning">
Simple bootstrap alert
</div>
第三种方式是通过 t-att=mapping 选项。此选项接受模板渲染后的字典,字典的数据被转换为属性和值。看看以下示例:
<div t-att="{'id': 'my_el_id', 'class': 'alert alert-danger'}"/>
在渲染此模板之后,它将被转换为以下形式:
<div id="my_el_id" class="alert alert-danger"/>
在我们的例子中,我们使用了 t-attf-class 来根据索引值获取动态背景。
字段
h3 和 div 标签使用 t-field 属性。t-field 属性的值必须与长度为之一的记录集一起使用;这允许用户在以编辑模式打开网站时更改网页内容。当你保存页面时,更新的值将被存储在数据库中。当然,这需要权限检查,并且只有当前用户有显示记录的写入权限时才允许。通过可选的 t-options 属性,你可以向字段渲染器传递一个字典选项,包括要使用的部件。目前,后端部件的选择相当有限。例如,如果你想从二进制字段显示一个图片,那么你可以使用 image 部件,如下所示:
<span t-field="author.image_small" t-options="{'widget': 'image'}"/>
t-field有一些限制。它仅在记录集上工作,并且不能在<t>元素上工作。为此,您需要使用一些 HTML 元素,如<span>或<div>。t-field属性有一个替代方案,即t-esc。t-esc属性不仅限于记录集;它也可以用于任何数据类型,但在网站上不可编辑。
t-esc和t-field之间的另一个区别是t-field根据用户的语言显示值,而t-esc显示数据库中的原始值。例如,对于在偏好设置中配置了英语语言并将datetime字段设置为与t-field一起使用的用户,结果将以12/15/2023 14:17:15的格式渲染。相比之下,如果使用t-esc属性,则结果将以如下渲染格式显示:2023-12-15 21:12:07。
条件语句
请注意,显示发布日期的分区被一个带有t-if属性的t元素包裹。此属性被评估为 Python 代码,并且只有当结果为真值时,元素才会被渲染。在以下示例中,我们只显示设置了发布日期的div类。然而,在复杂情况下,您可以使用t-elif和t-else,如下例所示:
<div t-if="state == 'new'">
Text will be added if the state is new.
</div>
<div t-elif="state == 'progress'">
Text will be added if the state is progress.
</div>
<div t-else="">
Text will be added for all other stages.
</div>
设置变量
QWeb 模板也能够在模板本身中定义变量。定义模板后,您可以在后续模板中使用该变量。您可以这样设置变量:
<t t-set="my_var" t-value="5 + 1"/>
<t t-esc="my_var"/>
子模板
如果您正在开发大型应用程序,管理大型模板可能会很困难。QWeb 模板支持子模板,因此您可以把大型模板分成更小的子模板,并在多个模板中重复使用它们。对于子模板,您可以使用t-call属性,如下例所示:
<template id="first_template">
<div> Test Template </div>
</template>
<template id="second_template">
<t t-call="first_template"/>
</template>
行内编辑
用户将能够在编辑模式下直接从网站上修改记录。使用t-field节点加载的数据默认可编辑。如果用户更改了此类节点中的值并保存页面,后端中的值也将更新。请放心;为了更新记录,用户需要对该记录有写权限。请注意,t-field仅在记录集上工作。要显示其他类型的数据,您可以使用t-esc。这与t-field的工作方式完全相同,但唯一的不同是t-esc不可编辑,并且可以用于任何类型的数据。
如果您想在页面上启用代码片段拖放支持,可以使用oe_structure类。在我们的示例中,我们在模板顶部添加了这一设置。使用oe_structure将启用编辑和代码片段拖放支持。
如果您想在某个块上禁用网站编辑功能,可以使用contenteditable=False属性。这会使元素变为只读。我们在最后一个<section>标签中使用了这个属性。
注意
为了使页面多网站兼容,当你通过网站编辑器编辑页面/视图时,Odoo 将为该网站创建页面的一个单独副本。这意味着后续的代码更新永远不会影响到已编辑的网站页面。为了同时获得内联编辑的便利性和在后续版本中更新你的 HTML 代码的可能性,创建一个包含语义 HTML 元素的一个视图,以及一个包含可编辑元素的第二视图。然后,只有后者视图会被复制,你仍然可以为父视图更新。
对于这里使用的其他 CSS 类,请参考 Bootstrap 的文档。
在 步骤 1 中,我们声明了渲染模板的路由。如果你注意到了,我们在 route() 中使用了 website=True 参数,这将向模板传递一些额外的上下文,例如菜单、用户语言、公司等。这些将在 website.layout 中用于渲染菜单和页脚。website=True 参数还启用了网站的多语言支持,并以更好的方式显示异常。
管理动态路由
在网站开发项目中,我们经常需要创建具有动态 URL 的页面。例如,在电子商务中,每个产品都有一个与不同 URL 链接的详细页面。在这个菜谱中,我们将创建一个网页来显示宿舍详情。
准备工作
在 hostel 模型中添加基本字段:
from odoo import fields, models
class Hostel(models.Model):
_name = 'hostel.hostel'
_description = "Information about hostel"
_order = "id desc, name"
_rec_name = 'hostel_code'
name = fields.Char(string="hostel Name", required=True)
hostel_code = fields.Char(string="Code", required=True)
street = fields.Char('Street')
street2 = fields.Char('Street2')
zip = fields.Char('Zip', change_default=True)
city = fields.Char('City')
state_id = fields.Many2one("res.country.state", string='State')
country_id = fields.Many2one('res.country', string='Country')
phone = fields.Char('Phone',required=True)
mobile = fields.Char('Mobile',required=True)
email = fields.Char('Email')
hostel_floors = fields.Integer(string="Total Floors")
image = fields.Binary('Hostel Image')
active = fields.Boolean("Active", default=True,
help="Activate/Deactivate hostel record")
type = fields.Selection([("male", "Boys"), ("female", "Girls"),
("common", "Common")], "Type", help="Type of Hostel",
required=True, default="common")
other_info = fields.Text("Other Information",
help="Enter more information")
description = fields.Html('Description')
hostel_rating = fields.Float('Hostel Average Rating', digits=(14, 4))
如何操作...
按照以下步骤生成宿舍的详情页面:
-
在
main.py中添加一个新的宿舍详情路由,如下所示:@http.route('/hostel/<model("hostel.hostel"):hostel>', type='http', auth="user", website=True) def hostel_room_detail(self, hostel): return request.render( 'my_hostel.hostel_detail', { 'hostel': hostel, }) -
在
hostel_templates.xml中添加一个新的宿舍详情模板,如下所示:<template id="hostel_detail" name="Hostel Detail"> <t t-call="website.layout"> <div class="container"> <div class="row mt16"> <div class="col-5"> <span t-field="hostel.image" t-options="{ 'widget': 'image', 'class': 'mx-auto d-block img-thumbnail'}"/> </div> <div class="offset-1 col-6"> <h1 t-field="hostel.name"/> <p t-esc="hostel.hostel_rating"></p> <t t-if="hostel.hostel_code"> <div t-field= "hostel.hostel_code" class="text-muted"/> </t> <b class="mt8"> State: </b> <ul> <li t-foreach="hostel.state_id" t-as="state"> <span t-esc="state.name" /> </li> </ul> </div> </div> </div> <div t-field="hostel.description"/> </t> </template> -
在宿舍列表模板中添加一个按钮,如下所示。此按钮将重定向到宿舍详情网页:
... <div t-attf-class="card mt24 #{'bg-light' if hostel_rating else ''}"> <div class="card-body"> <h3 t-field="hostel.name"/> <t t-if="hostel.hostel_rating"> <div t-field="hostel.hostel_rating" class="text-muted"/> </t> <b class="mt8"> Authors </b> <ul> <li t-foreach="hostel.state_id" t-as="state"> <span t-esc="state.name" /> </li> </ul> <a t-attf-href="/hostel/#{hostel.id}" class="btn btn-primary btn-sm"> <i class="fa fa-building"/> Hostel Detail </a> </div> </div> ...
更新 my_hostel 模块以应用更改。更新后,你将在宿舍卡片上看到宿舍详情页面的链接。点击这些链接后,将打开宿舍详情页面。
它是如何工作的...
在 步骤 1 中,我们为宿舍详情页面创建了一个动态路由。在这个路由中,我们添加了 <model("hostel.hostel"):hostel>。它接受整数 URL,例如 /hostel/1。Odoo 将这个整数视为 hostel.hostel 模型的 ID,当访问这个 URL 时,Odoo 会获取一个记录集并将其作为参数传递给函数。因此,当从浏览器访问 /hostel/1 时,hostel_detail() 函数中的 hostel 参数将包含 ID 为 1 的 hostel.hostel 模型的记录集。我们传递了这个 hostel 记录集并渲染了一个名为 my_hostel.hostel2_detail 的新模板。
在 步骤 2 中,我们创建了一个名为 hostel_detail 的新 QWeb 模板,用于渲染宿舍详情页面。这是简单的,并且使用 Bootstrap 结构创建的。如果您检查,我们在详情页面中添加了 html_description。html_description 字段具有 HTML 字段类型,因此您可以在字段中存储 HTML 数据。Odoo 自动为 HTML 类型的字段添加了片段拖放支持。因此,我们现在能够在宿舍详情页面中使用片段。在 HTML 字段中放置的片段存储在宿舍的记录中,因此您可以针对不同的记录设计不同的内容。
在 步骤 3 中,我们添加了一个带有锚点的链接,以便访客可以重定向到宿舍详情页面。
注意
模型路由也支持域过滤。例如,如果您想根据条件限制一些记录,可以通过将域传递给路由来实现,如下所示:
/hostel/<model("hostel.hostel", "[(name','!=', '``Hostel 1')]"):hostel>
这将限制对名为 Hostel 1 的宿舍的访问。
还有更多...
Odoo 使用 werkzeug 来处理 HTTP 请求。Odoo 在 werkzeug 上添加了一个薄薄的包装,以便轻松处理路由。您在上一个示例中看到了 <model("hostel.hostel"):hostel> 路由。这是 Odoo 自己的实现,但它也支持 werkzeug 路由的所有功能。因此,您可以使用类似以下的路由:
-
/page/<int:page>接受整数值 -
/page/<any(about, help):page_name>接受选定值 -
/pages/<page>接受字符串 -
/pages/<category>/<int:page>接受多个值
路由有很多变体可供选择,您可以在 werkzeug.pocoo.org/docs/0.14/routing/ 中了解相关信息。
向用户提供静态片段
静态片段是可以重复使用的组件或 HTML、CSS 和 JavaScript 的代码块,可以通过网站构建器插入到网页中。这些片段允许轻松地自定义和构建网页,无需从头编写代码。
Odoo 的网站编辑器提供了几个编辑构建块,可以根据您的需求拖放到页面上进行编辑。本教程将介绍如何提供您自己的构建块。这些块被称为片段。片段有几种类型,但通常我们可以将它们分为两种:静态和动态。静态片段是固定的,直到用户更改它才会改变。动态片段依赖于数据库记录,并根据记录值进行更改。在本教程中,我们将了解如何创建一个静态片段。
如何操作...
片段实际上只是一个注入到 插入块 栏中的 QWeb 视图。我们将创建一个小片段,用于显示宿舍的图片和标题。您将能够在页面上拖放片段,并且可以编辑图片和文本。按照以下步骤添加一个新的静态片段:
-
添加一个名为
views/snippets.xml的文件,如下所示(不要忘记在清单中注册文件): -
在
views/snippets.xml中为片段添加一个 QWeb 模板,如下所示:<template id="snippet_hostel_card" name="Hostel Card"> <section class="pt-3 pb-3"> <div class="container"> <div class="row align-items-center"> <div class="col-lg-6 pt16 pb16"> <h1>This is Hostel Card Block</h1> <p> Learn snippet development quickly with examples </p> <a class="btn btn-primary" href="#" >Hostel Details</a> </div> <div class="col-lg-6 pt16 pb16"> <img src="img/cover.jpeg" class="mx-auto img-thumbnail w-50 img img-fluid shadow"/> </div> </div> </div> </section> </template> -
按如下方式在片段列表中列出模板:
<template id="hostel_snippets_options" inherit_id="website.snippets"> <xpath expr="//div[@id='snippet_structure']/div[hasclass('o_panel_body')]" position="inside"> <t t-snippet="my_hostel.snippet_hostel_card" t-thumbnail="/my_hostel/static/src/img/s_hostel_thumb.png"/> </xpath> </template> -
在
/my_hostel/static/src/img目录中添加封面图像和片段缩略图。
重新启动服务器并更新 my_hostel 模块以应用更改。当您以编辑模式打开网站页面时,您将能够在片段块面板中看到我们的片段:

图 14.3 – 静态片段截图
它是如何工作的...
静态片段不过是一块 HTML 代码。在 步骤 1 中,我们为宿舍块创建了一个包含我们 HTML 的 QWeb 模板。在这个 HTML 中,我们只使用了 Bootstrap 列结构,但您可以使用任何 HTML 代码。请注意,您在片段的 QWeb 模板中添加的 HTML 代码将在您拖放时添加到页面上。通常,使用 section 元素和 Bootstrap 类为片段是一个好主意,因为对于它们,Odoo 的编辑器提供了编辑、背景和调整大小控件。
在 步骤 2 中,我们在片段列表中注册了我们的片段。您需要继承 website.snippets 以注册片段。在网站编辑器 GUI 中,片段根据其用途分为不同的部分。在我们的例子中,我们通过 xpath 在 Structure 部分注册了我们的片段。要列出您的片段,您需要使用具有 t-snippet 属性的 <t> 标签。t-snippet 属性将具有 QWeb 模板的 XML ID,在我们的例子中是 my_hostel.snippet_hostel_card。您还需要使用 t-thumbnail 属性,该属性用于在网站编辑器中显示一个小片段图像。
注意
website.snippets 模板包含所有默认片段,您可以通过探索 /addons/website/views/snippets/snippets.xml 文件了解更多信息。
/addons/website/views/snippets/snippets.xml file to see all the snippet options. In the next recipe, we will see how to add our own options.
在 步骤 3 中,我们将我们的片段列在 structure 块下。一旦更新模块,您将能够拖放该片段。在 步骤 4 中,我们只为片段缩略图添加了一张图片。
更多内容...
在这种情况下,不需要额外的 JavaScript。Odoo 的编辑器提供了大量的选项和控件,对于静态片段来说已经足够了。您可以在 website/views/snippets.xml 中找到所有现有的片段和选项。
片段选项也支持 data-exclude、data-drop-near 和 data-drop-in 属性,这些属性决定了在将片段从片段栏拖出时可以放置的位置。这些也是 jQuery 选择器,但在本食谱的 步骤 3 中我们没有使用它们,因为我们允许将片段放置在基本上任何内容可以放置的地方。
向用户提供动态片段
动态片段是指能够从数据库、模型或外部服务等各种来源显示动态内容的可重用组件或块。这些片段能够创建灵活且适应性强的网页,显示实时或上下文特定的信息。
识别数据源:
-
确定您想在动态片段中使用的源数据。这可以包括 Odoo 模型、数据库、API 等。
-
使用 QWeb 模板标签 (
{% %}) 或 Odoo 特定指令 (<t t-foreach="..." t-as="...">) 实现动态占位符。
我们将了解如何为 Odoo 创建动态片段。我们将根据数据库值生成内容。
如何操作…
执行以下步骤以添加显示宿舍数据列表的动态片段:
-
在
views/snippets.xml中添加给定的 QWeb 模板片段:<template id="snippet_hostel_dynamic" name="Hostel Dynamic"> <section class="hostel_list"> <div class="container"> <h2>Hostel</h2> <table class="table hostel_snippet table-striped" data-number-of-hostel="5"> <tr> <th>Name</th> <th>Available date</th> </tr> </table> </div> </section> </template> -
注册片段并添加一个选项以更改片段行为:
<template id="hostel_snippets_options" inherit_id="website.snippets"> <!-- register snippet --> <xpath expr="//div[@id='snippet_structure']/ div[hasclass('o_panel_body')]" position="inside"> <t t-snippet="my_hostel.snippet_hostel_dynamic" t-thumbnail="/my_hostel/static/src/img/s_list.png"/> </xpath> <xpath expr="//div[@id='snippet_options']" position="inside"> <!--Add step 3 here --> </xpath> </template> -
然后,为宿舍片段添加片段选项:
<div data-selector=".hostel_snippet"> <we-select string="Table Style"> <we-button data-select-class="table-striped"> Striped </we-button> <we-button data-select-class="table-dark"> Dark </we-button> <we-button data-select-class="table-bordered"> Bordered </we-button> </we-select> <we-button-group string="No of Rooms" data-attribute-name="numberOfRooms"> <we-button data-select-data-attribute="5"> 5 </we-button> <we-button data-select-data-attribute="10"> 10 </we-button> <we-button data-select-data-attribute="15"> 15 </we-button> </we-button-group> </div> -
在
/static/src/snippets.js文件中添加代码以渲染动态片段。 -
添加一个
public小部件以动态渲染宿舍片段:/** @odoo-module **/ import { _t } from "@web/core/l10n/translation"; import publicWidget from "@web/legacy/js/public/public_widget"; publicWidget.registry.HostelSnippet = publicWidget.Widget.extend({ selector: '.hostel_snippet', disabledInEditableMode: false, start: function () { var self = this; var rows = this.$el[0].dataset.numberOfRooms || '5'; this.$el.find('td').parents('tr').remove(); this._rpc({ model: 'hostel.hostel', method: 'search_read', domain: [], fields: ['name', 'hostel_code'], orderBy: [{ name: 'hostel_code', asc: false }], limit: parseInt(rows) }).then(function (data) { _.each(data, function (hostel) { self.$el.append( $('<tr />').append( $('<td />').text(hostel.name), $('<td />').text(hostel.hostel_code) )); }); }); }, }); -
将 JavaScript 文件加载到
__manifest__.py模块中:'assets': { 'web.assets_frontend': [ 'my_hostel/static/src/js/snippets.js', ], },
更新模块后,您将获得一个名为 Hostels 的新片段,该片段有一个选项可以更改最近添加的房间数量。我们还添加了更改表格设计的选项,当您点击表格时可以显示。
它是如何工作的…
在 步骤 1 中,我们为新的片段添加了一个 QWeb 模板(它就像之前的食谱一样)。请注意,我们为表格添加了一个基本结构。我们将在表格中动态添加宿舍的行。
在 步骤 2 中,我们注册了我们的动态片段并添加了自定义选项以更改动态片段的行为。我们添加的第一个选项是 表格样式。它将用于更改表格的样式。我们添加的第二个选项是 房间数量。我们使用了 <we-select> 和 <we-button-group> 标签来表示我们的选项。这些标签将为片段选项提供不同的 GUI。<we-select> 标签将以下拉菜单的形式显示选项,而 <we-button-group> 标签将以按钮组的形式显示选项。还有其他几个 GUI 选项,例如 <we-checkbox> 和 <we-colorpicker>。您可以在 /addons/website/views/snippets/snippets.xml 文件中探索更多 GUI 选项。
如果您仔细查看选项,您会看到我们为选项按钮添加了 data-select-class 和 data-select-data-attribute 属性。这将让 Odoo 知道当用户选择选项时要更改哪个属性。data-select-class 将在用户选择此选项时设置元素的类属性,而 data-select-data-attribute 将在元素上设置自定义属性和值。请注意,它将使用 data-attribute-name 的值来设置属性。
现在,我们已经添加了代码片段选项。如果您此时拖放代码片段,您将只会看到表头和代码片段选项。更改代码片段选项将更改表格样式,但还没有宿舍数据。为此,我们需要编写一些 JavaScript 代码来获取数据并在表格中显示。在步骤 3中,我们添加了将宿舍数据渲染到表格中的 JavaScript 代码。为了将 JavaScript 对象映射到 HTML 元素,Odoo 使用PublicWidget。现在,PublicWidget可以通过import publicWidget from "@web/legacy/js/public/public_widget";导入。使用PublicWidget的关键属性是selector属性。在selector属性中,您需要使用元素的 CSS 选择器,Odoo 将自动将元素与PublicWidget绑定。您可以通过$el属性访问相关元素。除了_rpc之外,其余的代码是基本的 JavaScript 和 jQuery。
更多内容…
如果您想创建自己的代码片段选项,您可以在代码片段选项中使用t-js选项。之后,您需要在 JavaScript 代码中定义自己的选项。通过探索addons/website/static/src/js/editor/snippets.options.js文件来了解更多关于代码片段选项的信息。
从网站用户获取输入
在 Odoo 中,您可以通过表单、调查或集成到您网站中的交互式元素从网站用户收集输入。Odoo 提供易于创建表单和管理从这些表单收集的数据的功能。以下是您如何设置输入收集的步骤。
-
提交的表单数据通常以与表单关联的特定模型记录的形式存储在数据库中
-
可以通过网站后端或配置视图来显示表单提交来访问收集到的数据
-
可选地,您可以将表单提交链接到 Odoo 中的特定模型,这样您就可以在 Odoo 后台管理和处理数据。
-
定义模型和字段以安全地存储表单数据
准备工作
对于这个菜谱,我们将使用my_hostel模块。我们需要一个新的模型来存储用户提交的宿舍预订查询。
因此,在开始这个菜谱之前,修改之前的代码并创建一个新的用于预订查询的模型,my_hostel/models/inquiries.py:
from odoo import fields, models
class Inquiries(models.Model):
_name = 'hostel.inquiries'
_description = "Inquiries about hostel"
_order = "id desc,"
name = fields.Char(string="Student Name", required=True)
phone = fields.Char(string="Phone", required=True)
email = fields.Char(string="Email")
book_fy = fields.Char(string="Book for Year")
queries = fields.Html(string="Your Question", required=True)
现在,在后台创建菜单、操作和视图来存储来自网站查询表单的提交数据。
为此,在my_hostel/views/inquiries_view.xml中创建一个 XML 文件,然后添加菜单、操作以及其基本的树形和表单视图:
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_hostel_inquiry_tree" model="ir.ui.view">
<field name="name">hostel.inquiry.tree</field>
<field name="model">hostel.inquiries</field>
<field name="arch" type="xml">
<tree string="Inquiries">
<field name="name"/>
<field name="phone"/>
<field name="email"/>
<field name="book_fy"/>
</tree>
</field>
</record>
<record id="view_hostel_inquiry_form" model="ir.ui.view">
<field name="name">hostel.inquiry.form</field>
<field name="model">hostel.inquiries</field>
<field name="arch" type="xml">
<form string="Inquiries">
<sheet>
<div class="oe_title">
<h3>
<table>
<tr>
<td style="padding-right:10px;"><field name="name" required="1"
placeholder="Name" /></td>
</tr>
</table>
</h3>
</div>
<group>
<group>
<field name="phone"/>
<field name="email"/>
<field name="book_fy"/>
</group>
</group>
<group>
<field name="queries"/>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.actions.act_window"
id="action_inquiry">
<field name="name">Inquiries</field>
<field name="type">
ir.actions.act_window</field>
<field name="res_model">
hostel.inquiries</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Create Inquiries.
</p>
</field>
</record>
<menuitem id="hostel_inquiry_main_menu" name="Inquiries"
parent="hostel_main_menu" sequence="2" />
<menuitem id="hostel_inquiry_menu" name="Inquiries"
parent="hostel_inquiry_main_menu"
action="my_hostel.action_inquiry"
groups="my_hostel.group_hostel_manager"
sequence="1"/>
</data>
</odoo>
现在,创建一个基本的表单来获取客户的详细信息,该表单发布在网站页面上。一旦用户提交该表单,所有填写的数据都将存储在Inquiries表中。
为此,在模块中创建一个新文件夹,my_hostel/controllers/main.py:
# -*- coding: utf-8 -*-
from odoo import http, tools, _
from odoo.http import request
class InquiryForm(http.Controller):
@http.route('/inquiry/form', type='http', auth="public", website=True)
def inquiry_form_template(self, **kw):
return request.render("my_hostel.hostel_inquiry_form")
@http.route('/inquiry/submit', type='http', auth="public", website=True)
def inquiry_form(self, **kwargs):
inquiry_obj = request.env['hostel.inquiries']
form_vals = {
'name': kwargs.get('name') or '',
'email': kwargs.get('email') or '',
'phone': kwargs.get('phone') or '',
'book_fy': kwargs.get('book_fy') or '',
'queries': kwargs.get('queries') or '',
}
submit_success = inquiry_obj.sudo().create(form_vals)
return request.redirect('/contactus-thank-you')
现在,为网站设计一个名为my_hostel/views/form_template.xml的表单:
<odoo>
<template id="hostel_inquiry_form"
name="Hostel Inquiry Form">
<t t-call="website.layout">
<section class="s_website_form" data-snippet="s_website_form">
<div class="container">
<div class="row">
<div class="col-md-12 mb64">
<div class="aboutus-section pl-5 pr-5 p-t-100 p-b-50">
<div class="wrapper wrapper--w900">
<div class="card">
<div class="card-body mt8">
<form action="/inquiry/submit" method="POST" class="o_mark_required" id="inquiry_form" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="row">
<div class="form-group col-md-12">
<label for="name"> Your Name </label>
<input type="text"
class="form-control" name="name"
id="name" required="True" />
</div>
</div>
<div class="row">
<div class="form-group col-md-12">
<label for="phone"> Phone </label>
<input type="text" class="form-control"
name="phone" id="phone" required="True" />
</div>
</div>
<div class="row">
<div class="form-group col-md-12">
<label for="email"> Email ID </label>
<input type="text" class="form-control"
name="email" id="email"/>
</div>
</div>
<div class="row">
<div class="form-group col-md-12">
<label for="book_fy"> Booking for the Year </label>
<input type="text" class="form-control"
name="book_fy" id="book_fy"/>
</div>
</div>
<div class="row">
<div class="form-group col-md-12">
<label for="queries"> Your Question </label>
<input type="text" class="form-control"
name="queries" id="queries"/>
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<button type="submit"
class="btn btn-primary btn-lg a-submit">
<span>Submit</span>
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</t>
</template>
</odoo>
如何操作…
更新模块并打开/inquiry/form URL。从该页面,您将能够提交宿舍的查询。提交后,您可以在后端的相关查询表单视图中检查它们。
管理 SEO 选项
Odoo 为模板(页面)内置了 SEO 功能。然而,一些模板被用于多个 URL。例如,在在线商店中,每个产品页面使用相同的模板但不同的产品数据。对于这些情况,我们需要为每个 URL 提供不同的 SEO 选项。
准备工作
对于这个菜谱,我们将使用my_hostel模块。我们将为每个宿舍详情页面存储单独的 SEO 数据。在遵循此菜谱之前,您应该在不同的宿舍页面上测试 SEO 选项。您可以从顶部的推广下拉菜单中获取 SEO 对话框,如图下所示:

图 14.4 – 打开页面的 SEO 配置
如果您在不同的宿舍详情页面上测试 SEO 选项,您将注意到更改一个书籍页面的 SEO 数据将在所有宿舍页面上反映出来。我们将在此菜谱中解决这个问题。
如何操作...
要为模型中的每个记录管理单独的 SEO 选项,您需要在您的模型中继承website.seo.metadata混合。这将向hostel.hostel模型添加一些字段和方法。这些字段和方法将被网站用于为每个书籍存储单独的数据。
-
在
hostel.hostel模型中继承website.seo.metadata混合,如下所示:class Hostel(models.Model): _name = 'hostel.hostel' _description = "Information about hostel" _inherit = ['website.seo.metadata'] _order = "id desc, name" _rec_name = 'hostel_code' -
在宿舍详情路由中将
hostel对象作为main_object传递,如下所示:@http.route('/hostels/<model("hostel.hostel"):hostel>', type='http', auth='public', website = True) def hostel_detail(self, hostel): return request.render( 'my_hostel.hostel_detail', { 'hostel': hostel, 'main_object': hostel } ...
更新模块并更改不同宿舍页面的 SEO。这可以通过优化 SEO选项进行更改。现在,您将能够为每个宿舍管理单独的 SEO 详情。
工作原理...
要在每个模型记录上启用 SEO,您需要继承您的模型中的website.seo.metadata混合。这将向hostel.hostel模型添加一些字段和方法。这些字段和方法将被网站用于为每个书籍存储单独的数据。
小贴士
如果您想查看 SEO 混合的字段和方法,请在/addons/website/models/website.py文件中搜索website.seo.metadata模型。
所有与 SEO 相关的代码都写在website.layout中,并且它从作为main_object传递的记录集中获取所有 SEO 元信息。因此,在步骤 2中,我们传递了一个带有main_object键的hostel对象,以便网站布局能够从宿舍获取所有 SEO 信息。如果您从控制器中没有传递main_object,那么模板记录集将被传递为main_object,这就是为什么您在所有宿舍中都得到了相同的 SEO 数据。
更多...
在 Odoo 中,您可以添加自定义元标签以用于 Open Graph 和 Twitter 分享。如果您想将自定义元标签添加到页面中,您可以在添加 SEO 混合后覆盖_default_website_meta()方法。例如,如果我们想将宿舍封面用作社交分享图片,那么我们可以在我们的hostel模型中使用以下代码:
def _default_website_meta(self):
res = super(Hostel, self)._default_website_meta()
res['default_opengraph']['og:image'] = self.env['website'].image_url(self, 'image')
res['default_twitter']['twitter:image'] = self.env['website'].image_url(self, 'image')
return res
之后,当您分享宿舍的 URL 时,宿舍封面将在社交媒体上显示。此外,您还可以使用相同的方法设置页面标题和描述。
管理网站的网站地图
一个网站的网站地图对任何网站都至关重要。搜索引擎将使用网站地图来索引网站的页面。在这个菜谱中,我们将向网站地图添加宿舍详情页面。
准备工作
对于这个菜谱,我们将使用之前菜谱中的 my_hostel 模块。如果您想检查 Odoo 中的当前网站地图,请在浏览器中打开 <your_odoo_server_url>/sitemap.xml。
如何操作...
按照以下步骤修改宿舍页面以添加到 sitemap.xml:
-
按如下方式在
main.py中导入方法:from odoo.addons.http_routing.models.ir_http import slug from odoo.addons.website.models.ir_http import sitemap_qs2dom -
将
sitemap_hostels方法添加到main.py中,如下所示:def sitemap_hostels(env, rule, qs): Hostels = env['hostel.hostel'] dom = sitemap_qs2dom(qs, '/hostels', Hostels._rec_name) #Ex. to filter urls #dom += [('name', 'ilike', 'abc')] for f in Hostels.search(dom): loc = '/hostels/%s' % slug(f) if not qs or qs.lower() in loc: yield {'loc': loc} -
按如下方式在宿舍详情路由中添加
sitemap_hostels函数引用:@http.route('/hostels/<model("hostel.hostel"):hostel>', type='http', auth='public', website = True, sitemap=sitemap_hostels) def hostel_detail(self, hostel):
更新模块以应用更改。一个 sitemap.xml 文件生成并存储在 附件 中。然后,每隔几小时会重新生成。要查看我们的更改,您需要从附件中删除网站地图文件。为此,请访问浏览器中的 /sitemap.xml URL,您将看到网站地图中的宿舍页面。
工作原理...
在 步骤 1 中,我们导入了一些必需的函数。slug 用于根据记录名称生成一个干净、用户友好的 URL。sitemap_qs2dom 用于根据路由和查询字符串生成一个域名。
在 步骤 2 中,我们创建了一个 Python 生成器函数 sitemap_hostels()。此函数将在生成网站地图时被调用。在调用期间,它将接收三个参数——env Odoo 环境、rule 路由规则和 qs 查询字符串。在函数中,我们使用 sitemap_qs2dom 生成一个域名。然后,我们使用生成的域名搜索宿舍记录,这些记录通过 slug() 方法生成位置。通过 slug,您将获得一个用户友好的 URL,例如 /hostels/cambridge-1。如果您不想在网站地图中列出所有宿舍,您只需在搜索中使用一个有效的域名来过滤宿舍即可。
在 步骤 3 中,我们将 sitemap_hostels() 函数引用传递给带有 sitemap 关键字的路由。
更多内容...
在这个菜谱中,我们看到了如何使用自定义方法为网站地图生成 URL。但是,如果您不想过滤宿舍并且想在网站地图中列出所有宿舍,那么在函数引用而不是传递 True 如下所示:
...
@http.route('/hostels/<model("hostel.hostel"):hostel>', type='http', auth='public', website = True, sitemap=True)
...
同样,如果您不希望任何 URL 显示在网站地图中,只需按如下方式传递 False:
...
@http.route('/hostels/<model("hostel.hostel"):hostel>', type='http', auth='public', website = True, sitemap=False)
...
获取访客的国家信息
Odoo CMS 内置了对 GeoIP 的支持。在实时环境中,您可以根据 IP 地址跟踪访客的国家。在这个菜谱中,我们将根据访客的 IP 地址获取访客的国家。
准备工作
对于这个配方,我们将使用上一个配方中的my_hostel模块。在这个配方中,我们将根据访客的国家在网页上隐藏一些旅舍。你需要下载 GeoIP 数据库来完成这个配方。之后,你需要从cli选项传递数据库位置,如下所示:
./odoo-bin -c config_file --geoip-db=location_of_geoip_DB
或者,按照此文档中的步骤进行:
如果你不想将 GeoIP 数据库定位在/usr/share/GeoIP/,请使用 Odoo 命令行界面的--geoip-city-db和--geoip-country-db选项。这些选项接受 GeoIP 数据库文件的绝对路径,并将其用作 GeoIP 数据库。
如何做到这一点...
按照以下步骤根据国家限制书籍:
-
在
hostel.hostel模型中添加restrict_country_idsMany2many字段,如下所示:class Hostel(models.Model): _name = 'library.book' _inherit = ['website.seo.metadata'] ... restrict_country_ids = fields.Many2many('res.country') ... -
在
hostel.hostel模型的表单视图中添加restrict_country_ids字段,如下所示:… <field name="restrict_country_ids" widget="many2many_tags"/> … -
更新
/hostel控制器以根据国家限制书籍,如下所示:@http.route('/hostels', type='http', auth='public', website = True) def hostel(self): country_id = False country_code = request.geoip and request.geoip.get('country_code') or False if country_code: country_ids = request.env['res.country'].sudo().search([('code', '=', country_code)]) if country_ids: country_id = country_ids[0].id domain = ['|', ('restrict_country_ids', '=', False), ('restrict_country_ids', 'not in', [country_id])] hostels = request.env['hostel.hostel'].sudo().search(domain) return request.render( 'my_hostel.hostels', { 'hostels': hostels,})
警告
这个配方在本地服务器上不起作用。它需要一个托管服务器,因为在使用本地机器时,你会得到本地 IP,这与任何国家无关。你还需要正确配置 NGINX。
它是如何工作的...
在步骤 1中,我们在hostel.hostel模型中添加了一个新的restricted_country_ids many2many类型字段。如果网站访客来自受限制的国家,我们将隐藏书籍。
在步骤 2中,我们在书籍的表单视图中添加了一个restricted_country_ids字段。如果 GeoIP 和 NGINX 配置正确,Odoo 将把 GeoIP 信息添加到request.geoip,然后你可以从那里获取国家代码。
在步骤 3中,我们根据country_code从 GeoIP 获取国家代码,然后获取基于国家代码的记录集。在获取访客的国家信息后,我们根据受限制的国家过滤了带有域名的旅舍。
重要信息
如果你没有真实的服务器,但仍然想测试这个配方,你可以在控制器中添加一个默认的国家代码,如下所示:country_code = request.geoip and request.geoip.get('country_code') or 'IN'。
GeoIP 数据库会定期更新,因此你需要更新你的副本以获取最新的国家信息。
跟踪营销活动
在任何商业或服务中,熟悉投资回报率(ROI)非常重要。ROI 用于评估投资的效率。广告投资可以通过 UTM 代码进行跟踪。UTM 代码是一个可以添加到 URL 的小字符串。这个 UTM 代码将帮助你跟踪活动、来源和媒体。
准备工作
对于这个配方,我们将使用my_library模块。Odoo 内置了对 UTMs 的支持。在我们的宿舍应用程序中,我们没有任何实际案例可以应用 UTMs。然而,在这个配方中,我们将在my_library中的/books/submit_issues生成的问题中添加一个 UTM。
如何操作...
按照以下步骤将来自我们网页生成的书籍问题中的 UTMs 链接到/books/submit_issues URL:
-
在
manifest.py的depends部分添加一个utm模块,如下所示:'depends': ['base', 'website', utm.mixin in the book.issue model, as follows:class LibraryBookIssues(models.Model):
_name = 'book.issue'
_inherit = ['utm.mixin']
book_id = fields.Many2one('library.book', required=True)
submitted_by = fields.Many2one('res.users')
issue_description = fields.Text()
-
在
book_issue_ids字段的树视图中添加一个campaign_id字段,如下所示:... <group string="Book Issues"> <field name="book_issue_ids" nolabel="1"> <tree name="Book issues"> <field name="create_date"/> <field name="submitted_by"/> <field name="issue_description"/> <field name="campaign_id"/> </tree> </field> </group> ...
更新模块以应用更改。要测试 UTM,您需要执行以下步骤:
-
在 Odoo 中,UTM 基于 cookie 进行处理,某些浏览器不支持 localhost 中的 cookie,因此如果您使用 localhost 进行测试,请通过
http://127.0.0.1:8069访问实例。默认情况下,UTM 跟踪对销售人员是禁用的。因此,要测试 UTM 功能,您需要以门户用户身份登录。
-
现在,打开
http://127.0.0.1:8069/books/submit_issues?utm_campaign=saleURL。 -
提交书籍问题并在后端检查书籍问题。这将显示书籍表单视图中的活动。
它是如何工作的...
在第一步中,我们在book.issue模型中继承了utm.mixin。这将向book.issue模型添加以下字段:
-
campaign_id: 与utm.campaign模型关联的Many2one字段。这用于跟踪不同的活动,例如夏季和圣诞节特别活动。 -
source_id: 与utm.source模型关联的Many2one字段。这用于跟踪不同的来源,例如搜索引擎和其他域名。 -
medium_id: 与utm.medium模型关联的Many2one字段。这用于跟踪不同的媒体,例如明信片、电子邮件和横幅广告。
要跟踪活动、媒体和来源,您需要在营销媒体中共享一个像这样的 URL:your_url?utm_campaign=campaign_name&utm_medium=medium_name&utm_source=source_name。
如果访客通过任何营销媒体访问您的网站,则在网站页面上创建记录时,campaign_id、source_id和medium_id字段将自动填写。
在我们的示例中,我们仅跟踪了campaign_id,但您也可以添加source_id和medium_id。
重要提示
在我们的测试示例中,我们使用了campaign_id=sale。现在,sale是utm.campaign模型中的记录名称。默认情况下,utm模块添加了一些活动、媒体和来源的记录。sale记录是其中之一。如果您想创建新的活动、媒体和来源,您可以通过在开发者模式下访问链接跟踪 > UTMs菜单来完成此操作。
管理多个网站
Odoo 内置了对多个网站的支持。这意味着相同的 Odoo 实例可以在多个域名上运行,也可以在显示不同记录时使用。
准备工作
对于这个菜谱,我们将使用之前菜谱中的 my_hostel 模块。在这个菜谱中,我们将根据网站隐藏旅舍。
如何操作...
按照以下步骤使在线网站多网站兼容:
-
按照以下方式在
hostel.hostel模型中添加website.multi.mixin:class Hostel(models.Model): _name = 'hostel.hostel' _description = "Information about hostel" _inherit = ['website.seo.metadata', 'website.multi.mixin'] ... -
按照以下方式在旅舍表单视图中添加
website_id:... <group> <field name="website_id"/> </group> ... -
按照以下方式修改
/hostels控制器中的域名:@http.route('/hostels', type='http', auth='public', website = True) def hostel(self, **post): ... domain = ['|', ('restrict_country_ids', '=', False), ('restrict_country_ids', 'not in', [country_id])] domain += request.website.website_domain() hostels = request.env['hostel.hostel'].sudo().search(domain) return request.render( 'my_hostel.hostels', { 'hostels': hostels, }) ... -
导入
werkzeug并修改旅舍详情控制器以限制来自其他网站的访问,如下所示:import werkzeug ... @http.route('/hostels/<model("hostel.hostel"):hostel>', type='http', auth='public', website = True, sitemap=sitemap_hostels) def hostel_detail(self, hostel, **post): if not hostel.can_access_from_current_website(): raise werkzeug.exceptions.NotFound() return request.render( 'my_hostel.hostel_detail', { 'hostel': hostel, 'main_object': hostel }) ...
更新模块以应用更改。为了测试此模块,在旅舍中设置不同的网站。现在,打开 /hostels URL 并检查书籍列表。之后,更改网站并检查书籍列表。为了测试,你可以从网站切换器下拉菜单更改网站。请参考以下截图进行操作:

图 14.5 – 网站切换器
你也可以尝试直接从 URL 访问书籍详情,例如 /hostels/1。如果旅舍不属于该网站,它将显示为 404。
它是如何工作的...
在 步骤 1 中,我们添加了 website.multi.mixin。这个混入(mixin)为模型中处理多个网站添加了基本工具。这个混入(mixin)在模型中添加了 website_id 字段。该字段用于确定记录是为哪个网站准备的。
在 步骤 2 中,我们在旅舍表单视图中添加了 website_id 字段,以便根据网站过滤旅舍。
在 步骤 3 中,我们修改了用于查找旅舍列表的域名。request.website.website_domain() 将返回过滤掉不属于该网站的旅舍的域名。
重要提示
注意,有一些记录没有设置任何 website_id 字段。这些记录将在所有网站上显示。这意味着,如果你在某个特定旅舍没有 website_id 字段,那么该旅舍将在所有网站上显示。
然后,我们在网络搜索中添加了域名,如下所示:
-
在 步骤 4 中,我们限制了书籍的访问权限。如果书籍不是为当前网站准备的,那么我们将引发一个
Not found错误。如果旅舍记录是为当前活动网站准备的,则can_access_from_current_website()方法将返回True值,如果是为其他网站准备的,则返回False。 -
如果你注意到了,我们在两个控制器中都添加了
**post。这是因为,如果没有它,**post/hostels和/hostels/<model("hostel.hostel"):hostel>将不接受查询参数。在从网站切换器切换网站时,它们也会生成错误,所以我们添加了它。通常,在每一个控制器中添加**post是一个好习惯,这样它们就可以处理查询参数。
重定向旧 URL
当你从一个现有的系统或网站移动到 Odoo 网站时,你必须将你的旧 URL 重定向到新 URL。通过适当的重定向,所有你的 SEO 排名都将移动到新页面。在本菜谱中,我们将了解如何在 Odoo 中将旧 URL 重定向到新 URL。
准备工作
对于本菜谱,我们将使用前一个菜谱中的 my_hostels 模块。对于本菜谱,我们假设你曾经有一个网站,并且刚刚迁移到 Odoo。
如何操作...
想象一下,在你的旧网站上,书籍在 /my-hostels URL 下列出;正如你所知,my_hostel 模块也在 /hostels URL 下列出旅舍。因此,我们现在将向新的 /hostels URL 添加一个 /my-hostels URL。执行以下步骤以添加重定向规则:
-
激活开发者模式。
-
打开 网站 | 配置 | 重定向。
-
点击 新建 添加新规则。
-
在表格中输入值,如图下所示。在
/my-hostels和/hostels中。 -
选择 动作 值为 301 永久移动。
-
保存记录。一旦你填写了数据,你的表格将看起来像这样:

图 14.6 – 重定向规则
一旦你添加了此规则,打开 /my-hostels 页面。你会注意到页面会自动重定向到 /hostels 页面。
它是如何工作的...
页面重定向很简单;它只是 HTTP 协议的一部分。在我们的例子中,我们将 /my-hostels 移动到 /hostels。我们使用了301 永久移动重定向。以下是 Odoo 中所有可用的重定向选项:
-
页面的
404 未找到响应。请注意,Odoo 将为此类请求显示默认的404页面。 -
301 永久移动:此选项永久将旧 URL 重定向到新 URL。此类重定向会将 SEO 排名移动到新页面。
-
302 临时移动:此选项临时将旧 URL 重定向到新 URL。当你需要临时重定向 URL 时使用此选项。此类重定向不会将 SEO 排名移动到新页面。
-
308 重定向/重写:一个有趣的选项——使用此选项,你将能够更改/重写现有的 Odoo URL 到新的 URL。
在此菜谱中,这将允许我们将旧的 /my-hostels URL 重写为新的 /hostels URL。因此,我们不需要使用 /my-hostels 来重定向旧 URL。
重定向规则表单上还有一些其他字段。其中之一是激活字段,如果你想要不时启用/禁用规则,则可以使用该字段。第二个重要字段是网站。当你在使用多网站功能并且只想将重定向规则限制在一个网站时,会使用网站字段。然而,默认情况下,规则将应用于所有网站。
发布与网站相关记录的管理
在业务流程中,有些情况下你需要允许或撤销公共用户对页面的访问权限。其中一种情况是电子商务产品,你需要根据可用性发布或取消发布产品。在这个菜谱中,我们将看到如何为公共用户发布和取消发布宿舍记录。
准备工作
对于这个菜谱,我们将使用前一个菜谱中的 my_hostel 模块。
重要提示
如果你注意到了,我们在 /hostels 和 /hostels/<model``
("hostel.hostel"):hostel> 路由上放置了 auth='user'。请将其更改为 auth='public' 以使这些 URL 可供公共用户访问。
如何操作...
执行以下步骤以启用宿舍详情页面的发布/取消发布选项:
-
将
website.published.mixin添加到hostel.hostel模型中,如下所示:class Hostel(models.Model): _name = 'hostel.hostel' _description = "Information about hostel" _inherit = ['website.seo.metadata', 'website.multi.mixin', ' website.published.mixin'] _order = "id desc, name" ... -
将新文件添加到
my_hostel/security/rules.xml中,并为类似以下内容的宿舍添加一条记录规则(确保你在清单中注册该文件):<?xml version="1.0" encoding="utf-8"?> <odoo noupdate="1"> <record id="hostels_rule_portal_public" model="ir.rule"> <field name="name">Portal/Public user: View published Hostels</field> <field name="model_id" ref="my_hostel.model_hostel_hostel"/> <field name="groups" eval="[(4, ref('base.group_portal')), ( 4, ref('base.group_public'))]"/> <field name="domain_force"> [('website_published','=', True)]</field> <field name="perm_read" eval="True"/> </record> </odoo>- 更新
my_hostel模块以应用更改。现在,你可以发布和取消发布宿舍页面:
- 更新

图 14.7 – 发布/取消发布切换
要发布/取消发布宿舍,你可以使用前一个宿舍详情页面截图所示的切换按钮。
它是如何工作的...
Odoo 提供了一个现成的 mixin 来处理记录的发布管理。它为你做了大部分工作。你所需要做的只是将 website.published.mixin 添加到你的模型中。在 步骤 1 中,我们将 website.published.mixin 添加到我们的 hostel 模型中。这将添加发布和取消发布宿舍所需的所有字段和方法。一旦你将这个 mixin 添加到书籍模型中,你将能够在书籍详情页面上看到切换状态的按钮,如前一个截图所示。
注意
我们从宿舍详情路由发送宿舍记录作为 main_object。如果没有这个,你将无法在宿舍详情页面上看到发布/取消发布的按钮。
添加 mixin 将在宿舍的详情页面上显示发布/取消发布按钮,但它不会限制公共用户访问它。为了做到这一点,我们需要添加一条记录规则。在 步骤 2 中,我们添加了一条记录规则以限制对未发布宿舍的访问。如果你想了解更多关于记录规则的信息,请参阅 第十章,安全访问。
还有更多...
publish mixin 将在网站上启用发布/取消发布按钮。但如果你想在后端表单视图中显示一个重定向按钮,publish mixin 也可以提供这样的方法。以下步骤显示了如何将重定向按钮添加到宿舍的表单视图中:
-
在
hostel.hostel模型中添加一个方法来计算宿舍的 URL:@api.depends('name') def _compute_website_url(self): for hostel in self: hostel.website_url = '/hostels/%s' % (slug(hostel)) -
在表单视图中添加一个按钮以跳转到网站:
... <sheet> <div class="oe_button_box" name="button_box"> <field name="is_published" widget="website_redirect_button"/> </div> ...
一旦添加了按钮,你将能够在宿舍的表单视图中看到它,点击它将重定向到宿舍的详情页面。
第十五章:Web 客户端开发
Odoo 的 Web 客户端,或后端,是员工大部分时间所在的地方。
在第九章**,后端视图中,您看到了如何使用后端提供的现有功能。在这里,我们将探讨如何扩展和定制这些功能。
web模块包含与 Odoo 用户界面相关的所有内容。
本章中的所有代码都将依赖于web模块。如您所知,Odoo 有两个不同的版本(企业版和社区版)。
社区版本使用web模块进行用户界面,而企业版本使用社区web模块的扩展版本,即web_enterprise模块。
企业版本提供的功能比社区版本更多,包括移动兼容性、可搜索菜单和材料设计。在这里,我们将使用社区版本。不用担心——在社区版中开发的模块在企业版中也能完美运行,因为内部web_enterprise依赖于社区web模块,并为其添加了一些功能。
重要信息
与其他 Odoo 版本相比,Odoo 17 的后端 Web 客户端略有不同。它包含两个不同的框架来维护 Odoo 后端的 GUI。第一个是基于小部件的遗留框架,第二个是称为Odoo Web Library(OWL)的现代基于组件的框架。OWL 是 Odoo v16 中引入的新 UI 框架。两者都使用 QWeb 模板进行结构,但在语法和这些框架的工作方式上都有显著的变化。
尽管 Odoo 17 有一个新的框架 OWL,但 Odoo 并没有在所有地方使用这个新框架。大部分 Web 客户端仍然使用旧的基于小部件的框架。在本章中,我们将看到如何使用基于小部件的框架来定制 Web 客户端。在下一章中,我们将探讨 OWL 框架。
在本章中,您将学习如何创建新的字段小部件以获取用户的输入。我们还将从头创建一个新的视图。阅读本章后,您将能够在 Odoo 后端创建自己的 UI 元素。
注意
Odoo 的用户界面高度依赖于 JavaScript。在本章中,我们将假设您具备 JavaScript、jQuery 和 SCSS 的基本知识。
在本章中,我们将介绍以下菜谱:
-
创建自定义小部件
-
使用客户端 QWeb 模板
-
向服务器发起 RPC 调用
-
创建新视图
-
调试客户端代码
-
通过导游改进入职体验
-
移动应用 JavaScript
技术要求
本章的技术要求是在线 Odoo 平台。
本章中使用的所有代码都可以从 GitHub 仓库github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter15下载。
创建自定义小部件
正如你在第九章**,后端视图中看到的,我们可以使用小部件以不同的格式显示某些数据。例如,我们使用了widget='image'将二进制字段显示为图像。为了演示如何创建自己的小部件,我们将编写一个小部件,允许用户选择一个整数字段,但我们将以不同的方式显示它。而不是输入框,我们将显示一个颜色选择器,以便我们可以选择一个颜色数字。在这里,每个数字都将映射到其相关的颜色。
准备工作
对于这个配方,我们将使用具有基本字段和视图的my_hostel模块。你将在 GitHub 仓库的Chapter15/00_initial_module目录中找到基本的my_hostel模块。
如何操作…
我们将添加一个包含我们小部件逻辑的 JavaScript 文件,一个包含设计逻辑的 XML 文件,以及一个用于一些样式的 SCSS 文件。然后,我们将向图书表单添加一个整数字段以使用我们新的小部件。
按照以下步骤添加新的字段小部件:
-
这个小部件可以用很少的 JavaScript 编写。让我们创建一个名为
static/src/js/field_widget.js的文件,如下所示:/** @odoo-module */ import { Component} from "@odoo/owl"; import { registry } from "@web/core/registry"; -
通过扩展
Component创建你的小部件:export class CategColorField extends Component { -
捕获 JavaScript 颜色小部件代码:
export class CategColorField extends Component { setup() { this.totalColors = [1,2,3,4,5,6]; super.setup(); } clickPill(value) { this.props.record.update({ [this.props.name]: value }); } } -
设置小部件的
template和支持的字段类型:CategColorField.template = "CategColorField"; CategColorField.supportedTypes = ["integer"]; -
在同一文件中,将组件注册到
fields注册表:registry.category("fields").add("category_color", { component: CategColorField, }); -
在
static/src/xml/field_widget.xml中添加 QWeb 模板设计代码:<templates xml:space="preserve"> <t t-name="CategColorField" owl="1"> <div> <t t-foreach="totalColors" t-as="color" t-key="color"> <span t-attf-class="o_color_pill o_color_#{color} {{props.record.data[props.name] == color ? 'active': ''}}" t-att-data-value="color" t-on-click="() => this.clickPill(color)"/> </t> <div class="categInformationPanel "/> </div> </t> </templates> -
在
static/src/scss/field_widget.scss中添加 SCSS:.o_field_category_color { .o_color_pill { display: inline-block; height: 25px; width: 25px; margin: 4px; border-radius: 15px; position: relative; @for $size from 1 through length($o-colors) { &.o_color_#{$size - 1} { background-color: nth($o-colors, $size); &:not(.readonly):hover { transform: scale(1.2); transition: 0.3s; cursor: pointer; } &.active:after{ content: "\f00c"; display: inline-block; font: normal 14px/1 FontAwesome; font-size: inherit; color: #fff; position: absolute; padding: 4px; font-size: 16px; } } } } } -
在清单文件中注册文件:
'assets': { 'web.assets_backend': [ 'my_hostel/static/src/scss/field_widget.scss', 'my_hostel/static/src/js/field_widget.js', 'my_hostel/static/src/xml/field_widget.xml', ], } -
然后,将
Category整数字段添加到hostel.room模型:category = fields.Integer('Category') -
将类别字段添加到宿舍表单视图,然后添加
widget="category_color":<field name="category" widget="category_color"/>
更新模块以应用更改。更新后,打开宿舍表单视图,你将看到类别颜色选择器,如下面的截图所示:

图 15.1 – 自定义小部件的显示方式
它是如何工作的…
在步骤 1中,我们导入了Component和注册表。
在步骤 2中,我们通过扩展Component创建了一个CategColorField。通过这种方式,CategColorField将获得Component的所有属性和方法。
在步骤 3中,我们继承了setup方法并设置了this.totalColors属性的值。我们将使用这个变量来决定颜色药丸的数量。我们希望显示六个颜色药丸,因此我们分配了[1,2,3,4,5,6]。
在步骤 4中,我们添加了clickPill处理方法来管理药丸点击。为了设置字段值,我们使用了this.props.update方法。此方法是从Component类添加的。
在步骤 5中,我们添加了一个模板名称,其中我们渲染了CategColorField设计并设置了支持类型。
supportedTypes已被用于决定哪些字段类型由这个小部件支持。在我们的例子中,我们想要为整数字段创建一个小部件。
在步骤 6中,在将组件注册到字段注册表之后。
最后,我们导出了我们的widget类,以便其他插件可以扩展它或从它继承。然后,我们在hostel.room模型中添加了一个名为 category 的新整数字段。我们还添加了具有widget="category_color"属性的相同字段到表单视图中。这将显示我们的小部件而不是默认的整数字段。
使用客户端 QWeb 模板
就像在 JavaScript 中程序性地创建 HTML 代码是一个坏习惯一样,你应在客户端 JavaScript 代码中仅创建最小 DOM 元素。幸运的是,客户端有一个模板引擎可用。
Odoo 中也有一个客户端模板引擎。这个模板引擎被称为Qweb 模板,完全在 JavaScript 代码中执行并在浏览器中渲染。
准备工作
对于这个配方,我们将使用之前配方中的my_hostel模块,并在类别颜色图标下方添加informationPanel。
使用renderToElement,我们渲染类别信息元素并将其设置在informationPanel上。
如何操作...
我们需要将 QWeb 定义添加到清单中并更改 JavaScript 代码,以便我们可以使用它。按照以下步骤开始操作:
-
导入
@web/core/utils/render并将renderToElement引用提取到变量中,如下面的代码所示:import { renderToElement } from "@web/core/utils/render"; -
将模板文件添加到
static/src/xml/field_widget.xml:<t t-name="CategColorField"> <div> <t t-foreach="totalColors" t-as="color" t-key="color"> <span t-attf-class="o_color_pill o_color_#{color} {{props.record.data[props.name] == color ? 'active': ''}}" t-att-data-value="color" t-on-click="() => this.clickPill(color)" t-on-mouseover.prevent="categInfo"/> </t> <div class="categInformationPanel"/> </div> </t> <t t-name="CategInformation"> <div t-attf-class="categ_info o_color_pill o_color_#{value}"> <t t-if="value == 1"> Single Room With AC<br/> <ul> <li> Small Dressing Table </li> <li> Small Bedside Table </li> <li> Small Writing Table </li> <li> Attached Bathroom </li> </ul> </t> <t t-if="value == 2"> Single Room With None AC<br/> <ul> <li> Small Dressing Table </li> <li> Small Bedside Table </li> <li> Small Writing Table </li> <li> Attached Bathroom </li> </ul> </t> <t t-if="value == 3"> King Double Room With AC<br/> <ul> <li> King Size Double Bed </li> <li> Small Dressing Table </li> <li> Small Bedside Table </li> <li> Small Writing Table </li> <li> TV </li> <li> Small Fridge </li> <li> Attached Bathroom </li> </ul> </t> <t t-if="value == 4"> King Double Room With None AC<br/> <ul> <li> King Size Double Bed </li> <li> Small Dressing Table </li> <li> Small Bedside Table </li> <li> Small Writing Table </li> <li> TV </li> <li> Small Fridge </li> <li> Attached Bathroom </li> </ul> </t> <t t-if="value == 5"> Queen Double Room With AC<br/> <ul> <li> Queen Size Double Bed </li> <li> Small Dressing Table </li> <li> Small Bedside Table </li> <li> Small Writing Table </li> <li> TV </li> <li> Small Fridge </li> <li> Attached Bathroom </li> </ul> </t> <t t-if="value == 6"> Queen Double Room With None AC<br/> <ul> <li> Queen Size Double Bed </li> <li> Small Dressing Table </li> <li> Small Bedside Table </li> <li> Small Writing Table </li> <li> TV </li> <li> Small Fridge </li> <li> Attached Bathroom </li> </ul> </t> </div> </t> -
添加鼠标悬停函数,在这个函数中,简单地使用
renderToElement.render渲染类别信息元素,并将categ信息元素附加到categInformationPanel:categInfo(ev){ var $target = $(ev.target); var data = $target.data(); $target.parent().find(".categInformationPanel").html( $(renderToElement('CategInformation',{ 'value': data.value, 'widget': this })) ) } -
在
static/src/scss/field_widget.scss中添加 SCSS 以设置类别信息样式:.categInformationPanel .categ_info{ padding: 10px; height: 100%; width: 100%; color: white; font-weight: bold; } -
在你的清单中注册 QWeb 文件:
'assets': { 'web.assets_backend': [ 'my_hostel/static/src/scss/field_widget.scss', 'my_hostel/static/src/js/field_widget.js', 'my_hostel/static/src/xml/field_widget.xml', ], }
重新启动服务器以应用更改。重启后,打开酒店表单视图,你将看到类别信息面板,如下面的截图所示:

图 15.2 – 类别信息面板
当我们悬停在类别颜色图标上时:

图 15.3 – 悬停在类别图标上显示类别信息
它是如何工作的...
由于在第十四章的“创建或修改模板 – QWeb”配方中已经对 QWeb 的基础进行了全面讨论,CMS 网站开发,我们将关注这里的不同之处。首先,你需要意识到我们正在处理的是客户端的 JavaScript QWeb 实现,而不是服务器端的 Python 实现。这意味着你无法访问浏览记录或环境;你只能访问从renderToElement函数传递的参数。
在我们的案例中,我们通过 widget 键传递了当前对象。这意味着你应该在组件的 JavaScript 代码中拥有所有智能,并且你的模板只能访问属性,或者可能是函数。鉴于我们可以访问组件上的所有可用属性,我们可以在模板中通过检查悬停类别颜色属性来简单地检查值。
由于客户端 QWeb 与 QWeb 视图无关,因此有一个不同的机制使这些模板为网络客户端所知——通过将它们添加到你的附加组件的清单中,作为相对于附加组件根目录的文件名列表。
还有更多…
在这里使用 QWeb 的原因是为了扩展性。例如,如果我们想从另一个模块向我们的组件添加信息图标,我们将使用以下代码在每个药丸中添加一个图标:
<t t-name="CategInformationCustom" t-inherit="my_hostel.CategInformation"
t-inherit-mode="extension">
<xpath expr='//t[@t-if="value == 1"]' position="before">
<i class="fa fa-info-circle" aria-hidden="true"></i>
</xpath>
</t>

图 15.4 – 类别信息面板上的信息图标
注意
如果你想了解更多关于 QWeb 模板的信息,请参考以下要点:
-
客户端 QWeb 引擎的错误消息和处理方式比 Odoo 的其他部分不太方便。一个小错误通常意味着什么都不会发生,对于初学者来说很难继续下去。
-
幸运的是,对于客户端 QWeb 模板有一些调试语句,将在本章后面的 调试客户端代码 菜谱中描述。
向服务器发起 RPC 调用
迟早你的组件将需要从服务器查找一些数据。在这个菜谱中,我们将向类别信息面板添加一个已预订面板。当用户将光标悬停在类别颜色药丸元素上时,预订面板将显示与该类别颜色相关的已预订房间数量。我们将向服务器发起 RPC 调用来获取与该特定类别关联的数据的预订计数。
准备工作
对于这个菜谱,我们将使用之前菜谱中的 my_hostel 模块。
如何操作…
按照以下步骤向服务器发起 RPC 调用并在 colorPreviewPanel 中显示结果:
-
导入
@odoo/owl并将onWillStart,onWillUpdateProps引用提取到一个变量中,如下面的代码所示:import { Component, onWillStart , onWillUpdateProps} from "@odoo/owl"; -
将
onWillStart方法添加到setup方法中并调用我们的自定义loadColorData方法:onWillStart(() => { this.loadCategInformation(); }); onWillUpdateProps(() => { this.loadCategInformation(); }); -
添加
loadCategInformation方法并在 RPC 调用中设置categInfoData:async loadCategInformation() { var self = this; self.categoryInfo = {}; var resModel = self.env.model.root.resModel; var domain = []; var fields = ['category']; var groupby = ['category']; const categInfoPromise = await self.env.services.orm.readGroup( resModel, domain, fields, groupby ); categInfoPromise.map((info) => { self.categoryInfo[info.category] = info.category_count; }); } -
更新
CategoryInformation模板并添加计数数据:<t t-name="CategInformationCustom" t-inherit="my_hostel.CategInformation" t-inherit-mode="extension"> <xpath expr='//t[@t-if="value == 1"]' position="before"> <i class="fa fa-info-circle" aria-hidden="true"></i> </xpath> <xpath expr="//div" position="inside"> <div class="text-center" style="color:gray;background: white;padding:3px;padding: 5px;border-radius: 5px;"> Total Booked Rooms: <t t-esc="widget.categoryInfo[value] or 0"/> </div> </xpath> </t>
更新模块以应用更改。更新后,你将看到类别信息的计数,如下面的截图所示:

图 15.5 – 使用 RPC 获取数据
它是如何工作的…
onWillStart 钩子将在组件第一次渲染之前被调用。如果我们需要在组件渲染到视图之前执行一些操作,例如加载一些初始数据,这将非常有用。
onWillUpdateProps 也是一个异步钩子,每当对相关组件进行更新时都会被调用。使用这个惊人的钩子可以保持 OWL 框架的响应式特性。
在处理数据访问时,我们依赖于由 ORM 提供的 _rpc 函数,用于 search、read、write 或在这种情况下,read_group。
在 步骤 1 中,我们进行了 RPC 调用,并在当前模型上调用了 read_group 方法,在我们的例子中是 hostel.room。我们根据 category 字段对数据进行分组,以便 RPC 调用将返回按 category 分组的书籍数据,并在 category_count 键中添加一个聚合。我们还映射了 category_count 和 category 索引在 categoryInfo 中,以便我们可以在 QWeb 模板中使用它。
步骤 2 没有什么特别之处。我们只是初始化了 bootstrap 工具提示。
在 步骤 3 中,我们使用了 categoryInfo 来设置显示分类信息所需的属性。在 loadCategInformation 方法中,我们通过 this.categoryInfo 分配了一个颜色映射,这样您就可以通过 widget.categoryInfo 在 QWeb 模板中访问它们。这是因为我们传递了小部件引用;这是 renderToElement 方法。
参见
Odoo 的 RPC 返回 JavaScript 的原生 Promise 对象。一旦 Promise 解决,您将获得请求的数据。您可以在以下位置了解更多关于 Promise 的信息:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
创建新视图
正如您在 第九章 中所看到的,后端视图,有不同类型的视图,例如表单、列表和看板。在本菜谱中,我们将创建一个新视图。此视图将显示房间列表,以及他们的学生。
准备工作
对于这个菜谱,我们将使用前一个菜谱中的 my_hostel 模块。请注意,视图是非常复杂的结构,每个视图都有不同的目的和实现。本菜谱的目的是让您了解 MVC 模式视图以及如何创建简单的视图。在本菜谱中,我们将创建一个名为 m2m_group 的视图,其目的是显示分组记录。为了将记录分成不同的组,视图将使用 many2x 字段数据。在 my_hostel 模块中,我们有 room_id 字段。在这里,我们将根据房间分组学生,并在卡片中显示他们。
此外,我们将在控制面板中添加一个新按钮。借助此按钮,您将能够添加新的学生记录。我们还将向房间的卡片中添加一个按钮,以便我们可以将用户重定向到另一个视图。
如何做到这一点…
按照以下步骤添加一个名为 m2m_group 的新视图:
-
在
ir.ui.view中添加一个新的视图类型:class View(models.Model): _inherit = 'ir.ui.view' type = fields.Selection(selection_add=[('m2m_group', 'M2m Group')]) -
在
ir.actions.act_window.view中添加一个新的视图模式:class ActWindowView(models.Model): _inherit = 'ir.actions.act_window.view' view_mode = fields.Selection(selection_add=[ ('m2m_group', 'M2m group')], ondelete={'m2m_group': 'cascade'}) -
通过从基模型继承来添加一个新方法。此方法将从 JavaScript 模型中调用(有关更多详细信息,请参阅 步骤 4):
class Base(models.AbstractModel): _inherit = 'base' @api.model def get_m2m_group_data(self, domain, m2m_field): records = self.search(domain) result_dict = {} for record in records: for m2m_record in record[m2m_field]: if m2m_record.id not in result_dict: result_dict[m2m_record.id] = { 'name': m2m_record.name, 'children': [], 'model': m2m_record._name } result_dict[m2m_record.id]['children'].append({ 'name': record.display_name, 'id': record.id, }) return result_dict -
添加一个名为
/static/src/js/m2m_group_model.js的新文件,并将以下内容添加到其中:/** @odoo-module **/ import { Model } from "@web/model/model"; export class M2mGroupModel extends Model { setup(params) { const metaData = Object.assign({}, params.metaData, {}); this.data = params.data || {}; this.metaData = this._buildMetaData(metaData); this.m2m_field = this.metaData.m2m_field; } _buildMetaData(params) { const metaData = Object.assign({}, this.metaData, params); return metaData; } async load(searchParams) { var self = this; const model = self.metaData.resModel; const method = 'get_m2m_group_data' const m2m_field = self.m2m_field const result = await this.orm.call( model, method, [searchParams.domain, m2m_field] ) self.data = result; return result; } } -
添加一个名为
/static/src/js/m2m_group_controller.js的新文件,并将以下内容添加到其中:/** @odoo-module **/ import { useService } from "@web/core/utils/hooks"; import { Layout } from "@web/search/layout"; import { useModelWithSampleData } from "@web/model/model"; import { standardViewProps } from "@web/views/standard_view_props"; import { Component } from "@odoo/owl"; export class M2mGroupController extends Component { setup() { this.actionService = useService("action"); this.model = useModelWithSampleData(this.props.Model, this.props.modelParams); } _onBtnClicked(domain) { this.actionService.doAction({ type: 'ir.actions.act_window', name: this.model.metaData.title, res_model: this.props.resModel, views: [[false, 'list'], [false, 'form']], domain: domain, }); } _onAddButtonClick(ev) { this.actionService.doAction({ type: 'ir.actions.act_window', name: this.model.metaData.title, res_model: this.props.resModel, views: [[false, 'form']], target: 'new' }); } } M2mGroupController.template = "M2mGroupView"; M2mGroupController.components = { Layout }; M2mGroupController.props = { ...standardViewProps, Model: Function, modelParams: Object, Renderer: Function, buttonTemplate: String, }; -
添加一个名为
/static/src/js/m2m_group_renderer.js的新文件,并将以下内容添加到其中:/** @odoo-module **/ import { Component } from "@odoo/owl"; export class M2mGroupRenderer extends Component { onClickViewButton(group) { var children_ids = group.children.map((group_id) => { return group_id.id; }); const domain = [['id', 'in', children_ids]] this.props.onClickViewButton(domain); } get groups() { return this.props.model.data } } M2mGroupRenderer.template = "M2mGroupRenderer"; M2mGroupRenderer.props = ["model", "onClickViewButton"]; -
添加一个名为
/static/src/js/m2m_group_arch_parser.js的新文件,并将以下内容添加到其中:/** @odoo-module **/ import { visitXML } from "@web/core/utils/xml"; export class M2mGroupArchParser { parse(arch, fields = {}) { const archInfo = { fields, fieldAttrs: {} }; visitXML(arch, (node) => { switch (node.tagName) { case "m2m_group": { const m2m_field = node.getAttribute("m2m_field"); if (m2m_field) { archInfo.m2m_field = m2m_field; } const title = node.getAttribute("string"); if (title) { archInfo.title = title; } break; } case "field": { const fieldName = node.getAttribute("name"); // exists (rng validation) if (fieldName === "id") { break; } const string = node.getAttribute("string"); if (string) { if (!archInfo.fieldAttrs[fieldName]) { archInfo.fieldAttrs[fieldName] = {}; } archInfo.fieldAttrs[fieldName].string = string; } const modifiers = JSON.parse(node.getAttribute("modifiers") || "{}"); if (modifiers.invisible === true) { if (!archInfo.fieldAttrs[fieldName]) { archInfo.fieldAttrs[fieldName] = {}; } archInfo.fieldAttrs[fieldName].isInvisible = true; break; } break; } } }); return archInfo; } } -
添加一个名为
/static/src/js/m2m_group_view.js的新文件,并将以下内容添加到其中:/** @odoo-module **/ import { _lt } from "@web/core/l10n/translation"; import { registry } from "@web/core/registry"; import { M2mGroupArchParser } from "./m2m_group_arch_parser"; import { M2mGroupController } from "./m2m_group_controller"; import { M2mGroupModel } from "./m2m_group_model"; import { M2mGroupRenderer } from "./m2m_group_renderer"; const viewRegistry = registry.category("views"); export const M2mGroupView = { type: "m2m_group", display_name: _lt("Author"), icon: "fa fa-id-card-o", multiRecord: true, Controller: M2mGroupController, Renderer: M2mGroupRenderer, Model: M2mGroupModel, ArchParser: M2mGroupArchParser, searchMenuTypes: ["filter", "favorite"], buttonTemplate: "ViewM2mGroup.buttons", props: (genericProps, view) => { const modelParams = {}; const { arch, fields, resModel } = genericProps; // parse arch const archInfo = new view.ArchParser().parse(arch); modelParams.metaData = { m2m_field: archInfo.m2m_field, fields: fields, fieldAttrs: archInfo.fieldAttrs, resModel: resModel, title: archInfo.title || _lt("Untitled"), widgets: archInfo.widgets, }; return { ...genericProps, Model: view.Model, modelParams, Renderer: view.Renderer, buttonTemplate: view.buttonTemplate, }; }, }; viewRegistry.add("m2m_group", M2mGroupView); -
将视图的 QWeb 模板添加到
/static/src/xml/m2m_group_controller.xml文件中:<?xml version="1.0" encoding="UTF-8"?> <templates xml:space="preserve"> <t t-name="M2mGroupView" owl="1"> <div t-att-class="props.className" t-ref="root"> <Layout display="props.display"> <t t-set-slot="layout-buttons"> <t t-call="{{ props.buttonTemplate }}"/> </t> <div> <t t-component="props.Renderer" model="model" onClickViewButton="group => this._onBtnClicked(group)"/> </div> </Layout> </div> </t> </templates> -
将视图的 QWeb 模板添加到
/static/src/xml/m2m_group_renderer.xml文件中:<?xml version="1.0" encoding="UTF-8"?> <templates xml:space="preserve"> <t t-name="M2mGroupRenderer" owl="1"> <div class="row ml16 mr16"> <div t-foreach="groups" t-as="group" class="col-3" t-key="group"> <t t-set="group_data" t-value="groups[group]" /> <div class="card mt16"> <img class="card-img-top" t-attf-src="img/image" style="height: 300px;"/> <div class="card-body"> <h5 class="card-title mt8"> <t t-esc="group_data['name']"/> </h5> </div> <ul class="list-group list-group-flush"> <t t-foreach="group_data['children']" t-as="child" t-key="child.id"> <li class="list-group-item"> <i class="fa fa-user"/><t t-esc="child.name"/> </li> </t> </ul> <div class="card-body"> <a href="#" class="btn btn-sm btn-primary o_primay_button" t-att-data-group="group" t-on-click="() => this.onClickViewButton(group_data)">View</a> </div> </div> </div> </div> </t> </templates> -
将视图的 QWeb 模板添加到
/static/src/xml/m2m_group_view.xml文件中:<?xml version="1.0" encoding="UTF-8"?> <templates xml:space="preserve"> <t t-name="ViewM2mGroup.buttons" owl="1"> <button type="button" class="btn btn-primary" t-on-click="() => this._onAddButtonClick()"> Add Record </button> </t> </templates> -
将所有 JavaScript 和 XML 文件添加到后端资产中:
'assets': { 'web.assets_backend': [ 'my_hostel/static/src/js/m2m_group_arch_parser.js', 'my_hostel/static/src/js/my_hostel_tour.js', 'my_hostel/static/src/js/m2m_group_view.js', 'my_hostel/static/src/js/m2m_group_renderer.js', 'my_hostel/static/src/js/m2m_group_model.js', 'my_hostel/static/src/js/m2m_group_controller.js', 'my_hostel/static/src/xml/m2m_group_controller.xml', 'my_hostel/static/src/xml/m2m_group_renderer.xml', 'my_hostel/static/src/xml/m2m_group_view.xml', 'my_hostel/static/src/xml/field_widget.xml', ], }, -
最后,添加我们为
hostel.student模型的新视图:<record id="view_hostel_student_m2m_group" model="ir.ui.view"> <field name="name">Students</field> <field name="model">hostel.student</field> <field name="arch" type="xml"> <m2m_group m2m_field="room_id"> </m2m_group> </field> </record> -
将
m2m_group添加到操作中:<field name="view_mode">tree,m2m_group,form</field>
更新my_hostel模块以打开学生视图,然后从视图切换器打开我们刚刚添加的新视图。这看起来如下所示:

图 15.6 – 多对多分组视图

图 15.7 – 多对多分组视图
重要信息
Odoo 视图非常易于使用,并且非常灵活。然而,通常情况下,简单且灵活的事情在底层有复杂的实现。
这适用于 Odoo JavaScript 视图:它们易于使用,但实现起来复杂。它们由许多组件组成,包括模型、渲染器、控制器、视图和 QWeb 模板。在下一节中,我们已添加所有必需的组件,并为my_hostel模型添加了一个新视图。如果您不想手动添加所有内容,可以从本书 GitHub 仓库中的示例文件中获取模块。
它是如何工作的…
在步骤 1和步骤 2中,我们在ir.ui.view和ir.actions.act_window.view中注册了一个新的视图类型,称为m2m_group。
在步骤 3中,我们将get_m2m_group_data方法添加到基础中。将此方法添加到基础中将在每个模型中使该方法可用。此方法将通过 JavaScript 视图的 RPC 调用进行调用。视图将传递两个参数——domain和m2m_field。在domain参数中,域的值将是通过搜索视图域和操作域的组合生成的域。m2m_field是我们想要按其分组记录的字段名称。此字段将在视图定义中设置。
在接下来的几个步骤中,我们添加了创建视图所需的 JavaScript 文件。Odoo JavaScript 视图由视图、模型、渲染器和控制器组成。在 Odoo 代码库中,视图一词具有历史意义,因此模型、视图、控制器(MVC)在 Odoo 中变为模型、渲染器、控制器(MRC)。一般来说,视图设置模型、渲染器和控制器,并设置 MVC 层次结构,使其看起来类似于以下内容:

图 15.8 – 视图组件
其任务是获取一组字段、架构、上下文和一些其他参数,然后构建一个控制器/渲染器/模型三元组:
-
视图的角色是正确设置 MVC 模式中的每一部分,并使用正确的信息。通常,它必须处理架构字符串并提取视图其他部分所需的数据。
注意,视图是一个类,而不是小部件。一旦完成其任务,它就可以被丢弃。
-
渲染器只有一个任务:在 DOM 元素中表示正在查看的数据。每个视图都可以以不同的方式渲染数据。此外,它应该监听适当的用户操作,并在必要时通知其父级(控制器)。渲染器是 MVC 模式中的 V。
-
模型:其任务是获取并保持视图的状态。通常,它以某种方式表示数据库中的一组记录。模型是业务数据的所有者。它是 MVC 模式中的 M。
-
控制器:其任务是协调渲染器和模型。它也是整个 Web 客户端的主要入口点。例如,当用户在搜索视图中更改某些内容时,控制器的
update方法将被调用,并带有适当的信息。它是 MVC 模式中的 C。
注意
视图的 JavaScript 代码被设计成可以在视图管理器/操作管理器之外使用。它可以用于客户端操作,或者可以在公共网站上显示(对资产进行一些工作)。
在步骤 8中,我们将 JavaScript 和 XML 文件添加到资产中。
最后,在最后两个步骤中,我们为hostel.student模型添加了一个视图定义。
在步骤 9中,我们为视图使用了<m2m_group>标签,并且还传递了m2m_field属性作为选项。这将传递给模型以从服务器获取数据。
客户端代码调试
本书包含一个专门用于调试服务器端代码的章节,第七章,调试模块。对于客户端部分,你将在这个菜谱中获得快速入门。
准备工作
这个菜谱不依赖于特定的代码,但如果你想能够精确地重现正在发生的事情,请获取上一个菜谱的代码。
如何操作...
客户端脚本调试困难的原因在于,Web 客户端严重依赖于 jQuery 的异步事件。鉴于断点会停止执行,因此由于时间问题导致的错误在调试时可能不会发生。我们稍后会讨论一些策略:
-
对于客户端调试,您需要通过资产激活调试模式。如果您不知道如何通过资产激活调试模式,请阅读第一章中的激活 Odoo 开发者工具配方,安装 Odoo 开发环境。
-
在您感兴趣的 JavaScript 函数中调用
debugger:debugger; -
如果您遇到时间同步问题,请通过 JavaScript 函数登录控制台:
console.log("Debugging call……."); -
如果您想在模板渲染期间进行调试,请从 QWeb 调用调试器:
<t t-debug="" /> -
您还可以让 QWeb 登录控制台,如下所示:
<t t-log="myvalue" />
所有这些都依赖于您的浏览器提供适当的调试功能。虽然所有主流浏览器都这样做,但在这里我们只关注 Chromium,用于演示目的。要使用调试工具,请通过点击右上角的菜单按钮并选择更多工具 | 开发者工具来打开它们:

图 15.9 – 在 Chrome 中打开开发者工具
它是如何工作的…
当调试器打开时,您应该看到以下截图类似的内容:

图 15.10 – 在 Chrome 中打开开发者工具
在这里,您可以通过单独的标签页访问许多不同的工具。前一个截图中的当前活动标签是 JavaScript 调试器,我们通过点击行号在第 31 行设置了断点。每次我们的小部件获取用户列表时,执行应该停止在此行,调试器将允许您检查变量或更改它们的值。在右侧的监视列表中,您还可以调用函数来尝试它们的效果,而无需不断保存您的脚本文件并重新加载页面。
我们之前描述的调试语句在您打开开发者工具后表现相同。然后执行将停止,浏览器将切换到源标签页,打开有问题的文件,并突出显示带有调试语句的行。
之前提到的两种日志记录方法最终会出现在控制台标签页上。在任何情况下,如果出现问题,您都应该首先检查这个标签页,因为如果由于语法错误或类似的基本问题而导致某些 JavaScript 代码根本无法加载,您将在这里看到一个错误消息,解释正在发生的事情。
还有更多…
使用元素选项卡检查浏览器当前显示的页面的 DOM 表示。这有助于您熟悉现有小部件生成的 HTML 代码,并允许您在一般上玩转类和 CSS 属性。这是一个测试布局更改的绝佳资源。
网络选项卡提供了当前页面发出的请求的概述以及它们花费的时间。当调试缓慢的页面加载时,这很有帮助,因为在网络选项卡中,您通常会找到请求的详细信息。如果您选择一个请求,您可以检查传递给服务器的有效载荷和返回的结果,这有助于您找出客户端意外行为的原因。您还会看到发出的请求的状态代码,例如,如果因为拼写错误找不到资源(例如文件名),则会显示 404。
使用游览改进入门
在开发大型应用程序后,向最终用户解释软件流程至关重要。Odoo 框架包括一个内置的游览管理器。使用这个游览管理器,您可以引导最终用户学习特定的流程。在这个菜谱中,我们将创建一个游览,以便我们可以在图书馆中创建一本书。
准备工作
我们将使用之前菜谱中的my_hostel模块。在没有演示数据的数据库中,游览仅在数据库中显示,所以如果您使用的是带有演示数据的数据库,请为此菜谱创建一个新的没有演示数据的数据库。
如何操作...
要向旅舍添加游览,请遵循以下步骤:
-
添加一个新的
/static/src/js/my_hostel_tour.js文件,包含以下代码:/** @odoo-module **/ import { _t } from "@web/core/l10n/translation"; import { registry } from "@web/core/registry"; import { markup } from "@odoo/owl" import { stepUtils } from "@web_tour/tour_service/tour_utils"; registry.category("web_tour.tours").add('hostel_tour', { url: "/web", rainbowManMessage: _t("Congrats, best of luck catching such big fish! :)"), sequence: 5, steps: () => [stepUtils.showAppsMenuItem(), { trigger: '.o_app[data-menu-xmlid="my_hostel.hostel_main_menu"]', content: markup(_t("Ready to launch your <b>hostel</b>?")), position: 'bottom', }, { trigger: '.o_list_button_add', content: markup(_t("Let's create new room.")), position: "bottom", },{ trigger: '.o_form_button_save', content: markup(_t('Save this room record')), position: "bottom", }] }); -
在后端资源中添加游览 JavaScript 文件:
'assets': { 'web.assets_backend': [ 'my_hostel/static/src/scss/field_widget.scss', 'my_hostel/static/src/js/field_widget.js', 'my_hostel/static/src/js/my_hostel_tour.js', 'my_hostel/static/src/xml/field_widget.xml', ], },
更新模块并打开 Odoo 后端。此时,您将看到游览,如下面的截图所示:
或者,您可以点击调试图标并点击开始游览。

图 15.11 – 用户入门游览步骤
它显示在游览弹出窗口下方。

图 15.12 – 用户入门游览步骤
点击开始图标按钮以查看准备好启动您的旅舍了吗?游览内容:

图 15.13 – 用户入门游览步骤
它是如何工作的...
游览管理器在web_tour.tours类别中可用。
在第一步中,我们导入了registry。然后我们可以使用registry.category("web_tour.tours")添加一个新的游览。我们使用hostel_tour名称注册了我们的游览,并传递了该游览应运行的 URL。
下一个参数是这些游览步骤的列表。游览步骤需要三个值。触发器用于选择游览应显示的元素。这是一个 JavaScript 选择器。我们使用了菜单的 XML ID,因为它在 DOM 中可用。
第一步,stepUtils.showAppsMenuItem(),是主菜单的预定义步骤。下一个键是内容,当用户悬停在导游下拉菜单上时显示。我们使用了 markup(_t()) 函数,因为我们想翻译字符串,而位置键用于决定导游下拉菜单的位置。可能的值是 top(顶部)、right(右侧)、left(左侧)或 bottom(底部)。
重要信息
导游改善了用户的入职体验并管理集成测试。当您以测试模式内部运行 Odoo 时,它也会运行导游,如果导游未完成,则会导致测试用例失败。
移动应用程序 JavaScript
Odoo v10 引入了 Odoo 移动应用程序。它提供了一些小工具以执行移动操作,例如振动手机、显示提示消息和扫描二维码。
准备工作
我们将使用之前菜谱中的 my_hostel 模块。当我们从移动应用程序更改颜色字段的值时,我们将显示提示。
警告
Odoo 移动应用程序仅支持企业版,因此如果您没有企业版,则无法对其进行测试。
如何操作...
按照以下步骤在 Odoo 移动应用程序中显示提示消息:
import mobile from "@web_mobile/js/services/core";
clickPill(value) {
if (mobile.methods.showToast) {
mobile.methods.showToast({ 'message': 'Color changed' });
}
this.props.record.update({ [this.props.name]: value });
}
更新模块并在移动应用程序中打开 hostel.room 模型的表单视图。当您更改颜色时,您将看到提示,如下面的截图所示:

图 15.14 – 颜色更改时的提示
它是如何工作的...
@web_mobile/js/services/core 提供了移动设备和 Odoo JavaScript 之间的桥梁。它公开了一些基本移动工具。在我们的示例中,我们使用了 showToast 方法在移动应用程序中显示提示。我们还需要检查函数的可用性。这样做的原因是某些手机可能不支持一些功能。例如,如果设备没有摄像头,则无法使用 scanBarcode() 方法。在这种情况下,为了避免回溯,我们需要用 if 条件将这些方法包裹起来。
更多...
Odoo 中可以找到的移动工具如下:
-
showToast(): 显示提示消息 -
vibrate(): 使手机振动 -
showSnackBar(): 显示带有按钮的 snack bar -
showNotification(): 显示移动通知 -
addContact(): 向电话簿添加新联系人 -
scanBarcode(): 扫描二维码 -
switchAccount(): 在 Android 中打开账户切换器
要了解更多关于移动 JavaScript 的信息,请参阅 www.odoo.com/documentation/16.0/developer/reference/frontend/mobile.html。
第十六章:Odoo Web Library (OWL)
Odoo V17 JavaScript 框架使用一个名为 OWL(代表 Odoo Web Library)的自定义组件框架。它是一个受 Vue 和 React 灵感启发的声明式组件系统。OWL 是一个基于组件的 UI 框架,并使用 QWeb 模板进行结构。与 Odoo 的传统小部件系统相比,OWL 非常快,并引入了大量的新功能,包括 hooks、reactivity、子组件的 自动实例化以及更多。
在本章中,我们将学习如何使用 OWL 组件生成交互式 UI 元素。我们将从一个最小的 OWL 组件开始,然后我们将了解组件的生命周期。最后,我们将为表单视图创建一个新的字段小部件。在本章中,我们将涵盖以下菜谱:
-
创建 OWL 组件
-
在 OWL 组件中管理用户操作
-
使用钩子制作 OWL 组件
-
理解 OWL 组件的生命周期
-
在表单视图中添加 OWL 字段
注意
你可能会问这样一个问题:为什么 Odoo 不使用一些知名的 JavaScript 框架,比如 React.js 或 Vue.js?请查看以下链接获取更多信息:github.com/odoo/owl/blob/master/doc/miscellaneous/comparison.md。
您可以参考github.com/odoo/owl了解 OWL 框架的更多信息。
技术要求
OWL 组件使用 ES6 类定义。在本章中,我们将使用一些 ES6 语法。此外,一些 ES6 语法在旧浏览器中不受支持,所以请确保您正在使用最新版本的 Chrome 或 Firefox。您可以在github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter16找到本章的代码。
创建 OWL 组件
OWL 的主要构建块是组件和模板。
在 OWL 中,UI 的每个部分都由一个组件管理:它们持有逻辑并定义用于渲染用户界面的模板
本菜谱的目标是学习 OWL 组件的基础知识。我们将创建一个最小的 OWL 组件并将其附加到 Odoo 网络客户端。在这个菜谱中,我们将创建一个用于小型水平栏的组件,其中包含一些文本。
准备工作
对于这个菜谱,我们将使用具有基本字段和视图的 my_hostel 模块。您可以在 GitHub 仓库的github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter16/00_initial_module/my_hostel目录中找到基本的 my_hostel 模块。
如何操作...
我们将向 Odoo 网页客户端添加一个小型水平栏组件。按照以下步骤将您的第一个组件添加到 Odoo 网页客户端:
-
在
my_hostel/static/src/js/component.jsJavaScript 文件中添加一个新的模块命名空间:odoo.define('my_hostel.component', [], function (require) { "use strict"; console.log("Load component......"); }); -
将组件 JavaScript 添加到
assets:'assets': { 'web.assets_backend': [ 'my_hostel/static/src/js/component.js', ], }, -
在 步骤 1 中创建的
component.js文件中定义 OWL 工具:const { Component, mount, xml , whenReady } = owl; -
将 OWL 组件及其基本模板添加到 步骤 1 中创建的
component.js文件:class MyComponent extends Component { static template = xml` <div class="bg-info text-white text-center p-3"> <b> Welcome To Odoo </b> </div>` } -
初始化并将组件添加到网页客户端。将以下内容添加到 步骤 1 中添加的
component.js文件:whenReady().then(() => { mount(MyComponent, document.body); });
安装/升级 my_hostel 模块以应用我们的更改。一旦我们的模块在 Odoo 中加载,您将看到水平栏,如下面的截图所示:

图 16.1 – OWL 组件
这只是一个简单的组件。目前,它不会处理任何用户事件,并且无法将其删除。
它是如何工作的...
在 步骤 1 和 步骤 2 中,我们添加了一个 JavaScript 文件并将其列入后端资产。如果您想了解更多关于资产的信息,请参阅 第十四章 中的 静态资产管理 菜谱,CMS 网站开发。
在 步骤 3 中,我们从 OWL 初始化了一个变量。所有 OWL 工具都在一个全局变量 owl 下可用。在我们的例子中,我们拉取了一个 OWL 工具。我们声明了 Component、mount、xml、whenReady。Component 是 OWL 组件的主要类,通过扩展它,我们将创建自己的组件。
在 步骤 4 中,我们通过扩展 OWL 的 Component 类创建了我们的组件 MyComponent。为了简化,我们只是将 QWeb 模板添加到 MyComponent 类的定义中。这里,如您所注意到的,我们使用了 xml`…` 来声明我们的模板。这种语法被称为内联模板。
然而,您可以通过单独的文件加载 QWeb 模板,这通常是情况。我们将在接下来的菜谱中看到外部 QWeb 模板的示例。
注意
内联 QWeb 模板不支持通过继承进行翻译或修改。因此,始终努力从单独的文件中加载 QWeb 模板。
在 步骤 5 中,我们实例化了 MyComponent 组件并将其附加到主体上。OWL 组件是一个 ES6 类,因此您可以通过 new 关键字创建一个对象。然后您可以使用 mount() 方法将组件添加到页面。如果您注意到,我们将代码放在了 whenReady() 回调中。这将确保在开始使用 OWL 组件之前,所有 OWL 功能都已正确加载。
还有更多...
OWL 是一个独立的库,作为外部 JavaScript 库加载到 Odoo 中。您也可以在其他项目中使用 OWL 库。OWL 库的链接为github.com/odoo/owl。如果您只想在本地机器上测试 OWL 而不设置它,还有一个在线游乐场可供使用。您可以在odoo.github.io/owl/playground/上与 OWL 互动。
在 OWL 组件中管理用户动作
要使 UI 交互式,组件需要处理用户动作,如点击、悬停和表单提交。在这个菜谱中,我们将向我们的组件添加一个按钮,并将处理点击事件。
准备工作
对于这个菜谱,我们将继续使用前一个菜谱中的my_hostel模块。
如何操作...
在这个菜谱中,我们将向组件添加一个删除按钮。点击删除按钮后,组件将被移除。按照以下步骤添加删除按钮及其事件到组件中:
-
更新 QWeb 模板并添加一个图标以删除栏:
class MyComponent extends Component { static template = xml` <div class="bg-info text-white text-center p-3"> <b> Welcome To Odoo </b> <i class="fa fa-close p-1 float-end" style="cursor: pointer;" t-on-click="onRemove"> </i> </div>` } -
要删除组件,请将
onRemove方法添加到MyComponent类中,如下所示:class MyComponent extends Component { static template = xml` <div class="bg-info text-white text-center p-3"> <b> Welcome To Odoo </b> <i class="fa fa-close p-1 float-end" style="cursor: pointer;" t-on-click="onRemove"> </i> </div>` onRemove(ev) { $(ev.target).parent().remove(); } }
更新模块以应用更改。更新后,您将在栏的右侧看到一个小十字图标,如下面的截图所示:

图 16.2 – 顶部栏组件上的删除按钮
点击删除图标后,我们的 OWL 组件将被移除。当您重新加载页面时,栏将重新出现。
工作原理...
在步骤 1中,我们向组件添加了一个删除图标。我们添加了t-on-click属性。这将用于绑定点击事件。属性的值将是组件中的方法。在我们的例子中,我们使用了t-on-click="onRemove"。这意味着当用户点击删除图标时,组件中的onRemove方法将被调用。定义事件的语法很简单:
t-on-<name of event>="<method name in component>"
例如,如果您想在用户将鼠标移至组件上时调用方法,您可以通过添加以下代码来实现:
t-on-mouseover="onMouseover"
添加前面的代码后,每当用户将鼠标光标移至组件上时,OWL 将调用组件中指定的onMouseover方法。
在步骤 2中,我们添加了onRemove方法。当用户点击删除图标时,此方法将被调用。在方法中,我们调用了remove()方法,这将从 DOM 中删除组件。在接下来的菜谱中,我们将看到几个默认方法。
更多...
事件处理不仅限于 DOM 事件。您还可以使用您自定义的事件。例如,如果您手动触发名为my-custom-event的事件,您可以使用t-on-my-custom-event来捕获自定义触发的事件。
使用钩子制作 OWL 组件
OWL 是一个强大的框架,支持基于 钩子 的 UI 自动更新。使用更新钩子,当组件的内部状态发生变化时,组件的 UI 将自动更新。在这个菜谱中,我们将根据用户操作更新组件中的消息。
准备工作
对于这个菜谱,我们将继续使用上一个菜谱中的 my_hostel 模块。
如何做到这一点…
在这个菜谱中,我们将在组件中的文本周围添加箭头。当我们点击箭头时,我们将更改消息。按照以下步骤使 OWL 组件具有响应性:
-
更新组件的 XML 模板。在文本周围添加两个带有事件指令的按钮。同时,从列表中动态检索消息:
static template = xml` <div class="bg-info text-white text-center p-3"> <i class="fa fa-arrow-left p-1" style="cursor: pointer;" t-on-click="onPrevious"> </i> <b t-esc="messageList[Math.abs(state.currentIndex%4)]"/> <i class="fa fa-arrow-right p-1" style="cursor: pointer;" t-on-click="onNext"> </i> <i class="fa fa-close p-1 float-end" style="cursor: pointer;" t-on-click="onRemove"> </i> </div>` -
在组件的 JavaScript 文件中,按照以下方式导入
useState钩子:const { Component, mount, xml , whenReady, useState } = owl; -
将
setup方法添加到组件中,并初始化一些变量如下:setup() { this.messageList = [ 'Hello World', 'Welcome to Odoo', 'Odoo is awesome', 'You are awesome too' ]; this.state = useState({ currentIndex: 0 }); } -
在
Component类中,添加处理用户点击事件的函数:onNext(ev) { this.state.currentIndex++; } onPrevious(ev) { this.state.currentIndex--; }
重新启动并更新模块以将更改应用到模块。更新后,你将看到文本周围的两个箭头图标,如下所示:

图 16.3 – 文本周围的箭头
如果你点击箭头,消息文本将根据构造函数中的消息列表进行更改。
它是如何工作的...
在 步骤 1 中,我们更新了组件的 XML 模板。基本上,我们对模板进行了两项更改。我们渲染了消息列表中的文本消息,并根据状态变量中的 currentIndex 值选择消息。我们在文本块周围添加了两个箭头图标。在箭头图标中,我们添加了 the t-on-click 属性来绑定点击事件到箭头。
在 步骤 2 中,我们从 OWL 导入了 useState 钩子。此钩子用于处理组件的状态。
在 步骤 3 中,我们添加了一个 setup。这将在你创建对象实例时被调用。在 setup 中,我们添加了我们想要显示的消息列表,然后使用 useState 钩子添加了 state 变量。这将使组件具有响应性。当 state 发生变化时,UI 将根据新的状态进行更新。在我们的例子中,我们使用了 useState 钩子中的 currentIndex。这意味着每当 currentIndex 的值发生变化时,UI 也会更新。
重要信息
定义钩子只有一个规则,即钩子只有在你在 setup 中声明它们时才会工作。还有其他几种类型的钩子可供使用,你可以在以下位置找到它们:github.com/odoo/owl/blob/master/doc/reference/hooks.md。
在 步骤 4 中,我们添加了处理箭头点击事件的函数。点击箭头时,我们正在更改组件的状态。由于我们正在使用状态上的钩子,组件的 UI 将自动更新。
理解 OWL 组件的生命周期
OWL 组件有几个方法可以帮助开发者创建强大且交互式的组件。OWL 组件的一些重要方法如下:
-
setup() -
onWillStart() -
onWillRender() -
onRendered() -
onMounted() -
onWillUpdateProps() -
onWillPatch() -
onPatched() -
onMounted() -
onWillUnmount() -
onWillDestroy() -
onError()
在这个菜谱中,我们将记录控制台中的消息,以帮助我们理解 OWL 组件的生命周期。
准备工作
对于这个菜谱,我们将继续使用前一个菜谱中的my_hostel模块。
如何做到这一点…
要将组件的方法添加以显示 OWL 组件的生命周期,您需要执行以下步骤:
-
首先,您需要导入
all钩子,如下所示:const { Component, mount, whenReady, onWillStart, onMounted, onWillUnmount, onWillUpdateProps, onPatched, onWillPatch, onWillRender, onRendered, onError, onWillDestroy, } = owl; -
由于我们已经在组件中有了
setup,让我们向控制台添加如下消息:setup() { console.log('CALLED:> setup'); } -
将
willStart方法添加到组件中:setup() { onWillStart(async () => { console.log('CALLED:> willStart'); }); } -
将
willrender方法添加到组件中:setup() { onWillRender(() => { console.log('CALLED:> willRender'); }); } -
将
render方法添加到组件中:setup() { onRendered(() => { console.log('CALLED:> Rendered'); }); } -
将
mounted方法添加到组件中:setup() { onMounted(() => { console.log('CALLED:> Mounted'); }); } -
将
willUpdateProps方法添加到组件中:setup() { onWillUpdateProps(() => { console.log('CALLED:> WillUpdateProps'); }); } -
将
willPatch方法添加到组件中:setup() { onWillPatch(() => { console.log('CALLED:> WillPatch'); }); } -
将
patched方法添加到组件中:setup() { onPatched(() => { console.log('CALLED:> Patch'); }); } -
将
willUnmount方法添加到组件中:setup() { onWillUnmount(() => { console.log('CALLED:> WillUnmount'); }); } -
将
willDestroy方法添加到组件中:setup() { onWillDestroy(() => { console.log('CALLED:> WillDestroy'); }); } -
将
Error方法添加到组件中:setup() { onError(() => { console.log('CALLED:> Error'); }); }
重新启动并更新模块以应用模块更改。更新后,执行一些操作,例如通过箭头更改消息和删除组件。在浏览器控制台中,您将看到如下日志:

图 16.4 – 浏览器控制台中的日志
您可能会根据对组件执行的操作看到不同的日志。
它是如何工作的…
在这个菜谱中,我们添加了几个方法,并将日志消息添加到方法中。您可以根据需求使用这些方法。让我们看看组件的生命周期以及这些方法何时被调用。
setup
setup在组件构建后立即运行。它是一个与constructor非常相似的生命周期方法,不同之处在于它不接收任何参数。
这是调用钩子函数的正确位置。请注意,在组件生命周期中设置 setup 钩子的主要原因是使其能够猴子补丁。这是 Odoo 生态系统中的一种常见需求。
willStart
willStart是一个异步钩子,可以用来在组件的初始渲染之前执行一些(大多数情况下是异步的)操作。
它将在初始渲染之前恰好被调用一次。在某些情况下很有用,例如,在组件渲染之前加载外部资源(如 JavaScript 库)。另一个用例是从服务器加载数据。
onWillStart钩子用于注册一个将被执行的函数:
setup() {
onWillStart(async () => {
this.data = await this.loadData()
});
}
willRender
虽然不常见,但您可能需要在组件渲染之前执行代码(更精确地说,当其编译的模板函数执行时)。为此,我们可以使用 onWillRender 钩子。
willRender 钩子在渲染模板之前被调用,首先是父组件,然后是子组件。
rendered
同样,虽然不常见,但您可能需要在组件渲染后立即执行代码(更精确地说,当其编译的模板函数执行时)。为此,我们可以使用 onRendered 钩子。
rendered 钩子在模板渲染后立即被调用,首先是父组件,然后是子组件。请注意,在这个时候,实际的 DOM 可能还不存在(如果是第一次渲染),或者尚未更新。这将在下一个动画帧中完成,一旦所有组件都准备就绪。
mounted
mounted 钩子每次组件被附加到 DOM 上,在初始渲染之后被调用。此时,组件被认为是活动的。这是一个添加监听器或与 DOM 交互的好地方,如果组件需要执行某些测量操作的话。
它是 willUnmount 的对立面。如果一个组件已经被挂载,它将在未来的某个时刻被卸载。
mounted 方法将在其每个子组件上递归调用。首先是子组件,然后是父组件。
在挂载钩子中修改状态是被允许的(但不鼓励)。这样做将导致重新渲染,用户可能不会察觉,但会稍微减慢组件的运行速度。
onMounted 钩子用于注册一个将在此时执行的功能。
willUpdateProps
willUpdateProps 是一个在设置新属性之前被调用的异步钩子。如果组件需要根据属性执行异步任务(例如,假设属性是一些记录 ID,获取记录数据),这很有用。
onWillUpdateProps 钩子用于注册一个将在此时执行的功能。
注意,它接收组件的下一个属性。
此钩子在第一次渲染期间不会被调用(但会调用 willStart 并执行类似的工作)。同样,像大多数钩子一样,它按照常规顺序调用:首先是父组件,然后是子组件。
willPatch
willPatch 钩子在 DOM 补丁过程开始之前被调用。在初始渲染时不会调用。这有助于从 DOM 中读取信息,例如滚动条当前的位置。
注意,在这里不允许修改状态。此方法在真正的 DOM 补丁之前被调用,并且仅用于保存一些本地 DOM 状态。此外,如果组件不在 DOM 中,则不会调用。
onWillPatch 钩子用于注册一个将在此时执行的功能。willPatch 按照常规的父/子顺序调用。
patched
当一个组件实际上更新其 DOM 时(最可能通过其状态/属性或环境的变化),会调用此钩子。
此方法在初始渲染时不会被调用。当组件被修补时,它用于与 DOM(例如,通过外部库)进行交互。请注意,如果组件不在 DOM 中,则此钩子不会被调用。
onPatched 钩子用于注册一个将在此时执行的功能。
在此钩子中更新组件状态是可能的,但并不推荐。我们需要小心,因为这里的更新将创建额外的渲染,这反过来又会引起对 patched 方法的其他调用。因此,我们需要特别小心,以防止无限循环。
与 mounted 一样,patched 钩子的调用顺序是:先子组件,然后是父组件。
willUnmount
willUnmount 是在组件从 DOM 中卸载之前被调用的钩子。这是一个移除监听器的好地方,例如。
onWillUnmount 钩子用于注册一个将在此时执行的功能。
这是 mounted 方法的相反方法。请注意,如果一个组件在挂载之前被销毁,则 willUnmount 方法可能不会被调用。
父 willUnmount 钩子将在子组件之前被调用。
willDestroy
有时,组件需要在设置时执行某些操作,并在它们不活跃时清理它们。然而,willUnmount 钩子不适合清理操作,因为组件可能在挂载之前就被销毁了。在这种情况下,willDestroy 钩子非常有用,因为它总是会被调用。
onWillUnmount 钩子用于注册一个将在此时执行的功能。
willDestroy 钩子首先在子组件上被调用,然后是在父组件上。
onError
可惜,组件可能在运行时崩溃。这是一个不幸的现实,这也是为什么 OWL 需要提供一种处理这些错误的方法。
当我们需要拦截并适当地对某些子组件中发生的错误做出反应时,onError 钩子非常有用。
还有更多…
组件生命周期中还有一个方法,但它在使用子组件时使用。OWL 通过 props 参数传递父组件状态,当 props 发生变化时,会调用 willUpdateProps 方法。这是一个异步方法,这意味着你可以在其中执行异步操作,例如 RPC。
将 OWL 字段添加到表单视图中
到目前为止,我们已经学习了 OWL 的所有基础知识。现在我们将继续学习更高级的方面,并创建一个可以在表单视图中使用的字段小部件,就像前一章中的字段小部件配方一样。
Odoo 在 UI 中为不同的功能提供了许多小部件,例如状态栏、复选框和单选按钮,这使得 Odoo 中的操作更加简单,运行更加顺畅。例如,我们使用widget='image'来显示二进制字段作为图像。为了演示如何创建自己的小部件,我们将编写一个小部件,允许用户选择一个整数字段,但我们将以不同的方式显示它。而不是输入框,我们将显示一个颜色选择器,以便我们可以选择一个颜色编号。在这里,每个数字都将映射到一个颜色。
在这个菜谱中,我们将创建一个颜色选择器小部件,它将根据所选颜色保存整数值。
为了使示例更具信息性,我们将使用一些 OWL 的高级概念。
准备工作
对于这个菜谱,我们将使用my_hostel模块。
如何做到这一点...
我们将添加一个包含我们小部件逻辑的 JavaScript 文件,一个包含设计逻辑的 XML 文件,以及一个用于一些样式的 SCSS 文件。然后,我们将向图书表单添加一个整数字段以使用我们新的小部件。
执行以下步骤以添加新的字段小部件:
-
按照以下方式将整数字段类别添加到
hostel.room模型中:category = fields.Integer('Category') -
将相同的字段添加到表单视图,并添加一个
widget属性:<field name="category" widget="category_color"/> -
在
static/src/xml/field_widget.xml中添加字段的 QWeb 模板:<t t-name="OWLColorPill"> <span t-attf-class="o_color_pill o_color_#{props.color} #{props.value == props.color ? 'active': ''}" t-att-data-val="props.color" t-on-click="() => this.pillClicked()" t-attf-title="#{props.category_count or 0 } Room booked in this category" /> </t> <span t-name="OWLFieldColorPills"> <t t-foreach="totalColors" t-as='color' t-key="color"> <ColorPill onClickColorUpdated="data => this.colorUpdated(data)" color='color' value="props.value" category_count="categoryInfo[color]"/> </t> </span> -
在模块的
manifest文件中列出 QWeb 文件:'assets': { 'web.assets_backend': [ 'my_hostel/static/src/js/field_widget.js', ], }, -
现在我们想在
static/src/scss/field_widget.scss中添加一些 SCSS。由于 SCSS 的内容太长,请在此书的 GitHub 仓库中找到 SCSS 文件的内容:github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter16/05_owl_field/my_hostel/static/src/scss。 -
添加以下基本内容的静态
/src/js/field_widget.jsJavaScript 文件:/** @odoo-module */ import { Component, onWillStart , onWillUpdateProps} from "@odoo/owl"; import { registry } from "@web/core/registry"; class ColorPill extends Component { static template = 'OWLColorPill'; pillClicked() { this.props.onClickColorUpdated(this.props.color); } } export class OWLCategColorField extends Component { static supportedFieldTypes = ['integer']; static template = 'OWLFieldColorPills'; static components = { ColorPill }; setup() { this.totalColors = [1,2,3,4,5,6]; onWillStart(async() => { await this.loadCategInformation(); }); onWillUpdateProps(async() => { await this.loadCategInformation(); }); super.setup(); } colorUpdated(value) { this.props.record.update({ [this.props.name]: value }); } async loadCategInformation() { var self = this; self.categoryInfo = {}; var resModel = self.env.model.root.resModel; var domain = []; var fields = ['category']; var groupby = ['category']; const categInfoPromise = await self.env.services.orm.readGroup( resModel, domain, fields, groupby ); categInfoPromise.map((info) => { self.categoryInfo[info.category] = info.category_count; }); } } registry.category("fields").add("category_color",{ component: OWLCategColorField }); -
按照以下方式将 JavaScript 和 SCSS 文件添加到后端资源中:
'assets': { 'web.assets_backend': [ 'my_hostel/static/src/scss/field_widget.scss', 'my_hostel/static/src/js/field_widget.js', 'my_hostel/static/src/xml/field_widget.xml', ], }, -
重新启动并更新模块以应用模块更改。打开房间表单视图。您将能够看到颜色选择器小部件,如下面的截图所示:

图 16.5 – 颜色选择器 OWL 小部件
-
这个字段看起来就像上一章中的颜色小部件,但实际上差异在于底层。这个新字段是用 OWL 组件和子组件构建的,而之前的那个是用小部件构建的。
-
这个子组件的好处是提供了一个全面的框架,用于在 OWL 中构建现代、响应式和交互式 UI。通过将功能模块化成小的、可重用的单元,开发者可以创建更易于维护和扩展的应用程序,同时减少代码重复并提高开发效率。
它是如何工作的...
在步骤 1中,我们将一个整数字段添加到hostel.room模型中。
在步骤 2中,我们将字段添加到房间的表单视图中。
在 第 3 步 中,我们添加了 QWeb 模板文件。如果你注意到,我们在文件中添加了两个模板,一个用于颜色药丸,另一个用于字段本身。我们使用两个模板是因为我们想看到 subcomponent 的概念。如果你仔细观察模板,你会发现我们使用了 <ColorPill> 标签。这将用于实例化子组件。在 <ColorPill> 标签上,我们传递了 active 和 color 属性。这些属性将在子组件的模板中作为 props 接收。还要注意,onClickColorUpdated 属性用于监听从子组件触发的自定义事件。
重要信息
Odoo v17 使用了小部件系统和 OWL 框架。
在 第 4 步 中,我们在清单中列出了我们的 QWeb 模板。这将自动在浏览器中加载我们的模板。
在 第 5 步 中,我们为颜色添加了 SCSS。这将帮助我们拥有一个漂亮的颜色选择器 UI。
在 第 6 步 中,我们为字段组件添加了 JavaScript。
我们导入了 OWL 工具,同时也导入了组件和 fieldRegistry。
fieldRegistry 用于将 OWL 组件列为字段组件。
在 第 7 步 中,我们创建了 ColorPill 组件。组件上的 template 变量是加载自外部 XML 文件的模板名称。ColorPill 组件具有 pillClicked 方法,当用户点击颜色药丸时会被调用。在方法体内,我们触发了 onClickColorUpdated 事件,该事件将被我们使用 colorUpdated 在 OWLCategColorField 组件上设置的父 OWLCategColorField 组件捕获。
在 第 8 步 和 第 9 步 中,我们通过扩展 Component 创建了 OWLCategColorField 组件。我们使用 Component 是因为它将包含创建字段小部件所需的所有实用工具。
如果你注意到,我们在开始时使用了 components 静态变量。当你使用模板中的子组件时,你需要通过 components 静态变量列出组件。我们还在我们的示例中添加了 onWillStart 方法。willStart 方法是一个异步方法,所以我们调用了 RPC(网络调用)来获取有关特定颜色预订房间数量的数据。在最后,我们添加了 colorUpdated 方法,当用户点击药丸时会被调用。因此,我们正在更改字段值。this.props.record.update 方法用于设置字段值(这些值将保存在数据库中)。注意,从子组件触发的数据在 event 参数的 detail 属性下可用。最后,我们在 fieldRegistry 中注册了我们的小部件,这意味着从此以后,我们将通过表单视图中的 widget 属性使用我们的字段。
在 第 10 步 中,我们将 JavaScript 和 SCSS 文件加载到后端资源中。
还有更多...
理解 QWeb
例如,t-,比如 t-if 用于条件,元素和其他属性将被直接渲染。以下 QWeb 模板的不同操作:
-
out将自动对输入进行 HTML 转义,限制显示用户提供的内时 XSS 风险。out接受一个表达式,评估它,并将结果注入到文档中:<p><t t-out="value"/></p> <p>42</p> -
set指令,它接受要创建的变量的名称。set的值可以以两种方式提供:-
包含表达式的
t-value属性,其评估结果将被设置:<t t-set="foo" t-value="2 + 1"/> <t t-out="foo"/>
-
-
if,它评估作为属性值给出的表达式:<div> <t t-if="condition"> <p>ok</p> </t> </div>如果条件为
true,则渲染该元素:<div> <p>ok</p> </div>但如果条件为
false,它将从结果中移除:<div> </div>额外的条件分支指令,
t-elif和t-else,也都可以使用:<div> <p t-if="user.birthday == today()">Happy birthday!</p> <p t-elif="user.login == 'root'">Welcome master!</p> <p t-else="">Welcome!</p> </div>foreach,它接受一个返回要迭代的集合的表达式,以及第二个参数t-as,提供迭代当前项的名称:
<t t-foreach="[1, 2, 3]" t-as="i"> <p><t t-out="i"/></p> </t>这将呈现如下:
<p>1</p> <p>2</p> <p>3</p>t-att(属性)指令,它存在三种不同的形式:
t-att-$name < div t-att-a="42"/>这将呈现如下:
<div a="42"></div> t-attf-$name This will be rendered as follows:<li class="row even">1</li><li class="row odd">2</li><li class="row even">3</li>t-att=mapping
<div t-att="{'a': 1, 'b': 2}"/>This will be rendered as follows:<div a="1" b="2"></div>t-att=pair
<div t-att="['a', 'b']"/>This will be rendered as follows:<div a="b">t-call指令:
<t t-call="other-template"/> <p><t t-value="var"/></p>前面的调用将呈现为
<p/> (无内容)。<t t-set="var" t-value="1"/> <t t-call="other-template"/> The body of the `call` directive can be arbitrarily complex (not just set directives), and its rendered form will be available within the called template as a magical `0` variable:<div>这个模板被调用时包含以下内容:
<t t-out="0"/>这将导致以下结果:
<div> This template was called with content: <em>content</em> </div>
理解子组件
在 OWL 的上下文中,子组件指的是可以集成到更大组件中以增强其功能或提供额外功能的小型、模块化功能单元。
OWL 中的子组件可以包括各种元素,如小部件、实用工具、服务和视图,这些元素被设计成在 OWL 框架内协同工作,以创建丰富、交互式的 UI 并有效地管理客户端逻辑。
这些子组件协同工作,为在 OWL 中构建现代、响应式和交互式 UI 提供了一个全面的框架。通过将功能模块化成小型、可重用的单元,开发者可以创建更易于维护和扩展的应用程序,同时减少代码重复并提高开发效率。
使用其他(子)组件定义组件非常方便。这被称为组合,在实践中非常强大。在 OWL 中,我们只需在其模板中使用以大写字母开头的标签,并在其静态 component 对象中注册子组件类:
class Child extends Component {
static template = xml`<div>child component <t t-esc="props.value"/></div>`;
}
class Parent extends Component {
static template = xml`
<div>
<Child value="1"/>
<Child value="2"/>
</div>`;
static components = { Child };
}
在这里,<Child> 有 subcomponent。这个例子也展示了我们如何将信息从父组件传递给子组件作为 props。在 OWL 中,props(简称属性)是一个包含所有由父组件提供给组件的数据的对象。请注意,props 是一个只有从子组件的角度才有意义的对象。
props 对象由模板上定义的每个属性组成,但有以下例外:以 t- 开头的每个属性都不是属性(它们是 QWeb 指令)。
在以下示例中:
<div>
<Child value="string"/>
<Child t-if="condition" model="model"/>
</div>
props 对象包含以下键:
for Child: value,
for Child: model,
第十七章:Odoo 应用内购买
Odoo 自 11 版本起就内置了对应用内购买(IAP)的支持。IAP 用于提供无需复杂配置的持续服务。通常,从应用商店购买的应用只需客户支付一次费用,因为它们是普通模块,一旦用户购买并开始使用该模块,就不会再向开发者收费。相比之下,IAP 应用用于向用户提供服务,因此提供持续服务会有运营成本。在这种情况下,仅通过单次购买提供服务是不可能的。服务提供商需要一种按使用情况定期向用户收费的方式。Odoo 的 IAP 解决了这些问题,并提供了一种基于使用情况收费的方式。
应用内购买通常指的是在应用程序内购买附加功能、内容或服务的能力。然而,Odoo 具有高度的可定制性,尽管它可能没有专门的 IAP 模块,但您可以通过自定义开发或利用现有模块来创建类似的功能。
此功能允许用户通过获取额外的应用、功能或服务来扩展他们的 Odoo 实例的功能,而无需离开 Odoo 环境。以下是 Odoo IAP 的概述:
-
应用市场集成:Odoo 的 IAP 与 Odoo 应用商店或市场紧密集成。用户可以从广泛的选项中浏览、选择和购买额外的应用或模块。
-
轻松访问扩展:用户可以直接从他们的 Odoo 仪表板访问和评估可用的应用和扩展。这使得企业能够方便地扩展他们的 Odoo 实例的功能,而无需进行广泛的手动安装。
-
试用版本:市场中的某些应用可能提供试用版本或限时试用,允许用户在购买前测试应用的功能。这有助于用户做出明智的决定。
-
简化许可:Odoo IAP 简化了购买应用的许可和订阅管理。用户可以轻松订阅、续订或管理他们的许可,无需外部流程。
-
一键安装:购买应用后,用户通常只需在他们的 Odoo 实例中点击一次即可安装。这一简化流程减少了应用安装的复杂性。
-
集中计费:购买应用的计费和支付通常通过 Odoo 的中央计费系统管理,简化了应用获取的财务方面。
-
应用更新:Odoo IAP 通常包括购买应用的自动更新,确保用户能够访问最新的功能和安全更新。
-
支持和文档:许多通过 Odoo IAP 提供的应用都包含文档和支持选项,使得用户在需要时更容易获得帮助。
-
与核心 Odoo 集成:购买的应用程序与核心 Odoo 系统无缝集成,确保兼容性和统一的用户体验。
有几种使用 IAP 的用例,例如发送文件的传真服务或短信服务。在本章中,我们将解释 Odoo 将提供的合作伙伴自动完成服务。
在本章中,我们将涵盖以下主题:
-
IAP 概念
-
购买信用额
-
IAP 账户
-
IAP 门户
-
获取低信用额通知
IAP 概念
IAP 包含了在使用 Odoo ERP 系统中此功能时必须理解的一些关键概念和元素。我们将探讨 IAP 流程中的一部分实体,并查看每个实体的作用以及它们如何结合完成 IAP 流程。
Odoo IAP 是使用 Odoo ERP 系统的企业的一项宝贵工具,因为它简化了扩展和定制其软件环境的过程。它提供了一个集中平台来管理额外的应用程序和模块,帮助组织优化其业务流程和运营。这一功能为 Odoo 生态系统增加了灵活性和可扩展性,使其成为更强大、适应性更强的解决方案,适用于各种企业和行业。
Odoo IAP 简化了从 Odoo 环境中直接发现、获取和安装额外应用程序和模块的过程。用户可以轻松扩展他们的 Odoo 系统功能,而无需离开平台。
应用程序开发者可以提供具有各种定价模式的产品,包括一次性购买、订阅计划和试用版。这种灵活性满足了不同客户的需求。
Odoo 的 IAP 功能显著增强了 Odoo ERP 系统的适应性和定制能力。它简化了应用获取的过程,鼓励开发者创新,并提供以用户为中心的体验,最终有助于平台的通用性和对各行各业企业价值的提升。
Odoo IAP
IAP 简化了获取和管理 Odoo ERP 系统额外应用、模块和功能的过程。
它是如何工作的…
IAP 流程中有三个主要实体:客户、服务提供商和 Odoo 本身。以下是它们的描述:
-
客户是希望使用该服务的最终用户。为了使用该服务,客户需要安装服务提供商提供的应用程序。然后,客户需要根据他们的使用需求购买服务计划。有了这个,客户就可以立即开始使用该服务。这避免了客户的困难,因为不需要进行复杂的配置。相反,他们只需支付服务费用并开始使用即可。
-
服务提供商是想要销售服务的开发者(可能就是你,因为你是开发者)。客户会向提供商请求服务,此时服务提供商将检查客户是否购买了有效的计划以及客户的账户中是否有足够的信用额度。如果客户有足够的信用额度,服务提供商将扣除信用额度并向客户提供服务。
-
Odoo本身在这一过程中是一种经纪人。它提供了一个处理支付、信用额度、计划等的媒介。客户从 Odoo 购买服务信用额度,服务提供商在提供服务时提取这些信用额度。然后 Odoo 在客户和服务提供商之间架起桥梁,因此客户无需进行复杂的配置,服务提供商也无需设置支付网关、客户账户管理等。
在此过程中还有一个可选实体,即外部服务。在某些情况下,服务提供商会使用一些外部服务。然而,在这里我们将忽略外部服务,因为它们是次要的服务提供商。一个例子可能是短信服务。如果您向 Odoo 用户提供短信 IAP 服务,那么您(服务提供商)将内部使用短信服务。
购买信用额度
每个 IAP 服务都有自己的定价。客户必须从 IAP 服务提供商那里购买该服务。要检查您的服务,请转到设置 | Odoo IAP | 查看我的服务。

图 17.1 – 购买信用额度
前面的截图显示了您想要购买信用额度时看到的界面。
IAP 账户
一旦您从提供商那里购买了信用额度,它就会存储在 IAP 账户中,这些账户将用于每个服务。默认情况下,IAP 账户对所有公司都是通用的,但可以配置为特定于公司。
要创建新的IAP 账户,激活开发者模式并转到技术设置 | IAP 账户。

图 17.2 – IAP 账户
以下为IAP 账户界面的截图:

图 17.3 – IAP 账户
IAP 门户
IAP 门户是一个平台,您可以在其中查看您的 IAP 服务和它们的信用额度,并且可以通过点击购买信用额度按钮来充值,这将带您转到 IAP 门户。可以设置阈值,一旦达到阈值,将通过提到的电子邮件 ID 通知您。

图 17.4 – IAP 账户
获取低信用额度通知
在这里,我们可以设置阈值中的信用额度,这意味着我们必须设置一个最低信用额度限制和电子邮件地址。一旦达到限制,将自动向提到的电子邮件 ID 发送提醒,转到设置 | Odoo IAP | 查看我的服务。接下来,展开服务,检查信用额度,并相应地进行配置。

图 17.5 – 信用额度低通知
这就是 Odoo 中应用内购买的工作方式。在下一章中,我们将看到自动测试用例。
第十八章:自动化测试用例
当涉及到开发大型应用程序时,使用自动化测试用例是提高您模块可靠性的良好实践。这使得您的模块更加健壮。每年,Odoo 都会发布其软件的新版本,自动化测试用例在检测应用程序中的回归(可能由版本升级引起)方面非常有帮助。幸运的是,任何 Odoo 框架都附带不同的自动化测试工具。Odoo 包括以下三种主要的测试类型:
-
一个 Python 测试用例:用于测试 Python 业务逻辑
-
一个 JavaScript QUnit 测试:用于测试 Odoo 中的 JavaScript 实现
-
旅游:一个集成测试,用于检查 Python 和 JavaScript 是否能够正确地协同工作
在本章中,我们将涵盖以下配方:
-
添加 Python 测试用例
-
运行标记的 Python 测试用例
-
为客户端测试用例设置无头 Chrome
-
添加客户端 QUnit 测试用例
-
添加旅游测试用例
-
从 UI 运行客户端测试用例
-
调试客户端测试用例
-
为失败的测试用例生成视频/屏幕截图
-
为测试填充随机数据
技术要求
在本章中,我们将详细查看所有测试用例。为了在一个模块中涵盖所有测试用例,我们创建了一个小型模块。其 Python 定义如下:
import logging
from odoo import api, fields, models
from odoo.exceptions import UserError
from odoo.tools.translate import _
_logger = logging.getLogger(__name__)
class HostelRoom(models.Model):
_name = 'hostel.room'
_description = "Information about hostel Room"
name = fields.Char(string="Hostel Name", required=True)
room_no = fields.Char(string="Room Number", required=True)
other_info = fields.Text("Other Information",
help="Enter more information")
description = fields.Html('Description')
room_rating = fields.Float('Hostel Average Rating', digits=(14, 4))
member_ids = fields.Many2many('hostel.room.member', string='Members')
state = fields.Selection([
('draft', 'Unavailable'),
('available', 'Available'),
('closed', 'Closed')],
'State', default="draft")
@api.model
def is_allowed_transition(self, old_state, new_state):
allowed = [('draft', 'available'),
('available', 'closed'),
('closed', 'draft')]
return (old_state, new_state) in allowed
def change_state(self, new_state):
for room in self:
if room.is_allowed_transition(room.state, new_state):
room.state = new_state
else:
message = _('Moving from %s to %s is not allowed') % (room.state, new_state)
raise UserError(message)
def make_available(self):
self.change_state('available')
return True
def make_closed(self):
self.change_state('closed')
class HostelRoomMember(models.Model):
_name = 'hostel.room.member'
_inherits = {'res.partner': 'partner_id'}
_description = "Hostel Room member"
partner_id = fields.Many2one('res.partner', ondelete='cascade')
date_start = fields.Date('Member Since')
date_end = fields.Date('Termination Date')
member_number = fields.Char()
date_of_birth = fields.Date('Date of birth')
这里给出的 Python 代码将帮助我们编写 Python 业务案例的测试用例。
对于 JavaScript 测试用例,我们从第十五章中“创建自定义小部件”的配方中添加了int_color小部件,Web 客户端开发。
您可以从以下链接获取此初始模块:github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter18/00_initial_module。
添加 Python 测试用例
Python 测试用例用于检查业务逻辑的正确性。在第五章“基本服务器端开发”中,您看到了如何修改现有应用程序的业务逻辑。由于定制可能会破坏应用程序的功能,这使得这一点尤为重要。在本章中,我们将编写一个测试用例来验证更改宿舍房间状态的业务逻辑。
准备工作
我们将使用 GitHub 仓库中Chapter18/00_initial_module目录下的my_hostel模块。
如何操作...
按照以下步骤将 Python 测试用例添加到my_hostel模块中:
-
添加一个新文件,
tests/__init__.py,如下所示:from . import test_hostel_room_state -
添加一个
tests/test_hostel_room_state.py文件,然后添加测试用例,如下所示:from odoo.tests.common import TransactionCase class TestHostelRoomState(TransactionCase): def setUp(self, *args, **kwargs): super(TestHostelRoomState, self).setUp(*args, **kwargs) self.partner_nikul = self.env['res.partner'].create({'name': 'Nikul Chaudhary'}) self.partner_deepak = self.env['res.partner'].create({'name': 'Deepak Ahir'}) self.member_ids = self.env['hostel.room.member'].create([ {'partner_id': self.partner_nikul.id, 'member_number': '007'}, {'partner_id': self.partner_deepak.id, 'member_number': '357'}]) self.test_hostel_room = self.env['hostel.room'].create({ 'name': 'Hostel Room 01', 'room_no': '1', 'member_ids': [(6, 0, self.member_ids.ids)] }) def test_button_available(self): """Make available button""" self.test_hostel_room.make_available() self.assertIn(self.partner_nikul, self.test_hostel_room.mapped('member_ids.partner_id')) self.assertEqual( self.test_hostel_room.state, 'available', 'Hostel Room state should changed to available') def test_button_closed(self): """Make closed button""" self.test_hostel_room.make_available() self.test_hostel_room.make_closed() self.assertEqual( self.test_hostel_room.state, 'closed', 'Hostel Room state should changed to closed') -
要运行测试用例,请使用以下选项启动 Odoo 服务器:
./odoo-bin -c server.conf -d db_name -i my_hostel --test-enable -
现在,检查服务器日志。如果我们的测试用例运行成功,您将找到以下日志:
INFO test odoo.addons.my_hostel.tests.test_hostel_room_state: Starting TestHostelRoomState.test_button_available ... INFO test odoo.addons.my_hostel.tests.test_hostel_room_state: Starting TestHostelRoomState.test_button_closed ... INFO test odoo.modules.loading: Module my_hostel loaded in 0.31s (incl. 0.05s test), 240 queries (+33 test, +240 other)
如果测试用例失败或出现错误,你将看到 ERROR 日志而不是 INFO。
它是如何工作的...
在 Odoo 中,Python 测试用例被添加到模块的 tests/ 目录中。Odoo 将自动识别此目录并在该文件夹下运行测试。
注意
你还需要在 tests/__init__.py 中列出你的测试用例文件。如果你不这样做,该测试用例将不会执行。
Odoo 使用 Python 的 unittest 进行 Python 测试用例。要了解更多关于 unittest 的信息,请参阅 docs.python.org/3.5/library/unittest.html。Odoo 提供以下辅助类:
-
Common类:这个类提供了测试用例的公共方法和设置。它包括在测试期间创建和管理数据库事务等功能。 -
SavepointCase类:这个类扩展了 Common 类。 -
SavepointCase在测试期间提供了处理保存点的额外功能。当你想在测试期间回滚对数据库所做的更改,确保每个测试从一个干净的状态开始时,这非常有用, -
TransactionCase类:这个类扩展了SavepointCase并提供了事务相关的功能。它有助于在测试期间管理数据库事务。 -
HttpCase类:这个类用于测试 HTTP 请求和响应。它允许你模拟 HTTP 请求并测试响应。 -
BaseCase类:这是 Odoo 中各种测试用例的基类。它提供了在不同测试场景中可重用的公共功能, -
SingleTransactionCase类:这个类扩展了TransactionCase并确保每个测试用例都在单个数据库事务中执行。这在需要完全隔离测试的场景中非常有用。 -
FormCase类:这个类用于测试表单视图及其交互。它提供了模拟用户与表单视图交互的方法。 -
FunctionCase类:这个类旨在测试服务器端 Python 函数。它有助于在 Odoo 框架中测试各种函数和方法,并包装在unittest中。
这些类简化了开发测试用例的过程。在我们的案例中,我们使用了 TransactionCase。现在,TransactionCase 在不同的事务中运行每个测试用例方法。一旦一个测试用例方法成功运行,事务将自动回滚。这意味着下一个测试用例将不会受到前一个测试用例所做的任何修改。
类方法从test_开始,被视为测试用例。在我们的例子中,我们添加了两个测试用例。这检查了改变宿舍房间状态的函数。self.assertEqual方法(Python 中的assertEqual())是unittest库中的一个函数,用于单元测试中检查两个值的相等性。这个函数将接受三个参数作为输入,并返回一个布尔值,取决于assert条件。如果两个输入值相等,assertEqual()将返回true,否则返回false)用于检查测试用例是否成功运行。我们在对宿舍房间记录执行操作后检查了宿舍房间的状态。因此,如果开发者犯了一个错误,并且方法没有按预期改变状态,测试用例将失败。
重要信息
请注意, setUp() 方法将自动为每个运行的测试用例调用,因此,在这个菜谱中,我们添加了两个测试用例,以便 setUp() 将调用两次。根据这个菜谱中的代码,测试期间只会有一条宿舍房间记录,因为,在 TransactionCase* 中,每个测试用例都会回滚事务。
在 Python 中,文档字符串(docstring)是一种字符串字面量,它出现在模块、函数、类或方法定义的第一行。文档字符串用于提供关于代码片段功能的文档。它们作为内联文档的一种形式,可以使用各种工具访问,例如help()函数。这可以帮助检查特定测试用例的状态。
更多...
测试套件提供了以下额外的测试实用类:
-
SingleTransactionCase:通过这个类生成的测试用例将在单个事务中运行所有用例,因此一个测试用例所做的更改将在第二个测试用例中可用。这样,事务从第一个测试方法开始,只在最后一个测试用例结束时回滚。 -
SavepointCase:这与SingleTransactionCase相同,但在这个情况下,测试方法是在回滚的保存点内运行的,而不是在单个事务中运行所有测试方法。这是通过只生成一次测试数据来创建大型测试用例并使其更快的方法。在这里,我们使用setUpClass()方法生成初始测试数据。
运行标记的 Python 测试用例
当您使用--test-enabled模块名称运行 Odoo 服务器时,测试用例将在模块安装后立即运行。如果您想在所有模块安装后运行测试用例,或者如果您只想为单个模块运行测试用例,tagged()装饰器就是答案。
在这个菜谱中,我们将向您展示如何具体使用这个装饰器来塑造测试用例。需要注意的是,这个装饰器仅适用于类;它不会影响函数或方法。可以通过添加一个前缀减号(-)来修改标签,这将移除它们而不是添加或选择它们。例如,如果您想防止默认执行您的测试,您可以移除标准标签。
准备工作
对于这个菜谱,我们将使用上一道菜谱中的my_hostel模块。我们将修改测试用例的顺序。
如何操作...
按照以下步骤为 Python 测试用例添加标签:
-
将
tagged()装饰器(如下所示)添加到测试类中,以便在所有模块安装后运行它:from odoo.tests.common import TransactionCase, tagged @tagged('-at_install', 'post_install') class TestHostelRoomState(TransactionCase): def setUp(self, *args, **kwargs): super(TestHostelRoomState, self).setUp(*args, **kwargs) self.partner_nikul = self.env['res.partner'].create({'name': 'Nikul Chaudhary'}) self.partner_deepak = self.env['res.partner'].create({'name': 'Deepak Ahir'}) self.member_ids = self.env['hostel.room.member'].create([ {'partner_id': self.partner_nikul.id, 'member_number': '007'}, {'partner_id': self.partner_deepak.id, 'member_number': '357'}]) self.test_hostel_room = self.env['hostel.room'].create({ 'name': 'Hostel Room 01', 'room_no': '1', 'member_ids': [(6, 0, self.member_ids.ids)] }) def test_button_available(self): """Make available button""" self.test_hostel_room.make_available() self.assertIn(self.partner_nikul, self.test_hostel_room.mapped('member_ids.partner_id')) self.assertEqual( self.test_hostel_room.state, 'available', 'Hostel Room state should changed to available') def test_button_closed(self): """Make closed button""" self.test_hostel_room.make_available() self.test_hostel_room.make_closed() self.assertEqual( self.test_hostel_room.state, 'closed', 'Hostel Room state should changed to closed') -
之后,按照以下方式运行测试用例,就像之前一样:
./odoo-bin -c server.conf -d db_name -i my_hostel --test-enable -
现在,检查服务器日志。这次,您将在以下日志之后看到我们的测试用例日志,这意味着我们的测试用例是在所有模块安装之后运行的,如下所示:
INFO test odoo.modules.loading: Module my_hostel loaded in 0.21s, 240 queries (+240 other) INFO test odoo.modules.loading: Modules loaded INFO test odoo.service.server: Starting post tests INFO test odoo.addons.my_hostel.tests.test_hostel_room_state: Starting TestHostelRoomState.test_button_available ... INFO test odoo.addons.my_hostel.tests.test_hostel_room_state: Starting TestHostelRoomState.test_button_closed ... INFO test odoo.service.server: 2 post-tests in 0.04s, 36 queries INFO test odoo.tests.stats: my_hostel: 4 tests 0.04s 36 queries
在这些日志中,第一行显示加载了九个模块。第二行显示所有请求的模块及其依赖项都安装成功,第三行显示将开始运行标记为post_install的测试用例。
它是如何工作的...
默认情况下,所有测试用例都带有standard、at_install和当前模块的技术名称(在我们的案例中,技术名称是my_hostel)。因此,如果您不使用tagged()装饰器,您的测试用例将具有这三个标签。
在我们的案例中,我们希望在安装所有模块后运行测试用例。为此,我们在TestHostelRoomState类中添加了一个tagged()装饰器。默认情况下,测试用例具有at_install标签。因为这个标签,您的测试用例将在模块安装后立即运行;它不会等待其他模块安装。我们不希望这样,所以为了移除at_install标签,我们在标记函数中添加了-at_install。以-为前缀的标签将移除该标签。
通过在tagged()函数中添加-at_install,我们停止了模块安装后的测试用例执行。由于我们没有在此指定任何其他标签,因此测试用例不会运行。
因此,我们添加了一个post_install标签。这个标签指定了在所有模块安装完成后需要运行测试用例。
正如您所看到的,所有测试用例默认都带有standard标签。Odoo 将在您不希望始终运行特定测试用例而只想在请求时运行它的情况下,运行所有带有standard标签的测试用例。为此,您需要通过在tagged()装饰器中添加-standard来移除standard标签,并需要添加一个自定义标签,如下所示:
@tagged('-standard', 'my_custom_tag')
class TestClass(TransactionCase):
...
所有非标准测试用例都不会在--test-enable选项下运行。要运行前面的测试用例,您需要使用--test-tags选项,如下所示(注意,在这里,我们不需要显式传递--test-enable选项):
./odoo-bin -c server.conf -d db_name -i my_hostel --test-tags=my_custom_tag
更多...
在测试用例的开发过程中,运行单个模块的测试用例非常重要。默认情况下,模块的技术名称会被添加为标签,因此您可以使用模块的技术名称与--test-tags选项一起使用。例如,如果您想为my_hostel模块运行测试用例,那么您可以像这样运行服务器:
./odoo-bin -c server.conf -d db_name -i my_hostel --test-tags=my_hostel
这里给出的命令将在my_hostel模块中运行测试用例,但它仍然会根据at_install和post_install选项来决定顺序。
设置客户端测试用例的无头 Chrome
Odoo 使用无头 Chrome 来执行 JavaScript 和巡检测试用例,便于模拟最终用户环境。无头 Chrome 没有完整的 UI,使得 JavaScript 测试用例的执行无缝,确保了测试环境的连贯性。
如何操作...
您需要安装 Chrome 以启用 JavaScript 测试用例。在模块的开发中,我们将主要使用桌面操作系统。因此,如果您在系统上安装了 Chrome 浏览器,那么就没有必要单独安装它。您可以使用桌面 Chrome 运行客户端测试用例。请确保您的 Chrome 版本高于 Chrome 59。Odoo 还支持 Chromium 浏览器。
注意
无头 Chrome 客户端测试用例在 macOS 和 Linux 上运行良好,但 Odoo 不支持 Windows 上的无头 Chrome 测试用例。
当您想在生产服务器或服务器操作系统上运行测试用例时,情况会有所变化。服务器操作系统没有 GUI,因此您需要以不同的方式安装 Chrome。如果您使用的是基于 Debian 的操作系统,可以使用以下命令安装 Chromium:
apt-get install chromium-browser
重要信息
Ubuntu 22.04 服务器版默认未启用 universe 仓库。因此,安装 chromium-browser 可能会显示安装候选错误。要修复此错误,请使用以下命令启用universe仓库 – sudo add-apt-repository universe.
Odoo 还使用websocket-client Python 库。要安装它,请使用以下命令:
pip3 install websocket-client
现在,您的系统已准备好运行客户端测试用例。
它是如何工作的...
Odoo 使用无头 Chrome 进行 JavaScript 测试用例。这样做的原因是它在后台运行测试用例,因此也可以在服务器操作系统上运行。无头 Chrome 更喜欢在后台运行 Chrome 浏览器,而不打开 GUI 浏览器。Odoo 在后台打开一个 Chrome 标签页并开始在其中运行测试用例。它还使用jQuery的QUnit进行 JavaScript 测试用例。在接下来的几个菜谱中,我们将为我们的自定义 JavaScript 小部件创建一个 QUnit 测试用例。
对于测试用例,Odoo 在一个单独的进程中打开 Headless Chrome,因此要找出在该进程中运行的测试用例的状态,Odoo 服务器使用 WebSockets。websocket-client Python 库用于管理 WebSockets,以便从 Odoo 服务器与 Chrome 进行通信。
添加客户端 QUnit 测试用例
在 Odoo 中构建新的字段或视图非常简单。只需几行 XML,您就可以定义一个新的视图。然而,在底层,它使用了大量的 JavaScript。在客户端修改/添加新功能是复杂的,可能会破坏一些东西。大多数客户端问题都未被注意到,因为大多数错误只会在控制台中显示。因此,Odoo 使用 QUnit 测试用例来检查不同 JavaScript 组件的正确性。
QUnit 是一个主要用于客户端测试的 JavaScript 测试框架。它通常与测试 Web 应用程序中的 JavaScript 代码相关联,尤其是用于前端开发。QUnit 通常用于测试在浏览器环境中 JavaScript 函数、模块和组件的逻辑和行为。
准备工作
对于这个菜谱,我们将继续使用前一个菜谱中的 my_hostel 模块。我们将为 int_color 小部件添加一个 QUnit 测试用例。
如何操作...
按照以下步骤将 JavaScript 测试用例添加到 int_color 小部件:
-
我们已经在我们的模块中使用了 JavaScript 实现了
int_color小部件。 -
将以下代码添加到
/static/tests/colorpicker_tests.js中: -
创建一个
beforeEach函数,在应用测试用例之前按字段加载数据:/** @odoo-module */ import { registry } from "@web/core/registry"; import { session } from "@web/session"; import { uiService } from "@web/core/ui/ui_service"; import { makeView, setupViewRegistries} from "@web/../tests/views/helpers"; import { click, getFixture, patchWithCleanup } from "@web/../tests/helpers/utils"; const serviceRegistry = registry.category("services"); QUnit.module("Color Picker Widget Tests", (hooks) => { let serverData; let target; hooks.beforeEach(async function (assert) { target = getFixture(); serverData = { models: { 'hostel.room': { fields: { name: { string: "Hostel Name", type: "char" }, room_no: { string: "Room Number", type: "char" }, color: { string: "color", type: "integer"}, }, records: [{ id: 1, name: "Hostel Room 01", room_no: 1, color: 1, }, { id: 2, name: "Hostel Room 02", room_no: 2, color: 3 }], }, }, views: { "hostel.room,false,form": `<form> <field name="name"/> <field name="room_no"/> <field name="color" widget="int_color"/> </form>`, }, }; serviceRegistry.add("ui", uiService); setupViewRegistries(); }); -
为颜色选择器字段添加一个
QUnit测试用例,如下所示:QUnit.module("IntColorField"); QUnit.test("factor is applied in IntColorField", async function (assert) { const form = await makeView({ serverData, type: "form", resModel: "hostel.room", }); assert.containsOnce(target, '.o_field_int_color'); assert.strictEqual(target.querySelectorAll(".o_int_color .o_color_pill").length, 10, "Color picker should have 10 pills"); await click(target.querySelectorAll(".o_int_color .o_color_pill")[3]); assert.strictEqual(target.querySelector('.o_int_color .o_color_4').classList.contains("active"), true, "Click on pill should make pill active"); }); }); -
将以下代码添加到
__manifest__.py中以将其注册到测试套件中:'assets': { 'web.qunit_suite_tests': [ 'my_hostel/static/tests/**/*', ], },
要运行此测试用例,请在终端中使用以下命令启动您的服务器:
./odoo-bin -c server.conf -i my_hostel,web --test-enable
要检查测试是否成功运行,搜索以下日志:
... INFO test odoo.addons.web.tests.test_js.WebSuite: console log: "Color Picker Widget Tests" passed 2 tests.
它是如何工作的...
在 Odoo 中,JavaScript 测试用例被添加到 /static/tests/ 目录。在 步骤 1 中,我们添加了一个 colorpicker_test.js 文件用于测试用例。在该文件中,我们导入了用于 serviceRegistry 和 setupViewRegistries 以及 makeView 的注册表,因为我们在表单视图中创建了 int_color 小部件,所以为了测试小部件,我们需要表单视图。
@web/../tests/helpers/utils 将为我们提供构建 JavaScript 测试用例所需的测试工具。如果您不了解 JavaScript 的导入方式,请参考 第十四章 中的 扩展 CSS 和 JavaScript 以用于网站 菜谱,CMS 网站开发。
Odoo 客户端测试用例是用 QUnit 框架构建的,这是 JavaScript 单元测试用例的 jQuery 框架。有关更多信息,请参阅qunitjs.com/。beforeEach函数在运行测试用例之前被调用,这有助于初始化测试数据。beforeEach函数的引用由 QUnit 框架本身提供。
我们在beforeEach函数中初始化了一些数据。让我们看看这些数据如何在测试用例中使用。客户端测试用例在隔离(模拟)环境中运行,并且它不会连接到数据库,因此对于这些测试用例,我们需要创建测试数据。内部,Odoo 创建模拟服务器来模拟serverData属性作为数据库。因此,在beforeEach中,我们在serverData属性中初始化了我们的测试数据。serverData属性中的键被视为一个表,值包含有关字段和表行的信息。fields键用于定义表字段,records键用于表行。在我们的例子中,我们添加了一个包含三个字段(name(char)、room_no(char)和color(integer))的room表。请注意,在这里,您可以使用任何 Odoo 字段,甚至是关系字段——例如,{string: "M2o Field", type: "many2one", relation: 'partner'}。我们还使用records键添加了两个房间记录。
然后,我们使用QUnit.test函数添加了测试用例。函数中的第一个参数是string,用于描述测试用例。第二个参数是需要添加测试用例代码的函数。此函数由 QUnit 框架调用,并传递断言实用工具作为参数。在我们的例子中,我们在assert.expect函数中传递了期望的测试用例数量。我们添加了两个测试用例,因此我们传递了2。
我们想在可编辑表单视图中添加int_color小部件到测试用例中,因此我们使用makeView创建了可编辑表单视图。makeView函数接受不同的参数,如下所示:
-
resModel是给定视图创建的模型的名称。所有模型都在resModel中以属性的形式列出。我们想为房间模型创建一个视图,所以在我们的例子中,我们使用了房间作为模型。 -
serverData是我们将在视图中使用的记录。serverData中的视图键是您想要创建的视图的定义。因为我们想测试int_color小部件,所以我们通过小部件传递了视图定义。请注意,您只能使用在模型中定义的字段。 -
Type:视图的类型。
在使用int_color小部件创建表单视图后,我们添加了两个测试用例。第一个用例用于检查 UI 上的颜色药丸数量,第二个测试用例用于检查点击后药丸是否正确激活。我们有来自 QUnit 框架断言实用工具的strictEqual函数。如果前两个参数匹配,strictEqual函数将通过测试用例。如果不匹配,它将使测试用例失败。
更多内容...
对于 QUnit 测试用例,还有一些其他的断言函数可用,例如assert.deepEqual、assert.ok和assert.notOk。要了解更多关于 QUnit 的信息,请参考其文档qunitjs.com/。
添加游览测试用例
你现在已经看到了 Python 和 JavaScript 测试用例。这两个都在隔离环境中工作,并且它们之间不交互。为了测试 JavaScript 和 Python 代码之间的集成,使用游览测试用例。
准备工作
对于这个配方,我们将继续使用之前配方中的my_hostel模块。我们将添加一个游览测试用例来检查房间模型的流程。同时,请确保你已经安装了web_tour模块,或者已经将web_tour模块依赖项添加到清单中。
如何操作...
按照以下步骤为rooms添加游览测试用例:
-
添加一个
/static/src/js/my_hostel_tour.js文件,然后添加如下所示的游览:/** @odoo-module **/ import { _t } from "@web/core/l10n/translation"; import { registry } from "@web/core/registry"; import { stepUtils } from "@web_tour/tour_service/tour_utils"; import { markup } from "@odoo/owl"; registry.category("web_tour.tours").add("hostel_tour", { url: "/web", rainbowMan: false, sequence: 20, steps: () => [stepUtils.showAppsMenuItem(), { trigger: '.o_app[data-menu-xmlid="my_hostel.hostel_base_menu"]', content: markup(_t("Ready to launch your <b>Hostel</b>?")), position: 'bottom', edition: 'community', } -
为测试游览添加步骤:
trigger: '.o_app[data-menu-xmlid="my_hostel.hostel_base_menu"]', content: markup(_t("Ready to launch your <b>Hostel</b>?")), position: 'bottom', edition: 'enterprise', }, { trigger: '.o_list_button_add', content: markup(_t("Let's create new room.")), position: 'bottom', }, { trigger: ".o_form_view .o_field_char[name='name']", content: markup(_t('Add a new <b> Hostel Room </b>.')), position: "top", run: function (actions) { actions.text("Hostel Room 01", this.$anchor.find("input")); }, }, { trigger: ".ui-menu-item > a", auto: true, in_modal: false, }, { trigger: ".breadcrumb-item:not(.active):first", content: _t("Click on the breadcrumb to go back to your Pipeline. Odoo will save all modifications as you navigate."), position: "bottom", run: function (actions) { actions.auto(".breadcrumb-item:not(.active):last"); }, },]}); -
将
my_hostel_tour.js文件添加到测试资源中:'web.assets_backend': [ 'my_hostel/static/src/js/tours/my_hostel_tour.js', ], -
添加一个
/tests/test_tour.py文件,并通过HttpCase运行游览,如下所示:from odoo.tests.common import TransactionCase, tagged from odoo.tests import HttpCase @tagged('post_install', '-at_install') class TestUi(HttpCase): def test_01_hostel_tour(self): self.start_tour("/web", 'hostel_tour', login="admin")
为了运行测试用例,使用以下选项启动 Odoo 服务器:
./odoo-bin -c server.conf -i my_hostel --test-enable
现在,检查服务器日志。如果我们的测试用例运行成功,你将在这里找到以下日志:
...INFO test odoo.addons.my_hostel.tests.test_tour.TestroomUI: console log: Tour hostel_tour succeeded
它是如何工作的...
为了创建游览测试用例,你需要首先创建 UI 游览。如果你想了解更多关于 UI 游览的信息,请参考第十五章中的使用游览改进欢迎流程配方,Web 客户端开发。
在步骤 1中,我们注册了一个名为hostel_tour的新游览。这个游览与我们在第十五章中的使用游览改进欢迎流程配方中创建的游览完全一样。在步骤 2中,我们添加了游览的步骤。
在这里,与欢迎游览相比,我们有两个主要的变化。首先,我们在游览定义中添加了一个test=true参数;其次,我们添加了一个额外的属性,run。在run函数中,你必须编写执行通常由用户完成的操作的逻辑。例如,在游览的第四步中,我们要求用户输入房间标题。
为了自动化这一步骤,我们添加了一个run函数来设置title字段的值。run函数将动作实用工具作为参数传递。这提供了一些执行基本操作的快捷方式。其中最重要的如下:
-
actions.click(element)用于点击指定的元素。 -
actions.dblclick(element)用于在给定元素上执行双击操作。 -
actions.tripleclick(element)用于在给定元素上执行三击操作。 -
actions.text(string)用于设置输入值。 -
actions.drag_and_drop(to, element)用于拖放元素。 -
actions.keydown(keyCodes, element)用于在元素上触发特定的键盘事件。 -
actions.auto()是默认操作。当您在巡游步骤中不传递run函数时,将执行actions.auto()。这通常点击巡游步骤的触发元素。唯一的例外是输入元素。如果触发元素是input,巡游将在输入中设置默认值Test。这就是为什么我们不需要在所有步骤中添加run函数。
如果默认操作不够用,您可以手动执行整个操作。在下一个巡游步骤中,我们想要为颜色选择器设置一个值。请注意,我们使用了手动操作,因为默认值在这里没有帮助。因此,我们添加了带有基本 jQuery 代码的run方法来点击颜色选择器的第三个药丸。在这里,您将找到具有this.$anchor属性的触发元素。
默认情况下,注册的巡游会显示给最终用户,以改善入职体验。为了将它们作为测试用例运行,您需要在无头 Chrome 中运行它们。为此,您需要使用HttpCase Python 测试用例。这提供了browser_js方法,它打开 URL 并执行作为第二个参数传递的命令。您可以手动运行巡游,如下所示:
odoo.__DEBUG__.services['web_tour.tour'].run('hostel_tour')
在我们的示例中,我们将巡游的名称作为browser_js方法中的参数传递。下一个参数用于在执行第一个命令之前等待给定对象就绪。browser_js()方法中的最后一个参数是用户名。此用户名将用于创建新的测试环境,并且所有测试操作都将代表此用户执行。
从 UI 运行客户端测试用例
Odoo 提供了一种从 UI 运行客户端测试用例的方法。通过从 UI 运行测试用例,您将能够看到测试用例的每个步骤的实际操作。这样,您可以验证 UI 测试用例是否完全按照您期望的方式工作。
如何操作...
您可以从 UI 运行QUnit测试用例和巡游测试用例。由于 Python 测试用例在服务器端运行,因此无法从 UI 运行。为了看到从 UI 运行测试用例的选项,您需要启用开发者模式。
从 UI 运行 QUnit 测试用例
点击 bug 图标以打开下拉菜单,如图所示。点击运行 JS 测试选项:

图 18.1 – 运行测试用例的选项
这将打开 QUnit 套件,并开始逐个运行测试用例,如下面的屏幕截图所示。默认情况下,它只会显示失败的测试用例。要显示所有通过测试用例,取消选中隐藏通过测试复选框,如下面的屏幕截图所示:

图 18.2 – QUnit 测试用例的结果
从 UI 运行导游
点击 bug 图标以打开下拉菜单,如以下屏幕截图所示,然后点击开始导游:

图 18.3 – 运行导游测试用例的选项
这将打开一个包含已注册导游列表的对话框,如以下屏幕截图所示。点击旁边的播放按钮以运行导游:

图 18.4 – 导游测试用例列表
如果您已启用测试资产模式,测试导游才会以列表形式显示。如果您在列表中找不到hostel_tour导游,请确保您已激活测试资产模式。
它是如何工作的...
QUnit 的 UI 由 QUnit 框架本身提供。在这里,您可以筛选模块的测试用例。您甚至可以为单个模块运行测试用例。通过 UI,您可以查看每个测试用例的进度,并且可以深入到测试用例的每个步骤。内部,Odoo 只是打开相同的 URL 在 Headless Chrome 中。
点击运行导游选项将显示可用导游的列表。通过点击列表上的播放按钮,您可以运行导游。请注意,当导游通过命令行选项运行时,它将在回滚事务中运行,因此导游期间所做的更改将在导游成功后回滚。然而,当导游从 UI 运行时,它就像用户操作一样工作,这意味着导游所做的更改不会回滚并保留在那里,因此请谨慎使用此选项。
调试客户端测试用例
开发复杂的客户端测试用例可能会很头疼。在这个菜谱中,您将学习如何在 Odoo 中调试客户端测试用例。我们不会运行所有测试用例,而只会运行一个。此外,我们还将显示测试用例的 UI。
准备工作
对于这个菜谱,我们将继续使用前一个菜谱中的my_hostel模块。
如何做...
按照以下步骤以调试模式运行测试用例:
-
打开
/static/tests/colorpicker_test.js文件,并更新和添加makeView函数,如下所示:await makeView({ type: "form", resModel: "hostel.room", serverData: { models: { 'hostel.room': { fields: { name: { string: "Hostel Name", type: "char" }, room_no: { string: "Room Number", type: "char" }, color: { string: "color", type: "integer"}, }, records: [ { id: 1, name: "Hostel Room 01", room_no: 1, color: 1, }, { id: 2, name: "Hostel Room 02", room_no: 2, color: 3 } ], }, }, views: { }, }, arch: ` <form> <field name="name"/> <field name="room_no"/> <field name="color" widget="int_color"/> </form>`, }); -
检查
containtsN函数中的target参数,如下所示:assert.containsN( target, ".o_field_int_color", 1, "Both records are rendered" ); }); });
打开开发者模式,点击顶部菜单中的 bug 图标以打开下拉菜单,然后点击运行 JS 测试。这将打开 QUnit 套件:

图 18.5 – 运行测试用例的选项
这将只运行一个测试用例,即我们的颜色选择器测试用例。

图 18.6 – 颜色选择器测试用例
它是如何工作的...
在步骤 1中,我们将QUnit.test替换为QUnit.only。这将只运行此测试用例。在测试用例的开发过程中,这可以节省时间。请注意,使用QUnit.only将阻止通过命令行选项运行测试用例。这只能在调试或测试时使用,并且只能在您从 UI 打开测试用例时使用,所以开发完成后别忘了将其替换回QUnit.test。
在我们的 QUnit 测试用例示例中,我们创建了表单视图来测试int_color小部件。如果您从 UI 运行 QUnit 测试用例,您会发现您无法在 UI 中看到创建的表单视图。从 QUnit 套件的 UI 中,您只能看到日志。这使得开发 QUnit 测试用例变得非常困难。为了解决这个问题,我们在makeView函数中使用了debug参数。在步骤 2中,我们在makeView函数中添加了debug: true。这将显示测试表单视图在浏览器中。在这里,您可以通过浏览器调试器定位文档对象模型(DOM)元素。
警告
在测试用例结束时,我们通过destroy()方法销毁视图。如果您已经销毁了视图,那么您将无法在 UI 中看到表单视图,因此为了在浏览器中看到它,在开发期间请删除该行。这将帮助您调试测试用例。
在调试模式下运行 QUnit 测试用例可以帮助您非常容易和快速地开发测试用例。
为失败的测试用例生成视频/截图
Odoo 使用无头 Chrome,这开辟了新的可能性。从 Odoo 12 开始,您可以录制失败的测试用例的视频,也可以为它们截图。
如何操作...
为测试用例录制视频需要ffmpeg包:
-
要安装此功能,您需要在终端中执行以下命令(请注意,此命令仅在基于 Debian 的操作系统上有效):
apt-get install ffmpeg -
要生成视频或截图,您需要提供一个目录位置来存储视频或截图。
-
如果您想生成测试用例的屏幕录像(视频),请使用
--screencasts命令,如下所示:--screenshosts command, like this:./odoo-bin -c server.conf -i my_hostel --test-enable --screenshots=/home/pga/odoo_test/
它是如何工作的...
为了为失败的测试用例生成截图/屏幕录像,您需要运行服务器,指定保存视频或图像文件的路径。当您运行测试用例时,如果测试用例失败,Odoo 将在指定的目录中保存失败的测试用例的截图/视频。
要生成测试用例的视频,Odoo 使用ffmpeg包。如果您尚未在服务器上安装此包,那么它将只保存失败的测试用例的截图。安装包后,您将能够看到任何失败的测试用例的mp4文件。
注意
为测试用例生成视频可能会消耗更多的磁盘空间,因此请谨慎使用此选项,并且仅在真正需要时使用。
请记住,截图和视频仅用于失败的测试用例,因此如果你想测试它们,你需要编写一个失败的测试用例。
为测试填充随机数据
到目前为止,我们已经看到了用于检测业务逻辑中错误或错误的测试用例。然而,有时我们需要用大量数据来测试我们的开发。生成大量数据可能是一项繁琐的工作。Odoo 提供了一套工具,可以帮助你为你的模型生成大量的随机数据。在这个菜谱中,我们将使用 populate 命令为 hostel.room 和 hostel.room.member 模型生成测试数据。
准备工作
对于这个菜谱,我们将继续使用前一个菜谱中的 my_hostel 模块。我们将添加 _populate_factories 方法,该方法将用于生成测试数据。
如何操作...
按照以下步骤为 hostel.room 模型生成数据:
-
将
populate文件夹添加到my_hostel模块中。同时,添加一个__init__.py文件,内容如下:from . import hostel_data -
添加一个
my_hostel/populate/hostel_data.py文件,然后添加以下代码以生成宿舍房间的数据:import logging import random from odoo import models from odoo.tools import populate _logger = logging.getLogger(__name__) class RoomData(models.Model): _inherit = 'hostel.room.member' _populate_sizes = {'small': 10, 'medium': 100, 'large': 500} _populate_dependencies = ["res.partner"] def _populate_factories(self): partner_ids = self.env.registry.populated_models['res.partner'] return [ ('partner_id', populate.randomize(partner_ids)), ] class HostelData(models.Model): _inherit = 'hostel.room' _populate_sizes = {'small': 10, 'medium': 100, 'large': 500} _populate_dependencies = ["hostel.room.member"] def _populate_factories(self): member_ids = self.env.registry.populated_models['hostel.room.member'] def get_member_ids(values, counter, random): return [ (6, 0, [ random.choice(member_ids) for i in range(random.randint(1, 2)) ]) ] return [ ('name', populate.constant('Hostel Room {counter}')), ('room_no', populate.constant('{counter}')), ('member_ids', populate.compute(get_member_ids)), ] -
运行此命令以生成宿舍的数据:
./odoo-bin -c server.conf -d db_name -i my_hostel ./odoo-bin populate --models=hostel.room --size=medium -c server.conf -d db_name
这将为宿舍房间生成 100 个数据单位。生成数据后,进程将终止。要查看宿舍房间的数据,请运行不带 populate 参数的命令。
它是如何工作的...
在 步骤 1 中,我们将 populate 文件夹添加到 my_hostel 模块中。此文件夹包含填充测试数据的代码。
在 步骤 2 中,我们添加了填充房间数据的代码。为了生成随机数据,使用了 _populate_factories 方法。_populate_factories 方法返回用于模型字段的生成器,这些生成器将被用于生成随机数据。hostel.room 模型有必需的 name 和 room_no 字段,因此在我们的示例中,我们返回了这些字段的生成器。这个生成器将被用于生成 hostel room 记录的随机数据。我们使用了 populate.constant 生成器来生成名称字段;这将在我们迭代数据生成时生成不同的名称。
就像 populate.constant 一样,Odoo 提供了几个其他生成器来填充数据;以下是这些生成器的列表:
-
populate.randomize(list)将从给定的列表中返回一个随机元素。 -
populate.cartesian(list)与randomize()类似,但它将尝试包含列表中的所有值。 -
populate.iterate(list)将遍历给定的列表,一旦所有元素都被遍历,它将根据randomize或随机元素返回。 -
populate.constant(str)用于生成格式化的字符串。你也可以传递formatter参数来格式化值。默认情况下,格式化器是一个字符串格式化函数。 -
当你想根据你的函数计算一个值时,使用
populate.compute(function)。 -
populate.randint(a,b)用于生成介于a和b参数之间的随机数。
这些生成器可以用来生成你所需的选择的测试数据。
另一个重要的属性是 _populate_sizes。它用于根据 --size 参数定义你想要生成的记录数量。它的值始终取决于业务对象。
在 步骤 3 中,我们生成了一个数据宿舍房间模型。为了填充测试数据,你需要使用 --size 和 --model 参数。内部,Odoo 使用 _populate 方法来生成随机记录。_populate 方法本身使用 _populate_factories 方法来获取记录的随机数据。_populate 方法将为 --model 参数中给出的模型生成数据,测试数据的数量将基于模型中 _populate_sizes 属性的值。根据我们的示例,如果我们使用 –-size=medium,将生成 100 个宿舍房间的数据。
注意
如果你多次运行 populate 命令,数据也将多次生成。小心使用这一点很重要;如果你在生产数据库中运行该命令,它将在生产数据库本身中生成测试数据。这是你想要避免的事情。
还有更多…
有时,你可能还想生成关系型数据。例如,对于房间,你可能还想要创建成员记录。为了管理这些记录,你可以使用 _populate_dependencies 属性:
class RoomData(models.Model):
_inherit = 'hostel.room.member'
_populate_sizes = {'small': 10, 'medium': 100, 'large': 500}
_populate_dependencies = ["res.partner"]
. . .
这将在填充当前模型的数据之前填充依赖数据。一旦完成,你可以通过 populated_models 注册表访问已填充的数据:
partner_ids = self.env.registry.populated_models['res.partner']
上一行将给出在为当前模型生成测试数据之前已填充的公司列表。
第十九章:使用 Odoo.sh 进行管理、部署和测试
在 2017 年,Odoo 发布了 Odoo.sh,这是一种新的云服务。Odoo.sh 是一个平台,它使测试、部署和监控 Odoo 实例的过程尽可能简单。在本章中,我们将探讨 Odoo.sh 的工作原理,何时应该使用它而不是其他部署选项,以及其功能。
在本章中,我们将介绍以下菜谱:
-
探索 Odoo.sh 的一些基本概念
-
创建 Odoo.sh 账户
-
添加和安装自定义模块
-
管理分支
-
访问调试选项
-
获取您实例的备份
-
检查构建状态
-
所有 Odoo.sh 选项
重要注意事项
本章的编写假设您有 Odoo.sh 访问权限。这是一个付费服务,您需要订阅代码才能访问该平台。如果您是 Odoo 合作伙伴,您将获得免费的 Odoo.sh 订阅代码。否则,您需要从 www.odoo.sh/pricing 购买。即使您没有订阅代码,您也可以阅读本章。它包含足够的截图,可以帮助您了解该平台。
印刷读者注意事项
为了方便印刷读者,本章中包含了一些显示窗口布局的图片,可能需要放大才能清晰查看。您可以通过以下链接访问包含高质量图片的图形包:packt.link/gbp/9781805124276
探索 Odoo.sh 的一些基本概念
在这个菜谱中,我们将探讨 Odoo.sh 平台的一些功能。我们将回答一些基本问题,例如何时使用它以及为什么应该使用它。
什么是 Odoo.sh?
Odoo.sh 是一种云服务,它为平台提供了托管带有自定义模块的 Odoo 实例的能力。简单来说,它是 Odoo 的 平台即服务(PaaS)云解决方案。它与 GitHub 完全集成。任何包含有效 Odoo 模块的 GitHub 仓库都可以在几分钟内在 Odoo.sh 上启动。您可以通过并行测试多个分支来检查正在进行中的开发。一旦您将实例迁移到生产环境,您可以使用生产数据库的副本测试一些新功能;这有助于避免回归。它还会进行每日备份。使用 Odoo.sh,您可以高效地部署 Odoo 实例,即使您没有深厚的 DevOps 知识。它自动设置具有顶级配置的 Odoo 实例。请注意,Odoo.sh 是 Odoo 的企业版。您不能使用 Odoo 社区版,因为 Odoo.sh 只会加载企业版。
为什么会引入 Odoo.sh?
在 Odoo.sh 推出之前,有两种托管 Odoo 实例的方式。第一种是使用 Odoo Online,这是一个软件即服务(SaaS)云服务。第二种方法是本地部署选项,其中你需要自己托管一个 Odoo 实例并在服务器上配置它。现在,这两种选项都有优缺点。在 Odoo 在线选项中,你不需要配置或部署它,因为它是一个 SaaS 服务。然而,你无法在这个平台上使用自定义模块。另一方面,使用本地部署选项,你可以使用自定义模块,但你需要自己完成所有工作。你需要购买服务器,你需要配置数据库和 NGINX,你还需要设置邮件服务器、每日备份和安全。
因此,需要一个新选项,它提供了 Odoo 在线的简单性和本地部署选项的灵活性。Odoo.sh 让你可以在不进行复杂配置的情况下使用自定义模块。它还提供了额外的功能,例如测试分支、预发布分支和自动测试。
重要提示
在 Odoo 在线上无法进行定制并不完全正确。使用 Odoo Studio 和其他技术,你可以进行定制。然而,这种定制的范围非常有限。
你应该在什么时候使用 Odoo.sh?
如果你不需要定制化,或者只需要在 Odoo 在线中可能实现的小量定制,你应该选择 Odoo 在线。这将节省时间和金钱。如果你想进行大量的定制,并且已经与专家 DevOps 工程师团队合作,你可以选择本地部署选项。Odoo.sh 适合当你对 Odoo 定制化有良好了解但没有任何 DevOps 专业知识时。使用 Odoo.sh,你无需执行复杂的配置;你可以立即开始使用它,包括你的定制。它甚至还会配置邮件服务器。
当你使用敏捷方法开发大型项目时,Odoo.sh 非常有用。这是因为 On Odoo.sh,你可以并行测试多个开发分支,并在几分钟内将稳定开发部署到生产环境中。你甚至可以将测试开发与最终客户共享。
Odoo.sh 有哪些功能?
Odoo 在 Odoo.sh 平台的发展上投入了大量时间,因此它功能丰富。让我们看看 Odoo.sh 的功能。请注意,Odoo 会不时添加新功能。在本节中,我提到了在撰写本书时可用的一些功能,但你可能还会发现一些其他功能:
-
GitHub 集成:这个平台完全集成了 GitHub。你可以在这里测试每个分支、拉取或提交。对于每个新的提交,都会自动拉取一个新的分支。它还会为新提交运行自动测试。你甚至可以直接从 Odoo.sh UI 创建/合并分支。
-
Web 壳:Odoo.sh 为当前构建(或生产服务器)提供浏览器中的 Web 壳。在这里,您可以查看所有模块和日志。
-
Web 代码编辑器:就像 Web 壳一样,Odoo.sh 在浏览器中提供了代码编辑器。在这里,您可以访问所有源代码,还可以获取当前构建的 Odoo 交互式 shell。
-
SSH 访问:通过注册您的公钥,您可以通过 SSH 连接到任何容器。
-
将
requirement.txt文件放置在 GitHub 仓库的根目录下。目前,您只能安装 Python 包。无法安装系统包(apt 包)。 -
服务器日志:您可以从浏览器访问每个构建的服务器日志。这些日志是实时的,您也可以从这里过滤它们。
-
自动化测试:Odoo.sh 提供您自己的 runbot,您可以使用它来执行一系列自动化测试。每次您添加新的提交或新的开发分支时,Odoo.sh 都会自动运行所有测试用例并显示测试状态。您可以访问完整的测试日志,这有助于您在测试用例失败时找到问题。
-
预发布和开发分支:Odoo.sh 提供两种类型的分支:开发分支和预发布分支。在开发分支中,您可以使用演示数据测试正在进行中的开发。当开发完成,您想在将功能合并到生产之前测试功能时,使用预发布分支。预发布分支不加载演示数据;相反,它使用生产服务器的副本。
-
邮件服务器:Odoo.sh 自动为生产服务器设置邮件服务器。就像 Odoo 在线一样,Odoo.sh 不需要任何额外的邮件配置,尽管您可以使用自己的邮件服务器。
-
邮件捕获器:预发布分支使用您生产数据库的副本,因此包含有关您真实客户的信息。在这样一个数据库上进行测试可以使向真实客户发送邮件成为可能。为了避免这个问题,邮件功能仅在生产分支上激活。预发布和开发分支不会发送真实邮件;相反,它们使用邮件捕获器,这样您就可以在预发布和开发分支中测试和查看邮件。
-
共享构建:使用 Odoo.sh,您可以与客户共享开发分支,以便他们在将功能合并到生产之前进行测试。
-
更快部署:由于 Odoo.sh 完全集成 GitHub,您可以直接从浏览器通过简单的拖放操作合并和部署开发分支。
-
备份和恢复:Odoo.sh 为生产实例保留完整备份。您只需点击几下即可下载或恢复这些备份。请参阅“获取实例备份”菜谱以了解更多关于备份的信息。Odoo.sh 保留最多 3 个月的 14 个完整备份:每天 1 个,持续 7 天;每周 1 个,持续 4 周;每月 1 个,持续 3 个月。
-
社区模块:您可以通过几个简单的点击测试和安装任何社区模块。您还可以直接从应用商店测试免费模块。
创建 Odoo.sh 账户
在这个配方中,我们将创建一个 Odoo.sh 账户和一个用于自定义附加组件的空仓库。
准备工作
对于这个配方,您需要一个可以添加自定义模块的 GitHub 账户。您还需要一个 Odoo.sh 订阅代码。如果您是 Odoo 合作伙伴,您将获得免费的 Odoo.sh 订阅代码。否则,您需要从 www.odoo.sh/pricing 购买。
如何操作...
按照以下步骤创建 Odoo.sh 账户:
- 打开
www.odoo.sh并点击顶部菜单中的 登录。这将您重定向到 GitHub 页面:

图 19.1 – GitHub 认证
- 授权您的仓库,这将您重定向回 Odoo.sh。填写表单以部署实例:

图 19.2 – 创建 Odoo.sh 实例
- 这将部署实例,您将被重定向到 Odoo.sh 控制面板。等待构建状态成功;然后,您可以使用以下截图显示的 连接 按钮连接到您的实例:

图 19.3 – 连接到开发实例
点击 连接 后,您将自动登录到您的实例。如果您是管理员,通过点击侧边的箭头按钮,您也可以以其他用户的身份连接。
它是如何工作的...
Odoo.sh 平台与 GitHub 集成。您需要完全授权 Odoo.sh 以便它可以访问您的仓库。Odoo.sh 还将创建 webhooks。GitHub webhooks 会在您的仓库中添加新的提交或分支时通知 Odoo.sh 平台。当您首次登录时,Odoo.sh 将您重定向到 GitHub。GitHub 将显示一个类似于 步骤 1 中的截图的页面,您需要提供对所有私有和公共仓库的访问权限。如果您不是仓库的所有者,您将看到向所有者请求访问权限的按钮。
在您授予 Odoo.sh 仓库访问权限后,您将被重定向回 Odoo.sh,在那里您将看到部署 Odoo 实例的表单。要创建新实例,您需要添加以下信息:
-
GitHub 仓库:在这里,您需要设置带有自定义模块的 GitHub 仓库。此仓库中的模块将可供 Odoo 实例使用。您将看到所有现有仓库的列表。您可以选择其中一个或创建一个新的。
-
Odoo 版本:选择您想要部署的 Odoo 版本。您可以从当前支持的 Odoo LTS 版本中选择。请确保您选择的版本与 GitHub 仓库中的模块兼容。在我们的示例中,我们将选择版本 14.0。
-
订阅代码:这是激活实例的代码。购买 Odoo.sh 计划后,您将通过电子邮件收到此代码;如果您是官方 Odoo 合作伙伴,您可以向 Odoo 申请此代码。
-
托管位置:在这里,您需要根据您的地理位置选择服务器位置。最近的服务器将提供最佳性能。托管位置下显示的延迟基于您的位置。因此,如果您为您的客户创建实例,而客户位于另一个国家,您需要选择一个接近客户位置且延迟较低的服务器位置。
-
一旦提交此表单,您的 Odoo 实例将被部署,您将被重定向到 Odoo.sh 控制面板。在这里,您将看到您的第一个构建。这需要几分钟时间,然后您将能够连接到您的 Odoo 实例。如果您检查左侧面板,您将看到生产和预发布部分没有分支,而开发部分只有一个分支。在接下来的几个食谱中,我们将看到您如何创建预发布和生产的分支。
还有更多...
目前,Odoo.sh 只支持 GitHub。其他版本控制系统,如 GitLab 和 Bitbucket,目前不支持。如果您想使用除 GitHub 之外的系统,您可以使用通过子模块链接到您实际仓库的中间 GitHub 仓库。根据 Odoo 官方的说法,未来 Odoo 将会增加对 GitLab 和 Bitbucket 的支持,但这目前不是优先事项。如果您想使用 GitLab 或 Bitbucket,这里提供的方法只是一个权宜之计。
添加和安装自定义模块
如我们之前在 探索 Odoo.sh 的一些基本概念 食谱中所述,在 Odoo.sh 平台上,您可以添加自定义 Odoo 模块。该平台与 GitHub 集成,因此向已注册的仓库添加新提交将在相应的分支中创建新的构建。在本食谱中,我们将在我们的仓库中添加一个自定义模块,并在 Odoo.sh 中访问该模块。
准备工作
在我们的示例中,我们将从 第十八章 的 自动化测试用例 中选择 my_hostel 模块。您可以在本食谱中添加任何有效的 Odoo 模块,但我们将在这里使用带有测试用例的模块,因为 Odoo.sh 平台将自动执行所有测试用例。为了简化,我们已将此模块添加到本书的 GitHub 仓库中,位于 Chapter20/r0_initial_module/my_hostel。
如何操作...
按照以下步骤将自定义模块添加到 Odoo.sh:
-
在你的本地机器上获取 Git 仓库,将其中的
my_hostel模块添加进去,然后执行以下命令将模块推送到 GitHub 仓库:git add . git commit -am"Added my_hostel module" git push origin main -
在 Odoo.sh 中打开你的项目。在这里,你可以找到针对此提交的新构建。它将开始运行测试用例,你将看到以下屏幕:

图 19.4 – 宿舍模块的新构建
- 当你在 Odoo.sh 项目中拉取新的提交后,你将在右侧看到安装进度。等待安装完成,然后通过点击绿色的
my_hostel模块来访问你的实例:

图 19.5 – 安装的宿舍模块
探索并测试 my_hostel 模块。请注意,这不是一个生产构建,所以你可以随意测试它。
工作原理...
在 步骤 1 中,我们将 my_hostel 模块上传到 GitHub 仓库。Odoo.sh 将通过 webhook 立即通知这些更改。然后,Odoo.sh 将开始构建一个新的实例。它将安装所有你的自定义模块及其依赖项。新的构建将自动为安装的模块执行测试用例。
重要提示
默认情况下,Odoo.sh 只会安装你的自定义模块及其依赖项。如果你想改变这种行为,你可以在全局设置的模块安装部分进行操作。我们将在接下来的几个菜谱中详细查看这些设置。
在 历史 选项卡中,你将能够看到分支的完整历史。在这里,你可以找到有关构建的一些基本信息。它将显示提交信息、作者信息和提交的 GitHub 链接。在右侧,你将获得构建的实时进度。请注意,开发部分中的构建将安装带有演示数据的模块。在接下来的几个菜谱中,你将详细了解生产、开发和预发布分支之间的差异。
构建成功后,你将看到一个按钮来连接实例。默认情况下,你将以管理员用户身份连接。通过 连接 作为下拉菜单,你可以登录为演示和门户用户。
更多内容...
Odoo.sh 将为每个新的提交创建一个新的构建。你可以从分支的 设置 选项卡更改此行为:

图 19.6 – 开发分支选项
在这里,你可以找到几个选项。其中之一是 对新提交的行为。它有三个可能的值:
-
新构建:此选项将为每个提交创建一个新的构建
-
不执行任何操作:此选项将忽略新的提交并执行任何操作
-
更新上一个构建:这将使用现有构建为新提交
模块安装 和 测试套件 选项可以帮助你控制测试套件。你可以使用这些选项禁用测试,也可以运行特定的测试用例。
管理分支
在 Odoo.sh 中,您可以创建多个开发分支和预发布分支,以及生产分支。在这个配方中,我们将创建不同类型的分支,并查看它们之间的区别。您将看到如何开发、测试和部署新功能的完整工作流程。
准备工作
访问www.odoo.sh/project并打开我们在创建 Odoo.sh 账户配方中创建的项目。我们将为新的功能创建一个开发分支,然后在预发布分支中对其进行测试。最后,我们将该功能合并到生产分支中。
如何操作...
在这个配方中,我们将创建 Odoo.sh 中的所有类型的分支。目前,我们在生产环境中没有任何分支,所以我们将从创建一个生产分支开始。
创建生产分支
目前,我们在开发部分只有一个主分支。主分支的最后构建显示一个绿色的标签,上面写着测试:成功,这意味着所有自动测试用例都已成功运行。我们可以将这个分支移动到生产分支,因为测试用例的状态显示一切正常。为了将您的主分支移动到生产分支,您只需将主分支从开发部分拖动到生产部分,如下面的截图所示:

图 19.7 – 将主分支移动到生产环境
这将创建您的生产分支。您可以通过右侧的连接按钮访问生产分支。一旦您打开生产实例,您会注意到生产数据库中还没有安装任何应用程序。这是因为生产实例需要您或您的最终客户根据要求安装和配置操作。请注意,这是一个生产实例,因此为了保持实例运行,您需要输入您的企业订阅代码。
创建开发分支
您可以直接从浏览器创建开发分支。点击开发部分旁边的加号(+)按钮。这将显示两种类型的输入。一种是分叉的分支,另一种是开发分支的名称。填写完输入后,按Enter键。
这将通过分叉指定的分支来创建一个新的分支,如下面的截图所示:

图 19.8 – 创建一个新的开发分支
重要提示
如果您不想从 UI 创建开发分支,您可以直接从 GitHub 创建。如果您在 GitHub 仓库中添加一个新的分支,Odoo.sh 将自动创建一个新的开发分支。
开发中的分支通常是新功能分支。例如,我们将在 hostel.room 模型中添加一个新字段。按照以下步骤在 hostel 模型中添加一个新 HTML 字段:
-
在
manifest文件中增加模块版本:... 'version': '17.0.1.0.1', ... -
在模型中添加新字段:
other_info = fields.Text("Other Information", help="Enter more information") description = fields.Html('Description') policy = fields.Html('Description') -
在宿舍的表单视图中添加一个 策略 字段:
<notebook> <page string="Policy"> <field name="policy"/> </page> </notebook> -
在终端中执行以下命令以推送功能分支的更改:
git commit -am"Added room policy" git push origin feature-branch
这将在 Odoo.sh 上创建一个新的构建。构建成功后,您可以通过访问实例来测试这个新功能。您将在书籍的表单视图中看到一个新 HTML 字段。请注意,这个分支是开发分支,所以新功能仅适用于这个分支。您的生产分支没有改变。
创建预发布分支
一旦您完成开发分支并且测试用例成功,您可以将分支移动到 预发布 部分。这是预生产部分。在这里,新功能将使用生产数据库的副本进行测试。这将帮助我们找到可能在生产数据库中产生的问题。要从开发分支移动到 预发布 分支,只需将分支拖放到 预发布 部分即可:

图 19.9 – 将开发分支移动到预发布
一旦您将 开发 分支移动到 预发布 部分,您就可以使用生产数据测试您的新开发。就像任何其他构建一样,您可以通过右侧的 连接 按钮访问 预发布 分支。唯一的区别是,在这种情况下,您将能够看到生产数据库的数据。在这里,只有当您从 manifest 中增加了模块版本时,您的开发模块才会自动升级。
重要提示
预发布分支将使用生产数据库的副本,因此预发布实例将包含真实客户及其电子邮件。因此,在预发布分支中,真实电子邮件被禁用,这样您在测试预发布分支中的新功能时就不会意外发送任何邮件。
如果您没有更改模块版本,您需要手动升级模块才能看到新功能的效果。
在生产分支中合并新功能
在您使用生产数据库(在预发布分支中)测试新开发之后,您可以将新开发部署到 生产 分支。就像以前一样,您只需要将 预发布 分支拖放到 生产 分支。这将合并新功能分支到主分支。就像 预发布 分支一样,只有当您从 manifest 中增加了模块版本时,您的开发模块才会自动升级。在此之后,新模块对最终客户可用:

图 19.10 – 合并更改到生产
一旦你将暂存分支移至生产,将显示一个弹出窗口,包含两个选项:
-
变基和合并:这将创建一个拉取请求,并将其与变基合并,这样你将拥有线性历史。
-
合并:这将创建一个不带快速前进的合并提交:

图 19.11 – 合并和变基以及合并按钮的显示弹出窗口
工作原理...
在前面的例子中,我们执行了完整的工作流程,将新功能部署到生产中。以下列表解释了 Odoo.sh 中不同类型分支的目的:
-
生产分支:这是最终客户使用的实际实例。只有一个生产分支,新功能旨在与这个分支合并。在这个分支中,邮件服务是活跃的,因此你的最终客户可以发送和接收电子邮件。此分支的每日备份也是活跃的。
-
开发分支:这种类型的分支显示了所有活跃的开发。你可以创建无限量的开发分支,并且分支中的每个新提交都会触发一个新的构建。这个分支中的数据库加载了演示数据。开发完成后,这个分支将被移动到暂存分支。在这些分支中,邮件服务是不活跃的。
-
暂存分支:这是工作流程的中间阶段。一个稳定的开发版本将被移动到暂存分支,与生产分支的副本进行测试。这是开发生命周期中的一个非常重要的步骤;可能会发生这样的情况,即开发分支中运行良好的功能在生产数据库中无法按预期工作。暂存分支给你提供了一个在部署到生产之前用生产数据库测试功能的机会。如果你在这个分支中发现任何开发问题,你可以将分支移回开发。暂存分支的数量基于你的 Odoo.sh 计划。默认情况下,你只有一个暂存分支,但如果你想的话,可以购买更多。
这是将新功能合并到生产中的完整工作流程。在下一个菜谱中,你将看到我们可以与这些分支一起使用的其他一些选项。
访问调试选项
Odoo.sh 提供不同的功能用于分析和调试目的。在这个菜谱中,我们将探索所有这些功能和选项。
如何操作...
我们将使用相同的 Odoo.sh 项目来完成这个菜谱。每个选项将在不同的部分中展示,并附有截图。
分支历史
你已经在之前的菜谱中看到了这个功能。历史标签页显示了分支的完整历史。你可以从这里连接到构建:

图 19.12 – 历史标签页
在历史标签页中,你可以看到对所选分支执行的所有过去操作。它将显示日志、合并、新提交和数据库恢复。
邮件捕捉器
预发布分支使用您生产数据库的副本,因此它包含有关您的客户信息。测试预发布分支可以向真实客户发送电子邮件。这就是为什么电子邮件仅在生产分支上激活。预发布和开发分支不会发送真实电子邮件。如果您想在将任何功能部署到生产之前测试电子邮件系统,您可以使用邮件捕捉器,您可以看到所有已发送电子邮件的列表。邮件捕捉器将在预发布和开发分支中可用。
邮件捕捉器将显示带有源和任何附件的电子邮件,如下一个截图所示:

图 19.13 – 邮件捕捉器
在MAILS标签页中,您可以看到所有捕获的邮件及其所有附件的列表。请注意,MAILS标签页仅在预发布和开发分支中显示。
网页壳
从pip维护多个标签。
查看以下截图:您可以通过点击SHELL来访问网页壳:

图 19.14 – 网页壳
使用 shell 访问,您可以在不同的目录之间导航并执行操作。您还可以使用pip命令安装 Python 包。
这是根目录的目录结构:
.
├── data
│ ├── addons
│ ├── filestore
│ └── sessions
├── logs
├── Maildir
│ ├── cur
│ ├── new
│ └── tmp
├── repositories
│ └── git_github.com_pga-odoo_odooshdemov17.git
├── src
│ ├── enterprise
│ ├── odoo
│ ├── themes
│ └── user
└── tmp
这些目录可以根据分支类型而不同。例如,Maildir仅在预发布和开发分支中可用,因为它使用邮件捕捉器。
有时,您需要从 shell 重新启动服务器或更新模块。您可以在 shell 中使用以下命令来重新启动服务器:
odoosh-restart
要更新模块,请在 shell 中执行给定的命令:
odoo-bin -u my_hostel --stop-after-init
odoo-update my_hostel
之前的命令将更新my_hostel模块。如果您想更新多个模块,您可以通过逗号分隔模块名称。
代码编辑器
如果您不习惯使用 shell 访问,Odoo.sh 提供了一个功能齐全的编辑器。在这里,您可以访问 Python shell、Odoo shell 和终端。您还可以从这里编辑源代码,如所提供的截图所示。修改源代码后,您可以从顶部的Odoo菜单重新启动服务器:

图 19.15 – 网页代码编辑器
如前一个截图所示,您将能够从编辑器更新文件。Odoo 将自动检测更改并重新启动服务器。请注意,如果您在数据文件中进行了更改,您将需要更新模块。
日志
在LOGS标签页中,您可以访问您实例的所有日志。您可以在不重新加载页面的情况下查看实时日志。您可以从这里过滤日志。这允许您从生产服务器中查找问题。以下是您可以在LOGS标签页中找到的不同日志文件列表:
-
install.log:这是在安装模块时生成的日志。所有自动化测试用例的日志都将位于此处。 -
pip.log:您可以使用requirement.txt文件添加 Python 包。在这个日志文件中,您将找到这些 Python 包的安装日志。 -
odoo.log:这是 Odoo 的正常访问日志。您将在这里找到完整的访问日志。您应该查看此日志以检查生产错误。 -
update.log:当您上传具有不同清单版本的模块时,您的模块会自动更新。此文件包含这些自动更新的日志。
看看下面的截图。这显示了生产分支的实时日志:

图 19.16 – 服务器日志
前面的截图显示日志是实时更新的,因此您将能够看到新日志而无需重新加载。此外,您还可以通过 UI 右上角的文本框搜索特定的日志。
更多...
模块顶部提供了一些常用的git命令,如下面的截图所示。您可以通过左侧的运行按钮来执行这些命令。这些命令不能编辑,但如果您想运行一个修改后的命令,您可以将其从这里复制,然后从 shell 中运行:

图 19.17 – Git 命令
您可以在 shell 中执行这些git命令以执行各种操作,如前一个截图所示。
获取实例的备份
备份对于生产服务器至关重要。Odoo.sh 提供了一个内置的备份功能。在这个菜谱中,我们将说明您如何从 Odoo.sh 下载和恢复备份。
如何操作...
在生产分支中,您可以从顶部的备份选项卡访问有关备份的完整信息。这将显示备份列表:

图 19.18 – 备份管理器
您可以通过顶部的按钮执行备份操作,例如下载转储、执行手动备份或从备份中恢复。数据库备份可能需要很长时间,因此它将在后台完成。当完成时,您将在顶部的铃铛图标上收到通知。
工作原理...
Odoo 会自动每天备份您的生产实例。每当您合并新的开发分支并更新模块时,Odoo 也会自动备份。您还可以使用顶部的按钮执行手动备份。
Odoo.sh 为 Odoo 生产实例保留总共 14 个完整的备份,最多 3 个月——每天 1 个,持续 7 天,每周 1 个,持续 4 周,每月 1 个,持续 3 个月。您可以从备份选项卡访问 1 个月的备份(一周中的所有 7 天和 4 个周备份)。
如果您从本地或在线选项迁移到 Odoo.sh,您可以使用导入数据库按钮导入您的数据库。如果您直接将数据库导入生产,可能会引起问题。为了避免这种情况,您应该首先将数据库导入预发布分支。
检查构建状态
每次您进行新的提交时,Odoo.sh 都会创建新的提交。它还会执行自动测试用例。为了管理所有这些,Odoo.sh 有自己的 runbot 版本。在这个菜谱中,我们将检查所有构建的状态。
如何操作...
点击顶部的构建菜单以打开构建列表。在这里,您可以查看所有分支及其提交的完整概述:

图 19.19 – 构建状态
通过点击连接按钮,您可以连接到实例。您可以通过分支的背景颜色查看构建的状态。
它是如何工作的...
在 runbot 屏幕上,您将获得对构建的额外控制。您可以从这里连接到之前的构建。不同的颜色显示了构建的状态。绿色表示一切正常;黄色表示警告,可以忽略,但建议您修复它;红色表示存在必须修复的严重问题,在将开发分支合并到生产之前。红色和黄色的分支在连接按钮附近显示感叹号图标(!)。当您点击它时,您将获得一个包含错误和警告日志的弹出窗口。通常,您需要搜索安装日志文件以找到错误或警告日志,但此弹出窗口将过滤其他日志,仅显示错误和警告日志。这意味着每次构建变为红色或黄色时,您都应该来这里修复错误和警告,然后再将它们合并到生产中。
不活跃的开发分支在几分钟后会销毁。通常,当您添加新的提交按钮时,将创建一个新的构建。如果您想在没有新提交的情况下重新激活构建;然而,您可以使用左侧的重建按钮。除了最后一个将保持活跃之外,预发布分支的构建也会在几分钟后被销毁。
更多内容...
您可以从顶部栏中的状态菜单看到您实例的整体统计信息。平台服务器持续监控。在状态屏幕上,您将看到服务器可用性的统计信息,它将自动从平台的监控系统中计算得出。它将显示包括服务器运行时间在内的数据。状态页面将显示服务器输入和输出数据。状态页面将显示以下信息:

图 19.20 – Odoo.sh 状态
状态选项卡中显示的数据是从 Odoo.sh 使用的各种监控工具收集的。
所有 Odoo.sh 选项
Odoo.sh 在设置菜单下提供了一些额外的选项。在这个菜谱中,你将看到所有用于修改平台某些默认行为的重要选项。
准备工作
我们将使用之前菜谱中使用的相同的 Odoo.sh 项目。你可以从顶部栏中的设置菜单访问所有 Odoo.sh 设置。如果你看不到这个菜单,这意味着你正在访问一个共享项目,并且你没有管理员权限。
如何操作...
从顶部栏中的设置菜单打开设置页面。我们将在以下部分查看不同的选项。
项目名称
你可以通过此选项更改 Odoo.sh 项目的名称。输入框中的项目名称将用于生成你的生产 URL。开发构建也使用此项目名称作为前缀。在这种情况下,我们的功能分支的 URL 可能如下所示:serpentcs-odooshdemov17-feature-branch-260887.dev.odoo.com

图 19.21 – 更改项目名称
重要提示
此选项将更改生产 URL,但你无法去掉*.odoo.com。如果你想在一个自定义域上运行生产分支,你可以在生产分支的设置选项卡中添加你的自定义域。你还需要在你的 DNS 管理器中添加一个 CNAME 条目。
协作者
你可以通过添加协作者来共享项目。在这里,你可以通过 GitHub ID 搜索并添加一个新的协作者。协作者可以有管理员或用户访问权限。具有管理员访问权限的协作者将拥有完全访问权限(包括设置)。另一方面,具有用户访问权限的协作者将拥有受限的访问权限。他们将能够看到所有构建,但无法访问生产或预发布分支的备份、日志、shell 或电子邮件,尽管他们可以完全访问开发分支:

图 19.22 – 添加协作者
重要提示
你还需要将这些用户权限给予 GitHub 仓库,否则他们无法从浏览器中创建新的仓库。
公开访问
使用此选项,你可以与最终客户共享构建。这可以用于演示或测试目的。为此,你需要启用允许公开****访问复选框:

图 19.23 – 向构建提供公开访问
注意,预发布分支将使用与生产分支相同的密码。然而,在开发分支中,你将看到表中显示的用户名和密码:

表 19.1
模块安装
在开发分支的设置标签页中,您将看到开发分支的模块安装选项。它提供了三个选项,如下面的截图所示:

图 19.24 – 模块安装选项
默认情况下,它设置为仅安装我的模块。此选项将安装所有自定义模块及其依赖模块到新的开发分支中。对这些模块仅执行自动化测试用例。第二个选项是完整安装。此选项将安装所有模块并对所有这些模块执行自动化测试用例。最后一个选项是安装模块列表。在此选项中,您需要传递一个以逗号分隔的模块列表,例如sales、purchases和my_hostel。此选项将安装指定的模块及其依赖项。
此设置仅适用于开发构建。预发布构建会复制生产构建,因此它们将在生产分支中安装相同的模块,并对已更新版本清单的模块执行测试用例。
子模块
当您使用私有模块作为子模块时,会使用子模块选项。此设置仅适用于私有子模块;公共子模块无需任何问题即可正常工作。无法公开下载私有仓库,因此您需要向 Odoo.sh 提供仓库访问权限。按照以下步骤添加对私有子模块的访问权限:
-
在输入框中复制您的私有子模块仓库的 SSH URL,然后点击添加。
-
复制显示的公钥。
-
将此公钥添加到 GitHub(Bitbucket 和 GitLab 上也有类似设置)的私有仓库设置中的部署密钥:

图 19.25 – 设置私有子模块
您也可以添加多个子模块,并且您也可以从这里删除子模块。
数据库工作者
您可以增加生产构建的工作者数量。当您有更多用户时,这很有用;通常,单个工作者可以处理 25 个后端用户或 5,000 名每日网站访客。这个公式并不完美;它可以根据使用情况而变化。此选项不是免费的,增加工作者数量会增加您的 Odoo.sh 订阅费用:

图 19.26 – 设置数据库工作者
这些数据库工作者是多线程的,每个工作者都能够处理 15 个并发请求。需要足够的工作者来处理所有到达的请求,但增加工作者数量并不会增加请求处理速度。它仅用于处理大量并发用户。
预发布分支
预发布分支用于在生产数据库上测试新的开发。默认情况下,Odoo.sh 为您提供一个预发布分支。如果您正在处理涉及大量开发者的大型项目,这可能在开发过程中成为瓶颈,因此您可以额外付费增加预发布分支的数量:

图 19.27 – 设置预发布分支
还有更多...
除了配置选项外,设置菜单还将显示一些与平台相关的统计信息。
数据库大小
本节将显示您生产数据库的大小。Odoo.sh 平台按每月每 GB USD 1 的费用计费数据库。此选项可以帮助您跟踪您的数据库。显示的数据库大小仅针对生产数据库;它不包括预发布和开发分支的数据库:

图 19.28 – 数据库大小
Odoo 源代码修订
本节将显示 Odoo 项目的 GitHub 修订号。它将显示当前在平台上使用的社区、企业版和主题项目的修订哈希。此源代码将每周自动更新。此选项可以帮助您在本地机器上获得完全相同的版本。您也可以通过仓库中的 git 命令从网络外壳中检查此信息。
第二十章:Odoo 中的远程过程调用
Odoo 服务器支持 远程过程调用(RPC),这意味着您可以从外部应用程序连接 Odoo 实例。例如,如果您想在用 Java 编写的 Fan Android 应用程序中显示发货订单的状态,您可以通过 RPC 从 Odoo 获取发货状态。使用 Odoo RPC API,您可以在数据库上执行任何 CRUD 操作。Odoo RPC 不仅限于 CRUD 操作;您还可以调用任何模型的公共方法。当然,您需要适当的访问权限来执行这些操作,因为 RPC 尊重您在数据库中定义的所有访问权限和记录规则。因此,它非常安全,因为 RPC 尊重所有访问权限和记录规则。Odoo RPC 不依赖于平台,因此您可以在任何平台上使用它,包括 Odoo.sh、在线或自托管平台。Odoo RPC 可以与任何编程语言一起使用,因此您可以将 Odoo 集成到任何外部应用程序中。
Odoo 提供两种类型的 RPC API:XML-RPC 和 JSON-RPC。在本章中,我们将学习如何从外部程序使用这些 RPC。最后,您将学习如何通过 OCA 的 odoorpc 库使用 Odoo RPC。
在本章中,我们将介绍以下食谱:
-
使用 XML-RPC 登录/连接 Odoo
-
使用 XML-RPC 搜索/读取记录
-
使用 XML-RPC 创建/更新/删除记录
-
使用 XML-RPC 调用方法
-
使用 JSON-RPC 登录/连接 Odoo
-
使用 JSON-RPC 获取/搜索记录
-
使用 JSON-RPC 创建/更新/删除记录
-
使用 JSON-RPC 调用方法
-
OCA odoorpc 库
-
生成 API 密钥
技术要求
在本章中,我们将使用我们在 第十九章 中创建的 my_hostel 模块,使用 Odoo.sh 进行管理、部署和测试。您可以在 GitHub 仓库中找到相同的初始 my_hostel 模块:github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter20。
在这里,我们不会介绍一种新的语言,因为您可能不熟悉它。我们将继续使用 Python 来访问 RPC API。如果您想使用其他语言,也可以,因为相同的程序可以在任何语言中应用以访问 RPC。
要通过 RPC 连接 Odoo,您需要一个正在运行的 Odoo 实例来连接。在本章中,我们将假设您有一个运行在 http://localhost:8017 的 Odoo 服务器,您调用了 cookbook_17e 数据库,并在其中安装了 my_hostel 模块。请注意,您可以通过 RPC 连接到任何有效的 IP 或域名。
使用 XML-RPC 登录/连接 Odoo
在这个食谱中,我们将通过 RPC 执行用户身份验证,以检查提供的凭据(服务器 _url、db_name、username 和 password)是否有效。
准备工作
要通过 RPC 连接到 Odoo 实例,您需要一个正在运行的 Odoo 实例来连接。我们将假设您在 http://localhost:8017 上运行了 Odoo 服务器,并且您已安装了 my_hostel 模块。
如何操作...
执行以下步骤以通过 RPC 进行用户身份验证:
-
添加
odoo_authenticate.py文件。您可以将此文件放在任何您想要的位置,因为 RPC 程序将独立工作。 -
将以下代码添加到文件中:
from xmlrpc import client server_url = 'http://localhost:8017' db_name = 'cookbook_17e' username = 'admin' password = 'admin' common = client.ServerProxy('%s/xmlrpc/2/common' % server_url) user_id = common.authenticate(db_name, username, password, {}) if user_id: print("Success: User id is", user_id) else: print("Failed: wrong credentials") -
使用以下命令从终端运行以下 Python 脚本:
python3 odoo_authenticate.py
如果您提供了有效的登录名和密码,它将打印一个包含用户 ID 的成功消息。
它是如何工作的...
在这个菜谱中,我们使用了 Python 的 xmlrpc 库通过 XML-RPC 访问 Odoo 实例。这是一个标准的 Python 库,您不需要安装任何其他东西来使用它。
对于身份验证,Odoo 在 /xmlrpc/2/common 端点上提供 XML-RPC。此端点用于元方法,不需要身份验证。authentication() 方法本身是一个公开方法,因此可以公开调用。authentication() 方法接受四个参数——数据库名称、用户名、密码和用户代理环境。用户代理环境是一个必填参数,但如果您不想传递用户代理参数,至少传递一个空字典。
当您使用所有有效参数执行 authenticate() 方法时,它将对 Odoo 服务器进行调用并执行身份验证。如果提供的登录 ID 和密码正确,它将返回用户 ID。如果用户不存在或密码不正确,它将返回 False。
在通过 RPC 访问任何数据之前,您需要使用 authenticate() 方法。这是因为使用错误的凭证访问数据将生成错误。
重要提示
Odoo 的在线实例(*.odoo.com)使用 OAuth 身份验证,因此实例上不设置本地密码。要在这些实例上使用 XML-RPC,您需要从实例的 设置 | 用户 | 用户 菜单手动设置用户的密码。
此外,用于访问数据的方法需要用户 ID 而不是用户名,因此需要 authenticate() 方法来获取用户的 ID。
更多内容...
/xmlrpc/2/common 端点提供了一种额外的方法:version()。您可以在没有凭证的情况下调用此方法。它将返回 Odoo 实例的版本信息。以下是一个 version() 方法使用的示例:
from xmlrpc import client
server_url = 'http://localhost:8017'
common = client.ServerProxy('%s/xmlrpc/2/common' % server_url)
version_info = common.version()
print(version_info)
前面的程序将生成以下输出:
$ python3 version_info.py
{'server_version': '17.0+e', 'server_version_info': [17, 0, 0, 'final', 0, 'e'], 'server_serie': '17.0', 'protocol_version': 1}
此程序将根据您的服务器打印版本信息。
使用 XML-RPC 搜索/读取记录
在本菜谱中,我们将了解如何通过 RPC 从 Odoo 实例获取数据。用户可以访问大部分数据,除了那些受安全访问控制和记录规则限制的数据。RPC 可以用在许多情况下,例如收集数据进行分析、一次性操作大量数据或获取数据以在其他软件/系统中显示。可能性无穷无尽,你可以在需要时随时使用 RPC。
准备中
我们将创建一个 Python 程序来从 hostel.room 模型获取房间数据。确保你已经安装了 my_hostel 模块,并且服务器正在 http://localhost:8017 上运行。
如何操作...
执行以下步骤通过 RPC 获取房间信息:
-
添加
rooms_data.py文件。你可以将此文件放置在任何你想要的位置,因为 RPC 程序将独立工作。 -
将以下代码添加到文件中:
from xmlrpc import client # room data with search method server_url = 'http://localhost:8017' db_name = 'cookbook_17e' username = 'admin' password = 'admin' common = client.ServerProxy('%s/xmlrpc/2/common' % server_url) user_id = common.authenticate(db_name, username, password, {}) models = client.ServerProxy('%s/xmlrpc/2/object' % server_url) if user_id: search_domain = [['name', 'ilike', 'Standard']] rooms_ids = models.execute_kw(db_name, user_id, password, 'hostel.room', 'search', [search_domain], {'limit': 5}) print('Rooms ids found:', rooms_ids) rooms_data = models.execute_kw(db_name, user_id, password, 'hostel.room', 'read', [rooms_ids, ['name', 'room_no']]) print("Rooms data:", rooms_data) else: print('Wrong credentials') -
使用以下命令从终端运行 Python 脚本:
python3 rooms_data.py
前一个程序将获取房间数据并给出以下输出:
$ python3 rooms_data.py
Rooms ids found: [1, 2, 3, 4, 5]
Rooms data: [{'id': 1, 'name': '8th Standard', 'room_no': '1'}, {'id': 2, 'name': '9th Standard', 'room_no': '2'}, {'id': 3, 'name': '10th Standard', 'room_no': '3'}, {'id': 4, 'name': '11th Standard', 'room_no': '4'}, {'id': 5, 'name': '12th Standard', 'room_no': '5'}]
之前截图中的输出是基于我的数据库中的数据。你的 Odoo 实例中的数据可能不同,因此输出也将不同。
它是如何工作的...
为了访问房间数据,你首先必须进行身份验证。在程序开始时,我们像在之前的 通过 XML-RPC 登录/连接 Odoo 菜谱中做的那样进行了身份验证。如果你提供了有效的凭证,authentication() 方法将返回用户记录的 id。我们将使用此用户 ID 来获取房间数据。
/xmlrpc/2/object 端点用于数据库操作。在我们的菜谱中,我们使用了 object 端点来获取房间数据。与 /xmlrpc/2/common 端点不同,此端点没有凭证将无法工作。使用此端点,你可以通过 execute_kw() 方法访问任何模型的公共方法。execute_kw() 接受以下参数:
-
数据库名称
-
用户 ID(我们从
authenticate()方法中获取) -
密码
-
模型名称,例如,
res.partner或hostel.room -
方法名称,例如,
search、read或create -
位置参数数组
-
关键字参数的字典(可选)
在我们的例子中,我们想要获取房间信息。这可以通过 search() 和 read() 的组合来完成。房间信息存储在 hostel.room 模型中,所以在 execute_kw() 中,我们使用 hostel.room 作为模型名称,search 作为方法名称。这将调用 ORM 的 search 方法并返回记录 ID。这里唯一的区别是 ORM 的 search 方法返回一个记录集,而此搜索方法返回一个 ID 列表。
在execute_kw()中,你可以为提供的方法传递参数和关键字参数。search()方法接受一个域作为位置参数,因此我们传递了一个域来过滤房间。search方法还有其他可选的关键字参数,如limit、offset、count和order,我们使用了limit参数来获取仅五条记录。这将返回包含名称中包含Standard字符串的房间 ID 的列表。
然而,我们需要从数据库中获取房间数据。我们将使用read方法来完成这项任务。read方法接受一个 ID 列表和字段列表来完成任务。在步骤 3的末尾,我们使用了从search方法接收到的房间 ID 列表,然后使用房间 ID 来获取房间的name和room_no。这将返回包含房间信息的字典列表。
重要提示
注意,在execute_kw()中传递的参数和关键字参数基于传递的方法。你可以通过execute_kw()使用任何公共 ORM 方法。你只需要给方法一个名称,有效的参数和关键字参数。这些参数将被传递到 ORM 中的方法。
还有更多...
通过search()和read()方法的组合获取的数据稍微有些耗时,因为它将进行两次调用。search_read是获取数据的另一种方法。你可以通过单个调用搜索和获取数据。以下是使用search_read()获取房间数据的替代方法。
重要提示
read和search_read方法即使在未请求id字段的情况下也会返回id字段。此外,对于many2one字段,你将得到一个由id和显示名称组成的数组。例如,create_uid many2one字段将返回如下数据:[07, '``Deepak ahir']。
它将返回与上一个示例相同的输出:
from xmlrpc import client
# room data with search_read method
server_url = 'http://localhost:8017'
db_name = 'cookbook_17e'
username = 'admin'
password = 'admin'
common = client.ServerProxy('%s/xmlrpc/2/common' % server_url)
user_id = common.authenticate(db_name, username, password, {})
models = client.ServerProxy('%s/xmlrpc/2/object' % server_url)
if user_id:
search_domain = [['name', 'ilike', 'Standard']]
rooms_ids = models.execute_kw(db_name, user_id, password,
'hostel.room', 'search_read',
[search_domain, ['name', 'room_no']],
{'limit': 5})
print('Rooms data:', rooms_ids)
else:
print('Wrong credentials')
search_read方法显著提高了性能,因为你可以在一个 RPC 调用中获取结果,所以使用search_read方法而不是search和read方法的组合。
使用 XML-RPC 创建/更新/删除记录
在上一个菜谱中,我们看到了如何通过 RPC 搜索和读取数据。在这个菜谱中,我们将通过 RPC 执行剩余的CRUD操作,这些操作包括创建、更新(写入)和删除(解除链接)。
准备工作
我们将创建一个 Python 程序,在hostel.room模型中create、write和unlink数据。确保你已经安装了my_hostel模块,并且服务器正在http://localhost:8017上运行。
如何做到这一点...
执行以下步骤通过 RPC 创建、写入和更新房间信息:
-
添加
rooms_operation.py文件。你可以将此文件放在任何你想要的位置,因为 RPC 程序将独立工作。 -
将以下代码添加到
rooms_operation.py文件中:from xmlrpc import client server_url = 'http://localhost:8017' db_name = 'cookbook_17e' username = 'admin' password = 'admin' common = client.ServerProxy('%s/xmlrpc/2/common' % server_url) user_id = common.authenticate(db_name, username, password, {}) models = client.ServerProxy('%s/xmlrpc/2/object' % server_url) if user_id: # create new room records. create_data = [ {'name': 'Room 1', 'room_no': '101'}, {'name': 'Room 3', 'room_no': '102'}, {'name': 'Room 5', 'room_no': '103'}, {'name': 'Room 7', 'room_no': '104'} ] rooms_ids = models.execute_kw(db_name, user_id, password, 'hostel.room', 'create', [create_data]) print("Rooms created:", rooms_ids) # Write in existing room record room_to_write = rooms_ids[1] # We will use ids of recently created rooms write_data = {'name': 'Room 2'} written = models.execute_kw(db_name, user_id, password, 'hostel.room', 'write', [room_to_write, write_data]) print("Rooms written", written) # Delete the room record rooms_to_delete = rooms_ids[2:] deleted = models.execute_kw(db_name, user_id, password, 'hostel.room', 'unlink', [rooms_to_delete]) print('Rooms unlinked:', deleted) else: print('Wrong credentials') -
使用给定的命令在终端中运行 Python 脚本:
python3 rooms_operation.py
上述程序将创建四个房间记录。在房间记录中更新数据并随后删除两个记录将给出以下输出(创建的 ID 可能因数据库而异):
$ python3 rooms_operation.py
Rooms created: [6, 7, 8, 9]
Rooms written True
Rooms unlinked: True
write和unlink方法在操作成功时返回True。这意味着如果你收到True响应,假设记录已成功更新或删除。
它是如何工作的...
在这个菜谱中,我们通过 XML-RPC 执行了create、write和delete操作。此操作也使用了/xmlrpc/2/对象端点和execute_kw()方法。
create()方法支持在单个调用中创建多个记录。在步骤 2中,我们首先创建了一个包含房间信息的字典。然后,我们使用房间的字典通过 XML-RPC 创建新的房间记录。创建新记录的 XML-RPC 调用需要两个参数:create方法名称和房间数据。这将创建hostel.room模型中的四个房间记录。在 ORM 中,当你创建记录时,它返回创建的记录集,但如果创建记录的 RPC,这将返回一个 ID 列表。
write方法的工作方式与create方法类似。在write方法中,你需要传递一个记录 ID 列表和要写入的字段值。在我们的例子中,我们更新了第一部分创建的房间名称。这将把第二个房间的名称从Room 3更新为Room 2。在这里,我们只为一个房间传递了一个id,但如果你想在一个调用中更新多个记录,你可以传递一个 ID 列表。
在程序的第三部分,我们删除了第一部分创建的两个房间。你可以使用unlink方法和记录 ID 列表来删除记录。
程序执行成功后,你将在数据库中找到两个房间记录,如图图 20**.3所示。在程序中,我们创建了四个记录,但我们还删除了两个,所以你只能在数据库中找到两个新记录。
还有更多...
当你通过 RPC 执行 CRUD 操作时,如果你没有权限执行该操作,可能会产生错误。使用check_access_rights方法,你可以检查用户是否有执行特定操作的适当访问权限。check_access_rights方法根据用户的访问权限返回True或False值。以下是一个示例,显示用户是否有创建房间记录的权限:
from xmlrpc import client
server_url = 'http://localhost:8017'
db_name = 'cookbook_17e'
username = 'admin'
password = 'admin'
common = client.ServerProxy('%s/xmlrpc/2/common' % server_url)
user_id = common.authenticate(db_name, username, password, {})
models = client.ServerProxy('%s/xmlrpc/2/object' % server_url)
if user_id:
has_access = models.execute_kw(db_name, user_id, password,
'hostel.room', 'check_access_rights',
['create'], {'raise_exception': False})
print('Has create access on room:', has_access)
else:
print('Wrong credentials')
# Output: Has create access on room: True
当你通过 RPC 执行复杂操作时,可以在执行操作之前使用check_access_rights方法来确保你有适当的访问权限。
使用 XML-RPC 调用方法
使用 Odoo,RPC API 不仅限于 CRUD 操作;你还可以调用业务方法。在这个菜谱中,我们将调用make_available方法来更改房间的状态。
准备工作
我们将创建一个 Python 程序来在hostel.room模型上调用make_available。确保你已经安装了my_hostel模块,并且服务器正在http://localhost:8017上运行。
如何做到这一点...
执行以下步骤通过 RPC 创建、写入和更新房间信息:
-
添加
rooms_method.py文件。你可以将此文件放在任何你想要的位置,因为 RPC 程序将独立工作。 -
将以下代码添加到文件中:
from xmlrpc import client server_url = 'http://localhost:8017' db_name = 'cookbook_17e' username = 'admin' password = 'admin' common = client.ServerProxy('%s/xmlrpc/2/common' % server_url) user_id = common.authenticate(db_name, username, password, {}) models = client.ServerProxy('%s/xmlrpc/2/object' % server_url) if user_id: # Create room with state draft room_id = models.execute_kw(db_name, user_id, password, 'hostel.room', 'create', [{ 'name': 'New Room', 'room_no': '35', 'state': 'draft' }]) # Call make_available method on new room models.execute_kw(db_name, user_id, password, 'hostel.room', 'make_available', [[room_id]]) # check room status after method call room_data = models.execute_kw(db_name, user_id, password, 'hostel.room', 'read', [[room_id], ['name', 'state']]) print('Room state after method call:', room_data[0]['state']) else: print('Wrong credentials') -
使用以下命令在终端中运行 Python 脚本:
python3 rooms_method.py
前面的程序将使用draft创建一个房间,然后我们将通过调用make_available方法来更改房间状态。之后,我们将获取房间数据以检查房间状态,这将生成以下输出:
$ python3 rooms_method.py
Room state after method call: available
此食谱的程序将创建一个新的房间记录并通过调用model方法来更改房间状态。到程序结束时,我们已经读取了房间记录并打印了更新的状态。
它是如何工作的...
你可以从 RPC 调用任何模型方法。这有助于你在不遇到任何副作用的情况下执行业务逻辑。例如,你从 RPC 创建了销售订单,然后调用了sale.order方法的action_confirm方法。这相当于在销售订单表单上点击确认按钮。
你可以调用模型的任何公共方法,但不能从 RPC 调用私有方法。以_开头的方法称为私有方法,例如_get_share_url()和_get_data()。
使用这些方法是安全的,因为它们通过 ORM 执行并遵循所有安全规则。如果方法访问未经授权的记录,它将生成错误。
在我们的示例中,我们创建了一个状态为draft的房间。然后,我们进行了另一个 RPC 调用以调用make_available方法,这将更改房间状态为available。最后,我们进行了另一个 RPC 调用以检查房间状态。这将显示房间状态已更改为可用,如图 20**.4所示。
在内部不返回任何内容的方法默认返回None。此类方法不能从 RPC 使用。因此,如果你想从 RPC 使用你的方法,至少添加返回True的语句。
更多内容...
如果从方法中生成异常,事务中执行的所有操作将自动回滚到初始状态。这仅适用于单个事务(单个 RPC 调用)。例如,假设你向服务器发出两个 RPC 调用,第二个调用期间生成了异常。这将回滚第二个 RPC 调用期间执行的操作。第一个 RPC 调用执行的操作不会回滚。因此,你希望通过 RPC 执行复杂操作。建议通过在模型中创建方法来在一个 RPC 调用中执行此操作。
通过 JSON-RPC 登录/连接 Odoo
Odoo 提供了一种类型的 RPC API:JSON-RPC。正如其名所示,JSON-RPC 使用 JSON 格式,并使用 jsonrpc 2.0 规范。在这个菜谱中,我们将看到如何使用 JSON-RPC 登录。Odoo 网页客户端本身使用 JSON-RPC 从服务器获取数据。
准备工作
在这个菜谱中,我们将通过 JSON-RPC 进行用户认证,以检查提供的凭据是否有效。请确保您已安装了 my_hostel 模块,并且服务器正在 http://localhost:8017 上运行。
如何做...
执行以下步骤以通过 RPC 进行用户认证:
-
添加
jsonrpc_authenticate.py文件。你可以把这个文件放在你想要的地方,因为 RPC 程序将独立工作。 -
将以下代码添加到文件中:
import json import random import requests server_url = 'http://localhost:8017' db_name = 'cookbook_17e' username = 'admin' password = 'admin' json_endpoint = "%s/jsonrpc" % server_url headers = {"Content-Type": "application/json"} def get_json_payload(service, method, *args): return json.dumps({ "jsonrpc": "2.0", "method": 'call', "params": { "service": service, "method": method, "args": args }, "id": random.randint(0, 1000000000), }) payload = get_json_payload("common", "login", db_name, username, password) response = requests.post(json_endpoint, data=payload, headers=headers) user_id = response.json()['result'] if user_id: print("Success: User id is", user_id) else: print("Failed: wrong credentials") -
使用以下命令从终端运行 Python 脚本:
python3 jsonrpc_authenticate.py
当你运行前面的程序,并且你已经传递了有效的登录名和密码时,程序将打印一条包含用户 id 的成功消息,如下所示:
$ python3 jsonrpc_authentication.py
Success: User id is 2
JSON 认证的工作方式与 XML-RPC 类似,但它返回 JSON 格式的结果。
它是如何工作的...
JSON-RPC 使用 JSON 格式通过 /jsonrpc 端点与服务器进行通信。在我们的示例中,我们使用了 Python 的 requests 包来发送 POST 请求,但如果你愿意,你也可以使用其他包,例如 urllib。
JSON-RPC 只接受 get_json_payload() 方法格式化的负载。这个方法将以有效的 JSON-RPC 2.0 格式准备负载。这个方法接受要调用的 service 名称和 method,其余参数将放在 *args 中。我们将在所有后续菜谱中使用这个方法。JSON-RPC 接受 JSON 格式的请求,并且只有当请求包含 {"Content-Type": "application/json"} 头信息时,这些请求才会被接受。请求的结果将以 JSON 格式返回。
与 XML-RPC 类似,所有公共方法,包括登录,都属于公共服务。因此,我们将 common 作为服务,将 login 作为方法来准备 JSON 负载。登录方法需要一些额外的参数,所以我们传递了数据库名、用户名和密码。然后,我们使用负载和头信息向 JSON 端点发送 POST 请求。如果你提供了正确的用户名和密码,该方法将返回用户 ID。响应将以 JSON 格式返回,你将在 result 键中获取结果。
重要提示
注意,在这个菜谱中创建的 get_json_payload() 方法用于从示例中移除重复的代码。这不是强制性的,所以请随意应用您自己的修改。
还有更多...
与 XML-RPC 类似,JSON-RPC 也提供了版本方法。这个版本的方法属于公共服务,并且可以公开访问。你可以不提供登录信息就获取版本信息。以下是一个示例,展示了如何获取 Odoo 服务器的版本信息:
import json
import random
import requests
server_url = 'http://localhost:8017'
db_name = 'cookbook_17e'
username = 'admin'
password = 'admin'
json_endpoint = "%s/jsonrpc" % server_url
headers = {"Content-Type": "application/json"}
def get_json_payload(service, method, *args):
return json.dumps({
"jsonrpc": "2.0",
"method": 'call',
"params": {
"service": service,
"method": method,
"args": args
},
"id": random.randint(0, 1000000000),
})
payload = get_json_payload("common", "version")
response = requests.post(json_endpoint, data=payload, headers=headers)
print(response.json())
这个程序将显示以下输出:
$ python3 jsonrpc_version_info.py
{'jsonrpc': '2.0', 'id': 361274992, 'result': {'server_version': '17.0+e', 'server_version_info': [17, 0, 0, 'final', 0, 'e'], 'server_serie': '17.0', 'protocol_version': 1}}
这个程序将根据你的服务器打印版本信息。
使用 JSON-RPC 获取/搜索记录
在上一个菜谱中,我们看到了如何通过 JSON-RPC 进行身份验证。在这个菜谱中,我们将看到如何使用 JSON-RPC 从 Odoo 实例中获取数据。
准备工作
在这个菜谱中,我们将使用 JSON-RPC 获取房间信息。确保你已经安装了my_hostel模块,并且服务器正在http://localhost:8017上运行。
如何操作...
执行以下步骤以从hostel.room模型获取房间数据:
-
添加
jsonrpc_fetch_data.py文件。你可以将此文件放在任何你想要的位置,因为 RPC 程序将独立工作。 -
将以下代码添加到文件中:
# place authentication and get_json_payload methods (see first jsonrpc recipe) if user_id: # search for the room's ids search_domain = [['name', 'ilike', 'Standard']] payload = get_json_payload("object", "execute_kw", db_name, user_id, password, 'hostel.room', 'search', [search_domain], {'limit': 5}) res = requests.post(json_endpoint, data=payload, headers=headers).json() print('Search Result:', res) # ids will be in result keys # read data for rooms ids payload = get_json_payload("object", "execute_kw", db_name, user_id, password, 'hostel.room', 'read', [res['result'], ['name', 'room_no']]) res = requests.post(json_endpoint, data=payload, headers=headers).json() print('Rooms data:', res) else: print("Failed: wrong credentials") -
使用以下命令从终端运行 Python 脚本:
python3 json_fetch_data.py
前面的程序将给出以下输出。第一个 RPC 调用将打印房间 ID,第二个将打印房间 ID 的信息:
$ python3 json_fetch_data.py
Search Result: {'jsonrpc': '2.0', 'id': 19247199, 'result': [1, 2, 3, 4, 5]}
Rooms data: {'jsonrpc': '2.0', 'id': 357582271, 'result': [{'id': 1, 'name': '8th Standard', 'room_no': '1'}, {'id': 2, 'name': '9th Standard', 'room_no': '2'}, {'id': 3, 'name': '10th Standard', 'room_no': '3'}, {'id': 4, 'name': '11th Standard', 'room_no': '4'}, {'id': 5, 'name': '12th Standard', 'room_no': '5'}]}
前面的截图显示的输出基于我的数据库中的数据。你的 Odoo 实例中的数据可能不同,因此输出也将不同。
工作原理...
在使用 JSON-RPC 登录/连接 Odoo的菜谱中,我们看到了你可以验证username和password。如果登录信息正确,RPC 调用将返回user_id。然后你可以使用这个user_id来获取模型的数据。像 XML-RPC 一样,我们需要使用search和read的组合来从模型获取数据。为了获取数据,我们使用object作为服务,execute_kw()作为方法。execute_kw()与我们在 XML-RPC 中用于数据的方法相同,因此它接受以下相同的参数:
-
数据库名称
-
用户 ID(我们从
authenticate()方法中获取) -
密码
-
模型名称,例如,
res.partner或hostel.room -
方法名称,例如,
search、read或create -
位置参数数组(
args) -
关键字参数字典(
optional)(kwargs)
在我们的示例中,我们首先调用了search方法。execute_kw()方法通常将强制参数作为位置参数,将可选参数作为关键字参数。在search方法中,domain是一个强制参数,所以我们将其放入列表中,并将optional参数限制作为keyword参数(字典)。你将得到一个 JSON 格式的响应,在这个菜谱中,search()方法 RPC 的响应将在result键中包含房间 ID。
在步骤 2中,我们使用read方法进行了 RPC 调用。为了读取房间信息,我们传递了两个位置参数:房间 ID 列表和要获取的字段列表。这个 RPC 调用将以 JSON 格式返回房间信息,你可以使用result键访问它。
重要提示
与execute_kw()方法相比,你可以使用execute作为方法。这个方法不支持关键字参数,因此如果你想传递一些可选参数,你需要传递所有中间参数。
更多...
与 XML-RPC 类似,您可以使用search_read()方法代替search()和read()方法组合,因为它稍微耗时一些。请看以下代码:
# place authentication and get_json_payload methods (see first jsonrpc recipe)
if user_id:
# search for the room's ids
search_domain = [['name', 'ilike', 'Standard']]
payload = get_json_payload("object", "execute_kw",
db_name, user_id, password,
'hostel.room', 'search_read', [search_domain, ['name', 'room_no']], {'limit': 5})
res = requests.post(json_endpoint, data=payload, headers=headers).json()
print('Rooms data:', res)
else:
print("Failed: wrong credentials")
代码片段是使用search_read()获取房间数据的另一种方式。它将返回与上一个示例相同的输出。
使用 JSON-RPC 创建/更新/删除记录
在上一个菜谱中,我们探讨了如何通过 JSON-RPC 搜索和读取数据。在本菜谱中,我们将通过 RPC 执行剩余的CRUD操作:创建、更新(写入)和删除(解除链接)。
准备工作
我们将创建一个 Python 程序来在hostel.room模型中create、write和unlink数据。请确保您已安装my_hostel模块,并且服务器正在http://localhost:8017上运行。
如何操作...
执行以下步骤通过 RPC 创建、写入和解除链接房间信息:
-
添加
jsonrpc_operation.py文件。您可以将此文件放在任何您想要的位置,因为 RPC 程序将独立工作。 -
将以下代码添加到文件中:
# place authentication and get_json_payload method (see last recipe for more) if user_id: # creates the room's records create_data = [ {'name': 'Room 1', 'room_no': '201'}, {'name': 'Room 3', 'room_no': '202'}, {'name': 'Room 5', 'room_no': '205'}, {'name': 'Room 7', 'room_no': '207'} ] payload = get_json_payload("object", "execute_kw", db_name, user_id, password, 'hostel.room', 'create', [create_data]) res = requests.post(json_endpoint, data=payload, headers=headers).json() print("Rooms created:", res) rooms_ids = res['result'] # Write in existing room record room_to_write = rooms_ids[1] # We will use ids of recently created rooms write_data = {'name': 'Room 2'} payload = get_json_payload("object", "execute_kw", db_name, user_id, password, 'hostel.room', 'write', [room_to_write, write_data]) res = requests.post(json_endpoint, data=payload, headers=headers).json() print("Rooms written:", res) # Delete in existing room record room_to_unlink = rooms_ids[2:] # We will use ids of recently created rooms payload = get_json_payload("object", "execute_kw", db_name, user_id, password, 'hostel.room', 'unlink', [room_to_unlink]) res = requests.post(json_endpoint, data=payload, headers=headers).json() print("Rooms deleted:", res) else: print("Failed: wrong credentials") -
使用以下命令从终端运行 Python 脚本:
python3 jsonrpc_operation.py
上述程序将创建四个房间。写一个房间并删除两个房间将给出以下输出(创建的 ID 可能因数据库的不同而不同):
$ python3 jsonrpc_operation.py
Rooms created: {'jsonrpc': '2.0', 'id': 837186761, 'result': [43, 44, 45, 46]}
Rooms written: {'jsonrpc': '2.0', 'id': 317256710, 'result': True}
Rooms deleted: {'jsonrpc': '2.0', 'id': 978974378, 'result': True}
write和unlink方法在操作成功时返回True。这意味着如果您收到True响应,则假设记录已成功更新或删除。
工作原理...
execute_kw()用于create、update和delete操作。从 Odoo 版本 12 开始,create方法支持创建多个记录。因此,我们准备了包含四个房间信息的数据字典。然后,我们使用hostel.room作为模型名称和create作为方法名称进行了 JSON-RPC 调用。这将创建数据库中的四个房间记录,并返回包含这些新创建房间 ID 的 JSON 响应。在下一个 RPC 调用中,我们想要使用这些 ID 来执行update和delete操作的 RPC 调用,因此我们将其分配给rooms_ids变量。
重要提示
当您尝试创建不提供所需字段值的记录时,JSON-RPC 和 XML-RPC 都会生成错误,所以请确保您已将所有必需字段添加到create值中。
在下一个 RPC 调用中,我们使用了write方法来更新现有记录。write方法接受两个位置参数;要更新的记录和要写入的值。在我们的例子中,我们通过使用创建的房间 ID 的第二个 ID 来更新房间的名称。这将把第二个房间的名称从Room 3改为Room 2。
然后,我们进行了最后一个 RPC 调用以删除两个房间记录。为此,我们使用了unlink方法。unlink方法只接受一个参数,即您想要删除的记录 ID。此 RPC 调用将删除最后两个房间。
还有更多...
与 XML-RPC 类似,您可以在 JSON-RPC 中使用 check_access_rights 方法来检查您是否有执行操作的访问权限。此方法需要两个参数:模型名称和操作名称。在以下示例中,我们检查对 hostel.room 模型的 create 操作的访问权限:
# place authentication and get_json_payload method (see last recipe for more)
if user_id:
payload = get_json_payload("object", "execute_kw",
db_name, user_id, password,
'hostel.room', 'check_access_rights', ['create'])
res = requests.post(json_endpoint, data=payload, headers=headers).json()
print("Has create access:", res['result'])
else:
print("Failed: wrong credentials")
此程序将生成以下输出:
$ python3 jsonrpc_access_rights.py
Has create access: True
当您通过 RPC 执行复杂操作时,在执行操作之前使用 check_access_rights 方法可以确保您有适当的访问权限。
使用 JSON-RPC 调用方法
在本菜谱中,我们将学习如何通过 JSON-RPC 调用模型的自定义方法。我们将通过调用 make_available() 方法来更改房间的状态。
准备工作
我们将创建一个 Python 程序来在 hostel.room 模型上调用 make_available。请确保您已安装 my_hostel 模块,并且服务器正在 http://localhost:8017 上运行。
如何操作...
执行以下步骤以通过 RPC 创建、写入和更新房间信息:
-
添加
jsonrpc_method.py文件。您可以将此文件放在任何您想要的位置,因为 RPC 程序将独立工作。 -
将以下代码添加到文件中:
# place authentication and get_json_payload method (see last recipe for more)
if user_id:
# Create the room record in draft state
payload = get_json_payload("object", "execute_kw",
db_name, user_id, password,
'hostel.room', 'create', [{
'name': 'Room 1',
'room_no': '101',
'state': 'draft'
}])
res = requests.post(json_endpoint, data=payload, headers=headers).json()
print("Room created with id:", res['result'])
room_id = res['result']
# Change the room state by calling make_available method
payload = get_json_payload("object", "execute_kw",
db_name, user_id, password,
'hostel.room', 'make_available', [room_id])
res = requests.post(json_endpoint, data=payload, headers=headers).json()
# Check the room status after method call
payload = get_json_payload("object", "execute_kw",
db_name, user_id, password,
'hostel.room', 'read', [room_id, ['name', 'state']])
res = requests.post(json_endpoint, data=payload, headers=headers).json()
print("Room state after the method call:", res['result'])
else:
print("Failed: wrong credentials")
- 使用以下命令在终端中运行 Python 脚本:
draft and then we will change the room state by calling the make_available method. After that, we will fetch the room data to check the room’s status, which will generate the following output:
$ python3 jsonrpc_method.py
创建的房间 ID:53
方法调用后的房间状态:[{'id': 53, 'name': 'Room 1', 'state': 'available'}]
The program of this recipe will create a new room record and change the state of the room by calling the model method. By the end of the program, we will have read the room record and printed the updated state.
How it works...
`execute_kw()` is capable of calling any public method of the model. As we saw in the *Calling methods through XML-RPC* recipe, public methods are those that have names that don’t start with `_` (underscore). Methods that start with `_` are private, and you cannot invoke them from JSON-RPC.
In our example, we created a room with a state of `draft`. Then, we made one more RPC call to invoke the `make_available` method, which will change the room’s state to `available`. Finally, we made one more RPC call to check the state of the room. This will show that the room’s state has changed to **Available**, as seen in *Figure 20**.10*.
Methods that do not return anything internally return `None` by default. Such methods cannot be used from RPC. Consequently, if you want to use your method from RPC, at least add the return `True` statement.
The OCA odoorpc library
The `odoorpc`. This is available at [`github.com/OCA/odoorpc`](https://github.com/OCA/odoorpc). The `odoorpc` library provides a user-friendly syntax from which to access Odoo data through RPC. It provides a similar a syntax similar to that of the server. In this recipe, we will see how you can use the `odoorpc` library to perform operations through RPC.
Getting ready
The `odoorpc` library is registered on the Python package (`PyPI`) index. In order to use the library, you need to install it using the following command. You can use this in a separate virtual environment if you want:
pip install OdooRPC
In this recipe, we will perform some basic operations using the `odoorpc` library. We will use the `hostel.room` model to perform these operations. Make sure you have installed the `my_hostel` module and that the server is running on `http://localhost:8017`.
How to do it...
Perform the following steps to create, write, and update a room’s information through RPC:
1. Add the `odoorpc_hostel.py` file. You can place this file anywhere you want because the RPC program will work independently.
2. Add the following code to the file:
```
import odoorpc
db_name = 'cookbook_17e'
user_name = 'admin'
password = 'admin'
# 准备与服务器建立连接
odoo = odoorpc.ODOO('localhost', port=8017)
odoo.login(db_name, user_name, password) # 登录
# 用户信息
user = odoo.env.user
print(user.name) # 连接用户的名称
print(user.company_id.name) # 用户的公司的名称
print(user.email) # 用户的电子邮件
RoomModel = odoo.env['hostel.room']
search_domain = [['name', 'ilike', 'Standard']]
rooms_ids = RoomModel.search(search_domain, limit=5)
for room in RoomModel.browse(rooms_ids):
print(room.name, room.room_no)
# 创建房间并更新状态
room_id = RoomModel.create({
'name': '测试房间',
'room_no': '103',
'state': 'draft'
})
room = RoomModel.browse(room_id)
print("Room state before make_available:", room.state)
room.make_available()
room = RoomModel.browse(room_id)
print("Room state after make_available:", room.state)
```py
3. Run the Python script from the Terminal with the following command:
```
python3 odoorpc_hostel.py
```py
The program will do the authentication, print user information, and perform an operation in the `hostel.room` model. It will generate the following output:
$ python3 odoorpc_hostel.py
米切尔管理员
Packt 出版
admin@yourcompany.example.com
8 年级 1 班
9 年级 2 班
10 年级 3 班
11 年级 4 班
12 年级 5 班
可用前的房间状态:草稿
可用后的房间状态:可用
The preceding output is the result of several RPC calls. We have fetched user info, some room info, and we have changed the state of the room.
How it works...
After installing the `odoorpc` library, you can start using it straight away. To do so, you will need to import the `odoorpc` package and then we will create the object of the `ODOO` class by passing the server URL and port. This will make the `/version_info` call to the server to check the connection. To log in, you need to use the `login()` method of the object. Here, you need to pass the `database` `name`, `username`, and `password`.
Upon successful login, you can access the user information at `odoo.env.user`. `odoorpc` provides a user-friendly version of RPC, so you can use this user object exactly like the record set in the server. In our example, we accessed the name, email, and company name from this user object.
If you want to access the model registry, you can use the `odoo.env` object. You can call any model method on the model. Under the hood, the `odoorpc` library uses `jsonrpc`, so you can’t invoke any private model method name that starts with an `_`. In our example, we accessed the `hostel.room` model from the registry. After that, we called the `search` method with the `domain` and `limit` parameters. This will return the IDs of the rooms. By passing the room IDs to the `browse()` method, you can generate a record set for the `hostel.room` model.
By the end of the program, we will have created a new room and changed the room’s state by calling the `make_available()` method. If you look closely at the syntax of the program, you will see that it uses the same syntax as the server.
There’s more...
Although it provides a user-friendly syntax like the server, you can use the library just like the normal RPC syntax. To do so, you need to use the `odoo.execute` method with the model name, method name, and arguments. Here is an example of reading some room information in the raw RPC syntax:
import odoorpc
db_name = 'cookbook_17e'
user_name = 'admin'
password = 'admin'
准备与服务器建立连接
odoo = odoorpc.ODOO('localhost', port=8017)
odoo.login(db_name, user_name, password) # 登录
rooms_info = odoo.execute('hostel.room', 'search_read',
[['name', 'ilike', 'Standard']])
print(rooms_info)
See also
There are several other implementations of RPC libraries for Odoo, as follows:
* [`github.com/akretion/ooor`](https://github.com/akretion/ooor)
* [`github.com/OCA/odoorpc`](https://github.com/OCA/odoorpc)
* [`github.com/odoo/openerp-client-lib`](https://github.com/odoo/openerp-client-lib)
* [`pythonhosted.org/OdooRPC`](http://pythonhosted.org/OdooRPC)
* [`github.com/abhishek-jaiswal/php-openerp-lib`](https://github.com/abhishek-jaiswal/php-openerp-lib)
Generating API keys
Odoo v17 has built-in support for the **two-factor authentication** (**2FA**) feature. 2FA is an extra layer of security for user accounts and users need to enter a password and time-based code. If you have enabled 2FA, then you won’t be able to use RPC by entering your user ID and password. To fix this, you will need to generate an API key for the user. In this recipe, we will see how you can generate API keys.
How to do it...
Perform the following steps to generate an API key for RPC:
1. Open user preferences and open the **Account** **Security** tab.
2. Click on the **New API** **Key** button:

Figure 20.1 – Generating a new API key
1. It will open a popup, as in the following screenshot. Enter the API key name and click on the **Generate** **key** button:

Figure 20.2 – Naming your key
1. This will generate the API key and show it in a new popup. Note down the API key because you will need this again:

Figure 20.3 – Noting the generated API key
Once the API key is generated, you can start using the API key for RPC in the same way as the normal password.
How it works…
Using API keys is straightforward. However, there are a few things that you need to take care of. The API keys are generated per user, and if you want to utilize RPC for multiple users, you will need to generate an API key for each user. Additionally, the API key for a user will have the same access rights as the user would have, so if someone gains access to the key, they can perform all the operations that the user can. So, you need to keep the API key secret.
Important note
When you generate the API key, it is displayed only once. You need to note down the key. If you lose it, there is no way to get it back. In such cases, you would need to delete the API key and generate a new one.
Using the API key is very simple. During RPC calls, you just need to use the API key instead of the user password. You will be able to call RPC even if 2FA is activated.
第二十一章:性能优化
在 Odoo 框架的帮助下,你可以开发大型且复杂的应用程序。良好的性能是任何项目成功的关键。在本章中,我们将探讨你需要用于优化性能的模式和工具。你还将了解用于找到性能问题根本原因的调试技术。
在本章中,我们将涵盖以下食谱:
-
记录集的预取模式
-
内存缓存 –
ormcache -
生成不同大小的图像
-
访问分组数据
-
创建或写入多个记录
-
通过数据库查询访问记录
-
分析 Python 代码
记录集的预取模式
当你从记录集中访问数据时,它会在数据库中执行一个查询。如果你有一个包含多个记录的记录集,对其上的记录进行检索可能会因为多个 SQL 查询而使系统变慢。在本食谱中,我们将探讨如何使用预取模式来解决这个问题。通过遵循预取模式,你可以减少所需的查询数量,这将提高性能并使你的系统更快。
如何做到这一点...
看看下面的代码;这是一个正常的compute方法。在这个方法中,self是一个包含多个记录的记录集。当你直接在记录集上迭代时,预取将完美地工作:
# Correct prefetching
def compute_method(self):
for rec in self:
print(rec.name)
然而,在某些情况下,预取变得更为复杂,例如使用browse方法获取数据。在下面的示例中,我们在for循环中逐个浏览记录。这将无法有效地使用预取,并且将执行比通常更多的查询:
# Incorrect prefetching
def some_action(self):
record_ids = []
self.env.cr.execute("some query to fetch record id")
for rec in self.env.cr.fetchall():
record = self.env['res.partner'].browse(rec[0])
print(record.name)
通过向browse方法传递一个 ID 列表,你可以创建一个包含多个记录的记录集。如果你对这个记录集执行操作,预取将完美地工作:
# Correct prefetching
def some_action(self):
record_ids = []
self.env.cr.execute("some query to fetch record id")
record_ids = [ rec[0] for rec in self.env.cr.fetchall() ]
recordset = self.env['res.partner'].browse(record_ids)
for record_id in recordset:
print(record.name)
这样,你将不会失去预取功能,并且数据将通过单个 SQL 查询进行获取。
它是如何工作的...
当你与多个记录集一起工作时,预取可以帮助减少 SQL 查询的数量。它是通过一次性获取所有数据来实现的。通常,在 Odoo 中预取是自动工作的,但在某些情况下,例如当你分割记录时,你会失去这个功能,如下面的示例所示:
recs = [r for r in recordset r.id not in [1,2,4,10]]
上述代码会将记录集分割成部分,因此你无法利用预取。
正确使用预取可以显著提高对象关系映射(ORM)的性能。让我们探索预取在底层是如何工作的。
当你通过for循环迭代记录集并访问第一次迭代的字段值时,预取过程开始发挥作用。预取不会为迭代中的当前记录获取数据,而是会获取所有记录的数据。背后的逻辑是,如果你在for循环中访问一个字段,你很可能还会在迭代中的下一个记录中获取该数据。在for循环的第一次迭代中,预取将获取所有记录集的数据并将其保存在缓存中。在for循环的下一个迭代中,数据将从这个缓存中提供,而不是执行新的 SQL 查询。这将把查询次数从O(n)减少到O(1)。
假设记录集有 10 条记录。当你处于第一个循环并访问记录的name字段时,它将获取所有 10 条记录的数据。这不仅适用于name字段;它还将获取这 10 条记录的所有字段。在随后的for循环迭代中,数据将从缓存中提供。这将把查询次数从 10 次减少到 1 次:
for record in recordset: # recordset with 10 records
record.name # Prefetch data of all 10 records in the first loop
record.email # data of email will be served from the cache.
注意,预取将获取所有字段的价值(除了*2many字段),即使这些字段在for循环的主体中没有被使用。这是因为额外的列与每个列的额外查询相比,对性能的影响较小。
注意
有时,预取字段可能会降低性能。在这些情况下,你可以通过将False传递给prefetch_fields上下文来禁用预取,如下所示:recordset.with_context(prefetch_fields=False)。
预取机制使用环境缓存来存储和检索记录值。这意味着一旦记录从数据库中获取,后续对字段的调用都将从环境缓存中提供。你可以使用env.cache属性来访问环境缓存。要使缓存失效,你可以使用环境的invalidate_cache()方法。
还有更多...
如果你拆分记录集,ORM 将生成一个新的记录集,并带有新的预取上下文。对这样的记录集执行操作将只预取相应记录的数据。如果你想在预取后预取所有记录,可以通过将预取记录 ID 传递给with_prefetch()方法来实现。在下面的示例中,我们将记录集拆分为两部分。在这里,我们在两个记录集中传递了共同的预取上下文,所以当你从其中一个中获取数据时,ORM 将获取另一个的数据并将其保存在缓存中以供将来使用:
recordset = ... # assume recordset has 10 records.
recordset1 = recordset[:5].with_prefetch(recordset._ids)
recordset2 = recordset[5:].with_prefetch(recordset._ids)
self.env.cr.execute("select id from sale_order limit 10")
record_ids = [rec[0] for rec in self.env.cr.fetchall()]
recordset = self.env['sale.order'].browse(record_ids)
recordset1 = recordset[:5]
for rec in recordset1:
print(rec.name) # Prefetch name of all 5 records in the first loop
print(rec.attention) # Prefetch attention of all 5 records in the first loop
recordset2 = recordset[5:].with_prefetch(recordset._ids)
for rec in recordset1:
print(rec.name) # Prefetch name of all 10 records in the first loop
print(rec.attention) # Prefetch attention of all 10 records in the first loop
预取上下文不仅限于拆分记录集。你还可以使用with_prefetch()方法在多个记录集之间有一个共同的预取上下文。这意味着当你从一个记录中获取数据时,它也会获取其他所有记录集的数据。
内存缓存 – ormcache
Odoo 框架提供了ormcache装饰器来管理内存缓存。在这个菜谱中,我们将探讨如何管理你函数的缓存。
如何做到这一点...
这个 ORM 缓存的类可以在/odoo/tools/cache.py中找到。为了在任何文件中使用这些类,你需要按照以下方式导入它们:
from odoo import tools
导入类后,你可以使用 ORM 缓存装饰器。Odoo 提供了不同类型的内存缓存装饰器。在接下来的小节中,我们将逐一查看这些装饰器。
ormcache
这是最简单也是最常用的缓存装饰器。你需要传递一个参数名称,该参数名称决定了方法的输出。以下是一个带有ormcache装饰器的方法示例:
@tools.ormcache('mode')
def fetch_mode_data(self, mode):
# some calculations
return result
当你第一次调用这个方法时,它将被执行,并返回结果。ormcache将根据mode参数的值存储这个结果。当你再次以相同的mode值调用该方法时,结果将从缓存中提供,而无需实际执行该方法。
有时,你的方法的结果依赖于环境属性。在这些情况下,你可以这样声明方法:
@tools.ormcache('self.env.uid', 'mode')
def fetch_data(self, mode):
# some calculations
return result
在这个例子中给出的方法将根据环境用户和mode参数的值来存储缓存。
ormcache_context
这个缓存的工作方式与ormcache类似,但它在参数的基础上还依赖于上下文中的值。在这个缓存的装饰器中,你需要传递参数名称和上下文键的列表。例如,如果你的方法输出依赖于上下文中的lang和website_id键,你可以使用ormcache_context:
@tools.ormcache_context('mode', keys=('website_id','lang'))
def fetch_data(self, mode):
# some calculations
return result
在前面的例子中,这个缓存将依赖于mode参数和context的值。
ormcache_multi
一些方法对多个记录或 ID 执行操作。如果你想给这类方法添加缓存,你可以使用ormcache_multi装饰器。你需要传递multi参数,在方法调用期间,ORM 将通过迭代这个参数来生成缓存键。在这个方法中,你需要以字典格式返回结果,其中multi参数的一个元素作为键。看看以下示例:
@tools.ormcache_multi('mode', multi='ids')
def fetch_data(self, mode, ids):
result = {}
for i in ids:
data = ... # some calculation based on ids
result[i] = data
return result
假设我们用[1,2,3]作为 ID 调用前面的方法。该方法将以{1:... , 2:..., 3:... }的格式返回一个结果。ORM 将根据这些键缓存结果。如果你再次调用该方法,并使用[1,2,3,4,5]作为 ID,你的方法将接收到[4, 5]作为ID参数,因此该方法将执行4和5 ID 的操作,其余的结果将从缓存中提供。
它是如何工作的...
ORM 缓存以字典格式(缓存查找)保持缓存。此缓存的关键字将基于装饰方法的签名生成,值将是结果。简单来说,当你用x, y参数调用方法,且方法的结果是x+y时,缓存查找将是{(x, y): x+y}。这意味着下次你用相同的参数调用此方法时,结果将直接从缓存中提供。这节省了计算时间并使响应更快。
ORM 缓存是内存缓存,因此它存储在 RAM 中并占用内存。不要使用ormcache来服务大型数据,如图片或文件。
警告
使用此装饰器的方法永远不应该返回一个记录集。如果它们这样做,将生成psycopg2.OperationalError,因为记录集的底层游标已关闭。
你应该在纯函数上使用 ORM 缓存。纯函数是一个总是对相同的参数返回相同结果的方法。这些方法的输出只依赖于参数,因此它们返回相同的结果。如果情况不是这样,当你执行使缓存状态无效的操作时,你需要手动清除缓存。要清除缓存,请调用clear_caches()方法:
self.env[model_name].clear_caches()
清除缓存后,下一次调用该方法将执行该方法并将结果存储在缓存中,所有后续具有相同参数的方法调用都将从缓存中提供服务。
还有更多...
ORM 缓存是发送给 Odoo 进程的SIGUSR1信号:
kill -SIGUSR1 <pid>
kill -SIGUSR1 496
在这里,496是进程 ID。执行命令后,你将在日志中看到 ORM 缓存的状况:
> 2023-10-18 09:22:49,350 496 INFO odoo-book-17.0 odoo.tools.cache: 1 entries, 31 hit, 1 miss, 0 err, 96.9% ratio, for ir.actions.act_window._existing
> 2023-10-18 09:22:49,350 496 INFO odoo-book-17.0 odoo.tools.cache: 1 entries, 1 hit, 1 miss, 0 err, 50.0% ratio, for ir.actions.actions.get_bindings
> 2023-10-18 09:22:49,350 496 INFO odoo-book-17.0 odoo.tools.cache: 4 entries, 1 hit, 9 miss, 0 err, 10.0% ratio, for ir.config_parameter._get_param
缓存中的百分比是命中-未命中比率。它是结果在缓存中找到的成功比率。如果缓存的命中-未命中比率太低,你应该从方法中移除 ORM 缓存。
生成不同尺寸的图片
大图片对任何网站都可能造成麻烦。它们增加了网页的大小,从而使得网页加载速度变慢。这会导致 SEO 排名下降和访客流失。在这个菜谱中,我们将探讨如何创建不同尺寸的图片;通过使用正确的图片,你可以减小网页大小并提高页面加载时间。
如何做...
你需要在你的模型中继承image.mixin。以下是向你的模型添加image.mixin的方法:
class HostelStudent(models.Model):
_name = "hostel.student"
_description = "Hostel Student Information"
_inherit = ["image.mixin"]
混合模型将自动为宿舍学生模型添加五个新字段以存储不同尺寸的图片。请参阅如何工作…部分了解所有五个字段。
如何工作...
image.mixin实例将自动为模型添加五个新的二进制字段。每个字段存储具有不同分辨率的图片。以下是字段及其分辨率的列表:
-
image_1920: 1,920x1,920 -
image_1024: 1,024x1,024 -
image_512: 512x1,512 -
image_256: 256x256 -
image_128: 128x128
在这里给出的所有字段中,只有 image_1920 是可编辑的。其他图像字段是只读的,并在您更改 image_1920 字段时自动更新。因此,在您模型的表单视图后端,您需要使用 image_1920 字段以允许用户上传图像。然而,这样做会在表单视图中加载大型的 image_1920 图像。但是,有一种方法可以通过在表单视图中使用 image_1920 图像来提高性能,同时显示较小的图像。例如,我们可以利用 image_1920 字段,但显示 image_128 字段。为此,您可以使用以下语法:
<field name="image_1920" widget="image"
options="{'preview_image': 'image_128'}" />
一旦您将图像保存到字段中,Odoo 将自动调整图像大小并将其存储在相应的字段中。表单视图将显示转换后的 image_128,因为我们将其用作 preview_image。
注意
image.mixin 模型是 AbstractModel,因此它的表不在数据库中。您需要在您的模型中继承它才能使用它。
使用此 image.mixin,您可以存储最大分辨率为 1,920x1,920 的图像。如果您保存的图像分辨率高于 1,920x1,920,Odoo 将将其减少到 1,920x1,920。在此过程中,Odoo 还将保留图像的分辨率,避免任何扭曲。例如,如果您上传分辨率为 2,400x1,600 的图像,image_1920 字段将具有 1,920x1,280 的分辨率。
还有更多...
使用 image.mixin,您可以获取具有特定分辨率的图像,但您想使用具有其他分辨率的图像怎么办?为此,您可以使用二进制包装器字段图像,如下例所示:
image_1500 = fields.Image("Image 1500", max_width=1500, max_height=1500)
这将创建一个新的 image_1500 字段,存储图像时将调整其分辨率为 1,500x1,500。请注意,这并不是 image.mixin 的一部分。它只是将图像减少到 1,500x1,500,因此您需要在表单视图中添加此字段;编辑它不会更改 image.mixin 中的其他图像字段。如果您想将其与现有的 image.mixin 字段链接,请将 related="image_1920" 属性添加到字段定义中。
访问分组数据
当您需要统计数据时,您通常需要以分组的形式获取数据,例如月度销售报告,或按客户显示销售情况的报告。手动搜索记录并分组是耗时的工作。在这个菜谱中,我们将探讨您如何使用 read_group() 方法来访问分组数据。
如何做到这一点...
执行以下步骤。
注意
read_group() 方法在统计和智能统计按钮中广泛使用。
-
假设您想在合作伙伴表单上显示销售订单的数量。这可以通过搜索客户的销售订单然后计数长度来完成:
# in res.partner model so_count = fields.Integer(compute='_compute_so_count', string='Sale order count') def _compute_so_count(self): sale_orders = self.env['sale.order'].search(domain=[('partner_id', 'in', self.ids)]) for partner in self: partner.so_count = len(sale_orders.filtered(lambda so: so.partner_id.id == partner.id))之前的示例将工作,但不是最优的。当您在树视图中显示
so_count字段时,它将获取并过滤列表中所有合作伙伴的销售订单。对于这么少量的数据,read_group()方法不会带来太大的差异,但随着数据量的增长,可能会成为问题。为了解决这个问题,您可以使用read_group方法。 -
以下示例将执行与上一个示例相同的功能,但对于大量数据集,它只消耗一个 SQL 查询:
# in res.partner model so_count = fields.Integer(compute='_compute_so_count', string='Sale order count') def _compute_so_count(self): sale_data = self.env['sale.order'].read_group( domain=[('partner_id', 'in', self.ids)], fields=['partner_id'], groupby=['partner_id']) mapped_data = dict([(m['partner_id'][0], m['partner_id_count']) for m in sale_data]) for partner in self: partner.so_count = mapped_data[partner.id]
之前的代码片段已经优化,因为它直接通过 SQL 的 GROUP BY 功能获取销售订单的数量。
它是如何工作的...
read_group() 方法在内部使用 SQL 的 GROUP BY 功能。这使得 read_group 方法即使在拥有大量数据集的情况下也能更快地执行。内部,Odoo 网页客户端在图表和分组树视图中使用此方法。您可以通过使用不同的参数来调整 read_group 方法的行为。
让我们探索 read_group 方法的签名:
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
可用于 read_group 方法的不同参数如下:
-
domain:此参数用于过滤记录。这将作为read_group方法的搜索条件。 -
fields:这是一个要分组获取的字段列表。请注意,这里提到的字段应包含在groupby参数中,除非您使用某些聚合函数。read_group方法支持 SQL 聚合函数。假设您想获取每个客户的平均订单金额。如果是这样,您可以使用以下read_group:self.env['sale.order'].read_group([], ['partner_id', 'amount_total:avg'], ['partner_id'])如果您想访问同一字段两次但使用不同的聚合函数,语法略有不同。您需要将字段名作为
alias:agg(field_name)传递。此示例将为您提供每个客户的订单总数和平均数:self.env['sale.order'].read_group([], ['partner_id', 'total:sum(amount_total)', 'avg_total:avg(amount_total)'], ['partner_id']) -
groupby:此参数将是一个字段列表,记录将根据这些字段进行分组。它允许您根据多个字段对记录进行分组。为此,您需要传递一个字段列表。例如,如果您想按客户和订单状态对销售订单进行分组,您可以在该参数中传递['partner_id', 'state']。 -
offset:此参数用于分页。如果您想跳过一些记录,可以使用此参数。 -
limit:此参数用于分页;它表示要获取的最大记录数。 -
lazy:此参数接受布尔值。默认情况下,其值为True。如果此参数为True,则结果仅按groupby参数中的第一个字段进行分组。您将在结果的__context和__domain键中获取剩余的groupby参数和域。如果此参数的值设置为False,则将按groupby参数中的所有字段对数据进行分组。
还有更多...
按日期字段分组可能很复杂,因为可以根据天、周、季度、月或年对记录进行分组。你可以通过在 groupby 参数中的 : 后传递 groupby_function 来更改日期字段的分组行为。如果你想按月分组销售订单的总数,你可以使用 read_group 方法:
self.env['sale.order'].read_group([], ['total:sum(amount_total)'], ['order_date:month'])
日期分组的可能选项有 day、week、month、quarter 和 year。
参考以下内容
如果你想了解更多关于 PostgreSQL 聚合函数的信息,请参阅文档:www.postgresql.org/docs/current/functions-aggregate.html。
创建或写入多个记录
如果你刚开始接触 Odoo 开发,你可能会执行多个查询来写入或创建多个记录。在本配方中,我们将探讨如何批量创建和写入记录。
如何操作...
在底层,创建多个记录和在多个记录上写入操作的工作方式不同。让我们逐一查看这些记录。
创建多个记录
Odoo 支持批量创建记录。如果你正在创建单个记录,只需传递一个包含字段值的字典。要批量创建记录,你只需传递这些字典的列表而不是单个字典。以下示例在单个 create 调用中创建三个房间记录:
vals = [{
'name': "Room A-101",
'room_no': 101,
'floor_no': 1,
'student_per_room': 2,
}, {
'name': "Room A-102",
'room_no': 102,
'floor_no': 1,
'student_per_room': 3,
}, {
'name': "Room B-201",
'room_no': 201,
'floor_no': 2,
'student_per_room': 3,
}]
self.env['hostel.room'].create(vals)
此代码片段将创建三本新书的记录。
在多个记录上写入
当与多个版本的 Odoo 一起工作时,了解写入方法的行为方式很重要。在这种情况下,它采用延迟更新的方法,这意味着它不会立即将数据写入数据库。相反,Odoo 只在必要时或当调用 flush() 方法时将数据写入数据库。
这里是 write 方法的两个示例:
# Example 1
data = {...}
for record in recordset:
record.write(data)
# Example 2
data = {...}
recordset.write(data)
如果你使用 Odoo v13 或更高版本,那么将不会有任何关于性能的问题。然而,如果你使用较旧版本,第二个示例将比第一个示例快得多,因为第一个示例将在每个迭代中执行一个 SQL 查询。
它是如何工作的...
为了批量创建多个记录,你需要以列表的形式传递包含值的字典来创建新记录。这将自动管理批量创建记录。当你批量创建记录时,内部操作将为每个记录插入一个查询。这意味着批量创建记录不是在一个查询中完成的。然而,这并不意味着批量创建记录不会提高性能。性能的提升是通过批量计算计算字段实现的。
对于 write 方法,操作方式不同。大多数事情都由框架自动处理。例如,如果你在所有记录上写入相同的数据,数据库将只通过一个 UPDATE 查询进行更新。如果框架在同一个事务中反复更新相同的记录,它也会处理这种情况,如下所示:
recordset.name= 'Admin'
recordset.email= 'admin@example.com'
recordset.name= 'Administrator'
recordset.email= 'admin-2@example.com'
在之前的代码片段中,对于write操作,只会执行一个查询,最终name=Administrator和email=admin-2@example.com的值。这不会对性能产生负面影响,因为分配的值在缓存中,并且稍后在一个查询中写入。
如果你在这之间使用flush()方法,情况会有所不同,如下面的示例所示:
recordset.name= 'Admin'
recordset.email= 'admin@example.com'
recordset.flush()
recordset.name= 'Administrator'
recordset.email= 'admin-2@example.com'
flush()方法将缓存中的值更新到数据库。因此,在前面的例子中,将执行两个UPDATE查询——一个是在刷新之前的数据,另一个是在刷新之后的数据。
还有更多...
如果你使用的是较旧版本,那么写入单个值将立即执行UPDATE查询。请查看以下示例以探索旧版 Odoo 中write操作的正确用法:
# incorrect usage
recordset.name= 'Admin'
recordset.email= 'admin@example.com'
# correct usage
recordset.write({'name': 'Admin', 'email'= 'admin@example.com'})
在这里,在第一个例子中,我们有两次UPDATE查询,而第二个例子将只执行一次UPDATE查询。
通过数据库查询访问记录
Odoo ORM 方法有限,有时从 ORM 中获取某些数据可能很困难。在这些情况下,你可以以所需格式获取数据,并需要对数据进行操作以获得特定结果。因此,这会变慢。为了处理这些特殊情况,你可以在数据库中执行 SQL 查询。在本食谱中,我们将探讨如何从 Odoo 运行 SQL 查询。
如何做到这一点...
你可以使用self._cr.execute方法执行数据库查询:
-
添加以下代码:
self.flush() self._cr.execute("SELECT id, name, room_no, floor_no FROM hostel_room WHERE name ilike %s", ('%Room A-%',)) data = self._cr.fetchall() print(data)这是输出:
dictfetchall() method. Take a look at the following example:self.flush()
self._cr.execute("SELECT id, name, room_no, floor_no FROM hostel_room WHERE name ilike %s", ('%Room A-%',))
data = self._cr.dictfetchall()
print(data)
Here is the output:[{'id': 4, 'name': 'Room A-101', 'room_no': 101, 'floor_no': 1}, {'id': 5, 'name': 'Room A-103', 'room_no': 103, 'floor_no': 1}, {'id': 6, 'name': 'Room A-201', 'room_no': 201, 'floor_no': 2}]
如果你只想获取单个记录,可以使用fetchone()和dictfetchone()方法。这些方法与fetchall()和dictfetchall()类似,但它们只返回单个记录,如果你想获取多个记录,需要多次调用fetchone()和dictfetchone()方法。
它是如何工作的...
有两种方式从记录集访问数据库游标——一种是从记录集本身,例如self._cr,另一种是从环境(特别是self.env.cr)。这个游标用于执行数据库查询。在前面的例子中,我们看到了如何通过原始查询获取数据。表名是模型名称,将.替换为_后的名称,因此hostel.room模型变为hostel_room。
注意,我们在执行查询之前使用了self.flush()。这样做的原因是 Odoo 过度使用缓存,数据库可能没有正确的值。self.flush()会将所有延迟的更新推送到数据库,并执行所有相关的计算,然后你将从数据库中获得正确的值。flush()方法还支持一些参数,可以帮助你控制要刷新到数据库中的内容。参数如下:
-
fname参数需要一个你想要刷新到数据库的字段列表 -
records参数需要一个记录集,如果你只想刷新某些记录,则使用它
如果你正在执行INSERT或UPDATE查询,在执行查询后也需要执行flush(),因为 ORM 可能不知道你做出的更改,并且它可能已经缓存了记录。
在执行原始查询之前,你需要考虑一些事情。只有在你没有其他选择时才使用原始查询。通过执行原始查询,你绕过了 ORM 层。因此,你也绕过了安全规则和 ORM 的性能优势。有时,错误构建的查询可能会引入 SQL 注入漏洞。考虑以下示例,其中的查询可能会允许攻击者执行 SQL 注入:
# very bad, SQL injection possible
self.env.cr.execute('SELECT id, name FROM hostel_room WHERE name ilike + search_keyword + ';')
# good
self.env.cr.execute('SELECT id, name FROM hostel_room WHERE name ilike %s ';', (search_keyword,))
也不要使用字符串格式化函数;它也会允许攻击者执行 SQL 注入。使用 SQL 查询会使你的代码对其他开发者来说更难阅读和理解,所以尽可能避免使用它们。
信息
许多 Odoo 开发者认为执行 SQL 查询可以使操作更快,因为它绕过了 ORM 层。然而,这并不完全正确;它取决于用例。在大多数操作中,ORM 的性能和速度都比RAW查询要好,因为数据是从记录集缓存中提供的。
还有更多...
在一个事务中进行的操作只在事务结束时提交。如果在 ORM 中发生错误,事务将被回滚。如果你已经执行了INSERT或UPDATE查询并希望使其永久,可以使用self._cr.commit()来提交更改。
注意
注意,使用commit()可能很危险,因为它可能会使记录处于不一致的状态。ORM 中的错误可能导致不完整的回滚,所以只有在你完全确定你在做什么的情况下才使用commit()。
如果你使用commit()方法,那么之后就没有必要使用flush()了。commit()方法会在内部刷新环境。
性能分析
有时,你将无法确定问题的原因。这尤其适用于性能问题。Odoo 提供了一些内置的性能分析工具,可以帮助你找到问题的真正原因。
性能分析是关于分析程序执行并测量汇总数据的过程。这些数据可以是每个函数的执行时间、执行的 SQL 查询等等。
虽然性能分析本身并不能提高程序的性能,但它可以非常有帮助,在寻找性能问题和确定程序中哪些部分负责这些问题时。
在 Odoo 中进行代码性能分析可以帮助您识别性能问题并优化您的代码。这是一种分析代码执行时间、程序复杂性和应用程序内存使用的技术。
通过在 Odoo 中使用性能分析技术,您可以提高应用程序的整体性能和用户体验,使其更快、更高效。
启用性能分析器
性能分析器可以通过用户界面启用,这是最简单的方法,但只能用于分析 Web 请求,或者从 Python 代码中启用:
-
启用开发者模式。
-
性能分析器必须在数据库上全局启用。这可以通过两种方式完成:
- 打开开发者模式工具,然后切换启用性能分析按钮。向导建议一组性能分析的有效期时间。单击启用性能分析以全局启用性能分析器。

图 21.1 – 启用性能分析

图 21.2 – 禁用性能分析
- 前往设置 | 常规设置 | 性能,并设置启用性能分析字段所需的时间。
分析结果
要浏览性能分析结果,请确保性能分析器在数据库上全局启用,然后打开开发者模式工具,并单击性能分析部分右上角的按钮。将打开按性能分析会话分组的ir.profile记录的列表视图。

每条记录都有一个可点击的链接,该链接在新标签页中打开速度分析结果。

Speedscope 超出了本文档的范围,但有很多工具可以尝试 – 搜索、突出显示类似帧、放大帧、时间线、左侧重、三明治视图等等。
根据激活的配置选项,Odoo 会生成不同的视图模式,您可以从顶部菜单访问它们。


-
组合:组合视图显示所有已整合的 SQL 查询和跟踪。
-
无上下文组合:无上下文组合视图产生相同的结果,但忽略了存储的执行上下文、性能/分析/启用>。
-
sql (无间隔):sql (无间隔)视图显示所有 SQL 查询,就像它们是顺序执行的一样,没有任何 Python 逻辑。这仅对 SQL 优化有益。
-
sql (密度):sql (密度)视图中仅显示 SQL 查询,它们之间有空格。这可以帮助您发现可以批量处理的多个小查询的区域,并确定问题是否出在 Python 或 SQL 代码上。
-
帧:帧视图中仅显示周期性收集器的结果。
注意
尽管分析器设计轻量级,但它仍然可能影响性能,尤其是在使用Sync收集器时。记住这一点,当你检查速度范围数据时。
收集器
每个收集器都有独特的格式和方法来收集分析数据。通过开发模式工具中的特定切换按钮,或使用 Python 代码通过它们的键或类,每个都可以从用户界面独立启用。
目前 Odoo 中有四个收集器可用:
-
SQLCollector -
PeriodicCollector -
QwebCollector -
SyncCollector
SQL 收集器
当前线程(对于所有游标)对数据库进行的所有 SQL 查询都由SQL收集器保存,包括堆栈跟踪。在大量小查询上使用收集器可能会影响执行时间和其他分析器,因为收集器的开销被添加到每个查询检查的线程上。
在组合速度范围视图中调试查询计数并向Periodic收集器添加数据是两个特别有用的用途:
class SQLCollector(Collector):
"""
Saves all executed queries in the current thread with the call stack.
"""
name = 'sql'
周期性收集器
此收集器在单独的线程中运行,并在每个间隔保存分析线程的堆栈跟踪。间隔(默认为 10 毫秒)可以通过用户界面中的间隔选项或 Python 代码中的间隔参数定义。
注意
如果间隔设置得非常低,在分析长时间查询时会出现内存问题。如果间隔设置得非常高,它将失去对短暂函数执行的详细信息。
由于它具有独特的线程,它应该对执行时间的影响相对较小,这使得它成为评估性能的最佳方式之一:
class PeriodicCollector(Collector):
"""
Record execution frames asynchronously at most every `interval` seconds.
:param interval (float): time to wait in seconds between two samples.
"""
name = 'traces_async'
Qweb 收集器
该收集器减少了每个指令的 Python 执行时间和查询。在执行大量小指令时,使用 SQL 收集器可能会产生显著的开销。就收集的数据而言,结果与其他收集器不同,可以使用自定义小部件从ir.profile表单视图检查它们。
在尝试最大化观看次数时,这非常有帮助:
class QwebCollector(Collector):
"""
Record qweb execution with directive trace.
"""
name = 'qweb'
同步收集器
由于这个收集器在单个线程上操作并为每个函数调用和返回保存堆栈,因此它对性能的影响很大。
调试和理解复杂的流程,以及跟踪它们在代码中的执行情况,可能会有所帮助。然而,由于显著的开销,不建议使用性能分析:
class SyncCollector(Collector):
"""
Record complete execution synchronously.
Note that --limit-memory-hard may need to be increased when launching Odoo.
"""
name = 'traces_sync'
性能陷阱
-
注意随机性。多次执行可能会导致不同的结果——例如,在执行过程中触发垃圾回收器。
-
注意阻塞调用。在某些情况下,外部的
c_call可能需要一些时间才能释放 GIL,从而导致周期性收集器出现意外的长帧。分析器应该检测到这一点并给出警告。如果需要,可以在这样的调用之前手动触发分析器。 -
注意缓存。在视图/资源/...进入缓存之前进行分析可能会导致不同的结果。
-
注意分析器的开销。当执行许多小查询时,SQL 收集器的开销可能很重要。分析是发现问题的实用方法,但你可能希望禁用分析器来测量代码更改的实际影响。
-
分析结果可能占用大量内存。在某些情况下(例如,分析安装或长时间请求),你可能会达到内存限制,尤其是在渲染 speedscope 结果时,这可能导致 HTTP 500 错误。在这种情况下,你可能需要以更高的内存限制启动服务器 -
--limit-memory-hard $((8*1024**3)).
第二十二章:销售点
销售点(Point of Sale)是一个完全集成的应用程序,它允许您使用任何设备在线或离线销售产品。它还会自动记录您库存中的产品移动,提供实时统计数据,并跨所有商店进行合并。在本章中,我们将了解如何修改销售点应用程序。
在本章中,我们将涵盖以下主题:
-
添加自定义 JavaScript/SCSS 文件
-
向键盘添加动作按钮
-
执行 RPC 调用
-
修改销售点屏幕 UI
-
修改现有业务逻辑
-
修改客户
注意
销售点应用程序主要使用 JavaScript 编写。本章假设您具备基本的 JavaScript 知识。本章还使用了 OWL 框架,如果您不熟悉这些 JavaScript 术语,请参阅第十六章,Odoo Web 库(OWL)。
在本章中,我们将使用一个名为point_of_sale_customization的附加模块。由于我们将在销售点应用程序中进行定制,因此point_of_sale_customization模块将依赖于point_of_sale模块。为了快速开始,我们已经准备了一个初始的point_of_sale_customization模块,您可以从本书的 GitHub 仓库中的Chapter22/00_initial_module/point_of_sale_customization目录中获取它。
技术要求
本章中使用的所有代码都可以从以下 GitHub 仓库下载:github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter22。
添加自定义 JavaScript/SCSS 文件
销售点应用程序使用不同的资产包来管理 JavaScript 和样式表文件。在本食谱中,我们将学习如何将SCSS和JavaScript文件添加到销售点资产包中。
准备工作
首先,我们将加载一个 SCSS 样式表和一个 JavaScript 文件到销售点应用程序中。
如何做到这一点...
要将资产加载到销售点应用程序中,请按照以下步骤操作:
-
在
point_of_sale_customization/static/src/scss/point_of_sale_customization.scss中添加一个新的 SCSS 文件,并插入以下代码:.pos .pos-content { .price-tag { background: #00abcd; width: 100%; right: 0; left: 0; top:0; } } -
在
point_of_sale_customization/static/src/js/point_of_sale_customization.js中添加一个 JavaScript 文件,并添加以下内容:/** @odoo-module */ console.log("Point Of Sale Javascript Loaded"); -
在
point_of_sale assets中注册这些 JavaScript 和 SCSS 文件。'assets': { 'point_of_sale._assets_pos': [ 'point_of_sale_customization/static/src/scss/point_of_sale_customization.scss', 'point_of_sale_customization/static/src/js/point_of_sale_customization.js' ], }, -
安装
point_of_sale_customization模块。

图 22.1 – 安装 POS 定制模块
要查看您的更改效果,请从销售点 | 仪表板菜单启动新会话。
它是如何工作的…
到目前为止,我们已经将一个 JavaScript 文件和一个 SCSS 文件加载到销售点应用程序中。
在步骤 1中,我们更改了产品卡片定价标签的背景颜色。安装point_of_sale_customization模块后,您将能够看到定价标签的变化:

图 22.2 – 更新的价格标签
在步骤 2中,我们添加了 JavaScript 文件。在其中,我们向控制台添加了日志。为了看到消息,您需要打开浏览器开发者工具。在控制台标签页中,您将看到以下日志。这表明您的 JavaScript 文件已成功加载。目前,我们只向 JavaScript 文件中添加了日志,但在未来的菜谱中,我们将添加更多内容:

图 22.3 – JavaScript 已加载(控制台中的日志)
在步骤 3中,我们添加了 JavaScript 文件和 SCSS 文件,如下所示:
'assets': {
'point_of_sale._assets_pos': [
'js,scss path'
],
}
更多内容...
Odoo 还为餐厅提供了销售点解决方案的附加模块。请注意,这个销售点餐厅模块只是销售点应用程序的扩展。如果您想在餐厅模块中进行定制,您需要将您的 JavaScript 和 SCSS 文件添加到相同的point_of_sale._assets_pos资产包中。
添加操作按钮到键盘
如前所述,销售点应用程序的设计使其能够离线工作。正因为如此,销售点应用程序的代码结构与剩余的 Odoo 应用程序不同。销售点应用程序的代码库主要使用 JavaScript 编写,并为定制提供了不同的实用工具。在此阶段,我们将使用这样一个实用工具,并在键盘面板顶部创建一个操作按钮。
准备工作
在这里,我们将使用在添加自定义 JavaScript/SCSS 文件菜谱中创建的point_of_sale_customization模块。我们将在键盘面板顶部添加一个按钮。此按钮将是一个将折扣应用于订单行的快捷方式。
如何操作...
按照以下步骤将 5%折扣操作按钮添加到销售点应用程序的键盘面板:
-
将以下代码添加到
/static/src/js/point_of_sale_customization.js文件中,这将定义操作按钮:/** @odoo-module */ import { Component } from "@odoo/owl"; import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen"; import { usePos } from "@point_of_sale/app/store/pos_hook"; export class PosDiscountButton extends Component { static template = "PosDiscountButton"; setup() { this.pos = usePos(); } async onClick() { const order = this.pos.get_order(); if (order.selected_orderline) { order.selected_orderline.set_discount(5); } } } ProductScreen.addControlButton({ component: PosDiscountButton, condition: function () { return true; } }); -
将按钮的 QWeb 模板添加到
/static/src/xml/point_of_sale_customization.xml文件中:<?xml version="1.0" encoding="UTF-8"?> <templates id="template" xml:space="preserve"> <t t-name="PosDiscountButton"> <span class="control-button btn btn-light rounded-0 fw-bolder" t-on-click="() => this.onClick()"> <i class="fa fa-gift"></i> <span>5%</span> <span>Discount</span> </span> </t> </templates> -
在
point_of_sale_customization/static/src/scss/point_of_sale_customization.scss中添加一个新的 SCSS 文件,并插入以下代码:.pos .pos-content { .price-tag { background: #00abcd; width: 100%; right: 0; left: 0; top:0; } } -
按照以下方式在清单文件中注册 QWeb 模板:
'assets': { 'point_of_sale._assets_pos': [ 'point_of_sale_customization/static/src/scss/point_of_sale_customization.scss', 'point_of_sale_customization/static/src/xml/point_of_sale_customization.xml', 'point_of_sale_customization/static/src/js/point_of_sale_customization.js' ], }, -
更新
point_of_sale_customization模块以应用更改。之后,您将能够在计算器上方看到5%折扣按钮:

图 22.4 – 折扣按钮
点击此按钮后,折扣将应用于所选订单行。
它是如何工作的...
在 Odoo v17 中,基于 Odoo 销售点应用程序的代码完全使用 OWL 框架重写。您可以在第十六章,Odoo Web 库(OWL)中了解更多关于 OWL 框架的信息。
要在销售点应用程序中创建动作按钮,您需要扩展Component。现在,Component在@odoo/owl命名空间中定义,因此要在您的代码中使用它,您需要导入它。
在步骤 1中,我们从@odoo/owl中导入了Component。然后,我们通过扩展Component创建了PosDiscountButton。在步骤 1中,我们还从@point_of_sale/app/screens/product_screen/product_screen导入了ProductScreen,以及从@point_of_sale/app/store/pos_hook导入了usePos。
现在,ProductScreen通过addControlButton方法被用来在销售点屏幕上添加一个按钮。
Component提供了一些内置工具,可以访问有用的信息,例如订单详情和销售点配置。您可以通过this.pos = usePos()变量来访问它。
在我们的示例中,我们通过this.pos.get_order()方法访问了当前订单信息。然后,我们使用set_discount()方法设置了 5%的折扣。
在步骤 2和步骤 3中,我们添加了 OWL 模板,该模板将在销售点键盘上渲染。如果您想了解更多信息,请参阅第十六章,Odoo Web 库(OWL)。
还有更多...
addControlButton()方法支持一个额外的参数,即condition。此参数用于根据某些条件隐藏/显示按钮。此参数的值是一个返回布尔值的函数。根据返回值,销售点系统将隐藏或显示按钮。
以下是一个更详细的示例:
ProductScreen.addControlButton({
component: PosDiscountButton,
condition: function () {
return true;
},
});
进行 RPC 调用
虽然销售点应用程序可以离线工作,但仍有可能向服务器发出 RPC 调用。RPC 调用可用于任何操作;您可以使用它进行 CRUD 操作,或在服务器上执行操作。
现在,我们将进行 RPC 调用以获取关于客户最后五笔订单的信息。
准备工作
现在,我们将使用为键盘配方中的添加动作按钮创建的point_of_sale_customization模块。我们将定义动作按钮。当用户点击动作按钮时,我们将发出 RPC 调用以获取订单信息并在弹出窗口中显示它。
如何做到这一点...
按照以下步骤显示所选客户的最后五笔订单:
-
将以下代码添加到
/static/src/js/point_of_sale_customization.js文件中;这将添加一个新动作按钮,当用户点击按钮时,将获取并显示最后五笔订单的信息:/** @odoo-module */ import { Component } from "@odoo/owl"; import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup"; import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen"; import { SelectionPopup } from "@point_of_sale/app/utils/input_popups/selection_popup"; import { usePos } from "@point_of_sale/app/store/pos_hook"; import { useService } from "@web/core/utils/hooks"; import { sprintf } from "@web/core/utils/strings"; export class PosLastOrderButton extends Component { static template = "PosLastOrderButton"; setup() { this.pos = usePos(); this.popup = useService("popup"); } } ProductScreen.addControlButton({ component: PosLastOrderButton, condition: function () { return true; }, }); -
将
onClick函数添加到PosLastOrders组件中,以管理按钮点击:export class PosLastOrderButton extends Component { static template = "PosLastOrderButton"; setup() { this.pos = usePos(); this.popup = useService("popup"); } async onClick() { var self = this; const order = this.pos.get_order(); const client = order.get_partner(); if (client) { var domain = [['partner_id', '=', client.id]]; const orders = await this.pos.orm.call( "pos.order", "search_read", [], { domain: domain, fields: ['name', 'amount_total'], limit:5 } ); if (orders.length > 0) { var order_list = orders.map((o) => { return { 'label': sprintf("%s -TOTAL: %s", o.name, o.amount_total) }; }); await this.popup.add(SelectionPopup, { title: 'Last 5 orders', list: order_list }); } else { await this.popup.add(ErrorPopup, { body: "No previous orders found" }); } } else { await this.popup.add(ErrorPopup, { body: "No previous orders found" }); } } } -
将按钮的 QWeb 模板添加到
/static/src/xml/point_of_sale_customization.xml文件中:<t t-name="PosLastOrderButton"> <span class="control-button btn btn-light rounded-0 fw-bolder" t-on-click="() => this.onClick()"> <i class="fa fa-shopping-cart"></i>Making RPC calls <span></span> <span>Last Orders</span> </span> </t> -
更新
point_of_sale_customization模块以应用更改。之后,你将能够在键盘面板上方看到最后订单按钮。当点击此按钮时,将显示一个包含订单信息的弹出窗口:

图 22.5 – 客户的最后五笔订单
如果没有找到以前的订单,将显示警告信息而不是订单列表。
它是如何工作的...
在步骤 1中,我们创建了操作按钮。如果你想了解更多关于操作按钮的信息,请参阅本章中的将操作按钮添加到键盘配方。
在深入了解技术细节之前,让我们了解我们希望通过此操作按钮实现什么。一旦点击,我们希望显示所选客户的最后五笔订单的信息。将有一些情况是未选择客户,或者客户没有以前的订单。在这种情况下,我们希望显示一个带有适当信息的弹出窗口。RPC 实用工具可通过组件的this.pos.orm.call属性使用。
在步骤 2中,我们添加了点击处理函数。点击操作按钮时,将调用点击处理函数。此函数将调用服务器上的 RPC 以获取订单信息。
我们使用了this.pos.orm.call()方法来进行 RPC 调用。
然后,我们使用了search_read方法通过 RPC 获取数据。我们传递了客户域以过滤订单。我们还传递了limit关键字参数以获取仅五个订单。this.pos.orm.call()是一个异步方法,返回一个Promise对象,因此要处理结果,你可以使用await关键字。
注意
RPC 调用在离线模式下不起作用。如果你有一个良好的互联网连接并且不经常使用离线模式,你可以使用 RPC。
虽然 Odoo 销售点应用程序可以在离线模式下工作,但一些操作,如创建或更新客户,需要互联网连接,因为这些功能使用 RPC 进行内部调用。
我们在弹出窗口中显示了以前的订单信息。我们使用了SelectionPopup,它用于显示可选择的列表;我们用它来显示最后五笔订单。我们还使用了ErrorPopup来在未选择客户或找不到以前的订单时显示警告信息。
在步骤 3中,我们为操作按钮添加了 QWeb 模板。销售点应用程序将渲染此模板以显示操作按钮。
还有更多...
还有许多其他的弹出工具。例如,NumberPopup用于从用户那里获取数字输入。请参阅@point_of_sale/app/utils/input_popups/number_popup目录中的文件,以查看所有这些工具。NumberPopup模块可能是一个自定义组件或用于处理 POS 应用程序中数字输入弹出的实用函数。根据上下文,此模块可能负责以用户友好的方式显示弹出对话框输入数值数据,例如在零售系统中输入数量或价格。使用以下代码打开数字弹出窗口:
import { NumberPopup } from "@point_of_sale/app/utils/input_popups/number_popup";
this.popup.add(NumberPopup, { title: ("Set the new quantity")});
修改销售点屏幕 UI
销售点应用程序的 UI 是用 OWL QWeb 模板编写的。在本教程中,我们将学习如何修改销售点应用程序中的 UI 元素。
准备工作
在这个教程中,我们将使用在制作 RPC 调用教程中创建的point_of_sale_customization模块。我们将修改产品卡片的 UI 并显示每个产品的利润率。
如何操作...
按照以下步骤在产品卡片上显示利润率:
-
将以下代码添加到
/models/pos_session.py文件中,以获取产品的实际价格额外字段:from odoo import models class PosSession(models.Model): _inherit = 'pos.session' def _loader_params_product_product(self): result = super()._loader_params_product_product() result['search_params']['fields'].append('standard_price') return result -
将以下代码添加到
/static/src/xml/point_of_sale_customization.xml中,以显示具有利润率的产品卡片:<t t-name="ProductsWidget" t-inherit="point_of_sale.ProductsWidget" t-inherit-mode="extension"> <xpath expr="//ProductCard" position="attributes"> <attribute name="standard_price"> pos.env.utils.formatCurrency(product.get_display_price() - product.standard_price) </attribute> </xpath> </t> <t t-name="ProductCard" t-inherit="point_of_sale.ProductCard" t-inherit-mode="extension"> <xpath expr="//span[hasclass('price-tag')]" position="after"> <span t-if="props.standard_price" class="sale_margin py-1 fw-bolder"> <t t-esc="props.standard_price"/> </span> </xpath> </t> -
添加以下样式表来设置利润率文本的样式:
.sale_margin { line-height: 21px; background: #CDDC39; padding: 0px 5px; } -
更新
point_of_sale_customization模块以应用更改。之后,您将能够在产品卡片上看到利润率:

图 22.6 – 产品的利润率
如果没有设置产品的成本,则产品卡片将不会显示利润率,因此请确保您设置了产品成本。
它是如何工作的...
在这个教程中,我们希望使用standard_price字段作为产品的采购成本。这个字段在销售点应用程序中默认不加载。
在步骤 1中,我们为product.product模型添加了standard_price字段。在此之后,产品数据将多出一个字段 – standard_price。
在步骤 2中,我们扩展了默认的产品卡片模板。您需要使用t-inherit属性来扩展现有的QWeb模板。
然后,您需要使用 XPath 来选择要执行操作的元素。如果您想了解更多关于 XPath 的信息,请参阅第九章,后端视图中的更改现有视图 – 视图继承教程。
要获取产品销售价格,我们使用了从父 OWL 组件发送的product属性。然后,我们通过使用产品价格和产品成本来计算利润率。如果您想了解更多信息,请参阅第十六章,Odoo Web 库(OWL)。
在步骤 3中,我们添加了样式表来修改边距元素的定位。这将给边距元素添加一个背景色,并将其放置在价格药丸下方。
修改现有业务逻辑
在之前的食谱中,我们看到了如何通过 RPC 获取数据以及如何修改销售点应用的用户界面。在这个食谱中,我们将看到如何修改或扩展现有的业务逻辑。
准备工作
在这个食谱中,我们将使用在修改销售点屏幕 UI食谱中创建的point_of_sale_customization模块,在那里我们获取了产品的购买价格并显示了产品利润。现在,在这个食谱中,我们将向用户显示一个警告,如果他们以低于产品利润的价格出售产品。
如何做到这一点...
销售点应用的业务逻辑大部分是用 JavaScript 编写的,所以我们只需对其进行修改即可实现本食谱的目标。将以下代码添加到/static/src/js/point_of_sale_customization.js以在用户以低于购买价格出售产品时显示警告:
/** @odoo-module */
import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup";
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
import { patch } from "@web/core/utils/patch";
patch(ProductScreen.prototype, {
_setValue(val) {
super._setValue(val);
const orderline = this.currentOrder.get_selected_orderline();
if (orderline && orderline.product.standard_price) {
var price_unit = orderline.get_unit_price() * (1.0 - (orderline.get_discount() / 100.0));
if (orderline.product.standard_price > price_unit) {
this.popup.add(ErrorPopup, {
title: 'Warning',
body: 'Product price set below cost of product.'
});
}
}
}
});
更新point_of_sale_customization模块以应用更改。更新后,以这种方式在订单行中添加折扣,使产品价格低于购买价格。将出现一个包含以下警告的弹出窗口:

图 22.7– 大折扣的警告
注意,当你将产品价格设置低于实际成本时,会显示一个警告,并且每次你采取行动时(例如,当你更改产品订单的数量时),它都会继续弹出。
它是如何工作的...
销售点组件注册提供了一个extend方法来修改现有函数。内部,它通过猴子补丁实际组件定义。
在我们的示例中,我们修改了_setValue()方法。每当用户更改订单行时,ProductScreen的_setValue()方法就会被调用。我们希望如果用户将产品价格设置低于产品成本时显示一个警告。因此,我们定义了一个新的_setValue()方法并调用了super方法;这将确保用户执行的所有操作都会被应用。在调用super方法之后,我们编写了我们的逻辑,该逻辑检查产品销售价格是否高于产品的实际成本。如果不是,我们就会向用户显示一个警告。
注意
如果使用不当,super可能会破坏某些东西。如果该方法是从几个文件中继承的,你必须调用super方法;否则,它将跳过后续继承中的逻辑。这有时会导致内部数据状态损坏。
我们在调用默认实现(super)之后放置了我们的业务逻辑。如果你想在默认实现之前编写业务逻辑,你可以通过将super调用移到函数末尾来实现。
修改客户收据
当您定制销售点应用程序时,客户通常会要求修改客户收据。在本配方中,您将学习如何修改客户收据。
准备工作
在本配方中,我们将使用在修改现有业务逻辑配方中创建的point_of_sale_customization模块。我们将向销售点收据添加一行,以显示客户在订单中节省了多少钱。
如何操作...
按照以下步骤修改销售点应用程序中的客户收据:
-
将以下代码添加到
/static/src/js/point_of_sale_customization.js文件中。这将向收据环境添加额外的数据:/** @odoo-module */ import { Order } from "@point_of_sale/app/store/models"; import { patch } from "@web/core/utils/patch"; patch(Order.prototype, { saved_amount(){ const order = this; return order.orderlines.reduce((rem, line) => { var diffrence = (line.product.lst_price * line.quantity) - line.get_base_price(); return rem + diffrence; }, 0); }, export_for_printing() { const json = super.export_for_printing(...arguments); var savedAmount = this.saved_amount(); if (savedAmount > 0) { json.saved_amount = this.env.utils.formatCurrency(savedAmount); } return json; } }) -
将以下代码添加到
/static/src/xml/point_of_sale_customization.xml文件中。这将扩展默认的收据模板并添加我们的定制:<t t-name="OrderReceipt" t-inherit="point_of_sale.OrderReceipt" t-inherit-mode="extension"> <xpath expr="//div[hasclass('pos-receipt')]//div[hasclass('before-footer')]" position="before"> <div style="text-align:center;" t-if="props.data.saved_amount"> <br/> <div > You saved <t t-esc="props.data.saved_amount"/> on this order. </div> </div> </xpath> </t>
更新point_of_sale_customization模块以应用更改。之后,添加一个带有折扣的产品并检查收据;您将在收据中看到一行额外的内容:

图 22.8 – 更新的收据
如果金额为零或负数,收据将不会显示节省金额屏幕。
它是如何工作的...
本配方中没有任何新内容。我们只是通过使用之前的配方更新了收据。
在步骤 1中,我们覆盖了export_for_printing()函数以向收据环境发送更多数据。从export_for_printing()方法发送的任何内容都将可在收据的 QWeb 模板中使用。我们比较了产品的基准价格与收据中的产品价格,以计算客户节省了多少钱。我们通过saved_amount键将此数据发送到收据环境。
在步骤 2中,我们修改了收据的默认 QWeb 模板。实际收据的模板名称是OrderReceipt,因此我们将其用作t-inherit属性的值。在步骤 1中,我们已经发送了修改收据所需的信息。在 QWeb 模板中,我们在props.data.saved_amount键中获取保存的金额,所以我们只需在页脚之前添加一个额外的<div>元素。这将打印出收据中的节省金额。如果您想了解更多关于覆盖的信息,请参阅修改销售点屏幕 UI 烹饪配方。
第二十三章:在 Odoo 中管理电子邮件
电子邮件集成是 Odoo 最突出的功能。您可以直接从 Odoo 用户界面发送和接收电子邮件。您甚至可以管理业务文档上的电子邮件线程,例如潜在客户、销售订单和项目。在本章中,我们将探讨一些处理 Odoo 中电子邮件的重要方法。
在这里,我们将涵盖以下食谱:
-
配置入站和出站电子邮件服务器
-
管理文档上的聊天
-
管理文档上的活动
-
使用 Jinja 模板发送电子邮件
-
使用 QWeb 模板发送电子邮件
-
管理电子邮件别名
-
在聊天中记录用户更改
-
发送周期性摘要电子邮件
技术要求
本章中使用的所有代码都可以从github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter23下载。
配置入站和出站电子邮件服务器
在您开始在 Odoo 中发送和接收电子邮件之前,您需要配置入站和出站电子邮件服务器。在本食谱中,您将学习如何在 Odoo 中配置电子邮件服务器。
准备工作
本食谱不需要开发,但您将需要电子邮件服务器信息,例如服务器 URL、端口、服务器类型、用户名和密码。我们将使用这些信息来配置电子邮件服务器。
注意
如果您使用Odoo Online或Odoo.sh,您不需要配置电子邮件服务器。您可以在这些平台上发送和接收电子邮件,而无需任何复杂的配置。本食谱是为本地 Odoo 实例准备的。
如何操作...
配置入站和出站电子邮件服务器涉及一些步骤,这些步骤对于入站和出站服务器是通用的,还有一些步骤是针对每种服务器独特的。因此,首先,我们将查看通用配置步骤,然后我们将分别配置入站和出站电子邮件服务器。以下是为入站和出站电子邮件服务器所需的步骤:
-
打开常规设置表单菜单,在设置 | 常规设置。
-
前往讨论部分,并在别名域名内部。这将显示以下选项:

图 23.1 – 设置别名域名
- 在别名域名字段中,输入您的电子邮件服务器正在运行的域名。然后,保存配置。
配置入站电子邮件服务器
执行以下步骤以配置入站电子邮件服务器:
-
打开常规设置并点击技术 | 电子邮件下的入站电子邮件服务器链接。这将重定向到入站电子邮件服务器的列表视图。
-
点击创建按钮,这将打开以下表单视图。输入您的入站电子邮件服务器的详细信息(有关每个字段的解释,请参阅如何操作…部分):

图 23.2 – 配置入站邮件服务器
- 点击测试 & 确认按钮以验证您的配置。如果您错误地配置了入站邮件服务器,它将显示错误消息。
配置出站邮件服务器
按照以下步骤配置出站邮件服务器:
-
打开常规设置并启用自定义邮件服务器选项,然后点击出站邮件服务器链接。这将带您到出站邮件服务器的列表视图。
-
点击创建,这将打开以下表单视图。输入您的出站邮件服务器的详细信息(有关每个字段的说明,请参阅如何工作…部分):

图 23.3 – 配置出站邮件服务器
- 在屏幕底部点击测试连接以验证您的配置。如果您错误地配置了出站邮件服务器,它将显示错误消息。
即使您已正确配置,出站邮件服务器也会显示错误对话框。在错误对话框正文中寻找连接测试成功消息。这意味着您的出站服务器已正确配置。
它是如何工作的...
本食谱中给出的步骤是自我解释的,不需要进一步解释。但是,出站邮件和入站邮件记录有多个字段,让我们看看它们的目的。
以下是为配置入站邮件服务器使用的字段列表:
-
名称:服务器的名称,有助于您在配置了多个入站邮件服务器时识别特定的入站邮件服务器。
-
服务器类型:在此处,您需要从三个选项中选择:POP 服务器、IMAP 服务器和本地服务器。此字段的值将基于您的电子邮件服务提供商。
-
服务器名称:运行服务的服务器域名。
-
端口:服务器运行的端口号。
-
SSL/TLS:如果您正在使用 SSL/TLS 加密,请勾选此字段。
-
用户名:您正在获取邮件的电子邮件地址。
-
密码:提供的电子邮件地址的密码。
-
激活:此字段用于启用或禁用入站邮件服务器。
-
保留附件:如果您不想管理入站邮件的附件,请关闭此选项。
-
保留原始:如果您想保留原始邮件及其前一个邮件,请启用此选项。
以下是为配置出站邮件服务器使用的字段列表:
-
名称:服务器的名称,有助于您在配置了多个入站邮件服务器时识别特定的入站邮件服务器。
-
优先级:此字段用于定义出站邮件服务器的优先级。数字越小,优先级越高,因此具有较低优先级数字的邮件服务器将被使用最多。
-
SMTP 服务器:运行服务的服务器域名。
-
SMTP 端口:服务器运行的端口号。
-
连接加密:用于发送电子邮件的安全类型。
-
用户名:用于发送电子邮件的电子邮件账户。
-
密码:提供的电子邮件账户的密码。
-
激活:此字段用于启用或禁用出站电子邮件服务器。
还有更多...
默认情况下,每 5 分钟检索一次传入的电子邮件。如果您想更改此间隔,请按照以下步骤操作:
-
激活开发者模式。
-
在设置 | 技术 | 自动化 | 计划任务中打开计划任务。
-
搜索并打开名为邮件:Fetchmail 服务的计划任务。
-
使用标记为执行间隔的字段更改间隔。
管理文档上的聊天
在本配方中,您将学习如何管理文档上的聊天,并将通信线程添加到记录中。
准备工作
对于本配方,我们将重用第八章中的my_hostel模块,高级服务器端开发技术。您可以从 GitHub 仓库中该宿舍房间的Chapter23/ 00_initial_module目录中获取模块的初始副本。在本配方中,我们将向hostel.student模型添加聊天。
如何做到这一点...
按照以下步骤在hostel.student模型的记录中添加聊天:
-
在
__manifest__.py文件中添加mail模块依赖项:... 'depends': ['mail'], ... -
在
hostel.student模型的 Python 定义中继承mail.thread:class HostelStudent(models.Model): _name = "hostel.student" _description = "Hostel Student Information" _inherit = ['mail.thread'] ... -
在
hostel.student模型的表单视图中添加聊天小部件:... </sheet> <div class="oe_chatter"> <field name="message_follower_ids" widget="mail_followers"/> <field name="message_ids" widget="mail_thread"/> </div> </form> ... -
安装
my_hostel模块以查看实际效果:

图 23.4 – 宿舍学生表单视图上的聊天
如前一个屏幕截图所示,安装模块后,您将在表单视图中看到聊天内容。
它是如何工作的...
为了在任何模型上启用聊天,您首先需要安装mail模块。这是因为启用聊天或邮件功能所需的所有代码都是mail模块的一部分。这就是为什么在步骤 1中,我们在my_hostel模块的清单文件中添加了mail模块依赖项。这将自动在您安装my_hostel模块时安装mail模块。
操作聊天所需字段和方法是mail.thread模型的一部分。mail.thread模型是一个抽象模型,仅用于继承目的。在步骤 2中,我们在hostel.student模型中继承了mail.thread模型。这将向hostel.student模型添加所有必要的字段和方法,以实现聊天功能。如果您不知道模型继承是如何工作的,请参阅第四章中的使用抽象模型实现可重用模型功能配方,应用模型。
在步骤 1和步骤 2中,我们添加了聊天所需的所有字段和方法。对于聊天来说,唯一剩下的事情是在表单视图中添加用户界面。在步骤 3中,我们添加了消息线程和关注者小部件。你可能想知道message_follower_ids和message_ids字段。这些字段没有在hostel.student模型定义中添加,但它们是通过继承从mail.thread模型添加的。
更多内容...
当你在聊天中发布消息时,会向关注者发送电子邮件。如果你注意到这个食谱的例子,学生的房间不是记录的关注者,所以他们不会收到消息。如果你想向学生发送电子邮件通知,你需要将他们添加到学生列表中。你可以从用户界面手动添加关注者,但如果你想自动添加,可以使用message_subscribe()方法。看看下面的代码——当我们分配宿舍房间时,给定的代码将自动将学生添加到关注者列表中:
@api.model
def create(self, values):
result = super().create(values)
partner_id = self.env['res.partner'].create({
'name': result.name,
'email': result.email
})
result.message_subscribe(partner_ids=[partner_id.id])
return result
同样,如果你想从列表中删除关注者,可以使用message_unsubscribe()方法。
管理文档上的活动
当使用聊天时,你也可以添加活动。这些用于规划你在记录上的操作。它类似于每个记录的待办事项列表。在这个食谱中,你将学习如何在任何模型上启用活动。
准备工作
对于这个食谱,我们将使用之前食谱中的my_hostel模块,在文档上管理聊天。我们将向hostel.student模型添加活动。
如何操作...
按照以下步骤将活动添加到hostel.student模型中:
-
在
hostel.student模型的 Python 定义中继承mail.activity.mixin:class HostelStudent(models.Model): _name = "hostel.student" _description = "Hostel Student Information" _inherit = ['mail.thread', 'mail.activity.mixin'] ... -
在
hostel.student模型的聊天中添加mail_activity小部件:... <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> ... -
更新
my_hostel模块以应用更改。这将显示聊天活动:

图 23.5 – 宿舍学生表单视图上的活动管理器
这就是用户将能够管理不同的聊天活动。请注意,由一个用户安排的活动对其他所有用户也是可见的。
它是如何工作的...
活动是mail模块的一部分,并且你可以选择在聊天中启用它们。为了在记录上启用活动,你需要继承mail.activity.mixin。类似于mail.thread模型,mail.activity.mixin也是一个抽象模型。继承mail.activity.mixin将在模块中添加所有必要的字段和方法。这些方法和字段用于管理记录上的活动。在步骤 1中,我们将mail.activity.mixin添加到hostel.student模型中。因此,hostel.student的继承将获得管理活动所需的所有方法和字段。
在 步骤 2 中,我们在表单视图中添加了 mail_activity 小部件。这将显示管理活动的 UI。通过继承,在 hostel.student 模型中添加了 activity_ids 字段。
活动可以是不同类型的。默认情况下,你可以创建具有 Email、Call、Meeting 和 To-Do 等类型的活动。如果你想添加自己的活动类型,可以在开发者模式下转到 设置 | 技术 | 讨论 | 活动类型 来实现。
更多内容...
如果你想自动安排活动,可以使用 mail.activity.mixin 模型的 activity_schedule() 方法。这将创建一个在指定退宿日期的活动。你可以使用 activity_schedule() 方法手动安排活动,如下所示:
@api.model
def create(self, values):
result = super(HostelStudent, self).create(values)
if result.discharge_date:
result.activity_schedule('mail.mail_activity_data_call',
date_deadline=result.discharge_date)
return result
return res
在此示例中,每当有人退宿时,将为学生安排一个呼叫活动。活动的截止日期将被设置为宿舍的退宿日期,以便校长可以在那天给学生打电话。
使用 Jinja 模板发送电子邮件
Odoo 支持通过 Jinja 模板创建动态电子邮件。Jinja 是一个基于文本的模板引擎,用于生成动态 HTML 内容。在本食谱中,我们将创建一个 Jinja 电子邮件模板,然后使用它发送电子邮件。
准备工作
对于这个食谱,我们将使用之前食谱中提到的 my_hostel 模块,即 管理文档上的活动。我们将添加 Jinja 模板来向学生发送电子邮件,通知他们宿舍的录取情况。
如何操作...
按照以下步骤向学生发送提醒电子邮件:
-
创建一个名为
my_hostel/data/mail_template.xml的新文件,并添加电子邮件模板:<?xml version="1.0" encoding="utf-8"?> <odoo noupdate="1"> <record id="assign_room_to_student" model="mail.template"> <field name="name">Assign Room To Student</field> <field name="model_id" ref="my_hostel.model_hostel_student"/> <field name="email_from">{{ (object.room_id.create_uid.email) }}</field> <field name="email_to">{{ (object.email) }}</field> <field name="subject">Assign Room</field> <field name="body_html" type="html"> <div style="margin: 0px; padding: 0px;"> <p style="margin: 0px; padding: 0px; font-size: 13px;"> Dear <t t-out="object.name"></t>, <br/><br/> <p>You have been assigned hostel <b><t t-out="object.hostel_id.name"></t></b> and room no <t t-out="object.room_id.room_no"></t>. <br/> Your admission date in a hostel is <b style="color:red;"><t t-out="format_date(object.admission_date)"></t>.</b> </p> <br/> <p>Best regards, <br/><t t-out="object.hostel_id.name"></t></p> </p> </div> </field> </record> </odoo> -
在清单文件中注册模板文件:
... "data": [ "security/hostel_security.xml", "security/ir.model.access.csv", "data/categ_data.xml", "data/mail_template.xml", "views/hostel.xml", "views/hostel_room.xml", "views/hostel_amenities.xml", "views/hostel_student.xml", "views/hostel_categ.xml", "views/hostel_room_category_view.xml", ], ... -
添加
hostel.student模型以发送电子邮件:... <header> <button name="send_mail_assign_room" string="Send Email For Assign Room" type="object"/> <button name="action_assign_room" string="Assign Room" type="object" class="btn-primary"/> <field name="status" widget="statusbar" options="{'clickable': '1'}"/> </header> ... -
将
send_mail_assign_room()方法添加到hostel.student模型:... def send_mail_assign_room(self): self.message_post_with_source('my_hostel.assign_room_to_student')
更新 my_hostel 模块以应用更改。这将添加一个 hostel.student 模型。当他们点击按钮时,关注者将收到以下消息:

图 23.6 – 通过 Jinja 模板发送的电子邮件
本食谱中所示的过程在你想要通过电子邮件向客户发送更新时很有用。由于 Jinja 模板,你可以根据单个记录动态发送电子邮件。
工作原理...
在 步骤 1 中,我们使用 Jinja 创建了一个电子邮件模板。Jinja 模板帮助我们根据记录数据生成动态电子邮件。电子邮件模板存储在 mail.template 模型中。让我们看看你需要传递的字段列表,以便创建一个 Jinja 电子邮件模板:
-
name:用于识别特定模板的模板名称。 -
email_from:此字段的值将是发送此电子邮件的电子邮件地址。 -
email_to:此字段的值将是收件人的电子邮件地址。 -
email_cc:此字段的值将用于发送电子邮件副本的电子邮件地址。 -
subject: 此字段包含电子邮件的主题。 -
model_id: 此字段包含模型的引用。电子邮件模板将使用此模型的数据进行渲染。 -
body_html: 此字段将包含电子邮件模板的主体。它是一个 Jinja 模板,因此您可以使用变量、循环、条件等。如果您想了解更多关于 Jinja 模板的信息,请访问jinja.pocoo.org/docs/2.10/。通常,我们在CDATA标签中包装内容,这样主体中的内容就被视为字符数据,而不是标记。 -
auto_delete: 这是一个布尔字段,在发送电子邮件后删除电子邮件。此字段的默认值为False。 -
lang: 此字段用于将电子邮件模板翻译成另一种语言。 -
scheduled_date: 此字段用于安排未来的电子邮件。
信息
您可以在email_form、email_to、email_cc、subject、scheduled_date和lang字段中使用${}。这有助于您动态设置值。看看我们配方中的步骤 1——我们使用了{{ (object.email) }}来动态设置email_to字段。
如果您仔细查看body_html字段的内容,您会注意到我们使用了<t t-out="object.name">。在这里,对象是hostel.student模型的记录集。在渲染过程中,<t t-out="object.hostel_id.name"></t>将被替换为宿舍名称。以及object,一些其他辅助函数和变量也被传递到渲染上下文中。以下是传递给渲染上下文的辅助函数列表:
-
object: 此变量将包含模型的记录集,该记录集由模板中的model_id字段设置 -
format_date: 这是用于格式化日期时间对象的方法的引用 -
format_datetime: 这是用于将 UTC 日期和时间转换为另一个时区的日期和时间的方法的引用 -
format_amount: 这是用于将float转换为带有货币符号的字符串的方法的引用 -
format_duration: 此方法用于将float转换为time——例如,将 1.5 转换为 01:30 -
user: 这将是当前用户的记录集 -
ctx: 这将包含环境上下文的字典
备注
如果您想查看模板列表,请激活开发者模式,并打开设置 | 技术 | 电子邮件 | 模板菜单。模板的表单视图还提供了一个按钮来预览渲染的模板。
在步骤 2中,我们在清单文件中注册了模板文件。
在步骤 3中,我们在表单视图中添加了一个按钮来调用send_mail_assign_room()方法,该方法将电子邮件发送给关注者。
在 步骤 4 中,我们添加了 send_mail_assign_room() 方法,该方法将在点击按钮时被调用。message_post_with_source() 方法用于发送电子邮件。message_post_with_source() 方法通过 mail.thread 继承在模型中。要发送电子邮件,您只需将模板 ID 作为参数传递。
更多...
message_post_with_source() 方法用于使用 Jinja 模板发送电子邮件。如果您只想发送纯文本电子邮件,可以使用 message_post() 方法:
self.message_post(body="Your hostel admission process is completed.")
上述代码将添加一个 subtype_id 参数。
使用 QWeb 模板发送电子邮件
在前面的菜谱中,我们学习了如何使用 Jinja 模板发送电子邮件。在这个菜谱中,我们将看到另一种发送动态电子邮件的方法。我们将借助 QWeb 模板发送电子邮件。
准备工作
对于本菜谱,我们将使用前一个菜谱 使用 Jinja 模板发送电子邮件 中的 my_hostel 模块。我们将使用 QWeb 模板向学生发送电子邮件,告知他们在宿舍的入学已完成。
如何操作...
按照以下步骤向学生发送提醒电子邮件:
-
将 QWeb 模板添加到
my_hostel/data/mail_template.xml文件中:<template id="assign_room_to_student_qweb"> <p>Dear <span t-field="object.name"/>,</p> <br/> <p>You have been assigned hostel <b> <span t-field="object.hostel_id.name"/> </b> and room no <span t-field="object.room_id.room_no"/>. <br/> Your admission date in a hostel is <b style="color:red;"> <span t-field="object.admission_date"/>. </b> </p> <br/> <p>Best regards, <br/> <span t-field="object.hostel_id.name"/> </p> </template> -
添加
hostel.student模型以发送电子邮件:... <header> <button name="send_mail_assign_room" string="Send Email For Assign Room" type="object"/> <button name="send_mail_assign_room_qweb" string="Send Email For Assign Room (QWeb)" type="object"/> <button name="action_assign_room" string="Assign Room" type="object" class="btn-primary"/> <field name="status" widget="statusbar" options="{'clickable': '1'}"/> </header> ... -
在
hostel.student模型中添加send_mail_assign_room_qweb()方法:... def send_mail_assign_room_qweb(self): self.message_post_with_source('my_hostel.assign_room_to_student_qweb') -
更新
my_hostel模块以应用更改。这将添加一个hostel.student模型。当按钮被点击时,关注者将收到如下消息:

图 23.7 – 通过 QWeb 模板发送的电子邮件
本菜谱中显示的程序与前面的菜谱 使用 Jinja 模板发送电子邮件 的工作方式完全相同。唯一的区别是模板类型,因为本菜谱使用 QWeb 模板。
它是如何工作的...
在 步骤 1 中,我们创建了一个具有 send_mail_assign_room_qweb ID 的 QWeb 模板。如果您查看模板,您会看到我们不再使用 format_date() 数据字段方法了。这是因为 QWeb 渲染引擎会自动处理这一点,并基于用户的语言显示日期。出于同样的原因,您不需要使用 format_amount() 方法来显示货币符号。QWeb 渲染引擎将自动处理这一点。如果您想了解更多关于 QWeb 模板的信息,请参考 第十四章 中的 创建或修改模板 菜谱,CMS 网站开发。
在 步骤 2 中,我们在表单视图中添加了一个按钮,用于调用 send_mail_assign_room_qweb() 方法,该方法将电子邮件发送给关注者。
在 步骤 3 中,我们添加了 send_mail_assign_room_qweb() 方法,该方法将由按钮点击触发。message_post_with_source() 方法用于发送电子邮件。message_post_with_source() 方法通过 mail.thread 继承在模型中继承。要发送电子邮件,只需将 Web 模板的 XML ID 作为参数传递即可。
使用 QWeb 模板发送电子邮件与上一个配方中的操作完全相同,但 QWeb 电子邮件模板和 Jinja 电子邮件模板之间存在一些细微的差异。以下是两个模板的快速比较:
-
在电子邮件模板中发送额外参数没有简单的方法。您必须使用
object变量中的记录集来获取动态数据。另一方面,使用 QWeb 电子邮件模板,您可以通过values参数在渲染上下文中传递额外值:self.message_post_with_source('my_hostel.assign_room_to_student_qweb', values={'extra_data': 'test'}) -
要管理日期格式、时区和带有货币符号的金额,在 Jinja 模板中,您必须使用
format_date、format_tz和format_amount函数,而在 QWeb 模板中,这是自动管理的。 -
在 Jinja 中无法修改其他模块的现有模板,而在 QWeb 模板中,您可以通过继承来修改电子邮件模板。如果您想了解更多关于 QWeb 继承的信息,请参阅 第十四章 中的 创建或修改模板 配方,CMS 网站开发。
-
您可以直接从消息编辑器中选择并使用 Jinja 模板。在以下屏幕截图中的右下角下拉菜单用于选择 Jinja 模板:

图 23.8 – 模板选择选项
- 使用 QWeb,直接从消息编辑器中选择模板不是一个选项。
更多内容...
所有方法(message_post 和 message_post_with_source)都尊重用户的偏好。如果用户从用户偏好中更改通知管理选项,用户将不会收到电子邮件;相反,他们将在 Odoo 的 UI 中收到通知。对于客户也是如此;如果客户选择退出电子邮件,他们将不会通过电子邮件收到任何更新。
此外,Odoo 消息线程遵循一个称为 subtype_id 的概念,在 message_post_* 方法中根据子类型发送电子邮件。通常,用户将从 关注 按钮的下拉菜单中管理他们的子类型。假设用户已将子类型设置为以下内容:

图 23.9 – 编辑子类型选项
根据用户的偏好,用户将只为 讨论 消息接收电子邮件。
管理电子邮件别名
电子邮件别名是 Odoo 中用于通过传入电子邮件创建记录的功能。电子邮件别名的最简单例子是销售团队。您只需向 sale@yourdomain.com 发送电子邮件,Odoo 就会在销售团队中为 crm.lead 创建一个新的记录。在这个配方中,我们将创建一个电子邮件别名来创建宿舍学生记录。
准备工作
对于这个配方,我们将使用前一个配方中的 my_hostel 模块,即 使用 QWeb 模板发送电子邮件。我们将使用 hostelstudent@yourdomain.com 电子邮件地址创建我们的电子邮件别名。如果您向此电子邮件地址发送主题包含书籍名称的电子邮件,将在 hostel.student 模型中创建一个记录。
如何操作...
按以下步骤为 hostel.student 模型添加电子邮件别名:
-
在
my_hostel/data/mail_template.xml文件中添加电子邮件别名数据:<record id="mail_alias_room_assign" model="mail.alias"> <field name="alias_name">room</field> <field name="alias_model_id" ref="model_hostel_student"/> <field name="alias_contact">partners</field> </record> -
在
my_hostel/models/hostel_student.py文件中添加以下导入:import re from odoo.tools import email_split, email_escape_char -
在
hostel.student模型中覆盖message_new()方法:@api.model def message_new(self, msg_dict, custom_values=None): self = self.with_context(default_user_id=False) if custom_values is None: custom_values = {} custom_values['name'] = re.match(r"(.+?)\s*<(.+?)>", msg_dict.get('from')).group(1) custom_values['email'] = email_escape_char(email_split(msg_dict.get('from'))[0]) return super(HostelStudent, self).message_new(msg_dict, custom_values)
更新 my_hostel 模块以应用更改。然后,向 hostelstudent@yourdomain.com 发送电子邮件。这将创建一个新的 hostel.student 记录,并如下所示显示:

图 23.10 – 通过电子邮件生成的记录
每次您向 hostelstudent@yourdomain.com 发送电子邮件时,Odoo 都会生成一个新的学生记录。
它是如何工作的...
在 步骤 1 中,我们创建了一个 mail.alias 记录。此别名将处理 hostelstudent@yourdomain.com 电子邮件地址。当您向此地址发送电子邮件时,Odoo 将在 hostel.student 模型中创建一个新的记录。如果您想查看系统中活跃的别名列表,请打开 设置 | 技术 | 电子邮件 | 别名。以下是可用于配置别名的字段列表:
-
alias_name: 此字段包含电子邮件地址的本地部分;例如,在hostelstudent@yourdomain.com中的hostelstudent部分是电子邮件地址的本地部分。 -
alias_model_id: 应为传入的电子邮件创建记录的模型引用。 -
alias_contact: 此字段包含别名的安全偏好设置。可能的选项有everyone、partners、followers和employees。 -
alias_defaults: 当收到一封新邮件时,其记录会在别名指定的模型中创建。如果您想在记录中设置默认值,请以字典的形式在此字段中给出这些值。
在 步骤 2 中,我们添加了必要的导入。在 步骤 3 中,我们覆盖了 message_new() 方法。当在别名电子邮件地址上收到新邮件时,会自动调用此方法。此方法将接受两个参数:
-
msg_dict: 此参数将是包含接收到的电子邮件信息的字典。它包含有关电子邮件的信息,例如发件人的电子邮件地址、收件人的电子邮件地址、电子邮件主题和电子邮件正文。 -
custom_values:这是一个用于创建新记录的自定义值。这是你在别名记录上使用alias_defaults字段设置的相同值。
在我们的配方中,我们覆盖了message_new()方法,并通过正则表达式从电子邮件中获取名称。然后,我们通过在步骤 2中导入的工具获取发送者的电子邮件地址。我们使用发送者的电子邮件地址来创建学生记录。然后,我们使用这两个值更新custom_values:name和email。我们将更新的custom_values数据传递给super()方法,该方法使用给定的name和email值创建一个新的hostel.student记录。这就是当你向别名发送电子邮件时创建记录的方式。
还有更多...
一些商业模式有这样一个要求,即每个记录都需要一个单独的别名。例如,销售团队模式为每个团队提供单独的别名,如印度团队的sale-in@example.com和比利时团队的sale-be@example.com。如果你想在你的模型中管理这样的别名,你可以使用mail.alias.mixin。为了在你的模型中使用它,你需要继承这个 mixin:
class Team(models.Model):
_name = 'crm.team'
_inherit = ['mail.alias.mixin', 'mail.thread']
继承 mixin 后,你需要将alias_name字段添加到表单视图中,以便最终用户可以自己添加别名。
在聊天记录中记录用户更改
Odoo 框架提供了一个内置功能来在聊天记录中记录字段更改。在这个配方中,我们将启用一些字段的日志记录,以便当它们发生变化时,Odoo 将在聊天记录中添加日志。
准备工作
对于这个配方,我们将使用之前配方中的my_hostel模块,即管理电子邮件别名。在这个配方中,我们将记录hostel.student模型中几个字段的更改。
如何做到这一点...
修改字段的定义,以便在更改它们时启用日志记录。这在下述代码片段中显示:
class HostelStudent(models.Model):
_name = "hostel.student"
_description = "Hostel Student Information"
_inherit = ['mail.thread', 'mail.activity.mixin']
name = fields.Char("Student Name")
email = fields.Char("Student Email")
gender = fields.Selection([("male", "Male"),
("female", "Female"), ("other", "Other")],
string="Gender", help="Student gender")
active = fields.Boolean("Active", default=True,
help="Activate/Deactivate hostel record")
hostel_id = fields.Many2one("hostel.hostel", "hostel", help="Name of hostel")
room_id = fields.Many2one("hostel.room", "Room",
help="Select hostel room")
status = fields.Selection([("draft", "Draft"),
("reservation", "Reservation"), ("pending", "Pending"),
("paid", "Done"),("discharge", "Discharge"), ("cancel", "Cancel")],
string="Status", copy=False, default="draft",
help="State of the student hostel")
admission_date = fields.Date("Admission Date",
help="Date of admission in hostel",
default=fields.Datetime.today,
tracking=True)
discharge_date = fields.Date("Discharge Date",
help="Date on which student discharge",
tracking=True)
duration = fields.Integer("Duration", compute="_compute_check_duration", inverse="_inverse_duration",
help="Enter duration of living")
更新my_hostel模块以应用更改。在hostel.student模型中创建一个新记录,更改字段,然后入住和退宿宿舍。如果你检查聊天记录,你会看到以下日志:

图 23.11 – 聊天记录中的更改日志
每当你更改state、admission_date或discharge_date时,你将在聊天记录中看到一条新的日志。这将帮助你看到记录的完整历史。
它是如何工作的...
通过在字段上添加tracking=True属性,你可以为该字段启用日志记录。当你设置tracking=True属性时,Odoo 会在你更新字段值时在聊天记录中添加日志。如果你在多个记录上启用跟踪,并且想在跟踪值中提供一个序列,你也可以在跟踪参数中传递一个数字,例如:tracking=20。当你传递tracking=True时,将使用默认序列,即100。
在我们的食谱中,我们在state、admission_date和discharge_date字段上添加了tracking=True。这意味着当您更新admission_date、discharge_date或state字段的值时,Odoo 将记录更改。请查看如何做…部分中的截图;我们只更改了admission_date和discharge_date字段。
注意,track_visibility功能仅在您的模型继承mail.thread模型时才有效,因为与代码相关的聊天和日志是mail.thread模型的一部分。
发送周期性摘要电子邮件
Odoo 框架内置了对发送周期性摘要电子邮件的支持。通过摘要电子邮件,您可以发送包含业务关键绩效指标(KPIs)信息的电子邮件。在本食谱中,我们将发送宿舍房间数据给校长(或任何其他授权人员)。
准备工作
对于本食谱,我们将使用之前食谱中的my_hostel模块,即在聊天中记录用户更改。
如何做…
按照以下步骤生成房间租金记录的摘要电子邮件:
-
继承
digest.digest模型并添加 KPI 字段:class Digest(models.Model): _inherit = 'digest.digest' kpi_room_rent = fields.Boolean('Room Rent') kpi_room_rent_value = fields.Integer(compute='_compute_kpi_room_rent_value') def _compute_kpi_room_rent_value(self): for record in self: start, end, company = record._get_kpi_compute_parameters() record.kpi_room_rent_value = self.env['hostel.room'].search_count([ ('create_date', '>=', start), ('create_date', '<', end) ]) -
继承
digest.digest模型的表单视图并添加 KPI 字段:<?xml version='1.0' encoding='utf-8'?> <odoo> <record id="digest_digest_view_form" model="ir.ui.view"> <field name="name">digest.digest.view.form.inherit.hostel</field> <field name="model">digest.digest</field> <field name="inherit_id" ref="digest.digest_digest_view_form"/> <field name="arch" type="xml"> <xpath expr="//group[@name='kpi_general']" position="after"> <group name="kpi_hostel" string="Hostel"> <field name="kpi_room_rent"/> </group> </xpath> </field> </record> </odoo>
更新模块以应用更改。一旦更新了模块,启用开发者模式并打开设置 | 技术 | 电子邮件 | 摘要电子邮件,如图所示:

图 23.12 – 启用房间租金数据的摘要电子邮件
一旦启用此功能并且您已订阅摘要电子邮件,您将开始接收摘要电子邮件。
它是如何工作的…
为了构建定制的摘要电子邮件,您需要两个字段。第一个字段将是一个Boolean字段,用于启用和禁用 KPI,而第二个字段将是一个compute字段,将被调用以获取 KPI 值。我们在步骤 1中创建了这两个字段。如果您检查compute字段的定义,它使用_get_kpi_compute_parameters方法。此方法返回三个参数:一个开始日期、一个结束日期和公司记录。您可以使用这些参数为您 KPI 生成一个值。我们返回了特定时间段内租出的房间数量。如果您的 KPI 是跨站点的,那么您可以使用company参数。
在步骤 2中,我们在摘要表单视图中添加了一个字段。此字段用于启用/禁用摘要电子邮件。当您启用它时,您将开始接收摘要电子邮件:

图 23.13 – 房间租金记录的摘要电子邮件
启用开发者模式,然后打开设置 | 技术 | 电子邮件 | 摘要电子邮件。在这里,您可以配置摘要电子邮件的收件人并设置摘要电子邮件的周期性。您还可以从这里启用/禁用摘要电子邮件。
第二十四章:管理 IoT Box
Odoo 提供对物联网(IoT)的支持。物联网是一个设备/传感器的网络,它们通过互联网交换数据。通过将此类设备与系统连接,您可以使用它们。例如,通过将打印机与 Odoo 连接,您可以直接将 PDF 报告发送到打印机。Odoo 使用一种称为 IoT Box 的硬件设备,用于连接打印机、卡尺、支付设备、脚踏开关等设备。在本章中,您将学习如何设置和配置 IoT Box。在此,我们将介绍以下食谱:
-
为树莓派刷写 IoT Box 图像
-
将 IoT Box 连接到网络
-
将 IoT Box 添加到 Odoo
-
加载驱动程序和列出连接的设备
-
从设备获取输入
-
通过 SSH 访问 IoT Box
-
配置销售点(POS)
-
直接将 PDF 报告发送到打印机
注意,本章的目标是安装和配置 IoT Box。开发硬件驱动程序超出了本书的范围。如果您想深入了解 IoT Box,请探索企业版中的 iot 模块。
技术要求
IoT Box 是基于 Raspberry Pi 的设备。本章中的食谱基于可从 www.raspberrypi.org/products/raspberry-pi-3-model-b-plus/ 获取的 Raspberry Pi 3 Model B+。IoT Box 是企业版的一部分,因此您需要使用企业版来遵循本章中的食谱。
本章中使用的所有代码均可从以下 GitHub 仓库下载:github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter24。
为树莓派刷写 IoT Box 图像
在本食谱中,您将学习如何使用 IoT Box 的映像刷写 microSD 卡。请注意,此食谱仅适用于已购买 空白树莓派 的用户。如果您已从 Odoo 购买了官方 IoT Box,则可以跳过此食谱,因为它已预装了 IoT Box 图像。
准备工作
树莓派 3 Model B+ 使用 microSD 卡,因此我们在此食谱中使用了 microSD 卡。您需要将 microSD 卡连接到您的计算机。
如何操作…
执行以下步骤将 IoT Box 图像安装到您的 SD 卡中:
-
将 microSD 卡插入您的计算机(如果您的计算机没有专用插槽,请使用适配器)。
-
从 Odoo 的夜间构建中下载 IoT Box 图像。该图像可在
nightly.odoo.com/master/iotbox/获取。 -
在您的计算机上下载并安装 balenaEtcher。您可以从
www.balena.io/etcher/下载此软件。 -
打开 balenaEtcher,选择 IoT Box 图像(我们使用的是 IoT Box 图像的版本 23.09),然后选择将您的 microSD 卡进行闪存。您将看到以下屏幕:

图 24.1 – 使用 IoT Box 图像闪存 SD 卡
-
点击 Flash! 按钮,等待过程完成。
-
取出 microSD 卡并将其放入 Raspberry Pi。
执行这些步骤后,您的 microSD 卡应该已经加载了 IoT Box 图像,并准备好在 IoT Box 中使用。
它是如何工作的...
在这个菜谱中,我们在 microSD 卡上安装了 IoT Box 图像。在第二步中,我们从 Odoo 夜间构建中下载了 IoT Box 图像。在夜间构建页面上,您可以找到不同版本的 IoT Box 图像。您需要从 Odoo 夜间构建中选择最新版本。在撰写本书时,我们使用了最新版本,即 iotboxv23_11.zip。Odoo IoT Box 图像是基于 Raspbian Stretch Lite 操作系统的,该镜像包含了与 Odoo 实例集成所需的库和模块。
在 步骤 3 中,我们下载了 balenaEtcher 工具来闪存 microSD 卡。
注意
在这个菜谱中,我们使用 balenaEtcher 闪存 microSD 卡,但您可以使用任何其他工具来闪存 microSD 卡。
在 步骤 4 中,我们使用 IoT Box 图像闪存了 microSD 卡。请注意,这个过程可能需要几分钟。完成过程后,microSD 卡将准备好使用。
如果您想验证图像是否成功闪存,请执行以下步骤:
-
将 microSD 卡安装到 Raspberry Pi 上。
-
将其连接到电源,并通过 HDMI 线缆(在实际使用中,外部显示器不是必需的;我们在这里只是用于验证目的)连接外部显示器。
-
操作系统将启动并显示以下页面:

图 24.2 – IoT Box 屏幕
如果您不使用显示器,只需将 IoT Box 连接到电源,过一段时间后,您将看到 IoT Box 的 Wi-Fi 网络。
更多...
在 Odoo 的早期版本中,PosBox 用于 POS 应用。IoT Box 支持所有 PosBox 的功能,因此如果您使用的是 Odoo 的社区版,并且想集成设备,您可以使用相同的 IoT Box 图像连接不同设备的 Odoo 实例。有关更多信息,请参阅 配置 POS 菜谱。
将 IoT Box 连接到网络
IoT Box 通过网络与 Odoo 实例通信。连接 IoT Box 是一个关键步骤,如果您在这里出错,您可能会在连接 IoT Box 与 Odoo 时遇到错误。
准备工作
将带有 IoT Box 图像的 microSD 卡安装到 Raspberry Pi 上,然后连接 Raspberry Pi 到电源。
如何操作...
Raspberry Pi 3 Model B+ 支持两种网络连接类型——以太网 和 Wi-Fi。
通过以太网连接物联网盒很简单;您只需将您的物联网盒用RJ45 以太网线连接,物联网盒就准备好使用了。通过 Wi-Fi 连接物联网盒比较复杂,因为您可能没有连接显示屏。按照以下步骤通过 Wi-Fi 连接物联网盒:
-
将物联网盒连接到电源(如果以太网线已插入物联网盒,请将其拔出并重新启动物联网盒)。
-
打开您的计算机并连接到名为
IoTBox的 Wi-Fi 网络,如下面的截图所示(不需要密码):

图 24.3 – 物联网盒 Wi-Fi 网络
- 连接到 Wi-Fi 网络后,您将看到一个弹出窗口,显示物联网盒主页,如下面的截图所示(如果不起作用,请在浏览器中打开盒子的 IP 地址):

图 24.4 – 连接到物联网盒
- 设置物联网盒名称并保持服务器令牌为空,然后点击下一步。这将带您到一个页面,您可以查看 Wi-Fi 网络的列表:

图 24.5 – 连接到 Wi-Fi
注意
如果您使用的是企业版并且希望立即将物联网盒与 Odoo 连接,您可以使用服务器令牌。您可以从您的 Odoo 实例中获取服务器令牌;有关更多信息,请参阅下一道菜谱。
- 选择您想要连接的 Wi-Fi 网络,并填写密码字段。完成此操作后,点击连接按钮。如果您输入了正确的信息,您将被重定向到最后一个页面:

图 24.6 – 确认页面
执行这些步骤后,您的物联网盒已连接到网络,并准备好与 Odoo 实例集成。
它是如何工作的...
通过以太网将 Odoo 实例连接到物联网盒很简单;只需将您的物联网盒用 RJ45 以太网线连接,物联网盒就准备好使用了。当您想要通过 Wi-Fi 连接物联网盒时,这会变得复杂,因为物联网盒没有显示屏或 GUI。您没有界面可以输入您的 Wi-Fi 网络密码。因此,解决这个问题的方法是断开您的物联网盒与以太网线的连接(如果已连接)并重新启动它。在这种情况下,物联网盒将创建自己的 Wi-Fi 热点,命名为IoTBox或类似名称,如步骤 2所示。您需要将 Wi-Fi 连接到名为IoTBox的名称;幸运的是,它不需要密码。一旦连接到IoTBox Wi-Fi,您将看到一个弹出窗口,如步骤 3所示。在这里,您可以给您的物联网盒起一个类似装配线物联网盒的名字。目前保持服务器令牌为空;我们将在将物联网盒添加到 Odoo菜谱中了解更多。然后,点击下一步按钮。
点击下一步按钮后,您将看到一个 Wi-Fi 网络列表,如图步骤 4所示。在此,您可以连接物联网盒子到您的 Wi-Fi 网络。请确保您选择了正确的网络。您需要将物联网盒子连接到将要使用 Odoo 实例的计算机相同的 Wi-Fi 网络。物联网盒子和 Odoo 实例在局域网(LAN)内进行通信。这意味着如果两者连接到不同的网络,它们将无法通信,因此物联网将无法工作。
选择正确的 Wi-Fi 网络后,点击连接。然后,物联网盒子将关闭其热点并重新连接到您配置的 Wi-Fi 网络。就这样——物联网盒子已准备好使用。
将物联网盒子添加到 Odoo
我们的物联网盒子已连接到本地网络,并准备好与 Odoo 一起使用。在本菜谱中,我们将连接物联网盒子与 Odoo 实例。
准备工作
确保物联网盒子已开启,并且您已将物联网盒子连接到与运行 Odoo 实例的计算机相同的 Wi-Fi 网络。
有几件事情您需要注意;否则,物联网盒子将不会被添加到 Odoo 中:
-
如果您在本地实例中测试物联网盒子,您需要使用
http://192.168.*.*:8069(您的本地 IP)而不是http://localhost:8069。如果您使用 localhost,物联网盒子将不会被添加到您的 Odoo 实例中。 -
您需要将物联网盒子连接到与运行 Odoo 实例的计算机相同的 Wi-Fi/Ethernet 网络。否则,物联网盒子将不会被添加到您的 Odoo 实例中。
-
如果您的 Odoo 实例运行在多个数据库上,物联网盒子将不会自动连接到 Odoo 实例。使用
--db-filter选项来避免此问题。
如何操作...
为了将物联网盒子与 Odoo 连接,首先您需要在您的 Odoo 实例上安装iot模块:
- 要这样做,请转到应用菜单并搜索物联网模块。该模块看起来像这样。激活模块,我们就可以开始了:

图 24.7 – 安装iot模块
-
安装
iot模块后,您可以将您的实例与物联网盒子连接。然后,通过点击物联网菜单手动将物联网盒子与 Odoo 实例连接。 -
在控制面板上点击连接按钮。这将显示以下弹出窗口。复制令牌值:

图 24.8 – 连接物联网盒子与 Odoo 的对话框
- 使用端口
8069打开物联网盒子的 IP。这将显示物联网盒子的主页。在名称部分点击配置按钮:

图 24.9 – 物联网盒子主页
- 设置物联网盒子名称设置并粘贴服务器令牌。然后,点击连接按钮。这将开始配置物联网盒子。等待过程完成:

图 24.10 – IoT Box 主页
- 在你的 Odoo 实例中检查IoT菜单。你将找到一个新的 IoT Box:

图 24.11 – 成功连接 IoT Box
它是如何工作的...
将 IoT Box 与 Odoo 连接很重要。这样,Odoo 将知道 IoT Box 的 IP 地址。Odoo 将使用该 IP 地址与连接到该设备的设备进行通信。这还将确保在多个 IoT Box 的情况下,Odoo 与正确的 IoT Box 进行通信。其余的都是直截了当的。
如果你想在 Wi-Fi 配置期间将 IoT Box 添加到 Odoo 实例,这是可以做到的。在连接 IoT Box 与网络菜谱中,我们保留了服务器令牌字段为空。你只需要在这一步添加服务器令牌:

图 24.12 – 在 Wi-Fi 配置期间添加服务器令牌
注意
在使用 IoT Box 时,请避免使用 DHCP 网络。这是因为 IoT Box 的网络配置是基于 IP 地址添加的。如果你使用 DHCP 网络,那么 IP 地址将被动态分配。因此,你的 IoT Box 可能会因为新的 IP 地址而停止响应。为了避免这个问题,你可以将 IoT Box 的 MAC 地址映射到固定的 IP 地址。
使用配对码连接 IoT Box
连接 IoT Box 还有另一种替代方法,即通过<IoTBOX IP>:8069/point_of_sale/display。一旦你打开 POS 客户端显示,你将能够看到以下配对码:

图 24.13 – IoT Box 的配对码
然后,你只需在你的 Odoo 实例的 IoT Box 连接对话框中使用配对码。
注意
如果你没有连接到互联网,配对码将不会显示。
在前面的屏幕截图中,我们看到了如何获取 POS 客户端显示的配对码。但是,如果你有一个以太网连接和打印机,你可以在没有显示的情况下获取配对码。你只需要将 IoT Box 与以太网和打印机连接。一旦 IoT Box 启动,它将打印带有配对码的收据。然后,你只需在你的 Odoo 实例的 IoT Box 连接对话框中使用配对码。
更多信息...
如果你想要将现有的 IoT Box 连接到任何其他 Odoo 实例,你需要清除配置。你可以通过 IoT Box 的 Odoo 服务器配置页面上的清除按钮清除 IoT Box 配置:

图 24.14 – 清除 IoT Box 配置
加载驱动程序和列出连接的设备
IoT Box 不仅限于企业版。您可以使用它就像社区版中的 PosBox 一样。设备的集成是企业版的一部分,因此 IoT Box 映像不包含设备驱动程序;您需要手动加载它们。通常,如果您使用企业 Odoo 实例连接 IoT Box,IoT Box 会自动加载设备驱动程序接口。但有时,您可能有一些自定义驱动程序或未正确加载的驱动程序。在这种情况下,您可以手动加载驱动程序。在本菜谱中,我们将了解如何加载驱动程序并获取连接设备列表。
准备工作
确保 IoT Box 已开启,并且您已将其连接到与运行 Odoo 实例的计算机相同的 Wi-Fi 网络。
如何操作...
执行以下步骤将设备驱动程序加载到 IoT Box 中:
- 打开 IoT Box 主页并点击底部的处理程序列表按钮:

图 24.15 – 处理程序列表按钮
- 处理程序列表按钮会将您重定向到处理程序列表页面,在那里您将找到加载处理程序按钮。点击该按钮以加载驱动程序:

图 24.16 – 驱动程序列表
- 返回到IoT Box主页。在这里,您将看到连接设备的列表:

图 24.17 – 连接设备
执行这些步骤后,IoT Box 将准备好您指定的设备,您就可以开始在您的应用程序中使用这些设备了。
它是如何工作的...
您可以从 IoT Box 的主页加载驱动程序。您可以使用底部的加载处理程序按钮来完成此操作。请注意,这仅适用于您的 IoT Box 使用企业版与 Odoo 实例连接。加载驱动程序后,您将能够在 IoT Box 主页上看到设备列表。您还可以通过IoT | 设备菜单在 Odoo 实例中查看连接设备列表。在这个菜单中,您将看到每个 IoT Box 的连接设备列表:

图 24.18 – 连接设备列表
目前,IoT Box 支持一些硬件设备,例如摄像头、脚踏开关、打印机和卡尺。可以在此处找到 Odoo 推荐设备列表:www.odoo.com/page/iot-hardware。如果您的设备不受支持,您可以支付驱动程序开发费用。
从设备获取输入
IoT Box 仅支持有限的设备。目前,这些硬件设备已集成到制造应用程序中。但如果您愿意,您可以将支持的设备集成到您的模块中。在本菜谱中,我们将通过我们的 IoT Box 从摄像头捕获图片。
准备工作
我们将使用来自第二十三章,“在 Odoo 中管理电子邮件”中“在聊天中记录用户更改”食谱的my_hostel模块。在这个食谱中,我们将添加一个新字段来捕捉和存储当借阅者归还书籍时的图像。确保 IoT Box 已开启,并且你已经连接了一个支持的视频设备。
如何操作……
执行以下步骤,使用带有 IoT Box 的摄像头捕捉图片:
-
在清单文件中添加一个依赖项:
'depends': ['base', 'quality_iot'], -
在
hostel.student模型中添加新字段:test_type_id = fields.Many2one('quality.point.test_type', 'Test Type',help="Defines the type of the quality control point.", required=True, default=_get_default_test_type_id) test_type = fields.Char(related='test_type_id.technical_name', readonly=True) device_id = fields.Many2one('iot.device', string='IoT Device', domain="[('type', '=', 'camera')]") ip = fields.Char(related="device_id.iot_id.ip") identifier = fields.Char(related='device_id.identifier') picture = fields.Binary() -
将这些字段添加到
hostel.student模型的表单视图中:<group> <field name="test_type_id" invisible="1"/> <field name="test_type" invisible="1"/> <field name="ip" invisible="0"/> <field name="identifier" invisible="0"/> <field name="device_id" required="1"/> <field name="picture" widget="iot_picture" options="{'ip_field': 'ip', 'identifier': 'identifier'}"/> <field name="name"/> <field name="gender"/> <field name="active"/> </group> -
更新
my_hostel模块以应用更改。更新后,你将有一个按钮来捕捉图像:

图 24.19 – 通过 IoT 捕捉图像
注意,如果网络摄像头未连接到 IoT Box 或驱动程序未在 IoT Box 中加载,则按钮将不会捕捉图像。
它是如何工作的……
在步骤 1中,我们在清单文件中添加了对quality_iot模块的依赖。quality_iot模块是企业版的一部分,包含一个小部件,允许你通过 IoT Box 请求摄像头的图像。这将安装stock模块,但为了简化,我们将使用quality_iot作为依赖。如果你不想使用这个依赖项,你可以创建自己的字段小部件。请参考第十五章“Web 客户端开发”中的“创建自定义小部件”食谱,了解更多关于小部件的信息。
在步骤 2中,我们添加了捕捉摄像头图像所需字段。要捕捉图像,我们需要两样东西:IoT Box 的设备标识符和 IP 地址。我们希望给用户选择摄像头的选项,因此添加了一个device_id字段。用户将选择一个摄像头来捕捉图像,并根据所选的摄像头设备,我们从相关字段中提取 IP 和设备标识符信息。基于这些字段,Odoo 将知道在多个 IoT Box 的情况下在哪里捕捉图像。我们还添加了一个二进制字段picture来保存图像。
在步骤 3中,我们在表单视图中添加了字段。请注意,我们在picture字段上使用了iot_picture小部件。我们将ip和identifier字段作为不可见字段添加,因为我们不想向用户显示它们;相反,我们想在picture字段选项中使用它们。这个小部件将在表单视图中添加按钮;点击按钮后,Odoo 将向 IoT Box 发出请求以捕捉图像。IoT Box 将返回图像数据作为响应。此响应将被保存在picture二进制字段中。
还有更多……
IoT Box 支持蓝牙卡尺。如果你想在你的模块中进行测量,你可以使用iot_measure小部件在 Odoo 中获取它们。注意,与iot_picture一样,这里你也需要在表单视图中添加不可见的ip和identifier字段:
<field name="measure" widget="iot_measure"
options="{'ip_field': 'ip', 'identifier': 'identifier'}"/>
这将使用从物联网卡尺捕获的数据填充measure字段。
通过 SSH 访问物联网盒子
物联网盒子运行在 Raspbian 操作系统上,并且可以通过 SSH 访问物联网盒子。在本教程中,我们将学习如何通过 SSH 访问物联网盒子。
准备工作
确保物联网盒子已开启,并且您已将物联网盒子连接到与运行 Odoo 实例的计算机相同的 Wi-Fi 网络。
如何操作…
为了通过 SSH 连接物联网盒子,您需要物联网盒子的 IP 地址。您可以在其表单视图中看到此 IP 地址。例如,在本教程中,我们将使用192.168.43.6作为物联网盒子的 IP 地址,因此请将其替换为您自己的 IP 地址。执行以下步骤通过 SSH 访问物联网盒子:
-
打开终端并执行以下命令:
$ ssh pi@192.168.43.6 raspberry as the password. -
如果您添加了正确的密码,您就可以访问 shell。执行以下命令以查看目录:
total 24 -rw-r--r-- 1 root root 6 Oct 26 08:12 iotbox_version drwxr-xr-x 5 pi pi 4096 Oct 23 09:05 odoo -rw-r--r-- 1 pi pi 36 Nov 15 13:10 odoo-db-uuid.conf -rw-r--r-- 1 pi pi 0 Nov 15 13:10 odoo-enterprise-code.conf -rw-r--r-- 1 pi pi 26 Nov 15 13:10 odoo-remote-server.conf -rw-r--r-- 1 pi pi 11 Nov 15 13:10 token -rw-r--r-- 1 pi pi 26 Aug 20 12:03 wifi_network.txt
由于您有 SSH 访问权限,您可以探索物联网盒子的完整文件系统。
它是如何工作的…
我们使用用户名Pi和密码raspberry通过 SSH 访问物联网盒子。当您想调试物联网盒子中的问题时会使用 SSH 连接。SSH 不需要任何解释,但让我们看看 Odoo 在物联网盒子中的工作方式。
以下是一些可能帮助您调试问题的信息:
-
物联网盒子内部运行一些 Odoo 模块。这些模块的名称通常以
hw_开头,并在社区版中可用。您可以在/home/pi/odoo/addon目录中找到所有模块。 -
如果您想查看 Odoo 服务器日志,您可以从
/var/log/odoo/odoo-server.log文件中访问它。 -
Odoo 通过名为
odoo的服务运行;您可以使用以下命令启动、停止或重启服务:sudo serviceodoo start/restart/stop
-
客户通常通过断开电源来关闭物联网盒子。这意味着在这种情况下,物联网盒子操作系统无法正确关闭。为了避免系统损坏,物联网盒子的文件系统是只读的。
更多内容…
注意,物联网盒子仅连接到本地机器。因此,您无法直接从远程位置(通过互联网)访问 shell。如果您想远程访问物联网盒子,可以将ngrok认证令牌密钥粘贴到物联网盒子的远程调试页面,如图所示。这将启用物联网盒子的 TCP 隧道,以便您可以从任何地方通过 SSH 连接物联网盒子。有关ngrok的更多信息,请访问ngrok.com/:

图 24.20 – 使用 ngrok 令牌进行调试
一旦您添加了令牌,您将能够从远程位置访问物联网盒子。
配置 POS
物联网盒子与 POS 应用程序配合使用。在本教程中,我们将学习如何为 POS 应用程序配置物联网盒子。
准备工作
确保物联网盒已开启,并且您已将物联网盒连接到与运行 Odoo 实例的计算机相同的 Wi-Fi 网络。另外,如果尚未安装,请安装 POS 应用程序。
如何操作…
执行以下步骤以配置 POS 应用程序的物联网盒:
- 打开 POS 应用程序,并从 POS 会话下拉菜单中打开设置:

图 24.21 – POS 会话设置
- 点击编辑按钮,然后点击物联网盒复选框。这将启用更多选项:

图 24.22 – 选择物联网设备
-
选择您在 POS 会话中想要使用的设备。如果您将要使用硬件,例如条形码扫描仪,请选择相关设备。
-
通过点击控制面板中的保存按钮保存更改。
配置完成后,您将能够在 POS 应用程序中使用物联网盒。
它是如何工作的…
物联网盒可以与 PosBox 等 POS 应用程序一起使用。为了在 POS 应用程序中使用物联网盒,您必须将物联网盒连接到 Odoo 实例。如果您不知道如何连接物联网盒,请遵循将物联网盒添加到 Odoo食谱。一旦您将物联网盒连接到 Odoo,您将能够在 POS 应用程序中选择物联网盒,如图步骤 2所示。
在这里,您可以选择在 POS 会话中想要使用的硬件。保存更改后,如果您打开 POS 会话,您将能够在 POS 中使用启用的硬件。如果您在设置中启用了特定的硬件,但该硬件未连接到物联网盒,您将在顶部栏看到以下警告:

图 24.23 – 物联网盒连接问题
您可以点击这些警告尝试重新连接。
更多内容…
POS 应用程序是社区版的一部分。如果您正在使用社区版,那么在 POS 设置中,您将看到物联网盒 IP 地址字段而不是物联网盒选择项:

图 24.24 – 社区版中的物联网盒设置
如果您想在社区版中集成硬件,您需要在字段中使用物联网盒的 IP 地址。
直接将 PDF 报告发送到打印机
物联网盒默认运行通用 UNIX 打印系统(CUPS)服务器。CUPS 是一种允许计算机充当打印服务器的打印系统。您可以在www.cups.org/上了解更多信息。因此,由于物联网盒内部运行 CUPS,您可以使用物联网盒连接网络打印机。在本食谱中,我们将了解如何直接从 Odoo 打印 PDF 报告。
准备工作
确保物联网盒已开启,并且您已将物联网盒与 Odoo 连接。
如何操作…
按照以下步骤直接从 Odoo 打印报告:
-
通过 IP 地址打开物联网盒主页。
-
点击底部的打印机服务器按钮。
-
这将打开 CUPS 配置主页。在这里配置您的打印机。
-
一旦您配置了打印机,您将能够在物联网设备列表中看到打印机。激活开发者模式并打开设置| 技术 | 操作 | 报告。
-
搜索您想要打印的报告,打开表单视图,并在物联网设备字段中选择打印机,如图下所示:

图 24.25 – 选择物联网设备的选项
完成此配置后,报告 PDF 将直接发送到打印机。
它是如何工作的……
在配置方面,这个菜谱很简单,但有一些事情您应该知道。物联网盒使用 CUPS 服务器打印报告。您可以在http://<IoT Box IP>:631访问 CUPS 主页。
使用 CUPS,您可以添加/删除打印机。在 CUPS 的主页上,您将能够看到所有帮助您连接不同类型打印机的文档。一旦您配置了打印机,您将在物联网设备列表中找到您的打印机。然后,您可以在报告记录中选择此物联网设备(打印机)。通常,当您在 Odoo 中打印报告时,它会下载报告的 PDF。但完成此配置后,Odoo 将直接将 PDF 报告发送到所选打印机。请注意,只有记录中在物联网设备字段设置了打印机的报告才会发送到打印机。
第二十五章:Web Studio
Odoo Web Studio 是 Odoo 企业版独有的功能。它是一个工具箱,让您能够直接从用户界面中定制 Odoo 用户界面及其报告,无需任何代码,例如通过将组件拖放到视图中。用户可以直接从用户界面创建或定制报告。
Odoo Web Studio 是一个可视化开发工具,允许用户在 Odoo 企业资源计划(ERP)平台上定制和创建应用程序。使用 Odoo Web Studio,用户可以在无需广泛编程或编码技能的情况下设计、修改和扩展其 Odoo 应用程序的各个方面。它提供了一个拖放界面,使得不同技术水平的用户都能使用。
Odoo Web Studio 通过提供一个用户友好的环境来创建模块、定制报告、自动化等,使用户能够完全控制他们的 Odoo ERP 系统。它是寻求适应和优化其 Odoo 应用以满足其独特需求和偏好的企业的宝贵工具。因此,Odoo Web Studio 是一个强大的工具,使用户能够轻松地在 Odoo ERP 系统中创建和定制应用程序。无论您是在构建新模块、定制现有模块还是设计报告,Odoo Web Studio 都提供了一个用户友好且直观的界面,以简化这些流程。
以下是 Odoo Web Studio 的关键功能和能力:
-
视觉定制:Odoo Web Studio 提供了一个可视化界面,允许用户定制其应用程序的布局、字段和表单。您可以修改现有模块或创建全新的模块。
-
数据模型编辑器:用户可以在其应用程序中定义新的数据模型、字段以及对象之间的关系。这有助于根据特定的业务需求定制数据库结构。
-
工作流配置:工作流自动化是 ERP 系统的一个关键方面。通过 Web Studio,用户可以设计和配置工作流、自动化规则和触发器,以简化业务流程。
-
报告和仪表板:用户可以设计自定义报告和仪表板,以可视化数据并深入了解他们的业务运营。
-
移动响应性:Odoo Web Studio 应用程序设计为响应式,这意味着它们可以适应不同的屏幕尺寸和设备,包括智能手机和平板电脑。
-
无代码或低代码:虽然一定程度的技能知识可能有所帮助,但 Odoo Web Studio 设计为用户友好且易于访问,无需广泛的编码技能。这使得业务用户能够进行更改并适应其特定需求。
-
实时协作:多个用户可以同时设计和修改应用程序。
-
集成:Odoo Web Studio 应用程序可以与其他 Odoo 模块和外部系统集成,以确保无缝的数据流和连接性。
在本章中,我们将介绍以下菜谱:
-
安装 Odoo Web Studio
-
从新建应用开始
-
建议功能
-
组件
-
字段属性
-
视图
-
创建新应用
-
自定义现有应用
-
内置函数
-
报表
安装 Odoo Web Studio
在本菜谱中,您将学习如何安装 Odoo Web Studio。
使用管理员或超级用户凭据登录您的 Odoo 实例。在 Odoo 界面中,转到应用模块。这是您可以安装或激活新模块和功能的地方:
-
前往应用。
-
搜索
Web Studio。 -
点击安装。
安装后,您应该在您的 Odoo 实例中看到一个名为工作室的新菜单项或部分。点击它以访问 Odoo Web Studio:

图 25.1 – Studio 按钮的截图
一旦您进入 Odoo Web Studio,您就可以开始自定义您的 Odoo 应用程序,设计工作流程,创建报告,并使用提供的视觉工具和界面进行其他修改。通过点击图标,激活了工作室自定义模式。
从新建应用开始
在 Odoo Web Studio 中,您通常首先创建一个新应用。可以将应用视为模块或您 ERP 系统的一部分。点击创建或新建按钮开始:
-
前往应用菜单屏幕。
-
点击自定义图标。
-
点击新建应用按钮开始创建新应用:

图 25.2 – 新建应用创建屏幕的截图
- 点击新建应用后,您将看到以下内容:

图 25.3 – 点击新建应用后您将看到的内容
点击下一步。从这里,您可以定义您模块的名称并更改您模块的标志。您可以上传自定义标志或自定义标志的图标、图标颜色和标志背景:

图 25.4 – 创建新应用
为您的应用选择一个名称。您可以通过选择内置图标中的任何一个来自定义图标。您还将有机会根据您的企业品牌修改背景颜色和图标颜色。在添加模块名称后,点击>按钮。此时,您可以添加您第一个菜单的名称。在这里,您必须构建一个新的菜单,因此您可以随意命名。一旦完成,您可以选择您希望创建的模型类型。如果您是从头创建应用,请选择新建模型。否则,选择现有模型:

图 25.5 – 创建您的第一个菜单
完成此操作后,点击>按钮。您的应用将准备好进行下一级别的自定义。
建议功能
Odoo Web Studio 是一个强大的工具,允许用户在不需进行大量编码的情况下自定义和扩展他们的 Odoo 应用。根据您的业务需求,当使用 Odoo Web Studio 时,您可以利用以下建议的功能和功能:

图 25.6 – 建议功能
一旦您点击创建您的应用按钮,您将看到以下屏幕。在这里,您可以添加组件和新字段,以及修改或重用现有的模型字段。您只需拖放字段即可:

图 25.7 – 模型组件
组件
Odoo Web Studio 提供了一套组件,您可以使用它们在 Odoo ERP 系统中创建和自定义模块。这些组件使您能够在无需大量编码的情况下设计数据模型、用户界面、工作流程和报告。以下是 Odoo Web Studio 中可用的关键组件和功能:
-
数据模型设计器:此组件允许您创建和修改数据模型,定义字段,指定数据类型,设置默认值,并建立对象之间的关系。您可以为存储与您的业务流程相关的数据创建自定义对象。
-
表单构建器:表单构建器组件允许您设计和自定义用于数据输入和显示的表单。您可以将字段拖放到表单上,排列它们,并设置字段属性,如标签、帮助文本和验证规则。
-
工作流程编辑器:使用工作流程编辑器组件,您可以设计自定义工作流程来自动化业务流程。您可以定义触发器、动作和转换,允许您模拟数据在应用程序中的移动以及每个阶段应该发生什么。
-
报告设计器:报告设计器组件允许您创建自定义报告和仪表板。您可以设计报告模板,添加图表、表格和图形以可视化数据,并生成可打印或数字报告。
-
菜单编辑器:菜单编辑器组件允许您创建和修改 Odoo 模块内的菜单和导航结构。您可以定义针对不同用户角色的菜单,并将它们组织起来以方便访问应用程序的各个部分。
-
视图和小部件:您可以使用视图和小部件自定义数据的显示方式。Odoo Web Studio 提供各种视图类型,如列表视图、表单视图和看板视图,您可以根据需要配置它们。
-
动作和触发器:动作和触发器允许您定义在特定事件或用户操作发生时应该发生什么。例如,您可以设置动作以发送电子邮件通知、更新记录或触发特定的工作流程。
-
访问控制:Odoo Web Studio 允许您为不同的用户角色设置权限和访问权限。您可以控制谁可以查看、编辑或删除记录,以及访问模块中的特定功能。
-
本地化支持:根据区域或行业特定要求自定义您的模块,包括税则、语言和会计标准。
-
数据导入和导出:启用数据导入和导出功能,以促进数据迁移和与外部系统的集成。
-
计划操作:您可以基于计划自动执行任务和操作,例如数据备份或自动电子邮件通知。
-
集成工具:Odoo Web Studio 提供工具,使您可以将自定义模块与其他 Odoo 模块或外部系统集成,确保无缝的数据交换和同步。
这是一个带有描述字段的默认列表视图:

图 25.8 – 默认列表视图
字段属性
在 Odoo Web Studio 中,当在数据模型中创建或自定义字段时,您有多种选项来配置和自定义这些字段以满足您的业务需求。以下是在使用 Odoo Web Studio 创建新字段时可用的一些常见选项:
-
字段名称:为您的字段提供一个描述性的名称,反映它将存储的数据类型。
-
字段类型:选择适合您字段的适当数据类型。Odoo 提供广泛的数据类型,包括文本、整数、浮点数、日期、日期时间、选择、多对一(与其他记录的关系)等。
-
必填字段:您可以设置字段为必填,这意味着在创建或编辑记录时,用户必须为此字段提供值。
-
默认值:为字段设置默认值。在创建新记录时,此值将预先填充。
-
只读:在此,您可以设置字段为只读,这样用户就不能编辑它。这对于一旦设置就不应修改的字段很有用。
-
帮助文本:添加一些帮助文本或描述,以提供有关字段或用户说明的附加信息。
-
占位符文本:对于文本或字符字段,您可以指定一个占位符文本,该文本将出现在输入字段中以指导用户。
-
验证约束:在此,您可以设置验证约束,例如字符限制、数字范围或文本字段的模式。
-
计算和默认函数:您可以定义计算函数,根据其他字段或条件计算字段的值。默认函数允许您设置动态默认值。
-
依赖关系:在此,您可以定义字段依赖关系,这些依赖关系基于其他字段的值确定字段何时可见或必填。
-
选择值:对于选择字段,指定用户可以选择的值列表。这通常用于下拉菜单等字段。
-
域过滤器:根据某些条件应用域过滤器,以限制许多一对一或多对多字段的可选值。
-
高级选项:Odoo Web Studio 还提供了一些高级选项,例如设置相关字段、指定更改动作或设置访问权限。
-
组和访问权限:配置哪些用户组可以查看或编辑此字段。您可以根据用户角色定义不同的访问权限。
-
计算字段:创建基于记录中其他字段的计算字段,以显示计算值。这些字段不存储数据,而是动态计算值。
-
小部件:选择不同的小部件来控制字段的显示方式,例如文本、选择、日期或颜色选择器小部件。
-
依赖于:定义字段依赖关系,指示哪些其他字段影响此字段的可见性或行为。
-
相关字段:创建相关字段以显示相关记录的信息。例如,您可以通过创建相关字段在发票上显示客户的姓名。
-
不可见或隐藏字段:使字段不可见或隐藏,以控制它们在表单中的可见性。
-
附件字段:配置字段以允许添加附件或上传文档或文件。
下面的截图显示了新字段:

图 25.9 – 新字段
下面是现有字段的截图:

图 25.10 – 现有字段
视图
在 Odoo Web Studio 中,视图是设计自定义模块用户界面的基本组件。视图决定了数据在 Odoo 应用程序中的显示和交互方式。在 Odoo Web Studio 中,你可以使用几种类型的视图来创建和自定义模块的用户界面:

图 25.11 – 视图
让我们看看一些常用的视图类型。
表单视图
表单视图允许用户查看和编辑单个记录。您可以通过添加、删除或重新排列字段来自定义表单视图的布局。此视图通常用于详细记录编辑:

图 25.12 – 表单视图
在 Odoo Web Studio 中,表单视图是设计自定义模块用户界面的关键组件。表单视图允许用户在应用程序中查看和编辑单个记录:

图 25.13 – 表单视图字段
我们可以使用前面截图中的字段创建表单视图。只需将字段拖放到表单视图中,即可创建我们想要显示的新字段:

图 25.14 – 表单视图的字段详情
这些是字段属性:

图 25.15 – 表单视图的字段属性
在这里,我们可以看到特定字段的视图选项:

图 25.16 – 各种表单视图字段视图选项
让我们看看表单视图的一些重要属性:
-
视图继承:在 Odoo Web Studio 中,您可以处理视图继承。这允许您基于现有的表单视图创建新的表单视图,并对它进行特定的修改或添加。这可以在创建类似视图时节省您的时间。
-
依赖关系:您可以在表单视图中配置字段依赖关系。例如,您可以根据其他字段中输入的值使某些字段可见或必填。
-
验证规则:表单视图可以设置验证规则以确保数据准确性。您可以对字段定义约束以控制输入数据。
-
保存和测试:当您对表单视图的设计满意时,保存您的更改。要测试表单视图,请转到使用该视图的应用或模块,创建或编辑记录,并观察您的表单视图如何显示和运行。
-
自定义操作:您还可以将自定义操作链接到表单视图中的按钮,使用户在交互记录时执行特定操作。
列表视图
列表视图以表格格式显示记录,这使得浏览和搜索多个记录变得容易。您可以通过选择要显示的字段、设置排序选项和添加筛选条件来自定义列表视图:

图 25.17 – 列表视图
当您在列表视图中点击一列时,您可以编辑该字段的属性。用户可以设置以下字段属性:
-
不可见
-
必填
-
只读
-
可选
-
标签
-
小部件
-
默认值
-
限制可见性到 组:

图 25.18 – 列表视图属性
在 Odoo Web Studio 中,列表视图是设计自定义模块用户界面的重要组件。以下是您如何在 Odoo Web Studio 中处理列表视图的方法:
-
创建新的列表视图:要创建新的列表视图,点击创建按钮。给它一个反映其在模块中用途或功能的名称。
-
设计列表视图:一旦创建了列表视图,您就可以开始设计它。
-
选择必要的字段:通过将它们从字段部分拖放到列表视图画布中,选择您想在列表视图中显示的字段。您可以按列排列这些字段。
-
列属性:点击每一列以访问其属性。您可以为每个列设置标签、格式化选项和排序行为。
-
排序和分组:配置在列表视图中记录的排序和分组方式。
-
筛选条件:添加筛选条件以根据特定条件限制在列表视图中显示的记录。
列表视图设置
点击列表视图本身以访问其设置。您可以配置各种方面,包括以下内容:
-
访问权限:定义哪些用户角色可以查看或访问此列表视图。
-
高级选项:指定列表视图在特定情况下应该是可见的、不可见的还是只读的。
-
组:为不同的用户组设置权限和访问权限。
-
视图继承:类似于表单视图,您还可以为列表视图处理视图继承。这允许您根据现有视图创建新的列表视图,并进行特定的修改或添加。
-
搜索和筛选:列表视图通常包括搜索和筛选功能,允许用户根据各种标准快速查找记录。
-
分组和总计:您可以在列表视图中根据特定字段启用记录的分组。此外,您还可以显示数值字段的总计和子总计。
-
批量操作:列表视图通常包括批量操作,允许用户同时对多个选定的记录执行操作,例如删除、存档或更新记录。
-
列可见性:用户通常可以自定义列表视图中列的可见性,根据他们的偏好显示或隐藏特定的列。
-
排序和分页:配置记录在列表视图中的排序和显示方式,包括升序或降序和分页选项。
看板视图
看板视图将记录可视化为卡片或瓷砖,因此它们通常用于管理任务或工作流程。您可以通过定义列和卡片的内容和外观来自定义看板视图:

图 25.19 – 看板视图
在 Odoo Web Studio 中,看板视图是设计将记录可视化为卡片或瓷砖的用户界面的有用组件。这些通常用于管理任务、工作流程或项目阶段。看板视图允许用户轻松跟踪记录在不同阶段中的进度。让我们学习如何在 Odoo Web Studio 中与看板视图一起工作。
访问看板视图
按照以下步骤操作:
-
要创建或自定义看板视图,请转到您 Odoo 实例中的工作室模块。
-
点击您想要创建或修改看板视图的应用程序或模块。
-
在左侧侧边栏中,您将找到一个视图部分,其中包含看板视图。点击看板视图以查看现有的看板视图或创建一个新的视图。
创建新的看板视图
要创建新的看板视图,请点击创建按钮。为看板视图提供一个名称,反映其在模块中的目的或功能。
设计看板视图
创建看板视图后,您可以开始设计它:
-
定义列:看板视图被组织成列,代表不同的阶段或类别。定义您工作流程所需的列。
-
添加卡片:将字段从字段部分拖放到看板视图中,以定义应在每个卡片上显示哪些信息。
-
配置卡片属性:点击每个卡片以访问其属性。您可以为每个卡片设置标签、格式化选项和排序行为。
看板视图设置
点击看板视图本身以访问其设置。您可以配置各种方面,包括以下内容:
-
访问权限:定义哪些用户角色可以查看或访问此看板视图
-
高级选项:指定看板视图在特定情况下应该是可见的、不可见的还是只读的
-
分组:为不同的用户组设置权限和访问权限
视图继承
与其他视图类型一样,您可以为看板视图使用视图继承。这允许您基于现有视图创建新的看板视图,并进行特定的修改或添加。
记录移动和操作
在看板视图中,记录通常可以从一列移动到另一列,以表示进度。您可以配置在记录移动或对卡片执行特定操作时发生的操作或触发器。
过滤和搜索
看板视图通常包括过滤和搜索功能,以帮助用户根据各种标准查找和组织卡片。
卡片颜色
您可以使用颜色编码来突出显示需要关注或具有特定属性的卡片或记录。
自定义操作
与其他视图一样,您可以在看板视图中将自定义操作链接到按钮或卡片交互。
日历视图
日历视图以日历格式显示具有日期字段的记录,这使得它适合调度和事件管理应用程序:

图 25.20 – 日历视图
在 Odoo Web Studio 中,日历视图是一个组件,允许您以日历格式展示与日期相关的记录。这种视图特别适用于涉及调度、事件、预约或任何可以与日期和时间关联的数据的应用程序。让我们学习如何在 Odoo Web Studio 中处理日历视图。
访问日历视图
按照以下步骤操作:
-
要创建或自定义日历视图,请转到您的 Odoo 实例中的Studio模块。
-
点击您想要创建或修改日历视图的应用程序或模块。
-
在左侧侧边栏中,您将找到一个视图部分,其中包含日历视图。点击日历视图以查看现有的日历视图或创建一个新的视图。
创建新的日历视图
要创建新的日历视图,请点击创建按钮。为日历视图提供一个名称,以反映其在模块中的目的或功能。
设计日历视图
一旦您创建了日历视图,您就可以开始设计它:
-
定义事件:日历视图通常表示与特定日期和时间相关的事件或记录。您可以选择数据模型中的哪些字段将在日历中显示,例如事件标题、开始和结束日期、描述等。
-
自定义事件外观:您可以根据日历中事件显示的方式进行配置,包括颜色、文本标签和工具提示。
日历视图设置
单击日历视图本身以访问其设置。您可以配置各种方面,包括以下内容:
-
访问权限:定义哪些用户角色可以查看或访问此日历视图
-
高级选项:指定在特定情况下日历视图应该是可见的、不可见的还是只读的
-
组别:为不同的用户组设置权限和访问权限
视图继承
与其他视图类型类似,您可以为日历视图使用视图继承。这允许您根据现有视图创建新的日历视图,并进行特定的修改或添加。
拖放交互
用户通常可以通过拖放事件来与日历交互,以重新安排或修改它们。
过滤和搜索
日历视图通常包括过滤和搜索功能,以帮助用户根据各种标准(如日期范围或事件类型)查找和组织事件。
事件详情
在日历视图中单击事件通常会显示有关事件的详细信息,使用户能够查看或编辑事件详情。
自定义操作
与其他视图一样,您可以将自定义操作链接到日历视图中的按钮或事件交互。
图形视图
图形视图允许您创建条形图、折线图和饼图,以根据所选字段可视化数据。这对于数据分析与报告很有用:

图 25.21 –图形视图
交叉视图
交叉视图提供了一种交互式分析数据的方法,通过根据所选字段汇总和总结记录。用户可以创建自定义报告并执行即席分析:

图 25.22 –交叉视图
搜索视图
搜索视图允许用户根据指定标准过滤记录。您可以通过定义搜索过滤器和过滤器组来自定义搜索视图:

图 25.23 –搜索视图
甘特视图
甘特视图用于项目管理并显示时间线上的任务或事件。用户可以使用此视图查看和管理项目进度:

图 25.24 –甘特视图
资源视图
资源视图用于资源管理和显示资源(例如,员工和机器)及其随时间的变化情况。
地图视图
地图视图在地图上显示带有地理信息的记录,使其适用于基于位置的应用程序。
活动视图
活动视图显示了与记录相关的活动时间线,帮助用户跟踪交互和历史记录:


构建新应用
在 Odoo Web Studio 中创建新应用涉及一系列步骤,以根据您的特定业务需求设计和配置数据模型、用户界面和功能。在此,我们将介绍您需要遵循的一般步骤来使用 Odoo Web Studio 构建新应用。
定义数据模型
在 Odoo Web Studio 中,您可以定义您应用程序的数据模型。这包括创建自定义对象(数据库表)以存储您的数据。
使用可视化界面添加字段、指定数据类型、设置默认值以及创建对象之间的关系:

图 25.26 – 定义数据模型
一旦您点击新建模型按钮,下一步就是指定模型的名称:

图 25.27 – 指定模型的名称
完成此操作后,您必须选择该模型的功能,然后点击创建 您的应用:

图 25.28 – 选择模型功能
到此为止,我们将有不同的选项来自定义应用:

图 25.29 – 各种模型选项
定义通用视图
如在视图菜谱中所述,我们必须通过点击VIEWS按钮来选择模型的视图:

图 25.30 – 视图选项
根据您的需求选择您希望使用的视图,并根据您的需求和功能添加字段。这些可以从左侧边栏中选择。
定义字段和组件
在表单视图中,我们可以从组件部分添加选项卡和列:

图 25.31 – 组件选项
一旦您将表单视图中的选项卡添加为一对多字段,您就可以编辑列表和表单视图,以及一对多字段本身:

图 25.32 – 选项卡选项
我们还可以根据字段类型设置小部件、域、限制对组的可见性、上下文等详细信息:

图 25.33 – 字段属性
文本(字符)
在 Odoo Web Studio 中,文本字段是一种常见的字段类型,用于存储和显示文本信息。文本字段功能多样,可以用来捕获各种类型的文本数据,如名称、描述、评论和笔记。
多行文本(文本)
在 Odoo Web Studio 中,多行文本字段允许用户输入和显示跨越多行或段落的文本。此类字段在需要捕获较长的描述、注释、笔记或任何超出单行文本的文本形式时非常有用。
整数(整数)
在 Odoo Web Studio 中,整数字段用于存储和显示整数(整数)值。整数字段常用于各种目的,例如计数、量化或捕获不需要小数点的数值数据。
小数(float)
在 Odoo Web Studio 中,小数字段用于存储和显示带有小数点或分数的数值。小数字段用途广泛,可以用于捕获和存储需要小数位精度数据。
HTML(html)
在 Odoo Web Studio 中,HTML 字段允许您在记录中存储和显示 HTML 格式的文本。此类字段在需要包含丰富文本、格式化描述或多媒体内容的应用程序中特别有用。
货币(monetary)
在 Odoo Web Studio 中,货币字段用于存储和显示货币值,例如货币金额。货币字段对于涉及财务交易、会计或需要处理与货币相关的数据的任何场景至关重要。
日期(date)
在 Odoo Web Studio 中,日期字段用于存储和显示日期值。日期字段对于涉及跟踪事件、安排和记录与各种记录相关的日期的应用程序至关重要。
日期和时间(datetime)
在 Odoo Web Studio 中,日期和时间字段用于存储和显示日期和时间值。此字段对于必须记录具有精确时间戳的事件、预约或交易的应用程序特别有用。
复选框(Boolean)
在 Odoo Web Studio 中,复选框字段用于捕获二进制或布尔值,这些值代表两种状态:选中(true)或未选中(false)。复选框字段常用于记录对问题或条件的肯定/否定、开启/关闭或真/假响应。
选择(selection)
在 Odoo Web Studio 中,选择字段用于向用户提供一个预定义的选项列表,用户可以从中选择单个值。此类字段常用于需要捕获具有有限选择集的分类或离散数据的情况。
文件(binary)
在 Odoo Web Studio 中,文件字段用于允许用户上传和存储文件,例如文档、图片、电子表格或任何其他类型的数字文件,在记录中。文件字段通常用于需要将文件与特定记录关联的情况,例如发票、合同或产品图片。
行(one2many)
在 Odoo Web Studio 中,行字段,也称为一对多字段,用于通过建立一对一多关系来在两个模型(数据库表)之间创建关系。它允许您将一个模型中的多个记录与另一个模型中的单个记录关联起来。行字段通常用于需要链接相关记录的场景,例如发票中的订单行或项目中的任务。
一对多(one2many)
在 Odoo Web Studio 中,一对多字段用于在两个模型(数据库表)之间建立一对一多关系,允许您将一个模型中的多个记录与另一个模型中的单个记录关联起来。一对多字段通常用于需要链接相关记录的场景,例如发票中的订单行、项目中的任务或销售订单中的产品。
多对一(many2one)
在 Odoo Web Studio 中,多对一字段用于在两个模型(数据库表)之间建立多对一关系,允许您将一个模型中的单个记录与另一个模型中的多个记录关联起来。多对一字段通常用于需要将记录链接到父记录或参考记录的场景,例如将产品链接到类别或将任务链接到项目。
多对多(many2many)
在 Odoo Web Studio 中,多对多字段用于在两个模型(数据库表)之间建立多对多关系,允许您将一个模型中的多个记录与另一个模型中的多个记录关联起来。多对多字段通常用于需要将多个记录相互链接的场景,例如用多个类别标记产品或将员工与多个技能关联起来。
图像(二进制)
在 Odoo Web Studio 中,图像字段用于允许用户在记录中上传和显示图像。图像字段通常用于需要将图像与特定记录关联的场景,例如产品图像、个人资料图片或与营销材料相关的图像。
标签(多对多)
在 Odoo Web Studio 中,标签字段用于允许用户为记录分配一个或多个标签或标签。标签是简短描述性标签,有助于根据特定标准或属性对记录进行分类和组织。标签字段通常用于需要实现灵活且用户驱动的分类系统的场景,例如用产品类别标记产品或用项目阶段标记任务。
优先级(选择)
在 Odoo Web Studio 中,优先级字段通常是一个选择字段,用于表示记录或任务的优先级或重要性级别。优先级字段通常用于项目管理、任务跟踪和问题跟踪等应用程序中,以帮助用户和团队优先处理工作。
签名(二进制)
在 Odoo Web Studio 中,签名字段允许用户在记录中捕获和存储数字签名。签名字段通常用于需要收集和验证签名作为工作流程或审批过程一部分的场景,例如签署文件、合同或交货确认。
相关字段(related)
在 Odoo Web Studio 中,相关字段是一种强大的字段类型,允许您在当前记录的表单视图中显示相关记录的数据,而无需在两个记录之间创建物理数据库链接。它通常用于您想显示与当前记录相关的另一个模型(数据库表)的信息时。
定义字段的计算方法
在 Odoo Web Studio 中,您可以使用 Python 方法定义计算字段,根据其他字段或数据动态计算其值。计算字段在您想在记录中显示计算或派生值时非常有用。
要定义字段的计算方法,您需要在与您的自定义模块关联的 Odoo 模型类中编写 Python 代码。让我们看看如何定义计算方法的基本示例。
使用代码定义计算方法
在这个例子中,我们创建了一个名为 computed_field 的计算字段,其值基于 field1 和 field2 计算。@api.depends 装饰器指定了当这些字段的值发生变化时触发计算的字段:
from odoo import models, fields, api
class YourModelName(models.Model):
_name = 'your.module.name'
_description = 'Your Module Description'
# Define the fields used in the computation
field1 = fields.Float('Field 1')
field2 = fields.Float('Field 2')
# Define the computed field
computed_field = fields.Float('Computed Field', compute='_compute_computed_field')
# Define the compute method
@api.depends('field1', 'field2')
def _compute_computed_field(self):
for record in self:
# Perform the computation and assign the result to the computed field
record.computed_field = record.field1 + record.field2
让我们看看如何使用 Odoo Web Studio 添加计算字段。
在这里,我们可以查看一个小型计算字段,用于计算销售订单行的总额。

图 25.34 – 销售订单
如以下截图所示,我们使用 Odoo Web Studio 添加了一个计算字段(总金额):

图 25.35 – 添加总金额浮点字段以编写计算方法的截图
点击 更多 按钮以查看字段的全部属性:

图 25.36 – 更多
完成此操作后,您将在 高级属性 下的 依赖项 和 计算 选项中看到:

图 25.37 – 高级属性区域
在 Odoo 中,计算字段是一种不存储在数据库中,而是基于其他字段或数据动态计算的字段。计算字段用于在您的记录中显示计算或派生值。它们在您需要对数据库记录中的字段执行计算或应用业务逻辑时特别有用。
只定义了以下预定义变量:
-
self(要计算的记录集)
-
datetime(Python 模块)
-
dateutil(Python 模块)
-
time(Python 模块)
其他功能可以通过 self 访问,例如 self.env。
因此,添加一些字段依赖关系,并在 计算 框中编写 Python 代码:

图 25.38 – 计算方法的代码
现在计算方法计算字段值并将它们存储在总计字段中:

图 25.39 – 总字段中的计算方法
添加按钮
在 Odoo Web Studio 中,您可以在自定义视图中添加按钮以触发 Odoo 应用程序中的特定操作或函数。按钮通常用于启动流程、验证数据或执行自定义操作。
要在视图中添加按钮,请点击 XML 部分并通过代码添加新按钮。请注意,按钮必须是动作类型按钮:

图 25.40 – 可以通过代码添加/修改任何内容的 XML 部分
一旦我们点击XML,编辑器将打开,以便我们可以通过代码进行修改或添加:

图 25.41 – 使用 XML 编辑器通过代码添加/修改任何内容
添加智能按钮
在 Odoo Web Studio 中,智能按钮是一个动态 UI 元素,显示汇总信息并提供快速访问相关记录。智能按钮通常用于显示相关记录的计数,例如与特定记录关联的订单、任务或潜在客户的数量,并允许用户通过单次点击导航到这些相关记录。
将鼠标光标悬停在右上角;一个+号将变得可见。您可以使用它来添加智能按钮:

图 25.42 – 添加智能按钮
一旦您点击+号,将打开一个名为添加按钮的新窗口。在这里,您可以添加标签并选择智能按钮的图标:

图 25.43 – 添加按钮选项
添加状态栏和过滤器
在 Odoo Web Studio 中,您可以创建和自定义状态栏和过滤器以增强用户体验并改进自定义视图的导航。状态栏通常显示有关当前记录或上下文的关键信息,而过滤器允许用户细化在列表或搜索视图中显示的记录:

图 25.44 – 添加管道状态栏
一旦您点击添加管道状态栏按钮,将打开一个窗口,您可以在其中添加状态栏选项:

图 25.45 – 状态栏属性
一旦您编辑并添加了状态栏的字段属性,点击确认。状态栏现在将在您的视图中可见:

图 25.46 – 添加的状态栏
过滤器
在 Odoo Web Studio 中,您可以通过创建和自定义过滤器来允许用户在列表视图和搜索视图中细化并过滤记录。过滤器在帮助用户在大量数据集中找到特定信息方面非常有价值:

图 25.47 – 过滤规则
以下截图显示了您必须根据需要配置的常见过滤规则。您还可以根据过滤规则自定义域:

图 25.48 – 添加新的过滤规则
编辑菜单
在 Odoo Web Studio 中,您可以通过添加、编辑或删除菜单项来自定义您的 Odoo 应用程序的菜单结构。这些菜单项允许用户访问应用程序的不同部分,如模块、视图和操作:

图 25.49 – 编辑菜单
一旦您点击编辑菜单按钮,将打开一个窗口,您可以在其中编辑菜单项:

图 25.50 – 编辑菜单项
点击您想要编辑的菜单项的编辑图标。在这里,您可以编辑以下内容:
-
名称:更改菜单项的显示名称。
-
操作:修改与菜单项关联的操作。操作定义了用户点击菜单项时会发生什么。您可以将特定的视图、操作或函数与菜单项关联。
-
父菜单:指定菜单项应出现的父菜单。这控制了菜单结构的层次和组织。
-
可见性:根据用户角色、组或条件定义菜单项的可见性。
-
图标:可选地,添加一个图标来表示菜单项。
-
顺序:调整菜单项在其父菜单中出现的顺序。
-
访问权限:配置菜单项的访问权限和权限,指定哪些用户角色可以查看或访问它。
自定义现有应用
在 Odoo Web Studio 中自定义现有应用涉及对应用的功能、视图和数据结构进行修改,以便与您的特定业务需求相一致。本食谱将涵盖您必须遵循的通用步骤,以在 Odoo Web Studio 中自定义现有应用:
注意
自定义现有应用通常需要开发者级别的访问权限或使用 Odoo Web Studio 进行简单的自定义。确保您拥有必要的权限和访问权限。

图 25.51 – 自定义现有应用
选择现有应用进行自定义
从主仪表板或菜单中选择您想要自定义的应用或模块。这可以是 Odoo 中的任何现有应用。在这里,您有多种选择:
-
自定义视图:使用 Odoo Web Studio 自定义应用的视图。您可以修改现有视图或创建新的视图以按您希望的方式显示数据。您可以在表单视图、列表视图、看板视图等中添加或删除字段、更改它们的标签和调整它们的位置。
-
添加或修改字段:向应用的数据模型添加新字段或修改现有字段。您可以使用 Odoo Web Studio 定义字段类型、标签、默认值和其他字段属性。
-
创建或编辑操作:定义应用的操作和工作流程。操作确定当用户执行特定操作(如点击按钮或菜单项)时会发生什么。您可以创建自定义操作或修改现有操作。
-
添加按钮和菜单项:通过添加按钮、菜单项和链接到应用的各个部分来自定义应用的菜单结构。这使用户能够轻松地在不同的视图和功能之间导航。
-
配置访问权限:为应用的视图、模型和操作设置访问权限和权限。定义谁可以查看、编辑或删除数据以及访问应用中的特定功能。
-
实现业务逻辑:使用 Odoo Web Studio 通过定义计算字段、服务器操作和其他规则来自定义实现业务逻辑,从而自动化应用内的流程和计算。
-
添加自定义报告:如有需要,使用 Odoo 的报表工具创建自定义报告和文档。定义报告模板和布局以生成诸如发票、采购订单和销售报价等文档。
内置函数
Odoo Web Studio 提供了一套内置功能和工具,允许用户在不需广泛编程或开发技能的情况下自定义、扩展和增强他们的 Odoo 应用。这些内置功能旨在简化应用自定义并赋予用户根据其特定业务需求调整 Odoo 实例的能力。
导入模块
一旦点击导入链接,将弹出一个窗口,您可以在其中上传模块的 ZIP 文件:

图 25.52 – 导入/导出选项截图
这就是导入模块选项的外观:

图 25.53 – 导入模块
上传您的模块文件 (.zip)并检查强制初始化。即使已安装,强制初始化模式也会更新‘noupdate == 1’记录:

图 25.54 – 成功导入模块
您上传模块文件并点击导入后,将得到一条消息,表明模块已成功导入:

图 25.55 – 导入的模块
前往应用列表并搜索Studio自定义模块。
导出模块
要从 Odoo 数据库导出模块,您必须安装 Odoo Studio 模块,然后在数据库中对其进行自定义。如果我们从 Odoo Web Studio 进行任何自定义,数据库中将创建一个新的模块:

图 25.56 – 导出选项
一旦点击导出链接,它将下载 Studio 自定义模块。您还可以将此模块导入到其他 Odoo 数据库中。
搜索视图
在 Odoo Web Studio 中,您可以自定义搜索视图,以调整用户在特定模块或应用程序中搜索和筛选记录的方式。搜索视图允许用户细化他们的搜索标准,使其更容易找到特定记录:

图 25.57 – 搜索过滤器
点击 Studio 图标,然后视图。选择搜索视图:

图 25.58 – 搜索视图
点击搜索视图后,将打开以下屏幕。在这里,您可以修改或添加搜索过滤器:

图 25.59 – 搜索视图过滤器
自动化
Odoo 提供自动化功能,以帮助简化业务流程并减少手动任务。这些自动化功能旨在使用户能够更容易地在他们的 Odoo 应用程序中配置和自定义自动化操作。
自动化操作
Odoo Studio 允许用户创建基于预定义条件或事件的自动化操作,这些操作可以触发特定任务。这些操作可以与各种 Odoo 模块相关联,包括创建记录、发送电子邮件、更新字段等操作。用户可以定义触发这些操作的条件,并指定条件满足时应发生什么。
计划中的操作
用户可以安排自动化操作在特定时间或间隔运行。这对于发送自动化提醒、生成报告或执行数据维护任务等任务非常有用。计划中的操作可以配置为每天、每周、每月或按自定义计划执行。
邮件自动化
Odoo Studio 允许用户自动化电子邮件通知和通信。用户可以为订单确认、发票生成或满足特定条件等事件设置自动化电子邮件触发器。电子邮件模板可以自定义以包含来自 Odoo 记录的动态数据。
服务器操作
服务器操作允许用户定义自定义 Python 代码或服务器端逻辑,这些代码或逻辑可以作为自动化操作的一部分执行。这为需要自定义编程的复杂自动化任务提供了高级定制选项:

图 25.60 – 自动化
一旦点击自动化链接,将出现以下屏幕。在这里,您可以添加新的自动化操作:

图 25.61 – 添加新的自动化
在执行自动化操作时,您可以选择多个选项:
-
执行 Python 代码
-
创建 新记录
-
更新 记录
-
执行 多个操作
-
发送电子邮件
-
添加关注者
-
创建 下一个活动
-
发送短信 文本消息
在这里,从待执行操作下拉菜单中选择执行 Python 代码来运行 Python 代码:

图 25.62 – 待执行操作
报告
在 Odoo Web Studio 中,您可以自定义报告的布局、内容和外观,以便它们满足您的业务需求。报告自定义允许您创建专业和品牌化的文档,例如发票、采购订单、报价等。
导航到 Odoo 中的Studio模块以访问报告自定义功能。
选择您想要自定义报告的应用程序或模块。通常,报告与特定模块相关联,例如销售、采购、库存或会计:

图 25.63 – 报告菜单
一旦您点击报告菜单,就会出现一个屏幕,您可以在其中选择现有模型报告或创建新报告:

图 25.64 – 报告
点击创建以为模型创建新的报告:

图 25.65 – 选择报告类型
Odoo Web Studio 允许您创建外部报告,也称为自定义报告,以便您可以在 Odoo 提供的标准内置报告之外生成文档和报告。外部报告可以高度自定义以满足特定的业务需求。
外部报告
这就是外部报告模板的外观:

图 25.66 – 一个外部报告模板
内部报告
要在 Odoo 中创建不带页眉和页脚的 PDF 报告,请选择内部报告模板:

图 25.67 – 内部报告模板
空白报告
要在 Odoo 中创建不带任何预定义结构的 PDF 报告,请选择空白报告模板:

图 25.68 – 一个空白报告模板
在 Odoo Web Studio 中,您可以根据布局、内容和外观自定义现有报告,以满足您的特定业务需求。这种自定义允许您调整 Odoo 提供的标准报告,以便它们与您的公司品牌和展示要求相匹配:

图 25.69 – 报价/订单销售报告
此外,还有一个XML编辑器选项,您可以通过代码设计报告的复杂部分:

图 25.70 – XML 选项
因此,点击XML通过代码自定义报告的设计:

图 25.71 – XML 编辑器
从选择中选取不同的报告模板:

图 25.72 – XML 编辑器 – 选择不同的报告
所有新创建的报告都将显示在报告下:

图 25.73 – 创建报告的截图
注意,您现在有打印报告的选项:

图 25.74 – 打印选项
模块
要从 Odoo 数据库导出模块,您必须安装 Odoo Studio 模块,然后开始自定义数据库。如果我们从工作室模块进行任何自定义,数据库中将创建一个新的模块:

图 25.75 – 导出模块
一旦点击导出链接,工作室自定义模块将被下载。您还可以将此模块导入到其他 Odoo 数据库中。
搜索视图
在 Odoo Web Studio 中,您可以自定义搜索视图,以调整用户在特定模块或应用程序中搜索和筛选记录的方式。搜索视图允许用户细化搜索条件,使其更容易找到特定记录:

图 25.76 – 搜索过滤器
点击工作室图标,然后视图。完成此操作后,选择搜索视图:

图 25.77 – 搜索视图
点击搜索视图后,将打开以下屏幕。在这里,您可以修改或添加搜索过滤器:

图 25.78 – 搜索视图过滤器
自动化
Odoo 提供自动化功能,以帮助简化业务流程并减少手动任务。这些自动化功能旨在使用户能够更容易地在他们的 Odoo 应用程序中配置和自定义自动化操作。
自动操作
Odoo Studio 允许用户创建基于预定义条件或事件的自动化操作,以触发特定任务。这些操作可以与各种 Odoo 模块相关联,包括创建记录、发送电子邮件、更新字段等操作。用户可以定义触发这些操作的条件,并指定条件满足时应发生什么。
定时操作
用户可以安排自动化操作在特定时间或间隔运行。这对于发送自动化提醒、生成报告或执行数据维护任务等任务很有用。定时操作可以配置为每天、每周、每月或按自定义计划执行。
邮件自动化
Odoo Studio 允许用户自动化电子邮件通知和通讯。用户可以为诸如订单确认、发票生成或满足特定条件等情况设置自动化的电子邮件触发器。电子邮件模板可以自定义,以包含来自 Odoo 记录的动态数据。
服务器操作
服务器操作允许用户定义自定义 Python 代码或服务器端逻辑,这些代码或逻辑可以作为自动化操作的一部分执行。这为需要自定义编程的复杂自动化任务提供了高级定制选项。

图 25.79 – 自动化截图
一旦点击自动化链接,它将打开屏幕以添加新的自动化操作。

图 25.80 – 添加新自动化截图
有多种选项可以进行自动化操作。
-
执行 Python 代码
-
创建新记录
-
更新记录
-
执行多个操作
-
发送电子邮件
-
添加关注者
-
创建下一活动
-
发送短信文本消息

图 25.81 – 要执行的操作截图



浙公网安备 33010602011771号