Alan Hou的个人博客

Odoo 12开发者指南第七章 模块数据

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

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

引言

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

技术准备

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

本章中的所有代码可通过GitHub代码仓库进行下载:https://github.com/PacktPublishing/Odoo-12-Development-Cookbook-Third-Edition/tree/master/Chapter07。

实时代码操作请观看如下视频:http://t.cn/Ailakrsh

使用外部ID和命名空间

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

如何操作…

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

  1. 在你的模块声明文件中添加数据文件:
  2. 修改主公司的名称:
  3. 设置主公司的成员作为出版商:

在安装这一模块时,公司会被重命名并且下一部分中的图书会被分配给成员。在该模块的后续更新中,只会分配出版商,但公司名称不做修改。

运行原理…

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

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

ref函数,在本章中使用XML文件加载数据一节还会使用到,如果xml文件中的ref属性值字符串未指定所在模块,则在当前模块记录的XML ID中搜索,如找不到则报错。如果XML记录的id属性没有指定所在模块,则生成记录时会自动加上当前模块名作为命名空间。

扩展知识…

迟早你会需要在代码中通过XML ID访问记录。这种情况下使用self.env.ref()函数。这返回所引用记录的浏览记录。注意在这里你要保持传入完整的XML ID。一个完整XML ID的示例为:<module_name>.<record_id>。可以在用户界面中看到任意记录的XML ID。但需要激活Odoo的开发者模式。

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

TODO

其它内容

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

使用XML文件加载数据

使用第五章 应用模型中的数据模型,我们将添加一本书和一位作者来作为演示数据。我们还会添加一个知名的出版社来作为模块中的常规数据。

如何操作…

创建两个XML文件并在__manifest__.py文件中关联它们。

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

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

运行原理…

演示数据在技术上普通数据相同。不同之外在于前者通过声明文件中的 demo 键拉取数据,后者通过 data 键来拉取数据。使用<record>元素来创建记录,它有一个必填的id和model属性。对于 id 属性,参见使用外部ID和命名空间一节,model属性引用模型的_name属性。然后,使用<field>元素来填入数据库中列,这在你所命名的模型中定义。模型还决定哪些字段是必填的并且有可能定义了默认值。在这种情况下,你无需显式地给这些字段赋值。在第2步中,<field>元素可以在标量值的情况下包含简单文本值。如果你需要传递一个文件的内容(例如设置图片时),使用<field>元素的file属性并传递相对插件路径的文件名。

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

ℹ️注意数据文件中的顺序很重要,并且数据文件中的记录仅能引用该文件此前列表中所定义的记录。这也是为什么你需要保持查看模块是否在空数据库中安装,因为在开发过程中,你会经常在各处添加记录,能够使用的前提是后续所定义记录通过前几次更新已存在于数据库中。演示数据总是会通过data键后的文件进行载入,这也是本例中的引用可以生效的原因。

扩展知识…

虽然基本上你可以对记录元素做任何操作,开发人员可以使用一些快捷元素来便捷地创建某种类型的记录。这包含menu item, template和 act window。参见第十章 后端视图第十六章 网页客户端开发来获取更多相关信息。

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

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

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

使用noupdate和forcecreate标记

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

如何操作…

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

  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机制的话这可能会导致重复记录以及关联安装的报错,例如Python代码中由<function>标签所调用来创建记录时。

通过这一代码,你可以绕过任意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. 提升__manifest__.py文件中的版本号:
  2. 在migrations/12.0.1.0.1/pre-migrate.py中提供预迁移代码:
  3. 在migrations/12.0.1.0.1/postmigrate.py中添加一个迁移后代码:

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

运行原理…

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

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

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

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

此后,通过同样的搜索算法,会搜索迁移后文件并在找到时进行执行。在我们的例子中,我们需要所有的值来了解我们是否能借助它让一些数据可以使用,否则我们会保持数据为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>标签调用该模型方法。

如何操作…

对于这一节,我们将使用前一节的代码。作为示例,我们将已有图书价格增加$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。.

退出移动版