reverted patch for 'opw 51026'
[odoo/odoo.git] / bin / osv / orm.py
index 50e0abd..d2386ba 100644 (file)
@@ -203,6 +203,8 @@ class browse_record(object):
             # read the results
             field_names = map(lambda x: x[0], fields_to_fetch)
             field_values = self._table.read(self._cr, self._uid, ids, field_names, context=self._context, load="_classic_write")
+
+            # TODO: improve this, very slow for reports
             if self._fields_process:
                 lang = self._context.get('lang', 'en_US') or 'en_US'
                 lang_obj_ids = self.pool.get('res.lang').search(self._cr, self._uid, [('code', '=', lang)])
@@ -238,6 +240,13 @@ class browse_record(object):
                                 #        testing to be sure we got the right
                                 #        object and not the parent one.
                                 if not isinstance(value, browse_record):
+                                    if obj is None:
+                                        # In some cases the target model is not available yet, so we must ignore it,
+                                        # which is safe in most cases, this value will just be loaded later when needed.
+                                        # This situation can be caused by custom fields that connect objects with m2o without
+                                        # respecting module dependencies, causing relationships to be connected to soon when
+                                        # the target is not loaded yet.
+                                        continue
                                     new_data[field_name] = browse_record(self._cr,
                                         self._uid, value, obj, self._cache,
                                         context=self._context,
@@ -270,7 +279,7 @@ class browse_record(object):
                 self._data[result_line['id']].update(new_data)
 
         if not name in self._data[self._id]:
-            #how did this happen?
+            # How did this happen? Could be a missing model due to custom fields used too soon, see above.
             self.logger.notifyChannel("browse_record", netsvc.LOG_ERROR,
                     "Fields to fetch: %s, Field values: %s"%(field_names, field_values))
             self.logger.notifyChannel("browse_record", netsvc.LOG_ERROR,
@@ -423,7 +432,7 @@ class orm_template(object):
             model_id = cr.fetchone()[0]
         if 'module' in context:
             name_id = 'model_'+self._name.replace('.', '_')
-            cr.execute('select * from ir_model_data where name=%s and res_id=%s and module=%s', (name_id, model_id, context['module']))
+            cr.execute('select * from ir_model_data where name=%s and module=%s', (name_id, context['module']))
             if not cr.rowcount:
                 cr.execute("INSERT INTO ir_model_data (name,date_init,date_update,module,model,res_id) VALUES (%s, now(), now(), %s, %s, %s)", \
                     (name_id, context['module'], 'ir.model', model_id)
@@ -449,8 +458,10 @@ class orm_template(object):
                 'readonly': (f.readonly and 1) or 0,
                 'required': (f.required and 1) or 0,
                 'selectable': (f.selectable and 1) or 0,
+                'translate': (f.translate and 1) or 0,
                 'relation_field': (f._type=='one2many' and isinstance(f, fields.one2many)) and f._fields_id or '',
             }
+
             # When its a custom field,it does not contain f.select
             if context.get('field_state', 'base') == 'manual':
                 if context.get('field_name', '') == k:
@@ -465,13 +476,13 @@ class orm_template(object):
                 vals['id'] = id
                 cr.execute("""INSERT INTO ir_model_fields (
                     id, model_id, model, name, field_description, ttype,
-                    relation,view_load,state,select_level,relation_field
+                    relation,view_load,state,select_level,relation_field, translate
                 ) VALUES (
-                    %s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s
+                    %s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s
                 )""", (
                     id, vals['model_id'], vals['model'], vals['name'], vals['field_description'], vals['ttype'],
                      vals['relation'], bool(vals['view_load']), 'base',
-                    vals['select_level'], vals['relation_field']
+                    vals['select_level'], vals['relation_field'], bool(vals['translate'])
                 ))
                 if 'module' in context:
                     name1 = 'field_' + self._table + '_' + k
@@ -488,12 +499,12 @@ class orm_template(object):
                         cr.commit()
                         cr.execute("""UPDATE ir_model_fields SET
                             model_id=%s, field_description=%s, ttype=%s, relation=%s,
-                            view_load=%s, select_level=%s, readonly=%s ,required=%s, selectable=%s, relation_field=%s
+                            view_load=%s, select_level=%s, readonly=%s ,required=%s, selectable=%s, relation_field=%s, translate=%s
                         WHERE
                             model=%s AND name=%s""", (
                                 vals['model_id'], vals['field_description'], vals['ttype'],
                                 vals['relation'], bool(vals['view_load']),
-                                vals['select_level'], bool(vals['readonly']), bool(vals['required']), bool(vals['selectable']), vals['relation_field'], vals['model'], vals['name']
+                                vals['select_level'], bool(vals['readonly']), bool(vals['required']), bool(vals['selectable']), vals['relation_field'], bool(vals['translate']), vals['model'], vals['name']
                             ))
                         break
         cr.commit()
@@ -618,8 +629,9 @@ class orm_template(object):
                                 if not data[fpos]:
                                     dt = ''
                                     for rr in r:
-                                        if isinstance(rr.name, browse_record):
-                                            rr = rr.name
+                                        name_relation = self.pool.get(rr._table_name)._rec_name
+                                        if isinstance(rr[name_relation], browse_record):
+                                            rr = rr[name_relation]
                                         rr_name = self.pool.get(rr._table_name).name_get(cr, uid, [rr.id], context=context)
                                         rr_name = rr_name and rr_name[0] and rr_name[0][1] or ''
                                         dt += tools.ustr(rr_name or '') + ','
@@ -711,7 +723,10 @@ class orm_template(object):
             if mode=='.id':
                 id = int(id)
                 obj_model = self.pool.get(model_name)
-                ids = obj_model.search(cr, uid, [('id', '=', int(id))])
+                dom = [('id', '=', id)]
+                if obj_model._columns.get('active'):
+                    dom.append(('active', 'in', ['True','False']))
+                ids = obj_model.search(cr, uid, dom, context=context)
                 if not len(ids):
                     raise Exception(_("Database ID doesn't exist: %s : %s") %(model_name, id))
             elif mode=='id':
@@ -720,13 +735,13 @@ class orm_template(object):
                 else:
                     module, xml_id = current_module, id
                 record_id = ir_model_data_obj._get_id(cr, uid, module, xml_id)
-                ir_model_data = ir_model_data_obj.read(cr, uid, [record_id], ['res_id'])
+                ir_model_data = ir_model_data_obj.read(cr, uid, [record_id], ['res_id'], context=context)
                 if not ir_model_data:
                     raise ValueError('No references to %s.%s' % (module, xml_id))
                 id = ir_model_data[0]['res_id']
             else:
                 obj_model = self.pool.get(model_name)
-                ids = obj_model.name_search(cr, uid, id)
+                ids = obj_model.name_search(cr, uid, id, operator='=', context=context)
                 if not ids:
                     raise ValueError('No record found for %s' % (id,))
                 id = ids[0][0]
@@ -783,7 +798,7 @@ class orm_template(object):
                         continue
                     done[field[len(prefix)]] = True
                     relation_obj = self.pool.get(fields_def[field[len(prefix)]]['relation'])
-                    newfd = relation_obj.fields_get( cr, uid, context=context )
+                    newfd = relation_obj.fields_get(cr, uid, context=context)
                     pos = position
                     res = []
                     first = 0
@@ -1802,7 +1817,7 @@ class orm_template(object):
     def copy(self, cr, uid, id, default=None, context=None):
         raise NotImplementedError(_('The copy method is not implemented on this object !'))
 
-    def exists(self, cr, uid, id, context=None):
+    def exists(self, cr, uid, ids, context=None):
         raise NotImplementedError(_('The exists method is not implemented on this object !'))
 
     def read_string(self, cr, uid, id, langs, fields=None, context=None):
@@ -1865,13 +1880,13 @@ class orm_template(object):
             # override defaults with the provided values, never allow the other way around
             defaults = self.default_get(cr, uid, missing_defaults, context)
             for dv in defaults:
-                if (dv in self._columns and self._columns[dv]._type == 'many2many') \
-                     or (dv in self._inherit_fields and self._inherit_fields[dv][2]._type == 'many2many') \
+                if ((dv in self._columns and self._columns[dv]._type == 'many2many') \
+                     or (dv in self._inherit_fields and self._inherit_fields[dv][2]._type == 'many2many')) \
                         and defaults[dv] and isinstance(defaults[dv][0], (int, long)):
                     defaults[dv] = [(6, 0, defaults[dv])]
-                if dv in self._columns and self._columns[dv]._type == 'one2many' \
-                    or (dv in self._inherit_fields and self._inherit_fields[dv][2]._type == 'one2many') \
-                        and isinstance(defaults[dv], (list, tuple)) and isinstance(defaults[dv][0], dict):
+                if (dv in self._columns and self._columns[dv]._type == 'one2many' \
+                    or (dv in self._inherit_fields and self._inherit_fields[dv][2]._type == 'one2many')) \
+                        and isinstance(defaults[dv], (list, tuple)) and defaults[dv] and isinstance(defaults[dv][0], dict):
                     defaults[dv] = [(0, 0, x) for x in defaults[dv]]
             defaults.update(values)
             values = defaults
@@ -1881,8 +1896,8 @@ class orm_memory(orm_template):
 
     _protected = ['read', 'write', 'create', 'default_get', 'perm_read', 'unlink', 'fields_get', 'fields_view_get', 'search', 'name_get', 'distinct_field_get', 'name_search', 'copy', 'import_data', 'search_count', 'exists']
     _inherit_fields = {}
-    _max_count = 200
-    _max_hours = 1
+    _max_count = config.get('osv_memory_count_limit')
+    _max_hours = config.get('osv_memory_age_limit')
     _check_time = 20
 
     def __init__(self, cr):
@@ -1896,21 +1911,32 @@ class orm_memory(orm_template):
         if uid != 1 and self.datas[object_id]['internal.create_uid'] != uid:
             raise except_orm(_('AccessError'), '%s access is only allowed on your own records for osv_memory objects except for the super-user' % mode.capitalize())
 
-    def vaccum(self, cr, uid):
+    def vaccum(self, cr, uid, force=False):
+        """Run the vaccuum cleaning system, expiring and removing old records from the
+        virtual osv_memory tables if the "max count" or "max age" conditions are enabled
+        and have been reached. This method can be called very often (e.g. everytime a record
+        is created), but will only actually trigger the cleanup process once out of
+        "_check_time" times (by default once out of 20 calls)."""
         self.check_id += 1
-        if self.check_id % self._check_time:
+        if (not force) and (self.check_id % self._check_time):
             return True
         tounlink = []
-        max = time.time() - self._max_hours * 60 * 60
-        for id in self.datas:
-            if self.datas[id]['internal.date_access'] < max:
-                tounlink.append(id)
-        self.unlink(cr, 1, tounlink)
-        if len(self.datas) > self._max_count:
-            sorted = map(lambda x: (x[1]['internal.date_access'], x[0]), self.datas.items())
-            sorted.sort()
-            ids = map(lambda x: x[1], sorted[:len(self.datas)-self._max_count])
-            self.unlink(cr, uid, ids)
+
+        # Age-based expiration
+        if self._max_hours:
+            max = time.time() - self._max_hours * 60 * 60
+            for k,v in self.datas.iteritems():
+                if v['internal.date_access'] < max:
+                    tounlink.append(k)
+            self.unlink(cr, 1, tounlink)
+
+        # Count-based expiration
+        if self._max_count and len(self.datas) > self._max_count:
+            # sort by access time to remove only the first/oldest ones in LRU fashion
+            records = self.datas.items()
+            records.sort(key=lambda x:x[1]['internal.date_access'])
+            self.unlink(cr, 1, [x[0] for x in records[:len(self.datas)-self._max_count]])
+
         return True
 
     def read(self, cr, user, ids, fields_to_read=None, context=None, load='_classic_read'):
@@ -2089,8 +2115,10 @@ class orm_memory(orm_template):
         # nothing to check in memory...
         pass
 
-    def exists(self, cr, uid, id, context=None):
-        return id in self.datas
+    def exists(self, cr, uid, ids, context=None):
+        if isinstance(ids, (int,long)):
+            ids = [ids]
+        return all(( id in self.datas for id in ids ))
 
 class orm(orm_template):
     _sql_constraints = []
@@ -2134,27 +2162,32 @@ class orm(orm_template):
 
         # Take care of adding join(s) if groupby is an '_inherits'ed field
         groupby_list = groupby
+        qualified_groupby_field = groupby
         if groupby:
             if isinstance(groupby, list):
                 groupby = groupby[0]
-            self._inherits_join_calc(groupby, query)
+            qualified_groupby_field = self._inherits_join_calc(groupby, query)
 
         if groupby:
             assert not groupby or groupby in fields, "Fields in 'groupby' must appear in the list of fields to read (perhaps it's missing in the list view?)"
             groupby_def = self._columns.get(groupby) or (self._inherit_fields.get(groupby) and self._inherit_fields.get(groupby)[2])
             assert groupby_def and groupby_def._classic_write, "Fields in 'groupby' must be regular database-persisted fields (no function or related fields), or function fields with store=True"
 
-        fget = self.fields_get(cr, uid, fields)
+        fget = self.fields_get(cr, uid, fields, context=context)
         float_int_fields = filter(lambda x: fget[x]['type'] in ('float', 'integer'), fields)
         flist = ''
         group_count = group_by = groupby
         if groupby:
             if fget.get(groupby):
-                if fget[groupby]['type'] in ('date', 'datetime'):
-                    flist = "to_char(%s,'yyyy-mm') as %s " % (groupby, groupby)
-                    groupby = "to_char(%s,'yyyy-mm')" % (groupby)
+                groupby_type = fget[groupby]['type']
+                if groupby_type in ('date', 'datetime'):    
+                    qualified_groupby_field = "to_char(%s,'yyyy-mm')" % qualified_groupby_field
+                    flist = "%s as %s " % (qualified_groupby_field, groupby)
+                elif groupby_type == 'boolean':
+                     qualified_groupby_field = "coalesce(%s,false)" % qualified_groupby_field
+                     flist = "%s as %s " % (qualified_groupby_field, groupby)
                 else:
-                    flist = groupby
+                    flist = qualified_groupby_field
             else:
                 # Don't allow arbitrary values, as this would be a SQL injection vector!
                 raise except_orm(_('Invalid group_by'),
@@ -2168,10 +2201,11 @@ class orm(orm_template):
             if f not in ['id', 'sequence']:
                 group_operator = fget[f].get('group_operator', 'sum')
                 if flist:
-                    flist += ','
-                flist += group_operator+'('+f+') as '+f
+                    flist += ', '
+                qualified_field = '"%s"."%s"' % (self._table, f)
+                flist += "%s(%s) AS %s" % (group_operator, qualified_field, f)
 
-        gb = groupby and (' GROUP BY '+groupby) or ''
+        gb = groupby and (' GROUP BY ' + qualified_groupby_field) or ''
 
         from_clause, where_clause, where_clause_params = query.get_sql()
         where_clause = where_clause and ' WHERE ' + where_clause
@@ -2559,7 +2593,7 @@ class orm(orm_template):
                                 # add the NOT NULL constraint
                                 cr.commit()
                                 try:
-                                    cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" SET NOT NULL' % (self._table, k))
+                                    cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" SET NOT NULL' % (self._table, k), log_exceptions=False)
                                     cr.commit()
                                     self.__schema.debug("Table '%s': column '%s': added NOT NULL constraint",
                                                         self._table, k)
@@ -2592,7 +2626,7 @@ class orm(orm_template):
                                 cr.execute('DROP INDEX "%s_%s_index"' % (self._table, k))
                                 cr.commit()
                                 msg = "Table '%s': dropping index for column '%s' of type '%s' as it is not required anymore"
-                                self.__schema.warn(msg, self._table, k, f._type)
+                                self.__schema.debug(msg, self._table, k, f._type)
 
                             if isinstance(f, fields.many2one):
                                 ref = self.pool.get(f._obj)._table
@@ -2665,7 +2699,7 @@ class orm(orm_template):
                             if f.required:
                                 try:
                                     cr.commit()
-                                    cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" SET NOT NULL' % (self._table, k))
+                                    cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" SET NOT NULL' % (self._table, k), log_exceptions=False)
                                     self.__schema.debug("Table '%s': column '%s': added a NOT NULL constraint",
                                         self._table, k)
                                 except Exception:
@@ -2798,10 +2832,11 @@ class orm(orm_template):
                     'string': field['field_description'],
                     'required': bool(field['required']),
                     'readonly': bool(field['readonly']),
-                    'domain': field['domain'] or None,
+                    'domain': eval(field['domain']) if field['domain'] else None,
                     'size': field['size'],
                     'ondelete': field['on_delete'],
                     'translate': (field['translate']),
+                    'manual': True,
                     #'select': int(field['select_level'])
                 }
 
@@ -2908,7 +2943,7 @@ class orm(orm_template):
             context = {}
         self.pool.get('ir.model.access').check(cr, user, self._name, 'read', context=context)
         if not fields:
-            fields = self._columns.keys() + self._inherit_fields.keys()
+            fields = list(set(self._columns.keys() + self._inherit_fields.keys()))
         if isinstance(ids, (int, long)):
             select = [ids]
         else:
@@ -3055,7 +3090,7 @@ class orm(orm_template):
                     for group in groups:
                         module = group.split(".")[0]
                         grp = group.split(".")[1]
-                        cr.execute("select count(*) from res_groups_users_rel where gid IN (select res_id from ir_model_data where name=%s and module=%s and model=%s) and uid=%s"  \
+                        cr.execute("select count(*) from res_groups_users_rel where gid IN (select res_id from ir_model_data where name=%s and module=%s and model=%s) and uid=%s",  \
                                    (grp, module, 'res.groups', user))
                         readonly = cr.fetchall()
                         if readonly[0][0] >= 1:
@@ -3204,9 +3239,26 @@ class orm(orm_template):
 
 
         self.check_access_rule(cr, uid, ids, 'unlink', context=context)
+        pool_model_data = self.pool.get('ir.model.data')
+        pool_ir_values = self.pool.get('ir.values')
         for sub_ids in cr.split_for_in_conditions(ids):
             cr.execute('delete from ' + self._table + ' ' \
                        'where id IN %s', (sub_ids,))
+
+            # Removing the ir_model_data reference if the record being deleted is a record created by xml/csv file,
+            # as these are not connected with real database foreign keys, and would be dangling references.
+            # Step 1. Calling unlink of ir_model_data only for the affected IDS.
+            referenced_ids = pool_model_data.search(cr, uid, [('res_id','in',list(sub_ids)),('model','=',self._name)], context=context)
+            # Step 2. Marching towards the real deletion of referenced records
+            pool_model_data.unlink(cr, uid, referenced_ids, context=context)
+
+            # For the same reason, removing the record relevant to ir_values
+            ir_value_ids = pool_ir_values.search(cr, uid,
+                    ['|',('value','in',['%s,%s' % (self._name, sid) for sid in sub_ids]),'&',('res_id','in',list(sub_ids)),('model','=',self._name)],
+                    context=context)
+            if ir_value_ids:
+                pool_ir_values.unlink(cr, uid, ir_value_ids, context=context)
+
         for order, object, store_ids, fields in result_store:
             if object != self._name:
                 obj = self.pool.get(object)
@@ -3214,6 +3266,7 @@ class orm(orm_template):
                 rids = map(lambda x: x[0], cr.fetchall())
                 if rids:
                     obj._store_set_values(cr, uid, rids, fields, context)
+
         return True
 
     #
@@ -3421,14 +3474,19 @@ class orm(orm_template):
                     clause, params = '%s=%%s' % (self._parent_name,), (parent_val,)
                 else:
                     clause, params = '%s IS NULL' % (self._parent_name,), ()
-                cr.execute('SELECT parent_right, id FROM %s WHERE %s ORDER BY %s' % (self._table, clause, order), params)
-                parents = cr.fetchall()
 
                 for id in parents_changed:
                     cr.execute('SELECT parent_left, parent_right FROM %s WHERE id=%%s' % (self._table,), (id,))
                     pleft, pright = cr.fetchone()
                     distance = pright - pleft + 1
 
+                    # Positions of current siblings, to locate proper insertion point;
+                    # this can _not_ be fetched outside the loop, as it needs to be refreshed
+                    # after each update, in case several nodes are sequentially inserted one
+                    # next to the other (i.e computed incrementally)
+                    cr.execute('SELECT parent_right, id FROM %s WHERE %s ORDER BY %s' % (self._table, clause, order), params)
+                    parents = cr.fetchall()
+
                     # Find Position of the element
                     position = None
                     for (parent_pright, parent_id) in parents:
@@ -3460,16 +3518,16 @@ class orm(orm_template):
         result.sort()
 
         done = {}
-        for order, object, ids, fields in result:
-            key = (object, tuple(fields))
+        for order, object, ids_to_update, fields_to_recompute in result:
+            key = (object, tuple(fields_to_recompute))
             done.setdefault(key, {})
             # avoid to do several times the same computation
             todo = []
-            for id in ids:
+            for id in ids_to_update:
                 if id not in done[key]:
                     done[key][id] = True
                     todo.append(id)
-            self.pool.get(object)._store_set_values(cr, user, todo, fields, context)
+            self.pool.get(object)._store_set_values(cr, user, todo, fields_to_recompute, context)
 
         wf_service = netsvc.LocalService("workflow")
         for id in ids:
@@ -3657,6 +3715,15 @@ class orm(orm_template):
         return id_new
 
     def _store_get_values(self, cr, uid, ids, fields, context):
+        """Returns an ordered list of fields.functions to call due to
+           an update operation on ``fields`` of records with ``ids``,
+           obtained by calling the 'store' functions of these fields,
+           as setup by their 'store' attribute.
+
+           :return: [(priority, model_name, [record_ids,], [function_fields,])]
+        """
+        # FIXME: rewrite, cleanup, use real variable names
+        # e.g.: http://pastie.org/1222060
         result = {}
         fncts = self.pool._store_function.get(self._name, [])
         for fnct in range(len(fncts)):
@@ -3695,6 +3762,8 @@ class orm(orm_template):
         return result2
 
     def _store_set_values(self, cr, uid, ids, fields, context):
+        """Calls the fields.function's "implementation function" for all ``fields``, on records with ``ids`` (taking care of
+           respecting ``multi`` attributes), and stores the resulting values in the database directly."""
         if not ids:
             return True
         field_flag = False
@@ -3789,7 +3858,7 @@ class orm(orm_template):
         domain = domain[:]
         # if the object has a field named 'active', filter out all inactive
         # records unless they were explicitely asked for
-        if 'active' in self._columns and (active_test and context.get('active_test', True)):
+        if 'active' in (self._columns.keys() + self._inherit_fields.keys()) and (active_test and context.get('active_test', True)):
             if domain:
                 active_in_args = False
                 for a in domain:
@@ -3878,7 +3947,7 @@ class orm(orm_template):
         else:
             # extract the field names, to be able to qualify them and add desc/asc
             m2o_order_list = []
-            for order_part in m2o_order.split(",",1):
+            for order_part in m2o_order.split(","):
                 m2o_order_list.append(order_part.strip().split(" ",1)[0].strip())
             m2o_order = m2o_order_list
 
@@ -3906,7 +3975,9 @@ class orm(orm_template):
                 order_field = order_split[0].strip()
                 order_direction = order_split[1].strip() if len(order_split) == 2 else ''
                 inner_clause = None
-                if order_field in self._columns:
+                if order_field == 'id':
+                    order_by_clause = '"%s"."%s"' % (self._table, order_field)
+                elif order_field in self._columns:
                     order_column = self._columns[order_field]
                     if order_column._classic_read:
                         inner_clause = '"%s"."%s"' % (self._table, order_field)
@@ -4173,27 +4244,47 @@ class orm(orm_template):
                     return False
         return True
 
+    def _get_xml_ids(self, cr, uid, ids, *args, **kwargs):
+        """Find out the XML ID(s) of any database record.
+
+        **Synopsis**: ``_get_xml_ids(cr, uid, ids) -> { 'id': ['module.xml_id'] }``
+
+        :return: map of ids to the list of their fully qualified XML IDs
+                 (empty list when there's none).
+        """
+        model_data_obj = self.pool.get('ir.model.data')
+        data_ids = model_data_obj.search(cr, uid, [('model', '=', self._name), ('res_id', 'in', ids)])
+        data_results = model_data_obj.read(cr, uid, data_ids, ['module', 'name', 'res_id'])
+        result = {}
+        for id in ids:
+            # can't use dict.fromkeys() as the list would be shared!
+            result[id] = []
+        for record in data_results:
+            result[record['res_id']].append('%(module)s.%(name)s' % record)
+        return result
+
     def get_xml_id(self, cr, uid, ids, *args, **kwargs):
         """Find out the XML ID of any database record, if there
         is one. This method works as a possible implementation
         for a function field, to be able to add it to any
         model object easily, referencing it as ``osv.osv.get_xml_id``.
 
+        When multiple XML IDs exist for a record, only one
+        of them is returned (randomly).
+
         **Synopsis**: ``get_xml_id(cr, uid, ids) -> { 'id': 'module.xml_id' }``
 
-        :return: the fully qualified XML ID of the given object,
+        :return: map of ids to their fully qualified XML ID,
                  defaulting to an empty string when there's none
                  (to be usable as a function field).
         """
-        result = dict.fromkeys(ids, '')
-        model_data_obj = self.pool.get('ir.model.data')
-        data_ids = model_data_obj.search(cr, uid,
-                [('model', '=', self._name), ('res_id', 'in', ids)])
-        data_results = model_data_obj.read(cr, uid, data_ids,
-                ['name', 'module', 'res_id'])
-        for record in data_results:
-            result[record['res_id']] = '%(module)s.%(name)s' % record
-        return result
+        results = self._get_xml_ids(cr, uid, ids)
+        for k, v in results.items():
+            if results[k]:
+                results[k] = v[0]
+            else:
+                results[k] = ''
+        return results
 
 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: