3f8c6f20b8d4c475df5b5ce4f9c7a8416fe82787
[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 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         active_todos = todos.search(cr, uid, [('state','=','open')],
70                                     limit=1)
71         if active_todos:
72             todo_obj = todos.browse(cr, uid, active_todos[0], context=None)
73             todo_groups = map(lambda x:x.id, todo_obj.groups_id)
74             dont_skip_todo = True
75             if todo_groups:
76                 cr.execute("select 1 from res_groups_users_rel where uid=%s and gid IN %s",(uid, tuple(todo_groups),))
77                 dont_skip_todo = bool(cr.fetchone())
78             if dont_skip_todo:
79                 return todos.browse(cr, uid, active_todos[0], context=None)
80             else:
81                 todos.write(cr, uid, active_todos[0], {'state':'skip'}, context=None)
82                 return self._next_action(cr, uid)
83         return None
84
85     def _set_previous_todo(self, cr, uid, state, context=None):
86         """ lookup the previous (which is still the next at this point)
87         ir.actions.todo, set it to whatever state was provided.
88         """
89         # this is ultra brittle, but apart from storing the todo id
90         # into the res.config view, I'm not sure how to get the
91         # "previous" todo
92         previous_todo = self._next_action(cr, uid, context=context)
93         if not previous_todo:
94             self.__logger.warn(_("Couldn't find previous ir.actions.todo"))
95             return
96         previous_todo.write({'state':state})
97
98     def _next(self, cr, uid, context=None):
99         self.__logger.info('getting next operation')
100         next = self._next_action(cr, uid)
101         self.__logger.info('next action is %s', next)
102         if next:
103             action = next.action_id
104             return {
105                 'view_mode': action.view_mode,
106                 'view_type': action.view_type,
107                 'view_id': action.view_id and [action.view_id.id] or False,
108                 'res_model': action.res_model,
109                 'type': action.type,
110                 'target': action.target,
111             }
112         self.__logger.info('all configuration actions have been executed')
113
114         current_user_menu = self.pool.get('res.users')\
115             .browse(cr, uid, uid).menu_id
116         # return the action associated with the menu
117         return self.pool.get(current_user_menu.type)\
118             .read(cr, uid, current_user_menu.id)
119
120     def start(self, cr, uid, ids, context=None):
121         ids2 = self.pool.get('ir.actions.todo').search(cr, uid, [], context=context)
122         for todo in self.pool.get('ir.actions.todo').browse(cr, uid, ids2, context=context):
123             if (todo.restart=='always') or (todo.restart=='onskip' and (todo.state in ('skip','cancel'))):
124                 todo.write({'state':'open'})
125         return self.next(cr, uid, ids, context)
126
127     def next(self, cr, uid, ids, context=None):
128         """ Returns the next todo action to execute (using the default
129         sort order)
130         """
131         return self._next(cr, uid, context=context)
132
133     def execute(self, cr, uid, ids, context=None):
134         """ Method called when the user clicks on the ``Next`` button.
135
136         Execute *must* be overloaded unless ``action_next`` is overloaded
137         (which is something you generally don't need to do).
138
139         If ``execute`` returns an action dictionary, that action is executed
140         rather than just going to the next configuration item.
141         """
142         raise NotImplementedError(
143             'Configuration items need to implement execute')
144     def cancel(self, cr, uid, ids, context=None):
145         """ Method called when the user click on the ``Skip`` button.
146
147         ``cancel`` should be overloaded instead of ``action_skip``. As with
148         ``execute``, if it returns an action dictionary that action is
149         executed in stead of the default (going to the next configuration item)
150
151         The default implementation is a NOOP.
152
153         ``cancel`` is also called by the default implementation of
154         ``action_cancel``.
155         """
156         pass
157
158     def action_next(self, cr, uid, ids, context=None):
159         """ Action handler for the ``next`` event.
160
161         Sets the status of the todo the event was sent from to
162         ``done``, calls ``execute`` and -- unless ``execute`` returned
163         an action dictionary -- executes the action provided by calling
164         ``next``.
165         """
166         self._set_previous_todo(cr, uid, state='done', context=context)
167         next = self.execute(cr, uid, ids, context=None)
168         if next: return next
169         return self.next(cr, uid, ids, context=context)
170
171     def action_skip(self, cr, uid, ids, context=None):
172         """ Action handler for the ``skip`` event.
173
174         Sets the status of the todo the event was sent from to
175         ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
176         an action dictionary -- executes the action provided by calling
177         ``next``.
178         """
179         self._set_previous_todo(cr, uid, state='skip', context=context)
180         next = self.cancel(cr, uid, ids, context=None)
181         if next: return next
182         return self.next(cr, uid, ids, context=context)
183
184     def action_cancel(self, cr, uid, ids, context=None):
185         """ Action handler for the ``cancel`` event. That event isn't
186         generated by the res.config.view.base inheritable view, the
187         inherited view has to overload one of the buttons (or add one
188         more).
189
190         Sets the status of the todo the event was sent from to
191         ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
192         an action dictionary -- executes the action provided by calling
193         ``next``.
194         """
195         self._set_previous_todo(cr, uid, state='cancel', context=context)
196         next = self.cancel(cr, uid, ids, context=None)
197         if next: return next
198         return self.next(cr, uid, ids, context=context)
199
200 res_config_configurable()
201
202 class res_config_installer(osv.osv_memory):
203     """ New-style configuration base specialized for addons selection
204     and installation.
205
206     Basic usage
207     -----------
208
209     Subclasses can simply define a number of _columns as
210     fields.boolean objects. The keys (column names) should be the
211     names of the addons to install (when selected). Upon action
212     execution, selected boolean fields (and those only) will be
213     interpreted as addons to install, and batch-installed.
214
215     Additional addons
216     -----------------
217
218     It is also possible to require the installation of an additional
219     addon set when a specific preset of addons has been marked for
220     installation (in the basic usage only, additionals can't depend on
221     one another).
222
223     These additionals are defined through the ``_install_if``
224     property. This property is a mapping of a collection of addons (by
225     name) to a collection of addons (by name) [#]_, and if all the *key*
226     addons are selected for installation, then the *value* ones will
227     be selected as well. For example::
228
229         _install_if = {
230             ('sale','crm'): ['sale_crm'],
231         }
232
233     This will install the ``sale_crm`` addon if and only if both the
234     ``sale`` and ``crm`` addons are selected for installation.
235
236     You can define as many additionals as you wish, and additionals
237     can overlap in key and value. For instance::
238
239         _install_if = {
240             ('sale','crm'): ['sale_crm'],
241             ('sale','project'): ['project_mrp'],
242         }
243
244     will install both ``sale_crm`` and ``project_mrp`` if all of
245     ``sale``, ``crm`` and ``project`` are selected for installation.
246
247     Hook methods
248     ------------
249
250     Subclasses might also need to express dependencies more complex
251     than that provided by additionals. In this case, it's possible to
252     define methods of the form ``_if_%(name)s`` where ``name`` is the
253     name of a boolean field. If the field is selected, then the
254     corresponding module will be marked for installation *and* the
255     hook method will be executed.
256
257     Hook methods take the usual set of parameters (cr, uid, ids,
258     context) and can return a collection of additional addons to
259     install (if they return anything, otherwise they should not return
260     anything, though returning any "falsy" value such as None or an
261     empty collection will have the same effect).
262
263     Complete control
264     ----------------
265
266     The last hook is to simply overload the ``modules_to_install``
267     method, which implements all the mechanisms above. This method
268     takes the usual set of parameters (cr, uid, ids, context) and
269     returns a ``set`` of addons to install (addons selected by the
270     above methods minus addons from the *basic* set which are already
271     installed) [#]_ so an overloader can simply manipulate the ``set``
272     returned by ``res_config_installer.modules_to_install`` to add or
273     remove addons.
274
275     Skipping the installer
276     ----------------------
277
278     Unless it is removed from the view, installers have a *skip*
279     button which invokes ``action_skip`` (and the ``cancel`` hook from
280     ``res.config``). Hooks and additionals *are not run* when skipping
281     installation, even for already installed addons.
282
283     Again, setup your hooks accordingly.
284
285     .. [#] note that since a mapping key needs to be hashable, it's
286            possible to use a tuple or a frozenset, but not a list or a
287            regular set
288
289     .. [#] because the already-installed modules are only pruned at
290            the very end of ``modules_to_install``, additionals and
291            hooks depending on them *are guaranteed to execute*. Setup
292            your hooks accordingly.
293     """
294     _name = 'res.config.installer'
295     _inherit = 'res.config'
296     __logger = logging.getLogger(_name)
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 (either 'to install', 'to upgrade' or 'installed')
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','in',['to install', 'installed', 'to upgrade'])],
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, write_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, write_access)
377
378         for module in self._already_installed(cr, uid, context=context):
379             if module.name not in fields:
380                 continue
381             fields[module.name].update(
382                 readonly=True,
383                 help= ustr(fields[module.name].get('help', '')) +
384                      _('\n\nThis addon is already installed on your system'))
385         return fields
386
387     def execute(self, cr, uid, ids, context=None):
388         modules = self.pool.get('ir.module.module')
389         to_install = list(self.modules_to_install(
390             cr, uid, ids, context=context))
391         self.__logger.info('Selecting addons %s to install', to_install)
392         modules.state_update(
393             cr, uid,
394             modules.search(cr, uid, [('name','in',to_install)]),
395             'to install', ['uninstalled'], context=context)
396         cr.commit() #TOFIX: after remove this statement, installation wizard is fail
397         new_db, self.pool = pooler.restart_pool(cr.dbname, update_module=True)
398 res_config_installer()
399
400 DEPRECATION_MESSAGE = 'You are using an addon using old-style configuration '\
401     'wizards (ir.actions.configuration.wizard). Old-style configuration '\
402     'wizards have been deprecated.\n'\
403     'The addon should be migrated to res.config objects.'
404 class ir_actions_configuration_wizard(osv.osv_memory):
405     ''' Compatibility configuration wizard
406
407     The old configuration wizard has been replaced by res.config, but in order
408     not to break existing but not-yet-migrated addons, the old wizard was
409     reintegrated and gutted.
410     '''
411     _name='ir.actions.configuration.wizard'
412     _inherit = 'res.config'
413     __logger = logging.getLogger(_name)
414
415     def _next_action_note(self, cr, uid, ids, context=None):
416         next = self._next_action(cr, uid)
417         if next:
418             # if the next one is also an old-style extension, you never know...
419             if next.note:
420                 return next.note
421             return _("Click 'Continue' to configure the next addon...")
422         return _("Your database is now fully configured.\n\n"\
423             "Click 'Continue' and enjoy your OpenERP experience...")
424
425     _columns = {
426         'note': fields.text('Next Wizard', readonly=True),
427         }
428     _defaults = {
429         'note': _next_action_note,
430         }
431
432     def execute(self, cr, uid, ids, context=None):
433         self.__logger.warn(DEPRECATION_MESSAGE)
434
435 ir_actions_configuration_wizard()
436
437 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: