
Odoo中文社区可以通过以下两个域名访问:shine-it.net , odoo.net.cn
由于系统升迁的原因,本论坛部分较早期的内容存在格式和链接损坏失效的问题,并非本论坛系统本身的缺陷,望谅解
本社区没有维护任何QQ群讨论组,任何与本社区同名的QQ群讨论组的言论与本社区无关!
开发人员可以登录gitter讨论组: http://gitter.im/odoo-china/Talk, 需要github账号
如果您登录系统碰到问题,请在微信公众号留言:
《Odoo14开发者指南》中文阅读网址
-
-
4.11.模型继承
Odoo一个最重要的功能是模块可以继承其它模块中定义的功能,而又无需编辑原功能中的代码。可以是添加字段或方法,修改已有字段或继承已有方法来执行额外的逻辑。
Odoo提供三种类型的继承:
类继承(扩展)
原型继承
代理继承
我们会通过不同的小节学习这些继承。本节中我们学习类型继承(扩展)。用于对已有模型添加新字段或方法。我们将继承内置的客户模型res.partner来添加所著书数量的计算字段。包含对已有模型添加一个字段或一个方法。
4.11.1.准备工作
本节中,我们继续使用上一节中的my_library插件模型。
4.11.2.如何实现
我们将继承内置的Partner模型。读者可能还记得我们在本章的向模型添加关联字段一节中继承过res.partner。为了简化讲解,我们将复用 models/library_book.py代码文件中的res.partner模型:
首先,我们将确保在Partner模型中有authored_book_ids反向关联并添加计算字段:
class ResPartner(models.Model):
_inherit = 'res.partner'
_order = 'name'
authored_book_ids = fields.Many2many(
'library.book', string='编撰的图书')
count_books = fields.Integer( '编撰的图书数量',
compute='_compute_count_books' )
然后,添加需要用于计算图书数量的方法:...
from odoo import api
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)
最后,我们需要升级这个插件模块来让修改生效。
4.11.3.运行原理
在模型类通过_inherit属性进行定义时,它向所继承模型添加了修改,而没有进行替换。
这意味着在继承类中定义的字段会在父级模型中新增或修改。在数据库层,ORM对同一张数据表添加字段。表示如果该字段在父类中已存在,仅修改在继承类中声明的属性,其它的保持原有父类中的内容不变。
在继承类中定义的方法替换父类中的方法。如果你不通过super调用触发父级方法,那么父级版本的方法则不会被调用,我们也就不拥有该项功能。因此,通过继承在已有方法中添加新逻辑时,应包含一个带有super的语句来调用其父类中的方法。这部分在《第五章 基本服务端开发》中做进一步的讲解。
本节会向已有模型新增字段。如果想在已有视图(用户界面)添加这些新字段的话,参见《第九章 后端视图》中的“修改已有视图 – 视图继承”一节。
4.11.4.扩展知识
通过_inherit经典继承,也可以将父级模型的功能拷贝到一个全新的模型中。这通过添加一个在带有不同标识符的_name类属性来实现。以下是一个示例:
class LibraryMember(models.Model):
_inherit = 'res.partner'
_name = 'library.member'
新模型有其自己的数据表,包含完全独立于res.partner父模型的自身数据。因其仍继承Partner模型,此后的任意修改也会影响到新模型。
在官方文档中,这被称为原型继承,但在实践中鲜有使用。原因在于代理继承通常可以更高效的方式满足了这一需求,也无需复制数据结构。参见本章中的使用代理继承将功能拷贝至另一个模型一节了解更多内容。 -
4.13.使用抽象模型实现可复用模型功能
有时,一个具体的功能,我们希望添加到几个不同的模型中。但在不同的文件中重复相同代码基本上是一种不良编程实践,最好可以一次实现多次复用。
抽象模型让我们可以创建一个通用模型来实现一些功能,然后由普通模型进行继承以使用该功能。
作为示例,我们将实现一个简单的存档功能。它将active字段加入到模型中(如果尚未存在)并添加一个存档方法来切换active标记。这可以生效是因为active是一个魔法字段,如果默认在模型中出现,active=False 的记录会在查询中被过滤掉。
下面我们将在图书模型中添加它。
4.13.1.准备工作
本节中,我们继续使用上一节中的my_library插件模型。
4.13.2.如何实现
存档功能显然可独立为一个插件模块或者至少应有自己的Python代码文件。但为保持讲解尽可能简单,我们将会把它塞到models/library_book.py文件中:
为存档功能添加抽象模型,应在使用它的图书模型中定义:
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
接着,我们编辑图书模型来继承存档模型:
class LibraryBook(models.Model):
_name = 'library.book'
_inherit = ['base.archive']
# ...
需要对插件模块进行升级来让修改生效。
4.13.3.运行原理
抽象模型基于models.AbstractModel的类进行创建,而非常用的models.Model。它拥有常规模型的所有属性和功能,区别在于ORM不会在数据库中创建实际的体现,这表示它不能存储任何数据,仅用作添加到常规模型中的可复用功能的一个模板。
我们的存档抽象模型非常简单,仅添加active字段和一个方法来切换active标记的值,我们将在稍后在用户界面中通过按钮进行使用。
模型类中定义了_inherit属性时,它继承那些类中的属性方法,定义在当前类中的属性方法对这些继承功能进行修改。
这里所采用的机制与常规模型继承相同(如使用继承向模型添加功能一节)。你可能注意到了_inherit使用一个模型标识符列表而不是带有一个模型标识符的字符串。其实_inherit可以使用这两种形式。使用列表形式允许我们继承多个(通常是抽象)类。在本例中,我们仅继承了一个类,因此使用文本字符串也没有问题。为进行演示我们使用了列表。
4.13.4.扩展知识
值得注意的内置抽象模型是mail.thread,这由mail(Discuss)插件模块提供。在模型中它启用讨论功能来驱动在不同表单底部看到的消息墙。AbstractModel外,还有第三种模型类型:models.TransientModel。像models.Model它有一个数据库体现,但所创建的记录供临时使用,会定期由服务端调度任务清除。除此之后,临时模型和常规模型的功能一致。
models.TransientModel对于称之为向导的更为复杂的用户交互会非常有用,向导用于请求用户输入,在《第八章 高级服务端开发》技巧中,我们探讨如何使用它们来实现高级用户交互。 -
5.3.获取其它模型的空记录集
在编写Odoo代码时,当前模型的方法可通过self访问。如果需要操作其它模型,不能直接实例化该模型的类,需要获取该模型的一个数据集再进行操作。
本节展示如何在Odoo中注册的模型方法中获取任意模型的空记录集。
5.3.1.准备工作
本节将复用my_library模块中所设置的图书示例。
我们会在library.book模型中编写一个小方法并搜索所有的图书会员。这时需要获取library.members的空记录集。确保添加了library.members模型并对该模型设置了访问权限。
5.3.2.如何实现
需要按照如下步骤来获取library.book方法中获取library.members的记录集:
在LibraryBook类中,编写一个名为get_all_library_members的方法:
class LibraryBook(models.Model):...
def log_all_library_members(self): library_member_model = self.env['library.member'] # 这是library.member的空记录集 all_members = library_member_model.search([]) print('所有成员:', all_members) return True
在
视图中添加一个按钮调用该方法:
-
此中文手册不仅仅是翻译,我在翻译中看到了这句话:
“注:原书中本章从本节开始存在多处错误,笔者尽量进行了规避。”
这说明Alanhou是一个非常严谨和负责的人。 -
5.8.过滤记录集
在某些情况下,已有一个记录集,仅需对其中的某些记录进行操作。当然可以遍历记录集并对每条遍历进行条件判断并根据所查看的结果执行操作,构造一个仅包含需操作的记录的新记录集并对该记录集调用同一操作会更容易,在某些情况下还会更高效。
本节展示如何使用 filter()方法来根据从另一个记录集中提取子集。
5.8.1.准备工作我们将复用新建记录一节中所展示的简化的library.book 模型。本节定义一个从给定记录集中提取含有多名作者的图书的方法。
5.8.2.如何实现执行如下步骤来从一个记录集中提取包含多名作者的记录:
定义接收原始记录集的方法:
@api.model
def books_with_multiple_authors(self, all_books):
定义内部的predicate函数:
def predicate(book):
if len(book.author_ids) > 1:
return True
return False
调用filter(),如下:
return all_books.filter(predicate)
可以打印或日志记录该方法的结果 ,在服务端日志中查看。参见本节中的示例代码了解更多。... 5.8.3.运行原理filter()方法的实现创建了一个空记录集,其中添加predicate函数运行结果为True的所有记录。最终返回一个新记录集。保留原记录集中记录的排序。
前面部分使用了一个内部命名函数。对这种简单场景会经常发现使用匿名函数 Lambda:
@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’)。
零基础学习OdooQQ群号3746264835.8.4.扩展知识
记住filter()是在内存中进行运算。如果尝试对关键路径上的方法进行性能优化,可能会要使用搜索域或者甚至是转向SQL,代价是损失代码易读性。 -
5.14.通过read_group()获取组中的数据
在前面的各节中,我们学习了如何从数据库中搜索和获取数据。但有时,会希望通过聚合记录来获取结果,如上个月销售订单的平均成本。在SQL中获取这样的结果我们通常使用group和aggregate函数。所幸的是在Odoo中有read_group() 方法。本节中我们学习如何使用read_group() 方法来获取聚合结果。
5.14.1.准备工作
本小节中,我们将使用《第三章 创建Odoo模块》中的my_library模块图书示例。
修改 library.book模型,如下面的模型定义所示:
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文件中:
class BookCategory(models.Model):
_name = 'library.book.category'name = fields.Char('Category')
description = fields.Text('Description')
我们将使用 library.book模型并获取每个分类的平均成本价。
添加分类模型对应的视图文件,添加相应的权限组配置(ir.model.access.csv)。
输出结果示例
2021-01-10 01:42:44,153 3562 INFO odoo-test odoo.addons.my_library.models.library_book: Grouped Data [{'category_id_count': 2, 'cost_price': 66.5, 'category_id': (1, <odoo.tools.func.lazy object at 0x7f9b38005e58>), '__domain': ['&', ('category_id', '=', 1), ('cost_price', '!=', False)]}, {'category_id_count': 1, 'cost_price': 79.2, 'category_id': (2, <odoo.tools.func.lazy object at 0x7f9b38005cf0>), '__domain': ['&', ('category_id', '=', 2), ('cost_price', '!=', False)]}]
1
2021-01-10 01:42:44,153 3562 INFO odoo-test odoo.addons.my_library.models.library_book: Grouped Data [{'category_id_count': 2, 'cost_price': 66.5, 'category_id': (1, <odoo.tools.func.lazy object at 0x7f9b38005e58>), '__domain': ['&', ('category_id', '=', 1), ('cost_price', '!=', False)]}, {'category_id_count': 1, 'cost_price': 79.2, 'category_id': (2, <odoo.tools.func.lazy object at 0x7f9b38005cf0>), '__domain': ['&', ('category_id', '=', 2), ('cost_price', '!=', False)]}]
5.14.2.如何实现
要提取分组结果,我们在library.book模型中添加_get_average_cost方法,它会使用read_group() 方法来获取分组中的数据:
@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
要测试这一实现,需要在用户界面中添加一个按钮来调用该方法。然后,可以在服务端日志中打印出结果。
5.14.3.运行原理
read_group()方法的内部使用SQL的group by及aggregate函数来获取数据。传递给read_group() 方法的最常用参数如下:
domain:用于为分组过滤记录。更多有关过滤域的知识,请参见《第九章 后端视图》中的“定义搜索视图”一节。
零基础学习OdooQQ群号:374626483
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()。