[IMP] ir.model.data: better message + support optional context
[odoo/odoo.git] / openerp / addons / base / ir / ir_model.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21 import logging
22 import re
23 import time
24
25 from osv import fields,osv
26 import netsvc
27 from osv.orm import except_orm, browse_record
28 import tools
29 from tools.safe_eval import safe_eval as eval
30 from tools import config
31 from tools.translate import _
32 import pooler
33
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)
41     return field_types
42
43 def _in_modules(self, cr, uid, ids, field_name, arg, context=None):
44     #pseudo-method used by fields.function in ir.model/ir.model.fields
45     module_pool = self.pool.get("ir.module.module")
46     installed_module_ids = module_pool.search(cr, uid, [('state','=','installed')])
47     installed_module_names = module_pool.read(cr, uid, installed_module_ids, ['name'], context=context)
48     installed_modules = set(x['name'] for x in installed_module_names)
49
50     result = {}
51     xml_ids = osv.osv._get_xml_ids(self, cr, uid, ids)
52     for k,v in xml_ids.iteritems():
53         result[k] = ', '.join(sorted(installed_modules & set(xml_id.split('.')[0] for xml_id in v)))
54     return result
55
56
57 class ir_model(osv.osv):
58     _name = 'ir.model'
59     _description = "Objects"
60     _order = 'model'
61
62     def _is_osv_memory(self, cr, uid, ids, field_name, arg, context=None):
63         models = self.browse(cr, uid, ids, context=context)
64         res = dict.fromkeys(ids)
65         for model in models:
66             res[model.id] = isinstance(self.pool.get(model.model), osv.osv_memory)
67         return res
68
69     def _search_osv_memory(self, cr, uid, model, name, domain, context=None):
70         if not domain:
71             return []
72         field, operator, value = domain[0]
73         if operator not in ['=', '!=']:
74             raise osv.except_osv(_('Invalid search criterions'), _('The osv_memory field can only be compared with = and != operator.'))
75         value = bool(value) if operator == '=' else not bool(value)
76         all_model_ids = self.search(cr, uid, [], context=context)
77         is_osv_mem = self._is_osv_memory(cr, uid, all_model_ids, 'osv_memory', arg=None, context=context)
78         return [('id', 'in', [id for id in is_osv_mem if bool(is_osv_mem[id]) == value])]
79
80     def _view_ids(self, cr, uid, ids, field_name, arg, context=None):
81         models = self.browse(cr, uid, ids)
82         res = {}
83         for model in models:
84             res[model.id] = self.pool.get("ir.ui.view").search(cr, uid, [('model', '=', model.model)])
85         return res
86
87     _columns = {
88         'name': fields.char('Object Name', size=64, translate=True, required=True),
89         'model': fields.char('Object', size=64, required=True, select=1),
90         'info': fields.text('Information'),
91         'field_id': fields.one2many('ir.model.fields', 'model_id', 'Fields', required=True),
92         'state': fields.selection([('manual','Custom Object'),('base','Base Object')],'Type',readonly=True),
93         'access_ids': fields.one2many('ir.model.access', 'model_id', 'Access'),
94         'osv_memory': fields.function(_is_osv_memory, method=True, string='In-memory model', type='boolean',
95             fnct_search=_search_osv_memory,
96             help="Indicates whether this object model lives in memory only, i.e. is not persisted (osv.osv_memory)"),
97         'modules': fields.function(_in_modules, method=True, type='char', size=128, string='In modules', help='List of modules in which the object is defined or inherited'),
98         'view_ids': fields.function(_view_ids, method=True, type='one2many', obj='ir.ui.view', string='Views'),
99     }
100     
101     _defaults = {
102         'model': lambda *a: 'x_',
103         'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
104     }
105     
106     def _check_model_name(self, cr, uid, ids, context=None):
107         for model in self.browse(cr, uid, ids, context=context):
108             if model.state=='manual':
109                 if not model.model.startswith('x_'):
110                     return False
111             if not re.match('^[a-z_A-Z0-9.]+$',model.model):
112                 return False
113         return True
114
115     def _model_name_msg(self, cr, uid, ids, context=None):
116         return _('The Object name must start with x_ and not contain any special character !')
117     _constraints = [
118         (_check_model_name, _model_name_msg, ['model']),
119     ]
120
121     # overridden to allow searching both on model name (model field)
122     # and model description (name field)
123     def name_search(self, cr, uid, name='', args=None, operator='ilike',  context=None, limit=None):
124         if args is None:
125             args = []
126         domain = args + ['|', ('model', operator, name), ('name', operator, name)]
127         return super(ir_model, self).name_search(cr, uid, None, domain,
128                         operator=operator, limit=limit, context=context)
129
130
131     def unlink(self, cr, user, ids, context=None):
132         for model in self.browse(cr, user, ids, context):
133             if model.state != 'manual':
134                 raise except_orm(_('Error'), _("You can not remove the model '%s' !") %(model.name,))
135         res = super(ir_model, self).unlink(cr, user, ids, context)
136         pooler.restart_pool(cr.dbname)
137         return res
138
139     def write(self, cr, user, ids, vals, context=None):
140         if context:
141             context.pop('__last_update', None)
142         return super(ir_model,self).write(cr, user, ids, vals, context)
143
144     def create(self, cr, user, vals, context=None):
145         if  context is None:
146             context = {}
147         if context and context.get('manual',False):
148             vals['state']='manual'
149         res = super(ir_model,self).create(cr, user, vals, context)
150         if vals.get('state','base')=='manual':
151             self.instanciate(cr, user, vals['model'], context)
152             self.pool.get(vals['model']).__init__(self.pool, cr)
153             ctx = context.copy()
154             ctx.update({'field_name':vals['name'],'field_state':'manual','select':vals.get('select_level','0')})
155             self.pool.get(vals['model'])._auto_init(cr, ctx)
156             #pooler.restart_pool(cr.dbname)
157         return res
158
159     def instanciate(self, cr, user, model, context={}):
160         class x_custom_model(osv.osv):
161             pass
162         x_custom_model._name = model
163         x_custom_model._module = False
164         a = x_custom_model.createInstance(self.pool, cr)
165         if (not a._columns) or ('x_name' in a._columns.keys()):
166             x_name = 'x_name'
167         else:
168             x_name = a._columns.keys()[0]
169         x_custom_model._rec_name = x_name
170 ir_model()
171
172 class ir_model_fields(osv.osv):
173     _name = 'ir.model.fields'
174     _description = "Fields"
175
176     _columns = {
177         'name': fields.char('Name', required=True, size=64, select=1),
178         'model': fields.char('Object Name', size=64, required=True, select=1,
179             help="The technical name of the model this field belongs to"),
180         'relation': fields.char('Object Relation', size=64,
181             help="For relationship fields, the technical name of the target model"),
182         'relation_field': fields.char('Relation Field', size=64,
183             help="For one2many fields, the field on the target model that implement the opposite many2one relationship"),
184         'model_id': fields.many2one('ir.model', 'Model', required=True, select=True, ondelete='cascade',
185             help="The model this field belongs to"),
186         'field_description': fields.char('Field Label', required=True, size=256),
187         'ttype': fields.selection(_get_fields_type, 'Field Type',size=64, required=True),
188         'selection': fields.char('Selection Options',size=128, help="List of options for a selection field, "
189             "specified as a Python expression defining a list of (key, label) pairs. "
190             "For example: [('blue','Blue'),('yellow','Yellow')]"),
191         'required': fields.boolean('Required'),
192         'readonly': fields.boolean('Readonly'),
193         'select_level': fields.selection([('0','Not Searchable'),('1','Always Searchable'),('2','Advanced Search (deprecated)')],'Searchable', required=True),
194         'translate': fields.boolean('Translate', help="Whether values for this field can be translated (enables the translation mechanism for that field)"),
195         'size': fields.integer('Size'),
196         'state': fields.selection([('manual','Custom Field'),('base','Base Field')],'Type', required=True, readonly=True, select=1),
197         'on_delete': fields.selection([('cascade','Cascade'),('set null','Set NULL')], 'On delete', help='On delete property for many2one fields'),
198         'domain': fields.char('Domain', size=256, help="The optional domain to restrict possible values for relationship fields, "
199             "specified as a Python expression defining a list of triplets. "
200             "For example: [('color','=','red')]"),
201         'groups': fields.many2many('res.groups', 'ir_model_fields_group_rel', 'field_id', 'group_id', 'Groups'),
202         'view_load': fields.boolean('View Auto-Load'),
203         'selectable': fields.boolean('Selectable'),
204         'modules': fields.function(_in_modules, method=True, type='char', size=128, string='In modules', help='List of modules in which the field is defined'),
205     }
206     _rec_name='field_description'
207     _defaults = {
208         'view_load': 0,
209         'selection': "",
210         'domain': "[]",
211         'name': 'x_',
212         'state': lambda self,cr,uid,ctx={}: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
213         'on_delete': 'set null',
214         'select_level': '0',
215         'size': 64,
216         'field_description': '',
217         'selectable': 1,
218     }
219     _order = "name"
220
221     def _check_selection(self, cr, uid, selection, context=None):
222         try:
223             selection_list = eval(selection)
224         except Exception:
225             logging.getLogger('ir.model').warning('Invalid selection list definition for fields.selection', exc_info=True)
226             raise except_orm(_('Error'),
227                     _("The Selection Options expression is not a valid Pythonic expression." \
228                       "Please provide an expression in the [('key','Label'), ...] format."))
229
230         check = True
231         if not (isinstance(selection_list, list) and selection_list):
232             check = False
233         else:
234             for item in selection_list:
235                 if not (isinstance(item, (tuple,list)) and len(item) == 2):
236                     check = False
237                     break
238
239         if not check:
240                 raise except_orm(_('Error'),
241                     _("The Selection Options expression is must be in the [('key','Label'), ...] format!"))
242         return True
243
244     def _size_gt_zero_msg(self, cr, user, ids, context=None):
245         return _('Size of the field can never be less than 1 !')
246
247     _sql_constraints = [
248         ('size_gt_zero', 'CHECK (size>0)',_size_gt_zero_msg ),
249     ]
250
251     def unlink(self, cr, user, ids, context=None):
252         for field in self.browse(cr, user, ids, context):
253             if field.state <> 'manual':
254                 raise except_orm(_('Error'), _("You cannot remove the field '%s' !") %(field.name,))
255         #
256         # MAY BE ADD A ALTER TABLE DROP ?
257         #
258             #Removing _columns entry for that table
259             self.pool.get(field.model)._columns.pop(field.name,None)
260         return super(ir_model_fields, self).unlink(cr, user, ids, context)
261
262     def create(self, cr, user, vals, context=None):
263         if 'model_id' in vals:
264             model_data = self.pool.get('ir.model').browse(cr, user, vals['model_id'])
265             vals['model'] = model_data.model
266         if context is None:
267             context = {}
268         if context and context.get('manual',False):
269             vals['state'] = 'manual'
270         if vals.get('ttype', False) == 'selection':
271             if not vals.get('selection',False):
272                 raise except_orm(_('Error'), _('For selection fields, the Selection Options must be given!'))
273             self._check_selection(cr, user, vals['selection'], context=context)
274         res = super(ir_model_fields,self).create(cr, user, vals, context)
275         if vals.get('state','base') == 'manual':
276             if not vals['name'].startswith('x_'):
277                 raise except_orm(_('Error'), _("Custom fields must have a name that starts with 'x_' !"))
278
279             if vals.get('relation',False) and not self.pool.get('ir.model').search(cr, user, [('model','=',vals['relation'])]):
280                  raise except_orm(_('Error'), _("Model %s does not exist!") % vals['relation'])
281
282             if self.pool.get(vals['model']):
283                 self.pool.get(vals['model']).__init__(self.pool, cr)
284                 #Added context to _auto_init for special treatment to custom field for select_level
285                 ctx = context.copy()
286                 ctx.update({'field_name':vals['name'],'field_state':'manual','select':vals.get('select_level','0'),'update_custom_fields':True})
287                 self.pool.get(vals['model'])._auto_init(cr, ctx)
288
289         return res
290
291     def write(self, cr, user, ids, vals, context=None):
292         if context is None:
293             context = {}
294         if context and context.get('manual',False):
295             vals['state'] = 'manual'
296
297         column_rename = None # if set, *one* column can be renamed here
298         obj = None
299         models_patch = {}    # structs of (obj, [(field, prop, change_to),..])
300                              # data to be updated on the orm model
301
302         # static table of properties
303         model_props = [ # (our-name, fields.prop, set_fn)
304             ('field_description', 'string', str),
305             ('required', 'required', bool),
306             ('readonly', 'readonly', bool),
307             ('domain', '_domain', eval),
308             ('size', 'size', int),
309             ('on_delete', 'ondelete', str),
310             ('translate', 'translate', bool),
311             ('view_load', 'view_load', bool),
312             ('selectable', 'selectable', bool),
313             ('select_level', 'select', int),
314             ('selection', 'selection', eval),
315             ]
316
317         if vals and ids:
318             checked_selection = False # need only check it once, so defer
319
320             for item in self.browse(cr, user, ids, context=context):
321                 if not (obj and obj._name == item.model):
322                     obj = self.pool.get(item.model)
323
324                 if item.state != 'manual':
325                     raise except_orm(_('Error!'),
326                         _('Properties of base fields cannot be altered in this manner! '
327                           'Please modify them through Python code, '
328                           'preferably through a custom addon!'))
329
330                 if item.ttype == 'selection' and 'selection' in vals \
331                         and not checked_selection:
332                     self._check_selection(cr, user, vals['selection'], context=context)
333                     checked_selection = True
334
335                 final_name = item.name
336                 if 'name' in vals and vals['name'] != item.name:
337                     # We need to rename the column
338                     if column_rename:
339                         raise except_orm(_('Error!'), _('Can only rename one column at a time!'))
340                     if vals['name'] in obj._columns:
341                         raise except_orm(_('Error!'), _('Cannot rename column to %s, because that column already exists!') % vals['name'])
342                     if vals.get('state', 'base') == 'manual' and not vals['name'].startswith('x_'):
343                         raise except_orm(_('Error!'), _('New column name must still start with x_ , because it is a custom field!'))
344                     if '\'' in vals['name'] or '"' in vals['name'] or ';' in vals['name']:
345                         raise ValueError('Invalid character in column name')
346                     column_rename = (obj, (obj._table, item.name, vals['name']))
347                     final_name = vals['name']
348
349                 if 'model_id' in vals and vals['model_id'] != item.model_id:
350                     raise except_orm(_("Error!"), _("Changing the model of a field is forbidden!"))
351
352                 if 'ttype' in vals and vals['ttype'] != item.ttype:
353                     raise except_orm(_("Error!"), _("Changing the type of a column is not yet supported. "
354                                 "Please drop it and create it again!"))
355
356                 # We don't check the 'state', because it might come from the context
357                 # (thus be set for multiple fields) and will be ignored anyway.
358                 if obj:
359                     models_patch.setdefault(obj._name, (obj,[]))
360                     # find out which properties (per model) we need to update
361                     for field_name, field_property, set_fn in model_props:
362                         if field_name in vals:
363                             property_value = set_fn(vals[field_name])
364                             if getattr(obj._columns[item.name], field_property) != property_value:
365                                 models_patch[obj._name][1].append((final_name, field_property, property_value))
366                         # our dict is ready here, but no properties are changed so far
367
368         # These shall never be written (modified)
369         for column_name in ('model_id', 'model', 'state'):
370             if column_name in vals:
371                 del vals[column_name]
372
373         res = super(ir_model_fields,self).write(cr, user, ids, vals, context=context)
374
375         if column_rename:
376             cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s"' % column_rename[1])
377             # This is VERY risky, but let us have this feature:
378             # we want to change the key of column in obj._columns dict
379             col = column_rename[0]._columns.pop(column_rename[1][1]) # take object out, w/o copy
380             column_rename[0]._columns[column_rename[1][2]] = col
381
382         if models_patch:
383             # We have to update _columns of the model(s) and then call their
384             # _auto_init to sync the db with the model. Hopefully, since write()
385             # was called earlier, they will be in-sync before the _auto_init.
386             # Anything we don't update in _columns now will be reset from
387             # the model into ir.model.fields (db).
388             ctx = context.copy()
389             ctx.update({'select': vals.get('select_level','0'),'update_custom_fields':True})
390
391             for model_key, patch_struct in models_patch.items():
392                 obj = patch_struct[0]
393                 for col_name, col_prop, val in patch_struct[1]:
394                     setattr(obj._columns[col_name], col_prop, val)
395                 obj._auto_init(cr, ctx)
396         return res
397
398 ir_model_fields()
399
400 class ir_model_access(osv.osv):
401     _name = 'ir.model.access'
402     _columns = {
403         'name': fields.char('Name', size=64, required=True, select=True),
404         'model_id': fields.many2one('ir.model', 'Object', required=True, domain=[('osv_memory','=', False)], select=True, ondelete='cascade'),
405         'group_id': fields.many2one('res.groups', 'Group', ondelete='cascade', select=True),
406         'perm_read': fields.boolean('Read Access'),
407         'perm_write': fields.boolean('Write Access'),
408         'perm_create': fields.boolean('Create Access'),
409         'perm_unlink': fields.boolean('Delete Access'),
410     }
411
412     def check_groups(self, cr, uid, group):
413         grouparr  = group.split('.')
414         if not grouparr:
415             return False
416         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],))
417         return bool(cr.fetchone())
418
419     def check_group(self, cr, uid, model, mode, group_ids):
420         """ Check if a specific group has the access mode to the specified model"""
421         assert mode in ['read','write','create','unlink'], 'Invalid access mode'
422
423         if isinstance(model, browse_record):
424             assert model._table_name == 'ir.model', 'Invalid model object'
425             model_name = model.name
426         else:
427             model_name = model
428
429         if isinstance(group_ids, (int, long)):
430             group_ids = [group_ids]
431         for group_id in group_ids:
432             cr.execute("SELECT perm_" + mode + " "
433                    "  FROM ir_model_access a "
434                    "  JOIN ir_model m ON (m.id = a.model_id) "
435                    " WHERE m.model = %s AND a.group_id = %s", (model_name, group_id)
436                    )
437             r = cr.fetchone()
438             if r is None:
439                 cr.execute("SELECT perm_" + mode + " "
440                        "  FROM ir_model_access a "
441                        "  JOIN ir_model m ON (m.id = a.model_id) "
442                        " WHERE m.model = %s AND a.group_id IS NULL", (model_name, )
443                        )
444                 r = cr.fetchone()
445
446             access = bool(r and r[0])
447             if access:
448                 return True
449         # pass no groups -> no access
450         return False
451
452     def group_names_with_access(self, cr, model_name, access_mode):
453         """Returns the names of visible groups which have been granted ``access_mode`` on
454            the model ``model_name``.
455            :rtype: list
456         """
457         assert access_mode in ['read','write','create','unlink'], 'Invalid access mode: %s' % access_mode
458         cr.execute('''SELECT
459                         g.name
460                       FROM
461                         ir_model_access a
462                         JOIN ir_model m ON (a.model_id=m.id)
463                         JOIN res_groups g ON (a.group_id=g.id)
464                       WHERE
465                         m.model=%s AND
466                         a.perm_''' + access_mode, (model_name,))
467         return [x[0] for x in cr.fetchall()]
468
469     @tools.ormcache()
470     def check(self, cr, uid, model, mode='read', raise_exception=True, context=None):
471         if uid==1:
472             # User root have all accesses
473             # TODO: exclude xml-rpc requests
474             return True
475
476         assert mode in ['read','write','create','unlink'], 'Invalid access mode'
477
478         if isinstance(model, browse_record):
479             assert model._table_name == 'ir.model', 'Invalid model object'
480             model_name = model.name
481         else:
482             model_name = model
483
484         # osv_memory objects can be read by everyone, as they only return
485         # results that belong to the current user (except for superuser)
486         model_obj = self.pool.get(model_name)
487         if isinstance(model_obj, osv.osv_memory):
488             return True
489
490         # We check if a specific rule exists
491         cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
492                    '  FROM ir_model_access a '
493                    '  JOIN ir_model m ON (m.id = a.model_id) '
494                    '  JOIN res_groups_users_rel gu ON (gu.gid = a.group_id) '
495                    ' WHERE m.model = %s '
496                    '   AND gu.uid = %s '
497                    , (model_name, uid,)
498                    )
499         r = cr.fetchone()[0]
500
501         if r is None:
502             # there is no specific rule. We check the generic rule
503             cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
504                        '  FROM ir_model_access a '
505                        '  JOIN ir_model m ON (m.id = a.model_id) '
506                        ' WHERE a.group_id IS NULL '
507                        '   AND m.model = %s '
508                        , (model_name,)
509                        )
510             r = cr.fetchone()[0]
511
512         if not r and raise_exception:
513             groups = ', '.join(self.group_names_with_access(cr, model_name, mode)) or '/'
514             msgs = {
515                 'read':   _("You can not read this document (%s) ! Be sure your user belongs to one of these groups: %s."),
516                 'write':  _("You can not write in this document (%s) ! Be sure your user belongs to one of these groups: %s."),
517                 'create': _("You can not create this document (%s) ! Be sure your user belongs to one of these groups: %s."),
518                 'unlink': _("You can not delete this document (%s) ! Be sure your user belongs to one of these groups: %s."),
519             }
520
521             raise except_orm(_('AccessError'), msgs[mode] % (model_name, groups) )
522         return r
523
524     __cache_clearing_methods = []
525
526     def register_cache_clearing_method(self, model, method):
527         self.__cache_clearing_methods.append((model, method))
528
529     def unregister_cache_clearing_method(self, model, method):
530         try:
531             i = self.__cache_clearing_methods.index((model, method))
532             del self.__cache_clearing_methods[i]
533         except ValueError:
534             pass
535
536     def call_cache_clearing_methods(self, cr):
537         self.check.clear_cache(self)    # clear the cache of check function
538         for model, method in self.__cache_clearing_methods:
539             object_ = self.pool.get(model)
540             if object_:
541                 getattr(object_, method)()
542
543     #
544     # Check rights on actions
545     #
546     def write(self, cr, uid, *args, **argv):
547         self.call_cache_clearing_methods(cr)
548         res = super(ir_model_access, self).write(cr, uid, *args, **argv)
549         return res
550
551     def create(self, cr, uid, *args, **argv):
552         self.call_cache_clearing_methods(cr)
553         res = super(ir_model_access, self).create(cr, uid, *args, **argv)
554         return res
555
556     def unlink(self, cr, uid, *args, **argv):
557         self.call_cache_clearing_methods(cr)
558         res = super(ir_model_access, self).unlink(cr, uid, *args, **argv)
559         return res
560
561 ir_model_access()
562
563 class ir_model_data(osv.osv):
564     """Holds external identifier keys for records in the database.
565        This has two main uses:
566
567            * allows easy data integration with third-party systems,
568              making import/export/sync of data possible, as records
569              can be uniquely identified across multiple systems
570            * allows tracking the origin of data installed by OpenERP
571              modules themselves, thus making it possible to later
572              update them seamlessly.
573     """
574     _name = 'ir.model.data'
575     __logger = logging.getLogger('addons.base.'+_name)
576     _order = 'module,model,name'
577     _columns = {
578         'name': fields.char('External Identifier', required=True, size=128, select=1,
579                             help="External Key/Identifier that can be used for "
580                                  "data integration with third-party systems"),
581         'model': fields.char('Model Name', required=True, size=64, select=1),
582         'module': fields.char('Module', required=True, size=64, select=1),
583         'res_id': fields.integer('Record ID', select=1,
584                                  help="ID of the target record in the database"),
585         'noupdate': fields.boolean('Non Updatable'),
586         'date_update': fields.datetime('Update Date'),
587         'date_init': fields.datetime('Init Date')
588     }
589     _defaults = {
590         'date_init': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
591         'date_update': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
592         'noupdate': False,
593         'module': ''
594     }
595     _sql_constraints = [
596         ('module_name_uniq', 'unique(name, module)', 'You cannot have multiple records with the same external ID in the same module!'),
597     ]
598
599     def __init__(self, pool, cr):
600         osv.osv.__init__(self, pool, cr)
601         self.doinit = True
602
603         # also stored in pool to avoid being discarded along with this osv instance
604         if getattr(pool, 'model_data_reference_ids', None) is None:
605             self.pool.model_data_reference_ids = {}
606         self.loads = self.pool.model_data_reference_ids
607
608     def _auto_init(self, cr, context=None):
609         super(ir_model_data, self)._auto_init(cr, context)
610         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_model_data_module_name_index\'')
611         if not cr.fetchone():
612             cr.execute('CREATE INDEX ir_model_data_module_name_index ON ir_model_data (module, name)')
613
614     @tools.ormcache()
615     def _get_id(self, cr, uid, module, xml_id, context=None):
616         """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"""
617         ids = self.search(cr, uid, [('module','=',module), ('name','=', xml_id)])
618         if not ids:
619             raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))
620         # the sql constraints ensure us we have only one result
621         return ids[0]
622
623     @tools.ormcache()
624     def get_object_reference(self, cr, uid, module, xml_id, context=None):
625         """Returns (model, res_id) corresponding to a given module and xml_id (cached) or raise ValueError if not found"""
626         data_id = self._get_id(cr, uid, module, xml_id)
627         res = self.read(cr, uid, data_id, ['model', 'res_id'])
628         if not res['res_id']:
629             raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))
630         return (res['model'], res['res_id'])
631
632     def get_object(self, cr, uid, module, xml_id, context=None):
633         """Returns a browsable record for the given module name and xml_id or raise ValueError if not found"""
634         res_model, res_id = self.get_object_reference(cr, uid, module, xml_id)
635         result = self.pool.get(res_model).browse(cr, uid, res_id, context=context)
636         if not result.exists():
637             raise ValueError('No record found for unique ID %s.%s. It may have been deleted.' % (module, xml_id))
638         return result
639
640     def _update_dummy(self,cr, uid, model, module, xml_id=False, store=True):
641         if not xml_id:
642             return False
643         try:
644             id = self.read(cr, uid, [self._get_id(cr, uid, module, xml_id)], ['res_id'])[0]['res_id']
645             self.loads[(module,xml_id)] = (model,id)
646         except:
647             id = False
648         return id
649
650     def unlink(self, cr, uid, ids, context=None):
651         """ Regular unlink method, but make sure to clear the caches. """
652         self._get_id.clear_cache(self)
653         self.get_object_reference.clear_cache(self)
654         return super(ir_model_data,self).unlink(cr, uid, ids, context=context)
655
656     def _update(self,cr, uid, model, module, values, xml_id=False, store=True, noupdate=False, mode='init', res_id=False, context=None):
657         model_obj = self.pool.get(model)
658         if not context:
659             context = {}
660
661         # records created during module install should result in res.log entries that are already read!
662         context = dict(context, res_log_read=True)
663
664         if xml_id and ('.' in xml_id):
665             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)
666             module, xml_id = xml_id.split('.')
667         if (not xml_id) and (not self.doinit):
668             return False
669         action_id = False
670
671         if xml_id:
672             cr.execute('''SELECT imd.id, imd.res_id, md.id
673                           FROM ir_model_data imd LEFT JOIN %s md ON (imd.res_id = md.id)
674                           WHERE imd.module=%%s AND imd.name=%%s''' % model_obj._table,
675                           (module, xml_id))
676             results = cr.fetchall()
677             for imd_id2,res_id2,real_id2 in results:
678                 if not real_id2:
679                     self._get_id.clear_cache(self, uid, module, xml_id)
680                     self.get_object_reference.clear_cache(self, uid, module, xml_id)
681                     cr.execute('delete from ir_model_data where id=%s', (imd_id2,))
682                     res_id = False
683                 else:
684                     res_id,action_id = res_id2,imd_id2
685
686         if action_id and res_id:
687             model_obj.write(cr, uid, [res_id], values, context=context)
688             self.write(cr, uid, [action_id], {
689                 'date_update': time.strftime('%Y-%m-%d %H:%M:%S'),
690                 },context=context)
691         elif res_id:
692             model_obj.write(cr, uid, [res_id], values, context=context)
693             if xml_id:
694                 self.create(cr, uid, {
695                     'name': xml_id,
696                     'model': model,
697                     'module':module,
698                     'res_id':res_id,
699                     'noupdate': noupdate,
700                     },context=context)
701                 if model_obj._inherits:
702                     for table in model_obj._inherits:
703                         inherit_id = model_obj.browse(cr, uid,
704                                 res_id,context=context)[model_obj._inherits[table]]
705                         self.create(cr, uid, {
706                             'name': xml_id + '_' + table.replace('.', '_'),
707                             'model': table,
708                             'module': module,
709                             'res_id': inherit_id.id,
710                             'noupdate': noupdate,
711                             },context=context)
712         else:
713             if mode=='init' or (mode=='update' and xml_id):
714                 res_id = model_obj.create(cr, uid, values, context=context)
715                 if xml_id:
716                     self.create(cr, uid, {
717                         'name': xml_id,
718                         'model': model,
719                         'module': module,
720                         'res_id': res_id,
721                         'noupdate': noupdate
722                         },context=context)
723                     if model_obj._inherits:
724                         for table in model_obj._inherits:
725                             inherit_id = model_obj.browse(cr, uid,
726                                     res_id,context=context)[model_obj._inherits[table]]
727                             self.create(cr, uid, {
728                                 'name': xml_id + '_' + table.replace('.', '_'),
729                                 'model': table,
730                                 'module': module,
731                                 'res_id': inherit_id.id,
732                                 'noupdate': noupdate,
733                                 },context=context)
734         if xml_id:
735             if res_id:
736                 self.loads[(module, xml_id)] = (model, res_id)
737                 if model_obj._inherits:
738                     for table in model_obj._inherits:
739                         inherit_field = model_obj._inherits[table]
740                         inherit_id = model_obj.read(cr, uid, res_id,
741                                 [inherit_field])[inherit_field]
742                         self.loads[(module, xml_id + '_' + \
743                                 table.replace('.', '_'))] = (table, inherit_id)
744         return res_id
745
746     def ir_set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=None, xml_id=False):
747         if type(models[0])==type([]) or type(models[0])==type(()):
748             model,res_id = models[0]
749         else:
750             res_id=None
751             model = models[0]
752
753         if res_id:
754             where = ' and res_id=%s' % (res_id,)
755         else:
756             where = ' and (res_id is null)'
757
758         if key2:
759             where += ' and key2=\'%s\'' % (key2,)
760         else:
761             where += ' and (key2 is null)'
762
763         cr.execute('select * from ir_values where model=%s and key=%s and name=%s'+where,(model, key, name))
764         res = cr.fetchone()
765         if not res:
766             ir_values_obj = pooler.get_pool(cr.dbname).get('ir.values')
767             res = ir_values_obj.set(cr, uid, key, key2, name, models, value, replace, isobject, meta)
768         elif xml_id:
769             cr.execute('UPDATE ir_values set value=%s WHERE model=%s and key=%s and name=%s'+where,(value, model, key, name))
770         return True
771
772     def _process_end(self, cr, uid, modules):
773         """ Clear records removed from updated module data.
774
775         This method is called at the end of the module loading process.
776         It is meant to removed records that are no longer present in the
777         updated data. Such records are recognised as the one with an xml id
778         and a module in ir_model_data and noupdate set to false, but not
779         present in self.loads.
780
781         """
782         if not modules:
783             return True
784         modules = list(modules)
785         module_in = ",".join(["%s"] * len(modules))
786         cr.execute('select id,name,model,res_id,module from ir_model_data where module IN (' + module_in + ') and noupdate=%s', modules + [False])
787         wkf_todo = []
788         to_unlink = []
789         for (id, name, model, res_id,module) in cr.fetchall():
790             if (module,name) not in self.loads:
791                 to_unlink.append((model,res_id))
792                 if model=='workflow.activity':
793                     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,))
794                     wkf_todo.extend(cr.fetchall())
795                     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))
796                     cr.execute("delete from wkf_transition where act_to=%s", (res_id,))
797
798         for model,id in wkf_todo:
799             wf_service = netsvc.LocalService("workflow")
800             wf_service.trg_write(uid, model, id, cr)
801
802         cr.commit()
803         if not config.get('import_partial'):
804             for (model, res_id) in to_unlink:
805                 if self.pool.get(model):
806                     self.__logger.info('Deleting %s@%s', res_id, model)
807                     try:
808                         self.pool.get(model).unlink(cr, uid, [res_id])
809                         cr.commit()
810                     except Exception:
811                         cr.rollback()
812                         self.__logger.warn(
813                             'Could not delete obsolete record with id: %d of model %s\n'
814                             'There should be some relation that points to this resource\n'
815                             'You should manually fix this and restart with --update=module',
816                             res_id, model)
817         return True
818 ir_model_data()
819
820 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: