From 3706e76e9969ea9ac87fb2da938015cf2507a41a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Mon, 23 Feb 2009 11:27:36 +0100 Subject: [PATCH] Add _history_table on ModelSQL to historize change on records Use _datetime in context to read record value at specific datetime Add datetime_field on Many2One to use a specific _datetime when reading the related record --- CHANGELOG | 3 + trytond/backend/postgresql/table.py | 13 +- trytond/backend/table.py | 3 +- trytond/model/browse.py | 17 +- trytond/model/fields/many2one.py | 3 +- trytond/model/fields/one2many.py | 2 + trytond/model/fields/reference.py | 4 +- trytond/model/modelsql.py | 289 ++++++++++++++++++++-------- trytond/model/modelstorage.py | 2 +- 9 files changed, 242 insertions(+), 94 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 10ba211997..0ac92e8416 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +* Add datetime_field on Many2One to use a specific _datetime when reading the related record +* Use _datetime in context to read record value at specific datetime +* Add _history_table on ModelSQL to historize change on records * Allow to use related fields (many2one and reference) in read * Use rec_name function field instead of name_get and name_search * Use a new Pool for objects diff --git a/trytond/backend/postgresql/table.py b/trytond/backend/postgresql/table.py index 3dedf0a82f..c3c4b021ce 100644 --- a/trytond/backend/postgresql/table.py +++ b/trytond/backend/postgresql/table.py @@ -8,7 +8,8 @@ import logging class TableHandler(TableHandlerInterface): - def __init__(self, cursor, table_name, object_name=None, module_name=None): + def __init__(self, cursor, table_name, object_name=None, module_name=None, + history=False): super(TableHandler, self).__init__(cursor, table_name, object_name=object_name, module_name=module_name) self._columns = {} @@ -19,9 +20,13 @@ class TableHandler(TableHandlerInterface): # Create new table if necessary if not self.table_exist(self.cursor, self.table_name): - self.cursor.execute('CREATE TABLE "%s" ' \ - "(id SERIAL NOT NULL, " \ - "PRIMARY KEY(id))"% self.table_name) + if not history: + self.cursor.execute('CREATE TABLE "%s" ' \ + "(id SERIAL NOT NULL, " \ + "PRIMARY KEY(id))"% self.table_name) + else: + self.cursor.execute('CREATE TABLE "%s" ' \ + '(id INTEGER)' % self.table_name) self._update_definitions() @staticmethod diff --git a/trytond/backend/table.py b/trytond/backend/table.py index 381b262ddd..e596448fb1 100644 --- a/trytond/backend/table.py +++ b/trytond/backend/table.py @@ -7,7 +7,8 @@ class TableHandlerInterface(object): Define generic interface to handle database table ''' - def __init__(self, cursor, table_name, object_name=None, module_name=None): + def __init__(self, cursor, table_name, object_name=None, module_name=None, + history=False): super(TableHandlerInterface, self).__init__() self.cursor = cursor self.table_name = table_name diff --git a/trytond/model/browse.py b/trytond/model/browse.py index 484371eff7..60484e05f4 100644 --- a/trytond/model/browse.py +++ b/trytond/model/browse.py @@ -103,6 +103,17 @@ class BrowseRecord(object): # otherwise we fetch only that field else: ffields = [(name, col)] + + # add datetime_field + for i, j in ffields: + if j._type == 'many2one' and hasattr(j, 'datetime_field') \ + and j.datetime_field: + if j.datetime_field in self._model._columns: + col = self._model._columns[j.datetime_field] + else: + col = self._model._inherit_fields[j.datetime_field][2] + ffields.append((j.datetime_field, col)) + ids = [x for x in self._data.keys() \ if not self._data[x].has_key(name)] # read the data @@ -121,9 +132,13 @@ class BrowseRecord(object): if not data[i]: data[i] = BrowseRecordNull() else: + ctx = self._context + if hasattr(j, 'datetime_field') and j.datetime_field: + ctx = self._context.copy() + ctx['_datetime'] = data[j.datetime_field] data[i] = BrowseRecord(self._cursor, self._user, data[i], model, self._cache, - context=self._context) + context=ctx) elif j._type in ('one2many', 'many2many') and len(data[i]): data[i] = BrowseRecordList([BrowseRecord(self._cursor, self._user, diff --git a/trytond/model/fields/many2one.py b/trytond/model/fields/many2one.py index 334d880c9c..210e3b9056 100644 --- a/trytond/model/fields/many2one.py +++ b/trytond/model/fields/many2one.py @@ -8,9 +8,10 @@ class Many2One(Field): _type = 'many2one' def __init__(self, model_name, string='', left=None, right=None, - ondelete='SET NULL', **args): + ondelete='SET NULL', datetime_field=None, **args): super(Many2One, self).__init__(string=string, **args) self.model_name = model_name self.left = left self.right = right self.ondelete = ondelete + self.datetime_field = datetime_field diff --git a/trytond/model/fields/one2many.py b/trytond/model/fields/one2many.py index 5d995fe0c9..4724f01407 100644 --- a/trytond/model/fields/one2many.py +++ b/trytond/model/fields/one2many.py @@ -26,6 +26,8 @@ class One2Many(Field): for i in model.pool.get(self.model_name).read(cursor, user, ids2, [self.field], context=context): res[i[self.field]].append(i['id']) + for val in res.values(): + val.sort(lambda x, y: cmp(ids2.index(x), ids2.index(y))) return res def set(self, cursor, user, record_id, model, name, values, context=None): diff --git a/trytond/model/fields/reference.py b/trytond/model/fields/reference.py index f1726f55d8..22c5c4c1b5 100644 --- a/trytond/model/fields/reference.py +++ b/trytond/model/fields/reference.py @@ -28,9 +28,9 @@ class Reference(Field): ref_model, ref_id = res[i].split(',', 1) if not ref_model: continue - ref_obj = model.pool.get(ref_model) - if not ref_obj: + if ref_model not in model.pool.object_name_list(): continue + ref_obj = model.pool.get(ref_model) try: ref_id = eval(ref_id) except: diff --git a/trytond/model/modelsql.py b/trytond/model/modelsql.py index 276db93325..4dc5fd61b6 100644 --- a/trytond/model/modelsql.py +++ b/trytond/model/modelsql.py @@ -17,6 +17,7 @@ class ModelSQL(ModelStorage): _sequence = None _sql = '' _auto = True #XXX to remove when module will use the new models + _history_table = None def __init__(self): super(ModelSQL, self).__init__() @@ -38,6 +39,9 @@ class ModelSQL(ModelStorage): # create/update table in the database table = TableHandler(cursor, self._table, self._name, module_name) + if self._history_table: + history_table = TableHandler(cursor, self._history_table, None, + module_name, history=True) datetime_field = FIELDS['datetime'] integer_field = FIELDS['integer'] logs = ( @@ -55,6 +59,24 @@ class ModelSQL(ModelStorage): for log in logs: table.add_raw_column(log[0], log[1], (log[2], log[3]), default_fun=log[4], migrate=False) + if self._history_table: + history_logs = ( + ('create_date', datetime_field.sql_type(None), + datetime_field.symbol_c, datetime_field.symbol_f), + ('write_date', datetime_field.sql_type(None), + datetime_field.symbol_c, datetime_field.symbol_f), + ('create_uid', (integer_field.sql_type(None)[0], + 'INTEGER REFERENCES res_user ON DELETE SET NULL',), + integer_field.symbol_c, integer_field.symbol_f), + ('write_uid', (integer_field.sql_type(None)[0], + 'INTEGER REFERENCES res_user ON DELETE SET NULL'), + integer_field.symbol_c, integer_field.symbol_f), + ) + for log in history_logs: + history_table.add_raw_column(log[0], log[1], (log[2], log[3]), + migrate=False) + history_table.index_action('id', action='add') + for field_name, field in self._columns.iteritems(): default_fun = None if field_name in ( @@ -88,6 +110,11 @@ class ModelSQL(ModelStorage): (FIELDS[field._type].symbol_c, FIELDS[field._type].symbol_f), default_fun, hasattr(field, 'size') and field.size or None) + if self._history_table: + history_table.add_raw_column(field_name, + FIELDS[field._type].sql_type(field), + (FIELDS[field._type].symbol_c, + FIELDS[field._type].symbol_f)) if isinstance(field, (fields.Integer, fields.Float)): table.db_default(field_name, 0) @@ -110,6 +137,7 @@ class ModelSQL(ModelStorage): field_name, action=required and 'add' or 'remove') elif isinstance(field, fields.Many2Many): + #TODO handle history if field.model_name in ('res.user', 'res.group'): ref = field.model_name.replace('.','_') else: @@ -130,6 +158,20 @@ class ModelSQL(ModelStorage): for ident, constraint, msg in self._sql_constraints: table.add_constraint(ident, constraint) + if self._history_table: + cursor.execute('SELECT id FROM "' + self._table + '"') + if cursor.rowcount: + cursor.execute('SELECT id FROM "' + self._history_table + '"') + if not cursor.rowcount: + columns = ['"' + str(x) + '"' for x in self._columns + if not hasattr(self._columns[x], 'set')] + cursor.execute('INSERT INTO "' + self._history_table + '" '\ + '(' + ','.join(columns) + ') ' \ + 'SELECT ' + ','.join(columns) + \ + ' FROM "' + self._table + '"') + cursor.execute('UPDATE "' + self._history_table + '" ' \ + 'SET write_date = NULL') + def _get_error_messages(self): res = super(ModelSQL, self)._get_error_messages() res += self._sql_error_messages.values() @@ -253,6 +295,13 @@ class ModelSQL(ModelStorage): self._columns[field].set(cursor, user, id_new, self, field, values[field], context=context) + if self._history_table: + cursor.execute('INSERT INTO "' + self._history_table + '" ' \ + '(id' + upd0 + ') ' \ + 'SELECT id' + upd0 + ' ' \ + 'FROM "' + self._table + '" ' \ + 'WHERE id = %s',(id_new,)) + self._validate(cursor, user, [id_new], context=context) # Check for Modified Preorder Tree Traversal @@ -290,14 +339,24 @@ class ModelSQL(ModelStorage): context=context) fields_related = {} + datetime_fields = [] for field_name in fields_names: + if field_name == '_timestamp': + continue if '.' in field_name: field, field_related = field_name.split('.', 1) fields_related.setdefault(field, []) fields_related[field].append(field_related) + elif field_name in self._columns: + field = self._columns[field_name] + else: + field = self._inherit_fields[field_name][2] + if hasattr(field, 'datetime_field') and field.datetime_field: + datetime_fields.append(field.datetime_field) # all inherited fields + all non inherited fields - fields_pre = [x for x in fields_names + fields_related.keys() \ + fields_pre = [x for x in \ + fields_names + fields_related.keys() + datetime_fields \ if (x in self._columns and \ not hasattr(self._columns[x], 'set')) or \ (x == '_timestamp')] + \ @@ -306,9 +365,23 @@ class ModelSQL(ModelStorage): res = [] table_query = '' table_args = [] + if self.table_query(context): table_query, table_args = self.table_query(context) table_query = '(' + table_query + ') AS ' + + in_max = cursor.IN_MAX + history_order = '' + history_clause = '' + history_limit = '' + history_args = [] + if self._history_table and context.get('_datetime') and not table_query: + in_max = 1 + table_query = self._history_table + ' AS ' + history_clause = ' AND (COALESCE(write_date, create_date) <= %s)' + history_order = ' ORDER BY COALESCE(write_date, create_date) DESC' + history_limit = ' LIMIT 1' + history_args = [context['_datetime']] if len(fields_pre) : fields_pre2 = [(x in ('create_date', 'write_date')) \ and ('date_trunc(\'second\', ' + x + ') as ' + x) \ @@ -316,38 +389,23 @@ class ModelSQL(ModelStorage): if x != '_timestamp'] if '_timestamp' in fields_pre: if not self.table_query(context): - fields_pre2 += ['(CASE WHEN write_date IS NOT NULL ' \ - 'THEN write_date ELSE create_date END) ' \ + fields_pre2 += ['(COALESCE(write_date, create_date)) ' \ 'AS _timestamp'] else: fields_pre2 += ['now()::timestamp AS _timestamp'] - if len(ids) > cursor.IN_MAX: - cursor.execute('SELECT id ' \ - 'FROM ' + table_query + '"' + self._table + '" ' \ - 'ORDER BY ' + \ - ','.join([self._table + '.' + x[0] + ' ' + x[1] - for x in self._order]), table_args) - i = 0 - ids_sorted = {} - for row in cursor.fetchall(): - ids_sorted[row[0]] = i - i += 1 - ids = ids[:] - ids.sort(lambda x, y: ids_sorted[x] - ids_sorted[y]) - - for i in range(0, len(ids), cursor.IN_MAX): - sub_ids = ids[i:i + cursor.IN_MAX] + for i in range(0, len(ids), in_max): + sub_ids = ids[i:i + in_max] if domain1: - cursor.execute(('SELECT ' + \ + cursor.execute('SELECT ' + \ ','.join(fields_pre2 + ['id']) + \ ' FROM ' + table_query + '\"' + self._table +'\" ' \ 'WHERE id IN ' \ - '(' + ','.join(['%s' for x in sub_ids]) + ')'\ - ' AND (' + domain1 + ') ORDER BY ' + \ - ','.join([self._table + '.' + x[0] + ' ' + x[1] \ - for x in self._order])), - table_args + sub_ids + domain2) + '(' + ','.join(['%s' for x in sub_ids]) + ')' + \ + history_clause + \ + ' AND (' + domain1 + ') ' + history_order + \ + history_limit, + table_args + sub_ids + history_args + domain2) if not cursor.rowcount == len({}.fromkeys(sub_ids)): raise Exception('AccessError', 'You try to bypass an access rule ' \ @@ -358,10 +416,9 @@ class ModelSQL(ModelStorage): ','.join(fields_pre2 + ['id']) + \ ' FROM ' + table_query + '\"' + self._table + '\" ' \ 'WHERE id IN ' \ - '(' + ','.join(['%s' for x in sub_ids]) + ')'\ - ' ORDER BY ' + \ - ','.join([self._table + '.' + x[0] + ' ' + x[1] \ - for x in self._order]), table_args + sub_ids) + '(' + ','.join(['%s' for x in sub_ids]) + ')' + \ + history_clause + history_order + history_limit, + table_args + sub_ids + history_args) res.extend(cursor.dictfetchall()) else: res = [{'id': x} for x in ids] @@ -381,8 +438,8 @@ class ModelSQL(ModelStorage): field = self._inherits[table] inherits_fields = list( set(self._inherit_fields.keys()).intersection( - set(fields_names + fields_related.keys())).difference( - set(self._columns.keys()))) + set(fields_names + fields_related.keys() + datetime_fields) + ).difference(set(self._columns.keys()))) if not inherits_fields: continue inherit_related_fields = [] @@ -403,13 +460,15 @@ class ModelSQL(ModelStorage): for record in res: record.update(res3[record[field]]) - if field not in fields_names + fields_related.keys(): + if field not in \ + fields_names + fields_related.keys() + datetime_fields: del record[field] ids = [x['id'] for x in res] # all non inherited fields for which there is a get attribute - fields_post = [x for x in fields_names + fields_related.keys() \ + fields_post = [x for x in \ + fields_names + fields_related.keys() + datetime_fields \ if x in self._columns \ and hasattr(self._columns[x], 'get')] func_fields = {} @@ -436,19 +495,33 @@ class ModelSQL(ModelStorage): to_del = [] fields_related2values = {} - for field in fields_related.keys(): + for field in fields_related.keys() + datetime_fields: if field not in fields_names: to_del.append(field) if field not in self._columns: continue + if field not in fields_related.keys(): + continue fields_related2values.setdefault(field, {}) if self._columns[field]._type == 'many2one': obj = self.pool.get(self._columns[field].model_name) - for record in obj.read(cursor, user, [x[field] for x in res - if x[field]], fields_related[field], context=context): - record_id = record['id'] - del record['id'] - fields_related2values[field][record_id] = record + if hasattr(self._columns[field], 'datetime_field') \ + and self._columns[field].datetime_field: + ctx = context.copy() + for record in res: + ctx['_datetime'] = \ + record[self._columns[field].datetime_field] + record2 = obj.read(cursor, user, record[field], + fields_related[field], context=ctx) + record_id = record2['id'] + del record2['id'] + fields_related2values[field][record_id] = record2 + else: + for record in obj.read(cursor, user, [x[field] for x in res + if x[field]], fields_related[field], context=context): + record_id = record['id'] + del record['id'] + fields_related2values[field][record_id] = record elif self._columns[field]._type == 'reference': for record in res: if not record[field]: @@ -465,7 +538,7 @@ class ModelSQL(ModelStorage): del record2['id'] fields_related2values[field][record_id] = record2 - if to_del or fields_related.keys(): + if to_del or fields_related.keys() or datetime_fields: for record in res: for field in fields_related.keys(): if field not in self._columns: @@ -570,8 +643,8 @@ class ModelSQL(ModelStorage): if not hasattr(self._columns[field], 'set'): if (not self._columns[field].translate) \ or (context.get('language') or 'en_US') == 'en_US': - upd0.append('"' + field + '"=' + \ - FIELDS[self._columns[field]._type].symbol_c) + upd0.append(('"' + field + '"', + FIELDS[self._columns[field]._type].symbol_c)) upd1.append(FIELDS[self._columns[field]._type].symbol_f( values[field])) direct.append(field) @@ -602,51 +675,52 @@ class ModelSQL(ModelStorage): 'is not in the selection' % \ (val, field)) - upd0.append('write_uid = %s') - upd0.append('write_date = now()') + upd0.append(('write_uid', '%s')) + upd0.append(('write_date', 'now()')) upd1.append(user) - if len(upd0): - domain1, domain2 = self.pool.get('ir.rule').domain_get(cursor, - user, self._name, context=context) + domain1, domain2 = self.pool.get('ir.rule').domain_get(cursor, + user, self._name, context=context) + if domain1: + domain1 = ' AND (' + domain1 + ') ' + for i in range(0, len(ids), cursor.IN_MAX): + sub_ids = ids[i:i + cursor.IN_MAX] + ids_str = ','.join(['%s' for x in sub_ids]) if domain1: - domain1 = ' AND (' + domain1 + ') ' - for i in range(0, len(ids), cursor.IN_MAX): - sub_ids = ids[i:i + cursor.IN_MAX] - ids_str = ','.join(['%s' for x in sub_ids]) - if domain1: - cursor.execute('SELECT id FROM "' + self._table + '" ' \ - 'WHERE id IN (' + ids_str + ') ' + domain1, - sub_ids + domain2) - if not cursor.rowcount == len({}.fromkeys(sub_ids)): - raise Exception('AccessError', - 'You try to bypass an access rule ' \ - '(Document type: %s).' % \ - self._description) - else: - cursor.execute('SELECT id FROM "' + self._table + '" ' \ - 'WHERE id IN (' + ids_str + ')', sub_ids) - if not cursor.rowcount == len({}.fromkeys(sub_ids)): - raise Exception('AccessError', - 'You try to bypass an access rule ' \ - '(Document type: %s).' % \ - self._description) - if domain1: - cursor.execute('UPDATE "' + self._table + '" ' \ - 'SET ' + ','.join(upd0) + ' ' \ - 'WHERE id IN (' + ids_str + ') ' + domain1, - upd1 + sub_ids + domain2) - else: - cursor.execute('UPDATE "' + self._table + '" ' \ - 'SET ' + ','.join(upd0) + ' ' \ - 'WHERE id IN (' + ids_str + ') ', upd1 + sub_ids) + cursor.execute('SELECT id FROM "' + self._table + '" ' \ + 'WHERE id IN (' + ids_str + ') ' + domain1, + sub_ids + domain2) + if not cursor.rowcount == len({}.fromkeys(sub_ids)): + raise Exception('AccessError', + 'You try to bypass an access rule ' \ + '(Document type: %s).' % \ + self._description) + else: + cursor.execute('SELECT id FROM "' + self._table + '" ' \ + 'WHERE id IN (' + ids_str + ')', sub_ids) + if not cursor.rowcount == len({}.fromkeys(sub_ids)): + raise Exception('AccessError', + 'You try to bypass an access rule ' \ + '(Document type: %s).' % \ + self._description) + if domain1: + cursor.execute('UPDATE "' + self._table + '" ' \ + 'SET ' + \ + ','.join([x[0] + ' = ' + x[1] for x in upd0]) + ' ' \ + 'WHERE id IN (' + ids_str + ') ' + domain1, + upd1 + sub_ids + domain2) + else: + cursor.execute('UPDATE "' + self._table + '" ' \ + 'SET ' + \ + ','.join([x[0] + ' = '+ x[1] for x in upd0]) + ' ' \ + 'WHERE id IN (' + ids_str + ') ', upd1 + sub_ids) - for field in direct: - if self._columns[field].translate: - self.pool.get('ir.translation')._set_ids(cursor, user, - self._name + ',' + field, 'model', - context.get('language') or 'en_US', ids, - values[field]) + for field in direct: + if self._columns[field].translate: + self.pool.get('ir.translation')._set_ids(cursor, user, + self._name + ',' + field, 'model', + context.get('language') or 'en_US', ids, + values[field]) # call the 'set' method of fields which are not classic_write upd_todo.sort(lambda x, y: self._columns[x].priority - \ @@ -656,6 +730,16 @@ class ModelSQL(ModelStorage): self._columns[field].set(cursor, user, select_id, self, field, values[field], context=context) + if self._history_table: + columns = ['"' + str(x) + '"' for x in self._columns + if not hasattr(self._columns[x], 'set')] + for obj_id in ids: + cursor.execute('INSERT INTO "' + self._history_table + '" ' \ + '(' + ','.join(columns) + ') ' \ + 'SELECT ' + ','.join(columns) + ' ' + \ + 'FROM "' + self._table + '" ' \ + 'WHERE id = %s', (obj_id,)) + for table in self._inherits: col = self._inherits[table] nids = [] @@ -779,6 +863,12 @@ class ModelSQL(ModelStorage): cursor.execute('DELETE FROM "'+self._table+'" ' \ 'WHERE id IN (' + str_d + ')', sub_ids) + if self._history_table: + for obj_id in ids: + cursor.execute('INSERT INTO "' + self._history_table + '" ' \ + '(id, write_uid, write_date) VALUES (%s, %s, now())', + (obj_id, user)) + for k in tree_ids.keys(): field = self._columns[k] if len(tree_ids[k]) == 1: @@ -793,6 +883,9 @@ class ModelSQL(ModelStorage): context=None, count=False, query_string=False): rule_obj = self.pool.get('ir.rule') + if context is None: + context = {} + # Get domain clauses qu1, qu2, tables, tables_args = self.search_domain(cursor, user, domain, context=context) @@ -838,12 +931,31 @@ class ModelSQL(ModelStorage): res = cursor.fetchall() return res[0][0] # execute the "main" query to fetch the ids we were searching for - query_str = 'SELECT %s.id FROM ' % self._table + \ + select_field = self._table + '.id' + if self._history_table and context.get('_datetime') \ + and not query_string: + select_field += ', COALESCE(' + self._table + '.write_date, ' + \ + self._table + '.create_date)' + query_str = 'SELECT ' + select_field + ' FROM ' + \ ' '.join(tables) + ' WHERE ' + (qu1 or 'True') + \ ' ORDER BY ' + order_by + limit_str + offset_str if query_string: return (query_str, tables_args + qu2) cursor.execute(query_str, tables_args + qu2) + + if self._history_table and context.get('_datetime'): + res = [] + ids_date = {} + for row in cursor.fetchall(): + if row[0] in ids_date: + if row[1] <= ids_date[row[0]]: + continue + if row[0] in res: + res.remove(row[0]) + res.append(row[0]) + ids_date[row[0]] = row[1] + return res + res = cursor.fetchall() return [x[0] for x in res] @@ -871,10 +983,19 @@ class ModelSQL(ModelStorage): table_query, tables_args = self.table_query(context) table_query = '(' + table_query + ') AS ' + if self._history_table and context.get('_datetime'): + table_query = self._history_table + ' AS ' + tables = [table_query + '"' + self._table + '"'] qu1, qu2 = self.__search_domain_oper(cursor, user, domain, tables, tables_args, context=context) + if self._history_table and context.get('_datetime'): + if qu1: + qu1 += ' AND' + qu1 += ' (COALESCE(' + self._table + '.write_date, ' + \ + self._table + '.create_date) <= %s)' + qu2 += [context['_datetime']] return qu1, qu2, tables, tables_args def __search_domain_oper(self, cursor, user, domain, tables, tables_args, diff --git a/trytond/model/modelstorage.py b/trytond/model/modelstorage.py index 50f5802e71..d6bfdedddf 100644 --- a/trytond/model/modelstorage.py +++ b/trytond/model/modelstorage.py @@ -103,7 +103,7 @@ class ModelStorage(Model): :param context: the context :return: a list of dictionnary or a dictionnary if ids is an id the dictionnaries will have fields names as key - and fields value as value + and fields value as value. The list will not be in the same order. ''' model_access_obj = self.pool.get('ir.model.access')