在本章中,我们将介绍以下内容:

  • 创建自定义小部件
  • 使用客户端QWeb模板
  • 对服务器进行RPC调用
  • 创建一个新视图
  • 调试客户端代码
  • 通过巡回演出提高新人入职能力
  • 手机应用程序的JavaScript

 

介绍

在第10章后端视图中,你看到了如何使用后端提供的现有可能性。在这里,我们将看看如何扩展和定制这些可能性。web模块包含所有与Odoo用户界面相关的内容。

如你所知,Odoo有两个不同的版本(企业版和社区版)。Community使用web模块作为用户界面,而Enterprise版本使用Community web模块的扩展版本,即web_enterprise模块。

我们将在这里开发社区版。不用担心——社区开发的模块在企业版中工作得很好,因为web_enterprise内部依赖于社区web模块,只是添加了一些特性。

我们还将从头创建一个新视图。读完这一章后,你将能够在Odoo后端创建自己的UI元素。

在本章中,我们将假设你有JavaScript, jQuery,Underscore.js和SCSS。

创建自定义小部件

例如,我们使用widget='image'将二进制字段显示为图像。为了演示如何创建自己的小部件,我们将编写一个小部件,让用户选择一个整数字段,但我们将以不同的方式显示它。我们将显示一个颜色选择器,而不是一个输入框,这样我们就可以选择一个色号。

准备

在这个内容中,我们将添加一个新的需要依赖于web模块的字段小部件。确保你已经在清单文件中添加了依赖onweb,像这样:

...
'depends': ['base', 'web'],
...

怎么做呢?

然后,我们将在books表单上添加一个整数字段来使用我们的新小部件。按照以下步骤添加一个新的字段小部件: 

添加一个静态/src/js/field_widget.js文件。这里使用的语法,请参考第15章,CMS网站开发中的扩展CSS和JavaScript。

odoo.define('my_field_widget', function (require) {
  "use strict";
  var AbstractField = require('web.AbstractField');
  var fieldRegistry = require('web.field_registry');
......

通过扩展AbstractField创建小部件:

var colorField = AbstractField.extend({
......

  3.为扩展的小部件设置CSS类、根元素标签和支持的字段类型:

var colorField = AbstractField.extend({
    className: 'o_int_colorpicker',
    tagName: 'span',
    supportedFieldTypes: ['integer'],
......

捕获一些JavaScript事件:

var colorField = AbstractField.extend({
    className: 'o_int_colorpicker',
    tagName: 'span',
    supportedFieldTypes: ['integer'],
    events: {
        'click .o_color_pill': 'clickPill',
    },
......

重写init来做一些初始化:

var colorField = AbstractField.extend({
    className: 'o_int_colorpicker',
    tagName: 'span',
    supportedFieldTypes: ['integer'],
    events: {
        'click .o_color_pill': 'clickPill',
    },
    init: function () {
        this.totalColors = 10;
        this._super.apply(this, arguments);
    },

覆盖_renderEdit和_renderReadonly来设置DOM元素:

var colorField = AbstractField.extend({
    className: 'o_int_colorpicker',
    tagName: 'span',
    supportedFieldTypes: ['integer'],
    events: {
        'click .o_color_pill': 'clickPill',
    },
    init: function () {
        this.totalColors = 10;
        this._super.apply(this, arguments);
    },
    _renderEdit: function () {
        this.$el.empty();
        for (var i = 0; i < this.totalColors; i++ ) {
            var className = "o_color_pill o_color_" + i;
            if (this.value === i ) {
                className += ' active';
            }
            this.$el.append($('<span>', {
                'class': className,
                'data-val': i,
            }));
        }
    },
    _renderReadonly: function () {
        var className = "o_color_pill active readonly o_color_" + this.value;
        this.$el.append($('<span>', {
            'class': className,
        }));
    },

定义我们前面提到的处理程序:

var colorField = AbstractField.extend({
    className: 'o_int_colorpicker',
    tagName: 'span',
    supportedFieldTypes: ['integer'],
    events: {
        'click .o_color_pill': 'clickPill',
    },
    init: function () {
        this.totalColors = 10;
        this._super.apply(this, arguments);
    },
    _renderEdit: function () {
        this.$el.empty();
        for (var i = 0; i < this.totalColors; i++ ) {
            var className = "o_color_pill o_color_" + i;
            if (this.value === i ) {
                className += ' active';
            }
            this.$el.append($('<span>', {
                'class': className,
                'data-val': i,
            }));
        }
    },
    _renderReadonly: function () {
        var className = "o_color_pill active readonly o_color_" + this.value;
        this.$el.append($('<span>', {
            'class': className,
        }));
    },
    clickPill: function (ev) {
        var $target = $(ev.currentTarget);
        var data = $target.data();
        this._setValue(data.val.toString());
    }

});

不要忘记注册你的小部件:

fieldRegistry.add('int_color', colorField);

使它可用于其他附加组件:

return {
      colorField: colorField,
  };
});// closing 'my_field_widget' namespace

在静态/src/ SCSS /field_widget.scss中添加一些SCSS:

.o_int_colorpicker {
    .o_color_pill {
        display: inline-block;
        height: 25px;
        width: 25px;
        margin: 4px;
        border-radius: 25px;
        position: relative;
        @for $size from 1 through length($o-colors) {
            &.o_color_#{$size - 1} {
                background-color: nth($o-colors, $size);
                &:not(.readonly):hover {
                    transform: scale(1.2);
                    transition: 0.3s;
                    cursor: pointer;
                }
                &.active:after{
                    content: "\f00c";
                    display: inline-block;
                    font: normal normal normal 14px/1 FontAwesome;
                    font-size: inherit;
                    color: #fff;
                    position: absolute;
                    padding: 4px;
                    font-size: 16px;
                }
            }
        }
    }
}

  11.在views/templates.xml的后端资源中注册两个文件:

<?xml version="1.0" encoding="UTF-8"?>
<odoo>

    <template id="assets_end" inherit_id="web.assets_backend">
        <xpath expr="." position="inside">
            <script src="/my_library/static/src/js/field_widget.js" type="text/javascript" />
            <link href="/my_library/static/src/scss/field_widget.scss" rel="stylesheet" type="text/scss" />
        </xpath>
    </template>

</odoo>

最后,在library_book模型中添加颜色整数字段:

color = fields.Integer()

在书的表单视图中添加颜色字段,并添加widget="int_color":

...
<group>
  <field name="date_release"/>
  <field name="color" widget="int_color"/>
</group>
...

更新完成后,打开图书的表单视图,你会看到颜色选择器,如下图所示:

Odoo Community下载 odoo客户端_Odoo Community下载

它是如何工作的…

  为了让您理解我们的示例,让我们通过查看widget的组件来回顾一下它的生命周期:

  init():这是小部件构造函数。它用于初始化目的。初始化小部件时,首先调用此方法。

  willStart():这个方法在小部件初始化和添加到DOM中的过程中被调用。它用于将异步数据初始化到小部件中。它还应该返回一个延迟对象,该对象可以简单地从super()调用中获得。我们将在后面的内容中使用这个方法。

  start():该方法在小部件完成呈现后调用,但还没有添加到DOM中。它对于post呈现作业非常有用,应该返回一个延迟对象。您可以访问this.$el中呈现的元素

  destroy():在销毁小部件时调用此方法。它主要用于基本的清理操作,如事件解绑定。

  Widget的基本基类是Widget(由web.Widget定义)。如果你想深入研究它,你可以在/addons/web/static/src/js/core/widget.js学习。

  在步骤1中,我们导入了AbstractFieldfieldRegistry

  在第2步中,我们通过扩展AbstractField创建了colorField。通过这样,我们的colorField将从AbstractField获得所有属性和方法。

  在步骤3中,我们添加了三个属性—classname用于为小部件的根元素定义类,tagName用于根元素类型,而supportedFieldTypes用于决定这个widget支持哪种类型的字段。在本例中,我们希望为integer类型字段创建一个widget。

步骤4中,我们映射了小部件的事件。通常,key是event名称和可选的CSS选择器的组合event和CSS选择器之间用空格隔开,值将是widget方法的名称。因此,当执行event时,将自动调用指定的方法。在此内容中,当用户单击颜色时,我们希望在字段中设置integer值。为了管理click事件,我们在events键中添加了一个CSS选择器和方法名。

  在第5步中,我们覆盖了init方法并设置了this.totalColors属性的值。我们将使用这个变量来决定颜色丸的数量。我们想要显示10个颜色丸,所以我们将值设置为10。

  在第6步中,我们添加了两个方法-_renderEdit_renderReadonly。顾名思义,当widget处于编辑模式时调用_renderEdit,而当widget处于只读模式时调用_renderReadonly。在edit方法中,我们添加了几个<span>标记,每个标记表示widget中的一个单独的颜色。单击<span>标记后,我们将在字段中设置值。我们把它们加到this.$el中。这里,$el是widget的根元素,它将被添加到表单视图中。在只读模式下,我们只想显示活动的颜色,因此我们通过_renderReadonly()方法添加了单个pill。现在,我们已经以硬编码的方式添加了pill,但是在下一个内容中,我们将使用一个JavaScript Qweb模板来呈现pill。注意,在编辑方法中,我们使用了totalColors属性,该属性是由init()方法设置的

  在第7步中,我们添加了clickPill处理程序方法来管理pill点击。为了设置字段值,我们使用了_setValue方法。此方法是从AbstractField类添加的。当你设置字段值时,Odoo框架会重新运行widget并再次调用_renderEdit方法,这样你就可以用更新后的值呈现widget。

  在步骤8中,在我们定义了新的widget之后,向表单widget注册表注册它是至关重要的,它位于web.field_registry中。注意,所有视图类型都会查看此注册表,因此如果您希望以另一种方式在列表视图中显示字段,您还可以在这里添加widget并在视图定义的字段上设置widget属性。

最后,导出我们的widget类,以便其他add-ons可以扩展它或从它继承它。然后,我们在library.book模型中添加了一个名为color的新整数字段。我们还使用widget="int_color"属性在表单视图中添加了相同的字段。这将在表单中显示我们的widget,而不是默认整数widget。

 

有更多的…

 

您已经在本内容中使用了这些mixins。AbstractField是通过继承Widget类创建的,Widget类继承两个mixins。第一个是EventDispatcherMixin,它提供了一个用于附加和触发事件处理程序的简单接口。第二个是ServicesMixin,它为RPC调用和操作提供函数

bug的一个常见原因是忘记返回超级用户的deferred对象这会导致异步操作出现问题

使用isValid函数来实现这个方面的定制。


  

使用客户端QWeb模板

幸运的是,客户端也有模板引擎可用,更幸运的是,客户端模板引擎具有与服务器端模板相同的语法。

准备

我们将通过将DOM元素创建移动到QWeb来使其更加模块化。

怎么做呢?

按照以下步骤开始:

导入web.core并将qweb引用提取到一个变量中,如下代码所示:

odoo.define('my_field_widget', function (require) {
"use strict";
var AbstractField = require('web.AbstractField');
var fieldRegistry = require('web.field_registry');
var core = require('web.core');
var qweb = core.qweb;
...

将_renderEdit函数改为简单地呈现元素(继承自widget):

_renderEdit: function () {
  this.$el.empty();
  var pills = qweb.render('FieldColorPills', {widget:this});
  this.$el.append(pills);
},

  3.将模板文件添加到static/src/xml/qweb_template.xml:

<?xml version="1.0" encoding="UTF-8"?>
<templates>
  <t t-name="FieldColorPills">
    <t t-foreach="widget.totalColors" t-as='pill_no'>
      <span t-attf-class="o_color_pill o_color_#{pill_no} #{widget.value === pill_no and 'active' or ''}" 
                t-att-data-val="pill_no"/>
    </t>
  </t>
</templates>

  4.在你的manifest中注册QWeb文件:

'qweb': [
        'static/src/xml/qweb_template.xml'
    ]

现在,对于其他add-ons,更改widget使用的HTML代码要容易得多,因为它们可以简单地用通常的QWeb模式覆盖它。

 

它是如何工作的…

 

首先,您需要认识到我们处理的是JavaScript QWeb实现,而不是服务器端的Python实现。这意味着你不能访问浏览记录或环境;您只能访问从qweb.render函数传递的参数。

在本例中,我们通过widget key传递了当前对象。这意味着您应该在小部件的JavaScript代码中拥有所有的智能,并且让您的模板只访问属性,或者可能是函数。假设我们可以访问widget上可用的所有属性,我们可以通过检查totalColors属性来检查模板中的值。

  由于客户端QWeb与QWeb views没有任何关系,所以有一种不同的机制可以让web客户机知道这些模板——通过与add-on的清单相关的文件名列表中的QWeb密钥来添加它们。

有更多的…

在客户端,不能使用XPath表达式;您需要使用jQuery选择器和操作。例如,如果我们想在widget中添加另一个模块的用户图标,我们将使用以下代码在每个pill中添加一个图标:

<t t-extend="FieldColorPills">
  <t t-jquery="span" t-operation="prepend">
    <i class="fa fa-user" />
  </t>
</t>

  如果我们在这里也提供了一个t-name属性,那么我们将对原始模板进行复制,并且不动那个模板。t-operation其他可能的属性值:append, before, after, inner, 和 replace,导致t元素的内容是通过添加附加到匹配的元素的内容,把匹配的元素之前或之后通过之前或之后,通过内部替换匹配的元素的内容,通过替换或取代完整的元素。还有t-operation= ' attributes',它允许您在匹配的元素上设置属性,遵循与服务器端QWeb相同的规则。 

  另一个不同之处在于,客户端QWeb中的名称不是由模块名称命名的,因此您必须为模板选择名称,而这些名称可能是您安装的所有外接程序中唯一的,这就是开发人员倾向于选择较长的名称的原因。

另请参阅

  如欲了解更多有关Qweb模版的资料,请参阅以下要点:

一个小错误通常意味着什么都没有发生,初学者很难从那里继续下去。

  幸运的是,有一些客户端QWeb模板的调试语句将在本章后面的调试客户端代码内容中描述。


 

对服务器进行RPC调用

在这个内容中,我们将在颜色药片上添加一个工具提示。当用户将光标悬停在color pill元素上时,工具提示将显示与该颜色相关的书籍数量。

我们将对服务器进行RPC调用,以获取与特定颜色相关联的数据的图书计数。

准备

  对于这个内容,我们将使用上一个内容中的my_library模块。

怎么做呢?

  执行以下步骤,对服务器进行RPC调用,并在工具提示中显示结果:

在RPC调用中添加willStart方法并设置colorGroupData:  

willStart: function () {
        var self = this;
        this.colorGroupData = {};
        var colorDataDef = this._rpc({
            model: this.model,
            method: 'read_group',
            domain: [],
            fields: ['color'],
            groupBy: ['color'],
        }).then(function (result) {
            _.each(result, function (r) {
                self.colorGroupData[r.color] = r.color_count;
            });
        });
        return $.when(this._super.apply(this, arguments), colorDataDef);
    },

  2.更新_renderEdit并设置药丸的引导工具提示:

_renderEdit: function () {
        this.$el.empty();
        var pills = qweb.render('FieldColorPills', {widget: this});
        this.$el.append(pills);
        this.$el.find('[data-toggle="tooltip"]').tooltip();
    },

  3.更新FieldColorPills药丸模板并添加工具提示数据:

<?xml version="1.0" encoding="UTF-8"?>
<templates>
    <t t-name="FieldColorPills">
        <t t-foreach="widget.totalColors" t-as='pill_no'>
            <span t-attf-class="o_color_pill o_color_#{pill_no} #{widget.value === pill_no and 'active' or ''}"
            t-att-data-val="pill_no"
            data-toggle="tooltip"
            data-placement="top"
            t-attf-title="This color is used in #{widget.colorGroupData[pill_no] or 0 } books."
            />
        </t>
    </t>
</templates>

更新后,你将会看到药丸的提示,如下截图所示:

Odoo Community下载 odoo客户端_表单_02

它是如何工作的…

 

  willStart函数在呈现之前被调用,更重要的是,它返回一个延迟对象,必须在呈现开始之前解析该对象。因此,在像我们这样的情况下,我们需要在呈现发生之前运行一个异步操作,这是做这件事的正确函数。 

 

  在处理数据访问时,我们依赖于ServicesMixin类提供的_rpc函数,正如前面解释的那样。这个函数允许您调用模型上的任何公共函数,比如search、read、write、或在本例中是read_group。 

 

  在步骤1中,我们对当前模型(在我们的例子中是library.book)进行了一个RPC调用并调用了read_group方法。我们根据颜色字段对数据进行分组,因此RPC调用将返回按颜色分组的图书数据,并在color_count键中添加一个聚合。我们还在colorGroupData中映射了color_count和颜色索引,以便可以在QWeb模板中使用它。在函数的最后一行中,我们解析了将从super开始,并使用$.when调用RPC。因此,渲染只发生在值被获取之后,并且在任何异步动作super已经完成之后。

我们刚刚初始化了引导工具提示。 

在willStart方法中,我们通过this.colorGroupData分配了一个颜色映射,这样您就可以通过widget.colorGroupData在QWeb模板中访问它们。这是因为我们传递了小部件引用;这是qweb.render方法。

您可以在小部件中的任何位置使用_rpc。请注意,这是一个异步调用,您需要正确地管理一个延迟对象以获得所需的结果。 

有更多的…

  AbstractField类具有两个有趣的属性,其中一个我们刚刚使用过。在我们的示例中,我们使用了this.model属性,它保存了当前模型的名称(例如,library_book)。另一个属性是this.field,它大致包含模型的fields_get()函数的输出,用于小部件显示的字段。这将给出与当前字段相关的所有信息。例如,对于x2x字段,fields_get()函数提供关于共同模型或域的信息。您还可以使用它来查询字段的stringsize或在模型定义期间可以在字段上设置的任何其他属性。

另一个有用的属性是nodeOptions,它包含通过<form>视图定义中的options属性传递的数据。这已经是JSON解析,所以您可以像访问任何对象一样访问它。有关这些属性的更多信息,请深入abstract_field.js文件。

另请参阅 

  如果您在管理异步操作方面有问题,请参考以下文档:

您应该学习延迟对象以完全理解JavaScript中的RPC调用。你可以了解更多关于延期的知识jQuery文档中的对象,地址:https://api.query.com/jquery.deferred。


创建一个新视图

在这个内容中,我们将创建一个全新的视图。这个视图将显示作者列表以及他们的书籍。

准备

请注意,视图是非常复杂的结构,每个现有视图都有不同的目的和实现。此内容的目的是让您了解MVC模式视图以及如何创建简单视图。在这个内容中,我们将创建一个名为m2m_group的视图,其目的是在组中显示记录。为了将记录划分为不同的组,视图将使用many2many字段数据。在my_library模块中,我们有author_ids字段。在这里,我们将根据作者对图书进行分组,并以卡片的形式显示它们。

在这个按钮的帮助下,您将能够添加这本书的新记录。我们还将在作者卡片上添加一个按钮,以便我们可以将用户重定向到另一个视图。

怎么做呢?

  按照以下步骤添加一个新的视图m2m_group:

在ir.ui.view中添加一个新的视图类型:(\models\ir_ui_view.py)

# -*- coding: utf-8 -*-
from odoo import fields, models
class View(models.Model):
    _inherit = 'ir.ui.view'
    type = fields.Selection(selection_add=[('m2m_group', 'M2m Group')])

在ir.actions.act_window.view中添加一个新的视图模式:(\models\ir_action_act_window.py)

# -*- coding: utf-8 -*-
from odoo import fields, models

class ActWindowView(models.Model):
    _inherit = 'ir.actions.act_window.view'

    view_mode = fields.Selection(selection_add=[('m2m_group', 'M2m group')])

3.通过继承基模型添加新方法(\models\model.py)。这个方法将从JavaScript模型中调用(详见步骤4):

# -*- coding: utf-8 -*-

from collections import defaultdict
from datetime import datetime
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models
class Base(models.AbstractModel):
    _inherit = 'base'

    @api.model
    def get_m2m_group_data(self, domain, m2m_field):
        records = self.search(domain)
        result_dict = {}
        for record in records:
            for m2m_record in record[m2m_field]:
                if m2m_record.id not in result_dict:
                    result_dict[m2m_record.id] = {
                        'name': m2m_record.display_name,
                        'children': [],
                        'model': m2m_record._name
                    }
                result_dict[m2m_record.id]['children'].append({
                    'name': record.display_name,
                    'id': record.id,
                })
        return result_dict

4.添加新文件/static/src/js/m2m_group_model.js,并添加以下内容:

odoo.define('m2m_group.Model', function (require) {
    'use strict';

    var AbstractModel = require('web.AbstractModel');

    var M2mGroupModel = AbstractModel.extend({
        __get: function () {
            return this.data;
        },
        __load: function (params) {
            this.modelName = params.modelName;
            this.domain = params.domain;
            this.m2m_field = params.m2m_field;
            return this._fetchData();
        },
        __reload: function (handle, params) {
            if ('domain' in params) {
                this.domain = params.domain;
            }
            return this._fetchData();
        },
        _fetchData: function () {
            var self = this;
            return this._rpc({
                model: this.modelName,
                method: 'get_m2m_group_data',
                kwargs: {
                    domain: this.domain,
                    m2m_field: this.m2m_field
                }
            }).then(function (result) {
                self.data = result;
            });
        },
    });

    return M2mGroupModel;

});

5.添加一个新文件/static/src/js/m2m_group_controller.js,添加如下内容:

odoo.define('m2m_group.Controller', function (require) {
    'use strict';

    var AbstractController = require('web.AbstractController');
    var core = require('web.core');
    var qweb = core.qweb;

    var M2mGroupController = AbstractController.extend({
        custom_events: _.extend({}, AbstractController.prototype.custom_events, {
            'btn_clicked': '_onBtnClicked',
        }),
        renderButtons: function ($node) {
            if ($node) {
                this.$buttons = $(qweb.render('ViewM2mGroup.buttons'));
                this.$buttons.appendTo($node);
                this.$buttons.on('click', 'button', this._onAddButtonClick.bind(this));
            }
        },
        _onBtnClicked: function (ev) {
            this.do_action({
                type: 'ir.actions.act_window',
                name: this.title,
                res_model: this.modelName,
                views: [[false, 'list'], [false, 'form']],
                domain: ev.data.domain,
            });
        },
        _onAddButtonClick: function (ev) {
            this.do_action({
                type: 'ir.actions.act_window',
                name: this.title,
                res_model: this.modelName,
                views: [[false, 'form']],
                target: 'new'
            });
        },


    });

    return M2mGroupController;

});

6.添加一个新文件/static/src/js/m2m_group_renderer.js,并添加以下内容: 

odoo.define('m2m_group.Renderer', function (require) {
    'use strict';

    var AbstractRenderer = require('web.AbstractRenderer');
    var core = require('web.core');

    var qweb = core.qweb;

    var M2mGroupRenderer = AbstractRenderer.extend({
        events: _.extend({}, AbstractRenderer.prototype.events, {
            'click .o_primay_button': '_onClickButton',
        }),
        _render: function () {
            var self = this;
            this.$el.empty();
            this.$el.append(qweb.render('ViewM2mGroup', {
                'groups': this.state,
            }));
            return this._super.apply(this, arguments);
        },
        _onClickButton: function (ev) {
            ev.preventDefault();
            var target =  $(ev.currentTarget);
            var group_id = target.data('group');
            var children_ids = _.map(this.state[group_id].children, function (group_id) {
                return group_id.id;
            });
            this.trigger_up('btn_clicked', {
                'domain': [['id', 'in', children_ids]]
            });
        }
    });

    return M2mGroupRenderer;

});

7.添加一个新文件/static/src/js/m2m_group_view.js,并添加以下内容:

odoo.define('m2m_group.View', function (require) {
    'use strict';

    var AbstractView = require('web.AbstractView');
    var view_registry = require('web.view_registry');
    var M2mGroupController = require('m2m_group.Controller');
    var M2mGroupModel = require('m2m_group.Model');
    var M2mGroupRenderer = require('m2m_group.Renderer');


    var M2mGroupView = AbstractView.extend({
        display_name: 'Author',
        icon: 'fa-id-card-o',
        config: _.extend({}, AbstractView.prototype.config, {
            Model: M2mGroupModel,
            Controller: M2mGroupController,
            Renderer: M2mGroupRenderer,
        }),

        viewType: 'm2m_group',
        searchMenuTypes: ['filter', 'favorite'],
        accesskey: "a",

        init: function (viewInfo, params) {
            this._super.apply(this, arguments);
            var attrs = this.arch.attrs;

            if (!attrs.m2m_field) {
                throw new Error('M2m view has not defined "m2m_field" attribute.');
            }
            // Model Parameters
            this.loadParams.m2m_field = attrs.m2m_field;

        },
    });

    view_registry.add('m2m_group', M2mGroupView);

    return M2mGroupView;

});

将视图的QWeb模板添加到/static/src/xml/qweb_template.xml文件中:

<?xml version="1.0" encoding="UTF-8"?>
<templates>
    <t t-name="ViewM2mGroup">
        <div class="row ml16 mr16">
            <div t-foreach="groups" t-as="group" class="col-3">
                <t t-set="group_data" t-value="groups[group]" />
                <div class="card mt16">
                    <img class="card-img-top" t-attf-src="/web/image/#{group_data.model}/#{group}/image_1920"/>
                    <div class="card-body">
                        <h5 class="card-title mt8">
                            <t t-esc="group_data['name']"/>
                        </h5>
                    </div>
                    <ul class="list-group list-group-flush">
                        <t t-foreach="group_data['children']" t-as="child">
                            <li class="list-group-item">
                                <i class="fa fa-book"/>
                                <t t-esc="child.name"/>
                            </li>
                        </t>
                    </ul>
                    <div class="card-body">
                        <a href="#" class="btn btn-sm btn-primary o_primay_button" t-att-data-group="group">View books</a>
                    </div>
                </div>
            </div>
        </div>
    </t>

    <div t-name="ViewM2mGroup.buttons">
        <button type="button" class="btn btn-primary">
                Add Record
        </button>
    </div>

    <t t-name="FieldColorPills">
        <t t-foreach="widget.totalColors" t-as='pill_no'>
            <span t-attf-class="o_color_pill o_color_#{pill_no} #{widget.value === pill_no and 'active' or ''}" t-att-data-val="pill_no"
            data-toggle="tooltip" data-placement="top" t-attf-title="This color is used in #{widget.colorGroupData[pill_no] or 0 } books."/>
        </t>
    </t>
</templates>

添加所有的JavaScript文件到后端资产:

<?xml version="1.0" encoding="UTF-8"?>
<odoo>

    <template id="assets_end" inherit_id="web.assets_backend">
        <xpath expr="." position="inside">

            <script type="text/javascript" src="/my_library/static/src/js/m2m_group_view.js" />
            <script type="text/javascript" src="/my_library/static/src/js/m2m_group_model.js" />
            <script type="text/javascript" src="/my_library/static/src/js/m2m_group_controller.js" />
            <script type="text/javascript" src="/my_library/static/src/js/m2m_group_renderer.js" />

            <script src="/my_library/static/src/js/field_widget.js" type="text/javascript" />
            <link href="/my_library/static/src/scss/field_widget.scss" rel="stylesheet" type="text/scss" />
        </xpath>
    </template>

</odoo>

10.最后,为library.book模型添加我们的新视图:

<record id="library_book_view_author" model="ir.ui.view">
        <field name="name">Library Book Author</field>
        <field name="model">library.book</field>
        <field name="arch" type="xml">
            <m2m_group m2m_field="author_ids" color_field="color"> </m2m_group>
        </field>
    </record>

11.在book action中添加m2m_group:

<record id='library_book_action' model='ir.actions.act_window'>
        <field name="name">Library Books</field>
        <field name="res_model">library.book</field>
        <field name="view_type">form</field>
        <field name="view_mode">tree,m2m_group,form</field>
    </record>

这看起来如下:

Odoo视图非常容易使用,非常灵活。然而,通常情况下,简单和灵活的东西背后的实现是复杂的。这与Odoo JavaScript视图是相同的情况:它们很容易使用,但是实现起来很复杂。它由许多组件组成,比如model、renderer、controller、view、QWeb模板等等。在下一节中,我们已经为视图添加了所有必需的组件,并且还为library.book模型使用了一个新视图。如果您不想手动添加所有内容,可以从本书GitHub存储库中的示例文件中获取一个模块。

  它是如何工作的…

  在步骤1和步骤2中,我们在ir.ui.viewir.actions.act_window.view中注册了一个新的视图类型,称为m2m_group

  在步骤3中,我们在基础(base)中添加了get_m2m_group_data方法。在基础中添加此方法将使该方法在每个模型中可用。这个方法将通过从JavaScript视图的RPC调用来调用。视图将传递两个参数—domainm2m_field。在域参数中,域的值将是由搜索视图域和操作域组合生成的域。m2m_field是我们要根据它对记录进行分组的字段名。这个字段将在视图定义中设置。 

一个Odoo JavaScript视图由view、model、renderer和controller组成。在Odoo代码库中,view这个词具有历史意义,所以model, view, controller(MVC)变成了model, renderer, controller (MRC)。通常,视图设置model、renderer和controller,并设置MVC层次结构,使其看起来类似于以下内容:

Odoo Community下载 odoo客户端_客户端_03

  让我们看看Model、Renderer、ControllerView的角色。Model、Renderer、ControllerView的抽象版本拥有形成视图所需的所有基本内容。因此,在我们的示例中,我们已经通过继承创建了model、renderer、controller和view

  下面是一个关于创建视图的不同部分的深入解释:

  Model: Model的作用是保存视图的状态。它向服务器发送一个获取数据的RPC请求,然后将数据传递给控制器controller和呈现器renderer。然后重写load_reload方法。当视图被初始化时,它调用load()方法来获取数据,当搜索条件改变时,视图需要一个新的状态,然后调用reload()方法。在我们的例子中,我们有创建公共_fetchData()方法来对数据进行RPC调用。注意,我们使用了步骤3中添加的get_m2m_group_data方法。控制器将调用get()方法以获取模型的状态。

  Controller: Controller的角色是管理ModelRenderer之间的协调。当Renderer中出现action时,它将该信息传递给controller并执行action相应的行动。有时,它也会调用模型中的一些方法。除此之外,它还管理控制面板中的按钮。在我们的示例中,我们添加了一个按钮来添加新记录。为此,我们必须重写AbstractControllerrenderButtons()方法。我们还注册了custom_events,以便当单击作者卡中的一个按钮时,renderer将触发事件到controller,使其执行action

  Renderer: Renderer的作用是管理视图的DOM元素。每个视图都可以以不同的方式呈现数据。在呈现器中,您可以在一个状态变量中获得模型的状态。它调用render()方法的呈现。在我们的示例中,我们呈现了ViewM2mGroup QWeb模板的当前状态,以显示我们的视图。我们还映射了JavaScript事件来执行用户操作。在这个配制中,我们为的卡片按钮绑定了click事件。单击author card按钮后,它将向控制器触发btn_clicked事件,并为该作者打开图书列表。

 

  注意:events和custom_events是不同的。Events是正常的JavaScript事件,而custom_events事件是来自Odoo JavaScript框架。自定义事件可以通过trigger_up方法调用。

  View: Renderer的作用是获取构建视图所需的所有基本东西,比如一组fields、一个context、一个View arch和一些其他参数。之后,视图将初始化controlle_rrenderermodel三元组。它将在MVC层次结构中设置它们。通常,它设置modelviewcontroller中需要的参数。在我们的示例中,我们希望m2m_field名称在模型中获得适当的分组数据,因此我们在其中设置了模型参数。同样,可以使用this.controllerParamsthis.rendererParams来设置contr_ollerrender_er中的参数

 

   在第8步中,我们为视图和控制面板按钮添加了一个QWeb模板。要了解更多关于QWeb模板的信息,请参考本章中的使用客户端QWeb模板内容。

   Odoo视图有大量的方法用于不同的目的;我们在本节中讨论了最重要的一个。如果你想了解更多关于视图的信息,你可以通过/addons/web/static/src/js/views/目录进一步了解它们。这个目录还包括抽象model、controller、renderer和view代码。

  在步骤9中,我们在资源中添加了JavaScript文件。最后,在最后两个步骤中,我们为book.library模型添加了一个视图定义。在步骤10中,我们为视图使用了<m2m_group>标记,并且我们还传递了m2m_field属性作为选项。它将被传递给模型以从服务器获取数据

有更多的…

  如果不想引入新的视图类型,而只想修改视图中的一些内容,则可以在视图上使用js_class。例如,如果我们想要一个类似于我们创建的看板视图的视图,那么我们可以扩展它如下:

var CustomRenderer = KanbanRenderer.extend({
...
});
var CustomRendererModel = KanbanModel.extend({
...
});
var CustomRendererController = KanbanController.extend({
...
});
var CustomDashboardView = KanbanView.extend({
  config: _.extend({}, KanbanView.prototype.config, {
  Model: CustomDashboardModel,
  Renderer: CustomDashboardRenderer,
  Controller: CustomDashboardController,
}),
});
var viewRegistry = require('web.view_registry');
viewRegistry.add('my_custom_view', CustomDashboardView);

然后我们可以使用js_class的看板视图(注意,服务器仍然认为这是一个看板视图):

...
<field name="arch" type="xml">
<kanban js_class="my_custom_view">
...
</kanban>
</field>
...

调试客户端代码

对于客户端部分,您将在本内容中入门。

  准备

  此内容实际上并不依赖于特定的代码,但如果您希望能够准确地重现所发生的事情,请获取上一个内容的代码。

  怎么做呢?

由于断点会使执行暂停,因此在调试时很有可能不会发生由计时问题引起的错误。我们稍后会讨论一些策略:

对于客户端调试,您需要使用资产激活调试模式。如果你不知道如何激活调试模式的资产,激活Odoo开发工具的秘诀从第1章,安装Odoo开发环境。

在你感兴趣的JavaScript函数中,调用debugger:

    debugger;

如果你有时间问题,登录到控制台通过JavaScript函数:

    console.log(“I'm in function X current”);

如果你想在模板渲染过程中调试,可以从QWeb调用调试器:

    <t t-debug ="" />;

您也可以使用QWeb登录到控制台,如下所示:

    <t t-log=“myvalue” />

虽然所有主流浏览器都能做到这一点,但出于演示目的,我们在这里只讨论Chromium。要使用调试工具,请点击右上方的菜单按钮,选择更多工具|开发工具:

  它是如何工作的…

  当调试器打开时,你应该看到类似下面的截图:

在前面的屏幕截图中,当前活动的选项卡是JavaScript调试器,我们在第31行中通过单击行号设置断点。每次我们的小部件获取用户列表时,执行应该在这一行停止,调试器将允许您检查变量或更改它们的值。在右边的观察列表中,您还可以调用函数来测试它们的效果,而不必连续地保存脚本文件并重新加载页面。 

然后,执行将停止,浏览器将切换到Sources选项卡,打开有问题的文件,并突出显示调试器语句所在的行。

  前面的两种日志记录可能会在Console选项卡中结束。无论如何,这是出现问题时您应该检查的第一个选项卡,因为如果一些JavaScript代码由于语法错误或类似的基本问题而根本没有加载,您将在那里看到一条错误消息,解释发生了什么。 

  有更多的…

  使用Elements选项卡检查浏览器当前显示的页面的DOM表示。当您熟悉现有小部件生成的HTML代码时,这将是很有帮助的,而且它还允许您处理类和CSS属性。这是测试布局变化的一个很好的资源。

这在调试缓慢的页面加载时很有帮助,因为在Network选项卡中,您通常可以找到请求的详细信息。如果您选择了一个请求,您可以检查传递给服务器的有效负载和返回的结果,这将帮助您找出客户端出现意外行为的原因。您还将看到请求的状态代码(例如404),以防由于文件名拼写错误而找不到资源。

 


通过toure的提高引导流程

Odoo框架包括一个内置的tour manager。有了这个tour manager,您可以指导最终用户学习特定的流程。在这个内容中,我们将创建一个旅行,这样我们就可以在图书馆中创建一本书。

准备

漫游只显示在没有演示数据的数据库中,因此如果您使用的数据库有演示数据,请为内容创建一个没有演示数据的新数据库。

怎么做呢?

  要向图书馆添加游览,请遵循以下步骤:

1.添加一个新的/static/src/js/my_library_tour.js文件,代码如下:

 

odoo.define('my_library.tour', function (require) {
"use strict";


var core = require('web.core');
var tour = require('web_tour.tour');

var _t = core._t;

tour.register('library_tour', {
    url: "/web",
    rainbowManMessage: _t("Congrats, you have listed a book."),
    sequence: 5,
    }, [tour.stepUtils.showAppsMenuItem(), {
        trigger: '.o_app[data-menu-xmlid="my_library.library_base_menu"]',
        content: _t('Manage books and authors in <b>Library app</b>.'),
        position: 'right'
    }, {
        trigger: '.o_list_button_add',
        content: _t("Let's create new book."),
        position: 'bottom'
    }, {
        trigger: 'input[name="name"]',
        extra_trigger: '.o_form_editable',
        content: _t('Set the book title'),
        position: 'right',
    }, {
        trigger: '.o_form_button_save',
        content: _t('Save this book record'),
        position: 'bottom',
    }
]);

});

2.在后端资产中添加tour JavaScript文件:

 

<script type="text/javascript" src="/my_library/static/src/js/my_library_tour.js" />

  

更新模块和打开Odoo后端。此时,您将看到旅程,如下面的截图所示:

 

它是如何工作的…

在第一步中,我们导入了web.tour_tour。然后,我们可以使用register()函数添加一个新的tour。我们使用library_tour名称注册了游览,并传递了该游览将在其上运行的URL。

一个巡回步骤需要三个值。触发器用于选择应该在其上显示tour的元素。这是一个JavaScript选择器。我们使用菜单的外部XML ID,因为它在DOM中可用。

SHOW_APPS_MENU_ITEM是主菜单指南中预定义的步骤。下一个键是内容,当用户将鼠标悬停在tour拖放上时将显示该内容。我们使用_t()函数是因为我们希望翻译字符串,而position键用于决定tour drop的位置。可能的值包括top、right、left或bottom。

当您在内部以测试模式运行Odoo时,它也会运行漫游,如果漫游没有完成,则会导致测试用例失败。

 


 

手机应用程序的JavaScript

 

它提供了一些小的实用程序来执行移动操作,如震动手机,显示吐司信息,扫描二维码,等等。

 

准备

 

当我们从移动应用程序中更改颜色字段的值时,我们将向您展示吐司。

 

  警告:Odoo手机应用程序只支持企业版,所以如果你没有企业版,你就不能测试它。

怎么做呢?

  按照以下步骤在Odoo手机应用程序中显示toast:

在field_widget.js中导入web_mobile.rpc:  

var mobile = require('web_mobile.core');

 

  2.修改clickPill方法,当用户从移动设备改变颜色时显示吐司:

clickPill: function (ev) {
        var $target = $(ev.currentTarget);
        var data = $target.data();
        this._setValue(data.val.toString());
        if (mobile.methods.showToast) {
            mobile.methods.showToast({ 'message': 'Color changed' });
        }
    }

 

  更新模块,在手机app中打开library.book模型的表单视图,改变颜色后会看到toast,如下图所示:

如何工作……

它暴露了一些基本的移动设备。在我们的示例中,我们使用showToast方法在移动应用程序中显示toast。我们还需要检查该函数的可用性。这背后的原因是一些移动电话可能不支持一些功能,例如,如果设备没有摄像头,那么您就不能使用scanBarcode()方法。在这种情况下,为了避免回溯,我们需要用if条件包装它们。

有更多的…

  在Odoo的移动公用设施如下:

  • show_Toast():显示祝酒词
  • vibr_ate():使电话震动
  • showSnackBar():显示带有按钮的小吃店
  • showNotification():显示移动通知
  • addContact():在电话簿中添加一个新联系人
  • scanB_arcode():扫描二维码
  • switchAccount():打开Android中的账户切换器