转载自「神州数码云基地」的原创文章,原文链接:https://blog.csdn.net/CBGCampus/article/details/125095633
一、前言
对于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">
<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>
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:绑定注册名
<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);
}
}
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、 看看实际效果
然后是横向滚动,操作列首列始终固定在第一列
感谢神州数码云基地团队整理的资料,非常实用。666