1 # -*- coding: utf-8 -*-
3 ##############################################################################
5 # OpenERP, Open Source Business Applications
6 # Copyright (C) 2004-2012 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 ##############################################################################
27 from openerp.osv import fields,osv
28 from openerp import netsvc, pooler, tools
29 from openerp.tools.safe_eval import safe_eval as eval
30 from openerp.tools import config
31 from openerp.tools.translate import _
32 from openerp.osv.orm import except_orm, browse_record
34 _logger = logging.getLogger(__name__)
36 MODULE_UNINSTALL_FLAG = '_force_unlink'
38 def _get_fields_type(self, cr, uid, context=None):
39 # Avoid too many nested `if`s below, as RedHat's Python 2.6
40 # break on it. See bug 939653.
41 return sorted([(k,k) for k,v in fields.__dict__.iteritems()
42 if type(v) == types.TypeType and \
43 issubclass(v, fields._column) and \
44 v != fields._column and \
45 not v._deprecated and \
46 not issubclass(v, fields.function)])
48 def _in_modules(self, cr, uid, ids, field_name, arg, context=None):
49 #pseudo-method used by fields.function in ir.model/ir.model.fields
50 module_pool = self.pool.get("ir.module.module")
51 installed_module_ids = module_pool.search(cr, uid, [('state','=','installed')])
52 installed_module_names = module_pool.read(cr, uid, installed_module_ids, ['name'], context=context)
53 installed_modules = set(x['name'] for x in installed_module_names)
56 xml_ids = osv.osv._get_xml_ids(self, cr, uid, ids)
57 for k,v in xml_ids.iteritems():
58 result[k] = ', '.join(sorted(installed_modules & set(xml_id.split('.')[0] for xml_id in v)))
62 class ir_model(osv.osv):
64 _description = "Models"
67 def _is_osv_memory(self, cr, uid, ids, field_name, arg, context=None):
68 models = self.browse(cr, uid, ids, context=context)
69 res = dict.fromkeys(ids)
71 res[model.id] = self.pool.get(model.model).is_transient()
74 def _search_osv_memory(self, cr, uid, model, name, domain, context=None):
77 __, operator, value = domain[0]
78 if operator not in ['=', '!=']:
79 raise osv.except_osv(_('Invalid search criterions'), _('The osv_memory field can only be compared with = and != operator.'))
80 value = bool(value) if operator == '=' else not bool(value)
81 all_model_ids = self.search(cr, uid, [], context=context)
82 is_osv_mem = self._is_osv_memory(cr, uid, all_model_ids, 'osv_memory', arg=None, context=context)
83 return [('id', 'in', [id for id in is_osv_mem if bool(is_osv_mem[id]) == value])]
85 def _view_ids(self, cr, uid, ids, field_name, arg, context=None):
86 models = self.browse(cr, uid, ids)
89 res[model.id] = self.pool.get("ir.ui.view").search(cr, uid, [('model', '=', model.model)])
93 'name': fields.char('Model Description', size=64, translate=True, required=True),
94 'model': fields.char('Model', size=64, required=True, select=1),
95 'info': fields.text('Information'),
96 'field_id': fields.one2many('ir.model.fields', 'model_id', 'Fields', required=True),
97 'state': fields.selection([('manual','Custom Object'),('base','Base Object')],'Type',readonly=True),
98 'access_ids': fields.one2many('ir.model.access', 'model_id', 'Access'),
99 'osv_memory': fields.function(_is_osv_memory, string='In-Memory Model', type='boolean',
100 fnct_search=_search_osv_memory,
101 help="Indicates whether this object model lives in memory only, i.e. is not persisted (osv.osv_memory)"),
102 'modules': fields.function(_in_modules, type='char', size=128, string='In Modules', help='List of modules in which the object is defined or inherited'),
103 'view_ids': fields.function(_view_ids, type='one2many', obj='ir.ui.view', string='Views'),
107 'model': lambda *a: 'x_',
108 'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
111 def _check_model_name(self, cr, uid, ids, context=None):
112 for model in self.browse(cr, uid, ids, context=context):
113 if model.state=='manual':
114 if not model.model.startswith('x_'):
116 if not re.match('^[a-z_A-Z0-9.]+$',model.model):
120 def _model_name_msg(self, cr, uid, ids, context=None):
121 return _('The Object name must start with x_ and not contain any special character !')
124 (_check_model_name, _model_name_msg, ['model']),
127 ('obj_name_uniq', 'unique (model)', 'Each model must be unique!'),
130 # overridden to allow searching both on model name (model field)
131 # and model description (name field)
132 def _name_search(self, cr, uid, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None):
135 domain = args + ['|', ('model', operator, name), ('name', operator, name)]
136 return self.name_get(cr, name_get_uid or uid,
137 super(ir_model, self).search(cr, uid, domain, limit=limit, context=context),
140 def _drop_table(self, cr, uid, ids, context=None):
141 for model in self.browse(cr, uid, ids, context):
142 model_pool = self.pool.get(model.model)
143 cr.execute('select relkind from pg_class where relname=%s', (model_pool._table,))
144 result = cr.fetchone()
145 if result and result[0] == 'v':
146 cr.execute('DROP view %s' % (model_pool._table,))
147 elif result and result[0] == 'r':
148 cr.execute('DROP TABLE %s' % (model_pool._table,))
151 def unlink(self, cr, user, ids, context=None):
152 # Prevent manual deletion of module tables
153 if context is None: context = {}
154 if isinstance(ids, (int, long)):
156 if not context.get(MODULE_UNINSTALL_FLAG) and \
157 any(model.state != 'manual' for model in self.browse(cr, user, ids, context)):
158 raise except_orm(_('Error'), _("Model '%s' contains module data and cannot be removed!") % (model.name,))
160 self._drop_table(cr, user, ids, context)
161 res = super(ir_model, self).unlink(cr, user, ids, context)
162 if not context.get(MODULE_UNINSTALL_FLAG):
163 # only reload pool for normal unlink. For module uninstall the
164 # reload is done independently in openerp.modules.loading
165 pooler.restart_pool(cr.dbname)
169 def write(self, cr, user, ids, vals, context=None):
171 context.pop('__last_update', None)
172 # Filter out operations 4 link from field id, because openerp-web
173 # always write (4,id,False) even for non dirty items
174 if 'field_id' in vals:
175 vals['field_id'] = [op for op in vals['field_id'] if op[0] != 4]
176 return super(ir_model,self).write(cr, user, ids, vals, context)
178 def create(self, cr, user, vals, context=None):
181 if context and context.get('manual',False):
182 vals['state']='manual'
183 res = super(ir_model,self).create(cr, user, vals, context)
184 if vals.get('state','base')=='manual':
185 self.instanciate(cr, user, vals['model'], context)
186 self.pool.get(vals['model']).__init__(self.pool, cr)
188 ctx.update({'field_name':vals['name'],'field_state':'manual','select':vals.get('select_level','0')})
189 self.pool.get(vals['model'])._auto_init(cr, ctx)
190 #pooler.restart_pool(cr.dbname)
193 def instanciate(self, cr, user, model, context=None):
194 class x_custom_model(osv.osv):
196 x_custom_model._name = model
197 x_custom_model._module = False
198 a = x_custom_model.create_instance(self.pool, cr)
199 if (not a._columns) or ('x_name' in a._columns.keys()):
202 x_name = a._columns.keys()[0]
203 x_custom_model._rec_name = x_name
206 class ir_model_fields(osv.osv):
207 _name = 'ir.model.fields'
208 _description = "Fields"
211 'name': fields.char('Name', required=True, size=64, select=1),
212 'model': fields.char('Object Name', size=64, required=True, select=1,
213 help="The technical name of the model this field belongs to"),
214 'relation': fields.char('Object Relation', size=64,
215 help="For relationship fields, the technical name of the target model"),
216 'relation_field': fields.char('Relation Field', size=64,
217 help="For one2many fields, the field on the target model that implement the opposite many2one relationship"),
218 'model_id': fields.many2one('ir.model', 'Model', required=True, select=True, ondelete='cascade',
219 help="The model this field belongs to"),
220 'field_description': fields.char('Field Label', required=True, size=256),
221 'ttype': fields.selection(_get_fields_type, 'Field Type',size=64, required=True),
222 'selection': fields.char('Selection Options',size=128, help="List of options for a selection field, "
223 "specified as a Python expression defining a list of (key, label) pairs. "
224 "For example: [('blue','Blue'),('yellow','Yellow')]"),
225 'required': fields.boolean('Required'),
226 'readonly': fields.boolean('Readonly'),
227 'select_level': fields.selection([('0','Not Searchable'),('1','Always Searchable'),('2','Advanced Search (deprecated)')],'Searchable', required=True),
228 'translate': fields.boolean('Translate', help="Whether values for this field can be translated (enables the translation mechanism for that field)"),
229 'size': fields.integer('Size'),
230 'state': fields.selection([('manual','Custom Field'),('base','Base Field')],'Type', required=True, readonly=True, select=1),
231 'on_delete': fields.selection([('cascade','Cascade'),('set null','Set NULL')], 'On Delete', help='On delete property for many2one fields'),
232 'domain': fields.char('Domain', size=256, help="The optional domain to restrict possible values for relationship fields, "
233 "specified as a Python expression defining a list of triplets. "
234 "For example: [('color','=','red')]"),
235 'groups': fields.many2many('res.groups', 'ir_model_fields_group_rel', 'field_id', 'group_id', 'Groups'),
236 'view_load': fields.boolean('View Auto-Load'),
237 'selectable': fields.boolean('Selectable'),
238 'modules': fields.function(_in_modules, type='char', size=128, string='In Modules', help='List of modules in which the field is defined'),
239 'serialization_field_id': fields.many2one('ir.model.fields', 'Serialization Field', domain = "[('ttype','=','serialized')]",
240 ondelete='cascade', help="If set, this field will be stored in the sparse "
241 "structure of the serialization field, instead "
242 "of having its own database column. This cannot be "
243 "changed after creation."),
245 _rec_name='field_description'
251 'state': lambda self,cr,uid,ctx={}: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
252 'on_delete': 'set null',
255 'field_description': '',
260 def _check_selection(self, cr, uid, selection, context=None):
262 selection_list = eval(selection)
264 _logger.warning('Invalid selection list definition for fields.selection', exc_info=True)
265 raise except_orm(_('Error'),
266 _("The Selection Options expression is not a valid Pythonic expression." \
267 "Please provide an expression in the [('key','Label'), ...] format."))
270 if not (isinstance(selection_list, list) and selection_list):
273 for item in selection_list:
274 if not (isinstance(item, (tuple,list)) and len(item) == 2):
279 raise except_orm(_('Error'),
280 _("The Selection Options expression is must be in the [('key','Label'), ...] format!"))
283 def _size_gt_zero_msg(self, cr, user, ids, context=None):
284 return _('Size of the field can never be less than 1 !')
287 ('size_gt_zero', 'CHECK (size>0)',_size_gt_zero_msg ),
290 def _drop_column(self, cr, uid, ids, context=None):
291 for field in self.browse(cr, uid, ids, context):
292 model = self.pool.get(field.model)
293 cr.execute('select relkind from pg_class where relname=%s', (model._table,))
294 result = cr.fetchone()
295 cr.execute("SELECT column_name FROM information_schema.columns WHERE table_name ='%s' and column_name='%s'" %(model._table, field.name))
296 column_name = cr.fetchone()
297 if column_name and (result and result[0] == 'r'):
298 cr.execute('ALTER table "%s" DROP column "%s" cascade' % (model._table, field.name))
299 model._columns.pop(field.name, None)
302 def unlink(self, cr, user, ids, context=None):
303 # Prevent manual deletion of module columns
304 if context is None: context = {}
305 if isinstance(ids, (int, long)):
307 if not context.get(MODULE_UNINSTALL_FLAG) and \
308 any(field.state != 'manual' for field in self.browse(cr, user, ids, context)):
309 raise except_orm(_('Error'), _("This column contains module data and cannot be removed!"))
311 self._drop_column(cr, user, ids, context)
312 res = super(ir_model_fields, self).unlink(cr, user, ids, context)
315 def create(self, cr, user, vals, context=None):
316 if 'model_id' in vals:
317 model_data = self.pool.get('ir.model').browse(cr, user, vals['model_id'])
318 vals['model'] = model_data.model
321 if context and context.get('manual',False):
322 vals['state'] = 'manual'
323 if vals.get('ttype', False) == 'selection':
324 if not vals.get('selection',False):
325 raise except_orm(_('Error'), _('For selection fields, the Selection Options must be given!'))
326 self._check_selection(cr, user, vals['selection'], context=context)
327 res = super(ir_model_fields,self).create(cr, user, vals, context)
328 if vals.get('state','base') == 'manual':
329 if not vals['name'].startswith('x_'):
330 raise except_orm(_('Error'), _("Custom fields must have a name that starts with 'x_' !"))
332 if vals.get('relation',False) and not self.pool.get('ir.model').search(cr, user, [('model','=',vals['relation'])]):
333 raise except_orm(_('Error'), _("Model %s does not exist!") % vals['relation'])
335 if self.pool.get(vals['model']):
336 self.pool.get(vals['model']).__init__(self.pool, cr)
337 #Added context to _auto_init for special treatment to custom field for select_level
339 ctx.update({'field_name':vals['name'],'field_state':'manual','select':vals.get('select_level','0'),'update_custom_fields':True})
340 self.pool.get(vals['model'])._auto_init(cr, ctx)
344 def write(self, cr, user, ids, vals, context=None):
347 if context and context.get('manual',False):
348 vals['state'] = 'manual'
350 #For the moment renaming a sparse field or changing the storing system is not allowed. This may be done later
351 if 'serialization_field_id' in vals or 'name' in vals:
352 for field in self.browse(cr, user, ids, context=context):
353 if 'serialization_field_id' in vals and field.serialization_field_id.id != vals['serialization_field_id']:
354 raise except_orm(_('Error!'), _('Changing the storing system for field "%s" is not allowed.')%field.name)
355 if field.serialization_field_id and (field.name != vals['name']):
356 raise except_orm(_('Error!'), _('Renaming sparse field "%s" is not allowed')%field.name)
358 column_rename = None # if set, *one* column can be renamed here
360 models_patch = {} # structs of (obj, [(field, prop, change_to),..])
361 # data to be updated on the orm model
363 # static table of properties
364 model_props = [ # (our-name, fields.prop, set_fn)
365 ('field_description', 'string', str),
366 ('required', 'required', bool),
367 ('readonly', 'readonly', bool),
368 ('domain', '_domain', eval),
369 ('size', 'size', int),
370 ('on_delete', 'ondelete', str),
371 ('translate', 'translate', bool),
372 ('view_load', 'view_load', bool),
373 ('selectable', 'selectable', bool),
374 ('select_level', 'select', int),
375 ('selection', 'selection', eval),
379 checked_selection = False # need only check it once, so defer
381 for item in self.browse(cr, user, ids, context=context):
382 if not (obj and obj._name == item.model):
383 obj = self.pool.get(item.model)
385 if item.state != 'manual':
386 raise except_orm(_('Error!'),
387 _('Properties of base fields cannot be altered in this manner! '
388 'Please modify them through Python code, '
389 'preferably through a custom addon!'))
391 if item.ttype == 'selection' and 'selection' in vals \
392 and not checked_selection:
393 self._check_selection(cr, user, vals['selection'], context=context)
394 checked_selection = True
396 final_name = item.name
397 if 'name' in vals and vals['name'] != item.name:
398 # We need to rename the column
400 raise except_orm(_('Error!'), _('Can only rename one column at a time!'))
401 if vals['name'] in obj._columns:
402 raise except_orm(_('Error!'), _('Cannot rename column to %s, because that column already exists!') % vals['name'])
403 if vals.get('state', 'base') == 'manual' and not vals['name'].startswith('x_'):
404 raise except_orm(_('Error!'), _('New column name must still start with x_ , because it is a custom field!'))
405 if '\'' in vals['name'] or '"' in vals['name'] or ';' in vals['name']:
406 raise ValueError('Invalid character in column name')
407 column_rename = (obj, (obj._table, item.name, vals['name']))
408 final_name = vals['name']
410 if 'model_id' in vals and vals['model_id'] != item.model_id:
411 raise except_orm(_("Error!"), _("Changing the model of a field is forbidden!"))
413 if 'ttype' in vals and vals['ttype'] != item.ttype:
414 raise except_orm(_("Error!"), _("Changing the type of a column is not yet supported. "
415 "Please drop it and create it again!"))
417 # We don't check the 'state', because it might come from the context
418 # (thus be set for multiple fields) and will be ignored anyway.
420 models_patch.setdefault(obj._name, (obj,[]))
421 # find out which properties (per model) we need to update
422 for field_name, field_property, set_fn in model_props:
423 if field_name in vals:
424 property_value = set_fn(vals[field_name])
425 if getattr(obj._columns[item.name], field_property) != property_value:
426 models_patch[obj._name][1].append((final_name, field_property, property_value))
427 # our dict is ready here, but no properties are changed so far
429 # These shall never be written (modified)
430 for column_name in ('model_id', 'model', 'state'):
431 if column_name in vals:
432 del vals[column_name]
434 res = super(ir_model_fields,self).write(cr, user, ids, vals, context=context)
437 cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s"' % column_rename[1])
438 # This is VERY risky, but let us have this feature:
439 # we want to change the key of column in obj._columns dict
440 col = column_rename[0]._columns.pop(column_rename[1][1]) # take object out, w/o copy
441 column_rename[0]._columns[column_rename[1][2]] = col
444 # We have to update _columns of the model(s) and then call their
445 # _auto_init to sync the db with the model. Hopefully, since write()
446 # was called earlier, they will be in-sync before the _auto_init.
447 # Anything we don't update in _columns now will be reset from
448 # the model into ir.model.fields (db).
450 ctx.update({'select': vals.get('select_level','0'),'update_custom_fields':True})
452 for __, patch_struct in models_patch.items():
453 obj = patch_struct[0]
454 for col_name, col_prop, val in patch_struct[1]:
455 setattr(obj._columns[col_name], col_prop, val)
456 obj._auto_init(cr, ctx)
461 class ir_model_access(osv.osv):
462 _name = 'ir.model.access'
464 'name': fields.char('Name', size=64, required=True, select=True),
465 '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.'),
466 'model_id': fields.many2one('ir.model', 'Object', required=True, domain=[('osv_memory','=', False)], select=True, ondelete='cascade'),
467 'group_id': fields.many2one('res.groups', 'Group', ondelete='cascade', select=True),
468 'perm_read': fields.boolean('Read Access'),
469 'perm_write': fields.boolean('Write Access'),
470 'perm_create': fields.boolean('Create Access'),
471 'perm_unlink': fields.boolean('Delete Access'),
478 def check_groups(self, cr, uid, group):
479 grouparr = group.split('.')
482 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],))
483 return bool(cr.fetchone())
485 def check_group(self, cr, uid, model, mode, group_ids):
486 """ Check if a specific group has the access mode to the specified model"""
487 assert mode in ['read','write','create','unlink'], 'Invalid access mode'
489 if isinstance(model, browse_record):
490 assert model._table_name == 'ir.model', 'Invalid model object'
491 model_name = model.name
495 if isinstance(group_ids, (int, long)):
496 group_ids = [group_ids]
497 for group_id in group_ids:
498 cr.execute("SELECT perm_" + mode + " "
499 " FROM ir_model_access a "
500 " JOIN ir_model m ON (m.id = a.model_id) "
501 " WHERE m.model = %s AND a.active IS True "
502 " AND a.group_id = %s", (model_name, group_id)
506 cr.execute("SELECT perm_" + mode + " "
507 " FROM ir_model_access a "
508 " JOIN ir_model m ON (m.id = a.model_id) "
509 " WHERE m.model = %s AND a.active IS True "
510 " AND a.group_id IS NULL", (model_name, )
514 access = bool(r and r[0])
517 # pass no groups -> no access
520 def group_names_with_access(self, cr, model_name, access_mode):
521 """Returns the names of visible groups which have been granted ``access_mode`` on
522 the model ``model_name``.
525 assert access_mode in ['read','write','create','unlink'], 'Invalid access mode: %s' % access_mode
530 JOIN ir_model m ON (a.model_id=m.id)
531 JOIN res_groups g ON (a.group_id=g.id)
532 LEFT JOIN ir_module_category c ON (c.id=g.category_id)
536 a.perm_''' + access_mode, (model_name,))
537 return [('%s/%s' % x) if x[0] else x[1] for x in cr.fetchall()]
540 def check(self, cr, uid, model, mode='read', raise_exception=True, context=None):
542 # User root have all accesses
543 # TODO: exclude xml-rpc requests
546 assert mode in ['read','write','create','unlink'], 'Invalid access mode'
548 if isinstance(model, browse_record):
549 assert model._table_name == 'ir.model', 'Invalid model object'
550 model_name = model.model
554 # TransientModel records have no access rights, only an implicit access rule
555 if self.pool.get(model_name).is_transient():
558 # We check if a specific rule exists
559 cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
560 ' FROM ir_model_access a '
561 ' JOIN ir_model m ON (m.id = a.model_id) '
562 ' JOIN res_groups_users_rel gu ON (gu.gid = a.group_id) '
563 ' WHERE m.model = %s '
565 ' AND a.active IS True '
571 # there is no specific rule. We check the generic rule
572 cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
573 ' FROM ir_model_access a '
574 ' JOIN ir_model m ON (m.id = a.model_id) '
575 ' WHERE a.group_id IS NULL '
577 ' AND a.active IS True '
582 if not r and raise_exception:
583 groups = '\n\t'.join('- %s' % g for g in self.group_names_with_access(cr, model_name, mode))
585 # Messages are declared in extenso so they are properly exported in translation terms
586 'read': _("Sorry, you are not allowed to access this document."),
587 'write': _("Sorry, you are not allowed to modify this document."),
588 'create': _("Sorry, you are not allowed to create this kind of document."),
589 'unlink': _("Sorry, you are not allowed to delete this document."),
592 msg_tail = _("Only users with the following access level are currently allowed to do that") + ":\n%s\n\n(" + _("Document model") + ": %s)"
593 msg_params = (groups, model_name)
595 msg_tail = _("Please contact your system administrator if you think this is an error.") + "\n\n(" + _("Document model") + ": %s)"
596 msg_params = (model_name,)
597 _logger.warning('Access Denied by ACLs for operation: %s, uid: %s, model: %s', mode, uid, model_name)
598 msg = '%s %s' % (msg_heads[mode], msg_tail)
599 raise except_orm(_('Access Denied'), msg % msg_params)
602 __cache_clearing_methods = []
604 def register_cache_clearing_method(self, model, method):
605 self.__cache_clearing_methods.append((model, method))
607 def unregister_cache_clearing_method(self, model, method):
609 i = self.__cache_clearing_methods.index((model, method))
610 del self.__cache_clearing_methods[i]
614 def call_cache_clearing_methods(self, cr):
615 self.check.clear_cache(self) # clear the cache of check function
616 for model, method in self.__cache_clearing_methods:
617 object_ = self.pool.get(model)
619 getattr(object_, method)()
622 # Check rights on actions
624 def write(self, cr, uid, *args, **argv):
625 self.call_cache_clearing_methods(cr)
626 res = super(ir_model_access, self).write(cr, uid, *args, **argv)
629 def create(self, cr, uid, *args, **argv):
630 self.call_cache_clearing_methods(cr)
631 res = super(ir_model_access, self).create(cr, uid, *args, **argv)
634 def unlink(self, cr, uid, *args, **argv):
635 self.call_cache_clearing_methods(cr)
636 res = super(ir_model_access, self).unlink(cr, uid, *args, **argv)
641 class ir_model_data(osv.osv):
642 """Holds external identifier keys for records in the database.
643 This has two main uses:
645 * allows easy data integration with third-party systems,
646 making import/export/sync of data possible, as records
647 can be uniquely identified across multiple systems
648 * allows tracking the origin of data installed by OpenERP
649 modules themselves, thus making it possible to later
650 update them seamlessly.
652 _name = 'ir.model.data'
653 _order = 'module,model,name'
654 def _display_name_get(self, cr, uid, ids, prop, unknow_none, context=None):
657 for res in self.browse(cr, uid, ids, context=context):
659 result.setdefault(res.model, {})
660 result[res.model][res.res_id] = res.id
661 result2[res.id] = False
665 r = dict(self.pool.get(model).name_get(cr, uid, result[model].keys(), context=context))
666 for key,val in result[model].items():
667 result2[val] = r.get(key, False)
669 # some object have no valid name_get implemented, we accept this
673 def _complete_name_get(self, cr, uid, ids, prop, unknow_none, context=None):
675 for res in self.browse(cr, uid, ids, context=context):
676 result[res.id] = (res.module and (res.module + '.') or '')+res.name
680 'name': fields.char('External Identifier', required=True, size=128, select=1,
681 help="External Key/Identifier that can be used for "
682 "data integration with third-party systems"),
683 'complete_name': fields.function(_complete_name_get, type='char', string='Complete ID'),
684 'display_name': fields.function(_display_name_get, type='char', string='Record Name'),
685 'model': fields.char('Model Name', required=True, size=64, select=1),
686 'module': fields.char('Module', required=True, size=64, select=1),
687 'res_id': fields.integer('Record ID', select=1,
688 help="ID of the target record in the database"),
689 'noupdate': fields.boolean('Non Updatable'),
690 'date_update': fields.datetime('Update Date'),
691 'date_init': fields.datetime('Init Date')
694 'date_init': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
695 'date_update': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
700 ('module_name_uniq', 'unique(name, module)', 'You cannot have multiple records with the same external ID in the same module!'),
703 def __init__(self, pool, cr):
704 osv.osv.__init__(self, pool, cr)
706 # also stored in pool to avoid being discarded along with this osv instance
707 if getattr(pool, 'model_data_reference_ids', None) is None:
708 self.pool.model_data_reference_ids = {}
710 self.loads = self.pool.model_data_reference_ids
712 def _auto_init(self, cr, context=None):
713 super(ir_model_data, self)._auto_init(cr, context)
714 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_model_data_module_name_index\'')
715 if not cr.fetchone():
716 cr.execute('CREATE INDEX ir_model_data_module_name_index ON ir_model_data (module, name)')
719 def _get_id(self, cr, uid, module, xml_id):
720 """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"""
721 ids = self.search(cr, uid, [('module','=',module), ('name','=', xml_id)])
723 raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))
724 # the sql constraints ensure us we have only one result
728 def get_object_reference(self, cr, uid, module, xml_id):
729 """Returns (model, res_id) corresponding to a given module and xml_id (cached) or raise ValueError if not found"""
730 data_id = self._get_id(cr, uid, module, xml_id)
731 res = self.read(cr, uid, data_id, ['model', 'res_id'])
732 if not res['res_id']:
733 raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))
734 return (res['model'], res['res_id'])
736 def get_object(self, cr, uid, module, xml_id, context=None):
737 """Returns a browsable record for the given module name and xml_id or raise ValueError if not found"""
738 res_model, res_id = self.get_object_reference(cr, uid, module, xml_id)
739 result = self.pool.get(res_model).browse(cr, uid, res_id, context=context)
740 if not result.exists():
741 raise ValueError('No record found for unique ID %s.%s. It may have been deleted.' % (module, xml_id))
744 def _update_dummy(self,cr, uid, model, module, xml_id=False, store=True):
748 id = self.read(cr, uid, [self._get_id(cr, uid, module, xml_id)], ['res_id'])[0]['res_id']
749 self.loads[(module,xml_id)] = (model,id)
755 def unlink(self, cr, uid, ids, context=None):
756 """ Regular unlink method, but make sure to clear the caches. """
757 self._get_id.clear_cache(self)
758 self.get_object_reference.clear_cache(self)
759 return super(ir_model_data,self).unlink(cr, uid, ids, context=context)
761 def _update(self,cr, uid, model, module, values, xml_id=False, store=True, noupdate=False, mode='init', res_id=False, context=None):
762 model_obj = self.pool.get(model)
765 # records created during module install should not display the messages of OpenChatter
766 context = dict(context, install_mode=True)
767 if xml_id and ('.' in xml_id):
768 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)
769 module, xml_id = xml_id.split('.')
770 if (not xml_id) and (not self.doinit):
774 cr.execute('''SELECT imd.id, imd.res_id, md.id, imd.model
775 FROM ir_model_data imd LEFT JOIN %s md ON (imd.res_id = md.id)
776 WHERE imd.module=%%s AND imd.name=%%s''' % model_obj._table,
778 results = cr.fetchall()
779 for imd_id2,res_id2,real_id2,real_model in results:
781 self._get_id.clear_cache(self, uid, module, xml_id)
782 self.get_object_reference.clear_cache(self, uid, module, xml_id)
783 cr.execute('delete from ir_model_data where id=%s', (imd_id2,))
786 assert model == real_model, "External ID conflict, %s already refers to a `%s` record,"\
787 " you can't define a `%s` record with this ID." % (xml_id, real_model, model)
788 res_id,action_id = res_id2,imd_id2
790 if action_id and res_id:
791 model_obj.write(cr, uid, [res_id], values, context=context)
792 self.write(cr, uid, [action_id], {
793 'date_update': time.strftime('%Y-%m-%d %H:%M:%S'),
796 model_obj.write(cr, uid, [res_id], values, context=context)
798 self.create(cr, uid, {
803 'noupdate': noupdate,
805 if model_obj._inherits:
806 for table in model_obj._inherits:
807 inherit_id = model_obj.browse(cr, uid,
808 res_id,context=context)[model_obj._inherits[table]]
809 self.create(cr, uid, {
810 'name': xml_id + '_' + table.replace('.', '_'),
813 'res_id': inherit_id.id,
814 'noupdate': noupdate,
817 if mode=='init' or (mode=='update' and xml_id):
818 res_id = model_obj.create(cr, uid, values, context=context)
820 self.create(cr, uid, {
827 if model_obj._inherits:
828 for table in model_obj._inherits:
829 inherit_id = model_obj.browse(cr, uid,
830 res_id,context=context)[model_obj._inherits[table]]
831 self.create(cr, uid, {
832 'name': xml_id + '_' + table.replace('.', '_'),
835 'res_id': inherit_id.id,
836 'noupdate': noupdate,
840 self.loads[(module, xml_id)] = (model, res_id)
841 if model_obj._inherits:
842 for table in model_obj._inherits:
843 inherit_field = model_obj._inherits[table]
844 inherit_id = model_obj.read(cr, uid, res_id,
845 [inherit_field])[inherit_field]
846 self.loads[(module, xml_id + '_' + \
847 table.replace('.', '_'))] = (table, inherit_id)
850 def ir_set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=None, xml_id=False):
851 if type(models[0])==type([]) or type(models[0])==type(()):
852 model,res_id = models[0]
858 where = ' and res_id=%s' % (res_id,)
860 where = ' and (res_id is null)'
863 where += ' and key2=\'%s\'' % (key2,)
865 where += ' and (key2 is null)'
867 cr.execute('select * from ir_values where model=%s and key=%s and name=%s'+where,(model, key, name))
870 ir_values_obj = pooler.get_pool(cr.dbname).get('ir.values')
871 res = ir_values_obj.set(cr, uid, key, key2, name, models, value, replace, isobject, meta)
873 cr.execute('UPDATE ir_values set value=%s WHERE model=%s and key=%s and name=%s'+where,(value, model, key, name))
876 def _module_data_uninstall(self, cr, uid, modules_to_remove, context=None):
877 """Deletes all the records referenced by the ir.model.data entries
878 ``ids`` along with their corresponding database backed (including
879 dropping tables, columns, FKs, etc, as long as there is no other
880 ir.model.data entry holding a reference to them (which indicates that
881 they are still owned by another module).
882 Attempts to perform the deletion in an appropriate order to maximize
883 the chance of gracefully deleting all records.
884 This step is performed as part of the full uninstallation of a module.
887 ids = self.search(cr, uid, [('module', 'in', modules_to_remove)])
889 if uid != 1 and not self.pool.get('ir.model.access').check_groups(cr, uid, "base.group_system"):
890 raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
892 context = dict(context or {})
893 context[MODULE_UNINSTALL_FLAG] = True # enable model/field deletion
900 for data in self.browse(cr, uid, ids, context):
903 model_obj = self.pool.get(model)
904 name = tools.ustr(data.name)
906 pair_to_unlink = (model, res_id)
907 if pair_to_unlink not in to_unlink:
908 to_unlink.append(pair_to_unlink)
910 if model == 'workflow.activity':
911 # Special treatment for workflow activities: temporarily revert their
912 # incoming transition and trigger an update to force all workflow items
913 # to move out before deleting them
914 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,))
915 wkf_todo.extend(cr.fetchall())
916 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))
918 wf_service = netsvc.LocalService("workflow")
919 for model,res_id in wkf_todo:
921 wf_service.trg_write(uid, model, res_id, cr)
923 _logger.info('Unable to force processing of workflow for item %s@%s in order to leave activity to be deleted', res_id, model)
925 def unlink_if_refcount(to_unlink):
926 for model, res_id in to_unlink:
927 external_ids = self.search(cr, uid, [('model', '=', model),('res_id', '=', res_id)])
928 if (set(external_ids)-ids_set):
929 # if other modules have defined this record, we must not delete it
931 _logger.info('Deleting %s@%s', res_id, model)
933 self.pool.get(model).unlink(cr, uid, [res_id], context=context)
935 _logger.info('Unable to delete %s@%s', res_id, model, exc_info=True)
937 # Remove non-model records first, then model fields, and finish with models
938 unlink_if_refcount((model, res_id) for model, res_id in to_unlink
939 if model not in ('ir.model','ir.model.fields'))
940 unlink_if_refcount((model, res_id) for model, res_id in to_unlink
941 if model == 'ir.model.fields')
943 ir_model_relation = self.pool.get('ir.model.relation')
944 relation_ids = ir_model_relation.search(cr, uid, [('module', 'in', modules_to_remove)])
945 ir_model_relation._module_data_uninstall(cr, uid, relation_ids, context)
947 unlink_if_refcount((model, res_id) for model, res_id in to_unlink
948 if model == 'ir.model')
952 self.unlink(cr, uid, ids, context)
954 def _process_end(self, cr, uid, modules):
955 """ Clear records removed from updated module data.
956 This method is called at the end of the module loading process.
957 It is meant to removed records that are no longer present in the
958 updated data. Such records are recognised as the one with an xml id
959 and a module in ir_model_data and noupdate set to false, but not
960 present in self.loads.
965 cr.execute("""SELECT id,name,model,res_id,module FROM ir_model_data
966 WHERE module IN %s AND res_id IS NOT NULL AND noupdate=%s""",
967 (tuple(modules), False))
968 for (id, name, model, res_id, module) in cr.fetchall():
969 if (module,name) not in self.loads:
970 to_unlink.append((model,res_id))
971 if not config.get('import_partial'):
972 for (model, res_id) in to_unlink:
973 if self.pool.get(model):
974 _logger.info('Deleting %s@%s', res_id, model)
975 self.pool.get(model).unlink(cr, uid, [res_id])
978 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: