Skip to content
This repository was archived by the owner on Apr 28, 2022. It is now read-only.

Commit 4def722

Browse files
committed
[IMP/FIX] Preventing duplicates via checks and refactored create_variant to get_variant
1 parent 3e29ee6 commit 4def722

File tree

7 files changed

+154
-71
lines changed

7 files changed

+154
-71
lines changed

product_configurator/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ Usage
1919

2020
This module is only the foundation for external configuration interfaces such as 'product_configurator_wizard' or 'website_product_configurator'.
2121

22-
By itself this module does not configure custom products but offers the basis for generating, validating, updating configurable products using configuration interfaces.
22+
By itself this module does not configure custom products but offers the basis for generating, validating, updating configurable products using configuration interfaces.

product_configurator/models/product.py

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -327,21 +327,35 @@ def get_variant_vals(self, value_ids, custom_values=None, **kwargs):
327327

328328
@api.multi
329329
def create_variant(self, value_ids, custom_values=None):
330-
""" Creates a product.variant with the attributes passed via value_ids
331-
and custom_values
330+
"""Wrapper method for backward compatibility"""
331+
# TODO: Remove in newer versions
332+
return self.create_get_variant(
333+
value_ids=value_ids, custom_values=custom_values)
334+
335+
@api.multi
336+
def create_get_variant(self, value_ids, custom_values=None):
337+
""" Creates a new product variant with the attributes passed via value_ids
338+
and custom_values or retrieves an existing one based on search result
332339
333340
:param value_ids: list of product.attribute.values ids
334341
:param custom_values: dict {product.attribute.id: custom_value}
335342
336-
:returns: product.product recordset of products matching domain
343+
:returns: new/existing product.product recordset
337344
338345
"""
339346
if custom_values is None:
340347
custom_values = {}
341348
valid = self.validate_configuration(value_ids, custom_values)
342349
if not valid:
343350
raise ValidationError(_('Invalid Configuration'))
344-
# TODO: Add all custom values to order line instead of product
351+
352+
duplicates = self.search_variant(value_ids)
353+
354+
# Only return duplicates without custom values for now:
355+
if duplicates.filtered(lambda p: not p.value_custom_ids):
356+
return duplicates[0]
357+
358+
# TODO: Handle duplicates with custom values
345359
vals = self.get_variant_vals(value_ids, custom_values)
346360
variant = self.env['product.product'].create(vals)
347361

@@ -383,7 +397,8 @@ def values_available(self, attr_val_ids, sel_val_ids):
383397
are available for selection given the configuration ids and the
384398
dependencies set on the product template
385399
386-
:param attr_val_ids: list of attribute values
400+
:param attr_val_ids: list of attribute value ids to check for
401+
availability
387402
:param sel_val_ids: list of attribute value ids already selected
388403
389404
:returns: list of available attribute values
@@ -500,6 +515,27 @@ def _get_conversions_dict(self):
500515
}
501516
return conversions
502517

518+
@api.multi
519+
@api.constrains('attribute_value_ids')
520+
def _check_duplicate_product(self):
521+
if not self.config_ok:
522+
return None
523+
524+
# All duplicates with and without custom values
525+
duplicates = self.product_tmpl_id.search_variant(
526+
self.attribute_value_ids.ids).filtered(lambda p: p.id != self.id)
527+
528+
# Prevent duplicates without custom values (only attribute values)
529+
if duplicates.filtered(lambda p: not p.value_custom_ids):
530+
raise ValidationError(
531+
_("Configurable Products cannot have duplicates "
532+
"(identical attribute values)")
533+
)
534+
535+
# TODO: For the future prevent duplicates with identical custom values
536+
# or implement custom values on the order line level since they are
537+
# specific to each order.
538+
503539
@api.multi
504540
def _compute_product_price_extra(self):
505541
"""Compute price of configurable products as sum

product_configurator/static/js/form_widgets.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@ var FieldBooleanButton = core.form_widget_registry.map['boolean_button'].extend(
2121
});
2222

2323

24-
core.form_widget_registry.add('boolean_button', FieldBooleanButton)
24+
core.form_widget_registry.add('boolean_button', FieldBooleanButton);
2525

26-
})
26+
});

product_configurator/tests/test_configuration_rules.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,4 @@ def test_invalid_custom_value_configuration(self):
101101
self.assertFalse(validation, "Custom value accepted for fixed "
102102
"attribute color")
103103

104-
# Test configuration with disallowed custom type value
104+
# TODO: Test configuration with disallowed custom type value

product_configurator_wizard/tests/test_wizard.py

Lines changed: 91 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,38 +12,112 @@ def setUp(self):
1212

1313
self.so = self.env.ref('sale.sale_order_5')
1414

15-
def get_wizard_write_dict(self, wizard, attr_val_ext_ids):
15+
def get_attr_values(self, attr_val_ext_ids=None):
16+
if not attr_val_ext_ids:
17+
attr_val_ext_ids = []
18+
ext_id_prefix = 'product_configurator.product_attribute_value_%s'
19+
attr_vals = self.env['product.attribute.value']
20+
21+
for ext_id in attr_val_ext_ids:
22+
attr_vals += self.env.ref(ext_id_prefix % ext_id)
23+
24+
return attr_vals
25+
26+
def get_wizard_write_dict(self, wizard, attr_values):
1627
"""Turn a series of attribute.value objects to a dictionary meant for
1728
writing values to the product.configurator wizard"""
1829

1930
write_dict = {}
20-
ext_id_prefix = 'product_configurator.product_attribute_value_%s'
2131

22-
for ext_id in attr_val_ext_ids:
23-
val = self.env.ref(ext_id_prefix % ext_id)
24-
write_dict.update({
25-
wizard.field_prefix + str(val.attribute_id.id): val.id
26-
})
32+
multi_attr_ids = wizard.product_tmpl_id.attribute_line_ids.filtered(
33+
lambda x: x.multi).mapped('attribute_id').ids
34+
35+
for val in attr_values:
36+
field_name = wizard.field_prefix + str(val.attribute_id.id)
37+
if val.attribute_id.id in multi_attr_ids:
38+
write_dict.setdefault(field_name, [(6, 0, [])])
39+
write_dict[field_name][0][2].append(val.id)
40+
continue
41+
write_dict.update({field_name: val.id})
42+
2743
return write_dict
2844

29-
def test_wizard(self):
45+
def wizard_write_proceed(self, wizard, attr_vals, value_ids=None):
46+
"""Writes config data to the wizard then proceeds to the next step"""
47+
vals = self.get_wizard_write_dict(wizard, attr_vals)
48+
wizard.write(vals)
49+
wizard.action_next_step()
50+
# Store the values since the wizard removes dynamic values from dict
51+
if type(value_ids) == list:
52+
value_ids += attr_vals.ids
53+
54+
def test_wizard_configuration(self):
3055
"""Test product configurator wizard"""
3156

3257
# Start a new configuration wizard
33-
wizard = self.env['product.configurator'].create({
34-
'product_tmpl_id': self.cfg_tmpl.id
58+
wizard_obj = self.env['product.configurator'].with_context({
59+
'active_model': 'sale.order',
60+
'active_id': self.so.id
3561
})
3662

63+
wizard = wizard_obj.create({'product_tmpl_id': self.cfg_tmpl.id})
3764
wizard.action_next_step()
3865

39-
# Get write values the first configuration step
40-
write_dict = self.get_wizard_write_dict(wizard, ['gasoline', '228i'])
66+
value_ids = []
67+
68+
attr_vals = self.get_attr_values(['gasoline', '228i'])
69+
self.wizard_write_proceed(wizard, attr_vals, value_ids)
70+
71+
attr_vals = self.get_attr_values(['silver', 'rims_387'])
72+
self.wizard_write_proceed(wizard, attr_vals, value_ids)
4173

42-
# Store the values since the wizard removed dynamic values from dict
43-
write_val_ids = write_dict.values()
74+
attr_vals = self.get_attr_values(['model_sport_line'])
75+
self.wizard_write_proceed(wizard, attr_vals, value_ids)
4476

45-
# Test wizard dynamic write
46-
wizard.write(write_dict)
77+
attr_vals = self.get_attr_values(['tapistry_black'])
78+
self.wizard_write_proceed(wizard, attr_vals, value_ids)
4779

48-
self.assertTrue(wizard.value_ids.ids == write_val_ids,
80+
attr_vals = self.get_attr_values(['steptronic', 'tow_hook', 'sunroof'])
81+
vals = self.get_wizard_write_dict(wizard, attr_vals)
82+
wizard.write(vals)
83+
value_ids += attr_vals.ids
84+
85+
self.assertTrue(set(wizard.value_ids.ids) == set(value_ids),
4986
"Wizard write did not update the config session")
87+
88+
wizard.action_next_step()
89+
90+
config_variants = self.env['product.product'].search([
91+
('config_ok', '=', True)
92+
])
93+
94+
self.assertTrue(len(config_variants) == 1,
95+
"Wizard did not create a configurable variant")
96+
97+
def test_reconfiguration(self):
98+
"""Test reconfiguration functionality of the wizard"""
99+
self.test_wizard_configuration()
100+
101+
order_line = self.so.order_line.filtered(
102+
lambda l: l.product_id.config_ok
103+
)
104+
105+
reconfig_action = order_line.reconfigure_product()
106+
107+
wizard = self.env['product.configurator'].browse(
108+
reconfig_action.get('res_id')
109+
)
110+
111+
attr_vals = self.get_attr_values(['diesel', '220d'])
112+
self.wizard_write_proceed(wizard, attr_vals)
113+
114+
# Cycle through steps until wizard ends
115+
while wizard.action_next_step():
116+
pass
117+
118+
config_variants = self.env['product.product'].search([
119+
('config_ok', '=', True)
120+
])
121+
122+
self.assertTrue(len(config_variants) == 2,
123+
"Wizard reconfiguration did not create a new variant")

product_configurator_wizard/wizard/product_configurator.py

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -804,22 +804,6 @@ def action_config_done(self):
804804
l.value or l.attachment_ids for l in self.custom_value_ids
805805
}
806806

807-
if self.product_id:
808-
product_tmpl = self.product_id.product_tmpl_id
809-
remove_cv_links = map(
810-
lambda cv: (2, cv), self.product_id.value_custom_ids.ids)
811-
new_cv_links = product_tmpl.encode_custom_values(custom_vals)
812-
self.product_id.write({
813-
'attribute_value_ids': [(6, 0, self.value_ids.ids)],
814-
'value_custom_ids': remove_cv_links + new_cv_links,
815-
})
816-
if self.order_line_id:
817-
order_line_vals = self._extra_line_values(
818-
self.order_line_id.order_id, self.product_id, new=False)
819-
self.order_line_id.write(order_line_vals)
820-
self.unlink()
821-
return
822-
#
823807
# This try except is too generic.
824808
# The create_variant routine could effectively fail for
825809
# a large number of reasons, including bad programming.
@@ -828,7 +812,7 @@ def action_config_done(self):
828812
# error legitimately raised in a nested routine
829813
# is passed through.
830814
try:
831-
variant = self.product_tmpl_id.create_variant(
815+
variant = self.product_tmpl_id.create_get_variant(
832816
self.value_ids.ids, custom_vals)
833817
except ValidationError:
834818
raise
@@ -841,11 +825,14 @@ def action_config_done(self):
841825
so = self.env['sale.order'].browse(self.env.context.get('active_id'))
842826

843827
line_vals = {'product_id': variant.id}
844-
line_vals.update(self._extra_line_values(so, variant, new=True))
828+
line_vals.update(self._extra_line_values(
829+
self.order_line_id.order_id or so, variant, new=True)
830+
)
845831

846-
so.write({
847-
'order_line': [(0, 0, line_vals)]
848-
})
832+
if self.order_line_id:
833+
self.order_line_id.write(line_vals)
834+
else:
835+
so.write({'order_line': [(0, 0, line_vals)]})
849836

850837
self.unlink()
851838
return

website_product_configurator/controllers/main.py

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -568,28 +568,12 @@ def image_update(self, product_tmpl, cfg_vals,
568568
return self.get_config_image(product_tmpl, value_ids, size)
569569

570570
def configure_product(self, product_tmpl, value_ids, custom_vals=None):
571-
"""Used for searching a variant with the values passed in cfg_vals
572-
and returning a redirect to it. In case a product is not found with
573-
the given valid configuration a new variant is generated with the
574-
specific values and then returned
575-
576-
:param product_tmpl: product.template object being configured
577-
:param cfg_vals: dict representing the client-side configuration
578-
579-
:returns: product.product object found or created
580-
"""
581-
# TODO: Implement a search and create method that can be extended
582-
# easily
571+
"""Method kept for backward compatiblity"""
572+
# TODO: Remove in next version
583573
if custom_vals is None:
584574
custom_vals = {}
585575

586-
product = product_tmpl.search_variant(value_ids, custom_vals)
587-
if product:
588-
if len(product) > 1:
589-
return False
590-
else:
591-
return product
592-
return product_tmpl.sudo().create_variant(value_ids, custom_vals)
576+
return product_tmpl.sudo().create_get_variant(value_ids, custom_vals)
593577

594578
def get_attr_classes(self, attr_line, attr_value=False, custom=False):
595579
"""Computes classes for attribute elements in frontend for the purpose
@@ -713,11 +697,13 @@ def cfg_session(self, cfg_session, **post):
713697
except:
714698
return request.redirect('/configurator')
715699
if post:
716-
product = self.configure_product(
717-
cfg_session.product_tmpl_id, cfg_session.value_ids.ids, {
718-
x.attribute_id.id: x.value or x.attachment_ids for x in
719-
cfg_session.custom_value_ids
720-
})
700+
custom_vals = {
701+
x.attribute_id.id: x.value or x.attachment_ids for x in
702+
cfg_session.custom_value_ids
703+
}
704+
product = cfg_session.product_tmpl_id.sudo().create_get_variant(
705+
cfg_session.value_ids.ids, custom_vals
706+
)
721707
cfg_session.sudo().unlink()
722708
return self.cart_update(product, post)
723709

0 commit comments

Comments
 (0)