Odoo入门(七)——  向导视图的创立

ORM应用逻辑-业务处理

  • 前面的章节我们学习了利于Odoo的视图来构建用户前端界面。本章介绍Odoo的后台业务逻辑实现。

创建一个向导

  • 假设有这么一个需求:To-Do应用中,用户需要设置一系列任务的截止时间跟负责人。如果单独的打开每条任务记录去修改那是十分麻烦的。我们就需要使用一个向导表单来选择需要更改的任务记录,再进行统一操作。

  • 向导表单可以理解为获取用户输入,然后再对这些输入进行储存,以便下一步应用到Odoo模型记录中。 任务向导表单

  • 我们来新建一个名为 todo_wizard 的模块用以演示。

    • 还是跟往常一样。创立todo_wizard/__manifest__.py文件添加如下代码来进行模块的描述.

      ```python
      {
          'name': 'To-do Tasks Management Assistant',
          'description': 'Mass edit your To-Do backlog.',
          'author': 'Xer',
          'depends': ['todo_user'],
          'data': ['views/todo_wizard_view.xml'],
      }
      ```
      

别忘记创建todo_wizard/__init__.py.进行导包操作

from . import models
  • 接下来,我们来对向导的数据支持模型进行介绍.

向导模型

  • 一个向导展示了表单视图给用户。通常作为一个对话窗口,上面展示了在向导逻辑中需要展示的字段。
  • 这与我们定义普通模型十分相似,唯一不同的就是我们使用 models.TransientModel 来代替 models.Model 基类.
  • 临时模型是用来提高效率的,临时模型在数据库中也有对应的数据库表结构. 使用向导模型时,数据库中存储最新的使用数据.每次都会对原有的数据进行覆盖. 这样就不会产生多余的无效数据.
  • 创建 models/todo_wizard_model.py 文件,添加代码:
class TodoWizard(models.TransientModel):
    _name = 'todo.wizard'
    _description = 'To-do Mass Assignment'
    task_ids = fields.Many2many('todo.task', string='Tasks')
    new_deadline = fields.Date('Deadline to Set')
    new_user_id = fields.Many2one('res.users', string='Responsible to Set')

注:别忘记添加__init__.py.加入代码from . import todo_wizard_model

  • 在临时模型中,不要使用 one-to-many 关系型字段.原因 在与普通模型与临时模型创建 many-to-one 关系需要垃圾回收的支持。这一般都不被允许。

向导表单

  • 与普通视图类似,唯一不同是有两个特殊元素:

    • <footer> : 可以用来存放动作按钮
    • type="cancel" : 按钮中使用的属性,可以取消向导表单的数据展示。
  • 编辑视图xml文件。views/todo_wizard_view.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <odoo>
        <record id="To-do Task Wizard" model="ir.ui.view">
    	<field name="name">To-do Task Wizard</field>
    	<field name="model">todo.wizard</field>
    	<field name="arch" type="xml">
    	    <form>
    		<div class="oe_right">
    		    <button type="object" name="do_count_tasks"
    			    string="Count" />
    		    <button type="object" name="do_populate_tasks"
    			    string="Get All"/>
    		</div>
    		<field name="task_ids">
    		    <tree>
    			<field name="name"/>
    			<field name="user_id"/>
    			<field name="date_deadline"/>
    		    </tree>
    		</field>
    		<gropu>
    		    <group><field name="new_user_id"/></group>
    		    <group><field name="new_deadline"/></group>
    		</gropu>
    		<footer>
    		    <button type="object" name="do_mass_update"
    			    string="Mass Update" class="oe_highlight"
    			    attrs="{'invisible':[('new_deadline','=',False),
    			    ('new_user_id','=',False)]}"/>
    		    <button special="cancel" string="Cancel"/>
    		</footer>
    	    </form>
    	</field>
        </record>
        <!--more button action-->
        <act_window id="todo_app.action_todo_wizard"
    		name="To-Do Tasks Wizard"
    		src_model="todo.task" res_model="todo.wizard"
    		view_mode="form" target="new" multi="True"/>
    </odoo>
    

<act_window>这个动作把打开向导视图添加到todo.task表单视图中的More 按钮中.设置target="new"能够打开一个新的窗口. 另外在确定按钮Mass Update 设置了额外的属性,只有选择了新的截止日期或者新的任务负责人才会显示这个按钮。

向导的业务逻辑

  • 我们现在来实现定义在向导视图中的3个动作的逻辑。

  • 首先我们来解决Mass Update 按钮 在todo_wizard_model.py文件中定义do_mass_update函数。

        @api.multi
        def do_mass_update(self):
    	self.ensure_one()
    	if not (self.new_deadline or self.new_user_id):
    	    raise exceptions.ValidationError('No data to update!')
    	_logger.debug('Mass update on Todo Tasks %s', self.task_ids.ids)
    	vals = {}
    	if self.new_deadline:
    	    vals['date_deadline'] = self.new_deadline
    	if self.new_user_id:
    	    vals['user_id'] = self.new_user_id
    	if vals:
    	    self.task_ids.write(vals)
    	return True
    

我们的代码只需要对向导的一个实例进行处理,使用 self.ensure_one() 来保证是单例. 处理逻辑:

  • 对向导中的新截止日期个么新任务负责人取值.如果两者不存在,即没有设置,此时点击 Mass Update 返回一个类型错误.
  • 构造一个名为vals的字典,如果有新的设置,保存到字典,然后对向导中选择的所有任务进行字段更新. 注意到write方法可以对一组recordset进行写入操作.

日志

  • 我们在使用向导功能批量修改任务时可能会有误操作,这时候就需要使用日志文件来对操作进行记录.
  • Odoo中,我们直接使用了python的自带logging标准库来进行日志的操作.
    • 可执行的日志记录:

      import logging
      _logger = logging.getLogger(__name__)
      
      _logger.debug('A DEBUG message')
      _logger.info('An INFO message')
      _logger.warning('A WARNING message')
      _logger.error('An ERROR message')
      

异常处理

  • 当程序运行出错时,我们希望暂停它,然后打印出出错信息。通过抛异常来进行此类操作。 Odoo中定义了的异常类:
from odoo import exceptions
raise exceptions.Warning('Warning message')
raise exceptions.ValidationError('Not valid message')

Odoo中,我们可以使用抛出警告类来实现用户界面的弹出窗口。编辑Count按钮的逻辑

```python
    @api.multi
    def do_count_tasks(self):
	Task = self.env['todo.task']
	count = Task.search_count([('is_done', '=', False)])
	raise exceptions.Warning(
	    'There are %d active tasks.' %count
	)
```

向导视图中的帮助动作

  • 我们来编写一个 Get All 按钮。它的功能在于点击后能够选取所有active属性为true的任务。

  • 这里有个小细节,在对话窗口中,一个按钮上的动作执行成功后会关闭对话窗口。(可能会说,上一个 Count 按钮没有这个问题,那是因为我们使用抛出异常来传递了任务数,本质上这个动作没有成功执行,自然不存在关闭对话窗口这个问题)。

  • 我们通过重新打开向导视图来解决这个细节问题。来定义这个重新打开功能

        @api.multi
        def _reopen_form(self):
    	self.ensure_one()
    	return {
    	    'type': 'ir.actions.act_window',
    	    'res_model': self._name,
    	    'res_id': self.id,
    	    'view_type': 'form',
    	    'view_mode': 'form',
    	    'target': 'new'
    	}
    
  • 编写 Get All 按钮逻辑

    @api.multi
    def do_populate_tasks(self):
        self.ensure_one()
        Task = self.env['todo.task']
        open_tasks = Task.search([('is_done', '=', False)])
        self.task_ids = open_tasks
        return self._reopen_form()

使用ORM API

  • 接下来我们来深入Odoo中的ORM API 以便更好的操作模型。

装饰器

  • 在这么多章节里,我们经常能看到类似于 @api.multi. 这样的形式的装饰器。下面来对这些形式的装饰器进行解释
  • @api.multi :这个装饰器是Odoo10中新的API,用来处理recordsets(数据集合)。在这个装饰器中,self代表了一个数据集合。我们用它装饰的方法经常会首先对self进行遍历以获取每一个record。
  • @api.one : 这个装饰器是在Odoo9.0版本中添加的.目前版本中我们应该减少它的使用.通过**@api.multi** 装饰器,使用self.ensure_one()来替代.
  • @api.model : 这个装饰器装饰了一个静态类方法。它不涉及到任何recordset数据。虽然方法中的 self还是一个recordset,但是里面的内容是不相关的.被这个装饰器装饰的方法无法在按钮上注册使用.
  • @api.depends : 用于计算字段.
  • @api.constrains : 用以限制字段.
  • @api.onchange : onchange机制用来触发字段值的变动. onchage装饰器还能在用户界面返回一条警告信息。在return 中定义下面的代码即可:
return {
          'warning' : {
          'title' : 'Warning!',
           'message' : 'You have benn warned'}
}

重写ORM的默认方法

  • 比较常见的是我们重写 createwrite 方法.我们可以把我们的代码逻辑加入到这两个方法中,这样在记录被创建或者修改时就能执行这些代码逻辑.

  • 还是通过 TodoTask 来举例,我们可以对create方法进行扩展.

        @api.model
        def create(self, vals):
    	# Code before create: can use the `vals` dict
    	new_record = super(TodoTask, self).create(vals)
    	# Code after create: can use the `new_record` created
    	return new_record
    
  • 对write方法进行扩展:

    @api.model
    def write(self, vals):
        # Code before write: can use the `self` dict, with the old vals
        super(TodoTask, self).create(vals)
        # Code after write: can use `self`, with the updated vals
        return True
  • Todo task适用的一些常用的技术方法:
    • 使用计算字段来从一个基础字段中获得更进一步的信息
    • 使用default方法来动态展示字段默认值
    • 使用on change 来监控字段的改变
    • 使用constraints装饰器来限制字段。

RPC,网页前端相关的方法。

  • 下面这些方法一般可以用来作为特殊的视图动作的基础。

    • read([fields]) : 与 browse 方法相同,但是不是返回一个记录集合,而是一个以字段为参数的所有记录的列表。这个方法常用作于序列化数据。通常在客户端使用。
    • search_read([domain], [fields], offset=0, limit=None, order=None) :与read方法相似,添加了搜索功能。
    • load([fields], [data]) : 在从csv导入数据时使用,前一个fiedls参数代表要导入的字段名.
    • export_data([fiedls], raw_data=False) : 网页客户端上的导出功能.返回一个包含数据的字典. raw_data 参数允许导出为Python类型的数据值而不用转换为string.
  • 下面的方法常用来网页客户端,为用户界面的展示作为基础。

    • name_get() : 返回了一个(ID, name)元组格式的列表。这个方法用来定义作为用户界面展示的模型默认名字的字段。
    • name_search(name=' ‘, args=None, operator=‘ilike’,limit=100) : 同样返回了一个(ID, name)元组格式的列表。不过需要跟参数中的name值相同,可以看做上一个方法的搜索形式。
    • name_create(name) : 这个是为关联字段中为关联模型快速创建记录时设置一个记录名字.
    • default_get([fields]) : 当一个新纪录被创建时,返回一个包含字段默认值的字典。里面的默认值依赖当前用户或者当前会话中的上下文。
  • fiedls_get() : 描述当前模型的默认字段定义.能够通过开发者模式中的 View Fields 选项来查看.

  • fields_view_get() : 可以认为是获取视图显示模式。

Shell 命令

  • Odoo提供了一个交互式对话的命令行界面来让我们更好的理解它的ORM API。
  • 使用./odoo-bin shell -d todo来进入到我们的Odoo shell界面。
  • 进入shell界面后。我们发现会跟python解释器的shell一样,出现了 »> 这样的等待输入标识. 我们输入self.可以从返回的信息中得出现在的self 代表了我们是管理员用户.

shell界面

服务器环境

刚才的shell界面提供的self 关联了res.users这个用户模型.我们知道,在Odoo中self一般代表了一个记录集。而记录集通常携带着当前的上下文环境信息。可以通过self.env来获取当前的上下文环境.

self的env环境信息

  • 当前用户的环境信息拥有以下属性:
    • env.cr : 目前使用的数据库游标
    • env.uid : 当前会话的用户ID
    • env.user : 当前用户
    • env.context 当前会话的上下文字典数据. 我们还可以通过env从Odoo模型登记处的获取已经安装的Odoo模型。例如self.env['res.partner']返回了Partners模型.我们可以再使用searchbrowse方法来获取模型中的记录集。

通过env获取模型记录集

改变环境的执行状态

  • 环境是无法更改的,但是我们能够创立一个修改过的环境,然后通过切换环境执行相应的动作。
  • 改变环境可以使用的方法:
    • env.sudo(user) : 可以通过传入的用户名来切换当前的用户使用环境。如果user不设置,默认为切换到管理员用户环境。
  • env.with_context(dictionary) : 使用新的字典数据来替换原来的context.
  • env.with_context(key=value,…) : 使用新的键值对代替了当前context中已经存在的。
  • env.ref() 使用了外部id来获取对于的记录.举例:

ref获取记录

事务及底层SQL

  • 通常数据写入操作都由已经定义好的SQL事务来处理。在某些情况下,我们需要对数据库执行更加完善的控制,就可以使用self.env.cr.
    • self.env.cr.commit() : 提交事务缓存区中的执行命令
    • self.env.savepoint() : 设置一个回滚点
  • self.env.rollback() : 回滚数据库数据到回滚点.
  • 使用游标类cr的*execute()*方法,我们可以直接使用SQL语句对Odoo数据库进行操作. 注意点:在直接使用SQL语句时,不要直接写死SQL语句,而是通过python的字符串代替符号%s来进行值的传入.这样能有效防止SQL注入攻击.
  • 在*execute()方法中使用查询语句,这是就需要用fetchall()*来获取查询记录. *fetchall()*方法返回记录的元组列表,使用 dictfetchall() 可以返回字典列表.

fetchall使用

  • 使用改变数据库结构语句(DML)时,例如UPDATE , INSERT。需要对缓存进行更新。使用self.env.invalidate_all()方法执行.

使用记录集

下面我们来扩展ORM的常用方法的实现。首先使用shell 命令来交互式的进行记录集(recordsets)的讲解。

查询模型

  • 通过self.env我们能够获取到Odoo中已安装的模型. 得到模型后可以使用 search() 方法来对记录集进行搜索.
    • search() : 搜索方法需要传入一个domain表达式.如果传入 domain = [ ] .就会返回所有的记录.另外要注意, 如果使用了active字段, 只有active=True的记录会被获取.另外一些参数如下
      • order : 排序规则,通常使用字段名字排序
      • limit : 设置搜索获取的记录个数的最大值。
      • offset : 偏移量,可以与limit一起使用来获取返回记录集中的特定记录段。
  • 有时候我们只需要获取满足条件的记录的个数。这时候我们使用 search_count() 方法就可以。
  • browse() : 这个方法通过传入ID列表或者特定的ID值来获取与之对应的记录集合或者单条记录。 举例:

search跟browse方法

单例

  • 只有一条记录的记录集我们成为单例记录集。单例记录集可以直接使用.操作符来获取记录的字段值。例:

单例记录集直接获取name字段 记录集有个 ensure_one() 方法可以来确认当前记录集是否为单例,如果记录集里有多条记录,这个方法就会抛出异常

记录的写入操作。

  • 我们获取到记录集中的记录后可以直接对其字段属性进行修改。这些修改会直接被写入到数据库中。

修改记录字段值

  • 记录集同样有3个方法来对数据进行操作
  • create() : 可以直接通过字典数据创建一个新的记录.

create方法

  • unlink() : 删除记录

unlink方法

  • write() : 更新记录的字段

write方法

  • copy() :复制一个已有的记录。注意,字段有copy=False属性的话不会被复制

copy方法

使用时间跟日期

  • 由于历史遗留问题,ORM记录集使用string来处理datedatetime的值.它们被分别存储在数据库的两张表中.
    • odoo.tools.DEFAULT_SERVER_DATE_FORMAT

    • odoo.tools.DEFAULT_SERVER_DATETIME_FORMAT

      它们的格式 %Y-%m-%d%Y-%m-%d %H:%M:%S

日期格式

  • date跟datetime在服务端使用UTC格式存储。实际使用过程中时区可能会有一些不同。可以使用下面的方法进行时区的处理:
  • fields.Date.today() :返回当前的日期字符串
  • fields.Datetime.now() : 返回当前的日期时间
  • fields.Date.context_today(record, timestamp=None) : 通过会话上下文中传入的时区的值来返回日期
  • fields.Datetime.context_timestamp(record, timestamp) : 根据时区转换一个当前的日期时间,时区的取值从会话上下文中获取。
  • Date跟Datetime字段对象都有2个方法用来与string进行互相转换。分别是 from_string(value)to_string(value) .

记录集操作

  • in 操作: 判断一条记录是否在记录集中

  • recordset.ids :返回记录集中记录的ID列表

  • reocrdset.ensure_one() : 确认是否只包含单条记录。

  • recordset.filtered(func) : 使用func方法作为过滤,返回过滤记录集

  • recordset.mapped(func) : 理解为python中的map方法.返回map操作后的数据集

  • recordset.sorted(func) : 使用func方法进行排序.返回排序后的数据集. 下面是一些例子用来理解这些方法:

    例子

    image.png

操纵数据集

  • 数据集中的数据是不可变的,就跟python中的str,int,tuple一样。所以对数据集的增加,取代,删除动作实际上是生成新的数据集。 数据集的操作跟集合一样有四种操作方式。
  • rs1 | rs2 :并操作。
  • rs1 + rs2 : 加操作。这个操作可能会产生重复的记录数据。
  • rs1 & rs2 : 交(intersection)操作。返回两个记录集中同时拥有的元素构成的记录集。
  • rs1 - rs2 : 差(difference)操作,取rs1中存在而rs2中不存在的元素的记录集。
  • 分片操作也可以使用: rs[0] : 取第一个 rs[-1] : 最后一个 另外的操作:
  • self.task_ids | = task1 :  添加task1这条记录。
  • self.task_ids -= task1 : 删除task1这条记录。
  • self.task_ids = self.task_ids[:-1] : 删除最后一条记录。 对于关系型字段。还有一种方式来进行记录集的操作。使用create()跟write()方法。使用类似在XML文件定义关联字段值的语法。 例
  • self.write([(4, task1.id, None)]) : 添加task1记录
  • self.write([(3, task1.id, None)]) : 删除task1记录。
  • self.write([(3, self.task_ids[-1].id, False)]) : 删除最后一条记录。

使用关系型字段

  • 如前面一开始所说的,我们可以使用.操作符来获取到单例记录的字段值。

  • 在many-to-one关系中。我们可以直接用.操作符直接获取到关联模型的所有记录。

    直接获取关联模型

  • 更为方便的是,空的记录集也会被看做一个单例记录集,使用.操作符来操作空记录集会得到False而不会抛出异常。

    空记录集返回False