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 ##############################################################################
25 from operator import attrgetter
27 from osv import osv, fields
29 from tools.translate import _
35 class res_config_configurable(osv.osv_memory):
36 ''' Base classes for new-style configuration items
38 Configuration items should inherit from this class, implement
39 the execute method (and optionally the cancel one) and have
40 their view inherit from the related res_config_view_base view.
43 logger = netsvc.Logger()
45 def _progress(self, cr, uid, context=None):
46 total = self.pool.get('ir.actions.todo')\
47 .search_count(cr, uid, [], context)
48 open = self.pool.get('ir.actions.todo')\
49 .search_count(cr, uid, [('state','<>','open')], context)
51 return round(open*100./total)
54 def _get_image(self, cr, uid, context=None):
55 file_no = str(random.randint(1,3))
56 path = os.path.join('base','res','config_pixmaps/%s.png'%file_no)
57 file_data = tools.file_open(path,'rb').read()
58 return base64.encodestring(file_data)
61 progress = fields.float('Configuration Progress', readonly=True),
62 config_logo = fields.binary('Image', readonly=True),
66 config_logo = _get_image
69 def _next_action(self, cr, uid):
70 todos = self.pool.get('ir.actions.todo')
71 self.logger.notifyChannel('actions', netsvc.LOG_INFO,
72 'getting next %s' % todos)
73 active_todos = todos.search(cr, uid, [('state','=','open')],
77 todo_obj = todos.browse(cr, uid, active_todos[0], context=None)
78 todo_groups = map(lambda x:x.id, todo_obj.groups_id)
80 cr.execute("select 1 from res_groups_users_rel where uid=%s and gid IN %s",(uid, tuple(todo_groups),))
81 dont_skip_todo = bool(cr.fetchone())
83 return todos.browse(cr, uid, active_todos[0], context=None)
85 todos.write(cr, uid, active_todos[0], {'state':'skip'}, context=None)
86 return self._next_action(cr, uid)
89 def _set_previous_todo(self, cr, uid, state):
90 """ lookup the previous (which is still the next at this point)
91 ir.actions.todo, set it to whatever state was provided.
94 `LookupError`: if we couldn't find *any* previous todo
95 `ValueError`: if no state is provided
96 anything ir_actions_todo.write can throw
98 # this is ultra brittle, but apart from storing the todo id
99 # into the res.config view, I'm not sure how to get the
101 previous_todo = self._next_action(cr, uid)
102 if not previous_todo:
103 raise LookupError(_("Couldn't find previous ir.actions.todo"))
105 raise ValueError(_("Can't set an ir.actions.todo's state to "
107 previous_todo.write({'state':state})
109 def _next(self, cr, uid):
110 self.logger.notifyChannel('actions', netsvc.LOG_INFO,
111 'getting next operation')
112 next = self._next_action(cr, uid)
113 self.logger.notifyChannel('actions', netsvc.LOG_INFO,
114 'next action is %s' % next)
116 action = next.action_id
118 'view_mode': action.view_mode,
119 'view_type': action.view_type,
120 'view_id': action.view_id and [action.view_id.id] or False,
121 'res_model': action.res_model,
123 'target': action.target,
126 'view_mode': action.view_mode,
127 'view_type': action.view_type,
128 'view_id': action.view_id and [action.view_id.id] or False,
129 'res_model': action.res_model,
131 'target': action.target,
133 self.logger.notifyChannel(
134 'actions', netsvc.LOG_INFO,
135 'all configuration actions have been executed')
137 current_user_menu = self.pool.get('res.users')\
138 .browse(cr, uid, uid).menu_id
139 # return the action associated with the menu
140 return self.pool.get(current_user_menu.type)\
141 .read(cr, uid, current_user_menu.id)
143 def start(self, cr, uid, ids, context=None):
145 ids2 = self.pool.get('ir.actions.todo').search(cr, uid, [], context=context)
146 for todo in self.pool.get('ir.actions.todo').browse(cr, uid, ids2, context=context):
147 if (todo.restart=='always') or (todo.restart=='onskip' and (todo.state in ('skip','cancel'))):
148 todo.write({'state':'open'})
149 return self.next(cr, uid, ids, context)
151 def next(self, cr, uid, ids, context=None):
152 """ Returns the next todo action to execute (using the default
155 return self._next(cr, uid)
157 def execute(self, cr, uid, ids, context=None):
158 """ Method called when the user clicks on the ``Next`` button.
160 Execute *must* be overloaded unless ``action_next`` is overloaded
161 (which is something you generally don't need to do).
163 If ``execute`` returns an action dictionary, that action is executed
164 rather than just going to the next configuration item.
166 raise NotImplementedError(
167 'Configuration items need to implement execute')
168 def cancel(self, cr, uid, ids, context=None):
169 """ Method called when the user click on the ``Skip`` button.
171 ``cancel`` should be overloaded instead of ``action_skip``. As with
172 ``execute``, if it returns an action dictionary that action is
173 executed in stead of the default (going to the next configuration item)
175 The default implementation is a NOOP.
177 ``cancel`` is also called by the default implementation of
182 def action_next(self, cr, uid, ids, context=None):
183 """ Action handler for the ``next`` event.
185 Sets the status of the todo the event was sent from to
186 ``done``, calls ``execute`` and -- unless ``execute`` returned
187 an action dictionary -- executes the action provided by calling
190 self._set_previous_todo(cr, uid, state='done')
191 next = self.execute(cr, uid, ids, context=None)
193 return self.next(cr, uid, ids, context=context)
195 def action_skip(self, cr, uid, ids, context=None):
196 """ Action handler for the ``skip`` event.
198 Sets the status of the todo the event was sent from to
199 ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
200 an action dictionary -- executes the action provided by calling
203 self._set_previous_todo(cr, uid, state='skip')
204 next = self.cancel(cr, uid, ids, context=None)
206 return self.next(cr, uid, ids, context=context)
208 def action_cancel(self, cr, uid, ids, context=None):
209 """ Action handler for the ``cancel`` event. That event isn't
210 generated by the res.config.view.base inheritable view, the
211 inherited view has to overload one of the buttons (or add one
214 Sets the status of the todo the event was sent from to
215 ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
216 an action dictionary -- executes the action provided by calling
219 self._set_previous_todo(cr, uid, state='cancel')
220 next = self.cancel(cr, uid, ids, context=None)
222 return self.next(cr, uid, ids, context=context)
224 res_config_configurable()
226 class res_config_installer(osv.osv_memory):
227 """ New-style configuration base specialized for addons selection
233 Subclasses can simply define a number of _columns as
234 fields.boolean objects. The keys (column names) should be the
235 names of the addons to install (when selected). Upon action
236 execution, selected boolean fields (and those only) will be
237 interpreted as addons to install, and batch-installed.
242 It is also possible to require the installation of an additional
243 addon set when a specific preset of addons has been marked for
244 installation (in the basic usage only, additionals can't depend on
247 These additionals are defined through the ``_install_if``
248 property. This property is a mapping of a collection of addons (by
249 name) to a collection of addons (by name) [#]_, and if all the *key*
250 addons are selected for installation, then the *value* ones will
251 be selected as well. For example::
254 ('sale','crm'): ['sale_crm'],
257 This will install the ``sale_crm`` addon if and only if both the
258 ``sale`` and ``crm`` addons are selected for installation.
260 You can define as many additionals as you wish, and additionals
261 can overlap in key and value. For instance::
264 ('sale','crm'): ['sale_crm'],
265 ('sale','project'): ['project_mrp'],
268 will install both ``sale_crm`` and ``project_mrp`` if all of
269 ``sale``, ``crm`` and ``project`` are selected for installation.
274 Subclasses might also need to express dependencies more complex
275 than that provided by additionals. In this case, it's possible to
276 define methods of the form ``_if_%(name)s`` where ``name`` is the
277 name of a boolean field. If the field is selected, then the
278 corresponding module will be marked for installation *and* the
279 hook method will be executed.
281 Hook methods take the usual set of parameters (cr, uid, ids,
282 context) and can return a collection of additional addons to
283 install (if they return anything, otherwise they should not return
284 anything, though returning any "falsy" value such as None or an
285 empty collection will have the same effect).
290 The last hook is to simply overload the ``modules_to_install``
291 method, which implements all the mechanisms above. This method
292 takes the usual set of parameters (cr, uid, ids, context) and
293 returns a ``set`` of addons to install (addons selected by the
294 above methods minus addons from the *basic* set which are already
295 installed) [#]_ so an overloader can simply manipulate the ``set``
296 returned by ``res_config_installer.modules_to_install`` to add or
299 Skipping the installer
300 ----------------------
302 Unless it is removed from the view, installers have a *skip*
303 button which invokes ``action_skip`` (and the ``cancel`` hook from
304 ``res.config``). Hooks and additionals *are not run* when skipping
305 installation, even for already installed addons.
307 Again, setup your hooks accordinly.
309 .. [#] note that since a mapping key needs to be hashable, it's
310 possible to use a tuple or a frozenset, but not a list or a
313 .. [#] because the already-installed modules are only pruned at
314 the very end of ``modules_to_install``, additionals and
315 hooks depending on them *are guaranteed to execute*. Setup
316 your hooks accordingly.
318 _name = 'res.config.installer'
319 _inherit = 'res.config'
323 def _already_installed(self, cr, uid, context=None):
324 """ For each module (boolean fields in a res.config.installer),
325 check if it's already installed (neither uninstallable nor uninstalled)
326 and if it is, check it by default
328 modules = self.pool.get('ir.module.module')
330 selectable = [field for field in self._columns
331 if type(self._columns[field]) is fields.boolean]
332 return modules.browse(
334 modules.search(cr, uid,
335 [('name','in',selectable),
336 ('state','not in',['uninstallable', 'uninstalled'])],
341 def modules_to_install(self, cr, uid, ids, context=None):
342 """ selects all modules to install:
344 * checked boolean fields
345 * return values of hook methods. Hook methods are of the form
346 ``_if_%(addon_name)s``, and are called if the corresponding
347 addon is marked for installation. They take the arguments
348 cr, uid, ids and context, and return an iterable of addon
350 * additionals, additionals are setup through the ``_install_if``
351 class variable. ``_install_if`` is a dict of {iterable:iterable}
352 where key and value are iterables of addon names.
354 If all the addons in the key are selected for installation
355 (warning: addons added through hooks don't count), then the
356 addons in the value are added to the set of modules to install
357 * not already installed
359 base = set(module_name
360 for installer in self.read(cr, uid, ids, context=context)
361 for module_name, to_install in installer.iteritems()
362 if module_name != 'id'
363 if type(self._columns[module_name]) is fields.boolean
366 hooks_results = set()
368 hook = getattr(self, '_if_%s'%(module), None)
370 hooks_results.update(hook(cr, uid, ids, context=None) or set())
373 module for requirements, consequences \
374 in self._install_if.iteritems()
375 if base.issuperset(requirements)
376 for module in consequences)
378 return (base | hooks_results | additionals) - set(
379 map(attrgetter('name'), self._already_installed(cr, uid, context)))
381 def default_get(self, cr, uid, fields_list, context=None):
382 ''' If an addon is already installed, check it by default
384 defaults = super(res_config_installer, self).default_get(
385 cr, uid, fields_list, context=context)
387 return dict(defaults,
389 map(attrgetter('name'),
390 self._already_installed(cr, uid, context=context)),
393 def fields_get(self, cr, uid, fields=None, context=None, read_access=True):
394 """ If an addon is already installed, set it to readonly as
395 res.config.installer doesn't handle uninstallations of already
398 fields = super(res_config_installer, self).fields_get(
399 cr, uid, fields, context, read_access)
401 for module in self._already_installed(cr, uid, context=context):
402 if module.name not in fields:
404 fields[module.name].update(
406 help=fields[module.name].get('help', '') +
407 _('\n\nThis addon is already installed on your system'))
411 def execute(self, cr, uid, ids, context=None):
412 modules = self.pool.get('ir.module.module')
413 to_install = list(self.modules_to_install(
414 cr, uid, ids, context=context))
415 self.logger.notifyChannel(
416 'installer', netsvc.LOG_INFO,
417 'Selecting addons %s to install'%to_install)
418 modules.state_update(
420 modules.search(cr, uid, [('name','in',to_install)]),
421 'to install', ['uninstalled'], context=context)
424 pooler.restart_pool(cr.dbname, update_module=True)
425 res_config_installer()
427 DEPRECATION_MESSAGE = 'You are using an addon using old-style configuration '\
428 'wizards (ir.actions.configuration.wizard). Old-style configuration '\
429 'wizards have been deprecated.\n'\
430 'The addon should be migrated to res.config objects.'
431 class ir_actions_configuration_wizard(osv.osv_memory):
432 ''' Compatibility configuration wizard
434 The old configuration wizard has been replaced by res.config, but in order
435 not to break existing but not-yet-migrated addons, the old wizard was
436 reintegrated and gutted.
438 _name='ir.actions.configuration.wizard'
439 _inherit = 'res.config'
441 def _next_action_note(self, cr, uid, ids, context=None):
442 next = self._next_action(cr, uid)
444 # if the next one is also an old-style extension, you never know...
447 return "Click 'Continue' to configure the next addon..."
448 return "Your database is now fully configured.\n\n"\
449 "Click 'Continue' and enjoy your OpenERP experience..."
452 'note': fields.text('Next Wizard', readonly=True),
455 'note': _next_action_note,
458 def execute(self, cr, uid, ids, context=None):
459 self.logger.notifyChannel(
460 'configuration', netsvc.LOG_WARNING, DEPRECATION_MESSAGE)
462 ir_actions_configuration_wizard()
464 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: