diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 29fcaa17c8e..daa407d636f 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -5,7 +5,7 @@ import math from odoo.osv import expression from odoo.tools.float_utils import float_round as round -from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, pycompat +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT from odoo.exceptions import UserError, ValidationError from odoo import api, fields, models, _ 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 d5630cdc134..ee0d1903fd0 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -395,7 +395,7 @@ class AccountInvoice(models.Model): '_onchange_partner_id': ['account_id', 'payment_term_id', 'fiscal_position_id', 'partner_bank_id'], '_onchange_journal_id': ['currency_id'], } - for onchange_method, changed_fields in pycompat.items(onchanges): + for onchange_method, changed_fields in onchanges.items(): if any(f not in vals for f in changed_fields): invoice = self.new(vals) getattr(invoice, onchange_method)() @@ -516,7 +516,7 @@ class AccountInvoice(models.Model): tax_grouped = invoice.get_taxes_values() # Create new tax lines - for tax in pycompat.values(tax_grouped): + for tax in tax_grouped.values(): account_invoice_tax.create(tax) # dummy write on self to trigger recomputations @@ -535,7 +535,7 @@ class AccountInvoice(models.Model): def _onchange_invoice_line_ids(self): taxes_grouped = self.get_taxes_values() tax_lines = self.tax_line_ids.filtered('manual') - for tax in pycompat.values(taxes_grouped): + for tax in taxes_grouped.values(): tax_lines += tax_lines.new(tax) self.tax_line_ids = tax_lines return @@ -886,7 +886,7 @@ class AccountInvoice(models.Model): else: line2[tmp] = l line = [] - for key, val in pycompat.items(line2): + for key, val in line2.items(): line.append((0, 0, val)) return line @@ -1079,7 +1079,7 @@ class AccountInvoice(models.Model): result = [] for line in lines: values = {} - for name, field in pycompat.items(line._fields): + for name, field in line._fields.items(): if name in MAGIC_COLUMNS: continue elif field.type == 'many2one': @@ -1249,7 +1249,7 @@ class AccountInvoice(models.Model): for line in self.tax_line_ids: res.setdefault(line.tax_id.tax_group_id, 0.0) res[line.tax_id.tax_group_id] += line.amount - res = sorted(pycompat.items(res), key=lambda l: l[0].sequence) + res = sorted(res.items(), key=lambda l: l[0].sequence) res = [(l[0].name, l[1]) for l in res] return res @@ -1350,7 +1350,7 @@ class AccountInvoiceLine(models.Model): node.set('domain', "[('purchase_ok', '=', True)]") else: node.set('domain', "[('sale_ok', '=', True)]") - res['arch'] = etree.tostring(doc) + res['arch'] = etree.tostring(doc, encoding='unicode') return res @api.v8 @@ -1550,14 +1550,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': @@ -1572,7 +1571,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/account/models/account_move.py b/addons/account/models/account_move.py index d2369a6292b..384e9b0f8e7 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: @@ -1030,7 +1030,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 @@ -1492,7 +1492,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 @@ -1720,7 +1720,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' for partial_rec in partial_rec_set: if partial_rec.currency_id != currency: diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index 2fe33e02e3a..d99b3fe99ce 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -7,8 +7,6 @@ from odoo.exceptions import UserError import logging -from odoo.tools import pycompat - _logger = logging.getLogger(__name__) def migrate_set_tags_and_taxes_updatable(cr, registry, module): @@ -327,7 +325,7 @@ class AccountChartTemplate(models.Model): # writing account values after creation of accounts company.transfer_account_id = account_template_ref[transfer_account_id.id] - for key, value in pycompat.items(generated_tax_res['account_dict']): + for key, value in generated_tax_res['account_dict'].items(): if value['refund_account_id'] or value['account_id'] or value['cash_basis_account']: AccountTaxObj.browse(key).write({ 'refund_account_id': account_ref.get(value['refund_account_id'], False), diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py index f2f0f85f7c0..de32aaa5f3d 100644 --- a/addons/account/models/partner.py +++ b/addons/account/models/partner.py @@ -5,7 +5,7 @@ from operator import itemgetter import time from odoo import api, fields, models, _ -from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, pycompat +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT from odoo.exceptions import ValidationError from odoo.addons.base.res.res_partner import WARNING_MESSAGE, WARNING_HELP @@ -73,7 +73,7 @@ class AccountFiscalPosition(models.Model): ref_dict = {} for line in self.account_ids: ref_dict[line.account_src_id] = line.account_dest_id - for key, acc in list(pycompat.items(accounts)): + for key, acc in accounts.items(): if acc in ref_dict: accounts[key] = ref_dict[acc] return accounts @@ -293,7 +293,7 @@ class ResPartner(models.Model): """ % where_clause self.env.cr.execute(query, where_clause_params) price_totals = self.env.cr.dictfetchall() - for partner, child_ids in pycompat.items(all_partners_and_children): + for partner, child_ids in all_partners_and_children.items(): partner.total_invoiced = sum(price['total'] for price in price_totals if price['partner_id'] in child_ids) @api.multi diff --git a/addons/account/report/account_partner_ledger.py b/addons/account/report/account_partner_ledger.py index 96e54607e4e..2da6c42abe7 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/account/report/account_report_financial.py b/addons/account/report/account_report_financial.py index 01e2080307f..65560736fff 100644 --- a/addons/account/report/account_report_financial.py +++ b/addons/account/report/account_report_financial.py @@ -3,7 +3,6 @@ import time from odoo import api, models, _ from odoo.exceptions import UserError -from odoo.tools import pycompat class ReportFinancial(models.AbstractModel): @@ -28,7 +27,7 @@ class ReportFinancial(models.AbstractModel): if where_clause.strip(): wheres.append(where_clause.strip()) filters = " AND ".join(wheres) - request = "SELECT account_id as id, " + ', '.join(pycompat.values(mapping)) + \ + request = "SELECT account_id as id, " + ', '.join(mapping.values()) + \ " FROM " + tables + \ " WHERE account_id IN %s " \ + filters + \ @@ -55,26 +54,26 @@ class ReportFinancial(models.AbstractModel): if report.type == 'accounts': # it's the sum of the linked accounts res[report.id]['account'] = self._compute_account_balance(report.account_ids) - for value in pycompat.values(res[report.id]['account']): + for value in res[report.id]['account'].values(): for field in fields: res[report.id][field] += value.get(field) elif report.type == 'account_type': # it's the sum the leaf accounts with such an account type accounts = self.env['account.account'].search([('user_type_id', 'in', report.account_type_ids.ids)]) res[report.id]['account'] = self._compute_account_balance(accounts) - for value in pycompat.values(res[report.id]['account']): + for value in res[report.id]['account'].values(): for field in fields: res[report.id][field] += value.get(field) elif report.type == 'account_report' and report.account_report_id: # it's the amount of the linked report res2 = self._compute_report_balance(report.account_report_id) - for key, value in pycompat.items(res2): + for key, value in res2.items(): for field in fields: res[report.id][field] += value[field] elif report.type == 'sum': # it's the sum of the children of this account.report res2 = self._compute_report_balance(report.children_ids) - for key, value in pycompat.items(res2): + for key, value in res2.items(): for field in fields: res[report.id][field] += value[field] return res @@ -86,11 +85,11 @@ class ReportFinancial(models.AbstractModel): res = self.with_context(data.get('used_context'))._compute_report_balance(child_reports) if data['enable_filter']: comparison_res = self.with_context(data.get('comparison_context'))._compute_report_balance(child_reports) - for report_id, value in pycompat.items(comparison_res): + for report_id, value in comparison_res.items(): res[report_id]['comp_bal'] = value['balance'] report_acc = res[report_id].get('account') if report_acc: - for account_id, val in pycompat.items(comparison_res[report_id].get('account')): + for account_id, val in comparison_res[report_id].get('account').items(): report_acc[account_id]['comp_bal'] = val['balance'] for report in child_reports: @@ -115,7 +114,7 @@ class ReportFinancial(models.AbstractModel): if res[report.id].get('account'): sub_lines = [] - for account_id, value in pycompat.items(res[report.id]['account']): + for account_id, value in res[report.id]['account'].items(): #if there are accounts to display, we add them to the lines with a level equals to their level in #the COA + 1 (to avoid having them with a too low level that would conflicts with the level of data #financial reports for Assets, liabilities...) diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index 0093915eee5..ac2aca16951 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -2,8 +2,6 @@ from odoo.addons.account.tests.account_test_classes import AccountingTestCase import time import unittest -from odoo.tools import pycompat - class TestReconciliation(AccountingTestCase): @@ -339,8 +337,8 @@ class TestReconciliation(AccountingTestCase): self.assertTrue(exchange_loss_line, 'There should be one move line of 0.01 EUR in credit') # The journal items of the reconciliation should have their debit and credit total equal # Besides, the total debit and total credit should be 60.61 EUR (2.00 USD) - self.assertEquals(sum(res['debit'] for res in pycompat.values(result)), 60.61) - self.assertEquals(sum(res['credit'] for res in pycompat.items(result)), 60.61) + self.assertEquals(sum(res['debit'] for res in result.values()), 60.61) + self.assertEquals(sum(res['credit'] for res in result.items()), 60.61) counterpart_exchange_loss_line = None for line in exchange_loss_line.move_id.line_id: if line.account_id.id == self.account_fx_expense_id: diff --git a/addons/account_asset/models/account_asset.py b/addons/account_asset/models/account_asset.py index cc2a29a09a9..15d471301b6 100644 --- a/addons/account_asset/models/account_asset.py +++ b/addons/account_asset/models/account_asset.py @@ -7,7 +7,7 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models, _ from odoo.exceptions import UserError, ValidationError -from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DF, pycompat +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DF from odoo.tools import float_compare, float_is_zero @@ -374,7 +374,7 @@ class AccountAssetAsset(models.Model): vals = self.onchange_category_id_values(self.category_id.id) # We cannot use 'write' on an object that doesn't exist yet if vals: - for k, v in pycompat.items(vals['value']): + for k, v in vals['value'].items(): setattr(self, k, v) def onchange_category_id_values(self, category_id): @@ -583,7 +583,7 @@ class AccountAssetDepreciationLine(models.Model): message = '' if message_description: message = '%s' % message_description - for name, values in pycompat.items(tracked_values): + for name, values in tracked_values.items(): message += '
    • %s: ' % name message += '%s
