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