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, itemgetter
24 from osv import osv, fields
25 from tools.translate import _
27 from tools import ustr
30 _logger = logging.getLogger(__name__)
32 class res_config_configurable(osv.osv_memory):
33 ''' Base classes for new-style configuration items
35 Configuration items should inherit from this class, implement
36 the execute method (and optionally the cancel one) and have
37 their view inherit from the related res_config_view_base view.
41 def _next_action(self, cr, uid, context=None):
42 Todos = self.pool['ir.actions.todo']
43 _logger.info('getting next %s', Todos)
45 active_todos = Todos.browse(cr, uid,
46 Todos.search(cr, uid, ['&', ('type', '=', 'automatic'), ('state','=','open')]),
49 user_groups = set(map(
51 self.pool['res.users'].browse(cr, uid, [uid], context=context)[0].groups_id))
53 valid_todos_for_user = [
54 todo for todo in active_todos
55 if not todo.groups_id or bool(user_groups.intersection((
56 group.id for group in todo.groups_id)))
59 if valid_todos_for_user:
60 return valid_todos_for_user[0]
64 def _next(self, cr, uid, context=None):
65 _logger.info('getting next operation')
66 next = self._next_action(cr, uid, context=context)
67 _logger.info('next action is %s', next)
69 res = next.action_launch(context=context)
70 res['nodestroy'] = False
72 # reload the client; open the first available root menu
73 menu_obj = self.pool.get('ir.ui.menu')
74 menu_ids = menu_obj.search(cr, uid, [('parent_id', '=', False)], context=context)
76 'type': 'ir.actions.client',
78 'params': {'menu_id': menu_ids and menu_ids[0] or False},
81 def start(self, cr, uid, ids, context=None):
82 return self.next(cr, uid, ids, context)
84 def next(self, cr, uid, ids, context=None):
85 """ Returns the next todo action to execute (using the default
88 return self._next(cr, uid, context=context)
90 def execute(self, cr, uid, ids, context=None):
91 """ Method called when the user clicks on the ``Next`` button.
93 Execute *must* be overloaded unless ``action_next`` is overloaded
94 (which is something you generally don't need to do).
96 If ``execute`` returns an action dictionary, that action is executed
97 rather than just going to the next configuration item.
99 raise NotImplementedError(
100 'Configuration items need to implement execute')
101 def cancel(self, cr, uid, ids, context=None):
102 """ Method called when the user click on the ``Skip`` button.
104 ``cancel`` should be overloaded instead of ``action_skip``. As with
105 ``execute``, if it returns an action dictionary that action is
106 executed in stead of the default (going to the next configuration item)
108 The default implementation is a NOOP.
110 ``cancel`` is also called by the default implementation of
115 def action_next(self, cr, uid, ids, context=None):
116 """ Action handler for the ``next`` event.
118 Sets the status of the todo the event was sent from to
119 ``done``, calls ``execute`` and -- unless ``execute`` returned
120 an action dictionary -- executes the action provided by calling
123 next = self.execute(cr, uid, ids, context=context)
125 return self.next(cr, uid, ids, context=context)
127 def action_skip(self, cr, uid, ids, context=None):
128 """ Action handler for the ``skip`` event.
130 Sets the status of the todo the event was sent from to
131 ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
132 an action dictionary -- executes the action provided by calling
135 next = self.cancel(cr, uid, ids, context=context)
137 return self.next(cr, uid, ids, context=context)
139 def action_cancel(self, cr, uid, ids, context=None):
140 """ Action handler for the ``cancel`` event. That event isn't
141 generated by the res.config.view.base inheritable view, the
142 inherited view has to overload one of the buttons (or add one
145 Sets the status of the todo the event was sent from to
146 ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
147 an action dictionary -- executes the action provided by calling
150 next = self.cancel(cr, uid, ids, context=context)
152 return self.next(cr, uid, ids, context=context)
154 res_config_configurable()
156 class res_config_installer(osv.osv_memory):
157 """ New-style configuration base specialized for addons selection
163 Subclasses can simply define a number of _columns as
164 fields.boolean objects. The keys (column names) should be the
165 names of the addons to install (when selected). Upon action
166 execution, selected boolean fields (and those only) will be
167 interpreted as addons to install, and batch-installed.
172 It is also possible to require the installation of an additional
173 addon set when a specific preset of addons has been marked for
174 installation (in the basic usage only, additionals can't depend on
177 These additionals are defined through the ``_install_if``
178 property. This property is a mapping of a collection of addons (by
179 name) to a collection of addons (by name) [#]_, and if all the *key*
180 addons are selected for installation, then the *value* ones will
181 be selected as well. For example::
184 ('sale','crm'): ['sale_crm'],
187 This will install the ``sale_crm`` addon if and only if both the
188 ``sale`` and ``crm`` addons are selected for installation.
190 You can define as many additionals as you wish, and additionals
191 can overlap in key and value. For instance::
194 ('sale','crm'): ['sale_crm'],
195 ('sale','project'): ['project_mrp'],
198 will install both ``sale_crm`` and ``project_mrp`` if all of
199 ``sale``, ``crm`` and ``project`` are selected for installation.
204 Subclasses might also need to express dependencies more complex
205 than that provided by additionals. In this case, it's possible to
206 define methods of the form ``_if_%(name)s`` where ``name`` is the
207 name of a boolean field. If the field is selected, then the
208 corresponding module will be marked for installation *and* the
209 hook method will be executed.
211 Hook methods take the usual set of parameters (cr, uid, ids,
212 context) and can return a collection of additional addons to
213 install (if they return anything, otherwise they should not return
214 anything, though returning any "falsy" value such as None or an
215 empty collection will have the same effect).
220 The last hook is to simply overload the ``modules_to_install``
221 method, which implements all the mechanisms above. This method
222 takes the usual set of parameters (cr, uid, ids, context) and
223 returns a ``set`` of addons to install (addons selected by the
224 above methods minus addons from the *basic* set which are already
225 installed) [#]_ so an overloader can simply manipulate the ``set``
226 returned by ``res_config_installer.modules_to_install`` to add or
229 Skipping the installer
230 ----------------------
232 Unless it is removed from the view, installers have a *skip*
233 button which invokes ``action_skip`` (and the ``cancel`` hook from
234 ``res.config``). Hooks and additionals *are not run* when skipping
235 installation, even for already installed addons.
237 Again, setup your hooks accordingly.
239 .. [#] note that since a mapping key needs to be hashable, it's
240 possible to use a tuple or a frozenset, but not a list or a
243 .. [#] because the already-installed modules are only pruned at
244 the very end of ``modules_to_install``, additionals and
245 hooks depending on them *are guaranteed to execute*. Setup
246 your hooks accordingly.
248 _name = 'res.config.installer'
249 _inherit = 'res.config'
253 def already_installed(self, cr, uid, context=None):
254 """ For each module, check if it's already installed and if it
257 :returns: a list of the already installed modules in this
261 return map(attrgetter('name'),
262 self._already_installed(cr, uid, context=context))
264 def _already_installed(self, cr, uid, context=None):
265 """ For each module (boolean fields in a res.config.installer),
266 check if it's already installed (either 'to install', 'to upgrade'
267 or 'installed') and if it is return the module's browse_record
269 :returns: a list of all installed modules in this installer
270 :rtype: [browse_record]
272 modules = self.pool.get('ir.module.module')
274 selectable = [field for field in self._columns
275 if type(self._columns[field]) is fields.boolean]
276 return modules.browse(
278 modules.search(cr, uid,
279 [('name','in',selectable),
280 ('state','in',['to install', 'installed', 'to upgrade'])],
285 def modules_to_install(self, cr, uid, ids, context=None):
286 """ selects all modules to install:
288 * checked boolean fields
289 * return values of hook methods. Hook methods are of the form
290 ``_if_%(addon_name)s``, and are called if the corresponding
291 addon is marked for installation. They take the arguments
292 cr, uid, ids and context, and return an iterable of addon
294 * additionals, additionals are setup through the ``_install_if``
295 class variable. ``_install_if`` is a dict of {iterable:iterable}
296 where key and value are iterables of addon names.
298 If all the addons in the key are selected for installation
299 (warning: addons added through hooks don't count), then the
300 addons in the value are added to the set of modules to install
301 * not already installed
303 base = set(module_name
304 for installer in self.read(cr, uid, ids, context=context)
305 for module_name, to_install in installer.iteritems()
306 if module_name != 'id'
307 if type(self._columns[module_name]) is fields.boolean
310 hooks_results = set()
312 hook = getattr(self, '_if_%s'% module, None)
314 hooks_results.update(hook(cr, uid, ids, context=None) or set())
317 module for requirements, consequences \
318 in self._install_if.iteritems()
319 if base.issuperset(requirements)
320 for module in consequences)
322 return (base | hooks_results | additionals).difference(
323 self.already_installed(cr, uid, context))
325 def default_get(self, cr, uid, fields_list, context=None):
326 ''' If an addon is already installed, check it by default
328 defaults = super(res_config_installer, self).default_get(
329 cr, uid, fields_list, context=context)
331 return dict(defaults,
333 self.already_installed(cr, uid, context=context),
336 def fields_get(self, cr, uid, fields=None, context=None, write_access=True):
337 """ If an addon is already installed, set it to readonly as
338 res.config.installer doesn't handle uninstallations of already
341 fields = super(res_config_installer, self).fields_get(
342 cr, uid, fields, context, write_access)
344 for name in self.already_installed(cr, uid, context=context):
345 if name not in fields:
349 help= ustr(fields[name].get('help', '')) +
350 _('\n\nThis addon is already installed on your system'))
353 def execute(self, cr, uid, ids, context=None):
354 modules = self.pool.get('ir.module.module')
355 to_install = list(self.modules_to_install(
356 cr, uid, ids, context=context))
357 _logger.info('Selecting addons %s to install', to_install)
358 modules.state_update(
360 modules.search(cr, uid, [('name','in',to_install)]),
361 'to install', ['uninstalled'], context=context)
362 cr.commit() #TOFIX: after remove this statement, installation wizard is fail
363 new_db, self.pool = pooler.restart_pool(cr.dbname, update_module=True)
365 res_config_installer()
367 DEPRECATION_MESSAGE = 'You are using an addon using old-style configuration '\
368 'wizards (ir.actions.configuration.wizard). Old-style configuration '\
369 'wizards have been deprecated.\n'\
370 'The addon should be migrated to res.config objects.'
371 class ir_actions_configuration_wizard(osv.osv_memory):
372 ''' Compatibility configuration wizard
374 The old configuration wizard has been replaced by res.config, but in order
375 not to break existing but not-yet-migrated addons, the old wizard was
376 reintegrated and gutted.
378 _name='ir.actions.configuration.wizard'
379 _inherit = 'res.config'
381 def _next_action_note(self, cr, uid, ids, context=None):
382 next = self._next_action(cr, uid)
384 # if the next one is also an old-style extension, you never know...
387 return _("Click 'Continue' to configure the next addon...")
388 return _("Your database is now fully configured.\n\n"\
389 "Click 'Continue' and enjoy your OpenERP experience...")
392 'note': fields.text('Next Wizard', readonly=True),
395 'note': _next_action_note,
398 def execute(self, cr, uid, ids, context=None):
399 _logger.warning(DEPRECATION_MESSAGE)
401 ir_actions_configuration_wizard()
405 class res_config_settings(osv.osv_memory):
406 """ Base configuration wizard for application settings. It provides support for setting
407 default values, assigning groups to employee users, and installing modules.
408 To make such a 'settings' wizard, define a model like::
410 class my_config_wizard(osv.osv_memory):
411 _name = 'my.settings'
412 _inherit = 'res.config.settings'
414 'default_foo': fields.type(..., default_model='my.model'),
415 'group_bar': fields.boolean(..., group='base.group_user', implied_group='my.group'),
416 'module_baz': fields.boolean(...),
417 'other_field': fields.type(...),
420 The method ``execute`` provides some support based on a naming convention:
422 * For a field like 'default_XXX', ``execute`` sets the (global) default value of
423 the field 'XXX' in the model named by ``default_model`` to the field's value.
425 * For a boolean field like 'group_XXX', ``execute`` adds/removes 'implied_group'
426 to/from the implied groups of 'group', depending on the field's value.
427 By default 'group' is the group Employee. Groups are given by their xml id.
429 * For a boolean field like 'module_XXX', ``execute`` triggers the immediate
430 installation of the module named 'XXX' if the field has value ``True``.
432 * For the other fields, the method ``execute`` invokes all methods with a name
433 that starts with 'set_'; such methods can be defined to implement the effect
436 The method ``default_get`` retrieves values that reflect the current status of the
437 fields like 'default_XXX', 'group_XXX' and 'module_XXX'. It also invokes all methods
438 with a name that starts with 'get_default_'; such methods can be defined to provide
439 current values for other fields.
441 _name = 'res.config.settings'
443 def copy(self, cr, uid, id, values, context=None):
444 raise osv.except_osv(_("Cannot duplicate configuration!"), "")
446 def _get_classified_fields(self, cr, uid, context=None):
447 """ return a dictionary with the fields classified by category::
449 { 'default': [('default_foo', 'model', 'foo'), ...],
450 'group': [('group_bar', browse_group, browse_implied_group), ...],
451 'module': [('module_baz', browse_module), ...],
452 'other': ['other_field', ...],
455 ir_model_data = self.pool.get('ir.model.data')
456 ir_module = self.pool.get('ir.module.module')
458 mod, xml = xml_id.split('.', 1)
459 return ir_model_data.get_object(cr, uid, mod, xml, context)
461 defaults, groups, modules, others = [], [], [], []
462 for name, field in self._columns.items():
463 if name.startswith('default_') and hasattr(field, 'default_model'):
464 defaults.append((name, field.default_model, name[8:]))
465 elif name.startswith('group_') and isinstance(field, fields.boolean) and hasattr(field, 'implied_group'):
466 field_group = getattr(field, 'group', 'base.group_user')
467 groups.append((name, ref(field_group), ref(field.implied_group)))
468 elif name.startswith('module_') and isinstance(field, fields.boolean):
469 mod_ids = ir_module.search(cr, uid, [('name', '=', name[7:])])
470 modules.append((name, ir_module.browse(cr, uid, mod_ids[0], context)))
474 return {'default': defaults, 'group': groups, 'module': modules, 'other': others}
476 def default_get(self, cr, uid, fields, context=None):
477 ir_values = self.pool.get('ir.values')
478 classified = self._get_classified_fields(cr, uid, context)
480 res = super(res_config_settings, self).default_get(cr, uid, fields, context)
482 # defaults: take the corresponding default value they set
483 for name, model, field in classified['default']:
484 value = ir_values.get_default(cr, uid, model, field)
485 if value is not None:
488 # groups: which groups are implied by the group Employee
489 for name, group, implied_group in classified['group']:
490 res[name] = implied_group in group.implied_ids
492 # modules: which modules are installed/to install
493 for name, module in classified['module']:
494 res[name] = module.state in ('installed', 'to install', 'to upgrade')
496 # other fields: call all methods that start with 'get_default_'
497 for method in dir(self):
498 if method.startswith('get_default_'):
499 res.update(getattr(self, method)(cr, uid, fields, context))
503 def execute(self, cr, uid, ids, context=None):
504 ir_values = self.pool.get('ir.values')
505 ir_model_data = self.pool.get('ir.model.data')
506 ir_module = self.pool.get('ir.module.module')
507 res_groups = self.pool.get('res.groups')
508 classified = self._get_classified_fields(cr, uid, context)
510 config = self.browse(cr, uid, ids[0], context)
512 # default values fields
513 for name, model, field in classified['default']:
514 ir_values.set_default(cr, uid, model, field, config[name])
516 # group fields: modify group / implied groups
517 for name, group, implied_group in classified['group']:
519 group.write({'implied_ids': [(4, implied_group.id)]})
521 group.write({'implied_ids': [(3, implied_group.id)]})
522 implied_group.write({'users': [(3, u.id) for u in group.users]})
524 # other fields: execute all methods that start with 'set_'
525 for method in dir(self):
526 if method.startswith('set_'):
527 getattr(self, method)(cr, uid, ids, context)
529 # module fields: install/uninstall the selected modules
531 to_uninstall_ids = []
532 for name, module in classified['module']:
534 if module.state == 'uninstalled': to_install_ids.append(module.id)
536 if module.state in ('installed','upgrade'): to_uninstall_ids.append(module.id)
538 if to_install_ids or to_uninstall_ids:
539 ir_module.button_uninstall(cr, uid, to_uninstall_ids, context=context)
540 ir_module.button_immediate_install(cr, uid, to_install_ids, context=context)
542 config = self.pool.get('res.config').next(cr, uid, [], context=context) or {}
543 if config.get('type') not in ('ir.actions.act_window_close',):
546 # force client-side reload (update user menu and current view)
548 'type': 'ir.actions.client',
552 def cancel(self, cr, uid, ids, context=None):
553 # ignore the current record, and send the action to reopen the view
554 act_window = self.pool.get('ir.actions.act_window')
555 action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)])
557 return act_window.read(cr, uid, action_ids[0], [], context=context)
560 def name_get(self, cr, uid, ids, context=None):
561 """ Override name_get method to return an appropriate configuration wizard
562 name, and not the generated name."""
566 # name_get may receive int id instead of an id list
567 if isinstance(ids, (int, long)):
570 act_window = self.pool.get('ir.actions.act_window')
571 action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)], context=context)
574 name = act_window.read(cr, uid, action_ids[0], ['name'], context=context)['name']
575 return [(record.id, name) for record in self.browse(cr, uid , ids, context=context)]
577 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: