[IMP]uninstalling module on uncheck from configuration wizard
[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, itemgetter
23
24 from osv import osv, fields
25 from tools.translate import _
26 import netsvc
27 from tools import ustr
28 import pooler
29
30 _logger = logging.getLogger(__name__)
31
32 class res_config_configurable(osv.osv_memory):
33     ''' Base classes for new-style configuration items
34
35     Configuration items should inherit from this class, implement
36     the execute method (and optionally the cancel one) and have
37     their view inherit from the related res_config_view_base view.
38     '''
39     _name = 'res.config'
40     _inherit = 'ir.wizard.screen'
41
42     def _next_action(self, cr, uid, context=None):
43         Todos = self.pool['ir.actions.todo']
44         _logger.info('getting next %s', Todos)
45
46         active_todos = Todos.browse(cr, uid,
47             Todos.search(cr, uid, ['&', ('type', '=', 'automatic'), ('state','=','open')]),
48                                     context=context)
49
50         user_groups = set(map(
51             lambda g: g.id,
52             self.pool['res.users'].browse(cr, uid, [uid], context=context)[0].groups_id))
53
54         valid_todos_for_user = [
55             todo for todo in active_todos
56             if not todo.groups_id or bool(user_groups.intersection((
57                 group.id for group in todo.groups_id)))
58         ]
59
60         if valid_todos_for_user:
61             return valid_todos_for_user[0]
62
63         return None
64
65     def _next(self, cr, uid, context=None):
66         _logger.info('getting next operation')
67         next = self._next_action(cr, uid, context=context)
68         _logger.info('next action is %s', next)
69         if next:
70             res = next.action_launch(context=context)
71             res['nodestroy'] = False
72             return res
73         #if there is no next action and if html is in the context: reload instead of closing
74         if context and 'html' in context:
75             return {'type' : 'ir.actions.reload'}
76         return {'type' : 'ir.actions.act_window_close'}
77
78     def start(self, cr, uid, ids, context=None):
79         return self.next(cr, uid, ids, context)
80
81     def next(self, cr, uid, ids, context=None):
82         """ Returns the next todo action to execute (using the default
83         sort order)
84         """
85         return self._next(cr, uid, context=context)
86
87     def execute(self, cr, uid, ids, context=None):
88         """ Method called when the user clicks on the ``Next`` button.
89
90         Execute *must* be overloaded unless ``action_next`` is overloaded
91         (which is something you generally don't need to do).
92
93         If ``execute`` returns an action dictionary, that action is executed
94         rather than just going to the next configuration item.
95         """
96         raise NotImplementedError(
97             'Configuration items need to implement execute')
98     def cancel(self, cr, uid, ids, context=None):
99         """ Method called when the user click on the ``Skip`` button.
100
101         ``cancel`` should be overloaded instead of ``action_skip``. As with
102         ``execute``, if it returns an action dictionary that action is
103         executed in stead of the default (going to the next configuration item)
104
105         The default implementation is a NOOP.
106
107         ``cancel`` is also called by the default implementation of
108         ``action_cancel``.
109         """
110         pass
111
112     def action_next(self, cr, uid, ids, context=None):
113         """ Action handler for the ``next`` event.
114
115         Sets the status of the todo the event was sent from to
116         ``done``, calls ``execute`` and -- unless ``execute`` returned
117         an action dictionary -- executes the action provided by calling
118         ``next``.
119         """
120         next = self.execute(cr, uid, ids, context=context)
121         if next: return next
122         return self.next(cr, uid, ids, context=context)
123
124     def action_skip(self, cr, uid, ids, context=None):
125         """ Action handler for the ``skip`` event.
126
127         Sets the status of the todo the event was sent from to
128         ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
129         an action dictionary -- executes the action provided by calling
130         ``next``.
131         """
132         next = self.cancel(cr, uid, ids, context=context)
133         if next: return next
134         return self.next(cr, uid, ids, context=context)
135
136     def action_cancel(self, cr, uid, ids, context=None):
137         """ Action handler for the ``cancel`` event. That event isn't
138         generated by the res.config.view.base inheritable view, the
139         inherited view has to overload one of the buttons (or add one
140         more).
141
142         Sets the status of the todo the event was sent from to
143         ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
144         an action dictionary -- executes the action provided by calling
145         ``next``.
146         """
147         next = self.cancel(cr, uid, ids, context=context)
148         if next: return next
149         return self.next(cr, uid, ids, context=context)
150
151 res_config_configurable()
152
153 class res_config_installer(osv.osv_memory):
154     """ New-style configuration base specialized for addons selection
155     and installation.
156
157     Basic usage
158     -----------
159
160     Subclasses can simply define a number of _columns as
161     fields.boolean objects. The keys (column names) should be the
162     names of the addons to install (when selected). Upon action
163     execution, selected boolean fields (and those only) will be
164     interpreted as addons to install, and batch-installed.
165
166     Additional addons
167     -----------------
168
169     It is also possible to require the installation of an additional
170     addon set when a specific preset of addons has been marked for
171     installation (in the basic usage only, additionals can't depend on
172     one another).
173
174     These additionals are defined through the ``_install_if``
175     property. This property is a mapping of a collection of addons (by
176     name) to a collection of addons (by name) [#]_, and if all the *key*
177     addons are selected for installation, then the *value* ones will
178     be selected as well. For example::
179
180         _install_if = {
181             ('sale','crm'): ['sale_crm'],
182         }
183
184     This will install the ``sale_crm`` addon if and only if both the
185     ``sale`` and ``crm`` addons are selected for installation.
186
187     You can define as many additionals as you wish, and additionals
188     can overlap in key and value. For instance::
189
190         _install_if = {
191             ('sale','crm'): ['sale_crm'],
192             ('sale','project'): ['project_mrp'],
193         }
194
195     will install both ``sale_crm`` and ``project_mrp`` if all of
196     ``sale``, ``crm`` and ``project`` are selected for installation.
197
198     Hook methods
199     ------------
200
201     Subclasses might also need to express dependencies more complex
202     than that provided by additionals. In this case, it's possible to
203     define methods of the form ``_if_%(name)s`` where ``name`` is the
204     name of a boolean field. If the field is selected, then the
205     corresponding module will be marked for installation *and* the
206     hook method will be executed.
207
208     Hook methods take the usual set of parameters (cr, uid, ids,
209     context) and can return a collection of additional addons to
210     install (if they return anything, otherwise they should not return
211     anything, though returning any "falsy" value such as None or an
212     empty collection will have the same effect).
213
214     Complete control
215     ----------------
216
217     The last hook is to simply overload the ``modules_to_install``
218     method, which implements all the mechanisms above. This method
219     takes the usual set of parameters (cr, uid, ids, context) and
220     returns a ``set`` of addons to install (addons selected by the
221     above methods minus addons from the *basic* set which are already
222     installed) [#]_ so an overloader can simply manipulate the ``set``
223     returned by ``res_config_installer.modules_to_install`` to add or
224     remove addons.
225
226     Skipping the installer
227     ----------------------
228
229     Unless it is removed from the view, installers have a *skip*
230     button which invokes ``action_skip`` (and the ``cancel`` hook from
231     ``res.config``). Hooks and additionals *are not run* when skipping
232     installation, even for already installed addons.
233
234     Again, setup your hooks accordingly.
235
236     .. [#] note that since a mapping key needs to be hashable, it's
237            possible to use a tuple or a frozenset, but not a list or a
238            regular set
239
240     .. [#] because the already-installed modules are only pruned at
241            the very end of ``modules_to_install``, additionals and
242            hooks depending on them *are guaranteed to execute*. Setup
243            your hooks accordingly.
244     """
245     _name = 'res.config.installer'
246     _inherit = 'res.config'
247
248     _install_if = {}
249
250     def already_installed(self, cr, uid, context=None):
251         """ For each module, check if it's already installed and if it
252         is return its name
253
254         :returns: a list of the already installed modules in this
255                   installer
256         :rtype: [str]
257         """
258         return map(attrgetter('name'),
259                    self._already_installed(cr, uid, context=context))
260
261     def _already_installed(self, cr, uid, context=None):
262         """ For each module (boolean fields in a res.config.installer),
263         check if it's already installed (either 'to install', 'to upgrade'
264         or 'installed') and if it is return the module's browse_record
265
266         :returns: a list of all installed modules in this installer
267         :rtype: [browse_record]
268         """
269         modules = self.pool.get('ir.module.module')
270
271         selectable = [field for field in self._columns
272                       if type(self._columns[field]) is fields.boolean]
273         return modules.browse(
274             cr, uid,
275             modules.search(cr, uid,
276                            [('name','in',selectable),
277                             ('state','in',['to install', 'installed', 'to upgrade'])],
278                            context=context),
279             context=context)
280
281
282     def modules_to_install(self, cr, uid, ids, context=None):
283         """ selects all modules to install:
284
285         * checked boolean fields
286         * return values of hook methods. Hook methods are of the form
287           ``_if_%(addon_name)s``, and are called if the corresponding
288           addon is marked for installation. They take the arguments
289           cr, uid, ids and context, and return an iterable of addon
290           names
291         * additionals, additionals are setup through the ``_install_if``
292           class variable. ``_install_if`` is a dict of {iterable:iterable}
293           where key and value are iterables of addon names.
294
295           If all the addons in the key are selected for installation
296           (warning: addons added through hooks don't count), then the
297           addons in the value are added to the set of modules to install
298         * not already installed
299         """
300         base = set(module_name
301                    for installer in self.read(cr, uid, ids, context=context)
302                    for module_name, to_install in installer.iteritems()
303                    if module_name != 'id'
304                    if type(self._columns[module_name]) is fields.boolean
305                    if to_install)
306
307         hooks_results = set()
308         for module in base:
309             hook = getattr(self, '_if_%s'%(module), None)
310             if hook:
311                 hooks_results.update(hook(cr, uid, ids, context=None) or set())
312
313         additionals = set(
314             module for requirements, consequences \
315                        in self._install_if.iteritems()
316                    if base.issuperset(requirements)
317                    for module in consequences)
318
319         return (base | hooks_results | additionals).difference(
320                     self.already_installed(cr, uid, context))
321
322     def default_get(self, cr, uid, fields_list, context=None):
323         ''' If an addon is already installed, check it by default
324         '''
325         defaults = super(res_config_installer, self).default_get(
326             cr, uid, fields_list, context=context)
327
328         return dict(defaults,
329                     **dict.fromkeys(
330                         self.already_installed(cr, uid, context=context),
331                         True))
332
333     def fields_get(self, cr, uid, fields=None, context=None, write_access=True):
334         """ If an addon is already installed, set it to readonly as
335         res.config.installer doesn't handle uninstallations of already
336         installed addons
337         """
338         fields = super(res_config_installer, self).fields_get(
339             cr, uid, fields, context, write_access)
340
341         for name in self.already_installed(cr, uid, context=context):
342             if name not in fields:
343                 continue
344             fields[name].update(
345                 readonly=True,
346                 help= ustr(fields[name].get('help', '')) +
347                      _('\n\nThis addon is already installed on your system'))
348         return fields
349
350     def execute(self, cr, uid, ids, context=None):
351         modules = self.pool.get('ir.module.module')
352         to_install = list(self.modules_to_install(
353             cr, uid, ids, context=context))
354         _logger.info('Selecting addons %s to install', to_install)
355         modules.state_update(
356             cr, uid,
357             modules.search(cr, uid, [('name','in',to_install)]),
358             'to install', ['uninstalled'], context=context)
359         cr.commit() #TOFIX: after remove this statement, installation wizard is fail
360         new_db, self.pool = pooler.restart_pool(cr.dbname, update_module=True)
361 res_config_installer()
362
363 DEPRECATION_MESSAGE = 'You are using an addon using old-style configuration '\
364     'wizards (ir.actions.configuration.wizard). Old-style configuration '\
365     'wizards have been deprecated.\n'\
366     'The addon should be migrated to res.config objects.'
367 class ir_actions_configuration_wizard(osv.osv_memory):
368     ''' Compatibility configuration wizard
369
370     The old configuration wizard has been replaced by res.config, but in order
371     not to break existing but not-yet-migrated addons, the old wizard was
372     reintegrated and gutted.
373     '''
374     _name='ir.actions.configuration.wizard'
375     _inherit = 'res.config'
376
377     def _next_action_note(self, cr, uid, ids, context=None):
378         next = self._next_action(cr, uid)
379         if next:
380             # if the next one is also an old-style extension, you never know...
381             if next.note:
382                 return next.note
383             return _("Click 'Continue' to configure the next addon...")
384         return _("Your database is now fully configured.\n\n"\
385             "Click 'Continue' and enjoy your OpenERP experience...")
386
387     _columns = {
388         'note': fields.text('Next Wizard', readonly=True),
389         }
390     _defaults = {
391         'note': _next_action_note,
392         }
393
394     def execute(self, cr, uid, ids, context=None):
395         _logger.warning(DEPRECATION_MESSAGE)
396
397 ir_actions_configuration_wizard()
398
399
400
401 class res_config_settings(osv.osv_memory):
402     """ Base configuration wizard for application settings.  It provides support for setting
403         default values, assigning groups to employee users, and installing modules.
404         To make such a 'settings' wizard, define a model like::
405
406             class my_config_wizard(osv.osv_memory):
407                 _name = 'my.settings'
408                 _inherit = 'res.config.settings'
409                 _columns = {
410                     'default_foo': fields.type(..., default_model='my.model'),
411                     'group_bar': fields.boolean(..., group='base.group_user', implied_group='my.group'),
412                     'module_baz': fields.boolean(...),
413                     'other_field': fields.type(...),
414                 }
415
416         The method ``execute`` provides some support based on a naming convention:
417
418         *   For a field like 'default_XXX', ``execute`` sets the (global) default value of
419             the field 'XXX' in the model named by ``default_model`` to the field's value.
420
421         *   For a boolean field like 'group_XXX', ``execute`` adds/removes 'implied_group'
422             to/from the implied groups of 'group', depending on the field's value.
423             By default 'group' is the group Employee.  Groups are given by their xml id.
424
425         *   For a boolean field like 'module_XXX', ``execute`` triggers the immediate
426             installation of the module named 'XXX' if the field has value ``True``.
427
428         *   For the other fields, the method ``execute`` invokes all methods with a name
429             that starts with 'set_'; such methods can be defined to implement the effect
430             of those fields.
431
432         The method ``default_get`` retrieves values that reflect the current status of the
433         fields like 'default_XXX', 'group_XXX' and 'module_XXX'.  It also invokes all methods
434         with a name that starts with 'get_default_'; such methods can be defined to provide
435         current values for other fields.
436     """
437     _name = 'res.config.settings'
438
439     def copy(self, cr, uid, id, values, context=None):
440         raise osv.except_osv(_("Cannot duplicate configuration!"), "")
441
442     def _get_classified_fields(self, cr, uid, context=None):
443         """ return a dictionary with the fields classified by category::
444
445                 {   'default': [('default_foo', 'model', 'foo'), ...],
446                     'group':   [('group_bar', browse_group, browse_implied_group), ...],
447                     'module':  [('module_baz', browse_module), ...],
448                     'other':   ['other_field', ...],
449                 }
450         """
451         ir_model_data = self.pool.get('ir.model.data')
452         ir_module = self.pool.get('ir.module.module')
453         def ref(xml_id):
454             mod, xml = xml_id.split('.', 1)
455             return ir_model_data.get_object(cr, uid, mod, xml, context)
456
457         defaults, groups, modules, others = [], [], [], []
458         for name, field in self._columns.items():
459             if name.startswith('default_') and hasattr(field, 'default_model'):
460                 defaults.append((name, field.default_model, name[8:]))
461             elif name.startswith('group_') and isinstance(field, fields.boolean) and hasattr(field, 'implied_group'):
462                 field_group = getattr(field, 'group', 'base.group_user')
463                 groups.append((name, ref(field_group), ref(field.implied_group)))
464             elif name.startswith('module_') and isinstance(field, fields.boolean):
465                 mod_ids = ir_module.search(cr, uid, [('name', '=', name[7:])])
466                 modules.append((name, ir_module.browse(cr, uid, mod_ids[0], context)))
467             else:
468                 others.append(name)
469
470         return {'default': defaults, 'group': groups, 'module': modules, 'other': others}
471
472     def default_get(self, cr, uid, fields, context=None):
473         ir_values = self.pool.get('ir.values')
474         classified = self._get_classified_fields(cr, uid, context)
475
476         res = super(res_config_settings, self).default_get(cr, uid, fields, context)
477
478         # defaults: take the corresponding default value they set
479         for name, model, field in classified['default']:
480             value = ir_values.get_default(cr, uid, model, field)
481             if value is not None:
482                 res[name] = value
483
484         # groups: which groups are implied by the group Employee
485         for name, group, implied_group in classified['group']:
486             res[name] = implied_group in group.implied_ids
487
488         # modules: which modules are installed/to install
489         for name, module in classified['module']:
490             res[name] = module.state in ('installed', 'to install', 'to upgrade')
491
492         # other fields: call all methods that start with 'get_default_'
493         for method in dir(self):
494             if method.startswith('get_default_'):
495                 res.update(getattr(self, method)(cr, uid, fields, context))
496
497         return res
498
499     def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
500         # overridden to make fields of installed modules readonly
501         res = super(res_config_settings, self).fields_get(cr, uid, allfields, context, write_access)
502         classified = self._get_classified_fields(cr, uid, context)
503         for name, module in classified['module']:
504             if name in res and module.state in ('installed', 'to install', 'to upgrade'):
505                 res[name]['readonly'] = False
506         return res
507
508     def execute(self, cr, uid, ids, context=None):
509         ir_values = self.pool.get('ir.values')
510         ir_model_data = self.pool.get('ir.model.data')
511         ir_module = self.pool.get('ir.module.module')
512         res_groups = self.pool.get('res.groups')
513         classified = self._get_classified_fields(cr, uid, context)
514
515         config = self.browse(cr, uid, ids[0], context)
516
517         # default values fields
518         for name, model, field in classified['default']:
519             ir_values.set_default(cr, uid, model, field, config[name])
520
521         # group fields: modify group / implied groups
522         for name, group, implied_group in classified['group']:
523             if config[name]:
524                 group.write({'implied_ids': [(4, implied_group.id)]})
525             else:
526                 group.write({'implied_ids': [(3, implied_group.id)]})
527                 implied_group.write({'users': [(3, u.id) for u in group.users]})
528
529         # other fields: execute all methods that start with 'set_'
530         for method in dir(self):
531             if method.startswith('set_'):
532                 getattr(self, method)(cr, uid, ids, context)
533
534         # module fields: install immediately the selected modules
535         to_install_ids = []
536         for name, module in classified['module']:
537             if config[name]:
538                 if module.state == 'uninstalled': to_install_ids.append(module.id)
539             else:
540                 if module.state in ['installed','upgrade']: to_uninstall_ids.append(module.id)
541         if to_install_ids:
542             ir_module.button_immediate_install(cr, uid, to_install_ids, context)
543         if to_uninstall_ids:
544             ir_module.module_uninstall(cr, uid, to_uninstall_ids, context)
545
546         # force client-side reload (update user menu and current view)
547         return {
548             'type': 'ir.actions.client',
549             'tag': 'reload',
550         }
551
552 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: