diff --git a/base_concurrency/__init__.py b/base_concurrency/__init__.py
new file mode 100644
index 000000000..364b1d0b3
--- /dev/null
+++ b/base_concurrency/__init__.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Matthieu Dietrich
+# Copyright 2015 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+from . import res_users
diff --git a/base_concurrency/__openerp__.py b/base_concurrency/__openerp__.py
new file mode 100644
index 000000000..90f08f946
--- /dev/null
+++ b/base_concurrency/__openerp__.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Matthieu Dietrich
+# Copyright 2015 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+{"name": "Base Concurrency",
+ "version": "1.0",
+ "author": "Camptocamp,Odoo Community Association (OCA)",
+ "category": "Specific Module",
+ "description": """
+Module to regroup all workarounds/fixes to avoid concurrency issues in SQL.
+
+* res.users login_date:
+the login date is now separated from res.users; on long transactions,
+"re-logging" by opening a new tab changes the current res.user row,
+which creates concurrency issues with PostgreSQL in the first transaction.
+
+This creates a new table and a function field to avoid this. In order to
+avoid breaking modules which access via SQL the login_date column, a cron
+(inactive by default) can be used to sync data.
+""",
+ "website": "http://camptocamp.com",
+ "depends": ['base'],
+ "data": ['security/ir.model.access.csv',
+ 'cron.xml'],
+ "auto_install": False,
+ "installable": True
+ }
diff --git a/base_concurrency/cron.xml b/base_concurrency/cron.xml
new file mode 100644
index 000000000..a134f7387
--- /dev/null
+++ b/base_concurrency/cron.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+ Synchronize login dates in res.users
+ 1
+
+
+ 1
+ days
+ -1
+
+
+
+
+
+
+
diff --git a/base_concurrency/res_users.py b/base_concurrency/res_users.py
new file mode 100644
index 000000000..905af8566
--- /dev/null
+++ b/base_concurrency/res_users.py
@@ -0,0 +1,137 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Matthieu Dietrich
+# Copyright 2015 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+import logging
+import psycopg2
+import openerp.exceptions
+from openerp import pooler, SUPERUSER_ID
+from openerp.osv import orm, fields
+
+_logger = logging.getLogger(__name__)
+
+
+# New class to store the login date
+class ResUsersLogin(orm.Model):
+
+ _name = 'res.users.login'
+ _columns = {
+ 'user_id': fields.many2one('res.users', 'User', required=True),
+ 'login_dt': fields.date('Latest connection'),
+ }
+
+ _sql_constraints = [
+ ('user_id_unique',
+ 'unique(user_id)',
+ 'The user can only have one login line!')
+ ]
+
+ # Cron method
+ def cron_sync_login_date(self, cr, uid, context=None):
+ # Simple SQL query to update the original login_date column.
+ try:
+ cr.execute("UPDATE res_users SET login_date = "
+ "(SELECT login_dt FROM res_users_login "
+ "WHERE res_users_login.user_id = res_users.id)")
+ cr.commit()
+ except Exception as e:
+ cr.rollback()
+ _logger.exception('Could not synchronize login dates: %s', e)
+
+ return True
+
+
+class ResUsers(orm.Model):
+
+ _inherit = 'res.users'
+
+ # Function to retrieve the login date from the res.users object
+ # (used in some functions, and the user state)
+ def _get_login_date(self, cr, uid, ids, name, args, context=None):
+ res = {}
+ user_login_obj = self.pool['res.users.login']
+ for user_id in ids:
+ login_ids = user_login_obj.search(
+ cr, uid, [('user_id', '=', user_id)], limit=1,
+ context=context)
+ if len(login_ids) == 0:
+ res[user_id] = False
+ else:
+ login = user_login_obj.browse(cr, uid, login_ids[0],
+ context=context)
+ res[user_id] = login.login_dt
+ return res
+
+ _columns = {
+ 'login_date': fields.function(_get_login_date,
+ string='Latest connection',
+ type='date', select=1,
+ readonly=True, store=False,
+ nodrop=True),
+ }
+
+ # Re-defining the login function in order to use the new table
+ def login(self, db, login, password):
+ if not password:
+ return False
+ user_id = False
+ cr = pooler.get_db(db).cursor()
+ try:
+ # check if user exists
+ res = self.search(cr, SUPERUSER_ID, [('login', '=', login)])
+ if res:
+ user_id = res[0]
+ try:
+ # check credentials
+ self.check_credentials(cr, user_id, password)
+ except openerp.exceptions.AccessDenied:
+ _logger.info("Login failed for db:%s login:%s", db, login)
+ user_id = False
+
+ if user_id:
+ try:
+ cr.execute("SELECT login_dt "
+ "FROM res_users_login "
+ "WHERE user_id=%s "
+ "FOR UPDATE NOWAIT", (user_id,),
+ log_exceptions=False)
+ # create login line if not existing
+ result = cr.fetchone()
+ if result:
+ cr.execute("UPDATE res_users_login "
+ "SET login_dt = now() "
+ "AT TIME ZONE 'UTC' "
+ "WHERE user_id=%s", (user_id,))
+ else:
+ cr.execute("INSERT INTO res_users_login "
+ "(user_id, login_dt) "
+ "VALUES (%s, now())", (user_id,))
+ cr.commit()
+ except psycopg2.OperationalError:
+ _logger.warning("Failed to update last_login "
+ "for db:%s login:%s",
+ db, login, exc_info=True)
+ cr.rollback()
+ except Exception as e:
+ _logger.exception('Login exception: %s', e)
+ user_id = False
+ finally:
+ cr.close()
+
+ return user_id
diff --git a/base_concurrency/security/ir.model.access.csv b/base_concurrency/security/ir.model.access.csv
new file mode 100644
index 000000000..922391f56
--- /dev/null
+++ b/base_concurrency/security/ir.model.access.csv
@@ -0,0 +1,2 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+"access_res_users_login_all","res_users_login all","model_res_users_login",,1,0,0,0