From 2b587d34ff30147c03baf16e998effe46ce5d9cf Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Mon, 22 Aug 2016 11:33:46 +0200 Subject: [PATCH] [REM] openerp.osv.fields: remove old-style function fields --- addons/membership/models/partner.py | 5 +- openerp/api.py | 8 - openerp/fields.py | 4 - openerp/models.py | 353 +------------------ openerp/modules/registry.py | 12 - openerp/osv/expression.py | 32 -- openerp/osv/fields.py | 525 ---------------------------- openerp/report/report_sxw.py | 4 +- 8 files changed, 16 insertions(+), 927 deletions(-) diff --git a/addons/membership/models/partner.py b/addons/membership/models/partner.py index 1fcdcdfaeb8..9b863bf4cce 100644 --- a/addons/membership/models/partner.py +++ b/addons/membership/models/partner.py @@ -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): diff --git a/openerp/api.py b/openerp/api.py index 67678e3ffb1..b8b4f1ed2d9 100644 --- a/openerp/api.py +++ b/openerp/api.py @@ -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``. """ diff --git a/openerp/fields.py b/openerp/fields.py index c8a3dd8d010..7c3a8644cdd 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -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: diff --git a/openerp/models.py b/openerp/models.py index 68449f0d4df..a2c7fe67149 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -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): diff --git a/openerp/modules/registry.py b/openerp/modules/registry.py index 97cb20866c6..e440f17ef84 100644 --- a/openerp/modules/registry.py +++ b/openerp/modules/registry.py @@ -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 diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 3b2e3970cc9..837750e5286 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -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 # ------------------------------------------------- diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index da55a4bada3..f65ae38ce4d 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -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 `). - :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 - ) diff --git a/openerp/report/report_sxw.py b/openerp/report/report_sxw.py index 8054414bf1e..6d2b2434d36 100644 --- a/openerp/report/report_sxw.py +++ b/openerp/report/report_sxw.py @@ -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: