Alan Hou的个人博客

Odoo 14开发者指南第五章 基本服务端开发

全书完整目录请见:Odoo 14开发者指南(Cookbook)第四版

第四章 应用模型中,我们学习了如何在自定义模块中声明或继承业务模型。该章中的各小节涵盖了为计算字段编写方法,以及编写约束字段值的方法。本章中将集中讲解服务端开发的基础,有Odoo方法定义、数据集操作及扩展已继承方法。这样我们就可以在Odoo中添加及修改业务逻辑了。

本章中,我们将讲解如下内容:

技术准备

本章的技术要求包括Odoo在线平台。

本章中所使用的代码可以在如下GitHub仓库中下载:https://github.com/alanhou/odoo14-cookbook。

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

Odoo模型中,类是字段定义和业务逻辑方法的混合体。第四章 应用模型中我们学习了如何在模型中添加字段。下面就来学习如何在模型中添加方法和业务逻辑。

本节中,我们来学习如何编写可由用户界面中按钮或应用其它代码块调用的方法。这一方法会操作图书并执行所需的动作来修改所选图书的状态。

译者注:本节中初始代码为第三章结束时的代码:GitHub传送门。开始本节前请卸载掉之前安装的应用,重新进行安装,否则可能会出现意想不到的各种错误。

准备工作

本节假定你已有准备好了实例,包含第三章 创建Odoo插件模块中所描述的my_library插件模块。需要在LibraryBook模型中添加一个state字段,定义如下:

参见第三章 创建Odoo插件模块添加模型一节获取更详细信息。

如何实现…

要定义方法来修改所选图书的状态,你需要在模型定义中添加如下代码:

  1. 添加一个帮助方法来查看是否允许状态转换:
  2. 添加方法来修改一些书籍的状态为所传参数的新状态:
  3. 添加方法来通过调用change_state方法修改图书状态:
  4. 在<form>视图中添加按钮和状态栏。这会帮助我们从用户界面中触发这些方法:

更新或安装该模块来让这些修改生效。

运行原理…

本节中的代码定义了一些方法。它们是常规的Python方法,带有self作为其第一个参数,也可以带有其它参数。这些方法通过odoo.api模块中的装饰器进行装饰。

📝这些中很多装饰器是在Odoo 9.0中引入的,用于兼容老框架和新框架。在Odoo 10.0中,不再支持老的 API 了,但一些装饰器,比如@api.model,仍在使用。

在编写一个新方法时,如果不使用装饰器,方法会对记录集执行。在这类方法中,self是可以引用任意数量数据库记录的记录集(包括空记录集),代码经常会遍历self中的记录来对每条记录进行操作。

@api.model装饰器也类似,但它用于仅仅是模型很重要而非方法所不操作的记录集中的内容的方法。它的概念类似于Python的@classmethod装饰器。

在第1步中,我们创建了is_allowed_transition() 方法。这个方法的目的是验证从一个状态到另一个状态的转换是否有效。allowed列表中的元组即为可使用转换。例如,我们不会允许lost到borrow的转换,因此没有加入(‘lost, ‘borrowed’)。

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

在第3步中,我们创建了一些通过调用change_state()方法来修改图书状态的方法。本例中,方法会由添加到用户界面中的按钮所触发。

第4步中,我们添加在<form>视图中添加了<button>。点击这一按钮时,Odoo客户端会触发name属性中所包含的Python函数。参见第九章 后端视图向表单添加按钮一节来学习如何从用户界面中调用该方法。我们还添加了带有statusbar组件的state字段,来在<form>视图中显示图书的状态。

在用户从用户界面中点击按钮时,会调用第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所不允许的状态时显示帮助信息。按照如下步骤来进行操作:

  1. 在该Python文件的开头添加如下导入语句:
  2. 修改change_state方法并在else部分中抛出UserError异常:

运行原理…

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会显示有明确含义的警告:

在odoo.exceptions中定义了更多的异常类,都从基本的原有except_orm异常类进行派生。它们大多数仅在内部使用,除以下几种:

获取其它模型的空记录集

在编写Odoo代码时,当前模型的方法可通过self访问。如果需要操作其它模型,不能直接实例化该模型的类,需要获取该模型的一个数据集再进行操作。

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

准备工作

本节将复用my_library插件模块中所设置的图书示例。

我们会在library.book模型中编写一个小方法并搜索所有的图书会员。这时需要获取library.members的空记录集。确保添加了library.members模型并对该模型设置了访问权限。

如何实现…

需要按照如下步骤来获取library.book方法中获取library.members的记录集:

  1. 在LibraryBook类中,编写一个名为get_all_library_members的方法:
  2. 在<form>视图中添加一个按钮调用该方法:

更新模块来应用以上修改。之后就会在书的<form>视图中看到Log Members按钮。点击该按钮,会在服务端日志中看到会员的记录集。

运行原理…

在启动时,Odoo加载了所有的模块并合并从Model中所获取的不同类,同时也定义或继承了给定的模型。这些类存储在Odoo仓库中,按名称索引。任意记录集中的env属性,可通过self.env访问,都是定义在odoo.api模块中的Environment类的实例。

这个类在Odoo开发中扮演着中心角色:

search()的调用在稍后的搜索记录一节中进行讲解。

其它内容

有时会希望使用环境的变更版本。一个例子是想要有其它用户及语言的环境。第八章 高级服务端开发技巧中的更改执行动作的用户使用变更的上下文调用方法小节中会讲解有关在运行时修改self.env的知识。

新建记录

在编写业务逻辑方法时一个普遍的要求是新建记录。本节讲解如何创建library.book.category中的记录。在我们的示例中,将会为library.book.category模型添加一个创建dummy分类的方法。要触发这一方法,需要在<form>视图中添加一个按钮。

注:原书中本章从本节开始存在多处错误,笔者尽量进行了规避。

准备工作

读者需要知道想要新建记录的模型结构,尤其是它们的名称和类型,以及各字段可能存在的约束(如有些字段为必填)。本节中,我们将复用第四章 应用模型中的my_library模块。我们看一下以下示例来快速回顾library.book.category模型:

确保已为library.book.category模型添加了目录、视图和访问权限。

如何实现…

需要执行如下步骤来创建带有一些子分类的分类:

  1. 在library.book.category中创建一个名为create_categories的方法:
  2. 在这一方法体内,准备一个包含第一个子类各字段值的字典:
  3. 准备包含第二个子类各字段值的字典:
  4. 准备包含父类各字段值的字典:
  5. 调用create()方法来新建记录:
  6. 在<form>视图中添加用户界面中触发create_categories方法的按钮:

运行原理…

要为模型新建记录,我们可以对任意与模型关联的记录集调用create(values)方法。该方法返回一个长度为1的新记录集,其中包含带有值字典中所指定字段值的这条新记录。

在字典中,各个键给定字段的名称,相应的值与字段值对应。根据字段类型的不同,需要对值传递不同的Python类型:

在本节中,我们在所要创建的公司中新建了两个联系人的字典,然后我们使用通过前述(0, 0, dict_val)语法所创建公司字典child_ids键中的这些字典。

在第5步中调用create()时,创建了三条记录:

扩展知识…

如果该模型为某些字段定义了一些默认值,不需要做什么特别的事情,create()会处理所提供字典中不存在字段默认值的计算。

从Odoo 12开始,create()方法还支持批量创建记录。要批量创建多条记录,需要向create() 方法传递一个多值列表,如下例所示:

更新记录集中记录值

业务逻辑经常要求我们通过修改其中的一些字段的值来更新记录。本节将展示如何修改partner中的date字段。

准备工作

本节将使用新建记录一节中相同的简化library.book定义。可以参照这一简化定义来找到这些字段。

我们在library.book模型中有date_release字段。为进行演示,我们通过点击按钮在这一字段上进行写入。

如何实现…

  1. 要更新图书的date_release字段,可以编写一个名为change_release_date()的新方法,定义如下:
  2. 然后我们在图书<form>视图的xml中添加一个按钮,如下:
  3. 重启服务并更新my_library模块来查看变化。在点击Update Date按钮时,date_release会被修改。

运行原理…

该方法通过调用ensure_one()检查self传递的图书记录集是否为一条记录。如不是该方法会抛出一个异常,并停止处理。需要这么做是因为我们不希望修改多条记录的日期。如果你想要更新多条值,可以删除ensure_one()并使用对记录集的循环来更新该属性。

最后,该方法修改图书记录的各属性值。它以当前时间更新date_release字段。通过修改记录集中的字段属性,可以执行写操作。

扩展知识…

如果想要向记录字段写入新值有三种选项:

📝重要提示:create() 方法无法使用操作类型1, 2, 3和5。

搜索记录

对记录的搜索也是业务逻辑方法中常见的操作。本节将展示如何通过书名和分类查找图书。

准备工作

本节将使用与新建记录一节中相同的library.book定义。我们在名为find_book(self)的方法中编写代码。

如何实现…

需要执行如下步骤来查找书籍:

  1. 在library.book模型中添加find_book方法:
  2. 为你的条件编写搜索域:
  3. 通过域调用search()方法,它会返回记录集:

books变量包含搜索到图书的记录集,可以打印或日志记录下该变量,在服务端日志中查看结果。

运行原理…

第1步中定义了该方法。

第2步中在本地变量中创建了一个搜索域。通常你会在对搜索调用的行内看到这一创建,但对于复杂的作用域,最好是分开进行定义。

关于搜索域语法的完整讲解,参见第九章 后端视图中的在记录列表上定义过滤器 – 域一节。

第3步使用该域调用了search()方法。该方法返回包含所有匹配这个作用域的记录的一个记录集,还可以对它进行进一步处理。本节中,我们仅通过该域调用此方法,但同时也支持如下关键词参数:

📝重要贴士:我们推荐使用search_count(domain)方法,而非search(domain, count=True),因为它的方法名以更清晰地方式传递了其用途,两者返回的值相同。

有时,需要搜索另一个模型来让对self的搜索返回当前模型的记录集。从另一个模型中进行搜索,我们需要获取该模型的空记录集。例如,假设我们想要搜索联系人。那么,我们需要对res.partner模型使用search()方法。参见下方的代码。这里我们获取了res.partner的空记录集来搜索联系人:

上述代码中,可以在domain 中去掉&,因为在域中未进行指定时,Odoo默认会使用&。

扩展知识…

前面我们说到search()方法返回匹配搜索域的所有记录。但事实并非完全如此。该方法会确保仅返回执行搜索的用户拥有访问权限的记录。此外,如果模型中有名为active的布尔字段,而搜索域中并未指定该字段的搜索条件,那么会隐式地添加一个active=True条件来仅返回这部分记录。因此,如果你想要搜索返回内容而返回了空记录集时,确保检查active的值(如果存在)来检查记录规则。

参见第八章 高级服务端开发技巧中的使用其它上下文调用方法一节,来了解如何不隐式的添加active=True条件。参见第十章 权限安全中的使用记录规则限制记录访问一节有获取有关记录级别方法权限的知识。

如果出于一些原因,你要使用原生SQL查询来查找记录ID,确保获取ID 后使用self.env[‘record.model’].search([(‘id’, ‘in’, tuple(ids))]).ids来应用权限规则。这对于需用记录规则对公司采取区别对待的多公司(租户)Odoo实例尤为重要。

合并记录集

有时,你会发现所获取的记录集并非你真正所需的。本节展示合并它们的不同方式。

准备工作

本节要求在同一个模型中有两个或多个记录集。

如何实现…

按照如下步骤来对记录集执行常用运算:

  1. 将两个记录集合并为一个并保留排序,使用如下操作:
  2. 使用如下运算合并两个记录集,可确保结果中没有重复内容:
  3. 使用如下运算来查找两个记录集中共同的记录:

运行原理…

针对记录集的类实现了很多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。

表5.3

还有些复合运算符+=, -=, &=和 |=,它们会修改左侧的运算项而不会新建记录集。在更新记录的One2many或Many2many字段时这些会非常的有用。参见更新记录集中记录值一节来查看此类示例。

过滤记录集

在某些情况下,已有一个记录集,仅需对其中的某些记录进行操作。当然可以遍历记录集并对每条遍历进行条件判断并根据所查看的结果执行操作,构造一个仅包含需操作的记录的新记录集并对该记录集调用同一操作会更容易,在某些情况下还会更高效。

本节展示如何使用 filtered()方法来根据从另一个记录集中提取子集。

准备工作

我们将复用新建记录一节中所展示的简化的library.book 模型。本节定义一个从给定记录集中提取含有多名作者的图书的方法。

如何实现…

执行如下步骤来从一个记录集中提取包含多名作者的记录:

  1. 定义接收原始记录集的方法:
  2. 定义内部的predicate函数:
  3. 调用filtered(),如下:

可以打印或日志记录该方法的结果 ,在服务端日志中查看。参见本节中的示例代码了解更多。

运行原理…

filtered()方法的实现创建了一个空记录集,其中添加predicate函数运行结果为True的所有记录。最终返回一个新记录集。保留原记录集中记录的排序。

前面部分使用了一个内部命名函数。对这种简单场景会经常发现使用匿名函数 Lambda:

事实上你需要基于 Python 层面为真的字段值(非空字符串,非零数字、非空容器等)进行记录集的过滤。因此如果希望过滤出带有某分类集合的记录,可以传递字段名来进行类似如下过滤:all_books.filtered(‘category_id’)。

扩展知识…

记住filtered()是在内存中进行运算。如果尝试对关键路径上的方法进行性能优化,可能会要使用搜索域或者甚至是转向SQL,代价是损失代码易读性。

遍历记录集关联

在操作长度为1的记录集时,有很多字段可用作记录属性。带有记录值作为值的关联属性(One2many, Many2one和Many2many)也同样可以使用。作为一个示例,我们假定需要访问library.book模型记录集中的分类名称。可以通过遍历many2one字段的category_id来访问分类名,如下:book.category_id.name。但是,在操作带有一条以上记录的记录集时,则不能使用该属性。

本节展示如何使用mapped() 方法来遍历记录集关联,我们会编写一个方法并传递图书参数来获取图书记录集中作者的名字。

准备工作

我们将复用本章中新建记录一节中使用的 library.book模型。

如何实现…

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

  1. 定义名为get_author_names()的方法:
  2. 调用mapped()来获取成员联系人的姓名:

运行原理…

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

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

📝重要信息:第二个属性在希望对self中所有记录经Many2many字段指向的所有记录执行操作时非常有用,但需要确保操作仅会执行一次(哪怕是self中的两条记录共享同一个目标记录)。

扩展知识…

在使用mapped()时,要记住它在Odoo服务端的内存中进行操作,反复地遍历关联并因此产生SQL查询,这样效率可能不高。但是这种代码很直白且具备表达性。如果你在尝试优化实例关键路径上的方法提高性能的话,可能会要重写调用为mapped()并以相应的域来以search()进行表现,甚至是转向SQL(代价是损失代码易读性)。

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

其它内容

记录集排序

在通过search()方法获取一个记录集时,你可以传递一个可选参数order来以指定排序获取记录集。这对于在此前代码中已获取记录集并想对其排序会非常有用。例如对使用集合运算来合并两个记录集时(会丢失排序)可能也会很有用。

本节展示如何使用sorted()方法来对已有记录集进行排序。我们会对图书进行发行日期的排序。

准备工作

我们将复用本章中新建记录一节中所展示的library.book模型。

如何实现…

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

  1. 定义一个名为 sort_books_by_date()的方法:
  2. 像例中那样,使用sorted()方法来根据release_date字段对图书记录排序:

运行原理…

第1步只是对方法的定义。在第2步中,我们调用了图书记录集中的sorted() 方法。sorted() 方法在内部会获取以参数key进行传递的字段数据。然后,通过使用Python的原生sorted方法返回一个排序后记录集。

它还有一个可选参数reverse=True,以逆向排序返回记录集。reverse的用法如下:

扩展知识…

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模型如下:

然后继承library.book.category模型如下:

需要按照第九章 后端视图中的修改已有视图 – 视图继承一节来在视图中添加该字段。你可以在https://github.com/alanhou/odoo14-cookbook中查看代码的完整示例。

如何实现…

需要执行如下步骤来在library.book模型中继承这一业务逻辑:

  1. 在my_library_return中,我们希望在修改图书状态为Borrowed时在图书记录中设置date_return。为此,我们将重载my_library_return模块中的make_borrowed方法:
  2. 我们还希望在图书归还、可供借阅时重置date_return,因此我们将重载make_available方法来重置该日期:

运行原理…

第1步和第2步执行对业务逻辑的继承。我们定义了一个继承library.books的模型并且重定义了make_borrowed()和make_available()方法。在这两个方法的最后一行,返回由父类实现的结果:

在Odoo模型的用例中,父类和你在Python类定义中所看到的并不太一样。框架动态为我们的记录集生成一个类等级,父类由我们所依赖的模块中的模型定义。因此,调用super()返回了my_library模块中library.book的实现。在这一实现中,make_borrowed() 修改图书的状态为Borrowed。因此调用 super 会触发父类方法并且它会设置图书的状态为Borrowed。

扩展知识…

在本节中,我们选择了继承方法的默认实现。在make_borrow()和make_available()方法中,我们在super()的调用前修改了返回的结果。注意在调用super() 时,它会执行其默认实现。也可以在super() 调用之后执行一些动作。当然,也可以同时执行两者。

但是,在方法的中间修改行为会更为困难。这时,我们需要重构代码来提取一个继承点以分隔方法并在继承模块中重载这一新方法。

📝你可能会萌生全新重写一个方法的念头。这么做时一定要小心,如果不调用方法的super() 实现,就在破坏继承机制并可能破坏继承该方法的插件,也即永远不调用该继承方法。除非所使用的环境完全受控,你了解具体安装了哪些插件并查看过不会破坏这些插件,否则不要这么做。同时应该确保用一种可见的方式来以文档记录所做的操作。

在调用方法的原有实现之前和之后你可以做哪些事呢?有很多,包括但不限于如下这些:

继承write()和create()

本章中的继承模型中定义的业务逻辑一节向我们展示了如何继承模型类中定义的方法。如果你考虑一下,模型的父类中定义的方法也是模型的一部分。这表示models.Model(实际为models.Model的父类models.BaseModel)上定义的所有基础方法都可以使用或被继承。

本节将展示如何继承create()和write() 来控制对记录中某些字段的访问。

准备工作

我们将通过第三章 创建Odoo插件模块中的my_library插件模块扩展图书示例。

在library.book模型中添加一个manager_remarks字段。我们仅希望图书管理员分组中的成员可以写入该字段:

在view/library_book.xml文件的<form>中添加manager_remarks字段来通过用户界面访问该字段:

修改security/ir.model.access.csv文件来给图书用户写入的权限:


如何实现…

要防止非librarian组的成员修改manager_remarks的值,需要执行如下步骤:

  1. 继承create()方法如下:
  2. 继承write()方法如下:

运行原理…

第1步中重新定义了create()方法。在调用create() 的基础实现之前,我们的方法使用了user_has_groups() 方法来查看启用是否属于my_library.group_librarian组(这是该组的XML ID)。如果并非如此且向manager_remarks传递了值,则抛出一个UserError异常,阻止止记录的创建。这一检查在基础实现的调用之前执行。

第2步对 write() 执行相同的操作。在写入之前,我们检查组以及写入的值中有哪些字段,在有问题时抛出UserError异常。

📝重要提示:在网页客户端中将该字段设为只读并不会防止RPC调用对其进行写入。这也是为什么我们继承了create()和write()。

本节中,我们学习如何重载create()和write() 方法。但注意这并不仅限于reate()和write() 方法。可以重载任意模型方法。例如,如果希望在记录删除时做一些操作。则需要重载unlink()方法(记录删除时会调用unlink()方法)。以下是一段重载unlink()方法的代码片段。

⚠️警告:在Odoo中重载方法时,别忘记调用super()方法,否则会碰到问题。这是因为不使用super()方法,原有方法中的代码不会进行执行。如果在以上代码中我们没有调用super(…).unlink(),就会删除记录。

译者注:可使用demo 用户进行测试,然后在Settings > User & Companies > Users选中对应用户点击 Edit,在 Other 中选择勾选 Librarians后再次测试。

扩展知识…

在继承write()时,注意调用write()的super()实现之前,self仍未被修改。你可以使用它对比该字段的当前值和values字典中的字段。

本节中,我们选择抛出异常,但你也可以选择从values字典中删除掉不符合规则的字段,静默地跳过记录中该字段的更新:

在调用super().write()之后,如果希望执行其它动作,则需要对任何其它再次引发write()的调用保持警惕,否则会造成一个无限递归循环。规避的方法是在上下文中加入标记来进行检查以解除这种递归:

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

自定义记录的搜索方式

第三章 创建Odoo插件模块定义模型表示及排序一节中引入了name_get()方法,用于计算不同地方记录的展现,包含用于在网页客户端中用于展示Many2one关联的微件。

本节将展示如何通过重新定义name_search在Many2one组件中通过标题、作者或ISBN来搜索图书。

准备工作

本节中,我们将使用如下模型定义:

在使用这一模型时,Many2one微件中的图书以图书名(作者1,作者2…)进行显示。用户预设可通过输入作者名查找根据这一姓名过滤出的列表,但并不会这样,因为name_search的默认实现仅使用了模型类中_rec_name属性所引用的属性,本例中为name。我们也希望可通过ISBN号来进行过滤。

如何实现…

执行如下步骤在实现本小节的功能:

  1. 要对library.book能够使用书名、作者或ISBN号进行搜索,需要在LibraryBook类中定义一个_name_search() 方法。
  2. 在library.book模型中添加old_editions Many2one字段来测试 _name_search的实现:
  3. 向用户界面中添加如下字段:
  4. 重启服务并更新模块来让修改生效。

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

运行原理…

name_search()的默认实现实际上仅仅是调用了_name_search()方法,它执行了真正的任务。_name_search()方法有一个额外的参数name_get_uid,用于一些极端用例中,如你希望使用sudo() 或通过不同的用户来计算结果。

我们将接收到的大部分参数不做修改的传递给该方法的super()实现:

我们实现的方法做了如下操作:

  1. 如果args为None,生成一个新的空列表,否则对args进行拷贝。我们通过做拷贝来避免对列表的修改对调用者产生负面效果。
  2. 然后,我们查看name是否为非空字符串或者运算符是否不是’ilike’。这用于避免生成无效的域, [(‘name’, ilike, ”)],它并不能过滤任何东西。在这种情况下,我们直接进行super()的调用实现。
  3. 如果name存在,或者运算符并非’ilike’,那么我们对args添加一些过滤条件。在本例中,我们添加了对所提供名称在图书标题、ISNB 或作者姓名中搜索的语句。
  4. 最后,我们以args中修改的域调用了super()实现并强制name为”以及运算符为ilike。我们通过这么做来强制_name_search() 的默认实现不对它所接收到的域做任何修改,因而使用我们所指定的域。

扩展知识…

我们在引言中提到这一方法用于Many2one微件。为保持完整性,它还可用于Odoo中如下部分:

其它内容

第三章 创建Odoo插件模块定义模型表示及排序一节中演示了如何定义name_get()方法,该方法用于创建记录的文本表现。

第九章 后端视图中的在记录列表上定义过滤器 – 域一节,提供了有关搜索域语法的更多内容。

通过read_group()获取组中的数据

在前面的各节中,我们学习了如何从数据库中搜索和获取数据。但有时,会希望通过聚合记录来获取结果,如上个月销售订单的平均成本。在SQL中获取这样的结果我们通常使用group和aggregate函数。所幸的是在Odoo中有read_group() 方法。本节中我们学习如何使用read_group() 方法来获取聚合结果。

准备工作

本小节中,我们将使用第三章 创建Odoo插件模块中的my_library插件模块图书示例。

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

添加library.book.category模型。为保持简化,我们仅将其添加到同一library_book.py文件中:

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

译者注:请注意添加分类模型对应的视图文件,更重要的是添加相应的权限组配置(ir.model.access.csv)。

输出结果示例

如何实现…

要提取分组结果,我们在library.book模型中添加_get_average_cost方法,它会使用read_group() 方法来获取分组中的数据:

要测试这一实现,需要在用户界面中添加一个按钮来调用该方法。然后,可以在服务端日志中打印出结果。

运行原理…

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

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

📝性能贴士:read_group()要比从记录集中读取和处理值快速的多。因此对KPI或图表应保持使用read_group()。

退出移动版