1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
25 from operator import attrgetter
27 from osv import osv, fields
29 from tools.translate import _
35 class res_config_configurable(osv.osv_memory):
36 ''' Base classes for new-style configuration items
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.
43 logger = netsvc.Logger()
45 def get_current_progress(self, cr, uid, context=None):
46 '''Return a description the current progress of configuration:
47 a tuple of (non_open_todos:int, total_todos: int)
49 return (self.pool.get('ir.actions.todo')\
50 .search_count(cr, uid, [('state','<>','open')], context),
51 self.pool.get('ir.actions.todo')\
52 .search_count(cr, uid, [], context))
54 def _progress(self, cr, uid, context=None):
55 closed, total = self.get_current_progress(cr, uid, context=context)
57 return round(closed*100./total)
60 def _get_image(self, cr, uid, context=None):
61 file_no = str(random.randint(1,3))
62 path = os.path.join('base','res','config_pixmaps/%s.png'%file_no)
63 file_data = tools.file_open(path,'rb').read()
64 return base64.encodestring(file_data)
67 progress = fields.float('Configuration Progress', readonly=True),
68 config_logo = fields.binary('Image', readonly=True),
72 config_logo = _get_image
75 def _next_action(self, cr, uid, context=None):
76 todos = self.pool.get('ir.actions.todo')
77 self.logger.notifyChannel('actions', netsvc.LOG_INFO,
78 'getting next %s' % todos)
79 active_todos = todos.search(cr, uid, [('state','=','open')],
83 todo_obj = todos.browse(cr, uid, active_todos[0], context=None)
84 todo_groups = map(lambda x:x.id, todo_obj.groups_id)
86 cr.execute("select 1 from res_groups_users_rel where uid=%s and gid IN %s",(uid, tuple(todo_groups),))
87 dont_skip_todo = bool(cr.fetchone())
89 return todos.browse(cr, uid, active_todos[0], context=None)
91 todos.write(cr, uid, active_todos[0], {'state':'skip'}, context=None)
92 return self._next_action(cr, uid)
95 def _set_previous_todo(self, cr, uid, state, context=None):
96 """ lookup the previous (which is still the next at this point)
97 ir.actions.todo, set it to whatever state was provided.
100 `LookupError`: if we couldn't find *any* previous todo
101 `ValueError`: if no state is provided
102 anything ir_actions_todo.write can throw
106 # this is ultra brittle, but apart from storing the todo id
107 # into the res.config view, I'm not sure how to get the
109 previous_todo = self._next_action(cr, uid, context=context)
110 if not previous_todo:
111 raise LookupError(_("Couldn't find previous ir.actions.todo"))
113 raise ValueError(_("Can't set an ir.actions.todo's state to "
115 previous_todo.write({'state':state})
117 def _next(self, cr, uid, context=None):
118 self.logger.notifyChannel('actions', netsvc.LOG_INFO,
119 'getting next operation')
120 next = self._next_action(cr, uid)
121 self.logger.notifyChannel('actions', netsvc.LOG_INFO,
122 'next action is %s' % next)
124 action = next.action_id
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,
131 'target': action.target,
133 self.logger.notifyChannel(
134 'actions', netsvc.LOG_INFO,
135 'all configuration actions have been executed')
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)
143 def start(self, cr, uid, ids, context=None):
144 ids2 = self.pool.get('ir.actions.todo').search(cr, uid, [], context=context)
145 for todo in self.pool.get('ir.actions.todo').browse(cr, uid, ids2, context=context):
146 if (todo.restart=='always') or (todo.restart=='onskip' and (todo.state in ('skip','cancel'))):
147 todo.write({'state':'open'})
148 return self.next(cr, uid, ids, context)
150 def next(self, cr, uid, ids, context=None):
151 """ Returns the next todo action to execute (using the default
154 return self._next(cr, uid, context=context)
156 def execute(self, cr, uid, ids, context=None):
157 """ Method called when the user clicks on the ``Next`` button.
159 Execute *must* be overloaded unless ``action_next`` is overloaded
160 (which is something you generally don't need to do).
162 If ``execute`` returns an action dictionary, that action is executed
163 rather than just going to the next configuration item.
165 raise NotImplementedError(
166 'Configuration items need to implement execute')
167 def cancel(self, cr, uid, ids, context=None):
168 """ Method called when the user click on the ``Skip`` button.
170 ``cancel`` should be overloaded instead of ``action_skip``. As with
171 ``execute``, if it returns an action dictionary that action is
172 executed in stead of the default (going to the next configuration item)
174 The default implementation is a NOOP.
176 ``cancel`` is also called by the default implementation of
181 def action_next(self, cr, uid, ids, context=None):
182 """ Action handler for the ``next`` event.
184 Sets the status of the todo the event was sent from to
185 ``done``, calls ``execute`` and -- unless ``execute`` returned
186 an action dictionary -- executes the action provided by calling
190 self._set_previous_todo(cr, uid, state='done', context=context)
192 raise osv.except_osv(_('Error'), e.message)
193 next = self.execute(cr, uid, ids, context=None)
195 return self.next(cr, uid, ids, context=context)
197 def action_skip(self, cr, uid, ids, context=None):
198 """ Action handler for the ``skip`` event.
200 Sets the status of the todo the event was sent from to
201 ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
202 an action dictionary -- executes the action provided by calling
206 self._set_previous_todo(cr, uid, state='skip', context=context)
208 raise osv.except_osv(_('Error'), e.message)
209 next = self.cancel(cr, uid, ids, context=None)
211 return self.next(cr, uid, ids, context=context)
213 def action_cancel(self, cr, uid, ids, context=None):
214 """ Action handler for the ``cancel`` event. That event isn't
215 generated by the res.config.view.base inheritable view, the
216 inherited view has to overload one of the buttons (or add one
219 Sets the status of the todo the event was sent from to
220 ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
221 an action dictionary -- executes the action provided by calling
225 self._set_previous_todo(cr, uid, state='cancel', context=context)
227 raise osv.except_osv(_('Error'), e.message)
228 next = self.cancel(cr, uid, ids, context=None)
230 return self.next(cr, uid, ids, context=context)
232 res_config_configurable()
234 class res_config_installer(osv.osv_memory):
235 """ New-style configuration base specialized for addons selection
241 Subclasses can simply define a number of _columns as
242 fields.boolean objects. The keys (column names) should be the
243 names of the addons to install (when selected). Upon action
244 execution, selected boolean fields (and those only) will be
245 interpreted as addons to install, and batch-installed.
250 It is also possible to require the installation of an additional
251 addon set when a specific preset of addons has been marked for
252 installation (in the basic usage only, additionals can't depend on
255 These additionals are defined through the ``_install_if``
256 property. This property is a mapping of a collection of addons (by
257 name) to a collection of addons (by name) [#]_, and if all the *key*
258 addons are selected for installation, then the *value* ones will
259 be selected as well. For example::
262 ('sale','crm'): ['sale_crm'],
265 This will install the ``sale_crm`` addon if and only if both the
266 ``sale`` and ``crm`` addons are selected for installation.
268 You can define as many additionals as you wish, and additionals
269 can overlap in key and value. For instance::
272 ('sale','crm'): ['sale_crm'],
273 ('sale','project'): ['project_mrp'],
276 will install both ``sale_crm`` and ``project_mrp`` if all of
277 ``sale``, ``crm`` and ``project`` are selected for installation.
282 Subclasses might also need to express dependencies more complex
283 than that provided by additionals. In this case, it's possible to
284 define methods of the form ``_if_%(name)s`` where ``name`` is the
285 name of a boolean field. If the field is selected, then the
286 corresponding module will be marked for installation *and* the
287 hook method will be executed.
289 Hook methods take the usual set of parameters (cr, uid, ids,
290 context) and can return a collection of additional addons to
291 install (if they return anything, otherwise they should not return
292 anything, though returning any "falsy" value such as None or an
293 empty collection will have the same effect).
298 The last hook is to simply overload the ``modules_to_install``
299 method, which implements all the mechanisms above. This method
300 takes the usual set of parameters (cr, uid, ids, context) and
301 returns a ``set`` of addons to install (addons selected by the
302 above methods minus addons from the *basic* set which are already
303 installed) [#]_ so an overloader can simply manipulate the ``set``
304 returned by ``res_config_installer.modules_to_install`` to add or
307 Skipping the installer
308 ----------------------
310 Unless it is removed from the view, installers have a *skip*
311 button which invokes ``action_skip`` (and the ``cancel`` hook from
312 ``res.config``). Hooks and additionals *are not run* when skipping
313 installation, even for already installed addons.
315 Again, setup your hooks accordinly.
317 .. [#] note that since a mapping key needs to be hashable, it's
318 possible to use a tuple or a frozenset, but not a list or a
321 .. [#] because the already-installed modules are only pruned at
322 the very end of ``modules_to_install``, additionals and
323 hooks depending on them *are guaranteed to execute*. Setup
324 your hooks accordingly.
326 _name = 'res.config.installer'
327 _inherit = 'res.config'
331 def _already_installed(self, cr, uid, context=None):
332 """ For each module (boolean fields in a res.config.installer),
333 check if it's already installed (either 'to install', 'to upgrade' or 'installed')
334 and if it is, check it by default
336 modules = self.pool.get('ir.module.module')
338 selectable = [field for field in self._columns
339 if type(self._columns[field]) is fields.boolean]
340 return modules.browse(
342 modules.search(cr, uid,
343 [('name','in',selectable),
344 ('state','in',['to install', 'installed', 'to upgrade'])],
349 def modules_to_install(self, cr, uid, ids, context=None):
350 """ selects all modules to install:
352 * checked boolean fields
353 * return values of hook methods. Hook methods are of the form
354 ``_if_%(addon_name)s``, and are called if the corresponding
355 addon is marked for installation. They take the arguments
356 cr, uid, ids and context, and return an iterable of addon
358 * additionals, additionals are setup through the ``_install_if``
359 class variable. ``_install_if`` is a dict of {iterable:iterable}
360 where key and value are iterables of addon names.
362 If all the addons in the key are selected for installation
363 (warning: addons added through hooks don't count), then the
364 addons in the value are added to the set of modules to install
365 * not already installed
367 base = set(module_name
368 for installer in self.read(cr, uid, ids, context=context)
369 for module_name, to_install in installer.iteritems()
370 if module_name != 'id'
371 if type(self._columns[module_name]) is fields.boolean
374 hooks_results = set()
376 hook = getattr(self, '_if_%s'%(module), None)
378 hooks_results.update(hook(cr, uid, ids, context=None) or set())
381 module for requirements, consequences \
382 in self._install_if.iteritems()
383 if base.issuperset(requirements)
384 for module in consequences)
386 return (base | hooks_results | additionals) - set(
387 map(attrgetter('name'), self._already_installed(cr, uid, context)))
389 def default_get(self, cr, uid, fields_list, context=None):
390 ''' If an addon is already installed, check it by default
392 defaults = super(res_config_installer, self).default_get(
393 cr, uid, fields_list, context=context)
395 return dict(defaults,
397 map(attrgetter('name'),
398 self._already_installed(cr, uid, context=context)),
401 def fields_get(self, cr, uid, fields=None, context=None, write_access=True):
402 """ If an addon is already installed, set it to readonly as
403 res.config.installer doesn't handle uninstallations of already
406 fields = super(res_config_installer, self).fields_get(
407 cr, uid, fields, context, write_access)
409 for module in self._already_installed(cr, uid, context=context):
410 if module.name not in fields:
412 fields[module.name].update(
414 help=fields[module.name].get('help', '') +
415 _('\n\nThis addon is already installed on your system'))
419 def execute(self, cr, uid, ids, context=None):
420 modules = self.pool.get('ir.module.module')
421 to_install = list(self.modules_to_install(
422 cr, uid, ids, context=context))
423 self.logger.notifyChannel(
424 'installer', netsvc.LOG_INFO,
425 'Selecting addons %s to install'%to_install)
426 modules.state_update(
428 modules.search(cr, uid, [('name','in',to_install)]),
429 'to install', ['uninstalled'], context=context)
430 cr.commit() #TOFIX: after remove this statement, installation wizard is fail
431 pooler.restart_pool(cr.dbname, update_module=True)
432 res_config_installer()
434 DEPRECATION_MESSAGE = 'You are using an addon using old-style configuration '\
435 'wizards (ir.actions.configuration.wizard). Old-style configuration '\
436 'wizards have been deprecated.\n'\
437 'The addon should be migrated to res.config objects.'
438 class ir_actions_configuration_wizard(osv.osv_memory):
439 ''' Compatibility configuration wizard
441 The old configuration wizard has been replaced by res.config, but in order
442 not to break existing but not-yet-migrated addons, the old wizard was
443 reintegrated and gutted.
445 _name='ir.actions.configuration.wizard'
446 _inherit = 'res.config'
448 def _next_action_note(self, cr, uid, ids, context=None):
449 next = self._next_action(cr, uid)
451 # if the next one is also an old-style extension, you never know...
454 return "Click 'Continue' to configure the next addon..."
455 return "Your database is now fully configured.\n\n"\
456 "Click 'Continue' and enjoy your OpenERP experience..."
459 'note': fields.text('Next Wizard', readonly=True),
462 'note': _next_action_note,
465 def execute(self, cr, uid, ids, context=None):
466 self.logger.notifyChannel(
467 'configuration', netsvc.LOG_WARNING, DEPRECATION_MESSAGE)
469 ir_actions_configuration_wizard()
471 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: