#
##############################################################################
import logging
-from operator import attrgetter, itemgetter
+from operator import attrgetter
+import re
-from osv import osv, fields
-from tools.translate import _
-import netsvc
-from tools import ustr
-import pooler
+import openerp
+from openerp import SUPERUSER_ID
+from openerp.osv import osv, fields
+from openerp.tools import ustr
+from openerp.tools.translate import _
+from openerp import exceptions
+from lxml import etree
_logger = logging.getLogger(__name__)
+
+class res_config_module_installation_mixin(object):
+ def _install_modules(self, cr, uid, modules, context):
+ """Install the requested modules.
+ return the next action to execute
+
+ modules is a list of tuples
+ (mod_name, browse_record | None)
+ """
+ ir_module = self.pool.get('ir.module.module')
+ to_install_ids = []
+ to_install_missing_names = []
+
+ for name, module in modules:
+ if not module:
+ to_install_missing_names.append(name)
+ elif module.state == 'uninstalled':
+ to_install_ids.append(module.id)
+ result = None
+ if to_install_ids:
+ result = ir_module.button_immediate_install(cr, uid, to_install_ids, context=context)
+ #FIXME: if result is not none, the corresponding todo will be skipped because it was just marked done
+ if to_install_missing_names:
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'apps',
+ 'params': {'modules': to_install_missing_names},
+ }
+
+ return result
+
class res_config_configurable(osv.osv_memory):
''' Base classes for new-style configuration items
their view inherit from the related res_config_view_base view.
'''
_name = 'res.config'
- _inherit = 'ir.wizard.screen'
def _next_action(self, cr, uid, context=None):
Todos = self.pool['ir.actions.todo']
res = next.action_launch(context=context)
res['nodestroy'] = False
return res
- #if there is no next action and if html is in the context: reload instead of closing
- if context and 'html' in context:
- return {'type' : 'ir.actions.reload'}
- return {'type' : 'ir.actions.act_window_close'}
+
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'reload',
+ }
def start(self, cr, uid, ids, context=None):
return self.next(cr, uid, ids, context)
"""
raise NotImplementedError(
'Configuration items need to implement execute')
+
def cancel(self, cr, uid, ids, context=None):
""" Method called when the user click on the ``Skip`` button.
if next: return next
return self.next(cr, uid, ids, context=context)
-res_config_configurable()
-
-class res_config_installer(osv.osv_memory):
+class res_config_installer(osv.osv_memory, res_config_module_installation_mixin):
""" New-style configuration base specialized for addons selection
and installation.
_install_if = {
('sale','crm'): ['sale_crm'],
- ('sale','project'): ['project_mrp'],
+ ('sale','project'): ['sale_service'],
}
- will install both ``sale_crm`` and ``project_mrp`` if all of
+ will install both ``sale_crm`` and ``sale_service`` if all of
``sale``, ``crm`` and ``project`` are selected for installation.
Hook methods
def _already_installed(self, cr, uid, context=None):
""" For each module (boolean fields in a res.config.installer),
check if it's already installed (either 'to install', 'to upgrade'
- or 'installed') and if it is return the module's browse_record
+ or 'installed') and if it is return the module's record
:returns: a list of all installed modules in this installer
- :rtype: [browse_record]
+ :rtype: recordset (collection of Record)
"""
- modules = self.pool.get('ir.module.module')
+ modules = self.pool['ir.module.module']
selectable = [field for field in self._columns
if type(self._columns[field]) is fields.boolean]
context=context),
context=context)
-
def modules_to_install(self, cr, uid, ids, context=None):
""" selects all modules to install:
for installer in self.read(cr, uid, ids, context=context)
for module_name, to_install in installer.iteritems()
if module_name != 'id'
- if type(self._columns[module_name]) is fields.boolean
+ if type(self._columns.get(module_name)) is fields.boolean
if to_install)
hooks_results = set()
for module in base:
- hook = getattr(self, '_if_%s'%(module), None)
+ hook = getattr(self, '_if_%s'% module, None)
if hook:
hooks_results.update(hook(cr, uid, ids, context=None) or set())
return fields
def execute(self, cr, uid, ids, context=None):
- modules = self.pool.get('ir.module.module')
to_install = list(self.modules_to_install(
cr, uid, ids, context=context))
_logger.info('Selecting addons %s to install', to_install)
- modules.state_update(
- cr, uid,
- modules.search(cr, uid, [('name','in',to_install)]),
- 'to install', ['uninstalled'], context=context)
- cr.commit() #TOFIX: after remove this statement, installation wizard is fail
- new_db, self.pool = pooler.restart_pool(cr.dbname, update_module=True)
-res_config_installer()
-
-DEPRECATION_MESSAGE = 'You are using an addon using old-style configuration '\
- 'wizards (ir.actions.configuration.wizard). Old-style configuration '\
- 'wizards have been deprecated.\n'\
- 'The addon should be migrated to res.config objects.'
-class ir_actions_configuration_wizard(osv.osv_memory):
- ''' Compatibility configuration wizard
-
- The old configuration wizard has been replaced by res.config, but in order
- not to break existing but not-yet-migrated addons, the old wizard was
- reintegrated and gutted.
- '''
- _name='ir.actions.configuration.wizard'
- _inherit = 'res.config'
-
- def _next_action_note(self, cr, uid, ids, context=None):
- next = self._next_action(cr, uid)
- if next:
- # if the next one is also an old-style extension, you never know...
- if next.note:
- return next.note
- return _("Click 'Continue' to configure the next addon...")
- return _("Your database is now fully configured.\n\n"\
- "Click 'Continue' and enjoy your OpenERP experience...")
-
- _columns = {
- 'note': fields.text('Next Wizard', readonly=True),
- }
- _defaults = {
- 'note': _next_action_note,
- }
-
- def execute(self, cr, uid, ids, context=None):
- _logger.warning(DEPRECATION_MESSAGE)
-
-ir_actions_configuration_wizard()
+ ir_module = self.pool.get('ir.module.module')
+ modules = []
+ for name in to_install:
+ mod_ids = ir_module.search(cr, uid, [('name', '=', name)])
+ record = ir_module.browse(cr, uid, mod_ids[0], context) if mod_ids else None
+ modules.append((name, record))
+ return self._install_modules(cr, uid, modules, context=context)
-class res_config_settings(osv.osv_memory):
+class res_config_settings(osv.osv_memory, res_config_module_installation_mixin):
""" Base configuration wizard for application settings. It provides support for setting
default values, assigning groups to employee users, and installing modules.
To make such a 'settings' wizard, define a model like::
class my_config_wizard(osv.osv_memory):
- _name = 'sale.config.prefs'
+ _name = 'my.settings'
_inherit = 'res.config.settings'
_columns = {
- 'default_foo': fields.type(..., default_model='model'),
- 'group_bar': fields.boolean(..., group='xml_id'),
+ 'default_foo': fields.type(..., default_model='my.model'),
+ 'group_bar': fields.boolean(..., group='base.group_user', implied_group='my.group'),
'module_baz': fields.boolean(...),
'other_field': fields.type(...),
}
* For a field like 'default_XXX', ``execute`` sets the (global) default value of
the field 'XXX' in the model named by ``default_model`` to the field's value.
- * For a boolean field like 'group_XXX', ``execute`` adds or removes the group given
- by its XML id to the implied groups of Employee, depending on the field's value.
+ * For a boolean field like 'group_XXX', ``execute`` adds/removes 'implied_group'
+ to/from the implied groups of 'group', depending on the field's value.
+ By default 'group' is the group Employee. Groups are given by their xml id.
+ The attribute 'group' may contain several xml ids, separated by commas.
* For a boolean field like 'module_XXX', ``execute`` triggers the immediate
installation of the module named 'XXX' if the field has value ``True``.
- * For other fields, the method ``execute`` only record their value for future
- invocations of the wizard. Override the method to define their effect.
+ * For the other fields, the method ``execute`` invokes all methods with a name
+ that starts with 'set_'; such methods can be defined to implement the effect
+ of those fields.
+
+ The method ``default_get`` retrieves values that reflect the current status of the
+ fields like 'default_XXX', 'group_XXX' and 'module_XXX'. It also invokes all methods
+ with a name that starts with 'get_default_'; such methods can be defined to provide
+ current values for other fields.
"""
_name = 'res.config.settings'
- _inherit = 'res.config'
+
+ def copy(self, cr, uid, id, values, context=None):
+ raise osv.except_osv(_("Cannot duplicate configuration!"), "")
+
+ def fields_view_get(self, cr, user, view_id=None, view_type='form',
+ context=None, toolbar=False, submenu=False):
+ ret_val = super(res_config_settings, self).fields_view_get(
+ cr, user, view_id=view_id, view_type=view_type, context=context,
+ toolbar=toolbar, submenu=submenu)
+
+ doc = etree.XML(ret_val['arch'])
+
+ for field in ret_val['fields']:
+ if not field.startswith("module_"):
+ continue
+ for node in doc.xpath("//field[@name='%s']" % field):
+ if 'on_change' not in node.attrib:
+ node.set("on_change",
+ "onchange_module(%s, '%s')" % (field, field))
+
+ ret_val['arch'] = etree.tostring(doc)
+ return ret_val
+
+ def onchange_module(self, cr, uid, ids, field_value, module_name, context={}):
+ module_pool = self.pool.get('ir.module.module')
+ module_ids = module_pool.search(
+ cr, uid, [('name', '=', module_name.replace("module_", '')),
+ ('state','in', ['to install', 'installed', 'to upgrade'])],
+ context=context)
+
+ if module_ids and not field_value:
+ dep_ids = module_pool.downstream_dependencies(cr, uid, module_ids, context=context)
+ dep_name = [x.shortdesc for x in module_pool.browse(
+ cr, uid, dep_ids + module_ids, context=context)]
+ message = '\n'.join(dep_name)
+ return {
+ 'warning': {
+ 'title': _('Warning!'),
+ 'message': _('Disabling this option will also uninstall the following modules \n%s') % message,
+ }
+ }
+ return {}
def _get_classified_fields(self, cr, uid, context=None):
""" return a dictionary with the fields classified by category::
{ 'default': [('default_foo', 'model', 'foo'), ...],
- 'group': [('group_bar', browse_group), ...],
+ 'group': [('group_bar', [browse_group], browse_implied_group), ...],
'module': [('module_baz', browse_module), ...],
'other': ['other_field', ...],
}
"""
- ir_model_data = self.pool.get('ir.model.data')
- ir_module = self.pool.get('ir.module.module')
+ ir_model_data = self.pool['ir.model.data']
+ ir_module = self.pool['ir.module.module']
+ def ref(xml_id):
+ mod, xml = xml_id.split('.', 1)
+ return ir_model_data.get_object(cr, uid, mod, xml, context=context)
defaults, groups, modules, others = [], [], [], []
for name, field in self._columns.items():
if name.startswith('default_') and hasattr(field, 'default_model'):
defaults.append((name, field.default_model, name[8:]))
- elif name.startswith('group_') and isinstance(field, fields.boolean) and hasattr(field, 'group'):
- mod, xml = field.group.split('.', 1)
- groups.append((name, ir_model_data.get_object(cr, uid, mod, xml, context)))
+ elif name.startswith('group_') and isinstance(field, fields.boolean) and hasattr(field, 'implied_group'):
+ field_groups = getattr(field, 'group', 'base.group_user').split(',')
+ groups.append((name, map(ref, field_groups), ref(field.implied_group)))
elif name.startswith('module_') and isinstance(field, fields.boolean):
mod_ids = ir_module.search(cr, uid, [('name', '=', name[7:])])
- modules.append((name, ir_module.browse(cr, uid, mod_ids[0], context)))
+ record = ir_module.browse(cr, uid, mod_ids[0], context) if mod_ids else None
+ modules.append((name, record))
else:
others.append(name)
return {'default': defaults, 'group': groups, 'module': modules, 'other': others}
def default_get(self, cr, uid, fields, context=None):
- ir_values = self.pool.get('ir.values')
- ir_model_data = self.pool.get('ir.model.data')
+ ir_values = self.pool['ir.values']
classified = self._get_classified_fields(cr, uid, context)
res = super(res_config_settings, self).default_get(cr, uid, fields, context)
res[name] = value
# groups: which groups are implied by the group Employee
- group_employee = ir_model_data.get_object(cr, uid, 'base', 'group_user', context)
- for name, group in classified['group']:
- res[name] = group in group_employee.implied_ids
+ for name, groups, implied_group in classified['group']:
+ res[name] = all(implied_group in group.implied_ids for group in groups)
# modules: which modules are installed/to install
for name, module in classified['module']:
- res[name] = module.state in ('installed', 'to install', 'to upgrade')
+ res[name] = module and module.state in ('installed', 'to install', 'to upgrade')
- return res
+ # other fields: call all methods that start with 'get_default_'
+ for method in dir(self):
+ if method.startswith('get_default_'):
+ res.update(getattr(self, method)(cr, uid, fields, context))
- def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
- # overridden to make fields of installed modules readonly
- res = super(res_config_settings, self).fields_get(cr, uid, allfields, context, write_access)
- classified = self._get_classified_fields(cr, uid, context)
- for name, module in classified['module']:
- if module.state in ('installed', 'to install', 'to upgrade'):
- res[name]['readonly'] = True
return res
def execute(self, cr, uid, ids, context=None):
- ir_values = self.pool.get('ir.values')
- ir_model_data = self.pool.get('ir.model.data')
- ir_module = self.pool.get('ir.module.module')
- res_groups = self.pool.get('res.groups')
- classified = self._get_classified_fields(cr, uid, context)
+ if context is None:
+ context = {}
- config = self.browse(cr, uid, ids[0], context)
+ context = dict(context, active_test=False)
+ if uid != SUPERUSER_ID and not self.pool['res.users'].has_group(cr, uid, 'base.group_erp_manager'):
+ raise openerp.exceptions.AccessError(_("Only administrators can change the settings"))
+
+ ir_values = self.pool['ir.values']
+ ir_module = self.pool['ir.module.module']
+ res_groups = self.pool['res.groups']
- # other fields: simply record their current value as default values
- for name in classified['other']:
- ir_values.set_default(cr, uid, self._name, name, config[name])
+ classified = self._get_classified_fields(cr, uid, context=context)
+
+ config = self.browse(cr, uid, ids[0], context)
# default values fields
for name, model, field in classified['default']:
- ir_values.set_default(cr, uid, model, field, config[name])
-
- # group fields: modify group employee, and remove employees from deselected groups
- if classified['group']:
- yes, nos = [], []
- for name, group in classified['group']:
- (yes if config[name] else nos).append(group.id)
- group_employee = ir_model_data.get_object(cr, uid, 'base', 'group_user', context)
- group_employee.write({'implied_ids': [(3, id) for id in nos] + [(4, id) for id in yes]})
- res_groups.write(cr, uid, nos, {'users': [(3, u.id) for u in group_employee.users]})
-
- # module fields: install immediately the selected modules
+ ir_values.set_default(cr, SUPERUSER_ID, model, field, config[name])
+
+ # group fields: modify group / implied groups
+ for name, groups, implied_group in classified['group']:
+ gids = map(int, groups)
+ if config[name]:
+ res_groups.write(cr, uid, gids, {'implied_ids': [(4, implied_group.id)]}, context=context)
+ else:
+ res_groups.write(cr, uid, gids, {'implied_ids': [(3, implied_group.id)]}, context=context)
+ uids = set()
+ for group in groups:
+ uids.update(map(int, group.users))
+ implied_group.write({'users': [(3, u) for u in uids]})
+
+ # other fields: execute all methods that start with 'set_'
+ for method in dir(self):
+ if method.startswith('set_'):
+ getattr(self, method)(cr, uid, ids, context)
+
+ # module fields: install/uninstall the selected modules
to_install = []
+ to_uninstall_ids = []
+ lm = len('module_')
for name, module in classified['module']:
- if config[name] and module.state == 'uninstalled':
- to_install.append(module.id)
- if to_install:
- return ir_module.button_immediate_install(cr, uid, to_install, context)
+ if config[name]:
+ to_install.append((name[lm:], module))
+ else:
+ if module and module.state in ('installed', 'to upgrade'):
+ to_uninstall_ids.append(module.id)
+
+ if to_uninstall_ids:
+ ir_module.button_immediate_uninstall(cr, uid, to_uninstall_ids, context=context)
+
+ action = self._install_modules(cr, uid, to_install, context=context)
+ if action:
+ return action
+
+ # After the uninstall/install calls, the self.pool is no longer valid.
+ # So we reach into the RegistryManager directly.
+ res_config = openerp.modules.registry.RegistryManager.get(cr.dbname)['res.config']
+ config = res_config.next(cr, uid, [], context=context) or {}
+ if config.get('type') not in ('ir.actions.act_window_close',):
+ return config
+
+ # force client-side reload (update user menu and current view)
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'reload',
+ }
+
+ def cancel(self, cr, uid, ids, context=None):
+ # ignore the current record, and send the action to reopen the view
+ act_window = self.pool['ir.actions.act_window']
+ action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)])
+ if action_ids:
+ return act_window.read(cr, uid, action_ids[0], [], context=context)
return {}
-res_config_settings()
+ def name_get(self, cr, uid, ids, context=None):
+ """ Override name_get method to return an appropriate configuration wizard
+ name, and not the generated name."""
+
+ if not ids:
+ return []
+ # name_get may receive int id instead of an id list
+ if isinstance(ids, (int, long)):
+ ids = [ids]
+
+ act_window = self.pool['ir.actions.act_window']
+ action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)], context=context)
+ name = self._name
+ if action_ids:
+ name = act_window.read(cr, uid, action_ids[0], ['name'], context=context)['name']
+ return [(record.id, name) for record in self.browse(cr, uid , ids, context=context)]
+
+ def get_option_path(self, cr, uid, menu_xml_id, context=None):
+ """
+ Fetch the path to a specified configuration view and the action id to access it.
+
+ :param string menu_xml_id: the xml id of the menuitem where the view is located,
+ structured as follows: module_name.menuitem_xml_id (e.g.: "base.menu_sale_config")
+ :return tuple:
+ - t[0]: string: full path to the menuitem (e.g.: "Settings/Configuration/Sales")
+ - t[1]: int or long: id of the menuitem's action
+ """
+ module_name, menu_xml_id = menu_xml_id.split('.')
+ dummy, menu_id = self.pool['ir.model.data'].get_object_reference(cr, uid, module_name, menu_xml_id)
+ ir_ui_menu = self.pool['ir.ui.menu'].browse(cr, uid, menu_id, context=context)
+
+ return (ir_ui_menu.complete_name, ir_ui_menu.action.id)
+
+ def get_option_name(self, cr, uid, full_field_name, context=None):
+ """
+ Fetch the human readable name of a specified configuration option.
+
+ :param string full_field_name: the full name of the field, structured as follows:
+ model_name.field_name (e.g.: "sale.config.settings.fetchmail_lead")
+ :return string: human readable name of the field (e.g.: "Create leads from incoming mails")
+ """
+ model_name, field_name = full_field_name.rsplit('.', 1)
+
+ return self.pool[model_name].fields_get(cr, uid, allfields=[field_name], context=context)[field_name]['string']
+
+ def get_config_warning(self, cr, msg, context=None):
+ """
+ Helper: return a Warning exception with the given message where the %(field:xxx)s
+ and/or %(menu:yyy)s are replaced by the human readable field's name and/or menuitem's
+ full path.
+
+ Usage:
+ ------
+ Just include in your error message %(field:model_name.field_name)s to obtain the human
+ readable field's name, and/or %(menu:module_name.menuitem_xml_id)s to obtain the menuitem's
+ full path.
+
+ Example of use:
+ ---------------
+ from openerp.addons.base.res.res_config import get_warning_config
+ 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)
+
+ This will return an exception containing the following message:
+ Error: this action is prohibited. You should check the field Create leads from incoming mails in Settings/Configuration/Sales.
+
+ What if there is another substitution in the message already?
+ -------------------------------------------------------------
+ You could have a situation where the error message you want to upgrade already contains a substitution. Example:
+ Cannot find any account journal of %s type for this company.\n\nYou can create one in the menu: \nConfiguration\Journals\Journals.
+ What you want to do here is simply to replace the path by %menu:account.menu_account_config)s, and leave the rest alone.
+ In order to do that, you can use the double percent (%%) to escape your new substitution, like so:
+ Cannot find any account journal of %s type for this company.\n\nYou can create one in the %%(menu:account.menu_account_config)s.
+ """
+
+ res_config_obj = openerp.registry(cr.dbname)['res.config.settings']
+ regex_path = r'%\(((?:menu|field):[a-z_\.]*)\)s'
+
+ # Process the message
+ # 1/ find the menu and/or field references, put them in a list
+ references = re.findall(regex_path, msg, flags=re.I)
+
+ # 2/ fetch the menu and/or field replacement values (full path and
+ # human readable field's name) and the action_id if any
+ values = {}
+ action_id = None
+ for item in references:
+ ref_type, ref = item.split(':')
+ if ref_type == 'menu':
+ values[item], action_id = res_config_obj.get_option_path(cr, SUPERUSER_ID, ref, context=context)
+ elif ref_type == 'field':
+ values[item] = res_config_obj.get_option_name(cr, SUPERUSER_ID, ref, context=context)
+
+ # 3/ substitute and return the result
+ if (action_id):
+ return exceptions.RedirectWarning(msg % values, action_id, _('Go to the configuration panel'))
+ return exceptions.Warning(msg % values)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: