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

一、前言

对于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视图增加按钮

1、创建一个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>

2、为该视图创建一个按钮

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

3、扩展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中,可以配置row和column,从而实现单元格的合并,然而在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、 更新模块便可实现单元格合并了!

四、Odoo__Tree视图绑定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进行searchBar和action的配置,进行隐藏。

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、 页面实图

图片

六、Odoo Tree视图单元格内添加操作按钮视

1、 我们对该模型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,从而实现按钮点击事件

图片

七、Odoo Tree视图固定表头和列

在实际业务场景中难免会碰到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,加入Odoo技术交流群!

  • 5
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值