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