第四章 视图
我们在第一部分中简单地介绍过常见的几种视图及其用法, 但鉴于当时的学习水平, 我们并没有过多的介绍其内部的工作原理。本章, 我们将深入视图的世界, 了解它的本质和加深对它的认知。
视图对象模型
Odoo中的视图在对象模型中与之对应的是ir.ui.view, 原生视图类型包含如下几种:
视图类型 | 视图代码 | 描述 |
---|---|---|
表单 | form | 表单页面视图,最常用的视图类型之一 |
树形 | tree | 列表视图,用来统一展示多条数据的视图类型,最常用的视图之一 |
看板 | kanban | 看板视图,用来在kanban上显示数据的视图类型 |
日历 | calendar | 日历视图,用来在日历上显示数据的视图类型 |
甘特 | gantt | 甘特图 |
搜索 | search | 搜索视图,用来控制视图过滤器布局的视图 |
透视表 | pivot | 透视图 |
Qweb | qweb | Qweb视图,通常用来制作报表的视图类型 |
除此之外,odoo在企业版中还新增了诸如地图视图等类型的新视图,这种适用于特殊场景的视图类型我们会在碰到时进行介绍。当然,我们也可以自定义自己的视图模型, 添加我们自己的视图类型。但添加视图, 涉及到Qweb的知识, 等我们学习完Qweb技术以后再来介绍如何自定义一个自己的视图类型。现在, 我们先来重新认识一下已知的几种视图。
每种视图都有其绑定的对象模型,我们视图的中的字段数据便取自于该数据模型。我们在xml文件中编写的视图,实际上就是ir.ui.view的一种记录。下面我们来解析一个典型的视图结构:
<record id="book_store.book_form" model="ir.ui.view">
<field name="name">图书</field>
<field name="model">book_store.book</field>
<field name="arch" type="xml">
</field>
</record>
以上面的例子来说,一个视图必不可少的元素有:
- name: 视图的名称
- model: 视图所绑定的对象模型
- arch: 视图的布局结构
我们定义完视图文件以后,把视图文件声明到模块的__manifest\.py文件中,然后重启升级模块即可将视图文件导入到系统中。
虽然我们在视图文件中是定义在了arch字段中,但是当我们通过开发者模式进行修改时,实际读写的是arch_db或者arch_fs字段。
视图的继承
我们在第一部分就讲到过视图的继承,继承的视图ID存储在inherit_id字段中,当前段视图进行渲染的时候,会将当前页面的所有子页面进行叠加,根据优先级顺序进行,最后得到一个最终的渲染之后的视图,显示到页面上。
每个视图都有一个inherit_children_ids字段用来记录所有继承了该字段的子视图。
视图的权限
每个视图都有一个groups_id字段,用来指定可以访问此页面的用户组,如果该字段为空,那么意味着该视图对所有用户可见。借助此字段,我们可以实现不同用户组的视图不同,从而达到我们对于某些字段的访问权限控制。
一个典型的例子就是,销售订单上的销售价格字段,假设我们最原始的页面代码如下:
<record id="sale_order_base" model="ir.ui.view">
<field name="name">销售</field>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<form>
...
<field name="price_unit">
</form>
</field>
</record>
如果我们直接在原始页面上通过groups的指定某些组,那么这些组可以进行访问,其他的组就不能看见该字段。如果如果我们希望实现拥有权限的组可以进行修改,没有权限的组就只能读,那么就可以利用不同的视图结合组的方式实现。
<record id="sale_order_sale" model="ir.ui.view">
<field name="name">销售</field>
<field name="model">sale.order</field>
<field name="groups_id" eval="[(4,ref('base.groups_sale_order'))]">
<field name="arch" type="xml">
<form>
...
<field name="price_unit" readonly="1">
</form>
</field>
</record>
该组对于价格只能读
<record id="sale_order_manager" model="ir.ui.view">
<field name="name">销售</field>
<field name="model">sale.order</field>
<field name="groups_id" eval="[(4,ref('base.groups_sale_order_manager'))]">
<field name="arch" type="xml">
<form>
...
<field name="price_unit">
</form>
</field>
</record>
该组对于价格就可以进行编辑
视图的生命周期
视图的加载过程
我们知道一个请求的发起, 首先由controller进行处理, 然后传递给视图进行渲染, 输出页面. 现在, 我们来聚焦一下视图的加载过程.
controller将请求发送给视图对象(ir.ui.view)以后, 视图首先调用load_views方法开始加载视图, load_views将执行3个步骤分别获取一下对象:
- fields_views: 包含了字段布局的视图结构, 其内部使用fields_view_get方法实现.
- fields: 该视图所包含的字段定义集合, 其内部使用fields_get方法实现.
- filters: 使用ir.filters对象的get_filters方法获取过滤器.
load_views在获取上述三个部分的数据以后,传递给前端进行渲染.
下面是一个load_views返回的field_views的结果:
calendar: {model: "stock.picking", field_parent: false,…}
form: {model: "stock.picking", field_parent: false,…}
kanban: {model: "stock.picking", field_parent: false,…}
list: {model: "stock.picking", field_parent: false,…}
search:{model: "stock.picking", field_parent: false,…}
其中关于打印和动作的相关结果也是在各视图的toolbar结果中。
action: []
print: [{id: 174, name: "Picking Operations", type: "ir.actions.report", binding_type: "report",…},…]
0: {id: 174, name: "Picking Operations", type: "ir.actions.report", binding_type: "report",…}
1: {id: 175, name: "Delivery Slip", type: "ir.actions.report", binding_type: "report",…}
2: {id: 188, name: "Barcodes (ZPL)", type: "ir.actions.report", binding_type: "report",…}
3: {id: 189, name: "Barcodes (PDF)", type: "ir.actions.report", binding_type: "report",…}
动态控制
我们知道可以在视图上定义各种各样的条件来控制视图的只读\编辑\跳转等属性, 那么odoo是如何实现这种条件控制的呢?
实际上, 我们定义在字段属性上条件最终都会在fields_view_get方法中进行翻译, 其结果为名为modifiers的属性, 然后WC再根据modifiers中的定义控制页面元素的显示.
按钮
按钮的两种类型,object和action。
默认视图
当你定义了一个模型,如果你没有写任何关于这个模型的视图,那么Odoo将会为你自动生成默认的视图。
- _get_default_form_view 方法用于生成默认form视图
- _get_default_search_view 方法用于生成默认的搜索视图
- _get_default_tree_view 方法用于生成默认的列表视图
- _get_default_pivot_view 方法用于生成默认的透视图
- _get_default_kanban_view 方法用于生成默认的看板视图
- _get_default_graph_view 方法用于生成默认的graph视图
- _get_default_calendar_view 方法用于生成默认的日历视图
Datetime类型转Date
在视图中可以使DateTime类型的部件表现得像Date一样,只需要在xml中指定部件
<field name="date" widget="date"/>
搜索视图
在odoo中像过滤、分组等功能是通过搜索视图来完成的。按照搜索条件的不同,可以分为三种类型的搜索:字段匹配、条件过滤和字段分组。
字段匹配
字段匹配的效果,就是在搜索框内输入关键字,出现下拉列表显示可以匹配的字段,特点是快速完成条件过滤。字段匹配的样板代码如下:
<search string="Tasks">
<field name="tag_ids"/>
<field name="partner_id"/>
<field name="project_id"/>
<field name="user_id"/>
<field name="stage_id"/>
</seach>
条件过滤
条件过滤是字段匹配的扩展版,预先存储了过滤条件的组合,可以在搜索框下的筛选按钮中显示,快速过滤。条件过滤的样板代码如下:
<filter string="My Tasks" name="my_tasks" domain="[('user_id','=',uid)]"/>
<filter string="My Followed Tasks" name="my_followed_tasks" domain="[('message_is_follower', '=', True)]"/>
<filter string="Unassigned" name="unassigned" domain="[('user_id', '=', False)]"/>
字段分组
分组同条件过滤类似,就是将预先设置好的分组,显示在搜索框的分组按钮中,分组的样板代码如下:
<group expand="0" string="Group By">
<filter string="Project" name="project" context="{'group_by':'project_id'}"/>
<filter string="Task" name="task" context="{'group_by':'name'}"/>
<filter string="Assigned to" name="user" context="{'group_by':'user_id'}"/>
<filter string="Stage" name="stage" context="{'group_by':'stage_id'}"/>
<filter string="Company" name="company" context="{'group_by':'company_id'}" groups="base.group_multi_company"/>
</group>
与条件过滤不同的是,条件过滤使用的domain,而分组是在context通过group_by+关键字的形式完成的。
分组的排序
分组不支持排序定义,并且定义在模型上的排序规则在分组的时候并未生效。退而求其次的实现方法是在tree视图中增加default_order属性来实现指定分组时的默认排序规则。
<tree default_order="date_order desc">
...
</tree>
动作中添加默认分组/过滤条件
我们也经常看到,点击某个菜单的时候,会带着默认的分组和过滤条件,这个是怎么完成的呢?
答案在动作的context字段中,上一章中,我们提到了给某个字段设置默认值的操作,本章中的设置默认条件也是类似,只要在我们的过滤条件name前加上search_default_前缀就可以了。
<record id="project.action_view_task" model="ir.actions.act_window">
<field name="view_mode">tree,kanban,form,calendar,pivot,graph,activity</field>
<field name="context">{'search_default_my_tasks': 1,'search_default_parent_task':1,'group_by':['group_id','project_id']}</field>
</record>
如果需要多个字段分组,使用列表将字段括起来即可,字段的先后顺序就是分组的先后顺序。
搜索明细中的字段
有一种场景,我们主要在主单的列表视图中搜索明细表中的某个字段。典型的例子就是我们在销售订单的视图中想要搜索包含某个产品的订单,因为产品是明细表中的字段,销售订单表中并没有提供产品这个字段供我们搜索,我们看odoo官方是如何解决这个问题的:
<field name="order_line" string="Product" filter_domain="[('order_line.product_id', 'ilike', self)]"/>
即,使用order_line.字段名 的方式引入需要的字段,然后传入self来完成。
视图中的特殊字符
有时候我们想在XML中添加一些特殊的字段,例如空格, &等,由于这些特殊字符存在转义问题,因此我们需要使用特殊的符号来替代。
下面是一些常见的特殊字符及其转义字符对照表:
显示结果 | 描述 | 输入 | 实体代号 |
---|---|---|---|
空格 | \ \; |  \; | |
< | 小于 | \<\; | <\; |
> | 大于 | \>\; | >\; |
& | 连接符 | \&\; | &\; |
" | 双引号 | \"\; | "\; |
' | 单引号 | \&apos\; | '\; |
¢ | 分 | \¢\; | ¢\; |
£ | 磅 | \£\; | £\; |
¥ | 日元 | \¥\; | ¥\; |
§ | 节 | \§\; | §\; |
© | 版权 | \©\; | ©\; |
® | 注册商标 | \®\; | ®\; |
× | 乘号 | \×\; | ×\; |
÷ | 除号 | \÷\; | ÷\; |
 \;在XML中不能正确的显示, 使用 \;代替或者使用CDATA.
表单视图(Form View)
给页面添加chatter
_inherit = ['mail.thread', 'mail.activity.mixin']
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="activity_ids" widget="mail_activity"/>
<field name="message_ids" widget="mail_thread"/>
</div>
快速编辑的取消
对于Manyone类型的字段,默认的渲染模式是会有一个下拉列表,附带这快速创建与编辑的功能,通常情况下,这是非常有用的。但是,特定情况下,我们也希望用户不能快速编辑和创建,希望Many2one的特性像Selection一样,只能让用户选择。
这种情况下,我们可以在xml中使用options来控制
<field name="xxx" options="{'no_create_edit': True, 'no_create':True, 'no_open':True">
快速编辑特性
在15.0版本时, 官方加入了快速编辑(quick edit)的新特性,其特点就是用户不用点击编辑按钮即可编辑内容, 当用户点击了启用快速编辑特性的字段, 就会自动进入编辑模式.
这种特性带来的好处是节省了操作时间, 缺点是对于X2many字段,曾经可以弹窗显示的内容不再可用.
为了解决这种问题, 笔者在基础解决方案模块中新增了设置选项,用户可以自行决定是否启用这种特性.
树形列表视图
树形列表视图是和表单视图使用频率一样高的视图类型。
树形列表中的按钮
从14.0版本开始,我们可以像表单视图一样给树形列表视图中的header部分添加按钮了,具体的写法如下:
<tree>
<header>
<button name="button_do_sth" string="生成订单" type="object" class="oe_highlight"/>
</header>
</tree>
其最终的效果就是在树形列表的创建按钮旁边新增了一个自定义按钮:
13.0之前的版本不支持此特性,如果需要在13.0-版本中添加类似的按钮需要自行实现. 本书提供了类似的思路, 具体参考附录相关章节.
隐藏属性列表中的某一列
我们知道在FORM视图中隐藏某个字段可以使用
attrs = "{'invisible':[('xx','=',True)]}"
但是对于FORM表单中的X2Many类型的字段, invisible属性缺只能够隐藏字段内容并不能隐藏表头
这里要讲的column_invisible属性是可以隐藏表头的,但是有个限制条件,就是需要配合parent属性使用
<field name="ratio" attrs="{'column_invisible':[('parent.advance_payment_method','!=','percentage')]}"/>
Graph视图
Graph视图是包含一系列图表类型的可视化视图,常见的支持类型有柱状图、折线图、饼状图等等,可以将数据以更直观的方式显示出来。
Kanban视图
kanban视图的中的格式化
我们在使用看板布局的时候,有时候希望能在字段前加入空格,因为kanban被渲染时使用html语法 ,因此我们手动在视图中添加的空格并不能被解析成空格,因此我们需要使用特殊的语法。
在xml中通常使用
来替代空格,但是在odoo中lxml进行解析时,又会提示:
lxml.etree.XMLSyntaxError: Entity 'nbsp' not defined
说明在lxml库中并没有实体entity与空格进行对应,这时我们就要使用空格的十进制编码 
进行替代了。