全书完整目录请见:Odoo 12开发者指南(Cookbook)第三版
本章中,我们将讲解如下内容:
- 定义模型方法及使用API装饰器
- 向用户报出错误
- 为不同模型获取一个空记录集
- 新建记录
- 更新记录集中记录值
- 搜索记录
- 合并记录集
- 过滤记录集
- 遍历记录集关联
- 记录集排序
- 继承模型中定义的业务逻辑
- 继承write()和create()
- 自定义记录的搜索方式
引言
在第五章 应用模型中,我们学习了如何在自定义模块中声明或继承业务模型。该章中的各小节涵盖了为计算字段编写方法,以及编写约束字段值的方法。本章中将集中讲解基本服务的服务端开发,有Odoo方法定义、数据集操纵及扩展已继承方法。
技术准备
本章的技术准备包括Odoo在线平台。
本章中所使用的代码可以在如下GitHub仓库中下载:https://github.com/alanhou/odoo12-cookbook。
观看视频来查看实时代码操作:https://dwz.cn/dBKge1V5
定义模型方法及使用API装饰器
定义自定义模型的模型类声明由模型处理的数据的字段。它们也可以通过在模型类上定义方法来定义自定义行为。
本节中,我们将来看如何编写可由用户界面中按钮或应用其它代码块调用的方法。这一方法会作用于图书并执行所需的动作来修改所选图书的状态。
准备工作
本节假定你已有准备好了实例,包含第四章 创建Odoo插件模块中所描述的my_library插件模块。你会需要在LibraryBook模型中有一个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插件模块中添加模型一节获取更详细信息。
如何操作…
要定义方法来修改所选图书的状态,你需要在模型定义中添加如下代码:
- 添加一个帮助方法来查看是否允许状态转换:
123456789@api.modeldef 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 - 添加方法来修改一些书籍的状态为所传参数的新状态:
1234567@api.multidef change_state(self, new_state):for book in self:if book.is_allowed_transition(book.state, new_state):book.state = new_stateelse:continue - 添加方法来通过调用change_state方法修改图书状态:
12345678def make_available(self):self.change_state('available')def make_borrowed(self):self.change_state('borrowed')def make_lost(self):self.change_state('lost') - 在<form>视图中添加按钮。这会帮助我们从用户界面中触发这些方法:
12345678910<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"/>...</form>
更新或安装该模块来让这些修改生效。
运行原理…
本节中的代码定义了一些方法。它们是常规的Python方法,带有self来作为其第一个参数,并且还带有其它参数。这些方法通过odoo.api 模块装饰器进行装饰。
ℹ️这些中很多装饰器是在Odoo 9.0中引入的,用于确保将使用老的或传统的API的调用转化为新的 API 调用。在Odoo 10.0中,不再支持老的 API 了,但这些装饰器是新 API 中的核心部分。
在编写一个新方法时,,你通常会使用@api.multi。这一装饰器表示该方法用于在记录集上执行。在此类方法中,self是可以引用指定数量数据库记录的记录集(包括空记录集),代码经常会遍历self中的记录来对每个记录进行操作。
@api.model装饰器也类似,但它用于仅仅是模型很重要而非方法所不操作的记录集中的内容的方法。它的概念类似于Python的@classmethod装饰器。
在第1步中,我们创建了is_allowed_transition() 方法。这个方法的目的是验证从一个状态到另一个状态的转换是否有效。在可允许列表中的元组即为可使用转换。例如,我们不想要允许lost到borrow的转换,因此没有加入(‘lost, ‘borrowed’)。
第2步中,我们创建了 change_state()方法。这个方法的用途是修改图书的状态。调用该方法时,它将图书的状态修改为给定new_state参数的状态。仅在允许进行转换时它都会修改图书状态。这里使用了循环是因为我们使用了可以处理多个记录集的@api.multi装饰器。
在第3步中,我们创建了一些通过调用change_state()方法来修改图书状态的方法。本例中,方法会由添加到用户界面中的按钮所触发。
第4步中,我们添加了<form>视图。点击这些按钮时,Odoo客户端会触发name属性中所包含的Python函数。参见第十章 后端视图中向表单添加按钮一节来学习如何从用户界面中调用此类方法。
在用户从用户界面中点击按钮时,会调用第3步中所定义的方法。self是一个包含library.book模型记录的记录集(可能为空)。然后,我们调用了 change_state()方法并根据所点击按钮传递相应的参数。
在调用change_state() 时,self是library.book模型中的相同数据集。change_state()方法中的内容体对self进行循环来处理记录集中的每一本书。循环self一开始看上去很奇怪,这但快你就会习惯这种模式。
在循环中,change_state() 调用is_allowed_transition()。调用通过本地变量book进行,但也可对library.book模型中的任意记录集进行调用,例如self,因为is_allowed_transition()由@api.model所装饰。如果允许转换,change_state()通过对记录的属性分配值来为书籍分配一个新的状态。这仅在记录集长度为1时有效,用于确保和遍历self的情况一致。
扩展知识…
在阅读源代码时你可能会碰到@api.one装饰器。该装饰器因其初看起会引起混淆而被淘汰。同时,如果你知道@api.multi的话,可能会想这个装饰器仅在记录集大小为1时允许调用该方法,但并非这样。在记录集大小方面,@api.one和@api.multi是相似的,但它是在方法外对记录集做for循环,并在列表中对循环的每个遍历返回值进行累加,然后返回给调用者。
向用户报出错误
在方法执行期间,因为用户请求的该动作无效或碰到错误条件有时需要停止进程。本节向你展示了如何通过显示有用的错误信息来管理这些用例。
准备工作
本节假定你已经准备好了一个实例,并包含有前面小节中所描述的my_library插件模块。
如何操作…
我们会对前面小节中的change_state方法作出修改,并对用户尝试修改is_allowed_transition所不允许的状态时显示帮助信息。按照如下步骤来进行操作:
- 在该Python文件的开头添加如下导入语句:
12from odoo.exceptions import UserErrorfrom odoo.tools.translate import _ - 修改change_state方法并抛出else部分中的UserError异常:
12345678@api.multidef change_state(self, new_state):for book in self:if book.is_allowed_transition(book.state, new_state):book.state = new_stateelse:msg = _('Moving from %s to %s is not allowed') % (book.state, new_state)raise UserError(msg)
运行原理…
在 Python 中抛出异常时,它会延着调用栈进行传递直接被处理。在Odoo中,响应网页客户端发出调用的RPC层会捕获所有异常,根据异常类的不同来触发网页客户端上的不同行为。
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异常类进行派生。它们大多数仅在内部使用,除以下几种:
- Warning:在Odoo 8.0中,odoo.exceptions.Warning起着和9.0及之后版本中UserError相同的作用。现在它被淘汰的原因是名称带有欺骗性(它是一个错误,而非警告),并且它与Python内置的Warning类有冲突。它仅保持着向后兼容,你应当在代码中使用UserError。
- ValidationError:这个异常在没有满足Python对字段的约束时抛出。参见第五章 应用模型中向模型添加约束验证一节获取更多信息。
为不同模型获取一个空记录集
在编写Odoo代码时,当前模型的方法可通过self访问。如果你需要操作不同模型,不能直接实例化该模型的类,你需要获取该模型的一个数据集在进行操作。
本节向你展示如何在Odoo的模型方法中注册任意模型在获取空记录集。
准备工作
本节将复用my_library插件模块中所设置的图书示例。
我们将在library.book模型中编写一个小方法并搜索所有的图书会员。这时需要获取library.members的空记录集。
如何操作…
需要按照如下步骤来获取library.book方法中获取library.members的记录集:
- 在LibraryBook类中,编写一个名为get_all_library_members的方法:
12345class LibraryBook(models.Model):# ...@api.modeldef get_all_library_members(self):# ... - 在该方法体中,使用如下代码:
12library_member_model = self.env['library.member']return library_member_model.search([])
运行原理…
在启动时,Odoo加载了所有的模块并合并从Model中所获取的不同类,同时也定义或继承了给定的模型。这些类存储在Odoo仓库中,以名称作为索引。任意记录集中的env属性,可通过self.env访问,是定义在odoo.api模型中的Environment类的实例。
这个类在Odoo开发中扮演着中心角色:
- 它通过模拟Python字典来提供对仓库的快速访问。如果你知道所要查找的模型名,self.env[model_name]即要获取该模型的空记录集。再者,该记录集会共享self的环境。
- 它有一个cr属性,是你可以用于传递原生SQL查询的数据库游标。参见第九章 高级服务端开发技巧中的执行原生SQL查询一节获取更多信息。
- 它有一个user属性,是对执行调用当前用户的调用。参见第九章 高级服务端开发技巧中的更改执行动作的用户一节获取更多信息。
- 它有一个context属性,是一个包含调用的上下文的字典。这包含用户语言、时区、当前记录选项等等的信息。参见第九章 高级服务端开发技巧中的使用变更的上下文调用方法一节获取更多信息。
search()的调用在稍后的搜索记录一节中进行讲解。
其它内容
第九章 高级服务端开发技巧中的更改执行动作的用户和使用变更的上下文调用方法小节中讲解有关运行时修改self.env的知识。
新建记录
在编写业务逻辑方法时一个普遍的要求是新建记录。本节讲解如何创建library.book.category中的记录。在我们的示例中,将会为library.book.category模型添加一个创建dummy分类的方法。要触发这一方法,需要添加一个表单视图中的按钮。
准备工作
你需要知道想要新建目录的模型结构,尤其是它们的名称和类型,以及各字段中所存在的约束(如有些字段为必填)。本节中,我们将复用第五章 应用模型中的my_library模块。我们看一下以下示例来快速回顾library.book.category模型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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中创建一个名为create_categories的方法:
12def create_categories(self):...... - 在这一方法体内,准备一个包含第一个子类各字段值的字典:
1234categ1 = {'name': 'Child category 1','description': 'Description for child 1'} - 准备包含第二个子类各字段值的字典:
1234categ2 = {'name': 'Child category 2','description': 'Description for child 2'} - 准备包含父类各字段值的字典:
12345678parent_category_val = {'name': 'Parent category','email': 'Description for parent category','child_ids': [(0, 0, categ1),(0, 0, categ2),]} - 调用create()方法来新建记录:
1record = self.env['library.book.category'].create(parent_category_val) - 在<form>视图中添加用户界面中触发create_categories方法的按钮:
1<button name="create_categories" string="Create Categories" type="object"/>
运行原理…
为模型新建记录,我们可以对任意与模型关联的记录集调用create(values)方法。该方法返回一个长度为1的新记录集,其中包含带有值字典中所指定字段值的这条新记录。
在字典中,各个键给定字段的名称,相对的值与字段值对应。根据字段类型的不同,你需要为值传递不同的Python类型:
- Text字段值给定的类型为Python字符串。
- Float和Integer字段值使用Python的浮点型和整型。
- Boolean字段值最好使用Python布尔类型或整型。
- Date字段值使用Python的datetime.date 对象。
- Datetime字段值使用Python的datetime.datetime对象。
- Binary字段值以Base64编码字符串进行传递。Python标准库中的base64模块提供诸如encodebytes(bytestring)的方法来对字符串以Base64进行编码。
- Many2one字段值给定的为整型,应为关联记录的数据库ID。
- One2many和Many2many字段使用一个特殊语法。值为包含三个元素元组的一个列表,如下:
元组 效果 (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()会处理所提供字典中不存在字段默认值的计算。
从Odoo 12开始,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_updated字段。为进行演示,我们通过点击按钮在这一字段上进行写入。
如何操作…
要更新图书的date_updated字段,可以编写一个名为change_update_date()的新方法,定义如下:
1 2 3 4 |
@api.multi def change_update_date(self): self.ensure_one() self.date_updated = fields.Datetime.now() |
然后我们以xml在图书<form>视图中添加一个按钮,如下:
运行原理…
该方法通过调用ensure_one()检查以self传递的图书记录是否是一条记录。如并非如此该方法会抛出一个异常,并停止处理。需要这么做是因为我们不希望修改多条记录的日期。如果你想要更新多条值,可以删除ensure_one()并使用对记录集的循环来更新该属性。
最后,该方法修改图书记录的各属性值。它以当前时间更新date_updated字段。通过修改记录集中的字段属性,你可以执行写操作。
扩展知识…
如果你想要向记录字段写入新值有三个可选项:
- 选项一在本节中进行了讲解。它通过直接向代表记录字段的属性分配值在所有上下文中均可使用。也可以一次同时对所有记录集分配值,这时你需要遍历记录集,除非你非常确定仅处理单条记录。
- 选项二通过使用update()方法将字典映射字段传递给你想要设置的值。仅也同样仅可作用于长度为1的记录集。在需要同时对同一记录更新多个字段的值时可以节约一些代码。本节中第二步使用此选项重写如下:
12345678@api.multidef change_update_date(self):self.ensure_one()self.update({'date_updated': fields.Datetime.now(),'another_field': 'value'...}) - 选项三为调用write()方法,向你所希望设置的值传递一个映射字段名的字典。该方法在前面两个选项对执行单记录单字段的单数据库调用的情况下作用于指定大小的记录集并会在单条数据库运行中以指定值更新所有记录。但是它也有一些限制:
- 如果记录尚不存在于数据库中则无法使用它。(更多相关知识参见第九章 高级服务端开发技巧中的定义onchange方法一节)
- 在写入关联字段时它要求有特殊的格式,类似于create() 方法所使用的格式。查看下表中用于为关联字段生成不同值的格式:
元组 效果 (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 在 Python 列表中名为 id_list。
ℹ️在写本书时,官方文档更新并不及时,其中提到了运算3, 4, 5和6在One2many字段中不可用,但早非如此了。但根据模型的约束不同,其中某些可能会无法用于One2many字段。例如,如果要求有反向Many2one关联,那么运算3将会失败,因为它会产生一个未设置的Many2one关联。
搜索记录
对记录的搜索也是业务逻辑方法中常见的操作。本节将展示如何通过书名和分类查找图书。
准备工作
本节将使用与新建记录一节中相同的library.book定义。我们在名为find_book(self)的方法中编写代码。
如何操作…
你需要执行如下步骤来查看书籍:
- 在library.book模型中添加find_book方法:
12def find_book(self):... - 为你的条件编写搜索域:
1234567domain = ['|','&', ('name', 'ilike', 'Book Name'),('category_id.name', 'ilike', 'Category Name'),'&', ('name', 'ilike', 'Book Name 2'),('category_id.name', 'ilike', 'Category Name 2')] - 通过域调用search()方法,它会返回记录集:
1books = 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模型,因为我们希望搜索res.partner记录中的联系人。那么,我们需要对res.partner模型的空记录集进行搜索。参见如下代码:
1 2 3 4 5 6 7 8 |
@api.multi def find_partner(self): PartnerObj = self.env['res.partner'] domain = [ '&', ('name', 'ilike', 'Parth Gajjar'), ('company_id.name', '=', 'Odoo') ] partner = PartnerObj.search(domain) |
扩展知识…
前面我们说到search()方法返回匹配搜索域的所有记录。但事实并非完全如此。该方法会确保仅返回执行搜索的用户拥有访问权限的记录。此外,如果模型中有名为active的布尔字段,而搜索域中并未指定该字段的搜索条件,那么会隐式地添加一个active=True条件来仅返回这部分值。因此,如果你想要搜索返回内容而仅返回空记录集时,确保检查active的值(如果存在)来检查记录规则。
参见第九章 高级服务端开发技巧中的使用变更的上下文调用方法一节,来了解如何不隐式的添加active = True条件。参见第十一章 权限安全中的使用记录规则限制记录访问一节有获取有关记录级别权限规则的知识。
如果出于一些原因,你要使用原生SQL查询来查找记录ID,确保获取ID 后使用self.env[‘record.model’].search([(‘id’, ‘in’, tuple(ids))]).ids来应用权限规则。这对于需对公司采取区别对待的记录规则的多公司(租户)Odoo实例尤为重要。
合并记录集
有时,你会发现所获取的记录集并非你真正所需的。本节展示合并它们的不同方式。
准备工作
本节要求你在同一个模型中有两个或多个记录集。
如何操作…
按照如下步骤来对记录集执行通过操作:
- 将两个记录集合并为一个并保留排序,使用如下操作:
1result = recordset1 + recordset2 - 使用如下操作合并两个记录集并确保结果中没有重复内容:
1result = recordset1 | recordset2 - 使用如下操作来查找两个记录集中共同的记录:
1result = recordset1 & recordset2
运行原理…
针对记录集的类实现了不同的Python运算符的重定义,在此处进行了使用。以下为可用于记录集的最有用的Python运算符的总结表:
运算符 | 执行操作 |
---|---|
R1 + R2 | 它返回一个包含 R1中记录的新记录集,后面跟 R2中的记录。这可能会产生记录集中的重复记录 |
R1 - R2 | 它返回一个包含 R1中记录但不包含 R2中记录的新记录集。保留原有排序。 |
R1 & R2 | 它返回一个既属于 R1又属于 R2的记录的新记录集(记录集的交集)。不保留原有排序。 |
R1 | R2 | 它返回一个或属于 R1或属于 R2的记录的新记录集(记录集的并集)。不保留原有排序,且没有重复值。 |
R1 == R2 | 如果两个记录集中包含相同的记录则返回 True。 |
R1 <= R2 R1 in R2 | 如果 R1的记录也存在于 R2中返回 True。两种语法异曲同工。 |
R1 >= R2 R2 in R1 | 如果 R2的记录也存在于 R1中返回 True。两种语法异曲同工。 |
R1 != R2 | 如果R1和 R2不包含相同记录返回 True。 |
还有些赋值运算符+=, -=, &=和 |=,它们会修改左侧的运算项而不会新建记录集。在更新记录的One2many或Many2many字段时这些会非常的有用。参见更新记录集中记录值一节有查看此类示例。
过滤记录集
在某些情况下,你已有一个记录集,仅需对其中的某些记录进行操作。当然你可以遍历记录集并对每条遍历进行条件判断并根据所查看的结果执行操作,构造一个仅包含需操作的记录的新记录集并对该记录集调用同一操作会更容易,在某些情况下还会更高效。
本节向你展示如何使用 filter()方法来从另一个记录集中提取记录集。
准备工作
我们将复用新建记录一节中所展示的简化的library.book 模型。本节定义一个从给定记录集中提取含有多名作者的图书的方法。
如何操作…
执行如下步骤来从一个记录集中提取包含多名作者的记录:
- 定义接收原始记录集的方法:
12@api.modeldef books_with_multiple_authors(self, all_books): - 定义内部的predicate函数:
1234def predicate(book):if len(book.author_ids) > 1:return Truereturn False - 调用filter(),如下:
1return all_books.filter(predicate)
运行原理…
filter()方法的实现创建了一个空记录集,其中添加predicate函数运行结果为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的记录集时,有很多字段可用作记录属性。带有记录值作为值的关联属性(One2many, Many2one和Many2many)也同样可以使用。作为一个示例,我们假定需要访问library.book模型记录集中的分类名称。你可以通过遍历category_id many2one字段来访问分类名,如下:book.category_id.name。但是,在操作带有一条以上记录的记录集时,则不能使用该属性。
本节向你展示如何使用mapped() 方法来遍历记录集关联,我们会编写一个方法并传递图书参数来获取图书记录集中作者的名称。
准备工作
我们将复用本章中新建记录一节中使用的 library.book模型。
如何操作…
你需要执行如下步骤来从图书记录集中获取作者名称:
- 定义名为get_author_names()的方法:
12@api.modeldef get_author_names(self, books): - 调用mapped()来获取成员联系人的email地址:
1return books.mapped('author_ids.name')
运行原理…
第1步中仅是定义了该方法。第2步中,我们调用了mapped(path) 方法来遍历该记录集中的字段:path是包含以点号分隔字段名的字符串。对于path中的每一个字段,mapped()生成了一个包含该字段所关联当前记录集所有元素的所有记录。然后将其应用于新记录集中path的下一个元素。如果path中的最后一个字段是关联字段,mapped()会返回一个记录集,否则,返回一个Python列表。
mapped()方法有两个有用的属性:
- 如果path是一个标量字段名,那么返回的列表与所处理记录集使用相同的排序。
- 如果path包含一个关联字段,那么不保留排序,复制内容会从结果中进行删除。
ℹ️第二个属性在以@api.multi,修饰的方法中非常有用,你可以对self中所有记录的所有由Many2many字段指向的记录执行操作,但需要确保操作仅会执行一次(哪怕是self中的两条记录共享同一个目录记录)。
扩展知识…
在使用mapped()时,要记住它在Odoo服务的内存中进行操作,该服务返利地遍历关联并因此产生很多SQL查询,这可能会影响效率。但是这种代码很直白且具备表达性。如果你在尝试优化实例关键路径上的方法提高性能的话,可能会要重写调用为mapped()并以相应的域来以search()进行表达,甚至是转向SQL(代价是损失代码易读性)。
mapped()方法也可以通过函数作为参数来进行调用。这种情况下,它返回包含应用于self每条记录的函数的结果列表,或者返回在函数返回的是记录集的情况下由该函数返回的记录集的并集。
其它内容
- 本章中的搜索记录一节
- 第九章 高级服务端开发技巧中的执行原生SQL查询一节
记录集排序
在通过search()方法获取一个记录集时,你可以传递一个可选参数order来以指定排序获取记录集。这对于在此前代码中已获取记录集并想对其排序会非常有用。例如对使用集合运算来合并两个记录集时(会丢失排序)可能也会很有用。
本节向你展示如何使用sorted()方法来对已有记录集进行排序。我们会对图书进行发生日期的排序。
准备工作
我们将复用本章中新建记录一节中所展示的library.book模型。
如何操作…
你需要执行如下步骤来获取基于release_date排序的图书记录集:
- 定义一个名为 sort_books_by_date()的方法:
12@api.modeldef sort_books_by_date(self, books): - 调用sort()来获取成员联系人的邮箱地址:
1return books.sorted(key='release_date')
运行原理…
第1步只是对方法的定义。在第2步中,我们调用了图书记录集中的sorted() 方法。sorted() 方法在内部会获取以参数key进行传递的字段数据。然后,通过使用Python的原生sorted方法返回一个排序后记录集。
它还有一个可选参数reverse=True,以逆向排序返回记录集。reverse的用法如下:
1 |
books.sorted(key='release_date', reverse=True) |
扩展知识…
sorted()方法会在记录集中进行排序。调用时若不传入参数,则会使用模型中的_order属性。另外,可传入一个函数来以Python内置sorted (sequence, key)函数相同的方式计算一个比较键。
ℹ️在使用模型的默认_order参数时,排序由数据库来代理,执行了一个新的SELECT函数来获取排序。否则,排序由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/alanhou/odoo12-cookbook中查看代码的完整示例。
如何操作…
你需要执行如下步骤来在library.book模型中继承这一业务逻辑:
- 在my_module_return中,我们希望在修改图书状态为Borrowed时在图书记录中设置date_return。为此,我们将重载my_module_return模块中的make_borrowed方法:
1234def make_borrowed(self):day_to_borrow = self.category_id.max_borrow_days or 10self.date_return = fields.Date.today() + timedelta(days=day_to_borrow)return super(LibraryBook, self).make_borrowed() - 我们还希望在图书归还并可供借阅时重置date_return,因此我们将重载make_available方法来重置该日期:
123def make_available(self):self.date_return = Falsereturn 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() 修改图书的状态为Borrowed。因此调用 super 会触发父类方法并且它会设置图书的状态为Borrowed。
扩展知识…
在本节中,我们选择了继承方法的默认实现。在make_boorrow()和make_available()方法中,我们在super()的调用前修改了返回的结果。注意在调用super() 时,它会执行其默认实现。也可能在super() 调用之后执行一些动作。当然,也可以同时执行两者。
但是,在方法的中间修改行为会更为困难。这时,我们需要重构代码来提取一个继承点以分隔方法并重载继承模块中的这一新方法。
ℹ️你可能会萌生重写一个方法的念头。这么做时一定要小心,如果你不调用你的方法的super() 实现,你在破坏继承机制并可能破坏继承该方法的插件,也即永远不调用该继承方法。除非你所使用的环境完全受控,你了解具体安装了哪些插件并查看过不会破坏这些插件,否则不要这么做。同时你应该确保心一种可见的方法以来以文档记录所做的操作。
在调用方法的原有实现之前和之后你可以做哪些事呢?有很多,包括但不限于如下这些:
- 修改传递给原有实现的参数(之前)
- 修改传递给原有实现的上下文(之前)
- 修改原有实现返回的结果(之后)
- 调用另一个方法(之前和之后)
- 创建方法(之前和之后)
- 抛出一个UserError来在禁止用例中取消执行(之前和之后)
- 分拆self为更小的记录集,并以不同方式调用每个子集的原有实现(之前)
继承write()和create()
本章中的继承模型中定义的业务逻辑一节向我们展示了如何继承模型类中定义的方法。如果你考虑一下,模型的父类中定义的方法也是模型的一部分。这表示models.Model(实际为models.Model的父类models.BaseModel)上定义的所有基础方法都可以使用或被继承。
本节将展示如何继承create()和write() 来控制对记录中某些字段的访问。
准备工作
我们将通过第四章 创建Odoo插件模块中的my_library插件模块扩展图书示例。
在library.book模型中添加一个manager_remarks字段。我们仅希望图书管理员分组中的成员可以写入该字段:
1 2 3 4 |
from odoo import models, api, exceptions class LibraryBook(models.Model): _name = 'library.book' manager_remarks = fields.Text('Manager Remarks') |
在view/library_book.xml文件的<form>中添加manager_remarks字段来通过用户界面访问该字段:
1 |
<field name="manager_remarks"/> |
修改security/ir.model.access.csv文件来给图书用户写入的权限:
如何操作…
要防止非Librarian组的成员修改manager_remarks的值,需要执行如下步骤:
- 继承create()方法如下:
123456789@api.modeldef create(self, values):if not self.user_has_groups('my_library.acl_book_librarian'):if 'manager_remarks' in values:raise UserError('You are not allowed to modify ''manager_remarks')return super(LibraryBook, self).create(values) - 继承write()方法如下:
123456789@api.multidef write(self, values):if not self.user_has_groups('my_library.acl_book_librarian'):if 'manager_remarks' in values:raise UserError('You are not allowed to modify ''manager_remarks')return super(LibraryBook, self).write(values)
运行原理…
第1步中重新定义了create()方法。在调用create() 的基础实现之前,我们的方法使用了user_has_groups() 方法来查看启用是否属于my_library.group_librarian组(这是该组的XML ID)。如果并非如此且向manager_remarks传递了值,则抛出一个UserError异常,以防止记录的创建。这一检查在基础实现的调用之前执行。
第2步为 write() 执行相同的操作,在写入之前,我们检查组以及写入的值中有哪此字段,在有问题时抛出UserError异常。
ℹ️在网页客户端中将该字段设为只读并不会防止RPC调用对其写入。这也是为什么我们继承了create()和write()。
测试这一实现,你可以demo用户登录或收回当前用户的librarian权限。
扩展知识…
在继承write()时,注意调用write()的super()实现之前,self仍未被修改。你可以使用它对比该字段的当前值和值字典中的字段。
本节中,我们选择抛出异常,但你也可以选择从值字典中删除不符合规则的字段,静默地跳过记录中该字段的更新:
1 2 3 4 5 6 |
@api.multi 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 9 |
class MyModel(models.Model): @api.multi 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 |
自定义记录的搜索方式
在第五章 应用模型中定义模型表示及排序一节中引入了name_get()方法,用于完成不同地方记录的展现,包含用于在网页客户端中用于展示Many2one关联的组件。
本节将向你展示如何通过重新定义name_search在Many2one组件中通过标题、作者或ISBN来搜索图书。
准备工作
本节中,我们将使用如下模型定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class LibraryBook(models.Model): _name = 'library.book' name = fields.Char('Title') isbn = fields.Char('ISBN') author_ids = fields.Many2many('res.partner', 'Authors') @api.multi 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号来进行过滤。
如何操作…
要对library.book能够使用书名、作者或ISBN号进行搜索,你需要在LibraryBook类中定义一个_name_search() 方法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@api.model def _name_search(self, name='', args=None, operator='ilike', limit=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) |
在library.book模型中添加old_editions Many2one字段来测试 _name_search的实现:
1 |
old_edition = fields.Many2one('library.book', string='Old Edition') |
向用户界面中添加如下字段:
1 |
<field name="old_edition" /> |
重启服务并更新模块来让修改生效。你可以通过在old_edition Many2one字段中进行搜索来调用_name_search方法。
运行原理…
name_search()的默认实现实际上仅仅是调用了_name_search()方法,它执行了真正的任务。_name_search()方法有一个额外的参数name_get_uid,用于一些极端用例中,如你希望使用sudo() 或通过不同的用户来计算结果。
我们将接收到的大部分参数不做修改的传递给该方法的super()实现:
- name是包含到此所输入值的字符串。
- args为None或者一个用于预过滤可能记录的搜索域。(比如,它可以来自Many2one关联的domain参数。)
- operator是一个包含匹配运算符的字符串。通常会有’ilike’ 或 ‘=’。
- limit是要获取的最大行数。
- name_get_uid可在调用 name_get()计算组件中显示字符串时用于指定不同的用户。
我们实现的方法做了如下操作:
- 如果args为None,它生成一个新的空列表,否则对args进行拷贝。我们通过做拷贝来避免对列表的修改对调用者产生负面效果。
- 然后,我们查看name是否为非空字符串或者运算符是否不是’ilike’。这用于避免生成无效的域, [(‘name’, ilike, ”)],它并不能过滤任务东西。在这种情况下,我们直接进行super()的调用实现。
- 如果name存在,或者运算符并非’ilike’,那么我们对args添加一些过滤条件。在本例中,我们添加了对所提供名称在图书标题、ISNB 或作者姓名中搜索的语句。
- 最后,我们以args中修改的域调用了super()实现并强制name为”以及运算符为ilike。我们这么做来强制_name_search() 的默认实现不对它所接收到的域做任何修改,因而使用我们所指定的域。
扩展知识…
我们在引言中提到这一方法用于Many2one组件。为保持完整性,它还可用于Odoo中如下部分:
- 在域中的One2many和Many2many字段上的运算符中使用
- 搜索many2many_tags组件中记录
- 搜索CSV导入文件中的记录
其它内容
在第五章 应用模型中定义模型表示及排序一节中演示了如何定义name_get()方法,该方法用于创建记录的文本表现。
在第十章 后端视图中的在记录列表上定义过滤器 – 域一节,提供了有关搜索域语法的更多信息。
通过read_group()获取组中的数据
在前面的各节中,我们学习了如何从数据库中搜索和获取数据。但有时,你希望通过聚合记录来获取结果,如上个月销售订单的平均成本。在这种情况下,你可以使用read_group() 方法来获取聚合结果。
准备工作
本小节中,我们将使用第四章 创建Odoo插件模块中的my_library插件模块图书示例。
修改 library.book模型,如下面的模型定义所示:
1 2 3 4 5 6 7 8 9 |
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 5 |
class BookCategory(models.Model): _name = 'library.book.category' name = fields.Char('Category') description = fields.Text('Description') |
我们将使用 library.book模型并获取每个分类的平均成本价。
如何操作…
要提取分组结果,我们将添加_get_average_cost方法,它会使用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 by及累加函数来获取数据。传递给read_group() 方法的最常用参数如下:
- domain:用于为分组过滤记录。更多有关过滤域的知识,请参见第十章 后端视图中的定义搜索视图一节。
- fields:它传递你希望获取分组数据的字段名称。该参数的值可能如下:
- 字段名:你可以向fields参数传递字段名,但如果使用这一选项,还应将该字段名同时传递给groupby参数,否则会产生错误
- field_name:agg:你可以传递带有聚合函数的字段名。例如,在cost_price:avg中,avg是一个SQL聚合函数。PostgreSQL中的聚合函数请参见https://www.postgresql.org/docs/current/functions-aggregate.html。
- name:agg(field_name):它与前面一个相同,但使用这种语句,你可以给数据列一个别名,例如average_price:avg(cost_price)。
- groupby:这个参数接收一个字段描述列表。记录将根据这些字段分组。对于date和datetime列,你可以传递groupby_function来根据不同的时长应用日期分组,如 date_release:month。这会根据月来应用分组。
read_group()还支持一些可选参数,如下:
- offset:表示可以跳过可选记录数量
- limit:表示可选的返回记录最大数量
- orderby:如果传递了该选项,结果会根据给定字段进行排序
- lazy:它接收布尔值,并且默认值为True。如果传递了True,结果仅通过第一个groupby进行分组,剩余的groupby会被放到__context键中。若为False,所有的groupby在一次调用中完成。
小贴士:性能贴士:read_group()要比从记录集中读取和处理值快速的多。因此对KPI或图表应保持使用read_group()。