暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

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

484




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视图


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



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


      <?xml version="1.0" encoding="utf-8" ?>
      <template id="create_imitate" xml:space="preserve">
          <t t-extend="ListView.buttons">
             <t t-jquery="div.o_list_buttons" t-operation="append">
                 <t t-if="widget.actionViews[0].fieldsView.name == 'order.dispatch.imitate'">
                     <button class="btn btn-primary create_imitate_button" type="button">创建模拟</button>
                 </t>
             </t>
         </t>
      </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属性中)

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



           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 = $('<td>', {class: tdClassName, tabindex: -1});
                 // We register modifiers on the <td> element 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, the <td> element 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 = $('<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视图上


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



                     #6 

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





                     #OdooTree视图 

                     绑定widget 


                     #1 

                    继承web.basic_fields

                    并编写widget


                    向上滑动阅览

                      var FieldMonetary = require('web.basic_fields').FieldMonetary;
                      var fieldRegistry = require('web.field_registry');
                      //widget
                      var 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视图中进行绑定


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




                         #隐藏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 = '<button type="button" name="confirm" class="btn-primary btn_radius o_list_button_edit">编辑</button>'



                               #2 

                              在tree视图中新增该字段


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



                                 #3 

                                对button_view字段绑定widget

                                从而实现按钮点击事件





                                 #固定表头和列 


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


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


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


                                 #1 

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

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


                                  <record id="materiel_tree_view" model="ir.ui.view">
                                     <field name="name">materiel</field>
                                     <field name="model">materiel</field>
                                     <field name="arch" type="xml">
                                         <tree create="false" js_class="material_button" class="materiel_tree material_table" editable="top" limit="50">
                                             <field name="button_view" type="html" widget="material_operate"/>
                                             <field name="prod_line1" />
                                             <field name="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视图的具体构建方法,是不是感觉并没有想象的复杂呢~


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



                                    扫描右下方二维码,

                                    加入群聊,关于 Odoo 你想了解的全都有!



                                     #更多热门文章 


                                    文章转载自神州数码云基地,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                    评论