[IMP] tests: make Form use onchange2() instead of onchange()
Part-of: odoo/odoo#124614
This commit is contained in:
@@ -87,7 +87,7 @@ class TestMailComposerForm(TestMailComposer):
|
||||
}))
|
||||
form.body = '<p>Hello</p>'
|
||||
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 = '<p>Hello</p>'
|
||||
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'
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
+221
-282
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user