diff --git a/product_attribute_value_dependent_mixin/README.rst b/product_attribute_value_dependent_mixin/README.rst new file mode 100644 index 00000000000..1c7d08740ba --- /dev/null +++ b/product_attribute_value_dependent_mixin/README.rst @@ -0,0 +1,188 @@ +======================================= +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/18.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-18-0/product-attribute-18-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=18.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..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..59270435ef6 --- /dev/null +++ b/product_attribute_value_dependent_mixin/__manifest__.py @@ -0,0 +1,15 @@ +# 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": "18.0.1.0.0", + "depends": ["product"], +} 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..76847dca721 --- /dev/null +++ b/product_attribute_value_dependent_mixin/models/product_attribute_value_dependent_mixin.py @@ -0,0 +1,70 @@ +# Copyright 2023-2026 Akretion +# License AGPL-3.0 or later (http://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/pyproject.toml b/product_attribute_value_dependent_mixin/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/product_attribute_value_dependent_mixin/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/product_attribute_value_dependent_mixin/readme/CONTRIBUTORS.md b/product_attribute_value_dependent_mixin/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..714484819c7 --- /dev/null +++ b/product_attribute_value_dependent_mixin/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- [Akretion](https://www.akretion.com) + - Chafique Delli \ + - Guillaume Masson \ diff --git a/product_attribute_value_dependent_mixin/readme/DESCRIPTION.md b/product_attribute_value_dependent_mixin/readme/DESCRIPTION.md new file mode 100644 index 00000000000..3b0343c261c --- /dev/null +++ b/product_attribute_value_dependent_mixin/readme/DESCRIPTION.md @@ -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.md b/product_attribute_value_dependent_mixin/readme/USAGE.md new file mode 100644 index 00000000000..1b19b42cba7 --- /dev/null +++ b/product_attribute_value_dependent_mixin/readme/USAGE.md @@ -0,0 +1,91 @@ +## 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: + +```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. + +```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: + +```python +for rule in self.env["my.model"].search([]): + if rule.is_matching_product(product): + # apply rule +``` 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..1dcc49c24f3 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..c55f3e530e0 --- /dev/null +++ b/product_attribute_value_dependent_mixin/static/description/index.html @@ -0,0 +1,537 @@ + + + + + +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.

+

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..8e79495c892 --- /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.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..cd78b79b23a --- /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 (http://www.gnu.org/licenses/agpl). + +from odoo_test_helper import FakeModelLoader + +from odoo.tests import TransactionCase + + +class TestProductAttributeValueDependentMixinCommon(TransactionCase): + def setUp(self): + # See OCA/server-ux#1242 to understand why not using setUpClass here + super().setUp() + self.env = self.env(context=dict(self.env.context, tracking_disable=True)) + self.loader = FakeModelLoader(self.env, self.__module__) + self.loader.backup_registry() + from .models import ProductSupplierinfoFake + + self.loader.update_registry((ProductSupplierinfoFake,)) + + # Attributs : Taille (S, M, L) et Couleur (Rouge, Bleu) + self.attr_size = self.env["product.attribute"].create({"name": "Size"}) + self.val_s = self.env["product.attribute.value"].create( + {"name": "S", "attribute_id": self.attr_size.id} + ) + self.val_m = self.env["product.attribute.value"].create( + {"name": "M", "attribute_id": self.attr_size.id} + ) + self.val_l = self.env["product.attribute.value"].create( + {"name": "L", "attribute_id": self.attr_size.id} + ) + self.attr_color = self.env["product.attribute"].create({"name": "Color"}) + self.val_red = self.env["product.attribute.value"].create( + {"name": "Red", "attribute_id": self.attr_color.id} + ) + self.val_blue = self.env["product.attribute.value"].create( + {"name": "Blue", "attribute_id": self.attr_color.id} + ) + + self.tmpl_a = self.env["product.template"].create( + { + "name": "Product A", + "attribute_line_ids": [ + ( + 0, + 0, + { + "attribute_id": self.attr_size.id, + "value_ids": [(6, 0, [self.val_s.id, self.val_m.id])], + }, + ), + ( + 0, + 0, + { + "attribute_id": self.attr_color.id, + "value_ids": [(6, 0, [self.val_red.id, self.val_blue.id])], + }, + ), + ], + } + ) + self.variant_a_s_red = self.tmpl_a.product_variant_ids.filtered( + lambda p: self.val_s + in p.product_template_attribute_value_ids.product_attribute_value_id + and self.val_red + in p.product_template_attribute_value_ids.product_attribute_value_id + ) + self.variant_a_m_red = self.tmpl_a.product_variant_ids.filtered( + lambda p: self.val_m + in p.product_template_attribute_value_ids.product_attribute_value_id + and self.val_red + in p.product_template_attribute_value_ids.product_attribute_value_id + ) + self.variant_a_s_blue = self.tmpl_a.product_variant_ids.filtered( + lambda p: self.val_s + in p.product_template_attribute_value_ids.product_attribute_value_id + and self.val_blue + in p.product_template_attribute_value_ids.product_attribute_value_id + ) + + self.tmpl_b = self.env["product.template"].create( + { + "name": "Product B", + "attribute_line_ids": [ + ( + 0, + 0, + { + "attribute_id": self.attr_size.id, + "value_ids": [(6, 0, [self.val_s.id])], + }, + ), + ], + } + ) + self.variant_b_s = self.tmpl_b.product_variant_ids + + self.Fake = self.env["product.supplierinfo.fake"] + self.partner = self.env.ref("base.res_partner_1") + + def tearDown(self): + self.loader.restore_registry() + super().tearDown() + + 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))