[FIX] orm: better removal of custom m2m fields
[odoo/odoo.git] / openerp / addons / base / ir / ir_model.py
1 # -*- coding: utf-8 -*-
2
3 ##############################################################################
4 #
5 #    OpenERP, Open Source Business Applications
6 #    Copyright (C) 2004-2012 OpenERP S.A. (<http://openerp.com>).
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU Affero General Public License as
10 #    published by the Free Software Foundation, either version 3 of the
11 #    License, or (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU Affero General Public License for more details.
17 #
18 #    You should have received a copy of the GNU Affero General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22 import logging
23 import re
24 import time
25 import types
26
27 import openerp
28 import openerp.modules.registry
29 from openerp import SUPERUSER_ID
30 from openerp import netsvc, pooler, tools
31 from openerp.osv import fields,osv
32 from openerp.osv.orm import Model
33 from openerp.tools.safe_eval import safe_eval as eval
34 from openerp.tools import config
35 from openerp.tools.translate import _
36 from openerp.osv.orm import except_orm, browse_record, MAGIC_COLUMNS
37
38 _logger = logging.getLogger(__name__)
39
40 MODULE_UNINSTALL_FLAG = '_force_unlink'
41
42 def _get_fields_type(self, cr, uid, context=None):
43     # Avoid too many nested `if`s below, as RedHat's Python 2.6
44     # break on it. See bug 939653.
45     return sorted([(k,k) for k,v in fields.__dict__.iteritems()
46                       if type(v) == types.TypeType and \
47                          issubclass(v, fields._column) and \
48                          v != fields._column and \
49                          not v._deprecated and \
50                          not issubclass(v, fields.function)])
51
52 def _in_modules(self, cr, uid, ids, field_name, arg, context=None):
53     #pseudo-method used by fields.function in ir.model/ir.model.fields
54     module_pool = self.pool.get("ir.module.module")
55     installed_module_ids = module_pool.search(cr, uid, [('state','=','installed')])
56     installed_module_names = module_pool.read(cr, uid, installed_module_ids, ['name'], context=context)
57     installed_modules = set(x['name'] for x in installed_module_names)
58
59     result = {}
60     xml_ids = osv.osv._get_xml_ids(self, cr, uid, ids)
61     for k,v in xml_ids.iteritems():
62         result[k] = ', '.join(sorted(installed_modules & set(xml_id.split('.')[0] for xml_id in v)))
63     return result
64
65 class ir_model(osv.osv):
66     _name = 'ir.model'
67     _description = "Models"
68     _order = 'model'
69
70     def _is_osv_memory(self, cr, uid, ids, field_name, arg, context=None):
71         models = self.browse(cr, uid, ids, context=context)
72         res = dict.fromkeys(ids)
73         for model in models:
74             if self.pool.get(model.model):
75                 res[model.id] = self.pool.get(model.model).is_transient()
76             else:
77                 _logger.error('Missing model %s' % (model.model, ))
78         return res
79
80     def _search_osv_memory(self, cr, uid, model, name, domain, context=None):
81         if not domain:
82             return []
83         __, operator, value = domain[0]
84         if operator not in ['=', '!=']:
85             raise osv.except_osv(_("Invalid Search Criteria"), _('The osv_memory field can only be compared with = and != operator.'))
86         value = bool(value) if operator == '=' else not bool(value)
87         all_model_ids = self.search(cr, uid, [], context=context)
88         is_osv_mem = self._is_osv_memory(cr, uid, all_model_ids, 'osv_memory', arg=None, context=context)
89         return [('id', 'in', [id for id in is_osv_mem if bool(is_osv_mem[id]) == value])]
90
91     def _view_ids(self, cr, uid, ids, field_name, arg, context=None):
92         models = self.browse(cr, uid, ids)
93         res = {}
94         for model in models:
95             res[model.id] = self.pool.get("ir.ui.view").search(cr, uid, [('model', '=', model.model)])
96         return res
97
98     _columns = {
99         'name': fields.char('Model Description', size=64, translate=True, required=True),
100         'model': fields.char('Model', size=64, required=True, select=1),
101         'info': fields.text('Information'),
102         'field_id': fields.one2many('ir.model.fields', 'model_id', 'Fields', required=True),
103         'state': fields.selection([('manual','Custom Object'),('base','Base Object')],'Type',readonly=True),
104         'access_ids': fields.one2many('ir.model.access', 'model_id', 'Access'),
105         'osv_memory': fields.function(_is_osv_memory, string='Transient Model', type='boolean',
106             fnct_search=_search_osv_memory,
107             help="This field specifies whether the model is transient or not (i.e. if records are automatically deleted from the database or not)"),
108         'modules': fields.function(_in_modules, type='char', size=128, string='In Modules', help='List of modules in which the object is defined or inherited'),
109         'view_ids': fields.function(_view_ids, type='one2many', obj='ir.ui.view', string='Views'),
110     }
111
112     _defaults = {
113         'model': 'x_',
114         'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
115     }
116
117     def _check_model_name(self, cr, uid, ids, context=None):
118         for model in self.browse(cr, uid, ids, context=context):
119             if model.state=='manual':
120                 if not model.model.startswith('x_'):
121                     return False
122             if not re.match('^[a-z_A-Z0-9.]+$',model.model):
123                 return False
124         return True
125
126     def _model_name_msg(self, cr, uid, ids, context=None):
127         return _('The Object name must start with x_ and not contain any special character !')
128
129     _constraints = [
130         (_check_model_name, _model_name_msg, ['model']),
131     ]
132     _sql_constraints = [
133         ('obj_name_uniq', 'unique (model)', 'Each model must be unique!'),
134     ]
135
136     # overridden to allow searching both on model name (model field)
137     # and model description (name field)
138     def _name_search(self, cr, uid, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None):
139         if args is None:
140             args = []
141         domain = args + ['|', ('model', operator, name), ('name', operator, name)]
142         return self.name_get(cr, name_get_uid or uid,
143                              super(ir_model, self).search(cr, uid, domain, limit=limit, context=context),
144                              context=context)
145
146     def _drop_table(self, cr, uid, ids, context=None):
147         for model in self.browse(cr, uid, ids, context):
148             model_pool = self.pool.get(model.model)
149             cr.execute('select relkind from pg_class where relname=%s', (model_pool._table,))
150             result = cr.fetchone()
151             if result and result[0] == 'v':
152                 cr.execute('DROP view %s' % (model_pool._table,))
153             elif result and result[0] == 'r':
154                 cr.execute('DROP TABLE %s CASCADE' % (model_pool._table,))
155         return True
156
157     def unlink(self, cr, user, ids, context=None):
158         # Prevent manual deletion of module tables
159         if context is None: context = {}
160         if isinstance(ids, (int, long)):
161             ids = [ids]
162         if not context.get(MODULE_UNINSTALL_FLAG):
163             for model in self.browse(cr, user, ids, context):
164                 if model.state != 'manual':
165                     raise except_orm(_('Error'), _("Model '%s' contains module data and cannot be removed!") % (model.name,))
166
167         self._drop_table(cr, user, ids, context)
168         res = super(ir_model, self).unlink(cr, user, ids, context)
169         if not context.get(MODULE_UNINSTALL_FLAG):
170             # only reload pool for normal unlink. For module uninstall the
171             # reload is done independently in openerp.modules.loading
172             cr.commit() # must be committed before reloading registry in new cursor
173             pooler.restart_pool(cr.dbname)
174             openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
175
176         return res
177
178     def write(self, cr, user, ids, vals, context=None):
179         if context:
180             context.pop('__last_update', None)
181         # Filter out operations 4 link from field id, because openerp-web
182         # always write (4,id,False) even for non dirty items
183         if 'field_id' in vals:
184             vals['field_id'] = [op for op in vals['field_id'] if op[0] != 4]
185         return super(ir_model,self).write(cr, user, ids, vals, context)
186
187     def create(self, cr, user, vals, context=None):
188         if  context is None:
189             context = {}
190         if context and context.get('manual'):
191             vals['state']='manual'
192         res = super(ir_model,self).create(cr, user, vals, context)
193         if vals.get('state','base')=='manual':
194             self.instanciate(cr, user, vals['model'], context)
195             ctx = dict(context,
196                 field_name=vals['name'],
197                 field_state='manual',
198                 select=vals.get('select_level', '0'),
199                 update_custom_fields=True)
200             self.pool.get(vals['model'])._auto_init(cr, ctx)
201             self.pool.get(vals['model'])._auto_end(cr, ctx) # actually create FKs!
202             openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
203         return res
204
205     def instanciate(self, cr, user, model, context=None):
206         class x_custom_model(osv.osv):
207             _custom = True
208         x_custom_model._name = model
209         x_custom_model._module = False
210         a = x_custom_model.create_instance(self.pool, cr)
211         if not a._columns:
212             x_name = 'id'
213         elif 'x_name' in a._columns.keys():
214             x_name = 'x_name'
215         else:
216             x_name = a._columns.keys()[0]
217         x_custom_model._rec_name = x_name
218         a._rec_name = x_name
219
220 class ir_model_fields(osv.osv):
221     _name = 'ir.model.fields'
222     _description = "Fields"
223
224     _columns = {
225         'name': fields.char('Name', required=True, size=64, select=1),
226         'model': fields.char('Object Name', size=64, required=True, select=1,
227             help="The technical name of the model this field belongs to"),
228         'relation': fields.char('Object Relation', size=64,
229             help="For relationship fields, the technical name of the target model"),
230         'relation_field': fields.char('Relation Field', size=64,
231             help="For one2many fields, the field on the target model that implement the opposite many2one relationship"),
232         'model_id': fields.many2one('ir.model', 'Model', required=True, select=True, ondelete='cascade',
233             help="The model this field belongs to"),
234         'field_description': fields.char('Field Label', required=True, size=256),
235         'ttype': fields.selection(_get_fields_type, 'Field Type',size=64, required=True),
236         'selection': fields.char('Selection Options',size=128, help="List of options for a selection field, "
237             "specified as a Python expression defining a list of (key, label) pairs. "
238             "For example: [('blue','Blue'),('yellow','Yellow')]"),
239         'required': fields.boolean('Required'),
240         'readonly': fields.boolean('Readonly'),
241         'select_level': fields.selection([('0','Not Searchable'),('1','Always Searchable'),('2','Advanced Search (deprecated)')],'Searchable', required=True),
242         'translate': fields.boolean('Translatable', help="Whether values for this field can be translated (enables the translation mechanism for that field)"),
243         'size': fields.integer('Size'),
244         'state': fields.selection([('manual','Custom Field'),('base','Base Field')],'Type', required=True, readonly=True, select=1),
245         'on_delete': fields.selection([('cascade','Cascade'),('set null','Set NULL')], 'On Delete', help='On delete property for many2one fields'),
246         'domain': fields.char('Domain', size=256, help="The optional domain to restrict possible values for relationship fields, "
247             "specified as a Python expression defining a list of triplets. "
248             "For example: [('color','=','red')]"),
249         'groups': fields.many2many('res.groups', 'ir_model_fields_group_rel', 'field_id', 'group_id', 'Groups'),
250         'view_load': fields.boolean('View Auto-Load'),
251         'selectable': fields.boolean('Selectable'),
252         'modules': fields.function(_in_modules, type='char', size=128, string='In Modules', help='List of modules in which the field is defined'),
253         'serialization_field_id': fields.many2one('ir.model.fields', 'Serialization Field', domain = "[('ttype','=','serialized')]",
254                                                   ondelete='cascade', help="If set, this field will be stored in the sparse "
255                                                                            "structure of the serialization field, instead "
256                                                                            "of having its own database column. This cannot be "
257                                                                            "changed after creation."),
258     }
259     _rec_name='field_description'
260     _defaults = {
261         'view_load': 0,
262         'selection': "",
263         'domain': "[]",
264         'name': 'x_',
265         'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
266         'on_delete': 'set null',
267         'select_level': '0',
268         'field_description': '',
269         'selectable': 1,
270     }
271     _order = "name"
272
273     def _check_selection(self, cr, uid, selection, context=None):
274         try:
275             selection_list = eval(selection)
276         except Exception:
277             _logger.warning('Invalid selection list definition for fields.selection', exc_info=True)
278             raise except_orm(_('Error'),
279                     _("The Selection Options expression is not a valid Pythonic expression."
280                       "Please provide an expression in the [('key','Label'), ...] format."))
281
282         check = True
283         if not (isinstance(selection_list, list) and selection_list):
284             check = False
285         else:
286             for item in selection_list:
287                 if not (isinstance(item, (tuple,list)) and len(item) == 2):
288                     check = False
289                     break
290
291         if not check:
292                 raise except_orm(_('Error'),
293                     _("The Selection Options expression is must be in the [('key','Label'), ...] format!"))
294         return True
295
296     def _size_gt_zero_msg(self, cr, user, ids, context=None):
297         return _('Size of the field can never be less than 0 !')
298
299     _sql_constraints = [
300         ('size_gt_zero', 'CHECK (size>=0)',_size_gt_zero_msg ),
301     ]
302
303     def _drop_column(self, cr, uid, ids, context=None):
304         for field in self.browse(cr, uid, ids, context):
305             if field.name in MAGIC_COLUMNS:
306                 continue
307             model = self.pool.get(field.model)
308             cr.execute('select relkind from pg_class where relname=%s', (model._table,))
309             result = cr.fetchone()
310             cr.execute("SELECT column_name FROM information_schema.columns WHERE table_name ='%s' and column_name='%s'" %(model._table, field.name))
311             column_name = cr.fetchone()
312             if column_name and (result and result[0] == 'r'):
313                 cr.execute('ALTER table "%s" DROP column "%s" cascade' % (model._table, field.name))
314             model._columns.pop(field.name, None)
315
316             # remove m2m relation table for custom fields
317             # we consider the m2m relation is only one way as it's not possible
318             # to specify the relation table in the interface for custom fields
319             # TODO master: maybe use ir.model.relations for custom fields
320             if field.state == 'manual' and field.ttype == 'many2many':
321                 rel_name = self.pool[field.model]._all_columns[field.name].column._rel
322                 cr.execute('DROP table "%s"' % (rel_name))
323         return True
324
325     def unlink(self, cr, user, ids, context=None):
326         # Prevent manual deletion of module columns
327         if context is None: context = {}
328         if isinstance(ids, (int, long)):
329             ids = [ids]
330         if not context.get(MODULE_UNINSTALL_FLAG) and \
331                 any(field.state != 'manual' for field in self.browse(cr, user, ids, context)):
332             raise except_orm(_('Error'), _("This column contains module data and cannot be removed!"))
333
334         self._drop_column(cr, user, ids, context)
335         res = super(ir_model_fields, self).unlink(cr, user, ids, context)
336         if not context.get(MODULE_UNINSTALL_FLAG):
337             cr.commit()
338             openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
339         return res
340
341     def create(self, cr, user, vals, context=None):
342         if 'model_id' in vals:
343             model_data = self.pool.get('ir.model').browse(cr, user, vals['model_id'])
344             vals['model'] = model_data.model
345         if context is None:
346             context = {}
347         if context and context.get('manual',False):
348             vals['state'] = 'manual'
349         if vals.get('ttype', False) == 'selection':
350             if not vals.get('selection',False):
351                 raise except_orm(_('Error'), _('For selection fields, the Selection Options must be given!'))
352             self._check_selection(cr, user, vals['selection'], context=context)
353         res = super(ir_model_fields,self).create(cr, user, vals, context)
354         if vals.get('state','base') == 'manual':
355             if not vals['name'].startswith('x_'):
356                 raise except_orm(_('Error'), _("Custom fields must have a name that starts with 'x_' !"))
357
358             if vals.get('relation',False) and not self.pool.get('ir.model').search(cr, user, [('model','=',vals['relation'])]):
359                 raise except_orm(_('Error'), _("Model %s does not exist!") % vals['relation'])
360
361             if self.pool.get(vals['model']):
362                 if vals['model'].startswith('x_') and vals['name'] == 'x_name':
363                     self.pool[vals['model']]._rec_name = 'x_name'
364                 self.pool.get(vals['model']).__init__(self.pool, cr)
365                 #Added context to _auto_init for special treatment to custom field for select_level
366                 ctx = dict(context,
367                     field_name=vals['name'],
368                     field_state='manual',
369                     select=vals.get('select_level', '0'),
370                     update_custom_fields=True)
371                 self.pool.get(vals['model'])._auto_init(cr, ctx)
372                 self.pool.get(vals['model'])._auto_end(cr, ctx) # actually create FKs!
373                 openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
374
375         return res
376
377     def write(self, cr, user, ids, vals, context=None):
378         if context is None:
379             context = {}
380         if context and context.get('manual',False):
381             vals['state'] = 'manual'
382
383         #For the moment renaming a sparse field or changing the storing system is not allowed. This may be done later
384         if 'serialization_field_id' in vals or 'name' in vals:
385             for field in self.browse(cr, user, ids, context=context):
386                 if 'serialization_field_id' in vals and field.serialization_field_id.id != vals['serialization_field_id']:
387                     raise except_orm(_('Error!'),  _('Changing the storing system for field "%s" is not allowed.')%field.name)
388                 if field.serialization_field_id and (field.name != vals['name']):
389                     raise except_orm(_('Error!'),  _('Renaming sparse field "%s" is not allowed')%field.name)
390
391         column_rename = None # if set, *one* column can be renamed here
392         obj = None
393         models_patch = {}    # structs of (obj, [(field, prop, change_to),..])
394                              # data to be updated on the orm model
395
396         # static table of properties
397         model_props = [ # (our-name, fields.prop, set_fn)
398             ('field_description', 'string', tools.ustr),
399             ('required', 'required', bool),
400             ('readonly', 'readonly', bool),
401             ('domain', '_domain', eval),
402             ('size', 'size', int),
403             ('on_delete', 'ondelete', str),
404             ('translate', 'translate', bool),
405             ('view_load', 'view_load', bool),
406             ('selectable', 'selectable', bool),
407             ('select_level', 'select', int),
408             ('selection', 'selection', eval),
409             ]
410
411         if vals and ids:
412             checked_selection = False # need only check it once, so defer
413
414             for item in self.browse(cr, user, ids, context=context):
415                 if not (obj and obj._name == item.model):
416                     obj = self.pool.get(item.model)
417
418                 if item.state != 'manual':
419                     raise except_orm(_('Error!'),
420                         _('Properties of base fields cannot be altered in this manner! '
421                           'Please modify them through Python code, '
422                           'preferably through a custom addon!'))
423
424                 if item.ttype == 'selection' and 'selection' in vals \
425                         and not checked_selection:
426                     self._check_selection(cr, user, vals['selection'], context=context)
427                     checked_selection = True
428
429                 final_name = item.name
430                 if 'name' in vals and vals['name'] != item.name:
431                     # We need to rename the column
432                     if column_rename:
433                         raise except_orm(_('Error!'), _('Can only rename one column at a time!'))
434                     if vals['name'] in obj._columns:
435                         raise except_orm(_('Error!'), _('Cannot rename column to %s, because that column already exists!') % vals['name'])
436                     if vals.get('state', 'base') == 'manual' and not vals['name'].startswith('x_'):
437                         raise except_orm(_('Error!'), _('New column name must still start with x_ , because it is a custom field!'))
438                     if '\'' in vals['name'] or '"' in vals['name'] or ';' in vals['name']:
439                         raise ValueError('Invalid character in column name')
440                     column_rename = (obj, (obj._table, item.name, vals['name']))
441                     final_name = vals['name']
442
443                 if 'model_id' in vals and vals['model_id'] != item.model_id:
444                     raise except_orm(_("Error!"), _("Changing the model of a field is forbidden!"))
445
446                 if 'ttype' in vals and vals['ttype'] != item.ttype:
447                     raise except_orm(_("Error!"), _("Changing the type of a column is not yet supported. "
448                                 "Please drop it and create it again!"))
449
450                 # We don't check the 'state', because it might come from the context
451                 # (thus be set for multiple fields) and will be ignored anyway.
452                 if obj:
453                     models_patch.setdefault(obj._name, (obj,[]))
454                     # find out which properties (per model) we need to update
455                     for field_name, field_property, set_fn in model_props:
456                         if field_name in vals:
457                             property_value = set_fn(vals[field_name])
458                             if getattr(obj._columns[item.name], field_property) != property_value:
459                                 models_patch[obj._name][1].append((final_name, field_property, property_value))
460                         # our dict is ready here, but no properties are changed so far
461
462         # These shall never be written (modified)
463         for column_name in ('model_id', 'model', 'state'):
464             if column_name in vals:
465                 del vals[column_name]
466
467         res = super(ir_model_fields,self).write(cr, user, ids, vals, context=context)
468
469         if column_rename:
470             cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s"' % column_rename[1])
471             # This is VERY risky, but let us have this feature:
472             # we want to change the key of column in obj._columns dict
473             col = column_rename[0]._columns.pop(column_rename[1][1]) # take object out, w/o copy
474             column_rename[0]._columns[column_rename[1][2]] = col
475
476         if models_patch:
477             # We have to update _columns of the model(s) and then call their
478             # _auto_init to sync the db with the model. Hopefully, since write()
479             # was called earlier, they will be in-sync before the _auto_init.
480             # Anything we don't update in _columns now will be reset from
481             # the model into ir.model.fields (db).
482             ctx = dict(context, select=vals.get('select_level', '0'),
483                        update_custom_fields=True)
484
485             for __, patch_struct in models_patch.items():
486                 obj = patch_struct[0]
487                 for col_name, col_prop, val in patch_struct[1]:
488                     setattr(obj._columns[col_name], col_prop, val)
489                 obj._auto_init(cr, ctx)
490                 obj._auto_end(cr, ctx) # actually create FKs!
491             openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
492         return res
493
494 class ir_model_constraint(Model):
495     """
496     This model tracks PostgreSQL foreign keys and constraints used by OpenERP
497     models.
498     """
499     _name = 'ir.model.constraint'
500     _columns = {
501         'name': fields.char('Constraint', required=True, size=128, select=1,
502             help="PostgreSQL constraint or foreign key name."),
503         'model': fields.many2one('ir.model', string='Model',
504             required=True, select=1),
505         'module': fields.many2one('ir.module.module', string='Module',
506             required=True, select=1),
507         'type': fields.char('Constraint Type', required=True, size=1, select=1,
508             help="Type of the constraint: `f` for a foreign key, "
509                 "`u` for other constraints."),
510         'date_update': fields.datetime('Update Date'),
511         'date_init': fields.datetime('Initialization Date')
512     }
513
514     _sql_constraints = [
515         ('module_name_uniq', 'unique(name, module)',
516             'Constraints with the same name are unique per module.'),
517     ]
518
519     def _module_data_uninstall(self, cr, uid, ids, context=None):
520         """
521         Delete PostgreSQL foreign keys and constraints tracked by this model.
522         """ 
523
524         if uid != SUPERUSER_ID and not self.pool.get('ir.model.access').check_groups(cr, uid, "base.group_system"):
525             raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
526
527         context = dict(context or {})
528
529         ids_set = set(ids)
530         ids.sort()
531         ids.reverse()
532         for data in self.browse(cr, uid, ids, context):
533             model = data.model.model
534             model_obj = self.pool.get(model)
535             name = openerp.tools.ustr(data.name)
536             typ = data.type
537
538             # double-check we are really going to delete all the owners of this schema element
539             cr.execute("""SELECT id from ir_model_constraint where name=%s""", (data.name,))
540             external_ids = [x[0] for x in cr.fetchall()]
541             if set(external_ids)-ids_set:
542                 # as installed modules have defined this element we must not delete it!
543                 continue
544
545             if typ == 'f':
546                 # test if FK exists on this table (it could be on a related m2m table, in which case we ignore it)
547                 cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
548                               WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('f', name, model_obj._table))
549                 if cr.fetchone():
550                     cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),)
551                     _logger.info('Dropped FK CONSTRAINT %s@%s', name, model)
552
553             if typ == 'u':
554                 # test if constraint exists
555                 cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
556                               WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('u', name, model_obj._table))
557                 if cr.fetchone():
558                     cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),)
559                     _logger.info('Dropped CONSTRAINT %s@%s', name, model)
560
561         self.unlink(cr, uid, ids, context)
562
563 class ir_model_relation(Model):
564     """
565     This model tracks PostgreSQL tables used to implement OpenERP many2many
566     relations.
567     """
568     _name = 'ir.model.relation'
569     _columns = {
570         'name': fields.char('Relation Name', required=True, size=128, select=1,
571             help="PostgreSQL table name implementing a many2many relation."),
572         'model': fields.many2one('ir.model', string='Model',
573             required=True, select=1),
574         'module': fields.many2one('ir.module.module', string='Module',
575             required=True, select=1),
576         'date_update': fields.datetime('Update Date'),
577         'date_init': fields.datetime('Initialization Date')
578     }
579
580     def _module_data_uninstall(self, cr, uid, ids, context=None):
581         """
582         Delete PostgreSQL many2many relations tracked by this model.
583         """ 
584
585         if uid != SUPERUSER_ID and not self.pool.get('ir.model.access').check_groups(cr, uid, "base.group_system"):
586             raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
587
588         ids_set = set(ids)
589         to_drop_table = []
590         ids.sort()
591         ids.reverse()
592         for data in self.browse(cr, uid, ids, context):
593             model = data.model
594             name = openerp.tools.ustr(data.name)
595
596             # double-check we are really going to delete all the owners of this schema element
597             cr.execute("""SELECT id from ir_model_relation where name = %s""", (data.name,))
598             external_ids = [x[0] for x in cr.fetchall()]
599             if set(external_ids)-ids_set:
600                 # as installed modules have defined this element we must not delete it!
601                 continue
602
603             cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name=%s", (name,))
604             if cr.fetchone() and not name in to_drop_table:
605                 to_drop_table.append(name)
606
607         self.unlink(cr, uid, ids, context)
608
609         # drop m2m relation tables
610         for table in to_drop_table:
611             cr.execute('DROP TABLE %s CASCADE'% table,)
612             _logger.info('Dropped table %s', table)
613
614         cr.commit()
615
616 class ir_model_access(osv.osv):
617     _name = 'ir.model.access'
618     _columns = {
619         'name': fields.char('Name', size=64, required=True, select=True),
620         '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.'),
621         'model_id': fields.many2one('ir.model', 'Object', required=True, domain=[('osv_memory','=', False)], select=True, ondelete='cascade'),
622         'group_id': fields.many2one('res.groups', 'Group', ondelete='cascade', select=True),
623         'perm_read': fields.boolean('Read Access'),
624         'perm_write': fields.boolean('Write Access'),
625         'perm_create': fields.boolean('Create Access'),
626         'perm_unlink': fields.boolean('Delete Access'),
627     }
628     _defaults = {
629         'active': True,
630     }
631
632     def check_groups(self, cr, uid, group):
633         grouparr  = group.split('.')
634         if not grouparr:
635             return False
636         cr.execute("select 1 from res_groups_users_rel where uid=%s and gid IN (select res_id from ir_model_data where module=%s and name=%s)", (uid, grouparr[0], grouparr[1],))
637         return bool(cr.fetchone())
638
639     def check_group(self, cr, uid, model, mode, group_ids):
640         """ Check if a specific group has the access mode to the specified model"""
641         assert mode in ['read','write','create','unlink'], 'Invalid access mode'
642
643         if isinstance(model, browse_record):
644             assert model._table_name == 'ir.model', 'Invalid model object'
645             model_name = model.name
646         else:
647             model_name = model
648
649         if isinstance(group_ids, (int, long)):
650             group_ids = [group_ids]
651         for group_id in group_ids:
652             cr.execute("SELECT perm_" + mode + " "
653                    "  FROM ir_model_access a "
654                    "  JOIN ir_model m ON (m.id = a.model_id) "
655                    " WHERE m.model = %s AND a.active IS True "
656                    " AND a.group_id = %s", (model_name, group_id)
657                    )
658             r = cr.fetchone()
659             if r is None:
660                 cr.execute("SELECT perm_" + mode + " "
661                        "  FROM ir_model_access a "
662                        "  JOIN ir_model m ON (m.id = a.model_id) "
663                        " WHERE m.model = %s AND a.active IS True "
664                        " AND a.group_id IS NULL", (model_name, )
665                        )
666                 r = cr.fetchone()
667
668             access = bool(r and r[0])
669             if access:
670                 return True
671         # pass no groups -> no access
672         return False
673
674     def group_names_with_access(self, cr, model_name, access_mode):
675         """Returns the names of visible groups which have been granted ``access_mode`` on
676            the model ``model_name``.
677            :rtype: list
678         """
679         assert access_mode in ['read','write','create','unlink'], 'Invalid access mode: %s' % access_mode
680         cr.execute('''SELECT
681                         c.name, g.name
682                       FROM
683                         ir_model_access a
684                         JOIN ir_model m ON (a.model_id=m.id)
685                         JOIN res_groups g ON (a.group_id=g.id)
686                         LEFT JOIN ir_module_category c ON (c.id=g.category_id)
687                       WHERE
688                         m.model=%s AND
689                         a.active IS True AND
690                         a.perm_''' + access_mode, (model_name,))
691         return [('%s/%s' % x) if x[0] else x[1] for x in cr.fetchall()]
692
693     @tools.ormcache()
694     def check(self, cr, uid, model, mode='read', raise_exception=True, context=None):
695         if uid==1:
696             # User root have all accesses
697             # TODO: exclude xml-rpc requests
698             return True
699
700         assert mode in ['read','write','create','unlink'], 'Invalid access mode'
701
702         if isinstance(model, browse_record):
703             assert model._table_name == 'ir.model', 'Invalid model object'
704             model_name = model.model
705         else:
706             model_name = model
707
708         # TransientModel records have no access rights, only an implicit access rule
709         if not self.pool.get(model_name):
710             _logger.error('Missing model %s' % (model_name, ))
711         elif self.pool.get(model_name).is_transient():
712             return True
713
714         # We check if a specific rule exists
715         cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
716                    '  FROM ir_model_access a '
717                    '  JOIN ir_model m ON (m.id = a.model_id) '
718                    '  JOIN res_groups_users_rel gu ON (gu.gid = a.group_id) '
719                    ' WHERE m.model = %s '
720                    '   AND gu.uid = %s '
721                    '   AND a.active IS True '
722                    , (model_name, uid,)
723                    )
724         r = cr.fetchone()[0]
725
726         if r is None:
727             # there is no specific rule. We check the generic rule
728             cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
729                        '  FROM ir_model_access a '
730                        '  JOIN ir_model m ON (m.id = a.model_id) '
731                        ' WHERE a.group_id IS NULL '
732                        '   AND m.model = %s '
733                        '   AND a.active IS True '
734                        , (model_name,)
735                        )
736             r = cr.fetchone()[0]
737
738         if not r and raise_exception:
739             groups = '\n\t'.join('- %s' % g for g in self.group_names_with_access(cr, model_name, mode))
740             msg_heads = {
741                 # Messages are declared in extenso so they are properly exported in translation terms
742                 'read': _("Sorry, you are not allowed to access this document."),
743                 'write':  _("Sorry, you are not allowed to modify this document."),
744                 'create': _("Sorry, you are not allowed to create this kind of document."),
745                 'unlink': _("Sorry, you are not allowed to delete this document."),
746             }
747             if groups:
748                 msg_tail = _("Only users with the following access level are currently allowed to do that") + ":\n%s\n\n(" + _("Document model") + ": %s)"
749                 msg_params = (groups, model_name)
750             else:
751                 msg_tail = _("Please contact your system administrator if you think this is an error.") + "\n\n(" + _("Document model") + ": %s)"
752                 msg_params = (model_name,)
753             _logger.warning('Access Denied by ACLs for operation: %s, uid: %s, model: %s', mode, uid, model_name)
754             msg = '%s %s' % (msg_heads[mode], msg_tail)
755             raise except_orm(_('Access Denied'), msg % msg_params)
756         return r or False
757
758     __cache_clearing_methods = []
759
760     def register_cache_clearing_method(self, model, method):
761         self.__cache_clearing_methods.append((model, method))
762
763     def unregister_cache_clearing_method(self, model, method):
764         try:
765             i = self.__cache_clearing_methods.index((model, method))
766             del self.__cache_clearing_methods[i]
767         except ValueError:
768             pass
769
770     def call_cache_clearing_methods(self, cr):
771         self.check.clear_cache(self)    # clear the cache of check function
772         for model, method in self.__cache_clearing_methods:
773             object_ = self.pool.get(model)
774             if object_:
775                 getattr(object_, method)()
776
777     #
778     # Check rights on actions
779     #
780     def write(self, cr, uid, *args, **argv):
781         self.call_cache_clearing_methods(cr)
782         res = super(ir_model_access, self).write(cr, uid, *args, **argv)
783         return res
784
785     def create(self, cr, uid, *args, **argv):
786         self.call_cache_clearing_methods(cr)
787         res = super(ir_model_access, self).create(cr, uid, *args, **argv)
788         return res
789
790     def unlink(self, cr, uid, *args, **argv):
791         self.call_cache_clearing_methods(cr)
792         res = super(ir_model_access, self).unlink(cr, uid, *args, **argv)
793         return res
794
795 class ir_model_data(osv.osv):
796     """Holds external identifier keys for records in the database.
797        This has two main uses:
798
799            * allows easy data integration with third-party systems,
800              making import/export/sync of data possible, as records
801              can be uniquely identified across multiple systems
802            * allows tracking the origin of data installed by OpenERP
803              modules themselves, thus making it possible to later
804              update them seamlessly.
805     """
806     _name = 'ir.model.data'
807     _order = 'module,model,name'
808     def _display_name_get(self, cr, uid, ids, prop, unknow_none, context=None):
809         result = {}
810         result2 = {}
811         for res in self.browse(cr, uid, ids, context=context):
812             if res.id:
813                 result.setdefault(res.model, {})
814                 result[res.model][res.res_id] = res.id
815             result2[res.id] = False
816
817         for model in result:
818             try:
819                 r = dict(self.pool.get(model).name_get(cr, uid, result[model].keys(), context=context))
820                 for key,val in result[model].items():
821                     result2[val] = r.get(key, False)
822             except:
823                 # some object have no valid name_get implemented, we accept this
824                 pass
825         return result2
826
827     def _complete_name_get(self, cr, uid, ids, prop, unknow_none, context=None):
828         result = {}
829         for res in self.browse(cr, uid, ids, context=context):
830             result[res.id] = (res.module and (res.module + '.') or '')+res.name
831         return result
832
833     _columns = {
834         'name': fields.char('External Identifier', required=True, size=128, select=1,
835                             help="External Key/Identifier that can be used for "
836                                  "data integration with third-party systems"),
837         'complete_name': fields.function(_complete_name_get, type='char', string='Complete ID'),
838         'display_name': fields.function(_display_name_get, type='char', string='Record Name'),
839         'model': fields.char('Model Name', required=True, size=64, select=1),
840         'module': fields.char('Module', required=True, size=64, select=1),
841         'res_id': fields.integer('Record ID', select=1,
842                                  help="ID of the target record in the database"),
843         'noupdate': fields.boolean('Non Updatable'),
844         'date_update': fields.datetime('Update Date'),
845         'date_init': fields.datetime('Init Date')
846     }
847     _defaults = {
848         'date_init': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
849         'date_update': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
850         'noupdate': False,
851         'module': ''
852     }
853     _sql_constraints = [
854         ('module_name_uniq', 'unique(name, module)', 'You cannot have multiple records with the same external ID in the same module!'),
855     ]
856
857     def __init__(self, pool, cr):
858         osv.osv.__init__(self, pool, cr)
859         self.doinit = True
860         # also stored in pool to avoid being discarded along with this osv instance
861         if getattr(pool, 'model_data_reference_ids', None) is None:
862             self.pool.model_data_reference_ids = {}
863
864         self.loads = self.pool.model_data_reference_ids
865
866     def _auto_init(self, cr, context=None):
867         super(ir_model_data, self)._auto_init(cr, context)
868         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_model_data_module_name_index\'')
869         if not cr.fetchone():
870             cr.execute('CREATE INDEX ir_model_data_module_name_index ON ir_model_data (module, name)')
871
872     @tools.ormcache()
873     def _get_id(self, cr, uid, module, xml_id):
874         """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"""
875         ids = self.search(cr, uid, [('module','=',module), ('name','=', xml_id)])
876         if not ids:
877             raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))
878         # the sql constraints ensure us we have only one result
879         return ids[0]
880
881     @tools.ormcache()
882     def get_object_reference(self, cr, uid, module, xml_id):
883         """Returns (model, res_id) corresponding to a given module and xml_id (cached) or raise ValueError if not found"""
884         data_id = self._get_id(cr, uid, module, xml_id)
885         res = self.read(cr, uid, data_id, ['model', 'res_id'])
886         if not res['res_id']:
887             raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))
888         return res['model'], res['res_id']
889
890     def get_object(self, cr, uid, module, xml_id, context=None):
891         """Returns a browsable record for the given module name and xml_id or raise ValueError if not found"""
892         res_model, res_id = self.get_object_reference(cr, uid, module, xml_id)
893         result = self.pool.get(res_model).browse(cr, uid, res_id, context=context)
894         if not result.exists():
895             raise ValueError('No record found for unique ID %s.%s. It may have been deleted.' % (module, xml_id))
896         return result
897
898     def _update_dummy(self,cr, uid, model, module, xml_id=False, store=True):
899         if not xml_id:
900             return False
901         try:
902             id = self.read(cr, uid, [self._get_id(cr, uid, module, xml_id)], ['res_id'])[0]['res_id']
903             self.loads[(module,xml_id)] = (model,id)
904         except:
905             id = False
906         return id
907
908     def clear_caches(self):
909         """ Clears all orm caches on the object's methods
910
911         :returns: itself
912         """
913         self._get_id.clear_cache(self)
914         self.get_object_reference.clear_cache(self)
915         return self
916
917     def unlink(self, cr, uid, ids, context=None):
918         """ Regular unlink method, but make sure to clear the caches. """
919         self.clear_caches()
920         return super(ir_model_data,self).unlink(cr, uid, ids, context=context)
921
922     def _update(self,cr, uid, model, module, values, xml_id=False, store=True, noupdate=False, mode='init', res_id=False, context=None):
923         model_obj = self.pool.get(model)
924         if not context:
925             context = {}
926         # records created during module install should not display the messages of OpenChatter
927         context = dict(context, install_mode=True)
928         if xml_id and ('.' in xml_id):
929             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
930             module, xml_id = xml_id.split('.')
931         if (not xml_id) and (not self.doinit):
932             return False
933         action_id = False
934         if xml_id:
935             cr.execute('''SELECT imd.id, imd.res_id, md.id, imd.model
936                           FROM ir_model_data imd LEFT JOIN %s md ON (imd.res_id = md.id)
937                           WHERE imd.module=%%s AND imd.name=%%s''' % model_obj._table,
938                           (module, xml_id))
939             results = cr.fetchall()
940             for imd_id2,res_id2,real_id2,real_model in results:
941                 if not real_id2:
942                     self._get_id.clear_cache(self, uid, module, xml_id)
943                     self.get_object_reference.clear_cache(self, uid, module, xml_id)
944                     cr.execute('delete from ir_model_data where id=%s', (imd_id2,))
945                     res_id = False
946                 else:
947                     assert model == real_model, "External ID conflict, %s already refers to a `%s` record,"\
948                         " you can't define a `%s` record with this ID." % (xml_id, real_model, model)
949                     res_id,action_id = res_id2,imd_id2
950
951         if action_id and res_id:
952             model_obj.write(cr, uid, [res_id], values, context=context)
953             self.write(cr, uid, [action_id], {
954                 'date_update': time.strftime('%Y-%m-%d %H:%M:%S'),
955                 },context=context)
956         elif res_id:
957             model_obj.write(cr, uid, [res_id], values, context=context)
958             if xml_id:
959                 self.create(cr, uid, {
960                     'name': xml_id,
961                     'model': model,
962                     'module':module,
963                     'res_id':res_id,
964                     'noupdate': noupdate,
965                     },context=context)
966                 if model_obj._inherits:
967                     for table in model_obj._inherits:
968                         inherit_id = model_obj.browse(cr, uid,
969                                 res_id,context=context)[model_obj._inherits[table]]
970                         self.create(cr, uid, {
971                             'name': xml_id + '_' + table.replace('.', '_'),
972                             'model': table,
973                             'module': module,
974                             'res_id': inherit_id.id,
975                             'noupdate': noupdate,
976                             },context=context)
977         else:
978             if mode=='init' or (mode=='update' and xml_id):
979                 res_id = model_obj.create(cr, uid, values, context=context)
980                 if xml_id:
981                     self.create(cr, uid, {
982                         'name': xml_id,
983                         'model': model,
984                         'module': module,
985                         'res_id': res_id,
986                         'noupdate': noupdate
987                         },context=context)
988                     if model_obj._inherits:
989                         for table in model_obj._inherits:
990                             inherit_id = model_obj.browse(cr, uid,
991                                     res_id,context=context)[model_obj._inherits[table]]
992                             self.create(cr, uid, {
993                                 'name': xml_id + '_' + table.replace('.', '_'),
994                                 'model': table,
995                                 'module': module,
996                                 'res_id': inherit_id.id,
997                                 'noupdate': noupdate,
998                                 },context=context)
999         if xml_id and res_id:
1000             self.loads[(module, xml_id)] = (model, res_id)
1001             for table, inherit_field in model_obj._inherits.iteritems():
1002                 inherit_id = model_obj.read(cr, uid, res_id,
1003                         [inherit_field])[inherit_field]
1004                 self.loads[(module, xml_id + '_' + table.replace('.', '_'))] = (table, inherit_id)
1005         return res_id
1006
1007     def ir_set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=None, xml_id=False):
1008         if isinstance(models[0], (list, tuple)):
1009             model,res_id = models[0]
1010         else:
1011             res_id=None
1012             model = models[0]
1013
1014         if res_id:
1015             where = ' and res_id=%s' % (res_id,)
1016         else:
1017             where = ' and (res_id is null)'
1018
1019         if key2:
1020             where += ' and key2=\'%s\'' % (key2,)
1021         else:
1022             where += ' and (key2 is null)'
1023
1024         cr.execute('select * from ir_values where model=%s and key=%s and name=%s'+where,(model, key, name))
1025         res = cr.fetchone()
1026         if not res:
1027             ir_values_obj = pooler.get_pool(cr.dbname).get('ir.values')
1028             ir_values_obj.set(cr, uid, key, key2, name, models, value, replace, isobject, meta)
1029         elif xml_id:
1030             cr.execute('UPDATE ir_values set value=%s WHERE model=%s and key=%s and name=%s'+where,(value, model, key, name))
1031         return True
1032
1033     def _module_data_uninstall(self, cr, uid, modules_to_remove, context=None):
1034         """Deletes all the records referenced by the ir.model.data entries
1035         ``ids`` along with their corresponding database backed (including
1036         dropping tables, columns, FKs, etc, as long as there is no other
1037         ir.model.data entry holding a reference to them (which indicates that
1038         they are still owned by another module). 
1039         Attempts to perform the deletion in an appropriate order to maximize
1040         the chance of gracefully deleting all records.
1041         This step is performed as part of the full uninstallation of a module.
1042         """ 
1043
1044         ids = self.search(cr, uid, [('module', 'in', modules_to_remove)])
1045
1046         if uid != 1 and not self.pool.get('ir.model.access').check_groups(cr, uid, "base.group_system"):
1047             raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
1048
1049         context = dict(context or {})
1050         context[MODULE_UNINSTALL_FLAG] = True # enable model/field deletion
1051
1052         ids_set = set(ids)
1053         wkf_todo = []
1054         to_unlink = []
1055         ids.sort()
1056         ids.reverse()
1057         for data in self.browse(cr, uid, ids, context):
1058             model = data.model
1059             res_id = data.res_id
1060
1061             pair_to_unlink = (model, res_id)
1062             if pair_to_unlink not in to_unlink:
1063                 to_unlink.append(pair_to_unlink)
1064
1065             if model == 'workflow.activity':
1066                 # Special treatment for workflow activities: temporarily revert their
1067                 # incoming transition and trigger an update to force all workflow items
1068                 # to move out before deleting them
1069                 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,))
1070                 wkf_todo.extend(cr.fetchall())
1071                 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))
1072
1073         wf_service = netsvc.LocalService("workflow")
1074         for model,res_id in wkf_todo:
1075             try:
1076                 wf_service.trg_write(uid, model, res_id, cr)
1077             except Exception:
1078                 _logger.info('Unable to force processing of workflow for item %s@%s in order to leave activity to be deleted', res_id, model, exc_info=True)
1079
1080         def unlink_if_refcount(to_unlink):
1081             for model, res_id in to_unlink:
1082                 external_ids = self.search(cr, uid, [('model', '=', model),('res_id', '=', res_id)])
1083                 if set(external_ids)-ids_set:
1084                     # if other modules have defined this record, we must not delete it
1085                     continue
1086                 _logger.info('Deleting %s@%s', res_id, model)
1087                 try:
1088                     cr.execute('SAVEPOINT record_unlink_save')
1089                     self.pool.get(model).unlink(cr, uid, [res_id], context=context)
1090                 except Exception:
1091                     _logger.info('Unable to delete %s@%s', res_id, model, exc_info=True)
1092                     cr.execute('ROLLBACK TO SAVEPOINT record_unlink_save')
1093                 else:
1094                     cr.execute('RELEASE SAVEPOINT record_unlink_save')
1095
1096         # Remove non-model records first, then model fields, and finish with models
1097         unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1098                                 if model not in ('ir.model','ir.model.fields','ir.model.constraint'))
1099         unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1100                                 if model == 'ir.model.constraint')
1101
1102         ir_module_module = self.pool['ir.module.module']
1103         ir_model_constraint = self.pool['ir.model.constraint']
1104         modules_to_remove_ids = ir_module_module.search(cr, uid, [('name', 'in', modules_to_remove)], context=context)
1105         constraint_ids = ir_model_constraint.search(cr, uid, [('module', 'in', modules_to_remove_ids)], context=context)
1106         ir_model_constraint._module_data_uninstall(cr, uid, constraint_ids, context)
1107
1108         unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1109                                 if model == 'ir.model.fields')
1110
1111         ir_model_relation = self.pool.get('ir.model.relation')
1112         relation_ids = ir_model_relation.search(cr, uid, [('module', 'in', modules_to_remove_ids)])
1113         ir_model_relation._module_data_uninstall(cr, uid, relation_ids, context)
1114
1115         unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1116                                 if model == 'ir.model')
1117
1118         cr.commit()
1119
1120         self.unlink(cr, uid, ids, context)
1121
1122     def _process_end(self, cr, uid, modules):
1123         """ Clear records removed from updated module data.
1124         This method is called at the end of the module loading process.
1125         It is meant to removed records that are no longer present in the
1126         updated data. Such records are recognised as the one with an xml id
1127         and a module in ir_model_data and noupdate set to false, but not
1128         present in self.loads.
1129         """
1130         if not modules:
1131             return True
1132         to_unlink = []
1133         cr.execute("""SELECT id,name,model,res_id,module FROM ir_model_data
1134                       WHERE module IN %s AND res_id IS NOT NULL AND noupdate=%s
1135                       ORDER BY id DESC""",
1136                       (tuple(modules), False))
1137         for (id, name, model, res_id, module) in cr.fetchall():
1138             if (module,name) not in self.loads:
1139                 to_unlink.append((model,res_id))
1140         if not config.get('import_partial'):
1141             for (model, res_id) in to_unlink:
1142                 if self.pool.get(model):
1143                     _logger.info('Deleting %s@%s', res_id, model)
1144                     self.pool.get(model).unlink(cr, uid, [res_id])
1145
1146 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: