2020-12-10 odoo探索日志:自定义视图

上一篇日志在一个空模块的基础上增加了数据模型,为了让用户能从网页上操作这个数据模型,需要给模块增加视图。视图有两种形式,一种是利用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菜单项。菜单项是我自定义应用的入口,可以形成一个层级结构,最顶级项为本应用,其下一级为该应用的主菜单,还可以添加更深层次的子菜单。菜单项可与动作关联,类似于按钮的事件响应。


菜单项.png

菜单项存储在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模板引擎解析为视图):

<odoo>
    <record id="my_first_action"
            model="ir.actions.act_window">
        <field name="name">Test action</field>
        <field name="res_model">my_test_model</field>
        <field name="view_mode">tree,form</field>
    </record>
    <record id="my_first_menu"
            model="ir.ui.menu">
        <field name="name">Test menu</field>
        <field name="action" ref="my_first_action"/>
    </record>
</odoo>

可以看到,代码中第二个record定义了一个数据类型为ir.ui.menu的菜单项,其name字段定义了上图中红圈标出的字符串。关于这个action字段,并没有使用xml的值域,而是使用ref属性引用了第一个record定义的表示动作的ir.actions.act_window数据类型,为什么不直接使用值域呢?这个疑问等下面介绍完odoo的动作后再来讲。
odoo的动作有五种:

  1. ir.actions.act_window
    窗口动作,最常用的action类型,用于打开模型的各种视图。
  2. ir.actions.act_url
    链接动作, 可以通过odoo的链接打开一个网站页面。
  3. ir.actions.server
    服务器动作, 可以通过服务器action来触发复杂的服务端动作。
  4. ir.actions.client
    客户端动作,触发一个在客户端js文件中定义的函数,js函数需通过core.action_registry.add(tag,函数名)提前注册到odoo中。
  5. 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 <str>` following the pattern
    ``"res_model.res_id"`` in database.
    """
    ...

可以看到这段说明文字,这个字段存储的实际上是经过转换后的一段字符串,字符串前面是类名(数据模型名),后面跟着的是该数据模型的某条record id。为何要这样设计呢?你想一下,在第2条record(定义菜单项的record)中,我们的菜单项动作可能是odoo现有5个动作的任何一种,odoo并不知道my_first_action这个record id对应的动作模型究竟是什么,也就是说这个字段的类型并不能事先确定,而是由后来的开发者指定。既然由我们指定,我们当然可以像一般字段那样,手工赋值:<field name="action">ir.actions.act_window,[my_first_action对应的数据库id]</field>,这样odoo模板引擎就能正确解析这个action的类型。下图是我从ir_ui__menu数据库表中截取的一部分数据:

ir_ui_menu.png

可以看到,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页面,内容如下:

<odoo>
    <record id="my_first_form"
            model="ir.ui.view">
        <field name="name">My first form</field>
        <field name="model">my_test_model</field>
        <field name="arch" type="xml">
            <form string="My first form">
                <group>
                    <field name="name"/>
                    <field name="description"/>
                    <field name="value"/>
                    <field name="value2"/>
                </group>
            </form>
        </field>
    </record>
</odoo>

这条记录是一条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:

    <record id="my_first_action"
            model="ir.actions.act_window">
        <field name="name">Test action</field>
        <field name="view_id" ref="testmodule.my_first_form"/>
        <field name="target">new</field>
    </record>

保存后再次更新模块,点击菜单项后,我们以新页面的方式打开了刚才自定义的my_first_form页面。


自定义页面.png

除了以新窗口方式打开,我们还可以选择其它方式。ir.actions.act_window模型的target字段定义了视图打开模式:

  • current:在当前视图上打开
  • fullscreen:使用全屏模式
  • new:新窗口打开
  • main:与current类似,但清除了面包屑导航(面包屑导航:标题栏下方,记录了用户的访问历史层级,让用户了解目前所处位置,以及当前页面在整个网站中的位置)

后面将进一步研究视图中静态资源的使用和js代码的嵌入,之后再研究owl前端框架与odoo控制器的交互。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352