The new product configurator introduces the possibility to create variants dynamically. One of the main goal is to be able to create product templates with **many** attributes and values, but not generate all variants automatically. Only the necessary variants are created on-the-fly. However, it turns out that **many** dynamic attributes and values still fails. For example, create a product with 10 attributes, 10 values for each. This allows 10^10 theoretical combinations; although the variants are not created, the creation of the product template fails. The reason is that the `variant_matrix` contains the list of all combinations, which causes a `MemoryError`. An easy modification to improve the process is to keep an iterator instead of generating the list. This avoids the `MemoryError`, but it remains necessary to go through all combinations three times, which would take too much time. Therefore, some refactoring is necessary in order to keep the same logic, but avoid going through any huge list. Here are the improvements: 1. Skip the creation part if nothing will be created. If any attribute is dynamic, no variant is created. Therefore, we can perform the check upfront and skip the variant creation part if possible. 2. Stop the creation loop as soon as possible. There is an hardcoded limit of maximum 1000 variants to create. We can perform this check along with the list creation instead of checking at the end. 3. Determine if a product has valid attributes in a different way. The existing logic is safe but expensive: we check that all attribute values of a product are part of the possible combinations. This can be simplified by checking if (1) all attribute values of a variant are part of the accepted attribute values of the template and (2) a variant uses all attributes of the template. The second check is mandatory to delete existing variants when an attribute is added to the template. Thanks to these modifications, we at worst go through 1000 possible attribute values combinations. It might still be possible to optimize step 3 with a fancy SQL query which would retrieve all (in)valid products at once. Hopefully this won't be necessary and the optimized logic will be enough. opw-1902392 closes odoo/odoo#28530
464 lines
20 KiB
Python
464 lines
20 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from . import common
|
|
from odoo.exceptions import UserError
|
|
from odoo.tests.common import TransactionCase
|
|
|
|
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.attribute_value_ids, 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.attribute_value_ids, 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)],
|
|
})]
|
|
})
|
|
|
|
# produced variants: two variants, simple matrix
|
|
self.assertEqual(len(test_template.product_variant_ids), 2)
|
|
for value in self.size_attr_value_s + self.size_attr_value_m:
|
|
products = self.env['product.product'].search([
|
|
('product_tmpl_id', '=', test_template.id),
|
|
('attribute_value_ids', 'in', value.id),
|
|
('attribute_value_ids', 'in', self.prod_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)],
|
|
})]
|
|
})
|
|
|
|
# produced variants: value matrix : 2x3 values
|
|
self.assertEqual(len(test_template.product_variant_ids), 6)
|
|
for value_1 in self.prod_attr1_v1 + self.prod_attr1_v2:
|
|
for value_2 in self.size_attr_value_m + self.size_attr_value_m + self.size_attr_value_l:
|
|
products = self.env['product.product'].search([
|
|
('product_tmpl_id', '=', test_template.id),
|
|
('attribute_value_ids', 'in', value_1.id),
|
|
('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)],
|
|
})]
|
|
})
|
|
|
|
|
|
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.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.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.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.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.attribute_value_ids 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.attribute_value_ids 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.attribute_value_ids 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.attribute_value_ids 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': [(4, self.prod_attr1_v1.id)],
|
|
}),
|
|
],
|
|
})
|
|
self.assertEqual(len(template.product_variant_ids), 1)
|
|
|
|
for variant_id in template.product_variant_ids:
|
|
variant_id.attribute_value_ids += self.size_S
|
|
template.attribute_line_ids += template.attribute_line_ids.browse()
|
|
self.assertEqual(len(template.product_variant_ids), 1)
|
|
|
|
|
|
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)
|