[IMP] add an 'active' field on ir.model.access and ir.rule objects
[odoo/odoo.git] / openerp / addons / base / ir / ir_model.py
index 6da502f..f6e9306 100644 (file)
@@ -1,8 +1,9 @@
 # -*- coding: utf-8 -*-
+
 ##############################################################################
 #
-#    OpenERP, Open Source Management Solution
-#    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
+#    OpenERP, Open Source Business Applications
+#    Copyright (C) 2004-2012 OpenERP S.A. (<http://openerp.com>).
 #
 #    This program is free software: you can redistribute it and/or modify
 #    it under the terms of the GNU Affero General Public License as
 import logging
 import re
 import time
-from operator import itemgetter
+import types
+
+from openerp.osv import fields,osv
+from openerp import netsvc, pooler, tools
+from openerp.tools.safe_eval import safe_eval as eval
+from openerp.tools import config
+from openerp.tools.translate import _
+from openerp.osv.orm import except_orm, browse_record
+
+_logger = logging.getLogger(__name__)
 
-from osv import fields,osv
-import netsvc
-from osv.orm import except_orm, browse_record
-import tools
-from tools.safe_eval import safe_eval as eval
-from tools import config
-from tools.translate import _
-import pooler
+MODULE_UNINSTALL_FLAG = '_force_unlink'
 
 def _get_fields_type(self, cr, uid, context=None):
-    cr.execute('select distinct ttype,ttype from ir_model_fields')
-    field_types = cr.fetchall()
-    field_types_copy = field_types
-    for types in field_types_copy:
-        if not hasattr(fields,types[0]):
-            field_types.remove(types)
-    return field_types
+    # Avoid too many nested `if`s below, as RedHat's Python 2.6
+    # break on it. See bug 939653.
+    return sorted([(k,k) for k,v in fields.__dict__.iteritems()
+                      if type(v) == types.TypeType and \
+                         issubclass(v, fields._column) and \
+                         v != fields._column and \
+                         not v._deprecated and \
+                         not issubclass(v, fields.function)])
 
 def _in_modules(self, cr, uid, ids, field_name, arg, context=None):
     #pseudo-method used by fields.function in ir.model/ir.model.fields
@@ -57,20 +61,20 @@ def _in_modules(self, cr, uid, ids, field_name, arg, context=None):
 
 class ir_model(osv.osv):
     _name = 'ir.model'
-    _description = "Objects"
+    _description = "Models"
     _order = 'model'
 
     def _is_osv_memory(self, cr, uid, ids, field_name, arg, context=None):
         models = self.browse(cr, uid, ids, context=context)
         res = dict.fromkeys(ids)
         for model in models:
-            res[model.id] = isinstance(self.pool.get(model.model), osv.osv_memory)
+            res[model.id] = self.pool.get(model.model).is_transient()
         return res
 
     def _search_osv_memory(self, cr, uid, model, name, domain, context=None):
         if not domain:
             return []
-        field, operator, value = domain[0]
+        __, operator, value = domain[0]
         if operator not in ['=', '!=']:
             raise osv.except_osv(_('Invalid search criterions'), _('The osv_memory field can only be compared with = and != operator.'))
         value = bool(value) if operator == '=' else not bool(value)
@@ -86,24 +90,24 @@ class ir_model(osv.osv):
         return res
 
     _columns = {
-        'name': fields.char('Object Name', size=64, translate=True, required=True),
-        'model': fields.char('Object', size=64, required=True, select=1),
+        'name': fields.char('Model Description', size=64, translate=True, required=True),
+        'model': fields.char('Model', size=64, required=True, select=1),
         'info': fields.text('Information'),
         'field_id': fields.one2many('ir.model.fields', 'model_id', 'Fields', required=True),
         'state': fields.selection([('manual','Custom Object'),('base','Base Object')],'Type',readonly=True),
         'access_ids': fields.one2many('ir.model.access', 'model_id', 'Access'),
-        'osv_memory': fields.function(_is_osv_memory, method=True, string='In-memory model', type='boolean',
+        'osv_memory': fields.function(_is_osv_memory, string='In-Memory Model', type='boolean',
             fnct_search=_search_osv_memory,
             help="Indicates whether this object model lives in memory only, i.e. is not persisted (osv.osv_memory)"),
-        'modules': fields.function(_in_modules, method=True, type='char', size=128, string='In modules', help='List of modules in which the object is defined or inherited'),
-        'view_ids': fields.function(_view_ids, method=True, type='one2many', obj='ir.ui.view', string='Views'),
+        'modules': fields.function(_in_modules, type='char', size=128, string='In Modules', help='List of modules in which the object is defined or inherited'),
+        'view_ids': fields.function(_view_ids, type='one2many', obj='ir.ui.view', string='Views'),
     }
-    
+
     _defaults = {
         'model': lambda *a: 'x_',
         'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
     }
-    
+
     def _check_model_name(self, cr, uid, ids, context=None):
         for model in self.browse(cr, uid, ids, context=context):
             if model.state=='manual':
@@ -115,31 +119,60 @@ class ir_model(osv.osv):
 
     def _model_name_msg(self, cr, uid, ids, context=None):
         return _('The Object name must start with x_ and not contain any special character !')
+
     _constraints = [
         (_check_model_name, _model_name_msg, ['model']),
     ]
+    _sql_constraints = [
+        ('obj_name_uniq', 'unique (model)', 'Each model must be unique!'),
+    ]
 
     # overridden to allow searching both on model name (model field)
     # and model description (name field)
-    def name_search(self, cr, uid, name='', args=None, operator='ilike',  context=None, limit=None):
+    def _name_search(self, cr, uid, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None):
         if args is None:
             args = []
         domain = args + ['|', ('model', operator, name), ('name', operator, name)]
-        return super(ir_model, self).name_search(cr, uid, None, domain,
-                        operator=operator, limit=limit, context=context)
-
+        return self.name_get(cr, name_get_uid or uid,
+                             super(ir_model, self).search(cr, uid, domain, limit=limit, context=context),
+                             context=context)
+
+    def _drop_table(self, cr, uid, ids, context=None):
+        for model in self.browse(cr, uid, ids, context):
+            model_pool = self.pool.get(model.model)
+            cr.execute('select relkind from pg_class where relname=%s', (model_pool._table,))
+            result = cr.fetchone()
+            if result and result[0] == 'v':
+                cr.execute('DROP view %s' % (model_pool._table,))
+            elif result and result[0] == 'r':
+                cr.execute('DROP TABLE %s' % (model_pool._table,))
+        return True
 
     def unlink(self, cr, user, ids, context=None):
-        for model in self.browse(cr, user, ids, context):
-            if model.state != 'manual':
-                raise except_orm(_('Error'), _("You can not remove the model '%s' !") %(model.name,))
+        # Prevent manual deletion of module tables
+        if context is None: context = {}
+        if isinstance(ids, (int, long)):
+            ids = [ids]
+        if not context.get(MODULE_UNINSTALL_FLAG) and \
+                any(model.state != 'manual' for model in self.browse(cr, user, ids, context)):
+            raise except_orm(_('Error'), _("Model '%s' contains module data and cannot be removed!") % (model.name,))
+
+        self._drop_table(cr, user, ids, context)
         res = super(ir_model, self).unlink(cr, user, ids, context)
-        pooler.restart_pool(cr.dbname)
+        if not context.get(MODULE_UNINSTALL_FLAG):
+            # only reload pool for normal unlink. For module uninstall the
+            # reload is done independently in openerp.modules.loading
+            pooler.restart_pool(cr.dbname)
+
         return res
 
     def write(self, cr, user, ids, vals, context=None):
         if context:
             context.pop('__last_update', None)
+        # Filter out operations 4 link from field id, because openerp-web
+        # always write (4,id,False) even for non dirty items
+        if 'field_id' in vals:
+            vals['field_id'] = [op for op in vals['field_id'] if op[0] != 4]
         return super(ir_model,self).write(cr, user, ids, vals, context)
 
     def create(self, cr, user, vals, context=None):
@@ -157,12 +190,12 @@ class ir_model(osv.osv):
             #pooler.restart_pool(cr.dbname)
         return res
 
-    def instanciate(self, cr, user, model, context={}):
+    def instanciate(self, cr, user, model, context=None):
         class x_custom_model(osv.osv):
             pass
         x_custom_model._name = model
         x_custom_model._module = False
-        a = x_custom_model.createInstance(self.pool, '', cr)
+        a = x_custom_model.create_instance(self.pool, cr)
         if (not a._columns) or ('x_name' in a._columns.keys()):
             x_name = 'x_name'
         else:
@@ -195,14 +228,19 @@ class ir_model_fields(osv.osv):
         'translate': fields.boolean('Translate', help="Whether values for this field can be translated (enables the translation mechanism for that field)"),
         'size': fields.integer('Size'),
         'state': fields.selection([('manual','Custom Field'),('base','Base Field')],'Type', required=True, readonly=True, select=1),
-        'on_delete': fields.selection([('cascade','Cascade'),('set null','Set NULL')], 'On delete', help='On delete property for many2one fields'),
+        'on_delete': fields.selection([('cascade','Cascade'),('set null','Set NULL')], 'On Delete', help='On delete property for many2one fields'),
         'domain': fields.char('Domain', size=256, help="The optional domain to restrict possible values for relationship fields, "
             "specified as a Python expression defining a list of triplets. "
             "For example: [('color','=','red')]"),
         'groups': fields.many2many('res.groups', 'ir_model_fields_group_rel', 'field_id', 'group_id', 'Groups'),
         'view_load': fields.boolean('View Auto-Load'),
         'selectable': fields.boolean('Selectable'),
-        'modules': fields.function(_in_modules, method=True, type='char', size=128, string='In modules', help='List of modules in which the field is defined'),
+        'modules': fields.function(_in_modules, type='char', size=128, string='In Modules', help='List of modules in which the field is defined'),
+        'serialization_field_id': fields.many2one('ir.model.fields', 'Serialization Field', domain = "[('ttype','=','serialized')]",
+                                                  ondelete='cascade', help="If set, this field will be stored in the sparse "
+                                                                           "structure of the serialization field, instead "
+                                                                           "of having its own database column. This cannot be "
+                                                                           "changed after creation."),
     }
     _rec_name='field_description'
     _defaults = {
@@ -223,7 +261,7 @@ class ir_model_fields(osv.osv):
         try:
             selection_list = eval(selection)
         except Exception:
-            logging.getLogger('ir.model').warning('Invalid selection list definition for fields.selection', exc_info=True)
+            _logger.warning('Invalid selection list definition for fields.selection', exc_info=True)
             raise except_orm(_('Error'),
                     _("The Selection Options expression is not a valid Pythonic expression." \
                       "Please provide an expression in the [('key','Label'), ...] format."))
@@ -249,16 +287,30 @@ class ir_model_fields(osv.osv):
         ('size_gt_zero', 'CHECK (size>0)',_size_gt_zero_msg ),
     ]
 
+    def _drop_column(self, cr, uid, ids, context=None):
+        for field in self.browse(cr, uid, ids, context):
+            model = self.pool.get(field.model)
+            cr.execute('select relkind from pg_class where relname=%s', (model._table,))
+            result = cr.fetchone()
+            cr.execute("SELECT column_name FROM information_schema.columns WHERE table_name ='%s' and column_name='%s'" %(model._table, field.name))
+            column_name = cr.fetchone()
+            if column_name and (result and result[0] == 'r'):
+                cr.execute('ALTER table "%s" DROP column "%s" cascade' % (model._table, field.name))
+            model._columns.pop(field.name, None)
+        return True
+
     def unlink(self, cr, user, ids, context=None):
-        for field in self.browse(cr, user, ids, context):
-            if field.state <> 'manual':
-                raise except_orm(_('Error'), _("You cannot remove the field '%s' !") %(field.name,))
-        #
-        # MAY BE ADD A ALTER TABLE DROP ?
-        #
-            #Removing _columns entry for that table
-            self.pool.get(field.model)._columns.pop(field.name,None)
-        return super(ir_model_fields, self).unlink(cr, user, ids, context)
+        # Prevent manual deletion of module columns
+        if context is None: context = {}
+        if isinstance(ids, (int, long)):
+            ids = [ids]
+        if not context.get(MODULE_UNINSTALL_FLAG) and \
+                any(field.state != 'manual' for field in self.browse(cr, user, ids, context)):
+            raise except_orm(_('Error'), _("This column contains module data and cannot be removed!"))
+
+        self._drop_column(cr, user, ids, context)
+        res = super(ir_model_fields, self).unlink(cr, user, ids, context)
+        return res
 
     def create(self, cr, user, vals, context=None):
         if 'model_id' in vals:
@@ -278,7 +330,7 @@ class ir_model_fields(osv.osv):
                 raise except_orm(_('Error'), _("Custom fields must have a name that starts with 'x_' !"))
 
             if vals.get('relation',False) and not self.pool.get('ir.model').search(cr, user, [('model','=',vals['relation'])]):
-                 raise except_orm(_('Error'), _("Model %s does not exist!") % vals['relation'])
+                raise except_orm(_('Error'), _("Model %s does not exist!") % vals['relation'])
 
             if self.pool.get(vals['model']):
                 self.pool.get(vals['model']).__init__(self.pool, cr)
@@ -295,6 +347,14 @@ class ir_model_fields(osv.osv):
         if context and context.get('manual',False):
             vals['state'] = 'manual'
 
+        #For the moment renaming a sparse field or changing the storing system is not allowed. This may be done later
+        if 'serialization_field_id' in vals or 'name' in vals:
+            for field in self.browse(cr, user, ids, context=context):
+                if 'serialization_field_id' in vals and field.serialization_field_id.id != vals['serialization_field_id']:
+                    raise except_orm(_('Error!'),  _('Changing the storing system for field "%s" is not allowed.')%field.name)
+                if field.serialization_field_id and (field.name != vals['name']):
+                    raise except_orm(_('Error!'),  _('Renaming sparse field "%s" is not allowed')%field.name)
+
         column_rename = None # if set, *one* column can be renamed here
         obj = None
         models_patch = {}    # structs of (obj, [(field, prop, change_to),..])
@@ -389,7 +449,7 @@ class ir_model_fields(osv.osv):
             ctx = context.copy()
             ctx.update({'select': vals.get('select_level','0'),'update_custom_fields':True})
 
-            for model_key, patch_struct in models_patch.items():
+            for __, patch_struct in models_patch.items():
                 obj = patch_struct[0]
                 for col_name, col_prop, val in patch_struct[1]:
                     setattr(obj._columns[col_name], col_prop, val)
@@ -402,6 +462,7 @@ class ir_model_access(osv.osv):
     _name = 'ir.model.access'
     _columns = {
         'name': fields.char('Name', size=64, required=True, select=True),
+        'active': fields.boolean('Active', help='If you uncheck the active field, it will disable the ACL without deleting it (if you delete a native ACL, it will be re-created when you reload the module.'),
         'model_id': fields.many2one('ir.model', 'Object', required=True, domain=[('osv_memory','=', False)], select=True, ondelete='cascade'),
         'group_id': fields.many2one('res.groups', 'Group', ondelete='cascade', select=True),
         'perm_read': fields.boolean('Read Access'),
@@ -410,6 +471,10 @@ class ir_model_access(osv.osv):
         'perm_unlink': fields.boolean('Delete Access'),
     }
 
+    _defaults = {
+        'active': True,
+    }
+
     def check_groups(self, cr, uid, group):
         grouparr  = group.split('.')
         if not grouparr:
@@ -433,14 +498,16 @@ class ir_model_access(osv.osv):
             cr.execute("SELECT perm_" + mode + " "
                    "  FROM ir_model_access a "
                    "  JOIN ir_model m ON (m.id = a.model_id) "
-                   " WHERE m.model = %s AND a.group_id = %s", (model_name, group_id)
+                   " WHERE m.model = %s AND a.active IS True "
+                   " AND a.group_id = %s", (model_name, group_id)
                    )
             r = cr.fetchone()
             if r is None:
                 cr.execute("SELECT perm_" + mode + " "
                        "  FROM ir_model_access a "
                        "  JOIN ir_model m ON (m.id = a.model_id) "
-                       " WHERE m.model = %s AND a.group_id IS NULL", (model_name, )
+                       " WHERE m.model = %s AND a.active IS True "
+                       " AND a.group_id IS NULL", (model_name, )
                        )
                 r = cr.fetchone()
 
@@ -450,6 +517,26 @@ class ir_model_access(osv.osv):
         # pass no groups -> no access
         return False
 
+    def group_names_with_access(self, cr, model_name, access_mode):
+        """Returns the names of visible groups which have been granted ``access_mode`` on
+           the model ``model_name``.
+           :rtype: list
+        """
+        assert access_mode in ['read','write','create','unlink'], 'Invalid access mode: %s' % access_mode
+        cr.execute('''SELECT
+                        c.name, g.name
+                      FROM
+                        ir_model_access a
+                        JOIN ir_model m ON (a.model_id=m.id)
+                        JOIN res_groups g ON (a.group_id=g.id)
+                        LEFT JOIN ir_module_category c ON (c.id=g.category_id)
+                      WHERE
+                        m.model=%s AND
+                        a.active IS True AND
+                        a.perm_''' + access_mode, (model_name,))
+        return [('%s/%s' % x) if x[0] else x[1] for x in cr.fetchall()]
+
+    @tools.ormcache()
     def check(self, cr, uid, model, mode='read', raise_exception=True, context=None):
         if uid==1:
             # User root have all accesses
@@ -460,14 +547,12 @@ class ir_model_access(osv.osv):
 
         if isinstance(model, browse_record):
             assert model._table_name == 'ir.model', 'Invalid model object'
-            model_name = model.name
+            model_name = model.model
         else:
             model_name = model
 
-        # osv_memory objects can be read by everyone, as they only return
-        # results that belong to the current user (except for superuser)
-        model_obj = self.pool.get(model_name)
-        if isinstance(model_obj, osv.osv_memory):
+        # TransientModel records have no access rights, only an implicit access rule
+        if self.pool.get(model_name).is_transient():
             return True
 
         # We check if a specific rule exists
@@ -477,6 +562,7 @@ class ir_model_access(osv.osv):
                    '  JOIN res_groups_users_rel gu ON (gu.gid = a.group_id) '
                    ' WHERE m.model = %s '
                    '   AND gu.uid = %s '
+                   '   AND a.active IS True '
                    , (model_name, uid,)
                    )
         r = cr.fetchone()[0]
@@ -488,32 +574,30 @@ class ir_model_access(osv.osv):
                        '  JOIN ir_model m ON (m.id = a.model_id) '
                        ' WHERE a.group_id IS NULL '
                        '   AND m.model = %s '
+                       '   AND a.active IS True '
                        , (model_name,)
                        )
             r = cr.fetchone()[0]
 
         if not r and raise_exception:
-            cr.execute('''select
-                    g.name
-                from
-                    ir_model_access a 
-                    left join ir_model m on (a.model_id=m.id) 
-                    left join res_groups g on (a.group_id=g.id)
-                where
-                    m.model=%s and
-                    a.group_id is not null and perm_''' + mode, (model_name, ))
-            groups = ', '.join(map(lambda x: x[0], cr.fetchall())) or '/'
-            msgs = {
-                'read':   _("You can not read this document (%s) ! Be sure your user belongs to one of these groups: %s."),
-                'write':  _("You can not write in this document (%s) ! Be sure your user belongs to one of these groups: %s."),
-                'create': _("You can not create this document (%s) ! Be sure your user belongs to one of these groups: %s."),
-                'unlink': _("You can not delete this document (%s) ! Be sure your user belongs to one of these groups: %s."),
+            groups = '\n\t'.join('- %s' % g for g in self.group_names_with_access(cr, model_name, mode))
+            msg_heads = {
+                # Messages are declared in extenso so they are properly exported in translation terms
+                'read': _("Sorry, you are not allowed to access this document."),
+                'write':  _("Sorry, you are not allowed to modify this document."),
+                'create': _("Sorry, you are not allowed to create this kind of document."),
+                'unlink': _("Sorry, you are not allowed to delete this document."),
             }
-
-            raise except_orm(_('AccessError'), msgs[mode] % (model_name, groups) )
-        return r
-
-    check = tools.cache()(check)
+            if groups:
+                msg_tail = _("Only users with the following access level are currently allowed to do that") + ":\n%s\n\n(" + _("Document model") + ": %s)"
+                msg_params = (groups, model_name)
+            else:
+                msg_tail = _("Please contact your system administrator if you think this is an error.") + "\n\n(" + _("Document model") + ": %s)"
+                msg_params = (model_name,)
+            _logger.warning('Access Denied by ACLs for operation: %s, uid: %s, model: %s', mode, uid, model_name)
+            msg = '%s %s' % (msg_heads[mode], msg_tail)
+            raise except_orm(_('Access Denied'), msg % msg_params)
+        return r or False
 
     __cache_clearing_methods = []
 
@@ -528,7 +612,7 @@ class ir_model_access(osv.osv):
             pass
 
     def call_cache_clearing_methods(self, cr):
-        self.check.clear_cache(cr.dbname)    # clear the cache of check function
+        self.check.clear_cache(self)    # clear the cache of check function
         for model, method in self.__cache_clearing_methods:
             object_ = self.pool.get(model)
             if object_:
@@ -555,14 +639,53 @@ class ir_model_access(osv.osv):
 ir_model_access()
 
 class ir_model_data(osv.osv):
+    """Holds external identifier keys for records in the database.
+       This has two main uses:
+
+           * allows easy data integration with third-party systems,
+             making import/export/sync of data possible, as records
+             can be uniquely identified across multiple systems
+           * allows tracking the origin of data installed by OpenERP
+             modules themselves, thus making it possible to later
+             update them seamlessly.
+    """
     _name = 'ir.model.data'
-    __logger = logging.getLogger('addons.base.'+_name)
     _order = 'module,model,name'
+    def _display_name_get(self, cr, uid, ids, prop, unknow_none, context=None):
+        result = {}
+        result2 = {}
+        for res in self.browse(cr, uid, ids, context=context):
+            if res.id:
+                result.setdefault(res.model, {})
+                result[res.model][res.res_id] = res.id
+            result2[res.id] = False
+
+        for model in result:
+            try:
+                r = dict(self.pool.get(model).name_get(cr, uid, result[model].keys(), context=context))
+                for key,val in result[model].items():
+                    result2[val] = r.get(key, False)
+            except:
+                # some object have no valid name_get implemented, we accept this
+                pass
+        return result2
+
+    def _complete_name_get(self, cr, uid, ids, prop, unknow_none, context=None):
+        result = {}
+        for res in self.browse(cr, uid, ids, context=context):
+            result[res.id] = (res.module and (res.module + '.') or '')+res.name
+        return result
+
     _columns = {
-        'name': fields.char('XML Identifier', required=True, size=128, select=1),
-        'model': fields.char('Object', required=True, size=64, select=1),
+        'name': fields.char('External Identifier', required=True, size=128, select=1,
+                            help="External Key/Identifier that can be used for "
+                                 "data integration with third-party systems"),
+        'complete_name': fields.function(_complete_name_get, type='char', string='Complete ID'),
+        'display_name': fields.function(_display_name_get, type='char', string='Record Name'),
+        'model': fields.char('Model Name', required=True, size=64, select=1),
         'module': fields.char('Module', required=True, size=64, select=1),
-        'res_id': fields.integer('Resource ID', select=1),
+        'res_id': fields.integer('Record ID', select=1,
+                                 help="ID of the target record in the database"),
         'noupdate': fields.boolean('Non Updatable'),
         'date_update': fields.datetime('Update Date'),
         'date_init': fields.datetime('Init Date')
@@ -574,17 +697,16 @@ class ir_model_data(osv.osv):
         'module': ''
     }
     _sql_constraints = [
-        ('module_name_uniq', 'unique(name, module)', 'You cannot have multiple records with the same id for the same module !'),
+        ('module_name_uniq', 'unique(name, module)', 'You cannot have multiple records with the same external ID in the same module!'),
     ]
 
     def __init__(self, pool, cr):
         osv.osv.__init__(self, pool, cr)
         self.doinit = True
-        self.unlink_mark = {}
-
         # also stored in pool to avoid being discarded along with this osv instance
         if getattr(pool, 'model_data_reference_ids', None) is None:
             self.pool.model_data_reference_ids = {}
+
         self.loads = self.pool.model_data_reference_ids
 
     def _auto_init(self, cr, context=None):
@@ -593,22 +715,22 @@ class ir_model_data(osv.osv):
         if not cr.fetchone():
             cr.execute('CREATE INDEX ir_model_data_module_name_index ON ir_model_data (module, name)')
 
-    @tools.cache()
+    @tools.ormcache()
     def _get_id(self, cr, uid, module, xml_id):
         """Returns the id of the ir.model.data record corresponding to a given module and xml_id (cached) or raise a ValueError if not found"""
         ids = self.search(cr, uid, [('module','=',module), ('name','=', xml_id)])
         if not ids:
-            raise ValueError('No references to %s.%s' % (module, xml_id))
+            raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))
         # the sql constraints ensure us we have only one result
         return ids[0]
 
-    @tools.cache()
+    @tools.ormcache()
     def get_object_reference(self, cr, uid, module, xml_id):
         """Returns (model, res_id) corresponding to a given module and xml_id (cached) or raise ValueError if not found"""
         data_id = self._get_id(cr, uid, module, xml_id)
         res = self.read(cr, uid, data_id, ['model', 'res_id'])
         if not res['res_id']:
-            raise ValueError('No references to %s.%s' % (module, xml_id))
+            raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))
         return (res['model'], res['res_id'])
 
     def get_object(self, cr, uid, module, xml_id, context=None):
@@ -629,34 +751,40 @@ class ir_model_data(osv.osv):
             id = False
         return id
 
+
+    def unlink(self, cr, uid, ids, context=None):
+        """ Regular unlink method, but make sure to clear the caches. """
+        self._get_id.clear_cache(self)
+        self.get_object_reference.clear_cache(self)
+        return super(ir_model_data,self).unlink(cr, uid, ids, context=context)
+
     def _update(self,cr, uid, model, module, values, xml_id=False, store=True, noupdate=False, mode='init', res_id=False, context=None):
         model_obj = self.pool.get(model)
         if not context:
             context = {}
-
-        # records created during module install should result in res.log entries that are already read!
-        context = dict(context, res_log_read=True)
-
+        # records created during module install should not display the messages of OpenChatter
+        context = dict(context, install_mode=True)
         if xml_id and ('.' in xml_id):
             assert len(xml_id.split('.'))==2, _("'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id") % (xml_id)
             module, xml_id = xml_id.split('.')
         if (not xml_id) and (not self.doinit):
             return False
         action_id = False
-
         if xml_id:
-            cr.execute('''SELECT imd.id, imd.res_id, md.id
+            cr.execute('''SELECT imd.id, imd.res_id, md.id, imd.model
                           FROM ir_model_data imd LEFT JOIN %s md ON (imd.res_id = md.id)
                           WHERE imd.module=%%s AND imd.name=%%s''' % model_obj._table,
                           (module, xml_id))
             results = cr.fetchall()
-            for imd_id2,res_id2,real_id2 in results:
+            for imd_id2,res_id2,real_id2,real_model in results:
                 if not real_id2:
-                    self._get_id.clear_cache(cr.dbname, uid, module, xml_id)
-                    self.get_object_reference.clear_cache(cr.dbname, uid, module, xml_id)
+                    self._get_id.clear_cache(self, uid, module, xml_id)
+                    self.get_object_reference.clear_cache(self, uid, module, xml_id)
                     cr.execute('delete from ir_model_data where id=%s', (imd_id2,))
                     res_id = False
                 else:
+                    assert model == real_model, "External ID conflict, %s already refers to a `%s` record,"\
+                        " you can't define a `%s` record with this ID." % (xml_id, real_model, model)
                     res_id,action_id = res_id2,imd_id2
 
         if action_id and res_id:
@@ -719,12 +847,6 @@ class ir_model_data(osv.osv):
                                 table.replace('.', '_'))] = (table, inherit_id)
         return res_id
 
-    def _unlink(self, cr, uid, model, res_ids):
-        for res_id in res_ids:
-            self.unlink_mark[(model, res_id)] = False
-            cr.execute('delete from ir_model_data where res_id=%s and model=%s', (res_id, model))
-        return True
-
     def ir_set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=None, xml_id=False):
         if type(models[0])==type([]) or type(models[0])==type(()):
             model,res_id = models[0]
@@ -751,59 +873,106 @@ class ir_model_data(osv.osv):
             cr.execute('UPDATE ir_values set value=%s WHERE model=%s and key=%s and name=%s'+where,(value, model, key, name))
         return True
 
+    def _module_data_uninstall(self, cr, uid, modules_to_remove, context=None):
+        """Deletes all the records referenced by the ir.model.data entries
+        ``ids`` along with their corresponding database backed (including
+        dropping tables, columns, FKs, etc, as long as there is no other
+        ir.model.data entry holding a reference to them (which indicates that
+        they are still owned by another module). 
+        Attempts to perform the deletion in an appropriate order to maximize
+        the chance of gracefully deleting all records.
+        This step is performed as part of the full uninstallation of a module.
+        """ 
+
+        ids = self.search(cr, uid, [('module', 'in', modules_to_remove)])
+
+        if uid != 1 and not self.pool.get('ir.model.access').check_groups(cr, uid, "base.group_system"):
+            raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
+
+        context = dict(context or {})
+        context[MODULE_UNINSTALL_FLAG] = True # enable model/field deletion
+
+        ids_set = set(ids)
+        wkf_todo = []
+        to_unlink = []
+        ids.sort()
+        ids.reverse()
+        for data in self.browse(cr, uid, ids, context):
+            model = data.model
+            res_id = data.res_id
+            model_obj = self.pool.get(model)
+            name = tools.ustr(data.name)
+
+            pair_to_unlink = (model, res_id)
+            if pair_to_unlink not in to_unlink:
+                to_unlink.append(pair_to_unlink)
+
+            if model == 'workflow.activity':
+                # Special treatment for workflow activities: temporarily revert their
+                # incoming transition and trigger an update to force all workflow items
+                # to move out before deleting them
+                cr.execute('select res_type,res_id from wkf_instance where id IN (select inst_id from wkf_workitem where act_id=%s)', (res_id,))
+                wkf_todo.extend(cr.fetchall())
+                cr.execute("update wkf_transition set condition='True', group_id=NULL, signal=NULL,act_to=act_from,act_from=%s where act_to=%s", (res_id,res_id))
+
+        wf_service = netsvc.LocalService("workflow")
+        for model,res_id in wkf_todo:
+            try:
+                wf_service.trg_write(uid, model, res_id, cr)
+            except:
+                _logger.info('Unable to force processing of workflow for item %s@%s in order to leave activity to be deleted', res_id, model)
+
+        def unlink_if_refcount(to_unlink):
+            for model, res_id in to_unlink:
+                external_ids = self.search(cr, uid, [('model', '=', model),('res_id', '=', res_id)])
+                if (set(external_ids)-ids_set):
+                    # if other modules have defined this record, we must not delete it
+                    continue
+                _logger.info('Deleting %s@%s', res_id, model)
+                try:
+                    self.pool.get(model).unlink(cr, uid, [res_id], context=context)
+                except:
+                    _logger.info('Unable to delete %s@%s', res_id, model, exc_info=True)
+
+        # Remove non-model records first, then model fields, and finish with models
+        unlink_if_refcount((model, res_id) for model, res_id in to_unlink
+                                if model not in ('ir.model','ir.model.fields'))
+        unlink_if_refcount((model, res_id) for model, res_id in to_unlink
+                                if model == 'ir.model.fields')
+
+        ir_model_relation = self.pool.get('ir.model.relation')
+        relation_ids = ir_model_relation.search(cr, uid, [('module', 'in', modules_to_remove)])
+        ir_model_relation._module_data_uninstall(cr, uid, relation_ids, context)
+
+        unlink_if_refcount((model, res_id) for model, res_id in to_unlink
+                                if model == 'ir.model')
+
+        cr.commit()
+
+        self.unlink(cr, uid, ids, context)
+
     def _process_end(self, cr, uid, modules):
+        """ Clear records removed from updated module data.
+        This method is called at the end of the module loading process.
+        It is meant to removed records that are no longer present in the
+        updated data. Such records are recognised as the one with an xml id
+        and a module in ir_model_data and noupdate set to false, but not
+        present in self.loads.
+        """
         if not modules:
             return True
-        modules = list(modules)
-        module_in = ",".join(["%s"] * len(modules))
-        cr.execute('select id,name,model,res_id,module from ir_model_data where module IN (' + module_in + ') and noupdate=%s', modules + [False])
-        wkf_todo = []
-        for (id, name, model, res_id,module) in cr.fetchall():
+        to_unlink = []
+        cr.execute("""SELECT id,name,model,res_id,module FROM ir_model_data
+                      WHERE module IN %s AND res_id IS NOT NULL AND noupdate=%s""",
+                      (tuple(modules), False))
+        for (id, name, model, res_id, module) in cr.fetchall():
             if (module,name) not in self.loads:
-                self.unlink_mark[(model,res_id)] = id
-                if model=='workflow.activity':
-                    cr.execute('select res_type,res_id from wkf_instance where id IN (select inst_id from wkf_workitem where act_id=%s)', (res_id,))
-                    wkf_todo.extend(cr.fetchall())
-                    cr.execute("update wkf_transition set condition='True', group_id=NULL, signal=NULL,act_to=act_from,act_from=%s where act_to=%s", (res_id,res_id))
-                    cr.execute("delete from wkf_transition where act_to=%s", (res_id,))
-
-        for model,id in wkf_todo:
-            wf_service = netsvc.LocalService("workflow")
-            wf_service.trg_write(uid, model, id, cr)
-
-        cr.commit()
+                to_unlink.append((model,res_id))
         if not config.get('import_partial'):
-            for (model, res_id) in self.unlink_mark.keys():
+            for (model, res_id) in to_unlink:
                 if self.pool.get(model):
-                    self.__logger.info('Deleting %s@%s', res_id, model)
-                    try:
-                        self.pool.get(model).unlink(cr, uid, [res_id])
-                        if id:
-                            ids = self.search(cr, uid, [('res_id','=',res_id),
-                                                        ('model','=',model)])
-                            self.__logger.debug('=> Deleting %s: %s',
-                                                self._name, ids)
-                            if len(ids) > 1 and \
-                               self.__logger.isEnabledFor(logging.WARNING):
-                                self.__logger.warn(
-                                    'Got %d %s for (%s, %d): %s',
-                                    len(ids), self._name, model, res_id,
-                                    map(itemgetter('module','name'),
-                                        self.read(cr, uid, ids,
-                                                  ['name', 'module'])))
-                            self.unlink(cr, uid, ids)
-                            cr.execute(
-                                'DELETE FROM ir_values WHERE value=%s',
-                                ('%s,%s'%(model, res_id),))
-                        cr.commit()
-                    except Exception:
-                        cr.rollback()
-                        self.__logger.warn(
-                            'Could not delete id: %d of model %s\nThere '
-                            'should be some relation that points to this '
-                            'resource\nYou should manually fix this and '
-                            'restart with --update=module', res_id, model)
-        return True
-ir_model_data()
+                    _logger.info('Deleting %s@%s', res_id, model)
+                    self.pool.get(model).unlink(cr, uid, [res_id])
+
 
 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: