summaryrefslogtreecommitdiff
path: root/addons/product
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2018-11-16 19:30:20 +0100
committerChristophe Simonis <chs@odoo.com>2018-11-16 19:30:20 +0100
commit6faad524cf5e4db02855cb213277c40aee14c42a (patch)
tree383c984dc1461a03481baa9fbdce8c156ca97e2e /addons/product
parent3657dc77c2ddb4275f07d1271b93e7f25e09913a (diff)
parentbc744a1da46efda6f1eb5c341f2fa5df30c9c7ad (diff)
[MERGE] forward port branch 12.0 up to bc744a1da46
Diffstat (limited to 'addons/product')
-rw-r--r--addons/product/i18n/product.pot22
-rw-r--r--addons/product/models/product.py16
-rw-r--r--addons/product/models/product_template.py71
-rw-r--r--addons/product/report/product_pricelist.py2
-rw-r--r--addons/product/tests/common.py16
-rw-r--r--addons/product/tests/test_variants.py94
-rw-r--r--addons/product/views/product_pricelist_views.xml2
-rw-r--r--addons/product/views/product_views.xml4
8 files changed, 185 insertions, 42 deletions
diff --git a/addons/product/i18n/product.pot b/addons/product/i18n/product.pot
index b039d460360..a72f3ca5eae 100644
--- a/addons/product/i18n/product.pot
+++ b/addons/product/i18n/product.pot
@@ -16,15 +16,6 @@ msgstr ""
"Plural-Forms: \n"
#. module: product
-#: code:addons/product/models/product_template.py:469
-#, python-format
-msgid "\n"
-" The number of variants to generate is too high.\n"
-" You should either not generate variants for each combination or generate them on demand from the sales order.\n"
-" To do so, open the form view of attributes and change the mode of *Create Variants*."
-msgstr ""
-
-#. module: product
#: selection:product.pricelist.item,applied_on:0
msgid " Product Category"
msgstr ""
@@ -1221,6 +1212,11 @@ msgid "Never"
msgstr ""
#. module: product
+#: model_terms:ir.ui.view,arch_db:product.product_pricelist_item_form_view
+msgid "New Price ="
+msgstr ""
+
+#. module: product
#: model:ir.model.fields,field_description:product.field_product_product__activity_date_deadline
#: model:ir.model.fields,field_description:product.field_product_template__activity_date_deadline
msgid "Next Activity Deadline"
@@ -2012,7 +2008,13 @@ msgid "The number of products under this category (Does not consider the childre
msgstr ""
#. module: product
-#: code:addons/product/models/product_attribute.py:55
+#: code:addons/product/models/product_template.py:475
+#, python-format
+msgid "The number of variants to generate is too high. You should either not generate variants for each combination or generate them on demand from the sales order. To do so, open the form view of attributes and change the mode of *Create Variants*."
+msgstr ""
+
+#. module: product
+#: code:addons/product/models/product_attribute.py:56
#, python-format
msgid "The operation cannot be completed:\n"
"You are trying to delete an attribute value with a reference on a product variant."
diff --git a/addons/product/models/product.py b/addons/product/models/product.py
index 0806370c998..b1b2340f16d 100644
--- a/addons/product/models/product.py
+++ b/addons/product/models/product.py
@@ -604,6 +604,22 @@ class ProductProduct(models.Model):
return name
+ def _has_valid_attributes(self, valid_attributes, valid_values):
+ """ Check if a product has valid attributes. It is considered valid if:
+ - it uses ALL valid attributes
+ - it ONLY uses valid values
+ We must make sure that all attributes are used to take into account the case where
+ attributes would be added to the template.
+
+ :param valid_attributes: a recordset of product.attribute
+ :param valid_values: a recordset of product.attribute.value
+ :return: True if the attibutes and values are correct, False instead
+ """
+ self.ensure_one()
+ values = self.attribute_value_ids.filtered(lambda v: v.attribute_id.create_variant != 'no_variant')
+ attributes = values.mapped('attribute_id')
+ return attributes == valid_attributes and values <= valid_values
+
class ProductPackaging(models.Model):
_name = "product.packaging"
diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py
index 102ba1ca109..1ccf742bd9a 100644
--- a/addons/product/models/product_template.py
+++ b/addons/product/models/product_template.py
@@ -434,7 +434,6 @@ class ProductTemplate(models.Model):
@api.multi
def create_variant_ids(self):
Product = self.env["product.product"]
- AttributeValues = self.env['product.attribute.value']
variants_to_create = []
variants_to_activate = []
@@ -448,34 +447,50 @@ class ProductTemplate(models.Model):
updated_products = tmpl_id.product_variant_ids.filtered(lambda product: value_id.attribute_id not in product.mapped('attribute_value_ids.attribute_id'))
updated_products.write({'attribute_value_ids': [(4, value_id.id)]})
- # iterator of n-uple of product.attribute.value *ids*
- variant_matrix = [
- AttributeValues.browse(value_ids)
- for value_ids in itertools.product(*(line.value_ids.ids for line in tmpl_id.attribute_line_ids if line.value_ids[:1].attribute_id.create_variant != 'no_variant'))
- ]
-
- # get the value (id) sets of existing variants
- existing_variants = {frozenset(variant.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant != 'no_variant').ids) for variant in tmpl_id.product_variant_ids}
- # -> for each value set, create a recordset of values to create a
- # variant for if the value set isn't already a variant
- for value_ids in variant_matrix:
- if set(value_ids.ids) not in existing_variants and not any(value_id.attribute_id.create_variant == 'dynamic' for value_id in value_ids):
- variants_to_create.append({
- 'product_tmpl_id': tmpl_id.id,
- 'attribute_value_ids': [(6, 0, value_ids.ids)]
- })
-
- if len(variants_to_create) > 1000:
- raise UserError(_("""
- The number of variants to generate is too high.
- You should either not generate variants for each combination or generate them on demand from the sales order.
- To do so, open the form view of attributes and change the mode of *Create Variants*."""))
-
- # check product
+ # Determine which product variants need to be created based on the attribute
+ # configuration. If any attribute is set to generate variants dynamically, skip the
+ # process.
+ # Technical note: if there is no attribute, a variant is still created because
+ # 'not any([])' and 'set([]) not in set([])' are True.
+ if not any(attrib.create_variant == 'dynamic' for attrib in tmpl_id.mapped('attribute_line_ids.attribute_id')):
+ # Iterator containing all possible attribute values combination
+ # The iterator is used to avoid MemoryError in case of a huge number of combination.
+ all_variants = itertools.product(*(
+ line.value_ids.ids for line in tmpl_id.attribute_line_ids if line.value_ids[:1].attribute_id.create_variant != 'no_variant'
+ ))
+ # Set containing existing attribute values combination
+ existing_variants = {
+ frozenset(variant.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant != 'no_variant').ids)
+ for variant in tmpl_id.product_variant_ids
+ }
+ # For each possible variant, create if it doesn't exist yet.
+ for value_ids in all_variants:
+ value_ids = frozenset(value_ids)
+ if value_ids not in existing_variants:
+ variants_to_create.append({
+ 'product_tmpl_id': tmpl_id.id,
+ 'attribute_value_ids': [(6, 0, list(value_ids))],
+ })
+ if len(variants_to_create) > 1000:
+ raise UserError(_(
+ 'The number of variants to generate is too high. '
+ 'You should either not generate variants for each combination or generate them on demand from the sales order. '
+ 'To do so, open the form view of attributes and change the mode of *Create Variants*.'))
+
+ # Check existing variants if any needs to be activated or unlinked.
+ # - if the product is not active and has valid attributes and attribute values, it
+ # should be activated
+ # - if the product does not have valid attributes or attribute values, it should be
+ # deleted
+ valid_value_ids = tmpl_id.mapped('attribute_line_ids.value_ids').filtered(
+ lambda v: v.attribute_id.create_variant != 'no_variant'
+ )
+ valid_attribute_ids = valid_value_ids.mapped('attribute_id')
for product_id in tmpl_id.product_variant_ids:
- if not product_id.active and product_id.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant != 'no_variant') in variant_matrix:
- variants_to_activate.append(product_id)
- elif product_id.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant != 'no_variant') not in variant_matrix:
+ if product_id._has_valid_attributes(valid_attribute_ids, valid_value_ids):
+ if not product_id.active:
+ variants_to_activate.append(product_id)
+ else:
variants_to_unlink.append(product_id)
if variants_to_activate:
diff --git a/addons/product/report/product_pricelist.py b/addons/product/report/product_pricelist.py
index 2a94a4b2a29..e6ccde44d8e 100644
--- a/addons/product/report/product_pricelist.py
+++ b/addons/product/report/product_pricelist.py
@@ -17,7 +17,7 @@ class report_product_pricelist(models.AbstractModel):
quantities = self._get_quantity(data)
return {
'doc_ids': data.get('ids', data.get('active_ids')),
- 'doc_model': 'hr.contribution.register',
+ 'doc_model': 'product.pricelist',
'docs': products,
'data': dict(
data,
diff --git a/addons/product/tests/common.py b/addons/product/tests/common.py
index 559b8b9dcfa..50831e2a88c 100644
--- a/addons/product/tests/common.py
+++ b/addons/product/tests/common.py
@@ -101,3 +101,19 @@ class TestProductCommon(common.SavepointCase):
'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_variants.py b/addons/product/tests/test_variants.py
index 8fc890c31f1..9381868b623 100644
--- a/addons/product/tests/test_variants.py
+++ b/addons/product/tests/test_variants.py
@@ -2,6 +2,7 @@
# 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):
@@ -367,3 +368,96 @@ class TestVariantsNoCreate(common.TestProductCommon):
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)
diff --git a/addons/product/views/product_pricelist_views.xml b/addons/product/views/product_pricelist_views.xml
index 2e219583d10..9f89adb2cc7 100644
--- a/addons/product/views/product_pricelist_views.xml
+++ b/addons/product/views/product_pricelist_views.xml
@@ -58,7 +58,7 @@
<group col="6" attrs="{'invisible':[('compute_price', '!=', 'formula')]}">
<field name="base" colspan="6"/>
- <span class="o_form_label">New Price =</span>
+ <label for="" string="New Price ="/>
<div>
<span attrs="{'invisible':[('base', '!=', 'list_price')]}">Public Price - </span>
<span attrs="{'invisible':[('base', '!=', 'standard_price')]}">Cost - </span>
diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml
index cf3127ca22d..0afcfcfa727 100644
--- a/addons/product/views/product_views.xml
+++ b/addons/product/views/product_views.xml
@@ -138,8 +138,8 @@
<filter string="Services" name="services" domain="[('type','=','service')]"/>
<filter string="Products" name="consumable" domain="[('type', 'in', ['consu', 'product'])]"/>
<separator/>
- <filter string="Can be Sold" name="filter_to_sell" domain="[('sale_ok','=',1)]"/>
- <filter string="Can be Purchased" name="filter_to_purchase" domain="[('purchase_ok', '=', 1)]"/>
+ <filter string="Can be Sold" name="filter_to_sell" domain="[('sale_ok','=',True)]"/>
+ <filter string="Can be Purchased" name="filter_to_purchase" domain="[('purchase_ok', '=', True)]"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<field string="Attributes" name="attribute_line_ids" groups="product.group_product_variant"/>