【odoo14】【好书学习】第八章、服务侧开发-进阶
本章代码位于作为GITHUB库 https://github.com/PacktPublishing/Odoo-14-Development-Cookbook-Fourth-Edition
在第五章(服务侧开发-基础篇)中,我们了解了如何在类中创建函数,如何从继承的类扩展函数以及如何处理数据集。本章将会讨论一些更进一步的内容,比如处理数据集的上下文,通过按钮点击触发函数,处理onchange函数。本章将包含如下内容:
-
更改执行动作的用户
-
通过编辑过的上下文执行方法
-
执行原生SQL查询
-
为用户编写操作向导
-
定义onchange方法
-
在服务器端调用onchange方法
-
通过计算方法定义onchange
-
基于SQL视图定义模型
-
添加用户配置选项
-
实现在模块安装时的函数(个人叫它钩子函数)
更改执行动作的用户
当我们写一些业务逻辑的时候,你可能需要通过不同的上下文执行动作。典型的场景是通过superuser用户越过权限控制。有这样一些场景,我们需要操作一些我们并没有权限的数据。
本节将介绍普通用户如何通过sudo()实现修改图书的状态。
准备
为了理解容易,我们新建一个管理图书借阅的模型,library.book.rent。
class LibraryBookRent(models.Model):
_name = 'library.book.rent'
book_id = fields.Many2one('library.book', 'Book', required=True)
borrower_id = fields.Many2one('res.partner', 'Borrower', required=True)
state = fields.Selection([('ongoing', 'Ongoing'), ('returned', 'Returned')],required=True)
rent_date = fields.Date(default=fields.Date.today)
return_date = fields.Date()
你需要添加form视图、菜单、动作等以可以在前台UI看到相关内容,实现交互。
步骤
如果您测试了该模块,您将发现只有拥有图书管理员访问权限的用户才能将图书标记为借阅。非图书管理员用户不能自己借书;他们需要问图书管理员用户。假设我们想添加一个新功能,这样非图书管理员用户就可以自己借书了。我们将在不给他们访问权限的情况下执行此操作library.book.rent模型。
- 添加book_rent()
class LibraryBook(models.Model):
_name = 'library.book'
...
def book_rent(self):
- 确保数据为一
self.ensure_one()
- 如果一本书无法借阅,则发出警告(请确保在顶部导入了UserError)
if self.state != 'available':
raise UserError(_('Book is not available for renting'))
- 获取的空记录:
rent_as_superuser = self.env['library.book.rent'].sudo()
- 使用适当的值创建新的图书借阅记录:
rent_as_superuser.create({
'book_id': self.id,
'borrower_id': self.env.user.partner_id.id,
})
- 要从用户界面触发此方法,请将按钮添加到书本的窗体视图:
<button name="book_rent" string="Rent this book" type="object" class="btn-primary"/>
重新启动服务器并更新给定的更改。更新后,您将在“书本窗体”视图上看到“租用本书”按钮。当你点击它,将创建新的租金记录。这也适用于非图书管理员用户。您可以作为演示用户访问Odoo来测试这一点。
原理
在前三个步骤中,我们添加了名为book_rent()的新方法。当用户单击“书本窗体”视图上的“租用本书”按钮时,将调用此方法。
在步骤4中,我们使用sudo()。此方法返回一个新的记录集,其中包含用户具有超级用户权限的已修改环境。调用记录集时使用sudo(),环境会将环境属性修改为su,它指示环境的超级用户状态。您可以通过记录集.env.su. 通过此sudo记录集的所有方法调用都是使用超级用户权限进行的。要更好地了解这一点,请从方法中删除.sudo(),然后单击“租这本书”按钮。它将引发访问错误,用户将无法再访问模型。简单地使用sudo()将绕过所有安全规则。
如果需要特定用户,可以传递包含该用户或该用户的数据库ID的记录集,如下所示:
public_user = self.env.ref('base.public_user')
public_book = self.env['library.book'].with_user(public_user)
public_book.search([('name', 'ilike', 'cookbook')])
更多
使用sudo(),可以绕过访问权限和安全记录规则。有时您可以访问要隔离的多个记录,例如多公司环境中来自不同公司的记录。sudo()记录集绕过Odoo的所有安全规则。
如果不小心,在此环境中搜索的记录可能会链接到数据库中的任何公司,这意味着您可能正在向用户泄漏信息;更糟的是,您可能会通过链接属于不同公司的记录而悄悄地损坏数据库。
重要提醒
使用sudo()时,请始终仔细检查以确保对search()的调用不依赖标准记录规则来筛选结果。
通过编辑过的上下文执行方法
上下文是记录集环境的一部分。它用于从用户界面传递额外的信息,例如时区和用户的语言。还可以使用上下文传递操作中指定的参数。标准Odoo附加组件中的许多方法使用上下文来根据这些上下文值调整其业务逻辑。有时需要修改记录集上的上下文,以便从方法调用中获得所需的结果或计算字段的所需值。
这个配方将展示如何根据环境上下文中的值更改方法的行为。
准备
对于本节,我们将使用上一节中的my_library模块。在library.book.rent模型视图中,我们将添加一个按钮来标记该书为丢失,以防普通用户丢失一本书。注意,我们在书的表单视图中已经有了相同的按钮,但是在这里,我们将有一个稍微不同的行为来理解Odoo中上下文的使用。
步骤
- 更新状态字段的定义,使其具有丢失状态:
state = fields.Selection([ ('ongoing', 'Ongoing'),
('returned', 'Returned'),
('lost', 'Lost')],
'State', default='ongoing', required=True)
- 在窗体视图中添加“标记为丢失”按钮:
<button name="book_lost" string="Lost the Book"
states="ongoing" type="object"/>
- 添加book_lost()方法
def book_lost(self):
···
- 在方法中,确保我们对单个记录执行操作,然后更改状态:
self.ensure_one() self.sudo().state = 'lost'
- 在方法中添加以下代码以更改环境的上下文,并调用该方法将书本的状态更改为lost:
book_with_different_context = self.book_id.with_context(avoid_deactivate=True) book_with_different_context.sudo().make_lost()
- 更改library.book模型具有不同的行为:
def make_lost(self):
self.ensure_one()
self.state = 'lost'
if not self.env.context.get('avoid_deactivate'):
self.active = False
原理
在步骤1中,我们为该书添加了一个新状态。这个新状态将显示丢失的书。在步骤2中,我们添加了一个新按钮,markaslost。用户将使用此按钮报告丢失的书。
在第3步和第4步中,我们添加了一个方法,当用户单击markaslost时将调用该方法。
第5步调用带有某些关键字参数的self.book_id.with_context()。这将返回具有更新上下文的book_id记录集的新版本。我们在这里的上下文中添加了一个键,avoid_deactivate=True,但是如果需要,可以添加多个键。我们在这里使用了sudo(),因此非图书管理员用户可以将书籍报告为丢失。
在第6步中,我们检查了上下文中的avoid_deactivate键是否为正值。我们避免停用图书,这样图书管理员即使丢了也能看到。
现在,当图书管理员在“图书窗体”视图中报告丢失的图书时,图书记录状态将更改为“丢失”,图书将被存档。但是,当非图书管理员用户在其租金记录中报告丢失的图书时,图书记录状态将更改为“丢失”;图书将不会存档,以便图书管理员以后查看。
这只是一个修改上下文的简单示例,但是您可以根据自己的需求在ORM中的任何地方使用它,即对象关系映射(Object Relational Mapping)的缩写。
更多
也可以通过上下文()将字典传递给。在本例中,字典用作新上下文,它将覆盖当前上下文。因此,步骤5也可以写为:
new_context = self.env.context.copy()
new_context.update({'avoid_deactivate': True})
book_with_different_context = self.book_id.with_context(new_context)
book_with_different_context.make_lost()
执行原生SQL查询
大多数时候,您可以通过使用Odoo的ORM来执行您想要的操作。例如,您可以使用search()方法来获取记录。然而,有时,你需要更多;要么您无法使用域语法表达您想要的内容(对于域语法,有些操作是棘手的,如果不是完全不可能的话),要么您的查询需要多次调用search(),这最终导致效率低下。
准备
对于本节,我们将使用上节中的my_library模块。为简单起见,我们将只在日志中打印结果,但在实际场景中,您将需要在业务逻辑中使用查询结果。在第9章“后端视图”中,我们将在用户界面中显示该查询的结果。
步骤
要获得关于用户保存特定图书的平均天数的信息,您需要执行以下步骤:
- 将average_book_occupation()方法添加到library.book:
def average_book_occupation(self): ...
- 将尚未提交到数据库的计算提交
self.flush()
- SQL代码如下
sql_query = """ SELECT
lb.name,
avg((EXTRACT(epoch from age(return_date, rent_ date)) / 86400))::int
FROM
library_book_rent AS lbr
JOIN
library_book as lb ON lb.id = lbr.book_id
WHERE lbr.state = 'returned' GROUP BY lb.name;"""
- 执行查询
self.env.cr.execute(sql_query)
- 获取数据并打印到日志
result = self.env.cr.fetchall()
logger.info("Average book occupation: %s", result)
- 添加按钮
<button name="average_book_occupation" string="Log Average Occ." type="object" />
原理
步骤1,我们添加了average_book_occupation()方法,当用户单击Log Average OCc. 按钮时调用。
步骤2,我们使用了flush()方法。从Odoo13开始,ORM多度使用内容。在每一个事务中,ORM使用一个全局缓存。所以,有可能数据库中的记录与缓存中的记录时不同的。通过flush()函数,可以确保所有在缓存中的更改同步到数据库中。
步骤3,我们声明一个SQL SELECT查询。这将返回用户持有某本书的平均天数。如果您在PostgreSQL CLI中运行这个查询,您将得到一个基于图书数据的结果。如下:
步骤4,对存储在self.env.cr中的数据库游标调用execute()方法。这会将查询发送给PostgreSQL并执行它。
步骤5,使用游标的fetchall()方法来检索所选行的列表的查询。这个方法返回一个行列表。在我的例子中,这是[('Odoo 12 Development Cookbook', 33), ('PostgreSQL 10 Administration Cookbook', 81)]。从我们执行的查询的形式中,我们知道每一行正好有两个值,第一个是name,另一个是用户持有某本书的平均天数。然后,我们简单地记录它。
步骤6,添加按钮。
重要提醒
当在使用UPDATE语句进行更新的时候,我们需要手动将缓存功能置为无效。可通过self.invalidate_cache()。
更多
self.env.cr是对psycopg2游标的简单封装。如下是常用的一些函数:
- execute(query, params): 这将执行SQL查询,查询中标记为%s的参数将被params中的值替换,params是一个元组。
警告
不要尝试自己通过%s的方式拼接SQL,这可能导致SQL注入的风险。
- fetchone(): 返回一行数据,元组格式。
- fetchall(): 返回所有的行,元组的序列。
- dictfetchall(): 返回所有的行,以列名和值的字典列表。
在使用原生SQL的时候需要特别小心
- 通过原生SQL将会跳过应用的权限验证。一定要确保在search([('id','in',tuple(ids))])中的IDs列表是过滤掉用户无权访问的记录。
- 你所做的任何修改都绕过了附加模块设置的约束,除了NOT NULL, UNIQUE和FOREIGN KEY约束,这些约束在数据库级别强制执行。对于任何计算字段重新计算触发器,也都是如此,所以最终可能会破坏数据库。
- 避免INSERT/UPDATE查询,因为通过查询插入或更新记录将不会运行通过覆盖create()和write()方法编写的任何业务逻辑。它不会更新存储的计算字段,ORM约束也会被绕过。
为用户编写操作向导
在第4章,应用模型,模型中使用可重用模型的抽象模型特性配方。引入了TransientModel基类。这个类与普通模型有很多共享,除了在数据库中定期清理暂态模型的记录之外,因此命名为transient。它们用于创建向导或对话框,这些向导或对话框由用户在用户界面中填充,通常用于对数据库的持久记录执行操作。
准备
对于本节,我们将使用前面食谱中的my_library模块。这个配方将添加一个新的向导。有了这个向导,图书管理员将能够同时发行多本书。
步骤
- 向模块中添加一个新的瞬态模型,定义如下:
class LibraryRentWizard(models.TransientModel):
_name = 'library.rent.wizard'
borrower_id = fields.Many2one('res.partner', string='Borrower')
book_ids = fields.Many2many('library.book', string='Books')
- 添加对瞬态模型执行操作的回调方法。将以下代码添加到LibraryRentWizard类中:
def add_book_rents(self):
rentModel = self.env['library.book.rent']
for wiz in self:
for book in wiz.book_ids:
rentModel.create({
'borrower_id': wiz.borrower_id.id,
'book_id': book.id
})
- 为模型创建一个表单视图。将以下视图定义添加到模块视图中:
<record id='library_rent_wizard_form' model='ir. ui.view'>
<field name='name'>library rent wizard form view</ field>
<field name='model'>library.rent.wizard</field>
<field name='arch' type='xml'>
<form string="Borrow books">
<sheet>
<group>
<field name='borrower_id'/>
</group>
<group>
<field name='book_ids'/>
</group>
</sheet>
<footer>
<button string='Rent' type='object' name='add_book_rents' class='btn-primary'/>
<button string='Cancel' class='btn-default' special='cancel'/>
</footer>
</form>
</field>
- 创建向导的动作及菜单。
<act_window id="action_wizard_rent_books" name="Give on Rent"
res_model="library.rent.wizard"
view_mode="form" target="new" />
<menuitem id="menu_wizard_rent_books"
parent="library_base_menu" action="action_wizard_rent_books" sequence="20" />
- 添加访问权限控制
acl_library_rent_wizard,library.library_rent_ wizard,model_library_rent_wizard,group_librarian,1,1,1,1
原理
步骤1,定义了一个新的模型。它与其他的模型没有什么区别。除了继承的基类,该模型继承自TransientModel。TransientModel和Model都继承自BaseModel。如果查看Odoo的源码,可看到99%的工作都是基于BaseModel的。
在TransientModel记录中唯一改变的事情如下:
- 记录定期从数据库中删除,以便瞬态模型的表不会随着时间而增长。
- 您不允许在引用普通模型的TransientModel实例上定义one2many字段,因为这会在链接到临时数据的持久模型上添加一个列。在这种情况下使用many2many关系。当然,如果one2many中的相关模型也是瞬态模型,你可以使用one2many字段。
我们在模型中定义了两个字段:一个存储借书的成员,另一个存储借书的列表。例如,我们可以添加其他标量字段来记录预定的返回日期。
步骤2,将代码添加到向导类中,单击步骤3中定义的按钮时将调用该类。此代码从向导中读取值并创建library.book.rent的记录。
步骤3,为向导定义一个视图。有关详细信息,请参阅第9章“后端视图”中的文档样式表单配方。这里的重点是页脚中的按钮;type属性设置为“object”,这意味着当用户单击按钮时,将调用按钮的name属性指定名称的方法。
步骤4,确保在应用程序的菜单中有向导的入口点。我们在操作中使用target='new',以便窗体视图在当前窗体上显示为对话框。有关详细信息,请参阅第9章“后端视图”中的添加菜单项和窗口操作方法。
步骤5,我们已经为library.rent.wizard向导模型。这样,图书管理员用户将拥有library.rent.wizard向导模型的所有访问权。
注意
在odoo14之前的版本中,TransientModel并不需要特定的访问权限。任何人都可以创建并访问自己创建的内容。但在odoo14中TransientModel也必须具有访问权限才可以使用。
更多
使用上下文去计算默认值
我们的向导需要用户输入成员的名称。根据网页客户端的特性,我们可以保存一些输入的内容。当窗体动作被执行的时候,上下文将携带这些值,供向导使用。
- active_model: 这是关联到动作的模型的名称。
- active_id: 这表示form视图下单个记录处于活动状态,并提供该记录的ID。
- active_ids: 有多个记录被选择,是一组ID列表。这表示在动作被触发的时候,有tree视图下多条记录被选择。
- active_domain: 这是在向导运行时额外的过滤。
这些值可以用来计算模型的默认值,甚至可以直接在按钮调用的方法中使用。为了提升本节中的例子,我们在res.partner视图下有一个按钮,可触发向导动作。其上下文中将包含{'active_model': 'res.partner', 'active_id': <partner_id>}。我们可以定义member_id字段以通过如下函数计算默认值:
def _default_member(self):
if self.context.get('active_model') == 'res.partner':
return self.context.get('active_id', False)
向导和代码复用
步骤2,我们可以移除for循环。并假设len(self)为1,我们可添加self.ensure_one()方法。
def add_book_rents(self):
self.ensure_one()
rentModel = self.env['library.book.rent']
for book in self.book_ids:
rentModel.create({
'borrower_id': self.borrower_id.id, 'book_id': book.id
})
在函数开通添加self.ensure_one()将确保记录的数量为1。如果超过1,那么将会报错。
我们建议使用这个版本。因为这可以让我们能够复用创建记录的代码。
重定向
步骤2中并没有返回任何内容。这会在向导视图完成相关操作后直接关闭。还有一个方式是返回一个ir.action对象的字典。这时,页面将会捕获到该信息并进行响应,就像用户点击了菜单一样。在BaseModel模型中的get_formview_action()将会动作。在这个实例中,我们计划展示那些人借了书的form视图。代码如下:
def add_book_rents(self):
rentModel = self.env['library.book.rent']
for wiz in self:
for book in wiz.book_ids:
rentModel.create({
'borrower_id': wiz.borrower_id.id,
'book_id': book.id
})
borrowers = self.mapped('borrower_id')
action = borrowers.get_formview_action()
if len(borrowers.ids) > 1:
action['domain'] = [('id', 'in', tuple(borrowers.ids))]
action['view_mode'] = 'tree,form'
return action
这将列出通过向导借阅了图书的人(实际上将会只有一个借阅者),并且创建了一个动态的行为(将展示特定ID的用户)。
参考
- 第九章,文档格式的form视图,将了解关于向导的更多知识
- 第九章,添加菜单及窗体工作,将会加深我们对服务器侧开发的理解。
- 第五章,组合数据集,可以更好的理解为向导创建记录并将其组合成一个数据集的情况。
定义onchange方法
在编写业务逻辑时,一些字段经常是相互关联的。我们在第4章“应用程序模型”的“向模型配方添加约束验证”中了解了如何在字段之间指定约束。这个食谱说明了一个稍微不同的概念。在这里,当在用户界面中修改字段时,调用onchange方法来更新web客户机中记录的其他字段的值。
举例说明,本节我们创建一个与”引导用户的向导“一节中类似的向导,但是它能用于记录图书的归还情况。当向导中一个成员被设置了,该成员的借书清单也将更新。我们将在TransientModel举例说明onchange函数,这些特性在普通模型中也是可用的。
准备
本节,我们将”引导用户的向导“一节中的my_library模块。我们创建了一个用于归还图书的向导。我们将添加onchange函数,用于当管理员选择成员字段的时候,实现自动填充图书字段。
我们新增一个向导的虚拟模型:
class LibraryReturnWizard(models.TransientModel): _name = 'library.return.wizard'
borrower_id = fields.Many2one('res.partner', string='Member')
book_ids = fields.Many2many('library.book', string='Books')
def books_returns(self):
loanModal = self.env['library.book.rent']
for rec in self:
loans = loanModal .search( [('state', '=', 'ongoing'),
('book_id', 'in', rec.book_ids.ids),
('borrower_id', '=', rec.borrower_id.id)] )
for loan in loans:
loan.book_return()
最后,我们还需要定义视图、动作及菜单。
步骤
为了当用户改变的时候,待归还的图书列表能够实现自动填充,我们需在LibraryReturnWizard中实现Onchange函数:
@api.onchange('borrower_id')
def onchange_member(self):
rentModel = self.env['library.book.rent']
books_on_rent = rentModel.search(
[('state', '=', 'ongoing'),
('borrower_id', '=', self.borrower_id.id)])
self.book_ids = books_on_rent.mapped('book_id')
原理
onchange方法使用@api.onchange装饰器,通过传递目标字段实现在目标字段变化后自动触发该函数。在我们的例子中,我们可以看到在borrowser_id变化后,该函数将被触发。
在方法体内部,我们查找用户最新借阅的图书列表,并将其赋值给book_ids属性。
更多
onchange函数的基本用法是在目标变化后自动计算相应字段的值。
在方法体的内部,我们可以访问当前记录的当前视图下的展示的字段,而并不一定能够访问该模型所有的字段。这是因为记录在被创建但尚未存储到数据库的时候,onchange就可以被调用。在onchange的内部,self是一个特殊的状态,实际上,self.id并不是一个整数,而是odoo.model.NewId的一个实例。因此,在Onchange中你不可以修改数据库的内容,因为用户可能会取消创建记录,而在用户取消创建记录后,数据库的修改并不会回滚。
此外,onchange函数可以返回一个Python的字典。字典中可以有如下key:
- warning: 值必须是另一个带有title、message键的字典。这将返回前端一个弹框信息,用于告知用户可能存在的错误。
- domain: 值必须是另一个将字段名映射到域的字典。当你改变one2many字段的域时非常有用。
例如,在libaray.book.rent模型中的expected_return_date字段是一个固定的值,我们想当用户有一些书过期尚未归还的时候展示警告信息。我们还希望将书籍的选择限制为用户当前所借的书籍。我们重写onchange函数如下:
@api.onchange('member_id')
def onchange_member(self):
rentModel = self.env['library.book.rent']
books_on_rent = rentModel.search( [('state', '=', 'ongoing'),
('borrower_id', '=', self.borrower_id.id)] )
self.book_ids = books_on_rent.mapped('book_id')
result = {
'domain': {'book_ids': [
('id', 'in', self.book_ids.ids)]
}
}
late_domain = [
('id', 'in', books_on_rent.ids),
('expected_return_date', '<', fields.Date.today())
]
late_books = loans.search(late_domain)
if late_books:
message = ('Warn the member that the following books are late:\n')
titles = late_books.mapped('book_id.name')
result['warning'] = {
'title': 'Late books',
'message': message + '\n'.join(titles)
}
return result
在服务器端调用onchange方法
onchange函数由一个限制: 当你在服务器侧动作的时候是不会被自动触发的。但是在一些场景中,onchange函数又是特别重要。因此,我们需要自己计算相关字段,但是,在我们修改第三方的模块时,我们并不了解其内部逻辑的情况下,是无法实现。
本节将介绍如果在创建记录前,手动触发onchange。
准备
在"改变执行动作的用户"一节中,我们添加了Rent this book按钮,这可以让非管理员自己实现借书。我们也想实现自动还书的功能,但是又不想去写还书的业务逻辑。我们将直接使用”定义onchange方法“中的还书向导。
步骤
在本节,我们将手动创建一个library.return.wizard模型记录。我们想通过onchange函数为我们自动计算归还的图书。
- 将tests中的Form导入library_book.py
from odoo.tests.common import Form
- 在library.book模型中创建return_this_books方法
def return_this_books(self):
self.ensure_one()
- 获取library.return.wizard模型的空数据集:
wizard = self.env['library.return.wizard']
- 创建向导Form块:
with Form(wizard) as return_form:
- 通过对borrower_id赋值触发onchange函数:
return_form.borrower_id = self.env.user.partner_id
record = return_from.save()
record.books_returns()
步骤
步骤1-3是前面几章的内容。
步骤4,创建一个虚拟的Form视图,类似于GUI。
步骤5,包含了返回所有图书的全部逻辑。我们首先对borrower_id进行赋值。这回触发onchange函数。然后通过save()函数,可返回向导记录。然后我们调用books_returns()放执行返回所有图书的函数。
通过计算方法定义onchange
在前两节中,我们看到了如何定义和调用onchange方法。我们也看到了它的局限性,即只能从用户界面自动调用它。作为这个问题的解决方案,Odoo v13引入了一种新的方法来定义onchange行为。在这篇文章中,我们将看到如何使用compute方法来产生类似onchange方法的行为。
准备
本节,我们将使用上节中的my_library模块。我们将用compute方法替换library.return.wizard的onchange方法。
步骤
- 替代api.onchange_member()方法中的onchange:
@api.depends('borrower_id') def onchange_member(self):
...
- 在字段的定义中添加计算参数,如下所示:
book_ids = fields.Many2many('library.book', string='Books',compute="onchange_member", readonly=False)
原理
在功能上,我们计算的onchange工作方式与普通的onchange方法类似。唯一的区别是现在onchange也会在后端更改时触发。
在步骤1中,我们替换了@api.onchange @api.compute。当字段值改变时,需要重新计算方法。
在步骤2中,我们将计算方法注册到字段中。如果您注意到,我们在计算字段定义中使用了readonly=False。默认情况下,计算方法是只读的,但是通过设置readonly=False,我们可以确保该字段是可编辑和存储的。
更多
由于计算的onchange也在后端工作,我们不再需要在return_all_books()方法中使用Form类。您可以替换代码如下:
def return_all_books(self):
self.ensure_one()
wizard = self.env['library.return.wizard']
wizard.create({
'borrower_id': self.env.user.partner_id.id
}).books_returns()
基于SQL视图定义模型
在设计附加模块时,我们对类中的数据进行建模,然后通过Odoo的ORM将这些数据映射到数据库表。我们应用了一些众所周知的设计原则,例如关注点分离和数据规范化。但是,在模块设计的后期阶段,将来自多个模型的数据聚合到一个表中,并在途中对它们执行一些操作,特别是用于报告或生成仪表板,可能会很有用。为了使这更容易,并充分利用Odoo中底层PostgreSQL数据库引擎的功能,可以定义一个由PostgreSQL视图支持的只读模型,而不是表。
在本节,我们将重用“编写向导”中的租用模型,并且我们将创建一个新模型,以便更容易地收集有关书籍和作者的统计信息。
准备
我们创建一个新的模型,library.book.rent.statistics以展示静态数据。
步骤
基于PostgreSQL视图创建新的方法
- 创建带有_auto=False的模型
class LibraryBookRentStatistics(models.Model):
_name = 'library.book.rent.statistics'
_auto = False
- 设置只读字段
book_id = fields.Many2one('library.book',string='Book',readonly=True)
rent_count = fields.Integer(string="Timesborrowe" ,readonly=True)
average_occupation = fields.Integer(string="Average Occupation (DAYS)",readonly=True)
- 定义init()创建视图
def init(self):
tools.drop_view_if_exists(self.env.cr, self._table)
quert = """
CREATE OR REPLACE VIEW library_book_rent_ statistics AS (
SELECT min(lbr.id) as id,
lbr.book_id as book_id,
count(lbr.id) as rent_count,
avg(
(
EXTRACT(
epoch
from age(return_date, rent_date)
) / 86400
)
)::int as average_occupation
FROM library_book_rent AS lbr
JOIN library_book as lb ON lb.id = lbr.book_id
WHERE lbr.state = 'returned'
GROUP BY lbr.book_id
);
- 我们现在可以为新模型定义视图了。pivot视图在浏览数据方面将会非常方便。
- 定义访问权限控制。
原理
通常,Odoo将使用列的字段定义为您正在定义的模型创建一个新表。这是因为在BaseModel类中,\u auto属性默认为True。在步骤1中,通过将这个class属性定位为False,我们告诉Odoo我们将自己管理它。
步骤2中,我们定义了一些字段,Odoo将使用这些字段生成一个表。我们注意将它们标记为readonly=True,这样视图就不会启用您无法保存的修改,因为PostgreSQL视图是只读的。
步骤3定义init()方法。此方法通常不执行任何操作;它在_auto_init()之后调用(当_auto=True时,它负责创建表,但在其他情况下不执行任何操作),我们使用它来创建新的SQL视图(或者在模块升级时更新现有视图)。视图创建查询必须创建列名与模型字段名匹配的视图。
重要提醒
在这种情况下,忘记重命名视图定义查询中的列是一个常见的错误,当Odoo找不到该列时,这将导致错误消息。
注意,我们还需要提供一个名为ID的整数列,该列包含唯一的值。
更多
在这样的模型上也可以有一些计算的和相关的字段。唯一的限制是不能存储这些字段(因此,不能使用它们对记录进行分组或搜索)。但是,在前面的示例中,我们可以通过添加一个列来提供该书的编辑器,定义如下:
publisher_id = fields.Many2one('res.partner', related='book_ id.publisher_id', readonly=True)
如果需要按发布者分组,则需要通过在视图定义中添加字段来存储字段,而不是使用相关字段。
添加用户配置选项
在Odoo中,您可以通过Settings选项提供可选功能。用户可以随时启用或禁用此选项。我们将演示如何在此配方中创建设置选项。
准备
在前面几节中,我们添加了按钮,以便非图书管理员用户可以借书和还书。并非每个库都是这样;但是,我们将创建一个设置选项来启用和禁用此功能。我们要把这些按钮藏起来。在本节,我们将使用与前面配方相同的my_library库模块。
步骤
- 添加权限组group:
<record id="group_self_borrow" model="res.groups">
<field name="name">Self borrow</field>
<field name="users" eval="[(4,ref('base.user_admin'))]"/>
</record>
- 继承res.config.setttins模型并添加字段
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
group_self_borrow = fields.Boolean(string="Self borrow",implied_group='my_library.group_self_borrow')
- 通过xpath添加字段到settings视图中:
<record id="res_config_settings_view_form" model="ir. ui.view">
<field name="name">res.config.settings.view.form. inherit.library</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="5"/>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('settings')]" position="inside">
<div class="app_settings_block" data-string="Library" string="Library" data-key="my_library" groups="my_library.group_librarian">
<h2>Library</h2>
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_ setting_box" id="library">
<div class="o_setting_left_pane">
<field name="group_self_borrow"/>
</div>
<div class="o_setting_right_pane">
<label for="group_self_borrow"/>
<div class="text-muted"> Allow users to borrow and return books by themself
</div>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
- 添加动作及菜单
<record id="library_config_settings_action" model="ir. actions.act_window">
<field name="name">Settings</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">res.config.settings</field>
<field name="view_id" ref="res_config_settings_view_form"/>
<field name="view_mode">form</field>
<field name="target">inline</field>
<field name="context">{'module' : 'my_library'}</field>
</record>
<menuitem name="Settings" id="library_book_setting_menu" parent="library_base_menu" action="library_config_settings_action" sequence="50"/>
- 修改书的窗体视图中的按钮并添加“my_library.group“以实现自助借阅:
<button name="book_rent" string="Rent this book" type="object" class="btn-primary" groups="my_library.group_self_borrow"/>
<button name="return_all_books" string="Return all book" type="object" class="btn-primary" groups="my_library.group_self_borrow"/>
原理
在Odoo中,所有设置选项都添加在资源配置设置模型。物件。配置设置是一个瞬态模型。在步骤1中,我们创建了一个新的安全组。我们将使用这个组来创建隐藏和显示按钮。
在步骤2中,我们在资源配置设置通过继承模型。我们添加了一个implied_group属性和my_library.group_self_borrow。当管理员使用布尔字段启用或禁用选项时,此组将分配给所有odoo用户。
Odoo设置使用窗体视图在用户界面上显示设置选项。所有这些选项都添加到具有外部ID的单个窗体视图中,base.res_config_settings_view_form。在步骤3中,我们通过继承这个设置表单视图在用户界面中添加了我们的选项。我们使用xpath添加了设置选项。在第9章“后端视图”中,我们将详细了解这一点。在表单定义中,您会发现这个选项的属性数据键值将是您的模块名。只有在“设置”中添加全新选项卡时,才需要此选项。否则,您可以使用xpath在现有模块的Settings选项卡中添加您的选项。
在步骤4中,我们添加了一个操作和一个菜单来从用户界面访问配置选项。您需要从操作中传递{module':'my_library}上下文,以便在单击菜单时默认打开my_library模块的设置选项卡。
在步骤5中,我们添加了my_library.group_self_borrow按钮。由于此组,将根据设置选项隐藏或显示“借用”和“归还”按钮。
之后,您将看到一个单独的库设置选项卡,在该选项卡中,您将看到一个布尔字段,用于启用或禁用自借选项。当您启用或禁用此选项时,在后台,Odoo将向所有Odoo用户应用或从中删除组。因为我们在按钮上添加了组,所以如果用户有组,按钮将显示;如果用户没有组,按钮将隐藏。在第10章“安全访问”中,我们将详细介绍安全组。
更多
还有其他一些方法可以管理设置选项。其中之一是分离新模块中的功能,并通过选项安装或卸载它们。为此,您需要添加一个以模块名称为前缀的布尔字段。例如,如果我们创建了一个名为my_library_extras的新模块,则需要添加一个布尔字段,如下所示:
module_my_library_extras = fields.Boolean( string='Library Extra Features')
启用或禁用此选项时,odoo将安装或卸载my_libarary_extras模块。
管理设置的另一种方法是使用系统参数。这些数据存储在ir.config_parameter参数模型。下面介绍如何创建系统范围的全局参数:
digest_emails = fields.Boolean(string="Digest Emails",config_parameter='digest.default_digest_emails')
字段中的config_parameter属性将确保用户数据存储在 设置|技术|参数|系统参数菜单的系统参数中。数据将与digest.default_digest_emails。
设置选项用于使应用程序通用。这些选项为用户提供了自由,允许他们动态地启用或禁用功能。当您将功能转换为选项时,您可以使用一个模块为更多客户提供服务,并且您的客户可以随时启用该功能。
实现在模块安装时的函数(个人叫它钩子函数)
在第6章“管理模块数据”中,您了解了如何从XML或CSV文件中添加、更新和删除记录。然而,有时业务案例很复杂,无法使用数据文件来解决。在这种情况下,可以使用清单文件中的init钩子来执行所需的操作。
准备
我们将使用与上节my_library模块。为了简单起见,在本节,我们将通过post_init_hook创建一些图书记录。
步骤
- 在__manifest__.py中添加post_init_hook的键。
'post_init_hook': 'add_book_hook',
- 在__init__.py中添加add_book_hook():
from odoo import api, fields, SUPERUSER_ID
def add_book_hook(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
book_data1 = {'name': 'Book 1', 'date_release':fields.Date.today()}
book_data2 = {'name': 'Book 2', 'date_release':fields.Date.today()}
env['library.book'].create([book_data1, book_data2])
原理
步骤1,我们在manifest文件中添加post_init_hook:add_book_hook的键值。
步骤2,我们声明了add_book_hook()方法,该方法将在安装模块后调用。我们用这个方法创建了两个记录。在实际情况中,您可以在这里编写复杂的业务逻辑。
odoo中还有两种hooks方法:
- pre_init_hook: 当您开始安装模块时,将调用此钩子。它与post_init_hook相反;它将在安装当前模块之前被调用。
- uninstall_hook:卸载模块时将调用此钩子。当您的模块需要垃圾回收机制时,通常会使用此方法。
本文来自博客园,作者:老韩头的开发日常,转载请注明原文链接:https://www.cnblogs.com/xushuotec/p/14484140.html