diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index 5c3d92dd0b4..5fc5354b946 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -848,16 +848,17 @@ class ProductTemplate(models.Model): self.ensure_one() parent_combination = parent_combination or self.env['product.template.attribute.value'] archived_products = self.with_context(active_test=False).product_variant_ids.filtered(lambda l: not l.active) + active_combinations = set(tuple(product.product_template_attribute_value_ids.ids) for product in self.product_variant_ids) return { 'exclusions': self._complete_inverse_exclusions(self._get_own_attribute_exclusions()), - 'archived_combinations': [ - product.product_template_attribute_value_ids.ids + 'archived_combinations': list(set( + tuple(product.product_template_attribute_value_ids.ids) for product in archived_products if product.product_template_attribute_value_ids and all( ptav.ptav_active for ptav in product.product_template_attribute_value_ids ) - ], + ) - active_combinations), 'parent_exclusions': self._get_parent_attribute_exclusions(parent_combination), 'parent_combination': parent_combination.ids, 'parent_product_name': parent_name, diff --git a/addons/sale_product_configurator/tests/test_product_configurator_data.py b/addons/sale_product_configurator/tests/test_product_configurator_data.py index bb997bc9f69..c420a6ba517 100644 --- a/addons/sale_product_configurator/tests/test_product_configurator_data.py +++ b/addons/sale_product_configurator/tests/test_product_configurator_data.py @@ -2,7 +2,7 @@ from odoo.fields import Command from odoo.tests import HttpCase, tagged -from odoo.addons.product.tests.common import ProductVariantsCommon +from odoo.addons.product.tests.common import ProductAttributesCommon, ProductVariantsCommon from odoo.addons.sale.tests.common import SaleCommon @@ -153,3 +153,100 @@ class TestProductConfiguratorData(HttpCase, ProductVariantsCommon, SaleCommon): # The inactive PTAVs should not be in the product exclusions dict self.assertFalse(str(ptav_with_exclusion.id) in result['products'][0]['exclusions']) self.assertFalse(str(ptav_excluded.id) in result['products'][0]['exclusions']) + + +@tagged('post_install', '-at_install') +class TestSaleProductVariants(ProductAttributesCommon, SaleCommon): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.product_template_2lines_2attributes = cls.env['product.template'].create({ + 'name': '2 lines 2 attributes', + 'uom_id': cls.uom_unit.id, + 'uom_po_id': cls.uom_unit.id, + 'categ_id': cls.product_category.id, + 'attribute_line_ids': [ + Command.create({ + 'attribute_id': cls.color_attribute.id, + 'value_ids': [Command.set([ + cls.color_attribute_red.id, + cls.color_attribute_blue.id, + ])], + }), + Command.create({ + 'attribute_id': cls.size_attribute.id, + 'value_ids': [Command.set([ + cls.size_attribute_s.id, + cls.size_attribute_m.id, + ])] + }) + ] + }) + + # Sell all variants + cls.empty_order.order_line = [ + Command.create({ + 'product_id': product.id, + }) + for product in cls.product_template_2lines_2attributes.product_variant_ids + ] + + def test_attribute_removal(self): + def _get_ptavs(): + return self.product_template_2lines_2attributes.with_context( + active_test=False + ).attribute_line_ids.product_template_value_ids + + def _get_archived_variants(): + return self.product_template_2lines_2attributes.with_context( + active_test=False + ).product_variant_ids.filtered(lambda p: not p.active) + + def _get_active_variants(): + return self.product_template_2lines_2attributes.product_variant_ids + + self.assertEqual(len(_get_ptavs()), 4) + self.product_template_2lines_2attributes.attribute_line_ids = [ + Command.unlink(self.product_template_2lines_2attributes.attribute_line_ids.filtered( + lambda ptal: ptal.attribute_id.id == self.size_attribute.id + ).id) + ] + self.assertEqual(len(_get_ptavs()), 4) + + # Use products s.t. they are archived and not deleted + self.empty_order.order_line = [ + Command.create({ + 'product_id': product.id, + }) + for product in self.product_template_2lines_2attributes.product_variant_ids + ] + + self.assertEqual(len(_get_archived_variants()), 4) + self.assertEqual(len(_get_active_variants()), 2) + + self.product_template_2lines_2attributes.attribute_line_ids = [ + Command.create({ + 'attribute_id': self.size_attribute.id, + 'value_ids': [Command.set([ + self.size_attribute_s.id, + ])] + }) + ] + self.assertEqual(len(_get_ptavs()), 4) + self.assertEqual(len(_get_active_variants()), 2) + self.assertEqual(len(_get_archived_variants()), 4) + + # When adding a single attribute line, the attribute will be added to all existing variants + # Instead of unarchiving existing archived variants with the same combination + # Leading to a state where the database holds two variants with the same combination + # We don't want this combination to be excluded from the product configurator as it is valid + # as long as there is one active variant with this configuration. + exclusions_data = self.product_template_2lines_2attributes._get_attribute_exclusions() + self.assertTrue( + all( + tuple(product.product_template_attribute_value_ids.ids) not in exclusions_data['archived_combinations'] + for product in _get_active_variants() + ) + )