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 new file mode 100644 index 00000000000..0dbf1a8fbd5 --- /dev/null +++ b/product_attribute_value_dependent_mixin/README.rst @@ -0,0 +1,192 @@ +.. 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 +======================================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:12dfa656bf593464de4ef8bdaf6570d35c2f32de73e6b802a7294629ca13bd4b + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%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. + +**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 +=========== + +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 + * Guillaume Masson + +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..0650744f6bc --- /dev/null +++ b/product_attribute_value_dependent_mixin/__init__.py @@ -0,0 +1 @@ +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..87d3cf6089c --- /dev/null +++ b/product_attribute_value_dependent_mixin/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2023 Akretion +# License AGPL-3.0 or later (https://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.2.0", + "depends": ["product"], +} 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 "" 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 new file mode 100644 index 00000000000..ed988b9f81d --- /dev/null +++ b/product_attribute_value_dependent_mixin/models/__init__.py @@ -0,0 +1 @@ +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..8cdf13ff36c --- /dev/null +++ b/product_attribute_value_dependent_mixin/models/product_attribute_value_dependent_mixin.py @@ -0,0 +1,70 @@ +# Copyright 2023 Akretion +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import itertools + +from odoo import api, fields, models + + +class AttributeValueDependentMixin(models.AbstractModel): + _name = "attribute.value.dependent.mixin" + _description = "Attribute Value Dependent Mixin" + + product_tmpl_id = fields.Many2one( + comodel_name="product.template", + string="Product Template", + ) + product_id = fields.Many2one( + comodel_name="product.product", + ) + 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.Binary( + compute="_compute_available_attribute_value_domain", + ) + + @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_product_domain = [ + ("id", "in", rec.product_tmpl_id.product_variant_ids.ids) + ] + else: + rec.available_product_domain = [] + + @api.depends("product_tmpl_id.attribute_line_ids.value_ids") + def _compute_available_attribute_value_domain(self): + for rec in self: + if rec.product_tmpl_id: + rec.available_attribute_value_domain = [ + ("id", "in", rec.product_tmpl_id.attribute_line_ids.value_ids.ids) + ] + else: + rec.available_attribute_value_domain = [] + + def is_matching_product(self, product): + self.ensure_one() + 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 + if 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 + 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 new file mode 100644 index 00000000000..969c27e2213 --- /dev/null +++ b/product_attribute_value_dependent_mixin/readme/CONTRIBUTORS.rst @@ -0,0 +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 new file mode 100644 index 00000000000..1eb14fb4beb --- /dev/null +++ b/product_attribute_value_dependent_mixin/readme/DESCRIPTION.rst @@ -0,0 +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. 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/icon.png b/product_attribute_value_dependent_mixin/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/product_attribute_value_dependent_mixin/static/description/icon.png differ 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..25a08a59a0e --- /dev/null +++ b/product_attribute_value_dependent_mixin/static/description/index.html @@ -0,0 +1,543 @@ + + + + + +README.rst + + + +
+ + + +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.

+

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

+

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..9e4109b0dee --- /dev/null +++ b/product_attribute_value_dependent_mixin/tests/models.py @@ -0,0 +1,12 @@ +# Copyright 2023 Akretion +# 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.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 new file mode 100644 index 00000000000..5cde6a13070 --- /dev/null +++ b/product_attribute_value_dependent_mixin/tests/test_product_attribute_value_dependent_mixin.py @@ -0,0 +1,209 @@ +# Copyright 2023 Akretion +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo_test_helper import FakeModelLoader + +from odoo.tests import TransactionCase + + +class TestProductAttributeValueDependentMixinCommon(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,)) + + # 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( + { + "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().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_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)) 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', 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, +)