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
25 from openerp import pooler, SUPERUSER_ID
26 from openerp.osv import osv, fields
27 from openerp.tools import ustr
28 from openerp.tools.translate import _
29 from openerp import exceptions
31 _logger = logging.getLogger(__name__)
33 class res_config_configurable(osv.osv_memory):
34 ''' Base classes for new-style configuration items
36 Configuration items should inherit from this class, implement
37 the execute method (and optionally the cancel one) and have
38 their view inherit from the related res_config_view_base view.
42 def _next_action(self, cr, uid, context=None):
43 Todos = self.pool['ir.actions.todo']
44 _logger.info('getting next %s', Todos)
46 active_todos = Todos.browse(cr, uid,
47 Todos.search(cr, uid, ['&', ('type', '=', 'automatic'), ('state','=','open')]),
50 user_groups = set(map(
52 self.pool['res.users'].browse(cr, uid, [uid], context=context)[0].groups_id))
54 valid_todos_for_user = [
55 todo for todo in active_todos
56 if not todo.groups_id or bool(user_groups.intersection((
57 group.id for group in todo.groups_id)))
60 if valid_todos_for_user:
61 return valid_todos_for_user[0]
65 def _next(self, cr, uid, context=None):
66 _logger.info('getting next operation')
67 next = self._next_action(cr, uid, context=context)
68 _logger.info('next action is %s', next)
70 res = next.action_launch(context=context)
71 res['nodestroy'] = False
73 # reload the client; open the first available root menu
74 menu_obj = self.pool.get('ir.ui.menu')
75 menu_ids = menu_obj.search(cr, uid, [('parent_id', '=', False)], context=context)
77 'type': 'ir.actions.client',
79 'params': {'menu_id': menu_ids and menu_ids[0] or False},
82 def start(self, cr, uid, ids, context=None):
83 return self.next(cr, uid, ids, context)
85 def next(self, cr, uid, ids, context=None):
86 """ Returns the next todo action to execute (using the default
89 return self._next(cr, uid, context=context)
91 def execute(self, cr, uid, ids, context=None):
92 """ Method called when the user clicks on the ``Next`` button.
94 Execute *must* be overloaded unless ``action_next`` is overloaded
95 (which is something you generally don't need to do).
97 If ``execute`` returns an action dictionary, that action is executed
98 rather than just going to the next configuration item.
100 raise NotImplementedError(
101 'Configuration items need to implement execute')
102 def cancel(self, cr, uid, ids, context=None):
103 """ Method called when the user click on the ``Skip`` button.
105 ``cancel`` should be overloaded instead of ``action_skip``. As with
106 ``execute``, if it returns an action dictionary that action is
107 executed in stead of the default (going to the next configuration item)
109 The default implementation is a NOOP.
111 ``cancel`` is also called by the default implementation of
116 def action_next(self, cr, uid, ids, context=None):
117 """ Action handler for the ``next`` event.
119 Sets the status of the todo the event was sent from to
120 ``done``, calls ``execute`` and -- unless ``execute`` returned
121 an action dictionary -- executes the action provided by calling
124 next = self.execute(cr, uid, ids, context=context)
126 return self.next(cr, uid, ids, context=context)
128 def action_skip(self, cr, uid, ids, context=None):
129 """ Action handler for the ``skip`` event.
131 Sets the status of the todo the event was sent from to
132 ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
133 an action dictionary -- executes the action provided by calling
136 next = self.cancel(cr, uid, ids, context=context)
138 return self.next(cr, uid, ids, context=context)
140 def action_cancel(self, cr, uid, ids, context=None):
141 """ Action handler for the ``cancel`` event. That event isn't
142 generated by the res.config.view.base inheritable view, the
143 inherited view has to overload one of the buttons (or add one
146 Sets the status of the todo the event was sent from to
147 ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
148 an action dictionary -- executes the action provided by calling
151 next = self.cancel(cr, uid, ids, context=context)
153 return self.next(cr, uid, ids, context=context)
155 res_config_configurable()
157 class res_config_installer(osv.osv_memory):
158 """ New-style configuration base specialized for addons selection
164 Subclasses can simply define a number of _columns as
165 fields.boolean objects. The keys (column names) should be the
166 names of the addons to install (when selected). Upon action
167 execution, selected boolean fields (and those only) will be
168 interpreted as addons to install, and batch-installed.
173 It is also possible to require the installation of an additional
174 addon set when a specific preset of addons has been marked for
175 installation (in the basic usage only, additionals can't depend on
178 These additionals are defined through the ``_install_if``
179 property. This property is a mapping of a collection of addons (by
180 name) to a collection of addons (by name) [#]_, and if all the *key*
181 addons are selected for installation, then the *value* ones will
182 be selected as well. For example::
185 ('sale','crm'): ['sale_crm'],
188 This will install the ``sale_crm`` addon if and only if both the
189 ``sale`` and ``crm`` addons are selected for installation.
191 You can define as many additionals as you wish, and additionals
192 can overlap in key and value. For instance::
195 ('sale','crm'): ['sale_crm'],
196 ('sale','project'): ['project_mrp'],
199 will install both ``sale_crm`` and ``project_mrp`` if all of
200 ``sale``, ``crm`` and ``project`` are selected for installation.
205 Subclasses might also need to express dependencies more complex
206 than that provided by additionals. In this case, it's possible to
207 define methods of the form ``_if_%(name)s`` where ``name`` is the
208 name of a boolean field. If the field is selected, then the
209 corresponding module will be marked for installation *and* the
210 hook method will be executed.
212 Hook methods take the usual set of parameters (cr, uid, ids,
213 context) and can return a collection of additional addons to
214 install (if they return anything, otherwise they should not return
215 anything, though returning any "falsy" value such as None or an
216 empty collection will have the same effect).
221 The last hook is to simply overload the ``modules_to_install``
222 method, which implements all the mechanisms above. This method
223 takes the usual set of parameters (cr, uid, ids, context) and
224 returns a ``set`` of addons to install (addons selected by the
225 above methods minus addons from the *basic* set which are already
226 installed) [#]_ so an overloader can simply manipulate the ``set``
227 returned by ``res_config_installer.modules_to_install`` to add or
230 Skipping the installer
231 ----------------------
233 Unless it is removed from the view, installers have a *skip*
234 button which invokes ``action_skip`` (and the ``cancel`` hook from
235 ``res.config``). Hooks and additionals *are not run* when skipping
236 installation, even for already installed addons.
238 Again, setup your hooks accordingly.
240 .. [#] note that since a mapping key needs to be hashable, it's
241 possible to use a tuple or a frozenset, but not a list or a
244 .. [#] because the already-installed modules are only pruned at
245 the very end of ``modules_to_install``, additionals and
246 hooks depending on them *are guaranteed to execute*. Setup
247 your hooks accordingly.
249 _name = 'res.config.installer'
250 _inherit = 'res.config'
254 def already_installed(self, cr, uid, context=None):
255 """ For each module, check if it's already installed and if it
258 :returns: a list of the already installed modules in this
262 return map(attrgetter('name'),
263 self._already_installed(cr, uid, context=context))
265 def _already_installed(self, cr, uid, context=None):
266 """ For each module (boolean fields in a res.config.installer),
267 check if it's already installed (either 'to install', 'to upgrade'
268 or 'installed') and if it is return the module's browse_record
270 :returns: a list of all installed modules in this installer
271 :rtype: [browse_record]
273 modules = self.pool.get('ir.module.module')
275 selectable = [field for field in self._columns
276 if type(self._columns[field]) is fields.boolean]
277 return modules.browse(
279 modules.search(cr, uid,
280 [('name','in',selectable),
281 ('state','in',['to install', 'installed', 'to upgrade'])],
286 def modules_to_install(self, cr, uid, ids, context=None):
287 """ selects all modules to install:
289 * checked boolean fields
290 * return values of hook methods. Hook methods are of the form
291 ``_if_%(addon_name)s``, and are called if the corresponding
292 addon is marked for installation. They take the arguments
293 cr, uid, ids and context, and return an iterable of addon
295 * additionals, additionals are setup through the ``_install_if``
296 class variable. ``_install_if`` is a dict of {iterable:iterable}
297 where key and value are iterables of addon names.
299 If all the addons in the key are selected for installation
300 (warning: addons added through hooks don't count), then the
301 addons in the value are added to the set of modules to install
302 * not already installed
304 base = set(module_name
305 for installer in self.read(cr, uid, ids, context=context)
306 for module_name, to_install in installer.iteritems()
307 if module_name != 'id'
308 if type(self._columns[module_name]) is fields.boolean
311 hooks_results = set()
313 hook = getattr(self, '_if_%s'% module, None)
315 hooks_results.update(hook(cr, uid, ids, context=None) or set())
318 module for requirements, consequences \
319 in self._install_if.iteritems()
320 if base.issuperset(requirements)
321 for module in consequences)
323 return (base | hooks_results | additionals).difference(
324 self.already_installed(cr, uid, context))
326 def default_get(self, cr, uid, fields_list, context=None):
327 ''' If an addon is already installed, check it by default
329 defaults = super(res_config_installer, self).default_get(
330 cr, uid, fields_list, context=context)
332 return dict(defaults,
334 self.already_installed(cr, uid, context=context),
337 def fields_get(self, cr, uid, fields=None, context=None, write_access=True):
338 """ If an addon is already installed, set it to readonly as
339 res.config.installer doesn't handle uninstallations of already
342 fields = super(res_config_installer, self).fields_get(
343 cr, uid, fields, context, write_access)
345 for name in self.already_installed(cr, uid, context=context):
346 if name not in fields:
350 help= ustr(fields[name].get('help', '')) +
351 _('\n\nThis addon is already installed on your system'))
354 def execute(self, cr, uid, ids, context=None):
355 modules = self.pool.get('ir.module.module')
356 to_install = list(self.modules_to_install(
357 cr, uid, ids, context=context))
358 _logger.info('Selecting addons %s to install', to_install)
359 modules.state_update(
361 modules.search(cr, uid, [('name','in',to_install)]),
362 'to install', ['uninstalled'], context=context)
363 cr.commit() #TOFIX: after remove this statement, installation wizard is fail
364 new_db, self.pool = pooler.restart_pool(cr.dbname, update_module=True)
366 res_config_installer()
368 DEPRECATION_MESSAGE = 'You are using an addon using old-style configuration '\
369 'wizards (ir.actions.configuration.wizard). Old-style configuration '\
370 'wizards have been deprecated.\n'\
371 'The addon should be migrated to res.config objects.'
372 class ir_actions_configuration_wizard(osv.osv_memory):
373 ''' Compatibility configuration wizard
375 The old configuration wizard has been replaced by res.config, but in order
376 not to break existing but not-yet-migrated addons, the old wizard was
377 reintegrated and gutted.
379 _name='ir.actions.configuration.wizard'
380 _inherit = 'res.config'
382 def _next_action_note(self, cr, uid, ids, context=None):
383 next = self._next_action(cr, uid)
385 # if the next one is also an old-style extension, you never know...
388 return _("Click 'Continue' to configure the next addon...")
389 return _("Your database is now fully configured.\n\n"\
390 "Click 'Continue' and enjoy your OpenERP experience...")
393 'note': fields.text('Next Wizard', readonly=True),
396 'note': _next_action_note,
399 def execute(self, cr, uid, ids, context=None):
400 _logger.warning(DEPRECATION_MESSAGE)
402 ir_actions_configuration_wizard()
406 class res_config_settings(osv.osv_memory):
407 """ Base configuration wizard for application settings. It provides support for setting
408 default values, assigning groups to employee users, and installing modules.
409 To make such a 'settings' wizard, define a model like::
411 class my_config_wizard(osv.osv_memory):
412 _name = 'my.settings'
413 _inherit = 'res.config.settings'
415 'default_foo': fields.type(..., default_model='my.model'),
416 'group_bar': fields.boolean(..., group='base.group_user', implied_group='my.group'),
417 'module_baz': fields.boolean(...),
418 'other_field': fields.type(...),
421 The method ``execute`` provides some support based on a naming convention:
423 * For a field like 'default_XXX', ``execute`` sets the (global) default value of
424 the field 'XXX' in the model named by ``default_model`` to the field's value.
426 * For a boolean field like 'group_XXX', ``execute`` adds/removes 'implied_group'
427 to/from the implied groups of 'group', depending on the field's value.
428 By default 'group' is the group Employee. Groups are given by their xml id.
430 * For a boolean field like 'module_XXX', ``execute`` triggers the immediate
431 installation of the module named 'XXX' if the field has value ``True``.
433 * For the other fields, the method ``execute`` invokes all methods with a name
434 that starts with 'set_'; such methods can be defined to implement the effect
437 The method ``default_get`` retrieves values that reflect the current status of the
438 fields like 'default_XXX', 'group_XXX' and 'module_XXX'. It also invokes all methods
439 with a name that starts with 'get_default_'; such methods can be defined to provide
440 current values for other fields.
442 _name = 'res.config.settings'
444 def copy(self, cr, uid, id, values, context=None):
445 raise osv.except_osv(_("Cannot duplicate configuration!"), "")
447 def _get_classified_fields(self, cr, uid, context=None):
448 """ return a dictionary with the fields classified by category::
450 { 'default': [('default_foo', 'model', 'foo'), ...],
451 'group': [('group_bar', browse_group, browse_implied_group), ...],
452 'module': [('module_baz', browse_module), ...],
453 'other': ['other_field', ...],
456 ir_model_data = self.pool.get('ir.model.data')
457 ir_module = self.pool.get('ir.module.module')
459 mod, xml = xml_id.split('.', 1)
460 return ir_model_data.get_object(cr, uid, mod, xml, context)
462 defaults, groups, modules, others = [], [], [], []
463 for name, field in self._columns.items():
464 if name.startswith('default_') and hasattr(field, 'default_model'):
465 defaults.append((name, field.default_model, name[8:]))
466 elif name.startswith('group_') and isinstance(field, fields.boolean) and hasattr(field, 'implied_group'):
467 field_group = getattr(field, 'group', 'base.group_user')
468 groups.append((name, ref(field_group), ref(field.implied_group)))
469 elif name.startswith('module_') and isinstance(field, fields.boolean):
470 mod_ids = ir_module.search(cr, uid, [('name', '=', name[7:])])
471 record = ir_module.browse(cr, uid, mod_ids[0], context) if mod_ids else None
472 modules.append((name, record))
476 return {'default': defaults, 'group': groups, 'module': modules, 'other': others}
478 def default_get(self, cr, uid, fields, context=None):
479 ir_values = self.pool.get('ir.values')
480 classified = self._get_classified_fields(cr, uid, context)
482 res = super(res_config_settings, self).default_get(cr, uid, fields, context)
484 # defaults: take the corresponding default value they set
485 for name, model, field in classified['default']:
486 value = ir_values.get_default(cr, uid, model, field)
487 if value is not None:
490 # groups: which groups are implied by the group Employee
491 for name, group, implied_group in classified['group']:
492 res[name] = implied_group in group.implied_ids
494 # modules: which modules are installed/to install
495 for name, module in classified['module']:
496 res[name] = module and module.state in ('installed', 'to install', 'to upgrade')
498 # other fields: call all methods that start with 'get_default_'
499 for method in dir(self):
500 if method.startswith('get_default_'):
501 res.update(getattr(self, method)(cr, uid, fields, context))
505 def execute(self, cr, uid, ids, context=None):
506 ir_values = self.pool.get('ir.values')
507 ir_module = self.pool.get('ir.module.module')
508 classified = self._get_classified_fields(cr, uid, context)
510 config = self.browse(cr, uid, ids[0], context)
512 # default values fields
513 for name, model, field in classified['default']:
514 ir_values.set_default(cr, uid, model, field, config[name])
516 # group fields: modify group / implied groups
517 for name, group, implied_group in classified['group']:
519 group.write({'implied_ids': [(4, implied_group.id)]})
521 group.write({'implied_ids': [(3, implied_group.id)]})
522 implied_group.write({'users': [(3, u.id) for u in group.users]})
524 # other fields: execute all methods that start with 'set_'
525 for method in dir(self):
526 if method.startswith('set_'):
527 getattr(self, method)(cr, uid, ids, context)
529 # module fields: install/uninstall the selected modules
530 to_install_missing_names = []
531 to_uninstall_ids = []
534 for name, module in classified['module']:
537 # missing module, will be provided by apps.openerp.com
538 to_install_missing_names.append(name[lm:])
539 elif module.state == 'uninstalled':
540 # local module, to be installed
541 to_install_ids.append(module.id)
543 if module and module.state in ('installed', 'to upgrade'):
544 to_uninstall_ids.append(module.id)
547 ir_module.button_immediate_uninstall(cr, uid, to_uninstall_ids, context=context)
549 ir_module.button_immediate_install(cr, uid, to_install_ids, context=context)
551 if to_install_missing_names:
553 'type': 'ir.actions.client',
555 'params': {'modules': to_install_missing_names},
558 config = self.pool.get('res.config').next(cr, uid, [], context=context) or {}
559 if config.get('type') not in ('ir.actions.act_window_close',):
562 # force client-side reload (update user menu and current view)
564 'type': 'ir.actions.client',
568 def cancel(self, cr, uid, ids, context=None):
569 # ignore the current record, and send the action to reopen the view
570 act_window = self.pool.get('ir.actions.act_window')
571 action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)])
573 return act_window.read(cr, uid, action_ids[0], [], context=context)
576 def name_get(self, cr, uid, ids, context=None):
577 """ Override name_get method to return an appropriate configuration wizard
578 name, and not the generated name."""
582 # name_get may receive int id instead of an id list
583 if isinstance(ids, (int, long)):
586 act_window = self.pool.get('ir.actions.act_window')
587 action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)], context=context)
590 name = act_window.read(cr, uid, action_ids[0], ['name'], context=context)['name']
591 return [(record.id, name) for record in self.browse(cr, uid , ids, context=context)]
593 def get_option_path(self, cr, uid, menu_xml_id=None, context=None):
595 Fetch the path to a specified configuration view.
597 :param string menu_xml_id: the xml id of the menuitem where the view
598 is located, structured as follows: module_name.menuitem_xml_id
599 (e.g.: "base.menu_sale_config")
600 :return string: full path to the menuitem
601 (e.g.: "Settings/Configuration/Sales")
603 module_name, menu_xml_id = menu_xml_id.split('.')
604 dummy, menu_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, module_name, menu_xml_id)
605 ir_ui_menu = self.pool.get('ir.ui.menu').browse(cr, uid, menu_id, context=context)
607 return ir_ui_menu.complete_name
609 def get_option_name(self, cr, uid, full_field_name=None, context=None):
611 Fetch the human readable name of a specified configuration option.
613 :param string full_field_name: the full name of the field, structured
614 as follows: model_name.field_name
615 (e.g.: "sale.config.settings.fetchmail_lead")
616 :return string: human readable name of the field
617 (e.g.: "Create leads from incoming mails")
619 model_name, field_name = full_field_name.rsplit('.', 1)
621 return self.pool.get(model_name).fields_get(cr, uid, allfields=[field_name], context=context)[field_name]['string']
623 def get_warning_config(cr, msg, context=None):
625 Helper: return a WarningConfig exception with the given message where the
626 %(field:)s and/or %(menu:)s are replaced by the human readable field's name
627 and/or menuitem's full path.
631 Just include in your error message %(field:model_name.field_name)s to
632 obtain the human readable field's name, and/or
633 %(menu:module_name.menuitem_xml_id)s to obtain the menuitem's full path.
637 from openerp.addons.base.res.res_config import get_warning_config
638 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)
639 will return an exception containing the following message:
640 Error: this action is prohibited. You should check the field Create leads from incoming mails in Settings/Configuration/Sales.
643 res_config_obj = pooler.get_pool(cr.dbname).get('res.config.settings')
644 regex_path = r'%\(((?:menu|field):[a-z_\.]*)\)s'
646 # Process the message
647 # 1/ find the path and/or field references, put them in a list
648 references = re.findall(regex_path, msg, flags=re.I)
650 # 2/ fetch the path and/or field replacement values
651 # (full path and human readable field's name)
653 for item in references:
654 ref_type, ref = item.split(':')
655 if ref_type == 'menu':
656 values[item] = res_config_obj.get_option_path(cr, SUPERUSER_ID, ref, context)
657 elif ref_type == 'field':
658 values[item] = res_config_obj.get_option_name(cr, SUPERUSER_ID, ref, context)
660 # 4/ substitute and return the result
661 return exceptions.WarningConfig(msg % values)
663 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: