odoo13学习---16 Web客户端开发

Posted ly-stranger

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了odoo13学习---16 Web客户端开发相关的知识,希望对你有一定的参考价值。

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

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

 

介绍

  Odoo的网络客户端,或后端,是员工花大部分时间的地方。在第10章后端视图中,你看到了如何使用后端提供的现有可能性。在这里,我们将看看如何扩展和定制这些可能性。web模块包含所有与Odoo用户界面相关的内容。

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

  企业版在社区web上提供了一些额外的特性,比如移动兼容性、可搜索菜单、材料设计等等。我们将在这里开发社区版。不用担心——社区开发的模块在企业版中工作得很好,因为web_enterprise内部依赖于社区web模块,只是添加了一些特性。

  在本章中,您将学习如何创建新的字段小部件来从用户那里获取输入。我们还将从头创建一个新视图。读完这一章后,你将能够在Odoo后端创建自己的UI元素。

  注意:Odoo的用户界面严重依赖于JavaScript。在本章中,我们将假设你有JavaScript, jQuery,Underscore.js和SCSS。

创建自定义小部件

  正如你在第10章后端视图中看到的,我们可以使用小部件以不同的格式显示特定的数据。例如,我们使用widget=‘image‘将二进制字段显示为图像。为了演示如何创建自己的小部件,我们将编写一个小部件,让用户选择一个整数字段,但我们将以不同的方式显示它。我们将显示一个颜色选择器,而不是一个输入框,这样我们就可以选择一个色号。

准备

  对于这个内容,我们将使用第4章中的my_library模块,创建Odoo附加模块。在这个内容中,我们将添加一个新的需要依赖于web模块的字段小部件。确保你已经在清单文件中添加了依赖onweb,像这样:

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

怎么做呢?

  我们将添加一个包含小部件逻辑的JavaScript文件和一个SCSS文件来进行一些样式化。然后,我们将在books表单上添加一个整数字段来使用我们的新小部件。按照以下步骤添加一个新的字段小部件: 

  1. 添加一个静态/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‘);
......

  2. 在field_widget.js文件中通过扩展AbstractField创建小部件:

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

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

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

  4. 为扩展的小部件捕获一些JavaScript事件:

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

  5. 重写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);
    },

  6. 覆盖_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,
        }));
    },

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

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());
    }

});

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

fieldRegistry.add(‘int_color‘, colorField);

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

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

  10. 在静态/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>

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

color = fields.Integer()

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

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

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

技术图片

它是如何工作的…

  为了让您理解我们的示例,让我们通过查看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。

 

有更多的…

 

  web.mixins名称空间定义了两个非常有用的mixin类,在开发表单widget时,您不应该错过这些类。您已经在本内容中使用了这些mixins。AbstractField是通过继承Widget类创建的,Widget类继承两个mixins。第一个是EventDispatcherMixin,它提供了一个用于附加和触发事件处理程序的简单接口。第二个是ServicesMixin,它为RPC调用和操作提供函数

  当您想要重写一个方法时,总是要研究基类,看看函数应该返回什么。bug的一个常见原因是忘记返回超级用户的deferred对象这会导致异步操作出现问题

  Widgets负责验证。使用isValid函数来实现这个方面的定制。


 

 

使用客户端QWeb模板

 

  正如用JavaScript编程创建html代码是一个坏习惯一样,您应该只在客户端JavaScript代码中创建最小数量的DOM元素。幸运的是,客户端也有模板引擎可用,更幸运的是,客户端模板引擎具有与服务器端模板相同的语法。

准备

  对于这个内容,我们将使用上一个内容中的my_library模块。我们将通过将DOM元素创建移动到QWeb来使其更加模块化。

怎么做呢?

  我们需要在清单中添加QWeb定义,并更改JavaScript代码以便使用它。按照以下步骤开始:

  1. 导入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;
...

  2. 将_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模式覆盖它。

 

它是如何工作的…

 

  在第15章“CMS网站开发”中,已经有了关于创建或修改模板的QWeb基础知识的全面讨论,我们将在这里重点讨论它的不同之处。首先,您需要认识到我们处理的是JavaScript QWeb实现,而不是服务器端的Python实现。这意味着你不能访问浏览记录或环境;您只能访问从qweb.render函数传递的参数。

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

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

有更多的…

  在这里努力使用QWeb的原因是可扩展性,这是客户端和服务器端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模版的资料,请参阅以下要点:

  与Odoo的其他部分相比,客户端QWeb引擎的错误消息和处理不太方便。一个小错误通常意味着什么都没有发生,初学者很难从那里继续下去。

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


 

 对服务器进行RPC调用

  您的小部件迟早需要从服务器查找一些数据。在这个内容中,我们将在颜色药片上添加一个工具提示。当用户将光标悬停在color pill元素上时,工具提示将显示与该颜色相关的书籍数量。

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

准备

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

怎么做呢?

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

  1. 在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>

  更新模块以应用更改。更新后,你将会看到药丸的提示,如下截图所示:

技术图片

它是如何工作的…

 

  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已经完成之后。

 

  第二步没什么特别的。我们刚刚初始化了引导工具提示。 

 

  在第3步中,我们使用colorGroupData设置显示工具提示所需的属性。在willStart方法中,我们通过this.colorGroupData分配了一个颜色映射,这样您就可以通过widget.colorGroupData在QWeb模板中访问它们。这是因为我们传递了小部件引用;这是qweb.render方法。

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

有更多的…

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

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

另请参阅 

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

  Odoo的RPC依赖于jQuery的延迟对象,所以它是一个异步函数。您应该学习延迟对象以完全理解JavaScript中的RPC调用。你可以了解更多关于延期的知识jQuery文档中的对象,地址:https://api.query.com/jquery.deferred。


 

创建一个新视图

  正如您在第10章后端视图中看到的,有不同种类的视图,如表单、列表、看板等。在这个内容中,我们将创建一个全新的视图。这个视图将显示作者列表以及他们的书籍。

准备

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

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

怎么做呢?

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

  1. 在ir.ui.view中添加一个新的视图类型:(modelsir_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)])

  2. 在ir.actions.act_window.view中添加一个新的视图模式:(modelsir_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)],
        ondelete={m2m_group: cascade})

 3.通过继承基模型添加新方法(modelsmodel.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;

});

    8. 将视图的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>

   9. 添加所有的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>

  更新my_library模块以打开图书视图,然后从视图切换器打开我们刚刚添加的新视图。这看起来如下:

   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是我们要根据它对记录进行分组的字段名。这个字段将在视图定义中设置。 

 

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

技术图片

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

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

  Model: Model的角色是保存视图的状态它向服务器发送一个RPC请求以获取数据,然后将数据传递给controller和renderer然后覆盖loadreload方法。当视图被初始化时,它调用load()方法来获取数据,当搜索条件被更改并且视图需要一个新的状态时,就调用reload()方法。在本例中,我们创建了common _fetchData()方法来对数据进行RPC调用。注意,我们使用了在步骤3中添加的get_m2m_group_data方法。将从controller调用get()方法来获取模型的状态

  Controller: Controller的角色是管理Model和Renderer之间的协调。当Renderer中出现一个操作时,它将该信息传递给控制器并执行该操作相应的行动。有时,它还调用Model中的一些方法。除此之外,它还管理控制面板中的按钮。在我们的示例中,我们添加了一个按钮来添加新记录。为此,我们必须重写AbstractController的renderButtons()方法我们还注册了custom_events,这样当单击author card中的按钮时,渲染器将向控制器触发事件,使其执行操作。

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

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

  View: View的角色是获取构建视图所需的所有基本内容,比如一组字段、一个上下文、一个视图和一些其他参数。在这之后,视图将初始化controller、renderer和model三元组。它将在MVC层次结构中设置它们。通常,它设置model、view和controller中所需的参数。在我们的示例中,我们希望m2m_field名称在Model中获得适当的分组数据,因此我们在其中设置了模型参数。同样,this.controllerParamsthis.rendererParams可以用来设置在controlller和renderer中的参数。

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

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

调试客户端代码

  为了调试服务器端代码,本书包含了一个完整的章节,即第8章,调试。对于客户端部分,您将在本内容中入门。

  准备

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

  怎么做呢?

  使调试客户端脚本变得困难的是web客户端严重依赖于jQuery的异步事件。由于断点会使执行暂停,因此在调试时很有可能不会发生由计时问题引起的错误。我们稍后会讨论一些策略:

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

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

    debugger;

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

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

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

    <t t-debug ="" />;

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

    <t t-log=“myvalue” />

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

  它是如何工作的…

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

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

  当您打开开发人员工具时,我们前面描述的调试器语句将具有相同的行为。然后,执行将停止,浏览器将切换到Sources选项卡,打开有问题的文件,并突出显示调试器语句所在的行。

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

  有更多的…

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

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

 


通过toure的提高引导流程

   在开发一个大型应用程序之后,向最终用户解释软件流是至关重要的。Odoo框架包括一个内置的tour manager。有了这个tour manager,您可以指导最终用户学习特定的流程。在这个内容中,我们将创建一个旅行,这样我们就可以在图书馆中创建一本书。

准备

  我们将使用上一个菜谱中的my_library模块。漫游只显示在没有演示数据的数据库中,因此如果您使用的数据库有演示数据,请为内容创建一个没有演示数据的新数据库。

怎么做呢?

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

怎么做呢?

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

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后端。此时,您将看到旅程,如下面的截图所示:

 

它是如何工作的…

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

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

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

  导览改善了用户的入职体验,并管理了集成测试。当您在内部以测试模式运行Odoo时,它也会运行漫游,如果漫游没有完成,则会导致测试用例失败。

 


 

手机应用程序的JavaScript

 

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

 

准备

 

  我们将使用前一个库中的my_library模块。当我们从移动应用程序中更改颜色字段的值时,我们将向您展示吐司。

 

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

怎么做呢?

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

  1. 在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,如下图所示:

如何工作……

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

有更多的…

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

  • show_Toast():显示祝酒词
  • vibr_ate():使电话震动
  • showSnackBar():显示带有按钮的小吃店
  • showNotification():显示移动通知
  • addContact():在电话簿中添加一个新联系人
  • scanB_arcode():扫描二维码
  • switchAccount():打开android中的账户切换器
  • 要了解移动JavaScript的更多信息,请参考https://www.odoo.com/documentation/12.0/reference/mobile.html。

 

 

 

 

 

 

 

 

 

 









以上是关于odoo13学习---16 Web客户端开发的主要内容,如果未能解决你的问题,请参考以下文章

odoo开发学习 -- odoo13 Docker镜像制作

odoo开发学习 -- odoo13 Docker镜像制作

odoo开发学习 --修改odoo12代码,允许跨域访问

将自定义模块从 Odoo 13 迁移到 15

《odoo快速入门与实战》的在线开发在 13版 与 11版 的问题与解决

Odoo 模块的升级:11,12升级13