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