[MERGE] Forward-port saas-4 up to 5ceded9
[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-2014 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 tools
31 from openerp.osv import fields,osv
32 from openerp.osv.orm import Model, browse_null
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["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 model.model in self.pool:
75                 res[model.id] = self.pool[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["ir.ui.view"].search(cr, uid, [('model', '=', model.model)])
96         return res
97
98     _columns = {
99         'name': fields.char('Model Description', translate=True, required=True),
100         'model': fields.char('Model', 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', 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[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' % (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             openerp.modules.registry.RegistryManager.new(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[vals['model']]._auto_init(cr, ctx)
201             self.pool[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     _rec_name = 'field_description'
224
225     _columns = {
226         'name': fields.char('Name', required=True, select=1),
227         'complete_name': fields.char('Complete Name', select=1),
228         'model': fields.char('Object Name', required=True, select=1,
229             help="The technical name of the model this field belongs to"),
230         'relation': fields.char('Object Relation',
231             help="For relationship fields, the technical name of the target model"),
232         'relation_field': fields.char('Relation Field',
233             help="For one2many fields, the field on the target model that implement the opposite many2one relationship"),
234         'model_id': fields.many2one('ir.model', 'Model', required=True, select=True, ondelete='cascade',
235             help="The model this field belongs to"),
236         'field_description': fields.char('Field Label', required=True),
237         'ttype': fields.selection(_get_fields_type, 'Field Type', required=True),
238         'selection': fields.char('Selection Options', help="List of options for a selection field, "
239             "specified as a Python expression defining a list of (key, label) pairs. "
240             "For example: [('blue','Blue'),('yellow','Yellow')]"),
241         'required': fields.boolean('Required'),
242         'readonly': fields.boolean('Readonly'),
243         'select_level': fields.selection([('0','Not Searchable'),('1','Always Searchable'),('2','Advanced Search (deprecated)')],'Searchable', required=True),
244         'translate': fields.boolean('Translatable', help="Whether values for this field can be translated (enables the translation mechanism for that field)"),
245         'size': fields.integer('Size'),
246         'state': fields.selection([('manual','Custom Field'),('base','Base Field')],'Type', required=True, readonly=True, select=1),
247         'on_delete': fields.selection([('cascade','Cascade'),('set null','Set NULL')], 'On Delete', help='On delete property for many2one fields'),
248         'domain': fields.char('Domain', help="The optional domain to restrict possible values for relationship fields, "
249             "specified as a Python expression defining a list of triplets. "
250             "For example: [('color','=','red')]"),
251         'groups': fields.many2many('res.groups', 'ir_model_fields_group_rel', 'field_id', 'group_id', 'Groups'),
252         'selectable': fields.boolean('Selectable'),
253         'modules': fields.function(_in_modules, type='char', string='In Modules', help='List of modules in which the field is defined'),
254         'serialization_field_id': fields.many2one('ir.model.fields', 'Serialization Field', domain = "[('ttype','=','serialized')]",
255                                                   ondelete='cascade', help="If set, this field will be stored in the sparse "
256                                                                            "structure of the serialization field, instead "
257                                                                            "of having its own database column. This cannot be "
258                                                                            "changed after creation."),
259     }
260     _rec_name='field_description'
261     _defaults = {
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[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         return True
316
317     def unlink(self, cr, user, ids, context=None):
318         # Prevent manual deletion of module columns
319         if context is None: context = {}
320         if isinstance(ids, (int, long)):
321             ids = [ids]
322         if not context.get(MODULE_UNINSTALL_FLAG) and \
323                 any(field.state != 'manual' for field in self.browse(cr, user, ids, context)):
324             raise except_orm(_('Error'), _("This column contains module data and cannot be removed!"))
325
326         self._drop_column(cr, user, ids, context)
327         res = super(ir_model_fields, self).unlink(cr, user, ids, context)
328         if not context.get(MODULE_UNINSTALL_FLAG):
329             cr.commit()
330             openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
331         return res
332
333     def create(self, cr, user, vals, context=None):
334         if 'model_id' in vals:
335             model_data = self.pool['ir.model'].browse(cr, user, vals['model_id'])
336             vals['model'] = model_data.model
337         if context is None:
338             context = {}
339         if context and context.get('manual',False):
340             vals['state'] = 'manual'
341         if vals.get('ttype', False) == 'selection':
342             if not vals.get('selection',False):
343                 raise except_orm(_('Error'), _('For selection fields, the Selection Options must be given!'))
344             self._check_selection(cr, user, vals['selection'], context=context)
345         res = super(ir_model_fields,self).create(cr, user, vals, context)
346         if vals.get('state','base') == 'manual':
347             if not vals['name'].startswith('x_'):
348                 raise except_orm(_('Error'), _("Custom fields must have a name that starts with 'x_' !"))
349
350             if vals.get('relation',False) and not self.pool['ir.model'].search(cr, user, [('model','=',vals['relation'])]):
351                 raise except_orm(_('Error'), _("Model %s does not exist!") % vals['relation'])
352
353             if vals['model'] in self.pool:
354                 if vals['model'].startswith('x_') and vals['name'] == 'x_name':
355                     self.pool[vals['model']]._rec_name = 'x_name'
356                 self.pool[vals['model']].__init__(self.pool, cr)
357                 #Added context to _auto_init for special treatment to custom field for select_level
358                 ctx = dict(context,
359                     field_name=vals['name'],
360                     field_state='manual',
361                     select=vals.get('select_level', '0'),
362                     update_custom_fields=True)
363                 self.pool[vals['model']]._auto_init(cr, ctx)
364                 self.pool[vals['model']]._auto_end(cr, ctx) # actually create FKs!
365                 openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
366
367         return res
368
369     def write(self, cr, user, ids, vals, context=None):
370         if context is None:
371             context = {}
372         if context and context.get('manual',False):
373             vals['state'] = 'manual'
374
375         #For the moment renaming a sparse field or changing the storing system is not allowed. This may be done later
376         if 'serialization_field_id' in vals or 'name' in vals:
377             for field in self.browse(cr, user, ids, context=context):
378                 if 'serialization_field_id' in vals and field.serialization_field_id.id != vals['serialization_field_id']:
379                     raise except_orm(_('Error!'),  _('Changing the storing system for field "%s" is not allowed.')%field.name)
380                 if field.serialization_field_id and (field.name != vals['name']):
381                     raise except_orm(_('Error!'),  _('Renaming sparse field "%s" is not allowed')%field.name)
382
383         column_rename = None # if set, *one* column can be renamed here
384         models_patch = {}    # structs of (obj, [(field, prop, change_to),..])
385                              # data to be updated on the orm model
386
387         # static table of properties
388         model_props = [ # (our-name, fields.prop, set_fn)
389             ('field_description', 'string', tools.ustr),
390             ('required', 'required', bool),
391             ('readonly', 'readonly', bool),
392             ('domain', '_domain', eval),
393             ('size', 'size', int),
394             ('on_delete', 'ondelete', str),
395             ('translate', 'translate', bool),
396             ('selectable', 'selectable', bool),
397             ('select_level', 'select', int),
398             ('selection', 'selection', eval),
399             ]
400
401         if vals and ids:
402             checked_selection = False # need only check it once, so defer
403
404             for item in self.browse(cr, user, ids, context=context):
405                 obj = self.pool.get(item.model)
406
407                 if item.state != 'manual':
408                     raise except_orm(_('Error!'),
409                         _('Properties of base fields cannot be altered in this manner! '
410                           'Please modify them through Python code, '
411                           'preferably through a custom addon!'))
412
413                 if item.ttype == 'selection' and 'selection' in vals \
414                         and not checked_selection:
415                     self._check_selection(cr, user, vals['selection'], context=context)
416                     checked_selection = True
417
418                 final_name = item.name
419                 if 'name' in vals and vals['name'] != item.name:
420                     # We need to rename the column
421                     if column_rename:
422                         raise except_orm(_('Error!'), _('Can only rename one column at a time!'))
423                     if vals['name'] in obj._columns:
424                         raise except_orm(_('Error!'), _('Cannot rename column to %s, because that column already exists!') % vals['name'])
425                     if vals.get('state', 'base') == 'manual' and not vals['name'].startswith('x_'):
426                         raise except_orm(_('Error!'), _('New column name must still start with x_ , because it is a custom field!'))
427                     if '\'' in vals['name'] or '"' in vals['name'] or ';' in vals['name']:
428                         raise ValueError('Invalid character in column name')
429                     column_rename = (obj, (obj._table, item.name, vals['name']))
430                     final_name = vals['name']
431
432                 if 'model_id' in vals and vals['model_id'] != item.model_id:
433                     raise except_orm(_("Error!"), _("Changing the model of a field is forbidden!"))
434
435                 if 'ttype' in vals and vals['ttype'] != item.ttype:
436                     raise except_orm(_("Error!"), _("Changing the type of a column is not yet supported. "
437                                 "Please drop it and create it again!"))
438
439                 # We don't check the 'state', because it might come from the context
440                 # (thus be set for multiple fields) and will be ignored anyway.
441                 if obj is not None:
442                     models_patch.setdefault(obj._name, (obj,[]))
443                     # find out which properties (per model) we need to update
444                     for field_name, field_property, set_fn in model_props:
445                         if field_name in vals:
446                             property_value = set_fn(vals[field_name])
447                             if getattr(obj._columns[item.name], field_property) != property_value:
448                                 models_patch[obj._name][1].append((final_name, field_property, property_value))
449                         # our dict is ready here, but no properties are changed so far
450
451         # These shall never be written (modified)
452         for column_name in ('model_id', 'model', 'state'):
453             if column_name in vals:
454                 del vals[column_name]
455
456         res = super(ir_model_fields,self).write(cr, user, ids, vals, context=context)
457
458         if column_rename:
459             cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s"' % column_rename[1])
460             # This is VERY risky, but let us have this feature:
461             # we want to change the key of column in obj._columns dict
462             col = column_rename[0]._columns.pop(column_rename[1][1]) # take object out, w/o copy
463             column_rename[0]._columns[column_rename[1][2]] = col
464
465         if models_patch:
466             # We have to update _columns of the model(s) and then call their
467             # _auto_init to sync the db with the model. Hopefully, since write()
468             # was called earlier, they will be in-sync before the _auto_init.
469             # Anything we don't update in _columns now will be reset from
470             # the model into ir.model.fields (db).
471             ctx = dict(context, select=vals.get('select_level', '0'),
472                        update_custom_fields=True)
473
474             for __, patch_struct in models_patch.items():
475                 obj = patch_struct[0]
476                 for col_name, col_prop, val in patch_struct[1]:
477                     setattr(obj._columns[col_name], col_prop, val)
478                 obj._auto_init(cr, ctx)
479                 obj._auto_end(cr, ctx) # actually create FKs!
480             openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
481         return res
482
483 class ir_model_constraint(Model):
484     """
485     This model tracks PostgreSQL foreign keys and constraints used by OpenERP
486     models.
487     """
488     _name = 'ir.model.constraint'
489     _columns = {
490         'name': fields.char('Constraint', required=True, select=1,
491             help="PostgreSQL constraint or foreign key name."),
492         'model': fields.many2one('ir.model', string='Model',
493             required=True, select=1),
494         'module': fields.many2one('ir.module.module', string='Module',
495             required=True, select=1),
496         'type': fields.char('Constraint Type', required=True, size=1, select=1,
497             help="Type of the constraint: `f` for a foreign key, "
498                 "`u` for other constraints."),
499         'date_update': fields.datetime('Update Date'),
500         'date_init': fields.datetime('Initialization Date')
501     }
502
503     _sql_constraints = [
504         ('module_name_uniq', 'unique(name, module)',
505             'Constraints with the same name are unique per module.'),
506     ]
507
508     def _module_data_uninstall(self, cr, uid, ids, context=None):
509         """
510         Delete PostgreSQL foreign keys and constraints tracked by this model.
511         """ 
512
513         if uid != SUPERUSER_ID and not self.pool['ir.model.access'].check_groups(cr, uid, "base.group_system"):
514             raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
515
516         context = dict(context or {})
517
518         ids_set = set(ids)
519         ids.sort()
520         ids.reverse()
521         for data in self.browse(cr, uid, ids, context):
522             model = data.model.model
523             model_obj = self.pool[model]
524             name = openerp.tools.ustr(data.name)
525             typ = data.type
526
527             # double-check we are really going to delete all the owners of this schema element
528             cr.execute("""SELECT id from ir_model_constraint where name=%s""", (data.name,))
529             external_ids = [x[0] for x in cr.fetchall()]
530             if set(external_ids)-ids_set:
531                 # as installed modules have defined this element we must not delete it!
532                 continue
533
534             if typ == 'f':
535                 # test if FK exists on this table (it could be on a related m2m table, in which case we ignore it)
536                 cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
537                               WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('f', name, model_obj._table))
538                 if cr.fetchone():
539                     cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),)
540                     _logger.info('Dropped FK CONSTRAINT %s@%s', name, model)
541
542             if typ == 'u':
543                 # test if constraint exists
544                 cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
545                               WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('u', name, model_obj._table))
546                 if cr.fetchone():
547                     cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),)
548                     _logger.info('Dropped CONSTRAINT %s@%s', name, model)
549
550         self.unlink(cr, uid, ids, context)
551
552 class ir_model_relation(Model):
553     """
554     This model tracks PostgreSQL tables used to implement OpenERP many2many
555     relations.
556     """
557     _name = 'ir.model.relation'
558     _columns = {
559         'name': fields.char('Relation Name', required=True, select=1,
560             help="PostgreSQL table name implementing a many2many relation."),
561         'model': fields.many2one('ir.model', string='Model',
562             required=True, select=1),
563         'module': fields.many2one('ir.module.module', string='Module',
564             required=True, select=1),
565         'date_update': fields.datetime('Update Date'),
566         'date_init': fields.datetime('Initialization Date')
567     }
568
569     def _module_data_uninstall(self, cr, uid, ids, context=None):
570         """
571         Delete PostgreSQL many2many relations tracked by this model.
572         """ 
573
574         if uid != SUPERUSER_ID and not self.pool['ir.model.access'].check_groups(cr, uid, "base.group_system"):
575             raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
576
577         ids_set = set(ids)
578         to_drop_table = []
579         ids.sort()
580         ids.reverse()
581         for data in self.browse(cr, uid, ids, context):
582             model = data.model
583             name = openerp.tools.ustr(data.name)
584
585             # double-check we are really going to delete all the owners of this schema element
586             cr.execute("""SELECT id from ir_model_relation where name = %s""", (data.name,))
587             external_ids = [x[0] for x in cr.fetchall()]
588             if set(external_ids)-ids_set:
589                 # as installed modules have defined this element we must not delete it!
590                 continue
591
592             cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name=%s", (name,))
593             if cr.fetchone() and not name in to_drop_table:
594                 to_drop_table.append(name)
595
596         self.unlink(cr, uid, ids, context)
597
598         # drop m2m relation tables
599         for table in to_drop_table:
600             cr.execute('DROP TABLE %s CASCADE'% table,)
601             _logger.info('Dropped table %s', table)
602
603         cr.commit()
604
605 class ir_model_access(osv.osv):
606     _name = 'ir.model.access'
607     _columns = {
608         'name': fields.char('Name', required=True, select=True),
609         '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.'),
610         'model_id': fields.many2one('ir.model', 'Object', required=True, domain=[('osv_memory','=', False)], select=True, ondelete='cascade'),
611         'group_id': fields.many2one('res.groups', 'Group', ondelete='cascade', select=True),
612         'perm_read': fields.boolean('Read Access'),
613         'perm_write': fields.boolean('Write Access'),
614         'perm_create': fields.boolean('Create Access'),
615         'perm_unlink': fields.boolean('Delete Access'),
616     }
617     _defaults = {
618         'active': True,
619     }
620
621     def check_groups(self, cr, uid, group):
622         grouparr  = group.split('.')
623         if not grouparr:
624             return False
625         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],))
626         return bool(cr.fetchone())
627
628     def check_group(self, cr, uid, model, mode, group_ids):
629         """ Check if a specific group has the access mode to the specified model"""
630         assert mode in ['read','write','create','unlink'], 'Invalid access mode'
631
632         if isinstance(model, browse_record):
633             assert model._table_name == 'ir.model', 'Invalid model object'
634             model_name = model.name
635         else:
636             model_name = model
637
638         if isinstance(group_ids, (int, long)):
639             group_ids = [group_ids]
640         for group_id in group_ids:
641             cr.execute("SELECT perm_" + mode + " "
642                    "  FROM ir_model_access a "
643                    "  JOIN ir_model m ON (m.id = a.model_id) "
644                    " WHERE m.model = %s AND a.active IS True "
645                    " AND a.group_id = %s", (model_name, group_id)
646                    )
647             r = cr.fetchone()
648             if r is None:
649                 cr.execute("SELECT perm_" + mode + " "
650                        "  FROM ir_model_access a "
651                        "  JOIN ir_model m ON (m.id = a.model_id) "
652                        " WHERE m.model = %s AND a.active IS True "
653                        " AND a.group_id IS NULL", (model_name, )
654                        )
655                 r = cr.fetchone()
656
657             access = bool(r and r[0])
658             if access:
659                 return True
660         # pass no groups -> no access
661         return False
662
663     def group_names_with_access(self, cr, model_name, access_mode):
664         """Returns the names of visible groups which have been granted ``access_mode`` on
665            the model ``model_name``.
666            :rtype: list
667         """
668         assert access_mode in ['read','write','create','unlink'], 'Invalid access mode: %s' % access_mode
669         cr.execute('''SELECT
670                         c.name, g.name
671                       FROM
672                         ir_model_access a
673                         JOIN ir_model m ON (a.model_id=m.id)
674                         JOIN res_groups g ON (a.group_id=g.id)
675                         LEFT JOIN ir_module_category c ON (c.id=g.category_id)
676                       WHERE
677                         m.model=%s AND
678                         a.active IS True AND
679                         a.perm_''' + access_mode, (model_name,))
680         return [('%s/%s' % x) if x[0] else x[1] for x in cr.fetchall()]
681
682     @tools.ormcache()
683     def check(self, cr, uid, model, mode='read', raise_exception=True, context=None):
684         if uid==1:
685             # User root have all accesses
686             # TODO: exclude xml-rpc requests
687             return True
688
689         assert mode in ['read','write','create','unlink'], 'Invalid access mode'
690
691         if isinstance(model, browse_record):
692             assert model._table_name == 'ir.model', 'Invalid model object'
693             model_name = model.model
694         else:
695             model_name = model
696
697         # TransientModel records have no access rights, only an implicit access rule
698         if model_name not in self.pool:
699             _logger.error('Missing model %s' % (model_name, ))
700         elif self.pool[model_name].is_transient():
701             return True
702
703         # We check if a specific rule exists
704         cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
705                    '  FROM ir_model_access a '
706                    '  JOIN ir_model m ON (m.id = a.model_id) '
707                    '  JOIN res_groups_users_rel gu ON (gu.gid = a.group_id) '
708                    ' WHERE m.model = %s '
709                    '   AND gu.uid = %s '
710                    '   AND a.active IS True '
711                    , (model_name, uid,)
712                    )
713         r = cr.fetchone()[0]
714
715         if r is None:
716             # there is no specific rule. We check the generic rule
717             cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
718                        '  FROM ir_model_access a '
719                        '  JOIN ir_model m ON (m.id = a.model_id) '
720                        ' WHERE a.group_id IS NULL '
721                        '   AND m.model = %s '
722                        '   AND a.active IS True '
723                        , (model_name,)
724                        )
725             r = cr.fetchone()[0]
726
727         if not r and raise_exception:
728             groups = '\n\t'.join('- %s' % g for g in self.group_names_with_access(cr, model_name, mode))
729             msg_heads = {
730                 # Messages are declared in extenso so they are properly exported in translation terms
731                 'read': _("Sorry, you are not allowed to access this document."),
732                 'write':  _("Sorry, you are not allowed to modify this document."),
733                 'create': _("Sorry, you are not allowed to create this kind of document."),
734                 'unlink': _("Sorry, you are not allowed to delete this document."),
735             }
736             if groups:
737                 msg_tail = _("Only users with the following access level are currently allowed to do that") + ":\n%s\n\n(" + _("Document model") + ": %s)"
738                 msg_params = (groups, model_name)
739             else:
740                 msg_tail = _("Please contact your system administrator if you think this is an error.") + "\n\n(" + _("Document model") + ": %s)"
741                 msg_params = (model_name,)
742             _logger.warning('Access Denied by ACLs for operation: %s, uid: %s, model: %s', mode, uid, model_name)
743             msg = '%s %s' % (msg_heads[mode], msg_tail)
744             raise openerp.exceptions.AccessError(msg % msg_params)
745         return r or False
746
747     __cache_clearing_methods = []
748
749     def register_cache_clearing_method(self, model, method):
750         self.__cache_clearing_methods.append((model, method))
751
752     def unregister_cache_clearing_method(self, model, method):
753         try:
754             i = self.__cache_clearing_methods.index((model, method))
755             del self.__cache_clearing_methods[i]
756         except ValueError:
757             pass
758
759     def call_cache_clearing_methods(self, cr):
760         self.check.clear_cache(self)    # clear the cache of check function
761         for model, method in self.__cache_clearing_methods:
762             if model in self.pool:
763                 getattr(self.pool[model], method)()
764
765     #
766     # Check rights on actions
767     #
768     def write(self, cr, uid, *args, **argv):
769         self.call_cache_clearing_methods(cr)
770         res = super(ir_model_access, self).write(cr, uid, *args, **argv)
771         return res
772
773     def create(self, cr, uid, *args, **argv):
774         self.call_cache_clearing_methods(cr)
775         res = super(ir_model_access, self).create(cr, uid, *args, **argv)
776         return res
777
778     def unlink(self, cr, uid, *args, **argv):
779         self.call_cache_clearing_methods(cr)
780         res = super(ir_model_access, self).unlink(cr, uid, *args, **argv)
781         return res
782
783 class ir_model_data(osv.osv):
784     """Holds external identifier keys for records in the database.
785        This has two main uses:
786
787            * allows easy data integration with third-party systems,
788              making import/export/sync of data possible, as records
789              can be uniquely identified across multiple systems
790            * allows tracking the origin of data installed by OpenERP
791              modules themselves, thus making it possible to later
792              update them seamlessly.
793     """
794     _name = 'ir.model.data'
795     _order = 'module,model,name'
796     def _display_name_get(self, cr, uid, ids, prop, unknow_none, context=None):
797         result = {}
798         result2 = {}
799         for res in self.browse(cr, uid, ids, context=context):
800             if res.id:
801                 result.setdefault(res.model, {})
802                 result[res.model][res.res_id] = res.id
803             result2[res.id] = False
804
805         for model in result:
806             try:
807                 r = dict(self.pool[model].name_get(cr, uid, result[model].keys(), context=context))
808                 for key,val in result[model].items():
809                     result2[val] = r.get(key, False)
810             except:
811                 # some object have no valid name_get implemented, we accept this
812                 pass
813         return result2
814
815     def _complete_name_get(self, cr, uid, ids, prop, unknow_none, context=None):
816         result = {}
817         for res in self.browse(cr, uid, ids, context=context):
818             result[res.id] = (res.module and (res.module + '.') or '')+res.name
819         return result
820
821     _columns = {
822         'name': fields.char('External Identifier', required=True, select=1,
823                             help="External Key/Identifier that can be used for "
824                                  "data integration with third-party systems"),
825         'complete_name': fields.function(_complete_name_get, type='char', string='Complete ID'),
826         'display_name': fields.function(_display_name_get, type='char', string='Record Name'),
827         'model': fields.char('Model Name', required=True, select=1),
828         'module': fields.char('Module', required=True, select=1),
829         'res_id': fields.integer('Record ID', select=1,
830                                  help="ID of the target record in the database"),
831         'noupdate': fields.boolean('Non Updatable'),
832         'date_update': fields.datetime('Update Date'),
833         'date_init': fields.datetime('Init Date')
834     }
835     _defaults = {
836         'date_init': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
837         'date_update': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
838         'noupdate': False,
839         'module': ''
840     }
841     _sql_constraints = [
842         ('module_name_uniq', 'unique(name, module)', 'You cannot have multiple records with the same external ID in the same module!'),
843     ]
844
845     def __init__(self, pool, cr):
846         osv.osv.__init__(self, pool, cr)
847         self.doinit = True
848         # also stored in pool to avoid being discarded along with this osv instance
849         if getattr(pool, 'model_data_reference_ids', None) is None:
850             self.pool.model_data_reference_ids = {}
851
852         self.loads = self.pool.model_data_reference_ids
853
854     def _auto_init(self, cr, context=None):
855         super(ir_model_data, self)._auto_init(cr, context)
856         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_model_data_module_name_index\'')
857         if not cr.fetchone():
858             cr.execute('CREATE INDEX ir_model_data_module_name_index ON ir_model_data (module, name)')
859
860     # NEW V8 API
861     @tools.ormcache(skiparg=3)
862     def xmlid_lookup(self, cr, uid, xmlid):
863         """Low level xmlid lookup
864         Return (id, res_model, res_id) or raise ValueError if not found
865         """
866         module, name = xmlid.split('.', 1)
867         ids = self.search(cr, uid, [('module','=',module), ('name','=', name)])
868         if not ids:
869             raise ValueError('External ID not found in the system: %s' % (xmlid))
870         # the sql constraints ensure us we have only one result
871         res = self.read(cr, uid, ids[0], ['model', 'res_id'])
872         if not res['res_id']:
873             raise ValueError('External ID not found in the system: %s' % (xmlid))
874         return ids[0], res['model'], res['res_id']
875     
876     def xmlid_to_res_model_res_id(self, cr, uid, xmlid, raise_if_not_found=False):
877         """ Return (res_model, res_id)"""
878         try:
879             return self.xmlid_lookup(cr, uid, xmlid)[1:3]
880         except ValueError:
881             if raise_if_not_found:
882                 raise
883             return (False, False)
884
885     def xmlid_to_res_id(self, cr, uid, xmlid, raise_if_not_found=False):
886         """ Returns res_id """
887         return self.xmlid_to_res_model_res_id(cr, uid, xmlid, raise_if_not_found)[1]
888
889     def xmlid_to_object(self, cr, uid, xmlid, raise_if_not_found=False, context=None):
890         """ Return a browse_record
891         if not found and raise_if_not_found is True return the browse_null
892         """ 
893         t = self.xmlid_to_res_model_res_id(cr, uid, xmlid, raise_if_not_found)
894         res_model, res_id = t
895
896         if res_model and res_id:
897             record = self.pool[res_model].browse(cr, uid, res_id, context=context)
898             if record.exists():
899                 return record
900             if raise_if_not_found:
901                 raise ValueError('No record found for unique ID %s. It may have been deleted.' % (xml_id))
902         return browse_null()
903
904     # OLD API
905     def _get_id(self, cr, uid, module, xml_id):
906         """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"""
907         return self.xmlid_lookup(cr, uid, "%s.%s" % (module, xml_id))[0]
908
909     def get_object_reference(self, cr, uid, module, xml_id):
910         """Returns (model, res_id) corresponding to a given module and xml_id (cached) or raise ValueError if not found"""
911         return self.xmlid_lookup(cr, uid, "%s.%s" % (module, xml_id))[1:3]
912
913     def check_object_reference(self, cr, uid, module, xml_id, raise_on_access_error=False):
914         """Returns (model, res_id) corresponding to a given module and xml_id (cached), if and only if the user has the necessary access rights
915         to see that object, otherwise raise a ValueError if raise_on_access_error is True or returns a tuple (model found, False)"""
916         model, res_id = self.get_object_reference(cr, uid, module, xml_id)
917         #search on id found in result to check if current user has read access right
918         check_right = self.pool.get(model).search(cr, uid, [('id', '=', res_id)])
919         if check_right:
920             return model, res_id
921         if raise_on_access_error:
922             raise ValueError('Not enough access rights on the external ID: %s.%s' % (module, xml_id))
923         return model, False
924
925     def get_object(self, cr, uid, module, xml_id, context=None):
926         """ Returns a browsable record for the given module name and xml_id.
927             If not found, raise a ValueError or return a browse_null, depending
928             on the value of `raise_exception`.
929         """
930         return self.xmlid_to_object(cr, uid, "%s.%s" % (module, xml_id), raise_if_not_found=True, context=context)
931
932     def _update_dummy(self,cr, uid, model, module, xml_id=False, store=True):
933         if not xml_id:
934             return False
935         try:
936             id = self.read(cr, uid, [self._get_id(cr, uid, module, xml_id)], ['res_id'])[0]['res_id']
937             self.loads[(module,xml_id)] = (model,id)
938         except:
939             id = False
940         return id
941
942     def clear_caches(self):
943         """ Clears all orm caches on the object's methods
944
945         :returns: itself
946         """
947         self.xmlid_lookup.clear_cache(self)
948         return self
949
950     def unlink(self, cr, uid, ids, context=None):
951         """ Regular unlink method, but make sure to clear the caches. """
952         self.clear_caches()
953         return super(ir_model_data,self).unlink(cr, uid, ids, context=context)
954
955     def _update(self,cr, uid, model, module, values, xml_id=False, store=True, noupdate=False, mode='init', res_id=False, context=None):
956         model_obj = self.pool[model]
957         if not context:
958             context = {}
959         # records created during module install should not display the messages of OpenChatter
960         context = dict(context, install_mode=True)
961         if xml_id and ('.' in xml_id):
962             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
963             module, xml_id = xml_id.split('.')
964         if (not xml_id) and (not self.doinit):
965             return False
966         action_id = False
967         if xml_id:
968             cr.execute('''SELECT imd.id, imd.res_id, md.id, imd.model, imd.noupdate
969                           FROM ir_model_data imd LEFT JOIN %s md ON (imd.res_id = md.id)
970                           WHERE imd.module=%%s AND imd.name=%%s''' % model_obj._table,
971                           (module, xml_id))
972             results = cr.fetchall()
973             for imd_id2,res_id2,real_id2,real_model,noupdate_imd in results:
974                 # In update mode, do not update a record if it's ir.model.data is flagged as noupdate
975                 if mode == 'update' and noupdate_imd:
976                     return res_id2
977                 if not real_id2:
978                     self.clear_caches()
979                     cr.execute('delete from ir_model_data where id=%s', (imd_id2,))
980                     res_id = False
981                 else:
982                     assert model == real_model, "External ID conflict, %s already refers to a `%s` record,"\
983                         " you can't define a `%s` record with this ID." % (xml_id, real_model, model)
984                     res_id,action_id = res_id2,imd_id2
985
986         if action_id and res_id:
987             model_obj.write(cr, uid, [res_id], values, context=context)
988             self.write(cr, uid, [action_id], {
989                 'date_update': time.strftime('%Y-%m-%d %H:%M:%S'),
990                 },context=context)
991         elif res_id:
992             model_obj.write(cr, uid, [res_id], values, context=context)
993             if xml_id:
994                 if model_obj._inherits:
995                     for table in model_obj._inherits:
996                         inherit_id = model_obj.browse(cr, uid,
997                                 res_id,context=context)[model_obj._inherits[table]]
998                         self.create(cr, uid, {
999                             'name': xml_id + '_' + table.replace('.', '_'),
1000                             'model': table,
1001                             'module': module,
1002                             'res_id': inherit_id.id,
1003                             'noupdate': noupdate,
1004                             },context=context)
1005                 self.create(cr, uid, {
1006                     'name': xml_id,
1007                     'model': model,
1008                     'module':module,
1009                     'res_id':res_id,
1010                     'noupdate': noupdate,
1011                     },context=context)
1012         else:
1013             if mode=='init' or (mode=='update' and xml_id):
1014                 res_id = model_obj.create(cr, uid, values, context=context)
1015                 if xml_id:
1016                     if model_obj._inherits:
1017                         for table in model_obj._inherits:
1018                             inherit_id = model_obj.browse(cr, uid,
1019                                     res_id,context=context)[model_obj._inherits[table]]
1020                             self.create(cr, uid, {
1021                                 'name': xml_id + '_' + table.replace('.', '_'),
1022                                 'model': table,
1023                                 'module': module,
1024                                 'res_id': inherit_id.id,
1025                                 'noupdate': noupdate,
1026                                 },context=context)
1027                     self.create(cr, uid, {
1028                         'name': xml_id,
1029                         'model': model,
1030                         'module': module,
1031                         'res_id': res_id,
1032                         'noupdate': noupdate
1033                         },context=context)
1034         if xml_id and res_id:
1035             self.loads[(module, xml_id)] = (model, res_id)
1036             for table, inherit_field in model_obj._inherits.iteritems():
1037                 inherit_id = model_obj.read(cr, uid, res_id,
1038                         [inherit_field])[inherit_field]
1039                 self.loads[(module, xml_id + '_' + table.replace('.', '_'))] = (table, inherit_id)
1040         return res_id
1041
1042     def ir_set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=None, xml_id=False):
1043         if isinstance(models[0], (list, tuple)):
1044             model,res_id = models[0]
1045         else:
1046             res_id=None
1047             model = models[0]
1048
1049         if res_id:
1050             where = ' and res_id=%s' % (res_id,)
1051         else:
1052             where = ' and (res_id is null)'
1053
1054         if key2:
1055             where += ' and key2=\'%s\'' % (key2,)
1056         else:
1057             where += ' and (key2 is null)'
1058
1059         cr.execute('select * from ir_values where model=%s and key=%s and name=%s'+where,(model, key, name))
1060         res = cr.fetchone()
1061         if not res:
1062             ir_values_obj = openerp.registry(cr.dbname)['ir.values']
1063             ir_values_obj.set(cr, uid, key, key2, name, models, value, replace, isobject, meta)
1064         elif xml_id:
1065             cr.execute('UPDATE ir_values set value=%s WHERE model=%s and key=%s and name=%s'+where,(value, model, key, name))
1066         return True
1067
1068     def _module_data_uninstall(self, cr, uid, modules_to_remove, context=None):
1069         """Deletes all the records referenced by the ir.model.data entries
1070         ``ids`` along with their corresponding database backed (including
1071         dropping tables, columns, FKs, etc, as long as there is no other
1072         ir.model.data entry holding a reference to them (which indicates that
1073         they are still owned by another module). 
1074         Attempts to perform the deletion in an appropriate order to maximize
1075         the chance of gracefully deleting all records.
1076         This step is performed as part of the full uninstallation of a module.
1077         """ 
1078
1079         ids = self.search(cr, uid, [('module', 'in', modules_to_remove)])
1080
1081         if uid != 1 and not self.pool['ir.model.access'].check_groups(cr, uid, "base.group_system"):
1082             raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
1083
1084         context = dict(context or {})
1085         context[MODULE_UNINSTALL_FLAG] = True # enable model/field deletion
1086
1087         ids_set = set(ids)
1088         wkf_todo = []
1089         to_unlink = []
1090         ids.sort()
1091         ids.reverse()
1092         for data in self.browse(cr, uid, ids, context):
1093             model = data.model
1094             res_id = data.res_id
1095
1096             pair_to_unlink = (model, res_id)
1097             if pair_to_unlink not in to_unlink:
1098                 to_unlink.append(pair_to_unlink)
1099
1100             if model == 'workflow.activity':
1101                 # Special treatment for workflow activities: temporarily revert their
1102                 # incoming transition and trigger an update to force all workflow items
1103                 # to move out before deleting them
1104                 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,))
1105                 wkf_todo.extend(cr.fetchall())
1106                 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))
1107
1108         for model,res_id in wkf_todo:
1109             try:
1110                 openerp.workflow.trg_write(uid, model, res_id, cr)
1111             except Exception:
1112                 _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)
1113
1114         def unlink_if_refcount(to_unlink):
1115             for model, res_id in to_unlink:
1116                 external_ids = self.search(cr, uid, [('model', '=', model),('res_id', '=', res_id)])
1117                 if set(external_ids)-ids_set:
1118                     # if other modules have defined this record, we must not delete it
1119                     continue
1120                 if model == 'ir.model.fields':
1121                     # Don't remove the LOG_ACCESS_COLUMNS unless _log_access
1122                     # has been turned off on the model.
1123                     field = self.pool[model].browse(cr, uid, [res_id], context=context)[0]
1124                     if not field.exists():
1125                         _logger.info('Deleting orphan external_ids %s', external_ids)
1126                         self.unlink(cr, uid, external_ids)
1127                         continue
1128                     if field.name in openerp.osv.orm.LOG_ACCESS_COLUMNS and self.pool[field.model]._log_access:
1129                         continue
1130                     if field.name == 'id':
1131                         continue
1132                 _logger.info('Deleting %s@%s', res_id, model)
1133                 try:
1134                     cr.execute('SAVEPOINT record_unlink_save')
1135                     self.pool[model].unlink(cr, uid, [res_id], context=context)
1136                 except Exception:
1137                     _logger.info('Unable to delete %s@%s', res_id, model, exc_info=True)
1138                     cr.execute('ROLLBACK TO SAVEPOINT record_unlink_save')
1139                 else:
1140                     cr.execute('RELEASE SAVEPOINT record_unlink_save')
1141
1142         # Remove non-model records first, then model fields, and finish with models
1143         unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1144                                 if model not in ('ir.model','ir.model.fields','ir.model.constraint'))
1145         unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1146                                 if model == 'ir.model.constraint')
1147
1148         ir_module_module = self.pool['ir.module.module']
1149         ir_model_constraint = self.pool['ir.model.constraint']
1150         modules_to_remove_ids = ir_module_module.search(cr, uid, [('name', 'in', modules_to_remove)], context=context)
1151         constraint_ids = ir_model_constraint.search(cr, uid, [('module', 'in', modules_to_remove_ids)], context=context)
1152         ir_model_constraint._module_data_uninstall(cr, uid, constraint_ids, context)
1153
1154         unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1155                                 if model == 'ir.model.fields')
1156
1157         ir_model_relation = self.pool['ir.model.relation']
1158         relation_ids = ir_model_relation.search(cr, uid, [('module', 'in', modules_to_remove_ids)])
1159         ir_model_relation._module_data_uninstall(cr, uid, relation_ids, context)
1160
1161         unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1162                                 if model == 'ir.model')
1163
1164         cr.commit()
1165
1166         self.unlink(cr, uid, ids, context)
1167
1168     def _process_end(self, cr, uid, modules):
1169         """ Clear records removed from updated module data.
1170         This method is called at the end of the module loading process.
1171         It is meant to removed records that are no longer present in the
1172         updated data. Such records are recognised as the one with an xml id
1173         and a module in ir_model_data and noupdate set to false, but not
1174         present in self.loads.
1175         """
1176         if not modules:
1177             return True
1178         to_unlink = []
1179         cr.execute("""SELECT id,name,model,res_id,module FROM ir_model_data
1180                       WHERE module IN %s AND res_id IS NOT NULL AND noupdate=%s ORDER BY id DESC""",
1181                       (tuple(modules), False))
1182         for (id, name, model, res_id, module) in cr.fetchall():
1183             if (module,name) not in self.loads:
1184                 to_unlink.append((model,res_id))
1185         if not config.get('import_partial'):
1186             for (model, res_id) in to_unlink:
1187                 if model in self.pool:
1188                     _logger.info('Deleting %s@%s', res_id, model)
1189                     self.pool[model].unlink(cr, uid, [res_id])
1190
1191 class wizard_model_menu(osv.osv_memory):
1192     _name = 'wizard.ir.model.menu.create'
1193     _columns = {
1194         'menu_id': fields.many2one('ir.ui.menu', 'Parent Menu', required=True),
1195         'name': fields.char('Menu Name', size=64, required=True),
1196     }
1197
1198     def menu_create(self, cr, uid, ids, context=None):
1199         if not context:
1200             context = {}
1201         model_pool = self.pool.get('ir.model')
1202         for menu in self.browse(cr, uid, ids, context):
1203             model = model_pool.browse(cr, uid, context.get('model_id'), context=context)
1204             val = {
1205                 'name': menu.name,
1206                 'res_model': model.model,
1207                 'view_type': 'form',
1208                 'view_mode': 'tree,form'
1209             }
1210             action_id = self.pool.get('ir.actions.act_window').create(cr, uid, val)
1211             self.pool.get('ir.ui.menu').create(cr, uid, {
1212                 'name': menu.name,
1213                 'parent_id': menu.menu_id.id,
1214                 'action': 'ir.actions.act_window,%d' % (action_id,),
1215                 'icon': 'STOCK_INDENT'
1216             }, context)
1217         return {'type':'ir.actions.act_window_close'}
1218
1219 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: