From 4fc1ffe10a6e0b5e1fb0da2f0c2fdc882477968f Mon Sep 17 00:00:00 2001 From: sebastien beau Date: Mon, 5 Sep 2011 01:43:04 +0200 Subject: [PATCH 1/6] [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 2/6] [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 3/6] [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 4/6] [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 5/6] [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 6/6] [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