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 ##############################################################################
22 from operator import attrgetter
24 from osv import osv, fields
25 from tools.translate import _
30 class res_config_configurable(osv.osv_memory):
31 ''' Base classes for new-style configuration items
33 Configuration items should inherit from this class, implement
34 the execute method (and optionally the cancel one) and have
35 their view inherit from the related res_config_view_base view.
38 _inherit = 'ir.wizard.screen'
39 logger = netsvc.Logger()
40 __logger = logging.getLogger(_name)
42 def get_current_progress(self, cr, uid, context=None):
43 '''Return a description the current progress of configuration:
44 a tuple of (non_open_todos:int, total_todos: int)
46 return (self.pool.get('ir.actions.todo')\
47 .search_count(cr, uid, [('state','<>','open')], context),
48 self.pool.get('ir.actions.todo')\
49 .search_count(cr, uid, [], context))
51 def _progress(self, cr, uid, context=None):
52 closed, total = self.get_current_progress(cr, uid, context=context)
54 return round(closed*100./total)
58 progress = fields.float('Configuration Progress', readonly=True),
65 def _next_action(self, cr, uid, context=None):
66 todos = self.pool.get('ir.actions.todo')
67 self.__logger.info('getting next %s', todos)
68 active_todos = todos.search(cr, uid, [('state','=','open')],
71 todo_obj = todos.browse(cr, uid, active_todos[0], context=None)
72 todo_groups = map(lambda x:x.id, todo_obj.groups_id)
75 cr.execute("select 1 from res_groups_users_rel where uid=%s and gid IN %s",(uid, tuple(todo_groups),))
76 dont_skip_todo = bool(cr.fetchone())
78 return todos.browse(cr, uid, active_todos[0], context=None)
80 todos.write(cr, uid, active_todos[0], {'state':'skip'}, context=None)
81 return self._next_action(cr, uid)
84 def _set_previous_todo(self, cr, uid, state, context=None):
85 """ lookup the previous (which is still the next at this point)
86 ir.actions.todo, set it to whatever state was provided.
89 `LookupError`: if we couldn't find *any* previous todo
90 `ValueError`: if no state is provided
91 anything ir_actions_todo.write can throw
93 # this is ultra brittle, but apart from storing the todo id
94 # into the res.config view, I'm not sure how to get the
96 previous_todo = self._next_action(cr, uid, context=context)
98 raise LookupError(_("Couldn't find previous ir.actions.todo"))
100 raise ValueError(_("Can't set an ir.actions.todo's state to an empty value."))
101 previous_todo.write({'state':state})
103 def _next(self, cr, uid, context=None):
104 self.__logger.info('getting next operation')
105 next = self._next_action(cr, uid)
106 self.__logger.info('next action is %s', next)
108 action = next.action_id
110 'view_mode': action.view_mode,
111 'view_type': action.view_type,
112 'view_id': action.view_id and [action.view_id.id] or False,
113 'res_model': action.res_model,
115 'target': action.target,
117 self.__logger.info('all configuration actions have been executed')
119 current_user_menu = self.pool.get('res.users')\
120 .browse(cr, uid, uid).menu_id
121 # return the action associated with the menu
122 return self.pool.get(current_user_menu.type)\
123 .read(cr, uid, current_user_menu.id)
125 def start(self, cr, uid, ids, context=None):
126 ids2 = self.pool.get('ir.actions.todo').search(cr, uid, [], context=context)
127 for todo in self.pool.get('ir.actions.todo').browse(cr, uid, ids2, context=context):
128 if (todo.restart=='always') or (todo.restart=='onskip' and (todo.state in ('skip','cancel'))):
129 todo.write({'state':'open'})
130 return self.next(cr, uid, ids, context)
132 def next(self, cr, uid, ids, context=None):
133 """ Returns the next todo action to execute (using the default
136 return self._next(cr, uid, context=context)
138 def execute(self, cr, uid, ids, context=None):
139 """ Method called when the user clicks on the ``Next`` button.
141 Execute *must* be overloaded unless ``action_next`` is overloaded
142 (which is something you generally don't need to do).
144 If ``execute`` returns an action dictionary, that action is executed
145 rather than just going to the next configuration item.
147 raise NotImplementedError(
148 'Configuration items need to implement execute')
149 def cancel(self, cr, uid, ids, context=None):
150 """ Method called when the user click on the ``Skip`` button.
152 ``cancel`` should be overloaded instead of ``action_skip``. As with
153 ``execute``, if it returns an action dictionary that action is
154 executed in stead of the default (going to the next configuration item)
156 The default implementation is a NOOP.
158 ``cancel`` is also called by the default implementation of
163 def action_next(self, cr, uid, ids, context=None):
164 """ Action handler for the ``next`` event.
166 Sets the status of the todo the event was sent from to
167 ``done``, calls ``execute`` and -- unless ``execute`` returned
168 an action dictionary -- executes the action provided by calling
172 self._set_previous_todo(cr, uid, state='done', context=context)
174 raise osv.except_osv(_('Error'), e.message)
175 next = self.execute(cr, uid, ids, context=None)
177 return self.next(cr, uid, ids, context=context)
179 def action_skip(self, cr, uid, ids, context=None):
180 """ Action handler for the ``skip`` event.
182 Sets the status of the todo the event was sent from to
183 ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
184 an action dictionary -- executes the action provided by calling
188 self._set_previous_todo(cr, uid, state='skip', context=context)
190 raise osv.except_osv(_('Error'), e.message)
191 next = self.cancel(cr, uid, ids, context=None)
193 return self.next(cr, uid, ids, context=context)
195 def action_cancel(self, cr, uid, ids, context=None):
196 """ Action handler for the ``cancel`` event. That event isn't
197 generated by the res.config.view.base inheritable view, the
198 inherited view has to overload one of the buttons (or add one
201 Sets the status of the todo the event was sent from to
202 ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
203 an action dictionary -- executes the action provided by calling
207 self._set_previous_todo(cr, uid, state='cancel', context=context)
209 raise osv.except_osv(_('Error'), e.message)
210 next = self.cancel(cr, uid, ids, context=None)
212 return self.next(cr, uid, ids, context=context)
214 res_config_configurable()
216 class res_config_installer(osv.osv_memory):
217 """ New-style configuration base specialized for addons selection
223 Subclasses can simply define a number of _columns as
224 fields.boolean objects. The keys (column names) should be the
225 names of the addons to install (when selected). Upon action
226 execution, selected boolean fields (and those only) will be
227 interpreted as addons to install, and batch-installed.
232 It is also possible to require the installation of an additional
233 addon set when a specific preset of addons has been marked for
234 installation (in the basic usage only, additionals can't depend on
237 These additionals are defined through the ``_install_if``
238 property. This property is a mapping of a collection of addons (by
239 name) to a collection of addons (by name) [#]_, and if all the *key*
240 addons are selected for installation, then the *value* ones will
241 be selected as well. For example::
244 ('sale','crm'): ['sale_crm'],
247 This will install the ``sale_crm`` addon if and only if both the
248 ``sale`` and ``crm`` addons are selected for installation.
250 You can define as many additionals as you wish, and additionals
251 can overlap in key and value. For instance::
254 ('sale','crm'): ['sale_crm'],
255 ('sale','project'): ['project_mrp'],
258 will install both ``sale_crm`` and ``project_mrp`` if all of
259 ``sale``, ``crm`` and ``project`` are selected for installation.
264 Subclasses might also need to express dependencies more complex
265 than that provided by additionals. In this case, it's possible to
266 define methods of the form ``_if_%(name)s`` where ``name`` is the
267 name of a boolean field. If the field is selected, then the
268 corresponding module will be marked for installation *and* the
269 hook method will be executed.
271 Hook methods take the usual set of parameters (cr, uid, ids,
272 context) and can return a collection of additional addons to
273 install (if they return anything, otherwise they should not return
274 anything, though returning any "falsy" value such as None or an
275 empty collection will have the same effect).
280 The last hook is to simply overload the ``modules_to_install``
281 method, which implements all the mechanisms above. This method
282 takes the usual set of parameters (cr, uid, ids, context) and
283 returns a ``set`` of addons to install (addons selected by the
284 above methods minus addons from the *basic* set which are already
285 installed) [#]_ so an overloader can simply manipulate the ``set``
286 returned by ``res_config_installer.modules_to_install`` to add or
289 Skipping the installer
290 ----------------------
292 Unless it is removed from the view, installers have a *skip*
293 button which invokes ``action_skip`` (and the ``cancel`` hook from
294 ``res.config``). Hooks and additionals *are not run* when skipping
295 installation, even for already installed addons.
297 Again, setup your hooks accordinly.
299 .. [#] note that since a mapping key needs to be hashable, it's
300 possible to use a tuple or a frozenset, but not a list or a
303 .. [#] because the already-installed modules are only pruned at
304 the very end of ``modules_to_install``, additionals and
305 hooks depending on them *are guaranteed to execute*. Setup
306 your hooks accordingly.
308 _name = 'res.config.installer'
309 _inherit = 'res.config'
310 __logger = logging.getLogger(_name)
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 (either 'to install', 'to upgrade' or 'installed')
317 and if it is, check it by default
319 modules = self.pool.get('ir.module.module')
321 selectable = [field for field in self._columns
322 if type(self._columns[field]) is fields.boolean]
323 return modules.browse(
325 modules.search(cr, uid,
326 [('name','in',selectable),
327 ('state','in',['to install', 'installed', 'to upgrade'])],
332 def modules_to_install(self, cr, uid, ids, context=None):
333 """ selects all modules to install:
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
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.
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
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
357 hooks_results = set()
359 hook = getattr(self, '_if_%s'%(module), None)
361 hooks_results.update(hook(cr, uid, ids, context=None) or set())
364 module for requirements, consequences \
365 in self._install_if.iteritems()
366 if base.issuperset(requirements)
367 for module in consequences)
369 return (base | hooks_results | additionals) - set(
370 map(attrgetter('name'), self._already_installed(cr, uid, context)))
372 def default_get(self, cr, uid, fields_list, context=None):
373 ''' If an addon is already installed, check it by default
375 defaults = super(res_config_installer, self).default_get(
376 cr, uid, fields_list, context=context)
378 return dict(defaults,
380 map(attrgetter('name'),
381 self._already_installed(cr, uid, context=context)),
384 def fields_get(self, cr, uid, fields=None, context=None, write_access=True):
385 """ If an addon is already installed, set it to readonly as
386 res.config.installer doesn't handle uninstallations of already
389 fields = super(res_config_installer, self).fields_get(
390 cr, uid, fields, context, write_access)
392 for module in self._already_installed(cr, uid, context=context):
393 if module.name not in fields:
395 fields[module.name].update(
397 help=fields[module.name].get('help', '') +
398 _('\n\nThis addon is already installed on your system'))
402 def execute(self, cr, uid, ids, context=None):
403 modules = self.pool.get('ir.module.module')
404 to_install = list(self.modules_to_install(
405 cr, uid, ids, context=context))
406 self.__logger.info('Selecting addons %s to install', to_install)
407 modules.state_update(
409 modules.search(cr, uid, [('name','in',to_install)]),
410 'to install', ['uninstalled'], context=context)
411 cr.commit() #TOFIX: after remove this statement, installation wizard is fail
412 pooler.restart_pool(cr.dbname, update_module=True)
413 res_config_installer()
415 DEPRECATION_MESSAGE = 'You are using an addon using old-style configuration '\
416 'wizards (ir.actions.configuration.wizard). Old-style configuration '\
417 'wizards have been deprecated.\n'\
418 'The addon should be migrated to res.config objects.'
419 class ir_actions_configuration_wizard(osv.osv_memory):
420 ''' Compatibility configuration wizard
422 The old configuration wizard has been replaced by res.config, but in order
423 not to break existing but not-yet-migrated addons, the old wizard was
424 reintegrated and gutted.
426 _name='ir.actions.configuration.wizard'
427 _inherit = 'res.config'
428 __logger = logging.getLogger(_name)
430 def _next_action_note(self, cr, uid, ids, context=None):
431 next = self._next_action(cr, uid)
433 # if the next one is also an old-style extension, you never know...
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..."
441 'note': fields.text('Next Wizard', readonly=True),
444 'note': _next_action_note,
447 def execute(self, cr, uid, ids, context=None):
448 self.__logger.warn(DEPRECATION_MESSAGE)
450 ir_actions_configuration_wizard()
452 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: