[FIX] translations: fix tests to adapt to new duplication behaviour and remove contex...
[odoo/odoo.git] / openerp / osv / fields.py
index fdbd6a6..da6dd72 100644 (file)
@@ -27,6 +27,9 @@
     Fields Attributes:
         * _classic_read: is a classic sql fields
         * _type   : field type
+        * _auto_join: for one2many and many2one fields, tells whether select
+            queries will join the relational table instead of replacing the
+            field condition by an equivalent-one based on a search.
         * readonly
         * required
         * size
@@ -44,12 +47,14 @@ import openerp
 import openerp.tools as tools
 from openerp.tools.translate import _
 from openerp.tools import float_round, float_repr
+from openerp.tools import html_sanitize
 import simplejson
+from openerp import SUPERUSER_ID
 
 _logger = logging.getLogger(__name__)
 
 def _symbol_set(symb):
-    if symb == None or symb == False:
+    if symb is None or symb == False:
         return None
     elif isinstance(symb, unicode):
         return symb.encode('utf-8')
@@ -65,6 +70,7 @@ class _column(object):
     """
     _classic_read = True
     _classic_write = True
+    _auto_join = False
     _prefetch = True
     _properties = False
     _type = 'unknown'
@@ -108,10 +114,11 @@ class _column(object):
         self.manual = manual
         self.selectable = True
         self.group_operator = args.get('group_operator', False)
+        self.groups = False  # CSV list of ext IDs of groups that can access this field
+        self.deprecated = False # Optional deprecation warning
         for a in args:
-            if args[a]:
-                setattr(self, a, args[a])
-
+            setattr(self, a, args[a])
     def restart(self):
         pass
 
@@ -126,6 +133,23 @@ class _column(object):
         res = obj.read(cr, uid, ids, [name], context=context)
         return [x[name] for x in res]
 
+    def as_display_name(self, cr, uid, obj, value, context=None):
+        """Converts a field value to a suitable string representation for a record,
+           e.g. when this field is used as ``rec_name``.
+
+           :param obj: the ``BaseModel`` instance this column belongs to 
+           :param value: a proper value as returned by :py:meth:`~openerp.orm.osv.BaseModel.read`
+                         for this column
+        """
+        # delegated to class method, so a column type A can delegate
+        # to a column type B. 
+        return self._as_display_name(self, cr, uid, obj, value, context=None)
+
+    @classmethod
+    def _as_display_name(cls, field, cr, uid, obj, value, context=None):
+        # This needs to be a class method, in case a column type A as to delegate
+        # to a column type B.
+        return tools.ustr(value)
 
 # ---------------------------------------------------------
 # Simple fields
@@ -142,7 +166,7 @@ class boolean(_column):
             _logger.debug(
                 "required=True is deprecated: making a boolean field"
                 " `required` has no effect, as NULL values are "
-                "automatically turned into False.")
+                "automatically turned into False. args: %r",args)
 
 class integer(_column):
     _type = 'integer'
@@ -153,37 +177,6 @@ class integer(_column):
 
     def __init__(self, string='unknown', required=False, **args):
         super(integer, self).__init__(string=string, required=required, **args)
-        if required:
-            _logger.debug(
-                "required=True is deprecated: making an integer field"
-                " `required` has no effect, as NULL values are "
-                "automatically turned into 0.")
-
-class integer_big(_column):
-    """Experimental 64 bit integer column type, currently unused.
-
-       TODO: this field should work fine for values up
-             to 32 bits, but greater values will not fit
-             in the XML-RPC int type, so a specific
-             get() method is needed to pass them as floats,
-             like what we do for integer functional fields.
-    """
-    _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
-    _deprecated = True
-
-    def __init__(self, string='unknown', required=False, **args):
-        super(integer_big, self).__init__(string=string, required=required, **args)
-        if required:
-            _logger.debug(
-                "required=True is deprecated: making an integer_big field"
-                " `required` has no effect, as NULL values are "
-                "automatically turned into 0.")
 
 class reference(_column):
     _type = 'reference'
@@ -203,32 +196,55 @@ class reference(_column):
                     result[value['id']] = False
         return result
 
-class char(_column):
-    _type = 'char'
+    @classmethod
+    def _as_display_name(cls, field, cr, uid, obj, value, context=None):
+        if value:
+            # reference fields have a 'model,id'-like value, that we need to convert
+            # to a real name
+            model_name, res_id = value.split(',')
+            model = obj.pool.get(model_name)
+            if model and res_id:
+                return model.name_get(cr, uid, [int(res_id)], context=context)[0][1]
+        return tools.ustr(value)
+
+# takes a string (encoded in utf8) and returns a string (encoded in utf8)
+def _symbol_set_char(self, symb):
+
+    #TODO:
+    # * we need to remove the "symb==False" from the next line BUT
+    #   for now too many things rely on this broken behavior
+    # * the symb==None test should be common to all data types
+    if symb is None or symb == False:
+        return None
 
-    def __init__(self, string, size, **args):
-        _column.__init__(self, string=string, size=size, **args)
-        self._symbol_set = (self._symbol_c, self._symbol_set_char)
-
-    # takes a string (encoded in utf8) and returns a string (encoded in utf8)
-    def _symbol_set_char(self, symb):
-        #TODO:
-        # * we need to remove the "symb==False" from the next line BUT
-        #   for now too many things rely on this broken behavior
-        # * the symb==None test should be common to all data types
-        if symb == None or symb == False:
-            return None
+    # we need to convert the string to a unicode object to be able
+    # to evaluate its length (and possibly truncate it) reliably
+    u_symb = tools.ustr(symb)
+    return u_symb[:self.size].encode('utf8')
 
-        # we need to convert the string to a unicode object to be able
-        # to evaluate its length (and possibly truncate it) reliably
-        u_symb = tools.ustr(symb)
+class char(_column):
+    _type = 'char'
 
-        return u_symb[:self.size].encode('utf8')
+    def __init__(self, string="unknown", size=None, **args):
+        _column.__init__(self, string=string, size=size or None, **args)
+        # self._symbol_set_char defined to keep the backward compatibility
+        self._symbol_f = self._symbol_set_char = lambda x: _symbol_set_char(self, x)
+        self._symbol_set = (self._symbol_c, self._symbol_f)
 
 
 class text(_column):
     _type = 'text'
 
+class html(text):
+    _type = 'html'
+    _symbol_c = '%s'
+    def _symbol_f(x):
+        if x is None or x == False:
+            return None
+        return html_sanitize(x)
+        
+    _symbol_set = (_symbol_c, _symbol_f)
+
 import __builtin__
 
 class float(_column):
@@ -243,11 +259,6 @@ class float(_column):
         self.digits = digits
         # synopsis: digits_compute(cr) ->  (precision, scale)
         self.digits_compute = digits_compute
-        if required:
-            _logger.debug(
-                "required=True is deprecated: making a float field"
-                " `required` has no effect, as NULL values are "
-                "automatically turned into 0.0.")
 
     def digits_change(self, cr):
         if self.digits_compute:
@@ -279,8 +290,8 @@ class date(_column):
            This method may be passed as value to initialize _defaults.
 
            :param Model model: model (osv) for which the date value is being
-                               computed - technical field, currently ignored,
-                               automatically passed when used in _defaults.
+                               computed - automatically passed when used in
+                                _defaults.
            :param datetime timestamp: optional datetime value to use instead of
                                       the current date and time (must be a
                                       datetime, regular dates can't be converted
@@ -293,9 +304,13 @@ class date(_column):
         today = timestamp or DT.datetime.now()
         context_today = None
         if context and context.get('tz'):
+            tz_name = context['tz']  
+        else:
+            tz_name = model.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
+        if tz_name:
             try:
                 utc = pytz.timezone('UTC')
-                context_tz = pytz.timezone(context['tz'])
+                context_tz = pytz.timezone(tz_name)
                 utc_today = utc.localize(today, is_dst=False) # UTC = no DST
                 context_today = utc_today.astimezone(context_tz)
             except Exception:
@@ -336,9 +351,14 @@ class datetime(_column):
         """
         assert isinstance(timestamp, DT.datetime), 'Datetime instance expected'
         if context and context.get('tz'):
+            tz_name = context['tz']  
+        else:
+            registry = openerp.modules.registry.RegistryManager.get(cr.dbname)
+            tz_name = registry.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
+        if tz_name:
             try:
                 utc = pytz.timezone('UTC')
-                context_tz = pytz.timezone(context['tz'])
+                context_tz = pytz.timezone(tz_name)
                 utc_timestamp = utc.localize(timestamp, is_dst=False) # UTC = no DST
                 return utc_timestamp.astimezone(context_tz)
             except Exception:
@@ -347,24 +367,18 @@ class datetime(_column):
                               exc_info=True)
         return timestamp
 
-class time(_column):
-    _type = 'time'
-    _deprecated = True
-    @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'
     _symbol_c = '%s'
-    _symbol_f = lambda symb: symb and Binary(symb) or None
+
+    # Binary values may be byte strings (python 2.6 byte array), but
+    # the legacy OpenERP convention is to transfer and store binaries
+    # as base64-encoded strings. The base64 string may be provided as a
+    # unicode in some circumstances, hence the str() cast in symbol_f.
+    # This str coercion will only work for pure ASCII unicode strings,
+    # on purpose - non base64 data must be passed as a 8bit byte strings.
+    _symbol_f = lambda symb: symb and Binary(str(symb)) or None
+
     _symbol_set = (_symbol_c, _symbol_f)
     _symbol_get = lambda self, x: x and str(x)
 
@@ -418,34 +432,6 @@ class selection(_column):
 #         (4, ID)                link
 #         (5)                    unlink all (only valid for one2many)
 #
-#CHECKME: dans la pratique c'est quoi la syntaxe utilisee pour le 5? (5) ou (5, 0)?
-class one2one(_column):
-    _classic_read = False
-    _classic_write = True
-    _type = 'one2one'
-    _deprecated = True
-
-    def __init__(self, obj, string='unknown', **args):
-        _logger.warning("The one2one field is deprecated and doesn't work anymore.")
-        _column.__init__(self, string=string, **args)
-        self._obj = obj
-
-    def set(self, cr, obj_src, id, field, act, user=None, context=None):
-        if not context:
-            context = {}
-        obj = obj_src.pool.get(self._obj)
-        self._table = obj_src.pool.get(self._obj)._table
-        if act[0] == 0:
-            id_new = obj.create(cr, user, act[1])
-            cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (id_new, id))
-        else:
-            cr.execute('select '+field+' from '+obj_src._table+' where id=%s', (act[0],))
-            id = cr.fetchone()[0]
-            obj.write(cr, user, [id], act[1], context=context)
-
-    def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
-        return obj.pool.get(self._obj).search(cr, uid, args+self._domain+[('name', 'like', value)], offset, limit, context=context)
-
 
 class many2one(_column):
     _classic_read = False
@@ -455,9 +441,10 @@ class many2one(_column):
     _symbol_f = lambda x: x or None
     _symbol_set = (_symbol_c, _symbol_f)
 
-    def __init__(self, obj, string='unknown', **args):
+    def __init__(self, obj, string='unknown', auto_join=False, **args):
         _column.__init__(self, string=string, **args)
         self._obj = obj
+        self._auto_join = auto_join
 
     def get(self, cr, obj, ids, name, user=None, context=None, values=None):
         if context is None:
@@ -475,7 +462,7 @@ class many2one(_column):
         # build a dictionary of the form {'id_of_distant_resource': name_of_distant_resource}
         # 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,
+        records = dict(obj.name_get(cr, SUPERUSER_ID,
                                     list(set([x for x in res.values() if isinstance(x, (int,long))])),
                                     context=context))
         for id in res:
@@ -512,6 +499,11 @@ class many2one(_column):
     def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
         return obj.pool.get(self._obj).search(cr, uid, args+self._domain+[('name', 'like', value)], offset, limit, context=context)
 
+    
+    @classmethod
+    def _as_display_name(cls, field, cr, uid, obj, value, context=None):
+        return value[1] if isinstance(value, tuple) else tools.ustr(value) 
+
 
 class one2many(_column):
     _classic_read = False
@@ -519,11 +511,12 @@ class one2many(_column):
     _prefetch = False
     _type = 'one2many'
 
-    def __init__(self, obj, fields_id, string='unknown', limit=None, **args):
+    def __init__(self, obj, fields_id, string='unknown', limit=None, auto_join=False, **args):
         _column.__init__(self, string=string, **args)
         self._obj = obj
         self._fields_id = fields_id
         self._limit = limit
+        self._auto_join = auto_join
         #one2many can't be used as condition for defaults
         assert(self.change_default != True)
 
@@ -540,7 +533,8 @@ class one2many(_column):
         for id in ids:
             res[id] = []
 
-        ids2 = obj.pool.get(self._obj).search(cr, user, self._domain + [(self._fields_id, 'in', ids)], limit=self._limit, context=context)
+        domain = self._domain(obj) if callable(self._domain) else self._domain
+        ids2 = obj.pool.get(self._obj).search(cr, user, 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'):
             if r[self._fields_id] in res:
                 res[r[self._fields_id]].append(r['id'])
@@ -582,7 +576,8 @@ class one2many(_column):
                 reverse_rel = obj._all_columns.get(self._fields_id)
                 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
                 # if the o2m has a static domain we must respect it when unlinking
-                extra_domain = self._domain if isinstance(getattr(self, '_domain', None), list) else [] 
+                domain = self._domain(obj) if callable(self._domain) else self._domain
+                extra_domain = domain or []
                 ids_to_unlink = obj.search(cr, user, [(self._fields_id,'=',id)] + extra_domain, context=context)
                 # If the model has cascade deletion, we delete the rows because it is the intended behavior,
                 # otherwise we only nullify the reverse foreign key column.
@@ -600,8 +595,13 @@ class one2many(_column):
         return result
 
     def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
-        return obj.pool.get(self._obj).name_search(cr, uid, value, self._domain, operator, context=context,limit=limit)
+        domain = self._domain(obj) if callable(self._domain) else self._domain
+        return obj.pool.get(self._obj).name_search(cr, uid, value, domain, operator, context=context,limit=limit)
 
+    
+    @classmethod
+    def _as_display_name(cls, field, cr, uid, obj, value, context=None):
+        raise NotImplementedError('One2Many columns should not be used as record name (_rec_name)') 
 
 #
 # Values: (0, 0,  { fields })    create
@@ -678,7 +678,21 @@ class many2many(_column):
                 col1 = '%s_id' % source_model._table
             if not col2:
                 col2 = '%s_id' % dest_model._table
-        return (tbl, col1, col2)
+        return tbl, col1, col2
+
+    def _get_query_and_where_params(self, cr, model, ids, values, where_params):
+        """ Extracted from ``get`` to facilitate fine-tuning of the generated
+            query. """
+        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' \
+                 % values
+        return query, where_params
 
     def get(self, cr, model, ids, name, user=None, offset=0, context=None, values=None):
         if not context:
@@ -708,24 +722,13 @@ class many2many(_column):
         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 = ''
+        order_by = ' ORDER BY "%s".%s' %(obj._table, obj._order.split(',')[0])
 
         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': rel,
+        query, where_params = self._get_query_and_where_params(cr, model, ids, {'rel': rel,
                'from_c': from_c,
                'tbl': obj._table,
                'id1': id1,
@@ -734,7 +737,8 @@ class many2many(_column):
                'limit': limit_str,
                'order_by': order_by,
                'offset': offset,
-              }
+                }, where_params)
+
         cr.execute(query, [tuple(ids),] + where_params)
         for r in cr.fetchall():
             res[r[1]].append(r[0])
@@ -784,6 +788,10 @@ class many2many(_column):
     def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
         return obj.pool.get(self._obj).search(cr, uid, args+self._domain+[('name', operator, value)], offset, limit, context=context)
 
+    @classmethod
+    def _as_display_name(cls, field, cr, uid, obj, value, context=None):
+        raise NotImplementedError('Many2Many columns should not be used as record name (_rec_name)') 
+
 
 def get_nice_size(value):
     size = 0
@@ -1060,6 +1068,8 @@ class function(_column):
             self._classic_write = True
             if type=='binary':
                 self._symbol_get=lambda x:x and str(x)
+            else:
+                self._prefetch = True
 
         if type == 'float':
             self._symbol_c = float._symbol_c
@@ -1071,11 +1081,16 @@ class function(_column):
             self._symbol_f = boolean._symbol_f
             self._symbol_set = boolean._symbol_set
 
-        if type in ['integer','integer_big']:
+        if type == 'integer':
             self._symbol_c = integer._symbol_c
             self._symbol_f = integer._symbol_f
             self._symbol_set = integer._symbol_set
 
+        if type == 'char':
+            self._symbol_c = char._symbol_c
+            self._symbol_f = lambda x: _symbol_set_char(self, x)
+            self._symbol_set = (self._symbol_c, self._symbol_f)
+
     def digits_change(self, cr):
         if self._type == 'float':
             if self.digits_compute:
@@ -1111,13 +1126,13 @@ class function(_column):
             elif not context.get('bin_raw'):
                 result = sanitize_binary_value(value)
 
-        if field_type in ("integer","integer_big") and value > xmlrpclib.MAXINT:
+        if field_type == "integer" and value > xmlrpclib.MAXINT:
             # integer/long values greater than 2^31-1 are not supported
             # in pure XMLRPC, so we have to pass them as floats :-(
             # This is not needed for stored fields and non-functional integer
             # fields, as their values are constrained by the database backend
             # to the same 32bits signed int limit.
-            result = float(value)
+            result = __builtin__.float(value)
         return result
 
     def get(self, cr, obj, ids, name, uid=False, context=None, values=None):
@@ -1137,6 +1152,12 @@ class function(_column):
         if self._fnct_inv:
             self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
 
+    @classmethod
+    def _as_display_name(cls, field, cr, uid, obj, value, context=None):
+        # Function fields are supposed to emulate a basic field type,
+        # so they can delegate to the basic type for record name rendering
+        return globals()[field._type]._as_display_name(field, cr, uid, obj, value, context=context)
+
 # ---------------------------------------------------------
 # Related fields
 # ---------------------------------------------------------
@@ -1159,80 +1180,45 @@ class related(function):
         return map(lambda x: (field, x[1], x[2]), domain)
 
     def _fnct_write(self,obj,cr, uid, ids, field_name, values, args, context=None):
-        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 = 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)
+        if isinstance(ids, (int, long)):
+            ids = [ids]
+        for record in obj.browse(cr, uid, ids, context=context):
+            # traverse all fields except the last one
+            for field in self.arg[:-1]:
+                record = record[field] or False
+                if not record:
+                    break
+                elif isinstance(record, list):
+                    # record is the result of a one2many or many2many field
+                    record = record[0]
+            if record:
+                # write on the last field
+                record.write({self.arg[-1]: values})
 
     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
-        if self._type in ('one2many', 'many2many'):
-            res = dict([(i, []) for i in ids])
-        else:
-            res = {}.fromkeys(ids, False)
-
-        objlst = obj.browse(cr, 1, ids, context=context)
-        for data in objlst:
-            if not data:
-                continue
-            t_data = data
-            relation = obj._name
-            for i in range(len(self.arg)):
-                field_detail = self._relations[i]
-                relation = field_detail['object']
-                try:
-                    if not t_data[self.arg[i]]:
-                        t_data = False
-                        break
-                except:
-                    t_data = False
+        res = {}
+        for record in obj.browse(cr, SUPERUSER_ID, ids, context=context):
+            value = record
+            for field in self.arg:
+                if isinstance(value, list):
+                    value = value[0]
+                value = value[field] or False
+                if not value:
                     break
-                if field_detail['type'] in ('one2many', 'many2many') and i != len(self.arg) - 1:
-                    t_data = t_data[self.arg[i]][0]
-                elif t_data:
-                    t_data = t_data[self.arg[i]]
-            if type(t_data) == type(objlst[0]):
-                res[data.id] = t_data.id
-            elif t_data:
-                res[data.id] = t_data
-        if self._type=='many2one':
-            ids = filter(None, res.values())
-            if ids:
-                # name_get as root, as seeing the name of a related
-                # object depends on access right of source document,
-                # not target, so user may not have access.
-                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]])
+            res[record.id] = value
+
+        if self._type == 'many2one':
+            # res[id] is a browse_record or False; convert it to (id, name) or False.
+            # Perform name_get as root, as seeing the name of a related object depends on
+            # access right of source document, not target, so user may not have access.
+            value_ids = list(set(value.id for value in res.itervalues() if value))
+            value_name = dict(obj.pool.get(self._obj).name_get(cr, SUPERUSER_ID, value_ids, context=context))
+            res = dict((id, value and (value.id, value_name[value.id])) for id, value in res.iteritems())
+
         elif self._type in ('one2many', 'many2many'):
-            for r in res:
-                if res[r]:
-                    res[r] = [x.id for x in res[r]]
+            # res[id] is a list of browse_record or False; convert it to a list of ids
+            res = dict((id, value and map(int, value) or []) for id, value in res.iteritems())
+
         return res
 
     def __init__(self, *arg, **args):
@@ -1243,23 +1229,6 @@ class related(function):
             # TODO: improve here to change self.store = {...} according to related objects
             pass
 
-    def _field_get2(self, cr, uid, obj, context=None):
-        if self._relations:
-            return
-        result = []
-        obj_name = obj._name
-        for i in range(len(self._arg)):
-            f = obj.pool.get(obj_name).fields_get(cr, uid, [self._arg[i]], context=context)[self._arg[i]]
-            result.append({
-                'object': obj_name,
-                'type': f['type']
-
-            })
-            if f.get('relation',False):
-                obj_name = f['relation']
-                result[-1]['relation'] = f['relation']
-        self._relations = result
-
 
 class sparse(function):   
 
@@ -1352,7 +1321,7 @@ class sparse(function):
 
     def __init__(self, serialization_field, **kwargs):
         self.serialization_field = serialization_field
-        return super(sparse, self).__init__(self._fnct_read, fnct_inv=self._fnct_write, multi='__sparse_multi', **kwargs)
+        super(sparse, self).__init__(self._fnct_read, fnct_inv=self._fnct_write, multi='__sparse_multi', **kwargs)
      
 
 
@@ -1491,7 +1460,7 @@ class property(function):
                 # not target, so user may not have access) in order to avoid
                 # pointing on an unexisting record.
                 if property_destination_obj:
-                    if res[id][prop_name] and obj.pool.get(property_destination_obj).exists(cr, 1, res[id][prop_name].id):
+                    if res[id][prop_name] and obj.pool.get(property_destination_obj).exists(cr, SUPERUSER_ID, res[id][prop_name].id):
                         name_get_ids[id] = res[id][prop_name].id
                     else:
                         res[id][prop_name] = False
@@ -1499,7 +1468,7 @@ class property(function):
                 # name_get as root (as seeing the name of a related
                 # object depends on access right of source document,
                 # not target, so user may not have access.)
-                name_get_values = dict(obj.pool.get(property_destination_obj).name_get(cr, 1, name_get_ids.values(), context=context))
+                name_get_values = dict(obj.pool.get(property_destination_obj).name_get(cr, SUPERUSER_ID, name_get_ids.values(), context=context))
                 # the property field is a m2o, we need to return a tuple with (id, name)
                 for k, v in name_get_ids.iteritems():
                     if res[k][prop_name]:
@@ -1535,9 +1504,7 @@ def field_to_dict(model, cr, user, field, context=None):
     """
 
     res = {'type': field._type}
-    # This additional attributes for M2M and function field is added
-    # because we need to display tooltip with this additional information
-    # when client is started in debug mode.
+    # some attributes for m2m/function field are added as debug info only
     if isinstance(field, function):
         res['function'] = field._fnct and field._fnct.func_name or False
         res['store'] = field.store
@@ -1546,33 +1513,25 @@ def field_to_dict(model, cr, user, field, context=None):
         res['fnct_search'] = field._fnct_search and field._fnct_search.func_name or False
         res['fnct_inv'] = field._fnct_inv and field._fnct_inv.func_name or False
         res['fnct_inv_arg'] = field._fnct_inv_arg or False
-        res['func_obj'] = field._obj or False
     if isinstance(field, many2many):
         (table, col1, col2) = field._sql_names(model)
-        res['related_columns'] = [col1, col2]
-        res['third_table'] = table
-    for arg in ('string', 'readonly', 'states', 'size', 'required', 'group_operator',
-            'change_default', 'translate', 'help', 'select', 'selectable'):
-        if getattr(field, arg):
-            res[arg] = getattr(field, arg)
-    for arg in ('digits', 'invisible', 'filters'):
+        res['m2m_join_columns'] = [col1, col2]
+        res['m2m_join_table'] = table
+    for arg in ('string', 'readonly', 'states', 'size', 'group_operator', 'required',
+            'change_default', 'translate', 'help', 'select', 'selectable', 'groups',
+            'deprecated', 'digits', 'invisible', 'filters'):
         if getattr(field, arg, None):
             res[arg] = getattr(field, arg)
 
-    if field.string:
-        res['string'] = field.string
-    if field.help:
-        res['help'] = field.help
-
     if hasattr(field, 'selection'):
         if isinstance(field.selection, (tuple, list)):
             res['selection'] = field.selection
         else:
             # call the 'dynamic selection' function
             res['selection'] = field.selection(model, cr, user, context)
-    if res['type'] in ('one2many', 'many2many', 'many2one', 'one2one'):
+    if res['type'] in ('one2many', 'many2many', 'many2one'):
         res['relation'] = field._obj
-        res['domain'] = field._domain
+        res['domain'] = field._domain(model) if callable(field._domain) else field._domain
         res['context'] = field._context
 
     if isinstance(field, one2many):
@@ -1582,19 +1541,32 @@ def field_to_dict(model, cr, user, field, context=None):
 
 
 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.
+    """ Struct containing details about an osv column, either one local to
+        its model, or one inherited via _inherits.
+
+        .. attribute:: name
+
+            name of the column
+
+        .. attribute:: column
+
+            column instance, subclass of :class:`_column`
+
+        .. attribute:: parent_model
+
+            if the column is inherited, name of the model that contains it,
+            ``None`` for local columns.
+
+        .. attribute:: parent_column
+
+            the name of the column containing the m2o relationship to the
+            parent model that contains this column, ``None`` for local columns.
+
+        .. attribute:: original_parent
+
+            if the column is inherited, name of the original parent model that
+            contains it i.e in case of multilevel inheritance, ``None`` for
+            local columns.
     """
     def __init__(self, name, column, parent_model=None, parent_column=None, original_parent=None):
         self.name = name
@@ -1603,5 +1575,10 @@ class column_info(object):
         self.parent_column = parent_column
         self.original_parent = original_parent
 
+    def __str__(self):
+        return '%s(%s, %s, %s, %s, %s)' % (
+            self.__class__.__name__, self.name, self.column,
+            self.parent_model, self.parent_column, self.original_parent)
+
 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: