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
114 changes: 114 additions & 0 deletions website_sale_comparison_specification_variant/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

=============================================
Website Sale Comparison Specification Variant
=============================================

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

.. |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/license-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/19.0/website_sale_comparison_specification_variant
: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-19-0/e-commerce-19-0-website_sale_comparison_specification_variant
: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=19.0
:alt: Try me on Runboat

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

On the eCommerce product page, the "Specifications" display added by
``website_sale_comparison`` (shown as a table, an accordion item, or
both, depending on the theme's display options) lists every attribute of
the product, whose values can be restricted to specific variant
combinations via the attribute value's "Exclude for" configuration.

Without this module, that display always lists every configured value of
such an attribute, and never updates when the customer changes the
selected variant. This module makes it show only the value(s) actually
compatible with the combination currently selected on the page, and
refreshes it when the customer changes variant. This applies to every
attribute type, not only non-variant-defining ("informative") ones.

The comparison page (``/shop/compare``) is covered too: for each
compared product, a non-variant-defining attribute only lists the
value(s) compatible with that product's own variant. Variant-defining
attributes are unaffected there, since the comparison page already shows
each product's own actual value for them.

**Table of contents**

.. contents::
:local:

Usage
=====

1. Go to *Website > Products > Attributes*.
2. On an attribute's value, configure "Exclude for" to list the value(s)
(of any attribute type) it is incompatible with.
3. On the eCommerce product page, the "Specifications" display (table
and/or accordion item, depending on the theme's display options) will
only show the value(s) of that attribute compatible with the
currently selected variant, and will update automatically when the
customer changes it. 3.1 On the comparison page (``/shop/compare``),
each compared product's specifications will only show the value(s) of
a non-variant-defining attribute compatible with that product's own
variant.

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_comparison_specification_variant%0Aversion:%2019.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
-------

* Camptocamp

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

- `Camptocamp <https://www.camptocamp.com/>`__:

- Ricardoalso <ricardo.almeidasoares@camptocamp.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/19.0/website_sale_comparison_specification_variant>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
2 changes: 2 additions & 0 deletions website_sale_comparison_specification_variant/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import controllers
from . import models
24 changes: 24 additions & 0 deletions website_sale_comparison_specification_variant/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright 2026 Camptocamp SA (https://www.camptocamp.com).
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Website Sale Comparison Specification Variant",
"version": "19.0.1.0.0",
"development_status": "Beta",
"category": "Website",
"website": "https://github.com/OCA/e-commerce",
"author": "Camptocamp, Odoo Community Association (OCA)",
"license": "AGPL-3",
"application": False,
"installable": True,
"depends": [
"website_sale_comparison",
],
"data": [
"views/templates.xml",
],
"assets": {
"web.assets_frontend": [
"website_sale_comparison_specification_variant/static/src/interactions/**/*",
],
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import variant
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Copyright 2026 Camptocamp SA (https://www.camptocamp.com).
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo.http import request, route

from odoo.addons.website_sale.controllers.variant import WebsiteSaleVariantController


class WebsiteSaleSpecificationExclusionVariantController(WebsiteSaleVariantController):
@route(
"/website_sale/get_combination_info",
type="jsonrpc",
auth="public",
methods=["POST"],
website=True,
readonly=True,
)
def get_combination_info_website(
self,
product_template_id,
product_id,
combination,
add_qty,
uom_id=None,
**kwargs,
):
combination_info = super().get_combination_info_website(
product_template_id=product_template_id,
product_id=product_id,
combination=combination,
add_qty=add_qty,
uom_id=uom_id,
**kwargs,
)
product_template = request.env["product.template"].browse(
int(product_template_id)
)
combination_values = request.env["product.template.attribute.value"].browse(
combination
)
specs_table_html = product_template._get_specs_table_html(combination_values)
if specs_table_html is not None:
combination_info["specs_table_html"] = specs_table_html
specs_accordion_html = product_template._get_specs_accordion_html(
combination_values
)
if specs_accordion_html is not None:
combination_info["specs_accordion_html"] = specs_accordion_html
return combination_info
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import product_product
from . import product_template
from . import product_template_attribute_line
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copyright 2026 Camptocamp SA (https://www.camptocamp.com).
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import models


class ProductProduct(models.Model):
_inherit = "product.product"

def _prepare_categories_for_display(self):
"""Same as the base method, but for non-variant-defining attributes,
only display the values compatible with each compared product's own
variant combination, instead of every configured value.

Variant-defining attributes are left untouched: the base method
already returns each product's own actual value for those, and
filtering it through `_get_display_value_ids` would incorrectly
turn it into every value still compatible with the product's other
attributes, instead of the single value the product actually has.
"""
categories = super()._prepare_categories_for_display()
exclusions_by_template = {}
for attributes in categories.values():
for attribute, values_by_product in attributes.items():
if attribute.create_variant != "no_variant":
continue
for product in values_by_product:
ptal = product.attribute_line_ids.filtered(
lambda line, attribute=attribute: line.attribute_id == attribute
)
if not ptal:
continue
template = product.product_tmpl_id
if template.id not in exclusions_by_template:
exclusions_by_template[template.id] = (
template._get_display_attribute_exclusions()
)
values_by_product[product] = ptal._get_display_value_ids(
product.product_template_attribute_value_ids,
exclusions_by_template[template.id],
)
return categories
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Copyright 2026 Camptocamp SA (https://www.camptocamp.com).
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import models


class ProductTemplate(models.Model):
_inherit = "product.template"

def _get_display_attribute_exclusions(self):
self.ensure_one()
return self._complete_inverse_exclusions(self._get_own_attribute_exclusions())

def _get_specs_render_context(self, combination):
"""Return the qweb rendering context for the specs tables matching
`combination` (recordset of `product.template.attribute.value`), or
`None` if no line has more than one value to filter.
"""
self.ensure_one()
has_filterable_line = any(
len(ptal.value_ids) > 1
for ptal in self.valid_product_template_attribute_line_ids
)
if not has_filterable_line:
return None
ptals = self.valid_product_template_attribute_line_ids
attrib_categories = ptals._prepare_categories_for_display_in_specs_table()
if not attrib_categories:
return None
return {
"attrib_categories": attrib_categories,
"combination": combination,
"attribute_exclusions": self._get_display_attribute_exclusions(),
}

def _get_specs_table_html(self, combination):
"""Render the specs table content for the given `combination`
(recordset of `product.template.attribute.value`), so it can be
refreshed client-side when the customer changes variant.

Called only from the `/website_sale/get_combination_info` controller
"""
self.ensure_one()
render_context = self._get_specs_render_context(combination)
if render_context is None:
return None
return self.env["ir.qweb"]._render(
"website_sale_comparison_specification_variant"
".product_specifications_content",
render_context,
)

def _get_specs_accordion_html(self, combination):
"""Render the specs accordion content for the given `combination`
(recordset of `product.template.attribute.value`), so it can be
refreshed client-side when the customer changes variant, without
disturbing the accordion's open/collapsed state.

Called only from the `/website_sale/get_combination_info` controller
"""
self.ensure_one()
render_context = self._get_specs_render_context(combination)
if render_context is None:
return None
return self.env["ir.qweb"]._render(
"website_sale_comparison_specification_variant"
".product_specifications_accordion_content",
render_context,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Copyright 2026 Camptocamp SA (https://www.camptocamp.com).
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import models


class ProductTemplateAttributeLine(models.Model):
_inherit = "product.template.attribute.line"

def _get_display_value_ids(self, combination, exclusions=None):
self.ensure_one()
if not combination:
return self.value_ids
if self.attribute_id.create_variant != "no_variant":
# There is no candidate set to narrow down here: `combination`
# already carries the one value selected for this line, exactly
# like `ProductProduct._prepare_categories_for_display` shows
# each compared product's own actual value.
return (
self.product_template_value_ids & combination
).product_attribute_value_id
if exclusions is None:
exclusions = self.product_tmpl_id._get_display_attribute_exclusions()
other_ids = set(combination.ids) - set(self.product_template_value_ids.ids)
displayed_value_ids = set()
for ptav in self.product_template_value_ids:
conflicts = any(
other_id in exclusions.get(ptav.id, []) for other_id in other_ids
)
if not conflicts:
displayed_value_ids.add(ptav.product_attribute_value_id.id)
return self.env["product.attribute.value"].browse(displayed_value_ids).exists()
3 changes: 3 additions & 0 deletions website_sale_comparison_specification_variant/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- [Camptocamp](https://www.camptocamp.com/):
- Ricardoalso \<<ricardo.almeidasoares@camptocamp.com>\>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
On the eCommerce product page, the "Specifications" display added by
`website_sale_comparison` (shown as a table, an accordion item, or
both, depending on the theme's display options) lists every attribute
of the product, whose values can be restricted to specific variant
combinations via the attribute value's "Exclude for" configuration.

Without this module, that display always lists every configured value
of such an attribute, and never updates when the customer changes the
selected variant. This module makes it show only the value(s) actually
compatible with the combination currently selected on the page, and
refreshes it when the customer changes variant. This applies to every
attribute type, not only non-variant-defining ("informative") ones.

The comparison page (`/shop/compare`) is covered too: for each compared
product, a non-variant-defining attribute only lists the value(s)
compatible with that product's own variant. Variant-defining attributes
are unaffected there, since the comparison page already shows each
product's own actual value for them.
11 changes: 11 additions & 0 deletions website_sale_comparison_specification_variant/readme/USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
1. Go to *Website \> Products \> Attributes*.
2. On an attribute's value, configure "Exclude for" to list the
value(s) (of any attribute type) it is incompatible with.
3. On the eCommerce product page, the "Specifications" display (table
and/or accordion item, depending on the theme's display options)
will only show the value(s) of that attribute compatible with the
currently selected variant, and will update automatically when the
customer changes it.
3.1 On the comparison page (`/shop/compare`), each compared product's
specifications will only show the value(s) of a non-variant-defining
attribute compatible with that product's own variant.
Loading
Loading