[REF] no longer use openerp.pooler.
[odoo/odoo.git] / openerp / addons / base / res / res_config.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 from operator import attrgetter
23 import re
24
25 import openerp
26 from openerp import SUPERUSER_ID
27 from openerp.osv import osv, fields
28 from openerp.tools import ustr
29 from openerp.tools.translate import _
30 from openerp import exceptions
31
32 _logger = logging.getLogger(__name__)
33
34 class res_config_configurable(osv.osv_memory):
35     ''' Base classes for new-style configuration items
36
37     Configuration items should inherit from this class, implement
38     the execute method (and optionally the cancel one) and have
39     their view inherit from the related res_config_view_base view.
40     '''
41     _name = 'res.config'
42
43     def _next_action(self, cr, uid, context=None):
44         Todos = self.pool['ir.actions.todo']
45         _logger.info('getting next %s', Todos)
46
47         active_todos = Todos.browse(cr, uid,
48             Todos.search(cr, uid, ['&', ('type', '=', 'automatic'), ('state','=','open')]),
49                                     context=context)
50
51         user_groups = set(map(
52             lambda g: g.id,
53             self.pool['res.users'].browse(cr, uid, [uid], context=context)[0].groups_id))
54
55         valid_todos_for_user = [
56             todo for todo in active_todos
57             if not todo.groups_id or bool(user_groups.intersection((
58                 group.id for group in todo.groups_id)))
59         ]
60
61         if valid_todos_for_user:
62             return valid_todos_for_user[0]
63
64         return None
65
66     def _next(self, cr, uid, context=None):
67         _logger.info('getting next operation')
68         next = self._next_action(cr, uid, context=context)
69         _logger.info('next action is %s', next)
70         if next:
71             res = next.action_launch(context=context)
72             res['nodestroy'] = False
73             return res
74         # reload the client; open the first available root menu
75         menu_obj = self.pool['ir.ui.menu']
76         menu_ids = menu_obj.search(cr, uid, [('parent_id', '=', False)], context=context)
77         return {
78             'type': 'ir.actions.client',
79             'tag': 'reload',
80             'params': {'menu_id': menu_ids and menu_ids[0] or False},
81         }
82
83     def start(self, cr, uid, ids, context=None):
84         return self.next(cr, uid, ids, context)
85
86     def next(self, cr, uid, ids, context=None):
87         """ Returns the next todo action to execute (using the default
88         sort order)
89         """
90         return self._next(cr, uid, context=context)
91
92     def execute(self, cr, uid, ids, context=None):
93         """ Method called when the user clicks on the ``Next`` button.
94
95         Execute *must* be overloaded unless ``action_next`` is overloaded
96         (which is something you generally don't need to do).
97
98         If ``execute`` returns an action dictionary, that action is executed
99         rather than just going to the next configuration item.
100         """
101         raise NotImplementedError(
102             'Configuration items need to implement execute')
103     def cancel(self, cr, uid, ids, context=None):
104         """ Method called when the user click on the ``Skip`` button.
105
106         ``cancel`` should be overloaded instead of ``action_skip``. As with
107         ``execute``, if it returns an action dictionary that action is
108         executed in stead of the default (going to the next configuration item)
109
110         The default implementation is a NOOP.
111
112         ``cancel`` is also called by the default implementation of
113         ``action_cancel``.
114         """
115         pass
116
117     def action_next(self, cr, uid, ids, context=None):
118         """ Action handler for the ``next`` event.
119
120         Sets the status of the todo the event was sent from to
121         ``done``, calls ``execute`` and -- unless ``execute`` returned
122         an action dictionary -- executes the action provided by calling
123         ``next``.
124         """
125         next = self.execute(cr, uid, ids, context=context)
126         if next: return next
127         return self.next(cr, uid, ids, context=context)
128
129     def action_skip(self, cr, uid, ids, context=None):
130         """ Action handler for the ``skip`` event.
131
132         Sets the status of the todo the event was sent from to
133         ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
134         an action dictionary -- executes the action provided by calling
135         ``next``.
136         """
137         next = self.cancel(cr, uid, ids, context=context)
138         if next: return next
139         return self.next(cr, uid, ids, context=context)
140
141     def action_cancel(self, cr, uid, ids, context=None):
142         """ Action handler for the ``cancel`` event. That event isn't
143         generated by the res.config.view.base inheritable view, the
144         inherited view has to overload one of the buttons (or add one
145         more).
146
147         Sets the status of the todo the event was sent from to
148         ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
149         an action dictionary -- executes the action provided by calling
150         ``next``.
151         """
152         next = self.cancel(cr, uid, ids, context=context)
153         if next: return next
154         return self.next(cr, uid, ids, context=context)
155
156 res_config_configurable()
157
158 class res_config_installer(osv.osv_memory):
159     """ New-style configuration base specialized for addons selection
160     and installation.
161
162     Basic usage
163     -----------
164
165     Subclasses can simply define a number of _columns as
166     fields.boolean objects. The keys (column names) should be the
167     names of the addons to install (when selected). Upon action
168     execution, selected boolean fields (and those only) will be
169     interpreted as addons to install, and batch-installed.
170
171     Additional addons
172     -----------------
173
174     It is also possible to require the installation of an additional
175     addon set when a specific preset of addons has been marked for
176     installation (in the basic usage only, additionals can't depend on
177     one another).
178
179     These additionals are defined through the ``_install_if``
180     property. This property is a mapping of a collection of addons (by
181     name) to a collection of addons (by name) [#]_, and if all the *key*
182     addons are selected for installation, then the *value* ones will
183     be selected as well. For example::
184
185         _install_if = {
186             ('sale','crm'): ['sale_crm'],
187         }
188
189     This will install the ``sale_crm`` addon if and only if both the
190     ``sale`` and ``crm`` addons are selected for installation.
191
192     You can define as many additionals as you wish, and additionals
193     can overlap in key and value. For instance::
194
195         _install_if = {
196             ('sale','crm'): ['sale_crm'],
197             ('sale','project'): ['project_mrp'],
198         }
199
200     will install both ``sale_crm`` and ``project_mrp`` if all of
201     ``sale``, ``crm`` and ``project`` are selected for installation.
202
203     Hook methods
204     ------------
205
206     Subclasses might also need to express dependencies more complex
207     than that provided by additionals. In this case, it's possible to
208     define methods of the form ``_if_%(name)s`` where ``name`` is the
209     name of a boolean field. If the field is selected, then the
210     corresponding module will be marked for installation *and* the
211     hook method will be executed.
212
213     Hook methods take the usual set of parameters (cr, uid, ids,
214     context) and can return a collection of additional addons to
215     install (if they return anything, otherwise they should not return
216     anything, though returning any "falsy" value such as None or an
217     empty collection will have the same effect).
218
219     Complete control
220     ----------------
221
222     The last hook is to simply overload the ``modules_to_install``
223     method, which implements all the mechanisms above. This method
224     takes the usual set of parameters (cr, uid, ids, context) and
225     returns a ``set`` of addons to install (addons selected by the
226     above methods minus addons from the *basic* set which are already
227     installed) [#]_ so an overloader can simply manipulate the ``set``
228     returned by ``res_config_installer.modules_to_install`` to add or
229     remove addons.
230
231     Skipping the installer
232     ----------------------
233
234     Unless it is removed from the view, installers have a *skip*
235     button which invokes ``action_skip`` (and the ``cancel`` hook from
236     ``res.config``). Hooks and additionals *are not run* when skipping
237     installation, even for already installed addons.
238
239     Again, setup your hooks accordingly.
240
241     .. [#] note that since a mapping key needs to be hashable, it's
242            possible to use a tuple or a frozenset, but not a list or a
243            regular set
244
245     .. [#] because the already-installed modules are only pruned at
246            the very end of ``modules_to_install``, additionals and
247            hooks depending on them *are guaranteed to execute*. Setup
248            your hooks accordingly.
249     """
250     _name = 'res.config.installer'
251     _inherit = 'res.config'
252
253     _install_if = {}
254
255     def already_installed(self, cr, uid, context=None):
256         """ For each module, check if it's already installed and if it
257         is return its name
258
259         :returns: a list of the already installed modules in this
260                   installer
261         :rtype: [str]
262         """
263         return map(attrgetter('name'),
264                    self._already_installed(cr, uid, context=context))
265
266     def _already_installed(self, cr, uid, context=None):
267         """ For each module (boolean fields in a res.config.installer),
268         check if it's already installed (either 'to install', 'to upgrade'
269         or 'installed') and if it is return the module's browse_record
270
271         :returns: a list of all installed modules in this installer
272         :rtype: [browse_record]
273         """
274         modules = self.pool['ir.module.module']
275
276         selectable = [field for field in self._columns
277                       if type(self._columns[field]) is fields.boolean]
278         return modules.browse(
279             cr, uid,
280             modules.search(cr, uid,
281                            [('name','in',selectable),
282                             ('state','in',['to install', 'installed', 'to upgrade'])],
283                            context=context),
284             context=context)
285
286
287     def modules_to_install(self, cr, uid, ids, context=None):
288         """ selects all modules to install:
289
290         * checked boolean fields
291         * return values of hook methods. Hook methods are of the form
292           ``_if_%(addon_name)s``, and are called if the corresponding
293           addon is marked for installation. They take the arguments
294           cr, uid, ids and context, and return an iterable of addon
295           names
296         * additionals, additionals are setup through the ``_install_if``
297           class variable. ``_install_if`` is a dict of {iterable:iterable}
298           where key and value are iterables of addon names.
299
300           If all the addons in the key are selected for installation
301           (warning: addons added through hooks don't count), then the
302           addons in the value are added to the set of modules to install
303         * not already installed
304         """
305         base = set(module_name
306                    for installer in self.read(cr, uid, ids, context=context)
307                    for module_name, to_install in installer.iteritems()
308                    if module_name != 'id'
309                    if type(self._columns[module_name]) is fields.boolean
310                    if to_install)
311
312         hooks_results = set()
313         for module in base:
314             hook = getattr(self, '_if_%s'% module, None)
315             if hook:
316                 hooks_results.update(hook(cr, uid, ids, context=None) or set())
317
318         additionals = set(
319             module for requirements, consequences \
320                        in self._install_if.iteritems()
321                    if base.issuperset(requirements)
322                    for module in consequences)
323
324         return (base | hooks_results | additionals).difference(
325                     self.already_installed(cr, uid, context))
326
327     def default_get(self, cr, uid, fields_list, context=None):
328         ''' If an addon is already installed, check it by default
329         '''
330         defaults = super(res_config_installer, self).default_get(
331             cr, uid, fields_list, context=context)
332
333         return dict(defaults,
334                     **dict.fromkeys(
335                         self.already_installed(cr, uid, context=context),
336                         True))
337
338     def fields_get(self, cr, uid, fields=None, context=None, write_access=True):
339         """ If an addon is already installed, set it to readonly as
340         res.config.installer doesn't handle uninstallations of already
341         installed addons
342         """
343         fields = super(res_config_installer, self).fields_get(
344             cr, uid, fields, context, write_access)
345
346         for name in self.already_installed(cr, uid, context=context):
347             if name not in fields:
348                 continue
349             fields[name].update(
350                 readonly=True,
351                 help= ustr(fields[name].get('help', '')) +
352                      _('\n\nThis addon is already installed on your system'))
353         return fields
354
355     def execute(self, cr, uid, ids, context=None):
356         modules = self.pool['ir.module.module']
357         to_install = list(self.modules_to_install(
358             cr, uid, ids, context=context))
359         _logger.info('Selecting addons %s to install', to_install)
360         modules.state_update(
361             cr, uid,
362             modules.search(cr, uid, [('name','in',to_install)]),
363             'to install', ['uninstalled'], context=context)
364         cr.commit()
365         openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
366         openerp.modules.registry.RegistryManager.new(cr.dbname, update_module=True)
367
368 res_config_installer()
369
370 DEPRECATION_MESSAGE = 'You are using an addon using old-style configuration '\
371     'wizards (ir.actions.configuration.wizard). Old-style configuration '\
372     'wizards have been deprecated.\n'\
373     'The addon should be migrated to res.config objects.'
374 class ir_actions_configuration_wizard(osv.osv_memory):
375     ''' Compatibility configuration wizard
376
377     The old configuration wizard has been replaced by res.config, but in order
378     not to break existing but not-yet-migrated addons, the old wizard was
379     reintegrated and gutted.
380     '''
381     _name='ir.actions.configuration.wizard'
382     _inherit = 'res.config'
383
384     def _next_action_note(self, cr, uid, ids, context=None):
385         next = self._next_action(cr, uid)
386         if next:
387             # if the next one is also an old-style extension, you never know...
388             if next.note:
389                 return next.note
390             return _("Click 'Continue' to configure the next addon...")
391         return _("Your database is now fully configured.\n\n"\
392             "Click 'Continue' and enjoy your OpenERP experience...")
393
394     _columns = {
395         'note': fields.text('Next Wizard', readonly=True),
396         }
397     _defaults = {
398         'note': _next_action_note,
399         }
400
401     def execute(self, cr, uid, ids, context=None):
402         _logger.warning(DEPRECATION_MESSAGE)
403
404 ir_actions_configuration_wizard()
405
406
407
408 class res_config_settings(osv.osv_memory):
409     """ Base configuration wizard for application settings.  It provides support for setting
410         default values, assigning groups to employee users, and installing modules.
411         To make such a 'settings' wizard, define a model like::
412
413             class my_config_wizard(osv.osv_memory):
414                 _name = 'my.settings'
415                 _inherit = 'res.config.settings'
416                 _columns = {
417                     'default_foo': fields.type(..., default_model='my.model'),
418                     'group_bar': fields.boolean(..., group='base.group_user', implied_group='my.group'),
419                     'module_baz': fields.boolean(...),
420                     'other_field': fields.type(...),
421                 }
422
423         The method ``execute`` provides some support based on a naming convention:
424
425         *   For a field like 'default_XXX', ``execute`` sets the (global) default value of
426             the field 'XXX' in the model named by ``default_model`` to the field's value.
427
428         *   For a boolean field like 'group_XXX', ``execute`` adds/removes 'implied_group'
429             to/from the implied groups of 'group', depending on the field's value.
430             By default 'group' is the group Employee.  Groups are given by their xml id.
431
432         *   For a boolean field like 'module_XXX', ``execute`` triggers the immediate
433             installation of the module named 'XXX' if the field has value ``True``.
434
435         *   For the other fields, the method ``execute`` invokes all methods with a name
436             that starts with 'set_'; such methods can be defined to implement the effect
437             of those fields.
438
439         The method ``default_get`` retrieves values that reflect the current status of the
440         fields like 'default_XXX', 'group_XXX' and 'module_XXX'.  It also invokes all methods
441         with a name that starts with 'get_default_'; such methods can be defined to provide
442         current values for other fields.
443     """
444     _name = 'res.config.settings'
445
446     def copy(self, cr, uid, id, values, context=None):
447         raise osv.except_osv(_("Cannot duplicate configuration!"), "")
448
449     def _get_classified_fields(self, cr, uid, context=None):
450         """ return a dictionary with the fields classified by category::
451
452                 {   'default': [('default_foo', 'model', 'foo'), ...],
453                     'group':   [('group_bar', browse_group, browse_implied_group), ...],
454                     'module':  [('module_baz', browse_module), ...],
455                     'other':   ['other_field', ...],
456                 }
457         """
458         ir_model_data = self.pool['ir.model.data']
459         ir_module = self.pool['ir.module.module']
460         def ref(xml_id):
461             mod, xml = xml_id.split('.', 1)
462             return ir_model_data.get_object(cr, uid, mod, xml, context)
463
464         defaults, groups, modules, others = [], [], [], []
465         for name, field in self._columns.items():
466             if name.startswith('default_') and hasattr(field, 'default_model'):
467                 defaults.append((name, field.default_model, name[8:]))
468             elif name.startswith('group_') and isinstance(field, fields.boolean) and hasattr(field, 'implied_group'):
469                 field_group = getattr(field, 'group', 'base.group_user')
470                 groups.append((name, ref(field_group), ref(field.implied_group)))
471             elif name.startswith('module_') and isinstance(field, fields.boolean):
472                 mod_ids = ir_module.search(cr, uid, [('name', '=', name[7:])])
473                 record = ir_module.browse(cr, uid, mod_ids[0], context) if mod_ids else None
474                 modules.append((name, record))
475             else:
476                 others.append(name)
477
478         return {'default': defaults, 'group': groups, 'module': modules, 'other': others}
479
480     def default_get(self, cr, uid, fields, context=None):
481         ir_values = self.pool['ir.values']
482         classified = self._get_classified_fields(cr, uid, context)
483
484         res = super(res_config_settings, self).default_get(cr, uid, fields, context)
485
486         # defaults: take the corresponding default value they set
487         for name, model, field in classified['default']:
488             value = ir_values.get_default(cr, uid, model, field)
489             if value is not None:
490                 res[name] = value
491
492         # groups: which groups are implied by the group Employee
493         for name, group, implied_group in classified['group']:
494             res[name] = implied_group in group.implied_ids
495
496         # modules: which modules are installed/to install
497         for name, module in classified['module']:
498             res[name] = module and module.state in ('installed', 'to install', 'to upgrade')
499
500         # other fields: call all methods that start with 'get_default_'
501         for method in dir(self):
502             if method.startswith('get_default_'):
503                 res.update(getattr(self, method)(cr, uid, fields, context))
504
505         return res
506
507     def execute(self, cr, uid, ids, context=None):
508         ir_values = self.pool['ir.values']
509         ir_module = self.pool['ir.module.module']
510         classified = self._get_classified_fields(cr, uid, context)
511
512         config = self.browse(cr, uid, ids[0], context)
513
514         # default values fields
515         for name, model, field in classified['default']:
516             ir_values.set_default(cr, uid, model, field, config[name])
517
518         # group fields: modify group / implied groups
519         for name, group, implied_group in classified['group']:
520             if config[name]:
521                 group.write({'implied_ids': [(4, implied_group.id)]})
522             else:
523                 group.write({'implied_ids': [(3, implied_group.id)]})
524                 implied_group.write({'users': [(3, u.id) for u in group.users]})
525
526         # other fields: execute all methods that start with 'set_'
527         for method in dir(self):
528             if method.startswith('set_'):
529                 getattr(self, method)(cr, uid, ids, context)
530
531         # module fields: install/uninstall the selected modules
532         to_install_missing_names = []
533         to_uninstall_ids = []
534         to_install_ids = []
535         lm = len('module_')
536         for name, module in classified['module']:
537             if config[name]:
538                 if not module:
539                     # missing module, will be provided by apps.openerp.com
540                     to_install_missing_names.append(name[lm:])
541                 elif module.state == 'uninstalled':
542                     # local module, to be installed
543                     to_install_ids.append(module.id)
544             else:
545                 if module and module.state in ('installed', 'to upgrade'):
546                     to_uninstall_ids.append(module.id)
547
548         if to_uninstall_ids:
549             ir_module.button_immediate_uninstall(cr, uid, to_uninstall_ids, context=context)
550         if to_install_ids:
551             ir_module.button_immediate_install(cr, uid, to_install_ids, context=context)
552
553         if to_install_missing_names:
554             return {
555                 'type': 'ir.actions.client',
556                 'tag': 'apps',
557                 'params': {'modules': to_install_missing_names},
558             }
559
560         # After the uninstall/install calls, the self.pool is no longer valid.
561         # So we reach into the RegistryManager directly.
562         res_config = openerp.modules.registry.RegistryManager.get(cr.dbname)['res.config']
563         config = res_config.next(cr, uid, [], context=context) or {}
564         if config.get('type') not in ('ir.actions.act_window_close',):
565             return config
566
567         # force client-side reload (update user menu and current view)
568         return {
569             'type': 'ir.actions.client',
570             'tag': 'reload',
571         }
572
573     def cancel(self, cr, uid, ids, context=None):
574         # ignore the current record, and send the action to reopen the view
575         act_window = self.pool['ir.actions.act_window']
576         action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)])
577         if action_ids:
578             return act_window.read(cr, uid, action_ids[0], [], context=context)
579         return {}
580
581     def name_get(self, cr, uid, ids, context=None):
582         """ Override name_get method to return an appropriate configuration wizard
583         name, and not the generated name."""
584
585         if not ids:
586             return []
587         # name_get may receive int id instead of an id list
588         if isinstance(ids, (int, long)):
589             ids = [ids]
590
591         act_window = self.pool['ir.actions.act_window']
592         action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)], context=context)
593         name = self._name
594         if action_ids:
595             name = act_window.read(cr, uid, action_ids[0], ['name'], context=context)['name']
596         return [(record.id, name) for record in self.browse(cr, uid , ids, context=context)]
597
598     def get_option_path(self, cr, uid, menu_xml_id, context=None):
599         """
600         Fetch the path to a specified configuration view and the action id to access it.
601
602         :param string menu_xml_id: the xml id of the menuitem where the view is located,
603             structured as follows: module_name.menuitem_xml_id (e.g.: "base.menu_sale_config")
604         :return tuple:
605             - t[0]: string: full path to the menuitem (e.g.: "Settings/Configuration/Sales")
606             - t[1]: long: id of the menuitem's action
607         """
608         module_name, menu_xml_id = menu_xml_id.split('.')
609         dummy, menu_id = self.pool['ir.model.data'].get_object_reference(cr, uid, module_name, menu_xml_id)
610         ir_ui_menu = self.pool['ir.ui.menu'].browse(cr, uid, menu_id, context=context)
611
612         return (ir_ui_menu.complete_name, ir_ui_menu.action.id)
613
614     def get_option_name(self, cr, uid, full_field_name, context=None):
615         """
616         Fetch the human readable name of a specified configuration option.
617
618         :param string full_field_name: the full name of the field, structured as follows:
619             model_name.field_name (e.g.: "sale.config.settings.fetchmail_lead")
620         :return string: human readable name of the field (e.g.: "Create leads from incoming mails")
621         """
622         model_name, field_name = full_field_name.rsplit('.', 1)
623
624         return self.pool[model_name].fields_get(cr, uid, allfields=[field_name], context=context)[field_name]['string']
625
626     def get_config_warning(self, cr, msg, context=None):
627         """
628         Helper: return a Warning exception with the given message where the %(field:xxx)s
629         and/or %(menu:yyy)s are replaced by the human readable field's name and/or menuitem's
630         full path.
631
632         Usage:
633         ------
634         Just include in your error message %(field:model_name.field_name)s to obtain the human
635         readable field's name, and/or %(menu:module_name.menuitem_xml_id)s to obtain the menuitem's
636         full path.
637
638         Example of use:
639         ---------------
640         from openerp.addons.base.res.res_config import get_warning_config
641         raise get_warning_config(cr, _("Error: this action is prohibited. You should check the field %(field:sale.config.settings.fetchmail_lead)s in %(menu:base.menu_sale_config)s."), context=context)
642
643         This will return an exception containing the following message:
644             Error: this action is prohibited. You should check the field Create leads from incoming mails in Settings/Configuration/Sales.
645
646         What if there is another substitution in the message already?
647         -------------------------------------------------------------
648         You could have a situation where the error message you want to upgrade already contains a substitution. Example:
649             Cannot find any account journal of %s type for this company.\n\nYou can create one in the menu: \nConfiguration\Journals\Journals.
650         What you want to do here is simply to replace the path by %menu:account.menu_account_config)s, and leave the rest alone.
651         In order to do that, you can use the double percent (%%) to escape your new substitution, like so:
652             Cannot find any account journal of %s type for this company.\n\nYou can create one in the %%(menu:account.menu_account_config)s.
653         """
654
655         res_config_obj = openerp.registry(cr.dbname)['res.config.settings']
656         regex_path = r'%\(((?:menu|field):[a-z_\.]*)\)s'
657
658         # Process the message
659         # 1/ find the menu and/or field references, put them in a list
660         references = re.findall(regex_path, msg, flags=re.I)
661
662         # 2/ fetch the menu and/or field replacement values (full path and
663         #    human readable field's name) and the action_id if any
664         values = {}
665         action_id = None
666         for item in references:
667             ref_type, ref = item.split(':')
668             if ref_type == 'menu':
669                 values[item], action_id = res_config_obj.get_option_path(cr, SUPERUSER_ID, ref, context=context)
670             elif ref_type == 'field':
671                 values[item] = res_config_obj.get_option_name(cr, SUPERUSER_ID, ref, context=context)
672
673         # 3/ substitute and return the result
674         if (action_id):
675             return exceptions.RedirectWarning(msg % values, action_id, _('Go to the configuration panel'))
676         return exceptions.Warning(msg % values)
677 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: