7 changed files with 433 additions and 112 deletions
			
			
		@ -0,0 +1,55 @@ | 
				
			|||
# -*- coding: utf-8 -*- | 
				
			|||
################################################################################### | 
				
			|||
# | 
				
			|||
#    Cybrosys Technologies Pvt. Ltd. | 
				
			|||
# | 
				
			|||
#    Copyright (C) 2021-TODAY Cybrosys Technologies (<https://www.cybrosys.com>). | 
				
			|||
#    Author: Neeraj Krishnan V M(<https://www.cybrosys.com>) | 
				
			|||
# | 
				
			|||
#    This program is free software: you can modify | 
				
			|||
#    it under the terms of the GNU Affero General Public License (AGPL) as | 
				
			|||
#    published by the Free Software Foundation, either version 3 of the | 
				
			|||
#    License, or (at your option) any later version. | 
				
			|||
# | 
				
			|||
#    This program is distributed in the hope that it will be useful, | 
				
			|||
#    but WITHOUT ANY WARRANTY; without even the implied warranty of | 
				
			|||
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
				
			|||
#    GNU Affero General Public License for more details. | 
				
			|||
# | 
				
			|||
#    You should have received a copy of the GNU Affero General Public License | 
				
			|||
#    along with this program.  If not, see <https://www.gnu.org/licenses/>. | 
				
			|||
# | 
				
			|||
################################################################################### | 
				
			|||
 | 
				
			|||
from ast import literal_eval | 
				
			|||
 | 
				
			|||
from odoo import api, models | 
				
			|||
 | 
				
			|||
 | 
				
			|||
class ProductPublicCategory(models.Model): | 
				
			|||
    _inherit = 'product.public.category' | 
				
			|||
    """Inherit the product.public.category model to filter the categories based  | 
				
			|||
       on the user's filter mode""" | 
				
			|||
 | 
				
			|||
    @api.model | 
				
			|||
    def _search_fetch(self, search_detail, search, limit, order): | 
				
			|||
        results, count = super(ProductPublicCategory, self)._search_fetch\ | 
				
			|||
            (search_detail, search, limit, order) | 
				
			|||
        filter_mode = self.env['ir.config_parameter'].sudo().get_param\ | 
				
			|||
            ('filter_mode') | 
				
			|||
        if not self.env.user.active and filter_mode == 'categ_only': | 
				
			|||
            category = literal_eval(self.env['ir.config_parameter'].sudo().get_param( | 
				
			|||
                'website_product_visibility.available_cat_ids')) | 
				
			|||
            results = results.filtered(lambda r: r.id in category) | 
				
			|||
        else: | 
				
			|||
            partner = self.env.user.partner_id | 
				
			|||
            if partner.filter_mode == 'categ_only': | 
				
			|||
                category = partner.website_available_cat_ids.ids | 
				
			|||
                results = results.filtered(lambda r: r.id in category) | 
				
			|||
            elif partner.filter_mode == 'product_only': | 
				
			|||
                products = partner.website_available_product_ids.ids | 
				
			|||
                results = results.filtered(lambda r: any(item in r.product_tmpl_ids.ids | 
				
			|||
                                                         for item in products)) | 
				
			|||
        return results, len(results) | 
				
			|||
 | 
				
			|||
 | 
				
			|||
@ -0,0 +1,58 @@ | 
				
			|||
# -*- coding: utf-8 -*- | 
				
			|||
################################################################################### | 
				
			|||
# | 
				
			|||
#    Cybrosys Technologies Pvt. Ltd. | 
				
			|||
# | 
				
			|||
#    Copyright (C) 2021-TODAY Cybrosys Technologies (<https://www.cybrosys.com>). | 
				
			|||
#    Author: Neeraj Krishnan V M(<https://www.cybrosys.com>) | 
				
			|||
# | 
				
			|||
#    This program is free software: you can modify | 
				
			|||
#    it under the terms of the GNU Affero General Public License (AGPL) as | 
				
			|||
#    published by the Free Software Foundation, either version 3 of the | 
				
			|||
#    License, or (at your option) any later version. | 
				
			|||
# | 
				
			|||
#    This program is distributed in the hope that it will be useful, | 
				
			|||
#    but WITHOUT ANY WARRANTY; without even the implied warranty of | 
				
			|||
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
				
			|||
#    GNU Affero General Public License for more details. | 
				
			|||
# | 
				
			|||
#    You should have received a copy of the GNU Affero General Public License | 
				
			|||
#    along with this program.  If not, see <https://www.gnu.org/licenses/>. | 
				
			|||
# | 
				
			|||
################################################################################### | 
				
			|||
from ast import literal_eval | 
				
			|||
from odoo import api, models | 
				
			|||
 | 
				
			|||
 | 
				
			|||
class ProductTemplate(models.Model): | 
				
			|||
    _inherit = 'product.template' | 
				
			|||
    """Inherit Product Template to add filter mode for website users""" | 
				
			|||
 | 
				
			|||
    @api.model | 
				
			|||
    def _search_fetch(self, search_detail, search, limit, order): | 
				
			|||
        results, count = super(ProductTemplate, self)._search_fetch( | 
				
			|||
            search_detail, search, limit, order) | 
				
			|||
        filter_mode = self.env['ir.config_parameter'].sudo().get_param( | 
				
			|||
            'filter_mode') | 
				
			|||
        if not self.env.user.active: | 
				
			|||
            if filter_mode == 'categ_only': | 
				
			|||
                category = literal_eval( | 
				
			|||
                    self.env['ir.config_parameter'].sudo().get_param( | 
				
			|||
                        'website_product_visibility.available_cat_ids')) | 
				
			|||
                results = results.filtered(lambda r: any( | 
				
			|||
                    item in r.public_categ_ids.ids for item in category)) | 
				
			|||
            elif filter_mode == 'product_only': | 
				
			|||
                products = literal_eval( | 
				
			|||
                    self.env['ir.config_parameter'].sudo().get_param( | 
				
			|||
                        'website_product_visibility.available_product_ids')) | 
				
			|||
                results = results.filtered(lambda r: r.id in products) | 
				
			|||
        else: | 
				
			|||
            partner = self.env.user.partner_id | 
				
			|||
            if partner.filter_mode == 'categ_only': | 
				
			|||
                category = partner.website_available_cat_ids.ids | 
				
			|||
                results = results.filtered(lambda r: any( | 
				
			|||
                    item in r.public_categ_ids.ids for item in category)) | 
				
			|||
            elif partner.filter_mode == 'product_only': | 
				
			|||
                products = partner.website_available_product_ids.ids | 
				
			|||
                results = results.filtered(lambda r: r.id in products) | 
				
			|||
        return results, len(results) | 
				
			|||
@ -0,0 +1,294 @@ | 
				
			|||
/** @odoo-module **/ | 
				
			|||
 | 
				
			|||
import concurrency from 'web.concurrency'; | 
				
			|||
import publicWidget from 'web.public.widget'; | 
				
			|||
 | 
				
			|||
import {qweb} from 'web.core'; | 
				
			|||
import {Markup} from 'web.utils'; | 
				
			|||
 | 
				
			|||
publicWidget.registry.searchBar = publicWidget.Widget.extend({ | 
				
			|||
    selector: '.o_searchbar_form', | 
				
			|||
    xmlDependencies: ['/website/static/src/snippets/s_searchbar/000.xml'], | 
				
			|||
    events: { | 
				
			|||
        'input .search-query': '_onInput', | 
				
			|||
        'focusout': '_onFocusOut', | 
				
			|||
        'keydown .search-query': '_onKeydown', | 
				
			|||
        'search .search-query': '_onSearch', | 
				
			|||
    }, | 
				
			|||
    autocompleteMinWidth: 300, | 
				
			|||
 | 
				
			|||
    /** | 
				
			|||
     * @constructor | 
				
			|||
     */ | 
				
			|||
    init: function () { | 
				
			|||
        this._super.apply(this, arguments); | 
				
			|||
 | 
				
			|||
        this._dp = new concurrency.DropPrevious(); | 
				
			|||
 | 
				
			|||
        this._onInput = _.debounce(this._onInput, 400); | 
				
			|||
        this._onFocusOut = _.debounce(this._onFocusOut, 100); | 
				
			|||
    }, | 
				
			|||
    /** | 
				
			|||
     * @override | 
				
			|||
     */ | 
				
			|||
    start: function () { | 
				
			|||
        this.$input = this.$('.search-query'); | 
				
			|||
 | 
				
			|||
        this.searchType = this.$input.data('searchType'); | 
				
			|||
        this.order = this.$('.o_search_order_by').val(); | 
				
			|||
        this.limit = parseInt(this.$input.data('limit')); | 
				
			|||
        this.displayDescription = this.$input.data('displayDescription'); | 
				
			|||
        this.displayExtraLink = this.$input.data('displayExtraLink'); | 
				
			|||
        this.displayDetail = this.$input.data('displayDetail'); | 
				
			|||
        this.displayImage = this.$input.data('displayImage'); | 
				
			|||
        this.wasEmpty = !this.$input.val(); | 
				
			|||
        // Make it easy for customization to disable fuzzy matching on specific searchboxes
 | 
				
			|||
        this.allowFuzzy = !this.$input.data('noFuzzy'); | 
				
			|||
        if (this.limit) { | 
				
			|||
            this.$input.attr('autocomplete', 'off'); | 
				
			|||
        } | 
				
			|||
 | 
				
			|||
        this.options = { | 
				
			|||
            'displayImage': this.displayImage, | 
				
			|||
            'displayDescription': this.displayDescription, | 
				
			|||
            'displayExtraLink': this.displayExtraLink, | 
				
			|||
            'displayDetail': this.displayDetail, | 
				
			|||
            'allowFuzzy': this.allowFuzzy, | 
				
			|||
        }; | 
				
			|||
        const form = this.$('.o_search_order_by').parents('form'); | 
				
			|||
        for (const field of form.find("input[type='hidden']")) { | 
				
			|||
            this.options[field.name] = field.value; | 
				
			|||
        } | 
				
			|||
        const action = form.attr('action') || window.location.pathname + window.location.search; | 
				
			|||
        const [urlPath, urlParams] = action.split('?'); | 
				
			|||
        if (urlParams) { | 
				
			|||
            for (const keyValue of urlParams.split('&')) { | 
				
			|||
                const [key, value] = keyValue.split('='); | 
				
			|||
                if (value && key !== 'search') { | 
				
			|||
                    this.options[key] = value; | 
				
			|||
                } | 
				
			|||
            } | 
				
			|||
        } | 
				
			|||
        const pathParts = urlPath.split('/'); | 
				
			|||
        for (const index in pathParts) { | 
				
			|||
            const value = pathParts[index]; | 
				
			|||
            if (index > 0 && /-[0-9]+$/.test(value)) { // is sluggish
 | 
				
			|||
                this.options[pathParts[index - 1]] = value; | 
				
			|||
            } | 
				
			|||
        } | 
				
			|||
 | 
				
			|||
        if (this.$input.data('noFuzzy')) { | 
				
			|||
            $("<input type='hidden' name='noFuzzy' value='true'/>").appendTo(this.$input); | 
				
			|||
        } | 
				
			|||
        return this._super.apply(this, arguments); | 
				
			|||
    }, | 
				
			|||
    /** | 
				
			|||
     * @override | 
				
			|||
     */ | 
				
			|||
    destroy() { | 
				
			|||
        this._super(...arguments); | 
				
			|||
        this._render(null); | 
				
			|||
    }, | 
				
			|||
 | 
				
			|||
    //--------------------------------------------------------------------------
 | 
				
			|||
    // Private
 | 
				
			|||
    //--------------------------------------------------------------------------
 | 
				
			|||
 | 
				
			|||
    /** | 
				
			|||
     * @private | 
				
			|||
     */ | 
				
			|||
    _adaptToScrollingParent() { | 
				
			|||
        const bcr = this.el.getBoundingClientRect(); | 
				
			|||
        this.$menu[0].style.setProperty('position', 'fixed', 'important'); | 
				
			|||
        this.$menu[0].style.setProperty('top', `${bcr.bottom}px`, 'important'); | 
				
			|||
        this.$menu[0].style.setProperty('left', `${bcr.left}px`, 'important'); | 
				
			|||
        this.$menu[0].style.setProperty('max-width', `${bcr.width}px`, 'important'); | 
				
			|||
        this.$menu[0].style.setProperty('max-height', `${document.body.clientHeight - bcr.bottom - 16}px`, 'important'); | 
				
			|||
    }, | 
				
			|||
    /** | 
				
			|||
     * @private | 
				
			|||
     */ | 
				
			|||
    async _fetch() { | 
				
			|||
        const res = await this._rpc({ | 
				
			|||
            route: '/website/snippet/autocomplete', | 
				
			|||
            params: { | 
				
			|||
                'search_type': this.searchType, | 
				
			|||
                'term': this.$input.val(), | 
				
			|||
                'order': this.order, | 
				
			|||
                'limit': this.limit, | 
				
			|||
                'max_nb_chars': Math.round(Math.max(this.autocompleteMinWidth, parseInt(this.$el.width())) * 0.22), | 
				
			|||
                'options': this.options, | 
				
			|||
            }, | 
				
			|||
        }); | 
				
			|||
        const fieldNames = [ | 
				
			|||
            'name', | 
				
			|||
            'description', | 
				
			|||
            'extra_link', | 
				
			|||
            'detail', | 
				
			|||
            'detail_strike', | 
				
			|||
            'detail_extra', | 
				
			|||
        ]; | 
				
			|||
        res.results.forEach(record => { | 
				
			|||
            for (const fieldName of fieldNames) { | 
				
			|||
                if (record[fieldName]) { | 
				
			|||
                    if (typeof record[fieldName] === "object") { | 
				
			|||
                        for (const fieldKey of Object.keys(record[fieldName])) { | 
				
			|||
                            record[fieldName][fieldKey] = Markup(record[fieldName][fieldKey]); | 
				
			|||
                        } | 
				
			|||
                    } else { | 
				
			|||
                        record[fieldName] = Markup(record[fieldName]); | 
				
			|||
                    } | 
				
			|||
                } | 
				
			|||
            } | 
				
			|||
        }); | 
				
			|||
        return res; | 
				
			|||
    }, | 
				
			|||
    /** | 
				
			|||
     * @private | 
				
			|||
     */ | 
				
			|||
    _render: function (res) { | 
				
			|||
        console.trace(res.results) | 
				
			|||
        if (this._scrollingParentEl) { | 
				
			|||
            this._scrollingParentEl.removeEventListener('scroll', this._menuScrollAndResizeHandler); | 
				
			|||
            window.removeEventListener('resize', this._menuScrollAndResizeHandler); | 
				
			|||
            delete this._scrollingParentEl; | 
				
			|||
            delete this._menuScrollAndResizeHandler; | 
				
			|||
        } | 
				
			|||
 | 
				
			|||
        const $prevMenu = this.$menu; | 
				
			|||
        this.$el.toggleClass('dropdown show', !!res); | 
				
			|||
        if (res && this.limit) { | 
				
			|||
            const results = res['results']; | 
				
			|||
            let template = 'website.s_searchbar.autocomplete'; | 
				
			|||
            const candidate = template + '.' + this.searchType; | 
				
			|||
            if (qweb.has_template(candidate)) { | 
				
			|||
                template = candidate; | 
				
			|||
            } | 
				
			|||
            console.log('vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv',template) | 
				
			|||
            this.$menu = $(qweb.render(template, { | 
				
			|||
                results: results, | 
				
			|||
                parts: res['parts'], | 
				
			|||
                hasMoreResults: results.length < res['results_count'], | 
				
			|||
                search: this.$input.val(), | 
				
			|||
                fuzzySearch: res['fuzzy_search'], | 
				
			|||
                widget: this, | 
				
			|||
            })); | 
				
			|||
 | 
				
			|||
            // TODO adapt directly in the template in master
 | 
				
			|||
            const mutedItemTextEl = this.$menu.find('span.dropdown-item-text.text-muted')[0]; | 
				
			|||
            if (mutedItemTextEl) { | 
				
			|||
                const newItemTextEl = document.createElement('span'); | 
				
			|||
                newItemTextEl.classList.add('dropdown-item-text'); | 
				
			|||
                mutedItemTextEl.after(newItemTextEl); | 
				
			|||
                mutedItemTextEl.classList.remove('dropdown-item-text'); | 
				
			|||
                newItemTextEl.appendChild(mutedItemTextEl); | 
				
			|||
            } | 
				
			|||
 | 
				
			|||
            this.$menu.css('min-width', this.autocompleteMinWidth); | 
				
			|||
 | 
				
			|||
            // Handle the case where the searchbar is in a mega menu by making
 | 
				
			|||
            // it position:fixed and forcing its size. Note: this could be the
 | 
				
			|||
            // default behavior or at least needed in more cases than the mega
 | 
				
			|||
            // menu only (all scrolling parents). But as a stable fix, it was
 | 
				
			|||
            // easier to fix that case only as a first step, especially since
 | 
				
			|||
            // this cannot generically work on all scrolling parent.
 | 
				
			|||
            const megaMenuEl = this.el.closest('.o_mega_menu'); | 
				
			|||
            if (megaMenuEl) { | 
				
			|||
                const navbarEl = this.el.closest('.navbar'); | 
				
			|||
                const navbarTogglerEl = navbarEl ? navbarEl.querySelector('.navbar-toggler') : null; | 
				
			|||
                if (navbarTogglerEl && navbarTogglerEl.clientWidth < 1) { | 
				
			|||
                    this._scrollingParentEl = megaMenuEl; | 
				
			|||
                    this._menuScrollAndResizeHandler = () => this._adaptToScrollingParent(); | 
				
			|||
                    this._scrollingParentEl.addEventListener('scroll', this._menuScrollAndResizeHandler); | 
				
			|||
                    window.addEventListener('resize', this._menuScrollAndResizeHandler); | 
				
			|||
 | 
				
			|||
                    this._adaptToScrollingParent(); | 
				
			|||
                } | 
				
			|||
            } | 
				
			|||
 | 
				
			|||
            this.$el.append(this.$menu); | 
				
			|||
 | 
				
			|||
            this.$el.find('button.extra_link').on('click', function (event) { | 
				
			|||
                event.preventDefault(); | 
				
			|||
                window.location.href = event.currentTarget.dataset['target']; | 
				
			|||
            }); | 
				
			|||
            this.$el.find('.s_searchbar_fuzzy_submit').on('click', (event) => { | 
				
			|||
                event.preventDefault(); | 
				
			|||
                this.$input.val(res['fuzzy_search']); | 
				
			|||
                const form = this.$('.o_search_order_by').parents('form'); | 
				
			|||
                form.submit(); | 
				
			|||
            }); | 
				
			|||
        } | 
				
			|||
 | 
				
			|||
        if ($prevMenu) { | 
				
			|||
            $prevMenu.remove(); | 
				
			|||
        } | 
				
			|||
    }, | 
				
			|||
 | 
				
			|||
    //--------------------------------------------------------------------------
 | 
				
			|||
    // Handlers
 | 
				
			|||
    //--------------------------------------------------------------------------
 | 
				
			|||
 | 
				
			|||
    /** | 
				
			|||
     * @private | 
				
			|||
     */ | 
				
			|||
    _onInput: function () { | 
				
			|||
        if (!this.limit) { | 
				
			|||
            return; | 
				
			|||
        } | 
				
			|||
        if (this.searchType === 'all' && !this.$input.val().trim().length) { | 
				
			|||
            this._render(); | 
				
			|||
        } else { | 
				
			|||
            this._dp.add(this._fetch()).then(this._render.bind(this)); | 
				
			|||
        } | 
				
			|||
    }, | 
				
			|||
    /** | 
				
			|||
     * @private | 
				
			|||
     */ | 
				
			|||
    _onFocusOut: function () { | 
				
			|||
        if (!this.$el.has(document.activeElement).length) { | 
				
			|||
            this._render(); | 
				
			|||
        } | 
				
			|||
    }, | 
				
			|||
    /** | 
				
			|||
     * @private | 
				
			|||
     */ | 
				
			|||
    _onKeydown: function (ev) { | 
				
			|||
        switch (ev.which) { | 
				
			|||
            case $.ui.keyCode.ESCAPE: | 
				
			|||
                this._render(); | 
				
			|||
                break; | 
				
			|||
            case $.ui.keyCode.UP: | 
				
			|||
            case $.ui.keyCode.DOWN: | 
				
			|||
                ev.preventDefault(); | 
				
			|||
                if (this.$menu) { | 
				
			|||
                    let $element = ev.which === $.ui.keyCode.UP ? this.$menu.children().last() : this.$menu.children().first(); | 
				
			|||
                    $element.focus(); | 
				
			|||
                } | 
				
			|||
                break; | 
				
			|||
            case $.ui.keyCode.ENTER: | 
				
			|||
                this.limit = 0; // prevent autocomplete
 | 
				
			|||
                break; | 
				
			|||
        } | 
				
			|||
    }, | 
				
			|||
    /** | 
				
			|||
     * @private | 
				
			|||
     */ | 
				
			|||
    _onSearch: function (ev) { | 
				
			|||
        if (this.$input[0].value) { // actual search
 | 
				
			|||
            this.limit = 0; // prevent autocomplete
 | 
				
			|||
        } else { // clear button clicked
 | 
				
			|||
            this._render(); // remove existing suggestions
 | 
				
			|||
            ev.preventDefault(); | 
				
			|||
            if (!this.wasEmpty) { | 
				
			|||
                this.limit = 0; // prevent autocomplete
 | 
				
			|||
                const form = this.$('.o_search_order_by').parents('form'); | 
				
			|||
                form.submit(); | 
				
			|||
            } | 
				
			|||
        } | 
				
			|||
    }, | 
				
			|||
}); | 
				
			|||
 | 
				
			|||
export default { | 
				
			|||
    searchBar: publicWidget.registry.searchBar, | 
				
			|||
}; | 
				
			|||
					Loading…
					
					
				
		Reference in new issue