From 8aadce1270d4d7d404f9163e2577e8a9320066ae Mon Sep 17 00:00:00 2001 From: Ricardoalso Date: Mon, 10 Aug 2026 11:00:15 +0200 Subject: [PATCH 1/4] [ADD] website_sale_hide_no_variant_attributes Exclude attributes with Variant Creation set to Never from the website variant selector and from combination-exclusion rules, so a hidden informational attribute can no longer gray out or block Add to Cart for a visible variant-defining value. --- .../README.rst | 112 +++++ .../__init__.py | 1 + .../__manifest__.py | 26 + .../models/__init__.py | 1 + .../models/product_template.py | 84 ++++ .../pyproject.toml | 3 + .../readme/CONTRIBUTORS.md | 2 + .../readme/DESCRIPTION.md | 10 + .../readme/USAGE.md | 14 + .../static/description/index.html | 462 ++++++++++++++++++ ...ale_hide_no_variant_attributes_tour.esm.js | 48 ++ .../tests/__init__.py | 2 + ...website_sale_hide_no_variant_attributes.py | 108 ++++ ...te_sale_hide_no_variant_attributes_tour.py | 79 +++ .../views/variant_templates.xml | 25 + 15 files changed, 977 insertions(+) create mode 100644 website_sale_hide_no_variant_attributes/README.rst create mode 100644 website_sale_hide_no_variant_attributes/__init__.py create mode 100644 website_sale_hide_no_variant_attributes/__manifest__.py create mode 100644 website_sale_hide_no_variant_attributes/models/__init__.py create mode 100644 website_sale_hide_no_variant_attributes/models/product_template.py create mode 100644 website_sale_hide_no_variant_attributes/pyproject.toml create mode 100644 website_sale_hide_no_variant_attributes/readme/CONTRIBUTORS.md create mode 100644 website_sale_hide_no_variant_attributes/readme/DESCRIPTION.md create mode 100644 website_sale_hide_no_variant_attributes/readme/USAGE.md create mode 100644 website_sale_hide_no_variant_attributes/static/description/index.html create mode 100644 website_sale_hide_no_variant_attributes/static/tests/tours/website_sale_hide_no_variant_attributes_tour.esm.js create mode 100644 website_sale_hide_no_variant_attributes/tests/__init__.py create mode 100644 website_sale_hide_no_variant_attributes/tests/test_website_sale_hide_no_variant_attributes.py create mode 100644 website_sale_hide_no_variant_attributes/tests/test_website_sale_hide_no_variant_attributes_tour.py create mode 100644 website_sale_hide_no_variant_attributes/views/variant_templates.xml diff --git a/website_sale_hide_no_variant_attributes/README.rst b/website_sale_hide_no_variant_attributes/README.rst new file mode 100644 index 0000000000..2c8940257a --- /dev/null +++ b/website_sale_hide_no_variant_attributes/README.rst @@ -0,0 +1,112 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +======================================= +Website Sale Hide No Variant Attributes +======================================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:95dfd5888476316eb16cd9f97321a9731b2cee9340c7b1f74d59df275af7f160 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_hide_no_variant_attributes + :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_hide_no_variant_attributes + :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| + +This module keeps attributes that never generate a variant (attributes +with their *Variant Creation* setting on *Never*, e.g. purely +informational specs) out of the website product page's variant selector, +and out of the combination-exclusion rules used to gray out incompatible +options. + +Without this module, an informational attribute's value can still +exclude a real, variant-defining value through a configured *Exclude +for* rule, even though the informational attribute itself is never shown +to the shopper as a selectable option — resulting in a visible option +being grayed out because of an attribute the shopper cannot see, with a +tooltip naming it. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +This module requires no configuration: it applies automatically to every +product attribute whose *Variant Creation* is set to *Never*. + +1. On a product, add an attribute with *Variant Creation* set to *Never* + (e.g. an informational spec such as a material or a care + instruction), alongside your normal variant-defining attributes. +2. Optionally, configure an *Exclude for* rule on one of that + attribute's values, targeting one of the variant-defining values. +3. Open the product page on the website: the informational attribute no + longer appears in the variant selector, and the variant-defining + value targeted by its exclusion rule is no longer grayed out because + of it. +4. Add the product to the cart: the hidden informational attribute never + blocks *Add to Cart*, even when its excluded or default value would + have conflicted with the one visibly selected. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Camptocamp + +Contributors +------------ + +- `Camptocamp `__: + + - Ricardoalso + +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 `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/website_sale_hide_no_variant_attributes/__init__.py b/website_sale_hide_no_variant_attributes/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/website_sale_hide_no_variant_attributes/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/website_sale_hide_no_variant_attributes/__manifest__.py b/website_sale_hide_no_variant_attributes/__manifest__.py new file mode 100644 index 0000000000..41cd7c4892 --- /dev/null +++ b/website_sale_hide_no_variant_attributes/__manifest__.py @@ -0,0 +1,26 @@ +# Copyright 2026 Camptocamp SA (https://www.camptocamp.com). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Website Sale Hide No Variant Attributes", + "summary": "Exclude non-variant-defining attributes from the variant " + "selector and its exclusion rules", + "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", + ], + "data": [ + "views/variant_templates.xml", + ], + "assets": { + "web.assets_tests": [ + "website_sale_hide_no_variant_attributes/static/tests/**/*", + ], + }, +} diff --git a/website_sale_hide_no_variant_attributes/models/__init__.py b/website_sale_hide_no_variant_attributes/models/__init__.py new file mode 100644 index 0000000000..e8fa8f6bf1 --- /dev/null +++ b/website_sale_hide_no_variant_attributes/models/__init__.py @@ -0,0 +1 @@ +from . import product_template diff --git a/website_sale_hide_no_variant_attributes/models/product_template.py b/website_sale_hide_no_variant_attributes/models/product_template.py new file mode 100644 index 0000000000..8dac1d6845 --- /dev/null +++ b/website_sale_hide_no_variant_attributes/models/product_template.py @@ -0,0 +1,84 @@ +# 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_attribute_exclusions( + self, parent_combination=None, parent_name=None, combination_ids=None + ): + result = super()._get_attribute_exclusions( + parent_combination=parent_combination, + parent_name=parent_name, + combination_ids=combination_ids, + ) + no_variant_ptav_ids = set( + self.attribute_line_ids.filtered( + lambda line: line.attribute_id.create_variant == "no_variant" + ).product_template_value_ids.ids + ) + if not no_variant_ptav_ids: + return result + + result["exclusions"] = { + ptav_id: ( + [] + if ptav_id in no_variant_ptav_ids + else [ + excluded_id + for excluded_id in excluded_ids + if excluded_id not in no_variant_ptav_ids + ] + ) + for ptav_id, excluded_ids in result["exclusions"].items() + } + result["parent_exclusions"] = { + parent_ptav_id: [ + excluded_id + for excluded_id in excluded_ids + if excluded_id not in no_variant_ptav_ids + ] + for parent_ptav_id, excluded_ids in result["parent_exclusions"].items() + } + result["mapped_attribute_names"] = { + ptav_id: name + for ptav_id, name in result["mapped_attribute_names"].items() + if ptav_id not in no_variant_ptav_ids + } + return result + + def _get_combination_info( + self, + combination=False, + product_id=False, + add_qty=1.0, + uom_id=False, + only_template=False, + ): + combination_info = super()._get_combination_info( + combination=combination, + product_id=product_id, + add_qty=add_qty, + uom_id=uom_id, + only_template=only_template, + ) + if combination_info.get("is_combination_possible"): + return combination_info + # Since no_variant attributes are never shown to the shopper (see the + # website_sale.variants override), they can never be submitted or + # corrected from the website, so they must not be able to block Add + # to Cart either: a missing or template-picked no_variant value would + # otherwise fail the exact-attribute-match/exclusion check in + # `_is_combination_possible` with no way for the shopper to fix it. + resolved_combination = ( + combination_info.get("combination") + or self.env["product.template.attribute.value"] + ) + combination_info["is_combination_possible"] = self._is_combination_possible( + combination=resolved_combination._without_no_variant_attributes(), + ignore_no_variant=True, + ) + return combination_info diff --git a/website_sale_hide_no_variant_attributes/pyproject.toml b/website_sale_hide_no_variant_attributes/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/website_sale_hide_no_variant_attributes/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/website_sale_hide_no_variant_attributes/readme/CONTRIBUTORS.md b/website_sale_hide_no_variant_attributes/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..f27dfd8053 --- /dev/null +++ b/website_sale_hide_no_variant_attributes/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Camptocamp](https://www.camptocamp.com/): + - Ricardoalso \<\> diff --git a/website_sale_hide_no_variant_attributes/readme/DESCRIPTION.md b/website_sale_hide_no_variant_attributes/readme/DESCRIPTION.md new file mode 100644 index 0000000000..754d12ee5c --- /dev/null +++ b/website_sale_hide_no_variant_attributes/readme/DESCRIPTION.md @@ -0,0 +1,10 @@ +This module keeps attributes that never generate a variant (attributes with +their *Variant Creation* setting on *Never*, e.g. purely informational specs) +out of the website product page's variant selector, and out of the +combination-exclusion rules used to gray out incompatible options. + +Without this module, an informational attribute's value can still exclude a +real, variant-defining value through a configured *Exclude for* rule, even +though the informational attribute itself is never shown to the shopper as a +selectable option — resulting in a visible option being grayed out because of +an attribute the shopper cannot see, with a tooltip naming it. diff --git a/website_sale_hide_no_variant_attributes/readme/USAGE.md b/website_sale_hide_no_variant_attributes/readme/USAGE.md new file mode 100644 index 0000000000..bd95cd55c8 --- /dev/null +++ b/website_sale_hide_no_variant_attributes/readme/USAGE.md @@ -0,0 +1,14 @@ +This module requires no configuration: it applies automatically to every +product attribute whose *Variant Creation* is set to *Never*. + +1. On a product, add an attribute with *Variant Creation* set to *Never* + (e.g. an informational spec such as a material or a care instruction), + alongside your normal variant-defining attributes. +2. Optionally, configure an *Exclude for* rule on one of that attribute's + values, targeting one of the variant-defining values. +3. Open the product page on the website: the informational attribute no + longer appears in the variant selector, and the variant-defining value + targeted by its exclusion rule is no longer grayed out because of it. +4. Add the product to the cart: the hidden informational attribute never + blocks *Add to Cart*, even when its excluded or default value would have + conflicted with the one visibly selected. diff --git a/website_sale_hide_no_variant_attributes/static/description/index.html b/website_sale_hide_no_variant_attributes/static/description/index.html new file mode 100644 index 0000000000..cf73154a2d --- /dev/null +++ b/website_sale_hide_no_variant_attributes/static/description/index.html @@ -0,0 +1,462 @@ + + + + + +Website Sale Hide No Variant Attributes + + + +
+ + + +Odoo Community Association + +
+

Website Sale Hide No Variant Attributes

+ +

Beta License: AGPL-3 OCA/e-commerce Translate me on Weblate Try me on Runboat

+

This module keeps attributes that never generate a variant (attributes +with their Variant Creation setting on Never, e.g. purely +informational specs) out of the website product page’s variant selector, +and out of the combination-exclusion rules used to gray out incompatible +options.

+

Without this module, an informational attribute’s value can still +exclude a real, variant-defining value through a configured Exclude +for rule, even though the informational attribute itself is never shown +to the shopper as a selectable option — resulting in a visible option +being grayed out because of an attribute the shopper cannot see, with a +tooltip naming it.

+

Table of contents

+ +
+

Usage

+

This module requires no configuration: it applies automatically to every +product attribute whose Variant Creation is set to Never.

+
    +
  1. On a product, add an attribute with Variant Creation set to Never +(e.g. an informational spec such as a material or a care +instruction), alongside your normal variant-defining attributes.
  2. +
  3. Optionally, configure an Exclude for rule on one of that +attribute’s values, targeting one of the variant-defining values.
  4. +
  5. Open the product page on the website: the informational attribute no +longer appears in the variant selector, and the variant-defining +value targeted by its exclusion rule is no longer grayed out because +of it.
  6. +
  7. Add the product to the cart: the hidden informational attribute never +blocks Add to Cart, even when its excluded or default value would +have conflicted with the one visibly selected.
  8. +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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 project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/website_sale_hide_no_variant_attributes/static/tests/tours/website_sale_hide_no_variant_attributes_tour.esm.js b/website_sale_hide_no_variant_attributes/static/tests/tours/website_sale_hide_no_variant_attributes_tour.esm.js new file mode 100644 index 0000000000..fe9e19b849 --- /dev/null +++ b/website_sale_hide_no_variant_attributes/static/tests/tours/website_sale_hide_no_variant_attributes_tour.esm.js @@ -0,0 +1,48 @@ +// Copyright 2026 Camptocamp SA (https://www.camptocamp.com). +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import {registry} from "@web/core/registry"; + +registry.category("web_tour.tours").add("website_sale_hide_no_variant_attributes", { + steps: () => [ + { + content: + "The informational (non-variant-defining) attribute must not be" + + " rendered in the variant selector.", + trigger: + 'ul.js_add_cart_variants:not(:has(li.variant_attribute[data-attribute-name="Test Material"]))', + }, + { + content: "The variant-defining attribute is rendered normally.", + trigger: + 'li.variant_attribute[data-attribute-name="Test Size"] ' + + 'input[data-value-name="Large"]', + }, + { + content: "Select the size value excluded by the hidden material value.", + trigger: + 'li.variant_attribute[data-attribute-name="Test Size"] ' + + 'input[data-value-name="Large"]', + run: "click", + }, + { + content: + "The selected size is not grayed out by the hidden material's" + + " exclusion rule.", + trigger: + 'li.variant_attribute[data-attribute-name="Test Size"] ' + + 'label:not(.css_not_available):has(input[data-value-name="Large"])', + }, + { + content: "Add the product to the cart.", + trigger: "#add_to_cart", + run: "click", + }, + { + content: + "The product was added to the cart, confirming the hidden" + + " material attribute never blocked Add to Cart.", + trigger: "sup.my_cart_quantity:contains(1)", + }, + ], +}); diff --git a/website_sale_hide_no_variant_attributes/tests/__init__.py b/website_sale_hide_no_variant_attributes/tests/__init__.py new file mode 100644 index 0000000000..9063ef7969 --- /dev/null +++ b/website_sale_hide_no_variant_attributes/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_website_sale_hide_no_variant_attributes +from . import test_website_sale_hide_no_variant_attributes_tour diff --git a/website_sale_hide_no_variant_attributes/tests/test_website_sale_hide_no_variant_attributes.py b/website_sale_hide_no_variant_attributes/tests/test_website_sale_hide_no_variant_attributes.py new file mode 100644 index 0000000000..f84be050f2 --- /dev/null +++ b/website_sale_hide_no_variant_attributes/tests/test_website_sale_hide_no_variant_attributes.py @@ -0,0 +1,108 @@ +# Copyright 2026 Camptocamp SA (https://www.camptocamp.com). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo import Command +from odoo.tests import TransactionCase + +from odoo.addons.website_sale.tests.common import MockRequest + + +class TestWebsiteSaleHideNoVariantAttributes(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.variant_attribute = cls.env["product.attribute"].create( + { + "name": "Test Size", + "create_variant": "always", + "value_ids": [ + Command.create({"name": "Small"}), + Command.create({"name": "Large"}), + ], + } + ) + cls.informational_attribute = cls.env["product.attribute"].create( + { + "name": "Test Material", + "create_variant": "no_variant", + "value_ids": [ + Command.create({"name": "Cotton"}), + Command.create({"name": "Wool"}), + ], + } + ) + cls.product = cls.env["product.template"].create( + { + "name": "Test product with mixed attributes", + "attribute_line_ids": [ + Command.create( + { + "attribute_id": cls.variant_attribute.id, + "value_ids": [ + Command.set(cls.variant_attribute.value_ids.ids) + ], + } + ), + Command.create( + { + "attribute_id": cls.informational_attribute.id, + "value_ids": [ + Command.set(cls.informational_attribute.value_ids.ids) + ], + } + ), + ], + } + ) + cls.variant_ptav_large = cls.product.attribute_line_ids.filtered( + lambda line: line.attribute_id == cls.variant_attribute + ).product_template_value_ids.filtered(lambda ptav: ptav.name == "Large") + cls.informational_ptav_wool = cls.product.attribute_line_ids.filtered( + lambda line: line.attribute_id == cls.informational_attribute + ).product_template_value_ids.filtered(lambda ptav: ptav.name == "Wool") + cls.informational_ptav_wool.exclude_for = [ + Command.create( + { + "product_tmpl_id": cls.product.id, + "value_ids": [Command.link(cls.variant_ptav_large.id)], + } + ) + ] + cls.website = cls.env["website"].get_current_website() + + def test_exclusions_ignore_informational_attribute(self): + exclusions = self.product._get_attribute_exclusions()["exclusions"] + self.assertIn(self.informational_ptav_wool.id, exclusions) + self.assertEqual(exclusions[self.informational_ptav_wool.id], []) + for excluded_ids in exclusions.values(): + self.assertNotIn(self.variant_ptav_large.id, excluded_ids) + + def test_mapped_attribute_names_ignore_informational_attribute(self): + mapped_names = self.product._get_attribute_exclusions()[ + "mapped_attribute_names" + ] + self.assertNotIn(self.informational_ptav_wool.id, mapped_names) + self.assertIn(self.variant_ptav_large.id, mapped_names) + + def test_combination_info_ignores_missing_informational_attribute(self): + # Mimics a combination check triggered after page load: since the + # informational attribute's input is never rendered, the browser can + # never submit a value for it again, so the combination only carries + # the variant-defining value. + with MockRequest(self.env, website=self.website): + combination_info = self.product._get_combination_info( + combination=self.variant_ptav_large + ) + self.assertTrue(combination_info["is_combination_possible"]) + + def test_combination_info_ignores_conflicting_informational_default(self): + # Mimics the very first page render, where the server still picks a + # default value for the hidden informational attribute; here that + # default happens to be the "Wool" value excluded by the rule above. + combination = self.variant_ptav_large | self.informational_ptav_wool + with MockRequest(self.env, website=self.website): + combination_info = self.product._get_combination_info( + combination=combination + ) + self.assertTrue(combination_info["is_combination_possible"]) diff --git a/website_sale_hide_no_variant_attributes/tests/test_website_sale_hide_no_variant_attributes_tour.py b/website_sale_hide_no_variant_attributes/tests/test_website_sale_hide_no_variant_attributes_tour.py new file mode 100644 index 0000000000..5d01cf42a3 --- /dev/null +++ b/website_sale_hide_no_variant_attributes/tests/test_website_sale_hide_no_variant_attributes_tour.py @@ -0,0 +1,79 @@ +# Copyright 2026 Camptocamp SA (https://www.camptocamp.com). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import Command +from odoo.tests import HttpCase, tagged + + +@tagged("post_install", "-at_install") +class TestWebsiteSaleHideNoVariantAttributesTour(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.variant_attribute = cls.env["product.attribute"].create( + { + "name": "Test Size", + "create_variant": "always", + "value_ids": [ + Command.create({"name": "Small"}), + Command.create({"name": "Large"}), + ], + } + ) + cls.informational_attribute = cls.env["product.attribute"].create( + { + "name": "Test Material", + "create_variant": "no_variant", + "value_ids": [ + Command.create({"name": "Cotton"}), + Command.create({"name": "Wool"}), + ], + } + ) + cls.product = cls.env["product.template"].create( + { + "name": "Test product with mixed attributes", + "sale_ok": True, + "is_published": True, + "list_price": 100.0, + "attribute_line_ids": [ + Command.create( + { + "attribute_id": cls.variant_attribute.id, + "value_ids": [ + Command.set(cls.variant_attribute.value_ids.ids) + ], + } + ), + Command.create( + { + "attribute_id": cls.informational_attribute.id, + "value_ids": [ + Command.set(cls.informational_attribute.value_ids.ids) + ], + } + ), + ], + } + ) + variant_ptav_large = cls.product.attribute_line_ids.filtered( + lambda line: line.attribute_id == cls.variant_attribute + ).product_template_value_ids.filtered(lambda ptav: ptav.name == "Large") + informational_ptav_wool = cls.product.attribute_line_ids.filtered( + lambda line: line.attribute_id == cls.informational_attribute + ).product_template_value_ids.filtered(lambda ptav: ptav.name == "Wool") + informational_ptav_wool.exclude_for = [ + Command.create( + { + "product_tmpl_id": cls.product.id, + "value_ids": [Command.link(variant_ptav_large.id)], + } + ) + ] + + def test_hide_no_variant_attributes(self): + self.start_tour( + self.product.website_url, + "website_sale_hide_no_variant_attributes", + login="admin", + ) diff --git a/website_sale_hide_no_variant_attributes/views/variant_templates.xml b/website_sale_hide_no_variant_attributes/views/variant_templates.xml new file mode 100644 index 0000000000..3844a328c5 --- /dev/null +++ b/website_sale_hide_no_variant_attributes/views/variant_templates.xml @@ -0,0 +1,25 @@ + + + + From 7f8fba5d67cca6a8ce9bfdc35ffd726c405ee953 Mon Sep 17 00:00:00 2001 From: mymage Date: Mon, 24 Aug 2026 06:34:59 +0000 Subject: [PATCH 2/4] Translated using Weblate (Italian) Currently translated at 100.0% (5 of 5 strings) Translation: e-commerce-19.0/e-commerce-19.0-website_sale_comparison_specification_variant Translate-URL: https://translation.odoo-community.org/projects/e-commerce-19-0/e-commerce-19-0-website_sale_comparison_specification_variant/it/ --- .../i18n/it.po | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/website_sale_comparison_specification_variant/i18n/it.po b/website_sale_comparison_specification_variant/i18n/it.po index 01a738230d..ad6bc9215c 100644 --- a/website_sale_comparison_specification_variant/i18n/it.po +++ b/website_sale_comparison_specification_variant/i18n/it.po @@ -6,39 +6,41 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 19.0\n" "Report-Msgid-Bugs-To: \n" -"Last-Translator: Automatically generated\n" +"PO-Revision-Date: 2026-08-24 08:46+0000\n" +"Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\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" +"X-Generator: Weblate 5.15.2\n" #. module: website_sale_comparison_specification_variant #: model:ir.model.fields,field_description:website_sale_comparison_specification_variant.field_product_product__display_name #: model:ir.model.fields,field_description:website_sale_comparison_specification_variant.field_product_template__display_name #: model:ir.model.fields,field_description:website_sale_comparison_specification_variant.field_product_template_attribute_line__display_name msgid "Display Name" -msgstr "" +msgstr "Nome visualizzato" #. module: website_sale_comparison_specification_variant #: model:ir.model.fields,field_description:website_sale_comparison_specification_variant.field_product_product__id #: model:ir.model.fields,field_description:website_sale_comparison_specification_variant.field_product_template__id #: model:ir.model.fields,field_description:website_sale_comparison_specification_variant.field_product_template_attribute_line__id msgid "ID" -msgstr "" +msgstr "ID" #. module: website_sale_comparison_specification_variant #: model:ir.model,name:website_sale_comparison_specification_variant.model_product_template msgid "Product" -msgstr "" +msgstr "Prodotto" #. module: website_sale_comparison_specification_variant #: model:ir.model,name:website_sale_comparison_specification_variant.model_product_template_attribute_line msgid "Product Template Attribute Line" -msgstr "" +msgstr "Riga attributo modello prodotto" #. module: website_sale_comparison_specification_variant #: model:ir.model,name:website_sale_comparison_specification_variant.model_product_product msgid "Product Variant" -msgstr "" +msgstr "Variante prodotto" From 201bd098243bcad81d59fa599f491350eced630c Mon Sep 17 00:00:00 2001 From: oca-ci Date: Mon, 24 Aug 2026 14:41:20 +0000 Subject: [PATCH 3/4] [UPD] Update website_sale_hide_no_variant_attributes.pot --- ...ebsite_sale_hide_no_variant_attributes.pot | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 website_sale_hide_no_variant_attributes/i18n/website_sale_hide_no_variant_attributes.pot diff --git a/website_sale_hide_no_variant_attributes/i18n/website_sale_hide_no_variant_attributes.pot b/website_sale_hide_no_variant_attributes/i18n/website_sale_hide_no_variant_attributes.pot new file mode 100644 index 0000000000..6268950ca7 --- /dev/null +++ b/website_sale_hide_no_variant_attributes/i18n/website_sale_hide_no_variant_attributes.pot @@ -0,0 +1,29 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_sale_hide_no_variant_attributes +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 19.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: website_sale_hide_no_variant_attributes +#: model:ir.model.fields,field_description:website_sale_hide_no_variant_attributes.field_product_template__display_name +msgid "Display Name" +msgstr "" + +#. module: website_sale_hide_no_variant_attributes +#: model:ir.model.fields,field_description:website_sale_hide_no_variant_attributes.field_product_template__id +msgid "ID" +msgstr "" + +#. module: website_sale_hide_no_variant_attributes +#: model:ir.model,name:website_sale_hide_no_variant_attributes.model_product_template +msgid "Product" +msgstr "" From 9aaa246c02d63a2c54d8a237f68bf33ef33a47a5 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 24 Aug 2026 14:48:52 +0000 Subject: [PATCH 4/4] [BOT] post-merge updates --- README.md | 1 + setup/_metapackage/pyproject.toml | 3 ++- .../README.rst | 2 +- .../static/description/icon.png | Bin 0 -> 10254 bytes .../static/description/index.html | 4 ++-- 5 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 website_sale_hide_no_variant_attributes/static/description/icon.png diff --git a/README.md b/README.md index 6ee9aee6b1..2a803c1218 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ addon | version | maintainers | summary [website_sale_charge_payment_fee](website_sale_charge_payment_fee/) | 19.0.1.0.0 | miguel-S73 | Payment fee charged to customer [website_sale_checkout_skip_payment](website_sale_checkout_skip_payment/) | 19.0.1.0.3 | | Skip payment for logged users in checkout process [website_sale_comparison_specification_variant](website_sale_comparison_specification_variant/) | 19.0.1.0.0 | | Website Sale Comparison Specification Variant +[website_sale_hide_no_variant_attributes](website_sale_hide_no_variant_attributes/) | 19.0.1.0.0 | | Exclude non-variant-defining attributes from the variant selector and its exclusion rules [website_sale_hide_price](website_sale_hide_price/) | 19.0.1.0.0 | | Hide product prices on the shop [website_sale_one_time_delivery_address](website_sale_one_time_delivery_address/) | 19.0.1.0.0 | | Create one-time delivery contacts from checkout delivery addresses [website_sale_order_type](website_sale_order_type/) | 19.0.1.1.0 | pilarvargas-tecnativa | This module allows sale_order_type to work with website_sale. diff --git a/setup/_metapackage/pyproject.toml b/setup/_metapackage/pyproject.toml index 766e58a9a9..9424453529 100644 --- a/setup/_metapackage/pyproject.toml +++ b/setup/_metapackage/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "odoo-addons-oca-e-commerce" -version = "19.0.20260819.0" +version = "19.0.20260824.0" dependencies = [ "odoo-addon-product_template_multi_link==19.0.*", "odoo-addon-product_template_multi_link_date_span==19.0.*", @@ -12,6 +12,7 @@ dependencies = [ "odoo-addon-website_sale_charge_payment_fee==19.0.*", "odoo-addon-website_sale_checkout_skip_payment==19.0.*", "odoo-addon-website_sale_comparison_specification_variant==19.0.*", + "odoo-addon-website_sale_hide_no_variant_attributes==19.0.*", "odoo-addon-website_sale_hide_price==19.0.*", "odoo-addon-website_sale_one_time_delivery_address==19.0.*", "odoo-addon-website_sale_order_type==19.0.*", diff --git a/website_sale_hide_no_variant_attributes/README.rst b/website_sale_hide_no_variant_attributes/README.rst index 2c8940257a..556a862f0f 100644 --- a/website_sale_hide_no_variant_attributes/README.rst +++ b/website_sale_hide_no_variant_attributes/README.rst @@ -11,7 +11,7 @@ Website Sale Hide No Variant Attributes !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:95dfd5888476316eb16cd9f97321a9731b2cee9340c7b1f74d59df275af7f160 + !! source digest: sha256:f8522ca3f5b49873b42a2d9d8cd49bef262cf9815ca0ecc9db31cc22bcbe5c20 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/website_sale_hide_no_variant_attributes/static/description/icon.png b/website_sale_hide_no_variant_attributes/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1dcc49c24f364e9adf0afbc6fc0bac6dbecdeb11 GIT binary patch literal 10254 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 -Website Sale Hide No Variant Attributes +README.rst