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', 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),
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 user.get('email'):
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'))
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 '
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})
209 def _get_password(self, cr, uid, ids, arg, karg, context=None):
210 return dict.fromkeys(ids, '')
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 "
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'),
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
238 'company_id': fields.many2one('res.company', 'Company', required=True,
239 help="The company this user is currently working for.", context={'user_preference': True}),
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),
256 def on_change_company_id(self, cr, uid, ids, company_id):
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)"),
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'] = '********'
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)
272 if isinstance(ids, (int, float)):
273 result = override_password(result)
275 result = map(override_password, result)
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))
283 (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
287 ('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !')
290 def _get_email_from(self, cr, uid, ids, context=None):
291 if not isinstance(ids, list):
293 res = dict.fromkeys(ids, False)
294 for user in self.browse(cr, uid, ids, context=context):
296 res[user.id] = "%s <%s>" % (user.name, user.user_email)
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]
306 def _get_company(self,cr, uid, context=None, uid2=False):
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
313 def _get_companies(self, cr, uid, context=None):
314 c = self._get_company(cr, uid, context)
319 def _get_menu(self,cr, uid, context=None):
320 dataobj = self.pool.get('ir.model.data')
322 model, res_id = dataobj.get_object_reference(cr, uid, 'base', 'action_menu_admin')
323 if model != 'ir.actions.act_window':
329 def _get_group(self,cr, uid, context=None):
330 dataobj = self.pool.get('ir.model.data')
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)
338 # If these groups does not exists anymore
344 'context_lang': 'en_US',
346 'menu_id': _get_menu,
347 'company_id': _get_company,
348 'company_ids': _get_companies,
349 'groups_id': _get_group,
355 def company_get(self, cr, uid, uid2, context=None):
356 return self._get_company(cr, uid, context=context, uid2=uid2)
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']
361 def write(self, cr, uid, ids, values, context=None):
362 if not hasattr(ids, '__iter__'):
365 for key in values.keys():
366 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
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
374 res = super(users, self).write(cr, uid, ids, values, context=context)
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)
382 if db in self._uid_cache:
384 if id in self._uid_cache[db]:
385 del self._uid_cache[db][id]
389 def unlink(self, cr, uid, ids, context=None):
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, ...)'))
393 if db in self._uid_cache:
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)
399 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
406 ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit)
408 ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit)
409 return self.name_get(cr, user, ids)
411 def copy(self, cr, uid, id, default=None, context=None):
412 user2copy = self.read(cr, uid, [id], ['login','name'])[0]
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!
420 copydef.update(default)
421 return super(users, self).copy(cr, uid, id, copydef, context)
423 def context_get(self, cr, uid, context=None):
424 user = self.browse(cr, uid, uid, context)
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):
431 result[k[8:]] = res or False
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
440 def login(self, db, login, password):
443 cr = pooler.get_db(db).cursor()
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)))
456 def check_super(self, passwd):
457 if passwd == tools.config['admin_passwd']:
460 raise security.ExceptionNoTb('AccessDenied')
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."""
466 # empty passwords disallowed for obvious security reasons
467 raise security.ExceptionNoTb('AccessDenied')
468 if self._uid_cache.get(db, {}).get(uid) == passwd:
470 cr = pooler.get_db(db).cursor()
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]
476 raise security.ExceptionNoTb('AccessDenied')
477 if self._uid_cache.has_key(db):
478 ulist = self._uid_cache[db]
481 self._uid_cache[db] = {uid:passwd}
485 def access(self, db, uid, passwd, sec_level, ids):
488 cr = pooler.get_db(db).cursor()
490 cr.execute('SELECT id FROM res_users WHERE id=%s AND password=%s', (uid, passwd))
493 raise security.ExceptionNoTb('Bad username or password')
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.
504 :raise: security.ExceptionNoTb when old password is wrong
505 :raise: except_osv when new password is not set or empty
507 self.check(cr.dbname, uid, old_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!"))
514 class config_users(osv.osv_memory):
515 _name = 'res.config.users'
516 _inherit = ['res.users', 'res.config']
518 def _generate_signature(self, cr, name, email, context=None):
519 return _('--\n%(name)s %(email)s\n') % {
521 'email': email and ' <'+email+'>' or '',
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.
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
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,},
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',[]))],
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)
553 def execute(self, cr, uid, ids, context=None):
554 'Do nothing on execution, just launch the next action/todo'
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)
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',
570 class groups2(osv.osv): ##FIXME: Is there a reason to inherit this object ?
571 _inherit = 'res.groups'
573 'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
576 def unlink(self, cr, uid, ids, context=None):
578 for record in self.read(cr, uid, ids, ['users'], context=context):
580 group_users.extend(record['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]
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)
594 class res_config_view(osv.osv_memory):
595 _name = 'res.config.view'
596 _inherit = 'res.config'
598 'name':fields.char('Name', size=64),
599 'view': fields.selection([('simple','Simplified'),
600 ('extended','Extended')],
601 'Interface', required=True ),
604 'view':lambda self,cr,uid,*args: self.pool.get('res.users').browse(cr, uid, uid).view or 'simple',
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)
614 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: