Alan Hou的个人博客

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

全书完整目录请见:Odoo 14开发者指南(Cookbook)第四版

本章中,我们将学习如何添加在安装时可提供数据的插件模块。这对于提供默认值以及添加视图描述、菜单或动作等元数据都非常有用。另一个重要的用途是提供演示数据,勾选了加载演示数据复选框时会在数据库创建的同时载入数据。

本章中,我们将讲解如下小节:

技术准备

本章的技术要求有在线的Odoo平台。

本章中的所有代码可通过GitHub代码仓库进行下载:https://github.com/alanhou/odoo14-cookbook

为避免重复输入大量代码,我们使用第四章 应用模型中所定义的模型。想按照下面的示例进行操作的话,请确保先从Chapter04/06_hierarchy_model中获取my_library模块的代码

使用外部ID和命名空间

Odoo中的外部ID 或XML ID用于标识记录。本书中到此为上,我们在视图、菜单和动作等区域使用了XML ID。但并没有学习XML ID实际是什么。这一节会带给你更深入的理解。

如何实现…

我们将在已有记录中进行写入来演示如何使用跨模块引用:

  1. 更新my_library模块的声明文件,注册数据文件如下:
  2. 在http://library.book/模型中新建一本书:
  3. 修改主公司的名称:

安装该模块来应用修改。在安装后,会创建一条新的图书记录Odoo 14 Development Cookbook 并会将公司重命名为Packt Publishing。

译者注:公司名称可点击 Settings 进行查看,仅执行更新仅会添加图书记录,修改公司的名称要求重新安装该模块

运行原理…

XML ID是一个引用数据库中某记录的字符串。ID本身是ir.model.data模型中的记录。这一模型包含声明XML ID的模块名、ID字符串、所引用模型和所引用ID等。

每次在<record>标签中使用XML ID时,Odoo会检查该字符串是否带有命名空间(即它是否只包含一个点标记),如果不带有,会添加当前模块名作为命名空间。然后,它查找ir.model.data中是否有指定名称的记录。若有,所列出字段会执行一个UPDATE语句。若没有,则执行一条CREATE语句。这样就可以像前面那样在记录已存在时添加部分数据。

本节中的第一个示例中,记录有一个 ID book_cookbook。因其不带有命名空间,最终的外部ID模块名为:my_library.book_cookbook。然后Odoo会尝试查找my_library.book_cookbook记录。因为Odoo还没有这个外部ID的记录,它会在library.book模型中生成一条新记录。

第二个示例中,我们使用了主公司的外部ID,即base.main_company。命名空间表明,它从base模块中进行的加载。因这个外部ID已存在,Odoo不会创建一条记录,而是执行write (UPDATE)操作,这样公司名就会变为Packt Publishing。

 

📝重要提示

除由其它模块定义的记录修改外,部分数据的一个广泛应用是使用快捷元素来以便捷的方式来创建记录并在记录中写入字段,这个快捷元素并不提供支持:

ref函数,在本章中使用XML文件加载数据一节中也会使用到,如果符合条件的话会以命名空间添加当前模块,但如果生成的XML ID尚不存在的话会抛出错误。如果没添加命名空间对于 id 属性也同样如此。

📝小贴士:如果希望查看所有外部标识符的列表的话,启动开发者模式,然后打开菜单访问Settings > Technical > Sequence & Identifiers > External Identifiers。

扩展知识…

迟早你会需要在Python代码中通过XML ID访问记录。这时使用self.env.ref()函数。它返回所引用记录的浏览记录(记录集)。注意在这里要保持传入完整的XML ID。一个完整XML ID的示例为:<module_name>.<record_id>。

可以在用户界面中看到任意记录的XML ID。这要求启动Odoo的开发者模式。

参见第一章 安装Odoo开发环境来在 Odoo 中启动开发者模式。在激活了开发者模式之后,打开你希望查看XML ID的记录的表单视图。会在顶栏中看到一个调试图标。在该菜单中,点击View Metadata选项。参见如下截图:

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

其它内容

参见本章中使用noupdate和forcecreate标记一节来了解为何公司名仅在模块安装时才会进行修改。

使用XML文件加载数据

上一节中,我们使用外部标识符book_cookbook新建了一条图书记录。本节中,我们会通过XML文件来添加另一种类型的数据。我们添加一条书和作者的演示数据。还会添加在模块中添加知名出版社作为常规数据。

如何实现…

按照如下步骤创建两个XML文件并在__manifest__.py文件中进行关联。

  1. 在你的声明文件中的demo版块中添加一个名为 data/demo.xml的文件:
  2. 在该文件中添加如下内容:
  3. 在声明文件的data版块中添加一个名为data/data.xml 的文件:
  4. 在data/data.xml 文件中加入如下 XML 内容:

此时若更新该模块,会看到我们所创建的出版社,并且如果像第三章 创建Odoo插件模块中所指出的那样启用了演示数据,还可以看到这本书以及其作者。

运行原理…

XML数据文件使用<record>标签来创建数据表中一行。<record>标签有两个必填属性:id和model。对于 id 属性,参见使用外部ID和命名空间一节,model属性引用模型的_name属性。然后,使用<field>元素来填入数据库中列,这在你所命名的模型中定义。模型还决定哪些字段是必填的并且定义了默认值。在这种情况下,无需显式地给这些字段赋值。

有两种方式在模块的声明文件中注册XML文件。一种是通过data键,另一种是通过demo键。data键中的数据文件在每次安装或更新模块时进行加载。而demo键中的XML文件仅在对数据库启用了演示数据时才会加载。

第1步中,我们使用了demo键在声明文件中注册了XML数据文件。因为使用的是demo键,XML文件仅在对数据库启用了演示数据时才会加载。

在第2步中,<field>元素可以在为标量值时可包含简单文本值。如果需要传递一个文件的内容(例如设置图片时),使用<field>元素的file属性并传递相对插件路径的文件名。

设置引用有两种方式。最简单的一种是使用ref属性,可用于many2one字段,仅需包含所要引用记录的XML ID。对于one2many和many2many字段,我们需要使用eval属性。这是一种通用目的属性,可用于运行Python代码来作为字段值使用,例如使用strftime(‘%Y-01-01’)来填充date字段。X2many中应使用三个元素元组列表,元组的第一个值决定所要采取的操作。在eval属性内,我们可以访问一个名为ref的函数,它返回以字符串传入的XML ID 对应的数据库ID。这让我们可以无需知道不同数据库中可能不同的原始 ID 即可引用记录,如下所示:

第3和第4步同前两步,只是使用data键来取代了demo键。这表示每次安装或升级模块时都会加载这些XML文件。

📝重要:注意数据文件中的顺序很重要,并且数据文件中的记录仅能引用该文件之前列表中数据文件所定义的记录。这也是为什么需要保持查看模块是否在空数据库中安装,因为在开发过程中,会经常在各处添加记录,能够使用的前提是后续所定义记录通过前几次更新已存在于数据库中。

演示数据总是会通过data键文件之后进行载入,这也是本例中的引用可以生效的原因。

扩展知识…

虽然基本上你可以对记录元素做任何操作,开发人员可以使用一些快捷元素来便捷地创建某种类型的记录。这包含菜单项、模板和动作窗口。参见第九章 后端视图第十四章 CMS网站开发来获取更多相关信息。

字段元素还可以包含function元素,它调用模型中的方法来提供字段值。参见在XML文件中调用函数一节,其中我们仅通过调用函数来在应用中直接绕过加载机制并写入数据库,

前述列表中没有包含0和1,因为在载入数据时这两者并没有什么用处。为保持完整性,说明如下:

这些语法与我们在第五章 基本服务端开发新建记录更新记录集中记录值小节中所讲解的相同。

使用noupdate和forcecreate标记

大部分的插件模块拥有不同类型的数据。有些数据只要存在模块就可正常运作,另一些数据不可由用户修改,大部分数据都可以供用户按需修改,仅出于方便目的予以提供。本节会详细讲解这些不同类型的数据。首先,我们将在已有记录中写入一个字段,然后我们会创建一条在模块更新时会重新创建的记录。

如何实现…

我们可以在加载数据时设置<odoo>元素或<record>元素自身的某些属性来对Odoo施加不同的行为,

  1. 添加在安装时会创建但后续升级中不更新的出版社。但在用户删除它时会被重新创建:
  2. 添加一个在插件更新时不会修改且用户删除后不会重建的图书分类:

运行原理…

<odoo>元素可包含一个noupdate属性,在 ir.model.data记录中由第一次读取所包含的数据记录创建,因此成为数据表中的一个字段。

在Odoo安装插件时(称为 init 模式),不论noupdate为true或false都会写入所有记录。在更新插件时(称为update模式),会查看已有的XML ID来确定是否设置了noupdate标记,如是,则会忽略准备写入到该XML ID 的元素。在用户删除该记录时则并非如此,因此可以通过在update模式下设置记录的forcecreate标记为false来强制不重建noupdate记录。

📝重要:在老版本的插件中(版本8.0及以前),经常会发现<openerp>元素中包含一个<data>元素,其中又包含<record>及其它元素。这仍然可用,但已被弃用。现在,<odoo>, <openerp>和<data>的语法完全一致,它们作为XML数据的一个包裹。

扩展知识…

如果在使用noupdate标记时依然想要加载记录,可以在运行Odoo服务时带上–init=your_addon或-i your_addon参数。这会强制Odoo重新加载记录。还会重建已删除的记录。注意如果模块绕过了XML ID机制的话这可能会导致重复记录以及关联安装出错,例如在用<function>标签调用Python代码来创建记录时。

通过这一代码,可以绕过noupdate标记,但首先请确保这确实是你所需要的。另一个解决这一场景的方案是编写一个迁移脚本,参见插件更新和数据迁移一节。

其它内容

Odoo还使用XML ID来追踪插件升级后所删除的数据。如果记录在更新前通过模块命名空间获取到一个XML ID,但在更新时重置了该XML ID,记录会因被视作已过期而从数据库中删除。有关这一机制更深入的讨论,请见插件更新和数据迁移一节。

使用CSV文件加载数据

虽然可以通过XML文件来进行所需任意操作,但在提供大量数据时这种格式并不是最方便的,尤其是现在很多人都习惯于使用Calc或其它数据表软件处理数据。CSV格式的另一个优势在于它是使用标准导出函数导出的数据格式。本节中,我们来学习导入表格类数据。

如何实现…

最初,访问控制列表(ACL,参见第十章 权限安全)是一种通过CSV文件加载的数据。

  1. 在数据文件中添加security/ir.model.access.csv:
  2. 在这个文件中添加我们图书的ACL(我们已经通过第三章 创建Odoo插件模块添加访问权限一节添加了一些记录):

现在我们有了一个ACL,它允许普通用户读取图书记录,但无法编辑、创建或删除这些记录。

运行原理…

仅需将所有数据文件放到声明文件的数据列表中。Odoo会通过文件扩展名来确定其文件类型。CSV文件的一个特别之处在于它必须匹配所导入的模型名称,本例中,该模型为 ir.model.access。第一行应为精确匹配模型字段名称的列名的头部。

对于标量值,可以使用带引号(如需要,因字符串自身包含引号或逗号)或不带引号的字符串。

在通过CSV文件编写many2one字段时,Odoo首先尝试将字段值解释为XML ID。如无点号,Odoo会将当前模块名添加为命名空间,并在 ir.model.data中查找结果。如若失败,会使用字段值来作为参数调用模型的name_search函数,获取第一条返回的结果。如依然失败,该行被视为无效,Odoo抛出错误。

📝重要:注意从CSV中所读取的数据保持为noupdate=False,并且没有快捷的修改方式。这表示所有后续的插件更新会一直重写由用户所做的修改。

如果需要加载大量的数据并且noupdate对你来说不是问题,可以在init钩子中加载CSV文件。

扩展知识…

也可通过CSV文件来导入one2many和many2many字段,但会有些麻烦。通常,最好是分别创建记录然后在XML文件中设置关联,或者通过另一个CSV文件来设置关联。

如果确需在同一个文件中创建关联记录,对数据列进行排序来让标量字段居左、关联模型字段居右,列头包含待关联字段名及所关联的模型字段,由冒号分隔:

这会创建一个名为my group的组,可以通过在组记录的右侧添加列来写入更多的字段。如果需要关联多条记录,重复该行并修改右侧列为对应值。因Odoo会为空列填写前一行的值,所以无需拷贝所有数据,添加一行时,对想要填充值的关联模型字段以外的字段只需使用空值。

对于x2m字段,只需要列出要关联记录的XML ID。

插件更新和数据迁移

在编写插件模块时所选择的数据模型可能会存在一些问题,因此会需要在插件模块的使用过程中对其进行调整。为允许这一操作而又无需过多技巧,Odoo支持插件模块中使用版本号并在需要时执行迁移。

如何实现…

我们假定在模块的早前版本中date_release字段是一个字符字段,人们可以填写任意他们认为是日期的字段。现在我们意识到需要对这一字段进行比较和聚合运算,因此需要将它的类型修改为Date。

Odoo在类型转换上做了很大的优化,但在这种情况下得靠我们自己了,因此我们需要给出指令来对已安装在数据库中的早前版本进行转换来让当前版本可以运行。按照如下步骤进行操作:

  1. def migrate(cr, version):

        cr.execute(‘ALTER TABLE library_book RENAME COLUMN date_release

                                                  TO date_release_char’)

  1. 提升__manifest__.py文件中的版本号:
  2. 在migrations/14.0.1.0.1/pre-migrate.py中提供预迁移代码:
  3. 在migrations/14.0.1.0.1/post-migrate.py中添加一个迁移后代码:

没有这代码,Odoo会将原来的date_release列重命名为date_release_moved并新建一列,因为没有字符字段对日期字段的直接自动转换。从用户的角度看,date_release的数据会消失。

运行原理…

第一个重要的点是在插件中增加版本号,因为迁移仅在不同版本间进行。在每次更新期间,Odoo在更新时将声明文件中的版本号写入到ir_module_module表中。如果版本号小于或等于3部分时,前缀使用Odoo的大版本和小版本号。前例中,我们显式地使用了Odoo的大小版本号,这是一种良好实践,但1.0.1也可以达到同样的效果,因为在内部,Odoo会为短版本号添加其大版本和小版本号。通常,使用长标记是一种不错的做法,因为很容易地看出它是针对Odoo的哪一个版本的。

两个迁移文件是无需在任何地方注册的代码文件。在更新插件时,Odoo对比在ir_module_module中记录的版本号与插件声明文件中所添加的版本号。如果声明文件的版本号更高(在添加了 Odoo 的大版本和小版本之后),会搜索这一插件的migrations文件夹,查看它是否包含带有范围内版本号的文件夹,包含当前更新的版本号。

然后,在查找到的文件夹内,Odoo搜索以pre-开头的Python文件,加载它们,并预设其中定义了名为migrate的函数,该函数有两个参数。此函数调用时以数据库游标作为第一个参数以及当前安装的版本号作为第二参数。这一时间在Odoo查找插件定义的其它代码之前,因此你可以假定你的数据库结构对比此前版本没有做过任何修改。

在所有预迁移函数成功调用之后,Odoo加载模型以及插件中所定义的数据,这会导致数据库结构的变化。例如我们在pre-migrate.py文件中重命名了date_release,Odoo会以正确的数据类型使用该名称新建一列。

此后,通过同样的搜索算法,会搜索post-migrate文件并在找到时进行执行。本例中,我们需要查看所有的值来了解我们是否能借助它让一些数据可以使用,否则我们会保持数据为NULL。除非绝对必要不要编写遍历整表的脚本,在这种情况下,我们可能会编写一个很大且可读性差的SQL的switch语句。

📝重要贴士:如果你仅仅是想要重命名一列,则无需编写迁移脚本。此时可以将oldname参数设置为需修改的字段原列名,然后Odoo会自己处理重命名。

扩展知识…

在预迁移和迁移后的步骤中,仅能访问到游标,如果你习惯于使用Odoo环境则不是很方便。它可能会导致在这一阶段使用模型预料外的结果,因为在预迁移步骤中,插件模型尚未被载入,同时,在迁移后步骤中,由依赖当前插件的插件定义的模型也还未被加载。但是,如果这对于你来说不是问题,也许是因为你想要使用你的插件所不涉及的模型或者是你已知这不会是一个问题的模型,那么就可以编写如下代码创建一个习惯的环境:

其它内容

在编写迁移脚本时,常常会碰到重复的任务,比如查某数据列或数据表是否存在、重命名或映射一些旧的值到新值。重复造轮子可能会容易产生问题并让人沮丧,如果可以接受额外依赖的话考虑使用https://github.com/OCA/openupgradelib。

从XML文件中删除记录

在前面的小节中,我们学习了如何通过XML文件创建或更新记录。有时,对依赖的模块,会希望要删除此前创建的记录。这可以通过<delete>标签实现。

准备工作

在这一节中,我们将通过XML文件添加一些分类,然后删除它们。在真实场景中,会从另一个模块中创建这一记录。但为进行简化,我们将在相同的 XML 文件中添加一些分类,如下:

如何实现…

有两种方式从XML文件中删除记录:

运行原理…

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

在第一个方法中,需要提供此前从另一个模块数据文件中创建记录的XML ID。在模块的安装过程中,Odoo会尝试查找该记录。如果以给定XML ID查找到了记录,会删除该记录,否则会抛出一个错误。可以仅删除通过XML文件创建的记录(或带有XML ID的记录)。

在第二个方法中,需要在domain属性中传递作用域。在模块的安装过程中,Odoo会通过该域搜索记录。如果查找到记录,则进行删除。如果给定域没有匹配到任何记录的话该选项不会抛出异常。使用该选项时要极其小心,因为搜索选项会删除所有匹配域的记录,而导致它会删除用户的数据。

⚠️警告:在Odoo很少使用<delete>,因为它很危险。如果使用不慎,可能会导致系统崩溃。尽可能避免使用它。

在XML文件中调用函数

可以通过XML文件创建各种类型的记录,但有时生成包含一些业务逻辑的数据会很困难。你可能会希望在用户在生产环境中安装依赖模块时修改记录。例如,假设你想要创建一个模块来在线展示书籍。my_library模块已有图片封面字段。设想在新的模块中你实现了缩减减图像大小以及在新的缩略图字段中存储它的逻辑。现在在用户安装这一模块时,可能已有图书和图像了。不太可能在XML文件中通过<record>标签生成缩略图。在这种情况下,可以通过<function>标签调用模型方法。

译者注:本节中如想反复测试,请先将 noupdate 删除或置为0

此外对于部分设置了 noupdate的, 如想继续测试更新可访问Technical > Sequences and Identifiers > External Identifiers,找到对应的记录,取消 noupdate 的勾选再次测试。

如何实现…

这一节中,我们将使用前一小节的代码。作为示例,我们将已有图书价格增加$10 USD。注意按照公司配置你可能使用的是其它币种。

按照如下步骤通过XML文件调用Python方法:

  1. 在library.book模型中添加_update_book_price()方法:
  2. 在数据XML文件中添加<function>:

运行原理…

在第一步中,我们添加了_update_book_price()方法,它搜索所有图书并将它们的价格增加$10 USD。我们在方法名前加上下划线_,是因为它被ORM视作私有,不允许通过RPC调用。

在第二步中,我们使用了<function>标签并添加了两个属性:

在安装这一模块时,_update_book_price()会被调用并且书籍的价格会被加上$10。

📝重要:保持为一函数添加noupdate选项。否则,会在每次更新模块时调用它。

扩展知识…

通过<function>可以向函数发送参数。假设只想要在某个分类中增加图书的价格并且通过参数发送这一增加的值。

那么,需要创建一个方法来接收分类作为参数,如下:

传递分类和数值作为参数,需要使用eval属性,如下:

在安装这一模块时,会对指定分类的图书单价增加$20。.

退出移动版