From 01e35141479b8dfc4a390518f89fc52e7cec5396 Mon Sep 17 00:00:00 2001
From: Xavier Morel <xmo@odoo.com>
Date: Thu, 11 May 2017 11:55:22 +0200
Subject: [PATCH] [FIX] P3: urllib, urllib2 and urlparse

In Python 3, all of these were "consolidated" under urllib(.request,
.parse, .errors) which is inconvenient.

Since we already have hard dependencies on requests and
werkzeug(.urls, which is a backport of Python 3's unicode-aware
urllib.parse) migrate *everything* to that.

A sticking point is urllib2.URLError, those were (mostly) replaced by
the slightly more general IOError which URLError extends.
---
 .../anonymization/wizard/anonymize_wizard.py  |  5 +-
 addons/auth_oauth/models/res_users.py         | 14 +---
 addons/auth_signup/models/res_partner.py      |  7 +-
 addons/base_geolocalize/models/res_partner.py |  8 +--
 .../google_account/models/google_service.py   | 67 +++++++++----------
 .../google_calendar/models/google_calendar.py | 16 +++--
 addons/google_drive/models/google_drive.py    | 42 ++++++------
 .../google_spreadsheet/models/google_drive.py | 12 ++--
 addons/link_tracker/models/link_tracker.py    | 24 +++----
 addons/mail/models/html2text.py               | 36 ++--------
 addons/mail/models/mail_template.py           | 19 +++---
 addons/mail/models/res_config.py              |  5 +-
 addons/mail/models/update.py                  | 13 ++--
 addons/mass_mailing/models/mail_mail.py       | 11 ++-
 addons/pad/models/pad.py                      | 16 +++--
 addons/pad/py_etherpad/__init__.py            | 27 ++------
 addons/payment_adyen/models/payment.py        |  7 +-
 addons/payment_adyen/tests/test_adyen.py      |  4 +-
 addons/payment_authorize/controllers/main.py  |  7 +-
 .../models/authorize_request.py               |  9 ++-
 addons/payment_authorize/models/payment.py    |  6 +-
 .../payment_authorize/tests/test_authorize.py |  6 +-
 addons/payment_buckaroo/models/payment.py     | 17 ++---
 .../payment_buckaroo/tests/test_buckaroo.py   | 10 +--
 addons/payment_ogone/models/payment.py        | 34 ++++------
 addons/payment_ogone/tests/test_ogone.py      | 11 +--
 addons/payment_paypal/controllers/main.py     | 15 +++--
 addons/payment_paypal/models/payment.py       |  8 +--
 addons/payment_paypal/tests/test_paypal.py    |  9 +--
 addons/payment_payumoney/models/payment.py    |  9 +--
 addons/payment_sips/models/payment.py         |  7 +-
 .../models/pos_mercury_transaction.py         | 11 +--
 addons/survey/models/survey.py                |  9 +--
 addons/survey/tests/test_survey.py            |  5 +-
 .../wizard/survey_email_compose_message.py    |  5 +-
 addons/web_editor/models/ir_qweb.py           | 17 ++---
 addons/web_planner/models/web_planner.py      |  5 +-
 addons/website/controllers/main.py            | 14 ++--
 addons/website/models/ir_actions.py           |  5 +-
 addons/website/models/website.py              | 12 ++--
 addons/website/tests/test_crawl.py            | 18 ++---
 addons/website_forum/controllers/main.py      |  8 ++-
 addons/website_forum/models/res_users.py      |  5 +-
 .../models/hr_recruitment.py                  |  7 +-
 .../models/mail_channel.py                    |  4 +-
 .../website_sale/controllers/website_mail.py  |  4 +-
 addons/website_slides/models/slides.py        | 27 +++-----
 .../website_twitter/models/website_twitter.py | 16 ++---
 .../models/website_twitter_config.py          | 14 ++--
 doc/_extensions/github_link.py                |  5 +-
 doc/_extensions/odoo_ext/translator.py        |  7 +-
 odoo/addons/base/ir/ir_qweb/ir_qweb.py        |  8 +--
 odoo/addons/base/module/module.py             | 18 ++---
 odoo/addons/base/res/res_partner.py           | 32 ++++-----
 odoo/http.py                                  | 13 ++--
 odoo/sql_db.py                                |  8 +--
 odoo/tests/common.py                          | 40 +++--------
 odoo/tools/misc.py                            | 24 -------
 58 files changed, 342 insertions(+), 480 deletions(-)

diff --git a/addons/anonymization/wizard/anonymize_wizard.py b/addons/anonymization/wizard/anonymize_wizard.py
index b9c35483e189..880dd34617c8 100644
--- a/addons/anonymization/wizard/anonymize_wizard.py
+++ b/addons/anonymization/wizard/anonymize_wizard.py
@@ -4,10 +4,7 @@
 import base64
 import os
 import random
-try:
-    import cPickle as pickle
-except ImportError:
-    import pickle
+import pickle
 from lxml import etree
 from operator import itemgetter
 
diff --git a/addons/auth_oauth/models/res_users.py b/addons/auth_oauth/models/res_users.py
index fc7aaca4f561..2f9bf22c6cec 100644
--- a/addons/auth_oauth/models/res_users.py
+++ b/addons/auth_oauth/models/res_users.py
@@ -1,11 +1,10 @@
 # -*- coding: utf-8 -*-
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 
-import werkzeug.urls
-import urlparse
-import urllib2
 import json
 
+import requests
+
 from odoo import api, fields, models
 from odoo.exceptions import AccessDenied
 from odoo.addons.auth_signup.models.res_users import SignupError
@@ -24,14 +23,7 @@ class ResUsers(models.Model):
 
     @api.model
     def _auth_oauth_rpc(self, endpoint, access_token):
-        params = werkzeug.url_encode({'access_token': access_token})
-        if urlparse.urlparse(endpoint)[4]:
-            url = endpoint + '&' + params
-        else:
-            url = endpoint + '?' + params
-        f = urllib2.urlopen(url)
-        response = f.read()
-        return json.loads(response)
+        return requests.get(endpoint, params={'access_token': access_token}).json()
 
     @api.model
     def _auth_oauth_validate(self, provider, access_token):
diff --git a/addons/auth_signup/models/res_partner.py b/addons/auth_signup/models/res_partner.py
index 8527ad6e33d5..d8111420a3cb 100644
--- a/addons/auth_signup/models/res_partner.py
+++ b/addons/auth_signup/models/res_partner.py
@@ -2,10 +2,9 @@
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 
 import random
-import werkzeug
+import werkzeug.urls
 
 from datetime import datetime, timedelta
-from urlparse import urljoin
 
 from odoo import api, fields, models, _
 from odoo.tools import pycompat
@@ -89,9 +88,9 @@ class ResPartner(models.Model):
                 fragment['res_id'] = res_id
 
             if fragment:
-                query['redirect'] = base + werkzeug.url_encode(fragment)
+                query['redirect'] = base + werkzeug.urls.url_encode(fragment)
 
-            res[partner.id] = urljoin(base_url, "/web/%s?%s" % (route, werkzeug.url_encode(query)))
+            res[partner.id] = werkzeug.urls.url_join(base_url, "/web/%s?%s" % (route, werkzeug.urls.url_encode(query)))
         return res
 
     @api.multi
diff --git a/addons/base_geolocalize/models/res_partner.py b/addons/base_geolocalize/models/res_partner.py
index 916f6c0d9e02..0331d2c97347 100644
--- a/addons/base_geolocalize/models/res_partner.py
+++ b/addons/base_geolocalize/models/res_partner.py
@@ -1,18 +1,18 @@
 # -*- coding: utf-8 -*-
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 import json
-import urllib
+
+import requests
 
 from odoo import api, fields, models, tools, _
 from odoo.exceptions import UserError
 
 
 def geo_find(addr):
-    url = 'https://maps.googleapis.com/maps/api/geocode/json?sensor=false&address='
-    url += urllib.quote(addr.encode('utf8'))
+    url = 'https://maps.googleapis.com/maps/api/geocode/json'
 
     try:
-        result = json.load(urllib.urlopen(url))
+        result = requests.get(url, params={'sensor': 'false', 'address': addr}).json()
     except Exception as e:
         raise UserError(_('Cannot contact geolocation servers. Please make sure that your Internet connection is up and running (%s).') % e)
 
diff --git a/addons/google_account/models/google_service.py b/addons/google_account/models/google_service.py
index 9cd47c391794..44972435da53 100644
--- a/addons/google_account/models/google_service.py
+++ b/addons/google_account/models/google_service.py
@@ -4,8 +4,9 @@
 from datetime import datetime
 import json
 import logging
-import urllib2
-import werkzeug.urls
+
+import requests
+from werkzeug import urls
 
 from odoo import api, fields, models, registry, _
 from odoo.http import request
@@ -38,27 +39,27 @@ class GoogleService(models.TransientModel):
 
         # Get the Refresh Token From Google And store it in ir.config_parameter
         headers = {"Content-type": "application/x-www-form-urlencoded"}
-        data = werkzeug.url_encode({
+        data = {
             'code': authorization_code,
             'client_id': client_id,
             'client_secret': client_secret,
             'redirect_uri': redirect_uri,
             'grant_type': "authorization_code"
-        })
+        }
         try:
-            req = urllib2.Request(GOOGLE_TOKEN_ENDPOINT, data, headers)
-            content = urllib2.urlopen(req, timeout=TIMEOUT).read()
-        except urllib2.HTTPError:
+            req = requests.post(GOOGLE_TOKEN_ENDPOINT, data=data, headers=headers, timeout=TIMEOUT)
+            req.raise_for_status()
+            content = req.json()
+        except IOError:
             error_msg = _("Something went wrong during your token generation. Maybe your Authorization Code is invalid or already expired")
             raise self.env['res.config.settings'].get_config_warning(error_msg)
 
-        content = json.loads(content)
         return content.get('refresh_token')
 
     @api.model
     def _get_google_token_uri(self, service, scope):
         get_param = self.env['ir.config_parameter'].sudo().get_param
-        encoded_params = werkzeug.url_encode({
+        encoded_params = urls.url_encode({
             'scope': scope,
             'redirect_uri': get_param('google_redirect_uri'),
             'client_id': get_param('google_%s_client_id' % service),
@@ -81,7 +82,7 @@ class GoogleService(models.TransientModel):
         base_url = get_param('web.base.url', default='http://www.odoo.com?NoBaseUrl')
         client_id = get_param('google_%s_client_id' % (service,), default=False)
 
-        encoded_params = werkzeug.url_encode({
+        encoded_params = urls.url_encode({
             'response_type': 'code',
             'client_id': client_id,
             'state': json.dumps(state),
@@ -103,17 +104,17 @@ class GoogleService(models.TransientModel):
         client_secret = get_param('google_%s_client_secret' % (service,), default=False)
 
         headers = {"content-type": "application/x-www-form-urlencoded"}
-        data = werkzeug.url_encode({
+        data = {
             'code': authorize_code,
             'client_id': client_id,
             'client_secret': client_secret,
             'grant_type': 'authorization_code',
             'redirect_uri': base_url + '/google_account/authentication'
-        })
+        }
         try:
             dummy, response, dummy = self._do_request(GOOGLE_TOKEN_ENDPOINT, params=data, headers=headers, type='POST', preuri='')
             return response
-        except urllib2.HTTPError:
+        except requests.HTTPError:
             error_msg = _("Something went wrong during your token generation. Maybe your Authorization Code is invalid")
             raise self.env['res.config.settings'].get_config_warning(error_msg)
 
@@ -125,21 +126,21 @@ class GoogleService(models.TransientModel):
         client_secret = get_param('google_%s_client_secret' % (service,), default=False)
 
         headers = {"content-type": "application/x-www-form-urlencoded"}
-        data = werkzeug.url_encode({
+        data = {
             'refresh_token': refresh_token,
             'client_id': client_id,
             'client_secret': client_secret,
             'grant_type': 'refresh_token',
-        })
+        }
 
         try:
             dummy, response, dummy = self._do_request(GOOGLE_TOKEN_ENDPOINT, params=data, headers=headers, type='POST', preuri='')
             return response
-        except urllib2.HTTPError as error:
-            if error.code == 400:  # invalid grant
+        except requests.HTTPError as error:
+            if error.response.status_code == 400:  # invalid grant
                 with registry(request.session.db).cursor() as cur:
                     self.env(cur)['res.users'].browse(self.env.uid).write({'google_%s_rtoken' % service: False})
-            error_key = json.loads(error.read()).get("error", "nc")
+            error_key = json.loads(error.response.json()).get("error", "nc")
             _logger.exception("Bad google request : %s !", error_key)
             error_msg = _("Something went wrong during your token generation. Maybe your Authorization Code is invalid or already expired [%s]") % error_key
             raise self.env['res.config.settings'].get_config_warning(error_msg)
@@ -154,40 +155,34 @@ class GoogleService(models.TransientModel):
             :param type : the method to use to make the request
             :param preuri : pre url to prepend to param uri.
         """
-        _logger.debug("Uri: %s - Type : %s - Headers: %s - Params : %s !" % (uri, type, headers, werkzeug.url_encode(params) if type == 'GET' else params))
+        _logger.debug("Uri: %s - Type : %s - Headers: %s - Params : %s !", (uri, type, headers, params))
 
-        status = 418
-        response = ""
         ask_time = fields.Datetime.now()
         try:
-            if type.upper() == 'GET' or type.upper() == 'DELETE':
-                data = werkzeug.url_encode(params)
-                req = urllib2.Request(preuri + uri + "?" + data)
-            elif type.upper() == 'POST' or type.upper() == 'PATCH' or type.upper() == 'PUT':
-                req = urllib2.Request(preuri + uri, params, headers)
+            if type.upper() in ('GET', 'DELETE'):
+                res = requests.request(type.lower(), preuri + uri, params=params, timeout=TIMEOUT)
+            elif type.upper() in ('POST', 'PATCH', 'PUT'):
+                res = requests.request(type.lower(), preuri + uri, data=params, headers=headers, timeout=TIMEOUT)
             else:
                 raise Exception(_('Method not supported [%s] not in [GET, POST, PUT, PATCH or DELETE]!') % (type))
-            req.get_method = lambda: type.upper()
-
-            resp = urllib2.urlopen(req, timeout=TIMEOUT)
-            status = resp.getcode()
+            res.raise_for_status()
+            status = res.status_code
 
             if int(status) in (204, 404):  # Page not found, no response
                 response = False
             else:
-                content = resp.read()
-                response = json.loads(content)
+                response = res.json()
 
             try:
-                ask_time = datetime.strptime(resp.headers.get('date'), "%a, %d %b %Y %H:%M:%S %Z")
+                ask_time = datetime.strptime(res.headers.get('date'), "%a, %d %b %Y %H:%M:%S %Z")
             except:
                 pass
-        except urllib2.HTTPError as error:
-            if error.code in (204, 404):
+        except request.HTTPError as error:
+            if error.response.status_code in (204, 404):
                 status = error.code
                 response = ""
             else:
-                _logger.exception("Bad google request : %s !", error.read())
+                _logger.exception("Bad google request : %s !", error.response.content)
                 if error.code in (400, 401, 410):
                     raise error
                 raise self.env['res.config.settings'].get_config_warning(_("Something went wrong with your request to google"))
diff --git a/addons/google_calendar/models/google_calendar.py b/addons/google_calendar/models/google_calendar.py
index f60ce78ef3d4..50d0e5d6d2d4 100644
--- a/addons/google_calendar/models/google_calendar.py
+++ b/addons/google_calendar/models/google_calendar.py
@@ -2,12 +2,14 @@
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 
 from datetime import datetime, timedelta
+
+import requests
 from dateutil import parser
 import json
 import logging
 import operator
 import pytz
-import urllib2
+from werkzeug import urls
 
 from odoo import api, fields, models, tools, _
 from odoo.tools import exception_to_unicode, pycompat
@@ -258,7 +260,7 @@ class GoogleCalendar(models.AbstractModel):
         """
         data = self.generate_data(event, isCreating=True)
 
-        url = "/calendar/v3/calendars/%s/events?fields=%s&access_token=%s" % ('primary', urllib2.quote('id,updated'), self.get_token())
+        url = "/calendar/v3/calendars/%s/events?fields=%s&access_token=%s" % ('primary', urls.url_quote('id,updated'), self.get_token())
         headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
         data_json = json.dumps(data)
         return self.env['google.service']._do_request(url, data_json, headers, type='POST')
@@ -289,8 +291,8 @@ class GoogleCalendar(models.AbstractModel):
 
         try:
             status, content, ask_time = self.env['google.service']._do_request(url, params, headers, type='GET')
-        except urllib2.HTTPError as e:
-            if e.code == 401:  # Token invalid / Acces unauthorized
+        except requests.HTTPError as e:
+            if e.response.status_code == 401:  # Token invalid / Acces unauthorized
                 error_msg = _("Your token is invalid or has been revoked !")
 
                 self.env.user.write({'google_calendar_token': False, 'google_calendar_token_validity': False})
@@ -674,13 +676,13 @@ class GoogleCalendar(models.AbstractModel):
         if lastSync:
             try:
                 all_event_from_google = self.get_event_synchro_dict(lastSync=lastSync)
-            except urllib2.HTTPError as e:
-                if e.code == 410:  # GONE, Google is lost.
+            except requests.HTTPError as e:
+                if e.response.code == 410:  # GONE, Google is lost.
                     # we need to force the rollback from this cursor, because it locks my res_users but I need to write in this tuple before to raise.
                     self.env.cr.rollback()
                     self.env.user.write({'google_calendar_last_sync_date': False})
                     self.env.cr.commit()
-                error_key = json.loads(str(e))
+                error_key = e.response.json()
                 error_key = error_key.get('error', {}).get('message', 'nc')
                 error_msg = _("Google is lost... the next synchro will be a full synchro. \n\n %s") % error_key
                 raise self.env['res.config.settings'].get_config_warning(error_msg)
diff --git a/addons/google_drive/models/google_drive.py b/addons/google_drive/models/google_drive.py
index b1eda64b8420..e653985f20a7 100644
--- a/addons/google_drive/models/google_drive.py
+++ b/addons/google_drive/models/google_drive.py
@@ -3,7 +3,8 @@
 import logging
 import json
 import re
-import urllib2
+
+import requests
 import werkzeug.urls
 
 from odoo import api, fields, models
@@ -62,26 +63,25 @@ class GoogleDrive(models.Model):
         google_drive_client_id = Config.get_param('google_drive_client_id')
         google_drive_client_secret = Config.get_param('google_drive_client_secret')
         #For Getting New Access Token With help of old Refresh Token
-        data = werkzeug.url_encode({
+        data = {
             'client_id': google_drive_client_id,
             'refresh_token': google_drive_refresh_token,
             'client_secret': google_drive_client_secret,
             'grant_type': "refresh_token",
             'scope': scope or 'https://www.googleapis.com/auth/drive'
-        })
+        }
         headers = {"Content-type": "application/x-www-form-urlencoded"}
         try:
-            req = urllib2.Request(GOOGLE_TOKEN_ENDPOINT, data, headers)
-            content = urllib2.urlopen(req, timeout=TIMEOUT).read()
-        except urllib2.HTTPError:
+            req = requests.post(GOOGLE_TOKEN_ENDPOINT, data=data, headers=headers, timeout=TIMEOUT)
+            req.raise_for_status()
+        except requests.HTTPError:
             if user_is_admin:
                 dummy, action_id = self.env['ir.model.data'].get_object_reference('base_setup', 'action_general_configuration')
                 msg = _("Something went wrong during the token generation. Please request again an authorization code .")
                 raise RedirectWarning(msg, action_id, _('Go to the configuration panel'))
             else:
                 raise UserError(_("Google Drive is not yet configured. Please contact your administrator."))
-        content = json.loads(content)
-        return content.get('access_token')
+        return req.json().get('access_token')
 
     @api.model
     def copy_doc(self, res_id, template_id, name_gdocs, res_model):
@@ -91,11 +91,11 @@ class GoogleDrive(models.Model):
         request_url = "https://www.googleapis.com/drive/v2/files/%s?fields=parents/id&access_token=%s" % (template_id, access_token)
         headers = {"Content-type": "application/x-www-form-urlencoded"}
         try:
-            req = urllib2.Request(request_url, None, headers)
-            parents = urllib2.urlopen(req, timeout=TIMEOUT).read()
-        except urllib2.HTTPError:
+            req = requests.post(request_url, headers=headers, timeout=TIMEOUT)
+            req.raise_for_status()
+            parents_dict = req.json()
+        except requests.HTTPError:
             raise UserError(_("The Google Template cannot be found. Maybe it has been deleted."))
-        parents_dict = json.loads(parents)
 
         record_url = "Click on link to open Record in Odoo\n %s/?db=%s#id=%s&model=%s" % (google_web_base_url, self._cr.dbname, res_id, res_model)
         data = {
@@ -108,11 +108,10 @@ class GoogleDrive(models.Model):
             'Content-type': 'application/json',
             'Accept': 'text/plain'
         }
-        data_json = json.dumps(data)
         # resp, content = Http().request(request_url, "POST", data_json, headers)
-        req = urllib2.Request(request_url, data_json, headers)
-        content = urllib2.urlopen(req, timeout=TIMEOUT).read()
-        content = json.loads(content)
+        req = requests.post(request_url, data=json.dumps(data), headers=headers, timeout=TIMEOUT)
+        req.raise_for_status()
+        content = req.json()
         res = {}
         if content.get('alternateLink'):
             res['id'] = self.env["ir.attachment"].create({
@@ -129,16 +128,15 @@ class GoogleDrive(models.Model):
             request_url = "https://www.googleapis.com/drive/v2/files/%s/permissions?emailMessage=This+is+a+drive+file+created+by+Odoo&sendNotificationEmails=false&access_token=%s" % (key, access_token)
             data = {'role': 'writer', 'type': 'anyone', 'value': '', 'withLink': True}
             try:
-                req = urllib2.Request(request_url, json.dumps(data), headers)
-                urllib2.urlopen(req, timeout=TIMEOUT)
-            except urllib2.HTTPError:
+                req = requests.post(request_url, data=json.dumps(data), headers=headers, timeout=TIMEOUT)
+                req.raise_for_status()
+            except requests.HTTPError:
                 raise self.env['res.config.settings'].get_config_warning(_("The permission 'reader' for 'anyone with the link' has not been written on the document"))
             if self.env.user.email:
                 data = {'role': 'writer', 'type': 'user', 'value': self.env.user.email}
                 try:
-                    req = urllib2.Request(request_url, json.dumps(data), headers)
-                    urllib2.urlopen(req, timeout=TIMEOUT)
-                except urllib2.HTTPError:
+                    requests.post(request_url, data=json.dumps(data), headers=headers, timeout=TIMEOUT)
+                except requests.HTTPError:
                     pass
         return res
 
diff --git a/addons/google_spreadsheet/models/google_drive.py b/addons/google_spreadsheet/models/google_drive.py
index 88b75a65ca38..dce4777a25e0 100644
--- a/addons/google_spreadsheet/models/google_drive.py
+++ b/addons/google_spreadsheet/models/google_drive.py
@@ -3,10 +3,11 @@
 import cgi
 import json
 import logging
+
+import requests
 from lxml import etree
 import re
 import werkzeug.urls
-import urllib2
 
 from odoo import api, models
 from odoo.addons.google_account import TIMEOUT
@@ -72,12 +73,13 @@ class GoogleDrive(models.Model):
 </feed>''' .format(key=spreadsheet_key, formula=cgi.escape(formula, quote=True), config=cgi.escape(config_formula, quote=True))
 
         try:
-            req = urllib2.Request(
+            req = requests.post(
                 'https://spreadsheets.google.com/feeds/cells/%s/od6/private/full/batch?%s' % (spreadsheet_key, werkzeug.url_encode({'v': 3, 'access_token': access_token})),
                 data=request,
-                headers={'content-type': 'application/atom+xml', 'If-Match': '*'})
-            urllib2.urlopen(req, timeout=TIMEOUT)
-        except (urllib2.HTTPError, urllib2.URLError):
+                headers={'content-type': 'application/atom+xml', 'If-Match': '*'},
+                timeout=TIMEOUT,
+            )
+        except IOError:
             _logger.warning("An error occured while writting the formula on the Google Spreadsheet.")
 
         description = '''
diff --git a/addons/link_tracker/models/link_tracker.py b/addons/link_tracker/models/link_tracker.py
index 12dcd46c1401..1d9a7cdf5610 100644
--- a/addons/link_tracker/models/link_tracker.py
+++ b/addons/link_tracker/models/link_tracker.py
@@ -6,11 +6,10 @@ import random
 import re
 import string
 
+import requests
 from lxml import html
-from urllib2 import urlopen
-from urlparse import urljoin
-from urlparse import urlparse
-from werkzeug import url_encode, unescape
+from werkzeug import urls, utils
+
 
 from odoo import models, fields, api, _
 from odoo.tools import ustr, pycompat
@@ -18,7 +17,7 @@ from odoo.tools import ustr, pycompat
 URL_REGEX = r'(\bhref=[\'"](?!mailto:)([^\'"]+)[\'"])'
 
 def VALIDATE_URL(url):
-    if urlparse(url).scheme not in ('http', 'https', 'ftp', 'ftps'):
+    if urls.url_parse(url).scheme not in ('http', 'https', 'ftp', 'ftps'):
         return 'http://' + url
 
     return url
@@ -56,7 +55,7 @@ class link_tracker(models.Model):
             href = match[0]
             long_url = match[1]
 
-            vals['url'] = unescape(long_url)
+            vals['url'] = utils.unescape(long_url)
 
             if not blacklist or not [s for s in blacklist if s in long_url] and not long_url.startswith(short_schema):
                 link = self.create(vals)
@@ -77,7 +76,7 @@ class link_tracker(models.Model):
     @api.depends('code')
     def _compute_short_url(self):
         base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
-        self.short_url = urljoin(base_url, '/r/%(code)s' % {'code': self.code})
+        self.short_url = urls.url_join(base_url, '/r/%(code)s' % {'code': self.code})
 
     @api.one
     def _compute_short_url_host(self):
@@ -96,22 +95,23 @@ class link_tracker(models.Model):
     @api.one
     @api.depends('url')
     def _compute_redirected_url(self):
-        parsed = urlparse(self.url)
+        parsed = urls.url_parse(self.url)
 
         utms = {}
         for key, field, cook in self.env['utm.mixin'].tracking_fields():
             attr = getattr(self, field).name
             if attr:
                 utms[key] = attr
+        utms.update(parsed.decode_query())
 
-        self.redirected_url = '%s://%s%s?%s&%s#%s' % (parsed.scheme, parsed.netloc, parsed.path, url_encode(utms), parsed.query, parsed.fragment)
+        self.redirected_url = parsed.replace(query=urls.url_encode(utms)).to_url()
 
     @api.model
     @api.depends('url')
     def _get_title_from_url(self, url):
         try:
-            page = urlopen(url, timeout=5)
-            p = html.fromstring(ustr(page.read()).encode('utf-8'), parser=html.HTMLParser(encoding='utf-8'))
+            page = requests.get(url, timeout=5)
+            p = html.fromstring(page.text.encode('utf-8'), parser=html.HTMLParser(encoding='utf-8'))
             title = p.find('.//title').text
         except:
             title = url
@@ -122,7 +122,7 @@ class link_tracker(models.Model):
     @api.depends('url')
     def _compute_favicon(self):
         try:
-            icon = urlopen('http://www.google.com/s2/favicons?domain=' + self.url, timeout=5).read()
+            icon = requests.get('http://www.google.com/s2/favicons', params={'domain': self.url}, timeout=5).content
             icon_base64 = icon.encode('base64').replace("\n", "")
         except:
             icon_base64 = 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsSAAALEgHS3X78AAACiElEQVQ4EaVTzU8TURCf2tJuS7tQtlRb6UKBIkQwkRRSEzkQgyEc6lkOKgcOph78Y+CgjXjDs2i44FXY9AMTlQRUELZapVlouy3d7kKtb0Zr0MSLTvL2zb75eL838xtTvV6H/xELBptMJojeXLCXyobnyog4YhzXYvmCFi6qVSfaeRdXdrfaU1areV5KykmX06rcvzumjY/1ggkR3Jh+bNf1mr8v1D5bLuvR3qDgFbvbBJYIrE1mCIoCrKxsHuzK+Rzvsi29+6DEbTZz9unijEYI8ObBgXOzlcrx9OAlXyDYKUCzwwrDQx1wVDGg089Dt+gR3mxmhcUnaWeoxwMbm/vzDFzmDEKMMNhquRqduT1KwXiGt0vre6iSeAUHNDE0d26NBtAXY9BACQyjFusKuL2Ry+IPb/Y9ZglwuVscdHaknUChqLF/O4jn3V5dP4mhgRJgwSYm+gV0Oi3XrvYB30yvhGa7BS70eGFHPoTJyQHhMK+F0ZesRVVznvXw5Ixv7/C10moEo6OZXbWvlFAF9FVZDOqEABUMRIkMd8GnLwVWg9/RkJF9sA4oDfYQAuzzjqzwvnaRUFxn/X2ZlmGLXAE7AL52B4xHgqAUqrC1nSNuoJkQtLkdqReszz/9aRvq90NOKdOS1nch8TpL555WDp49f3uAMXhACRjD5j4ykuCtf5PP7Fm1b0DIsl/VHGezzP1KwOiZQobFF9YyjSRYQETRENSlVzI8iK9mWlzckpSSCQHVALmN9Az1euDho9Xo8vKGd2rqooA8yBcrwHgCqYR0kMkWci08t/R+W4ljDCanWTg9TJGwGNaNk3vYZ7VUdeKsYJGFNkfSzjXNrSX20s4/h6kB81/271ghG17l+rPTAAAAAElFTkSuQmCC'
diff --git a/addons/mail/models/html2text.py b/addons/mail/models/html2text.py
index 5e2fb25150fa..8830cb0266d2 100755
--- a/addons/mail/models/html2text.py
+++ b/addons/mail/models/html2text.py
@@ -1,5 +1,7 @@
 #!/usr/bin/env python
 """html2text: Turn HTML into equivalent Markdown-structured text."""
+from werkzeug import urls
+
 from odoo.tools import pycompat
 
 __version__ = "2.36"
@@ -10,9 +12,8 @@ __contributors__ = ["Martin 'Joey' Schulze", "Ricardo Reyes", "Kevin Jay North"]
 # TODO:
 #   Support decoded entities with unifiable.
 
-import re, sys, urllib, htmlentitydefs, codecs
+import re, sys, htmlentitydefs, codecs
 import sgmllib
-import urlparse
 sgmllib.charref = re.compile('&#([xX]?[0-9a-fA-F]+)[^0-9a-fA-F]')
 
 try: from textwrap import wrap
@@ -390,7 +391,7 @@ class _html2text(sgmllib.SGMLParser):
                 newa = []
                 for link in self.a:
                     if self.outcount > link['outcount']:
-                        self.out("   ["+repr(link['count'])+"]: " + urlparse.urljoin(self.baseurl, link['href']))
+                        self.out("   ["+repr(link['count'])+"]: " + urls.url_join(self.baseurl, link['href']))
                         if 'title' in link: self.out(" ("+link['title']+")")
                         self.out("\n")
                     else:
@@ -426,32 +427,3 @@ def html2text_file(html, out=wrapwrite, baseurl=''):
 
 def html2text(html, baseurl=''):
     return optwrap(html2text_file(html, None, baseurl))
-
-if __name__ == "__main__":
-    baseurl = ''
-    if sys.argv[1:]:
-        arg = sys.argv[1]
-        if arg.startswith('http://'):
-            baseurl = arg
-            j = urllib.urlopen(baseurl)
-            try:
-                from feedparser import _getCharacterEncoding as enc
-            except ImportError:
-                   enc = lambda x, y: ('utf-8', 1)
-            text = j.read()
-            encoding = enc(j.headers, text)[0]
-            if encoding == 'us-ascii': encoding = 'utf-8'
-            data = text.decode(encoding)
-
-        else:
-            encoding = 'utf8'
-            if len(sys.argv) > 2:
-                encoding = sys.argv[2]
-            f = open(arg, 'r')
-            try:
-                    data = f.read().decode(encoding)
-            finally:
-                    f.close()
-    else:
-        data = sys.stdin.read().decode('utf8')
-    wrapwrite(html2text(data, baseurl))
diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py
index ff89ca2d413d..be73a374a452 100644
--- a/addons/mail/models/mail_template.py
+++ b/addons/mail/models/mail_template.py
@@ -10,9 +10,7 @@ import logging
 
 import functools
 import lxml
-import urlparse
-
-from urllib import urlencode, quote as quote
+from werkzeug import urls
 
 from odoo import _, api, fields, models, tools
 from odoo.exceptions import UserError
@@ -83,8 +81,8 @@ try:
     )
     mako_template_env.globals.update({
         'str': str,
-        'quote': quote,
-        'urlencode': urlencode,
+        'quote': urls.url_quote,
+        'urlencode': urls.url_encode,
         'datetime': datetime,
         'len': len,
         'abs': abs,
@@ -297,14 +295,13 @@ class MailTemplate(models.Model):
             root = lxml.html.fromstring(html)
 
         base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
-        (base_scheme, base_netloc, bpath, bparams, bquery, bfragment) = urlparse.urlparse(base_url)
+        base = urls.url_parse(base_url)
 
         def _process_link(url):
-            new_url = url
-            (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url)
-            if not scheme and not netloc:
-                new_url = urlparse.urlunparse((base_scheme, base_netloc, path, params, query, fragment))
-            return new_url
+            new_url = urls.url_parse(url)
+            if new_url.scheme and new_url.netloc:
+                return url
+            return new_url.replace(scheme=base.scheme, netloc=base.netloc).to_url()
 
         # check all nodes, replace :
         # - img src -> check URL
diff --git a/addons/mail/models/res_config.py b/addons/mail/models/res_config.py
index d34a6663a4f6..c0e4f6bab62f 100644
--- a/addons/mail/models/res_config.py
+++ b/addons/mail/models/res_config.py
@@ -1,9 +1,10 @@
 # -*- coding: utf-8 -*-
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 
-import urlparse
 import datetime
 
+from werkzeug import urls
+
 from odoo import api, fields, models, tools
 
 
@@ -29,7 +30,7 @@ class BaseConfiguration(models.TransientModel):
         if alias_domain is None:
             domain = self.env["ir.config_parameter"].get_param("web.base.url")
             try:
-                alias_domain = urlparse.urlsplit(domain).netloc.split(':')[0]
+                alias_domain = urls.url_parse(domain).host
             except Exception:
                 pass
         return {'alias_domain': alias_domain or False}
diff --git a/addons/mail/models/update.py b/addons/mail/models/update.py
index 296bbc32ee1c..accbc120f138 100644
--- a/addons/mail/models/update.py
+++ b/addons/mail/models/update.py
@@ -3,8 +3,9 @@
 
 import datetime
 import logging
+
+import requests
 import werkzeug.urls
-import urllib2
 
 from ast import literal_eval
 
@@ -71,16 +72,12 @@ class PublisherWarrantyContract(AbstractModel):
         """
         msg = self._get_message()
         arguments = {'arg0': msg, "action": "update"}
-        arguments_raw = werkzeug.urls.url_encode(arguments)
 
         url = config.get("publisher_warranty_url")
 
-        uo = urllib2.urlopen(url, arguments_raw, timeout=30)
-        try:
-            submit_result = uo.read()
-            return literal_eval(submit_result)
-        finally:
-            uo.close()
+        r = requests.post(url, data=arguments, timeout=30)
+        r.raise_for_status()
+        return literal_eval(r.text)
 
     @api.multi
     def update_notification(self, cron_mode=True):
diff --git a/addons/mass_mailing/models/mail_mail.py b/addons/mass_mailing/models/mail_mail.py
index 3a69530db1a9..1772cdaf1ff1 100644
--- a/addons/mass_mailing/models/mail_mail.py
+++ b/addons/mass_mailing/models/mail_mail.py
@@ -2,7 +2,6 @@
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 
 import re
-import urlparse
 import werkzeug.urls
 
 from odoo import api, fields, models, tools
@@ -29,20 +28,20 @@ class MailMail(models.Model):
 
     def _get_tracking_url(self, partner=None):
         base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
-        track_url = urlparse.urljoin(
+        track_url = werkzeug.urls.url_join(
             base_url, 'mail/track/%(mail_id)s/blank.gif?%(params)s' % {
                 'mail_id': self.id,
-                'params': werkzeug.url_encode({'db': self.env.cr.dbname})
+                'params': werkzeug.urls.url_encode({'db': self.env.cr.dbname})
             }
         )
         return '<img src="%s" alt=""/>' % track_url
 
     def _get_unsubscribe_url(self, email_to):
         base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
-        url = urlparse.urljoin(
+        url = werkzeug.urls.url_join(
             base_url, 'mail/mailing/%(mailing_id)s/unsubscribe?%(params)s' % {
                 'mailing_id': self.mailing_id.id,
-                'params': werkzeug.url_encode({
+                'params': werkzeug.urls.url_encode({
                     'db': self.env.cr.dbname,
                     'res_id': self.res_id,
                     'email': email_to,
@@ -79,7 +78,7 @@ class MailMail(models.Model):
         body = tools.append_content_to_html(base, body, plaintext=False, container_tag='div')
         # resolve relative image url to absolute for outlook.com
         def _sub_relative2absolute(match):
-            return match.group(1) + urlparse.urljoin(domain, match.group(2))
+            return match.group(1) + werkzeug.urls.url_join(domain, match.group(2))
         body = re.sub('(<img(?=\s)[^>]*\ssrc=")(/[^/][^"]+)', _sub_relative2absolute, body)
         body = re.sub(r'(<[^>]+\bstyle="[^"]+\burl\(\'?)(/[^/\'][^\'")]+)', _sub_relative2absolute, body)
 
diff --git a/addons/pad/models/pad.py b/addons/pad/models/pad.py
index 1c8f08e79bd3..8af18fa1b60e 100644
--- a/addons/pad/models/pad.py
+++ b/addons/pad/models/pad.py
@@ -5,7 +5,8 @@ import logging
 import random
 import re
 import string
-import urllib2
+
+import requests
 
 from odoo import api, models, _
 from odoo.exceptions import UserError
@@ -53,7 +54,7 @@ class PadCommon(models.AbstractModel):
             myPad = EtherpadLiteClient(pad["key"], pad["server"] + '/api')
             try:
                 myPad.createPad(path)
-            except urllib2.URLError:
+            except IOError:
                 raise UserError(_("Pad creation failed, either there is a problem with your pad server URL or with your connection."))
 
             # get attr on the field model
@@ -79,12 +80,15 @@ class PadCommon(models.AbstractModel):
         content = ''
         if url:
             try:
-                page = urllib2.urlopen('%s/export/html' % url).read()
-                mo = re.search('<body>(.*)</body>', page, re.DOTALL)
-                if mo:
-                    content = mo.group(1)
+                r = requests.get('%s/export/html' % url)
+                r.raise_for_status()
             except:
                 _logger.warning("No url found '%s'.", url)
+            else:
+                mo = re.search('<body>(.*)</body>', r.content, re.DOTALL)
+                if mo:
+                    content = mo.group(1)
+
         return content
 
     # TODO
diff --git a/addons/pad/py_etherpad/__init__.py b/addons/pad/py_etherpad/__init__.py
index 0db607fe16f1..d1040cec852c 100644
--- a/addons/pad/py_etherpad/__init__.py
+++ b/addons/pad/py_etherpad/__init__.py
@@ -1,8 +1,5 @@
 """Module to talk to EtherpadLite API."""
-
-import json
-import urllib
-import urllib2
+import requests
 
 
 class EtherpadLiteClient:
@@ -31,23 +28,11 @@ class EtherpadLiteClient:
         url = '%s/%d/%s' % (self.baseUrl, self.API_VERSION, function)
 
         params = arguments or {}
-        params.update({'apikey': self.apiKey})
-        data = urllib.urlencode(params, True)
-
-        try:
-            opener = urllib2.build_opener()
-            request = urllib2.Request(url=url, data=data)
-            response = opener.open(request, timeout=self.TIMEOUT)
-            result = response.read()
-            response.close()
-        except urllib2.HTTPError:
-            raise
-
-        result = json.loads(result)
-        if result is None:
-            raise ValueError("JSON response could not be decoded")
-
-        return self.handleResult(result)
+        params['apikey'] = self.apiKey
+
+        r = requests.get(url, data=params, timeout=self.TIMEOUT)
+        r.raise_for_status()
+        return self.handleResult(r.json())
 
     def handleResult(self, result):
         """Handle API call result"""
diff --git a/addons/payment_adyen/models/payment.py b/addons/payment_adyen/models/payment.py
index 8b2d8944813e..87e51e619cbd 100644
--- a/addons/payment_adyen/models/payment.py
+++ b/addons/payment_adyen/models/payment.py
@@ -7,9 +7,10 @@ from collections import OrderedDict
 import hashlib
 import hmac
 import logging
-import urlparse
 from itertools import chain
 
+from werkzeug import urls
+
 from odoo import api, fields, models, tools, _
 from odoo.addons.payment.models.payment_acquirer import ValidationError
 from odoo.addons.payment_adyen.controllers.main import AdyenController
@@ -132,7 +133,7 @@ class AcquirerAdyen(models.Model):
                 'merchantAccount': self.adyen_merchant_account,
                 'shopperLocale': values.get('partner_lang', ''),
                 'sessionValidity': tmp_date.isoformat('T')[:19] + "Z",
-                'resURL': '%s' % urlparse.urljoin(base_url, AdyenController._return_url),
+                'resURL': urls.url_join(base_url, AdyenController._return_url),
                 'merchantReturnData': json.dumps({'return_url': '%s' % values.pop('return_url')}) if values.get('return_url', '') else False,
                 'shopperEmail': values.get('partner_email', ''),
             })
@@ -150,7 +151,7 @@ class AcquirerAdyen(models.Model):
                 'merchantAccount': self.adyen_merchant_account,
                 'shopperLocale': values.get('partner_lang'),
                 'sessionValidity': tmp_date,
-                'resURL': '%s' % urlparse.urljoin(base_url, AdyenController._return_url),
+                'resURL': urls.url_join(base_url, AdyenController._return_url),
                 'merchantReturnData': json.dumps({'return_url': '%s' % values.pop('return_url')}) if values.get('return_url') else False,
                 'merchantSig': self._adyen_generate_merchant_sig('in', values),
             })
diff --git a/addons/payment_adyen/tests/test_adyen.py b/addons/payment_adyen/tests/test_adyen.py
index 2015df6922ee..2e1d3eb53f2c 100644
--- a/addons/payment_adyen/tests/test_adyen.py
+++ b/addons/payment_adyen/tests/test_adyen.py
@@ -1,10 +1,10 @@
 # -*- coding: utf-8 -*-
 
 from lxml import objectify
-import urlparse
 
 from odoo.addons.payment.tests.common import PaymentAcquirerCommon
 from odoo.addons.payment_adyen.controllers.main import AdyenController
+from werkzeug import urls
 
 
 class AdyenCommon(PaymentAcquirerCommon):
@@ -47,7 +47,7 @@ class AdyenForm(AdyenCommon):
             'skinCode': 'cbqYWvVL',
             'paymentAmount': '1',
             'currencyCode': 'EUR',
-            'resURL': '%s' % urlparse.urljoin(base_url, AdyenController._return_url),
+            'resURL': urls.url_join(base_url, AdyenController._return_url),
         }
 
         # render the button
diff --git a/addons/payment_authorize/controllers/main.py b/addons/payment_authorize/controllers/main.py
index d88a74554b92..b42efe817bb3 100644
--- a/addons/payment_authorize/controllers/main.py
+++ b/addons/payment_authorize/controllers/main.py
@@ -1,8 +1,7 @@
 # -*- coding: utf-8 -*-
 import pprint
 import logging
-import urlparse
-import werkzeug
+from werkzeug import urls, utils
 
 from odoo import http
 from odoo.http import request
@@ -29,7 +28,7 @@ class AuthorizeController(http.Controller):
         # This response is in the form of a URL that Authorize.Net will pass on to the
         # client's browser to redirect them to the desired location need javascript.
         return request.render('payment_authorize.payment_authorize_redirect', {
-            'return_url': '%s' % urlparse.urljoin(base_url, return_url)
+            'return_url': urls.url_join(base_url, return_url)
         })
 
     @http.route(['/payment/authorize/s2s/create_json'], type='json', auth='public')
@@ -43,4 +42,4 @@ class AuthorizeController(http.Controller):
         acquirer_id = int(post.get('acquirer_id'))
         acquirer = request.env['payment.acquirer'].browse(acquirer_id)
         acquirer.s2s_process(post)
-        return werkzeug.utils.redirect(post.get('return_url', '/'))
+        return utils.redirect(post.get('return_url', '/'))
diff --git a/addons/payment_authorize/models/authorize_request.py b/addons/payment_authorize/models/authorize_request.py
index 75a54d935266..21716ec558f4 100644
--- a/addons/payment_authorize/models/authorize_request.py
+++ b/addons/payment_authorize/models/authorize_request.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
+import requests
 from lxml import etree, objectify
-from urllib2 import urlopen, Request
 from StringIO import StringIO
 import xml.etree.ElementTree as ET
 from uuid import uuid4
@@ -57,10 +57,9 @@ class AuthorizeAPI():
         :param etree._Element data: etree data to process
         """
         data = etree.tostring(data, xml_declaration=True, encoding='utf-8')
-        request = Request(self.url, data)
-        request.add_header('Content-Type', 'text/xml')
-        response = urlopen(request).read()
-        response = strip_ns(response, XMLNS)
+        r = requests.post(self.url, data=data, headers={'Content-Type': 'text/xml'})
+        r.raise_for_status()
+        response = strip_ns(r.content, XMLNS)
         if response.find('messages/resultCode').text == 'Error':
             messages = [m.text for m in response.findall('messages/message/text')]
             raise ValidationError('Authorize.net Error Message(s):\n %s' % '\n'.join(messages))
diff --git a/addons/payment_authorize/models/payment.py b/addons/payment_authorize/models/payment.py
index 12f5b353062c..9134549b3933 100644
--- a/addons/payment_authorize/models/payment.py
+++ b/addons/payment_authorize/models/payment.py
@@ -1,4 +1,5 @@
 # coding: utf-8
+from werkzeug import urls
 
 from .authorize_request import AuthorizeAPI
 from datetime import datetime
@@ -6,7 +7,6 @@ import hashlib
 import hmac
 import logging
 import time
-import urlparse
 
 from odoo import api, fields, models
 from odoo.addons.payment.models.payment_acquirer import ValidationError
@@ -72,8 +72,8 @@ class PaymentAcquirerAuthorize(models.Model):
             'x_version': '3.1',
             'x_relay_response': 'TRUE',
             'x_fp_timestamp': str(int(time.time())),
-            'x_relay_url': '%s' % urlparse.urljoin(base_url, AuthorizeController._return_url),
-            'x_cancel_url': '%s' % urlparse.urljoin(base_url, AuthorizeController._cancel_url),
+            'x_relay_url': urls.url_join(base_url, AuthorizeController._return_url),
+            'x_cancel_url': urls.url_join(base_url, AuthorizeController._cancel_url),
             'x_currency_code': values['currency'] and values['currency'].name or '',
             'address': values.get('partner_address'),
             'city': values.get('partner_city'),
diff --git a/addons/payment_authorize/tests/test_authorize.py b/addons/payment_authorize/tests/test_authorize.py
index c16113cd52b3..d19690284a8a 100644
--- a/addons/payment_authorize/tests/test_authorize.py
+++ b/addons/payment_authorize/tests/test_authorize.py
@@ -3,9 +3,9 @@
 import hashlib
 import hmac
 import time
-import urlparse
 import unittest
 from lxml import objectify
+from werkzeug import urls
 
 import odoo
 from odoo.addons.payment.models.payment_acquirer import ValidationError
@@ -59,8 +59,8 @@ class AuthorizeForm(AuthorizeCommon):
             'x_version': '3.1',
             'x_relay_response': 'TRUE',
             'x_fp_timestamp': str(int(time.time())),
-            'x_relay_url': '%s' % urlparse.urljoin(base_url, AuthorizeController._return_url),
-            'x_cancel_url': '%s' % urlparse.urljoin(base_url, AuthorizeController._cancel_url),
+            'x_relay_url': urls.url_join(base_url, AuthorizeController._return_url),
+            'x_cancel_url': urls.url_join(base_url, AuthorizeController._cancel_url),
             'return_url': None,
             'x_currency_code': 'USD',
             'x_invoice_num': 'SO004',
diff --git a/addons/payment_buckaroo/models/payment.py b/addons/payment_buckaroo/models/payment.py
index e46f078c1348..a07ca83318da 100644
--- a/addons/payment_buckaroo/models/payment.py
+++ b/addons/payment_buckaroo/models/payment.py
@@ -1,8 +1,8 @@
 # coding: utf-8
 from hashlib import sha1
 import logging
-import urllib
-import urlparse
+
+from werkzeug import urls
 
 from odoo import api, fields, models, _
 from odoo.addons.payment.models.payment_acquirer import ValidationError
@@ -74,14 +74,11 @@ class AcquirerBuckaroo(models.Model):
                     break
 
             items = sorted(pycompat.items(values), key=lambda pair: pair[0].lower())
-            sign = ''.join('%s=%s' % (k, urllib.unquote_plus(v)) for k, v in items)
+            sign = ''.join('%s=%s' % (k, urls.url_unquote_plus(v)) for k, v in items)
         else:
             sign = ''.join('%s=%s' % (k, get_value(k)) for k in keys)
         # Add the pre-shared secret key at the end of the signature
         sign = sign + self.brq_secretkey
-        if isinstance(sign, str):
-            # TODO: remove me? should not be used
-            sign = urlparse.parse_qsl(sign)
         shasign = sha1(sign.encode('utf-8')).hexdigest()
         return shasign
 
@@ -95,10 +92,10 @@ class AcquirerBuckaroo(models.Model):
             'Brq_currency': values['currency'] and values['currency'].name or '',
             'Brq_invoicenumber': values['reference'],
             'brq_test': False if self.environment == 'prod' else True,
-            'Brq_return': '%s' % urlparse.urljoin(base_url, BuckarooController._return_url),
-            'Brq_returncancel': '%s' % urlparse.urljoin(base_url, BuckarooController._cancel_url),
-            'Brq_returnerror': '%s' % urlparse.urljoin(base_url, BuckarooController._exception_url),
-            'Brq_returnreject': '%s' % urlparse.urljoin(base_url, BuckarooController._reject_url),
+            'Brq_return': urls.url_join(base_url, BuckarooController._return_url),
+            'Brq_returncancel': urls.url_join(base_url, BuckarooController._cancel_url),
+            'Brq_returnerror': urls.url_join(base_url, BuckarooController._exception_url),
+            'Brq_returnreject': urls.url_join(base_url, BuckarooController._reject_url),
             'Brq_culture': (values.get('partner_lang') or 'en_US').replace('_', '-'),
             'add_returndata': buckaroo_tx_values.pop('return_url', '') or '',
         })
diff --git a/addons/payment_buckaroo/tests/test_buckaroo.py b/addons/payment_buckaroo/tests/test_buckaroo.py
index 016f8ec193ea..0825f67e8755 100644
--- a/addons/payment_buckaroo/tests/test_buckaroo.py
+++ b/addons/payment_buckaroo/tests/test_buckaroo.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
 from lxml import objectify
-import urlparse
+from werkzeug import urls
 
 import odoo
 from odoo.addons.payment.models.payment_acquirer import ValidationError
@@ -41,10 +41,10 @@ class BuckarooForm(BuckarooCommon):
             'Brq_invoicenumber': 'SO004',
             'Brq_signature': '1b8c10074c622d965272a91a9e88b5b3777d2474',  # update me
             'brq_test': 'True',
-            'Brq_return': '%s' % urlparse.urljoin(base_url, BuckarooController._return_url),
-            'Brq_returncancel': '%s' % urlparse.urljoin(base_url, BuckarooController._cancel_url),
-            'Brq_returnerror': '%s' % urlparse.urljoin(base_url, BuckarooController._exception_url),
-            'Brq_returnreject': '%s' % urlparse.urljoin(base_url, BuckarooController._reject_url),
+            'Brq_return': urls.url_join(base_url, BuckarooController._return_url),
+            'Brq_returncancel': urls.url_join(base_url, BuckarooController._cancel_url),
+            'Brq_returnerror': urls.url_join(base_url, BuckarooController._exception_url),
+            'Brq_returnreject': urls.url_join(base_url, BuckarooController._reject_url),
             'Brq_culture': 'en-US',
         }
 
diff --git a/addons/payment_ogone/models/payment.py b/addons/payment_ogone/models/payment.py
index 34cf982c82ed..559f119fbbd5 100644
--- a/addons/payment_ogone/models/payment.py
+++ b/addons/payment_ogone/models/payment.py
@@ -1,22 +1,20 @@
 # coding: utf-8
-
+import datetime
+import logging
+import time
 from hashlib import sha1
-from lxml import etree, objectify
 from pprint import pformat
 from unicodedata import normalize
-from urllib import urlencode
 
-import datetime
-import logging
-import time
-import urllib2
-import urlparse
+import requests
+from lxml import etree, objectify
+from werkzeug import urls
 
 from odoo import api, fields, models, _
 from odoo.addons.payment.models.payment_acquirer import ValidationError
 from odoo.addons.payment_ogone.controllers.main import OgoneController
 from odoo.addons.payment_ogone.data import ogone
-from odoo.tools import float_round, DEFAULT_SERVER_DATE_FORMAT
+from odoo.tools import float_round, DEFAULT_SERVER_DATE_FORMAT, pycompat
 from odoo.tools.float_utils import float_compare, float_repr
 from odoo.tools.safe_eval import safe_eval
 
@@ -165,10 +163,10 @@ class PaymentAcquirerOgone(models.Model):
             'OWNERTOWN': values.get('partner_city'),
             'OWNERCTY': values.get('partner_country') and values.get('partner_country').code or '',
             'OWNERTELNO': values.get('partner_phone'),
-            'ACCEPTURL': '%s' % urlparse.urljoin(base_url, OgoneController._accept_url),
-            'DECLINEURL': '%s' % urlparse.urljoin(base_url, OgoneController._decline_url),
-            'EXCEPTIONURL': '%s' % urlparse.urljoin(base_url, OgoneController._exception_url),
-            'CANCELURL': '%s' % urlparse.urljoin(base_url, OgoneController._cancel_url),
+            'ACCEPTURL': urls.url_join(base_url, OgoneController._accept_url),
+            'DECLINEURL': urls.url_join(base_url, OgoneController._decline_url),
+            'EXCEPTIONURL': urls.url_join(base_url, OgoneController._exception_url),
+            'CANCELURL': urls.url_join(base_url, OgoneController._cancel_url),
             'PARAMPLUS': 'return_url=%s' % ogone_tx_values.pop('return_url') if ogone_tx_values.get('return_url') else False,
         }
         if self.save_token in ['ask', 'always']:
@@ -370,8 +368,7 @@ class PaymentTxOgone(models.Model):
         direct_order_url = 'https://secure.ogone.com/ncol/%s/orderdirect.asp' % (self.acquirer_id.environment)
 
         _logger.debug("Ogone data %s", pformat(data))
-        request = urllib2.Request(direct_order_url, urlencode(data))
-        result = urllib2.urlopen(request).read()
+        result = requests.post(direct_order_url, data=data).content
         _logger.debug('Ogone response = %s', result)
 
         try:
@@ -455,8 +452,7 @@ class PaymentTxOgone(models.Model):
         query_direct_url = 'https://secure.ogone.com/ncol/%s/querydirect.asp' % (self.acquirer_id.environment)
 
         _logger.debug("Ogone data %s", pformat(data))
-        request = urllib2.Request(query_direct_url, urlencode(data))
-        result = urllib2.urlopen(request).read()
+        result = requests.post(query_direct_url, data=data).content
         _logger.debug('Ogone response = %s', result)
 
         try:
@@ -497,9 +493,7 @@ class PaymentToken(models.Model):
             }
 
             url = 'https://secure.ogone.com/ncol/%s/AFU_agree.asp' % (acquirer.environment,)
-            request = urllib2.Request(url, urlencode(data))
-
-            result = urllib2.urlopen(request).read()
+            result = requests.post(url, data=data).content
 
             try:
                 tree = objectify.fromstring(result)
diff --git a/addons/payment_ogone/tests/test_ogone.py b/addons/payment_ogone/tests/test_ogone.py
index ac32f0c4fe50..b27e7a3c4ce3 100644
--- a/addons/payment_ogone/tests/test_ogone.py
+++ b/addons/payment_ogone/tests/test_ogone.py
@@ -2,11 +2,12 @@
 
 from lxml import objectify
 import time
-import urlparse
 
 from odoo.addons.payment.models.payment_acquirer import ValidationError
 from odoo.addons.payment.tests.common import PaymentAcquirerCommon
 from odoo.addons.payment_ogone.controllers.main import OgoneController
+from werkzeug import urls
+
 from odoo.tools import mute_logger
 
 
@@ -40,10 +41,10 @@ class OgonePayment(PaymentAcquirerCommon):
             'OWNERTOWN': 'Sin City',
             'OWNERTELNO': '0032 12 34 56 78',
             'SHASIGN': '815f67b8ff70d234ffcf437c13a9fa7f807044cc',
-            'ACCEPTURL': '%s' % urlparse.urljoin(base_url, OgoneController._accept_url),
-            'DECLINEURL': '%s' % urlparse.urljoin(base_url, OgoneController._decline_url),
-            'EXCEPTIONURL': '%s' % urlparse.urljoin(base_url, OgoneController._exception_url),
-            'CANCELURL': '%s' % urlparse.urljoin(base_url, OgoneController._cancel_url),
+            'ACCEPTURL': urls.url_join(base_url, OgoneController._accept_url),
+            'DECLINEURL': urls.url_join(base_url, OgoneController._decline_url),
+            'EXCEPTIONURL': urls.url_join(base_url, OgoneController._exception_url),
+            'CANCELURL': urls.url_join(base_url, OgoneController._cancel_url),
         }
 
         # render the button
diff --git a/addons/payment_paypal/controllers/main.py b/addons/payment_paypal/controllers/main.py
index 56023740d271..edd86c96685a 100644
--- a/addons/payment_paypal/controllers/main.py
+++ b/addons/payment_paypal/controllers/main.py
@@ -3,9 +3,10 @@
 import json
 import logging
 import pprint
-import urllib
-import urllib2
+
+import requests
 import werkzeug
+from werkzeug import urls
 
 from odoo import http
 from odoo.addons.payment.models.payment_acquirer import ValidationError
@@ -23,7 +24,7 @@ class PaypalController(http.Controller):
         """ Extract the return URL from the data coming from paypal. """
         return_url = post.pop('return_url', '')
         if not return_url:
-            custom = json.loads(urllib.unquote_plus(post.pop('custom', False) or post.pop('cm', False) or '{}'))
+            custom = json.loads(urls.url_unquote_plus(post.pop('custom', False) or post.pop('cm', False) or '{}'))
             return_url = custom.get('return_url', '/')
         return return_url
 
@@ -44,7 +45,7 @@ class PaypalController(http.Controller):
         for line in lines:
             split = line.split('=', 1)
             if len(split) == 2:
-                pdt_post[split[0]] = urllib.unquote_plus(split[1]).decode('utf8')
+                pdt_post[split[0]] = urls.url_unquote_plus(split[1]).decode('utf8')
             else:
                 _logger.warning('Paypal: error processing pdt response: %s', line)
 
@@ -75,9 +76,9 @@ class PaypalController(http.Controller):
             new_post['at'] = tx and tx.acquirer_id.paypal_pdt_token or ''
             new_post['cmd'] = '_notify-synch'  # command is different in PDT than IPN/DPN
         validate_url = paypal_urls['paypal_form_url']
-        urequest = urllib2.Request(validate_url, werkzeug.url_encode(new_post))
-        uopen = urllib2.urlopen(urequest)
-        resp = uopen.read()
+        urequest = requests.post(validate_url, new_post)
+        urequest.raise_for_status()
+        resp = urequest.content
         if pdt_request:
             resp, post = self._parse_pdt_response(resp)
         if resp in ['VERIFIED', 'SUCCESS']:
diff --git a/addons/payment_paypal/models/payment.py b/addons/payment_paypal/models/payment.py
index 3219e3cc4698..5d8e5d9ed275 100644
--- a/addons/payment_paypal/models/payment.py
+++ b/addons/payment_paypal/models/payment.py
@@ -2,10 +2,10 @@
 
 import json
 import logging
-import urlparse
 
 import dateutil.parser
 import pytz
+from werkzeug import urls
 
 from odoo import api, fields, models, _
 from odoo.addons.payment.models.payment_acquirer import ValidationError
@@ -109,9 +109,9 @@ class AcquirerPaypal(models.Model):
             'zip_code': values.get('partner_zip'),
             'first_name': values.get('partner_first_name'),
             'last_name': values.get('partner_last_name'),
-            'paypal_return': '%s' % urlparse.urljoin(base_url, PaypalController._return_url),
-            'notify_url': '%s' % urlparse.urljoin(base_url, PaypalController._notify_url),
-            'cancel_return': '%s' % urlparse.urljoin(base_url, PaypalController._cancel_url),
+            'paypal_return': urls.url_join(base_url, PaypalController._return_url),
+            'notify_url': urls.url_join(base_url, PaypalController._notify_url),
+            'cancel_return': urls.url_join(base_url, PaypalController._cancel_url),
             'handling': '%.2f' % paypal_tx_values.pop('fees', 0.0) if self.fees_active else False,
             'custom': json.dumps({'return_url': '%s' % paypal_tx_values.pop('return_url')}) if paypal_tx_values.get('return_url') else False,
         })
diff --git a/addons/payment_paypal/tests/test_paypal.py b/addons/payment_paypal/tests/test_paypal.py
index c6a44dec41fd..e93bfd039b1e 100644
--- a/addons/payment_paypal/tests/test_paypal.py
+++ b/addons/payment_paypal/tests/test_paypal.py
@@ -3,10 +3,11 @@
 from odoo.addons.payment.models.payment_acquirer import ValidationError
 from odoo.addons.payment.tests.common import PaymentAcquirerCommon
 from odoo.addons.payment_paypal.controllers.main import PaypalController
+from werkzeug import urls
+
 from odoo.tools import mute_logger
 
 from lxml import objectify
-import urlparse
 
 
 class PaypalCommon(PaymentAcquirerCommon):
@@ -60,9 +61,9 @@ class PaypalForm(PaypalCommon):
             'zip': '1000',
             'country': 'BE',
             'email': 'norbert.buyer@example.com',
-            'return': '%s' % urlparse.urljoin(base_url, PaypalController._return_url),
-            'notify_url': '%s' % urlparse.urljoin(base_url, PaypalController._notify_url),
-            'cancel_return': '%s' % urlparse.urljoin(base_url, PaypalController._cancel_url),
+            'return': urls.url_join(base_url, PaypalController._return_url),
+            'notify_url': urls.url_join(base_url, PaypalController._notify_url),
+            'cancel_return': urls.url_join(base_url, PaypalController._cancel_url),
         }
 
         # check form result
diff --git a/addons/payment_payumoney/models/payment.py b/addons/payment_payumoney/models/payment.py
index 8f44b1233cb8..243ef5cef531 100644
--- a/addons/payment_payumoney/models/payment.py
+++ b/addons/payment_payumoney/models/payment.py
@@ -2,7 +2,8 @@
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 
 import hashlib
-import urlparse
+
+from werkzeug import urls
 
 from odoo import api, fields, models, _
 from odoo.addons.payment.models.payment_acquirer import ValidationError
@@ -64,9 +65,9 @@ class PaymentAcquirerPayumoney(models.Model):
                                 email=values.get('partner_email'),
                                 phone=values.get('partner_phone'),
                                 service_provider='payu_paisa',
-                                surl='%s' % urlparse.urljoin(base_url, '/payment/payumoney/return'),
-                                furl='%s' % urlparse.urljoin(base_url, '/payment/payumoney/error'),
-                                curl='%s' % urlparse.urljoin(base_url, '/payment/payumoney/cancel')
+                                surl=urls.url_join(base_url, '/payment/payumoney/return'),
+                                furl=urls.url_join(base_url, '/payment/payumoney/error'),
+                                curl=urls.url_join(base_url, '/payment/payumoney/cancel')
                                 )
 
         payumoney_values['udf1'] = payumoney_values.pop('return_url', '/')
diff --git a/addons/payment_sips/models/payment.py b/addons/payment_sips/models/payment.py
index e9cd4d40e6bf..51f78007d91d 100644
--- a/addons/payment_sips/models/payment.py
+++ b/addons/payment_sips/models/payment.py
@@ -3,7 +3,8 @@
 import json
 import logging
 from hashlib import sha256
-import urlparse
+
+from werkzeug import urls
 
 from odoo import models, fields, api
 from odoo.tools.float_utils import float_compare
@@ -89,8 +90,8 @@ class AcquirerSips(models.Model):
             'Data': u'amount=%s|' % amount +
                     u'currencyCode=%s|' % currency_code +
                     u'merchantId=%s|' % merchant_id +
-                    u'normalReturnUrl=%s|' % urlparse.urljoin(base_url, SipsController._return_url) +
-                    u'automaticResponseUrl=%s|' % urlparse.urljoin(base_url, SipsController._return_url) +
+                    u'normalReturnUrl=%s|' % urls.url_join(base_url, SipsController._return_url) +
+                    u'automaticResponseUrl=%s|' % urls.url_join(base_url, SipsController._return_url) +
                     u'transactionReference=%s|' % values['reference'] +
                     u'statementReference=%s|' % values['reference'] +
                     u'keyVersion=%s' % key_version,
diff --git a/addons/pos_mercury/models/pos_mercury_transaction.py b/addons/pos_mercury/models/pos_mercury_transaction.py
index 36748c5ea0c9..5efbdaa0ff57 100644
--- a/addons/pos_mercury/models/pos_mercury_transaction.py
+++ b/addons/pos_mercury/models/pos_mercury_transaction.py
@@ -4,8 +4,9 @@
 from datetime import date, timedelta
 
 import cgi
-import urllib2
 import ssl
+
+import requests
 import werkzeug
 
 from odoo import models, api, service
@@ -61,11 +62,11 @@ class MercuryTransaction(models.Model):
             'SOAPAction': 'http://www.mercurypay.com/CreditTransaction',
         }
 
-        r = urllib2.Request('https://w1.mercurypay.com/ws/ws.asmx', data=xml_transaction, headers=headers)
         try:
-            u = urllib2.urlopen(r, timeout=65)
-            response = werkzeug.utils.unescape(u.read())
-        except (urllib2.URLError, ssl.SSLError):
+            r = requests.post('https://w1.mercurypay.com/ws/ws.asmx', data=xml_transaction, headers=headers, timeout=65)
+            r.raise_for_status()
+            response = werkzeug.utils.unescape(r.content)
+        except:
             response = "timeout"
 
         return response
diff --git a/addons/survey/models/survey.py b/addons/survey/models/survey.py
index a35ae5ab84e1..84692c63f9d3 100644
--- a/addons/survey/models/survey.py
+++ b/addons/survey/models/survey.py
@@ -5,10 +5,11 @@ import datetime
 import logging
 import re
 import uuid
-from urlparse import urljoin
 from collections import Counter, OrderedDict
 from itertools import product
 
+from werkzeug import urls
+
 from odoo import api, fields, models, tools, SUPERUSER_ID, _
 from odoo.exceptions import UserError, ValidationError
 
@@ -108,9 +109,9 @@ class Survey(models.Model):
         base_url = '/' if self.env.context.get('relative_url') else \
                    self.env['ir.config_parameter'].sudo().get_param('web.base.url')
         for survey in self:
-            survey.public_url = urljoin(base_url, "survey/start/%s" % (slug(survey)))
-            survey.print_url = urljoin(base_url, "survey/print/%s" % (slug(survey)))
-            survey.result_url = urljoin(base_url, "survey/results/%s" % (slug(survey)))
+            survey.public_url = urls.url_join(base_url, "survey/start/%s" % (slug(survey)))
+            survey.print_url = urls.url_join(base_url, "survey/print/%s" % (slug(survey)))
+            survey.result_url = urls.url_join(base_url, "survey/results/%s" % (slug(survey)))
             survey.public_url_html = '<a href="%s">%s</a>' % (survey.public_url, _("Click here to start survey"))
 
     @api.model
diff --git a/addons/survey/tests/test_survey.py b/addons/survey/tests/test_survey.py
index 01786368b722..699aac0cde30 100644
--- a/addons/survey/tests/test_survey.py
+++ b/addons/survey/tests/test_survey.py
@@ -5,7 +5,8 @@ import random
 import re
 from collections import Counter
 from itertools import product
-from urlparse import urljoin
+
+from werkzeug import urls
 
 from odoo import _
 from odoo.exceptions import UserError
@@ -183,7 +184,7 @@ class TestSurvey(TransactionCase):
             survey_url_relative = getattr(self.survey1.with_context({'relative_url': True}), urltype + '_url')
             self.assertTrue(validate_url(survey_url))
             url = "survey/%s/%s" % (urltxt, slug(self.survey1))
-            full_url = urljoin(base_url, url)
+            full_url = urls.url_join(base_url, url)
             self.assertEqual(full_url, survey_url)
             self.assertEqual('/' + url, survey_url_relative)
             if urltype == 'public':
diff --git a/addons/survey/wizard/survey_email_compose_message.py b/addons/survey/wizard/survey_email_compose_message.py
index 2e333822dc1c..fb36d839c57c 100644
--- a/addons/survey/wizard/survey_email_compose_message.py
+++ b/addons/survey/wizard/survey_email_compose_message.py
@@ -3,7 +3,8 @@
 
 import re
 import uuid
-import urlparse
+
+from werkzeug import urls
 
 from odoo import api, fields, models, _
 from odoo.exceptions import UserError
@@ -85,7 +86,7 @@ class SurveyMailComposeMessage(models.TransientModel):
             #set url
             url = wizard.survey_id.public_url
 
-            url = urlparse.urlparse(url).path[1:]  # dirty hack to avoid incorrect urls
+            url = urls.url_parse(url).path[1:]  # dirty hack to avoid incorrect urls
 
             if token:
                 url = url + '/' + token
diff --git a/addons/web_editor/models/ir_qweb.py b/addons/web_editor/models/ir_qweb.py
index e3bd9ec94e2b..71445d4919e8 100644
--- a/addons/web_editor/models/ir_qweb.py
+++ b/addons/web_editor/models/ir_qweb.py
@@ -14,15 +14,16 @@ import itertools
 import json
 import logging
 import os
-import urllib2
-import urlparse
 import re
 import hashlib
 
 import pytz
+import requests
 from dateutil import parser
 from lxml import etree, html
 from PIL import Image as I
+from werkzeug import urls
+
 import odoo.modules
 
 from odoo import api, models, fields
@@ -319,11 +320,11 @@ class Image(models.AbstractModel):
     def from_html(self, model, field, element):
         url = element.find('img').get('src')
 
-        url_object = urlparse.urlsplit(url)
+        url_object = urls.url_parse(url)
         if url_object.path.startswith('/web/image'):
             # url might be /web/image/<model>/<id>[_<checksum>]/<field>[/<width>x<height>]
             fragments = url_object.path.split('/')
-            query = dict(urlparse.parse_qsl(url_object.query))
+            query = url_object.decode_query()
             if fragments[3].isdigit():
                 model = 'ir.attachment'
                 oid = fragments[3]
@@ -341,7 +342,7 @@ class Image(models.AbstractModel):
         return self.load_remote_url(url)
 
     def load_local_url(self, url):
-        match = self.local_url_re.match(urlparse.urlsplit(url).path)
+        match = self.local_url_re.match(urls.url_parse(url).path)
 
         rest = match.group('rest')
         for sep in os.sep, os.altsep:
@@ -374,9 +375,9 @@ class Image(models.AbstractModel):
             #   linking to HTTP images
             # implement drag & drop image upload to mitigate?
 
-            req = urllib2.urlopen(url, timeout=REMOTE_CONNECTION_TIMEOUT)
-            # PIL needs a seekable file-like image, urllib result is not seekable
-            image = I.open(cStringIO.StringIO(req.read()))
+            req = requests.get(url, timeout=REMOTE_CONNECTION_TIMEOUT)
+            # PIL needs a seekable file-like image so wrap result in IO buffer
+            image = I.open(cStringIO.StringIO(req.content))
             # force a complete load of the image data to validate it
             image.load()
         except Exception:
diff --git a/addons/web_planner/models/web_planner.py b/addons/web_planner/models/web_planner.py
index 3fa38c1dc7c3..f1cae08d5507 100644
--- a/addons/web_planner/models/web_planner.py
+++ b/addons/web_planner/models/web_planner.py
@@ -1,7 +1,6 @@
 # -*- coding: utf-8 -*-
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from urllib import urlencode
+from werkzeug import urls
 
 from odoo import api, models, fields
 
@@ -69,7 +68,7 @@ class Planner(models.Model):
                 params['id'] = module.id
             else:
                 return "#show_enterprise"
-        return "/web#%s" % (urlencode(params),)
+        return "/web#%s" % (urls.url_encode(params),)
 
     @api.model
     def is_module_installed(self, module_name=None):
diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py
index 5fab12ac6002..e67697e2bccc 100644
--- a/addons/website/controllers/main.py
+++ b/addons/website/controllers/main.py
@@ -7,7 +7,8 @@ import json
 import xml.etree.ElementTree as ET
 import logging
 import re
-import urllib2
+
+import requests
 import werkzeug.utils
 import werkzeug.wrappers
 
@@ -300,12 +301,13 @@ class Website(Home):
         language = lang.split("_")
         url = "http://google.com/complete/search"
         try:
-            req = urllib2.Request("%s?%s" % (url, werkzeug.url_encode({
-                'ie': 'utf8', 'oe': 'utf8', 'output': 'toolbar', 'q': keywords, 'hl': language[0], 'gl': language[1]})))
-            response = urllib2.urlopen(req)
-        except (urllib2.HTTPError, urllib2.URLError):
+            req = requests.get(url, params={
+                'ie': 'utf8', 'oe': 'utf8', 'output': 'toolbar', 'q': keywords, 'hl': language[0], 'gl': language[1]})
+            req.raise_for_status()
+            response = req.content
+        except IOError:
             return []
-        xmlroot = ET.fromstring(response.read())
+        xmlroot = ET.fromstring(response)
         return json.dumps([sugg[0].attrib['data'] for sugg in xmlroot if len(sugg) and sugg[0].attrib['data']])
 
     #------------------------------------------------------
diff --git a/addons/website/models/ir_actions.py b/addons/website/models/ir_actions.py
index 176187ea4e0c..f26d6263004c 100644
--- a/addons/website/models/ir_actions.py
+++ b/addons/website/models/ir_actions.py
@@ -1,7 +1,6 @@
 # -*- coding: utf-8 -*-
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-import urlparse
+from werkzeug import urls
 
 from odoo import api, fields, models
 from odoo.http import request
@@ -32,7 +31,7 @@ class ServerAction(models.Model):
         link = website_path or xml_id or (self.id and '%d' % self.id) or ''
         if base_url and link:
             path = '%s/%s' % ('/website/action', link)
-            return '%s' % urlparse.urljoin(base_url, path)
+            return urls.url_join(base_url, path)
         return ''
 
     @api.depends('state', 'website_published', 'website_path', 'xml_id')
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index 9968ad8e6366..fe79a8ac1a32 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -6,9 +6,9 @@ import logging
 import math
 import unicodedata
 import re
-import urlparse
 import hashlib
-import werkzeug
+
+from werkzeug import urls
 from werkzeug.exceptions import NotFound
 
 # optional python-slugify import (https://github.com/un33k/python-slugify)
@@ -44,10 +44,10 @@ def url_for(path_or_uri, lang=None):
         current_path = current_path.encode('utf-8')
     location = path_or_uri.strip()
     force_lang = lang is not None
-    url = urlparse.urlparse(location)
+    url = urls.url_parse(location)
 
     if request and not url.netloc and not url.scheme and (url.path or force_lang):
-        location = urlparse.urljoin(current_path, location)
+        location = urls.url_join(current_path, location)
 
         lang = lang or request.context.get('lang')
         langs = [lg[0] for lg in request.website.get_languages()]
@@ -459,7 +459,7 @@ class Website(models.Model):
         def get_url(page):
             _url = "%s/page/%s" % (url, page) if page > 1 else url
             if url_args:
-                _url = "%s?%s" % (_url, werkzeug.url_encode(url_args))
+                _url = "%s?%s" % (_url, urls.url_encode(url_args))
             return _url
 
         return {
@@ -598,7 +598,7 @@ class Website(models.Model):
             cdn_filters = (request.website.cdn_filters or '').splitlines()
             for flt in cdn_filters:
                 if flt and re.match(flt, uri):
-                    return urlparse.urljoin(cdn_url, uri)
+                    return urls.url_join(cdn_url, uri)
         return uri
 
     @api.model
diff --git a/addons/website/tests/test_crawl.py b/addons/website/tests/test_crawl.py
index a9f1e9b1f845..f5d4bc4ec126 100644
--- a/addons/website/tests/test_crawl.py
+++ b/addons/website/tests/test_crawl.py
@@ -2,10 +2,10 @@
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 
 import logging
-import urlparse
 import time
 
 import lxml.html
+from werkzeug import urls
 
 import odoo
 import re
@@ -39,23 +39,17 @@ class Crawler(odoo.tests.HttpCase):
 
         _logger.info("%s %s", msg, url)
         r = self.url_open(url)
-        code = r.getcode()
+        code = r.status_code
         self.assertIn(code, range(200, 300), "%s Fetching %s returned error response (%d)" % (msg, url, code))
 
-        if r.info().gettype() == 'text/html':
-            doc = lxml.html.fromstring(r.read())
+        if r.headers['Content-Type'].startswith('text/html'):
+            doc = lxml.html.fromstring(r.content)
             for link in doc.xpath('//a[@href]'):
                 href = link.get('href')
 
-                parts = urlparse.urlsplit(href)
+                parts = urls.url_parse(href)
                 # href with any fragment removed
-                href = urlparse.urlunsplit((
-                    parts.scheme,
-                    parts.netloc,
-                    parts.path,
-                    parts.query,
-                    ''
-                ))
+                href = parts.replace(fragment='').to_url()
 
                 # FIXME: handle relative link (not parts.path.startswith /)
                 if parts.netloc or \
diff --git a/addons/website_forum/controllers/main.py b/addons/website_forum/controllers/main.py
index 0c8535c9b309..6760ad911fef 100644
--- a/addons/website_forum/controllers/main.py
+++ b/addons/website_forum/controllers/main.py
@@ -4,12 +4,12 @@
 import base64
 import json
 import lxml
+import requests
 import werkzeug.exceptions
 import werkzeug.urls
 import werkzeug.wrappers
 
 from datetime import datetime
-from urllib2 import urlopen, URLError
 
 from odoo import http, modules, SUPERUSER_ID, tools, _
 from odoo.addons.web.controllers.main import binary_content
@@ -217,9 +217,11 @@ class WebsiteForum(http.Controller):
     @http.route('/forum/get_url_title', type='json', auth="user", methods=['POST'], website=True)
     def get_url_title(self, **kwargs):
         try:
-            arch = lxml.html.parse(urlopen(kwargs.get('url')))
+            req = requests.get(kwargs.get('url'), stream=True)
+            req.raise_for_status()
+            arch = lxml.html.parse(req.raw)
             return arch.find(".//title").text
-        except URLError:
+        except IOError:
             return False
 
     @http.route(['''/forum/<model("forum.forum"):forum>/question/<model("forum.post", "[('forum_id','=',forum[0]),('parent_id','=',False),('can_view', '=', True)]"):question>'''], type='http', auth="public", website=True)
diff --git a/addons/website_forum/models/res_users.py b/addons/website_forum/models/res_users.py
index 499fccaba8f8..9d22ee3c9d8f 100644
--- a/addons/website_forum/models/res_users.py
+++ b/addons/website_forum/models/res_users.py
@@ -4,7 +4,8 @@
 import hashlib
 
 from datetime import datetime
-from urllib import urlencode
+
+from werkzeug import urls
 
 from odoo import api, fields, models
 
@@ -85,7 +86,7 @@ class Users(models.Model):
             if forum_id:
                 params['forum_id'] = forum_id
             base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
-            token_url = base_url + '/forum/validate_email?%s' % urlencode(params)
+            token_url = base_url + '/forum/validate_email?%s' % urls.url_encode(params)
             activation_template.sudo().with_context(token_url=token_url).send_mail(self.id, force_send=True)
         return True
 
diff --git a/addons/website_hr_recruitment/models/hr_recruitment.py b/addons/website_hr_recruitment/models/hr_recruitment.py
index 662bf0b4f483..7810ae2fccd6 100644
--- a/addons/website_hr_recruitment/models/hr_recruitment.py
+++ b/addons/website_hr_recruitment/models/hr_recruitment.py
@@ -1,8 +1,7 @@
 # -*- coding: utf-8 -*-
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 
-from urlparse import urljoin
-from werkzeug import url_encode
+from werkzeug import urls
 
 from odoo import api, fields, models
 from odoo.addons.website.models.website import slug
@@ -19,8 +18,8 @@ class RecruitmentSource(models.Model):
     def _compute_url(self):
         base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
         for source in self:
-            source.url = urljoin(base_url, "%s?%s" % (source.job_id.website_url,
-                url_encode({
+            source.url = urls.url_join(base_url, "%s?%s" % (source.job_id.website_url,
+                urls.url_encode({
                     'utm_campaign': self.env.ref('hr_recruitment.utm_campaign_job').name,
                     'utm_medium': self.env.ref('utm.utm_medium_website').name,
                     'utm_source': source.source_id.name
diff --git a/addons/website_mail_channel/models/mail_channel.py b/addons/website_mail_channel/models/mail_channel.py
index 924d4ea25ea6..519dfe7afc65 100644
--- a/addons/website_mail_channel/models/mail_channel.py
+++ b/addons/website_mail_channel/models/mail_channel.py
@@ -3,7 +3,7 @@
 
 import hmac
 
-from urlparse import urljoin
+from werkzeug import urls
 
 from odoo import api, models
 from odoo.tools.safe_eval import safe_eval
@@ -47,7 +47,7 @@ class MailGroup(models.Model):
             # generate a new token per subscriber
             token = self._generate_action_token(partner_id, action=action)
 
-            token_url = urljoin(base_url, route % {
+            token_url = urls.url_join(base_url, route % {
                 'action': action,
                 'channel': self.id,
                 'partner': partner_id,
diff --git a/addons/website_sale/controllers/website_mail.py b/addons/website_sale/controllers/website_mail.py
index 76c6d29ddda8..5931098d3a1c 100644
--- a/addons/website_sale/controllers/website_mail.py
+++ b/addons/website_sale/controllers/website_mail.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
-import urlparse
+from werkzeug import urls
 
 from odoo import http
 from odoo.http import request
@@ -37,7 +37,7 @@ class WebsiteMailController(WebsiteMail):
         response = super(WebsiteMailController, self).chatter_post(res_model=res_model, res_id=res_id, message=message, redirect=redirect, **params)
         if kw.get('rating') and res_model == 'product.template':  # restrict rating only for product template
             try:
-                fragment = urlparse.urlparse(response.location).fragment
+                fragment = urls.url_parse(response.location).fragment
                 message_id = int(fragment.replace('message-', ''))
                 res_model_id = request.env.ref('product.model_product_template').id
                 request.env['rating.rating'].create({
diff --git a/addons/website_slides/models/slides.py b/addons/website_slides/models/slides.py
index d391246b9cd6..578bca3a1473 100644
--- a/addons/website_slides/models/slides.py
+++ b/addons/website_slides/models/slides.py
@@ -1,15 +1,14 @@
 # -*- coding: utf-8 -*-
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
-
+import requests
 from PIL import Image
-from urllib import urlencode
-from urlparse import urlparse
 
 import datetime
 import io
 import json
 import re
-import urllib2
+
+from werkzeug import urls
 
 from odoo import api, fields, models, SUPERUSER_ID, _
 from odoo.tools import image, pycompat
@@ -224,8 +223,7 @@ class EmbeddedSlide(models.Model):
     count_views = fields.Integer('# Views', default=1)
 
     def add_embed_url(self, slide_id, url):
-        schema = urlparse(url)
-        baseurl = schema.netloc
+        baseurl = urls.url_parse(url).netloc
         embeds = self.search([('url', '=', baseurl), ('slide_id', '=', int(slide_id))], limit=1)
         if embeds:
             embeds.count_views += 1
@@ -498,22 +496,19 @@ class Slide(models.Model):
     def _fetch_data(self, base_url, data, content_type=False, extra_params=False):
         result = {'values': dict()}
         try:
-            if data:
-                sep = '?' if not extra_params else '&'
-                base_url = base_url + '%s%s' % (sep, urlencode(data))
-            req = urllib2.Request(base_url)
-            content = urllib2.urlopen(req).read()
+            response = requests.get(base_url, params=data)
+            response.raise_for_status()
+            content = response.content
             if content_type == 'json':
                 result['values'] = json.loads(content)
             elif content_type in ('image', 'pdf'):
                 result['values'] = content.encode('base64')
             else:
                 result['values'] = content
-        except urllib2.HTTPError as e:
-            result['error'] = e.read()
-            e.close()
-        except urllib2.URLError as e:
-            result['error'] = e.reason
+        except requests.exceptions.HTTPError as e:
+            result['error'] = e.response.content
+        except requests.exceptions.ConnectionError as e:
+            result['error'] = str(e)
         return result
 
     def _find_document_data_from_url(self, url):
diff --git a/addons/website_twitter/models/website_twitter.py b/addons/website_twitter/models/website_twitter.py
index 2b17411b72cb..49c8b52c337c 100644
--- a/addons/website_twitter/models/website_twitter.py
+++ b/addons/website_twitter/models/website_twitter.py
@@ -4,8 +4,9 @@
 import base64
 import json
 import logging
+
+import requests
 import werkzeug
-from urllib2 import urlopen, Request, HTTPError
 from odoo import api, fields, models
 
 API_ENDPOINT = 'https://api.twitter.com'
@@ -28,16 +29,13 @@ class WebsiteTwitter(models.Model):
     def _request(self, website, url, params=None):
         """Send an authenticated request to the Twitter API."""
         access_token = self._get_access_token(website)
-        if params:
-            params = werkzeug.url_encode(params)
-            url = url + '?' + params
         try:
-            request = Request(url)
-            request.add_header('Authorization', 'Bearer %s' % access_token)
-            return json.load(urlopen(request, timeout=URLOPEN_TIMEOUT))
-        except HTTPError as e:
+            request = requests.get(url, params=params, headers={'Authorization': 'Bearer %s' % access_token}, timeout=URLOPEN_TIMEOUT)
+            request.raise_for_status()
+            return request.json()
+        except requests.HTTPError as e:
             _logger.debug("Twitter API request failed with code: %r, msg: %r, content: %r",
-                          e.code, e.msg, e.fp.read())
+                          e.response.status_code, e.response.reason, e.response.content)
             raise
 
     @api.model
diff --git a/addons/website_twitter/models/website_twitter_config.py b/addons/website_twitter/models/website_twitter_config.py
index 49542e396069..493a15da2415 100644
--- a/addons/website_twitter/models/website_twitter_config.py
+++ b/addons/website_twitter/models/website_twitter_config.py
@@ -2,7 +2,9 @@
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 
 import logging
-from urllib2 import URLError, HTTPError
+
+import requests
+
 from odoo import api, fields, models, _
 from odoo.exceptions import UserError
 
@@ -49,13 +51,13 @@ class WebsiteTwitterConfig(models.TransientModel):
         try:
             self.website_id.fetch_favorite_tweets()
 
-        except HTTPError as e:
-            _logger.info("%s - %s" % (e.code, e.reason), exc_info=True)
-            raise UserError("%s - %s" % (e.code, e.reason) + ':' + self._get_twitter_exception_message(e.code))
-        except URLError as e:
+        except requests.HTTPError as e:
+            _logger.info("%s - %s" % (e.response.status_code, e.response.reason), exc_info=True)
+            raise UserError("%s - %s" % (e.response.status_code, e.response.reason) + ':' + self._get_twitter_exception_message(e.response.status_code))
+        except IOError:
             _logger.info(_('We failed to reach a twitter server.'), exc_info=True)
             raise UserError(_('Internet connection refused') + ' ' + _('We failed to reach a twitter server.'))
-        except Exception as e:
+        except Exception:
             _logger.info(_('Please double-check your Twitter API Key and Secret!'), exc_info=True)
             raise UserError(_('Twitter authorization error!') + ' ' + _('Please double-check your Twitter API Key and Secret!'))
 
diff --git a/doc/_extensions/github_link.py b/doc/_extensions/github_link.py
index cd0c53806219..d3e5d61c28e7 100644
--- a/doc/_extensions/github_link.py
+++ b/doc/_extensions/github_link.py
@@ -1,7 +1,8 @@
 import inspect
 import importlib
 import os.path
-from urlparse import urlunsplit
+
+from werkzeug import urls
 
 """
 * adds github_link(mode) context variable: provides URL (in relevant mode) of
@@ -81,7 +82,7 @@ def make_github_link(app, path, line=None, mode="blob"):
         path=path,
         mode=mode,
     )
-    return urlunsplit((
+    return urls.url_unparse((
         'https',
         'github.com',
         urlpath,
diff --git a/doc/_extensions/odoo_ext/translator.py b/doc/_extensions/odoo_ext/translator.py
index 1f7fba93772d..3a1928364eb1 100644
--- a/doc/_extensions/odoo_ext/translator.py
+++ b/doc/_extensions/odoo_ext/translator.py
@@ -2,7 +2,10 @@
 import os.path
 import posixpath
 import re
-import urllib
+try:
+    from urllib.request import url2pathname  # pylint: disable=deprecated-module
+except ImportError:
+    from urllib import url2pathname  # pylint: disable=deprecated-module
 
 from docutils import nodes
 from sphinx import addnodes, util
@@ -636,7 +639,7 @@ class BootstrapTranslator(nodes.NodeVisitor, object):
                     banner = '_static/' + cover
                     base, ext = os.path.splitext(banner)
                     small = "{}.small{}".format(base, ext)
-                    if os.path.isfile(urllib.url2pathname(small)):
+                    if os.path.isfile(url2pathname(small)):
                         banner = small
                     style = u"background-image: url('{}')".format(
                         util.relative_uri(baseuri, banner) or '#')
diff --git a/odoo/addons/base/ir/ir_qweb/ir_qweb.py b/odoo/addons/base/ir/ir_qweb/ir_qweb.py
index 19119891ec44..102b36e0898f 100644
--- a/odoo/addons/base/ir/ir_qweb/ir_qweb.py
+++ b/odoo/addons/base/ir/ir_qweb/ir_qweb.py
@@ -5,14 +5,10 @@ import json
 import logging
 from collections import OrderedDict
 from time import time
-try:
-    from urllib.parse import urlparse
-except ImportError:
-    # pylint: disable=bad-python3-import
-    from urlparse import urlparse
 
 from lxml import html
 from lxml import etree
+from werkzeug import urls
 
 from odoo.tools import pycompat
 
@@ -230,7 +226,7 @@ class IrQWeb(models.AbstractModel, QWeb):
                 atype = el.get('type')
                 media = el.get('media')
 
-                can_aggregate = not urlparse(href).netloc and not href.startswith('/web/content')
+                can_aggregate = not urls.url_parse(href).netloc and not href.startswith('/web/content')
                 if el.tag == 'style' or (el.tag == 'link' and el.get('rel') == 'stylesheet' and can_aggregate):
                     if href.endswith('.sass'):
                         atype = 'text/sass'
diff --git a/odoo/addons/base/module/module.py b/odoo/addons/base/module/module.py
index c8a83a01bf1c..fd5ba44c9d19 100644
--- a/odoo/addons/base/module/module.py
+++ b/odoo/addons/base/module/module.py
@@ -10,15 +10,9 @@ import shutil
 import tempfile
 import zipfile
 
-from odoo.tools import pycompat
+import requests
 
-try:
-    from urllib import parse as urlparse
-    from urllib.request import urlopen
-except ImportError:
-    # pylint: disable=bad-python3-import
-    import urlparse
-    from urllib2 import urlopen
+from odoo.tools import pycompat
 
 from docutils import nodes
 from docutils.core import publish_string
@@ -663,7 +657,7 @@ class Module(models.Model):
             _logger.warning(msg)
             raise UserError(msg)
 
-        apps_server = urlparse.urlparse(self.get_apps_server())
+        apps_server = urls.url_parse(self.get_apps_server())
 
         OPENERP = odoo.release.product_name.lower()
         tmp = tempfile.mkdtemp()
@@ -674,13 +668,15 @@ class Module(models.Model):
                 if not url:
                     continue    # nothing to download, local version is already the last one
 
-                up = urlparse.urlparse(url)
+                up = urls.url_parse(url)
                 if up.scheme != apps_server.scheme or up.netloc != apps_server.netloc:
                     raise AccessDenied()
 
                 try:
                     _logger.info('Downloading module `%s` from OpenERP Apps', module_name)
-                    content = urlopen(url).read()
+                    response = requests.get(url)
+                    response.raise_for_status()
+                    content = response.content
                 except Exception:
                     _logger.exception('Failed to fetch module %s', module_name)
                     raise UserError(_('The `%s` module appears to be unavailable at the moment, please try again later.') % module_name)
diff --git a/odoo/addons/base/res/res_partner.py b/odoo/addons/base/res/res_partner.py
index 17e6507a01e8..18e76a4a46c3 100644
--- a/odoo/addons/base/res/res_partner.py
+++ b/odoo/addons/base/res/res_partner.py
@@ -7,16 +7,11 @@ import hashlib
 import pytz
 import threading
 
-try:
-    from urllib import parse as urlparse
-    from urllib.request import urlopen
-except ImportError:
-    # pylint: disable=bad-python3-import
-    import urlparse
-    from urllib2 import urlopen
-
 from email.utils import formataddr
+
+import requests
 from lxml import etree
+from werkzeug import urls
 
 from odoo import api, fields, models, tools, SUPERUSER_ID, _
 from odoo.modules import get_module_resource
@@ -481,11 +476,11 @@ class Partner(models.Model):
             parent.update_address(addr_vals)
 
     def _clean_website(self, website):
-        (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(website)
-        if not scheme:
-            if not netloc:
-                netloc, path = path, ''
-            website = urlparse.urlunparse(('http', netloc, path, params, query, fragment))
+        url = urls.url_parse(website)
+        if not url.scheme:
+            if not url.netloc:
+                url = url.replace(netloc=url.path, path='')
+            website = url.replace(scheme='http').to_url()
         return website
 
     @api.multi
@@ -703,15 +698,12 @@ class Partner(models.Model):
         return partners.id or self.name_create(email)[0]
 
     def _get_gravatar_image(self, email):
-        gravatar_image = False
         email_hash = hashlib.md5(email.lower()).hexdigest()
         url = "https://www.gravatar.com/avatar/" + email_hash
-        try:
-            image_content = urlopen(url + "?d=404&s=128", timeout=5).read()
-            gravatar_image = base64.b64encode(image_content)
-        except Exception:
-            pass
-        return gravatar_image
+        res = requests.get(url, params={'d': '404', 's': '128'}, timeout=5)
+        if res.status_code != requests.codes.ok:
+            return False
+        return base64.b64encode(res.content)
 
     @api.multi
     def _email_send(self, email_from, subject, body, on_error=None):
diff --git a/odoo/http.py b/odoo/http.py
index a69a569cbcae..b0d509419371 100644
--- a/odoo/http.py
+++ b/odoo/http.py
@@ -20,12 +20,6 @@ import sys
 import threading
 import time
 import traceback
-try:
-    from urllib.parse import parse_qs, urlparse, quote
-except ImportError:
-    # pylint: disable=bad-python3-import
-    from urllib2 import quote
-    from urlparse import parse_qs, urlparse
 import warnings
 from os.path import join as opj
 from zlib import adler32
@@ -41,6 +35,7 @@ import werkzeug.local
 import werkzeug.routing
 import werkzeug.wrappers
 import werkzeug.wsgi
+from werkzeug import urls
 from werkzeug.wsgi import wrap_file
 
 try:
@@ -351,7 +346,7 @@ class WebRequest(object):
             debug = self.httprequest.environ.get('HTTP_X_DEBUG_MODE')
 
         if not debug and self.httprequest.referrer:
-            debug = bool(parse_qs(urlparse(self.httprequest.referrer).query, keep_blank_values=True).get('debug'))
+            debug = 'debug' in urls.url_parse(self.httprequest.referrer).decode_query()
         return debug
 
     @contextlib.contextmanager
@@ -1254,7 +1249,7 @@ class DisableCacheMiddleware(object):
     def __call__(self, environ, start_response):
         def start_wrapped(status, headers):
             referer = environ.get('HTTP_REFERER', '')
-            parsed = urlparse(referer)
+            parsed = urls.url_parse(referer)
             debug = parsed.query.count('debug') >= 1
 
             new_headers = []
@@ -1619,7 +1614,7 @@ def send_file(filepath_or_fp, mimetype=None, as_attachment=False, filename=None,
 
 def content_disposition(filename):
     filename = odoo.tools.ustr(filename)
-    escaped = quote(filename.encode('utf8'))
+    escaped = urls.url_quote(filename.encode('utf8'))
     browser = request.httprequest.user_agent.browser
     version = int((request.httprequest.user_agent.version or '0').split('.')[0])
     if browser == 'msie' and version < 9:
diff --git a/odoo/sql_db.py b/odoo/sql_db.py
index a722c3ea41b8..ee91704bb72d 100644
--- a/odoo/sql_db.py
+++ b/odoo/sql_db.py
@@ -13,17 +13,13 @@ from functools import wraps
 import logging
 import time
 import uuid
-try:
-    from urllib import parse as urlparse
-except ImportError:
-    #pylint: disable=bad-python3-import
-    import urlparse
 
 import psycopg2
 import psycopg2.extras
 import psycopg2.extensions
 from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT, ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_REPEATABLE_READ
 from psycopg2.pool import PoolError
+from werkzeug import urls
 
 from .tools import pycompat
 
@@ -656,7 +652,7 @@ def connection_info_for(db_or_uri):
     """
     if db_or_uri.startswith(('postgresql://', 'postgres://')):
         # extract db from uri
-        us = urlparse.urlsplit(db_or_uri)
+        us = urls.url_parse(db_or_uri)
         if len(us.path) > 1:
             db_name = us.path[1:]
         elif us.username:
diff --git a/odoo/tests/common.py b/odoo/tests/common.py
index a491c23ef76c..823a56c6190f 100644
--- a/odoo/tests/common.py
+++ b/odoo/tests/common.py
@@ -19,16 +19,15 @@ import unittest
 from contextlib import contextmanager
 from datetime import datetime, timedelta
 from pprint import pformat
+
+import requests
+
 try:
-    from urllib import request as urllib2
     from xmlrpc import client as xmlrpclib
 except ImportError:
     # pylint: disable=bad-python3-import
-    import urllib2
     import xmlrpclib
 
-import werkzeug
-
 import odoo
 from odoo import api
 
@@ -215,26 +214,6 @@ class SavepointCase(SingleTransactionCase):
         self.registry.clear_caches()
 
 
-class RedirectHandler(urllib2.HTTPRedirectHandler):
-    """
-    HTTPRedirectHandler is predicated upon HTTPErrorProcessor being used and
-    works by intercepting 3xy "errors".
-
-    Inherit from it to handle 3xy non-error responses instead, as we're not
-    using the error processor
-    """
-
-    def http_response(self, request, response):
-        code, msg, hdrs = response.code, response.msg, response.info()
-
-        if 300 <= code < 400:
-            return self.parent.error(
-                'http', request, response, code, msg, hdrs)
-
-        return response
-
-    https_response = http_response
-
 class HttpCase(TransactionCase):
     """ Transactional HTTP TestCase with url_open and phantomjs helpers.
     """
@@ -259,18 +238,15 @@ class HttpCase(TransactionCase):
         self.session.db = get_db_name()
         odoo.http.root.session_store.save(self.session)
         # setup an url opener helper
-        self.opener = urllib2.OpenerDirector()
-        self.opener.add_handler(urllib2.UnknownHandler())
-        self.opener.add_handler(urllib2.HTTPHandler())
-        self.opener.add_handler(urllib2.HTTPSHandler())
-        self.opener.add_handler(urllib2.HTTPCookieProcessor())
-        self.opener.add_handler(RedirectHandler())
-        self.opener.addheaders.append(('Cookie', 'session_id=%s' % self.session_id))
+        self.opener = requests.Session()
+        self.opener.cookies['session_id'] = self.session_id
 
     def url_open(self, url, data=None, timeout=10):
         if url.startswith('/'):
             url = "http://%s:%s%s" % (HOST, PORT, url)
-        return self.opener.open(url, data, timeout)
+        if data:
+            return self.opener.post(url, data=data, timeout=timeout)
+        return self.opener.get(url, timeout=timeout)
 
     def authenticate(self, user, password):
         # stay non-authenticated
diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py
index 81c27105ff14..293ad1486322 100644
--- a/odoo/tools/misc.py
+++ b/odoo/tools/misc.py
@@ -26,7 +26,6 @@ from itertools import islice, groupby, repeat
 from lxml import etree
 
 from .which import which
-from threading import local
 import traceback
 import csv
 from operator import itemgetter
@@ -677,29 +676,6 @@ def split_every(n, iterable, piece_maker=tuple):
         yield piece
         piece = piece_maker(islice(iterator, n))
 
-if __name__ == '__main__':
-    import doctest
-    doctest.testmod()
-
-class upload_data_thread(threading.Thread):
-    def __init__(self, email, data, type):
-        self.args = [('email',email),('type',type),('data',data)]
-        super(upload_data_thread,self).__init__()
-    def run(self):
-        try:
-            import urllib
-            args = urllib.urlencode(self.args)
-            fp = urllib.urlopen('http://www.openerp.com/scripts/survey.php', args)
-            fp.read()
-            fp.close()
-        except Exception:
-            pass
-
-def upload_data(email, data, type='SURVEY'):
-    a = upload_data_thread(email, data, type)
-    a.start()
-    return True
-
 def get_and_group_by_field(cr, uid, obj, ids, field, context=None):
     """ Read the values of ``field´´ for the given ``ids´´ and group ids by value.
 
-- 
GitLab