[IMP] fields: x2many fields of new records are new records
The value of an x2many field on a new record are new records, possibly related to a real record, called its "origin".
This commit is contained in:
+99
-73
@@ -23,8 +23,8 @@ except ImportError:
|
||||
import psycopg2
|
||||
|
||||
from .sql_db import LazyCursor
|
||||
from .tools import float_repr, float_round, frozendict, html_sanitize, human_size, pg_varchar,\
|
||||
ustr, OrderedSet, pycompat, sql, date_utils, unique
|
||||
from .tools import float_repr, float_round, frozendict, html_sanitize, human_size, pg_varchar, \
|
||||
ustr, OrderedSet, pycompat, sql, date_utils, unique, IterableGenerator
|
||||
from .tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT
|
||||
from .tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT
|
||||
from .tools.translate import html_translate, _
|
||||
@@ -1019,11 +1019,17 @@ class Field(MetaField('DummyField', (object,), {})):
|
||||
# determine dependent fields
|
||||
spec = self.modified_draft(record)
|
||||
|
||||
# set value in cache, inverse field, and mark record as dirty
|
||||
# set value in cache
|
||||
record.env.cache.set(record, self, value)
|
||||
if env.in_onchange:
|
||||
for invf in record._field_inverses[self]:
|
||||
invf._update(record[self.name], record)
|
||||
|
||||
if not record.id or env.in_onchange:
|
||||
# set inverse fields on new records in the comodel
|
||||
if self.relational:
|
||||
inv_recs = record[self.name].filtered(lambda r: not r.id)
|
||||
if inv_recs:
|
||||
for invf in record._field_inverses[self]:
|
||||
invf._update(inv_recs, record)
|
||||
# mark field as dirty
|
||||
env.dirty[record].add(self.name)
|
||||
|
||||
# determine more dependent fields, and invalidate them
|
||||
@@ -1125,6 +1131,18 @@ class Field(MetaField('DummyField', (object,), {})):
|
||||
self.compute_value(record)
|
||||
return
|
||||
|
||||
origin = record._origin
|
||||
if origin:
|
||||
# retrieve value from original record
|
||||
value = self.convert_to_cache(origin[self.name], record)
|
||||
return record.env.cache.set(record, self, value)
|
||||
|
||||
if self.type == 'many2one' and self.delegate:
|
||||
# special case: parent records are new as well
|
||||
parent = record.env[self.comodel_name].new()
|
||||
value = self.convert_to_cache(parent, record)
|
||||
return record.env.cache.set(record, self, value)
|
||||
|
||||
null = self.convert_to_cache(False, record, validate=False)
|
||||
record.env.cache.set_special(record, self, lambda: null)
|
||||
|
||||
@@ -1157,14 +1175,14 @@ class Field(MetaField('DummyField', (object,), {})):
|
||||
# Notification when fields are modified
|
||||
#
|
||||
|
||||
def modified_draft(self, records):
|
||||
def modified_draft(self, record):
|
||||
""" Same as :meth:`modified`, but in draft mode. """
|
||||
env = records.env
|
||||
env = record.env
|
||||
|
||||
# invalidate the fields on the records in cache that depend on
|
||||
# ``records``, except fields currently being computed
|
||||
# ``record``, except fields currently being computed
|
||||
spec = []
|
||||
for field, path in records._field_triggers[self]:
|
||||
for field, path in record._field_triggers[self]:
|
||||
if not field.compute:
|
||||
# Note: do not invalidate non-computed fields. Such fields may
|
||||
# require invalidation in general (like *2many fields with
|
||||
@@ -1172,16 +1190,12 @@ class Field(MetaField('DummyField', (object,), {})):
|
||||
# we would simply lose their values during an onchange!
|
||||
continue
|
||||
|
||||
target = env[field.model_name]
|
||||
protected = env.protected(field)
|
||||
if path == 'id' and field.model_name == records._name:
|
||||
target = records - protected
|
||||
elif path and env.in_onchange:
|
||||
target = (env.cache.get_records(target, field) - protected).filtered(
|
||||
lambda rec: rec if path == 'id' else rec._mapped_cache(path) & records
|
||||
)
|
||||
if path == 'id' and field.model_name == record._name:
|
||||
target = record
|
||||
else:
|
||||
target = env.cache.get_records(target, field) - protected
|
||||
target = env.cache.get_records(env[field.model_name], field)
|
||||
target = target.filtered(lambda t: not t.id)
|
||||
target -= env.protected(field)
|
||||
|
||||
if target:
|
||||
spec.append((field, target._ids))
|
||||
@@ -2173,22 +2187,28 @@ class Many2one(_Relational):
|
||||
def convert_to_cache(self, value, record, validate=True):
|
||||
# cache format: tuple(ids)
|
||||
if type(value) in IdType:
|
||||
return (value,)
|
||||
ids = (value,)
|
||||
elif isinstance(value, BaseModel):
|
||||
if not validate or (value._name == self.comodel_name and len(value) <= 1):
|
||||
return value._ids
|
||||
raise ValueError("Wrong value for %s: %r" % (self, value))
|
||||
if validate and (value._name != self.comodel_name or len(value) > 1):
|
||||
raise ValueError("Wrong value for %s: %r" % (self, value))
|
||||
ids = value._ids
|
||||
elif isinstance(value, tuple):
|
||||
# value is either a pair (id, name), or a tuple of ids
|
||||
return value[:1]
|
||||
ids = value[:1]
|
||||
elif isinstance(value, dict):
|
||||
return record.env[self.comodel_name].new(value)._ids
|
||||
ids = record.env[self.comodel_name].new(value)._ids
|
||||
else:
|
||||
return ()
|
||||
ids = ()
|
||||
|
||||
if self.delegate and record and not record.id:
|
||||
# the parent record of a new record is a new record
|
||||
ids = tuple(it and NewId(it) for it in ids)
|
||||
|
||||
return ids
|
||||
|
||||
def convert_to_record(self, value, record):
|
||||
# use registry to avoid creating a recordset for the model
|
||||
prefetch_ids = PrefetchValueIds(record, self)
|
||||
prefetch_ids = IterableGenerator(prefetch_value_ids, record, self)
|
||||
return record.pool[self.comodel_name]._browse(record.env, value, prefetch_ids)
|
||||
|
||||
def convert_to_read(self, value, record, use_name_get=True):
|
||||
@@ -2274,45 +2294,56 @@ class _RelationalMulti(_Relational):
|
||||
def convert_to_cache(self, value, record, validate=True):
|
||||
# cache format: tuple(ids)
|
||||
if isinstance(value, BaseModel):
|
||||
if not validate or (value._name == self.comodel_name):
|
||||
return value._ids
|
||||
if validate and value._name != self.comodel_name:
|
||||
raise ValueError("Wrong value for %s: %s" % (self, value))
|
||||
ids = value._ids
|
||||
if record and not record.id:
|
||||
# x2many field value of new record is new records
|
||||
ids = tuple(it and NewId(it) for it in ids)
|
||||
return ids
|
||||
|
||||
elif isinstance(value, (list, tuple)):
|
||||
# value is a list/tuple of commands, dicts or record ids
|
||||
comodel = record.env[self.comodel_name]
|
||||
# if record is new, the field's value is new records
|
||||
if record and not record.id:
|
||||
browse = lambda it: comodel.browse([it and NewId(it)])
|
||||
else:
|
||||
browse = comodel.browse
|
||||
# determine the value ids
|
||||
ids = OrderedSet(record[self.name]._ids)
|
||||
# modify ids with the commands
|
||||
for command in value:
|
||||
if isinstance(command, (tuple, list)):
|
||||
if command[0] == 0:
|
||||
ids.add(comodel.new(command[2], command[1]).id)
|
||||
ids.add(comodel.new(command[2], ref=command[1]).id)
|
||||
elif command[0] == 1:
|
||||
comodel.browse(command[1]).update(command[2])
|
||||
ids.add(command[1])
|
||||
elif command[0] == 2:
|
||||
# note: the record will be deleted by write()
|
||||
ids.discard(command[1])
|
||||
elif command[0] == 3:
|
||||
ids.discard(command[1])
|
||||
line = browse(command[1])
|
||||
line.update(command[2])
|
||||
ids.add(line.id)
|
||||
elif command[0] in (2, 3):
|
||||
ids.discard(browse(command[1]).id)
|
||||
elif command[0] == 4:
|
||||
ids.add(command[1])
|
||||
ids.add(browse(command[1]).id)
|
||||
elif command[0] == 5:
|
||||
ids.clear()
|
||||
elif command[0] == 6:
|
||||
ids = OrderedSet(command[2])
|
||||
ids = OrderedSet(browse(it).id for it in command[2])
|
||||
elif isinstance(command, dict):
|
||||
ids.add(comodel.new(command).id)
|
||||
else:
|
||||
ids.add(command)
|
||||
ids.add(browse(command).id)
|
||||
# return result as a tuple
|
||||
return tuple(ids)
|
||||
|
||||
elif not value:
|
||||
return ()
|
||||
|
||||
raise ValueError("Wrong value for %s: %s" % (self, value))
|
||||
|
||||
def convert_to_record(self, value, record):
|
||||
# use registry to avoid creating a recordset for the model
|
||||
prefetch_ids = PrefetchValueIds(record, self)
|
||||
prefetch_ids = IterableGenerator(prefetch_value_ids, record, self)
|
||||
return record.pool[self.comodel_name]._browse(record.env, value, prefetch_ids)
|
||||
|
||||
def convert_to_read(self, value, record, use_name_get=True):
|
||||
@@ -2322,16 +2353,23 @@ class _RelationalMulti(_Relational):
|
||||
# make result with new and existing records
|
||||
result = [(6, 0, [])]
|
||||
for record in value:
|
||||
if not record.id:
|
||||
values = {name: record[name] for name in record._cache}
|
||||
values = record._convert_to_write(values)
|
||||
origin = record._origin
|
||||
if not origin:
|
||||
values = record._convert_to_write({
|
||||
name: record[name]
|
||||
for name in record._cache
|
||||
})
|
||||
result.append((0, 0, values))
|
||||
elif record._is_dirty():
|
||||
values = {name: record[name] for name in record._get_dirty()}
|
||||
values = record._convert_to_write(values)
|
||||
result.append((1, record.id, values))
|
||||
else:
|
||||
result[0][2].append(record.id)
|
||||
result[0][2].append(origin.id)
|
||||
if record != origin:
|
||||
values = record._convert_to_write({
|
||||
name: record[name]
|
||||
for name in record._cache
|
||||
if record[name] != origin[name]
|
||||
})
|
||||
if values:
|
||||
result.append((1, origin.id, values))
|
||||
return result
|
||||
|
||||
def convert_to_onchange(self, value, record, names):
|
||||
@@ -2350,12 +2388,12 @@ class _RelationalMulti(_Relational):
|
||||
|
||||
result = [(5,)]
|
||||
for record in value:
|
||||
if not record.id:
|
||||
if not record.id and not record._origin:
|
||||
result.append((0, record.id.ref or 0, vals[record]))
|
||||
elif vals[record]:
|
||||
result.append((1, record.id, vals[record]))
|
||||
result.append((1, record._origin.id, vals[record]))
|
||||
else:
|
||||
result.append((4, record.id))
|
||||
result.append((4, record._origin.id))
|
||||
return result
|
||||
|
||||
def convert_to_export(self, value, record):
|
||||
@@ -2369,9 +2407,8 @@ class _RelationalMulti(_Relational):
|
||||
super(_RelationalMulti, self)._compute_related(records)
|
||||
if self.related_sudo:
|
||||
# determine which records in the relation are actually accessible
|
||||
target = records[self.name]
|
||||
target_ids = set(target.search([('id', 'in', target.ids)]).ids)
|
||||
accessible = lambda target: target.id in target_ids
|
||||
line_ids = set(records[self.name]._filter_access_rules('read')._ids)
|
||||
accessible = lambda line: line.id in line_ids
|
||||
# filter values to keep the accessible records only
|
||||
for record in records:
|
||||
record[self.name] = record[self.name].filtered(accessible)
|
||||
@@ -2873,27 +2910,16 @@ class Id(Field):
|
||||
raise TypeError("field 'id' cannot be assigned")
|
||||
|
||||
|
||||
class PrefetchValueIds(object):
|
||||
""" An iterable on the ids of the cached values of a relational field for
|
||||
the prefetch set of a record.
|
||||
def prefetch_value_ids(record, field):
|
||||
""" Return an iterator over the ids of the cached values of a relational
|
||||
field for the prefetch set of a record.
|
||||
"""
|
||||
__slots__ = ('_record', '_field')
|
||||
|
||||
def __init__(self, record, field):
|
||||
self._record = record
|
||||
self._field = field
|
||||
|
||||
def __iter__(self):
|
||||
record = self._record
|
||||
records = record.browse(record._prefetch_ids)
|
||||
return unique(
|
||||
id_
|
||||
for ids in record.env.cache.get_values(records, self._field, ())
|
||||
for id_ in ids
|
||||
)
|
||||
records = record.browse(record._prefetch_ids)
|
||||
ids_seq = record.env.cache.get_values(records, field, ())
|
||||
return unique(id_ for ids in ids_seq for id_ in ids)
|
||||
|
||||
|
||||
# imported here to avoid dependency cycle issues
|
||||
from odoo import SUPERUSER_ID
|
||||
from .exceptions import AccessError, MissingError, UserError
|
||||
from .models import check_pg_name, BaseModel, IdType
|
||||
from .models import check_pg_name, BaseModel, NewId, IdType
|
||||
|
||||
Reference in New Issue
Block a user