From ec140258c9f443f406fc60216912c70bfd5deb67 Mon Sep 17 00:00:00 2001 From: Kev-Roche Date: Fri, 27 Mar 2026 09:45:43 +0100 Subject: [PATCH 1/4] [16.0][ADD] product_attribute_value_dependant_mixin --- .../README.rst | 83 ++++ .../__init__.py | 3 + .../__manifest__.py | 16 + .../models/__init__.py | 2 + ...product_attribute_value_dependent_mixin.py | 91 ++++ .../readme/CONTRIBUTORS.rst | 3 + .../readme/DESCRIPTION.rst | 6 + .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 432 ++++++++++++++++++ .../tests/__init__.py | 1 + .../tests/models.py | 12 + ...product_attribute_value_dependent_mixin.py | 40 ++ .../product_attribute_value_dependent_mixin | 1 + .../setup.py | 6 + 14 files changed, 696 insertions(+) create mode 100644 product_attribute_value_dependent_mixin/README.rst create mode 100644 product_attribute_value_dependent_mixin/__init__.py create mode 100644 product_attribute_value_dependent_mixin/__manifest__.py create mode 100644 product_attribute_value_dependent_mixin/models/__init__.py create mode 100644 product_attribute_value_dependent_mixin/models/product_attribute_value_dependent_mixin.py create mode 100644 product_attribute_value_dependent_mixin/readme/CONTRIBUTORS.rst create mode 100644 product_attribute_value_dependent_mixin/readme/DESCRIPTION.rst create mode 100644 product_attribute_value_dependent_mixin/static/description/icon.png create mode 100644 product_attribute_value_dependent_mixin/static/description/index.html create mode 100644 product_attribute_value_dependent_mixin/tests/__init__.py create mode 100644 product_attribute_value_dependent_mixin/tests/models.py create mode 100644 product_attribute_value_dependent_mixin/tests/test_product_attribute_value_dependent_mixin.py create mode 120000 setup/product_attribute_value_dependent_mixin/odoo/addons/product_attribute_value_dependent_mixin create mode 100644 setup/product_attribute_value_dependent_mixin/setup.py diff --git a/product_attribute_value_dependent_mixin/README.rst b/product_attribute_value_dependent_mixin/README.rst new file mode 100644 index 00000000000..65815d6ff4d --- /dev/null +++ b/product_attribute_value_dependent_mixin/README.rst @@ -0,0 +1,83 @@ +======================================= +Product Attribute Value Dependent Mixin +======================================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:ba5d1efad0e1d29166513c1a23b430a0477e8fc7f44fe2afe6dfea4f6718ea82 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fproduct--attribute-lightgray.png?logo=github + :target: https://github.com/OCA/product-attribute/tree/16.0/product_attribute_value_dependent_mixin + :alt: OCA/product-attribute +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/product-attribute-16-0/product-attribute-16-0-product_attribute_value_dependent_mixin + :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/product-attribute&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This technical module introduces a reusable mixin designed to enable any model to establish dependencies on specific product attribute values. +By inheriting from this mixin, developers can easily link business rules, configurations, or records to precise product variants without duplicating complex filtering logic. + +- Automatically computes available products and attribute values based on the selected product.template. +- Supports domain construction to filter attribute values based on context. +- Matching Logic: Includes a is_matching_product(product) method to validate whether a specific product variant satisfies the configured attribute constraints. + +**Table of contents** + +.. contents:: + :local: + +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 +~~~~~~~ + +* Akretion + +Contributors +~~~~~~~~~~~~ + +* `Akretion `_ + + * Chafique Delli + +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/product-attribute `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_attribute_value_dependent_mixin/__init__.py b/product_attribute_value_dependent_mixin/__init__.py new file mode 100644 index 00000000000..83e553ac462 --- /dev/null +++ b/product_attribute_value_dependent_mixin/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import models diff --git a/product_attribute_value_dependent_mixin/__manifest__.py b/product_attribute_value_dependent_mixin/__manifest__.py new file mode 100644 index 00000000000..643dba70339 --- /dev/null +++ b/product_attribute_value_dependent_mixin/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2023 Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Product Attribute Value Dependent Mixin", + "summary": "Mixin to make product attribute values fields on models", + "author": "Akretion,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/product-attribute", + "license": "AGPL-3", + "application": False, + "installable": True, + "category": "Product", + "version": "16.0.1.0.0", + "depends": ["product"], + "data": [], +} diff --git a/product_attribute_value_dependent_mixin/models/__init__.py b/product_attribute_value_dependent_mixin/models/__init__.py new file mode 100644 index 00000000000..781a514fa88 --- /dev/null +++ b/product_attribute_value_dependent_mixin/models/__init__.py @@ -0,0 +1,2 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import product_attribute_value_dependent_mixin diff --git a/product_attribute_value_dependent_mixin/models/product_attribute_value_dependent_mixin.py b/product_attribute_value_dependent_mixin/models/product_attribute_value_dependent_mixin.py new file mode 100644 index 00000000000..be93bcbd145 --- /dev/null +++ b/product_attribute_value_dependent_mixin/models/product_attribute_value_dependent_mixin.py @@ -0,0 +1,91 @@ +# Copyright 2023-2026 Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import itertools +import json + +from odoo import api, fields, models + + +class AttributeValueDependantMixin(models.AbstractModel): + _name = "attribute.value.dependant.mixin" + _description = "Attribute Value Dependant Mixin" + + product_tmpl_id = fields.Many2one( + comodel_name="product.template", + string="Product Template", + ) + product_id = fields.Many2one( + comodel_name="product.product", + string="Product", + domain="[('id', 'in', available_product_ids)]", + ) + available_product_ids = fields.Many2many( + comodel_name="product.product", + string="Available Products", + compute="_compute_available_product_ids", + ) + + attribute_value_ids = fields.Many2many( + comodel_name="product.attribute.value", + string="Attribute Values", + ) + + available_attribute_value_domain = fields.Char( + compute="_compute_available_attribute_value_domain", + ) + + @api.depends("product_tmpl_id") + def _compute_available_product_ids(self): + for rec in self: + rec.available_product_ids = self.env["product.product"].search( + [("product_tmpl_id", "=", self.product_tmpl_id._origin.id)] + ) + + @api.depends( + "product_tmpl_id", + "product_tmpl_id.attribute_line_ids.value_ids", + "available_attribute_value_domain", + ) + def _compute_available_attribute_value_ids(self): + for rec in self: + if rec.product_tmpl_id: + rec.available_attribute_value_ids = rec.product_tmpl_id.mapped( + "attribute_line_ids.value_ids" + ).filtered_domain(json.loads(rec.available_attribute_value_domain)) + else: + rec.available_attribute_value_ids = None + + @api.depends("product_tmpl_id", "product_tmpl_id.attribute_line_ids.value_ids") + def _compute_available_attribute_value_domain(self): + for rec in self: + domain = [] + if rec.product_tmpl_id: + domain = [ + ("id", "in", rec.product_tmpl_id.attribute_line_ids.value_ids.ids) + ] + rec.available_attribute_value_domain = json.dumps(domain) + + def is_matching_product(self, product): + self.ensure_one() + if self.product_tmpl_id != product.product_tmpl_id: + return False + elif self.product_id: + if self.product_id == product: + return True + else: + return False + elif self.attribute_value_ids: + ptav = product.product_template_attribute_value_ids + attr2vals = { + attribute: set(values) + for attribute, values in itertools.groupby( + self.attribute_value_ids, lambda pav: pav.attribute_id + ) + } + for attribute in attr2vals: + if attribute not in ptav.attribute_id: + return False + elif not attr2vals[attribute] & set(ptav.product_attribute_value_id): + return False + return True diff --git a/product_attribute_value_dependent_mixin/readme/CONTRIBUTORS.rst b/product_attribute_value_dependent_mixin/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..4d6f88298d7 --- /dev/null +++ b/product_attribute_value_dependent_mixin/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Akretion `_ + + * Chafique Delli diff --git a/product_attribute_value_dependent_mixin/readme/DESCRIPTION.rst b/product_attribute_value_dependent_mixin/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..ef501c94724 --- /dev/null +++ b/product_attribute_value_dependent_mixin/readme/DESCRIPTION.rst @@ -0,0 +1,6 @@ +This technical module introduces a reusable mixin designed to enable any model to establish dependencies on specific product attribute values. +By inheriting from this mixin, developers can easily link business rules, configurations, or records to precise product variants without duplicating complex filtering logic. + +- Automatically computes available products and attribute values based on the selected product.template. +- Supports domain construction to filter attribute values based on context. +- Matching Logic: Includes a is_matching_product(product) method to validate whether a specific product variant satisfies the configured attribute constraints. diff --git a/product_attribute_value_dependent_mixin/static/description/icon.png b/product_attribute_value_dependent_mixin/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/product_attribute_value_dependent_mixin/static/description/index.html b/product_attribute_value_dependent_mixin/static/description/index.html new file mode 100644 index 00000000000..6f4738d7e33 --- /dev/null +++ b/product_attribute_value_dependent_mixin/static/description/index.html @@ -0,0 +1,432 @@ + + + + + +Product Attribute Value Dependent Mixin + + + +
+

Product Attribute Value Dependent Mixin

+ + +

Beta License: AGPL-3 OCA/product-attribute Translate me on Weblate Try me on Runboat

+

This technical module introduces a reusable mixin designed to enable any model to establish dependencies on specific product attribute values. +By inheriting from this mixin, developers can easily link business rules, configurations, or records to precise product variants without duplicating complex filtering logic.

+
    +
  • Automatically computes available products and attribute values based on the selected product.template.
  • +
  • Supports domain construction to filter attribute values based on context.
  • +
  • Matching Logic: Includes a is_matching_product(product) method to validate whether a specific product variant satisfies the configured attribute constraints.
  • +
+

Table of contents

+ +
+

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

+
    +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

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

+

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

+
+
+
+ + diff --git a/product_attribute_value_dependent_mixin/tests/__init__.py b/product_attribute_value_dependent_mixin/tests/__init__.py new file mode 100644 index 00000000000..f000bbc7f7b --- /dev/null +++ b/product_attribute_value_dependent_mixin/tests/__init__.py @@ -0,0 +1 @@ +from . import test_product_attribute_value_dependent_mixin diff --git a/product_attribute_value_dependent_mixin/tests/models.py b/product_attribute_value_dependent_mixin/tests/models.py new file mode 100644 index 00000000000..f70e7621a61 --- /dev/null +++ b/product_attribute_value_dependent_mixin/tests/models.py @@ -0,0 +1,12 @@ +# Copyright 2023 Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ProductSupplierinfoFake(models.Model): + _name = "product.supplierinfo.fake" + _inherit = ["product.supplierinfo", "attribute.value.dependant.mixin"] + _description = "Product supplierinfo fake model for tests" + + name = fields.Char() diff --git a/product_attribute_value_dependent_mixin/tests/test_product_attribute_value_dependent_mixin.py b/product_attribute_value_dependent_mixin/tests/test_product_attribute_value_dependent_mixin.py new file mode 100644 index 00000000000..315948300ea --- /dev/null +++ b/product_attribute_value_dependent_mixin/tests/test_product_attribute_value_dependent_mixin.py @@ -0,0 +1,40 @@ +# Copyright 2023 Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo_test_helper import FakeModelLoader + +from odoo.tests import TransactionCase + + +class TestProductAttributeValueDependentMixin(TransactionCase, FakeModelLoader): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.loader = FakeModelLoader(cls.env, cls.__module__) + cls.loader.backup_registry() + from .models import ProductSupplierinfoFake + + cls.loader.update_registry((ProductSupplierinfoFake,)) + + # Fake model which inherit from + cls.product_supplierinfo_fake = cls.env["product.supplierinfo.fake"].create( + { + "partner_id": cls.env.ref("base.res_partner_1").id, + "product_tmpl_id": cls.env.ref( + "product.product_product_4_product_template" + ).id, + "price": 100.00, + "currency_id": cls.env.ref("base.USD").id, + "min_qty": 1.0, + "delay": 1.0, + } + ) + + @classmethod + def tearDownClass(cls): + cls.loader.restore_registry() + super(TestProductAttributeValueDependentMixin, cls).tearDownClass() + + def test_product_attribute_value_dependent_mixin(self): + fake_model = self.product_supplierinfo_fake + self.assertTrue(fake_model.available_attribute_value_domain) diff --git a/setup/product_attribute_value_dependent_mixin/odoo/addons/product_attribute_value_dependent_mixin b/setup/product_attribute_value_dependent_mixin/odoo/addons/product_attribute_value_dependent_mixin new file mode 120000 index 00000000000..e460190373d --- /dev/null +++ b/setup/product_attribute_value_dependent_mixin/odoo/addons/product_attribute_value_dependent_mixin @@ -0,0 +1 @@ +../../../../product_attribute_value_dependent_mixin \ No newline at end of file diff --git a/setup/product_attribute_value_dependent_mixin/setup.py b/setup/product_attribute_value_dependent_mixin/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/product_attribute_value_dependent_mixin/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 513773d26230a096075558331abb038442902214 Mon Sep 17 00:00:00 2001 From: Guillaume MASSON Date: Tue, 28 Jul 2026 11:28:22 -1000 Subject: [PATCH 2/4] [IMP] product_attribute_dependent_mixin : backport refactoring from migration to 18.0 (see OCA/product-attribute#2250) --- .../README.rst | 123 ++++++++++- .../__init__.py | 2 - .../__manifest__.py | 5 +- .../migrations/16.0.1.2.0/pre-migrate.py | 14 ++ .../models/__init__.py | 1 - ...product_attribute_value_dependent_mixin.py | 67 ++---- .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 11 +- .../readme/USAGE.rst | 101 +++++++++ .../static/description/index.html | 153 +++++++++++-- .../tests/models.py | 4 +- ...product_attribute_value_dependent_mixin.py | 201 ++++++++++++++++-- 12 files changed, 581 insertions(+), 102 deletions(-) create mode 100644 product_attribute_value_dependent_mixin/migrations/16.0.1.2.0/pre-migrate.py create mode 100644 product_attribute_value_dependent_mixin/readme/USAGE.rst diff --git a/product_attribute_value_dependent_mixin/README.rst b/product_attribute_value_dependent_mixin/README.rst index 65815d6ff4d..1188f5ce9cb 100644 --- a/product_attribute_value_dependent_mixin/README.rst +++ b/product_attribute_value_dependent_mixin/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ======================================= Product Attribute Value Dependent Mixin ======================================= @@ -13,7 +17,7 @@ Product Attribute Value Dependent Mixin .. |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 +.. |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%2Fproduct--attribute-lightgray.png?logo=github @@ -28,18 +32,122 @@ Product Attribute Value Dependent Mixin |badge1| |badge2| |badge3| |badge4| |badge5| -This technical module introduces a reusable mixin designed to enable any model to establish dependencies on specific product attribute values. -By inheriting from this mixin, developers can easily link business rules, configurations, or records to precise product variants without duplicating complex filtering logic. - -- Automatically computes available products and attribute values based on the selected product.template. -- Supports domain construction to filter attribute values based on context. -- Matching Logic: Includes a is_matching_product(product) method to validate whether a specific product variant satisfies the configured attribute constraints. +This technical module introduces a reusable mixin designed to enable any model to +establish dependencies on specific product attribute values. +By inheriting from this mixin, developers can easily link business rules, +configurations, or records to precise product variants without duplicating complex +filtering logic. **Table of contents** .. contents:: :local: +Usage +===== + +Fields +~~~~~~ + +The mixin exposes the following fields: + +- ``product_tmpl_id``: the product template to filter on (optional). +- ``product_id``: a specific product variant (optional). +- ``attribute_value_ids``: a set of attribute values to filter on + (optional). +- ``available_product_domain``: a computed domain to restrict the + selection of ``product_id`` in views, based on ``product_tmpl_id``. +- ``available_attribute_value_domain``: a computed domain to restrict + the selection of ``attribute_value_ids`` in views, based on + ``product_tmpl_id``. + +Inheriting the Mixin +~~~~~~~~~~~~~~~~~~~~ + +To use this mixin, inherit from ``attribute.value.dependent.mixin`` in +your model alongside your base model: + +.. code:: python + + from odoo import fields, models + + + class MyModel(models.Model): + _name = "my.model" + _inherit = ["my.model", "attribute.value.dependent.mixin"] + +All fields from the mixin (``product_tmpl_id``, ``product_id``, +``attribute_value_ids``, ``available_product_domain``, +``available_attribute_value_domain``) are automatically available on +your model. + +Using the Domain Fields in a Form View +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The computed domain fields must be referenced in the ``domain`` +attribute of the corresponding fields in the view. Odoo 18.0 +automatically loads fields referenced in ``domain`` attributes, so no +additional declaration is needed. + +.. code:: xml + + + my.model.form + my.model + +
+ + + + + +
+
+
+ +Matching Logic +~~~~~~~~~~~~~~ + +The ``is_matching_product(product)`` method validates whether a given +product variant satisfies the configured constraints. All fields are +optional and act as independent filters combined with AND logic: + +- If ``product_id`` is set, it is the most restrictive criterion: the + method returns ``True`` only if the given product is exactly that + variant, regardless of other fields. +- If ``product_tmpl_id`` is set, the given product must belong to that + template. +- If ``attribute_value_ids`` is set, the given product must match the + configured attribute values with the following logic: + + - Values belonging to the **same attribute** are combined with + **OR** (e.g. size S *or* M). + - Values belonging to **different attributes** are combined with + **AND** (e.g. size S or M *and* color Red). + - Since an attribute value can exist across multiple templates, + ``product_tmpl_id`` and ``attribute_value_ids`` should be used + together to avoid unintended matches across templates. + +- If no field is set, the method returns ``True`` for any product (no + constraint). + +Using ``is_matching_product`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``is_matching_product(product)`` method can be called from Python +code to check whether a given ``product.product`` record satisfies the +constraints defined on a mixin record: + +.. code:: python + + for rule in self.env["my.model"].search([]): + if rule.is_matching_product(product): + # apply rule + Bug Tracker =========== @@ -64,6 +172,7 @@ Contributors * `Akretion `_ * Chafique Delli + * Guillaume Masson Maintainers ~~~~~~~~~~~ diff --git a/product_attribute_value_dependent_mixin/__init__.py b/product_attribute_value_dependent_mixin/__init__.py index 83e553ac462..0650744f6bc 100644 --- a/product_attribute_value_dependent_mixin/__init__.py +++ b/product_attribute_value_dependent_mixin/__init__.py @@ -1,3 +1 @@ -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - from . import models diff --git a/product_attribute_value_dependent_mixin/__manifest__.py b/product_attribute_value_dependent_mixin/__manifest__.py index 643dba70339..87d3cf6089c 100644 --- a/product_attribute_value_dependent_mixin/__manifest__.py +++ b/product_attribute_value_dependent_mixin/__manifest__.py @@ -1,5 +1,5 @@ # Copyright 2023 Akretion -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). { "name": "Product Attribute Value Dependent Mixin", @@ -10,7 +10,6 @@ "application": False, "installable": True, "category": "Product", - "version": "16.0.1.0.0", + "version": "16.0.1.2.0", "depends": ["product"], - "data": [], } diff --git a/product_attribute_value_dependent_mixin/migrations/16.0.1.2.0/pre-migrate.py b/product_attribute_value_dependent_mixin/migrations/16.0.1.2.0/pre-migrate.py new file mode 100644 index 00000000000..3a4943ed7cf --- /dev/null +++ b/product_attribute_value_dependent_mixin/migrations/16.0.1.2.0/pre-migrate.py @@ -0,0 +1,14 @@ +# Copyright 2026 Akretion +# @author Guillaume MASSON +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openupgradelib import openupgrade + +_model_renames = [ + ("attribute.value.dependant.mixin", "attribute.value.dependent.mixin"), +] + + +@openupgrade.migrate() +def migrate(env, version): + openupgrade.rename_models(env.cr, _model_renames) diff --git a/product_attribute_value_dependent_mixin/models/__init__.py b/product_attribute_value_dependent_mixin/models/__init__.py index 781a514fa88..ed988b9f81d 100644 --- a/product_attribute_value_dependent_mixin/models/__init__.py +++ b/product_attribute_value_dependent_mixin/models/__init__.py @@ -1,2 +1 @@ -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import product_attribute_value_dependent_mixin diff --git a/product_attribute_value_dependent_mixin/models/product_attribute_value_dependent_mixin.py b/product_attribute_value_dependent_mixin/models/product_attribute_value_dependent_mixin.py index be93bcbd145..8cdf13ff36c 100644 --- a/product_attribute_value_dependent_mixin/models/product_attribute_value_dependent_mixin.py +++ b/product_attribute_value_dependent_mixin/models/product_attribute_value_dependent_mixin.py @@ -1,15 +1,14 @@ -# Copyright 2023-2026 Akretion -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# Copyright 2023 Akretion +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). import itertools -import json from odoo import api, fields, models -class AttributeValueDependantMixin(models.AbstractModel): - _name = "attribute.value.dependant.mixin" - _description = "Attribute Value Dependant Mixin" +class AttributeValueDependentMixin(models.AbstractModel): + _name = "attribute.value.dependent.mixin" + _description = "Attribute Value Dependent Mixin" product_tmpl_id = fields.Many2one( comodel_name="product.template", @@ -17,65 +16,45 @@ class AttributeValueDependantMixin(models.AbstractModel): ) product_id = fields.Many2one( comodel_name="product.product", - string="Product", - domain="[('id', 'in', available_product_ids)]", ) - available_product_ids = fields.Many2many( - comodel_name="product.product", - string="Available Products", - compute="_compute_available_product_ids", + available_product_domain = fields.Binary( + compute="_compute_available_product_domain", ) - attribute_value_ids = fields.Many2many( comodel_name="product.attribute.value", string="Attribute Values", ) - - available_attribute_value_domain = fields.Char( + available_attribute_value_domain = fields.Binary( compute="_compute_available_attribute_value_domain", ) - @api.depends("product_tmpl_id") - def _compute_available_product_ids(self): - for rec in self: - rec.available_product_ids = self.env["product.product"].search( - [("product_tmpl_id", "=", self.product_tmpl_id._origin.id)] - ) - - @api.depends( - "product_tmpl_id", - "product_tmpl_id.attribute_line_ids.value_ids", - "available_attribute_value_domain", - ) - def _compute_available_attribute_value_ids(self): + @api.depends("product_tmpl_id.product_variant_ids") + def _compute_available_product_domain(self): for rec in self: if rec.product_tmpl_id: - rec.available_attribute_value_ids = rec.product_tmpl_id.mapped( - "attribute_line_ids.value_ids" - ).filtered_domain(json.loads(rec.available_attribute_value_domain)) + rec.available_product_domain = [ + ("id", "in", rec.product_tmpl_id.product_variant_ids.ids) + ] else: - rec.available_attribute_value_ids = None + rec.available_product_domain = [] - @api.depends("product_tmpl_id", "product_tmpl_id.attribute_line_ids.value_ids") + @api.depends("product_tmpl_id.attribute_line_ids.value_ids") def _compute_available_attribute_value_domain(self): for rec in self: - domain = [] if rec.product_tmpl_id: - domain = [ + rec.available_attribute_value_domain = [ ("id", "in", rec.product_tmpl_id.attribute_line_ids.value_ids.ids) ] - rec.available_attribute_value_domain = json.dumps(domain) + else: + rec.available_attribute_value_domain = [] def is_matching_product(self, product): self.ensure_one() - if self.product_tmpl_id != product.product_tmpl_id: + if self.product_id: + return self.product_id == product + if self.product_tmpl_id and self.product_tmpl_id != product.product_tmpl_id: return False - elif self.product_id: - if self.product_id == product: - return True - else: - return False - elif self.attribute_value_ids: + if self.attribute_value_ids: ptav = product.product_template_attribute_value_ids attr2vals = { attribute: set(values) @@ -86,6 +65,6 @@ def is_matching_product(self, product): for attribute in attr2vals: if attribute not in ptav.attribute_id: return False - elif not attr2vals[attribute] & set(ptav.product_attribute_value_id): + if not attr2vals[attribute] & set(ptav.product_attribute_value_id): return False return True diff --git a/product_attribute_value_dependent_mixin/readme/CONTRIBUTORS.rst b/product_attribute_value_dependent_mixin/readme/CONTRIBUTORS.rst index 4d6f88298d7..969c27e2213 100644 --- a/product_attribute_value_dependent_mixin/readme/CONTRIBUTORS.rst +++ b/product_attribute_value_dependent_mixin/readme/CONTRIBUTORS.rst @@ -1,3 +1,4 @@ * `Akretion `_ * Chafique Delli + * Guillaume Masson diff --git a/product_attribute_value_dependent_mixin/readme/DESCRIPTION.rst b/product_attribute_value_dependent_mixin/readme/DESCRIPTION.rst index ef501c94724..1eb14fb4beb 100644 --- a/product_attribute_value_dependent_mixin/readme/DESCRIPTION.rst +++ b/product_attribute_value_dependent_mixin/readme/DESCRIPTION.rst @@ -1,6 +1,5 @@ -This technical module introduces a reusable mixin designed to enable any model to establish dependencies on specific product attribute values. -By inheriting from this mixin, developers can easily link business rules, configurations, or records to precise product variants without duplicating complex filtering logic. - -- Automatically computes available products and attribute values based on the selected product.template. -- Supports domain construction to filter attribute values based on context. -- Matching Logic: Includes a is_matching_product(product) method to validate whether a specific product variant satisfies the configured attribute constraints. +This technical module introduces a reusable mixin designed to enable any model to +establish dependencies on specific product attribute values. +By inheriting from this mixin, developers can easily link business rules, +configurations, or records to precise product variants without duplicating complex +filtering logic. diff --git a/product_attribute_value_dependent_mixin/readme/USAGE.rst b/product_attribute_value_dependent_mixin/readme/USAGE.rst new file mode 100644 index 00000000000..dc468688479 --- /dev/null +++ b/product_attribute_value_dependent_mixin/readme/USAGE.rst @@ -0,0 +1,101 @@ +Fields +~~~~~~ + +The mixin exposes the following fields: + +- ``product_tmpl_id``: the product template to filter on (optional). +- ``product_id``: a specific product variant (optional). +- ``attribute_value_ids``: a set of attribute values to filter on + (optional). +- ``available_product_domain``: a computed domain to restrict the + selection of ``product_id`` in views, based on ``product_tmpl_id``. +- ``available_attribute_value_domain``: a computed domain to restrict + the selection of ``attribute_value_ids`` in views, based on + ``product_tmpl_id``. + +Inheriting the Mixin +~~~~~~~~~~~~~~~~~~~~ + +To use this mixin, inherit from ``attribute.value.dependent.mixin`` in +your model alongside your base model: + +.. code:: python + + from odoo import fields, models + + + class MyModel(models.Model): + _name = "my.model" + _inherit = ["my.model", "attribute.value.dependent.mixin"] + +All fields from the mixin (``product_tmpl_id``, ``product_id``, +``attribute_value_ids``, ``available_product_domain``, +``available_attribute_value_domain``) are automatically available on +your model. + +Using the Domain Fields in a Form View +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The computed domain fields must be referenced in the ``domain`` +attribute of the corresponding fields in the view. Odoo 18.0 +automatically loads fields referenced in ``domain`` attributes, so no +additional declaration is needed. + +.. code:: xml + + + my.model.form + my.model + +
+ + + + + +
+
+
+ +Matching Logic +~~~~~~~~~~~~~~ + +The ``is_matching_product(product)`` method validates whether a given +product variant satisfies the configured constraints. All fields are +optional and act as independent filters combined with AND logic: + +- If ``product_id`` is set, it is the most restrictive criterion: the + method returns ``True`` only if the given product is exactly that + variant, regardless of other fields. +- If ``product_tmpl_id`` is set, the given product must belong to that + template. +- If ``attribute_value_ids`` is set, the given product must match the + configured attribute values with the following logic: + + - Values belonging to the **same attribute** are combined with + **OR** (e.g. size S *or* M). + - Values belonging to **different attributes** are combined with + **AND** (e.g. size S or M *and* color Red). + - Since an attribute value can exist across multiple templates, + ``product_tmpl_id`` and ``attribute_value_ids`` should be used + together to avoid unintended matches across templates. + +- If no field is set, the method returns ``True`` for any product (no + constraint). + +Using ``is_matching_product`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``is_matching_product(product)`` method can be called from Python +code to check whether a given ``product.product`` record satisfies the +constraints defined on a mixin record: + +.. code:: python + + for rule in self.env["my.model"].search([]): + if rule.is_matching_product(product): + # apply rule \ No newline at end of file diff --git a/product_attribute_value_dependent_mixin/static/description/index.html b/product_attribute_value_dependent_mixin/static/description/index.html index 6f4738d7e33..b0b05f41da0 100644 --- a/product_attribute_value_dependent_mixin/static/description/index.html +++ b/product_attribute_value_dependent_mixin/static/description/index.html @@ -3,7 +3,7 @@ -Product Attribute Value Dependent Mixin +README.rst -
-

Product Attribute Value Dependent Mixin

+
+ + +Odoo Community Association + +
+

Product Attribute Value Dependent Mixin

-

Beta License: AGPL-3 OCA/product-attribute Translate me on Weblate Try me on Runboat

-

This technical module introduces a reusable mixin designed to enable any model to establish dependencies on specific product attribute values. -By inheriting from this mixin, developers can easily link business rules, configurations, or records to precise product variants without duplicating complex filtering logic.

-
    -
  • Automatically computes available products and attribute values based on the selected product.template.
  • -
  • Supports domain construction to filter attribute values based on context.
  • -
  • Matching Logic: Includes a is_matching_product(product) method to validate whether a specific product variant satisfies the configured attribute constraints.
  • -
+

Beta License: AGPL-3 OCA/product-attribute Translate me on Weblate Try me on Runboat

+

This technical module introduces a reusable mixin designed to enable any model to +establish dependencies on specific product attribute values. +By inheriting from this mixin, developers can easily link business rules, +configurations, or records to precise product variants without duplicating complex +filtering logic.

Table of contents

+
+

Usage

+
+

Fields

+

The mixin exposes the following fields:

+
    +
  • product_tmpl_id: the product template to filter on (optional).
  • +
  • product_id: a specific product variant (optional).
  • +
  • attribute_value_ids: a set of attribute values to filter on +(optional).
  • +
  • available_product_domain: a computed domain to restrict the +selection of product_id in views, based on product_tmpl_id.
  • +
  • available_attribute_value_domain: a computed domain to restrict +the selection of attribute_value_ids in views, based on +product_tmpl_id.
  • +
+
+
+

Inheriting the Mixin

+

To use this mixin, inherit from attribute.value.dependent.mixin in +your model alongside your base model:

+
+from odoo import fields, models
+
+
+class MyModel(models.Model):
+    _name = "my.model"
+    _inherit = ["my.model", "attribute.value.dependent.mixin"]
+
+

All fields from the mixin (product_tmpl_id, product_id, +attribute_value_ids, available_product_domain, +available_attribute_value_domain) are automatically available on +your model.

+
+
+

Using the Domain Fields in a Form View

+

The computed domain fields must be referenced in the domain +attribute of the corresponding fields in the view. Odoo 18.0 +automatically loads fields referenced in domain attributes, so no +additional declaration is needed.

+
+<record id="view_my_model_form" model="ir.ui.view">
+    <field name="name">my.model.form</field>
+    <field name="model">my.model</field>
+    <field name="arch" type="xml">
+        <form>
+            <group>
+                <field name="product_tmpl_id"/>
+                <field name="product_id"
+                       domain="available_product_domain"
+                       context="{'default_product_tmpl_id': product_tmpl_id}"/>
+                <field name="attribute_value_ids"
+                       domain="available_attribute_value_domain"
+                       widget="many2many_tags"/>
+            </group>
+        </form>
+    </field>
+</record>
+
+
+
+

Matching Logic

+

The is_matching_product(product) method validates whether a given +product variant satisfies the configured constraints. All fields are +optional and act as independent filters combined with AND logic:

+
    +
  • If product_id is set, it is the most restrictive criterion: the +method returns True only if the given product is exactly that +variant, regardless of other fields.
  • +
  • If product_tmpl_id is set, the given product must belong to that +template.
  • +
  • If attribute_value_ids is set, the given product must match the +configured attribute values with the following logic:
      +
    • Values belonging to the same attribute are combined with +OR (e.g. size S or M).
    • +
    • Values belonging to different attributes are combined with +AND (e.g. size S or M and color Red).
    • +
    • Since an attribute value can exist across multiple templates, +product_tmpl_id and attribute_value_ids should be used +together to avoid unintended matches across templates.
    • +
    +
  • +
  • If no field is set, the method returns True for any product (no +constraint).
  • +
+
+
+

Using is_matching_product

+

The is_matching_product(product) method can be called from Python +code to check whether a given product.product record satisfies the +constraints defined on a mixin record:

+
+for rule in self.env["my.model"].search([]):
+    if rule.is_matching_product(product):
+        # apply rule
+
+
+
-

Bug Tracker

+

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 @@ -398,24 +507,25 @@

Bug Tracker

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

+
diff --git a/product_attribute_value_dependent_mixin/tests/models.py b/product_attribute_value_dependent_mixin/tests/models.py index f70e7621a61..9e4109b0dee 100644 --- a/product_attribute_value_dependent_mixin/tests/models.py +++ b/product_attribute_value_dependent_mixin/tests/models.py @@ -1,12 +1,12 @@ # Copyright 2023 Akretion -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import fields, models class ProductSupplierinfoFake(models.Model): _name = "product.supplierinfo.fake" - _inherit = ["product.supplierinfo", "attribute.value.dependant.mixin"] + _inherit = ["product.supplierinfo", "attribute.value.dependent.mixin"] _description = "Product supplierinfo fake model for tests" name = fields.Char() diff --git a/product_attribute_value_dependent_mixin/tests/test_product_attribute_value_dependent_mixin.py b/product_attribute_value_dependent_mixin/tests/test_product_attribute_value_dependent_mixin.py index 315948300ea..5cde6a13070 100644 --- a/product_attribute_value_dependent_mixin/tests/test_product_attribute_value_dependent_mixin.py +++ b/product_attribute_value_dependent_mixin/tests/test_product_attribute_value_dependent_mixin.py @@ -1,12 +1,12 @@ # Copyright 2023 Akretion -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo_test_helper import FakeModelLoader from odoo.tests import TransactionCase -class TestProductAttributeValueDependentMixin(TransactionCase, FakeModelLoader): +class TestProductAttributeValueDependentMixinCommon(TransactionCase, FakeModelLoader): @classmethod def setUpClass(cls): super().setUpClass() @@ -16,25 +16,194 @@ def setUpClass(cls): cls.loader.update_registry((ProductSupplierinfoFake,)) - # Fake model which inherit from - cls.product_supplierinfo_fake = cls.env["product.supplierinfo.fake"].create( + # Attributs : Taille (S, M, L) et Couleur (Rouge, Bleu) + cls.attr_size = cls.env["product.attribute"].create({"name": "Size"}) + cls.val_s = cls.env["product.attribute.value"].create( + {"name": "S", "attribute_id": cls.attr_size.id} + ) + cls.val_m = cls.env["product.attribute.value"].create( + {"name": "M", "attribute_id": cls.attr_size.id} + ) + cls.val_l = cls.env["product.attribute.value"].create( + {"name": "L", "attribute_id": cls.attr_size.id} + ) + cls.attr_color = cls.env["product.attribute"].create({"name": "Color"}) + cls.val_red = cls.env["product.attribute.value"].create( + {"name": "Red", "attribute_id": cls.attr_color.id} + ) + cls.val_blue = cls.env["product.attribute.value"].create( + {"name": "Blue", "attribute_id": cls.attr_color.id} + ) + + cls.tmpl_a = cls.env["product.template"].create( { - "partner_id": cls.env.ref("base.res_partner_1").id, - "product_tmpl_id": cls.env.ref( - "product.product_product_4_product_template" - ).id, - "price": 100.00, - "currency_id": cls.env.ref("base.USD").id, - "min_qty": 1.0, - "delay": 1.0, + "name": "Product A", + "attribute_line_ids": [ + ( + 0, + 0, + { + "attribute_id": cls.attr_size.id, + "value_ids": [(6, 0, [cls.val_s.id, cls.val_m.id])], + }, + ), + ( + 0, + 0, + { + "attribute_id": cls.attr_color.id, + "value_ids": [(6, 0, [cls.val_red.id, cls.val_blue.id])], + }, + ), + ], } ) + cls.variant_a_s_red = cls.tmpl_a.product_variant_ids.filtered( + lambda p: cls.val_s + in p.product_template_attribute_value_ids.product_attribute_value_id + and cls.val_red + in p.product_template_attribute_value_ids.product_attribute_value_id + ) + cls.variant_a_m_red = cls.tmpl_a.product_variant_ids.filtered( + lambda p: cls.val_m + in p.product_template_attribute_value_ids.product_attribute_value_id + and cls.val_red + in p.product_template_attribute_value_ids.product_attribute_value_id + ) + cls.variant_a_s_blue = cls.tmpl_a.product_variant_ids.filtered( + lambda p: cls.val_s + in p.product_template_attribute_value_ids.product_attribute_value_id + and cls.val_blue + in p.product_template_attribute_value_ids.product_attribute_value_id + ) + + cls.tmpl_b = cls.env["product.template"].create( + { + "name": "Product B", + "attribute_line_ids": [ + ( + 0, + 0, + { + "attribute_id": cls.attr_size.id, + "value_ids": [(6, 0, [cls.val_s.id])], + }, + ), + ], + } + ) + cls.variant_b_s = cls.tmpl_b.product_variant_ids + + cls.Fake = cls.env["product.supplierinfo.fake"] + cls.partner = cls.env.ref("base.res_partner_1") @classmethod def tearDownClass(cls): cls.loader.restore_registry() - super(TestProductAttributeValueDependentMixin, cls).tearDownClass() + super().tearDownClass() + + def _make(self, vals): + base = { + "partner_id": self.partner.id, + "price": 1.0, + "currency_id": self.env.ref("base.USD").id, + "min_qty": 1.0, + "delay": 1, + } + base.update(vals) + return self.Fake.create(base) + + +class TestProductAttributeValueDependentMixin( + TestProductAttributeValueDependentMixinCommon +): + # --- available_product_domain --- + + def test_available_product_domain_with_template(self): + rec = self._make({"product_tmpl_id": self.tmpl_a.id}) + self.assertEqual( + rec.available_product_domain, + [("id", "in", self.tmpl_a.product_variant_ids.ids)], + ) + + def test_available_product_domain_without_template(self): + rec = self._make({}) + self.assertEqual(rec.available_product_domain, []) + + # --- available_attribute_value_domain --- - def test_product_attribute_value_dependent_mixin(self): - fake_model = self.product_supplierinfo_fake - self.assertTrue(fake_model.available_attribute_value_domain) + def test_available_attribute_value_domain_with_template(self): + rec = self._make({"product_tmpl_id": self.tmpl_a.id}) + expected_ids = self.tmpl_a.attribute_line_ids.value_ids.ids + self.assertEqual( + rec.available_attribute_value_domain, + [("id", "in", expected_ids)], + ) + + def test_available_attribute_value_domain_without_template(self): + rec = self._make({}) + self.assertEqual(rec.available_attribute_value_domain, []) + + # --- is_matching_product --- + + def test_no_criteria_matches_any_product(self): + rec = self._make({}) + self.assertTrue(rec.is_matching_product(self.variant_a_s_red)) + self.assertTrue(rec.is_matching_product(self.variant_b_s)) + + def test_product_id_matches_exact_variant(self): + rec = self._make({"product_id": self.variant_a_s_red.id}) + self.assertTrue(rec.is_matching_product(self.variant_a_s_red)) + + def test_product_id_rejects_other_variant(self): + rec = self._make({"product_id": self.variant_a_s_red.id}) + self.assertFalse(rec.is_matching_product(self.variant_a_m_red)) + + def test_product_id_takes_precedence_over_attribute_values(self): + rec = self._make( + { + "product_id": self.variant_a_s_red.id, + "attribute_value_ids": [(6, 0, [self.val_m.id])], + } + ) + self.assertTrue(rec.is_matching_product(self.variant_a_s_red)) + self.assertFalse(rec.is_matching_product(self.variant_a_m_red)) + + def test_tmpl_matches_any_variant_of_template(self): + rec = self._make({"product_tmpl_id": self.tmpl_a.id}) + self.assertTrue(rec.is_matching_product(self.variant_a_s_red)) + self.assertTrue(rec.is_matching_product(self.variant_a_m_red)) + + def test_tmpl_rejects_variant_of_other_template(self): + rec = self._make({"product_tmpl_id": self.tmpl_a.id}) + self.assertFalse(rec.is_matching_product(self.variant_b_s)) + + def test_attribute_values_or_within_same_attribute(self): + rec = self._make( + {"attribute_value_ids": [(6, 0, [self.val_s.id, self.val_m.id])]} + ) + self.assertTrue(rec.is_matching_product(self.variant_a_s_red)) + self.assertTrue(rec.is_matching_product(self.variant_a_m_red)) + + def test_attribute_values_or_within_same_attribute_rejects_l(self): + rec = self._make({"attribute_value_ids": [(6, 0, [self.val_l.id])]}) + self.assertFalse(rec.is_matching_product(self.variant_a_s_red)) + + def test_attribute_values_and_across_attributes(self): + rec = self._make( + {"attribute_value_ids": [(6, 0, [self.val_s.id, self.val_red.id])]} + ) + self.assertTrue(rec.is_matching_product(self.variant_a_s_red)) + self.assertFalse(rec.is_matching_product(self.variant_a_m_red)) + self.assertFalse(rec.is_matching_product(self.variant_a_s_blue)) + + def test_attribute_values_with_template_rejects_other_template(self): + rec = self._make( + { + "product_tmpl_id": self.tmpl_a.id, + "attribute_value_ids": [(6, 0, [self.val_s.id])], + } + ) + self.assertTrue(rec.is_matching_product(self.variant_a_s_red)) + self.assertTrue(rec.is_matching_product(self.variant_a_s_blue)) + self.assertFalse(rec.is_matching_product(self.variant_b_s)) From 7acd8d356e8359521751cfb73bdc53b152e25f0e Mon Sep 17 00:00:00 2001 From: oca-ci Date: Thu, 30 Jul 2026 06:57:50 +0000 Subject: [PATCH 3/4] [UPD] Update product_attribute_value_dependent_mixin.pot --- ...roduct_attribute_value_dependent_mixin.pot | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 product_attribute_value_dependent_mixin/i18n/product_attribute_value_dependent_mixin.pot diff --git a/product_attribute_value_dependent_mixin/i18n/product_attribute_value_dependent_mixin.pot b/product_attribute_value_dependent_mixin/i18n/product_attribute_value_dependent_mixin.pot new file mode 100644 index 00000000000..cd5f1eb878b --- /dev/null +++ b/product_attribute_value_dependent_mixin/i18n/product_attribute_value_dependent_mixin.pot @@ -0,0 +1,44 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_attribute_value_dependent_mixin +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.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: product_attribute_value_dependent_mixin +#: model:ir.model,name:product_attribute_value_dependent_mixin.model_attribute_value_dependent_mixin +msgid "Attribute Value Dependent Mixin" +msgstr "" + +#. module: product_attribute_value_dependent_mixin +#: model:ir.model.fields,field_description:product_attribute_value_dependent_mixin.field_attribute_value_dependent_mixin__attribute_value_ids +msgid "Attribute Values" +msgstr "" + +#. module: product_attribute_value_dependent_mixin +#: model:ir.model.fields,field_description:product_attribute_value_dependent_mixin.field_attribute_value_dependent_mixin__available_attribute_value_domain +msgid "Available Attribute Value Domain" +msgstr "" + +#. module: product_attribute_value_dependent_mixin +#: model:ir.model.fields,field_description:product_attribute_value_dependent_mixin.field_attribute_value_dependent_mixin__available_product_domain +msgid "Available Product Domain" +msgstr "" + +#. module: product_attribute_value_dependent_mixin +#: model:ir.model.fields,field_description:product_attribute_value_dependent_mixin.field_attribute_value_dependent_mixin__product_id +msgid "Product" +msgstr "" + +#. module: product_attribute_value_dependent_mixin +#: model:ir.model.fields,field_description:product_attribute_value_dependent_mixin.field_attribute_value_dependent_mixin__product_tmpl_id +msgid "Product Template" +msgstr "" From e1d17ca2dab66fe8dfba2c12741750f276cc23b6 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 30 Jul 2026 07:04:12 +0000 Subject: [PATCH 4/4] [BOT] post-merge updates --- README.md | 1 + product_attribute_value_dependent_mixin/README.rst | 2 +- .../static/description/index.html | 2 +- setup/_metapackage/VERSION.txt | 2 +- setup/_metapackage/setup.py | 1 + 5 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 75a1839d02c..6c612eb3d37 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ addon | version | maintainers | summary [product_attribute_archive](product_attribute_archive/) | 16.0.1.0.0 | | Add an active field on product attributes [product_attribute_company_favorite](product_attribute_company_favorite/) | 16.0.1.0.0 | victor-champonnois | Possibility to set favorite product attributes per company [product_attribute_model_link](product_attribute_model_link/) | 16.0.1.0.1 | | Use any model records as product attribute values +[product_attribute_value_dependent_mixin](product_attribute_value_dependent_mixin/) | 16.0.1.2.0 | | Mixin to make product attribute values fields on models [product_attribute_value_menu](product_attribute_value_menu/) | 16.0.1.0.1 | | Product attributes values tree and form. Import attribute values. [product_catalog](product_catalog/) | 16.0.1.0.0 | | Backport of Odoos v17 product catalog [product_catalog_sale](product_catalog_sale/) | 16.0.1.0.0 | | Backport of Odoos v17 product catalog for sale orders diff --git a/product_attribute_value_dependent_mixin/README.rst b/product_attribute_value_dependent_mixin/README.rst index 1188f5ce9cb..0dbf1a8fbd5 100644 --- a/product_attribute_value_dependent_mixin/README.rst +++ b/product_attribute_value_dependent_mixin/README.rst @@ -11,7 +11,7 @@ Product Attribute Value Dependent Mixin !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:ba5d1efad0e1d29166513c1a23b430a0477e8fc7f44fe2afe6dfea4f6718ea82 + !! source digest: sha256:12dfa656bf593464de4ef8bdaf6570d35c2f32de73e6b802a7294629ca13bd4b !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/product_attribute_value_dependent_mixin/static/description/index.html b/product_attribute_value_dependent_mixin/static/description/index.html index b0b05f41da0..25a08a59a0e 100644 --- a/product_attribute_value_dependent_mixin/static/description/index.html +++ b/product_attribute_value_dependent_mixin/static/description/index.html @@ -372,7 +372,7 @@

Product Attribute Value Dependent Mixin

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:ba5d1efad0e1d29166513c1a23b430a0477e8fc7f44fe2afe6dfea4f6718ea82 +!! source digest: sha256:12dfa656bf593464de4ef8bdaf6570d35c2f32de73e6b802a7294629ca13bd4b !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: AGPL-3 OCA/product-attribute Translate me on Weblate Try me on Runboat

This technical module introduces a reusable mixin designed to enable any model to diff --git a/setup/_metapackage/VERSION.txt b/setup/_metapackage/VERSION.txt index 728dfd0c782..3509c646644 100644 --- a/setup/_metapackage/VERSION.txt +++ b/setup/_metapackage/VERSION.txt @@ -1 +1 @@ -16.0.20260720.0 \ No newline at end of file +16.0.20260730.0 \ No newline at end of file diff --git a/setup/_metapackage/setup.py b/setup/_metapackage/setup.py index 1b0bc5815b8..3342df3c2b9 100644 --- a/setup/_metapackage/setup.py +++ b/setup/_metapackage/setup.py @@ -18,6 +18,7 @@ 'odoo-addon-product_attribute_archive>=16.0dev,<16.1dev', 'odoo-addon-product_attribute_company_favorite>=16.0dev,<16.1dev', 'odoo-addon-product_attribute_model_link>=16.0dev,<16.1dev', + 'odoo-addon-product_attribute_value_dependent_mixin>=16.0dev,<16.1dev', 'odoo-addon-product_attribute_value_menu>=16.0dev,<16.1dev', 'odoo-addon-product_catalog>=16.0dev,<16.1dev', 'odoo-addon-product_catalog_sale>=16.0dev,<16.1dev',