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