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