Packer.Odoo.10---Chapter 7

ORM应用逻辑-业务处理

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

创建一个向导

  • 假设有这么一个需求:To-Do应用中,用户需要设置一系列任务的截止时间跟负责人。如果单独的打开每条任务记录去修改那是十分麻烦的。我们就需要使用一个向导表单来选择需要更改的任务记录,再进行统一操作。
  • 向导表单可以理解为获取用户输入,然后再对这些输入进行储存,以便下一步应用到Odoo模型记录中。
任务向导表单
  • 我们来新建一个名为 todo_wizard 的模块用以演示。
    • 还是跟往常一样。创立todo_wizard/__manifest__.py文件添加如下代码来进行模块的描述.
{
    '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按钮的逻辑

    @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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,527评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,314评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,535评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,006评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,961评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,220评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,664评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,351评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,481评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,397评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,443评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,123评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,713评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,801评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,010评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,494评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,075评论 2 341

推荐阅读更多精彩内容