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