1 # -*- coding: utf-8 -*-
3 ##############################################################################
5 # OpenERP, Open Source Business Applications
6 # Copyright (C) 2004-2014 OpenERP S.A. (<http://openerp.com>).
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.
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.
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/>.
21 ##############################################################################
22 from collections import defaultdict
29 import openerp.modules.registry
30 from openerp import SUPERUSER_ID
31 from openerp import tools
32 from openerp.osv import fields, osv
33 from openerp.osv.orm import BaseModel, Model, MAGIC_COLUMNS, except_orm
34 from openerp.tools import config
35 from openerp.tools.safe_eval import safe_eval as eval
36 from openerp.tools.translate import _
38 _logger = logging.getLogger(__name__)
40 MODULE_UNINSTALL_FLAG = '_force_unlink'
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)])
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)
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)))
65 class ir_model(osv.osv):
67 _description = "Models"
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)
74 if model.model in self.pool:
75 res[model.id] = self.pool[model.model].is_transient()
77 _logger.error('Missing model %s' % (model.model, ))
80 def _search_osv_memory(self, cr, uid, model, name, domain, context=None):
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])]
91 def _view_ids(self, cr, uid, ids, field_name, arg, context=None):
92 models = self.browse(cr, uid, ids)
95 res[model.id] = self.pool["ir.ui.view"].search(cr, uid, [('model', '=', model.model)])
98 def _inherited_models(self, cr, uid, ids, field_name, arg, context=None):
100 for model in self.browse(cr, uid, ids, context=context):
102 inherited_models = [model_name for model_name in self.pool[model.model]._inherits]
104 res[model.id] = self.search(cr, uid, [('model', 'in', inherited_models)], context=context)
108 'name': fields.char('Model Description', translate=True, required=True),
109 'model': fields.char('Model', required=True, select=1),
110 'info': fields.text('Information'),
111 'field_id': fields.one2many('ir.model.fields', 'model_id', 'Fields', required=True, copy=True),
112 'inherited_model_ids': fields.function(_inherited_models, type="many2many", obj="ir.model", string="Inherited models",
113 help="The list of models that extends the current model."),
114 'state': fields.selection([('manual','Custom Object'),('base','Base Object')],'Type', readonly=True),
115 'access_ids': fields.one2many('ir.model.access', 'model_id', 'Access'),
116 'osv_memory': fields.function(_is_osv_memory, string='Transient Model', type='boolean',
117 fnct_search=_search_osv_memory,
118 help="This field specifies whether the model is transient or not (i.e. if records are automatically deleted from the database or not)"),
119 'modules': fields.function(_in_modules, type='char', string='In Modules', help='List of modules in which the object is defined or inherited'),
120 'view_ids': fields.function(_view_ids, type='one2many', obj='ir.ui.view', string='Views'),
125 'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
128 def _check_model_name(self, cr, uid, ids, context=None):
129 for model in self.browse(cr, uid, ids, context=context):
130 if model.state=='manual':
131 if not model.model.startswith('x_'):
133 if not re.match('^[a-z_A-Z0-9.]+$',model.model):
137 def _model_name_msg(self, cr, uid, ids, context=None):
138 return _('The Object name must start with x_ and not contain any special character !')
141 (_check_model_name, _model_name_msg, ['model']),
144 ('obj_name_uniq', 'unique (model)', 'Each model must be unique!'),
147 # overridden to allow searching both on model name (model field)
148 # and model description (name field)
149 def _name_search(self, cr, uid, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None):
152 domain = args + ['|', ('model', operator, name), ('name', operator, name)]
153 return self.name_get(cr, name_get_uid or uid,
154 super(ir_model, self).search(cr, uid, domain, limit=limit, context=context),
157 def _drop_table(self, cr, uid, ids, context=None):
158 for model in self.browse(cr, uid, ids, context):
159 model_pool = self.pool[model.model]
160 cr.execute('select relkind from pg_class where relname=%s', (model_pool._table,))
161 result = cr.fetchone()
162 if result and result[0] == 'v':
163 cr.execute('DROP view %s' % (model_pool._table,))
164 elif result and result[0] == 'r':
165 cr.execute('DROP TABLE %s CASCADE' % (model_pool._table,))
168 def unlink(self, cr, user, ids, context=None):
169 # Prevent manual deletion of module tables
170 if context is None: context = {}
171 if isinstance(ids, (int, long)):
173 if not context.get(MODULE_UNINSTALL_FLAG):
174 for model in self.browse(cr, user, ids, context):
175 if model.state != 'manual':
176 raise except_orm(_('Error'), _("Model '%s' contains module data and cannot be removed!") % (model.name,))
178 self._drop_table(cr, user, ids, context)
179 res = super(ir_model, self).unlink(cr, user, ids, context)
180 if not context.get(MODULE_UNINSTALL_FLAG):
181 # only reload pool for normal unlink. For module uninstall the
182 # reload is done independently in openerp.modules.loading
183 cr.commit() # must be committed before reloading registry in new cursor
184 openerp.modules.registry.RegistryManager.new(cr.dbname)
185 openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
189 def write(self, cr, user, ids, vals, context=None):
191 context = dict(context)
192 context.pop('__last_update', None)
193 # Filter out operations 4 link from field id, because openerp-web
194 # always write (4,id,False) even for non dirty items
195 if 'field_id' in vals:
196 vals['field_id'] = [op for op in vals['field_id'] if op[0] != 4]
197 return super(ir_model,self).write(cr, user, ids, vals, context)
199 def create(self, cr, user, vals, context=None):
202 if context and context.get('manual'):
203 vals['state']='manual'
204 res = super(ir_model,self).create(cr, user, vals, context)
205 if vals.get('state','base')=='manual':
206 self.instanciate(cr, user, vals['model'], context)
207 model = self.pool[vals['model']]
208 model._prepare_setup_fields(cr, SUPERUSER_ID)
209 model._setup_fields(cr, SUPERUSER_ID)
211 field_name=vals['name'],
212 field_state='manual',
213 select=vals.get('select_level', '0'),
214 update_custom_fields=True)
215 model._auto_init(cr, ctx)
216 model._auto_end(cr, ctx) # actually create FKs!
217 openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
220 def instanciate(self, cr, user, model, context=None):
221 class x_custom_model(osv.osv):
223 if isinstance(model, unicode):
224 model = model.encode('utf-8')
225 x_custom_model._name = model
226 x_custom_model._module = False
227 a = x_custom_model._build_model(self.pool, cr)
230 elif 'x_name' in a._columns.keys():
233 x_name = a._columns.keys()[0]
234 x_custom_model._rec_name = x_name
237 class ir_model_fields(osv.osv):
238 _name = 'ir.model.fields'
239 _description = "Fields"
240 _rec_name = 'field_description'
243 'name': fields.char('Name', required=True, select=1),
244 'complete_name': fields.char('Complete Name', select=1),
245 'model': fields.char('Object Name', required=True, select=1,
246 help="The technical name of the model this field belongs to"),
247 'relation': fields.char('Object Relation',
248 help="For relationship fields, the technical name of the target model"),
249 'relation_field': fields.char('Relation Field',
250 help="For one2many fields, the field on the target model that implement the opposite many2one relationship"),
251 'model_id': fields.many2one('ir.model', 'Model', required=True, select=True, ondelete='cascade',
252 help="The model this field belongs to"),
253 'field_description': fields.char('Field Label', required=True),
254 'ttype': fields.selection(_get_fields_type, 'Field Type', required=True),
255 'selection': fields.char('Selection Options', help="List of options for a selection field, "
256 "specified as a Python expression defining a list of (key, label) pairs. "
257 "For example: [('blue','Blue'),('yellow','Yellow')]"),
258 'required': fields.boolean('Required'),
259 'readonly': fields.boolean('Readonly'),
260 'select_level': fields.selection([('0','Not Searchable'),('1','Always Searchable'),('2','Advanced Search (deprecated)')],'Searchable', required=True),
261 'translate': fields.boolean('Translatable', help="Whether values for this field can be translated (enables the translation mechanism for that field)"),
262 'size': fields.integer('Size'),
263 'state': fields.selection([('manual','Custom Field'),('base','Base Field')],'Type', required=True, readonly=True, select=1),
264 'on_delete': fields.selection([('cascade', 'Cascade'), ('set null', 'Set NULL'), ('restrict', 'Restrict')],
265 'On Delete', help='On delete property for many2one fields'),
266 'domain': fields.char('Domain', help="The optional domain to restrict possible values for relationship fields, "
267 "specified as a Python expression defining a list of triplets. "
268 "For example: [('color','=','red')]"),
269 'groups': fields.many2many('res.groups', 'ir_model_fields_group_rel', 'field_id', 'group_id', 'Groups'),
270 'selectable': fields.boolean('Selectable'),
271 'modules': fields.function(_in_modules, type='char', string='In Modules', help='List of modules in which the field is defined'),
272 'serialization_field_id': fields.many2one('ir.model.fields', 'Serialization Field', domain = "[('ttype','=','serialized')]",
273 ondelete='cascade', help="If set, this field will be stored in the sparse "
274 "structure of the serialization field, instead "
275 "of having its own database column. This cannot be "
276 "changed after creation."),
278 _rec_name='field_description'
283 'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
284 'on_delete': 'set null',
286 'field_description': '',
291 def _check_selection(self, cr, uid, selection, context=None):
293 selection_list = eval(selection)
295 _logger.warning('Invalid selection list definition for fields.selection', exc_info=True)
296 raise except_orm(_('Error'),
297 _("The Selection Options expression is not a valid Pythonic expression."
298 "Please provide an expression in the [('key','Label'), ...] format."))
301 if not (isinstance(selection_list, list) and selection_list):
304 for item in selection_list:
305 if not (isinstance(item, (tuple,list)) and len(item) == 2):
310 raise except_orm(_('Error'),
311 _("The Selection Options expression is must be in the [('key','Label'), ...] format!"))
314 def _size_gt_zero_msg(self, cr, user, ids, context=None):
315 return _('Size of the field can never be less than 0 !')
318 ('size_gt_zero', 'CHECK (size>=0)',_size_gt_zero_msg ),
321 def _drop_column(self, cr, uid, ids, context=None):
322 for field in self.browse(cr, uid, ids, context):
323 if field.name in MAGIC_COLUMNS:
325 model = self.pool[field.model]
326 cr.execute('select relkind from pg_class where relname=%s', (model._table,))
327 result = cr.fetchone()
328 cr.execute("SELECT column_name FROM information_schema.columns WHERE table_name ='%s' and column_name='%s'" %(model._table, field.name))
329 column_name = cr.fetchone()
330 if column_name and (result and result[0] == 'r'):
331 cr.execute('ALTER table "%s" DROP column "%s" cascade' % (model._table, field.name))
332 model._columns.pop(field.name, None)
334 # remove m2m relation table for custom fields
335 # we consider the m2m relation is only one way as it's not possible
336 # to specify the relation table in the interface for custom fields
337 # TODO master: maybe use ir.model.relations for custom fields
338 if field.state == 'manual' and field.ttype == 'many2many':
339 rel_name = self.pool[field.model]._all_columns[field.name].column._rel
340 cr.execute('DROP table "%s"' % (rel_name))
343 def unlink(self, cr, user, ids, context=None):
344 # Prevent manual deletion of module columns
345 if context is None: context = {}
346 if isinstance(ids, (int, long)):
348 if not context.get(MODULE_UNINSTALL_FLAG) and \
349 any(field.state != 'manual' for field in self.browse(cr, user, ids, context)):
350 raise except_orm(_('Error'), _("This column contains module data and cannot be removed!"))
352 self._drop_column(cr, user, ids, context)
353 res = super(ir_model_fields, self).unlink(cr, user, ids, context)
354 if not context.get(MODULE_UNINSTALL_FLAG):
356 openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
359 def create(self, cr, user, vals, context=None):
360 if 'model_id' in vals:
361 model_data = self.pool['ir.model'].browse(cr, user, vals['model_id'])
362 vals['model'] = model_data.model
365 if context and context.get('manual',False):
366 vals['state'] = 'manual'
367 if vals.get('ttype', False) == 'selection':
368 if not vals.get('selection',False):
369 raise except_orm(_('Error'), _('For selection fields, the Selection Options must be given!'))
370 self._check_selection(cr, user, vals['selection'], context=context)
371 res = super(ir_model_fields,self).create(cr, user, vals, context)
372 if vals.get('state','base') == 'manual':
373 if not vals['name'].startswith('x_'):
374 raise except_orm(_('Error'), _("Custom fields must have a name that starts with 'x_' !"))
376 if vals.get('relation',False) and not self.pool['ir.model'].search(cr, user, [('model','=',vals['relation'])]):
377 raise except_orm(_('Error'), _("Model %s does not exist!") % vals['relation'])
379 if vals['model'] in self.pool:
380 model = self.pool[vals['model']]
381 if vals['model'].startswith('x_') and vals['name'] == 'x_name':
382 model._rec_name = 'x_name'
384 if self.pool.fields_by_model is not None:
385 cr.execute('SELECT * FROM ir_model_fields WHERE id=%s', (res,))
386 self.pool.fields_by_model.setdefault(vals['model'], []).append(cr.dictfetchone())
387 model.__init__(self.pool, cr)
388 model._prepare_setup_fields(cr, SUPERUSER_ID)
389 model._setup_fields(cr, SUPERUSER_ID)
391 #Added context to _auto_init for special treatment to custom field for select_level
393 field_name=vals['name'],
394 field_state='manual',
395 select=vals.get('select_level', '0'),
396 update_custom_fields=True)
397 model._auto_init(cr, ctx)
398 model._auto_end(cr, ctx) # actually create FKs!
399 openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
403 def write(self, cr, user, ids, vals, context=None):
406 if context and context.get('manual',False):
407 vals['state'] = 'manual'
409 #For the moment renaming a sparse field or changing the storing system is not allowed. This may be done later
410 if 'serialization_field_id' in vals or 'name' in vals:
411 for field in self.browse(cr, user, ids, context=context):
412 if 'serialization_field_id' in vals and field.serialization_field_id.id != vals['serialization_field_id']:
413 raise except_orm(_('Error!'), _('Changing the storing system for field "%s" is not allowed.')%field.name)
414 if field.serialization_field_id and (field.name != vals['name']):
415 raise except_orm(_('Error!'), _('Renaming sparse field "%s" is not allowed')%field.name)
417 column_rename = None # if set, *one* column can be renamed here
418 models_patch = {} # structs of (obj, [(field, prop, change_to),..])
419 # data to be updated on the orm model
421 # static table of properties
422 model_props = [ # (our-name, fields.prop, set_fn)
423 ('field_description', 'string', tools.ustr),
424 ('required', 'required', bool),
425 ('readonly', 'readonly', bool),
426 ('domain', '_domain', eval),
427 ('size', 'size', int),
428 ('on_delete', 'ondelete', str),
429 ('translate', 'translate', bool),
430 ('selectable', 'selectable', bool),
431 ('select_level', 'select', int),
432 ('selection', 'selection', eval),
436 checked_selection = False # need only check it once, so defer
438 for item in self.browse(cr, user, ids, context=context):
439 obj = self.pool.get(item.model)
441 if item.state != 'manual':
442 raise except_orm(_('Error!'),
443 _('Properties of base fields cannot be altered in this manner! '
444 'Please modify them through Python code, '
445 'preferably through a custom addon!'))
447 if item.ttype == 'selection' and 'selection' in vals \
448 and not checked_selection:
449 self._check_selection(cr, user, vals['selection'], context=context)
450 checked_selection = True
452 final_name = item.name
453 if 'name' in vals and vals['name'] != item.name:
454 # We need to rename the column
456 raise except_orm(_('Error!'), _('Can only rename one column at a time!'))
457 if vals['name'] in obj._columns:
458 raise except_orm(_('Error!'), _('Cannot rename column to %s, because that column already exists!') % vals['name'])
459 if vals.get('state', 'base') == 'manual' and not vals['name'].startswith('x_'):
460 raise except_orm(_('Error!'), _('New column name must still start with x_ , because it is a custom field!'))
461 if '\'' in vals['name'] or '"' in vals['name'] or ';' in vals['name']:
462 raise ValueError('Invalid character in column name')
463 column_rename = (obj, (obj._table, item.name, vals['name']))
464 final_name = vals['name']
466 if 'model_id' in vals and vals['model_id'] != item.model_id.id:
467 raise except_orm(_("Error!"), _("Changing the model of a field is forbidden!"))
469 if 'ttype' in vals and vals['ttype'] != item.ttype:
470 raise except_orm(_("Error!"), _("Changing the type of a column is not yet supported. "
471 "Please drop it and create it again!"))
473 # We don't check the 'state', because it might come from the context
474 # (thus be set for multiple fields) and will be ignored anyway.
476 models_patch.setdefault(obj._name, (obj,[]))
477 # find out which properties (per model) we need to update
478 for field_name, field_property, set_fn in model_props:
479 if field_name in vals:
480 property_value = set_fn(vals[field_name])
481 if getattr(obj._columns[item.name], field_property) != property_value:
482 models_patch[obj._name][1].append((final_name, field_property, property_value))
483 # our dict is ready here, but no properties are changed so far
485 # These shall never be written (modified)
486 for column_name in ('model_id', 'model', 'state'):
487 if column_name in vals:
488 del vals[column_name]
490 res = super(ir_model_fields,self).write(cr, user, ids, vals, context=context)
493 cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s"' % column_rename[1])
494 # This is VERY risky, but let us have this feature:
495 # we want to change the key of column in obj._columns dict
496 col = column_rename[0]._columns.pop(column_rename[1][1]) # take object out, w/o copy
497 column_rename[0]._columns[column_rename[1][2]] = col
500 # We have to update _columns of the model(s) and then call their
501 # _auto_init to sync the db with the model. Hopefully, since write()
502 # was called earlier, they will be in-sync before the _auto_init.
503 # Anything we don't update in _columns now will be reset from
504 # the model into ir.model.fields (db).
505 ctx = dict(context, select=vals.get('select_level', '0'),
506 update_custom_fields=True)
508 for __, patch_struct in models_patch.items():
509 obj = patch_struct[0]
510 for col_name, col_prop, val in patch_struct[1]:
511 setattr(obj._columns[col_name], col_prop, val)
512 obj._auto_init(cr, ctx)
513 obj._auto_end(cr, ctx) # actually create FKs!
514 openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
517 class ir_model_constraint(Model):
519 This model tracks PostgreSQL foreign keys and constraints used by OpenERP
522 _name = 'ir.model.constraint'
524 'name': fields.char('Constraint', required=True, select=1,
525 help="PostgreSQL constraint or foreign key name."),
526 'model': fields.many2one('ir.model', string='Model',
527 required=True, select=1),
528 'module': fields.many2one('ir.module.module', string='Module',
529 required=True, select=1),
530 'type': fields.char('Constraint Type', required=True, size=1, select=1,
531 help="Type of the constraint: `f` for a foreign key, "
532 "`u` for other constraints."),
533 'date_update': fields.datetime('Update Date'),
534 'date_init': fields.datetime('Initialization Date')
538 ('module_name_uniq', 'unique(name, module)',
539 'Constraints with the same name are unique per module.'),
542 def _module_data_uninstall(self, cr, uid, ids, context=None):
544 Delete PostgreSQL foreign keys and constraints tracked by this model.
547 if uid != SUPERUSER_ID and not self.pool['ir.model.access'].check_groups(cr, uid, "base.group_system"):
548 raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
550 context = dict(context or {})
555 for data in self.browse(cr, uid, ids, context):
556 model = data.model.model
557 model_obj = self.pool[model]
558 name = openerp.tools.ustr(data.name)
561 # double-check we are really going to delete all the owners of this schema element
562 cr.execute("""SELECT id from ir_model_constraint where name=%s""", (data.name,))
563 external_ids = [x[0] for x in cr.fetchall()]
564 if set(external_ids)-ids_set:
565 # as installed modules have defined this element we must not delete it!
569 # test if FK exists on this table (it could be on a related m2m table, in which case we ignore it)
570 cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
571 WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('f', name, model_obj._table))
573 cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),)
574 _logger.info('Dropped FK CONSTRAINT %s@%s', name, model)
577 # test if constraint exists
578 cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
579 WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('u', name, model_obj._table))
581 cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),)
582 _logger.info('Dropped CONSTRAINT %s@%s', name, model)
584 self.unlink(cr, uid, ids, context)
586 class ir_model_relation(Model):
588 This model tracks PostgreSQL tables used to implement OpenERP many2many
591 _name = 'ir.model.relation'
593 'name': fields.char('Relation Name', required=True, select=1,
594 help="PostgreSQL table name implementing a many2many relation."),
595 'model': fields.many2one('ir.model', string='Model',
596 required=True, select=1),
597 'module': fields.many2one('ir.module.module', string='Module',
598 required=True, select=1),
599 'date_update': fields.datetime('Update Date'),
600 'date_init': fields.datetime('Initialization Date')
603 def _module_data_uninstall(self, cr, uid, ids, context=None):
605 Delete PostgreSQL many2many relations tracked by this model.
608 if uid != SUPERUSER_ID and not self.pool['ir.model.access'].check_groups(cr, uid, "base.group_system"):
609 raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
615 for data in self.browse(cr, uid, ids, context):
617 name = openerp.tools.ustr(data.name)
619 # double-check we are really going to delete all the owners of this schema element
620 cr.execute("""SELECT id from ir_model_relation where name = %s""", (data.name,))
621 external_ids = [x[0] for x in cr.fetchall()]
622 if set(external_ids)-ids_set:
623 # as installed modules have defined this element we must not delete it!
626 cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name=%s", (name,))
627 if cr.fetchone() and not name in to_drop_table:
628 to_drop_table.append(name)
630 self.unlink(cr, uid, ids, context)
632 # drop m2m relation tables
633 for table in to_drop_table:
634 cr.execute('DROP TABLE %s CASCADE'% table,)
635 _logger.info('Dropped table %s', table)
639 class ir_model_access(osv.osv):
640 _name = 'ir.model.access'
642 'name': fields.char('Name', required=True, select=True),
643 'active': fields.boolean('Active', help='If you uncheck the active field, it will disable the ACL without deleting it (if you delete a native ACL, it will be re-created when you reload the module.'),
644 'model_id': fields.many2one('ir.model', 'Object', required=True, domain=[('osv_memory','=', False)], select=True, ondelete='cascade'),
645 'group_id': fields.many2one('res.groups', 'Group', ondelete='cascade', select=True),
646 'perm_read': fields.boolean('Read Access'),
647 'perm_write': fields.boolean('Write Access'),
648 'perm_create': fields.boolean('Create Access'),
649 'perm_unlink': fields.boolean('Delete Access'),
655 def check_groups(self, cr, uid, group):
656 grouparr = group.split('.')
659 cr.execute("select 1 from res_groups_users_rel where uid=%s and gid IN (select res_id from ir_model_data where module=%s and name=%s)", (uid, grouparr[0], grouparr[1],))
660 return bool(cr.fetchone())
662 def check_group(self, cr, uid, model, mode, group_ids):
663 """ Check if a specific group has the access mode to the specified model"""
664 assert mode in ['read','write','create','unlink'], 'Invalid access mode'
666 if isinstance(model, BaseModel):
667 assert model._name == 'ir.model', 'Invalid model object'
668 model_name = model.name
672 if isinstance(group_ids, (int, long)):
673 group_ids = [group_ids]
674 for group_id in group_ids:
675 cr.execute("SELECT perm_" + mode + " "
676 " FROM ir_model_access a "
677 " JOIN ir_model m ON (m.id = a.model_id) "
678 " WHERE m.model = %s AND a.active IS True "
679 " AND a.group_id = %s", (model_name, group_id)
683 cr.execute("SELECT perm_" + mode + " "
684 " FROM ir_model_access a "
685 " JOIN ir_model m ON (m.id = a.model_id) "
686 " WHERE m.model = %s AND a.active IS True "
687 " AND a.group_id IS NULL", (model_name, )
691 access = bool(r and r[0])
694 # pass no groups -> no access
697 def group_names_with_access(self, cr, model_name, access_mode):
698 """Returns the names of visible groups which have been granted ``access_mode`` on
699 the model ``model_name``.
702 assert access_mode in ['read','write','create','unlink'], 'Invalid access mode: %s' % access_mode
707 JOIN ir_model m ON (a.model_id=m.id)
708 JOIN res_groups g ON (a.group_id=g.id)
709 LEFT JOIN ir_module_category c ON (c.id=g.category_id)
713 a.perm_''' + access_mode, (model_name,))
714 return [('%s/%s' % x) if x[0] else x[1] for x in cr.fetchall()]
717 def check(self, cr, uid, model, mode='read', raise_exception=True, context=None):
719 # User root have all accesses
720 # TODO: exclude xml-rpc requests
723 assert mode in ['read','write','create','unlink'], 'Invalid access mode'
725 if isinstance(model, BaseModel):
726 assert model._name == 'ir.model', 'Invalid model object'
727 model_name = model.model
731 # TransientModel records have no access rights, only an implicit access rule
732 if model_name not in self.pool:
733 _logger.error('Missing model %s' % (model_name, ))
734 elif self.pool[model_name].is_transient():
737 # We check if a specific rule exists
738 cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
739 ' FROM ir_model_access a '
740 ' JOIN ir_model m ON (m.id = a.model_id) '
741 ' JOIN res_groups_users_rel gu ON (gu.gid = a.group_id) '
742 ' WHERE m.model = %s '
744 ' AND a.active IS True '
750 # there is no specific rule. We check the generic rule
751 cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
752 ' FROM ir_model_access a '
753 ' JOIN ir_model m ON (m.id = a.model_id) '
754 ' WHERE a.group_id IS NULL '
756 ' AND a.active IS True '
761 if not r and raise_exception:
762 groups = '\n\t'.join('- %s' % g for g in self.group_names_with_access(cr, model_name, mode))
764 # Messages are declared in extenso so they are properly exported in translation terms
765 'read': _("Sorry, you are not allowed to access this document."),
766 'write': _("Sorry, you are not allowed to modify this document."),
767 'create': _("Sorry, you are not allowed to create this kind of document."),
768 'unlink': _("Sorry, you are not allowed to delete this document."),
771 msg_tail = _("Only users with the following access level are currently allowed to do that") + ":\n%s\n\n(" + _("Document model") + ": %s)"
772 msg_params = (groups, model_name)
774 msg_tail = _("Please contact your system administrator if you think this is an error.") + "\n\n(" + _("Document model") + ": %s)"
775 msg_params = (model_name,)
776 _logger.warning('Access Denied by ACLs for operation: %s, uid: %s, model: %s', mode, uid, model_name)
777 msg = '%s %s' % (msg_heads[mode], msg_tail)
778 raise openerp.exceptions.AccessError(msg % msg_params)
781 __cache_clearing_methods = []
783 def register_cache_clearing_method(self, model, method):
784 self.__cache_clearing_methods.append((model, method))
786 def unregister_cache_clearing_method(self, model, method):
788 i = self.__cache_clearing_methods.index((model, method))
789 del self.__cache_clearing_methods[i]
793 def call_cache_clearing_methods(self, cr):
794 self.invalidate_cache(cr, SUPERUSER_ID)
795 self.check.clear_cache(self) # clear the cache of check function
796 for model, method in self.__cache_clearing_methods:
797 if model in self.pool:
798 getattr(self.pool[model], method)()
801 # Check rights on actions
803 def write(self, cr, uid, ids, values, context=None):
804 self.call_cache_clearing_methods(cr)
805 res = super(ir_model_access, self).write(cr, uid, ids, values, context=context)
808 def create(self, cr, uid, values, context=None):
809 self.call_cache_clearing_methods(cr)
810 res = super(ir_model_access, self).create(cr, uid, values, context=context)
813 def unlink(self, cr, uid, ids, context=None):
814 self.call_cache_clearing_methods(cr)
815 res = super(ir_model_access, self).unlink(cr, uid, ids, context=context)
818 class ir_model_data(osv.osv):
819 """Holds external identifier keys for records in the database.
820 This has two main uses:
822 * allows easy data integration with third-party systems,
823 making import/export/sync of data possible, as records
824 can be uniquely identified across multiple systems
825 * allows tracking the origin of data installed by OpenERP
826 modules themselves, thus making it possible to later
827 update them seamlessly.
829 _name = 'ir.model.data'
830 _order = 'module,model,name'
832 def name_get(self, cr, uid, ids, context=None):
833 bymodel = defaultdict(dict)
836 for res in self.browse(cr, uid, ids, context=context):
837 bymodel[res.model][res.res_id] = res
838 names[res.id] = res.complete_name
839 #result[res.model][res.res_id] = res.id
841 for model, id_map in bymodel.iteritems():
843 ng = dict(self.pool[model].name_get(cr, uid, id_map.keys(), context=context))
847 for r in id_map.itervalues():
848 names[r.id] = ng.get(r.res_id, r.complete_name)
850 return [(i, names[i]) for i in ids]
852 def _complete_name_get(self, cr, uid, ids, prop, unknow_none, context=None):
854 for res in self.browse(cr, uid, ids, context=context):
855 result[res.id] = (res.module and (res.module + '.') or '')+res.name
859 'name': fields.char('External Identifier', required=True, select=1,
860 help="External Key/Identifier that can be used for "
861 "data integration with third-party systems"),
862 'complete_name': fields.function(_complete_name_get, type='char', string='Complete ID'),
863 'model': fields.char('Model Name', required=True, select=1),
864 'module': fields.char('Module', required=True, select=1),
865 'res_id': fields.integer('Record ID', select=1,
866 help="ID of the target record in the database"),
867 'noupdate': fields.boolean('Non Updatable'),
868 'date_update': fields.datetime('Update Date'),
869 'date_init': fields.datetime('Init Date')
872 'date_init': fields.datetime.now,
873 'date_update': fields.datetime.now,
878 ('module_name_uniq', 'unique(name, module)', 'You cannot have multiple records with the same external ID in the same module!'),
881 def __init__(self, pool, cr):
882 osv.osv.__init__(self, pool, cr)
883 # also stored in pool to avoid being discarded along with this osv instance
884 if getattr(pool, 'model_data_reference_ids', None) is None:
885 self.pool.model_data_reference_ids = {}
886 # put loads on the class, in order to share it among all instances
887 type(self).loads = self.pool.model_data_reference_ids
889 def _auto_init(self, cr, context=None):
890 super(ir_model_data, self)._auto_init(cr, context)
891 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_model_data_module_name_index\'')
892 if not cr.fetchone():
893 cr.execute('CREATE INDEX ir_model_data_module_name_index ON ir_model_data (module, name)')
896 @tools.ormcache(skiparg=3)
897 def xmlid_lookup(self, cr, uid, xmlid):
898 """Low level xmlid lookup
899 Return (id, res_model, res_id) or raise ValueError if not found
901 module, name = xmlid.split('.', 1)
902 ids = self.search(cr, uid, [('module','=',module), ('name','=', name)])
904 raise ValueError('External ID not found in the system: %s' % (xmlid))
905 # the sql constraints ensure us we have only one result
906 res = self.read(cr, uid, ids[0], ['model', 'res_id'])
907 if not res['res_id']:
908 raise ValueError('External ID not found in the system: %s' % (xmlid))
909 return ids[0], res['model'], res['res_id']
911 def xmlid_to_res_model_res_id(self, cr, uid, xmlid, raise_if_not_found=False):
912 """ Return (res_model, res_id)"""
914 return self.xmlid_lookup(cr, uid, xmlid)[1:3]
916 if raise_if_not_found:
918 return (False, False)
920 def xmlid_to_res_id(self, cr, uid, xmlid, raise_if_not_found=False):
921 """ Returns res_id """
922 return self.xmlid_to_res_model_res_id(cr, uid, xmlid, raise_if_not_found)[1]
924 def xmlid_to_object(self, cr, uid, xmlid, raise_if_not_found=False, context=None):
925 """ Return a browse_record
926 if not found and raise_if_not_found is True return None
928 t = self.xmlid_to_res_model_res_id(cr, uid, xmlid, raise_if_not_found)
929 res_model, res_id = t
931 if res_model and res_id:
932 record = self.pool[res_model].browse(cr, uid, res_id, context=context)
935 if raise_if_not_found:
936 raise ValueError('No record found for unique ID %s. It may have been deleted.' % (xmlid))
940 def _get_id(self, cr, uid, module, xml_id):
941 """Returns the id of the ir.model.data record corresponding to a given module and xml_id (cached) or raise a ValueError if not found"""
942 return self.xmlid_lookup(cr, uid, "%s.%s" % (module, xml_id))[0]
944 def get_object_reference(self, cr, uid, module, xml_id):
945 """Returns (model, res_id) corresponding to a given module and xml_id (cached) or raise ValueError if not found"""
946 return self.xmlid_lookup(cr, uid, "%s.%s" % (module, xml_id))[1:3]
948 def check_object_reference(self, cr, uid, module, xml_id, raise_on_access_error=False):
949 """Returns (model, res_id) corresponding to a given module and xml_id (cached), if and only if the user has the necessary access rights
950 to see that object, otherwise raise a ValueError if raise_on_access_error is True or returns a tuple (model found, False)"""
951 model, res_id = self.get_object_reference(cr, uid, module, xml_id)
952 #search on id found in result to check if current user has read access right
953 check_right = self.pool.get(model).search(cr, uid, [('id', '=', res_id)])
956 if raise_on_access_error:
957 raise ValueError('Not enough access rights on the external ID: %s.%s' % (module, xml_id))
960 def get_object(self, cr, uid, module, xml_id, context=None):
961 """ Returns a browsable record for the given module name and xml_id.
962 If not found, raise a ValueError or return None, depending
963 on the value of `raise_exception`.
965 return self.xmlid_to_object(cr, uid, "%s.%s" % (module, xml_id), raise_if_not_found=True, context=context)
967 def _update_dummy(self,cr, uid, model, module, xml_id=False, store=True):
971 id = self.read(cr, uid, [self._get_id(cr, uid, module, xml_id)], ['res_id'])[0]['res_id']
972 self.loads[(module,xml_id)] = (model,id)
977 def clear_caches(self):
978 """ Clears all orm caches on the object's methods
982 self.xmlid_lookup.clear_cache(self)
985 def unlink(self, cr, uid, ids, context=None):
986 """ Regular unlink method, but make sure to clear the caches. """
988 return super(ir_model_data,self).unlink(cr, uid, ids, context=context)
990 def _update(self,cr, uid, model, module, values, xml_id=False, store=True, noupdate=False, mode='init', res_id=False, context=None):
991 model_obj = self.pool[model]
994 # records created during module install should not display the messages of OpenChatter
995 context = dict(context, install_mode=True)
996 if xml_id and ('.' in xml_id):
997 assert len(xml_id.split('.'))==2, _("'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id") % xml_id
998 module, xml_id = xml_id.split('.')
1001 cr.execute('''SELECT imd.id, imd.res_id, md.id, imd.model, imd.noupdate
1002 FROM ir_model_data imd LEFT JOIN %s md ON (imd.res_id = md.id)
1003 WHERE imd.module=%%s AND imd.name=%%s''' % model_obj._table,
1005 results = cr.fetchall()
1006 for imd_id2,res_id2,real_id2,real_model,noupdate_imd in results:
1007 # In update mode, do not update a record if it's ir.model.data is flagged as noupdate
1008 if mode == 'update' and noupdate_imd:
1012 cr.execute('delete from ir_model_data where id=%s', (imd_id2,))
1015 assert model == real_model, "External ID conflict, %s already refers to a `%s` record,"\
1016 " you can't define a `%s` record with this ID." % (xml_id, real_model, model)
1017 res_id,action_id = res_id2,imd_id2
1019 if action_id and res_id:
1020 model_obj.write(cr, uid, [res_id], values, context=context)
1021 self.write(cr, uid, [action_id], {
1022 'date_update': time.strftime('%Y-%m-%d %H:%M:%S'),
1025 model_obj.write(cr, uid, [res_id], values, context=context)
1027 if model_obj._inherits:
1028 for table in model_obj._inherits:
1029 inherit_id = model_obj.browse(cr, uid,
1030 res_id,context=context)[model_obj._inherits[table]]
1031 self.create(cr, uid, {
1032 'name': xml_id + '_' + table.replace('.', '_'),
1035 'res_id': inherit_id.id,
1036 'noupdate': noupdate,
1038 self.create(cr, uid, {
1043 'noupdate': noupdate,
1046 if mode=='init' or (mode=='update' and xml_id):
1047 res_id = model_obj.create(cr, uid, values, context=context)
1049 if model_obj._inherits:
1050 for table in model_obj._inherits:
1051 inherit_id = model_obj.browse(cr, uid,
1052 res_id,context=context)[model_obj._inherits[table]]
1053 self.create(cr, uid, {
1054 'name': xml_id + '_' + table.replace('.', '_'),
1057 'res_id': inherit_id.id,
1058 'noupdate': noupdate,
1060 self.create(cr, uid, {
1065 'noupdate': noupdate
1067 if xml_id and res_id:
1068 self.loads[(module, xml_id)] = (model, res_id)
1069 for table, inherit_field in model_obj._inherits.iteritems():
1070 inherit_id = model_obj.read(cr, uid, [res_id],
1071 [inherit_field])[0][inherit_field]
1072 self.loads[(module, xml_id + '_' + table.replace('.', '_'))] = (table, inherit_id)
1075 def ir_set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=None, xml_id=False):
1076 if isinstance(models[0], (list, tuple)):
1077 model,res_id = models[0]
1083 where = ' and res_id=%s' % (res_id,)
1085 where = ' and (res_id is null)'
1088 where += ' and key2=\'%s\'' % (key2,)
1090 where += ' and (key2 is null)'
1092 cr.execute('select * from ir_values where model=%s and key=%s and name=%s'+where,(model, key, name))
1094 ir_values_obj = openerp.registry(cr.dbname)['ir.values']
1096 ir_values_obj.set(cr, uid, key, key2, name, models, value, replace, isobject, meta)
1098 cr.execute('UPDATE ir_values set value=%s WHERE model=%s and key=%s and name=%s'+where,(value, model, key, name))
1099 ir_values_obj.invalidate_cache(cr, uid, ['value'])
1102 def _module_data_uninstall(self, cr, uid, modules_to_remove, context=None):
1103 """Deletes all the records referenced by the ir.model.data entries
1104 ``ids`` along with their corresponding database backed (including
1105 dropping tables, columns, FKs, etc, as long as there is no other
1106 ir.model.data entry holding a reference to them (which indicates that
1107 they are still owned by another module).
1108 Attempts to perform the deletion in an appropriate order to maximize
1109 the chance of gracefully deleting all records.
1110 This step is performed as part of the full uninstallation of a module.
1113 ids = self.search(cr, uid, [('module', 'in', modules_to_remove)])
1115 if uid != 1 and not self.pool['ir.model.access'].check_groups(cr, uid, "base.group_system"):
1116 raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
1118 context = dict(context or {})
1119 context[MODULE_UNINSTALL_FLAG] = True # enable model/field deletion
1126 for data in self.browse(cr, uid, ids, context):
1128 res_id = data.res_id
1130 pair_to_unlink = (model, res_id)
1131 if pair_to_unlink not in to_unlink:
1132 to_unlink.append(pair_to_unlink)
1134 if model == 'workflow.activity':
1135 # Special treatment for workflow activities: temporarily revert their
1136 # incoming transition and trigger an update to force all workflow items
1137 # to move out before deleting them
1138 cr.execute('select res_type,res_id from wkf_instance where id IN (select inst_id from wkf_workitem where act_id=%s)', (res_id,))
1139 wkf_todo.extend(cr.fetchall())
1140 cr.execute("update wkf_transition set condition='True', group_id=NULL, signal=NULL,act_to=act_from,act_from=%s where act_to=%s", (res_id,res_id))
1141 self.invalidate_cache(cr, uid, context=context)
1143 for model,res_id in wkf_todo:
1145 openerp.workflow.trg_write(uid, model, res_id, cr)
1147 _logger.info('Unable to force processing of workflow for item %s@%s in order to leave activity to be deleted', res_id, model, exc_info=True)
1149 def unlink_if_refcount(to_unlink):
1150 for model, res_id in to_unlink:
1151 external_ids = self.search(cr, uid, [('model', '=', model),('res_id', '=', res_id)])
1152 if set(external_ids)-ids_set:
1153 # if other modules have defined this record, we must not delete it
1155 if model == 'ir.model.fields':
1156 # Don't remove the LOG_ACCESS_COLUMNS unless _log_access
1157 # has been turned off on the model.
1158 field = self.pool[model].browse(cr, uid, [res_id], context=context)[0]
1159 if not field.exists():
1160 _logger.info('Deleting orphan external_ids %s', external_ids)
1161 self.unlink(cr, uid, external_ids)
1163 if field.name in openerp.models.LOG_ACCESS_COLUMNS and self.pool[field.model]._log_access:
1165 if field.name == 'id':
1167 _logger.info('Deleting %s@%s', res_id, model)
1169 cr.execute('SAVEPOINT record_unlink_save')
1170 self.pool[model].unlink(cr, uid, [res_id], context=context)
1172 _logger.info('Unable to delete %s@%s', res_id, model, exc_info=True)
1173 cr.execute('ROLLBACK TO SAVEPOINT record_unlink_save')
1175 cr.execute('RELEASE SAVEPOINT record_unlink_save')
1177 # Remove non-model records first, then model fields, and finish with models
1178 unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1179 if model not in ('ir.model','ir.model.fields','ir.model.constraint'))
1180 unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1181 if model == 'ir.model.constraint')
1183 ir_module_module = self.pool['ir.module.module']
1184 ir_model_constraint = self.pool['ir.model.constraint']
1185 modules_to_remove_ids = ir_module_module.search(cr, uid, [('name', 'in', modules_to_remove)], context=context)
1186 constraint_ids = ir_model_constraint.search(cr, uid, [('module', 'in', modules_to_remove_ids)], context=context)
1187 ir_model_constraint._module_data_uninstall(cr, uid, constraint_ids, context)
1189 unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1190 if model == 'ir.model.fields')
1192 ir_model_relation = self.pool['ir.model.relation']
1193 relation_ids = ir_model_relation.search(cr, uid, [('module', 'in', modules_to_remove_ids)])
1194 ir_model_relation._module_data_uninstall(cr, uid, relation_ids, context)
1196 unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1197 if model == 'ir.model')
1201 self.unlink(cr, uid, ids, context)
1203 def _process_end(self, cr, uid, modules):
1204 """ Clear records removed from updated module data.
1205 This method is called at the end of the module loading process.
1206 It is meant to removed records that are no longer present in the
1207 updated data. Such records are recognised as the one with an xml id
1208 and a module in ir_model_data and noupdate set to false, but not
1209 present in self.loads.
1214 cr.execute("""SELECT id,name,model,res_id,module FROM ir_model_data
1215 WHERE module IN %s AND res_id IS NOT NULL AND noupdate=%s ORDER BY id DESC""",
1216 (tuple(modules), False))
1217 for (id, name, model, res_id, module) in cr.fetchall():
1218 if (module,name) not in self.loads:
1219 to_unlink.append((model,res_id))
1220 if not config.get('import_partial'):
1221 for (model, res_id) in to_unlink:
1222 if model in self.pool:
1223 _logger.info('Deleting %s@%s', res_id, model)
1224 self.pool[model].unlink(cr, uid, [res_id])
1226 class wizard_model_menu(osv.osv_memory):
1227 _name = 'wizard.ir.model.menu.create'
1229 'menu_id': fields.many2one('ir.ui.menu', 'Parent Menu', required=True),
1230 'name': fields.char('Menu Name', required=True),
1233 def menu_create(self, cr, uid, ids, context=None):
1236 model_pool = self.pool.get('ir.model')
1237 for menu in self.browse(cr, uid, ids, context):
1238 model = model_pool.browse(cr, uid, context.get('model_id'), context=context)
1241 'res_model': model.model,
1242 'view_type': 'form',
1243 'view_mode': 'tree,form'
1245 action_id = self.pool.get('ir.actions.act_window').create(cr, uid, val)
1246 self.pool.get('ir.ui.menu').create(cr, uid, {
1248 'parent_id': menu.menu_id.id,
1249 'action': 'ir.actions.act_window,%d' % (action_id,),
1250 'icon': 'STOCK_INDENT'
1252 return {'type':'ir.actions.act_window_close'}
1254 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: