c9eb1da381cdd7dd2b1662064094f09273259d5e
[odoo/odoo.git] / openerp / addons / base / res / res_user.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-2011 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 from osv import fields,osv
24 from osv.orm import browse_record
25 import tools
26 from functools import partial
27 import pytz
28 import pooler
29 from tools.translate import _
30 from service import security
31 import netsvc
32
33 class groups(osv.osv):
34     _name = "res.groups"
35     _order = 'name'
36     _description = "Access Groups"
37     _columns = {
38         'name': fields.char('Group Name', size=64, required=True),
39         'model_access': fields.one2many('ir.model.access', 'group_id', 'Access Controls'),
40         'rule_groups': fields.many2many('ir.rule', 'rule_group_rel',
41             'group_id', 'rule_group_id', 'Rules', domain=[('global', '=', False)]),
42         'menu_access': fields.many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', 'Access Menu'),
43         'comment' : fields.text('Comment',size=250),
44     }
45     _sql_constraints = [
46         ('name_uniq', 'unique (name)', 'The name of the group must be unique !')
47     ]
48
49     def copy(self, cr, uid, id, default=None, context=None):
50         group_name = self.read(cr, uid, [id], ['name'])[0]['name']
51         default.update({'name': _('%s (copy)')%group_name})
52         return super(groups, self).copy(cr, uid, id, default, context)
53
54     def write(self, cr, uid, ids, vals, context=None):
55         if 'name' in vals:
56             if vals['name'].startswith('-'):
57                 raise osv.except_osv(_('Error'),
58                         _('The name of the group can not start with "-"'))
59         res = super(groups, self).write(cr, uid, ids, vals, context=context)
60         self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
61         return res
62
63     def create(self, cr, uid, vals, context=None):
64         if 'name' in vals:
65             if vals['name'].startswith('-'):
66                 raise osv.except_osv(_('Error'),
67                         _('The name of the group can not start with "-"'))
68         gid = super(groups, self).create(cr, uid, vals, context=context)
69         if context and context.get('noadmin', False):
70             pass
71         else:
72             # assign this new group to user_root
73             user_obj = self.pool.get('res.users')
74             aid = user_obj.browse(cr, 1, user_obj._get_admin_id(cr))
75             if aid:
76                 aid.write({'groups_id': [(4, gid)]})
77         return gid
78
79     def get_extended_interface_group(self, cr, uid, context=None):
80         data_obj = self.pool.get('ir.model.data')
81         extended_group_data_id = data_obj._get_id(cr, uid, 'base', 'group_extended')
82         return data_obj.browse(cr, uid, extended_group_data_id, context=context).res_id
83
84 groups()
85
86 def _lang_get(self, cr, uid, context=None):
87     obj = self.pool.get('res.lang')
88     ids = obj.search(cr, uid, [('translatable','=',True)])
89     res = obj.read(cr, uid, ids, ['code', 'name'], context=context)
90     res = [(r['code'], r['name']) for r in res]
91     return res
92
93 def _tz_get(self,cr,uid, context=None):
94     return [(x, x) for x in pytz.all_timezones]
95
96 class users(osv.osv):
97     __admin_ids = {}
98     _uid_cache = {}
99     _name = "res.users"
100     _order = 'name'
101
102     WELCOME_MAIL_SUBJECT = u"Welcome to OpenERP"
103     WELCOME_MAIL_BODY = u"An OpenERP account has been created for you, "\
104         "\"%(name)s\".\n\nYour login is %(login)s, "\
105         "you should ask your supervisor or system administrator if you "\
106         "haven't been given your password yet.\n\n"\
107         "If you aren't %(name)s, this email reached you errorneously, "\
108         "please delete it."
109
110     def get_welcome_mail_subject(self, cr, uid, context=None):
111         """ Returns the subject of the mail new users receive (when
112         created via the res.config.users wizard), default implementation
113         is to return config_users.WELCOME_MAIL_SUBJECT
114         """
115         return self.WELCOME_MAIL_SUBJECT
116     def get_welcome_mail_body(self, cr, uid, context=None):
117         """ Returns the subject of the mail new users receive (when
118         created via the res.config.users wizard), default implementation
119         is to return config_users.WELCOME_MAIL_BODY
120         """
121         return self.WELCOME_MAIL_BODY
122
123     def get_current_company(self, cr, uid):
124         cr.execute('select company_id, res_company.name from res_users left join res_company on res_company.id = company_id where res_users.id=%s' %uid)
125         return cr.fetchall()
126
127     def send_welcome_email(self, cr, uid, id, context=None):
128         logger= netsvc.Logger()
129         user = self.pool.get('res.users').read(cr, uid, id, context=context)
130         if not user.get('email'):
131             return False
132         if not tools.config.get('smtp_server'):
133             logger.notifyChannel('mails', netsvc.LOG_WARNING,
134                 _('"smtp_server" needs to be set to send mails to users'))
135             return False
136         if not tools.config.get('email_from'):
137             logger.notifyChannel("mails", netsvc.LOG_WARNING,
138                 _('"email_from" needs to be set to send welcome mails '
139                   'to users'))
140             return False
141
142         return tools.email_send(email_from=None, email_to=[user['email']],
143                                 subject=self.get_welcome_mail_subject(
144                                     cr, uid, context=context),
145                                 body=self.get_welcome_mail_body(
146                                     cr, uid, context=context) % user)
147
148     def _set_interface_type(self, cr, uid, ids, name, value, arg, context=None):
149         """Implementation of 'view' function field setter, sets the type of interface of the users.
150         @param name: Name of the field
151         @param arg: User defined argument
152         @param value: new value returned
153         @return:  True/False
154         """
155         if not value or value not in ['simple','extended']:
156             return False
157         group_obj = self.pool.get('res.groups')
158         extended_group_id = group_obj.get_extended_interface_group(cr, uid, context=context)
159         # First always remove the users from the group (avoids duplication if called twice)
160         self.write(cr, uid, ids, {'groups_id': [(3, extended_group_id)]}, context=context)
161         # Then add them back if requested
162         if value == 'extended':
163             self.write(cr, uid, ids, {'groups_id': [(4, extended_group_id)]}, context=context)
164         return True
165
166
167     def _get_interface_type(self, cr, uid, ids, name, args, context=None):
168         """Implementation of 'view' function field getter, returns the type of interface of the users.
169         @param field_name: Name of the field
170         @param arg: User defined argument
171         @return:  Dictionary of values
172         """
173         group_obj = self.pool.get('res.groups')
174         extended_group_id = group_obj.get_extended_interface_group(cr, uid, context=context)
175         extended_users = group_obj.read(cr, uid, extended_group_id, ['users'], context=context)['users']
176         return dict(zip(ids, ['extended' if user in extended_users else 'simple' for user in ids]))
177
178     def _email_get(self, cr, uid, ids, name, arg, context=None):
179         # perform this as superuser because the current user is allowed to read users, and that includes
180         # the email, even without any direct read access on the res_partner_address object.
181         return dict([(user.id, user.address_id.email) for user in self.browse(cr, 1, ids)]) # no context to avoid potential security issues as superuser
182
183     def _email_set(self, cr, uid, ids, name, value, arg, context=None):
184         if not isinstance(ids,list):
185             ids = [ids]
186         address_obj = self.pool.get('res.partner.address')
187         for user in self.browse(cr, uid, ids, context=context):
188             # perform this as superuser because the current user is allowed to write to the user, and that includes
189             # the email even without any direct write access on the res_partner_address object.
190             if user.address_id:
191                 address_obj.write(cr, 1, user.address_id.id, {'email': value or None}) # no context to avoid potential security issues as superuser
192             else:
193                 address_id = address_obj.create(cr, 1, {'name': user.name, 'email': value or None}) # no context to avoid potential security issues as superuser
194                 self.write(cr, uid, ids, {'address_id': address_id}, context)
195         return True
196
197     def _set_new_password(self, cr, uid, id, name, value, args, context=None):
198         if value is False:
199             # Do not update the password if no value is provided, ignore silently.
200             # For example web client submits False values for all empty fields.
201             return
202         if uid == id:
203             # To change their own password users must use the client-specific change password wizard,
204             # so that the new password is immediately used for further RPC requests, otherwise the user
205             # will face unexpected 'Access Denied' exceptions.
206             raise osv.except_osv(_('Operation Canceled'), _('Please use the change password wizard (in User Preferences or User menu) to change your own password.'))
207         self.write(cr, uid, id, {'password': value})
208
209     def _get_password(self, cr, uid, ids, arg, karg, context=None):
210         return dict.fromkeys(ids, '')
211
212     _columns = {
213         'name': fields.char('User Name', size=64, required=True, select=True,
214                             help="The new user's real name, used for searching"
215                                  " and most listings"),
216         'login': fields.char('Login', size=64, required=True,
217                              help="Used to log into the system"),
218         'password': fields.char('Password', size=64, invisible=True, help="Keep empty if you don't want the user to be able to connect on the system."),
219         'new_password': fields.function(_get_password, method=True, type='char', size=64,
220                                 fnct_inv=_set_new_password,
221                                 string='Change password', help="Only specify a value if you want to change the user password. "
222                                 "This user will have to logout and login again!"),
223         'email': fields.char('E-mail', size=64,
224             help='If an email is provided, the user will be sent a message '
225                  'welcoming him.\n\nWarning: if "email_from" and "smtp_server"'
226                  " aren't configured, it won't be possible to email new "
227                  "users."),
228         'signature': fields.text('Signature', size=64),
229         'address_id': fields.many2one('res.partner.address', 'Address'),
230         'active': fields.boolean('Active'),
231         '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."),
232         'menu_id': fields.many2one('ir.actions.actions', 'Menu Action', help="If specified, the action will replace the standard menu for this user."),
233         'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
234
235         # Special behavior for this field: res.company.search() will only return the companies
236         # available to the current user (should be the user's companies?), when the user_preference
237         # context is set.
238         'company_id': fields.many2one('res.company', 'Company', required=True,
239             help="The company this user is currently working for.", context={'user_preference': True}),
240
241         'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
242         'context_lang': fields.selection(_lang_get, 'Language', required=True,
243             help="Sets the language for the user's user interface, when UI "
244                  "translations are available"),
245         'context_tz': fields.selection(_tz_get,  'Timezone', size=64,
246             help="The user's timezone, used to perform timezone conversions "
247                  "between the server and the client."),
248         'view': fields.function(_get_interface_type, method=True, type='selection', fnct_inv=_set_interface_type,
249                                 selection=[('simple','Simplified'),('extended','Extended')],
250                                 string='Interface', help="Choose between the simplified interface and the extended one"),
251         'user_email': fields.function(_email_get, method=True, fnct_inv=_email_set, string='Email', type="char", size=240),
252         'menu_tips': fields.boolean('Menu Tips', help="Check out this box if you want to always display tips on each menu action"),
253         'date': fields.datetime('Last Connection', readonly=True),
254     }
255
256     def on_change_company_id(self, cr, uid, ids, company_id):
257         return {
258                 'warning' : {
259                     'title': _("Company Switch Warning"),
260                     '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)"),
261                 }
262         }
263
264     def read(self,cr, uid, ids, fields=None, context=None, load='_classic_read'):
265         def override_password(o):
266             if 'password' in o and ( 'id' not in o or o['id'] != uid ):
267                 o['password'] = '********'
268             return o
269         result = super(users, self).read(cr, uid, ids, fields, context, load)
270         canwrite = self.pool.get('ir.model.access').check(cr, uid, 'res.users', 'write', False)
271         if not canwrite:
272             if isinstance(ids, (int, float)):
273                 result = override_password(result)
274             else:
275                 result = map(override_password, result)
276         return result
277
278
279     def _check_company(self, cr, uid, ids, context=None):
280         return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context))
281
282     _constraints = [
283         (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
284     ]
285
286     _sql_constraints = [
287         ('login_key', 'UNIQUE (login)',  'You can not have two users with the same login !')
288     ]
289
290     def _get_email_from(self, cr, uid, ids, context=None):
291         if not isinstance(ids, list):
292             ids = [ids]
293         res = dict.fromkeys(ids, False)
294         for user in self.browse(cr, uid, ids, context=context):
295             if user.user_email:
296                 res[user.id] = "%s <%s>" % (user.name, user.user_email)
297         return res
298
299     def _get_admin_id(self, cr):
300         if self.__admin_ids.get(cr.dbname) is None:
301             ir_model_data_obj = self.pool.get('ir.model.data')
302             mdid = ir_model_data_obj._get_id(cr, 1, 'base', 'user_root')
303             self.__admin_ids[cr.dbname] = ir_model_data_obj.read(cr, 1, [mdid], ['res_id'])[0]['res_id']
304         return self.__admin_ids[cr.dbname]
305
306     def _get_company(self,cr, uid, context=None, uid2=False):
307         if not uid2:
308             uid2 = uid
309         user = self.pool.get('res.users').read(cr, uid, uid2, ['company_id'], context)
310         company_id = user.get('company_id', False)
311         return company_id and company_id[0] or False
312
313     def _get_companies(self, cr, uid, context=None):
314         c = self._get_company(cr, uid, context)
315         if c:
316             return [c]
317         return False
318
319     def _get_menu(self,cr, uid, context=None):
320         dataobj = self.pool.get('ir.model.data')
321         try:
322             model, res_id = dataobj.get_object_reference(cr, uid, 'base', 'action_menu_admin')
323             if model != 'ir.actions.act_window':
324                 return False
325             return res_id
326         except ValueError:
327             return False
328
329     def _get_group(self,cr, uid, context=None):
330         dataobj = self.pool.get('ir.model.data')
331         result = []
332         try:
333             dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_user')
334             result.append(group_id)
335             dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_partner_manager')
336             result.append(group_id)
337         except ValueError:
338             # If these groups does not exists anymore
339             pass
340         return result
341
342     _defaults = {
343         'password' : '',
344         'context_lang': 'en_US',
345         'active' : True,
346         'menu_id': _get_menu,
347         'company_id': _get_company,
348         'company_ids': _get_companies,
349         'groups_id': _get_group,
350         'address_id': False,
351         'menu_tips':True
352     }
353
354     @tools.ormcache()
355     def company_get(self, cr, uid, uid2, context=None):
356         return self._get_company(cr, uid, context=context, uid2=uid2)
357
358     # User can write to a few of her own fields (but not her groups for example)
359     SELF_WRITEABLE_FIELDS = ['menu_tips','view', 'password', 'signature', 'action_id', 'company_id', 'user_email']
360
361     def write(self, cr, uid, ids, values, context=None):
362         if not hasattr(ids, '__iter__'):
363             ids = [ids]
364         if ids == [uid]:
365             for key in values.keys():
366                 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
367                     break
368             else:
369                 if 'company_id' in values:
370                     if not (values['company_id'] in self.read(cr, 1, uid, ['company_ids'], context=context)['company_ids']):
371                         del values['company_id']
372                 uid = 1 # safe fields only, so we write as super-user to bypass access rights
373
374         res = super(users, self).write(cr, uid, ids, values, context=context)
375
376         # clear caches linked to the users
377         self.company_get.clear_cache(self)
378         self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
379         clear = partial(self.pool.get('ir.rule').clear_cache, cr)
380         map(clear, ids)
381         db = cr.dbname
382         if db in self._uid_cache:
383             for id in ids:
384                 if id in self._uid_cache[db]:
385                     del self._uid_cache[db][id]
386
387         return res
388
389     def unlink(self, cr, uid, ids, context=None):
390         if 1 in ids:
391             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, ...)'))
392         db = cr.dbname
393         if db in self._uid_cache:
394             for id in ids:
395                 if id in self._uid_cache[db]:
396                     del self._uid_cache[db][id]
397         return super(users, self).unlink(cr, uid, ids, context=context)
398
399     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
400         if not args:
401             args=[]
402         if not context:
403             context={}
404         ids = []
405         if name:
406             ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit)
407         if not ids:
408             ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit)
409         return self.name_get(cr, user, ids)
410
411     def copy(self, cr, uid, id, default=None, context=None):
412         user2copy = self.read(cr, uid, [id], ['login','name'])[0]
413         if default is None:
414             default = {}
415         copy_pattern = _("%s (copy)")
416         copydef = dict(login=(copy_pattern % user2copy['login']),
417                        name=(copy_pattern % user2copy['name']),
418                        address_id=False, # avoid sharing the address of the copied user!
419                        )
420         copydef.update(default)
421         return super(users, self).copy(cr, uid, id, copydef, context)
422
423     def context_get(self, cr, uid, context=None):
424         user = self.browse(cr, uid, uid, context)
425         result = {}
426         for k in self._columns.keys():
427             if k.startswith('context_'):
428                 res = getattr(user,k) or False
429                 if isinstance(res, browse_record):
430                     res = res.id
431                 result[k[8:]] = res or False
432         return result
433
434     def action_get(self, cr, uid, context=None):
435         dataobj = self.pool.get('ir.model.data')
436         data_id = dataobj._get_id(cr, 1, 'base', 'action_res_users_my')
437         return dataobj.browse(cr, uid, data_id, context=context).res_id
438
439
440     def login(self, db, login, password):
441         if not password:
442             return False
443         cr = pooler.get_db(db).cursor()
444         try:
445             cr.execute('UPDATE res_users SET date=now() WHERE login=%s AND password=%s AND active RETURNING id',
446                     (tools.ustr(login), tools.ustr(password)))
447             res = cr.fetchone()
448             cr.commit()
449             if res:
450                 return res[0]
451             else:
452                 return False
453         finally:
454             cr.close()
455
456     def check_super(self, passwd):
457         if passwd == tools.config['admin_passwd']:
458             return True
459         else:
460             raise security.ExceptionNoTb('AccessDenied')
461
462     def check(self, db, uid, passwd):
463         """Verifies that the given (uid, password) pair is authorized for the database ``db`` and
464            raise an exception if it is not."""
465         if not passwd:
466             # empty passwords disallowed for obvious security reasons
467             raise security.ExceptionNoTb('AccessDenied')
468         if self._uid_cache.get(db, {}).get(uid) == passwd:
469             return
470         cr = pooler.get_db(db).cursor()
471         try:
472             cr.execute('SELECT COUNT(1) FROM res_users WHERE id=%s AND password=%s AND active=%s',
473                         (int(uid), passwd, True))
474             res = cr.fetchone()[0]
475             if not res:
476                 raise security.ExceptionNoTb('AccessDenied')
477             if self._uid_cache.has_key(db):
478                 ulist = self._uid_cache[db]
479                 ulist[uid] = passwd
480             else:
481                 self._uid_cache[db] = {uid:passwd}
482         finally:
483             cr.close()
484
485     def access(self, db, uid, passwd, sec_level, ids):
486         if not passwd:
487             return False
488         cr = pooler.get_db(db).cursor()
489         try:
490             cr.execute('SELECT id FROM res_users WHERE id=%s AND password=%s', (uid, passwd))
491             res = cr.fetchone()
492             if not res:
493                 raise security.ExceptionNoTb('Bad username or password')
494             return res[0]
495         finally:
496             cr.close()
497
498     def change_password(self, cr, uid, old_passwd, new_passwd, context=None):
499         """Change current user password. Old password must be provided explicitly
500         to prevent hijacking an existing user session, or for cases where the cleartext
501         password is not used to authenticate requests.
502
503         :return: True
504         :raise: security.ExceptionNoTb when old password is wrong
505         :raise: except_osv when new password is not set or empty
506         """
507         self.check(cr.dbname, uid, old_passwd)
508         if new_passwd:
509             return self.write(cr, uid, uid, {'password': new_passwd})
510         raise osv.except_osv(_('Warning!'), _("Setting empty passwords is not allowed for security reasons!"))
511
512 users()
513
514 class config_users(osv.osv_memory):
515     _name = 'res.config.users'
516     _inherit = ['res.users', 'res.config']
517
518     def _generate_signature(self, cr, name, email, context=None):
519         return _('--\n%(name)s %(email)s\n') % {
520             'name': name or '',
521             'email': email and ' <'+email+'>' or '',
522             }
523
524     def create_user(self, cr, uid, new_id, context=None):
525         """ create a new res.user instance from the data stored
526         in the current res.config.users.
527
528         If an email address was filled in for the user, sends a mail
529         composed of the return values of ``get_welcome_mail_subject``
530         and ``get_welcome_mail_body`` (which should be unicode values),
531         with the user's data %-formatted into the mail body
532         """
533         base_data = self.read(cr, uid, new_id, context=context)
534         partner_id = self.pool.get('res.partner').main_partner(cr, uid)
535         address = self.pool.get('res.partner.address').create(
536             cr, uid, {'name': base_data['name'],
537                       'email': base_data['email'],
538                       'partner_id': partner_id,},
539             context)
540         # Change the read many2one values from (id,name) to id, and
541         # the one2many from ids to (6,0,ids).
542         base_data.update({'menu_id' : base_data.get('menu_id') and base_data['menu_id'][0],
543                           'company_id' : base_data.get('company_id') and base_data['company_id'][0],
544                           'action_id' :  base_data.get('action_id') and base_data['action_id'][0],
545                           'signature' : self._generate_signature(cr, base_data['name'], base_data['email'], context=context),
546                           'address_id' : address,
547                           'groups_id' : [(6,0, base_data.get('groups_id',[]))],
548                 })
549         new_user = self.pool.get('res.users').create(
550             cr, uid, base_data, context)
551         self.send_welcome_email(cr, uid, new_user, context=context)
552
553     def execute(self, cr, uid, ids, context=None):
554         'Do nothing on execution, just launch the next action/todo'
555         pass
556     def action_add(self, cr, uid, ids, context=None):
557         'Create a user, and re-display the view'
558         self.create_user(cr, uid, ids[0], context=context)
559         return {
560             'view_type': 'form',
561             "view_mode": 'form',
562             'res_model': 'res.config.users',
563             'view_id':self.pool.get('ir.ui.view')\
564                 .search(cr,uid,[('name','=','res.config.users.confirm.form')]),
565             'type': 'ir.actions.act_window',
566             'target':'new',
567             }
568 config_users()
569
570 class groups2(osv.osv): ##FIXME: Is there a reason to inherit this object ?
571     _inherit = 'res.groups'
572     _columns = {
573         'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
574     }
575
576     def unlink(self, cr, uid, ids, context=None):
577         group_users = []
578         for record in self.read(cr, uid, ids, ['users'], context=context):
579             if record['users']:
580                 group_users.extend(record['users'])
581
582         if group_users:
583             user_names = [user.name for user in self.pool.get('res.users').browse(cr, uid, group_users, context=context)]
584             if len(user_names) >= 5:
585                 user_names = user_names[:5]
586                 user_names += '...'
587             raise osv.except_osv(_('Warning !'),
588                         _('Group(s) cannot be deleted, because some user(s) still belong to them: %s !') % \
589                             ', '.join(user_names))
590         return super(groups2, self).unlink(cr, uid, ids, context=context)
591
592 groups2()
593
594 class res_config_view(osv.osv_memory):
595     _name = 'res.config.view'
596     _inherit = 'res.config'
597     _columns = {
598         'name':fields.char('Name', size=64),
599         'view': fields.selection([('simple','Simplified'),
600                                   ('extended','Extended')],
601                                  'Interface', required=True ),
602     }
603     _defaults={
604         'view':lambda self,cr,uid,*args: self.pool.get('res.users').browse(cr, uid, uid).view or 'simple',
605     }
606
607     def execute(self, cr, uid, ids, context=None):
608         res = self.read(cr, uid, ids)[0]
609         self.pool.get('res.users').write(cr, uid, [uid],
610                                  {'view':res['view']}, context=context)
611
612 res_config_view()
613
614 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: