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