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 # Don't forget to change the domain in search view, if this condition is changed
70 active_todos = todos.search(cr, uid, [('state','=','open')],
73 todo_obj = todos.browse(cr, uid, active_todos[0], context=None)
74 todo_groups = map(lambda x:x.id, todo_obj.groups_id)
77 cr.execute("select 1 from res_groups_users_rel where uid=%s and gid IN %s",(uid, tuple(todo_groups),))
78 dont_skip_todo = bool(cr.fetchone())
80 res = todos.browse(cr, uid, active_todos[0], context=None)
81 # Wizards that directly opens a form stays in Todo state even if its called,
82 # as next_action is not called, so, setting state as done 'manually'
83 if res.action_id.target == 'current':
84 res.write({'state': 'done'})
87 todos.write(cr, uid, active_todos[0], {'state':'skip'}, context=None)
88 return self._next_action(cr, uid)
91 def _set_previous_todo(self, cr, uid, state, context=None):
92 """ lookup the previous (which is still the next at this point)
93 ir.actions.todo, set it to whatever state was provided.
95 # this is ultra brittle, but apart from storing the todo id
96 # into the res.config view, I'm not sure how to get the
98 previous_todo = self._next_action(cr, uid, context=context)
100 self.__logger.warn(_("Couldn't find previous ir.actions.todo"))
102 previous_todo.write({'state':state})
104 def _next(self, cr, uid, context=None):
105 self.__logger.info('getting next operation')
106 next = self._next_action(cr, uid)
107 self.__logger.info('next action is %s', next)
109 res = next.action_launch(context=context)
110 res.update({'nodestroy': False})
112 self.__logger.info('all configuration actions have been executed')
114 current_user_menu = self.pool.get('res.users').browse(cr, uid, uid).menu_id
115 # return the action associated with the menu
116 return self.pool.get(current_user_menu.type).read(cr, uid, current_user_menu.id)
118 def start(self, cr, uid, ids, context=None):
119 ids2 = self.pool.get('ir.actions.todo').search(cr, uid, [], context=context)
120 for todo in self.pool.get('ir.actions.todo').browse(cr, uid, ids2, context=context):
121 if (todo.type=='normal_recurring'):
122 todo.write({'state':'open'})
123 return self.next(cr, uid, ids, context)
125 def next(self, cr, uid, ids, context=None):
126 """ Returns the next todo action to execute (using the default
129 return self._next(cr, uid, context=context)
131 def execute(self, cr, uid, ids, context=None):
132 """ Method called when the user clicks on the ``Next`` button.
134 Execute *must* be overloaded unless ``action_next`` is overloaded
135 (which is something you generally don't need to do).
137 If ``execute`` returns an action dictionary, that action is executed
138 rather than just going to the next configuration item.
140 raise NotImplementedError(
141 'Configuration items need to implement execute')
142 def cancel(self, cr, uid, ids, context=None):
143 """ Method called when the user click on the ``Skip`` button.
145 ``cancel`` should be overloaded instead of ``action_skip``. As with
146 ``execute``, if it returns an action dictionary that action is
147 executed in stead of the default (going to the next configuration item)
149 The default implementation is a NOOP.
151 ``cancel`` is also called by the default implementation of
156 def action_next(self, cr, uid, ids, context=None):
157 """ Action handler for the ``next`` event.
159 Sets the status of the todo the event was sent from to
160 ``done``, calls ``execute`` and -- unless ``execute`` returned
161 an action dictionary -- executes the action provided by calling
164 self._set_previous_todo(cr, uid, state='done', context=context)
165 next = self.execute(cr, uid, ids, context=None)
167 return self.next(cr, uid, ids, context=context)
169 def action_skip(self, cr, uid, ids, context=None):
170 """ Action handler for the ``skip`` event.
172 Sets the status of the todo the event was sent from to
173 ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
174 an action dictionary -- executes the action provided by calling
177 self._set_previous_todo(cr, uid, state='skip', context=context)
178 next = self.cancel(cr, uid, ids, context=None)
180 return self.next(cr, uid, ids, context=context)
182 def action_cancel(self, cr, uid, ids, context=None):
183 """ Action handler for the ``cancel`` event. That event isn't
184 generated by the res.config.view.base inheritable view, the
185 inherited view has to overload one of the buttons (or add one
188 Sets the status of the todo the event was sent from to
189 ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
190 an action dictionary -- executes the action provided by calling
193 self._set_previous_todo(cr, uid, state='cancel', context=context)
194 next = self.cancel(cr, uid, ids, context=None)
196 return self.next(cr, uid, ids, context=context)
198 res_config_configurable()
200 class res_config_installer(osv.osv_memory):
201 """ New-style configuration base specialized for addons selection
207 Subclasses can simply define a number of _columns as
208 fields.boolean objects. The keys (column names) should be the
209 names of the addons to install (when selected). Upon action
210 execution, selected boolean fields (and those only) will be
211 interpreted as addons to install, and batch-installed.
216 It is also possible to require the installation of an additional
217 addon set when a specific preset of addons has been marked for
218 installation (in the basic usage only, additionals can't depend on
221 These additionals are defined through the ``_install_if``
222 property. This property is a mapping of a collection of addons (by
223 name) to a collection of addons (by name) [#]_, and if all the *key*
224 addons are selected for installation, then the *value* ones will
225 be selected as well. For example::
228 ('sale','crm'): ['sale_crm'],
231 This will install the ``sale_crm`` addon if and only if both the
232 ``sale`` and ``crm`` addons are selected for installation.
234 You can define as many additionals as you wish, and additionals
235 can overlap in key and value. For instance::
238 ('sale','crm'): ['sale_crm'],
239 ('sale','project'): ['project_mrp'],
242 will install both ``sale_crm`` and ``project_mrp`` if all of
243 ``sale``, ``crm`` and ``project`` are selected for installation.
248 Subclasses might also need to express dependencies more complex
249 than that provided by additionals. In this case, it's possible to
250 define methods of the form ``_if_%(name)s`` where ``name`` is the
251 name of a boolean field. If the field is selected, then the
252 corresponding module will be marked for installation *and* the
253 hook method will be executed.
255 Hook methods take the usual set of parameters (cr, uid, ids,
256 context) and can return a collection of additional addons to
257 install (if they return anything, otherwise they should not return
258 anything, though returning any "falsy" value such as None or an
259 empty collection will have the same effect).
264 The last hook is to simply overload the ``modules_to_install``
265 method, which implements all the mechanisms above. This method
266 takes the usual set of parameters (cr, uid, ids, context) and
267 returns a ``set`` of addons to install (addons selected by the
268 above methods minus addons from the *basic* set which are already
269 installed) [#]_ so an overloader can simply manipulate the ``set``
270 returned by ``res_config_installer.modules_to_install`` to add or
273 Skipping the installer
274 ----------------------
276 Unless it is removed from the view, installers have a *skip*
277 button which invokes ``action_skip`` (and the ``cancel`` hook from
278 ``res.config``). Hooks and additionals *are not run* when skipping
279 installation, even for already installed addons.
281 Again, setup your hooks accordingly.
283 .. [#] note that since a mapping key needs to be hashable, it's
284 possible to use a tuple or a frozenset, but not a list or a
287 .. [#] because the already-installed modules are only pruned at
288 the very end of ``modules_to_install``, additionals and
289 hooks depending on them *are guaranteed to execute*. Setup
290 your hooks accordingly.
292 _name = 'res.config.installer'
293 _inherit = 'res.config'
294 __logger = logging.getLogger(_name)
298 def _already_installed(self, cr, uid, context=None):
299 """ For each module (boolean fields in a res.config.installer),
300 check if it's already installed (either 'to install', 'to upgrade' or 'installed')
301 and if it is, check it by default
303 modules = self.pool.get('ir.module.module')
305 selectable = [field for field in self._columns
306 if type(self._columns[field]) is fields.boolean]
307 return modules.browse(
309 modules.search(cr, uid,
310 [('name','in',selectable),
311 ('state','in',['to install', 'installed', 'to upgrade'])],
316 def modules_to_install(self, cr, uid, ids, context=None):
317 """ selects all modules to install:
319 * checked boolean fields
320 * return values of hook methods. Hook methods are of the form
321 ``_if_%(addon_name)s``, and are called if the corresponding
322 addon is marked for installation. They take the arguments
323 cr, uid, ids and context, and return an iterable of addon
325 * additionals, additionals are setup through the ``_install_if``
326 class variable. ``_install_if`` is a dict of {iterable:iterable}
327 where key and value are iterables of addon names.
329 If all the addons in the key are selected for installation
330 (warning: addons added through hooks don't count), then the
331 addons in the value are added to the set of modules to install
332 * not already installed
334 base = set(module_name
335 for installer in self.read(cr, uid, ids, context=context)
336 for module_name, to_install in installer.iteritems()
337 if module_name != 'id'
338 if type(self._columns[module_name]) is fields.boolean
341 hooks_results = set()
343 hook = getattr(self, '_if_%s'%(module), None)
345 hooks_results.update(hook(cr, uid, ids, context=None) or set())
348 module for requirements, consequences \
349 in self._install_if.iteritems()
350 if base.issuperset(requirements)
351 for module in consequences)
353 return (base | hooks_results | additionals) - set(
354 map(attrgetter('name'), self._already_installed(cr, uid, context)))
356 def default_get(self, cr, uid, fields_list, context=None):
357 ''' If an addon is already installed, check it by default
359 defaults = super(res_config_installer, self).default_get(
360 cr, uid, fields_list, context=context)
362 return dict(defaults,
364 map(attrgetter('name'),
365 self._already_installed(cr, uid, context=context)),
368 def fields_get(self, cr, uid, fields=None, context=None, write_access=True):
369 """ If an addon is already installed, set it to readonly as
370 res.config.installer doesn't handle uninstallations of already
373 fields = super(res_config_installer, self).fields_get(
374 cr, uid, fields, context, write_access)
376 for module in self._already_installed(cr, uid, context=context):
377 if module.name not in fields:
379 fields[module.name].update(
381 help= ustr(fields[module.name].get('help', '')) +
382 _('\n\nThis addon is already installed on your system'))
385 def execute(self, cr, uid, ids, context=None):
386 modules = self.pool.get('ir.module.module')
387 to_install = list(self.modules_to_install(
388 cr, uid, ids, context=context))
389 self.__logger.info('Selecting addons %s to install', to_install)
390 modules.state_update(
392 modules.search(cr, uid, [('name','in',to_install)]),
393 'to install', ['uninstalled'], context=context)
394 cr.commit() #TOFIX: after remove this statement, installation wizard is fail
395 new_db, self.pool = pooler.restart_pool(cr.dbname, update_module=True)
396 res_config_installer()
398 DEPRECATION_MESSAGE = 'You are using an addon using old-style configuration '\
399 'wizards (ir.actions.configuration.wizard). Old-style configuration '\
400 'wizards have been deprecated.\n'\
401 'The addon should be migrated to res.config objects.'
402 class ir_actions_configuration_wizard(osv.osv_memory):
403 ''' Compatibility configuration wizard
405 The old configuration wizard has been replaced by res.config, but in order
406 not to break existing but not-yet-migrated addons, the old wizard was
407 reintegrated and gutted.
409 _name='ir.actions.configuration.wizard'
410 _inherit = 'res.config'
411 __logger = logging.getLogger(_name)
413 def _next_action_note(self, cr, uid, ids, context=None):
414 next = self._next_action(cr, uid)
416 # if the next one is also an old-style extension, you never know...
419 return _("Click 'Continue' to configure the next addon...")
420 return _("Your database is now fully configured.\n\n"\
421 "Click 'Continue' and enjoy your OpenERP experience...")
424 'note': fields.text('Next Wizard', readonly=True),
427 'note': _next_action_note,
430 def execute(self, cr, uid, ids, context=None):
431 self.__logger.warn(DEPRECATION_MESSAGE)
433 ir_actions_configuration_wizard()
435 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: