1. 视图层不只是XML:动作、视图与菜单的协作机制
1.1 先想清楚模型、视图、动作三者分别干什么
很多朋友做到“视图层”这一讲,容易把注意力全部放在XML标签上,总觉得<form>、<tree>写对了,界面就能出来。其实在Odoo里,视图层有一个非常明确的三角关系:模型负责数据规则,视图负责展示形式,动作负责把“哪个模型、用哪些视图、加什么过滤条件”打包成一个可供菜单调用的入口。
我在带新人做模块开发时,最常用的一个类比是:模型是仓库里的货架,视图是货架上的不同标签牌,动作则是那个“引导员”。用户点开菜单,先找到引导员(动作),引导员按你写好的清单,把对应的标签牌(视图)挂出来。菜单本身不直接和模型打交道,它只负责指向动作。
所以你在自定义模块里写代码时,哪怕只加一个很简单的分类字段,也会牵动三处改动:模型里加字段,表单视图里加控件,搜索视图里加过滤项。如果漏了任何一处,功能虽然能用,但用户会觉得这个界面“半成品”。反过来,如果视图太多而动作没有按顺序组织,用户又会觉得入口混乱。
1.2 动作是“入口清单”,view_mode 决定用户可以切换几种形态
有经验以后你会发现,一个模型的视图可以有很多套:列表视图、表单视图、看板视图、搜索视图,甚至还有日历和透视视图。但用户一次打开菜单,默认看到什么,能不能切换,全由动作的view_mode决定。
<record id="action_training_course" model="ir.actions.act_window"> <field name="name">课程管理</field> <field name="res_model">training.course</field> <field name="view_mode">list,form,kanban</field> <field name="domain">[('state', '!=', 'cancel')]</field> <field name="context">{'search_default_group_state': 1}</field> </record>上面这段里,view_mode写的是list,form,kanban,表示用户可以在列表、表单、看板三种形态之间切换。这里有一个常见误区:view_mode并不是“视图定义顺序”,而是“入口顺序”。Odoo 会按这里的顺序,从ir.ui.view里找到适合当前模型、且满足当前模式的视图记录。
另外要注意,不同版本对“列表视图”的称呼不一样。Odoo 17 之后官方方案里更倾向用list,但很多老教程和旧版本代码里仍然写tree。如果你的项目是从旧版本升级过来的,或者参考了不同时期的资料,切记先确认当前版本的写法,不要照抄。
2. 视图类型那么多,核心配置先抓这三个
2.1 列表视图和表单视图是绝大多数业务的底盘
列表视图是最容易上手的,甚至可以只在动作里写view_mode="tree",系统会自动生成一个默认列表。但生产环境不可能这么随意,因为你总要控制显示哪些列、哪些字段只读、哪些字段可以点击跳转。
<record id="view_training_course_tree" model="ir.ui.view"> <field name="name">training.course.tree</field> <field name="model">training.course</field> <field name="arch" type="xml"> <list string="课程列表" editable="bottom"> <field name="name"/> <field name="category"/> <field name="state" widget="badge"/> <field name="price" sum="合计"/> <field name="start_date" optional="show"/> </list> </field> </record>这里我故意用了<list>而不是<tree>,因为在较新的Odoo里,新代码更推荐list。editable="bottom"表示允许在当前列表底部直接录入新记录,这对内部管理类页面很实用。sum="合计"会在列表底部显示一个合计行,适合金额、课时这类数值字段。
表单视图的配置则要更细一些,因为用户的日常操作大多集中在表单页。我的建议是要把“必填”“只读”“按状态显隐”这些规则放在模型层或视图层统一管理,不要在界面层面靠前端校验硬撑。比如:
<form string="课程信息"> <header> <button name="action_start" string="开始开班" type="object" class="btn-primary" confirm="确认开始该课程?"/> <field name="state" widget="statusbar"/> </header> <sheet> <group> <group> <field name="name" required="1"/> <field name="category"/> </group> <group> <field name="teacher_id"/> <field name="start_date"/> <field name="duration"/> <field name="price"/> </group> </group> <notebook> <page string="说明"> <field name="description"/> </page> </notebook> </sheet> </form>这里有个很实用的经验:表单视图里如果放了state字段,并且用了statusbar控件,用户就能看到当前状态流转的进度条。配合头部的按钮,可以让“草稿-开班中-已结课”这种状态流变得非常直观。这个设计在自定义业务模块里出现频率极高,几乎可以当作标准范式。
2.2 看板视图:状态型业务的“清单化”展示利器
看板视图在不少新手眼里显得很神秘,因为它用到了QWeb模板,看起来像前端代码。其实看板的核心逻辑很简洁:按某个字段分组,把每条记录渲染成一张卡片。
<record id="view_training_course_kanban" model="ir.ui.view"> <field name="name">training.course.kanban</field> <field name="model">training.course</field> <field name="arch" type="xml"> <kanban class="o_kanban_mobile"> <field name="name"/> <field name="state"/> <field name="category"/> <field name="color"/> <templates> <t t-name="kanban-box"> <div class="oe_kanban_global_click"> <div class="o_kanban_card_header"> <div class="o_kanban_card_header_title"> <field name="name"/> </div> </div> <div class="oe_kanban_details"> <field name="category"/> <field name="price"/> <field name="state"/> </div> </div> </t> </templates> </kanban> </field> </record>要让看板真正好用,关键往往不在卡片样式,而在给模型补一个color整型字段。这个字段不是给人看的,而是给看板卡片底色用的。你可以像下面这样加:
color = fields.Integer('颜色索引')然后在看板模板里引用<field name="color"/>。Odoo 会根据数值对应到预置色板。如果你想让某条记录特别显眼,比如即将开课的课程,可以用widget="priority"或者自己写个小计算字段去更新颜色索引,这样用户的视觉焦点就被引导过去了。
看板的另一个隐藏功能是“分组拖拽”。默认情况下,看板会按第一个分组字段展示,比如state。用户可以直接把卡片从“草稿”拖到“开班中”,如果模型里对应字段允许,这条记录的状态就更新了。这个体验很好,但要注意权限控制好,不能让人随手把已结课的记录拖回草稿。
2.3 搜索视图:决定用户每天怎么找数据
很多模块开发到后面,真正被用户夸“好用”的,往往不是界面多好看,而是搜索是否顺手。搜索视图的配置并不复杂,但粒度可以做得非常细。
<record id="view_training_course_search" model="ir.ui.view"> <field name="name">training.course.search</field> <field name="model">training.course</field> <field name="arch" type="xml"> <search> <field name="name"/> <field name="category"/> <field name="teacher_id"/> <filter name="state_ongoing" string="开班中" domain="[('state', '=', 'ongoing')]"/> <filter name="state_done" string="已结课" domain="[('state', '=', 'done')]"/> <separator/> <filter name="group_state" string="按状态分组" context="{'group_by': 'state'}"/> <filter name="group_category" string="按分类分组" context="{'group_by': 'category'}"/> </search> </field> </record>这个搜索视图包含了三个最常用的元素:普通字段搜索、过滤条件、分组条件。普通字段搜索会在搜索栏显示下拉框,用户输入关键字后按对应字段匹配;过滤条件则以按钮形式出现在搜索框下方,一键切换数据的筛选范围;分组条件则控制列表和看板的展示分组方式。
我特别想提醒一个细节:分组条件在很多业务场景里,比过滤条件更重要。比如课程管理里,“按分类分组”能让你一眼看到不同分类下有多少课程;“按状态分组”则能让用户直接看到流程堵在哪个环节。如果你在建模块初期就规划好这些分组,后面做报表统计时会省很多事。
3. 实操分解:从字段设计到一个可点击的菜单入口
3.1 模块目录与数据文件的加载顺序
到了视图层这一讲,模块的文件结构也基本成型了。我这里以一个培训机构的课程管理模块为例,展示最常规的目录:
training_center/ ├── __init__.py ├── __manifest__.py ├── models/ │ ├── __init__.py │ └── training_course.py ├── security/ │ └── ir.model.access.csv ├── views/ │ ├── training_course_views.xml │ └── menu_views.xml └── data/ └── demo_data.xml在__manifest__.py里,顺序是敏感点。data列表里必须先加载安全文件,再加载视图,最后加载菜单。因为菜单引用了动作,动作引用了视图,视图又引用了模型权限。如果顺序颠倒,系统在安装时就会提示找不到对应的XML ID。
{ 'name': '培训中心', 'version': '1.0', 'depends': ['base'], 'data': [ 'security/ir.model.access.csv', 'views/training_course_views.xml', 'views/menu_views.xml', ], 'application': True, }这里有经验的人会再做一件事:给模块名和XML ID设置清晰的前缀。比如这个模块叫training_center,那所有视图的ID都以view_training_course_开头,动作以action_training_course_开头。后期模块一旦多起来,这个习惯能帮你少踩一半的“ID重复导致视图串线”的坑。
3.2 一个完整可用的动作与菜单配置
这一讲标题里强调“设置视图层”,所以我把最常见的动作配置再展开讲一下。许多教程会把动作和菜单写在同一个XML文件里,这是完全可行的,也方便维护。
<record id="action_training_course" model="ir.actions.act_window"> <field name="name">课程管理</field> <field name="res_model">training.course</field> <field name="view_mode">list,kanban,form</field> <field name="domain">[('state', '!=', 'cancel')]</field> <field name="context">{'search_default_group_state': 1}</field> </record> <record id="view_training_course_form" model="ir.ui.view"> <field name="name">training.course.form</field> <field name="model">training.course</field> <field name="arch" type="xml"> <form string="课程信息"> <sheet> <group> <field name="name"/> <field name="category"/> <field name="price"/> <field name="start_date"/> </group> </sheet> </form> </field> </record> <menuitem id="menu_training_root" name="培训中心" sequence="10"/> <menuitem id="menu_training_course" name="课程管理" parent="menu_training_root" action="action_training_course" sequence="20"/>这段代码里,动作的context写了一句{'search_default_group_state': 1}。这句话很多人不理解,它并不是强制分组,而是“默认帮用户打开按状态分组”的开关。如果对应的搜索视图里定义了一个名为group_state的分组过滤条件,那用户一进页面就会看到数据已经按状态分组好了。这是一个非常实用的小技巧,尤其在列表视图和看板视图里效果明显。
菜单结构我建议分成两层:根菜单 + 业务菜单。根菜单的sequence控制它在应用切换器里的排序,业务菜单的sequence则控制在根菜单下拉列表里的顺序。如果以后还有报名管理、讲师管理等模块,只要挂在同一个根菜单下,整个业务域就非常整齐。
3.3 在视图里放按钮动作,串起关联窗口
视图层不只是展示字段,它还要承担操作入口的职责。最常见的是在表单页放一个按钮,一键跳转到关联模型。我在这个课程模块里,通常会加一个“查看报名学员”的智能按钮。
模型里写一个返回动作的方法:
def action_open_students(self): return { 'name': '报名学员', 'type': 'ir.actions.act_window', 'res_model': 'training.student', 'view_mode': 'list,form', 'domain': [('course_id', '=', self.id)], 'context': {'default_course_id': self.id}, }然后在表单视图的<sheet>外侧或顶部加按钮:
<button name="action_open_students" type="object" string="查看报名学员" class="oe_stat_button" icon="fa-users"/>这个配置的核心思路是:按钮的name对应模型方法,type="object"表示调用Python方法,context里预先设置默认值,让用户从课程页面顺手创建一个关联学员时,系统能自动填好课程字段。
我在做这类跳转时,会特别注意domain的写法。如果不在动作里限制domain,那点击按钮后用户看到的是所有学员,而不是当前课程的学员,只差一个条件,体验却天差地别。新手最容易漏的就是这一步。
4. 权限与数据规则:视图层背后的两道隐性门槛
4.1 ir.model.access.csv 的字段顺序与权限粒度
很多教程会把权限文件一笔带过,但实际开发中,权限文件写错导致的“能看见菜单,但打开报错”是最常见的故障之一。这个CSV文件有五列必填项:id、name、model_id:id、group_id:id、然后才是四个权限位。
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_training_course_user,training.course.user,model_training_course,base.group_user,1,1,1,0 access_training_course_manager,training.course.manager,model_training_course,base.group_system,1,1,1,1注意model_id:id的写法。模型名是training.course,在权限文件里对应的外部ID是model_training_course,把点号换成下划线。如果模型名里带有复杂前缀,这个规则同样适用。
我一般会建至少两行权限:一行给普通用户,只读或可编辑;一行给管理员,允许删除。这里有一个细节:如果普通用户不需要删除记录,perm_unlink写0。否则,用户可能在列表视图批量删除时,把不该删的数据清掉。虽然Odoo有回收站机制,但业务数据一旦误删,恢复起来很麻烦。
4.2 记录规则控制行级可见性
CSV权限控制的是“能不能访问这个模型”,而记录规则(ir.rule)控制的是“能访问这个模型里的哪些行”。这两个概念很容易混淆。简单来说,CSV权限是粗粒度门禁,记录规则是细粒度门禁。
例如课程管理中,讲师只想看到自己负责的课程,可以这样定义:
<record id="rule_training_course_teacher" model="ir.rule"> <field name="name">讲师只能看自己负责的课程</field> <field name="model_id" ref="model_training_course"/> <field name="domain_force">[('teacher_id', '=', user.id)]</field> <field name="groups" eval="[(4, ref('base.group_user'))]"/> </record>这里user.id是写死在规则里的特殊变量,Odoo解析时会把当前登录用户替换进去。过滤器、看板、列表都会自动带这个条件。视图层加了这条规则之后,普通用户看数据就等于每一条SQL查询都自带了一个不可见的WHERE条件。
但是要注意,记录规则写得太死也容易坑人。比如管理员也属于base.group_user,如果规则对所有人生效,管理员看全局数据反而要额外绕过,很容易让人不明所以。我的习惯是单独建一个“讲师”权限组,把这条规则只挂在该权限组上,普通管理员不受影响。
4.3 改视图不刷新?先想想模块有没有升级
“为什么我改了XML,界面一点变化没有?”这个问题几乎每周都会出现在开发者群里。Odoo的视图文件不是改完立刻生效的,它是在模块升级时重新加载的。如果你只是在文件系统里改了XML,但没有到“应用”页面升级对应模块,系统里存储的视图记录仍然是旧版本。
升级模块的正确姿势是:先停掉可能导致冲突的测试操作,在后台的“已安装的模块”里找到你的模块,点击升级。升级过程中,Odoo会卸载并重新加载模块的data文件,更新视图、菜单、权限和演示数据。
有一个提升效率的小技巧:如果你只改了视图,不要直接升级包含很多演示数据的模块,因为升级会把数据列表重跑一遍,可能触发唯一性约束。你可以把演示数据单独放到data/demo_data.xml,并在__manifest__.py中把demo键设为True,这样只有勾选了“演示数据”的数据库才会加载。开发自己测试时,逻辑更干净。
5. 视图开发中必须收藏的排错手记
5.1 白屏、路径错误、日志优先级
视图出问题,第一反应不要盯着界面看,先去看服务器日志。Odoo在加载XML时如果遇到错误,会在日志中抛出完整的异常栈。最常见的几种报错都很有辨识度:
Element <field> cannot be located in parent view:这是继承视图里xpath写错了,找不到对应的父节点。Invalid model 'training.course' in <field name="model">:说明模型还没建立,或者模块依赖顺序不对。Record not found:引用了一个不存在的XML ID,常见于视图ID拼写错误。
看日志不要只看最后一行,要找File "xxx.py", line这类路径提示。它通常会准确告诉你哪个文件、哪个记录出了问题。
5.2 xpath 继承的三种正确姿势
视图继承是Odoo里复用功能最核心的手段,但也是最容易犯错的地方。继承视图时,inherit_id指向被继承的视图,arch里用xpath定位插入点。我的经验是先打开被继承视图的原XML,确认父节点,再写表达式。
<record id="view_training_course_form_inherit" model="ir.ui.view"> <field name="name">training.course.form.inherit</field> <field name="model">training.course</field> <field name="inherit_id" ref="view_training_course_form"/> <field name="arch" type="xml"> <xpath expr="//group/group[2]" position="inside"> <field name="color"/> </xpath> <xpath expr="//header" position="inside"> <button name="action_archive" type="object" string="归档"/> </xpath> </field> </record>position有几种取值:inside是放入选中节点内部末尾,after是放到选中节点后面,before是前面,replace是替换,attributes是修改属性。我建议先用inside和after两种就够解决大部分场景,其他少用。
还有一个关键技巧:同名视图可以同时存在多份继承,不要担心覆盖。Odoo的最终展示视图是“基础视图 + 所有继承视图”按ID顺序合并后的结果,不是后一个覆盖前一个。
5.3 用超级用户和权限组分别测试视图
在视图开发后期,我有一个固定习惯:切换成超级用户身份、带限制的普通用户身份各看一遍页面。因为很多“视图显示不对”并不是架构错误,而是当前用户没有对应权限组,导致字段被前端自动隐藏了。
具体操作是:在用户页面新建一个测试用户,只勾选基础权限组,然后打开模块看看菜单是否可见、字段是否可编辑。如果字段在管理员账号下正常,在测试用户下消失,问题通常出在groups属性上。视图里的字段可以这样限制:
<field name="price" groups="base.group_system"/>这句的意思是只有系统管理员能看见这个字段。这种配置常用于“成本价”“折扣率”等敏感字段。如果测试用户看不见,大概率就是groups设置导致,而不是前端渲染问题。
5.4 只更新安全文件时避免全量升级
很多人都在后台点“升级”按钮,但如果每次只是加了一条访问权限记录,全量升级会把所有视图重新载入一遍,耗时也容易引入无关问题。有一个更精准的加载方式:先进入“技术”菜单下的“外部标识”页面确认CSV里的模型ID,再用开发者模式下的“更新应用”列表。
其实最简单可控的办法,是在命令行执行模块升级的特定操作,或者直接把CSV文件内容手动改到数据库里再重启服务。这个话题有点深入,但如果你负责一个大型生产库,我强烈建议掌握“只加载某个XML数据”的命令,而不是动不动全量升级。
日常开发阶段可以随意升级,到了生产环境就要慎之又慎。视图层的问题绝大多数可以在测试库解决,不要拿生产库当试验田。
6. 把视图层再往后推一步
视图层做到这里,你已经可以用一个自定义模型搭出列表、表单、看板、搜索视图,再配上动作和菜单,形成完整的操作闭环。接下来值得花时间的,是把视图里的数据能力做深。
比如给看板卡片加一个小图表,或者把表单里的字段按业务阶段做动态显隐。这些都可以通过视图属性配合模型字段完成,不需要写前端框架代码。Odoo的视图层本质上是配置驱动的,你掌握的原则越多,越能组合出灵活的功能。
我个人在项目里的体会是,先把最常用的三类视图做扎实,比追求花哨的看板样式更实在。等你在项目里真正跑过一遍“建模-视图-菜单-权限-升级排错”的完整流程,后续再接触报表视图、仪表盘视图,就会发现那些不过是同一套规则的延伸。
补充一个小技巧:开发时给每个视图都写清楚name,别偷懒让它自动生成。这个 name 在系统里显示为视图的技术名称,后期排查问题时,你能一眼看出是哪条记录。视图名尽量体现“模型用途+视图类型”,比如training.course.list、training.course.kanban,一看到就知道是给哪个模型哪个模式用的。
这一讲先到这里。下一讲如果继续做这个培训中心模块,可以把报名记录、课程评价这些关联业务串起来,那又会遇到新的视图布局问题。到那一步,你再回头看今天这些配置,会发现视图层其实是一种通用的表达语言,只是入口和组合方式不同罢了。