diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/product/tests | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/product/tests')
| -rw-r--r-- | addons/product/tests/__init__.py | 9 | ||||
| -rw-r--r-- | addons/product/tests/common.py | 118 | ||||
| -rw-r--r-- | addons/product/tests/test_name.py | 24 | ||||
| -rw-r--r-- | addons/product/tests/test_pricelist.py | 105 | ||||
| -rw-r--r-- | addons/product/tests/test_product_attribute_value_config.py | 649 | ||||
| -rw-r--r-- | addons/product/tests/test_product_pricelist.py | 204 | ||||
| -rw-r--r-- | addons/product/tests/test_seller.py | 67 | ||||
| -rw-r--r-- | addons/product/tests/test_variants.py | 1181 |
8 files changed, 2357 insertions, 0 deletions
diff --git a/addons/product/tests/__init__.py b/addons/product/tests/__init__.py new file mode 100644 index 00000000..d125ddbe --- /dev/null +++ b/addons/product/tests/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_name +from . import test_variants +from . import test_pricelist +from . import test_product_pricelist +from . import test_seller +from . import test_product_attribute_value_config diff --git a/addons/product/tests/common.py b/addons/product/tests/common.py new file mode 100644 index 00000000..3e3f11c3 --- /dev/null +++ b/addons/product/tests/common.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- + +from odoo.tests import common + + +class TestProductCommon(common.SavepointCase): + + @classmethod + def setUpClass(cls): + super(TestProductCommon, cls).setUpClass() + + # Customer related data + cls.partner_1 = cls.env['res.partner'].create({ + 'name': 'Julia Agrolait', + 'email': 'julia@agrolait.example.com', + }) + + # Product environment related data + Uom = cls.env['uom.uom'] + cls.uom_unit = cls.env.ref('uom.product_uom_unit') + cls.uom_dozen = cls.env.ref('uom.product_uom_dozen') + cls.uom_dunit = Uom.create({ + 'name': 'DeciUnit', + 'category_id': cls.uom_unit.category_id.id, + 'factor_inv': 0.1, + 'factor': 10.0, + 'uom_type': 'smaller', + 'rounding': 0.001}) + cls.uom_weight = cls.env.ref('uom.product_uom_kgm') + Product = cls.env['product.product'] + cls.product_0 = Product.create({ + 'name': 'Work', + 'type': 'service', + 'uom_id': cls.uom_unit.id, + 'uom_po_id': cls.uom_unit.id}) + cls.product_1 = Product.create({ + 'name': 'Courage', + 'type': 'consu', + 'default_code': 'PROD-1', + 'uom_id': cls.uom_dunit.id, + 'uom_po_id': cls.uom_dunit.id}) + + cls.product_2 = Product.create({ + 'name': 'Wood', + 'uom_id': cls.uom_unit.id, + 'uom_po_id': cls.uom_unit.id}) + cls.product_3 = Product.create({ + 'name': 'Stone', + 'uom_id': cls.uom_dozen.id, + 'uom_po_id': cls.uom_dozen.id}) + + cls.product_4 = Product.create({ + 'name': 'Stick', + 'uom_id': cls.uom_dozen.id, + 'uom_po_id': cls.uom_dozen.id}) + cls.product_5 = Product.create({ + 'name': 'Stone Tools', + 'uom_id': cls.uom_unit.id, + 'uom_po_id': cls.uom_unit.id}) + + cls.product_6 = Product.create({ + 'name': 'Door', + 'uom_id': cls.uom_unit.id, + 'uom_po_id': cls.uom_unit.id}) + + cls.prod_att_1 = cls.env['product.attribute'].create({'name': 'Color'}) + cls.prod_attr1_v1 = cls.env['product.attribute.value'].create({'name': 'red', 'attribute_id': cls.prod_att_1.id, 'sequence': 1}) + cls.prod_attr1_v2 = cls.env['product.attribute.value'].create({'name': 'blue', 'attribute_id': cls.prod_att_1.id, 'sequence': 2}) + cls.prod_attr1_v3 = cls.env['product.attribute.value'].create({'name': 'green', 'attribute_id': cls.prod_att_1.id, 'sequence': 3}) + + cls.product_7_template = cls.env['product.template'].create({ + 'name': 'Sofa', + 'uom_id': cls.uom_unit.id, + 'uom_po_id': cls.uom_unit.id, + 'attribute_line_ids': [(0, 0, { + 'attribute_id': cls.prod_att_1.id, + 'value_ids': [(6, 0, [cls.prod_attr1_v1.id, cls.prod_attr1_v2.id, cls.prod_attr1_v3.id])] + })] + }) + + cls.product_7_attr1_v1 = cls.product_7_template.attribute_line_ids[0].product_template_value_ids[0] + cls.product_7_attr1_v2 = cls.product_7_template.attribute_line_ids[0].product_template_value_ids[1] + cls.product_7_attr1_v3 = cls.product_7_template.attribute_line_ids[0].product_template_value_ids[2] + + cls.product_7_1 = cls.product_7_template._get_variant_for_combination(cls.product_7_attr1_v1) + cls.product_7_2 = cls.product_7_template._get_variant_for_combination(cls.product_7_attr1_v2) + cls.product_7_3 = cls.product_7_template._get_variant_for_combination(cls.product_7_attr1_v3) + + cls.product_8 = Product.create({ + 'name': 'House', + 'uom_id': cls.uom_unit.id, + 'uom_po_id': cls.uom_unit.id}) + + cls.product_9 = Product.create({ + 'name': 'Paper', + 'uom_id': cls.uom_unit.id, + 'uom_po_id': cls.uom_unit.id}) + + cls.product_10 = Product.create({ + 'name': 'Stone', + 'uom_id': cls.uom_unit.id, + 'uom_po_id': cls.uom_unit.id}) + + +class TestAttributesCommon(common.SavepointCase): + + @classmethod + def setUpClass(cls): + super(TestAttributesCommon, cls).setUpClass() + + # create 10 attributes with 10 values each + cls.att_names = "ABCDEFGHIJ" + cls.attributes = cls.env['product.attribute'].create([{ + 'name': name, + 'create_variant': 'no_variant', + 'value_ids': [(0, 0, {'name': n}) for n in range(10)] + } for name in cls.att_names + ]) diff --git a/addons/product/tests/test_name.py b/addons/product/tests/test_name.py new file mode 100644 index 00000000..7afbd6cc --- /dev/null +++ b/addons/product/tests/test_name.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests.common import TransactionCase + + +class TestName(TransactionCase): + + def setUp(self): + super().setUp() + self.product_name = 'Product Test Name' + self.product_code = 'PTN' + self.product = self.env['product.product'].create({ + 'name': self.product_name, + 'default_code': self.product_code, + }) + + def test_10_product_name(self): + display_name = self.product.display_name + self.assertEqual(display_name, "[%s] %s" % (self.product_code, self.product_name), + "Code should be preprended the the name as the context is not preventing it.") + display_name = self.product.with_context(display_default_code=False).display_name + self.assertEqual(display_name, self.product_name, + "Code should not be preprended to the name as context should prevent it.") diff --git a/addons/product/tests/test_pricelist.py b/addons/product/tests/test_pricelist.py new file mode 100644 index 00000000..610865f5 --- /dev/null +++ b/addons/product/tests/test_pricelist.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests.common import TransactionCase + + +class TestPricelist(TransactionCase): + + def setUp(self): + super(TestPricelist, self).setUp() + + self.datacard = self.env['product.product'].create({'name': 'Office Lamp'}) + self.usb_adapter = self.env['product.product'].create({'name': 'Office Chair'}) + self.uom_ton = self.env.ref('uom.product_uom_ton') + self.uom_unit_id = self.ref('uom.product_uom_unit') + self.uom_dozen_id = self.ref('uom.product_uom_dozen') + self.uom_kgm_id = self.ref('uom.product_uom_kgm') + + self.public_pricelist = self.env.ref('product.list0') + self.sale_pricelist_id = self.env['product.pricelist'].create({ + 'name': 'Sale pricelist', + 'item_ids': [(0, 0, { + 'compute_price': 'formula', + 'base': 'list_price', # based on public price + 'price_discount': 10, + 'product_id': self.usb_adapter.id, + 'applied_on': '0_product_variant', + }), (0, 0, { + 'compute_price': 'formula', + 'base': 'list_price', # based on public price + 'price_surcharge': -0.5, + 'product_id': self.datacard.id, + 'applied_on': '0_product_variant', + })] + }) + + def test_10_discount(self): + # Make sure the price using a pricelist is the same than without after + # applying the computation manually + context = {} + + public_context = dict(context, pricelist=self.public_pricelist.id) + pricelist_context = dict(context, pricelist=self.sale_pricelist_id.id) + + usb_adapter_without_pricelist = self.usb_adapter.with_context(public_context) + usb_adapter_with_pricelist = self.usb_adapter.with_context(pricelist_context) + self.assertEqual(usb_adapter_with_pricelist.price, usb_adapter_without_pricelist.price*0.9) + + datacard_without_pricelist = self.datacard.with_context(public_context) + datacard_with_pricelist = self.datacard.with_context(pricelist_context) + self.assertEqual(datacard_with_pricelist.price, datacard_without_pricelist.price-0.5) + + # Make sure that changing the unit of measure does not break the unit + # price (after converting) + unit_context = dict(context, pricelist=self.sale_pricelist_id.id, uom=self.uom_unit_id) + dozen_context = dict(context, pricelist=self.sale_pricelist_id.id, uom=self.uom_dozen_id) + + usb_adapter_unit = self.usb_adapter.with_context(unit_context) + usb_adapter_dozen = self.usb_adapter.with_context(dozen_context) + self.assertAlmostEqual(usb_adapter_unit.price*12, usb_adapter_dozen.price) + datacard_unit = self.datacard.with_context(unit_context) + datacard_dozen = self.datacard.with_context(dozen_context) + # price_surcharge applies to product default UoM, here "Units", so surcharge will be multiplied + self.assertAlmostEqual(datacard_unit.price*12, datacard_dozen.price) + + def test_20_pricelist_uom(self): + # Verify that the pricelist rules are correctly using the product's default UoM + # as reference, and return a result according to the target UoM (as specific in the context) + + kg, tonne = self.uom_kgm_id, self.uom_ton.id + tonne_price = 100 + + # make sure 'tonne' resolves down to 1 'kg'. + self.uom_ton.write({'rounding': 0.001}) + # setup product stored in 'tonnes', with a discounted pricelist for qty > 3 tonnes + spam_id = self.env['product.product'].create({ + 'name': '1 tonne of spam', + 'uom_id': self.uom_ton.id, + 'uom_po_id': self.uom_ton.id, + 'list_price': tonne_price, + 'type': 'consu' + }) + + self.env['product.pricelist.item'].create({ + 'pricelist_id': self.public_pricelist.id, + 'applied_on': '0_product_variant', + 'compute_price': 'formula', + 'base': 'list_price', # based on public price + 'min_quantity': 3, # min = 3 tonnes + 'price_surcharge': -10, # -10 EUR / tonne + 'product_id': spam_id.id + }) + pricelist = self.public_pricelist + + def test_unit_price(qty, uom, expected_unit_price): + spam = spam_id.with_context({'uom': uom}) + unit_price = pricelist.with_context({'uom': uom}).get_product_price(spam, qty, False) + self.assertAlmostEqual(unit_price, expected_unit_price, msg='Computed unit price is wrong') + + # Test prices - they are *per unit*, the quantity is only here to match the pricelist rules! + test_unit_price(2, kg, tonne_price / 1000.0) + test_unit_price(2000, kg, tonne_price / 1000.0) + test_unit_price(3500, kg, (tonne_price - 10) / 1000.0) + test_unit_price(2, tonne, tonne_price) + test_unit_price(3, tonne, tonne_price - 10) diff --git a/addons/product/tests/test_product_attribute_value_config.py b/addons/product/tests/test_product_attribute_value_config.py new file mode 100644 index 00000000..238f696b --- /dev/null +++ b/addons/product/tests/test_product_attribute_value_config.py @@ -0,0 +1,649 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import time +from psycopg2 import IntegrityError + +from odoo.exceptions import UserError, ValidationError +from odoo.tests import tagged +from odoo.tests.common import SavepointCase +from odoo.tools import mute_logger + + +class TestProductAttributeValueCommon(SavepointCase): + @classmethod + def setUpClass(cls): + super(TestProductAttributeValueCommon, cls).setUpClass() + + cls.computer = cls.env['product.template'].create({ + 'name': 'Super Computer', + 'price': 2000, + }) + + cls._add_ssd_attribute() + cls._add_ram_attribute() + cls._add_hdd_attribute() + + cls.computer_case = cls.env['product.template'].create({ + 'name': 'Super Computer Case' + }) + + cls._add_size_attribute() + + @classmethod + def _add_ssd_attribute(cls): + cls.ssd_attribute = cls.env['product.attribute'].create({'name': 'Memory', 'sequence': 1}) + cls.ssd_256 = cls.env['product.attribute.value'].create({ + 'name': '256 GB', + 'attribute_id': cls.ssd_attribute.id, + 'sequence': 1, + }) + cls.ssd_512 = cls.env['product.attribute.value'].create({ + 'name': '512 GB', + 'attribute_id': cls.ssd_attribute.id, + 'sequence': 2, + }) + + cls._add_ssd_attribute_line() + + @classmethod + def _add_ssd_attribute_line(cls): + cls.computer_ssd_attribute_lines = cls.env['product.template.attribute.line'].create({ + 'product_tmpl_id': cls.computer.id, + 'attribute_id': cls.ssd_attribute.id, + 'value_ids': [(6, 0, [cls.ssd_256.id, cls.ssd_512.id])], + }) + cls.computer_ssd_attribute_lines.product_template_value_ids[0].price_extra = 200 + cls.computer_ssd_attribute_lines.product_template_value_ids[1].price_extra = 400 + + @classmethod + def _add_ram_attribute(cls): + cls.ram_attribute = cls.env['product.attribute'].create({'name': 'RAM', 'sequence': 2}) + cls.ram_8 = cls.env['product.attribute.value'].create({ + 'name': '8 GB', + 'attribute_id': cls.ram_attribute.id, + 'sequence': 1, + }) + cls.ram_16 = cls.env['product.attribute.value'].create({ + 'name': '16 GB', + 'attribute_id': cls.ram_attribute.id, + 'sequence': 2, + }) + cls.ram_32 = cls.env['product.attribute.value'].create({ + 'name': '32 GB', + 'attribute_id': cls.ram_attribute.id, + 'sequence': 3, + }) + cls.computer_ram_attribute_lines = cls.env['product.template.attribute.line'].create({ + 'product_tmpl_id': cls.computer.id, + 'attribute_id': cls.ram_attribute.id, + 'value_ids': [(6, 0, [cls.ram_8.id, cls.ram_16.id, cls.ram_32.id])], + }) + cls.computer_ram_attribute_lines.product_template_value_ids[0].price_extra = 20 + cls.computer_ram_attribute_lines.product_template_value_ids[1].price_extra = 40 + cls.computer_ram_attribute_lines.product_template_value_ids[2].price_extra = 80 + + @classmethod + def _add_hdd_attribute(cls): + cls.hdd_attribute = cls.env['product.attribute'].create({'name': 'HDD', 'sequence': 3}) + cls.hdd_1 = cls.env['product.attribute.value'].create({ + 'name': '1 To', + 'attribute_id': cls.hdd_attribute.id, + 'sequence': 1, + }) + cls.hdd_2 = cls.env['product.attribute.value'].create({ + 'name': '2 To', + 'attribute_id': cls.hdd_attribute.id, + 'sequence': 2, + }) + cls.hdd_4 = cls.env['product.attribute.value'].create({ + 'name': '4 To', + 'attribute_id': cls.hdd_attribute.id, + 'sequence': 3, + }) + + cls._add_hdd_attribute_line() + + @classmethod + def _add_hdd_attribute_line(cls): + cls.computer_hdd_attribute_lines = cls.env['product.template.attribute.line'].create({ + 'product_tmpl_id': cls.computer.id, + 'attribute_id': cls.hdd_attribute.id, + 'value_ids': [(6, 0, [cls.hdd_1.id, cls.hdd_2.id, cls.hdd_4.id])], + }) + cls.computer_hdd_attribute_lines.product_template_value_ids[0].price_extra = 2 + cls.computer_hdd_attribute_lines.product_template_value_ids[1].price_extra = 4 + cls.computer_hdd_attribute_lines.product_template_value_ids[2].price_extra = 8 + + def _add_ram_exclude_for(self): + self._get_product_value_id(self.computer_ram_attribute_lines, self.ram_16).update({ + 'exclude_for': [(0, 0, { + 'product_tmpl_id': self.computer.id, + 'value_ids': [(6, 0, [self._get_product_value_id(self.computer_hdd_attribute_lines, self.hdd_1).id])] + })] + }) + + @classmethod + def _add_size_attribute(cls): + cls.size_attribute = cls.env['product.attribute'].create({'name': 'Size', 'sequence': 4}) + cls.size_m = cls.env['product.attribute.value'].create({ + 'name': 'M', + 'attribute_id': cls.size_attribute.id, + 'sequence': 1, + }) + cls.size_l = cls.env['product.attribute.value'].create({ + 'name': 'L', + 'attribute_id': cls.size_attribute.id, + 'sequence': 2, + }) + cls.size_xl = cls.env['product.attribute.value'].create({ + 'name': 'XL', + 'attribute_id': cls.size_attribute.id, + 'sequence': 3, + }) + cls.computer_case_size_attribute_lines = cls.env['product.template.attribute.line'].create({ + 'product_tmpl_id': cls.computer_case.id, + 'attribute_id': cls.size_attribute.id, + 'value_ids': [(6, 0, [cls.size_m.id, cls.size_l.id, cls.size_xl.id])], + }) + + def _get_product_value_id(self, product_template_attribute_lines, product_attribute_value): + return product_template_attribute_lines.product_template_value_ids.filtered( + lambda product_value_id: product_value_id.product_attribute_value_id == product_attribute_value)[0] + + def _get_product_template_attribute_value(self, product_attribute_value, model=False): + """ + Return the `product.template.attribute.value` matching + `product_attribute_value` for self. + + :param: recordset of one product.attribute.value + :return: recordset of one product.template.attribute.value if found + else empty + """ + if not model: + model = self.computer + return model.valid_product_template_attribute_line_ids.filtered( + lambda l: l.attribute_id == product_attribute_value.attribute_id + ).product_template_value_ids.filtered( + lambda v: v.product_attribute_value_id == product_attribute_value + ) + + def _add_exclude(self, m1, m2, product_template=False): + m1.update({ + 'exclude_for': [(0, 0, { + 'product_tmpl_id': (product_template or self.computer).id, + 'value_ids': [(6, 0, [m2.id])] + })] + }) + + +@tagged('post_install', '-at_install') +class TestProductAttributeValueConfig(TestProductAttributeValueCommon): + + def test_product_template_attribute_values_creation(self): + self.assertEqual(len(self.computer_ssd_attribute_lines.product_template_value_ids), 2, + 'Product attribute values (ssd) were not automatically created') + self.assertEqual(len(self.computer_ram_attribute_lines.product_template_value_ids), 3, + 'Product attribute values (ram) were not automatically created') + self.assertEqual(len(self.computer_hdd_attribute_lines.product_template_value_ids), 3, + 'Product attribute values (hdd) were not automatically created') + self.assertEqual(len(self.computer_case_size_attribute_lines.product_template_value_ids), 3, + 'Product attribute values (size) were not automatically created') + + def test_get_variant_for_combination(self): + computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256) + computer_ram_8 = self._get_product_template_attribute_value(self.ram_8) + computer_ram_16 = self._get_product_template_attribute_value(self.ram_16) + computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1) + + # completely defined variant + combination = computer_ssd_256 + computer_ram_8 + computer_hdd_1 + ok_variant = self.computer._get_variant_for_combination(combination) + self.assertEqual(ok_variant.product_template_attribute_value_ids, combination) + + # over defined variant + combination = computer_ssd_256 + computer_ram_8 + computer_ram_16 + computer_hdd_1 + variant = self.computer._get_variant_for_combination(combination) + self.assertEqual(len(variant), 0) + + # under defined variant + combination = computer_ssd_256 + computer_ram_8 + variant = self.computer._get_variant_for_combination(combination) + self.assertFalse(variant) + + def test_product_filtered_exclude_for(self): + """ + Super Computer has 18 variants total (2 ssd * 3 ram * 3 hdd) + RAM 16 excudes HDD 1, that matches 2 variants: + - SSD 256 RAM 16 HDD 1 + - SSD 512 RAM 16 HDD 1 + + => There has to be 16 variants left when filtered + """ + computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256) + computer_ssd_512 = self._get_product_template_attribute_value(self.ssd_512) + computer_ram_8 = self._get_product_template_attribute_value(self.ram_8) + computer_ram_16 = self._get_product_template_attribute_value(self.ram_16) + computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1) + + self.assertEqual(len(self.computer._get_possible_variants()), 18) + self._add_ram_exclude_for() + self.assertEqual(len(self.computer._get_possible_variants()), 16) + self.assertTrue(self.computer._get_variant_for_combination(computer_ssd_256 + computer_ram_8 + computer_hdd_1)._is_variant_possible()) + self.assertFalse(self.computer._get_variant_for_combination(computer_ssd_256 + computer_ram_16 + computer_hdd_1)._is_variant_possible()) + self.assertFalse(self.computer._get_variant_for_combination(computer_ssd_512 + computer_ram_16 + computer_hdd_1)._is_variant_possible()) + + def test_children_product_filtered_exclude_for(self): + """ + Super Computer Case has 3 variants total (3 size) + Reference product Computer with HDD 4 excludes Size M + The following variant will be excluded: + - Size M + + => There has to be 2 variants left when filtered + """ + computer_hdd_4 = self._get_product_template_attribute_value(self.hdd_4) + computer_size_m = self._get_product_template_attribute_value(self.size_m, self.computer_case) + self._add_exclude(computer_hdd_4, computer_size_m, self.computer_case) + self.assertEqual(len(self.computer_case._get_possible_variants(computer_hdd_4)), 2) + self.assertFalse(self.computer_case._get_variant_for_combination(computer_size_m)._is_variant_possible(computer_hdd_4)) + + def test_is_combination_possible(self): + computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256) + computer_ram_8 = self._get_product_template_attribute_value(self.ram_8) + computer_ram_16 = self._get_product_template_attribute_value(self.ram_16) + computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1) + self._add_exclude(computer_ram_16, computer_hdd_1) + + # CASE: basic + self.assertTrue(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_1)) + + # CASE: ram 16 excluding hdd1 + self.assertFalse(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_16 + computer_hdd_1)) + + # CASE: under defined combination + self.assertFalse(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_16)) + + # CASE: no combination, no variant, just return the only variant + mouse = self.env['product.template'].create({'name': 'Mouse'}) + self.assertTrue(mouse._is_combination_possible(self.env['product.template.attribute.value'])) + + # prep work for the last part of the test + color_attribute = self.env['product.attribute'].create({'name': 'Color'}) + color_red = self.env['product.attribute.value'].create({ + 'name': 'Red', + 'attribute_id': color_attribute.id, + }) + color_green = self.env['product.attribute.value'].create({ + 'name': 'Green', + 'attribute_id': color_attribute.id, + }) + self.env['product.template.attribute.line'].create({ + 'product_tmpl_id': mouse.id, + 'attribute_id': color_attribute.id, + 'value_ids': [(6, 0, [color_red.id, color_green.id])], + }) + + mouse_color_red = self._get_product_template_attribute_value(color_red, mouse) + mouse_color_green = self._get_product_template_attribute_value(color_green, mouse) + + self._add_exclude(computer_ssd_256, mouse_color_green, mouse) + + variant = self.computer._get_variant_for_combination(computer_ssd_256 + computer_ram_8 + computer_hdd_1) + + # CASE: wrong attributes (mouse_color_red not on computer) + self.assertFalse(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_16 + mouse_color_red)) + + # CASE: parent ok + self.assertTrue(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_1, mouse_color_red)) + self.assertTrue(mouse._is_combination_possible(mouse_color_red, computer_ssd_256 + computer_ram_8 + computer_hdd_1)) + + # CASE: parent exclusion but good direction (parent is directional) + self.assertTrue(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_1, mouse_color_green)) + + # CASE: parent exclusion and wrong direction (parent is directional) + self.assertFalse(mouse._is_combination_possible(mouse_color_green, computer_ssd_256 + computer_ram_8 + computer_hdd_1)) + + # CASE: deleted combination + variant.unlink() + self.assertFalse(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_1)) + + # CASE: if multiple variants exist for the same combination and at least + # one of them is not archived, the combination is possible + combination = computer_ssd_256 + computer_ram_8 + computer_hdd_1 + self.env['product.product'].create({ + 'product_tmpl_id': self.computer.id, + 'product_template_attribute_value_ids': [(6, 0, combination.ids)], + 'active': False, + }) + self.env['product.product'].create({ + 'product_tmpl_id': self.computer.id, + 'product_template_attribute_value_ids': [(6, 0, combination.ids)], + 'active': True, + }) + self.assertTrue(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_1)) + + def test_get_first_possible_combination(self): + computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256) + computer_ssd_512 = self._get_product_template_attribute_value(self.ssd_512) + computer_ram_8 = self._get_product_template_attribute_value(self.ram_8) + computer_ram_16 = self._get_product_template_attribute_value(self.ram_16) + computer_ram_32 = self._get_product_template_attribute_value(self.ram_32) + computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1) + computer_hdd_2 = self._get_product_template_attribute_value(self.hdd_2) + computer_hdd_4 = self._get_product_template_attribute_value(self.hdd_4) + self._add_exclude(computer_ram_16, computer_hdd_1) + + # Basic case: test all iterations of generator + gen = self.computer._get_possible_combinations() + self.assertEqual(next(gen), computer_ssd_256 + computer_ram_8 + computer_hdd_1) + self.assertEqual(next(gen), computer_ssd_256 + computer_ram_8 + computer_hdd_2) + self.assertEqual(next(gen), computer_ssd_256 + computer_ram_8 + computer_hdd_4) + self.assertEqual(next(gen), computer_ssd_256 + computer_ram_16 + computer_hdd_2) + self.assertEqual(next(gen), computer_ssd_256 + computer_ram_16 + computer_hdd_4) + self.assertEqual(next(gen), computer_ssd_256 + computer_ram_32 + computer_hdd_1) + self.assertEqual(next(gen), computer_ssd_256 + computer_ram_32 + computer_hdd_2) + self.assertEqual(next(gen), computer_ssd_256 + computer_ram_32 + computer_hdd_4) + self.assertEqual(next(gen), computer_ssd_512 + computer_ram_8 + computer_hdd_1) + self.assertEqual(next(gen), computer_ssd_512 + computer_ram_8 + computer_hdd_2) + self.assertEqual(next(gen), computer_ssd_512 + computer_ram_8 + computer_hdd_4) + self.assertEqual(next(gen), computer_ssd_512 + computer_ram_16 + computer_hdd_2) + self.assertEqual(next(gen), computer_ssd_512 + computer_ram_16 + computer_hdd_4) + self.assertEqual(next(gen), computer_ssd_512 + computer_ram_32 + computer_hdd_1) + self.assertEqual(next(gen), computer_ssd_512 + computer_ram_32 + computer_hdd_2) + self.assertEqual(next(gen), computer_ssd_512 + computer_ram_32 + computer_hdd_4) + self.assertIsNone(next(gen, None)) + + # Give priority to ram_16 but it is not allowed by hdd_1 so it should return hhd_2 instead + # Test invalidate_cache on product.attribute.value write + computer_ram_16.product_attribute_value_id.sequence = -1 + self.assertEqual(self.computer._get_first_possible_combination(), computer_ssd_256 + computer_ram_16 + computer_hdd_2) + + # Move down the ram, so it will try to change the ram instead of the hdd + # Test invalidate_cache on product.attribute write + self.ram_attribute.sequence = 10 + self.assertEqual(self.computer._get_first_possible_combination(), computer_ssd_256 + computer_ram_8 + computer_hdd_1) + + # Give priority to ram_32 and is allowed with the rest so it should return it + self.ram_attribute.sequence = 2 + computer_ram_16.product_attribute_value_id.sequence = 2 + computer_ram_32.product_attribute_value_id.sequence = -1 + self.assertEqual(self.computer._get_first_possible_combination(), computer_ssd_256 + computer_ram_32 + computer_hdd_1) + + # Give priority to ram_16 but now it is not allowing any hdd so it should return ram_8 instead + computer_ram_32.product_attribute_value_id.sequence = 3 + computer_ram_16.product_attribute_value_id.sequence = -1 + self._add_exclude(computer_ram_16, computer_hdd_2) + self._add_exclude(computer_ram_16, computer_hdd_4) + self.assertEqual(self.computer._get_first_possible_combination(), computer_ssd_256 + computer_ram_8 + computer_hdd_1) + + # Only the last combination is possible + computer_ram_16.product_attribute_value_id.sequence = 2 + self._add_exclude(computer_ram_8, computer_hdd_1) + self._add_exclude(computer_ram_8, computer_hdd_2) + self._add_exclude(computer_ram_8, computer_hdd_4) + self._add_exclude(computer_ram_32, computer_hdd_1) + self._add_exclude(computer_ram_32, computer_hdd_2) + self._add_exclude(computer_ram_32, computer_ssd_256) + self.assertEqual(self.computer._get_first_possible_combination(), computer_ssd_512 + computer_ram_32 + computer_hdd_4) + + # No possible combination (test helper and iterator) + self._add_exclude(computer_ram_32, computer_hdd_4) + self.assertEqual(self.computer._get_first_possible_combination(), self.env['product.template.attribute.value']) + gen = self.computer._get_possible_combinations() + self.assertIsNone(next(gen, None)) + + # Testing parent case + mouse = self.env['product.template'].create({'name': 'Mouse'}) + self.assertTrue(mouse._is_combination_possible(self.env['product.template.attribute.value'])) + + # prep work for the last part of the test + color_attribute = self.env['product.attribute'].create({'name': 'Color'}) + color_red = self.env['product.attribute.value'].create({ + 'name': 'Red', + 'attribute_id': color_attribute.id, + }) + color_green = self.env['product.attribute.value'].create({ + 'name': 'Green', + 'attribute_id': color_attribute.id, + }) + self.env['product.template.attribute.line'].create({ + 'product_tmpl_id': mouse.id, + 'attribute_id': color_attribute.id, + 'value_ids': [(6, 0, [color_red.id, color_green.id])], + }) + + mouse_color_red = self._get_product_template_attribute_value(color_red, mouse) + mouse_color_green = self._get_product_template_attribute_value(color_green, mouse) + + self._add_exclude(computer_ssd_256, mouse_color_red, mouse) + self.assertEqual(mouse._get_first_possible_combination(parent_combination=computer_ssd_256 + computer_ram_8 + computer_hdd_1), mouse_color_green) + + # Test to see if several attribute_line for same attribute is well handled + color_blue = self.env['product.attribute.value'].create({ + 'name': 'Blue', + 'attribute_id': color_attribute.id, + }) + color_yellow = self.env['product.attribute.value'].create({ + 'name': 'Yellow', + 'attribute_id': color_attribute.id, + }) + self.env['product.template.attribute.line'].create({ + 'product_tmpl_id': mouse.id, + 'attribute_id': color_attribute.id, + 'value_ids': [(6, 0, [color_blue.id, color_yellow.id])], + }) + mouse_color_yellow = self._get_product_template_attribute_value(color_yellow, mouse) + self.assertEqual(mouse._get_first_possible_combination(necessary_values=mouse_color_yellow), mouse_color_red + mouse_color_yellow) + + # Making sure it's not extremely slow (has to discard invalid combinations early !) + product_template = self.env['product.template'].create({ + 'name': 'many combinations', + }) + + for i in range(10): + # create the attributes + product_attribute = self.env['product.attribute'].create({ + 'name': "att %s" % i, + 'create_variant': 'dynamic', + 'sequence': i, + }) + + for j in range(50): + # create the attribute values + value = self.env['product.attribute.value'].create([{ + 'name': "val %s" % j, + 'attribute_id': product_attribute.id, + 'sequence': j, + }]) + + # set attribute and attribute values on the template + self.env['product.template.attribute.line'].create([{ + 'attribute_id': product_attribute.id, + 'product_tmpl_id': product_template.id, + 'value_ids': [(6, 0, product_attribute.value_ids.ids)] + }]) + + self._add_exclude( + self._get_product_template_attribute_value(product_template.attribute_line_ids[1].value_ids[0], + model=product_template), + self._get_product_template_attribute_value(product_template.attribute_line_ids[0].value_ids[0], + model=product_template), + product_template) + self._add_exclude( + self._get_product_template_attribute_value(product_template.attribute_line_ids[0].value_ids[0], + model=product_template), + self._get_product_template_attribute_value(product_template.attribute_line_ids[1].value_ids[1], + model=product_template), + product_template) + + combination = self.env['product.template.attribute.value'] + for idx, ptal in enumerate(product_template.attribute_line_ids): + if idx != 1: + value = ptal.product_template_value_ids[0] + else: + value = ptal.product_template_value_ids[2] + combination += value + + started_at = time.time() + self.assertEqual(product_template._get_first_possible_combination(), combination) + elapsed = time.time() - started_at + # It should be about instantaneous, 0.5 to avoid false positives + self.assertLess(elapsed, 0.5) + + + + + def test_get_closest_possible_combinations(self): + computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256) + computer_ssd_512 = self._get_product_template_attribute_value(self.ssd_512) + computer_ram_8 = self._get_product_template_attribute_value(self.ram_8) + computer_ram_16 = self._get_product_template_attribute_value(self.ram_16) + computer_ram_32 = self._get_product_template_attribute_value(self.ram_32) + computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1) + computer_hdd_2 = self._get_product_template_attribute_value(self.hdd_2) + computer_hdd_4 = self._get_product_template_attribute_value(self.hdd_4) + self._add_exclude(computer_ram_16, computer_hdd_1) + + # CASE nothing special (test 2 iterations) + gen = self.computer._get_closest_possible_combinations(None) + self.assertEqual(next(gen), computer_ssd_256 + computer_ram_8 + computer_hdd_1) + self.assertEqual(next(gen), computer_ssd_256 + computer_ram_8 + computer_hdd_2) + + # CASE contains computer_hdd_1 (test all iterations) + gen = self.computer._get_closest_possible_combinations(computer_hdd_1) + self.assertEqual(next(gen), computer_ssd_256 + computer_ram_8 + computer_hdd_1) + self.assertEqual(next(gen), computer_ssd_256 + computer_ram_32 + computer_hdd_1) + self.assertEqual(next(gen), computer_ssd_512 + computer_ram_8 + computer_hdd_1) + self.assertEqual(next(gen), computer_ssd_512 + computer_ram_32 + computer_hdd_1) + self.assertIsNone(next(gen, None)) + + # CASE contains computer_hdd_2 + self.assertEqual(self.computer._get_closest_possible_combination(computer_hdd_2), + computer_ssd_256 + computer_ram_8 + computer_hdd_2) + + # CASE contains computer_hdd_2, computer_ram_16 + self.assertEqual(self.computer._get_closest_possible_combination(computer_hdd_2 + computer_ram_16), + computer_ssd_256 + computer_ram_16 + computer_hdd_2) + + # CASE invalid combination (excluded): + self.assertEqual(self.computer._get_closest_possible_combination(computer_hdd_1 + computer_ram_16), + computer_ssd_256 + computer_ram_8 + computer_hdd_1) + + # CASE invalid combination (too much): + self.assertEqual(self.computer._get_closest_possible_combination(computer_ssd_256 + computer_ram_8 + computer_hdd_4 + computer_hdd_2), + computer_ssd_256 + computer_ram_8 + computer_hdd_4) + + # Make sure this is not extremely slow: + product_template = self.env['product.template'].create({ + 'name': 'many combinations', + }) + + for i in range(10): + # create the attributes + product_attribute = self.env['product.attribute'].create({ + 'name': "att %s" % i, + 'create_variant': 'dynamic', + 'sequence': i, + }) + + for j in range(10): + # create the attribute values + self.env['product.attribute.value'].create([{ + 'name': "val %s/%s" % (i, j), + 'attribute_id': product_attribute.id, + 'sequence': j, + }]) + + # set attribute and attribute values on the template + self.env['product.template.attribute.line'].create([{ + 'attribute_id': product_attribute.id, + 'product_tmpl_id': product_template.id, + 'value_ids': [(6, 0, product_attribute.value_ids.ids)] + }]) + + # Get a value in the middle for each attribute to make sure it would + # take time to reach it (if looping one by one like before the fix). + combination = self.env['product.template.attribute.value'] + for ptal in product_template.attribute_line_ids: + combination += ptal.product_template_value_ids[5] + + started_at = time.time() + self.assertEqual(product_template._get_closest_possible_combination(combination), combination) + elapsed = time.time() - started_at + # It should take around 10ms, but to avoid false positives we check an + # higher value. Before the fix it would take hours. + self.assertLess(elapsed, 0.5) + + def test_clear_caches(self): + """The goal of this test is to make sure the cache is invalidated when + it should be.""" + computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256) + computer_ram_8 = self._get_product_template_attribute_value(self.ram_8) + computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1) + combination = computer_ssd_256 + computer_ram_8 + computer_hdd_1 + + # CASE: initial result of _get_variant_for_combination + variant = self.computer._get_variant_for_combination(combination) + self.assertTrue(variant) + + # CASE: clear_caches in product.product unlink + variant.unlink() + self.assertFalse(self.computer._get_variant_for_combination(combination)) + + # CASE: clear_caches in product.product create + variant = self.env['product.product'].create({ + 'product_tmpl_id': self.computer.id, + 'product_template_attribute_value_ids': [(6, 0, combination.ids)], + }) + self.assertEqual(variant, self.computer._get_variant_for_combination(combination)) + + # CASE: clear_caches in product.product write + variant.product_template_attribute_value_ids = False + self.assertFalse(self.computer._get_variant_id_for_combination(combination)) + + def test_constraints(self): + """The goal of this test is to make sure constraints are correct.""" + with self.assertRaises(UserError, msg="can't change variants creation mode of attribute used on product"): + self.ram_attribute.create_variant = 'no_variant' + + with self.assertRaises(UserError, msg="can't delete attribute used on product"): + self.ram_attribute.unlink() + + with self.assertRaises(UserError, msg="can't change the attribute of an value used on product"): + self.ram_32.attribute_id = self.hdd_attribute.id + + with self.assertRaises(UserError, msg="can't delete value used on product"): + self.ram_32.unlink() + + with self.assertRaises(ValidationError, msg="can't have attribute without value on product"): + self.env['product.template.attribute.line'].create({ + 'product_tmpl_id': self.computer_case.id, + 'attribute_id': self.hdd_attribute.id, + 'value_ids': [(6, 0, [])], + }) + + with self.assertRaises(ValidationError, msg="value attribute must match line attribute"): + self.env['product.template.attribute.line'].create({ + 'product_tmpl_id': self.computer_case.id, + 'attribute_id': self.ram_attribute.id, + 'value_ids': [(6, 0, [self.ssd_256.id])], + }) + + with self.assertRaises(UserError, msg="can't change the attribute of an attribute line"): + self.computer_ssd_attribute_lines.attribute_id = self.hdd_attribute.id + + with self.assertRaises(UserError, msg="can't change the product of an attribute line"): + self.computer_ssd_attribute_lines.product_tmpl_id = self.computer_case.id + + with self.assertRaises(UserError, msg="can't change the value of a product template attribute value"): + self.computer_ram_attribute_lines.product_template_value_ids[0].product_attribute_value_id = self.hdd_1 + + with self.assertRaises(UserError, msg="can't change the product of a product template attribute value"): + self.computer_ram_attribute_lines.product_template_value_ids[0].product_tmpl_id = self.computer_case.id + + with mute_logger('odoo.sql_db'), self.assertRaises(IntegrityError, msg="can't have two values with the same name for the same attribute"): + self.env['product.attribute.value'].create({ + 'name': '32 GB', + 'attribute_id': self.ram_attribute.id, + }) diff --git a/addons/product/tests/test_product_pricelist.py b/addons/product/tests/test_product_pricelist.py new file mode 100644 index 00000000..0cb23565 --- /dev/null +++ b/addons/product/tests/test_product_pricelist.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime + +from odoo.tests.common import TransactionCase +from odoo.tools import float_compare, test_reports + + +class TestProductPricelist(TransactionCase): + + def setUp(self): + super(TestProductPricelist, self).setUp() + self.ProductPricelist = self.env['product.pricelist'] + self.res_partner_4 = self.env['res.partner'].create({'name': 'Ready Mat'}) + self.res_partner_1 = self.env['res.partner'].create({'name': 'Wood Corner'}) + self.category_5_id = self.env['product.category'].create({ + 'name': 'Office Furniture', + 'parent_id': self.env.ref('product.product_category_1').id + }).id + self.computer_SC234 = self.env['product.product'].create({ + 'name': 'Desk Combination', + 'categ_id': self.category_5_id, + }) + self.ipad_retina_display = self.env['product.product'].create({ + 'name': 'Customizable Desk', + }) + self.custom_computer_kit = self.env['product.product'].create({ + 'name': 'Corner Desk Right Sit', + 'categ_id': self.category_5_id, + }) + self.ipad_mini = self.env['product.product'].create({ + 'name': 'Large Cabinet', + 'categ_id': self.category_5_id, + 'standard_price': 800.0, + }) + self.monitor = self.env['product.product'].create({ + 'name': 'Super nice monitor', + 'categ_id': self.category_5_id, + 'list_price': 1000.0, + }) + + self.env['product.supplierinfo'].create([ + { + 'name': self.res_partner_1.id, + 'product_tmpl_id': self.ipad_mini.product_tmpl_id.id, + 'delay': 3, + 'min_qty': 1, + 'price': 750, + }, { + 'name': self.res_partner_4.id, + 'product_tmpl_id': self.ipad_mini.product_tmpl_id.id, + 'delay': 3, + 'min_qty': 1, + 'price': 790, + }, { + 'name': self.res_partner_4.id, + 'product_tmpl_id': self.ipad_mini.product_tmpl_id.id, + 'delay': 3, + 'min_qty': 3, + 'price': 785, + }, { + 'name': self.res_partner_4.id, + 'product_tmpl_id': self.monitor.product_tmpl_id.id, + 'delay': 3, + 'min_qty': 3, + 'price': 100, + } + ]) + self.apple_in_ear_headphones = self.env['product.product'].create({ + 'name': 'Storage Box', + 'categ_id': self.category_5_id, + }) + self.laptop_E5023 = self.env['product.product'].create({ + 'name': 'Office Chair', + 'categ_id': self.category_5_id, + }) + self.laptop_S3450 = self.env['product.product'].create({ + 'name': 'Acoustic Bloc Screens', + 'categ_id': self.category_5_id, + }) + + self.uom_unit_id = self.ref('uom.product_uom_unit') + self.list0 = self.ref('product.list0') + + self.ipad_retina_display.write({'uom_id': self.uom_unit_id, 'categ_id': self.category_5_id}) + self.customer_pricelist = self.ProductPricelist.create({ + 'name': 'Customer Pricelist', + 'item_ids': [(0, 0, { + 'name': 'Default pricelist', + 'compute_price': 'formula', + 'base': 'pricelist', + 'base_pricelist_id': self.list0 + }), (0, 0, { + 'name': '10% Discount on Assemble Computer', + 'applied_on': '1_product', + 'product_tmpl_id': self.ipad_retina_display.product_tmpl_id.id, + 'compute_price': 'formula', + 'base': 'list_price', + 'price_discount': 10 + }), (0, 0, { + 'name': '1 surchange on Laptop', + 'applied_on': '1_product', + 'product_tmpl_id': self.laptop_E5023.product_tmpl_id.id, + 'compute_price': 'formula', + 'base': 'list_price', + 'price_surcharge': 1 + }), (0, 0, { + 'name': '5% Discount on all Computer related products', + 'applied_on': '2_product_category', + 'min_quantity': 2, + 'compute_price': 'formula', + 'base': 'list_price', + 'categ_id': self.category_5_id, + 'price_discount': 5 + }), (0, 0, { + 'name': '30% Discount on all products', + 'applied_on': '3_global', + 'date_start': '2011-12-27', + 'date_end': '2011-12-31', + 'compute_price': 'formula', + 'price_discount': 30, + 'base': 'list_price' + }), (0, 0, { + 'name': 'Fixed on all products', + 'applied_on': '1_product', + 'product_tmpl_id': self.monitor.product_tmpl_id.id, + 'date_start': '2020-04-06 09:00:00', + 'date_end': '2020-04-09 12:00:00', + 'compute_price': 'formula', + 'price_discount': 50, + 'base': 'list_price' + })] + }) + + def test_10_calculation_price_of_products_pricelist(self): + """Test calculation of product price based on pricelist""" + # I check sale price of Customizable Desk + context = {} + context.update({'pricelist': self.customer_pricelist.id, 'quantity': 1}) + ipad_retina_display = self.ipad_retina_display.with_context(context) + msg = "Wrong sale price: Customizable Desk. should be %s instead of %s" % (ipad_retina_display.price, (ipad_retina_display.lst_price-ipad_retina_display.lst_price*(0.10))) + self.assertEqual(float_compare(ipad_retina_display.price, (ipad_retina_display.lst_price-ipad_retina_display.lst_price*(0.10)), precision_digits=2), 0, msg) + + # I check sale price of Laptop. + laptop_E5023 = self.laptop_E5023.with_context(context) + msg = "Wrong sale price: Laptop. should be %s instead of %s" % (laptop_E5023.price, (laptop_E5023.lst_price + 1)) + self.assertEqual(float_compare(laptop_E5023.price, laptop_E5023.lst_price + 1, precision_digits=2), 0, msg) + + # I check sale price of IT component. + apple_headphones = self.apple_in_ear_headphones.with_context(context) + msg = "Wrong sale price: IT component. should be %s instead of %s" % (apple_headphones.price, apple_headphones.lst_price) + self.assertEqual(float_compare(apple_headphones.price, apple_headphones.lst_price, precision_digits=2), 0, msg) + + # I check sale price of IT component if more than 3 Unit. + context.update({'quantity': 5}) + laptop_S3450 = self.laptop_S3450.with_context(context) + msg = "Wrong sale price: IT component if more than 3 Unit. should be %s instead of %s" % (laptop_S3450.price, (laptop_S3450.lst_price-laptop_S3450.lst_price*(0.05))) + self.assertEqual(float_compare(laptop_S3450.price, laptop_S3450.lst_price-laptop_S3450.lst_price*(0.05), precision_digits=2), 0, msg) + + # I check sale price of LCD Monitor. + context.update({'quantity': 1}) + ipad_mini = self.ipad_mini.with_context(context) + msg = "Wrong sale price: LCD Monitor. should be %s instead of %s" % (ipad_mini.price, ipad_mini.lst_price) + self.assertEqual(float_compare(ipad_mini.price, ipad_mini.lst_price, precision_digits=2), 0, msg) + + # I check sale price of LCD Monitor on end of year. + context.update({'quantity': 1, 'date': '2011-12-31'}) + ipad_mini = self.ipad_mini.with_context(context) + msg = "Wrong sale price: LCD Monitor on end of year. should be %s instead of %s" % (ipad_mini.price, ipad_mini.lst_price-ipad_mini.lst_price*(0.30)) + self.assertEqual(float_compare(ipad_mini.price, ipad_mini.lst_price-ipad_mini.lst_price*(0.30), precision_digits=2), 0, msg) + + # I check cost price of LCD Monitor. + context.update({'quantity': 1, 'date': False, 'partner_id': self.res_partner_4.id}) + ipad_mini = self.ipad_mini.with_context(context) + partner = self.res_partner_4.with_context(context) + msg = "Wrong cost price: LCD Monitor. should be 790 instead of %s" % ipad_mini._select_seller(partner_id=partner, quantity=1.0).price + self.assertEqual(float_compare(ipad_mini._select_seller(partner_id=partner, quantity=1.0).price, 790, precision_digits=2), 0, msg) + + # I check cost price of LCD Monitor if more than 3 Unit. + context.update({'quantity': 3}) + ipad_mini = self.ipad_mini.with_context(context) + partner = self.res_partner_4.with_context(context) + msg = "Wrong cost price: LCD Monitor if more than 3 Unit.should be 785 instead of %s" % ipad_mini._select_seller(partner_id=partner, quantity=3.0).price + self.assertEqual(float_compare(ipad_mini._select_seller(partner_id=partner, quantity=3.0).price, 785, precision_digits=2), 0, msg) + + # Check if the pricelist is applied at precise datetime + context.update({'quantity': 1, 'date': datetime.strptime('2020-04-05 08:00:00', '%Y-%m-%d %H:%M:%S')}) + monitor = self.monitor.with_context(context) + partner = self.res_partner_4.with_context(context) + msg = "Wrong cost price: LCD Monitor. should be 1000 instead of %s" % monitor._select_seller( + partner_id=partner, quantity=1.0).price + self.assertEqual( + float_compare(monitor.price, monitor.lst_price, precision_digits=2), 0, + msg) + context.update({'quantity': 1, 'date': datetime.strptime('2020-04-06 10:00:00', '%Y-%m-%d %H:%M:%S')}) + monitor = self.monitor.with_context(context) + msg = "Wrong cost price: LCD Monitor. should be 500 instead of %s" % monitor._select_seller( + partner_id=partner, quantity=1.0).price + self.assertEqual( + float_compare(monitor.price, monitor.lst_price/2, precision_digits=2), 0, + msg) + + diff --git a/addons/product/tests/test_seller.py b/addons/product/tests/test_seller.py new file mode 100644 index 00000000..e8e92afb --- /dev/null +++ b/addons/product/tests/test_seller.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests.common import TransactionCase + + +class TestSeller(TransactionCase): + + def setUp(self): + super(TestSeller, self).setUp() + self.product_service = self.env['product.product'].create({ + 'name': 'Virtual Home Staging', + }) + self.product_service.default_code = 'DEFCODE' + self.product_consu = self.env['product.product'].create({ + 'name': 'Boudin', + 'type': 'consu', + }) + self.product_consu.default_code = 'DEFCODE' + self.asustec = self.env['res.partner'].create({'name': 'Wood Corner'}) + self.camptocamp = self.env['res.partner'].create({'name': 'Azure Interior'}) + + def test_10_sellers(self): + self.product_service.write({'seller_ids': [ + (0, 0, {'name': self.asustec.id, 'product_code': 'ASUCODE'}), + (0, 0, {'name': self.camptocamp.id, 'product_code': 'C2CCODE'}), + ]}) + + default_code = self.product_service.code + self.assertEqual("DEFCODE", default_code, "Default code not used in product name") + + context_code = self.product_service\ + .with_context(partner_id=self.camptocamp.id)\ + .code + self.assertEqual('C2CCODE', context_code, "Partner's code not used in product name with context set") + + def test_20_sellers_company(self): + company_a = self.env.company + company_b = self.env['res.company'].create({ + 'name': 'Saucisson Inc.', + }) + self.product_consu.write({'seller_ids': [ + (0, 0, {'name': self.asustec.id, 'product_code': 'A', 'company_id': company_a.id}), + (0, 0, {'name': self.asustec.id, 'product_code': 'B', 'company_id': company_b.id}), + (0, 0, {'name': self.asustec.id, 'product_code': 'NO', 'company_id': False}), + ]}) + + names = self.product_consu.with_context( + partner_id=self.asustec.id, + ).name_get() + ref = set([x[1] for x in names]) + self.assertEqual(len(names), 3, "3 vendor references should have been found") + self.assertEqual(ref, {'[A] Boudin', '[B] Boudin', '[NO] Boudin'}, "Incorrect vendor reference list") + names = self.product_consu.with_context( + partner_id=self.asustec.id, + company_id=company_a.id, + ).name_get() + ref = set([x[1] for x in names]) + self.assertEqual(len(names), 2, "2 vendor references should have been found") + self.assertEqual(ref, {'[A] Boudin', '[NO] Boudin'}, "Incorrect vendor reference list") + names = self.product_consu.with_context( + partner_id=self.asustec.id, + company_id=company_b.id, + ).name_get() + ref = set([x[1] for x in names]) + self.assertEqual(len(names), 2, "2 vendor references should have been found") + self.assertEqual(ref, {'[B] Boudin', '[NO] Boudin'}, "Incorrect vendor reference list") diff --git a/addons/product/tests/test_variants.py b/addons/product/tests/test_variants.py new file mode 100644 index 00000000..e952ad1a --- /dev/null +++ b/addons/product/tests/test_variants.py @@ -0,0 +1,1181 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 +from collections import OrderedDict +import io +from PIL import Image + +from . import common +from odoo.exceptions import UserError +from odoo.tests.common import TransactionCase, Form + + +class TestVariantsSearch(TransactionCase): + + def setUp(self): + res = super(TestVariantsSearch, self).setUp() + self.size_attr = self.env['product.attribute'].create({'name': 'Size'}) + self.size_attr_value_s = self.env['product.attribute.value'].create({'name': 'S', 'attribute_id': self.size_attr.id}) + self.size_attr_value_m = self.env['product.attribute.value'].create({'name': 'M', 'attribute_id': self.size_attr.id}) + self.size_attr_value_l = self.env['product.attribute.value'].create({'name': 'L', 'attribute_id': self.size_attr.id}) + self.product_shirt_template = self.env['product.template'].create({ + 'name': 'Shirt', + 'attribute_line_ids': [(0, 0, { + 'attribute_id': self.size_attr.id, + 'value_ids': [(6, 0, [self.size_attr_value_l.id])], + })] + }) + return res + + def test_attribute_line_search(self): + search_not_to_be_found = self.env['product.template'].search( + [('attribute_line_ids', '=', 'M')] + ) + self.assertNotIn(self.product_shirt_template, search_not_to_be_found, + 'Shirt should not be found searching M') + + search_attribute = self.env['product.template'].search( + [('attribute_line_ids', '=', 'Size')] + ) + self.assertIn(self.product_shirt_template, search_attribute, + 'Shirt should be found searching Size') + + search_value = self.env['product.template'].search( + [('attribute_line_ids', '=', 'L')] + ) + self.assertIn(self.product_shirt_template, search_value, + 'Shirt should be found searching L') + + def test_name_search(self): + self.product_slip_template = self.env['product.template'].create({ + 'name': 'Slip', + }) + res = self.env['product.product'].name_search('Shirt', [], 'not ilike', None) + res_ids = [r[0] for r in res] + self.assertIn(self.product_slip_template.product_variant_ids.id, res_ids, + 'Slip should be found searching \'not ilike\'') + + +class TestVariants(common.TestProductCommon): + + def setUp(self): + res = super(TestVariants, self).setUp() + self.size_attr = self.env['product.attribute'].create({'name': 'Size'}) + self.size_attr_value_s = self.env['product.attribute.value'].create({'name': 'S', 'attribute_id': self.size_attr.id}) + self.size_attr_value_m = self.env['product.attribute.value'].create({'name': 'M', 'attribute_id': self.size_attr.id}) + self.size_attr_value_l = self.env['product.attribute.value'].create({'name': 'L', 'attribute_id': self.size_attr.id}) + return res + + def test_variants_is_product_variant(self): + template = self.product_7_template + variants = template.product_variant_ids + self.assertFalse(template.is_product_variant, + 'Product template is not a variant') + self.assertEqual({True}, set(v.is_product_variant for v in variants), + 'Product variants are variants') + + def test_variants_creation_mono(self): + test_template = self.env['product.template'].create({ + 'name': 'Sofa', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + 'attribute_line_ids': [(0, 0, { + 'attribute_id': self.size_attr.id, + 'value_ids': [(4, self.size_attr_value_s.id)], + })] + }) + + # produced variants: one variant, because mono value + self.assertEqual(len(test_template.product_variant_ids), 1) + self.assertEqual(test_template.product_variant_ids.product_template_attribute_value_ids.product_attribute_value_id, self.size_attr_value_s) + + def test_variants_creation_mono_double(self): + test_template = self.env['product.template'].create({ + 'name': 'Sofa', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + 'attribute_line_ids': [(0, 0, { + 'attribute_id': self.prod_att_1.id, + 'value_ids': [(4, self.prod_attr1_v2.id)], + }), (0, 0, { + 'attribute_id': self.size_attr.id, + 'value_ids': [(4, self.size_attr_value_s.id)], + })] + }) + + # produced variants: one variant, because only 1 combination is possible + self.assertEqual(len(test_template.product_variant_ids), 1) + self.assertEqual(test_template.product_variant_ids.product_template_attribute_value_ids.product_attribute_value_id, self.size_attr_value_s + self.prod_attr1_v2) + + def test_variants_creation_mono_multi(self): + test_template = self.env['product.template'].create({ + 'name': 'Sofa', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + 'attribute_line_ids': [(0, 0, { + 'attribute_id': self.prod_att_1.id, + 'value_ids': [(4, self.prod_attr1_v2.id)], + }), (0, 0, { + 'attribute_id': self.size_attr.id, + 'value_ids': [(4, self.size_attr_value_s.id), (4, self.size_attr_value_m.id)], + })] + }) + sofa_attr1_v2 = test_template.attribute_line_ids[0].product_template_value_ids[0] + sofa_size_s = test_template.attribute_line_ids[1].product_template_value_ids[0] + sofa_size_m = test_template.attribute_line_ids[1].product_template_value_ids[1] + + # produced variants: two variants, simple matrix + self.assertEqual(len(test_template.product_variant_ids), 2) + for ptav in sofa_size_s + sofa_size_m: + products = self.env['product.product'].search([ + ('product_tmpl_id', '=', test_template.id), + ('product_template_attribute_value_ids', 'in', ptav.id), + ('product_template_attribute_value_ids', 'in', sofa_attr1_v2.id) + ]) + self.assertEqual(len(products), 1) + + def test_variants_creation_matrix(self): + test_template = self.env['product.template'].create({ + 'name': 'Sofa', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + 'attribute_line_ids': [(0, 0, { + 'attribute_id': self.prod_att_1.id, + 'value_ids': [(4, self.prod_attr1_v1.id), (4, self.prod_attr1_v2.id)], + }), (0, 0, { + 'attribute_id': self.size_attr.id, + 'value_ids': [(4, self.size_attr_value_s.id), (4, self.size_attr_value_m.id), (4, self.size_attr_value_l.id)], + })] + }) + + sofa_attr1_v1 = test_template.attribute_line_ids[0].product_template_value_ids[0] + sofa_attr1_v2 = test_template.attribute_line_ids[0].product_template_value_ids[1] + sofa_size_s = test_template.attribute_line_ids[1].product_template_value_ids[0] + sofa_size_m = test_template.attribute_line_ids[1].product_template_value_ids[1] + sofa_size_l = test_template.attribute_line_ids[1].product_template_value_ids[2] + + # produced variants: value matrix : 2x3 values + self.assertEqual(len(test_template.product_variant_ids), 6) + for value_1 in sofa_attr1_v1 + sofa_attr1_v2: + for value_2 in sofa_size_s + sofa_size_m + sofa_size_l: + products = self.env['product.product'].search([ + ('product_tmpl_id', '=', test_template.id), + ('product_template_attribute_value_ids', 'in', value_1.id), + ('product_template_attribute_value_ids', 'in', value_2.id) + ]) + self.assertEqual(len(products), 1) + + def test_variants_creation_multi_update(self): + test_template = self.env['product.template'].create({ + 'name': 'Sofa', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + 'attribute_line_ids': [(0, 0, { + 'attribute_id': self.prod_att_1.id, + 'value_ids': [(4, self.prod_attr1_v1.id), (4, self.prod_attr1_v2.id)], + }), (0, 0, { + 'attribute_id': self.size_attr.id, + 'value_ids': [(4, self.size_attr_value_s.id), (4, self.size_attr_value_m.id)], + })] + }) + size_attribute_line = test_template.attribute_line_ids.filtered(lambda line: line.attribute_id == self.size_attr) + test_template.write({ + 'attribute_line_ids': [(1, size_attribute_line.id, { + 'value_ids': [(4, self.size_attr_value_l.id)], + })] + }) + + def test_variants_copy(self): + template = self.env['product.template'].create({ + 'name': 'Test Copy', + 'attribute_line_ids': [(0, 0, { + 'attribute_id': self.size_attr.id, + 'value_ids': [(4, self.size_attr_value_s.id), (4, self.size_attr_value_m.id)], + })] + }) + self.assertEqual(len(template.product_variant_ids), 2) + self.assertEqual(template.name, 'Test Copy') + + # test copy of template + template_copy = template.copy() + self.assertEqual(template.name, 'Test Copy') + self.assertEqual(template_copy.name, 'Test Copy (copy)') + self.assertEqual(len(template_copy.product_variant_ids), 2) + + # test copy of variant (actually just copying template) + variant_copy = template_copy.product_variant_ids[0].copy() + self.assertEqual(template.name, 'Test Copy') + self.assertEqual(template_copy.name, 'Test Copy (copy)') + self.assertEqual(variant_copy.name, 'Test Copy (copy) (copy)') + self.assertEqual(len(variant_copy.product_variant_ids), 2) + + def test_standard_price(self): + """ Ensure template values are correctly (re)computed depending on the context """ + one_variant_product = self.product_1 + self.assertEqual(one_variant_product.product_variant_count, 1) + + company_a = self.env.company + company_b = self.env['res.company'].create({'name': 'CB', 'currency_id': self.env.ref('base.VEF').id}) + + self.assertEqual(one_variant_product.cost_currency_id, company_a.currency_id) + self.assertEqual(one_variant_product.with_company(company_b).cost_currency_id, company_b.currency_id) + + one_variant_template = one_variant_product.product_tmpl_id + self.assertEqual(one_variant_product.standard_price, one_variant_template.standard_price) + one_variant_product.with_company(company_b).standard_price = 500.0 + self.assertEqual( + one_variant_product.with_company(company_b).standard_price, + one_variant_template.with_company(company_b).standard_price + ) + self.assertEqual( + 500.0, + one_variant_template.with_company(company_b).standard_price + ) + + def test_archive_variant(self): + template = self.env['product.template'].create({ + 'name': 'template' + }) + self.assertEqual(len(template.product_variant_ids), 1) + + template.write({ + 'attribute_line_ids': [(0, False, { + 'attribute_id': self.size_attr.id, + 'value_ids': [ + (4, self.size_attr.value_ids[0].id, self.size_attr_value_s), + (4, self.size_attr.value_ids[1].id, self.size_attr_value_m) + ], + })] + }) + self.assertEqual(len(template.product_variant_ids), 2) + variant_1 = template.product_variant_ids[0] + variant_1.toggle_active() + self.assertFalse(variant_1.active) + self.assertEqual(len(template.product_variant_ids), 1) + self.assertEqual(len(template.with_context( + active_test=False).product_variant_ids), 2) + variant_1.toggle_active() + self.assertTrue(variant_1.active) + self.assertTrue(template.active) + + def test_archive_all_variants(self): + template = self.env['product.template'].create({ + 'name': 'template' + }) + self.assertEqual(len(template.product_variant_ids), 1) + + template.write({ + 'attribute_line_ids': [(0, False, { + 'attribute_id': self.size_attr.id, + 'value_ids': [ + (4, self.size_attr.value_ids[0].id, self.size_attr_value_s), + (4, self.size_attr.value_ids[1].id, self.size_attr_value_m) + ], + })] + }) + self.assertEqual(len(template.product_variant_ids), 2) + variant_1 = template.product_variant_ids[0] + variant_2 = template.product_variant_ids[1] + template.product_variant_ids.toggle_active() + self.assertFalse(variant_1.active, 'Should archive all variants') + self.assertFalse(template.active, 'Should archive related template') + variant_1.toggle_active() + self.assertTrue(variant_1.active, 'Should activate variant') + self.assertFalse(variant_2.active, 'Should not re-activate other variant') + self.assertTrue(template.active, 'Should re-activate template') + +class TestVariantsNoCreate(common.TestProductCommon): + + def setUp(self): + super(TestVariantsNoCreate, self).setUp() + self.size = self.env['product.attribute'].create({ + 'name': 'Size', + 'create_variant': 'no_variant', + 'value_ids': [(0, 0, {'name': 'S'}), (0, 0, {'name': 'M'}), (0, 0, {'name': 'L'})], + }) + self.size_S = self.size.value_ids[0] + self.size_M = self.size.value_ids[1] + self.size_L = self.size.value_ids[2] + + def test_create_mono(self): + """ create a product with a 'nocreate' attribute with a single value """ + template = self.env['product.template'].create({ + 'name': 'Sofa', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + 'attribute_line_ids': [(0, 0, { + 'attribute_id': self.size.id, + 'value_ids': [(4, self.size_S.id)], + })], + }) + self.assertEqual(len(template.product_variant_ids), 1) + self.assertFalse(template.product_variant_ids.product_template_attribute_value_ids) + + def test_update_mono(self): + """ modify a product with a 'nocreate' attribute with a single value """ + template = self.env['product.template'].create({ + 'name': 'Sofa', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + }) + self.assertEqual(len(template.product_variant_ids), 1) + + template.write({ + 'attribute_line_ids': [(0, 0, { + 'attribute_id': self.size.id, + 'value_ids': [(4, self.size_S.id)], + })], + }) + self.assertEqual(len(template.product_variant_ids), 1) + self.assertFalse(template.product_variant_ids.product_template_attribute_value_ids) + + def test_create_multi(self): + """ create a product with a 'nocreate' attribute with several values """ + template = self.env['product.template'].create({ + 'name': 'Sofa', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + 'attribute_line_ids': [(0, 0, { + 'attribute_id': self.size.id, + 'value_ids': [(6, 0, self.size.value_ids.ids)], + })], + }) + self.assertEqual(len(template.product_variant_ids), 1) + self.assertFalse(template.product_variant_ids.product_template_attribute_value_ids) + + def test_update_multi(self): + """ modify a product with a 'nocreate' attribute with several values """ + template = self.env['product.template'].create({ + 'name': 'Sofa', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + }) + self.assertEqual(len(template.product_variant_ids), 1) + + template.write({ + 'attribute_line_ids': [(0, 0, { + 'attribute_id': self.size.id, + 'value_ids': [(6, 0, self.size.value_ids.ids)], + })], + }) + self.assertEqual(len(template.product_variant_ids), 1) + self.assertFalse(template.product_variant_ids.product_template_attribute_value_ids) + + def test_create_mixed_mono(self): + """ create a product with regular and 'nocreate' attributes """ + template = self.env['product.template'].create({ + 'name': 'Sofa', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + 'attribute_line_ids': [ + (0, 0, { # no variants for this one + 'attribute_id': self.size.id, + 'value_ids': [(4, self.size_S.id)], + }), + (0, 0, { # two variants for this one + 'attribute_id': self.prod_att_1.id, + 'value_ids': [(4, self.prod_attr1_v1.id), (4, self.prod_attr1_v2.id)], + }), + ], + }) + self.assertEqual(len(template.product_variant_ids), 2) + self.assertEqual( + {variant.product_template_attribute_value_ids.product_attribute_value_id for variant in template.product_variant_ids}, + {self.prod_attr1_v1, self.prod_attr1_v2}, + ) + + def test_update_mixed_mono(self): + """ modify a product with regular and 'nocreate' attributes """ + template = self.env['product.template'].create({ + 'name': 'Sofa', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + }) + self.assertEqual(len(template.product_variant_ids), 1) + + template.write({ + 'attribute_line_ids': [ + (0, 0, { # no variants for this one + 'attribute_id': self.size.id, + 'value_ids': [(4, self.size_S.id)], + }), + (0, 0, { # two variants for this one + 'attribute_id': self.prod_att_1.id, + 'value_ids': [(4, self.prod_attr1_v1.id), (4, self.prod_attr1_v2.id)], + }), + ], + }) + self.assertEqual(len(template.product_variant_ids), 2) + self.assertEqual( + {variant.product_template_attribute_value_ids.product_attribute_value_id for variant in template.product_variant_ids}, + {self.prod_attr1_v1, self.prod_attr1_v2}, + ) + + def test_create_mixed_multi(self): + """ create a product with regular and 'nocreate' attributes """ + template = self.env['product.template'].create({ + 'name': 'Sofa', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + 'attribute_line_ids': [ + (0, 0, { # no variants for this one + 'attribute_id': self.size.id, + 'value_ids': [(6, 0, self.size.value_ids.ids)], + }), + (0, 0, { # two variants for this one + 'attribute_id': self.prod_att_1.id, + 'value_ids': [(4, self.prod_attr1_v1.id), (4, self.prod_attr1_v2.id)], + }), + ], + }) + self.assertEqual(len(template.product_variant_ids), 2) + self.assertEqual( + {variant.product_template_attribute_value_ids.product_attribute_value_id for variant in template.product_variant_ids}, + {self.prod_attr1_v1, self.prod_attr1_v2}, + ) + + def test_update_mixed_multi(self): + """ modify a product with regular and 'nocreate' attributes """ + template = self.env['product.template'].create({ + 'name': 'Sofa', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + }) + self.assertEqual(len(template.product_variant_ids), 1) + + template.write({ + 'attribute_line_ids': [ + (0, 0, { # no variants for this one + 'attribute_id': self.size.id, + 'value_ids': [(6, 0, self.size.value_ids.ids)], + }), + (0, 0, { # two variants for this one + 'attribute_id': self.prod_att_1.id, + 'value_ids': [(4, self.prod_attr1_v1.id), (4, self.prod_attr1_v2.id)], + }), + ], + }) + self.assertEqual(len(template.product_variant_ids), 2) + self.assertEqual( + {variant.product_template_attribute_value_ids.product_attribute_value_id for variant in template.product_variant_ids}, + {self.prod_attr1_v1, self.prod_attr1_v2}, + ) + + def test_update_variant_with_nocreate(self): + """ update variants with a 'nocreate' value on variant """ + template = self.env['product.template'].create({ + 'name': 'Sofax', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + 'attribute_line_ids': [ + (0, 0, { # one variant for this one + 'attribute_id': self.prod_att_1.id, + 'value_ids': [(6, 0, self.prod_attr1_v1.ids)], + }), + ], + }) + self.assertEqual(len(template.product_variant_ids), 1) + template.attribute_line_ids = [(0, 0, { + 'attribute_id': self.size.id, + 'value_ids': [(6, 0, self.size_S.ids)], + })] + self.assertEqual(len(template.product_variant_ids), 1) + # no_variant attribute should not appear on the variant + self.assertNotIn(self.size_S, template.product_variant_ids.product_template_attribute_value_ids.product_attribute_value_id) + + +class TestVariantsManyAttributes(common.TestAttributesCommon): + + def test_01_create_no_variant(self): + toto = self.env['product.template'].create({ + 'name': 'Toto', + 'attribute_line_ids': [(0, 0, { + 'attribute_id': attribute.id, + 'value_ids': [(6, 0, attribute.value_ids.ids)], + }) for attribute in self.attributes], + }) + self.assertEqual(len(toto.attribute_line_ids.mapped('attribute_id')), 10) + self.assertEqual(len(toto.attribute_line_ids.mapped('value_ids')), 100) + self.assertEqual(len(toto.product_variant_ids), 1) + + def test_02_create_dynamic(self): + self.attributes.write({'create_variant': 'dynamic'}) + toto = self.env['product.template'].create({ + 'name': 'Toto', + 'attribute_line_ids': [(0, 0, { + 'attribute_id': attribute.id, + 'value_ids': [(6, 0, attribute.value_ids.ids)], + }) for attribute in self.attributes], + }) + self.assertEqual(len(toto.attribute_line_ids.mapped('attribute_id')), 10) + self.assertEqual(len(toto.attribute_line_ids.mapped('value_ids')), 100) + self.assertEqual(len(toto.product_variant_ids), 0) + + def test_03_create_always(self): + self.attributes.write({'create_variant': 'always'}) + with self.assertRaises(UserError): + self.env['product.template'].create({ + 'name': 'Toto', + 'attribute_line_ids': [(0, 0, { + 'attribute_id': attribute.id, + 'value_ids': [(6, 0, attribute.value_ids.ids)], + }) for attribute in self.attributes], + }) + + def test_04_create_no_variant_dynamic(self): + self.attributes[:5].write({'create_variant': 'dynamic'}) + toto = self.env['product.template'].create({ + 'name': 'Toto', + 'attribute_line_ids': [(0, 0, { + 'attribute_id': attribute.id, + 'value_ids': [(6, 0, attribute.value_ids.ids)], + }) for attribute in self.attributes], + }) + self.assertEqual(len(toto.attribute_line_ids.mapped('attribute_id')), 10) + self.assertEqual(len(toto.attribute_line_ids.mapped('value_ids')), 100) + self.assertEqual(len(toto.product_variant_ids), 0) + + def test_05_create_no_variant_always(self): + self.attributes[:2].write({'create_variant': 'always'}) + toto = self.env['product.template'].create({ + 'name': 'Toto', + 'attribute_line_ids': [(0, 0, { + 'attribute_id': attribute.id, + 'value_ids': [(6, 0, attribute.value_ids.ids)], + }) for attribute in self.attributes], + }) + self.assertEqual(len(toto.attribute_line_ids.mapped('attribute_id')), 10) + self.assertEqual(len(toto.attribute_line_ids.mapped('value_ids')), 100) + self.assertEqual(len(toto.product_variant_ids), 100) + + def test_06_create_dynamic_always(self): + self.attributes[:5].write({'create_variant': 'dynamic'}) + self.attributes[5:].write({'create_variant': 'always'}) + toto = self.env['product.template'].create({ + 'name': 'Toto', + 'attribute_line_ids': [(0, 0, { + 'attribute_id': attribute.id, + 'value_ids': [(6, 0, attribute.value_ids.ids)], + }) for attribute in self.attributes], + }) + self.assertEqual(len(toto.attribute_line_ids.mapped('attribute_id')), 10) + self.assertEqual(len(toto.attribute_line_ids.mapped('value_ids')), 100) + self.assertEqual(len(toto.product_variant_ids), 0) + + def test_07_create_no_create_dynamic_always(self): + self.attributes[3:6].write({'create_variant': 'dynamic'}) + self.attributes[6:].write({'create_variant': 'always'}) + toto = self.env['product.template'].create({ + 'name': 'Toto', + 'attribute_line_ids': [(0, 0, { + 'attribute_id': attribute.id, + 'value_ids': [(6, 0, attribute.value_ids.ids)], + }) for attribute in self.attributes], + }) + self.assertEqual(len(toto.attribute_line_ids.mapped('attribute_id')), 10) + self.assertEqual(len(toto.attribute_line_ids.mapped('value_ids')), 100) + self.assertEqual(len(toto.product_variant_ids), 0) + + +class TestVariantsImages(common.TestProductCommon): + + def setUp(self): + res = super(TestVariantsImages, self).setUp() + + self.colors = OrderedDict([('none', ''), ('red', '#FF0000'), ('green', '#00FF00'), ('blue', '#0000FF')]) + self.images = {} + + product_attribute = self.env['product.attribute'].create({'name': 'Color'}) + + self.template = self.env['product.template'].create({ + 'name': 'template', + }) + + color_values = self.env['product.attribute.value'].create([{ + 'name': color, + 'attribute_id': product_attribute.id, + 'sequence': i, + } for i, color in enumerate(self.colors)]) + + ptal = self.env['product.template.attribute.line'].create({ + 'attribute_id': product_attribute.id, + 'product_tmpl_id': self.template.id, + 'value_ids': [(6, 0, color_values.ids)], + }) + + for color_value in ptal.product_template_value_ids[1:]: + f = io.BytesIO() + Image.new('RGB', (800, 500), self.colors[color_value.name]).save(f, 'PNG') + f.seek(0) + self.images.update({color_value.name: base64.b64encode(f.read())}) + + self.template._get_variant_for_combination(color_value).write({ + 'image_variant_1920': self.images[color_value.name], + }) + # the first one has no image + self.variants = self.template.product_variant_ids + + return res + + def test_variant_images(self): + """Check that on variant, the image used is the image_variant_1920 if set, + and defaults to the template image otherwise. + """ + f = io.BytesIO() + Image.new('RGB', (800, 500), '#000000').save(f, 'PNG') + f.seek(0) + image_black = base64.b64encode(f.read()) + + images = self.variants.mapped('image_1920') + self.assertEqual(len(set(images)), 4) + + variant_no_image = self.variants[0] + self.assertFalse(variant_no_image.image_1920) + self.template.image_1920 = image_black + + # the first has no image variant, all the others do + self.assertFalse(variant_no_image.image_variant_1920) + self.assertTrue(all(images[1:])) + + # template image is the same as this one, since it has no image variant + self.assertEqual(variant_no_image.image_1920, self.template.image_1920) + # having changed the template image should not have changed these + self.assertEqual(images[1:], self.variants.mapped('image_1920')[1:]) + + def test_update_images_with_archived_variants(self): + """Update images after variants have been archived""" + self.variants[1:].write({'active': False}) + self.variants[0].image_1920 = self.images['red'] + self.assertEqual(self.template.image_1920, self.images['red']) + self.assertEqual(self.variants[0].image_variant_1920, False) + self.assertEqual(self.variants[0].image_1920, self.images['red']) + + +class TestVariantsArchive(common.TestProductCommon): + """Once a variant is used on orders/invoices, etc, they can't be unlinked. + As a result, updating attributes on a product template would simply + archive the variants instead. We make sure that at each update, we have + the correct active and inactive records. + + In these tests, we use the commands sent by the JS framework to the ORM + when using the interface. + """ + def setUp(self): + res = super(TestVariantsArchive, self).setUp() + + self.pa_color = self.env['product.attribute'].create({'name': "color", 'sequence': 1}) + color_values = self.env['product.attribute.value'].create([{ + 'name': n, + 'sequence': i, + 'attribute_id': self.pa_color.id, + } for i, n in enumerate(['white', 'black'])]) + self.pav_color_white = color_values[0] + self.pav_color_black = color_values[1] + + self.pa_size = self.env['product.attribute'].create({'name': "size", 'sequence': 2}) + size_values = self.env['product.attribute.value'].create([{ + 'name': n, + 'sequence': i, + 'attribute_id': self.pa_size.id, + } for i, n in enumerate(['s', 'm'])]) + self.pav_size_s = size_values[0] + self.pav_size_m = size_values[1] + + self.template = self.env['product.template'].create({ + 'name': 'consume product', + 'attribute_line_ids': self._get_add_all_attributes_command(), + }) + self._update_color_vars(self.template.attribute_line_ids[0]) + self._update_size_vars(self.template.attribute_line_ids[1]) + return res + + def test_01_update_variant_unlink(self): + """Variants are not used anywhere, so removing an attribute line would + unlink the variants and create new ones. Nothing too fancy here. + """ + variants_2x2 = self.template.product_variant_ids + self._assert_2color_x_2size() + + # Remove the size line, corresponding variants will be removed too since + # they are used nowhere. Since we only kept color, we should have as many + # variants as it has values. + self._remove_ptal_size() + self._assert_2color_x_0size() + archived_variants = self._get_archived_variants() + self.assertFalse(archived_variants) + + # We re-add the line we just removed, so we should get new variants. + self._add_ptal_size_s_m() + self._assert_2color_x_2size() + self.assertFalse(self.template.product_variant_ids & variants_2x2) + + def test_02_update_variant_archive_1_value(self): + """We do the same operations on the template as in the previous test, + except we simulate that the variants can't be unlinked. + + It follows that variants should be archived instead, so the results + should all be different from previous test. + + In this test we have a line that has only one possible value: + this is handled differently than the case where we have more than + one value, since it does not add new variants. + """ + self._remove_ptal_size() + self._add_ptal_size_s() + + # create a patch to make as if one variant was undeletable + # (e.g. present in a field with ondelete=restrict) + Product = self.env['product.product'] + + def unlink(self): + raise Exception('just') + Product._patch_method('unlink', unlink) + + variants_2x1 = self.template.product_variant_ids + self._assert_2color_x_1size() + archived_variants = self._get_archived_variants() + self.assertFalse(archived_variants) + + # Remove the size line, which is the one with only one possible value. + # Variants should be kept, just the single value removed from them. + self._remove_ptal_size() + self.assertEqual(variants_2x1, self.template.product_variant_ids) + self._assert_2color_x_0size() + archived_variants = self._get_archived_variants() + self.assertFalse(archived_variants) + + # Add the line just removed, so it is added back to the variants. + self._add_ptal_size_s() + self.assertEqual(variants_2x1, self.template.product_variant_ids) + self._assert_2color_x_1size() + archived_variants = self._get_archived_variants() + self.assertFalse(archived_variants) + + Product._revert_method('unlink') + + def test_02_update_variant_archive_2_value(self): + """We do the same operations on the template as in the previous tests, + except we simulate that the variants can't be unlinked. + + It follows that variants should be archived instead, so the results + should all be different from previous test. + """ + Product = self.env['product.product'] + + def unlink(slef): + raise Exception('just') + Product._patch_method('unlink', unlink) + + variants_2x2 = self.template.product_variant_ids + self._assert_2color_x_2size() + archived_variants = self._get_archived_variants() + self.assertFalse(archived_variants) + + # CASE remove one attribute line (going from 2*2 to 2*1) + # Since they can't be unlinked, existing variants should be archived. + self._remove_ptal_size() + variants_2x0 = self.template.product_variant_ids + self._assert_2color_x_0size() + archived_variants = self._get_archived_variants() + self.assertEqual(archived_variants, variants_2x2) + self._assert_2color_x_2size(archived_variants) + + # Add the line just removed, so get back the previous variants. + # Since they can't be unlinked, existing variants should be archived. + self._add_ptal_size_s_m() + self.assertEqual(self.template.product_variant_ids, variants_2x2) + self._assert_2color_x_2size() + archived_variants = self._get_archived_variants() + self.assertEqual(archived_variants, variants_2x0) + self._assert_2color_x_0size(archived_variants) + + # we redo the whole remove/read to check + self._remove_ptal_size() + self.assertEqual(self.template.product_variant_ids, variants_2x0) + self._assert_2color_x_0size() + archived_variants = self._get_archived_variants() + self.assertEqual(archived_variants, variants_2x2) + self._assert_2color_x_2size(archived_variants) + + self._add_ptal_size_s_m() + self.assertEqual(self.template.product_variant_ids, variants_2x2) + self._assert_2color_x_2size() + archived_variants = self._get_archived_variants() + self.assertEqual(archived_variants, variants_2x0) + self._assert_2color_x_0size(archived_variants) + + self._remove_ptal_size() + self.assertEqual(self.template.product_variant_ids, variants_2x0) + self._assert_2color_x_0size() + archived_variants = self._get_archived_variants() + self.assertEqual(archived_variants, variants_2x2) + self._assert_2color_x_2size(archived_variants) + + # This time we only add one of the two attributes we've been removing. + # This is a single value line, so the value is simply added to existing + # variants. + self._add_ptal_size_s() + self.assertEqual(self.template.product_variant_ids, variants_2x0) + self._assert_2color_x_1size() + self.assertEqual(archived_variants, variants_2x2) + self._assert_2color_x_2size(archived_variants) + + Product._revert_method('unlink') + + def test_03_update_variant_archive_3_value(self): + self._remove_ptal_size() + self._add_ptal_size_s() + + Product = self.env['product.product'] + + def unlink(slef): + raise Exception('just') + Product._patch_method('unlink', unlink) + + self._assert_2color_x_1size() + archived_variants = self._get_archived_variants() + self.assertFalse(archived_variants) + variants_2x1 = self.template.product_variant_ids + + # CASE: remove single value line, no variant change + self._remove_ptal_size() + self.assertEqual(self.template.product_variant_ids, variants_2x1) + self._assert_2color_x_0size() + archived_variants = self._get_archived_variants() + self.assertFalse(archived_variants) + + # CASE: empty combination, this generates a new variant + self.template.write({'attribute_line_ids': [(2, self.ptal_color.id)]}) + self._assert_0color_x_0size() + archived_variants = self._get_archived_variants() + self.assertEqual(archived_variants, variants_2x1) + self._assert_2color_x_0size(archived_variants) # single value are removed + variant_0x0 = self.template.product_variant_ids + + # CASE: add single value on empty + self._add_ptal_size_s() + self.assertEqual(self.template.product_variant_ids, variant_0x0) + self._assert_0color_x_1size() + archived_variants = self._get_archived_variants() + self.assertEqual(archived_variants, variants_2x1) + self._assert_2color_x_0size(archived_variants) # single value are removed + + # CASE: empty again + self._remove_ptal_size() + self.assertEqual(self.template.product_variant_ids, variant_0x0) + self._assert_0color_x_0size() + archived_variants = self._get_archived_variants() + self.assertEqual(archived_variants, variants_2x1) + self._assert_2color_x_0size(archived_variants) # single value are removed + + # CASE: re-add everything + self.template.write({ + 'attribute_line_ids': self._get_add_all_attributes_command(), + }) + self._update_color_vars(self.template.attribute_line_ids[0]) + self._update_size_vars(self.template.attribute_line_ids[1]) + self._assert_2color_x_2size() + archived_variants = self._get_archived_variants() + self.assertEqual(archived_variants, variants_2x1 + variant_0x0) + + Product._revert_method('unlink') + + def test_04_from_to_single_values(self): + Product = self.env['product.product'] + + def unlink(slef): + raise Exception('just') + Product._patch_method('unlink', unlink) + + # CASE: remove one value, line becoming single value + variants_2x2 = self.template.product_variant_ids + self.ptal_size.write({'value_ids': [(3, self.pav_size_m.id)]}) + self._assert_2color_x_1size() + self.assertEqual(self.template.product_variant_ids, variants_2x2[0] + variants_2x2[2]) + archived_variants = self._get_archived_variants() + self._assert_2color_x_1size(archived_variants, ptav=self.ptav_size_m) + self.assertEqual(archived_variants, variants_2x2[1] + variants_2x2[3]) + + # CASE: add back the value + self.ptal_size.write({'value_ids': [(4, self.pav_size_m.id)]}) + self._assert_2color_x_2size() + self.assertEqual(self.template.product_variant_ids, variants_2x2) + archived_variants = self._get_archived_variants() + self.assertFalse(archived_variants) + + # CASE: remove one value, line becoming single value, and then remove + # the remaining value + self.ptal_size.write({'value_ids': [(3, self.pav_size_m.id)]}) + self._remove_ptal_size() + self._assert_2color_x_0size() + self.assertFalse(self.template.product_variant_ids & variants_2x2) + archived_variants = self._get_archived_variants() + self._assert_2color_x_2size(archived_variants) + self.assertEqual(archived_variants, variants_2x2) + variants_2x0 = self.template.product_variant_ids + + # CASE: add back the values + self._add_ptal_size_s_m() + self._assert_2color_x_2size() + self.assertEqual(self.template.product_variant_ids, variants_2x2) + archived_variants = self._get_archived_variants() + self._assert_2color_x_0size(archived_variants) + self.assertEqual(archived_variants, variants_2x0) + + Product._revert_method('unlink') + + def test_name_search_dynamic_attributes(self): + dynamic_attr = self.env['product.attribute'].create({ + 'name': 'Dynamic', + 'create_variant': 'dynamic', + 'value_ids': [(0, False, {'name': 'ValueDynamic'})], + }) + template = self.env['product.template'].create({ + 'name': 'cimanyd', + 'attribute_line_ids': [(0, False, { + 'attribute_id': dynamic_attr.id, + 'value_ids': [(4, dynamic_attr.value_ids[0].id, False)], + })] + }) + self.assertEqual(len(template.product_variant_ids), 0) + + name_searched = self.env['product.template'].name_search(name='cima') + self.assertIn(template.id, [ng[0] for ng in name_searched]) + + def test_uom_update_variant(self): + """ Changing the uom on the template do not behave the same + as changing on the product product.""" + units = self.env.ref('uom.product_uom_unit') + cm = self.env.ref('uom.product_uom_cm') + template = self.env['product.template'].create({ + 'name': 'kardon' + }) + + template_form = Form(template) + template_form.uom_id = cm + self.assertEqual(template_form.uom_po_id, cm) + template = template_form.save() + + variant_form = Form(template.product_variant_ids) + variant_form.uom_id = units + self.assertEqual(variant_form.uom_po_id, units) + variant = variant_form.save() + self.assertEqual(variant.uom_po_id, units) + self.assertEqual(template.uom_po_id, units) + + def test_dynamic_attributes_archiving(self): + Product = self.env['product.product'] + ProductAttribute = self.env['product.attribute'] + ProductAttributeValue = self.env['product.attribute.value'] + + # Patch unlink method to force archiving instead deleting + def unlink(self): + self.active = False + Product._patch_method('unlink', unlink) + + # Creating attributes + pa_color = ProductAttribute.create({'sequence': 1, 'name': 'color', 'create_variant': 'dynamic'}) + color_values = ProductAttributeValue.create([{ + 'name': n, + 'sequence': i, + 'attribute_id': pa_color.id, + } for i, n in enumerate(['white', 'black'])]) + pav_color_white = color_values[0] + pav_color_black = color_values[1] + + pa_size = ProductAttribute.create({'sequence': 2, 'name': 'size', 'create_variant': 'dynamic'}) + size_values = ProductAttributeValue.create([{ + 'name': n, + 'sequence': i, + 'attribute_id': pa_size.id, + } for i, n in enumerate(['s', 'm'])]) + pav_size_s = size_values[0] + pav_size_m = size_values[1] + + pa_material = ProductAttribute.create({'sequence': 3, 'name': 'material', 'create_variant': 'no_variant'}) + material_values = ProductAttributeValue.create([{ + 'name': 'Wood', + 'sequence': 1, + 'attribute_id': pa_material.id, + }]) + pav_material_wood = material_values[0] + + # Define a template with only color attribute & white value + template = self.env['product.template'].create({ + 'name': 'test product', + 'attribute_line_ids': [(0, 0, { + 'attribute_id': pa_color.id, + 'value_ids': [(6, 0, [pav_color_white.id])], + })], + }) + + # Create a variant (because of dynamic attribute) + ptav_white = self.env['product.template.attribute.value'].search([ + ('attribute_line_id', '=', template.attribute_line_ids.id), + ('product_attribute_value_id', '=', pav_color_white.id) + ]) + product_white = template._create_product_variant(ptav_white) + + # Adding a new value to an existing attribute should not archive the variant + template.write({ + 'attribute_line_ids': [(1, template.attribute_line_ids[0].id, { + 'attribute_id': pa_color.id, + 'value_ids': [(4, pav_color_black.id, False)], + })] + }) + self.assertTrue(product_white.active) + + # Removing an attribute value should archive the product using it + template.write({ + 'attribute_line_ids': [(1, template.attribute_line_ids[0].id, { + 'value_ids': [(3, pav_color_white.id, 0)], + })] + }) + self.assertFalse(product_white.active) + self.assertFalse(template._is_combination_possible_by_config( + combination=product_white.product_template_attribute_value_ids, + ignore_no_variant=True, + )) + + # Creating a product with the same attributes for testing duplicates + product_white_duplicate = Product.create({ + 'product_tmpl_id': template.id, + 'product_template_attribute_value_ids': [(6, 0, [ptav_white.id])], + 'active': False, + }) + # Reset archiving for the next assert + template.write({ + 'attribute_line_ids': [(1, template.attribute_line_ids[0].id, { + 'value_ids': [(4, pav_color_white.id, 0)], + })] + }) + self.assertTrue(product_white.active) + self.assertFalse(product_white_duplicate.active) + + # Adding a new attribute should archive the old variant + template.write({ + 'attribute_line_ids': [(0, 0, { + 'attribute_id': pa_size.id, + 'value_ids': [(6, 0, [pav_size_s.id, pav_size_m.id])], + })] + }) + self.assertFalse(product_white.active) + + # Reset archiving for the next assert + template.write({ + 'attribute_line_ids': [(3, template.attribute_line_ids[1].id, 0)] + }) + self.assertTrue(product_white.active) + + # Adding a no_variant attribute should not archive the product + template.write({ + 'attribute_line_ids': [(0, 0, { + 'attribute_id': pa_material.id, + 'value_ids': [(6, 0, [pav_material_wood.id])], + })] + }) + self.assertTrue(product_white.active) + + Product._revert_method('unlink') + + def _update_color_vars(self, ptal): + self.ptal_color = ptal + self.assertEqual(self.ptal_color.attribute_id, self.pa_color) + self.ptav_color_white = self.ptal_color.product_template_value_ids[0] + self.assertEqual(self.ptav_color_white.product_attribute_value_id, self.pav_color_white) + self.ptav_color_black = self.ptal_color.product_template_value_ids[1] + self.assertEqual(self.ptav_color_black.product_attribute_value_id, self.pav_color_black) + + def _update_size_vars(self, ptal): + self.ptal_size = ptal + self.assertEqual(self.ptal_size.attribute_id, self.pa_size) + self.ptav_size_s = self.ptal_size.product_template_value_ids[0] + self.assertEqual(self.ptav_size_s.product_attribute_value_id, self.pav_size_s) + if len(self.ptal_size.product_template_value_ids) > 1: + self.ptav_size_m = self.ptal_size.product_template_value_ids[1] + self.assertEqual(self.ptav_size_m.product_attribute_value_id, self.pav_size_m) + + def _get_add_all_attributes_command(self): + return [(0, 0, { + 'attribute_id': pa.id, + 'value_ids': [(6, 0, pa.value_ids.ids)], + }) for pa in self.pa_color + self.pa_size] + + def _get_archived_variants(self): + # Change context to also get archived values when reading them from the + # variants. + return self.env['product.product'].with_context(active_test=False).search([ + ('active', '=', False), + ('product_tmpl_id', '=', self.template.id) + ]) + + def _remove_ptal_size(self): + self.template.write({'attribute_line_ids': [(2, self.ptal_size.id)]}) + + def _add_ptal_size_s_m(self): + self.template.write({ + 'attribute_line_ids': [(0, 0, { + 'attribute_id': self.pa_size.id, + 'value_ids': [(6, 0, (self.pav_size_s + self.pav_size_m).ids)], + })], + }) + self._update_size_vars(self.template.attribute_line_ids[-1]) + + def _add_ptal_size_s(self): + self.template.write({ + 'attribute_line_ids': [(0, 0, { + 'attribute_id': self.pa_size.id, + 'value_ids': [(6, 0, self.pav_size_s.ids)], + })], + }) + self._update_size_vars(self.template.attribute_line_ids[-1]) + + def _get_combinations_names(self, combinations): + return ' | '.join([','.join(c.mapped('name')) for c in combinations]) + + def _assert_required_combinations(self, variants, required_values): + actual_values = [v.product_template_attribute_value_ids for v in variants] + self.assertEqual(set(required_values), set(actual_values), + "\nRequired: %s\nActual: %s" % (self._get_combinations_names(required_values), self._get_combinations_names(actual_values))) + + def _assert_2color_x_2size(self, variants=None): + """Assert the full matrix 2 color x 2 size""" + variants = variants or self.template.product_variant_ids + self.assertEqual(len(variants), 4) + self._assert_required_combinations(variants, required_values=[ + self.ptav_color_white + self.ptav_size_s, + self.ptav_color_white + self.ptav_size_m, + self.ptav_color_black + self.ptav_size_s, + self.ptav_color_black + self.ptav_size_m, + ]) + + def _assert_2color_x_1size(self, variants=None, ptav=None): + """Assert the matrix 2 color x 1 size""" + variants = variants or self.template.product_variant_ids + self.assertEqual(len(variants), 2) + self._assert_required_combinations(variants, required_values=[ + self.ptav_color_white + (ptav or self.ptav_size_s), + self.ptav_color_black + (ptav or self.ptav_size_s), + ]) + + def _assert_2color_x_0size(self, variants=None): + """Assert the matrix 2 color x no size""" + variants = variants or self.template.product_variant_ids + self.assertEqual(len(variants), 2) + self._assert_required_combinations(variants, required_values=[ + self.ptav_color_white, + self.ptav_color_black, + ]) + + def _assert_0color_x_1size(self, variants=None): + """Assert the matrix no color x 1 size""" + variants = variants or self.template.product_variant_ids + self.assertEqual(len(variants), 1) + self.assertEqual(variants[0].product_template_attribute_value_ids, self.ptav_size_s) + + def _assert_0color_x_0size(self, variants=None): + """Assert the matrix no color x no size""" + variants = variants or self.template.product_variant_ids + self.assertEqual(len(variants), 1) + self.assertFalse(variants[0].product_template_attribute_value_ids) |
