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