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,
125 self.logger.notifyChannel(
126 'actions', netsvc.LOG_INFO,
127 'all configuration actions have been executed')
129 current_user_menu = self.pool.get('res.users')\
130 .browse(cr, uid, uid).menu_id
131 # return the action associated with the menu
132 return self.pool.get(current_user_menu.type)\
133 .read(cr, uid, current_user_menu.id)
135 def start(self, cr, uid, ids, context=None):
137 ids2 = self.pool.get('ir.actions.todo').search(cr, uid, [], context=context)
138 for todo in self.pool.get('ir.actions.todo').browse(cr, uid, ids2, context=context):
139 if (todo.restart=='always') or (todo.restart=='onskip' and (todo.state in ('skip','cancel'))):
140 todo.write({'state':'open'})
141 return self.next(cr, uid, ids, context)
143 def next(self, cr, uid, ids, context=None):
144 """ Returns the next todo action to execute (using the default
147 return self._next(cr, uid)
149 def execute(self, cr, uid, ids, context=None):
150 """ Method called when the user clicks on the ``Next`` button.
152 Execute *must* be overloaded unless ``action_next`` is overloaded
153 (which is something you generally don't need to do).
155 If ``execute`` returns an action dictionary, that action is executed
156 rather than just going to the next configuration item.
158 raise NotImplementedError(
159 'Configuration items need to implement execute')
160 def cancel(self, cr, uid, ids, context=None):
161 """ Method called when the user click on the ``Skip`` button.
163 ``cancel`` should be overloaded instead of ``action_skip``. As with
164 ``execute``, if it returns an action dictionary that action is
165 executed in stead of the default (going to the next configuration item)
167 The default implementation is a NOOP.
169 ``cancel`` is also called by the default implementation of
174 def action_next(self, cr, uid, ids, context=None):
175 """ Action handler for the ``next`` event.
177 Sets the status of the todo the event was sent from to
178 ``done``, calls ``execute`` and -- unless ``execute`` returned
179 an action dictionary -- executes the action provided by calling
182 self._set_previous_todo(cr, uid, state='done')
183 next = self.execute(cr, uid, ids, context=None)
185 return self.next(cr, uid, ids, context=context)
187 def action_skip(self, cr, uid, ids, context=None):
188 """ Action handler for the ``skip`` event.
190 Sets the status of the todo the event was sent from to
191 ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
192 an action dictionary -- executes the action provided by calling
195 self._set_previous_todo(cr, uid, state='skip')
196 next = self.cancel(cr, uid, ids, context=None)
198 return self.next(cr, uid, ids, context=context)
200 def action_cancel(self, cr, uid, ids, context=None):
201 """ Action handler for the ``cancel`` event. That event isn't
202 generated by the res.config.view.base inheritable view, the
203 inherited view has to overload one of the buttons (or add one
206 Sets the status of the todo the event was sent from to
207 ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
208 an action dictionary -- executes the action provided by calling
211 self._set_previous_todo(cr, uid, state='cancel')
212 next = self.cancel(cr, uid, ids, context=None)
214 return self.next(cr, uid, ids, context=context)
216 res_config_configurable()
218 class res_config_installer(osv.osv_memory):
219 """ New-style configuration base specialized for addons selection
225 Subclasses can simply define a number of _columns as
226 fields.boolean objects. The keys (column names) should be the
227 names of the addons to install (when selected). Upon action
228 execution, selected boolean fields (and those only) will be
229 interpreted as addons to install, and batch-installed.
234 It is also possible to require the installation of an additional
235 addon set when a specific preset of addons has been marked for
236 installation (in the basic usage only, additionals can't depend on
239 These additionals are defined through the ``_install_if``
240 property. This property is a mapping of a collection of addons (by
241 name) to a collection of addons (by name) [#]_, and if all the *key*
242 addons are selected for installation, then the *value* ones will
243 be selected as well. For example::
246 ('sale','crm'): ['sale_crm'],
249 This will install the ``sale_crm`` addon if and only if both the
250 ``sale`` and ``crm`` addons are selected for installation.
252 You can define as many additionals as you wish, and additionals
253 can overlap in key and value. For instance::
256 ('sale','crm'): ['sale_crm'],
257 ('sale','project'): ['project_mrp'],
260 will install both ``sale_crm`` and ``project_mrp`` if all of
261 ``sale``, ``crm`` and ``project`` are selected for installation.
266 Subclasses might also need to express dependencies more complex
267 than that provided by additionals. In this case, it's possible to
268 define methods of the form ``_if_%(name)s`` where ``name`` is the
269 name of a boolean field. If the field is selected, then the
270 corresponding module will be marked for installation *and* the
271 hook method will be executed.
273 Hook methods take the usual set of parameters (cr, uid, ids,
274 context) and can return a collection of additional addons to
275 install (if they return anything, otherwise they should not return
276 anything, though returning any "falsy" value such as None or an
277 empty collection will have the same effect).
282 The last hook is to simply overload the ``modules_to_install``
283 method, which implements all the mechanisms above. This method
284 takes the usual set of parameters (cr, uid, ids, context) and
285 returns a ``set`` of addons to install (addons selected by the
286 above methods minus addons from the *basic* set which are already
287 installed) [#]_ so an overloader can simply manipulate the ``set``
288 returned by ``res_config_installer.modules_to_install`` to add or
291 Skipping the installer
292 ----------------------
294 Unless it is removed from the view, installers have a *skip*
295 button which invokes ``action_skip`` (and the ``cancel`` hook from
296 ``res.config``). Hooks and additionals *are not run* when skipping
297 installation, even for already installed addons.
299 Again, setup your hooks accordinly.
301 .. [#] note that since a mapping key needs to be hashable, it's
302 possible to use a tuple or a frozenset, but not a list or a
305 .. [#] because the already-installed modules are only pruned at
306 the very end of ``modules_to_install``, additionals and
307 hooks depending on them *are guaranteed to execute*. Setup
308 your hooks accordingly.
310 _name = 'res.config.installer'
311 _inherit = 'res.config'
315 def _already_installed(self, cr, uid, context=None):
316 """ For each module (boolean fields in a res.config.installer),
317 check if it's already installed (neither uninstallable nor uninstalled)
318 and if it is, check it by default
320 modules = self.pool.get('ir.module.module')
322 selectable = [field for field in self._columns
323 if type(self._columns[field]) is fields.boolean]
324 return modules.browse(
326 modules.search(cr, uid,
327 [('name','in',selectable),
328 ('state','not in',['uninstallable', 'uninstalled'])],
333 def modules_to_install(self, cr, uid, ids, context=None):
334 """ selects all modules to install:
336 * checked boolean fields
337 * return values of hook methods. Hook methods are of the form
338 ``_if_%(addon_name)s``, and are called if the corresponding
339 addon is marked for installation. They take the arguments
340 cr, uid, ids and context, and return an iterable of addon
342 * additionals, additionals are setup through the ``_install_if``
343 class variable. ``_install_if`` is a dict of {iterable:iterable}
344 where key and value are iterables of addon names.
346 If all the addons in the key are selected for installation
347 (warning: addons added through hooks don't count), then the
348 addons in the value are added to the set of modules to install
349 * not already installed
351 base = set(module_name
352 for installer in self.read(cr, uid, ids, context=context)
353 for module_name, to_install in installer.iteritems()
354 if module_name != 'id'
355 if type(self._columns[module_name]) is fields.boolean
358 hooks_results = set()
360 hook = getattr(self, '_if_%s'%(module), None)
362 hooks_results.update(hook(cr, uid, ids, context=None) or set())
365 module for requirements, consequences \
366 in self._install_if.iteritems()
367 if base.issuperset(requirements)
368 for module in consequences)
370 return (base | hooks_results | additionals) - set(
371 map(attrgetter('name'), self._already_installed(cr, uid, context)))
373 def default_get(self, cr, uid, fields_list, context=None):
374 ''' If an addon is already installed, check it by default
376 defaults = super(res_config_installer, self).default_get(
377 cr, uid, fields_list, context=context)
379 return dict(defaults,
381 map(attrgetter('name'),
382 self._already_installed(cr, uid, context=context)),
385 def fields_get(self, cr, uid, fields=None, context=None, read_access=True):
386 """ If an addon is already installed, set it to readonly as
387 res.config.installer doesn't handle uninstallations of already
390 fields = super(res_config_installer, self).fields_get(
391 cr, uid, fields, context, read_access)
393 for module in self._already_installed(cr, uid, context=context):
394 if module.name not in fields:
396 fields[module.name].update(
398 help=fields[module.name].get('help', '') +
399 _('\n\nThis addon is already installed on your system'))
403 def execute(self, cr, uid, ids, context=None):
404 modules = self.pool.get('ir.module.module')
405 to_install = list(self.modules_to_install(
406 cr, uid, ids, context=context))
407 self.logger.notifyChannel(
408 'installer', netsvc.LOG_INFO,
409 'Selecting addons %s to install'%to_install)
410 modules.state_update(
412 modules.search(cr, uid, [('name','in',to_install)]),
413 'to install', ['uninstalled'], context=context)
414 pooler.restart_pool(cr.dbname, update_module=True)
415 res_config_installer()
417 DEPRECATION_MESSAGE = 'You are using an addon using old-style configuration '\
418 'wizards (ir.actions.configuration.wizard). Old-style configuration '\
419 'wizards have been deprecated.\n'\
420 'The addon should be migrated to res.config objects.'
421 class ir_actions_configuration_wizard(osv.osv_memory):
422 ''' Compatibility configuration wizard
424 The old configuration wizard has been replaced by res.config, but in order
425 not to break existing but not-yet-migrated addons, the old wizard was
426 reintegrated and gutted.
428 _name='ir.actions.configuration.wizard'
429 _inherit = 'res.config'
431 def _next_action_note(self, cr, uid, ids, context=None):
432 next = self._next_action(cr, uid)
434 # if the next one is also an old-style extension, you never know...
437 return "Click 'Continue' to configure the next addon..."
438 return "Your database is now fully configured.\n\n"\
439 "Click 'Continue' and enjoy your OpenERP experience..."
442 'note': fields.text('Next Wizard', readonly=True),
445 'note': _next_action_note,
448 def execute(self, cr, uid, ids, context=None):
449 self.logger.notifyChannel(
450 'configuration', netsvc.LOG_WARNING, DEPRECATION_MESSAGE)
452 ir_actions_configuration_wizard()
454 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: