基于odoo开发的系统已经给公司用户使用了。在用户使用体验层面,最吐槽的就是odoo的过滤器,用起来不方便。当然这个过滤器也是有它自身的优点的,比如支持自定义查询字段,丰富的查询过滤条件,对常用查询进行保存等。吐槽的地方是如果设置多个查询字段的话,操作不方便。

下图是筛选器的操作界面:

ODOO modules app 区别 odoo好用吗_python


下图是点击应用后的界面:

ODOO modules app 区别 odoo好用吗_ide_02

如上,对已添加的查询字段只支持删除或恢复操作,修改查询条件或者修改查询内容的话只能再添加新的查询字段来完成。比如,我想修改查询币种为美元的话,需要这么操作才可以。

ODOO modules app 区别 odoo好用吗_ODOO modules app 区别_03


而且每次修改的时候都会自动触发后台的数据查询。实际的用户使用方式可能是修改了多个查询的内容,才需再次查询。

为此我们对筛选器做了体验升级处理,先给出最终的效果:

ODOO modules app 区别 odoo好用吗_postgresql_04


上面的这种操作体验方式是不是更合理方便啊,呵呵。

想要实现上面的效果,肯定就要对odoo的前端代码进行改造了。对于开发来说,改造有两种方法:要么直接修改源代码,要么通过扩展继承的方式来改。幸好odoo前端提供了include及extend的两种扩展集成方式,改起来就方便多了,而且是一种插件的方式,对原代码没有任何的侵入。

代码基于odoo13版本,整个改造过程分为以下几步:

1、自定义QWEB的templates文件,实现查询字段的行显示模式以及增加查询按钮

<?xml version="1.0" encoding="UTF-8"?>

<templates xml:space="preserve">
    <div  class="dropdown" t-name="web.DropdownMenu.custom" t-att-class="widget.dropdownStyle.el.class" t-att="widget.dropdownStyle.el.attrs">
        <div>
        <button t-att-class="widget.dropdownStyle.mainButton.class" data-toggle="dropdown" aria-expanded="false" tabindex="-1" data-flip="false" data-boundary="viewport">
            <span t-att-class="widget.dropdownIcon"/> <t t-esc="widget.dropdownTitle"/> <span t-if="widget.dropdownSymbol" t-att-class="widget.dropdownSymbol"/>
        </button>
        <div class="dropdown-menu o_dropdown_menu" role="menu">
    
        </div>
        </div>
    </div>
    <t t-name="CustomFilterMenuGenerator">
        <div t-if="Object.keys(widget.fields).length !== 0 && widget.items.length !== 0" role="separator" class="dropdown-divider o_generator_menu"/>
        <div class="dropdown-item-text o_generator_menu ">
            <button class="btn btn-primary  o_apply_filter_commit" type="button">查询</button>
            <button class="btn btn-primary  o_apply_filter_reset" type="button">重置</button>
            <button class="btn btn-secondary  o_add_custom_filter" type="button">
                <span class="fa fa-plus-circle"/>
                Add Custom Filter
            </button>
        </div>
    
        <div  class="dropdown-item-text o_generator_menu o_add_filter_menu" >
    
        </div>
    </t>

    <t t-name="CustomFilterMenuSpace">
        <div class="o_generator_menu_custom"></div>
    </t>
    <t t-name="SearchView.extended_search.proposition.hiden">
        <div role="menuitem" class="dropdown-item-text o_filter_condition" style="display:none">
            <span class="o_or_filter">or</span>
            <span>
                <select class="o_input o_searchview_extended_prop_field">
                    <t t-foreach="widget.attrs.fields" t-as="field">
                        <option t-att="{'selected': field === widget.attrs.selected ? 'selected' : null}"
                                t-att-value="field.name">
                            <t t-esc="field.string"/>
                        </option>
                    </t>
                </select>
                <span class="o_searchview_extended_delete_prop fa fa-trash-o" role="img" aria-label="Delete" title="Delete"/>
            </span>
            <span>
            <select class="o_input o_searchview_extended_prop_op"/>
            </span>
            <span>
            <span class="o_searchview_extended_prop_value"/>
            </span>
        </div>
    </t>

    <t t-name="SearchView.extended_search.proposition.and">
    <div role="menuitem" class="dropdown-item-text o_filter_condition">
        <span class="o_or_filter"></span>
        <span>
            <select class="o_input o_searchview_extended_prop_field_extra o_searchview_extended_prop_field ">
                <t t-foreach="widget.attrs.fields" t-as="field">
                    <option t-att="{'selected': field === widget.attrs.selected ? 'selected' : null}"
                            t-att-value="field.name">
                        <t t-esc="field.string"/>
                    </option>
                </t>
            </select>
            <span class="o_searchview_extended_delete_prop fa fa-trash-o" role="img" aria-label="Delete" title="Delete" style="position: absolute;top: 11px;left: auto;bottom: auto;right: 5%;cursor: pointer;"/>
        </span>
        <span>
        <select class="o_input o_searchview_extended_prop_op_extra o_searchview_extended_prop_op "/>
        </span>
        <span>
        <span class="o_searchview_extended_prop_value"/>
        </span>
    </div>
</t>

</templates>

涉及到的scss如下:

.o_dropdown_menu {
    min-width: 550px;
}

.o_cp_searchview {
    display:none;
}

.o_searchview_extended_prop_field_extra {
    display:inline-block;
    width:25%;
}

.o_searchview_extended_prop_op_extra {
    display:inline-block;
    width:15%;
}

.o_filters_menu .o_filter_condition {
    max-width: none;
}

.o_searchview_extended_prop_value select {
    display:inline-block;
    width:50%;
}

.o_searchview_extended_prop_value input {
    display:inline-block;
    width:50%;
}

.o_searchview_extended_prop_value div {
    display:inline-block;
    width:25%;
}

2、QWEB元素的js渲染处理

var ExtendedSearchPropositionHide = search_filters.ExtendedSearchProposition.extend({
    template: 'SearchView.extended_search.proposition.hiden',
    get_filter: function () {
        if (this.attrs.selected === null || this.attrs.selected === undefined) {
            return null;
        }
        var field = this.attrs.selected,
            op_select = this.$('.o_searchview_extended_prop_op')[0],
            operator = op_select.options[op_select.selectedIndex];

        return {
            attrs: {
                domain: this.value.get_domain(field, operator),
                string: this.value.get_label(field, operator),
            },
            children: [],
            tag: 'filter',
        };
    },
});

var ExtendedSearchPropositionAnd = search_filters.ExtendedSearchProposition.extend({
    template: 'SearchView.extended_search.proposition.and',
    init: function (parent, fields, domainsel) {
        this._super(parent);
        this.fields = _(fields).chain()
            .map(function (val, key) { return _.extend({}, val, {'name': key}); })
            .filter(function (field) { return !field.deprecated && field.searchable; })
            .sortBy(function (field) {return field.string;})
            .value();
        this.attrs = {_: _, fields: this.fields, selected: null};
        this.value = null;

        var nval = domainsel;
        nval = nval.substring(nval.indexOf("'") + 1, nval.length);
        nval = nval.substring(0, nval.indexOf("'"));
        if(nval == "") {
            nval = domainsel;
            nval = nval.substring(nval.indexOf('"') + 1, nval.length);
            nval = nval.substring(0, nval.indexOf('"'));
        }
        if(nval != "") {
            if (nval=='id'&&!fields.id.searchable){
                nval = this.fields[0].name;
            }
            this.selectFieldName = nval;
        }
    },
    start: function () {
        var parent =  this._super();
        this.$(".o_searchview_extended_prop_field").val(this.selectFieldName);
        parent.then(this.proxy('changed'));
        return parent;
    },
    select_field: function (field) {
        var self = this;
        if(this.attrs.selected !== null && this.attrs.selected !== undefined) {
            this.value.destroy();
            this.value = null;
            this.$('.o_searchview_extended_prop_op').html('');
        }
        this.attrs.selected = field;
        if(field === null || field === undefined) {
            return;
        }

        var type = field.type;

        var model = this.getParent().getParent().getParent().modelName;
        var search_filters_registry = search_filters_registry_registry.getAny([model,'search_filters_registry'])

        var Field = search_filters_registry.getAny([type, "char"]);

        this.value = new Field(this, field);
        _.each(this.value.operators, function (operator) {
            $('<option>', {value: operator.value})
                .text(String(operator.text))
                .appendTo(self.$('.o_searchview_extended_prop_op'));
        });
        var $value_loc = this.$('.o_searchview_extended_prop_value').hide().empty();
        if (type=='datetime'||type=='date'){
            $value_loc = this.$('.o_searchview_extended_prop_value').show().empty();
        }
        this.value.appendTo($value_loc);
    },
    get_filter: function () {
        if (this.attrs.selected === null || this.attrs.selected === undefined) {
            return null;
        }
        var field = this.attrs.selected,
            op_select = this.$('.o_searchview_extended_prop_op')[0],
            operator = op_select.options[op_select.selectedIndex];

        return {
            attrs: {
                domain: this.value.get_domain(field, operator),
                string: this.value.get_label(field, operator),
            },
            children: [],
            tag: 'filter',
        };
    },
});

3、绑定按钮事件

var FilterMenuCustom = FilterMenu.extend({
    template: 'web.DropdownMenu.custom',
    events: _.extend({}, FilterMenu.prototype.events, {
        'click .o_menu_item': '_onItemClick',
        'click .o_apply_filter_commit': '_onAllCommitClick',
        'click .o_apply_filter_reset':'_onFilterReset'
    }),
    init: function (parent, filters, fields) {
        this.oldFilters = filters;
        this._super(parent, filters);
        this.menuArray = [];
        this.fields = fields;
        this.close = parent.close||false;
    },
    start: function () {
        this._super.apply(this, arguments);
        this.$menu.find('.o_generator_menu').remove();
        var $CustomFilterMenuSpace = QWeb.render('CustomFilterMenuSpace', {widget: this});
        this.$menu.append($CustomFilterMenuSpace);
        this._renderMenuItems();
        if (!this.close){
            this.$dropdownReference.dropdown('show');
        }
    },

    _renderMenuItems: function () {
        this.menuArray = [];
        this.$el.find('.o_filter_condition, .dropdown-divider[data-removable="1"]').remove();
        this.items.forEach(element=>{
            if(element.domain != undefined && element.domain.indexOf('&') < 0 && element.domain.indexOf('|') < 0) {
                var prop = new search_filters_custom.ExtendedSearchPropositionAnd(this, this.fields, element.domain);
                this.menuArray.push(prop);
                prop.insertBefore(this.$menu.find('.o_generator_menu_custom'));
            }
        })
        this._renderGeneratorMenu();
    },

    _renderGeneratorMenu: function () {
        this.$el.find('.o_generator_menu').remove();
        if (!this.generatorMenuIsOpen) {
            _.invoke(this.propositions, 'destroy');
            this.propositions = [];
        }
        var $generatorMenu = QWeb.render('CustomFilterMenuGenerator', {widget: this});
        this.$menu.append($generatorMenu);
        this.$addFilterMenu = this.$menu.find('.o_add_filter_menu');
        if (!this.propositions.length) {
            this._appendProposition();
        }
        this.$dropdownReference.dropdown('update');
    },

    _appendProposition: function () {
        // make modern sear_filters code!!! It works but...
        var prop = new search_filters_custom.ExtendedSearchPropositionHide(this, this.fields);
        this.propositions.push(prop);
        this.$('.o_apply_filter').prop('disabled', false);
        return prop.insertBefore(this.$addFilterMenu);
    },

        _onAddCustomFilterClick: function (ev) {
            //ev.preventDefault();
            //ev.stopPropagation();
            //this._toggleCustomFilterMenu();
            ev.preventDefault();
            ev.stopPropagation();
            var filters = _.invoke(this.menuArray, 'get_filter').map(function (preFilter) {
                return {
                    type: 'filter',
                    description: preFilter.attrs.string,
                    domain: Domain.prototype.arrayToString(preFilter.attrs.domain),
                };
            });
            this.items = filters;
            this._commitSearch();
        },

        _onAllCommitClick: function (ev) {
            ev.preventDefault();
            ev.stopPropagation();
            var filters = _.invoke(this.menuArray, 'get_filter').map(function (preFilter) {
                return {
                    type: 'filter',
                    description: preFilter.attrs.string,
                    domain: Domain.prototype.arrayToString(preFilter.attrs.domain),
                };
            });
            this.items = filters;
            this.trigger_up('allCommit', {filters: filters});
        },

        _commitSearch: function () {
            var new_filter = _.invoke(this.propositions, 'get_filter').map(function (preFilter) {
                return {
                    type: 'filter',
                    description: preFilter.attrs.string,
                    domain: Domain.prototype.arrayToString(preFilter.attrs.domain),
                };
            });

            var element = new_filter[0];
            this.items.push(element);
            _.invoke(this.propositions, 'destroy');
            this.propositions = [];
            // this._renderGeneratorMenu();
            // this.$el.find('.o_filter_condition, .dropdown-divider[data-removable="1"]').remove();
            if(element.domain != undefined && element.domain.indexOf('&') < 0 && element.domain.indexOf('|') < 0) {
                var prop = new search_filters_custom.ExtendedSearchPropositionAnd(this, this.fields, element.domain);
                this.menuArray.push(prop);
                prop.insertBefore(this.$menu.find('.o_generator_menu_custom'));
            }
            this._renderGeneratorMenu();
        },

        _onBootstrapClose: function () {
            this.openItems = {};
            this.generatorMenuIsOpen = false;
        },

        _onItemClick: function (event) {
            event.preventDefault();
            event.stopPropagation();
        },
    _onRemoveProposition: function (ev) {
        ev.stopPropagation();
        this.propositions = _.without(this.propositions, ev.target);
        this.menuArray  = _.without(this.menuArray, ev.target);
        if (!this.propositions.length) {
            this.$('.o_apply_filter').prop('disabled', true);
        }
        ev.target.destroy();
    },
    _onFilterReset:function (ev) {
            ev.preventDefault();
            ev.stopPropagation();
            this.items = this.oldFilters;
            this._renderMenuItems();
            // this.$el.find(".o_filters_menu").remove();
            // this.init(this.oldParent,this.oldFilters,this.oldFields);
            // this.start();
    }
    });
    return FilterMenuCustom;

});

以上代码是改造涉及的主要部分,有此需求的读者需要根据自己的需要再加工下即可。