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