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