[FIX] account: fiscal position may map single tax to multiple taxes
[odoo/odoo.git] / openerp / osv / fields.py
index 372d7e4..9506a5c 100644 (file)
 
 import base64
 import datetime as DT
+import functools
 import logging
 import pytz
 import re
 import xmlrpclib
+from operator import itemgetter
 from psycopg2 import Binary
 
 import openerp
@@ -80,10 +82,30 @@ class _column(object):
     _symbol_f = _symbol_set
     _symbol_set = (_symbol_c, _symbol_f)
     _symbol_get = None
-
-    # used to hide a certain field type in the list of field types
     _deprecated = False
 
+    copy = True                 # whether value is copied by BaseModel.copy()
+    string = None
+    help = ""
+    required = False
+    readonly = False
+    _domain = []
+    _context = {}
+    states = None
+    priority = 0
+    change_default = False
+    size = None
+    ondelete = None
+    translate = False
+    select = False
+    manual = False
+    write = False
+    read = False
+    selectable = True
+    group_operator = False
+    groups = False              # CSV list of ext IDs of groups
+    deprecated = False          # Optional deprecation warning
+
     def __init__(self, string='unknown', required=False, readonly=False, domain=None, context=None, states=None, priority=0, change_default=False, size=None, ondelete=None, translate=False, select=False, manual=False, **args):
         """
 
@@ -91,34 +113,79 @@ class _column(object):
         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
-        self.required = required
-        self.size = size
-        self.help = args.get('help', '')
-        self.priority = priority
-        self.change_default = change_default
-        self.ondelete = ondelete.lower() if ondelete else None # defaults to 'set null' in ORM
-        self.translate = translate
-        self._domain = domain
-        self._context = context
-        self.write = False
-        self.read = False
-        self.select = select
-        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])
+        args0 = {
+            'string': string,
+            'required': required,
+            'readonly': readonly,
+            '_domain': domain,
+            '_context': context,
+            'states': states,
+            'priority': priority,
+            'change_default': change_default,
+            'size': size,
+            'ondelete': ondelete.lower() if ondelete else None,
+            'translate': translate,
+            'select': select,
+            'manual': manual,
+        }
+        for key, val in args0.iteritems():
+            if val:
+                setattr(self, key, val)
+
+        self._args = args
+        for key, val in args.iteritems():
+            setattr(self, key, val)
+
+        # prefetch only if self._classic_write, not self.groups, and not
+        # self.deprecated
+        if not self._classic_write or self.deprecated:
+            self._prefetch = False
+
+    def new(self, **args):
+        """ return a column like `self` with the given parameters """
+        # memory optimization: reuse self whenever possible; you can reduce the
+        # average memory usage per registry by 10 megabytes!
+        return self if self.same_parameters(args) else type(self)(**args)
+
+    def same_parameters(self, args):
+        dummy = object()
+        return all(
+            # either both are falsy, or they are equal
+            (not val1 and not val) or (val1 == val)
+            for key, val in args.iteritems()
+            for val1 in [getattr(self, key, getattr(self, '_' + key, dummy))]
+        )
+
+    def to_field(self):
+        """ convert column `self` to a new-style field """
+        from openerp.fields import Field
+        return Field.by_type[self._type](**self.to_field_args())
+
+    def to_field_args(self):
+        """ return a dictionary with all the arguments to pass to the field """
+        base_items = [
+            ('column', self),                   # field interfaces self
+            ('copy', self.copy),
+        ]
+        truthy_items = filter(itemgetter(1), [
+            ('index', self.select),
+            ('manual', self.manual),
+            ('string', self.string),
+            ('help', self.help),
+            ('readonly', self.readonly),
+            ('required', self.required),
+            ('states', self.states),
+            ('groups', self.groups),
+            ('change_default', self.change_default),
+            ('deprecated', self.deprecated),
+            ('size', self.size),
+            ('ondelete', self.ondelete),
+            ('translate', self.translate),
+            ('domain', self._domain),
+            ('context', self._context),
+        ])
+        return dict(base_items + truthy_items + self._args.items())
+
     def restart(self):
         pass
 
@@ -157,7 +224,7 @@ class _column(object):
 class boolean(_column):
     _type = 'boolean'
     _symbol_c = '%s'
-    _symbol_f = lambda x: x and 'True' or 'False'
+    _symbol_f = bool
     _symbol_set = (_symbol_c, _symbol_f)
 
     def __init__(self, string='unknown', required=False, **args):
@@ -183,8 +250,16 @@ class reference(_column):
     _classic_read = False # post-process to handle missing target
 
     def __init__(self, string, selection, size=None, **args):
+        if callable(selection):
+            from openerp import api
+            selection = api.expected(api.cr_uid_context, selection)
         _column.__init__(self, string=string, size=size, selection=selection, **args)
 
+    def to_field_args(self):
+        args = super(reference, self).to_field_args()
+        args['selection'] = self.selection
+        return args
+
     def get(self, cr, obj, ids, name, uid=None, context=None, values=None):
         result = {}
         # copy initial values fetched previously.
@@ -204,44 +279,60 @@ class reference(_column):
             model_name, res_id = value.split(',')
             if model_name in obj.pool and res_id:
                 model = obj.pool[model_name]
-                return model.name_get(cr, uid, [int(res_id)], context=context)[0][1]
+                names = model.name_get(cr, uid, [int(res_id)], context=context)
+                return names[0][1] if names else False
         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
+
+    # 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')
+
 class char(_column):
     _type = 'char'
 
     def __init__(self, string="unknown", size=None, **args):
         _column.__init__(self, string=string, size=size or None, **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 is 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')
-
+        # 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:
+
+    def _symbol_set_html(self, value):
+        if value is None or value is False:
             return None
-        return html_sanitize(x)
-        
-    _symbol_set = (_symbol_c, _symbol_f)
+        if not self._sanitize:
+            return value
+        return html_sanitize(value)
+
+    def __init__(self, string='unknown', sanitize=True, **args):
+        super(html, self).__init__(string=string, **args)
+        self._sanitize = sanitize
+        # symbol_set redefinition because of sanitize specific behavior
+        self._symbol_f = self._symbol_set_html
+        self._symbol_set = (self._symbol_c, self._symbol_f)
+
+    def to_field_args(self):
+        args = super(html, self).to_field_args()
+        args['sanitize'] = self._sanitize
+        return args
 
 import __builtin__
 
@@ -258,6 +349,15 @@ class float(_column):
         # synopsis: digits_compute(cr) ->  (precision, scale)
         self.digits_compute = digits_compute
 
+    def new(self, **args):
+        # float columns are database-dependent, so always recreate them
+        return type(self)(**args)
+
+    def to_field_args(self):
+        args = super(float, self).to_field_args()
+        args['digits'] = self.digits_compute or self.digits
+        return args
+
     def digits_change(self, cr):
         if self.digits_compute:
             self.digits = self.digits_compute(cr)
@@ -319,7 +419,8 @@ class date(_column):
         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']
+            user = model.pool['res.users'].browse(cr, SUPERUSER_ID, uid)
+            tz_name = user.tz
         if tz_name:
             try:
                 utc = pytz.timezone('UTC')
@@ -332,6 +433,30 @@ class date(_column):
                               exc_info=True)
         return (context_today or today).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
 
+    @staticmethod
+    def date_to_datetime(model, cr, uid, userdate, context=None):
+        """ Convert date values expressed in user's timezone to
+        server-side UTC timestamp, assuming a default arbitrary
+        time of 12:00 AM - because a time is needed.
+
+        :param str userdate: date string in in user time zone
+        :return: UTC datetime string for server-side use
+        """
+        user_date = DT.datetime.strptime(userdate, tools.DEFAULT_SERVER_DATE_FORMAT)
+        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:
+            utc = pytz.timezone('UTC')
+            context_tz = pytz.timezone(tz_name)
+            user_datetime = user_date + DT.timedelta(hours=12.0)
+            local_timestamp = context_tz.localize(user_datetime, is_dst=False)
+            user_datetime = local_timestamp.astimezone(utc)
+            return user_datetime.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
+        return user_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
+
+
 class datetime(_column):
     _type = 'datetime'
 
@@ -383,18 +508,18 @@ class datetime(_column):
             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']
+            user = registry['res.users'].browse(cr, SUPERUSER_ID, uid)
+            tz_name = user.tz
+        utc_timestamp = pytz.utc.localize(timestamp, is_dst=False) # UTC = no DST
         if tz_name:
             try:
-                utc = pytz.timezone('UTC')
                 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:
                 _logger.debug("failed to compute context/client-specific timestamp, "
                               "using the UTC value",
                               exc_info=True)
-        return timestamp
+        return utc_timestamp
 
 class binary(_column):
     _type = 'binary'
@@ -446,9 +571,50 @@ class selection(_column):
     _type = 'selection'
 
     def __init__(self, selection, string='unknown', **args):
+        if callable(selection):
+            from openerp import api
+            selection = api.expected(api.cr_uid_context, selection)
         _column.__init__(self, string=string, **args)
         self.selection = selection
 
+    def to_field_args(self):
+        args = super(selection, self).to_field_args()
+        args['selection'] = self.selection
+        return args
+
+    @classmethod
+    def reify(cls, cr, uid, model, field, context=None):
+        """ Munges the field's ``selection`` attribute as necessary to get
+        something useable out of it: calls it if it's a function, applies
+        translations to labels if it's not.
+
+        A callable ``selection`` is considered translated on its own.
+
+        :param orm.Model model:
+        :param _column field:
+        """
+        if callable(field.selection):
+            return field.selection(model, cr, uid, context)
+
+        if not (context and 'lang' in context):
+            return field.selection
+
+        # field_to_dict isn't given a field name, only a field object, we
+        # need to get the name back in order to perform the translation lookup
+        field_name = next(
+            name for name, column in model._columns.iteritems()
+            if column == field)
+
+        translation_filter = "%s,%s" % (model._name, field_name)
+        translate = functools.partial(
+            model.pool['ir.translation']._get_source,
+            cr, uid, translation_filter, 'selection', context['lang'])
+
+        return [
+            (value, translate(label))
+            for value, label in field.selection
+        ]
+
 # ---------------------------------------------------------
 # Relationals fields
 # ---------------------------------------------------------
@@ -470,36 +636,18 @@ class many2one(_column):
     _symbol_f = lambda x: x or None
     _symbol_set = (_symbol_c, _symbol_f)
 
+    ondelete = 'set null'
+
     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:
-            context = {}
-        if values is None:
-            values = {}
-
-        res = {}
-        for r in values:
-            res[r['id']] = r[name]
-        for id in ids:
-            res.setdefault(id, '')
-        obj = obj.pool[self._obj]
-
-        # 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, SUPERUSER_ID,
-                                    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[id] = False
-        return res
+    def to_field_args(self):
+        args = super(many2one, self).to_field_args()
+        args['comodel_name'] = self._obj
+        args['auto_join'] = self._auto_join
+        return args
 
     def set(self, cr, obj_src, id, field, values, user=None, context=None):
         if not context:
@@ -528,7 +676,6 @@ class many2one(_column):
     def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
         return obj.pool[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) 
@@ -540,6 +687,9 @@ class one2many(_column):
     _prefetch = False
     _type = 'one2many'
 
+    # one2many columns are not copied by default
+    copy = False
+
     def __init__(self, obj, fields_id, string='unknown', limit=None, auto_join=False, **args):
         _column.__init__(self, string=string, **args)
         self._obj = obj
@@ -549,35 +699,39 @@ class one2many(_column):
         #one2many can't be used as condition for defaults
         assert(self.change_default != True)
 
+    def to_field_args(self):
+        args = super(one2many, self).to_field_args()
+        args['comodel_name'] = self._obj
+        args['inverse_name'] = self._fields_id
+        args['auto_join'] = self._auto_join
+        args['limit'] = self._limit
+        return args
+
     def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
-        if context is None:
-            context = {}
         if self._context:
-            context = context.copy()
-        context.update(self._context)
-        if values is None:
-            values = {}
-
-        res = {}
-        for id in ids:
-            res[id] = []
+            context = dict(context or {})
+            context.update(self._context)
 
+        # retrieve the records in the comodel
+        comodel = obj.pool[self._obj].browse(cr, user, [], context)
+        inverse = self._fields_id
         domain = self._domain(obj) if callable(self._domain) else self._domain
-        model = obj.pool[self._obj]
-        ids2 = model.search(cr, user, domain + [(self._fields_id, 'in', ids)], limit=self._limit, context=context)
-        for r in model._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'])
-        return res
+        domain = domain + [(inverse, 'in', ids)]
+        records = comodel.search(domain, limit=self._limit)
+
+        result = {id: [] for id in ids}
+        # read the inverse of records without prefetching other fields on them
+        for record in records.with_context(prefetch_fields=False):
+            # record[inverse] may be a record or an integer
+            result[int(record[inverse])].append(record.id)
+
+        return result
 
     def set(self, cr, obj, id, field, values, user=None, context=None):
         result = []
-        if not context:
-            context = {}
-        if self._context:
-            context = context.copy()
+        context = dict(context or {})
         context.update(self._context)
-        context['no_store_function'] = True
+        context['recompute'] = False    # recomputation is done by outer create/write
         if not values:
             return
         obj = obj.pool[self._obj]
@@ -600,8 +754,13 @@ class one2many(_column):
                 else:
                     cr.execute('update '+_table+' set '+self._fields_id+'=null where id=%s', (act[1],))
             elif act[0] == 4:
-                # Must use write() to recompute parent_store structure if needed
-                obj.write(cr, user, [act[1]], {self._fields_id:id}, context=context or {})
+                # table of the field (parent_model in case of inherit)
+                field_model = self._fields_id in obj.pool[self._obj]._columns and self._obj or obj.pool[self._obj]._all_columns[self._fields_id].parent_model
+                field_table = obj.pool[field_model]._table
+                cr.execute("select 1 from {0} where id=%s and {1}=%s".format(field_table, self._fields_id), (act[1], id))
+                if not cr.fetchone():
+                    # Must use write() to recompute parent_store structure if needed and check access rules
+                    obj.write(cr, user, [act[1]], {self._fields_id:id}, context=context or {})
             elif act[0] == 5:
                 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'
@@ -628,7 +787,6 @@ class one2many(_column):
         domain = self._domain(obj) if callable(self._domain) else self._domain
         return obj.pool[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)') 
@@ -687,6 +845,15 @@ class many2many(_column):
         self._id2 = id2
         self._limit = limit
 
+    def to_field_args(self):
+        args = super(many2many, self).to_field_args()
+        args['comodel_name'] = self._obj
+        args['relation'] = self._rel
+        args['column1'] = self._id1
+        args['column2'] = self._id2
+        args['limit'] = self._limit
+        return args
+
     def _sql_names(self, source_model):
         """Return the SQL names defining the structure of the m2m relationship table
 
@@ -1065,6 +1232,9 @@ class function(_column):
     _type = 'function'
     _properties = True
 
+    # function fields are not copied by default
+    copy = False
+
 #
 # multi: compute several fields in one call
 #
@@ -1080,6 +1250,9 @@ class function(_column):
 
         self.digits = args.get('digits', (16,2))
         self.digits_compute = args.get('digits_compute', None)
+        if callable(args.get('selection')):
+            from openerp import api
+            self.selection = api.expected(api.cr_uid_context, args['selection'])
 
         self._fnct_inv_arg = fnct_inv_arg
         if not fnct_inv:
@@ -1101,20 +1274,31 @@ class function(_column):
             else:
                 self._prefetch = True
 
-        if type == 'float':
-            self._symbol_c = float._symbol_c
-            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 == '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)
+        else:
+            type_class = globals().get(type)
+            if type_class is not None:
+                self._symbol_c = type_class._symbol_c
+                self._symbol_f = type_class._symbol_f
+                self._symbol_set = type_class._symbol_set
+
+    def new(self, **args):
+        # HACK: function fields are tricky to recreate, simply return a copy
+        import copy
+        return copy.copy(self)
+
+    def to_field_args(self):
+        args = super(function, self).to_field_args()
+        if self._type in ('float',):
+            args['digits'] = self.digits_compute or self.digits
+        elif self._type in ('selection', 'reference'):
+            args['selection'] = self.selection
+        elif self._type in ('many2one', 'one2many', 'many2many'):
+            args['comodel_name'] = self._obj
+        return args
 
     def digits_change(self, cr):
         if self._type == 'float':
@@ -1133,42 +1317,57 @@ class function(_column):
         return self._fnct_search(obj, cr, uid, obj, name, args, context=context)
 
     def postprocess(self, cr, uid, obj, field, value=None, context=None):
+        return self._postprocess_batch(cr, uid, obj, field, {0: value}, context=context)[0]
+
+    def _postprocess_batch(self, cr, uid, obj, field, values, context=None):
+        if not values:
+            return values
+
         if context is None:
             context = {}
-        result = value
+
         field_type = obj._columns[field]._type
-        if field_type == "many2one":
-            # make the result a tuple if it is not already one
-            if isinstance(value, (int,long)) and hasattr(obj._columns[field], 'relation'):
-                obj_model = obj.pool[obj._columns[field].relation]
-                dict_names = dict(obj_model.name_get(cr, uid, [value], context))
-                result = (value, dict_names[value])
+        new_values = dict(values)
 
         if field_type == 'binary':
             if context.get('bin_size'):
                 # client requests only the size of binary fields
-                result = get_nice_size(value)
+                for rid, value in values.iteritems():
+                    if value:
+                        new_values[rid] = get_nice_size(value)
             elif not context.get('bin_raw'):
-                result = sanitize_binary_value(value)
-
-        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 = __builtin__.float(value)
-        return result
+                for rid, value in values.iteritems():
+                    if value:
+                        new_values[rid] = sanitize_binary_value(value)
+
+        return new_values
 
     def get(self, cr, obj, ids, name, uid=False, context=None, values=None):
-        result = self._fnct(obj, cr, uid, ids, name, self._arg, context)
-        for id in ids:
-            if self._multi and id in result:
-                for field, value in result[id].iteritems():
-                    if value:
-                        result[id][field] = self.postprocess(cr, uid, obj, field, value, context)
-            elif result.get(id):
-                result[id] = self.postprocess(cr, uid, obj, name, result[id], context)
+        multi = self._multi
+        # if we already have a value, don't recompute it.
+        # This happen if case of stored many2one fields
+        if values and not multi and name in values[0]:
+            result = dict((v['id'], v[name]) for v in values)
+        elif values and multi and all(n in values[0] for n in name):
+            result = dict((v['id'], dict((n, v[n]) for n in name)) for v in values)
+        else:
+            result = self._fnct(obj, cr, uid, ids, name, self._arg, context)
+        if multi:
+            swap = {}
+            for rid, values in result.iteritems():
+                for f, v in values.iteritems():
+                    if f not in name:
+                        continue
+                    swap.setdefault(f, {})[rid] = v
+
+            for field, values in swap.iteritems():
+                new_values = self._postprocess_batch(cr, uid, obj, field, values, context)
+                for rid, value in new_values.iteritems():
+                    result[rid][field] = value
+
+        else:
+            result = self._postprocess_batch(cr, uid, obj, name, result, context)
+
         return result
 
     def set(self, cr, obj, id, name, value, user=None, context=None):
@@ -1204,45 +1403,38 @@ class related(function):
         field = '.'.join(self._arg)
         return map(lambda x: (field, x[1], x[2]), domain)
 
-    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):
         if isinstance(ids, (int, long)):
             ids = [ids]
-        for record in obj.browse(cr, uid, ids, context=context):
+        for instance 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})
+                instance = instance[field][:1]
+            if instance:
+                # write on the last field of the target record
+                instance.write({self.arg[-1]: values})
 
     def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
         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
-            res[record.id] = value
+            # traverse all fields except the last one
+            for field in self.arg[:-1]:
+                value = value[field][:1]
+            # read the last field on the target record
+            res[record.id] = value[self.arg[-1]]
 
         if self._type == 'many2one':
-            # res[id] is a browse_record or False; convert it to (id, name) or False.
+            # res[id] is a recordset; 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[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())
+            res = dict((id, bool(value) and (value.id, value_name[value.id])) for id, value in res.iteritems())
 
         elif self._type in ('one2many', 'many2many'):
-            # 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())
+            # res[id] is a recordset; convert it to a list of ids
+            res = dict((id, value.ids) for id, value in res.iteritems())
 
         return res
 
@@ -1367,7 +1559,7 @@ class dummy(function):
     def __init__(self, *arg, **args):
         self.arg = arg
         self._relations = []
-        super(dummy, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=None, **args)
+        super(dummy, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
 
 # ---------------------------------------------------------
 # Serialized fields
@@ -1396,177 +1588,58 @@ class serialized(_column):
 # TODO: review completly this class for speed improvement
 class property(function):
 
-    def _get_default(self, obj, cr, uid, prop_name, context=None):
-        return self._get_defaults(obj, cr, uid, [prop_name], context=None)[prop_name]
+    def to_field_args(self):
+        args = super(property, self).to_field_args()
+        args['company_dependent'] = True
+        return args
 
-    def _get_defaults(self, obj, cr, uid, prop_names, context=None):
-        """Get the default values for ``prop_names´´ property fields (result of ir.property.get() function for res_id = False).
-
-           :param list of string prop_names: list of name of property fields for those we want the default value
-           :return: map of property field names to their default value
-           :rtype: dict
-        """
-        prop = obj.pool.get('ir.property')
-        res = {}
-        for prop_name in prop_names:
-            res[prop_name] = prop.get(cr, uid, prop_name, obj._name, context=context)
-        return res
-
-    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]
-        def_id = self._field_get(cr, uid, obj._name, prop_name[0])
-        company = obj.pool.get('res.company')
-        cid = company._company_default_get(cr, uid, obj._name, def_id, context=context)
-        domain = [('fields_id.model', '=', obj._name), ('fields_id.name', 'in', prop_name), ('company_id', '=', cid)]
-        #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 = {}
+    def _fnct_search(self, tobj, cr, uid, obj, name, domain, context=None):
+        ir_property = obj.pool['ir.property']
+        result = []
+        for field, operator, value in domain:
+            result += ir_property.search_multi(cr, uid, name, tobj._name, operator, value, context=context)
+        return result
 
-        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, openerp.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': id_val,
-                'res_id': obj._name+','+str(id),
-                'company_id': cid,
-                'fields_id': def_id,
-                'type': self._type,
-            }, context=context)
-        return False
+    def _fnct_write(self, obj, cr, uid, id, prop_name, value, obj_dest, context=None):
+        ir_property = obj.pool['ir.property']
+        ir_property.set_multi(cr, uid, prop_name, obj._name, {id: value}, context=context)
+        return True
 
     def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
-        prop = obj.pool.get('ir.property')
-        # get the default values (for res_id = False) for the property fields
-        default_val = self._get_defaults(obj, cr, uid, prop_names, context)
-
-        # build the dictionary that will be returned
-        res = {}
-        for id in ids:
-            res[id] = default_val.copy()
+        ir_property = obj.pool['ir.property']
 
+        res = {id: {} for id in ids}
         for prop_name in prop_names:
-            property_field = obj._all_columns.get(prop_name).column
-            property_destination_obj = property_field._obj if property_field._type == 'many2one' else False
-            # If the property field is a m2o field, we will append the id of the value to name_get_ids
-            # in order to make a name_get in batch for all the ids needed.
-            name_get_ids = {}
-            for id in ids:
-                # get the result of ir.property.get() for this res_id and save it in res if it's existing
-                obj_reference = obj._name + ',' + str(id)
-                value = prop.get(cr, uid, prop_name, obj._name, res_id=obj_reference, context=context)
-                if value:
+            column = obj._all_columns[prop_name].column
+            values = ir_property.get_multi(cr, uid, prop_name, obj._name, ids, context=context)
+            if column._type == 'many2one':
+                # name_get the non-null values as SUPERUSER_ID
+                vals = sum(set(filter(None, values.itervalues())),
+                           obj.pool[column._obj].browse(cr, uid, [], context=context))
+                vals_name = dict(vals.sudo().name_get()) if vals else {}
+                for id, value in values.iteritems():
+                    ng = False
+                    if value and value.id in vals_name:
+                        ng = value.id, vals_name[value.id]
+                    res[id][prop_name] = ng
+            else:
+                for id, value in values.iteritems():
                     res[id][prop_name] = value
-                # Check existence 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) in order to avoid
-                # pointing on an unexisting record.
-                if property_destination_obj:
-                    if res[id][prop_name] and obj.pool[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
-            if property_destination_obj:
-                # 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[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]:
-                        res[k][prop_name] = (v , name_get_values.get(v))
-        return res
-
-    def _field_get(self, cr, uid, model_name, prop):
-        if not self.field_id.get(cr.dbname):
-            cr.execute('SELECT id \
-                    FROM ir_model_fields \
-                    WHERE name=%s AND model=%s', (prop, model_name))
-            res = cr.fetchone()
-            self.field_id[cr.dbname] = res and res[0]
-        return self.field_id[cr.dbname]
 
+        return res
 
     def __init__(self, **args):
-        self.field_id = {}
         if 'view_load' in args:
             _logger.warning("view_load attribute is deprecated on ir.fields. Args: %r", args)
-        obj = 'relation' in args and args['relation'] or ''
-        function.__init__(self, self._fnct_read, False, self._fnct_write, obj=obj, multi='properties', **args)
-
-    def restart(self):
-        self.field_id = {}
-
-
-def field_to_dict(model, cr, user, field, context=None):
-    """ Return a dictionary representation of a field.
-
-    The string, help, and selection attributes (if any) are untranslated.  This
-    representation is the one returned by fields_get() (fields_get() will do
-    the translation).
-
-    """
-
-    res = {'type': field._type}
-    # 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
-        if isinstance(field.store, dict):
-            res['store'] = str(field.store)
-        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
-    if isinstance(field, many2many):
-        (table, col1, col2) = field._sql_names(model)
-        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 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'):
-        res['relation'] = field._obj
-        res['domain'] = field._domain(model) if callable(field._domain) else field._domain
-        res['context'] = field._context
-
-    if isinstance(field, one2many):
-        res['relation_field'] = field._fields_id
-
-    return res
+        args = dict(args)
+        args['obj'] = args.pop('relation', '') or args.get('obj', '')
+        super(property, self).__init__(
+            fnct=self._fnct_read,
+            fnct_inv=self._fnct_write,
+            fnct_search=self._fnct_search,
+            multi='properties',
+            **args
+        )
 
 
 class column_info(object):
@@ -1609,5 +1682,5 @@ class column_info(object):
             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:
 
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: