在本章中,我们将介绍以下内容:
- 创建自定义小部件
- 使用客户端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中,我们导入了AbstractField和fieldRegistry。
在第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中添加一个新的视图类型:(\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‘)])
2. 在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‘)],
ondelete={‘m2m_group‘: ‘cascade‘})
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;
});
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.view和ir.actions.act_window.view中注册了一个新的视图类型,称为m2m_group。
在步骤3中,我们在基础(base)中添加了get_m2m_group_data方法。在基础中添加此方法将使该方法在每个模型中可用。这个方法将通过从JavaScript视图的RPC调用来调用。视图将传递两个参数—domain和m2m_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、Controller和View的角色。Model、Renderer、Controller和View的抽象版本拥有形成视图所需的所有基本内容。因此,在我们的示例中,我们已经通过继承创建了model、renderer、controller和view。
下面是一个关于创建视图的不同部分的深入解释:
Model: Model的角色是保存视图的状态。它向服务器发送一个RPC请求以获取数据,然后将数据传递给controller和renderer。然后覆盖load和reload方法。当视图被初始化时,它调用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.controllerParams和this.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。