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