Odoo14开发者指南第五章-基本服务端开发【翻译】

第四章《应用模型》中,我们看到了如何在自定义模块中声明或扩展业务模型。那一章涵盖了计算字段的编写方法,以及约束字段的方法。本章重点介绍Odoo方法定义、记录集操作和扩展继承方法的服务器端开发基础知识。有了这个,您将能够在Odoo模块中添加/修改业务逻辑。

在本章中,我们将介绍以下内容:

技术要求

本章的技术要求包括可用的Odoo平台。

本章中使用的代码可以在https://github.com/PacktPublishing/Odoo-14-Development-Cookbook-Fourth-Edition/tree/master/Chapter05下载。

定义模型方法和使用API装饰器

在Odoo模型中,类定义了字段和业务逻辑方法。在第四章《应用模型》中,我们看到了如何将字段添加到模型中。现在我们将看到如何向模型添加方法和业务逻辑。

在本节中我们将看到如何编写一个可以被用户界面中的按钮或应用程序中的另一段代码调用的方法。此方法将作用于Library Books 并执行所需的操作以更改所选书籍的状态。

准备工作

本节假设你已经准备好一个实例,并且my_library 模块可用,如第三章《创建Odoo附加模块》中所述。 您需要在Library Books 模型中添加一个state 字段,其定义如下:

1
2
3
4
5
6
7
8
9
from odoo import models, fields, api
class LibraryBook(models.Model):
# [...]
state = fields.Selection([
('draft', 'Unavailable'),
('available', 'Available'),
('borrowed', 'Borrowed'),
('lost', 'Lost')],
'State', default="draft")

请参阅第三章《创建Odoo附加模块》,以获取更多信息。

实现步骤

要在library books上定义一个方法来更改所选书籍的状态,您需要在模型定义中添加以下代码:

  1. 添加辅助方法来检查是否允许状态转换:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @api.model
    def is_allowed_transition(self, old_state, new_state):
    allowed = [('draft', 'available'),
    ('available', 'borrowed'),
    ('borrowed', 'available'),
    ('available', 'lost'),
    ('borrowed', 'lost'),
    ('lost', 'available')]
    return (old_state, new_state) in allowed
  2. 添加一个方法以将某些书籍的状态更改为作为参数传递的新状态:

    1
    2
    3
    4
    5
    6
    def change_state(self, new_state):
    for book in self:
    if book.is_allowed_transition(book.state, new_state):
    book.state = new_state
    else:
    continue
  3. 通过调用change_state 方法添加一个更改图书状态的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    def make_available(self):
    self.change_state('available')

    def make_borrowed(self):
    self.change_state('borrowed')

    def make_lost(self):
    self.change_state('lost')
  4. <form> 视图中添加一个按钮和状态栏。这将帮助我们从用户界面触发这些方法:

    1
    2
    3
    4
    5
    6
    7
    8
    <form>
    ...
    <button name="make_available" string="Make Available" type="object"/>
    <button name="make_borrowed" string="Make Borrowed" type="object"/>
    <button name="make_lost" string="Make Lost" type="object"/>
    <field name="state" widget="statusbar"/>
    ...
    </form>

更新或安装模块以使这些更改可用。

运行原理

本节的代码定义了一些方法。它们是普通的Python方法,它们的第一个参数是self ,也可以有其他参数。一些方法用odoo.api 模块中的装饰器装饰。

提示
API装饰器最初是在Odoo 9.0中引入的,用于支持新旧框架。从Odoo 10.0开始,不再支持旧的API,但仍然使用一些装饰器,例如@api.model

编写新方法时,如果不使用任何装饰器,则该方法将在记录集上执行。在这样的方法中,self是一个可以引用任意数量的数据库记录(这包括空记录集),并且代码通常会循环遍历self 中的记录以对每个单独的记录执行某些操作。

@api.model 装饰器类似,但它用于仅对模型很重要的方法,而不是记录集的内容,方法不对其进行操作。 这个概念类似于Python的@classmethod 装饰器。

在第1步中,我们创建了is_allowed_transition() 方法。此方法的目的是验证从一种状态到另一种状态的转换是否有效。允许列表中的元组是可用的转换。例如,我们希望不允许从lost 状态转换到borrowed 状态,这就是我们没有加入('lost, 'borrowed')的原因。

在第2步中,我们创建了change_state() 方法。此方法的目的是更改图书的状态。调用此方法时,它将书的状态更改为new_state 参数给定的状态。如果允许转换它只会更改图书状态。我们在这里使用了一个for 循环,因为self 可以包含多个记录集。

在第3步中,我们通过调用change_state() 方法创建了更改图书状态的方法。在我们的例子中,这个方法将由添加到用户界面的按钮触发。

在第4步中,我们在<form> 视图中添加了<button> 标签。 单击此按钮后,Odoo Web 客户端将调用name 属性中的Python函数。 请参阅第九章《后端视图》中的向表单添加按钮 一节,了解如何从用户界面调用此类方法。我们还添加了带有状态栏小部件的状态字段,以在<form> 视图中显示图书的状态。

当用户从用户界面单击按钮时,将调用步骤3中的方法之一。在这里,self 将是包含library.book 模型记录的记录集。之后我们调用change_state() 方法并根据单击的按钮传递适当的参数。

当调用change_state() 时,selflibrary.book 模型的相同记录集。change_state() 方法的主体循环遍历self 以处理记录集中的每一本书。循环遍历self 一开始看起来很奇怪,但你很快就会习惯这种模式。

在循环内部change_state() 调用is_allowed_transition() ,它使用book 局部变量进行的,但它可以在library.book 模型的任何记录集上进行,包括例如self ,因为is_allowed_transition() 是使用@api.model 修饰的。如果允许转换,则change_state() 通过为记录集的属性分配一个值来将新状态分配给书籍。这仅对长度为1 的记录集有效,用于确保和遍历self 的情况一致。

向用户报告错误

在方法执行期间,有时需要中止处理,因为用户请求的操作无效或已满足错误条件。本节向你展示了如何通过显示有用的错误消息来管理这些情况。

准备工作

本节假设你已经准备好一个实例,并且my_library 插件模块可用,如前一节中所述。

实现步骤

我们将对上一节中的change_state 方法进行改造,并在用户尝试更改is_allowed_transition 方法不允许的状态时显示有用的消息。 执行以下步骤开始:

  1. 在Python文件的开头添加以下导入

    1
    2
    from odoo.exceptions import UserError
    from odoo.tools.translate import _
  2. 修改change_state 方法并从else 部分抛出UserError 异常:

    1
    2
    3
    4
    5
    6
    7
    def change_state(self, new_state):
    for book in self:
    if book.is_allowed_transition(book.state, new_state):
    book.state = new_state
    else:
    msg = _('Moving from %s to %s is not allowed') % (book.state, new_state)
    raise UserError(msg)

运行原理

当Python中抛出异常时,它会向上传播调用堆栈,直到它被处理。在Odoo中,RPC (remote procedure call) 层会响应Web客户端捕获所有异常,并根据异常类在Web客户端上触发不同的行为。

odoo.exceptions 中未定义的任何异常都将作为内部服务器错误(HTTP状态500 )与堆栈跟踪一起处理。UserError 将在用户界面中显示错误消息。本节的代码引发UserError 以确保消息以用户友好的方式显示。在所有情况下,当前数据库事务都会回滚。

我们正在使用一个名字奇怪的函数:_(),它是在odoo.tools.translate 中定义的。此函数用于将字符串标记为可翻译,并在运行时根据上下文中的语言检索已翻译的字符串。有关这方面的更多信息,请查阅第十一章《国际化》。

重要提示
使用_() 函数时,请确保只传递带有插值占位符的字符串,而不是整个插值字符串。例如,_(‘Warning: could not find %s’) % value 是正确的,但_(‘Warning: could not find %s’ % value) 是不正确的,因为第一个字符串不能在翻译数据库中找到替换值。

扩展内容

有时您正在处理容易出错的代码,这意味着您正在执行的操作可能会产生错误。Odoo将捕获此错误并向用户显示回溯。如果您不想向用户显示完整的错误日志,您可以缓存错误并使用有意义的消息引发自定义异常。在提供的示例中,我们从try…cache 块生成UserError,因此Odoo现在不会显示完整的错误日志,而是显示带有有意义消息的警告:

1
2
3
4
5
6
7
8
def post_to_webservice(self, data):
try:
req = requests.post('http://my-test-service.com', data=data, timeout=10)
content = req.json()
except IOError:
error_msg = _("Something went wrong during data submission")
raise UserError(error_msg)
return content

odoo.exceptions 中定义了更多的异常类,它们都派生自基本的遗留的except_orm 异常类。它们中的大多数仅在内部使用,除了以下内容:

  • ValidationError :当不遵守字段上的Python约束时会引发此异常。请参阅第四章《应用模型》向模型添加约束验证 以获取更多信息。
  • AccessError :当用户尝试访问不允许的内容时,通常会自动生成此错误。如果要显示代码中的访问错误,可以手动引发错误。
  • RedirectWarning :使用此错误您可以显示带有错误消息的重定向按钮。你需要给这个异常传递两个参数:第一个参数是action ID,第二个参数是错误信息。
  • Warning :在Odoo 8.0中,odoo.exceptions.Warning 与9.0及更高版本中的UserError 所起的作用是相同的。它现在已被弃用,因为该名称具有欺骗性(这是一个错误,而不是警告)并且它与Python内置的警告类发生冲突。它只是为了向后兼容而保留的,您应该在代码中使用UserError

获取不同模型的空记录集

在编写Odoo代码时,当前模型的方法可以通过self 获得。如果您需要处理不同的模型,则无法直接实例化该模型的类;首先您需要获取该模型的记录集。

本节向您展示了如何在模型方法中为在Odoo中注册的任何模型获取空记录集。

准备工作

本节将重用my_library 附加模块中库示例的设置。

我们将在library.book 模型中编写一个小方法并搜索所有library.members。为此我们需要为library.members 获取一个空记录集。确保您已添加library.members 模型和该模型的访问权限。

实现步骤

要在library.book 的方法中获取library.members 的记录集,您需要执行以下步骤:

  1. LibraryBook 类中添加名为get_all_library_members 方法:

    1
    2
    3
    4
    5
    6
    7
    8
    class LibraryBook(models.Model):
    # ...
    def log_all_library_members(self):
    # This is an empty recordset of model library.member
    library_member_model = self.env['library.member']
    all_members = library_member_model.search([])
    print("ALL MEMBERS:", all_members)
    return True
  2. <form> 视图添加一个按钮来调用我们的方法:

    1
    <button name="log_all_library_members" string="Log Members" type="object"/>

更新模块以应用更改。之后您将在本书的<form> 视图中看到Log Members 按钮。单击该按钮后,您将在服务器日志中看到该成员的记录集。

运行原理

在启动时Odoo加载所有模块并组合从模型派生的各种类,并定义或扩展给定模型。这些类存储在Odoo注册表中,按名称索引。任何记录集的env 属性(可用作self.env )是odoo.api 模块中定义的Environment 类的一个实例。

Environment 类在Odoo开发中起着核心作用:

  • 它通过模拟Python字典提供对注册表的快捷访问。如果您知道要查找的模型的名称,可通过self.env[model_name] 来获取该模型的空记录集。此外,记录集将共享self 的环境。
  • 它有一个cr 属性,这是一个可以用来传递原生SQL 查询的数据库游标。有关这方面的更多信息,请参阅第八章《高级服务器端开发》中的执行原生SQL查询
  • 它有一个user 属性,是对当前执行的用户的引用。请查看第八章《高级服务器端开发》中的更改执行操作的用户 以了解更多信息。
  • 它有一个context 属性,这是一个包含调用上下文的字典。这包括有关用户语言、时区、当前记录选择等的信息。有关这方面的更多信息,请参阅第八章《高级服务器端开发》中的使用修改的上下文调用方法 方法一节。

search() 的调用将在稍后的搜索记录中解释

也可以看看
有时您想使用环境的修改版本,一个这样的例子是您想要一个具有不同用户和语言的环境。在第八章《高级服务器端开发》中您将学习如何在运行时修改环境。

创建新记录

编写业务逻辑方法时的一个常见的要求是创建新记录。本节解释了如何创建library.book.category 模型的记录。对于我们的示例,我们将为library.book.category 模型创建虚拟类别的方法。要触发此方法,我们将在<form> 视图中添加一个按钮。

准备工作

您需要知道要为其创建记录的模型的结构,尤其是它们的名称和类型,以及这些字段上存在的任何约束(例如其中一些是否是强制性的)。

本节我们将重用第四章《应用模型》my_library 模块。看下面的例子快速回忆library.book.category 模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
class BookCategory(models.Model):
_name = 'library.book.category'
name = fields.Char('Category')
description = fields.Text('Description')
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')

确保您已经为library.book.category 模型添加了菜单、视图和访问权限。

实现步骤

要创建具有一些子类别,您需要执行以下步骤:

  1. library.book.category 模型中创建一个名为create_categories 的方法:

    1
    2
    def create_categories(self):
    ......
  2. 在此方法的主体内,为第一个子类别的字段准备一个值字典:

    1
    2
    3
    4
    categ1 = {
    'name': 'Child category 1',
    'description': 'Description for child 1'
    }
  3. 为第二类字段准备一个值字典:

    1
    2
    3
    4
    categ2 = {
    'name': 'Child category 2',
    'description': 'Description for child 2'
    }
  4. 为父类别的字段准备一个值字典:

    1
    2
    3
    4
    5
    6
    7
    8
    parent_category_val = {
    'name': 'Parent category',
    'email': 'Description for parent category',
    'child_ids': [
    (0, 0, categ1),
    (0, 0, categ2),
    ]
    }
  5. 调用create() 方法创建新记录:

    1
    record = self.env['library.book.category'].create(parent_category_val)
  6. <form> 视图中添加一个按钮以从用户界面触发create_categories 方法:

    1
    <button name="create_categories" string="Create Categories" type="object"/>

运行原理

要为模型创建新记录,我们可以在与模型相关的任何记录集上调用create(values) 方法。此方法返回一个长度为1 的新记录集,其中包含新记录,其字段值在值字典中指定。

在字典中键是字段的名称,值是对应于字段的值。根据字段类型您需要为值传递不同的Python数据类型:

  • Text 字段对应Python的字符串。
  • FloatInterger 字段对应Python的浮点型和整型。
  • Boolean 字段对应Python的布尔型。
  • Date 字段对应Python的datetime.date对象。
  • Datetime 字段对应Python的datetime.datetime对象。
  • Binary 字段的值是Base64编码的字符串。Python标准库中的base64 模块提供了诸如encodebytes(bytestring) 之类的方法来对Base64 中的字符串进行编码。
  • Many2one 字段的值以整数形式给出,该整数必须是关联记录的数据库ID。
  • One2manyMany2many 字段使用特殊语法,该值是一个包含三个元素的元组的列表,如下所示:
Tuple Effect
(0, 0, dict_val) 创建与主记录关联的新记录。
(6, 0, id_list) 在正在创建的记录和现有记录之间创建关联,其ID在名为id_list的Python列表中。 注意:当在One2many 字段上使用时,这会删除此前关联中的记录。

本节中我们为我们想要创建的公司中的两个联系人创建字典,然后我们使用我们解释过的(0, 0, dict_val) 语法在正在创建的公司的字典的child_ids 条目中使用这些字典。

在步骤5中调用create() 时,会创建三个记录:

  • 一个用于父书类别,由create 返回。
  • 子书类别的两条记录,在record.child_ids 中可用。

扩展内容

如果模型为某些字段定义了一些默认值,则不需要做任何特别的事情。create() 将自动处理字典中不存在的字段的默认值。

create() 方法还支持批量创建记录。要批量创建多条记录,您需要将多个值的列表传递给create() 方法,如下例所示:

1
2
3
4
5
6
7
8
9
categ1 = {
'name': 'Category 1',
'description': 'Description for Category 1'
}
categ2 = {
'name': 'Category 2',
'description': 'Description for Category 2'
}
multiple_records = self.env['library.book.category'].create([categ1, categ2])

更新记录集记录的值

业务逻辑通常要求我们通过更改某些字段的值来更新记录。本节向你展示了如何修改partner的date 字段。

准备工作

本章将使用与上一节相同的library.book 定义。您可以参考这个简化的定义来了解这些字段。

我们在library.book 模型中有date_release 字段。出于说明目的,我们将通过单击按钮更新此字段。

实现步骤

  1. 要实现更新书籍的data_update 字段,你可以新增一个名为change_update_date() 的方法,它的定义如下:

    1
    2
    3
    def change_release_date(self):
    self.ensure_one()
    self.date_release = fields.Date.today()
  2. 然后你需要在<form> 视图中添加一个按钮,定义如下:

    1
    <button name="change_release_date" string="Update Date" type="object"/>
  3. 重新启动服务器并更新my_library 模块以查看更改。单击更新日期按钮后,更新日期将更改。

运行原理

该方法首先通过调用ensure_one() 检查作为self 传递的book 记录集是否恰好包含一条记录。如果不是这种情况,此方法将引发异常,并且处理将中止。这是必要的,因为我们不想更改多条记录的日期。如果要更新多个值,可以删除ensure_one() 并使用记录集上的循环更新属性。

最后,该方法修改书籍记录的属性值。它使用当前日期更新date_release 字段。只需修改记录集的字段属性,就可以进行写操作。

扩展内容

如果您想将新值写入记录字段,可以使用三个选项:

  • 选项一是本节中使用的选项。它通过将值直接分配给表示记录字段的属性,这种方法可用在所有上下文中。它不可能一次性为所有记录集元素赋值,因此您需要遍历记录集,除非您确定您只处理一条记录。

  • 选项二是通过将字典映射字段名称传递给您要设置的值来使用update() 方法来赋值。这也仅适用于长度为1 的记录集。当您需要在同一笔记录一次更新多个字段的值时,它可以节省一些输入。将本节的第2步重写为以下内容来使用此选项:

    1
    2
    3
    4
    5
    6
    7
    def change_update_date(self):
    self.ensure_one()
    self.update({
    'date_release': fields.Datetime.now(),
    'another_field': 'value'
    ...
    })
  • 选项三是调用write() 方法,传递一个将字段名称映射到您要设置的值的字典。 此方法适用于任意大小的记录集,并且当前两个选项对每个记录和每个字段执行一次数据库调用时,将在一次数据库操作中使用指定值更新所有记录。但是,它有一些限制:如果数据库中还没有记录,它就不起作用(有关这方面的更多信息,请参阅第八章《高级服务器端开发》中的关于定义onchange方法 )。此外,在编写关联字段时,它需要一种特殊的格式,类似于create() 方法使用的格式。查看下表以了解用于为关系字段生成不同值的格式:

    Tuple Effect
    (0, 0, dict_val) 这将创建一个与主记录关联的新记录。
    (1, id, dict_val) 这将使用提供的值更新具有指定ID的关联记录。
    (2, id) 这将从关联记录中删除具有指定ID的记录,并从数据库中删除。
    (3, id) 这将从关联记录中删除具有指定ID的记录。该记录不会从数据库中删除。
    (4, id) 这会将具有提供的ID的现有记录添加到相关记录列表中。
    (5, ) 这将删除所有其他相关记录,相当于为每个关联id调用**(3, id)** 。
    (6, 0, id_list) 这会在正在更新的记录与现有记录之间创建关联,其ID在名为id_list 的Python列表中。

重要提示
操作类型 1、2、3和5 不能与create() 方法一起使用。

搜索记录

搜索记录也是业务逻辑方法中的常见操作。本节向您展示如何按名称和类别查找书籍。

准备工作

本章将使用与上一节相同的library.book 定义。我们将新增一个名为find_book(self) 的新方法。

实现步骤

要查找书籍,您需要执行以下步骤:

  1. library.book 模型种添加find_book 方法:

    1
    2
    def find_book(self):
    ...
  2. 为您的条件编写搜索域:

    1
    2
    3
    4
    5
    6
    7
    domain = [
    '|',
    '&', ('name', 'ilike', 'Book Name'),
    ('category_id.name', 'ilike', 'Category Name'),
    '&', ('name', 'ilike', 'Book Name 2'),
    ('category_id.name', 'ilike', 'Category Name 2')
    ]
  3. 使用域调用search() 方法,这将返回记录集:

    1
    books = self.search(domain

书籍变量将有一个搜索书籍的记录集。您可以打印或记录该变量以在服务器日志中查看结果。

运行原理

第1部定义方法。

第2步局部变量中创建搜索域。通常您会在搜索调用中看到这种创建内联,但对于复杂的域,最好单独定义它。

有关搜索域语法的完整说明,请参阅第九章《后端视图》中的在记录列表上定义域过滤器

第3步使用域调用search() 方法。该方法返回一个记录集,其中包含与域匹配的所有记录,然后可以进一步处理。在本节中我们只使用域调用方法,但也支持以下关键字参数:

  • offset=N :这用于跳过与查询匹配的前N 条记录。这可以与limit 一起使用来实现分页或在处理大量记录时减少内存消耗。它的默认值为0

  • limit=N :表示最多返回N 条记录。默认情况下是没有限制。

  • order=sort_specification :这用于返回的记录集中的排序。 默认情况下,顺序由模型类的_order 属性给出。

  • count=boolean :如果为True ,则返回记录数而不是记录集。它默认为False

    重要提示
    我们建议使用search_count(domain) 方法而不是search(domain, count=True) ,因为该方法的名称以更清晰的方式传达了行为。两者都会给出相同的结果。

有时您需要从另一个模型中搜索,以self 来搜索将返回当前模型的记录集。要从另一个模型中搜索,我们需要为模型获取一个空记录集。例如,假设我们要搜索一些联系人。为此,我们需要在res.partner 模型上使用search() 方法。请参考以下代码,这里我们得到res.partner 的空记录集来搜索联系人:

1
2
3
4
5
6
7
def find_partner(self):
PartnerObj = self.env['res.partner']
domain = [
'&', ('name', 'ilike', 'Parth Gajjar'),
('company_id.name', '=', 'Odoo')
]
partner = PartnerObj.search(domain)

在上面的代码中,你可以在域中省略’& ‘,因为当你不指定域时,Odoo会默认使用’& ‘。

扩展内容

我们之前说过search() 方法返回所有匹配域的记录。这实际上并不完全正确。安全规则确保用户只获得他们具有读取 访问权限的那些记录。

此外,如果模型有一个名为active 的布尔字段,并且没有搜索域的术语在该字段上指定条件,则搜索会添加一个隐式条件以仅返回active=True 的记录。 因此,如果您希望搜索返回某些内容,但只得到空记录集,请确保检查active 字段的值(如果存在)以检查记录规则。

有关不添加隐式active=True 条件的方法,请参阅第八章《高级服务器端开发》中的使用不同上下文调用方法一节。查看第十章《安全访问》中的使用记录规则限制记录访问 了解有关记录级访问规则的更多信息。

如果由于某种原因,您需要自己编写原生SQL查询来查找记录ID,请确保使用self.env[‘record.model’].search([(‘id’, ‘in’, tuple(ids)) ]).ids 在检索ID之后以确保应用安全规则。这在使用记录规则来区分的多公司的Odoo实例中尤其重要。

组合记录集

有时,您会发现您获得的记录集并非您所需要的。本节将展示各种组合它们的方法。

准备工作

在开始学习本节内容之前,你需要有两个或多个相同模型的记录集。

实现步骤

执行以下步骤以对记录集执行常见操作:

  1. 要将两个记录集合并为一个,同时保留它们的顺序,请使用以下操作:

    1
    result = recordset1 + recordset2
  2. 要将两个记录集合并为一个,同时确保结果中没有重复项,请使用以下操作:

    1
    result = recordset1 | recordset2
  3. 要查找两个记录集共有的记录,请使用以下操作:

    1
    result = recordset1 & recordset2

运行原理

记录集的类重载了各种Python的运算符。以下是可用于记录集的最有用的Python运算符的汇总表:

Oprator Action performed
R1 + R2 这将返回一个新记录集,其中包含来自R1的记录,然后是来自R2的记录。这会在记录集中生成重复记录。
R1 - R2 这将返回一个新记录集,其中包含R1中不在R2中的记录,保留排序。
R1 & R2 这将返回一个新记录集,其中包含属于R1和R2的所有记录(记录集的交集)。此处不保留顺序,但没有重复。
R1 R2
R1 == R2 如果两个记录集包含相同的记录,则为True
R1 <= R2 如果R1中的所有记录都是R2的子集,则为True
R1 < R2 如果R1中的所有记录都是R2的子集,则为True
R1 >= R2 如果R1中的所有记录都是R2的超集,则为True
R1 > R2 如果R1中的所有记录都是R2的超集,则为True
R1 != R2 如果R1和R2不包含相同的记录,则为True
R1 in R2 如果R1(必须是一条记录)是R2的一部分,则为True
R1 not in R2 如果R1(必须是一条记录)不属于R2,则为True

还有就地运算符,+=、-=、&= 和 |=,它们是修改左侧操作数而不是创建新记录集。这些在更新记录的One2manyMany2many 字段时非常有用。有关此示例,请参阅更新记录集记录的值

过滤记录集

在某些情况下,您已经有一个记录集,但您只需要对某些记录进行操作。当然,您可以对记录集进行迭代,检查每次迭代的条件并根据检查结果采取行动。构造一个只包含感兴趣的记录并在该记录集上调用单个操作的新记录集可能更容易,并且在某些情况下更有效。

本节展示了如何使用filter() 方法根据条件提取记录集的子集。

准备工作

我们将重用创建新记录一节中显示的简化library.book 模型。本节定义了一种从提供的记录集中提取具有多个作者的书籍的方法。

  1. 定义接受原始记录集的方法

    1
    2
    @api.model
    def books_with_multiple_authors(self, all_books):
  2. 定义一个内部谓词函数:

    1
    2
    3
    4
    def predicate(book):
    if len(book.author_ids) > 1:
    return True
    return False
  3. 调用filter() ,如下:

    1
    return all_books.filter(predicate)

您可以打印或记录此方法的结果以在服务器日志中查看它。有关更多信息,请参阅本节的示例代码。

运行原理

filter() 方法的实现会创建一个空记录集。谓词函数计算结果为True 的所有记录都添加到这个空记录集中。最终返回新记录集。原始记录集中的记录顺序被保留。

前面的例子使用了一个命名的内部函数。对于这样简单的谓词,您经常会发现使用了一个匿名Lambda 函数:

1
2
3
@api.model
def books_with_multiple_authors(self, all_books):
return all_books.filter(lambda b: len(b.author_ids) > 1)

实际上,您需要根据字段的值在Python意义上为真(非空字符串、非零数字、非空容器等)这一事实来过滤记录集。因此,如果要过滤具有类别集合的记录,可以像这样传递字段名称进行过滤:all_books.filter(‘category_id’)

扩展内容

请记住filter() 是在内存中运行的。如果您需要在关键路径上优化方法的性能,您可能希望使用搜索域甚至迁移到SQL,但以牺牲可读性为代价。

遍历记录集关联

使用长度为1 的记录集时,各种字段可用作记录属性。关联属性(One2manyMany2oneMany2many ) 也可用于记录集的值。例如,假设我们要从library.book 模型的记录集中访问类别的名称。您可以通过遍历Many2one 字段的category_id 来访问类别名称,如下所示:book.category_id.name。但是,当使用具有多个记录的记录集时,不能使用属性。

本节向你展示了如何使用mapped() 方法来遍历记录集关系。我们将编写一个方法来从书籍记录集中检索作者姓名,并作为参数传递。

准备工作

本章重用与创建新记录相同的library.book 模型。

实现步骤

要从图书记录集中获取作者姓名,您需要执行以下步骤:

  1. 定义一个名为get_author_name() 的函数:

    1
    2
    @api.model
    def get_author_name(self, books):
  2. 调用mapped() 获取partner的联系人邮箱地址:

    1
    return books.mapped('author_ids.name')

运行原理

第1步只是定义方法。第2步我们调用mapped(path) 方法遍历记录集的字段;path 是一个字符串,其中包含用点分隔的字段名称。对于路径中的每个字段,mapped() 生成一个新记录集,其中包含与该字段相关的所有记录与当前记录集中的所有元素,然后路径中的下一个元素应用于该新记录集。如果路径中的最后一个字段是关联字段,则mapped() 将返回一个记录集;否则,返回的是Python列表。

mapped() 方法有两个有用的属性:

  • 如果路径是单个标量字段名称,则返回的列表与处理的记录集的顺序相同。

  • 如果路径包含关联字段,则不会保留顺序,但会从结果中删除重复项。

    重要信息
    当您想对self 中的所有记录的Many2many 字段指向的所有记录执行操作时,第二个属性非常有用,但是您需要确保该操作只执行一次(即使self 中的两个记录共享同一个目标记录)。

扩展内容

使用mapped() 时,请记住它在Odoo服务器内部的内存中运行的,通过重复遍历关联并因此进行SQL查询,这效率可能不高。但是,这种代码更简洁且可读性高。如果您尝试在实例性能的关键路径上优化方法,您可能需要重写对mapped() 的调用并以适当的域以search() 进行表现,或者甚至转移到SQL(代码可读性成本增加)。

mapped() 方法也可以通过函数作为参数来调用。在这种情况下,它返回一个列表,包含应用于self 每条记录的函数的结果列表,或者返回在函数返回的是记录集的情况下由该函数返回的记录集的并集。

也可以看看

  • 搜索记录章节。
  • 第八章《高级服务器端开发》中的执行原生SQL查询章节。

排序记录集

当您使用search() 方法获取记录集时,您可以传递一个可选参数order 来获取特定排序的记录集。如果您已经拥有来自前一段代码的记录集并且想要对其进行排序,这将非常有用。如果您使用集合操作来组合两个记录集,它也可能很有用,例如,这会导致排序丢失。

本节向你展示了如何使用sorted() 方法对现有记录集进行排序。我们将按发行日期对书籍进行排序。

准备工作

本章重用与创建新记录相同的library.book 模型。

实现步骤

您需要执行以下步骤来获取基于release_date 排序的图书记录集:

  1. 定义名为sort_books_by_date()

    1
    2
    @api.model
    def sort_books_by_date(self, books):
  2. 如给定示例所示,使用sorted() 方法根据release_date 字段对图书记录进行排序:

    1
    return books.sorted(key='release_date')

运行原理

第1步只是定义方法。在步骤2中,我们调用书籍记录集中的sorted() 方法。在内部,sorted() 方法将获取作为键参数传递的字段的数据。然后,通过使用Python的原生排序方法,返回一个排序的记录集。

它还有一个可选参数reverse=True ,它以相反的顺序返回一个记录集。反向使用如下:

1
books.sorted(key='release_date', reverse=True)

扩展内容

sorted() 方法将对记录集中的记录进行排序。不带参数调用,默认使用模型的_order 属性来排序。也可以传递一个函数以与Python内置的sorted(sequence, key) 函数相同的方式计算比较。

重要笔记
当使用模型默认的_order参数时,排序会在数据库中进行,执行新的SELECT函数获取排序。否则排序由Odoo执行。根据所操作的内容以及记录集的大小,可能存在一些重要的性能差异。

扩展模型中定义的业务逻辑

在Odoo中,将应用程序功能划分为不同的模块是一种非常常见的做法。通过这样做,您可以通过安装/卸载应用程序来简单地启用/禁用功能。而且,当您向现有应用程序添加新功能时,就需要自定义在原始应用程序中定义的某些方法的行为。有时,您还想向现有模型添加新字段。这在Odoo中是一项非常简单的任务,也是底层框架最强大的功能之一。

在本节中,我们将看到如何从另一个模块中的方法扩展一个方法的业务逻辑。我们还将从新模块向现有模块添加新字段。

准备工作

对于本节我们将继续使用上一节中的my_library 模块。确保在my_library 模块中有library.book.category 模型。

对于本节我们将创建一个名为my_library_return 的新模块,它依赖于my_library 模块。在新模块中,我们将管理借书的归还日期。我们还将根据类别自动计算归还日期。

在第四章《应用模型》中的使用继承向模型添加功能小节中,我们看到了如何向现有模型添加字段。在这个模块中,需要扩展library.book 模型如下:

1
2
3
class LibraryBook(models.Model):
_inherit = 'library.book'
date_return = fields.Date('Date to return')

然后扩展library.book.category 模型,如下:

1
2
3
4
5
6
class LibraryBookCategory(models.Model):
_inherit = 'library.book.category'
max_borrow_days = fields.Integer(
'Maximum borrow days',
help="For how many days book can be borrowed",
default=10)

要在视图中添加此字段,您需要遵循第九章《后端视图》中的更改现有视图-视图继承 章节。您可以在https://github.com/PacktPublishing/Odoo-13-Development-Cookbook-Fourth-Edition找到完整的代码示例。

实现步骤

要扩展library.book 模型中的业务逻辑,您需要执行以下步骤:

  1. my_library_return 中,当我们将图书状态更改为已借阅时,我们希望在图书记录中设置date_return 。 为此,我们将重写my_module_return 模块中的make_borrowed 方法:

    1
    2
    3
    4
    def make_borrowed(self):
    day_to_borrow = self.category_id.max_borrow_days or 10
    self.date_return = fields.Date.today() + timedelta(days=day_to_borrow)
    return super(LibraryBook, self).make_borrowed()
  2. 我们还想在图书归还时重置date_return ,因此我们将重写make_available 方法来重置日期:

    1
    2
    3
    def make_available(self):
    self.date_return = False
    return super(LibraryBook, self).make_available()

运行原理

步骤1和2对业务逻辑进行了扩展。我们定义了一个扩展的library.books 模型,并重新定义了make_borrowed()make_available() 方法。在这两个方法的最后一行,返回的是父类实现的结果:

1
return super(LibraryBook, self).make_borrowed()

对于Odoo模型,通过查看Python类定义,父类并不是您所期望的。框架为我们的记录集动态生成了一个类层次结构,父类是我们依赖的模块中模型的定义。因此使用super() 方法从my_module 中调用library.book 中的实现。 本例中的make_borrowed() 需要将图书的状态更改为已借阅。 因此使用super() 调用父类中的方法并将图书状态设置为已借阅。

扩展内容

在本节中我们选择扩展默认的实现方法。在make_borrow()make_available() 方法中,我们在super() 调用之前修改了返回的结果。请注意,当您调用super() 时,它将执行父类中的逻辑。也可以在super() 调用之后执行一些操作。当然,我们也可以同时做这两件事。

但是,更改方法中间的行为更加困难。为此我们需要重构代码,以便我们可以将扩展点提取到单独的方法中,并在扩展模块中重写这个新方法。

您可能很想完全重写一个方法。这样做时要非常小心。如果您不调用方法的 super() 实现,您将破坏扩展机制并可能破坏扩展方法的附加组件,这意味着永远不会调用扩展方法。除非您在受控环境中工作,您确切知道安装了哪些附加组件,并且您已经检查过您没有破坏它们,请避免这样做。此外如果必须确保以非常明显的方式记录您正在做的事情。

在调用方法的原始实现之前和之后可以做什么?有很多东西包括(但不限于)以下内容:

  • 修改传递给原始实现的参数(之前)
  • 修改传递给原始实现的上下文(之前)
  • 修改原始实现返回的结果(之后)
  • 调用另一个方法(之前和之后)
  • 创建记录(之前和之后)
  • 在禁止的情况下(之前和之后)抛出UserError 错误以取消执行
  • self 拆分为更小的记录集,并以不同的方式(之前)在每个子集上调用原始实现

扩展write()和create()

本章的扩展模型中定义的业务逻辑一节向我们展示了如何扩展定义在模型类上的方法。您应该知道,在父类模型上定义的方法也是模型的一部分。这意味着models.Model 上定义的所有基础方法(实际上是models.BaseModel ,它是models.Model 的父类)也是可用的并且可以扩展。

本节向你展示了如何扩展create()write() 来控制对记录的某些字段的访问。

准备工作

我们将使用第三章《创建Odoo附加模块》中的my_library 模块来扩展。

manager_remarks 字段添加到library.book 模型。我们只希望Library Managers 组的成员能够写入该字段:

1
2
3
4
from odoo import models, api, exceptions
class LibraryBook(models.Model):
_name = 'library.book'
manager_remarks = fields.Text('Manager Remarks')

manager_remarks 字段添加到view/library_book.xml 文件的<form> 视图,以便从用户界面访问此字段:

1
<field name="manager_remarks"/>

修改security/ir.model.access.csv 文件,为Library用户提供写入权限:

1
2
3
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
acl_book_user,library.book_default,model_library_book,base.group_user,1,1,0,0
acl_book_librarian,library.book_librarian,model_library_book,group_librarian,1,1,1,1

实现步骤

为了防止非librarian组成员的用户修改manager_remarks 的值,您需要执行以下步骤:

  1. 扩展create() 方法,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @api.model
    def create(self, values):
    if not self.user_has_groups('my_library.group_librarian'):
    if 'manager_remarks' in values:
    raise UserError(
    'You are not allowed to modify '
    'manager_remarks'
    )
    return super(LibraryBook, self).create(values)
  2. 扩展write() 方法,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    def write(self, values):
    if not self.user_has_groups('my_library.group_librarian'):
    if 'manager_remarks' in values:
    raise UserError(
    'You are not allowed to modify '
    'manager_remarks'
    )
    return super(LibraryBook, self).write(values)

安装模块以查看运行中的代码。现在只有manager类型的用户可以修改manager_remarks 字段。要测试此实现,您可以以demo用户登录或撤销当前用户的图书管理员访问权限。

运行原理

第1步重新定义create() 方法。在调用create() 的基础实现之前,我们的方法使用user_has_groups() 方法检查用户是否属于my_library.group_librarian 组(这是组的XML ID)。如果用户不属于my_library.group_librarian 组并且为manager_remarks 传递了一个值,则会抛出UserError 异常,从而阻止创建记录。此检查应放在调用父类实现之前执行。

第2步对write() 方法执行相同的操作。在写入之前,我们检查组和值中是否存在要写入的字段,如果有问题则抛出UserError 异常

重要笔记
当在Web客户端中将该字段设置为只读时不会阻止RPC调用写入它。这就是我们扩展create()write() 的原因。

在本节中,你已经看到了如何重写create()write() 方法。 但请注意,这不仅限于create()write() 方法。您可以重写任何模型方法。例如假设您想在删除记录时执行某些操作。为此您需要重写unlink() 方法(删除记录时会调用unlink() 方法)。下面是重写unlink() 方法的小代码片段:

1
2
3
def unlink(self):
# 这里写你的业务逻辑
return super(LibraryBook, self).unlink()

警告
在Odoo中重写方法时,千万不要忘记调用super() 方法,否则会遇到问题。这是因为当你不使用super() 方法时,父类方法中的代码永远不会执行。如果在我们之前的代码片段中,我们没有调用**super(…).unlink()**,则记录不会被删除。

扩展内容

在扩展write() 时,请注意在调用write()super() 实现之前,self仍然是未修改的。您可以使用它来将字段的当前值与值字典中的值进行比较。

在本节中,我们选择了抛出异常,但我们也可以选择从值字典中删除有问题的字段,并默默地跳过更新记录中的该字段:

1
2
3
4
5
def write(self, values):
if not self.user_has_groups( 'my_library.group_librarian'):
if 'manager_remarks' in values:
del values['manager_remarks']
return super(LibraryBook, self).write(values)

在调用super().write() 之后,如果你想执行额外的操作,你必须警惕任何可能导致再次调用write() 的事情,否则你将创建一个无限递归的循环。解决方法是在上下文中放置一个标记,以便检查以中断递归:

1
2
3
4
5
6
7
8
class MyModel(models.Model):
def write(self, values):
sup = super(MyModel, self).write(values)
if self.env.context.get('MyModelLoopBreaker'):
return
self = self.with_context(MyModelLoopBreaker=True)
self.compute_things() # can cause calls to writes
return sup

在前面的示例中,我们在compute_things() 方法之前添加了MyModelLoopBreaker 键。因此,如果再次调用write() 方法,它不会进入无限循环。

自定义搜索记录的方式

第四章《应用模型》中的定义模型表现及排序 ,介绍了name_get() 方法,该方法用于计算不同位置的记录表示,包括用于显示Many2one 关联的小部件。

本节向您展示如何通过重新定义name_searchMany2one 小部件中按标题、作者或ISBN搜索一本书。

准备工作

对于本节,我们将使用以下模型定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
class LibraryBook(models.Model):
_name = 'library.book'
name = fields.Char('Title')
isbn = fields.Char('ISBN')
author_ids = fields.Many2many('res.partner', 'Authors')

def name_get(self):
result = []
for book in self:
authors = book.author_ids.mapped('name')
name = '%s (%s)' % (book.name, ', '.join(authors))
result.append((book.id, name))
return result

使用此模型时,Many2one 小部件中的一本书显示为书名(作者1, 作者2…) 。 用户希望能够输入作者的姓名并找到根据此姓名过滤的列表,但这不会起作用,因为name_search 的默认实现仅使用模型类的_rec_name 属性所引用的属性,在我们的例子是'name' 。我们还希望允许按ISBN编号进行过滤。

实现步骤

您需要执行以下步骤:

  1. 为了能够通过书名、作者或ISBN号搜索library.book ,您需要在LibraryBook 类中定义name_search() 方法,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @api.model
    def _name_search(self, name='', args=None, operator='ilike', imit=100, name_get_uid=None):
    args = [] if args is None else args.copy()
    if not(name == '' and operator == 'ilike'):
    args += ['|', '|',
    ('name', operator, name),
    ('isbn', operator, name),
    ('author_ids.name', operator, name)
    ]
    return super(LibraryBook, self)._name_search(name=name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid)
  2. library.book 模型中添加名为old_editionsMany2one 字段以测试_name_search 实现:

    1
    old_edition = fields.Many2one('library.book', string='Old Edition')
  3. 将以下字段添加到用户界面:

    1
    <field name="old_edition" />
  4. 重新启动并更新模块以反映这些更改。

您可以通过在old_edition Many2one 字段中来调用_name_search 方法搜索。

运行原理

name_search() 的默认实现实际上只调用_name_search() 方法,它完成了真正的工作。这个_name_search() 方法有一个额外的参数name_get_uid,它在一些极端情况下使用,例如如果你想使用sudo() 或不同的用户来计算结果。

我们将收到的大部分参数原封不动地传递给方法的super() 实现:

  • name 是一个字符串,其中包含用户输入的值。
  • argsNone 或用作可能记录的预过滤器的搜索域。(例如它可以来自Many2one 关联的域参数。)
  • operator 是包含匹配运算符的字符串。一般来说,你会有'ilike''='
  • limit 是要检索的最大行数。
  • name_get_uid 可用于在调用name_get() 以计算要在小部件中显示的字符串时指定不同的用户。

我们对该方法的实现执行以下操作:

  1. 如果argsNone,它会生成一个新的空列表,否则会生成args 的副本。我们制作一个副本以避免我们对列表的修改对调用者产生副作用。

  2. 然后,我们检查name 不是空字符串或者操作符不是'ilike' 。这是为了避免生成一个不过滤任何内容的哑域[('name', ilike, '')] 。在这种情况下,我们直接跳转到super() 调用实现。

  3. 如果我们有name ,或者如果operator 不是'ilike' ,那么我们会在args 中添加一些过滤条件。在我们的例子中,我们添加了对所提供名称在图书标题、ISNB 或作者姓名中搜索的语句。

  4. 最后,我们使用args 中修改的域调用super() 实现,并强制name''operatorilike 。我们这样做是为了强制_name_search() 的默认实现不改变它接收的域,因此将使用我们指定的域。

扩展内容

我们在介绍中提到,这个方法用在Many2one 小部件中。为了完整起见,它也用于Odoo 的以下部分:

  • 在域中的One2manyMany2many 字段上使用in 运算符时
  • many2many_tags 小部件中搜索记录
  • 在CSV文件中搜索记录导入

也可以看看
第四章《应用模型》中的定义模型表现及排序 介绍了如何定义name_get() 方法,该方法用于创建记录的文本表示。
第九章《后端视图》中的在记录列表上定义过滤器-域 提供了有关搜索域语法的更多信息。

使用read_group()分组获取数据

在之前章节中,我们看到了如何从数据库中搜索和获取数据。但有时,您希望通过汇总记录获得结果,例如上个月销售订单的平均成本。通常我们在SQL查询中使用group byaggregate 函数来获得这样的结果。幸运的是在Odoo中,我们有read_group() 方法。在本节中,你将学习如何使用read_group() 方法来获取聚合结果。

准备工作

本节我们将使用第三章《创建Odoo附加模块》中的my_library 模块。

修改library.book 模型,如下模型定义所示:

1
2
3
4
5
6
7
8
class LibraryBook(models.Model):
_name = 'library.book'
name = fields.Char('Title', required=True)
date_release = fields.Date('Release Date')
pages = fields.Integer('Number of Pages')
cost_price = fields.Float('Book Cost')
category_id = fields.Many2one('library.book.category')
author_ids = fields.Many2many('res.partner', string='Authors')

添加library.book.category 模型。为简单起见,我们只需将其添加到同一个library_book.py 文件中:

1
2
3
4
class BookCategory(models.Model):
_name = 'library.book.category'
name = fields.Char('Category')
description = fields.Text('Description')

我们将使用library.book 模型并获得每个类别的平均成本价格。

实现步骤

为了提取分组结果,我们将_get_average_cost 方法添加到library.book 模型中,该模型将使用read_group() 方法获取组中的数据:

1
2
3
4
5
6
7
8
@api.model
def _get_average_cost(self):
grouped_result = self.read_group(
[('cost_price', "!=", False)], # Domain
['category_id', 'cost_price:avg'], # Fields to access
['category_id'] # group_by
)
return grouped_result

要测试此实现,您需要在用户界面中添加一个按钮来触发此方法。然后,您可以在服务器日志中打印结果。

运行原理

read_group() 方法在内部使用SQL group byaggregate 函数来获取数据。传递给read_group() 方法的最常见参数如下:

  • domain :这用于过滤记录以进行分组。有关域的更多信息请参阅第九章《后端视图》中的搜索视图 章节。

  • fields :这将传递您要与分组数据一起获取的字段的名称。此参数的可能值如下:

    • field name :可以将字段名称传递给fields 参数,但如果使用此选项,则必须将此字段名称也传递给groupby 参数,否则会产生错误。
    • field_name:agg:可以通过aggregate 函数传递字段名称。例如,在cost_price:avg 中,avg 是一个SQL聚合函数。可以在https://www.postgresql.org/docs/current/static/functions-aggregate.html 找到PostgreSQL聚合函数的列表。
    • name:agg(field_name):这与前一个相同,但使用此语法,您可以提供列别名,例如average_price:avg(cost_price)
  • groupby :此参数接受字段描述列表。记录将根据这些字段进行分组。对于日期和时间列,您可以
    通过groupby_function 根据不同的持续时间应用日期分组,例如date_release:month 。这将应用基于月份的分组。

  • read_group() 还支持一些可选参数,如下:

    • offset :这表示要跳过的记录数。
    • limit :这表示要返回的最大记录数。
    • orderby :如果使用了这个选项,结果将根据给定的字段进行排序。
    • lazy :这接收布尔值,默认情况下为True 。如果为True ,则结果仅按第一个groupby 分组,其余groupby 放在__context 键中。如果为False ,则所有groupby 函数都在一次调用中完成。

    性能提示
    read_group() 比从记录集中读取和处理值要快得多。 因此,对于KPI或图表,您应该始终使用read_group()

扩展内容

Odoo14开发者指南第五章-基本服务端开发【翻译】

https://www.junle.org/Odoo14开发者指南第五章-基本服务端开发【翻译】/

作者

Junle

发布于

2022-06-14

更新于

2024-03-22

许可协议

评论