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