[MERGE] base: raise an exception if the format of the bank account is wrong
[odoo/odoo.git] / openerp / addons / base / res / res_users.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #    Copyright (C) 2010-2012 OpenERP s.a. (<http://openerp.com>).
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU Affero General Public License as
10 #    published by the Free Software Foundation, either version 3 of the
11 #    License, or (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU Affero General Public License for more details.
17 #
18 #    You should have received a copy of the GNU Affero General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23
24 from functools import partial
25 import logging
26 from lxml import etree
27 from lxml.builder import E
28 import netsvc
29 from openerp import SUPERUSER_ID
30 import openerp
31 import openerp.exceptions
32 from osv import fields,osv
33 from osv.orm import browse_record
34 import pooler
35 import random
36 from service import security
37 import tools
38 from tools.translate import _
39
40 _logger = logging.getLogger(__name__)
41
42 class groups(osv.osv):
43     _name = "res.groups"
44     _description = "Access Groups"
45     _rec_name = 'full_name'
46
47     def _get_full_name(self, cr, uid, ids, field, arg, context=None):
48         res = {}
49         for g in self.browse(cr, uid, ids, context):
50             if g.category_id:
51                 res[g.id] = '%s / %s' % (g.category_id.name, g.name)
52             else:
53                 res[g.id] = g.name
54         return res
55
56     _columns = {
57         'name': fields.char('Name', size=64, required=True, translate=True),
58         'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
59         'model_access': fields.one2many('ir.model.access', 'group_id', 'Access Controls'),
60         'rule_groups': fields.many2many('ir.rule', 'rule_group_rel',
61             'group_id', 'rule_group_id', 'Rules', domain=[('global', '=', False)]),
62         'menu_access': fields.many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', 'Access Menu'),
63         'comment' : fields.text('Comment', size=250, translate=True),
64         'category_id': fields.many2one('ir.module.category', 'Application', select=True),
65         'full_name': fields.function(_get_full_name, type='char', string='Group Name'),
66     }
67
68     _sql_constraints = [
69         ('name_uniq', 'unique (category_id, name)', 'The name of the group must be unique !')
70     ]
71
72     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
73         # add explicit ordering if search is sorted on full_name
74         if order and order.startswith('full_name'):
75             ids = super(groups, self).search(cr, uid, args, context=context)
76             gs = self.browse(cr, uid, ids, context)
77             gs.sort(key=lambda g: g.full_name, reverse=order.endswith('DESC'))
78             gs = gs[offset:offset+limit] if limit else gs[offset:]
79             return map(int, gs)
80         return super(groups, self).search(cr, uid, args, offset, limit, order, context, count)
81
82     def copy(self, cr, uid, id, default=None, context=None):
83         group_name = self.read(cr, uid, [id], ['name'])[0]['name']
84         default.update({'name': _('%s (copy)')%group_name})
85         return super(groups, self).copy(cr, uid, id, default, context)
86
87     def write(self, cr, uid, ids, vals, context=None):
88         if 'name' in vals:
89             if vals['name'].startswith('-'):
90                 raise osv.except_osv(_('Error'),
91                         _('The name of the group can not start with "-"'))
92         res = super(groups, self).write(cr, uid, ids, vals, context=context)
93         self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
94         return res
95
96 groups()
97
98 class res_users(osv.osv):
99     """ User class. A res.users record models an OpenERP user and is different
100         from an employee.
101
102         res.users class now inherits from res.partner. The partner model is
103         used to store the data related to the partner: lang, name, address,
104         avatar, ... The user model is now dedicated to technical data.
105     """
106     __admin_ids = {}
107     _uid_cache = {}
108     _inherits = {
109         'res.partner': 'partner_id',
110     }
111     _name = "res.users"
112     _description = 'Users'
113     _order = 'login'
114
115     def _set_new_password(self, cr, uid, id, name, value, args, context=None):
116         if value is False:
117             # Do not update the password if no value is provided, ignore silently.
118             # For example web client submits False values for all empty fields.
119             return
120         if uid == id:
121             # To change their own password users must use the client-specific change password wizard,
122             # so that the new password is immediately used for further RPC requests, otherwise the user
123             # will face unexpected 'Access Denied' exceptions.
124             raise osv.except_osv(_('Operation Canceled'), _('Please use the change password wizard (in User Preferences or User menu) to change your own password.'))
125         self.write(cr, uid, id, {'password': value})
126
127     def _get_password(self, cr, uid, ids, arg, karg, context=None):
128         return dict.fromkeys(ids, '')
129     
130     _columns = {
131         'id': fields.integer('ID'),
132         'login_date': fields.date('Latest connection', select=1),
133         'partner_id': fields.many2one('res.partner', required=True,
134             string='Related Partner', ondelete='cascade',
135             help='Partner-related data of the user'),
136         'login': fields.char('Login', size=64, required=True,
137             help="Used to log into the system"),
138         'password': fields.char('Password', size=64, invisible=True,
139             help="Keep empty if you don't want the user to be able to connect on the system."),
140         'new_password': fields.function(_get_password, type='char', size=64,
141             fnct_inv=_set_new_password, string='Set Password',
142             help="Specify a value only when creating a user or if you're "\
143                  "changing the user's password, otherwise leave empty. After "\
144                  "a change of password, the user has to login again."),
145         'signature': fields.text('Signature', size=64),
146         'active': fields.boolean('Active'),
147         'action_id': fields.many2one('ir.actions.actions', 'Home Action', help="If specified, this action will be opened at logon for this user, in addition to the standard menu."),
148         'menu_id': fields.many2one('ir.actions.actions', 'Menu Action', help="If specified, the action will replace the standard menu for this user."),
149         'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
150         # Special behavior for this field: res.company.search() will only return the companies
151         # available to the current user (should be the user's companies?), when the user_preference
152         # context is set.
153         'company_id': fields.many2one('res.company', 'Company', required=True,
154             help='The company this user is currently working for.', context={'user_preference': True}),
155         'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
156         # backward compatibility fields
157         'user_email': fields.related('email', type='char',
158             deprecated='Use the email field instead of user_email. This field will be removed with OpenERP 7.1.'),
159     }
160
161     def on_change_company_id(self, cr, uid, ids, company_id):
162         return {'warning' : {
163                     'title': _("Company Switch Warning"),
164                     'message': _("Please keep in mind that documents currently displayed may not be relevant after switching to another company. If you have unsaved changes, please make sure to save and close all forms before switching to a different company. (You can click on Cancel in the User Preferences now)"),
165                 }
166         }
167
168     def onchange_type(self, cr, uid, ids, is_company, context=None):
169         """ Wrapper on the user.partner onchange_type, because some calls to the
170             partner form view applied to the user may trigger the
171             partner.onchange_type method, but applied to the user object.
172         """
173         partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
174         return self.pool.get('res.partner').onchange_type(cr, uid, partner_ids, is_company, context=context)
175
176     def onchange_address(self, cr, uid, ids, use_parent_address, parent_id, context=None):
177         """ Wrapper on the user.partner onchange_address, because some calls to the
178             partner form view applied to the user may trigger the
179             partner.onchange_type method, but applied to the user object.
180         """
181         partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
182         return self.pool.get('res.partner').onchange_address(cr, uid, partner_ids, use_parent_address, parent_id, context=context)
183
184     def read(self,cr, uid, ids, fields=None, context=None, load='_classic_read'):
185         def override_password(o):
186             if 'password' in o and ( 'id' not in o or o['id'] != uid ):
187                 o['password'] = '********'
188             return o
189         result = super(res_users, self).read(cr, uid, ids, fields, context, load)
190         canwrite = self.pool.get('ir.model.access').check(cr, uid, 'res.users', 'write', False)
191         if not canwrite:
192             if isinstance(ids, (int, long)):
193                 result = override_password(result)
194             else:
195                 result = map(override_password, result)
196         return result
197
198
199     def _check_company(self, cr, uid, ids, context=None):
200         return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context))
201
202     _constraints = [
203         (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
204     ]
205
206     _sql_constraints = [
207         ('login_key', 'UNIQUE (login)',  'You can not have two users with the same login !')
208     ]
209
210     def _get_company(self,cr, uid, context=None, uid2=False):
211         if not uid2:
212             uid2 = uid
213         user = self.pool.get('res.users').read(cr, uid, uid2, ['company_id'], context)
214         company_id = user.get('company_id', False)
215         return company_id and company_id[0] or False
216
217     def _get_companies(self, cr, uid, context=None):
218         c = self._get_company(cr, uid, context)
219         if c:
220             return [c]
221         return False
222
223     def _get_menu(self,cr, uid, context=None):
224         dataobj = self.pool.get('ir.model.data')
225         try:
226             model, res_id = dataobj.get_object_reference(cr, uid, 'base', 'action_menu_admin')
227             if model != 'ir.actions.act_window':
228                 return False
229             return res_id
230         except ValueError:
231             return False
232
233     def _get_group(self,cr, uid, context=None):
234         dataobj = self.pool.get('ir.model.data')
235         result = []
236         try:
237             dummy,group_id = dataobj.get_object_reference(cr, SUPERUSER_ID, 'base', 'group_user')
238             result.append(group_id)
239             dummy,group_id = dataobj.get_object_reference(cr, SUPERUSER_ID, 'base', 'group_partner_manager')
240             result.append(group_id)
241         except ValueError:
242             # If these groups does not exists anymore
243             pass
244         return result
245
246     _defaults = {
247         'password': '',
248         'active': True,
249         'customer': False,
250         'menu_id': _get_menu,
251         'company_id': _get_company,
252         'company_ids': _get_companies,
253         'groups_id': _get_group,
254         'image': lambda self, cr, uid, ctx={}: self.pool.get('res.partner')._get_default_image(cr, uid, False, ctx, colorize=True),
255     }
256
257     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
258         """ Override of res.users fields_view_get.
259             - if the view is specified: resume with normal behavior
260             - else: the default view is overrided and redirected to the partner
261               view
262         """
263         if not view_id and view_type == 'form':
264             return self.pool.get('res.partner').fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu)
265         return super(res_users, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu)
266
267     # User can write to a few of her own fields (but not her groups for example)
268     SELF_WRITEABLE_FIELDS = ['password', 'signature', 'action_id', 'company_id', 'email', 'name', 'image', 'image_medium', 'image_small']
269
270     def write(self, cr, uid, ids, values, context=None):
271         if not hasattr(ids, '__iter__'):
272             ids = [ids]
273         if ids == [uid]:
274             for key in values.keys():
275                 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
276                     break
277             else:
278                 if 'company_id' in values:
279                     if not (values['company_id'] in self.read(cr, SUPERUSER_ID, uid, ['company_ids'], context=context)['company_ids']):
280                         del values['company_id']
281                 uid = 1 # safe fields only, so we write as super-user to bypass access rights
282
283         res = super(res_users, self).write(cr, uid, ids, values, context=context)
284
285         # clear caches linked to the users
286         self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
287         clear = partial(self.pool.get('ir.rule').clear_cache, cr)
288         map(clear, ids)
289         db = cr.dbname
290         if db in self._uid_cache:
291             for id in ids:
292                 if id in self._uid_cache[db]:
293                     del self._uid_cache[db][id]
294
295         return res
296
297     def unlink(self, cr, uid, ids, context=None):
298         if 1 in ids:
299             raise osv.except_osv(_('Can not remove root user!'), _('You can not remove the admin user as it is used internally for resources created by OpenERP (updates, module installation, ...)'))
300         db = cr.dbname
301         if db in self._uid_cache:
302             for id in ids:
303                 if id in self._uid_cache[db]:
304                     del self._uid_cache[db][id]
305         return super(res_users, self).unlink(cr, uid, ids, context=context)
306
307     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
308         if not args:
309             args=[]
310         if not context:
311             context={}
312         ids = []
313         if name:
314             ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit)
315         if not ids:
316             ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit)
317         return self.name_get(cr, user, ids)
318
319     def copy(self, cr, uid, id, default=None, context=None):
320         user2copy = self.read(cr, uid, [id], ['login','name'])[0]
321         if default is None:
322             default = {}
323         copy_pattern = _("%s (copy)")
324         copydef = dict(login=(copy_pattern % user2copy['login']),
325                        name=(copy_pattern % user2copy['name']),
326                        )
327         copydef.update(default)
328         return super(res_users, self).copy(cr, uid, id, copydef, context)
329
330     def context_get(self, cr, uid, context=None):
331         user = self.browse(cr, SUPERUSER_ID, uid, context)
332         result = {}
333         for k in self._all_columns.keys():
334             if k.startswith('context_'):
335                 context_key = k[8:]
336             elif k in ['lang', 'tz']:
337                 context_key = k
338             else:
339                 context_key = False
340             if context_key:
341                 res = getattr(user,k) or False
342                 if isinstance(res, browse_record):
343                     res = res.id
344                 result[context_key] = res or False
345         return result
346
347     def action_get(self, cr, uid, context=None):
348         dataobj = self.pool.get('ir.model.data')
349         data_id = dataobj._get_id(cr, SUPERUSER_ID, 'base', 'action_res_users_my')
350         return dataobj.browse(cr, uid, data_id, context=context).res_id
351
352     def check_super(self, passwd):
353         if passwd == tools.config['admin_passwd']:
354             return True
355         else:
356             raise openerp.exceptions.AccessDenied()
357
358     def check_credentials(self, cr, uid, password):
359         """ Override this method to plug additional authentication methods"""
360         res = self.search(cr, SUPERUSER_ID, [('id','=',uid),('password','=',password)])
361         if not res:
362             raise openerp.exceptions.AccessDenied()
363
364     def login(self, db, login, password):
365         if not password:
366             return False
367         user_id = False
368         cr = pooler.get_db(db).cursor()
369         try:
370             # autocommit: our single update request will be performed atomically.
371             # (In this way, there is no opportunity to have two transactions
372             # interleaving their cr.execute()..cr.commit() calls and have one
373             # of them rolled back due to a concurrent access.)
374             cr.autocommit(True)
375             # check if user exists
376             res = self.search(cr, SUPERUSER_ID, [('login','=',login)])
377             if res:
378                 user_id = res[0]
379                 # check credentials
380                 self.check_credentials(cr, user_id, password)
381                 # We effectively unconditionally write the res_users line.
382                 # Even w/ autocommit there's a chance the user row will be locked,
383                 # in which case we can't delay the login just for the purpose of
384                 # update the last login date - hence we use FOR UPDATE NOWAIT to
385                 # try to get the lock - fail-fast
386                 # Failing to acquire the lock on the res_users row probably means
387                 # another request is holding it. No big deal, we don't want to
388                 # prevent/delay login in that case. It will also have been logged
389                 # as a SQL error, if anyone cares.
390                 try:
391                     cr.execute("SELECT id FROM res_users WHERE id=%s FOR UPDATE NOWAIT", str(user_id))
392                     cr.execute("UPDATE res_users SET login_date = now() AT TIME ZONE 'UTC' WHERE id=%s", str(user_id))
393                 except Exception, e:
394                     _logger.exception("Failed to update last_login for db:%s login:%s", db, login)
395         except openerp.exceptions.AccessDenied:
396             _logger.info("Login failed for db:%s login:%s", db, login)
397             user_id = False
398         finally:
399             cr.close()
400
401         return user_id
402
403     def authenticate(self, db, login, password, user_agent_env):
404         """Verifies and returns the user ID corresponding to the given
405           ``login`` and ``password`` combination, or False if there was
406           no matching user.
407
408            :param str db: the database on which user is trying to authenticate
409            :param str login: username
410            :param str password: user password
411            :param dict user_agent_env: environment dictionary describing any
412                relevant environment attributes
413         """
414         uid = self.login(db, login, password)
415         if uid == openerp.SUPERUSER_ID:
416             # Successfully logged in as admin!
417             # Attempt to guess the web base url...
418             if user_agent_env and user_agent_env.get('base_location'):
419                 cr = pooler.get_db(db).cursor()
420                 try:
421                     base = user_agent_env['base_location']
422                     self.pool.get('ir.config_parameter').set_param(cr, uid, 'web.base.url', base)
423                     cr.commit()
424                 except Exception:
425                     _logger.exception("Failed to update web.base.url configuration parameter")
426                 finally:
427                     cr.close()
428         return uid
429
430     def check(self, db, uid, passwd):
431         """Verifies that the given (uid, password) is authorized for the database ``db`` and
432            raise an exception if it is not."""
433         if not passwd:
434             # empty passwords disallowed for obvious security reasons
435             raise openerp.exceptions.AccessDenied()
436         if self._uid_cache.get(db, {}).get(uid) == passwd:
437             return
438         cr = pooler.get_db(db).cursor()
439         try:
440             self.check_credentials(cr, uid, passwd)
441             if self._uid_cache.has_key(db):
442                 self._uid_cache[db][uid] = passwd
443             else:
444                 self._uid_cache[db] = {uid:passwd}
445         finally:
446             cr.close()
447
448     def change_password(self, cr, uid, old_passwd, new_passwd, context=None):
449         """Change current user password. Old password must be provided explicitly
450         to prevent hijacking an existing user session, or for cases where the cleartext
451         password is not used to authenticate requests.
452
453         :return: True
454         :raise: openerp.exceptions.AccessDenied when old password is wrong
455         :raise: except_osv when new password is not set or empty
456         """
457         self.check(cr.dbname, uid, old_passwd)
458         if new_passwd:
459             return self.write(cr, uid, uid, {'password': new_passwd})
460         raise osv.except_osv(_('Warning!'), _("Setting empty passwords is not allowed for security reasons!"))
461
462     def preference_save(self, cr, uid, ids, context=None):
463         return {
464             'type': 'ir.actions.client',
465             'tag': 'reload',
466         }
467
468     def preference_change_password(self, cr, uid, ids, context=None):
469         return {
470             'type': 'ir.actions.client',
471             'tag': 'change_password',
472             'target': 'new',
473         }
474
475     def has_group(self, cr, uid, group_ext_id):
476         """Checks whether user belongs to given group.
477
478         :param str group_ext_id: external ID (XML ID) of the group.
479            Must be provided in fully-qualified form (``module.ext_id``), as there
480            is no implicit module to use..
481         :return: True if the current user is a member of the group with the
482            given external ID (XML ID), else False.
483         """
484         assert group_ext_id and '.' in group_ext_id, "External ID must be fully qualified"
485         module, ext_id = group_ext_id.split('.')
486         cr.execute("""SELECT 1 FROM res_groups_users_rel WHERE uid=%s AND gid IN 
487                         (SELECT res_id FROM ir_model_data WHERE module=%s AND name=%s)""",
488                    (uid, module, ext_id))
489         return bool(cr.fetchone())
490
491
492 #
493 # Extension of res.groups and res.users with a relation for "implied" or 
494 # "inherited" groups.  Once a user belongs to a group, it automatically belongs
495 # to the implied groups (transitively).
496 #
497
498 class cset(object):
499     """ A cset (constrained set) is a set of elements that may be constrained to
500         be a subset of other csets.  Elements added to a cset are automatically
501         added to its supersets.  Cycles in the subset constraints are supported.
502     """
503     def __init__(self, xs):
504         self.supersets = set()
505         self.elements = set(xs)
506     def subsetof(self, other):
507         if other is not self:
508             self.supersets.add(other)
509             other.update(self.elements)
510     def update(self, xs):
511         xs = set(xs) - self.elements
512         if xs:      # xs will eventually be empty in case of a cycle
513             self.elements.update(xs)
514             for s in self.supersets:
515                 s.update(xs)
516     def __iter__(self):
517         return iter(self.elements)
518
519 def concat(ls):
520     """ return the concatenation of a list of iterables """
521     res = []
522     for l in ls: res.extend(l)
523     return res
524
525
526
527 class groups_implied(osv.osv):
528     _inherit = 'res.groups'
529
530     def _get_trans_implied(self, cr, uid, ids, field, arg, context=None):
531         "computes the transitive closure of relation implied_ids"
532         memo = {}           # use a memo for performance and cycle avoidance
533         def computed_set(g):
534             if g not in memo:
535                 memo[g] = cset(g.implied_ids)
536                 for h in g.implied_ids:
537                     computed_set(h).subsetof(memo[g])
538             return memo[g]
539
540         res = {}
541         for g in self.browse(cr, SUPERUSER_ID, ids, context):
542             res[g.id] = map(int, computed_set(g))
543         return res
544
545     _columns = {
546         'implied_ids': fields.many2many('res.groups', 'res_groups_implied_rel', 'gid', 'hid',
547             string='Inherits', help='Users of this group automatically inherit those groups'),
548         'trans_implied_ids': fields.function(_get_trans_implied,
549             type='many2many', relation='res.groups', string='Transitively inherits'),
550     }
551
552     def create(self, cr, uid, values, context=None):
553         users = values.pop('users', None)
554         gid = super(groups_implied, self).create(cr, uid, values, context)
555         if users:
556             # delegate addition of users to add implied groups
557             self.write(cr, uid, [gid], {'users': users}, context)
558         return gid
559
560     def write(self, cr, uid, ids, values, context=None):
561         res = super(groups_implied, self).write(cr, uid, ids, values, context)
562         if values.get('users') or values.get('implied_ids'):
563             # add all implied groups (to all users of each group)
564             for g in self.browse(cr, uid, ids):
565                 gids = map(int, g.trans_implied_ids)
566                 vals = {'users': [(4, u.id) for u in g.users]}
567                 super(groups_implied, self).write(cr, uid, gids, vals, context)
568         return res
569
570 class users_implied(osv.osv):
571     _inherit = 'res.users'
572
573     def create(self, cr, uid, values, context=None):
574         groups = values.pop('groups_id', None)
575         user_id = super(users_implied, self).create(cr, uid, values, context)
576         if groups:
577             # delegate addition of groups to add implied groups
578             self.write(cr, uid, [user_id], {'groups_id': groups}, context)
579         return user_id
580
581     def write(self, cr, uid, ids, values, context=None):
582         if not isinstance(ids,list):
583             ids = [ids]
584         res = super(users_implied, self).write(cr, uid, ids, values, context)
585         if values.get('groups_id'):
586             # add implied groups for all users
587             for user in self.browse(cr, uid, ids):
588                 gs = set(concat([g.trans_implied_ids for g in user.groups_id]))
589                 vals = {'groups_id': [(4, g.id) for g in gs]}
590                 super(users_implied, self).write(cr, uid, [user.id], vals, context)
591         return res
592
593 #
594 # Extension of res.groups and res.users for the special groups view in the users
595 # form.  This extension presents groups with selection and boolean widgets:
596 # - Groups are shown by application, with boolean and/or selection fields.
597 #   Selection fields typically defines a role "Name" for the given application.
598 # - Uncategorized groups are presented as boolean fields and grouped in a
599 #   section "Others".
600 #
601 # The user form view is modified by an inherited view (base.user_groups_view);
602 # the inherited view replaces the field 'groups_id' by a set of reified group
603 # fields (boolean or selection fields).  The arch of that view is regenerated
604 # each time groups are changed.
605 #
606 # Naming conventions for reified groups fields:
607 # - boolean field 'in_group_ID' is True iff
608 #       ID is in 'groups_id'
609 # - boolean field 'in_groups_ID1_..._IDk' is True iff
610 #       any of ID1, ..., IDk is in 'groups_id'
611 # - selection field 'sel_groups_ID1_..._IDk' is ID iff
612 #       ID is in 'groups_id' and ID is maximal in the set {ID1, ..., IDk}
613
614 def name_boolean_group(id): return 'in_group_' + str(id)
615 def name_boolean_groups(ids): return 'in_groups_' + '_'.join(map(str, ids))
616 def name_selection_groups(ids): return 'sel_groups_' + '_'.join(map(str, ids))
617
618 def is_boolean_group(name): return name.startswith('in_group_')
619 def is_boolean_groups(name): return name.startswith('in_groups_')
620 def is_selection_groups(name): return name.startswith('sel_groups_')
621 def is_reified_group(name):
622     return is_boolean_group(name) or is_boolean_groups(name) or is_selection_groups(name)
623
624 def get_boolean_group(name): return int(name[9:])
625 def get_boolean_groups(name): return map(int, name[10:].split('_'))
626 def get_selection_groups(name): return map(int, name[11:].split('_'))
627
628 def partition(f, xs):
629     "return a pair equivalent to (filter(f, xs), filter(lambda x: not f(x), xs))"
630     yes, nos = [], []
631     for x in xs:
632         (yes if f(x) else nos).append(x)
633     return yes, nos
634
635
636
637 class groups_view(osv.osv):
638     _inherit = 'res.groups'
639
640     def create(self, cr, uid, values, context=None):
641         res = super(groups_view, self).create(cr, uid, values, context)
642         self.update_user_groups_view(cr, uid, context)
643         return res
644
645     def write(self, cr, uid, ids, values, context=None):
646         res = super(groups_view, self).write(cr, uid, ids, values, context)
647         self.update_user_groups_view(cr, uid, context)
648         return res
649
650     def unlink(self, cr, uid, ids, context=None):
651         res = super(groups_view, self).unlink(cr, uid, ids, context)
652         self.update_user_groups_view(cr, uid, context)
653         return res
654
655     def update_user_groups_view(self, cr, uid, context=None):
656         # the view with id 'base.user_groups_view' inherits the user form view,
657         # and introduces the reified group fields
658         view = self.get_user_groups_view(cr, uid, context)
659         if view:
660             xml1, xml2 = [], []
661             xml1.append(E.separator(string=_('Application'), colspan="4"))
662             for app, kind, gs in self.get_groups_by_application(cr, uid, context):
663                 # hide groups in category 'Hidden' (except to group_no_one)
664                 attrs = {'groups': 'base.group_no_one'} if app and app.xml_id == 'base.module_category_hidden' else {}
665                 if kind == 'selection':
666                     # application name with a selection field
667                     field_name = name_selection_groups(map(int, gs))
668                     xml1.append(E.field(name=field_name, **attrs))
669                     xml1.append(E.newline())
670                 else:
671                     # application separator with boolean fields
672                     app_name = app and app.name or _('Other')
673                     xml2.append(E.separator(string=app_name, colspan="4", **attrs))
674                     for g in gs:
675                         field_name = name_boolean_group(g.id)
676                         xml2.append(E.field(name=field_name, **attrs))
677
678             xml = E.field(*(xml1 + xml2), name="groups_id", position="replace")
679             xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS"))
680             xml_content = etree.tostring(xml, pretty_print=True, xml_declaration=True, encoding="utf-8")
681             view.write({'arch': xml_content})
682         return True
683
684     def get_user_groups_view(self, cr, uid, context=None):
685         try:
686             view = self.pool.get('ir.model.data').get_object(cr, SUPERUSER_ID, 'base', 'user_groups_view', context)
687             assert view and view._table_name == 'ir.ui.view'
688         except Exception:
689             view = False
690         return view
691
692     def get_application_groups(self, cr, uid, domain=None, context=None):
693         return self.search(cr, uid, domain or [])
694
695     def get_groups_by_application(self, cr, uid, context=None):
696         """ return all groups classified by application (module category), as a list of pairs:
697                 [(app, kind, [group, ...]), ...],
698             where app and group are browse records, and kind is either 'boolean' or 'selection'.
699             Applications are given in sequence order.  If kind is 'selection', the groups are
700             given in reverse implication order.
701         """
702         def linearized(gs):
703             gs = set(gs)
704             # determine sequence order: a group should appear after its implied groups
705             order = dict.fromkeys(gs, 0)
706             for g in gs:
707                 for h in gs.intersection(g.trans_implied_ids):
708                     order[h] -= 1
709             # check whether order is total, i.e., sequence orders are distinct
710             if len(set(order.itervalues())) == len(gs):
711                 return sorted(gs, key=lambda g: order[g])
712             return None
713
714         # classify all groups by application
715         gids = self.get_application_groups(cr, uid, context=context)
716         by_app, others = {}, []
717         for g in self.browse(cr, uid, gids, context):
718             if g.category_id:
719                 by_app.setdefault(g.category_id, []).append(g)
720             else:
721                 others.append(g)
722         # build the result
723         res = []
724         apps = sorted(by_app.iterkeys(), key=lambda a: a.sequence or 0)
725         for app in apps:
726             gs = linearized(by_app[app])
727             if gs:
728                 res.append((app, 'selection', gs))
729             else:
730                 res.append((app, 'boolean', by_app[app]))
731         if others:
732             res.append((False, 'boolean', others))
733         return res
734
735 class users_view(osv.osv):
736     _inherit = 'res.users'
737
738     def create(self, cr, uid, values, context=None):
739         self._set_reified_groups(values)
740         return super(users_view, self).create(cr, uid, values, context)
741
742     def write(self, cr, uid, ids, values, context=None):
743         self._set_reified_groups(values)
744         return super(users_view, self).write(cr, uid, ids, values, context)
745
746     def _set_reified_groups(self, values):
747         """ reflect reified group fields in values['groups_id'] """
748         if 'groups_id' in values:
749             # groups are already given, ignore group fields
750             for f in filter(is_reified_group, values.iterkeys()):
751                 del values[f]
752             return
753
754         add, remove = [], []
755         for f in values.keys():
756             if is_boolean_group(f):
757                 target = add if values.pop(f) else remove
758                 target.append(get_boolean_group(f))
759             elif is_boolean_groups(f):
760                 if not values.pop(f):
761                     remove.extend(get_boolean_groups(f))
762             elif is_selection_groups(f):
763                 remove.extend(get_selection_groups(f))
764                 selected = values.pop(f)
765                 if selected:
766                     add.append(selected)
767         # update values *only* if groups are being modified, otherwise
768         # we introduce spurious changes that might break the super.write() call.
769         if add or remove:
770             # remove groups in 'remove' and add groups in 'add'
771             values['groups_id'] = [(3, id) for id in remove] + [(4, id) for id in add]
772
773     def default_get(self, cr, uid, fields, context=None):
774         group_fields, fields = partition(is_reified_group, fields)
775         fields1 = (fields + ['groups_id']) if group_fields else fields
776         values = super(users_view, self).default_get(cr, uid, fields1, context)
777         self._get_reified_groups(group_fields, values)
778         return values
779
780     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
781         if not fields:
782             fields = self.fields_get(cr, uid, context=context).keys()
783         group_fields, fields = partition(is_reified_group, fields)
784         if not 'groups_id' in fields:
785             fields.append('groups_id')
786         res = super(users_view, self).read(cr, uid, ids, fields, context=context, load=load)
787         for values in (res if isinstance(res, list) else [res]):
788             self._get_reified_groups(group_fields, values)
789         return res
790
791     def _get_reified_groups(self, fields, values):
792         """ compute the given reified group fields from values['groups_id'] """
793         gids = set(values.get('groups_id') or [])
794         for f in fields:
795             if is_boolean_group(f):
796                 values[f] = get_boolean_group(f) in gids
797             elif is_boolean_groups(f):
798                 values[f] = not gids.isdisjoint(get_boolean_groups(f))
799             elif is_selection_groups(f):
800                 selected = [gid for gid in get_selection_groups(f) if gid in gids]
801                 values[f] = selected and selected[-1] or False
802
803     def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
804         res = super(users_view, self).fields_get(cr, uid, allfields, context, write_access)
805         # add reified groups fields
806         for app, kind, gs in self.pool.get('res.groups').get_groups_by_application(cr, uid, context):
807             if kind == 'selection':
808                 # selection group field
809                 tips = ['%s: %s' % (g.name, g.comment or '') for g in gs]
810                 res[name_selection_groups(map(int, gs))] = {
811                     'type': 'selection',
812                     'string': app and app.name or _('Other'),
813                     'selection': [(False, '')] + [(g.id, g.name) for g in gs],
814                     'help': '\n'.join(tips),
815                 }
816             else:
817                 # boolean group fields
818                 for g in gs:
819                     res[name_boolean_group(g.id)] = {
820                         'type': 'boolean',
821                         'string': g.name,
822                         'help': g.comment,
823                     }
824         return res
825
826 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: