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='平台') |
安装上模块后,便可看到基本的视图。
定义新视图
可以通过
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/echarts@4.6.0/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
的饼图会重新载入。