[REM] openerp.osv.fields: remove old-style function fields

This commit is contained in:
Raphael Collet
2016-08-31 17:21:48 +02:00
parent 0fc697a3f7
commit 2b587d34ff
8 changed files with 16 additions and 927 deletions
+2 -3
View File
@@ -161,9 +161,8 @@ class Partner(models.Model):
@api.model
def _cron_update_membership(self):
partners = self.search([('membership_state', 'in', ['invoiced', 'paid'])])
if partners:
partners._store_set_values(['membership_state'])
# used to recompute 'membership_state'; should no longer be necessary
pass
@api.multi
def create_membership_invoice(self, product_id=None, datas=None):
-8
View File
@@ -1105,13 +1105,6 @@ class Environment(Mapping):
finally:
self.all.recompute = tmp
@property
def recompute_old(self):
return self.all.recompute_old
def clear_recompute_old(self):
del self.all.recompute_old[:]
class Environments(object):
""" A common object for all environments in a request. """
@@ -1120,7 +1113,6 @@ class Environments(object):
self.todo = {} # recomputations {field: [records]}
self.mode = False # flag for draft/onchange
self.recompute = True
self.recompute_old = [] # list of old api compute fields to recompute
def add(self, env):
""" Add the environment ``env``. """
-4
View File
@@ -447,10 +447,6 @@ class Field(object):
if not attrs.get('readonly'):
attrs['inverse'] = self._inverse_sparse
# fix for function fields overridden by regular columns
if not isinstance(attrs.get('origin'), (NoneType, fields.function)):
attrs.pop('store', None)
self.set_all_attrs(attrs)
if not self.string and not self.related:
+12 -341
View File
@@ -201,11 +201,6 @@ def get_pg_type(f, type_override=None):
pg_type = ('int4', 'INTEGER')
else:
pg_type = ('varchar', pg_varchar(getattr(f, 'size', None)))
elif issubclass(field_type, fields.function):
if f._type == 'selection':
pg_type = ('varchar', pg_varchar())
else:
pg_type = get_pg_type(f, getattr(fields, f._type))
else:
_logger.warning('%s type not supported!', field_type)
pg_type = None
@@ -700,44 +695,6 @@ class BaseModel(object):
child_class = type(pool[child_name])
child_class._build_model_attributes(pool)
@classmethod
def _init_function_fields(cls, pool, cr):
# initialize the list of non-stored function fields for this model
pool._pure_function_fields[cls._name] = []
# process store of low-level function fields
for fname, column in cls._columns.iteritems():
# filter out existing store about this field
pool._store_function[cls._name] = [
stored
for stored in pool._store_function.get(cls._name, [])
if (stored[0], stored[1]) != (cls._name, fname)
]
if not isinstance(column, fields.function):
continue
if not column.store:
# register it on the pool for invalidation
pool._pure_function_fields[cls._name].append(fname)
continue
# process store parameter
store = column.store
if store is True:
get_ids = lambda self, cr, uid, ids, c={}: ids
store = {cls._name: (get_ids, None, column.priority, None)}
for model, spec in store.iteritems():
if len(spec) == 4:
(fnct, fields2, order, length) = spec
elif len(spec) == 3:
(fnct, fields2, order) = spec
length = None
else:
raise UserError(_('Invalid function definition %s in object %s !\nYou must use the definition: store={object:(fnct, fields, priority, time length)}.') % (fname, cls._name))
pool._store_function.setdefault(model, [])
t = (cls._name, fname, fnct, tuple(fields2) if fields2 else None, order, length)
if t not in pool._store_function[model]:
pool._store_function[model].append(t)
pool._store_function[model].sort(key=lambda x: x[4])
@api.model
def _add_manual_fields(self, partial):
manual_fields = self.pool.get_manual_fields(self._cr, self._name)
@@ -2258,25 +2215,6 @@ class BaseModel(object):
self.invalidate_cache(['parent_left', 'parent_right'])
return True
@api.model_cr
def _update_store(self, column, name):
_logger.info("storing computed values of fields.function '%s'", name)
cr = self._cr
setc, setf = column._symbol_set
update_query = 'UPDATE "%s" SET "%s"=%s WHERE id=%%s' % (self._table, name, setc)
cr.execute('SELECT id FROM "%s"' % self._table)
ids = [row[0] for row in cr.fetchall()]
for sub_ids in cr.split_for_in_conditions(ids, AUTOINIT_RECALCULATE_STORED_FIELDS):
res = column.get(cr, self._model, sub_ids, name, SUPERUSER_ID, {})
for id, val in res.iteritems():
if column._multi:
val = val[name]
# if val is a many2one, just write the ID
if isinstance(val, tuple):
val = val[0]
if column._type == 'boolean' or val is not False:
cr.execute(update_query, (setf(val), id))
@api.model
def _check_selection_field_value(self, field, value):
""" Check whether value is among the valid values for the given
@@ -2291,9 +2229,7 @@ class BaseModel(object):
# fields which were required but have been removed (or will be added by
# another module)
cr = self._cr
cols = [name
for name, column in self._columns.iteritems()
if not (isinstance(column, fields.function) and not column.store)]
cols = list(self._columns)
cr.execute("SELECT a.attname, a.attnotnull"
" FROM pg_class c, pg_attribute a"
" WHERE c.relname=%s"
@@ -2551,16 +2487,7 @@ class BaseModel(object):
f_pg_type = res['typname']
f_pg_size = res['size']
f_pg_notnull = res['attnotnull']
if isinstance(column, fields.function) and not column.store and \
not getattr(column, 'nodrop', False):
_logger.info('column %s (%s) converted to a function, removed from table %s',
name, column.string, self._table)
cr.execute('ALTER TABLE "%s" DROP COLUMN "%s" CASCADE' % (self._table, name))
cr.commit()
_schema.debug("Table '%s': dropped column '%s' with cascade", self._table, name)
f_obj_type = None
else:
f_obj_type = get_pg_type(column) and get_pg_type(column)[0]
f_obj_type = get_pg_type(column) and get_pg_type(column)[0]
if f_obj_type:
converted = False
@@ -2667,7 +2594,7 @@ class BaseModel(object):
msg = "Table '%s': dropping index for column '%s' of type '%s' as it is not required anymore"
_schema.debug(msg, self._table, name, column._type)
if isinstance(column, fields.many2one) or (isinstance(column, fields.function) and column._type == 'many2one' and column.store):
if isinstance(column, fields.many2one):
dest_model = self.env[column._obj]
if dest_model._auto and dest_model._table != 'ir_actions':
self._m2o_fix_foreign_key(self._table, name, dest_model, column.ondelete)
@@ -2683,13 +2610,6 @@ class BaseModel(object):
if has_rows:
self._init_column(name)
# remember the functions to call for the stored fields
if isinstance(column, fields.function):
order = 10
if column.store is not True: # i.e. if column.store is a dict
order = column.store[column.store.keys()[0]][2]
todo_end.append((order, self._update_store, (column, name)))
# remember new-style stored fields with compute method
if self._fields[name].compute:
stored_fields.append(self._fields[name])
@@ -3102,9 +3022,6 @@ class BaseModel(object):
with tools.ignore(*exceptions):
field.setup_triggers(self)
# register stuff about low-level function fields
cls._init_function_fields(cls.pool, self._cr)
# register constraints and onchange methods
cls._init_constraints_onchanges()
@@ -3268,17 +3185,6 @@ class BaseModel(object):
# discard fields that must be recomputed
if not (f.compute and self.env.field_todo(f))
)
elif field.column._multi:
# prefetch all function fields with the same value for 'multi'
multi = field.column._multi
fs.update(
f
for f in self._fields.itervalues()
# select stored fields with the same multi
if f.column and f.column._multi == multi
# discard fields with groups that the user may not access
if not (f.groups and not self.user_has_groups(f.groups))
)
# special case: discard records to recompute for field
records -= self.env.field_todo(field)
@@ -3400,33 +3306,13 @@ class BaseModel(object):
# determine the fields that must be processed now;
# for the sake of simplicity, we ignore inherited fields
fields_post = [f for f in field_names if not self._columns[f]._classic_write]
# Compute POST fields, grouped by multi
by_multi = defaultdict(list)
for f in fields_post:
by_multi[self._columns[f]._multi].append(f)
for multi, fs in by_multi.iteritems():
if multi:
res2 = self._columns[fs[0]].get(cr, self._model, ids, fs, user, context=context, values=result)
assert res2 is not None, \
'The function field "%s" on the "%s" model returned None\n' \
'(a dictionary was expected).' % (fs[0], self._name)
for vals in result:
# TOCHECK : why got string instend of dict in python2.6
# if isinstance(res2[vals['id']], str): res2[vals['id']] = safe_eval(res2[vals['id']])
multi_fields = res2.get(vals['id'], {})
if multi_fields:
for f in fs:
vals[f] = multi_fields.get(f, [])
else:
for f in fs:
res2 = self._columns[f].get(cr, self._model, ids, f, user, context=context, values=result)
for vals in result:
if res2:
vals[f] = res2[vals['id']]
else:
vals[f] = []
res2 = self._columns[f].get(cr, self._model, ids, f, user, context=context, values=result)
for vals in result:
if res2:
vals[f] = res2[vals['id']]
else:
vals[f] = []
# Warn about deprecated fields now that fields_pre and fields_post are computed
for f in field_names:
@@ -3650,8 +3536,7 @@ class BaseModel(object):
if not self:
return True
# for recomputing old-style and new-style fields
result_store = self._store_get_values(self._fields)
# for recomputing fields
self.modified(self._fields)
self._check_concurrency()
@@ -3712,16 +3597,6 @@ class BaseModel(object):
# changes made in the database, like cascading delete!
self.invalidate_cache()
# recompute old-style fields
for order, model_name, ids, fields in result_store:
ids = set(ids) - set(self.ids) if model_name == self._name else ids
if ids:
model = self.env[model_name]
query = "SELECT id FROM %s WHERE id IN %%s" % model._table
cr.execute(query, (tuple(ids),))
recs = model.browse([row[0] for row in cr.fetchall()])
recs._store_set_values(fields)
# recompute new-style fields
if self.env.recompute and self._context.get('recompute', True):
self.recompute()
@@ -3861,16 +3736,6 @@ class BaseModel(object):
cr = self._cr
# for recomputing old-style fields
deleted_related = defaultdict(list) # {model_name: ids}
for name, val in vals.iteritems():
field = self._fields[name]
if field.type in ('one2many', 'many2many') and val:
for command in val:
if isinstance(command, (tuple, list)) and command[0] == 2:
deleted_related[field.comodel_name].append(command[1])
result_store = self._store_get_values(list(vals))
# for recomputing new-style fields
extra_fields = ['write_date', 'write_uid'] if self._log_access else []
self.modified(list(vals) + extra_fields)
@@ -3968,6 +3833,7 @@ class BaseModel(object):
if not key.startswith('default_')}
# call the 'set' method of fields which are not classic_write
result_store = []
for name in sorted(upd_todo, key=lambda name: self._columns[name].priority):
column = self._columns[name]
for id in self.ids:
@@ -4060,23 +3926,6 @@ class BaseModel(object):
self.invalidate_cache(['parent_left', 'parent_right'])
result_store += self._store_get_values(vals)
done = defaultdict(set) # {(model, fields): ids}
self.env.recompute_old.extend(result_store)
while self.env.recompute_old:
sorted_recompute_old = sorted(self.env.recompute_old)
self.env.clear_recompute_old()
for order, model_name, ids, fnames in sorted_recompute_old:
key = (model_name, tuple(fnames))
# avoid to do several times the same computation
ids = [id for id in ids
if id not in done[key]
if id not in deleted_related[model_name]]
done[key].update(ids)
recs = self.env[model_name].browse(ids)
recs._store_set_values(fnames)
# recompute new-style fields
if self.env.recompute and self._context.get('recompute', True):
self.recompute()
@@ -4202,27 +4051,7 @@ class BaseModel(object):
if column._classic_write:
setc, setf = column._symbol_set
updates.append((name, setc, setf(val)))
# for the function fields that receive a value, we set them
# directly in the database (they may be required), but we also
# need to trigger the _fct_inv()
if (hasattr(column, '_fnct_inv')) and not isinstance(column, fields.related):
# TODO: this way to special case the related fields is
# really creepy but it shouldn't be changed at one week of
# the release candidate. It seems the only good way to
# handle correctly this is to add an attribute to make a
# field `really readonly´ and thus totally ignored by the
# create()... otherwise if, for example, the related has a
# default value (for usability) then the fct_inv is called
# and it may raise some access rights error. Changing this
# is a too big change for now, and is thus postponed after
# the release but, definitively, the behavior shouldn't be
# different for related and function fields.
upd_todo.append(name)
elif not isinstance(column, fields.related):
# TODO: this special case should be removed because there is no
# good reason to special case the fields related. See the above
# comment for further explanations.
else:
upd_todo.append(name)
if hasattr(column, 'selection') and val:
@@ -4313,20 +4142,7 @@ class BaseModel(object):
# check Python constraints
self._validate_fields(vals)
result_store += self._store_get_values(list(set(vals.keys() + self._inherits.values())))
self.env.recompute_old.extend(result_store)
if self.env.recompute and self._context.get('recompute', True):
done = []
while self.env.recompute_old:
sorted_recompute_old = sorted(self.env.recompute_old)
self.env.clear_recompute_old()
for order, model_name, ids, fnames in sorted_recompute_old:
if (model_name, ids, fnames) not in done:
recs = self.env[model_name].browse(ids)
recs._store_set_values(fnames)
done.append((model_name, ids, fnames))
# recompute new-style fields
self.recompute()
@@ -4334,142 +4150,6 @@ class BaseModel(object):
self.create_workflow()
return id_new
@api.multi
def _store_get_values(self, fields):
""" Returns an ordered list of fields.function to call due to an update
operation on ``fields`` of records ``self``, obtained by calling the
'store' triggers of these fields, as setup by their 'store' attribute.
:return: [(priority, model_name, [record_ids,], [function_fields,])]
"""
fields = fields or []
# use indexed names for the details of the triggers:
MODEL, FUNC, TARGET_FUNC, FIELDS, PRIORITY = range(5)
# only keep store triggers that should be triggered for the ``fields``
# being written to.
triggers = [
trigger
for trigger in self.pool._store_function.get(self._name, [])
if not trigger[FIELDS] or set(fields).intersection(trigger[FIELDS])
]
to_compute_map = defaultdict(lambda: defaultdict(set))
target_id_results = {}
for trigger in triggers:
target_func_id = id(trigger[TARGET_FUNC])
if target_func_id not in target_id_results:
# use admin user for accessing objects having rules defined on store fields
ids = trigger[TARGET_FUNC](self._model, self._cr, SUPERUSER_ID, self.ids, self._context)
target_id_results[target_func_id] = filter(None, ids)
target_ids = target_id_results[target_func_id]
# the compound key must consider the priority and model name
key = (trigger[PRIORITY], trigger[MODEL])
for target_id in target_ids:
to_compute_map[key][target_id].add(tuple(trigger))
# Here to_compute_map looks like:
# { (10, 'model_a') : { target_id1: [ (trigger_1_tuple, trigger_2_tuple) ], ... }
# (20, 'model_a') : { target_id2: [ (trigger_3_tuple, trigger_4_tuple) ], ... }
# (99, 'model_a') : { target_id1: [ (trigger_5_tuple, trigger_6_tuple) ], ... }
# }
# Now we need to generate the batch function calls list
# call_map =
# { (10, 'model_a') : [(10, 'model_a', [record_ids,], [function_fields,])] }
call_map = defaultdict(list)
for ((priority, model), id_map) in to_compute_map.iteritems():
trigger_ids_maps = defaultdict(list)
# function_ids_maps =
# { (function_1_tuple, function_2_tuple) : [target_id1, target_id2, ..] }
for target_id, triggers in id_map.iteritems():
trigger_ids_maps[tuple(triggers)].append(target_id)
for triggers, target_ids in trigger_ids_maps.iteritems():
call_map[(priority, model)].append((priority, model, target_ids, [t[FUNC] for t in triggers]))
return list(itertools.chain.from_iterable(v for k, v in sorted(call_map.iteritems())))
@api.multi
def _store_set_values(self, fields):
""" Call the fields.function's "implementation function" for all
``fields``, on records ``self`` (taking care of respecting ``multi``
attributes), and store the resulting values in the database directly.
"""
if not self:
return True
from .fields import Datetime
cr = self._cr
field_dict = defaultdict(list)
if self._log_access:
cr.execute('SELECT id, write_date FROM %s WHERE id IN %%s' % self._table, [tuple(self.ids)])
for id, write_date in cr.fetchall():
if write_date:
write_date = Datetime.from_string(write_date)
for trigger in self.pool._store_function.get(self._name, []):
if trigger[5]:
up_write_date = write_date + datetime.timedelta(hours=trigger[5])
if datetime.datetime.now() < up_write_date:
if trigger[1] in fields:
field_dict[id].append(trigger[1])
todo = defaultdict(list)
for name in fields:
todo[self._columns[name]._multi].append(name)
for key, names in todo.iteritems():
if key:
column = self._columns[names[0]]
# use admin user for accessing objects having rules defined on store fields
result = column.get(cr, self._model, self.ids, names, SUPERUSER_ID, context=self._context)
for id, vals in result.iteritems():
if field_dict:
for name in field_dict[id]:
vals.pop(name, None)
updates = [] # list of (column, pattern, value)
for name, val in vals.iteritems():
if name not in names:
continue
column = self._columns[name]
if column._type == 'many2one':
try:
val = val[0]
except:
pass
setc, setf = column._symbol_set
updates.append((name, setc, setf(val)))
if updates:
query = 'UPDATE "%s" SET %s WHERE id=%%s' % (
self._table, ','.join('"%s"=%s' % u[:2] for u in updates),
)
params = tuple(u[2] for u in updates)
cr.execute(query, params + (id,))
else:
for name in names:
column = self._columns[name]
# use admin user for accessing objects having rules defined on store fields
result = column.get(cr, self._model, self.ids, name, SUPERUSER_ID, context=self._context)
for rid in result.keys():
if rid in field_dict and name in field_dict[rid]:
result.pop(rid)
for id, val in result.iteritems():
if column._type == 'many2one':
try:
val = val[0]
except:
pass
setc, setf = column._symbol_set
query = 'UPDATE "%s" SET "%s"=%s WHERE id=%%s' % (self._table, name, setc)
cr.execute(query, (setf(val), id))
# invalidate and mark new-style fields to recompute
self.modified(fields)
return True
# TODO: ameliorer avec NULL
@api.model
def _where_calc(self, domain, active_test=True):
@@ -5754,15 +5434,6 @@ class BaseModel(object):
for fname in fnames:
spec += self._fields[fname].modified(self)
cached_fields = {
field
for env in self.env.all
for field in env.cache
}
# invalidate non-stored fields.function which are currently cached
spec += [(f, None) for f in self.pool.pure_function_fields
if f in cached_fields]
self.env.invalidate(spec)
def _recompute_check(self, field):
-12
View File
@@ -29,8 +29,6 @@ class Registry(Mapping):
super(Registry, self).__init__()
self.models = {} # model name/model instance mapping
self._sql_error = {}
self._store_function = {}
self._pure_function_fields = {} # {model: [field, ...], ...}
self._init = True
self._init_parent = {}
self._assertion_report = assertion_report.assertion_report()
@@ -92,16 +90,6 @@ class Registry(Mapping):
def model_cache(self):
return RegistryManager.model_cache
@lazy_property
def pure_function_fields(self):
""" Return the list of pure function fields (field objects) """
fields = []
for mname, fnames in self._pure_function_fields.iteritems():
model_fields = self[mname]._fields
for fname in fnames:
fields.append(model_fields[fname])
return fields
@lazy_property
def field_sequence(self):
""" Return a function mapping a field to an integer. The value of a
-32
View File
@@ -908,38 +908,6 @@ class expression(object):
for elem in reversed(domain):
push(create_substitution_leaf(leaf, elem, model, internal=True))
# -------------------------------------------------
# FUNCTION FIELD
# -> not stored: error if no _fnct_search, otherwise handle the result domain
# -> stored: management done in the remaining of parsing
# -------------------------------------------------
elif isinstance(column, fields.function) and not column.store:
# this is a function field that is not stored
if not column._fnct_search:
_logger.error(
"Field '%s' (%s) can not be searched: "
"non-stored function field without fnct_search",
column.string, left)
# avoid compiling stack trace if not needed
if _logger.isEnabledFor(logging.DEBUG):
_logger.debug(''.join(traceback.format_stack()))
# ignore it: generate a dummy leaf
fct_domain = []
else:
fct_domain = column.search(cr, uid, model._model, left, [leaf.leaf], context=context)
if not fct_domain:
leaf.leaf = TRUE_LEAF
push(leaf)
else:
# we assume that the expression is valid
# we create a dummy leaf for forcing the parsing of the resulting expression
for domain_element in reversed(fct_domain):
push(create_substitution_leaf(leaf, domain_element, model))
# self.push(create_substitution_leaf(leaf, TRUE_LEAF, model))
# self.push(create_substitution_leaf(leaf, AND_OPERATOR, model))
# -------------------------------------------------
# RELATIONAL FIELDS
# -------------------------------------------------
-525
View File
@@ -848,7 +848,6 @@ class one2many(_column):
if act[0] == 0:
act[2][self._fields_id] = id
id_new = obj.create(cr, user, act[2], context=context)
result += obj._store_get_values(cr, user, [id_new], act[2].keys(), context)
elif act[0] == 1:
obj.write(cr, user, [act[1]], act[2], context=context)
elif act[0] == 2:
@@ -1159,500 +1158,6 @@ def sanitize_binary_value(value):
return value
# ---------------------------------------------------------
# Function fields
# ---------------------------------------------------------
class function(_column):
"""
A field whose value is computed by a function (rather
than being read from the database).
:param fnct: the callable that will compute the field value.
:param arg: arbitrary value to be passed to ``fnct`` when computing the value.
:param fnct_inv: the callable that will allow writing values in that field
(if not provided, the field is read-only).
:param fnct_inv_arg: arbitrary value to be passed to ``fnct_inv`` when
writing a value.
:param str type: type of the field simulated by the function field
:param fnct_search: the callable that allows searching on the field
(if not provided, search will not return any result).
:param store: store computed value in database
(see :ref:`The *store* parameter <field-function-store>`).
:type store: True or dict specifying triggers for field computation
:param multi: name of batch for batch computation of function fields.
All fields with the same batch name will be computed by
a single function call. This changes the signature of the
``fnct`` callable.
.. _field-function-fnct: The ``fnct`` parameter
.. rubric:: The ``fnct`` parameter
The callable implementing the function field must have the following signature:
.. function:: fnct(model, cr, uid, ids, field_name(s), arg, context)
Implements the function field.
:param orm model: model to which the field belongs (should be ``self`` for
a model method)
:param field_name(s): name of the field to compute, or if ``multi`` is provided,
list of field names to compute.
:type field_name(s): str | [str]
:param arg: arbitrary value passed when declaring the function field
:rtype: dict
:return: mapping of ``ids`` to computed values, or if multi is provided,
to a map of field_names to computed values
The values in the returned dictionary must be of the type specified by the type
argument in the field declaration.
Here is an example with a simple function ``char`` function field::
# declarations
def compute(self, cr, uid, ids, field_name, arg, context):
result = {}
# ...
return result
_columns['my_char'] = fields.function(compute, type='char', size=50)
# when called with ``ids=[1,2,3]``, ``compute`` could return:
{
1: 'foo',
2: 'bar',
3: False # null values should be returned explicitly too
}
If ``multi`` is set, then ``field_name`` is replaced by ``field_names``: a list
of the field names that should be computed. Each value in the returned
dictionary must then be a dictionary mapping field names to values.
Here is an example where two function fields (``name`` and ``age``)
are both computed by a single function field::
# declarations
def compute(self, cr, uid, ids, field_names, arg, context):
result = {}
# ...
return result
_columns['name'] = fields.function(compute_person_data, type='char',\
size=50, multi='person_data')
_columns[''age'] = fields.function(compute_person_data, type='integer',\
multi='person_data')
# when called with ``ids=[1,2,3]``, ``compute_person_data`` could return:
{
1: {'name': 'Bob', 'age': 23},
2: {'name': 'Sally', 'age': 19},
3: {'name': 'unknown', 'age': False}
}
.. _field-function-fnct-inv:
.. rubric:: The ``fnct_inv`` parameter
This callable implements the write operation for the function field
and must have the following signature:
.. function:: fnct_inv(model, cr, uid, id, field_name, field_value, fnct_inv_arg, context)
Callable that implements the ``write`` operation for the function field.
:param orm model: model to which the field belongs (should be ``self`` for
a model method)
:param int id: the identifier of the object to write on
:param str field_name: name of the field to set
:param fnct_inv_arg: arbitrary value passed when declaring the function field
:return: True
When writing values for a function field, the ``multi`` parameter is ignored.
.. _field-function-fnct-search:
.. rubric:: The ``fnct_search`` parameter
This callable implements the search operation for the function field
and must have the following signature:
.. function:: fnct_search(model, cr, uid, model_again, field_name, criterion, context)
Callable that implements the ``search`` operation for the function field by expanding
a search criterion based on the function field into a new domain based only on
columns that are stored in the database.
:param orm model: model to which the field belongs (should be ``self`` for
a model method)
:param orm model_again: same value as ``model`` (seriously! this is for backwards
compatibility)
:param str field_name: name of the field to search on
:param list criterion: domain component specifying the search criterion on the field.
:rtype: list
:return: domain to use instead of ``criterion`` when performing the search.
This new domain must be based only on columns stored in the database, as it
will be used directly without any translation.
The returned value must be a domain, that is, a list of the form [(field_name, operator, operand)].
The most generic way to implement ``fnct_search`` is to directly search for the records that
match the given ``criterion``, and return their ``ids`` wrapped in a domain, such as
``[('id','in',[1,3,5])]``.
.. _field-function-store:
.. rubric:: The ``store`` parameter
The ``store`` parameter allows caching the result of the field computation in the
database, and defining the triggers that will invalidate that cache and force a
recomputation of the function field.
When not provided, the field is computed every time its value is read.
The value of ``store`` may be either ``True`` (to recompute the field value whenever
any field in the same record is modified), or a dictionary specifying a more
flexible set of recomputation triggers.
A trigger specification is a dictionary that maps the names of the models that
will trigger the computation, to a tuple describing the trigger rule, in the
following form::
store = {
'trigger_model': (mapping_function,
['trigger_field1', 'trigger_field2'],
priority),
}
A trigger rule is defined by a 3-item tuple where:
* The ``mapping_function`` is defined as follows:
.. function:: mapping_function(trigger_model, cr, uid, trigger_ids, context)
Callable that maps record ids of a trigger model to ids of the
corresponding records in the source model (whose field values
need to be recomputed).
:param orm model: trigger_model
:param list trigger_ids: ids of the records of trigger_model that were
modified
:rtype: list
:return: list of ids of the source model whose function field values
need to be recomputed
* The second item is a list of the fields who should act as triggers for
the computation. If an empty list is given, all fields will act as triggers.
* The last item is the priority, used to order the triggers when processing them
after any write operation on a model that has function field triggers. The
default priority is 10.
In fact, setting store = True is the same as using the following trigger dict::
store = {
'model_itself': (lambda self, cr, uid, ids, context: ids,
[],
10)
}
"""
_properties = True
__slots__ = [
'_type',
'_classic_read',
'_classic_write',
'_symbol_c',
'_symbol_f',
'_symbol_set',
'_symbol_get',
'_fnct',
'_arg',
'_fnct_inv',
'_fnct_inv_arg',
'_fnct_search',
'_multi',
'store',
'_digits',
'_digits_compute',
'selection',
'_obj',
]
@property
def digits(self):
if self._digits_compute:
with LazyCursor() as cr:
return self._digits_compute(cr)
else:
return self._digits
#
# multi: compute several fields in one call
#
def __init__(self, fnct, arg=None, fnct_inv=None, fnct_inv_arg=None, type='float', fnct_search=None, obj=None, store=False, multi=False, **args):
self._classic_read = False
self._classic_write = False
self._prefetch = False
self._symbol_c = '%s'
self._symbol_f = _symbol_set
self._symbol_set = (self._symbol_c, self._symbol_f)
self._symbol_get = None
# pop attributes that should not be assigned to self
self._digits = args.pop('digits', (16,2))
self._digits_compute = args.pop('digits_compute', None)
self._obj = args.pop('relation', obj)
# function fields are not copied by default
args['copy'] = args.get('copy', False)
_column.__init__(self, **args)
self._type = type
self._fnct = fnct
self._arg = arg
self._fnct_inv = fnct_inv
self._fnct_inv_arg = fnct_inv_arg
self._fnct_search = fnct_search
self.store = store
self._multi = multi
if not fnct_inv:
self.readonly = 1
if not fnct_search and not store:
self.selectable = False
if callable(args.get('selection')):
from openerp import api
self.selection = api.expected(api.cr_uid_context, args['selection'])
if store:
if self._type != 'many2one':
# m2o fields need to return tuples with name_get, not just foreign keys
self._classic_read = True
self._classic_write = True
if type=='binary':
self._symbol_get=lambda x:x and str(x)
else:
self._prefetch = True
if type == 'char':
self._symbol_c = char._symbol_c
self._symbol_f = lambda x: _symbol_set_char(self, x)
self._symbol_set = (self._symbol_c, self._symbol_f)
elif type == 'float':
self._symbol_c = float._symbol_c
self._symbol_f = lambda x: _symbol_set_float(self, x)
self._symbol_set = (self._symbol_c, self._symbol_f)
else:
type_class = globals().get(type)
if type_class is not None:
self._symbol_c = type_class._symbol_c
self._symbol_f = type_class._symbol_f
self._symbol_set = type_class._symbol_set
def new(self, _computed_field=False, **args):
if _computed_field:
# field is computed, we need an instance of a non-function column
type_class = globals()[self._type]
return type_class(**args)
else:
# HACK: function fields are tricky to recreate, simply return a copy
import copy
return copy.copy(self)
def to_field_args(self):
args = super(function, self).to_field_args()
args['store'] = bool(self.store)
args['company_dependent'] = False
if self._type in ('float',):
args['digits'] = self._digits_compute or self._digits
elif self._type in ('binary',):
# limitation: binary function fields cannot be stored in attachments
args['attachment'] = False
elif self._type in ('selection', 'reference'):
args['selection'] = self.selection
elif self._type in ('many2one', 'one2many', 'many2many'):
args['comodel_name'] = self._obj
return args
def digits_change(self, cr):
pass
def search(self, cr, uid, obj, name, args, context=None):
if not self._fnct_search:
#CHECKME: should raise an exception
return []
return self._fnct_search(obj, cr, uid, obj, name, args, context=context)
def postprocess(self, cr, uid, obj, field, value=None, context=None):
return self._postprocess_batch(cr, uid, obj, field, {0: value}, context=context)[0]
def _postprocess_batch(self, cr, uid, obj, field, values, context=None):
if not values:
return values
if context is None:
context = {}
field_type = obj._columns[field]._type
new_values = dict(values)
if field_type == 'binary':
if context.get('bin_size'):
# client requests only the size of binary fields
for rid, value in values.iteritems():
if value:
new_values[rid] = get_nice_size(value)
elif not context.get('bin_raw'):
for rid, value in values.iteritems():
if value:
new_values[rid] = sanitize_binary_value(value)
return new_values
def get(self, cr, obj, ids, name, uid=False, context=None, values=None):
multi = self._multi
# if we already have a value, don't recompute it.
# This happen if case of stored many2one fields
if values and not multi and name in values[0]:
result = dict((v['id'], v[name]) for v in values)
elif values and multi and all(n in values[0] for n in name):
result = dict((v['id'], dict((n, v[n]) for n in name)) for v in values)
else:
result = self._fnct(obj, cr, uid, ids, name, self._arg, context)
if multi:
swap = {}
for rid, values in result.iteritems():
for f, v in values.iteritems():
if f not in name:
continue
swap.setdefault(f, {})[rid] = v
for field, values in swap.iteritems():
new_values = self._postprocess_batch(cr, uid, obj, field, values, context)
for rid, value in new_values.iteritems():
result[rid][field] = value
else:
result = self._postprocess_batch(cr, uid, obj, name, result, context)
return result
def set(self, cr, obj, id, name, value, user=None, context=None):
if not context:
context = {}
if self._fnct_inv:
self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
@classmethod
def _as_display_name(cls, field, cr, uid, obj, value, context=None):
# Function fields are supposed to emulate a basic field type,
# so they can delegate to the basic type for record name rendering
return globals()[field._type]._as_display_name(field, cr, uid, obj, value, context=context)
# ---------------------------------------------------------
# Related fields
# ---------------------------------------------------------
class related(function):
"""Field that points to some data inside another field of the current record.
Example::
_columns = {
'foo_id': fields.many2one('my.foo', 'Foo'),
'bar': fields.related('foo_id', 'frol', type='char', string='Frol of Foo'),
}
"""
__slots__ = ['arg', '_relations']
def _related_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
# assume self._arg = ('foo', 'bar', 'baz')
# domain = [(name, op, val)] => search [('foo.bar.baz', op, val)]
field = '.'.join(self._arg)
return map(lambda x: (field, x[1], x[2]), domain)
def _related_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
if isinstance(ids, (int, long)):
ids = [ids]
for instance in obj.browse(cr, uid, ids, context=context):
# traverse all fields except the last one
for field in self.arg[:-1]:
instance = instance[field][:1]
if instance:
# write on the last field of the target record
instance.write({self.arg[-1]: values})
def _related_read(self, obj, cr, uid, ids, field_name, args, context=None):
res = {}
for record in obj.browse(cr, SUPERUSER_ID, ids, context=context):
value = record
# traverse all fields except the last one
for field in self.arg[:-1]:
value = value[field][:1]
# read the last field on the target record
res[record.id] = value[self.arg[-1]]
if self._type == 'many2one':
# res[id] is a recordset; convert it to (id, name) or False.
# Perform name_get as root, as seeing the name of a related object depends on
# access right of source document, not target, so user may not have access.
value_ids = list(set(value.id for value in res.itervalues() if value))
value_name = dict(obj.pool[self._obj].name_get(cr, SUPERUSER_ID, value_ids, context=context))
res = dict((id, bool(value) and (value.id, value_name[value.id])) for id, value in res.iteritems())
elif self._type in ('one2many', 'many2many'):
# res[id] is a recordset; convert it to a list of ids
res = dict((id, value.ids) for id, value in res.iteritems())
return res
def __init__(self, *arg, **args):
self.arg = arg
self._relations = []
super(related, self).__init__(self._related_read, arg, self._related_write, fnct_inv_arg=arg, fnct_search=self._related_search, **args)
if self.store is True:
# TODO: improve here to change self.store = {...} according to related objects
pass
class sparse(function):
__slots__ = ['serialization_field']
def convert_value(self, obj, cr, uid, record, value, read_value, context=None):
raise TypeError("Dead method")
def _sparse_write(self,obj,cr, uid, ids, field_name, value, args, context=None):
raise TypeError("Dead method")
def _sparse_read(self, obj, cr, uid, ids, field_names, args, context=None):
raise TypeError("Dead method")
def __init__(self, serialization_field, **kwargs):
self.serialization_field = serialization_field
super(sparse, self).__init__(self._sparse_read, fnct_inv=self._sparse_write, multi='__sparse_multi', **kwargs)
# ---------------------------------------------------------
# Dummy fields
# ---------------------------------------------------------
class dummy(function):
__slots__ = ['arg', '_relations']
def _dummy_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
return []
def _dummy_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
return False
def _dummy_read(self, obj, cr, uid, ids, field_name, args, context=None):
return {}
def __init__(self, *arg, **args):
self.arg = arg
self._relations = []
super(dummy, self).__init__(self._dummy_read, arg, self._dummy_write, fnct_inv_arg=arg, fnct_search=self._dummy_search, **args)
# ---------------------------------------------------------
# Serialized fields
# ---------------------------------------------------------
@@ -1679,33 +1184,3 @@ class serialized(_column):
def __init__(self, *args, **kwargs):
kwargs['_prefetch'] = kwargs.get('_prefetch', False)
super(serialized, self).__init__(*args, **kwargs)
# TODO: review completly this class for speed improvement
class property(function):
__slots__ = []
def to_field_args(self):
args = super(property, self).to_field_args()
args['company_dependent'] = True
return args
def _property_search(self, tobj, cr, uid, obj, name, domain, context=None):
raise TypeError("Dead method")
def _property_write(self, obj, cr, uid, id, prop_name, value, obj_dest, context=None):
raise TypeError("Dead method")
def _property_read(self, obj, cr, uid, ids, prop_name, obj_dest, context=None):
raise TypeError("Dead method")
def __init__(self, **args):
if 'view_load' in args:
_logger.warning("view_load attribute is deprecated on ir.fields. Args: %r", args)
args = dict(args)
args['obj'] = args.pop('relation', '') or args.get('obj', '')
super(property, self).__init__(
fnct=self._property_read,
fnct_inv=self._property_write,
fnct_search=self._property_search,
**args
)
+2 -2
View File
@@ -19,7 +19,7 @@ from . import common
from . import preprocess
from .interface import report_rml
from openerp.exceptions import AccessError
from openerp.osv.fields import float as float_field, function as function_field, datetime as datetime_field
from openerp.osv.fields import float as float_field, datetime as datetime_field
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
from openerp.tools.safe_eval import safe_eval
from openerp.tools.translate import _
@@ -177,7 +177,7 @@ class rml_parse(object):
else:
d = res_digits(self.cr)[1]
elif (hasattr(obj, '_field') and\
isinstance(obj._field, (float_field, function_field)) and\
isinstance(obj._field, float_field) and\
obj._field.digits):
d = obj._field.digits[1]
if not d and d is not 0: