From ace4ee39e81a6da9e94236b5746f9ee70f8c9b95 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 17 May 2017 16:50:52 +0200 Subject: [PATCH 1/6] [FIX] P3: round() compatibility shim In Python 3 round: * rounds half to even (banker's rounding) rather than up * returns an int if not given a precision (or given a precision of None) * discards the negative sign when rounding to 0 Add a compatibility shim which implements P2 behaviour and at least allows passing float_utils's tests. Implementing float_round on top of Decimal was attempted but did not work: although Decimal.quantize takes a non-integral Decimal (e.g. ``d.quantize(D('0.001'))``) it only supports integral powers of 10 and does not support quantizing to e.g. 0.05: while that will not fail it will just use the exponent for quantization and 0.05 is thus equivalent to 0.01, which is not what we want if e.g. need to round a value to 5 cents. This is hinted at but maybe not clearly spelled out by the official documentation: > Return a value equal to the first operand after rounding > and **having the exponent of** the second operand. Also convert a bunch of hand-rolled currency rounding to just using the round() method on res.currency. --- addons/account/models/account_analytic_line.py | 2 +- addons/account/models/account_invoice.py | 9 ++++----- addons/purchase/models/account_invoice.py | 7 +++---- addons/resource/models/resource_mixin.py | 6 +++--- addons/stock_account/models/account_invoice.py | 2 +- odoo/tools/float_utils.py | 2 +- odoo/tools/pycompat.py | 17 +++++++++++++++++ 7 files changed, 30 insertions(+), 15 deletions(-) diff --git a/addons/account/models/account_analytic_line.py b/addons/account/models/account_analytic_line.py index 20b44c04dbb..1d212d31bf8 100644 --- a/addons/account/models/account_analytic_line.py +++ b/addons/account/models/account_analytic_line.py @@ -44,7 +44,7 @@ class AccountAnalyticLine(models.Model): # Compute based on pricetype amount_unit = self.product_id.price_compute('standard_price', uom=unit)[self.product_id.id] amount = amount_unit * self.unit_amount or 0.0 - result = round(amount, self.currency_id.decimal_places) * -1 + result = self.currency_id.round(amount) * -1 self.amount = result self.general_account_id = account self.product_uom_id = unit diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 24e8abc3049..37ec02186e9 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -1551,14 +1551,13 @@ class AccountPaymentTerm(models.Model): currency = self.env['res.currency'].browse(self.env.context['currency_id']) else: currency = self.env.user.company_id.currency_id - prec = currency.decimal_places for line in self.line_ids: if line.value == 'fixed': - amt = round(line.value_amount, prec) + amt = currency.round(line.value_amount) elif line.value == 'percent': - amt = round(value * (line.value_amount / 100.0), prec) + amt = currency.round(value * (line.value_amount / 100.0)) elif line.value == 'balance': - amt = round(amount, prec) + amt = currency.round(amount) if amt: next_date = fields.Date.from_string(date_ref) if line.option == 'day_after_invoice_date': @@ -1573,7 +1572,7 @@ class AccountPaymentTerm(models.Model): result.append((fields.Date.to_string(next_date), amt)) amount -= amt amount = sum(amt for _, amt in result) - dist = round(value - amount, prec) + dist = currency.round(value - amount) if dist: last_date = result and result[-1][0] or fields.Date.today() result.append((last_date, dist)) diff --git a/addons/purchase/models/account_invoice.py b/addons/purchase/models/account_invoice.py index c1c62f32da6..350e8ad8684 100644 --- a/addons/purchase/models/account_invoice.py +++ b/addons/purchase/models/account_invoice.py @@ -146,7 +146,6 @@ class AccountInvoice(models.Model): # reference_account_id is the stock input account reference_account_id = i_line.product_id.product_tmpl_id.get_product_accounts(fiscal_pos=fpos)['stock_input'].id diff_res = [] - account_prec = inv.company_id.currency_id.decimal_places # calculate and write down the possible price difference between invoice price and product price for line in res: if line.get('invl_id', 0) == i_line.id and reference_account_id == line['account_id']: @@ -180,13 +179,13 @@ class AccountInvoice(models.Model): if child.type_tax_use != 'none': tax_ids.append((4, child.id, None)) price_before = line.get('price', 0.0) - line.update({'price': round(valuation_price_unit * line['quantity'], account_prec)}) + line.update({'price': company_currency.round(valuation_price_unit * line['quantity'])}) diff_res.append({ 'type': 'src', 'name': i_line.name[:64], - 'price_unit': round(price_unit - valuation_price_unit, account_prec), + 'price_unit': company_currency.round(price_unit - valuation_price_unit), 'quantity': line['quantity'], - 'price': round(price_before - line.get('price', 0.0), account_prec), + 'price': company_currency.round(price_before - line.get('price', 0.0)), 'account_id': acc, 'product_id': line['product_id'], 'uom_id': line['uom_id'], diff --git a/addons/resource/models/resource_mixin.py b/addons/resource/models/resource_mixin.py index 8b922dd5ff8..0d45ec646a1 100644 --- a/addons/resource/models/resource_mixin.py +++ b/addons/resource/models/resource_mixin.py @@ -4,7 +4,7 @@ from datetime import timedelta from odoo import api, fields, models - +from odoo.tools import pycompat class ResourceMixin(models.AbstractModel): _name = "resource.mixin" @@ -47,7 +47,7 @@ class ResourceMixin(models.AbstractModel): theoric_hours = self.get_day_work_hours_count(day_intervals[0][0].date(), calendar=calendar) work_time = sum((interval[1] - interval[0] for interval in day_intervals), timedelta()) total_work_time += work_time - days_count += round((work_time.total_seconds() / 3600 / theoric_hours) * 4) / 4 + days_count += pycompat.round((work_time.total_seconds() / 3600 / theoric_hours) * 4) / 4 return { 'days': days_count, 'hours': total_work_time.total_seconds() / 3600, @@ -70,7 +70,7 @@ class ResourceMixin(models.AbstractModel): for day_intervals in calendar._iter_leave_intervals(from_datetime, to_datetime, self.resource_id.id): theoric_hours = self.get_day_work_hours_count(day_intervals[0][0].date(), calendar=calendar) leave_time = sum((interval[1] - interval[0] for interval in day_intervals), timedelta()) - days_count += round((leave_time.total_seconds()/3600 / theoric_hours) * 4) / 4 + days_count += pycompat.round((leave_time.total_seconds() / 3600 / theoric_hours) * 4) / 4 return days_count def iter_leaves(self, from_datetime, to_datetime, calendar=None): diff --git a/addons/stock_account/models/account_invoice.py b/addons/stock_account/models/account_invoice.py index 6291da2922f..281d15de0e3 100644 --- a/addons/stock_account/models/account_invoice.py +++ b/addons/stock_account/models/account_invoice.py @@ -94,7 +94,7 @@ class AccountInvoiceLine(models.Model): price = company_currency.with_context(date=self.invoice_id.date_invoice).compute(price_unit * self.quantity, self.invoice_id.currency_id) else: price = price_unit * self.quantity - return round(price, self.invoice_id.currency_id.decimal_places) + return self.invoice_id.currency_id.round(price) def get_invoice_line_account(self, type, product, fpos, company): if company.anglo_saxon_accounting and type in ('in_invoice', 'in_refund') and product and product.type == 'product': diff --git a/odoo/tools/float_utils.py b/odoo/tools/float_utils.py index 9c37da07fe9..d71576adc92 100644 --- a/odoo/tools/float_utils.py +++ b/odoo/tools/float_utils.py @@ -58,7 +58,7 @@ def float_round(value, precision_digits=None, precision_rounding=None, rounding_ 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 + rounded_value = pycompat.round(normalized_value) # round to integer # TIE-BREAKING: UP (for ceiling operations) # When rounding the value up, we instead subtract the epsilon value diff --git a/odoo/tools/pycompat.py b/odoo/tools/pycompat.py index 7ce4f45773a..19b821863b2 100644 --- a/odoo/tools/pycompat.py +++ b/odoo/tools/pycompat.py @@ -8,6 +8,7 @@ PY2 = sys.version_info[0] == 2 if PY2: # pylint: disable=long-builtin,dict-iter-method integer_types = (int, long) + round = round keys = lambda d: iter(d.iterkeys()) values = lambda d: iter(d.itervalues()) @@ -23,8 +24,24 @@ if PY2: exec ('def reraise(tp, value, tb=None):\n raise tp, value, tb') else: + import builtins, math # pylint: disable=bad-functions integer_types = (int,) + def round(f): + # P3's builtin round differs from P2 in the following manner: + # * it rounds half to even rather than up (away from 0) + # * round(-0.) loses the sign (it returns -0 rather than 0) + # * round(x) returns an int rather than a float + # + # this compatibility shim implements Python 2's round in terms of + # Python 3's so that important rounding error under P3 can be + # trivially fixed, assuming the P2 behaviour to be debugged and + # correct. + roundf = builtins.round(f) + if builtins.round(f + 1) - roundf != 1: + return f + math.copysign(0.5, f) + # copysign ensures round(-0.) -> -0 *and* result is a float + return math.copysign(roundf, f) keys = lambda d: iter(d.keys()) values = lambda d: iter(d.values()) From 17fe075cda4676cd5e6e3455e610f120d0ddc251 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 17 May 2017 10:01:47 +0200 Subject: [PATCH 2/6] [FIX] P3: absolute ordering removed --- addons/account/models/account_move.py | 10 +++++----- addons/account/report/account_partner_ledger.py | 2 +- addons/crm/models/crm_lead.py | 4 ++-- addons/mail/controllers/main.py | 5 ++--- addons/membership/models/account_invoice.py | 6 +++--- addons/survey/tests/test_survey.py | 4 +--- addons/web_editor/models/ir_qweb.py | 4 ++-- addons/website_blog/models/website_blog.py | 2 +- addons/website_customer/controllers/main.py | 2 +- addons/website_event_track/controllers/main.py | 2 +- odoo/addons/test_read_group/tests/test_empty.py | 2 +- 11 files changed, 20 insertions(+), 23 deletions(-) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index a47f232f3f2..ca46bbb5612 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -192,10 +192,10 @@ class AccountMove(models.Model): @api.multi def _check_lock_date(self): for move in self: - lock_date = max(move.company_id.period_lock_date, move.company_id.fiscalyear_lock_date) + lock_date = max(move.company_id.period_lock_date or '0000-00-00', move.company_id.fiscalyear_lock_date or '0000-00-00') if self.user_has_groups('account.group_account_manager'): lock_date = move.company_id.fiscalyear_lock_date - if move.date <= lock_date: + if move.date <= (lock_date or '0000-00-00'): if self.user_has_groups('account.group_account_manager'): message = _("You cannot add/modify entries prior to and inclusive of the lock date %s") % (lock_date) else: @@ -1029,7 +1029,7 @@ class AccountMoveLine(models.Model): partial_rec_set = self.env['account.partial.reconcile'] aml_id = False partial_rec_id = False - maxdate = None + maxdate = '0000-00-00' for aml in self: total_debit += aml.debit total_credit += aml.credit @@ -1491,7 +1491,7 @@ class AccountPartialReconcile(models.Model): # The move date should be the maximum date between payment and invoice # (in case of payment in advance). However, we should make sure the # move date is not recorded after the end of year closing. - if move_date > self.company_id.fiscalyear_lock_date: + if (move_date or '0000-00-00' > self.company_id.fiscalyear_lock_date or '0000-00-00'): res['date'] = move_date return res @@ -1689,7 +1689,7 @@ class AccountPartialReconcile(models.Model): #make sure that all partial reconciliations share the same secondary currency otherwise it's not #possible to compute the exchange difference entry and it has to be done manually. currency = list(partial_rec_set)[0].currency_id - maxdate = None + maxdate = '0000-00-00' aml_to_balance = None for partial_rec in partial_rec_set: if partial_rec.currency_id != currency: diff --git a/addons/account/report/account_partner_ledger.py b/addons/account/report/account_partner_ledger.py index b3e0b03b6ba..f22ef71527c 100644 --- a/addons/account/report/account_partner_ledger.py +++ b/addons/account/report/account_partner_ledger.py @@ -109,7 +109,7 @@ class ReportPartnerLedger(models.AbstractModel): self.env.cr.execute(query, tuple(params)) partner_ids = [res['partner_id'] for res in self.env.cr.dictfetchall()] partners = obj_partner.browse(partner_ids) - partners = sorted(partners, key=lambda x: (x.ref, x.name)) + partners = sorted(partners, key=lambda x: (x.ref or '', x.name or '')) return { 'doc_ids': partner_ids, diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 040ddd097d3..76f8a286205 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -298,9 +298,9 @@ class Lead(models.Model): # stage change with new stage: update probability and date_closed if vals.get('stage_id') and 'probability' not in vals: vals.update(self._onchange_stage_id_values(vals.get('stage_id'))) - if vals.get('probability') >= 100 or not vals.get('active', True): + if vals.get('probability', 0) >= 100 or not vals.get('active', True): vals['date_closed'] = fields.Datetime.now() - elif 'probability' in vals and vals['probability'] < 100: + elif 'probability' in vals: vals['date_closed'] = False return super(Lead, self).write(vals) diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py index 95165441dfd..cfa5654b0cd 100644 --- a/addons/mail/controllers/main.py +++ b/addons/mail/controllers/main.py @@ -7,7 +7,6 @@ import logging import psycopg2 import werkzeug -from operator import itemgetter from werkzeug import url_encode from odoo import api, http, registry, SUPERUSER_ID, _ @@ -154,10 +153,10 @@ class MailController(http.Controller): 'default': subtype.default, 'internal': subtype.internal, 'followed': subtype.id in followers.mapped('subtype_ids').ids, - 'parent_model': subtype.parent_id and subtype.parent_id.res_model or False, + 'parent_model': subtype.parent_id.res_model, 'id': subtype.id } for subtype in subtypes] - subtypes_list = sorted(subtypes_list, key=itemgetter('parent_model', 'res_model', 'internal', 'sequence')) + subtypes_list = sorted(subtypes_list, key=lambda it: (it['parent_model'] or '', it['res_model'] or '', it['internal'], it['sequence'])) return subtypes_list @http.route('/mail/view', type='http', auth='none') diff --git a/addons/membership/models/account_invoice.py b/addons/membership/models/account_invoice.py index 273dd4290ba..73622ce0fb2 100644 --- a/addons/membership/models/account_invoice.py +++ b/addons/membership/models/account_invoice.py @@ -60,9 +60,9 @@ class AccountInvoiceLine(models.Model): invoice_line.product_id.membership and \ not MemberLine.search([('account_invoice_line', '=', invoice_line.id)]): # Product line is a membership product - date_from = invoice_line.product_id.membership_date_from - date_to = invoice_line.product_id.membership_date_to - if invoice_line.invoice_id.date_invoice > date_from and invoice_line.invoice_id.date_invoice < date_to: + date_from = invoice_line.product_id.membership_date_from or '0000-00-00' + date_to = invoice_line.product_id.membership_date_to or '0000-00-00' + if date_from < (invoice_line.invoice_id.date_invoice or '0000-00-00') < date_to: date_from = invoice_line.invoice_id.date_invoice MemberLine.create({ 'partner': invoice_line.invoice_id.partner_id.id, diff --git a/addons/survey/tests/test_survey.py b/addons/survey/tests/test_survey.py index 699aac0cde3..04a65a02562 100644 --- a/addons/survey/tests/test_survey.py +++ b/addons/survey/tests/test_survey.py @@ -226,9 +226,7 @@ class TestSurvey(TransactionCase): lines = [line.value_suggested.id for line in question.user_input_line_ids] answers = [{'text': label.value, 'count': lines.count(label.id), 'answer_id': label.id} for label in question.labels_ids] prp_result = self.env['survey.survey'].prepare_result(question)['answers'] - answers.sort() - prp_result.sort() - self.assertEqual(prp_result, answers, msg="Statistics of simple, multiple choice questions are different from expectation") + self.assertItemsEqual(prp_result, answers, msg="Statistics of simple, multiple choice questions are different from expectation") def test_11_survey_result_matrix(self): question = self.env['survey.question'].sudo(self.survey_manager).create({ diff --git a/addons/web_editor/models/ir_qweb.py b/addons/web_editor/models/ir_qweb.py index fcab2902a3b..4cceec10421 100644 --- a/addons/web_editor/models/ir_qweb.py +++ b/addons/web_editor/models/ir_qweb.py @@ -510,7 +510,7 @@ def _realize_padding(it): requests for at least n newlines of padding. Runs thereof can be collapsed into the largest requests and converted to newlines. """ - padding = None + padding = 0 for item in it: if isinstance(item, int): padding = max(padding, item) @@ -518,7 +518,7 @@ def _realize_padding(it): if padding: yield '\n' * padding - padding = None + padding = 0 yield item # leftover padding irrelevant as the output will be stripped diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py index 22456302949..979ae14c5d3 100644 --- a/addons/website_blog/models/website_blog.py +++ b/addons/website_blog/models/website_blog.py @@ -209,7 +209,7 @@ class BlogPost(models.Model): def write(self, vals): self.ensure_one() if 'website_published' in vals and 'published_date' not in vals: - if self.published_date <= fields.Datetime.now(): + if (self.published_date or '') <= fields.Datetime.now(): vals['published_date'] = vals['website_published'] and fields.Datetime.now() result = super(BlogPost, self).write(vals) self._check_for_publication(vals) diff --git a/addons/website_customer/controllers/main.py b/addons/website_customer/controllers/main.py index d6b10b37dc1..e7318a64abf 100644 --- a/addons/website_customer/controllers/main.py +++ b/addons/website_customer/controllers/main.py @@ -73,7 +73,7 @@ class WebsiteCustomer(http.Controller): 'country_id_count': 0, 'country_id': (country.id, country.name) }) - countries.sort(key=lambda d: d['country_id'] and d['country_id'][1]) + countries.sort(key=lambda d: (d['country_id'] or (0, ""))[1]) countries.insert(0, { 'country_id_count': country_count, diff --git a/addons/website_event_track/controllers/main.py b/addons/website_event_track/controllers/main.py index 3fb37210589..1864b28379c 100644 --- a/addons/website_event_track/controllers/main.py +++ b/addons/website_event_track/controllers/main.py @@ -68,7 +68,7 @@ class WebsiteEventTrackController(http.Controller): @http.route(['''/event/ Date: Mon, 15 May 2017 16:37:07 +0200 Subject: [PATCH 3/6] [FIX] P3: / has become 'real division' in all cases --- .../views/event_track_templates.xml | 2 +- addons/website_forum/controllers/main.py | 5 ++--- addons/website_sale/controllers/main.py | 14 +++++++------- odoo/addons/base/tests/test_api.py | 2 +- odoo/tools/image.py | 11 +++++------ 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/addons/website_event_track/views/event_track_templates.xml b/addons/website_event_track/views/event_track_templates.xml index c8282180215..14eee014c6f 100644 --- a/addons/website_event_track/views/event_track_templates.xml +++ b/addons/website_event_track/views/event_track_templates.xml @@ -15,7 +15,7 @@

Our Sponsors

-
+
', 1), ('website_published', '=', True)], limit=step, offset=pager['offset'], order='karma DESC') # put the users in block of 3 to display them as a table - users = [[] for i in range(len(user_obj) / 3 + 1)] + users = [[] for i in range(len(user_obj) // 3 + 1)] for index, user in enumerate(user_obj): - users[index / 3].append(user) + users[index // 3].append(user) searches['users'] = 'True' values = self._prepare_forum_values(forum=forum, searches=searches) diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 05abe7627b1..8ec73048832 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -53,28 +53,28 @@ class TableCompute(object): x = y = 1 pos = minpos - while not self._check_place(pos % PPR, pos / PPR, x, y): + while not self._check_place(pos % PPR, pos // PPR, x, y): pos += 1 # if 21st products (index 20) and the last line is full (PPR products in it), break # (pos + 1.0) / PPR is the line where the product would be inserted # maxy is the number of existing lines # + 1.0 is because pos begins at 0, thus pos 20 is actually the 21st block # and to force python to not round the division operation - if index >= ppg and ((pos + 1.0) / PPR) > maxy: + if index >= ppg and ((pos + 1.0) // PPR) > maxy: break if x == 1 and y == 1: # simple heuristic for CPU optimization - minpos = pos / PPR + minpos = pos // PPR for y2 in range(y): for x2 in range(x): - self.table[(pos / PPR) + y2][(pos % PPR) + x2] = False - self.table[pos / PPR][pos % PPR] = { + self.table[(pos // PPR) + y2][(pos % PPR) + x2] = False + self.table[pos // PPR][pos % PPR] = { 'product': p, 'x': x, 'y': y, 'class': " ".join(x.html_class for x in p.website_style_ids if x.html_class) } if index <= ppg: - maxy = max(maxy, y + (pos / PPR)) + maxy = max(maxy, y + (pos // PPR)) index += 1 # Format table according to HTML needs @@ -909,7 +909,7 @@ class WebsiteSale(http.Controller): @http.route(['/shop/get_unit_price'], type='json', auth="public", methods=['POST'], website=True) def get_unit_price(self, product_ids, add_qty, **kw): products = request.env['product.product'].with_context({'quantity': add_qty}).browse(product_ids) - return {product.id: product.website_price / add_qty for product in products} + return {product.id: product.website_price // add_qty for product in products} # ------------------------------------------------------ # Edit diff --git a/odoo/addons/base/tests/test_api.py b/odoo/addons/base/tests/test_api.py index 55cf1ff26b5..ad2dd1f0e85 100644 --- a/odoo/addons/base/tests/test_api.py +++ b/odoo/addons/base/tests/test_api.py @@ -482,7 +482,7 @@ class TestAPI(common.TransactionCase): ps = self.env['res.partner'].search([]) # sort by model order - qs = ps[:len(ps) / 2] + ps[len(ps) / 2:] + qs = ps[:len(ps) // 2] + ps[len(ps) // 2:] self.assertEqual(qs.sorted().ids, ps.ids) # sort by name, with a function or a field name diff --git a/odoo/tools/image.py b/odoo/tools/image.py index d6dd1488aba..791d1a978d3 100644 --- a/odoo/tools/image.py +++ b/odoo/tools/image.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - try: import cStringIO as StringIO except ImportError: @@ -102,7 +101,7 @@ def image_resize_and_sharpen(image, size, preserve_aspect_ratio=False, factor=2. resized_image = sharpener.enhance(factor) # create a transparent image for background and paste the image on it image = Image.new('RGBA', size, (255, 255, 255, 0)) - image.paste(resized_image, ((size[0] - resized_image.size[0]) / 2, (size[1] - resized_image.size[1]) / 2)) + image.paste(resized_image, ((size[0] - resized_image.size[0]) // 2, (size[1] - resized_image.size[1]) // 2)) if image.mode != origin_mode: image = image.convert(origin_mode) return image @@ -181,17 +180,17 @@ def crop_image(data, type='top', ratio=False, thumbnail_ratio=None, image_format if ratio: w_ratio, h_ratio = ratio - new_h = (w * h_ratio) / w_ratio + new_h = (w * h_ratio) // w_ratio new_w = w if new_h > h: new_h = h - new_w = (h * w_ratio) / h_ratio + new_w = (h * w_ratio) // h_ratio if type == "top": cropped_image = image_stream.crop((0, 0, new_w, new_h)) cropped_image.save(output_stream, format=image_format) elif type == "center": - cropped_image = image_stream.crop(((w - new_w) / 2, (h - new_h) / 2, (w + new_w) / 2, (h + new_h) / 2)) + cropped_image = image_stream.crop(((w - new_w) // 2, (h - new_h) // 2, (w + new_w) // 2, (h + new_h) // 2)) cropped_image.save(output_stream, format=image_format) elif type == "bottom": cropped_image = image_stream.crop((0, h - new_h, new_w, h)) @@ -201,7 +200,7 @@ def crop_image(data, type='top', ratio=False, thumbnail_ratio=None, image_format # TDE FIXME: should not have a ratio, makes no sense -> should have maximum width (std: 64; 256 px) if thumbnail_ratio: thumb_image = Image.open(StringIO.StringIO(output_stream.getvalue())) - thumb_image.thumbnail((new_w / thumbnail_ratio, new_h / thumbnail_ratio), Image.ANTIALIAS) + thumb_image.thumbnail((new_w // thumbnail_ratio, new_h // thumbnail_ratio), Image.ANTIALIAS) thumb_image.save(output_stream, image_format) return output_stream.getvalue().encode('base64') From 3cad551a88d206950e31be1ae6d64a35febfadd4 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 17 May 2017 13:56:46 +0200 Subject: [PATCH 4/6] [FIX] P3: stupid use of hasattr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In Python 2, hasattr swallows *any* exception raised during the access. In Python 3, it only swallows AttributeError. Accessing an attribute on an unbound threadlocal proxy raises RuntimeError, while running test_ir_http_mimetype ``request`` is unbound, in Python 2 ``hasattr`` would return ``False`` on an unbound threadlocal, in Python 3 it lets ``RuntimeError`` pass through blowing up the test. Fix this by just checking if the threadlocal is bound by using it as a boolean… --- odoo/addons/base/ir/ir_http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odoo/addons/base/ir/ir_http.py b/odoo/addons/base/ir/ir_http.py index ff4654cf6c9..0aa1abc52ca 100644 --- a/odoo/addons/base/ir/ir_http.py +++ b/odoo/addons/base/ir/ir_http.py @@ -323,7 +323,7 @@ class IrHttp(models.AbstractModel): headers += [('Content-Type', mimetype), ('X-Content-Type-Options', 'nosniff')] # cache - etag = hasattr(request, 'httprequest') and request.httprequest.headers.get('If-None-Match') + etag = bool(request) and request.httprequest.headers.get('If-None-Match') retag = '"%s"' % hashlib.md5(last_update).hexdigest() status = status or (304 if etag == retag else 200) headers.append(('ETag', retag)) From bc3cbe9eca892460b0bc21f59eda946f5052bfd1 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 26 May 2017 13:07:33 +0200 Subject: [PATCH 5/6] [FIX] P3: surprise, time.time() isn't falsy https://bugs.python.org/issue13936 That's a pretty sensible change, but it turns out we did have some bits of resource which implicitly depended on the old behaviour. --- addons/resource/models/resource.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py index cd26abaabc8..b50faa855fc 100644 --- a/addons/resource/models/resource.py +++ b/addons/resource/models/resource.py @@ -420,8 +420,12 @@ class ResourceCalendar(models.Model): dtstart=start_dt, until=end_dt, byweekday=self._get_weekdays()): - start_time = day.date() == start_dt.date() and start_dt.time() or datetime.time.min - end_time = day.date() == end_dt.date() and end_dt.time() or datetime.time.max + start_time = datetime.time.min + if day.date() == start_dt.date(): + start_time = start_dt.time() + end_time = datetime.time.max + if day.date() == end_dt.date() and end_dt.time() != datetime.time(): + end_time = end_dt.time() intervals = self._get_day_work_intervals( day.date(), @@ -445,8 +449,12 @@ class ResourceCalendar(models.Model): dtstart=start_dt, until=end_dt, byweekday=self._get_weekdays()): - start_time = day.date() == start_dt.date() and start_dt.time() or datetime.time.min - end_time = day.date() == end_dt.date() and end_dt.time() or datetime.time.max + start_time = datetime.time.min + if day.date() == start_dt.date(): + start_time = start_dt.time() + end_time = datetime.time.max + if day.date() == end_dt.date() and end_dt.time() != datetime.time(): + end_time = end_dt.time() intervals = self._get_day_leave_intervals( day.date(), From e26a212304199235df4f3dacc37b390f695086fd Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 1 Jun 2017 11:38:22 +0200 Subject: [PATCH 6/6] [FIX] P3: reorder finders in meta_path So that was a fun one: mock.patch calls would regularly fail refusing to find the addon in odoo.addon (e.g. essentially getattr(odoo.addon, 'account_budget' deep within the bowels of mock). Turns out the answer is that our import hooks would not be used for many imports: while in Python 2, sys.meta_path is empty and the default finders are run after all meta_path finders fail as noted by the documentation[0]. However when the import system was rewritten in Python 3.3[1] meta_path was "despecialised" and the default finders were moved to meta_path rather than be a hidden part of the import machinery[2]: > sys.meta_path and sys.path_hooks now store all of the meta path > finders and path entry hooks used by import. Previously the finders > were implicit and hidden within the C code of import instead of > being directly exposed. The result of this change is that ``sys.meta_path.append`` means the default finders should take priority and the custom ones should be fallback. This is the exact opposite of what we want. Fix issue by ``sys.meta_path.insert``-ing our finders at the start of the path rather than appending them at the end. This should change nothing in Python 2 but seems to fix the issue in P3. [0] https://docs.python.org/2/library/sys.html?highlight=meta_path#sys.meta_path [1] https://docs.python.org/3/whatsnew/3.3.html#importlib [2] https://docs.python.org/3/whatsnew/3.3.html#visible-changes --- odoo/modules/module.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/odoo/modules/module.py b/odoo/modules/module.py index e6194a5fb10..f0836cbe041 100644 --- a/odoo/modules/module.py +++ b/odoo/modules/module.py @@ -149,8 +149,8 @@ def initialize_sys_path(): ad_paths.append(ad) if not hooked: - sys.meta_path.append(AddonsHook()) - sys.meta_path.append(OdooHook()) + sys.meta_path.insert(0, OdooHook()) + sys.meta_path.insert(0, AddonsHook()) hooked = True def get_module_path(module, downloaded=False, display_warning=True):