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
26 from openerp import SUPERUSER_ID
27 from openerp.osv import osv, fields
28 from openerp.tools import ustr
29 from openerp.tools.translate import _
30 from openerp import exceptions
32 _logger = logging.getLogger(__name__)
35 class res_config_module_installation_mixin(object):
36 def _install_modules(self, cr, uid, modules, context):
37 """Install the requested modules.
38 return the next action to execute
40 modules is a list of tuples
41 (mod_name, browse_record | None)
43 ir_module = self.pool.get('ir.module.module')
45 to_install_missing_names = []
47 for name, module in modules:
49 to_install_missing_names.append(name)
50 elif module.state == 'uninstalled':
51 to_install_ids.append(module.id)
54 ir_module.button_immediate_install(cr, uid, to_install_ids, context=context)
56 if to_install_missing_names:
58 'type': 'ir.actions.client',
60 'params': {'modules': to_install_missing_names},
65 class res_config_configurable(osv.osv_memory):
66 ''' Base classes for new-style configuration items
68 Configuration items should inherit from this class, implement
69 the execute method (and optionally the cancel one) and have
70 their view inherit from the related res_config_view_base view.
74 def _next_action(self, cr, uid, context=None):
75 Todos = self.pool['ir.actions.todo']
76 _logger.info('getting next %s', Todos)
78 active_todos = Todos.browse(cr, uid,
79 Todos.search(cr, uid, ['&', ('type', '=', 'automatic'), ('state','=','open')]),
82 user_groups = set(map(
84 self.pool['res.users'].browse(cr, uid, [uid], context=context)[0].groups_id))
86 valid_todos_for_user = [
87 todo for todo in active_todos
88 if not todo.groups_id or bool(user_groups.intersection((
89 group.id for group in todo.groups_id)))
92 if valid_todos_for_user:
93 return valid_todos_for_user[0]
97 def _next(self, cr, uid, context=None):
98 _logger.info('getting next operation')
99 next = self._next_action(cr, uid, context=context)
100 _logger.info('next action is %s', next)
102 res = next.action_launch(context=context)
103 res['nodestroy'] = False
105 # reload the client; open the first available root menu
106 menu_obj = self.pool['ir.ui.menu']
107 menu_ids = menu_obj.search(cr, uid, [('parent_id', '=', False)], context=context)
109 'type': 'ir.actions.client',
111 'params': {'menu_id': menu_ids and menu_ids[0] or False},
114 def start(self, cr, uid, ids, context=None):
115 return self.next(cr, uid, ids, context)
117 def next(self, cr, uid, ids, context=None):
118 """ Returns the next todo action to execute (using the default
121 return self._next(cr, uid, context=context)
123 def execute(self, cr, uid, ids, context=None):
124 """ Method called when the user clicks on the ``Next`` button.
126 Execute *must* be overloaded unless ``action_next`` is overloaded
127 (which is something you generally don't need to do).
129 If ``execute`` returns an action dictionary, that action is executed
130 rather than just going to the next configuration item.
132 raise NotImplementedError(
133 'Configuration items need to implement execute')
134 def cancel(self, cr, uid, ids, context=None):
135 """ Method called when the user click on the ``Skip`` button.
137 ``cancel`` should be overloaded instead of ``action_skip``. As with
138 ``execute``, if it returns an action dictionary that action is
139 executed in stead of the default (going to the next configuration item)
141 The default implementation is a NOOP.
143 ``cancel`` is also called by the default implementation of
148 def action_next(self, cr, uid, ids, context=None):
149 """ Action handler for the ``next`` event.
151 Sets the status of the todo the event was sent from to
152 ``done``, calls ``execute`` and -- unless ``execute`` returned
153 an action dictionary -- executes the action provided by calling
156 next = self.execute(cr, uid, ids, context=context)
158 return self.next(cr, uid, ids, context=context)
160 def action_skip(self, cr, uid, ids, context=None):
161 """ Action handler for the ``skip`` event.
163 Sets the status of the todo the event was sent from to
164 ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
165 an action dictionary -- executes the action provided by calling
168 next = self.cancel(cr, uid, ids, context=context)
170 return self.next(cr, uid, ids, context=context)
172 def action_cancel(self, cr, uid, ids, context=None):
173 """ Action handler for the ``cancel`` event. That event isn't
174 generated by the res.config.view.base inheritable view, the
175 inherited view has to overload one of the buttons (or add one
178 Sets the status of the todo the event was sent from to
179 ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
180 an action dictionary -- executes the action provided by calling
183 next = self.cancel(cr, uid, ids, context=context)
185 return self.next(cr, uid, ids, context=context)
187 res_config_configurable()
189 class res_config_installer(osv.osv_memory, res_config_module_installation_mixin):
190 """ New-style configuration base specialized for addons selection
196 Subclasses can simply define a number of _columns as
197 fields.boolean objects. The keys (column names) should be the
198 names of the addons to install (when selected). Upon action
199 execution, selected boolean fields (and those only) will be
200 interpreted as addons to install, and batch-installed.
205 It is also possible to require the installation of an additional
206 addon set when a specific preset of addons has been marked for
207 installation (in the basic usage only, additionals can't depend on
210 These additionals are defined through the ``_install_if``
211 property. This property is a mapping of a collection of addons (by
212 name) to a collection of addons (by name) [#]_, and if all the *key*
213 addons are selected for installation, then the *value* ones will
214 be selected as well. For example::
217 ('sale','crm'): ['sale_crm'],
220 This will install the ``sale_crm`` addon if and only if both the
221 ``sale`` and ``crm`` addons are selected for installation.
223 You can define as many additionals as you wish, and additionals
224 can overlap in key and value. For instance::
227 ('sale','crm'): ['sale_crm'],
228 ('sale','project'): ['project_mrp'],
231 will install both ``sale_crm`` and ``project_mrp`` if all of
232 ``sale``, ``crm`` and ``project`` are selected for installation.
237 Subclasses might also need to express dependencies more complex
238 than that provided by additionals. In this case, it's possible to
239 define methods of the form ``_if_%(name)s`` where ``name`` is the
240 name of a boolean field. If the field is selected, then the
241 corresponding module will be marked for installation *and* the
242 hook method will be executed.
244 Hook methods take the usual set of parameters (cr, uid, ids,
245 context) and can return a collection of additional addons to
246 install (if they return anything, otherwise they should not return
247 anything, though returning any "falsy" value such as None or an
248 empty collection will have the same effect).
253 The last hook is to simply overload the ``modules_to_install``
254 method, which implements all the mechanisms above. This method
255 takes the usual set of parameters (cr, uid, ids, context) and
256 returns a ``set`` of addons to install (addons selected by the
257 above methods minus addons from the *basic* set which are already
258 installed) [#]_ so an overloader can simply manipulate the ``set``
259 returned by ``res_config_installer.modules_to_install`` to add or
262 Skipping the installer
263 ----------------------
265 Unless it is removed from the view, installers have a *skip*
266 button which invokes ``action_skip`` (and the ``cancel`` hook from
267 ``res.config``). Hooks and additionals *are not run* when skipping
268 installation, even for already installed addons.
270 Again, setup your hooks accordingly.
272 .. [#] note that since a mapping key needs to be hashable, it's
273 possible to use a tuple or a frozenset, but not a list or a
276 .. [#] because the already-installed modules are only pruned at
277 the very end of ``modules_to_install``, additionals and
278 hooks depending on them *are guaranteed to execute*. Setup
279 your hooks accordingly.
281 _name = 'res.config.installer'
282 _inherit = 'res.config'
286 def already_installed(self, cr, uid, context=None):
287 """ For each module, check if it's already installed and if it
290 :returns: a list of the already installed modules in this
294 return map(attrgetter('name'),
295 self._already_installed(cr, uid, context=context))
297 def _already_installed(self, cr, uid, context=None):
298 """ For each module (boolean fields in a res.config.installer),
299 check if it's already installed (either 'to install', 'to upgrade'
300 or 'installed') and if it is return the module's browse_record
302 :returns: a list of all installed modules in this installer
303 :rtype: [browse_record]
305 modules = self.pool['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).difference(
356 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 self.already_installed(cr, uid, context=context),
369 def fields_get(self, cr, uid, fields=None, context=None, write_access=True):
370 """ If an addon is already installed, set it to readonly as
371 res.config.installer doesn't handle uninstallations of already
374 fields = super(res_config_installer, self).fields_get(
375 cr, uid, fields, context, write_access)
377 for name in self.already_installed(cr, uid, context=context):
378 if name not in fields:
382 help= ustr(fields[name].get('help', '')) +
383 _('\n\nThis addon is already installed on your system'))
386 def execute(self, cr, uid, ids, context=None):
387 to_install = list(self.modules_to_install(
388 cr, uid, ids, context=context))
389 _logger.info('Selecting addons %s to install', to_install)
391 ir_module = self.pool.get('ir.module.module')
393 for name in to_install:
394 mod_ids = ir_module.search(cr, uid, [('name', '=', name)])
395 record = ir_module.browse(cr, uid, mod_ids[0], context) if mod_ids else None
396 modules.append((name, record))
398 return self._install_modules(cr, uid, modules, context=context)
400 res_config_installer()
402 DEPRECATION_MESSAGE = 'You are using an addon using old-style configuration '\
403 'wizards (ir.actions.configuration.wizard). Old-style configuration '\
404 'wizards have been deprecated.\n'\
405 'The addon should be migrated to res.config objects.'
406 class ir_actions_configuration_wizard(osv.osv_memory):
407 ''' Compatibility configuration wizard
409 The old configuration wizard has been replaced by res.config, but in order
410 not to break existing but not-yet-migrated addons, the old wizard was
411 reintegrated and gutted.
413 _name='ir.actions.configuration.wizard'
414 _inherit = 'res.config'
416 def _next_action_note(self, cr, uid, ids, context=None):
417 next = self._next_action(cr, uid)
419 # if the next one is also an old-style extension, you never know...
422 return _("Click 'Continue' to configure the next addon...")
423 return _("Your database is now fully configured.\n\n"\
424 "Click 'Continue' and enjoy your OpenERP experience...")
427 'note': fields.text('Next Wizard', readonly=True),
430 'note': _next_action_note,
433 def execute(self, cr, uid, ids, context=None):
434 _logger.warning(DEPRECATION_MESSAGE)
436 ir_actions_configuration_wizard()
439 class res_config_settings(osv.osv_memory, res_config_module_installation_mixin):
440 """ Base configuration wizard for application settings. It provides support for setting
441 default values, assigning groups to employee users, and installing modules.
442 To make such a 'settings' wizard, define a model like::
444 class my_config_wizard(osv.osv_memory):
445 _name = 'my.settings'
446 _inherit = 'res.config.settings'
448 'default_foo': fields.type(..., default_model='my.model'),
449 'group_bar': fields.boolean(..., group='base.group_user', implied_group='my.group'),
450 'module_baz': fields.boolean(...),
451 'other_field': fields.type(...),
454 The method ``execute`` provides some support based on a naming convention:
456 * For a field like 'default_XXX', ``execute`` sets the (global) default value of
457 the field 'XXX' in the model named by ``default_model`` to the field's value.
459 * For a boolean field like 'group_XXX', ``execute`` adds/removes 'implied_group'
460 to/from the implied groups of 'group', depending on the field's value.
461 By default 'group' is the group Employee. Groups are given by their xml id.
463 * For a boolean field like 'module_XXX', ``execute`` triggers the immediate
464 installation of the module named 'XXX' if the field has value ``True``.
466 * For the other fields, the method ``execute`` invokes all methods with a name
467 that starts with 'set_'; such methods can be defined to implement the effect
470 The method ``default_get`` retrieves values that reflect the current status of the
471 fields like 'default_XXX', 'group_XXX' and 'module_XXX'. It also invokes all methods
472 with a name that starts with 'get_default_'; such methods can be defined to provide
473 current values for other fields.
475 _name = 'res.config.settings'
477 def copy(self, cr, uid, id, values, context=None):
478 raise osv.except_osv(_("Cannot duplicate configuration!"), "")
480 def _get_classified_fields(self, cr, uid, context=None):
481 """ return a dictionary with the fields classified by category::
483 { 'default': [('default_foo', 'model', 'foo'), ...],
484 'group': [('group_bar', browse_group, browse_implied_group), ...],
485 'module': [('module_baz', browse_module), ...],
486 'other': ['other_field', ...],
489 ir_model_data = self.pool['ir.model.data']
490 ir_module = self.pool['ir.module.module']
492 mod, xml = xml_id.split('.', 1)
493 return ir_model_data.get_object(cr, uid, mod, xml, context)
495 defaults, groups, modules, others = [], [], [], []
496 for name, field in self._columns.items():
497 if name.startswith('default_') and hasattr(field, 'default_model'):
498 defaults.append((name, field.default_model, name[8:]))
499 elif name.startswith('group_') and isinstance(field, fields.boolean) and hasattr(field, 'implied_group'):
500 field_group = getattr(field, 'group', 'base.group_user')
501 groups.append((name, ref(field_group), ref(field.implied_group)))
502 elif name.startswith('module_') and isinstance(field, fields.boolean):
503 mod_ids = ir_module.search(cr, uid, [('name', '=', name[7:])])
504 record = ir_module.browse(cr, uid, mod_ids[0], context) if mod_ids else None
505 modules.append((name, record))
509 return {'default': defaults, 'group': groups, 'module': modules, 'other': others}
511 def default_get(self, cr, uid, fields, context=None):
512 ir_values = self.pool['ir.values']
513 classified = self._get_classified_fields(cr, uid, context)
515 res = super(res_config_settings, self).default_get(cr, uid, fields, context)
517 # defaults: take the corresponding default value they set
518 for name, model, field in classified['default']:
519 value = ir_values.get_default(cr, uid, model, field)
520 if value is not None:
523 # groups: which groups are implied by the group Employee
524 for name, group, implied_group in classified['group']:
525 res[name] = implied_group in group.implied_ids
527 # modules: which modules are installed/to install
528 for name, module in classified['module']:
529 res[name] = module and module.state in ('installed', 'to install', 'to upgrade')
531 # other fields: call all methods that start with 'get_default_'
532 for method in dir(self):
533 if method.startswith('get_default_'):
534 res.update(getattr(self, method)(cr, uid, fields, context))
538 def execute(self, cr, uid, ids, context=None):
539 ir_values = self.pool['ir.values']
540 ir_module = self.pool['ir.module.module']
541 classified = self._get_classified_fields(cr, uid, context)
543 config = self.browse(cr, uid, ids[0], context)
545 # default values fields
546 for name, model, field in classified['default']:
547 ir_values.set_default(cr, uid, model, field, config[name])
549 # group fields: modify group / implied groups
550 for name, group, implied_group in classified['group']:
552 group.write({'implied_ids': [(4, implied_group.id)]})
554 group.write({'implied_ids': [(3, implied_group.id)]})
555 implied_group.write({'users': [(3, u.id) for u in group.users]})
557 # other fields: execute all methods that start with 'set_'
558 for method in dir(self):
559 if method.startswith('set_'):
560 getattr(self, method)(cr, uid, ids, context)
562 # module fields: install/uninstall the selected modules
564 to_uninstall_ids = []
566 for name, module in classified['module']:
568 to_install.append((name[lm:], module))
570 if module and module.state in ('installed', 'to upgrade'):
571 to_uninstall_ids.append(module.id)
574 ir_module.button_immediate_uninstall(cr, uid, to_uninstall_ids, context=context)
576 action = self._install_modules(cr, uid, to_install, context=context)
580 # After the uninstall/install calls, the self.pool is no longer valid.
581 # So we reach into the RegistryManager directly.
582 res_config = openerp.modules.registry.RegistryManager.get(cr.dbname)['res.config']
583 config = res_config.next(cr, uid, [], context=context) or {}
584 if config.get('type') not in ('ir.actions.act_window_close',):
587 # force client-side reload (update user menu and current view)
589 'type': 'ir.actions.client',
593 def cancel(self, cr, uid, ids, context=None):
594 # ignore the current record, and send the action to reopen the view
595 act_window = self.pool['ir.actions.act_window']
596 action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)])
598 return act_window.read(cr, uid, action_ids[0], [], context=context)
601 def name_get(self, cr, uid, ids, context=None):
602 """ Override name_get method to return an appropriate configuration wizard
603 name, and not the generated name."""
607 # name_get may receive int id instead of an id list
608 if isinstance(ids, (int, long)):
611 act_window = self.pool['ir.actions.act_window']
612 action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)], context=context)
615 name = act_window.read(cr, uid, action_ids[0], ['name'], context=context)['name']
616 return [(record.id, name) for record in self.browse(cr, uid , ids, context=context)]
618 def get_option_path(self, cr, uid, menu_xml_id, context=None):
620 Fetch the path to a specified configuration view and the action id to access it.
622 :param string menu_xml_id: the xml id of the menuitem where the view is located,
623 structured as follows: module_name.menuitem_xml_id (e.g.: "base.menu_sale_config")
625 - t[0]: string: full path to the menuitem (e.g.: "Settings/Configuration/Sales")
626 - t[1]: long: id of the menuitem's action
628 module_name, menu_xml_id = menu_xml_id.split('.')
629 dummy, menu_id = self.pool['ir.model.data'].get_object_reference(cr, uid, module_name, menu_xml_id)
630 ir_ui_menu = self.pool['ir.ui.menu'].browse(cr, uid, menu_id, context=context)
632 return (ir_ui_menu.complete_name, ir_ui_menu.action.id)
634 def get_option_name(self, cr, uid, full_field_name, context=None):
636 Fetch the human readable name of a specified configuration option.
638 :param string full_field_name: the full name of the field, structured as follows:
639 model_name.field_name (e.g.: "sale.config.settings.fetchmail_lead")
640 :return string: human readable name of the field (e.g.: "Create leads from incoming mails")
642 model_name, field_name = full_field_name.rsplit('.', 1)
644 return self.pool[model_name].fields_get(cr, uid, allfields=[field_name], context=context)[field_name]['string']
646 def get_config_warning(self, cr, msg, context=None):
648 Helper: return a Warning exception with the given message where the %(field:xxx)s
649 and/or %(menu:yyy)s are replaced by the human readable field's name and/or menuitem's
654 Just include in your error message %(field:model_name.field_name)s to obtain the human
655 readable field's name, and/or %(menu:module_name.menuitem_xml_id)s to obtain the menuitem's
660 from openerp.addons.base.res.res_config import get_warning_config
661 raise get_warning_config(cr, _("Error: this action is prohibited. You should check the field %(field:sale.config.settings.fetchmail_lead)s in %(menu:base.menu_sale_config)s."), context=context)
663 This will return an exception containing the following message:
664 Error: this action is prohibited. You should check the field Create leads from incoming mails in Settings/Configuration/Sales.
666 What if there is another substitution in the message already?
667 -------------------------------------------------------------
668 You could have a situation where the error message you want to upgrade already contains a substitution. Example:
669 Cannot find any account journal of %s type for this company.\n\nYou can create one in the menu: \nConfiguration\Journals\Journals.
670 What you want to do here is simply to replace the path by %menu:account.menu_account_config)s, and leave the rest alone.
671 In order to do that, you can use the double percent (%%) to escape your new substitution, like so:
672 Cannot find any account journal of %s type for this company.\n\nYou can create one in the %%(menu:account.menu_account_config)s.
675 res_config_obj = openerp.registry(cr.dbname)['res.config.settings']
676 regex_path = r'%\(((?:menu|field):[a-z_\.]*)\)s'
678 # Process the message
679 # 1/ find the menu and/or field references, put them in a list
680 references = re.findall(regex_path, msg, flags=re.I)
682 # 2/ fetch the menu and/or field replacement values (full path and
683 # human readable field's name) and the action_id if any
686 for item in references:
687 ref_type, ref = item.split(':')
688 if ref_type == 'menu':
689 values[item], action_id = res_config_obj.get_option_path(cr, SUPERUSER_ID, ref, context=context)
690 elif ref_type == 'field':
691 values[item] = res_config_obj.get_option_name(cr, SUPERUSER_ID, ref, context=context)
693 # 3/ substitute and return the result
695 return exceptions.RedirectWarning(msg % values, action_id, _('Go to the configuration panel'))
696 return exceptions.Warning(msg % values)
697 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: