[MERGE] orm: implement new model for the prefetching of records
The new model is now the following:
- each recordset is attached to a prefetch object: records._prefetch
- the prefetch object associates model names to record ids to prefetch:
ids = records._prefetch[model_name]
- a call to model.browse(ids) returns a recordset with a new prefetch object
- the prefetch object is passed and populated by
- iteration
- access of relational field
- with_env(), sudo(), with_context()
This commit is contained in:
@@ -287,7 +287,7 @@ class BaseActionRule(models.Model):
|
||||
if res:
|
||||
if 'value' in res:
|
||||
res['value'].pop('id', None)
|
||||
self.update(self._convert_to_cache(res['value'], validate=False))
|
||||
self.update({key: val for key, val in res['value'].iteritems() if key in self._fields})
|
||||
if 'domain' in res:
|
||||
result.setdefault('domain', {}).update(res['domain'])
|
||||
if 'warning' in res:
|
||||
|
||||
@@ -41,7 +41,6 @@ class SaleOrderEventRegistration(models.TransientModel):
|
||||
'sale_order_line_id': so_line.id,
|
||||
}])
|
||||
res['event_registration_ids'] = attendee_list
|
||||
res = self._convert_to_cache(res, validate=False)
|
||||
res = self._convert_to_write(res)
|
||||
return res
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ class TestMailTemplate(TestMail):
|
||||
|
||||
values = composer.onchange_template_id(self.email_template.id, 'comment', 'mail.channel', self.group_pigs.id)['value']
|
||||
# use _convert_to_cache to return a browse record list from command list or id list for x2many fields
|
||||
values = self.env['mail.compose.message']._convert_to_cache(values)
|
||||
values = composer._convert_to_record(composer._convert_to_cache(values))
|
||||
recipients = values['partner_ids']
|
||||
attachments = values['attachment_ids']
|
||||
|
||||
|
||||
@@ -398,7 +398,7 @@ class MailComposer(models.TransientModel):
|
||||
# ORM handle the assignation of command list on new onchange (api.v8),
|
||||
# this force the complete replacement of x2many field with
|
||||
# command and is compatible with onchange api.v7
|
||||
values = self._convert_to_write(self._convert_to_cache(values))
|
||||
values = self._convert_to_write(values)
|
||||
|
||||
return {'value': values}
|
||||
|
||||
|
||||
@@ -363,7 +363,7 @@ class Partner(osv.osv):
|
||||
# create a record in cache, apply onchange then revert back to a dictionnary
|
||||
invoice_line = invoice_line_obj.new(cr, uid, line_values, context=context)
|
||||
invoice_line._onchange_product_id()
|
||||
line_values = invoice_line._convert_to_write(invoice_line._cache)
|
||||
line_values = invoice_line._convert_to_write({name: invoice_line[name] for name in invoice_line._cache})
|
||||
line_values['price_unit'] = amount
|
||||
invoice_obj.write(cr, uid, [invoice_id], {'invoice_line_ids': [(0, 0, line_values)]}, context=context)
|
||||
invoice_list.append(invoice_id)
|
||||
|
||||
@@ -156,7 +156,7 @@ class PosOrder(models.Model):
|
||||
invoice_line.invoice_line_tax_ids = invoice_line.invoice_line_tax_ids.ids
|
||||
# We convert a new id object back to a dictionary to write to
|
||||
# bridge between old and new api
|
||||
inv_line = invoice_line._convert_to_write(invoice_line._cache)
|
||||
inv_line = invoice_line._convert_to_write({name: invoice_line[name] for name in invoice_line._cache})
|
||||
inv_line.update(price_unit=line.price_unit, discount=line.discount)
|
||||
return InvoiceLine.sudo().create(inv_line)
|
||||
|
||||
@@ -410,7 +410,7 @@ class PosOrder(models.Model):
|
||||
invoice._onchange_partner_id()
|
||||
invoice.fiscal_position_id = order.fiscal_position_id
|
||||
|
||||
inv = invoice._convert_to_write(invoice._cache)
|
||||
inv = invoice._convert_to_write({name: invoice[name] for name in invoice._cache})
|
||||
new_invoice = Invoice.with_context(local_context).sudo().create(inv)
|
||||
order.write({'invoice_id': new_invoice.id, 'state': 'invoiced'})
|
||||
Invoice += new_invoice
|
||||
|
||||
@@ -79,6 +79,45 @@ Accessing a relational field (:class:`~openerp.fields.Many2one`,
|
||||
# 1 database update
|
||||
records.write({'a': 1, 'b': 2, 'c': 3})
|
||||
|
||||
Record cache and prefetching
|
||||
----------------------------
|
||||
|
||||
Odoo maintains a cache for the fields of the records, so that not every field
|
||||
access issues a database request, which would be terrible for performance. The
|
||||
following example queries the database only for the first statement::
|
||||
|
||||
record.name # first access reads value from database
|
||||
record.name # second access gets value from cache
|
||||
|
||||
To avoid reading one field on one record at a time, Odoo *prefetches* records
|
||||
and fields following some heuristics to get good performance. Once a field must
|
||||
be read on a given record, the ORM actually reads that field on a larger
|
||||
recordset, and stores the returned values in cache for later use. The prefetched
|
||||
recordset is usually the recordset from which the record comes by iteration.
|
||||
Moreover, all simple stored fields (boolean, integer, float, char, text, date,
|
||||
datetime, selection, many2one) are fetched altogether; they correspond to the
|
||||
columns of the model's table, and are fetched efficiently in the same query.
|
||||
|
||||
Consider the following example, where ``partners`` is a recordset of 1000
|
||||
records. Without prefetching, the loop would make 2000 queries to the database.
|
||||
With prefetching, only one query is made::
|
||||
|
||||
for partner in partners:
|
||||
print partner.name # first pass prefetches 'name' and 'lang'
|
||||
# (and other fields) on all 'partners'
|
||||
print partner.lang
|
||||
|
||||
The prefetching also works on *secondary records*: when relational fields are
|
||||
read, their values (which are records) are subscribed for future prefetching.
|
||||
Accessing one of those secondary records prefetches all secondary records from
|
||||
the same model. This makes the following example generate only two queries, one
|
||||
for partners and one for countries::
|
||||
|
||||
countries = set()
|
||||
for partner in partners:
|
||||
country = partner.country_id # first pass prefetches all partners
|
||||
countries.add(country.name) # first pass prefetches all countries
|
||||
|
||||
Set operations
|
||||
--------------
|
||||
|
||||
|
||||
@@ -402,7 +402,7 @@ class IrValues(models.Model):
|
||||
# FIXME: needs cleanup
|
||||
try:
|
||||
action_def = {
|
||||
field: action._fields[field].convert_to_read(action[field])
|
||||
field: action._fields[field].convert_to_read(action[field], action)
|
||||
for field in fields
|
||||
}
|
||||
if action._name in ('ir.actions.report.xml', 'ir.actions.act_window'):
|
||||
|
||||
@@ -324,29 +324,82 @@ class TestAPI(common.TransactionCase):
|
||||
self.env.check_cache()
|
||||
|
||||
@mute_logger('openerp.models')
|
||||
def test_60_cache_prefetching(self):
|
||||
def test_60_prefetch(self):
|
||||
""" Check the record cache prefetching """
|
||||
self.env.invalidate_all()
|
||||
partners = self.env['res.partner'].search([], limit=models.PREFETCH_MAX)
|
||||
self.assertTrue(len(partners) > 1)
|
||||
|
||||
# all the records of an instance already have an entry in cache
|
||||
partners = self.env['res.partner'].search([])
|
||||
partner_ids = self.env.prefetch['res.partner']
|
||||
self.assertEqual(set(partners.ids), set(partner_ids))
|
||||
|
||||
# countries have not been fetched yet; their cache must be empty
|
||||
countries = self.env['res.country'].browse()
|
||||
self.assertFalse(self.env.prefetch['res.country'])
|
||||
# all the records in partners are ready for prefetching
|
||||
self.assertItemsEqual(partners.ids, partners._prefetch['res.partner'])
|
||||
|
||||
# reading ONE partner should fetch them ALL
|
||||
countries |= partners[0].country_id
|
||||
country_cache = self.env.cache[partners._fields['country_id']]
|
||||
self.assertLessEqual(set(partners._ids), set(country_cache))
|
||||
partner = next(p for p in partners)
|
||||
partner.country_id
|
||||
country_id_cache = self.env.cache[type(partners).country_id]
|
||||
self.assertItemsEqual(partners.ids, country_id_cache)
|
||||
|
||||
# read all partners, and check that the cache already contained them
|
||||
country_ids = list(self.env.prefetch['res.country'])
|
||||
for p in partners:
|
||||
countries |= p.country_id
|
||||
self.assertLessEqual(set(countries.ids), set(country_ids))
|
||||
# partners' countries are ready for prefetching
|
||||
country_ids = set(cid for cids in country_id_cache.itervalues() for cid in cids)
|
||||
self.assertTrue(len(country_ids) > 1)
|
||||
self.assertItemsEqual(country_ids, partners._prefetch['res.country'])
|
||||
|
||||
# reading ONE partner country should fetch ALL partners' countries
|
||||
country = next(p.country_id for p in partners if p.country_id)
|
||||
country.name
|
||||
name_cache = self.env.cache[type(country).name]
|
||||
self.assertItemsEqual(country_ids, name_cache)
|
||||
|
||||
@mute_logger('openerp.models')
|
||||
def test_60_prefetch_object(self):
|
||||
""" Check the prefetching model. """
|
||||
partners = self.env['res.partner'].search([], limit=models.PREFETCH_MAX)
|
||||
self.assertTrue(partners)
|
||||
|
||||
def same_prefetch(a, b):
|
||||
self.assertIs(a._prefetch, b._prefetch)
|
||||
def diff_prefetch(a, b):
|
||||
self.assertIsNot(a._prefetch, b._prefetch)
|
||||
|
||||
# the recordset operations below should create new prefetch objects
|
||||
diff_prefetch(partners, partners.browse())
|
||||
diff_prefetch(partners, partners.browse(partners.ids))
|
||||
diff_prefetch(partners, partners[0])
|
||||
diff_prefetch(partners, partners[:10])
|
||||
|
||||
# the recordset operations below should pass the prefetch object
|
||||
same_prefetch(partners, partners.sudo(self.env.ref('base.user_demo')))
|
||||
same_prefetch(partners, partners.with_context(active_test=False))
|
||||
same_prefetch(partners, partners[:10].with_prefetch(partners._prefetch))
|
||||
|
||||
# iterating and reading relational fields should pass the prefetch object
|
||||
self.assertEqual(type(partners).country_id.type, 'many2one')
|
||||
self.assertEqual(type(partners).bank_ids.type, 'one2many')
|
||||
self.assertEqual(type(partners).category_id.type, 'many2many')
|
||||
|
||||
vals0 = {
|
||||
'name': 'Empty relational fields',
|
||||
'country_id': False,
|
||||
'bank_ids': [],
|
||||
'category_id': [],
|
||||
}
|
||||
vals1 = {
|
||||
'name': 'Non-empty relational fields',
|
||||
'country_id': self.ref('base.be'),
|
||||
'bank_ids': [(0, 0, {'acc_number': 'FOO42'})],
|
||||
'category_id': [(4, self.ref('base.res_partner_category_0'))],
|
||||
}
|
||||
partners = partners.create(vals0) + partners.create(vals1)
|
||||
for partner in partners:
|
||||
same_prefetch(partners, partner)
|
||||
same_prefetch(partners, partner.country_id)
|
||||
same_prefetch(partners, partner.bank_ids)
|
||||
same_prefetch(partners, partner.category_id)
|
||||
|
||||
# same with empty recordsets
|
||||
empty = partners.browse()
|
||||
same_prefetch(empty, empty.country_id)
|
||||
same_prefetch(empty, empty.bank_ids)
|
||||
same_prefetch(empty, empty.category_id)
|
||||
|
||||
@mute_logger('openerp.models')
|
||||
def test_70_one(self):
|
||||
|
||||
@@ -385,20 +385,21 @@ class TestNewFields(common.TransactionCase):
|
||||
def test_31_prefetch(self):
|
||||
""" test prefetch of records handle AccessError """
|
||||
Category = self.env['test_new_api.category']
|
||||
cat_1 = Category.create({'name': 'NOACCESS'}).id
|
||||
cat_2 = Category.create({'name': 'ACCESS', 'parent': cat_1}).id
|
||||
cat1 = Category.create({'name': 'NOACCESS'})
|
||||
cat2 = Category.create({'name': 'ACCESS', 'parent': cat1.id})
|
||||
cats = cat1 + cat2
|
||||
|
||||
self.env.clear()
|
||||
|
||||
cat = Category.browse(cat_2)
|
||||
self.assertEqual(cat.name, 'ACCESS')
|
||||
# both categories should be in prefetch ids
|
||||
self.assertSetEqual(self.env.prefetch[Category._name], set([cat_1, cat_2]))
|
||||
cat1, cat2 = cats
|
||||
self.assertEqual(cat2.name, 'ACCESS')
|
||||
# both categories should be ready for prefetching
|
||||
self.assertItemsEqual(cat2._prefetch[Category._name], cats.ids)
|
||||
# but due to our (lame) overwrite of `read`, it should not forbid us to read records we have access to
|
||||
self.assertFalse(len(cat.discussions))
|
||||
self.assertEqual(cat.parent.id, cat_1)
|
||||
self.assertFalse(cat2.discussions)
|
||||
self.assertEqual(cat2.parent, cat1)
|
||||
with self.assertRaises(AccessError):
|
||||
Category.browse(cat_1).name
|
||||
cat1.name
|
||||
|
||||
def test_40_new(self):
|
||||
""" test new records. """
|
||||
|
||||
+7
-5
@@ -865,7 +865,6 @@ class Environment(object):
|
||||
self.cr, self.uid, self.context = self.args = (cr, uid, frozendict(context))
|
||||
self.registry = RegistryManager.get(cr.dbname)
|
||||
self.cache = defaultdict(dict) # {field: {id: value, ...}, ...}
|
||||
self.prefetch = defaultdict(set) # {model_name: set(id), ...}
|
||||
self.computed = defaultdict(set) # {field: set(id), ...}
|
||||
self.dirty = defaultdict(set) # {record: set(field_name), ...}
|
||||
self.all = envs
|
||||
@@ -878,7 +877,7 @@ class Environment(object):
|
||||
|
||||
def __getitem__(self, model_name):
|
||||
""" Return an empty recordset from the given model. """
|
||||
return self.registry[model_name]._browse(self, ())
|
||||
return self.registry[model_name]._browse((), self)
|
||||
|
||||
def __iter__(self):
|
||||
""" Return an iterator on model names. """
|
||||
@@ -973,7 +972,6 @@ class Environment(object):
|
||||
""" Clear the cache of all environments. """
|
||||
for env in list(self.all):
|
||||
env.cache.clear()
|
||||
env.prefetch.clear()
|
||||
env.computed.clear()
|
||||
env.dirty.clear()
|
||||
|
||||
@@ -1038,6 +1036,8 @@ class Environment(object):
|
||||
|
||||
def check_cache(self):
|
||||
""" Check the cache consistency. """
|
||||
from openerp.fields import SpecialValue
|
||||
|
||||
# make a full copy of the cache, and invalidate it
|
||||
cache_dump = dict(
|
||||
(field, dict(field_cache))
|
||||
@@ -1053,9 +1053,11 @@ class Environment(object):
|
||||
for record in records:
|
||||
try:
|
||||
cached = field_dump[record.id]
|
||||
cached = cached.get() if isinstance(cached, SpecialValue) else cached
|
||||
value = field.convert_to_record(cached, record)
|
||||
fetched = record[field.name]
|
||||
if fetched != cached:
|
||||
info = {'cached': cached, 'fetched': fetched}
|
||||
if fetched != value:
|
||||
info = {'cached': value, 'fetched': fetched}
|
||||
invalids.append((field, record, info))
|
||||
except (AccessError, MissingError):
|
||||
pass
|
||||
|
||||
+162
-135
@@ -52,11 +52,12 @@ def copy_cache(records, env):
|
||||
if record not in done:
|
||||
done.add(record)
|
||||
target = record.with_env(env)
|
||||
for name, value in record._cache.iteritems():
|
||||
for name in record._cache:
|
||||
field = record._fields[name]
|
||||
value = record[name]
|
||||
if isinstance(value, BaseModel):
|
||||
todo.update(value)
|
||||
value = value.with_env(env)
|
||||
target._cache[name] = value
|
||||
target._cache[name] = field.convert_to_cache(value, target, validate=False)
|
||||
|
||||
|
||||
def resolve_mro(model, name, predicate):
|
||||
@@ -81,19 +82,16 @@ def default_new_to_old(field, value):
|
||||
""" Convert the new-API default ``value`` to the old API. """
|
||||
if callable(value):
|
||||
from openerp import api
|
||||
return api.model(lambda model: field.convert_to_write(value(model)))
|
||||
return api.model(lambda model: field.convert_to_write(value(model), model))
|
||||
else:
|
||||
return value
|
||||
|
||||
def default_old_to_new(field, value):
|
||||
""" Convert the old-API default ``value`` to the new API. """
|
||||
if callable(value):
|
||||
return lambda model: field.convert_to_cache(
|
||||
value(model._model, model._cr, model._uid, model._context),
|
||||
model, validate=False,
|
||||
)
|
||||
return lambda model: value(model._model, model._cr, model._uid, model._context)
|
||||
else:
|
||||
return lambda model: field.convert_to_cache(value, model, validate=False)
|
||||
return lambda model: value
|
||||
|
||||
def default_old_to_old(field, value):
|
||||
""" Convert the old-API default ``value`` to the old API. """
|
||||
@@ -758,57 +756,60 @@ class Field(object):
|
||||
# Conversion of values
|
||||
#
|
||||
|
||||
def null(self, env):
|
||||
""" return the null value for this field in the given environment """
|
||||
def null(self, record):
|
||||
""" Return the null value for this field in the record format. """
|
||||
return False
|
||||
|
||||
def convert_to_cache(self, value, record, validate=True):
|
||||
""" convert ``value`` to the cache level in ``env``; ``value`` may come from
|
||||
an assignment, or have the format of methods :meth:`BaseModel.read`
|
||||
or :meth:`BaseModel.write`
|
||||
""" Convert ``value`` to the cache format; ``value`` may come from an
|
||||
assignment, or have the format of methods :meth:`BaseModel.read` or
|
||||
:meth:`BaseModel.write`. If the value represents a recordset, it should
|
||||
be added for prefetching on ``record``.
|
||||
|
||||
:param record: the target record for the assignment, or an empty recordset
|
||||
|
||||
:param bool validate: when True, field-specific validation of
|
||||
``value`` will be performed
|
||||
:param bool validate: when True, field-specific validation of ``value``
|
||||
will be performed
|
||||
"""
|
||||
return value
|
||||
|
||||
def convert_to_read(self, value, use_name_get=True):
|
||||
""" convert ``value`` from the cache to a value as returned by method
|
||||
:meth:`BaseModel.read`
|
||||
def convert_to_record(self, value, record):
|
||||
""" Convert ``value`` from the cache format to the record format.
|
||||
If the value represents a recordset, it should share the prefetching of
|
||||
``record``.
|
||||
"""
|
||||
return value
|
||||
|
||||
:param bool use_name_get: when True, value's diplay name will
|
||||
be computed using :meth:`BaseModel.name_get`, if relevant
|
||||
for the field
|
||||
def convert_to_read(self, value, record, use_name_get=True):
|
||||
""" Convert ``value`` from the record format to the format returned by
|
||||
method :meth:`BaseModel.read`.
|
||||
|
||||
:param bool use_name_get: when True, the value's display name will be
|
||||
computed using :meth:`BaseModel.name_get`, if relevant for the field
|
||||
"""
|
||||
return False if value is None else value
|
||||
|
||||
def convert_to_write(self, value):
|
||||
""" convert ``value`` from the cache to a valid value for method
|
||||
:meth:`BaseModel.write`.
|
||||
def convert_to_write(self, value, record):
|
||||
""" Convert ``value`` from the record format to the format of method
|
||||
:meth:`BaseModel.write`.
|
||||
"""
|
||||
return self.convert_to_read(value)
|
||||
return self.convert_to_read(value, record)
|
||||
|
||||
def convert_to_onchange(self, value, fnames=None):
|
||||
""" convert ``value`` from the cache to a value as returned by method
|
||||
:meth:`BaseModel.onchange`.
|
||||
def convert_to_onchange(self, value, record, fnames=()):
|
||||
""" Convert ``value`` from the record format to the format returned by
|
||||
method :meth:`BaseModel.onchange`.
|
||||
|
||||
:param fnames: an optional collection of field names to convert
|
||||
(for relational fields only)
|
||||
:param fnames: an optional collection of field names to convert
|
||||
(for relational fields only)
|
||||
"""
|
||||
return self.convert_to_read(value)
|
||||
return self.convert_to_read(value, record)
|
||||
|
||||
def convert_to_export(self, value, env):
|
||||
""" convert ``value`` from the cache to a valid value for export. The
|
||||
parameter ``env`` is given for managing translations.
|
||||
"""
|
||||
def convert_to_export(self, value, record):
|
||||
""" Convert ``value`` from the record format to the export format. """
|
||||
if not value:
|
||||
return ''
|
||||
return value if env.context.get('export_raw_data') else ustr(value)
|
||||
return value if record._context.get('export_raw_data') else ustr(value)
|
||||
|
||||
def convert_to_display_name(self, value, record=None):
|
||||
""" convert ``value`` from the cache to a suitable display name. """
|
||||
def convert_to_display_name(self, value, record):
|
||||
""" Convert ``value`` from the record format to a suitable display name. """
|
||||
return ustr(value)
|
||||
|
||||
############################################################################
|
||||
@@ -821,28 +822,23 @@ class Field(object):
|
||||
if record is None:
|
||||
return self # the field is accessed through the owner class
|
||||
|
||||
if not record:
|
||||
# null record -> return the null value for this field
|
||||
return self.null(record.env)
|
||||
|
||||
# only a single record may be accessed
|
||||
record.ensure_one()
|
||||
|
||||
try:
|
||||
return record._cache[self]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# cache miss, retrieve value
|
||||
if record.id:
|
||||
# normal record -> read or compute value for this field
|
||||
self.determine_value(record)
|
||||
if record:
|
||||
# only a single record may be accessed
|
||||
record.ensure_one()
|
||||
try:
|
||||
value = record._cache[self]
|
||||
except KeyError:
|
||||
# cache miss, determine value and retrieve it
|
||||
if record.id:
|
||||
self.determine_value(record)
|
||||
else:
|
||||
self.determine_draft_value(record)
|
||||
value = record._cache[self]
|
||||
else:
|
||||
# draft record -> compute the value or let it be null
|
||||
self.determine_draft_value(record)
|
||||
# null record -> return the null value for this field
|
||||
value = self.convert_to_cache(False, record, validate=False)
|
||||
|
||||
# the result should be in cache now
|
||||
return record._cache[self]
|
||||
return self.convert_to_record(value, record)
|
||||
|
||||
def __set__(self, record, value):
|
||||
""" set the value of field ``self`` on ``record`` """
|
||||
@@ -862,7 +858,7 @@ class Field(object):
|
||||
record._cache[self] = value
|
||||
if env.in_onchange:
|
||||
for invf in record._field_inverses[self]:
|
||||
invf._update(value, record)
|
||||
invf._update(record[self.name], record)
|
||||
record._set_dirty(self.name)
|
||||
|
||||
# determine more dependent fields, and invalidate them
|
||||
@@ -872,7 +868,8 @@ class Field(object):
|
||||
|
||||
else:
|
||||
# simply write to the database, and update cache
|
||||
record.write({self.name: self.convert_to_write(value)})
|
||||
write_value = self.convert_to_write(self.convert_to_record(value, record), record)
|
||||
record.write({self.name: write_value})
|
||||
record._cache[self] = value
|
||||
|
||||
############################################################################
|
||||
@@ -885,7 +882,8 @@ class Field(object):
|
||||
# initialize the fields to their corresponding null value in cache
|
||||
computed = records._field_computed[self]
|
||||
for field in computed:
|
||||
records._cache[field] = field.null(records.env)
|
||||
for record in records:
|
||||
record._cache[field] = field.convert_to_cache(False, record, validate=False)
|
||||
records.env.computed[field].update(records._ids)
|
||||
if isinstance(self.compute, basestring):
|
||||
getattr(records, self.compute)()
|
||||
@@ -947,14 +945,15 @@ class Field(object):
|
||||
|
||||
else:
|
||||
# this is a non-stored non-computed field
|
||||
record._cache[self] = self.null(env)
|
||||
record._cache[self] = self.convert_to_cache(False, record, validate=False)
|
||||
|
||||
def determine_draft_value(self, record):
|
||||
""" Determine the value of ``self`` for the given draft ``record``. """
|
||||
if self.compute:
|
||||
self._compute_value(record)
|
||||
else:
|
||||
record._cache[self] = SpecialValue(self.null(record.env))
|
||||
null = self.convert_to_cache(False, record, validate=False)
|
||||
record._cache[self] = SpecialValue(null)
|
||||
|
||||
def determine_inverse(self, records):
|
||||
""" Given the value of ``self`` on ``records``, inverse the computation. """
|
||||
@@ -1047,8 +1046,8 @@ class Boolean(Field):
|
||||
def convert_to_cache(self, value, record, validate=True):
|
||||
return bool(value)
|
||||
|
||||
def convert_to_export(self, value, env):
|
||||
if env.context.get('export_raw_data'):
|
||||
def convert_to_export(self, value, record):
|
||||
if record._context.get('export_raw_data'):
|
||||
return value
|
||||
return ustr(value)
|
||||
|
||||
@@ -1068,7 +1067,7 @@ class Integer(Field):
|
||||
return value.get('id', False)
|
||||
return int(value or 0)
|
||||
|
||||
def convert_to_read(self, value, use_name_get=True):
|
||||
def convert_to_read(self, value, record, use_name_get=True):
|
||||
# Integer values greater than 2^31-1 are not supported in pure XMLRPC,
|
||||
# so we have to pass them as floats :-(
|
||||
if value and value > xmlrpclib.MAXINT:
|
||||
@@ -1079,9 +1078,9 @@ class Integer(Field):
|
||||
# special case, when an integer field is used as inverse for a one2many
|
||||
records._cache[self] = value.id or 0
|
||||
|
||||
def convert_to_export(self, value, env):
|
||||
def convert_to_export(self, value, record):
|
||||
if value or value == 0:
|
||||
return value if env.context.get('export_raw_data') else ustr(value)
|
||||
return value if record._context.get('export_raw_data') else ustr(value)
|
||||
return ''
|
||||
|
||||
|
||||
@@ -1125,9 +1124,9 @@ class Float(Field):
|
||||
digits = self.digits
|
||||
return float_round(value, precision_digits=digits[1]) if digits else value
|
||||
|
||||
def convert_to_export(self, value, env):
|
||||
def convert_to_export(self, value, record):
|
||||
if value or value == 0.0:
|
||||
return value if env.context.get('export_raw_data') else ustr(value)
|
||||
return value if record._context.get('export_raw_data') else ustr(value)
|
||||
return ''
|
||||
|
||||
|
||||
@@ -1165,11 +1164,12 @@ class Monetary(Field):
|
||||
"Field %s with unknown currency_field %r" % (self, self.currency_field)
|
||||
|
||||
def convert_to_cache(self, value, record, validate=True):
|
||||
currency = record[self.currency_field]
|
||||
# FIXME @rco-odoo: currency may not be already initialized if it is a
|
||||
# function or related field!
|
||||
if currency:
|
||||
return currency.round(float(value or 0.0))
|
||||
if validate:
|
||||
currency = record[self.currency_field]
|
||||
# FIXME @rco-odoo: currency may not be already initialized if it is
|
||||
# a function or related field!
|
||||
if currency:
|
||||
return currency.round(float(value or 0.0))
|
||||
return float(value or 0.0)
|
||||
|
||||
|
||||
@@ -1255,6 +1255,7 @@ class Char(_String):
|
||||
return False
|
||||
return ustr(value)[:self.size]
|
||||
|
||||
|
||||
class Text(_String):
|
||||
""" Very similar to :class:`~.Char` but used for longer contents, does not
|
||||
have a size and usually displayed as a multiline text box.
|
||||
@@ -1272,6 +1273,7 @@ class Text(_String):
|
||||
return False
|
||||
return ustr(value)
|
||||
|
||||
|
||||
class Html(_String):
|
||||
type = 'html'
|
||||
_slots = {
|
||||
@@ -1362,10 +1364,10 @@ class Date(Field):
|
||||
return value[:DATE_LENGTH]
|
||||
return self.to_string(value)
|
||||
|
||||
def convert_to_export(self, value, env):
|
||||
def convert_to_export(self, value, record):
|
||||
if not value:
|
||||
return ''
|
||||
return self.from_string(value) if env.context.get('export_raw_data') else ustr(value)
|
||||
return self.from_string(value) if record._context.get('export_raw_data') else ustr(value)
|
||||
|
||||
|
||||
class Datetime(Field):
|
||||
@@ -1433,12 +1435,12 @@ class Datetime(Field):
|
||||
return value
|
||||
return self.to_string(value)
|
||||
|
||||
def convert_to_export(self, value, env):
|
||||
def convert_to_export(self, value, record):
|
||||
if not value:
|
||||
return ''
|
||||
return self.from_string(value) if env.context.get('export_raw_data') else ustr(value)
|
||||
return self.from_string(value) if record._context.get('export_raw_data') else ustr(value)
|
||||
|
||||
def convert_to_display_name(self, value, record=None):
|
||||
def convert_to_display_name(self, value, record):
|
||||
assert record, 'Record expected'
|
||||
return Datetime.to_string(Datetime.context_timestamp(record, Datetime.from_string(value)))
|
||||
|
||||
@@ -1544,11 +1546,11 @@ class Selection(Field):
|
||||
return False
|
||||
raise ValueError("Wrong value for %s: %r" % (self, value))
|
||||
|
||||
def convert_to_export(self, value, env):
|
||||
def convert_to_export(self, value, record):
|
||||
if not isinstance(self.selection, list):
|
||||
# FIXME: this reproduces an existing buggy behavior!
|
||||
return value if value else ''
|
||||
for item in self._description_selection(env):
|
||||
for item in self._description_selection(record.env):
|
||||
if item[0] == value:
|
||||
return item[1]
|
||||
return False
|
||||
@@ -1569,24 +1571,31 @@ class Reference(Selection):
|
||||
"Reference field %s with non-integer size %r" % (self, self.size)
|
||||
|
||||
def convert_to_cache(self, value, record, validate=True):
|
||||
# cache format: (res_model, res_id) or False
|
||||
def process(res_model, res_id):
|
||||
record._prefetch[res_model].add(res_id)
|
||||
return (res_model, res_id)
|
||||
|
||||
if isinstance(value, BaseModel):
|
||||
if ((not validate or value._name in self.get_values(record.env))
|
||||
and len(value) <= 1):
|
||||
return value.with_env(record.env) or False
|
||||
if not validate or (value._name in self.get_values(record.env) and len(value) <= 1):
|
||||
return process(value._name, value.id) if value else False
|
||||
elif isinstance(value, basestring):
|
||||
res_model, res_id = value.split(',')
|
||||
return record.env[res_model].browse(int(res_id))
|
||||
return process(res_model, int(res_id))
|
||||
elif not value:
|
||||
return False
|
||||
raise ValueError("Wrong value for %s: %r" % (self, value))
|
||||
|
||||
def convert_to_read(self, value, use_name_get=True):
|
||||
def convert_to_record(self, value, record):
|
||||
return value and record.env[value[0]].browse([value[1]], record._prefetch)
|
||||
|
||||
def convert_to_read(self, value, record, use_name_get=True):
|
||||
return "%s,%s" % (value._name, value.id) if value else False
|
||||
|
||||
def convert_to_export(self, value, env):
|
||||
def convert_to_export(self, value, record):
|
||||
return value.name_get()[0][1] if value else ''
|
||||
|
||||
def convert_to_display_name(self, value, record=None):
|
||||
def convert_to_display_name(self, value, record):
|
||||
return ustr(value and value.display_name)
|
||||
|
||||
|
||||
@@ -1625,8 +1634,8 @@ class _Relational(Field):
|
||||
_column_domain = property(attrgetter('domain'))
|
||||
_column_context = property(attrgetter('context'))
|
||||
|
||||
def null(self, env):
|
||||
return env[self.comodel_name]
|
||||
def null(self, record):
|
||||
return record.env[self.comodel_name]
|
||||
|
||||
def modified(self, records):
|
||||
# Invalidate cache for inverse fields, too. Note that the recomputation
|
||||
@@ -1682,47 +1691,54 @@ class Many2one(_Relational):
|
||||
_column_auto_join = property(attrgetter('auto_join'))
|
||||
|
||||
def _update(self, records, value):
|
||||
""" Update the cached value of ``self`` for ``records`` with ``value``. """
|
||||
records._cache[self] = value
|
||||
""" Update the cached value of ``self`` for ``records`` with ``value``.
|
||||
This is used to reflect the assignment ``value[name] = records``, where
|
||||
``name`` is the inverse field of ``self``.
|
||||
"""
|
||||
records._cache[self] = self.convert_to_cache(value, records, validate=False)
|
||||
|
||||
def convert_to_cache(self, value, record, validate=True):
|
||||
if isinstance(value, (NoneType, int, long)):
|
||||
return record.env[self.comodel_name].browse(value)
|
||||
if isinstance(value, BaseModel):
|
||||
if value._name == self.comodel_name and len(value) <= 1:
|
||||
return value.with_env(record.env)
|
||||
# cache format: tuple(ids)
|
||||
def process(ids):
|
||||
return record._prefetch[self.comodel_name].update(ids) or ids
|
||||
|
||||
if type(value) in IdType:
|
||||
return process((value,))
|
||||
elif isinstance(value, BaseModel):
|
||||
if not validate or (value._name == self.comodel_name and len(value) <= 1):
|
||||
return process(value._ids)
|
||||
raise ValueError("Wrong value for %s: %r" % (self, value))
|
||||
elif isinstance(value, tuple):
|
||||
return record.env[self.comodel_name].browse(value[0])
|
||||
return process((value[0],))
|
||||
elif isinstance(value, dict):
|
||||
return record.env[self.comodel_name].new(value)
|
||||
return process(record.env[self.comodel_name].new(value)._ids)
|
||||
else:
|
||||
return self.null(record.env)
|
||||
return ()
|
||||
|
||||
def convert_to_read(self, value, use_name_get=True):
|
||||
def convert_to_record(self, value, record):
|
||||
return record.env[self.comodel_name]._browse(value, record.env, record._prefetch)
|
||||
|
||||
def convert_to_read(self, value, record, use_name_get=True):
|
||||
if use_name_get and value:
|
||||
# evaluate name_get() as superuser, because the visibility of a
|
||||
# many2one field value (id and name) depends on the current record's
|
||||
# access rights, and not the value's access rights.
|
||||
try:
|
||||
value_sudo = value.sudo()
|
||||
# performance trick: make sure that all records of the same
|
||||
# model as value in value.env will be prefetched in value_sudo.env
|
||||
value_sudo.env.prefetch[value._name].update(value.env.prefetch[value._name])
|
||||
return value_sudo.name_get()[0]
|
||||
# performance: value.sudo() prefetches the same records as value
|
||||
return value.sudo().name_get()[0]
|
||||
except MissingError:
|
||||
# Should not happen, unless the foreign key is missing.
|
||||
return False
|
||||
else:
|
||||
return value.id
|
||||
|
||||
def convert_to_write(self, value):
|
||||
def convert_to_write(self, value, record):
|
||||
return value.id
|
||||
|
||||
def convert_to_export(self, value, env):
|
||||
def convert_to_export(self, value, record):
|
||||
return value.name_get()[0][1] if value else ''
|
||||
|
||||
def convert_to_display_name(self, value, record=None):
|
||||
def convert_to_display_name(self, value, record):
|
||||
return ustr(value.display_name)
|
||||
|
||||
|
||||
@@ -1738,8 +1754,9 @@ class UnionUpdate(SpecialValue):
|
||||
# in order to read the current field's value, remove self from cache
|
||||
del record._cache[field]
|
||||
# read the current field's value, and update it in cache only
|
||||
record._cache[field] = new_value = record[field.name] | value
|
||||
return new_value
|
||||
value = field.convert_to_cache(record[field.name] | value, record, validate=False)
|
||||
record._cache[field] = value
|
||||
return value
|
||||
|
||||
|
||||
class _RelationalMulti(_Relational):
|
||||
@@ -1749,14 +1766,19 @@ class _RelationalMulti(_Relational):
|
||||
""" Update the cached value of ``self`` for ``records`` with ``value``. """
|
||||
for record in records:
|
||||
if self in record._cache:
|
||||
record._cache[self] = record[self.name] | value
|
||||
value = self.convert_to_cache(record[self.name] | value, record, validate=False)
|
||||
else:
|
||||
record._cache[self] = UnionUpdate(self, record, value)
|
||||
value = UnionUpdate(self, record, value)
|
||||
record._cache[self] = value
|
||||
|
||||
def convert_to_cache(self, value, record, validate=True):
|
||||
# cache format: tuple(ids)
|
||||
def process(ids):
|
||||
return record._prefetch[self.comodel_name].update(ids) or ids
|
||||
|
||||
if isinstance(value, BaseModel):
|
||||
if value._name == self.comodel_name:
|
||||
return value.with_env(record.env)
|
||||
if not validate or (value._name == self.comodel_name):
|
||||
return process(value._ids)
|
||||
elif isinstance(value, list):
|
||||
# value is a list of record ids or commands
|
||||
comodel = record.env[self.comodel_name]
|
||||
@@ -1785,39 +1807,43 @@ class _RelationalMulti(_Relational):
|
||||
ids.add(comodel.new(command).id)
|
||||
else:
|
||||
ids.add(command)
|
||||
# return result as a recordset
|
||||
return comodel.browse(list(ids))
|
||||
# return result as a tuple
|
||||
return process(tuple(ids))
|
||||
elif not value:
|
||||
return self.null(record.env)
|
||||
return ()
|
||||
raise ValueError("Wrong value for %s: %s" % (self, value))
|
||||
|
||||
def convert_to_read(self, value, use_name_get=True):
|
||||
def convert_to_record(self, value, record):
|
||||
return record.env[self.comodel_name]._browse(value, record.env, record._prefetch)
|
||||
|
||||
def convert_to_read(self, value, record, use_name_get=True):
|
||||
return value.ids
|
||||
|
||||
def convert_to_write(self, value):
|
||||
def convert_to_write(self, value, record):
|
||||
# make result with new and existing records
|
||||
result = [(5,)]
|
||||
for record in value:
|
||||
if not record.id:
|
||||
values = dict(record._cache)
|
||||
values = {name: record[name] for name in record._cache}
|
||||
values = record._convert_to_write(values)
|
||||
result.append((0, 0, values))
|
||||
elif record._is_dirty():
|
||||
values = {k: record._cache[k] for k in record._get_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.append((4, record.id))
|
||||
return result
|
||||
|
||||
def convert_to_onchange(self, value, fnames=None):
|
||||
def convert_to_onchange(self, value, record, fnames=()):
|
||||
# return the recordset value as a list of commands; the commands may
|
||||
# give all fields values, the client is responsible for figuring out
|
||||
# which fields are actually dirty
|
||||
fields = [(name, value._fields[name]) for name in (fnames or []) if name != 'id']
|
||||
converters = [(name, value._fields[name].convert_to_onchange)
|
||||
for name in fnames if name != 'id']
|
||||
result = [(5,)]
|
||||
for record in value:
|
||||
vals = {name: field.convert_to_onchange(record[name]) for name, field in fields}
|
||||
vals = {name: convert(record[name], record) for name, convert in converters}
|
||||
if not record.id:
|
||||
result.append((0, 0, vals))
|
||||
elif vals:
|
||||
@@ -1826,10 +1852,10 @@ class _RelationalMulti(_Relational):
|
||||
result.append((4, record.id))
|
||||
return result
|
||||
|
||||
def convert_to_export(self, value, env):
|
||||
def convert_to_export(self, value, record):
|
||||
return ','.join(name for id, name in value.name_get()) if value else ''
|
||||
|
||||
def convert_to_display_name(self, value, record=None):
|
||||
def convert_to_display_name(self, value, record):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _compute_related(self, records):
|
||||
@@ -1898,11 +1924,11 @@ class One2many(_RelationalMulti):
|
||||
_column_auto_join = property(attrgetter('auto_join'))
|
||||
_column_limit = property(attrgetter('limit'))
|
||||
|
||||
def convert_to_onchange(self, value, fnames=None):
|
||||
def convert_to_onchange(self, value, record, fnames=()):
|
||||
if fnames:
|
||||
# do not serialize self's inverse field
|
||||
fnames = [name for name in fnames if name != self.inverse_name]
|
||||
return super(One2many, self).convert_to_onchange(value, fnames)
|
||||
return super(One2many, self).convert_to_onchange(value, record, fnames)
|
||||
|
||||
|
||||
class Many2many(_RelationalMulti):
|
||||
@@ -2016,8 +2042,9 @@ class Id(Field):
|
||||
def __set__(self, record, value):
|
||||
raise TypeError("field 'id' cannot be assigned")
|
||||
|
||||
|
||||
# imported here to avoid dependency cycle issues
|
||||
from openerp import SUPERUSER_ID
|
||||
from .exceptions import Warning, AccessError, MissingError
|
||||
from .models import check_pg_name, BaseModel, MAGIC_COLUMNS
|
||||
from .exceptions import AccessError, MissingError
|
||||
from .models import check_pg_name, BaseModel, IdType
|
||||
from .osv import fields
|
||||
|
||||
+64
-34
@@ -269,7 +269,7 @@ class NewId(object):
|
||||
def __nonzero__(self):
|
||||
return False
|
||||
|
||||
IdType = (int, long, basestring, NewId)
|
||||
IdType = (int, long, str, unicode, NewId)
|
||||
|
||||
|
||||
# maximum number of prefetched records
|
||||
@@ -945,7 +945,7 @@ class BaseModel(object):
|
||||
# this part could be simpler, but it has to be done this way
|
||||
# in order to reproduce the former behavior
|
||||
if not isinstance(value, BaseModel):
|
||||
current[i] = field.convert_to_export(value, self.env)
|
||||
current[i] = field.convert_to_export(value, record)
|
||||
else:
|
||||
primary_done.append(name)
|
||||
|
||||
@@ -1383,7 +1383,6 @@ class BaseModel(object):
|
||||
parent_fields[field.model_name].append(field.name)
|
||||
|
||||
# convert default values to the right format
|
||||
defaults = self._convert_to_cache(defaults, validate=False)
|
||||
defaults = self._convert_to_write(defaults)
|
||||
|
||||
# add default values for inherited fields
|
||||
@@ -3381,7 +3380,8 @@ class BaseModel(object):
|
||||
else:
|
||||
_logger.warning("%s.read() with unknown field '%s'", self._name, name)
|
||||
|
||||
# fetch stored fields from the database to the cache
|
||||
# fetch stored fields from the database to the cache; this should feed
|
||||
# the prefetching of secondary records
|
||||
self._read_from_database(stored, inherited)
|
||||
|
||||
# retrieve results from records; this takes values from the cache and
|
||||
@@ -3393,7 +3393,7 @@ class BaseModel(object):
|
||||
try:
|
||||
values = {'id': record.id}
|
||||
for name, field in name_fields:
|
||||
values[name] = field.convert_to_read(record[name], use_name_get)
|
||||
values[name] = field.convert_to_read(record[name], record, use_name_get)
|
||||
result.append(values)
|
||||
except MissingError:
|
||||
pass
|
||||
@@ -3450,6 +3450,7 @@ class BaseModel(object):
|
||||
|
||||
# fetch records with read()
|
||||
assert self in records and field in fs
|
||||
records = records.with_prefetch(self._prefetch)
|
||||
result = []
|
||||
try:
|
||||
result = records.read([f.name for f in fs], load='_classic_write')
|
||||
@@ -3460,7 +3461,7 @@ class BaseModel(object):
|
||||
# check the cache, and update it if necessary
|
||||
if field not in self._cache:
|
||||
for values in result:
|
||||
record = self.browse(values.pop('id'))
|
||||
record = self.browse(values.pop('id'), self._prefetch)
|
||||
record._cache.update(record._convert_to_cache(values, validate=False))
|
||||
if not self._cache.contains(field):
|
||||
e = AccessError("No value found for %s.%s" % (self, field.name))
|
||||
@@ -3546,7 +3547,7 @@ class BaseModel(object):
|
||||
|
||||
# store result in cache for POST fields
|
||||
for vals in result:
|
||||
record = self.browse(vals['id'])
|
||||
record = self.browse(vals['id'], self._prefetch)
|
||||
record._cache.update(record._convert_to_cache(vals, validate=False))
|
||||
|
||||
# determine the fields that must be processed now;
|
||||
@@ -3588,7 +3589,7 @@ class BaseModel(object):
|
||||
|
||||
# store result in cache
|
||||
for vals in result:
|
||||
record = self.browse(vals.pop('id'))
|
||||
record = self.browse(vals.pop('id'), self._prefetch)
|
||||
record._cache.update(record._convert_to_cache(vals, validate=False))
|
||||
|
||||
# store failed values in cache for the records that could not be read
|
||||
@@ -5510,24 +5511,30 @@ class BaseModel(object):
|
||||
#
|
||||
|
||||
@classmethod
|
||||
def _browse(cls, env, ids):
|
||||
""" Create an instance attached to ``env``; ``ids`` is a tuple of record
|
||||
ids.
|
||||
def _browse(cls, ids, env, prefetch=None):
|
||||
""" Create a recordset instance.
|
||||
|
||||
:param ids: a tuple of record ids
|
||||
:param env: an environment
|
||||
:param prefetch: an optional prefetch object
|
||||
"""
|
||||
records = object.__new__(cls)
|
||||
records.env = env
|
||||
records._ids = ids
|
||||
env.prefetch[cls._name].update(ids)
|
||||
if prefetch is None:
|
||||
prefetch = defaultdict(set) # {model_name: set(ids)}
|
||||
records._prefetch = prefetch
|
||||
prefetch[cls._name].update(ids)
|
||||
return records
|
||||
|
||||
@api.v7
|
||||
def browse(self, cr, uid, arg=None, context=None):
|
||||
ids = _normalize_ids(arg)
|
||||
#assert all(isinstance(id, IdType) for id in ids), "Browsing invalid ids: %s" % ids
|
||||
return self._browse(Environment(cr, uid, context or {}), ids)
|
||||
return self._browse(ids, Environment(cr, uid, context or {}))
|
||||
|
||||
@api.v8
|
||||
def browse(self, arg=None):
|
||||
def browse(self, arg=None, prefetch=None):
|
||||
""" browse([ids]) -> records
|
||||
|
||||
Returns a recordset for the ids provided as parameter in the current
|
||||
@@ -5537,7 +5544,7 @@ class BaseModel(object):
|
||||
"""
|
||||
ids = _normalize_ids(arg)
|
||||
#assert all(isinstance(id, IdType) for id in ids), "Browsing invalid ids: %s" % ids
|
||||
return self._browse(self.env, ids)
|
||||
return self._browse(ids, self.env, prefetch)
|
||||
|
||||
#
|
||||
# Internal properties, for manipulating the instance's implementation
|
||||
@@ -5575,10 +5582,11 @@ class BaseModel(object):
|
||||
The new environment will not benefit from the current
|
||||
environment's data cache, so later data access may incur extra
|
||||
delays while re-fetching from the database.
|
||||
The returned recordset has the same prefetch object as ``self``.
|
||||
|
||||
:type env: :class:`~openerp.api.Environment`
|
||||
"""
|
||||
return self._browse(env, self._ids)
|
||||
return self._browse(self._ids, env, self._prefetch)
|
||||
|
||||
def sudo(self, user=SUPERUSER_ID):
|
||||
""" sudo([user=SUPERUSER])
|
||||
@@ -5606,6 +5614,7 @@ class BaseModel(object):
|
||||
re-evaluated, the new recordset will not benefit from the current
|
||||
environment's data cache, so later data access may incur extra
|
||||
delays while re-fetching from the database.
|
||||
The returned recordset has the same prefetch object as ``self``.
|
||||
|
||||
"""
|
||||
return self.with_env(self.env(user=user))
|
||||
@@ -5625,10 +5634,22 @@ class BaseModel(object):
|
||||
# -> r2._context is {'key2': True}
|
||||
r2 = records.with_context(key2=True)
|
||||
# -> r2._context is {'key1': True, 'key2': True}
|
||||
|
||||
.. note:
|
||||
|
||||
The returned recordset has the same prefetch object as ``self``.
|
||||
"""
|
||||
context = dict(args[0] if args else self._context, **kwargs)
|
||||
return self.with_env(self.env(context=context))
|
||||
|
||||
def with_prefetch(self, prefetch=None):
|
||||
""" with_prefetch([prefetch]) -> records
|
||||
|
||||
Return a new version of this recordset that uses the given prefetch
|
||||
object, or a new prefetch object if not given.
|
||||
"""
|
||||
return self._browse(self._ids, self.env, prefetch)
|
||||
|
||||
def _convert_to_cache(self, values, update=False, validate=True):
|
||||
""" Convert the ``values`` dictionary into cached values.
|
||||
|
||||
@@ -5637,20 +5658,32 @@ class BaseModel(object):
|
||||
:param validate: whether values must be checked
|
||||
"""
|
||||
fields = self._fields
|
||||
target = self if update else self.browse()
|
||||
target = self if update else self.browse([], self._prefetch)
|
||||
return {
|
||||
name: fields[name].convert_to_cache(value, target, validate=validate)
|
||||
for name, value in values.iteritems()
|
||||
if name in fields
|
||||
}
|
||||
|
||||
def _convert_to_record(self, values):
|
||||
""" Convert the ``values`` dictionary from the cache format to the
|
||||
record format.
|
||||
"""
|
||||
return {
|
||||
name: self._fields[name].convert_to_record(value, self)
|
||||
for name, value in values.iteritems()
|
||||
}
|
||||
|
||||
def _convert_to_write(self, values):
|
||||
""" Convert the ``values`` dictionary into the format of :meth:`write`. """
|
||||
fields = self._fields
|
||||
result = {}
|
||||
for name, value in values.iteritems():
|
||||
if name in fields:
|
||||
value = fields[name].convert_to_write(value)
|
||||
field = fields[name]
|
||||
value = field.convert_to_cache(value, self, validate=False)
|
||||
value = field.convert_to_record(value, self)
|
||||
value = field.convert_to_write(value, self)
|
||||
if not isinstance(value, NewId):
|
||||
result[name] = value
|
||||
return result
|
||||
@@ -5696,8 +5729,8 @@ class BaseModel(object):
|
||||
recs = self
|
||||
for name in name_seq.split('.'):
|
||||
field = recs._fields[name]
|
||||
null = field.null(self.env)
|
||||
recs = recs.mapped(lambda rec: rec._cache.get(field, null))
|
||||
null = field.convert_to_cache(False, self, validate=False)
|
||||
recs = recs.mapped(lambda rec: field.convert_to_record(rec._cache.get(field, null), rec))
|
||||
return recs
|
||||
|
||||
def filtered(self, func):
|
||||
@@ -5797,7 +5830,7 @@ class BaseModel(object):
|
||||
def __iter__(self):
|
||||
""" Return an iterator over ``self``. """
|
||||
for id in self._ids:
|
||||
yield self._browse(self.env, (id,))
|
||||
yield self._browse((id,), self.env, self._prefetch)
|
||||
|
||||
def __contains__(self, item):
|
||||
""" Test whether ``item`` (record or field name) is an element of ``self``.
|
||||
@@ -5907,9 +5940,9 @@ class BaseModel(object):
|
||||
# important: one must call the field's getter
|
||||
return self._fields[key].__get__(self, type(self))
|
||||
elif isinstance(key, slice):
|
||||
return self._browse(self.env, self._ids[key])
|
||||
return self._browse(self._ids[key], self.env)
|
||||
else:
|
||||
return self._browse(self.env, (self._ids[key],))
|
||||
return self._browse((self._ids[key],), self.env)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
""" Assign the field ``key`` to ``value`` in record ``self``. """
|
||||
@@ -5931,10 +5964,7 @@ class BaseModel(object):
|
||||
the records of model ``self`` in cache that have no value for ``field``
|
||||
(:class:`Field` instance).
|
||||
"""
|
||||
env = self.env
|
||||
prefetch_ids = env.prefetch[self._name]
|
||||
prefetch_ids.update(self._ids)
|
||||
ids = filter(None, prefetch_ids - set(env.cache[field]))
|
||||
ids = filter(None, self._prefetch[self._name] - set(self.env.cache[field]))
|
||||
return self.browse(ids)
|
||||
|
||||
@api.model
|
||||
@@ -6078,7 +6108,7 @@ class BaseModel(object):
|
||||
return
|
||||
if res.get('value'):
|
||||
res['value'].pop('id', None)
|
||||
self.update(self._convert_to_cache(res['value'], validate=False))
|
||||
self.update({key: val for key, val in res['value'].iteritems() if key in self._fields})
|
||||
if res.get('domain'):
|
||||
result.setdefault('domain', {}).update(res['domain'])
|
||||
if res.get('warning'):
|
||||
@@ -6111,9 +6141,9 @@ class BaseModel(object):
|
||||
def __init__(self, record):
|
||||
self._record = record
|
||||
def __getitem__(self, name):
|
||||
field = self._record._fields[name]
|
||||
value = self._record[name]
|
||||
return field.convert_to_write(value)
|
||||
record = self._record
|
||||
field = record._fields[name]
|
||||
return field.convert_to_write(record[name], record)
|
||||
def __getattr__(self, name):
|
||||
return self[name]
|
||||
|
||||
@@ -6167,7 +6197,7 @@ class BaseModel(object):
|
||||
# create a new record with values, and attach ``self`` to it
|
||||
with env.do_in_onchange():
|
||||
record = self.new(values)
|
||||
values = dict(record._cache)
|
||||
values = {name: record[name] for name in record._cache}
|
||||
# attach ``self`` with a different context (for cache consistency)
|
||||
record._origin = self.with_context(__onchange=True)
|
||||
|
||||
@@ -6228,7 +6258,7 @@ class BaseModel(object):
|
||||
|
||||
# collect values from dirty fields
|
||||
result['value'] = {
|
||||
name: self._fields[name].convert_to_onchange(record[name], subfields.get(name))
|
||||
name: self._fields[name].convert_to_onchange(record[name], record, subfields[name])
|
||||
for name in dirty
|
||||
}
|
||||
|
||||
@@ -6392,7 +6422,7 @@ PGERROR_TO_OE = defaultdict(
|
||||
'23505': convert_pgerror_23505,
|
||||
})
|
||||
|
||||
def _normalize_ids(arg, atoms={int, long, str, unicode, NewId}):
|
||||
def _normalize_ids(arg, atoms=set(IdType)):
|
||||
""" Normalizes the ids argument for ``browse`` (v7 and v8) to a tuple.
|
||||
|
||||
Various implementations were tested on the corpus of all browse() calls
|
||||
|
||||
Reference in New Issue
Block a user