0%

Odoo自定义视图教程

我们在Odoo开发时基本都会对模型定义相关视图,其中常常用到的有form,tree,kanban,另外还有calendar,pivot,graph等视图,可以说视图是Odoo很重要的一个组成部分。此外有时视图自带的功能无法满足需求时,我们还需要尝试去对视图做自定义扩展,所以适当的了解视图的背后的运行机制可以让我们更从容、高效的面对视图开发。

这篇教程中我会介绍如何定义视图,视图的基本运行流程,一些主要属性以及实战部分。为了避免篇幅过长,一些在 Odoo 中添加自定义dashboard页面已经讲解过相同功能点,这篇教程中我不再作讲解,如果读者学习本篇教程感觉困难,那么可以先阅读自定义dashboard的教程。

Prerequisite

本教程基于以下环境开发:

  • 系统: windows wsl - Ubuntu 18.04
  • Odoo: Nightly Odoo 构建的post-20200101 12.0 版本
  • 数据库: PostgreSQL 10.11

本教程中的示例代码可以从https://github.com/findsomeoneyys/odoo-custom-view-tutorial中获取,仓库中的每个tag对应一个章节结束后的完整代码,读者可以通过类似以下方式来自由切换到不同章节代码。

1
git checkout v0.1

定义基本模型

可以通过git checkout v0.1查看本章节的完整代码

为了方便展示新视图,我们需要建立基本的模型,视图,和默认数据,这里我建了个Game模型,包含名称,下载量和平台字段。

1
2
3
4
5
6
7
8
9
10
11
# -*- coding: utf-8 -*-
from odoo import models, fields, api


class Game(models.Model):
_name = 'echart_views.game'
_description = 'Games'

name = fields.Char('游戏名', required=True)
downloads = fields.Integer(string='下载量', default=0)
platform = fields.Char(string='平台')

安装上模块后,便可看到基本的视图。
alt

定义新视图

可以通过git checkout v0.2查看本章节的完整代码

定义一个新视图的操作量比较大,我们需要给odoo的python代码中增加新视图类型与视图模式,其次我们还需要定义js相关文件和模板代码。

让Odoo识别新视图类型

首先我们在model下建立两个文件ir_action_act_window.pyir_ui_view.py,然后加入相关代码,这是为了odoo可以识别我们新定义的视图tag,如果没有这部分代码,在加载相关的xml文件会报错并提示你odoo没有这种类型视图。
这里我把我的新视图命名为eview

1
2
3
4
5
6
7
8
9
10
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=[('eview', 'echart views')])
1
2
3
4
5
6
7
8
9
10
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=[('eview', 'echart views')])

同时也别忘了在models/__init__.py中加入新增的class

1
2
3
4
5
# -*- coding: utf-8 -*-

from . import models
from . import ir_ui_view
from . import ir_action_act_window

增加视图所需js文件

Odoo的视图中的底层实现已经将相关功能抽象成几个部分,所以我们只需要继承并实现Odoo为我们预留好的逻辑即可, 一个完整的视图是由view, controller, model, renderer这几个组件组成的。Odoo的视图的实现使用了MVC设计模式,它们之间的关系如下图所示:

alt

其中需要注意的是MVC设计模式在视图中实际对应controller, model, renderer(MRC),这是因为View在Odoo中有特殊的历史含义(也就是我们提到的展示数据的一种视图类型)。在这几部分中,View更多充当一个入口的角色,类似后端的路由。

现在我们增加相关js文件与实现逻辑,同时我会讲解各个组件的相关生命周期函数。这里需要注意的是,相关代码注释中如果上面包含@returns {Deferred},则需要返回一个Deferred对象,这是因为odoo是通过这种方式来增加相关函数的回调执行,如果不返回Deferred对象,有时会产生程序错误,大部分的时候我们只需加上return this._super.apply(this, arguments)或者$.when()即可。

实现Controller

Odoo对于Controller部分抽象出web.AbstractController,所以我们只需继承这个类并填写相关逻辑。
static/src/js新增eview_controller.js文件,并键入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
odoo.define('echart_views.Controller', function (require) {
'use strict';

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

var EchartController = AbstractController.extend({
init: function (parent, model, renderer, params) {
console.log("eview controller >>> init");
this._super.apply(this, arguments);
},
/**
* @returns {Deferred}
*/
start: function() {
console.log("eview controller >>> start");
return this._super();
},
// 该方法会生成导航栏中的按钮,并可增加绑定按钮事件
renderButtons: function ($node) {
console.log("eview controller >>> renderButtons");
this._super.apply(this, arguments);
},
/**
* 执行该方法重新加载视图,默认逻辑是对调用update的封装
* @param {Object} [params] This object will simply be given to the update
* @returns {Deferred}
*/
reload: function (params) {
console.log("eview controller >>> reload");
return this._super.apply(this, arguments);
},
/**
* update是Controller的关键方法,在Odoo默认逻辑中,当用户操作搜索视图,或者部分内部更改会主动调用该方法。
* 当我们自行编写相关方法时需要主动调用该函数。
* 这个方法会调用model重新加载数据并通知renderer执行渲染
* @param {*} params
* @param {*} options
* @param {boolean} [options.reload=true] if true, the model will reload data
*
* @returns {Deferred}
*/
update: function (params, options) {
console.log("eview controller >>> update");
return this._super.apply(this, arguments);
},
/**
* _update是update的回调方法,区别在于update是重新渲染页面主体部分,
* _update则是渲染除了主体部分外的组件,比如控制面板中的组件 (buttons, pager, sidebar...)
* @param {*} state
* @returns {Deferred}
*/
_update: function (state) {
console.log("eview controller >>> _update");
return this._super.apply(this, arguments);
},


});

return EchartController;

});

实现Model

同样的,Model部分对应的抽象类是web.AbstractModel, Model是挂在Controller的一个对象,所有数据相关的部分都需要通过它来处理,这部分的主要逻辑很简单,只需要实现getload方法,通过rpc等方式向后台请求数据,将数据结果保存在对象上比如this.data=result,然后在get方法中返回this.data即可。

static/src/js新增eview_model.js文件,并键入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
odoo.define('echart_views.Model', function (require) {
'use strict';

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

var EchartModel = AbstractModel.extend({
/**
* 该方法需要返回renderer所需的数据
* 数据可以通过load/reload执行相关获取数据方法时,设置到该对象上
*/
get: function () {
console.log("eview model >>> get");
this._super();
},
/**
* 只会初次加载时执行一次,需要自定义相关数据获取方法获取数据并设置到该对象上
*
* @param {Object} params
* @param {string} params.modelName the name of the model
* @returns {Deferred} The deferred resolves to some kind of handle
*/
load: function (params) {
console.log("eview model >>> load");
return this._super.apply(this, arguments);
},
/**
* 当有相关数据变动时,重新获取数据。
*
* @param {Object} params
* @returns {Deferred}
*/
reload: function (handle, params) {
console.log("eview model >>> reload");
return this._super.apply(this, arguments);
},
});

return EchartModel;

});

实现Renderer

Renderer部分对应的抽象类是web.AbstractModel,renderer只需关注拿到数据并渲染页面即可,其中this.state对应的是Modelget方法获取的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
odoo.define('echart_views.Renderer', function (require) {
'use strict';

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

var qweb = core.qweb;

var EchartRenderer = AbstractRenderer.extend({
init: function (parent, state, params) {
console.log("eview renderer >>> init");
this._super.apply(this, arguments);

},
/**
* renderer的渲染逻辑部分,自行渲染相关数据并插入this.$el中
*
* @abstract
* @private
* @returns {Deferred}
*/
_render: function () {
console.log("eview renderer >>> _render");
var content = $("<div><p> eview </p></div>");
this.$el.append(content);
return this._super.apply(this, arguments);
},
});

return EchartRenderer;

});

实现View

View对应的是web.AbstractView抽象类,是View的函数入口,它包含视图的基本定义信息,同时会根据传入的视图结构信息,相关参数初始化controller, model, renderer,当初始化controller完毕后,页面之后的相关处理都与这个类无关了。

static/src/js新增eview_view.js文件,并键入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
odoo.define('echart_views.View', function (require) {
'use strict';

var AbstractView = require('web.AbstractView');
var view_registry = require('web.view_registry');
var Controller = require('echart_views.Controller');
var eViewModel = require('echart_views.Model');
var eViewRenderer = require('echart_views.Renderer');


var EchartView = AbstractView.extend({
display_name: 'EchartView',
icon: 'fa-bar-chart',
cssLibs: [
],
jsLibs: [
],
config: {
Model: eViewModel,
Controller: Controller,
Renderer: eViewRenderer,
},
viewType: 'eview',
groupable: false,
/**
* View的入口,会传入相关视图定义的参数(视图结构,字段信息等。。),
* 函数会处理并生产3个主要字段:this.rendererParams, this.controllerParams,this.loadParams
* 分别对应renderer,controller,model的初始化参数,我们可以根据需要自行对相关增加相关参数
* @param {Object} viewInfo.arch
* @param {Object} viewInfo
* @param {Object} viewInfo.fields
* @param {Object} viewInfo.fieldsInfo
* @param {Object} params
* @param {string} params.modelName The actual model name
* @param {Object} params.context
*/
init: function (viewInfo, params) {
console.log("eview view >>> init");
this._super.apply(this, arguments);
},
/**
* View的主要的执行逻辑,这个方法会分别执行getModel,getRenderer初始化相关组件,
* 然后对renderer, model设置controller就完成了作用,之后的View相关操作与这个类无关了
* @param {}} parent
*/
getController: function (parent) {
console.log("eview view >>> getController");
return this._super.apply(this, arguments);
},
// 这里会初始化model,并执行model中load方法
getModel: function (parent) {
console.log("eview view >>> getModel");
return this._super.apply(this, arguments);
},
getRenderer: function (parent, state) {
console.log("eview view >>> getRenderer");
return this._super.apply(this, arguments);
},

});

view_registry.add('eview', EchartView);

return EchartView;

});

加载资源与添加新视图

js部分实现后,我们需要把相关文件加载进odoo中,在views目录下新建文件templates.xml并添加相关代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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="/echart_views/static/src/js/eview_view.js" />
<script type="text/javascript" src="/echart_views/static/src/js/eview_model.js" />
<script type="text/javascript" src="/echart_views/static/src/js/eview_controller.js" />
<script type="text/javascript" src="/echart_views/static/src/js/eview_renderer.js" />

</xpath>
</template>

</odoo>

然后在__manifest__.py中引入该文件,最后在views.xmlact_window添加我们的新视图模式,以及我们的新视图定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<record id='echart_views_game_action' model='ir.actions.act_window'>
<field name="name">Games</field>
<field name="res_model">echart_views.game</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,eview</field>
</record>

...

<!-- eview View -->
<record id="echart_views_game_view_eview" model="ir.ui.view">
<field name="name">Game echart view</field>
<field name="model">echart_views.game</field>
<field name="arch" type="xml">
<eview>
<field name="name"/>
<field name="downloads"/>
<field name="platform"/>
</eview>
</field>
</record>

小结

完成以上步骤后,重启Odoo并更新模块,打开debug=assets模式并进入视图,我们即可看到新增的视图效果与组件和相关函数的加载顺序了。
alt

实战

在前面的教程中我们了解到了views的组件初始化与生命周期函数,这也意味着我们可以在相关周期函数中加入自己的一系列事件,来实现我们自己独特的视图。
在接下来的章节中我会逐步实现加入视图模板,解析视图字段,事件绑定与处理等功能来实现一个基于echart的自定义饼图,这个视图中我们可以左上角自由切换定义在xml中的字段,饼图中则会统计该字段在数据库的全部数据:如果字段是数值,根据Name自动分类叠加,如果字段是字符串,则对该字段分组统计数量。

自定义模板与按钮事件绑定

可以通过git checkout v0.3查看本章节的完整代码

在 Odoo 中添加自定义dashboard页面中的模板渲染流程一样,首先我们在eview_view.jsjsLibs中加上echart。

1
2
3
4
5
...

jsLibs: [
'https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js',
],

接着在static/src/xml新增qweb_template.xml文件并增加模板代码,同时在__manifest__.py中引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
qweb_template.xml

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

<t t-name="echart_views.page">
<div class="container-fluid mt-3">
<div id="app" class="mt-2" style="width: 800px;height:500px;">
<p>echart</p>
</div>
</div>
</t>

</templates>
1
2
3
4
5
__manifest__.py

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

然后在eview_renderer.js中做相关处理即可,这里我直接根据echart-饼图演示实现相关功能。

init中加入option参数,同时在_render中渲染模板并初始化echart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
init: function (parent, state, params) {
console.log("eview renderer >>> init");
this._super.apply(this, arguments);
this.echart_option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 10,
data: ['直接访问', '邮件营销', '联盟广告', '视频广告', '搜索引擎']
},
series: [
{
name: '访问来源',
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
label: {
normal: {
show: false,
position: 'center'
},
emphasis: {
show: true,
textStyle: {
fontSize: '30',
fontWeight: 'bold'
}
}
},
labelLine: {
normal: {
show: false
}
},
data: [
{value: 335, name: '直接访问'},
{value: 310, name: '邮件营销'},
{value: 234, name: '联盟广告'},
{value: 135, name: '视频广告'},
{value: 1548, name: '搜索引擎'}
]
}
]
};


},
_render: function () {
console.log("eview renderer >>> _render");
this.$el.empty();
this.$el.append(qweb.render('echart_views.page'));
var el = this.$el.find('#app')[0];
var myChart = echarts.init(el);
myChart.setOption(this.echart_option);
return this._super.apply(this, arguments);
},

更新模板并刷新页面,再次打开eview时,我们就会看到一个饼图:
alt

接着我们为导航栏增加按钮,视图导航栏的按钮就是类似tree视图中创建、导入等按钮,通过重写Controller中的renderButtons方法便可轻松实现。
我们继续在qweb_template.xml中新增按钮组的模板代码

1
2
3
4
5
6
7
8
9
10
11
<t t-name="echart_views.buttons">
<div class="btn-group" role="toolbar" aria-label="Main actions">
<button class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
统计字段
</button>
<div class="dropdown-menu o_echart_measures_list" role="menu">
<a class="dropdown-item" href="#" data-field="name">名字</a>
<a class="dropdown-item" href="#" data-field="downloads">下载量</a>
</div>
</div>
</t>

eview_controller.js中修改renderButtons函数,渲染按钮组并为它们绑定事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
renderButtons: function ($node) {
console.log("eview controller >>> renderButtons");
this._super.apply(this, arguments);
this.$buttons = $(qweb.render('echart_views.buttons'));
this.$measureList = this.$buttons.find('.o_echart_measures_list');
this.$buttons.click(this._onButtonClick.bind(this));
this.$buttons.appendTo($node);
},

....

_onButtonClick: function (event) {
var $target = $(event.target);
var field;
if ($target.parents('.o_echart_measures_list').length) {
event.preventDefault();
event.stopPropagation();
field = $target.data('field');
_.each(this.$measureList.find('.dropdown-item'), function (item) {
var $item = $(item);
$item.toggleClass('selected', $item.data('field') === field);
});
}
},

再次进入页面,我们可以发现导航栏部分多了 ‘统计字段’下拉按钮,点击相关选项,按钮组就会处于激活状态
alt

获取视图定义结构信息

可以通过git checkout v0.4查看本章节的完整代码

刚刚我们的例子中按钮组的数组是写死的,现在我们来做一些改动,把这部分修改成根据我们定义在xml的字段动态生成下拉菜单。

在我们使用Odoo原生xml定义的field中,是可以自定义添加属性,并且odoo对应会有不一样的行为,比如加上invisible=1时,该字段在视图中会自动隐藏,现在我们也为eview视图中做一些类似的自定义属性处理,我们增加一个type属性,type="name"代表这个字段是记录的显示名字,type="measure"代表这个字段是可加入我们按钮组的下拉菜单中。

现在我们打开views/views.xml,修改eview的视图,为field加上type属性, 此外 也为eview加上一个chart="bar"属性

1
2
3
4
5
6
7
8
9
10
11
<record id="echart_views_game_view_eview" model="ir.ui.view">
<field name="name">Game echart view</field>
<field name="model">echart_views.game</field>
<field name="arch" type="xml">
<eview chart="bar">
<field name="name" type="name"/>
<field name="downloads" type="measure"/>
<field name="platform" type="measure"/>
</eview>
</field>
</record>

之前提的介绍中提到过视图的结构信息都是会传入View的init方法中,其中this.arch包含Odoo的已经为我们解析好的视图结构化数据,this.fields则包含对应模型中全部字段的信息(包括魔法字段),在debug=assets控制台打断点输出,我们可以轻松看到完整的结构。
alt

知道了数据结构后剩下的事就简单多了,我们自定义三个参数displayNameField, measure, measures,分别表示哪个字段对应记录的显示名称,视图当前所选择的统计字段,统计字段的所对应字段定义信息。其中measure, measures会在下章节中使用到。
现在我们回到eview_view.js,修改init方法为如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
init: function (viewInfo, params) {
console.log("eview view >>> init");
this._super.apply(this, arguments);

var self = this;
var displayNameField;
var measure;
var measures = {};

this.arch.children.forEach(function (field) {
var fieldName = field.attrs.name;
if (field.attrs.type === 'measure') {
if (!measure) {
measure = fieldName;
}
measures[fieldName] = self.fields[fieldName];
} else if(field.attrs.type === 'name') {
displayNameField = fieldName;
}
});

this.controllerParams.measures = measures;
},

这段代码中最后一句this.controllerParams.measures = measures;代表我们为Controller的初始参数中添加measures属性,这样我们可以在Controller获取到
measures数据,到时就可使用这部分数据来渲染模板。
接着我们打开eview_controller.jsinit中接收measures字段,并在renderButtons使用这部分数据渲染视图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
init: function (parent, model, renderer, params) {
console.log("eview controller >>> init");
this._super.apply(this, arguments);
this.measures = params.measures;
}

....

renderButtons: function ($node) {
console.log("eview controller >>> renderButtons");
this._super.apply(this, arguments);
var context = {
measures: _.sortBy(_.pairs(this.measures), function (x) {
return x[1].string.toLowerCase();
}),
};
this.$buttons = $(qweb.render('echart_views.buttons', context));

.....
},

最后打开xml/qweb_template.xml,将下拉选项部分改成模板语法渲染

1
2
3
4
5
<div class="dropdown-menu o_echart_measures_list" role="menu">
<t t-foreach="measures" t-as="measure">
<a role="menuitem" href="#" class="dropdown-item" t-att-data-field="measure[0]"><t t-esc="measure[1].string"/></a>
</t>
</div>

此时重启Odoo并更新模块,再次进入视图我们可以发现统计字段的选项改变了,我们也可以自行尝试在xml中去除相关field,统计字段的选项也会对应动态改变。

通过Model在页面中传递数据

可以通过git checkout v0.5查看本章节的完整代码

在视图操作用经常会改变数据,数据改变后我们需要及时处理相关数据并更新视图,在这章里我们将改进按钮组的相关处理,当点击选项时,数据会更新到model中并实时更新我们的视图页面。

回到eview_view.js,在init末尾加上model的初始参数

1
2
3
4
5
6
7
8
init: function (viewInfo, params) {

...

this.loadParams.measure = measure;
this.loadParams.measures = measures;
this.loadParams.displayNameField = displayNameField || 'display_name';
},

然后打开eview_model.js,在loadreload的方法中增加获取相关字段值逻辑,同时修改get方法为返回相关字段数据,这里返回数据的部分设置measureString字段来返回对应字段的定义名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
get: function () {
console.log("eview model >>> get");
var measureString = this.measures[this.measure]['string'];
return {measure: this.measure, measureString: measureString};
},
load: function (params) {
console.log("eview model >>> load");
this.measure = params.measure;
this.measures = params.measures;
this.displayNameField = params.displayNameField;
return this._super.apply(this, arguments);
},
reload: function (handle, params) {
console.log("eview model >>> reload");
if ('measure' in params) {
this.measure = params.measure;
}
return this._super.apply(this, arguments);
},

同时我们要修改Controller的逻辑,当有数据变动时,我们需要通过调用update方法来更新数据,update会自动代入参数调用model中的reload方法,
同时,触发视图的_render方法重新渲染数据。现在我们稍微修改下_onButtonClick的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
_onButtonClick: function (event) {
var $target = $(event.target);
var field;
if ($target.parents('.o_echart_measures_list').length) {
event.preventDefault();
event.stopPropagation();
field = $target.data('field');
this._setMeasure(field);
}
},
_setMeasure: function (measure) {
var self = this;
this.update({measure: measure}).then(function () {
self._updateButtons();
});
},
_updateButtons: function () {
if (!this.$buttons) {
return;
}
var state = this.model.get();
_.each(this.$measureList.find('.dropdown-item'), function (item) {
var $item = $(item);
$item.toggleClass('selected', $item.data('field') === state.measure);
});
},

这里把逻辑拆成了两个方法,一个是_updateButtons,他会通过model.get()来获取当前的数据,然后激活下拉菜单的选项状态,另外一个是_setMeasure
这个方法的逻辑也很简单,就是对update的一个封装。此外我们再把_updateButtons方法也放在renderButtons中调用下,这样初次加载视图时也会有默认的选项激活状态

1
2
3
4
5
renderButtons: function ($node) {
...
this._updateButtons();
this.$buttons.appendTo($node);
},

最后我们要修改下renderer里面的_render方法,根据model里面的数据来渲染页面。我们为option增加个title属性,并在_render方法中设置这个属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
this.echart_option = {
...
title: {
text: '',
left: 'center',
top: 20,
textStyle: {
color: '#ccc'
}
},
};

....

_render: function () {
console.log("eview renderer >>> _render");
this.$el.empty();

this.echart_option.title.text = this.state.measureString;
....
},

Odoo会自动把从model.get()的数据放到this.state中,直接获取即可。

刷新页面,此时我们我们可以看到点击下拉选项时,页面会刷新,同时上方标题属性会显示对应字段的定义名。
alt

在Model向后台请求数据

可以通过git checkout v0.6查看本章节的完整代码

到目前为止,我们基本完成了视图的基本功能了,接下来我们要增加model的逻辑,向后台获取再渲染显示。在eview_model.js中新增一个_fetchData方法获取数据,同时在其他需要获取数据的方法中调用这个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
get: function () {
console.log("eview model >>> get");
return this.data;
},
load: function (params) {
console.log("eview model >>> load");
this.modelName = params.modelName;
this.domain = params.domain || [];
this.measure = params.measure;
this.measures = params.measures;
this.displayNameField = params.displayNameField;
return this._fetchData();
},
reload: function (handle, params) {
console.log("eview model >>> reload");
if ('measure' in params) {
this.measure = params.measure;
}
if ('domain' in params) {
this.domain = params.domain;
}
return this._fetchData();
},
_fetchData: function () {
var self = this;
var measureFieldInfo = this.measures[this.measure];
var measureString = measureFieldInfo['string'];
var seriesLegend = [];
if (measureFieldInfo.type === 'integer') {
return this._rpc({
model: this.modelName,
method: 'search_read',
domain: this.domain,
fields: [this.measure, this.displayNameField],
}).then(function (result) {
var seriesData = _.map(result, function (data) {
return {value: data[self.measure], name: data[self.displayNameField]}
});
_.each(seriesData, function (d) {
seriesLegend.push(d['name']);
});
self.data = {seriesData: seriesData, measureString: measureString, measure: self.measure, seriesLegend: seriesLegend};
});
} else {
return this._rpc({
model: this.modelName,
method: 'search_read',
domain: this.domain,
fields: [this.measure],
}).then(function (result) {
var resGroupAndCount = _.pairs(_.countBy(result, function(o){return o[self.measure]}));
var seriesData = _.map(resGroupAndCount, function (data) {
return {value: data[1], name: data[0]}
});
_.each(seriesData, function (d) {
seriesLegend.push(d['name']);
});
self.data = {seriesData: seriesData, measureString: measureString, measure: self.measure, seriesLegend:seriesLegend};
});
}
},

这段代码中在初始化数据中加入Odoo参数中的模型名modelName,和搜索框的内容domain,之后通过_rpc方法调用模型自带的search_read获取字段数据,接着在根据字段类型进行分类统计把数据归纳出来放入self.data中。
之后在eview_renderer.js里把optiondata部分删除,接着和上个章节一样,在_renderer设置相关字段。

1
2
3
4
5
6
...
this.echart_option.title.text = this.state.measureString;
this.echart_option.series[0].name = this.state.measureString;
this.echart_option.series[0].data = this.state.seriesData;
this.echart_option.legend.data = this.state.seriesLegend;
...

完成后刷新页面再次进入视图,点击选项Odoo会自动从后台获取对应字段的数据,同时右上角的搜索框我们也可以自由输入数据过滤结果。
alt

让Crontroller处理组件自定义事件

可以通过git checkout v0.7查看本章节的完整代码

在之前的绑定点击事件是指针对按钮组的,实际上当renderer为我们渲染好页面的时候,我们也会有要处理页面相关事件的要求,这个实际上也很简单:
qweb_template.xml中div上方我们新增一个按钮:

1
2
3
4
5
6
<div class="container-fluid mt-3">
<button class="btn btn-primary ml-2" id="reloadView">重新加载</button>
<div id="app" class="mt-2" style="width: 800px;height:500px;">
<p>echart</p>
</div>
</div>

然后在eview_renderer.js中加入事件注册和相关处理函数:

1
2
3
4
5
6
7
8
9
10
11
events: _.extend({}, AbstractRenderer.prototype.events, {
'click #reloadView': '_onClickReloadView',
}),

...

_onClickReloadView: function (ev) {
ev.preventDefault();
console.log("eview renderer >>> _onClickReloadView");

}

这时刷新页面点击按钮,可以看到控制台的对应输出。但是这只能处理特定元素上的事件,有时候我们会希望点击后整个视图能响应到变化,做一些特别的处理,这时候就要主动触发一个OdooEvent,同时Controller里面加入对应事件处理,比如接下来的代码中就实现了点击按钮让视图重新加载的功能:

修改renderer中的_onClickReloadView函数,在里面主动通过trigger_up触发一个OdooEvent

1
2
3
4
5
6
_onClickReloadView: function (ev) {
ev.preventDefault();
console.log("eview renderer >>> _onClickReloadView");
this.trigger_up('reload_view');

}

eview_controller.js中加入相关事件处理:

1
2
3
4
5
6
7
8
9
10
custom_events: _.extend({}, AbstractController.prototype.custom_events, {
'reload_view': '_onClickReloadView',
}),

...

_onClickReloadView: function (ev) {
console.log("eview controller >>> _onClickReloadView");
this.reload();
},

再次刷新页面点击按钮,可以看到echart的饼图会重新载入。

小结

这篇教程中我详细介绍了View的各个组件的作用、主要函数和交互方式,同时包含了一个简短的教程。虽然Odoo自带的视图实现是会更复杂的,但是基本的主要逻辑还是与教程中一致。

当明白了View的整体运行逻辑后,我们再面对视图时对于它们的运行机制就不至于一头雾水了,比如官方tree视图是不支持在导航栏那增加自定义按钮的,而我们可以通过继承ListController并重写renderButtons方法的方式来实现我们自定义视图。

当然这篇教程中所使用到的相关参数只是Odoo视图其中的冰山一角,比如其中还有enableTimeRangeMenu来实现按时间过滤分组参数等,更丰富的组件可以在官方源码研究。