From c91d340d759cc9f9668b972be55d313113f71d42 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Fri, 20 Oct 2023 13:27:08 +0200 Subject: [PATCH 1/3] [ADD] product_attribute_variant_rules --- product_attribute_variant_rules/README.rst | 110 +++++ product_attribute_variant_rules/__init__.py | 1 + .../__manifest__.py | 16 + .../models/__init__.py | 2 + .../models/product_attribute_rule.py | 152 ++++++ .../models/product_template.py | 72 +++ .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 23 + .../readme/USAGE.rst | 7 + .../security/ir.model.access.csv | 3 + .../static/description/index.html | 451 ++++++++++++++++++ .../tests/__init__.py | 1 + .../test_product_attribute_variant_rules.py | 316 ++++++++++++ .../views/product_template_views.xml | 42 ++ .../addons/product_attribute_variant_rules | 1 + .../product_attribute_variant_rules/setup.py | 6 + 16 files changed, 1204 insertions(+) create mode 100644 product_attribute_variant_rules/README.rst create mode 100644 product_attribute_variant_rules/__init__.py create mode 100644 product_attribute_variant_rules/__manifest__.py create mode 100644 product_attribute_variant_rules/models/__init__.py create mode 100644 product_attribute_variant_rules/models/product_attribute_rule.py create mode 100644 product_attribute_variant_rules/models/product_template.py create mode 100644 product_attribute_variant_rules/readme/CONTRIBUTORS.rst create mode 100644 product_attribute_variant_rules/readme/DESCRIPTION.rst create mode 100644 product_attribute_variant_rules/readme/USAGE.rst create mode 100644 product_attribute_variant_rules/security/ir.model.access.csv create mode 100644 product_attribute_variant_rules/static/description/index.html create mode 100644 product_attribute_variant_rules/tests/__init__.py create mode 100644 product_attribute_variant_rules/tests/test_product_attribute_variant_rules.py create mode 100644 product_attribute_variant_rules/views/product_template_views.xml create mode 120000 setup/product_attribute_variant_rules/odoo/addons/product_attribute_variant_rules create mode 100644 setup/product_attribute_variant_rules/setup.py diff --git a/product_attribute_variant_rules/README.rst b/product_attribute_variant_rules/README.rst new file mode 100644 index 00000000000..9993d526e9e --- /dev/null +++ b/product_attribute_variant_rules/README.rst @@ -0,0 +1,110 @@ +=============================== +Product Attribute Variant Rules +=============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:3557e19d06bc602378a8840ca208031e48ff3106d82706f0ffe66904d612c18b + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/14.0/product_attribute_variant_rules + :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-14-0/product-attribute-14-0-product_attribute_variant_rules + :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=14.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds a more powerful way to describe your product attributes combinations +than the default exclusions. + +It allows to write rules like: + + * All products with blue or green color in XL size will appear only with a V neck collar + and a short sleeve. + * All L size products will never appear with a sailor collar. + +The rules are split between a precondition a type and a postcondition. + +Different attributes are ANDed and same attributes are ORed. + +For instance the rule:: + + All products with blue or green color in XL size will appear only with a V neck collar and a short sleeve. + + +Will be written as:: + + Precondition: (color: blue), (color: green), (size: XL) + Type: Only With + Postcondition: (collar: V neck), (sleeve: short) + + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +After saving your product click on the Use Attribute Rules checkbox in the product +variants tab. + +Then add your different rules, specifying optionally some rule preconditions, a type of +inclusion/exclusion and the rule postconditions. + +When you save your product the variant will be recomputed. + +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 +~~~~~~~~~~~~ + +* Florian Mounier + +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_variant_rules/__init__.py b/product_attribute_variant_rules/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/product_attribute_variant_rules/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/product_attribute_variant_rules/__manifest__.py b/product_attribute_variant_rules/__manifest__.py new file mode 100644 index 00000000000..6eea0253b64 --- /dev/null +++ b/product_attribute_variant_rules/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2023 Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Product Attribute Variant Rules", + "summary": "Add better rules for product variants generation from attributes", + "version": "14.0.1.0.0", + "license": "AGPL-3", + "author": "Akretion,Odoo Community Association (OCA)", + "depends": ["product"], + "data": [ + "security/ir.model.access.csv", + "views/product_template_views.xml", + ], + "website": "https://github.com/OCA/product-attribute", +} diff --git a/product_attribute_variant_rules/models/__init__.py b/product_attribute_variant_rules/models/__init__.py new file mode 100644 index 00000000000..25d52922cb3 --- /dev/null +++ b/product_attribute_variant_rules/models/__init__.py @@ -0,0 +1,2 @@ +from . import product_attribute_rule +from . import product_template diff --git a/product_attribute_variant_rules/models/product_attribute_rule.py b/product_attribute_variant_rules/models/product_attribute_rule.py new file mode 100644 index 00000000000..efa78f5afd3 --- /dev/null +++ b/product_attribute_variant_rules/models/product_attribute_rule.py @@ -0,0 +1,152 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Florian Mounier +from collections import defaultdict + +from odoo import api, fields, models + + +class ProductAttributeRule(models.Model): + _name = "product.attribute.rule" + _description = "Product Attribute Rule" + + product_tmpl_id = fields.Many2one( + comodel_name="product.template", + string="Product Template", + required=True, + ondelete="cascade", + ) + + product_attribute_value_precondition_ids = fields.Many2many( + comodel_name="product.attribute.value", + relation="product_attribute_rule_precondition_rel", + string="Rule preconditions", + help="This rule will only be applied if all the preconditions are met.\n" + "The attribute values are ANDed together except if they are from the " + "same attribute in which case they are ORed.\n" + "If empty, the rule will always be applied.", + ) + + type = fields.Selection( + [ + ("only", "Only With"), + ("never", "Never With"), + ], + string="Type", + default="only", + required=True, + ) + + product_attribute_value_postcondition_ids = fields.Many2many( + comodel_name="product.attribute.value", + relation="product_attribute_rule_postcondition_rel", + string="Rule postconditions", + help="The product variant will exist only if these conditions are met " + "if the precondition is met.\n" + "The attribute values are ANDed together except if they are from the " + "same attribute in which case they are ORed.", + required=True, + ) + + available_precondition_attribute_ids = fields.Many2many( + comodel_name="product.attribute", + compute="_compute_available_precondition_attribute_ids", + string="Available preconditions", + ) + + available_postcondition_attribute_ids = fields.Many2many( + comodel_name="product.attribute", + compute="_compute_available_postcondition_attribute_ids", + string="Available postconditions", + ) + + @api.depends( + "product_tmpl_id.attribute_line_ids", + "product_attribute_value_postcondition_ids", + ) + def _compute_available_precondition_attribute_ids(self): + """ + Compute the available preconditions. + """ + for rule in self: + rule.available_precondition_attribute_ids = ( + rule.product_tmpl_id.attribute_line_ids.mapped("attribute_id") + - rule.product_attribute_value_postcondition_ids.mapped("attribute_id") + ) + + @api.depends( + "product_tmpl_id.attribute_line_ids", "product_attribute_value_precondition_ids" + ) + def _compute_available_postcondition_attribute_ids(self): + """ + Compute the available postconditions. + """ + for rule in self: + rule.available_postcondition_attribute_ids = ( + rule.product_tmpl_id.attribute_line_ids.mapped("attribute_id") + - rule.product_attribute_value_precondition_ids.mapped("attribute_id") + ) + + def _is_combination_possible(self, combination): + """ + Check if the combination is possible with the rules defined on the + product template. + """ + # Check if the combination matches the preconditions + if not self._combination_matches_conditions( + combination, self.product_attribute_value_precondition_ids + ): + # If the combination does not match the preconditions, + # the rule is not applied + return True + + # Check if the combination matches the postconditions + match = self._combination_matches_conditions( + combination, self.product_attribute_value_postcondition_ids + ) + + if self.type == "only" and match: + # Both conditions are met in only, the combination is possible + return True + elif self.type == "never" and not match: + # Precondition is met but postcondition is not met in never, + # the combination is possible + return True + + # The combination is not possible + return False + + def _combination_matches_conditions(self, combination, conditions): + """ + Check if the combination matches the given conditions. + """ + # If there is no condition, the combination matches + # (only possible for the preconditions) + if not conditions: + return True + + # Check if the combination matches the preconditions ANDed between + # different attributes + for attribute, attribute_values in self._aggregate_conditions( + conditions + ).items(): + if ( + combination.filtered( + lambda value: value.attribute_id == attribute + ).product_attribute_value_id + not in attribute_values # The OR between the same attribute values + ): + # The combination does not match a precondition + return False + + # The combination matches all preconditions + return True + + def _aggregate_conditions(self, conditions): + """ + Group the attribute values by attribute. + """ + aggregated_conditions = defaultdict(set) + for condition in conditions: + aggregated_conditions[condition.attribute_id].add(condition) + + return aggregated_conditions diff --git a/product_attribute_variant_rules/models/product_template.py b/product_attribute_variant_rules/models/product_template.py new file mode 100644 index 00000000000..57f0348a10b --- /dev/null +++ b/product_attribute_variant_rules/models/product_template.py @@ -0,0 +1,72 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Florian Mounier + +from odoo import api, fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + product_attribute_rule_ids = fields.One2many( + comodel_name="product.attribute.rule", + inverse_name="product_tmpl_id", + string="Product Attribute Rules", + ) + + use_attribute_rules = fields.Boolean( + string="Use Attribute Rules", + help="If checked, the product variants will be generated based on the rules " + "defined below.", + ) + + product_attribute_value_ids = fields.Many2many( + string="Technical Attributes", + comodel_name="product.attribute.value", + compute="_compute_product_attribute_value_ids", + ) + + @api.depends("attribute_line_ids.value_ids") + def _compute_product_attribute_value_ids(self): + for template in self: + template.product_attribute_value_ids = template.mapped( + "attribute_line_ids.value_ids" + ) + + def _is_combination_possible_by_config(self, combination, ignore_no_variant=False): + rv = super()._is_combination_possible_by_config( + combination, ignore_no_variant=ignore_no_variant + ) + if ( + not rv # Combination is not possible + or not self.use_attribute_rules # Rules are not enabled + or not self.product_attribute_rule_ids # No rules defined + ): + return rv + + # Check if the combination matches the rules + return self._is_combination_possible_with_rules(combination) + + def _is_combination_possible_with_rules(self, combination): + """ + Check if the combination is possible with the rules defined on the product template. + """ + # Rules are ANDed together + for rule in self.product_attribute_rule_ids: + if not rule._is_combination_possible(combination): + return False + return True + + def write(self, vals): + res = super(ProductTemplate, self).write(vals) + + # Recreate variants if the rules have changed + empty = vals.get("active") and len(self.product_variant_ids) == 0 + if "attribute_line_ids" in vals or empty: + # Already done in super + return res + + if "use_attribute_rules" in vals or "product_attribute_rule_ids" in vals: + # Recreate variants + self._create_variant_ids() + + return res diff --git a/product_attribute_variant_rules/readme/CONTRIBUTORS.rst b/product_attribute_variant_rules/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..8d00d9f04be --- /dev/null +++ b/product_attribute_variant_rules/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Florian Mounier diff --git a/product_attribute_variant_rules/readme/DESCRIPTION.rst b/product_attribute_variant_rules/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..86741cbe197 --- /dev/null +++ b/product_attribute_variant_rules/readme/DESCRIPTION.rst @@ -0,0 +1,23 @@ +This module adds a more powerful way to describe your product attributes combinations +than the default exclusions. + +It allows to write rules like: + + * All products with blue or green color in XL size will appear only with a V neck collar + and a short sleeve. + * All L size products will never appear with a sailor collar. + +The rules are split between a precondition a type and a postcondition. + +Different attributes are ANDed and same attributes are ORed. + +For instance the rule:: + + All products with blue or green color in XL size will appear only with a V neck collar and a short sleeve. + + +Will be written as:: + + Precondition: (color: blue), (color: green), (size: XL) + Type: Only With + Postcondition: (collar: V neck), (sleeve: short) diff --git a/product_attribute_variant_rules/readme/USAGE.rst b/product_attribute_variant_rules/readme/USAGE.rst new file mode 100644 index 00000000000..b7f0e2a5a89 --- /dev/null +++ b/product_attribute_variant_rules/readme/USAGE.rst @@ -0,0 +1,7 @@ +After saving your product click on the Use Attribute Rules checkbox in the product +variants tab. + +Then add your different rules, specifying optionally some rule preconditions, a type of +inclusion/exclusion and the rule postconditions. + +When you save your product the variant will be recomputed. diff --git a/product_attribute_variant_rules/security/ir.model.access.csv b/product_attribute_variant_rules/security/ir.model.access.csv new file mode 100644 index 00000000000..88383d070b3 --- /dev/null +++ b/product_attribute_variant_rules/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_product_product_attribute_rule,product.template.attribute rule,model_product_attribute_rule,base.group_user,1,0,0,0 +access_product_product_attribute_rule_manager,product.template.attribute rule manager,model_product_attribute_rule,base.group_system,1,1,1,1 diff --git a/product_attribute_variant_rules/static/description/index.html b/product_attribute_variant_rules/static/description/index.html new file mode 100644 index 00000000000..f6dfcb79797 --- /dev/null +++ b/product_attribute_variant_rules/static/description/index.html @@ -0,0 +1,451 @@ + + + + + + +Product Attribute Variant Rules + + + +
+

Product Attribute Variant Rules

+ + +

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

+

This module adds a more powerful way to describe your product attributes combinations +than the default exclusions.

+

It allows to write rules like:

+
+
    +
  • All products with blue or green color in XL size will appear only with a V neck collar +and a short sleeve.
  • +
  • All L size products will never appear with a sailor collar.
  • +
+
+

The rules are split between a precondition a type and a postcondition.

+

Different attributes are ANDed and same attributes are ORed.

+

For instance the rule:

+
+All products with blue or green color in XL size will appear only with a V neck collar and a short sleeve.
+
+

Will be written as:

+
+Precondition: (color: blue), (color: green), (size: XL)
+Type: Only With
+Postcondition: (collar: V neck), (sleeve: short)
+
+

Table of contents

+ +
+

Usage

+

After saving your product click on the Use Attribute Rules checkbox in the product +variants tab.

+

Then add your different rules, specifying optionally some rule preconditions, a type of +inclusion/exclusion and the rule postconditions.

+

When you save your product the variant will be recomputed.

+
+
+

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_variant_rules/tests/__init__.py b/product_attribute_variant_rules/tests/__init__.py new file mode 100644 index 00000000000..befd4a59270 --- /dev/null +++ b/product_attribute_variant_rules/tests/__init__.py @@ -0,0 +1 @@ +from . import test_product_attribute_variant_rules diff --git a/product_attribute_variant_rules/tests/test_product_attribute_variant_rules.py b/product_attribute_variant_rules/tests/test_product_attribute_variant_rules.py new file mode 100644 index 00000000000..63d9ee6d44d --- /dev/null +++ b/product_attribute_variant_rules/tests/test_product_attribute_variant_rules.py @@ -0,0 +1,316 @@ +from odoo.addons.product.tests.test_product_attribute_value_config import ( + TestProductAttributeValueCommon, +) + + +class TestProductAttributeVariantRules(TestProductAttributeValueCommon): + def _expand_variants_attributes(self, product_template): + return { + self._expand_variant_attributes(variant) + for variant in product_template._get_possible_variants() + } + + def _expand_variant_attributes(self, variant): + return tuple(variant.product_template_attribute_value_ids.mapped("name")) + + def test_product_attribute_no_rules_variants(self): + self.assertEqual( + self._expand_variants_attributes(self.computer), + { + ("256 GB", "8 GB", "1 To"), + ("256 GB", "8 GB", "2 To"), + ("256 GB", "8 GB", "4 To"), + ("256 GB", "16 GB", "1 To"), + ("256 GB", "16 GB", "2 To"), + ("256 GB", "16 GB", "4 To"), + ("256 GB", "32 GB", "1 To"), + ("256 GB", "32 GB", "2 To"), + ("256 GB", "32 GB", "4 To"), + ("512 GB", "8 GB", "1 To"), + ("512 GB", "8 GB", "2 To"), + ("512 GB", "8 GB", "4 To"), + ("512 GB", "16 GB", "1 To"), + ("512 GB", "16 GB", "2 To"), + ("512 GB", "16 GB", "4 To"), + ("512 GB", "32 GB", "1 To"), + ("512 GB", "32 GB", "2 To"), + ("512 GB", "32 GB", "4 To"), + }, + ) + + def test_product_attribute_inactive_rules_variants(self): + self.computer.product_attribute_rule_ids = [ + ( + 0, + 0, + { + "product_tmpl_id": self.computer.id, + "product_attribute_value_precondition_ids": [(4, self.ram_16.id)], + "type": "only", + "product_attribute_value_postcondition_ids": [(4, self.hdd_1.id)], + }, + ) + ] + self.assertEqual( + self._expand_variants_attributes(self.computer), + { + ("256 GB", "8 GB", "1 To"), + ("256 GB", "8 GB", "2 To"), + ("256 GB", "8 GB", "4 To"), + ("256 GB", "16 GB", "1 To"), + ("256 GB", "16 GB", "2 To"), + ("256 GB", "16 GB", "4 To"), + ("256 GB", "32 GB", "1 To"), + ("256 GB", "32 GB", "2 To"), + ("256 GB", "32 GB", "4 To"), + ("512 GB", "8 GB", "1 To"), + ("512 GB", "8 GB", "2 To"), + ("512 GB", "8 GB", "4 To"), + ("512 GB", "16 GB", "1 To"), + ("512 GB", "16 GB", "2 To"), + ("512 GB", "16 GB", "4 To"), + ("512 GB", "32 GB", "1 To"), + ("512 GB", "32 GB", "2 To"), + ("512 GB", "32 GB", "4 To"), + }, + ) + + def test_product_attribute_rule_simple_only_rule(self): + self.computer.use_attribute_rules = True + # Remove all variants that have 16GB RAM and not 1To HDD + self.computer.product_attribute_rule_ids = [ + ( + 0, + 0, + { + "product_tmpl_id": self.computer.id, + "product_attribute_value_precondition_ids": [(4, self.ram_16.id)], + "type": "only", + "product_attribute_value_postcondition_ids": [(4, self.hdd_1.id)], + }, + ) + ] + self.assertEqual( + self._expand_variants_attributes(self.computer), + { + ("256 GB", "8 GB", "1 To"), + ("256 GB", "8 GB", "2 To"), + ("256 GB", "8 GB", "4 To"), + ("256 GB", "16 GB", "1 To"), + ("256 GB", "32 GB", "1 To"), + ("256 GB", "32 GB", "2 To"), + ("256 GB", "32 GB", "4 To"), + ("512 GB", "8 GB", "1 To"), + ("512 GB", "8 GB", "2 To"), + ("512 GB", "8 GB", "4 To"), + ("512 GB", "16 GB", "1 To"), + ("512 GB", "32 GB", "1 To"), + ("512 GB", "32 GB", "2 To"), + ("512 GB", "32 GB", "4 To"), + }, + ) + + def test_product_attribute_rule_simple_never_rule(self): + self.computer.use_attribute_rules = True + # Remove all variants that have 16GB RAM and 1To HDD + self.computer.product_attribute_rule_ids = [ + ( + 0, + 0, + { + "product_tmpl_id": self.computer.id, + "product_attribute_value_precondition_ids": [(4, self.ram_16.id)], + "type": "never", + "product_attribute_value_postcondition_ids": [(4, self.hdd_1.id)], + }, + ) + ] + self.assertEqual( + self._expand_variants_attributes(self.computer), + { + ("256 GB", "8 GB", "1 To"), + ("256 GB", "8 GB", "2 To"), + ("256 GB", "8 GB", "4 To"), + ("256 GB", "16 GB", "2 To"), + ("256 GB", "16 GB", "4 To"), + ("256 GB", "32 GB", "1 To"), + ("256 GB", "32 GB", "2 To"), + ("256 GB", "32 GB", "4 To"), + ("512 GB", "8 GB", "1 To"), + ("512 GB", "8 GB", "2 To"), + ("512 GB", "8 GB", "4 To"), + ("512 GB", "16 GB", "2 To"), + ("512 GB", "16 GB", "4 To"), + ("512 GB", "32 GB", "1 To"), + ("512 GB", "32 GB", "2 To"), + ("512 GB", "32 GB", "4 To"), + }, + ) + + def test_product_attribute_rule_complex(self): + self.computer.use_attribute_rules = True + # Remove all variants that have (8GB or 32GB RAM and 2To HDD) and not 256GB SSD + # Remove all variants that have (16GB RAM) and not ((2To or 4To HDD) and 512GB SSD) + self.computer.product_attribute_rule_ids = [ + ( + 0, + 0, + { + "product_tmpl_id": self.computer.id, + "product_attribute_value_precondition_ids": [ + (4, self.ram_8.id), + (4, self.ram_32.id), + (4, self.hdd_2.id), + ], + "type": "only", + "product_attribute_value_postcondition_ids": [(4, self.ssd_256.id)], + }, + ), + ( + 0, + 0, + { + "product_tmpl_id": self.computer.id, + "product_attribute_value_precondition_ids": [(4, self.ram_16.id)], + "type": "never", + "product_attribute_value_postcondition_ids": [ + (4, self.hdd_4.id), + (4, self.ssd_512.id), + (4, self.hdd_2.id), + ], + }, + ), + ] + self.assertEqual( + self._expand_variants_attributes(self.computer), + { + ("256 GB", "8 GB", "1 To"), + ("256 GB", "8 GB", "2 To"), + ("256 GB", "8 GB", "4 To"), + ("256 GB", "16 GB", "1 To"), + ("256 GB", "16 GB", "2 To"), + ("256 GB", "16 GB", "4 To"), + ("256 GB", "32 GB", "1 To"), + ("256 GB", "32 GB", "2 To"), + ("256 GB", "32 GB", "4 To"), + ("512 GB", "8 GB", "1 To"), + ("512 GB", "8 GB", "4 To"), + ("512 GB", "16 GB", "1 To"), + ("512 GB", "32 GB", "1 To"), + ("512 GB", "32 GB", "4 To"), + }, + ) + + def test_product_attribute_rule_no_precondition_only(self): + self.computer.use_attribute_rules = True + # Remove all variants that have not (256GB SSD and 8GB or 32GB RAM and 2To HDD) + + self.computer.product_attribute_rule_ids = [ + ( + 0, + 0, + { + "product_tmpl_id": self.computer.id, + "product_attribute_value_precondition_ids": [(5,)], + "type": "only", + "product_attribute_value_postcondition_ids": [ + (4, self.ssd_256.id), + (4, self.ram_8.id), + (4, self.ram_32.id), + (4, self.hdd_2.id), + ], + }, + ), + ] + self.assertEqual( + self._expand_variants_attributes(self.computer), + { + ("256 GB", "8 GB", "2 To"), + ("256 GB", "32 GB", "2 To"), + }, + ) + + def test_product_attribute_rule_no_precondition_never(self): + self.computer.use_attribute_rules = True + # Remove all variants that have (256GB SSD and 8GB or 32GB RAM and 2To HDD) + + self.computer.product_attribute_rule_ids = [ + ( + 0, + 0, + { + "product_tmpl_id": self.computer.id, + "product_attribute_value_precondition_ids": [(5,)], + "type": "never", + "product_attribute_value_postcondition_ids": [ + (4, self.ssd_256.id), + (4, self.ram_8.id), + (4, self.ram_32.id), + (4, self.hdd_2.id), + ], + }, + ), + ] + self.assertEqual( + self._expand_variants_attributes(self.computer), + { + ("256 GB", "8 GB", "1 To"), + ("256 GB", "8 GB", "4 To"), + ("256 GB", "16 GB", "1 To"), + ("256 GB", "16 GB", "2 To"), + ("256 GB", "16 GB", "4 To"), + ("256 GB", "32 GB", "1 To"), + ("256 GB", "32 GB", "4 To"), + ("512 GB", "8 GB", "1 To"), + ("512 GB", "8 GB", "2 To"), + ("512 GB", "8 GB", "4 To"), + ("512 GB", "16 GB", "1 To"), + ("512 GB", "16 GB", "2 To"), + ("512 GB", "16 GB", "4 To"), + ("512 GB", "32 GB", "1 To"), + ("512 GB", "32 GB", "2 To"), + ("512 GB", "32 GB", "4 To"), + }, + ) + + def test_product_attribute_rule_no_precondition_both(self): + self.computer.use_attribute_rules = True + + self.computer.product_attribute_rule_ids = [ + ( + 0, + 0, + { + "product_tmpl_id": self.computer.id, + "product_attribute_value_precondition_ids": [(5,)], + "type": "never", + "product_attribute_value_postcondition_ids": [ + (4, self.ssd_256.id), + (4, self.ram_8.id), + (4, self.ram_32.id), + (4, self.hdd_2.id), + ], + }, + ), + ( + 0, + 0, + { + "product_tmpl_id": self.computer.id, + "product_attribute_value_precondition_ids": [(5,)], + "type": "only", + "product_attribute_value_postcondition_ids": [ + (4, self.ssd_256.id), + (4, self.ram_8.id), + (4, self.hdd_1.id), + (4, self.hdd_2.id), + ], + }, + ), + ] + self.assertEqual( + self._expand_variants_attributes(self.computer), + { + ("256 GB", "8 GB", "1 To"), + }, + ) diff --git a/product_attribute_variant_rules/views/product_template_views.xml b/product_attribute_variant_rules/views/product_template_views.xml new file mode 100644 index 00000000000..43e8d2f78a0 --- /dev/null +++ b/product_attribute_variant_rules/views/product_template_views.xml @@ -0,0 +1,42 @@ + + + + product.template + + + + + + + + + + + + + + diff --git a/setup/product_attribute_variant_rules/odoo/addons/product_attribute_variant_rules b/setup/product_attribute_variant_rules/odoo/addons/product_attribute_variant_rules new file mode 120000 index 00000000000..c2b14b8647c --- /dev/null +++ b/setup/product_attribute_variant_rules/odoo/addons/product_attribute_variant_rules @@ -0,0 +1 @@ +../../../../product_attribute_variant_rules \ No newline at end of file diff --git a/setup/product_attribute_variant_rules/setup.py b/setup/product_attribute_variant_rules/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/product_attribute_variant_rules/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 4b8f3ccf46b0253d28c6e7c5f54b6ee875427e64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Mon, 23 Oct 2023 19:22:38 +0200 Subject: [PATCH 2/3] product_attribute_variant_rules: rename type to rule_type --- .../models/product_attribute_rule.py | 6 +++--- .../test_product_attribute_variant_rules.py | 18 +++++++++--------- .../views/product_template_views.xml | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/product_attribute_variant_rules/models/product_attribute_rule.py b/product_attribute_variant_rules/models/product_attribute_rule.py index efa78f5afd3..dcfb1eec954 100644 --- a/product_attribute_variant_rules/models/product_attribute_rule.py +++ b/product_attribute_variant_rules/models/product_attribute_rule.py @@ -26,7 +26,7 @@ class ProductAttributeRule(models.Model): "If empty, the rule will always be applied.", ) - type = fields.Selection( + rule_type = fields.Selection( [ ("only", "Only With"), ("never", "Never With"), @@ -104,10 +104,10 @@ def _is_combination_possible(self, combination): combination, self.product_attribute_value_postcondition_ids ) - if self.type == "only" and match: + if self.rule_type == "only" and match: # Both conditions are met in only, the combination is possible return True - elif self.type == "never" and not match: + elif self.rule_type == "never" and not match: # Precondition is met but postcondition is not met in never, # the combination is possible return True diff --git a/product_attribute_variant_rules/tests/test_product_attribute_variant_rules.py b/product_attribute_variant_rules/tests/test_product_attribute_variant_rules.py index 63d9ee6d44d..b9b969109fa 100644 --- a/product_attribute_variant_rules/tests/test_product_attribute_variant_rules.py +++ b/product_attribute_variant_rules/tests/test_product_attribute_variant_rules.py @@ -46,7 +46,7 @@ def test_product_attribute_inactive_rules_variants(self): { "product_tmpl_id": self.computer.id, "product_attribute_value_precondition_ids": [(4, self.ram_16.id)], - "type": "only", + "rule_type": "only", "product_attribute_value_postcondition_ids": [(4, self.hdd_1.id)], }, ) @@ -85,7 +85,7 @@ def test_product_attribute_rule_simple_only_rule(self): { "product_tmpl_id": self.computer.id, "product_attribute_value_precondition_ids": [(4, self.ram_16.id)], - "type": "only", + "rule_type": "only", "product_attribute_value_postcondition_ids": [(4, self.hdd_1.id)], }, ) @@ -120,7 +120,7 @@ def test_product_attribute_rule_simple_never_rule(self): { "product_tmpl_id": self.computer.id, "product_attribute_value_precondition_ids": [(4, self.ram_16.id)], - "type": "never", + "rule_type": "never", "product_attribute_value_postcondition_ids": [(4, self.hdd_1.id)], }, ) @@ -162,7 +162,7 @@ def test_product_attribute_rule_complex(self): (4, self.ram_32.id), (4, self.hdd_2.id), ], - "type": "only", + "rule_type": "only", "product_attribute_value_postcondition_ids": [(4, self.ssd_256.id)], }, ), @@ -172,7 +172,7 @@ def test_product_attribute_rule_complex(self): { "product_tmpl_id": self.computer.id, "product_attribute_value_precondition_ids": [(4, self.ram_16.id)], - "type": "never", + "rule_type": "never", "product_attribute_value_postcondition_ids": [ (4, self.hdd_4.id), (4, self.ssd_512.id), @@ -212,7 +212,7 @@ def test_product_attribute_rule_no_precondition_only(self): { "product_tmpl_id": self.computer.id, "product_attribute_value_precondition_ids": [(5,)], - "type": "only", + "rule_type": "only", "product_attribute_value_postcondition_ids": [ (4, self.ssd_256.id), (4, self.ram_8.id), @@ -241,7 +241,7 @@ def test_product_attribute_rule_no_precondition_never(self): { "product_tmpl_id": self.computer.id, "product_attribute_value_precondition_ids": [(5,)], - "type": "never", + "rule_type": "never", "product_attribute_value_postcondition_ids": [ (4, self.ssd_256.id), (4, self.ram_8.id), @@ -283,7 +283,7 @@ def test_product_attribute_rule_no_precondition_both(self): { "product_tmpl_id": self.computer.id, "product_attribute_value_precondition_ids": [(5,)], - "type": "never", + "rule_type": "never", "product_attribute_value_postcondition_ids": [ (4, self.ssd_256.id), (4, self.ram_8.id), @@ -298,7 +298,7 @@ def test_product_attribute_rule_no_precondition_both(self): { "product_tmpl_id": self.computer.id, "product_attribute_value_precondition_ids": [(5,)], - "type": "only", + "rule_type": "only", "product_attribute_value_postcondition_ids": [ (4, self.ssd_256.id), (4, self.ram_8.id), diff --git a/product_attribute_variant_rules/views/product_template_views.xml b/product_attribute_variant_rules/views/product_template_views.xml index 43e8d2f78a0..d5947814e87 100644 --- a/product_attribute_variant_rules/views/product_template_views.xml +++ b/product_attribute_variant_rules/views/product_template_views.xml @@ -5,7 +5,7 @@ - + @@ -26,7 +26,7 @@ widget="many2many_tags" domain="[('attribute_id', 'in', available_precondition_attribute_ids), ('id', 'in', parent.product_attribute_value_ids )]" /> - + Date: Wed, 25 Oct 2023 09:41:45 +0200 Subject: [PATCH 3/3] [FIX] product_attribute_variant_rules: Review fixes --- .../models/product_attribute_rule.py | 18 ++++-------------- .../models/product_template.py | 2 +- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/product_attribute_variant_rules/models/product_attribute_rule.py b/product_attribute_variant_rules/models/product_attribute_rule.py index dcfb1eec954..94aa0b52342 100644 --- a/product_attribute_variant_rules/models/product_attribute_rule.py +++ b/product_attribute_variant_rules/models/product_attribute_rule.py @@ -1,8 +1,8 @@ # Copyright 2023 Akretion (https://www.akretion.com). # @author Florian Mounier -from collections import defaultdict from odoo import api, fields, models +from odoo.tools import groupby class ProductAttributeRule(models.Model): @@ -126,9 +126,9 @@ def _combination_matches_conditions(self, combination, conditions): # Check if the combination matches the preconditions ANDed between # different attributes - for attribute, attribute_values in self._aggregate_conditions( - conditions - ).items(): + for attribute, attribute_values in groupby( + conditions, lambda condition: condition.attribute_id + ): if ( combination.filtered( lambda value: value.attribute_id == attribute @@ -140,13 +140,3 @@ def _combination_matches_conditions(self, combination, conditions): # The combination matches all preconditions return True - - def _aggregate_conditions(self, conditions): - """ - Group the attribute values by attribute. - """ - aggregated_conditions = defaultdict(set) - for condition in conditions: - aggregated_conditions[condition.attribute_id].add(condition) - - return aggregated_conditions diff --git a/product_attribute_variant_rules/models/product_template.py b/product_attribute_variant_rules/models/product_template.py index 57f0348a10b..d2dc7b7e310 100644 --- a/product_attribute_variant_rules/models/product_template.py +++ b/product_attribute_variant_rules/models/product_template.py @@ -57,7 +57,7 @@ def _is_combination_possible_with_rules(self, combination): return True def write(self, vals): - res = super(ProductTemplate, self).write(vals) + res = super().write(vals) # Recreate variants if the rules have changed empty = vals.get("active") and len(self.product_variant_ids) == 0