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 9a6bfe56..62762c12 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,44 @@ 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()
+
+ 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()
+ if self.validate_domains_against_sels(domains, value_ids):
+ # Domain 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..bf723bed 100644
--- a/product_configurator/models/product_config.py
+++ b/product_configurator/models/product_config.py
@@ -197,6 +197,59 @@ 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'
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/tests/test_configuration_rules.py b/product_configurator/tests/test_configuration_rules.py
index 984a3010..feac814e 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/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/tests/test_wizard.py b/product_configurator_wizard/tests/test_wizard.py
index 3acd8643..2e410242 100644
--- a/product_configurator_wizard/tests/test_wizard.py
+++ b/product_configurator_wizard/tests/test_wizard.py
@@ -121,3 +121,51 @@ 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:
+ field_name = '%s%s' % (
+ wizard.field_prefix,
+ attribute_line.attribute_id.id
+ )
+ dynamic_fields[field_name] = [] if attribute_line.multi else False
+
+ 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
+
+ oc_vals = dynamic_fields.copy()
+ oc_vals.update({'id': wizard.id,
+ })
+ oc_vals.update(attr_gasoline_dict)
+ oc_result = wizard.onchange(
+ oc_vals,
+ attr_gasoline_dict.keys()[0],
+ {}
+ )
+ k, v = attr_218i_dict.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_vals.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 5e0055d0..674cc8ee 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,22 +123,56 @@ 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]
+ # 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[0][2])
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})
+ dynamic_fields[k] = [[6, 0, 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
+
+ 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)
@@ -196,7 +232,34 @@ 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)
+ 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:
+ 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)
+
+ domains = self.get_onchange_domains(values, cfg_val_ids)
+ 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
+ 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(
@@ -273,6 +336,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 +433,26 @@ 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