Skip to content

Commit c2f17de

Browse files
committed
[ADD] website_sale_product_attribute_filter_range: range slider filter for numeric attributes
Add a new "Range Slider" display type for product attributes on the website shop. When configured, the attribute appears as a dual-handle range slider (similar to the built-in price filter) instead of the standard checkboxes, allowing customers to filter by numeric ranges. Features: - New "Range Slider" option in attribute Display Type - Configurable step increment per attribute - Automatic numeric value parsing from attribute value names - Works in both desktop sidebar and mobile offcanvas - SQL constraint ensures range attributes use no_variant mode - URL parameter format: attrib_range=attr_id-min-max
1 parent 8d630fd commit c2f17de

23 files changed

Lines changed: 1631 additions & 0 deletions
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
===========================================
2+
Website Sale Product Attribute Range Filter
3+
===========================================
4+
5+
..
6+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
7+
!! This file is generated by oca-gen-addon-readme !!
8+
!! changes will be overwritten. !!
9+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
10+
!! source digest: sha256:0b87e0ca2554ce7f865427ef51df37b1fde97549bb6dd7b3f0cc1f64e54c4fa2
11+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12+
13+
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
14+
:target: https://odoo-community.org/page/development-status
15+
:alt: Beta
16+
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
17+
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
18+
:alt: License: AGPL-3
19+
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fe--commerce-lightgray.png?logo=github
20+
:target: https://github.com/OCA/e-commerce/tree/18.0/website_sale_product_attribute_filter_range
21+
:alt: OCA/e-commerce
22+
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
23+
:target: https://translation.odoo-community.org/projects/e-commerce-18-0/e-commerce-18-0-website_sale_product_attribute_filter_range
24+
:alt: Translate me on Weblate
25+
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
26+
:target: https://runboat.odoo-community.org/builds?repo=OCA/e-commerce&target_branch=18.0
27+
:alt: Try me on Runboat
28+
29+
|badge1| |badge2| |badge3| |badge4| |badge5|
30+
31+
This module adds a **Range Slider** display type for product attributes
32+
in the website shop.
33+
34+
When an attribute is configured with the "Range Slider" display type, it
35+
appears as a dual-handle range slider in the shop sidebar (and mobile
36+
offcanvas), similar to the built-in price filter. This allows customers
37+
to filter products by numeric ranges such as scores, weights,
38+
dimensions, or any other numeric attribute.
39+
40+
The range slider integrates into the standard attribute filter chain,
41+
respecting the natural attribute sequence order alongside other filter
42+
types (checkboxes, pills, colors, etc.).
43+
44+
**Table of contents**
45+
46+
.. contents::
47+
:local:
48+
49+
Configuration
50+
=============
51+
52+
1. Go to *Inventory / Configuration / Attributes* (or *Sales /
53+
Configuration / Attributes*).
54+
2. Open the attribute you want to use as a range filter (e.g., "SCA
55+
Score", "Weight", "Volume").
56+
3. Set the **Display Type** to **Range Slider**.
57+
4. The **Variant Creation** will be automatically set to "Never" since
58+
range sliders are not compatible with variant generation.
59+
5. Set the **Range Step** value (default 0.5). Use 1 for integer steps.
60+
6. For each attribute value, ensure the **Numeric Value** field is set.
61+
The module will attempt to parse it from the value name
62+
automatically, but you can also set it manually.
63+
7. Make sure the attribute **Visibility** is set to "Visible".
64+
65+
Usage
66+
=====
67+
68+
Once configured, a range slider will appear in the website shop sidebar
69+
for each attribute with the "Range Slider" display type. The slider
70+
appears in the natural attribute sequence order, alongside other filter
71+
types.
72+
73+
Customers can drag the slider handles to set minimum and maximum values,
74+
filtering the product list accordingly.
75+
76+
The slider uses the same visual style as the built-in price range filter
77+
and works in both desktop sidebar and mobile offcanvas views.
78+
79+
Bug Tracker
80+
===========
81+
82+
Bugs are tracked on `GitHub Issues <https://github.com/OCA/e-commerce/issues>`_.
83+
In case of trouble, please check there if your issue has already been reported.
84+
If you spotted it first, help us to smash it by providing a detailed and welcomed
85+
`feedback <https://github.com/OCA/e-commerce/issues/new?body=module:%20website_sale_product_attribute_filter_range%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
86+
87+
Do not contact contributors directly about support or help with technical issues.
88+
89+
Credits
90+
=======
91+
92+
Authors
93+
-------
94+
95+
* EthicHub
96+
97+
Contributors
98+
------------
99+
100+
- `EthicHub <https://ethichub.com>`__
101+
102+
Maintainers
103+
-----------
104+
105+
This module is maintained by the OCA.
106+
107+
.. image:: https://odoo-community.org/logo.png
108+
:alt: Odoo Community Association
109+
:target: https://odoo-community.org
110+
111+
OCA, or the Odoo Community Association, is a nonprofit organization whose
112+
mission is to support the collaborative development of Odoo features and
113+
promote its widespread use.
114+
115+
This module is part of the `OCA/e-commerce <https://github.com/OCA/e-commerce/tree/18.0/website_sale_product_attribute_filter_range>`_ project on GitHub.
116+
117+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Copyright 2025 EthicHub
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
4+
from . import controllers
5+
from . import models
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Copyright 2025 EthicHub
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
{
4+
"name": "Website Sale Product Attribute Range Filter",
5+
"version": "18.0.1.0.0",
6+
"category": "Website",
7+
"summary": "Filter products by numeric attribute ranges with a slider",
8+
"author": "EthicHub, Odoo Community Association (OCA)",
9+
"website": "https://github.com/OCA/e-commerce",
10+
"license": "AGPL-3",
11+
"depends": ["website_sale"],
12+
"data": [
13+
"views/product_attribute_views.xml",
14+
"views/templates.xml",
15+
],
16+
"assets": {
17+
"web.assets_frontend": [
18+
"website_sale_product_attribute_filter_range"
19+
"/static/src/interactions/attribute_range.esm.js",
20+
],
21+
},
22+
"installable": True,
23+
"application": False,
24+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Copyright 2025 EthicHub
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
4+
from . import main
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Copyright 2025 EthicHub
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
4+
from odoo import http
5+
from odoo.http import request
6+
from odoo.tools import float_round
7+
8+
from odoo.addons.website_sale.controllers.main import WebsiteSale
9+
10+
11+
class WebsiteSaleAttributeRange(WebsiteSale):
12+
@staticmethod
13+
def _parse_attrib_ranges(attrib_ranges):
14+
"""Parse attribute range query params.
15+
16+
Format: ['attr_id-min-max', ...]
17+
Returns: {attr_id: (min_val, max_val)}
18+
"""
19+
result = {}
20+
for item in attrib_ranges:
21+
parts = item.split("-", 2)
22+
if len(parts) != 3:
23+
continue
24+
try:
25+
attr_id = int(parts[0])
26+
min_val = float(parts[1]) if parts[1] else 0.0
27+
max_val = float(parts[2]) if parts[2] else 0.0
28+
result[attr_id] = (min_val, max_val)
29+
except ValueError:
30+
continue
31+
return result
32+
33+
def _get_search_options(self, **kwargs):
34+
options = super()._get_search_options(**kwargs)
35+
options["attrib_range_dict"] = kwargs.get("attrib_range_dict", {})
36+
return options
37+
38+
def _shop_get_query_url_kwargs(self, *args, **kwargs):
39+
result = super()._shop_get_query_url_kwargs(*args, **kwargs)
40+
result["attrib_range"] = kwargs.get("attrib_range", [])
41+
return result
42+
43+
@http.route()
44+
def shop(
45+
self,
46+
page=0,
47+
category=None,
48+
search="",
49+
min_price=0.0,
50+
max_price=0.0,
51+
tags="",
52+
**post,
53+
):
54+
# Parse range params before calling super so they flow through options
55+
request_args = request.httprequest.args
56+
attrib_ranges = request_args.getlist("attrib_range")
57+
attrib_range_dict = self._parse_attrib_ranges(attrib_ranges)
58+
59+
if attrib_ranges:
60+
post["attrib_range"] = attrib_ranges
61+
post["attrib_range_dict"] = attrib_range_dict
62+
63+
response = super().shop(
64+
page=page,
65+
category=category,
66+
search=search,
67+
min_price=min_price,
68+
max_price=max_price,
69+
tags=tags,
70+
**post,
71+
)
72+
73+
if not hasattr(response, "qcontext"):
74+
return response
75+
76+
# Compute available min/max for each range attribute
77+
ProductAttribute = request.env["product.attribute"]
78+
range_attributes = ProductAttribute.search(
79+
[
80+
("display_type", "=", "range"),
81+
("visibility", "=", "visible"),
82+
]
83+
)
84+
85+
range_data = {}
86+
AttribValue = request.env["product.attribute.value"]
87+
for attr in range_attributes:
88+
values = AttribValue.search(
89+
[("attribute_id", "=", attr.id), ("numeric_value", "!=", 0)]
90+
)
91+
if not values:
92+
continue
93+
numeric_values = values.mapped("numeric_value")
94+
available_min = float_round(min(numeric_values), 2)
95+
available_max = float_round(max(numeric_values), 2)
96+
if available_min == available_max:
97+
continue
98+
99+
current_min, current_max = attrib_range_dict.get(attr.id, (0.0, 0.0))
100+
# Clamp values to available range
101+
if current_min and current_min > available_max:
102+
current_min = available_min
103+
if current_max and current_max < available_min:
104+
current_max = available_max
105+
106+
range_data[attr.id] = {
107+
"attribute": attr,
108+
"available_min": available_min,
109+
"available_max": available_max,
110+
"current_min": current_min or available_min,
111+
"current_max": current_max or available_max,
112+
"step": attr.website_range_step or 0.5,
113+
}
114+
115+
response.qcontext["range_filter_data"] = range_data
116+
response.qcontext["attrib_range_dict"] = attrib_range_dict
117+
118+
# Exclude range attributes from the standard checkbox filter
119+
if range_attributes:
120+
attributes = response.qcontext.get("attributes")
121+
if attributes is not None:
122+
response.qcontext["attributes"] = attributes.filtered(
123+
lambda a: a.id not in range_data
124+
)
125+
126+
return response
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Translation of Odoo Server.
2+
# This file contains the translation of the following modules:
3+
# * website_sale_product_attribute_filter_range
4+
#
5+
msgid ""
6+
msgstr ""
7+
"Project-Id-Version: Odoo Server 18.0\n"
8+
"Report-Msgid-Bugs-To: \n"
9+
"POT-Creation-Date: 2025-01-01 00:00+0000\n"
10+
"PO-Revision-Date: 2025-01-01 00:00+0000\n"
11+
"Last-Translator: \n"
12+
"Language-Team: Spanish <>\n"
13+
"Language: es\n"
14+
"MIME-Version: 1.0\n"
15+
"Content-Type: text/plain; charset=UTF-8\n"
16+
"Content-Transfer-Encoding: \n"
17+
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
18+
19+
#. module: website_sale_product_attribute_filter_range
20+
#: model:product.attribute.value,field_description:website_sale_product_attribute_filter_range.field_product_attribute_value__numeric_value
21+
msgid "Numeric Value"
22+
msgstr "Valor numérico"
23+
24+
#. module: website_sale_product_attribute_filter_range
25+
#: model:product.attribute.value,help:website_sale_product_attribute_filter_range.field_product_attribute_value__numeric_value
26+
msgid ""
27+
"Numeric value used for range filtering on the website. Automatically "
28+
"computed from the value name if it contains a number."
29+
msgstr ""
30+
"Valor numérico utilizado para el filtrado por rango en el sitio web. Se "
31+
"calcula automáticamente a partir del nombre del valor si contiene un número."
32+
33+
#. module: website_sale_product_attribute_filter_range
34+
#: model:product.attribute,field_description:website_sale_product_attribute_filter_range.field_product_attribute__website_range_step
35+
msgid "Range Step"
36+
msgstr "Incremento del rango"
37+
38+
#. module: website_sale_product_attribute_filter_range
39+
#: model:product.attribute,help:website_sale_product_attribute_filter_range.field_product_attribute__website_range_step
40+
msgid ""
41+
"Step increment for the range slider on the website shop. For example, 0.5 "
42+
"allows selecting 80.0, 80.5, 81.0, etc. Use 1 for integer steps."
43+
msgstr ""
44+
"Incremento del deslizador de rango en la tienda web. Por ejemplo, 0.5 "
45+
"permite seleccionar 80.0, 80.5, 81.0, etc. Use 1 para pasos enteros."
46+
47+
#. module: website_sale_product_attribute_filter_range
48+
#: model:product.attribute,selection:website_sale_product_attribute_filter_range.field_product_attribute__display_type
49+
msgid "Range Slider"
50+
msgstr "Deslizador de rango"
51+
52+
#. module: website_sale_product_attribute_filter_range
53+
#: model:ir.model.constraint,message:website_sale_product_attribute_filter_range.constraint_product_attribute__check_range_no_variant
54+
msgid "Range slider display type is not compatible with the creation of variants."
55+
msgstr "El tipo de visualización deslizador de rango no es compatible con la creación de variantes."
56+
57+
#. module: website_sale_product_attribute_filter_range
58+
#: model:ir.ui.view,name:website_sale_product_attribute_filter_range.filter_attribute_range
59+
msgid "Filter Attribute by Range"
60+
msgstr "Filtrar atributo por rango"
61+
62+
#. module: website_sale_product_attribute_filter_range
63+
#: model:ir.ui.view,name:website_sale_product_attribute_filter_range.range_filters_loop
64+
msgid "Range Filters Loop"
65+
msgstr "Bucle de filtros de rango"
66+
67+
#. module: website_sale_product_attribute_filter_range
68+
#: model:ir.ui.view,name:website_sale_product_attribute_filter_range.products_add_range_filters
69+
msgid "Attribute Range Filters in Shop Sidebar"
70+
msgstr "Filtros de rango de atributos en la barra lateral"
71+
72+
#. module: website_sale_product_attribute_filter_range
73+
#: model:ir.ui.view,name:website_sale_product_attribute_filter_range.offcanvas_add_range_filters
74+
msgid "Attribute Range Filters in Offcanvas"
75+
msgstr "Filtros de rango de atributos en el menú móvil"

0 commit comments

Comments
 (0)