[MERGE] forward port of branch 8.0 up to 591e329
[odoo/odoo.git] / openerp / addons / base / res / res_users.py
index 909b14d..7cb8f30 100644 (file)
@@ -3,7 +3,7 @@
 #
 #    OpenERP, Open Source Management Solution
 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
-#    Copyright (C) 2010-2013 OpenERP s.a. (<http://openerp.com>).
+#    Copyright (C) 2010-2014 OpenERP s.a. (<http://openerp.com>).
 #
 #    This program is free software: you can redistribute it and/or modify
 #    it under the terms of the GNU Affero General Public License as
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 ##############################################################################
-from functools import partial
+import itertools
 import logging
+from functools import partial
+from itertools import repeat
+
 from lxml import etree
 from lxml.builder import E
 
 import openerp
-from openerp import SUPERUSER_ID
-from openerp import pooler, tools
+from openerp import SUPERUSER_ID, models
+from openerp import tools
 import openerp.exceptions
-from openerp.osv import fields,osv, expression
-from openerp.osv.orm import browse_record
+from openerp.osv import fields, osv, expression
 from openerp.tools.translate import _
+from openerp.http import request
 
 _logger = logging.getLogger(__name__)
 
-class groups(osv.osv):
+#----------------------------------------------------------
+# Basic res.groups and res.users
+#----------------------------------------------------------
+
+class res_groups(osv.osv):
     _name = "res.groups"
     _description = "Access Groups"
     _rec_name = 'full_name'
+    _order = 'name'
 
     def _get_full_name(self, cr, uid, ids, field, arg, context=None):
         res = {}
@@ -81,9 +89,9 @@ class groups(osv.osv):
         return where
 
     _columns = {
-        'name': fields.char('Name', size=64, required=True, translate=True),
+        'name': fields.char('Name', required=True, translate=True),
         'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
-        'model_access': fields.one2many('ir.model.access', 'group_id', 'Access Controls'),
+        'model_access': fields.one2many('ir.model.access', 'group_id', 'Access Controls', copy=True),
         'rule_groups': fields.many2many('ir.rule', 'rule_group_rel',
             'group_id', 'rule_group_id', 'Rules', domain=[('global', '=', False)]),
         'menu_access': fields.many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', 'Access Menu'),
@@ -100,29 +108,28 @@ class groups(osv.osv):
     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
         # add explicit ordering if search is sorted on full_name
         if order and order.startswith('full_name'):
-            ids = super(groups, self).search(cr, uid, args, context=context)
+            ids = super(res_groups, self).search(cr, uid, args, context=context)
             gs = self.browse(cr, uid, ids, context)
             gs.sort(key=lambda g: g.full_name, reverse=order.endswith('DESC'))
             gs = gs[offset:offset+limit] if limit else gs[offset:]
             return map(int, gs)
-        return super(groups, self).search(cr, uid, args, offset, limit, order, context, count)
+        return super(res_groups, self).search(cr, uid, args, offset, limit, order, context, count)
 
     def copy(self, cr, uid, id, default=None, context=None):
         group_name = self.read(cr, uid, [id], ['name'])[0]['name']
         default.update({'name': _('%s (copy)')%group_name})
-        return super(groups, self).copy(cr, uid, id, default, context)
+        return super(res_groups, self).copy(cr, uid, id, default, context)
 
     def write(self, cr, uid, ids, vals, context=None):
         if 'name' in vals:
             if vals['name'].startswith('-'):
                 raise osv.except_osv(_('Error'),
                         _('The name of the group can not start with "-"'))
-        res = super(groups, self).write(cr, uid, ids, vals, context=context)
-        self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
+        res = super(res_groups, self).write(cr, uid, ids, vals, context=context)
+        self.pool['ir.model.access'].call_cache_clearing_methods(cr)
+        self.pool['res.users'].has_group.clear_cache(self.pool['res.users'])
         return res
 
-groups()
-
 class res_users(osv.osv):
     """ User class. A res.users record models an OpenERP user and is different
         from an employee.
@@ -156,23 +163,22 @@ class res_users(osv.osv):
 
     _columns = {
         'id': fields.integer('ID'),
-        'login_date': fields.date('Latest connection', select=1),
+        'login_date': fields.datetime('Latest connection', select=1, copy=False),
         'partner_id': fields.many2one('res.partner', required=True,
             string='Related Partner', ondelete='restrict',
-            help='Partner-related data of the user'),
+            help='Partner-related data of the user', auto_join=True),
         'login': fields.char('Login', size=64, required=True,
             help="Used to log into the system"),
-        'password': fields.char('Password', size=64, invisible=True,
+        'password': fields.char('Password', size=64, invisible=True, copy=False,
             help="Keep empty if you don't want the user to be able to connect on the system."),
         'new_password': fields.function(_get_password, type='char', size=64,
             fnct_inv=_set_new_password, string='Set Password',
             help="Specify a value only when creating a user or if you're "\
                  "changing the user's password, otherwise leave empty. After "\
                  "a change of password, the user has to login again."),
-        'signature': fields.text('Signature'),
+        'signature': fields.html('Signature'),
         'active': fields.boolean('Active'),
-        '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."),
-        'menu_id': fields.many2one('ir.actions.actions', 'Menu Action', help="If specified, the action will replace the standard menu for this user."),
+        'action_id': fields.many2one('ir.actions.actions', 'Home Action', help="If specified, this action will be opened at log on for this user, in addition to the standard menu."),
         'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
         # Special behavior for this field: res.company.search() will only return the companies
         # available to the current user (should be the user's companies?), when the user_preference
@@ -180,17 +186,17 @@ class res_users(osv.osv):
         'company_id': fields.many2one('res.company', 'Company', required=True,
             help='The company this user is currently working for.', context={'user_preference': True}),
         'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
-        # backward compatibility fields
-        'user_email': fields.related('email', type='char',
-            deprecated='Use the email field instead of user_email. This field will be removed with OpenERP 7.1.'),
     }
 
-    def on_change_company_id(self, cr, uid, ids, company_id):
-        return {'warning' : {
-                    'title': _("Company Switch Warning"),
-                    '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)"),
-                }
-        }
+    # overridden inherited fields to bypass access rights, in case you have
+    # access to the user but not its corresponding partner
+    name = openerp.fields.Char(related='partner_id.name', inherited=True)
+    email = openerp.fields.Char(related='partner_id.email', inherited=True)
+
+    def on_change_login(self, cr, uid, ids, login, context=None):
+        if login and tools.single_email_re.match(login):
+            return {'value': {'email': login}}
+        return {}
 
     def onchange_state(self, cr, uid, ids, state_id, context=None):
         partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
@@ -202,7 +208,7 @@ class res_users(osv.osv):
             partner.onchange_type method, but applied to the user object.
         """
         partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
-        return self.pool.get('res.partner').onchange_type(cr, uid, partner_ids, is_company, context=context)
+        return self.pool['res.partner'].onchange_type(cr, uid, partner_ids, is_company, context=context)
 
     def onchange_address(self, cr, uid, ids, use_parent_address, parent_id, context=None):
         """ Wrapper on the user.partner onchange_address, because some calls to the
@@ -210,7 +216,7 @@ class res_users(osv.osv):
             partner.onchange_type method, but applied to the user object.
         """
         partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
-        return self.pool.get('res.partner').onchange_address(cr, uid, partner_ids, use_parent_address, parent_id, context=context)
+        return self.pool['res.partner'].onchange_address(cr, uid, partner_ids, use_parent_address, parent_id, context=context)
 
     def _check_company(self, cr, uid, ids, context=None):
         return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context))
@@ -226,9 +232,14 @@ class res_users(osv.osv):
     def _get_company(self,cr, uid, context=None, uid2=False):
         if not uid2:
             uid2 = uid
-        user = self.pool.get('res.users').read(cr, uid, uid2, ['company_id'], context)
-        company_id = user.get('company_id', False)
-        return company_id and company_id[0] or False
+        # Use read() to compute default company, and pass load=_classic_write to
+        # avoid useless name_get() calls. This will avoid prefetching fields
+        # while computing default values for new db columns, as the
+        # db backend may not be fully initialized yet.
+        user_data = self.pool['res.users'].read(cr, uid, uid2, ['company_id'],
+                                                context=context, load='_classic_write')
+        comp_id = user_data['company_id']
+        return comp_id or False
 
     def _get_companies(self, cr, uid, context=None):
         c = self._get_company(cr, uid, context)
@@ -236,16 +247,6 @@ class res_users(osv.osv):
             return [c]
         return False
 
-    def _get_menu(self,cr, uid, context=None):
-        dataobj = self.pool.get('ir.model.data')
-        try:
-            model, res_id = dataobj.get_object_reference(cr, uid, 'base', 'action_menu_admin')
-            if model != 'ir.actions.act_window':
-                return False
-            return res_id
-        except ValueError:
-            return False
-
     def _get_group(self,cr, uid, context=None):
         dataobj = self.pool.get('ir.model.data')
         result = []
@@ -259,15 +260,17 @@ class res_users(osv.osv):
             pass
         return result
 
+    def _get_default_image(self, cr, uid, context=None):
+        return self.pool['res.partner']._get_default_image(cr, uid, False, colorize=True, context=context)
+
     _defaults = {
         'password': '',
         'active': True,
         'customer': False,
-        'menu_id': _get_menu,
         'company_id': _get_company,
         'company_ids': _get_companies,
         'groups_id': _get_group,
-        'image': lambda self, cr, uid, ctx={}: self.pool.get('res.partner')._get_default_image(cr, uid, False, ctx, colorize=True),
+        'image': _get_default_image,
     }
 
     # User can write on a few of his own fields (but not his groups for example)
@@ -290,7 +293,7 @@ class res_users(osv.osv):
                 uid = SUPERUSER_ID
 
         result = super(res_users, self).read(cr, uid, ids, fields=fields, context=context, load=load)
-        canwrite = self.pool.get('ir.model.access').check(cr, uid, 'res.users', 'write', False)
+        canwrite = self.pool['ir.model.access'].check(cr, uid, 'res.users', 'write', False)
         if not canwrite:
             if isinstance(ids, (int, long)):
                 result = override_password(result)
@@ -315,7 +318,8 @@ class res_users(osv.osv):
                     break
             else:
                 if 'company_id' in values:
-                    if not (values['company_id'] in self.read(cr, SUPERUSER_ID, uid, ['company_ids'], context=context)['company_ids']):
+                    user = self.browse(cr, SUPERUSER_ID, uid, context=context)
+                    if not (values['company_id'] in user.company_ids.ids):
                         del values['company_id']
                 uid = 1 # safe fields only, so we write as super-user to bypass access rights
 
@@ -326,8 +330,8 @@ class res_users(osv.osv):
                 if user.partner_id.company_id and user.partner_id.company_id.id != values['company_id']: 
                     user.partner_id.write({'company_id': user.company_id.id})
         # clear caches linked to the users
-        self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
-        clear = partial(self.pool.get('ir.rule').clear_cache, cr)
+        self.pool['ir.model.access'].call_cache_clearing_methods(cr)
+        clear = partial(self.pool['ir.rule'].clear_cache, cr)
         map(clear, ids)
         db = cr.dbname
         if db in self._uid_cache:
@@ -335,11 +339,12 @@ class res_users(osv.osv):
                 if id in self._uid_cache[db]:
                     del self._uid_cache[db][id]
         self.context_get.clear_cache(self)
+        self.has_group.clear_cache(self)
         return res
 
     def unlink(self, cr, uid, ids, context=None):
         if 1 in ids:
-            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, ...)'))
+            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 Odoo (updates, module installation, ...)'))
         db = cr.dbname
         if db in self._uid_cache:
             for id in ids:
@@ -353,7 +358,7 @@ class res_users(osv.osv):
         if not context:
             context={}
         ids = []
-        if name:
+        if name and operator in ['=', 'ilike']:
             ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit, context=context)
         if not ids:
             ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
@@ -368,17 +373,11 @@ class res_users(osv.osv):
             default['login'] = _("%s (copy)") % user2copy['login']
         return super(res_users, self).copy(cr, uid, id, default, context)
 
-    def copy_data(self, cr, uid, ids, default=None, context=None):
-        if default is None:
-            default = {}
-        default.update({'login_date': False})
-        return super(res_users, self).copy_data(cr, uid, ids, default, context=context)
-
     @tools.ormcache(skiparg=2)
     def context_get(self, cr, uid, context=None):
         user = self.browse(cr, SUPERUSER_ID, uid, context)
         result = {}
-        for k in self._all_columns.keys():
+        for k in self._fields:
             if k.startswith('context_'):
                 context_key = k[8:]
             elif k in ['lang', 'tz']:
@@ -386,14 +385,14 @@ class res_users(osv.osv):
             else:
                 context_key = False
             if context_key:
-                res = getattr(user,k) or False
-                if isinstance(res, browse_record):
+                res = getattr(user, k) or False
+                if isinstance(res, models.BaseModel):
                     res = res.id
                 result[context_key] = res or False
         return result
 
     def action_get(self, cr, uid, context=None):
-        dataobj = self.pool.get('ir.model.data')
+        dataobj = self.pool['ir.model.data']
         data_id = dataobj._get_id(cr, SUPERUSER_ID, 'base', 'action_res_users_my')
         return dataobj.browse(cr, uid, data_id, context=context).res_id
 
@@ -409,11 +408,11 @@ class res_users(osv.osv):
         if not res:
             raise openerp.exceptions.AccessDenied()
 
-    def login(self, db, login, password):
+    def _login(self, db, login, password):
         if not password:
             return False
         user_id = False
-        cr = pooler.get_db(db).cursor()
+        cr = self.pool.cursor()
         try:
             # autocommit: our single update request will be performed atomically.
             # (In this way, there is no opportunity to have two transactions
@@ -436,8 +435,11 @@ class res_users(osv.osv):
                 # prevent/delay login in that case. It will also have been logged
                 # as a SQL error, if anyone cares.
                 try:
-                    cr.execute("SELECT id FROM res_users WHERE id=%s FOR UPDATE NOWAIT", (user_id,), log_exceptions=False)
+                    # NO KEY introduced in PostgreSQL 9.3 http://www.postgresql.org/docs/9.3/static/release-9-3.html#AEN115299
+                    update_clause = 'NO KEY UPDATE' if cr._cnx.server_version >= 90300 else 'UPDATE'
+                    cr.execute("SELECT id FROM res_users WHERE id=%%s FOR %s NOWAIT" % update_clause, (user_id,), log_exceptions=False)
                     cr.execute("UPDATE res_users SET login_date = now() AT TIME ZONE 'UTC' WHERE id=%s", (user_id,))
+                    self.invalidate_cache(cr, user_id, ['login_date'], [user_id])
                 except Exception:
                     _logger.debug("Failed to update last_login for db:%s login:%s", db, login, exc_info=True)
         except openerp.exceptions.AccessDenied:
@@ -459,15 +461,15 @@ class res_users(osv.osv):
            :param dict user_agent_env: environment dictionary describing any
                relevant environment attributes
         """
-        uid = self.login(db, login, password)
+        uid = self._login(db, login, password)
         if uid == openerp.SUPERUSER_ID:
             # Successfully logged in as admin!
             # Attempt to guess the web base url...
             if user_agent_env and user_agent_env.get('base_location'):
-                cr = pooler.get_db(db).cursor()
+                cr = self.pool.cursor()
                 try:
                     base = user_agent_env['base_location']
-                    ICP = self.pool.get('ir.config_parameter')
+                    ICP = self.pool['ir.config_parameter']
                     if not ICP.get_param(cr, uid, 'web.base.url.freeze'):
                         ICP.set_param(cr, uid, 'web.base.url', base)
                     cr.commit()
@@ -485,7 +487,7 @@ class res_users(osv.osv):
             raise openerp.exceptions.AccessDenied()
         if self._uid_cache.get(db, {}).get(uid) == passwd:
             return
-        cr = pooler.get_db(db).cursor()
+        cr = self.pool.cursor()
         try:
             self.check_credentials(cr, uid, passwd)
             if self._uid_cache.has_key(db):
@@ -512,7 +514,7 @@ class res_users(osv.osv):
     def preference_save(self, cr, uid, ids, context=None):
         return {
             'type': 'ir.actions.client',
-            'tag': 'reload',
+            'tag': 'reload_context',
         }
 
     def preference_change_password(self, cr, uid, ids, context=None):
@@ -522,6 +524,7 @@ class res_users(osv.osv):
             'target': 'new',
         }
 
+    @tools.ormcache(skiparg=2)
     def has_group(self, cr, uid, group_ext_id):
         """Checks whether user belongs to given group.
 
@@ -538,12 +541,13 @@ class res_users(osv.osv):
                    (uid, module, ext_id))
         return bool(cr.fetchone())
 
-
-#
-# Extension of res.groups and res.users with a relation for "implied" or
-# "inherited" groups.  Once a user belongs to a group, it automatically belongs
-# to the implied groups (transitively).
+#----------------------------------------------------------
+# Implied groups
 #
+# Extension of res.groups and res.users with a relation for "implied"
+# or "inherited" groups.  Once a user belongs to a group, it
+# automatically belongs to the implied groups (transitively).
+#----------------------------------------------------------
 
 class cset(object):
     """ A cset (constrained set) is a set of elements that may be constrained to
@@ -566,13 +570,7 @@ class cset(object):
     def __iter__(self):
         return iter(self.elements)
 
-def concat(ls):
-    """ return the concatenation of a list of iterables """
-    res = []
-    for l in ls: res.extend(l)
-    return res
-
-
+concat = itertools.chain.from_iterable
 
 class groups_implied(osv.osv):
     _inherit = 'res.groups'
@@ -611,7 +609,7 @@ class groups_implied(osv.osv):
         res = super(groups_implied, self).write(cr, uid, ids, values, context)
         if values.get('users') or values.get('implied_ids'):
             # add all implied groups (to all users of each group)
-            for g in self.browse(cr, uid, ids):
+            for g in self.browse(cr, uid, ids, context=context):
                 gids = map(int, g.trans_implied_ids)
                 vals = {'users': [(4, u.id) for u in g.users]}
                 super(groups_implied, self).write(cr, uid, gids, vals, context)
@@ -626,6 +624,7 @@ class users_implied(osv.osv):
         if groups:
             # delegate addition of groups to add implied groups
             self.write(cr, uid, [user_id], {'groups_id': groups}, context)
+            self.pool['ir.ui.view'].clear_cache()
         return user_id
 
     def write(self, cr, uid, ids, values, context=None):
@@ -635,11 +634,14 @@ class users_implied(osv.osv):
         if values.get('groups_id'):
             # add implied groups for all users
             for user in self.browse(cr, uid, ids):
-                gs = set(concat([g.trans_implied_ids for g in user.groups_id]))
+                gs = set(concat(g.trans_implied_ids for g in user.groups_id))
                 vals = {'groups_id': [(4, g.id) for g in gs]}
                 super(users_implied, self).write(cr, uid, [user.id], vals, context)
+            self.pool['ir.ui.view'].clear_cache()
         return res
 
+#----------------------------------------------------------
+# Vitrual checkbox and selection for res.user form view
 #
 # Extension of res.groups and res.users for the special groups view in the users
 # form.  This extension presents groups with selection and boolean widgets:
@@ -656,24 +658,30 @@ class users_implied(osv.osv):
 # Naming conventions for reified groups fields:
 # - boolean field 'in_group_ID' is True iff
 #       ID is in 'groups_id'
-# - boolean field 'in_groups_ID1_..._IDk' is True iff
-#       any of ID1, ..., IDk is in 'groups_id'
 # - selection field 'sel_groups_ID1_..._IDk' is ID iff
 #       ID is in 'groups_id' and ID is maximal in the set {ID1, ..., IDk}
+#----------------------------------------------------------
+
+def name_boolean_group(id):
+    return 'in_group_' + str(id)
+
+def name_selection_groups(ids):
+    return 'sel_groups_' + '_'.join(map(str, ids))
+
+def is_boolean_group(name):
+    return name.startswith('in_group_')
 
-def name_boolean_group(id): return 'in_group_' + str(id)
-def name_boolean_groups(ids): return 'in_groups_' + '_'.join(map(str, ids))
-def name_selection_groups(ids): return 'sel_groups_' + '_'.join(map(str, ids))
+def is_selection_groups(name):
+    return name.startswith('sel_groups_')
 
-def is_boolean_group(name): return name.startswith('in_group_')
-def is_boolean_groups(name): return name.startswith('in_groups_')
-def is_selection_groups(name): return name.startswith('sel_groups_')
 def is_reified_group(name):
-    return is_boolean_group(name) or is_boolean_groups(name) or is_selection_groups(name)
+    return is_boolean_group(name) or is_selection_groups(name)
 
-def get_boolean_group(name): return int(name[9:])
-def get_boolean_groups(name): return map(int, name[10:].split('_'))
-def get_selection_groups(name): return map(int, name[11:].split('_'))
+def get_boolean_group(name):
+    return int(name[9:])
+
+def get_selection_groups(name):
+    return map(int, name[11:].split('_'))
 
 def partition(f, xs):
     "return a pair equivalent to (filter(f, xs), filter(lambda x: not f(x), xs))"
@@ -682,6 +690,20 @@ def partition(f, xs):
         (yes if f(x) else nos).append(x)
     return yes, nos
 
+def parse_m2m(commands):
+    "return a list of ids corresponding to a many2many value"
+    ids = []
+    for command in commands:
+        if isinstance(command, (tuple, list)):
+            if command[0] in (1, 4):
+                ids.append(command[2])
+            elif command[0] == 5:
+                ids = []
+            elif command[0] == 6:
+                ids = list(command[2])
+        else:
+            ids.append(command)
+    return ids
 
 
 class groups_view(osv.osv):
@@ -705,8 +727,10 @@ class groups_view(osv.osv):
     def update_user_groups_view(self, cr, uid, context=None):
         # the view with id 'base.user_groups_view' inherits the user form view,
         # and introduces the reified group fields
-        view = self.get_user_groups_view(cr, uid, context)
-        if view:
+        # we have to try-catch this, because at first init the view does not exist
+        # but we are already creating some basic groups
+        view = self.pool['ir.model.data'].xmlid_to_object(cr, SUPERUSER_ID, 'base.user_groups_view', context=context)
+        if view and view.exists() and view._name == 'ir.ui.view':
             xml1, xml2 = [], []
             xml1.append(E.separator(string=_('Application'), colspan="4"))
             for app, kind, gs in self.get_groups_by_application(cr, uid, context):
@@ -731,14 +755,6 @@ class groups_view(osv.osv):
             view.write({'arch': xml_content})
         return True
 
-    def get_user_groups_view(self, cr, uid, context=None):
-        try:
-            view = self.pool.get('ir.model.data').get_object(cr, SUPERUSER_ID, 'base', 'user_groups_view', context)
-            assert view and view._table_name == 'ir.ui.view'
-        except Exception:
-            view = False
-        return view
-
     def get_application_groups(self, cr, uid, domain=None, context=None):
         return self.search(cr, uid, domain or [])
 
@@ -786,67 +802,86 @@ class users_view(osv.osv):
     _inherit = 'res.users'
 
     def create(self, cr, uid, values, context=None):
-        self._set_reified_groups(values)
+        values = self._remove_reified_groups(values)
         return super(users_view, self).create(cr, uid, values, context)
 
     def write(self, cr, uid, ids, values, context=None):
-        self._set_reified_groups(values)
+        values = self._remove_reified_groups(values)
         return super(users_view, self).write(cr, uid, ids, values, context)
 
-    def _set_reified_groups(self, values):
-        """ reflect reified group fields in values['groups_id'] """
-        if 'groups_id' in values:
-            # groups are already given, ignore group fields
-            for f in filter(is_reified_group, values.iterkeys()):
-                del values[f]
-            return
+    def _remove_reified_groups(self, values):
+        """ return `values` without reified group fields """
+        add, rem = [], []
+        values1 = {}
+
+        for key, val in values.iteritems():
+            if is_boolean_group(key):
+                (add if val else rem).append(get_boolean_group(key))
+            elif is_selection_groups(key):
+                rem += get_selection_groups(key)
+                if val:
+                    add.append(val)
+            else:
+                values1[key] = val
 
-        add, remove = [], []
-        for f in values.keys():
-            if is_boolean_group(f):
-                target = add if values.pop(f) else remove
-                target.append(get_boolean_group(f))
-            elif is_boolean_groups(f):
-                if not values.pop(f):
-                    remove.extend(get_boolean_groups(f))
-            elif is_selection_groups(f):
-                remove.extend(get_selection_groups(f))
-                selected = values.pop(f)
-                if selected:
-                    add.append(selected)
-        # update values *only* if groups are being modified, otherwise
-        # we introduce spurious changes that might break the super.write() call.
-        if add or remove:
-            # remove groups in 'remove' and add groups in 'add'
-            values['groups_id'] = [(3, id) for id in remove] + [(4, id) for id in add]
+        if 'groups_id' not in values and (add or rem):
+            # remove group ids in `rem` and add group ids in `add`
+            values1['groups_id'] = zip(repeat(3), rem) + zip(repeat(4), add)
+
+        return values1
 
     def default_get(self, cr, uid, fields, context=None):
         group_fields, fields = partition(is_reified_group, fields)
         fields1 = (fields + ['groups_id']) if group_fields else fields
         values = super(users_view, self).default_get(cr, uid, fields1, context)
-        self._get_reified_groups(group_fields, values)
+        self._add_reified_groups(group_fields, values)
+
+        # add "default_groups_ref" inside the context to set default value for group_id with xml values
+        if 'groups_id' in fields and isinstance(context.get("default_groups_ref"), list):
+            groups = []
+            ir_model_data = self.pool.get('ir.model.data')
+            for group_xml_id in context["default_groups_ref"]:
+                group_split = group_xml_id.split('.')
+                if len(group_split) != 2:
+                    raise osv.except_osv(_('Invalid context value'), _('Invalid context default_groups_ref value (model.name_id) : "%s"') % group_xml_id)
+                try:
+                    temp, group_id = ir_model_data.get_object_reference(cr, uid, group_split[0], group_split[1])
+                except ValueError:
+                    group_id = False
+                groups += [group_id]
+            values['groups_id'] = groups
         return values
 
     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
-        if not fields:
-            fields = self.fields_get(cr, uid, context=context).keys()
-        group_fields, fields = partition(is_reified_group, fields)
-        if not 'groups_id' in fields:
-            fields.append('groups_id')
-        res = super(users_view, self).read(cr, uid, ids, fields, context=context, load=load)
-        if res:
+        # determine whether reified groups fields are required, and which ones
+        fields1 = fields or self.fields_get(cr, uid, context=context).keys()
+        group_fields, other_fields = partition(is_reified_group, fields1)
+
+        # read regular fields (other_fields); add 'groups_id' if necessary
+        drop_groups_id = False
+        if group_fields and fields:
+            if 'groups_id' not in other_fields:
+                other_fields.append('groups_id')
+                drop_groups_id = True
+        else:
+            other_fields = fields
+
+        res = super(users_view, self).read(cr, uid, ids, other_fields, context=context, load=load)
+
+        # post-process result to add reified group fields
+        if group_fields:
             for values in (res if isinstance(res, list) else [res]):
-                self._get_reified_groups(group_fields, values)
+                self._add_reified_groups(group_fields, values)
+                if drop_groups_id:
+                    values.pop('groups_id', None)
         return res
 
-    def _get_reified_groups(self, fields, values):
-        """ compute the given reified group fields from values['groups_id'] """
-        gids = set(values.get('groups_id') or [])
+    def _add_reified_groups(self, fields, values):
+        """ add the given reified group fields into `values` """
+        gids = set(parse_m2m(values.get('groups_id') or []))
         for f in fields:
             if is_boolean_group(f):
                 values[f] = get_boolean_group(f) in gids
-            elif is_boolean_groups(f):
-                values[f] = not gids.isdisjoint(get_boolean_groups(f))
             elif is_selection_groups(f):
                 selected = [gid for gid in get_selection_groups(f) if gid in gids]
                 values[f] = selected and selected[-1] or False
@@ -854,7 +889,7 @@ class users_view(osv.osv):
     def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
         res = super(users_view, self).fields_get(cr, uid, allfields, context, write_access)
         # add reified groups fields
-        for app, kind, gs in self.pool.get('res.groups').get_groups_by_application(cr, uid, context):
+        for app, kind, gs in self.pool['res.groups'].get_groups_by_application(cr, uid, context):
             if kind == 'selection':
                 # selection group field
                 tips = ['%s: %s' % (g.name, g.comment) for g in gs if g.comment]
@@ -878,4 +913,72 @@ class users_view(osv.osv):
                     }
         return res
 
+#----------------------------------------------------------
+# change password wizard
+#----------------------------------------------------------
+
+class change_password_wizard(osv.TransientModel):
+    """
+        A wizard to manage the change of users' passwords
+    """
+
+    _name = "change.password.wizard"
+    _description = "Change Password Wizard"
+    _columns = {
+        'user_ids': fields.one2many('change.password.user', 'wizard_id', string='Users'),
+    }
+
+    def _default_user_ids(self, cr, uid, context=None):
+        if context is None:
+            context = {}
+        user_model = self.pool['res.users']
+        user_ids = context.get('active_model') == 'res.users' and context.get('active_ids') or []
+        return [
+            (0, 0, {'user_id': user.id, 'user_login': user.login})
+            for user in user_model.browse(cr, uid, user_ids, context=context)
+        ]
+
+    _defaults = {
+        'user_ids': _default_user_ids,
+    }
+
+    def change_password_button(self, cr, uid, ids, context=None):
+        wizard = self.browse(cr, uid, ids, context=context)[0]
+        need_reload = any(uid == user.user_id.id for user in wizard.user_ids)
+
+        line_ids = [user.id for user in wizard.user_ids]
+        self.pool.get('change.password.user').change_password_button(cr, uid, line_ids, context=context)
+
+        if need_reload:
+            return {
+                'type': 'ir.actions.client',
+                'tag': 'reload'
+            }
+
+        return {'type': 'ir.actions.act_window_close'}
+
+class change_password_user(osv.TransientModel):
+    """
+        A model to configure users in the change password wizard
+    """
+
+    _name = 'change.password.user'
+    _description = 'Change Password Wizard User'
+    _columns = {
+        'wizard_id': fields.many2one('change.password.wizard', string='Wizard', required=True),
+        'user_id': fields.many2one('res.users', string='User', required=True),
+        'user_login': fields.char('User Login', readonly=True),
+        'new_passwd': fields.char('New Password'),
+    }
+    _defaults = {
+        'new_passwd': '',
+    }
+
+    def change_password_button(self, cr, uid, ids, context=None):
+        for line in self.browse(cr, uid, ids, context=context):
+            line.user_id.write({'password': line.new_passwd})
+        # don't keep temporary passwords in the database longer than necessary
+        self.write(cr, uid, ids, {'new_passwd': False}, context=context)
+
+
 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: