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 openerp.osv import osv, fields
25 from openerp.tools import ustr
26 from openerp.tools.translate import _
28 _logger = logging.getLogger(__name__)
31 class res_config_module_installation_mixin(object):
32 def _install_modules(self, cr, uid, modules, context):
33 """Install the requested modules.
34 return the next action to execute
36 modules is a list of tuples
37 (mod_name, browse_record | None)
39 ir_module = self.pool.get('ir.module.module')
41 to_install_missing_names = []
43 for name, module in modules:
45 to_install_missing_names.append(name)
46 elif module.state == 'uninstalled':
47 to_install_ids.append(module.id)
50 ir_module.button_immediate_install(cr, uid, to_install_ids, context=context)
52 if to_install_missing_names:
54 'type': 'ir.actions.client',
56 'params': {'modules': to_install_missing_names},
61 class res_config_configurable(osv.osv_memory):
62 ''' Base classes for new-style configuration items
64 Configuration items should inherit from this class, implement
65 the execute method (and optionally the cancel one) and have
66 their view inherit from the related res_config_view_base view.
70 def _next_action(self, cr, uid, context=None):
71 Todos = self.pool['ir.actions.todo']
72 _logger.info('getting next %s', Todos)
74 active_todos = Todos.browse(cr, uid,
75 Todos.search(cr, uid, ['&', ('type', '=', 'automatic'), ('state','=','open')]),
78 user_groups = set(map(
80 self.pool['res.users'].browse(cr, uid, [uid], context=context)[0].groups_id))
82 valid_todos_for_user = [
83 todo for todo in active_todos
84 if not todo.groups_id or bool(user_groups.intersection((
85 group.id for group in todo.groups_id)))
88 if valid_todos_for_user:
89 return valid_todos_for_user[0]
93 def _next(self, cr, uid, context=None):
94 _logger.info('getting next operation')
95 next = self._next_action(cr, uid, context=context)
96 _logger.info('next action is %s', next)
98 res = next.action_launch(context=context)
99 res['nodestroy'] = False
101 # reload the client; open the first available root menu
102 menu_obj = self.pool.get('ir.ui.menu')
103 menu_ids = menu_obj.search(cr, uid, [('parent_id', '=', False)], context=context)
105 'type': 'ir.actions.client',
107 'params': {'menu_id': menu_ids and menu_ids[0] or False},
110 def start(self, cr, uid, ids, context=None):
111 return self.next(cr, uid, ids, context)
113 def next(self, cr, uid, ids, context=None):
114 """ Returns the next todo action to execute (using the default
117 return self._next(cr, uid, context=context)
119 def execute(self, cr, uid, ids, context=None):
120 """ Method called when the user clicks on the ``Next`` button.
122 Execute *must* be overloaded unless ``action_next`` is overloaded
123 (which is something you generally don't need to do).
125 If ``execute`` returns an action dictionary, that action is executed
126 rather than just going to the next configuration item.
128 raise NotImplementedError(
129 'Configuration items need to implement execute')
130 def cancel(self, cr, uid, ids, context=None):
131 """ Method called when the user click on the ``Skip`` button.
133 ``cancel`` should be overloaded instead of ``action_skip``. As with
134 ``execute``, if it returns an action dictionary that action is
135 executed in stead of the default (going to the next configuration item)
137 The default implementation is a NOOP.
139 ``cancel`` is also called by the default implementation of
144 def action_next(self, cr, uid, ids, context=None):
145 """ Action handler for the ``next`` event.
147 Sets the status of the todo the event was sent from to
148 ``done``, calls ``execute`` and -- unless ``execute`` returned
149 an action dictionary -- executes the action provided by calling
152 next = self.execute(cr, uid, ids, context=context)
154 return self.next(cr, uid, ids, context=context)
156 def action_skip(self, cr, uid, ids, context=None):
157 """ Action handler for the ``skip`` event.
159 Sets the status of the todo the event was sent from to
160 ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
161 an action dictionary -- executes the action provided by calling
164 next = self.cancel(cr, uid, ids, context=context)
166 return self.next(cr, uid, ids, context=context)
168 def action_cancel(self, cr, uid, ids, context=None):
169 """ Action handler for the ``cancel`` event. That event isn't
170 generated by the res.config.view.base inheritable view, the
171 inherited view has to overload one of the buttons (or add one
174 Sets the status of the todo the event was sent from to
175 ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
176 an action dictionary -- executes the action provided by calling
179 next = self.cancel(cr, uid, ids, context=context)
181 return self.next(cr, uid, ids, context=context)
183 res_config_configurable()
185 class res_config_installer(osv.osv_memory, res_config_module_installation_mixin):
186 """ New-style configuration base specialized for addons selection
192 Subclasses can simply define a number of _columns as
193 fields.boolean objects. The keys (column names) should be the
194 names of the addons to install (when selected). Upon action
195 execution, selected boolean fields (and those only) will be
196 interpreted as addons to install, and batch-installed.
201 It is also possible to require the installation of an additional
202 addon set when a specific preset of addons has been marked for
203 installation (in the basic usage only, additionals can't depend on
206 These additionals are defined through the ``_install_if``
207 property. This property is a mapping of a collection of addons (by
208 name) to a collection of addons (by name) [#]_, and if all the *key*
209 addons are selected for installation, then the *value* ones will
210 be selected as well. For example::
213 ('sale','crm'): ['sale_crm'],
216 This will install the ``sale_crm`` addon if and only if both the
217 ``sale`` and ``crm`` addons are selected for installation.
219 You can define as many additionals as you wish, and additionals
220 can overlap in key and value. For instance::
223 ('sale','crm'): ['sale_crm'],
224 ('sale','project'): ['project_mrp'],
227 will install both ``sale_crm`` and ``project_mrp`` if all of
228 ``sale``, ``crm`` and ``project`` are selected for installation.
233 Subclasses might also need to express dependencies more complex
234 than that provided by additionals. In this case, it's possible to
235 define methods of the form ``_if_%(name)s`` where ``name`` is the
236 name of a boolean field. If the field is selected, then the
237 corresponding module will be marked for installation *and* the
238 hook method will be executed.
240 Hook methods take the usual set of parameters (cr, uid, ids,
241 context) and can return a collection of additional addons to
242 install (if they return anything, otherwise they should not return
243 anything, though returning any "falsy" value such as None or an
244 empty collection will have the same effect).
249 The last hook is to simply overload the ``modules_to_install``
250 method, which implements all the mechanisms above. This method
251 takes the usual set of parameters (cr, uid, ids, context) and
252 returns a ``set`` of addons to install (addons selected by the
253 above methods minus addons from the *basic* set which are already
254 installed) [#]_ so an overloader can simply manipulate the ``set``
255 returned by ``res_config_installer.modules_to_install`` to add or
258 Skipping the installer
259 ----------------------
261 Unless it is removed from the view, installers have a *skip*
262 button which invokes ``action_skip`` (and the ``cancel`` hook from
263 ``res.config``). Hooks and additionals *are not run* when skipping
264 installation, even for already installed addons.
266 Again, setup your hooks accordingly.
268 .. [#] note that since a mapping key needs to be hashable, it's
269 possible to use a tuple or a frozenset, but not a list or a
272 .. [#] because the already-installed modules are only pruned at
273 the very end of ``modules_to_install``, additionals and
274 hooks depending on them *are guaranteed to execute*. Setup
275 your hooks accordingly.
277 _name = 'res.config.installer'
278 _inherit = 'res.config'
282 def already_installed(self, cr, uid, context=None):
283 """ For each module, check if it's already installed and if it
286 :returns: a list of the already installed modules in this
290 return map(attrgetter('name'),
291 self._already_installed(cr, uid, context=context))
293 def _already_installed(self, cr, uid, context=None):
294 """ For each module (boolean fields in a res.config.installer),
295 check if it's already installed (either 'to install', 'to upgrade'
296 or 'installed') and if it is return the module's browse_record
298 :returns: a list of all installed modules in this installer
299 :rtype: [browse_record]
301 modules = self.pool.get('ir.module.module')
303 selectable = [field for field in self._columns
304 if type(self._columns[field]) is fields.boolean]
305 return modules.browse(
307 modules.search(cr, uid,
308 [('name','in',selectable),
309 ('state','in',['to install', 'installed', 'to upgrade'])],
314 def modules_to_install(self, cr, uid, ids, context=None):
315 """ selects all modules to install:
317 * checked boolean fields
318 * return values of hook methods. Hook methods are of the form
319 ``_if_%(addon_name)s``, and are called if the corresponding
320 addon is marked for installation. They take the arguments
321 cr, uid, ids and context, and return an iterable of addon
323 * additionals, additionals are setup through the ``_install_if``
324 class variable. ``_install_if`` is a dict of {iterable:iterable}
325 where key and value are iterables of addon names.
327 If all the addons in the key are selected for installation
328 (warning: addons added through hooks don't count), then the
329 addons in the value are added to the set of modules to install
330 * not already installed
332 base = set(module_name
333 for installer in self.read(cr, uid, ids, context=context)
334 for module_name, to_install in installer.iteritems()
335 if module_name != 'id'
336 if type(self._columns[module_name]) is fields.boolean
339 hooks_results = set()
341 hook = getattr(self, '_if_%s'% module, None)
343 hooks_results.update(hook(cr, uid, ids, context=None) or set())
346 module for requirements, consequences \
347 in self._install_if.iteritems()
348 if base.issuperset(requirements)
349 for module in consequences)
351 return (base | hooks_results | additionals).difference(
352 self.already_installed(cr, uid, context))
354 def default_get(self, cr, uid, fields_list, context=None):
355 ''' If an addon is already installed, check it by default
357 defaults = super(res_config_installer, self).default_get(
358 cr, uid, fields_list, context=context)
360 return dict(defaults,
362 self.already_installed(cr, uid, context=context),
365 def fields_get(self, cr, uid, fields=None, context=None, write_access=True):
366 """ If an addon is already installed, set it to readonly as
367 res.config.installer doesn't handle uninstallations of already
370 fields = super(res_config_installer, self).fields_get(
371 cr, uid, fields, context, write_access)
373 for name in self.already_installed(cr, uid, context=context):
374 if name not in fields:
378 help= ustr(fields[name].get('help', '')) +
379 _('\n\nThis addon is already installed on your system'))
382 def execute(self, cr, uid, ids, context=None):
383 to_install = list(self.modules_to_install(
384 cr, uid, ids, context=context))
385 _logger.info('Selecting addons %s to install', to_install)
387 ir_module = self.pool.get('ir.module.module')
389 for name in to_install:
390 mod_ids = ir_module.search(cr, uid, [('name', '=', name)])
391 record = ir_module.browse(cr, uid, mod_ids[0], context) if mod_ids else None
392 modules.append((name, record))
394 return self._install_modules(cr, uid, modules, context=context)
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'
412 def _next_action_note(self, cr, uid, ids, context=None):
413 next = self._next_action(cr, uid)
415 # if the next one is also an old-style extension, you never know...
418 return _("Click 'Continue' to configure the next addon...")
419 return _("Your database is now fully configured.\n\n"\
420 "Click 'Continue' and enjoy your OpenERP experience...")
423 'note': fields.text('Next Wizard', readonly=True),
426 'note': _next_action_note,
429 def execute(self, cr, uid, ids, context=None):
430 _logger.warning(DEPRECATION_MESSAGE)
432 ir_actions_configuration_wizard()
435 class res_config_settings(osv.osv_memory, res_config_module_installation_mixin):
436 """ Base configuration wizard for application settings. It provides support for setting
437 default values, assigning groups to employee users, and installing modules.
438 To make such a 'settings' wizard, define a model like::
440 class my_config_wizard(osv.osv_memory):
441 _name = 'my.settings'
442 _inherit = 'res.config.settings'
444 'default_foo': fields.type(..., default_model='my.model'),
445 'group_bar': fields.boolean(..., group='base.group_user', implied_group='my.group'),
446 'module_baz': fields.boolean(...),
447 'other_field': fields.type(...),
450 The method ``execute`` provides some support based on a naming convention:
452 * For a field like 'default_XXX', ``execute`` sets the (global) default value of
453 the field 'XXX' in the model named by ``default_model`` to the field's value.
455 * For a boolean field like 'group_XXX', ``execute`` adds/removes 'implied_group'
456 to/from the implied groups of 'group', depending on the field's value.
457 By default 'group' is the group Employee. Groups are given by their xml id.
459 * For a boolean field like 'module_XXX', ``execute`` triggers the immediate
460 installation of the module named 'XXX' if the field has value ``True``.
462 * For the other fields, the method ``execute`` invokes all methods with a name
463 that starts with 'set_'; such methods can be defined to implement the effect
466 The method ``default_get`` retrieves values that reflect the current status of the
467 fields like 'default_XXX', 'group_XXX' and 'module_XXX'. It also invokes all methods
468 with a name that starts with 'get_default_'; such methods can be defined to provide
469 current values for other fields.
471 _name = 'res.config.settings'
473 def copy(self, cr, uid, id, values, context=None):
474 raise osv.except_osv(_("Cannot duplicate configuration!"), "")
476 def _get_classified_fields(self, cr, uid, context=None):
477 """ return a dictionary with the fields classified by category::
479 { 'default': [('default_foo', 'model', 'foo'), ...],
480 'group': [('group_bar', browse_group, browse_implied_group), ...],
481 'module': [('module_baz', browse_module), ...],
482 'other': ['other_field', ...],
485 ir_model_data = self.pool.get('ir.model.data')
486 ir_module = self.pool.get('ir.module.module')
488 mod, xml = xml_id.split('.', 1)
489 return ir_model_data.get_object(cr, uid, mod, xml, context)
491 defaults, groups, modules, others = [], [], [], []
492 for name, field in self._columns.items():
493 if name.startswith('default_') and hasattr(field, 'default_model'):
494 defaults.append((name, field.default_model, name[8:]))
495 elif name.startswith('group_') and isinstance(field, fields.boolean) and hasattr(field, 'implied_group'):
496 field_group = getattr(field, 'group', 'base.group_user')
497 groups.append((name, ref(field_group), ref(field.implied_group)))
498 elif name.startswith('module_') and isinstance(field, fields.boolean):
499 mod_ids = ir_module.search(cr, uid, [('name', '=', name[7:])])
500 record = ir_module.browse(cr, uid, mod_ids[0], context) if mod_ids else None
501 modules.append((name, record))
505 return {'default': defaults, 'group': groups, 'module': modules, 'other': others}
507 def default_get(self, cr, uid, fields, context=None):
508 ir_values = self.pool.get('ir.values')
509 classified = self._get_classified_fields(cr, uid, context)
511 res = super(res_config_settings, self).default_get(cr, uid, fields, context)
513 # defaults: take the corresponding default value they set
514 for name, model, field in classified['default']:
515 value = ir_values.get_default(cr, uid, model, field)
516 if value is not None:
519 # groups: which groups are implied by the group Employee
520 for name, group, implied_group in classified['group']:
521 res[name] = implied_group in group.implied_ids
523 # modules: which modules are installed/to install
524 for name, module in classified['module']:
525 res[name] = module and module.state in ('installed', 'to install', 'to upgrade')
527 # other fields: call all methods that start with 'get_default_'
528 for method in dir(self):
529 if method.startswith('get_default_'):
530 res.update(getattr(self, method)(cr, uid, fields, context))
534 def execute(self, cr, uid, ids, context=None):
535 ir_values = self.pool.get('ir.values')
536 ir_module = self.pool.get('ir.module.module')
537 classified = self._get_classified_fields(cr, uid, context)
539 config = self.browse(cr, uid, ids[0], context)
541 # default values fields
542 for name, model, field in classified['default']:
543 ir_values.set_default(cr, uid, model, field, config[name])
545 # group fields: modify group / implied groups
546 for name, group, implied_group in classified['group']:
548 group.write({'implied_ids': [(4, implied_group.id)]})
550 group.write({'implied_ids': [(3, implied_group.id)]})
551 implied_group.write({'users': [(3, u.id) for u in group.users]})
553 # other fields: execute all methods that start with 'set_'
554 for method in dir(self):
555 if method.startswith('set_'):
556 getattr(self, method)(cr, uid, ids, context)
558 # module fields: install/uninstall the selected modules
560 to_uninstall_ids = []
562 for name, module in classified['module']:
564 to_install.append((name[lm:], module))
566 if module and module.state in ('installed', 'to upgrade'):
567 to_uninstall_ids.append(module.id)
570 ir_module.button_immediate_uninstall(cr, uid, to_uninstall_ids, context=context)
572 action = self._install_modules(cr, uid, to_install, context=context)
576 config = self.pool.get('res.config').next(cr, uid, [], context=context) or {}
577 if config.get('type') not in ('ir.actions.act_window_close',):
580 # force client-side reload (update user menu and current view)
582 'type': 'ir.actions.client',
586 def cancel(self, cr, uid, ids, context=None):
587 # ignore the current record, and send the action to reopen the view
588 act_window = self.pool.get('ir.actions.act_window')
589 action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)])
591 return act_window.read(cr, uid, action_ids[0], [], context=context)
594 def name_get(self, cr, uid, ids, context=None):
595 """ Override name_get method to return an appropriate configuration wizard
596 name, and not the generated name."""
600 # name_get may receive int id instead of an id list
601 if isinstance(ids, (int, long)):
604 act_window = self.pool.get('ir.actions.act_window')
605 action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)], context=context)
608 name = act_window.read(cr, uid, action_ids[0], ['name'], context=context)['name']
609 return [(record.id, name) for record in self.browse(cr, uid , ids, context=context)]
611 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: