diff --git a/addons/project_timesheet_holidays/models/res_company.py b/addons/project_timesheet_holidays/models/res_company.py index 3d21974573c..8dfe3ff580c 100644 --- a/addons/project_timesheet_holidays/models/res_company.py +++ b/addons/project_timesheet_holidays/models/res_company.py @@ -14,11 +14,8 @@ class Company(models.Model): 'project.task', string="Leave Task", domain="[('project_id', '=', leave_timesheet_project_id)]") - def _init_column(self, name): - super(Company, self)._init_column(name) - - if name == 'leave_timesheet_project_id': - self.search([('leave_timesheet_project_id', '=', False)])._create_leave_project_task() + def init(self): + self.search([('leave_timesheet_project_id', '=', False)])._create_leave_project_task() @api.model def create(self, values): diff --git a/addons/sale/tests/test_sale_common.py b/addons/sale/tests/test_sale_common.py index 85ecdeac108..ebea8f85c5f 100644 --- a/addons/sale/tests/test_sale_common.py +++ b/addons/sale/tests/test_sale_common.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - +from collections import OrderedDict from odoo.addons.account.tests.account_test_classes import AccountingTestCase @@ -27,11 +27,11 @@ class TestSale(AccountingTestCase): 'groups_id': [(6, 0, [group_user.id])] }) # create quotation with differend kinds of products (all possible combinations) - self.products = { - 'prod_order': self.env.ref('product.product_order_01'), - 'prod_del': self.env.ref('product.product_delivery_01'), - 'serv_order': self.env.ref('product.service_order_01'), - 'serv_del': self.env.ref('product.service_delivery'), - } + self.products = OrderedDict([ + ('prod_order', self.env.ref('product.product_order_01')), + ('serv_del', self.env.ref('product.service_delivery')), + ('serv_order', self.env.ref('product.service_order_01')), + ('prod_del', self.env.ref('product.product_delivery_01')), + ]) self.partner = self.env.ref('base.res_partner_1') diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 2f97218ce72..e0eb3c34c3a 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -20,7 +20,7 @@ from odoo.http import request from odoo.addons.website.models.website import slug from odoo.addons.web.controllers.main import WebClient, Binary, Home -from odoo.tools import pycompat +from odoo.tools import pycompat, OrderedSet logger = logging.getLogger(__name__) @@ -34,27 +34,29 @@ class QueryURL(object): def __init__(self, path='', path_args=None, **args): self.path = path self.args = args - self.path_args = set(path_args or []) + self.path_args = OrderedSet(path_args or []) def __call__(self, path=None, path_args=None, **kw): path = path or self.path for key, value in pycompat.items(self.args): kw.setdefault(key, value) - path_args = set(path_args or []).union(self.path_args) - paths, fragments = [], [] + path_args = OrderedSet(path_args or []) | self.path_args + paths, fragments = {}, [] for key, value in pycompat.items(kw): if value and key in path_args: if isinstance(value, models.BaseModel): - paths.append((key, slug(value))) + paths[key] = slug(value) else: - paths.append((key, value)) + paths[key] = u"%s" % value elif value: if isinstance(value, list) or isinstance(value, set): fragments.append(werkzeug.url_encode([(key, item) for item in value])) else: fragments.append(werkzeug.url_encode([(key, value)])) - for key, value in paths: - path += '/' + key + '/%s' % value + for key in path_args: + value = paths.get(key) + if value is not None: + path += '/' + key + '/' + value if fragments: path += '?' + '&'.join(fragments) return path diff --git a/addons/website_sale_digital/__manifest__.py b/addons/website_sale_digital/__manifest__.py index 973a3bd1817..84018a35c61 100644 --- a/addons/website_sale_digital/__manifest__.py +++ b/addons/website_sale_digital/__manifest__.py @@ -8,7 +8,7 @@ Sell digital product using attachments to virtual products """, 'depends': [ 'document', - 'website_sale', + 'website_portal_sale', ], 'installable': True, 'data': [ diff --git a/addons/website_sale_digital/controllers/main.py b/addons/website_sale_digital/controllers/main.py index 162daa5c0c8..2df86980bc6 100644 --- a/addons/website_sale_digital/controllers/main.py +++ b/addons/website_sale_digital/controllers/main.py @@ -7,7 +7,7 @@ from werkzeug.utils import redirect from odoo import http from odoo.http import request -from odoo.addons.website_portal.controllers.main import website_account +from odoo.addons.website_portal_sale.controllers.main import website_account from odoo.addons.website_sale.controllers.main import WebsiteSale class WebsiteSaleDigitalConfirmation(WebsiteSale): diff --git a/doc/python3.rst b/doc/python3.rst index ea14b6e4515..0b358ac9e3e 100644 --- a/doc/python3.rst +++ b/doc/python3.rst @@ -61,6 +61,35 @@ features whereas: generator delegation, pathlib, ...), you must *not *use them in Odoo until Python 2 support is dropped +Semantics changes +================= + +Dict & set iteration order ("Hash Randomisation") +------------------------------------------------- + +In Python 2, the iteration order depends on the value's hash (modulo the +collection's capacity and conflict resolution), which provides a +spec-undefined but implementation-defined order. While that's not supposed to +happen, it turns out code may depend on the specific order of iteration over +a hash collection (``dict`` or ``set``). + +Python 3.3 enables `hash randomisation`_ by default (this can be optionally +enabled on previous versions including Python 2 by providing the ``-R`` +command-line parameter), which means *the order of iteration changes from one +run to the next*. + +When discovered, this can be fixed by one of: + +* making iteration steps properly independent (removing the dependency of + order of iteration) +* using different checking method (e.g. when serialising sets or dictionaries + and checking against the specific serialised value) +* fixing dependencies +* using a ``collections.OrderedDict`` or ``odoo.tools.misc.OrderedSet`` instead + of a regular one, they guarantee order of iteration is order of insertion +* sorting the collection's items before iterating over them (this may require + adding some sort of iteration key to the items) + Moved and removed ================= @@ -340,6 +369,8 @@ Minor syntax changes error, octal literals now follow the hexadecimal convention with a ``0o`` prefix. +.. _hash randomisation: http://bugs.python.org/issue13703 + .. _requests: http://docs.python-requests.org/ .. _werkzeug: http://werkzeug.pocoo.org/docs/urls/ diff --git a/odoo/addons/base/res/res_company.py b/odoo/addons/base/res/res_company.py index 9dfdba96cba..98eff189f6e 100644 --- a/odoo/addons/base/res/res_company.py +++ b/odoo/addons/base/res/res_company.py @@ -27,6 +27,7 @@ class Company(models.Model): return currency_id or self._get_euro() name = fields.Char(related='partner_id.name', string='Company Name', required=True, store=True) + sequence = fields.Integer(help='Used to order Companies in the company switcher', default=10) parent_id = fields.Many2one('res.company', string='Parent Company', index=True) child_ids = fields.One2many('res.company', 'parent_id', string='Child Companies') partner_id = fields.Many2one('res.partner', string='Partner', required=True) @@ -52,7 +53,6 @@ class Company(models.Model): website = fields.Char(related='partner_id.website') vat = fields.Char(related='partner_id.vat', string="TIN") company_registry = fields.Char() - sequence = fields.Integer(help='Used to order Companies in the company switcher', default=10) paperformat_id = fields.Many2one('report.paperformat', 'Paper format', default=lambda self: self.env.ref('report.paperformat_euro', raise_if_not_found=False)) external_report_layout = fields.Selection([ ('background', 'Background'), diff --git a/odoo/addons/base/tests/test_mail_examples.py b/odoo/addons/base/tests/test_mail_examples.py index a83e668d940..dfecfe7c0d4 100644 --- a/odoo/addons/base/tests/test_mail_examples.py +++ b/odoo/addons/base/tests/test_mail_examples.py @@ -510,7 +510,7 @@ REMOVE_CLASS = """ """ REMOVE_CLASS_IN = [ - '
', + '
', 'An error occured in a modal and I will send you back the html to try opening one on your end'] REMOVE_CLASS_OUT = [ '