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