在本章中,我们将介绍以下内容:
- 创建自定义小部件
- 使用客户端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>
...
更新完成后,打开图书的表单视图,你会看到颜色选择器,如下图所示:
它是如何工作的…
为了让您理解我们的示例,让我们通过查看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。
有更多的…
您已经在本内容中使用了这些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>
更新后,你将会看到药丸的提示,如下截图所示:
它是如何工作的…
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()函数提供关于共同模型或域的信息。您还可以使用它来查询字段的string、size或在模型定义期间可以在字段上设置的任何其他属性。
另一个有用的属性是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.view和ir.actions.act_window.view中注册了一个新的视图类型,称为m2m_group。
在步骤3中,我们在基础(base)中添加了get_m2m_group_data方法。在基础中添加此方法将使该方法在每个模型中可用。这个方法将通过从JavaScript视图的RPC调用来调用。视图将传递两个参数—domain和m2m_field。在域参数中,域的值将是由搜索视图域和操作域组合生成的域。m2m_field是我们要根据它对记录进行分组的字段名。这个字段将在视图定义中设置。
一个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()方法。在我们的例子中,我们有创建公共_fetchData()方法来对数据进行RPC调用。注意,我们使用了步骤3中添加的get_m2m_group_data方法。控制器将调用get()方法以获取模型的状态。
Controller: Controller的角色是管理Model和Renderer之间的协调。当Renderer中出现action时,它将该信息传递给controller并执行action相应的行动。有时,它也会调用模型中的一些方法。除此之外,它还管理控制面板中的按钮。在我们的示例中,我们添加了一个按钮来添加新记录。为此,我们必须重写AbstractController的renderButtons()方法。我们还注册了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_r、renderer和model三元组。它将在MVC层次结构中设置它们。通常,它设置model、view和controller中需要的参数。在我们的示例中,我们希望m2m_field名称在模型中获得适当的分组数据,因此我们在其中设置了模型参数。同样,可以使用this.controllerParams和this.rendererParams来设置contr_oller和render_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中的账户切换器