From ddcb6f300cc2136aaeae836b601b651d2aa49d2c Mon Sep 17 00:00:00 2001 From: Kev-Roche Date: Fri, 27 Mar 2026 09:45:43 +0100 Subject: [PATCH 1/3] [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 ++ 12 files changed, 689 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 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 z?n?eq1XP>p-IM$Z^C;2L3itnbJZAip*Zo0aw2bs8@(s^~*8T9go!%dHcAz2lM;`yp zD=7&xjFV$S&5uDaiScyD?B-i1ze`+CoRtz`Wn+Zl&#s4&}MO{@N!ufrzjG$B79)Y2d3tBk&)TxUTw@QS0TEL_?njX|@vq?Uz(nBFK5Pq7*xj#u*R&i|?7+6# z+|r_n#SW&LXhtheZdah{ZVoqwyT{D>MC3nkFF#N)xLi{p7J1jXlmVeb;cP5?e(=f# zuT7fvjSbjS781v?7{)-X3*?>tq?)Yd)~|1{BDS(pqC zC}~H#WXlkUW*H5CDOo<)#x7%RY)A;ShGhI5s*#cRDA8YgqG(HeKDx+#(ZQ?386dv! zlXCO)w91~Vw4AmOcATuV653fa9R$fyK8ul%rG z-wfS zihugoZyr38Im?Zuh6@RcF~t1anQu7>#lPpb#}4cOA!EM11`%f*07RqOVkmX{p~KJ9 z^zP;K#|)$`^Rb{rnHGH{~>1(fawV0*Z#)}M`m8-?ZJV<+e}s9wE# z)l&az?w^5{)`S(%MRzxdNqrs1n*-=jS^_jqE*5XDrA0+VE`5^*p3CuM<&dZEeCjoz zR;uu_H9ZPZV|fQq`Cyw4nscrVwi!fE6ciMmX$!_hN7uF;jjKG)d2@aC4ropY)8etW=xJvni)8eHi`H$%#zn^WJ5NLc-rqk|u&&4Z6fD_m&JfSI1Bvb?b<*n&sfl0^t z=HnmRl`XrFvMKB%9}>PaA`m-fK6a0(8=qPkWS5bb4=v?XcWi&hRY?O5HdulRi4?fN zlsJ*N-0Qw+Yic@s0(2uy%F@ib;GjXt01Fmx5XbRo6+n|pP(&nodMoap^z{~q ziEeaUT@Mxe3vJSfI6?uLND(CNr=#^W<1b}jzW58bIfyWTDle$mmS(|x-0|2UlX+9k zQ^EX7Nw}?EzVoBfT(-LT|=9N@^hcn-_p&sqG z&*oVs2JSU+N4ZD`FhCAWaS;>|wH2G*Id|?pa#@>tyxX`+4HyIArWDvVrX)2WAOQff z0qyHu&-S@i^MS-+j--!pr4fPBj~_8({~e1bfcl0wI1kaoN>mJL6KUPQm5N7lB(ui1 zE-o%kq)&djzWJ}ob<-GfDlkB;F31j-VHKvQUGQ3sp`CwyGJk_i!y^sD0fqC@$9|jO zOqN!r!8-p==F@ZVP=U$qSpY(gQ0)59P1&t@y?5rvg<}E+GB}26NYPp4f2YFQrQtot5mn3wu_qprZ=>Ig-$ zbW26Ws~IgY>}^5w`vTB(G`PTZaDiGBo5o(tp)qli|NeV( z@H_=R8V39rt5J5YB2Ky?4eJJ#b`_iBe2ot~6%7mLt5t8Vwi^Jy7|jWXqa3amOIoRb zOr}WVFP--DsS`1WpN%~)t3R!arKF^Q$e12KEqU36AWwnCBICpH4XCsfnyrHr>$I$4 z!DpKX$OKLWarN7nv@!uIA+~RNO)l$$w}p(;b>mx8pwYvu;dD_unryX_NhT8*Tj>BTrTTL&!?O+%Rv;b?B??gSzdp?6Uug9{ zd@V08Z$BdI?fpoCS$)t4mg4rT8Q_I}h`0d-vYZ^|dOB*Q^S|xqTV*vIg?@fVFSmMpaw0qtTRbx} z({Pg?#{2`sc9)M5N$*N|4;^t$+QP?#mov zGVC@I*lBVrOU-%2y!7%)fAKjpEFsgQc4{amtiHb95KQEwvf<(3T<9-Zm$xIew#P22 zc2Ix|App^>v6(3L_MCU0d3W##AB0M~3D00EWoKZqsJYT(#@w$Y_H7G22M~ApVFTRHMI_3be)Lkn#0F*V8Pq zc}`Cjy$bE;FJ6H7p=0y#R>`}-m4(0F>%@P|?7fx{=R^uFdISRnZ2W_xQhD{YuR3t< z{6yxu=4~JkeA;|(J6_nv#>Nvs&FuLA&PW^he@t(UwFFE8)|a!R{`E`K`i^ZnyE4$k z;(749Ix|oi$c3QbEJ3b~D_kQsPz~fIUKym($a_7dJ?o+40*OLl^{=&oq$<#Q(yyrp z{J-FAniyAw9tPbe&IhQ|a`DqFTVQGQ&Gq3!C2==4x{6EJwiPZ8zub-iXoUtkJiG{} zPaR&}_fn8_z~(=;5lD-aPWD3z8PZS@AaUiomF!G8I}Mf>e~0g#BelA-5#`cj;O5>N 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) From 86abfcbeb408dd071af0449aa827df0b7ba8b545 Mon Sep 17 00:00:00 2001 From: Guillaume MASSON Date: Fri, 27 Mar 2026 11:59:25 -1000 Subject: [PATCH 2/3] [IMP] product_attribute_value_dependent_mixin: pre-commit auto fixes --- .../README.rst | 37 +++++++++++-------- .../pyproject.toml | 3 ++ .../readme/CONTRIBUTORS.md | 2 + .../readme/CONTRIBUTORS.rst | 3 -- .../readme/DESCRIPTION.md | 13 +++++++ .../readme/DESCRIPTION.rst | 6 --- .../static/description/index.html | 23 ++++++++---- ...product_attribute_value_dependent_mixin.py | 2 +- 8 files changed, 56 insertions(+), 33 deletions(-) create mode 100644 product_attribute_value_dependent_mixin/pyproject.toml create mode 100644 product_attribute_value_dependent_mixin/readme/CONTRIBUTORS.md delete mode 100644 product_attribute_value_dependent_mixin/readme/CONTRIBUTORS.rst create mode 100644 product_attribute_value_dependent_mixin/readme/DESCRIPTION.md delete mode 100644 product_attribute_value_dependent_mixin/readme/DESCRIPTION.rst diff --git a/product_attribute_value_dependent_mixin/README.rst b/product_attribute_value_dependent_mixin/README.rst index 65815d6ff4d..ef1d48ed5f4 100644 --- a/product_attribute_value_dependent_mixin/README.rst +++ b/product_attribute_value_dependent_mixin/README.rst @@ -17,23 +17,30 @@ Product Attribute Value Dependent Mixin :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 + :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-16-0/product-attribute-16-0-product_attribute_value_dependent_mixin + :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=16.0 + :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. +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. +- 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** @@ -46,7 +53,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -54,19 +61,19 @@ Credits ======= Authors -~~~~~~~ +------- * Akretion Contributors -~~~~~~~~~~~~ +------------ -* `Akretion `_ +- `Akretion `__ - * Chafique Delli + - Chafique Delli Maintainers -~~~~~~~~~~~ +----------- This module is maintained by the OCA. @@ -78,6 +85,6 @@ 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. +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/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..3efa1e694f2 --- /dev/null +++ b/product_attribute_value_dependent_mixin/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Akretion](https://www.akretion.com) + - Chafique Delli \ diff --git a/product_attribute_value_dependent_mixin/readme/CONTRIBUTORS.rst b/product_attribute_value_dependent_mixin/readme/CONTRIBUTORS.rst deleted file mode 100644 index 4d6f88298d7..00000000000 --- a/product_attribute_value_dependent_mixin/readme/CONTRIBUTORS.rst +++ /dev/null @@ -1,3 +0,0 @@ -* `Akretion `_ - - * Chafique Delli 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..a817aa0e64d --- /dev/null +++ b/product_attribute_value_dependent_mixin/readme/DESCRIPTION.md @@ -0,0 +1,13 @@ +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/readme/DESCRIPTION.rst b/product_attribute_value_dependent_mixin/readme/DESCRIPTION.rst deleted file mode 100644 index ef501c94724..00000000000 --- a/product_attribute_value_dependent_mixin/readme/DESCRIPTION.rst +++ /dev/null @@ -1,6 +0,0 @@ -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/index.html b/product_attribute_value_dependent_mixin/static/description/index.html index 6f4738d7e33..c0f47e604e9 100644 --- a/product_attribute_value_dependent_mixin/static/description/index.html +++ b/product_attribute_value_dependent_mixin/static/description/index.html @@ -369,13 +369,20 @@

Product Attribute Value Dependent Mixin

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:ba5d1efad0e1d29166513c1a23b430a0477e8fc7f44fe2afe6dfea4f6718ea82 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

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.

+

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.
  • +
  • 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

@@ -394,7 +401,7 @@

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.

+feedback.

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

@@ -423,7 +430,7 @@

Maintainers

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.

+

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/test_product_attribute_value_dependent_mixin.py b/product_attribute_value_dependent_mixin/tests/test_product_attribute_value_dependent_mixin.py index 315948300ea..4e04b76c413 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 @@ -33,7 +33,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): cls.loader.restore_registry() - super(TestProductAttributeValueDependentMixin, cls).tearDownClass() + super().tearDownClass() def test_product_attribute_value_dependent_mixin(self): fake_model = self.product_supplierinfo_fake From b1b0a49d22d5d54ac1fa5d8a6e3b4e055d097e72 Mon Sep 17 00:00:00 2001 From: Guillaume MASSON Date: Fri, 27 Mar 2026 11:59:25 -1000 Subject: [PATCH 3/3] [MIG] product_attribute_value_dependent_mixin: Migration to 18.0 --- .../README.rst | 114 ++++++++- .../__manifest__.py | 3 +- ...product_attribute_value_dependent_mixin.py | 63 ++--- .../readme/CONTRIBUTORS.md | 1 + .../readme/DESCRIPTION.md | 8 - .../readme/USAGE.md | 91 ++++++++ .../static/description/icon.png | Bin 9455 -> 10254 bytes .../static/description/index.html | 136 +++++++++-- .../tests/models.py | 2 +- ...product_attribute_value_dependent_mixin.py | 217 ++++++++++++++++-- 10 files changed, 531 insertions(+), 104 deletions(-) create mode 100644 product_attribute_value_dependent_mixin/readme/USAGE.md diff --git a/product_attribute_value_dependent_mixin/README.rst b/product_attribute_value_dependent_mixin/README.rst index ef1d48ed5f4..1c7d08740ba 100644 --- a/product_attribute_value_dependent_mixin/README.rst +++ b/product_attribute_value_dependent_mixin/README.rst @@ -34,19 +34,116 @@ 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: +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 =========== @@ -71,6 +168,7 @@ Contributors - `Akretion `__ - Chafique Delli + - Guillaume Masson Maintainers ----------- diff --git a/product_attribute_value_dependent_mixin/__manifest__.py b/product_attribute_value_dependent_mixin/__manifest__.py index 643dba70339..59270435ef6 100644 --- a/product_attribute_value_dependent_mixin/__manifest__.py +++ b/product_attribute_value_dependent_mixin/__manifest__.py @@ -10,7 +10,6 @@ "application": False, "installable": True, "category": "Product", - "version": "16.0.1.0.0", + "version": "18.0.1.0.0", "depends": ["product"], - "data": [], } 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..76847dca721 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 @@ -2,14 +2,13 @@ # 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" +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.md b/product_attribute_value_dependent_mixin/readme/CONTRIBUTORS.md index 3efa1e694f2..714484819c7 100644 --- a/product_attribute_value_dependent_mixin/readme/CONTRIBUTORS.md +++ b/product_attribute_value_dependent_mixin/readme/CONTRIBUTORS.md @@ -1,2 +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 index a817aa0e64d..3b0343c261c 100644 --- a/product_attribute_value_dependent_mixin/readme/DESCRIPTION.md +++ b/product_attribute_value_dependent_mixin/readme/DESCRIPTION.md @@ -3,11 +3,3 @@ 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/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 index 3a0328b516c4980e8e44cdb63fd945757ddd132d..1dcc49c24f364e9adf0afbc6fc0bac6dbecdeb11 100644 GIT binary patch literal 10254 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I diff --git a/product_attribute_value_dependent_mixin/static/description/index.html b/product_attribute_value_dependent_mixin/static/description/index.html index c0f47e604e9..c55f3e530e0 100644 --- a/product_attribute_value_dependent_mixin/static/description/index.html +++ b/product_attribute_value_dependent_mixin/static/description/index.html @@ -375,29 +375,126 @@

Product Attribute Value Dependent Mixin

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

+
+

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 @@ -405,24 +502,25 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Akretion
-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association diff --git a/product_attribute_value_dependent_mixin/tests/models.py b/product_attribute_value_dependent_mixin/tests/models.py index f70e7621a61..8e79495c892 100644 --- a/product_attribute_value_dependent_mixin/tests/models.py +++ b/product_attribute_value_dependent_mixin/tests/models.py @@ -6,7 +6,7 @@ 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 4e04b76c413..cd78b79b23a 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 @@ -6,35 +6,204 @@ 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() +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 - cls.loader.update_registry((ProductSupplierinfoFake,)) + self.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) + 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( { - "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, + "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)) - @classmethod - def tearDownClass(cls): - cls.loader.restore_registry() - super().tearDownClass() + 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_product_attribute_value_dependent_mixin(self): - fake_model = self.product_supplierinfo_fake - self.assertTrue(fake_model.available_attribute_value_domain) + 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))