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 from openerp import SUPERUSER_ID
30 from openerp import models, tools, api
31 from openerp.modules.registry import RegistryManager
32 from openerp.osv import fields, osv
33 from openerp.osv.orm import BaseModel, Model, MAGIC_COLUMNS, except_orm
34 from openerp.tools import config
35 from openerp.tools.safe_eval import safe_eval as eval
36 from openerp.tools.translate import _
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 api.Environment.reset()
185 RegistryManager.new(cr.dbname)
186 RegistryManager.signal_registry_change(cr.dbname)
190 def write(self, cr, user, ids, vals, context=None):
192 context = dict(context)
193 context.pop('__last_update', None)
194 # Filter out operations 4 link from field id, because openerp-web
195 # always write (4,id,False) even for non dirty items
196 if 'field_id' in vals:
197 vals['field_id'] = [op for op in vals['field_id'] if op[0] != 4]
198 return super(ir_model,self).write(cr, user, ids, vals, context)
200 def create(self, cr, user, vals, context=None):
203 if context and context.get('manual'):
204 vals['state']='manual'
205 res = super(ir_model,self).create(cr, user, vals, context)
206 if vals.get('state','base')=='manual':
207 # add model in registry
208 self.instanciate(cr, user, vals['model'], context)
209 self.pool.setup_models(cr, partial=(not self.pool.ready))
210 # update database schema
211 model = self.pool[vals['model']]
213 field_name=vals['name'],
214 field_state='manual',
215 select=vals.get('select_level', '0'),
216 update_custom_fields=True)
217 model._auto_init(cr, ctx)
218 model._auto_end(cr, ctx) # actually create FKs!
219 RegistryManager.signal_registry_change(cr.dbname)
222 def instanciate(self, cr, user, model, context=None):
223 if isinstance(model, unicode):
224 model = model.encode('utf-8')
226 class CustomModel(models.Model):
231 obj = CustomModel._build_model(self.pool, cr)
232 obj._rec_name = CustomModel._rec_name = (
233 'x_name' if 'x_name' in obj._columns else
234 list(obj._columns)[0] if obj._columns else
238 class ir_model_fields(osv.osv):
239 _name = 'ir.model.fields'
240 _description = "Fields"
241 _rec_name = 'field_description'
244 'name': fields.char('Name', required=True, select=1),
245 'complete_name': fields.char('Complete Name', select=1),
246 'model': fields.char('Object Name', required=True, select=1,
247 help="The technical name of the model this field belongs to"),
248 'relation': fields.char('Object Relation',
249 help="For relationship fields, the technical name of the target model"),
250 'relation_field': fields.char('Relation Field',
251 help="For one2many fields, the field on the target model that implement the opposite many2one relationship"),
252 'model_id': fields.many2one('ir.model', 'Model', required=True, select=True, ondelete='cascade',
253 help="The model this field belongs to"),
254 'field_description': fields.char('Field Label', required=True),
255 'ttype': fields.selection(_get_fields_type, 'Field Type', required=True),
256 'selection': fields.char('Selection Options', help="List of options for a selection field, "
257 "specified as a Python expression defining a list of (key, label) pairs. "
258 "For example: [('blue','Blue'),('yellow','Yellow')]"),
259 'required': fields.boolean('Required'),
260 'readonly': fields.boolean('Readonly'),
261 'select_level': fields.selection([('0','Not Searchable'),('1','Always Searchable'),('2','Advanced Search (deprecated)')],'Searchable', required=True),
262 'translate': fields.boolean('Translatable', help="Whether values for this field can be translated (enables the translation mechanism for that field)"),
263 'size': fields.integer('Size'),
264 'state': fields.selection([('manual','Custom Field'),('base','Base Field')],'Type', required=True, readonly=True, select=1),
265 'on_delete': fields.selection([('cascade', 'Cascade'), ('set null', 'Set NULL'), ('restrict', 'Restrict')],
266 'On Delete', help='On delete property for many2one fields'),
267 'domain': fields.char('Domain', help="The optional domain to restrict possible values for relationship fields, "
268 "specified as a Python expression defining a list of triplets. "
269 "For example: [('color','=','red')]"),
270 'groups': fields.many2many('res.groups', 'ir_model_fields_group_rel', 'field_id', 'group_id', 'Groups'),
271 'selectable': fields.boolean('Selectable'),
272 'modules': fields.function(_in_modules, type='char', string='In Modules', help='List of modules in which the field is defined'),
273 'serialization_field_id': fields.many2one('ir.model.fields', 'Serialization Field', domain = "[('ttype','=','serialized')]",
274 ondelete='cascade', help="If set, this field will be stored in the sparse "
275 "structure of the serialization field, instead "
276 "of having its own database column. This cannot be "
277 "changed after creation."),
279 _rec_name='field_description'
284 'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
285 'on_delete': 'set null',
287 'field_description': '',
292 def _check_selection(self, cr, uid, selection, context=None):
294 selection_list = eval(selection)
296 _logger.warning('Invalid selection list definition for fields.selection', exc_info=True)
297 raise except_orm(_('Error'),
298 _("The Selection Options expression is not a valid Pythonic expression."
299 "Please provide an expression in the [('key','Label'), ...] format."))
302 if not (isinstance(selection_list, list) and selection_list):
305 for item in selection_list:
306 if not (isinstance(item, (tuple,list)) and len(item) == 2):
311 raise except_orm(_('Error'),
312 _("The Selection Options expression is must be in the [('key','Label'), ...] format!"))
315 def _size_gt_zero_msg(self, cr, user, ids, context=None):
316 return _('Size of the field can never be less than 0 !')
319 ('size_gt_zero', 'CHECK (size>=0)',_size_gt_zero_msg ),
322 def _drop_column(self, cr, uid, ids, context=None):
323 for field in self.browse(cr, uid, ids, context):
324 if field.name in MAGIC_COLUMNS:
326 model = self.pool[field.model]
327 cr.execute('select relkind from pg_class where relname=%s', (model._table,))
328 result = cr.fetchone()
329 cr.execute("SELECT column_name FROM information_schema.columns WHERE table_name ='%s' and column_name='%s'" %(model._table, field.name))
330 column_name = cr.fetchone()
331 if column_name and (result and result[0] == 'r'):
332 cr.execute('ALTER table "%s" DROP column "%s" cascade' % (model._table, field.name))
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 = model._fields[field.name].relation
339 cr.execute('DROP table "%s"' % (rel_name))
340 model._pop_field(field.name)
344 def unlink(self, cr, user, ids, context=None):
345 # Prevent manual deletion of module columns
346 if context is None: context = {}
347 if isinstance(ids, (int, long)):
349 if not context.get(MODULE_UNINSTALL_FLAG) and \
350 any(field.state != 'manual' for field in self.browse(cr, user, ids, context)):
351 raise except_orm(_('Error'), _("This column contains module data and cannot be removed!"))
353 self._drop_column(cr, user, ids, context)
354 res = super(ir_model_fields, self).unlink(cr, user, ids, context)
355 if not context.get(MODULE_UNINSTALL_FLAG):
356 # The field we just deleted might have be inherited, and registry is
357 # inconsistent in this case; therefore we reload the registry.
359 api.Environment.reset()
360 RegistryManager.new(cr.dbname)
361 RegistryManager.signal_registry_change(cr.dbname)
364 def create(self, cr, user, vals, context=None):
365 if 'model_id' in vals:
366 model_data = self.pool['ir.model'].browse(cr, user, vals['model_id'])
367 vals['model'] = model_data.model
370 if context and context.get('manual',False):
371 vals['state'] = 'manual'
372 if vals.get('ttype', False) == 'selection':
373 if not vals.get('selection',False):
374 raise except_orm(_('Error'), _('For selection fields, the Selection Options must be given!'))
375 self._check_selection(cr, user, vals['selection'], context=context)
376 res = super(ir_model_fields,self).create(cr, user, vals, context)
377 if vals.get('state','base') == 'manual':
378 if not vals['name'].startswith('x_'):
379 raise except_orm(_('Error'), _("Custom fields must have a name that starts with 'x_' !"))
381 if vals.get('relation',False) and not self.pool['ir.model'].search(cr, user, [('model','=',vals['relation'])]):
382 raise except_orm(_('Error'), _("Model %s does not exist!") % vals['relation'])
384 if vals['model'] in self.pool:
385 model = self.pool[vals['model']]
386 if vals['model'].startswith('x_') and vals['name'] == 'x_name':
387 model._rec_name = 'x_name'
389 if self.pool.fields_by_model is not None:
390 cr.execute('SELECT * FROM ir_model_fields WHERE id=%s', (res,))
391 self.pool.fields_by_model.setdefault(vals['model'], []).append(cr.dictfetchone())
393 # re-initialize model in registry
394 model.__init__(self.pool, cr)
395 self.pool.setup_models(cr, partial=(not self.pool.ready))
396 # update database schema
397 model = self.pool[vals['model']]
399 field_name=vals['name'],
400 field_state='manual',
401 select=vals.get('select_level', '0'),
402 update_custom_fields=True)
403 model._auto_init(cr, ctx)
404 model._auto_end(cr, ctx) # actually create FKs!
405 RegistryManager.signal_registry_change(cr.dbname)
409 def write(self, cr, user, ids, vals, context=None):
412 if context and context.get('manual',False):
413 vals['state'] = 'manual'
415 #For the moment renaming a sparse field or changing the storing system is not allowed. This may be done later
416 if 'serialization_field_id' in vals or 'name' in vals:
417 for field in self.browse(cr, user, ids, context=context):
418 if 'serialization_field_id' in vals and field.serialization_field_id.id != vals['serialization_field_id']:
419 raise except_orm(_('Error!'), _('Changing the storing system for field "%s" is not allowed.')%field.name)
420 if field.serialization_field_id and (field.name != vals['name']):
421 raise except_orm(_('Error!'), _('Renaming sparse field "%s" is not allowed')%field.name)
423 # if set, *one* column can be renamed here
426 # field patches {model: {field_name: {prop_name: prop_value, ...}, ...}, ...}
427 patches = defaultdict(lambda: defaultdict(dict))
429 # static table of properties
430 model_props = [ # (our-name, fields.prop, set_fn)
431 ('field_description', 'string', tools.ustr),
432 ('required', 'required', bool),
433 ('readonly', 'readonly', bool),
434 ('domain', 'domain', eval),
435 ('size', 'size', int),
436 ('on_delete', 'ondelete', str),
437 ('translate', 'translate', bool),
438 ('select_level', 'index', lambda x: bool(int(x))),
439 ('selection', 'selection', eval),
443 checked_selection = False # need only check it once, so defer
445 for item in self.browse(cr, user, ids, context=context):
446 obj = self.pool.get(item.model)
448 if item.state != 'manual':
449 raise except_orm(_('Error!'),
450 _('Properties of base fields cannot be altered in this manner! '
451 'Please modify them through Python code, '
452 'preferably through a custom addon!'))
454 if item.ttype == 'selection' and 'selection' in vals \
455 and not checked_selection:
456 self._check_selection(cr, user, vals['selection'], context=context)
457 checked_selection = True
459 final_name = item.name
460 if 'name' in vals and vals['name'] != item.name:
461 # We need to rename the column
463 raise except_orm(_('Error!'), _('Can only rename one column at a time!'))
464 if vals['name'] in obj._columns:
465 raise except_orm(_('Error!'), _('Cannot rename column to %s, because that column already exists!') % vals['name'])
466 if vals.get('state', 'base') == 'manual' and not vals['name'].startswith('x_'):
467 raise except_orm(_('Error!'), _('New column name must still start with x_ , because it is a custom field!'))
468 if '\'' in vals['name'] or '"' in vals['name'] or ';' in vals['name']:
469 raise ValueError('Invalid character in column name')
470 column_rename = (obj, (obj._table, item.name, vals['name']))
471 final_name = vals['name']
473 if 'model_id' in vals and vals['model_id'] != item.model_id.id:
474 raise except_orm(_("Error!"), _("Changing the model of a field is forbidden!"))
476 if 'ttype' in vals and vals['ttype'] != item.ttype:
477 raise except_orm(_("Error!"), _("Changing the type of a column is not yet supported. "
478 "Please drop it and create it again!"))
480 # We don't check the 'state', because it might come from the context
481 # (thus be set for multiple fields) and will be ignored anyway.
483 # find out which properties (per model) we need to update
484 for field_name, prop_name, func in model_props:
485 if field_name in vals:
486 prop_value = func(vals[field_name])
487 if getattr(obj._fields[item.name], prop_name) != prop_value:
488 patches[obj][final_name][prop_name] = prop_value
490 # These shall never be written (modified)
491 for column_name in ('model_id', 'model', 'state'):
492 if column_name in vals:
493 del vals[column_name]
495 res = super(ir_model_fields,self).write(cr, user, ids, vals, context=context)
498 obj, rename = column_rename
499 cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s"' % rename)
500 # This is VERY risky, but let us have this feature:
501 # we want to change the key of field in obj._fields and obj._columns
502 field = obj._pop_field(rename[1])
503 obj._add_field(rename[2], field)
504 self.pool.setup_models(cr, partial=(not self.pool.ready))
507 # We have to update _columns of the model(s) and then call their
508 # _auto_init to sync the db with the model. Hopefully, since write()
509 # was called earlier, they will be in-sync before the _auto_init.
510 # Anything we don't update in _columns now will be reset from
511 # the model into ir.model.fields (db).
513 select=vals.get('select_level', '0'),
514 update_custom_fields=True,
517 for obj, model_patches in patches.iteritems():
518 for field_name, field_patches in model_patches.iteritems():
519 # update field properties, and adapt corresponding column
520 field = obj._fields[field_name]
521 attrs = dict(field._attrs, **field_patches)
522 obj._add_field(field_name, field.new(**attrs))
524 # update database schema
525 self.pool.setup_models(cr, partial=(not self.pool.ready))
526 obj._auto_init(cr, ctx)
527 obj._auto_end(cr, ctx) # actually create FKs!
529 if column_rename or patches:
530 RegistryManager.signal_registry_change(cr.dbname)
534 class ir_model_constraint(Model):
536 This model tracks PostgreSQL foreign keys and constraints used by OpenERP
539 _name = 'ir.model.constraint'
541 'name': fields.char('Constraint', required=True, select=1,
542 help="PostgreSQL constraint or foreign key name."),
543 'model': fields.many2one('ir.model', string='Model',
544 required=True, select=1),
545 'module': fields.many2one('ir.module.module', string='Module',
546 required=True, select=1),
547 'type': fields.char('Constraint Type', required=True, size=1, select=1,
548 help="Type of the constraint: `f` for a foreign key, "
549 "`u` for other constraints."),
550 'date_update': fields.datetime('Update Date'),
551 'date_init': fields.datetime('Initialization Date')
555 ('module_name_uniq', 'unique(name, module)',
556 'Constraints with the same name are unique per module.'),
559 def _module_data_uninstall(self, cr, uid, ids, context=None):
561 Delete PostgreSQL foreign keys and constraints tracked by this model.
564 if uid != SUPERUSER_ID and not self.pool['ir.model.access'].check_groups(cr, uid, "base.group_system"):
565 raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
567 context = dict(context or {})
572 for data in self.browse(cr, uid, ids, context):
573 model = data.model.model
574 model_obj = self.pool[model]
575 name = openerp.tools.ustr(data.name)
578 # double-check we are really going to delete all the owners of this schema element
579 cr.execute("""SELECT id from ir_model_constraint where name=%s""", (data.name,))
580 external_ids = [x[0] for x in cr.fetchall()]
581 if set(external_ids)-ids_set:
582 # as installed modules have defined this element we must not delete it!
586 # test if FK exists on this table (it could be on a related m2m table, in which case we ignore it)
587 cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
588 WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('f', name, model_obj._table))
590 cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),)
591 _logger.info('Dropped FK CONSTRAINT %s@%s', name, model)
594 # test if constraint exists
595 cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
596 WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('u', name, model_obj._table))
598 cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),)
599 _logger.info('Dropped CONSTRAINT %s@%s', name, model)
601 self.unlink(cr, uid, ids, context)
603 class ir_model_relation(Model):
605 This model tracks PostgreSQL tables used to implement OpenERP many2many
608 _name = 'ir.model.relation'
610 'name': fields.char('Relation Name', required=True, select=1,
611 help="PostgreSQL table name implementing a many2many relation."),
612 'model': fields.many2one('ir.model', string='Model',
613 required=True, select=1),
614 'module': fields.many2one('ir.module.module', string='Module',
615 required=True, select=1),
616 'date_update': fields.datetime('Update Date'),
617 'date_init': fields.datetime('Initialization Date')
620 def _module_data_uninstall(self, cr, uid, ids, context=None):
622 Delete PostgreSQL many2many relations tracked by this model.
625 if uid != SUPERUSER_ID and not self.pool['ir.model.access'].check_groups(cr, uid, "base.group_system"):
626 raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
632 for data in self.browse(cr, uid, ids, context):
634 name = openerp.tools.ustr(data.name)
636 # double-check we are really going to delete all the owners of this schema element
637 cr.execute("""SELECT id from ir_model_relation where name = %s""", (data.name,))
638 external_ids = [x[0] for x in cr.fetchall()]
639 if set(external_ids)-ids_set:
640 # as installed modules have defined this element we must not delete it!
643 cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name=%s", (name,))
644 if cr.fetchone() and not name in to_drop_table:
645 to_drop_table.append(name)
647 self.unlink(cr, uid, ids, context)
649 # drop m2m relation tables
650 for table in to_drop_table:
651 cr.execute('DROP TABLE %s CASCADE'% table,)
652 _logger.info('Dropped table %s', table)
656 class ir_model_access(osv.osv):
657 _name = 'ir.model.access'
659 'name': fields.char('Name', required=True, select=True),
660 '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.'),
661 'model_id': fields.many2one('ir.model', 'Object', required=True, domain=[('osv_memory','=', False)], select=True, ondelete='cascade'),
662 'group_id': fields.many2one('res.groups', 'Group', ondelete='cascade', select=True),
663 'perm_read': fields.boolean('Read Access'),
664 'perm_write': fields.boolean('Write Access'),
665 'perm_create': fields.boolean('Create Access'),
666 'perm_unlink': fields.boolean('Delete Access'),
672 def check_groups(self, cr, uid, group):
673 grouparr = group.split('.')
676 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],))
677 return bool(cr.fetchone())
679 def check_group(self, cr, uid, model, mode, group_ids):
680 """ Check if a specific group has the access mode to the specified model"""
681 assert mode in ['read','write','create','unlink'], 'Invalid access mode'
683 if isinstance(model, BaseModel):
684 assert model._name == 'ir.model', 'Invalid model object'
685 model_name = model.name
689 if isinstance(group_ids, (int, long)):
690 group_ids = [group_ids]
691 for group_id in group_ids:
692 cr.execute("SELECT perm_" + mode + " "
693 " FROM ir_model_access a "
694 " JOIN ir_model m ON (m.id = a.model_id) "
695 " WHERE m.model = %s AND a.active IS True "
696 " AND a.group_id = %s", (model_name, group_id)
700 cr.execute("SELECT perm_" + mode + " "
701 " FROM ir_model_access a "
702 " JOIN ir_model m ON (m.id = a.model_id) "
703 " WHERE m.model = %s AND a.active IS True "
704 " AND a.group_id IS NULL", (model_name, )
708 access = bool(r and r[0])
711 # pass no groups -> no access
714 def group_names_with_access(self, cr, model_name, access_mode):
715 """Returns the names of visible groups which have been granted ``access_mode`` on
716 the model ``model_name``.
719 assert access_mode in ['read','write','create','unlink'], 'Invalid access mode: %s' % access_mode
724 JOIN ir_model m ON (a.model_id=m.id)
725 JOIN res_groups g ON (a.group_id=g.id)
726 LEFT JOIN ir_module_category c ON (c.id=g.category_id)
730 a.perm_''' + access_mode, (model_name,))
731 return [('%s/%s' % x) if x[0] else x[1] for x in cr.fetchall()]
734 def check(self, cr, uid, model, mode='read', raise_exception=True, context=None):
736 # User root have all accesses
737 # TODO: exclude xml-rpc requests
740 assert mode in ['read','write','create','unlink'], 'Invalid access mode'
742 if isinstance(model, BaseModel):
743 assert model._name == 'ir.model', 'Invalid model object'
744 model_name = model.model
748 # TransientModel records have no access rights, only an implicit access rule
749 if model_name not in self.pool:
750 _logger.error('Missing model %s' % (model_name, ))
751 elif self.pool[model_name].is_transient():
754 # We check if a specific rule exists
755 cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
756 ' FROM ir_model_access a '
757 ' JOIN ir_model m ON (m.id = a.model_id) '
758 ' JOIN res_groups_users_rel gu ON (gu.gid = a.group_id) '
759 ' WHERE m.model = %s '
761 ' AND a.active IS True '
767 # there is no specific rule. We check the generic rule
768 cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
769 ' FROM ir_model_access a '
770 ' JOIN ir_model m ON (m.id = a.model_id) '
771 ' WHERE a.group_id IS NULL '
773 ' AND a.active IS True '
778 if not r and raise_exception:
779 groups = '\n\t'.join('- %s' % g for g in self.group_names_with_access(cr, model_name, mode))
781 # Messages are declared in extenso so they are properly exported in translation terms
782 'read': _("Sorry, you are not allowed to access this document."),
783 'write': _("Sorry, you are not allowed to modify this document."),
784 'create': _("Sorry, you are not allowed to create this kind of document."),
785 'unlink': _("Sorry, you are not allowed to delete this document."),
788 msg_tail = _("Only users with the following access level are currently allowed to do that") + ":\n%s\n\n(" + _("Document model") + ": %s)"
789 msg_params = (groups, model_name)
791 msg_tail = _("Please contact your system administrator if you think this is an error.") + "\n\n(" + _("Document model") + ": %s)"
792 msg_params = (model_name,)
793 _logger.warning('Access Denied by ACLs for operation: %s, uid: %s, model: %s', mode, uid, model_name)
794 msg = '%s %s' % (msg_heads[mode], msg_tail)
795 raise openerp.exceptions.AccessError(msg % msg_params)
798 __cache_clearing_methods = []
800 def register_cache_clearing_method(self, model, method):
801 self.__cache_clearing_methods.append((model, method))
803 def unregister_cache_clearing_method(self, model, method):
805 i = self.__cache_clearing_methods.index((model, method))
806 del self.__cache_clearing_methods[i]
810 def call_cache_clearing_methods(self, cr):
811 self.invalidate_cache(cr, SUPERUSER_ID)
812 self.check.clear_cache(self) # clear the cache of check function
813 for model, method in self.__cache_clearing_methods:
814 if model in self.pool:
815 getattr(self.pool[model], method)()
818 # Check rights on actions
820 def write(self, cr, uid, ids, values, context=None):
821 self.call_cache_clearing_methods(cr)
822 res = super(ir_model_access, self).write(cr, uid, ids, values, context=context)
825 def create(self, cr, uid, values, context=None):
826 self.call_cache_clearing_methods(cr)
827 res = super(ir_model_access, self).create(cr, uid, values, context=context)
830 def unlink(self, cr, uid, ids, context=None):
831 self.call_cache_clearing_methods(cr)
832 res = super(ir_model_access, self).unlink(cr, uid, ids, context=context)
835 class ir_model_data(osv.osv):
836 """Holds external identifier keys for records in the database.
837 This has two main uses:
839 * allows easy data integration with third-party systems,
840 making import/export/sync of data possible, as records
841 can be uniquely identified across multiple systems
842 * allows tracking the origin of data installed by OpenERP
843 modules themselves, thus making it possible to later
844 update them seamlessly.
846 _name = 'ir.model.data'
847 _order = 'module,model,name'
849 def name_get(self, cr, uid, ids, context=None):
850 bymodel = defaultdict(dict)
853 for res in self.browse(cr, uid, ids, context=context):
854 bymodel[res.model][res.res_id] = res
855 names[res.id] = res.complete_name
856 #result[res.model][res.res_id] = res.id
858 for model, id_map in bymodel.iteritems():
860 ng = dict(self.pool[model].name_get(cr, uid, id_map.keys(), context=context))
864 for r in id_map.itervalues():
865 names[r.id] = ng.get(r.res_id, r.complete_name)
867 return [(i, names[i]) for i in ids]
869 def _complete_name_get(self, cr, uid, ids, prop, unknow_none, context=None):
871 for res in self.browse(cr, uid, ids, context=context):
872 result[res.id] = (res.module and (res.module + '.') or '')+res.name
876 'name': fields.char('External Identifier', required=True, select=1,
877 help="External Key/Identifier that can be used for "
878 "data integration with third-party systems"),
879 'complete_name': fields.function(_complete_name_get, type='char', string='Complete ID'),
880 'model': fields.char('Model Name', required=True, select=1),
881 'module': fields.char('Module', required=True, select=1),
882 'res_id': fields.integer('Record ID', select=1,
883 help="ID of the target record in the database"),
884 'noupdate': fields.boolean('Non Updatable'),
885 'date_update': fields.datetime('Update Date'),
886 'date_init': fields.datetime('Init Date')
889 'date_init': fields.datetime.now,
890 'date_update': fields.datetime.now,
895 ('module_name_uniq', 'unique(name, module)', 'You cannot have multiple records with the same external ID in the same module!'),
898 def __init__(self, pool, cr):
899 osv.osv.__init__(self, pool, cr)
900 # also stored in pool to avoid being discarded along with this osv instance
901 if getattr(pool, 'model_data_reference_ids', None) is None:
902 self.pool.model_data_reference_ids = {}
903 # put loads on the class, in order to share it among all instances
904 type(self).loads = self.pool.model_data_reference_ids
906 def _auto_init(self, cr, context=None):
907 super(ir_model_data, self)._auto_init(cr, context)
908 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_model_data_module_name_index\'')
909 if not cr.fetchone():
910 cr.execute('CREATE INDEX ir_model_data_module_name_index ON ir_model_data (module, name)')
913 @tools.ormcache(skiparg=3)
914 def xmlid_lookup(self, cr, uid, xmlid):
915 """Low level xmlid lookup
916 Return (id, res_model, res_id) or raise ValueError if not found
918 module, name = xmlid.split('.', 1)
919 ids = self.search(cr, uid, [('module','=',module), ('name','=', name)])
921 raise ValueError('External ID not found in the system: %s' % (xmlid))
922 # the sql constraints ensure us we have only one result
923 res = self.read(cr, uid, ids[0], ['model', 'res_id'])
924 if not res['res_id']:
925 raise ValueError('External ID not found in the system: %s' % (xmlid))
926 return ids[0], res['model'], res['res_id']
928 def xmlid_to_res_model_res_id(self, cr, uid, xmlid, raise_if_not_found=False):
929 """ Return (res_model, res_id)"""
931 return self.xmlid_lookup(cr, uid, xmlid)[1:3]
933 if raise_if_not_found:
935 return (False, False)
937 def xmlid_to_res_id(self, cr, uid, xmlid, raise_if_not_found=False):
938 """ Returns res_id """
939 return self.xmlid_to_res_model_res_id(cr, uid, xmlid, raise_if_not_found)[1]
941 def xmlid_to_object(self, cr, uid, xmlid, raise_if_not_found=False, context=None):
942 """ Return a browse_record
943 if not found and raise_if_not_found is True return None
945 t = self.xmlid_to_res_model_res_id(cr, uid, xmlid, raise_if_not_found)
946 res_model, res_id = t
948 if res_model and res_id:
949 record = self.pool[res_model].browse(cr, uid, res_id, context=context)
952 if raise_if_not_found:
953 raise ValueError('No record found for unique ID %s. It may have been deleted.' % (xmlid))
957 def _get_id(self, cr, uid, module, xml_id):
958 """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"""
959 return self.xmlid_lookup(cr, uid, "%s.%s" % (module, xml_id))[0]
961 def get_object_reference(self, cr, uid, module, xml_id):
962 """Returns (model, res_id) corresponding to a given module and xml_id (cached) or raise ValueError if not found"""
963 return self.xmlid_lookup(cr, uid, "%s.%s" % (module, xml_id))[1:3]
965 def check_object_reference(self, cr, uid, module, xml_id, raise_on_access_error=False):
966 """Returns (model, res_id) corresponding to a given module and xml_id (cached), if and only if the user has the necessary access rights
967 to see that object, otherwise raise a ValueError if raise_on_access_error is True or returns a tuple (model found, False)"""
968 model, res_id = self.get_object_reference(cr, uid, module, xml_id)
969 #search on id found in result to check if current user has read access right
970 check_right = self.pool.get(model).search(cr, uid, [('id', '=', res_id)])
973 if raise_on_access_error:
974 raise ValueError('Not enough access rights on the external ID: %s.%s' % (module, xml_id))
977 def get_object(self, cr, uid, module, xml_id, context=None):
978 """ Returns a browsable record for the given module name and xml_id.
979 If not found, raise a ValueError or return None, depending
980 on the value of `raise_exception`.
982 return self.xmlid_to_object(cr, uid, "%s.%s" % (module, xml_id), raise_if_not_found=True, context=context)
984 def _update_dummy(self,cr, uid, model, module, xml_id=False, store=True):
988 id = self.read(cr, uid, [self._get_id(cr, uid, module, xml_id)], ['res_id'])[0]['res_id']
989 self.loads[(module,xml_id)] = (model,id)
994 def clear_caches(self):
995 """ Clears all orm caches on the object's methods
999 self.xmlid_lookup.clear_cache(self)
1002 def unlink(self, cr, uid, ids, context=None):
1003 """ Regular unlink method, but make sure to clear the caches. """
1005 return super(ir_model_data,self).unlink(cr, uid, ids, context=context)
1007 def _update(self,cr, uid, model, module, values, xml_id=False, store=True, noupdate=False, mode='init', res_id=False, context=None):
1008 model_obj = self.pool[model]
1011 # records created during module install should not display the messages of OpenChatter
1012 context = dict(context, install_mode=True)
1013 if xml_id and ('.' in xml_id):
1014 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
1015 module, xml_id = xml_id.split('.')
1018 cr.execute('''SELECT imd.id, imd.res_id, md.id, imd.model, imd.noupdate
1019 FROM ir_model_data imd LEFT JOIN %s md ON (imd.res_id = md.id)
1020 WHERE imd.module=%%s AND imd.name=%%s''' % model_obj._table,
1022 results = cr.fetchall()
1023 for imd_id2,res_id2,real_id2,real_model,noupdate_imd in results:
1024 # In update mode, do not update a record if it's ir.model.data is flagged as noupdate
1025 if mode == 'update' and noupdate_imd:
1029 cr.execute('delete from ir_model_data where id=%s', (imd_id2,))
1032 assert model == real_model, "External ID conflict, %s already refers to a `%s` record,"\
1033 " you can't define a `%s` record with this ID." % (xml_id, real_model, model)
1034 res_id,action_id = res_id2,imd_id2
1036 if action_id and res_id:
1037 model_obj.write(cr, uid, [res_id], values, context=context)
1038 self.write(cr, uid, [action_id], {
1039 'date_update': time.strftime('%Y-%m-%d %H:%M:%S'),
1042 model_obj.write(cr, uid, [res_id], values, context=context)
1044 if model_obj._inherits:
1045 for table in model_obj._inherits:
1046 inherit_id = model_obj.browse(cr, uid,
1047 res_id,context=context)[model_obj._inherits[table]]
1048 self.create(cr, uid, {
1049 'name': xml_id + '_' + table.replace('.', '_'),
1052 'res_id': inherit_id.id,
1053 'noupdate': noupdate,
1055 self.create(cr, uid, {
1060 'noupdate': noupdate,
1063 if mode=='init' or (mode=='update' and xml_id):
1064 res_id = model_obj.create(cr, uid, values, context=context)
1066 if model_obj._inherits:
1067 for table in model_obj._inherits:
1068 inherit_id = model_obj.browse(cr, uid,
1069 res_id,context=context)[model_obj._inherits[table]]
1070 self.create(cr, uid, {
1071 'name': xml_id + '_' + table.replace('.', '_'),
1074 'res_id': inherit_id.id,
1075 'noupdate': noupdate,
1077 self.create(cr, uid, {
1082 'noupdate': noupdate
1084 if xml_id and res_id:
1085 self.loads[(module, xml_id)] = (model, res_id)
1086 for table, inherit_field in model_obj._inherits.iteritems():
1087 inherit_id = model_obj.read(cr, uid, [res_id],
1088 [inherit_field])[0][inherit_field]
1089 self.loads[(module, xml_id + '_' + table.replace('.', '_'))] = (table, inherit_id)
1092 def ir_set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=None, xml_id=False):
1093 if isinstance(models[0], (list, tuple)):
1094 model,res_id = models[0]
1100 where = ' and res_id=%s' % (res_id,)
1102 where = ' and (res_id is null)'
1105 where += ' and key2=\'%s\'' % (key2,)
1107 where += ' and (key2 is null)'
1109 cr.execute('select * from ir_values where model=%s and key=%s and name=%s'+where,(model, key, name))
1111 ir_values_obj = openerp.registry(cr.dbname)['ir.values']
1113 ir_values_obj.set(cr, uid, key, key2, name, models, value, replace, isobject, meta)
1115 cr.execute('UPDATE ir_values set value=%s WHERE model=%s and key=%s and name=%s'+where,(value, model, key, name))
1116 ir_values_obj.invalidate_cache(cr, uid, ['value'])
1119 def _module_data_uninstall(self, cr, uid, modules_to_remove, context=None):
1120 """Deletes all the records referenced by the ir.model.data entries
1121 ``ids`` along with their corresponding database backed (including
1122 dropping tables, columns, FKs, etc, as long as there is no other
1123 ir.model.data entry holding a reference to them (which indicates that
1124 they are still owned by another module).
1125 Attempts to perform the deletion in an appropriate order to maximize
1126 the chance of gracefully deleting all records.
1127 This step is performed as part of the full uninstallation of a module.
1130 ids = self.search(cr, uid, [('module', 'in', modules_to_remove)])
1132 if uid != 1 and not self.pool['ir.model.access'].check_groups(cr, uid, "base.group_system"):
1133 raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
1135 context = dict(context or {})
1136 context[MODULE_UNINSTALL_FLAG] = True # enable model/field deletion
1143 for data in self.browse(cr, uid, ids, context):
1145 res_id = data.res_id
1147 pair_to_unlink = (model, res_id)
1148 if pair_to_unlink not in to_unlink:
1149 to_unlink.append(pair_to_unlink)
1151 if model == 'workflow.activity':
1152 # Special treatment for workflow activities: temporarily revert their
1153 # incoming transition and trigger an update to force all workflow items
1154 # to move out before deleting them
1155 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,))
1156 wkf_todo.extend(cr.fetchall())
1157 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))
1158 self.invalidate_cache(cr, uid, context=context)
1160 for model,res_id in wkf_todo:
1162 openerp.workflow.trg_write(uid, model, res_id, cr)
1164 _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)
1166 def unlink_if_refcount(to_unlink):
1167 for model, res_id in to_unlink:
1168 external_ids = self.search(cr, uid, [('model', '=', model),('res_id', '=', res_id)])
1169 if set(external_ids)-ids_set:
1170 # if other modules have defined this record, we must not delete it
1172 if model == 'ir.model.fields':
1173 # Don't remove the LOG_ACCESS_COLUMNS unless _log_access
1174 # has been turned off on the model.
1175 field = self.pool[model].browse(cr, uid, [res_id], context=context)[0]
1176 if not field.exists():
1177 _logger.info('Deleting orphan external_ids %s', external_ids)
1178 self.unlink(cr, uid, external_ids)
1180 if field.name in openerp.models.LOG_ACCESS_COLUMNS and self.pool[field.model]._log_access:
1182 if field.name == 'id':
1184 _logger.info('Deleting %s@%s', res_id, model)
1186 cr.execute('SAVEPOINT record_unlink_save')
1187 self.pool[model].unlink(cr, uid, [res_id], context=context)
1189 _logger.info('Unable to delete %s@%s', res_id, model, exc_info=True)
1190 cr.execute('ROLLBACK TO SAVEPOINT record_unlink_save')
1192 cr.execute('RELEASE SAVEPOINT record_unlink_save')
1194 # Remove non-model records first, then model fields, and finish with models
1195 unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1196 if model not in ('ir.model','ir.model.fields','ir.model.constraint'))
1197 unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1198 if model == 'ir.model.constraint')
1200 ir_module_module = self.pool['ir.module.module']
1201 ir_model_constraint = self.pool['ir.model.constraint']
1202 modules_to_remove_ids = ir_module_module.search(cr, uid, [('name', 'in', modules_to_remove)], context=context)
1203 constraint_ids = ir_model_constraint.search(cr, uid, [('module', 'in', modules_to_remove_ids)], context=context)
1204 ir_model_constraint._module_data_uninstall(cr, uid, constraint_ids, context)
1206 unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1207 if model == 'ir.model.fields')
1209 ir_model_relation = self.pool['ir.model.relation']
1210 relation_ids = ir_model_relation.search(cr, uid, [('module', 'in', modules_to_remove_ids)])
1211 ir_model_relation._module_data_uninstall(cr, uid, relation_ids, context)
1213 unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1214 if model == 'ir.model')
1218 self.unlink(cr, uid, ids, context)
1220 def _process_end(self, cr, uid, modules):
1221 """ Clear records removed from updated module data.
1222 This method is called at the end of the module loading process.
1223 It is meant to removed records that are no longer present in the
1224 updated data. Such records are recognised as the one with an xml id
1225 and a module in ir_model_data and noupdate set to false, but not
1226 present in self.loads.
1231 cr.execute("""SELECT id,name,model,res_id,module FROM ir_model_data
1232 WHERE module IN %s AND res_id IS NOT NULL AND noupdate=%s ORDER BY id DESC""",
1233 (tuple(modules), False))
1234 for (id, name, model, res_id, module) in cr.fetchall():
1235 if (module,name) not in self.loads:
1236 to_unlink.append((model,res_id))
1237 if not config.get('import_partial'):
1238 for (model, res_id) in to_unlink:
1239 if model in self.pool:
1240 _logger.info('Deleting %s@%s', res_id, model)
1241 self.pool[model].unlink(cr, uid, [res_id])
1243 class wizard_model_menu(osv.osv_memory):
1244 _name = 'wizard.ir.model.menu.create'
1246 'menu_id': fields.many2one('ir.ui.menu', 'Parent Menu', required=True),
1247 'name': fields.char('Menu Name', required=True),
1250 def menu_create(self, cr, uid, ids, context=None):
1253 model_pool = self.pool.get('ir.model')
1254 for menu in self.browse(cr, uid, ids, context):
1255 model = model_pool.browse(cr, uid, context.get('model_id'), context=context)
1258 'res_model': model.model,
1259 'view_type': 'form',
1260 'view_mode': 'tree,form'
1262 action_id = self.pool.get('ir.actions.act_window').create(cr, uid, val)
1263 self.pool.get('ir.ui.menu').create(cr, uid, {
1265 'parent_id': menu.menu_id.id,
1266 'action': 'ir.actions.act_window,%d' % (action_id,),
1267 'icon': 'STOCK_INDENT'
1269 return {'type':'ir.actions.act_window_close'}
1271 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: