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__)
34 class res_config_configurable(osv.osv_memory):
35 ''' Base classes for new-style configuration items
37 Configuration items should inherit from this class, implement
38 the execute method (and optionally the cancel one) and have
39 their view inherit from the related res_config_view_base view.
43 def _next_action(self, cr, uid, context=None):
44 Todos = self.pool['ir.actions.todo']
45 _logger.info('getting next %s', Todos)
47 active_todos = Todos.browse(cr, uid,
48 Todos.search(cr, uid, ['&', ('type', '=', 'automatic'), ('state','=','open')]),
51 user_groups = set(map(
53 self.pool['res.users'].browse(cr, uid, [uid], context=context)[0].groups_id))
55 valid_todos_for_user = [
56 todo for todo in active_todos
57 if not todo.groups_id or bool(user_groups.intersection((
58 group.id for group in todo.groups_id)))
61 if valid_todos_for_user:
62 return valid_todos_for_user[0]
66 def _next(self, cr, uid, context=None):
67 _logger.info('getting next operation')
68 next = self._next_action(cr, uid, context=context)
69 _logger.info('next action is %s', next)
71 res = next.action_launch(context=context)
72 res['nodestroy'] = False
74 # reload the client; open the first available root menu
75 menu_obj = self.pool['ir.ui.menu']
76 menu_ids = menu_obj.search(cr, uid, [('parent_id', '=', False)], context=context)
78 'type': 'ir.actions.client',
80 'params': {'menu_id': menu_ids and menu_ids[0] or False},
83 def start(self, cr, uid, ids, context=None):
84 return self.next(cr, uid, ids, context)
86 def next(self, cr, uid, ids, context=None):
87 """ Returns the next todo action to execute (using the default
90 return self._next(cr, uid, context=context)
92 def execute(self, cr, uid, ids, context=None):
93 """ Method called when the user clicks on the ``Next`` button.
95 Execute *must* be overloaded unless ``action_next`` is overloaded
96 (which is something you generally don't need to do).
98 If ``execute`` returns an action dictionary, that action is executed
99 rather than just going to the next configuration item.
101 raise NotImplementedError(
102 'Configuration items need to implement execute')
103 def cancel(self, cr, uid, ids, context=None):
104 """ Method called when the user click on the ``Skip`` button.
106 ``cancel`` should be overloaded instead of ``action_skip``. As with
107 ``execute``, if it returns an action dictionary that action is
108 executed in stead of the default (going to the next configuration item)
110 The default implementation is a NOOP.
112 ``cancel`` is also called by the default implementation of
117 def action_next(self, cr, uid, ids, context=None):
118 """ Action handler for the ``next`` event.
120 Sets the status of the todo the event was sent from to
121 ``done``, calls ``execute`` and -- unless ``execute`` returned
122 an action dictionary -- executes the action provided by calling
125 next = self.execute(cr, uid, ids, context=context)
127 return self.next(cr, uid, ids, context=context)
129 def action_skip(self, cr, uid, ids, context=None):
130 """ Action handler for the ``skip`` event.
132 Sets the status of the todo the event was sent from to
133 ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
134 an action dictionary -- executes the action provided by calling
137 next = self.cancel(cr, uid, ids, context=context)
139 return self.next(cr, uid, ids, context=context)
141 def action_cancel(self, cr, uid, ids, context=None):
142 """ Action handler for the ``cancel`` event. That event isn't
143 generated by the res.config.view.base inheritable view, the
144 inherited view has to overload one of the buttons (or add one
147 Sets the status of the todo the event was sent from to
148 ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
149 an action dictionary -- executes the action provided by calling
152 next = self.cancel(cr, uid, ids, context=context)
154 return self.next(cr, uid, ids, context=context)
156 res_config_configurable()
158 class res_config_installer(osv.osv_memory):
159 """ New-style configuration base specialized for addons selection
165 Subclasses can simply define a number of _columns as
166 fields.boolean objects. The keys (column names) should be the
167 names of the addons to install (when selected). Upon action
168 execution, selected boolean fields (and those only) will be
169 interpreted as addons to install, and batch-installed.
174 It is also possible to require the installation of an additional
175 addon set when a specific preset of addons has been marked for
176 installation (in the basic usage only, additionals can't depend on
179 These additionals are defined through the ``_install_if``
180 property. This property is a mapping of a collection of addons (by
181 name) to a collection of addons (by name) [#]_, and if all the *key*
182 addons are selected for installation, then the *value* ones will
183 be selected as well. For example::
186 ('sale','crm'): ['sale_crm'],
189 This will install the ``sale_crm`` addon if and only if both the
190 ``sale`` and ``crm`` addons are selected for installation.
192 You can define as many additionals as you wish, and additionals
193 can overlap in key and value. For instance::
196 ('sale','crm'): ['sale_crm'],
197 ('sale','project'): ['project_mrp'],
200 will install both ``sale_crm`` and ``project_mrp`` if all of
201 ``sale``, ``crm`` and ``project`` are selected for installation.
206 Subclasses might also need to express dependencies more complex
207 than that provided by additionals. In this case, it's possible to
208 define methods of the form ``_if_%(name)s`` where ``name`` is the
209 name of a boolean field. If the field is selected, then the
210 corresponding module will be marked for installation *and* the
211 hook method will be executed.
213 Hook methods take the usual set of parameters (cr, uid, ids,
214 context) and can return a collection of additional addons to
215 install (if they return anything, otherwise they should not return
216 anything, though returning any "falsy" value such as None or an
217 empty collection will have the same effect).
222 The last hook is to simply overload the ``modules_to_install``
223 method, which implements all the mechanisms above. This method
224 takes the usual set of parameters (cr, uid, ids, context) and
225 returns a ``set`` of addons to install (addons selected by the
226 above methods minus addons from the *basic* set which are already
227 installed) [#]_ so an overloader can simply manipulate the ``set``
228 returned by ``res_config_installer.modules_to_install`` to add or
231 Skipping the installer
232 ----------------------
234 Unless it is removed from the view, installers have a *skip*
235 button which invokes ``action_skip`` (and the ``cancel`` hook from
236 ``res.config``). Hooks and additionals *are not run* when skipping
237 installation, even for already installed addons.
239 Again, setup your hooks accordingly.
241 .. [#] note that since a mapping key needs to be hashable, it's
242 possible to use a tuple or a frozenset, but not a list or a
245 .. [#] because the already-installed modules are only pruned at
246 the very end of ``modules_to_install``, additionals and
247 hooks depending on them *are guaranteed to execute*. Setup
248 your hooks accordingly.
250 _name = 'res.config.installer'
251 _inherit = 'res.config'
255 def already_installed(self, cr, uid, context=None):
256 """ For each module, check if it's already installed and if it
259 :returns: a list of the already installed modules in this
263 return map(attrgetter('name'),
264 self._already_installed(cr, uid, context=context))
266 def _already_installed(self, cr, uid, context=None):
267 """ For each module (boolean fields in a res.config.installer),
268 check if it's already installed (either 'to install', 'to upgrade'
269 or 'installed') and if it is return the module's browse_record
271 :returns: a list of all installed modules in this installer
272 :rtype: [browse_record]
274 modules = self.pool['ir.module.module']
276 selectable = [field for field in self._columns
277 if type(self._columns[field]) is fields.boolean]
278 return modules.browse(
280 modules.search(cr, uid,
281 [('name','in',selectable),
282 ('state','in',['to install', 'installed', 'to upgrade'])],
287 def modules_to_install(self, cr, uid, ids, context=None):
288 """ selects all modules to install:
290 * checked boolean fields
291 * return values of hook methods. Hook methods are of the form
292 ``_if_%(addon_name)s``, and are called if the corresponding
293 addon is marked for installation. They take the arguments
294 cr, uid, ids and context, and return an iterable of addon
296 * additionals, additionals are setup through the ``_install_if``
297 class variable. ``_install_if`` is a dict of {iterable:iterable}
298 where key and value are iterables of addon names.
300 If all the addons in the key are selected for installation
301 (warning: addons added through hooks don't count), then the
302 addons in the value are added to the set of modules to install
303 * not already installed
305 base = set(module_name
306 for installer in self.read(cr, uid, ids, context=context)
307 for module_name, to_install in installer.iteritems()
308 if module_name != 'id'
309 if type(self._columns[module_name]) is fields.boolean
312 hooks_results = set()
314 hook = getattr(self, '_if_%s'% module, None)
316 hooks_results.update(hook(cr, uid, ids, context=None) or set())
319 module for requirements, consequences \
320 in self._install_if.iteritems()
321 if base.issuperset(requirements)
322 for module in consequences)
324 return (base | hooks_results | additionals).difference(
325 self.already_installed(cr, uid, context))
327 def default_get(self, cr, uid, fields_list, context=None):
328 ''' If an addon is already installed, check it by default
330 defaults = super(res_config_installer, self).default_get(
331 cr, uid, fields_list, context=context)
333 return dict(defaults,
335 self.already_installed(cr, uid, context=context),
338 def fields_get(self, cr, uid, fields=None, context=None, write_access=True):
339 """ If an addon is already installed, set it to readonly as
340 res.config.installer doesn't handle uninstallations of already
343 fields = super(res_config_installer, self).fields_get(
344 cr, uid, fields, context, write_access)
346 for name in self.already_installed(cr, uid, context=context):
347 if name not in fields:
351 help= ustr(fields[name].get('help', '')) +
352 _('\n\nThis addon is already installed on your system'))
355 def execute(self, cr, uid, ids, context=None):
356 modules = self.pool['ir.module.module']
357 to_install = list(self.modules_to_install(
358 cr, uid, ids, context=context))
359 _logger.info('Selecting addons %s to install', to_install)
360 modules.state_update(
362 modules.search(cr, uid, [('name','in',to_install)]),
363 'to install', ['uninstalled'], context=context)
365 openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
366 openerp.modules.registry.RegistryManager.new(cr.dbname, update_module=True)
368 res_config_installer()
370 DEPRECATION_MESSAGE = 'You are using an addon using old-style configuration '\
371 'wizards (ir.actions.configuration.wizard). Old-style configuration '\
372 'wizards have been deprecated.\n'\
373 'The addon should be migrated to res.config objects.'
374 class ir_actions_configuration_wizard(osv.osv_memory):
375 ''' Compatibility configuration wizard
377 The old configuration wizard has been replaced by res.config, but in order
378 not to break existing but not-yet-migrated addons, the old wizard was
379 reintegrated and gutted.
381 _name='ir.actions.configuration.wizard'
382 _inherit = 'res.config'
384 def _next_action_note(self, cr, uid, ids, context=None):
385 next = self._next_action(cr, uid)
387 # if the next one is also an old-style extension, you never know...
390 return _("Click 'Continue' to configure the next addon...")
391 return _("Your database is now fully configured.\n\n"\
392 "Click 'Continue' and enjoy your OpenERP experience...")
395 'note': fields.text('Next Wizard', readonly=True),
398 'note': _next_action_note,
401 def execute(self, cr, uid, ids, context=None):
402 _logger.warning(DEPRECATION_MESSAGE)
404 ir_actions_configuration_wizard()
408 class res_config_settings(osv.osv_memory):
409 """ Base configuration wizard for application settings. It provides support for setting
410 default values, assigning groups to employee users, and installing modules.
411 To make such a 'settings' wizard, define a model like::
413 class my_config_wizard(osv.osv_memory):
414 _name = 'my.settings'
415 _inherit = 'res.config.settings'
417 'default_foo': fields.type(..., default_model='my.model'),
418 'group_bar': fields.boolean(..., group='base.group_user', implied_group='my.group'),
419 'module_baz': fields.boolean(...),
420 'other_field': fields.type(...),
423 The method ``execute`` provides some support based on a naming convention:
425 * For a field like 'default_XXX', ``execute`` sets the (global) default value of
426 the field 'XXX' in the model named by ``default_model`` to the field's value.
428 * For a boolean field like 'group_XXX', ``execute`` adds/removes 'implied_group'
429 to/from the implied groups of 'group', depending on the field's value.
430 By default 'group' is the group Employee. Groups are given by their xml id.
432 * For a boolean field like 'module_XXX', ``execute`` triggers the immediate
433 installation of the module named 'XXX' if the field has value ``True``.
435 * For the other fields, the method ``execute`` invokes all methods with a name
436 that starts with 'set_'; such methods can be defined to implement the effect
439 The method ``default_get`` retrieves values that reflect the current status of the
440 fields like 'default_XXX', 'group_XXX' and 'module_XXX'. It also invokes all methods
441 with a name that starts with 'get_default_'; such methods can be defined to provide
442 current values for other fields.
444 _name = 'res.config.settings'
446 def copy(self, cr, uid, id, values, context=None):
447 raise osv.except_osv(_("Cannot duplicate configuration!"), "")
449 def _get_classified_fields(self, cr, uid, context=None):
450 """ return a dictionary with the fields classified by category::
452 { 'default': [('default_foo', 'model', 'foo'), ...],
453 'group': [('group_bar', browse_group, browse_implied_group), ...],
454 'module': [('module_baz', browse_module), ...],
455 'other': ['other_field', ...],
458 ir_model_data = self.pool['ir.model.data']
459 ir_module = self.pool['ir.module.module']
461 mod, xml = xml_id.split('.', 1)
462 return ir_model_data.get_object(cr, uid, mod, xml, context)
464 defaults, groups, modules, others = [], [], [], []
465 for name, field in self._columns.items():
466 if name.startswith('default_') and hasattr(field, 'default_model'):
467 defaults.append((name, field.default_model, name[8:]))
468 elif name.startswith('group_') and isinstance(field, fields.boolean) and hasattr(field, 'implied_group'):
469 field_group = getattr(field, 'group', 'base.group_user')
470 groups.append((name, ref(field_group), ref(field.implied_group)))
471 elif name.startswith('module_') and isinstance(field, fields.boolean):
472 mod_ids = ir_module.search(cr, uid, [('name', '=', name[7:])])
473 record = ir_module.browse(cr, uid, mod_ids[0], context) if mod_ids else None
474 modules.append((name, record))
478 return {'default': defaults, 'group': groups, 'module': modules, 'other': others}
480 def default_get(self, cr, uid, fields, context=None):
481 ir_values = self.pool['ir.values']
482 classified = self._get_classified_fields(cr, uid, context)
484 res = super(res_config_settings, self).default_get(cr, uid, fields, context)
486 # defaults: take the corresponding default value they set
487 for name, model, field in classified['default']:
488 value = ir_values.get_default(cr, uid, model, field)
489 if value is not None:
492 # groups: which groups are implied by the group Employee
493 for name, group, implied_group in classified['group']:
494 res[name] = implied_group in group.implied_ids
496 # modules: which modules are installed/to install
497 for name, module in classified['module']:
498 res[name] = module and module.state in ('installed', 'to install', 'to upgrade')
500 # other fields: call all methods that start with 'get_default_'
501 for method in dir(self):
502 if method.startswith('get_default_'):
503 res.update(getattr(self, method)(cr, uid, fields, context))
507 def execute(self, cr, uid, ids, context=None):
508 ir_values = self.pool['ir.values']
509 ir_module = self.pool['ir.module.module']
510 classified = self._get_classified_fields(cr, uid, context)
512 config = self.browse(cr, uid, ids[0], context)
514 # default values fields
515 for name, model, field in classified['default']:
516 ir_values.set_default(cr, uid, model, field, config[name])
518 # group fields: modify group / implied groups
519 for name, group, implied_group in classified['group']:
521 group.write({'implied_ids': [(4, implied_group.id)]})
523 group.write({'implied_ids': [(3, implied_group.id)]})
524 implied_group.write({'users': [(3, u.id) for u in group.users]})
526 # other fields: execute all methods that start with 'set_'
527 for method in dir(self):
528 if method.startswith('set_'):
529 getattr(self, method)(cr, uid, ids, context)
531 # module fields: install/uninstall the selected modules
532 to_install_missing_names = []
533 to_uninstall_ids = []
536 for name, module in classified['module']:
539 # missing module, will be provided by apps.openerp.com
540 to_install_missing_names.append(name[lm:])
541 elif module.state == 'uninstalled':
542 # local module, to be installed
543 to_install_ids.append(module.id)
545 if module and module.state in ('installed', 'to upgrade'):
546 to_uninstall_ids.append(module.id)
549 ir_module.button_immediate_uninstall(cr, uid, to_uninstall_ids, context=context)
551 ir_module.button_immediate_install(cr, uid, to_install_ids, context=context)
553 if to_install_missing_names:
555 'type': 'ir.actions.client',
557 'params': {'modules': to_install_missing_names},
560 # After the uninstall/install calls, the self.pool is no longer valid.
561 # So we reach into the RegistryManager directly.
562 res_config = openerp.modules.registry.RegistryManager.get(cr.dbname)['res.config']
563 config = res_config.next(cr, uid, [], context=context) or {}
564 if config.get('type') not in ('ir.actions.act_window_close',):
567 # force client-side reload (update user menu and current view)
569 'type': 'ir.actions.client',
573 def cancel(self, cr, uid, ids, context=None):
574 # ignore the current record, and send the action to reopen the view
575 act_window = self.pool['ir.actions.act_window']
576 action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)])
578 return act_window.read(cr, uid, action_ids[0], [], context=context)
581 def name_get(self, cr, uid, ids, context=None):
582 """ Override name_get method to return an appropriate configuration wizard
583 name, and not the generated name."""
587 # name_get may receive int id instead of an id list
588 if isinstance(ids, (int, long)):
591 act_window = self.pool['ir.actions.act_window']
592 action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)], context=context)
595 name = act_window.read(cr, uid, action_ids[0], ['name'], context=context)['name']
596 return [(record.id, name) for record in self.browse(cr, uid , ids, context=context)]
598 def get_option_path(self, cr, uid, menu_xml_id, context=None):
600 Fetch the path to a specified configuration view and the action id to access it.
602 :param string menu_xml_id: the xml id of the menuitem where the view is located,
603 structured as follows: module_name.menuitem_xml_id (e.g.: "base.menu_sale_config")
605 - t[0]: string: full path to the menuitem (e.g.: "Settings/Configuration/Sales")
606 - t[1]: long: id of the menuitem's action
608 module_name, menu_xml_id = menu_xml_id.split('.')
609 dummy, menu_id = self.pool['ir.model.data'].get_object_reference(cr, uid, module_name, menu_xml_id)
610 ir_ui_menu = self.pool['ir.ui.menu'].browse(cr, uid, menu_id, context=context)
612 return (ir_ui_menu.complete_name, ir_ui_menu.action.id)
614 def get_option_name(self, cr, uid, full_field_name, context=None):
616 Fetch the human readable name of a specified configuration option.
618 :param string full_field_name: the full name of the field, structured as follows:
619 model_name.field_name (e.g.: "sale.config.settings.fetchmail_lead")
620 :return string: human readable name of the field (e.g.: "Create leads from incoming mails")
622 model_name, field_name = full_field_name.rsplit('.', 1)
624 return self.pool[model_name].fields_get(cr, uid, allfields=[field_name], context=context)[field_name]['string']
626 def get_config_warning(self, cr, msg, context=None):
628 Helper: return a Warning exception with the given message where the %(field:xxx)s
629 and/or %(menu:yyy)s are replaced by the human readable field's name and/or menuitem's
634 Just include in your error message %(field:model_name.field_name)s to obtain the human
635 readable field's name, and/or %(menu:module_name.menuitem_xml_id)s to obtain the menuitem's
640 from openerp.addons.base.res.res_config import get_warning_config
641 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)
643 This will return an exception containing the following message:
644 Error: this action is prohibited. You should check the field Create leads from incoming mails in Settings/Configuration/Sales.
646 What if there is another substitution in the message already?
647 -------------------------------------------------------------
648 You could have a situation where the error message you want to upgrade already contains a substitution. Example:
649 Cannot find any account journal of %s type for this company.\n\nYou can create one in the menu: \nConfiguration\Journals\Journals.
650 What you want to do here is simply to replace the path by %menu:account.menu_account_config)s, and leave the rest alone.
651 In order to do that, you can use the double percent (%%) to escape your new substitution, like so:
652 Cannot find any account journal of %s type for this company.\n\nYou can create one in the %%(menu:account.menu_account_config)s.
655 res_config_obj = openerp.registry(cr.dbname)['res.config.settings']
656 regex_path = r'%\(((?:menu|field):[a-z_\.]*)\)s'
658 # Process the message
659 # 1/ find the menu and/or field references, put them in a list
660 references = re.findall(regex_path, msg, flags=re.I)
662 # 2/ fetch the menu and/or field replacement values (full path and
663 # human readable field's name) and the action_id if any
666 for item in references:
667 ref_type, ref = item.split(':')
668 if ref_type == 'menu':
669 values[item], action_id = res_config_obj.get_option_path(cr, SUPERUSER_ID, ref, context=context)
670 elif ref_type == 'field':
671 values[item] = res_config_obj.get_option_name(cr, SUPERUSER_ID, ref, context=context)
673 # 3/ substitute and return the result
675 return exceptions.RedirectWarning(msg % values, action_id, _('Go to the configuration panel'))
676 return exceptions.Warning(msg % values)
677 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: