[FIX] res.config.settings: fix method fields_get
[odoo/odoo.git] / openerp / addons / base / res / res_config.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21 import logging
22 from operator import attrgetter, itemgetter
23
24 from osv import osv, fields
25 from tools.translate import _
26 import netsvc
27 from tools import ustr
28 import pooler
29
30 _logger = logging.getLogger(__name__)
31
32 class res_config_configurable(osv.osv_memory):
33     ''' Base classes for new-style configuration items
34
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.
38     '''
39     _name = 'res.config'
40     _inherit = 'ir.wizard.screen'
41
42     def _next_action(self, cr, uid, context=None):
43         Todos = self.pool['ir.actions.todo']
44         _logger.info('getting next %s', Todos)
45
46         active_todos = Todos.browse(cr, uid,
47             Todos.search(cr, uid, ['&', ('type', '=', 'automatic'), ('state','=','open')]),
48                                     context=context)
49
50         user_groups = set(map(
51             lambda g: g.id,
52             self.pool['res.users'].browse(cr, uid, [uid], context=context)[0].groups_id))
53
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)))
58         ]
59
60         if valid_todos_for_user:
61             return valid_todos_for_user[0]
62
63         return None
64
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)
69         if next:
70             res = next.action_launch(context=context)
71             res['nodestroy'] = False
72             return res
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'}
77
78     def start(self, cr, uid, ids, context=None):
79         return self.next(cr, uid, ids, context)
80
81     def next(self, cr, uid, ids, context=None):
82         """ Returns the next todo action to execute (using the default
83         sort order)
84         """
85         return self._next(cr, uid, context=context)
86
87     def execute(self, cr, uid, ids, context=None):
88         """ Method called when the user clicks on the ``Next`` button.
89
90         Execute *must* be overloaded unless ``action_next`` is overloaded
91         (which is something you generally don't need to do).
92
93         If ``execute`` returns an action dictionary, that action is executed
94         rather than just going to the next configuration item.
95         """
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.
100
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)
104
105         The default implementation is a NOOP.
106
107         ``cancel`` is also called by the default implementation of
108         ``action_cancel``.
109         """
110         pass
111
112     def action_next(self, cr, uid, ids, context=None):
113         """ Action handler for the ``next`` event.
114
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
118         ``next``.
119         """
120         next = self.execute(cr, uid, ids, context=context)
121         if next: return next
122         return self.next(cr, uid, ids, context=context)
123
124     def action_skip(self, cr, uid, ids, context=None):
125         """ Action handler for the ``skip`` event.
126
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
130         ``next``.
131         """
132         next = self.cancel(cr, uid, ids, context=context)
133         if next: return next
134         return self.next(cr, uid, ids, context=context)
135
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
140         more).
141
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
145         ``next``.
146         """
147         next = self.cancel(cr, uid, ids, context=context)
148         if next: return next
149         return self.next(cr, uid, ids, context=context)
150
151 res_config_configurable()
152
153 class res_config_installer(osv.osv_memory):
154     """ New-style configuration base specialized for addons selection
155     and installation.
156
157     Basic usage
158     -----------
159
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.
165
166     Additional addons
167     -----------------
168
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
172     one another).
173
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::
179
180         _install_if = {
181             ('sale','crm'): ['sale_crm'],
182         }
183
184     This will install the ``sale_crm`` addon if and only if both the
185     ``sale`` and ``crm`` addons are selected for installation.
186
187     You can define as many additionals as you wish, and additionals
188     can overlap in key and value. For instance::
189
190         _install_if = {
191             ('sale','crm'): ['sale_crm'],
192             ('sale','project'): ['project_mrp'],
193         }
194
195     will install both ``sale_crm`` and ``project_mrp`` if all of
196     ``sale``, ``crm`` and ``project`` are selected for installation.
197
198     Hook methods
199     ------------
200
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.
207
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).
213
214     Complete control
215     ----------------
216
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
224     remove addons.
225
226     Skipping the installer
227     ----------------------
228
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.
233
234     Again, setup your hooks accordingly.
235
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
238            regular set
239
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.
244     """
245     _name = 'res.config.installer'
246     _inherit = 'res.config'
247
248     _install_if = {}
249
250     def already_installed(self, cr, uid, context=None):
251         """ For each module, check if it's already installed and if it
252         is return its name
253
254         :returns: a list of the already installed modules in this
255                   installer
256         :rtype: [str]
257         """
258         return map(attrgetter('name'),
259                    self._already_installed(cr, uid, context=context))
260
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
265
266         :returns: a list of all installed modules in this installer
267         :rtype: [browse_record]
268         """
269         modules = self.pool.get('ir.module.module')
270
271         selectable = [field for field in self._columns
272                       if type(self._columns[field]) is fields.boolean]
273         return modules.browse(
274             cr, uid,
275             modules.search(cr, uid,
276                            [('name','in',selectable),
277                             ('state','in',['to install', 'installed', 'to upgrade'])],
278                            context=context),
279             context=context)
280
281
282     def modules_to_install(self, cr, uid, ids, context=None):
283         """ selects all modules to install:
284
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
290           names
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.
294
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
299         """
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
305                    if to_install)
306
307         hooks_results = set()
308         for module in base:
309             hook = getattr(self, '_if_%s'%(module), None)
310             if hook:
311                 hooks_results.update(hook(cr, uid, ids, context=None) or set())
312
313         additionals = set(
314             module for requirements, consequences \
315                        in self._install_if.iteritems()
316                    if base.issuperset(requirements)
317                    for module in consequences)
318
319         return (base | hooks_results | additionals).difference(
320                     self.already_installed(cr, uid, context))
321
322     def default_get(self, cr, uid, fields_list, context=None):
323         ''' If an addon is already installed, check it by default
324         '''
325         defaults = super(res_config_installer, self).default_get(
326             cr, uid, fields_list, context=context)
327
328         return dict(defaults,
329                     **dict.fromkeys(
330                         self.already_installed(cr, uid, context=context),
331                         True))
332
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
336         installed addons
337         """
338         fields = super(res_config_installer, self).fields_get(
339             cr, uid, fields, context, write_access)
340
341         for name in self.already_installed(cr, uid, context=context):
342             if name not in fields:
343                 continue
344             fields[name].update(
345                 readonly=True,
346                 help= ustr(fields[name].get('help', '')) +
347                      _('\n\nThis addon is already installed on your system'))
348         return fields
349
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(
356             cr, uid,
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()
362
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
369
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.
373     '''
374     _name='ir.actions.configuration.wizard'
375     _inherit = 'res.config'
376
377     def _next_action_note(self, cr, uid, ids, context=None):
378         next = self._next_action(cr, uid)
379         if next:
380             # if the next one is also an old-style extension, you never know...
381             if next.note:
382                 return next.note
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...")
386
387     _columns = {
388         'note': fields.text('Next Wizard', readonly=True),
389         }
390     _defaults = {
391         'note': _next_action_note,
392         }
393
394     def execute(self, cr, uid, ids, context=None):
395         _logger.warning(DEPRECATION_MESSAGE)
396
397 ir_actions_configuration_wizard()
398
399
400
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::
405
406             class my_config_wizard(osv.osv_memory):
407                 _name = 'my.settings'
408                 _inherit = 'res.config.settings'
409                 _columns = {
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(...),
414                 }
415
416         The method ``execute`` is automatically called after creating and writing on records.
417         It provides some support based on a naming convention:
418
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.
421
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.
425
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``.
428
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
431             of those fields.
432
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.
437     """
438     _name = 'res.config.settings'
439     _inherit = 'res.config'
440
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)
444         return id
445
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)
449         return res
450
451     def copy(self, cr, uid, id, values, context=None):
452         raise osv.except_osv(_("Cannot duplicate configuration!"), "")
453
454     def _get_classified_fields(self, cr, uid, context=None):
455         """ return a dictionary with the fields classified by category::
456
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', ...],
461                 }
462         """
463         ir_model_data = self.pool.get('ir.model.data')
464         ir_module = self.pool.get('ir.module.module')
465         def ref(xml_id):
466             mod, xml = xml_id.split('.', 1)
467             return ir_model_data.get_object(cr, uid, mod, xml, context)
468
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)))
479             else:
480                 others.append(name)
481
482         return {'default': defaults, 'group': groups, 'module': modules, 'other': others}
483
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)
488
489         res = super(res_config_settings, self).default_get(cr, uid, fields, context)
490
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:
495                 res[name] = value
496
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
500
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')
504
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))
509
510         return res
511
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
519         return res
520
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)
527
528         config = self.browse(cr, uid, ids[0], context)
529
530         # default values fields
531         for name, model, field in classified['default']:
532             ir_values.set_default(cr, uid, model, field, config[name])
533
534         # group fields: modify group / implied groups
535         for name, group, implied_group in classified['group']:
536             if config[name]:
537                 group.write({'implied_ids': [(4, implied_group.id)]})
538             else:
539                 group.write({'implied_ids': [(3, implied_group.id)]})
540                 implied_group.write({'users': [(3, u.id) for u in group.users]})
541
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)
546
547         # module fields: install immediately the selected modules
548         to_install_ids = []
549         for name, module in classified['module']:
550             if config[name] and module.state == 'uninstalled':
551                 to_install_ids.append(module.id)
552         if to_install_ids:
553             ir_module.button_immediate_install(cr, uid, to_install_ids, context)
554
555         return {}
556
557 res_config_settings()
558
559 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: