《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
在
视图中添加一个按钮调用该方法:
-
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()。 -
6.4.使用noupdate和forcecreate标记
大部分的插件模块拥有不同类型的数据。有些数据只要存在模块就可正常运作,另一些数据不可由用户修改,大部分数据都可以供用户按需修改,仅出于方便目的予以提供。本节会详细讲解这些不同类型的数据。首先,我们将在已有记录中写入一个字段,然后我们会创建一条在模块更新时会重新创建的记录。
6.4.1.如何实现
我们可以在加载数据时设置元素或元素自身的某些属性来对Odoo施加不同的行为,
添加在安装时会创建但后续升级中不更新的出版社。但在用户删除它时会被重新创建:
Packt publishing
Birmingham
添加一个在插件更新时不会修改且用户删除后不会重建的图书分类:
All books
6.4.2.运行原理
元素可包含一个noupdate属性,在 ir.model.data记录中由第一次读取所包含的数据记录创建,因此成为数据表中的一个字段。
在Odoo安装插件时(称为 init 模式),不论noupdate为true或false都会写入所有记录。在更新插件时(称为update模式),会查看已有的XML ID来确定是否设置了noupdate标记,如是,则会忽略准备写入到该XML ID 的元素。在用户删除该记录时则并非如此,因此可以通过在update模式下设置记录的forcecreate标记为false来强制不重建noupdate记录。
6.4.3.扩展知识
如果在使用noupdate标记时依然想要加载记录,可以在运行Odoo服务时带上–init=your_addon或-i your_addon参数。这会强制Odoo重新加载记录。还会重建已删除的记录。注意如果模块绕过了XML ID机制的话这可能会导致重复记录以及关联安装出错,例如在用标签调用Python代码来创建记录时。
通过这一代码,可以绕过noupdate标记,但首先请确保这确实是你所需要的。另一个解决这一场景的方案是编写一个迁移脚本,参见《插件更新和数据迁移》一节。
“Odoo学习社区”QQ群号:374626483
6.4.4.其它内容
Odoo还使用XML ID来追踪模块升级后所删除的数据。如果记录在更新前通过模块命名空间获取到一个XML ID,但在更新时重置了该XML ID,记录会因被视作已过期而从数据库中删除。有关这一机制更深入的讨论,请见《模块更新和数据迁移》一节。 -
6.6.模块更新和数据迁移
在编写模块时所选择的数据模型可能会存在一些问题,因此会需要在模块的使用过程中对其进行调整。为允许这一操作而又无需过多技巧,Odoo支持模块中使用版本号并在需要时执行迁移。
6.6.1.如何实现
我们假定在模块的早前版本中date_release字段是一个字符字段,人们可以填写任意他们认为是日期的字段。现在我们意识到需要对这一字段进行比较和聚合运算,因此需要将它的类型修改为Date。
Odoo在类型转换上做了很大的优化,但在这种情况下得靠我们自己了,因此我们需要给出指令来对已安装在数据库中的早前版本进行转换来让当前版本可以运行。按照如下步骤进行操作:
def migrate(cr, version):cr.execute(‘ALTER TABLE library_book RENAME COLUMN date_release TO date_release_char’)
提升__manifest__.py文件中的版本号:
'version': '14.0.1.0.1',
在migrations/14.0.1.0.1/pre-migrate.py中提供预迁移代码:
def migrate(cr, version):
cr.execute('ALTER TABLE library_book RENAME COLUMN date_release TO date_release_char')
在migrations/14.0.1.0.1/post-migrate.py中添加一个迁移后代码:
from odoo import fields
from datetime import datedef migrate(cr, version):
cr.execute('SELECT id, date_release_char FROM library_book')
for record_id, old_date in cr.fetchall():
# check if the field happens to be set in Odoo's internal
# format
new_date = None
try:
new_date = fields.Date.to_date(old_date)
except ValueError:
if len(old_date) == 4 and old_date.isdigit():
# probably a year
new_date = date(int(old_date), 1, 1)
else:
# try some separators, play with day/month/year
# order ...
pass
if new_date:
cr.execute('UPDATE library_book SET date_release=%s', (new_date,))
没有这代码,Odoo会将原来的date_release列重命名为date_release_moved并新建一列,因为没有字符字段对日期字段的直接自动转换。从用户的角度看,date_release的数据会消失。
6.6.2.运行原理
第一个重点是在模块中增加版本号,因为迁移仅在不同版本间进行。在每次更新期间,Odoo在更新时将声明文件中的版本号写入到ir_module_module表中。如果版本号小于或等于3部分时,前缀使用Odoo的大版本和小版本号。前例中,我们显式地使用了Odoo的大小版本号,这是一种良好实践,但1.0.1也可以达到同样的效果,因为在内部,Odoo会为短版本号添加其大版本和小版本号。通常,使用长标记是一种不错的做法,因为很容易地看出它是针对Odoo的哪一个版本的。
两个迁移文件是无需在任何地方注册的代码文件。在更新模块时,Odoo对比在ir_module_module中记录的版本号与插件声明文件中所添加的版本号。如果声明文件的版本号更高(在添加了 Odoo 的大版本和小版本之后),会搜索这一模块的migrations文件夹,查看它是否包含带有范围内版本号的文件夹,包含当前更新的版本号。
然后,在查找到的文件夹内,Odoo搜索以pre-开头的Python文件,加载它们,并预设其中定义了名为migrate的函数,该函数有两个参数。此函数调用时以数据库游标作为第一个参数以及当前安装的版本号作为第二参数。这一时间在Odoo查找插件定义的其它代码之前,因此你可以假定你的数据库结构对比此前版本没有做过任何修改。
在所有预迁移函数成功调用之后,Odoo加载模型以及模块中所定义的数据,这会导致数据库结构的变化。例如我们在pre-migrate.py文件中重命名了date_release,Odoo会以正确的数据类型使用该名称新建一列。
此后,通过同样的搜索算法,会搜索post-migrate文件并在找到时进行执行。本例中,我们需要查看所有的值来了解我们是否能借助它让一些数据可以使用,否则我们会保持数据为NULL。除非绝对必要不要编写遍历整表的脚本,在这种情况下,我们可能会编写一个很大且可读性差的SQL的switch语句。
注意:如果你仅仅是想要重命名一列,则无需编写迁移脚本。此时可以将oldname参数设置为需修改的字段原列名,然后Odoo会自己处理重命名。
6.6.3.扩展知识
在预迁移和迁移后的步骤中,仅能访问到游标,如果你习惯于使用Odoo环境则不是很方便。它可能会导致在这一阶段使用模型预料外的结果,因为在预迁移步骤中,模块模型尚未被载入,同时,在迁移后步骤中,由依赖当前模块定义的模型也还未被加载。但是,如果这对于你来说不是问题,也许是因为你想要使用你的模块所不涉及的模型或者是你已知这不会是一个问题的模型,那么就可以编写如下代码创建一个习惯的环境:
from odoo import api, SUPERUSER_IDdef migrate(cr, version):
env = api.Environment(cr, SUPERUSER_ID, {})env holds all currently loaded models
6.6.4.其它内容
Odoo学习QQ群,群号374626483
在编写迁移脚本时,常常会碰到重复的任务,比如查某数据列或数据表是否存在、重命名或映射一些旧的值到新值。重复造轮子可能会容易产生问题并让人沮丧,如果可以接受额外依赖的话考虑使用https://github.com/OCA/openupgradelib。 -
@digitalsatori 好的,以后注意。
-
9.4.将参数传递给表单和动作——上下文
在内部,Odoo中的每个方法都可以访问一个称为上下文的字典,它从每个动作传播到交付该动作所涉及的方法。UI还可以访问它,并且可以通过在上下文中设置值以各种方式对它进行修改。在本章中,我们将通过使用语言、默认值和隐式过滤器来探索这种机制的一些应用。
9.4.1.准备工作
虽然不是严格必要的,但如果你安装了法语语言,这个小节会更有趣,如果你还没有得到这个。请参阅《第11章国际化》,了解如何做到这一点。如果你有一个法语数据库,将fr_FR改为其他语言;en_US可以代替英语。此外,单击一个客户的活动按钮(当鼠标悬停在它上时更改为存档),以便存档,并验证该合作伙伴不再显示在列表中。
9.4.2.如何实现
1). 创建一个新动作,非常类似于添加菜单项和窗口动作小节中的动作:<act_window id="action_all_customers_fr"
name="Tous les clients"
res_model="res.partner"
domain="[('customer_rank', '<', 1)]"
context="{'lang': 'fr_FR', 'default_lang': 'fr_FR',
'active_test': False, 'default_customer_rank': 1}" />
2). 添加一个调用此动作的菜单。这将留给读者作为练习。
当您打开这个菜单时,视图将以法语显示,如果您创建了一个新的合作伙伴,它们将以法语作为其预选语言。一个不太明显的区别是,您还将看到停用(存档)的合作伙伴记录。
9.4.3.工作原理
上下文字典由几个源填充。首先,从当前用户的记录中读取一些值(lang和tz,代表用户的语言和时区)。然后,我们有一些附加组件,它们根据自己的目的添加键。此外,UI添加了关于当前我们正在处理哪个模型和哪个记录的键(active_id, active_ids, active_model)。同样,正如在打开特定视图的动作中所看到的,我们可以在动作中添加我们自己的键。它们被合并在一起,并传递给底层服务器函数和客户端UI。
因此,通过设置lang上下文键,我们将强制显示语言为法语。您将注意到,这不会改变整个UI语言,这是因为我们打开的列表视图位于此上下文中的范围内。UI的其余部分已经通过包含用户原始语言的另一个上下文加载了。但是,如果在这个列表视图中打开一条记录,它也会以法语显示,如果在表单上打开一条链接的记录或按下执行动作的按钮,语言也会传播。
通过设置default_lang,我们为在此上下文范围内创建的每个记录设置一个默认值。一般模式是default_$fieldname: my_default_value,在本例中,它允许您为新创建的合作伙伴设置默认值。假定我们的菜单是关于客户的,我们已经添加了default_customer_rank: 1作为默认情况下客户等级字段的值。但是,这是res.partner的模型范围内的默认值,所以这不会改变任何东西。对于标量字段,其语法与你在Python代码中编写的一样:字符串字段加引号,数字字段保持原样,布尔字段为真或假。对于关系字段,语法稍微复杂一些;请参阅《第6章,管理模块数据》,了解如何编写它们。
注意:
上下文中设置的默认值会覆盖模型定义中设置的默认值,因此您可以在不同的情况下使用不同的默认值。
最后一个键是active_test,它具有非常特殊的语义。对于每个具有active字段的模型,Odoo会自动过滤掉该字段为False的记录。这就是您取消选中该字段的合作伙伴从列表中消失的原因。通过设置这个键,我们可以抑制这个行为。
这对于UI本身是很有用的,但在您的Python代码中,当您需要确保一个动作应用于所有记录,而不仅仅是活动的记录时,它就更有用了。 -
-
作者 上海-Alan 说:不支持盗版,其实也不想鼓励用爱发电,毕竟好的开源项目都需要好的商业运作的支持,我会做完我计划做的事,剩下的交给社区吧:
https://github.com/iTranslateX关于如何接手,区块链给出了答案,PoW,PoS,要么出力,要么有江湖地位,当然你也完全可以一键fork走~