merge
[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 IN %s",(uid, tuple(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             print {
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             return {
126                 'view_mode': action.view_mode,
127                 'view_type': action.view_type,
128                 'view_id': action.view_id and [action.view_id.id] or False,
129                 'res_model': action.res_model,
130                 'type': action.type,
131                 'target': action.target,
132             }
133         self.logger.notifyChannel(
134             'actions', netsvc.LOG_INFO,
135             'all configuration actions have been executed')
136
137         current_user_menu = self.pool.get('res.users')\
138             .browse(cr, uid, uid).menu_id
139         # return the action associated with the menu
140         return self.pool.get(current_user_menu.type)\
141             .read(cr, uid, current_user_menu.id)
142
143     def start(self, cr, uid, ids, context=None):
144         print 'Start'
145         ids2 = self.pool.get('ir.actions.todo').search(cr, uid, [], context=context)
146         for todo in self.pool.get('ir.actions.todo').browse(cr, uid, ids2, context=context):
147             if (todo.restart=='always') or (todo.restart=='onskip' and (todo.state in ('skip','cancel'))):
148                 todo.write({'state':'open'})
149         return self.next(cr, uid, ids, context)
150
151     def next(self, cr, uid, ids, context=None):
152         """ Returns the next todo action to execute (using the default
153         sort order)
154         """
155         return self._next(cr, uid)
156
157     def execute(self, cr, uid, ids, context=None):
158         """ Method called when the user clicks on the ``Next`` button.
159
160         Execute *must* be overloaded unless ``action_next`` is overloaded
161         (which is something you generally don't need to do).
162
163         If ``execute`` returns an action dictionary, that action is executed
164         rather than just going to the next configuration item.
165         """
166         raise NotImplementedError(
167             'Configuration items need to implement execute')
168     def cancel(self, cr, uid, ids, context=None):
169         """ Method called when the user click on the ``Skip`` button.
170
171         ``cancel`` should be overloaded instead of ``action_skip``. As with
172         ``execute``, if it returns an action dictionary that action is
173         executed in stead of the default (going to the next configuration item)
174
175         The default implementation is a NOOP.
176
177         ``cancel`` is also called by the default implementation of
178         ``action_cancel``.
179         """
180         pass
181
182     def action_next(self, cr, uid, ids, context=None):
183         """ Action handler for the ``next`` event.
184
185         Sets the status of the todo the event was sent from to
186         ``done``, calls ``execute`` and -- unless ``execute`` returned
187         an action dictionary -- executes the action provided by calling
188         ``next``.
189         """
190         self._set_previous_todo(cr, uid, state='done')
191         next = self.execute(cr, uid, ids, context=None)
192         if next: return next
193         return self.next(cr, uid, ids, context=context)
194
195     def action_skip(self, cr, uid, ids, context=None):
196         """ Action handler for the ``skip`` event.
197
198         Sets the status of the todo the event was sent from to
199         ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
200         an action dictionary -- executes the action provided by calling
201         ``next``.
202         """
203         self._set_previous_todo(cr, uid, state='skip')
204         next = self.cancel(cr, uid, ids, context=None)
205         if next: return next
206         return self.next(cr, uid, ids, context=context)
207
208     def action_cancel(self, cr, uid, ids, context=None):
209         """ Action handler for the ``cancel`` event. That event isn't
210         generated by the res.config.view.base inheritable view, the
211         inherited view has to overload one of the buttons (or add one
212         more).
213
214         Sets the status of the todo the event was sent from to
215         ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
216         an action dictionary -- executes the action provided by calling
217         ``next``.
218         """
219         self._set_previous_todo(cr, uid, state='cancel')
220         next = self.cancel(cr, uid, ids, context=None)
221         if next: return next
222         return self.next(cr, uid, ids, context=context)
223
224 res_config_configurable()
225
226 class res_config_installer(osv.osv_memory):
227     """ New-style configuration base specialized for addons selection
228     and installation.
229
230     Basic usage
231     -----------
232
233     Subclasses can simply define a number of _columns as
234     fields.boolean objects. The keys (column names) should be the
235     names of the addons to install (when selected). Upon action
236     execution, selected boolean fields (and those only) will be
237     interpreted as addons to install, and batch-installed.
238
239     Additional addons
240     -----------------
241
242     It is also possible to require the installation of an additional
243     addon set when a specific preset of addons has been marked for
244     installation (in the basic usage only, additionals can't depend on
245     one another).
246
247     These additionals are defined through the ``_install_if``
248     property. This property is a mapping of a collection of addons (by
249     name) to a collection of addons (by name) [#]_, and if all the *key*
250     addons are selected for installation, then the *value* ones will
251     be selected as well. For example::
252
253         _install_if = {
254             ('sale','crm'): ['sale_crm'],
255         }
256
257     This will install the ``sale_crm`` addon if and only if both the
258     ``sale`` and ``crm`` addons are selected for installation.
259
260     You can define as many additionals as you wish, and additionals
261     can overlap in key and value. For instance::
262
263         _install_if = {
264             ('sale','crm'): ['sale_crm'],
265             ('sale','project'): ['project_mrp'],
266         }
267
268     will install both ``sale_crm`` and ``project_mrp`` if all of
269     ``sale``, ``crm`` and ``project`` are selected for installation.
270
271     Hook methods
272     ------------
273
274     Subclasses might also need to express dependencies more complex
275     than that provided by additionals. In this case, it's possible to
276     define methods of the form ``_if_%(name)s`` where ``name`` is the
277     name of a boolean field. If the field is selected, then the
278     corresponding module will be marked for installation *and* the
279     hook method will be executed.
280
281     Hook methods take the usual set of parameters (cr, uid, ids,
282     context) and can return a collection of additional addons to
283     install (if they return anything, otherwise they should not return
284     anything, though returning any "falsy" value such as None or an
285     empty collection will have the same effect).
286
287     Complete control
288     ----------------
289
290     The last hook is to simply overload the ``modules_to_install``
291     method, which implements all the mechanisms above. This method
292     takes the usual set of parameters (cr, uid, ids, context) and
293     returns a ``set`` of addons to install (addons selected by the
294     above methods minus addons from the *basic* set which are already
295     installed) [#]_ so an overloader can simply manipulate the ``set``
296     returned by ``res_config_installer.modules_to_install`` to add or
297     remove addons.
298
299     Skipping the installer
300     ----------------------
301
302     Unless it is removed from the view, installers have a *skip*
303     button which invokes ``action_skip`` (and the ``cancel`` hook from
304     ``res.config``). Hooks and additionals *are not run* when skipping
305     installation, even for already installed addons.
306
307     Again, setup your hooks accordinly.
308
309     .. [#] note that since a mapping key needs to be hashable, it's
310            possible to use a tuple or a frozenset, but not a list or a
311            regular set
312
313     .. [#] because the already-installed modules are only pruned at
314            the very end of ``modules_to_install``, additionals and
315            hooks depending on them *are guaranteed to execute*. Setup
316            your hooks accordingly.
317     """
318     _name = 'res.config.installer'
319     _inherit = 'res.config'
320
321     _install_if = {}
322
323     def _already_installed(self, cr, uid, context=None):
324         """ For each module (boolean fields in a res.config.installer),
325         check if it's already installed (neither uninstallable nor uninstalled)
326         and if it is, check it by default
327         """
328         modules = self.pool.get('ir.module.module')
329
330         selectable = [field for field in self._columns
331                       if type(self._columns[field]) is fields.boolean]
332         return modules.browse(
333             cr, uid,
334             modules.search(cr, uid,
335                            [('name','in',selectable),
336                             ('state','not in',['uninstallable', 'uninstalled'])],
337                            context=context),
338             context=context)
339
340
341     def modules_to_install(self, cr, uid, ids, context=None):
342         """ selects all modules to install:
343
344         * checked boolean fields
345         * return values of hook methods. Hook methods are of the form
346           ``_if_%(addon_name)s``, and are called if the corresponding
347           addon is marked for installation. They take the arguments
348           cr, uid, ids and context, and return an iterable of addon
349           names
350         * additionals, additionals are setup through the ``_install_if``
351           class variable. ``_install_if`` is a dict of {iterable:iterable}
352           where key and value are iterables of addon names.
353
354           If all the addons in the key are selected for installation
355           (warning: addons added through hooks don't count), then the
356           addons in the value are added to the set of modules to install
357         * not already installed
358         """
359         base = set(module_name
360                    for installer in self.read(cr, uid, ids, context=context)
361                    for module_name, to_install in installer.iteritems()
362                    if module_name != 'id'
363                    if type(self._columns[module_name]) is fields.boolean
364                    if to_install)
365
366         hooks_results = set()
367         for module in base:
368             hook = getattr(self, '_if_%s'%(module), None)
369             if hook:
370                 hooks_results.update(hook(cr, uid, ids, context=None) or set())
371
372         additionals = set(
373             module for requirements, consequences \
374                        in self._install_if.iteritems()
375                    if base.issuperset(requirements)
376                    for module in consequences)
377
378         return (base | hooks_results | additionals) - set(
379             map(attrgetter('name'), self._already_installed(cr, uid, context)))
380
381     def default_get(self, cr, uid, fields_list, context=None):
382         ''' If an addon is already installed, check it by default
383         '''
384         defaults = super(res_config_installer, self).default_get(
385             cr, uid, fields_list, context=context)
386
387         return dict(defaults,
388                     **dict.fromkeys(
389                         map(attrgetter('name'),
390                             self._already_installed(cr, uid, context=context)),
391                         True))
392
393     def fields_get(self, cr, uid, fields=None, context=None, read_access=True):
394         """ If an addon is already installed, set it to readonly as
395         res.config.installer doesn't handle uninstallations of already
396         installed addons
397         """
398         fields = super(res_config_installer, self).fields_get(
399             cr, uid, fields, context, read_access)
400
401         for module in self._already_installed(cr, uid, context=context):
402             if module.name not in fields:
403                 continue
404             fields[module.name].update(
405                 readonly=True,
406                 help=fields[module.name].get('help', '') +
407                      _('\n\nThis addon is already installed on your system'))
408
409         return fields
410
411     def execute(self, cr, uid, ids, context=None):
412         modules = self.pool.get('ir.module.module')
413         to_install = list(self.modules_to_install(
414             cr, uid, ids, context=context))
415         self.logger.notifyChannel(
416             'installer', netsvc.LOG_INFO,
417             'Selecting addons %s to install'%to_install)
418         modules.state_update(
419             cr, uid,
420             modules.search(cr, uid, [('name','in',to_install)]),
421             'to install', ['uninstalled'], context=context)
422         cr.commit()
423
424         pooler.restart_pool(cr.dbname, update_module=True)
425 res_config_installer()
426
427 DEPRECATION_MESSAGE = 'You are using an addon using old-style configuration '\
428     'wizards (ir.actions.configuration.wizard). Old-style configuration '\
429     'wizards have been deprecated.\n'\
430     'The addon should be migrated to res.config objects.'
431 class ir_actions_configuration_wizard(osv.osv_memory):
432     ''' Compatibility configuration wizard
433
434     The old configuration wizard has been replaced by res.config, but in order
435     not to break existing but not-yet-migrated addons, the old wizard was
436     reintegrated and gutted.
437     '''
438     _name='ir.actions.configuration.wizard'
439     _inherit = 'res.config'
440
441     def _next_action_note(self, cr, uid, ids, context=None):
442         next = self._next_action(cr, uid)
443         if next:
444             # if the next one is also an old-style extension, you never know...
445             if next.note:
446                 return next.note
447             return "Click 'Continue' to configure the next addon..."
448         return "Your database is now fully configured.\n\n"\
449             "Click 'Continue' and enjoy your OpenERP experience..."
450
451     _columns = {
452         'note': fields.text('Next Wizard', readonly=True),
453         }
454     _defaults = {
455         'note': _next_action_note,
456         }
457
458     def execute(self, cr, uid, ids, context=None):
459         self.logger.notifyChannel(
460             'configuration', netsvc.LOG_WARNING, DEPRECATION_MESSAGE)
461
462 ir_actions_configuration_wizard()
463
464 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: