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 result = ir_module.button_immediate_install(cr, uid, to_install_ids, context=context)
55 #FIXME: if result is not none, the corresponding todo will be skipped because it was just marked done
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
107 'type': 'ir.actions.client',
111 def start(self, cr, uid, ids, context=None):
112 return self.next(cr, uid, ids, context)
114 def next(self, cr, uid, ids, context=None):
115 """ Returns the next todo action to execute (using the default
118 return self._next(cr, uid, context=context)
120 def execute(self, cr, uid, ids, context=None):
121 """ Method called when the user clicks on the ``Next`` button.
123 Execute *must* be overloaded unless ``action_next`` is overloaded
124 (which is something you generally don't need to do).
126 If ``execute`` returns an action dictionary, that action is executed
127 rather than just going to the next configuration item.
129 raise NotImplementedError(
130 'Configuration items need to implement execute')
132 def cancel(self, cr, uid, ids, context=None):
133 """ Method called when the user click on the ``Skip`` button.
135 ``cancel`` should be overloaded instead of ``action_skip``. As with
136 ``execute``, if it returns an action dictionary that action is
137 executed in stead of the default (going to the next configuration item)
139 The default implementation is a NOOP.
141 ``cancel`` is also called by the default implementation of
146 def action_next(self, cr, uid, ids, context=None):
147 """ Action handler for the ``next`` event.
149 Sets the status of the todo the event was sent from to
150 ``done``, calls ``execute`` and -- unless ``execute`` returned
151 an action dictionary -- executes the action provided by calling
154 next = self.execute(cr, uid, ids, context=context)
156 return self.next(cr, uid, ids, context=context)
158 def action_skip(self, cr, uid, ids, context=None):
159 """ Action handler for the ``skip`` event.
161 Sets the status of the todo the event was sent from to
162 ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
163 an action dictionary -- executes the action provided by calling
166 next = self.cancel(cr, uid, ids, context=context)
168 return self.next(cr, uid, ids, context=context)
170 def action_cancel(self, cr, uid, ids, context=None):
171 """ Action handler for the ``cancel`` event. That event isn't
172 generated by the res.config.view.base inheritable view, the
173 inherited view has to overload one of the buttons (or add one
176 Sets the status of the todo the event was sent from to
177 ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
178 an action dictionary -- executes the action provided by calling
181 next = self.cancel(cr, uid, ids, context=context)
183 return self.next(cr, uid, ids, context=context)
185 class res_config_installer(osv.osv_memory, res_config_module_installation_mixin):
186 """ New-style configuration base specialized for addons selection
192 Subclasses can simply define a number of _columns as
193 fields.boolean objects. The keys (column names) should be the
194 names of the addons to install (when selected). Upon action
195 execution, selected boolean fields (and those only) will be
196 interpreted as addons to install, and batch-installed.
201 It is also possible to require the installation of an additional
202 addon set when a specific preset of addons has been marked for
203 installation (in the basic usage only, additionals can't depend on
206 These additionals are defined through the ``_install_if``
207 property. This property is a mapping of a collection of addons (by
208 name) to a collection of addons (by name) [#]_, and if all the *key*
209 addons are selected for installation, then the *value* ones will
210 be selected as well. For example::
213 ('sale','crm'): ['sale_crm'],
216 This will install the ``sale_crm`` addon if and only if both the
217 ``sale`` and ``crm`` addons are selected for installation.
219 You can define as many additionals as you wish, and additionals
220 can overlap in key and value. For instance::
223 ('sale','crm'): ['sale_crm'],
224 ('sale','project'): ['project_mrp'],
227 will install both ``sale_crm`` and ``project_mrp`` if all of
228 ``sale``, ``crm`` and ``project`` are selected for installation.
233 Subclasses might also need to express dependencies more complex
234 than that provided by additionals. In this case, it's possible to
235 define methods of the form ``_if_%(name)s`` where ``name`` is the
236 name of a boolean field. If the field is selected, then the
237 corresponding module will be marked for installation *and* the
238 hook method will be executed.
240 Hook methods take the usual set of parameters (cr, uid, ids,
241 context) and can return a collection of additional addons to
242 install (if they return anything, otherwise they should not return
243 anything, though returning any "falsy" value such as None or an
244 empty collection will have the same effect).
249 The last hook is to simply overload the ``modules_to_install``
250 method, which implements all the mechanisms above. This method
251 takes the usual set of parameters (cr, uid, ids, context) and
252 returns a ``set`` of addons to install (addons selected by the
253 above methods minus addons from the *basic* set which are already
254 installed) [#]_ so an overloader can simply manipulate the ``set``
255 returned by ``res_config_installer.modules_to_install`` to add or
258 Skipping the installer
259 ----------------------
261 Unless it is removed from the view, installers have a *skip*
262 button which invokes ``action_skip`` (and the ``cancel`` hook from
263 ``res.config``). Hooks and additionals *are not run* when skipping
264 installation, even for already installed addons.
266 Again, setup your hooks accordingly.
268 .. [#] note that since a mapping key needs to be hashable, it's
269 possible to use a tuple or a frozenset, but not a list or a
272 .. [#] because the already-installed modules are only pruned at
273 the very end of ``modules_to_install``, additionals and
274 hooks depending on them *are guaranteed to execute*. Setup
275 your hooks accordingly.
277 _name = 'res.config.installer'
278 _inherit = 'res.config'
282 def already_installed(self, cr, uid, context=None):
283 """ For each module, check if it's already installed and if it
286 :returns: a list of the already installed modules in this
290 return map(attrgetter('name'),
291 self._already_installed(cr, uid, context=context))
293 def _already_installed(self, cr, uid, context=None):
294 """ For each module (boolean fields in a res.config.installer),
295 check if it's already installed (either 'to install', 'to upgrade'
296 or 'installed') and if it is return the module's browse_record
298 :returns: a list of all installed modules in this installer
299 :rtype: [browse_record]
301 modules = self.pool['ir.module.module']
303 selectable = [field for field in self._columns
304 if type(self._columns[field]) is fields.boolean]
305 return modules.browse(
307 modules.search(cr, uid,
308 [('name','in',selectable),
309 ('state','in',['to install', 'installed', 'to upgrade'])],
313 def modules_to_install(self, cr, uid, ids, context=None):
314 """ selects all modules to install:
316 * checked boolean fields
317 * return values of hook methods. Hook methods are of the form
318 ``_if_%(addon_name)s``, and are called if the corresponding
319 addon is marked for installation. They take the arguments
320 cr, uid, ids and context, and return an iterable of addon
322 * additionals, additionals are setup through the ``_install_if``
323 class variable. ``_install_if`` is a dict of {iterable:iterable}
324 where key and value are iterables of addon names.
326 If all the addons in the key are selected for installation
327 (warning: addons added through hooks don't count), then the
328 addons in the value are added to the set of modules to install
329 * not already installed
331 base = set(module_name
332 for installer in self.read(cr, uid, ids, context=context)
333 for module_name, to_install in installer.iteritems()
334 if module_name != 'id'
335 if type(self._columns[module_name]) is fields.boolean
338 hooks_results = set()
340 hook = getattr(self, '_if_%s'% module, None)
342 hooks_results.update(hook(cr, uid, ids, context=None) or set())
345 module for requirements, consequences \
346 in self._install_if.iteritems()
347 if base.issuperset(requirements)
348 for module in consequences)
350 return (base | hooks_results | additionals).difference(
351 self.already_installed(cr, uid, context))
353 def default_get(self, cr, uid, fields_list, context=None):
354 ''' If an addon is already installed, check it by default
356 defaults = super(res_config_installer, self).default_get(
357 cr, uid, fields_list, context=context)
359 return dict(defaults,
361 self.already_installed(cr, uid, context=context),
364 def fields_get(self, cr, uid, fields=None, context=None, write_access=True):
365 """ If an addon is already installed, set it to readonly as
366 res.config.installer doesn't handle uninstallations of already
369 fields = super(res_config_installer, self).fields_get(
370 cr, uid, fields, context, write_access)
372 for name in self.already_installed(cr, uid, context=context):
373 if name not in fields:
377 help= ustr(fields[name].get('help', '')) +
378 _('\n\nThis addon is already installed on your system'))
381 def execute(self, cr, uid, ids, context=None):
382 to_install = list(self.modules_to_install(
383 cr, uid, ids, context=context))
384 _logger.info('Selecting addons %s to install', to_install)
386 ir_module = self.pool.get('ir.module.module')
388 for name in to_install:
389 mod_ids = ir_module.search(cr, uid, [('name', '=', name)])
390 record = ir_module.browse(cr, uid, mod_ids[0], context) if mod_ids else None
391 modules.append((name, record))
393 return self._install_modules(cr, uid, modules, context=context)
395 class res_config_settings(osv.osv_memory, res_config_module_installation_mixin):
396 """ Base configuration wizard for application settings. It provides support for setting
397 default values, assigning groups to employee users, and installing modules.
398 To make such a 'settings' wizard, define a model like::
400 class my_config_wizard(osv.osv_memory):
401 _name = 'my.settings'
402 _inherit = 'res.config.settings'
404 'default_foo': fields.type(..., default_model='my.model'),
405 'group_bar': fields.boolean(..., group='base.group_user', implied_group='my.group'),
406 'module_baz': fields.boolean(...),
407 'other_field': fields.type(...),
410 The method ``execute`` provides some support based on a naming convention:
412 * For a field like 'default_XXX', ``execute`` sets the (global) default value of
413 the field 'XXX' in the model named by ``default_model`` to the field's value.
415 * For a boolean field like 'group_XXX', ``execute`` adds/removes 'implied_group'
416 to/from the implied groups of 'group', depending on the field's value.
417 By default 'group' is the group Employee. Groups are given by their xml id.
419 * For a boolean field like 'module_XXX', ``execute`` triggers the immediate
420 installation of the module named 'XXX' if the field has value ``True``.
422 * For the other fields, the method ``execute`` invokes all methods with a name
423 that starts with 'set_'; such methods can be defined to implement the effect
426 The method ``default_get`` retrieves values that reflect the current status of the
427 fields like 'default_XXX', 'group_XXX' and 'module_XXX'. It also invokes all methods
428 with a name that starts with 'get_default_'; such methods can be defined to provide
429 current values for other fields.
431 _name = 'res.config.settings'
433 def copy(self, cr, uid, id, values, context=None):
434 raise osv.except_osv(_("Cannot duplicate configuration!"), "")
436 def _get_classified_fields(self, cr, uid, context=None):
437 """ return a dictionary with the fields classified by category::
439 { 'default': [('default_foo', 'model', 'foo'), ...],
440 'group': [('group_bar', browse_group, browse_implied_group), ...],
441 'module': [('module_baz', browse_module), ...],
442 'other': ['other_field', ...],
445 ir_model_data = self.pool['ir.model.data']
446 ir_module = self.pool['ir.module.module']
448 mod, xml = xml_id.split('.', 1)
449 return ir_model_data.get_object(cr, uid, mod, xml, context)
451 defaults, groups, modules, others = [], [], [], []
452 for name, field in self._columns.items():
453 if name.startswith('default_') and hasattr(field, 'default_model'):
454 defaults.append((name, field.default_model, name[8:]))
455 elif name.startswith('group_') and isinstance(field, fields.boolean) and hasattr(field, 'implied_group'):
456 field_group = getattr(field, 'group', 'base.group_user')
457 groups.append((name, ref(field_group), ref(field.implied_group)))
458 elif name.startswith('module_') and isinstance(field, fields.boolean):
459 mod_ids = ir_module.search(cr, uid, [('name', '=', name[7:])])
460 record = ir_module.browse(cr, uid, mod_ids[0], context) if mod_ids else None
461 modules.append((name, record))
465 return {'default': defaults, 'group': groups, 'module': modules, 'other': others}
467 def default_get(self, cr, uid, fields, context=None):
468 ir_values = self.pool['ir.values']
469 classified = self._get_classified_fields(cr, uid, context)
471 res = super(res_config_settings, self).default_get(cr, uid, fields, context)
473 # defaults: take the corresponding default value they set
474 for name, model, field in classified['default']:
475 value = ir_values.get_default(cr, uid, model, field)
476 if value is not None:
479 # groups: which groups are implied by the group Employee
480 for name, group, implied_group in classified['group']:
481 res[name] = implied_group in group.implied_ids
483 # modules: which modules are installed/to install
484 for name, module in classified['module']:
485 res[name] = module and module.state in ('installed', 'to install', 'to upgrade')
487 # other fields: call all methods that start with 'get_default_'
488 for method in dir(self):
489 if method.startswith('get_default_'):
490 res.update(getattr(self, method)(cr, uid, fields, context))
494 def execute(self, cr, uid, ids, context=None):
495 ir_values = self.pool['ir.values']
496 ir_module = self.pool['ir.module.module']
497 classified = self._get_classified_fields(cr, uid, context)
499 config = self.browse(cr, uid, ids[0], context)
501 # default values fields
502 for name, model, field in classified['default']:
503 ir_values.set_default(cr, uid, model, field, config[name])
505 # group fields: modify group / implied groups
506 for name, group, implied_group in classified['group']:
508 group.write({'implied_ids': [(4, implied_group.id)]})
510 group.write({'implied_ids': [(3, implied_group.id)]})
511 implied_group.write({'users': [(3, u.id) for u in group.users]})
513 # other fields: execute all methods that start with 'set_'
514 for method in dir(self):
515 if method.startswith('set_'):
516 getattr(self, method)(cr, uid, ids, context)
518 # module fields: install/uninstall the selected modules
520 to_uninstall_ids = []
522 for name, module in classified['module']:
524 to_install.append((name[lm:], module))
526 if module and module.state in ('installed', 'to upgrade'):
527 to_uninstall_ids.append(module.id)
530 ir_module.button_immediate_uninstall(cr, uid, to_uninstall_ids, context=context)
532 action = self._install_modules(cr, uid, to_install, context=context)
536 # After the uninstall/install calls, the self.pool is no longer valid.
537 # So we reach into the RegistryManager directly.
538 res_config = openerp.modules.registry.RegistryManager.get(cr.dbname)['res.config']
539 config = res_config.next(cr, uid, [], context=context) or {}
540 if config.get('type') not in ('ir.actions.act_window_close',):
543 # force client-side reload (update user menu and current view)
545 'type': 'ir.actions.client',
549 def cancel(self, cr, uid, ids, context=None):
550 # ignore the current record, and send the action to reopen the view
551 act_window = self.pool['ir.actions.act_window']
552 action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)])
554 return act_window.read(cr, uid, action_ids[0], [], context=context)
557 def name_get(self, cr, uid, ids, context=None):
558 """ Override name_get method to return an appropriate configuration wizard
559 name, and not the generated name."""
563 # name_get may receive int id instead of an id list
564 if isinstance(ids, (int, long)):
567 act_window = self.pool['ir.actions.act_window']
568 action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)], context=context)
571 name = act_window.read(cr, uid, action_ids[0], ['name'], context=context)['name']
572 return [(record.id, name) for record in self.browse(cr, uid , ids, context=context)]
574 def get_option_path(self, cr, uid, menu_xml_id, context=None):
576 Fetch the path to a specified configuration view and the action id to access it.
578 :param string menu_xml_id: the xml id of the menuitem where the view is located,
579 structured as follows: module_name.menuitem_xml_id (e.g.: "base.menu_sale_config")
581 - t[0]: string: full path to the menuitem (e.g.: "Settings/Configuration/Sales")
582 - t[1]: int or long: id of the menuitem's action
584 module_name, menu_xml_id = menu_xml_id.split('.')
585 dummy, menu_id = self.pool['ir.model.data'].get_object_reference(cr, uid, module_name, menu_xml_id)
586 ir_ui_menu = self.pool['ir.ui.menu'].browse(cr, uid, menu_id, context=context)
588 return (ir_ui_menu.complete_name, ir_ui_menu.action.id)
590 def get_option_name(self, cr, uid, full_field_name, context=None):
592 Fetch the human readable name of a specified configuration option.
594 :param string full_field_name: the full name of the field, structured as follows:
595 model_name.field_name (e.g.: "sale.config.settings.fetchmail_lead")
596 :return string: human readable name of the field (e.g.: "Create leads from incoming mails")
598 model_name, field_name = full_field_name.rsplit('.', 1)
600 return self.pool[model_name].fields_get(cr, uid, allfields=[field_name], context=context)[field_name]['string']
602 def get_config_warning(self, cr, msg, context=None):
604 Helper: return a Warning exception with the given message where the %(field:xxx)s
605 and/or %(menu:yyy)s are replaced by the human readable field's name and/or menuitem's
610 Just include in your error message %(field:model_name.field_name)s to obtain the human
611 readable field's name, and/or %(menu:module_name.menuitem_xml_id)s to obtain the menuitem's
616 from openerp.addons.base.res.res_config import get_warning_config
617 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)
619 This will return an exception containing the following message:
620 Error: this action is prohibited. You should check the field Create leads from incoming mails in Settings/Configuration/Sales.
622 What if there is another substitution in the message already?
623 -------------------------------------------------------------
624 You could have a situation where the error message you want to upgrade already contains a substitution. Example:
625 Cannot find any account journal of %s type for this company.\n\nYou can create one in the menu: \nConfiguration\Journals\Journals.
626 What you want to do here is simply to replace the path by %menu:account.menu_account_config)s, and leave the rest alone.
627 In order to do that, you can use the double percent (%%) to escape your new substitution, like so:
628 Cannot find any account journal of %s type for this company.\n\nYou can create one in the %%(menu:account.menu_account_config)s.
631 res_config_obj = openerp.registry(cr.dbname)['res.config.settings']
632 regex_path = r'%\(((?:menu|field):[a-z_\.]*)\)s'
634 # Process the message
635 # 1/ find the menu and/or field references, put them in a list
636 references = re.findall(regex_path, msg, flags=re.I)
638 # 2/ fetch the menu and/or field replacement values (full path and
639 # human readable field's name) and the action_id if any
642 for item in references:
643 ref_type, ref = item.split(':')
644 if ref_type == 'menu':
645 values[item], action_id = res_config_obj.get_option_path(cr, SUPERUSER_ID, ref, context=context)
646 elif ref_type == 'field':
647 values[item] = res_config_obj.get_option_name(cr, SUPERUSER_ID, ref, context=context)
649 # 3/ substitute and return the result
651 return exceptions.RedirectWarning(msg % values, action_id, _('Go to the configuration panel'))
652 return exceptions.Warning(msg % values)
654 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: