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