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