升级工具¶
升级工具 是一个包含辅助函数的库,用于简化升级脚本的编写。该库被 Odoo 用于标准模块的升级脚本,提供了可靠性并有助于加快升级过程:
辅助函数帮助确保数据库中的数据一致性。
它处理更新记录的间接引用。
允许调用函数以避免编写代码,节省时间并降低开发风险。
辅助工具使您可以专注于升级的重要部分,而无需考虑细节。
安装¶
克隆 升级工具仓库 到本地,并在启动 odoo 时将 src 目录添加到 --upgrade-path 选项之前。
$ ./odoo-bin --upgrade-path=/path/to/upgrade-util/src,/path/to/other/upgrade/script/directory [...]
在您不自行管理 Odoo 的平台上,可以通过 pip 安装此库:
$ python3 -m pip install git+https://github.com/odoo/upgrade-util@master
在 Odoo.sh 上,建议将其添加到自定义仓库的 requirements.txt 文件中。为此,请在文件中添加以下行:
odoo_upgrade @ git+https://github.com/odoo/upgrade-util@master
使用升级工具¶
安装完成后,以下包可供升级脚本使用:
odoo.upgrade.util: 辅助工具本身。odoo.upgrade.testing: 基础 TestCase 类。
要在升级脚本中使用它,只需导入即可:
from odoo.upgrade import util
def migrate(cr, version):
# Rest of the script
现在,可以通过 util 调用辅助函数。
工具函数¶
升级工具提供了许多有用的函数来简化升级过程。在此,我们将描述一些最有用的函数。有关辅助函数的完整声明,请参阅 util 文件夹 。
注解
工具函数中的 cr 参数始终指代数据库游标。传递在 migrate() 中接收到的游标。并非所有函数都需要此参数。
模块¶
用于模块级操作的实用函数。
在大多数情况下,模块操作(重命名、合并、删除等)应在 base 脚本中执行。原因是一旦 base 模块升级完成,所有与模块相关的信息应该已经设置在数据库中,以确保升级过程正常运行。命令行选项 --pre-upgrade-scripts (从 Odoo 16 开始可用)允许在加载 base 模块之前运行升级脚本。这是在主要升级后执行模块操作的推荐方式。
- force_install_module(cr, module, if_installed=None, reason='it has been explicitly asked for')[源代码]¶
强制 ORM 安装模块。
- force_upgrade_of_fresh_module(cr, module, init=True)[源代码]¶
强制执行正在安装的模块的升级脚本。
标准 Odoo 在安装模块时不运行升级脚本。这在技术上是有意义的,因为模块并未真正升级。然而,有时(新)模块需要执行一些操作才能正确安装,例如从其他模块获取数据。这种情况在模块功能被 拆分 为多个模块时很常见。
处于 init 模式的一个副作用是不遵守 XML 文件或
ir_model_data中的 noupdate 标志。
- merge_module(cr, old, into, update_dependers=True, xmlid_mapping=None)[源代码]¶
将一个模块合并到另一个模块中。
此函数将源模块中的所有引用和记录移动到目标模块中。
警告
此函数不会删除任何记录。它会移除源模块中与目标模块中名称冲突的 XMLID,除非通过
xmlid_mapping参数显式重命名。- 参数
old (str) – 要合并的模块名称
into (str) – 要合并到的目标模块名称
update_dependers (bool) – 是否更新依赖于
old的模块的依赖项xmlid_mapping (dict) – 可选的
{old_name: new_name}XMLID 映射,用于在合并过程中重命名。当来自old的 XMLID 否则会按 名称 与into中已有的 XMLID 冲突时使用。参见rename_xmlid()
- module_installed(cr, module)[源代码]¶
返回模块是否已安装。
参见
modules_installed()。
- move_model(cr, model, from_module, to_module, move_data=False, keep=())[源代码]¶
将模型从一个模块移动到另一个模块。
- 参数
此函数可用于将模型的覆盖移动到另一个模块。由于它无法区分源模型或继承模型,如果目标模块未安装,则会引发异常。
- remove_module(cr, module)[源代码]¶
完全删除模块。
此操作等同于卸载并移除模块的所有引用——数据库中不会留下任何痕迹。
- 参数
module (str) – 要移除的模块名称
警告
由于此函数会移除与模块关联的所有数据,请在调用此函数之前确保重新分配记录。
- remove_theme(cr, theme, base_theme=None)[源代码]¶
卸载主题模块。
警告
此函数只能在
post-脚本中使用。参见
remove_module()和uninstall_theme()。
- uninstall_theme(cr, theme, base_theme=None)[源代码]¶
卸载主题模块并将其从网站中移除。
警告
此函数只能在
website模块的post-脚本中使用,因为它依赖于 ORM。参见
remove_theme()和uninstall_module()。
模型¶
用于修改模型的实用函数。
模型操作最好在相关模块的 pre- 脚本中完成。
- merge_model(cr, source, target, drop_table=True, fields_mapping=None, ignore_m2m=())[源代码]¶
将一个模型合并到另一个模型中。
此函数将所有引用从
source模型移动到target模型,并 删除source模型及其引用。默认情况下,仅移动两个模型中同名的字段;可选地,可以提供不同字段名称的映射。警告
此函数不会将记录从
source模型移动到target模型。
- remove_inherit_from_model(cr, model, inherit, keep=(), skip_inherit=(), with_inherit_parents=True, skip_update_references=())[源代码]¶
从
model中移除inherit。此函数会从
model及其所有子模型中移除通过inherit继承的所有字段。包括继承模型及其父模型中的所有字段也将被移除,除非未设置with_inherit_parents模式。在这种情况下,仅移除inherit中的字段,而保留其父模型中的字段。在keep中列出的字段永远不会被移除。如果model的某些子模型列在skip_inherit中,它们将保留来自inherit的字段。
- remove_model(cr, model, drop_table=True, ignore_m2m=())[源代码]¶
从数据库中删除模型及其引用。
某些对模型的必要间接引用会被替换为 未知模型 —— 一个作为空占位符的空模型,用于替代已移除的模型。
字段¶
用于修改模型字段的实用函数。
字段操作最好在相关模块的 pre- 脚本中完成。在某些情况下,初步操作可以在 pre 中完成,然后在 post 中完成。一个常见的例子是在 pre- 中移除字段但保留其列,稍后在 post 中当列最终被删除时使用。
- change_field_selection_values(cr, model, field, mapping, skip_inherit=())[源代码]¶
替换选择字段值的引用。
此函数根据映射替换对选择值的所有引用。域也会被更新。
- convert_m2o_field_to_m2m(cr, model, field, new_name=None, m2m_table=None, col1=None, col2=None)[源代码]¶
将 Many2one 字段转换为 Many2many。
它会创建关系表并填充它。
- dump_field_to_chatter(cr, model, field, res_id='id', res_model=None, label='%s', fk_display=None, fallback_column=None, where=None, html_escape=True, message_type='notification', subtype_xmlid=None)[源代码]¶
在每条记录上发布一条讨论消息,以在列被删除之前保留其值。
当迁移过程中要移除某个字段,但客户端可能需要恢复其值时,这很有用。该值会作为
mail_message发布到记录的讨论中。如果未安装
mail模块(没有mail_message表),则在提供了fallback_column时把值转储到其中,否则该函数会静默地不做任何事。Example
# Simple varchar column util.dump_field_to_chatter( cr, "crm_lead", "mobile", label="Previous Mobile", where="{0}.phone != {0}.mobile" ) # Drop team_id, save the team name util.dump_field_to_chatter( cr, "res_partner", "team_id", label="Former Sales Team", fk_display="name" ) # Dump from hr_version onto hr_employee's chatter util.dump_field_to_chatter( cr, "hr_employee", "current_version_id", label="SSN", fk_display="ssnid" ) # Raw HTML body (no label prefix) util.dump_field_to_chatter( cr, "hr_employee", "notes", html_escape=False, message_type="comment", subtype_xmlid="mail.mt_note" )
- 参数
model (str) – 要转储的字段所属的模型
field (str) – 要保留其值的字段
label (str) – 消息格式。允许使用任何包含单个
%s的有效 PGformat字符串。如果缺少%s,会自动追加: %s。默认的"%s"生成不带前缀的原始值。res_id (str) – 标识要放入讨论中的记录 ID 的列。
fk_display (str or None) – 当
column是外键时,用于显示值的 FK 目标表中的列(例如"name")。FK 目标表会自动检测。省略时仅显示 ID。ID 始终以括号形式追加。fallback_column (str or None) – 当未安装
mail模块(没有mail_message表)时,用于转储该值的同一表上的列。该列必须存在(例如res_partner.comment),否则会抛出异常。翻译(jsonb)列通过其en_US键写入。where (str) – 源记录上额外的 SQL
WHERE条件。它是一个str.format模板,其中{0}是源表别名;用它来限定每个列引用,例如where="{0}.phone != {0}.mobile"。默认情况下,只包含被转储列不为NULL(文本类型时也不为空)的行。html_escape (bool) – 是否对值进行 HTML 转义(默认
True)。message_type (str) –
mail_message.message_type(默认"notification")。subtype_xmlid (str) – 消息子类型的可选 XML ID(例如
"mail.mt_note")。
- move_field_to_module(cr, model, fieldname, old_module, new_module, skip_inherit=())[源代码]¶
将字段从一个模块移动到另一个模块。
此函数更新对特定字段的所有引用,从源模块切换到目标模块。它避免了在注册表完全加载后数据丢失。继承模型中的字段也会被移动,除非跳过。
- remove_field(cr, model, fieldname, cascade=False, drop_column=True, skip_inherit=(), keep_as_attachments=False, update_references=True)[源代码]¶
从数据库中移除字段及其引用。
此函数还会从继承模型中移除字段,除非在
skip_inherit中指定了例外情况。当字段已存储时,我们可以选择不删除该列。- 参数
model (str) – 要移除字段的模型名称
fieldname (str) – 要移除字段的名称
cascade (bool) – 是否以
CASCADE模式移除字段列drop_column (bool) – 是否删除字段的列
skip_inherit (list(str) or str) – 跳过字段移除的继承模型列表,使用
"*"跳过所有模型keep_as_attachments (bool) – 对于二进制字段,是否应将数据保留为附件
update_references (bool) – 是否更新所有引用。若为
False,则不更新仪表板、过滤器、导出路径、域以及关联字段的关系。
- rename_field(cr, model, old, new, update_references=True, domain_adapter=None, skip_inherit=())[源代码]¶
在给定的
model上将字段及其引用从old重命名为new。该字段会在所有继承模型中更新,但
skip_inherit中指定的模型除外。此函数还会更新直接或间接的引用,包括过滤器、服务器操作、相关字段、电子邮件、仪表板、域等更多内容。参见
update_field_usage()可以使用特殊的适配器函数来更新域。默认适配器仅在每个域叶中将
old替换为new。有关域适配器的信息,请参阅adapt_domains()。
- update_field_usage(cr, model, old, new, domain_adapter=None, skip_inherit=())[源代码]¶
在不同位置将字段
old的所有引用替换为new。- 搜索范围:
ir_filters
ir_exports_line
ir_act_server
mail_alias
ir_ui_view_custom(仪表板)
sign_item_type
sign_item
域(使用
domain_adapter)相关字段
此函数可用于替换字段的使用。域会使用
domain_adapter进行更新。默认情况下,域适配器仅在域叶中将old替换为new。有关域适配器的更多信息,请参阅adapt_domains()。
记录¶
用于记录级操作的实用函数。
- delete_unused(cr, *xmlids, **kwargs)[源代码]¶
删除未使用的记录。
此函数仅在
xmlids指向的记录未被任何表引用时才会删除这些记录。对于具有层级结构的记录(例如产品分类),它会检查标记为级联删除的子记录是否也未被引用。在这种情况下,记录及其子记录将全部删除。注解
无法删除的记录将被设置为
noupdate=True。
- edit_view(cr, xmlid=None, view_id=None, skip_if_not_noupdate=True, active='auto')[源代码]¶
用于编辑视图架构的上下文管理器。
此函数返回一个上下文管理器,可能会生成视图的解析架构,作为 etree 元素 。返回对象中的任何更改将在退出上下文管理器时写回数据库,并更新架构的翻译版本。由于函数可能不会生成内容,可以使用
skippable_cm()来避免错误。with util.skippable_cm(), util.edit_view(cr, "xml.id") as arch: arch.attrib["string"] = "My Form"
要选择要编辑的目标视图,请使用
xmlid或view_id,但不能同时使用两者。当通过
view_id确定视图时,如果视图存在,则始终生成其架构,而不考虑其可能关联的noupdate标志。当设置了xmlid时,如果视图的noupdate标志为False,则不会生成架构,除非将skip_if_not_noupdate设置为False。如果noupdate为True,则生成视图以供编辑。如果
active参数为True或False,则视图的active标志将相应设置。注解
如果
active为 “auto”(默认值),则通过xmlid选择时视图将被激活,而通过view_id选择时保持不变。
- ensure_xmlid_match_record(cr, xmlid, model, values)[源代码]¶
确保 xml_id 引用具有特定值的记录。
此函数确保 xml_id 指向的记录与
values参数中指定字段的值匹配。如果 xmlid 存在,但它指向的记录与值不匹配,则会将 xmlid 更新为指向一个匹配的记录(如果找到)。如果 xmlid 不存在,则使用找到的记录创建它。如果未找到匹配的记录,则不会执行任何操作。在所有情况下,都会返回 xml_id 引用的记录(可能已更新)。- 参数
- 返回
匹配记录的 ID,如果未找到记录则为
None- 返回类型
小技巧
在将数据库中的记录迁移到自定义模块时,此函数非常有用,可以在模块更新之前创建 xml_ids 并避免重复。
- if_unchanged(cr, xmlid, callback, interval='1 minute', **kwargs)[源代码]¶
如果记录未更改,则运行
callback。当引用的记录未更改时,此函数将运行
callback。xmlid和任何其他参数(但不包括interval)将传递给callback。如果记录已更改,则会将其标记为noupdate=True。另请参见is_changed()和force_noupdate()。此函数在记录未更新时执行操作非常有用,一个常见示例是即使记录为
noupdate=True,也强制从 XML 更新。util.if_unchanged(cr, "mymodule.myrecord", util.update_record_from_xml)
- is_changed(cr, xmlid, interval='1 minute')[源代码]¶
返回记录是否已更改。
此函数检查记录是否在当前升级开始时间之前已更改。参见
upgrade-util/src/base/0.0.0/pre-00-upgrade-start.py对于符合以下条件的记录的 xmlid,此工具将返回假阳性:
已在当前升级之前的升级中更新过
尚未在当前升级中更新
如果
xmlid不存在于数据库中,此函数返回None。
- remove_view(cr, xml_id=None, view_id=None, silent=False, key=None)[源代码]¶
删除视图及其所有子视图。
只要给定视图及其继承视图属于某个模块,此函数就会递归删除它们。它还会删除多网站 COW 视图。
- 参数
警告
必须设置
xml_id或view_id。同时指定两者将引发错误。小技巧
当要删除多个视图时,请使用
remove_views(),它会批量执行开销较大的操作::util.remove_views(cr, "module.view_one", "module.view_two")
- remove_views(cr, *xml_ids, **kwargs)[源代码]¶
删除多个视图及其所有后代。
只要给定视图及其继承视图属于某个模块,此函数就会删除它们。它还会删除多网站 COW 视图。子视图或 COW 视图是被删除还是仅被禁用,取决于其关联的 xml_id;属于模块的视图会被删除,用户自定义的视图会被禁用。
- rename_xmlid(cr, old, new, noupdate=None, on_collision='fail')[源代码]¶
重命名记录的 external identifier (
xml_id)。如果目标名称已存在于数据库中,则无法进行重命名。在这种情况下,有两种选项可以控制此函数的行为:
fail:引发异常并阻止重命名merge:重命名外部标识符,删除旧标识符,并替换引用。参见replace_record_references_batch()
注解
此函数不会删除记录,仅更新 xml_ids。
- replace_in_all_jsonb_values(cr, table, column, old, new, extra_filter=None)[源代码]¶
替换 JSONB 列中的值。
此函数将 JSONB 值中的
old替换为new。它适用于替换翻译字段的所有翻译中的值。
- replace_record_references_batch(cr, id_mapping, model_src, model_dst=None, replace_xmlid=True, ignores=(), parent_field='parent_id')[源代码]¶
替换对记录的所有引用。
此函数将对
model_src记录的所有引用(直接或间接)替换为映射中的对应记录。如果映射的目标模型与源模型不同,则必须设置model_dst参数。
- update_record_from_xml(cr, xmlid, reset_write_metadata=True, force_create=AUTO, from_module=None, reset_translations=(), ensure_references=False, fields=None)[源代码]¶
根据其在 数据文件(Data Files) 中的定义更新记录。
此函数忽略记录的
noupdate标志。它会在 xmlid 或from_module参数(如果设置)所指定的模块清单中的所有 XML 文件中搜索匹配的定义。找到后,它会强制 ORM 按照数据文件中的规范更新记录。可选地,此函数可以重置某些字段的翻译。
- 参数
xmlid (str) – 记录的 xml_id,格式为
module.namereset_write_metadata (bool) – 是否更新记录的
write_dateforce_create (bool) – 如果记录不存在是否创建记录。默认为
True,除非fields不为 None。from_module (str) – 从中更新记录的模块名称,仅当规范位于不同于 xml_id 所在模块的其他模块中时才需要。
ensure_references (bool) – 通过
refXML 属性引用的记录是否也应更新。fields (set(str) or None) – 要包含在 XML 声明中的可选字段列表。若设置,则忽略所有其他字段。设置后,若记录缺失则不会被创建,且 XML 声明中未找到的所有字段将被设为 NULL/其默认值。
警告
此函数使用 ORM,因此只能在记录的数据规范中引用的所有模型均已加载之后使用。实际上,这意味着此函数应在
post-或end-脚本中使用。注解
ORM 的标准行为是如果记录不存在则创建记录,包括其 xml_id。此函数也会发生这种情况,除非将
force_create设置为False。
ORM¶
通过 ORM 执行操作的实用函数。
此模块中的函数允许在升级期间安全地使用 ORM。它们增强或修补了 ORM,使其能够高效处理大量数据。在某些情况下,提供了完全不同于 ORM 自身功能的替代方案。此模块中的函数适用于 所有 支持版本的 ORM。
- env(cr)[源代码]¶
创建一个新环境。
警告
此函数不会清空为超级用户维护的缓存(在空环境中)。每次直接在数据库中修改数据时,可能需要调用
invalidate_cache。- 返回
新环境
- 返回类型
- class iter_browse(model, *args, **kw)[源代码]¶
遍历记录集。
此类返回的
callable对象可以用作迭代器,按块加载记录(到recordset中)。每个块耗尽后,其数据会被发送回数据库(刷新或提交),然后加载下一个块。此类允许运行如下代码:
for record in env['my.model'].browse(ids): record.field1 = val env['my.model'].browse(ids)._compute_field2() env['my.model'].create(values)
以高效的方式运行,同时避免
MemoryError,即使ids或values包含数百万条目。使用此类的替代方法是:Example
MyModel = util.env(cr)['my.model'] for record in util.iter_browse(MyModel, ids): record.field1 = val util.iter_browse(MyModel, ids)._compute_field2() util.iter_browse(MyModel, ids).create(values)
- 参数
model (
odoo.model.Model) – 要迭代的模型ids (iterable(int)) – 要迭代的记录的 ID 可迭代对象
size (int) –
ids的长度。当ids没有__length__时,可用它来启用进度日志记录query (str) – ids 的替代方案,即可以生成它们的 SQL 查询。也可以是带 RETURNING 子句的 DML 语句。参见
query_ids()chunk_size (int) – 每次迭代块中加载的记录数量,默认为
200yield_chunks (bool) – 迭代时,按
chunk_size分块产出记录,而不是逐个产出。默认为Falselogger (
logging.Logger) – 用于报告进度的日志记录器,默认为_loggerstrategy (str) – 是否在每个块上执行
flush或commit,默认为flush
- 返回
此类返回的对象可用于安全地迭代或调用任何模型方法,即使是数百万条记录。
另请参见
env()
- recompute_fields(cr, model, fields, ids=None, logger=<Logger odoo.upgrade.util.orm (WARNING)>, chunk_size=256, strategy='auto', query=None)[源代码]¶
重新计算字段值。
此函数将重新计算模型的字段值,限制为一组记录或所有记录。重新计算不会一次性处理所有记录,而是分成多个批次(块)。这样可以避免性能问题,并在最坏情况下避免因
MemoryError导致失败。每个块处理完成后,数据会根据以下策略之一返回数据库:flush :使用 ORM 的
flush方法commit :提交游标(同时也会刷新)
auto :根据要计算的记录数量和是否存在跟踪字段,从上述两种策略中选择最佳选项。
commit 策略在处理大量数据时更不容易导致
MemoryError。- 参数
- adapt_domains(cr, model, old, new, adapter=None, skip_inherit=(), force_adapt=False)[源代码]¶
在域中使用
model和继承模型将old替换为new。adapter是一个用于调整叶子节点的回调函数。适配器函数必须接受三个参数,并返回一个替换原始叶子节点的 域 。参数如下:leaf:一个域叶子节点,形式为(left, op, right)的元组in_or:布尔值,当为True时表示该叶子节点是 OR ("|") 域的一部分,否则是 AND ("&") 域的一部分negated:布尔值,当为True时表示该叶子节点被取反 ("!")
Example
def adapter(leaf, in_or, negated): left, op, right = leaf ok, ko = (1, 2) if not negated else (2, 1) if op == "=" return [(left, "=", ok)] elif op == "!=": return [(left, "=", ko)] return [leaf]
除非
force_adapt为True,否则adapter仅对那些将model的old字段作为叶子节点left部分的 最后一部分 使用的叶子节点调用。在后一种情况下,如果字段出现在路径中的任何位置,则会调用适配器,这仅对关系字段有用。适配器返回的域不需要在输入叶子节点的
left部分将old字段替换为new。无论如何,替换都会应用于适配器返回的整个域。适配器的通常用途是修改操作符和输入叶子节点的right部分。如果未设置adapter,则仅执行替换操作。Example
当将
"field1"替换为"field2"时,会发生以下情况:("foo.bar.baz.field1", "=", 1)仅在foo.bar.baz指向的记录属于请求的model时才会被调整。("foo.field1.baz", "=", 1)即使foo指向model,也不会被调整,除非force_adapt为True,因为field1不是此叶子节点中left的最后一部分。
注解
此函数将替换所有 标准 域字段中的域,包括过滤器、仪表板和已知表示域的标准字段。
SQL¶
用于与 PostgreSQL 交互的实用函数。
- class ColumnList(list_=(), quoted=())[源代码]¶
封装表示列名称的元素列表。
生成的对象可以渲染为带有前导/尾随逗号或别名的字符串。
Example
>>> columns = ColumnList(["id", "field_Yx"], ["id", '"field_Yx"'])
>>> list(columns) ['id', '"field_Yx"']
>>> columns.using(alias="t").as_string(cr._obj) '"t"."id", "t"."field_Yx"'
>>> columns.using(leading_comma=True).as_string(cr._obj) ', "id", "field_Yx"'
>>> util.format_query(cr, "SELECT {} t.name FROM table t", columns.using(alias="t", trailing_comma=True)) 'SELECT "t"."id", "t"."field_Yx", t.name FROM table t'
>>> columns = ColumnList.from_unquoted(cr, ["foo", "BAR"])
>>> list(columns) ['"foo"', '"BAR"']
>>> list(columns.iter_unquoted()) ['foo', 'BAR']
注解
此类最好通过
get_columns()使用
- alter_column_type(cr, table, column, type, using=None, where=None, logger=<Logger odoo.upgrade.util.pg (WARNING)>)[源代码]¶
更改列的类型。
通过使用临时列和并行 UPDATE 查询高效完成。
- bulk_update_table(cr, table, columns, mapping, key_col='id')[源代码]¶
基于映射更新表。
每个
mapping条目为key_col值与键匹配的行定义指定columns的新值。Example
# single column update util.bulk_update_table(cr, "res_users", "active", {42: False, 27: True}) # multi-column update util.bulk_update_table( cr, "res_users", ["active", "password"], { "admin": [True, "1234"], "demo": [True, "5678"], }, key_col="login", )
- 参数
警告
映射中的值将被转换为目标列的类型。此函数设计用于更新标量值,避免通过映射设置数组或 JSON 数据。
- create_column(cr, table, column, definition, **kwargs)[源代码]¶
创建列。
此函数仅在列不存在时创建该列。如果现有列的类型不同,它会记录错误。如果设置了
fk_table,它将确保外键已设置,并在必要时进行更新,同时应用正确的on_delete_action(如果有)。
- create_m2m(cr, m2m, fk1, fk2, col1=None, col2=None)[源代码]¶
确保多对多表存在或被创建。
此函数创建与多对多字段关联的表。如果表已存在,则对其运行
fixup_m2m()。表名可以自动生成,应用与 ORM 相同的逻辑。为此,请为m2m参数使用值 “auto”。
- explode_execute(cr, query, table, alias=None, bucket_size=10000, logger=<Logger odoo.upgrade.util.pg (WARNING)>, qualifier='queries', explode_on='id')[源代码]¶
并行执行查询。
查询按 ID 分桶拆分,然后由工作线程并行处理。如果查询未包含特殊的
{parallel_filter}值,则会将其添加到最后一个WHERE子句中(如果未找到WHERE子句,也可能新增)。如果查询已经包含过滤器,则不执行任何操作。过滤器始终扩展为拆分策略。拆分时,每个单独查询更新的 ID 数量不超过bucket_size。Example
util.explode_execute( cr, ''' UPDATE res_users u SET active = False WHERE (u.login LIKE 'dummy' OR u.login = 'bob') AND {parallel_filter} ''', table="res_users" alias="u", )
- 参数
- 返回
每个查询运行后的
cr.rowcount总和- 返回类型
警告
调用者需要确保查询不会在不同的桶中更新相同的记录。建议不要对具有自引用的表使用此函数执行
DELETE查询,因为可能存在ON DELETE影响。更多详细信息,请参见parallel_execute()。
- format_query(cr, query, *args, **kwargs)[源代码]¶
安全格式化查询。
传递给此函数的
str参数假定为 SQL 标识符。在使用str.format()展开之前,它们会被包裹在双引号中。任何其他 psycopg2.sql.Composable 也是允许的。这包括ColumnList,另请参见get_columns()。Example
>>> util.format_query(cr, "SELECT {0} FROM {table}", "id", table="res_users") SELECT "id" FROM "res_users"
- 参数
query (str) – 要格式化的查询,可以像
str.format()一样使用大括号{}
- get_m2m_on(cr, table)[源代码]¶
返回与
table关联的 m2m 表列表。我们把所有只有两列且两列都是外键的表识别为 m2m 表。此函数将返回其中一个外键指向
table的 m2m 表。注解
m2m 表只返回一次,无论是在同一列上定义了多个外键,还是该 m2m 的两列都指向
table。在后一种情况下,列按字母顺序返回。- 返回
(m2m_table, fk_col_to_table, other_fk_col, other_table) 元组列表
- class named_cursor(cr, itersize=None)[源代码]¶
服务器端游标。
此类包装了 psycopg2 服务器端游标。它添加了
dictfetchmany和dictfetchall等便捷方法。应将其用作上下文管理器。服务器端游标可用于分块从数据库获取海量数据,同时继续使用主升级游标。
- 参数
itersize (int) – 决定一次从 PG 获取的行数。
- parallel_execute(cr, queries, logger=<Logger odoo.upgrade.util.pg (WARNING)>, qualifier='queries')[源代码]¶
并行执行查询。
Example
util.parallel_execute(cr, [util.format_query(cr, "REINDEX TABLE {}", t) for t in tables])
小技巧
如果希望加速单个查询,请参见
explode_execute()。- 参数
- 返回
每个查询运行后的
cr.rowcount总和- 返回类型
警告
由于
cr.rowcount的特性,此函数的返回值可能低估了实际受影响记录的数量。例如,当某些记录因ondelete子句而被删除或更新时,它们不会被计入。作为副作用,游标将被提交。
注解
如果发生并发问题,失败的查询将按顺序重试。
- class query_ids(cr, query, itersize=None)[源代码]¶
对查询返回的 ID 的迭代器。
这允许在不耗尽内存的情况下迭代可能数量巨大的 ID。
- 参数
query (str) – 返回 ID 的查询。它可以是 DML,例如
UPDATE table WHERE ... RETURNING id。itersize (int) – 决定一次从 PG 获取的行数,参见
named_cursor()。
杂项¶
各种独立函数。
- chunks(iterable, size, fmt=None)[源代码]¶
将
iterable拆分为大小为size的块,并使用fmt函数包装每个块。此函数对于将大量输入数据拆分为可独立处理的小块非常有用。
Example
>>> list(chunks(range(10), 4, fmt=tuple)) [(0, 1, 2, 3), (4, 5, 6, 7), (8, 9)] >>> ' '.join(chunks('abcdefghijklm', 3)) 'abc def ghi jkl m'
- 参数
iterable (iterable) – 要拆分的可迭代对象
size (int) – 块大小
fmt (function) – 应用于每个块的函数,当传入
None时,fmt在iterable为字符串时变为"".join,否则为iter。
- 返回
一个生成器,迭代
fmt应用于每个块的结果
- expand_braces(s)[源代码]¶
展开输入中的大括号。
Example
>>> util.expand_braces("a_{this,that}_b") ['a_this_b', 'a_that_b']
- 参数
s (str) – 要展开的字符串,必须包含且仅包含一对大括号。
- 返回
展开后的输入
- import_code_upgrade(name)[源代码]¶
导入代码升级脚本。
允许导入 upgrade_code scripts 以在升级脚本内使用。
- 参数
name (str) – 要导入的脚本名称。
- import_script(path, name=None)[源代码]¶
导入升级脚本。
此函数允许从其他升级脚本中导入函数到当前脚本中。
Example
在
mymodule/15.0.1.0/pre-migrate.py中def my_util(cr): # do stuff
在
myothermodule/16.0.1.0/post-migrate.py中from odoo.upgrade import util script = util.import_script("mymodule/15.0.1.0/pre-migrate.py") def migrate(cr, version): script.my_util(cr) # reuse the function
此函数返回一个 Python
module。>>> util.import_script("base/0.0.0/end-moved0.py", name="my-moved0") <module 'my-moved0' from '/home/odoo/src/upgrade-util/src/base/0.0.0/end-moved0.py'>
- class once(lower, upper, logger=None)[源代码]¶
在多步骤升级期间,仅在某个版本区间内运行一次代码。
当升级源和目标已知时(通过
ODOO_UPG_DB_SOURCE_VERSION和ODOO_UPG_DB_TARGET_VERSION环境变量),该对象仅在目标版本落在[lower, upper]内的 第一个 升级步骤中求值为真,其余每一步都求值为假。当环境变量不存在时(例如在单步或独立运行中),当前版本处于区间
[lower, upper]内时该对象为真。None值表示无界。必须至少设置一个边界。它既可以用作普通布尔值,也可以用作修饰符:
Example
作为布尔值,只在第一个到达 Odoo 17 的步骤中运行迁移:
def migrate(cr, version): if util.once("17.0", "18.0"): util.rename_field(cr, "sale.order", "old_name", "new_name")
作为修饰符,效果相同,会包装整个函数体:
@util.once("17.0", "18.0") def migrate(cr, version): util.rename_field(cr, "sale.order", "old_name", "new_name")
注解
这只在多步骤升级中有意义,即针对
0.0.0脚本或符号链接脚本。
- parse_version(s: str) tuple[str, ...][源代码]¶
将版本字符串转换为可按时间顺序排序的键
这是 distutils 的 StrictVersion 和 LooseVersion 的粗略混合体;如果传入的版本在 StrictVersion 下可用,它的行为与之相同;否则它的表现就像一个稍微聪明一些的 LooseVersion。有可能 构造出能骗过此解析器的病态版本编码方案,但在实践中它们应该非常罕见。
返回值是一个字符串元组。版本中的数字部分会补零到 8 位,以便按数值比较,而不依赖于数字与字符串的比较方式。点号会被丢弃,但短横线会保留。字母段或短横线之间的尾随零会被抑制,因此例如 “2.4.0” 被视为与 “2.4” 相同。字母数字部分会转换为小写。
该算法假定像 “-” 这样的字符串以及任何按字母顺序排在 “final” 之后的字母字符串都表示 “补丁级别”。因此,”2.4-1” 被视为 “2.4” 的一个分支或补丁,所以 “2.4.1” 被认为比 “2.4-1” 更新,而 “2.4-1” 又比 “2.4” 更新。
像 “a”、”b”、”c”、”alpha”、”beta”、”candidate” 等字符串(按字母顺序排在 “final” 之前)被视为预发布版本,因此版本 “2.4” 被认为比 “2.4a1” 更新。
最后,为处理各种杂项情况,字符串 “pre”、”preview” 和 “rc” 会被当作 “c” 处理,即视为候选发布版本,因此它们不如不包含这些字符串的版本新。
- skippable_cm()[源代码]¶
返回一个上下文管理器,允许另一个上下文管理器不产生值。
参见
edit_view()获取示例用法。
- version_between(a, b)¶
返回当前运行的 Odoo 版本是否在范围
[a,b]内。另请参见
version_gte()注解
边界值包含在内。
报表¶
用于报告升级变更的工具函数。
报告函数生成的信息在升级后会以帖子的形式显示在管理员频道中。
- html_escape(s: Any, /) markupsafe.Markup[源代码]¶
将字符串中的
&、<、>、'和"字符替换为 HTML 安全的序列。如果需要在 HTML 中显示可能包含此类字符的文本,请使用此函数。如果对象具有
__html__方法,则会调用该方法,并假定其返回值对 HTML 而言已经是安全的。- 参数
s – 要转换为字符串并转义的对象。
- 返回
包含转义后文本的
Markup字符串。
- report_with_list(summary, data, columns, row_format, links=None, total=None, limit=100, category='Other')[源代码]¶
向升级报告追加一条新条目,用于显示记录列表。
该条目由类别(标题)和摘要(正文)组成。该条目显示先前由 SQL 查询返回的记录列表,或任意列表。
警告
如果要渲染的列表为空,则报告中不会发布任何内容。
Example
total = cr.rowcount data = cr.fetchmany(20) util.report_with_list( summary="The following records were altered.", data=data, columns=("id", "name", "city", "comment", "company_id", "company_name"), row_format="Partner with id {partner_link} works at company {company_link} in {city}, ({comment})", links={"company_link": ("res.company", "company_id", "company_name"), "partner_link": ("res.partner", "id", "name")}, total=total, category="Accounting" )
- 参数
summary (str) – 报告条目的描述。
row_format (str) – 行的格式,可使用
columns或links中的任意名称,例如:”住在 {city} 的合作伙伴 {partner_link} 在 {company_link} 公司工作”。links (dict(str, tuple(str, str, str))) – 可选的模型/记录链接规范,其键可在
row_format中引用。total (int) – 可选,记录总数。当传入
None时取len(data)。当调用方限制了data时很有用。limit (int) – 报告中列出的最大记录数。如果
data包含的记录多于limit,则报告中也会包含total数量。设置为None表示不限制。category (str) – 报告条目的标题。
测试升级脚本¶
升级期间用于测试的主要有两个类。
UpgradeCase用于测试升级脚本,IntegrityCase用于测试必须跨版本保持不变的不变量值。
子类必须实现:
对于
UpgradeCase:prepare方法:在升级前准备数据,check方法:检查数据是否已正确升级。
对于
IntegrityCase:invariant方法:计算一个在升级前后必须保持一致的值。
将您的测试类放在任意包含模块升级脚本的文件夹下的 tests Python 模块(文件夹)中。包含测试的脚本必须带有 test_ 前缀。tests 模块必须包含 __init__.py 文件,才能被 Odoo 检测到。
示例目录结构:
myupgrades/
└── mymodule1/
├── 18.0.1.1.2/
│ └── pre-myupgrade.py
└── tests/
├── __init__.py
└── test_myupgrade.py
注解
仅当正在 升级 mymodule1 时,才会加载上例中的测试。
运行升级测试¶
在收到升级后的数据库(其中所有标准 Odoo 模块都已升级到目标版本)后,您可以按照以下三步流程测试自定义模块的升级:
准备测试数据
$ ~/odoo/$version/odoo-bin -d DB --test-tags=$prepare_test_tag \ --upgrade-path=~/upgrade-util/src,~/myupgrades \ --addons=~/odoo/$version/addons,~/enterprise/$version,~/mymodules --stop
升级模块
$ ~/odoo/$version/odoo-bin -d DB -u mymodule1,mymodule2 \ --upgrade-path=~/upgrade-util/src,~/myupgrades \ --addons=~/odoo/$version/addons,~/enterprise/$version,~/mymodules --stop
检查升级后的数据
$ ~/odoo/$version/odoo-bin -d DB --test-tags=$check_test_tag \ --upgrade-path=~/upgrade-util/src,~/myupgrades \ --addons=~/odoo/$version/addons,~/enterprise/$version,~/mymodules --stop
上例假定 $version 是您升级的目标版本(例如 18.0),DB 是数据库的名称,mymodule1,mymodule2 是您要升级的模块。该目录结构假定 ~/odoo/$version 和 ~/enterprise/$version 分别包含目标 Odoo 版本的社区版和企业版源代码。~/mymodules 包含您的自定义模块(mymodule1 等)的代码,~/myupgrades 包含您的自定义升级脚本,~/upgrade-util 包含 upgrade utils 仓库。
变量 $prepare_test_tag 和 $check_test_tag 必须按以下方式设置:
变量 |
|
|
|---|---|---|
|
|
|
|
|
|
注解
upgrade.test_prepare 也会运行 IntegrityCase 测试,因此仅使用此标签即可为 UpgradeCase 和 IntegrityCase 测试准备数据。
警告
在将数据库发送到 upgrade.odoo.com 进行 生产环境 升级之前,不要 运行 UpgradeCase 的任何 prepare 方法。这样做可能导致您的升级被阻止并标记为失败。
API 文档¶
- class IntegrityCase(methodName='runTest')[源代码]¶
用于验证升级不变量的测试用例。
重写:
invariant,返回一个可 JSON 序列化的值,用于表示要检查的不变量。
invariant方法会在升级前后各调用一次,并比较结果。如果存在任何差异,测试将失败。Example
from odoo.upgrade.testing import IntegrityCase class NoNewUsers(IntegrityCase): def invariant(self): return self.env["res.users"].search_count([])
- class UpgradeCase(methodName='runTest')[源代码]¶
用于验证升级流程的测试用例。
重写:
prepare,用于设置数据,check,用于在升级后断言预期结果。
在这些方法中可以使用 ORM 来执行被测的功能流程。
prepare的返回值会被持久化,并作为参数传递给check。它必须是可 JSON 序列化的。注解
由于
prepare会注入或修改数据,此类测试 仅用于开发。可在开发升级脚本时用它来测试脚本。不要 在生产环境升级中运行这些测试。要验证升级在生产环境中是否保留了重要的不变量,请改用IntegrityCase测试。Example
from odoo.upgrade.testing import UpgradeCase, change_version class DeactivateBobUsers(UpgradeCase): def prepare(self): u = self.env["res.users"].create({"login": "bob", "name": "Bob"}) return u.id # will be passed to check def check(self, uid): # uid is the value returned by prepare self.env.cr.execute( "SELECT * FROM res_users WHERE id=%s AND NOT active", [uid] ) self.assertEqual(self.env.cr.rowcount, 1)
- change_version(version_str)[源代码]¶
用于指定测试所适用版本的类修饰符。
使用
@change_version(version)表示:仅当当前 Odoo 版本位于
[next_major_version-1, version)范围内时,才会运行test_prepare。仅当当前 Odoo 版本位于
[version, next_major_version)范围内时,才会运行test_check。
next_major_version是version之后的下一个主版本,例如对于saas~17.2即为18.0。注解
如果您的升级在同一主版本内进行,请不要使用此修饰符。否则您的测试将不会运行。
- parametrize(argvalues)[源代码]¶
对测试函数进行参数化。
用于升级测试函数的修饰符,可对其进行参数化并由此生成多个测试。新的测试函数会被注入到所属的类中。灵感来自 parameterized 包。
Example
@parametrize([(1, 2), (2, 4), (-1, -2), (0, 0)]) def test_double(self, input, expected): self.assertEqual(input * 2, expected)
这将生成四个测试方法:
test_double__0、test_double__1、test_double__2和test_double__3,每个方法的参数值都不同。