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