别人写Odoo教程总觉得视图层随随便便带过就行因为无非就是写几个XML标签。但我做了这么多年的Odoo实施和二次开发说句实在话数据模型写得再漂亮用户根本看不见他们每天面对的只有界面。视图做得好不好直接决定了一个项目交付的时候是在收锦旗还是在收事故单。今天是这个系列的第十二篇也是视图层的第三讲前两讲我们搭好了模块骨架搞清楚了模型字段把树形视图和表单视图的基础用法完整过了一遍。这篇我们往深里走覆盖看板视图、图表视图、视图继承、搜索视图高级玩法还有一堆常用widget。这篇内容适合已经开始写自己的Odoo模块、但还想往更高级视图能力上走的朋友尤其是做定制项目时总感觉“官方默认界面不太够用”的那部分人。既然已经是第三讲了我先交代一下前置能力你已经能创建一个最基本的Odoo插件模块写了几个简单的model字段成功加载过tree视图和form视图也知道__manifest__.py里data字段的基本规则。如果你的状态是这样那这篇讲完你就能做出一个有看板、能统计、可以被二次继承改动的完整体验的视图层。我们这一讲的目标不是堆功能而是让你明白Odoo视图层的逻辑骨架同一个界面你知其所以然之后就能举一反三。1. 为什么视图层值得用三讲来啃1.1 视图是用户唯一能感知到的“软件”我见过太多刚上手Odoo的开发者一上来就纠结Python业务逻辑怎么写、继承哪个model、重写哪个方法。这些确实重要但我要泼一盆冷水你的建模能力再强视图不给力用户的真实反馈永远是两个字——“难用”。尤其是做企业实施的时候关键用户绝大多数不是技术人员他们判断你这个系统好不好就看他打开菜单之后的那几秒钟。列表能不能分组、看板卡片是否直观、点进去之后表单布局是否顺眼、筛选按钮是不是一步到位这些全部由视图层决定。视图层不是“配置一下就好”的杂活它就是你交付的产品本身。这也是为什么我在系列里舍得花三讲来啃视图层。第一讲我们建立了模块的基本骨架把树形视图和表单视图的字段摆放、属性含义、基本布局讲透了。第二讲我们深入了模型端的配合比如字段类型如何在视图中被渲染、related和computed字段如何支撑界面的展示逻辑以及一部分常用的widget。到了这第三讲我们要解决的是“从能展示到能好用”的跨越让界面根据业务状态灵活变化让数据有可视化的展示形态让定制需求不修改原始代码也能融入界面。1.2 一个贯穿本讲的模拟项目这一讲我会用一个具体的模拟业务场景来演示否则光谈理论和标签语法看完了还是不知道怎么拼。我们搞一个合同管理的小模块模型叫contract.management字段有这些name合同编号字符型partner_id客户多对一amount合同金额数值型state状态字段可选值draft、confirm、done、canceldate_start起始日期date_end截止日期note备注文本型后面所有代码示例都围绕这套字段编写。你不需要真正把这个模块写出来重点是跟着思路走理解每个视图声明想达到什么效果。1.3 视图文件是怎么被Odoo加载的先复习一个容易被忽略的基础点视图本质上是一条一条存储在数据库ir.ui.view表中的记录。你写的XML文件只是这些记录的“描述文件”在模块安装时被解析然后写入数据库。这也是为什么有时候你改了XML界面却没变化——因为Odoo只在模块升级时重新加载XML定义。如果视图在数据库中已经是旧版本界面自然就不变。视图加载的顺序依赖__manifest__.py文件中data列表的顺序这点极其重要。比如先加载了某个视图这个视图引用了另一个还没加载的动作或视图记录就会直接报错。我的经验是在data里把security放前面然后是views最后是actions和menus。如果你有父子依赖关系父级一定要写在前面。小窍门data列表顺序错了报错信息里经常出现“KeyError”或者“Could not find view with id”但导致问题的根源往往就在顺序上。2. 看板视图与图表视图让界面真正能“看数据”2.1 看板视图用卡片搭建任务墙树形视图和表单视图解决的是“逐条处理数据”的问题而看板视图解决的是“快速总览、按状态分组”的问题。说白了看板就是一张任务墙每张卡片对应一条记录卡片按某个字段的值分组排列。Odoo自带看板视图几乎成了标配因为它对管理层特别友好打开就是全局画面。看板视图的XML结构核心在于kanban标签里嵌套一个QWeb模板。外层属性我们要关注两个default_group_by和group_create。前者决定首次打开时默认按哪个字段分组后者控制是否可以动态创建新的分组列。我们给合同模块写一个最小可用的看板视图record idcontract_management_kanban modelir.ui.view field namenamecontract.management.kanban/field field namemodelcontract.management/field field namearch typexml kanban default_group_bystate templates t t-namekanban-box div classoe_kanban_global_click div classo_kanban_record_title field namename/ /div div classo_kanban_record_subtitle field namepartner_id/ /div div classo_kanban_record_amount field nameamount/ /div /div /t /templates /kanban /field /record这个例子虽然简单但你要理解的不是XML标签本身而是它背后的机制。看板里的field标签不是给用户看表单控件而是让QWeb模板在渲染时读取这条记录的字段值。你写在模板里哪些字段Odoo就只查询哪些字段这也意味着你不需要把所有字段都放进卡片里只放用户最关心的几项即可性能反而好。再看一眼细节o_kanban_global_click这个class意味着点击整张卡片都触发打开表单的动作这是看板视图最常用的交互模式。如果你需要在一张卡片上放多个操作入口比如一个是“确认合同”、一个是“删除”那就不能用这个class你要把点击区域改成局部可点击这个我们后面讲按钮的时候会再展开。卡片颜色和优先级的信息展示可以通过decoration_系列属性去实现。比如给逾期合同加红色背景可以这样写kanban default_group_bystate decoration-dangerdate_end lt; today decoration-infostate draftdecoration-danger是一个条件表达式注意XML中的符号必须写成lt;这是个容易踩的小坑。2.2 图表视图pivot和graph各显神通看板管的是宏观状态而图表视图管的则是“数据怎么被分析和统计”。Odoo的图表视图分两种pivot数据透视表和graph可视化图表二者经常配合使用。pivot视图适合逐行看数字比如按月、按客户交叉汇总graph视图则是把这些数字化成柱状图、折线图、饼图用于汇报和展示。给合同模块加一个图表视图代码长这样record idcontract_management_graph modelir.ui.view field namenamecontract.management.graph/field field namemodelcontract.management/field field namearch typexml graph typebar field namepartner_id/ field nameamount typemeasure/ /graph /field /recordgraph视图的关键点在于前几个field是分组维度排在最后并且标记了typemeasure的那个字段才是统计值。默认情况下如果只有一个可统计字段你甚至可以不写typemeasureOdoo会自动识别但如果模型里数值字段比较多最好还是显式指定否则Odoo可能默认选了你不想要的字段。pivot视图和graph视图在字段声明上很接近但多一个侧重pivot讲究的是行列交叉。比如我想看“每个状态的合同总额按客户分组”就可以写record idcontract_management_pivot modelir.ui.view field namenamecontract.management.pivot/field field namemodelcontract.management/field field namearch typexml pivot field namepartner_id typerow/ field namestate typecol/ field nameamount typemeasure/ /pivot /field /record这里的type含义分别是行维度、列维度、统计值。用户打开之后默认就是按这个交叉组合展示的后续用户也可以自己拖拽调整但默认状态决定了第一印象。图表视图是我在实施项目时最常拿来“展示工作成果”的部分因为管理层不关心你的Python代码写得多优雅他们只关心这个月的合同签约金额是不是能按区域、按客户类型出一个图。所以我的习惯是项目里只要有数值字段一定配一个pivot或者graph视图成本很低但汇报的时候收益很高。3. 视图继承定制化开发里最高频的技能3.1 继承的原理和两种姿势Odoo生态里有一句老话能不改源代码就不改源代码。这句话落到视图层就是视图继承。视图继承的根本原理是把一份已有视图可以是本模块写的也可以是其他模块写的当作“父视图”然后在它的基础上定位到某个节点插入新字段、新按钮或者修改已有节点的属性。这样做的好处很明确升级第三方模块不会丢失你的定制因为这个定制是单独存放在你自己的模块里的。Odoo新版里视图继承统一用inherit_id配合xpath来实现。老版本里还有一种inherit的简写形式现在已经不推荐了我建议你统一使用record加inherit_id的写法。最基础的结构是record idcontract_management_view_form_inherit modelir.ui.view field namenamecontract.management.form.inherit/field field namemodelcontract.management/field field nameinherit_id ref原模块里的某个视图id/ field namearch typexml xpath expr//form/sheet/group positioninside field namenote/ /xpath /field /record这里面的灵魂就是xpath表达式和position属性。expr是你要定位到父视图中的哪个位置position说明你要在找到的位置做什么操作。常见取值包括inside在该节点内部末尾追加内容after在该节点后面插入before在该节点前面插入replace替换该节点内容attributes修改该节点的属性expr的语法是标准XPath比如//field[namepartner_id]会定位到所有name属性等于partner_id的field节点。这里我要提醒一个高频踩坑点如果父视图里同一个字段在表单不同位置出现了多次XPath会定位到第一个匹配的节点。如果你本意是想改的是第二个必须把XPath写得足够精准否则你真的会修改到一个完全想不到的地方。3.2 实战给标准视图加入我们的字段还是拿合同模块举例。假设我们模块里已经有一个基本表单视图现在要在partner_id字段后面插入date_start和date_end并且还要把state显示为一个状态栏那继承视图可以这样写record idview_contract_management_form_enhance modelir.ui.view field namenamecontract.management.form.enhance/field field namemodelcontract.management/field field nameinherit_id refview_contract_management_form/ field namearch typexml xpath expr//field[namepartner_id] positionafter field namedate_start/ field namedate_end/ /xpath xpath expr//form positioninside header button nameaction_confirm typeobject string确认 classbtn-primary statesdraft/ button nameaction_cancel typeobject string取消 statesdraft,confirm/ /header /xpath xpath expr//field[namestate] positionattributes attribute namewidgetstatusbar/attribute /xpath /field /record带inherit_id的视图里可以写多个xpath每个xpath对应一次定位和修改这一点让继承视图功能非常强大它不再只是“在某个位置加一个字段”而是可以同时做多个改动。这里的button在header里面属于表单视图顶部的动作按钮它们会根据记录状态决定是否可见。states属性填状态值的列表比如起草状态下只能看到“确认”按钮。这个设计配合工作流状态栏能让表单的操作路径变得非常清晰。再补充一个细节typeobject表示点击按钮时调用模型里的方法这个方法的name要和Python里定义的方法名完全一致。如果方法不在当前模型里而是在别的地方就要在name前加模型的点路径或者通过typeaction来触发一个ir.actions.act_window。这两种我在项目里都用过typeaction适合弹出向导或者跳到其他视图typeobject适合直接改数据走业务流程。3.3 继承操作最容易踩的坑继承视图看着简单实际踩坑特别多。第一个坑我已经说过XPath表达式写不精准特别是父视图结构复杂的时候一个//field[namexxx]很可能匹配到多个节点。第二个坑和配置有关你的父视图module升级后如果里面某个节点被原模块自己改掉了你的xpath就可能找不到目标界面会直接报错而且报错信息很隐晦比如Element cannot be located in parent view。这时候不要慌打开开发者模式去设置里找到“查看继承的视图”功能浏览器会高亮显示你的xpath实际匹配到的位置排查效率会高很多。第三个坑是继承链的叠加问题。A模块继承了一个基础视图B模块又继承了A模块继承后的视图C模块再继承B的……这种多层继承在大型项目里非常常见但是每多一层继承xpath的定位复杂度就上升一分。你在C模块里写//field[namexxx]很可能定位到的已经不是原始节点的属性了而是被A或者B改过之后的属性。所以我的建议是能少继承一层就少继承一层做定制之前先用开发者工具看清楚这条继承链上有哪些模块改过目标节点。还有属性修改注意positionattributes的写法。很多人第一次写会习惯性写成xpath expr//field[namestate] positionreplace field namestate widgetstatusbar/ /xpath这种方式也能达到目的但它是“用新节点替换旧节点”一旦原字段节点上有其它模块加的属性就会被你全部丢掉。换成positionattributes加attribute namewidgetstatusbar/attribute是在原节点上做微调不会影响其它属性。这个差异在复杂定制项目里特别关键记住了能不动原节点就不动原节点。4. 搜索视图与widget实战把交互细节打磨到位4.1 搜索视图默认过滤、分组与收藏我见过很多视图层开发把全部精力放在表单和看板搜索视图只写了几个字段就完事了。这是很大的疏忽。用户一天到晚用最多的其实是搜索找一个合同、筛一个状态、按客户归拢一下。搜索体验差整个系统的口碑就没了。搜索视图用search标签包裹里面可以放普通字段、筛选器filter和分组器group by...。最基本的写法是record idview_contract_management_search modelir.ui.view field namenamecontract.management.search/field field namemodelcontract.management/field field namearch typexml search field namename/ field namepartner_id/ filter namemy_contracts string我的合同 domain[(user_id,,uid)]/ filter namerunning string执行中 domain[(state,,confirm)]/ group expand0 string分组 filter namegroup_by_state string状态 context{group_by:state}/ filter namegroup_by_partner string客户 context{group_by:partner_id}/ /group /search /field /recordfield放进搜索视图会自动生成对应的搜索输入控件filter则是一个预定义的条件按钮用户可以点选点击后该过滤条件会叠加到当前搜索中。domain字段里的uid指的是当前登录用户这是实现“只看我的记录”这种需求的通用做法。分组器这里要特别留意group里的filter通过context的group_by触发分组而不是通过domain。这个写法经常被搞混。我早期也犯过这个错在filter里写domain又写group_by结果点击之后过滤和分组同时生效完全不是想要的效果。还有一个实用技巧搜索视图支持“收藏”也就是filter可以被用户保存为个人收藏下次直接点击使用。但这需要在动作定义里开启search_view_id关联以及给过滤条件写一个清晰的name。如果你的name写得太潦草用户收藏夹里就会出现一串看不懂的代号体验会很糟。4.2 常用widget清单与选型建议视图层的widget说白了就是“我告诉Odoo这个字段在界面上用哪种控件来展示”。Odoo内置了很多widget我挑几个使用频率最高、且很多教程还没讲透的给一份选型参考。widget适用场景我的使用建议statusbar记录状态流程展示搭配header里的按钮使用状态切换的视觉反馈最直观progressbar展示百分比进度用于项目完成度、合同履约进度之类字段monetary带货币符号的金额需要配合currency_field属性使用否则不会显示货币符号tags多对多分类标签在列表和表单里显示多个标签比下拉好看得多selection_badge选择字段的彩色徽章Odoo 14可用让状态值带颜色比下拉框醒目html富文本编辑用于备注、描述类字段适合需要排版的内容image图片展示用于产品图、照片字段注意设置preview_image避免列表卡顿举个例子把合同的金额字段显示成带货币符号的完整金额你可以这样写field nameamount widgetmonetary currency_fieldcurrency_id/这里有个前提模型里必须有一个currency_id否则currency_field找不到关联货币。如果你没有多币种需求也可以在模型里定义一个固定的currency_id字段或者在视图里写options{currency_field: currency_id}。我建议宁可在模型上加一个关联字段也别硬写固定值后者的灵活性太差一旦客户说要支持多币种改起来很麻烦。另外一个我用得比较多的widget是selection_badge配合状态字段的效果非常醒目field namestate widgetselection_badge/它会在界面上把draft、confirm、done、cancel这些选项值显示为不同颜色的徽章。比如draft是灰色的、done是绿色的。这样一来用户在列表里扫一眼就能判断哪些合同已经完成哪些还是草稿。比起默认的下拉框信息传递效率高了一个档次。4.3 表单头部按钮与状态栏的组合玩法一个优秀的表单视图顶部是一排操作按钮接下来是状态进度条再往下才是字段的明细。这套组合在Odoo里的实现是视图的form节点下先写header再写sheet。header里的按钮配合states属性控制可见范围状态字段用statusbarwidget渲染。这里有个细节值得多说states属性支持多个状态值用逗号分隔。比如“确认”按钮只在draft状态可见“取消”按钮在draft和confirm都可见写法分别是header button nameaction_confirm string确认合同 typeobject classbtn-primary statesdraft/ button nameaction_cancel string取消合同 typeobject statesdraft,confirm/ /header这个设计不需要写任何Python逻辑来控制按钮显示Odoo会根据记录当前的state字段值自动判断。前提是你的模型里确实有一个state字段并且你在视图里把这个字段渲染出来了。如果你在sheet里写了field namestate widgetstatusbar/那顶部按钮的states判断才生效因为Odoo需要这个字段的当前值来渲染。如果表单视图没有把state字段放出来但你又希望按钮按状态显隐Odoo也能工作——因为按钮的states判断本质上是从记录数据里读取的不依赖视图是否展示这个字段。但用户体验上你没放状态栏用户就不知道当前处于哪个状态按钮的显隐对他来说就像魔法这不好。5. 常见报错、调试技巧与避坑速查5.1 五个高频报错及排查思路视图层开发报错最典型的有五类我一个个说排查思路。第一类是最常见的Element cannot be located in parent view。原因是你的xpath表达式在父视图中找不到任何匹配节点。排查思路先确认inherit_id引用的视图id是否正确再确认父视图升级之后是否改动了目标节点的结构。最强力的工具是开发者模式里的“视图继承”检查器能直接把你的xpath和目标节点的匹配情况可视化。第二类是Field xxx does not exist。这个报错通常不是视图的写法问题而是模型里根本没有这个字段。常见场景视图里写了个字段名但模型里拼写有误。排查思路打开应用的技术菜单查看字段列表对照一下即可。第三类是Invalid view declaration多见于arch里面的XML格式错误比如标签没闭合、属性值里写了没有转义的或。排查思路看日志定位到具体行号或者本地安装时开启开发模式Odoo会给出更详细的错误提示。第四类是视图能加载但界面显示混乱。典型情况是字段被放在了sheet外面或者把一个字段直接写在form下但没有套sheet。这类问题不会报错但视图渲染出来的布局会很奇怪。排查思路对照官方标准视图结构看自己的header、sheet、group、page层级是否合理。第五类是刷新之后还是老界面。这个我前面提过视图定义存在数据库里只改XML文件不升级模块数据库记录不会更新。排查思路每次改完XML要么升级自己这个模块要么用开发者模式里的“重新加载视图”按钮。我习惯是升级模块因为还能顺便跑一遍测试更稳妥。5.2 开发者模式下调试视图的实操技巧Odoo的开发者模式是视图开发者的宝藏很多人没充分利用。开启方式设置页面左下角把“开发者模式”打开。开启后编辑视图界面会出现一个“视图继承”的调试器。在调试器里输入你的xpath表达式界面会高亮显示匹配到的目标节点并且会标明当前是哪个模块添加的、覆盖了哪些属性。还有个更快的技巧在表单视图右上角的开发者工具里选“查看元数据”能看到某个字段的完整属性来源包括它是哪个模块的哪个视图添加进来的。这个对排查“为什么这个字段在这里但我找不到定义”极其有用。我曾经接手过一个项目表单上有个字段看着哪哪都不对想删掉又找不到它在哪个视图里加进来的。打开元数据两分钟就定位到是某个自定义模块的继承视图加的直接去那里改就完事了。看板视图的调试走的是QWeb路线报错信息有时候比较生僻比如QWeb template error。这种时候别慌第一个动作是去看日志里给的那行模板代码通常问题就出在你写的t-foreach范围或者t-field的用法上。QWeb就三个核心概念t-foreach循环、t-field取值、t-if条件判断。心里装着这三件事看板视图的报错基本都能解决。5.3 视图开发避坑速查表我根据自己的开发经验整理了一份避坑速查表每一行都是真金白银换来的场景坑我的建议xpath定位多个字段同名用层级更深的表达式精确到父级节点视图加载改XML后界面不变升级模块或使用开发者模式重载视图属性修改用了replace导致其它模块属性丢失优先使用attributes除非确实要换整个节点按钮显示states写错状态值确认模型state字段的selection值完全一致看板布局卡片内容随手乱堆参考官方看板模板结构用oe_kanban_*的CSS类图表字段measure字段未指定显式给唯一需要统计的字段加typemeasure搜索视图filter和group_by混淆filter用domain分组器用context中的group_by多语言字符串硬编码不翻译用XML注释标记翻译或确保模块启用了翻译导入性能看板里放太多字段只放卡片展示必需的字段减少主表查询压力这里面我特别想强调最后一行“性能”。看板视图的字段展示数量直接影响数据库查询量因为看板是批量渲染每张卡片都会触发字段读取。如果一个看板页面上有50张卡片每张卡片放8个字段数据库就要额外处理400个字段值的读取。我在一个真实项目里遇到过看板打开非常卡顿的情况最后排查下来纯粹是因为卡片字段太多且包含了大量的href和计算字段。把字段精简到4个以内之后速度立刻上来了。额外提醒如果看板卡片里需要显示关联表的字段比如partner_id的名称尽量用related字段在模型上做好存储别在看板模板里通过t-foreach去遍历关联对象。遍历关联对象在数据量大的时候会产生严重的内存开销。再补充一个关于group_by的细节看板的分组字段如果是个日期或者状态字段Odoo会自动允许你拖动卡片来改变该字段的值。这是看板的一个隐藏能力但也是一把双刃剑。比如你做了一个按state分组的看板用户把一张草稿状态的合同卡片拖到“已确认”那一列Odoo会把这条记录的state改成confirm。看起来很方便但如果此时没有对应的Python方法去做状态流转里的额外校验比如检查必填字段、触发邮件通知数据的完整性就会出问题。我的建议是允许拖拽改变状态的场景一定要在模型端重写对应字段的write方法或者在界面层把拖拽禁掉做成点按钮走流程。具体做法是在kanban上设置group_deletefalse等属性只解决一部分真正拦住拖拽改值需要在模型重写write。写在后面我做Odoo项目这些年越来越觉得视图层是个“看起来像体力活、实际上全是经验活”的领域。同一个界面新手写出来是字段堆砌老手写出来是业务路径清晰、状态一目了然、用户几乎不需要培训就能上手。差距不在语法而在于对“用户到底怎么用这个界面”的理解。这篇第三讲里提到的看板、图表、继承、搜索和widget每一个单独拎出来都还可以再展开写一篇但核心思想是不变的视图是模型的镜像更是业务流转的载体你让界面和按钮往用户的操作习惯上靠系统的接受度就会高一截。最后再分享一个小经验每次你升级模块之后发现界面报错先不要着急怀疑是自己的代码有问题用开发者模式里的“视图继承”检查器看一下目标节点还在不在。很多时候是第三方模块升级改了结构我们的定制xpath就悬空了。先定位后修改别一顿乱调参数那样只会越调越乱。这行做久了你就知道稳扎稳打、流程清晰比炫技管用得多。
阅读完成 · 觉得有帮助?