[FIX] models: process onchange methods on new records in the order of the view

The onchange() on new records processes fields in non-predictable order.  This
is problematic when onchange methods are designed to be applied after each
other.  The expected order is presumed to be the one of the fields in the view.

In order to implement this behavior, the JS client invokes method onchange()
with the list of fields (in view order) instead of False.  The server then uses
that order for evaluating the onchange methods.

This fixes #4897.
This commit is contained in:
Raphael Collet 2015-02-09 17:35:17 +01:00
parent 372f4a82d5
commit 2067a206ec
2 changed files with 21 additions and 10 deletions

View File

@ -452,8 +452,10 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
return "";
};
self._onchange_fields = [];
self._onchange_specs = {};
_.each(this.fields, function(field, name) {
self._onchange_fields.push(name);
self._onchange_specs[name] = find(name, field.node);
_.each(field.field.views, function(view) {
_.each(view.fields, function(_, subname) {
@ -490,7 +492,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
var change_spec = widget ? onchange_specs[widget.name] : null;
if (!widget || (!_.isEmpty(change_spec) && change_spec !== "0")) {
var ids = [],
trigger_field_name = widget ? widget.name : false,
trigger_field_name = widget ? widget.name : self._onchange_fields,
values = self._get_onchange_values(),
context = new instance.web.CompoundContext(self.dataset.get_context());

View File

@ -5734,13 +5734,20 @@ class BaseModel(object):
:param values: dictionary mapping field names to values, giving the
current state of modification
:param field_name: name of the modified field_name
:param field_name: name of the modified field, or list of field
names (in view order), or False
:param field_onchange: dictionary mapping field names to their
on_change attribute
"""
env = self.env
if isinstance(field_name, list):
names = field_name
elif field_name:
names = [field_name]
else:
names = []
if field_name and field_name not in self._fields:
if not all(name in self._fields for name in names):
return {}
# determine subfields for field.convert_to_write() below
@ -5759,23 +5766,24 @@ class BaseModel(object):
# attach `self` with a different context (for cache consistency)
record._origin = self.with_context(__onchange=True)
# determine which field should be triggered an onchange
todo = set([field_name]) if field_name else set(values)
# determine which field(s) should be triggered an onchange
todo = list(names) or list(values)
done = set()
# dummy assignment: trigger invalidations on the record
for name in todo:
value = record[name]
field = self._fields[name]
if not field_name and field.type == 'many2one' and field.delegate and not value:
if field.type == 'many2one' and field.delegate and not value:
# do not nullify all fields of parent record for new records
continue
record[name] = value
result = {'value': {}}
# process names in order (or the keys of values if no name given)
while todo:
name = todo.pop()
name = todo.pop(0)
if name in done:
continue
done.add(name)
@ -5799,7 +5807,7 @@ class BaseModel(object):
result['value'][name] = field.convert_to_write(
newval, record._origin, subfields.get(name),
)
todo.add(name)
todo.append(name)
else:
# keep result: newval may have been dirty before
pass
@ -5809,14 +5817,15 @@ class BaseModel(object):
result['value'][name] = field.convert_to_write(
newval, record._origin, subfields.get(name),
)
todo.add(name)
todo.append(name)
else:
# clean up result to not return another value
result['value'].pop(name, None)
# At the moment, the client does not support updates on a *2many field
# while this one is modified by the user.
if field_name and self._fields[field_name].type in ('one2many', 'many2many'):
if field_name and not isinstance(field_name, list) and \
self._fields[field_name].type in ('one2many', 'many2many'):
result['value'].pop(field_name, None)
return result