Odoo 14 开发者指南第八章 高级服务端开发技巧 | Alan Hou 的个人博客


本文由 简悦 SimpRead 转码, 原文地址 alanhou.org

全书完整目录请见:Odoo 14 开发者指南(Cookbook)第四版

第五章 基本服务端开发中,我们学习了如何在模型类中编写方法、如何扩展所继承模型的方法以及如何处理记录集。本章将讲解更为高级的话题,比如使用记录集的环境、对按钮点击调用方法和操作 onchange 方法。本章中的各小节有助于处理更复杂的业务问题。我们将学习如何创建交互性更强的应用。

本章中,我们将讲解如下内容:

  • 更改执行动作的用户
  • 使用变更的上下文调用方法
  • 执行原生 SQL 查询
  • 编写向导来引导用户
  • 定义 onchange 方法
  • 在服务端调用 onchange 方法
  • 借助 compute 方法调用 onchange
  • 定义基于 SQL 视图的模型
  • 添加自定义设置选项
  • 实现 init 钩子

技术准备

本章的技术要求包含 Odoo 在线平台。

本章中所使用的所有代码可通过 GitHub 仓库进行下载:https://github.com/alanhou/odoo14-cookbook/tree/main/Chapter08

更改执行动作的用户

在编写业务逻辑代码时,可能要通过不同的权限上下文来执行一些动作。典型的用例是跳过权限检查的以及超级用户的权限执行一个动作。这类需求源自有些用户需要操作自己不具有权限的记录。

本节将向展示如何让普通用户通过使用 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')],

                            'State', default='ongoing',

                            required=True)

  rent_date = fields.Date(default=fields.Date.today)

  return_date = fields.Date()

需要添加一个表单视图、一个动作以及一个通过用户界面查看新模型的菜单项。还需要为 librarian 添加一个权限规则,这样他们可以发布供出借的图书。如果不了解如保添加,请参见第三章 创建 Odoo 插件模块

同时,也可以使用 GitHub 上已经编写好的初始模块代码示例来节约一点时间。该模块位于 Chapter08/00_initial_module 文件夹下。GitHub 代码示例请参见 https://github.com/alanhou/odoo14-cookbook/tree/main/Chapter08/00_initial_module。

如何实现…

如果你测试过该模型,会发现只有拥有访问权限的图书管理员才能将图书标记为已借阅。非管理员用户无法自行借阅图书,他们需要求助图书管理员才能借书。假设我们需要为非管理员用户添加一个自行为自己借书的功能。我们将不为这些用户添加 library.book.rent 模型的访问权限并实现这一功能。

需要执行如下步骤来让普通用户可以借书:

  1. 在 library.book 模型中添加 book_rent() 方法:

    class LibraryBook(models.Model):
    
      _name = 'library.book'
    
      ...
    
      def book_rent(self):
    
  2. 在该方法中,确保我们是对单条记录进行操作:

  3. 如果图书不可借阅则抛出一个警告(要确保在文件头部导入 UserError):

    if self.state != 'available':
    
      raise UserError(_('Book is not available for renting'))
    
  4. 使用超级用户来获取 library.book.rent 的空记录集:

    rent_as_superuser = self.env['library.book.rent'].sudo(
    
  5. 通过相应的值新建一条图书借阅记录:

    rent_as_superuser.create({
    
      'book_id': self.id,
    
      'borrower_id': self.env.user.partner_id.id,
    
    })
    
  6. 在图书的表单视图中添加按钮来通过用户界面调用这一方法:

重启服务并更新 my_library 来应用这些修改。更新后,我们在图书的表单视图中会看到一个 Rent this book 按钮。点击按钮会新建一条借阅记录。非管理员用户可进行同样的操作。我们可以使用 demo 用户访问 Odoo 进行测试。

invalid image (图片无法加载)

运行原理…

前 3 步中,我们新增了一个方法 book_rent()。该方法会在用户在图书表单视图中点击 Rent this book 按钮时进行调用。

在第 4 步中,我们使用了 sudo()。该方法返回一条带有修改后环境的新记录集,其中的用户具有超级用户权限。使用 sudo() 调用记录集时,环境变量中会将 environment 属性修改为 su,表示为环境的超级用户状态。可通过 recordset.env.su 访问这一状态。所有通过这一 sudo 记录集的方法调用都具有超级用户权限。要更好地了解这点,从方法中删除. sudo() 然后再点击 Rent this book 按钮。它会抛出 Access Error 并且用户无法再访问该模型。简言之,sudo() 可以跳过所有权限规则。

如果需要一个具体的用户,可以传递一个包含该用户或用户数据库 id 的记录集。如下:

public_user = self.env.ref('base.public_user')

public_book = self.env['library.book'].sudo(public_user)

public_book.search([('name', 'ilike', 'cookbook')])

这段代码让我们可以使用公共用户搜索可见的图书。

📝在使用 sudo() 时,对由谁创建或更新记录的动作都不进行跟踪记录。本节中公司的最后一次修改人为 Administrator,而非最初调用 create 的用户。

位于 https://github.com/OCA/server-backend / 的社区插件 base_suspend_security 可用于处理这一问题。

扩展知识…

使用 sudo() 时,我们跳过了访问权限及权限访问规则。有时可访问独立存在的多条记录,比如多公司环境中不同公司的记录。sudo() 记录集跳过 Odoo 的所有权限规则。

如果你不小心,这一环境中搜索的记录会与数据库中存在的任意公司相关联,这表示可能会向真实用户泄漏信息,更糟糕的是可能通过将记录关联到不同的公司而默默地致使数据库的损坏。

**📝重要:**使用 sudo() 时,应反复确认 search() 的调用不依赖于标准记录规则来过滤结果,

其它内容

参考如下内容获取更多信息:

使用变更的上下文调用方法

上下文是记录集环境的一部分。它用于从用户界面传递用户的时区和语言等信息,以及动作中所指定的上下文参数。Odoo 标准插件中的很多方法使用该上下文来根据这些值适配业务逻辑。有时会需要修改记录集的上下文来从方法调用获取所需要的结果或计算字段的所需结果。

本节将展示如何借助于环境上下文从改变的方法的行为。

准备工作

本节中我们将使用前面小节的 my_library 模块。在 library.book.rent 模型的表单视图中,我们将添加一个按钮来将图书标记为遗失,以备普通用户遗失图书时使用。注意我们在图书的表单视图中已经有相同的按钮了,但此处我们将使用稍稍不同的行为在理解 Odoo 中上下文的使用。

如何实现…

我们需要执行如下步骤来添加按钮:

  1. 更新 state 字段的定义来添加一个遗失状态:

    state = fields.Selection([('ongoing', 'Ongoing'),
    
                            ('returned', 'Returned'),
    
                            ('lost', 'Lost')],
    
                            'State', default='ongoing',
    
                            required=True)
    
  2. 在 library.book.rent 的表单视图中添加一个 Mark as lost 按钮:

  3. 在 library.book.rent 模型中添加 book_lost() 方法:

  4. 在这个方法中,确保我们操作的是单条房记录,然后修改其状态:

    self.ensure_one()
    
    self.state = 'lost'
    
  5. 在方法中添加如下代码来修改环境的上下文并调用方法来改变图书的状态为遗失:

    book_with_different_context =
    
    self.book_id.with_context(avoid_deactivate=True)
    
    book_with_different_context.make_lost()
    
  6. 更新 library.book 模型的 make_lost() 方法来产生不同的行为:

    def make_lost(self):
    
      self.ensure_one()
    
      self.state = 'lost'
    
      if not self.env.context.get('avoid_deactivate'):
    
        self.active = False
    

运行原理…

第 1 步中我们为图书添加了一种新状态。这一状态表示图书遗失。

第 2 步中,我们新增了一个按钮 Mark as lost。用户可使用该按钮报告图书遗失。

第 3 和第 4 步中,我们添加了在点击 Mark as lost 按钮时调用的方法。

第 5 步使用一些关键词参数调用 self.book_id.with_context()。它返回一个带有更新了上下文的新版本 book_id 记录集。这里我们只添加了一个键,avoid_deactivate=True,但是可以添加多个键。我们还使用了 sudo() 来让非图书管理员可以将图书报告为遗失。

第 6 步中,我们查看了该上下文中 avoid_deactivate 键是否为真值。这样避免了取消图书,即便是遗失了图书管理员仍然可以看到它们。

现在图书管理员在图书表单视图中报告为遗失时,图书记录的状态会变为 lost,书会被存档。但普通用户在租借记录中报告图书为遗失时,记录状态会变为 lost,图书不会存档,这样管理员可以稍后查看。

这仅仅是有关变更上下文的一个简单示例,读者可以基于自己的要求在 ORM(对象关系映射)中的任何地方使用它。

扩展知识…

还可以将字典传递给 with_context()。此时,该字典用作新的上下文,它会覆盖当前上下文。因此第 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()

使用 with_context() 包含创建一个新的环境实例。该环境会有一个初始空记录集缓存,它独立于 self.env 缓存进行增长。这会导致不必要的数据库查询。这种情况下,应避免在循环内新建环境并将这些环境的创建移到尽可能外面的层。

其它内容

参见如下小节来了解 Odoo 中更多有关上下文的知识:

执行原生 SQL 查询

大多数时候,可以通过 Odoo 的 ORM 执行所需操作。例如,使用 search() 方法来获取记录。但是有时我们的所需不止于此,抑或是无法使用 domain 语法(有些操作非常烧脑甚至是完全不可能)来进行表达,又或是查询需要对 search() 进行多次调用,进而导致低效。

本节将展示如何使用原生 SQL 查询来获取用户借阅某本书天数的平均值。

准备工作

本节中我们将使用前面小节的 my_library 模块。为进行简化,我们只会在日志中打印结果,但在真实场景中,会需要在业务逻辑中使用查询结果。在第九章 后端视图中,我们将在用户界面中展示查询的结果。

如何实现…

需要执行如下步骤来获取用户保留某本书的平均天数:

  1. 在 library.book 中添加 average_book_occupation() 方法:

    def average_book_occupation(self):
    
      ...
    
  2. 在方法中添加如下代码推送所有待更新内容:

  3. 在该方法中,编写如下 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;"""
    
  4. 执行该查询:

    self.env.cr.execute(sql_query)
    
  5. 获取结果并进行日志记录(注意应导入 logger):

    result = self.env.cr.fetchall()
    
    logger.info("Average book occupation: %s", result)
    
  6. 在 library.book 模型的表单视图中添加一个按钮来触发我们的方法:

别忘了在这个文件中导入 logger。然后重启并更新 my_library 模块。

运行原理…

第 1 步中,我们添加了 average_book_occupation() 方法,在用户点击 Log Average Occ. 按钮时会进行调用。

在第 2 步中,我们使用 flush() 方法。Odoo v13 开始 ORM 中大量使用了缓存。ORM 对每个事务使用一个全局缓存。这样数据库中的记录与 ORM 缓存中的记录数据可能会不同。在执行查询前使用 flush() 方法可以确保缓存中的所有修改被推送到数据库中。

第 3 步中,我们声明了一条 SELECT 查询 SQL。这会返回用户持有某本书的平均天数。如果在 PostgreSQL 命令行运行这条查询,根据数据库中的图书数据会得到类似下面的结果:

+---------------------------------------+-------+

| name | avg |

|---------------------------------------+-------|

| Odoo 12 Development Cookbook          | 33    |

| PostgreSQL 10 Administration Cookbook | 81    |

+---------------------------------------+-------+

第 4 步对存储在 self.env.cr 中的数据库游标调用 execute(方法。这会发送查询到 PostgreSQL 并进行执行。

第 2 步为点击第 3 步中所定义按钮调用的向导类添加了代码。这段代码从向导中读取值并为每本书创建 library.book.rent 记录。

第 3 步为我们的向导定义了一个视图,参见第九章 后端视图中_定义文档样式表单_一节获取更多详情。这里的重点是 footer 中的按钮,type 属性设置为了 object,表示在用户点击该按钮时,会调用按钮 name 属性所指定的同名方法。

第 4 步保障我们在应用的菜单中有一个向导的入口。我们在动作中使用了 target=’new’ ,这样该表单视图会在当前表单之上以对话框形式展示。参见第九章 后端视图中的_添加菜单项和窗口动作_一节来了解详情。

第 5 步中我们为 library.rent.wizard 模型添加了访问权限。这们图书管理员就具备了 library.rent.wizard 模型的所有操作权限。

📝注:Odoo v14 之前,TransientModel 无需配置权限规则。所有人都可以创建记录,且仅能访问自己创建的记录。而在 TransientModel 中 TransientModel 强制要求设置访问权限。

扩展知识…

下面是一些优化向导的贴士:

使用上下文来计算默认值

我们所展示的向导要求用户在表单中输入会员名。在网页客户端中有一个功能可用于快速输入。在执行动作时,上下文中会更新一些值,并可由向导所使用:

active_model这是与动作相关联的模型名。通常为在屏幕上所展示的模型。
active_id这表明单条记录是活跃的并且提供了该记录的 ID。
active_ids如果选择了多条记录,这将是带有 ID 的列表。这在树状视图中选择多项并触发动作时产生。在表单视图中,所获取的为 [active_id]。
active_domain这是向导将要操作的其它域。

这些值可用于计算模型的默认值,甚或直接在按钮的调用方法中进行计算。我们来改进一下本节中的示例,如果 res.partner 模型的表单视图中展示了一个启动向导的按钮,向导创建的上下文中会包含 {‘active_model’: ‘res.partner’, ‘active_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() 会确保 self 中的记录条数为 1。如果 self 中的记录条数多于 1 会抛出错误。

我们推荐使用这一小节中的代码,因为这让我们可以通过对向导创建记录复用代码中其它部分的向导,将它们放在单个记录集中(参见第五章 基本服务端开发中的_合并记录集_一节了解如何实现),然后对记录集调用 add_book_rents()。这里的代码很简单,无需执行完所有循环来记录某些书由不同成员借阅。但是在 Odoo 实例中,有些操作非常的复杂,带有可以实现正确操作的向导会非常的好。在使用这些向导时,务必检查源代码中是否使用了上下文中的 active_model/active_id/active_ids keys。如果使用了,需要传递一个自定义上下文(参见_使用变更的上下文调用方法_一节)。

重定向用户

第 2 步中的方法没有任何返回。这会导致向导对话框在执行完动作后关闭。另一种可能是让方法返回带有 ir.action 中字段的字典。此时,网页客户端会像用户点击了菜单入口那样处理该动作。BaseModel 类中定义的 get_formview_action() 方法可用于这一实现。例如,如果我们想要展示刚刚借阅书籍的会员的表单视图,可以编写如下代码:

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

      })

  members = self.mapped('borrower_id')

  action = members.get_formview_action()

  if len(borrowers.ids) > 1:

    action['domain'] = [('id', 'in', tuple(members.ids))]

    action['view_mode'] = 'tree,form'

  return action

这会通过该向导创建一个借阅了图书的成员列表(实例情况中,在用户界面中调用该向导时仅会有一个成员)并创建一个动态的动作,来使用指定的 ID 展示成员。

重定向用户技术可用于创建包含多个逐一执行步骤的向导。向导中的每一步可使用前面小步中的值。通过提供 Next 按钮来调用向导中定义了更新向导某些字段的方法,并返回在相同更新后向导中重新展示的动作以及准备好下一个步骤。

准备工作

本节中我们将使用_编写向导来引导用户_一节中的 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):

    loanModel = self.env['library.book.rent']

    for rec in self:

      loans = loanModel.search(

        [('state', '=', 'ongoing'),

        ('book_id', 'in', rec.book_ids.ids),

        ('borrower_id', '=', rec.borrower_id.id)]

      )

      for loan in loans:

        loan.book_return()

最后,需要为向导定义视图、动作和菜单入口。这些部分读者可作为练习自行添加。

如何实现…

在用户修改时自动跳出要归还的书籍,我们需要在 LibraryReturnsWizard 中添加 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 装饰器,传递变化的字段名,因而会触发该方法的调用。本例中,我们告诉用户界面在 borrower_id 发生变更时应调用该方法。

在方法体中,我们搜索当前由指定成员借阅的图书,并且我们使用属性赋值来更新向导中的 book_ids 属性。

ℹ️@api.onchange 装饰器处理发往网页客户端的视图变更来为字段添加一个 on_change 属性。这在老 API 中为手动操作。

扩展知识…

onchange 的基础用法是在用户界面中其它字段发生变化时为字段计算新值,本节中我们也看到了。

在方法体中,我们获取对记录当前视图中展示的字段的访问,但不一定需要模型的所有字段。这是因为可在用户界面中创建记录并还未存储到数据库中时调用。在 onchange 方法内,self 处于一种特殊状态,表现为 self.id 其实并不是整型,而是 odoo.models.NewId 的实例。因此我们不应在 onchange 方法内对数据库进行任何修改,原因在于用户可能会取消记录的创建,而在编辑的过程中并不会对 onchange 方法所做的修改进行回滚。可以使用 self.env.in_onchange() 和 self.env.in_draft() 对此进行查看,前者在当前执行上下文为 onchange 方法时返回 True,后者在 self 还未在数据库中提交时返回 True

此外,onchange 方法可以返回一个 Python 字典。字典中可以有如下的键:

  • warning:值应为一个字典,其中有 title 和 message 键,分别包含着对话框的标题和内容,在 onchange 方法运行时会进行展示。这在发生不连续或潜在问题时用于引起用户的注意。
  • domain:值应为将字段名映射到域的字典。用于在想要根据另一个字段的值修改 One2many 字段的域时。

例如,假定我们在 library.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 = rentModel.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 仅在所依赖操作通过 Odoo 用户界面执行时自动调用。但在一些情况下,调用 onchange 方法非常的重要,因为它在所创建或更新记录中更新了某些重要字段。当然, 你可以自己完成所需计算,但有时可能做不到,因为 onchange 可由你不知道的安装在实例中的第三方插件所添加或修改。

本节讲解如何通过在创建记录前手动触发 onchange 方法对记录的 onchange 方法进行调用。

准备工作

在_更改执行动作的用户_一节中,我们添加了一个 Rent this book 按钮来让非图书管理员用户来自己借阅图书。现在我们需要对归还图书做相同操作,但不是编写归还图书的逻辑,而是仅仅使用_定义 onchange 方法_一节中所创建的图书归还向导。

如何实现…

本节中,我们将手动创建 library.return.wizard 模型的一条记录。我们希望 onchange 方法来为我们计算归还的图书。需要执行如下步骤来进行实现:

  1. 在 library_book.py 文件中导入 tests 工具集中的 Form:

    from odoo.tests.common import Form
    
  2. 在 library.book 模型中创建 return_all_books 方法:

    def return_all_books(self):
    
      self.ensure_one()
    
  3. 为 library.return.wizard 获取一个空记录集:

    wizard = self.env['library.return.wizard']
    
  4. 创建向导 Form 代码块如下:

    with Form(wizard) as return_form:
    
  5. 通过赋值借阅者触发 onchange,然后归还图书:

            return_form.borrower_id = self.env.user.partner_id
    
            record = return_form.save()
    
            record.books_returns()
    

运行原理…

有关第 1 至 3 步的讲解,参见第五章 基本服务端开发中_新建记录_一节。

第 4 步创建了一个虚拟表单来处理 onchange 规范,就像 GUI 那样。

第 5 步中包含归还所有图书的完整逻辑。第一行中我们对向导中的 borrower_id 赋值。这会触发 library.return.wizard 模型中的 onchange 方法(更多有关 onchange 方法的知识,参见前面小节中的 onchange 方法定义)。然后我们调用了表单中的 save() 方法,它返回一条向导记录。之后调用了 books_returns() 方法来执行归还所有书籍的逻辑。

模型的 onchange(values, field_name, field_onchange) 方法,有三个参数:

  • values:我们要对记录设置的值的列表。你需要为所有需由 onchange 方法修改的字段提供值。本节中,基于这一原因我们将 book_ids 设置为 False。
  • field_name:我们希望触发 onchange 方法的字段列表。你可以传递一个空列表,ORM 会使用值中所定义的字段。但是,我们会经常想要手动指定这一列表来在不同的字段更新相同的字段时手动控制运行的顺序。
  • field_onchange:第 4 步中所计算的 onchange 详情。这一方法查找应调用哪些 onchange 方法,以及调用的顺序,它返回一个字典,其中包含如下键:
    • value:这是一个新计算字段值的字典。这个字典仅包含传递给 onchange() 的 values 参数的键。注意 Many2one 字段会映射到包含 (id, display_name) 的元组来进行对网页客户端的优化。
    • warning:这是一个包含在网页客户端展示给客户的警告信息字典。
    • domain:这是一个映射字段名到新有效域的字典。

onchange 方法大多在用户界面中调用。但本节中,我们学习了如何在服务端使用 / 触发 onchange 的业务逻辑。这样,可以在创建记录时不跳过任何业务逻辑。

其它内容

如果想要了解创建和更新记录的更多知识,参见第五章 基本服务端开发的_新建记录_和_更新记录集中记录值_小节。

借助 compute 方法调用 onchange

在上两小节中,我们学习了如何定义及调用 onchange 方法。我们还了解到了其局限性,即仅能从用户界面中自动调用。为解决这一问题,Odoo v13 中引入了一种全新定义 onchange 行为的方式。本节中,我们就来学习如何使用 compute 方法来产生像 onchange 方法那样的行为。

准备工作

本节我们使用前一小节中的 my_library 模块。我们会将 library.return.wizard 中的 onchange 方法替换为 compute 方法。

如何实现…

按如下步骤使用 compute 方法来修改 onchange 方法:

  1. 使用 compute 替换 onchange_member() 方法中的 api.onchange 如下:

    @api.depends('borrower_id')
    
    def onchange_member(self):
    
        ...
    
  2. 在字段的定义中添加 compute 参数如下:

    book_ids = fields.Many2many('library.book',
    
       string='Books',
    
       compute="onchange_member",
    
       readonly=False)
    

升级 my_library 模块应用代码,然后测试归还图书向导来查看变化。

运行原理…

在功能上,我们计算的 onchange 与普通的 onchange 方法相似。唯一的不同在于现在 onchange 在后台修改时也会触发。

第 1 步中,我们使用 @api.compute 替换了 @api.onchange。这要求在字段值变化时重新计算方法。

第 2 步中,我们通过字段注册了 compute 方法。细心的你会发现在定义 compute 属性的同时使用了 readonly=False。默认 compute 方法为只读,但通过设置 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()

这段代码无需使用 Form 类即可返回用户所借出的书籍。使用常规的 onchange 方法,需要创建 Form 对象,但使用计算的 onchange,则无需再创建 Form 对象。在记录创建时会调用相应的 onchange 方法。

其它内容

参见第四章 应用模型中的_向模型添加计算字段_一节学习更多有关计算字段的知识。

基于 SQL 视图定义模型

在进行插件模块的设计时,我们对类中的数据建模由 Odoo 的 ORM 映射到数据表中。我们应用了一些知名的设计原则,比如关注点分离 (separation of concerns) 和数据归一化(data normalization)。但在模块设计的后续阶段,它可用于为同一张表中对多个模型进行数据聚合,顺便对它们执行一些运算,尤其是报告或生成仪表盘。要进行简化,以及利用 Odoo 中 PostgreSQL 数据库引擎底层的强大之处,可以定义基于 PostgreSQL 视图的只读模型而非数据表。

本节中,我们将复用本章中_编写向导来引导用户_一节的租借模型,并且我们会新建一个模型来让收集图书及作者数据更为容易。

准备工作

本节中,我们将使用前一节中的 my_library 模块。我们会创建一个名为 library.book.rent.statistics 的新模型来保留统计数据。

如何实现…

按照如下步骤创建基于 PostgreSQL 视图的新模型:

  1. 新建一个模型并将_auto 类属性设置为 False:

    class LibraryBookRentStatistics(models.Model):
    
      _name = 'library.book.rent.statistics'
    
      _auto = False
    
  2. 声明想要在该模型中看到的字段,将它们设置为 readonly:

    book_id = fields.Many2one('library.book', 'Book', readonly=True)
    
    rent_count = fields.Integer(string="Times borrowed", readonly=True)
    
    average_occupation = fields.Integer(string="Average Occupation (DAYS)", readonly=True)
    
  3. 定义 init() 方法来创建视图:

    @api.model_cr
    
    def init(self):
    
      tools.drop_view_if_exists(self.env.cr, self._table)
    
      query = """
    
      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
    
      );
    
      """
    
      self.env.cr.execute(query)
    
  4. 现在可以为新模型定义视图了。透视表视图对于挖掘数据尤为有用(参见第九章 后端视图)。

  5. 不要忘记为新模型定义一些权限规则(参见第十章 权限安全)。

invalid image (图片无法加载)

运行原理…

通常,Odoo 会为通过为数据列使用字段定义的模型新建一张表。这是因为在 BaseModel 类中,_auto 属性默认为 True。在第 1 步中,通常将这个类属性设为 False,我们告诉 Odoo 我们自己管理它。

第 2 步中,我们定义将由 Odoo 使用的一些字段来生成数据表。将它们标记为 readonly=True,这样视图不会启用无法保存的修改,因为 PostgreSQL 的视图是只读的。

第 3 步中定义了 init() 方法。这个方法通常什么也不做,它在_auto_init() 之后调用(在_auto = True 时负责数据表的创建,否则什么也不做),并且我们使用它来创建一个新的 SQL 视图(或在模块升级时更新已有视图)。视图创建查询必须创建一个包含匹配 Model 字段名的字段名的视图。

**📝重要贴士:**这是一个常见错误,这种情况下,忘记在视图定义查询中重命名该字段,会在 Odoo 无法查找到该字段时产生错误信息。

注意我们还需要加入一个名为 ID 包含唯一值的整型列。

扩展知识…

还可以在这种模型中包含一些计算和关联字段。唯一的限制是这些字段不能被存储(因此不能使用它们来对记录分组或进行搜索)。但是在前例中,我们可通过添加一个字段来让图书可编辑,定义如下:

publisher_id = fields.Many2one('res.partner', related='book_id.publisher_id', readonly=True)

如果需要使用 publisher 分组,需要通过在视图定义中添加该字段进行存储,而不是使用关联字段。

添加自定义设置选项

在 Odoo 中,可以通过 Settings 选项设置可选功能。用户可以随时启用或禁用该选项。本节中我们将描述如何创建设置选项。

准备工作

在前面的小节中,我们添加了一些按钮,这样非图书管理员的用户可以借阅及归还图书。并非每个图书馆都是如此,因此我们会创建设置选项来启用及禁用这一功能。我们将通过隐藏这些按钮来实现。本节中,我们会使用前面小节中相同的 my_library 模块。

如何实现…

按照如下步骤来创建自定义设置选项:

  1. 在 my_library/security/groups.xml 文件中添加一个新分组:

     Self borrow 
    
  2. 通过继承 res.config.settings 模型来添加新字段:

    class ResConfigSettings(models.TransientModel):
    
      _inherit = 'res.config.settings'
    
      group_self_borrow = fields.Boolean(string="Self borrow", implied_group='my_library.group_self_borrow')
    
  3. 通过 xpath 在已有的 settings 视图中添加这一字段(更多详情,请参见第九章 后端视图):

     res.config.settings.view.form.inherit.library res.config.settings    Library
    -------
    
     
    
     
    
     
    
     
    
      
    
     Allow users to borrow and return books by themself 
    
  4. 为 Settings 添加一个菜单及一些动作:

     Settings ir.actions.act_window res.config.settings  form inline {'module' : 'my_library'}  
    
  5. 修改图书表单视图中的按钮并添加一个 my_library.group_self_borrow 分组:

重启服务并更新 my_library 模型来应用修改。

invalid image (图片无法加载)

扩展知识…

还有一些其它方式来管理设置选项。其中之一是分离功能到新模块中并通过选项安装或卸载这些模块实现。需要使用以 module_为前缀加模块名的名称来添加一个布尔字段进行实现。例如,我们新建一个名为 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 属性将保障用户数据存储在系统参数中,位于 Settings > Technical > Parameters > System Parameters 菜单下。这一数据存储在 digest.default_digest_emails 键下。

设置选项用于让应用更通用。这些选项给你取用户充分自由,让他们可以随时启用或禁用一些功能。在将功能转化为选项时,可以使用同一个模块服务多个客户,并且客户可以在想要的时候启用该功能。

实现 init 钩子

第六章 管理模块数据中,我们学习了如何通过 XML 或 CSV 文件添加、更新及删除记录。但有时,业务用例非常复杂,无法通过数据文件进行解决。这类情况下,可以在声明文件中使用 init 钩子来执行所需要的操作。

准备工作

我们将使用前一节中相同的 my_library 模块。本节中为进行简化,仅通过 post_init_hook 来创建一些图书记录。

如何实现…

按照如下步骤来添加 post_init_hook:

  1. 在__manifest__.py 文件中通过 post_init_hook 键来注册这个钩子:

    ...
    
    'post_init_hook': 'add_book_hook',
    
    ...
    
  2. 在__init__.py 文件中添加 add_book_hook() 方法:

    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])
    

运行原理…

在第一步中,我们在声明文件文件中通过 add_book_hook 注册了 post_init_hook。这表示在模块安装之后,Odoo 会在__init__.py 中查找 add_book_hook 方法。如果找到,它会使用数据库游标和 registry 调用该方法。

第 2 步中,我们声明了 add_book_hook() 方法,在模块安装后会被调用。我们通过该方法创建了两条记录。在实际场景中,可以在此处编写复杂的业务逻辑。

本例中,我们学习了 post_init_hook,但 Odoo 还支持另外两种钩子:

  • pre_init_hook:这个钩子会在开始安装模块时调用。它与 post_init_hook 正好相反,会在当前模块安装前触发。
  • uninstall_hook:这个钩子会在你卸载该模块时调用。它多用于模块需要有垃圾回收机制时。

幻翼 2021年11月23日 10:44 收藏文档