Alan Hou的个人博客

Odoo 17开发者指南第四章 应用模型

本章将介绍对现有的插件模块进行一些小的增强。我们已经在第三章 创建Odoo插件模块中将插件模块注册到Odoo实例中。现在,我们将更深入地探索模块的数据库层面。读者将学习如何创建一个新的模型(数据库表),添加新的字段,并应用约束。读者还将了解如何在Odoo中使用继承来修改现有模型。在本章中,您将使用在上一章中创建的相同模块。

本章涵盖以下内容:

技术要求

在继续本章中的示例之前,请确保您已经拥有我们在第三章 创建Odoo插件模块中开发的模块,并已进行正确安装和配置。

定义模型表示和顺序

模型是数据库表的表示。模型定义了数据库表的结构和行为,包括字段、关系和各种方法。模型使用Odoo的对象关系映射(ORM)系统在Python代码中定义。ORM允许开发人员使用Python类和方法与数据库交互,无需编写原始的SQL查询。

模型属性是在我们新建模型时定义的特性;否则,我们使用已有模型的属性。模型使用带下划线前缀的结构属性来定义其行为。

准备工作

my_hostel实例应该已经包含一个名为models/hostel.py的Python文件,该文件定义了一个基本模型。我们将编辑它以添加新的类级属性。

如何操作…

通过有效利用这些属性,开发人员可以在Odoo中创建组织良好、可重用且易维护的代码,从而提高应用程序的效率和稳健性。以下是可以在模型中使用的属性:

  1. _name: name属性是最重要的,因为它决定了内部全局标识符和数据库表名。模型名在模块命名空间内用点号表示。例如,name=”hostel.hostel”将在数据库中创建hostel_hostel表:
  2. _table: 如果启用了_auto,我们可以定义模型使用的SQL表名:
  3. _description: 为模型分配一个描述性的标题,以反映其目的和功能,插入以下代码片段:

    注:如果模型没有使用_description,Odoo将在日志中显示警告。
  4. _order: 默认情况下,搜索结果按id字段排序。但可以通过提供_order属性设置以逗号分隔的字段名字符串来更改。字段名后可以跟desc关键字以降序排序。要按id降序排序,然后按名称升序排序,请使用以下代码语法:

    注:只能使用存储在数据库中的字段。不能使用未存储的计算字段对记录进行排序。_order 字符串的语法类似于SQL ORDER BY 子句,但有所简化。例如,不允许使用特殊子句NULLS FIRST
  5. _rec_name:用于设置表示或标记记录的字段。默认的 rec_name 字段是 name_rec_name 是 Odoo 图形用户界面 (GUI) 用于表示该记录的显示名称。如果您想更改 rec_name 并将 hostel_code 设置为模型的表示,请使用以下代码:

    注:如果你的模型没有 name 字段,并且也没有指定 _rec_name,那么显示名称为模型名称和记录 ID 的组合,如 (hostel.hostel, 1)
  6. _rec_names_search:用于通过指定的字段值搜索特定记录。类似于使用 name_search 函数。可以直接使用此属性,而不使用 name_search 方法。为此,请使用以下代码:

更多信息…

所有模型都有一个 display_name 字段,以人类可读的格式显示记录表示,自 8.0 版本以来,所有模型中都自动添加了该字段。默认的 _compute_display_name() 方法使用 _rec_name 属性来确定哪个字段包含显示名称的数据。要自定义显示名称,您可以重写 _compute_display_name() 方法并提供逻辑。该方法应返回一个包含记录 ID 和 Unicode 字符串表示的元组列表。

例如,要在表示中包含公寓名称和代码,如 Youth Hostel (YHG015),我们可以定义以下内容:

请看以下示例。将把公寓代码添加到记录的名称中:

添加了以上代码后,会更新display_name记录。假设记录的名称和代码分别为Bell House HostelBHH101,那么_compute_display_name()方法所生成的名称即为Bell House Hostel (BHH101)

完成后,hostel.py的代码如下:

hostel.xml文件中的<form>视图如下:

应当更新模块以使用修改在Odoo中生效。

按如下步骤更新模块:

然后搜索my_hostel模块并通过下拉菜单进行更新,如下图所示:


图4.1 – 更新模块

此外,也可以在命令行中使用 -u my_hostel 命令。

向模型添加数据字段

字段表示数据表中的一列,定义可存储在该列中的数据结构。Odoo 模型中的字段用于指定模型存储数据的属性和特征。每个字段都有一个数据类型(例如 CharIntegerFloatDate)等属性,这些属性决定了字段的行为。

本节中,我们将探讨字段可以支持的各种数据类型以及如何将它们添加到模型中。

准备工作

本节假定读者已经准备好一个实例,并参照第三章 创建Odoo插件模块创建了 my_hostel 插件模块。

如何操作…

my_hostel 插件模块应该已经有了定义基本模型的 models/hostel.py文件。我们将编辑它添加新字段:

  1. 使用最简化语法向 Hostel 模型添加字段:
  2. 添加好新字段后,我们仍需要将这些字段添加到表单视图中,以便在用户界面中体现这些更改。参照以下代码将字段添加到表单视图:

升级模块,这些更改将在 Odoo 模型中生效。

工作原理…

要向模型添加字段,需要在其 Python 类中定义相应类型的属性。可用的非关系字段类型如下:

  • Char:存储字符串值。
  • Text:存储多行字符串值。
  • Selection:存储从预定义值和描述列表中选择的一个值。它是一组值和描述对。选定的值是存储在数据库中的值,可以是字符串或整型。描述是可自动翻译的。
    :如果整型值为零,Odoo 不会显示描述。Selection 字段还接收函数引用代替列表作为其选择属性。这样可动态生成选项列表。在本章的使用引用字段添加动态关系一节中可以找到相关示例,其中也使用了selection属性。
  • Html:以 HTML 格式存储富文本。
  • Binary:存储二进制文件,如图像或文档。
  • Boolean:存储 True/False 值。
  • Date:以 Python 日期对象的形式存储日期值。使用 fields.Date.today()将默认值设置为当前日期。
  • Datetime:以 Python 日期时间对象的形式存储UTC日期时间值。使用 fields.Date.now()将默认值设置为当前时间。
  • Integer:存储整数值。
  • Float:存储带有可选精度(总位数和小数位数)的数值。
  • Monetary:存储特定货币的金额。在本章的向模型添加货币字段一节中进一步讲解。

本节的第 1 步展示了向每种字段类型添加字段的最简语法。字段定义可以扩展为添加其他可选属性,如第 2 步所示。以下是使用的字段属性的说明:

  • string:字段的标题,用于 UI 视图标签。它是可选的。如未设置,将从字段名称派生出标签,首字母大写并用空格替换下划线。
  • translate:如设置为 True,则使字段可翻译。可根据用户界面语言保存不同的值。
  • default:默认值。它也可以是一个用于计算默认值的函数,例如 default=_compute_default,其中 _compute_default 是在字段定义之前在模型中定义的方法。
  • help:在 UI 提示中显示的解释文本。
  • groups:使字段仅对某些安全组可用。这是一个包含安全组 XML ID 的逗号分隔列表字符串。在第十章 权限安全中对此进行了更详细的介绍。
  • copy:标识字段值在记录复制时是否被复制。默认情况下,对于非关系字段和 Many2one 字段为 True,对于 One2many 和计算字段为 False
  • index:如果设置为 True,则为该字段创建一个数据库索引,有时可以加快搜索速度。它取代了已弃用的 select=1属性。
  • readonly标记使字段在用户界面中默认只读。
  • required:标志使字段在用户界面中默认为必填项。
  • 这里所说的各种字段在odoo/fields.py文件中定义。
  • company_dependent:使字段为每个公司存储不同的值。它取代了已弃用的 Property 字段类型。该值未存储在模型表中。它注册为 ir.property。在需要 company_dependent 字段的值时,会搜索并链接到当前公司(如果存在属性,则链接到当前记录)。如果在记录上更改了值,它要么修改当前记录的现有属性(如存在),要么为当前公司和 res_id 创建一个新属性。若在公司端更改了值,将影响所有未更改值的记录。
  • group_operator:显示结果中按模式分组的聚合函数。
    此属性的值可为 countcount_distinctarray_aggbool_andbool_ormaxminavg 和 sumIntegerfloatmonetary 字段类型具有此属性的默认sum 值。该字段由 :meth:~odoo.models.Model.read_group 方法按字段对行分组。
    支持的聚合函数如下:
    • array_agg:将所有值(包括空值)拼接到一个数组中
    • count:计算行数
    • count_distinct:计算独特行数量
    • bool_and:如果所有值都为真,则返回 true,否则返回 false
    • bool_or:如果至少有一个值为真,则返回 true,否则返回 false
    • max:返回所有值中的最大值
    • min:返回所有值中的最小值
    • avg:返回所有值的平均值
    • sum:返回所有值的总和
  • store:是否将字段存储在数据库中(默认值为 True,计算字段为 False)。
  • group_expand:此函数用于在当前字段分组时扩展 read_group 结果:
  • sanitize:用于 HTML 字段,系统地从其内容中删除潜在的不安全标签。启用此标记会导致输入的全面清洗。如果需要对 HTML 清洗进行更精细的控制,可以使用一些附加属性。请注意,这些属性仅在启用 sanitize 标记时有效。

如果需要对 HTML 清洗进行更精细的控制,可以使用一些附加属性,但仅在启用 sanitize 标记时有效:

  • sanitize_tags=True:删除不在白名单中的标签(这是默认值)
  • sanitize_attributes=True:删除标签不在白名单中的属性
  • sanitize_style=True:删除不在白名单中的样式属性
  • strip_style=True:删除所有样式元素
  • strip_class=True:删除 class 属性

最后,我们根据模型中新增字段更新表单视图。我们将所有字段都放到了表单视图中,但读者可以根据需要将它们放置在任意位置。表单视图在第 九章 后端视图中有更详细的讲解。

更多…

DateDatetime 对象提供了一些方便的工具方法:

Date们有以下方法:

  • fields.Date.to_date(string_value):将字符串解析为日期对象。
  • fields.Date.to_string(date_value):将 Python 日期对象转换为字符串。
  • fields.Date.today():返回当前日期的字符串格式。适用于默认值。
  • fields.Date.context_today(record, timestamp):根据记录(或记录集)上下文的时区,将时间戳(如果省略则为当前日期)转换为字符串格式的日期。

Datetime有以下方法:

  • fields.Datetime.to_datetime(string_value):将字符串解析为日期时间对象。
  • fields.Datetime.to_string(datetime_value):将日期时间对象转换为字符串。
  • fields.Datetime.now():返回当前日期和时间的字符串格式。适用于默认值。
  • fields.Datetime.context_timestamp(record, timestamp):使用记录上下文中的时区,将原生时间戳的日期时间对象转换为带时区的日期时间对象。它不适用于默认值,但可用于向外部系统发送数据。

除了基础字段外,还有一些关系字段,如 Many2oneOne2manyMany2many。这些将在本章的向模型添加关系字段一节中介绍。

还可以使用 compute 字段属性定义计算函数,创建自动计算值的字段。这将在本章的向模型添加计算字段一节中介绍。

有些字段在 Odoo 模型中默认添加,因此应避免使用这些名称来定义字段。它们是:

  • id(记录的自动生成标识符)
  • create_date(记录创建时间戳)
  • create_uid(创建记录的用户)
  • write_date(记录最后编辑的时间戳)
  • write_uid(最后编辑记录的用户)

可以通过设置 _log_access=False 模型属性禁止自动创建这些日志字段。

另一个可以添加到模型中的特殊列是 active。它必须是一个布尔型字段,允许用户将记录标记为非活跃。它用于启用记录的归档/取消归档功能。其定义如下:

默认只能看到 active 设置为 True 的记录。要对其进行检索,需要使用带有 [('active', '=', False)] 的域过滤器。或者如将 'active_test': False 值添加到环境的上下文中,ORM 就不会过滤掉非活跃记录。

在某些情况下,可能无法修改上下文同时获取活跃和非活跃记录。这时,可以使用 ['|', ('active', '=', True), ('active', '=', False)] 作用域。

小贴士:[('active', 'in', (True, False))] 和预期会不太一样。Odoo 在作用域中显式查找 ('active', '=', False) 子句。它将默认限定仅搜索活跃记录。

添加具有可配置精度的浮点字段

在使用浮点字段时,我们可能希望让终端用户配置所使用的小数精度。在本节中,我们向公寓模型添加一个hostel_rating字段,并允许用户配置小数精度。

准备工作

我们将继续使用上一节中的my_hostel插件模块。

操作步骤…

执行以下步骤,对模型的hostel_rating字段应用动态小数精度:

  1. 创建data文件夹并添加一个data.xml文件。在此文件中,为小数精度模型添加以下记录。这将添加一个新的配置。
  2. 从设置菜单中启用开发者模式(参见第一章中的启用Odoo开发者工具)。这将启用Settings | Technical菜单。
  3. 访问小数精度配置。方法为打开Settings顶级菜单并选择Technical | Database Structure | Decimal Accuracy。我们应该能看到当前定义的设置列表。


    图4.2:新增小数精度
  4. 添加模型字段并使用该小数精度设置,编辑models/hostel.py文件,添加以下代码:

实现原理…

当向字段的digits属性添加字符串值时,Odoo会在小数精度模型的Usage字段中查找该字符串,并返回一个元组,具有16位精度和配置中定义的小数位数。使用字段定义替代硬编码,我们允许终端用户根据需要进行配置。

向模型添加货币字段

要在模型中处理货币值和货币,我们可以使用Odoo提供的特定字段类型和功能。Odoo对货币值和货币的特别支持简化了财务数据的处理,确保准确性、一致性和货币相关要求的合规性。

准备工作

我们将使用上一节中的my_hostel插件模块。

操作步骤…

我们需要添加一个币种字段和一个货币字段来存储某一金额的货币。

新增models/hostel_room.py,以添加必要的字段:

  1. 创建字段来存储倾向的币种:
  2. 添加货币字段来存储金额:

    为新模型创建权限文件和表单视图以在UI中显示它。升级插件模块以应用更改。货币字段将显示如下:


    图4.3:货币字段中的货币符号

实现原理…

Odoo得以在用户界面中正确显示货币字段,是因为它有第二个字段指示其币种。该字段类似于浮点字段。

货币字段通常命名为currency_id,但我们可以使用任何其他名称,只要在货币字段中指定currency_field参数即可。

当需要在同一记录中存储不同币种的金额时,这很有帮助。例如,如果希望拥有销售订单和公司的币种,可以创建两个fields.Many2one(res.currency)字段并对每个金额使用一个字段。

货币定义(res.currency模型的小数精度字段)决定了金额的小数精度。

向模型添加关系字段

关系字段用于表示Odoo模型之间的关系。有三种类型的关系:

  • 多对一(m2o)
  • 一对多(o2m)
  • 多对多(m2m)

以公寓房间模型为例。房间属于某一公寓,所以公寓和房间之间的关系是m2o。但一个旅馆可以有多个房间,所以反过来的关系是o2m

我们还可以有m2m关系。例如,一个房间可以提供各种设施,设施可以在不同的房间中使用。这是一个双向的m2m关系。

准备工作

我们将继续使用上一节中的my_hostel插件模块。

操作步骤…

编辑models/hostel_room.py文件添加字段:

  1. Hostel Room中添加m2o字段:
  2. 为学生创建一个关联房间的o2m字段。
  3. 首先需要创建一个学生模型。创建hostel_student.py文件并在公寓学生模型中添加一些基础字段。然后添加room_id字段来关联学生和房间模型。
  4. 最后添加一个o2m字段student_ids,关联hostel.student 与hostel.room模型:
  5. 新建文件hostel_amenities.py。添加以下代码:

    此时,将m2m设施字段添加到hostel.room模型中。以下代码添加至hostel_room.py

升级插件模块,新字段在模型中已可用。若要出现在视图中,必须先将它们添加到视图中。我们将在hostel_room.xml文件中添加新字段。

可以通过在开发者模式下Settings | Technical | Database Structure | Models中的模型字段来确认其添加成功。

实现原理…

m2o字段在模型表的列中存储另一条记录的数据库ID。这会在数据库中创建一个外键约束,确保存储的ID是另一个表中记录的有效引用。默认,这些关系字段没有数据库索引,但可以通过设置index=True属性添加。

可以指定在m2o字段引用的记录被删除时所进行的操作。ondelete属性控制这一行为。默认选项是‘set null’,即将字段设为空值。另一个选项是‘restrict’,即相关记录不能被删除。第三个选项是‘cascade’,即链接记录也将被删除。

可以对其他关系字段使用contextdomain。这些属性主要在客户端有用,为通过字段访问的相关记录的视图提供默认值:

  • context用于在点击字段查看相关记录的视图时,在客户端上下文中设置一些变量。例如,可以使用它为在该视图中创建的新记录设置默认值。
  • domain是一个过滤器,限定可以选择的相关记录列表。

可以在第九章中了解更多有关contextdomain的知识。

o2m字段与m2o字段的相反,它允许从模型访问关联的记录列表。与其他字段不同,它没有在数据库表中创建列。只是以一种方便的方式在视图中显示这些相关记录。要使用o2m字段,需要在另一个模型中有一个对应的m2o字段。本例中,我们在房间模型中添加了一个o2m字段。student_ids o2m字段引用hostel.room模型的room_id字段。

m2m字段没有在模型的表中创建列。相反,它使用数据库中的另一张表来存储两个模型之间的关系。该表有两列,分别存储相关记录的ID。使用m2m字段链接房间和其设施时,会在该表中创建一条新记录,记录房间的ID和设施的ID。

Odoo会为我们创建关联表。默认,关联表的名称由两个模型的名称按字母顺序排序,并加上_rel后缀。可以使用relation属性更改此名称。

当两个模型的名称过长时,应当使用relation属性。PostgreSQL对数据库标识符的长度限制为63个字符。因此,如果两个模型的名称各超过23个字符,应当使用relation属性设置较短的名称。我们在下一节中进行讲解。

更多…

还可以为多对一字段使用auto_join属性。此属性允许ORM在该字段上使用SQL连接。这意味着ORM不会检查用户访问控制和记录访问规则。虽然在某些情况下这这助解决性能问题,但最好避免这么做。

我们已经了解了定义关系字段的最简单方法。现在,来看看这些字段特有的属性。

以下是o2m字段的属性:

  • comodel_name:这是字段关联的模型名称。需要为所有关联字段设置此属性。可以在不使用关键字,将其传为第一个参数。
  • inverse_name:仅适用于o2m字段。这是另一个模型中指向该模型的m2o字段的名称。
  • limit:适用于o2mm2m字段。它设置了在用户界面中读取和显示的记录的最大数量。

以下是m2m字段的属性:

  • comodel_name:这是字段关联的模型名称,与o2m字段相同。
  • relation:这是存储关系的数据库表的名称。可以使用此属性更改默认名称。
  • column1:这是关系表中链接到此模型的列1的名称。
  • column2:这是关系表中链接到另一个模型的列2的名称。

通常,Odoo会自动处理这些属性的创建和管理。它可以识别并利用现有的反向m2m字段的关系表。但在某些特定情况下需要手动干预。

在处理两个模型之间的多个m2m字段时,需要为每个字段分配不同的关系表名称。

在模型名称超过PostgreSQL的数据库对象名称63字符的限制时,必须自行设置这些属性。默认的关系表名称通常是<model1>_<model2>rel。但该表包含一个更长名称的主键索引(<model1><model2>rel<model1>id<model2>_id_key),也需要遵守63字符的限制。因此,如果两个模型的名称总和超过此限制,则必须选择一个较短的关系表名称。

为模型添加层级关系

可以使用m2o字段表示层级关系,其中每条记录在同一模型中都有一个父记录和多个子记录。但Odoo还通过使用嵌套集模型 (https://en.wikipedia.org/wiki/Nested_set_model)提供对层级关系的特殊支持。启用后查询在其域过滤器中使用child_of运算符,显著提升速度。

依然以Hostel为例,我们将构建一个对公寓进行分类的层级分类树。

准备工作

我们将使用上一节中的my_hostel插件模块。

操作步骤…

为分类树新增Python文件models/hostel_categ.py,如下:

  1. 要加载新的Python代码文件,需在models/__init__.py中添加以下代码:
  2. 使用以下代码创建models/hostel_categ.py,这样可创建带有父子关系的公寓分类模型:
  3. 还需添加以下代码来启用特殊层级的支持:

  4. 在模型中新增如下代码来防止循环关联:

  5. 接下来需要为公寓分配分类。为此,在hostel.hostel模型中添加一个m2o字段:

最后升级模块使用修改生效。

要在用户界面中显示hostel.category模型,还需添加菜单、视图和权限规则。更多详情,请参见第三章 创建Odoo插件模块。也可以直接查看代码:https://github.com/alanhou/odoo-cookbook/tree/main/Chapter04。

实现原理…

我们希要创建一个具有层级关系的新模型。这就要求每条记录可以在同一模型中有一个父记录和多个子记录。以下是实现步骤:

  1. 创建一个 m2o 字段来引用父记录。使用 index=True 让这个字段在数据库中建立索引以加快查询速度。我们还使用 ondelete='cascade'ondelete='restrict' 来控制删除父记录时的行为。
  2. 创建一个 o2m 字段来访问记录的所有子记录。这个字段不会向数据库添加任何内容,但它是获取子记录的便捷方式。我们通过在模型属性中使用 parent_store=True 为层级结构添加特殊支持。这使得使用 child_of 运算符的查询更快,但也让写操作变慢。我们还添加了一个名为 parent_path 的辅助字段来存储层级搜索的数据。如果使用不同于 parent_id 的名称作为父字段,还需要在模型属性中使用 parent_name 指定它。
  3. 通过使用 models.Model中的 _check_recursion 方法来防止层级中的循环依赖。这避免了记录既是另一记录的祖先又是其后代的情况,从而防止无限循环。
  4. hostel.hostel 模型中添加一个类型为 Many2onecategory_id 字段,以便我们可以为每个公寓分配一个类别。这么做只是为了完成我们的示例。

更多…

应在层级结构变动不大但读取和查询频繁时,使用这种技术。因为数据库中的嵌套集合模型需要在添加、删除或移动类别时更新 parent_path 列(及相关的数据库索引)。这可能会很慢且代价高昂,特别是在有许多并发事务的情况下。

如果有一个经常变化的层级结构,使用标准的 parent_idchild_ids 关系可能会获得更好的性能。这样可以避免表级锁。

为模型添加字段约束

我们希望确保模型没有无效或不一致的数据。Odoo 提供了两种约束来实现这一点:

  1. 数据库级约束:这些是 PostgreSQL 支持的约束。最常见的是 UNIQUE 约束,它可以防止重复值。我们还可以使用 CHECKEXCLUDE 约束来实现其他条件。这些约束快速可靠,但受限于 PostgreSQL 的功能。
  2. 服务级约束:这些是用 Python 代码编写的约束。当数据库级别的约束不足以满足我们的需求时,可以使用这类约束。它更加灵活和强大,但速度较慢且复杂。

准备工作

继续使用之前小节中的 my_hostel 插件模块。我们将使用第三章 创建Odoo插件模块中的公寓房间模型并添加一些约束。

使用 UNIQUE 约束来确保房间号不重复。还将添加一个 Python 模型约束来检查租金金额为正值。

实现步骤

  1. SQL 约束通过 _sql_constraints 模型属性定义。此属性被赋值为包含字符串的三元组(name, sql_definition, message)列表,其中 name 是有效的 SQL 约束名,sql_definitiontable_constraint 表达式,message 是错误消息。我们可以将以下代码添加到 hostel.room 模型中:
  2. Python 约束是一个检查一组记录条件的方法。我们使用 @api.constrains() 装饰器来标记该方法为约束并表明涉及条件的字段。每当这些字段中的任何一个发生变化时,约束会自动检查。如果条件不满足,该方法应引发异常:

    对代码文件进行这些更改后,需要升级插件模块并重启服务。

注:如果通过模型继承向现有模型添加 SQL 约束,请确保没有违反约束的行。如果有这样的行,则不会添加 SQL 约束,并且日志中会生成错误。

有关 PostgreSQL 约束的内容和表约束的更多说明,请参阅 PostgreSQL 官方文档

实现原理

我们可以使用 Python 代码来验证模型并防止无效数据。可以使用以下两者:

检查一组记录条件的方法。我们使用 @api.constrains()装饰器来标记该方法为约束并表明涉及条件的字段。在这些字段中的任何一个发生变化时,约束会自动检查。

在条件不满足时报 ValidationError 异常。此异常会向用户显示错误消息并停止操作。

为模型添加计算字段

我们可能希望创建一个依赖于同一记录或相关记录中的其他字段值的字段。例如,我们可以通过用单价乘以数量来计算总金额。在 Odoo 模型中,可以使用计算字段来实现。

为演示计算字段的工作原理,我们将向公寓房间模型添加一个计算字段,用于根据学生入住情况计算房间的可用性。

还可以使计算字段可编辑和可搜索。在示例中,我们将展示如何实现。

准备工作

我们将继续使用之前小节中的 my_hostel 插件模块。

实现步骤

我们将修改 models/hostel_room.py 文件,添加一个新字段及实现其逻辑的方法:

  1. 计算字段的值通常依赖于同一记录中其他字段的值。ORM 要求开发者使用 @depends() 装饰器在计算方法上声明这些依赖关系。ORM 使用给定的依赖关系在依赖项发生变化时重新计算字段的值。首先,将新字段添加到公寓房间模型中:

     
  2. 默认,计算字段是只读的,因为用户不应输入值。
    但在某些情况下,允许用户直接设置值可能会有所帮助。例如,在我们的学生公寓场景中,我们将添加一个入住日期、退房日期和居住时长。希望用户能够输入时长或退房日期,相应地更新其他值:

    计算方法为字段赋值,而反向方法为字段的依赖项赋值。
    注意,记录保存时会调用反向方法,而依赖项发生变化时会调用计算方法。
  3. 默认计算字段不会存储在数据库中。可通过 store=True 属性来存储字段:

    由于计算字段默认不存储在数据库中,因此除非我们使用 store=True 属性或添加搜索方法,否则无法对其进行搜索。

实现原理

计算字段看起来像一个普通字段,不同之处在于它具有指定计算方法名称的 compute 属性。

但从内部看计算字段与普通字段不同。计算字段在运行时即时计算,因此,它们不会存储在数据库中,因此默认情况下无法对其进行搜索或写入。需要做一些额外工作来启用写入或搜索支持。我们来看如何实现。

计算方法在运行时实时计算,但 ORM 使用缓存来避免每次访问其值时不必要的重新计算。因此,它需要知道依赖哪些其他字段。使用 @depends 装饰器来确定何时让缓存值失效并重新计算。

确保计算方法始终为计算字段赋值。否则,会发生错误。代码中的条件有时无法为计算字段赋值时,这可能很难调试。

可以通过实现逆向方法添加写入支持。逆向方法使用赋给计算字段的值来更新源字段。当然,这仅适用于简单计算。在某些场景中,它还能发挥其它作用。本例中,我们通过编辑时长天数来设置退房日期,因为Duration是一个计算字段。

inverse属性是可选的;如果不想让计算字段可编辑,可以忽略。

还可以通过将 search 属性设置为方法名称(类似于 computeinverse)来使未存储的计算字段可搜索。与 inverse 一样,search 也是可选的;如果不想让计算字段可搜索,可以忽略。

但该方法并不执行实际的搜索。而是接收用于在字段上搜索的运算符和值作为参数,并返回一个包含备用搜索条件的作用域。

可选的 store=True 标记将字段存储在数据库中。此时,字段值在计算后存储于数据库中,然后,它们与普通字段一样被检索,而不是在运行时重新计算。由于有 @api.depends 装饰器,ORM 知道何时需要重新计算和更新这些存储的值。可以将其视为持久缓存。它还具有使字段可用于搜索条件的好处,包括排序和分组操作。如果在计算字段中使用store=True,则无需实现搜索方法,因为字段存储在数据库中,可以基于它进行搜索/排序。

compute_sudo=True 标记用于计算需要更高权限之时。当计算需要使用终端用户可能无法访问的数据时,可能需要它。

:在 Odoo v13 中,compute_sudo的默认值发生了变化。在 Odoo v13 之前,compute_sudo的值为 False,但在 v13 中,compute_sudo的默认值取决于 store 属性。如果 store 属性为 True,则 compute_sudo 为 True;否则为 False。但可以通过在字段定义中显式设置 compute_sudo来覆盖它。

更多…

Odoo v13 引入了 ORM 的新缓存机制。以前,缓存基于环境,但在 Odoo v13 中有了一个全局缓存。因此,如果有一个依赖上下文值的计算字段,有时可能会得到错误的值。为解决这个问题,需要使用 @api.depends_context 装饰器。参考以下示例:

可以看到在上例中,我们的计算使用了上下文中的 company_id。通过在 @depends_context 装饰器中使用 company_id,我们可确保字段值将根据上下文中的 company_id 值重新计算。

暴露存储在其他模型中的关联字段

Odoo 客户端只能读取属于其查询模型的字段的数据。无法像服务端代码那样使用点号访问相关表的数据。

不过,我们可以通过将相关表的数据添加为相关字段,使其对客户端可访问。下面通过这种方法在学生模型中获取公寓房间的信息。

准备工作

我们将继续使用之前小节中的my_hostel插件模块。

实现步骤

编辑 models/hostel_student.py 文件添加新的相关字段。

确保我们有公寓房间字段,然后添加一个新的关联字段,将学生与其公寓链接起来:

最后,我们需要升级插件模块,以使新字段在模型中可用。

实现原理

相关字段是一种特殊类型的字段,它引用不同记录中的另一个字段。要创建关联字段,需要指定 related 属性,并提供显示字段路径的字符串。例如,我们可以通过 room_id.hostel_id 路径创建一个显示学生公寓房间的相关字段。

更多

相关字段实际上是计算字段。它们只提供了一个方便的快捷语法来读取相关模型中的字段值。由于是计算字段,也就同样可以使用 store 属性。作为快捷方式,它们还具有引用字段的所有属性,如nametranslatable等。

此外,它们支持 related_sudo 标记,似于 compute_sudo;设置为 True 时,字段链将不检查用户访问权限直接遍历。

create() 方法中使用相关字段可能会影响性能,因为这些字段的计算会延迟到创建结束。因此,如果有一个一对多关系,例如在 sale.ordersale.order.line模型中,并且在行模型上有一个相关字段引用订单模型上的字段,应当在记录创建过程中显式读取订单模型上的字段,而不是使用相关字段快捷方式,特别是在有很多行时。

使用引用字段添加动态关系

对于关联字段,我们需要预先决定关系的目标模型(或协模型)。但有时我们可能需要将这一决定交给用户,先选择想要的模型,然后选择要链接的记录。

在 Odoo 中,可以使用引用字段来实现。

准备工作

我们继续使用之前小节中的 my_hostel 插件模块。

实现步骤

编辑 models/hostel.py 文件添加新的相关字段:

  1. 首先,我们需要添加一个辅助方法来动态构建可选择的目标模型列表:
  2. 然后,需要添加引用字段并使用前面的函数提供可选择的模型列表:

由于我们改变了模型的结构,需要升级模块以启用这些更改。

实现原理

引用字段类似于 m2o 字段,只是会允许用户选择要链接的模型。

目标模型可以从 selection 属性提供的列表中选择。selection属性必须是一个包含两个元素元组的列表,第一个是模型的内部标识符,第二个是其文本描述。

示例如下:

但我们可以使用最常见的模型,而不必提供一个固定列表。为进行简化,我们使用了所有具有消息功能的模型。通过使用 _referencable_models 方法,我们动态提供了一个模型列表。

本节首先提供了一个函数来浏览所有可引用的模型记录,以动态构建一个将提供给 selection 属性的列表。虽然这两种形式都可以,我们在引号中声明函数名,而没有直接引用函数。这更灵活,允许在代码中稍后定义引用的函数,比如在直接引用时就无法实现。

由于函数在模型级别操作,而不是在记录集级别,需要 @api.model 装饰器。

虽然这个功能看起来很不错,但它带来了显著的执行开销。在显示大量记录的引用字段(例如,在列表视图中)时,可能会产生巨大的数据库负载,因为每个值都必须在单独的查询中查找。与常规关系字段不同,它也无法利用数据库的引用完整性。

使用继承向模型添加功能

Odoo 拥有一个健壮的功能,显著增强了其灵活性和功能性,对于寻求定制解决方案的企业特别有利。这个功能允许集成模块插件,增强现有模块的功能,且无需更改其底层代码库。这是通过添加或修改字段和方法,以及通过补充逻辑扩展当前方法来实现的。这种模块化方法不仅促成了一个可定制和可扩展的系统,还确保了升级和维护的简化,防止了通常与自定义修改相关的复杂性。

官方文档描述了 Odoo 中的三种继承:

我们将在单独的小节中学习这三种继承。在本节中,我们将学习类继承(扩展)。它用于向现有模型添加新字段或方法。

我们将扩展现有的合作伙伴模型 res.partner,添加一个计算字段,计算每个用户分配的公寓房间数。这将有助于确定每个房间分配给哪个科系以及哪个用户入住。

准备工作

我们继续使用之前小节中的my_hostel插件模块。

实现步骤

我们将扩展内置的合作伙伴模型。读者可能还记得,我们已经在本章的向模型添加关系字段一节中继承了 res.partner 模型。为了尽可能简化y讲解,我们将在 models/hostel_book.py 代码文件中重用 res.partner 模型:

  1. 首先,我们将确保合作伙伴模型中有 authored_book_ids 反向关联,并添加计算字段:
  2. 接下来,添加计算书籍数量所需的方法:
  3. 最后,我们需要升级插件模块,以使修改生效。

实现原理

模型类使用 _inherit 属性定义时,它会向继承的模型添加修改,而不是替换掉它。

这意味着在继承类中定义的字段会添加或更改父模型上的字段。在数据库层,ORM 将字段添加到同一个数据库表中。

字段也会增量修改。即如果字段已存在于父类中,则仅修改继承类中声明的属性;其他属性保持在父类中的状态。

在继承类中定义的方法将替换父类中的方法。如果不使用 super 调用父方法,父类中的方法将不会执行,我们也失去其功能。因此,每当继承现有方法添加新逻辑时,应该添加一个super 语句调用父类中的方法。这将在第五章 基础服务端开发中详细讨论。

本节将向现有模型添加新字段。如果还想将这些新字段添加到已有视图(用户界面),请参见第九章 后端视图中的更改已有视图 – 视图继承一节。

使用继承复制模型定义

在上一节中,我们学习了类继承(扩展)。现在,我们将介绍原型继承,它用于复制现有模型的整个定义。在本节中,我们将复制 hostel.room 模型。

准备工作

我们将继续使用前一节中的 my_hostel 插件模块。

操作步骤

原型继承通过同时使用 _name_inherit 类属性来执行。按以下步骤生成 hostel.room 模型的副本:

  1. /my_hostel/models/目录中添加一个名为 hostel_room_copy.py 的新文件。
  2. hostel_room_copy.py 文件中添加以下内容:
  3. /my_library/models/__init__.py 文件中导入新文件引用。更改后的 __init__.py 文件应如下所示:
  4. 最后,我们需要插件模块以使更改生效。
  5. 要检查新模型的定义,请进入Settings | Technical | Database Structure | Models菜单。你会在这里看到新的 hostel.room.copy 模型。

小贴士:为查看新模型的菜单和视图,需要添加视图和菜单的 XML 定义。了解更多关于视图和菜单的信息,请参考第三章 创建Odoo插件模块添加菜单项和视图一节。

实现原理

通过同时使用 _name_inherit 类属性,可以复制模型的定义。在模型中同时使用这两个属性时,Odoo 将复制 _inherit 的模型定义并使用 _name 属性创建一个新模型。

本例中,Odoo 复制 hostel.room 模型的定义并创建一个新模型 hostel.room.copy。新的 hostel.room.copy 模型有自己独立的数据表,其数据完全独立于 hostel.room 父模型。由于它还继承自 partner 模型,因此对其的任何后续修改也会影响新模型。

原型继承复制父类的所有属性。它会复制字段、属性和方法。如果想在子类中修改它们,可以简单地通过在子类中添加新定义来实现。例如,hostel.room 模型有 _name_get 方法。如果你想在子类中使用不同版本的 _name_get,你=需要在 hostel.room.copy 模型中重新定义该方法。

:如果你在 _inherit_name 属性中使用相同的模型名称,原型继承将无法工作。如果确实在 _inherit_name 属性中使用相同的模型名称,它将仅表现为普通的扩展继承。

更多…

在官方文档中,这称为原型继承,但实际上很少使用。原因是委托继承通常能以更高效的方式满足需求,而无需复制数据结构。相关的更多信息,请参见下一节使用委托继承将功能复制到另一个模型

使用委托继承将功能复制到另一个模型

第三种继承类型是委托继承。它使用 _inherits 类属性,注意不是 _inherit。在某些情况下,我们希望创建一个基于现有模型的新模型,以使用其已有的功能,而不去修改现有模型。可以使用原型继承复制模型的定义,但这会生成重复的数据结构。如果你想复制模型的定义而不复制数据结构,那么答案就是使用 Odoo 的委托继承,它使用 _inherits 模型属性(注意多一个 s)。

传统继承与面向对象编程中的类似概念有很大不同。而委托继承则相似,因为可创建新模型并包含来自父模型的功能。它还支持多态继承,我们可以继承两个或更多其他模型。

我们经营一个公寓,提供房间和学生的住宿。为了更好地管理我们的住宿情况,将学生相关信息集成到系统中是至关重要的。具体来说,对于每个学生,我们需要类似于 partner 模型中的全面的身份和地址详细信息。此外,维护与房间分配相关的记录也很重要,包括每个学生入住和离开的日期以及他们的卡号。

直接将这些字段添加到现有的 partner 模型中并不是理想的方法,因为这会使模型充满与非学生合伙人无关的学生特有数据。更有效的解决方案是通过创建一个继承它的新模型来增强 partner 模型,并引入管理学生信息所需的额外字段。这种方法确保了系统的简洁、有序和高效,以满足公寓的独特需求。

准备工作

我们将继续使用前一节中的 my_hostel 插件模块。

操作步骤

新的学生成员模型应有其自己的 Python 代码文件,但为了尽量简化说明,我们将重复使用 models/hostel_student.py 文件:

  1. 添加继承自 res.partner 的新模型:
  2. 接下来,我们将添加针对每个学生的字段:

升级插件模块以启用更改。

工作原理

_inherits 模型属性设置我们要继承的父模型。本例中,我们只有一个,即res.partner。它的值是一个键值对字典,其中键是继承的模型,值是用于链接它们的字段名称。这些是我们还必须在模型中定义的 m2o 字段。本例中,partner_id 是用于链接 Partner 父模型的字段。

要更好地理解其原理,我们来看创建新成员时在数据库级别发生了什么:

学生记录自动链接到一个新的 partner 记录。这只是一个 m2o 关系,但委托机制添加了一些魔法,使 partner 的字段看起来像是属于学生记录,并且还会自动为新学生创建一个新的 partner 记录。

其实这个自动创建的 partner 记录没有任何特别之处。它是一个常规的 partner,如果查看partner 模型,会发现该条记录(当然不包括额外的成员数据)。所有学生都是 partner,但只有一些 partner 也是学生。那么,如果删除了一个同时也是学生的 partner 记录会发生什么?可以通过选择关系字段的 ondelete 值来决定。对于 partner_id,我们使用了级联删除(cascade)。这意味着删除 partner 也会删除相应的学生。我们也可以使用更保守的设置,即限制删除(restrict),以禁止在有链接成员时删除 partner。这时,只能删除学生才能成功。

需要注意的是,委托继承仅适用于字段,而不适用于方法。因此,如果 partner 模型有一个 do_something() 方法,成员模型不会自动继承它。

更多…

委托继承有一个快捷方式。你可以在 m2o 字段定义中使用 delegate=True 属性来替代 _inherits 字典。这就完全像 _inherits 选项一样。主要优势在于这更简单。在下例中,我们执行了与前一个示例相同的继承委托,但这里,我们在 partner_id 字段中使用了 delegate=True 选项,而不是创建一个 _inherits 字典:

委托继承的一个显著案例是用户模型 res.users。它继承自合作伙伴(res.partner)。也就是说在用户上看到的一些字段实际上存储在 partner 模型中(尤其是 name 字段)。创建新用户时,我们还会获得一个自动创建的新 partner

还有,传统的 _inherit 继承也可以将功能复制到新模型中,尽管效率较低。这在使用继承向模型添加功能一节中有讨论。

使用抽象模型实现可重用的模型功能

有时,我们希望能够将特定功能添加到几个不同的模型中。在不同文件中重复相同的代码是一种不好的编程实践;最好是实现一次并复用。

抽象模型允许我们创建一个通用模型,实现一些功能,然后可以被常规模型继承,以使用该功能。

作为示例,我们将实现一个简单的归档功能。这会将 active 字段添加到模型中(若尚不存在),并启用一个归档方法,用于切换 active 标记的值。这是因为 active 是一个魔术字段。如果它存在于模型中,默认情况下,active=False 的记录将从查询中被过滤掉。然后,我们会将其添加到公寓房间模型中。

准备工作

我们将继续使用前一节中的 my_hostel 插件模块。

操作步骤

归档功能当然值得自成插件模块,或至少有自己的 Python 代码文件。但为了尽量简化说明,我们将其放到了 models/hostel_room.py 文件中:

  1. 为归档功能添加抽象模型。它必须在其所使用的公寓房间模型中定义:
  2. 下面我们将编辑公寓房间模型以继承归档模型:

需要升级插件模块以使更改生效。

实现原理

抽象模型是通过基于 models.AbstractModel 而不是models.Model的类创建 。它具有常规模型的所有属性和功能;不同之处在于 ORM 不会为其在数据库中创建实际体现。也即它不能有任何数据存储在其中。仅作为一个可重用功能的模板,可添加至常规模型。

我们的归档抽象模型非常简单。它只添加了 active 字段和一个方法来切换 active 标记的值,我们期望稍后通过用户界面上的一个按钮来使用该方法。当一个模型类使用 _inherit 属性定义时,它会继承这些类的属性和方法,而在当前类中定义的属性和方法将对这些继承的功能进行修改。

这里使用的机制与常规模型扩展(如通过继承向模型添加功能一节所述)相同。你可能注意到 _inherit 使用的是模型标识符列表,而不是一个包含单个模型标识符的字符串。事实上,_inherit 可以有这两种形式。使用列表形式允许我们从多个(通常是抽象的)类继承。这里我们只继承了一个,所以使用字符串也可以。使用列表只是为了进行演示。

更多…

一个值得注意的内置抽象模型是 mail.thread,它由邮件Discuss)插件模块提供。在模型中,它启用了讨论功能,驱动着许多表单底部看到的消息墙。

除了 AbstractModel 之外,还有第三种模型类型可用:models.TransientModel。它具有类似于 models.Model 的数据库表示,但在其中创建的记录应是临时的,并由服务端调度的任务定期清除。除此之外,临时模型的工作方式与常规模型一样。

models.TransientModel 对于更复杂的用户交互(向导)非常有用。向导用于请求用户输入。在第八章 高级服务器端开发技术中,我们将探讨如何使用这些技术进行高级用户交互。

退出移动版