1 # -*- coding: utf-8 -*-
2 ##############################################################################
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>).
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.
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.
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/>.
21 ##############################################################################
23 from osv import fields,osv
24 from osv.orm import browse_record
26 from functools import partial
29 from tools.translate import _
30 from service import security
33 class groups(osv.osv):
36 _description = "Access Groups"
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'),
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),
46 ('name_uniq', 'unique (name)', 'The name of the group must be unique !')
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)
54 def write(self, cr, uid, ids, vals, context=None):
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)
63 def create(self, cr, uid, vals, context=None):
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):
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))
76 aid.write({'groups_id': [(4, gid)]})
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
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]
93 def _tz_get(self,cr,uid, context=None):
94 return [(x, x) for x in pytz.all_timezones]
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, "\
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
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
121 return self.WELCOME_MAIL_BODY
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)
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 tools.config.get('smtp_server'):
131 logger.notifyChannel('mails', netsvc.LOG_WARNING,
132 _('"smtp_server" needs to be set to send mails to users'))
134 if not tools.config.get('email_from'):
135 logger.notifyChannel("mails", netsvc.LOG_WARNING,
136 _('"email_from" needs to be set to send welcome mails '
139 if not user.get('email'):
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)
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
155 if not value or value not in ['simple','extended']:
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)
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
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]))
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
183 def _email_set(self, cr, uid, ids, name, value, arg, context=None):
184 if not isinstance(ids,list):
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.
191 address_obj.write(cr, 1, user.address_id.id, {'email': value or None}) # no context to avoid potential security issues as superuser
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)
197 def _set_new_password(self, cr, uid, id, name, value, args, context=None):
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.
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})
210 'name': fields.char('User Name', size=64, required=True, select=True,
211 help="The new user's real name, used for searching"
212 " and most listings"),
213 'login': fields.char('Login', size=64, required=True,
214 help="Used to log into the system"),
215 '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."),
216 'new_password': fields.function(lambda *a:'', method=True, type='char', size=64,
217 fnct_inv=_set_new_password,
218 string='Change password', help="Only specify a value if you want to change the user password. "
219 "This user will have to logout and login again!"),
220 'email': fields.char('E-mail', size=64,
221 help='If an email is provided, the user will be sent a message '
222 'welcoming him.\n\nWarning: if "email_from" and "smtp_server"'
223 " aren't configured, it won't be possible to email new "
225 'signature': fields.text('Signature', size=64),
226 'address_id': fields.many2one('res.partner.address', 'Address'),
227 'active': fields.boolean('Active'),
228 '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."),
229 'menu_id': fields.many2one('ir.actions.actions', 'Menu Action', help="If specified, the action will replace the standard menu for this user."),
230 'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
232 # Special behavior for this field: res.company.search() will only return the companies
233 # available to the current user (should be the user's companies?), when the user_preference
235 'company_id': fields.many2one('res.company', 'Company', required=True,
236 help="The company this user is currently working for.", context={'user_preference': True}),
238 'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
239 'context_lang': fields.selection(_lang_get, 'Language', required=True,
240 help="Sets the language for the user's user interface, when UI "
241 "translations are available"),
242 'context_tz': fields.selection(_tz_get, 'Timezone', size=64,
243 help="The user's timezone, used to perform timezone conversions "
244 "between the server and the client."),
245 'view': fields.function(_get_interface_type, method=True, type='selection', fnct_inv=_set_interface_type,
246 selection=[('simple','Simplified'),('extended','Extended')],
247 string='Interface', help="Choose between the simplified interface and the extended one"),
248 'user_email': fields.function(_email_get, method=True, fnct_inv=_email_set, string='Email', type="char", size=240),
249 'menu_tips': fields.boolean('Menu Tips', help="Check out this box if you want to always display tips on each menu action"),
250 'date': fields.datetime('Last Connection', readonly=True),
253 def on_change_company_id(self, cr, uid, ids, company_id):
256 'title': _("Company Switch Warning"),
257 '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 def read(self,cr, uid, ids, fields=None, context=None, load='_classic_read'):
262 def override_password(o):
263 if 'password' in o and ( 'id' not in o or o['id'] != uid ):
264 o['password'] = '********'
267 result = super(users, self).read(cr, uid, ids, fields, context, load)
268 canwrite = self.pool.get('ir.model.access').check(cr, uid, 'res.users', 'write', raise_exception=False)
270 if isinstance(ids, (int, float)):
271 result = override_password(result)
273 result = map(override_password, result)
277 def _check_company(self, cr, uid, ids, context=None):
278 return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context))
281 (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
285 ('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !')
288 def _get_email_from(self, cr, uid, ids, context=None):
289 if not isinstance(ids, list):
291 res = dict.fromkeys(ids, False)
292 for user in self.browse(cr, uid, ids, context=context):
294 res[user.id] = "%s <%s>" % (user.name, user.user_email)
297 def _get_admin_id(self, cr):
298 if self.__admin_ids.get(cr.dbname) is None:
299 ir_model_data_obj = self.pool.get('ir.model.data')
300 mdid = ir_model_data_obj._get_id(cr, 1, 'base', 'user_root')
301 self.__admin_ids[cr.dbname] = ir_model_data_obj.read(cr, 1, [mdid], ['res_id'])[0]['res_id']
302 return self.__admin_ids[cr.dbname]
304 def _get_company(self,cr, uid, context=None, uid2=False):
307 user = self.pool.get('res.users').read(cr, uid, uid2, ['company_id'], context)
308 company_id = user.get('company_id', False)
309 return company_id and company_id[0] or False
311 def _get_companies(self, cr, uid, context=None):
312 c = self._get_company(cr, uid, context)
317 def _get_menu(self,cr, uid, context=None):
318 dataobj = self.pool.get('ir.model.data')
320 model, res_id = dataobj.get_object_reference(cr, uid, 'base', 'action_menu_admin')
321 if model != 'ir.actions.act_window':
327 def _get_group(self,cr, uid, context=None):
328 dataobj = self.pool.get('ir.model.data')
331 dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_user')
332 result.append(group_id)
333 dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_partner_manager')
334 result.append(group_id)
336 # If these groups does not exists anymore
342 'context_lang': 'en_US',
344 'menu_id': _get_menu,
345 'company_id': _get_company,
346 'company_ids': _get_companies,
347 'groups_id': _get_group,
353 def company_get(self, cr, uid, uid2, context=None):
354 return self._get_company(cr, uid, context=context, uid2=uid2)
356 # User can write to a few of her own fields (but not her groups for example)
357 SELF_WRITEABLE_FIELDS = ['menu_tips','view', 'password', 'signature', 'action_id', 'company_id', 'user_email']
359 def write(self, cr, uid, ids, values, context=None):
360 if not hasattr(ids, '__iter__'):
363 for key in values.keys():
364 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
367 if 'company_id' in values:
368 if not (values['company_id'] in self.read(cr, 1, uid, ['company_ids'], context=context)['company_ids']):
369 del values['company_id']
370 uid = 1 # safe fields only, so we write as super-user to bypass access rights
372 res = super(users, self).write(cr, uid, ids, values, context=context)
374 # clear caches linked to the users
375 self.company_get.clear_cache(cr.dbname)
376 self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
377 clear = partial(self.pool.get('ir.rule').clear_cache, cr)
380 if db in self._uid_cache:
382 if id in self._uid_cache[db]:
383 del self._uid_cache[db][id]
387 def unlink(self, cr, uid, ids, context=None):
389 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, ...)'))
391 if db in self._uid_cache:
393 if id in self._uid_cache[db]:
394 del self._uid_cache[db][id]
395 return super(users, self).unlink(cr, uid, ids, context=context)
397 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
404 ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit)
406 ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit)
407 return self.name_get(cr, user, ids)
409 def copy(self, cr, uid, id, default=None, context=None):
410 user2copy = self.read(cr, uid, [id], ['login','name'])[0]
413 copy_pattern = _("%s (copy)")
414 copydef = dict(login=(copy_pattern % user2copy['login']),
415 name=(copy_pattern % user2copy['name']),
416 address_id=False, # avoid sharing the address of the copied user!
418 copydef.update(default)
419 return super(users, self).copy(cr, uid, id, copydef, context)
421 def context_get(self, cr, uid, context=None):
422 user = self.browse(cr, uid, uid, context)
424 for k in self._columns.keys():
425 if k.startswith('context_'):
426 res = getattr(user,k) or False
427 if isinstance(res, browse_record):
429 result[k[8:]] = res or False
432 def action_get(self, cr, uid, context=None):
433 dataobj = self.pool.get('ir.model.data')
434 data_id = dataobj._get_id(cr, 1, 'base', 'action_res_users_my')
435 return dataobj.browse(cr, uid, data_id, context=context).res_id
438 def login(self, db, login, password):
441 cr = pooler.get_db(db).cursor()
443 # autocommit: our single request will be performed atomically.
444 # (In this way, there is no opportunity to have two transactions
445 # interleaving their cr.execute()..cr.commit() calls and have one
446 # of them rolled back due to a concurrent access.)
447 # We effectively unconditionally write the res_users line.
449 # Even w/ autocommit there's a chance the user row will be locked,
450 # in which case we can't delay the login just for the purpose of
451 # update the last login date - hence we use FOR UPDATE NOWAIT to
452 # try to get the lock - fail-fast
453 cr.execute("""SELECT id from res_users
454 WHERE login=%s AND password=%s
455 AND active FOR UPDATE NOWAIT""",
456 (tools.ustr(login), tools.ustr(password)), log_exceptions=False)
457 cr.execute('UPDATE res_users SET date=now() WHERE login=%s AND password=%s AND active RETURNING id',
458 (tools.ustr(login), tools.ustr(password)))
460 # Failing to acquire the lock on the res_users row probably means
461 # another request is holding it - no big deal, we skip the update
462 # for this time, and let the user login anyway.
464 cr.execute("""SELECT id from res_users
465 WHERE login=%s AND password=%s
467 (tools.ustr(login), tools.ustr(password)))
475 def check_super(self, passwd):
476 if passwd == tools.config['admin_passwd']:
479 raise security.ExceptionNoTb('AccessDenied')
481 def check(self, db, uid, passwd):
482 """Verifies that the given (uid, password) pair is authorized for the database ``db`` and
483 raise an exception if it is not."""
485 # empty passwords disallowed for obvious security reasons
486 raise security.ExceptionNoTb('AccessDenied')
487 if self._uid_cache.get(db, {}).get(uid) == passwd:
489 cr = pooler.get_db(db).cursor()
491 cr.execute('SELECT COUNT(1) FROM res_users WHERE id=%s AND password=%s AND active=%s',
492 (int(uid), passwd, True))
493 res = cr.fetchone()[0]
495 raise security.ExceptionNoTb('AccessDenied')
496 if self._uid_cache.has_key(db):
497 ulist = self._uid_cache[db]
500 self._uid_cache[db] = {uid:passwd}
504 def access(self, db, uid, passwd, sec_level, ids):
507 cr = pooler.get_db(db).cursor()
509 cr.execute('SELECT id FROM res_users WHERE id=%s AND password=%s', (uid, passwd))
512 raise security.ExceptionNoTb('Bad username or password')
517 def change_password(self, cr, uid, old_passwd, new_passwd, context=None):
518 """Change current user password. Old password must be provided explicitly
519 to prevent hijacking an existing user session, or for cases where the cleartext
520 password is not used to authenticate requests.
523 :raise: security.ExceptionNoTb when old password is wrong
524 :raise: except_osv when new password is not set or empty
526 self.check(cr.dbname, uid, old_passwd)
528 return self.write(cr, uid, uid, {'password': new_passwd})
529 raise osv.except_osv(_('Warning!'), _("Setting empty passwords is not allowed for security reasons!"))
533 class config_users(osv.osv_memory):
534 _name = 'res.config.users'
535 _inherit = ['res.users', 'res.config']
537 def _generate_signature(self, cr, name, email, context=None):
538 return _('--\n%(name)s %(email)s\n') % {
540 'email': email and ' <'+email+'>' or '',
543 def create_user(self, cr, uid, new_id, context=None):
544 """ create a new res.user instance from the data stored
545 in the current res.config.users.
547 If an email address was filled in for the user, sends a mail
548 composed of the return values of ``get_welcome_mail_subject``
549 and ``get_welcome_mail_body`` (which should be unicode values),
550 with the user's data %-formatted into the mail body
552 base_data = self.read(cr, uid, new_id, context=context)
553 partner_id = self.pool.get('res.partner').main_partner(cr, uid)
554 address = self.pool.get('res.partner.address').create(
555 cr, uid, {'name': base_data['name'],
556 'email': base_data['email'],
557 'partner_id': partner_id,},
561 signature=self._generate_signature(
562 cr, base_data['name'], base_data['email'], context=context),
565 new_user = self.pool.get('res.users').create(
566 cr, uid, user_data, context)
567 self.send_welcome_email(cr, uid, new_user, context=context)
568 def execute(self, cr, uid, ids, context=None):
569 'Do nothing on execution, just launch the next action/todo'
571 def action_add(self, cr, uid, ids, context=None):
572 'Create a user, and re-display the view'
573 self.create_user(cr, uid, ids[0], context=context)
577 'res_model': 'res.config.users',
578 'view_id':self.pool.get('ir.ui.view')\
579 .search(cr,uid,[('name','=','res.config.users.confirm.form')]),
580 'type': 'ir.actions.act_window',
585 class groups2(osv.osv): ##FIXME: Is there a reason to inherit this object ?
586 _inherit = 'res.groups'
588 'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
591 def unlink(self, cr, uid, ids, context=None):
593 for record in self.read(cr, uid, ids, ['users'], context=context):
595 group_users.extend(record['users'])
598 user_names = [user.name for user in self.pool.get('res.users').browse(cr, uid, group_users, context=context)]
599 if len(user_names) >= 5:
600 user_names = user_names[:5]
602 raise osv.except_osv(_('Warning !'),
603 _('Group(s) cannot be deleted, because some user(s) still belong to them: %s !') % \
604 ', '.join(user_names))
605 return super(groups2, self).unlink(cr, uid, ids, context=context)
609 class res_config_view(osv.osv_memory):
610 _name = 'res.config.view'
611 _inherit = 'res.config'
613 'name':fields.char('Name', size=64),
614 'view': fields.selection([('simple','Simplified'),
615 ('extended','Extended')],
616 'Interface', required=True ),
619 'view':lambda self,cr,uid,*args: self.pool.get('res.users').browse(cr, uid, uid).view or 'simple',
622 def execute(self, cr, uid, ids, context=None):
623 res = self.read(cr, uid, ids)[0]
624 self.pool.get('res.users').write(cr, uid, [uid],
625 {'view':res['view']}, context=context)
629 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: