diff --git a/addons/mail/tests/test_mail_composer.py b/addons/mail/tests/test_mail_composer.py index ee24131263f..0214f5e9010 100644 --- a/addons/mail/tests/test_mail_composer.py +++ b/addons/mail/tests/test_mail_composer.py @@ -87,7 +87,7 @@ class TestMailComposerForm(TestMailComposer): })) form.body = '
Hello
' self.assertEqual( - form.partner_ids._get_ids(), partner_classic.ids, + form.partner_ids.ids, partner_classic.ids, 'Default populates the field' ) saved_form = form.save() @@ -119,7 +119,7 @@ class TestMailComposerForm(TestMailComposer): })) form.body = 'Hello
' self.assertEqual( - sorted(form.partner_ids._get_ids()), + sorted(form.partner_ids.ids), sorted((partner_private + partner_classic).ids), 'Default populates the field' ) @@ -191,7 +191,7 @@ class TestMailComposerForm(TestMailComposer): self.assertEqual(new_partner.type, 'contact', 'Should create a new contact') self.assertEqual( - sorted(form.partner_ids._get_ids()), + sorted(form.partner_ids.ids), sorted((partner_private + partner_classic + partner_private_2 + new_partner).ids), 'Template populates the field with both email_to and partner_to' ) diff --git a/addons/mass_mailing/tests/test_mailing_list.py b/addons/mass_mailing/tests/test_mailing_list.py index f65b0fcff1f..53514b40f35 100644 --- a/addons/mass_mailing/tests/test_mailing_list.py +++ b/addons/mass_mailing/tests/test_mailing_list.py @@ -29,7 +29,7 @@ class TestMailingContactToList(MassMailCommon): # create wizard with context values wizard_form = Form(self.env['mailing.contact.to.list'].with_context(default_contact_ids=contacts.ids)) - self.assertEqual(wizard_form.contact_ids._get_ids(), contacts.ids) + self.assertEqual(wizard_form.contact_ids.ids, contacts.ids) # set mailing list and add contacts wizard_form.mailing_list_id = mailing diff --git a/odoo/addons/test_testing_utilities/tests/test_form_impl.py b/odoo/addons/test_testing_utilities/tests/test_form_impl.py index b48f8a2666e..9e553797471 100644 --- a/odoo/addons/test_testing_utilities/tests/test_form_impl.py +++ b/odoo/addons/test_testing_utilities/tests/test_form_impl.py @@ -497,7 +497,7 @@ class TestO2M(TransactionCase): def test_o2m_remove(self): def commands(): - return [c[0] for c in f._values['line_ids']] + return [c[0] for c in f._values['line_ids'].to_commands()] with Form(self.env['test_testing_utilities.onchange_count']) as f: self.assertEqual(f.count, 0) @@ -525,9 +525,9 @@ class TestO2M(TransactionCase): with Form(r) as f: f.line_ids.remove(0) - self.assertEqual(commands(), [2, 1]) + self.assertEqual(commands(), [2]) f.count = 1 - self.assertEqual(commands(), [0, 2, 2], "should contain 1 '0' command and 2 deletions") + self.assertEqual(commands(), [0, 2, 2], "should contain 1 creation and 2 deletions") self.assertEqual(len(r.line_ids), 1) def test_o2m_self_recursive(self): diff --git a/odoo/tests/form.py b/odoo/tests/form.py index 096803ab5ec..66965788b95 100644 --- a/odoo/tests/form.py +++ b/odoo/tests/form.py @@ -284,12 +284,6 @@ class Form: # call onchange with no field; this retrieves default values, applies # onchanges and return the result self._perform_onchange() - # fill in whatever fields are still missing with falsy values - vals.update({ - field_name: _cleanup_from_default(field_info['type'], False) - for field_name, field_info in self._view['fields'].items() - if field_name not in vals - }) # mark all fields as modified self._values._changed.update(self._view['fields']) @@ -348,23 +342,6 @@ class Form: vals = self._values eval_context = self._get_eval_context(vals) - for name, desc in self._view['fields'].items(): - if name in eval_context and desc['type'] in ('one2many', 'many2many'): - ids = set() - for command in eval_context.get(name): - if command[0] == Command.LINK: - ids.add(command[1]) - elif command[0] == Command.CREATE: - ids.add(command[1]) - elif command[0] == Command.UPDATE: - ids.add(command[1]) - elif command[0] == Command.UNLINK: - ids.remove(command[1]) - elif command[0] == Command.CLEAR: - ids.clear() - elif command[0] == Command.SET: - ids = set(command[2]) - eval_context[name] = list(ids) return bool(safe_eval(expr, eval_context)) @@ -446,14 +423,14 @@ class Form: def _get_values(self, mode, values=None, view=None, modifiers_values=None, parent_link=None): """ Validate & extract values, recursively in order to handle o2ms properly. - :param mode: can be ``"save"`` (validate and return non-readonly modified fields) - or ``"all"`` (return all field values) + :param mode: can be ``"save"`` (validate and return non-readonly modified fields), + ``"onchange"`` (return modified fields) or ``"all"`` (return all field values) :param UpdateDict values: values of the record to extract :param view: view info :param dict modifiers_values: defaults to ``values``, but o2ms need some additional massaging :param parent_link: optional field representing "parent" """ - assert mode in ('save', 'all') + assert mode in ('save', 'onchange', 'all') if values is None: values = self._values @@ -465,7 +442,7 @@ class Form: result = {} for field_name, field_info in view['fields'].items(): - if field_name == 'id': + if field_name == 'id' or field_name not in values: continue value = values[field_name] @@ -483,7 +460,7 @@ class Form: raise AssertionError(f"{field_name} is a required field ({view['modifiers'][field_name]})") # skip unmodified fields unless all_fields - if mode == 'save' and field_name not in values._changed: + if mode in ('save', 'onchange') and field_name not in values._changed: continue if mode == 'save' and self._get_modifier(field_name, 'readonly', view=view, vals=modifiers_values): @@ -496,43 +473,27 @@ class Form: continue if field_info['type'] == 'one2many': - subview = field_info['edition_view'] - subfields = subview['fields'] - res = [] - for (cmd, rid, vs) in value: - if cmd == Command.UPDATE and not vs: - cmd, vs = Command.LINK, False - elif cmd in (Command.CREATE, Command.UPDATE): - vs = vs or {} + if mode == 'all': + # in the context of an eval, format it as a list of ids + value = list(value) + else: + subview = field_info['edition_view'] + value = value.to_commands(lambda vals: self._get_values( + mode, vals, subview, + modifiers_values={'id': False, **vals, 'parent': Dotter(values)}, + # related o2m don't have a relation_field + parent_link=field_info.get('relation_field'), + )) - missing = subfields.keys() - vs.keys() - # FIXME: maybe do this during initial loading instead? - if missing: - comodel = self._env[field_info['relation']] - if cmd == Command.CREATE: - vs.update(dict.fromkeys(missing, False)) - vs.update({ - key: _cleanup_from_default(subfields[key], val) - for key, val in comodel.default_get(list(missing)).items() - }) - else: - vs.update(read_record( - comodel.browse(rid), - {key: val for key, val in subfields.items() if key not in vs}, - )) - if not isinstance(vs, UpdateDict): - vs = UpdateDict(vs) - vs._changed.update(vs) - vs = self._get_values( - mode, vs, subview, - modifiers_values={'id': False, **vs, 'parent': Dotter(values)}, - # related o2m don't have a relation_field - parent_link=field_info.get('relation_field'), - ) - res.append((cmd, rid, vs)) - value = res + elif field_info['type'] == 'many2many': + if mode == 'all': + # in the context of an eval, format it as a list of ids + value = list(value) + else: + value = value.to_commands() result[field_name] = value + return result def _perform_onchange(self, field_name=None): @@ -540,11 +501,13 @@ class Form: # marks onchange source as changed if field_name: + field_names = [field_name] self._values._changed.add(field_name) + else: + field_names = [] # skip calling onchange() if there's no on_change on the field - spec = self._view['onchange'] - if field_name and not spec[field_name]: + if field_name and not self._view['onchange'][field_name]: return record = self._record @@ -555,133 +518,61 @@ class Form: if context: record = record.with_context(**context) - result = record.onchange(self._onchange_values(), field_name, spec) + values = self._get_onchange_values() + fields_spec = get_fields_spec(self._view['fields']) + result = record.onchange2(values, field_names, fields_spec) self._env.flush_all() self._env.clear() # discard cache and pending recomputations if result.get('warning'): _logger.getChild('onchange').warning("%(title)s %(message)s", result['warning']) - # mark onchange output as changed - fields = self._view['fields'] - values = { - key: self._cleanup_onchange(fields[key], val, self._values.get(key)) - for key, val in result.get('value', {}).items() - if key in fields - } - self._values.update(values) - self._values._changed.update(values) + if not field_name: + # fill in whatever fields are still missing with falsy values + self._values.update({ + field_name: _cleanup_from_default(field_info['type'], False) + for field_name, field_info in self._view['fields'].items() + if field_name not in self._values + }) + + if result.get('value'): + self._apply_onchange(result['value']) + return result - def _onchange_values(self): - return self._onchange_values_(self._view['fields'], self._values) + def _get_onchange_values(self): + """ Return modified field values for onchange. """ + return self._get_values('onchange') - def _onchange_values_(self, fields, values): - """ Recursively cleanup o2m values for onchanges: + def _apply_onchange(self, values): + self._apply_onchange_(self._values, self._view['fields'], values) - * if an o2m command is a 1 (UPDATE) and there is nothing to update, send - a 4 instead (LINK_TO) instead as that's what the webclient sends for - unmodified rows - * if an o2m command is a 1 (UPDATE) and only a subset of its fields have - been modified, only send the modified ones - - This needs to be recursive as there are people who put invisible o2ms - inside their o2ms. - """ - result = {} - for key, val in values.items(): - if fields[key]['type'] == 'one2many': - subfields = fields[key]['edition_view']['fields'] - result[key] = [] - for (cmd, rid, vs) in val: - if cmd == Command.UPDATE and isinstance(vs, UpdateDict): - vs = dict(vs.changed_items()) - if cmd == Command.UPDATE and not vs: - result[key].append((Command.LINK, rid, False)) - elif cmd in (Command.CREATE, Command.UPDATE): - result[key].append((cmd, rid, self._onchange_values_(subfields, vs))) + def _apply_onchange_(self, values, fields, onchange_values): + assert isinstance(values, UpdateDict) + for fname, value in onchange_values.items(): + field_info = fields[fname] + if field_info['type'] in ('one2many', 'many2many'): + subfields = {} + if field_info['type'] == 'one2many': + subfields = field_info['edition_view']['fields'] + field_value = values[fname] + for cmd in value: + if cmd[0] == Command.CREATE: + vals = UpdateDict(convert_read_to_form(dict.fromkeys(subfields, False), subfields)) + self._apply_onchange_(vals, subfields, cmd[2]) + field_value.create(vals) + elif cmd[0] == Command.UPDATE: + vals = field_value.get_vals(cmd[1]) + self._apply_onchange_(vals, subfields, cmd[2]) + elif cmd[0] in (Command.DELETE, Command.UNLINK): + field_value.remove(cmd[1]) + elif cmd[0] == Command.LINK: + field_value.add(cmd[1], convert_read_to_form(cmd[2], subfields)) else: - result[key].append((cmd, rid, vs)) + assert False, "Unexpected onchange() result" else: - result[key] = val - return result - - def _cleanup_onchange(self, field_info, value, current): - """ Transform the value returned by onchange() into the expected format - for self._values. - """ - if field_info['type'] == 'many2one': - return value[0] if value else False - - if field_info['type'] == 'one2many': - # ignore o2ms nested in o2ms - if not field_info['edition_view']: - return [] - - if current is None: - current = [] - - result = [] - current_ids = {rid for cmd, rid, vs in current if cmd in (1, 2)} - current_values = {rid: vs for cmd, rid, vs in current if cmd == 1} - # which view should this be??? - subfields = field_info['edition_view']['fields'] - # TODO: simplistic, unlikely to work if e.g. there's a 5 inbetween other commands - for command in value: - if command[0] == Command.CREATE: - result.append((Command.CREATE, virtual_id(), { - key: self._cleanup_onchange(subfields[key], val, None) - for key, val in command[2].items() - if key in subfields - })) - elif command[0] == Command.UPDATE: - record_id = command[1] - current_ids.discard(record_id) - stored = current_values.get(record_id) - if stored is None: - record = self._env[field_info['relation']].browse(record_id) - stored = UpdateDict(read_record(record, subfields)) - for key, val in command[2].items(): - if key in subfields: - val = self._cleanup_onchange(subfields[key], val, stored.get(key)) - # if there are values from the onchange which differ - # from current values, update & mark field as changed - if stored.get(key, val) != val: - stored[key] = val - stored._changed.add(key) - result.append((Command.UPDATE, record_id, stored)) - elif command[0] == Command.DELETE: - current_ids.discard(command[1]) - result.append((Command.DELETE, command[1], False)) - elif command[0] == Command.LINK: - current_ids.discard(command[1]) - result.append((Command.UPDATE, command[1], None)) - elif command[0] == Command.CLEAR: - result = [] - # explicitly mark all non-relinked (or modified) records as deleted - for id_ in current_ids: - result.append((Command.DELETE, id_, False)) - return result - - if field_info['type'] == 'many2many': - # onchange result is a bunch of commands, normalize to single 6 - ids = [] if current is None else list(current[0][2]) - for command in value: - if command[0] == Command.UPDATE: - ids.append(command[1]) - elif command[0] == Command.UNLINK: - ids.remove(command[1]) - elif command[0] == Command.LINK: - ids.append(command[1]) - elif command[0] == Command.CLEAR: - del ids[:] - elif command[0] == Command.SET: - ids[:] = command[2] - else: - raise ValueError(f"Unsupported M2M command {command[0]}") - return [(Command.SET, False, ids)] - - return value + values[fname] = value + values._changed.add(fname) class O2MForm(Form): @@ -717,12 +608,16 @@ class O2MForm(Form): eval_context['parent'] = Dotter(self._proxy._form._values) return eval_context - def _onchange_values(self): - values = super()._onchange_values() + def _get_onchange_values(self): + values = super()._get_onchange_values() # computed o2m may not have a relation_field(?) field_info = self._proxy._field_info if 'relation_field' in field_info: # note: should be fine because not recursive - values[field_info['relation_field']] = self._proxy._form._onchange_values() + parent_form = self._proxy._form + parent_values = parent_form._get_onchange_values() + if parent_form._record.id: + parent_values['id'] = parent_form._record.id + values[field_info['relation_field']] = parent_values return values def save(self): @@ -730,20 +625,10 @@ class O2MForm(Form): field_value = proxy._form._values[proxy._field] values = self._get_save_values() if self._index is None: - field_value.append((Command.CREATE, virtual_id(), values)) + field_value.create(values) else: - index = proxy._command_index(self._index) - (cmd, id_, vs) = field_value[index] - if cmd == Command.CREATE: - vs.update(values) - elif cmd == Command.UPDATE: - if vs is None: - vs = UpdateDict() - assert isinstance(vs, UpdateDict), type(vs) - vs.update(values) - field_value[index] = (Command.UPDATE, id_, vs) - else: - raise AssertionError(f"Expected command 0 or 1, found {cmd!r}") + id_ = field_value[self._index] + field_value.update(id_, values) proxy._form._perform_onchange(proxy._field) @@ -761,13 +646,6 @@ class O2MForm(Form): return values -_virtual_id_counter = itertools.count() - - -def virtual_id(): - return f'virtual_{next(_virtual_id_counter)}' - - class UpdateDict(dict): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -775,6 +653,13 @@ class UpdateDict(dict): if args and isinstance(args[0], UpdateDict): self._changed.update(args[0]._changed) + def __repr__(self): + items = [ + f"{key!r}{'*' if key in self._changed else ''}: {val!r}" + for key, val in self.items() + ] + return f"{{{', '.join(items)}}}" + def changed_items(self): return ( (k, v) for k, v in self.items() @@ -791,6 +676,102 @@ class UpdateDict(dict): self._changed.clear() +class X2MValue(collections.abc.Sequence): + """ The value of a one2many field, with the API of a sequence of record ids. """ + _virtual_seq = itertools.count() + + def __init__(self, iterable_of_vals=()): + self._data = {vals['id']: UpdateDict(vals) for vals in iterable_of_vals} + + def __repr__(self): + return repr(self._data) + + def __contains__(self, id_): + return id_ in self._data + + def __getitem__(self, index): + return list(self._data)[index] + + def __iter__(self): + return iter(self._data) + + def __len__(self): + return len(self._data) + + def __eq__(self, other): + # this enables to compare self with a list + return list(self) == other + + def get_vals(self, id_): + return self._data[id_] + + def add(self, id_, vals): + assert id_ not in self._data + self._data[id_] = UpdateDict(vals) + + def remove(self, id_): + self._data.pop(id_) + + def clear(self): + self._data.clear() + + def create(self, vals): + id_ = f'virtual_{next(self._virtual_seq)}' + create_vals = UpdateDict(vals) + create_vals._changed.update(vals) + self._data[id_] = create_vals + + def update(self, id_, changes, changed=()): + vals = self._data[id_] + vals.update(changes) + vals._changed.update(changed) + + def to_list_of_vals(self): + return list(self._data.values()) + + +class O2MValue(X2MValue): + def __init__(self, iterable_of_vals=()): + super().__init__(iterable_of_vals) + self._given = list(self._data) + + def to_commands(self, convert_values=lambda vals: vals): + given = set(self._given) + result = [] + for id_, vals in self._data.items(): + if isinstance(id_, str) and id_.startswith('virtual_'): + result.append((Command.CREATE, id_, convert_values(vals))) + continue + if id_ not in given: + result.append(Command.link(id_)) + if vals._changed: + result.append(Command.update(id_, convert_values(vals))) + for id_ in self._given: + if id_ not in self._data: + result.append(Command.delete(id_)) + return result + + +class M2MValue(X2MValue): + def to_commands(self): + ids = [] + result = [Command.set(ids)] + for id_, vals in self._data.items(): + if isinstance(id_, str) and id_.startswith('virtual_'): + result.append((Command.CREATE, id_, { + key: val.to_commands() if isinstance(val, X2MValue) else val + for key, val in vals.changed_items() + })) + continue + ids.append(id_) + if vals._changed: + result.append(Command.update(id_, { + key: val.to_commands() if isinstance(val, X2MValue) else val + for key, val in vals.changed_items() + })) + return result + + class X2MProxy: """ A proxy represents the value of an x2many field, but not directly. Instead, it provides an API to add, remove or edit records in the value. @@ -802,7 +783,12 @@ class X2MProxy: def __init__(self, form, field_name): self._form = form self._field = field_name - self._field_info = self._form._view['fields'][field_name] + self._field_info = form._view['fields'][field_name] + self._field_value = form._values[field_name] + + @property + def ids(self): + return list(self._field_value) def _assert_editable(self): assert not self._form._get_modifier(self._field, 'readonly'), f'field {self._field!r} is not editable' @@ -811,28 +797,8 @@ class X2MProxy: class O2MProxy(X2MProxy): """ Proxy object for editing the value of a one2many field. """ - def __init__(self, form, field_name): - super().__init__(form, field_name) - # reify records to a list so they can be manipulated easily? - self._records = [] - model = self._model - fields = self._field_info['edition_view']['fields'] - for (command, rid, values) in self._form._values[self._field]: - if command == Command.CREATE: - self._records.append(values) - elif command == Command.UPDATE: - if values is None: - # read based on view info - r = model.browse(rid) - values = UpdateDict(read_record(r, fields)) - self._records.append(values) - elif command == Command.DELETE: - pass - else: - raise AssertionError("O2M proxy only supports commands 0, 1 and 2, found %s" % command) - def __len__(self): - return len(self._records) + return len(self._field_value) @property def _model(self): @@ -842,6 +808,10 @@ class O2MProxy(X2MProxy): model = model.with_context(**context) return model + @property + def _records(self): + return self._field_value.to_list_of_vals() + def new(self): """ Returns a :class:`Form` for a new :class:`~odoo.fields.One2many` record, properly initialised. @@ -872,39 +842,9 @@ class O2MProxy(X2MProxy): :raises AssertionError: if the field is not editable """ self._assert_editable() - # remove reified record from local list & either remove 0 from - # commands list or replace 1 (update) by 2 (remove) - cidx = self._command_index(index) - commands = self._form._values[self._field] - (command, rid, _) = commands[cidx] - if command == Command.CREATE: - # record not saved yet -> just remove the command - del commands[cidx] - elif command == Command.UPDATE: - # record already saved, replace by 2 - commands[cidx] = (Command.DELETE, rid, 0) - else: - raise AssertionError("Expected command 0 or 1, got %s" % commands[cidx]) - # remove reified record - del self._records[index] + self._field_value.remove(self._field_value[index]) self._form._perform_onchange(self._field) - def _command_index(self, for_record): - """ Takes a record index and finds the corresponding record index - (skips all 2s, basically) - - :param int for_record: - """ - commands = self._form._values[self._field] - return next( - cidx - for ridx, cidx in enumerate( - cidx for cidx, (c, _1, _2) in enumerate(commands) - if c in (Command.CREATE, Command.UPDATE) - ) - if ridx == for_record - ) - class M2MProxy(X2MProxy, collections.abc.Sequence): """ Proxy object for editing the value of a many2many field. @@ -914,23 +854,20 @@ class M2MProxy(X2MProxy, collections.abc.Sequence): """ def __getitem__(self, index): comodel_name = self._field_info['relation'] - return self._form._env[comodel_name].browse(self._get_ids()[index]) + return self._form._env[comodel_name].browse(self._field_value[index]) def __len__(self): - return len(self._get_ids()) + return len(self._field_value) def __iter__(self): comodel_name = self._field_info['relation'] - records = self._form._env[comodel_name].browse(self._get_ids()) + records = self._form._env[comodel_name].browse(self._field_value) return iter(records) def __contains__(self, record): comodel_name = self._field_info['relation'] assert isinstance(record, BaseModel) and record._name == comodel_name - return record.id in self._get_ids() - - def _get_ids(self): - return self._form._values[self._field][0][2] + return record.id in self._field_value def add(self, record): """ Adds ``record`` to the field, the record must already exist. @@ -942,9 +879,10 @@ class M2MProxy(X2MProxy, collections.abc.Sequence): comodel_name = self._field_info['relation'] assert isinstance(record, BaseModel) and record._name == comodel_name, \ f"trying to assign a {record._name!r} object to a {comodel_name!r} field" - self._get_ids().append(record.id) - parent._perform_onchange(self._field) + if record.id not in self._field_value: + self._field_value.add(record.id, {'id': record.id}) + parent._perform_onchange(self._field) # pylint: disable=redefined-builtin def remove(self, id=None, index=None): @@ -954,10 +892,8 @@ class M2MProxy(X2MProxy, collections.abc.Sequence): self._assert_editable() assert (id is None) ^ (index is None), "can remove by either id or index" if id is None: - # remove by index - del self._get_ids()[index] - else: - self._get_ids().remove(id) + id = self._field_value[index] + self._field_value.remove(id) self._form._perform_onchange(self._field) def set(self, records): @@ -967,15 +903,17 @@ class M2MProxy(X2MProxy, collections.abc.Sequence): assert isinstance(records, BaseModel) and records._name == comodel_name, \ f"trying to assign a {records._name!r} object to a {comodel_name!r} field" - if set(records.ids) != set(self._get_ids()): - self._get_ids()[:] = records.ids + if set(records.ids) != set(self._field_value): + self._field_value.clear() + for id_ in records.ids: + self._field_value.add(id_, {'id': id_}) self._form._perform_onchange(self._field) def clear(self): """ Removes all existing records in the m2m """ self._assert_editable() - self._get_ids()[:] = [] + self._field_value.clear() self._form._perform_onchange(self._field) @@ -997,15 +935,15 @@ def get_fields_spec(fields): def convert_read_to_form(values, fields): result = {} for fname, value in values.items(): - field_info = fields[fname] + field_info = {'type': 'id'} if fname == 'id' else fields[fname] if field_info['type'] == 'one2many': - subfields = field_info['edition_view']['fields'] - value = [ - (Command.UPDATE, vals['id'], UpdateDict(convert_read_to_form(vals, subfields))) - for vals in value - ] + if 'edition_view' in field_info: + subfields = field_info['edition_view']['fields'] + value = O2MValue(convert_read_to_form(vals, subfields) for vals in (value or ())) + else: + value = O2MValue({'id': id_} for id_ in (value or ())) elif field_info['type'] == 'many2many': - value = [Command.set(value)] + value = M2MValue({'id': id_} for id_ in (value or ())) elif field_info['type'] == 'datetime' and isinstance(value, datetime): value = odoo.fields.Datetime.to_string(value) elif field_info['type'] == 'date' and isinstance(value, date): @@ -1016,15 +954,16 @@ def convert_read_to_form(values, fields): def _cleanup_from_default(type_, value): if not value: - if type_ == 'many2many': - return [(Command.SET, False, [])] - elif type_ == 'one2many': - return [] + if type_ == 'one2many': + return O2MValue() + elif type_ == 'many2many': + return M2MValue() elif type_ in ('integer', 'float'): return 0 return value if type_ == 'one2many': + assert False, "not implemented yet" return [cmd for cmd in value if cmd[0] != Command.SET] elif type_ == 'datetime' and isinstance(value, datetime): return odoo.fields.Datetime.to_string(value)