在书写业务逻辑代码时,常会需要在不同权限上下文中操作动作,比如使用管理员权限绕过权限检查。下面我们来看一下普通用户如何使用 sudo()来修改公司电话号码。默认仅 Administration/Access Rights 用户组的用户可修改 res.company 记录。
from odoo import models, api
class ResCompany(models.Model):
_inherit = 'res.company'
@api.multi
def update_phone_number(self, new_number):
# 仅操作一条记录
self.ensure_one()
# 修改用户环境,self.sudo()会返回新上下文而非 self下的记录集,sudo()不加参就默认使用超级管理员,因而也就具备了管理员权限
company_as_superuser = self.sudo()
# 如需使用特定用户,传入在数据库中的用户id
# 如以下代码段可允许 public 用户搜索可见书籍
# public_user = self.env.ref('base.public_user')
# public_book = self.env['library.book'].sudo(public_user)
# 写入新电话号
company_as_superuser.phone = new_number
注意:使用 sudo()时的操作是不可追踪的,所以使用 update_phone_number 后查看到的最后修改人仍是管理员,OCA的 base_suspend_security 可用于突破这一限制。
扩展知识
使用 sudo()不加参用户的上下文会变成 Odoo 超级管理员,该用户不受任何权限控制列表(acl)和记录集的权限规则限制。默认该用户有一个 company_id 字段设置为实例的主公司(ID 为1),这对于多公司的场景会存在问题:
小贴士:使用 sudo()时,反复确认调用 search()时不依赖标准记录集过滤结果,并确保在执行 create()时不使用当前用户字段 如 company_id 所计算的默认值。
使用 sudo()也会创建一个新的 Environment 实例,该环境初始带有一个空的记录集缓存 ,它与 self.env 的缓存是独立开来的。这可能会导致伪造数据查询,请避免在循环内创建新环境,并且越靠外层越好。
上下文是记录集环境的一部分,用于传递时区、用户界面语言、及动作中指定的上下文参数等信息。标准插件中的很多方法都使用上文来根据这些值来调整行为,有时需要变更记录集上下文来从方法调用获取预期结果或从可计算字段获取预期值。以下讨论在给定的 stock.location 中读取 product.product 的仓储级别。
以下会使用到 stock 和 product 两个 addon
from odoo import models, api
class ProductProduct(models.Model):
_inherit = 'product.product'
@api.model
def stock_in_location(self, location):
# 修改上下文
product_in_loc = self.with_context(
location=location.id,
active_test=False
)
# 搜索所有产品
all_products = product_in_loc.search([])
# 使用指定位置所有产品的产品名和仓储级别创建一个数组
stock_levels = []
for product in all_products:
if product.qty_available:
stock_levels.append((product.name,
product.qty_available))
return stock_levels
以上 self.with_context()传递了一些参数,它返回一个新的带有键值的 self 版本(product.product 记录集),两个键分别为:
扩展知识
也可为 self.with_context()传入字典,此时字典会覆盖原有环境成为新的上下文,上述对应代码可修改为
new_context = self.env.context.copy()
new_context.update({
'location': location.id,
'active_test': False})
product_in_loc = self.with_context(new_context)
同样的使用 with_context()会创建一个新的 Environment 实例,该环境初始带有一个空的记录集缓存 ,它与 self.env 的缓存是独立开来的。这可能会导致伪造数据查询,请避免在循环内创建新环境,并且越靠外层越好。
大多数情况下,可以使用 search()方法执行操作,但有时这并不够,比如使用域的句法无法达到要求,或者一些查询需要多次调用 search()而导致效率低下。
以下我们将使用原生 SQL 查询来读取按国家分组的 res.partner 记录。
from odoo import models, api
class ResPartner(models.Model):
_inherit = 'res.partner'
@api.model
def partners_by_country(self):
# 原生查询语句:使用了 id 字段和 country_id 外键(指向 res_country 表),array_agg 是 PostgreSQL 对 SQL的扩展,将信息放入数组
sql = ('SELECT country_id, array_agg(id) '
'FROM res_partner '
'WHERE active=true AND country_id IS NOT NULL '
'GROUP BY country_id')
# 执行语句
self.env.cr.execute(sql)
# 遍历查询结果来生成结果集字典
country_model = self.env['res_country']
result = {
}
for country_id, partner_ids in self.env.cr.fetchall():
country = country_model.browse(country_id)
partners = self.search(
[('id', 'in', tuple(partner_ids))]
)
result[country] = partners
return result
扩展知识
self.env.cr 是 包裹psycopg2游标(cursor)的装饰器,以下是常用的一些方法:
处理原生 SQL 查询时应注意:
我们在第五篇中曾介绍过 models.TransientModel 基类,该类与很多常规类相似,不同之处在于会在数据库中定期清理,所以才会被称为临时模型。一般用于创建向导或对话框,由用户在界面中填写然后再向数据库的持久记录操作。
下面我们向之前的模块添加记录借书的向导
from odoo import models, api, fields
class LibraryBookLoan(models.Model):
_name = 'library.book.loan'
book_id = fields.Many2one('library.book', 'Book', required=True)
member_id = fields.Many2one('library.member', 'Borrower',
required=True)
state = fields.Selection([('ongoing', 'Ongoing'),
('done', 'Done')],
'State',
default='ongoing', require=True)
class LibraryLoanWizard(models.TransientModel):
# 创建临时模型: Model 和 TransientModel 的公有基类是 BaseModel,Odoo 源码中99%都使用 BaseModel
_name = 'library.loan.wizard'
# 以下两个字段分别存储借书人和所借的书
member_id = fields.Many2one('library.member', string='Member')
book_ids = fields.Many2many('library.book', string='Books')
# 添加方法在临时模型上执行操作
@api.multi
def record_loans(self):
loan = self.env['library.book.loan']
for wizard in self:
member = wizard.member_id
books = wizard.book_ids
for book in books:
loan.create({
'member_id': member.id,
'book_id': book.id})
然后在 xml 添加对应的菜单设置即可。TransientModel 的不同之处在于
xml 文件中 button 类型设置为 object 表明在点击按钮时会调用name 属性所赋值的方法,操作中的target=’new’会在当前表单之上显示一个对话框。
<act_window id="action_wizard_loan_books"
name="Record Loans"
res_model="library.loan.wizard"
view_mode="form"
target="new"
/>
<menuitem id="menu_wizard_loan_books"
parent="library_book_menu"
action="action_wizard_loan_books"
sequence="20"
/>
以下可用于增强向导的功能:
使用上下文计算默认值
以上的向导要求用户填写姓名,web 客户端的特性可以让用户少打一些字,在操作执行时,上下文可以更新一些值给向导使用
Key | Value |
---|---|
active_model | 与操作相关的模型,通常是屏幕上显示的模型 |
active_id | 表明单条记录处于活跃状态,提供出该记录的 ID |
active_ids | 在选择多条记录时,则会是一个 ID 列表(树状视图中选择多条),在表单视图中得到[active_id] |
active_domain | 向导所操作的额外的域 |
这些值可用于计算模型的默认值,甚至直接通过按钮给方法调用。假如在 library.member 模型的表单视图中有一个按钮启动向导,向导创建的上下文中会包含{‘active_model’:’library.member’, ‘active_id’:},这时可以使用如下方法定义 member_id 字段来计算默认值
def _default_member(self):
if self.context.get('active_model') == 'library_member':
return self.context.get('active_id', False)
向导和代码复用
在方法中我们可以将 for wizard 设为自循环,假设 len(self)为1,可以在方法最前面调用 self.ensure_one()
@api.multi
def record_borrows(self):
self.ensure_one()
member = self.member_id
books = self.book_ids
loan = self.env['library.book.loan']
for book in books:
loan.create({
'member_id': member.id, 'book_id': book.id})
推荐使用这段代码,因为这样可以通过为向导创建记录在其它部分的代码中复用这个向导,放到一个单独的记录集中然后调用记录集中的 record_loans()。确实在此处代码有些琐碎并且无需通过所有的不同成员借用某些书的循环。但在 Odoo 实例中,有些操作更为复杂,通常有向导来做“对的事”会比较好。使用这类向导时,确保查看上下文中任何使用 active_model/active_id/active_ids 键的源代三,这时应传入自定义的上下文。