From 4fc1ffe10a6e0b5e1fb0da2f0c2fdc882477968f Mon Sep 17 00:00:00 2001 From: sebastien beau Date: Mon, 5 Sep 2011 01:43:04 +0200 Subject: [PATCH 01/75] [REF] refactor serialized field based on a patch sent by xrg by email, thanks xrg ;) bzr revid: sebastien.beau@akretion.com.br-20110904234304-6uablzky9kwn5x3k --- openerp/osv/fields.py | 26 +++++++++++++++++++------- openerp/osv/orm.py | 1 + 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index fb973d2e521..497d3359e6e 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -42,6 +42,7 @@ from psycopg2 import Binary import openerp.netsvc as netsvc import openerp.tools as tools from openerp.tools.translate import _ +import json def _symbol_set(symb): if symb == None or symb == False: @@ -1218,13 +1219,24 @@ class dummy(function): # Serialized fields # --------------------------------------------------------- class serialized(_column): - def __init__(self, string='unknown', serialize_func=repr, deserialize_func=eval, type='text', **args): - self._serialize_func = serialize_func - self._deserialize_func = deserialize_func - self._type = type - self._symbol_set = (self._symbol_c, self._serialize_func) - self._symbol_get = self._deserialize_func - super(serialized, self).__init__(string=string, **args) + """ A field able to store an arbitrary python data structure. + + Note: only plain components allowed. + """ + + def _symbol_set_struct(val): + return json.dumps(val) + + def _symbol_get_struct(self, val): + return json.loads(val or '{}') + + _prefetch = False + _type = 'serialized' + + _symbol_c = '%s' + _symbol_f = _symbol_set_struct + _symbol_set = (_symbol_c, _symbol_f) + _symbol_get = _symbol_get_struct # TODO: review completly this class for speed improvement class property(function): diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index b87672c012d..1ba5abbf85b 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -492,6 +492,7 @@ def get_pg_type(f): fields.datetime: 'timestamp', fields.binary: 'bytea', fields.many2one: 'int4', + fields.serialized: 'text', } if type(f) in type_dict: f_type = (type_dict[type(f)], type_dict[type(f)]) From 926980b325f4880248780e9fe86451f922109b8e Mon Sep 17 00:00:00 2001 From: sebastien beau Date: Sat, 17 Sep 2011 12:05:38 +0200 Subject: [PATCH 02/75] [IMP] add a new class sparse bzr revid: sebastien.beau@akretion.com.br-20110917100538-63iyfpcuyhff4z5d --- openerp/osv/fields.py | 86 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 497d3359e6e..e71249e8ff4 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -1196,6 +1196,91 @@ class related(function): obj_name = f['relation'] self._relations[-1]['relation'] = f['relation'] + +class sparse(function): + + def convert_value(self, obj, cr, uid, record, value, read_value, context=None): + """ + + For a many2many field, a list of tuples is expected. + Here is the list of tuple that are accepted, with the corresponding semantics :: + + (0, 0, { values }) link to a new record that needs to be created with the given values dictionary + (1, ID, { values }) update the linked record with id = ID (write *values* on it) + (2, ID) remove and delete the linked record with id = ID (calls unlink on ID, that will delete the object completely, and the link to it as well) + (3, ID) cut the link to the linked record with id = ID (delete the relationship between the two objects but does not delete the target object itself) + (4, ID) link to existing record with id = ID (adds a relationship) + (5) unlink all (like using (3,ID) for all linked records) + (6, 0, [IDs]) replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs) + + Example: + [(6, 0, [8, 5, 6, 4])] sets the many2many to ids [8, 5, 6, 4] + + + For a one2many field, a lits of tuples is expected. + Here is the list of tuple that are accepted, with the corresponding semantics :: + + (0, 0, { values }) link to a new record that needs to be created with the given values dictionary + (1, ID, { values }) update the linked record with id = ID (write *values* on it) + (2, ID) remove and delete the linked record with id = ID (calls unlink on ID, that will delete the object completely, and the link to it as well) + + Example: + [(0, 0, {'field_name':field_value_record1, ...}), (0, 0, {'field_name':field_value_record2, ...})] + """ + + if self._type == 'many2many': + #NOTE only the option (0, 0, { values }) is supported for many2many + if value[0][0] == 6: + return value[0][2] + + elif self._type == 'one2many': + if not read_value: + read_value=[] + relation_obj = obj.pool.get(self.relation) + for vals in value: + if vals[0] == 0: + read_value.append(relation_obj.create(cr, uid, vals[2], context=context)) + elif vals[0] == 1: + relation_obj.write(cr, uid, vals[1], vals[2], context=context) + elif vals[0] == 2: + relation_obj.unlink(cr, uid, vals[1]) + read_value.remove(vals[1]) + return read_value + return value + + + def _fnct_write(self,obj,cr, uid, ids, field_name, value, args, context=None): + if not type(ids) == list: + ids = [ids] + records = obj.browse(cr, uid, ids, context=context) + for record in records: + # grab serialized value as object - already deserialized + serialized = record.__getattr__(self.serialization_field) + serialized[field_name] = self.convert_value(obj, cr, uid, record, value, serialized.get(field_name), context=context) + obj.write(cr, uid, ids, {self.serialization_field: serialized}, context=context) + return True + + def _fnct_read(self, obj, cr, uid, ids, field_names, args, context=None): + results={} + records = obj.browse(cr, uid, ids, context=context) + for record in records: + # grab serialized value as object - already deserialized + serialized = record.__getattr__(self.serialization_field) + results[record.id] ={} + for field_name in field_names: + if obj._columns[field_name]._type in ['one2many']: + results[record.id].update({field_name : serialized.get(field_name, [])}) + else: + results[record.id].update({field_name : serialized.get(field_name)}) + return results + + def __init__(self, serialization_field, **kwargs): + self.serialization_field = serialization_field + #assert serialization_field._type == 'serialized' + return super(sparse, self).__init__(self._fnct_read, fnct_inv=self._fnct_write, multi='_json_multi', method=True, **kwargs) + + + + + # --------------------------------------------------------- # Dummy fields # --------------------------------------------------------- @@ -1218,6 +1303,7 @@ class dummy(function): # --------------------------------------------------------- # Serialized fields # --------------------------------------------------------- + class serialized(_column): """ A field able to store an arbitrary python data structure. From 29297ed841bcc01df77d80e1bb2cd9e2432a0163 Mon Sep 17 00:00:00 2001 From: sebastien beau Date: Sat, 17 Sep 2011 20:35:35 +0200 Subject: [PATCH 03/75] [IMP] add the posibility to create sparse field dynamically bzr revid: sebastien.beau@akretion.com.br-20110917183535-idzw9wg340gywduy --- openerp/addons/base/ir/ir.xml | 2 ++ openerp/addons/base/ir/ir_model.py | 13 ++++++++++++- openerp/osv/orm.py | 18 ++++++++++++------ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/openerp/addons/base/ir/ir.xml b/openerp/addons/base/ir/ir.xml index 011ad437cca..c54b4e3657d 100644 --- a/openerp/addons/base/ir/ir.xml +++ b/openerp/addons/base/ir/ir.xml @@ -1037,6 +1037,7 @@ + @@ -1149,6 +1150,7 @@ + diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index a9d2a21b7a5..da9c5f9a7d4 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -202,6 +202,7 @@ class ir_model_fields(osv.osv): 'view_load': fields.boolean('View Auto-Load'), 'selectable': fields.boolean('Selectable'), 'modules': fields.function(_in_modules, method=True, type='char', size=128, string='In modules', help='List of modules in which the field is defined'), + 'serialization_field': fields.char('Serialization Field', size=64), } _rec_name='field_description' _defaults = { @@ -279,11 +280,14 @@ class ir_model_fields(osv.osv): if vals.get('relation',False) and not self.pool.get('ir.model').search(cr, user, [('model','=',vals['relation'])]): raise except_orm(_('Error'), _("Model %s does not exist!") % vals['relation']) + if vals.get('serialization_field',False) and not self.search(cr, user, [('model','=',vals['model']), ('name', '=', vals['serialization_field'])]): + raise except_orm(_('Error'), _("The field %s does not exist!") % vals['serialization_field']) + if self.pool.get(vals['model']): self.pool.get(vals['model']).__init__(self.pool, cr) #Added context to _auto_init for special treatment to custom field for select_level ctx = context.copy() - ctx.update({'field_name':vals['name'],'field_state':'manual','select':vals.get('select_level','0'),'update_custom_fields':True}) + ctx.update({'field_name':vals['name'],'field_state':'manual','select':vals.get('select_level','0'),'update_custom_fields':True, 'serialization_field': vals.get('serialization_field',False)}) self.pool.get(vals['model'])._auto_init(cr, ctx) return res @@ -294,6 +298,13 @@ class ir_model_fields(osv.osv): if context and context.get('manual',False): vals['state'] = 'manual' + if vals['serialization_field'] or vals['name']: + for field in self.browse(cr, user, ids, context=context): + if field.serialization_field and field.serialization_field != vals['serialization_field'] or (not field.serialization_field and vals['serialization_field']): + raise except_orm(_('Error!'), _('Changing the storing system for the field "%s" is not allowed.'%field.name)) + elif field.serialization_field and (field.name != vals['name']): + raise except_orm(_('Error!'), _('Renaming the sparse field "%s" is not allowed'%field.name)) + column_rename = None # if set, *one* column can be renamed here obj = None models_patch = {} # structs of (obj, [(field, prop, change_to),..]) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 1ba5abbf85b..b3a71ee79c5 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -678,6 +678,7 @@ class orm_template(object): 'selectable': (f.selectable and 1) or 0, 'translate': (f.translate and 1) or 0, 'relation_field': (f._type=='one2many' and isinstance(f, fields.one2many)) and f._fields_id or '', + 'serialization_field': 'serialization_field' in dir(f) and f.serialization_field or "", } # When its a custom field,it does not contain f.select if context.get('field_state', 'base') == 'manual': @@ -693,13 +694,13 @@ class orm_template(object): vals['id'] = id cr.execute("""INSERT INTO ir_model_fields ( id, model_id, model, name, field_description, ttype, - relation,view_load,state,select_level,relation_field, translate + relation,view_load,state,select_level,relation_field, translate, serialization_field ) VALUES ( - %s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s + %s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s )""", ( id, vals['model_id'], vals['model'], vals['name'], vals['field_description'], vals['ttype'], vals['relation'], bool(vals['view_load']), 'base', - vals['select_level'], vals['relation_field'], bool(vals['translate']) + vals['select_level'], vals['relation_field'], bool(vals['translate']), vals['serialization_field'] )) if 'module' in context: name1 = 'field_' + self._table + '_' + k @@ -716,12 +717,12 @@ class orm_template(object): cr.commit() cr.execute("""UPDATE ir_model_fields SET model_id=%s, field_description=%s, ttype=%s, relation=%s, - view_load=%s, select_level=%s, readonly=%s ,required=%s, selectable=%s, relation_field=%s, translate=%s + view_load=%s, select_level=%s, readonly=%s ,required=%s, selectable=%s, relation_field=%s, translate=%s, serialization_field=%s WHERE model=%s AND name=%s""", ( vals['model_id'], vals['field_description'], vals['ttype'], vals['relation'], bool(vals['view_load']), - vals['select_level'], bool(vals['readonly']), bool(vals['required']), bool(vals['selectable']), vals['relation_field'], bool(vals['translate']), vals['model'], vals['name'] + vals['select_level'], bool(vals['readonly']), bool(vals['required']), bool(vals['selectable']), vals['relation_field'], bool(vals['translate']), vals['serialization_field'], vals['model'], vals['name'] )) break cr.commit() @@ -3290,7 +3291,12 @@ class orm(orm_template): #'select': int(field['select_level']) } - if field['ttype'] == 'selection': + if field['serialization_field']: + attrs.update({'serialization_field': field['serialization_field']}) + if field['ttype'] in ['many2one', 'one2many', 'many2many']: + attrs.update({'relation': field['relation']}) + self._columns[field['name']] = fields.sparse(**attrs) + elif field['ttype'] == 'selection': self._columns[field['name']] = fields.selection(eval(field['selection']), **attrs) elif field['ttype'] == 'reference': self._columns[field['name']] = fields.reference(selection=eval(field['selection']), **attrs) From b243c0567c28e22a8089d7f1243657675c6bfd31 Mon Sep 17 00:00:00 2001 From: sebastien beau Date: Tue, 20 Sep 2011 19:07:07 +0200 Subject: [PATCH 04/75] [REF] ir_model_field replace the serialisation_field by serialization_field_id change request in base.sql as a new field is created in ir_model_fields bzr revid: sebastien.beau@akretion.com.br-20110920170707-2wmfs1sg37hgvbl1 --- openerp/addons/base/base.sql | 2 ++ openerp/addons/base/ir/ir.xml | 6 ++++-- openerp/addons/base/ir/ir_model.py | 13 +++++------- openerp/osv/orm.py | 33 ++++++++++++++++++++++-------- 4 files changed, 36 insertions(+), 18 deletions(-) diff --git a/openerp/addons/base/base.sql b/openerp/addons/base/base.sql index 54dcc3a55ae..2a13348843e 100644 --- a/openerp/addons/base/base.sql +++ b/openerp/addons/base/base.sql @@ -49,6 +49,8 @@ CREATE TABLE ir_model_fields ( primary key(id) ); +ALTER TABLE ir_model_fields ADD column serialization_field_id int references ir_model_fields on delete cascade; + ------------------------------------------------------------------------- -- Actions diff --git a/openerp/addons/base/ir/ir.xml b/openerp/addons/base/ir/ir.xml index c54b4e3657d..d8d77c22a35 100644 --- a/openerp/addons/base/ir/ir.xml +++ b/openerp/addons/base/ir/ir.xml @@ -1037,7 +1037,8 @@ - + + @@ -1150,7 +1151,8 @@ - + + diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index da9c5f9a7d4..bbd021e237a 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -202,7 +202,7 @@ class ir_model_fields(osv.osv): 'view_load': fields.boolean('View Auto-Load'), 'selectable': fields.boolean('Selectable'), 'modules': fields.function(_in_modules, method=True, type='char', size=128, string='In modules', help='List of modules in which the field is defined'), - 'serialization_field': fields.char('Serialization Field', size=64), + 'serialization_field_id': fields.many2one('ir.model.fields', 'Serialization Field', domain = "[('ttype','=','serialized')]", ondelete='cascade'), } _rec_name='field_description' _defaults = { @@ -280,14 +280,11 @@ class ir_model_fields(osv.osv): if vals.get('relation',False) and not self.pool.get('ir.model').search(cr, user, [('model','=',vals['relation'])]): raise except_orm(_('Error'), _("Model %s does not exist!") % vals['relation']) - if vals.get('serialization_field',False) and not self.search(cr, user, [('model','=',vals['model']), ('name', '=', vals['serialization_field'])]): - raise except_orm(_('Error'), _("The field %s does not exist!") % vals['serialization_field']) - if self.pool.get(vals['model']): self.pool.get(vals['model']).__init__(self.pool, cr) #Added context to _auto_init for special treatment to custom field for select_level ctx = context.copy() - ctx.update({'field_name':vals['name'],'field_state':'manual','select':vals.get('select_level','0'),'update_custom_fields':True, 'serialization_field': vals.get('serialization_field',False)}) + ctx.update({'field_name':vals['name'],'field_state':'manual','select':vals.get('select_level','0'),'update_custom_fields':True}) self.pool.get(vals['model'])._auto_init(cr, ctx) return res @@ -298,11 +295,11 @@ class ir_model_fields(osv.osv): if context and context.get('manual',False): vals['state'] = 'manual' - if vals['serialization_field'] or vals['name']: + if vals['serialization_field_id'] or vals['name']: for field in self.browse(cr, user, ids, context=context): - if field.serialization_field and field.serialization_field != vals['serialization_field'] or (not field.serialization_field and vals['serialization_field']): + if field.serialization_field_id and field.serialization_field_id.id != vals['serialization_field_id'] or (not field.serialization_field_id and vals['serialization_field_id']): raise except_orm(_('Error!'), _('Changing the storing system for the field "%s" is not allowed.'%field.name)) - elif field.serialization_field and (field.name != vals['name']): + elif field.serialization_field_id and (field.name != vals['name']): raise except_orm(_('Error!'), _('Renaming the sparse field "%s" is not allowed'%field.name)) column_rename = None # if set, *one* column can be renamed here diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index b3a71ee79c5..1ca0c192fd1 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -663,7 +663,17 @@ class orm_template(object): for rec in cr.dictfetchall(): cols[rec['name']] = rec - for (k, f) in self._columns.items(): + ir_model_fields_obj = self.pool.get('ir.model.fields') + + high_priority_items=[] + low_priority_items=[] + #sparse field should be created at the end, indeed this field depend of other field + for item in self._columns.items(): + if item[1].__class__ == fields.sparse: + low_priority_items.append(item) + else: + high_priority_items.append(item) + for (k, f) in high_priority_items + low_priority_items: vals = { 'model_id': model_id, 'model': self._name, @@ -678,8 +688,14 @@ class orm_template(object): 'selectable': (f.selectable and 1) or 0, 'translate': (f.translate and 1) or 0, 'relation_field': (f._type=='one2many' and isinstance(f, fields.one2many)) and f._fields_id or '', - 'serialization_field': 'serialization_field' in dir(f) and f.serialization_field or "", + 'serialization_field_id': None, } + if 'serialization_field' in dir(f): + serialization_field_id = ir_model_fields_obj.search(cr, 1, [('model','=',vals['model']), ('name', '=', f.serialization_field)]) + if not serialization_field_id: + raise except_orm(_('Error'), _("The field %s does not exist!" %f.serialization_field)) + vals['serialization_field_id'] = serialization_field_id[0] + # When its a custom field,it does not contain f.select if context.get('field_state', 'base') == 'manual': if context.get('field_name', '') == k: @@ -694,13 +710,13 @@ class orm_template(object): vals['id'] = id cr.execute("""INSERT INTO ir_model_fields ( id, model_id, model, name, field_description, ttype, - relation,view_load,state,select_level,relation_field, translate, serialization_field + relation,view_load,state,select_level,relation_field, translate, serialization_field_id ) VALUES ( %s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s )""", ( id, vals['model_id'], vals['model'], vals['name'], vals['field_description'], vals['ttype'], vals['relation'], bool(vals['view_load']), 'base', - vals['select_level'], vals['relation_field'], bool(vals['translate']), vals['serialization_field'] + vals['select_level'], vals['relation_field'], bool(vals['translate']), vals['serialization_field_id'] )) if 'module' in context: name1 = 'field_' + self._table + '_' + k @@ -717,12 +733,12 @@ class orm_template(object): cr.commit() cr.execute("""UPDATE ir_model_fields SET model_id=%s, field_description=%s, ttype=%s, relation=%s, - view_load=%s, select_level=%s, readonly=%s ,required=%s, selectable=%s, relation_field=%s, translate=%s, serialization_field=%s + view_load=%s, select_level=%s, readonly=%s ,required=%s, selectable=%s, relation_field=%s, translate=%s, serialization_field_id=%s WHERE model=%s AND name=%s""", ( vals['model_id'], vals['field_description'], vals['ttype'], vals['relation'], bool(vals['view_load']), - vals['select_level'], bool(vals['readonly']), bool(vals['required']), bool(vals['selectable']), vals['relation_field'], bool(vals['translate']), vals['serialization_field'], vals['model'], vals['name'] + vals['select_level'], bool(vals['readonly']), bool(vals['required']), bool(vals['selectable']), vals['relation_field'], bool(vals['translate']), vals['serialization_field_id'], vals['model'], vals['name'] )) break cr.commit() @@ -3291,8 +3307,9 @@ class orm(orm_template): #'select': int(field['select_level']) } - if field['serialization_field']: - attrs.update({'serialization_field': field['serialization_field']}) + if field['serialization_field_id']: + cr.execute('SELECT name FROM ir_model_fields WHERE id=%s', (field['serialization_field_id'],)) + attrs.update({'serialization_field': cr.fetchone()[0], 'type': field['ttype']}) if field['ttype'] in ['many2one', 'one2many', 'many2many']: attrs.update({'relation': field['relation']}) self._columns[field['name']] = fields.sparse(**attrs) From 4a8c48b4b1d2a310d778af655c13265951d2037d Mon Sep 17 00:00:00 2001 From: sebastien beau Date: Fri, 23 Sep 2011 13:26:18 +0200 Subject: [PATCH 05/75] [FIX] fix bug when updating a field. The support of renaming a field or changing the storing system will be done later, I not sure that I will have the time to implement it for 6.1. At least I will propose a new merge for 6.2 bzr revid: sebastien.beau@akretion.com.br-20110923112618-vq44f15cpcvxozxi --- openerp/addons/base/ir/ir_model.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index bbd021e237a..96e7a647372 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -295,11 +295,12 @@ class ir_model_fields(osv.osv): if context and context.get('manual',False): vals['state'] = 'manual' - if vals['serialization_field_id'] or vals['name']: + #For the moment renaming a sparse field or changing the storing system is not allowed. This will be done later + if 'serialization_field_id' in vals or 'name' in vals: for field in self.browse(cr, user, ids, context=context): - if field.serialization_field_id and field.serialization_field_id.id != vals['serialization_field_id'] or (not field.serialization_field_id and vals['serialization_field_id']): + if 'serialization_field_id' in vals and field.serialization_field_id.id != vals['serialization_field_id']: raise except_orm(_('Error!'), _('Changing the storing system for the field "%s" is not allowed.'%field.name)) - elif field.serialization_field_id and (field.name != vals['name']): + if field.serialization_field_id and (field.name != vals['name']): raise except_orm(_('Error!'), _('Renaming the sparse field "%s" is not allowed'%field.name)) column_rename = None # if set, *one* column can be renamed here From a59c66ed40600da2148d3e6a27a318f7c978f29c Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 1 Nov 2011 16:16:39 +0100 Subject: [PATCH 06/75] [FIX] fields.sparse: do not store the keys with null values anymore. We don't need them (and this will help to keep clean json fields) and this caused problems at the read from client because None values were sent by XMLRPC bzr revid: guewen.baconnier@camptocamp.com-20111101151639-rcos08pbnxhhxfxa --- openerp/osv/fields.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index e71249e8ff4..bd04cda3814 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -1249,12 +1249,20 @@ class sparse(function): def _fnct_write(self,obj,cr, uid, ids, field_name, value, args, context=None): if not type(ids) == list: - ids = [ids] + ids = [ids] records = obj.browse(cr, uid, ids, context=context) for record in records: # grab serialized value as object - already deserialized serialized = record.__getattr__(self.serialization_field) - serialized[field_name] = self.convert_value(obj, cr, uid, record, value, serialized.get(field_name), context=context) + # we have to delete the key in the json when the value is null + if value is None: + if field_name in serialized: + del serialized[field_name] + else: + # nothing to do, we dont wan't to store the key with a null value + continue + else: + serialized[field_name] = self.convert_value(obj, cr, uid, record, value, serialized.get(field_name), context=context) obj.write(cr, uid, ids, {self.serialization_field: serialized}, context=context) return True From dcb5ff6eacc63cebdc03f8615b18a002cb6ad23d Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 14 Nov 2011 17:36:17 +0100 Subject: [PATCH 07/75] [IMP] res.currency,base: factor out float rounding methods in tools to allow use without currency Pending: addition of tests bzr revid: odo@openerp.com-20111114163617-kw7u8rxcie7m2hj5 --- openerp/addons/base/res/res_currency.py | 41 ++++++++++--- openerp/tools/misc.py | 79 ++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 9 deletions(-) diff --git a/openerp/addons/base/res/res_currency.py b/openerp/addons/base/res/res_currency.py index 32aa387e828..c0a2bebc104 100644 --- a/openerp/addons/base/res/res_currency.py +++ b/openerp/addons/base/res/res_currency.py @@ -24,7 +24,7 @@ import netsvc from osv import fields, osv import tools -from tools.misc import currency +from tools.misc import currency, float_round, float_is_zero, float_compare from tools.translate import _ CURRENCY_DISPLAY_PATTERN = re.compile(r'(\w+)\s*(?:\((.*)\))?') @@ -127,15 +127,40 @@ class res_currency(osv.osv): return [(x['id'], tools.ustr(x['name']) + (x['symbol'] and (' (' + tools.ustr(x['symbol']) + ')') or '')) for x in reads] def round(self, cr, uid, currency, amount): - if currency.rounding == 0: - return 0.0 - else: - # /!\ First member below must be rounded to full unit! - # Do not pass a rounding digits value to round() - return round(amount / currency.rounding) * currency.rounding + """Return ``amount`` rounded according to ``currency``'s + rounding rules, also minimizing IEEE-754 floating point + representation errors. + + :param browse_record currency: currency for which we are rounding + :param float amount: the amount to round + :return: rounded float + """ + return float_round(amount, precision_rounding=currency.rounding) + + def compare_amounts(self, cr, uid, currency, amount1, amount2): + """Compare ``amount1`` and ``amount2`` according to ``currency``'s + rounding rules, and return (resp.) -1, 0 or 1, if ``amount1`` + is (resp.) lower than, equal to, or greater than ``amount2``. + + For example 1.432 and 1.431 are equal if currency is rounded to + 2 digits, so this method would return 0 + + :param browse_record currency: currency for which we are rounding + :param float amount1: first amount to compare + :param float amount2: second amount to compare + :return: (resp.) -1, 0 or 1, if ``amount1`` is (resp.) lower than, + equal to, or greater than ``amount2``, according to + ``currency``'s rounding. + """ + return float_compare(amount1, amount2, precision_rounding=currency.rounding) def is_zero(self, cr, uid, currency, amount): - return abs(self.round(cr, uid, currency, amount)) < currency.rounding + """Returns true if ``amount`` is small enough to be treated as + zero according to ``currency``'s rounding rules. + :param browse_record currency: currency for which we are rounding + :param float amount: amount to compare with currency's zero + """ + return float_is_zero(amount, precision_rounding=currency.rounding) def _get_conversion_rate(self, cr, uid, from_currency, to_currency, context=None): if context is None: diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index dbccbe5b217..ae28567e19b 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -1200,4 +1200,81 @@ class UnquoteEvalContext(defaultdict): def __missing__(self, key): return unquote(key) -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: +def _float_check_precision(precision_digits=None, precision_rounding=None): + assert (precision_digits is not None or precision_rounding is not None) and \ + not (precision_digits and precision_rounding),\ + "exactly one of precision_digits and precision_rounding must be specified" + if precision_digits is not None: + return 10 ** -precision_digits + return precision_rounding + +def float_round(amount, precision_digits=None, precision_rounding=None): + """Return ``amount`` rounded to ``precision_digits`` + decimal digits, minimizing IEEE-754 floating point representation + errors. + Precision must be given by ``precision_digits`` or ``precision_rounding``, + not both! + Example on Python 2.7.2:: + + >>> round_float(2.675) + 2.68 + >>> round(2.675,2) + 2.67 + + :param float amount: the amount to round + :param int precision_digits: number of decimal digits to round to. + :param float precision_rounding: decimal number representing the minimum + non-zero value at the desired precision (for example, 0.01 for a + 2-digit precision). + :return: rounded float + """ + rounding_factor = _float_check_precision(precision_digits=precision_digits, + precision_rounding=precision_rounding) + if rounding_factor == 0: return 0.0 + # /!\ First member below must be rounded to full unit! + # Do not pass rounding digits to round()! + return round(amount / rounding_factor) * rounding_factor + +def float_is_zero(amount, precision_digits=None, precision_rounding=None): + """Returns true if ``amount`` is small enough to be treated as + zero at the given precision. + Precision must be given by ``precision_digits`` or ``precision_rounding``, + not both! + + :param int precision_digits: number of decimal digits to round to. + :param float precision_rounding: decimal number representing the minimum + non-zero value at the desired precision (for example, 0.01 for a + 2-digit precision). + :param float amount: amount to compare with currency's zero + :return: True if ``amount`` is considered 0 + """ + rounding_factor = _float_check_precision(precision_digits=precision_digits, + precision_rounding=precision_rounding) + return abs(float_round(amount, precision_rounding=rounding_factor)) < rounding_factor + +def float_compare(amount1, amount2, precision_digits=None, precision_rounding=None): + """Compare ``amount1`` and ``amount2`` according + to the given precision. + Precision must be given by ``precision_digits`` or ``precision_rounding``, + not both! + + For example 1.432 and 1.431 are equal at 2 digits precision, + so this method would return 0 + + :param int precision_digits: number of decimal digits to round to. + :param float precision_rounding: decimal number representing the minimum + non-zero value at the desired precision (for example, 0.01 for a + 2-digit precision). + :param float amount1: first amount to compare + :param float amount2: second amount to compare + :return: (resp.) -1, 0 or 1, if ``amount1`` is (resp.) lower than, + equal to, or greater than ``amount2``, at the given precision. + """ + rounding_factor = _float_check_precision(precision_digits=precision_digits, + precision_rounding=precision_rounding) + delta = amount1 - amount2 + if float_is_zero(delta, precision_rounding=rounding_factor): return 0 + delta = float_round(delta, precision_rounding=rounding_factor) + return -1 if delta < 0 else 1 + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file From ffe640cecea84380fd11e9a05bb6bc4882fef852 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 14 Nov 2011 19:23:10 +0100 Subject: [PATCH 08/75] [IMP] tools.float_*: added tests and docstrings bzr revid: odo@openerp.com-20111114182310-kkzh78uej0uuwhaz --- openerp/addons/base/res/res_currency.py | 23 ++++++--- openerp/addons/base/test/base_test.yml | 68 +++++++++++++++++++++++++ openerp/tools/misc.py | 31 ++++++++--- 3 files changed, 107 insertions(+), 15 deletions(-) diff --git a/openerp/addons/base/res/res_currency.py b/openerp/addons/base/res/res_currency.py index c0a2bebc104..c67c7d65260 100644 --- a/openerp/addons/base/res/res_currency.py +++ b/openerp/addons/base/res/res_currency.py @@ -128,8 +128,7 @@ class res_currency(osv.osv): def round(self, cr, uid, currency, amount): """Return ``amount`` rounded according to ``currency``'s - rounding rules, also minimizing IEEE-754 floating point - representation errors. + rounding rules. :param browse_record currency: currency for which we are rounding :param float amount: the amount to round @@ -138,12 +137,16 @@ class res_currency(osv.osv): return float_round(amount, precision_rounding=currency.rounding) def compare_amounts(self, cr, uid, currency, amount1, amount2): - """Compare ``amount1`` and ``amount2`` according to ``currency``'s - rounding rules, and return (resp.) -1, 0 or 1, if ``amount1`` - is (resp.) lower than, equal to, or greater than ``amount2``. + """Compare ``amount1`` and ``amount2`` after rounding them according to the + given currency's precision.. + An amount is considered lower/greater than another amount if their rounded + value is different. This is not the same as having a non-zero difference! - For example 1.432 and 1.431 are equal if currency is rounded to - 2 digits, so this method would return 0 + For example 1.432 and 1.431 are equal at 2 digits precision, + so this method would return 0. + However 0.006 and 0.002 are considered different (returns 1) because + they respectively round to 0.01 and 0.0, even though + 0.006-0.002 = 0.004 which would be considered zero at 2 digits precision. :param browse_record currency: currency for which we are rounding :param float amount1: first amount to compare @@ -157,6 +160,12 @@ class res_currency(osv.osv): def is_zero(self, cr, uid, currency, amount): """Returns true if ``amount`` is small enough to be treated as zero according to ``currency``'s rounding rules. + + Warning: ``is_zero(amount1-amount2)`` is not always equivalent to + ``compare_amounts(amount1,amount2) == 0``, as the former will round after + computing the difference, while the latter will round before, giving + different results for e.g. 0.006 and 0.002 at 2 digits precision. + :param browse_record currency: currency for which we are rounding :param float amount: amount to compare with currency's zero """ diff --git a/openerp/addons/base/test/base_test.yml b/openerp/addons/base/test/base_test.yml index 6ea578a8659..9b686ccb0d3 100644 --- a/openerp/addons/base/test/base_test.yml +++ b/openerp/addons/base/test/base_test.yml @@ -144,3 +144,71 @@ !python {model: res.partner.category}: | self.pool._init = True +- + "Float precision tests: verify that float rounding methods are working correctly" +- + !python {model: res.currency}: | + currency = self.browse(cr, uid, ref('base.EUR')) + def try_round(self, cr, currency, amount, expected): + result = str(self.round(cr, 1, currency, amount)) + assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected) + try_round(self, cr, currency, 2.674,'2.67') + try_round(self, cr, currency, 2.675,'2.68') # in Python 2.7.2, round(2.675,2) gives 2.67 + try_round(self, cr, currency, 0.001,'0.0') + try_round(self, cr, currency, 0.0049,'0.0') # 0.0049 is closer to 0 than to 0.01, so should round down + try_round(self, cr, currency, 0.005,'0.01') # the rule is to round half up + + def try_zero(self, cr, currency, amount, expected): + assert self.is_zero(cr, 1, currency, amount) == expected, "Rounding error: %s should be zero!" % amount + try_zero(self, cr, currency, 0.01, False) + try_zero(self, cr, currency, 0.001, True) + try_zero(self, cr, currency, 0.0046, True) + try_zero(self, cr, currency, 2.68-2.675, False) # 2.68 - 2.675 = 0.005 -> rounds to 0.01 + try_zero(self, cr, currency, 2.68-2.676, True) # 2.68 - 2.675 = 0.004 -> rounds to 0.0 + + def try_compare(self, cr, currency, amount1, amount2, expected): + assert self.compare_amounts(cr, 1, currency, amount1, amount2) == expected, \ + "Rounding error, compare_amounts(%s,%s) should be %s" % (amount1, amount2, expected) + try_compare(self, cr, currency, 0.001, 0.001, 0) + try_compare(self, cr, currency, 0.001, 0.002, 0) + try_compare(self, cr, currency, 2.675, 2.68, 0) + try_compare(self, cr, currency, 2.676, 2.68, 0) + try_compare(self, cr, currency, 2.674, 2.68, -1) + try_compare(self, cr, currency, 3, 2.68, 1) + try_compare(self, cr, currency, 0.01, 0, 1) + + from tools import float_compare, float_is_zero, float_round + def try_round_digits(float_round, amount, expected): + result = str(float_round(amount, precision_digits=3)) + assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected) + try_round_digits(float_round, 2.6745, '2.675') + try_round_digits(float_round, 2.6744, '2.674') + try_round_digits(float_round, 0.0004, '0.0') + + def try_zero_digits(float_is_zero, amount, expected): + assert float_is_zero(amount, precision_digits=3) == expected, "Rounding error: %s should be zero!" % amount + try_zero_digits(float_is_zero, 0.0002, True) + try_zero_digits(float_is_zero, 0.00034, True) + try_zero_digits(float_is_zero, 0.0005, False) + try_zero_digits(float_is_zero, 0.0008, False) + + def try_compare_digits(float_compare, amount1, amount2, expected): + assert float_compare(amount1, amount2, precision_digits=3) == expected, \ + "Rounding error, compare_amounts(%s,%s) should be %s" % (amount1, amount2, expected) + try_compare_digits(float_compare, 0.0003, 0.0004, 0) + try_compare_digits(float_compare, 0.0002, 0.0005, -1) + try_compare_digits(float_compare, 0.0009, 0.0004, 1) + + # specifying 2 precisions is illegal: + try: + float_is_zero(0.01, precision_digits=3, precision_rounding=0.01) + except AssertionError: + pass + try: + float_compare(0.01, 0.02, precision_digits=3, precision_rounding=0.01) + except AssertionError: + pass + try: + float_round(0.01, precision_digits=3, precision_rounding=0.01) + except AssertionError: + pass diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index ae28567e19b..496767f7c41 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -1210,11 +1210,13 @@ def _float_check_precision(precision_digits=None, precision_rounding=None): def float_round(amount, precision_digits=None, precision_rounding=None): """Return ``amount`` rounded to ``precision_digits`` - decimal digits, minimizing IEEE-754 floating point representation + decimal digits, minimizing IEEE-854 floating point representation errors. Precision must be given by ``precision_digits`` or ``precision_rounding``, not both! - Example on Python 2.7.2:: + + To illustrate how this is different from the default round() builtin, + here is an example (depends on Python version, here is for v2.7.2 x64):: >>> round_float(2.675) 2.68 @@ -1241,6 +1243,11 @@ def float_is_zero(amount, precision_digits=None, precision_rounding=None): Precision must be given by ``precision_digits`` or ``precision_rounding``, not both! + Warning: ``float_is_zero(amount1-amount2)`` is not always equivalent to + ``float_compare(amount1,amount2) == 0``, as the former will round after + computing the difference, while the latter will round before, giving + different results for e.g. 0.006 and 0.002 at 2 digits precision. + :param int precision_digits: number of decimal digits to round to. :param float precision_rounding: decimal number representing the minimum non-zero value at the desired precision (for example, 0.01 for a @@ -1253,13 +1260,20 @@ def float_is_zero(amount, precision_digits=None, precision_rounding=None): return abs(float_round(amount, precision_rounding=rounding_factor)) < rounding_factor def float_compare(amount1, amount2, precision_digits=None, precision_rounding=None): - """Compare ``amount1`` and ``amount2`` according - to the given precision. - Precision must be given by ``precision_digits`` or ``precision_rounding``, - not both! + """Compare ``amount1`` and ``amount2`` after rounding them according to the + given precision. An amount is considered lower/greater than another amount + if their rounded value is different. This is not the same as having a + non-zero difference! For example 1.432 and 1.431 are equal at 2 digits precision, so this method would return 0 + However 0.006 and 0.002 are considered different (returns 1) because + they respectively round to 0.01 and 0.0, even though + 0.006-0.002 = 0.004 which would be considered zero at 2 digits precision. + + + Precision must be given by ``precision_digits`` or ``precision_rounding``, + not both! :param int precision_digits: number of decimal digits to round to. :param float precision_rounding: decimal number representing the minimum @@ -1272,9 +1286,10 @@ def float_compare(amount1, amount2, precision_digits=None, precision_rounding=No """ rounding_factor = _float_check_precision(precision_digits=precision_digits, precision_rounding=precision_rounding) + amount1 = float_round(amount1, precision_rounding=rounding_factor) + amount2 = float_round(amount2, precision_rounding=rounding_factor) delta = amount1 - amount2 if float_is_zero(delta, precision_rounding=rounding_factor): return 0 - delta = float_round(delta, precision_rounding=rounding_factor) - return -1 if delta < 0 else 1 + return -1 if delta < 0.0 else 1 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file From ba2b2bdae168ef5d5451798925bfcb5df1a08d7b Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Tue, 15 Nov 2011 12:36:05 +0100 Subject: [PATCH 09/75] [IMP] tools.float_*: more tests with negative values, some cleanup bzr revid: odo@openerp.com-20111115113605-l5e6gg675qbrvjz6 --- openerp/addons/base/test/base_test.yml | 122 +++++++++++++++++-------- openerp/tools/misc.py | 48 +++++----- 2 files changed, 110 insertions(+), 60 deletions(-) diff --git a/openerp/addons/base/test/base_test.yml b/openerp/addons/base/test/base_test.yml index 9b686ccb0d3..56d723bf8a3 100644 --- a/openerp/addons/base/test/base_test.yml +++ b/openerp/addons/base/test/base_test.yml @@ -145,61 +145,111 @@ self.pool._init = True - - "Float precision tests: verify that float rounding methods are working correctly" + "Float precision tests: verify that float rounding methods are working correctly via res.currency" - !python {model: res.currency}: | currency = self.browse(cr, uid, ref('base.EUR')) - def try_round(self, cr, currency, amount, expected): + def try_round(amount, expected, self=self, cr=cr, currency=currency): result = str(self.round(cr, 1, currency, amount)) assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected) - try_round(self, cr, currency, 2.674,'2.67') - try_round(self, cr, currency, 2.675,'2.68') # in Python 2.7.2, round(2.675,2) gives 2.67 - try_round(self, cr, currency, 0.001,'0.0') - try_round(self, cr, currency, 0.0049,'0.0') # 0.0049 is closer to 0 than to 0.01, so should round down - try_round(self, cr, currency, 0.005,'0.01') # the rule is to round half up + try_round(2.674,'2.67') + try_round(2.675,'2.68') # in Python 2.7.2, round(2.675,2) gives 2.67 + try_round(-2.675,'-2.68') # in Python 2.7.2, round(2.675,2) gives 2.67 + try_round(0.001,'0.0') + try_round(-0.001,'-0.0') + try_round(0.0049,'0.0') # 0.0049 is closer to 0 than to 0.01, so should round down + try_round(0.005,'0.01') # the rule is to round half away from zero + try_round(-0.005,'-0.01') # the rule is to round half away from zero - def try_zero(self, cr, currency, amount, expected): + def try_zero(amount, expected, self=self, cr=cr, currency=currency): assert self.is_zero(cr, 1, currency, amount) == expected, "Rounding error: %s should be zero!" % amount - try_zero(self, cr, currency, 0.01, False) - try_zero(self, cr, currency, 0.001, True) - try_zero(self, cr, currency, 0.0046, True) - try_zero(self, cr, currency, 2.68-2.675, False) # 2.68 - 2.675 = 0.005 -> rounds to 0.01 - try_zero(self, cr, currency, 2.68-2.676, True) # 2.68 - 2.675 = 0.004 -> rounds to 0.0 + try_zero(0.01, False) + try_zero(-0.01, False) + try_zero(0.001, True) + try_zero(-0.001, True) + try_zero(0.0046, True) + try_zero(-0.0046, True) + try_zero(2.68-2.675, False) # 2.68 - 2.675 = 0.005 -> rounds to 0.01 + try_zero(2.68-2.676, True) # 2.68 - 2.675 = 0.004 -> rounds to 0.0 + try_zero(2.676-2.68, True) # 2.675 - 2.68 = -0.004 -> rounds to -0.0 + try_zero(2.675-2.68, False) # 2.675 - 2.68 = -0.005 -> rounds to -0.01 - def try_compare(self, cr, currency, amount1, amount2, expected): + def try_compare(amount1, amount2, expected, self=self, cr=cr, currency=currency): assert self.compare_amounts(cr, 1, currency, amount1, amount2) == expected, \ "Rounding error, compare_amounts(%s,%s) should be %s" % (amount1, amount2, expected) - try_compare(self, cr, currency, 0.001, 0.001, 0) - try_compare(self, cr, currency, 0.001, 0.002, 0) - try_compare(self, cr, currency, 2.675, 2.68, 0) - try_compare(self, cr, currency, 2.676, 2.68, 0) - try_compare(self, cr, currency, 2.674, 2.68, -1) - try_compare(self, cr, currency, 3, 2.68, 1) - try_compare(self, cr, currency, 0.01, 0, 1) + try_compare(0.001, 0.001, 0) + try_compare(-0.001, -0.001, 0) + try_compare(0.001, 0.002, 0) + try_compare(-0.001, -0.002, 0) + try_compare(2.675, 2.68, 0) + try_compare(2.676, 2.68, 0) + try_compare(-2.676, -2.68, 0) + try_compare(2.674, 2.68, -1) + try_compare(-2.674, -2.68, 1) + try_compare(3, 2.68, 1) + try_compare(-3, -2.68, -1) + try_compare(0.01, 0, 1) + try_compare(-0.01, 0, -1) +- + "Float precision tests: verify that float rounding methods are working correctly via tools" +- + !python {model: res.currency}: | from tools import float_compare, float_is_zero, float_round - def try_round_digits(float_round, amount, expected): - result = str(float_round(amount, precision_digits=3)) + def try_round(amount, expected, precision_digits=3, float_round=float_round): + result = str(float_round(amount, precision_digits=precision_digits)) assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected) - try_round_digits(float_round, 2.6745, '2.675') - try_round_digits(float_round, 2.6744, '2.674') - try_round_digits(float_round, 0.0004, '0.0') + try_round(2.6745, '2.675') + try_round(-2.6745, '-2.675') + try_round(2.6744, '2.674') + try_round(-2.6744, '-2.674') + try_round(0.0004, '0.0') + try_round(-0.0004, '-0.0') + #try_round(357.4555, '357.456') + #try_round(-357.4555, '-357.456') + try_round(457.4554, '457.455') + try_round(-457.4554, '-457.455') - def try_zero_digits(float_is_zero, amount, expected): + + def try_zero(amount, expected, float_is_zero=float_is_zero): assert float_is_zero(amount, precision_digits=3) == expected, "Rounding error: %s should be zero!" % amount - try_zero_digits(float_is_zero, 0.0002, True) - try_zero_digits(float_is_zero, 0.00034, True) - try_zero_digits(float_is_zero, 0.0005, False) - try_zero_digits(float_is_zero, 0.0008, False) + try_zero(0.0002, True) + try_zero(-0.0002, True) + try_zero(0.00034, True) + try_zero(0.0005, False) + try_zero(-0.0005, False) + try_zero(0.0008, False) + try_zero(-0.0008, False) - def try_compare_digits(float_compare, amount1, amount2, expected): + def try_compare(amount1, amount2, expected, float_compare=float_compare): assert float_compare(amount1, amount2, precision_digits=3) == expected, \ "Rounding error, compare_amounts(%s,%s) should be %s" % (amount1, amount2, expected) - try_compare_digits(float_compare, 0.0003, 0.0004, 0) - try_compare_digits(float_compare, 0.0002, 0.0005, -1) - try_compare_digits(float_compare, 0.0009, 0.0004, 1) + try_compare(0.0003, 0.0004, 0) + try_compare(-0.0003, -0.0004, 0) + try_compare(0.0002, 0.0005, -1) + try_compare(-0.0002, -0.0005, 1) + try_compare(0.0009, 0.0004, 1) + try_compare(-0.0009, -0.0004, -1) + try_compare(557.4555, 557.4556, 0) + try_compare(-557.4555, -557.4556, 0) + try_compare(657.4444, 657.445, -1) + try_compare(-657.4444, -657.445, 1) - # specifying 2 precisions is illegal: + # Rounding to different precision_roundings + def try_round(amount, expected, precision_rounding=None, float_round=float_round): + result = str(float_round(amount, precision_rounding=precision_rounding)) + assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected) + try_round(-457.4554, '-457.45', precision_rounding=0.05) + try_round(457.444, '457.5', precision_rounding=0.5) + try_round(457.3, '455.0', precision_rounding=5) + try_round(457.5, '460.0', precision_rounding=5) + try_round(457.1, '456.0', precision_rounding=3) + +- + "Float precision tests: verify that invalid parameters are forbidden" +- + !python {model: res.currency}: | + from tools import float_compare, float_is_zero, float_round try: float_is_zero(0.01, precision_digits=3, precision_rounding=0.01) except AssertionError: diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index 496767f7c41..5b5713749ac 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -1208,9 +1208,9 @@ def _float_check_precision(precision_digits=None, precision_rounding=None): return 10 ** -precision_digits return precision_rounding -def float_round(amount, precision_digits=None, precision_rounding=None): - """Return ``amount`` rounded to ``precision_digits`` - decimal digits, minimizing IEEE-854 floating point representation +def float_round(value, precision_digits=None, precision_rounding=None): + """Return ``value`` rounded to ``precision_digits`` + decimal digits, minimizing IEEE-754 floating point representation errors. Precision must be given by ``precision_digits`` or ``precision_rounding``, not both! @@ -1223,7 +1223,7 @@ def float_round(amount, precision_digits=None, precision_rounding=None): >>> round(2.675,2) 2.67 - :param float amount: the amount to round + :param float value: the value to round :param int precision_digits: number of decimal digits to round to. :param float precision_rounding: decimal number representing the minimum non-zero value at the desired precision (for example, 0.01 for a @@ -1233,18 +1233,18 @@ def float_round(amount, precision_digits=None, precision_rounding=None): rounding_factor = _float_check_precision(precision_digits=precision_digits, precision_rounding=precision_rounding) if rounding_factor == 0: return 0.0 - # /!\ First member below must be rounded to full unit! - # Do not pass rounding digits to round()! - return round(amount / rounding_factor) * rounding_factor -def float_is_zero(amount, precision_digits=None, precision_rounding=None): - """Returns true if ``amount`` is small enough to be treated as + # Then round to integer wrt. rounding factor + return round(value / rounding_factor) * rounding_factor + +def float_is_zero(value, precision_digits=None, precision_rounding=None): + """Returns true if ``value`` is small enough to be treated as zero at the given precision. Precision must be given by ``precision_digits`` or ``precision_rounding``, not both! - Warning: ``float_is_zero(amount1-amount2)`` is not always equivalent to - ``float_compare(amount1,amount2) == 0``, as the former will round after + Warning: ``float_is_zero(value1-value2)`` is not always equivalent to + ``float_compare(value1,value2) == 0``, as the former will round after computing the difference, while the latter will round before, giving different results for e.g. 0.006 and 0.002 at 2 digits precision. @@ -1252,16 +1252,16 @@ def float_is_zero(amount, precision_digits=None, precision_rounding=None): :param float precision_rounding: decimal number representing the minimum non-zero value at the desired precision (for example, 0.01 for a 2-digit precision). - :param float amount: amount to compare with currency's zero - :return: True if ``amount`` is considered 0 + :param float value: value to compare with currency's zero + :return: True if ``value`` is considered 0 """ rounding_factor = _float_check_precision(precision_digits=precision_digits, precision_rounding=precision_rounding) - return abs(float_round(amount, precision_rounding=rounding_factor)) < rounding_factor + return abs(float_round(value, precision_rounding=rounding_factor)) < rounding_factor -def float_compare(amount1, amount2, precision_digits=None, precision_rounding=None): - """Compare ``amount1`` and ``amount2`` after rounding them according to the - given precision. An amount is considered lower/greater than another amount +def float_compare(value1, value2, precision_digits=None, precision_rounding=None): + """Compare ``value1`` and ``value2`` after rounding them according to the + given precision. A value is considered lower/greater than another value if their rounded value is different. This is not the same as having a non-zero difference! @@ -1279,16 +1279,16 @@ def float_compare(amount1, amount2, precision_digits=None, precision_rounding=No :param float precision_rounding: decimal number representing the minimum non-zero value at the desired precision (for example, 0.01 for a 2-digit precision). - :param float amount1: first amount to compare - :param float amount2: second amount to compare - :return: (resp.) -1, 0 or 1, if ``amount1`` is (resp.) lower than, - equal to, or greater than ``amount2``, at the given precision. + :param float value1: first value to compare + :param float value2: second value to compare + :return: (resp.) -1, 0 or 1, if ``value1`` is (resp.) lower than, + equal to, or greater than ``value2``, at the given precision. """ rounding_factor = _float_check_precision(precision_digits=precision_digits, precision_rounding=precision_rounding) - amount1 = float_round(amount1, precision_rounding=rounding_factor) - amount2 = float_round(amount2, precision_rounding=rounding_factor) - delta = amount1 - amount2 + value1 = float_round(value1, precision_rounding=rounding_factor) + value2 = float_round(value2, precision_rounding=rounding_factor) + delta = value1 - value2 if float_is_zero(delta, precision_rounding=rounding_factor): return 0 return -1 if delta < 0.0 else 1 From 1156aee52a8950c608054ba883fcf15783c1be92 Mon Sep 17 00:00:00 2001 From: Florent Xicluna Date: Thu, 17 Nov 2011 10:33:35 +0100 Subject: [PATCH 10/75] [FIX] discard closed connections. Restart not required anymore when connection is lost. bzr revid: florent.xicluna@wingo.ch-20111117093335-6t16181mqt49z7bi --- openerp/sql_db.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openerp/sql_db.py b/openerp/sql_db.py index ab92d7cc21d..63ab2a13fb9 100644 --- a/openerp/sql_db.py +++ b/openerp/sql_db.py @@ -175,18 +175,18 @@ class Cursor(object): self._cnx = pool.borrow(dsn(dbname)) self._obj = self._cnx.cursor(cursor_factory=psycopg1cursor) - self.__closed = False # real initialisation value - self.autocommit(False) if self.sql_log: self.__caller = frame_codeinfo(currentframe(),2) else: self.__caller = False + self.__closed = False # real initialisation value + self.autocommit(False) self.__closer = False self._default_log_exceptions = True def __del__(self): - if not self.__closed: + if not self.__closed and not self._cnx.closed: # Oops. 'self' has not been closed explicitly. # The cursor will be deleted by the garbage collector, # but the database connection is not put back into the connection @@ -390,6 +390,10 @@ class ConnectionPool(object): # free leaked connections for i, (cnx, _) in tools.reverse_enumerate(self._connections): + if cnx.closed: + self._connections.pop(i) + self._debug('Removing closed connection at index %d: %r', i, cnx.dsn) + continue if getattr(cnx, 'leaked', False): delattr(cnx, 'leaked') self._connections.pop(i) From ca5fcc2bdfd49920a8d54ef8f361b9571c9b06d2 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Thu, 17 Nov 2011 15:30:10 +0100 Subject: [PATCH 11/75] [IMP] orm,fields: some cleanup for sparse field implementation (wip) bzr revid: odo@openerp.com-20111117143010-9nv7inr5ijzykgem --- openerp/osv/fields.py | 37 ++++++++++++++++--------------------- openerp/osv/orm.py | 21 ++++++++------------- 2 files changed, 24 insertions(+), 34 deletions(-) diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 04364a63074..222dd4aaeb0 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -1210,23 +1210,23 @@ class sparse(function): """ if self._type == 'many2many': - #NOTE only the option (0, 0, { values }) is supported for many2many - if value[0][0] == 6: - return value[0][2] - + assert value[0][0] == 6, 'Unsupported m2m value for sparse field: %s' % value + return value[0][2] + elif self._type == 'one2many': if not read_value: - read_value=[] + read_value = [] relation_obj = obj.pool.get(self.relation) for vals in value: + assert vals[0] in (0,1,2), 'Unsupported o2m value for sparse field: %s' % vals if vals[0] == 0: read_value.append(relation_obj.create(cr, uid, vals[2], context=context)) elif vals[0] == 1: relation_obj.write(cr, uid, vals[1], vals[2], context=context) elif vals[0] == 2: - relation_obj.unlink(cr, uid, vals[1]) + relation_obj.unlink(cr, uid, vals[1], context=context) read_value.remove(vals[1]) - return read_value + return read_value return value @@ -1236,37 +1236,32 @@ class sparse(function): records = obj.browse(cr, uid, ids, context=context) for record in records: # grab serialized value as object - already deserialized - serialized = record.__getattr__(self.serialization_field) - # we have to delete the key in the json when the value is null + serialized = getattr(record, self.serialization_field) if value is None: - if field_name in serialized: - del serialized[field_name] - else: - # nothing to do, we dont wan't to store the key with a null value - continue + # simply delete the key to unset it. + serialized.pop(field_name, None) else: serialized[field_name] = self.convert_value(obj, cr, uid, record, value, serialized.get(field_name), context=context) obj.write(cr, uid, ids, {self.serialization_field: serialized}, context=context) return True def _fnct_read(self, obj, cr, uid, ids, field_names, args, context=None): - results={} + results = {} records = obj.browse(cr, uid, ids, context=context) for record in records: # grab serialized value as object - already deserialized - serialized = record.__getattr__(self.serialization_field) - results[record.id] ={} + serialized = getattr(record, self.serialization_field) + results[record.id] = {} for field_name in field_names: if obj._columns[field_name]._type in ['one2many']: - results[record.id].update({field_name : serialized.get(field_name, [])}) + value = serialized.get(field_name, []) else: - results[record.id].update({field_name : serialized.get(field_name)}) + results[record.id].update(field_name=value) return results def __init__(self, serialization_field, **kwargs): self.serialization_field = serialization_field - #assert serialization_field._type == 'serialized' - return super(sparse, self).__init__(self._fnct_read, fnct_inv=self._fnct_write, multi='_json_multi', method=True, **kwargs) + return super(sparse, self).__init__(self._fnct_read, fnct_inv=self._fnct_write, multi='__sparse_multi', method=True, **kwargs) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 357204bd374..a004ef96321 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -756,16 +756,10 @@ class BaseModel(object): cols[rec['name']] = rec ir_model_fields_obj = self.pool.get('ir.model.fields') - - high_priority_items=[] - low_priority_items=[] - #sparse field should be created at the end, indeed this field depend of other field - for item in self._columns.items(): - if item[1].__class__ == fields.sparse: - low_priority_items.append(item) - else: - high_priority_items.append(item) - for (k, f) in high_priority_items + low_priority_items: + + # sparse field should be created at the end, as it depends on its serialized field already existing + fields = sorted(self._columns.items(), key=lambda x: 1 if x[1]._type == 'sparse' else 0) + for (k, f) in fields: vals = { 'model_id': model_id, 'model': self._name, @@ -782,12 +776,13 @@ class BaseModel(object): 'relation_field': (f._type=='one2many' and isinstance(f, fields.one2many)) and f._fields_id or '', 'serialization_field_id': None, } - if 'serialization_field' in dir(f): + if getattr(f, 'serialization_field', None): + # resolve link to serialization_field if specified by name serialization_field_id = ir_model_fields_obj.search(cr, 1, [('model','=',vals['model']), ('name', '=', f.serialization_field)]) if not serialization_field_id: - raise except_orm(_('Error'), _("The field %s does not exist!" %f.serialization_field)) + raise except_orm(_('Error'), _("Serialization field `%s` not found for sparse field `%s`!") % (f.serialization_field, k)) vals['serialization_field_id'] = serialization_field_id[0] - + # When its a custom field,it does not contain f.select if context.get('field_state', 'base') == 'manual': if context.get('field_name', '') == k: From 663030a591c018ddabbbf105c4993830ff611ddf Mon Sep 17 00:00:00 2001 From: "Turkesh Patel (Open ERP)" Date: Tue, 29 Nov 2011 15:22:34 +0530 Subject: [PATCH 12/75] [FIX] solved problem of duplication in apprisal survey in hr_evaluation module bzr revid: tpa@tinyerp.com-20111129095234-p0d7u8ihfhp706k2 --- addons/hr_evaluation/hr_evaluation.py | 7 +++---- addons/hr_evaluation/hr_evaluation_view.xml | 2 -- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/addons/hr_evaluation/hr_evaluation.py b/addons/hr_evaluation/hr_evaluation.py index e1d451c9f7f..54a15d5593c 100644 --- a/addons/hr_evaluation/hr_evaluation.py +++ b/addons/hr_evaluation/hr_evaluation.py @@ -250,13 +250,12 @@ class hr_evaluation(osv.osv): return True def button_cancel(self, cr, uid, ids, context=None): + interview_obj=self.pool.get('hr.evaluation.interview') + evaluation = self.browse(cr, uid, ids[0], context) + interview_obj.survey_req_cancel(cr, uid, [r.id for r in evaluation.survey_request_ids]) self.write(cr, uid, ids,{'state':'cancel'}, context=context) return True - def button_draft(self, cr, uid, ids, context=None): - self.write(cr, uid, ids,{'state': 'draft'}, context=context) - return True - def write(self, cr, uid, ids, vals, context=None): if 'date' in vals: new_vals = {'date_deadline': vals.get('date')} diff --git a/addons/hr_evaluation/hr_evaluation_view.xml b/addons/hr_evaluation/hr_evaluation_view.xml index d8dfa7e40ca..4bf3ba0fff7 100644 --- a/addons/hr_evaluation/hr_evaluation_view.xml +++ b/addons/hr_evaluation/hr_evaluation_view.xml @@ -207,8 +207,6 @@ icon="gtk-execute"/> ' ].join('') } - if (!row_data[column.id]) { return value_if_empty === undefined ? '' : value_if_empty; } diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index 472001390e8..c8469eefe4d 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -557,6 +557,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# * @param {Function} callback should be called after the action is executed, if non-null */ do_button_action: function (name, id, callback) { + var self = this; var action = _.detect(this.columns, function (field) { return field.name === name; }); @@ -575,7 +576,9 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# c.add(action.context); } action.context = c; - this.do_execute_action(action, this.dataset, id, callback); + this.do_execute_action(action, this.dataset, id, function(){ + self.o2m.view.reload(); + }); }, /** * Handles the activation of a record (clicking on it) From 87ba81c4d72bd547f492991a0832c99220b5d159 Mon Sep 17 00:00:00 2001 From: Minh Tran Date: Tue, 20 Dec 2011 13:12:27 +0100 Subject: [PATCH 31/75] fixes some more demo data issues bzr revid: mit@openerp.com-20111220121227-wy70e5amctmlq116 --- addons/project/project_demo.xml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/addons/project/project_demo.xml b/addons/project/project_demo.xml index 8af8b1a7635..26993452494 100644 --- a/addons/project/project_demo.xml +++ b/addons/project/project_demo.xml @@ -22,26 +22,26 @@ Quentin - qdp - qdp + qdpa + qdpa Francois - fpi - fpi + fpie + fpie Julien - jth - jth + jthe + jthe Aline - apr - apr + apre + apre Fabian From ed53ef476137c707245e3ef50ab800f31de58aeb Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Tue, 20 Dec 2011 17:34:20 +0100 Subject: [PATCH 32/75] [IMP] float_utils: fix HALF_UP rounding according to discussions on bug 882036 Also improved test and added warning logging when crossing the precision limit due to too many significant digits. There is apparently a limitation in Python's float implementation for this. bzr revid: odo@openerp.com-20111220163420-lz0sh1h0yjkh6jdc --- openerp/addons/base/test/base_test.yml | 21 +++++- openerp/tools/float_utils.py | 89 ++++++++++++++++++++++++-- 2 files changed, 103 insertions(+), 7 deletions(-) diff --git a/openerp/addons/base/test/base_test.yml b/openerp/addons/base/test/base_test.yml index 56d723bf8a3..41c5c8d84e8 100644 --- a/openerp/addons/base/test/base_test.yml +++ b/openerp/addons/base/test/base_test.yml @@ -205,11 +205,28 @@ try_round(-2.6744, '-2.674') try_round(0.0004, '0.0') try_round(-0.0004, '-0.0') - #try_round(357.4555, '357.456') - #try_round(-357.4555, '-357.456') + try_round(357.4555, '357.456') + try_round(-357.4555, '-357.456') try_round(457.4554, '457.455') try_round(-457.4554, '-457.455') + # Extended float range test, inspired by Cloves Almeida's test on bug #882036. + fractions = [.0, .015, .01499, .675, .67499, .4555, .4555, .45555] + expecteds = ['.0', '.02', '.01', '.68', '.67', '.46', '.456', '.4556'] + precisions = [2, 2, 2, 2, 2, 2, 3, 4] + # We can't go higher than 12 significant digits with a fractional part, so + # that means max magnitude = 5 with precision 4 if sample values (x) go up to 10000 + # because that gives 5 + 4 + 4 = 13 significant digits. + for magnitude in range(5): + for i in xrange(len(fractions)): + frac, exp, prec = fractions[i], expecteds[i], precisions[i] + for sign in [-1,1]: + for x in xrange(0,10000,34): + n = x * 10**magnitude + f = sign * (n + frac) + f_exp = ('-' if f != 0 and sign == -1 else '') + str(n) + exp + try_round(f, f_exp, precision_digits=prec) + def try_zero(amount, expected, float_is_zero=float_is_zero): assert float_is_zero(amount, precision_digits=3) == expected, "Rounding error: %s should be zero!" % amount diff --git a/openerp/tools/float_utils.py b/openerp/tools/float_utils.py index effd473e269..202de89d0cf 100644 --- a/openerp/tools/float_utils.py +++ b/openerp/tools/float_utils.py @@ -19,6 +19,16 @@ # ############################################################################## +import logging +import math +from decimal import Decimal, ROUND_HALF_UP + +# When a number crosses this threshold, significant decimal +# digits may be lost when trying to render the float value, due to +# Python's float implementation. +# e.g. str(10060000.45556) == '10060000.4556' => lost 1 digit! +SIGNIFICANT_DIGITS_SCALE_LIMIT = math.log(10**12, 2) # 10**12 ~= 2**39.86 + def _float_check_precision(precision_digits=None, precision_rounding=None): assert (precision_digits is not None or precision_rounding is not None) and \ not (precision_digits and precision_rounding),\ @@ -51,10 +61,39 @@ def float_round(value, precision_digits=None, precision_rounding=None): """ rounding_factor = _float_check_precision(precision_digits=precision_digits, precision_rounding=precision_rounding) - if rounding_factor == 0: return 0.0 - - # Then round to integer wrt. rounding factor - return round(value / rounding_factor) * rounding_factor + if rounding_factor == 0 or value == 0: return 0.0 + # scale up by rounding_factor, in order to implement rounding to arbitrary + # `units` or 'rounding_factors'. + # Example: if rounding_factor is 0.5, 1.3 should round to 1.5 + # So we'll do this: scaled_value = 1.3 / 0.5 = 2.6 + # int_rounded = round(2.6) = 3 + # result = 3 * 0.5 = 1.5 + # Also, .5 is a binary fraction, so this automatically solves some tricky + # cases when rounding_factor is a negative power of 10. E.g 2.6745 is + # difficult to round to 0.001 because it does not have an exact IEEE754 + # representation, but 2674.5 is simple to round to 2675 because both + # are exactly represented. + scaled_value = value / rounding_factor + # Despite the advantage of rounding to .5 binary fractions, we still need + # to add a small epsilon value to take care of cases where the float repr + # is slightly too far below .5 to properly round *up* automatically. + # That epsilon needs to be scaled according to the order of magnitude of + # the value. (Credit: discussed with several community members on bug 882036) + epsilon_scale = math.log(abs(scaled_value), 2) + frac_part, _ = math.modf(scaled_value) + if frac_part and epsilon_scale > SIGNIFICANT_DIGITS_SCALE_LIMIT: + print 'Float rounding of %r to %r precision requires too many '\ + 'significant digits, a loss of precision may occur in the '\ + 'least significant digits' % (value,rounding_factor) + logging.getLogger('float_utils')\ + .warning('Float rounding of %r to %r precision requires too many ' + 'significant digits, a loss of precision may occur in the ' + 'least significant digits', value, rounding_factor) + epsilon = 2**(epsilon_scale-50) + scaled_value += cmp(scaled_value,0) * epsilon + rounded_value = round(scaled_value) + result = rounded_value * rounding_factor + return result def float_is_zero(value, precision_digits=None, precision_rounding=None): """Returns true if ``value`` is small enough to be treated as @@ -114,4 +153,44 @@ def float_compare(value1, value2, precision_digits=None, precision_rounding=None value2 = float_round(value2, precision_rounding=rounding_factor) delta = value1 - value2 if float_is_zero(delta, precision_rounding=rounding_factor): return 0 - return -1 if delta < 0.0 else 1 \ No newline at end of file + return -1 if delta < 0.0 else 1 + + + + + +if __name__ == "__main__": + + import time + start = time.time() + count = 0 + errors = 0 + + def try_round(amount, expected, precision_digits=3): + global count, errors; count += 1 + result = float_round(amount, precision_digits=precision_digits) + if str(result) != expected: + errors += 1 + print '###!!! Rounding error: got %s or %s, expected %s' % (str(result), repr(result), expected) + + # Extended float range test, inspired by Cloves Almeida's test on bug #882036. + fractions = [.0, .015, .01499, .675, .67499, .4555, .4555, .45555] + expecteds = ['.0', '.02', '.01', '.68', '.67', '.46', '.456', '.4556'] + precisions = [2, 2, 2, 2, 2, 2, 3, 4] + for magnitude in range(5): + for i in xrange(len(fractions)): + frac, exp, prec = fractions[i], expecteds[i], precisions[i] + for sign in [-1,1]: + for x in xrange(0,10000,17): + n = x * 10**magnitude + f = sign * (n + frac) + f_exp = ('-' if f != 0 and sign == -1 else '') + str(n) + exp + try_round(f, f_exp, precision_digits=prec) + + stop = time.time() + + # Micro-bench results: + # 47130 round calls in 0.422306060791 secs, with Python 2.6.7 on Core i3 x64 + # with decimal: + # 47130 round calls in 6.612248100021 secs, with Python 2.6.7 on Core i3 x64 + print count, " round calls, ", errors, "errors, done in ", (stop-start), 'secs' From 707bf0d8aa55a4c493d1a94393f38a1799233b96 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Tue, 20 Dec 2011 18:03:28 +0100 Subject: [PATCH 33/75] [FIX] fields.float: properly round when coercing to desired float precision lp bug: https://launchpad.net/bugs/882036 fixed bzr revid: odo@openerp.com-20111220170328-nvutckcxnitkcq9d --- openerp/osv/fields.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 42bc9b75c1d..0579677fd5e 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -45,6 +45,7 @@ import openerp import openerp.netsvc as netsvc import openerp.tools as tools from openerp.tools.translate import _ +from openerp.tools import float_round def _symbol_set(symb): if symb == None or symb == False: @@ -229,17 +230,17 @@ class float(_column): def __init__(self, string='unknown', digits=None, digits_compute=None, required=False, **args): _column.__init__(self, string=string, required=required, **args) self.digits = digits + # synopsis: digits_compute(cr) -> (precision, scale) self.digits_compute = digits_compute if required: warnings.warn("Making a float field `required` has no effect, as NULL values are " "automatically turned into 0.0", PendingDeprecationWarning, stacklevel=2) - def digits_change(self, cr): if self.digits_compute: - t = self.digits_compute(cr) - self._symbol_set=('%s', lambda x: ('%.'+str(t[1])+'f') % (__builtin__.float(x or 0.0),)) - self.digits = t + self.digits = self.digits_compute(cr) + precision, scale = self.digits + self._symbol_set = ('%s', lambda x: str(float_round(__builtin__.float(x or 0.0), precision_digits=scale))) class date(_column): _type = 'date' @@ -991,10 +992,9 @@ class function(_column): def digits_change(self, cr): if self.digits_compute: - t = self.digits_compute(cr) - self._symbol_set=('%s', lambda x: ('%.'+str(t[1])+'f') % (__builtin__.float(x or 0.0),)) - self.digits = t - + self.digits = self.digits_compute(cr) + precision, scale = self.digits + self._symbol_set = ('%s', lambda x: str(float_round(__builtin__.float(x or 0.0), precision_digits=scale))) def search(self, cr, uid, obj, name, args, context=None): if not self._fnct_search: From 759ccd984542ce4077dfcc9fa14198b60719dded Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 21 Dec 2011 00:47:40 +0100 Subject: [PATCH 34/75] [IMP] float_utils: simplified code, added float_repr lp bug: https://launchpad.net/bugs/882036 fixed bzr revid: odo@openerp.com-20111220234740-kotcgoz3opcbkx4b --- openerp/addons/base/test/base_test.yml | 51 +++++----- openerp/osv/fields.py | 8 +- openerp/tools/float_utils.py | 128 ++++++++++++------------- 3 files changed, 92 insertions(+), 95 deletions(-) diff --git a/openerp/addons/base/test/base_test.yml b/openerp/addons/base/test/base_test.yml index 41c5c8d84e8..fcead834d93 100644 --- a/openerp/addons/base/test/base_test.yml +++ b/openerp/addons/base/test/base_test.yml @@ -148,16 +148,20 @@ "Float precision tests: verify that float rounding methods are working correctly via res.currency" - !python {model: res.currency}: | + from tools import float_repr + from math import log10 currency = self.browse(cr, uid, ref('base.EUR')) - def try_round(amount, expected, self=self, cr=cr, currency=currency): - result = str(self.round(cr, 1, currency, amount)) + def try_round(amount, expected, self=self, cr=cr, currency=currency, float_repr=float_repr, + log10=log10): + digits = max(0,-int(log10(currency.rounding))) + result = float_repr(self.round(cr, 1, currency, amount), precision_digits=digits) assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected) try_round(2.674,'2.67') try_round(2.675,'2.68') # in Python 2.7.2, round(2.675,2) gives 2.67 try_round(-2.675,'-2.68') # in Python 2.7.2, round(2.675,2) gives 2.67 - try_round(0.001,'0.0') - try_round(-0.001,'-0.0') - try_round(0.0049,'0.0') # 0.0049 is closer to 0 than to 0.01, so should round down + try_round(0.001,'0.00') + try_round(-0.001,'-0.00') + try_round(0.0049,'0.00') # 0.0049 is closer to 0 than to 0.01, so should round down try_round(0.005,'0.01') # the rule is to round half away from zero try_round(-0.005,'-0.01') # the rule is to round half away from zero @@ -195,16 +199,17 @@ "Float precision tests: verify that float rounding methods are working correctly via tools" - !python {model: res.currency}: | - from tools import float_compare, float_is_zero, float_round - def try_round(amount, expected, precision_digits=3, float_round=float_round): - result = str(float_round(amount, precision_digits=precision_digits)) + from tools import float_compare, float_is_zero, float_round, float_repr + def try_round(amount, expected, precision_digits=3, float_round=float_round, float_repr=float_repr): + result = float_repr(float_round(amount, precision_digits=precision_digits), + precision_digits=precision_digits) assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected) try_round(2.6745, '2.675') try_round(-2.6745, '-2.675') try_round(2.6744, '2.674') try_round(-2.6744, '-2.674') - try_round(0.0004, '0.0') - try_round(-0.0004, '-0.0') + try_round(0.0004, '0.000') + try_round(-0.0004, '-0.000') try_round(357.4555, '357.456') try_round(-357.4555, '-357.456') try_round(457.4554, '457.455') @@ -212,16 +217,15 @@ # Extended float range test, inspired by Cloves Almeida's test on bug #882036. fractions = [.0, .015, .01499, .675, .67499, .4555, .4555, .45555] - expecteds = ['.0', '.02', '.01', '.68', '.67', '.46', '.456', '.4556'] + expecteds = ['.00', '.02', '.01', '.68', '.67', '.46', '.456', '.4556'] precisions = [2, 2, 2, 2, 2, 2, 3, 4] - # We can't go higher than 12 significant digits with a fractional part, so - # that means max magnitude = 5 with precision 4 if sample values (x) go up to 10000 - # because that gives 5 + 4 + 4 = 13 significant digits. - for magnitude in range(5): + # Note: max precision for double floats is 53 bits of precision or + # 17 significant decimal digits + for magnitude in range(7): for i in xrange(len(fractions)): frac, exp, prec = fractions[i], expecteds[i], precisions[i] for sign in [-1,1]: - for x in xrange(0,10000,34): + for x in xrange(0,10000,97): n = x * 10**magnitude f = sign * (n + frac) f_exp = ('-' if f != 0 and sign == -1 else '') + str(n) + exp @@ -252,15 +256,16 @@ try_compare(657.4444, 657.445, -1) try_compare(-657.4444, -657.445, 1) - # Rounding to different precision_roundings - def try_round(amount, expected, precision_rounding=None, float_round=float_round): - result = str(float_round(amount, precision_rounding=precision_rounding)) + # Rounding to unusual rounding units (e.g. coin values) + def try_round(amount, expected, precision_rounding=None, float_round=float_round, float_repr=float_repr): + result = float_repr(float_round(amount, precision_rounding=precision_rounding), + precision_digits=2) assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected) try_round(-457.4554, '-457.45', precision_rounding=0.05) - try_round(457.444, '457.5', precision_rounding=0.5) - try_round(457.3, '455.0', precision_rounding=5) - try_round(457.5, '460.0', precision_rounding=5) - try_round(457.1, '456.0', precision_rounding=3) + try_round(457.444, '457.50', precision_rounding=0.5) + try_round(457.3, '455.00', precision_rounding=5) + try_round(457.5, '460.00', precision_rounding=5) + try_round(457.1, '456.00', precision_rounding=3) - "Float precision tests: verify that invalid parameters are forbidden" diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 0579677fd5e..ea1b6d93329 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -45,7 +45,7 @@ import openerp import openerp.netsvc as netsvc import openerp.tools as tools from openerp.tools.translate import _ -from openerp.tools import float_round +from openerp.tools import float_round, float_repr def _symbol_set(symb): if symb == None or symb == False: @@ -240,7 +240,8 @@ class float(_column): if self.digits_compute: self.digits = self.digits_compute(cr) precision, scale = self.digits - self._symbol_set = ('%s', lambda x: str(float_round(__builtin__.float(x or 0.0), precision_digits=scale))) + self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0), precision_digits=scale), + precision_digits=scale)) class date(_column): _type = 'date' @@ -994,7 +995,8 @@ class function(_column): if self.digits_compute: self.digits = self.digits_compute(cr) precision, scale = self.digits - self._symbol_set = ('%s', lambda x: str(float_round(__builtin__.float(x or 0.0), precision_digits=scale))) + self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0), precision_digits=scale), + precision_digits=scale)) def search(self, cr, uid, obj, name, args, context=None): if not self._fnct_search: diff --git a/openerp/tools/float_utils.py b/openerp/tools/float_utils.py index 202de89d0cf..5c934114e3c 100644 --- a/openerp/tools/float_utils.py +++ b/openerp/tools/float_utils.py @@ -19,15 +19,7 @@ # ############################################################################## -import logging import math -from decimal import Decimal, ROUND_HALF_UP - -# When a number crosses this threshold, significant decimal -# digits may be lost when trying to render the float value, due to -# Python's float implementation. -# e.g. str(10060000.45556) == '10060000.4556' => lost 1 digit! -SIGNIFICANT_DIGITS_SCALE_LIMIT = math.log(10**12, 2) # 10**12 ~= 2**39.86 def _float_check_precision(precision_digits=None, precision_rounding=None): assert (precision_digits is not None or precision_rounding is not None) and \ @@ -40,20 +32,12 @@ def _float_check_precision(precision_digits=None, precision_rounding=None): def float_round(value, precision_digits=None, precision_rounding=None): """Return ``value`` rounded to ``precision_digits`` decimal digits, minimizing IEEE-754 floating point representation - errors. + errors, and applying HALF-UP (away from zero) tie-breaking rule. Precision must be given by ``precision_digits`` or ``precision_rounding``, not both! - To illustrate how this is different from the default round() builtin, - here is an example (depends on Python version, here is for v2.7.2 x64):: - - >>> round_float(2.675) - 2.68 - >>> round(2.675,2) - 2.67 - :param float value: the value to round - :param int precision_digits: number of decimal digits to round to. + :param int precision_digits: number of fractional digits to round to. :param float precision_rounding: decimal number representing the minimum non-zero value at the desired precision (for example, 0.01 for a 2-digit precision). @@ -62,58 +46,51 @@ def float_round(value, precision_digits=None, precision_rounding=None): rounding_factor = _float_check_precision(precision_digits=precision_digits, precision_rounding=precision_rounding) if rounding_factor == 0 or value == 0: return 0.0 - # scale up by rounding_factor, in order to implement rounding to arbitrary - # `units` or 'rounding_factors'. - # Example: if rounding_factor is 0.5, 1.3 should round to 1.5 - # So we'll do this: scaled_value = 1.3 / 0.5 = 2.6 - # int_rounded = round(2.6) = 3 - # result = 3 * 0.5 = 1.5 - # Also, .5 is a binary fraction, so this automatically solves some tricky - # cases when rounding_factor is a negative power of 10. E.g 2.6745 is - # difficult to round to 0.001 because it does not have an exact IEEE754 - # representation, but 2674.5 is simple to round to 2675 because both - # are exactly represented. - scaled_value = value / rounding_factor - # Despite the advantage of rounding to .5 binary fractions, we still need - # to add a small epsilon value to take care of cases where the float repr - # is slightly too far below .5 to properly round *up* automatically. - # That epsilon needs to be scaled according to the order of magnitude of - # the value. (Credit: discussed with several community members on bug 882036) - epsilon_scale = math.log(abs(scaled_value), 2) - frac_part, _ = math.modf(scaled_value) - if frac_part and epsilon_scale > SIGNIFICANT_DIGITS_SCALE_LIMIT: - print 'Float rounding of %r to %r precision requires too many '\ - 'significant digits, a loss of precision may occur in the '\ - 'least significant digits' % (value,rounding_factor) - logging.getLogger('float_utils')\ - .warning('Float rounding of %r to %r precision requires too many ' - 'significant digits, a loss of precision may occur in the ' - 'least significant digits', value, rounding_factor) - epsilon = 2**(epsilon_scale-50) - scaled_value += cmp(scaled_value,0) * epsilon - rounded_value = round(scaled_value) - result = rounded_value * rounding_factor + + # NORMALIZE - ROUND - DENORMALIZE + # In order to easily support rounding to arbitrary 'steps' (e.g. coin values), + # we normalize the value before rounding it as an integer, and de-normalize + # after rounding: e.g. float_round(1.3, precision_rounding=.5) == 1.5 + + # TIE-BREAKING: HALF-UP + # We want to apply HALF-UP tie-breaking rules, i.e. 0.5 rounds away from 0. + # Due to IEE754 float/double representation limits, the approximation of the + # real value may be slightly below the tie limit, resulting in an error of + # 1 unit in the last place (ulp) after rounding. + # For example 2.675 == 2.6749999999999998. + # To correct this, we add a very small epsilon value, scaled to the + # the order of magnitude of the value, to tip the tie-break in the right + # direction. + # Credit: discussion with OpenERP community members on bug 882036 + + normalized_value = value / rounding_factor # normalize + epsilon_magnitude = math.log(abs(normalized_value), 2) + epsilon = 2**(epsilon_magnitude-53) + normalized_value += cmp(normalized_value,0) * epsilon + rounded_value = round(normalized_value) # round to integer + result = rounded_value * rounding_factor # de-normalize return result def float_is_zero(value, precision_digits=None, precision_rounding=None): """Returns true if ``value`` is small enough to be treated as - zero at the given precision (smaller than the given *epsilon*). + zero at the given precision (smaller than the corresponding *epsilon*). + The precision (``10**-precision_digits`` or ``precision_rounding``) + is used as the zero *epsilon*: values less than that are considered + to be zero. Precision must be given by ``precision_digits`` or ``precision_rounding``, - not both! Here the precision (``10**-precision_digits`` or - ``precision_rounding``) is used as the zero *epsilon*: values smaller - than that are considered to be zero. + not both! - Warning: ``float_is_zero(value1-value2)`` is not always equivalent to + Warning: ``float_is_zero(value1-value2)`` is not equivalent to ``float_compare(value1,value2) == 0``, as the former will round after computing the difference, while the latter will round before, giving different results for e.g. 0.006 and 0.002 at 2 digits precision. - :param int precision_digits: number of decimal digits to round to. + :param int precision_digits: number of fractional digits to round to. :param float precision_rounding: decimal number representing the minimum non-zero value at the desired precision (for example, 0.01 for a 2-digit precision). - :param float value: value to compare with currency's zero - :return: True if ``value`` is considered 0 + :param float value: value to compare with the precision's zero + :return: True if ``value`` is considered zero """ epsilon = _float_check_precision(precision_digits=precision_digits, precision_rounding=precision_rounding) @@ -129,16 +106,16 @@ def float_compare(value1, value2, precision_digits=None, precision_rounding=None Example: 1.432 and 1.431 are equal at 2 digits precision, so this method would return 0 - However 0.006 and 0.002 are considered different (method returns 1) because - they respectively round to 0.01 and 0.0, even though 0.006-0.002 = 0.004 - which would be considered zero at 2 digits precision. + However 0.006 and 0.002 are considered different (this method returns 1) + because they respectively round to 0.01 and 0.0, even though + 0.006-0.002 = 0.004 which would be considered zero at 2 digits precision. - Warning: ``float_is_zero(value1-value2)`` is not always equivalent to + Warning: ``float_is_zero(value1-value2)`` is not equivalent to ``float_compare(value1,value2) == 0``, as the former will round after computing the difference, while the latter will round before, giving different results for e.g. 0.006 and 0.002 at 2 digits precision. - :param int precision_digits: number of decimal digits to round to. + :param int precision_digits: number of fractional digits to round to. :param float precision_rounding: decimal number representing the minimum non-zero value at the desired precision (for example, 0.01 for a 2-digit precision). @@ -155,8 +132,20 @@ def float_compare(value1, value2, precision_digits=None, precision_rounding=None if float_is_zero(delta, precision_rounding=rounding_factor): return 0 return -1 if delta < 0.0 else 1 +def float_repr(value, precision_digits): + """Returns a string representation of a float with the + the given number of fractional digits. This should not be + used to perform a rounding operation (this is done via + :meth:`~.float_round`), but only to produce a suitable + string representation for a float. - + :param int precision_digits: number of fractional digits to + include in the output + """ + # Can't use str() here because it seems to have an intrisic + # rounding to 12 significant digits, which causes a loss of + # precision. e.g. str(123456789.1234) == str(123456789.123)!! + return ("%%.%sf" % precision_digits) % value if __name__ == "__main__": @@ -168,20 +157,21 @@ if __name__ == "__main__": def try_round(amount, expected, precision_digits=3): global count, errors; count += 1 - result = float_round(amount, precision_digits=precision_digits) - if str(result) != expected: + result = float_repr(float_round(amount, precision_digits=precision_digits), + precision_digits=precision_digits) + if result != expected: errors += 1 - print '###!!! Rounding error: got %s or %s, expected %s' % (str(result), repr(result), expected) + print '###!!! Rounding error: got %s , expected %s' % (result, expected) # Extended float range test, inspired by Cloves Almeida's test on bug #882036. fractions = [.0, .015, .01499, .675, .67499, .4555, .4555, .45555] - expecteds = ['.0', '.02', '.01', '.68', '.67', '.46', '.456', '.4556'] + expecteds = ['.00', '.02', '.01', '.68', '.67', '.46', '.456', '.4556'] precisions = [2, 2, 2, 2, 2, 2, 3, 4] - for magnitude in range(5): + for magnitude in range(7): for i in xrange(len(fractions)): frac, exp, prec = fractions[i], expecteds[i], precisions[i] for sign in [-1,1]: - for x in xrange(0,10000,17): + for x in xrange(0,10000,97): n = x * 10**magnitude f = sign * (n + frac) f_exp = ('-' if f != 0 and sign == -1 else '') + str(n) + exp From e410b81cc4adba82e5619849da802ecb7b3e44ec Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 21 Dec 2011 02:11:00 +0100 Subject: [PATCH 35/75] [FIX] fields.float: always round floats when persisting Added test to try a full round-trip to the database as well to make sure the automatic rounding is working properly. lp bug: https://launchpad.net/bugs/882036 fixed bzr revid: odo@openerp.com-20111221011100-gybt9pjdfpg4wc86 --- openerp/addons/base/test/base_test.yml | 20 ++++++++++++++++++++ openerp/osv/fields.py | 17 +++++++++++------ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/openerp/addons/base/test/base_test.yml b/openerp/addons/base/test/base_test.yml index fcead834d93..450719af046 100644 --- a/openerp/addons/base/test/base_test.yml +++ b/openerp/addons/base/test/base_test.yml @@ -267,6 +267,26 @@ try_round(457.5, '460.00', precision_rounding=5) try_round(457.1, '456.00', precision_rounding=3) +- + "Float precision tests: check that proper rounding is performed for float persistence" +- + !python {model: res.currency}: | + currency = self.browse(cr, uid, ref('base.EUR')) + res_currency_rate = self.pool.get('res.currency.rate') + from tools import float_compare, float_is_zero, float_round, float_repr + def try_roundtrip(value, expected, self=self, cr=cr, currency=currency, + res_currency_rate=res_currency_rate): + rate_id = res_currency_rate.create(cr, 1, {'name':'2000-01-01', + 'rate': value, + 'currency_id': currency.id}) + rate = res_currency_rate.read(cr, 1, rate_id, ['rate'])['rate'] + assert rate == expected, 'Roundtrip error: got %s back from db, expected %s' % (rate, expected) + # res.currency.rate uses 6 digits of precision by default + try_roundtrip(2.6748955, 2.674896) + try_roundtrip(-2.6748955, -2.674896) + try_roundtrip(10000.999999, 10000.999999) + try_roundtrip(-10000.999999, -10000.999999) + - "Float precision tests: verify that invalid parameters are forbidden" - diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index ea1b6d93329..5a40eaff5fa 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -239,8 +239,10 @@ class float(_column): def digits_change(self, cr): if self.digits_compute: self.digits = self.digits_compute(cr) + if self.digits: precision, scale = self.digits - self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0), precision_digits=scale), + self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0), + precision_digits=scale), precision_digits=scale)) class date(_column): @@ -992,11 +994,14 @@ class function(_column): self._symbol_set = integer._symbol_set def digits_change(self, cr): - if self.digits_compute: - self.digits = self.digits_compute(cr) - precision, scale = self.digits - self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0), precision_digits=scale), - precision_digits=scale)) + if self._type == 'float': + if self.digits_compute: + self.digits = self.digits_compute(cr) + if self.digits: + precision, scale = self.digits + self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0), + precision_digits=scale), + precision_digits=scale)) def search(self, cr, uid, obj, name, args, context=None): if not self._fnct_search: From 84a517a6d7520ffc1ead1fe58f29e4993bf8891f Mon Sep 17 00:00:00 2001 From: "Rucha (Open ERP)" Date: Wed, 21 Dec 2011 14:38:11 +0530 Subject: [PATCH 36/75] [IMP]: Removed pageSize from report rml as it will be taken from company paper format; bzr revid: rpa@tinyerp.com-20111221090811-tg1vbfqs303f8yvq --- addons/account/project/report/analytic_balance.rml | 2 +- addons/account/project/report/analytic_journal.rml | 2 +- addons/account/project/report/cost_ledger.rml | 2 +- addons/account/project/report/inverted_analytic_balance.rml | 2 +- addons/account/project/report/quantity_cost_ledger.rml | 2 +- addons/account/report/account_balance.rml | 2 +- addons/account/report/account_central_journal.rml | 2 +- addons/account/report/account_financial_report.rml | 2 +- addons/account/report/account_general_journal.rml | 2 +- addons/account/report/account_general_ledger.rml | 2 +- addons/account/report/account_journal.rml | 2 +- addons/account/report/account_partner_balance.rml | 2 +- addons/account/report/account_partner_ledger.rml | 2 +- addons/account/report/account_partner_ledger_other.rml | 2 +- addons/account/report/account_print_invoice.rml | 2 +- addons/account/report/account_print_overdue.rml | 2 +- addons/account/report/account_tax_report.rml | 2 +- addons/account_analytic_plans/report/crossovered_analytic.rml | 2 +- addons/account_budget/report/analytic_account_budget_report.rml | 2 +- addons/account_budget/report/budget_report.rml | 2 +- addons/account_budget/report/crossovered_budget_report.rml | 2 +- addons/account_followup/report/account_followup_print.rml | 2 +- .../report/report_account_invoice_layout.rml | 2 +- .../account_invoice_layout/report/special_message_invoice.rml | 2 +- addons/account_payment/report/order.rml | 2 +- addons/account_voucher/report/account_voucher.rml | 2 +- addons/account_voucher/report/account_voucher_print.rml | 2 +- addons/auction/report/auction_artists.rml | 2 +- addons/auction/report/auction_bids.rml | 2 +- addons/auction/report/auction_buyer_result.rml | 2 +- addons/auction/report/auction_catelog.rml | 2 +- addons/auction/report/auction_objects.rml | 2 +- addons/auction/report/auction_result.rml | 2 +- addons/auction/report/auction_total.rml | 2 +- addons/auction/report/auction_total_rml.rml | 2 +- addons/auction/report/bids_lots.rml | 2 +- addons/auction/report/bids_phones_details.rml | 2 +- addons/auction/report/buyer_form_report.rml | 2 +- addons/auction/report/buyer_list.rml | 2 +- addons/auction/report/catelogwithpictures.rml | 2 +- addons/auction/report/deposit_seller.rml | 2 +- addons/auction/report/lots_list.rml | 2 +- addons/auction/report/lots_list_inventory.rml | 2 +- addons/auction/report/lots_list_landscape.rml | 2 +- addons/auction/report/seller_form_report.rml | 2 +- addons/base_module_doc_rst/report/ir_module_reference_graph.rml | 2 +- addons/delivery/report/shipping.rml | 2 +- addons/hr_attendance/report/attendance_errors.rml | 2 +- addons/hr_expense/report/expense.rml | 2 +- addons/hr_payroll/report/report_contribution_register.rml | 2 +- addons/hr_payroll/report/report_payslip.rml | 2 +- addons/hr_payroll/report/report_payslip_details.rml | 2 +- addons/hr_timesheet_invoice/report/account_analytic_profit.rml | 2 +- addons/hr_timesheet_invoice/report/cost_ledger.rml | 2 +- addons/l10n_fr/report/bilan_report.rml | 2 +- addons/l10n_fr/report/compute_resultant_report.rml | 2 +- addons/lunch/report/order.rml | 2 +- addons/marketing_campaign_crm_demo/crm_lead_mc_demo_report.rml | 2 +- addons/mrp/report/bom_structure.rml | 2 +- addons/mrp/report/order.rml | 2 +- addons/mrp_repair/report/order.rml | 2 +- addons/point_of_sale/report/account_statement.rml | 2 +- addons/point_of_sale/report/all_closed_cashbox_of_the_day.rml | 2 +- addons/point_of_sale/report/pos_details.rml | 2 +- addons/point_of_sale/report/pos_details_summary.rml | 2 +- addons/point_of_sale/report/pos_lines.rml | 2 +- addons/point_of_sale/report/pos_payment_report.rml | 2 +- addons/point_of_sale/report/pos_payment_report_user.rml | 2 +- addons/point_of_sale/report/pos_sales_user.rml | 2 +- addons/point_of_sale/report/pos_sales_user_today.rml | 2 +- addons/point_of_sale/report/pos_users_product.rml | 2 +- addons/product/report/product_pricelist.rml | 2 +- addons/project_planning/report/report_planning.rml | 2 +- addons/purchase/report/order.rml | 2 +- addons/purchase/report/request_quotation.rml | 2 +- addons/purchase_requisition/report/purchase_requisition.rml | 2 +- addons/report_intrastat/report/invoice.rml | 2 +- addons/sale/report/sale_order.rml | 2 +- addons/sale_layout/report/report_sale_layout.rml | 2 +- addons/stock/report/lot_overview.rml | 2 +- addons/stock/report/lot_overview_all.rml | 2 +- addons/stock/report/picking.rml | 2 +- addons/stock/report/stock_inventory_move.rml | 2 +- 83 files changed, 83 insertions(+), 83 deletions(-) diff --git a/addons/account/project/report/analytic_balance.rml b/addons/account/project/report/analytic_balance.rml index 77182b5bb9f..0c488eac400 100644 --- a/addons/account/project/report/analytic_balance.rml +++ b/addons/account/project/report/analytic_balance.rml @@ -1,6 +1,6 @@ -