summaryrefslogtreecommitdiff
path: root/addons/product/tests
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/product/tests
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/product/tests')
-rw-r--r--addons/product/tests/__init__.py9
-rw-r--r--addons/product/tests/common.py118
-rw-r--r--addons/product/tests/test_name.py24
-rw-r--r--addons/product/tests/test_pricelist.py105
-rw-r--r--addons/product/tests/test_product_attribute_value_config.py649
-rw-r--r--addons/product/tests/test_product_pricelist.py204
-rw-r--r--addons/product/tests/test_seller.py67
-rw-r--r--addons/product/tests/test_variants.py1181
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)