diff --git a/doc/cla/individual/mtantin.md b/doc/cla/individual/mtantin.md
new file mode 100644
index 00000000000..b3aee232ccf
--- /dev/null
+++ b/doc/cla/individual/mtantin.md
@@ -0,0 +1,11 @@
+France, 2017-11-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,
+
+Maximilien TANTIN https://github.com/MTantin
\ No newline at end of file
diff --git a/doc/cla/individual/ruter.md b/doc/cla/individual/ruter.md
new file mode 100644
index 00000000000..1560ba2b7f3
--- /dev/null
+++ b/doc/cla/individual/ruter.md
@@ -0,0 +1,11 @@
+China, 2017-12-05
+
+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,
+
+Ruter ruter.lv@gmail.com https://github.com/ruter
diff --git a/doc/cla/individual/satriani-vai.md b/doc/cla/individual/satriani-vai.md
new file mode 100644
index 00000000000..df2c38eb7be
--- /dev/null
+++ b/doc/cla/individual/satriani-vai.md
@@ -0,0 +1,11 @@
+Germany, 2017-12-01
+
+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,
+
+Alex Vai satriani-vai@users.noreply.github.com https://github.com/satriani-vai
diff --git a/odoo/addons/base/ir/ir_cron.py b/odoo/addons/base/ir/ir_cron.py
index 2aa7a1e2773..375d9a966da 100644
--- a/odoo/addons/base/ir/ir_cron.py
+++ b/odoo/addons/base/ir/ir_cron.py
@@ -5,7 +5,7 @@ import threading
import time
import psycopg2
import pytz
-from datetime import datetime
+from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
import odoo
@@ -15,6 +15,7 @@ from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
BASE_VERSION = odoo.modules.load_information_from_description_file('base')['version']
+MAX_FAIL_TIME = timedelta(hours=5) # chosen with a fair roll of the dice
class BadVersion(Exception):
@@ -170,7 +171,7 @@ class ir_cron(models.Model):
(version,) = cr.fetchone()
cr.execute("SELECT COUNT(*) FROM ir_module_module WHERE state LIKE %s", ['to %'])
(changes,) = cr.fetchone()
- if not version or changes:
+ if version is None:
raise BadModuleState()
elif version != BASE_VERSION:
raise BadVersion()
@@ -181,6 +182,19 @@ class ir_cron(models.Model):
ORDER BY priority""")
jobs = cr.dictfetchall()
+ if changes:
+ if not jobs:
+ raise BadModuleState()
+ # nextcall is never updated if the cron is not executed,
+ # it is used as a sentinel value to check whether cron jobs
+ # have been locked for a long time (stuck)
+ parse = fields.Datetime.from_string
+ oldest = min([parse(job['nextcall']) for job in jobs])
+ if datetime.now() - oldest > MAX_FAIL_TIME:
+ odoo.modules.reset_modules_state(db_name)
+ else:
+ raise BadModuleState()
+
for job in jobs:
lock_cr = db.cursor()
try:
diff --git a/odoo/models.py b/odoo/models.py
index ec219398d78..e7be6c5b381 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -1651,7 +1651,8 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
order = '"%s" %s' % (order_field, '' if len(order_split) == 1 else order_split[1])
orderby_terms.append(order)
elif order_field in aggregated_fields:
- orderby_terms.append(order_part)
+ order_split[0] = '"' + order_field + '"'
+ orderby_terms.append(' '.join(order_split))
else:
# Cannot order by a field that will not appear in the results (needs to be grouped or aggregated)
_logger.warn('%s: read_group order by `%s` ignored, cannot sort on empty columns (not grouped/aggregated)',
diff --git a/odoo/modules/__init__.py b/odoo/modules/__init__.py
index 530f81ff5e3..b223df99280 100644
--- a/odoo/modules/__init__.py
+++ b/odoo/modules/__init__.py
@@ -7,7 +7,7 @@
from . import db, graph, loading, migration, module, registry
-from odoo.modules.loading import load_modules
+from odoo.modules.loading import load_modules, reset_modules_state
from odoo.modules.module import (
adapt_version,
diff --git a/odoo/modules/loading.py b/odoo/modules/loading.py
index d8ee12c1690..81c90e2b728 100644
--- a/odoo/modules/loading.py
+++ b/odoo/modules/loading.py
@@ -431,3 +431,24 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
finally:
cr.close()
+
+
+def reset_modules_state(db_name):
+ """
+ Resets modules flagged as "to x" to their original state
+ """
+ # Warning, this function was introduced in response to commit 763d714
+ # which locks cron jobs for dbs which have modules marked as 'to %'.
+ # The goal of this function is to be called ONLY when module
+ # installation/upgrade/uninstallation fails, which is the only known case
+ # for which modules can stay marked as 'to %' for an indefinite amount
+ # of time
+ db = odoo.sql_db.db_connect(db_name)
+ with db.cursor() as cr:
+ cr.execute(
+ "UPDATE ir_module_module SET state='installed' WHERE state IN ('to remove', 'to upgrade')"
+ )
+ cr.execute(
+ "UPDATE ir_module_module SET state='uninstalled' WHERE state='to install'"
+ )
+ _logger.warning("Transient module states were reset")
diff --git a/odoo/modules/registry.py b/odoo/modules/registry.py
index 1efc6aa195f..5c1c5111cf0 100644
--- a/odoo/modules/registry.py
+++ b/odoo/modules/registry.py
@@ -81,7 +81,11 @@ class Registry(Mapping):
try:
registry.setup_signaling()
# This should be a method on Registry
- odoo.modules.load_modules(registry._db, force_demo, status, update_module)
+ try:
+ odoo.modules.load_modules(registry._db, force_demo, status, update_module)
+ except Exception:
+ odoo.modules.reset_modules_state(db_name)
+ raise
except Exception:
_logger.exception('Failed to load registry')
del cls.registries[db_name]