[FIX] res.config.installer: correct module installing
[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
24 from openerp.osv import osv, fields
25 from openerp.tools import ustr
26 from openerp.tools.translate import _
27
28 _logger = logging.getLogger(__name__)
29
30
31 class res_config_module_installation_mixin(object):
32     def _install_modules(self, cr, uid, modules, context):
33         """Install the requested modules.
34             return the next action to execute
35
36           modules is a list of tuples
37             (mod_name, browse_record | None)
38         """
39         ir_module = self.pool.get('ir.module.module')
40         to_install_ids = []
41         to_install_missing_names = []
42
43         for name, module in modules:
44             if not module:
45                 to_install_missing_names.append(name)
46             elif module.state == 'uninstalled':
47                 to_install_ids.append(module.id)
48
49         if to_install_ids:
50             ir_module.button_immediate_install(cr, uid, to_install_ids, context=context)
51
52         if to_install_missing_names:
53             return {
54                 'type': 'ir.actions.client',
55                 'tag': 'apps',
56                 'params': {'modules': to_install_missing_names},
57             }
58
59         return None
60
61 class res_config_configurable(osv.osv_memory):
62     ''' Base classes for new-style configuration items
63
64     Configuration items should inherit from this class, implement
65     the execute method (and optionally the cancel one) and have
66     their view inherit from the related res_config_view_base view.
67     '''
68     _name = 'res.config'
69
70     def _next_action(self, cr, uid, context=None):
71         Todos = self.pool['ir.actions.todo']
72         _logger.info('getting next %s', Todos)
73
74         active_todos = Todos.browse(cr, uid,
75             Todos.search(cr, uid, ['&', ('type', '=', 'automatic'), ('state','=','open')]),
76                                     context=context)
77
78         user_groups = set(map(
79             lambda g: g.id,
80             self.pool['res.users'].browse(cr, uid, [uid], context=context)[0].groups_id))
81
82         valid_todos_for_user = [
83             todo for todo in active_todos
84             if not todo.groups_id or bool(user_groups.intersection((
85                 group.id for group in todo.groups_id)))
86         ]
87
88         if valid_todos_for_user:
89             return valid_todos_for_user[0]
90
91         return None
92
93     def _next(self, cr, uid, context=None):
94         _logger.info('getting next operation')
95         next = self._next_action(cr, uid, context=context)
96         _logger.info('next action is %s', next)
97         if next:
98             res = next.action_launch(context=context)
99             res['nodestroy'] = False
100             return res
101         # reload the client; open the first available root menu
102         menu_obj = self.pool.get('ir.ui.menu')
103         menu_ids = menu_obj.search(cr, uid, [('parent_id', '=', False)], context=context)
104         return {
105             'type': 'ir.actions.client',
106             'tag': 'reload',
107             'params': {'menu_id': menu_ids and menu_ids[0] or False},
108         }
109
110     def start(self, cr, uid, ids, context=None):
111         return self.next(cr, uid, ids, context)
112
113     def next(self, cr, uid, ids, context=None):
114         """ Returns the next todo action to execute (using the default
115         sort order)
116         """
117         return self._next(cr, uid, context=context)
118
119     def execute(self, cr, uid, ids, context=None):
120         """ Method called when the user clicks on the ``Next`` button.
121
122         Execute *must* be overloaded unless ``action_next`` is overloaded
123         (which is something you generally don't need to do).
124
125         If ``execute`` returns an action dictionary, that action is executed
126         rather than just going to the next configuration item.
127         """
128         raise NotImplementedError(
129             'Configuration items need to implement execute')
130     def cancel(self, cr, uid, ids, context=None):
131         """ Method called when the user click on the ``Skip`` button.
132
133         ``cancel`` should be overloaded instead of ``action_skip``. As with
134         ``execute``, if it returns an action dictionary that action is
135         executed in stead of the default (going to the next configuration item)
136
137         The default implementation is a NOOP.
138
139         ``cancel`` is also called by the default implementation of
140         ``action_cancel``.
141         """
142         pass
143
144     def action_next(self, cr, uid, ids, context=None):
145         """ Action handler for the ``next`` event.
146
147         Sets the status of the todo the event was sent from to
148         ``done``, calls ``execute`` and -- unless ``execute`` returned
149         an action dictionary -- executes the action provided by calling
150         ``next``.
151         """
152         next = self.execute(cr, uid, ids, context=context)
153         if next: return next
154         return self.next(cr, uid, ids, context=context)
155
156     def action_skip(self, cr, uid, ids, context=None):
157         """ Action handler for the ``skip`` event.
158
159         Sets the status of the todo the event was sent from to
160         ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
161         an action dictionary -- executes the action provided by calling
162         ``next``.
163         """
164         next = self.cancel(cr, uid, ids, context=context)
165         if next: return next
166         return self.next(cr, uid, ids, context=context)
167
168     def action_cancel(self, cr, uid, ids, context=None):
169         """ Action handler for the ``cancel`` event. That event isn't
170         generated by the res.config.view.base inheritable view, the
171         inherited view has to overload one of the buttons (or add one
172         more).
173
174         Sets the status of the todo the event was sent from to
175         ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
176         an action dictionary -- executes the action provided by calling
177         ``next``.
178         """
179         next = self.cancel(cr, uid, ids, context=context)
180         if next: return next
181         return self.next(cr, uid, ids, context=context)
182
183 res_config_configurable()
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.get('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
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[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 res_config_installer()
397
398 DEPRECATION_MESSAGE = 'You are using an addon using old-style configuration '\
399     'wizards (ir.actions.configuration.wizard). Old-style configuration '\
400     'wizards have been deprecated.\n'\
401     'The addon should be migrated to res.config objects.'
402 class ir_actions_configuration_wizard(osv.osv_memory):
403     ''' Compatibility configuration wizard
404
405     The old configuration wizard has been replaced by res.config, but in order
406     not to break existing but not-yet-migrated addons, the old wizard was
407     reintegrated and gutted.
408     '''
409     _name='ir.actions.configuration.wizard'
410     _inherit = 'res.config'
411
412     def _next_action_note(self, cr, uid, ids, context=None):
413         next = self._next_action(cr, uid)
414         if next:
415             # if the next one is also an old-style extension, you never know...
416             if next.note:
417                 return next.note
418             return _("Click 'Continue' to configure the next addon...")
419         return _("Your database is now fully configured.\n\n"\
420             "Click 'Continue' and enjoy your OpenERP experience...")
421
422     _columns = {
423         'note': fields.text('Next Wizard', readonly=True),
424         }
425     _defaults = {
426         'note': _next_action_note,
427         }
428
429     def execute(self, cr, uid, ids, context=None):
430         _logger.warning(DEPRECATION_MESSAGE)
431
432 ir_actions_configuration_wizard()
433
434
435 class res_config_settings(osv.osv_memory, res_config_module_installation_mixin):
436     """ Base configuration wizard for application settings.  It provides support for setting
437         default values, assigning groups to employee users, and installing modules.
438         To make such a 'settings' wizard, define a model like::
439
440             class my_config_wizard(osv.osv_memory):
441                 _name = 'my.settings'
442                 _inherit = 'res.config.settings'
443                 _columns = {
444                     'default_foo': fields.type(..., default_model='my.model'),
445                     'group_bar': fields.boolean(..., group='base.group_user', implied_group='my.group'),
446                     'module_baz': fields.boolean(...),
447                     'other_field': fields.type(...),
448                 }
449
450         The method ``execute`` provides some support based on a naming convention:
451
452         *   For a field like 'default_XXX', ``execute`` sets the (global) default value of
453             the field 'XXX' in the model named by ``default_model`` to the field's value.
454
455         *   For a boolean field like 'group_XXX', ``execute`` adds/removes 'implied_group'
456             to/from the implied groups of 'group', depending on the field's value.
457             By default 'group' is the group Employee.  Groups are given by their xml id.
458
459         *   For a boolean field like 'module_XXX', ``execute`` triggers the immediate
460             installation of the module named 'XXX' if the field has value ``True``.
461
462         *   For the other fields, the method ``execute`` invokes all methods with a name
463             that starts with 'set_'; such methods can be defined to implement the effect
464             of those fields.
465
466         The method ``default_get`` retrieves values that reflect the current status of the
467         fields like 'default_XXX', 'group_XXX' and 'module_XXX'.  It also invokes all methods
468         with a name that starts with 'get_default_'; such methods can be defined to provide
469         current values for other fields.
470     """
471     _name = 'res.config.settings'
472
473     def copy(self, cr, uid, id, values, context=None):
474         raise osv.except_osv(_("Cannot duplicate configuration!"), "")
475
476     def _get_classified_fields(self, cr, uid, context=None):
477         """ return a dictionary with the fields classified by category::
478
479                 {   'default': [('default_foo', 'model', 'foo'), ...],
480                     'group':   [('group_bar', browse_group, browse_implied_group), ...],
481                     'module':  [('module_baz', browse_module), ...],
482                     'other':   ['other_field', ...],
483                 }
484         """
485         ir_model_data = self.pool.get('ir.model.data')
486         ir_module = self.pool.get('ir.module.module')
487         def ref(xml_id):
488             mod, xml = xml_id.split('.', 1)
489             return ir_model_data.get_object(cr, uid, mod, xml, context)
490
491         defaults, groups, modules, others = [], [], [], []
492         for name, field in self._columns.items():
493             if name.startswith('default_') and hasattr(field, 'default_model'):
494                 defaults.append((name, field.default_model, name[8:]))
495             elif name.startswith('group_') and isinstance(field, fields.boolean) and hasattr(field, 'implied_group'):
496                 field_group = getattr(field, 'group', 'base.group_user')
497                 groups.append((name, ref(field_group), ref(field.implied_group)))
498             elif name.startswith('module_') and isinstance(field, fields.boolean):
499                 mod_ids = ir_module.search(cr, uid, [('name', '=', name[7:])])
500                 record = ir_module.browse(cr, uid, mod_ids[0], context) if mod_ids else None
501                 modules.append((name, record))
502             else:
503                 others.append(name)
504
505         return {'default': defaults, 'group': groups, 'module': modules, 'other': others}
506
507     def default_get(self, cr, uid, fields, context=None):
508         ir_values = self.pool.get('ir.values')
509         classified = self._get_classified_fields(cr, uid, context)
510
511         res = super(res_config_settings, self).default_get(cr, uid, fields, context)
512
513         # defaults: take the corresponding default value they set
514         for name, model, field in classified['default']:
515             value = ir_values.get_default(cr, uid, model, field)
516             if value is not None:
517                 res[name] = value
518
519         # groups: which groups are implied by the group Employee
520         for name, group, implied_group in classified['group']:
521             res[name] = implied_group in group.implied_ids
522
523         # modules: which modules are installed/to install
524         for name, module in classified['module']:
525             res[name] = module and module.state in ('installed', 'to install', 'to upgrade')
526
527         # other fields: call all methods that start with 'get_default_'
528         for method in dir(self):
529             if method.startswith('get_default_'):
530                 res.update(getattr(self, method)(cr, uid, fields, context))
531
532         return res
533
534     def execute(self, cr, uid, ids, context=None):
535         ir_values = self.pool.get('ir.values')
536         ir_module = self.pool.get('ir.module.module')
537         classified = self._get_classified_fields(cr, uid, context)
538
539         config = self.browse(cr, uid, ids[0], context)
540
541         # default values fields
542         for name, model, field in classified['default']:
543             ir_values.set_default(cr, uid, model, field, config[name])
544
545         # group fields: modify group / implied groups
546         for name, group, implied_group in classified['group']:
547             if config[name]:
548                 group.write({'implied_ids': [(4, implied_group.id)]})
549             else:
550                 group.write({'implied_ids': [(3, implied_group.id)]})
551                 implied_group.write({'users': [(3, u.id) for u in group.users]})
552
553         # other fields: execute all methods that start with 'set_'
554         for method in dir(self):
555             if method.startswith('set_'):
556                 getattr(self, method)(cr, uid, ids, context)
557
558         # module fields: install/uninstall the selected modules
559         to_install = []
560         to_uninstall_ids = []
561         lm = len('module_')
562         for name, module in classified['module']:
563             if config[name]:
564                 to_install.append((name[lm:], module))
565             else:
566                 if module and module.state in ('installed', 'to upgrade'):
567                     to_uninstall_ids.append(module.id)
568
569         if to_uninstall_ids:
570             ir_module.button_immediate_uninstall(cr, uid, to_uninstall_ids, context=context)
571
572         action = self._install_modules(cr, uid, to_install, context=context)
573         if action:
574             return action
575
576         config = self.pool.get('res.config').next(cr, uid, [], context=context) or {}
577         if config.get('type') not in ('ir.actions.act_window_close',):
578             return config
579
580         # force client-side reload (update user menu and current view)
581         return {
582             'type': 'ir.actions.client',
583             'tag': 'reload',
584         }
585
586     def cancel(self, cr, uid, ids, context=None):
587         # ignore the current record, and send the action to reopen the view
588         act_window = self.pool.get('ir.actions.act_window')
589         action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)])
590         if action_ids:
591             return act_window.read(cr, uid, action_ids[0], [], context=context)
592         return {}
593
594     def name_get(self, cr, uid, ids, context=None):
595         """ Override name_get method to return an appropriate configuration wizard
596         name, and not the generated name."""
597
598         if not ids:
599             return []
600         # name_get may receive int id instead of an id list
601         if isinstance(ids, (int, long)):
602             ids = [ids]
603
604         act_window = self.pool.get('ir.actions.act_window')
605         action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)], context=context)
606         name = self._name
607         if action_ids:
608             name = act_window.read(cr, uid, action_ids[0], ['name'], context=context)['name']
609         return [(record.id, name) for record in self.browse(cr, uid , ids, context=context)]
610
611 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: