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`` is automatically called after creating and writing on records.
417 It provides some support based on a naming convention:
419 * For a field like 'default_XXX', ``execute`` sets the (global) default value of
420 the field 'XXX' in the model named by ``default_model`` to the field's value.
422 * For a boolean field like 'group_XXX', ``execute`` adds/removes 'implied_group'
423 to/from the implied groups of 'group', depending on the field's value.
424 By default 'group' is the group Employee. Groups are given by their xml id.
426 * For a boolean field like 'module_XXX', ``execute`` triggers the immediate
427 installation of the module named 'XXX' if the field has value ``True``.
429 * For the other fields, the method ``execute`` invokes all methods with a name
430 that starts with 'set_'; such methods can be defined to implement the effect
433 The method ``default_get`` retrieves values that reflect the current status of the
434 fields like 'default_XXX', 'group_XXX' and 'module_XXX'. It also invokes all methods
435 with a name that starts with 'get_default_'; such methods can be defined to provide
436 current values for other fields.
438 _name = 'res.config.settings'
439 _inherit = 'res.config'
441 def create(self, cr, uid, values, context=None):
442 id = super(res_config_settings, self).create(cr, uid, values, context)
443 self.execute(cr, uid, [id], context)
446 def write(self, cr, uid, ids, values, context=None):
447 res = super(res_config_settings, self).write(cr, uid, ids, values, context)
448 self.execute(cr, uid, ids, context)
451 def copy(self, cr, uid, id, values, context=None):
452 raise osv.except_osv(_("Cannot duplicate configuration!"), "")
454 def _get_classified_fields(self, cr, uid, context=None):
455 """ return a dictionary with the fields classified by category::
457 { 'default': [('default_foo', 'model', 'foo'), ...],
458 'group': [('group_bar', browse_group, browse_implied_group), ...],
459 'module': [('module_baz', browse_module), ...],
460 'other': ['other_field', ...],
463 ir_model_data = self.pool.get('ir.model.data')
464 ir_module = self.pool.get('ir.module.module')
466 mod, xml = xml_id.split('.', 1)
467 return ir_model_data.get_object(cr, uid, mod, xml, context)
469 defaults, groups, modules, others = [], [], [], []
470 for name, field in self._columns.items():
471 if name.startswith('default_') and hasattr(field, 'default_model'):
472 defaults.append((name, field.default_model, name[8:]))
473 elif name.startswith('group_') and isinstance(field, fields.boolean) and hasattr(field, 'implied_group'):
474 field_group = getattr(field, 'group', 'base.group_user')
475 groups.append((name, ref(field_group), ref(field.implied_group)))
476 elif name.startswith('module_') and isinstance(field, fields.boolean):
477 mod_ids = ir_module.search(cr, uid, [('name', '=', name[7:])])
478 modules.append((name, ir_module.browse(cr, uid, mod_ids[0], context)))
482 return {'default': defaults, 'group': groups, 'module': modules, 'other': others}
484 def default_get(self, cr, uid, fields, context=None):
485 ir_values = self.pool.get('ir.values')
486 ir_model_data = self.pool.get('ir.model.data')
487 classified = self._get_classified_fields(cr, uid, context)
489 res = super(res_config_settings, self).default_get(cr, uid, fields, context)
491 # defaults: take the corresponding default value they set
492 for name, model, field in classified['default']:
493 value = ir_values.get_default(cr, uid, model, field)
494 if value is not None:
497 # groups: which groups are implied by the group Employee
498 for name, group, implied_group in classified['group']:
499 res[name] = implied_group in group.implied_ids
501 # modules: which modules are installed/to install
502 for name, module in classified['module']:
503 res[name] = module.state in ('installed', 'to install', 'to upgrade')
505 # other fields: call all methods that start with 'get_default_'
506 for method in dir(self):
507 if method.startswith('get_default_'):
508 res.update(getattr(self, method)(cr, uid, fields, context))
512 def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
513 # overridden to make fields of installed modules readonly
514 res = super(res_config_settings, self).fields_get(cr, uid, allfields, context, write_access)
515 classified = self._get_classified_fields(cr, uid, context)
516 for name, module in classified['module']:
517 if name in res and module.state in ('installed', 'to install', 'to upgrade'):
518 res[name]['readonly'] = True
521 def execute(self, cr, uid, ids, context=None):
522 ir_values = self.pool.get('ir.values')
523 ir_model_data = self.pool.get('ir.model.data')
524 ir_module = self.pool.get('ir.module.module')
525 res_groups = self.pool.get('res.groups')
526 classified = self._get_classified_fields(cr, uid, context)
528 config = self.browse(cr, uid, ids[0], context)
530 # default values fields
531 for name, model, field in classified['default']:
532 ir_values.set_default(cr, uid, model, field, config[name])
534 # group fields: modify group / implied groups
535 for name, group, implied_group in classified['group']:
537 group.write({'implied_ids': [(4, implied_group.id)]})
539 group.write({'implied_ids': [(3, implied_group.id)]})
540 implied_group.write({'users': [(3, u.id) for u in group.users]})
542 # other fields: execute all methods that start with 'set_'
543 for method in dir(self):
544 if method.startswith('set_'):
545 getattr(self, method)(cr, uid, ids, context)
547 # module fields: install immediately the selected modules
549 for name, module in classified['module']:
550 if config[name] and module.state == 'uninstalled':
551 to_install_ids.append(module.id)
553 ir_module.button_immediate_install(cr, uid, to_install_ids, context)
557 res_config_settings()
559 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: