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
31 from lxml import etree
33 _logger = logging.getLogger(__name__)
36 class res_config_module_installation_mixin(object):
37 def _install_modules(self, cr, uid, modules, context):
38 """Install the requested modules.
39 return the next action to execute
41 modules is a list of tuples
42 (mod_name, browse_record | None)
44 ir_module = self.pool.get('ir.module.module')
46 to_install_missing_names = []
48 for name, module in modules:
50 to_install_missing_names.append(name)
51 elif module.state == 'uninstalled':
52 to_install_ids.append(module.id)
55 result = ir_module.button_immediate_install(cr, uid, to_install_ids, context=context)
56 #FIXME: if result is not none, the corresponding todo will be skipped because it was just marked done
57 if to_install_missing_names:
59 'type': 'ir.actions.client',
61 'params': {'modules': to_install_missing_names},
66 class res_config_configurable(osv.osv_memory):
67 ''' Base classes for new-style configuration items
69 Configuration items should inherit from this class, implement
70 the execute method (and optionally the cancel one) and have
71 their view inherit from the related res_config_view_base view.
75 def _next_action(self, cr, uid, context=None):
76 Todos = self.pool['ir.actions.todo']
77 _logger.info('getting next %s', Todos)
79 active_todos = Todos.browse(cr, uid,
80 Todos.search(cr, uid, ['&', ('type', '=', 'automatic'), ('state','=','open')]),
83 user_groups = set(map(
85 self.pool['res.users'].browse(cr, uid, [uid], context=context)[0].groups_id))
87 valid_todos_for_user = [
88 todo for todo in active_todos
89 if not todo.groups_id or bool(user_groups.intersection((
90 group.id for group in todo.groups_id)))
93 if valid_todos_for_user:
94 return valid_todos_for_user[0]
98 def _next(self, cr, uid, context=None):
99 _logger.info('getting next operation')
100 next = self._next_action(cr, uid, context=context)
101 _logger.info('next action is %s', next)
103 res = next.action_launch(context=context)
104 res['nodestroy'] = False
108 'type': 'ir.actions.client',
112 def start(self, cr, uid, ids, context=None):
113 return self.next(cr, uid, ids, context)
115 def next(self, cr, uid, ids, context=None):
116 """ Returns the next todo action to execute (using the default
119 return self._next(cr, uid, context=context)
121 def execute(self, cr, uid, ids, context=None):
122 """ Method called when the user clicks on the ``Next`` button.
124 Execute *must* be overloaded unless ``action_next`` is overloaded
125 (which is something you generally don't need to do).
127 If ``execute`` returns an action dictionary, that action is executed
128 rather than just going to the next configuration item.
130 raise NotImplementedError(
131 'Configuration items need to implement execute')
133 def cancel(self, cr, uid, ids, context=None):
134 """ Method called when the user click on the ``Skip`` button.
136 ``cancel`` should be overloaded instead of ``action_skip``. As with
137 ``execute``, if it returns an action dictionary that action is
138 executed in stead of the default (going to the next configuration item)
140 The default implementation is a NOOP.
142 ``cancel`` is also called by the default implementation of
147 def action_next(self, cr, uid, ids, context=None):
148 """ Action handler for the ``next`` event.
150 Sets the status of the todo the event was sent from to
151 ``done``, calls ``execute`` and -- unless ``execute`` returned
152 an action dictionary -- executes the action provided by calling
155 next = self.execute(cr, uid, ids, context=context)
157 return self.next(cr, uid, ids, context=context)
159 def action_skip(self, cr, uid, ids, context=None):
160 """ Action handler for the ``skip`` event.
162 Sets the status of the todo the event was sent from to
163 ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
164 an action dictionary -- executes the action provided by calling
167 next = self.cancel(cr, uid, ids, context=context)
169 return self.next(cr, uid, ids, context=context)
171 def action_cancel(self, cr, uid, ids, context=None):
172 """ Action handler for the ``cancel`` event. That event isn't
173 generated by the res.config.view.base inheritable view, the
174 inherited view has to overload one of the buttons (or add one
177 Sets the status of the todo the event was sent from to
178 ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
179 an action dictionary -- executes the action provided by calling
182 next = self.cancel(cr, uid, ids, context=context)
184 return self.next(cr, uid, ids, context=context)
186 class res_config_installer(osv.osv_memory, res_config_module_installation_mixin):
187 """ New-style configuration base specialized for addons selection
193 Subclasses can simply define a number of _columns as
194 fields.boolean objects. The keys (column names) should be the
195 names of the addons to install (when selected). Upon action
196 execution, selected boolean fields (and those only) will be
197 interpreted as addons to install, and batch-installed.
202 It is also possible to require the installation of an additional
203 addon set when a specific preset of addons has been marked for
204 installation (in the basic usage only, additionals can't depend on
207 These additionals are defined through the ``_install_if``
208 property. This property is a mapping of a collection of addons (by
209 name) to a collection of addons (by name) [#]_, and if all the *key*
210 addons are selected for installation, then the *value* ones will
211 be selected as well. For example::
214 ('sale','crm'): ['sale_crm'],
217 This will install the ``sale_crm`` addon if and only if both the
218 ``sale`` and ``crm`` addons are selected for installation.
220 You can define as many additionals as you wish, and additionals
221 can overlap in key and value. For instance::
224 ('sale','crm'): ['sale_crm'],
225 ('sale','project'): ['sale_service'],
228 will install both ``sale_crm`` and ``sale_service`` if all of
229 ``sale``, ``crm`` and ``project`` are selected for installation.
234 Subclasses might also need to express dependencies more complex
235 than that provided by additionals. In this case, it's possible to
236 define methods of the form ``_if_%(name)s`` where ``name`` is the
237 name of a boolean field. If the field is selected, then the
238 corresponding module will be marked for installation *and* the
239 hook method will be executed.
241 Hook methods take the usual set of parameters (cr, uid, ids,
242 context) and can return a collection of additional addons to
243 install (if they return anything, otherwise they should not return
244 anything, though returning any "falsy" value such as None or an
245 empty collection will have the same effect).
250 The last hook is to simply overload the ``modules_to_install``
251 method, which implements all the mechanisms above. This method
252 takes the usual set of parameters (cr, uid, ids, context) and
253 returns a ``set`` of addons to install (addons selected by the
254 above methods minus addons from the *basic* set which are already
255 installed) [#]_ so an overloader can simply manipulate the ``set``
256 returned by ``res_config_installer.modules_to_install`` to add or
259 Skipping the installer
260 ----------------------
262 Unless it is removed from the view, installers have a *skip*
263 button which invokes ``action_skip`` (and the ``cancel`` hook from
264 ``res.config``). Hooks and additionals *are not run* when skipping
265 installation, even for already installed addons.
267 Again, setup your hooks accordingly.
269 .. [#] note that since a mapping key needs to be hashable, it's
270 possible to use a tuple or a frozenset, but not a list or a
273 .. [#] because the already-installed modules are only pruned at
274 the very end of ``modules_to_install``, additionals and
275 hooks depending on them *are guaranteed to execute*. Setup
276 your hooks accordingly.
278 _name = 'res.config.installer'
279 _inherit = 'res.config'
283 def already_installed(self, cr, uid, context=None):
284 """ For each module, check if it's already installed and if it
287 :returns: a list of the already installed modules in this
291 return map(attrgetter('name'),
292 self._already_installed(cr, uid, context=context))
294 def _already_installed(self, cr, uid, context=None):
295 """ For each module (boolean fields in a res.config.installer),
296 check if it's already installed (either 'to install', 'to upgrade'
297 or 'installed') and if it is return the module's record
299 :returns: a list of all installed modules in this installer
300 :rtype: recordset (collection of Record)
302 modules = self.pool['ir.module.module']
304 selectable = [field for field in self._columns
305 if type(self._columns[field]) is fields.boolean]
306 return modules.browse(
308 modules.search(cr, uid,
309 [('name','in',selectable),
310 ('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.get(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 class res_config_settings(osv.osv_memory, res_config_module_installation_mixin):
397 """ Base configuration wizard for application settings. It provides support for setting
398 default values, assigning groups to employee users, and installing modules.
399 To make such a 'settings' wizard, define a model like::
401 class my_config_wizard(osv.osv_memory):
402 _name = 'my.settings'
403 _inherit = 'res.config.settings'
405 'default_foo': fields.type(..., default_model='my.model'),
406 'group_bar': fields.boolean(..., group='base.group_user', implied_group='my.group'),
407 'module_baz': fields.boolean(...),
408 'other_field': fields.type(...),
411 The method ``execute`` provides some support based on a naming convention:
413 * For a field like 'default_XXX', ``execute`` sets the (global) default value of
414 the field 'XXX' in the model named by ``default_model`` to the field's value.
416 * For a boolean field like 'group_XXX', ``execute`` adds/removes 'implied_group'
417 to/from the implied groups of 'group', depending on the field's value.
418 By default 'group' is the group Employee. Groups are given by their xml id.
419 The attribute 'group' may contain several xml ids, separated by commas.
421 * For a boolean field like 'module_XXX', ``execute`` triggers the immediate
422 installation of the module named 'XXX' if the field has value ``True``.
424 * For the other fields, the method ``execute`` invokes all methods with a name
425 that starts with 'set_'; such methods can be defined to implement the effect
428 The method ``default_get`` retrieves values that reflect the current status of the
429 fields like 'default_XXX', 'group_XXX' and 'module_XXX'. It also invokes all methods
430 with a name that starts with 'get_default_'; such methods can be defined to provide
431 current values for other fields.
433 _name = 'res.config.settings'
435 def copy(self, cr, uid, id, values, context=None):
436 raise osv.except_osv(_("Cannot duplicate configuration!"), "")
438 def fields_view_get(self, cr, user, view_id=None, view_type='form',
439 context=None, toolbar=False, submenu=False):
440 ret_val = super(res_config_settings, self).fields_view_get(
441 cr, user, view_id=view_id, view_type=view_type, context=context,
442 toolbar=toolbar, submenu=submenu)
444 doc = etree.XML(ret_val['arch'])
446 for field in ret_val['fields']:
447 if not field.startswith("module_"):
449 for node in doc.xpath("//field[@name='%s']" % field):
450 if 'on_change' not in node.attrib:
451 node.set("on_change",
452 "onchange_module(%s, '%s')" % (field, field))
454 ret_val['arch'] = etree.tostring(doc)
457 def onchange_module(self, cr, uid, ids, field_value, module_name, context={}):
458 module_pool = self.pool.get('ir.module.module')
459 module_ids = module_pool.search(
460 cr, uid, [('name', '=', module_name.replace("module_", '')),
461 ('state','in', ['to install', 'installed', 'to upgrade'])],
464 if module_ids and not field_value:
465 dep_ids = module_pool.downstream_dependencies(cr, uid, module_ids, context=context)
466 dep_name = [x.shortdesc for x in module_pool.browse(
467 cr, uid, dep_ids + module_ids, context=context)]
468 message = '\n'.join(dep_name)
469 return {'warning': {'title': _('Warning!'),
471 _('Disabling this option will also uninstall the following modules \n%s' % message)
475 def _get_classified_fields(self, cr, uid, context=None):
476 """ return a dictionary with the fields classified by category::
478 { 'default': [('default_foo', 'model', 'foo'), ...],
479 'group': [('group_bar', [browse_group], browse_implied_group), ...],
480 'module': [('module_baz', browse_module), ...],
481 'other': ['other_field', ...],
484 ir_model_data = self.pool['ir.model.data']
485 ir_module = self.pool['ir.module.module']
487 mod, xml = xml_id.split('.', 1)
488 return ir_model_data.get_object(cr, uid, mod, xml, context=context)
490 defaults, groups, modules, others = [], [], [], []
491 for name, field in self._columns.items():
492 if name.startswith('default_') and hasattr(field, 'default_model'):
493 defaults.append((name, field.default_model, name[8:]))
494 elif name.startswith('group_') and isinstance(field, fields.boolean) and hasattr(field, 'implied_group'):
495 field_groups = getattr(field, 'group', 'base.group_user').split(',')
496 groups.append((name, map(ref, field_groups), ref(field.implied_group)))
497 elif name.startswith('module_') and isinstance(field, fields.boolean):
498 mod_ids = ir_module.search(cr, uid, [('name', '=', name[7:])])
499 record = ir_module.browse(cr, uid, mod_ids[0], context) if mod_ids else None
500 modules.append((name, record))
504 return {'default': defaults, 'group': groups, 'module': modules, 'other': others}
506 def default_get(self, cr, uid, fields, context=None):
507 ir_values = self.pool['ir.values']
508 classified = self._get_classified_fields(cr, uid, context)
510 res = super(res_config_settings, self).default_get(cr, uid, fields, context)
512 # defaults: take the corresponding default value they set
513 for name, model, field in classified['default']:
514 value = ir_values.get_default(cr, uid, model, field)
515 if value is not None:
518 # groups: which groups are implied by the group Employee
519 for name, groups, implied_group in classified['group']:
520 res[name] = all(implied_group in group.implied_ids for group in groups)
522 # modules: which modules are installed/to install
523 for name, module in classified['module']:
524 res[name] = module and module.state in ('installed', 'to install', 'to upgrade')
526 # other fields: call all methods that start with 'get_default_'
527 for method in dir(self):
528 if method.startswith('get_default_'):
529 res.update(getattr(self, method)(cr, uid, fields, context))
533 def execute(self, cr, uid, ids, context=None):
537 context = dict(context, active_test=False)
538 if uid != SUPERUSER_ID and not self.pool['res.users'].has_group(cr, uid, 'base.group_erp_manager'):
539 raise openerp.exceptions.AccessError(_("Only administrators can change the settings"))
541 ir_values = self.pool['ir.values']
542 ir_module = self.pool['ir.module.module']
543 res_groups = self.pool['res.groups']
545 classified = self._get_classified_fields(cr, uid, context=context)
547 config = self.browse(cr, uid, ids[0], context)
549 # default values fields
550 for name, model, field in classified['default']:
551 ir_values.set_default(cr, SUPERUSER_ID, model, field, config[name])
553 # group fields: modify group / implied groups
554 for name, groups, implied_group in classified['group']:
555 gids = map(int, groups)
557 res_groups.write(cr, uid, gids, {'implied_ids': [(4, implied_group.id)]}, context=context)
559 res_groups.write(cr, uid, gids, {'implied_ids': [(3, implied_group.id)]}, context=context)
562 uids.update(map(int, group.users))
563 implied_group.write({'users': [(3, u) for u in uids]})
565 # other fields: execute all methods that start with 'set_'
566 for method in dir(self):
567 if method.startswith('set_'):
568 getattr(self, method)(cr, uid, ids, context)
570 # module fields: install/uninstall the selected modules
572 to_uninstall_ids = []
574 for name, module in classified['module']:
576 to_install.append((name[lm:], module))
578 if module and module.state in ('installed', 'to upgrade'):
579 to_uninstall_ids.append(module.id)
582 ir_module.button_immediate_uninstall(cr, uid, to_uninstall_ids, context=context)
584 action = self._install_modules(cr, uid, to_install, context=context)
588 # After the uninstall/install calls, the self.pool is no longer valid.
589 # So we reach into the RegistryManager directly.
590 res_config = openerp.modules.registry.RegistryManager.get(cr.dbname)['res.config']
591 config = res_config.next(cr, uid, [], context=context) or {}
592 if config.get('type') not in ('ir.actions.act_window_close',):
595 # force client-side reload (update user menu and current view)
597 'type': 'ir.actions.client',
601 def cancel(self, cr, uid, ids, context=None):
602 # ignore the current record, and send the action to reopen the view
603 act_window = self.pool['ir.actions.act_window']
604 action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)])
606 return act_window.read(cr, uid, action_ids[0], [], context=context)
609 def name_get(self, cr, uid, ids, context=None):
610 """ Override name_get method to return an appropriate configuration wizard
611 name, and not the generated name."""
615 # name_get may receive int id instead of an id list
616 if isinstance(ids, (int, long)):
619 act_window = self.pool['ir.actions.act_window']
620 action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)], context=context)
623 name = act_window.read(cr, uid, action_ids[0], ['name'], context=context)['name']
624 return [(record.id, name) for record in self.browse(cr, uid , ids, context=context)]
626 def get_option_path(self, cr, uid, menu_xml_id, context=None):
628 Fetch the path to a specified configuration view and the action id to access it.
630 :param string menu_xml_id: the xml id of the menuitem where the view is located,
631 structured as follows: module_name.menuitem_xml_id (e.g.: "base.menu_sale_config")
633 - t[0]: string: full path to the menuitem (e.g.: "Settings/Configuration/Sales")
634 - t[1]: int or long: id of the menuitem's action
636 module_name, menu_xml_id = menu_xml_id.split('.')
637 dummy, menu_id = self.pool['ir.model.data'].get_object_reference(cr, uid, module_name, menu_xml_id)
638 ir_ui_menu = self.pool['ir.ui.menu'].browse(cr, uid, menu_id, context=context)
640 return (ir_ui_menu.complete_name, ir_ui_menu.action.id)
642 def get_option_name(self, cr, uid, full_field_name, context=None):
644 Fetch the human readable name of a specified configuration option.
646 :param string full_field_name: the full name of the field, structured as follows:
647 model_name.field_name (e.g.: "sale.config.settings.fetchmail_lead")
648 :return string: human readable name of the field (e.g.: "Create leads from incoming mails")
650 model_name, field_name = full_field_name.rsplit('.', 1)
652 return self.pool[model_name].fields_get(cr, uid, allfields=[field_name], context=context)[field_name]['string']
654 def get_config_warning(self, cr, msg, context=None):
656 Helper: return a Warning exception with the given message where the %(field:xxx)s
657 and/or %(menu:yyy)s are replaced by the human readable field's name and/or menuitem's
662 Just include in your error message %(field:model_name.field_name)s to obtain the human
663 readable field's name, and/or %(menu:module_name.menuitem_xml_id)s to obtain the menuitem's
668 from openerp.addons.base.res.res_config import get_warning_config
669 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)
671 This will return an exception containing the following message:
672 Error: this action is prohibited. You should check the field Create leads from incoming mails in Settings/Configuration/Sales.
674 What if there is another substitution in the message already?
675 -------------------------------------------------------------
676 You could have a situation where the error message you want to upgrade already contains a substitution. Example:
677 Cannot find any account journal of %s type for this company.\n\nYou can create one in the menu: \nConfiguration\Journals\Journals.
678 What you want to do here is simply to replace the path by %menu:account.menu_account_config)s, and leave the rest alone.
679 In order to do that, you can use the double percent (%%) to escape your new substitution, like so:
680 Cannot find any account journal of %s type for this company.\n\nYou can create one in the %%(menu:account.menu_account_config)s.
683 res_config_obj = openerp.registry(cr.dbname)['res.config.settings']
684 regex_path = r'%\(((?:menu|field):[a-z_\.]*)\)s'
686 # Process the message
687 # 1/ find the menu and/or field references, put them in a list
688 references = re.findall(regex_path, msg, flags=re.I)
690 # 2/ fetch the menu and/or field replacement values (full path and
691 # human readable field's name) and the action_id if any
694 for item in references:
695 ref_type, ref = item.split(':')
696 if ref_type == 'menu':
697 values[item], action_id = res_config_obj.get_option_path(cr, SUPERUSER_ID, ref, context=context)
698 elif ref_type == 'field':
699 values[item] = res_config_obj.get_option_name(cr, SUPERUSER_ID, ref, context=context)
701 # 3/ substitute and return the result
703 return exceptions.RedirectWarning(msg % values, action_id, _('Go to the configuration panel'))
704 return exceptions.Warning(msg % values)
706 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: