diff --git a/product_configurator/models/product.py b/product_configurator/models/product.py
index 9a6bfe56..f89a9982 100644
--- a/product_configurator/models/product.py
+++ b/product_configurator/models/product.py
@@ -457,6 +457,9 @@ def validate_configuration(self, value_ids, custom_vals=None, final=True):
if custom_vals is None:
custom_vals = {}
+ # Check if all the values passed are not restricted
+ avail_val_ids = self.values_available(value_ids, value_ids)
+
for line in self.attribute_line_ids:
# Validate custom values
attr = line.attribute_id
@@ -466,11 +469,12 @@ def validate_configuration(self, value_ids, custom_vals=None, final=True):
common_vals = set(value_ids) & set(line.value_ids.ids)
custom_val = custom_vals.get(attr.id)
if line.required and not common_vals and not custom_val:
- # TODO: Verify custom value type to be correct
- return False
+ # If there are possible values, then enforce
+ # the "required" status.
+ if set(line.value_ids.ids) & set(avail_val_ids):
+ # TODO: Verify custom value type to be correct
+ return False
- # Check if all all the values passed are not restricted
- avail_val_ids = self.values_available(value_ids, value_ids)
if set(value_ids) - set(avail_val_ids):
return False
diff --git a/product_configurator_wizard/tests/__init__.py b/product_configurator_wizard/tests/__init__.py
index e037c216..404c70da 100644
--- a/product_configurator_wizard/tests/__init__.py
+++ b/product_configurator_wizard/tests/__init__.py
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from . import test_wizard
+from . import test_wizard_attrs
diff --git a/product_configurator_wizard/tests/test_wizard.py b/product_configurator_wizard/tests/test_wizard.py
index 3acd8643..eaad1d16 100644
--- a/product_configurator_wizard/tests/test_wizard.py
+++ b/product_configurator_wizard/tests/test_wizard.py
@@ -23,11 +23,13 @@ def get_attr_values(self, attr_val_ext_ids=None):
return attr_vals
- def get_wizard_write_dict(self, wizard, attr_values):
+ def get_wizard_write_dict(self, wizard, attr_values, remove_values=None):
"""Turn a series of attribute.value objects to a dictionary meant for
writing values to the product.configurator wizard"""
write_dict = {}
+ if remove_values is None:
+ remove_values = []
multi_attr_ids = wizard.product_tmpl_id.attribute_line_ids.filtered(
lambda x: x.multi).mapped('attribute_id').ids
@@ -40,6 +42,10 @@ def get_wizard_write_dict(self, wizard, attr_values):
continue
write_dict.update({field_name: val.id})
+ for val in remove_values:
+ field_name = wizard.field_prefix + str(val.attribute_id.id)
+ write_dict.update({field_name: False})
+
return write_dict
def wizard_write_proceed(self, wizard, attr_vals, value_ids=None):
@@ -54,6 +60,10 @@ def wizard_write_proceed(self, wizard, attr_vals, value_ids=None):
def test_wizard_configuration(self):
"""Test product configurator wizard"""
+ config_variants = self.env['product.product'].search([
+ ('config_ok', '=', True)
+ ])
+
# Start a new configuration wizard
wizard_obj = self.env['product.configurator'].with_context({
'active_model': 'sale.order',
@@ -87,37 +97,66 @@ def test_wizard_configuration(self):
wizard.action_next_step()
- config_variants = self.env['product.product'].search([
+ new_config_variants = self.env['product.product'].search([
('config_ok', '=', True)
])
- self.assertTrue(len(config_variants) == 1,
+ self.assertTrue(len(new_config_variants - config_variants) == 1,
"Wizard did not create a configurable variant")
+ def do_reconfigure(self, order_line, attr_vals):
+ reconfig_action = order_line.reconfigure_product()
+
+ wizard = self.env['product.configurator'].browse(
+ reconfig_action.get('res_id')
+ )
+
+ self.wizard_write_proceed(wizard, attr_vals)
+
+ # if wizard only had one step, it would already have completed
+ if wizard.exists():
+ # Cycle through steps until wizard ends
+ while wizard.action_next_step():
+ pass
+
def test_reconfiguration(self):
"""Test reconfiguration functionality of the wizard"""
+ config_variants = self.env['product.product'].search([
+ ('config_ok', '=', True)
+ ])
+
self.test_wizard_configuration()
+ existing_lines = self.so.order_line
+
order_line = self.so.order_line.filtered(
lambda l: l.product_id.config_ok
)
- reconfig_action = order_line.reconfigure_product()
+ self.do_reconfigure(order_line,
+ self.get_attr_values(['diesel', '220d'])
+ )
- wizard = self.env['product.configurator'].browse(
- reconfig_action.get('res_id')
- )
+ new_config_variants = self.env['product.product'].search([
+ ('config_ok', '=', True)
+ ])
- attr_vals = self.get_attr_values(['diesel', '220d'])
- self.wizard_write_proceed(wizard, attr_vals)
+ self.assertTrue(len(new_config_variants - config_variants) == 2,
+ "Wizard reconfiguration did not create a new variant")
- # Cycle through steps until wizard ends
- while wizard.action_next_step():
- pass
+ created_line = self.so.order_line - existing_lines
+ self.assertTrue(len(created_line) == 0,
+ "Wizard created an order line on reconfiguration")
- config_variants = self.env['product.product'].search([
+ # test that running through again with the same values does not
+ # create another variant
+ self.do_reconfigure(order_line,
+ self.get_attr_values(['diesel', '220d'])
+ )
+
+ new_config_variants = self.env['product.product'].search([
('config_ok', '=', True)
])
- self.assertTrue(len(config_variants) == 2,
- "Wizard reconfiguration did not create a new variant")
+ self.assertTrue(len(new_config_variants - config_variants) == 2,
+ "Wizard reconfiguration created a redundant variant")
diff --git a/product_configurator_wizard/tests/test_wizard_attrs.py b/product_configurator_wizard/tests/test_wizard_attrs.py
new file mode 100644
index 00000000..fc4728f4
--- /dev/null
+++ b/product_configurator_wizard/tests/test_wizard_attrs.py
@@ -0,0 +1,233 @@
+# -*- coding: utf-8 -*-
+
+from odoo.addons.product_configurator_wizard.tests.test_wizard \
+ import ConfigurationRules
+
+
+class ConfigurationAttributes(ConfigurationRules):
+
+ def setUp(self):
+ """
+ Product with 3 sizes:
+ Small or Medium allow Blue or Red, colour is required
+ Large does not allow colour selection
+ """
+ super(ConfigurationAttributes, self).setUp()
+
+ self.attr_size = self.env['product.attribute'].create(
+ {'name': 'Size'})
+ self.attr_val_small = self.env['product.attribute.value'].create(
+ {'attribute_id': self.attr_size.id,
+ 'name': 'Small',
+ }
+ )
+ self.attr_val_med = self.env['product.attribute.value'].create(
+ {'attribute_id': self.attr_size.id,
+ 'name': 'Medium',
+ }
+ )
+ self.attr_val_large = self.env['product.attribute.value'].create(
+ {'attribute_id': self.attr_size.id,
+ 'name': 'Large (Green)',
+ }
+ )
+ domain_small_med = self.env['product.config.domain'].create(
+ {'name': 'Small/Med',
+ 'domain_line_ids': [
+ (0, 0, {'attribute_id': self.attr_size.id,
+ 'condition': 'in',
+ 'operator': 'and',
+ 'value_ids':
+ [(6, 0,
+ [self.attr_val_small.id, self.attr_val_med.id]
+ )]
+ }),
+ ],
+ }
+ )
+ self.attr_colour = self.env['product.attribute'].create(
+ {'name': 'Colour'})
+ self.attr_val_blue = self.env['product.attribute.value'].create(
+ {'attribute_id': self.attr_colour.id,
+ 'name': 'Blue',
+ }
+ )
+ self.attr_val_red = self.env['product.attribute.value'].create(
+ {'attribute_id': self.attr_colour.id,
+ 'name': 'Red',
+ }
+ )
+ self.product_temp = self.env['product.template'].create(
+ {'name': 'Config Product',
+ 'config_ok': True,
+ 'type': 'product',
+ 'categ_id': self.env['ir.model.data'].xmlid_to_res_id(
+ 'product.product_category_5'
+ ),
+ 'attribute_line_ids': [
+ (0, 0, {'attribute_id': self.attr_size.id,
+ 'value_ids': [
+ (6, 0, self.attr_size.value_ids.ids),
+ ],
+ 'required': True,
+ }),
+ (0, 0, {'attribute_id': self.attr_colour.id,
+ 'value_ids': [
+ (6, 0, self.attr_colour.value_ids.ids),
+ ],
+ 'required': False,
+ })
+ ],
+ }
+ )
+ self.template_colour_line = \
+ self.product_temp.attribute_line_ids.filtered(
+ lambda a: a.attribute_id == self.attr_colour)
+ self.env['product.config.line'].create({
+ 'product_tmpl_id': self.product_temp.id,
+ 'attribute_line_id': self.template_colour_line.id,
+ 'value_ids': [(6, 0, self.attr_colour.value_ids.ids)],
+ 'domain_id': domain_small_med.id,
+ })
+
+ def test_configurations_option_or_not_reqd(self):
+ # Start a new configuration wizard
+ wizard_obj = self.env['product.configurator'].with_context({
+ 'active_model': 'sale.order',
+ 'active_id': self.so.id
+ # 'default_order_id': self.so.id
+ })
+
+ wizard = wizard_obj.create({'product_tmpl_id': self.product_temp.id})
+ wizard.action_next_step()
+
+ dynamic_fields = {}
+ for attribute_line in self.product_temp.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
+ field_name_colour = '%s%s' % (
+ wizard.field_prefix,
+ self.attr_colour.id
+ )
+ ro_name_colour = '%s%s' % (
+ wizard.ro_field_prefix,
+ self.attr_colour.id
+ )
+ reqd_name_colour = '%s%s' % (
+ wizard.reqd_field_prefix,
+ self.attr_colour.id
+ )
+
+ # Define small without colour specified
+ self.wizard_write_proceed(wizard, [self.attr_val_small])
+ new_variant = self.product_temp.product_variant_ids
+ self.assertTrue(len(new_variant) == 1 and
+ set(new_variant.attribute_value_ids.ids) ==
+ set([self.attr_val_small.id]),
+ "Wizard did not accurately create a variant with "
+ "optional value undefined")
+ config_variants = self.product_temp.product_variant_ids
+
+ order_line = self.so.order_line.filtered(
+ lambda l: l.product_id.config_ok
+ )
+
+ # Redefine to medium without colour
+ self.do_reconfigure(order_line, [self.attr_val_med])
+ new_variant = self.product_temp.product_variant_ids - config_variants
+ self.assertTrue(len(new_variant) == 1 and
+ set(new_variant.attribute_value_ids.ids) ==
+ set([self.attr_val_med.id]),
+ "Wizard did not accurately reconfigure a variant with "
+ "optional value undefined")
+ config_variants = self.product_temp.product_variant_ids
+
+ # Redefine to medium blue
+ self.do_reconfigure(order_line, [self.attr_val_blue])
+ new_variant = self.product_temp.product_variant_ids - config_variants
+ self.assertTrue(len(new_variant) == 1 and
+ set(new_variant.attribute_value_ids.ids) ==
+ set([self.attr_val_med.id, self.attr_val_blue.id]),
+ "Wizard did not accurately reconfigure a variant with "
+ "to add an optional value")
+ config_variants = self.product_temp.product_variant_ids
+
+ # Redefine to large - should remove colour, as this is invalid
+ reconfig_action = order_line.reconfigure_product()
+ wizard = self.env['product.configurator'].browse(
+ reconfig_action.get('res_id')
+ )
+ attr_large_dict = self.get_wizard_write_dict(wizard,
+ [self.attr_val_large])
+ attr_blue_dict = self.get_wizard_write_dict(wizard,
+ [self.attr_val_blue])
+ oc_vals = dynamic_fields.copy()
+ oc_vals.update({'id': wizard.id})
+ oc_vals.update(dict(attr_blue_dict, **attr_large_dict))
+ oc_result = wizard.onchange(
+ oc_vals,
+ attr_large_dict.keys()[0],
+ {}
+ )
+ self.assertTrue(field_name_colour in oc_result['value'] and
+ not oc_result['value'][field_name_colour],
+ "Colour should have been cleared by wizard"
+ )
+ self.assertTrue(ro_name_colour in oc_result['value'] and
+ oc_result['value'][ro_name_colour],
+ "Colour should have become readonly"
+ )
+ self.assertFalse(oc_result['value'].get(reqd_name_colour, False),
+ "Colour should not have become required"
+ )
+
+ vals = self.get_wizard_write_dict(wizard, [self.attr_val_large],
+ remove_values=[self.attr_val_blue])
+ wizard.write(vals)
+ wizard.action_next_step()
+ if wizard.exists():
+ while wizard.action_next_step():
+ pass
+ new_variant = self.product_temp.product_variant_ids - config_variants
+ self.assertTrue(len(new_variant) == 1 and
+ set(new_variant.attribute_value_ids.ids) ==
+ set([self.attr_val_large.id]),
+ "Wizard did not accurately reconfigure a variant "
+ "to remove invalid attribute")
+
+ # Now test reqd attribute changes.
+ last_variant = new_variant
+ self.template_colour_line.write({'required': True})
+
+ # Redefine to medium - should make colour required
+ reconfig_action = order_line.reconfigure_product()
+ wizard = self.env['product.configurator'].browse(
+ reconfig_action.get('res_id')
+ )
+ attr_med_dict = self.get_wizard_write_dict(wizard,
+ [self.attr_val_med])
+ oc_vals = dynamic_fields.copy()
+ oc_vals.update({'id': wizard.id})
+ oc_vals.update(attr_med_dict)
+ oc_result = wizard.onchange(
+ oc_vals,
+ attr_med_dict.keys()[0],
+ {}
+ )
+ self.assertTrue(oc_result['value'].get(reqd_name_colour),
+ "Colour should have become required"
+ )
+ # Redefine to large - should make colour not required, despite master
+ # file
+ vals = self.get_wizard_write_dict(wizard, [self.attr_val_large])
+ wizard.write(vals)
+ wizard.action_next_step()
+ if wizard.exists():
+ while wizard.action_next_step():
+ pass
+ self.assertTrue(order_line.product_id == last_variant,
+ "Wizard did not end up with the same "
+ "variant")
diff --git a/product_configurator_wizard/wizard/product_configurator.py b/product_configurator_wizard/wizard/product_configurator.py
index 5e0055d0..90501188 100644
--- a/product_configurator_wizard/wizard/product_configurator.py
+++ b/product_configurator_wizard/wizard/product_configurator.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from lxml import etree
+import ast
from odoo.osv import orm
from odoo import models, fields, api, _
@@ -21,6 +22,9 @@ class ProductConfigurator(models.TransientModel):
# Prefix for the dynamicly injected fields
field_prefix = '__attribute-'
custom_field_prefix = '__custom-'
+ ro_field_prefix = '__ro-'
+ reqd_field_prefix = '__reqd-'
+ invis_field_prefix = '__invis-'
# TODO: Since the configuration process can take a bit of time
# depending on complexity and AFK time we must increase the lifespan
@@ -78,13 +82,13 @@ def onchange_product_tmpl(self):
def get_onchange_domains(self, values, cfg_val_ids):
"""Generate domains to be returned by onchange method in order
- to restrict the availble values of dynamically inserted fields
+ to restrict the available values of dynamically inserted fields
- :param values: values argument passed to onchance wrapper
+ :param values: values argument passed to onchange wrapper
:cfg_val_ids: current configuration passed as a list of value_ids
(usually in the form of db value_ids + interface value_ids)
- :returns: a dictionary of domains returned by onchance method
+ :returns: a dictionary of domains returned by onchange method
"""
domains = {}
for line in self.product_tmpl_id.attribute_line_ids.sorted():
@@ -109,7 +113,33 @@ def get_onchange_domains(self, values, cfg_val_ids):
continue
return domains
- def get_form_vals(self, dynamic_fields, domains):
+ def get_support_fields(self, k, available_val_ids, attribute_line):
+ """ Sets the readonly, required, and invisible boolean values
+ which drive the behaviour of individual fields.
+
+ :param k: the field name of the dynamic field
+ :param available_val_ids: list of the possible value ids for the field
+ :param attribute_line: corresponding attribute line definition from the
+ product template
+
+ :returns vals: a dictionary with three values
+ """
+ attribute_id = int(k.replace(self.field_prefix, ''))
+ ro_field = self.ro_field_prefix + str(attribute_id)
+ reqd_field = self.reqd_field_prefix + str(attribute_id)
+ invis_field = self.invis_field_prefix + str(attribute_id)
+
+ vals = {}
+ vals[ro_field] = len(available_val_ids) == 0
+ if attribute_line.required:
+ required = len(available_val_ids) > 0
+ else:
+ required = False
+ vals[reqd_field] = required
+ vals[invis_field] = False
+ return vals
+
+ 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.
@@ -121,19 +151,31 @@ 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()
for k, v in dynamic_fields.iteritems():
- if not v:
- continue
+ attribute_id = int(k.replace(self.field_prefix, ''))
+ attribute_line = self.product_tmpl_id.attribute_line_ids.filtered(
+ lambda a: a.attribute_id.id == attribute_id)
available_val_ids = domains[k][0][2]
- if isinstance(v, list):
+
+ vals.update(
+ self.get_support_fields(k, available_val_ids, attribute_line)
+ )
+
+ if v and 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:
+ elif v and v not in available_val_ids:
dynamic_fields.update({k: None})
vals[k] = None
+ v = None
+ if not v:
+ if attribute_line.required and not attribute_line.multi and \
+ len(available_val_ids) == 1:
+ dynamic_fields.update({k: available_val_ids[0]})
+ vals[k] = available_val_ids[0]
product_img = self.product_tmpl_id.get_config_image_obj(
dynamic_fields.values())
@@ -159,12 +201,7 @@ def onchange(self, values, field_name, field_onchange):
view_val_ids = set()
view_attribute_ids = set()
- try:
- cfg_step_id = int(self.state)
- cfg_step = self.product_tmpl_id.config_step_line_ids.filtered(
- lambda x: x.id == cfg_step_id)
- except:
- cfg_step = self.env['product.config.step.line']
+ cfg_step = self.get_cfg_step()
dynamic_fields = {
k: v for k, v in values.iteritems() if k.startswith(
@@ -196,7 +233,43 @@ 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)
+
+ # See if any of the dynamic_fields were modified by the get_form_vals
+ # (cleared or set), they may change domains!
+ modified_dynamics = {
+ k: v for k, v in vals.iteritems() if k in dynamic_fields
+ }
+ while modified_dynamics:
+ dynamic_fields.update(modified_dynamics)
+ for k, v in modified_dynamics.iteritems():
+ attr_id = int(k.split(self.field_prefix)[1])
+ # First take out what the value may have been before
+ view_val_ids -= set(self.env['product.attribute.value'].search(
+ [('attribute_id', '=', attr_id)]).ids)
+ # And if set, now put it back in
+ 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)
+
+ # To ensure the full suite of support vals is written if any are
+ # changed by onchange.
+ if self.stored_support_vals:
+ vals['stored_support_vals'] = self.stored_support_vals + ' '
return {'value': vals, 'domain': domains}
attribute_line_ids = fields.One2many(
@@ -219,12 +292,13 @@ def onchange(self, values, field_name, field_onchange):
comodel_name='product.product',
readonly=True,
string='Product Variant',
- help='Set only when re-configuring a existing variant'
+ help='Set only when re-configuring an existing variant'
)
product_img = fields.Binary(
compute='_compute_cfg_image',
readonly=True
)
+ stored_support_vals = fields.Char()
state = FreeSelection(
selection='get_state_selection',
default='select',
@@ -253,25 +327,25 @@ def fields_get(self, allfields=None, attributes=None):
# Get the wizard object from the database
wiz = self.browse(wizard_id)
- active_step_id = wiz.state
# If the product template is not set it is still at the 1st step
if not wiz.product_tmpl_id:
return res
- cfg_step_lines = wiz.product_tmpl_id.config_step_line_ids
-
- try:
- # Get only the attribute lines for the next step if defined
- active_step_line = cfg_step_lines.filtered(
- lambda l: l.id == int(active_step_id))
- if active_step_line:
- attribute_lines = active_step_line.attribute_line_ids
- else:
- attribute_lines = wiz.product_tmpl_id.attribute_line_ids
- except:
- # If no configuration steps exist then get all attribute lines
- attribute_lines = wiz.product_tmpl_id.attribute_line_ids
+ # At the moment, this is not being used, so let's not call it
+ # unnecessarily
+# cfg_step_lines = wiz.product_tmpl_id.config_step_line_ids
+# try:
+# # Get only the attribute lines for the next step if defined
+# active_step_line = cfg_step_lines.filtered(
+# lambda l: l.id == int(active_step_id))
+# if active_step_line:
+# attribute_lines = active_step_line.attribute_line_ids
+# else:
+# attribute_lines = wiz.product_tmpl_id.attribute_line_ids
+# except:
+# # If no configuration steps exist then get all attribute lines
+# attribute_lines = wiz.product_tmpl_id.attribute_line_ids
attribute_lines = wiz.product_tmpl_id.attribute_line_ids
@@ -310,7 +384,7 @@ def fields_get(self, allfields=None, attributes=None):
field_types = self.env['ir.model.fields']._get_field_types()
if attribute.custom_type:
- custom_type = line.attribute_id.custom_type
+ custom_type = attribute.custom_type
# TODO: Rename int to integer in values
if custom_type == 'int':
field_type = 'integer'
@@ -320,7 +394,7 @@ def fields_get(self, allfields=None, attributes=None):
# TODO: Implement custom string on custom attribute
res[self.custom_field_prefix + str(attribute.id)] = dict(
default_attrs,
- string="Custom",
+ string='%s Custom' % (attribute.name,),
type=field_type,
sequence=line.sequence,
)
@@ -331,11 +405,24 @@ def fields_get(self, allfields=None, attributes=None):
default_attrs,
type='many2many' if line.multi else 'many2one',
domain=[('id', 'in', value_ids)],
- string=line.attribute_id.name,
+ string=attribute.name,
relation='product.attribute.value',
sequence=line.sequence,
)
+ res[self.ro_field_prefix + str(attribute.id)] = dict(
+ default_attrs,
+ type='boolean',
+ )
+ res[self.reqd_field_prefix + str(attribute.id)] = dict(
+ default_attrs,
+ type='boolean',
+ )
+ res[self.invis_field_prefix + str(attribute.id)] = dict(
+ default_attrs,
+ type='boolean',
+ )
+
return res
@api.model
@@ -361,17 +448,40 @@ def fields_view_get(self, view_id=None, view_type='form',
k: v for k, v in fields.iteritems() if k.startswith(
self.field_prefix) or k.startswith(self.custom_field_prefix)
}
+ support_fields = {
+ k: v for k, v in fields.iteritems() if
+ k.startswith(self.ro_field_prefix) or
+ k.startswith(self.reqd_field_prefix) or
+ k.startswith(self.invis_field_prefix)
+ }
res['fields'].update(dynamic_fields)
- mod_view = self.add_dynamic_fields(res, dynamic_fields, wiz)
+ res['fields'].update(support_fields)
+ mod_view = self.add_dynamic_fields(
+ res, dynamic_fields, support_fields, wiz)
# Update result dict from super with modified view
res.update({'arch': etree.tostring(mod_view)})
+ wiz_vals = wiz.with_context(from_fvg=True).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()}
+ cfg_step = wiz.get_cfg_step()
+ vals = wiz.get_form_vals(dynamic_field_vals, domains, cfg_step)
+ if vals:
+ wiz.write(vals)
return res
@api.model
- def add_dynamic_fields(self, res, dynamic_fields, wiz):
+ def add_dynamic_fields(self, res, dynamic_fields, support_fields, wiz):
""" Create the configuration view using the dynamically generated
fields in fields_get()
"""
@@ -405,6 +515,9 @@ def add_dynamic_fields(self, res, dynamic_fields, wiz):
attribute_id = attr_line.attribute_id.id
field_name = self.field_prefix + str(attribute_id)
custom_field = self.custom_field_prefix + str(attribute_id)
+ ro_field = self.ro_field_prefix + str(attribute_id)
+ reqd_field = self.reqd_field_prefix + str(attribute_id)
+ invis_field = self.invis_field_prefix + str(attribute_id)
# Check if the attribute line has been added to the db fields
if field_name not in dynamic_fields:
@@ -415,61 +528,27 @@ def add_dynamic_fields(self, res, dynamic_fields, wiz):
# attrs property for dynamic fields
attrs = {
- 'readonly': ['|'],
+ 'readonly': [],
'required': [],
- 'invisible': ['|']
+ 'invisible': []
}
- if config_steps:
- cfg_step_ids = [str(id) for id in config_steps.ids]
- attrs['invisible'].append(('state', 'not in', cfg_step_ids))
- attrs['readonly'].append(('state', 'not in', cfg_step_ids))
-
- # If attribute is required make it so only in the proper step
- if attr_line.required:
- attrs['required'].append(('state', 'in', cfg_step_ids))
-
if attr_line.custom:
- pass
# TODO: Implement restrictions for ranges
+ pass
- config_lines = wiz.product_tmpl_id.config_line_ids
- dependencies = config_lines.filtered(
- lambda cl: cl.attribute_line_id == attr_line)
-
- # If an attribute field depends on another field from the same
- # configuration step then we must use attrs to enable/disable the
- # required and readonly depending on the value entered in the
- # dependee
-
- if attr_line.value_ids <= dependencies.mapped('value_ids'):
- attr_depends = {}
- domain_lines = dependencies.mapped('domain_id.domain_line_ids')
- for domain_line in domain_lines:
- attr_id = domain_line.attribute_id.id
- attr_field = self.field_prefix + str(attr_id)
- attr_lines = wiz.product_tmpl_id.attribute_line_ids
- # If the fields it depends on are not in the config step
- if config_steps and str(attr_line.id) != wiz.state:
- continue
- if attr_field not in attr_depends:
- attr_depends[attr_field] = set()
- if domain_line.condition == 'in':
- attr_depends[attr_field] |= set(
- domain_line.value_ids.ids)
- elif domain_line.condition == 'not in':
- val_ids = attr_lines.filtered(
- lambda l: l.attribute_id.id == attr_id).value_ids
- val_ids = val_ids - domain_line.value_ids
- attr_depends[attr_field] |= set(val_ids.ids)
-
- for dependee_field, val_ids in attr_depends.iteritems():
- if not val_ids:
- continue
- attrs['readonly'].append(
- (dependee_field, 'not in', list(val_ids)))
- attrs['required'].append(
- (dependee_field, 'in', list(val_ids)))
+ attrs['readonly'].append(
+ (ro_field, '=', True))
+ attrs['required'].append(
+ (reqd_field, '=', True))
+ attrs['invisible'].append(
+ (invis_field, '=', True))
+ if config_steps:
+ cfg_step_ids = [str(id) for id in config_steps.ids]
+ attrs['invisible'] = ['|'] + attrs['invisible'][:] +\
+ [('state', 'not in', cfg_step_ids)]
+ attrs['required'] = ['&'] + attrs['required'][:] +\
+ [('state', 'in', cfg_step_ids)]
# Create the new field in the view
node = etree.Element(
@@ -486,9 +565,6 @@ def add_dynamic_fields(self, res, dynamic_fields, wiz):
})
)
- if attr_line.required and not config_steps:
- node.attrib['required'] = '1'
-
field_type = dynamic_fields[field_name].get('type')
if field_type == 'many2many':
node.attrib['widget'] = 'many2many_tags'
@@ -508,13 +584,10 @@ def add_dynamic_fields(self, res, dynamic_fields, wiz):
else:
field_val = custom_option_id
- attrs['readonly'] += [(field_name, '!=', field_val)]
- attrs['invisible'] += [(field_name, '!=', field_val)]
+ attrs['invisible'] = ['|'] + attrs['invisible'][:] + \
+ [(field_name, '!=', field_val)]
attrs['required'] += [(field_name, '=', field_val)]
- if config_steps:
- attrs['required'] += [('state', 'in', cfg_step_ids)]
-
# TODO: Add a field2widget mapper
if attr_line.attribute_id.custom_type == 'color':
widget = 'color'
@@ -527,12 +600,50 @@ def add_dynamic_fields(self, res, dynamic_fields, wiz):
orm.setup_modifiers(node)
xml_dynamic_form.append(node)
+ node = etree.Element(
+ "field",
+ name=ro_field,
+ invisible='1',
+ )
+ orm.setup_modifiers(node)
+ xml_dynamic_form.append(node)
+
+ node = etree.Element(
+ "field",
+ name=reqd_field,
+ invisible='1',
+ )
+ orm.setup_modifiers(node)
+ xml_dynamic_form.append(node)
+
+ node = etree.Element(
+ "field",
+ name=invis_field,
+ invisible='1',
+ )
+ orm.setup_modifiers(node)
+ xml_dynamic_form.append(node)
+
return xml_view
+ @api.multi
+ def get_cfg_step(self):
+ """Attempt to return product.config.step.line object which
+ is encoded in the wizard state string"""
+ self.ensure_one()
+ try:
+ cfg_step_id = int(self.state)
+ except:
+ cfg_step = self.env['product.config.step.line']
+ else:
+ cfg_step = self.product_tmpl_id.config_step_line_ids.filtered(
+ lambda x: x.id == cfg_step_id)
+ return cfg_step
+
@api.model
def create(self, vals):
"""Sets the configuration values of the product_id if given (if any).
- This is used in reconfiguration of a existing variant"""
+ This is used in reconfiguration of an existing variant"""
vals.update(user_id=self.env.uid)
if 'product_id' in vals:
@@ -562,7 +673,13 @@ def read(self, fields=None, load='_classic_read'):
]
dynamic_fields = attr_vals + custom_attr_vals
- fields = [f for f in fields if f not in dynamic_fields]
+ support_fields = [
+ f for f in fields if f.startswith(self.ro_field_prefix) or
+ f.startswith(self.reqd_field_prefix) or
+ f.startswith(self.invis_field_prefix)
+ ]
+ fields = [
+ f for f in fields if f not in dynamic_fields + support_fields]
custom_ext_id = 'product_configurator.custom_attribute_value'
custom_val = self.env.ref(custom_ext_id)
@@ -610,6 +727,13 @@ def read(self, fields=None, load='_classic_read'):
except:
continue
res[0].update(dynamic_vals)
+
+ if not self.env.context.get('from_fvg'):
+ for res_data in res:
+ if res_data.get('stored_support_vals'):
+ res_data.update(
+ ast.literal_eval(res_data['stored_support_vals'])
+ )
return res
@api.multi
@@ -622,6 +746,20 @@ def write(self, vals):
custom_ext_id = 'product_configurator.custom_attribute_value'
custom_val = self.env.ref(custom_ext_id)
+ if vals.get('stored_support_vals'):
+ support_vals = ast.literal_eval(vals['stored_support_vals'])
+ else:
+ support_vals = {}
+ support_vals.update(
+ {k: v for k, v in vals.iteritems()
+ if k.startswith(self.ro_field_prefix) or
+ k.startswith(self.reqd_field_prefix) or
+ k.startswith(self.invis_field_prefix)
+ }
+ )
+ if support_vals:
+ vals.update({'stored_support_vals': str(support_vals)})
+
attr_val_dict = {}
custom_val_dict = {}
@@ -629,6 +767,18 @@ def write(self, vals):
attr_id = attr_line.attribute_id.id
field_name = self.field_prefix + str(attr_id)
custom_field_name = self.custom_field_prefix + str(attr_id)
+ ro_field_name = self.ro_field_prefix + str(attr_id)
+ reqd_field_name = self.reqd_field_prefix + str(attr_id)
+ invis_field_name = self.invis_field_prefix + str(attr_id)
+
+ if ro_field_name in vals and vals.get(ro_field_name):
+ # readonly - must have an empty domain. Ensure we
+ # store an empty value...
+ vals[field_name] = False
+ vals[custom_field_name] = False
+ vals.pop(ro_field_name, None)
+ vals.pop(reqd_field_name, None)
+ vals.pop(invis_field_name, None)
if field_name not in vals and custom_field_name not in vals:
continue
@@ -646,7 +796,7 @@ def write(self, vals):
field_val = vals[field_name]
else:
raise Warning(
- _('An error occursed while parsing value for '
+ _('An error occurred while parsing value for '
'attribute %s' % attr_line.attribute_id.name)
)
attr_val_dict.update({
@@ -672,10 +822,8 @@ def write(self, vals):
attr_val_dict.update({attr_id: False})
# Remove dynamic field from value list to prevent error
- if field_name in vals:
- del vals[field_name]
- if custom_field_name in vals:
- del vals[custom_field_name]
+ vals.pop(field_name, None)
+ vals.pop(custom_field_name, None)
self.config_session.update_config(attr_val_dict, custom_val_dict)
res = super(ProductConfigurator, self).write(vals)
@@ -722,14 +870,7 @@ def action_next_step(self):
self.state = 'configure'
return wizard_action
- try:
- cfg_step_line_id = int(self.state)
- except:
- cfg_step_line_id = None
-
- active_cfg_line_id = cfg_step_lines.filtered(
- lambda x: x.id == cfg_step_line_id).id
-
+ active_cfg_line_id = self.get_cfg_step().id
adjacent_steps = self.product_tmpl_id.get_adjacent_steps(
self.value_ids.ids, active_cfg_line_id)
@@ -766,13 +907,7 @@ def action_previous_step(self):
if not cfg_step_lines:
return wizard_action
- try:
- cfg_step_line_id = int(self.state)
- active_cfg_line_id = cfg_step_lines.filtered(
- lambda x: x.id == cfg_step_line_id).id
- except:
- active_cfg_line_id = None
-
+ active_cfg_line_id = self.get_cfg_step().id
adjacent_steps = self.product_tmpl_id.get_adjacent_steps(
self.value_ids.ids, active_cfg_line_id)
diff --git a/product_configurator_wizard/wizard/product_configurator_view.xml b/product_configurator_wizard/wizard/product_configurator_view.xml
index 88580774..4368b2ed 100644
--- a/product_configurator_wizard/wizard/product_configurator_view.xml
+++ b/product_configurator_wizard/wizard/product_configurator_view.xml
@@ -21,6 +21,7 @@
+