1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from operator import itemgetter
23 from osv import fields,osv
26 from osv.orm import except_orm, browse_record
30 from tools import config
31 from tools.translate import _
34 def _get_fields_type(self, cr, uid, context=None):
35 cr.execute('select distinct ttype,ttype from ir_model_fields')
36 field_types = cr.fetchall()
37 field_types_copy = field_types
38 for types in field_types_copy:
39 if not hasattr(fields,types[0]):
40 field_types.remove(types)
43 class ir_model(osv.osv):
45 _description = "Objects"
48 def _is_osv_memory(self, cr, uid, ids, field_name, arg, context=None):
49 models = self.browse(cr, uid, ids, context=context)
50 res = dict.fromkeys(ids)
52 res[model.id] = isinstance(self.pool.get(model.model), osv.osv_memory)
55 def _search_osv_memory(self, cr, uid, model, name, domain, context=None):
58 field, operator, value = domain[0]
59 if operator not in ['=', '!=']:
60 raise osv.except_osv('Invalid search criterions','The osv_memory field can only be compared with = and != operator.')
61 value = bool(value) if operator == '=' else not bool(value)
62 all_model_ids = self.search(cr, uid, [], context=context)
63 is_osv_mem = self._is_osv_memory(cr, uid, all_model_ids, 'osv_memory', arg=None, context=context)
64 return [('id', 'in', [id for id in is_osv_mem if bool(is_osv_mem[id]) == value])]
67 'name': fields.char('Object Name', size=64, translate=True, required=True),
68 'model': fields.char('Object', size=64, required=True, select=1),
69 'info': fields.text('Information'),
70 'field_id': fields.one2many('ir.model.fields', 'model_id', 'Fields', required=True),
71 'state': fields.selection([('manual','Custom Object'),('base','Base Object')],'Type',readonly=True),
72 'access_ids': fields.one2many('ir.model.access', 'model_id', 'Access'),
73 'osv_memory': fields.function(_is_osv_memory, method=True, string='In-memory model', type='boolean',
74 fnct_search=_search_osv_memory,
75 help="Indicates whether this object model lives in memory only, i.e. is not persisted (osv.osv_memory)")
78 'model': lambda *a: 'x_',
79 'state': lambda self,cr,uid,ctx={}: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
81 def _check_model_name(self, cr, uid, ids):
82 for model in self.browse(cr, uid, ids):
83 if model.state=='manual':
84 if not model.model.startswith('x_'):
86 if not re.match('^[a-z_A-Z0-9.]+$',model.model):
91 (_check_model_name, 'The Object name must start with x_ and not contain any special character !', ['model']),
94 # overridden to allow searching both on model name (model field)
95 # and model description (name field)
96 def name_search(self, cr, uid, name='', args=None, operator='ilike', context=None, limit=None):
99 domain = args + ['|', ('model', operator, name), ('name', operator, name)]
100 return super(ir_model, self).name_search(cr, uid, None, domain,
101 operator=operator, limit=limit, context=context)
104 def unlink(self, cr, user, ids, context=None):
105 for model in self.browse(cr, user, ids, context):
106 if model.state != 'manual':
107 raise except_orm(_('Error'), _("You can not remove the model '%s' !") %(model.name,))
108 res = super(ir_model, self).unlink(cr, user, ids, context)
109 pooler.restart_pool(cr.dbname)
112 def write(self, cr, user, ids, vals, context=None):
114 context.pop('__last_update', None)
115 return super(ir_model,self).write(cr, user, ids, vals, context)
117 def create(self, cr, user, vals, context=None):
120 if context and context.get('manual',False):
121 vals['state']='manual'
122 res = super(ir_model,self).create(cr, user, vals, context)
123 if vals.get('state','base')=='manual':
124 self.instanciate(cr, user, vals['model'], context)
125 self.pool.get(vals['model']).__init__(self.pool, cr)
127 ctx.update({'field_name':vals['name'],'field_state':'manual','select':vals.get('select_level','0')})
128 self.pool.get(vals['model'])._auto_init(cr, ctx)
129 #pooler.restart_pool(cr.dbname)
132 def instanciate(self, cr, user, model, context={}):
133 class x_custom_model(osv.osv):
135 x_custom_model._name = model
136 x_custom_model._module = False
137 a = x_custom_model.createInstance(self.pool, '', cr)
138 if (not a._columns) or ('x_name' in a._columns.keys()):
141 x_name = a._columns.keys()[0]
142 x_custom_model._rec_name = x_name
146 class ir_model_grid(osv.osv):
147 _name = 'ir.model.grid'
149 _inherit = 'ir.model'
150 _description = "Objects Security Grid"
152 def create(self, cr, uid, vals, context=None):
153 raise osv.except_osv('Error !', 'You cannot add an entry to this view !')
155 def unlink(self, *args, **argv):
156 raise osv.except_osv('Error !', 'You cannot delete an entry of this view !')
158 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
159 result = super(osv.osv, self).read(cr, uid, ids, fields, context, load)
160 allgr = self.pool.get('res.groups').search(cr, uid, [], context=context)
161 acc_obj = self.pool.get('ir.model.access')
163 if not isinstance(result,list):
167 rules = acc_obj.search(cr, uid, [('model_id', '=', res['id'])])
168 rules_br = acc_obj.browse(cr, uid, rules, context=context)
170 res['group_'+str(g)] = ''
171 for rule in rules_br:
174 perm_list.append('r')
176 perm_list.append('w')
178 perm_list.append('c')
180 perm_list.append('u')
181 perms = ",".join(perm_list)
183 res['group_%d'%rule.group_id.id] = perms
185 res['group_0'] = perms
189 # This function do not write fields from ir.model because
190 # access rights may be different for managing models and
193 def write(self, cr, uid, ids, vals, context=None):
194 vals_new = vals.copy()
195 acc_obj = self.pool.get('ir.model.access')
196 for grid in self.browse(cr, uid, ids, context=context):
198 perms_rel = ['read','write','create','unlink']
200 if not val[:6]=='group_':
202 group_id = int(val[6:]) or False
203 rules = acc_obj.search(cr, uid, [('model_id', '=', model_id),('group_id', '=', group_id)])
205 rules = [acc_obj.create(cr, uid, {
210 vals2 = dict(map(lambda x: ('perm_'+x, x[0] in (vals[val] or '')), perms_rel))
211 acc_obj.write(cr, uid, rules, vals2, context=context)
214 def fields_get(self, cr, uid, fields=None, context=None):
215 result = super(ir_model_grid, self).fields_get(cr, uid, fields, context)
216 groups = self.pool.get('res.groups').search(cr, uid, [])
217 groups_br = self.pool.get('res.groups').browse(cr, uid, groups)
218 result['group_0'] = {'string': 'All Users','type': 'char','size': 7}
219 for group in groups_br:
220 result['group_%d'%group.id] = {'string': '%s'%group.name,'type': 'char','size': 7}
223 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context={}, toolbar=False, submenu=False):
224 result = super(ir_model_grid, self).fields_view_get(cr, uid, view_id, view_type, context=context, toolbar=toolbar, submenu=submenu)
225 groups = self.pool.get('res.groups').search(cr, uid, [])
226 groups_br = self.pool.get('res.groups').browse(cr, uid, groups)
227 cols = ['model', 'name']
228 xml = '''<?xml version="1.0"?>
229 <%s editable="bottom">
230 <field name="name" select="1" readonly="1" required="1"/>
231 <field name="model" select="1" readonly="1" required="1"/>
232 <field name="group_0"/>
234 for group in groups_br:
235 xml += '''<field name="group_%d"/>''' % (group.id, )
236 xml += '''</%s>''' % (view_type,)
238 result['fields'] = self.fields_get(cr, uid, cols, context)
242 class ir_model_fields(osv.osv):
243 _name = 'ir.model.fields'
244 _description = "Fields"
246 'name': fields.char('Name', required=True, size=64, select=1),
247 'model': fields.char('Object Name', size=64, required=True, select=1),
248 'relation': fields.char('Object Relation', size=64),
249 'relation_field': fields.char('Relation Field', size=64),
250 'model_id': fields.many2one('ir.model', 'Object ID', required=True, select=True, ondelete='cascade'),
251 'field_description': fields.char('Field Label', required=True, size=256),
252 'ttype': fields.selection(_get_fields_type, 'Field Type',size=64, required=True),
253 'selection': fields.char('Field Selection',size=128),
254 'required': fields.boolean('Required'),
255 'readonly': fields.boolean('Readonly'),
256 'select_level': fields.selection([('0','Not Searchable'),('1','Always Searchable'),('2','Advanced Search')],'Searchable', required=True),
257 'translate': fields.boolean('Translate'),
258 'size': fields.integer('Size'),
259 'state': fields.selection([('manual','Custom Field'),('base','Base Field')],'Manually Created', required=True, readonly=True, select=1),
260 'on_delete': fields.selection([('cascade','Cascade'),('set null','Set NULL')], 'On delete', help='On delete property for many2one fields'),
261 'domain': fields.char('Domain', size=256),
262 'groups': fields.many2many('res.groups', 'ir_model_fields_group_rel', 'field_id', 'group_id', 'Groups'),
263 'view_load': fields.boolean('View Auto-Load'),
264 'selectable': fields.boolean('Selectable'),
266 _rec_name='field_description'
268 'view_load': lambda *a: 0,
269 'selection': lambda *a: "[]",
270 'domain': lambda *a: "[]",
271 'name': lambda *a: 'x_',
272 'state': lambda self,cr,uid,ctx={}: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
273 'on_delete': lambda *a: 'set null',
274 'select_level': lambda *a: '0',
275 'size': lambda *a: 64,
276 'field_description': lambda *a: '',
277 'selectable': lambda *a: 1,
281 ('size_gt_zero', 'CHECK (size>0)', 'Size of the field can never be less than 1 !'),
283 def unlink(self, cr, user, ids, context=None):
284 for field in self.browse(cr, user, ids, context):
285 if field.state <> 'manual':
286 raise except_orm(_('Error'), _("You cannot remove the field '%s' !") %(field.name,))
288 # MAY BE ADD A ALTER TABLE DROP ?
290 #Removing _columns entry for that table
291 self.pool.get(field.model)._columns.pop(field.name,None)
292 return super(ir_model_fields, self).unlink(cr, user, ids, context)
294 def create(self, cr, user, vals, context=None):
295 if 'model_id' in vals:
296 model_data = self.pool.get('ir.model').browse(cr, user, vals['model_id'])
297 vals['model'] = model_data.model
300 if context and context.get('manual',False):
301 vals['state'] = 'manual'
302 res = super(ir_model_fields,self).create(cr, user, vals, context)
303 if vals.get('state','base') == 'manual':
304 if not vals['name'].startswith('x_'):
305 raise except_orm(_('Error'), _("Custom fields must have a name that starts with 'x_' !"))
307 if vals.get('relation',False) and not self.pool.get('ir.model').search(cr, user, [('model','=',vals['relation'])]):
308 raise except_orm(_('Error'), _("Model %s Does not Exist !" % vals['relation']))
310 if self.pool.get(vals['model']):
311 self.pool.get(vals['model']).__init__(self.pool, cr)
312 #Added context to _auto_init for special treatment to custom field for select_level
314 ctx.update({'field_name':vals['name'],'field_state':'manual','select':vals.get('select_level','0'),'update_custom_fields':True})
315 self.pool.get(vals['model'])._auto_init(cr, ctx)
321 class ir_model_access(osv.osv):
322 _name = 'ir.model.access'
324 'name': fields.char('Name', size=64, required=True, select=True),
325 'model_id': fields.many2one('ir.model', 'Object', required=True, domain=[('osv_memory','=', False)], select=True),
326 'group_id': fields.many2one('res.groups', 'Group', ondelete='cascade', select=True),
327 'perm_read': fields.boolean('Read Access'),
328 'perm_write': fields.boolean('Write Access'),
329 'perm_create': fields.boolean('Create Access'),
330 'perm_unlink': fields.boolean('Delete Permission'),
333 def check_groups(self, cr, uid, group):
335 grouparr = group.split('.')
339 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],))
340 return bool(cr.fetchone())
342 def check_group(self, cr, uid, model, mode, group_ids):
343 """ Check if a specific group has the access mode to the specified model"""
344 assert mode in ['read','write','create','unlink'], 'Invalid access mode'
346 if isinstance(model, browse_record):
347 assert model._table_name == 'ir.model', 'Invalid model object'
348 model_name = model.name
352 if isinstance(group_ids, (int, long)):
353 group_ids = [group_ids]
354 for group_id in group_ids:
355 cr.execute("SELECT perm_" + mode + " "
356 " FROM ir_model_access a "
357 " JOIN ir_model m ON (m.id = a.model_id) "
358 " WHERE m.model = %s AND a.group_id = %s", (model_name, group_id)
362 cr.execute("SELECT perm_" + mode + " "
363 " FROM ir_model_access a "
364 " JOIN ir_model m ON (m.id = a.model_id) "
365 " WHERE m.model = %s AND a.group_id IS NULL", (model_name, )
369 access = bool(r and r[0])
372 # pass no groups -> no access
375 def check(self, cr, uid, model, mode='read', raise_exception=True, context=None):
377 # User root have all accesses
378 # TODO: exclude xml-rpc requests
381 assert mode in ['read','write','create','unlink'], 'Invalid access mode'
383 if isinstance(model, browse_record):
384 assert model._table_name == 'ir.model', 'Invalid model object'
385 model_name = model.name
389 # osv_memory objects can be read by everyone, as they only return
390 # results that belong to the current user (except for superuser)
391 model_obj = self.pool.get(model_name)
392 if isinstance(model_obj, osv.osv_memory):
395 # We check if a specific rule exists
396 cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
397 ' FROM ir_model_access a '
398 ' JOIN ir_model m ON (m.id = a.model_id) '
399 ' JOIN res_groups_users_rel gu ON (gu.gid = a.group_id) '
400 ' WHERE m.model = %s '
407 # there is no specific rule. We check the generic rule
408 cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
409 ' FROM ir_model_access a '
410 ' JOIN ir_model m ON (m.id = a.model_id) '
411 ' WHERE a.group_id IS NULL '
417 if not r and raise_exception:
419 'read': _('You can not read this document! (%s)'),
420 'write': _('You can not write in this document! (%s)'),
421 'create': _('You can not create this kind of document! (%s)'),
422 'unlink': _('You can not delete this document! (%s)'),
425 raise except_orm(_('AccessError'), msgs[mode] % model_name )
428 check = tools.cache()(check)
430 __cache_clearing_methods = []
432 def register_cache_clearing_method(self, model, method):
433 self.__cache_clearing_methods.append((model, method))
435 def unregister_cache_clearing_method(self, model, method):
437 i = self.__cache_clearing_methods.index((model, method))
438 del self.__cache_clearing_methods[i]
442 def call_cache_clearing_methods(self, cr):
443 self.check.clear_cache(cr.dbname) # clear the cache of check function
444 for model, method in self.__cache_clearing_methods:
445 getattr(self.pool.get(model), method)()
448 # Check rights on actions
450 def write(self, cr, uid, *args, **argv):
451 self.call_cache_clearing_methods(cr)
452 res = super(ir_model_access, self).write(cr, uid, *args, **argv)
455 def create(self, cr, uid, *args, **argv):
456 self.call_cache_clearing_methods(cr)
457 res = super(ir_model_access, self).create(cr, uid, *args, **argv)
460 def unlink(self, cr, uid, *args, **argv):
461 self.call_cache_clearing_methods(cr)
462 res = super(ir_model_access, self).unlink(cr, uid, *args, **argv)
467 class ir_model_data(osv.osv):
468 _name = 'ir.model.data'
469 __logger = logging.getLogger('addons.base.'+_name)
471 'name': fields.char('XML Identifier', required=True, size=128, select=1),
472 'model': fields.char('Object', required=True, size=64, select=1),
473 'module': fields.char('Module', required=True, size=64, select=1),
474 'res_id': fields.integer('Resource ID', select=1),
475 'noupdate': fields.boolean('Non Updatable'),
476 'date_update': fields.datetime('Update Date'),
477 'date_init': fields.datetime('Init Date')
480 'date_init': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
481 'date_update': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
482 'noupdate': lambda *a: False,
483 'module': lambda *a: ''
486 ('module_name_uniq', 'unique(name, module)', 'You cannot have multiple records with the same id for the same module'),
489 def __init__(self, pool, cr):
490 osv.osv.__init__(self, pool, cr)
493 self.unlink_mark = {}
496 def _get_id(self, cr, uid, module, xml_id):
497 """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"""
498 ids = self.search(cr, uid, [('module','=',module), ('name','=', xml_id)])
500 raise ValueError('No references to %s.%s' % (module, xml_id))
501 # the sql constraints ensure us we have only one result
505 def get_object_reference(self, cr, uid, module, xml_id):
506 """Returns (model, res_id) corresponding to a given module and xml_id (cached) or raise ValueError if not found"""
507 data_id = self._get_id(cr, uid, module, xml_id)
508 res = self.read(cr, uid, data_id, ['model', 'res_id'])
509 return (res['model'], res['res_id'])
511 def get_object(self, cr, uid, module, xml_id, context=None):
512 """Returns a browsable record for the given module name and xml_id or raise ValueError if not found"""
513 res_model, res_id = self.get_object_reference(cr, uid, module, xml_id)
514 return self.pool.get(res_model).browse(cr, uid, res_id, context=context)
516 def _update_dummy(self,cr, uid, model, module, xml_id=False, store=True):
520 id = self.read(cr, uid, [self._get_id(cr, uid, module, xml_id)], ['res_id'])[0]['res_id']
521 self.loads[(module,xml_id)] = (model,id)
526 def _update(self,cr, uid, model, module, values, xml_id=False, store=True, noupdate=False, mode='init', res_id=False, context=None):
528 model_obj = self.pool.get(model)
531 if xml_id and ('.' in xml_id):
532 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)
534 module, xml_id = xml_id.split('.')
535 if (not xml_id) and (not self.doinit):
540 cr.execute('select id,res_id from ir_model_data where module=%s and name=%s', (module,xml_id))
541 results = cr.fetchall()
542 for action_id2,res_id2 in results:
543 cr.execute('select id from '+model_obj._table+' where id=%s', (res_id2,))
544 result3 = cr.fetchone()
546 self._get_id.clear_cache(cr.dbname, uid, module, xml_id)
547 self.get_object_reference.clear_cache(cr.dbname, uid, module, xml_id)
548 cr.execute('delete from ir_model_data where id=%s', (action_id2,))
551 res_id,action_id = res_id2,action_id2
553 if action_id and res_id:
554 model_obj.write(cr, uid, [res_id], values, context=context)
555 self.write(cr, uid, [action_id], {
556 'date_update': time.strftime('%Y-%m-%d %H:%M:%S'),
559 model_obj.write(cr, uid, [res_id], values, context=context)
561 self.create(cr, uid, {
566 'noupdate': noupdate,
568 if model_obj._inherits:
569 for table in model_obj._inherits:
570 inherit_id = model_obj.browse(cr, uid,
571 res_id,context=context)[model_obj._inherits[table]]
572 self.create(cr, uid, {
573 'name': xml_id + '_' + table.replace('.', '_'),
576 'res_id': inherit_id,
577 'noupdate': noupdate,
580 if mode=='init' or (mode=='update' and xml_id):
581 res_id = model_obj.create(cr, uid, values, context=context)
583 self.create(cr, uid, {
590 if model_obj._inherits:
591 for table in model_obj._inherits:
592 inherit_id = model_obj.browse(cr, uid,
593 res_id,context=context)[model_obj._inherits[table]]
594 self.create(cr, uid, {
595 'name': xml_id + '_' + table.replace('.', '_'),
598 'res_id': inherit_id,
599 'noupdate': noupdate,
603 self.loads[(module, xml_id)] = (model, res_id)
604 if model_obj._inherits:
605 for table in model_obj._inherits:
606 inherit_field = model_obj._inherits[table]
607 inherit_id = model_obj.read(cr, uid, res_id,
608 [inherit_field])[inherit_field]
609 self.loads[(module, xml_id + '_' + \
610 table.replace('.', '_'))] = (table, inherit_id)
613 def _unlink(self, cr, uid, model, res_ids):
614 for res_id in res_ids:
615 self.unlink_mark[(model, res_id)] = False
616 cr.execute('delete from ir_model_data where res_id=%s and model=%s', (res_id, model))
619 def ir_set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=None, xml_id=False):
620 obj = self.pool.get('ir.values')
621 if type(models[0])==type([]) or type(models[0])==type(()):
622 model,res_id = models[0]
628 where = ' and res_id=%s' % (res_id,)
630 where = ' and (res_id is null)'
633 where += ' and key2=\'%s\'' % (key2,)
635 where += ' and (key2 is null)'
637 cr.execute('select * from ir_values where model=%s and key=%s and name=%s'+where,(model, key, name))
640 res = ir.ir_set(cr, uid, key, key2, name, models, value, replace, isobject, meta)
642 cr.execute('UPDATE ir_values set value=%s WHERE model=%s and key=%s and name=%s'+where,(value, model, key, name))
645 def _process_end(self, cr, uid, modules):
648 modules = list(modules)
649 module_in = ",".join(["%s"] * len(modules))
650 cr.execute('select id,name,model,res_id,module from ir_model_data where module IN (' + module_in + ') and noupdate=%s', modules + [False])
652 for (id, name, model, res_id,module) in cr.fetchall():
653 if (module,name) not in self.loads:
654 self.unlink_mark[(model,res_id)] = id
655 if model=='workflow.activity':
656 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,))
657 wkf_todo.extend(cr.fetchall())
658 cr.execute("update wkf_transition set condition='True', role_id=NULL, signal=NULL,act_to=act_from,act_from=%s where act_to=%s", (res_id,res_id))
659 cr.execute("delete from wkf_transition where act_to=%s", (res_id,))
661 for model,id in wkf_todo:
662 wf_service = netsvc.LocalService("workflow")
663 wf_service.trg_write(uid, model, id, cr)
666 if not config.get('import_partial'):
667 for (model, res_id) in self.unlink_mark.keys():
668 if self.pool.get(model):
669 self.__logger.info('Deleting %s@%s', res_id, model)
671 self.pool.get(model).unlink(cr, uid, [res_id])
673 ids = self.search(cr, uid, [('res_id','=',res_id),
674 ('model','=',model)])
675 self.__logger.debug('=> Deleting %s: %s',
677 if len(ids) > 1 and \
678 self.__logger.isEnabledFor(logging.WARNING):
680 'Got %d %s for (%s, %d): %s',
681 len(ids), self._name, model, res_id,
682 map(itemgetter('module','name'),
683 self.read(cr, uid, ids,
684 ['name', 'module'])))
685 self.unlink(cr, uid, ids)
687 'DELETE FROM ir_values WHERE value=%s',
688 ('%s,%s'%(model, res_id),))
692 self.__logger.exception(
693 'Could not delete id: %d of model %s\nThere '
694 'should be some relation that points to this '
695 'resource\nYou should manually fix this and '
696 'restart with --update=module', res_id, model)
700 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: