From 925fcd8555754a3c353e9c35a723b78850c4a7e7 Mon Sep 17 00:00:00 2001 From: Richard deMeester Date: Wed, 5 Apr 2017 14:54:55 +1000 Subject: [PATCH 1/9] ENH - Default Values based on domains. --- product_configurator/models/product.py | 55 +++++++++++++++++++ product_configurator/models/product_config.py | 47 +++++++++++++++- .../security/ir.model.access.csv | 3 + product_configurator/views/product_view.xml | 18 ++++++ .../wizard/product_configurator.py | 50 +++++++++++++++-- 5 files changed, 166 insertions(+), 7 deletions(-) diff --git a/product_configurator/models/product.py b/product_configurator/models/product.py index 9a6bfe56..d14e8344 100644 --- a/product_configurator/models/product.py +++ b/product_configurator/models/product.py @@ -17,6 +17,12 @@ class ProductTemplate(models.Model): string="Attribute Dependencies" ) + config_default_ids = fields.One2many( + comodel_name='product.config.default', + inverse_name='product_tmpl_id', + string="Attribute Defaults" + ) + config_image_ids = fields.One2many( comodel_name='product.config.image', inverse_name='product_tmpl_id', @@ -439,6 +445,55 @@ def values_available(self, attr_val_ids, sel_val_ids): return avail_val_ids + @api.multi + def find_default_value(self, selectable_value_ids, value_ids): + """Based on the current values, which of the available template value ids + is the best default value to use. + + :param selectable_value_ids: list of product.attribute.value + object already trimmed down as selectable, for one + attribute line. + :param value_ids: list of attribute value ids already chosen + + :returns: The first matched default id + + """ + self.ensure_one() + + #TODO remove this check, which is only if the module has not been upgraded. + if not 'config_default_ids' in self._fields: + return False + + if not selectable_value_ids: + return False + # assume all values are from the same attribute line - they should be! + default_lines = self.config_default_ids.filtered( + lambda l: set(l.value_ids.ids) & set(selectable_value_ids) + ) + + for default_line in default_lines: + if not default_line.domain_id: + # No domain - always considered true. Use this. + break + domains = default_line.mapped('domain_id').compute_domain() + for domain in domains: + if domain[1] == 'in': + if not set(domain[2]) & set(value_ids): + # Domain mismatch, skip this line + break + else: + if set(domain[2]) & set(value_ids): + # Domain mismatch, skip this line + break + else: + # All domains OK, use this + break + else: + # parsed all lines without a match + return False + # pick one at random... + return (set(default_line.value_ids.ids) & set(selectable_value_ids)).pop() + @api.multi def validate_configuration(self, value_ids, custom_vals=None, final=True): """ Verifies if the configuration values passed via value_ids and custom_vals diff --git a/product_configurator/models/product_config.py b/product_configurator/models/product_config.py index 64ae1a31..d4b0f004 100644 --- a/product_configurator/models/product_config.py +++ b/product_configurator/models/product_config.py @@ -197,6 +197,52 @@ def check_value_attributes(self): ) +class ProductConfigDefault(models.Model): + _name = 'product.config.default' + + product_tmpl_id = fields.Many2one( + comodel_name='product.template', + string='Product Template', + ondelete='cascade', + required=True + ) + + # TODO: Find a more elegant way to restrict the value_ids + attr_line_val_ids = fields.Many2many( + comodel_name='product.attribute.value', + compute='_compute_attr_vals' + ) + + value_ids = fields.Many2many( + comodel_name='product.attribute.value', + id1="cfg_dflt_id", + id2="attr_val_id", + string="Values" + ) + + domain_id = fields.Many2one( + comodel_name='product.config.domain', + string='Applied If', + help='Default will be attempted if this rule passes, or leave blank for a generic default' + ) + + sequence = fields.Integer(string='Sequence', default=10) + + _order = 'product_tmpl_id, sequence, id' + + @api.multi + def _compute_attr_vals(self): + for config_default in self: + config_default.attr_line_val_ids=config_default.product_tmpl_id.attribute_line_ids.mapped('value_ids') + + @api.model + def default_get(self, fields): + result = super(ProductConfigDefault, self).default_get(fields) + if self.env.context.get('for_template_id'): + result['attr_line_val_ids'] = \ + self.env['product.template'].browse(self.env.context['for_template_id']).attribute_line_ids.mapped('value_ids').ids + return result + class ProductConfigImage(models.Model): _name = 'product.config.image' @@ -285,7 +331,6 @@ def _check_config_step(self): if self.config_step_id in cfg_steps: raise Warning(_('Cannot have a configuration step defined twice.')) - class ProductConfigSession(models.Model): _name = 'product.config.session' diff --git a/product_configurator/security/ir.model.access.csv b/product_configurator/security/ir.model.access.csv index 5d1d44e2..e21b3421 100644 --- a/product_configurator/security/ir.model.access.csv +++ b/product_configurator/security/ir.model.access.csv @@ -1,5 +1,6 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink product_configurator_config_line,Config Line,model_product_config_line,group_product_configurator,1,1,1,1 +product_configurator_config_default,Config Default,model_product_config_default,group_product_configurator,1,1,1,1 product_configurator_config_image,Config Image,model_product_config_image,group_product_configurator,1,1,1,1 product_configurator_config_step,Config Step,model_product_config_step,group_product_configurator,1,1,1,1 product_configurator_config_step_line,Config Step Line,model_product_config_step_line,group_product_configurator,1,1,1,1 @@ -10,6 +11,7 @@ product_configurator_config_session,Config Session,model_product_config_session, product_configurator_config_session_custom_value,Config Session Custom Value,model_product_config_session_custom_value,group_product_configurator,1,1,1,1 ,,,,,,, user_config_line,User Config Line,model_product_config_line,base.group_user,1,0,0,0 +user_config_default,User Config Default,model_product_config_default,base.group_user,1,0,0,0 user_config_image,User Config Image,model_product_config_image,base.group_user,1,0,0,0 user_config_step,User Config Step,model_product_config_step,base.group_user,1,0,0,0 user_config_step_line,User Config Step Line,model_product_config_step_line,base.group_user,1,0,0,0 @@ -25,6 +27,7 @@ portal_config_step,Portal Config Step,model_product_config_step,base.group_porta portal_config_session,Portal Config Session,model_product_config_session,base.group_portal,1,0,0,0 portal_config_session_custom_value,Portal Config Session Custom Value,model_product_config_session_custom_value,base.group_portal,1,0,0,0 portal_configurator_config_line,Portal Config Line,model_product_config_line,base.group_portal,1,0,0,0 +portal_configurator_config_default,Portal Config Default,model_product_config_default,base.group_portal,1,0,0,0 portal_configurator_config_step_line,Portal Config Step Line,model_product_config_step_line,base.group_portal,1,0,0,0 portal_configurator_config_domain,Portal Config Domain,model_product_config_domain,base.group_portal,1,0,0,0 portal_configurator_config_domain_line,Portal Config Domain Line,model_product_config_domain_line,base.group_portal,1,0,0,0 diff --git a/product_configurator/views/product_view.xml b/product_configurator/views/product_view.xml index 1519dece..994f0ab9 100644 --- a/product_configurator/views/product_view.xml +++ b/product_configurator/views/product_view.xml @@ -65,6 +65,24 @@ + + + + + + + + + + diff --git a/product_configurator_wizard/wizard/product_configurator.py b/product_configurator_wizard/wizard/product_configurator.py index 5e0055d0..17602a3a 100644 --- a/product_configurator_wizard/wizard/product_configurator.py +++ b/product_configurator_wizard/wizard/product_configurator.py @@ -109,10 +109,12 @@ def get_onchange_domains(self, values, cfg_val_ids): continue return domains - def get_form_vals(self, dynamic_fields, domains): + def get_form_vals(self, dynamic_fields, domains, cfg_step): """Generate a dictionary to return new values via onchange method. Domains hold the values available, this method enforces these values if a selection exists in the view that is not available anymore. + Also, if there are values blanked out by this, then try and see if + there is an available default. :param dynamic_fields: Dictionary with the current {dynamic_field: val} :param domains: Odoo domains restricting attribute values @@ -121,19 +123,37 @@ def get_form_vals(self, dynamic_fields, domains): """ vals = {} - dynamic_fields = {k: v for k, v in dynamic_fields.iteritems() if v} + dynamic_fields = dynamic_fields.copy() + # validate and eliminate values, and set defaults if they are on the + # current step + step_val_ids = cfg_step and cfg_step.attribute_line_ids.mapped('value_ids').ids \ + or self.product_tmpl_id.attribute_line_ids.mapped('value_ids').ids for k, v in dynamic_fields.iteritems(): + available_val_ids = domains[k][0][2] + config_val_ids = [dfv for dfv in dynamic_fields.values() if dfv and not isinstance(dfv, list)] + for list_dfv in [dfv for dfv in dynamic_fields.values() if dfv and isinstance(dfv, list)]: + config_val_ids.extend(list_dfv) if not v: + # if the value currently is blank and on the current step, see if one can be set + if set(available_val_ids) & set(step_val_ids): + def_value_id = self.product_tmpl_id.find_default_value(available_val_ids, config_val_ids) + if def_value_id: + dynamic_fields.update({k: def_value_id}) + vals[k] = def_value_id continue - available_val_ids = domains[k][0][2] if isinstance(v, list): value_ids = list(set(v[0][2]) & set(available_val_ids)) dynamic_fields.update({k: value_ids}) vals[k] = [[6, 0, value_ids]] elif v not in available_val_ids: - dynamic_fields.update({k: None}) - vals[k] = None + # if the value is to be blanked, and it is on the current step, see if a default can be set + if set(available_val_ids) & set(step_val_ids): + def_value_id = self.product_tmpl_id.find_default_value(available_val_ids, config_val_ids) or None + else: + def_value_id = None + dynamic_fields.update({k: def_value_id}) + vals[k] = def_value_id product_img = self.product_tmpl_id.get_config_image_obj( dynamic_fields.values()) @@ -196,7 +216,7 @@ def onchange(self, values, field_name, field_onchange): cfg_val_ids = cfg_vals.ids + list(view_val_ids) domains = self.get_onchange_domains(values, cfg_val_ids) - vals = self.get_form_vals(dynamic_fields, domains) + vals = self.get_form_vals(dynamic_fields, domains, cfg_step) return {'value': vals, 'domain': domains} attribute_line_ids = fields.One2many( @@ -273,6 +293,8 @@ def fields_get(self, allfields=None, attributes=None): # If no configuration steps exist then get all attribute lines attribute_lines = wiz.product_tmpl_id.attribute_line_ids + # TODO: If last block is attempting to be clever, this next + # line is ignoring it. Need to determine what is best. attribute_lines = wiz.product_tmpl_id.attribute_line_ids # Generate relational fields with domains restricting values to @@ -368,6 +390,22 @@ def fields_view_get(self, view_id=None, view_type='form', # Update result dict from super with modified view res.update({'arch': etree.tostring(mod_view)}) + # set any default values + wiz_vals = wiz.read(dynamic_fields.keys())[0] + dynamic_field_vals = { + k: wiz_vals.get(k, [] if v['type'] == 'many2many' else False) + for k, v in fields.iteritems() if k.startswith(self.field_prefix) + } + domains = {k: dynamic_fields[k]['domain'] for k in dynamic_field_vals.keys()} + try: + cfg_step_id = int(wiz.state) + cfg_step = wiz.product_tmpl_id.config_step_line_ids.filtered( + lambda x: x.id == cfg_step_id) + except: + cfg_step = self.env['product.config.step.line'] + vals = wiz.get_form_vals(dynamic_field_vals, domains, cfg_step) + if vals: + wiz.write(vals) return res @api.model From b51df94bbfe217267114bf764f5b81e25bfa13a7 Mon Sep 17 00:00:00 2001 From: Richard deMeester Date: Mon, 10 Apr 2017 14:36:27 +1000 Subject: [PATCH 2/9] New tests, and PEP8 code fixes. --- product_configurator/__manifest__.py | 1 + .../demo/product_config_defaults.xml | 20 +++++++++ product_configurator/models/product.py | 8 ++-- product_configurator/models/product_config.py | 14 ++++-- .../tests/test_configuration_rules.py | 39 +++++++++++++++- .../tests/test_wizard.py | 45 +++++++++++++++++++ .../wizard/product_configurator.py | 33 +++++++++----- 7 files changed, 140 insertions(+), 20 deletions(-) create mode 100644 product_configurator/demo/product_config_defaults.xml diff --git a/product_configurator/__manifest__.py b/product_configurator/__manifest__.py index 4f75285a..dd241883 100755 --- a/product_configurator/__manifest__.py +++ b/product_configurator/__manifest__.py @@ -24,6 +24,7 @@ 'demo/product_attribute.xml', 'demo/product_config_domain.xml', 'demo/product_config_lines.xml', + 'demo/product_config_defaults.xml', 'demo/product_config_step.xml', 'demo/config_image_ids.xml', ], diff --git a/product_configurator/demo/product_config_defaults.xml b/product_configurator/demo/product_config_defaults.xml new file mode 100644 index 00000000..a305ccc7 --- /dev/null +++ b/product_configurator/demo/product_config_defaults.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/product_configurator/models/product.py b/product_configurator/models/product.py index d14e8344..ffd8b6d2 100644 --- a/product_configurator/models/product.py +++ b/product_configurator/models/product.py @@ -460,10 +460,6 @@ def find_default_value(self, selectable_value_ids, value_ids): """ self.ensure_one() - #TODO remove this check, which is only if the module has not been upgraded. - if not 'config_default_ids' in self._fields: - return False - if not selectable_value_ids: return False # assume all values are from the same attribute line - they should be! @@ -492,7 +488,9 @@ def find_default_value(self, selectable_value_ids, value_ids): # parsed all lines without a match return False # pick one at random... - return (set(default_line.value_ids.ids) & set(selectable_value_ids)).pop() + return ( + set(default_line.value_ids.ids) & set(selectable_value_ids) + ).pop() @api.multi def validate_configuration(self, value_ids, custom_vals=None, final=True): diff --git a/product_configurator/models/product_config.py b/product_configurator/models/product_config.py index d4b0f004..bf723bed 100644 --- a/product_configurator/models/product_config.py +++ b/product_configurator/models/product_config.py @@ -223,7 +223,8 @@ class ProductConfigDefault(models.Model): domain_id = fields.Many2one( comodel_name='product.config.domain', string='Applied If', - help='Default will be attempted if this rule passes, or leave blank for a generic default' + help='Default will be attempted if this rule passes, or leave blank ' + 'for a generic default' ) sequence = fields.Integer(string='Sequence', default=10) @@ -233,16 +234,22 @@ class ProductConfigDefault(models.Model): @api.multi def _compute_attr_vals(self): for config_default in self: - config_default.attr_line_val_ids=config_default.product_tmpl_id.attribute_line_ids.mapped('value_ids') + config_default.attr_line_val_ids = \ + config_default.product_tmpl_id.attribute_line_ids.mapped( + 'value_ids' + ) @api.model def default_get(self, fields): result = super(ProductConfigDefault, self).default_get(fields) if self.env.context.get('for_template_id'): result['attr_line_val_ids'] = \ - self.env['product.template'].browse(self.env.context['for_template_id']).attribute_line_ids.mapped('value_ids').ids + self.env['product.template'].browse( + self.env.context['for_template_id'] + ).attribute_line_ids.mapped('value_ids').ids return result + class ProductConfigImage(models.Model): _name = 'product.config.image' @@ -331,6 +338,7 @@ def _check_config_step(self): if self.config_step_id in cfg_steps: raise Warning(_('Cannot have a configuration step defined twice.')) + class ProductConfigSession(models.Model): _name = 'product.config.session' diff --git a/product_configurator/tests/test_configuration_rules.py b/product_configurator/tests/test_configuration_rules.py index 984a3010..ee833ace 100644 --- a/product_configurator/tests/test_configuration_rules.py +++ b/product_configurator/tests/test_configuration_rules.py @@ -101,4 +101,41 @@ def test_invalid_custom_value_configuration(self): self.assertFalse(validation, "Custom value accepted for fixed " "attribute color") - # TODO: Test configuration with disallowed custom type value + def test_configuration_defaults(self): + conf = ['gasoline', 'tapistry_black'] + engine_selections = self.env.ref( + 'product_configurator.product_config_line_gasoline_engines' + ) + attr_val_ids = self.get_attr_val_ids(conf) + default_value_engine = self.cfg_tmpl.find_default_value( + engine_selections.value_ids.ids, + attr_val_ids, + ) + self.assertEqual( + [default_value_engine], self.get_attr_val_ids(['218i']), + "Gasoline Engine default not set correctly" + ) + + color_selection_ids = self.get_attr_val_ids(['red', 'silver', 'black']) + attr_val_ids = self.get_attr_val_ids(conf) + default_value_color = self.cfg_tmpl.find_default_value( + color_selection_ids, + attr_val_ids, + ) + self.assertEqual( + [default_value_color], self.get_attr_val_ids(['red']), + "Gasoline Color default not set correctly" + ) + + color_selection_ids = self.get_attr_val_ids(['silver', 'black']) + attr_val_ids = self.get_attr_val_ids(conf) + default_value_color = self.cfg_tmpl.find_default_value( + color_selection_ids, + attr_val_ids, + ) + self.assertFalse( + default_value_color, + "Gasoline Color should not have been returned unselectable value" + ) + + # Test configuration with disallowed custom type value diff --git a/product_configurator_wizard/tests/test_wizard.py b/product_configurator_wizard/tests/test_wizard.py index 3acd8643..b2b61f86 100644 --- a/product_configurator_wizard/tests/test_wizard.py +++ b/product_configurator_wizard/tests/test_wizard.py @@ -121,3 +121,48 @@ def test_reconfiguration(self): self.assertTrue(len(config_variants) == 2, "Wizard reconfiguration did not create a new variant") + + def test_wizard_domains(self): + """Test product configurator wizard default values""" + + # Start a new configuration wizard + wizard = self.env['product.configurator'].create({ + 'product_tmpl_id': self.cfg_tmpl.id + }) + + dynamic_fields = {} + for attribute_line in self.cfg_tmpl.attribute_line_ids: + dynamic_fields['%s%s' % ( + wizard.field_prefix, + attribute_line.attribute_id.id + ) + ] = [] if attribute_line.multi else False + + write_dict_gasoline = self.get_wizard_write_dict(wizard, ['gasoline']) + write_dict_218i = self.get_wizard_write_dict(wizard, ['218i']) + gasoline_engine_ids = self.env.ref( + 'product_configurator.product_config_line_gasoline_engines' + ).value_ids.ids + + oc_vals = dynamic_fields.copy() + oc_vals.update({'id': wizard.id, + }) + oc_vals.update(self.get_wizard_write_dict(wizard, ['gasoline'])) + oc_result = wizard.onchange( + oc_vals, + write_dict_gasoline.keys()[0], + {} + ) + k, v = write_dict_218i.iteritems().next() + self.assertEqual( + oc_result.get('value', {}).get(k), + v, + "Engine default value not set correctly by onchange wizard" + ) + oc_domain = oc_result.get('domain', {}).get(k, []) + domain_ids = oc_domain and oc_domain[0][2] or [] + self.assertEqual( + set(domain_ids), + set(gasoline_engine_ids), + "Engine domain value not set correctly by onchange wizard" + ) diff --git a/product_configurator_wizard/wizard/product_configurator.py b/product_configurator_wizard/wizard/product_configurator.py index 17602a3a..a15a4417 100644 --- a/product_configurator_wizard/wizard/product_configurator.py +++ b/product_configurator_wizard/wizard/product_configurator.py @@ -127,17 +127,22 @@ def get_form_vals(self, dynamic_fields, domains, cfg_step): # validate and eliminate values, and set defaults if they are on the # current step - step_val_ids = cfg_step and cfg_step.attribute_line_ids.mapped('value_ids').ids \ - or self.product_tmpl_id.attribute_line_ids.mapped('value_ids').ids + step_val_ids = cfg_step and \ + cfg_step.attribute_line_ids.mapped('value_ids').ids or \ + self.product_tmpl_id.attribute_line_ids.mapped('value_ids').ids for k, v in dynamic_fields.iteritems(): available_val_ids = domains[k][0][2] - config_val_ids = [dfv for dfv in dynamic_fields.values() if dfv and not isinstance(dfv, list)] - for list_dfv in [dfv for dfv in dynamic_fields.values() if dfv and isinstance(dfv, list)]: + config_val_ids = [dfv for dfv in dynamic_fields.values() + if dfv and not isinstance(dfv, list)] + for list_dfv in [dfv for dfv in dynamic_fields.values() + if dfv and isinstance(dfv, list)]: config_val_ids.extend(list_dfv) if not v: - # if the value currently is blank and on the current step, see if one can be set + # if the value currently is blank and on the current step, see + # if one can be set if set(available_val_ids) & set(step_val_ids): - def_value_id = self.product_tmpl_id.find_default_value(available_val_ids, config_val_ids) + def_value_id = self.product_tmpl_id.find_default_value( + available_val_ids, config_val_ids) if def_value_id: dynamic_fields.update({k: def_value_id}) vals[k] = def_value_id @@ -147,9 +152,11 @@ def get_form_vals(self, dynamic_fields, domains, cfg_step): dynamic_fields.update({k: value_ids}) vals[k] = [[6, 0, value_ids]] elif v not in available_val_ids: - # if the value is to be blanked, and it is on the current step, see if a default can be set + # if the value is to be blanked, and it is on the current + # step, see if a default can be set if set(available_val_ids) & set(step_val_ids): - def_value_id = self.product_tmpl_id.find_default_value(available_val_ids, config_val_ids) or None + def_value_id = self.product_tmpl_id.find_default_value( + available_val_ids, config_val_ids) or None else: def_value_id = None dynamic_fields.update({k: def_value_id}) @@ -393,10 +400,14 @@ def fields_view_get(self, view_id=None, view_type='form', # set any default values wiz_vals = wiz.read(dynamic_fields.keys())[0] dynamic_field_vals = { - k: wiz_vals.get(k, [] if v['type'] == 'many2many' else False) - for k, v in fields.iteritems() if k.startswith(self.field_prefix) + k: wiz_vals.get( + k, [] if v['type'] == 'many2many' else False + ) + for k, v in fields.iteritems() + if k.startswith(self.field_prefix) } - domains = {k: dynamic_fields[k]['domain'] for k in dynamic_field_vals.keys()} + domains = {k: dynamic_fields[k]['domain'] + for k in dynamic_field_vals.keys()} try: cfg_step_id = int(wiz.state) cfg_step = wiz.product_tmpl_id.config_step_line_ids.filtered( From 5e2676bed8fa0b5cbbc4c86065ddf1c8a87450ac Mon Sep 17 00:00:00 2001 From: Richard deMeester Date: Mon, 10 Apr 2017 15:38:01 +1000 Subject: [PATCH 3/9] PEP8 checks. --- .../tests/test_configuration_rules.py | 16 ++++++++-------- product_configurator_wizard/tests/test_wizard.py | 10 +++++----- .../wizard/product_configurator.py | 6 ++++-- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/product_configurator/tests/test_configuration_rules.py b/product_configurator/tests/test_configuration_rules.py index ee833ace..feac814e 100644 --- a/product_configurator/tests/test_configuration_rules.py +++ b/product_configurator/tests/test_configuration_rules.py @@ -104,7 +104,7 @@ def test_invalid_custom_value_configuration(self): def test_configuration_defaults(self): conf = ['gasoline', 'tapistry_black'] engine_selections = self.env.ref( - 'product_configurator.product_config_line_gasoline_engines' + 'product_configurator.product_config_line_gasoline_engines' ) attr_val_ids = self.get_attr_val_ids(conf) default_value_engine = self.cfg_tmpl.find_default_value( @@ -112,9 +112,9 @@ def test_configuration_defaults(self): attr_val_ids, ) self.assertEqual( - [default_value_engine], self.get_attr_val_ids(['218i']), - "Gasoline Engine default not set correctly" - ) + [default_value_engine], self.get_attr_val_ids(['218i']), + "Gasoline Engine default not set correctly" + ) color_selection_ids = self.get_attr_val_ids(['red', 'silver', 'black']) attr_val_ids = self.get_attr_val_ids(conf) @@ -123,8 +123,8 @@ def test_configuration_defaults(self): attr_val_ids, ) self.assertEqual( - [default_value_color], self.get_attr_val_ids(['red']), - "Gasoline Color default not set correctly" + [default_value_color], self.get_attr_val_ids(['red']), + "Gasoline Color default not set correctly" ) color_selection_ids = self.get_attr_val_ids(['silver', 'black']) @@ -134,8 +134,8 @@ def test_configuration_defaults(self): attr_val_ids, ) self.assertFalse( - default_value_color, - "Gasoline Color should not have been returned unselectable value" + default_value_color, + "Gasoline Color should not have been returned unselectable value" ) # Test configuration with disallowed custom type value diff --git a/product_configurator_wizard/tests/test_wizard.py b/product_configurator_wizard/tests/test_wizard.py index b2b61f86..737b1cb2 100644 --- a/product_configurator_wizard/tests/test_wizard.py +++ b/product_configurator_wizard/tests/test_wizard.py @@ -132,11 +132,11 @@ def test_wizard_domains(self): dynamic_fields = {} for attribute_line in self.cfg_tmpl.attribute_line_ids: - dynamic_fields['%s%s' % ( - wizard.field_prefix, - attribute_line.attribute_id.id - ) - ] = [] if attribute_line.multi else False + field_name = '%s%s' % ( + wizard.field_prefix, + attribute_line.attribute_id.id + ) + dynamic_fields[field_name] = [] if attribute_line.multi else False write_dict_gasoline = self.get_wizard_write_dict(wizard, ['gasoline']) write_dict_218i = self.get_wizard_write_dict(wizard, ['218i']) diff --git a/product_configurator_wizard/wizard/product_configurator.py b/product_configurator_wizard/wizard/product_configurator.py index a15a4417..57ab1671 100644 --- a/product_configurator_wizard/wizard/product_configurator.py +++ b/product_configurator_wizard/wizard/product_configurator.py @@ -142,7 +142,8 @@ def get_form_vals(self, dynamic_fields, domains, cfg_step): # if one can be set if set(available_val_ids) & set(step_val_ids): def_value_id = self.product_tmpl_id.find_default_value( - available_val_ids, config_val_ids) + available_val_ids, config_val_ids + ) if def_value_id: dynamic_fields.update({k: def_value_id}) vals[k] = def_value_id @@ -156,7 +157,8 @@ def get_form_vals(self, dynamic_fields, domains, cfg_step): # step, see if a default can be set if set(available_val_ids) & set(step_val_ids): def_value_id = self.product_tmpl_id.find_default_value( - available_val_ids, config_val_ids) or None + available_val_ids, config_val_ids + ) or None else: def_value_id = None dynamic_fields.update({k: def_value_id}) From 30fdbb13a321c4ab0244a3befabbd49cdb8b977d Mon Sep 17 00:00:00 2001 From: Richard deMeester Date: Tue, 25 Apr 2017 16:40:46 +1000 Subject: [PATCH 4/9] Changes to use enhanced domain validation for defaults. --- product_configurator/models/product.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/product_configurator/models/product.py b/product_configurator/models/product.py index ffd8b6d2..62762c12 100644 --- a/product_configurator/models/product.py +++ b/product_configurator/models/product.py @@ -472,17 +472,8 @@ def find_default_value(self, selectable_value_ids, value_ids): # No domain - always considered true. Use this. break domains = default_line.mapped('domain_id').compute_domain() - for domain in domains: - if domain[1] == 'in': - if not set(domain[2]) & set(value_ids): - # Domain mismatch, skip this line - break - else: - if set(domain[2]) & set(value_ids): - # Domain mismatch, skip this line - break - else: - # All domains OK, use this + if self.validate_domains_against_sels(domains, value_ids): + # Domain OK, use this break else: # parsed all lines without a match From 996982c35e9149922ccfa068fc7379e9469ff3d1 Mon Sep 17 00:00:00 2001 From: Richard deMeester Date: Sun, 30 Apr 2017 17:13:29 +1000 Subject: [PATCH 5/9] ENH - Multi call to onchange domain after defaults set. --- .../wizard/product_configurator.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/product_configurator_wizard/wizard/product_configurator.py b/product_configurator_wizard/wizard/product_configurator.py index 57ab1671..e58fd421 100644 --- a/product_configurator_wizard/wizard/product_configurator.py +++ b/product_configurator_wizard/wizard/product_configurator.py @@ -226,6 +226,30 @@ def onchange(self, values, field_name, field_onchange): domains = self.get_onchange_domains(values, cfg_val_ids) vals = self.get_form_vals(dynamic_fields, domains, cfg_step) + modified_dynamics = {k: v + for k, v in vals.iteritems() + if k in dynamic_fields} + + while modified_dynamics: + # modified values may change domains! + dynamic_fields.update(modified_dynamics) + for k, v in modified_dynamics.iteritems(): + attr_id = int(k.split(self.field_prefix)[1]) + view_val_ids -= set(self.env['product.attribute.value'].search( + [('attribute_id', '=', attr_id)]).ids) + if v: + view_val_ids.add(v) + + cfg_val_ids = cfg_vals.ids + list(view_val_ids) + + domains = self.get_onchange_domains(values, cfg_val_ids) + nvals = self.get_form_vals(dynamic_fields, domains) + # Stop possible recursion by not including values which have + # previously looped + modified_dynamics = {k: v + for k, v in nvals.iteritems() + if k in dynamic_fields and k not in vals} + vals.update(nvals) return {'value': vals, 'domain': domains} attribute_line_ids = fields.One2many( From e0b879087e3b8dcbbd6eadd75459beec8ec87377 Mon Sep 17 00:00:00 2001 From: Richard deMeester Date: Sun, 30 Apr 2017 17:45:33 +1000 Subject: [PATCH 6/9] Correct parameter passig --- product_configurator_wizard/wizard/product_configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/product_configurator_wizard/wizard/product_configurator.py b/product_configurator_wizard/wizard/product_configurator.py index e58fd421..9549ccb6 100644 --- a/product_configurator_wizard/wizard/product_configurator.py +++ b/product_configurator_wizard/wizard/product_configurator.py @@ -243,7 +243,7 @@ def onchange(self, values, field_name, field_onchange): cfg_val_ids = cfg_vals.ids + list(view_val_ids) domains = self.get_onchange_domains(values, cfg_val_ids) - nvals = self.get_form_vals(dynamic_fields, domains) + nvals = self.get_form_vals(dynamic_fields, domains, cfg_step) # Stop possible recursion by not including values which have # previously looped modified_dynamics = {k: v From 8ceb23c198e00bc1b9aaf69c165555e2580175da Mon Sep 17 00:00:00 2001 From: Richard deMeester Date: Thu, 4 May 2017 12:21:30 +1000 Subject: [PATCH 7/9] Test updated. --- .../tests/test_wizard.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/product_configurator_wizard/tests/test_wizard.py b/product_configurator_wizard/tests/test_wizard.py index 737b1cb2..2e410242 100644 --- a/product_configurator_wizard/tests/test_wizard.py +++ b/product_configurator_wizard/tests/test_wizard.py @@ -138,22 +138,25 @@ def test_wizard_domains(self): ) dynamic_fields[field_name] = [] if attribute_line.multi else False - write_dict_gasoline = self.get_wizard_write_dict(wizard, ['gasoline']) - write_dict_218i = self.get_wizard_write_dict(wizard, ['218i']) - gasoline_engine_ids = self.env.ref( + attr_gasoline_vals = self.get_attr_values(['gasoline']) + attr_gasoline_dict = self.get_wizard_write_dict(wizard, + attr_gasoline_vals) + attr_218i_vals = self.get_attr_values(['218i']) + attr_218i_dict = self.get_wizard_write_dict(wizard, attr_218i_vals) + gasoline_engine_vals = self.env.ref( 'product_configurator.product_config_line_gasoline_engines' - ).value_ids.ids + ).value_ids oc_vals = dynamic_fields.copy() oc_vals.update({'id': wizard.id, }) - oc_vals.update(self.get_wizard_write_dict(wizard, ['gasoline'])) + oc_vals.update(attr_gasoline_dict) oc_result = wizard.onchange( oc_vals, - write_dict_gasoline.keys()[0], + attr_gasoline_dict.keys()[0], {} ) - k, v = write_dict_218i.iteritems().next() + k, v = attr_218i_dict.iteritems().next() self.assertEqual( oc_result.get('value', {}).get(k), v, @@ -163,6 +166,6 @@ def test_wizard_domains(self): domain_ids = oc_domain and oc_domain[0][2] or [] self.assertEqual( set(domain_ids), - set(gasoline_engine_ids), + set(gasoline_engine_vals.ids), "Engine domain value not set correctly by onchange wizard" ) From 37a6768ded33ba40c3eac4f8d6e7a324d28231e8 Mon Sep 17 00:00:00 2001 From: Richard deMeester Date: Fri, 5 May 2017 13:59:35 +1000 Subject: [PATCH 8/9] Deal with multi selects correctly in recursive onchange --- product_configurator_wizard/wizard/product_configurator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/product_configurator_wizard/wizard/product_configurator.py b/product_configurator_wizard/wizard/product_configurator.py index 9549ccb6..1a4fff2a 100644 --- a/product_configurator_wizard/wizard/product_configurator.py +++ b/product_configurator_wizard/wizard/product_configurator.py @@ -238,7 +238,10 @@ def onchange(self, values, field_name, field_onchange): view_val_ids -= set(self.env['product.attribute.value'].search( [('attribute_id', '=', attr_id)]).ids) if v: - view_val_ids.add(v) + if isinstance(v, list): + view_val_ids |= set(v[0][2]) + elif isinstance(v, int): + view_val_ids.add(v) cfg_val_ids = cfg_vals.ids + list(view_val_ids) From b59d86e329b876e8c6fa6005243c16e289f5de7b Mon Sep 17 00:00:00 2001 From: Richard deMeester Date: Fri, 9 Jun 2017 09:21:38 +1000 Subject: [PATCH 9/9] [Fix] ensure multi selects dealt with consistently during default evaluation --- .../wizard/product_configurator.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/product_configurator_wizard/wizard/product_configurator.py b/product_configurator_wizard/wizard/product_configurator.py index 1a4fff2a..674cc8ee 100644 --- a/product_configurator_wizard/wizard/product_configurator.py +++ b/product_configurator_wizard/wizard/product_configurator.py @@ -132,11 +132,13 @@ def get_form_vals(self, dynamic_fields, domains, cfg_step): self.product_tmpl_id.attribute_line_ids.mapped('value_ids').ids for k, v in dynamic_fields.iteritems(): available_val_ids = domains[k][0][2] + # Get this fresh every time as the loop can change the values as + # it goes! config_val_ids = [dfv for dfv in dynamic_fields.values() if dfv and not isinstance(dfv, list)] for list_dfv in [dfv for dfv in dynamic_fields.values() if dfv and isinstance(dfv, list)]: - config_val_ids.extend(list_dfv) + config_val_ids.extend(list_dfv[0][2]) if not v: # if the value currently is blank and on the current step, see # if one can be set @@ -150,7 +152,7 @@ def get_form_vals(self, dynamic_fields, domains, cfg_step): continue if isinstance(v, list): value_ids = list(set(v[0][2]) & set(available_val_ids)) - dynamic_fields.update({k: value_ids}) + dynamic_fields[k] = [[6, 0, value_ids]] vals[k] = [[6, 0, value_ids]] elif v not in available_val_ids: # if the value is to be blanked, and it is on the current @@ -164,8 +166,13 @@ def get_form_vals(self, dynamic_fields, domains, cfg_step): dynamic_fields.update({k: def_value_id}) vals[k] = def_value_id + config_val_ids = [dfv for dfv in dynamic_fields.values() + if dfv and not isinstance(dfv, list)] + for list_dfv in [dfv for dfv in dynamic_fields.values() + if dfv and isinstance(dfv, list)]: + config_val_ids.extend(list_dfv[0][2]) product_img = self.product_tmpl_id.get_config_image_obj( - dynamic_fields.values()) + config_val_ids) vals.update(product_img=product_img.image)