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