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 _
27 from tools import ustr
31 class res_config_configurable(osv.osv_memory):
32 ''' Base classes for new-style configuration items
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.
39 _inherit = 'ir.wizard.screen'
40 logger = netsvc.Logger()
41 __logger = logging.getLogger(_name)
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)
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))
52 def _progress(self, cr, uid, context=None):
53 closed, total = self.get_current_progress(cr, uid, context=context)
55 return round(closed*100./total)
59 progress = fields.float('Configuration Progress', readonly=True),
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')],
72 todo_obj = todos.browse(cr, uid, active_todos[0], context=None)
73 todo_groups = map(lambda x:x.id, todo_obj.groups_id)
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())
79 return todos.browse(cr, uid, active_todos[0], context=None)
81 todos.write(cr, uid, active_todos[0], {'state':'skip'}, context=None)
82 return self._next_action(cr, uid)
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.
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
92 previous_todo = self._next_action(cr, uid, context=context)
94 self.__logger.warn(_("Couldn't find previous ir.actions.todo"))
96 previous_todo.write({'state':state})
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)
103 action = next.action_id
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,
110 'target': action.target,
112 self.__logger.info('all configuration actions have been executed')
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)
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'):
124 todo.write({'state':'open'})
125 return self.next(cr, uid, ids, context)
127 def next(self, cr, uid, ids, context=None):
128 """ Returns the next todo action to execute (using the default
131 return self._next(cr, uid, context=context)
133 def execute(self, cr, uid, ids, context=None):
134 """ Method called when the user clicks on the ``Next`` button.
136 Execute *must* be overloaded unless ``action_next`` is overloaded
137 (which is something you generally don't need to do).
139 If ``execute`` returns an action dictionary, that action is executed
140 rather than just going to the next configuration item.
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.
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)
151 The default implementation is a NOOP.
153 ``cancel`` is also called by the default implementation of
158 def action_next(self, cr, uid, ids, context=None):
159 """ Action handler for the ``next`` event.
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
166 self._set_previous_todo(cr, uid, state='done', context=context)
167 next = self.execute(cr, uid, ids, context=None)
169 return self.next(cr, uid, ids, context=context)
171 def action_skip(self, cr, uid, ids, context=None):
172 """ Action handler for the ``skip`` event.
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
179 self._set_previous_todo(cr, uid, state='skip', context=context)
180 next = self.cancel(cr, uid, ids, context=None)
182 return self.next(cr, uid, ids, context=context)
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
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
195 self._set_previous_todo(cr, uid, state='cancel', context=context)
196 next = self.cancel(cr, uid, ids, context=None)
198 return self.next(cr, uid, ids, context=context)
200 res_config_configurable()
202 class res_config_installer(osv.osv_memory):
203 """ New-style configuration base specialized for addons selection
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.
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
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::
230 ('sale','crm'): ['sale_crm'],
233 This will install the ``sale_crm`` addon if and only if both the
234 ``sale`` and ``crm`` addons are selected for installation.
236 You can define as many additionals as you wish, and additionals
237 can overlap in key and value. For instance::
240 ('sale','crm'): ['sale_crm'],
241 ('sale','project'): ['project_mrp'],
244 will install both ``sale_crm`` and ``project_mrp`` if all of
245 ``sale``, ``crm`` and ``project`` are selected for installation.
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.
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).
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
275 Skipping the installer
276 ----------------------
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.
283 Again, setup your hooks accordingly.
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
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.
294 _name = 'res.config.installer'
295 _inherit = 'res.config'
296 __logger = logging.getLogger(_name)
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
305 modules = self.pool.get('ir.module.module')
307 selectable = [field for field in self._columns
308 if type(self._columns[field]) is fields.boolean]
309 return modules.browse(
311 modules.search(cr, uid,
312 [('name','in',selectable),
313 ('state','in',['to install', 'installed', 'to upgrade'])],
318 def modules_to_install(self, cr, uid, ids, context=None):
319 """ selects all modules to install:
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
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.
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
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
343 hooks_results = set()
345 hook = getattr(self, '_if_%s'%(module), None)
347 hooks_results.update(hook(cr, uid, ids, context=None) or set())
350 module for requirements, consequences \
351 in self._install_if.iteritems()
352 if base.issuperset(requirements)
353 for module in consequences)
355 return (base | hooks_results | additionals) - set(
356 map(attrgetter('name'), self._already_installed(cr, uid, context)))
358 def default_get(self, cr, uid, fields_list, context=None):
359 ''' If an addon is already installed, check it by default
361 defaults = super(res_config_installer, self).default_get(
362 cr, uid, fields_list, context=context)
364 return dict(defaults,
366 map(attrgetter('name'),
367 self._already_installed(cr, uid, context=context)),
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
375 fields = super(res_config_installer, self).fields_get(
376 cr, uid, fields, context, write_access)
378 for module in self._already_installed(cr, uid, context=context):
379 if module.name not in fields:
381 fields[module.name].update(
383 help= ustr(fields[module.name].get('help', '')) +
384 _('\n\nThis addon is already installed on your system'))
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(
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()
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
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.
411 _name='ir.actions.configuration.wizard'
412 _inherit = 'res.config'
413 __logger = logging.getLogger(_name)
415 def _next_action_note(self, cr, uid, ids, context=None):
416 next = self._next_action(cr, uid)
418 # if the next one is also an old-style extension, you never know...
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...")
426 'note': fields.text('Next Wizard', readonly=True),
429 'note': _next_action_note,
432 def execute(self, cr, uid, ids, context=None):
433 self.__logger.warn(DEPRECATION_MESSAGE)
435 ir_actions_configuration_wizard()
437 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: