1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from osv import fields,osv
23 from osv.orm import browse_record
25 from functools import partial
28 from tools.translate import _
29 from service import security
33 class groups(osv.osv):
37 'name': fields.char('Group Name', size=64, required=True),
38 'model_access': fields.one2many('ir.model.access', 'group_id', 'Access Controls'),
39 'rule_groups': fields.many2many('ir.rule', 'rule_group_rel',
40 'group_id', 'rule_group_id', 'Rules', domain=[('global', '=', False)]),
41 'menu_access': fields.many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', 'Access Menu'),
42 'comment' : fields.text('Comment',size=250),
45 ('name_uniq', 'unique (name)', 'The name of the group must be unique !')
48 def copy(self, cr, uid, id, default=None, context={}):
49 group_name = self.read(cr, uid, [id], ['name'])[0]['name']
50 default.update({'name': _('%s (copy)')%group_name})
51 return super(groups, self).copy(cr, uid, id, default, context)
53 def write(self, cr, uid, ids, vals, context=None):
55 if vals['name'].startswith('-'):
56 raise osv.except_osv(_('Error'),
57 _('The name of the group can not start with "-"'))
58 res = super(groups, self).write(cr, uid, ids, vals, context=context)
59 self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
62 def create(self, cr, uid, vals, context=None):
64 if vals['name'].startswith('-'):
65 raise osv.except_osv(_('Error'),
66 _('The name of the group can not start with "-"'))
67 gid = super(groups, self).create(cr, uid, vals, context=context)
68 if context and context.get('noadmin', False):
71 # assign this new group to user_root
72 user_obj = self.pool.get('res.users')
73 aid = user_obj.browse(cr, 1, user_obj._get_admin_id(cr))
75 aid.write({'groups_id': [(4, gid)]})
78 def get_extended_interface_group(self, cr, uid, context=None):
79 data_obj = self.pool.get('ir.model.data')
80 extended_group_data_id = data_obj._get_id(cr, uid, 'base', 'group_extended')
81 return data_obj.browse(cr, uid, extended_group_data_id, context=context).res_id
85 def _lang_get(self, cr, uid, context={}):
86 obj = self.pool.get('res.lang')
87 ids = obj.search(cr, uid, [('translatable','=',True)])
88 res = obj.read(cr, uid, ids, ['code', 'name'], context)
89 res = [(r['code'], r['name']) for r in res]
92 def _tz_get(self,cr,uid, context={}):
93 return [(x, x) for x in pytz.all_timezones]
100 WELCOME_MAIL_SUBJECT = u"Welcome to OpenERP"
101 WELCOME_MAIL_BODY = u"An OpenERP account has been created for you, "\
102 "\"%(name)s\".\n\nYour login is %(login)s, "\
103 "you should ask your supervisor or system administrator if you "\
104 "haven't been given your password yet.\n\n"\
105 "If you aren't %(name)s, this email reached you errorneously, "\
108 def get_welcome_mail_subject(self, cr, uid, context=None):
109 """ Returns the subject of the mail new users receive (when
110 created via the res.config.users wizard), default implementation
111 is to return config_users.WELCOME_MAIL_SUBJECT
113 return self.WELCOME_MAIL_SUBJECT
114 def get_welcome_mail_body(self, cr, uid, context=None):
115 """ Returns the subject of the mail new users receive (when
116 created via the res.config.users wizard), default implementation
117 is to return config_users.WELCOME_MAIL_BODY
119 return self.WELCOME_MAIL_BODY
121 def get_current_company(self, cr, uid):
122 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 def send_welcome_email(self, cr, uid, id, context=None):
126 logger= netsvc.Logger()
127 user = self.pool.get('res.users').read(cr, uid, id, context=context)
128 if not tools.config.get('smtp_server'):
129 logger.notifyChannel('mails', netsvc.LOG_WARNING,
130 _('"smtp_server" needs to be set to send mails to users'))
132 if not tools.config.get('email_from'):
133 logger.notifyChannel("mails", netsvc.LOG_WARNING,
134 _('"email_from" needs to be set to send welcome mails '
137 if not user.get('email'):
140 return tools.email_send(email_from=None, email_to=[user['email']],
141 subject=self.get_welcome_mail_subject(
142 cr, uid, context=context),
143 body=self.get_welcome_mail_body(
144 cr, uid, context=context) % user)
146 def _set_interface_type(self, cr, uid, ids, name, value, arg, context=None):
147 """Implementation of 'view' function field setter, sets the type of interface of the users.
148 @param name: Name of the field
149 @param arg: User defined argument
150 @param value: new value returned
153 if not value or value not in ['simple','extended']:
155 group_obj = self.pool.get('res.groups')
156 extended_group_id = group_obj.get_extended_interface_group(cr, uid, context=context)
157 # First always remove the users from the group (avoids duplication if called twice)
158 self.write(cr, uid, ids, {'groups_id': [(3, extended_group_id)]}, context=context)
159 # Then add them back if requested
160 if value == 'extended':
161 self.write(cr, uid, ids, {'groups_id': [(4, extended_group_id)]}, context=context)
165 def _get_interface_type(self, cr, uid, ids, name, args, context=None):
166 """Implementation of 'view' function field getter, returns the type of interface of the users.
167 @param field_name: Name of the field
168 @param arg: User defined argument
169 @return: Dictionary of values
171 group_obj = self.pool.get('res.groups')
172 extended_group_id = group_obj.get_extended_interface_group(cr, uid, context=context)
173 extended_users = group_obj.read(cr, uid, extended_group_id, ['users'], context=context)['users']
174 return dict(zip(ids, ['extended' if user in extended_users else 'simple' for user in ids]))
176 def _email_get(self, cr, uid, ids, name, arg, context=None):
177 # perform this as superuser because the current user is allowed to read users, and that includes
178 # the email, even without any direct read access on the res_partner_address object.
179 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
181 def _email_set(self, cr, uid, ids, name, value, arg, context=None):
182 if not isinstance(ids,list):
184 address_obj = self.pool.get('res.partner.address')
185 for user in self.browse(cr, uid, ids, context=context):
186 # perform this as superuser because the current user is allowed to write to the user, and that includes
187 # the email even without any direct write access on the res_partner_address object.
189 address_obj.write(cr, 1, user.address_id.id, {'email': value or None}) # no context to avoid potential security issues as superuser
191 address_id = address_obj.create(cr, 1, {'name': user.name, 'email': value or None}) # no context to avoid potential security issues as superuser
192 self.write(cr, uid, ids, {'address_id': address_id}, context)
196 'name': fields.char('User Name', size=64, required=True, select=True,
197 help="The new user's real name, used for searching"
198 " and most listings"),
199 'login': fields.char('Login', size=64, required=True,
200 help="Used to log into the system"),
201 '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."),
202 'email': fields.char('E-mail', size=64,
203 help='If an email is provided, the user will be sent a message '
204 'welcoming him.\n\nWarning: if "email_from" and "smtp_server"'
205 " aren't configured, it won't be possible to email new "
207 'signature': fields.text('Signature', size=64),
208 'address_id': fields.many2one('res.partner.address', 'Address'),
209 'active': fields.boolean('Active'),
210 '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."),
211 'menu_id': fields.many2one('ir.actions.actions', 'Menu Action', help="If specified, the action will replace the standard menu for this user."),
212 'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
214 # Special behavior for this field: res.company.search() will only return the companies
215 # available to the current user (should be the user's companies?), when the user_preference
217 'company_id': fields.many2one('res.company', 'Company', required=True,
218 help="The company this user is currently working for.", context={'user_preference': True}),
220 'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
221 'context_lang': fields.selection(_lang_get, 'Language', required=True,
222 help="Sets the language for the user's user interface, when UI "
223 "translations are available"),
224 'context_tz': fields.selection(_tz_get, 'Timezone', size=64,
225 help="The user's timezone, used to perform timezone conversions "
226 "between the server and the client."),
227 'view': fields.function(_get_interface_type, method=True, type='selection', fnct_inv=_set_interface_type,
228 selection=[('simple','Simplified'),('extended','Extended')],
229 string='Interface', help="Choose between the simplified interface and the extended one"),
230 'user_email': fields.function(_email_get, method=True, fnct_inv=_email_set, string='Email', type="char", size=240),
231 'menu_tips': fields.boolean('Menu Tips', help="Check out this box if you want to always display tips on each menu action"),
232 'date': fields.datetime('Last Connection', readonly=True),
235 def on_change_company_id(self, cr, uid, ids, company_id):
238 'title': _("Company Switch Warning"),
239 '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)"),
243 def read(self,cr, uid, ids, fields=None, context=None, load='_classic_read'):
244 def override_password(o):
245 if 'password' in o and ( 'id' not in o or o['id'] != uid ):
246 o['password'] = '********'
249 result = super(users, self).read(cr, uid, ids, fields, context, load)
250 canwrite = self.pool.get('ir.model.access').check(cr, uid, 'res.users', 'write', raise_exception=False)
252 if isinstance(ids, (int, float)):
253 result = override_password(result)
255 result = map(override_password, result)
259 def _check_company(self, cr, uid, ids, context=None):
260 return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context))
263 (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
267 ('login_key', 'UNIQUE (login)', _('You can not have two users with the same login !'))
270 def _get_email_from(self, cr, uid, ids, context=None):
271 if not isinstance(ids, list):
273 res = dict.fromkeys(ids, False)
274 for user in self.browse(cr, uid, ids, context=context):
276 res[user.id] = "%s <%s>" % (user.name, user.user_email)
279 def _get_admin_id(self, cr):
280 if self.__admin_ids.get(cr.dbname) is None:
281 ir_model_data_obj = self.pool.get('ir.model.data')
282 mdid = ir_model_data_obj._get_id(cr, 1, 'base', 'user_root')
283 self.__admin_ids[cr.dbname] = ir_model_data_obj.read(cr, 1, [mdid], ['res_id'])[0]['res_id']
284 return self.__admin_ids[cr.dbname]
286 def _get_company(self,cr, uid, context=None, uid2=False):
289 user = self.pool.get('res.users').read(cr, uid, uid2, ['company_id'], context)
290 company_id = user.get('company_id', False)
291 return company_id and company_id[0] or False
293 def _get_companies(self, cr, uid, context=None):
294 c = self._get_company(cr, uid, context)
299 def _get_menu(self,cr, uid, context=None):
300 ids = self.pool.get('ir.actions.act_window').search(cr, uid, [('usage','=','menu')], context=context)
301 return ids and ids[0] or False
303 def _get_group(self,cr, uid, context=None):
304 dataobj = self.pool.get('ir.model.data')
307 dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_user')
308 result.append(group_id)
309 dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_partner_manager')
310 result.append(group_id)
312 # If these groups does not exists anymore
317 'password' : lambda *a : '',
318 'context_lang': lambda *args: 'en_US',
319 'active' : lambda *a: True,
320 'menu_id': _get_menu,
321 'company_id': _get_company,
322 'company_ids': _get_companies,
323 'groups_id': _get_group,
329 def company_get(self, cr, uid, uid2, context=None):
330 return self._get_company(cr, uid, context=context, uid2=uid2)
332 # User can write to a few of her own fields (but not her groups for example)
333 SELF_WRITEABLE_FIELDS = ['menu_tips','view', 'password', 'signature', 'action_id', 'company_id', 'user_email']
335 def write(self, cr, uid, ids, values, context=None):
336 if not hasattr(ids, '__iter__'):
339 for key in values.keys():
340 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
343 if 'company_id' in values:
344 if not (values['company_id'] in self.read(cr, uid, uid, ['company_ids'], context=context)['company_ids']):
345 del values['company_id']
346 uid = 1 # safe fields only, so we write as super-user to bypass access rights
348 res = super(users, self).write(cr, uid, ids, values, context=context)
350 # clear caches linked to the users
351 self.company_get.clear_cache(cr.dbname)
352 self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
353 clear = partial(self.pool.get('ir.rule').clear_cache, cr)
358 def unlink(self, cr, uid, ids, context=None):
360 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, ...)'))
361 return super(users, self).unlink(cr, uid, ids, context=context)
363 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
370 ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit)
372 ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit)
373 return self.name_get(cr, user, ids)
375 def copy(self, cr, uid, id, default=None, context={}):
376 user2copy = self.read(cr, uid, [id], ['login','name'])[0]
379 copy_pattern = _("%s (copy)")
380 default.update(login=(copy_pattern % user2copy['login']),
381 name=(copy_pattern % user2copy['name']),
382 address_id=False, # avoid sharing the address of the copied user!
384 return super(users, self).copy(cr, uid, id, default, context)
386 def context_get(self, cr, uid, context=None):
387 user = self.browse(cr, uid, uid, context)
389 for k in self._columns.keys():
390 if k.startswith('context_'):
391 res = getattr(user,k) or False
392 if isinstance(res, browse_record):
394 result[k[8:]] = res or False
397 def action_get(self, cr, uid, context={}):
398 dataobj = self.pool.get('ir.model.data')
399 data_id = dataobj._get_id(cr, 1, 'base', 'action_res_users_my')
400 return dataobj.browse(cr, uid, data_id, context).res_id
403 def login(self, db, login, password):
406 cr = pooler.get_db(db).cursor()
407 cr.execute('select id from res_users where login=%s and password=%s and active', (tools.ustr(login), tools.ustr(password)))
411 cr.execute("update res_users set date=%s where id=%s", (time.strftime('%Y-%m-%d %H:%M:%S'),res[0]))
416 def check_super(self, passwd):
417 if passwd == tools.config['admin_passwd']:
420 raise security.ExceptionNoTb('AccessDenied')
422 def check(self, db, uid, passwd):
425 cached_pass = self._uid_cache.get(db, {}).get(uid)
426 if (cached_pass is not None) and cached_pass == passwd:
428 cr = pooler.get_db(db).cursor()
429 cr.execute('select count(1) from res_users where id=%s and password=%s and active=%s', (int(uid), passwd, True))
430 res = cr.fetchone()[0]
433 raise security.ExceptionNoTb('AccessDenied')
435 if self._uid_cache.has_key(db):
436 ulist = self._uid_cache[db]
439 self._uid_cache[db] = {uid:passwd}
442 def access(self, db, uid, passwd, sec_level, ids):
445 cr = pooler.get_db(db).cursor()
446 cr.execute('select id from res_users where id=%s and password=%s', (uid, passwd))
450 raise security.ExceptionNoTb('Bad username or password')
455 class config_users(osv.osv_memory):
456 _name = 'res.config.users'
457 _inherit = ['res.users', 'res.config']
459 def _generate_signature(self, cr, name, email, context=None):
460 return _('--\n%(name)s %(email)s\n') % {
462 'email': email and ' <'+email+'>' or '',
465 def create_user(self, cr, uid, new_id, context=None):
466 """ create a new res.user instance from the data stored
467 in the current res.config.users.
469 If an email address was filled in for the user, sends a mail
470 composed of the return values of ``get_welcome_mail_subject``
471 and ``get_welcome_mail_body`` (which should be unicode values),
472 with the user's data %-formatted into the mail body
474 base_data = self.read(cr, uid, new_id, context=context)
475 partner_id = self.pool.get('res.partner').main_partner(cr, uid)
476 address = self.pool.get('res.partner.address').create(
477 cr, uid, {'name': base_data['name'],
478 'email': base_data['email'],
479 'partner_id': partner_id,},
483 signature=self._generate_signature(
484 cr, base_data['name'], base_data['email'], context=context),
487 new_user = self.pool.get('res.users').create(
488 cr, uid, user_data, context)
489 self.send_welcome_email(cr, uid, new_user, context=context)
490 def execute(self, cr, uid, ids, context=None):
491 'Do nothing on execution, just launch the next action/todo'
493 def action_add(self, cr, uid, ids, context=None):
494 'Create a user, and re-display the view'
495 self.create_user(cr, uid, ids[0], context=context)
499 'res_model': 'res.config.users',
500 'view_id':self.pool.get('ir.ui.view')\
501 .search(cr,uid,[('name','=','res.config.users.confirm.form')]),
502 'type': 'ir.actions.act_window',
507 class groups2(osv.osv): ##FIXME: Is there a reason to inherit this object ?
508 _inherit = 'res.groups'
510 'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
513 def unlink(self, cr, uid, ids, context=None):
514 for record in self.read(cr, uid, ids, ['users'], context=context):
516 raise osv.except_osv(_('Warning !'), _('Make sure you have no users linked with the group(s)!'))
517 return super(groups2, self).unlink(cr, uid, ids, context=context)
521 class res_config_view(osv.osv_memory):
522 _name = 'res.config.view'
523 _inherit = 'res.config'
525 'name':fields.char('Name', size=64),
526 'view': fields.selection([('simple','Simplified'),
527 ('extended','Extended')],
528 'Interface', required=True ),
531 'view':lambda self,cr,uid,*args: self.pool.get('res.users').browse(cr, uid, uid).view or 'simple',
534 def execute(self, cr, uid, ids, context=None):
535 res = self.read(cr, uid, ids)[0]
536 self.pool.get('res.users').write(cr, uid, [uid],
537 {'view':res['view']}, context=context)
541 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: