X-Git-Url: http://git.inspyration.org/?a=blobdiff_plain;f=bin%2Fosv%2Ffields.py;h=652a4819d48575812c65993d5a60ecd6fcd8ca00;hb=c6b96e6cd5140ea91e88bab9864d9a419afaa587;hp=35ff16674180e34d9eafcd2a476bd048f23a8b1e;hpb=3af6db6180cebedc4369d5958e8c35dde8af2ac2;p=odoo%2Fodoo.git diff --git a/bin/osv/fields.py b/bin/osv/fields.py index 35ff166..652a481 100644 --- a/bin/osv/fields.py +++ b/bin/osv/fields.py @@ -31,15 +31,17 @@ # required # size # +import datetime as DT import string -import netsvc import sys - -from psycopg2 import Binary import warnings +import xmlrpclib +from psycopg2 import Binary +import osv +import netsvc import tools - +from tools.translate import _ def _symbol_set(symb): if symb == None or symb == False: @@ -62,7 +64,17 @@ class _column(object): _symbol_set = (_symbol_c, _symbol_f) _symbol_get = None - def __init__(self, string='unknown', required=False, readonly=False, domain=None, context='', states=None, priority=0, change_default=False, size=None, ondelete="set null", translate=False, select=False, **args): + def __init__(self, string='unknown', required=False, readonly=False, domain=None, context=None, states=None, priority=0, change_default=False, size=None, ondelete="set null", translate=False, select=False, manual=False, **args): + """ + + The 'manual' keyword argument specifies if the field is a custom one. + It corresponds to the 'state' column in ir_model_fields. + + """ + if domain is None: + domain = [] + if context is None: + context = {} self.states = states or {} self.string = string self.readonly = readonly @@ -73,12 +85,13 @@ class _column(object): self.change_default = change_default self.ondelete = ondelete self.translate = translate - self._domain = domain or [] + self._domain = domain self._context = context self.write = False self.read = False self.view_load = 0 self.select = select + self.manual = manual self.selectable = True self.group_operator = args.get('group_operator', False) for a in args: @@ -118,26 +131,39 @@ class boolean(_column): _symbol_f = lambda x: x and 'True' or 'False' _symbol_set = (_symbol_c, _symbol_f) -class integer_big(_column): - _type = 'integer_big' +class integer(_column): + _type = 'integer' _symbol_c = '%s' _symbol_f = lambda x: int(x or 0) _symbol_set = (_symbol_c, _symbol_f) _symbol_get = lambda self,x: x or 0 -class integer(_column): - _type = 'integer' +class integer_big(_column): + _type = 'integer_big' + # do not reference the _symbol_* of integer class, as that would possibly + # unbind the lambda functions _symbol_c = '%s' _symbol_f = lambda x: int(x or 0) _symbol_set = (_symbol_c, _symbol_f) _symbol_get = lambda self,x: x or 0 - class reference(_column): _type = 'reference' + _classic_read = False def __init__(self, string, selection, size, **args): _column.__init__(self, string=string, size=size, selection=selection, **args) + def get(self, cr, obj, ids, name, uid=None, context=None, values=None): + result = {} + # copy initial values fetched previously. + for value in values: + result[value['id']] = value[name] + if value[name]: + model, res_id = value[name].split(',') + if not obj.pool.get(model).exists(cr, uid, [int(res_id)], context=context): + result[value['id']] = False + return result + class char(_column): _type = 'char' @@ -188,14 +214,42 @@ class float(_column): class date(_column): _type = 'date' + @staticmethod + def today(*args): + """ Returns the current date in a format fit for being a + default value to a ``date`` field. + This method should be provided as is to the _defaults dict, it + should not be called. + """ + return DT.date.today().strftime( + tools.DEFAULT_SERVER_DATE_FORMAT) class datetime(_column): _type = 'datetime' + @staticmethod + def now(*args): + """ Returns the current datetime in a format fit for being a + default value to a ``datetime`` field. + This method should be provided as is to the _defaults dict, it + should not be called. + """ + return DT.datetime.now().strftime( + tools.DEFAULT_SERVER_DATETIME_FORMAT) class time(_column): _type = 'time' + @staticmethod + def now( *args): + """ Returns the current time in a format fit for being a + default value to a ``time`` field. + + This method should be proivided as is to the _defaults dict, + it should not be called. + """ + return DT.datetime.now().strftime( + tools.DEFAULT_SERVER_TIME_FORMAT) class binary(_column): _type = 'binary' @@ -223,7 +277,13 @@ class binary(_column): if v['id'] == i: val = v[name] break - if context.get('bin_size', False) and val: + + # If client is requesting only the size of the field, we return it instead + # of the content. Presumably a separate request will be done to read the actual + # content if it's needed at some point. + # TODO: after 6.0 we should consider returning a dict with size and content instead of + # having an implicit convention for the value + if val and context.get('bin_size_%s' % name, context.get('bin_size')): res[i] = tools.human_size(long(val)) else: res[i] = val @@ -245,7 +305,7 @@ class selection(_column): # # Values: (0, 0, { fields }) create -# (1, ID, { fields }) modification +# (1, ID, { fields }) update # (2, ID) remove (delete) # (3, ID) unlink one (target id or target of relation) # (4, ID) link @@ -298,14 +358,15 @@ class many2one(_column): def get_memory(self, cr, obj, ids, name, user=None, context=None, values=None): result = {} for id in ids: - result[id] = obj.datas[id][name] + result[id] = obj.datas[id].get(name, False) return result def get(self, cr, obj, ids, name, user=None, context=None, values=None): - if not context: + if context is None: context = {} - if not values: + if values is None: values = {} + res = {} for r in values: res[r['id']] = r[name] @@ -314,21 +375,16 @@ class many2one(_column): obj = obj.pool.get(self._obj) # build a dictionary of the form {'id_of_distant_resource': name_of_distant_resource} - from orm import except_orm - names = {} - for record in list(set(filter(None, res.values()))): - try: - record_name = dict(obj.name_get(cr, user, [record], context)) - except except_orm: - record_name = {} - record_name[record] = '// Access Denied //' - names.update(record_name) - - for r in res.keys(): - if res[r] and res[r] in names: - res[r] = (res[r], names[res[r]]) + # we use uid=1 because the visibility of a many2one field value (just id and name) + # must be the access right of the parent form and not the linked object itself. + records = dict(obj.name_get(cr, 1, + list(set([x for x in res.values() if isinstance(x, (int,long))])), + context=context)) + for id in res: + if res[id] in records: + res[id] = (res[id], records[res[id]]) else: - res[r] = False + res[id] = False return res def set(self, cr, obj_src, id, field, values, user=None, context=None): @@ -374,7 +430,7 @@ class one2many(_column): assert(self.change_default != True) def get_memory(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None): - if not context: + if context is None: context = {} if self._context: context = context.copy() @@ -410,7 +466,7 @@ class one2many(_column): elif act[0] == 3: obj.datas[act[1]][self._fields_id] = False elif act[0] == 4: - obj.datas[act[1]] = id + obj.datas[act[1]][self._fields_id] = id elif act[0] == 5: for o in obj.datas.values(): if o[self._fields_id] == id: @@ -423,19 +479,22 @@ class one2many(_column): raise _('Not Implemented') def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None): - if not context: + if context is None: context = {} if self._context: context = context.copy() context.update(self._context) - if not values: + if values is None: values = {} + res = {} for id in ids: res[id] = [] - ids2 = obj.pool.get(self._obj).search(cr, user, [(self._fields_id, 'in', ids)], limit=self._limit, context=context) + + ids2 = obj.pool.get(self._obj).search(cr, user, self._domain + [(self._fields_id, 'in', ids)], limit=self._limit, context=context) for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'): - res[r[self._fields_id]].append(r['id']) + if r[self._fields_id] in res: + res[r[self._fields_id]].append(r['id']) return res def set(self, cr, obj, id, field, values, user=None, context=None): @@ -479,10 +538,10 @@ class one2many(_column): # # Values: (0, 0, { fields }) create -# (1, ID, { fields }) modification -# (2, ID) remove -# (3, ID) unlink -# (4, ID) link +# (1, ID, { fields }) update (write fields to ID) +# (2, ID) remove (calls unlink on ID, that will also delete the relationship because of the ondelete) +# (3, ID) unlink (delete the relationship between the two objects but does not delete ID) +# (4, ID) link (add a relationship) # (5, ID) unlink all # (6, ?, ids) set a list of links # @@ -491,7 +550,6 @@ class many2many(_column): _classic_write = False _prefetch = False _type = 'many2many' - def __init__(self, obj, rel, id1, id2, string='unknown', limit=None, **args): _column.__init__(self, string=string, **args) self._obj = obj @@ -513,20 +571,50 @@ class many2many(_column): return res for id in ids: res[id] = [] - limit_str = self._limit is not None and ' limit %d' % self._limit or '' + if offset: + warnings.warn("Specifying offset at a many2many.get() may produce unpredictable results.", + DeprecationWarning, stacklevel=2) obj = obj.pool.get(self._obj) - d1, d2, tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context) - if d1: - d1 = ' and ' + ' and '.join(d1) - else: d1 = '' - - cr.execute('SELECT '+self._rel+'.'+self._id2+','+self._rel+'.'+self._id1+' \ - FROM '+self._rel+' , '+(','.join(tables))+' \ - WHERE '+self._rel+'.'+self._id1+' = ANY (%s) \ - AND '+self._rel+'.'+self._id2+' = '+obj._table+'.id '+d1 - +limit_str+' order by '+obj._table+'.'+obj._order+' offset %s', - [ids,]+d2+[offset]) + # static domains are lists, and are evaluated both here and on client-side, while string + # domains supposed by dynamic and evaluated on client-side only (thus ignored here) + # FIXME: make this distinction explicit in API! + domain = isinstance(self._domain, list) and self._domain or [] + + wquery = obj._where_calc(cr, user, domain, context=context) + obj._apply_ir_rules(cr, user, wquery, 'read', context=context) + from_c, where_c, where_params = wquery.get_sql() + if where_c: + where_c = ' AND ' + where_c + + if offset or self._limit: + order_by = ' ORDER BY "%s".%s' %(obj._table, obj._order.split(',')[0]) + else: + order_by = '' + + limit_str = '' + if self._limit is not None: + limit_str = ' LIMIT %d' % self._limit + + query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \ + FROM %(rel)s, %(from_c)s \ + WHERE %(rel)s.%(id1)s IN %%s \ + AND %(rel)s.%(id2)s = %(tbl)s.id \ + %(where_c)s \ + %(order_by)s \ + %(limit)s \ + OFFSET %(offset)d' \ + % {'rel': self._rel, + 'from_c': from_c, + 'tbl': obj._table, + 'id1': self._id1, + 'id2': self._id2, + 'where_c': where_c, + 'limit': limit_str, + 'order_by': order_by, + 'offset': offset, + } + cr.execute(query, [tuple(ids),] + where_params) for r in cr.fetchall(): res[r[1]].append(r[0]) return res @@ -550,9 +638,12 @@ class many2many(_column): elif act[0] == 3: cr.execute('delete from '+self._rel+' where ' + self._id1 + '=%s and '+ self._id2 + '=%s', (id, act[1])) elif act[0] == 4: - cr.execute('insert into '+self._rel+' ('+self._id1+','+self._id2+') values (%s,%s)', (id, act[1])) + # following queries are in the same transaction - so should be relatively safe + cr.execute('SELECT 1 FROM '+self._rel+' WHERE '+self._id1+' = %s and '+self._id2+' = %s', (id, act[1])) + if not cr.fetchone(): + cr.execute('insert into '+self._rel+' ('+self._id1+','+self._id2+') values (%s,%s)', (id, act[1])) elif act[0] == 5: - cr.execute('update '+self._rel+' set '+self._id2+'=null where '+self._id2+'=%s', (id,)) + cr.execute('delete from '+self._rel+' where ' + self._id1 + ' = %s', (id,)) elif act[0] == 6: d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context) @@ -608,6 +699,37 @@ def get_nice_size(a): size = 0 return (x, tools.human_size(size)) +def sanitize_binary_value(dict_item): + # binary fields should be 7-bit ASCII base64-encoded data, + # but we do additional sanity checks to make sure the values + # are not something else that won't pass via xmlrpc + index, value = dict_item + if isinstance(value, (xmlrpclib.Binary, tuple, list, dict)): + # these builtin types are meant to pass untouched + return index, value + + # For all other cases, handle the value as a binary string: + # it could be a 7-bit ASCII string (e.g base64 data), but also + # any 8-bit content from files, with byte values that cannot + # be passed inside XML! + # See for more info: + # - http://bugs.python.org/issue10066 + # - http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char + # + # One solution is to convert the byte-string to unicode, + # so it gets serialized as utf-8 encoded data (always valid XML) + # If invalid XML byte values were present, tools.ustr() uses + # the Latin-1 codec as fallback, which converts any 8-bit + # byte value, resulting in valid utf-8-encoded bytes + # in the end: + # >>> unicode('\xe1','latin1').encode('utf8') == '\xc3\xa1' + # Note: when this happens, decoding on the other endpoint + # is not likely to produce the expected output, but this is + # just a safety mechanism (in these cases base64 data or + # xmlrpc.Binary values should be used instead) + return index, tools.ustr(value) + + # --------------------------------------------------------- # Function fields # --------------------------------------------------------- @@ -646,7 +768,9 @@ class function(_column): self.selectable = False if store: - self._classic_read = True + if self._type != 'many2one': + # m2o fields need to return tuples with name_get, not just foreign keys + self._classic_read = True self._classic_write = True if type=='binary': self._symbol_get=lambda x:x and str(x) @@ -656,6 +780,16 @@ class function(_column): self._symbol_f = float._symbol_f self._symbol_set = float._symbol_set + if type == 'boolean': + self._symbol_c = boolean._symbol_c + self._symbol_f = boolean._symbol_f + self._symbol_set = boolean._symbol_set + + if type in ['integer','integer_big']: + self._symbol_c = integer._symbol_c + self._symbol_f = integer._symbol_f + self._symbol_set = integer._symbol_set + def digits_change(self, cr): if self.digits_compute: t = self.digits_compute(cr) @@ -670,9 +804,9 @@ class function(_column): return self._fnct_search(obj, cr, uid, obj, name, args, context=context) def get(self, cr, obj, ids, name, user=None, context=None, values=None): - if not context: + if context is None: context = {} - if not values: + if values is None: values = {} res = {} if self._method: @@ -691,9 +825,13 @@ class function(_column): if res[r] and res[r] in dict_names: res[r] = (res[r], dict_names[res[r]]) - if self._type == 'binary' and context.get('bin_size', False): - # convert the data returned by the function with the size of that data... - res = dict(map( get_nice_size, res.items())) + if self._type == 'binary': + if context.get('bin_size', False): + # client requests only the size of binary fields + res = dict(map(get_nice_size, res.items())) + else: + res = dict(map(sanitize_binary_value, res.items())) + if self._type == "integer": for r in res.keys(): # Converting value into string so that it does not affect XML-RPC Limits @@ -718,7 +856,7 @@ class function(_column): class related(function): - def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context={}): + def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None): self._field_get2(cr, uid, obj, context) i = len(self._arg)-1 sarg = name @@ -735,46 +873,43 @@ class related(function): return [(self._arg[0], 'in', sarg)] def _fnct_write(self,obj,cr, uid, ids, field_name, values, args, context=None): - if values and field_name: - self._field_get2(cr, uid, obj, context) - relation = obj._name - res = {} - if type(ids) != type([]): - ids=[ids] - objlst = obj.browse(cr, uid, ids) - for data in objlst: - t_id=None - t_data = data - relation = obj._name - for i in range(len(self.arg)): - field_detail = self._relations[i] - relation = field_detail['object'] - if not t_data[self.arg[i]]: - if self._type not in ('one2many', 'many2many'): - t_id = t_data['id'] - t_data = False - break - if field_detail['type'] in ('one2many', 'many2many'): - if self._type != "many2one": - t_id = t_data.id - t_data = t_data[self.arg[i]][0] - else: - t_data = False - break - else: + self._field_get2(cr, uid, obj, context=context) + if type(ids) != type([]): + ids=[ids] + objlst = obj.browse(cr, uid, ids) + for data in objlst: + t_id = data.id + t_data = data + for i in range(len(self.arg)): + if not t_data: break + field_detail = self._relations[i] + if not t_data[self.arg[i]]: + if self._type not in ('one2many', 'many2many'): t_id = t_data['id'] - t_data = t_data[self.arg[i]] - - if t_id and t_data: - obj.pool.get(field_detail['object']).write(cr,uid,[t_id],{args[-1]:values}, context=context) + t_data = False + elif field_detail['type'] in ('one2many', 'many2many'): + if self._type != "many2one": + t_id = t_data.id + t_data = t_data[self.arg[i]][0] + else: + t_data = False + else: + t_id = t_data['id'] + t_data = t_data[self.arg[i]] + else: + model = obj.pool.get(self._relations[-1]['object']) + model.write(cr, uid, [t_id], {args[-1]: values}, context=context) def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None): self._field_get2(cr, uid, obj, context) if not ids: return {} relation = obj._name - res = {}.fromkeys(ids, False) + if self._type in ('one2many', 'many2many'): + res = dict([(i, []) for i in ids]) + else: + res = {}.fromkeys(ids, False) - objlst = obj.browse(cr, uid, ids, context=context) + objlst = obj.browse(cr, 1, ids, context=context) for data in objlst: if not data: continue @@ -792,16 +927,16 @@ class related(function): break if field_detail['type'] in ('one2many', 'many2many') and i != len(self.arg) - 1: t_data = t_data[self.arg[i]][0] - else: + elif t_data: t_data = t_data[self.arg[i]] if type(t_data) == type(objlst[0]): res[data.id] = t_data.id - else: + elif t_data: res[data.id] = t_data if self._type=='many2one': ids = filter(None, res.values()) if ids: - ng = dict(obj.pool.get(self._obj).name_get(cr, uid, ids, context=context)) + ng = dict(obj.pool.get(self._obj).name_get(cr, 1, ids, context=context)) for r in res: if res[r]: res[r] = (res[r], ng[res[r]]) @@ -819,7 +954,7 @@ class related(function): # TODO: improve here to change self.store = {...} according to related objects pass - def _field_get2(self, cr, uid, obj, context={}): + def _field_get2(self, cr, uid, obj, context=None): if self._relations: return obj_name = obj._name @@ -839,10 +974,10 @@ class related(function): # --------------------------------------------------------- class dummy(function): - def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context={}): + def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None): return [] - def _fnct_write(self,obj,cr, uid, ids, field_name, values, args, context=None): + def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None): return False def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None): @@ -866,89 +1001,109 @@ class serialized(_column): super(serialized, self).__init__(string=string, **args) +# TODO: review completly this class for speed improvement class property(function): - def _fnct_write(self, obj, cr, uid, id, prop, id_val, val, context=None): - if not context: + def _get_default(self, obj, cr, uid, prop_name, context=None): + return self._get_defaults(obj, cr, uid, [prop_name], context=None)[0][prop_name] + + def _get_defaults(self, obj, cr, uid, prop_name, context=None): + prop = obj.pool.get('ir.property') + domain = [('fields_id.model', '=', obj._name), ('fields_id.name','in',prop_name), ('res_id','=',False)] + ids = prop.search(cr, uid, domain, context=context) + replaces = {} + default_value = {}.fromkeys(prop_name, False) + for prop_rec in prop.browse(cr, uid, ids, context=context): + if default_value.get(prop_rec.fields_id.name, False): + continue + value = prop.get_by_record(cr, uid, prop_rec, context=context) or False + default_value[prop_rec.fields_id.name] = value + if value and (prop_rec.type == 'many2one'): + replaces.setdefault(value._name, {}) + replaces[value._name][value.id] = True + return default_value, replaces + + def _get_by_id(self, obj, cr, uid, prop_name, ids, context=None): + prop = obj.pool.get('ir.property') + vids = [obj._name + ',' + str(oid) for oid in ids] + + domain = [('fields_id.model', '=', obj._name), ('fields_id.name','in',prop_name)] + #domain = prop._get_domain(cr, uid, prop_name, obj._name, context) + if vids: + domain = [('res_id', 'in', vids)] + domain + return prop.search(cr, uid, domain, context=context) + + # TODO: to rewrite more clean + def _fnct_write(self, obj, cr, uid, id, prop_name, id_val, obj_dest, context=None): + if context is None: context = {} - (obj_dest,) = val - definition_id = self._field_get(cr, uid, obj._name, prop) - - property = obj.pool.get('ir.property') - nid = property.search(cr, uid, [('fields_id', '=', definition_id), - ('res_id', '=', obj._name+','+str(id))]) - while len(nid): - cr.execute('DELETE FROM ir_property WHERE id=%s', (nid.pop(),)) - - nid = property.search(cr, uid, [('fields_id', '=', definition_id), - ('res_id', '=', False)]) - default_val = False - if nid: - default_val = property.browse(cr, uid, nid[0], context).value - - company_id = obj.pool.get('res.company')._company_default_get(cr, uid, obj._name, prop, context=context) - res = False - if val[0]: - newval = (id_val and obj_dest+','+str(id_val)) or False - else: - newval = id_val or False - if (newval != default_val) and newval: - propdef = obj.pool.get('ir.model.fields').browse(cr, uid, - definition_id, context=context) - res = property.create(cr, uid, { + + nids = self._get_by_id(obj, cr, uid, [prop_name], [id], context) + if nids: + cr.execute('DELETE FROM ir_property WHERE id IN %s', (tuple(nids),)) + + default_val = self._get_default(obj, cr, uid, prop_name, context) + property_create = False + if isinstance(default_val, osv.orm.browse_record): + if default_val.id != id_val: + property_create = True + elif default_val != id_val: + property_create = True + + if property_create: + def_id = self._field_get(cr, uid, obj._name, prop_name) + company = obj.pool.get('res.company') + cid = company._company_default_get(cr, uid, obj._name, def_id, + context=context) + propdef = obj.pool.get('ir.model.fields').browse(cr, uid, def_id, + context=context) + prop = obj.pool.get('ir.property') + return prop.create(cr, uid, { 'name': propdef.name, - 'value': newval, + 'value': id_val, 'res_id': obj._name+','+str(id), - 'company_id': company_id, - 'fields_id': definition_id + 'company_id': cid, + 'fields_id': def_id, + 'type': self._type, }, context=context) - return res - - def _fnct_read(self, obj, cr, uid, ids, prop, val, context=None): - if not context: - context = {} - property = obj.pool.get('ir.property') - definition_id = self._field_get(cr, uid, obj._name, prop) + return False - nid = property.search(cr, uid, [('fields_id', '=', definition_id), - ('res_id', '=', False)]) - default_val = False - if nid: - d = property.browse(cr, uid, nid[0], context).value - default_val = (d and int(d.split(',')[1])) or False - vids = [obj._name + ',' + str(id) for id in ids] - nids = property.search(cr, uid, [('fields_id', '=', definition_id), - ('res_id', 'in', vids)]) + def _fnct_read(self, obj, cr, uid, ids, prop_name, obj_dest, context=None): + properties = obj.pool.get('ir.property') + domain = [('fields_id.model', '=', obj._name), ('fields_id.name','in',prop_name)] + domain += [('res_id','in', [obj._name + ',' + str(oid) for oid in ids])] + nids = properties.search(cr, uid, domain, context=context) + default_val,replaces = self._get_defaults(obj, cr, uid, prop_name, context) res = {} for id in ids: - res[id] = default_val - for prop in property.browse(cr, uid, nids): - if prop.value.find(',') >= 0: - res[int(prop.res_id.id)] = (prop.value and \ - int(prop.value.split(',')[1])) or False - else: - res[int(prop.res_id.id)] = prop.value or '' - - if self._obj: - obj = obj.pool.get(self._obj) - to_check = res.values() - if default_val and default_val not in to_check: - to_check += [default_val] - existing_ids = obj.search(cr, uid, [('id', 'in', to_check)]) - for id, res_id in res.items(): - if res_id not in existing_ids: - cr.execute('DELETE FROM ir_property WHERE value=%s', ((obj._name+','+str(res_id)),)) - res[id] = default_val - names = dict(obj.name_get(cr, uid, existing_ids, context)) - for r in res.keys(): - if res[r] and res[r] in names: - res[r] = (res[r], names[res[r]]) + res[id] = default_val.copy() + + brs = properties.browse(cr, uid, nids, context=context) + for prop in brs: + value = properties.get_by_record(cr, uid, prop, context=context) + res[prop.res_id.id][prop.fields_id.name] = value or False + if value and (prop.type == 'many2one'): + record_exists = obj.pool.get(value._name).exists(cr, uid, value.id) + if record_exists: + replaces.setdefault(value._name, {}) + replaces[value._name][value.id] = True else: - res[r] = False + res[prop.res_id.id][prop.fields_id.name] = False + + for rep in replaces: + nids = obj.pool.get(rep).search(cr, uid, [('id','in',replaces[rep].keys())], context=context) + replaces[rep] = dict(obj.pool.get(rep).name_get(cr, uid, nids, context=context)) + + for prop in prop_name: + for id in ids: + if res[id][prop] and hasattr(res[id][prop], '_name'): + res[id][prop] = (res[id][prop].id , replaces[res[id][prop]._name].get(res[id][prop].id, False)) + return res + def _field_get(self, cr, uid, model_name, prop): if not self.field_id.get(cr.dbname): cr.execute('SELECT id \ @@ -959,13 +1114,36 @@ class property(function): return self.field_id[cr.dbname] def __init__(self, obj_prop, **args): + # TODO remove obj_prop parameter (use many2one type) self.field_id = {} function.__init__(self, self._fnct_read, False, self._fnct_write, - (obj_prop, ), **args) + obj_prop, multi='properties', **args) def restart(self): self.field_id = {} +class column_info(object): + """Struct containing details about an osv column, either one local to + its model, or one inherited via _inherits. + + :attr name: name of the column + :attr column: column instance, subclass of osv.fields._column + :attr parent_model: if the column is inherited, name of the model + that contains it, None for local columns. + :attr parent_column: the name of the column containing the m2o + relationship to the parent model that contains + this column, None for local columns. + :attr original_parent: if the column is inherited, name of the original + parent model that contains it i.e in case of multilevel + inheritence, None for local columns. + """ + def __init__(self, name, column, parent_model=None, parent_column=None, original_parent=None): + self.name = name + self.column = column + self.parent_model = parent_model + self.parent_column = parent_column + self.original_parent = original_parent + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: