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