Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions website_sale_product_attribute_filter_range/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
===========================================
Website Sale Product Attribute Range Filter
===========================================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:0b87e0ca2554ce7f865427ef51df37b1fde97549bb6dd7b3f0cc1f64e54c4fa2
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fe--commerce-lightgray.png?logo=github
:target: https://github.com/OCA/e-commerce/tree/18.0/website_sale_product_attribute_filter_range
:alt: OCA/e-commerce
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/e-commerce-18-0/e-commerce-18-0-website_sale_product_attribute_filter_range
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/e-commerce&target_branch=18.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module adds a **Range Slider** display type for product attributes
in the website shop.

When an attribute is configured with the "Range Slider" display type, it
appears as a dual-handle range slider in the shop sidebar (and mobile
offcanvas), similar to the built-in price filter. This allows customers
to filter products by numeric ranges such as scores, weights,
dimensions, or any other numeric attribute.

The range slider integrates into the standard attribute filter chain,
respecting the natural attribute sequence order alongside other filter
types (checkboxes, pills, colors, etc.).

**Table of contents**

.. contents::
:local:

Configuration
=============

1. Go to *Inventory / Configuration / Attributes* (or *Sales /
Configuration / Attributes*).
2. Open the attribute you want to use as a range filter (e.g., "SCA
Score", "Weight", "Volume").
3. Set the **Display Type** to **Range Slider**.
4. The **Variant Creation** will be automatically set to "Never" since
range sliders are not compatible with variant generation.
5. Set the **Range Step** value (default 0.5). Use 1 for integer steps.
6. For each attribute value, ensure the **Numeric Value** field is set.
The module will attempt to parse it from the value name
automatically, but you can also set it manually.
7. Make sure the attribute **Visibility** is set to "Visible".

Usage
=====

Once configured, a range slider will appear in the website shop sidebar
for each attribute with the "Range Slider" display type. The slider
appears in the natural attribute sequence order, alongside other filter
types.

Customers can drag the slider handles to set minimum and maximum values,
filtering the product list accordingly.

The slider uses the same visual style as the built-in price range filter
and works in both desktop sidebar and mobile offcanvas views.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/e-commerce/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`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**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* EthicHub

Contributors
------------

- `EthicHub <https://ethichub.com>`__

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

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.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
5 changes: 5 additions & 0 deletions website_sale_product_attribute_filter_range/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Copyright 2025 EthicHub
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from . import controllers
from . import models
24 changes: 24 additions & 0 deletions website_sale_product_attribute_filter_range/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright 2025 EthicHub
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Website Sale Product Attribute Range Filter",
"version": "18.0.1.0.0",
"category": "Website",
"summary": "Filter products by numeric attribute ranges with a slider",
"author": "EthicHub, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/e-commerce",
"license": "AGPL-3",
"depends": ["website_sale"],
"data": [
"views/product_attribute_views.xml",
"views/templates.xml",
],
"assets": {
"web.assets_frontend": [
"website_sale_product_attribute_filter_range"
"/static/src/interactions/attribute_range.esm.js",
],
},
"installable": True,
"application": False,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright 2025 EthicHub
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from . import main
126 changes: 126 additions & 0 deletions website_sale_product_attribute_filter_range/controllers/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Copyright 2025 EthicHub
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import http
from odoo.http import request
from odoo.tools import float_round

from odoo.addons.website_sale.controllers.main import WebsiteSale


class WebsiteSaleAttributeRange(WebsiteSale):
@staticmethod
def _parse_attrib_ranges(attrib_ranges):
"""Parse attribute range query params.

Format: ['attr_id-min-max', ...]
Returns: {attr_id: (min_val, max_val)}
"""
result = {}
for item in attrib_ranges:
parts = item.split("-", 2)
if len(parts) != 3:
continue
try:
attr_id = int(parts[0])
min_val = float(parts[1]) if parts[1] else 0.0
max_val = float(parts[2]) if parts[2] else 0.0
result[attr_id] = (min_val, max_val)
except ValueError:
continue
return result

def _get_search_options(self, **kwargs):
options = super()._get_search_options(**kwargs)
options["attrib_range_dict"] = kwargs.get("attrib_range_dict", {})
return options

def _shop_get_query_url_kwargs(self, *args, **kwargs):
result = super()._shop_get_query_url_kwargs(*args, **kwargs)
result["attrib_range"] = kwargs.get("attrib_range", [])
return result

@http.route()
def shop(
self,
page=0,
category=None,
search="",
min_price=0.0,
max_price=0.0,
tags="",
**post,
):
# Parse range params before calling super so they flow through options
request_args = request.httprequest.args
attrib_ranges = request_args.getlist("attrib_range")
attrib_range_dict = self._parse_attrib_ranges(attrib_ranges)

if attrib_ranges:
post["attrib_range"] = attrib_ranges
post["attrib_range_dict"] = attrib_range_dict

response = super().shop(
page=page,
category=category,
search=search,
min_price=min_price,
max_price=max_price,
tags=tags,
**post,
)

if not hasattr(response, "qcontext"):
return response

# Compute available min/max for each range attribute
ProductAttribute = request.env["product.attribute"]
range_attributes = ProductAttribute.search(
[
("display_type", "=", "range"),
("visibility", "=", "visible"),
]
)

range_data = {}
AttribValue = request.env["product.attribute.value"]
for attr in range_attributes:
values = AttribValue.search(
[("attribute_id", "=", attr.id), ("numeric_value", "!=", 0)]
)
if not values:
continue
numeric_values = values.mapped("numeric_value")
available_min = float_round(min(numeric_values), 2)
available_max = float_round(max(numeric_values), 2)
if available_min == available_max:
continue

current_min, current_max = attrib_range_dict.get(attr.id, (0.0, 0.0))
# Clamp values to available range
if current_min and current_min > available_max:
current_min = available_min
if current_max and current_max < available_min:
current_max = available_max

range_data[attr.id] = {
"attribute": attr,
"available_min": available_min,
"available_max": available_max,
"current_min": current_min or available_min,
"current_max": current_max or available_max,
"step": attr.website_range_step or 0.5,
}

response.qcontext["range_filter_data"] = range_data
response.qcontext["attrib_range_dict"] = attrib_range_dict

# Exclude range attributes from the standard checkbox filter
if range_attributes:
attributes = response.qcontext.get("attributes")
if attributes is not None:
response.qcontext["attributes"] = attributes.filtered(
lambda a: a.id not in range_data
)

return response
75 changes: 75 additions & 0 deletions website_sale_product_attribute_filter_range/i18n/es.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_sale_product_attribute_filter_range
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-01 00:00+0000\n"
"PO-Revision-Date: 2025-01-01 00:00+0000\n"
"Last-Translator: \n"
"Language-Team: Spanish <>\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. module: website_sale_product_attribute_filter_range
#: model:product.attribute.value,field_description:website_sale_product_attribute_filter_range.field_product_attribute_value__numeric_value
msgid "Numeric Value"
msgstr "Valor numérico"

#. module: website_sale_product_attribute_filter_range
#: model:product.attribute.value,help:website_sale_product_attribute_filter_range.field_product_attribute_value__numeric_value
msgid ""
"Numeric value used for range filtering on the website. Automatically "
"computed from the value name if it contains a number."
msgstr ""
"Valor numérico utilizado para el filtrado por rango en el sitio web. Se "
"calcula automáticamente a partir del nombre del valor si contiene un número."

#. module: website_sale_product_attribute_filter_range
#: model:product.attribute,field_description:website_sale_product_attribute_filter_range.field_product_attribute__website_range_step
msgid "Range Step"
msgstr "Incremento del rango"

#. module: website_sale_product_attribute_filter_range
#: model:product.attribute,help:website_sale_product_attribute_filter_range.field_product_attribute__website_range_step
msgid ""
"Step increment for the range slider on the website shop. For example, 0.5 "
"allows selecting 80.0, 80.5, 81.0, etc. Use 1 for integer steps."
msgstr ""
"Incremento del deslizador de rango en la tienda web. Por ejemplo, 0.5 "
"permite seleccionar 80.0, 80.5, 81.0, etc. Use 1 para pasos enteros."

#. module: website_sale_product_attribute_filter_range
#: model:product.attribute,selection:website_sale_product_attribute_filter_range.field_product_attribute__display_type
msgid "Range Slider"
msgstr "Deslizador de rango"

#. module: website_sale_product_attribute_filter_range
#: model:ir.model.constraint,message:website_sale_product_attribute_filter_range.constraint_product_attribute__check_range_no_variant
msgid "Range slider display type is not compatible with the creation of variants."
msgstr "El tipo de visualización deslizador de rango no es compatible con la creación de variantes."

#. module: website_sale_product_attribute_filter_range
#: model:ir.ui.view,name:website_sale_product_attribute_filter_range.filter_attribute_range
msgid "Filter Attribute by Range"
msgstr "Filtrar atributo por rango"

#. module: website_sale_product_attribute_filter_range
#: model:ir.ui.view,name:website_sale_product_attribute_filter_range.range_filters_loop
msgid "Range Filters Loop"
msgstr "Bucle de filtros de rango"

#. module: website_sale_product_attribute_filter_range
#: model:ir.ui.view,name:website_sale_product_attribute_filter_range.products_add_range_filters
msgid "Attribute Range Filters in Shop Sidebar"
msgstr "Filtros de rango de atributos en la barra lateral"

#. module: website_sale_product_attribute_filter_range
#: model:ir.ui.view,name:website_sale_product_attribute_filter_range.offcanvas_add_range_filters
msgid "Attribute Range Filters in Offcanvas"
msgstr "Filtros de rango de atributos en el menú móvil"
Loading
Loading