上一篇日志在一个空模块的基础上增加了数据模型,为了让用户能从网页上操作这个数据模型,需要给模块增加视图。视图有两种形式,一种是利用odoo MVC框架的QWeb模板引擎进行渲染,另一种是独立于odoo的模板引擎,利用前端框架搭建视图与用户交互,并调用odoo的控制器与odoo交互。odoo14提供了一套全新的前端框架owl,该框架相较于vue、React、AngularJS等目前主流框架更轻量,且运行速度更快,最主要的是owl的模板与odoo的XML模板一致,这是vue等其他主流框架所不支持的。
这里出现了XML模板的概念,这个是odoo模块化设计的一个特征,odoo的MVC框架中,视图使用XML模板编写,并保存在数据库中,这样做的好处是使视图具有高度的可配置性,即使不熟悉HTML、JS、CSS等,也可以配置出理想的视图。odoo的视图具有可继承性,若通过前端框架进行视图开发,则可以通过owl获取odoo的已有视图,owl与odoo的模板引擎一致,从而保持了odoo视图可继承的特性,这就是前文所说vue等其他主流框架所不具备的能力。
既然odoo的MVC已经有了视图层,为何还需要使用owl构建前端框架?这就是现代BS架构前后端分离的理念。就像你使用Django框架,也可以摒弃Django的模板引擎,完全使用vue搭建前端,并直接调用Django的控制器。这样做的好处很明显:一是能各司其职,前端框架比MVC框架的视图模板具有更专业的视图控制技术,且完全在前端实现,与用户的交互性更强;二是更有利于提高并发性能,前端服务器和后端服务器分离,且前端服务器可以很容易地通过反向代理实现横向扩展。这也就是说,没有owl,我们已经可以开发由后端渲染的视图,这也足够满足我们开发交互应用的需求。但如果有了owl,我们可以开发出更好、更快、更强大的交互应用。关于owl的研究,将作为后继工作。我们先来探索使用odoo的模板引擎配置和建立视图。
视图开发
1. 应用的入口:菜单项
点开odoo主页面左上角的开始按钮(类似windows的左下角徽标),如下图所示就是我定义的一个test_menu菜单项。菜单项是我自定义应用的入口,可以形成一个层级结构,最顶级项为本应用,其下一级为该应用的主菜单,还可以添加更深层次的子菜单。菜单项可与动作关联,类似于按钮的事件响应。
菜单项存储在ir.ui.menu模型中,可通过Settings > Technical > User Interface > Menu Items菜单进行查看。ir.ui.menu模型类定义在odoo.addons.base.models.ir_ui_menu.py文件中,打开该文件可以看到除了模型类固有的_name、_description等属性外,还有name、active、sequence、child_id、parent_id、parent_path、groups_id、complete_name、web_icon、action、web_icon_data等字段属性和一些装饰函数,对于模块开发,暂时不需要对这些底层细节完全掌握,只需要掌握其中某些常用字段的使用方法。以我定义的这个Test menu为例,在my-modules.testmodule.views文件夹下新建一个my_first_menu.xml文件(odoo的视图是以xml形式定义,其中的每一项都是一个record,这些record将存储到数据库中,应用加载时由Qweb模板引擎解析为视图):
Test action
my_test_model
tree,form
Test menu
可以看到,代码中第二个record定义了一个数据类型为ir.ui.menu的菜单项,其name字段定义了上图中红圈标出的字符串。关于这个action字段,并没有使用xml的值域,而是使用ref属性引用了第一个record定义的表示动作的ir.actions.act_window数据类型,为什么不直接使用值域呢?这个疑问等下面介绍完odoo的动作后再来讲。
odoo的动作有五种:
- ir.actions.act_window
窗口动作,最常用的action类型,用于打开模型的各种视图。 - ir.actions.act_url
链接动作, 可以通过odoo的链接打开一个网站页面。 - ir.actions.server
服务器动作, 可以通过服务器action来触发复杂的服务端动作。 - ir.actions.client
客户端动作,触发一个在客户端js文件中定义的函数,js函数需通过core.action_registry.add(tag,函数名)提前注册到odoo中。 - ir.actions.report.xml
报表渲染动作,用于在报表渲染前进行一些前置设定,如纸张大小、输出文件名等等。
这些动作也是数据类型,它们被定义在odoo.odoo.addons.base模块的数据模型中,其中定义了很多我们需要用到的数据类型(包括字段类型、动作类型、视图类型等等),所以我们的自定义模块一般都会以base模块为基。
这里先不用细究这五种动作,以后遇到了再来研究。只需先理解第一个窗口动作,也就是我们第一个record的类型。其中res_model字段指明了该窗口要关联的数据模型(my_test_model是上一篇日志中定义的数据模型),view_mode字段指明窗口显示数据模型方式,首先是以tree方式列出my_test_model的所有record,点开某条record后再以form的形式展示。
回到刚才留下的疑问:为何action字段不直接使用xml值域来记录动作record的id,而是用ref属性来引用?
首先我们到odoo源码中看一下ir.ui.menu模型对action字段的定义:
action = fields.Reference(selection=[('ir.actions.report', 'ir.actions.report'),
('ir.actions.act_window', 'ir.actions.act_window'),
('ir.actions.act_url', 'ir.actions.act_url'),
('ir.actions.server', 'ir.actions.server'),
('ir.actions.client', 'ir.actions.client')])
可以看到,action字段是一个Reference类型的数据。那这个Reference究竟是什么呢?和一般的Char、Boolean、Integer等数据库字段类型有什么区别呢?我们再去找odoo源码中对fields.Reference的定义。
class Reference(Selection):
""" Pseudo-relational field (no FK in database).
The field value is stored as a :class:`string ` following the pattern
``"res_model.res_id"`` in database.
"""
...
可以看到这段说明文字,这个字段存储的实际上是经过转换后的一段字符串,字符串前面是类名(数据模型名),后面跟着的是该数据模型的某条record id。为何要这样设计呢?你想一下,在第2条record(定义菜单项的record)中,我们的菜单项动作可能是odoo现有5个动作的任何一种,odoo并不知道my_first_action这个record id对应的动作模型究竟是什么,也就是说这个字段的类型并不能事先确定,而是由后来的开发者指定。既然由我们指定,我们当然可以像一般字段那样,手工赋值:
可以看到,action字段存储的内容是【动作类型,ID】,这个ID号并不是我们在第一个定义动作的record中所定义的id="my_first_action",而是该条动作record存入ir_act_window数据库表后自动生成的ID。
为了避免如此繁琐地去接触这些底层细节,odoo贴心地设计了Reference字段类型,我们通过在该类型字段中添加ref属性,则可自动完成这一转换工作,ref属性是一套预处理机制。
回到我们原先的轨道,我们定义了一个窗口动作和一个菜单项,菜单项的action字段指向了我们定义的这个窗口动作。那窗口打开的页面我们还没有定义。这里我们先不自定义页面,看看窗口动作默认打开的页面是什么。
注意:在升级我们的模块前,先要将刚才定义的数据文件:my_first_menu.xml注册到__manifest__.py文件中的data项中,这样odoo才会知道需要更新那些数据。
在odoo应用列表中,找到我们的自定义模块testmodel,点击升级,升级完毕后就可以看到本文第一张图给出的odoo菜单栏。点击Test menu,打开了一个默认的页面,该页面以tree的样式关联了我们的数据模型,可以编辑增加若干条记录,编辑过程中我们会发现,上一篇日志中定义的模型约束在这里都奏效了。回到tree列表页面,点开一条记录,可以打开这条记录的详细form页。
2. 自定义页面
实际业务中,我们可能会不满足于odoo给出的这个默认页面。我们也可以自定义页面。我们在testmodule模块的views目录下新建一个my_first_form.xml文件,用来向数据库写入我们定义的form页面,内容如下:
My first form
my_test_model
这条记录是一条ir.ui.view类型的数据记录,model字段指明该视图所关联的数据模型,arch字段则是基于xml的视图模板(owl的视图模板与此一致),该模板定义了视图的组织形式,并用field字段指明与关联模型的字段对应关系。
定义好这个视图后,我们同样需要将该数据文件注册到__manifest__.py文件的data字段。再次到odoo应用列表更新本模块,更新后我们可以到”设置->技术->用户界面->视图“中搜索my_first_form,可以看到odoo已经加载了该视图。随着对数据库的了解,我们也可以直接到数据库中的使用sql查看select * from ir_ui_view where name='My first form'。
接下来就是将我们定义的窗口动作指向这个视图。修改前面定义窗口动作的record:
Test action
new
保存后再次更新模块,点击菜单项后,我们以新页面的方式打开了刚才自定义的my_first_form页面。
除了以新窗口方式打开,我们还可以选择其它方式。ir.actions.act_window模型的target字段定义了视图打开模式:
- current:在当前视图上打开
- fullscreen:使用全屏模式
- new:新窗口打开
- main:与current类似,但清除了面包屑导航(面包屑导航:标题栏下方,记录了用户的访问历史层级,让用户了解目前所处位置,以及当前页面在整个网站中的位置)
后面将进一步研究视图中静态资源的使用和js代码的嵌入,之后再研究owl前端框架与odoo控制器的交互。