我们在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
对应一个章节结束后的完整代码,读者可以通过类似以下方式来自由切换到不同章节代码。
定义基本模型
可以通过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='平台')
|
安装上模块后,便可看到基本的视图。

定义新视图
可以通过git checkout v0.2
查看本章节的完整代码
定义一个新视图的操作量比较大,我们需要给odoo的python代码中增加新视图类型与视图模式,其次我们还需要定义js相关文件和模板代码。
让Odoo识别新视图类型
首先我们在model下建立两个文件ir_action_act_window.py
和ir_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设计模式,它们之间的关系如下图所示:

其中需要注意的是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
的一个对象,所有数据相关的部分都需要通过它来处理,这部分的主要逻辑很简单,只需要实现get
和load
方法,通过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
对应的是Model
中get
方法获取的数据。
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.xml
的act_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
模式并进入视图,我们即可看到新增的视图效果与组件和相关函数的加载顺序了。

实战
在前面的教程中我们了解到了views的组件初始化与生命周期函数,这也意味着我们可以在相关周期函数中加入自己的一系列事件,来实现我们自己独特的视图。
在接下来的章节中我会逐步实现加入视图模板,解析视图字段,事件绑定与处理等功能来实现一个基于echart的自定义饼图,这个视图中我们可以左上角自由切换定义在xml中的字段,饼图中则会统计该字段在数据库的全部数据:如果字段是数值,根据Name自动分类叠加,如果字段是字符串,则对该字段分组统计数量。
自定义模板与按钮事件绑定
可以通过git checkout v0.3
查看本章节的完整代码
和在 Odoo 中添加自定义dashboard页面中的模板渲染流程一样,首先我们在eview_view.js
的jsLibs
中加上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时,我们就会看到一个饼图:

接着我们为导航栏增加按钮,视图导航栏的按钮就是类似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);
});
}
},
|
再次进入页面,我们可以发现导航栏部分多了 ‘统计字段’下拉按钮,点击相关选项,按钮组就会处于激活状态

获取视图定义结构信息
可以通过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
控制台打断点输出,我们可以轻松看到完整的结构。

知道了数据结构后剩下的事就简单多了,我们自定义三个参数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.js
在init
中接收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
,在load
和reload
的方法中增加获取相关字段值逻辑,同时修改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
中,直接获取即可。
刷新页面,此时我们我们可以看到点击下拉选项时,页面会刷新,同时上方标题属性会显示对应字段的定义名。

在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
里把option
的data
部分删除,接着和上个章节一样,在_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会自动从后台获取对应字段的数据,同时右上角的搜索框我们也可以自由输入数据过滤结果。

让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
来实现按时间过滤分组参数等,更丰富的组件可以在官方源码研究。