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 import pooler
25 from openerp.osv import osv, fields
26 from openerp.tools import ustr
27 from openerp.tools.translate import _
29 _logger = logging.getLogger(__name__)
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.
40 def _next_action(self, cr, uid, context=None):
41 Todos = self.pool['ir.actions.todo']
42 _logger.info('getting next %s', Todos)
44 active_todos = Todos.browse(cr, uid,
45 Todos.search(cr, uid, ['&', ('type', '=', 'automatic'), ('state','=','open')]),
48 user_groups = set(map(
50 self.pool['res.users'].browse(cr, uid, [uid], context=context)[0].groups_id))
52 valid_todos_for_user = [
53 todo for todo in active_todos
54 if not todo.groups_id or bool(user_groups.intersection((
55 group.id for group in todo.groups_id)))
58 if valid_todos_for_user:
59 return valid_todos_for_user[0]
63 def _next(self, cr, uid, context=None):
64 _logger.info('getting next operation')
65 next = self._next_action(cr, uid, context=context)
66 _logger.info('next action is %s', next)
68 res = next.action_launch(context=context)
69 res['nodestroy'] = False
71 # reload the client; open the first available root menu
72 menu_obj = self.pool.get('ir.ui.menu')
73 menu_ids = menu_obj.search(cr, uid, [('parent_id', '=', False)], context=context)
75 'type': 'ir.actions.client',
77 'params': {'menu_id': menu_ids and menu_ids[0] or False},
80 def start(self, cr, uid, ids, context=None):
81 return self.next(cr, uid, ids, context)
83 def next(self, cr, uid, ids, context=None):
84 """ Returns the next todo action to execute (using the default
87 return self._next(cr, uid, context=context)
89 def execute(self, cr, uid, ids, context=None):
90 """ Method called when the user clicks on the ``Next`` button.
92 Execute *must* be overloaded unless ``action_next`` is overloaded
93 (which is something you generally don't need to do).
95 If ``execute`` returns an action dictionary, that action is executed
96 rather than just going to the next configuration item.
98 raise NotImplementedError(
99 'Configuration items need to implement execute')
100 def cancel(self, cr, uid, ids, context=None):
101 """ Method called when the user click on the ``Skip`` button.
103 ``cancel`` should be overloaded instead of ``action_skip``. As with
104 ``execute``, if it returns an action dictionary that action is
105 executed in stead of the default (going to the next configuration item)
107 The default implementation is a NOOP.
109 ``cancel`` is also called by the default implementation of
114 def action_next(self, cr, uid, ids, context=None):
115 """ Action handler for the ``next`` event.
117 Sets the status of the todo the event was sent from to
118 ``done``, calls ``execute`` and -- unless ``execute`` returned
119 an action dictionary -- executes the action provided by calling
122 next = self.execute(cr, uid, ids, context=context)
124 return self.next(cr, uid, ids, context=context)
126 def action_skip(self, cr, uid, ids, context=None):
127 """ Action handler for the ``skip`` event.
129 Sets the status of the todo the event was sent from to
130 ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
131 an action dictionary -- executes the action provided by calling
134 next = self.cancel(cr, uid, ids, context=context)
136 return self.next(cr, uid, ids, context=context)
138 def action_cancel(self, cr, uid, ids, context=None):
139 """ Action handler for the ``cancel`` event. That event isn't
140 generated by the res.config.view.base inheritable view, the
141 inherited view has to overload one of the buttons (or add one
144 Sets the status of the todo the event was sent from to
145 ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
146 an action dictionary -- executes the action provided by calling
149 next = self.cancel(cr, uid, ids, context=context)
151 return self.next(cr, uid, ids, context=context)
153 res_config_configurable()
155 class res_config_installer(osv.osv_memory):
156 """ New-style configuration base specialized for addons selection
162 Subclasses can simply define a number of _columns as
163 fields.boolean objects. The keys (column names) should be the
164 names of the addons to install (when selected). Upon action
165 execution, selected boolean fields (and those only) will be
166 interpreted as addons to install, and batch-installed.
171 It is also possible to require the installation of an additional
172 addon set when a specific preset of addons has been marked for
173 installation (in the basic usage only, additionals can't depend on
176 These additionals are defined through the ``_install_if``
177 property. This property is a mapping of a collection of addons (by
178 name) to a collection of addons (by name) [#]_, and if all the *key*
179 addons are selected for installation, then the *value* ones will
180 be selected as well. For example::
183 ('sale','crm'): ['sale_crm'],
186 This will install the ``sale_crm`` addon if and only if both the
187 ``sale`` and ``crm`` addons are selected for installation.
189 You can define as many additionals as you wish, and additionals
190 can overlap in key and value. For instance::
193 ('sale','crm'): ['sale_crm'],
194 ('sale','project'): ['project_mrp'],
197 will install both ``sale_crm`` and ``project_mrp`` if all of
198 ``sale``, ``crm`` and ``project`` are selected for installation.
203 Subclasses might also need to express dependencies more complex
204 than that provided by additionals. In this case, it's possible to
205 define methods of the form ``_if_%(name)s`` where ``name`` is the
206 name of a boolean field. If the field is selected, then the
207 corresponding module will be marked for installation *and* the
208 hook method will be executed.
210 Hook methods take the usual set of parameters (cr, uid, ids,
211 context) and can return a collection of additional addons to
212 install (if they return anything, otherwise they should not return
213 anything, though returning any "falsy" value such as None or an
214 empty collection will have the same effect).
219 The last hook is to simply overload the ``modules_to_install``
220 method, which implements all the mechanisms above. This method
221 takes the usual set of parameters (cr, uid, ids, context) and
222 returns a ``set`` of addons to install (addons selected by the
223 above methods minus addons from the *basic* set which are already
224 installed) [#]_ so an overloader can simply manipulate the ``set``
225 returned by ``res_config_installer.modules_to_install`` to add or
228 Skipping the installer
229 ----------------------
231 Unless it is removed from the view, installers have a *skip*
232 button which invokes ``action_skip`` (and the ``cancel`` hook from
233 ``res.config``). Hooks and additionals *are not run* when skipping
234 installation, even for already installed addons.
236 Again, setup your hooks accordingly.
238 .. [#] note that since a mapping key needs to be hashable, it's
239 possible to use a tuple or a frozenset, but not a list or a
242 .. [#] because the already-installed modules are only pruned at
243 the very end of ``modules_to_install``, additionals and
244 hooks depending on them *are guaranteed to execute*. Setup
245 your hooks accordingly.
247 _name = 'res.config.installer'
248 _inherit = 'res.config'
252 def already_installed(self, cr, uid, context=None):
253 """ For each module, check if it's already installed and if it
256 :returns: a list of the already installed modules in this
260 return map(attrgetter('name'),
261 self._already_installed(cr, uid, context=context))
263 def _already_installed(self, cr, uid, context=None):
264 """ For each module (boolean fields in a res.config.installer),
265 check if it's already installed (either 'to install', 'to upgrade'
266 or 'installed') and if it is return the module's browse_record
268 :returns: a list of all installed modules in this installer
269 :rtype: [browse_record]
271 modules = self.pool.get('ir.module.module')
273 selectable = [field for field in self._columns
274 if type(self._columns[field]) is fields.boolean]
275 return modules.browse(
277 modules.search(cr, uid,
278 [('name','in',selectable),
279 ('state','in',['to install', 'installed', 'to upgrade'])],
284 def modules_to_install(self, cr, uid, ids, context=None):
285 """ selects all modules to install:
287 * checked boolean fields
288 * return values of hook methods. Hook methods are of the form
289 ``_if_%(addon_name)s``, and are called if the corresponding
290 addon is marked for installation. They take the arguments
291 cr, uid, ids and context, and return an iterable of addon
293 * additionals, additionals are setup through the ``_install_if``
294 class variable. ``_install_if`` is a dict of {iterable:iterable}
295 where key and value are iterables of addon names.
297 If all the addons in the key are selected for installation
298 (warning: addons added through hooks don't count), then the
299 addons in the value are added to the set of modules to install
300 * not already installed
302 base = set(module_name
303 for installer in self.read(cr, uid, ids, context=context)
304 for module_name, to_install in installer.iteritems()
305 if module_name != 'id'
306 if type(self._columns[module_name]) is fields.boolean
309 hooks_results = set()
311 hook = getattr(self, '_if_%s'%(module), None)
313 hooks_results.update(hook(cr, uid, ids, context=None) or set())
316 module for requirements, consequences \
317 in self._install_if.iteritems()
318 if base.issuperset(requirements)
319 for module in consequences)
321 return (base | hooks_results | additionals).difference(
322 self.already_installed(cr, uid, context))
324 def default_get(self, cr, uid, fields_list, context=None):
325 ''' If an addon is already installed, check it by default
327 defaults = super(res_config_installer, self).default_get(
328 cr, uid, fields_list, context=context)
330 return dict(defaults,
332 self.already_installed(cr, uid, context=context),
335 def fields_get(self, cr, uid, fields=None, context=None, write_access=True):
336 """ If an addon is already installed, set it to readonly as
337 res.config.installer doesn't handle uninstallations of already
340 fields = super(res_config_installer, self).fields_get(
341 cr, uid, fields, context, write_access)
343 for name in self.already_installed(cr, uid, context=context):
344 if name not in fields:
348 help= ustr(fields[name].get('help', '')) +
349 _('\n\nThis addon is already installed on your system'))
352 def execute(self, cr, uid, ids, context=None):
353 modules = self.pool.get('ir.module.module')
354 to_install = list(self.modules_to_install(
355 cr, uid, ids, context=context))
356 _logger.info('Selecting addons %s to install', to_install)
357 modules.state_update(
359 modules.search(cr, uid, [('name','in',to_install)]),
360 'to install', ['uninstalled'], context=context)
361 cr.commit() #TOFIX: after remove this statement, installation wizard is fail
362 new_db, self.pool = pooler.restart_pool(cr.dbname, update_module=True)
364 res_config_installer()
366 DEPRECATION_MESSAGE = 'You are using an addon using old-style configuration '\
367 'wizards (ir.actions.configuration.wizard). Old-style configuration '\
368 'wizards have been deprecated.\n'\
369 'The addon should be migrated to res.config objects.'
370 class ir_actions_configuration_wizard(osv.osv_memory):
371 ''' Compatibility configuration wizard
373 The old configuration wizard has been replaced by res.config, but in order
374 not to break existing but not-yet-migrated addons, the old wizard was
375 reintegrated and gutted.
377 _name='ir.actions.configuration.wizard'
378 _inherit = 'res.config'
380 def _next_action_note(self, cr, uid, ids, context=None):
381 next = self._next_action(cr, uid)
383 # if the next one is also an old-style extension, you never know...
386 return _("Click 'Continue' to configure the next addon...")
387 return _("Your database is now fully configured.\n\n"\
388 "Click 'Continue' and enjoy your OpenERP experience...")
391 'note': fields.text('Next Wizard', readonly=True),
394 'note': _next_action_note,
397 def execute(self, cr, uid, ids, context=None):
398 _logger.warning(DEPRECATION_MESSAGE)
400 ir_actions_configuration_wizard()
404 class res_config_settings(osv.osv_memory):
405 """ Base configuration wizard for application settings. It provides support for setting
406 default values, assigning groups to employee users, and installing modules.
407 To make such a 'settings' wizard, define a model like::
409 class my_config_wizard(osv.osv_memory):
410 _name = 'my.settings'
411 _inherit = 'res.config.settings'
413 'default_foo': fields.type(..., default_model='my.model'),
414 'group_bar': fields.boolean(..., group='base.group_user', implied_group='my.group'),
415 'module_baz': fields.boolean(...),
416 'other_field': fields.type(...),
419 The method ``execute`` provides some support based on a naming convention:
421 * For a field like 'default_XXX', ``execute`` sets the (global) default value of
422 the field 'XXX' in the model named by ``default_model`` to the field's value.
424 * For a boolean field like 'group_XXX', ``execute`` adds/removes 'implied_group'
425 to/from the implied groups of 'group', depending on the field's value.
426 By default 'group' is the group Employee. Groups are given by their xml id.
428 * For a boolean field like 'module_XXX', ``execute`` triggers the immediate
429 installation of the module named 'XXX' if the field has value ``True``.
431 * For the other fields, the method ``execute`` invokes all methods with a name
432 that starts with 'set_'; such methods can be defined to implement the effect
435 The method ``default_get`` retrieves values that reflect the current status of the
436 fields like 'default_XXX', 'group_XXX' and 'module_XXX'. It also invokes all methods
437 with a name that starts with 'get_default_'; such methods can be defined to provide
438 current values for other fields.
440 _name = 'res.config.settings'
442 def copy(self, cr, uid, id, values, context=None):
443 raise osv.except_osv(_("Cannot duplicate configuration!"), "")
445 def _get_classified_fields(self, cr, uid, context=None):
446 """ return a dictionary with the fields classified by category::
448 { 'default': [('default_foo', 'model', 'foo'), ...],
449 'group': [('group_bar', browse_group, browse_implied_group), ...],
450 'module': [('module_baz', browse_module), ...],
451 'other': ['other_field', ...],
454 ir_model_data = self.pool.get('ir.model.data')
455 ir_module = self.pool.get('ir.module.module')
457 mod, xml = xml_id.split('.', 1)
458 return ir_model_data.get_object(cr, uid, mod, xml, context)
460 defaults, groups, modules, others = [], [], [], []
461 for name, field in self._columns.items():
462 if name.startswith('default_') and hasattr(field, 'default_model'):
463 defaults.append((name, field.default_model, name[8:]))
464 elif name.startswith('group_') and isinstance(field, fields.boolean) and hasattr(field, 'implied_group'):
465 field_group = getattr(field, 'group', 'base.group_user')
466 groups.append((name, ref(field_group), ref(field.implied_group)))
467 elif name.startswith('module_') and isinstance(field, fields.boolean):
468 mod_ids = ir_module.search(cr, uid, [('name', '=', name[7:])])
469 modules.append((name, ir_module.browse(cr, uid, mod_ids[0], context)))
473 return {'default': defaults, 'group': groups, 'module': modules, 'other': others}
475 def default_get(self, cr, uid, fields, context=None):
476 ir_values = self.pool.get('ir.values')
477 classified = self._get_classified_fields(cr, uid, context)
479 res = super(res_config_settings, self).default_get(cr, uid, fields, context)
481 # defaults: take the corresponding default value they set
482 for name, model, field in classified['default']:
483 value = ir_values.get_default(cr, uid, model, field)
484 if value is not None:
487 # groups: which groups are implied by the group Employee
488 for name, group, implied_group in classified['group']:
489 res[name] = implied_group in group.implied_ids
491 # modules: which modules are installed/to install
492 for name, module in classified['module']:
493 res[name] = module.state in ('installed', 'to install', 'to upgrade')
495 # other fields: call all methods that start with 'get_default_'
496 for method in dir(self):
497 if method.startswith('get_default_'):
498 res.update(getattr(self, method)(cr, uid, fields, context))
502 def execute(self, cr, uid, ids, context=None):
503 ir_values = self.pool.get('ir.values')
504 ir_model_data = self.pool.get('ir.model.data')
505 ir_module = self.pool.get('ir.module.module')
506 res_groups = self.pool.get('res.groups')
507 classified = self._get_classified_fields(cr, uid, context)
509 config = self.browse(cr, uid, ids[0], context)
511 # default values fields
512 for name, model, field in classified['default']:
513 ir_values.set_default(cr, uid, model, field, config[name])
515 # group fields: modify group / implied groups
516 for name, group, implied_group in classified['group']:
518 group.write({'implied_ids': [(4, implied_group.id)]})
520 group.write({'implied_ids': [(3, implied_group.id)]})
521 implied_group.write({'users': [(3, u.id) for u in group.users]})
523 # other fields: execute all methods that start with 'set_'
524 for method in dir(self):
525 if method.startswith('set_'):
526 getattr(self, method)(cr, uid, ids, context)
528 # module fields: install/uninstall the selected modules
530 to_uninstall_ids = []
531 for name, module in classified['module']:
533 if module.state == 'uninstalled': to_install_ids.append(module.id)
535 if module.state in ('installed','upgrade'): to_uninstall_ids.append(module.id)
537 if to_install_ids or to_uninstall_ids:
538 ir_module.button_uninstall(cr, uid, to_uninstall_ids, context=context)
539 ir_module.button_immediate_install(cr, uid, to_install_ids, context=context)
541 config = self.pool.get('res.config').next(cr, uid, [], context=context) or {}
542 if config.get('type') not in ('ir.actions.act_window_close',):
545 # force client-side reload (update user menu and current view)
547 'type': 'ir.actions.client',
551 def cancel(self, cr, uid, ids, context=None):
552 # ignore the current record, and send the action to reopen the view
553 act_window = self.pool.get('ir.actions.act_window')
554 action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)])
556 return act_window.read(cr, uid, action_ids[0], [], context=context)
559 def name_get(self, cr, uid, ids, context=None):
560 """ Override name_get method to return an appropriate configuration wizard
561 name, and not the generated name."""
565 # name_get may receive int id instead of an id list
566 if isinstance(ids, (int, long)):
569 act_window = self.pool.get('ir.actions.act_window')
570 action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)], context=context)
573 name = act_window.read(cr, uid, action_ids[0], ['name'], context=context)['name']
574 return [(record.id, name) for record in self.browse(cr, uid , ids, context=context)]
576 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: