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, itemgetter
24 from osv import osv, fields
25 from tools.translate import _
27 from tools import ustr
30 _logger = logging.getLogger(__name__)
32 class res_config_configurable(osv.osv_memory):
33 ''' Base classes for new-style configuration items
35 Configuration items should inherit from this class, implement
36 the execute method (and optionally the cancel one) and have
37 their view inherit from the related res_config_view_base view.
40 _inherit = 'ir.wizard.screen'
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 #if there is no next action and if html is in the context: reload instead of closing
74 if context and 'html' in context:
75 return {'type' : 'ir.actions.reload'}
76 return {'type' : 'ir.actions.act_window_close'}
78 def start(self, cr, uid, ids, context=None):
79 return self.next(cr, uid, ids, context)
81 def next(self, cr, uid, ids, context=None):
82 """ Returns the next todo action to execute (using the default
85 return self._next(cr, uid, context=context)
87 def execute(self, cr, uid, ids, context=None):
88 """ Method called when the user clicks on the ``Next`` button.
90 Execute *must* be overloaded unless ``action_next`` is overloaded
91 (which is something you generally don't need to do).
93 If ``execute`` returns an action dictionary, that action is executed
94 rather than just going to the next configuration item.
96 raise NotImplementedError(
97 'Configuration items need to implement execute')
98 def cancel(self, cr, uid, ids, context=None):
99 """ Method called when the user click on the ``Skip`` button.
101 ``cancel`` should be overloaded instead of ``action_skip``. As with
102 ``execute``, if it returns an action dictionary that action is
103 executed in stead of the default (going to the next configuration item)
105 The default implementation is a NOOP.
107 ``cancel`` is also called by the default implementation of
112 def action_next(self, cr, uid, ids, context=None):
113 """ Action handler for the ``next`` event.
115 Sets the status of the todo the event was sent from to
116 ``done``, calls ``execute`` and -- unless ``execute`` returned
117 an action dictionary -- executes the action provided by calling
120 next = self.execute(cr, uid, ids, context=context)
122 return self.next(cr, uid, ids, context=context)
124 def action_skip(self, cr, uid, ids, context=None):
125 """ Action handler for the ``skip`` event.
127 Sets the status of the todo the event was sent from to
128 ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
129 an action dictionary -- executes the action provided by calling
132 next = self.cancel(cr, uid, ids, context=context)
134 return self.next(cr, uid, ids, context=context)
136 def action_cancel(self, cr, uid, ids, context=None):
137 """ Action handler for the ``cancel`` event. That event isn't
138 generated by the res.config.view.base inheritable view, the
139 inherited view has to overload one of the buttons (or add one
142 Sets the status of the todo the event was sent from to
143 ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
144 an action dictionary -- executes the action provided by calling
147 next = self.cancel(cr, uid, ids, context=context)
149 return self.next(cr, uid, ids, context=context)
151 res_config_configurable()
153 class res_config_installer(osv.osv_memory):
154 """ New-style configuration base specialized for addons selection
160 Subclasses can simply define a number of _columns as
161 fields.boolean objects. The keys (column names) should be the
162 names of the addons to install (when selected). Upon action
163 execution, selected boolean fields (and those only) will be
164 interpreted as addons to install, and batch-installed.
169 It is also possible to require the installation of an additional
170 addon set when a specific preset of addons has been marked for
171 installation (in the basic usage only, additionals can't depend on
174 These additionals are defined through the ``_install_if``
175 property. This property is a mapping of a collection of addons (by
176 name) to a collection of addons (by name) [#]_, and if all the *key*
177 addons are selected for installation, then the *value* ones will
178 be selected as well. For example::
181 ('sale','crm'): ['sale_crm'],
184 This will install the ``sale_crm`` addon if and only if both the
185 ``sale`` and ``crm`` addons are selected for installation.
187 You can define as many additionals as you wish, and additionals
188 can overlap in key and value. For instance::
191 ('sale','crm'): ['sale_crm'],
192 ('sale','project'): ['project_mrp'],
195 will install both ``sale_crm`` and ``project_mrp`` if all of
196 ``sale``, ``crm`` and ``project`` are selected for installation.
201 Subclasses might also need to express dependencies more complex
202 than that provided by additionals. In this case, it's possible to
203 define methods of the form ``_if_%(name)s`` where ``name`` is the
204 name of a boolean field. If the field is selected, then the
205 corresponding module will be marked for installation *and* the
206 hook method will be executed.
208 Hook methods take the usual set of parameters (cr, uid, ids,
209 context) and can return a collection of additional addons to
210 install (if they return anything, otherwise they should not return
211 anything, though returning any "falsy" value such as None or an
212 empty collection will have the same effect).
217 The last hook is to simply overload the ``modules_to_install``
218 method, which implements all the mechanisms above. This method
219 takes the usual set of parameters (cr, uid, ids, context) and
220 returns a ``set`` of addons to install (addons selected by the
221 above methods minus addons from the *basic* set which are already
222 installed) [#]_ so an overloader can simply manipulate the ``set``
223 returned by ``res_config_installer.modules_to_install`` to add or
226 Skipping the installer
227 ----------------------
229 Unless it is removed from the view, installers have a *skip*
230 button which invokes ``action_skip`` (and the ``cancel`` hook from
231 ``res.config``). Hooks and additionals *are not run* when skipping
232 installation, even for already installed addons.
234 Again, setup your hooks accordingly.
236 .. [#] note that since a mapping key needs to be hashable, it's
237 possible to use a tuple or a frozenset, but not a list or a
240 .. [#] because the already-installed modules are only pruned at
241 the very end of ``modules_to_install``, additionals and
242 hooks depending on them *are guaranteed to execute*. Setup
243 your hooks accordingly.
245 _name = 'res.config.installer'
246 _inherit = 'res.config'
250 def already_installed(self, cr, uid, context=None):
251 """ For each module, check if it's already installed and if it
254 :returns: a list of the already installed modules in this
258 return map(attrgetter('name'),
259 self._already_installed(cr, uid, context=context))
261 def _already_installed(self, cr, uid, context=None):
262 """ For each module (boolean fields in a res.config.installer),
263 check if it's already installed (either 'to install', 'to upgrade'
264 or 'installed') and if it is return the module's browse_record
266 :returns: a list of all installed modules in this installer
267 :rtype: [browse_record]
269 modules = self.pool.get('ir.module.module')
271 selectable = [field for field in self._columns
272 if type(self._columns[field]) is fields.boolean]
273 return modules.browse(
275 modules.search(cr, uid,
276 [('name','in',selectable),
277 ('state','in',['to install', 'installed', 'to upgrade'])],
282 def modules_to_install(self, cr, uid, ids, context=None):
283 """ selects all modules to install:
285 * checked boolean fields
286 * return values of hook methods. Hook methods are of the form
287 ``_if_%(addon_name)s``, and are called if the corresponding
288 addon is marked for installation. They take the arguments
289 cr, uid, ids and context, and return an iterable of addon
291 * additionals, additionals are setup through the ``_install_if``
292 class variable. ``_install_if`` is a dict of {iterable:iterable}
293 where key and value are iterables of addon names.
295 If all the addons in the key are selected for installation
296 (warning: addons added through hooks don't count), then the
297 addons in the value are added to the set of modules to install
298 * not already installed
300 base = set(module_name
301 for installer in self.read(cr, uid, ids, context=context)
302 for module_name, to_install in installer.iteritems()
303 if module_name != 'id'
304 if type(self._columns[module_name]) is fields.boolean
307 hooks_results = set()
309 hook = getattr(self, '_if_%s'%(module), None)
311 hooks_results.update(hook(cr, uid, ids, context=None) or set())
314 module for requirements, consequences \
315 in self._install_if.iteritems()
316 if base.issuperset(requirements)
317 for module in consequences)
319 return (base | hooks_results | additionals).difference(
320 self.already_installed(cr, uid, context))
322 def default_get(self, cr, uid, fields_list, context=None):
323 ''' If an addon is already installed, check it by default
325 defaults = super(res_config_installer, self).default_get(
326 cr, uid, fields_list, context=context)
328 return dict(defaults,
330 self.already_installed(cr, uid, context=context),
333 def fields_get(self, cr, uid, fields=None, context=None, write_access=True):
334 """ If an addon is already installed, set it to readonly as
335 res.config.installer doesn't handle uninstallations of already
338 fields = super(res_config_installer, self).fields_get(
339 cr, uid, fields, context, write_access)
341 for name in self.already_installed(cr, uid, context=context):
342 if name not in fields:
346 help= ustr(fields[name].get('help', '')) +
347 _('\n\nThis addon is already installed on your system'))
350 def execute(self, cr, uid, ids, context=None):
351 modules = self.pool.get('ir.module.module')
352 to_install = list(self.modules_to_install(
353 cr, uid, ids, context=context))
354 _logger.info('Selecting addons %s to install', to_install)
355 modules.state_update(
357 modules.search(cr, uid, [('name','in',to_install)]),
358 'to install', ['uninstalled'], context=context)
359 cr.commit() #TOFIX: after remove this statement, installation wizard is fail
360 new_db, self.pool = pooler.restart_pool(cr.dbname, update_module=True)
361 res_config_installer()
363 DEPRECATION_MESSAGE = 'You are using an addon using old-style configuration '\
364 'wizards (ir.actions.configuration.wizard). Old-style configuration '\
365 'wizards have been deprecated.\n'\
366 'The addon should be migrated to res.config objects.'
367 class ir_actions_configuration_wizard(osv.osv_memory):
368 ''' Compatibility configuration wizard
370 The old configuration wizard has been replaced by res.config, but in order
371 not to break existing but not-yet-migrated addons, the old wizard was
372 reintegrated and gutted.
374 _name='ir.actions.configuration.wizard'
375 _inherit = 'res.config'
377 def _next_action_note(self, cr, uid, ids, context=None):
378 next = self._next_action(cr, uid)
380 # if the next one is also an old-style extension, you never know...
383 return _("Click 'Continue' to configure the next addon...")
384 return _("Your database is now fully configured.\n\n"\
385 "Click 'Continue' and enjoy your OpenERP experience...")
388 'note': fields.text('Next Wizard', readonly=True),
391 'note': _next_action_note,
394 def execute(self, cr, uid, ids, context=None):
395 _logger.warning(DEPRECATION_MESSAGE)
397 ir_actions_configuration_wizard()
401 class res_config_settings(osv.osv_memory):
402 """ Base configuration wizard for application settings. It provides support for setting
403 default values, assigning groups to employee users, and installing modules.
404 To make such a 'settings' wizard, define a model like::
406 class my_config_wizard(osv.osv_memory):
407 _name = 'my.settings'
408 _inherit = 'res.config.settings'
410 'default_foo': fields.type(..., default_model='my.model'),
411 'group_bar': fields.boolean(..., group='base.group_user', implied_group='my.group'),
412 'module_baz': fields.boolean(...),
413 'other_field': fields.type(...),
416 The method ``execute`` provides some support based on a naming convention:
418 * For a field like 'default_XXX', ``execute`` sets the (global) default value of
419 the field 'XXX' in the model named by ``default_model`` to the field's value.
421 * For a boolean field like 'group_XXX', ``execute`` adds/removes 'implied_group'
422 to/from the implied groups of 'group', depending on the field's value.
423 By default 'group' is the group Employee. Groups are given by their xml id.
425 * For a boolean field like 'module_XXX', ``execute`` triggers the immediate
426 installation of the module named 'XXX' if the field has value ``True``.
428 * For the other fields, the method ``execute`` invokes all methods with a name
429 that starts with 'set_'; such methods can be defined to implement the effect
432 The method ``default_get`` retrieves values that reflect the current status of the
433 fields like 'default_XXX', 'group_XXX' and 'module_XXX'. It also invokes all methods
434 with a name that starts with 'get_default_'; such methods can be defined to provide
435 current values for other fields.
437 _name = 'res.config.settings'
439 def copy(self, cr, uid, id, values, context=None):
440 raise osv.except_osv(_("Cannot duplicate configuration!"), "")
442 def _get_classified_fields(self, cr, uid, context=None):
443 """ return a dictionary with the fields classified by category::
445 { 'default': [('default_foo', 'model', 'foo'), ...],
446 'group': [('group_bar', browse_group, browse_implied_group), ...],
447 'module': [('module_baz', browse_module), ...],
448 'other': ['other_field', ...],
451 ir_model_data = self.pool.get('ir.model.data')
452 ir_module = self.pool.get('ir.module.module')
454 mod, xml = xml_id.split('.', 1)
455 return ir_model_data.get_object(cr, uid, mod, xml, context)
457 defaults, groups, modules, others = [], [], [], []
458 for name, field in self._columns.items():
459 if name.startswith('default_') and hasattr(field, 'default_model'):
460 defaults.append((name, field.default_model, name[8:]))
461 elif name.startswith('group_') and isinstance(field, fields.boolean) and hasattr(field, 'implied_group'):
462 field_group = getattr(field, 'group', 'base.group_user')
463 groups.append((name, ref(field_group), ref(field.implied_group)))
464 elif name.startswith('module_') and isinstance(field, fields.boolean):
465 mod_ids = ir_module.search(cr, uid, [('name', '=', name[7:])])
466 modules.append((name, ir_module.browse(cr, uid, mod_ids[0], context)))
470 return {'default': defaults, 'group': groups, 'module': modules, 'other': others}
472 def default_get(self, cr, uid, fields, context=None):
473 ir_values = self.pool.get('ir.values')
474 classified = self._get_classified_fields(cr, uid, context)
476 res = super(res_config_settings, self).default_get(cr, uid, fields, context)
478 # defaults: take the corresponding default value they set
479 for name, model, field in classified['default']:
480 value = ir_values.get_default(cr, uid, model, field)
481 if value is not None:
484 # groups: which groups are implied by the group Employee
485 for name, group, implied_group in classified['group']:
486 res[name] = implied_group in group.implied_ids
488 # modules: which modules are installed/to install
489 for name, module in classified['module']:
490 res[name] = module.state in ('installed', 'to install', 'to upgrade')
492 # other fields: call all methods that start with 'get_default_'
493 for method in dir(self):
494 if method.startswith('get_default_'):
495 res.update(getattr(self, method)(cr, uid, fields, context))
499 def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
500 # overridden to make fields of installed modules readonly
501 res = super(res_config_settings, self).fields_get(cr, uid, allfields, context, write_access)
502 classified = self._get_classified_fields(cr, uid, context)
503 for name, module in classified['module']:
504 if name in res and module.state in ('installed', 'to install', 'to upgrade'):
505 res[name]['readonly'] = False
508 def execute(self, cr, uid, ids, context=None):
509 ir_values = self.pool.get('ir.values')
510 ir_model_data = self.pool.get('ir.model.data')
511 ir_module = self.pool.get('ir.module.module')
512 res_groups = self.pool.get('res.groups')
513 classified = self._get_classified_fields(cr, uid, context)
515 config = self.browse(cr, uid, ids[0], context)
517 # default values fields
518 for name, model, field in classified['default']:
519 ir_values.set_default(cr, uid, model, field, config[name])
521 # group fields: modify group / implied groups
522 for name, group, implied_group in classified['group']:
524 group.write({'implied_ids': [(4, implied_group.id)]})
526 group.write({'implied_ids': [(3, implied_group.id)]})
527 implied_group.write({'users': [(3, u.id) for u in group.users]})
529 # other fields: execute all methods that start with 'set_'
530 for method in dir(self):
531 if method.startswith('set_'):
532 getattr(self, method)(cr, uid, ids, context)
534 # module fields: install immediately the selected modules
536 for name, module in classified['module']:
538 if module.state == 'uninstalled': to_install_ids.append(module.id)
540 if module.state in ['installed','upgrade']: to_uninstall_ids.append(module.id)
542 ir_module.button_immediate_install(cr, uid, to_install_ids, context)
544 ir_module.module_uninstall(cr, uid, to_uninstall_ids, context)
546 # force client-side reload (update user menu and current view)
548 'type': 'ir.actions.client',
552 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: