Alan Hou的个人博客

Odoo 17开发者指南第六章 管理模块数据

Odoo 中,管理模块数据涉及各种任务,例如安装、升级或移除模块时,在数据库中创建、更新或删除记录。这通常是通过称为数据文件的 XML 文件完成的。

本章我们将学习如何在模块安装期间提供数据。这样可以帮助我们提供元数据,如视图描述、菜单或动作,或者提供默认设置。另一个重要用途是提供演示数据,在创建数据库时选中加载演示数据复选框时,就会加载这些数据。

本章涵盖以下内容:

技术要求

学习本章要求已安装在线 Odoo 平台。

本章中使用的所有代码可以通过GitHub 仓库下载。

为了避免重复大量代码,我们将利用第四章 应用模型中定义的模型。这些示例可下载Chapter05/my_hostel目录中my_hostel模块的代码。

使用外部ID和命名空间

Odoo 中的记录使用外部 IDXML ID 进行标识。到目前为止,我们在本书中已在视图、菜单和动作等区域使用了 XML ID,但我们仍不清楚 XML ID 是什么。本教程将为读者提供进一步讲解。

如何操作…

我们将通过对已存在的记录进行写入操作来演示如何使用跨模块引用(cross-module references)

  1. 更新 my_hostel 模块的清单文件,注册数据文件如下:
  2. hostel.room 模型中创建一个新房间:
  3. 更改主公司(main company)的名称:

安装模块以应用更改。安装后,将创建一个名为 Hostel Room 01 的新房间记录,并且公司将被重命名为 Packt Publishing

工作原理…

XML ID 是一个字符串,用于引用数据库中的记录。这些 ID 本身是 ir.model.data 模型中的记录。该模型包括诸如声明 XML ID 的模块名称、ID 字符串、引用的模型以及引用的 ID 等信息。

每当我们对 <record> 标签使用 XML ID 时,Odoo 都会检查该字符串是否带有命名空间(即,是否恰好包含一个点),如果没有,它会添加当前的模块名称作为命名空间。然后,它会查找 ir.model.data 中是否已存在具有指定名称的记录。如果存在,则执行针对所列字段的 UPDATE 语句;如果不存在,则执行 CREATE 语句。这就是您可以在记录已存在时提供部分数据的方式,正如我们前面所做的。

在本教程的第一个示例中,记录的 IDhostel_room。由于它没有命名空间,最终的外部 ID 将具有类似于 my_hostel.hostel_room 的模块名称。然后,Odoo 将尝试查找 my_hostel.hostel_room 的记录。由于 Odoo 尚未有该外部 ID 的记录,它将在 hostel.room 模型中生成一条新记录。

在第二个示例中,我们使用了主公司的外部 ID,即 base.main_company。正如其命名空间所暗示的,它是从 base 模块加载的。由于该外部 ID 已经存在,Odoo 不会创建新记录,而是执行 写入(UPDATE)操作,以便公司名称更改为 Packt Publishing

重要提示

**部分数据(partial data)的一个广泛应用是,除了更改其他模块定义的记录之外,还可以使用快捷元素(shortcut element)**方便地创建记录,并对该记录写入快捷元素不支持的字段,例如:

<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 函数用于在系统内建立不同记录之间的关系。它允许您创建从一个记录到另一个记录的引用,通常使用 many2one 关系。

ref 函数(如本章**《使用 XML 文件加载数据》**教程中所用)在适用时也会将当前模块添加为命名空间,但如果生成的 XML ID 尚不存在,则会引发错误。这也适用于尚未命名空间的 id 属性。

如果您想查看所有外部标识符的列表,请启动**开发者模式(developer mode)**并打开菜单 “设置(Settings)”| “技术(Technical)”| “序列与标识符(Sequence & Identifiers)”| “外部标识符(External Identifiers)”

更多内容…

您迟早可能需要在 Python 代码中访问具有 XML ID 的记录。在这种情况下,请使用 self.env.ref() 函数。它会返回所引用记录的浏览记录(browse record)(记录集 recordset)。请注意,在这里您必须始终传递完整的 XML ID。一个完整的 XML ID 示例如下:<module_name>.<record_id>

您迟早可能需要使用 Python 代码检索具有 XML ID 的记录。在这种情况下,请使用 self.env.ref() 方法。这使您可以访问链接记录的浏览记录(记录集)。请记住,您必须始终在此处传递完整的 XML ID

您可以从用户界面看到任何记录的 XML ID。为此,您需要激活 Odoo 中的开发者模式;请参阅第 1 章《安装 Odoo 开发环境了解如何操作。激活开发者模式后,打开您想要查找 XML ID 的记录的表单视图(Form view)。您将在顶部栏中看到一个“臭虫(bug)”图标。从该菜单中,单击**“查看元数据(View Metadata)”**选项。请参阅以下截图作为参考:

图 6.1 – 打开记录元数据的菜单

另请参阅

请参阅本章的使用 noupdate 和 forcecreate 标记一节,了解为什么公司的名称只在模块安装期间更改。

使用 XML 文件加载数据

在前面的教程中,我们使用 hostel_room 外部标识符创建了新的房间记录。在本教程中,我们将从 XML 文件中添加不同类型的数据。我们将添加一个房间和一位作者作为演示数据。我们还将一个著名的出版商作为普通数据添加到我们的模块中。

如何操作…

按照以下步骤创建两个数据 XML 文件并将它们链接到您的 __manifest__.py 文件中:

  1. 将一个名为 data/demo.xml 的文件添加到您的清单文件的 demo 部分:
  2. 将以下内容添加到此文件:
  3. 将一个名为 data/data.xml 的文件添加到您的清单文件的 data 部分:

  4. 将以下 XML 内容添加到 data/data.xml 文件中:

现在,当您更新您的模块时,您会看到我们创建的出版商,并且如果您的数据库启用了演示数据(如第 3 章《创建 Odoo 插件模块》中所述),您还会找到这个房间及其成员。

工作原理…

数据 XML 文件使用 <record> 标签在数据库表中创建一行。<record> 标签有两个强制属性:idmodel。对于 id 属性,请参阅使用外部 ID 和命名空间一节;model 属性引用模型的 _name 属性。然后,我们使用 <field> 元素来填充数据库中的列,这些列由您命名的模型定义。模型还决定了哪些字段是强制填充的,并定义了默认值。在这种情况下,您不需要为这些字段显式地赋一个值。

有两种方法可以在模块清单中注册数据 XML 文件。第一种是通过 data 键,第二种是通过 demo 键。data 键中的 XML 文件在每次安装或更新模块时都会加载,而 demo 键中的 XML 文件在您为数据库启用了演示数据时加载。

步骤 1 中,我们在清单文件中使用 demo 键注册了一个数据 XML 文件。由于我们使用的是 demo 键,所以该 XML 文件仅在您为数据库启用了演示数据时才会加载。

步骤 2 中,在标量值的情况下,<field> 元素可以包含简单的文本值。如果您需要传递文件的内容(例如设置图像),请在 <field> 元素上使用 file 属性,并传递相对于插件路径的文件名。

要设置引用,有两种可能性。最简单的是使用 ref 属性,它适用于 many2one 字段,并且只包含要引用记录的 XML ID。对于 one2manymany2many 字段,我们需要使用 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(在不同的数据库中可能会不同)的情况下引用记录,如下所示:

重要提示

请注意,数据文件中的顺序很重要,数据文件中的记录只能引用列表中较早定义的数据文件中的记录。这就是为什么您应该始终检查您的模块是否可以在一个空数据库中安装,因为在开发过程中,您经常在各处添加记录,而后续定义的记录可能已经存在于数据库中,这来自较早的更新

演示数据始终在 data 键的文件之后加载,这就是此示例中的引用有效的原因。

更多内容…

虽然您基本上可以使用 record 元素做任何事情,但存在一些快捷元素(shortcut elements),使开发人员创建某些类型的记录更加方便。这些包括菜单项(menu items)、**模板(templates)和 act windows。请参阅第 9 章《后端视图(Backend Views)》第 14 章《CMS 网站开发(CMS Website Development)》**以获取更多信息。

field 元素还可以包含 function 元素,该元素调用模型上定义的函数来提供字段的值。请参阅**《从 XML 文件调用函数》**教程,了解我们简单地调用函数直接写入数据库,绕过加载机制的应用。

上述列表缺少 01 的条目,因为它们在加载数据时用处不大。为了完整性,它们如下所示:

这些语法与我们在**第 5 章《基本服务器端开发(Basic Server-Side Development)》《创建新记录(Creating new records)》《更新记录值(Updating values of records)》**教程中解释的语法相同。

使用 noupdate 和 forcecreate 标记

大多数插件模块都有不同类型的数据。有些数据只是模块正常工作所必需的,有些数据不应被用户更改,而大多数数据都可以根据用户的需要进行更改,并且仅作为方便而提供。本教程将详细说明如何处理不同类型的数据。首先,我们将在一个已存在的记录中写入一个字段,然后我们将创建一个在模块更新期间应该重新创建的记录。

如何操作…

我们可以通过在封闭的 <odoo> 元素或 <record> 元素本身上设置某些属性,来强制 Odoo 在加载数据时执行不同的行为:

  1. 添加一个将在安装时创建但在后续更新时不会更新的出版商。但是,如果用户删除它,它将被重新创建:
  2. 添加一个在插件更新期间不会更改,并且如果用户删除它也不会重新创建的房间类别

工作原理…

 

<odoo> 元素可以有一个 noupdate 属性,它会传播到在首次读取封闭的数据记录时创建的 ir.model.data 记录,从而最终作为该表中的一列。

Odoo 安装一个插件(称为 init 模式)时,无论 noupdatetrue 还是 false,所有记录都会被写入。当您更新一个插件(称为 update 模式)时,会检查现有的 XML ID 是否设置了 noupdate 标志,如果是,则尝试写入此 XML ID 的元素将被忽略。如果相关记录被用户删除,则情况并非如此,这就是为什么您可以通过将记录上的 forcecreate 标志设置为 false,来强制在 update 模式下不重新创建 noupdate 记录。

重要提示

在旧版插件(8.0 版及以前,含 8.0 版)中,您经常会发现一个 <openerp> 元素封闭着一个 <data> 元素,其中包含 <record> 和其他元素。这仍然是可能的,但已被弃用。现在,<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)功能时,您得到的就是它。在本教程中,我们将看看如何导入表格状数据(table-like data)

准备工作

 

传统上,Odoo 中的访问控制列表(Access-Control Lists,ACLs)用于管理记录和操作的访问权限。 ACLs 使用预定义的规则指定谁可以对指定条目执行特定动作(例如,读取、写入、创建和删除)。 ACLs 通常通过 XML 文件在 Odoo 模块中定义。有关 ACLs 的更多详细信息,请查看第 10 章 安全访问中的 ACLs 教程。

security/ir.model.access.csv 添加到您的数据文件中:

  1. ir.model.access CSV 文件中向模块添加访问安全:

我们现在有一个 ACL,允许旅舍管理员读取房间记录,并且还允许他们编辑创建删除它们。

工作原理…

您只需将所有数据文件放入清单的 data 列表中。 Odoo 将使用文件扩展名来决定它是哪种类型的文件。 CSV 文件的一个特点是它们的文件名必须与要导入的模型的名称匹配——在我们的例子中是 ir.model.access。第一行必须是标题行(header),其中的列名必须与模型的字段名称完全匹配

对于标量值(scalar values),您可以使用带引号(如果必要,因为字符串包含引号或逗号本身)或不带引号的字符串。

当使用 CSV 文件写入 many2one 字段时,Odoo 首先尝试将列值解释为 XML ID。如果没有点,Odoo 会添加当前模块名称作为命名空间,并在 ir.model.data 中查找结果。如果失败,则使用列值作为参数调用模型的 name_search 函数,并以返回的第一个结果为准。如果这也失败了,则该行被视为无效,Odoo 会引发错误。

插件更新和数据迁移

 

您在编写插件模块时选择的数据模型(data model)可能会暴露出一些弱点,因此您可能需要在插件的生命周期内对其进行调整。为了允许这样做而无需大量的黑客手段,Odoo 支持插件模块的版本控制(versioning)并在必要时运行迁移(migrations)

如何操作…

我们假设在我们的模块的早期版本中,allocation_date 字段是一个字符字段(character field),人们在其中写入他们认为合适的任何日期。我们现在意识到我们需要此字段进行比较和聚合,这就是我们想要将其类型更改为 Date 的原因。

Odoo 在类型转换方面做得很好,但在这种情况下,我们需要自己处理,这就是为什么我们需要提供有关如何转换安装了我们模块的先前版本(当前版本可以运行)的数据库的说明。让我们尝试以下步骤:

  1. 在您的 __manifest__.py 文件中提升版本:
  2. migrations/17.0.1.0.1/pre-migrate.py 中提供预迁移(pre-migration)代码:
  3. migrations/17.0.1.0.1/post-migrate.py 中提供后迁移(post-migration)代码:

如果没有这段代码,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 文件夹,查看它是否包含中间版本的文件夹,直到(包括)当前更新的版本。

然后,在找到的文件夹中,Odoo 会搜索名称以 pre- 开头的 Python 文件,加载它们,并期望它们定义一个名为 migrate 的函数,该函数有两个参数。此函数被调用时,第一个参数是数据库游标(database cursor),第二个参数是当前安装的版本。这发生在 Odoo 查看插件定义的其余代码之前,因此您可以假设您的数据库布局与先前版本相比没有变化。

在所有 pre-migrate 函数成功运行后,Odoo 会加载插件中声明的模型和数据,这可能会导致数据库布局发生变化。鉴于我们在 pre-migrate.py 中重命名了 allocation_dateOdoo 将只创建一个具有该名称但具有正确数据类型的新列。

之后,使用相同的搜索算法,将搜索 post-migrate 文件并在找到时执行。在我们的例子中,我们需要查看每个值,看我们是否可以从中提取出一些可用的东西;否则,我们将数据保留为 NULL。除非绝对必要,否则不要编写迭代整个表的脚本;在这种情况下,我们本可以编写一个非常大、难以阅读的 SQL switch

重要提示

如果您只想重命名列,则不需要迁移脚本。在这种情况下,您可以将相关字段的 oldname 参数设置为该字段的原始列名;然后 Odoo 会自己处理重命名。

更多内容…

在预迁移和后迁移步骤中,您都只能访问游标(cursor),如果您习惯于 Odoo 环境(Odoo environments),这不是很方便。在此阶段使用模型可能会导致意外结果,因为在预迁移步骤中,插件的模型尚未加载,并且在后迁移步骤中,依赖于当前插件的插件所定义的模型也尚未加载。但是,如果这对您来说不是问题(要么是因为您想使用您的插件未触及的模型,要么是您知道此问题不会影响的模型),您可以通过编写以下代码来创建您习惯的环境:

另请参阅

在编写迁移脚本时,您经常会遇到重复性任务,例如检查列或表是否存在、重命名事物或将一些旧值映射到新值。在这里重新发明轮子是令人沮丧且容易出错的;如果您可以承受额外的依赖,请考虑使用 https://github.com/OCA/openupgradelib

从XML文件中删除记录

在前面的教程中,我们学习了如何从 XML 文件生成更改记录。您可能偶尔希望从**依赖模块(dependent module)**中删除已创建的记录。可以使用 <delete> 标签。

准备工作

在本教程中,我们将从 XML 文件中添加一些类别,然后将其删除。在实际情况中,您将从另一个模块创建此记录。但为了简单起见,我们将向同一个 XML 文件添加一些类别,如下所示:

如何操作…

有两种方法可以从 XML 文件中删除记录:

  1. 使用先前创建记录的 XML ID
  2. 使用搜索域(search domain)

工作原理…

您需要使用 <delete> 标签。要从模型中删除记录,您需要在 model 属性中提供模型的名称。这是一个强制属性

在第一种方法中,必须提供从另一个模块的数据文件生成的记录的 XML IDOdoo 将在安装模块时查找该记录。如果指定的 XML ID 匹配一条记录,则该记录将被删除;否则,将引发错误。只有XML 文件生成的记录(或具有 XML ID 的记录)才能被删除。

在第二种方法中,您需要在 search 属性中传递域(domain)。在模块安装期间,Odoo 将通过此域搜索记录。如果找到记录,它将删除它们。如果给定域不匹配任何记录,此选项不会引发错误。使用此选项时要格外小心,因为它可能会删除用户的数据,因为搜索选项会删除所有匹配该域的记录。

警告

<delete>Odoo 中很少使用,因为它很危险。如果您不小心,可能会破坏系统。如果可能,请避免使用它。

在XML文件中调用函数

您可以从 XML 文件创建所有类型的记录,但有时难以生成包含某些业务逻辑的数据。您可能希望在用户在生产环境中安装依赖模块时修改记录。在这种情况下,您可以通过 <function> 标签调用模型的方法(method)

如何操作…

对于本教程,我们将使用上一个教程中的代码。作为一个示例,我们将把现有房间的价格增加 10 USD。请注意,您可能会根据公司配置使用其他货币。

按照以下步骤从 XML 文件中调用 Python 方法:

  1. _update_room_price() 方法添加到 hostel.room 模型:
  2. <function> 添加到数据 XML 文件中:

工作原理…

步骤 1 中,我们添加了 _update_room_price() 方法,该方法搜索所有房间并将价格增加 10 USD。我们将方法名称以 _ 开头,因为 ORM 将其视为私有方法,不能通过 RPC 调用。

步骤 2 中,我们使用了带有两个属性的 <function> 标签:

当您安装此模块时,将调用 _update_room_price(),并且房间的价格将增加 10 USD

重要提示

始终将此函数与 noupdate 选项一起使用。否则,它将在您每次更新模块时被调用。

更多内容…

使用 <function>,也可以向函数发送参数。假设您只想增加特定类别房间的价格,并且希望将该金额作为参数发送。

为此,您需要创建一个接受类别作为参数的方法,如下所示:

要将类别和金额作为参数传递,您需要使用 eval 属性,如下所示:


当您安装模块时,它将把给定类别的房间价格增加 20 USD
退出移动版