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):
99 res = dict.fromkeys(ids, [])
100 for model in self.browse(cr, uid, ids, context=context):
101 inherited_models = [model_name for model_name in self.pool[model.model]._inherits]
103 res[model.id] = self.search(cr, uid, [('model', 'in', inherited_models)], context=context)
107 'name': fields.char('Model Description', translate=True, required=True),
108 'model': fields.char('Model', required=True, select=1),
109 'info': fields.text('Information'),
110 'field_id': fields.one2many('ir.model.fields', 'model_id', 'Fields', required=True, copy=True),
111 'inherited_model_ids': fields.function(_inherited_models, type="many2many", obj="ir.model", string="Inherited models",
112 help="The list of models that extends the current model."),
113 'state': fields.selection([('manual','Custom Object'),('base','Base Object')],'Type', readonly=True),
114 'access_ids': fields.one2many('ir.model.access', 'model_id', 'Access'),
115 'osv_memory': fields.function(_is_osv_memory, string='Transient Model', type='boolean',
116 fnct_search=_search_osv_memory,
117 help="This field specifies whether the model is transient or not (i.e. if records are automatically deleted from the database or not)"),
118 'modules': fields.function(_in_modules, type='char', string='In Modules', help='List of modules in which the object is defined or inherited'),
119 'view_ids': fields.function(_view_ids, type='one2many', obj='ir.ui.view', string='Views'),
124 'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
127 def _check_model_name(self, cr, uid, ids, context=None):
128 for model in self.browse(cr, uid, ids, context=context):
129 if model.state=='manual':
130 if not model.model.startswith('x_'):
132 if not re.match('^[a-z_A-Z0-9.]+$',model.model):
136 def _model_name_msg(self, cr, uid, ids, context=None):
137 return _('The Object name must start with x_ and not contain any special character !')
140 (_check_model_name, _model_name_msg, ['model']),
143 ('obj_name_uniq', 'unique (model)', 'Each model must be unique!'),
146 # overridden to allow searching both on model name (model field)
147 # and model description (name field)
148 def _name_search(self, cr, uid, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None):
151 domain = args + ['|', ('model', operator, name), ('name', operator, name)]
152 return self.name_get(cr, name_get_uid or uid,
153 super(ir_model, self).search(cr, uid, domain, limit=limit, context=context),
156 def _drop_table(self, cr, uid, ids, context=None):
157 for model in self.browse(cr, uid, ids, context):
158 model_pool = self.pool[model.model]
159 cr.execute('select relkind from pg_class where relname=%s', (model_pool._table,))
160 result = cr.fetchone()
161 if result and result[0] == 'v':
162 cr.execute('DROP view %s' % (model_pool._table,))
163 elif result and result[0] == 'r':
164 cr.execute('DROP TABLE %s CASCADE' % (model_pool._table,))
167 def unlink(self, cr, user, ids, context=None):
168 # Prevent manual deletion of module tables
169 if context is None: context = {}
170 if isinstance(ids, (int, long)):
172 if not context.get(MODULE_UNINSTALL_FLAG):
173 for model in self.browse(cr, user, ids, context):
174 if model.state != 'manual':
175 raise except_orm(_('Error'), _("Model '%s' contains module data and cannot be removed!") % (model.name,))
177 self._drop_table(cr, user, ids, context)
178 res = super(ir_model, self).unlink(cr, user, ids, context)
179 if not context.get(MODULE_UNINSTALL_FLAG):
180 # only reload pool for normal unlink. For module uninstall the
181 # reload is done independently in openerp.modules.loading
182 cr.commit() # must be committed before reloading registry in new cursor
183 openerp.modules.registry.RegistryManager.new(cr.dbname)
184 openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
188 def write(self, cr, user, ids, vals, context=None):
190 context = dict(context)
191 context.pop('__last_update', None)
192 # Filter out operations 4 link from field id, because openerp-web
193 # always write (4,id,False) even for non dirty items
194 if 'field_id' in vals:
195 vals['field_id'] = [op for op in vals['field_id'] if op[0] != 4]
196 return super(ir_model,self).write(cr, user, ids, vals, context)
198 def create(self, cr, user, vals, context=None):
201 if context and context.get('manual'):
202 vals['state']='manual'
203 res = super(ir_model,self).create(cr, user, vals, context)
204 if vals.get('state','base')=='manual':
205 self.instanciate(cr, user, vals['model'], context)
206 model = self.pool[vals['model']]
207 model._prepare_setup_fields(cr, SUPERUSER_ID)
208 model._setup_fields(cr, SUPERUSER_ID)
210 field_name=vals['name'],
211 field_state='manual',
212 select=vals.get('select_level', '0'),
213 update_custom_fields=True)
214 model._auto_init(cr, ctx)
215 model._auto_end(cr, ctx) # actually create FKs!
216 openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
219 def instanciate(self, cr, user, model, context=None):
220 class x_custom_model(osv.osv):
222 if isinstance(model, unicode):
223 model = model.encode('utf-8')
224 x_custom_model._name = model
225 x_custom_model._module = False
226 a = x_custom_model._build_model(self.pool, cr)
229 elif 'x_name' in a._columns.keys():
232 x_name = a._columns.keys()[0]
233 x_custom_model._rec_name = x_name
236 class ir_model_fields(osv.osv):
237 _name = 'ir.model.fields'
238 _description = "Fields"
239 _rec_name = 'field_description'
242 'name': fields.char('Name', required=True, select=1),
243 'complete_name': fields.char('Complete Name', select=1),
244 'model': fields.char('Object Name', required=True, select=1,
245 help="The technical name of the model this field belongs to"),
246 'relation': fields.char('Object Relation',
247 help="For relationship fields, the technical name of the target model"),
248 'relation_field': fields.char('Relation Field',
249 help="For one2many fields, the field on the target model that implement the opposite many2one relationship"),
250 'model_id': fields.many2one('ir.model', 'Model', required=True, select=True, ondelete='cascade',
251 help="The model this field belongs to"),
252 'field_description': fields.char('Field Label', required=True),
253 'ttype': fields.selection(_get_fields_type, 'Field Type', required=True),
254 'selection': fields.char('Selection Options', help="List of options for a selection field, "
255 "specified as a Python expression defining a list of (key, label) pairs. "
256 "For example: [('blue','Blue'),('yellow','Yellow')]"),
257 'required': fields.boolean('Required'),
258 'readonly': fields.boolean('Readonly'),
259 'select_level': fields.selection([('0','Not Searchable'),('1','Always Searchable'),('2','Advanced Search (deprecated)')],'Searchable', required=True),
260 'translate': fields.boolean('Translatable', help="Whether values for this field can be translated (enables the translation mechanism for that field)"),
261 'size': fields.integer('Size'),
262 'state': fields.selection([('manual','Custom Field'),('base','Base Field')],'Type', required=True, readonly=True, select=1),
263 'on_delete': fields.selection([('cascade', 'Cascade'), ('set null', 'Set NULL'), ('restrict', 'Restrict')],
264 'On Delete', help='On delete property for many2one fields'),
265 'domain': fields.char('Domain', help="The optional domain to restrict possible values for relationship fields, "
266 "specified as a Python expression defining a list of triplets. "
267 "For example: [('color','=','red')]"),
268 'groups': fields.many2many('res.groups', 'ir_model_fields_group_rel', 'field_id', 'group_id', 'Groups'),
269 'selectable': fields.boolean('Selectable'),
270 'modules': fields.function(_in_modules, type='char', string='In Modules', help='List of modules in which the field is defined'),
271 'serialization_field_id': fields.many2one('ir.model.fields', 'Serialization Field', domain = "[('ttype','=','serialized')]",
272 ondelete='cascade', help="If set, this field will be stored in the sparse "
273 "structure of the serialization field, instead "
274 "of having its own database column. This cannot be "
275 "changed after creation."),
277 _rec_name='field_description'
282 'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
283 'on_delete': 'set null',
285 'field_description': '',
290 def _check_selection(self, cr, uid, selection, context=None):
292 selection_list = eval(selection)
294 _logger.warning('Invalid selection list definition for fields.selection', exc_info=True)
295 raise except_orm(_('Error'),
296 _("The Selection Options expression is not a valid Pythonic expression."
297 "Please provide an expression in the [('key','Label'), ...] format."))
300 if not (isinstance(selection_list, list) and selection_list):
303 for item in selection_list:
304 if not (isinstance(item, (tuple,list)) and len(item) == 2):
309 raise except_orm(_('Error'),
310 _("The Selection Options expression is must be in the [('key','Label'), ...] format!"))
313 def _size_gt_zero_msg(self, cr, user, ids, context=None):
314 return _('Size of the field can never be less than 0 !')
317 ('size_gt_zero', 'CHECK (size>=0)',_size_gt_zero_msg ),
320 def _drop_column(self, cr, uid, ids, context=None):
321 for field in self.browse(cr, uid, ids, context):
322 if field.name in MAGIC_COLUMNS:
324 model = self.pool[field.model]
325 cr.execute('select relkind from pg_class where relname=%s', (model._table,))
326 result = cr.fetchone()
327 cr.execute("SELECT column_name FROM information_schema.columns WHERE table_name ='%s' and column_name='%s'" %(model._table, field.name))
328 column_name = cr.fetchone()
329 if column_name and (result and result[0] == 'r'):
330 cr.execute('ALTER table "%s" DROP column "%s" cascade' % (model._table, field.name))
331 model._columns.pop(field.name, None)
333 # remove m2m relation table for custom fields
334 # we consider the m2m relation is only one way as it's not possible
335 # to specify the relation table in the interface for custom fields
336 # TODO master: maybe use ir.model.relations for custom fields
337 if field.state == 'manual' and field.ttype == 'many2many':
338 rel_name = self.pool[field.model]._all_columns[field.name].column._rel
339 cr.execute('DROP table "%s"' % (rel_name))
342 def unlink(self, cr, user, ids, context=None):
343 # Prevent manual deletion of module columns
344 if context is None: context = {}
345 if isinstance(ids, (int, long)):
347 if not context.get(MODULE_UNINSTALL_FLAG) and \
348 any(field.state != 'manual' for field in self.browse(cr, user, ids, context)):
349 raise except_orm(_('Error'), _("This column contains module data and cannot be removed!"))
351 self._drop_column(cr, user, ids, context)
352 res = super(ir_model_fields, self).unlink(cr, user, ids, context)
353 if not context.get(MODULE_UNINSTALL_FLAG):
355 openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
358 def create(self, cr, user, vals, context=None):
359 if 'model_id' in vals:
360 model_data = self.pool['ir.model'].browse(cr, user, vals['model_id'])
361 vals['model'] = model_data.model
364 if context and context.get('manual',False):
365 vals['state'] = 'manual'
366 if vals.get('ttype', False) == 'selection':
367 if not vals.get('selection',False):
368 raise except_orm(_('Error'), _('For selection fields, the Selection Options must be given!'))
369 self._check_selection(cr, user, vals['selection'], context=context)
370 res = super(ir_model_fields,self).create(cr, user, vals, context)
371 if vals.get('state','base') == 'manual':
372 if not vals['name'].startswith('x_'):
373 raise except_orm(_('Error'), _("Custom fields must have a name that starts with 'x_' !"))
375 if vals.get('relation',False) and not self.pool['ir.model'].search(cr, user, [('model','=',vals['relation'])]):
376 raise except_orm(_('Error'), _("Model %s does not exist!") % vals['relation'])
378 if vals['model'] in self.pool:
379 model = self.pool[vals['model']]
380 if vals['model'].startswith('x_') and vals['name'] == 'x_name':
381 model._rec_name = 'x_name'
383 if self.pool.fields_by_model is not None:
384 cr.execute('SELECT * FROM ir_model_fields WHERE id=%s', (res,))
385 self.pool.fields_by_model.setdefault(vals['model'], []).append(cr.dictfetchone())
386 model.__init__(self.pool, cr)
387 model._prepare_setup_fields(cr, SUPERUSER_ID)
388 model._setup_fields(cr, SUPERUSER_ID)
390 #Added context to _auto_init for special treatment to custom field for select_level
392 field_name=vals['name'],
393 field_state='manual',
394 select=vals.get('select_level', '0'),
395 update_custom_fields=True)
396 model._auto_init(cr, ctx)
397 model._auto_end(cr, ctx) # actually create FKs!
398 openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
402 def write(self, cr, user, ids, vals, context=None):
405 if context and context.get('manual',False):
406 vals['state'] = 'manual'
408 #For the moment renaming a sparse field or changing the storing system is not allowed. This may be done later
409 if 'serialization_field_id' in vals or 'name' in vals:
410 for field in self.browse(cr, user, ids, context=context):
411 if 'serialization_field_id' in vals and field.serialization_field_id.id != vals['serialization_field_id']:
412 raise except_orm(_('Error!'), _('Changing the storing system for field "%s" is not allowed.')%field.name)
413 if field.serialization_field_id and (field.name != vals['name']):
414 raise except_orm(_('Error!'), _('Renaming sparse field "%s" is not allowed')%field.name)
416 column_rename = None # if set, *one* column can be renamed here
417 models_patch = {} # structs of (obj, [(field, prop, change_to),..])
418 # data to be updated on the orm model
420 # static table of properties
421 model_props = [ # (our-name, fields.prop, set_fn)
422 ('field_description', 'string', tools.ustr),
423 ('required', 'required', bool),
424 ('readonly', 'readonly', bool),
425 ('domain', '_domain', eval),
426 ('size', 'size', int),
427 ('on_delete', 'ondelete', str),
428 ('translate', 'translate', bool),
429 ('selectable', 'selectable', bool),
430 ('select_level', 'select', int),
431 ('selection', 'selection', eval),
435 checked_selection = False # need only check it once, so defer
437 for item in self.browse(cr, user, ids, context=context):
438 obj = self.pool.get(item.model)
440 if item.state != 'manual':
441 raise except_orm(_('Error!'),
442 _('Properties of base fields cannot be altered in this manner! '
443 'Please modify them through Python code, '
444 'preferably through a custom addon!'))
446 if item.ttype == 'selection' and 'selection' in vals \
447 and not checked_selection:
448 self._check_selection(cr, user, vals['selection'], context=context)
449 checked_selection = True
451 final_name = item.name
452 if 'name' in vals and vals['name'] != item.name:
453 # We need to rename the column
455 raise except_orm(_('Error!'), _('Can only rename one column at a time!'))
456 if vals['name'] in obj._columns:
457 raise except_orm(_('Error!'), _('Cannot rename column to %s, because that column already exists!') % vals['name'])
458 if vals.get('state', 'base') == 'manual' and not vals['name'].startswith('x_'):
459 raise except_orm(_('Error!'), _('New column name must still start with x_ , because it is a custom field!'))
460 if '\'' in vals['name'] or '"' in vals['name'] or ';' in vals['name']:
461 raise ValueError('Invalid character in column name')
462 column_rename = (obj, (obj._table, item.name, vals['name']))
463 final_name = vals['name']
465 if 'model_id' in vals and vals['model_id'] != item.model_id.id:
466 raise except_orm(_("Error!"), _("Changing the model of a field is forbidden!"))
468 if 'ttype' in vals and vals['ttype'] != item.ttype:
469 raise except_orm(_("Error!"), _("Changing the type of a column is not yet supported. "
470 "Please drop it and create it again!"))
472 # We don't check the 'state', because it might come from the context
473 # (thus be set for multiple fields) and will be ignored anyway.
475 models_patch.setdefault(obj._name, (obj,[]))
476 # find out which properties (per model) we need to update
477 for field_name, field_property, set_fn in model_props:
478 if field_name in vals:
479 property_value = set_fn(vals[field_name])
480 if getattr(obj._columns[item.name], field_property) != property_value:
481 models_patch[obj._name][1].append((final_name, field_property, property_value))
482 # our dict is ready here, but no properties are changed so far
484 # These shall never be written (modified)
485 for column_name in ('model_id', 'model', 'state'):
486 if column_name in vals:
487 del vals[column_name]
489 res = super(ir_model_fields,self).write(cr, user, ids, vals, context=context)
492 cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s"' % column_rename[1])
493 # This is VERY risky, but let us have this feature:
494 # we want to change the key of column in obj._columns dict
495 col = column_rename[0]._columns.pop(column_rename[1][1]) # take object out, w/o copy
496 column_rename[0]._columns[column_rename[1][2]] = col
499 # We have to update _columns of the model(s) and then call their
500 # _auto_init to sync the db with the model. Hopefully, since write()
501 # was called earlier, they will be in-sync before the _auto_init.
502 # Anything we don't update in _columns now will be reset from
503 # the model into ir.model.fields (db).
504 ctx = dict(context, select=vals.get('select_level', '0'),
505 update_custom_fields=True)
507 for __, patch_struct in models_patch.items():
508 obj = patch_struct[0]
509 for col_name, col_prop, val in patch_struct[1]:
510 setattr(obj._columns[col_name], col_prop, val)
511 obj._auto_init(cr, ctx)
512 obj._auto_end(cr, ctx) # actually create FKs!
513 openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
516 class ir_model_constraint(Model):
518 This model tracks PostgreSQL foreign keys and constraints used by OpenERP
521 _name = 'ir.model.constraint'
523 'name': fields.char('Constraint', required=True, select=1,
524 help="PostgreSQL constraint or foreign key name."),
525 'model': fields.many2one('ir.model', string='Model',
526 required=True, select=1),
527 'module': fields.many2one('ir.module.module', string='Module',
528 required=True, select=1),
529 'type': fields.char('Constraint Type', required=True, size=1, select=1,
530 help="Type of the constraint: `f` for a foreign key, "
531 "`u` for other constraints."),
532 'date_update': fields.datetime('Update Date'),
533 'date_init': fields.datetime('Initialization Date')
537 ('module_name_uniq', 'unique(name, module)',
538 'Constraints with the same name are unique per module.'),
541 def _module_data_uninstall(self, cr, uid, ids, context=None):
543 Delete PostgreSQL foreign keys and constraints tracked by this model.
546 if uid != SUPERUSER_ID and not self.pool['ir.model.access'].check_groups(cr, uid, "base.group_system"):
547 raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
549 context = dict(context or {})
554 for data in self.browse(cr, uid, ids, context):
555 model = data.model.model
556 model_obj = self.pool[model]
557 name = openerp.tools.ustr(data.name)
560 # double-check we are really going to delete all the owners of this schema element
561 cr.execute("""SELECT id from ir_model_constraint where name=%s""", (data.name,))
562 external_ids = [x[0] for x in cr.fetchall()]
563 if set(external_ids)-ids_set:
564 # as installed modules have defined this element we must not delete it!
568 # test if FK exists on this table (it could be on a related m2m table, in which case we ignore it)
569 cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
570 WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('f', name, model_obj._table))
572 cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),)
573 _logger.info('Dropped FK CONSTRAINT %s@%s', name, model)
576 # test if constraint exists
577 cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
578 WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('u', name, model_obj._table))
580 cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),)
581 _logger.info('Dropped CONSTRAINT %s@%s', name, model)
583 self.unlink(cr, uid, ids, context)
585 class ir_model_relation(Model):
587 This model tracks PostgreSQL tables used to implement OpenERP many2many
590 _name = 'ir.model.relation'
592 'name': fields.char('Relation Name', required=True, select=1,
593 help="PostgreSQL table name implementing a many2many relation."),
594 'model': fields.many2one('ir.model', string='Model',
595 required=True, select=1),
596 'module': fields.many2one('ir.module.module', string='Module',
597 required=True, select=1),
598 'date_update': fields.datetime('Update Date'),
599 'date_init': fields.datetime('Initialization Date')
602 def _module_data_uninstall(self, cr, uid, ids, context=None):
604 Delete PostgreSQL many2many relations tracked by this model.
607 if uid != SUPERUSER_ID and not self.pool['ir.model.access'].check_groups(cr, uid, "base.group_system"):
608 raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
614 for data in self.browse(cr, uid, ids, context):
616 name = openerp.tools.ustr(data.name)
618 # double-check we are really going to delete all the owners of this schema element
619 cr.execute("""SELECT id from ir_model_relation where name = %s""", (data.name,))
620 external_ids = [x[0] for x in cr.fetchall()]
621 if set(external_ids)-ids_set:
622 # as installed modules have defined this element we must not delete it!
625 cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name=%s", (name,))
626 if cr.fetchone() and not name in to_drop_table:
627 to_drop_table.append(name)
629 self.unlink(cr, uid, ids, context)
631 # drop m2m relation tables
632 for table in to_drop_table:
633 cr.execute('DROP TABLE %s CASCADE'% table,)
634 _logger.info('Dropped table %s', table)
638 class ir_model_access(osv.osv):
639 _name = 'ir.model.access'
641 'name': fields.char('Name', required=True, select=True),
642 '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.'),
643 'model_id': fields.many2one('ir.model', 'Object', required=True, domain=[('osv_memory','=', False)], select=True, ondelete='cascade'),
644 'group_id': fields.many2one('res.groups', 'Group', ondelete='cascade', select=True),
645 'perm_read': fields.boolean('Read Access'),
646 'perm_write': fields.boolean('Write Access'),
647 'perm_create': fields.boolean('Create Access'),
648 'perm_unlink': fields.boolean('Delete Access'),
654 def check_groups(self, cr, uid, group):
655 grouparr = group.split('.')
658 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],))
659 return bool(cr.fetchone())
661 def check_group(self, cr, uid, model, mode, group_ids):
662 """ Check if a specific group has the access mode to the specified model"""
663 assert mode in ['read','write','create','unlink'], 'Invalid access mode'
665 if isinstance(model, BaseModel):
666 assert model._name == 'ir.model', 'Invalid model object'
667 model_name = model.name
671 if isinstance(group_ids, (int, long)):
672 group_ids = [group_ids]
673 for group_id in group_ids:
674 cr.execute("SELECT perm_" + mode + " "
675 " FROM ir_model_access a "
676 " JOIN ir_model m ON (m.id = a.model_id) "
677 " WHERE m.model = %s AND a.active IS True "
678 " AND a.group_id = %s", (model_name, group_id)
682 cr.execute("SELECT perm_" + mode + " "
683 " FROM ir_model_access a "
684 " JOIN ir_model m ON (m.id = a.model_id) "
685 " WHERE m.model = %s AND a.active IS True "
686 " AND a.group_id IS NULL", (model_name, )
690 access = bool(r and r[0])
693 # pass no groups -> no access
696 def group_names_with_access(self, cr, model_name, access_mode):
697 """Returns the names of visible groups which have been granted ``access_mode`` on
698 the model ``model_name``.
701 assert access_mode in ['read','write','create','unlink'], 'Invalid access mode: %s' % access_mode
706 JOIN ir_model m ON (a.model_id=m.id)
707 JOIN res_groups g ON (a.group_id=g.id)
708 LEFT JOIN ir_module_category c ON (c.id=g.category_id)
712 a.perm_''' + access_mode, (model_name,))
713 return [('%s/%s' % x) if x[0] else x[1] for x in cr.fetchall()]
716 def check(self, cr, uid, model, mode='read', raise_exception=True, context=None):
718 # User root have all accesses
719 # TODO: exclude xml-rpc requests
722 assert mode in ['read','write','create','unlink'], 'Invalid access mode'
724 if isinstance(model, BaseModel):
725 assert model._name == 'ir.model', 'Invalid model object'
726 model_name = model.model
730 # TransientModel records have no access rights, only an implicit access rule
731 if model_name not in self.pool:
732 _logger.error('Missing model %s' % (model_name, ))
733 elif self.pool[model_name].is_transient():
736 # We check if a specific rule exists
737 cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
738 ' FROM ir_model_access a '
739 ' JOIN ir_model m ON (m.id = a.model_id) '
740 ' JOIN res_groups_users_rel gu ON (gu.gid = a.group_id) '
741 ' WHERE m.model = %s '
743 ' AND a.active IS True '
749 # there is no specific rule. We check the generic rule
750 cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
751 ' FROM ir_model_access a '
752 ' JOIN ir_model m ON (m.id = a.model_id) '
753 ' WHERE a.group_id IS NULL '
755 ' AND a.active IS True '
760 if not r and raise_exception:
761 groups = '\n\t'.join('- %s' % g for g in self.group_names_with_access(cr, model_name, mode))
763 # Messages are declared in extenso so they are properly exported in translation terms
764 'read': _("Sorry, you are not allowed to access this document."),
765 'write': _("Sorry, you are not allowed to modify this document."),
766 'create': _("Sorry, you are not allowed to create this kind of document."),
767 'unlink': _("Sorry, you are not allowed to delete this document."),
770 msg_tail = _("Only users with the following access level are currently allowed to do that") + ":\n%s\n\n(" + _("Document model") + ": %s)"
771 msg_params = (groups, model_name)
773 msg_tail = _("Please contact your system administrator if you think this is an error.") + "\n\n(" + _("Document model") + ": %s)"
774 msg_params = (model_name,)
775 _logger.warning('Access Denied by ACLs for operation: %s, uid: %s, model: %s', mode, uid, model_name)
776 msg = '%s %s' % (msg_heads[mode], msg_tail)
777 raise openerp.exceptions.AccessError(msg % msg_params)
780 __cache_clearing_methods = []
782 def register_cache_clearing_method(self, model, method):
783 self.__cache_clearing_methods.append((model, method))
785 def unregister_cache_clearing_method(self, model, method):
787 i = self.__cache_clearing_methods.index((model, method))
788 del self.__cache_clearing_methods[i]
792 def call_cache_clearing_methods(self, cr):
793 self.invalidate_cache(cr, SUPERUSER_ID)
794 self.check.clear_cache(self) # clear the cache of check function
795 for model, method in self.__cache_clearing_methods:
796 if model in self.pool:
797 getattr(self.pool[model], method)()
800 # Check rights on actions
802 def write(self, cr, uid, ids, values, context=None):
803 self.call_cache_clearing_methods(cr)
804 res = super(ir_model_access, self).write(cr, uid, ids, values, context=context)
807 def create(self, cr, uid, values, context=None):
808 self.call_cache_clearing_methods(cr)
809 res = super(ir_model_access, self).create(cr, uid, values, context=context)
812 def unlink(self, cr, uid, ids, context=None):
813 self.call_cache_clearing_methods(cr)
814 res = super(ir_model_access, self).unlink(cr, uid, ids, context=context)
817 class ir_model_data(osv.osv):
818 """Holds external identifier keys for records in the database.
819 This has two main uses:
821 * allows easy data integration with third-party systems,
822 making import/export/sync of data possible, as records
823 can be uniquely identified across multiple systems
824 * allows tracking the origin of data installed by OpenERP
825 modules themselves, thus making it possible to later
826 update them seamlessly.
828 _name = 'ir.model.data'
829 _order = 'module,model,name'
831 def name_get(self, cr, uid, ids, context=None):
832 bymodel = defaultdict(dict)
835 for res in self.browse(cr, uid, ids, context=context):
836 bymodel[res.model][res.res_id] = res
837 names[res.id] = res.complete_name
838 #result[res.model][res.res_id] = res.id
840 for model, id_map in bymodel.iteritems():
842 ng = dict(self.pool[model].name_get(cr, uid, id_map.keys(), context=context))
846 for r in id_map.itervalues():
847 names[r.id] = ng.get(r.res_id, r.complete_name)
849 return [(i, names[i]) for i in ids]
851 def _complete_name_get(self, cr, uid, ids, prop, unknow_none, context=None):
853 for res in self.browse(cr, uid, ids, context=context):
854 result[res.id] = (res.module and (res.module + '.') or '')+res.name
858 'name': fields.char('External Identifier', required=True, select=1,
859 help="External Key/Identifier that can be used for "
860 "data integration with third-party systems"),
861 'complete_name': fields.function(_complete_name_get, type='char', string='Complete ID'),
862 'model': fields.char('Model Name', required=True, select=1),
863 'module': fields.char('Module', required=True, select=1),
864 'res_id': fields.integer('Record ID', select=1,
865 help="ID of the target record in the database"),
866 'noupdate': fields.boolean('Non Updatable'),
867 'date_update': fields.datetime('Update Date'),
868 'date_init': fields.datetime('Init Date')
871 'date_init': fields.datetime.now,
872 'date_update': fields.datetime.now,
877 ('module_name_uniq', 'unique(name, module)', 'You cannot have multiple records with the same external ID in the same module!'),
880 def __init__(self, pool, cr):
881 osv.osv.__init__(self, pool, cr)
882 # also stored in pool to avoid being discarded along with this osv instance
883 if getattr(pool, 'model_data_reference_ids', None) is None:
884 self.pool.model_data_reference_ids = {}
885 # put loads on the class, in order to share it among all instances
886 type(self).loads = self.pool.model_data_reference_ids
888 def _auto_init(self, cr, context=None):
889 super(ir_model_data, self)._auto_init(cr, context)
890 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_model_data_module_name_index\'')
891 if not cr.fetchone():
892 cr.execute('CREATE INDEX ir_model_data_module_name_index ON ir_model_data (module, name)')
895 @tools.ormcache(skiparg=3)
896 def xmlid_lookup(self, cr, uid, xmlid):
897 """Low level xmlid lookup
898 Return (id, res_model, res_id) or raise ValueError if not found
900 module, name = xmlid.split('.', 1)
901 ids = self.search(cr, uid, [('module','=',module), ('name','=', name)])
903 raise ValueError('External ID not found in the system: %s' % (xmlid))
904 # the sql constraints ensure us we have only one result
905 res = self.read(cr, uid, ids[0], ['model', 'res_id'])
906 if not res['res_id']:
907 raise ValueError('External ID not found in the system: %s' % (xmlid))
908 return ids[0], res['model'], res['res_id']
910 def xmlid_to_res_model_res_id(self, cr, uid, xmlid, raise_if_not_found=False):
911 """ Return (res_model, res_id)"""
913 return self.xmlid_lookup(cr, uid, xmlid)[1:3]
915 if raise_if_not_found:
917 return (False, False)
919 def xmlid_to_res_id(self, cr, uid, xmlid, raise_if_not_found=False):
920 """ Returns res_id """
921 return self.xmlid_to_res_model_res_id(cr, uid, xmlid, raise_if_not_found)[1]
923 def xmlid_to_object(self, cr, uid, xmlid, raise_if_not_found=False, context=None):
924 """ Return a browse_record
925 if not found and raise_if_not_found is True return None
927 t = self.xmlid_to_res_model_res_id(cr, uid, xmlid, raise_if_not_found)
928 res_model, res_id = t
930 if res_model and res_id:
931 record = self.pool[res_model].browse(cr, uid, res_id, context=context)
934 if raise_if_not_found:
935 raise ValueError('No record found for unique ID %s. It may have been deleted.' % (xmlid))
939 def _get_id(self, cr, uid, module, xml_id):
940 """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"""
941 return self.xmlid_lookup(cr, uid, "%s.%s" % (module, xml_id))[0]
943 def get_object_reference(self, cr, uid, module, xml_id):
944 """Returns (model, res_id) corresponding to a given module and xml_id (cached) or raise ValueError if not found"""
945 return self.xmlid_lookup(cr, uid, "%s.%s" % (module, xml_id))[1:3]
947 def check_object_reference(self, cr, uid, module, xml_id, raise_on_access_error=False):
948 """Returns (model, res_id) corresponding to a given module and xml_id (cached), if and only if the user has the necessary access rights
949 to see that object, otherwise raise a ValueError if raise_on_access_error is True or returns a tuple (model found, False)"""
950 model, res_id = self.get_object_reference(cr, uid, module, xml_id)
951 #search on id found in result to check if current user has read access right
952 check_right = self.pool.get(model).search(cr, uid, [('id', '=', res_id)])
955 if raise_on_access_error:
956 raise ValueError('Not enough access rights on the external ID: %s.%s' % (module, xml_id))
959 def get_object(self, cr, uid, module, xml_id, context=None):
960 """ Returns a browsable record for the given module name and xml_id.
961 If not found, raise a ValueError or return None, depending
962 on the value of `raise_exception`.
964 return self.xmlid_to_object(cr, uid, "%s.%s" % (module, xml_id), raise_if_not_found=True, context=context)
966 def _update_dummy(self,cr, uid, model, module, xml_id=False, store=True):
970 id = self.read(cr, uid, [self._get_id(cr, uid, module, xml_id)], ['res_id'])[0]['res_id']
971 self.loads[(module,xml_id)] = (model,id)
976 def clear_caches(self):
977 """ Clears all orm caches on the object's methods
981 self.xmlid_lookup.clear_cache(self)
984 def unlink(self, cr, uid, ids, context=None):
985 """ Regular unlink method, but make sure to clear the caches. """
987 return super(ir_model_data,self).unlink(cr, uid, ids, context=context)
989 def _update(self,cr, uid, model, module, values, xml_id=False, store=True, noupdate=False, mode='init', res_id=False, context=None):
990 model_obj = self.pool[model]
993 # records created during module install should not display the messages of OpenChatter
994 context = dict(context, install_mode=True)
995 if xml_id and ('.' in xml_id):
996 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
997 module, xml_id = xml_id.split('.')
1000 cr.execute('''SELECT imd.id, imd.res_id, md.id, imd.model, imd.noupdate
1001 FROM ir_model_data imd LEFT JOIN %s md ON (imd.res_id = md.id)
1002 WHERE imd.module=%%s AND imd.name=%%s''' % model_obj._table,
1004 results = cr.fetchall()
1005 for imd_id2,res_id2,real_id2,real_model,noupdate_imd in results:
1006 # In update mode, do not update a record if it's ir.model.data is flagged as noupdate
1007 if mode == 'update' and noupdate_imd:
1011 cr.execute('delete from ir_model_data where id=%s', (imd_id2,))
1014 assert model == real_model, "External ID conflict, %s already refers to a `%s` record,"\
1015 " you can't define a `%s` record with this ID." % (xml_id, real_model, model)
1016 res_id,action_id = res_id2,imd_id2
1018 if action_id and res_id:
1019 model_obj.write(cr, uid, [res_id], values, context=context)
1020 self.write(cr, uid, [action_id], {
1021 'date_update': time.strftime('%Y-%m-%d %H:%M:%S'),
1024 model_obj.write(cr, uid, [res_id], values, context=context)
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('.', '_'),
1034 'res_id': inherit_id.id,
1035 'noupdate': noupdate,
1037 self.create(cr, uid, {
1042 'noupdate': noupdate,
1045 if mode=='init' or (mode=='update' and xml_id):
1046 res_id = model_obj.create(cr, uid, values, context=context)
1048 if model_obj._inherits:
1049 for table in model_obj._inherits:
1050 inherit_id = model_obj.browse(cr, uid,
1051 res_id,context=context)[model_obj._inherits[table]]
1052 self.create(cr, uid, {
1053 'name': xml_id + '_' + table.replace('.', '_'),
1056 'res_id': inherit_id.id,
1057 'noupdate': noupdate,
1059 self.create(cr, uid, {
1064 'noupdate': noupdate
1066 if xml_id and res_id:
1067 self.loads[(module, xml_id)] = (model, res_id)
1068 for table, inherit_field in model_obj._inherits.iteritems():
1069 inherit_id = model_obj.read(cr, uid, [res_id],
1070 [inherit_field])[0][inherit_field]
1071 self.loads[(module, xml_id + '_' + table.replace('.', '_'))] = (table, inherit_id)
1074 def ir_set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=None, xml_id=False):
1075 if isinstance(models[0], (list, tuple)):
1076 model,res_id = models[0]
1082 where = ' and res_id=%s' % (res_id,)
1084 where = ' and (res_id is null)'
1087 where += ' and key2=\'%s\'' % (key2,)
1089 where += ' and (key2 is null)'
1091 cr.execute('select * from ir_values where model=%s and key=%s and name=%s'+where,(model, key, name))
1093 ir_values_obj = openerp.registry(cr.dbname)['ir.values']
1095 ir_values_obj.set(cr, uid, key, key2, name, models, value, replace, isobject, meta)
1097 cr.execute('UPDATE ir_values set value=%s WHERE model=%s and key=%s and name=%s'+where,(value, model, key, name))
1098 ir_values_obj.invalidate_cache(cr, uid, ['value'])
1101 def _module_data_uninstall(self, cr, uid, modules_to_remove, context=None):
1102 """Deletes all the records referenced by the ir.model.data entries
1103 ``ids`` along with their corresponding database backed (including
1104 dropping tables, columns, FKs, etc, as long as there is no other
1105 ir.model.data entry holding a reference to them (which indicates that
1106 they are still owned by another module).
1107 Attempts to perform the deletion in an appropriate order to maximize
1108 the chance of gracefully deleting all records.
1109 This step is performed as part of the full uninstallation of a module.
1112 ids = self.search(cr, uid, [('module', 'in', modules_to_remove)])
1114 if uid != 1 and not self.pool['ir.model.access'].check_groups(cr, uid, "base.group_system"):
1115 raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
1117 context = dict(context or {})
1118 context[MODULE_UNINSTALL_FLAG] = True # enable model/field deletion
1125 for data in self.browse(cr, uid, ids, context):
1127 res_id = data.res_id
1129 pair_to_unlink = (model, res_id)
1130 if pair_to_unlink not in to_unlink:
1131 to_unlink.append(pair_to_unlink)
1133 if model == 'workflow.activity':
1134 # Special treatment for workflow activities: temporarily revert their
1135 # incoming transition and trigger an update to force all workflow items
1136 # to move out before deleting them
1137 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,))
1138 wkf_todo.extend(cr.fetchall())
1139 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))
1140 self.invalidate_cache(cr, uid, context=context)
1142 for model,res_id in wkf_todo:
1144 openerp.workflow.trg_write(uid, model, res_id, cr)
1146 _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)
1148 def unlink_if_refcount(to_unlink):
1149 for model, res_id in to_unlink:
1150 external_ids = self.search(cr, uid, [('model', '=', model),('res_id', '=', res_id)])
1151 if set(external_ids)-ids_set:
1152 # if other modules have defined this record, we must not delete it
1154 if model == 'ir.model.fields':
1155 # Don't remove the LOG_ACCESS_COLUMNS unless _log_access
1156 # has been turned off on the model.
1157 field = self.pool[model].browse(cr, uid, [res_id], context=context)[0]
1158 if not field.exists():
1159 _logger.info('Deleting orphan external_ids %s', external_ids)
1160 self.unlink(cr, uid, external_ids)
1162 if field.name in openerp.models.LOG_ACCESS_COLUMNS and self.pool[field.model]._log_access:
1164 if field.name == 'id':
1166 _logger.info('Deleting %s@%s', res_id, model)
1168 cr.execute('SAVEPOINT record_unlink_save')
1169 self.pool[model].unlink(cr, uid, [res_id], context=context)
1171 _logger.info('Unable to delete %s@%s', res_id, model, exc_info=True)
1172 cr.execute('ROLLBACK TO SAVEPOINT record_unlink_save')
1174 cr.execute('RELEASE SAVEPOINT record_unlink_save')
1176 # Remove non-model records first, then model fields, and finish with models
1177 unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1178 if model not in ('ir.model','ir.model.fields','ir.model.constraint'))
1179 unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1180 if model == 'ir.model.constraint')
1182 ir_module_module = self.pool['ir.module.module']
1183 ir_model_constraint = self.pool['ir.model.constraint']
1184 modules_to_remove_ids = ir_module_module.search(cr, uid, [('name', 'in', modules_to_remove)], context=context)
1185 constraint_ids = ir_model_constraint.search(cr, uid, [('module', 'in', modules_to_remove_ids)], context=context)
1186 ir_model_constraint._module_data_uninstall(cr, uid, constraint_ids, context)
1188 unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1189 if model == 'ir.model.fields')
1191 ir_model_relation = self.pool['ir.model.relation']
1192 relation_ids = ir_model_relation.search(cr, uid, [('module', 'in', modules_to_remove_ids)])
1193 ir_model_relation._module_data_uninstall(cr, uid, relation_ids, context)
1195 unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1196 if model == 'ir.model')
1200 self.unlink(cr, uid, ids, context)
1202 def _process_end(self, cr, uid, modules):
1203 """ Clear records removed from updated module data.
1204 This method is called at the end of the module loading process.
1205 It is meant to removed records that are no longer present in the
1206 updated data. Such records are recognised as the one with an xml id
1207 and a module in ir_model_data and noupdate set to false, but not
1208 present in self.loads.
1213 cr.execute("""SELECT id,name,model,res_id,module FROM ir_model_data
1214 WHERE module IN %s AND res_id IS NOT NULL AND noupdate=%s ORDER BY id DESC""",
1215 (tuple(modules), False))
1216 for (id, name, model, res_id, module) in cr.fetchall():
1217 if (module,name) not in self.loads:
1218 to_unlink.append((model,res_id))
1219 if not config.get('import_partial'):
1220 for (model, res_id) in to_unlink:
1221 if model in self.pool:
1222 _logger.info('Deleting %s@%s', res_id, model)
1223 self.pool[model].unlink(cr, uid, [res_id])
1225 class wizard_model_menu(osv.osv_memory):
1226 _name = 'wizard.ir.model.menu.create'
1228 'menu_id': fields.many2one('ir.ui.menu', 'Parent Menu', required=True),
1229 'name': fields.char('Menu Name', required=True),
1232 def menu_create(self, cr, uid, ids, context=None):
1235 model_pool = self.pool.get('ir.model')
1236 for menu in self.browse(cr, uid, ids, context):
1237 model = model_pool.browse(cr, uid, context.get('model_id'), context=context)
1240 'res_model': model.model,
1241 'view_type': 'form',
1242 'view_mode': 'tree,form'
1244 action_id = self.pool.get('ir.actions.act_window').create(cr, uid, val)
1245 self.pool.get('ir.ui.menu').create(cr, uid, {
1247 'parent_id': menu.menu_id.id,
1248 'action': 'ir.actions.act_window,%d' % (action_id,),
1249 'icon': 'STOCK_INDENT'
1251 return {'type':'ir.actions.act_window_close'}
1253 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: