diff --git a/product_visibility_website/__manifest__.py b/product_visibility_website/__manifest__.py index 87295b5d3..e22fc0608 100644 --- a/product_visibility_website/__manifest__.py +++ b/product_visibility_website/__manifest__.py @@ -24,7 +24,7 @@ { 'name': 'Website Product Visibility', 'summary': 'Website Product visibility for Users', - 'version': '15.0.1.0.1', + 'version': '15.0.1.0.2', 'description': """Website Product visibility for Users""", 'author': 'Cybrosys Techno Solution', 'maintainer': 'Cybrosys Techno Solutions', diff --git a/product_visibility_website/controllers/main.py b/product_visibility_website/controllers/main.py index 8368c4202..ae211e362 100644 --- a/product_visibility_website/controllers/main.py +++ b/product_visibility_website/controllers/main.py @@ -73,22 +73,20 @@ class ProductVisibilityCon(WebsiteSale): if categories: domains.append([('public_categ_ids', 'child_of', categories.ids)]) if attrib_values: - print("hello world....") - # attrib = None - # ids = [] - # print("hello...", ids) - # for value in attrib_values: - # if not attrib: - # attrib = value[0] - # ids.append(value[1]) - # elif value[0] == attrib: - # ids.append(value[1]) - # else: - # domains.append([('attribute_line_ids.value_ids', 'in', ids)]) - # attrib = value[0] - # ids = [value[1]] - # if attrib: - # domains.append([('attribute_line_ids.value_ids', 'in', ids)]) + attrib = None + ids = [] + for value in attrib_values: + if not attrib: + attrib = value[0] + ids.append(value[1]) + elif value[0] == attrib: + ids.append(value[1]) + else: + domains.append([('attribute_line_ids.value_ids', 'in', ids)]) + attrib = value[0] + ids = [value[1]] + if attrib: + domains.append([('attribute_line_ids.value_ids', 'in', ids)]) return expression.AND(domains) @@ -231,6 +229,7 @@ class ProductVisibilityCon(WebsiteSale): if category: values['main_object'] = category + return request.render("website_sale.products", values) def availavle_products(self): @@ -239,94 +238,3 @@ class ProductVisibilityCon(WebsiteSale): partner = request.env['res.partner'].sudo().search([('id', '=', user.partner_id.id)]) return partner.website_available_product_ids - # -------------------------------------------------------------------------- - # Products Search Bar - # -------------------------------------------------------------------------- - - @http.route('/shop/products/autocomplete', type='json', auth='public', website=True) - def products_autocomplete(self, term, options={}, **kwargs): - """ - Returns list of products according to the term and product options - - Params: - term (str): search term written by the user - options (dict) - - 'limit' (int), default to 5: number of products to consider - - 'display_description' (bool), default to True - - 'display_price' (bool), default to True - - 'order' (str) - - 'max_nb_chars' (int): max number of characters for the - description if returned - - Returns: - dict (or False if no result) - - 'products' (list): products (only their needed field values) - note: the prices will be strings properly formatted and - already containing the currency - - 'products_count' (int): the number of products in the database - that matched the search query - """ - - user = request.env['res.users'].sudo().search([('id', '=', request.env.user.id)]) - available_categ = available_products = '' - if not user: - mode = request.env['ir.config_parameter'].sudo().get_param('filter_mode') - products = literal_eval( - request.env['ir.config_parameter'].sudo().get_param('website_product_visibility.available_product_ids', - 'False')) - if mode == 'product_only': - available_products = request.env['product.template'].search([('id', 'in', products)]) - cat = literal_eval( - request.env['ir.config_parameter'].sudo().get_param('website_product_visibility.available_cat_ids', - 'False')) - available_categ = request.env['product.public.category'].search([('id', 'in', cat)]) - else: - partner = request.env['res.partner'].sudo().search([('id', '=', user.partner_id.id)]) - mode = partner.filter_mode - if mode != 'categ_only': - available_products = self.availavle_products() - available_categ = partner.website_available_cat_ids - ProductTemplate = request.env['product.template'] - display_description = options.get('display_description', True) - display_price = options.get('display_price', True) - order = self._get_search_order(options) - max_nb_chars = options.get('max_nb_chars', 999) - category = options.get('category') - attrib_values = options.get('attrib_values') - - if not available_products and not available_categ: - domain = self._get_search_domain(term, category, attrib_values, display_description) - else: - domain = self.reset_domain(term,available_categ, available_products, attrib_values,display_description) - products = ProductTemplate.search( - domain, - limit=min(20, options.get('limit', 5)), - order=order - ) - fields = ['id', 'name', 'website_url'] - if display_description: - fields.append('description_sale') - - res = { - 'products': products.read(fields), - 'products_count': ProductTemplate.search_count(domain), - } - - if display_description: - for res_product in res['products']: - desc = res_product['description_sale'] - if desc and len(desc) > max_nb_chars: - res_product['description_sale'] = "%s..." % desc[:(max_nb_chars - 3)] - - if display_price: - FieldMonetary = request.env['ir.qweb.field.monetary'] - monetary_options = { - 'display_currency': request.website.get_current_pricelist().currency_id, - } - for res_product, product in zip(res['products'], products): - combination_info = product._get_combination_info(only_template=True) - res_product.update(combination_info) - res_product['list_price'] = FieldMonetary.value_to_html(res_product['list_price'], monetary_options) - res_product['price'] = FieldMonetary.value_to_html(res_product['price'], monetary_options) - - return res diff --git a/product_visibility_website/models/__init__.py b/product_visibility_website/models/__init__.py index d6bd6554a..21f9a454d 100644 --- a/product_visibility_website/models/__init__.py +++ b/product_visibility_website/models/__init__.py @@ -22,3 +22,5 @@ ################################################################################### from . import website_product_visibility +from . import product_public_category +from . import product_template diff --git a/product_visibility_website/models/product_public_category.py b/product_visibility_website/models/product_public_category.py new file mode 100644 index 000000000..bf807d6e6 --- /dev/null +++ b/product_visibility_website/models/product_public_category.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +################################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2021-TODAY Cybrosys Technologies (). +# Author: Neeraj Krishnan V M() +# +# 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 . +# +################################################################################### + +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) + + diff --git a/product_visibility_website/models/product_template.py b/product_visibility_website/models/product_template.py new file mode 100644 index 000000000..90eccc00e --- /dev/null +++ b/product_visibility_website/models/product_template.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +################################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2021-TODAY Cybrosys Technologies (). +# Author: Neeraj Krishnan V M() +# +# 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 . +# +################################################################################### +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) diff --git a/product_visibility_website/models/website_product_visibility.py b/product_visibility_website/models/website_product_visibility.py index 40bb62444..37a1e9c1d 100644 --- a/product_visibility_website/models/website_product_visibility.py +++ b/product_visibility_website/models/website_product_visibility.py @@ -51,9 +51,11 @@ class WebsiteGuestVisibility(models.TransientModel): product_visibility_guest_user = fields.Boolean(string="Product visibility Guest User") filter_mode = fields.Selection([('product_only', 'Product Wise'), - ('categ_only', 'Category Wise')], string='Filter Mode', default='product_only') + ('categ_only', 'Category Wise')], + string='Filter Mode', default='product_only') - available_product_ids = fields.Many2many('product.template', string='Available Product', + available_product_ids = fields.Many2many('product.template', + string='Available Product', domain="[('is_published', '=', True)]", help='The website will only display products which are within one ' 'of the selected category trees. If no category is specified,' @@ -87,8 +89,10 @@ class WebsiteGuestVisibility(models.TransientModel): @api.model def get_values(self): res = super(WebsiteGuestVisibility, self).get_values() - product_ids = literal_eval(self.env['ir.config_parameter'].sudo().get_param('website_product_visibility.available_product_ids', 'False')) - cat_ids = literal_eval(self.env['ir.config_parameter'].sudo().get_param('website_product_visibility.available_cat_ids', 'False')) + product_ids = literal_eval(self.env['ir.config_parameter'].sudo() + .get_param('website_product_visibility.available_product_ids', 'False')) + cat_ids = literal_eval(self.env['ir.config_parameter'].sudo() + .get_param('website_product_visibility.available_cat_ids', 'False')) mod = self.env['ir.config_parameter'].sudo().get_param('filter_mode') if self.env['ir.config_parameter'].sudo().get_param('filter_mode'): diff --git a/product_visibility_website/static/src/js/000.js b/product_visibility_website/static/src/js/000.js new file mode 100644 index 000000000..7b68f2011 --- /dev/null +++ b/product_visibility_website/static/src/js/000.js @@ -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')) { + $("").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, +};