DTCloud 动作介绍

263 阅读10分钟

dtcloud 中的动作,指的是一系列点击的操作,对应不同的应用场景。最常见的就是 act_window 这个动作,像我们打开 form 视图、tree 视图和 search 视图的操作都是 act_window 动作。 dtcloud 中的动作可以分为如下几类:

  • act_window: 与视图相关的工作,常见的有 form\tree\search\kanban 等等。
  • act_window_close: 与 act_widnow 配合使用,用于关闭窗口。
  • act_url: 页面跳转相关的动作
  • server: 触发服务器动作
  • todo: 配置向导
  • client: 客户端相关动作

通用属性
上面列出的几种动作类型,均是继承自 ir.actions.actions 对象而来,所有他们有着共同的几个比较重要的属性:

  • name: 动作名称
  • type: 动作类型,通常为类型名称
  • xml_id: 引用的外部 xml_id
  • help: 说明
  • binding_model_id: 在 Sidebar 中显示菜单项的模型(绑定模型)
  • binding_type: 绑定类型,可选值有 action,action_form_only 和 report,默认为 action,action 与 action_form_only 的区别在于 action 会是 sidebar 上的按钮显示在 tree 和 form 视图中,action_form_only 则只显示在 form 中。report 用于报表打印。

act_window
视图动作类型,这个是我们最经常用到的一个动作。我们在前面的例子中创建的视图都会通过绑定一个动作关联到一个菜单中,然后才会在页面中显示出来。当我们单击菜单时,就会触发绑定的动作,找到关联的视图,最后渲染成我们所见到的页面。
下面详细介绍 act_window 所拥有的属性及作用:

  • view_id: 指定动作所绑定的页面,值为页面的 xml_id。
  • domain: 过滤条件,值为 python 表达式,过滤目标数据。
  • context: 上下文
  • res_id: 关联的数据库 ID,只有当 view_mode 参数仅为 form 时起作用
  • res_model: 要打开的视图数据所属模型
  • src_model: 该动作绑定的源模型(在该模型上打开动作)
  • target: 目标窗口,可选值有 current\new\inline\fullscreen\main,默认为 current,即在当前窗口打开,new 是弹出窗口,就是我们常见的模态窗口。
  • view_mode: 视图类型,tree,form,kanban 等
  • view_type: 用于展示列表视图的类型,可选值有 tree 和 form。
  • usage: 在 user 页面过滤菜单和默认动作的选项
  • view_ids:关联的视图对象(one2many)
  • views: 显示的视图
  • limit: 树形视图显示的条数
  • groups_id: 拥有访问权限的组 id
  • search_view_id: 关联的搜索视图 id
  • multi: 是否实只在树形视图中显示。
    关于 ir.actions.act_window 的使用示例,这里就不过多介绍了,前面的例子中很多地方都用到了,相信大家对此也已经驾轻就熟。
    不过有个问题还是需要大家留意一下,就是我们通常在定义 XML 的时候要先定义 action 然后再定义菜单 menu 对象,否则有可能会出现报错,找不到 action 的问题。
    13.0 取消了 view_type 字段

act_window_close
ir.actions.act_window_close 的作用非常简单,就是关闭当前窗口。常用的场景就是完成一段业务逻辑后,需要将此窗口关闭时。这时,只需要返回一个 act_window_close 的动作即可。

def btn_OK(self):
    return {
        'type':'ir.actions.act_window_close'
    }

上述的代码是笔者的一个三方模块中的部分代码,这个模块的作用就是在一段代码执行完成后弹窗告诉用户,执行的结果,当用户点击 OK 按钮时自动关闭弹窗。详细代码可以参考这里
act_url
dtcloud 中还有一个重要但不是特别常见的应用场景,就是页面的跳转或是文件的下载。通常由于已经封装好的 many2one 和 many2many 空间都自带了跳转连接,一般不需要我们太多的关注页面的跳转问题。但是当我们需要指定一个跳转页面的时候,就需要使用 ir.actions.act_url 来帮我们完成这个了。
url 可选的参数比较少:

  • url: 需要跳转的 url
  • target: 是在新窗口打开还是本页面跳转(new,self),默认是 new
    举例来说,我希望增加一个菜单,单击后打开必应搜索,那么我的 action 就可以这么写:
<record id="act_bing" model="ir.actions.act_url">
    <field name="name">打开bing网页</field>
    <field name="target">new</field>
    <field name="url">http://www.bing.com</field>
</record>

url 的写法需要注意,如果你连接的是站内连接不需要添加前面的 host 及 http 协议头,如果你添加的是外链,则需要保证 URL 完整。
另一个比较实用的场景就是下载,原理是一样,我们通常把附件放到文档管理中,那么我在某些地方想要下载一些文件(比如说模板),那么就可以添加一个按钮用于下载,而这个按钮的后台逻辑就是简单的返回一个 act_url 即可:

return {
        'type': 'ir.actions.act_url',
        'url': f"/web/content/{doc.id}?download=true",
        'target': 'new',
    }

doc.id 是通过搜索出来的附件的记录 ID。
server
server 类型的 action 主要的使用场景是执行一段预定义的 python 代码。server 类型的 action 主要包含如下几个属性:

  • sequence: 当要同时执行多个 server action 时,根据本字段的值排序执行。
  • model_id: server 脚本要在哪个 model 对象执行。
  • model_name: model 对象的名称
  • code: 要执行的 python 代码。(包含一些预置的变量类型,后续章节会讲到)
  • child_ids: 子 server action 列表,最后一个子动作返回的结果作为整个动作的返回结果
  • crud_model_id: 要变更的模型 id
  • crud_model_name: 要创建/变更的模型名称
  • link_field_id: 指定当前记录与新记录进行 many2one 关联的字段
  • fields_lines: 创建或复制记录时需要的字段。
    server action 的用处有很多,dtcloud 中的定时任务就是利用 server action 实现的。

下面我们将以导出销售订单 Excel 文件为例,看如何利用 server action。
action server 应用之一 导出销售订单 Excel 文件
这是一个实际实施过程中常见的需求,要求将某模型的数据导出为 Excel。首先,我们需要把数据组织出来,然后使用 xlwt 库写成 Excel 文件,最后将文件返回给用户。由于我们这个动作是在更多按钮中进行的,因此定义为一个 server action 更为合适。
server action
定义 server action

<record id="act_sale_export" model="ir.actions.server">
    <field name="name">销售订单导出</field>
    <field name="model_id" ref="sale.model_sale_order"/>
    <field name="state">code</field>
    <field name="code">
        action=model.export_order()
    </field>
    <field name="binding_model_id" ref="sale.model_sale_order"/>
</record>

这个 server action 中定义了要调用的模型(sale.order)和要调用的方法(export_order)。由于我们需要通过 controller 将文件返回给用户,因此,我们需要这个方法返回一个 action,返回 action 的方法是定义一个 action 变量存储被调用方法的返回值,odoo 会自动识别 action 并执行这个动作。

def export_order(self):
    """导出销售订单"""
    order = self.browse(self.env.context.get("active_id", None))
    if order:
        wkbook = xlwt.Workbook()
        wksheet = wkbook.add_sheet(f"销售订单{order.name}")
 
        wksheet.write(0, 0, "产品")
        wksheet.write(0, 1, "订购数量")
        wksheet.write(0, 2, "计量单位")
        wksheet.write(0, 3, "单价")
        wksheet.write(0, 4, "小计")
 
        row = 1
        for line in order.order_line:
            wksheet.write(row, 0, line.product_id.name)
            wksheet.write(row, 1, line.product_uom_qty)
            wksheet.write(row, 2, line.product_uom.name)
            wksheet.write(row, 3, line.price_unit)
            wksheet.write(row, 4, line.price_subtotal)
            row += 1
        buffer = BytesIO()
        wkbook.save(buffer)
        order.export_file = buffer.getvalue()
 
        return {
            'type': 'ir.actions.act_url',
            'url': f"/web/binary/download_document?model=sale.order&field=export_file&id={order.id}&filename={order.name}.xls",
            'target': 'self',
        }

server action 在调用的时候并没有带入当前记录的 id,因此,我们需要手动在上下文 context 中获取当前导出事件的记录 id,然后利用 xlwt 写入 Excel 文件。最后,我们返回了一个 act_url 的动作,该动作的作用是调用我们定义的下载 controller,将文件返回给用户。

from dtcloud import http
from dtcloud.http import request
from dtcloud.addons.web.controllers.main import serialize_exception, content_disposition, ensure_db
 
class Binary(http.Controller):
    @http.route('/web/binary/download_document', type='http', auth="public")
    @serialize_exception
    def download_document(self, model, field, id, filename=None, **kw):
        """ Download link for files stored as binary fields.
        :param str model: name of the model to fetch the binary from
        :param str field: binary field
        :param str id: id of the record from which to fetch the binary
        :param str filename: field holding the file's name, if any
        :returns: :class:`werkzeug.wrappers.Response`
        """
        export = request.env[model].sudo().browse(int(id))
        filecontent = export.export_file
        if not filecontent:
            return request.not_found()
        else:
            if not filename:
                filename = '%s_%s' % (model.replace('.', '_'), id)
            return request.make_response(filecontent,
                                         [('Content-Type', 'application/octet-stream'),
                                          ('Content-Disposition', content_disposition(filename))])

这是一个通用的下载 controller,方便以后有其他类型的 Excel 文件需要下载,可以直接调用此接口。有关 controller 的更多内容,请参考 Controller 相关章节。
todo
ir.actions.todo 虽然被定义在了 ir.actions,但它确实是这些对象中的“异类”,它没有继承自 ir.actions.actions,这也就是说,ir.actions.todo 不是一个动作。todo 的属性列表如下:

  • action_id: 要执行的动作 id
  • state: 状态,open 或是 done,默认为 open,当被执行完成后设置为 done.
  • sequence: 序列,默认为 10
  • name: 名称

示例:

<record id="act_todo" model="ir.actions.todo">
    <field name="action_id" ref="act_bing"/>
    <field name="state">open</field>
    <field name="sequence">1</field>
    <field name="type">automatic</field>
</record>

type 的可选值有如下三个:

  • manual: 人工设置
  • automatic: 自动设置(每次系统设置,或者安装或是升级系统的时候自动执行)
  • once: 仅执行一次
    todo 的使用场景是当在安装或是升级模块时,需要执行某些特殊的动作。
    client
    ir.actions.act_client 动作是执行完全定义在客户端的动作,而不经过后台。这样就给我们提供了一种绕过后台定义的 widget 而实现自己的页面的一种方式。client 包含如下几个属性:
  • tag: 指定客户端部件的 id
  • target: 打开方式,可选值:current\new\fullscreen\main
  • res_model: 目标模型
  • params:根据视图 tag 一同发给 cleint 的参数
  • params_store:储存的参数

举例:
这里涉及到 QWeb 相关内容,没了解 Qweb 的同学可以参考第四章和第五章
我们先定义一个菜单,绑定我们的客户端动作:

<record id="act_bing" model="ir.actions.client">
    <field name="name">打开Bing</field>
    <field name="tag">web.bing</field>
</record>
 
<menuitem name="打开Bing" id="book_store.menu_open_bing" action="act_bing" parent="book_store.menu_root"/>

然后我们创建我们自己的 Web widget:

dtcloud.define('require', function (require) {
    'use strict';
 
    var core = require("web.core");
    var Widget = require("web.AbstractAction");
 
    var Bing = Widget.extend({
        template: "bing",
 
        init: function (parent, data) {
            return this._super.apply(this, arguments);
        },
 
        start: function () {
            return true;
        },
        on_attach_callback: function () {
 
        }
    });
 
    core.action_registry.add("web.bing", Bing);
 
    return {
        Bing: Bing
    };
 
});

在 v11 版本中,Widget 需要为 Widget 的子部件,v12 中则需要为 AbstractAction
最后,我们将定义的 Bing 部件,加载到 XML 页面中:

<template id="assets_backend" inherit_id="web.assets_backend">
    <xpath expr="script[last()]" position="after">
        <script type="text/javascript" src="/book_store/static/src/js/widget.js"/>
    </xpath>
</template>

这样就完成了我们自定义的页面,升级模块我们就能看到效果了:

image.png 这里再给出一个企业版模块中示例,我们希望在生产单的工单页面中,跳转到扫码模块界面,我们可以利用扫码模块的客户端动作:

<record id="stock_barcode_action_main_menu" model="ir.actions.client">
    <field name="name">Barcode</field>
    <field name="tag">stock_barcode_main_menu</field>
    <field name="target">fullscreen</field>
</record>

从这里可以看出,tag 不仅可以指定 widget 部件,还可以指定目录。
给动作设置默认值
很多时候,我们需要设置一个默认值,比如,当我们打开某一个菜单的时候,希望能够按照我们的要求,默认显示一些分组或是过滤条件,再或者是给某些字段添加默认值,这个时候,我们就可以使用动作中的 context 来完成这个目的。
设置默认分组
当我们打开某个列表视图的时候,我们希望能够显示默认分组的效果,就可以使用 context 来完成:

<record id="project.open_view_project_all" model="ir.actions.act_window">
    <field name="type">ir.actions.act_window</field>
    <field name="view_mode">tree</field>
    <field name="context">{'group_by':'group_id'}</field>
    <field name="view_ids" eval="[(6,0,[ref('project_action_view_ref')])]"/>
</record>

这里的 group_by 就是分组关键字,后边跟着要分组的字段。
设置默认值
假设我们有一个 list 列表,列表中有一个按钮,我们希望当我们点击这个按钮的时候,执行一个动作,给新打开的页面赋一个默认值,那么我们可以这么写:

<!--重新定义tree-->
<record id="project_tree_list" model="ir.ui.view">
    <field name="name">项目</field>
    <field name="model">project.project</field>
    <field name="priority">1</field>
    <field name="arch" type="xml">
        <tree>
            <field name="name"/>
            <field name="user_id"/>
            <field name="partner_id"/>
            <button string="查看" class="oe_stat_button" icon="fa-filter" name="%(act_project_to_tasks)d" type="action" 
            context="{
                'default_project_id':active_id
            }"/>
        </tree>
    </field>
</record>

active_id 代表的时当前记录的 id,这样实现的效果就是当我们单击按钮后,新页面中的 project_id 就被赋予了默认值。
示例代码
下面展示了如何创建一个向导,并将他绑定在指定的模型列表中:

class juhui_repair_top_wizard(models.TransientModel):
 
    _name = "juhui.repair.top.wizard"
 
    def button_mark_top(self):
        """将维修单置顶"""
        #[TODO] 置顶
        pass

首先创建一个临时模型(因为向导终将是要被销毁的),然后创建与之对应的 form 视图:

<record id="juhui_repair_top_wizard_form" model="ir.ui.view">
    <field name="name">juhui.repair.top.wizard</field>
    <field name="model">juhui.repair.top.wizard</field>
    <field name="arch" type="xml">
    <form string="" class="">
        <div>确定将这些单据置顶吗?</div>
        <footer>
        <button name="button_mark_top" type="object" string="置顶订单" class="oe_highlight"/>
or
        <button string="取消" special="cancel" class="oe_link"/>
        </footer>
    </form>
    </field>
</record>

然后是配置动作,与常规使用不同的是,这里的动作要指明绑定的对象。

<record id="action_juhui_repair_top" model="ir.actions.act_window">
    <field name="name">批量置顶订单</field>
    <field name="type">ir.actions.act_window</field>
    <field name="res_model">juhui.repair.top.wizard</field>
    <field name="target">new</field>
    <field name="view_id" ref="juhui_repair_top_wizard_form"/>
    <field name="binding_model_id" ref="juhui_repair.model_juhui_repair_order"/>
</record>

当然也可以使用简写的方式:

<act_window id="action_juhui_repair_top" name="批量置顶维修单" res_model="juhui.repair.top.wizard"
      target="new" binding_views="list" view_mode="form" binding_model="juhui.repair.order"/>

其中 binding_views 的取值为 form 或者 list,用于指定是否仅在列表视图中显示。