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