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