9899830a4f33e7268801ceee8087716ddf00eaf3
[odoo/odoo.git] / bin / 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
22 from operator import attrgetter
23
24 from osv import osv, fields
25 from tools.translate import _
26 import netsvc
27 import pooler
28
29 class res_config_configurable(osv.osv_memory):
30     ''' Base classes for new-style configuration items
31
32     Configuration items should inherit from this class, implement
33     the execute method (and optionally the cancel one) and have
34     their view inherit from the related res_config_view_base view.
35     '''
36     _name = 'res.config'
37     logger = netsvc.Logger()
38
39     def _progress(self, cr, uid, context=None):
40         total = self.pool.get('ir.actions.todo')\
41             .search_count(cr, uid, [], context)
42         open = self.pool.get('ir.actions.todo')\
43             .search_count(cr, uid, [('state','<>','open')], context)
44         if total:
45             return round(open*100./total)
46         return 100.
47
48     _columns = dict(
49         progress=fields.float('Configuration Progress', readonly=True),
50         )
51     _defaults = dict(
52         progress=_progress
53         )
54
55     def _next_action(self, cr, uid):
56         todos = self.pool.get('ir.actions.todo')
57         self.logger.notifyChannel('actions', netsvc.LOG_INFO,
58                                   'getting next %s' % todos)
59         active_todos = todos.search(cr, uid, [('state','=','open')],
60                                     limit=1)
61         dont_skip_todo = True
62         if active_todos:
63             todo_obj = todos.browse(cr, uid, active_todos[0], context=None)
64             todo_groups = map(lambda x:x.id, todo_obj.groups_id)
65             if todo_groups:
66                 cr.execute("select 1 from res_groups_users_rel where uid=%s and gid=ANY(%s)",(uid, todo_groups,))
67                 dont_skip_todo = bool(cr.fetchone())
68             if dont_skip_todo:
69                 return todos.browse(cr, uid, active_todos[0], context=None)
70             else:
71                 todos.write(cr, uid, active_todos[0], {'state':'skip'}, context=None)
72                 return self._next_action(cr, uid)
73         return None
74
75     def _set_previous_todo(self, cr, uid, state):
76         """ lookup the previous (which is still the next at this point)
77         ir.actions.todo, set it to whatever state was provided.
78
79         Raises
80         `LookupError`: if we couldn't find *any* previous todo
81         `ValueError`: if no state is provided
82         anything ir_actions_todo.write can throw
83         """
84         # this is ultra brittle, but apart from storing the todo id
85         # into the res.config view, I'm not sure how to get the
86         # "previous" todo
87         previous_todo = self._next_action(cr, uid)
88         if not previous_todo:
89             raise LookupError(_("Couldn't find previous ir.actions.todo"))
90         if not state:
91             raise ValueError(_("Can't set an ir.actions.todo's state to "
92                                "nothingness"))
93         previous_todo.write({'state':state})
94
95     def _next(self, cr, uid):
96         self.logger.notifyChannel('actions', netsvc.LOG_INFO,
97                                   'getting next operation')
98         next = self._next_action(cr, uid)
99         self.logger.notifyChannel('actions', netsvc.LOG_INFO,
100                                   'next action is %s' % next)
101         if next:
102             action = next.action_id
103             return {
104                 'view_mode': action.view_mode,
105                 'view_type': action.view_type,
106                 'view_id': action.view_id and [action.view_id.id] or False,
107                 'res_model': action.res_model,
108                 'type': action.type,
109                 'target': action.target,
110                 }
111         self.logger.notifyChannel(
112             'actions', netsvc.LOG_INFO,
113             'all configuration actions have been executed')
114
115         current_user_menu = self.pool.get('res.users')\
116             .browse(cr, uid, uid).menu_id
117         # return the action associated with the menu
118         return self.pool.get(current_user_menu.type)\
119             .read(cr, uid, current_user_menu.id)
120
121     def start(self, cr, uid, ids, context=None):
122         ids2 = self.pool.get('ir.actions.todo').search(cr, uid, [], context=context)
123         for todo in self.pool.get('ir.actions.todo').browse(cr, uid, ids2, context=context):
124             if (todo.restart=='always') or (todo.restart=='onskip' and (todo.state in ('skip','cancel'))):
125                 todo.write({'state':'open'})
126         return self.next(cr, uid, ids, context)
127
128     def next(self, cr, uid, ids, context=None):
129         """ Returns the next todo action to execute (using the default
130         sort order)
131         """
132         return self._next(cr, uid)
133
134     def execute(self, cr, uid, ids, context=None):
135         """ Method called when the user clicks on the ``Next`` button.
136
137         Execute *must* be overloaded unless ``action_next`` is overloaded
138         (which is something you generally don't need to do).
139
140         If ``execute`` returns an action dictionary, that action is executed
141         rather than just going to the next configuration item.
142         """
143         raise NotImplementedError(
144             'Configuration items need to implement execute')
145     def cancel(self, cr, uid, ids, context=None):
146         """ Method called when the user click on the ``Skip`` button.
147
148         ``cancel`` should be overloaded instead of ``action_skip``. As with
149         ``execute``, if it returns an action dictionary that action is
150         executed in stead of the default (going to the next configuration item)
151
152         The default implementation is a NOOP.
153
154         ``cancel`` is also called by the default implementation of
155         ``action_cancel``.
156         """
157         pass
158
159     def action_next(self, cr, uid, ids, context=None):
160         """ Action handler for the ``next`` event.
161
162         Sets the status of the todo the event was sent from to
163         ``done``, calls ``execute`` and -- unless ``execute`` returned
164         an action dictionary -- executes the action provided by calling
165         ``next``.
166         """
167         self._set_previous_todo(cr, uid, state='done')
168         next = self.execute(cr, uid, ids, context=None)
169         if next: return next
170         return self.next(cr, uid, ids, context=context)
171
172     def action_skip(self, cr, uid, ids, context=None):
173         """ Action handler for the ``skip`` event.
174
175         Sets the status of the todo the event was sent from to
176         ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
177         an action dictionary -- executes the action provided by calling
178         ``next``.
179         """
180         self._set_previous_todo(cr, uid, state='skip')
181         next = self.cancel(cr, uid, ids, context=None)
182         if next: return next
183         return self.next(cr, uid, ids, context=context)
184
185     def action_cancel(self, cr, uid, ids, context=None):
186         """ Action handler for the ``cancel`` event. That event isn't
187         generated by the res.config.view.base inheritable view, the
188         inherited view has to overload one of the buttons (or add one
189         more).
190
191         Sets the status of the todo the event was sent from to
192         ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
193         an action dictionary -- executes the action provided by calling
194         ``next``.
195         """
196         self._set_previous_todo(cr, uid, state='cancel')
197         next = self.cancel(cr, uid, ids, context=None)
198         if next: return next
199         return self.next(cr, uid, ids, context=context)
200
201 res_config_configurable()
202
203 class res_config_installer(osv.osv_memory):
204     """ New-style configuration base specialized for addons selection
205     and installation.
206
207     Basic usage
208     -----------
209
210     Subclasses can simply define a number of _columns as
211     fields.boolean objects. The keys (column names) should be the
212     names of the addons to install (when selected). Upon action
213     execution, selected boolean fields (and those only) will be
214     interpreted as addons to install, and batch-installed.
215
216     Additional addons
217     -----------------
218
219     It is also possible to require the installation of an additional
220     addon set when a specific preset of addons has been marked for
221     installation (in the basic usage only, additionals can't depend on
222     one another).
223
224     These additionals are defined through the ``_install_if``
225     property. This property is a mapping of a collection of addons (by
226     name) to a collection of addons (by name) [#]_, and if all the *key*
227     addons are selected for installation, then the *value* ones will
228     be selected as well. For example::
229
230         _install_if = {
231             ('sale','crm'): ['sale_crm'],
232         }
233
234     This will install the ``sale_crm`` addon if and only if both the
235     ``sale`` and ``crm`` addons are selected for installation.
236
237     You can define as many additionals as you wish, and additionals
238     can overlap in key and value. For instance::
239
240         _install_if = {
241             ('sale','crm'): ['sale_crm'],
242             ('sale','project'): ['project_mrp'],
243         }
244
245     will install both ``sale_crm`` and ``project_mrp`` if all of
246     ``sale``, ``crm`` and ``project`` are selected for installation.
247
248     Hook methods
249     ------------
250
251     Subclasses might also need to express dependencies more complex
252     than that provided by additionals. In this case, it's possible to
253     define methods of the form ``_if_%(name)s`` where ``name`` is the
254     name of a boolean field. If the field is selected, then the
255     corresponding module will be marked for installation *and* the
256     hook method will be executed.
257
258     Hook methods take the usual set of parameters (cr, uid, ids,
259     context) and can return a collection of additional addons to
260     install (if they return anything, otherwise they should not return
261     anything, though returning any "falsy" value such as None or an
262     empty collection will have the same effect).
263
264     Complete control
265     ----------------
266
267     The last hook is to simply overload the ``modules_to_install``
268     method, which implements all the mechanisms above. This method
269     takes the usual set of parameters (cr, uid, ids, context) and
270     returns a ``set`` of addons to install (addons selected by the
271     above methods minus addons from the *basic* set which are already
272     installed) [#]_ so an overloader can simply manipulate the ``set``
273     returned by ``res_config_installer.modules_to_install`` to add or
274     remove addons.
275
276     Skipping the installer
277     ----------------------
278
279     Unless it is removed from the view, installers have a *skip*
280     button which invokes ``action_skip`` (and the ``cancel`` hook from
281     ``res.config``). Hooks and additionals *are not run* when skipping
282     installation, even for already installed addons.
283
284     Again, setup your hooks accordinly.
285
286     .. [#] note that since a mapping key needs to be hashable, it's
287            possible to use a tuple or a frozenset, but not a list or a
288            regular set
289
290     .. [#] because the already-installed modules are only pruned at
291            the very end of ``modules_to_install``, additionals and
292            hooks depending on them *are guaranteed to execute*. Setup
293            your hooks accordingly.
294     """
295     _name = 'res.config.installer'
296     _inherit = 'res.config'
297
298     _install_if = {}
299
300     def _already_installed(self, cr, uid, context=None):
301         """ For each module (boolean fields in a res.config.installer),
302         check if it's already installed (neither uninstallable nor uninstalled)
303         and if it is, check it by default
304         """
305         modules = self.pool.get('ir.module.module')
306
307         selectable = [field for field in self._columns
308                       if type(self._columns[field]) is fields.boolean]
309         return modules.browse(
310             cr, uid,
311             modules.search(cr, uid,
312                            [('name','in',selectable),
313                             ('state','not in',['uninstallable', 'uninstalled'])],
314                            context=context),
315             context=context)
316
317
318     def modules_to_install(self, cr, uid, ids, context=None):
319         """ selects all modules to install:
320
321         * checked boolean fields
322         * return values of hook methods. Hook methods are of the form
323           ``_if_%(addon_name)s``, and are called if the corresponding
324           addon is marked for installation. They take the arguments
325           cr, uid, ids and context, and return an iterable of addon
326           names
327         * additionals, additionals are setup through the ``_install_if``
328           class variable. ``_install_if`` is a dict of {iterable:iterable}
329           where key and value are iterables of addon names.
330
331           If all the addons in the key are selected for installation
332           (warning: addons added through hooks don't count), then the
333           addons in the value are added to the set of modules to install
334         * not already installed
335         """
336         base = set(module_name
337                    for installer in self.read(cr, uid, ids, context=context)
338                    for module_name, to_install in installer.iteritems()
339                    if module_name != 'id'
340                    if type(self._columns[module_name]) is fields.boolean
341                    if to_install)
342
343         hooks_results = set()
344         for module in base:
345             hook = getattr(self, '_if_%s'%(module), None)
346             if hook:
347                 hooks_results.update(hook(cr, uid, ids, context=None) or set())
348
349         additionals = set(
350             module for requirements, consequences \
351                        in self._install_if.iteritems()
352                    if base.issuperset(requirements)
353                    for module in consequences)
354
355         return (base | hooks_results | additionals) - set(
356             map(attrgetter('name'), self._already_installed(cr, uid, context)))
357
358     def default_get(self, cr, uid, fields_list, context=None):
359         ''' If an addon is already installed, check it by default
360         '''
361         defaults = super(res_config_installer, self).default_get(
362             cr, uid, fields_list, context=context)
363
364         return dict(defaults,
365                     **dict.fromkeys(
366                         map(attrgetter('name'),
367                             self._already_installed(cr, uid, context=context)),
368                         True))
369
370     def fields_get(self, cr, uid, fields=None, context=None, read_access=True):
371         """ If an addon is already installed, set it to readonly as
372         res.config.installer doesn't handle uninstallations of already
373         installed addons
374         """
375         fields = super(res_config_installer, self).fields_get(
376             cr, uid, fields, context, read_access)
377
378         for module in self._already_installed(cr, uid, context=context):
379             fields[module.name].update(
380                 readonly=True,
381                 help=fields[module.name].get('help', '') +
382                      _('\n\nThis addon is already installed on your system'))
383
384         return fields
385
386     def execute(self, cr, uid, ids, context=None):
387         modules = self.pool.get('ir.module.module')
388         to_install = list(self.modules_to_install(
389             cr, uid, ids, context=context))
390         self.logger.notifyChannel(
391             'installer', netsvc.LOG_INFO,
392             'Selecting addons %s to install'%to_install)
393         modules.state_update(
394             cr, uid,
395             modules.search(cr, uid, [('name','in',to_install)]),
396             'to install', ['uninstalled'], context=context)
397         cr.commit()
398
399         pooler.restart_pool(cr.dbname, update_module=True)
400 res_config_installer()
401
402 DEPRECATION_MESSAGE = 'You are using an addon using old-style configuration '\
403     'wizards (ir.actions.configuration.wizard). Old-style configuration '\
404     'wizards have been deprecated.\n'\
405     'The addon should be migrated to res.config objects.'
406 class ir_actions_configuration_wizard(osv.osv_memory):
407     ''' Compatibility configuration wizard
408
409     The old configuration wizard has been replaced by res.config, but in order
410     not to break existing but not-yet-migrated addons, the old wizard was
411     reintegrated and gutted.
412     '''
413     _name='ir.actions.configuration.wizard'
414     _inherit = 'res.config'
415
416     def _next_action_note(self, cr, uid, ids, context=None):
417         next = self._next_action(cr, uid)
418         if next:
419             # if the next one is also an old-style extension, you never know...
420             if next.note:
421                 return next.note
422             return "Click 'Continue' to configure the next addon..."
423         return "Your database is now fully configured.\n\n"\
424             "Click 'Continue' and enjoy your OpenERP experience..."
425
426     _columns = {
427         'note': fields.text('Next Wizard', readonly=True),
428         }
429     _defaults = {
430         'note': _next_action_note,
431         }
432
433     def execute(self, cr, uid, ids, context=None):
434         self.logger.notifyChannel(
435             'configuration', netsvc.LOG_WARNING, DEPRECATION_MESSAGE)
436
437 ir_actions_configuration_wizard()
438
439 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: