扫码阅读
手机扫码阅读

Odoo Tree视图详解,读完这篇就够了!

238 2023-09-07

Odoo

神州数码云基地

在 Odoo 上的尝试、调研与分享

本期作者

丁涛

前端开发工程师

做最好的自己

你我就是奇迹

对于Odoo初学者而言,Tree视图是我们应该首先掌握的基础视图。

这篇文章包含对Tree视图的基本介绍、视图顶部增加按钮、绑定widget、单元格合并、searchBar和action按钮的隐藏、固定首行首列,视图内部增加按钮等~

相信看完这篇文章后,你会更加了解Tree视图,快速解决日常开发所需~

#Odoo Tree视图介绍

在实际开发中我们不可避免的会用到列表展示数据。

那在Odoo中已经为我们集成好了Tree视图我们只需要通过固有的写法定义字段,就能完成列表的数据展示。

我们来看看它有哪些具体写法和属性吧:

1、editable

该属性让数据可以在列表内进行编辑,有效的值是top和bottom。

2、default_order

进行初始化排序,可使用desc来进行倒序。

3、decoration-样式名

样式可为:bf加粗, it斜体。或其他bootstrap样式,如:danger红色, info, muted, primary, success绿色,

warning橙色等等。

值为python表达式,对每条记录执行相应表达式判断,当结果为true的时候将对应的样式应用。

4、create, edit

可以通过将它们设置为false来禁用视图中的对应操作按钮:create对应创建按钮、edit对应编辑按钮。

#Odoo Tree视图

增加按钮

Step1:创建一个tree视图

"dispatch_imitate_list_view" model="ir.ui.view"> "name">order.dispatch.imitate "model">order.dispatch.imitate "arch" type="xml"> "false" import="false" editable="top" class="order_dispatch_list"> "imitate_name" readonly="1"/> "imitate_note" /> "create_uid" string="创建人" readonly="1"/> "create_date" string="创建时间" readonly="1"/>  

Step2:为该视图创建一个按钮

"1.0" encoding="utf-8" ?><template id="create_imitate" xml:space="preserve"> "ListView.buttons"> "div.o_list_buttons" t-operation="append"> if="widget.actionViews[0].fieldsView.name == 'order.dispatch.imitate'"> "btn btn-primary create_imitate_button" type="button">创建模拟   template>

Step3:扩展ListController

实现该按钮方法,并绑定到Tree视图)

向上滑动阅览

var ListView = require('web.ListView');var viewRegistry = require('web.view_registry');var ListController = require('web.ListController');//这块代码是继承ListController在原来的基础上进⾏扩展var BiConListController = ListController.extend({ renderButtons: function () { this._super.apply(this, arguments); if (this.$buttons) { //这⾥找到刚才定义的按钮和输入框 this.$buttons.find('.create_imitate_button').on('click', this.proxy('create_imitate_function')); } }, //创建模拟 create_imitate_function: function () { let self = this console.log('创建按钮被点击') },});var BiConListView = ListView.extend({ config: _.extend({}, ListView.prototype.config, { Controller: BiConListController, })});//这⾥⽤来注册编写的视图BiConListView,第⼀个字符串是注册名到时候需要根据注册名调⽤视图viewRegistry.add('imitate_list_view_button', BiConListView);return BiConListView;

Step4:绑定注册名

将此时注册名绑定到tree视图的js_class属性中)

"dispatch_imitate_list_view" model="ir.ui.view"> "name">order.dispatch.imitate "model">order.dispatch.imitate "arch" type="xml"> "imitate_list_view_button" create="false" import="false" editable="top" class="order_dispatch_list"> "imitate_name" readonly="1"/> "imitate_note" /> "create_uid" string="创建人" readonly="1"/> "create_date" string="创建时间" readonly="1"/>  

Step5:页面效果,按钮事件生效

#Odoo Tree视图

单元格合并

在日常开发中,table表格的单元格合并是个很常见的场景。

一般在vue+elementUI中,可以配置rowcolumn,从而实现单元格的合并,然而在Odoo Tree视图中,是无法通过配置来实现相关场景的。

那我们该如何处理呢?实际场景如下:

Odoo的Tree视图通过加载list_renderer.js文件来完成单元格渲染。

也就是说,我们可以通过修改具体的renderder方法进而实现单元格的合并。

方法如下:

#1

确保合并条数和合并数据的处理

首先保证后台返回的数据中,有明确的合并条数字段。

然后需要前端对返回数据进行一定标识处理,改写_renderRows方法,为后面合并单元格做准备:

向上滑动阅览

var orderListRenderer = ListRenderer.extend({ _renderRows: function () { //对data数据进行处理 if (this.state.data.length > 0) { // var crm_no = this.state.data[0].data.crm_no var id = this.state.data[0].data.id var quot_no = this.state.data[0].data.quot_no this.state.data[0].data.first_flag = true for (let item of this.state.data) { if (item.data.quot_no !== quot_no) { item.data.first_flag = true quot_no = item.data.quot_no } }  } return this.state.data.map(this._renderRow.bind(this)); })

#2

改写单元格的渲染方法

_renderBodyCell

向上滑动阅览

var ListRenderer = require('web.ListRenderer');var orderListRenderer = ListRenderer.extend({_renderBodyCell: function (record, node, colIndex, options) { var tdClassName = 'o_data_cell'; let len = 1 //获取合并单元格的lenth if (node.attrs.name !== "prod_desc" && node.attrs.name !== "prod_num") { len = Number(record.data.prod_length) } if (node.tag === 'button_group') { tdClassName += ' o_list_button'; } else if (node.tag === 'field') { tdClassName += ' o_field_cell'; var typeClass = FIELD_CLASSES[this.state.fields[node.attrs.name].type]; if (typeClass) { tdClassName += (' ' + typeClass); } if (node.attrs.widget) { tdClassName += (' o_' + node.attrs.widget + '_cell'); } } if (node.attrs.editOnly) { tdClassName += ' oe_edit_only'; } if (node.attrs.readOnly) { tdClassName += ' oe_read_only'; } var $td = $('', {class: tdClassName, tabindex: -1}); // We register modifiers on theelement so that it gets the correct // modifiers classes (for styling) var modifiers = this._registerModifiers(node, record, $td, _.pick(options, 'mode')); // If the invisible modifiers is true, theelement is left empty. // Indeed, if the modifiers was to change the whole cell would be // rerendered anyway. if (modifiers.invisible && !(options && options.renderInvisible)) { //进行单元格合并+样式居中 return $td.attr('rowSpan', len).css({'vertical-align': 'middle'}); } if (node.tag === 'button_group') { for (const buttonNode of node.children) { if (!this.columnInvisibleFields[buttonNode.attrs.name]) { //进行单元格合并+样式居中 $td.append(this._renderButton(record, buttonNode)).attr('rowSpan', len).css({'vertical-align': 'middle'});; } } return $td; } else if (node.tag === 'widget') { //进行单元格合并+样式居中 return $td.append(this._renderWidget(record, node)).attr('rowSpan', len).css({'vertical-align': 'middle'}); } if (node.attrs.widget || (options && options.renderWidgets)) { //判断是否是合并列的第一跳数据,并且是否开启了编辑权限 if (record.data.first_flag == undefined) { if (node.attrs.name !== "prod_desc" && node.attrs.name !== "prod_num") { //对合并列进行隐藏 var $el = this._renderFieldWidget(node, record, _.pick(options, 'mode')); return $td.append($el).attr('rowSpan', len).css({'vertical-align': 'middle','display': 'none'}); } else { var $el = this._renderFieldWidget(node, record, _.pick(options, 'mode')); return $td.append($el).attr('rowSpan', len).css({'vertical-align': 'middle'}); } } else { var $el = this._renderFieldWidget(node, record, _.pick(options, 'mode')); return $td.append($el).attr('rowSpan', len).css({'vertical-align': 'middle'}); } } this._handleAttributes($td, node); this._setDecorationClasses($td, this.fieldDecorations[node.attrs.name], record); var name = node.attrs.name; var field = this.state.fields[name]; var value = record.data[name]; var formatter = field_utils.format[field.type]; var formatOptions = { escape: true, data: record.data, isPassword: 'password' in node.attrs, digits: node.attrs.digits && JSON.parse(node.attrs.digits), }; var formattedValue = formatter(value, field, formatOptions); var title = ''; if (field.type !== 'boolean') { title = formatter(value, field, _.extend(formatOptions, {escape: false})); } //同上 if (record.data.first_flag == undefined) { if (node.attrs.name !== "prod_desc" && node.attrs.name !== "prod_num") { return $td.html(formattedValue).attr('title', title).attr('rowSpan', len).css({ 'vertical-align': 'middle','display': 'none'}); } else { return $td.html(formattedValue).attr('title', title).attr('rowSpan', len).css({'vertical-align': 'middle'}); } } else { return $td.html(formattedValue).attr('title', title).attr('rowSpan', len).css({'vertical-align': 'middle'}); } }})

#3

勾选框渲染

改写_renderRow和_renderSelector

向上滑动阅览

_renderRow: function (record) { var self = this; var $cells = this.columns.map(function (node, index) { return self._renderBodyCell(record, node, index, {mode: 'readonly'}); });  var $tr = $('', {class: 'o_data_row'}).attr('data-id', record.id).append($cells); if (this.hasSelectors) { //增加record.data参数,便于渲染勾选列 $tr.prepend(this._renderSelector('td', !record.res_id, record.data)); } this._setDecorationClasses($tr, this.rowDecorations, record); return $tr; },_renderSelector: function (tag, disableInput, data) { var $content = dom.renderCheckbox(); if (disableInput) { $content.find("input[type='checkbox']").prop('disabled', disableInput); } if (data) { if (data.first_flag != undefined) {//对于同一将合并单元格的勾选按钮,只渲染第一次,其他勾选按钮不渲染 return $('<' + tag + '>').addClass('o_list_record_selector').attr('rowSpan', Number(data.prod_length)).css({'vertical-align': 'middle'}).append($content); } else { return } else { return $('<' + tag + '>').addClass('o_list_record_selector').css({'vertical-align': 'middle'}).append($content); }}

#4

将改写的ListRenderer

挂载到ListView上

var viewRegistry = require('web.view_registry');var ListView = require('web.ListView');var BiConListView = ListView.extend({ config: _.extend({}, ListView.prototype.config, { Renderer: orderListRenderer, })});viewRegistry.add('project_list_view_button', BiConListView);return BiConListView;

#5

将此view挂载到tree视图上

string="项目列表" js_class="project_list_view_button" create="false" import="false"/>

#6

更新模块便可实现单元格合并了!

#Odoo Tree视图

绑定widget

#1

继承web.basic_fields

并编写widget

向上滑动阅览

var FieldMonetary = require('web.basic_fields').FieldMonetary;var fieldRegistry = require('web.field_registry');//widgetvar imitateOperateFields = FieldMonetary.extend({ className: 'o_field_orderOperate', events: _.extend({}, FieldMonetary.prototype.events, { 'click': '_onClick', }), _onClick: function (event) { event.preventDefault() let self = this console.log('按钮点击') }, //字段渲染 _render: function () { this.$el.data('value', this.value).css('color', '#4e6ef2').attr('title', this.value); return this._super.apply(this, arguments); }})//widget绑定fieldRegistry.add('imitate_fields', imitateOperateFields);

#2

对需要绑定widget的字段

在tree视图中进行绑定

"dispatch_imitate_list_view" model="ir.ui.view"> "name">order.dispatch.imitate "model">order.dispatch.imitate "arch" type="xml"> "imitate_list_view_button" create="false" import="false" editable="top" class="order_dispatch_list"> "imitate_name" readonly="1" widget="imitate_fields"/> "imitate_note" /> "create_uid" string="创建人" readonly="1"/> "create_date" string="创建时间" readonly="1"/>  

#隐藏searchBar、

action和勾选栏

#1 隐藏searchBar、action

在上面,我们已经知道了tree视图如何绑定js_class。这里,我们可以对ListView进行searchBaraction的配置,进行隐藏。

var BiConListView = ListView.extend({ config: _.extend({}, ListView.prototype.config, { Renderer: orderListRenderer, Controller: BiConListController, }), _extractParamsFromAction: function (action) { var params = this._super.apply(this, arguments); //隐藏勾选后的action按钮 params.hasActionMenus = false; //隐藏searchBar视图 params.withSearchBar = false; return params; },});

#2 隐藏勾选框

tree视图的勾选框是在列表render的时候进行渲染的,所以我们得改写ListRender里的_renderSelector方法

var ListRenderer = require('web.ListRenderer');var materialListRenderer = ListRenderer.extend({ _renderSelector: function (tag, disableInput) { var $content = dom.renderCheckbox(); if (disableInput) { $content.find("input[type='checkbox']").prop('disabled', disableInput); } //可以根据条件判断,return空时将不会渲染勾选框 return }});

#3 页面实图

#单元格内

添加操作按钮视

#1

et对模型py文件添加html类型的字段

用compute属性计算

class OrderDispatchImitate(models.Model): _name = "order.dispatch.imitate" _description = "齐套模拟基本模型"  imitate_name = fields.Char(string="模拟名称") imitate_note = fields.Char(string="备注") project_id = fields.One2many("order.dispatch.imitate.project", "dispatch_imitate_id", "齐套模拟项目id") button_view = fields.Html('操作', compute="_button_html_view", sanitize=False)  @api.model def _button_html_view(self): self.button_view = '编辑'

#2

在tree视图中新增该字段

"dispatch_imitate_list_view" model="ir.ui.view"> "name">order.dispatch.imitate "model">order.dispatch.imitate "arch" type="xml"> "imitate_list_view_button" create="false" import="false" editable="top" class="order_dispatch_list"> "button_view" type="html" readonly="1"/> "imitate_name" readonly="1" widget="imitate_fields"/> "imitate_note" /> "create_uid" string="创建人" readonly="1"/> "create_date" string="创建时间" readonly="1"/>  

#3

对button_view字段绑定widget

从而实现按钮点击事件

#固定表头和列

在实际业务场景中难免会碰到Tree视图里面字段数过多,主要字段和操作列大都集中在前几列的情况。

那么在tree视图横向滑动的时候,前几列就会隐藏。

Tree视图上如何固定表头和列呢?不需要在应用商店单独下载模块,几行css就可以实现以下功能~


#1

给需要固定表头的Tree视图定义class

这样我们在元素中获取到该DOM

"materiel_tree_view" model="ir.ui.view"> "name">materiel "model">materiel "arch" type="xml"> "false" js_class="material_button" class="materiel_tree material_table" editable="top" limit="50"> "button_view" type="html" widget="material_operate"/> "prod_line1" /> "prod_line2" /> ......

#2

设置表头样式+背景色

向上滑动阅览

// Sticky Header & Footer in List View.material_table { .table-responsive { .o_list_table { td:first-child, th:first-child { position:sticky; left:0; /* 首行永远固定在左侧 */ z-index:1; background-color:$o-list-footer-bg-color; } thead tr th { position:sticky !important; top:0; /* 列首永远固定在头部  */ } th:first-child{ z-index:2; background-color:$o-list-footer-bg-color; }  thead tr:nth-child(1) th { background-color: $o-list-footer-bg-color; } thead tr th:nth-child(2)  { z-index:2; left:40px; background-color: $o-list-footer-bg-color; }  tbody tr td:nth-child(2)  { position:sticky; z-index:1; left:40px; background-color: $o-list-footer-bg-color; } tfoot, tfoot tr:nth-child(1) td { position: sticky; bottom: 0; } tfoot tr:nth-child(1) td { background-color: $o-list-footer-bg-color; } } }}

#3

看看成果

先是纵向滚动,表头始终固定在第一行

然后是横向滚动,操作列首列始终固定在第一列

以上就是Odoo Tree视图的具体构建方法,是不是感觉并没有想象的复杂呢~

话不多说,赶快动手试试吧!


原文链接: http://mp.weixin.qq.com/s?__biz=Mzg5MzUyOTgwMQ==&mid=2247502697&idx=1&sn=b72f85459e86ce30065f1f4eb57b0887&chksm=c02ff0cff75879d900dfee76d014b90de6841827a2c0fd4329fff3b09d299a12b6b08df078e9#rd