diff --git a/addons/website/static/lib/multirange/LICENSE b/addons/website/static/lib/multirange/LICENSE
new file mode 100644
index 00000000000..1943bc613da
--- /dev/null
+++ b/addons/website/static/lib/multirange/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Lea Verou
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/addons/website/static/lib/multirange/multirange_custom.js b/addons/website/static/lib/multirange/multirange_custom.js
new file mode 100644
index 00000000000..aaf3628e337
--- /dev/null
+++ b/addons/website/static/lib/multirange/multirange_custom.js
@@ -0,0 +1,345 @@
+/**
+ * This code has been more that widely inspired by the multirange library
+ * which can be found on https://github.com/LeaVerou/multirange.
+ *
+ * The license file can be found in the same folder as this file.
+ */
+
+odoo.define('website_sale.multirange', function () {
+'use strict';
+
+/**
+ * The multirange library will display the two values as one range input with
+ * two cursors linked by a background. This is to be used with Bootstrap
+ * custom-range inputs.
+ *
+ * There is 2 number inputs on the right and left of the multirange to
+ * display and allow quick and precise value modifications. They are
+ * initialized with the same value provided to the input (min, max, step).
+ *
+ * There is 2 events that are added to the input:
+ * - oldRangeValue: Triggered when the user clicks on a cursor or on focus
+ * of the right or left number input.
+ * - newRangeValue: Triggered when the user release a cursor or on focus
+ * out of the right or left number input.
+ *
+ * The options available for the multirange are :
+ * - On range input or as multirange method options:
+ * - min: minimal value of the range. Default: 0.
+ * - max: maximal value of the range. Default: 100.
+ * - step: precision of the range. Default: 1.
+ * - currency: symbol preceding the displayed values. Default: Empty.
+ * - currencyPosition: currency before/after value. Default: "after".
+ * - value: the current value of the range. Default: "0,100".
+ *
+ * - As multirange method options only:
+ * - displayCounterInput: if we display the value. Default: true.
+ *
+ * Initialization of a multiple range input can be done in two ways:
+ *
+ * Having the inputs with the options as properties and the multiple
+ * property set will let the library initialize it just after DOM loaded.
+ *
+ *
+ *
+ * Providing a HTMLElement and an Object with the desired options.
+ *
+ *
+ *
+ * multirange(document.querySelector('#multi'), {
+ * min: 2,
+ * max: 10,
+ * step: 0.5,
+ * currency: "€",
+ * currencyPosition: "before",
+ * value: "4,8"
+ * rangeWithInput: true,
+ * });
+ */
+
+const HTMLInputElement = window.HTMLInputElement;
+const descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value");
+
+class Multirange {
+ constructor(input, options = {}) {
+ const self = this;
+
+ /* Set default and optionnal values */
+ this.input = input;
+ this.rangeWithInput = options.rangeWithInput === true || this.input.classList.contains('range-with-input');
+ const value = options.value || this.input.getAttribute("value");
+ const values = value === null ? [] : value.split(",");
+ this.input.min = this.min = options.min || this.input.min || 0;
+ this.input.max = this.max = options.max || this.input.max || 100;
+ this.input.step = this.step = options.step || this.input.step || 1;
+ this.currency = options.currency || this.input.dataset.currency || '';
+ this.currencyPosition = options.currencyPosition || this.input.dataset.currencyPosition || 'after';
+
+ /* Wrap the input and add its ghost */
+ this.rangeDiv = document.createElement("div");
+ this.rangeDiv.classList = "multirange-wrapper";
+ this.input.parentNode.insertBefore(this.rangeDiv, this.input.nextSibling);
+ this.rangeDiv.appendChild(this.input);
+ this.ghost = this.input.cloneNode();
+ this.rangeDiv.appendChild(this.ghost);
+
+ this.input.classList.add("multirange", "original");
+ this.ghost.classList.add("multirange", "ghost");
+ this.input.value = values[0] || this.min;
+ this.ghost.value = values[1] || this.max;
+
+ this.inputTipLocator = document.createElement("div");
+ this.inputTipLocator.classList = "tip-locator";
+ this.ghostTipLocator = document.createElement("div");
+ this.ghostTipLocator.classList = "tip-locator";
+ this.rangeDiv.insertBefore(this.ghostTipLocator, this.input.nextSibling);
+ this.rangeDiv.insertBefore(this.inputTipLocator, this.ghost.nextSibling);
+ this.leftCounter = document.createElement("span");
+ this.leftCounter.classList = "multirange-min";
+ this.rightCounter = document.createElement("span");
+ this.rightCounter.classList = "multirange-max";
+ this.tipLocatorOptions = {
+ container: this.rangeDiv,
+ html: true,
+ };
+ $(this.inputTipLocator).popover(Object.assign(
+ this.tipLocatorOptions,
+ {
+ placement: 'top',
+ content: this.leftCounter
+ })
+ );
+ $(this.ghostTipLocator).popover(Object.assign(
+ this.tipLocatorOptions,
+ {
+ placement: 'bottom',
+ content: this.rightCounter
+ })
+ );
+
+ $(this.inputTipLocator).add($(this.ghostTipLocator)).popover('show');
+ /* Add the counterInput */
+ if (this.rangeWithInput) {
+ this.leftInput = document.createElement("input");
+ this.leftInput.type = "number";
+ this.leftInput.style.display = "none";
+ this.leftInput.min = this.min;
+ this.leftInput.max = this.max;
+ this.leftInput.step = this.step;
+ this.rightInput = this.leftInput.cloneNode();
+
+ this.leftInput.classList = "multirange-min";
+ this.rightInput.classList = "multirange-max";
+
+ this.leftCounter.parentNode.appendChild(this.leftInput);
+ this.rightCounter.parentNode.appendChild(this.rightInput);
+ }
+
+ /* Define new properties on range input to link it with ghost, especially for Safari compatibility*/
+ Object.defineProperty(this.input, "originalValue", descriptor.get ? descriptor : {
+ get: function () {
+ return this.value;
+ },
+ set: function (v) {
+ this.value = v;
+ }
+ });
+
+ Object.defineProperties(this.input, {
+ valueLow: {
+ get: function () {
+ return Math.min(this.originalValue, self.ghost.value);
+ },
+ set: function (v) {
+ this.originalValue = v;
+ },
+ enumerable: true
+ },
+ valueHigh: {
+ get: function () {
+ return Math.max(this.originalValue, self.ghost.value);
+ },
+ set: function (v) {
+ self.ghost.value = v;
+ },
+ enumerable: true
+ }
+ });
+
+ if (descriptor.get) {
+ Object.defineProperty(this.input, "value", {
+ get: function () {
+ return this.valueLow + "," + this.valueHigh;
+ },
+ set: function (v) {
+ const values = v.split(",");
+ this.valueLow = values[0];
+ this.valueHigh = values[1];
+ this.update();
+ },
+ enumerable: true
+ });
+ }
+
+ if (typeof this.input.oninput === "function") {
+ this.ghost.oninput = this.input.oninput.bind(this.input);
+ }
+
+ this.input.addEventListener("input", this.update.bind(this));
+ this.ghost.addEventListener("input", this.update.bind(this));
+
+ this.input.addEventListener("touchstart", this.saveOldValues.bind(this));
+ this.ghost.addEventListener("touchstart", this.saveOldValues.bind(this));
+ this.input.addEventListener("mousedown", this.saveOldValues.bind(this));
+ this.ghost.addEventListener("mousedown", this.saveOldValues.bind(this));
+
+ this.input.addEventListener("touchend", this.dispatchNewValueEvent.bind(this));
+ this.ghost.addEventListener("touchend", this.dispatchNewValueEvent.bind(this));
+ this.input.addEventListener("mouseup", this.dispatchNewValueEvent.bind(this));
+ this.ghost.addEventListener("mouseup", this.dispatchNewValueEvent.bind(this));
+
+ if (this.rangeWithInput) {
+ this.leftCounter.addEventListener("click", this.counterInputSwitch.bind(this));
+ this.rightCounter.addEventListener("click", this.counterInputSwitch.bind(this));
+
+ this.leftInput.addEventListener("blur", this.counterInputSwitch.bind(this));
+ this.rightInput.addEventListener("blur", this.counterInputSwitch.bind(this));
+
+ this.leftInput.addEventListener("keypress", this.elementBlurOnEnter.bind(this));
+ this.rightInput.addEventListener("keypress", this.elementBlurOnEnter.bind(this));
+
+ this.leftInput.addEventListener("focus", this.selectAllFocus.bind(this));
+ this.rightInput.addEventListener("focus", this.selectAllFocus.bind(this));
+ }
+ this.update();
+ $(this.rangeDiv).addClass('visible');
+
+ }
+
+ update() {
+ const low = 100 * (this.input.valueLow - this.min) / (this.max - this.min);
+ const high = 100 * (this.input.valueHigh - this.min) / (this.max - this.min);
+ const tipOffsetLow = 8 - (low * 0.15);
+ const tipOffsetHigh = 8 - (high * 0.15);
+ this.rangeDiv.style.setProperty("--low", low + '%');
+ this.rangeDiv.style.setProperty("--high", high + '%');
+ $(this.inputTipLocator).css({
+ 'left': `calc(${low}% + (${tipOffsetLow}px))`,
+ 'top': '3px'
+ });
+ $(this.ghostTipLocator).css({
+ 'left': `calc(${high}% + (${tipOffsetHigh}px))`,
+ 'top': '18px'
+ });
+ $(this.inputTipLocator).add($(this.ghostTipLocator)).popover('update');
+ this.counterInputUpdate();
+ }
+
+ counterInputUpdate() {
+ if (this.rangeWithInput) {
+ this.leftCounter.innerText = this.formatNumber(this.input.valueLow);
+ this.rightCounter.innerText = this.formatNumber(this.input.valueHigh);
+ this.leftInput.value = this.input.valueLow;
+ this.rightInput.value = this.input.valueHigh;
+ }
+ }
+
+ counterInputSwitch(ev) {
+ let counter = this.rightCounter;
+ let input = this.rightInput;
+ if (ev.currentTarget.classList.contains('multirange-min')) {
+ counter = this.leftCounter;
+ input = this.leftInput;
+ }
+ if (counter.style.display === "none") {
+ this.input.valueLow = this.leftInput.value;
+ this.input.valueHigh = this.rightInput.value;
+ this.dispatchNewValueEvent();
+ this.update();
+ counter.style.display = "";
+ input.style.display = "none";
+ } else {
+ counter.style.display = "none";
+ input.style.display = "";
+ this.saveOldValues();
+ // Hack because firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=1057858
+ window.setTimeout(function () {
+ input.focus();
+ }, 1);
+ }
+ }
+
+ elementBlurOnEnter(ev) {
+ if (ev.key === "Enter") {
+ ev.currentTarget.blur();
+ }
+ }
+
+ selectAllFocus(ev) {
+ ev.currentTarget.select();
+ }
+
+ dispatchNewValueEvent() {
+ if (this._previousMaxPrice !== this.input.valueHigh || this._previousMinPrice !== this.input.valueLow) {
+ this.input.dispatchEvent(new CustomEvent("newRangeValue", {
+ bubbles: true,
+ }));
+ }
+ }
+
+ saveOldValues() {
+ this._previousMinPrice = this.input.valueLow;
+ this._previousMaxPrice = this.input.valueHigh;
+ }
+
+ formatNumber(number) {
+ number = String(number).split('.');
+ if (number[1] && number[1].length === 1) {
+ number[1] += '0';
+ }
+ let formatedNumber = number[0].replace(/(?=(?:\d{3})+$)(?!\b)/g, ',') + (number[1] ? '.' + number[1] : '.00');
+ if (this.currency.length) {
+ if (this.currencyPosition === 'after') {
+ formatedNumber = formatedNumber + ' ' + this.currency;
+ } else {
+ formatedNumber = this.currency + ' ' + formatedNumber;
+ }
+ }
+ return formatedNumber;
+ }
+}
+
+function multirange(input, options) {
+ if (input.classList.contains('multirange')) {
+ return;
+ }
+ new Multirange(input, options);
+}
+
+return {
+ Multirange: Multirange,
+ init: multirange,
+};
+});
+
+odoo.define('website_sale.multirange.instance', function (require) {
+'use strict';
+
+const publicWidget = require('web.public.widget');
+const multirange = require('website_sale.multirange');
+
+publicWidget.registry.WebsiteMultirangeInputs = publicWidget.Widget.extend({
+ selector: 'input[type=range][multiple]:not(.multirange)',
+
+ /**
+ * @override
+ */
+ start() {
+ return this._super.apply(this, arguments).then(() => {
+ multirange.init(this.el);
+ });
+ },
+});
+});
diff --git a/addons/website/static/lib/multirange/multirange_custom.scss b/addons/website/static/lib/multirange/multirange_custom.scss
new file mode 100644
index 00000000000..c170b0f2724
--- /dev/null
+++ b/addons/website/static/lib/multirange/multirange_custom.scss
@@ -0,0 +1,85 @@
+// This code has been more that widely inspired by the multirange library
+// which can be found on https://github.com/LeaVerou/multirange.
+// The license file can be found in the same folder as this file.
+
+ input[type="range"][multiple] {
+ pointer-events: none;
+ visibility: hidden;
+
+ &::-webkit-slider-thumb {
+ pointer-events: auto;
+ }
+
+ &::-moz-range-thumb {
+ pointer-events: auto;
+ }
+
+ &.reverse {
+ direction: rtl;
+ }
+
+ &.multirange {
+ width: 100%;
+ padding: 0;
+ margin: 0;
+ display: inline-block;
+ vertical-align: top;
+
+
+ &::-webkit-slider-thumb {
+ height: 16px;
+ width: 16px;
+ }
+
+ &.original {
+ position: absolute;
+
+ &::-webkit-slider-thumb {
+ position: relative;
+ z-index: 2;
+ }
+
+ &::-moz-range-thumb {
+ transform: scale(1); /* Firefox doesn't apply position it seems */
+ z-index: 1;
+ }
+ }
+
+ &::-moz-range-track {
+ border-color: transparent; /* needed to switch Firefox to "styleable" control */
+ }
+
+ &.ghost {
+ border-radius: 45%;
+ position: relative;
+ background: var(--track-background);
+ --track-background: linear-gradient(to right, transparent var(--low), #{theme-color('primary')} var(--low) var(--high), transparent 0) no-repeat 50% / 100% 35%;
+
+ &::-webkit-slider-runnable-track {
+ background: var(--track-background);
+ }
+
+ &::-moz-range-track {
+ background: var(--track-background);
+ }
+ }
+ }
+}
+
+.multirange-wrapper {
+ position: relative;
+ margin: 3rem 0;
+ visibility: hidden;
+
+ input[type="range"][multiple] {
+ visibility: initial;
+ }
+
+ [x-out-of-boundaries] {
+ display: none;
+ }
+}
+
+.tip-locator {
+ position: absolute;
+}
diff --git a/addons/website_sale/__manifest__.py b/addons/website_sale/__manifest__.py
index c68035ee7e8..74e2021d53d 100644
--- a/addons/website_sale/__manifest__.py
+++ b/addons/website_sale/__manifest__.py
@@ -43,6 +43,7 @@
'website_sale/static/src/scss/website_sale.scss',
'website_sale/static/src/scss/website_mail.scss',
'website_sale/static/src/scss/website_sale_frontend.scss',
+ 'website/static/lib/multirange/multirange_custom.scss',
'sale/static/src/scss/sale_portal.scss',
'sale/static/src/scss/product_configurator.scss',
'sale/static/src/js/variant_mixin.js',
@@ -53,6 +54,7 @@
'website_sale/static/src/js/website_sale_validate.js',
'website_sale/static/src/js/website_sale_recently_viewed.js',
'website_sale/static/src/js/website_sale_tracking.js',
+ 'website/static/lib/multirange/multirange_custom.js',
],
'web._assets_primary_variables': [
'website_sale/static/src/scss/primary_variables.scss',
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index 53c584c4e78..2c87750f7a2 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -3,6 +3,7 @@
import json
import logging
from werkzeug.exceptions import Forbidden, NotFound
+from werkzeug.urls import url_decode, url_encode, url_parse
from odoo import fields, http, SUPERUSER_ID, tools, _
from odoo.fields import Command
@@ -207,8 +208,17 @@ class WebsiteSale(http.Controller):
'''/shop/category/''',
'''/shop/category//page/'''
], type='http', auth="public", website=True, sitemap=sitemap_shop)
- def shop(self, page=0, category=None, search='', ppg=False, **post):
+ def shop(self, page=0, category=None, search='', min_price=0.0, max_price=0.0, ppg=False, **post):
add_qty = int(post.get('add_qty', 1))
+ try:
+ min_price = float(min_price)
+ except ValueError:
+ min_price = 0
+ try:
+ max_price = float(max_price)
+ except ValueError:
+ max_price = 0
+
Category = request.env['product.public.category']
if category:
category = Category.search([('id', '=', int(category))], limit=1)
@@ -232,10 +242,9 @@ class WebsiteSale(http.Controller):
attrib_values = [[int(x) for x in v.split("-")] for v in attrib_list if v]
attributes_ids = {v[0] for v in attrib_values}
attrib_set = {v[1] for v in attrib_values}
-
domain = self._get_search_domain(search, category, attrib_values)
- keep = QueryURL('/shop', category=category and int(category), search=search, attrib=attrib_list, order=post.get('order'))
+ keep = QueryURL('/shop', category=category and int(category), search=search, attrib=attrib_list, min_price=min_price, max_price=max_price, order=post.get('order'))
pricelist_context, pricelist = self._get_pricelist_context()
@@ -249,6 +258,37 @@ class WebsiteSale(http.Controller):
Product = request.env['product.template'].with_context(bin_size=True)
+ filter_by_price_enabled = request.website.is_view_active('website_sale.filter_products_price')
+ if filter_by_price_enabled:
+ company_currency = request.website.company_id.currency_id
+ conversion_rate = request.env['res.currency']._get_conversion_rate(company_currency, pricelist.currency_id, request.website.company_id, fields.Date.today())
+
+ # This is ~4 times more efficient than a search for the cheapest and most expensive products
+ from_clause, where_clause, where_params = Product._where_calc(domain).get_sql()
+ query = f"""
+ SELECT COALESCE(MIN(list_price), 0) * {conversion_rate}, COALESCE(MAX(list_price), 0) * {conversion_rate}
+ FROM {from_clause}
+ WHERE {where_clause}
+ """
+ request.env.cr.execute(query, where_params)
+ available_min_price, available_max_price = request.env.cr.fetchone()
+
+ if min_price or max_price:
+ # The if/else condition in the min_price / max_price value assignment
+ # tackles the case where we switch to a list of products with different
+ # available min / max prices than the ones set in the previous page.
+ # In order to have logical results and not yield empty product lists, the
+ # price filter is set to their respective available prices when the specified
+ # min exceeds the max, and / or the specified max is lower than the available min.
+ if min_price:
+ min_price = min_price if min_price <= available_max_price else available_min_price
+ post['min_price'] = min_price
+ domain = expression.AND([domain, [('list_price', '>=', min_price / conversion_rate)]])
+ if max_price:
+ max_price = max_price if max_price >= available_min_price else available_max_price
+ post['max_price'] = max_price
+ domain = expression.AND([domain, [('list_price', '<=', max_price / conversion_rate)]])
+
search_product = Product.search(domain, order=self._get_search_order(post))
website_domain = request.website.website_domain()
categs_domain = [('parent_id', '=', False)] + website_domain
@@ -300,6 +340,11 @@ class WebsiteSale(http.Controller):
'search_categories_ids': search_categories.ids,
'layout_mode': layout_mode,
}
+ if filter_by_price_enabled:
+ values['min_price'] = min_price or available_min_price
+ values['max_price'] = max_price or available_max_price
+ values['available_min_price'] = tools.float_round(available_min_price, 2)
+ values['available_max_price'] = tools.float_round(available_max_price, 2)
if category:
values['main_object'] = category
return request.render("website_sale.products", values)
@@ -325,10 +370,12 @@ class WebsiteSale(http.Controller):
category = ProductCategory.browse(int(category)).exists()
attrib_list = request.httprequest.args.getlist('attrib')
+ min_price = request.params.get('min_price')
+ max_price = request.params.get('max_price')
attrib_values = [[int(x) for x in v.split("-")] for v in attrib_list if v]
attrib_set = {v[1] for v in attrib_values}
- keep = QueryURL('/shop', category=category and category.id, search=search, attrib=attrib_list)
+ keep = QueryURL('/shop', category=category and category.id, search=search, attrib=attrib_list, min_price=min_price, max_price=max_price)
categs = ProductCategory.search([('parent_id', '=', False)])
@@ -359,9 +406,32 @@ class WebsiteSale(http.Controller):
def pricelist_change(self, pl_id, **post):
if (pl_id.selectable or pl_id == request.env.user.partner_id.property_product_pricelist) \
and request.website.is_pricelist_available(pl_id.id):
+ redirect_url = request.httprequest.referrer
+ if redirect_url and request.website.is_view_active('website_sale.filter_products_price'):
+ decoded_url = url_parse(redirect_url)
+ args = url_decode(decoded_url.query)
+ min_price = args.get('min_price')
+ max_price = args.get('max_price')
+ if min_price or max_price:
+ previous_price_list = request.website.get_current_pricelist()
+ try:
+ min_price = float(min_price)
+ args['min_price'] = min_price and str(
+ previous_price_list.currency_id._convert(min_price, pl_id.currency_id, request.website.company_id, fields.Date.today(), round=False)
+ )
+ except (ValueError, TypeError):
+ pass
+ try:
+ max_price = float(max_price)
+ args['max_price'] = max_price and str(
+ previous_price_list.currency_id._convert(max_price, pl_id.currency_id, request.website.company_id, fields.Date.today(), round=False)
+ )
+ except (ValueError, TypeError):
+ pass
+ redirect_url = decoded_url.replace(query=url_encode(args)).to_url()
request.session['website_sale_current_pl'] = pl_id.id
request.website.sale_get_order(force_pricelist=pl_id.id)
- return request.redirect(request.httprequest.referrer or '/shop')
+ return request.redirect(redirect_url or '/shop')
@http.route(['/shop/pricelist'], type='http', auth="public", website=True, sitemap=False)
def pricelist(self, promo, **post):
diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js
index 7ae80105d97..da1e435f41d 100644
--- a/addons/website_sale/static/src/js/website_sale.js
+++ b/addons/website_sale/static/src/js/website_sale.js
@@ -962,3 +962,38 @@ publicWidget.registry.websiteSaleProductPageReviews = publicWidget.Widget.extend
},
});
});
+
+odoo.define('website_sale.price_range_option', function (require) {
+'use strict';
+
+const publicWidget = require('web.public.widget');
+
+publicWidget.registry.multirangePriceSelector = publicWidget.Widget.extend({
+ selector: '#o_wsale_price_range_option',
+ events: {
+ 'newRangeValue input[type="range"]': '_onPriceRangeSelected',
+ },
+
+ //----------------------------------------------------------------------
+ // Handlers
+ //----------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onPriceRangeSelected(ev) {
+ const range = ev.currentTarget;
+ const search = $.deparam(window.location.search.substring(1));
+ delete search.min_price;
+ delete search.max_price;
+ if (parseFloat(range.min) !== range.valueLow) {
+ search['min_price'] = range.valueLow;
+ }
+ if (parseFloat(range.max) !== range.valueHigh) {
+ search['max_price'] = range.valueHigh;
+ }
+ window.location.search = $.param(search);
+ },
+});
+});
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 9277029f138..3b68bf5fde4 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -541,6 +541,22 @@
+
+
+
+
+
+
+
+
+
+
+
+