Odoo14开发者指南第四章-应用模型【翻译】

本章中的教程将对现有的附加模块稍稍进行补充。在上一章中,我们在Odoo实例中注册了我们的附加模块。在本章中,我们将对模块的数据库方面进行深入的探讨。我们将添加一个新模型(数据库表)、新字段和约束。我们还将研究Odoo中模型的继承使用。本章中我们将继续使用在第三章中创建的附加模块。
本章中包含以下小节:

技术要求

您应该拥有我们在第三章创建的模块,并且该模块必须可以使用。
本章中使用的所有代码都可以从https://github.com/PacktPublishing/Odoo-14-Development-Cookbook-Fourth-Edition/tree/master/Chapter04上下载。

定义模型表现及排序

模型中使用结构性属性来定义模型的行为,这些属性是前缀带下划线的。_name 属性是模型中最重要的属性,它是模型的内部全局标识符,Odoo会根据_name 属性来创建数据库表。例如:模型的_name=”library.book” ,Odoo的ORM会在数据库中创建名为library_book 的表。模型的_name 属性在整个Odoo实例中必须是唯一的。
在模型中可以使用另外两个属性:

  • _rac_name :用于设置用作记录的表示或标题的字段。
  • _order :用于设置记录的排序。

准备工作

你需要准备好一个带有my_library 模块的Odoo实例(第三章中创建的模块)。

实现过程

my_library 模块应该已经包含一个名为models/library_book.py 的Python文件,它定义了一个基本模型。我们将对其进行编辑,在_name 之后添加一个新的类级属性:

  1. 要为模型添加用户友好的标题,请添加以下代码:

    1
    _description = 'Library Book'
  2. 要首先对记录进行排序(从最新到最旧,然后按标题排序),请添加以下代码:

    1
    _order = 'date_release desc, name'
  3. 要将short_name 字段用作记录的显示,请添加以下代码:

    1
    2
    _rec_name = 'short_name'
    short_name = fields.Char('Short Title', required=True)
  4. 在表单视图中添加short_name 字段,以便它可以在视图中显示新字段:

    1
    <field name="short_name"/>

完成后,我们的library_book.py 文件应如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
from odoo import models, fields

class LibraryBook(models.Model):
_name = 'library.book'
_description = 'Library Book'
_order = 'date_release desc, name'
_rec_name = 'short_name'

name = fields.Char('Title', required=True)
short_name = fields.Char('Short Title', required=True)
date_release = fields.Date('Release Date')
author_ids = fields.Many2many('res.partner', string='Authors')

library_book.xml 文件中的<form> 视图将如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
<form>
<group>
<group>
<field name="name"/>
<field name="author_ids" widget="many2many_tags"/>
</group>
<group>
<field name="short_name"/>
<field name="date_release"/>
</group>
</group>
</form>

然后我们需要升级模块以激活Odoo中的这些更改。要更新模块,您可以打开Apps 菜单,搜索my_library 模块,然后通过下拉列表更新模块,如下图所示:

更新模块选项

或者,您也可以在命令行中使用-u my_library 命令来更新模块。

运行原理

第一步为模型的定义添加一个对用户更友好的标题。这不是强制性的,但可以为一些附加组件使用。例如,邮件插件模块中的跟踪功能将它用于创建新记录时的通知文本。 有关详细信息,请参阅第二十三章,在Odoo中管理电子邮件。如果您的模型不使用_description ,在这种情况下,Odoo将在日志中显示警告信息。

默认情况下,Odoo使用内部id值(自动生成的主键)对记录进行排序。但是,这可以更改,我们可以通过提供一个包含字段名以逗号隔开的字符串作为_order 属性,字段名称后可以跟desc 关键字以降序排列。

重要提示
只能使用存储在数据库中的字段。非存储计算域不能用于对记录进行排序;
_order 字符串的语法类似于SQL ORDER BY子句,不允许使用例如NULLS FIRST等特殊子句;

模型记录在被其他记录引用时使用的一种表示形式。例如,user_id值为1的 员用户代表的管理员用户。当在表单视图中显示时,Odoo将显示用户名,而不是数据库ID。简而言之,_rec_name 是用于Odoo界面上显示记录的名称。默认情况下,使用的是name 字段。事实上,这是_rec_name 属性的默认值。在我们的示例中,library.book 模型有一个name 字段,因此,默认情况下,Odoo将使用它作为显示名称。我们第3步中使用了short_name 作为_rec_name 。之后,library.book 模型的显示名称从name 更改为了short_name ,Odoo显示界面将使用short_name 的值来展示记录。

警告
如果您的模型没有name 字段并且在这种情况下您也没有指定_rec_name ,那么您的显示名称将是模型名称和记录ID的组合,如下所示:(library.book, 1)

由于我们向模型添加了一个新字段short_name ,Odoo的ORM将向数据库表添加一个新列,但它不会在视图中显示该字段。为此,我们需要将此字段添加到表单视图中。在第4步中,我们将short_name 字段添加到表单视图中。

扩展内容

记录的展示也可以在一个神奇的计算display_name 设置,并且自8.0版以来已自动添加到所有模型中。它的值是使用name_get() 模型方法生成的,该方法在当前版本的Odoo中已经存在。

name_get() 的默认实现使用_rec_name 属性来查找哪个字段保存数据,用于生成显示名称。如果您想要自己实现显示名称的方法,您可以覆盖name_get() 的逻辑以生成自定义显示名称。该方法必须返回一个包含两个元素的元组列表:记录的ID和记录的Unicode字符串。

例如,要在显示中包含标题及其发布日期,例如Moby Dick (1851-10-18) ,我们可以定义以下内容:

1
2
3
4
5
6
def name_get(self):
result = []
for record in self:
rec_name = "%s (%s)" % (record.name, record.date_release)
result.append((record.id, rec_name))
return result

添加上述代码后,您的display_name 记录将被更新。假设您有一个名为Odoo Cookbook 的记录和发布日期为19-04-2019 ,那么前面的name_get() 方法会生成一个这样的名字Cookbook (19-04-2019)

向模型添加数据字段

模型用于存储数据,并且这些数据是按字段构造的。在这里,您将了解可以存储在字段中的几种数据类型,以及如何将它们添加到模型中。

准备工作

你需要准备好一个带有my_library 模块的Odoo实例(第三章中创建的模块)。

实现过程

my_library 模块应该已经有models/library_book.py 定义了一个基本模型。 我们将对其进行编辑以添加新字段:

  1. 使用最少的语法添加字段到Libraty Book 模型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from odoo import models, fields

class LibraryBook(models.Model):
# ...
short_name = fields.Char('Short Title')
notes = fields.Text('Internal Notes')
state = fields.Selection(
[('draft', 'Not Available'),
('available', 'Available'),
('lost', 'Lost')],
'State')
description = fields.Html('Description')
cover = fields.Binary('Book Cover')
out_of_print = fields.Boolean('Out of Print?')
date_release = fields.Date('Release Date')
date_updated = fields.Datetime('Last Updated')
pages = fields.Integer('Number of Pages')
reader_rating = fields.Float(
'Reader Average Rating',
digits=(14, 4), # Optional precision decimals,
)
  1. 我们在模型中添加了新字段。我们仍然需要将这些字段添加到表单视图中,以便在用户界面中反映这些更改。参考以下代码在表单视图中添加字段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<form>
<group>
<group>
<field name="name"/>
<field name="author_ids" widget="many2many_tags"/>
<field name="state"/>
<field name="pages"/>
<field name="notes"/>
</group>
<group>
<field name="short_name"/>
<field name="date_release"/>
<field name="date_updated"/>
<field name="cover" widget="image" class="oe_avatar"/>
<field name="reader_rating"/>
</group>
</group>
<group>
<field name="description"/>
</group>
</form>

升级模块将使这些更改在 Odoo 模型中生效。
查看如下这些不同字段的示例。这里我们对字段使用了不同类型的属性。这会让读者对字段声明拥有更好的概念:

1
2
3
4
5
6
7
8
9
10
11
short_name = fields.Char('Short Title',translate=True, index=True)
state = fields.Selection(
[('draft', 'Not Available'),
('available', 'Available'),
('lost', 'Lost')],
'State', default="draft")
description = fields.Html('Description', sanitize=True, strip_style=False)
pages = fields.Integer('Number of Pages',
groups='base.group_user',
states={'lost': [('readonly', True)]},
help='Total book page count', company_dependent=False)

运行原理

通过在其Python类中定义一个属性,将字段添加到模型中。可用的非关系字段类型如下:

  • Char 用于字符串值。

  • Text 用于多行字符串值。

  • Selection 用于选择列表。这是一个值和描述的列表。所选择的值会存储在数据库中,可以是字符串或整型。描述可自动翻译。

    重要提示
    在Selection类型的字段中,值您可以使用整数,但注意Odoo会将0解释为内部未设置,如果存储的值为0,则不会显示描述。因此您需要考虑到这一点。

  • Html 类似于文本字段,通常以HTML格式存的储富文本。

  • Binary 二进制字段存储二进制文件,例如图像或文档。

  • Boolean 布尔类型,存储True/False 值。

  • Date 用于存储日期值。在数据库中以日期存储。在ORM中以Python日期对象的形式处理。您可以使用fields.Date.today() 将当前日期设置为日期字段中的默认值。

  • Datetime 用于存储日期时间值。在数据库中以原生UTC 时区存储。在ORM中以Python日期时间对象的形式处理。您可以使用fields.Date.now() 将当前时间设置为DateTime字段中的默认值。

  • Integer 字段无需进一步解释。

  • Float 字段存储数值。精度可以用总位数和小数位数来定义。

  • Monetary 可以存储某种货币的金额。这也将在本章的向模型添加货币字段 中进行解释。

本小节第一步展示了添加到每个字段类型的最小语法。第二步中扩展了字段定义的其他可选属性。
这是对使用的字段属性的解释:

  • string 是字段的标题,用于Odoo界面视图标签。它是可选的。如果未设置,自动通过字段名首字母大写,并用空格替换下划线规则产生。

  • translate 当设置为True 时,该字段可翻译。该字段可以根据用户界面设置的语言来保存不同的值。

  • default 字段的默认值,也可以设置为一个计算默认值的函数;例如:default = _computer_default,其中_compute_default 是在字段定义之前在模型上定义的方法。

  • help 是显示在界面提示中的说明文本。

  • groups 使该字段仅对某些安全组可用。它是一个字符串,包含以逗号分隔的安全组XML ID列表。这在第十章《安全访问》 中有更详细的说明。

  • states 允许用户界面根据state 字段的值来动态设置readonlyrequiredinvisible 属性的值。因此,它需要state 字段存在并在表单视图中使用(即使它是隐藏的)。状态属性的名称在Odoo中是硬编码的,不能更改。

  • copy 标记在复制记录时是否复制字段值。默认情况下,非关联字段和Many2one 字段为True,One2many 和计算字段为 False。

  • index 当设置为True 时,为该字段会创建一个数据库索引。它替换了已弃用的select=1属性。

  • readonly 标志该字段在用户界面中默认为只读。

  • required 标志该字段在用户界面中默认为必填项。

    这里提到的各种白名单在odoo/tools/mail.py 中定义。

  • company_dependent 标志该字段按不同的公司存储不同的值。取代了已弃用的Property 字段类型。

  • group_operator 是一个聚合函数,用于在group by 模式下显示结果。此属性的可能值包括countcount_distinctarray_aggbool_andbool_ormaxminavgsum 。整数、浮点数和货币字段类型此属性的默认值为sum

  • sanitize 标志用于HTML字段,并从可能不安全的标签中去除其内容。使用它执行输入的全局清理。
    如果您需要对HTML清理进行更精细的控制,可以使用以下属性,这些属性仅在启用sanitize 时才有效:

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

最后,我们根据模型中新增的字段更新了表单视图。我们在这里以任意方式放置 <field> 标记,但您也可以将它们放置在任何您想要的位置。表单视图在第九章《后端视图》 中有更详细的解释。

扩展内容

Selection 字段还接受函数引用作为其选择属性而不是列表。这允许动态生成选项列表。您可以在本章的使用引用字段添加动态关联 一节中找到与此相关的示例,其中也使用了选择属性。

DateDatetime 字段对象公开了一些方便的实用方法。

对于Date ,我们有以下内容:

  • fields.Date.to_date(string_value) :将字符串解析为日期对象。
  • fields.Date.to_string(date_value) :将Python Date对象转换为字符串。
  • 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) :将原生的时间戳Datime对象转换到记录的上下文中的时区。这不适用于默认值,但可用于将数据发送到外部系统的实例。

除了基本字段,我们还有关系字段:Many2oneOne2manyMany2many 。这些在本章的向模型添加关联字段 中进行了解释。

也可以有具有自动计算值的字段,使用计算字段 属性定义计算函数。这在向模型添加计算字段 中进行了说明。

在Odoo模型中默认添加了一些字段,因此我们不应该为我们的字段使用这些名称。这些是id字段,用于记录自动生成的标识符,以及一些审计日志字段,如下所示:

  • create_date :记录创建的时间戳。
  • create_uid :记录创建的用户。
  • write_date :记录最后一次修改的时间戳。
  • write_uid :记录最后一次修改的用户。

可以通过设置_log_access=False 模型属性来禁用这些日志字段的自动创建。
另一个特殊字段是active 。它必须是一个布尔字段,允许用户将记录标记为非活动。它用于启用记录的归档/取消归档功能。其定义如下:

1
active = fields.Boolean('Active', default=True)

默认情况下,只有active 属性设置为True 的记录才可见,要检索它们,我们需要使用带有[(‘active’, ‘=’, False)] 的域过滤器。或者,如果'active_test': False值被添加到环境的上下文中,ORM将不会过滤掉非活动记录。

在某些情况下,您可能无法修改上下文以获取活动记录和非活动记录。在这种情况下,您可以使用[‘|’, (‘active’, ‘=’, True), (‘active’, ‘=’, False)] 域过滤器。

警告
[(‘active’, ‘in’ (True, False))] 不像你想象的那样工作。Odoo明确地在域中寻找* (‘active’, ‘=’, False)* 子句。它将默认将搜索限制为仅活动记录。

使用可配置精度的浮点型字段

使用浮点字段时,我们可能希望让用户可配置要使用的小数精度。在本节中,我们将向Library Books模型添加一个成本价格字段,该字段具有用户可配置的小数精度。

准备工作

我们将继续使用上一小节中的my_library 附加模块。

实现步骤

执行以下步骤以将动态小数精度应用于模型的cost_price 字段:

  1. 从设置菜单中的链接激活开发人员模式(请参阅第一章安装Odoo开发环境中的激活Odoo激活Odoo开发者模式工具)。这将启用Settings | Technical 菜单。

  2. 访问小数精度配置。为此,请打开设置顶部菜单并选择Technical | Database Structure | Decimal Accuracy 。我们应该看到当前定义的设置列表。

  3. 添加新配置,将Usage 设置为Book Price ,并填写Digits 精度:

    创建新的小数精度

  4. 要使用此小数精度设置需要添加模型字段,请通过添加以下代码来编辑models/library_book.py 文件:

    1
    2
    class LibraryBook(models.Model):
    cost_price = fields.Float('Book Cost', digits='Book Price')

    提示
    每当您在模型中添加新字段时,您还需要将它们添加到视图中以便从用户界面访问它们。在前面的示例中,我们添加了cost_price 字段。 要在表单视图中看到这一点,您需要使用<field name="cost_price"/>添加它。

运行原理

当您将字符串值添加到字段的digits 属性时,Odoo在小数精度模型的Usage 字段中查找该字符串并返回具有16 位精度和配置中定义的小数位数的元组。使用字段定义,而不是硬编码,允许最终用户根据他们的需要进行配置。

提示
如果您使用的是v13 之前的版本,则需要一些额外的工作才能在浮点字段中使用digits 属性。在旧版本中,小数精度在一个名为decimal_precision 的单独模块中。要在您的字段中启用自定义小数精度,您必须使用decimal_precision 模块的get_precision()方法,如下所示:cost_price = fields.Float(‘Book Cost’, digits=dp.get_precision(‘Book Price’))

向模型添加货币字段

Odoo对与货币相关的货币字段有特殊的支持。 让我们看看如何在模型中使用它。

准备工作

我们将继续使用上一小节中的my_library 附加模块。

实现步骤

货币字段需要一个额外的币别字段来存储相应的货币金额。

my_library 已经有models/library_book.py ,它定义了一个基本模型。我们将对其进行编辑以添加必填字段:

  1. 添加币别字段用于存储币别:

    1
    2
    3
    class LibraryBook(models.Model):
    # ...
    currency_id = fields.Many2one('res.currency', string='Currency')
  2. 添加货币字段用于存储金额:

    1
    2
    3
    4
    5
    class LibraryBook(models.Model):
    # ...
    retail_price = fields.Monetary('Retail Price',
    # optional: currency_field='currency_id',
    )

现在,升级附加模块,新字段应该在模型中可用。在将它们添加到视图之前,它们不会在视图中可见,但我们可以通过开发者模式进入到Settings | Technical | Database Structure | Models 中查看。

将它们添加到表单视图后,它将如下所示:

货币字段的货币符号

运行原理

货币字段类似于浮点型字段,Odoo能够在用户界面中正确地表示它们,因为它通过第二个字段知道它们的货别是什么。

这个货币字段应该被称为currency_id ,但我们可以使用我们喜欢的任何字段名称,只要它使用可选的currency_field 参数来指示即可。

提示
如果您将币别信息存储在名称为currency_id 的字段中,则可以省略货币字段中的currency_field 属性。
当您需要在同一记录中维护不同货币的金额时,这非常有用。例如,如果我们要包含销售订单的货币和公司的货币,您可以将这两个字段配置为fields.Many2one(res.currency)并将第一个用于第一个金额,另一个用于 第二笔金额。
您还应知道金额的小数精度取自货币定义(res.currency 模型的decimal_precision 字段)。

向模型添加关联字段

Odoo模型之间的关系由关系字段表示。存在三种不同类型的关系:

  • many-to-one :多对一,通常缩写为m2o。
  • one-to-many :一对多,通常缩写为o2m。
  • many-to-many :多对多,通常缩写为m2m。

查看Library Books 示例,我们可以看到每本书只能有一个出版商,因此我们可以在书籍和出版商之间建立多对一的关系。

但是,每个出版商可以拥有许多书籍。 因此,前面的多对一关系意味着一对多的反向关系。

最后,在某些情况下,我们可以建立多对多关系。在我们的示例中,每本书可以有几个(许多)作者。此外,相反,每个作者可能写过很多书。从任何一方来看,这是一个多对多的关系。

准备工作

我们将继续使用上一小节中的my_library 附加模块。

实现步骤

Odoo使用partner 模型res.partner 来表示人员、组织和地址。我们应该为作者和出版商使用它。我们将编辑models/library_book.py 文件以添加这些字段:

  1. 向Library Books模型添加图书出版商的多对一(many-to-one )字段:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class LibraryBook(models.Model):
    # ...
    publisher_id = fields.Many2one(
    'res.partner', string='Publisher',
    # optional:
    ondelete='set null',
    context={},
    domain=[],
    )
  2. 要为出版商的书籍添加一对多(one-to-many )字段,我们需要扩展partner 模型。为简单起见,我们将其添加到同一个 Python文件中:

    1
    2
    3
    4
    5
    6
    class ResPartner(models.Model):
    _inherit = 'res.partner'
    published_book_ids = fields.One2many(
    'library.book', 'publisher_id',
    string='Published Books'
    )

    我们在这里使用的_inherit 属性用于继承现有模型。这将在本章后面的使用继承向模型添加功能 中解释。

  3. 我们已经创建了书籍和作者之间的多对多(many-to-many )关系,但让我们重新查看一下:

    1
    2
    3
    class LibraryBook(models.Model):
    # ...
    author_ids = fields.Many2many('res.partner', string='Authors')
  4. 同样的关联,但作者对书籍的关联应该添加到partner 模型中:

    1
    2
    3
    4
    5
    6
    7
    class ResPartner(models.Model):
    # ...
    authored_book_ids = fields.Many2many(
    'library.book',
    string='Authored Books',
    # relation='library_book_res_partner_rel' # optional
    )

现在,升级附加模块,新字段应该在模型中可用。在将它们添加到视图之前,它们不会出现在视图中,但我们可以通过开发者模式进入到Settings | Technical | Database Structure | Models 中查看。

运行原理

多对一字段会在模型的数据库表中添加一列,存储相关记录的数据库ID。在数据库级别,还将创建外键约束,确保存储的ID是对相关表中记录的有效引用。这些关联字段不会创建数据库索引,但这可以通过添加index=True 属性来创建数据库索引。

我们可以看到还有四个属性可以用于多对一字段。ondelete 属性确定删除相关记录时会发生什么。例如,当图书的出版商记录被删除时,图书会发生什么情况?默认值为'set null' ,它在字段上设置一个空值。它也可以是'restrict' ,防止删除相关记录,或'cascade' ,这将导致链接的记录也被删除。

最后两个(contextdomain )对其他关系字段也有效。这些字段在客户端是很有意义的,在模型层面,它们作为默认值在客户端视图中使用。

  • context :在点击字段进入相关记录视图时,会将添加到客户端上下文变量中。例如,我们可以用它来为创建的新记录设置默认值。
  • domain :是一个搜索过滤器,用于限制可用的关联记录的列表。

context和domain在第九章《后台视图》 中都有详细的解释。

一对多字段是多对一的反关联,尽管它们像其他字段一样被添加到模型中,但它们在数据库中没有实际的表示。相反,它们是程序化的快捷方式,它们使视图能够表示这些相关记录的列表。这意味着一对多字段在关联模型中需要一个多对一字段。在我们的例子中,我们通过继承一个partner 模型来添加一对多字段。我们将在本章的使用继承向模型添加功能 中详细了解模型的继承。在我们的例子中,一对多字段published_book_ids 引用了library.book 模型的publisher_id 字段。

多对多的关联也不为模型的表添加列。这种类型的关联在数据库中使用一个中间关联表来存储,其中两列存储两个关联对象的ID。在书籍和作者之间添加新关系会在关联表中创建一条新记录,其中包含书籍ID和作者ID。

Odoo自动处理这个关联表的创建。默认情况下,关联表名称是使用两个关联模型的名称构建的,按字母顺序排序,加上一个_rel 后缀。但是,我们可以使用字段属性覆盖它。

需要记住的一个情况是,当两个表名足够大,以至于自动生成的数据库标识符超过PostgreSQL 63个字符的限制时。根据规则,如果两个相关表的名称超过23个字符,则应使用字段属性设置较短的名称。在下一节中,我们将对此进行更详细的介绍。

扩展内容

Many2one字段支持附加的auto_join 属性。这是一个允许ORM在该字段上使用SQL连接的标志。因此,它绕过了通常的ORM控制,例如用户访问控制和记录访问规则。在特定情况下,它可以解决性能问题,一般建议避免使用它。

我们已经介绍了如何通过最简单的方法来定义关联字段。让我们看一下特定于该类型字段的属性。

One2many 字段有以下属性:

  • commodel_name :这是目标模型标识符,对于所有关系字段都是必需的,但可以按位置定义,无需使用关键字。
  • inverse_name :这仅适用于One2many 并且是反向Many2one 关联的目标模型中的字段名称。
  • limit :这适用于One2manyMany2many ,并可在用户界面级别限制用户读取的记录数量。

Many2many 字段有以下属性:

  • comodel_name :这与One2many 字段相同。
  • relation :设置多对多关联表表名。
  • column1 :这是链接到此模型的关联表中的Many2one 字段的名称。
  • column2 :这是链接到comodel 的关联表中的Many2one 字段的名称。

对于Many2many 关联,在大多数情况下ORM会处理这些属性的默认值。它甚至能够检测反向Many2many关联,检测已经存在的关联表,并适当地反转column1和column2的值。

但是,有两种情况我们需要介入并为这些属性提供我们自己的值:

  • 一种情况是我们在相同的两个模型之间需要不止一个Many2many 关系时,我们必须自己提供第二个关系的关联表名称,该名称必须与第一个关联表不同。
  • 另一种情况是相关表的数据库名称足够长,以至于自动生成的关联表名超过了PostgreSQL 数据库对象名称的63 个字符的限制。

自动生成的关联表名称格式是<model1>_<model2>_rel 。但此关联表还为其创建了一个主键索引,其标识符如下:

1
<model1>_<model2>_rel_<model1>_id_<model2>_id_key

此主键还需要满足63 个字符的限制。因此,如果两个表名的字符总和超过63 ,需要手动设置关联属性。

添加模型层次结构

层次结构(Hierarchies )表示在同一模型之间做关联。每个记录在同一模型中都有一个父记录,以及许多子记录。这可以通过简单地使用模型与其自身之间的多对一关联来实现。

然而,Odoo还可通过使用嵌套集模型 (https://en.wikipedia.org/wiki/Nested_set_model) 为此类字段提供了更好的支持。启用后,在其域过滤器中使用child_of 运算符进行查询会发现查询速度明显的变快。

继续以Library Books 为例,我们将构建一个分层类别树,可用于对图书进行分类。

准备工作

我们将继续使用上一小节中的my_library 附加模块。

实现步骤

我们将新建一个Python文件,models/library_book_categ.py ,如下所示:

  1. 要加载新的Python代码文件,请将以下行添加到models/init.py 文件中:

    1
    from . import library_book_categ
  2. 要创建具有父子关系的Book Category 模型,请使用以下代码创建models/library_book_categ.py 文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    from odoo import models, fields, api
    class BookCategory(models.Model):
    _name = 'library.book.category'
    name = fields.Char('Category')
    parent_id = fields.Many2one(
    'library.book.category',
    string='Parent Category',
    ondelete='restrict',
    index=True
    )
    child_ids = fields.One2many(
    'library.book.category', 'parent_id',
    string='Child Categories'
    )
  3. 要启用特殊的层次结构支持,还要添加以下代码:

    1
    2
    3
    _parent_store = True
    _parent_name = "parent_id" # optional if field is 'parent_id'
    parent_path = fields.Char(index=True)
  4. 要添加防止循环关联的检查,请将以下行添加到模型中:

    1
    2
    3
    4
    5
    6
    7
    from odoo.exceptions import ValidationError
    ...
    @api.constraints('parent_id')
    def _check_hierarchy(self):
    if not self._check_recursion():
    raise models.ValidationError(
    'Error! You cannot create recursive categories.')
  5. 现在我们需要为一本书分配一个类别。为此,我们将向library.book 模型添加一个新的many2one 字段:

    1
    category_id = fields.Many2one('library.book.category')

最后需要升级模块使这些更改生效。

要在用户界面中显示librart.book.category 模型,您需要添加菜单、视图和安全规则。 有关详细信息,请参阅第三章创建Odoo附加模块。 或者,您可以在https://github.com/PacktPublishing/Odoo-13-Development-Cookbook-FourthEdition查看所有代码。

运行原理

步骤1和2创建具有层次结构的新模型。Many2one 关联添加一个字段来与父记录关联。为了更快地搜索子记录,使用index=True 参数在数据库中对该字段进行索引。parent_id 字段必须将ondelete 设置为'cascade''restrict' 。 在这一点上,我们已经具备了实现层次结构所需的一切,但我们还可以做一些额外的事情来改善它。One2many 关系不会向数据库添加任何其他字段,但提供了一种快捷方式来访问以该记录的所有子记录。

在第3步中,我们启用了对层次结构的特殊支持。这对于高读低写指令非常有用,因为它带来了更快的数据浏览速度,但代价是性能更低的的写操作。这是通过添加一个辅助字段parent_path 并将模型属性设置为_parent_store=True 来实现的。启用此属性后,帮助字段将用于在分层树中的搜索中存储数据。默认情况下,假定记录的父字段名为parent_id ,但也可以使用不同的名称。在这种情况下,应使用附加模型属性_parent_name 指示正确的字段名称。 默认如下:

1
_parent_name = 'parent_id'

步骤4是为了防止层次结构中出现循环依赖,这意味着在升序树和降序树中都有记录。这对于在树中导航的程序来说是危险的,因为它们可能会进入无限循环。models.Model 为我们提供了一个有效的方法(_check_recursion )我们在这里进行了复用。

第5步是在libary.book 图书中添加类型为many2onecategory_id 字段,这样我们就可以在图书记录上设置一个类别。 这只是为了完成我们的示例。

扩展内容

此处显示的技术应该用于静态 层次结构,这些层次结构经常被读取和查询,但更新频率较低。图书类别就是一个很好的例子,因为图书馆不会不断地创建新类别;但是,读者通常会将他们的搜索限制在一个类别及其子类别中。其原因在于数据库中嵌套集合模型的实现,每当插入、删除或移动类别时,都需要更新所有记录的parent_path 列(以及相关的数据库索引)。这可能是一项非常消耗资源的操作,尤其是在并行事务中执行多行编辑时。

如果您正在处理一个非常动态的层次结构,标准的parent_idchild_ids 关联通常会通过避免表级锁定来提高性能。

向模型添加约束验证

模型可以进行验证,以防止它们输入不希望的条件判断。

Odoo提供两种不同类型的约束:

  • 数据库级别的约束检查。
  • 服务端级别的约束检查。

数据库级别的约束仅限于PostgreSQL 支持的约束。最常用的是UNIQUE 约束,但也可以使用CHECKEXCLUDE 约束。 如果这些还不足以满足我们的需求,我们可以使用Python代码编写的Odoo服务端级约束。

我们将使用在第三章创建的Library Books 模型,创建Odoo附加模块,并为其添加一些约束。我们将添加一个防止重复书名的数据库约束,以及一个防止发布日期大于当前日期的Python模型约束。

准备工作

我们将继续使用上一小节中的my_library 附加模块。

我们希望它至少包含以下内容:

1
2
3
4
5
from odoo import models, fields
class LibraryBook(models.Model):
_name = 'library.book'
name = fields.Char('Title', required=True)
date_release = fields.Date('Release Date')

实现步骤

我们将在models/library_book.py Python文件中编辑LibraryBook 类:

  1. 要创建数据库约束,请添加模型属性:

    1
    2
    3
    4
    5
    6
    7
    8
    class LibraryBook(models.Model):
    # ...
    _sql_constraints = [
    ('name_uniq', 'UNIQUE (name)',
    'Book title must be unique.'),
    ('positive_page', 'CHECK(pages>0)',
    'No of pages must be positive')
    ]
  2. 要创建Python代码约束,请添加模型方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    from odoo import api, models, fields
    from odoo.exceptions import ValidationError
    class LibraryBook(models.Model):
    # ...
    @api.constrains('date_release')
    def _check_release_date(self):
    for record in self:
    if record.date_release and record.date_release > fields.Date.today():
    raise models.ValidationError('Release date must be in the past')

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

运行原理

第一步在模型的表上创建一个数据库约束。 它在数据库级别强制执行。_sql_constraints 模型属性接受要创建的约束列表。每个约束由一个三元素元组定义。这些列表如下:

  • 用于约束标识符的后缀。在我们的示例中,我们使用了name_uniq ,生成的约束名称是library_book_name_uniq
  • PostgreSQL 用于更改或创建数据库表的SQL语句。
  • 违反约束时向用户报告的消息。

在我们的示例中,我们使用了两个SQL约束。第一个是唯一的书名,第二个是检查该书的页数是否为正数。

警告
如果通过模型继承向现有模型添加SQL约束,请确保没有违反约束的行。如果有这样的行,则不会添加SQL约束,并且会在日志中输出错误信息。

正如我们前面提到的,也可以使用其他数据库表约束。请注意,不能以例如NOT NULL 这种方式添加列约束。有关一般PostgreSQL 约束和特别是表约束的更多信息,请查看 http://www.postgresql.org/docs/current/static/ddlconstraints.html

在第二步中,我们添加了一个方法来执行Python代码验证。这里使用了@api.constrains 修饰,这意味着当参数列表中的一个字段发生更改时,应该执行它以运行检查。如果检查失败,将会抛出ValidationError 异常。

扩展内容

通常如果需要复杂的验证,可以使用@api.constrains ,但对于一些简单的情况,可以使用带有CHECK 选项的_sql_constraints 。 看看下面的例子:

1
2
3
4
5
6
_sql_constraints = [
( 'check_credit_debit',
'CHECK(credit + debit>=0 AND credit * debit=0)',
'Wrong credit or debit value in accounting entry!'
)
]

在前面的示例中,我们使用了CHECK 选项,并且我们正在使用AND 运算符检查同一约束中的多个条件。

向模型添加计算字段

有时我们需要一个字段,该字段的值是从同一记录或相关记录中的其他字段计算或派生的。一个典型的例子是总金额,它是通过将单价乘以数量来计算的。在Odoo模型中,这可以使用计算字段来实现。

为了向您展示计算字段的工作原理,我们将在Library Books 模型中添加一个来计算自图书发行日期以来的天数。

还可以使计算字段可编辑和可搜索。 这也将在我们的示例中实现这一点。

准备工作

我们将继续使用上一小节中的my_library 附加模块。

实现步骤

我们将编辑models/library_book.py 代码文件以添加一个新字段和支持其逻辑的方法:

  1. 首先将新字段添加到Library Books 模型:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class LibraryBook(models.Model):
    # ...
    age_days = fields.Float(
    string='Days Since Release',
    compute='_compute_age',
    inverse='_inverse_age',
    search='_search_age',
    store=False, # optional
    compute_sudo=True # optional
    )
  2. 接下来,添加具有值计算逻辑的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # ...
    from odoo import api # if not already imported
    # ...
    class LibraryBook(models.Model):
    # ...
    @api.depends('date_release')
    def _compute_age(self):
    today = fields.Date.today()
    for book in self:
    if book.date_release:
    delta = today - book.date_release
    book.age_days = delta.days
    else:
    book.age_days = 0
  3. 要添加方法并实现写入计算字段的逻辑,请使用以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    from datetime import timedelta
    # ...
    class LibraryBook(models.Model):
    # ...
    def _inverse_age(self):
    today = fields.Date.today()
    for book in self.filtered('date_release'):
    d = today - timedelta(days=book.age_days)
    book.date_release = d
  4. 要实现允许您在计算字段可搜索的逻辑,请使用以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    from datetime import timedelta
    class LibraryBook(models.Model):
    # ...
    def _search_age(self, operator, value):
    today = fields.Date.today()
    value_days = timedelta(days=value)
    value_date = today - value_days
    # convert the operator:
    # book with age > value have a date < value_date
    operator_map = {
    '>': '<', '>=': '<=',
    '<': '>', '<=': '>=',
    }
    new_op = operator_map.get(operator, operator)
    return [('date_release', new_op, value_date)]

需要重新启动Odoo,然后升级模块,才能正确激活这些新增功能。

运行原理

计算字段的定义与常规字段的定义相同,只是添加了一个compute 属性来指定用于其计算的方法的名称。

它们的相似性可能具有欺骗性,因为计算域在内部与常规域完全不同。计算字段是在运行时动态计算的,因此它们不会存储在数据库中,因此默认情况下您无法搜索或写入计算字段。您需要做一些额外的工作才能启用对计算字段的写入和搜索支持。让我们看看怎么做。

计算函数是在运行时动态计算的,但是ORM使用缓存来避免每次访问其值时重新计算它而造成性能低下。 所以它需要知道它依赖于哪些字段。使用@depends 装饰器来检测其缓存值何时应失效并重新计算。

确保compute 函数始终在计算字段上设置一个值。否则将引发错误。当您的代码中有if 条件且无法在计算字段上设置值时,可能会发生这种情况。这可能很难进行调试。

可以通过实现反函数(inverse )来添加写支持。使用分配给计算字段的值来更新源字段。当然这只对简单的计算有用。尽管如此,仍然存在一些有用的情况。在我们的示例中,我们可以通过编辑自发行以来的天数计算字段来设置图书发行日期。inverse 属性是可选的;如果您不想使计算字段可编辑,则可以跳过它。

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

但是这种方法预计不会实现在实际的搜索中。相反,它接收用于在字段上搜索的运算符和值作为参数,并期望返回一个具有替换搜索条件的域以供使用。在我们的示例中,我们将对发布以来的天数 字段的搜索转换为发布日期 字段上的等效搜索条件。

可选的store=True 标志将字段存储在数据库中。在这种情况下,在计算之后字段值将存储在数据库中,然后以与常规字段相同的方式检索它们,而不是在运行时重新计算。 由于@api.depends 装饰器,ORM将知道何时需要重新计算和更新这些存储的值。您可以将其视为持久缓存。它还具有使该字段可用于搜索条件的优点,包括按操作排序和分组。如果在计算字段中使用store=True ,则不再需要实现search 方法,因为该字段存储在数据库中,您可以根据存储的字段进行搜索或排序。

在需要以提升的权限完成计算的情况下,使用compute_sudo=True 标志来实现。在当计算需要使用最终用户可能无法访问的字段数据时,可使用这种方法实现。

重要提示
在Odoo v13中更改了compute_sudo 的默认值。在Odoo v13之前,compute_sudo 的值为False 。 但在v13中,compute_sudo 的默认值将基于store 属性。 如果store 属性的值为True ,则compute_sudoTrue 否则为False 。但是您始终可以通过在字段定义中显式放置compute_sudo 来手动更改它。

扩展内容

Odoo v13为ORM引入了一种新的缓存机制。早期版本中,缓存是基于环境的,但现在在Odoo v13中,我们有一个全局缓存。因此,如果您有一个依赖于上下文值的计算字段,那么您有时可能会得到不正确的值。要解决此问题,您需要使用@api.depends_context 装饰器。 请参考以下示例:

1
2
3
4
5
6
@api.depends('price')
@api.depends_context('company_id')
def _compute_value(self):
company_id = self.env.context.get('company_id')
...
# other computation

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

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

从服务器读取数据时,Odoo客户端只能获取模型中可用和正在查询的字段的值。与服务器端代码不同,客户端代码不能使用点表示法访问相关表中的数据。

但是这些字段可通过将它们添加为关联字段来进行访问。我们将这样做以使出版商所在的城市在Library Books 模型中可用。

准备工作

我们将继续使用上一小节中的my_library 附加模块。

实现步骤

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

  1. 确保我们有图书出版商的字段:

    1
    2
    3
    class LibraryBook(models.Model):
    # ...
    publisher_id = fields.Many2one('res.partner', string='Publisher')
  2. 现在,添加发布者所在城市的相关字段:

    1
    2
    3
    4
    5
    6
    7
    # class LibraryBook(models.Model):
    # ...
    publisher_city = fields.Char(
    'Publisher City',
    related='publisher_id.city',
    readonly=True
    )

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

运行原理

关联字段就像常规字段一样,但它们有一个附加属性related ,带有一个分隔字段链遍历的字符串。

在我们的例子中,我们通过publisher_id 访问与发布者相关的记录,然后读取它的city 字段。我们还可以有更长的链,例如publisher_id.country_id.country_code

请注意在本例中,我们将相关字段设置为只读 。如果我们不这样做,该字段将是可写的,并且用户可能会更改其值。这将具有更改相关发布者的城市字段值的效果。虽然这可能是一个有用的副作用,但需要谨慎。由同一出版商出版的所有书籍都将更新其publisher_city 字段,这可能不是用户期望的。

扩展内容

关联字段实际上是计算字段。它们只是提供了一种方便的快捷语法来从相关模型中读取字段值。作为一个计算域,这意味着store 属性也是可用的。作为一种快捷方式,它们还具有引用字段的所有属性,例如nametranslatable

此外,它们支持类似于compute_sudorelated_sudo 标志;当设置为True 时,遍历时不检查用户的访问权限。

create() 方法中使用关联字段可能会影响性能,因为这些字段的计算会延迟到它们的创建结束。因此,如果您有一个One2many 关联,例如在sale.ordersale.order.line 模型中,并且您在line模型上有一个关联字段引用了order模型上的字段,应当在记录创建时在order模型中显式读取该字段,而不是使用关联字段快捷方式,尤其是在有很多行的情况下。

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

对于关联字段,我们需要事先确定关联的目标模型(或comodel)。但是,有时我们可能需要将决定权留给用户,首先选择我们想要的模型,然后选择我们想要链接到的记录。

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

准备工作

我们将继续使用上一小节中的my_library 附加模块。

实现步骤

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

  1. 首先,我们需要添加一个辅助方法来动态构建可选择目标模型的列表:

    1
    2
    3
    4
    5
    6
    7
    8
    from odoo import models, fields, api
    class LibraryBook(models.Model):
    # ...
    @api.model
    def _referencable_models(self):
    models = self.env['ir.model'].search([
    ('field_id.name', '=', 'message_ids')])
    return [(x.model, x.name) for x in models]
  2. 然后,我们需要添加引用字段并使用前面的函数提供可选模型列表:

    1
    2
    3
    ref_doc_id = fields.Reference(
    selection='_referencable_models',
    string='Reference Document')

由于我们正在更改模型的结构,因此需要升级模块来激活这些更改。

运行原理

引用字段类似于多对一字段,不同之处在于它们允许用户选择要链接到的模型。

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

1
[('res.users', 'User'), ('res.partner', 'Partner')]

但是,我们可以使用最常见的模型,而不是提供固定列表。为简单起见,我们使用所有具有消息传递功能的模型。使用_referencable_models 方法动态组装模型列表。

上例中,我们首先提供了一个函数来浏览所有可以引用的模型记录,以动态构建selection 属性。虽然这两种形式都允许,但是我们在引号内声明了函数名,而不是直接引用不带引号的函数。这样更灵活,它允许引用的函数只在代码的后面定义,而使用直接引用时这是不可以的。

该函数需要@api.model 装饰器,因为它在模型级别上运行,而不是在记录集级别上运行。

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

使用继承向模型添加功能

Odoo最重要的特性之一是模块插件能够在其他模块插件的基础上扩展功能,而无需编辑原始功能的代码。这可能是添加字段或方法、修改现有字段或扩展现有方法以执行附加逻辑。

根据官方文档,Odoo提供了三种继承方式:

  • 类继承(扩展)
  • 原型继承
  • 代理继承

我们将在单独的小节中介绍每一种继承方式。在本小节中我们将介绍类继承(扩展)。它用于向现有模型添加新字段或方法。

我们将扩展Odoo内置的合作伙伴模型res.partner 为其添加创作书籍数量 的计算字段。这涉及向现有模型添加字段和方法。

准备工作

我们将继续使用上一小节中的my_library 附加模块。

实现步骤

我们将扩展Odoo内置的合作伙伴模型。如果你还记得的话,我们已经在本章的向模型添加关联字段 中继承了res.parnter 模型。为了使解释尽可能简单,我们将在models/library_book.py 代码文件中重用res.partner 模型:

  1. 首先我们要确保在partner模型中存在authored_book_ids 反向关联,并添加计算域:

    1
    2
    3
    4
    5
    6
    7
    class ResPartner(models.Model):
    _inherit = 'res.partner'
    _order = 'name'
    authored_book_ids = fields.Many2many(
    'library.book', string='Authored Books')
    count_books = fields.Integer( 'Number of Authored Books',
    compute='_compute_count_books' )
  2. 接下来添加计算图书数量所需的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    # ...
    from odoo import api # if not already imported
    # class ResPartner(models.Model):
    # ...
    @api.depends('authored_book_ids')
    def _compute_count_books(self):
    for r in self:
    r.count_books = len(r.authored_book_ids)

最后我们需要升级附加模块才能使修改生效。

运行原理

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

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

字段也被增量修改。这意味着如果该字段已经存在于父类中,则只修改继承类中声明的属性;其他属性保持在父类中。

继承类中定义的方法替换父类中的方法。如果你不使用super 调用调用父方法,在这种情况下,父类中的方法将不会被执行,我们将失去父类中的功能。因此,每当您通过继承现有方法添加新逻辑时,您应该包含一个带有super 的语句以调用父类中的功能。这在第五章《基本服务器端开发》 中有更详细的讨论。

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

使用继承复制模型定义

我们已经在前一节中看到了类继承(扩展)。现在我们将看到原型继承,它用于复制现有模型的整个定义。在本节中,我们将复制library.book 模型。

准备工作

我们将继续使用上一小节中的my_library 附加模块。

实现步骤

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

  1. 将名为library_book_copy.py 的新文件添加到/my_library/models/ 目录。

  2. 将以下内容添加到library_book_copy.py 文件中:

    1
    2
    3
    4
    5
    from odoo import models, fields, api
    class LibraryBookCopy(models.Model):
    _name = "library.book.copy"
    _inherit = "library.book"
    _description = "Library Book's Copy"
  3. 将新文件引用导入/my_library/models/__init__.py 文件。更改后您的__init__.py 文件将如下所示:

    1
    2
    3
    from . import library_book
    from . import library_book_categ
    from . import library_book_copy

最后,我们需要升级附加模块才能使修改生效。进入Settings | Technical |
Database Structure | Models
菜单检查新模型的定义,您将在此处看到library.book.copy 模型的新条目。

提示
为了查看新模型的菜单和视图,您需要添加视图和菜单的XML定义。要了解有关视图和菜单的更多信息,请参阅请参阅第三章创建Odoo附加模块 中的添加菜单项和视图。

运行原理

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

在我们的示例中,Odoo将复制library.book 模型的定义并创建一个新模型library.book.copy 。新的library.book.copy 模型有自己的数据库表,其数据完全独立于library.book 父模型。由于它仍然继承自partner模型,因此对它的任何后续修改也会影响新模型。

原型继承复制父类的所有属性。它复制字段、属性和方法。如果要在子类中修改它们,只需向子类添加新定义即可。例如,library.book 模型具有_name_get 方法。如果要在子类中使用不同的_name_get 逻辑,需要重新定义library.book.copy 模型中的_name_get 方法。

警告
如果您在_inherit_name 属性中使用相同的模型名称,则原型继承不起作用。如果您确实在_inherit_name 属性中使用了相同的模型名称,那么它的行为就像普通的扩展继承一样。

扩展内容

在官方文档中,这称为原型继承,但在实践中很少使用。这样做的原因是代理继承通常以更有效的方式满足这一需求,而无需复制数据结构。有关这方面的更多信息,您可以参考下一小节使用代理继承将功能复制至另一个模型

使用代理继承将功能复制至另一个模型

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

传统的继承与面向对象编程中的概念有很大不同。反过来,委托继承也是类似的,因为可以创建一个新模型来包含父模型的特征。它还支持多态继承,我们从两个或多个其他模型继承。

我们图书馆中已经有书了。是时候给图书馆添加会员了。对于图书馆会员,我们需要在partner模型中找到的所有身份和地址数据,并且我们还希望它还能记录一些与会员资格相关的信息:开始日期、终止日期和卡号。

将这些字段添加到partner模型不是最佳解决方案,因为这些字段对于非会员是不需要的。我们需要将partner模型扩展到具有这些附加字段的新模型。

准备工作

我们将继续使用上一小节中的my_library 附加模块。

实现步骤

新的图书馆会员模型应该在它自己的Python代码文件中,但为了使解释尽可能简单,我们将重用models/library_book.py 文件:

  1. 添加新的模型,继承自res.partner 模型:

    1
    2
    3
    4
    5
    6
    7
    class LibraryMember(models.Model):
    _name = 'library.member'
    _inherits = {'res.partner': 'parent_id'}
    partner_id = fields.Many2one(
    'res.partner',
    ondelete='cascade'
    )
  2. 接着我们添加会员的其他特殊字段:

    1
    2
    3
    4
    5
    6
    # class LibraryMenber(models.Model):
    # ...
    date_start = fields.Date('Member Since')
    date_end = fields.Date('Termination Date')
    member_number = fields.Char()
    date_of_birth = fields.Date('Date of birth')

现在我们需要升级附加模块才能使修改生效。

运行原理

_inherits 模型属性填的是我们需要继承的父模型。在这种情况下,我们的示例中只有一个res.partner 。它的值是一个键值字典,其中键是继承的模型,值是用于链接到它们的字段名称。我们还必须在模型中定义的Many2one 字段。在我们的示例中,partner_id 是用于与Partner 父模型链接的字段。

为了更好的理解它的运行原理,让我们从数据库层面看看,当新建一个图书馆会员时它做了什么动作:

  • res_partner 表新增了一条记录。
  • library_member 表新增了一条记录。
  • library_member 表中的partner_id 字段存储的是res_partner 表记录的ID。

会员记录会自动链接到新的partner记录。这只是一个多对一的关系,但代理机制增加了一些魔力,使partner的字段可以在会员记录中使用,并且新的partner记录也会自动与新会员记录一起创建。

您可能想知道这个自动创建的partner记录并没有什么特别之处。它是一个普通的partner,如果您浏览partner模型,您将能够找到该记录(当然没有额外的会员特殊字段数据)。所有会员都是partner,但只有部分partner也是会员。

那么如果您删除了partner记录那会员记录会发生什么?您可以通过选择关系字段的ondelete 值来决定。对于partner_id ,我们使用了cascade 。这意味着当partner删除时相关的会员记录也会删除。我们本可以使用更保守的restrict 来禁止删除具有链接到会员的partner记录。在这种情况下,只有删除该会员才有效。

请务必注意,代理继承仅适用于字段继承,不适用于方法继承。因此如果partner模型有一个do_something() 方法,会员模型将不会自动继承它。

扩展内容

代理继承有一个捷径,您可以在Many2one 字段定义中使用delegate=True 属性,而不是创建_inherits 字典。 这样做与使用_inherits 选项效果完全相同。主要优点是这样写更简单。在给定的示例中,我们执行了与前一个相同的继承委托,但在这种情况下,我们没有创建_inherits 字典,而是在partner_id 字段中使用了delegate=True 选项:

1
2
3
4
5
6
7
class LibraryMember(models.Model):
_name = 'library.member'
partner_id = fields.Meny2one('res.partner', ondelete='cascade', delegate=True)
date_start = fields.Date('Member Since')
date_end = fields.Date('Termination Date')
member_mumber = fields.Char()
date_of_birth = fields.Date('Date of birth')

一个值得注意的代理继承案例是用户模型res.users 。它继承自partner模型 (res.partner )。 这意味着您可以在用户上看到的某些字段实际上存储在partner模型中(特别是名称字段)。创建新用户时,我们还会获得一个自动创建的新partner。

我们还应该提到,使用_inherit 的传统继承也可以将功能复制到新模型中,尽管效率较低。这在使用继承向模型添加功能中进行了讨论。

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

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

抽象模型允许我们创建一个通用模型,该模型实现了一些可以被常规模型继承的特性,以使该特性可用。

例如我们需要实现一个简单的存档功能。它将活动字段添加到模型中(如果它不存在)并提供存档方法来切换活动标志。这是有效的,因为active 是一个魔法字段。如果默认在模型中出现,active=False 的记录会在查询中被过滤掉。

然后我们将它添加到Library Books 模型中。

准备工作

我们将继续使用上一小节中的my_library 附加模块。

实现步骤

本节中我们需要添加存档功能到Library Books模型中。为了使解释尽可能简单,我们将把它塞进models/library_book.py 文件中:

  1. 为存档功能添加抽象模型。 它必须在Library Book 模型中定义,将在其中使用它:

    1
    2
    3
    4
    5
    6
    class BaseArchive(models.AbstractModel):
    _name = 'base.archive'
    active = fields.Boolean(default=True)
    def do_archive(self):
    for record in self:
    record.active = not record.active
  2. 接着我们将编辑Library Book 模型以继承上一步的抽象模型:

    1
    2
    3
    4
    class LibraryBook(models.Model):
    _name = 'library.book'
    _inherit = ['base.archive']
    # ...

需要升级附加模块才能激活更改。

运行原理

抽象模型基于models.AbstractModel 类,而不是通常的models.Model 。它具有常规模型的所有属性和功能;不同之处在于ORM不会在数据库中为其创建实际的表。这意味着它不能存储任何数据。它仅用作要添加到常规模型中的可重用功能的模板。

我们的存档抽象模型非常简单。它只是添加了活动字段和一个方法来切换活动标志的值,我们希望稍后通过用户界面上的按钮使用它。

当使用_inherit 属性定义模型类时,它会继承那些类的属性方法,并且在当前类中定义的属性方法会对这些继承的特性进行修改。

这里起作用的机制与常规模型扩展的机制相同(根据使用继承向模型添加功能)。 您可能已经注意到_inherit 使用模型标识符列表而不是具有一个模型标识符的字符串。事实上,_inherit 可以有两种形式。使用列表形式允许我们从多个(通常是抽象的)类继承。在这种情况下,我们只继承一个,所以一个文本字符串就可以了。为了说明的目的,使用了一个列表。

扩展内容

一个值得注意的内置抽象模型是mail.thread ,它由mail (Discuss) 附加模块提供。在模型上,它启用了为在许多表单底部看到的消息墙提供动力的讨论功能。

除了AbstractModel 之外,还有第三种模型类型可用:models.TransientModel 。这有一个类似models.Model 的数据库表示,但是在那里创建的记录应该是临时的,并且由服务器计划的作业定期清除。除此之外,瞬态模型就像常规模型一样工作。

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

Odoo14开发者指南第四章-应用模型【翻译】

https://www.junle.org/Odoo14开发者指南第四章-应用模型【翻译】/

作者

Junle

发布于

2022-06-08

更新于

2024-03-22

许可协议

评论