' % values return message diff --git a/addons/account_asset/wizard/asset_modify.py b/addons/account_asset/wizard/asset_modify.py index 9f8ce7cd497..d31df656455 100644 --- a/addons/account_asset/wizard/asset_modify.py +++ b/addons/account_asset/wizard/asset_modify.py @@ -39,7 +39,7 @@ class AssetModify(models.TransientModel): node = doc.xpath("//field[@name='method_number']")[0] node.set('invisible', '1') setup_modifiers(node, result['fields']['method_number']) - result['arch'] = etree.tostring(doc) + result['arch'] = etree.tostring(doc, encoding='unicode') return result @api.model diff --git a/addons/account_voucher/models/account_voucher.py b/addons/account_voucher/models/account_voucher.py index e394af36296..40ae66c3395 100644 --- a/addons/account_voucher/models/account_voucher.py +++ b/addons/account_voucher/models/account_voucher.py @@ -5,7 +5,6 @@ from odoo import fields, models, api, _ from odoo.addons import decimal_precision as dp from odoo.exceptions import UserError -from odoo.tools import pycompat class AccountVoucher(models.Model): @@ -358,7 +357,7 @@ class AccountVoucherLine(models.Model): self.company_id.id, self.voucher_id.currency_id.id, self.voucher_id.voucher_type) - for fname, fvalue in pycompat.items(onchange_res['value']): + for fname, fvalue in onchange_res['value'].items(): setattr(self, fname, fvalue) def _get_account(self, product, fpos, type): diff --git a/addons/anonymization/models/anonymization.py b/addons/anonymization/models/anonymization.py index 2d4fe1b17d2..a0c90382938 100644 --- a/addons/anonymization/models/anonymization.py +++ b/addons/anonymization/models/anonymization.py @@ -6,6 +6,7 @@ from operator import itemgetter from odoo import api, fields, models, _ from odoo.exceptions import UserError +from odoo.tools import pycompat FIELD_STATES = [('clear', 'Clear'), ('anonymized', 'Anonymized'), ('not_existing', 'Not Existing'), ('new', 'New')] ANONYMIZATION_HISTORY_STATE = [('started', 'Started'), ('done', 'Done'), ('in_exception', 'Exception occured')] @@ -13,7 +14,7 @@ ANONYMIZATION_DIRECTION = [('clear -> anonymized', 'clear -> anonymized'), ('ano def group(lst, cols): - if isinstance(cols, basestring): + if isinstance(cols, pycompat.string_types): cols = [cols] return dict((k, [v for v in itr]) for k, itr in groupby(sorted(lst, key=itemgetter(*cols)), itemgetter(*cols))) diff --git a/addons/anonymization/wizard/anonymize_wizard.py b/addons/anonymization/wizard/anonymize_wizard.py index fb514fe778e..a319347f918 100644 --- a/addons/anonymization/wizard/anonymize_wizard.py +++ b/addons/anonymization/wizard/anonymize_wizard.py @@ -120,7 +120,7 @@ class IrModelFieldsAnonymizeWizard(models.TransientModel): else: raise UserError(_("The database anonymization is currently in an unstable state. Some fields are anonymized," " while some fields are not anonymized. You should try to solve this problem before trying to do anything else.")) - res['arch'] = etree.tostring(eview) + res['arch'] = etree.tostring(eview, encoding='unicode') return res @api.multi diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py index 8f8db0fab0d..262dfdad86d 100644 --- a/addons/auth_signup/controllers/main.py +++ b/addons/auth_signup/controllers/main.py @@ -8,7 +8,6 @@ from odoo.addons.auth_signup.models.res_users import SignupError from odoo.addons.web.controllers.main import ensure_db, Home from odoo.exceptions import UserError from odoo.http import request -from odoo.tools import pycompat _logger = logging.getLogger(__name__) @@ -106,7 +105,7 @@ class AuthSignupHome(Home): try: # retrieve the user info (name, login or email) corresponding to a signup token token_infos = request.env['res.partner'].sudo().signup_retrieve_info(qcontext.get('token')) - for k, v in pycompat.items(token_infos): + for k, v in token_infos.items(): qcontext.setdefault(k, v) except: qcontext['error'] = _("Invalid signup token") diff --git a/addons/auth_signup/models/res_partner.py b/addons/auth_signup/models/res_partner.py index 0cf5c25b7ff..ecf63246580 100644 --- a/addons/auth_signup/models/res_partner.py +++ b/addons/auth_signup/models/res_partner.py @@ -8,8 +8,6 @@ from collections import defaultdict from datetime import datetime, timedelta from odoo import api, exceptions, fields, models, _ -from odoo.tools import pycompat - class SignupError(Exception): pass diff --git a/addons/base_address_city/models/res_partner.py b/addons/base_address_city/models/res_partner.py index f6fcbf61d42..e3e3ad574e0 100644 --- a/addons/base_address_city/models/res_partner.py +++ b/addons/base_address_city/models/res_partner.py @@ -35,5 +35,5 @@ class Partner(models.Model): city_id_node = etree.fromstring(replacement_xml) city_node.getparent().replace(city_node, city_id_node) - arch = etree.tostring(doc) + arch = etree.tostring(doc, encoding='unicode') return arch diff --git a/addons/base_address_extended/models/base_address_extended.py b/addons/base_address_extended/models/base_address_extended.py index 1d49c1c94c4..9a210dde769 100644 --- a/addons/base_address_extended/models/base_address_extended.py +++ b/addons/base_address_extended/models/base_address_extended.py @@ -5,7 +5,6 @@ import re from odoo import api, fields, models, _ from odoo.exceptions import UserError -from odoo.tools import pycompat STREET_FIELDS = ('street_name', 'street_number', 'street_number2') @@ -135,7 +134,7 @@ class Partner(models.Model): vals[field_name] = street_raw # assign the values to the fields # /!\ Note that a write(vals) would cause a recursion since it would bypass the cache - for k, v in pycompat.items(vals): + for k, v in vals.items(): partner[k] = v diff --git a/addons/base_automation/models/base_automation.py b/addons/base_automation/models/base_automation.py index 061828c161e..9896a9706d8 100644 --- a/addons/base_automation/models/base_automation.py +++ b/addons/base_automation/models/base_automation.py @@ -12,7 +12,6 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models, SUPERUSER_ID from odoo.modules.registry import Registry -from odoo.tools import pycompat from odoo.tools.safe_eval import safe_eval _logger = logging.getLogger(__name__) @@ -264,7 +263,7 @@ class BaseAutomation(models.Model): if res: if 'value' in res: res['value'].pop('id', None) - self.update({key: val for key, val in pycompat.items(res['value']) if key in self._fields}) + self.update({key: val for key, val in res['value'].items() if key in self._fields}) if 'domain' in res: result.setdefault('domain', {}).update(res['domain']) if 'warning' in res: diff --git a/addons/base_gengo/wizard/base_gengo_translations.py b/addons/base_gengo/wizard/base_gengo_translations.py index 98df9356576..d6e5ce9176a 100644 --- a/addons/base_gengo/wizard/base_gengo_translations.py +++ b/addons/base_gengo/wizard/base_gengo_translations.py @@ -8,7 +8,6 @@ import uuid from odoo import api, fields, models, tools, _ from odoo.exceptions import UserError, ValidationError -from odoo.tools import pycompat _logger = logging.getLogger(__name__) @@ -194,7 +193,7 @@ class BaseGengoTranslations(models.TransientModel): term_ids.write(vals) jobs = response.get('jobs', []) if jobs: - for t_id, job in pycompat.items(jobs): + for t_id, job in jobs.items(): self._update_terms_job(job) return diff --git a/addons/base_import/models/base_import.py b/addons/base_import/models/base_import.py index 85fb49924f6..19941858dfa 100644 --- a/addons/base_import/models/base_import.py +++ b/addons/base_import/models/base_import.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import csv import datetime import io import itertools @@ -17,12 +16,6 @@ from odoo.tools.mimetypes import guess_mimetype from odoo.tools.misc import ustr from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, pycompat -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO - - FIELDS_RECURSION_LIMIT = 2 ERROR_PREVIEW_BYTES = 200 _logger = logging.getLogger(__name__) @@ -49,7 +42,7 @@ FILE_TYPE_DICT = { } EXTENSIONS = { '.' + ext: handler - for mime, (ext, handler, req) in pycompat.items(FILE_TYPE_DICT) + for mime, (ext, handler, req) in FILE_TYPE_DICT.items() } @@ -121,7 +114,7 @@ class Import(models.TransientModel): }] model_fields = Model.fields_get() blacklist = models.MAGIC_COLUMNS + [Model.CONCURRENCY_CHECK_FIELD] - for name, field in pycompat.items(model_fields): + for name, field in model_fields.items(): if name in blacklist: continue # an empty string means the field is deprecated, @deprecated must @@ -134,7 +127,7 @@ class Import(models.TransientModel): continue # states = {state: [(attr, value), (attr2, value2)], state2:...} if not any(attr == 'readonly' and value is False - for attr, value in itertools.chain.from_iterable(pycompat.values(states))): + for attr, value in itertools.chain.from_iterable(states.values())): continue field_value = { 'id': name, @@ -214,9 +207,9 @@ class Import(models.TransientModel): if cell.ctype is xlrd.XL_CELL_NUMBER: is_float = cell.value % 1 != 0.0 values.append( - unicode(cell.value) + pycompat.text_type(cell.value) if is_float - else unicode(int(cell.value)) + else pycompat.text_type(int(cell.value)) ) elif cell.ctype is xlrd.XL_CELL_DATE: is_datetime = cell.value % 1 != 0.0 @@ -268,14 +261,13 @@ class Import(models.TransientModel): # csv module expect utf-8, see http://docs.python.org/2/library/csv.html csv_data = csv_data.decode(encoding).encode('utf-8') - csv_iterator = csv.reader( - StringIO(csv_data), + csv_iterator = pycompat.csv_reader( + io.BytesIO(csv_data), quotechar=str(options['quoting']), delimiter=str(options['separator'])) return ( - [item.decode('utf-8') for item in row] - for row in csv_iterator + row for row in csv_iterator if any(x for x in row if x.strip()) ) @@ -603,7 +595,7 @@ class Import(models.TransientModel): def _parse_import_data(self, data, import_fields, options): # Get fields of type date/datetime all_fields = self.env[self.res_model].fields_get() - for name, field in pycompat.items(all_fields): + for name, field in all_fields.items(): if field['type'] in ('date', 'datetime') and name in import_fields: # Parse date index = import_fields.index(name) @@ -611,11 +603,13 @@ class Import(models.TransientModel): server_format = DEFAULT_SERVER_DATE_FORMAT if field['type'] == 'date' else DEFAULT_SERVER_DATETIME_FORMAT if options.get('%s_format' % field['type'], server_format) != server_format: - user_format = ustr(options.get('%s_format' % field['type'])).encode('utf-8') + # datetime.str[fp]time takes *native strings* in both + # versions, for both data and pattern + user_format = pycompat.to_native(options.get('%s_format' % field['type'])) for num, line in enumerate(data): if line[index]: try: - line[index] = dt.strftime(dt.strptime(ustr(line[index].strip()).encode('utf-8'), user_format), server_format) + line[index] = dt.strftime(dt.strptime(pycompat.to_native(line[index].strip()), user_format), server_format) except ValueError as e: raise ValueError(_("Column %s contains incorrect values. Error in line %d: %s") % (name, num + 1, e)) except Exception as e: @@ -659,7 +653,7 @@ class Import(models.TransientModel): except ValueError as error: return [{ 'type': 'error', - 'message': unicode(error), + 'message': pycompat.text_type(error), 'record': False, }] diff --git a/addons/base_import/models/odf_ods_reader.py b/addons/base_import/models/odf_ods_reader.py index c2613c6baa5..daa7d2b16e2 100644 --- a/addons/base_import/models/odf_ods_reader.py +++ b/addons/base_import/models/odf_ods_reader.py @@ -19,8 +19,6 @@ from odf import opendocument from odf.table import Table, TableRow, TableCell from odf.text import P -from odoo.tools import pycompat - class ODSReader(object): @@ -97,4 +95,4 @@ class ODSReader(object): return self.SHEETS[name] def getFirstSheet(self): - return next(iter(pycompat.values(self.SHEETS))) + return next(iter(self.SHEETS.values())) diff --git a/addons/base_import/tests/test_base_import.py b/addons/base_import/tests/test_base_import.py index 2d1d850d3a9..cf324b44abd 100644 --- a/addons/base_import/tests/test_base_import.py +++ b/addons/base_import/tests/test_base_import.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - -import csv +import base64 import io import unittest from odoo.tests.common import TransactionCase, can_import from odoo.modules.module import get_module_resource -from odoo.tools import mute_logger +from odoo.tools import mute_logger, pycompat ID_FIELD = { 'id': 'id', @@ -231,10 +230,10 @@ class TestPreview(TransactionCase): def test_csv_success(self): import_wizard = self.env['base_import.import'].create({ 'res_model': 'base_import.tests.models.preview', - 'file': 'name,Some Value,Counter\n' - 'foo,1,2\n' - 'bar,3,4\n' - 'qux,5,6\n', + 'file': b'name,Some Value,Counter\n' + b'foo,1,2\n' + b'bar,3,4\n' + b'qux,5,6\n', 'file_type': 'text/csv' }) @@ -359,10 +358,10 @@ class test_convert_import_data(TransactionCase): def test_all(self): import_wizard = self.env['base_import.import'].create({ 'res_model': 'base_import.tests.models.preview', - 'file': 'name,Some Value,Counter\n' - 'foo,1,2\n' - 'bar,3,4\n' - 'qux,5,6\n', + 'file': b'name,Some Value,Counter\n' + b'foo,1,2\n' + b'bar,3,4\n' + b'qux,5,6\n', 'file_type': 'text/csv' }) @@ -381,8 +380,8 @@ class test_convert_import_data(TransactionCase): def test_date_fields(self): import_wizard = self.env['base_import.import'].create({ 'res_model': 'res.partner', - 'file': 'name,date,create_date\n' - '"foo","2013年07月18日","2016-10-12 06:06"\n', + 'file': u'name,date,create_date\n' + u'"foo","2013年07月18日","2016-10-12 06:06"\n'.encode('utf-8'), 'file_type': 'text/csv' }) @@ -409,10 +408,10 @@ class test_convert_import_data(TransactionCase): """ import_wizard = self.env['base_import.import'].create({ 'res_model': 'base_import.tests.models.preview', - 'file': 'name,Some Value,Counter\n' - 'foo,1,2\n' - 'bar,3,4\n' - 'qux,5,6\n', + 'file': b'name,Some Value,Counter\n' + b'foo,1,2\n' + b'bar,3,4\n' + b'qux,5,6\n', 'file_type': 'text/csv' }) data, fields = import_wizard._convert_import_data( @@ -433,10 +432,10 @@ class test_convert_import_data(TransactionCase): """ import_wizard = self.env['base_import.import'].create({ 'res_model': 'base_import.tests.models.preview', - 'file': 'name,Some Value,Counter\n' - 'foo,1,2\n' - ',3,\n' - ',5,6\n', + 'file': b'name,Some Value,Counter\n' + b'foo,1,2\n' + b',3,\n' + b',5,6\n', 'file_type': 'text/csv' }) data, fields = import_wizard._convert_import_data( @@ -453,12 +452,12 @@ class test_convert_import_data(TransactionCase): def test_empty_rows(self): import_wizard = self.env['base_import.import'].create({ 'res_model': 'base_import.tests.models.preview', - 'file': 'name,Some Value\n' - 'foo,1\n' - '\n' - 'bar,2\n' - ' \n' - '\t \n', + 'file': b'name,Some Value\n' + b'foo,1\n' + b'\n' + b'bar,2\n' + b' \n' + b'\t \n', 'file_type': 'text/csv' }) data, fields = import_wizard._convert_import_data( @@ -475,8 +474,8 @@ class test_convert_import_data(TransactionCase): def test_nofield(self): import_wizard = self.env['base_import.import'].create({ 'res_model': 'base_import.tests.models.preview', - 'file': 'name,Some Value,Counter\n' - 'foo,1,2\n', + 'file': b'name,Some Value,Counter\n' + b'foo,1,2\n', 'file_type': 'text/csv' }) @@ -485,8 +484,8 @@ class test_convert_import_data(TransactionCase): def test_falsefields(self): import_wizard = self.env['base_import.import'].create({ 'res_model': 'base_import.tests.models.preview', - 'file': 'name,Some Value,Counter\n' - 'foo,1,2\n', + 'file': b'name,Some Value,Counter\n' + b'foo,1,2\n', 'file_type': 'text/csv' }) @@ -501,11 +500,11 @@ class test_convert_import_data(TransactionCase): Ensure importing keep newlines """ output = io.BytesIO() - writer = csv.writer(output, quoting=csv.QUOTE_ALL) + writer = pycompat.csv_writer(output, quoting=1) - data_row = ["\tfoo\n\tbar", " \"hello\" \n\n 'world' "] + data_row = [u"\tfoo\n\tbar", u" \"hello\" \n\n 'world' "] - writer.writerow(["name", "Some Value"]) + writer.writerow([u"name", u"Some Value"]) writer.writerow(data_row) import_wizard = self.env['base_import.import'].create({ @@ -527,16 +526,15 @@ class test_failures(TransactionCase): Ensure big fields (e.g. b64-encoded image data) can be imported and we're not hitting limits of the default CSV parser config """ - import csv, cStringIO from PIL import Image im = Image.new('RGB', (1920, 1080)) - fout = cStringIO.StringIO() + fout = io.BytesIO() - writer = csv.writer(fout, dialect=None) + writer = pycompat.csv_writer(fout, dialect=None) writer.writerows([ - ['name', 'db_datas'], - ['foo', im.tobytes().encode('base64')] + [u'name', u'db_datas'], + [u'foo', base64.b64encode(im.tobytes()).decode('ascii')] ]) import_wizard = self.env['base_import.import'].create({ diff --git a/addons/base_import_module/models/ir_module.py b/addons/base_import_module/models/ir_module.py index ec94bc22d27..5bad5ea53eb 100644 --- a/addons/base_import_module/models/ir_module.py +++ b/addons/base_import_module/models/ir_module.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import base64 import logging import os import sys @@ -8,7 +9,7 @@ from os.path import join as opj from odoo import api, fields, models, _ from odoo.exceptions import UserError from odoo.modules import load_information_from_description_file -from odoo.tools import convert_file, exception_to_unicode, pycompat +from odoo.tools import convert_file, exception_to_unicode from odoo.tools.osutil import tempdir _logger = logging.getLogger(__name__) @@ -63,8 +64,8 @@ class IrModule(models.Model): for root, dirs, files in os.walk(path_static): for static_file in files: full_path = opj(root, static_file) - with open(full_path, 'r') as fp: - data = fp.read().encode('base64') + with open(full_path, 'rb') as fp: + data = base64.b64encode(fp.read()) url_path = '/%s%s' % (module, full_path.split(path)[1].replace(os.path.sep, '/')) url_path = url_path.decode(sys.getfilesystemencoding()) filename = os.path.split(url_path)[1] @@ -118,6 +119,6 @@ class IrModule(models.Model): finally: addons.module.ad_paths.remove(module_dir) r = ["Successfully imported module '%s'" % mod for mod in success] - for mod, error in pycompat.items(errors): + for mod, error in errors.items(): r.append("Error while importing module '%s': %r" % (mod, error)) return '\n'.join(r), module_names diff --git a/addons/board/controllers/main.py b/addons/board/controllers/main.py index 2ba236ea109..ebd9b568583 100644 --- a/addons/board/controllers/main.py +++ b/addons/board/controllers/main.py @@ -29,7 +29,7 @@ class Board(Controller): 'domain': str(domain) }) column.insert(0, new_action) - arch = ElementTree.tostring(xml, 'utf-8') + arch = ElementTree.tostring(xml, encoding='unicode') request.env['ir.ui.view.custom'].create({ 'user_id': request.session.uid, 'ref_id': view_id, diff --git a/addons/board/models/board.py b/addons/board/models/board.py index 3acd277826a..ebbbec74e2c 100644 --- a/addons/board/models/board.py +++ b/addons/board/models/board.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, models +from odoo.tools import pycompat class Board(models.AbstractModel): @@ -41,13 +42,8 @@ class Board(models.AbstractModel): if child.tag == 'action' and child.get('invisible'): node.remove(child) else: - child = remove_unauthorized_children(child) + remove_unauthorized_children(child) return node - def encode(s): - if isinstance(s, unicode): - return s.encode('utf8') - return s - - archnode = etree.fromstring(encode(arch)) - return etree.tostring(remove_unauthorized_children(archnode), pretty_print=True) + archnode = etree.fromstring(arch) + return etree.tostring(remove_unauthorized_children(archnode), pretty_print=True, encoding='unicode') diff --git a/addons/bus/controllers/main.py b/addons/bus/controllers/main.py index 40118764751..4e82116e7c7 100644 --- a/addons/bus/controllers/main.py +++ b/addons/bus/controllers/main.py @@ -4,6 +4,8 @@ from odoo import exceptions from odoo.http import Controller, request, route from odoo.addons.bus.models.bus import dispatch +from odoo.tools import pycompat + class BusController(Controller): """ Examples: @@ -14,7 +16,7 @@ class BusController(Controller): @route('/longpolling/send', type="json", auth="public") def send(self, channel, message): - if not isinstance(channel, basestring): + if not isinstance(channel, pycompat.string_types): raise Exception("bus.Bus only string channels are allowed.") return request.env['bus.bus'].sendone(channel, message) @@ -33,7 +35,7 @@ class BusController(Controller): options = {} if not dispatch: raise Exception("bus.Bus unavailable") - if [c for c in channels if not isinstance(c, basestring)]: + if [c for c in channels if not isinstance(c, pycompat.string_types)]: raise Exception("bus.Bus only string channels are allowed.") if request.registry.in_test_mode(): raise exceptions.UserError("bus.Bus not available in test mode") diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py index 9c8b088aeb1..e80a2727282 100644 --- a/addons/calendar/models/calendar.py +++ b/addons/calendar/models/calendar.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import base64 import babel.dates import collections @@ -33,7 +34,7 @@ def calendar_id2real_id(calendar_id=None, with_date=False): :param with_date: if a value is passed to this param it will return dates based on value of withdate + calendar_id :return: real event id """ - if calendar_id and isinstance(calendar_id, (basestring)): + if calendar_id and isinstance(calendar_id, pycompat.string_types): res = [bit for bit in calendar_id.split('-') if bit] if len(res) == 2: real_id = res[0] @@ -47,7 +48,7 @@ def calendar_id2real_id(calendar_id=None, with_date=False): def get_real_ids(ids): - if isinstance(ids, (basestring, pycompat.integer_types)): + if isinstance(ids, (pycompat.string_types, pycompat.integer_types)): return calendar_id2real_id(ids) if isinstance(ids, (list, tuple)): @@ -173,6 +174,7 @@ class Attendee(models.Model): mails_to_send = self.env['mail.mail'] for attendee in self: if attendee.email or attendee.partner_id.email: + # FIXME: is ics_file text or bytes? ics_file = ics_files.get(attendee.event_id.id) mail_id = invitation_template.send_mail(attendee.id) @@ -181,7 +183,7 @@ class Attendee(models.Model): vals['attachment_ids'] = [(0, 0, {'name': 'invitation.ics', 'mimetype': 'text/calendar', 'datas_fname': 'invitation.ics', - 'datas': str(ics_file).encode('base64')})] + 'datas': base64.b64encode(ics_file)})] vals['model'] = None # We don't want to have the mail in the tchatter while in queue! vals['res_id'] = False current_mail = self.env['mail.mail'].browse(mail_id) @@ -468,7 +470,7 @@ class Alarm(models.Model): name = fields.Char('Name', required=True) type = fields.Selection([('notification', 'Notification'), ('email', 'Email')], 'Type', required=True, default='email') duration = fields.Integer('Remind Before', required=True, default=1) - interval = fields.Selection(list(pycompat.items(_interval_selection)), 'Unit', required=True, default='hours') + interval = fields.Selection(list(_interval_selection.items()), 'Unit', required=True, default='hours') duration_minutes = fields.Integer('Duration in minutes', compute='_compute_duration_minutes', store=True, help="Duration in minutes") @api.onchange('duration', 'interval') @@ -630,8 +632,8 @@ class Meeting(models.Model): } # formats will be used for str{f,p}time() which do not support unicode in Python 2, coerce to str - format_date = lang_params.get("date_format", '%B-%d-%Y').encode('utf-8') - format_time = lang_params.get("time_format", '%I-%M %p').encode('utf-8') + format_date = pycompat.to_native(lang_params.get("date_format", '%B-%d-%Y')) + format_time = pycompat.to_native(lang_params.get("time_format", '%I-%M %p')) return (format_date, format_time) @api.model @@ -917,11 +919,11 @@ class Meeting(models.Model): elif interval == 'minutes': delta = timedelta(minutes=duration) trigger.value = delta - valarm.add('DESCRIPTION').value = alarm.name or 'Odoo' + valarm.add('DESCRIPTION').value = alarm.name or u'Odoo' for attendee in meeting.attendee_ids: attendee_add = event.add('attendee') - attendee_add.value = 'MAILTO:' + (attendee.email or '') - result[meeting.id] = cal.serialize() + attendee_add.value = u'MAILTO:' + (attendee.email or u'') + result[meeting.id] = cal.serialize().encode('utf-8') return result @@ -1058,7 +1060,7 @@ class Meeting(models.Model): pile.reverse() new_pile = [] for item in pile: - if not isinstance(item, basestring): + if not isinstance(item, pycompat.string_types): res = item elif str(item) == str('&'): first = new_pile.pop() @@ -1207,7 +1209,7 @@ class Meeting(models.Model): if interval == 'day': # Day number (1-31) - result = unicode(date.day) + result = pycompat.text_type(date.day) elif interval == 'month': # Localized month name and year @@ -1219,6 +1221,7 @@ class Meeting(models.Model): elif interval == 'time': # Localized time + # FIXME: formats are specifically encoded to bytes, maybe use babel? dummy, format_time = self._get_date_formats() result = tools.ustr(date.strftime(format_time + " %Z")) @@ -1289,7 +1292,7 @@ class Meeting(models.Model): @api.multi def _get_message_unread(self): id_map = {x: calendar_id2real_id(x) for x in self.ids} - real = self.browse(set(pycompat.values(id_map))) + real = self.browse(set(id_map.values())) super(Meeting, real)._get_message_unread() for event in self: if event.id == id_map[event.id]: @@ -1301,7 +1304,7 @@ class Meeting(models.Model): @api.multi def _get_message_needaction(self): id_map = {x: calendar_id2real_id(x) for x in self.ids} - real = self.browse(set(pycompat.values(id_map))) + real = self.browse(set(id_map.values())) super(Meeting, real)._get_message_needaction() for event in self: if event.id == id_map[event.id]: @@ -1314,7 +1317,7 @@ class Meeting(models.Model): @api.returns('self', lambda value: value.id) def message_post(self, **kwargs): thread_id = self.id - if isinstance(self.id, basestring): + if isinstance(self.id, pycompat.string_types): thread_id = get_real_ids(self.id) if self.env.context.get('default_date'): context = dict(self.env.context) @@ -1350,7 +1353,7 @@ class Meeting(models.Model): for arg in args: if arg[0] == 'id': for n, calendar_id in enumerate(arg[2]): - if isinstance(calendar_id, basestring): + if isinstance(calendar_id, pycompat.string_types): arg[2][n] = calendar_id.split('-')[0] return super(Meeting, self)._name_search(name=name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid) @@ -1388,7 +1391,7 @@ class Meeting(models.Model): # set end_date for calendar searching if any(field in values for field in ['recurrency', 'end_type', 'count', 'rrule_type', 'start', 'stop']): for real_meeting in real_meetings: - if real_meeting.recurrency and real_meeting.end_type in ('count', unicode('count')): + if real_meeting.recurrency and real_meeting.end_type == u'count': final_date = real_meeting._get_recurrency_end_date() super(Meeting, real_meeting).write({'final_date': final_date}) @@ -1472,7 +1475,7 @@ class Meeting(models.Model): for calendar_id, real_id in select: res = real_data[real_id].copy() ls = calendar_id2real_id(calendar_id, with_date=res and res.get('duration', 0) > 0 and res.get('duration') or 1) - if not isinstance(ls, (basestring, pycompat.integer_types)) and len(ls) >= 2: + if not isinstance(ls, (pycompat.string_types, pycompat.integer_types)) and len(ls) >= 2: res['start'] = ls[1] res['stop'] = ls[2] diff --git a/addons/calendar/models/ir_attachment.py b/addons/calendar/models/ir_attachment.py index c98242db7e7..ae734ec59f4 100644 --- a/addons/calendar/models/ir_attachment.py +++ b/addons/calendar/models/ir_attachment.py @@ -5,6 +5,8 @@ from odoo import api, models from odoo.addons.calendar.models.calendar import get_real_ids +from odoo.tools import pycompat + class Attachment(models.Model): @@ -16,13 +18,13 @@ class Attachment(models.Model): args = list(args) if any([leaf for leaf in args if leaf[0] == "res_model" and leaf[2] == 'calendar.event']): for index in range(len(args)): - if args[index][0] == "res_id" and isinstance(args[index][2], basestring): + if args[index][0] == "res_id" and isinstance(args[index][2], pycompat.string_types): args[index] = (args[index][0], args[index][1], get_real_ids(args[index][2])) return super(Attachment, self).search(args, offset=offset, limit=limit, order=order, count=count) @api.multi def write(self, vals): """ When posting an attachment (new or not), convert the virtual ids in real ids. """ - if isinstance(vals.get('res_id'), basestring): + if isinstance(vals.get('res_id'), pycompat.string_types): vals['res_id'] = get_real_ids(vals.get('res_id')) return super(Attachment, self).write(vals) diff --git a/addons/calendar/models/mail_message.py b/addons/calendar/models/mail_message.py index 0dc4d24e364..26be5080ab5 100644 --- a/addons/calendar/models/mail_message.py +++ b/addons/calendar/models/mail_message.py @@ -5,6 +5,8 @@ from odoo import api, models from odoo.addons.calendar.models.calendar import get_real_ids +from odoo.tools import pycompat + class Message(models.Model): @@ -16,7 +18,7 @@ class Message(models.Model): args = list(args) for index in range(len(args)): if args[index][0] == "res_id": - if isinstance(args[index][2], basestring): + if isinstance(args[index][2], pycompat.string_types): args[index] = (args[index][0], args[index][1], get_real_ids(args[index][2])) elif isinstance(args[index][2], list): args[index] = (args[index][0], args[index][1], [get_real_ids(x) for x in args[index][2]]) 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/crm/wizard/base_partner_merge.py b/addons/crm/wizard/base_partner_merge.py index 7751e0106ed..a91b9ec3dc1 100644 --- a/addons/crm/wizard/base_partner_merge.py +++ b/addons/crm/wizard/base_partner_merge.py @@ -2,56 +2,18 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from ast import literal_eval -from email.utils import parseaddr import functools -try: - from html.entities import entitydefs -except ImportError: - from htmlentitydefs import entitydefs # pylint: disable=deprecated-module import itertools import logging -import operator import psycopg2 -import re - -# Validation Library https://pypi.python.org/pypi/validate_email/1.1 -from .validate_email import validate_email from odoo import api, fields, models from odoo import SUPERUSER_ID, _ from odoo.exceptions import ValidationError, UserError -from odoo.tools import mute_logger, pycompat +from odoo.tools import mute_logger _logger = logging.getLogger('base.partner.merge') - -# http://www.php2python.com/wiki/function.html-entity-decode/ -def html_entity_decode_char(m, defs=entitydefs): - try: - return defs[m.group(1)] - except KeyError: - return m.group(0) - - -def html_entity_decode(string): - pattern = re.compile("&(\w+?);") - return pattern.sub(html_entity_decode_char, string) - - -def sanitize_email(email): - assert isinstance(email, basestring) and email - - result = re.subn(r';|/|:', ',', html_entity_decode(email or ''))[0].split(',') - - emails = [parseaddr(email)[1] - for item in result - for email in item.split()] - - return [email.lower() - for email in emails - if validate_email(email)] - - class MergePartnerLine(models.TransientModel): _name = 'base.partner.merge.line' @@ -271,7 +233,7 @@ class MergePartnerAutomatic(models.TransientModel): return item # get all fields that are not computed or x2many values = dict() - for column, field in pycompat.items(model_fields): + for column, field in model_fields.items(): if field.type not in ('many2many', 'one2many') and field.compute is None: for item in itertools.chain(src_partners, [dst_partner]): if item[column]: @@ -409,7 +371,7 @@ class MergePartnerAutomatic(models.TransientModel): """ return any( self.env[model].search_count([(field, 'in', aggr_ids)]) - for model, field in pycompat.items(models) + for model, field in models.items() ) @api.model diff --git a/addons/crm/wizard/validate_email.py b/addons/crm/wizard/validate_email.py deleted file mode 100644 index 38de5743fca..00000000000 --- a/addons/crm/wizard/validate_email.py +++ /dev/null @@ -1,123 +0,0 @@ -# RFC 2822 - style email validation for Python -# (c) 2012 Syrus Akbary -# Extended from (c) 2011 Noel Bush -# for support of mx and user check -# This code is made available to you under the GNU LGPL v3. -# -# This module provides a single method, valid_email_address(), -# which returns True or False to indicate whether a given address -# is valid according to the 'addr-spec' part of the specification -# given in RFC 2822. Ideally, we would like to find this -# in some other library, already thoroughly tested and well- -# maintained. The standard Python library email.utils -# contains a parse_addr() function, but it is not sufficient -# to detect many malformed addresses. -# -# This implementation aims to be faithful to the RFC, with the -# exception of a circular definition (see comments below), and -# with the omission of the pattern components marked as "obsolete". - -import re -import smtplib -import socket - -try: - import DNS - ServerError = DNS.ServerError -except: - DNS = None - class ServerError(Exception): pass -# All we are really doing is comparing the input string to one -# gigantic regular expression. But building that regexp, and -# ensuring its correctness, is made much easier by assembling it -# from the "tokens" defined by the RFC. Each of these tokens is -# tested in the accompanying unit test file. -# -# The section of RFC 2822 from which each pattern component is -# derived is given in an accompanying comment. -# -# (To make things simple, every string below is given as 'raw', -# even when it's not strictly necessary. This way we don't forget -# when it is necessary.) -# -WSP = r'[ \t]' # see 2.2.2. Structured Header Field Bodies -CRLF = r'(?:\r\n)' # see 2.2.3. Long Header Fields -NO_WS_CTL = r'\x01-\x08\x0b\x0c\x0f-\x1f\x7f' # see 3.2.1. Primitive Tokens -QUOTED_PAIR = r'(?:\\.)' # see 3.2.2. Quoted characters -FWS = r'(?:(?:' + WSP + r'*' + CRLF + r')?' + \ - WSP + r'+)' # see 3.2.3. Folding white space and comments -CTEXT = r'[' + NO_WS_CTL + \ - r'\x21-\x27\x2a-\x5b\x5d-\x7e]' # see 3.2.3 -CCONTENT = r'(?:' + CTEXT + r'|' + \ - QUOTED_PAIR + r')' # see 3.2.3 (NB: The RFC includes COMMENT here - # as well, but that would be circular.) -COMMENT = r'\((?:' + FWS + r'?' + CCONTENT + \ - r')*' + FWS + r'?\)' # see 3.2.3 -CFWS = r'(?:' + FWS + r'?' + COMMENT + ')*(?:' + \ - FWS + '?' + COMMENT + '|' + FWS + ')' # see 3.2.3 -ATEXT = r'[\w!#$%&\'\*\+\-/=\?\^`\{\|\}~]' # see 3.2.4. Atom -ATOM = CFWS + r'?' + ATEXT + r'+' + CFWS + r'?' # see 3.2.4 -DOT_ATOM_TEXT = ATEXT + r'+(?:\.' + ATEXT + r'+)*' # see 3.2.4 -DOT_ATOM = CFWS + r'?' + DOT_ATOM_TEXT + CFWS + r'?' # see 3.2.4 -QTEXT = r'[' + NO_WS_CTL + \ - r'\x21\x23-\x5b\x5d-\x7e]' # see 3.2.5. Quoted strings -QCONTENT = r'(?:' + QTEXT + r'|' + \ - QUOTED_PAIR + r')' # see 3.2.5 -QUOTED_STRING = CFWS + r'?' + r'"(?:' + FWS + \ - r'?' + QCONTENT + r')*' + FWS + \ - r'?' + r'"' + CFWS + r'?' -LOCAL_PART = r'(?:' + DOT_ATOM + r'|' + \ - QUOTED_STRING + r')' # see 3.4.1. Addr-spec specification -DTEXT = r'[' + NO_WS_CTL + r'\x21-\x5a\x5e-\x7e]' # see 3.4.1 -DCONTENT = r'(?:' + DTEXT + r'|' + \ - QUOTED_PAIR + r')' # see 3.4.1 -DOMAIN_LITERAL = CFWS + r'?' + r'\[' + \ - r'(?:' + FWS + r'?' + DCONTENT + \ - r')*' + FWS + r'?\]' + CFWS + r'?' # see 3.4.1 -DOMAIN = r'(?:' + DOT_ATOM + r'|' + \ - DOMAIN_LITERAL + r')' # see 3.4.1 -ADDR_SPEC = LOCAL_PART + r'@' + DOMAIN # see 3.4.1 - -# A valid address will match exactly the 3.4.1 addr-spec. -VALID_ADDRESS_REGEXP = '^' + ADDR_SPEC + '$' - -def validate_email(email, check_mx=False,verify=False): - - """Indicate whether the given string is a valid email address - according to the 'addr-spec' portion of RFC 2822 (see section - 3.4.1). Parts of the spec that are marked obsolete are *not* - included in this test, and certain arcane constructions that - depend on circular definitions in the spec may not pass, but in - general this should correctly identify any email address likely - to be in use as of 2011.""" - try: - assert re.match(VALID_ADDRESS_REGEXP, email) is not None - check_mx |= verify - if check_mx: - if not DNS: raise Exception('For check the mx records or check if the email exists you must have installed pyDNS python package') - DNS.DiscoverNameServers() - hostname = email[email.find('@')+1:] - mx_hosts = DNS.mxlookup(hostname) - for mx in mx_hosts: - try: - smtp = smtplib.SMTP() - smtp.connect(mx[1]) - if not verify: return True - status, _ = smtp.helo() - if status != 250: continue - smtp.mail('') - status, _ = smtp.rcpt(email) - if status != 250: return False - break - except smtplib.SMTPServerDisconnected: #Server not permits verify user - break - except smtplib.SMTPConnectError: - continue - except (AssertionError, ServerError): - return False - return True - -# import sys - -# sys.modules[__name__],sys.modules['validate_email_module'] = validate_email,sys.modules[__name__] -# from validate_email_module import * diff --git a/addons/document/models/ir_attachment.py b/addons/document/models/ir_attachment.py index 53ae1f1f463..2bc6aee234d 100644 --- a/addons/document/models/ir_attachment.py +++ b/addons/document/models/ir_attachment.py @@ -1,30 +1,16 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import io import logging import PyPDF2 import xml.dom.minidom import zipfile -from StringIO import StringIO - from odoo import api, models _logger = logging.getLogger(__name__) FTYPES = ['docx', 'pptx', 'xlsx', 'opendoc', 'pdf'] -# Keep function in case it is necessary to do toUnicode(buf.encode('ascii', 'replace')) -def toUnicode(s): - try: - return s.decode('utf-8') - except UnicodeError: - try: - return s.decode('latin') - except UnicodeError: - try: - return s.encode('ascii') - except UnicodeError: - return s - def textToString(element): buff = u"" for node in element.childNodes: @@ -41,7 +27,7 @@ class IrAttachment(models.Model): def _index_docx(self, bin_data): '''Index Microsoft .docx documents''' buf = u"" - f = StringIO(bin_data) + f = io.BytesIO(bin_data) if zipfile.is_zipfile(f): try: zf = zipfile.ZipFile(f) @@ -57,7 +43,7 @@ class IrAttachment(models.Model): '''Index Microsoft .pptx documents''' buf = u"" - f = StringIO(bin_data) + f = io.BytesIO(bin_data) if zipfile.is_zipfile(f): try: zf = zipfile.ZipFile(f) @@ -75,7 +61,7 @@ class IrAttachment(models.Model): '''Index Microsoft .xlsx documents''' buf = u"" - f = StringIO(bin_data) + f = io.BytesIO(bin_data) if zipfile.is_zipfile(f): try: zf = zipfile.ZipFile(f) @@ -91,7 +77,7 @@ class IrAttachment(models.Model): '''Index OpenDocument documents (.odt, .ods...)''' buf = u"" - f = StringIO(bin_data) + f = io.BytesIO(bin_data) if zipfile.is_zipfile(f): try: zf = zipfile.ZipFile(f) @@ -107,8 +93,8 @@ class IrAttachment(models.Model): '''Index PDF documents''' buf = u"" - if bin_data.startswith('%PDF-'): - f = StringIO(bin_data) + if bin_data.startswith(b'%PDF-'): + f = io.BytesIO(bin_data) try: pdf = PyPDF2.PdfFileReader(f, overwriteWarnings=False) for page in pdf.pages: diff --git a/addons/event/models/event.py b/addons/event/models/event.py index 95310a67122..c5f7f43915a 100644 --- a/addons/event/models/event.py +++ b/addons/event/models/event.py @@ -5,7 +5,6 @@ import pytz from odoo import _, api, fields, models from odoo.addons.mail.models.mail_template import format_tz from odoo.exceptions import AccessError, UserError, ValidationError -from odoo.tools import pycompat from odoo.tools.translate import html_translate from dateutil.relativedelta import relativedelta @@ -389,7 +388,7 @@ class EventRegistration(models.Model): 'partner_id': partner_id.id, 'event_id': event_id and event_id.id or False, } - data.update({key: value for key, value in pycompat.items(registration) if key in self._fields}) + data.update({key: value for key, value in registration.items() if key in self._fields}) return data @api.one diff --git a/addons/fleet/models/fleet_vehicle_cost.py b/addons/fleet/models/fleet_vehicle_cost.py index 12c6a7d7f2e..827863e3adc 100644 --- a/addons/fleet/models/fleet_vehicle_cost.py +++ b/addons/fleet/models/fleet_vehicle_cost.py @@ -6,8 +6,6 @@ from odoo.exceptions import UserError from dateutil.relativedelta import relativedelta -from odoo.tools import pycompat - class FleetVehicleCost(models.Model): _name = 'fleet.vehicle.cost' @@ -264,7 +262,7 @@ class FleetVehicleLogContract(models.Model): res[contract.vehicle_id.id] = 1 Vehicle = self.env['fleet.vehicle'] - for vehicle, value in pycompat.items(res): + for vehicle, value in res.items(): Vehicle.browse(vehicle).message_post(body=_('%s contract(s) need(s) to be renewed and/or closed!') % value) return contracts.write({'state': 'toclose'}) diff --git a/addons/gamification/models/goal.py b/addons/gamification/models/goal.py index 56c447dd9d4..3c062401662 100644 --- a/addons/gamification/models/goal.py +++ b/addons/gamification/models/goal.py @@ -262,7 +262,7 @@ class Goal(models.Model): for goal in self: goals_by_definition.setdefault(goal.definition_id, []).append(goal) - for definition, goals in pycompat.items(goals_by_definition): + for definition, goals in goals_by_definition.items(): goals_to_write = {} if definition.computation_mode == 'manually': for goal in goals: @@ -307,9 +307,9 @@ class Goal(models.Model): subqueries.setdefault((start_date, end_date), {}).update({goal.id:safe_eval(definition.batch_user_expression, {'user': goal.user_id})}) # the global query should be split by time periods (especially for recurrent goals) - for (start_date, end_date), query_goals in pycompat.items(subqueries): + for (start_date, end_date), query_goals in subqueries.items(): subquery_domain = list(general_domain) - subquery_domain.append((field_name, 'in', list(set(pycompat.values(query_goals))))) + subquery_domain.append((field_name, 'in', list(set(query_goals.values())))) if start_date: subquery_domain.append((field_date_name, '>=', start_date)) if end_date: @@ -353,7 +353,7 @@ class Goal(models.Model): goals_to_write.update(goal._get_write_values(new_value)) - for goal, values in pycompat.items(goals_to_write): + for goal, values in goals_to_write.items(): if not values: continue goal.write(values) diff --git a/addons/google_calendar/models/google_calendar.py b/addons/google_calendar/models/google_calendar.py index 0303dbe5154..ac91835f530 100644 --- a/addons/google_calendar/models/google_calendar.py +++ b/addons/google_calendar/models/google_calendar.py @@ -12,7 +12,7 @@ import pytz from werkzeug import urls from odoo import api, fields, models, tools, _ -from odoo.tools import exception_to_unicode, pycompat +from odoo.tools import exception_to_unicode _logger = logging.getLogger(__name__) @@ -25,15 +25,13 @@ class Meta(type): """ This Meta class allow to define class as a structure, and so instancied variable in __init__ to avoid to have side effect alike 'static' variable """ def __new__(typ, name, parents, attrs): - methods = {k: v for k, v in pycompat.items(attrs) - if callable(v)} - attrs = {k: v for k, v in pycompat.items(attrs) - if not callable(v)} + methods = {k: v for k, v in attrs.items() if callable(v)} + attrs = {k: v for k, v in attrs.items() if not callable(v)} def init(self, **kw): - for key, val in pycompat.items(attrs): + for key, val in attrs.items(): setattr(self, key, val) - for key, val in pycompat.items(kw): + for key, val in kw.items(): assert key in attrs setattr(self, key, val) @@ -161,7 +159,7 @@ class SyncOperation(object): def __init__(self, src, info, **kw): self.src = src self.info = info - for key, val in pycompat.items(kw): + for key, val in kw.items(): setattr(self, key, val) def __str__(self): @@ -689,7 +687,7 @@ class GoogleCalendar(models.AbstractModel): my_google_attendees = CalendarAttendee.with_context(context_novirtual).search([ ('partner_id', '=', my_partner_id), - ('google_internal_event_id', 'in', pycompat.keys(all_event_from_google)) + ('google_internal_event_id', 'in', all_event_from_google.keys()) ]) my_google_att_ids = my_google_attendees.ids @@ -753,7 +751,7 @@ class GoogleCalendar(models.AbstractModel): ev_to_sync.OE.status = event.active ev_to_sync.OE.synchro = att.oe_synchro_date - for event in pycompat.values(all_event_from_google): + for event in all_event_from_google.values(): event_id = event.get('id') base_event_id = event_id.rsplit('_', 1)[0] @@ -788,7 +786,7 @@ class GoogleCalendar(models.AbstractModel): # DO ACTION # ###################### for base_event in event_to_synchronize: - event_to_synchronize[base_event] = sorted(pycompat.items(event_to_synchronize[base_event]), key=operator.itemgetter(0)) + event_to_synchronize[base_event] = sorted(event_to_synchronize[base_event].items(), key=operator.itemgetter(0)) for current_event in event_to_synchronize[base_event]: self.env.cr.commit() event = current_event[1] # event is an Sync Event ! diff --git a/addons/hr/models/hr.py b/addons/hr/models/hr.py index ccfe751025b..d3bb213cb6a 100644 --- a/addons/hr/models/hr.py +++ b/addons/hr/models/hr.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - +import base64 import logging from odoo import api, fields, models from odoo import tools, _ from odoo.exceptions import ValidationError from odoo.modules.module import get_module_resource -from odoo.tools import pycompat _logger = logging.getLogger(__name__) @@ -103,7 +102,7 @@ class Employee(models.Model): @api.model def _default_image(self): image_path = get_module_resource('hr', 'static/src/img', 'default_image.png') - return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64')) + return tools.image_resize_image_big(base64.b64encode(open(image_path, 'rb').read())) # resource and user name = fields.Char(related='resource_id.name', store=True, oldname='name_related') @@ -250,7 +249,7 @@ class Employee(models.Model): if auto_follow_fields is None: auto_follow_fields = ['user_id'] user_field_lst = [] - for name, field in pycompat.items(self._fields): + for name, field in self._fields.items(): if name in auto_follow_fields and name in updated_fields and field.comodel_name == 'res.users': user_field_lst.append(name) return user_field_lst diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index ce0054a5636..6699dddeb2a 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -5,7 +5,7 @@ import re from odoo import api, fields, models, _ from odoo.exceptions import UserError -from odoo.tools import email_split, float_is_zero, pycompat +from odoo.tools import email_split, float_is_zero from odoo.addons import decimal_precision as dp @@ -243,7 +243,7 @@ class HrExpense(models.Model): expense.sheet_id.write({'account_move_id': move.id}) if expense.payment_mode == 'company_account': expense.sheet_id.paid_expense_sheets() - for move in pycompat.values(move_group_by_sheet): + for move in move_group_by_sheet.values(): move.post() return True diff --git a/addons/hr_payroll/models/hr_payslip.py b/addons/hr_payroll/models/hr_payslip.py index fd30b7a1b23..2f310c3456e 100644 --- a/addons/hr_payroll/models/hr_payslip.py +++ b/addons/hr_payroll/models/hr_payslip.py @@ -11,8 +11,6 @@ import babel from odoo import api, fields, models, tools, _ from odoo.addons import decimal_precision as dp from odoo.exceptions import UserError, ValidationError -from odoo.tools import pycompat - class HrPayslip(models.Model): _name = 'hr.payslip' @@ -205,7 +203,7 @@ class HrPayslip(models.Model): } res.append(attendances) - res.extend(pycompat.values(leaves)) + res.extend(leaves.values()) return res @api.model @@ -369,7 +367,7 @@ class HrPayslip(models.Model): #blacklist this rule and its children blacklist += [id for id, seq in rule._recursive_search_of_rules()] - return [value for code, value in pycompat.items(result_dict)] + return list(result_dict.values()) # YTI TODO To rename. This method is not really an onchange, as it is not in any view # employee_id and contract_id could be browse records diff --git a/addons/hr_payroll/report/report_payslip_details.py b/addons/hr_payroll/report/report_payslip_details.py index 23043d2edb9..ffd5b9d913d 100644 --- a/addons/hr_payroll/report/report_payslip_details.py +++ b/addons/hr_payroll/report/report_payslip_details.py @@ -2,8 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, models -from odoo.tools import pycompat - class PayslipDetailsReport(models.AbstractModel): _name = 'report.hr_payroll.report_payslipdetails' @@ -38,9 +36,9 @@ class PayslipDetailsReport(models.AbstractModel): result.setdefault(x[2], {}) result[x[2]].setdefault(x[1], []) result[x[2]][x[1]].append(x[0]) - for payslip_id, lines_dict in pycompat.items(result): + for payslip_id, lines_dict in result.items(): res.setdefault(payslip_id, []) - for rule_categ_id, line_ids in pycompat.items(lines_dict): + for rule_categ_id, line_ids in lines_dict.items(): rule_categories = RuleCateg.browse(rule_categ_id) lines = PayslipLine.browse(line_ids) level = 0 @@ -70,9 +68,9 @@ class PayslipDetailsReport(models.AbstractModel): result.setdefault(line.slip_id.id, {}) result[line.slip_id.id].setdefault(line.register_id, line) result[line.slip_id.id][line.register_id] |= line - for payslip_id, lines_dict in pycompat.items(result): + for payslip_id, lines_dict in result.items(): res.setdefault(payslip_id, []) - for register, lines in pycompat.items(lines_dict): + for register, lines in lines_dict.items(): res[payslip_id].append({ 'register_name': register.name, 'total': sum(lines.mapped('total')), diff --git a/addons/hr_recruitment/models/hr_recruitment.py b/addons/hr_recruitment/models/hr_recruitment.py index adec7b15f57..4e4a694e9eb 100644 --- a/addons/hr_recruitment/models/hr_recruitment.py +++ b/addons/hr_recruitment/models/hr_recruitment.py @@ -4,7 +4,6 @@ from datetime import datetime from odoo import api, fields, models, tools, SUPERUSER_ID -from odoo.tools import pycompat from odoo.tools.translate import _ from odoo.exceptions import UserError @@ -251,7 +250,7 @@ class Applicant(models.Model): self = self.with_context(default_department_id=vals.get('department_id')) if vals.get('job_id') or self._context.get('default_job_id'): job_id = vals.get('job_id') or self._context.get('default_job_id') - for key, value in pycompat.items(self._onchange_job_id_internal(job_id)['value']): + for key, value in self._onchange_job_id_internal(job_id)['value'].items(): if key not in vals: vals[key] = value if vals.get('user_id'): diff --git a/addons/hw_escpos/escpos/escpos.py b/addons/hw_escpos/escpos/escpos.py index e50537e0eeb..7159ac3b31e 100644 --- a/addons/hw_escpos/escpos/escpos.py +++ b/addons/hw_escpos/escpos/escpos.py @@ -12,6 +12,8 @@ from hashlib import md5 from PIL import Image from xml.etree import ElementTree as ET +from odoo.tools import pycompat + try: import jcconv except ImportError: @@ -27,7 +29,7 @@ from .exceptions import * def utfstr(stuff): """ converts stuff to string and does without failing if stuff is a utf8 string """ - if isinstance(stuff,basestring): + if isinstance(stuff,pycompat.string_types): return stuff else: return str(stuff) @@ -431,7 +433,7 @@ class Escpos: print('print_b64_img') - id = md5(img).digest() + id = md5(img.encode('utf-8')).digest() if id not in self.img_cache: print('not in cache') diff --git a/addons/im_livechat/models/im_livechat_channel.py b/addons/im_livechat/models/im_livechat_channel.py index 1c72ba8725d..c2741fcef92 100644 --- a/addons/im_livechat/models/im_livechat_channel.py +++ b/addons/im_livechat/models/im_livechat_channel.py @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - +import base64 import random import re from datetime import datetime, timedelta from odoo import api, fields, models, modules, tools -from odoo.tools import pycompat - class ImLivechatChannel(models.Model): """ Livechat Channel @@ -21,7 +19,7 @@ class ImLivechatChannel(models.Model): def _default_image(self): image_path = modules.get_module_resource('im_livechat', 'static/src/img', 'default.png') - return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64')) + return tools.image_resize_image_big(base64.b64encode(open(image_path, 'rb').read())) def _default_user_ids(self): return [(6, 0, [self._uid])] @@ -96,7 +94,7 @@ class ImLivechatChannel(models.Model): for record in self: dt = fields.Datetime.to_string(datetime.utcnow() - timedelta(days=7)) repartition = record.channel_ids.rating_get_grades([('create_date', '>=', dt)]) - total = sum(pycompat.values(repartition)) + total = sum(repartition.values()) if total > 0: happy = repartition['great'] record.rating_percentage_satisfaction = ((happy*100) / total) if happy > 0 else 0 diff --git a/addons/im_livechat/models/mail_channel.py b/addons/im_livechat/models/mail_channel.py index 32d66a0154c..c64bcd7108c 100644 --- a/addons/im_livechat/models/mail_channel.py +++ b/addons/im_livechat/models/mail_channel.py @@ -2,8 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models, _ -from odoo.tools import pycompat - class ChannelPartner(models.Model): _inherit = 'mail.channel.partner' @@ -78,7 +76,7 @@ class MailChannel(models.Model): last_msg = self.env['mail.message'].search([("channel_ids", "in", [channel.id])], limit=1) if last_msg: channel_infos_dict[channel.id]['last_message_date'] = last_msg.date - return list(pycompat.values(channel_infos_dict)) + return list(channel_infos_dict.values()) @api.model def channel_fetch_slot(self): diff --git a/addons/l10n_be_intrastat/wizard/xml_decl.py b/addons/l10n_be_intrastat/wizard/xml_decl.py index f7d01db30d1..b5486d963c9 100644 --- a/addons/l10n_be_intrastat/wizard/xml_decl.py +++ b/addons/l10n_be_intrastat/wizard/xml_decl.py @@ -6,6 +6,7 @@ from xml.etree import ElementTree as ET from collections import namedtuple from odoo import api, exceptions, fields, models, _ +from odoo.tools.pycompat import text_type INTRASTAT_XMLNS = 'http://www.onegate.eu/2010-01-01' @@ -264,18 +265,18 @@ class XmlDeclaration(models.TransientModel): continue numlgn += 1 item = ET.SubElement(datas, 'Item') - self._set_Dim(item, 'EXSEQCODE', unicode(numlgn)) - self._set_Dim(item, 'EXTRF', unicode(linekey.EXTRF)) - self._set_Dim(item, 'EXCNT', unicode(linekey.EXCNT)) - self._set_Dim(item, 'EXTTA', unicode(linekey.EXTTA)) - self._set_Dim(item, 'EXREG', unicode(linekey.EXREG)) - self._set_Dim(item, 'EXTGO', unicode(linekey.EXGO)) + self._set_Dim(item, 'EXSEQCODE', text_type(numlgn)) + self._set_Dim(item, 'EXTRF', text_type(linekey.EXTRF)) + self._set_Dim(item, 'EXCNT', text_type(linekey.EXCNT)) + self._set_Dim(item, 'EXTTA', text_type(linekey.EXTTA)) + self._set_Dim(item, 'EXREG', text_type(linekey.EXREG)) + self._set_Dim(item, 'EXTGO', text_type(linekey.EXGO)) if extendedmode: - self._set_Dim(item, 'EXTPC', unicode(linekey.EXTPC)) - self._set_Dim(item, 'EXDELTRM', unicode(linekey.EXDELTRM)) - self._set_Dim(item, 'EXTXVAL', unicode(round(amounts[0], 0)).replace(".", ",")) - self._set_Dim(item, 'EXWEIGHT', unicode(round(amounts[1], 0)).replace(".", ",")) - self._set_Dim(item, 'EXUNITS', unicode(round(amounts[2], 0)).replace(".", ",")) + self._set_Dim(item, 'EXTPC', text_type(linekey.EXTPC)) + self._set_Dim(item, 'EXDELTRM', text_type(linekey.EXDELTRM)) + self._set_Dim(item, 'EXTXVAL', text_type(round(amounts[0], 0)).replace(".", ",")) + self._set_Dim(item, 'EXWEIGHT', text_type(round(amounts[1], 0)).replace(".", ",")) + self._set_Dim(item, 'EXUNITS', text_type(round(amounts[2], 0)).replace(".", ",")) if numlgn == 0: #no datas diff --git a/addons/l10n_fr_fec/wizard/account_fr_fec.py b/addons/l10n_fr_fec/wizard/account_fr_fec.py index 215ed49cb4e..b8c4b571124 100644 --- a/addons/l10n_fr_fec/wizard/account_fr_fec.py +++ b/addons/l10n_fr_fec/wizard/account_fr_fec.py @@ -4,11 +4,11 @@ # Copyright (C) 2013-2015 Akretion (http://www.akretion.com) import base64 -import csv -import StringIO +import io from odoo import api, fields, models, _ from odoo.exceptions import Warning +from odoo.tools import pycompat class AccountFrFec(models.TransientModel): @@ -86,24 +86,24 @@ class AccountFrFec(models.TransientModel): # So it will be easier for the accountant to check the file before # sending it to the fiscal administration header = [ - 'JournalCode', # 0 - 'JournalLib', # 1 - 'EcritureNum', # 2 - 'EcritureDate', # 3 - 'CompteNum', # 4 - 'CompteLib', # 5 - 'CompAuxNum', # 6 We use partner.id - 'CompAuxLib', # 7 - 'PieceRef', # 8 - 'PieceDate', # 9 - 'EcritureLib', # 10 - 'Debit', # 11 - 'Credit', # 12 - 'EcritureLet', # 13 - 'DateLet', # 14 - 'ValidDate', # 15 - 'Montantdevise', # 16 - 'Idevise', # 17 + u'JournalCode', # 0 + u'JournalLib', # 1 + u'EcritureNum', # 2 + u'EcritureDate', # 3 + u'CompteNum', # 4 + u'CompteLib', # 5 + u'CompAuxNum', # 6 We use partner.id + u'CompAuxLib', # 7 + u'PieceRef', # 8 + u'PieceDate', # 9 + u'EcritureLib', # 10 + u'Debit', # 11 + u'Credit', # 12 + u'EcritureLet', # 13 + u'DateLet', # 14 + u'ValidDate', # 15 + u'Montantdevise', # 16 + u'Idevise', # 17 ] company = self.env.user.company_id @@ -114,8 +114,8 @@ class AccountFrFec(models.TransientModel): raise Warning( _("FEC is for French companies only !")) - fecfile = StringIO.StringIO() - w = csv.writer(fecfile, delimiter='|') + fecfile = io.BytesIO() + w = pycompat.csv_writer(fecfile, delimiter='|') w.writerow(header) # INITIAL BALANCE @@ -190,7 +190,7 @@ class AccountFrFec(models.TransientModel): else: listrow[11] = '0,00' listrow[12] = str(-listrow_amount).replace('.', ',') - w.writerow([s.encode("utf-8") for s in listrow]) + w.writerow(listrow) #if the unaffected earnings account wasn't in the selection yet: add it manually if (not unaffected_earnings_line and unaffected_earnings_results @@ -201,7 +201,7 @@ class AccountFrFec(models.TransientModel): if unaffected_earnings_account: unaffected_earnings_results[4] = unaffected_earnings_account.code unaffected_earnings_results[5] = unaffected_earnings_account.name - w.writerow([s.encode("utf-8") for s in unaffected_earnings_results]) + w.writerow(unaffected_earnings_results) # LINES sql_query = ''' @@ -266,8 +266,7 @@ class AccountFrFec(models.TransientModel): sql_query, (self.date_from, self.date_to, company.id)) for row in self._cr.fetchall(): - listrow = list(row) - w.writerow([s.encode("utf-8") for s in listrow]) + w.writerow(list(row)) siren = company.vat[4:13] end_date = self.date_to.replace('-', '') diff --git a/addons/l10n_in_hr_payroll/report/report_hr_yearly_salary_detail.py b/addons/l10n_in_hr_payroll/report/report_hr_yearly_salary_detail.py index 001a5a782f8..0d26efe9bf5 100644 --- a/addons/l10n_in_hr_payroll/report/report_hr_yearly_salary_detail.py +++ b/addons/l10n_in_hr_payroll/report/report_hr_yearly_salary_detail.py @@ -5,8 +5,6 @@ from datetime import date from odoo import api, models, _ from odoo.exceptions import UserError -from odoo.tools import pycompat - class EmployeesYearlySalaryReport(models.AbstractModel): _name = 'report.l10n_in_hr_payroll.report_hryearlysalary' @@ -99,7 +97,7 @@ class EmployeesYearlySalaryReport(models.AbstractModel): def salary_list(self, salaries): cat_salary_all = [] - for category_name, amount in pycompat.items(salaries): + for category_name, amount in salaries.items(): cat_salary = [] total = 0.0 cat_salary.append(category_name) diff --git a/addons/l10n_lu/scripts/tax2csv.py b/addons/l10n_lu/scripts/tax2csv.py index 9993272573f..23fec3c734c 100644 --- a/addons/l10n_lu/scripts/tax2csv.py +++ b/addons/l10n_lu/scripts/tax2csv.py @@ -1,18 +1,8 @@ from collections import OrderedDict -import csv import xlrd from odoo.tools import pycompat -def _e(s): - if type(s) is unicode: - return s.encode('utf8') - elif s is None: - return '' - else: - return str(s) - - def _is_true(s): return s not in ('F', 'False', 0, '', None, False) @@ -56,9 +46,9 @@ class LuxTaxGenerator: yield OrderedDict(pycompat.izip(keys, row)) def tax_codes_to_csv(self): - writer = csv.writer(open('account.tax.code.template-%s.csv' % + writer = pycompat.csv_writer(open('account.tax.code.template-%s.csv' % self.suffix, 'wb')) - tax_codes_iterator = self.iter_tax_codes + tax_codes_iterator = self.iter_tax_codes() keys = next(tax_codes_iterator) writer.writerow(keys) @@ -69,7 +59,7 @@ class LuxTaxGenerator: if tax_code in tax_codes: raise RuntimeError('duplicate tax code %s' % tax_code) tax_codes[tax_code] = row['id'] - writer.writerow(pycompat.imap(_e, pycompat.values(row))) + writer.writerow([pycompat.to_text(v) for v in row.values()]) # read taxes and add leaf tax codes new_tax_codes = {} # id: parent_code @@ -145,17 +135,19 @@ class LuxTaxGenerator: for tax_code_id in sorted(new_tax_codes): name, parent_code = new_tax_codes[tax_code_id] - writer.writerow((tax_code_id, - 'lu_tct_m' + parent_code, - tax_code_id.replace('lu_tax_code_template_', ''), - '1', - '', - _e(name), - '')) + writer.writerow([ + tax_code_id, + u'lu_tct_m' + parent_code, + tax_code_id.replace('lu_tax_code_template_', u''), + u'1', + u'', + pycompat.to_text(name), + u'' + ]) def taxes_to_csv(self): - writer = csv.writer(open('account.tax.template-%s.csv' % - self.suffix, 'wb')) + writer = pycompat.csv_writer(open('account.tax.template-%s.csv' % + self.suffix, 'wb')) taxes_iterator = self.iter_taxes() keys = next(taxes_iterator) writer.writerow(keys[3:] + ['sequence']) @@ -168,17 +160,20 @@ class LuxTaxGenerator: cur_seq = seq + 1000 else: cur_seq = seq - writer.writerow(list(pycompat.imap(_e, list(pycompat.values(row))[3:])) + [cur_seq]) + writer.writerow([ + pycompat.to_text(v) + for v in list(row.values())[3:] + ] + [cur_seq]) def fiscal_pos_map_to_csv(self): - writer = csv.writer(open('account.fiscal.' - 'position.tax.template-%s.csv' % - self.suffix, 'wb')) + writer = pycompat.csv_writer(open('account.fiscal.' + 'position.tax.template-%s.csv' % + self.suffix, 'wb')) fiscal_pos_map_iterator = self.iter_fiscal_pos_map() keys = next(fiscal_pos_map_iterator) writer.writerow(keys) for row in fiscal_pos_map_iterator: - writer.writerow(pycompat.imap(_e, pycompat.values(row))) + writer.writerow([pycompat.to_text(s) for s in row.values()]) if __name__ == '__main__': diff --git a/addons/link_tracker/models/link_tracker.py b/addons/link_tracker/models/link_tracker.py index 1d9a7cdf561..509fc21e153 100644 --- a/addons/link_tracker/models/link_tracker.py +++ b/addons/link_tracker/models/link_tracker.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - +import base64 import datetime import random import re @@ -12,7 +12,7 @@ from werkzeug import urls, utils from odoo import models, fields, api, _ -from odoo.tools import ustr, pycompat +from odoo.tools import ustr URL_REGEX = r'(\bhref=[\'"](?!mailto:)([^\'"]+)[\'"])' @@ -90,7 +90,7 @@ class link_tracker(models.Model): @api.one @api.depends('favicon') def _compute_icon_src(self): - self.icon_src = 'data:image/png;base64,' + self.favicon + self.icon_src = b'data:image/png;base64,' + self.favicon @api.one @api.depends('url') @@ -123,7 +123,7 @@ class link_tracker(models.Model): def _compute_favicon(self): try: icon = requests.get('http://www.google.com/s2/favicons', params={'domain': self.url}, timeout=5).content - icon_base64 = icon.encode('base64').replace("\n", "") + icon_base64 = base64.b64encode(icon).replace(b"\n", b"").decode('ascii') 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' @@ -165,7 +165,7 @@ class link_tracker(models.Model): create_vals['url'] = VALIDATE_URL(vals['url']) search_domain = [] - for fname, value in pycompat.items(create_vals): + for fname, value in create_vals.items(): search_domain.append((fname, '=', value)) result = self.search(search_domain, limit=1) diff --git a/addons/lunch/models/lunch.py b/addons/lunch/models/lunch.py index ee8bba3492e..e9c13060fc3 100644 --- a/addons/lunch/models/lunch.py +++ b/addons/lunch/models/lunch.py @@ -10,8 +10,6 @@ from odoo import api, fields, models, _ from odoo.exceptions import AccessError, ValidationError from odoo.addons import decimal_precision as dp -from odoo.tools import pycompat - class LunchOrder(models.Model): """ @@ -26,10 +24,10 @@ class LunchOrder(models.Model): prev_order = self.env['lunch.order.line'].search([('user_id', '=', self.env.uid), ('product_id.active', '!=', False)], limit=20, order='id desc') # If we return return prev_order.ids, we will have duplicates (identical orders). # Therefore, this following part removes duplicates based on product_id and note. - return list(pycompat.values({ + return list({ (order.product_id, order.note): order.id for order in prev_order - })) + }.values()) user_id = fields.Many2one('res.users', 'User', readonly=True, states={'new': [('readonly', False)]}, @@ -88,10 +86,10 @@ class LunchOrder(models.Model): prev_order = self.env['lunch.order.line'].search([('user_id', '=', self.env.uid), ('product_id.active', '!=', False)], limit=20, order='date desc, id desc') # If we use prev_order.ids, we will have duplicates (identical orders). # Therefore, this following part removes duplicates based on product_id and note. - self.previous_order_ids = list(pycompat.values({ + self.previous_order_ids = list({ (order.product_id, order.note): order.id for order in prev_order - })) + }.values()) if self.previous_order_ids: lunch_data = {} diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py index 95165441dfd..52ea655c078 100644 --- a/addons/mail/controllers/main.py +++ b/addons/mail/controllers/main.py @@ -7,14 +7,13 @@ import logging import psycopg2 import werkzeug -from operator import itemgetter from werkzeug import url_encode from odoo import api, http, registry, SUPERUSER_ID, _ from odoo.addons.web.controllers.main import binary_content from odoo.exceptions import AccessError from odoo.http import request -from odoo.tools import consteq +from odoo.tools import consteq, pycompat _logger = logging.getLogger(__name__) @@ -102,7 +101,7 @@ class MailController(http.Controller): """ End-point to receive mail from an external SMTP server. """ dbs = req.jsonrequest.get('databases') for db in dbs: - message = dbs[db].decode('base64') + message = base64.b64decode(dbs[db]) try: db_registry = registry(db) with db_registry.cursor() as cr: @@ -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') @@ -183,7 +182,7 @@ class MailController(http.Controller): else: # either a wrong message_id, either someone trying ids -> just go to messaging return self._redirect_to_messaging() - elif res_id and isinstance(res_id, basestring): + elif res_id and isinstance(res_id, pycompat.string_types): res_id = int(res_id) return self._redirect_to_record(model, res_id, access_token) diff --git a/addons/mail/models/ir_model.py b/addons/mail/models/ir_model.py index e2231e38f5d..01862352e96 100644 --- a/addons/mail/models/ir_model.py +++ b/addons/mail/models/ir_model.py @@ -3,6 +3,7 @@ from odoo import _, api, fields, models from odoo.exceptions import UserError +from odoo.tools import pycompat class IrModel(models.Model): @@ -41,7 +42,7 @@ class IrModel(models.Model): model_class = super(IrModel, self)._instanciate(model_data) if model_data.get('is_mail_thread') and model_class._name != 'mail.thread': parents = model_class._inherit or [] - parents = [parents] if isinstance(parents, basestring) else parents + parents = [parents] if isinstance(parents, pycompat.string_types) else parents model_class._inherit = parents + ['mail.thread'] return model_class diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index fac6edfff98..31e03b99081 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - +import base64 from email.utils import formataddr import re @@ -9,7 +9,7 @@ import uuid from odoo import _, api, fields, models, modules, tools from odoo.exceptions import UserError from odoo.osv import expression -from odoo.tools import ormcache, pycompat +from odoo.tools import ormcache from odoo.tools.safe_eval import safe_eval @@ -41,7 +41,7 @@ class Channel(models.Model): def _get_default_image(self): image_path = modules.get_module_resource('mail', 'static/src/img', 'groupdefault.png') - return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64')) + return tools.image_resize_image_big(base64.b64encode(open(image_path, 'rb').read())) @api.model def default_get(self, fields): @@ -619,7 +619,7 @@ class Channel(models.Model): channel = channels_preview[message['id']] del(channel['message_id']) channel['last_message'] = message - return list(pycompat.values(channels_preview)) + return list(channels_preview.values()) #------------------------------------------------------ # Commands diff --git a/addons/mail/models/mail_followers.py b/addons/mail/models/mail_followers.py index a6bd60f82c9..3edcfa748c3 100644 --- a/addons/mail/models/mail_followers.py +++ b/addons/mail/models/mail_followers.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models -from odoo.tools import pycompat class Followers(models.Model): @@ -42,7 +41,7 @@ class Followers(models.Model): using the subtypes given in the parameters """ res_model_id = self.env['ir.model']._get(res_model).id - force_mode = force or (all(data for data in pycompat.values(partner_data)) and all(data for data in pycompat.values(channel_data))) + force_mode = force or (all(partner_data.values()) and all(channel_data.values())) generic = [] specific = {} existing = {} # {res_id: follower_ids} @@ -71,13 +70,13 @@ class Followers(models.Model): if force_mode: employee_pids = self.env['res.users'].sudo().search([('partner_id', 'in', list(partner_data)), ('share', '=', False)]).mapped('partner_id').ids - for pid, data in pycompat.items(partner_data): + for pid, data in partner_data.items(): if not data: if pid not in employee_pids: partner_data[pid] = external_default_subtypes.ids else: partner_data[pid] = default_subtypes.ids - for cid, data in pycompat.items(channel_data): + for cid, data in channel_data.items(): if not data: channel_data[cid] = default_subtypes.ids diff --git a/addons/mail/models/mail_mail.py b/addons/mail/models/mail_mail.py index 3f206424e07..e723f71bdc6 100644 --- a/addons/mail/models/mail_mail.py +++ b/addons/mail/models/mail_mail.py @@ -13,7 +13,6 @@ from email.utils import formataddr from odoo import _, api, fields, models from odoo import tools from odoo.addons.base.ir.ir_mail_server import MailDeliveryException -from odoo.tools import pycompat from odoo.tools.safe_eval import safe_eval _logger = logging.getLogger(__name__) @@ -169,7 +168,7 @@ class MailMail(models.Model): - else fallback on mail.email_to splitting """ self.ensure_one() if partner: - email_to = [formataddr((partner.name, partner.email))] + email_to = [formataddr((partner.name or 'False', partner.email or 'False'))] else: email_to = tools.email_split_and_format(self.email_to) return email_to @@ -205,7 +204,7 @@ class MailMail(models.Model): groups[mail.mail_server_id.id].append(mail.id) sys_params = self.env['ir.config_parameter'].sudo() batch_size = int(sys_params.get_param('mail.session.batch.size', 1000)) - for server_id, record_ids in pycompat.items(groups): + for server_id, record_ids in groups.items(): for mail_batch in tools.split_every(batch_size, record_ids): yield server_id, mail_batch diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index 2970933e704..182e3d6a19c 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -9,7 +9,6 @@ from email.utils import formataddr from odoo import _, api, fields, models, modules, SUPERUSER_ID, tools from odoo.exceptions import UserError, AccessError from odoo.osv import expression -from odoo.tools import pycompat _logger = logging.getLogger(__name__) _image_dataurl = re.compile(r'(data:image/[a-z]+?);base64,([a-z0-9+/]{3,}=*)([\'"])', re.I) @@ -282,7 +281,7 @@ class Message(models.Model): partners = self.env['res.partner'].sudo() attachments = self.env['ir.attachment'] trackings = self.env['mail.tracking.value'] - for key, message in pycompat.items(message_tree): + for key, message in message_tree.items(): if message.author_id: partners |= message.author_id if message.subtype_id and message.partner_ids: # take notified people of message with a subtype @@ -446,7 +445,7 @@ class Message(models.Model): def _find_allowed_doc_ids(self, model_ids): IrModelAccess = self.env['ir.model.access'] allowed_ids = set() - for doc_model, doc_dict in pycompat.items(model_ids): + for doc_model, doc_dict in model_ids.items(): if not IrModelAccess.check(doc_model, 'read', False): continue allowed_ids |= self._find_allowed_model_wise(doc_model, doc_dict) @@ -607,17 +606,17 @@ class Message(models.Model): # Author condition (READ, WRITE, CREATE (private)) author_ids = [] if operation == 'read' or operation == 'write': - author_ids = [mid for mid, message in pycompat.items(message_values) + author_ids = [mid for mid, message in message_values.items() if message.get('author_id') and message.get('author_id') == self.env.user.partner_id.id] elif operation == 'create': - author_ids = [mid for mid, message in pycompat.items(message_values) + author_ids = [mid for mid, message in message_values.items() if not message.get('model') and not message.get('res_id')] # Parent condition, for create (check for received notifications for the created message parent) notified_ids = [] if operation == 'create': # TDE: probably clean me - parent_ids = [message.get('parent_id') for mid, message in pycompat.items(message_values) + parent_ids = [message.get('parent_id') for message in message_values.values() if message.get('parent_id')] self._cr.execute("""SELECT DISTINCT m.id, partner_rel.res_partner_id, channel_partner.partner_id FROM "%s" m LEFT JOIN "mail_message_res_partner_rel" partner_rel @@ -630,37 +629,37 @@ class Message(models.Model): ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = (%%s) WHERE m.id = ANY (%%s)""" % self._table, (self.env.user.partner_id.id, self.env.user.partner_id.id, parent_ids,)) not_parent_ids = [mid[0] for mid in self._cr.fetchall() if any([mid[1], mid[2]])] - notified_ids += [mid for mid, message in pycompat.items(message_values) + notified_ids += [mid for mid, message in message_values.items() if message.get('parent_id') in not_parent_ids] # Recipients condition, for read and write (partner_ids) and create (message_follower_ids) other_ids = set(self.ids).difference(set(author_ids), set(notified_ids)) model_record_ids = _generate_model_record_ids(message_values, other_ids) if operation in ['read', 'write']: - notified_ids = [mid for mid, message in pycompat.items(message_values) if message.get('notified')] + notified_ids = [mid for mid, message in message_values.items() if message.get('notified')] elif operation == 'create': - for doc_model, doc_ids in pycompat.items(model_record_ids): + for doc_model, doc_ids in model_record_ids.items(): followers = self.env['mail.followers'].sudo().search([ ('res_model', '=', doc_model), ('res_id', 'in', list(doc_ids)), ('partner_id', '=', self.env.user.partner_id.id), ]) fol_mids = [follower.res_id for follower in followers] - notified_ids += [mid for mid, message in pycompat.items(message_values) + notified_ids += [mid for mid, message in message_values.items() if message.get('model') == doc_model and message.get('res_id') in fol_mids] # CRUD: Access rights related to the document other_ids = other_ids.difference(set(notified_ids)) model_record_ids = _generate_model_record_ids(message_values, other_ids) document_related_ids = [] - for model, doc_ids in pycompat.items(model_record_ids): + for model, doc_ids in model_record_ids.items(): DocumentModel = self.env[model] mids = DocumentModel.browse(doc_ids).exists() if hasattr(DocumentModel, 'check_mail_message_access'): DocumentModel.check_mail_message_access(mids.ids, operation) # ?? mids ? else: self.env['mail.thread'].check_mail_message_access(mids.ids, operation, model_name=model) - document_related_ids += [mid for mid, message in pycompat.items(message_values) + document_related_ids += [mid for mid, message in message_values.items() if message.get('model') == model and message.get('res_id') in mids.ids] # Calculate remaining ids: if not void, raise an error @@ -745,7 +744,7 @@ class Message(models.Model): values['attachment_ids'].append((4, attachment.id)) data_to_url[key] = '/web/image/%s' % attachment.id return '%s%s alt="%s"' % (data_to_url[key], match.group(3), name) - values['body'] = _image_dataurl.sub(base64_to_boundary, values['body']) + values['body'] = _image_dataurl.sub(base64_to_boundary, tools.ustr(values['body'])) message = super(Message, self).create(values) message._invalidate_documents() diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py index e9bf605c382..1bd92aff430 100644 --- a/addons/mail/models/mail_template.py +++ b/addons/mail/models/mail_template.py @@ -45,7 +45,7 @@ def format_tz(env, dt, tz=False, format=False): return format_datetime(ts, format or 'medium', locale=env.context.get("lang") or 'en_US') if format: - return ts.strftime(format) + return pycompat.text_type(ts.strftime(format)) else: lang = env.context.get("lang") langs = env['res.lang'] @@ -54,9 +54,9 @@ def format_tz(env, dt, tz=False, format=False): format_date = langs.date_format or '%B-%d-%Y' format_time = langs.time_format or '%I-%M %p' - fdate = ts.strftime(format_date).decode('utf-8') - ftime = ts.strftime(format_time).decode('utf-8') - return "%s %s%s" % (fdate, ftime, (' (%s)' % tz) if tz else '') + fdate = pycompat.text_type(ts.strftime(format_date)) + ftime = pycompat.text_type(ts.strftime(format_time)) + return u"%s %s%s" % (fdate, ftime, (u' (%s)' % tz) if tz else u'') def format_amount(env, amount, currency): fmt = "%.{0}f".format(currency.decimal_places) @@ -306,8 +306,8 @@ class MailTemplate(models.Model): # form a tree root = lxml.html.fromstring(html) if not len(root) and root.text is None and root.tail is None: - html = '
%s
' % html - root = lxml.html.fromstring(html) + html = u'
%s
' % html + root = lxml.html.fromstring(html, encoding='unicode') base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') base = urls.url_parse(base_url) @@ -324,12 +324,12 @@ class MailTemplate(models.Model): for node in root.iter(): if node.tag == 'a' and node.get('href'): node.set('href', _process_link(node.get('href'))) - elif node.tag == 'img' and not node.get('src', 'data').startswith('data'): + elif node.tag == 'img' and not node.get('src', 'data').startswith(u'data'): node.set('src', _process_link(node.get('src'))) - html = lxml.html.tostring(root, pretty_print=False, method='html') + html = lxml.html.tostring(root, pretty_print=False, method='html', encoding='unicode') # this is ugly, but lxml/etree tostring want to put everything in a 'div' that breaks the editor -> remove that - if html.startswith('
') and html.endswith('
'): + if html.startswith(u'
') and html.endswith(u'
'): html = html[5:-6] return html @@ -379,7 +379,7 @@ class MailTemplate(models.Model): 'user': self.env.user, 'ctx': self._context, # context kw would clash with mako internals } - for res_id, record in pycompat.items(res_to_rec): + for res_id, record in res_to_rec.items(): variables['object'] = record try: render_result = template.render(variables) @@ -391,7 +391,7 @@ class MailTemplate(models.Model): results[res_id] = render_result if post_process: - for res_id, result in pycompat.items(results): + for res_id, result in results.items(): results[res_id] = self.render_post_process(result) return multi_mode and results or results[res_ids[0]] @@ -412,7 +412,7 @@ class MailTemplate(models.Model): self.ensure_one() langs = self.render_template(self.lang, self.model, res_ids) - for res_id, lang in pycompat.items(langs): + for res_id, lang in langs.items(): if lang: template = self.with_context(lang=lang) else: @@ -431,11 +431,11 @@ class MailTemplate(models.Model): if self.use_default_to or self._context.get('tpl_force_default_to'): default_recipients = self.env['mail.thread'].message_get_default_recipients(res_model=self.model, res_ids=res_ids) - for res_id, recipients in pycompat.items(default_recipients): + for res_id, recipients in default_recipients.items(): results[res_id].pop('partner_to', None) results[res_id].update(recipients) - for res_id, values in pycompat.items(results): + for res_id, values in results.items(): partner_ids = values.get('partner_ids', list()) if self._context.get('tpl_partners_only'): mails = tools.email_split(values.pop('email_to', '')) + tools.email_split(values.pop('email_cc', '')) @@ -474,11 +474,11 @@ class MailTemplate(models.Model): # templates: res_id -> template; template -> res_ids templates_to_res_ids = {} - for res_id, template in pycompat.items(res_ids_to_templates): + for res_id, template in res_ids_to_templates.items(): templates_to_res_ids.setdefault(template, []).append(res_id) results = dict() - for template, template_res_ids in pycompat.items(templates_to_res_ids): + for template, template_res_ids in templates_to_res_ids.items(): Template = self.env['mail.template'] # generate fields value for all res_ids linked to the current template if template.lang: @@ -488,7 +488,7 @@ class MailTemplate(models.Model): generated_field_values = Template.render_template( getattr(template, field), template.model, template_res_ids, post_process=(field == 'body_html')) - for res_id, field_value in pycompat.items(generated_field_values): + for res_id, field_value in generated_field_values.items(): results.setdefault(res_id, dict())[field] = field_value # compute recipients if any(field in fields for field in ['email_to', 'partner_to', 'email_cc']): diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 0d23032d051..296f79bed14 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -238,7 +238,7 @@ class MailThread(models.AbstractModel): # auto_subscribe: take values and defaults into account create_values = dict(values) - for key, val in pycompat.items(self._context): + for key, val in self._context.items(): if key.startswith('default_') and key[8:] not in create_values: create_values[key[8:]] = val thread.message_auto_subscribe(list(create_values), values=create_values) @@ -373,7 +373,7 @@ class MailThread(models.AbstractModel): options['display_log_button'] = is_employee # save options on the node node.set('options', repr(options)) - res['arch'] = etree.tostring(doc) + res['arch'] = etree.tostring(doc, encoding='unicode') return res # ------------------------------------------------------ @@ -388,7 +388,7 @@ class MailThread(models.AbstractModel): always tracked fields and modified on_change fields """ tracked_fields = [] - for name, field in pycompat.items(self._fields): + for name, field in self._fields.items(): if getattr(field, 'track_visibility', False): tracked_fields.append(name) @@ -416,13 +416,13 @@ class MailThread(models.AbstractModel): @api.multi def _message_track_post_template(self, tracking): - if not any(change for rec_id, (change, tracking_value_ids) in pycompat.items(tracking)): + if not any(change for rec_id, (change, tracking_value_ids) in tracking.items()): return True templates = self._track_template(tracking) - for field_name, (template, post_kwargs) in pycompat.items(templates): + for field_name, (template, post_kwargs) in templates.items(): if not template: continue - if isinstance(template, basestring): + if isinstance(template, pycompat.string_types): self.message_post_with_view(template, **post_kwargs) else: self.message_post_with_template(template.id, **post_kwargs) @@ -450,7 +450,7 @@ class MailThread(models.AbstractModel): display_values_ids = [] # generate tracked_values data structure: {'col_name': {col_info, new_value, old_value}} - for col_name, col_info in pycompat.items(tracked_fields): + for col_name, col_info in tracked_fields.items(): track_visibility = getattr(self._fields[col_name], 'track_visibility', 'onchange') initial_value = initial[col_name] new_value = getattr(self, col_name) @@ -569,7 +569,7 @@ class MailThread(models.AbstractModel): def _generate_notification_token(self, base_link, params): secret = self.env['ir.config_parameter'].sudo().get_param('database.secret') token = '%s?%s' % (base_link, ' '.join('%s=%s' % (key, params[key]) for key in sorted(params))) - hm = hmac.new(str(secret), token, hashlib.sha1).hexdigest() + hm = hmac.new(secret.encode('utf-8'), token.encode('utf-8'), hashlib.sha1).hexdigest() return hm @api.multi @@ -816,7 +816,7 @@ class MailThread(models.AbstractModel): def message_capable_models(self): """ Used by the plugin addon, based for plugin_outlook and others. """ ret_dict = {} - for model_name, model in pycompat.items(self.env): + for model_name, model in self.env.items(): if hasattr(model, "message_process") and hasattr(model, "message_post"): ret_dict[model_name] = model._description return ret_dict @@ -1266,12 +1266,16 @@ class MailThread(models.AbstractModel): # we don't know its encoding until we parse its headers and hence can't # convert it to utf-8 for transport between the mailgate script and here. if isinstance(message, xmlrpclib.Binary): - message = str(message.data) - # Warning: message_from_string doesn't always work correctly on unicode, - # we must use utf-8 strings here :-( - if isinstance(message, unicode): + message = bytes(message.data) + # message_from_string parses from a *native string*, except apparently + # sometimes message is ISO-8859-1 binary data or some shit and the + # straightforward version (pycompat.to_native) won't work right -> + # always encode message to bytes then use the relevant method + # depending on ~python version + if isinstance(message, pycompat.text_type): message = message.encode('utf-8') - msg_txt = email.message_from_string(message) + extract = getattr(email, 'message_from_bytes', email.message_from_string) + msg_txt = extract(message) # parse the message, verify we are not in a loop by checking message_id is not duplicated msg = self.message_parse(msg_txt, save_original=save_original) @@ -1492,9 +1496,9 @@ class MailThread(models.AbstractModel): 'message_type': 'email', } if not isinstance(message, Message): - if isinstance(message, unicode): - # Warning: message_from_string doesn't always work correctly on unicode, - # we must use utf-8 strings here :-( + # message_from_string works on a native str, so on py2 we need to + # encode incoming unicode strings + if pycompat.PY2 and not isinstance(message, str): message = message.encode('utf-8') message = email.message_from_string(message) @@ -1723,11 +1727,11 @@ class MailThread(models.AbstractModel): cid = info and info.get('cid') else: continue - if isinstance(content, unicode): + if isinstance(content, pycompat.text_type): content = content.encode('utf-8') data_attach = { 'name': name, - 'datas': base64.b64encode(str(content)), + 'datas': base64.b64encode(content), 'type': 'binary', 'datas_fname': name, 'description': name, @@ -1920,7 +1924,7 @@ class MailThread(models.AbstractModel): values['slug'] = slug except ImportError: values['slug'] = lambda self: self.id - if isinstance(views_or_xmlid, basestring): + if isinstance(views_or_xmlid, pycompat.string_types): views = self.env.ref(views_or_xmlid, raise_if_not_found=False) else: views = views_or_xmlid @@ -2052,7 +2056,7 @@ class MailThread(models.AbstractModel): if auto_follow_fields is None: auto_follow_fields = ['user_id'] user_field_lst = [] - for name, field in pycompat.items(self._fields): + for name, field in self._fields.items(): if name in auto_follow_fields and name in updated_fields and getattr(field, 'track_visibility', False) and field.comodel_name == 'res.users': user_field_lst.append(name) return user_field_lst @@ -2148,10 +2152,10 @@ class MailThread(models.AbstractModel): for partner in to_add_users.mapped('partner_id'): new_partners.setdefault(partner.id, None) - for pid, subtypes in pycompat.items(new_partners): + for pid, subtypes in new_partners.items(): subtypes = list(subtypes) if subtypes is not None else None self.message_subscribe(partner_ids=[pid], subtype_ids=subtypes, force=(subtypes != None)) - for cid, subtypes in pycompat.items(new_channels): + for cid, subtypes in new_channels.items(): subtypes = list(subtypes) if subtypes is not None else None self.message_subscribe(channel_ids=[cid], subtype_ids=subtypes, force=(subtypes != None)) diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py index 6379ddb8c42..9140959f89e 100644 --- a/addons/mail/models/res_partner.py +++ b/addons/mail/models/res_partner.py @@ -4,7 +4,6 @@ import logging import threading -from odoo.tools import pycompat from odoo.tools.misc import split_every from odoo import _, api, fields, models, registry, SUPERUSER_ID @@ -175,7 +174,7 @@ class Partner(models.Model): emails = self.env['mail.mail'] recipients_nbr, recipients_max = 0, 50 - for email_type, recipient_template_values in pycompat.items(recipients): + for email_type, recipient_template_values in recipients.items(): if recipient_template_values['followers']: # generate notification email content template_fol_values = dict(base_template_ctx, **recipient_template_values) # fixme: set button_unfollow to none diff --git a/addons/mail/tests/test_mail_features.py b/addons/mail/tests/test_mail_features.py index 92613f55e51..8e2264ad6d3 100644 --- a/addons/mail/tests/test_mail_features.py +++ b/addons/mail/tests/test_mail_features.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import base64 from odoo.addons.mail.tests.common import TestMail from odoo.addons.mail.tests.test_mail_gateway import MAIL_TEMPLATE_PLAINTEXT @@ -89,8 +90,8 @@ class TestMessagePost(TestMail): _body, _body_alt = '

Test Body

', 'Test Body' _subject = 'Test Subject' _attachments = [ - ('List1', 'My first attachment'), - ('List2', 'My second attachment') + ('List1', b'My first attachment'), + ('List2', b'My second attachment') ] _attach_1 = self.env['ir.attachment'].sudo(self.user_employee).create({ 'name': 'Attach1', 'datas_fname': 'Attach1', @@ -129,8 +130,8 @@ class TestMessagePost(TestMail): 'message_post: all atttachments should be linked to the mail.test model') self.assertEqual(set(msg.attachment_ids.mapped('res_id')), set([self.test_pigs.id]), 'message_post: all atttachments should be linked to the pigs group') - self.assertEqual(set([x.decode('base64') for x in msg.attachment_ids.mapped('datas')]), - set(['migration test', _attachments[0][1], _attachments[1][1]])) + self.assertEqual(set([base64.b64decode(x) for x in msg.attachment_ids.mapped('datas')]), + set([b'migration test', _attachments[0][1], _attachments[1][1]])) self.assertTrue(set([_attach_1.id, _attach_2.id]).issubset(msg.attachment_ids.ids), 'message_post: mail.message attachments duplicated') # notifications diff --git a/addons/mail/tests/test_mail_message.py b/addons/mail/tests/test_mail_message.py index db6dc9457a5..566a038d9ef 100644 --- a/addons/mail/tests/test_mail_message.py +++ b/addons/mail/tests/test_mail_message.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - +import base64 import itertools from odoo.addons.mail.tests.common import TestMail @@ -215,7 +215,7 @@ class TestMailMessageAccess(TestMail): def test_mail_message_access_read_notification(self): attachment = self.env['ir.attachment'].create({ - 'datas': 'My attachment'.encode('base64'), + 'datas': base64.b64encode(b'My attachment'), 'name': 'doc.txt', 'datas_fname': 'doc.txt'}) # attach the attachment to the message diff --git a/addons/mail/tests/test_mail_template.py b/addons/mail/tests/test_mail_template.py index 60df04f0654..e5da1c628ae 100644 --- a/addons/mail/tests/test_mail_template.py +++ b/addons/mail/tests/test_mail_template.py @@ -17,13 +17,13 @@ class TestMailTemplate(TestMail): 'name': '_Test_First', 'datas_fname': 'first.txt', - 'datas': base64.b64encode('My first attachment'), + 'datas': base64.b64encode(b'My first attachment'), 'res_model': 'res.partner', 'res_id': self.user_admin.partner_id.id }, { 'name': '_Test_Second', 'datas_fname': 'second.txt', - 'datas': base64.b64encode('My second attachment'), + 'datas': base64.b64encode(b'My second attachment'), 'res_model': 'res.partner', 'res_id': self.user_admin.partner_id.id }] diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 76412ce041c..767462df892 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -140,7 +140,7 @@ class MailComposer(models.TransientModel): for mid, rmod, rid in self._cr.fetchall(): message_values[mid] = {'model': rmod, 'res_id': rid} # remove from the set to check the ids that mail_compose_message accepts - author_ids = [mid for mid, message in pycompat.items(message_values) + author_ids = [mid for mid, message in message_values.items() if message.get('model') and not message.get('res_id')] self = self.browse(list(set(self.ids) - set(author_ids))) # not sure slef = ... @@ -248,7 +248,7 @@ class MailComposer(models.TransientModel): for res_ids in sliced_res_ids: batch_mails = Mail all_mail_values = wizard.get_mail_values(res_ids) - for res_id, mail_values in pycompat.items(all_mail_values): + for res_id, mail_values in all_mail_values.items(): if wizard.composition_mode == 'mass_mail': batch_mails |= Mail.create(mail_values) else: @@ -338,7 +338,7 @@ class MailComposer(models.TransientModel): def onchange_template_id_wrapper(self): self.ensure_one() values = self.onchange_template_id(self.template_id.id, self.composition_mode, self.model, self.res_id)['value'] - for fname, value in pycompat.items(values): + for fname, value in values.items(): setattr(self, fname, value) @api.multi diff --git a/addons/marketing_campaign/models/marketing_campaign.py b/addons/marketing_campaign/models/marketing_campaign.py index 7370633114e..6fea98870d8 100644 --- a/addons/marketing_campaign/models/marketing_campaign.py +++ b/addons/marketing_campaign/models/marketing_campaign.py @@ -9,7 +9,6 @@ import re from odoo import api, fields, models, _ from odoo.exceptions import UserError, ValidationError -from odoo.tools import pycompat from odoo.tools.safe_eval import safe_eval from odoo.addons import decimal_precision as dp @@ -466,7 +465,7 @@ class MarketingCampaignWorkitem(models.Model): matching_workitems = [] for id, res_id, model in res: workitem_map.setdefault(model, {}).setdefault(res_id, set()).add(id) - for model, id_map in pycompat.items(workitem_map): + for model, id_map in workitem_map.items(): Model = self.env[model] condition_name[0] = Model._rec_name condition = [('id', 'in', list(id_map)), condition_name] diff --git a/addons/mass_mailing/controllers/main.py b/addons/mass_mailing/controllers/main.py index 276056311b8..b2e77c2875e 100644 --- a/addons/mass_mailing/controllers/main.py +++ b/addons/mass_mailing/controllers/main.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import base64 import werkzeug @@ -37,7 +38,7 @@ class MassMailController(http.Controller): request.env['mail.mail.statistics'].sudo().set_opened(mail_mail_ids=[mail_id]) response = werkzeug.wrappers.Response() response.mimetype = 'image/gif' - response.data = 'R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='.decode('base64') + response.data = base64.b64decode(b'R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==') return response diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index 44993bdc907..a10e6143c89 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -430,7 +430,7 @@ class MassMailing(models.Model): secret = self.env["ir.config_parameter"].sudo().get_param( "database.secret") token = (self.env.cr.dbname, self.id, int(res_id), tools.ustr(email)) - return hmac.new(str(secret), repr(token), hashlib.sha512).hexdigest() + return hmac.new(secret.encode('utf-8'), repr(token).encode('utf-8'), hashlib.sha512).hexdigest() def _compute_next_departure(self): cron_next_call = self.env.ref('mass_mailing.ir_cron_mass_mailing_queue').sudo().nextcall 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/mrp/models/mrp_workcenter.py b/addons/mrp/models/mrp_workcenter.py index 58226b2e9e8..cc12ec0060b 100644 --- a/addons/mrp/models/mrp_workcenter.py +++ b/addons/mrp/models/mrp_workcenter.py @@ -5,7 +5,6 @@ from dateutil import relativedelta import datetime from odoo import api, exceptions, fields, models, _ -from odoo.tools import pycompat class MrpWorkcenter(models.Model): @@ -74,7 +73,7 @@ class MrpWorkcenter(models.Model): if res_group['state'] in ('pending', 'ready', 'progress'): result_duration_expected[res_group['workcenter_id'][0]] += res_group['duration_expected'] for workcenter in self: - workcenter.workorder_count = sum(count for state, count in pycompat.items(result[workcenter.id]) if state not in ('done', 'cancel')) + workcenter.workorder_count = sum(count for state, count in result[workcenter.id].items() if state not in ('done', 'cancel')) workcenter.workorder_pending_count = result[workcenter.id].get('pending', 0) workcenter.workcenter_load = result_duration_expected[workcenter.id] workcenter.workorder_ready_count = result[workcenter.id].get('ready', 0) diff --git a/addons/mrp_repair/wizard/mrp_repair_make_invoice.py b/addons/mrp_repair/wizard/mrp_repair_make_invoice.py index a659e45e962..03e89da5cd5 100644 --- a/addons/mrp_repair/wizard/mrp_repair_make_invoice.py +++ b/addons/mrp_repair/wizard/mrp_repair_make_invoice.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models -from odoo.tools import pycompat class MakeInvoice(models.TransientModel): @@ -25,7 +24,7 @@ class MakeInvoice(models.TransientModel): # but that second call will not do anything, since the repairs are already invoiced. repairs.action_repair_invoice_create() return { - 'domain': [('id', 'in', list(pycompat.values(new_invoice)))], + 'domain': [('id', 'in', list(new_invoice.values()))], 'name': 'Invoices', 'view_type': 'form', 'view_mode': 'tree,form', diff --git a/addons/pad/models/pad.py b/addons/pad/models/pad.py index 945e24f4308..d0c2055c739 100644 --- a/addons/pad/models/pad.py +++ b/addons/pad/models/pad.py @@ -10,7 +10,7 @@ import requests from odoo import api, models, _ from odoo.exceptions import UserError -from odoo.tools import html2plaintext, pycompat +from odoo.tools import html2plaintext from ..py_etherpad import EtherpadLiteClient @@ -106,7 +106,7 @@ class PadCommon(models.AbstractModel): # In case the pad is created programmatically, the content is not filled in yet since it is # normally initialized by the JS layer - for k, field in pycompat.items(self._fields): + for k, field in self._fields.items(): if hasattr(field, 'pad_content_field') and k not in vals: ctx = { 'model': self._name, @@ -121,7 +121,7 @@ class PadCommon(models.AbstractModel): def _set_pad_value(self, vals): # Update the pad if the `pad_content_field` is modified - for k, field in pycompat.items(self._fields): + for k, field in self._fields.items(): if hasattr(field, 'pad_content_field') and vals.get(field.pad_content_field) and self[k]: company = self.env.user.sudo().company_id myPad = EtherpadLiteClient(company.pad_key, company.pad_server + '/api') @@ -129,7 +129,7 @@ class PadCommon(models.AbstractModel): myPad.setText(path, (html2plaintext(vals[field.pad_content_field]).encode('utf-8'))) # Update the `pad_content_field` if the pad is modified - for k, v in list(pycompat.items(vals)): + for k, v in vals.items(): field = self._fields[k] if hasattr(field, 'pad_content_field'): vals[field.pad_content_field] = self.pad_get_content(v) @@ -139,7 +139,7 @@ class PadCommon(models.AbstractModel): self.ensure_one() if not default: default = {} - for k, field in pycompat.items(self._fields): + for k, field in self._fields.items(): if hasattr(field, 'pad_content_field'): pad = self.pad_generate_url() default[k] = pad.get('url') diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py index 623676c7a9c..2e36d891344 100644 --- a/addons/payment/models/payment_acquirer.py +++ b/addons/payment/models/payment_acquirer.py @@ -5,7 +5,7 @@ import logging import datetime from odoo import api, exceptions, fields, models, _ -from odoo.tools import consteq, float_round, image_resize_images, ustr, pycompat +from odoo.tools import consteq, float_round, image_resize_images, ustr from odoo.addons.base.module import module from odoo.exceptions import ValidationError @@ -169,7 +169,7 @@ class PaymentAcquirer(models.Model): """ If the field has 'required_if_provider=""' attribute, then it required if record.provider is . """ for acquirer in self: - if any(getattr(f, 'required_if_provider', None) == acquirer.provider and not acquirer[k] for k, f in pycompat.items(self._fields)): + if any(getattr(f, 'required_if_provider', None) == acquirer.provider and not acquirer[k] for k, f in self._fields.items()): return False return True @@ -583,7 +583,7 @@ class PaymentTransaction(models.Model): token = '%s%s%s' % (self.callback_model_id.model, self.callback_res_id, self.sudo().callback_method) - return hmac.new(str(secret), token, hashlib.sha256).hexdigest() + return hmac.new(secret.encode('utf-8'), token.encode('utf-8'), hashlib.sha256).hexdigest() # -------------------------------------------------- # FORM RELATED METHODS diff --git a/addons/payment_adyen/models/payment.py b/addons/payment_adyen/models/payment.py index 87e51e619cb..dbc24ced0c0 100644 --- a/addons/payment_adyen/models/payment.py +++ b/addons/payment_adyen/models/payment.py @@ -15,8 +15,6 @@ 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 -from odoo.tools import pycompat - _logger = logging.getLogger(__name__) @@ -52,9 +50,9 @@ class AcquirerAdyen(models.Model): def signParams(parms): signing_string = ':'.join( escapeVal(v) - for v in chain(pycompat.keys(parms), pycompat.values(parms)) + for v in chain(parms.keys(), parms.values()) ) - hm = hmac.new(hmac_key, signing_string, hashlib.sha256) + hm = hmac.new(hmac_key, signing_string.encode('utf-8'), hashlib.sha256) return base64.b64encode(hm.digest()) assert inout in ('in', 'out') @@ -81,7 +79,7 @@ class AcquirerAdyen(models.Model): hmac_key = binascii.a2b_hex(self.adyen_skin_hmac_key.encode('ascii')) raw_values = {k: values.get(k.encode('ascii'), '') for k in keys if k in values} - raw_values_ordered = OrderedDict(sorted(pycompat.items(raw_values), key=lambda t: t[0])) + raw_values_ordered = OrderedDict(sorted(raw_values.items(), key=lambda t: t[0])) return signParams(raw_values_ordered) diff --git a/addons/payment_authorize/models/authorize_request.py b/addons/payment_authorize/models/authorize_request.py index fcfab1e3ac2..7388a1a4329 100644 --- a/addons/payment_authorize/models/authorize_request.py +++ b/addons/payment_authorize/models/authorize_request.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- +import io import requests from lxml import etree, objectify -from StringIO import StringIO from xml.etree import ElementTree as ET from uuid import uuid4 @@ -21,7 +21,7 @@ def strip_ns(xml, ns): :rtype: etree._Element :return: the parsed xml string with the namespace prefix removed """ - it = ET.iterparse(StringIO(xml)) + it = ET.iterparse(io.BytesIO(xml)) ns_prefix = '{%s}' % XMLNS for _, el in it: if el.tag.startswith(ns_prefix): @@ -58,7 +58,7 @@ class AuthorizeAPI(): :param etree._Element data: etree data to process """ - data = etree.tostring(data, xml_declaration=True, encoding='utf-8') + data = etree.tostring(data, encoding='utf-8') r = requests.post(self.url, data=data, headers={'Content-Type': 'text/xml'}) r.raise_for_status() response = strip_ns(r.content, XMLNS) diff --git a/addons/payment_authorize/models/payment.py b/addons/payment_authorize/models/payment.py index bfc5bfd569a..caa15732e15 100644 --- a/addons/payment_authorize/models/payment.py +++ b/addons/payment_authorize/models/payment.py @@ -54,7 +54,7 @@ class PaymentAcquirerAuthorize(models.Model): values['x_fp_timestamp'], values['x_amount'], values['x_currency_code']]) - return hmac.new(str(values['x_trans_key']), data, hashlib.md5).hexdigest() + return hmac.new(values['x_trans_key'].encode('utf-8'), data.encode('utf-8'), hashlib.md5).hexdigest() @api.multi def authorize_form_generate_values(self, values): diff --git a/addons/payment_authorize/tests/test_authorize.py b/addons/payment_authorize/tests/test_authorize.py index b02d3d81209..59fb401b091 100644 --- a/addons/payment_authorize/tests/test_authorize.py +++ b/addons/payment_authorize/tests/test_authorize.py @@ -11,7 +11,7 @@ import odoo from odoo.addons.payment.models.payment_acquirer import ValidationError from odoo.addons.payment.tests.common import PaymentAcquirerCommon from odoo.addons.payment_authorize.controllers.main import AuthorizeController -from odoo.tools import mute_logger, pycompat +from odoo.tools import mute_logger @odoo.tests.common.at_install(True) @@ -39,7 +39,7 @@ class AuthorizeForm(AuthorizeCommon): values['x_fp_timestamp'], values['x_amount'], ]) + '^' - return hmac.new(str(values['x_trans_key']), data, hashlib.md5).hexdigest() + return hmac.new(values['x_trans_key'].encode('utf-8'), data.encode('utf-8'), hashlib.md5).hexdigest() def test_10_Authorize_form_render(self): self.assertEqual(self.authorize.environment, 'test', 'test without test environment') @@ -91,11 +91,11 @@ class AuthorizeForm(AuthorizeCommon): tree = objectify.fromstring(res) self.assertEqual(tree.get('action'), 'https://test.authorize.net/gateway/transact.dll', 'Authorize: wrong form POST url') for el in tree.iterfind('input'): - values = list(pycompat.values(el.attrib)) + values = list(el.attrib.values()) if values[1] in ['submit', 'x_fp_hash', 'return_url', 'x_state', 'x_ship_to_state']: continue self.assertEqual( - unicode(values[2], "utf-8"), + values[2], form_values[values[1]], 'Authorize: wrong value for input %s: received %s instead of %s' % (values[1], values[2], form_values[values[1]]) ) diff --git a/addons/payment_buckaroo/controllers/main.py b/addons/payment_buckaroo/controllers/main.py index 5af6a454b47..40e4ae539e4 100644 --- a/addons/payment_buckaroo/controllers/main.py +++ b/addons/payment_buckaroo/controllers/main.py @@ -6,7 +6,6 @@ import werkzeug from odoo import http from odoo.http import request -from odoo.tools import pycompat _logger = logging.getLogger(__name__) @@ -27,6 +26,6 @@ class BuckarooController(http.Controller): """ Buckaroo.""" _logger.info('Buckaroo: entering form_feedback with post data %s', pprint.pformat(post)) # debug request.env['payment.transaction'].sudo().form_feedback(post, 'buckaroo') - post = {key.upper(): value for key, value in pycompat.items(post)} + post = {key.upper(): value for key, value in post.items()} return_url = post.get('ADD_RETURNDATA') or '/' return werkzeug.utils.redirect(return_url) diff --git a/addons/payment_buckaroo/models/payment.py b/addons/payment_buckaroo/models/payment.py index a07ca83318d..ec0cadfab01 100644 --- a/addons/payment_buckaroo/models/payment.py +++ b/addons/payment_buckaroo/models/payment.py @@ -8,7 +8,6 @@ from odoo import api, fields, models, _ from odoo.addons.payment.models.payment_acquirer import ValidationError from odoo.addons.payment_buckaroo.controllers.main import BuckarooController -from odoo.tools import pycompat from odoo.tools.float_utils import float_compare _logger = logging.getLogger(__name__) @@ -21,7 +20,7 @@ def normalize_keys_upper(data): convert everything to upper case to be able to easily detected the presence of a parameter by checking the uppercase key only """ - return {key.upper(): val for key, val in pycompat.items(data)} + return {key.upper(): val for key, val in data.items()} class AcquirerBuckaroo(models.Model): @@ -73,7 +72,7 @@ class AcquirerBuckaroo(models.Model): del values[key] break - items = sorted(pycompat.items(values), key=lambda pair: pair[0].lower()) + items = sorted(values.items(), key=lambda pair: pair[0].lower()) 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) diff --git a/addons/payment_ogone/models/payment.py b/addons/payment_ogone/models/payment.py index 9ac90f9478f..7a2adf37d45 100644 --- a/addons/payment_ogone/models/payment.py +++ b/addons/payment_ogone/models/payment.py @@ -1,4 +1,5 @@ # coding: utf-8 +import base64 import datetime import logging import time @@ -14,7 +15,7 @@ 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, pycompat +from odoo.tools import float_round, DEFAULT_SERVER_DATE_FORMAT from odoo.tools.float_utils import float_compare, float_repr from odoo.tools.safe_eval import safe_eval @@ -141,7 +142,7 @@ class PaymentAcquirerOgone(models.Model): ] return key.upper() in keys - items = sorted((k.upper(), v) for k, v in pycompat.items(values)) + items = sorted((k.upper(), v) for k, v in values.items()) sign = ''.join('%s=%s%s' % (k, v, key) for k, v in items if v and filter_key(k)) sign = sign.encode("utf-8") shasign = sha1(sign).hexdigest() @@ -469,7 +470,7 @@ class PaymentTxOgone(models.Model): self.write({ 'state': new_state, 'acquirer_reference': tree.get('PAYID'), - 'html_3ds': str(tree.HTML_ANSWER).decode('base64') + 'html_3ds': base64.b64decode(tree.HTML_ANSWER.decode('ascii')), }) elif status in self._ogone_wait_tx_status and tries > 0: time.sleep(0.5) diff --git a/addons/payment_payumoney/models/payment.py b/addons/payment_payumoney/models/payment.py index 243ef5cef53..cbda9e64527 100644 --- a/addons/payment_payumoney/models/payment.py +++ b/addons/payment_payumoney/models/payment.py @@ -49,7 +49,7 @@ class PaymentAcquirerPayumoney(models.Model): sign = ''.join('%s|' % (values.get(k) or '') for k in keys) sign = self.payumoney_merchant_salt + sign + self.payumoney_merchant_key - shasign = hashlib.sha512(sign).hexdigest() + shasign = hashlib.sha512(sign.encode('utf-8')).hexdigest() return shasign @api.multi diff --git a/addons/payment_sips/models/payment.py b/addons/payment_sips/models/payment.py index f01c59d1880..a6c9b390eae 100644 --- a/addons/payment_sips/models/payment.py +++ b/addons/payment_sips/models/payment.py @@ -66,7 +66,7 @@ class AcquirerSips(models.Model): if self.environment == 'prod': key = getattr(self, 'sips_secret') - shasign = sha256(data + key) + shasign = sha256((data + key).encode('utf-8')) return shasign.hexdigest() @api.multi diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 5e059a41cfc..9e730f9a2e5 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -8,7 +8,7 @@ import psycopg2 import pytz from odoo import api, fields, models, tools, _ -from odoo.tools import float_is_zero, pycompat +from odoo.tools import float_is_zero from odoo.exceptions import UserError from odoo.http import request from odoo.addons import decimal_precision as dp @@ -333,7 +333,7 @@ class PosOrder(models.Model): # round tax lines per order if rounding_method == 'round_globally': - for group_key, group_value in pycompat.items(grouped_data): + for group_key, group_value in grouped_data.items(): if group_key[0] == 'tax': for line in group_value: line['credit'] = cur.round(line['credit']) @@ -351,7 +351,7 @@ class PosOrder(models.Model): order.write({'state': 'done', 'account_move': move.id}) all_lines = [] - for group_key, group_data in pycompat.items(grouped_data): + for group_key, group_data in grouped_data.items(): for value in group_data: all_lines.append((0, 0, value),) if move: # In case no order was changed @@ -1032,7 +1032,7 @@ class ReportSaleDetails(models.AbstractModel): 'total_paid': user_currency.round(total), 'payments': payments, 'company_name': self.env.user.company_id.name, - 'taxes': list(pycompat.values(taxes)), + 'taxes': list(taxes.values()), 'products': sorted([{ 'product_id': product.id, 'product_name': product.name, @@ -1041,7 +1041,7 @@ class ReportSaleDetails(models.AbstractModel): 'price_unit': price_unit, 'discount': discount, 'uom': product.uom_id.name - } for (product, price_unit, discount), qty in pycompat.items(products_sold)], key=lambda l: l['product_name']) + } for (product, price_unit, discount), qty in products_sold.items()], key=lambda l: l['product_name']) } @api.multi diff --git a/addons/product/models/product.py b/addons/product/models/product.py index 638e898aed8..c550e063709 100644 --- a/addons/product/models/product.py +++ b/addons/product/models/product.py @@ -158,7 +158,7 @@ class ProductProduct(models.Model): quantity = self._context.get('quantity', 1.0) # Support context pricelists specified as display_name or ID for compatibility - if isinstance(pricelist_id_or_name, basestring): + if isinstance(pricelist_id_or_name, pycompat.string_types): pricelist_name_search = self.env['product.pricelist'].name_search(pricelist_id_or_name, operator='=', limit=1) if pricelist_name_search: pricelist = self.env['product.pricelist'].browse([pricelist_name_search[0][0]]) diff --git a/addons/product/models/product_pricelist.py b/addons/product/models/product_pricelist.py index a0457f021ba..392447809f4 100644 --- a/addons/product/models/product_pricelist.py +++ b/addons/product/models/product_pricelist.py @@ -90,7 +90,7 @@ class Pricelist(models.Model): results = {} for pricelist in pricelists: subres = pricelist._compute_price_rule(products_qty_partner, date=date, uom_id=uom_id) - for product_id, price in pycompat.items(subres): + for product_id, price in subres.items(): results.setdefault(product_id, {}) results[product_id][pricelist.id] = price return results @@ -254,11 +254,11 @@ class Pricelist(models.Model): self.ensure_one() return { product_id: res_tuple[0] - for product_id, res_tuple in pycompat.items(self._compute_price_rule( + for product_id, res_tuple in self._compute_price_rule( list(pycompat.izip(products, quantities, partners)), date=date, uom_id=uom_id - )) + ).items() } def get_product_price(self, product, quantity, partner, date=False, uom_id=False): @@ -280,7 +280,7 @@ class Pricelist(models.Model): @api.multi def price_get(self, prod_id, qty, partner=None): """ Multi pricelist, mono product - returns price per pricelist """ - return {key: price[0] for key, price in pycompat.items(self.price_rule_get(prod_id, qty, partner=partner))} + return {key: price[0] for key, price in self.price_rule_get(prod_id, qty, partner=partner).items()} @api.multi def price_rule_get_multi(self, products_by_qty_by_partner): diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index ccfbcf28b96..c9a2411f173 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -171,7 +171,7 @@ class ProductTemplate(models.Model): quantity = self._context.get('quantity', 1.0) # Support context pricelists specified as display_name or ID for compatibility - if isinstance(pricelist_id_or_name, basestring): + if isinstance(pricelist_id_or_name, pycompat.string_types): pricelist_data = self.env['product.pricelist'].name_search(pricelist_id_or_name, operator='=', limit=1) if pricelist_data: pricelist = self.env['product.pricelist'].browse(pricelist_data[0][0]) diff --git a/addons/product_expiry/models/production_lot.py b/addons/product_expiry/models/production_lot.py index 89d2a7569ff..6be9a2865dd 100644 --- a/addons/product_expiry/models/production_lot.py +++ b/addons/product_expiry/models/production_lot.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import datetime from odoo import api, fields, models -from odoo.tools import pycompat class StockProductionLot(models.Model): @@ -54,5 +53,5 @@ class StockProductionLot(models.Model): @api.onchange('product_id') def _onchange_product(self): dates_dict = self._get_dates() - for field, value in pycompat.items(dates_dict): + for field, value in dates_dict.items(): setattr(self, field, value) diff --git a/addons/product_margin/models/product_product.py b/addons/product_margin/models/product_product.py index f203a9e146b..3dd735b26a4 100644 --- a/addons/product_margin/models/product_product.py +++ b/addons/product_margin/models/product_product.py @@ -4,7 +4,6 @@ import time from odoo import api, fields, models -from odoo.tools import pycompat class ProductProduct(models.Model): @@ -139,6 +138,6 @@ class ProductProduct(models.Model): res[val.id]['expected_margin'] = res[val.id]['sale_expected'] - res[val.id]['normal_cost'] res[val.id]['total_margin_rate'] = res[val.id]['turnover'] and res[val.id]['total_margin'] * 100 / res[val.id]['turnover'] or 0.0 res[val.id]['expected_margin_rate'] = res[val.id]['sale_expected'] and res[val.id]['expected_margin'] * 100 / res[val.id]['sale_expected'] or 0.0 - for k, v in pycompat.items(res[val.id]): + for k, v in res[val.id].items(): setattr(val, k, v) return res diff --git a/addons/project/models/project.py b/addons/project/models/project.py index cfe3f91aff5..8e20f1bc029 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -500,7 +500,7 @@ class Task(models.Model): _check_rec(eview) - res['arch'] = etree.tostring(eview) + res['arch'] = etree.tostring(eview, encoding='unicode') # replace reference of 'Hours' to 'Day(s)' for f in res['fields']: 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/rating/models/rating.py b/addons/rating/models/rating.py index 78369403d2d..f4bd17b48f3 100644 --- a/addons/rating/models/rating.py +++ b/addons/rating/models/rating.py @@ -1,12 +1,11 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - +import base64 import uuid from odoo import api, fields, models, tools, _ from odoo.modules.module import get_resource_path -from odoo.tools import pycompat RATING_LIMIT_SATISFIED = 7 RATING_LIMIT_OK = 3 @@ -70,7 +69,7 @@ class Rating(models.Model): for rating in self: try: image_path = get_resource_path('rating', 'static/src/img', 'rating_%s.png' % (int(rating.rating),)) - rating.rating_image = open(image_path, 'rb').read().encode('base64') + rating.rating_image = base64.b64decode(open(image_path, 'rb').read()) except (IOError, OSError): rating.rating_image = False @@ -275,7 +274,7 @@ class RatingMixin(models.AbstractModel): values.update((d['rating'], d['rating_count']) for d in data) # add other stats if add_stats: - rating_number = sum(pycompat.values(values)) + rating_number = sum(values.values()) result = { 'repartition': values, 'avg': sum(float(key * values[key]) for key in values) / rating_number if rating_number > 0 else 0, diff --git a/addons/rating_project/models/project.py b/addons/rating_project/models/project.py index 915c97848ec..8a4f86d3b96 100644 --- a/addons/rating_project/models/project.py +++ b/addons/rating_project/models/project.py @@ -3,7 +3,6 @@ from datetime import timedelta -from odoo.tools import pycompat from odoo import api, fields, models, _ from odoo.tools.safe_eval import safe_eval @@ -80,13 +79,13 @@ class Project(models.Model): domain = [('create_date', '>=', fields.Datetime.to_string(fields.datetime.now() - timedelta(days=30)))] for project in self: activity = project.tasks.rating_get_grades(domain) - project.percentage_satisfaction_project = activity['great'] * 100 / sum(pycompat.values(activity)) if sum(pycompat.values(activity)) else -1 + project.percentage_satisfaction_project = activity['great'] * 100 / sum(activity.values()) if sum(activity.values()) else -1 @api.one @api.depends('tasks.rating_ids.rating') def _compute_percentage_satisfaction_task(self): activity = self.tasks.rating_get_grades() - self.percentage_satisfaction_task = activity['great'] * 100 / sum(pycompat.values(activity)) if sum(pycompat.values(activity)) else -1 + self.percentage_satisfaction_task = activity['great'] * 100 / sum(activity.values()) if sum(activity.values()) else -1 percentage_satisfaction_task = fields.Integer( compute='_compute_percentage_satisfaction_task', string="Happy % on Task", store=True, default=-1) diff --git a/addons/rating_project_issue/models/project_issue.py b/addons/rating_project_issue/models/project_issue.py index 8116c0f50f1..cd3d541d1ef 100644 --- a/addons/rating_project_issue/models/project_issue.py +++ b/addons/rating_project_issue/models/project_issue.py @@ -4,7 +4,6 @@ from datetime import timedelta from odoo import api, fields, models -from odoo.tools import pycompat from odoo.tools.safe_eval import safe_eval @@ -63,11 +62,11 @@ class Project(models.Model): if project.use_tasks: activity_task = project.tasks.rating_get_grades(domain) activity_great = activity_task['great'] - activity_sum = sum(pycompat.values(activity_task)) + activity_sum = sum(activity_task.values()) if project.use_issues: activity_issue = self.env['project.issue'].search([('project_id', '=', project.id)]).rating_get_grades(domain) activity_great += activity_issue['great'] - activity_sum += sum(pycompat.values(activity_issue)) + activity_sum += sum(activity_issue.values()) project.percentage_satisfaction_project = activity_great * 100 / activity_sum if activity_sum else -1 @api.one @@ -75,7 +74,7 @@ class Project(models.Model): def _compute_percentage_satisfaction_issue(self): project_issue = self.env['project.issue'].search([('project_id', '=', self.id)]) activity = project_issue.rating_get_grades() - self.percentage_satisfaction_issue = activity['great'] * 100 / sum(pycompat.values(activity)) if sum(pycompat.values(activity)) else -1 + self.percentage_satisfaction_issue = activity['great'] * 100 / sum(activity.values()) if sum(activity.values()) else -1 percentage_satisfaction_issue = fields.Integer(compute='_compute_percentage_satisfaction_issue', string="Happy % on Issue", store=True, default=-1) 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(), diff --git a/addons/resource/models/resource_mixin.py b/addons/resource/models/resource_mixin.py index 8b922dd5ff8..9c21f7f935f 100644 --- a/addons/resource/models/resource_mixin.py +++ b/addons/resource/models/resource_mixin.py @@ -5,7 +5,6 @@ from datetime import timedelta from odoo import api, fields, models - class ResourceMixin(models.AbstractModel): _name = "resource.mixin" _description = 'Resource Mixin' @@ -70,7 +69,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 += 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/sale/models/account_invoice.py b/addons/sale/models/account_invoice.py index 5163f4baa6c..c4ad56d1277 100644 --- a/addons/sale/models/account_invoice.py +++ b/addons/sale/models/account_invoice.py @@ -3,7 +3,6 @@ from itertools import groupby from odoo import api, fields, models, _ -from odoo.tools import pycompat class AccountInvoice(models.Model): @@ -58,7 +57,7 @@ class AccountInvoice(models.Model): result = super(AccountInvoice, self)._refund_cleanup_lines(lines) if self.env.context.get('mode') == 'modify': for i, line in enumerate(lines): - for name, field in pycompat.items(line._fields): + for name, field in line._fields.items(): if name == 'sale_line_ids': result[i][2][name] = [(6, 0, line[name].ids)] line[name] = False diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 7e554c6af08..8fe5623e339 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -8,7 +8,7 @@ from datetime import datetime, timedelta from odoo import api, fields, models, _ from odoo.exceptions import UserError -from odoo.tools import float_is_zero, float_compare, DEFAULT_SERVER_DATETIME_FORMAT, pycompat +from odoo.tools import float_is_zero, float_compare, DEFAULT_SERVER_DATETIME_FORMAT from odoo.tools.misc import formatLang from odoo.addons import decimal_precision as dp @@ -400,7 +400,7 @@ class SaleOrder(models.Model): if not invoices: raise UserError(_('There is no invoicable line.')) - for invoice in pycompat.values(invoices): + for invoice in invoices.values(): if not invoice.invoice_line_ids: raise UserError(_('There is no invoicable line.')) # If invoice is negative, do a refund invoice instead @@ -417,7 +417,7 @@ class SaleOrder(models.Model): invoice.message_post_with_view('mail.message_origin_link', values={'self': invoice, 'origin': references[invoice]}, subtype_id=self.env.ref('mail.mt_note').id) - return [inv.id for inv in pycompat.values(invoices)] + return [inv.id for inv in invoices.values()] @api.multi def action_draft(self): @@ -557,7 +557,7 @@ class SaleOrder(models.Model): if tax.include_base_amount: base_tax += tax.compute_all(price_reduce + base_tax, quantity=1, product=line.product_id, partner=self.partner_shipping_id)['taxes'][0]['amount'] - res = sorted(pycompat.items(res), key=lambda l: l[0].sequence) + res = sorted(res.items(), key=lambda l: l[0].sequence) res = [(l[0].name, l[1]) for l in res] return res diff --git a/addons/sale/models/sale_analytic.py b/addons/sale/models/sale_analytic.py index 733bd74bc2a..96f2f896f44 100644 --- a/addons/sale/models/sale_analytic.py +++ b/addons/sale/models/sale_analytic.py @@ -3,7 +3,6 @@ from odoo import api, fields, models, _ from odoo.exceptions import UserError -from odoo.tools import pycompat class SaleOrderLine(models.Model): @@ -41,7 +40,7 @@ class SaleOrderLine(models.Model): qty = d['unit_amount'] lines[line] += qty - for line, qty in pycompat.items(lines): + for line, qty in lines.items(): line.qty_delivered = qty return True diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index 84dbee6a3fd..2b6c082b150 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo.exceptions import UserError, AccessError -from odoo.tools import pycompat from .test_sale_common import TestSale @@ -18,10 +17,10 @@ class TestSaleOrder(TestSale): 'partner_id': self.partner.id, 'partner_invoice_id': self.partner.id, 'partner_shipping_id': self.partner.id, - 'order_line': [(0, 0, {'name': p.name, 'product_id': p.id, 'product_uom_qty': 2, 'product_uom': p.uom_id.id, 'price_unit': p.list_price}) for (_, p) in pycompat.items(self.products)], + 'order_line': [(0, 0, {'name': p.name, 'product_id': p.id, 'product_uom_qty': 2, 'product_uom': p.uom_id.id, 'price_unit': p.list_price}) for p in self.products.values()], 'pricelist_id': self.env.ref('product.list0').id, }) - self.assertEqual(so.amount_total, sum([2 * p.list_price for (k, p) in pycompat.items(self.products)]), 'Sale: total amount is wrong') + self.assertEqual(so.amount_total, sum([2 * p.list_price for p in self.products.values()]), 'Sale: total amount is wrong') # send quotation so.force_quotation_send() @@ -36,7 +35,7 @@ class TestSaleOrder(TestSale): inv_id = so.action_invoice_create() inv = inv_obj.browse(inv_id) self.assertEqual(len(inv.invoice_line_ids), 2, 'Sale: invoice is missing lines') - self.assertEqual(inv.amount_total, sum([2 * p.list_price if p.invoice_policy == 'order' else 0 for (k, p) in pycompat.items(self.products)]), 'Sale: invoice total amount is wrong') + self.assertEqual(inv.amount_total, sum([2 * p.list_price if p.invoice_policy == 'order' else 0 for p in self.products.values()]), 'Sale: invoice total amount is wrong') self.assertTrue(so.invoice_status == 'no', 'Sale: SO status after invoicing should be "nothing to invoice"') self.assertTrue(len(so.invoice_ids) == 1, 'Sale: invoice is missing') @@ -47,7 +46,7 @@ class TestSaleOrder(TestSale): inv_id = so.action_invoice_create() inv = inv_obj.browse(inv_id) self.assertEqual(len(inv.invoice_line_ids), 2, 'Sale: second invoice is missing lines') - self.assertEqual(inv.amount_total, sum([2 * p.list_price if p.invoice_policy == 'delivery' else 0 for (k, p) in pycompat.items(self.products)]), 'Sale: second invoice total amount is wrong') + self.assertEqual(inv.amount_total, sum([2 * p.list_price if p.invoice_policy == 'delivery' else 0 for p in self.products.values()]), 'Sale: second invoice total amount is wrong') self.assertTrue(so.invoice_status == 'invoiced', 'Sale: SO status after invoicing everything should be "invoiced"') self.assertTrue(len(so.invoice_ids) == 2, 'Sale: invoice is missing') # go over the sold quantity @@ -72,7 +71,7 @@ class TestSaleOrder(TestSale): 'partner_id': self.partner.id, 'partner_invoice_id': self.partner.id, 'partner_shipping_id': self.partner.id, - 'order_line': [(0, 0, {'name': p.name, 'product_id': p.id, 'product_uom_qty': 2, 'product_uom': p.uom_id.id, 'price_unit': p.list_price}) for (_, p) in pycompat.items(self.products)], + 'order_line': [(0, 0, {'name': p.name, 'product_id': p.id, 'product_uom_qty': 2, 'product_uom': p.uom_id.id, 'price_unit': p.list_price}) for p in self.products.values()], 'pricelist_id': self.env.ref('product.list0').id, }) diff --git a/addons/sale_stock/tests/test_sale_stock.py b/addons/sale_stock/tests/test_sale_stock.py index 5a4755d2c16..3dd6d5103ca 100644 --- a/addons/sale_stock/tests/test_sale_stock.py +++ b/addons/sale_stock/tests/test_sale_stock.py @@ -3,7 +3,6 @@ from odoo.addons.sale.tests.test_sale_common import TestSale from odoo.exceptions import UserError -from odoo.tools import pycompat class TestSaleStock(TestSale): @@ -17,7 +16,7 @@ class TestSaleStock(TestSale): 'partner_id': self.partner.id, 'partner_invoice_id': self.partner.id, 'partner_shipping_id': self.partner.id, - 'order_line': [(0, 0, {'name': p.name, 'product_id': p.id, 'product_uom_qty': 2, 'product_uom': p.uom_id.id, 'price_unit': p.list_price}) for (_, p) in pycompat.items(self.products)], + 'order_line': [(0, 0, {'name': p.name, 'product_id': p.id, 'product_uom_qty': 2, 'product_uom': p.uom_id.id, 'price_unit': p.list_price}) for p in self.products.values()], 'pricelist_id': self.env.ref('product.list0').id, 'picking_policy': 'direct', }) @@ -74,7 +73,7 @@ class TestSaleStock(TestSale): 'partner_id': self.partner.id, 'partner_invoice_id': self.partner.id, 'partner_shipping_id': self.partner.id, - 'order_line': [(0, 0, {'name': p.name, 'product_id': p.id, 'product_uom_qty': 2, 'product_uom': p.uom_id.id, 'price_unit': p.list_price}) for (_, p) in pycompat.items(self.products)], + 'order_line': [(0, 0, {'name': p.name, 'product_id': p.id, 'product_uom_qty': 2, 'product_uom': p.uom_id.id, 'price_unit': p.list_price}) for p in self.products.values()], 'pricelist_id': self.env.ref('product.list0').id, 'picking_policy': 'direct', }) diff --git a/addons/stock/models/procurement.py b/addons/stock/models/procurement.py index 8ac7fc638d7..8d5e419fb90 100644 --- a/addons/stock/models/procurement.py +++ b/addons/stock/models/procurement.py @@ -9,8 +9,7 @@ from psycopg2 import OperationalError from odoo import api, fields, models, registry, _ from odoo.osv import expression -from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, float_compare, \ - float_round, pycompat +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, float_compare, float_round import logging @@ -311,7 +310,7 @@ class ProcurementOrder(models.Model): location_data[key]['orderpoints'] += orderpoint location_data[key]['groups'] = self._procurement_from_orderpoint_get_groups([orderpoint.id]) - for location_id, location_data in pycompat.items(location_data): + for location_id, location_data in location_data.items(): location_orderpoints = location_data['orderpoints'] product_context = dict(self._context, location=location_orderpoints[0].location_id.id) substract_quantity = location_orderpoints.subtract_procurements_from_orderpoints() diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index 43cbe1e2e4d..693f5c0119e 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -161,7 +161,7 @@ class Product(models.Model): if self.env.context.get('location', False): if isinstance(self.env.context['location'], pycompat.integer_types): location_ids = [self.env.context['location']] - elif isinstance(self.env.context['location'], basestring): + elif isinstance(self.env.context['location'], pycompat.string_types): domain = [('complete_name', 'ilike', self.env.context['location'])] if self.env.context.get('force_company', False): domain += [('company_id', '=', self.env.context['force_company'])] @@ -172,7 +172,7 @@ class Product(models.Model): if self.env.context.get('warehouse', False): if isinstance(self.env.context['warehouse'], pycompat.integer_types): wids = [self.env.context['warehouse']] - elif isinstance(self.env.context['warehouse'], basestring): + elif isinstance(self.env.context['warehouse'], pycompat.string_types): domain = [('name', 'ilike', self.env.context['warehouse'])] if self.env.context.get('force_company', False): domain += [('company_id', '=', self.env.context['force_company'])] diff --git a/addons/stock/models/stock_inventory.py b/addons/stock/models/stock_inventory.py index 866f3e2104c..bb14b839d32 100644 --- a/addons/stock/models/stock_inventory.py +++ b/addons/stock/models/stock_inventory.py @@ -4,7 +4,7 @@ from odoo import api, fields, models, _ from odoo.addons import decimal_precision as dp from odoo.exceptions import UserError -from odoo.tools import float_utils, pycompat +from odoo.tools import float_utils class Inventory(models.Model): @@ -251,7 +251,7 @@ class Inventory(models.Model): for product_data in self.env.cr.dictfetchall(): # replace the None the dictionary by False, because falsy values are tested later on - for void_field in [item[0] for item in pycompat.items(product_data) if item[1] is None]: + for void_field in [item[0] for item in product_data.items() if item[1] is None]: product_data[void_field] = False product_data['theoretical_qty'] = product_data['product_qty'] if product_data['product_id']: diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 5eab237011e..3c27e5ba6ec 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -10,7 +10,7 @@ from odoo import api, fields, models, _ from odoo.addons import decimal_precision as dp from odoo.addons.procurement.models import procurement from odoo.exceptions import UserError -from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, pycompat +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT from odoo.tools.float_utils import float_compare, float_round, float_is_zero @@ -595,7 +595,7 @@ class StockMove(models.Model): (move_waiting | move_create_proc).write({'state': 'waiting'}) # assign picking in batch for all confirmed move that share the same details - for key, moves in pycompat.items(to_assign): + for moves in to_assign.values(): moves.assign_picking() self._push_apply() return self @@ -767,7 +767,7 @@ class StockMove(models.Model): grouped_move_lines_out[k] = sum(self.env['stock.move.line'].concat(*list(g)).mapped('product_qty')) available_move_lines = {key: grouped_move_lines_in[key] - grouped_move_lines_out.get(key, 0) for key in grouped_move_lines_in.keys()} # pop key if the quantity available amount to 0 - available_move_lines = dict((k, v) for k, v in available_move_lines.iteritems() if v) + available_move_lines = dict((k, v) for k, v in available_move_lines.items() if v) if not available_move_lines: continue diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index 536ac2a5e6d..de736ef0bcf 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -7,7 +7,7 @@ import time from itertools import groupby from odoo import api, fields, models, _ -from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, pycompat +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT from odoo.tools.float_utils import float_compare, float_round from odoo.addons.procurement.models import procurement from odoo.exceptions import UserError @@ -665,7 +665,7 @@ class Picking(models.Model): self.ensure_one() moves = self.env['stock.move'] for move_line in self.move_line_ids: - for product, remaining_qty in pycompat.items(move_line._get_remaining_prod_quantities()): + for product, remaining_qty in move_line._get_remaining_prod_quantities().items(): if float_compare(remaining_qty, 0, precision_rounding=product.uom_id.rounding) > 0: vals = self._prepare_values_extra_move(move_line, product, remaining_qty) moves |= moves.create(vals) diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py index 5609f5be6a6..5e99ebc841e 100644 --- a/addons/stock/models/stock_quant.py +++ b/addons/stock/models/stock_quant.py @@ -5,7 +5,6 @@ from psycopg2 import OperationalError from odoo import api, fields, models, _ from odoo.exceptions import UserError, ValidationError -from odoo.tools import pycompat from odoo.osv import expression @@ -175,7 +174,7 @@ class StockQuant(models.Model): if quant.quantity == 0 and quant.reserved_quantity == 0: quant.unlink() break - except OperationalError, e: + except OperationalError as e: if e.pgcode == '55P03': # could not obtain the lock continue else: @@ -274,7 +273,7 @@ class QuantPackage(models.Model): @api.multi def name_get(self): - return list(pycompat.items(self._compute_complete_name())) + return list(self._compute_complete_name().items()) def _compute_complete_name(self): """ Forms complete name of location from parent location to child location. """ diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py index 1f7e44f23ea..d8bd64253c6 100644 --- a/addons/stock/models/stock_warehouse.py +++ b/addons/stock/models/stock_warehouse.py @@ -8,7 +8,7 @@ from dateutil import relativedelta from odoo import api, fields, models, _ from odoo.addons import decimal_precision as dp from odoo.exceptions import UserError, ValidationError -from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, pycompat +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT import logging @@ -99,7 +99,7 @@ class Warehouse(models.Model): 'wh_output_stock_loc_id': {'name': _('Output'), 'active': delivery_steps != 'ship_only', 'usage': 'internal'}, 'wh_pack_stock_loc_id': {'name': _('Packing Zone'), 'active': delivery_steps == 'pick_pack_ship', 'usage': 'internal'}, } - for field_name, values in pycompat.items(sub_locations): + for field_name, values in sub_locations.items(): values['location_id'] = vals['view_location_id'] if vals.get('company_id'): values['company_id'] = vals.get('company_id') @@ -240,10 +240,10 @@ class Warehouse(models.Model): }, } data = self._get_picking_type_values(self.reception_steps, self.delivery_steps, self.wh_pack_stock_loc_id) - for field_name, values in pycompat.items(data): + for field_name in data: data[field_name].update(create_data[field_name]) - for picking_type, values in pycompat.items(data): + for picking_type, values in data.items(): sequence = IrSequenceSudo.create(sequence_data[picking_type]) values.update(warehouse_id=self.id, color=color, sequence_id=sequence.id) warehouse_data[picking_type] = PickingType.create(values).id @@ -598,8 +598,8 @@ class Warehouse(models.Model): @api.one def _update_picking_type(self): picking_type_values = self._get_picking_type_values(self.reception_steps, self.delivery_steps, self.wh_pack_stock_loc_id) - for field_name, values in pycompat.items(picking_type_values): - getattr(self, field_name).write(values) + for field_name, values in picking_type_values.items(): + self[field_name].write(values) @api.multi def _update_name_and_code(self, new_name=False, new_code=False): diff --git a/addons/stock/wizard/make_procurement.py b/addons/stock/wizard/make_procurement.py index 369995e7a15..2fa3db75b5d 100644 --- a/addons/stock/wizard/make_procurement.py +++ b/addons/stock/wizard/make_procurement.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models -from odoo.tools import pycompat class MakeProcurement(models.TransientModel): @@ -51,8 +50,8 @@ class MakeProcurement(models.TransientModel): @api.onchange('product_id') def onchange_product_id(self): if self.product_id: - for key, value in pycompat.items(self.onchange_product_id_dict(self.product_id.id)): - setattr(self, key, value) + for key, value in self.onchange_product_id_dict(self.product_id.id).items(): + self[key] = value @api.model def create(self, values): 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/addons/stock_landed_costs/models/stock_landed_cost.py b/addons/stock_landed_costs/models/stock_landed_cost.py index aeb6e42366f..5fd5ce8f63b 100644 --- a/addons/stock_landed_costs/models/stock_landed_cost.py +++ b/addons/stock_landed_costs/models/stock_landed_cost.py @@ -7,7 +7,6 @@ from odoo import api, fields, models, tools, _ from odoo.addons import decimal_precision as dp from odoo.addons.stock_landed_costs.models import product from odoo.exceptions import UserError -from odoo.tools import pycompat class StockMove(models.Model): @@ -123,7 +122,7 @@ class LandedCost(models.Model): for val_line in landed_cost.valuation_adjustment_lines: val_to_cost_lines[val_line.cost_line_id] += val_line.additional_landed_cost if any(tools.float_compare(cost_line.price_unit, val_amount, precision_digits=prec_digits) != 0 - for cost_line, val_amount in pycompat.items(val_to_cost_lines)): + for cost_line, val_amount in val_to_cost_lines.items()): return False return True @@ -204,9 +203,8 @@ class LandedCost(models.Model): towrite_dict[valuation.id] = value else: towrite_dict[valuation.id] += value - if towrite_dict: - for key, value in pycompat.items(towrite_dict): - AdjustementLines.browse(key).write({'additional_landed_cost': value}) + for key, value in towrite_dict.items(): + AdjustementLines.browse(key).write({'additional_landed_cost': value}) return True diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index 553fc5ad4b2..150503f07da 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -174,7 +174,7 @@ class WebsiteSurvey(http.Controller): answer_tag = "%s_%s" % (answer_tag, 'comment') answer_value = answer.value_text elif answer.answer_type == 'number': - answer_value = answer.value_number.__str__() + answer_value = str(answer.value_number) elif answer.answer_type == 'date': answer_value = answer.value_date elif answer.answer_type == 'suggestion' and not answer.value_suggested_row: diff --git a/addons/survey/models/survey.py b/addons/survey/models/survey.py index 84692c63f9d..7bcf53f67ab 100644 --- a/addons/survey/models/survey.py +++ b/addons/survey/models/survey.py @@ -15,8 +15,6 @@ from odoo.exceptions import UserError, ValidationError from odoo.addons.website.models.website import slug -from odoo.tools import pycompat - email_validator = re.compile(r"[^@]+@[^@]+\.[^@]+") _logger = logging.getLogger(__name__) @@ -26,7 +24,7 @@ def dict_keys_startswith(dictionary, string): .. note:: This function uses dictionary comprehensions (Python >= 2.7) """ - return {k: v for k, v in pycompat.items(dictionary) if k.startswith(string)} + return {k: v for k, v in dictionary.items() if k.startswith(string)} class SurveyStage(models.Model): @@ -240,7 +238,7 @@ class Survey(models.Model): answers[input_line.value_suggested.id]['count'] += 1 if input_line.answer_type == 'text' and (not(current_filters) or input_line.user_input_id.id in current_filters): comments.append(input_line) - result_summary = {'answers': list(pycompat.values(answers)), 'comments': comments} + result_summary = {'answers': list(answers.values()), 'comments': comments} # Calculate and return statistics for matrix if question.type == 'matrix': @@ -640,7 +638,7 @@ class SurveyQuestion(models.Model): if self.comments_allowed: comment_answer = answer_candidates.pop(("%s_%s" % (answer_tag, 'comment')), '').strip() # Preventing answers with blank value - if all(not answer.strip() for answer in pycompat.values(answer_candidates)) and answer_candidates: + if all(not answer.strip() for answer in answer_candidates.values()) and answer_candidates: errors.update({answer_tag: self.constr_error_msg}) # There is no answer neither comments (if comments count as answer) if not answer_candidates and self.comment_count_as_answer and (not comment_flag or not comment_answer): diff --git a/addons/survey/tests/test_survey.py b/addons/survey/tests/test_survey.py index 699aac0cde3..b5f9897438b 100644 --- a/addons/survey/tests/test_survey.py +++ b/addons/survey/tests/test_survey.py @@ -13,8 +13,6 @@ from odoo.exceptions import UserError from odoo.tests.common import TransactionCase from odoo.addons.website.models.website import slug -from odoo.tools import pycompat - class TestSurvey(TransactionCase): @@ -179,7 +177,7 @@ class TestSurvey(TransactionCase): base_url = self.env['ir.config_parameter'].get_param('web.base.url') urltypes = {'public': 'start', 'print': 'print', 'result': 'results'} - for urltype, urltxt in pycompat.items(urltypes): + for urltype, urltxt in urltypes.items(): survey_url = getattr(self.survey1, urltype + '_url') survey_url_relative = getattr(self.survey1.with_context({'relative_url': True}), urltype + '_url') self.assertTrue(validate_url(survey_url)) @@ -211,8 +209,8 @@ class TestSurvey(TransactionCase): answers = [input_portal.user_input_line_ids[0], input_public.user_input_line_ids[0]] expected_values = {'answer_type': 'free_text', 'value_free_text': "Test Answer"} for answer in answers: - for field, value in pycompat.items(expected_values): - self.assertEqual(getattr(answer, field), value, msg="Unable to answer the survey. Expected behaviour of %s is not proper." % (field)) + for field, value in expected_values.items(): + self.assertEqual(answer[field], value, msg="Unable to answer the survey. Expected behaviour of %s is not proper." % (field)) def test_10_survey_result_simple_multiple_choice(self): question = self.env['survey.question'].sudo(self.survey_manager).create({ @@ -226,9 +224,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({ @@ -267,7 +263,7 @@ class TestSurvey(TransactionCase): 'print': {'method': 'print', 'token': '/test', 'text': 'Print'}, 'result': {'method': 'result', 'token': '', 'text': 'Results of the'}, 'test': {'method': 'public', 'token': '/phantom', 'text': 'Results of the'}} - for action, val in pycompat.items(actions): + for action, val in actions.items(): result = getattr(self.survey1.with_context({'survey_token': val['token'][1:]}), 'action_' + action + '_survey')() url = getattr(self.survey1.with_context({'relative_url': True}), val['method'] + '_url') + val['token'] self.assertEqual(result['url'], url) diff --git a/addons/survey/wizard/survey_email_compose_message.py b/addons/survey/wizard/survey_email_compose_message.py index fb36d839c57..f23d836bc30 100644 --- a/addons/survey/wizard/survey_email_compose_message.py +++ b/addons/survey/wizard/survey_email_compose_message.py @@ -8,6 +8,7 @@ from werkzeug import urls from odoo import api, fields, models, _ from odoo.exceptions import UserError +from odoo.tools import pycompat emails_split = re.compile(r"[;,\n\r]+") email_validator = re.compile(r"[^@]+@[^@]+\.[^@]+") @@ -119,7 +120,7 @@ class SurveyMailComposeMessage(models.TransientModel): if wizard.public != 'email_private': return None else: - token = uuid.uuid4().__str__() + token = pycompat.text_type(uuid.uuid4()) # create response with token survey_user_input = SurveyUserInput.create({ 'survey_id': wizard.survey_id.id, diff --git a/addons/utm/models/utm.py b/addons/utm/models/utm.py index 5cb04d51425..b73329c1011 100644 --- a/addons/utm/models/utm.py +++ b/addons/utm/models/utm.py @@ -3,6 +3,7 @@ from odoo import api, fields, models, SUPERUSER_ID from odoo.http import request +from odoo.tools import pycompat class UtmMedium(models.Model): @@ -72,7 +73,7 @@ class UtmMixin(models.AbstractModel): # ir_http dispatch saves the url params in a cookie value = request.httprequest.cookies.get(cookie_name) # if we receive a string for a many2one, we search/create the id - if field.type == 'many2one' and isinstance(value, basestring) and value: + if field.type == 'many2one' and isinstance(value, pycompat.string_types) and value: Model = self.env[field.comodel_name] records = Model.search([('name', '=', value)], limit=1) if not records: diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 9a29c2ec3e8..abf019371dc 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. + import babel.messages.pofile import base64 -import csv import datetime import functools import glob import hashlib import imghdr +import io import itertools import jinja2 import json @@ -22,8 +23,8 @@ import werkzeug.utils import werkzeug.wrappers import zlib from xml.etree import ElementTree -from cStringIO import StringIO from werkzeug import url_decode +from werkzeug import iri_to_uri import odoo @@ -117,12 +118,11 @@ def ensure_db(redirect='/web/database/selector'): # Thus, we redirect the user to the same page but with the session cookie set. # This will force using the database route dispatcher... r = request.httprequest - url_redirect = r.base_url + url_redirect = werkzeug.urls.url_parse(r.base_url) if r.query_string: - # Can't use werkzeug.wrappers.BaseRequest.url with encoded hashes: - # https://github.com/amigrave/werkzeug/commit/b4a62433f2f7678c234cdcac6247a869f90a7eb7 - url_redirect += '?' + r.query_string - response = werkzeug.utils.redirect(url_redirect, 302) + # in P3, request.query_string is bytes, the rest is text, can't mix them + query_string = iri_to_uri(r.query_string) + url_redirect = url_redirect.replace(query=query_string) request.session.db = db abort_and_redirect(url_redirect) @@ -963,7 +963,7 @@ class Binary(http.Controller): def force_contenttype(self, headers, contenttype='image/png'): dictheaders = dict(headers) dictheaders['Content-Type'] = contenttype - return list(pycompat.items(dictheaders)) + return list(dictheaders.items()) @http.route(['/web/content', '/web/content/', @@ -1136,8 +1136,8 @@ class Binary(http.Controller): """, (uid,)) row = cr.fetchone() if row and row[0]: - image_base64 = str(row[0]).decode('base64') - image_data = StringIO(image_base64) + image_base64 = base64.b64decode(row[0]) + image_data = io.BytesIO(image_base64) imgext = '.' + (imghdr.what(None, h=image_base64) or 'png') response = http.send_file(image_data, filename=imgname + imgext, mtime=row[1]) else: @@ -1216,7 +1216,7 @@ class Export(http.Controller): else: fields['.id'] = fields.pop('id', {'string': 'ID'}) - fields_sequence = sorted(pycompat.items(fields), + fields_sequence = sorted(fields.items(), key=lambda field: odoo.tools.ustr(field[1].get('string', ''))) records = [] @@ -1227,7 +1227,7 @@ class Export(http.Controller): if field.get('readonly'): # If none of the field's states unsets readonly, skip the field if all(dict(attrs).get('readonly', True) - for attrs in pycompat.values(field.get('states', {}))): + for attrs in field.get('states', {}).values()): continue if not field.get('exportable', True): continue @@ -1319,7 +1319,7 @@ class Export(http.Controller): export_fields = [field.split('/', 1)[1] for field in fields] return ( (prefix + '/' + k, prefix_string + '/' + v) - for k, v in pycompat.items(self.fields_info(model, export_fields))) + for k, v in self.fields_info(model, export_fields).items()) class ExportFormat(object): raw_data = False @@ -1387,32 +1387,22 @@ class CSVExport(ExportFormat, http.Controller): return base + '.csv' def from_data(self, fields, rows): - fp = StringIO() - writer = csv.writer(fp, quoting=csv.QUOTE_ALL) + fp = io.BytesIO() + writer = pycompat.csv_writer(fp, quoting=1) - writer.writerow([name.encode('utf-8') for name in fields]) + writer.writerow(fields) for data in rows: row = [] for d in data: - if isinstance(d, unicode): - try: - d = d.encode('utf-8') - except UnicodeError: - pass - if d is False: d = None - # Spreadsheet apps tend to detect formulas on leading =, + and - - if type(d) is str and d.startswith(('=', '-', '+')): + if isinstance(d, pycompat.string_types) and d.startswith(('=', '-', '+')): d = "'" + d - row.append(d) + row.append(pycompat.to_text(d)) writer.writerow(row) - fp.seek(0) - data = fp.read() - fp.close() - return data + return fp.getvalue() class ExcelExport(ExportFormat, http.Controller): # Excel needs raw data to correctly handle numbers and date values @@ -1445,7 +1435,7 @@ class ExcelExport(ExportFormat, http.Controller): for row_index, row in enumerate(rows): for cell_index, cell_value in enumerate(row): cell_style = base_style - if isinstance(cell_value, basestring): + if isinstance(cell_value, pycompat.string_types): cell_value = re.sub("\r", " ", cell_value) elif isinstance(cell_value, datetime.datetime): cell_style = datetime_style @@ -1453,7 +1443,7 @@ class ExcelExport(ExportFormat, http.Controller): cell_style = date_style worksheet.write(row_index + 1, cell_index, cell_value, cell_style) - fp = StringIO() + fp = io.BytesIO() workbook.save(fp) fp.seek(0) data = fp.read() diff --git a/addons/web_diagram/controllers/main.py b/addons/web_diagram/controllers/main.py index 56b6c76eefe..d29b60df40e 100644 --- a/addons/web_diagram/controllers/main.py +++ b/addons/web_diagram/controllers/main.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import odoo.http as http -from odoo.tools import pycompat from odoo.tools.safe_eval import safe_eval @@ -46,7 +45,7 @@ class DiagramView(http.Controller): isolate_nodes[blnk_node['id']] = blnk_node y = [ t['y'] - for t in pycompat.values(nodes) + for t in nodes.values() if t['x'] == 20 if t['y'] ] @@ -99,11 +98,11 @@ class DiagramView(http.Controller): color='white', options={} ) - for color, expr in pycompat.items(bgcolors): + for color, expr in bgcolors.items(): if safe_eval(expr, act): n['color'] = color - for shape, expr in pycompat.items(shapes): + for shape, expr in shapes.items(): if safe_eval(expr, act): n['shape'] = shape diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py index 5a3eca776ef..50a9540323a 100644 --- a/addons/web_editor/controllers/main.py +++ b/addons/web_editor/controllers/main.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - -import cStringIO +import base64 import io import json import logging @@ -14,11 +13,11 @@ from lxml import etree, html from odoo.http import request from odoo import http, tools +from odoo.tools import pycompat from odoo.modules.module import get_resource_path, get_module_path logger = logging.getLogger(__name__) - class Web_Editor(http.Controller): #------------------------------------------------------ # Backend snippet @@ -40,7 +39,7 @@ class Web_Editor(http.Controller): debug=request.debug) for k in kwargs: - if isinstance(kwargs[k], basestring) and kwargs[k].isdigit(): + if isinstance(kwargs[k], pycompat.string_types) and kwargs[k].isdigit(): kwargs[k] = int(kwargs[k]) trans = dict( @@ -96,7 +95,7 @@ class Web_Editor(http.Controller): font_obj = ImageFont.truetype(addons_path + font, size) # if received character is not a number, keep old behaviour (icon is character) - icon = unichr(int(icon)) if icon.isdigit() else icon + icon = pycompat.unichr(int(icon)) if icon.isdigit() else icon # Determine the dimensions of the icon image = Image.new("RGBA", (size, size), color=(0, 0, 0, 0)) @@ -165,7 +164,7 @@ class Web_Editor(http.Controller): for c_file in request.httprequest.files.getlist('upload'): data = c_file.read() try: - image = Image.open(cStringIO.StringIO(data)) + image = Image.open(io.BytesIO(data)) w, h = image.size if w*h > 42e6: # Nokia Lumia 1020 photo resolution raise ValueError( @@ -178,7 +177,7 @@ class Web_Editor(http.Controller): attachment = Attachments.create({ 'name': c_file.filename, - 'datas': data.encode('base64'), + 'datas': base64.b64encode(data), 'datas_fname': c_file.filename, 'public': True, 'res_model': 'ir.ui.view', @@ -187,7 +186,7 @@ class Web_Editor(http.Controller): uploads += attachments.read(['name', 'mimetype', 'checksum', 'url']) except Exception as e: logger.exception("Failed to upload image to attachment") - message = unicode(e) + message = pycompat.text_type(e) return """' + replacement = u'' view.save(replacement, xpath='/t/p/h1') self.assertIn( - replacement.replace('&', '&'), + replacement.replace(u'&', u'&'), view.arch, 'inline script should be escaped server side' ) self.assertIn( replacement, - view.render(), + view.render().decode('utf-8'), 'inline script should not be escaped when rendering' ) # common text nodes should be be escaped client side - replacement = 'world &amp; &lt;b&gt;cie' + replacement = u'world &amp; &lt;b&gt;cie' view.save(replacement, xpath='/t/p') self.assertIn(replacement, view.arch, 'common text node should not be escaped server side') self.assertIn( replacement, - view.render().replace('&', '&'), + view.render().decode('utf-8').replace(u'&', u'&'), 'text node characters wrongly unescaped when rendering' ) @@ -225,7 +224,8 @@ class TestViewSaving(common.TransactionCase): node = html.tostring(h.SPAN( "Acme Corporation", - attrs(model='res.company', id=company_id, field="name", expression='bob', type='char'))) + attrs(model='res.company', id=company_id, field="name", expression='bob', type='char')), + encoding='unicode') View = self.env['ir.ui.view'] View.browse(company_id).save(value=node) self.assertEqual(company.name, "Acme Corporation") diff --git a/addons/website_blog/controllers/main.py b/addons/website_blog/controllers/main.py index c400b8b792f..a7e930d8e9f 100644 --- a/addons/website_blog/controllers/main.py +++ b/addons/website_blog/controllers/main.py @@ -288,8 +288,8 @@ class WebsiteBlog(http.Controller): "id": message.id, "author_name": message.author_id.name, "author_image": message.author_id.image and \ - ("data:image/png;base64,%s" % message.author_id.image) or \ - '/website_blog/static/src/img/anonymous.png', + (b"data:image/png;base64,%s" % message.author_id.image) or \ + b'/website_blog/static/src/img/anonymous.png', "date": message.date, 'body': html2plaintext(message.body), 'website_published' : message.website_published, 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_crm_partner_assign/controllers/main.py b/addons/website_crm_partner_assign/controllers/main.py index 3c18c7cf470..6e300e66cd0 100644 --- a/addons/website_crm_partner_assign/controllers/main.py +++ b/addons/website_crm_partner_assign/controllers/main.py @@ -13,7 +13,6 @@ from odoo.http import request from odoo.addons.website.models.website import slug, unslug from odoo.addons.website_partner.controllers.main import WebsitePartnerPage -from odoo.tools import pycompat from odoo.tools.translate import _ from odoo.addons.website_portal.controllers.main import website_account @@ -151,7 +150,7 @@ class WebsiteAccount(website_account): 'pager': pager, 'searchbar_sortings': searchbar_sortings, 'sortby': sortby, - 'searchbar_filters': OrderedDict(sorted(pycompat.items(searchbar_filters))), + 'searchbar_filters': OrderedDict(sorted(searchbar_filters.items())), 'filterby': filterby, }) return request.render("website_crm_partner_assign.portal_my_opportunities", values) diff --git a/addons/website_crm_partner_assign/models/crm_lead.py b/addons/website_crm_partner_assign/models/crm_lead.py index f1f002d5dd0..b73be955c42 100644 --- a/addons/website_crm_partner_assign/models/crm_lead.py +++ b/addons/website_crm_partner_assign/models/crm_lead.py @@ -5,7 +5,6 @@ import random from odoo.addons.base_geolocalize.models.res_partner import geo_find, geo_query_address from odoo import api, fields, models, _ -from odoo.tools import pycompat class CrmLead(models.Model): @@ -45,7 +44,7 @@ class CrmLead(models.Model): if lead.partner_assigned_id and lead.partner_assigned_id.user_id != lead.user_id: salesmans_leads.setdefault(lead.partner_assigned_id.user_id.id, []).append(lead.id) - for salesman_id, leads_ids in pycompat.items(salesmans_leads): + for salesman_id, leads_ids in salesmans_leads.items(): leads = self.browse(leads_ids) leads.write({'user_id': salesman_id}) diff --git a/addons/website_crm_partner_assign/wizard/crm_forward_to_partner.py b/addons/website_crm_partner_assign/wizard/crm_forward_to_partner.py index cb2fbf3ba03..5aa8073192f 100644 --- a/addons/website_crm_partner_assign/wizard/crm_forward_to_partner.py +++ b/addons/website_crm_partner_assign/wizard/crm_forward_to_partner.py @@ -3,7 +3,6 @@ from odoo import api, fields, models, _ from odoo.exceptions import UserError -from odoo.tools import pycompat class CrmLeadForwardToPartner(models.TransientModel): @@ -86,7 +85,7 @@ class CrmLeadForwardToPartner(models.TransientModel): else: partners_leads[partner.id] = {'partner': partner, 'leads': [lead_details]} - for partner_id, partner_leads in pycompat.items(partners_leads): + for partner_id, partner_leads in partners_leads.items(): in_portal = False if portal_group: for contact in (partner.child_ids or partner).filtered(lambda contact: contact.user_ids): 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/controllers/main.py b/addons/website_event/controllers/main.py index cd020a74372..5aed9a25ffa 100644 --- a/addons/website_event/controllers/main.py +++ b/addons/website_event/controllers/main.py @@ -9,7 +9,6 @@ from dateutil.relativedelta import relativedelta from odoo import fields, http, _ from odoo.addons.website.models.website import slug from odoo.http import request -from odoo.tools import pycompat class WebsiteEventController(http.Controller): @@ -80,7 +79,7 @@ class WebsiteEventController(http.Controller): def dom_without(without): domain = [('state', "in", ['draft', 'confirm', 'done'])] - for key, search in pycompat.items(domain_search): + for key, search in domain_search.items(): if key != without: domain += search return domain @@ -234,16 +233,16 @@ class WebsiteEventController(http.Controller): ''' Process data posted from the attendee details form. ''' registrations = {} global_values = {} - for key, value in pycompat.items(details): + for key, value in details.items(): counter, field_name = key.split('-', 1) if counter == '0': global_values[field_name] = value else: registrations.setdefault(counter, dict())[field_name] = value - for key, value in pycompat.items(global_values): - for registration in pycompat.values(registrations): + for key, value in global_values.items(): + for registration in registrations.values(): registration[key] = value - return list(pycompat.values(registrations)) + return list(registrations.values()) @http.route(['/event//registration/confirm'], type='http', auth="public", methods=['POST'], website=True) def registration_confirm(self, event, **post): diff --git a/addons/website_event_questions/controllers/main.py b/addons/website_event_questions/controllers/main.py index e709e0b3d06..684999a26ff 100644 --- a/addons/website_event_questions/controllers/main.py +++ b/addons/website_event_questions/controllers/main.py @@ -3,8 +3,6 @@ from odoo.addons.website_event.controllers.main import WebsiteEventController -from odoo.tools import pycompat - class WebsiteEvent(WebsiteEventController): @@ -13,7 +11,7 @@ class WebsiteEvent(WebsiteEventController): registrations = super(WebsiteEvent, self)._process_registration_details(details) for registration in registrations: answer_ids = [] - for key, value in pycompat.items(registration): + for key, value in registration.items(): if key.startswith('answer_ids-'): answer_ids.append([4, int(value)]) registration['answer_ids'] = answer_ids diff --git a/addons/website_event_sale/controllers/main.py b/addons/website_event_sale/controllers/main.py index 324fa1026ed..7ccb7f9e4b5 100644 --- a/addons/website_event_sale/controllers/main.py +++ b/addons/website_event_sale/controllers/main.py @@ -4,7 +4,6 @@ from odoo import http, _ from odoo.addons.website_event.controllers.main import WebsiteEventController from odoo.http import request -from odoo.tools import pycompat class WebsiteEventSaleController(WebsiteEventController): @@ -16,7 +15,7 @@ class WebsiteEventSaleController(WebsiteEventController): def _process_tickets_details(self, data): ticket_post = {} - for key, value in pycompat.items(data): + for key, value in data.items(): if not key.startswith('nb_register') or '-' not in key: continue items = key.split('-') diff --git a/addons/website_event_track/controllers/main.py b/addons/website_event_track/controllers/main.py index 3fb37210589..f11aa0bea13 100644 --- a/addons/website_event_track/controllers/main.py +++ b/addons/website_event_track/controllers/main.py @@ -8,7 +8,7 @@ import pytz from odoo import fields, http from odoo.http import request -from odoo.tools import html_escape as escape, html2plaintext, pycompat +from odoo.tools import html_escape as escape, html2plaintext class WebsiteEventTrackController(http.Controller): @@ -68,14 +68,14 @@ class WebsiteEventTrackController(http.Controller): @http.route(['''/event/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) @@ -578,7 +577,7 @@ class WebsiteForum(http.Controller): img_path = modules.get_module_resource('web', 'static/src/img', 'placeholder.png') with open(img_path, 'rb') as f: image = f.read() - content = image.encode('base64') + content = base64.b64encode(image) if status == 304: return werkzeug.wrappers.Response(status=304) image_base64 = base64.b64decode(content) @@ -712,7 +711,7 @@ class WebsiteForum(http.Controller): values['image'] = False elif kwargs.get('ufile'): image = kwargs.get('ufile').read() - values['image'] = image.encode('base64') + values['image'] = base64.b64encode(image) if request.uid == user.id: # the controller allows to edit only its own privacy settings; use partner management for other cases values['website_published'] = kwargs.get('website_published') == 'True' diff --git a/addons/website_forum/models/res_users.py b/addons/website_forum/models/res_users.py index 9d22ee3c9d8..054e3a4d56a 100644 --- a/addons/website_forum/models/res_users.py +++ b/addons/website_forum/models/res_users.py @@ -66,11 +66,12 @@ class Users(models.Model): and is a hash based on a (secret) uuid generated by the forum module, the user_id, the email and currently the day (to be updated if necessary). """ forum_uuid = self.env['ir.config_parameter'].sudo().get_param('website_forum.uuid') - return hashlib.sha256('%s-%s-%s-%s' % ( + return hashlib.sha256((u'%s-%s-%s-%s' % ( datetime.now().replace(hour=0, minute=0, second=0, microsecond=0), forum_uuid, user_id, - email)).hexdigest() + email + )).encode('utf-8')).hexdigest() @api.one def send_forum_validation_email(self, forum_id=None): diff --git a/addons/website_livechat/controllers/main.py b/addons/website_livechat/controllers/main.py index 675b56e561f..753e4eef1ae 100644 --- a/addons/website_livechat/controllers/main.py +++ b/addons/website_livechat/controllers/main.py @@ -3,7 +3,6 @@ from odoo import http from odoo.http import request -from odoo.tools import pycompat class WebsiteLivechat(http.Controller): @@ -27,7 +26,7 @@ class WebsiteLivechat(http.Controller): # compute percentage percentage = dict.fromkeys(['great', 'okay', 'bad'], 0) for grade in repartition: - percentage[grade] = repartition[grade] * 100 / sum(pycompat.values(repartition)) if sum(pycompat.values(repartition)) else 0 + percentage[grade] = repartition[grade] * 100 / sum(repartition.values()) if sum(repartition.values()) else 0 # the value dict to render the template values = { diff --git a/addons/website_mail/controllers/main.py b/addons/website_mail/controllers/main.py index c10525fd5e0..ab42fb5c3b5 100644 --- a/addons/website_mail/controllers/main.py +++ b/addons/website_mail/controllers/main.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - from werkzeug.exceptions import NotFound, Forbidden from odoo import http diff --git a/addons/website_mail_channel/models/mail_channel.py b/addons/website_mail_channel/models/mail_channel.py index 519dfe7afc6..79c6cc27287 100644 --- a/addons/website_mail_channel/models/mail_channel.py +++ b/addons/website_mail_channel/models/mail_channel.py @@ -68,4 +68,4 @@ class MailGroup(models.Model): str(self.id), str(partner_id), action]) - return hmac.new(secret.encode('utf-8'), data).hexdigest() + return hmac.new(secret.encode('utf-8'), data.encode('utf-8')).hexdigest() diff --git a/addons/website_portal/controllers/main.py b/addons/website_portal/controllers/main.py index 3b2fbfc2a2d..865312cc7c6 100644 --- a/addons/website_portal/controllers/main.py +++ b/addons/website_portal/controllers/main.py @@ -4,7 +4,6 @@ from odoo import http from odoo.http import request from odoo import tools -from odoo.tools import pycompat from odoo.tools.translate import _ from odoo.exceptions import ValidationError @@ -134,7 +133,7 @@ class website_account(http.Controller): error["vat"] = 'error' # error message for empty required fields - if [err for err in pycompat.values(error) if err == 'missing']: + if [err for err in error.values() if err == 'missing']: error_message.append(_('Some required fields are empty.')) unknown = [k for k in data if k not in self.MANDATORY_BILLING_FIELDS + self.OPTIONAL_BILLING_FIELDS] diff --git a/addons/website_portal_purchase/controllers/website_portal.py b/addons/website_portal_purchase/controllers/website_portal.py index 874c311f92a..4d8762e4346 100644 --- a/addons/website_portal_purchase/controllers/website_portal.py +++ b/addons/website_portal_purchase/controllers/website_portal.py @@ -6,7 +6,6 @@ from collections import OrderedDict from odoo import http from odoo.exceptions import AccessError from odoo.http import request -from odoo.tools import pycompat from odoo.tools.translate import _ from odoo.addons.website_portal.controllers.main import website_account, get_records_pager @@ -89,7 +88,7 @@ class WebsitePortal(website_account): 'archive_groups': archive_groups, 'searchbar_sortings': searchbar_sortings, 'sortby': sortby, - 'searchbar_filters': OrderedDict(sorted(pycompat.items(searchbar_filters))), + 'searchbar_filters': OrderedDict(sorted(searchbar_filters.items())), 'filterby': filterby, 'default_url': '/my/purchase', }) diff --git a/addons/website_project/controllers/main.py b/addons/website_project/controllers/main.py index d8716efd9ac..720c4f65700 100644 --- a/addons/website_project/controllers/main.py +++ b/addons/website_project/controllers/main.py @@ -8,7 +8,6 @@ from odoo.http import request from odoo.addons.website_portal.controllers.main import website_account, get_records_pager from odoo.osv.expression import OR -from odoo.tools import pycompat class WebsiteAccount(website_account): @@ -156,7 +155,7 @@ class WebsiteAccount(website_account): 'searchbar_inputs': searchbar_inputs, 'search_in': search_in, 'sortby': sortby, - 'searchbar_filters': OrderedDict(sorted(pycompat.items(searchbar_filters))), + 'searchbar_filters': OrderedDict(sorted(searchbar_filters.items())), 'filterby': filterby, }) return request.render("website_project.my_tasks", values) diff --git a/addons/website_project_issue/controllers/main.py b/addons/website_project_issue/controllers/main.py index 1ce21cff8e8..940544a2188 100644 --- a/addons/website_project_issue/controllers/main.py +++ b/addons/website_project_issue/controllers/main.py @@ -105,7 +105,7 @@ class WebsiteAccount(website_account): 'searchbar_sortings': searchbar_sortings, 'searchbar_inputs': searchbar_inputs, 'sortby': sortby, - 'searchbar_filters': OrderedDict(sorted(pycompat.items(searchbar_filters))), + 'searchbar_filters': OrderedDict(sorted(searchbar_filters.items())), 'filterby': filterby, 'search_in': search_in, 'search': search, diff --git a/addons/website_quote/controllers/main.py b/addons/website_quote/controllers/main.py index 8e4359ecbec..c9dff4745ad 100644 --- a/addons/website_quote/controllers/main.py +++ b/addons/website_quote/controllers/main.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import base64 import werkzeug @@ -91,7 +92,7 @@ class sale_quote(http.Controller): return request.render('website.404') if Order.state != 'sent': return False - attachments = [('signature.png', sign.decode('base64'))] if sign else [] + attachments = [('signature.png', base64.b64decode(sign))] if sign else [] Order.action_confirm() message = _('Order signed by %s') % (signer,) _message_post_helper(message=message, res_id=order_id, res_model='sale.order', attachments=attachments, **({'token': token, 'token_field': 'access_token'} if token else {})) diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 8f02d4a0117..40057484efe 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -12,8 +12,6 @@ from odoo.addons.website.controllers.main import QueryURL from odoo.exceptions import ValidationError from odoo.addons.website_form.controllers.main import WebsiteForm -from odoo.tools import pycompat - _logger = logging.getLogger(__name__) PPG = 20 # Products Per Page @@ -53,35 +51,35 @@ 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 - rows = sorted(pycompat.items(self.table)) + rows = sorted(self.table.items()) rows = [r[1] for r in rows] for col in range(len(rows)): - cols = sorted(pycompat.items(rows[col])) + cols = sorted(rows[col].items()) x += len(cols) rows[col] = [r[1] for r in cols if r[1]] @@ -373,7 +371,7 @@ class WebsiteSale(http.Controller): return request.redirect("/shop/cart") def _filter_attributes(self, **kw): - return {k: v for k, v in pycompat.items(kw) if "attribute" in k} + return {k: v for k, v in kw.items() if "attribute" in k} @http.route(['/shop/cart/update_json'], type='json', auth="public", methods=['POST'], website=True, csrf=False) def cart_update_json(self, product_id, line_id=None, add_qty=None, set_qty=None, display=True): @@ -491,7 +489,7 @@ class WebsiteSale(http.Controller): except ValidationError: error["vat"] = 'error' - if [err for err in pycompat.items(error) if err == 'missing']: + if [err for err in error.items() if err == 'missing']: error_message.append(_('Some required fields are empty.')) return error, error_message @@ -517,7 +515,7 @@ class WebsiteSale(http.Controller): def values_postprocess(self, order, mode, values, errors, error_msg): new_values = {} authorized_fields = request.env['ir.model']._get('res.partner')._get_form_writable_fields() - for k, v in pycompat.items(values): + for k, v in values.items(): # don't drop empty value, it could be a field to reset if k in authorized_fields and v is not None: new_values[k] = v @@ -680,7 +678,7 @@ class WebsiteSale(http.Controller): # if form posted if 'post_values' in post: values = {} - for field_name, field_value in pycompat.items(post): + for field_name, field_value in post.items(): if field_name in request.env['sale.order']._fields and field_name.startswith('x_'): values[field_name] = field_value if values: @@ -910,7 +908,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/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index cffff7f2f3d..724d24b5c03 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -6,7 +6,6 @@ import random from odoo import api, models, fields, _ from odoo.http import request from odoo.exceptions import UserError, ValidationError -from odoo.tools import pycompat _logger = logging.getLogger(__name__) @@ -93,7 +92,7 @@ class SaleOrder(models.Model): # add untracked attributes in the name untracked_attributes = [] - for k, v in pycompat.items(attributes): + for k, v in attributes.items(): # attribute should be like 'attribute-48-1' where 48 is the product_id, 1 is the attribute_id and v is the attribute value attribute_value = self.env['product.attribute.value'].sudo().browse(int(v)) if attribute_value and not attribute_value.attribute_id.create_variant: diff --git a/addons/website_sale/tests/test_website_sale_pricelist.py b/addons/website_sale/tests/test_website_sale_pricelist.py index cf97fd815cc..21accb52814 100644 --- a/addons/website_sale/tests/test_website_sale_pricelist.py +++ b/addons/website_sale/tests/test_website_sale_pricelist.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from mock import patch from odoo.tests.common import TransactionCase -from odoo.tools import pycompat class TestWebsitePriceList(TransactionCase): @@ -61,7 +60,7 @@ class TestWebsitePriceList(TransactionCase): 'CA': ['Canada'], 'US': ['USD', 'EUR', 'Benelux', 'Canada'] } - for country, result in pycompat.items(country_list): + for country, result in country_list.items(): pls = self.get_pl(show, current_pl, country) self.assertEquals(len(set(pls.mapped('name')) & set(result)), len(pls), 'Test failed for %s (%s %s vs %s %s)' % (country, len(pls), pls.mapped('name'), len(result), result)) @@ -78,7 +77,7 @@ class TestWebsitePriceList(TransactionCase): 'CA': ['Canada'] } - for country, result in pycompat.items(country_list): + for country, result in country_list.items(): pls = self.get_pl(show, current_pl, country) self.assertEquals(len(set(pls.mapped('name')) & set(result)), len(pls), 'Test failed for %s (%s %s vs %s %s)' % (country, len(pls), pls.mapped('name'), len(result), result)) @@ -99,7 +98,7 @@ class TestWebsitePriceList(TransactionCase): 'CA': False } - for country, result in pycompat.items(country_list): + for country, result in country_list.items(): self.args['country'] = country # mock patch method could not pass env context available = self.website.is_pricelist_available(christmas_pl) @@ -120,7 +119,7 @@ class TestWebsitePriceList(TransactionCase): 'CA': ['EUR', 'Canada'], 'US': ['USD', 'EUR', 'Benelux', 'Canada'] } - for country, result in pycompat.items(country_list): + for country, result in country_list.items(): pls = self.get_pl(show, current_pl, country) self.assertEquals(len(set(pls.mapped('name')) & set(result)), len(pls), 'Test failed for %s (%s %s vs %s %s)' % (country, len(pls), pls.mapped('name'), len(result), result)) diff --git a/addons/website_sale_digital/controllers/main.py b/addons/website_sale_digital/controllers/main.py index ec0aeec82a7..67e125ee341 100644 --- a/addons/website_sale_digital/controllers/main.py +++ b/addons/website_sale_digital/controllers/main.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import base64 -from cStringIO import StringIO +import io from werkzeug.utils import redirect from odoo import http @@ -99,7 +99,7 @@ class WebsiteSaleDigital(website_account): else: return request.not_found() elif attachment["datas"]: - data = StringIO(base64.standard_b64decode(attachment["datas"])) + data = io.BytesIO(base64.standard_b64decode(attachment["datas"])) return http.send_file(data, filename=attachment['name'], as_attachment=True) else: return request.not_found() diff --git a/addons/website_sale_options/controllers/main.py b/addons/website_sale_options/controllers/main.py index 8349f0e08bc..af48c17f2e1 100644 --- a/addons/website_sale_options/controllers/main.py +++ b/addons/website_sale_options/controllers/main.py @@ -5,8 +5,6 @@ from odoo import http from odoo.http import request from odoo.addons.website_sale.controllers.main import WebsiteSale -from odoo.tools import pycompat - class WebsiteSaleOptions(WebsiteSale): @@ -26,7 +24,7 @@ class WebsiteSaleOptions(WebsiteSale): option_ids = product.optional_product_ids.mapped('product_variant_ids').ids optional_product_ids = [] - for k, v in pycompat.items(kw): + for k, v in kw.items(): if "optional-product-" in k and int(kw.get(k.replace("product", "add"))) and int(v) in option_ids: optional_product_ids.append(int(v)) diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index 0b7fdb47982..7bf77624fe6 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -164,7 +164,7 @@ class WebsiteSlides(http.Controller): @http.route('''/slides/slide//pdf_content''', type='http', auth="public", website=True) def slide_get_pdf_content(self, slide): response = werkzeug.wrappers.Response() - response.data = slide.datas and slide.datas.decode('base64') or '' + response.data = slide.datas and base64.b64decode(slide.datas) or b'' response.mimetype = 'application/pdf' return response diff --git a/addons/website_slides/models/slides.py b/addons/website_slides/models/slides.py index ee65b15734b..ad5df26cadd 100644 --- a/addons/website_slides/models/slides.py +++ b/addons/website_slides/models/slides.py @@ -3,6 +3,7 @@ import requests from PIL import Image +import base64 import datetime import io import json @@ -11,7 +12,7 @@ import re from werkzeug import urls from odoo import api, fields, models, SUPERUSER_ID, _ -from odoo.tools import image, pycompat +from odoo.tools import image from odoo.tools.translate import html_translate from odoo.exceptions import Warning from odoo.addons.website.models.website import slug @@ -322,8 +323,8 @@ class Slide(models.Model): values = res['values'] if not values.get('document_id'): raise Warning(_('Please enter valid Youtube or Google Doc URL')) - for key, value in pycompat.items(values): - setattr(self, key, value) + for key, value in values.items(): + self[key] = value # website date_published = fields.Datetime('Publish Date') @@ -384,7 +385,7 @@ class Slide(models.Model): values['date_published'] = datetime.datetime.now() if values.get('url') and not values.get('document_id'): doc_data = self._parse_document_url(values['url']).get('values', dict()) - for key, value in pycompat.items(doc_data): + for key, value in doc_data.items(): values.setdefault(key, value) # Do not publish slide if user has not publisher rights if not self.user_has_groups('website.group_website_publisher'): @@ -398,7 +399,7 @@ class Slide(models.Model): def write(self, values): if values.get('url') and values['url'] != self.url: doc_data = self._parse_document_url(values['url']).get('values', dict()) - for key, value in pycompat.items(doc_data): + for key, value in doc_data.items(): values.setdefault(key, value) if values.get('channel_id'): custom_channels = self.env['slide.channel'].search([('custom_slide_id', '=', self.id), ('id', '!=', values.get('channel_id'))]) @@ -498,13 +499,12 @@ class Slide(models.Model): try: response = requests.get(base_url, params=data) response.raise_for_status() - content = response.content if content_type == 'json': - result['values'] = json.loads(content) + result['values'] = response.json() elif content_type in ('image', 'pdf'): - result['values'] = content.encode('base64') + result['values'] = base64.b64encode(response.content) else: - result['values'] = content + result['values'] = response.content except requests.exceptions.HTTPError as e: result['error'] = e.response.content except requests.exceptions.ConnectionError as e: @@ -565,7 +565,7 @@ class Slide(models.Model): # TDE FIXME: WTF ?? slide_type = 'presentation' if vals.get('image'): - image = Image.open(io.BytesIO(vals['image'].decode('base64'))) + image = Image.open(io.BytesIO(base64.b64decode(vals['image']))) width, height = image.size if height > width: return 'document' diff --git a/doc/_extensions/github_link.py b/doc/_extensions/github_link.py index d3e5d61c28e..155e4083025 100644 --- a/doc/_extensions/github_link.py +++ b/doc/_extensions/github_link.py @@ -4,6 +4,8 @@ import os.path from werkzeug import urls +from odoo.tools import pycompat + """ * adds github_link(mode) context variable: provides URL (in relevant mode) of current document on github @@ -99,7 +101,7 @@ def add_doc_link(app, pagename, templatename, context, doctree): # in Sphinx 1.3 it's possible to have mutliple source suffixes and that # may be useful in the future source_suffix = app.config.source_suffix - source_suffix = source_suffix if isinstance(source_suffix, basestring) else source_suffix[0] + source_suffix = source_suffix if isinstance(source_suffix, pycompat.string_types) else source_suffix[0] # can't use functools.partial because 3rd positional is line not mode context['github_link'] = lambda mode='edit': make_github_link( app, 'doc/%s%s' % (pagename, source_suffix), mode=mode) diff --git a/doc/_extensions/odoo_ext/translator.py b/doc/_extensions/odoo_ext/translator.py index 3a1928364eb..443458f33c7 100644 --- a/doc/_extensions/odoo_ext/translator.py +++ b/doc/_extensions/odoo_ext/translator.py @@ -11,6 +11,8 @@ from docutils import nodes from sphinx import addnodes, util from sphinx.locale import admonitionlabels +from odoo.tools import pycompat + def _parents(node): while node.parent: @@ -62,7 +64,7 @@ class BootstrapTranslator(nodes.NodeVisitor, object): self.param_separator = ',' def encode(self, text): - return unicode(text).translate({ + return pycompat.text_type(text).translate({ ord('&'): u'&', ord('<'): u'<', ord('"'): u'"', @@ -71,7 +73,7 @@ class BootstrapTranslator(nodes.NodeVisitor, object): }) def starttag(self, node, tagname, **attributes): - tagname = unicode(tagname).lower() + tagname = pycompat.text_type(tagname).lower() # extract generic attributes attrs = {name.lower(): value for name, value in attributes.items()} @@ -105,8 +107,8 @@ class BootstrapTranslator(nodes.NodeVisitor, object): ) # only "space characters" SPACE, CHARACTER TABULATION, LINE FEED, # FORM FEED and CARRIAGE RETURN should be collapsed, not al White_Space - def attval(self, value, whitespace=re.compile(u'[ \t\n\f\r]')): - return self.encode(whitespace.sub(u' ', unicode(value))) + def attval(self, value, whitespace=re.compile(u'[ \t\n\f\r]+')): + return self.encode(whitespace.sub(u' ', pycompat.text_type(value))) def astext(self): return u''.join(self.body) diff --git a/doc/python3.rst b/doc/python3.rst index 3773d77c4e7..c8e8269936e 100644 --- a/doc/python3.rst +++ b/doc/python3.rst @@ -61,6 +61,11 @@ features whereas: generator delegation, pathlib, ...), you must *not* use them in Odoo until Python 2 support is dropped +.. note:: + + In the *very rare* cases where you *need* to differentiate between + Python 2 and Python 3, use the :data:`odoo.tools.pycompat.PY2` flag. + Semantics changes ================= @@ -228,10 +233,7 @@ methods or functions having been *removed entirely*: .. important:: When possible, use comprehensions (list, generator, ...) rather than - ``map`` or ``filter``, otherwise use the cross-version ``pycompat`` - versions (``pycompat.imap``, ``pycompat.ifilter`` and - ``pycompat.izip``). The ``pycompat`` versions all return *iterators* and - may need to be wrapped in a ``list()`` call to yield a list. + ``map`` or ``filter``. * In Python 3, ``dict.keys``, ``dict.values`` and ``dict.items`` return *views* rather than lists, and the ``iter*`` and ``view*`` methods have @@ -239,11 +241,9 @@ methods or functions having been *removed entirely*: .. important:: - Prefer using :func:`odoo.tools.pycompat.keys`, - :func:`odoo.tools.pycompat.values` and :func:`odoo.tools.pycompat.items` - return cross-version iterators. When needing actual lists (e.g. to - modify a dictionary during iteration), wrap one of the calls above in a - ``list()``. + When the result of the above methods is used for more than a one-shot + loop (e.g. to be included in returned value), or when the dict needs + to be modified during iteration, wrap the calls in a ``list()``. builtins -------- @@ -369,8 +369,121 @@ Minor syntax changes error, octal literals now follow the hexadecimal convention with a ``0o`` prefix. +.. _changed-strings: + +Bytes/String/Text: The Big One +============================== + +The most impactful Python 3 change by far is to the text model: for historical +reasons the distinction Python 2's bytestrings (``bytes``/``str``) and text +strings (``unicode``) is fuzzy and it will try to implicitly convert between +one and the other using the ASCII encoding. + +Python 3 changes this, it removes the implicit conversions, removes APIs which +contribute to the fuzz and tends to strictly segregate other to work on either +bytes or text. + +This is fundamentally good and mostly sensible, but it means lots of breakage: + +the builtins +------------ + +Python 3 removes both ``unicode`` and ``basestring``, and ``str`` now +corresponds to *text* strings (the old ``unicode``) with ``bytes`` being +bytestrings in both languages [#bytes]_. + +Both versions have the following prefixes for string literals: + +* ``b'foo'`` is a bytestring (``bytes`` object). + +* ``'foo'`` is that version's ``str`` type, which may be either a bytestring + or a text string [#native-string]_. + +* ``u'foo'`` is that version's text string. + +For best cross-version compatibility you should avoid unprefixed string +literals unless you *specifically* need a "native string" [#native-string]_. + +For easier type-testing, :mod:`odoo.tools.pycompat` provides the following +constants: + +* :data:`~odoo.tools.pycompat.string_types` is an alias/type tuple for testing + string types, essentially a replacement of testing for ``basestring`` or + ``(str, unicode)``. +* :data:`~odoo.tools.pycompat.text_type` is the proper *text* type for the + current version, it should mostly be used for converting non-bytes objects + to text. +* ``bytes`` should be avoided for type conversions, though it can be used to + check if an object is a bytestring. + +``open`` +-------- + +.. important:: + + the ``open`` builtin should always be explicitly used in binary mode + (``rb``, ``wb``, ...) + + To read *text* files, use ``io.open``. + +On both P2 and P3, ``open`` defaults to returning *native strings* in default +("text") mode, however in P3 that means it actually decodes the file's bytes +using whatever encoding was set up (default: UTF-8) while on Python 2 it has +no concept of encoding. + +Using ``open`` in binary mode provides bytestrings on both versions and works +fine. To read *text* files, use ``io.open`` and provide an explicit encoding. + +base64 +------ + +base64 is a bytes->bytes conversion. bytes->bytes codecs were removed from the +"native" encoding/decoding system which is now exclusively for bytes<->text +conversions: text is *encoded* to bytes and bytes are *decoded* to text. + +.. important:: + + both ``bytes.encode('base64')`` and ``bytes.decode('base64')`` must be + migrated to using ``base64.b64encode`` and ``base64.b64decode`` + respectively. + +csv +--- + +``csv`` is a fairly vicious one: not only is it not a very good format, the +Python 2 and Python 3 versions of the library are text-model incompatible in +significant ways: + +* Python 2's CSV only works on *ascii-compatible byte streams* (it has no + encoding support at all) and extracts bytestring values +* Python 3's CSV only works on *text streams* and extract text values +* And ``io`` doesn't provide "native string" streaming facilities. + +However with respect to Odoo it turns out most or all uses of ``csv`` fit +inside a model of *byte stream to and from text values*. + +The latter is thus a model implemented by cross-version wrappers +:func:`odoo.tools.pycompat.csv_reader` and +:func:`odoo.tools.pycompat.csv_writer`: they take a *UTF-8 byte stream* and +read or write *text* values. + .. _hash randomisation: http://bugs.python.org/issue13703 .. _requests: http://docs.python-requests.org/ .. _werkzeug: http://werkzeug.pocoo.org/docs/urls/ + +.. [#bytes] + + with the caveat that Python 3 makes them less text-y and more byte-y e.g. + in Python 2 ``b"foo"[0]`` is ``b"f"``, but in Python 3 it's ``102`` (the + value of the first byte), you'll want to *slice* bytestrings for + compatibility. + +.. [#native-string] + + this is important because some API/contexts take a *native string* rather + than either bytes or text. The ``csv`` module of the standard library is + one such problematic API (it is also notoriously problematic for its + terrible support of non-ascii-compatible encodings in Python 2). + ``email.message_from_string`` is an other one. diff --git a/doc/reference/guidelines.rst b/doc/reference/guidelines.rst index 058fe7ffb1c..fc15c10f7ab 100644 --- a/doc/reference/guidelines.rst +++ b/doc/reference/guidelines.rst @@ -452,12 +452,9 @@ So, you can write ``if some_collection:`` instead of ``if len(some_collection):` # better for key in my_dict: "do something..." - # creates a temporary list + # accessing the key,value pair for key, value in my_dict.items(): "do something..." - # only iterates - for key, value in my_dict.iteritems(): - "do something..." - Use dict.setdefault diff --git a/odoo/addons/base/ir/ir_actions_report.py b/odoo/addons/base/ir/ir_actions_report.py index 27cfafe1326..3f81bd13e95 100644 --- a/odoo/addons/base/ir/ir_actions_report.py +++ b/odoo/addons/base/ir/ir_actions_report.py @@ -10,6 +10,7 @@ from odoo.http import request import time import base64 +import io import logging import os import lxml.html @@ -56,9 +57,9 @@ except (OSError, IOError): else: _logger.info('Will use the Wkhtmltopdf binary at %s' % _get_wkhtmltopdf_bin()) out, err = process.communicate() - match = re.search('([0-9.]+)', out) + match = re.search(b'([0-9.]+)', out) if match: - version = match.group(0) + version = match.group(0).decode('ascii') if LooseVersion(version) < LooseVersion('0.12.0'): _logger.info('Upgrade Wkhtmltopdf to (at least) 0.12.0') wkhtmltopdf_state = 'upgrade' @@ -464,20 +465,20 @@ class IrActionsReport(models.Model): if wkhtmltopdf_obj.header: head_file_fd, head_file_path = tempfile.mkstemp(suffix='.html', prefix='report.header.tmp.') temporary_files.append(head_file_path) - with closing(os.fdopen(head_file_fd, 'w')) as head_file: + with closing(os.fdopen(head_file_fd, 'wb')) as head_file: head_file.write(wkhtmltopdf_obj.header) local_command_args.extend(['--header-html', head_file_path]) if wkhtmltopdf_obj.footer: foot_file_fd, foot_file_path = tempfile.mkstemp(suffix='.html', prefix='report.footer.tmp.') temporary_files.append(foot_file_path) - with closing(os.fdopen(foot_file_fd, 'w')) as foot_file: + with closing(os.fdopen(foot_file_fd, 'wb')) as foot_file: foot_file.write(wkhtmltopdf_obj.footer) local_command_args.extend(['--footer-html', foot_file_path]) # Body stuff content_file_fd, content_file_path = tempfile.mkstemp(suffix='.html', prefix='report.body.tmp.') temporary_files.append(content_file_path) - with closing(os.fdopen(content_file_fd, 'w')) as content_file: + with closing(os.fdopen(content_file_fd, 'wb')) as content_file: content_file.write(wkhtmltopdf_obj.content) try: diff --git a/odoo/addons/base/ir/ir_attachment.py b/odoo/addons/base/ir/ir_attachment.py index 9d19d076438..41b85913ed9 100644 --- a/odoo/addons/base/ir/ir_attachment.py +++ b/odoo/addons/base/ir/ir_attachment.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - +import base64 import hashlib import itertools import logging @@ -11,7 +11,7 @@ from collections import defaultdict from odoo import api, fields, models, tools, SUPERUSER_ID, _ from odoo.exceptions import AccessError -from odoo.tools import config, human_size, ustr, html_escape, pycompat +from odoo.tools import config, human_size, ustr, html_escape from odoo.tools.mimetypes import guess_mimetype _logger = logging.getLogger(__name__) @@ -97,14 +97,14 @@ class IrAttachment(models.Model): if bin_size: r = human_size(os.path.getsize(full_path)) else: - r = open(full_path,'rb').read().encode('base64') + r = base64.b64encode(open(full_path,'rb').read()) except (IOError, OSError): _logger.info("_read_file reading %s", full_path, exc_info=True) return r @api.model def _file_write(self, value, checksum): - bin_value = value.decode('base64') + bin_value = base64.b64decode(value) fname, full_path = self._get_path(bin_value, checksum) if not os.path.exists(full_path): try: @@ -167,7 +167,7 @@ class IrAttachment(models.Model): # remove garbage files, and clean up checklist removed = 0 - for fname, filepath in pycompat.items(checklist): + for fname, filepath in checklist.items(): if fname not in whitelist: try: os.unlink(self._full_path(fname)) @@ -195,7 +195,7 @@ class IrAttachment(models.Model): for attach in self: # compute the fields that depend on datas value = attach.datas - bin_data = value and value.decode('base64') or '' + bin_data = base64.b64decode(value) if value else b'' vals = { 'file_size': len(bin_data), 'checksum': self._compute_checksum(bin_data), @@ -220,7 +220,7 @@ class IrAttachment(models.Model): :param bin_data : datas in its binary form """ # an empty file has a checksum too (for caching) - return hashlib.sha1(bin_data or '').hexdigest() + return hashlib.sha1(bin_data or b'').hexdigest() def _compute_mimetype(self, values): """ compute the mimetype of the given values @@ -235,7 +235,7 @@ class IrAttachment(models.Model): if not mimetype and values.get('url'): mimetype = mimetypes.guess_type(values['url'])[0] if values.get('datas') and (not mimetype or mimetype == 'application/octet-stream'): - mimetype = guess_mimetype(values['datas'].decode('base64')) + mimetype = guess_mimetype(base64.b64decode(values['datas'])) return mimetype or 'application/octet-stream' def _check_contents(self, values): @@ -258,8 +258,8 @@ class IrAttachment(models.Model): if file_type: index_content = file_type.split('/')[0] if index_content == 'text': # compute index_content only for text type - words = re.findall("[^\x00-\x1F\x7F-\xFF]{4,}", bin_data) - index_content = ustr("\n".join(words)) + words = re.findall(b"[\x20-\x7E]{4,}", bin_data) + index_content = b"\n".join(words).decode('ascii') return index_content name = fields.Char('Attachment Name', required=True) @@ -318,7 +318,7 @@ class IrAttachment(models.Model): model_ids[values['res_model']].add(values['res_id']) # check access rights on the records - for res_model, res_ids in pycompat.items(model_ids): + for res_model, res_ids in model_ids.items(): # ignore attachments that are not attached to a resource anymore # when checking access rights (resource was deleted but attachment # was not) @@ -374,12 +374,12 @@ class IrAttachment(models.Model): # To avoid multiple queries for each attachment found, checks are # performed in batch as much as possible. - for res_model, targets in pycompat.items(model_attachments): + for res_model, targets in model_attachments.items(): if res_model not in self.env: continue if not self.env[res_model].check_access_rights('read', False): # remove all corresponding attachment ids - ids.difference_update(itertools.chain(*pycompat.values(targets))) + ids.difference_update(itertools.chain(*targets.values())) continue # filter ids according to what access rules permit target_ids = list(targets) diff --git a/odoo/addons/base/ir/ir_config_parameter.py b/odoo/addons/base/ir/ir_config_parameter.py index 8e4ea20b5a5..a286a8836ce 100644 --- a/odoo/addons/base/ir/ir_config_parameter.py +++ b/odoo/addons/base/ir/ir_config_parameter.py @@ -16,8 +16,8 @@ _logger = logging.getLogger(__name__) A dictionary holding some configuration parameters to be initialized when the database is created. """ _default_parameters = { - "database.secret": lambda: str(uuid.uuid4()), - "database.uuid": lambda: str(uuid.uuid1()), + "database.secret": lambda: pycompat.text_type(uuid.uuid4()), + "database.uuid": lambda: pycompat.text_type(uuid.uuid1()), "database.create_date": fields.Datetime.now, "web.base.url": lambda: "http://localhost:%s" % config.get('xmlrpc_port'), } @@ -42,7 +42,7 @@ class IrConfigParameter(models.Model): Initializes the parameters listed in _default_parameters. It overrides existing parameters if force is ``True``. """ - for key, func in pycompat.items(_default_parameters): + for key, func in _default_parameters.items(): # force=True skips search and always performs the 'if' body (because ids=False) params = self.sudo().search([('key', '=', key)]) if force or not params: diff --git a/odoo/addons/base/ir/ir_fields.py b/odoo/addons/base/ir/ir_fields.py index 204d4599425..d7521ed6ba1 100644 --- a/odoo/addons/base/ir/ir_fields.py +++ b/odoo/addons/base/ir/ir_fields.py @@ -12,9 +12,9 @@ from odoo.tools import ustr, pycompat REFERENCING_FIELDS = {None, 'id', '.id'} def only_ref_fields(record): - return {k: v for k, v in pycompat.items(record) if k in REFERENCING_FIELDS} + return {k: v for k, v in record.items() if k in REFERENCING_FIELDS} def exclude_ref_fields(record): - return {k: v for k, v in pycompat.items(record) if k not in REFERENCING_FIELDS} + return {k: v for k, v in record.items() if k not in REFERENCING_FIELDS} CREATE = lambda values: (0, False, values) UPDATE = lambda id, values: (1, id, values) @@ -38,12 +38,12 @@ class IrFieldsConverter(models.AbstractModel): @api.model def _format_import_error(self, error_type, error_msg, error_params=(), error_args=None): # sanitize error params for later formatting by the import system - sanitize = lambda p: p.replace('%', '%%') if isinstance(p, basestring) else p + sanitize = lambda p: p.replace('%', '%%') if isinstance(p, pycompat.string_types) else p if error_params: - if isinstance(error_params, basestring): + if isinstance(error_params, pycompat.string_types): error_params = sanitize(error_params) elif isinstance(error_params, dict): - error_params = {k: sanitize(v) for k, v in pycompat.items(error_params)} + error_params = {k: sanitize(v) for k, v in error_params.items()} elif isinstance(error_params, tuple): error_params = tuple(sanitize(v) for v in error_params) return error_type(error_msg % error_params, error_args) @@ -64,12 +64,12 @@ class IrFieldsConverter(models.AbstractModel): converters = { name: self.to_field(model, field, fromtype) - for name, field in pycompat.items(model._fields) + for name, field in model._fields.items() } def fn(record, log): converted = {} - for field, value in pycompat.items(record): + for field, value in record.items(): if field in REFERENCING_FIELDS: continue if not value: @@ -78,7 +78,7 @@ class IrFieldsConverter(models.AbstractModel): try: converted[field], ws = converters[field](value) for w in ws: - if isinstance(w, basestring): + if isinstance(w, pycompat.string_types): # wrap warning string in an ImportWarning for # uniform handling w = ImportWarning(w) @@ -269,14 +269,14 @@ class IrFieldsConverter(models.AbstractModel): for item, label in selection: label = ustr(label) labels = [label] + self._get_translations(('selection', 'model', 'code'), label) - if value == unicode(item) or value in labels: + if value == pycompat.text_type(item) or value in labels: return item, [] raise self._format_import_error( ValueError, _(u"Value '%s' not found in selection field '%%(field)s'"), value, - {'moreinfo': [_label or unicode(item) for item, _label in selection if _label or item]} + {'moreinfo': [_label or pycompat.text_type(item) for item, _label in selection if _label or item]} ) @api.model diff --git a/odoo/addons/base/ir/ir_http.py b/odoo/addons/base/ir/ir_http.py index ff4654cf6c9..af9fde0b012 100644 --- a/odoo/addons/base/ir/ir_http.py +++ b/odoo/addons/base/ir/ir_http.py @@ -125,7 +125,7 @@ class IrHttp(models.AbstractModel): attach = env['ir.attachment'].search_read(domain, fields) if attach: wdate = attach[0]['__last_update'] - datas = attach[0]['datas'] or '' + datas = attach[0]['datas'] or b'' name = attach[0]['name'] checksum = attach[0]['checksum'] or hashlib.sha1(datas).hexdigest() @@ -148,7 +148,7 @@ class IrHttp(models.AbstractModel): return response response.mimetype = attach[0]['mimetype'] or 'application/octet-stream' - response.data = datas.decode('base64') + response.data = base64.b64decode(datas) return response @classmethod @@ -203,7 +203,7 @@ class IrHttp(models.AbstractModel): @classmethod def _postprocess_args(cls, arguments, rule): """ post process arg to set uid on browse records """ - for name, arg in list(pycompat.items(arguments)): + for name, arg in list(arguments.items()): if isinstance(arg, models.BaseModel) and arg._uid is UID_PLACEHOLDER: arguments[name] = arg.sudo(request.uid) if not arg.exists(): @@ -288,7 +288,7 @@ class IrHttp(models.AbstractModel): if module_resource_path.startswith(module_path): with open(module_resource_path, 'rb') as f: content = base64.b64encode(f.read()) - last_update = str(os.path.getmtime(module_resource_path)) + last_update = pycompat.text_type(os.path.getmtime(module_resource_path)) if not module_resource_path: module_resource_path = obj.url @@ -323,8 +323,8 @@ 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') - retag = '"%s"' % hashlib.md5(last_update).hexdigest() + etag = bool(request) and request.httprequest.headers.get('If-None-Match') + retag = '"%s"' % hashlib.md5(last_update.encode('utf-8')).hexdigest() status = status or (304 if etag == retag else 200) headers.append(('ETag', retag)) headers.append(('Cache-Control', 'max-age=%s' % (STATIC_CACHE if unique else 0))) diff --git a/odoo/addons/base/ir/ir_mail_server.py b/odoo/addons/base/ir/ir_mail_server.py index 91cf0e61e73..0ecb895e082 100644 --- a/odoo/addons/base/ir/ir_mail_server.py +++ b/odoo/addons/base/ir/ir_mail_server.py @@ -39,20 +39,8 @@ class WriteToLogger(object): _logger.debug(s) smtplib.stderr = WriteToLogger() -def try_coerce_ascii(string_utf8): - """Attempts to decode the given utf8-encoded string - as ASCII after coercing it to UTF-8, then return - the confirmed 7-bit ASCII string. - - If the process fails (because the string - contains non-ASCII characters) returns ``None``. - """ - try: - string_utf8.decode('ascii') - except UnicodeDecodeError: - return - return string_utf8 - +def is_ascii(s): + return all(ord(cp) < 128 for cp in s) def encode_header(header_text): """Returns an appropriate representation of the given header value, @@ -70,16 +58,10 @@ def encode_header(header_text): """ if not header_text: return "" - # convert anything to utf-8, suitable for testing ASCIIness, as 7-bit chars are - # encoded as ASCII in utf-8 - header_text_utf8 = ustr(header_text).encode('utf-8') - header_text_ascii = try_coerce_ascii(header_text_utf8) - # if this header contains non-ASCII characters, - # we'll need to wrap it up in a message.header.Header - # that will take care of RFC2047-encoding it as - # 7-bit string. - return header_text_ascii or Header(header_text_utf8, 'utf-8') - + header_text = ustr(header_text) # FIXME: require unicode higher up? + if is_ascii(header_text): + return pycompat.to_native(header_text) + Header(header_text, 'utf-8') def encode_header_param(param_text): """Returns an appropriate RFC2047 encoded representation of the given @@ -97,10 +79,10 @@ def encode_header_param(param_text): # For details see the encode_header() method that uses the same logic if not param_text: return "" - param_text_utf8 = ustr(param_text).encode('utf-8') - param_text_ascii = try_coerce_ascii(param_text_utf8) - return param_text_ascii or Charset('utf8').header_encode(param_text_utf8) - + param_text = ustr(param_text) # FIXME: require unicode higher up? + if is_ascii(param_text): + return pycompat.to_native(param_text) # TODO: is that actually necessary? + return Charset("utf-8").header_encode(param_text) address_pattern = re.compile(r'([^ ,<@]+@[^> ,]+)') @@ -111,8 +93,8 @@ def extract_rfc2822_addresses(text): """ if not text: return [] - candidates = address_pattern.findall(ustr(text).encode('utf-8')) - return [c for c in candidates if try_coerce_ascii(c)] + candidates = address_pattern.findall(ustr(text)) + return [c for c in candidates if is_ascii(c)] def encode_rfc2822_address_header(header_text): @@ -124,11 +106,20 @@ def encode_rfc2822_address_header(header_text): """ def encode_addr(addr): name, email = addr - if not try_coerce_ascii(name): - name = str(Header(name, 'utf-8')) + # If s is a , then charset is a hint specifying the + # character set of the characters in the string. The Unicode string + # will be encoded using the following charsets in order: us-ascii, + # the charset hint, utf-8. The first character set to not provoke a + # UnicodeError is used. + # -> always pass a text string to Header + + # also Header.__str__ in Python 3 "Returns an approximation of the + # Header as a string, using an unlimited line length.", the old one + # was "A synonym for Header.encode()." so call encode() directly? + name = Header(pycompat.to_text(name)).encode() return formataddr((name, email)) - addresses = getaddresses([ustr(header_text).encode('utf-8')]) + addresses = getaddresses([pycompat.to_native(ustr(header_text))]) return COMMASPACE.join(encode_addr(a) for a in addresses) @@ -252,8 +243,8 @@ class IrMailServer(models.Model): # The user/password must be converted to bytestrings in order to be usable for # certain hashing schemes, like HMAC. # See also bug #597143 and python issue #5285 - smtp_user = ustr(smtp_user).encode('utf-8') - smtp_password = ustr(smtp_password).encode('utf-8') + smtp_user = pycompat.to_native(ustr(smtp_user)) + smtp_password = pycompat.to_native(ustr(smtp_password)) connection.login(smtp_user, smtp_password) return connection @@ -301,8 +292,8 @@ class IrMailServer(models.Model): email_bcc = email_bcc or [] body = body or u'' - email_body_utf8 = ustr(body).encode('utf-8') - email_text_part = MIMEText(email_body_utf8, _subtype=subtype, _charset='utf-8') + email_body = ustr(body) + email_text_part = MIMEText(email_body, _subtype=subtype, _charset='utf-8') msg = MIMEMultipart() if not message_id: @@ -327,21 +318,21 @@ class IrMailServer(models.Model): msg['Bcc'] = encode_rfc2822_address_header(COMMASPACE.join(email_bcc)) msg['Date'] = formatdate() # Custom headers may override normal headers or provide additional ones - for key, value in pycompat.items(headers): - msg[ustr(key).encode('utf-8')] = encode_header(value) + for key, value in headers.items(): + msg[pycompat.to_native(ustr(key))] = encode_header(value) if subtype == 'html' and not body_alternative: # Always provide alternative text body ourselves if possible. - text_utf8 = html2text.html2text(email_body_utf8.decode('utf-8')).encode('utf-8') + text = html2text.html2text(email_body_utf8) alternative_part = MIMEMultipart(_subtype="alternative") - alternative_part.attach(MIMEText(text_utf8, _charset='utf-8', _subtype='plain')) + alternative_part.attach(MIMEText(text, _charset='utf-8', _subtype='plain')) alternative_part.attach(email_text_part) msg.attach(alternative_part) elif body_alternative: # Include both alternatives, as specified, within a multipart/alternative part alternative_part = MIMEMultipart(_subtype="alternative") - body_alternative_utf8 = ustr(body_alternative).encode('utf-8') - alternative_body_part = MIMEText(body_alternative_utf8, _subtype=subtype_alternative, _charset='utf-8') + body_alternative_ = ustr(body_alternative) + alternative_body_part = MIMEText(body_alternative_, _subtype=subtype_alternative, _charset='utf-8') alternative_part.attach(alternative_body_part) alternative_part.attach(email_text_part) msg.attach(alternative_part) diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py index 4fa66406108..25967c6b212 100644 --- a/odoo/addons/base/ir/ir_model.py +++ b/odoo/addons/base/ir/ir_model.py @@ -18,11 +18,6 @@ _logger = logging.getLogger(__name__) MODULE_UNINSTALL_FLAG = '_force_unlink' -def encode(s): - """ Return an UTF8-encoded version of ``s``. """ - return s.encode('utf8') if isinstance(s, unicode) else s - - # base environment for doing a safe_eval SAFE_EVAL_BASE = { 'datetime': datetime, @@ -199,7 +194,7 @@ class IrModel(models.Model): @api.multi def write(self, vals): if '__last_update' in self._context: - self = self.with_context({k: v for k, v in pycompat.items(self._context) if k != '__last_update'}) + self = self.with_context({k: v for k, v in self._context.items() if k != '__last_update'}) if 'model' in vals and any(rec.model != vals['model'] for rec in self): raise UserError(_('Field "Model" cannot be modified on models.')) if 'state' in vals and any(rec.state != vals['state'] for rec in self): @@ -272,7 +267,7 @@ class IrModel(models.Model): def _instanciate(self, model_data): """ Return a class for the custom model given by parameters ``model_data``. """ class CustomModel(models.Model): - _name = encode(model_data['model']) + _name = pycompat.to_native(model_data['model']) _description = model_data['name'] _module = False _custom = True @@ -771,7 +766,7 @@ class IrModelFields(models.Model): fields_data[field.name] = dict(params, id=record.id) return record - diff = {key for key, val in pycompat.items(params) if field_data[key] != val} + diff = {key for key, val in params.items() if field_data[key] != val} if diff: cr = self.env.cr # update the entry in this table @@ -789,7 +784,7 @@ class IrModelFields(models.Model): def _reflect_model(self, model): """ Reflect the given model's fields. """ self.clear_caches() - for field in pycompat.values(model._fields): + for field in model._fields.values(): self._reflect_field(field) if not self.pool._init: @@ -872,7 +867,7 @@ class IrModelFields(models.Model): def _add_manual_fields(self, model): """ Add extra fields on model. """ fields_data = self._get_manual_field_data(model._name) - for name, field_data in pycompat.items(fields_data): + for name, field_data in fields_data.items(): if name not in model._fields and field_data['state'] == 'manual': field = self._instanciate(field_data) if field: @@ -1134,7 +1129,7 @@ class IrModelAccess(models.Model): # User root have all accesses return True - assert isinstance(model, basestring), 'Not a model name: %s' % (model,) + assert isinstance(model, pycompat.string_types), 'Not a model name: %s' % (model,) assert mode in ('read', 'write', 'create', 'unlink'), 'Invalid access mode' # TransientModel records have no access rights, only an implicit access rule @@ -1283,7 +1278,7 @@ class IrModelData(models.Model): model_id_name[xid.model][xid.res_id] = None # fill in model_id_name with name_get() of corresponding records - for model, id_name in pycompat.items(model_id_name): + for model, id_name in model_id_name.items(): try: ng = self.env[model].browse(id_name).name_get() id_name.update(ng) @@ -1380,7 +1375,7 @@ class IrModelData(models.Model): record = self.get_object(module, xml_id) if record: self.loads[(module, xml_id)] = (model, record.id) - for parent_model, parent_field in pycompat.items(self.env[model]._inherits): + for parent_model, parent_field in self.env[model]._inherits.items(): parent = record[parent_field] parent_xid = '%s_%s' % (xml_id, parent_model.replace('.', '_')) self.loads[(module, parent_xid)] = (parent_model, parent.id) @@ -1435,7 +1430,7 @@ class IrModelData(models.Model): elif record: record.write(values) if xml_id: - for parent_model, parent_field in pycompat.items(record._inherits): + for parent_model, parent_field in record._inherits.items(): self.sudo().create({ 'name': xml_id + '_' + parent_model.replace('.', '_'), 'model': parent_model, @@ -1454,7 +1449,7 @@ class IrModelData(models.Model): elif mode == 'init' or (mode == 'update' and xml_id): existing_parents = set() # {parent_model, ...} if xml_id: - for parent_model, parent_field in pycompat.items(record._inherits): + for parent_model, parent_field in record._inherits.items(): xid = self.sudo().search([ ('module', '=', module), ('name', '=', xml_id + '_' + parent_model.replace('.', '_')), @@ -1474,7 +1469,7 @@ class IrModelData(models.Model): inherit_models = [record] while inherit_models: current_model = inherit_models.pop() - for parent_model_name, parent_field in pycompat.items(current_model._inherits): + for parent_model_name, parent_field in current_model._inherits.items(): inherit_models.append(self.env[parent_model_name]) if parent_model_name in existing_parents: continue @@ -1500,7 +1495,7 @@ class IrModelData(models.Model): if xml_id and record: self.loads[(module, xml_id)] = (model, record.id) - for parent_model, parent_field in pycompat.items(record._inherits): + for parent_model, parent_field in record._inherits.items(): parent_xml_id = xml_id + '_' + parent_model.replace('.', '_') self.loads[(module, parent_xml_id)] = (parent_model, record[parent_field].id) diff --git a/odoo/addons/base/ir/ir_qweb/__init__.py b/odoo/addons/base/ir/ir_qweb/__init__.py index b7fb78857b1..b0add281fdb 100644 --- a/odoo/addons/base/ir/ir_qweb/__init__.py +++ b/odoo/addons/base/ir/ir_qweb/__init__.py @@ -5,5 +5,5 @@ from . import fields from . import assetsbundle from .assetsbundle import AssetsBundle -from .qweb import unicodifier, QWebException +from .qweb import QWebException from .ir_qweb import IrQWeb, QWeb diff --git a/odoo/addons/base/ir/ir_qweb/assetsbundle.py b/odoo/addons/base/ir/ir_qweb/assetsbundle.py index ac7c04fba38..fbf0d42f8b5 100644 --- a/odoo/addons/base/ir/ir_qweb/assetsbundle.py +++ b/odoo/addons/base/ir/ir_qweb/assetsbundle.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import base64 import os import re import hashlib @@ -12,7 +13,7 @@ from odoo import fields, tools from odoo.http import request from odoo.modules.module import get_resource_path import psycopg2 -from odoo.tools import func, misc, pycompat +from odoo.tools import func, misc import logging _logger = logging.getLogger(__name__) @@ -101,7 +102,7 @@ class AssetsBundle(object): def to_html(self, sep=None, css=True, js=True, debug=False, async=False, url_for=(lambda url: url)): if sep is None: - sep = '\n ' + sep = u'\n ' response = [] if debug == 'assets': if css and self.stylesheets: @@ -120,12 +121,12 @@ class AssetsBundle(object): if css and self.stylesheets: css_attachments = self.css() or [] for attachment in css_attachments: - response.append('' % url_for(attachment.url)) + response.append(u'' % url_for(attachment.url)) if self.css_errors: msg = '\n'.join(self.css_errors) response.append(JavascriptAsset(self, inline=self.dialog_message(msg)).to_html()) if js and self.javascripts: - response.append('' % (async and 'async="async"' or '', url_for(self.js().url))) + response.append(u'' % (async and u'async="async"' or '', url_for(self.js().url))) response.extend(self.remains) return sep + sep.join(response) @@ -148,8 +149,8 @@ class AssetsBundle(object): Not really a full checksum. We compute a SHA1 on the rendered bundle + max linked files last_modified date """ - check = json.dumps(self.files) + ",".join(self.remains) + str(self.last_modified) - return hashlib.sha1(check).hexdigest() + check = u"%s%s%s" % (json.dumps(self.files), u",".join(self.remains), self.last_modified) + return hashlib.sha1(check.encode('utf-8')).hexdigest() def clean_attachments(self, type): """ Takes care of deleting any outdated ir.attachment records associated to a bundle before @@ -207,7 +208,7 @@ class AssetsBundle(object): 'res_id': False, 'type': 'binary', 'public': True, - 'datas': content.encode('utf8').encode('base64'), + 'datas': base64.b64encode(content.encode('utf8')), } attachment = ira.sudo().create(values) @@ -315,11 +316,11 @@ class AssetsBundle(object): outdated = True break if asset._content is None: - asset._content = attachment.datas and attachment.datas.decode('base64').decode('utf8') or '' + asset._content = attachment.datas and base64.b64decode(attachment.datas).decode('utf8') or '' if not asset._content and attachment.file_size > 0: asset._content = None # file missing, force recompile - if any(asset._content is None for asset in pycompat.values(assets)): + if any(asset._content is None for asset in assets.values()): outdated = True if outdated: @@ -357,7 +358,7 @@ class AssetsBundle(object): url = asset.html_url with self.env.cr.savepoint(): self.env['ir.attachment'].sudo().create(dict( - datas=asset.content.encode('utf8').encode('base64'), + datas=base64.b64encode(asset.content.encode('utf8')), mimetype='text/css', type='binary', name=url, @@ -501,7 +502,7 @@ class WebAsset(object): with open(self._filename, 'rb') as fp: return fp.read().decode('utf-8') else: - return self._ir_attach['datas'].decode('base64').decode('utf-8') + return base64.b64decode(self._ir_attach['datas']).decode('utf-8') except UnicodeDecodeError: raise AssetError('%s is not utf-8 encoded.' % self.name) except IOError: diff --git a/odoo/addons/base/ir/ir_qweb/fields.py b/odoo/addons/base/ir/ir_qweb/fields.py index 6b9b0536b1a..27d13d3076c 100644 --- a/odoo/addons/base/ir/ir_qweb/fields.py +++ b/odoo/addons/base/ir/ir_qweb/fields.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- +import base64 import re from collections import OrderedDict from io import BytesIO from odoo import api, fields, models, _ from PIL import Image import babel -from odoo.tools import html_escape as escape, posix_to_ldml, safe_eval, float_utils, format_date -from .qweb import unicodifier +from odoo.tools import html_escape as escape, posix_to_ldml, safe_eval, float_utils, format_date, pycompat import logging _logger = logging.getLogger(__name__) @@ -19,7 +19,7 @@ def nl2br(string): :param str string: :rtype: unicode """ - return unicodifier(string).replace(u'\n', u'
\n') + return pycompat.to_text(string).replace(u'\n', u'
\n') def html_escape(string, options): """ Automatically escapes content unless options['html-escape'] @@ -88,7 +88,7 @@ class FieldConverter(models.AbstractModel): Converts a single value to its HTML version/output :rtype: unicode """ - return html_escape(unicodifier(value) or u'', options) + return html_escape(pycompat.to_text(value), options) @api.model def record_to_html(self, record, field_name, options): @@ -123,7 +123,7 @@ class IntegerConverter(models.AbstractModel): @api.model def value_to_html(self, value, options): - return unicodifier(self.user_lang().format('%d', value, grouping=True).replace(r'-', u'\u2011')) + return pycompat.to_text(self.user_lang().format('%d', value, grouping=True).replace(r'-', u'\u2011')) class FloatConverter(models.AbstractModel): @@ -152,7 +152,7 @@ class FloatConverter(models.AbstractModel): if precision is None: formatted = re.sub(r'(?:(0|\d+?)0+)$', r'\1', formatted) - return unicodifier(formatted) + return pycompat.to_text(formatted) @api.model def record_to_html(self, record, field_name, options): @@ -182,7 +182,7 @@ class DateTimeConverter(models.AbstractModel): lang = self.user_lang() locale = babel.Locale.parse(lang.code) - if isinstance(value, basestring): + if isinstance(value, pycompat.string_types): value = fields.Datetime.from_string(value) value = fields.Datetime.context_timestamp(self, value) @@ -200,7 +200,7 @@ class DateTimeConverter(models.AbstractModel): if options and options.get('hide_seconds'): pattern = pattern.replace(":ss", "").replace(":s", "") - return unicodifier(babel.dates.format_datetime(value, format=pattern, locale=locale)) + return pycompat.to_text(babel.dates.format_datetime(value, format=pattern, locale=locale)) class TextConverter(models.AbstractModel): @@ -223,7 +223,7 @@ class SelectionConverter(models.AbstractModel): def value_to_html(self, value, options): if not value: return '' - return html_escape(unicodifier(options['selection'][value]) or u'', options) + return html_escape(pycompat.to_text(options['selection'][value]) or u'', options) @api.model def record_to_html(self, record, field_name, options): @@ -252,7 +252,7 @@ class HTMLConverter(models.AbstractModel): @api.model def value_to_html(self, value, options): - return unicodifier(value) or u'' + return pycompat.to_text(value) class ImageConverter(models.AbstractModel): @@ -269,15 +269,15 @@ class ImageConverter(models.AbstractModel): @api.model def value_to_html(self, value, options): - try: - image = Image.open(BytesIO(value.decode('base64'))) + try: # FIXME: maaaaaybe it could also take raw bytes? + image = Image.open(BytesIO(base64.b64decode(value))) image.verify() except IOError: raise ValueError("Non-image binary fields can not be converted to HTML") except: # image.verify() throws "suitable exceptions", I have no idea what they are raise ValueError("Invalid image content") - return unicodifier('' % (Image.MIME[image.format], value)) + return u'' % (Image.MIME[image.format], value.decode('ascii')) class MonetaryConverter(models.AbstractModel): @@ -398,13 +398,13 @@ class RelativeDatetimeConverter(models.AbstractModel): def value_to_html(self, value, options): locale = babel.Locale.parse(self.user_lang().code) - if isinstance(value, basestring): + if isinstance(value, pycompat.string_types): value = fields.Datetime.from_string(value) # value should be a naive datetime in UTC. So is fields.Datetime.now() reference = fields.Datetime.from_string(options['now']) - return unicodifier(babel.dates.format_timedelta(value - reference, add_direction=True, locale=locale)) + return pycompat.to_text(babel.dates.format_timedelta(value - reference, add_direction=True, locale=locale)) @api.model def record_to_html(self, record, field_name, options): @@ -427,8 +427,8 @@ class BarcodeConverter(models.AbstractModel): barcode = self.env['ir.actions.report'].barcode( barcode_type, value, - **dict((key, value) for key, value in options.items() if key in ['width', 'height', 'humanreadable'])) - return unicodifier('' % ('png', barcode.encode('base64'))) + **{key: value for key, value in options.items() if key in ['width', 'height', 'humanreadable']}) + return u'' % base64.b64encode(barcode).decode('ascii') @api.model def from_html(self, model, field, element): @@ -481,4 +481,4 @@ class QwebView(models.AbstractModel): view = view.with_context(object=record) - return unicodifier(view.render(view._context, engine='ir.qweb')) + return pycompat.to_text(view.render(view._context, engine='ir.qweb')) diff --git a/odoo/addons/base/ir/ir_qweb/ir_qweb.py b/odoo/addons/base/ir/ir_qweb/ir_qweb.py index 102b36e0898..bd40a2ce76c 100644 --- a/odoo/addons/base/ir/ir_qweb/ir_qweb.py +++ b/odoo/addons/base/ir/ir_qweb/ir_qweb.py @@ -169,7 +169,7 @@ class IrQWeb(models.AbstractModel, QWeb): if field_options and 'monetary' in field_options: try: options = "{'widget': 'monetary'" - for k, v in pycompat.items(json.loads(field_options)): + for k, v in json.loads(field_options).items(): if k in ('display_currency', 'from_currency'): options = "%s, '%s': %s" % (options, k, v) else: @@ -218,8 +218,8 @@ class IrQWeb(models.AbstractModel, QWeb): files = [] remains = [] for el in html.fragments_fromstring(template): - if isinstance(el, basestring): - remains.append(el) + if isinstance(el, pycompat.string_types): + remains.append(pycompat.to_text(el)) elif isinstance(el, html.HtmlElement): href = el.get('href', '') src = el.get('src', '') @@ -243,10 +243,10 @@ class IrQWeb(models.AbstractModel, QWeb): filename = get_resource_path(*path) if path else None files.append({'atype': atype, 'url': src, 'filename': filename, 'content': el.text, 'media': media}) else: - remains.append(html.tostring(el)) + remains.append(html.tostring(el, encoding='unicode')) else: try: - remains.append(html.tostring(el)) + remains.append(html.tostring(el, encoding='unicode')) except Exception: # notYETimplementederror raise NotImplementedError diff --git a/odoo/addons/base/ir/ir_qweb/qweb.py b/odoo/addons/base/ir/ir_qweb/qweb.py index 69a01635f2d..43c10fe19f0 100644 --- a/odoo/addons/base/ir/ir_qweb/qweb.py +++ b/odoo/addons/base/ir/ir_qweb/qweb.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import ast import logging +import os.path import re import traceback @@ -176,13 +177,6 @@ class QWebException(Exception): # Avoid DeprecationWarning while still remaining compatible with werkzeug pre-0.9 escape = (lambda text: _escape(text, quote=True)) if getattr(werkzeug, '__version__', '0.0') < '0.9.0' else _escape -def unicodifier(val): - if val is None or val is False: - return u'' - if isinstance(val, str): - return val.decode('utf-8') - return unicode(val) - def foreach_iterator(base_ctx, enum, name): ctx = base_ctx.copy() if not enum: @@ -193,7 +187,7 @@ def foreach_iterator(base_ctx, enum, name): if isinstance(enum, Sized): ctx["%s_size" % name] = size = len(enum) if isinstance(enum, Mapping): - enum = pycompat.items(enum) + enum = enum.items() else: enum = pycompat.izip(*tee(enum)) value_key = '%s_value' % name @@ -246,7 +240,7 @@ class frozendict(dict): def update(self, *args, **kwargs): raise NotImplementedError("'update' not supported on frozendict") def __hash__(self): - return hash(frozenset((key, freehash(val)) for key, val in pycompat.items(self))) + return hash(frozenset((key, freehash(val)) for key, val in self.items())) #################################### @@ -380,10 +374,11 @@ class QWeb(object): if isinstance(document, etree._Element): element = document document = etree.tostring(document) - elif document.startswith("'))] + content else: @@ -789,10 +785,10 @@ class QWeb(object): # Etree will also remove the ns prefixes indirection in the attributes. As we only have # the namespace definition, we'll use an nsmap where the keys are the definitions and # the values the prefixes in order to get back the right prefix and restore it. - nsprefixmap = {v: k for k, v in itertools.chain(pycompat.items(options['nsmap']), pycompat.items(el.nsmap))} + nsprefixmap = {v: k for k, v in itertools.chain(options['nsmap'].items(), el.nsmap.items())} nodes = [] - for key, value in pycompat.items(el.attrib): + for key, value in el.attrib.items(): if not key.startswith('t-'): attrib_qname = etree.QName(key) if attrib_qname.namespace: @@ -807,7 +803,7 @@ class QWeb(object): We do not support namespaced dynamic attributes. """ nodes = [] - for name, value in pycompat.items(el.attrib): + for name, value in el.attrib.items(): if name.startswith('t-attf-'): nodes.append((name[7:], self._compile_format(value))) elif name.startswith('t-att-'): @@ -833,7 +829,7 @@ class QWeb(object): def _compile_all_attributes(self, el, options, attr_already_created=False): """ Compile the attributes of the given elements into a list of AST nodes. """ body = [] - if any(name.startswith('t-att') or not name.startswith('t-') for name, value in pycompat.items(el.attrib)): + if any(name.startswith('t-att') or not name.startswith('t-') for name, value in el.attrib.items()): if not attr_already_created: attr_already_created = True body.append( @@ -874,22 +870,22 @@ class QWeb(object): ))) if attr_already_created: - # for name, value in pycompat.items(t_attrs): + # for name, value in t_attrs.items(): # if value or isinstance(value, basestring)): # append(u' ') # append(name) # append(u'="') - # append(escape(unicodifier((value))) + # append(escape(to_text((value))) # append(u'"') body.append(ast.For( target=ast.Tuple(elts=[ast.Name(id='name', ctx=ast.Store()), ast.Name(id='value', ctx=ast.Store())], ctx=ast.Store()), iter=ast.Call( func=ast.Attribute( - value=ast.Name(id='pycompat', ctx=ast.Load()), + value=ast.Name(id='t_attrs', ctx=ast.Load()), attr='items', ctx=ast.Load() ), - args=[ast.Name(id='t_attrs', ctx=ast.Load())], keywords=[], + args=[], keywords=[], starargs=None, kwargs=None ), body=[ast.If( @@ -901,7 +897,7 @@ class QWeb(object): func=ast.Name(id='isinstance', ctx=ast.Load()), args=[ ast.Name(id='value', ctx=ast.Load()), - ast.Name(id='basestring', ctx=ast.Load()) + ast.Name(id='string_types', ctx=ast.Load()) ], keywords=[], starargs=None, kwargs=None @@ -915,7 +911,7 @@ class QWeb(object): self._append(ast.Call( func=ast.Name(id='escape', ctx=ast.Load()), args=[ast.Call( - func=ast.Name(id='unicodifier', ctx=ast.Load()), + func=ast.Name(id='to_text', ctx=ast.Load()), args=[ast.Name(id='value', ctx=ast.Load())], keywords=[], starargs=None, kwargs=None )], keywords=[], @@ -946,7 +942,7 @@ class QWeb(object): # If `el` introduced new namespaces, write them as attribute by using the # `extra_attrib` dict. - for ns_prefix, ns_definition in set(pycompat.items(el.nsmap)) - set(pycompat.items(options['nsmap'])): + for ns_prefix, ns_definition in set(el.nsmap.items()) - set(options['nsmap'].items()): if ns_prefix is None: extra_attrib['xmlns'] = ns_definition else: @@ -955,7 +951,7 @@ class QWeb(object): if unqualified_el_tag == 't': return content - body = [self._append(ast.Str(u'<%s%s' % (el_tag, u''.join([u' %s="%s"' % (name, escape(unicodifier(value))) for name, value in pycompat.items(extra_attrib)]))))] + body = [self._append(ast.Str(u'<%s%s' % (el_tag, u''.join([u' %s="%s"' % (name, escape(pycompat.to_text(value))) for name, value in extra_attrib.items()]))))] body.extend(self._compile_all_attributes(el, options, attr_already_created)) if unqualified_el_tag in self._void_elements: body.append(self._append(ast.Str(u'/>'))) @@ -1044,7 +1040,7 @@ class QWeb(object): def _compile_directive_content(self, el, options): body = [] if el.text is not None: - body.append(self._append(ast.Str(unicodifier(el.text)))) + body.append(self._append(ast.Str(pycompat.to_text(el.text)))) if el.getchildren(): for item in el: # ignore comments & processing instructions @@ -1135,7 +1131,7 @@ class QWeb(object): )] def _compile_tail(self, el): - return el.tail is not None and [self._append(ast.Str(unicodifier(el.tail)))] or [] + return el.tail is not None and [self._append(ast.Str(pycompat.to_text(el.tail)))] or [] def _compile_directive_esc(self, el, options): field_options = self._compile_widget_options(el, 'esc') @@ -1148,14 +1144,14 @@ class QWeb(object): return content + self._compile_widget_value(el, options) def _compile_widget(self, el, expression, field_options, escape=None): - # if isinstance(value, basestring): - # value = escape(unicodifier(value)) + # if isinstance(value, string_types): + # value = escape(to_text(value)) escaped = ast.If( test=ast.Call( func=ast.Name(id='isinstance', ctx=ast.Load()), args=[ ast.Name(id='content', ctx=ast.Load()), - ast.Name(id='basestring', ctx=ast.Load()) + ast.Name(id='string_types', ctx=ast.Load()) ], keywords=[], starargs=None, kwargs=None ), @@ -1164,7 +1160,7 @@ class QWeb(object): value=ast.Call( func=ast.Name(id=escape, ctx=ast.Load()), args=[ast.Call( - func=ast.Name(id='unicodifier', ctx=ast.Load()), + func=ast.Name(id='to_text', ctx=ast.Load()), args=[ast.Name(id='content', ctx=ast.Load())], keywords=[], starargs=None, kwargs=None )], keywords=[], @@ -1332,7 +1328,7 @@ class QWeb(object): ] # if content is not None: - # display the tag (unicodifier(content)) + # display the tag (to_text(content)) # else # if default_content: # display the tag with default content @@ -1341,7 +1337,7 @@ class QWeb(object): return [self._if_content_is_not_Falsy( body=self._compile_tag(el, [self._append( ast.Call( - func=ast.Name(id='unicodifier', ctx=ast.Load()), + func=ast.Name(id='to_text', ctx=ast.Load()), args=[ast.Name(id='content', ctx=ast.Load())], keywords=[], starargs=None, kwargs=None ) @@ -1460,8 +1456,8 @@ class QWeb(object): # make the nsmap an ast dict keys = [] values = [] - for key, value in pycompat.items(options['nsmap']): - if isinstance(key, basestring): + for key, value in options['nsmap'].items(): + if isinstance(key, pycompat.string_types): keys.append(ast.Str(s=key)) elif key is None: keys.append(ast.Name(id='None', ctx=ast.Load())) @@ -1547,7 +1543,7 @@ class QWeb(object): def _compile_strexpr(self, expr): # ensure result is unicode return ast.Call( - func=ast.Name(id='unicodifier', ctx=ast.Load()), + func=ast.Name(id='to_text', ctx=ast.Load()), args=[self._compile_expr(expr)], keywords=[], starargs=None, kwargs=None ) @@ -1591,7 +1587,7 @@ class QWeb(object): for m in _FORMAT_REGEX.finditer(f): literal = f[base_idx:m.start()] if literal: - elts.append(ast.Str(literal if isinstance(literal, unicode) else literal.decode('utf-8'))) + elts.append(ast.Str(literal if isinstance(literal, pycompat.text_type) else literal.decode('utf-8'))) expr = m.group(1) or m.group(2) elts.append(self._compile_strexpr(expr)) @@ -1599,7 +1595,7 @@ class QWeb(object): # string past last regex match literal = f[base_idx:] if literal: - elts.append(ast.Str(literal if isinstance(literal, unicode) else literal.decode('utf-8'))) + elts.append(ast.Str(literal if isinstance(literal, pycompat.text_type) else literal.decode('utf-8'))) return reduce(lambda acc, it: ast.BinOp( left=acc, diff --git a/odoo/addons/base/ir/ir_sequence.py b/odoo/addons/base/ir/ir_sequence.py index aa893e3f316..105f0f9bbf4 100644 --- a/odoo/addons/base/ir/ir_sequence.py +++ b/odoo/addons/base/ir/ir_sequence.py @@ -6,7 +6,6 @@ import pytz from odoo import api, fields, models, _ from odoo.exceptions import UserError -from odoo.tools import pycompat _logger = logging.getLogger(__name__) @@ -195,7 +194,7 @@ class IrSequence(models.Model): 'weekday': '%w', 'h24': '%H', 'h12': '%I', 'min': '%M', 'sec': '%S' } res = {} - for key, format in pycompat.items(sequences): + for key, format in sequences.items(): res[key] = effective_date.strftime(format) res['range_' + key] = range_date.strftime(format) res['current_' + key] = now.strftime(format) diff --git a/odoo/addons/base/ir/ir_translation.py b/odoo/addons/base/ir/ir_translation.py index cd954778a92..f77d84e758b 100644 --- a/odoo/addons/base/ir/ir_translation.py +++ b/odoo/addons/base/ir/ir_translation.py @@ -128,7 +128,7 @@ class IrTranslationImport(object): env = api.Environment(cr, SUPERUSER_ID, {}) src_relevant_fields = [] for model in env: - for field_name, field in pycompat.items(env[model]._fields): + for field_name, field in env[model]._fields.items(): if hasattr(field, 'translate') and callable(field.translate): src_relevant_fields.append("%s,%s" % (model, field_name)) @@ -394,7 +394,7 @@ class IrTranslation(models.Model): # always pass unicode so we can remove the string encoding/decoding. if not lang: return tools.ustr(source or '') - if isinstance(types, basestring): + if isinstance(types, pycompat.string_types): types = (types,) if res_id: if isinstance(res_id, pycompat.integer_types): @@ -531,7 +531,7 @@ class IrTranslation(models.Model): # check for read/write access on translated field records fmode = 'read' if mode == 'read' else 'write' - for mname, ids in pycompat.items(model_ids): + for mname, ids in model_ids.items(): records = self.env[mname].browse(ids) records.check_access_rights(fmode) records.check_field_access_rights(fmode, model_fields[mname]) @@ -646,7 +646,7 @@ class IrTranslation(models.Model): return ['&', ('res_id', '=', rec.id), ('name', '=', name)] # insert missing translations, and extend domain for related fields - for name, fld in pycompat.items(record._fields): + for name, fld in record._fields.items(): if not fld.translate: continue diff --git a/odoo/addons/base/ir/ir_ui_view.py b/odoo/addons/base/ir/ir_ui_view.py index d70ee4cf951..085200a7475 100644 --- a/odoo/addons/base/ir/ir_ui_view.py +++ b/odoo/addons/base/ir/ir_ui_view.py @@ -29,7 +29,7 @@ from odoo.tools.convert import _fix_multiple_roots from odoo.tools.parse_version import parse_version from odoo.tools.safe_eval import safe_eval from odoo.tools.view_validation import valid_view -from odoo.tools.translate import encode, xml_translate, TRANSLATED_ATTRS +from odoo.tools.translate import xml_translate, TRANSLATED_ATTRS _logger = logging.getLogger(__name__) @@ -110,7 +110,7 @@ def get_view_arch_from_file(filename, xmlid): if node.tag == 'record': field = node.find('field[@name="arch"]') _fix_multiple_roots(field) - inner = ''.join([etree.tostring(child) for child in field.iterchildren()]) + inner = u''.join([etree.tostring(child, encoding='unicode') for child in field.iterchildren()]) return field.text + inner elif node.tag == 'template': # The following dom operations has been copied from convert.py's _tag_template() @@ -120,7 +120,7 @@ def get_view_arch_from_file(filename, xmlid): else: node.tag = 'data' node.attrib.pop('id', None) - return etree.tostring(node) + return etree.tostring(node, encoding='unicode') _logger.warning("Could not find view arch definition in file '%s' for xmlid '%s'", filename, xmlid) return None @@ -233,7 +233,7 @@ actual arch. else: _logger.warning("View %s: Full path [%s] cannot be found.", view.xml_id, view.arch_fs) arch_fs = False - view.arch = arch_fs or view.arch_db + view.arch = pycompat.to_text(arch_fs or view.arch_db) def _inverse_arch(self): for view in self: @@ -269,7 +269,7 @@ actual arch. view.model_data_id = data['id'] def _search_model_data_id(self, operator, value): - name = 'name' if isinstance(value, basestring) else 'id' + name = 'name' if isinstance(value, pycompat.string_types) else 'id' domain = [('model', '=', 'ir.ui.view'), (name, operator, value)] data = self.env['ir.model.data'].sudo().search(domain) return [('id', 'in', data.mapped('res_id'))] @@ -313,7 +313,7 @@ actual arch. # Sanity checks: the view should not break anything upon rendering! # Any exception raised below will cause a transaction rollback. for view in self: - view_arch = etree.fromstring(encode(view.arch)) + view_arch = etree.fromstring(view.arch.encode('utf-8')) view._valid_inheritance(view_arch) view_def = view.read_combined(['arch']) view_arch_utf8 = view_def['arch'] @@ -702,7 +702,7 @@ actual arch. # and apply inheritance arch = self.apply_view_inheritance(arch_tree, root.id, self.model) - return dict(view_data, arch=etree.tostring(arch, encoding='utf-8')) + return dict(view_data, arch=etree.tostring(arch, encoding='unicode')) def _apply_group(self, model, node, modifiers, fields): """Apply group restrictions, may be set at view level or model level:: @@ -845,7 +845,7 @@ actual arch. collect(arch, self.env[model_name]) - for field, nodes in pycompat.items(field_nodes): + for field, nodes in field_nodes.items(): # if field should trigger an onchange, add on_change="1" on the # nodes referring to field model = self.env[field.model_name] @@ -910,7 +910,7 @@ actual arch. not self._context.get(action, True) and is_base_model): node.set(action, 'false') - arch = etree.tostring(node, encoding="utf-8").replace('\t', '') + arch = etree.tostring(node, encoding="unicode").replace('\t', '') for k in list(fields): if k not in fields_def: del fields[k] @@ -942,7 +942,7 @@ actual arch. arch_tree = etree.fromstring(arch) self.distribute_branding(arch_tree) root = E.templates(arch_tree) - arch = etree.tostring(root, encoding='utf-8', xml_declaration=True) + arch = etree.tostring(root, encoding='unicode') return arch @api.model @@ -1102,12 +1102,12 @@ actual arch. Model = self.env[model] Node = self.env[node_obj] - for model_key, model_value in pycompat.items(Model._fields): + for model_key, model_value in Model._fields.items(): if model_value.type == 'one2many': if model_value.comodel_name == node_obj: _Node_Field = model_key _Model_Field = model_value.inverse_name - for node_key, node_value in pycompat.items(Node._fields): + for node_key, node_value in Node._fields.items(): if node_value.type == 'one2many': if node_value.comodel_name == conn_obj: # _Source_Field = "Incoming Arrows" (connected via des_node) @@ -1184,7 +1184,7 @@ actual arch. xmlid_filter = "AND md.name IN %s" names = tuple( name - for (xmod, name), (model, res_id) in pycompat.items(self.pool.model_data_reference_ids) + for (xmod, name), (model, res_id) in self.pool.model_data_reference_ids.items() if xmod == module and model == self._name ) if not names: diff --git a/odoo/addons/base/ir/ir_values.py b/odoo/addons/base/ir/ir_values.py index ace2998b2b3..851c4705738 100644 --- a/odoo/addons/base/ir/ir_values.py +++ b/odoo/addons/base/ir/ir_values.py @@ -96,8 +96,8 @@ class IrValues(models.Model): "helper field for binding an action, will " "automatically set the correct reference") - value = fields.Text(help="Default value (pickled) or reference to an action") - value_unpickle = fields.Text(string='Default value or action reference', + value = fields.Binary(help="Default value (pickled) or reference to an action") + value_unpickle = fields.Binary(string='Default value or action reference', compute='_value_unpickle', inverse='_value_pickle') key = fields.Selection([('action', 'Action'), ('default', 'Default')], string='Type', index=True, required=True, default='action', @@ -211,8 +211,8 @@ class IrValues(models.Model): match) :return: the newly created ir.values entry """ - if isinstance(value, unicode): - value = value.encode('utf8') + if isinstance(value, pycompat.string_types): + value = pycompat.to_native(value) if company_id is True: # should be company-specific, need to get company id company_id = self.env.user.company_id.id @@ -261,7 +261,7 @@ class IrValues(models.Model): ('company_id', '=', company_id) ] defaults = self.search(search_criteria) - return pickle.loads(defaults.value.encode('utf-8')) if defaults else None + return pickle.loads(defaults.value) if defaults else None @api.model def get_defaults(self, model, condition=False): @@ -313,9 +313,9 @@ class IrValues(models.Model): # keep only the highest priority default for each field defaults = {} for row in self._cr.dictfetchall(): - value = pickle.loads(row['value'].encode('utf-8')) + value = pickle.loads(row['value']) defaults.setdefault(row['name'], (row['id'], row['name'], value)) - return list(pycompat.values(defaults)) + return list(defaults.values()) # use ormcache: this is called a lot by BaseModel.default_get()! @api.model @@ -347,11 +347,13 @@ class IrValues(models.Model): specific record of the model, not all records. :return: the newly created ir.values entry """ - assert isinstance(action, basestring) and ',' in action, \ + assert isinstance(action, pycompat.string_types) and ',' in action, \ 'Action definition must be an action reference, e.g. "ir.actions.act_window,42"' assert action_slot in ACTION_SLOTS, \ 'Action slot (%s) must be one of: %r' % (action_slot, ACTION_SLOTS) + action = action.encode('utf-8') + # remove existing action definition of same slot and value search_criteria = [ ('key', '=', 'action'), @@ -405,7 +407,7 @@ class IrValues(models.Model): for id, name, value in self._cr.fetchall(): if not value: continue # skip if undefined - action_model, action_id = value.split(',') + action_model, action_id = bytes(value).decode('utf-8').split(',') if action_model not in self.env: continue # unknown model? skip it! action = self.env[action_model].browse(int(action_id)) @@ -426,4 +428,4 @@ class IrValues(models.Model): results[name] = (id, name, action_def) except (AccessError, MissingError): continue - return sorted(pycompat.values(results)) + return sorted(results.values()) diff --git a/odoo/addons/base/module/module.py b/odoo/addons/base/module/module.py index 32c875c3fd8..cc77b392a18 100644 --- a/odoo/addons/base/module/module.py +++ b/odoo/addons/base/module/module.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import base64 from collections import defaultdict from operator import attrgetter import importlib @@ -220,7 +221,7 @@ class Module(models.Model): path = modules.module.get_module_icon(module.name) if path: with tools.file_open(path, 'rb') as image_file: - module.icon_image = image_file.read().encode('base64') + module.icon_image = base64.b64encode(image_file.read()) name = fields.Char('Technical Name', readonly=True, required=True, index=True) category_id = fields.Many2one('ir.module.category', string='Category', readonly=True, index=True) @@ -644,7 +645,7 @@ class Module(models.Model): updated_values = {} for key in values: old = getattr(mod, key) - updated = tools.ustr(values[key]) if isinstance(values[key], basestring) else values[key] + updated = tools.ustr(values[key]) if isinstance(values[key], pycompat.string_types) else values[key] if (old or updated) and updated != old: updated_values[key] = values[key] if terp.get('installable', True) and mod.state == 'uninstallable': @@ -693,7 +694,7 @@ class Module(models.Model): _logger.debug('Install from url: %r', urls) try: # 1. Download & unzip missing modules - for module_name, url in pycompat.items(urls): + for module_name, url in urls.items(): if not url: continue # nothing to download, local version is already the last one @@ -714,7 +715,7 @@ class Module(models.Model): assert os.path.isdir(os.path.join(tmp, module_name)) # 2a. Copy/Replace module source in addons path - for module_name, url in pycompat.items(urls): + for module_name, url in urls.items(): if module_name == OPENERP or not url: continue # OPENERP is special case, handled below, and no URL means local module module_path = modules.get_module_path(module_name, downloaded=True, display_warning=False) @@ -746,7 +747,7 @@ class Module(models.Model): self.update_list() - with_urls = [module_name for module_name, url in pycompat.items(urls) if url] + with_urls = [module_name for module_name, url in urls.items() if url] downloaded = self.search([('name', 'in', with_urls)]) installed = self.search([('id', 'in', downloaded.ids), ('state', '=', 'installed')]) diff --git a/odoo/addons/base/module/report/ir_module_reference_print.py b/odoo/addons/base/module/report/ir_module_reference_print.py index a933cdd9430..03632644159 100644 --- a/odoo/addons/base/module/report/ir_module_reference_print.py +++ b/odoo/addons/base/module/report/ir_module_reference_print.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, models -from odoo.tools import pycompat class IrModelReferenceReport(models.AbstractModel): @@ -23,7 +22,7 @@ class IrModelReferenceReport(models.AbstractModel): if data: res_ids = data.mapped('res_id') fnames = self.env['ir.model.fields'].browse(res_ids).mapped('name') - return sorted(pycompat.items(self.env[model].fields_get(fnames))) + return sorted(self.env[model].fields_get(fnames).items()) return [] @api.model diff --git a/odoo/addons/base/res/ir_property.py b/odoo/addons/base/res/ir_property.py index ec086fb6c61..73f8aafccad 100644 --- a/odoo/addons/base/res/ir_property.py +++ b/odoo/addons/base/res/ir_property.py @@ -217,7 +217,7 @@ class Property(models.Model): prop.write({'value': value}) # create new properties for records that do not have one yet - for ref, id in pycompat.items(refs): + for ref, id in refs.items(): value = clean(values[id]) if value != default_value: self.create({ diff --git a/odoo/addons/base/res/res_bank.py b/odoo/addons/base/res/res_bank.py index e1f9bb85f00..5a397fcc831 100644 --- a/odoo/addons/base/res/res_bank.py +++ b/odoo/addons/base/res/res_bank.py @@ -2,8 +2,11 @@ import re +import collections + from odoo import api, fields, models from odoo.osv import expression +from odoo.tools import pycompat def sanitize_account_number(acc_number): @@ -88,7 +91,7 @@ class ResPartnerBank(models.Model): if args[pos][0] == 'acc_number': op = args[pos][1] value = args[pos][2] - if hasattr(value, '__iter__'): + if not isinstance(value, pycompat.string_types) and isinstance(value, collections.Iterable): value = [sanitize_account_number(i) for i in value] else: value = sanitize_account_number(value) diff --git a/odoo/addons/base/res/res_company.py b/odoo/addons/base/res/res_company.py index 387ba1afe1c..5a711c6b9c0 100644 --- a/odoo/addons/base/res/res_company.py +++ b/odoo/addons/base/res/res_company.py @@ -1,12 +1,11 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - +import base64 import os import re from odoo import api, fields, models, tools, _ from odoo.exceptions import ValidationError -from odoo.tools import pycompat class Company(models.Model): @@ -28,7 +27,7 @@ class Company(models.Model): return super(Company, self).copy(default) def _get_logo(self): - return open(os.path.join(tools.config['root_path'], 'addons', 'base', 'res', 'res_company_logo.png'), 'rb') .read().encode('base64') + return base64.b64encode(open(os.path.join(tools.config['root_path'], 'addons', 'base', 'res', 'res_company_logo.png'), 'rb') .read()) @api.model def _get_euro(self): @@ -154,7 +153,7 @@ class Company(models.Model): if self.country_id: res['domain']['state_id'] = [('country_id', '=', self.country_id.id)] values = self.on_change_country(self.country_id.id)['value'] - for fname, value in pycompat.items(values): + for fname, value in values.items(): setattr(self, fname, value) return res diff --git a/odoo/addons/base/res/res_config.py b/odoo/addons/base/res/res_config.py index d5c1ac59717..4176d57ae52 100644 --- a/odoo/addons/base/res/res_config.py +++ b/odoo/addons/base/res/res_config.py @@ -9,7 +9,7 @@ from lxml import etree from odoo import api, models, registry, SUPERUSER_ID, _ from odoo.exceptions import AccessError, RedirectWarning, UserError -from odoo.tools import ustr, pycompat +from odoo.tools import ustr _logger = logging.getLogger(__name__) @@ -247,7 +247,7 @@ class ResConfigInstaller(models.TransientModel, ResConfigModuleInstallationMixin :returns: a list of all installed modules in this installer :rtype: recordset (collection of Record) """ - selectable = [name for name, field in pycompat.items(self._fields) + selectable = [name for name, field in self._fields.items() if field.type == 'boolean'] return self.env['ir.module.module'].search([('name', 'in', selectable), ('state', 'in', ['to install', 'installed', 'to upgrade'])]) @@ -272,7 +272,7 @@ class ResConfigInstaller(models.TransientModel, ResConfigModuleInstallationMixin """ base = set(module_name for installer in self.read() - for module_name, to_install in pycompat.items(installer) + for module_name, to_install in installer.items() if self._fields[module_name].type == 'boolean' and to_install) hooks_results = set() @@ -282,7 +282,7 @@ class ResConfigInstaller(models.TransientModel, ResConfigModuleInstallationMixin hooks_results.update(hook() or set()) additionals = set(module - for requirements, consequences in pycompat.items(self._install_if) + for requirements, consequences in self._install_if.items() if base.issuperset(requirements) for module in consequences) @@ -405,7 +405,7 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin) node.set("on_change", "onchange_module(%s, '%s')" % (field, field)) - ret_val['arch'] = etree.tostring(doc) + ret_val['arch'] = etree.tostring(doc, encoding='unicode') return ret_val @api.multi @@ -442,7 +442,7 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin) ref = self.env.ref defaults, groups, modules, others = [], [], [], [] - for name, field in pycompat.items(self._fields): + for name, field in self._fields.items(): if name.startswith('default_') and hasattr(field, 'default_model'): defaults.append((name, field.default_model, name[8:])) elif name.startswith('group_') and field.type in ('boolean', 'selection') and \ diff --git a/odoo/addons/base/res/res_lang.py b/odoo/addons/base/res/res_lang.py index f4e2d7a1578..1cdc8a5db43 100644 --- a/odoo/addons/base/res/res_lang.py +++ b/odoo/addons/base/res/res_lang.py @@ -8,7 +8,6 @@ import re from operator import itemgetter from odoo import api, fields, models, tools, _ -from odoo.tools import pycompat from odoo.tools.safe_eval import safe_eval from odoo.exceptions import UserError, ValidationError @@ -126,7 +125,7 @@ class Lang(models.Model): # For some locales, nl_langinfo returns a D_FMT/T_FMT that contains # unsupported '%-' patterns, e.g. for cs_CZ format = format.replace('%-', '%') - for pattern, replacement in pycompat.items(tools.DATETIME_FORMATS_MAP): + for pattern, replacement in tools.DATETIME_FORMATS_MAP.items(): format = format.replace(pattern, replacement) return str(format) diff --git a/odoo/addons/base/res/res_partner.py b/odoo/addons/base/res/res_partner.py index 5b73d6848cd..32772fa19a6 100644 --- a/odoo/addons/base/res/res_partner.py +++ b/odoo/addons/base/res/res_partner.py @@ -61,7 +61,7 @@ class FormatAddressMixin(models.AbstractModel): except ValueError: return arch address_node.getparent().replace(address_node, sub_view_node) - arch = etree.tostring(doc) + arch = etree.tostring(doc, encoding='unicode') return arch class PartnerCategory(models.Model): @@ -337,7 +337,7 @@ class Partner(models.Model): @api.depends('name', 'email') def _compute_email_formatted(self): for partner in self: - partner.email_formatted = formataddr((partner.name, partner.email)) + partner.email_formatted = formataddr((partner.name or u"False", partner.email or u"False")) @api.depends('is_company') def _compute_company_type(self): @@ -665,7 +665,7 @@ class Partner(models.Model): return partners.id or self.name_create(email)[0] def _get_gravatar_image(self, email): - email_hash = hashlib.md5(email.lower()).hexdigest() + email_hash = hashlib.md5(email.lower().encode('utf-8')).hexdigest() url = "https://www.gravatar.com/avatar/" + email_hash try: res = requests.get(url, params={'d': '404', 's': '128'}, timeout=5) diff --git a/odoo/addons/base/res/res_users.py b/odoo/addons/base/res/res_users.py index 2a66c05a4aa..fa17d182fd5 100644 --- a/odoo/addons/base/res/res_users.py +++ b/odoo/addons/base/res/res_users.py @@ -107,7 +107,7 @@ class Groups(models.Model): return expression.AND(domains) else: return expression.OR(domains) - if isinstance(operand, basestring): + if isinstance(operand, pycompat.string_types): lst = False operand = [operand] where = [] @@ -306,7 +306,7 @@ class Users(models.Model): @api.model def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): - groupby_fields = set([groupby] if isinstance(groupby, basestring) else groupby) + groupby_fields = set([groupby] if isinstance(groupby, pycompat.string_types) else groupby) if groupby_fields.intersection(USER_PRIVATE_FIELDS): raise AccessError(_("Invalid 'group by' parameter")) return super(Users, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) @@ -739,7 +739,7 @@ class GroupsView(models.Model): xml2.append({'class': "o_label_nowrap"}) xml = E.field(E.group(*(xml1), col="2"), E.group(*(xml2), col="4"), name="groups_id", position="replace") xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS")) - xml_content = etree.tostring(xml, pretty_print=True, xml_declaration=True, encoding="utf-8") + xml_content = etree.tostring(xml, pretty_print=True, encoding="unicode") view.with_context(lang=None).write({'arch': xml_content, 'arch_fs': False}) def get_application_groups(self, domain): @@ -761,7 +761,7 @@ class GroupsView(models.Model): # determine sequence order: a group appears after its implied groups order = {g: len(g.trans_implied_ids & gs) for g in gs} # check whether order is total, i.e., sequence orders are distinct - if len(set(pycompat.values(order))) == len(gs): + if len(set(order.values())) == len(gs): return (app, 'selection', gs.sorted(key=order.get)) else: return (app, 'boolean', gs) @@ -775,7 +775,7 @@ class GroupsView(models.Model): others += g # build the result res = [] - for app, gs in sorted(pycompat.items(by_app), key=lambda it: it[0].sequence or 0): + for app, gs in sorted(by_app.items(), key=lambda it: it[0].sequence or 0): res.append(linearize(app, gs)) if others: res.append((self.env['ir.module.category'], 'boolean', others)) @@ -815,7 +815,7 @@ class UsersView(models.Model): add, rem = [], [] values1 = {} - for key, val in pycompat.items(values): + for key, val in values.items(): if is_boolean_group(key): (add if val else rem).append(get_boolean_group(key)) elif is_selection_groups(key): diff --git a/odoo/addons/base/static/img/public_user-image.png b/odoo/addons/base/static/img/public_user-image.png index e333176e3e4..e69de29bb2d 100644 Binary files a/odoo/addons/base/static/img/public_user-image.png and b/odoo/addons/base/static/img/public_user-image.png differ diff --git a/odoo/addons/base/tests/test_api.py b/odoo/addons/base/tests/test_api.py index 55cf1ff26b5..96fba1179b7 100644 --- a/odoo/addons/base/tests/test_api.py +++ b/odoo/addons/base/tests/test_api.py @@ -98,7 +98,7 @@ class TestAPI(common.TransactionCase): self.assertIsRecordset(user.groups_id, 'res.groups') partners = self.env['res.partner'].search([]) - for name, field in pycompat.items(partners._fields): + for name, field in partners._fields.items(): if field.type == 'many2one': for p in partners: self.assertIsRecord(p[name], field.comodel_name) @@ -296,7 +296,7 @@ class TestAPI(common.TransactionCase): self.assertItemsEqual(partners.ids, country_id_cache) # partners' countries are ready for prefetching - country_ids = set(cid for cids in pycompat.values(country_id_cache) for cid in cids) + country_ids = set(cid for cids in country_id_cache.values() for cid in cids) self.assertTrue(len(country_ids) > 1) self.assertItemsEqual(country_ids, partners._prefetch['res.country']) @@ -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/addons/base/tests/test_expression.py b/odoo/addons/base/tests/test_expression.py index 07a087d0842..f27c376c9b5 100644 --- a/odoo/addons/base/tests/test_expression.py +++ b/odoo/addons/base/tests/test_expression.py @@ -5,7 +5,7 @@ import psycopg2 from odoo.models import BaseModel from odoo.tests.common import TransactionCase -from odoo.tools import mute_logger, pycompat +from odoo.tools import mute_logger from odoo.osv import expression @@ -85,10 +85,10 @@ class TestExpression(TransactionCase): 'b ab': [cids['B'], cids['AB']], } pids = {} - for name, cat_ids in pycompat.items(partners_config): + for name, cat_ids in partners_config.items(): pids[name] = partners.create({'name': name, 'category_id': [(6, 0, cat_ids)]}).id - base_domain = [('id', 'in', list(pycompat.values(pids)))] + base_domain = [('id', 'in', list(pids.values()))] def test(op, value, expected): found_ids = partners.search(base_domain + [('category_id', op, value)]).ids diff --git a/odoo/addons/base/tests/test_func.py b/odoo/addons/base/tests/test_func.py index ac394cb9b79..66ad02f4e9d 100644 --- a/odoo/addons/base/tests/test_func.py +++ b/odoo/addons/base/tests/test_func.py @@ -4,7 +4,7 @@ import functools import unittest -from odoo.tools import frozendict +from odoo.tools import frozendict, pycompat from odoo.tools.func import compose @@ -16,7 +16,7 @@ class TestCompose(unittest.TestCase): def test_decorator(self): """ ensure compose() can be partially applied as a decorator """ - @functools.partial(compose, unicode) + @functools.partial(compose, pycompat.text_type) def mul(a, b): return a * b diff --git a/odoo/addons/base/tests/test_ir_actions.py b/odoo/addons/base/tests/test_ir_actions.py index cb48752a960..a2663964f59 100644 --- a/odoo/addons/base/tests/test_ir_actions.py +++ b/odoo/addons/base/tests/test_ir_actions.py @@ -64,7 +64,7 @@ class TestServerActions(TestServerActionsBase): # Test: ir_values created ir_values = self.env['ir.values'].search([('name', '=', 'Run TestAction')]) self.assertEqual(len(ir_values), 1, 'ir_actions_server: create_action should have created an entry in ir_values') - self.assertEqual(ir_values.value, 'ir.actions.server,%s' % self.action.id, 'ir_actions_server: created ir_values should reference the server action') + self.assertEqual(ir_values.value, b'ir.actions.server,%d' % self.action.id, 'ir_actions_server: created ir_values should reference the server action') self.assertEqual(ir_values.model, 'res.partner', 'ir_actions_server: created ir_values should be linked to the action base model') # Do: remove contextual action diff --git a/odoo/addons/base/tests/test_ir_attachment.py b/odoo/addons/base/tests/test_ir_attachment.py index 284d8c71da2..8c3769b1178 100644 --- a/odoo/addons/base/tests/test_ir_attachment.py +++ b/odoo/addons/base/tests/test_ir_attachment.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - +import base64 import hashlib import os @@ -16,14 +16,14 @@ class TestIrAttachment(TransactionCase): self.filestore = self.Attachment._filestore() # Blob1 - self.blob1 = 'blob1' - self.blob1_b64 = self.blob1.encode('base64') + self.blob1 = b'blob1' + self.blob1_b64 = base64.b64encode(self.blob1) blob1_hash = hashlib.sha1(self.blob1).hexdigest() self.blob1_fname = blob1_hash[:HASH_SPLIT] + '/' + blob1_hash # Blob2 - self.blob2 = 'blob2' - self.blob2_b64 = self.blob2.encode('base64') + self.blob2 = b'blob2' + self.blob2_b64 = base64.b64encode(self.blob2) def test_01_store_in_db(self): # force storing in database diff --git a/odoo/addons/base/tests/test_ir_http.py b/odoo/addons/base/tests/test_ir_http.py index ef50cdb5a3f..420a16004ce 100644 --- a/odoo/addons/base/tests/test_ir_http.py +++ b/odoo/addons/base/tests/test_ir_http.py @@ -3,7 +3,7 @@ from odoo.tests import common import odoo -GIF = "R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=" +GIF = b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=" class test_ir_http_mimetype(common.TransactionCase): diff --git a/odoo/addons/base/tests/test_mail.py b/odoo/addons/base/tests/test_mail.py index 1a30ef4690b..84cf892ea30 100644 --- a/odoo/addons/base/tests/test_mail.py +++ b/odoo/addons/base/tests/test_mail.py @@ -171,14 +171,14 @@ class TestSanitizer(unittest.TestCase): for ext in test_mail_examples.QUOTE_BLOCKQUOTE_IN: self.assertIn(ext, html) for ext in test_mail_examples.QUOTE_BLOCKQUOTE_OUT: - self.assertIn('%s' % misc.html_escape(ext.decode('utf-8')), html) + self.assertIn(u'%s' % misc.html_escape(ext), html) def test_quote_thunderbird(self): html = html_sanitize(test_mail_examples.QUOTE_THUNDERBIRD_1) for ext in test_mail_examples.QUOTE_THUNDERBIRD_1_IN: self.assertIn(ext, html) for ext in test_mail_examples.QUOTE_THUNDERBIRD_1_OUT: - self.assertIn('%s' % misc.html_escape(ext.decode('utf-8')), html) + self.assertIn(u'%s' % misc.html_escape(ext), html) def test_quote_hotmail_html(self): html = html_sanitize(test_mail_examples.QUOTE_HOTMAIL_HTML) @@ -225,7 +225,7 @@ class TestSanitizer(unittest.TestCase): for text in in_lst: self.assertIn(text, new_html) for text in out_lst: - self.assertIn('%s' % misc.html_escape(text), new_html) + self.assertIn(u'%s' % misc.html_escape(text), new_html) def test_quote_signature(self): test_data = [ @@ -244,27 +244,27 @@ class TestSanitizer(unittest.TestCase): for ext in test_mail_examples.GMAIL_1_IN: self.assertIn(ext, html) for ext in test_mail_examples.GMAIL_1_OUT: - self.assertIn('%s' % misc.html_escape(ext), html) + self.assertIn(u'%s' % misc.html_escape(ext), html) def test_quote_text(self): html = html_sanitize(test_mail_examples.TEXT_1) for ext in test_mail_examples.TEXT_1_IN: self.assertIn(ext, html) for ext in test_mail_examples.TEXT_1_OUT: - self.assertIn('%s' % misc.html_escape(ext), html) + self.assertIn(u'%s' % misc.html_escape(ext), html) html = html_sanitize(test_mail_examples.TEXT_2) for ext in test_mail_examples.TEXT_2_IN: self.assertIn(ext, html) for ext in test_mail_examples.TEXT_2_OUT: - self.assertIn('%s' % misc.html_escape(ext), html) + self.assertIn(u'%s' % misc.html_escape(ext), html) def test_quote_bugs(self): html = html_sanitize(test_mail_examples.BUG1) for ext in test_mail_examples.BUG_1_IN: self.assertIn(ext, html) for ext in test_mail_examples.BUG_1_OUT: - self.assertIn('%s' % misc.html_escape(ext.decode('utf-8')), html) + self.assertIn(u'%s' % misc.html_escape(ext), html) def test_misc(self): # False / void should not crash diff --git a/odoo/addons/base/tests/test_mail_examples.py b/odoo/addons/base/tests/test_mail_examples.py index dfecfe7c0d4..4f3b5b48300 100644 --- a/odoo/addons/base/tests/test_mail_examples.py +++ b/odoo/addons/base/tests/test_mail_examples.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -MISC_HTML_SOURCE = """ +MISC_HTML_SOURCE = u""" test1
test2
@@ -19,7 +19,7 @@ test12

test link
""" -EDI_LIKE_HTML_SOURCE = """
+EDI_LIKE_HTML_SOURCE = u"""

Hello ${object.partner_id.name},

A new invoice is available for you:

@@ -63,7 +63,7 @@ EDI_LIKE_HTML_SOURCE = """

+QUOTE_BLOCKQUOTE = u""" @@ -103,14 +103,14 @@ web: https://opener.am """ -QUOTE_BLOCKQUOTE_IN = ["""
"""] -QUOTE_BLOCKQUOTE_OUT = ["""-- +QUOTE_BLOCKQUOTE_IN = [u"""
"""] +QUOTE_BLOCKQUOTE_OUT = [u"""-- Opener B.V. - Business solutions driven by open source collaboration Stefan Rijnhart - Consultant/developer"""] -QUOTE_THUNDERBIRD_HTML = """ +QUOTE_THUNDERBIRD_HTML = u""" @@ -137,13 +137,13 @@ Raoul Poilvache """ -QUOTE_THUNDERBIRD_HTML_IN = ["""
"""] -QUOTE_THUNDERBIRD_HTML_OUT = ["""
-- 
+QUOTE_THUNDERBIRD_HTML_IN = [u"""
"""] +QUOTE_THUNDERBIRD_HTML_OUT = [u"""
-- 
 Raoul Poilvache
 
"""] -QUOTE_HOTMAIL_HTML = """ +QUOTE_HOTMAIL_HTML = u"""