《Odoo14开发者指南》
-
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走~