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
24 from osv import osv, fields
25 from tools.translate import _
29 class res_config_configurable(osv.osv_memory):
30 ''' Base classes for new-style configuration items
32 Configuration items should inherit from this class, implement
33 the execute method (and optionally the cancel one) and have
34 their view inherit from the related res_config_view_base view.
37 logger = netsvc.Logger()
39 def _progress(self, cr, uid, context=None):
40 total = self.pool.get('ir.actions.todo')\
41 .search_count(cr, uid, [], context)
42 open = self.pool.get('ir.actions.todo')\
43 .search_count(cr, uid, [('active','=',True),
44 ('state','<>','open')],
47 return round(open*100./total)
51 progress=fields.float('Configuration Progress', readonly=True),
57 def _next_action(self, cr, uid):
58 todos = self.pool.get('ir.actions.todo')
59 self.logger.notifyChannel('actions', netsvc.LOG_INFO,
60 'getting next %s' % todos)
61 active_todos = todos.search(cr, uid, [('state','=','open'),
65 return todos.browse(cr, uid, active_todos[0], context=None)
68 def _set_previous_todo(self, cr, uid, state):
69 """ lookup the previous (which is still the next at this point)
70 ir.actions.todo, set it to whatever state was provided.
73 `LookupError`: if we couldn't find *any* previous todo
74 `ValueError`: if no state is provided
75 anything ir_actions_todo.write can throw
77 # this is ultra brittle, but apart from storing the todo id
78 # into the res.config view, I'm not sure how to do it
79 previous_todo = self._next_action(cr, uid)
81 raise LookupError(_("Couldn't find previous ir.actions.todo"))
83 raise ValueError(_("Can't set an ir.actions.todo's state to "
85 previous_todo.write({'state':state})
87 def _next(self, cr, uid):
88 self.logger.notifyChannel('actions', netsvc.LOG_INFO,
89 'getting next operation')
90 next = self._next_action(cr, uid)
91 self.logger.notifyChannel('actions', netsvc.LOG_INFO,
92 'next action is %s' % next)
94 action = next.action_id
96 'view_mode': action.view_mode,
97 'view_type': action.view_type,
98 'view_id': action.view_id and [action.view_id.id] or False,
99 'res_model': action.res_model,
101 'target': action.target,
103 self.logger.notifyChannel(
104 'actions', netsvc.LOG_INFO,
105 'all configuration actions have been executed')
107 current_user_menu = self.pool.get('res.users')\
108 .browse(cr, uid, uid).menu_id
109 # return the action associated with the menu
110 return self.pool.get(current_user_menu.type)\
111 .read(cr, uid, current_user_menu.id)
113 def next(self, cr, uid, ids, context=None):
114 """ Returns the next action to execute execute (using the default
117 return self._next(cr, uid)
119 def execute(self, cr, uid, ids, context=None):
120 """ Method called when the user clicks on the ``Next`` button.
122 Execute *must* be overloaded unless ``action_next`` is overloaded
123 (which is something you generally don't need to do).
125 If ``execute`` returns an action dictionary, that action is executed
126 rather than just going to the next configuration item.
128 raise NotImplementedError(
129 'Configuration items need to implement execute')
130 def cancel(self, cr, uid, ids, context=None):
131 """ Method called when the user click on the ``Skip`` button.
133 ``cancel`` should be overloaded instead of ``action_skip``. As with
134 ``execute``, if it returns an action dictionary that action is
135 executed in stead of the default (going to the next configuration item)
137 The default implementation is a NOOP.
141 def action_next(self, cr, uid, ids, context=None):
142 """ Action handler for the ``next`` event.
144 Sets the status of the todo the event was sent from to
145 ``done``, calls ``execute`` and -- unless ``execute`` returned
146 an action dictionary -- executes the action provided by calling
149 self._set_previous_todo(cr, uid, state='done')
150 next = self.execute(cr, uid, ids, context=None)
152 return self.next(cr, uid, ids, context=context)
154 def action_skip(self, cr, uid, ids, context=None):
155 """ Action handler for the ``skip`` event.
157 Sets the status of the todo the event was sent from to
158 ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
159 an action dictionary -- executes the action provided by calling
162 self._set_previous_todo(cr, uid, state='skip')
163 next = self.cancel(cr, uid, ids, context=None)
165 return self.next(cr, uid, ids, context=context)
167 def action_cancel(self, cr, uid, ids, context=None):
168 """ Action handler for the ``cancel`` event. That event isn't
169 generated by the res.config.view.base inheritable view, the
170 inherited view has to overload one of the buttons (or add one
173 Sets the status of the todo the event was sent from to
174 ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
175 an action dictionary -- executes the action provided by calling
178 self._set_previous_todo(cr, uid, state='cancel')
179 next = self.cancel(cr, uid, ids, context=None)
181 return self.next(cr, uid, ids, context=context)
182 res_config_configurable()
184 class res_config_installer(osv.osv_memory):
185 """ New-style configuration base specialized for addons selection
191 Subclasses can simply define a number of _columns as
192 fields.boolean objects. The keys (column names) should be the
193 names of the addons to install (when selected). Upon action
194 execution, selected boolean fields (and those only) will be
195 interpreted as addons to install, and batch-installed.
200 It is also possible to require the installation of an additional
201 addon set when a specific preset of addons has been marked for
202 installation (in the basic usage only, additionals can't depend on
205 These additionals are defined through the ``_install_if``
206 property. This property is a mapping of a collection of addons (by
207 name) to a collection of addons (by name) [#]_, and if all the *key*
208 addons are selected for installation, then the *value* ones will
209 be selected as well. For example::
212 ('sale','crm'): ['sale_crm'],
215 This will install the ``sale_crm`` addon if and only if both the
216 ``sale`` and ``crm`` addons are selected for installation.
218 You can define as many additionals as you wish, and additionals
219 can overlap in key and value. For instance::
222 ('sale','crm'): ['sale_crm'],
223 ('sale','project'): ['project_mrp'],
226 will install both ``sale_crm`` and ``project_mrp`` if all of
227 ``sale``, ``crm`` and ``project`` are selected for installation.
232 Subclasses might also need to express dependencies more complex
233 than that provided by additionals. In this case, it's possible to
234 define methods of the form ``_if_%(name)s`` where ``name`` is the
235 name of a boolean field. If the field is selected, then the
236 corresponding module will be marked for installation *and* the
237 hook method will be executed.
239 Hook methods take the usual set of parameters (cr, uid, ids,
240 context) and can return a collection of additional addons to
241 install (if they return anything, otherwise they should not return
242 anything, though returning any "falsy" value such as None or an
243 empty collection will have the same effect).
248 The last hook is to simply overload the ``modules_to_install``
249 method, which implements all the mechanisms above. This method
250 takes the usual set of parameters (cr, uid, ids, context) and
251 returns a ``set`` of addons to install (addons selected by the
252 above methods minus addons from the *basic* set which are already
253 installed) [#]_ so an overloader can simply manipulate the ``set``
254 returned by ``res_config_installer.modules_to_install`` to add or
257 Skipping the installer
258 ----------------------
260 Unless it is removed from the view, installers have a *skip*
261 button which invokes ``action_skip`` (and the ``cancel`` hook from
262 ``res.config``). Hooks and additionals *are not run* when skipping
263 installation, even for already installed addons.
265 Again, setup your hooks accordinly.
267 .. [#] note that since a mapping key needs to be hashable, it's
268 possible to use a tuple or a frozenset, but not a list or a
271 .. [#] because the already-installed modules are only pruned at
272 the very end of ``modules_to_install``, additionals and
273 hooks depending on them *are guaranteed to execute*. Setup
274 your hooks accordingly.
276 _name = 'res.config.installer'
277 _inherit = 'res.config'
281 def _already_installed(self, cr, uid, context=None):
282 """ For each module (boolean fields in a res.config.installer),
283 check if it's already installed (neither uninstallable nor uninstalled)
284 and if it is, check it by default
286 modules = self.pool.get('ir.module.module')
288 selectable = [field for field in self._columns
289 if type(self._columns[field]) is fields.boolean]
290 return modules.browse(
292 modules.search(cr, uid,
293 [('name','in',selectable),
294 ('state','not in',['uninstallable', 'uninstalled'])],
299 def modules_to_install(self, cr, uid, ids, context=None):
300 """ selects all modules to install:
302 * checked boolean fields
303 * return values of hook methods. Hook methods are of the form
304 ``_if_%(addon_name)s``, and are called if the corresponding
305 addon is marked for installation. They take the arguments
306 cr, uid, ids and context, and return an iterable of addon
308 * additionals, additionals are setup through the ``_install_if``
309 class variable. ``_install_if`` is a dict of {iterable:iterable}
310 where key and value are iterables of addon names.
312 If all the addons in the key are selected for installation
313 (warning: addons added through hooks don't count), then the
314 addons in the value are added to the set of modules to install
315 * not already installed
317 base = set(module_name
318 for installer in self.read(cr, uid, ids, context=context)
319 for module_name, to_install in installer.iteritems()
320 if module_name != 'id'
321 if type(self._columns[module_name]) is fields.boolean
324 hooks_results = set()
326 hook = getattr(self, '_if_%s'%(module), None)
328 hooks_results.update(hook(cr, uid, ids, context=None) or set())
331 module for requirements, consequences \
332 in self._install_if.iteritems()
333 if base.issuperset(requirements)
334 for module in consequences)
336 return (base | hooks_results | additionals) - set(
337 map(attrgetter('name'), self._already_installed(cr, uid, context)))
339 def default_get(self, cr, uid, fields_list, context=None):
340 ''' If an addon is already installed, check it by default
342 defaults = super(res_config_installer, self).default_get(
343 cr, uid, fields_list, context=context)
345 return dict(defaults,
347 map(attrgetter('name'),
348 self._already_installed(cr, uid, context=context)),
351 def fields_get(self, cr, uid, fields=None, context=None, read_access=True):
352 """ If an addon is already installed, set it to readonly as
353 res.config.installer doesn't handle uninstallations of already
356 fields = super(res_config_installer, self).fields_get(
357 cr, uid, fields, context, read_access)
359 for module in self._already_installed(cr, uid, context=context):
360 fields[module.name].update(
362 help=fields[module.name].get('help', '') +
363 _('\n\nThis addon is already installed on your system'))
367 def execute(self, cr, uid, ids, context=None):
368 modules = self.pool.get('ir.module.module')
369 to_install = list(self.modules_to_install(
370 cr, uid, ids, context=context))
371 self.logger.notifyChannel(
372 'installer', netsvc.LOG_INFO,
373 'Selecting addons %s to install'%to_install)
374 modules.state_update(
376 modules.search(cr, uid, [('name','in',to_install)]),
377 'to install', ['uninstalled'], context=context)
380 pooler.restart_pool(cr.dbname, update_module=True)
381 res_config_installer()
383 DEPRECATION_MESSAGE = 'You are using an addon using old-style configuration '\
384 'wizards (ir.actions.configuration.wizard). Old-style configuration '\
385 'wizards have been deprecated.\n'\
386 'The addon should be migrated to res.config objects.'
387 class ir_actions_configuration_wizard(osv.osv_memory):
388 ''' Compatibility configuration wizard
390 The old configuration wizard has been replaced by res.config, but in order
391 not to break existing but not-yet-migrated addons, the old wizard was
392 reintegrated and gutted.
394 _name='ir.actions.configuration.wizard'
395 _inherit = 'res.config'
397 def _next_action_note(self, cr, uid, ids, context=None):
398 next = self._next_action(cr, uid)
400 # if the next one is also an old-style extension, you never know...
403 return "Click 'Continue' to configure the next addon..."
404 return "Your database is now fully configured.\n\n"\
405 "Click 'Continue' and enjoy your OpenERP experience..."
408 'note': fields.text('Next Wizard', readonly=True),
411 'note': _next_action_note,
414 def execute(self, cr, uid, ids, context=None):
415 self.logger.notifyChannel(
416 'configuration', netsvc.LOG_WARNING, DEPRECATION_MESSAGE)
418 ir_actions_configuration_wizard()
420 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: