[FIX] user preferences: fix company_id on_change warning and move to correct view
[odoo/odoo.git] / bin / 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 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 from osv import fields,osv
23 from osv.orm import browse_record
24 import tools
25 from functools import partial
26 import pytz
27 import pooler
28 from tools.translate import _
29 from service import security
30 import netsvc
31 import time
32
33 class groups(osv.osv):
34     _name = "res.groups"
35     _order = 'name'
36     _columns = {
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),
43     }
44     _sql_constraints = [
45         ('name_uniq', 'unique (name)', 'The name of the group must be unique !')
46     ]
47
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)
52
53     def write(self, cr, uid, ids, vals, context=None):
54         if 'name' in vals:
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)
60         return res
61
62     def create(self, cr, uid, vals, context=None):
63         if 'name' in vals:
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):
69             pass
70         else:
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))
74             if aid:
75                 aid.write({'groups_id': [(4, gid)]})
76         return gid
77
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
82
83 groups()
84
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]
90     return res
91
92 def _tz_get(self,cr,uid, context={}):
93     return [(x, x) for x in pytz.all_timezones]
94
95 class users(osv.osv):
96     __admin_ids = {}
97     _uid_cache = {}
98     _name = "res.users"
99
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, "\
106         "please delete it."
107
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
112         """
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
118         """
119         return self.WELCOME_MAIL_BODY
120
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)
123         return cr.fetchall()
124
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'))
131             return False
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 '
135                   'to users'))
136             return False
137         if not user.get('email'):
138             return False
139
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)
145
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
151         @return:  True/False
152         """
153         if not value or value not in ['simple','extended']:
154             return False
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)
162         return True
163
164
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
170         """
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]))
175
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
180
181     def _email_set(self, cr, uid, ids, name, value, arg, context=None):
182         if not isinstance(ids,list):
183             ids = [ids]
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.
188             if user.address_id:
189                 address_obj.write(cr, 1, user.address_id.id, {'email': value or None}) # no context to avoid potential security issues as superuser
190             else:
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)
193         return True
194
195     _columns = {
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 "
206                  "users."),
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'),
213
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
216         # context is set.
217         'company_id': fields.many2one('res.company', 'Company', required=True,
218             help="The company this user is currently working for.", context={'user_preference': True}),
219
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),
233     }
234
235     def on_change_company_id(self, cr, uid, ids, company_id):
236         return {
237                 'warning' : {
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)"),
240                 }
241         }
242
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'] = '********'
247             return o
248
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)
251         if not canwrite:
252             if isinstance(ids, (int, float)):
253                 result = override_password(result)
254             else:
255                 result = map(override_password, result)
256         return result
257
258
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))
261
262     _constraints = [
263         (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
264     ]
265
266     _sql_constraints = [
267         ('login_key', 'UNIQUE (login)',  _('You can not have two users with the same login !'))
268     ]
269
270     def _get_email_from(self, cr, uid, ids, context=None):
271         if not isinstance(ids, list):
272             ids = [ids]
273         res = dict.fromkeys(ids, False)
274         for user in self.browse(cr, uid, ids, context=context):
275             if user.user_email:
276                 res[user.id] = "%s <%s>" % (user.name, user.user_email)
277         return res
278
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]
285
286     def _get_company(self,cr, uid, context=None, uid2=False):
287         if not uid2:
288             uid2 = uid
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
292
293     def _get_companies(self, cr, uid, context=None):
294         c = self._get_company(cr, uid, context)
295         if c:
296             return [c]
297         return False
298
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
302
303     def _get_group(self,cr, uid, context=None):
304         dataobj = self.pool.get('ir.model.data')
305         result = []
306         try:
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)
311         except ValueError:
312             # If these groups does not exists anymore
313             pass
314         return result
315
316     _defaults = {
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,
324         'address_id': False,
325         'menu_tips':True
326     }
327
328     @tools.cache()
329     def company_get(self, cr, uid, uid2, context=None):
330         return self._get_company(cr, uid, context=context, uid2=uid2)
331
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']
334
335     def write(self, cr, uid, ids, values, context=None):
336         if not hasattr(ids, '__iter__'):
337             ids = [ids]
338         if ids == [uid]:
339             for key in values.keys():
340                 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
341                     break
342             else:
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
347
348         res = super(users, self).write(cr, uid, ids, values, context=context)
349
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)
354         map(clear, ids)
355
356         return res
357
358     def unlink(self, cr, uid, ids, context=None):
359         if 1 in ids:
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)
362
363     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
364         if not args:
365             args=[]
366         if not context:
367             context={}
368         ids = []
369         if name:
370             ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit)
371         if not ids:
372             ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit)
373         return self.name_get(cr, user, ids)
374
375     def copy(self, cr, uid, id, default=None, context={}):
376         user2copy = self.read(cr, uid, [id], ['login','name'])[0]
377         if default is None:
378             default = {}
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!
383                        )
384         return super(users, self).copy(cr, uid, id, default, context)
385
386     def context_get(self, cr, uid, context=None):
387         user = self.browse(cr, uid, uid, context)
388         result = {}
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):
393                     res = res.id
394                 result[k[8:]] = res or False
395         return result
396
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
401
402
403     def login(self, db, login, password):
404         if not password:
405             return False
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)))
408         res = cr.fetchone()
409         result = False
410         if res:
411             cr.execute("update res_users set date=%s where id=%s", (time.strftime('%Y-%m-%d %H:%M:%S'),res[0]))
412             cr.commit()
413             result = res[0]
414         cr.close()
415         return result
416     def check_super(self, passwd):
417         if passwd == tools.config['admin_passwd']:
418             return True
419         else:
420             raise security.ExceptionNoTb('AccessDenied')
421
422     def check(self, db, uid, passwd):
423         if not passwd:
424             return False
425         cached_pass = self._uid_cache.get(db, {}).get(uid)
426         if (cached_pass is not None) and cached_pass == passwd:
427             return True
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]
431         cr.close()
432         if not bool(res):
433             raise security.ExceptionNoTb('AccessDenied')
434         if res:
435             if self._uid_cache.has_key(db):
436                 ulist = self._uid_cache[db]
437                 ulist[uid] = passwd
438             else:
439                 self._uid_cache[db] = {uid:passwd}
440         return bool(res)
441
442     def access(self, db, uid, passwd, sec_level, ids):
443         if not passwd:
444             return False
445         cr = pooler.get_db(db).cursor()
446         cr.execute('select id from res_users where id=%s and password=%s', (uid, passwd))
447         res = cr.fetchone()
448         cr.close()
449         if not res:
450             raise security.ExceptionNoTb('Bad username or password')
451         return res[0]
452
453 users()
454
455 class config_users(osv.osv_memory):
456     _name = 'res.config.users'
457     _inherit = ['res.users', 'res.config']
458
459     def _generate_signature(self, cr, name, email, context=None):
460         return _('--\n%(name)s %(email)s\n') % {
461             'name': name or '',
462             'email': email and ' <'+email+'>' or '',
463             }
464
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.
468
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
473         """
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,},
480             context)
481         user_data = dict(
482             base_data,
483             signature=self._generate_signature(
484                 cr, base_data['name'], base_data['email'], context=context),
485             address_id=address,
486             )
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'
492         pass
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)
496         return {
497             'view_type': 'form',
498             "view_mode": 'form',
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',
503             'target':'new',
504             }
505 config_users()
506
507 class groups2(osv.osv): ##FIXME: Is there a reason to inherit this object ?
508     _inherit = 'res.groups'
509     _columns = {
510         'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
511     }
512
513     def unlink(self, cr, uid, ids, context=None):
514         for record in self.read(cr, uid, ids, ['users'], context=context):
515             if record['users']:
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)
518
519 groups2()
520
521 class res_config_view(osv.osv_memory):
522     _name = 'res.config.view'
523     _inherit = 'res.config'
524     _columns = {
525         'name':fields.char('Name', size=64),
526         'view': fields.selection([('simple','Simplified'),
527                                   ('extended','Extended')],
528                                  'Interface', required=True ),
529     }
530     _defaults={
531         'view':lambda self,cr,uid,*args: self.pool.get('res.users').browse(cr, uid, uid).view or 'simple',
532     }
533
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)
538
539 res_config_view()
540
541 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
542