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