这是Odoo系列文章的第七篇,完整目录请见最好用的免费ERP系统Odoo 11开发指南
以下开发均假设读者已完成第六篇的代码,并且所有代码更新后均需自行更新方会在客户端看到变化。如未阅读该篇,请参考代码:Chapter 6
本文主要内容:
- 使用外部 ID 和命名空间
- 使用 XML 文件加载数据
- 使用 noupdate 和 forcecreate 标记
- 使用 CSV文件加载数据
- 使用 YAML 文件加载数据
- 插件更新和数据迁移
本文学习如何在安装时为插件提供数据,包含添加默认值、添加描述、菜单、动作等元数据,另一个重要的知识点就是添加演示数据,在安装时我们勾选 Load demonstration data 便会自动载入演示数据。
使用外部 ID 和命名空间
前面我们已经用到 XML ID,本文将进行更深入的探讨。
data/res_partner.xml(请自行加入__manifest__.py 的 data 中)
1 2 3 4 5 6 7 8 9 10 |
<odoo> <!--修改公司名,仅在安装时更改--> <record id="base.main_company" model="res.company"> <field name="name">Packt publishing</field> </record> <!--把公司的 Partner 设为出版商--> <record id="book_cookbook" model="library.book"> <field name="publisher_id" ref="base.main_partner" /> </record> </odoo> |
XML ID对应数据库中的一条记录,ID 本身是ir.model.data中的记录。该表每行中包含声明 XML ID 的模块,以及标识字符串、引用模型以及引用ID。每次写入 XML ID 时,Odoo 会检测是否已包含命空间(即是否包含了一个点号),没包含则以当前模型作为命名空间。
有一种除修改其它模块记录的部分数据的广泛应用,即使用便捷元素在创建记录并向记录中写入一个字段 ,而这个字段是不为便捷元素所支持的
1 2 3 4 5 |
<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> |
下面一节用到的 ref 方法,也会适时添加当前模块为命名空间,但如果结果 XML ID 不存在时会抛出错误,对于没有命名空间的 id 属性也是一样的。
在书写代码时可能会需要获取某一 XML ID 的记录,可以使用 self.env.ref()方法,它会返回一个引用记录的显示记录。注意此处需传入完事的 XML ID。
使用 XML 文件加载数据
首先添加一些书和作者的演示数据
data/demo.xml(请自行加入__mainfest__.py 的 demo 中加入)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<odoo> <record id="author_af" model="res.partner"> <field name="name">Alexandre Fayolle</field> </record> <record id="author_dr" model="res.partner"> <field name="name">Daniel Reis</field> </record> <record id="author_hb" model="res.partner"> <field name="name">Holger Brunn</field> </record> <record id="book_cookbook" model="library.book"> <field name="name">Odoo Cookbook</field> <field name="short_name">cookbook</field> <field name="date_release">2016-03-01</field> <field name="author_ids" eval="[(6, 0, [ref('author_af'), ref('author_dr'), ref('author_hb')])]" /> <field name="publisher_id" ref="res_partner_packt" /> </record> </odoo> |
演示数据需在创建数据库时勾选“加载演示数据”才会有效
使用xml 添加信息,
data/res_partner.xml
1 2 3 4 5 6 7 |
<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> |
更新模块即会插入上述记录
可以看出 demo 数据和普通数据几乎一样,区别在于 manifest 中声明的版块不同。要创建数据,必填属性有 id 和 model,id 属性参见外部 ID 和命名空间一节,model 则为模型的_name 属性。
然后我们使用 field 元素来为 model 定义的字段向数据库中添加数据。模型中定义的必填字段是必须也进行插入的,对于标量数据可能过普通文件舍入,而图片等文件则需使用 file 属性来传入 addon 与文件的相对路径。
对于引用有两种可能,最简单的是使用 ref 属性,适用于 many2one 字段,仅需包含所需引用记录的 XML ID。而对 one2many 和 many2many 字段,我们需要使用 eval 属性。这是一个通用属性,可以使用 Python 代码来传入字段值,如日期字段使用strftime(‘%Y-01-01’)。x2many 字段通常要传入一个三元元组,第一个值决定要进行的操作。在 eval 属性内部可以使用 ref方法,它返回传入的XML ID字符串的数据库 ID。这样我们可以在不知道具体 ID 的情况下引用记录:
- (2, id, False)删除数据库中对应 id 的关联记录,第三个元素无需传入
- (3, id, False)取消与对应 id 记录的关联,不会删除该记录
- (4, id, False)添加与对应 id 记录的关联,同样最一个元素无需传入。这应该是使用最频繁的,通过会伴随一个使用 XML ID 获取数据库 ID 的 ref 方法
- (5, False, False) 删除所有关联,但不会删除相关记录
- (6, False, [id, …])清除当前所有引用记录,使用指定ID列表中的记录进行替换,第二个参数无需传入
注意数据文件中的顺序也很重要,只有后面的记录可以引用前面的记录。所以在安装时应该模块时应检查数据库,避免我们在其它地方也添加了重复的记录。而演示数据总是在 data 模块加载后再载入,所以上例并不会发生报错。
虽然记录元素可以实现几乎所有功能,但我们会通过快捷元素来更便捷地创建有些记录。比如 menu item, template 或 act window,在第十篇和第十六篇中会有更多的介绍。
field 元素中可以包含 function 元素,用于调用模型中定义的方法。上面列表中没有列出0和1,因为在加载数据方便并没有什么用处,为了完整性,我们补充一下
- (0, False, {‘key’: value})创建一条引用模型记录,数据由第三项来指定。第二项参数无需指定。因这些记录没有 XML ID,并在每次模型更新都会被执行,也就会导致重复数据,因而应避免使用。
- (1, id, {‘key’: value}) 用于向已关联记录中写入,但与前述原因类似,也是应避免使用。
使用 noupdate 和 forcecreate 标记
大多数插件模型都有不同类型的数据,有些数据用于确保模型正常工作,有些数据应禁止用户修改,但大多数数据都是由用户按需要修改。下面我们首先向已存在记录写入一个字段,然后创建一个模型更新时需重新创建的记录。
1.我们可以为<odoo>或<record>添加特定属性来在载入数据时执行不同的操作添加一个仅在安装时创建的出版商,但在随后的更新中不再进行更新。但如果用户删除了它,又会重新创建
1 2 3 4 5 |
<odoo noupdate="1"> <record id="res_partner_packt" model="res.partner"> <field name="name">Packt publishing</field> </record> </odoo> |
2. 添加一个图书分类,在插件更新时不再变化且在用户删除后不再重新创建
1 2 3 4 5 6 |
<odoo noupdate="1"> <record id="book_category_all" model="library.book.category" forcecreate="false"> <field name="name">All books</field> </record> </odoo> |
可以为<odoo>元素添加 noupdate 属性,它在第一次读取所包裹的数据记录时,在 ir.model.data中创建记录时触发,在数据表中的 noupdate列体现。在 Odoo 安装插件时(称为 init 模式),所有的 noupdate 记录会被写入,而在更新插件时(update 模式),会查询已存在的 XML ID,如果发现设置了 noupdate 标记,则忽略写入操作。但如果用户删除了记录则仍会创建,因此我们还可以通过在记录中设置 forcecreate 参数为 false 来避免这种创建。
扩展知识
可以通过在 Odoo 服务启动时添加–init=your_addon 来强制进入 init 模式,来重写所有已有的或被删除的 noupdate 记录。但如果模块绕开了 XML ID机制(如使用 YAML 文件),则可能会造成重复记录或相应的安装错误。
对于全新写入的模型,则无需担心 noupdate 记录。而对于已安装过的则不会再更新数据,可以通过<function>元素来临时设置 noupdate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<function name="write" model="ir.model.data"> <function name="search" model="ir.model.data"> <value eval="[('module', '=', 'base'), ('name', '=', 'main_partner')]" /> </function> <value eval="{'noupdate': False}" /> </function> <record id="base.main_partner" model="res.partner"> <field name="book_ids" eval="[(4, ref('book_cookbook'))]"></field> </record> <function name="write" model="ir.model.data"> <function name="search" model="ir.model.data"> <value eval="[('module', '=', 'base'), ('name', '=', 'main_partner')]" /> </function> <value eval="{'noupdate': True}" /> </function> |
使用以上代码,可以绕过 noupdate 标记,另一个解决方案是书写迁移(migration)脚本,我们会来后面讲到。
Odoo 使用 XML ID 来记录在插件更新后哪些数据要被删除。如在更新前记录已经从模块命名空间中获取 XML ID,但在更新时还没有重新获取到 XML ID,记录和它在数据库的 XML ID 都会因被认为过期而被删除。在插件更新和数据迁移部分会进行更深入的探讨。
使用 CSV文件加载数据
虽然可以使用 XML 文件来进行所有操作,但在需要更大量数据时这种格式则不太方便,尤其是对于习惯了 Excel 等电子表格的用户更是如此。此外使用标准导出方法获取到的也是这格式。
很早开始权限控制列表(ACL)就是一种通过 CSV 文件来载入的数据
security/ir.model.access.csv
1 2 |
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" "access_library_book_user","ACL for books","model_library_book","base.group_user",1,0,0,0 |
这样就可以让普通用户仅能读取图书记录,而无法进行编辑、添加或删除操作。我们仅需把数据文件放到 manifest 的 data 版块中,Odoo 会根据扩展名来判定文件类型。但对 CSV文件有一些特殊要求,就是其文件名与要导入的模型名称要相匹配,如上面的 ir. model.access,然后第一行的标题要与模型中的列名相一致。对于标量数据,可以用带引号或不带引号的字符串。
在使用 CSV 文件写入 many2one 字段时,Odoo 首先会尝试将字段值解释为 XML ID,如果中间没有点,则会将模型名作为命名空间,然后在 ir.model.data 中查询结果。如果查询失败,会调用模型中的 name_search,交将字段值作为参数传入,返回查询到的第一条记录。如果该查询也失败的话,这一行就会被当作无效数据处理,Odoo 会抛出错误。
注:CSV中读取的数据都是作为 noupdate=False 来处理的,并且没有快捷方法来绕过这一处理。那也就意味着在随后的更新中用户的修改会被覆盖。如果碰巧你有大量的数据但又不希望受 noupdate 所限的话,使用 init 钩子来载入 CSV 文件。
可以通过 CSV 文件来导入 one2many 或 many2many 字段,但有些复杂。通常建议单独创建记录,随后使用 XML 文件或另一个 CSV 文件来设定关联。而如果你坚持要在一个文件里创建关联记录的话,对字段进行排序,确保标量字段在左边、关联模型的字段在右边,标题使用逗号分隔:
1 2 |
"id","name","model_id:id","perm_read","perm_read", "group_id:name" "access_library_book_user","ACL for books","model_library_book",1,"my group" |
以上会创建一个 my_group 组,如果需要关联更多记录,重复该行,仅对右侧列进行相应更改。Odoo 会对空列填充前一行的数据,所以我们也可以不拷贝所有数据,仅添加一个包含空值的行并修改关联模型的字段。对 x2m 字段,仅需列出需关联记录的 XML ID。
使用 YAML 文件加载数据
第三种数据导入模式是 YAML,比 XML 要轻,但比 CSV 要更全能。这种格式一度被用于测试而非数据导入,因而可以进行更多的代码调用。
data/demo.yml(请自行加入__manifest__.py 中的 demo 版块)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
- !record {id: author_af, model: res.partner}: name: Alexandre Fayolle - !record {id: author_dr, model: res.partner}: name: Daniel Reis - !record {id: author_hb, model: res.partner}: name: Holger Brunn - !record {id: book_cookbook, model: library.book}: name: Odoo cookbook short_name: cookbook date_release: 2016-03-01 author_ids: [(6, 0, [ref('author_af'), ref('author_dr'), ref('author_hb')])] publisher_id: res_partner_packt |
data/res_partner.yml(请自行加入__manifest__.py 中的 data 版块)
1 2 3 4 5 |
- !record {id: res_partner_packt, model: res.partner}: name: Packt publishing city: Birmingham country_id: base.uk |
上述操作和 XML 部分相同,只是改成了 YAML 语法,注意 YAML 文件的后缀名是.yml。
Odoo 引入了 YAML 数据类型 record,大多数Odoo 相关行为都是通过它来实现的。如同 XML,record 需要传入 id 和 model。字段值使用 YAML 的标准标记方式,字符串无需加引号。填充 many2one 字段时,Odoo 预设这里给到的是 XML ID,所以无额外标记。
我们看到了使用 XML 或 CSV 关联 one2many和 many2many 字段时有些麻烦。而在 YAML中可以更优雅地实现:
1 2 3 4 5 6 7 8 9 |
- !record {id: book_cookbook, model: library.book}: name: Odoo cookbook short_name: cookbook date_release: 2016-03-01 author_ids: - name: Alexandre Fayolle - name: Daniel Reis - name: Holger Brunn |
以上为 res.partner 类开创建了三条记录,并关联 author_ids 字段。要注意这里通过行内创建记录同样没有 XML ID,这样给其它地方的引用带来难度。如同 CSV,YAML 文件无法影响到 noupdate 标记。
插件更新和数据迁移
在写插件模块时选择的数据模型可能存在劣势,那么在模块的生命周期中需要进行调整。Odoo 通过对插件版本和必要时的数据迁移来实现这一调整。
假设在模块的定义中 date_release 是一个字符字段,在写入时表面看上日期。但后来发现需要做对比和累加,因而要将类型改为 Date。Odoo 中类型转换非常方便,但此处我们要自己进行更改。
首先在__manifest__.py 中修改版本号:
‘version’: ‘11.0.1.0.1’
migrations/11.0.1.0.1/pre-migrate.py
1 2 |
def migrate(cr, version): cr.execute('ALTER TABLE library_book RENAME COLUMN date_release TO date_release_char') |
migrations/11.0.1.0.1/post-migrate.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
def migrate(cr, version): cr.execute('SELECT id, date_release_char FROM library_book') 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.from_string(old_date) except ValueError: if len(old_date) == 4 and old_date.isdigit(): # probably a year new_date = date(int(old_date), 1, 1) else: # try some separators, play with day/month/year # order ... pass if new_date: cr.execute('UPDATE library_book SET date_release=%s', (new_date,)) |
不使用以上代码,Odoo 会将老的 date_release 字段重命名为 date_release_moved 并重新创建该字段,因为没有字符向日期字段的自动转换。
第一个重点是提升插件的版本号,因为迁移仅在不同版本中进行。每次更新时,Odoo 会在更新时从 manifest获取版本号,更新到 ir_module_module 表中。版本号前面有 Odoo 的大版本和小版本号,上例中我们就显式地写入了Odoo 的大小版本号,但仅用1.0.1也可达到同样的效果。通常长标记是一种更好的实践,这样Odoo 的版本号可以一目了然。
两个迁移文件是代码文件无需在任何地方注册。在更新插件时,Odoo 对比 ir_module_module 表中与 manifest的版本号,如果有更高版本,会自动搜索 migrations 文件夹,查找其中 pre-开头的 Python 文件并载入,默认应用 migrate 方法并包含两个参数。该方法第一个参数是数据库 cursor,第二个参数是当前安装版本。
在 pre-migrate 方法成功执行后,Odoo载入模型以及插件中声明的数据,这可能会带来数据库结构的改变。比如我们在其中重命名了 date_release,Odoo 会创建新的列。
然后使用同样的搜索算法查找到 post-migrate 文件,这里我们查询每条值来确定可用性,否则置为 NULL。除非绝对必要,不要自己写脚本遍历整张表,比如本例就可能变成一个非常长、可读性差的 SQL switch。
注:如果仅仅是要重命名一列无需书写迁移脚本。通过设置字段的 oldname 参数为原始列名,Odoo 会处理剩余的工作。
无论在 pre-还是 post-migration的步骤中,我们仅能获取到 cursor,对于习惯于 Odoo 环境的人来说并不方便。在这一步使用模型可能会带来意外的结果,因为 pre步骤中,插件模型还没有被载入,并且在 post 步骤中,插件所定义依赖当前插件的模型还未被载入。但是如果你使用的模型未被插件操作或确定上述问题并不存在,可以创建你所熟悉的环境:
1 2 3 4 |
from odoo import api, SUPERUSER_ID def migrate(cr, version): env = api.Environment(cr, SUPERUSER_ID, {}) # env holds all currently loaded models |
在编写迁移脚本时,通常会遇到一些重复的工作,如检查列或数据表是否存在、重命名、映射老的值到新的值。重复操作会比较麻烦并有可能出错,可以参考https://github.com/OCA/openupgradelib
本文参考代码:Chapter 7