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 self.instanciate(cr, user, vals['model'], context)
208 model = self.pool[vals['model']]
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 self.pool.setup_models(cr, partial=(not self.pool.ready))
217 RegistryManager.signal_registry_change(cr.dbname)
220 def instanciate(self, cr, user, model, context=None):
221 if isinstance(model, unicode):
222 model = model.encode('utf-8')
224 class CustomModel(models.Model):
229 obj = CustomModel._build_model(self.pool, cr)
230 obj._rec_name = CustomModel._rec_name = (
231 'x_name' if 'x_name' in obj._columns else
232 list(obj._columns)[0] if obj._columns else
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 # remove m2m relation table for custom fields
332 # we consider the m2m relation is only one way as it's not possible
333 # to specify the relation table in the interface for custom fields
334 # TODO master: maybe use ir.model.relations for custom fields
335 if field.state == 'manual' and field.ttype == 'many2many':
336 rel_name = model._fields[field.name].relation
337 cr.execute('DROP table "%s"' % (rel_name))
338 model._pop_field(field.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 self.pool.setup_models(cr, partial=(not self.pool.ready))
356 RegistryManager.signal_registry_change(cr.dbname)
359 def create(self, cr, user, vals, context=None):
360 if 'model_id' in vals:
361 model_data = self.pool['ir.model'].browse(cr, user, vals['model_id'])
362 vals['model'] = model_data.model
365 if context and context.get('manual',False):
366 vals['state'] = 'manual'
367 if vals.get('ttype', False) == 'selection':
368 if not vals.get('selection',False):
369 raise except_orm(_('Error'), _('For selection fields, the Selection Options must be given!'))
370 self._check_selection(cr, user, vals['selection'], context=context)
371 res = super(ir_model_fields,self).create(cr, user, vals, context)
372 if vals.get('state','base') == 'manual':
373 if not vals['name'].startswith('x_'):
374 raise except_orm(_('Error'), _("Custom fields must have a name that starts with 'x_' !"))
376 if vals.get('relation',False) and not self.pool['ir.model'].search(cr, user, [('model','=',vals['relation'])]):
377 raise except_orm(_('Error'), _("Model %s does not exist!") % vals['relation'])
379 if vals['model'] in self.pool:
380 model = self.pool[vals['model']]
381 if vals['model'].startswith('x_') and vals['name'] == 'x_name':
382 model._rec_name = 'x_name'
384 if self.pool.fields_by_model is not None:
385 cr.execute('SELECT * FROM ir_model_fields WHERE id=%s', (res,))
386 self.pool.fields_by_model.setdefault(vals['model'], []).append(cr.dictfetchone())
388 model.__init__(self.pool, cr)
389 #Added context to _auto_init for special treatment to custom field for select_level
391 field_name=vals['name'],
392 field_state='manual',
393 select=vals.get('select_level', '0'),
394 update_custom_fields=True)
395 model._auto_init(cr, ctx)
396 model._auto_end(cr, ctx) # actually create FKs!
397 self.pool.setup_models(cr, partial=(not self.pool.ready))
398 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 obj, rename = column_rename
493 cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s"' % rename)
494 # This is VERY risky, but let us have this feature:
495 # we want to change the key of field in obj._fields and obj._columns
496 field = obj._pop_field(rename[1])
497 obj._add_field(rename[2], field)
500 # We have to update _columns of the model(s) and then call their
501 # _auto_init to sync the db with the model. Hopefully, since write()
502 # was called earlier, they will be in-sync before the _auto_init.
503 # Anything we don't update in _columns now will be reset from
504 # the model into ir.model.fields (db).
505 ctx = dict(context, select=vals.get('select_level', '0'),
506 update_custom_fields=True)
508 for __, patch_struct in models_patch.items():
509 obj = patch_struct[0]
510 # TODO: update new-style fields accordingly
511 for col_name, col_prop, val in patch_struct[1]:
512 setattr(obj._columns[col_name], col_prop, val)
513 obj._auto_init(cr, ctx)
514 obj._auto_end(cr, ctx) # actually create FKs!
516 if column_rename or models_patch:
517 self.pool.setup_models(cr, partial=(not self.pool.ready))
518 RegistryManager.signal_registry_change(cr.dbname)
522 class ir_model_constraint(Model):
524 This model tracks PostgreSQL foreign keys and constraints used by OpenERP
527 _name = 'ir.model.constraint'
529 'name': fields.char('Constraint', required=True, select=1,
530 help="PostgreSQL constraint or foreign key name."),
531 'definition': fields.char('Definition', help="PostgreSQL constraint definition"),
532 'model': fields.many2one('ir.model', string='Model',
533 required=True, select=1),
534 'module': fields.many2one('ir.module.module', string='Module',
535 required=True, select=1),
536 'type': fields.char('Constraint Type', required=True, size=1, select=1,
537 help="Type of the constraint: `f` for a foreign key, "
538 "`u` for other constraints."),
539 'date_update': fields.datetime('Update Date'),
540 'date_init': fields.datetime('Initialization Date')
544 ('module_name_uniq', 'unique(name, module)',
545 'Constraints with the same name are unique per module.'),
548 def _module_data_uninstall(self, cr, uid, ids, context=None):
550 Delete PostgreSQL foreign keys and constraints tracked by this model.
553 if uid != SUPERUSER_ID and not self.pool['ir.model.access'].check_groups(cr, uid, "base.group_system"):
554 raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
556 context = dict(context or {})
561 for data in self.browse(cr, uid, ids, context):
562 model = data.model.model
563 model_obj = self.pool[model]
564 name = openerp.tools.ustr(data.name)
567 # double-check we are really going to delete all the owners of this schema element
568 cr.execute("""SELECT id from ir_model_constraint where name=%s""", (data.name,))
569 external_ids = [x[0] for x in cr.fetchall()]
570 if set(external_ids)-ids_set:
571 # as installed modules have defined this element we must not delete it!
575 # test if FK exists on this table (it could be on a related m2m table, in which case we ignore it)
576 cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
577 WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('f', name, model_obj._table))
579 cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),)
580 _logger.info('Dropped FK CONSTRAINT %s@%s', name, model)
583 # test if constraint exists
584 cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
585 WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('u', name, model_obj._table))
587 cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),)
588 _logger.info('Dropped CONSTRAINT %s@%s', name, model)
590 self.unlink(cr, uid, ids, context)
592 class ir_model_relation(Model):
594 This model tracks PostgreSQL tables used to implement OpenERP many2many
597 _name = 'ir.model.relation'
599 'name': fields.char('Relation Name', required=True, select=1,
600 help="PostgreSQL table name implementing a many2many relation."),
601 'model': fields.many2one('ir.model', string='Model',
602 required=True, select=1),
603 'module': fields.many2one('ir.module.module', string='Module',
604 required=True, select=1),
605 'date_update': fields.datetime('Update Date'),
606 'date_init': fields.datetime('Initialization Date')
609 def _module_data_uninstall(self, cr, uid, ids, context=None):
611 Delete PostgreSQL many2many relations tracked by this model.
614 if uid != SUPERUSER_ID and not self.pool['ir.model.access'].check_groups(cr, uid, "base.group_system"):
615 raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
621 for data in self.browse(cr, uid, ids, context):
623 name = openerp.tools.ustr(data.name)
625 # double-check we are really going to delete all the owners of this schema element
626 cr.execute("""SELECT id from ir_model_relation where name = %s""", (data.name,))
627 external_ids = [x[0] for x in cr.fetchall()]
628 if set(external_ids)-ids_set:
629 # as installed modules have defined this element we must not delete it!
632 cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name=%s", (name,))
633 if cr.fetchone() and not name in to_drop_table:
634 to_drop_table.append(name)
636 self.unlink(cr, uid, ids, context)
638 # drop m2m relation tables
639 for table in to_drop_table:
640 cr.execute('DROP TABLE %s CASCADE'% table,)
641 _logger.info('Dropped table %s', table)
645 class ir_model_access(osv.osv):
646 _name = 'ir.model.access'
648 'name': fields.char('Name', required=True, select=True),
649 '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.'),
650 'model_id': fields.many2one('ir.model', 'Object', required=True, domain=[('osv_memory','=', False)], select=True, ondelete='cascade'),
651 'group_id': fields.many2one('res.groups', 'Group', ondelete='cascade', select=True),
652 'perm_read': fields.boolean('Read Access'),
653 'perm_write': fields.boolean('Write Access'),
654 'perm_create': fields.boolean('Create Access'),
655 'perm_unlink': fields.boolean('Delete Access'),
661 def check_groups(self, cr, uid, group):
662 grouparr = group.split('.')
665 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],))
666 return bool(cr.fetchone())
668 def check_group(self, cr, uid, model, mode, group_ids):
669 """ Check if a specific group has the access mode to the specified model"""
670 assert mode in ['read','write','create','unlink'], 'Invalid access mode'
672 if isinstance(model, BaseModel):
673 assert model._name == 'ir.model', 'Invalid model object'
674 model_name = model.name
678 if isinstance(group_ids, (int, long)):
679 group_ids = [group_ids]
680 for group_id in group_ids:
681 cr.execute("SELECT perm_" + mode + " "
682 " FROM ir_model_access a "
683 " JOIN ir_model m ON (m.id = a.model_id) "
684 " WHERE m.model = %s AND a.active IS True "
685 " AND a.group_id = %s", (model_name, group_id)
689 cr.execute("SELECT perm_" + mode + " "
690 " FROM ir_model_access a "
691 " JOIN ir_model m ON (m.id = a.model_id) "
692 " WHERE m.model = %s AND a.active IS True "
693 " AND a.group_id IS NULL", (model_name, )
697 access = bool(r and r[0])
700 # pass no groups -> no access
703 def group_names_with_access(self, cr, model_name, access_mode):
704 """Returns the names of visible groups which have been granted ``access_mode`` on
705 the model ``model_name``.
708 assert access_mode in ['read','write','create','unlink'], 'Invalid access mode: %s' % access_mode
713 JOIN ir_model m ON (a.model_id=m.id)
714 JOIN res_groups g ON (a.group_id=g.id)
715 LEFT JOIN ir_module_category c ON (c.id=g.category_id)
719 a.perm_''' + access_mode, (model_name,))
720 return [('%s/%s' % x) if x[0] else x[1] for x in cr.fetchall()]
723 def check(self, cr, uid, model, mode='read', raise_exception=True, context=None):
725 # User root have all accesses
726 # TODO: exclude xml-rpc requests
729 assert mode in ['read','write','create','unlink'], 'Invalid access mode'
731 if isinstance(model, BaseModel):
732 assert model._name == 'ir.model', 'Invalid model object'
733 model_name = model.model
737 # TransientModel records have no access rights, only an implicit access rule
738 if model_name not in self.pool:
739 _logger.error('Missing model %s' % (model_name, ))
740 elif self.pool[model_name].is_transient():
743 # We check if a specific rule exists
744 cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
745 ' FROM ir_model_access a '
746 ' JOIN ir_model m ON (m.id = a.model_id) '
747 ' JOIN res_groups_users_rel gu ON (gu.gid = a.group_id) '
748 ' WHERE m.model = %s '
750 ' AND a.active IS True '
756 # there is no specific rule. We check the generic rule
757 cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
758 ' FROM ir_model_access a '
759 ' JOIN ir_model m ON (m.id = a.model_id) '
760 ' WHERE a.group_id IS NULL '
762 ' AND a.active IS True '
767 if not r and raise_exception:
768 groups = '\n\t'.join('- %s' % g for g in self.group_names_with_access(cr, model_name, mode))
770 # Messages are declared in extenso so they are properly exported in translation terms
771 'read': _("Sorry, you are not allowed to access this document."),
772 'write': _("Sorry, you are not allowed to modify this document."),
773 'create': _("Sorry, you are not allowed to create this kind of document."),
774 'unlink': _("Sorry, you are not allowed to delete this document."),
777 msg_tail = _("Only users with the following access level are currently allowed to do that") + ":\n%s\n\n(" + _("Document model") + ": %s)"
778 msg_params = (groups, model_name)
780 msg_tail = _("Please contact your system administrator if you think this is an error.") + "\n\n(" + _("Document model") + ": %s)"
781 msg_params = (model_name,)
782 _logger.warning('Access Denied by ACLs for operation: %s, uid: %s, model: %s', mode, uid, model_name)
783 msg = '%s %s' % (msg_heads[mode], msg_tail)
784 raise openerp.exceptions.AccessError(msg % msg_params)
787 __cache_clearing_methods = []
789 def register_cache_clearing_method(self, model, method):
790 self.__cache_clearing_methods.append((model, method))
792 def unregister_cache_clearing_method(self, model, method):
794 i = self.__cache_clearing_methods.index((model, method))
795 del self.__cache_clearing_methods[i]
799 def call_cache_clearing_methods(self, cr):
800 self.invalidate_cache(cr, SUPERUSER_ID)
801 self.check.clear_cache(self) # clear the cache of check function
802 for model, method in self.__cache_clearing_methods:
803 if model in self.pool:
804 getattr(self.pool[model], method)()
807 # Check rights on actions
809 def write(self, cr, uid, ids, values, context=None):
810 self.call_cache_clearing_methods(cr)
811 res = super(ir_model_access, self).write(cr, uid, ids, values, context=context)
814 def create(self, cr, uid, values, context=None):
815 self.call_cache_clearing_methods(cr)
816 res = super(ir_model_access, self).create(cr, uid, values, context=context)
819 def unlink(self, cr, uid, ids, context=None):
820 self.call_cache_clearing_methods(cr)
821 res = super(ir_model_access, self).unlink(cr, uid, ids, context=context)
824 class ir_model_data(osv.osv):
825 """Holds external identifier keys for records in the database.
826 This has two main uses:
828 * allows easy data integration with third-party systems,
829 making import/export/sync of data possible, as records
830 can be uniquely identified across multiple systems
831 * allows tracking the origin of data installed by OpenERP
832 modules themselves, thus making it possible to later
833 update them seamlessly.
835 _name = 'ir.model.data'
836 _order = 'module,model,name'
838 def name_get(self, cr, uid, ids, context=None):
839 bymodel = defaultdict(dict)
842 for res in self.browse(cr, uid, ids, context=context):
843 bymodel[res.model][res.res_id] = res
844 names[res.id] = res.complete_name
845 #result[res.model][res.res_id] = res.id
847 for model, id_map in bymodel.iteritems():
849 ng = dict(self.pool[model].name_get(cr, uid, id_map.keys(), context=context))
853 for r in id_map.itervalues():
854 names[r.id] = ng.get(r.res_id, r.complete_name)
856 return [(i, names[i]) for i in ids]
858 def _complete_name_get(self, cr, uid, ids, prop, unknow_none, context=None):
860 for res in self.browse(cr, uid, ids, context=context):
861 result[res.id] = (res.module and (res.module + '.') or '')+res.name
865 'name': fields.char('External Identifier', required=True, select=1,
866 help="External Key/Identifier that can be used for "
867 "data integration with third-party systems"),
868 'complete_name': fields.function(_complete_name_get, type='char', string='Complete ID'),
869 'model': fields.char('Model Name', required=True, select=1),
870 'module': fields.char('Module', required=True, select=1),
871 'res_id': fields.integer('Record ID', select=1,
872 help="ID of the target record in the database"),
873 'noupdate': fields.boolean('Non Updatable'),
874 'date_update': fields.datetime('Update Date'),
875 'date_init': fields.datetime('Init Date')
878 'date_init': fields.datetime.now,
879 'date_update': fields.datetime.now,
884 ('module_name_uniq', 'unique(name, module)', 'You cannot have multiple records with the same external ID in the same module!'),
887 def __init__(self, pool, cr):
888 osv.osv.__init__(self, pool, cr)
889 # also stored in pool to avoid being discarded along with this osv instance
890 if getattr(pool, 'model_data_reference_ids', None) is None:
891 self.pool.model_data_reference_ids = {}
892 # put loads on the class, in order to share it among all instances
893 type(self).loads = self.pool.model_data_reference_ids
895 def _auto_init(self, cr, context=None):
896 super(ir_model_data, self)._auto_init(cr, context)
897 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_model_data_module_name_index\'')
898 if not cr.fetchone():
899 cr.execute('CREATE INDEX ir_model_data_module_name_index ON ir_model_data (module, name)')
902 @tools.ormcache(skiparg=3)
903 def xmlid_lookup(self, cr, uid, xmlid):
904 """Low level xmlid lookup
905 Return (id, res_model, res_id) or raise ValueError if not found
907 module, name = xmlid.split('.', 1)
908 ids = self.search(cr, uid, [('module','=',module), ('name','=', name)])
910 raise ValueError('External ID not found in the system: %s' % (xmlid))
911 # the sql constraints ensure us we have only one result
912 res = self.read(cr, uid, ids[0], ['model', 'res_id'])
913 if not res['res_id']:
914 raise ValueError('External ID not found in the system: %s' % (xmlid))
915 return ids[0], res['model'], res['res_id']
917 def xmlid_to_res_model_res_id(self, cr, uid, xmlid, raise_if_not_found=False):
918 """ Return (res_model, res_id)"""
920 return self.xmlid_lookup(cr, uid, xmlid)[1:3]
922 if raise_if_not_found:
924 return (False, False)
926 def xmlid_to_res_id(self, cr, uid, xmlid, raise_if_not_found=False):
927 """ Returns res_id """
928 return self.xmlid_to_res_model_res_id(cr, uid, xmlid, raise_if_not_found)[1]
930 def xmlid_to_object(self, cr, uid, xmlid, raise_if_not_found=False, context=None):
931 """ Return a browse_record
932 if not found and raise_if_not_found is True return None
934 t = self.xmlid_to_res_model_res_id(cr, uid, xmlid, raise_if_not_found)
935 res_model, res_id = t
937 if res_model and res_id:
938 record = self.pool[res_model].browse(cr, uid, res_id, context=context)
941 if raise_if_not_found:
942 raise ValueError('No record found for unique ID %s. It may have been deleted.' % (xmlid))
946 def _get_id(self, cr, uid, module, xml_id):
947 """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"""
948 return self.xmlid_lookup(cr, uid, "%s.%s" % (module, xml_id))[0]
950 def get_object_reference(self, cr, uid, module, xml_id):
951 """Returns (model, res_id) corresponding to a given module and xml_id (cached) or raise ValueError if not found"""
952 return self.xmlid_lookup(cr, uid, "%s.%s" % (module, xml_id))[1:3]
954 def check_object_reference(self, cr, uid, module, xml_id, raise_on_access_error=False):
955 """Returns (model, res_id) corresponding to a given module and xml_id (cached), if and only if the user has the necessary access rights
956 to see that object, otherwise raise a ValueError if raise_on_access_error is True or returns a tuple (model found, False)"""
957 model, res_id = self.get_object_reference(cr, uid, module, xml_id)
958 #search on id found in result to check if current user has read access right
959 check_right = self.pool.get(model).search(cr, uid, [('id', '=', res_id)])
962 if raise_on_access_error:
963 raise ValueError('Not enough access rights on the external ID: %s.%s' % (module, xml_id))
966 def get_object(self, cr, uid, module, xml_id, context=None):
967 """ Returns a browsable record for the given module name and xml_id.
968 If not found, raise a ValueError or return None, depending
969 on the value of `raise_exception`.
971 return self.xmlid_to_object(cr, uid, "%s.%s" % (module, xml_id), raise_if_not_found=True, context=context)
973 def _update_dummy(self,cr, uid, model, module, xml_id=False, store=True):
977 id = self.read(cr, uid, [self._get_id(cr, uid, module, xml_id)], ['res_id'])[0]['res_id']
978 self.loads[(module,xml_id)] = (model,id)
983 def clear_caches(self):
984 """ Clears all orm caches on the object's methods
988 self.xmlid_lookup.clear_cache(self)
991 def unlink(self, cr, uid, ids, context=None):
992 """ Regular unlink method, but make sure to clear the caches. """
994 return super(ir_model_data,self).unlink(cr, uid, ids, context=context)
996 def _update(self,cr, uid, model, module, values, xml_id=False, store=True, noupdate=False, mode='init', res_id=False, context=None):
997 model_obj = self.pool[model]
1000 # records created during module install should not display the messages of OpenChatter
1001 context = dict(context, install_mode=True)
1002 if xml_id and ('.' in xml_id):
1003 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
1004 module, xml_id = xml_id.split('.')
1007 cr.execute('''SELECT imd.id, imd.res_id, md.id, imd.model, imd.noupdate
1008 FROM ir_model_data imd LEFT JOIN %s md ON (imd.res_id = md.id)
1009 WHERE imd.module=%%s AND imd.name=%%s''' % model_obj._table,
1011 results = cr.fetchall()
1012 for imd_id2,res_id2,real_id2,real_model,noupdate_imd in results:
1013 # In update mode, do not update a record if it's ir.model.data is flagged as noupdate
1014 if mode == 'update' and noupdate_imd:
1018 cr.execute('delete from ir_model_data where id=%s', (imd_id2,))
1021 assert model == real_model, "External ID conflict, %s already refers to a `%s` record,"\
1022 " you can't define a `%s` record with this ID." % (xml_id, real_model, model)
1023 res_id,action_id = res_id2,imd_id2
1025 if action_id and res_id:
1026 model_obj.write(cr, uid, [res_id], values, context=context)
1027 self.write(cr, uid, [action_id], {
1028 'date_update': time.strftime('%Y-%m-%d %H:%M:%S'),
1031 model_obj.write(cr, uid, [res_id], values, context=context)
1033 if model_obj._inherits:
1034 for table in model_obj._inherits:
1035 inherit_id = model_obj.browse(cr, uid,
1036 res_id,context=context)[model_obj._inherits[table]]
1037 self.create(cr, uid, {
1038 'name': xml_id + '_' + table.replace('.', '_'),
1041 'res_id': inherit_id.id,
1042 'noupdate': noupdate,
1044 self.create(cr, uid, {
1049 'noupdate': noupdate,
1052 if mode=='init' or (mode=='update' and xml_id):
1053 res_id = model_obj.create(cr, uid, values, context=context)
1055 if model_obj._inherits:
1056 for table in model_obj._inherits:
1057 inherit_id = model_obj.browse(cr, uid,
1058 res_id,context=context)[model_obj._inherits[table]]
1059 self.create(cr, uid, {
1060 'name': xml_id + '_' + table.replace('.', '_'),
1063 'res_id': inherit_id.id,
1064 'noupdate': noupdate,
1066 self.create(cr, uid, {
1071 'noupdate': noupdate
1073 if xml_id and res_id:
1074 self.loads[(module, xml_id)] = (model, res_id)
1075 for table, inherit_field in model_obj._inherits.iteritems():
1076 inherit_id = model_obj.read(cr, uid, [res_id],
1077 [inherit_field])[0][inherit_field]
1078 self.loads[(module, xml_id + '_' + table.replace('.', '_'))] = (table, inherit_id)
1081 def ir_set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=None, xml_id=False):
1082 if isinstance(models[0], (list, tuple)):
1083 model,res_id = models[0]
1089 where = ' and res_id=%s' % (res_id,)
1091 where = ' and (res_id is null)'
1094 where += ' and key2=\'%s\'' % (key2,)
1096 where += ' and (key2 is null)'
1098 cr.execute('select * from ir_values where model=%s and key=%s and name=%s'+where,(model, key, name))
1100 ir_values_obj = openerp.registry(cr.dbname)['ir.values']
1102 ir_values_obj.set(cr, uid, key, key2, name, models, value, replace, isobject, meta)
1104 cr.execute('UPDATE ir_values set value=%s WHERE model=%s and key=%s and name=%s'+where,(value, model, key, name))
1105 ir_values_obj.invalidate_cache(cr, uid, ['value'])
1108 def _module_data_uninstall(self, cr, uid, modules_to_remove, context=None):
1109 """Deletes all the records referenced by the ir.model.data entries
1110 ``ids`` along with their corresponding database backed (including
1111 dropping tables, columns, FKs, etc, as long as there is no other
1112 ir.model.data entry holding a reference to them (which indicates that
1113 they are still owned by another module).
1114 Attempts to perform the deletion in an appropriate order to maximize
1115 the chance of gracefully deleting all records.
1116 This step is performed as part of the full uninstallation of a module.
1119 ids = self.search(cr, uid, [('module', 'in', modules_to_remove)])
1121 if uid != 1 and not self.pool['ir.model.access'].check_groups(cr, uid, "base.group_system"):
1122 raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
1124 context = dict(context or {})
1125 context[MODULE_UNINSTALL_FLAG] = True # enable model/field deletion
1132 for data in self.browse(cr, uid, ids, context):
1134 res_id = data.res_id
1136 pair_to_unlink = (model, res_id)
1137 if pair_to_unlink not in to_unlink:
1138 to_unlink.append(pair_to_unlink)
1140 if model == 'workflow.activity':
1141 # Special treatment for workflow activities: temporarily revert their
1142 # incoming transition and trigger an update to force all workflow items
1143 # to move out before deleting them
1144 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,))
1145 wkf_todo.extend(cr.fetchall())
1146 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))
1147 self.invalidate_cache(cr, uid, context=context)
1149 for model,res_id in wkf_todo:
1151 openerp.workflow.trg_write(uid, model, res_id, cr)
1153 _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)
1155 def unlink_if_refcount(to_unlink):
1156 for model, res_id in to_unlink:
1157 external_ids = self.search(cr, uid, [('model', '=', model),('res_id', '=', res_id)])
1158 if set(external_ids)-ids_set:
1159 # if other modules have defined this record, we must not delete it
1161 if model == 'ir.model.fields':
1162 # Don't remove the LOG_ACCESS_COLUMNS unless _log_access
1163 # has been turned off on the model.
1164 field = self.pool[model].browse(cr, uid, [res_id], context=context)[0]
1165 if not field.exists():
1166 _logger.info('Deleting orphan external_ids %s', external_ids)
1167 self.unlink(cr, uid, external_ids)
1169 if field.name in openerp.models.LOG_ACCESS_COLUMNS and self.pool[field.model]._log_access:
1171 if field.name == 'id':
1173 _logger.info('Deleting %s@%s', res_id, model)
1175 cr.execute('SAVEPOINT record_unlink_save')
1176 self.pool[model].unlink(cr, uid, [res_id], context=context)
1178 _logger.info('Unable to delete %s@%s', res_id, model, exc_info=True)
1179 cr.execute('ROLLBACK TO SAVEPOINT record_unlink_save')
1181 cr.execute('RELEASE SAVEPOINT record_unlink_save')
1183 # Remove non-model records first, then model fields, and finish with models
1184 unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1185 if model not in ('ir.model','ir.model.fields','ir.model.constraint'))
1186 unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1187 if model == 'ir.model.constraint')
1189 ir_module_module = self.pool['ir.module.module']
1190 ir_model_constraint = self.pool['ir.model.constraint']
1191 modules_to_remove_ids = ir_module_module.search(cr, uid, [('name', 'in', modules_to_remove)], context=context)
1192 constraint_ids = ir_model_constraint.search(cr, uid, [('module', 'in', modules_to_remove_ids)], context=context)
1193 ir_model_constraint._module_data_uninstall(cr, uid, constraint_ids, context)
1195 unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1196 if model == 'ir.model.fields')
1198 ir_model_relation = self.pool['ir.model.relation']
1199 relation_ids = ir_model_relation.search(cr, uid, [('module', 'in', modules_to_remove_ids)])
1200 ir_model_relation._module_data_uninstall(cr, uid, relation_ids, context)
1202 unlink_if_refcount((model, res_id) for model, res_id in to_unlink
1203 if model == 'ir.model')
1207 self.unlink(cr, uid, ids, context)
1209 def _process_end(self, cr, uid, modules):
1210 """ Clear records removed from updated module data.
1211 This method is called at the end of the module loading process.
1212 It is meant to removed records that are no longer present in the
1213 updated data. Such records are recognised as the one with an xml id
1214 and a module in ir_model_data and noupdate set to false, but not
1215 present in self.loads.
1220 cr.execute("""SELECT id,name,model,res_id,module FROM ir_model_data
1221 WHERE module IN %s AND res_id IS NOT NULL AND noupdate=%s ORDER BY id DESC""",
1222 (tuple(modules), False))
1223 for (id, name, model, res_id, module) in cr.fetchall():
1224 if (module,name) not in self.loads:
1225 to_unlink.append((model,res_id))
1226 if not config.get('import_partial'):
1227 for (model, res_id) in to_unlink:
1228 if model in self.pool:
1229 _logger.info('Deleting %s@%s', res_id, model)
1230 self.pool[model].unlink(cr, uid, [res_id])
1232 class wizard_model_menu(osv.osv_memory):
1233 _name = 'wizard.ir.model.menu.create'
1235 'menu_id': fields.many2one('ir.ui.menu', 'Parent Menu', required=True),
1236 'name': fields.char('Menu Name', required=True),
1239 def menu_create(self, cr, uid, ids, context=None):
1242 model_pool = self.pool.get('ir.model')
1243 for menu in self.browse(cr, uid, ids, context):
1244 model = model_pool.browse(cr, uid, context.get('model_id'), context=context)
1247 'res_model': model.model,
1248 'view_type': 'form',
1249 'view_mode': 'tree,form'
1251 action_id = self.pool.get('ir.actions.act_window').create(cr, uid, val)
1252 self.pool.get('ir.ui.menu').create(cr, uid, {
1254 'parent_id': menu.menu_id.id,
1255 'action': 'ir.actions.act_window,%d' % (action_id,),
1256 'icon': 'STOCK_INDENT'
1258 return {'type':'ir.actions.act_window_close'}
1260 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: