diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py
index e0e16eeef7c..65ffe08e1e1 100644
--- a/addons/mail/models/mail_template.py
+++ b/addons/mail/models/mail_template.py
@@ -300,7 +300,7 @@ class MailTemplate(models.Model):
def _process_link(url):
new_url = urls.url_parse(url)
- if new_url.scheme and new_url.netloc:
+ if new_url.scheme and (new_url.netloc or new_url.scheme == 'mailto'):
return url
return new_url.replace(scheme=base.scheme, netloc=base.netloc).to_url()
diff --git a/addons/mail/static/src/js/client_action.js b/addons/mail/static/src/js/client_action.js
index 2b5b342787b..61650ceaa73 100644
--- a/addons/mail/static/src/js/client_action.js
+++ b/addons/mail/static/src/js/client_action.js
@@ -890,7 +890,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
var def = window.Notification && window.Notification.requestPermission();
if (def) {
def.then(function (value) {
- if (value === 'denied') {
+ if (value !== 'granted') {
utils.send_notification(self, _t('Permission denied'),
_t('Odoo will not have the permission to send native notifications on this device.'));
} else {
diff --git a/addons/maintenance/models/maintenance.py b/addons/maintenance/models/maintenance.py
index d46b688ca79..53b450baee7 100644
--- a/addons/maintenance/models/maintenance.py
+++ b/addons/maintenance/models/maintenance.py
@@ -368,7 +368,7 @@ class MaintenanceTeam(models.Model):
_name = 'maintenance.team'
_description = 'Maintenance Teams'
- name = fields.Char(required=True)
+ name = fields.Char(required=True, translate=True)
member_ids = fields.Many2many('res.users', 'maintenance_team_users_rel', string="Team Members")
color = fields.Integer("Color Index", default=0)
request_ids = fields.One2many('maintenance.request', 'maintenance_team_id', copy=False)
diff --git a/addons/mass_mailing/static/src/js/mass_mailing_editor.js b/addons/mass_mailing/static/src/js/mass_mailing_editor.js
index 7705e62fa9e..c40f31c258f 100644
--- a/addons/mass_mailing/static/src/js/mass_mailing_editor.js
+++ b/addons/mass_mailing/static/src/js/mass_mailing_editor.js
@@ -323,20 +323,20 @@ snippets_editor.Class.include({
$body.removeClass(all_classes).addClass(theme_params.className);
var $old_layout = $editable_area.find(".o_layout");
- // This wrapper structure is the only way to have a responsive and
- // centered fixed-width content column on all mail clients
var $new_wrapper, $new_wrapper_content;
if (theme_params.nowrap) {
$new_wrapper = $new_wrapper_content = $("", {"class": "oe_structure"});
}
else {
+ // This wrapper structure is the only way to have a responsive and
+ // centered fixed-width content column on all mail clients
$new_wrapper = $('
diff --git a/doc/cla/corporate/vauxoo.md b/doc/cla/corporate/vauxoo.md
index f250546a6ef..0d8c8b623e9 100644
--- a/doc/cla/corporate/vauxoo.md
+++ b/doc/cla/corporate/vauxoo.md
@@ -42,3 +42,4 @@ Edgar Rivero edgar@vauxoo.com https://github.com/egrivero
Alexander Olivares alexander@vauxoo.com https://github.com/alxolivares
Jose Manuel Robles josemanuel@vauxoo.com https://github.com/keylor2906
Erick Birbe erick@vauxoo.com https://github.com/ebirbe
+Tomas Alvarez tomas@vauxoo.com https://github.com/tomeyro
diff --git a/doc/cla/individual/kaberer.md b/doc/cla/individual/kaberer.md
new file mode 100644
index 00000000000..e17852fe338
--- /dev/null
+++ b/doc/cla/individual/kaberer.md
@@ -0,0 +1,11 @@
+Switzerland, 2018-01-30
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Kumar Aberer office@aberer-solutions.ch https://github.com/kaberer
diff --git a/doc/cla/individual/moutio.md b/doc/cla/individual/moutio.md
new file mode 100644
index 00000000000..4a1b5b4b955
--- /dev/null
+++ b/doc/cla/individual/moutio.md
@@ -0,0 +1,9 @@
+Spain, 2018-01-29
+
+I hereby agree to the terms of the Odoo Individual Contributor License Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this declaration.
+
+Signed,
+
+Nacho Muñoz nacmuro@gmail.com https://github.com/moutio
diff --git a/doc/cla/individual/phucngta.md b/doc/cla/individual/phucngta.md
new file mode 100644
index 00000000000..c53fe2b7136
--- /dev/null
+++ b/doc/cla/individual/phucngta.md
@@ -0,0 +1,11 @@
+Vietnam, 2018.01.17
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Nguyen Phuc https://github.com/phucngta
\ No newline at end of file
diff --git a/doc/reference/module.rst b/doc/reference/module.rst
index aecb8de8a8f..9ec0ef39c93 100644
--- a/doc/reference/module.rst
+++ b/doc/reference/module.rst
@@ -1,5 +1,3 @@
-:banner: banners/module.jpg
-
=======
Modules
=======
@@ -84,6 +82,49 @@ Available manifest fields are:
to ``auto_install``. When both ``sale`` and ``crm`` are installed, it
automatically adds CRM campaigns tracking to sale orders without either
``sale`` or ``crm`` being aware of one another
+``external_dependencies`` (``dict(key=list(str))``)
+ A dictionary containing python and/or binary dependencies.
+
+ For python dependencies, the ``python`` key must be defined for this
+ dictionary and a list of python modules to be imported should be assigned
+ to it.
+
+ For binary dependencies, the ``bin`` key must be defined for this
+ dictionary and a list of binary executable names should be assigned to it.
+
+ The module won't be installed if either the python module is not installed
+ in the host machine or the binary executable is not found within the
+ host machine's PATH environment variable.
+``application`` (``bool``, default: ``False``)
+ Whether the module should be considered as a fully-fledged application
+ (``True``) or is just a technical module (``False``) that provides some
+ extra functionality to an existing application module.
+``css`` (``list(str)``)
+ Specify css files with custom rules to be imported, these files should be
+ located in ``static/src/css`` inside the module.
+``images`` (``list(str)``)
+ Specify image files to be used by the module.
+``installable`` (``bool`` default: ``False``)
+ Whether a user should be able to install the module from the Web UI or not.
+``maintainer`` (``str``)
+ Person or entity in charge of the maintenance of this module, by default
+ it is assumed that the author is the maintainer.
+``{pre_init, post_init, uninstall}_hook`` (``str``)
+ Hooks for module installation/uninstallation, their value should be a
+ string representing the name of a function defined inside the module's
+ ``__init__.py``.
+
+ ``pre_init_hook`` takes a cursor as its only argument, this function is
+ executed prior to the module's installation.
+
+ ``post_init_hook`` takes a cursor and a registry as its arguments, this
+ function is executed right after the module's installation.
+
+ ``uninstall_hook`` takes a cursor and a registry as its arguments, this
+ function is executed after the module's uninstallation.
+
+ These hooks should only be used when setup/cleanup required for this module
+ is either extremely difficult or impossible through the api.
.. _semantic versioning: http://semver.org
.. _existing categories:
diff --git a/doc/setup/deploy.rst b/doc/setup/deploy.rst
index 0d7303a795a..b0a5cf3224e 100644
--- a/doc/setup/deploy.rst
+++ b/doc/setup/deploy.rst
@@ -44,7 +44,7 @@ correctly.
Configuration samples
---------------------
-* show only databases with names beginning with 'mycompany'
+* Show only databases with names beginning with 'mycompany'
in ``/etc/odoo.conf`` set:
@@ -87,18 +87,18 @@ machine, and is the default when no host is provided, but if you want Odoo and
PostgreSQL to execute on different machines [#different-machines]_ it will
need to `listen to network interfaces`_ [#remote-socket]_, either:
-* only accept loopback connections and `use an SSH tunnel`_ between the
+* Only accept loopback connections and `use an SSH tunnel`_ between the
machine on which Odoo runs and the one on which PostgreSQL runs, then
configure Odoo to connect to its end of the tunnel
-* accept connections to the machine on which Odoo is installed, possibly
+* Accept connections to the machine on which Odoo is installed, possibly
over ssl (see `PostgreSQL connection settings`_ for details), then configure
Odoo to connect over the network
Configuration sample
--------------------
-* allow tcp connection on localhost
-* allow tcp connection from 192.168.1.x network
+* Allow tcp connection on localhost
+* Allow tcp connection from 192.168.1.x network
in ``/etc/postgresql/9.5/main/pg_hba.conf`` set:
@@ -134,11 +134,11 @@ create a new user (``odoo``) and set it as the database user.
simply checked before performing database alterations. It should be set to
a randomly generated value to ensure third parties can not use this
interface.
-* all database operations use the :ref:`database options
+* All database operations use the :ref:`database options
`, including the database management
screen. For the database management screen to work requires that the PostgreSQL user
have ``createdb`` right.
-* users can always drop databases they own. For the database management screen
+* Users can always drop databases they own. For the database management screen
to be completely non-functional, the PostgreSQL user needs to be created with
``no-createdb`` and the database must be owned by a different PostgreSQL user.
@@ -248,10 +248,10 @@ authentication information in cleartext. This means a secure deployment of
Odoo must use HTTPS\ [#switching]_. SSL termination can be implemented via
just about any SSL termination proxy, but requires the following setup:
-* enable Odoo's :option:`proxy mode `. This should only be enabled when Odoo is behind a reverse proxy
-* set up the SSL termination proxy (`Nginx termination example`_)
-* set up the proxying itself (`Nginx proxying example`_)
-* your SSL termination proxy should also automatically redirect non-secure
+* Enable Odoo's :option:`proxy mode `. This should only be enabled when Odoo is behind a reverse proxy
+* Set up the SSL termination proxy (`Nginx termination example`_)
+* Set up the proxying itself (`Nginx proxying example`_)
+* Your SSL termination proxy should also automatically redirect non-secure
connections to the secure port
.. warning::
@@ -263,8 +263,8 @@ just about any SSL termination proxy, but requires the following setup:
Configuration sample
--------------------
-* redirect http requests to https
-* proxy requests to odoo
+* Redirect http requests to https
+* Proxy requests to odoo
in ``/etc/odoo.conf`` set:
@@ -349,10 +349,10 @@ Cron Workers
To run cron jobs for an Odoo deployment as a WSGI application requires
-* a classical Odoo (run via ``odoo-bin``)
-* connected to the database in which cron jobs have to be run (via
+* A classical Odoo (run via ``odoo-bin``)
+* Connected to the database in which cron jobs have to be run (via
:option:`odoo-bin -d`)
-* which should not be exposed to the network. To ensure cron runners are not
+* Which should not be exposed to the network. To ensure cron runners are not
network-accessible, it is possible to disable the built-in HTTP server
entirely with :option:`odoo-bin --no-http` or setting ``http_enable = False``
in the configuration file
@@ -372,11 +372,11 @@ notifications.
The solutions to support livechat/motifications in a WSGI application are:
-* deploy a threaded version of Odoo (instread of a process-based preforking
+* Deploy a threaded version of Odoo (instread of a process-based preforking
one) and redirect only requests to URLs starting with ``/longpolling/`` to
that Odoo, this is the simplest and the longpolling URL can double up as
the cron instance.
-* deploy an evented Odoo via ``odoo-gevent`` and proxy requests starting
+* Deploy an evented Odoo via ``odoo-gevent`` and proxy requests starting
with ``/longpolling/`` to
:option:`the longpolling port `.
diff --git a/doc/webservices/localization.rst b/doc/webservices/localization.rst
index 17559b2a01a..39e1c98181c 100644
--- a/doc/webservices/localization.rst
+++ b/doc/webservices/localization.rst
@@ -1,12 +1,13 @@
:banner: banners/localization.jpg
-==========================
-Making my Own Localization
-==========================
+=======================
+Creating a Localization
+=======================
.. warning::
- This tutorial requires knowledges about how to build a module in Odoo.
+ This tutorial requires knowledges about how to build a module in Odoo (see
+ :doc:`../../howtos/backend`).
Building a localization module
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/odoo/addons/base/models/ir_actions.py b/odoo/addons/base/models/ir_actions.py
index d857c14593b..646615cb593 100644
--- a/odoo/addons/base/models/ir_actions.py
+++ b/odoo/addons/base/models/ir_actions.py
@@ -8,6 +8,7 @@ from odoo.tools.safe_eval import safe_eval, test_python_expr
from odoo.tools import pycompat
from odoo.http import request
+import base64
from collections import defaultdict
import datetime
import dateutil
@@ -76,6 +77,8 @@ class IrActions(models.Model):
'datetime': datetime,
'dateutil': dateutil,
'timezone': timezone,
+ 'b64encode': base64.b64encode,
+ 'b64decode': base64.b64decode,
}
@api.model
@@ -253,7 +256,7 @@ class IrActionsActWindowView(models.Model):
_name = 'ir.actions.act_window.view'
_table = 'ir_act_window_view'
_rec_name = 'view_id'
- _order = 'sequence'
+ _order = 'sequence,id'
sequence = fields.Integer()
view_id = fields.Many2one('ir.ui.view', string='View')
diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py
index 2915d9a0086..53a3438b21a 100644
--- a/odoo/addons/base/models/res_users.py
+++ b/odoo/addons/base/models/res_users.py
@@ -15,7 +15,6 @@ from odoo.exceptions import AccessDenied, AccessError, UserError, ValidationErro
from odoo.osv import expression
from odoo.service.db import check_super
from odoo.tools import partition, pycompat
-from odoo.http import root
_logger = logging.getLogger(__name__)
@@ -373,8 +372,6 @@ class Users(models.Model):
if any(key.startswith('context_') or key in ('lang', 'tz') for key in values):
self.context_get.clear_cache(self)
if any(key in values for key in ['active'] + USER_PRIVATE_FIELDS):
- # force deletion of all sessions for these users
- root.session_store.delete_sessions_for_uids(self.ids)
db = self._cr.dbname
for id in self.ids:
self.__uid_cache[db].pop(id, None)
@@ -388,10 +385,7 @@ class Users(models.Model):
db = self._cr.dbname
for id in self.ids:
self.__uid_cache[db].pop(id, None)
- res = super(Users, self).unlink()
- # force deletion of all sessions for these users
- root.session_store.delete_sessions_for_uids(self.ids)
- return res
+ return super(Users, self).unlink()
@api.model
def name_search(self, name='', args=None, operator='ilike', limit=100):
diff --git a/odoo/addons/base/tests/test_float.py b/odoo/addons/base/tests/test_float.py
index 1c2fcc3e91f..6e42f8adf1d 100644
--- a/odoo/addons/base/tests/test_float.py
+++ b/odoo/addons/base/tests/test_float.py
@@ -92,6 +92,18 @@ class TestFloatPrecision(TransactionCase):
try_round(1.8, '2', 0, method='UP')
try_round(-1.8, '-2', 0, method='UP')
+ # Try some rounding value with rounding method DOWN instead of HALF-UP
+ # We use 2.425 because when normalizing 2.425 with precision_digits=3 it gives
+ # us 2424.9999999999995 as value, and if not handle correctly the rounding DOWN
+ # value will be incorrect (should be 2.425 and not 2.424)
+ try_round(2.425, '2.425', method='DOWN')
+ try_round(2.4249, '2.424', method='DOWN')
+ try_round(-2.425, '-2.425', method='DOWN')
+ try_round(-2.4249, '-2.424', method='DOWN')
+ try_round(-2.500, '-2.500', method='DOWN')
+ try_round(1.8, '1', 0, method='DOWN')
+ try_round(-1.8, '-1', 0, method='DOWN')
+
# Extended float range test, inspired by Cloves Almeida's test on bug #882036.
fractions = [.0, .015, .01499, .675, .67499, .4555, .4555, .45555]
expecteds = ['.00', '.02', '.01', '.68', '.67', '.46', '.456', '.4556']
@@ -135,8 +147,8 @@ class TestFloatPrecision(TransactionCase):
try_compare(-657.4444, -657.445, 1)
# Rounding to unusual rounding units (e.g. coin values)
- def try_round(amount, expected, precision_rounding=None):
- value = float_round(amount, precision_rounding=precision_rounding)
+ def try_round(amount, expected, precision_rounding=None, method='HALF-UP'):
+ value = float_round(amount, precision_rounding=precision_rounding, rounding_method=method)
result = float_repr(value, precision_digits=2)
self.assertEqual(result, expected, 'Rounding error: got %s, expected %s' % (result, expected))
@@ -145,6 +157,8 @@ class TestFloatPrecision(TransactionCase):
try_round(457.3, '455.00', precision_rounding=5)
try_round(457.5, '460.00', precision_rounding=5)
try_round(457.1, '456.00', precision_rounding=3)
+ try_round(2.5, '2.50', precision_rounding=0.05, method='DOWN')
+ try_round(-2.5, '-2.50', precision_rounding=0.05, method='DOWN')
def test_rounding_04(self):
""" check that proper rounding is performed for float persistence """
diff --git a/odoo/http.py b/odoo/http.py
index 63d7ece05d5..4cfe76a76a7 100644
--- a/odoo/http.py
+++ b/odoo/http.py
@@ -635,8 +635,9 @@ class JsonRequest(WebRequest):
body = json.dumps(response, default=ustr)
return Response(
- body, headers=[('Content-Type', mime),
- ('Content-Length', len(body))])
+ body, status=error and error.pop('http_status', 200) or 200,
+ headers=[('Content-Type', mime), ('Content-Length', len(body))]
+ )
def _handle_exception(self, exception):
"""Called within an except block to allow converting exceptions
@@ -645,13 +646,18 @@ class JsonRequest(WebRequest):
try:
return super(JsonRequest, self)._handle_exception(exception)
except Exception:
- if not isinstance(exception, (odoo.exceptions.Warning, SessionExpiredException, odoo.exceptions.except_orm)):
+ if not isinstance(exception, (odoo.exceptions.Warning, SessionExpiredException,
+ odoo.exceptions.except_orm, werkzeug.exceptions.NotFound)):
_logger.exception("Exception during JSON request handling.")
error = {
'code': 200,
'message': "Odoo Server Error",
'data': serialize_exception(exception)
}
+ if isinstance(exception, werkzeug.exceptions.NotFound):
+ error['http_status'] = 404
+ error['code'] = 404
+ error['message'] = "404: Not Found"
if isinstance(exception, AuthenticationError):
error['code'] = 100
error['message'] = "Odoo Session Invalid"
@@ -1029,6 +1035,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
self.db = db
self.uid = uid
self.login = login
+ self.password = password
request.uid = uid
request.disable_db = False
@@ -1043,6 +1050,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
"""
if not self.db or not self.uid:
raise SessionExpiredException("Session expired")
+ security.check(self.db, self.uid, self.password)
def logout(self, keep_db=False):
for k in list(self):
@@ -1055,6 +1063,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
self.setdefault("db", None)
self.setdefault("uid", None)
self.setdefault("login", None)
+ self.setdefault("password", None)
self.setdefault("context", {})
def get_context(self):
@@ -1268,17 +1277,6 @@ class DisableCacheMiddleware(object):
start_response(status, new_headers)
return self.app(environ, start_wrapped)
-class OdooSessionStore(werkzeug.contrib.sessions.FilesystemSessionStore):
- def delete_sessions_for_uids(self, uids):
- # pretty expensive on large session stores, especially non-local!
- uids = set(uids)
- _logger.info('Deleting all HTTP sessions for UIDs %s', uids)
- for sid in self.list():
- s = self.get(sid)
- if s.uid and s.uid in uids:
- _logger.debug('Deleting session %s', sid)
- self.delete(s)
-
class Root(object):
"""Root WSGI application for the OpenERP Web Client.
"""
@@ -1290,7 +1288,7 @@ class Root(object):
# Setup http sessions
path = odoo.tools.config.session_dir
_logger.debug('HTTP sessions stored in: %s', path)
- return OdooSessionStore(path, session_class=OpenERPSession)
+ return werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
@lazy_property
def nodb_routing_map(self):
diff --git a/odoo/tools/float_utils.py b/odoo/tools/float_utils.py
index 33044d21f93..041a87cabbe 100644
--- a/odoo/tools/float_utils.py
+++ b/odoo/tools/float_utils.py
@@ -61,9 +61,6 @@ def float_round(value, precision_digits=None, precision_rounding=None, rounding_
# In order to easily support rounding to arbitrary 'steps' (e.g. coin values),
# we normalize the value before rounding it as an integer, and de-normalize
# after rounding: e.g. float_round(1.3, precision_rounding=.5) == 1.5
-
- # TIE-BREAKING: HALF-UP (for normal rounding)
- # We want to apply HALF-UP tie-breaking rules, i.e. 0.5 rounds away from 0.
# Due to IEE754 float/double representation limits, the approximation of the
# real value may be slightly below the tie limit, resulting in an error of
# 1 unit in the last place (ulp) after rounding.
@@ -74,25 +71,31 @@ def float_round(value, precision_digits=None, precision_rounding=None, rounding_
# Credit: discussion with OpenERP community members on bug 882036
normalized_value = value / rounding_factor # normalize
+ sign = math.copysign(1.0, normalized_value)
epsilon_magnitude = math.log(abs(normalized_value), 2)
epsilon = 2**(epsilon_magnitude-53)
- if rounding_method == 'HALF-UP':
- normalized_value += math.copysign(epsilon, normalized_value)
- rounded_value = round(normalized_value) # round to integer
- # TIE-BREAKING: UP (for ceiling[resp. flooring] operations)
- # When rounding the value up[resp. down], we instead subtract the epsilon value
+ # TIE-BREAKING: UP/DOWN (for ceiling[resp. flooring] operations)
+ # When rounding the value up[resp. down], we instead subtract[resp. add] the epsilon value
# as the the approximation of the real value may be slightly *above* the
# tie limit, this would result in incorrectly rounding up[resp. down] to the next number
# The math.ceil[resp. math.floor] operation is applied on the absolute value in order to
# round "away from zero" and not "towards infinity", then the sign is
# restored.
- else:
- func = math.floor if rounding_method == 'DOWN' else math.ceil
- sign = math.copysign(1.0, normalized_value)
+ if rounding_method == 'UP':
normalized_value -= sign*epsilon
- rounded_value = func(abs(normalized_value)) * sign
+ rounded_value = math.ceil(abs(normalized_value)) * sign
+
+ elif rounding_method == 'DOWN':
+ normalized_value += sign*epsilon
+ rounded_value = math.floor(abs(normalized_value)) * sign
+
+ # TIE-BREAKING: HALF-UP (for normal rounding)
+ # We want to apply HALF-UP tie-breaking rules, i.e. 0.5 rounds away from 0.
+ else:
+ normalized_value += math.copysign(epsilon, normalized_value)
+ rounded_value = round(normalized_value) # round to integer
result = rounded_value * rounding_factor # de-normalize
return result