summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/account/models/account.py2
-rw-r--r--addons/account/models/account_bank_statement.py9
-rw-r--r--addons/account/models/account_config_settings.py6
-rw-r--r--addons/account/models/account_invoice.py22
-rw-r--r--addons/account/models/account_move.py50
-rw-r--r--addons/account/models/chart_template.py4
-rw-r--r--addons/account/models/partner.py6
-rw-r--r--addons/account/report/account_report_financial.py17
-rw-r--r--addons/account/static/src/js/account_payment_field.js2
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_action.js52
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_model.js100
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_renderer.js15
-rw-r--r--addons/account/static/src/less/account_reconciliation.less3
-rw-r--r--addons/account/static/src/xml/account_reconciliation.xml14
-rw-r--r--addons/account/static/tests/reconciliation_tests.js320
-rw-r--r--addons/account/tests/test_reconciliation.py6
-rw-r--r--addons/account/views/account_invoice_view.xml4
-rw-r--r--addons/account/wizard/account_invoice_refund.py2
-rw-r--r--addons/account_asset/models/account_asset.py6
-rw-r--r--addons/account_asset/wizard/asset_modify.py2
-rw-r--r--addons/account_voucher/models/account_voucher.py7
-rw-r--r--addons/anonymization/models/anonymization.py3
-rw-r--r--addons/anonymization/wizard/anonymize_wizard.py2
-rw-r--r--addons/auth_signup/controllers/main.py3
-rw-r--r--addons/auth_signup/models/res_partner.py2
-rw-r--r--addons/base_address_city/models/res_partner.py2
-rw-r--r--addons/base_address_extended/models/base_address_extended.py3
-rw-r--r--addons/base_automation/models/base_automation.py3
-rw-r--r--addons/base_gengo/wizard/base_gengo_translations.py3
-rw-r--r--addons/base_import/models/base_import.py42
-rw-r--r--addons/base_import/models/odf_ods_reader.py4
-rw-r--r--addons/base_import/tests/test_base_import.py76
-rw-r--r--addons/base_import_module/models/ir_module.py9
-rw-r--r--addons/board/controllers/main.py2
-rw-r--r--addons/board/models/board.py12
-rw-r--r--addons/bus/controllers/main.py6
-rw-r--r--addons/calendar/models/calendar.py37
-rw-r--r--addons/calendar/models/ir_attachment.py6
-rw-r--r--addons/calendar/models/mail_message.py4
-rw-r--r--addons/crm/wizard/base_partner_merge.py44
-rw-r--r--addons/crm/wizard/validate_email.py123
-rw-r--r--addons/document/models/ir_attachment.py28
-rw-r--r--addons/event/models/event.py3
-rw-r--r--addons/fleet/models/fleet_vehicle_cost.py4
-rw-r--r--addons/gamification/models/challenge.py6
-rw-r--r--addons/gamification/models/goal.py8
-rw-r--r--addons/google_account/models/google_service.py4
-rw-r--r--addons/google_calendar/models/google_calendar.py20
-rw-r--r--addons/hr/models/hr.py7
-rw-r--r--addons/hr_expense/models/hr_expense.py4
-rw-r--r--addons/hr_payroll/models/hr_payslip.py6
-rw-r--r--addons/hr_payroll/report/report_payslip_details.py10
-rw-r--r--addons/hr_recruitment/models/hr_recruitment.py3
-rw-r--r--addons/hw_escpos/escpos/escpos.py6
-rw-r--r--addons/hw_proxy/controllers/main.py11
-rw-r--r--addons/im_livechat/models/im_livechat_channel.py8
-rw-r--r--addons/im_livechat/models/mail_channel.py4
-rw-r--r--addons/l10n_be_intrastat/wizard/xml_decl.py23
-rw-r--r--addons/l10n_fr_fec/wizard/account_fr_fec.py51
-rw-r--r--addons/l10n_in_hr_payroll/report/report_hr_yearly_salary_detail.py4
-rw-r--r--addons/l10n_lu/scripts/tax2csv.py49
-rw-r--r--addons/link_tracker/models/link_tracker.py12
-rw-r--r--addons/lunch/models/lunch.py10
-rw-r--r--addons/mail/controllers/main.py8
-rw-r--r--addons/mail/models/ir_model.py3
-rw-r--r--addons/mail/models/mail_channel.py8
-rw-r--r--addons/mail/models/mail_followers.py9
-rw-r--r--addons/mail/models/mail_mail.py5
-rw-r--r--addons/mail/models/mail_message.py25
-rw-r--r--addons/mail/models/mail_template.py34
-rw-r--r--addons/mail/models/mail_thread.py50
-rw-r--r--addons/mail/models/res_partner.py3
-rw-r--r--addons/mail/static/src/xml/client_action.xml4
-rw-r--r--addons/mail/static/src/xml/composer.xml4
-rw-r--r--addons/mail/tests/test_mail_features.py9
-rw-r--r--addons/mail/tests/test_mail_message.py4
-rw-r--r--addons/mail/tests/test_mail_template.py4
-rw-r--r--addons/mail/views/mail_message_views.xml4
-rw-r--r--addons/mail/wizard/mail_compose_message.py9
-rw-r--r--addons/marketing_campaign/models/marketing_campaign.py3
-rw-r--r--addons/mass_mailing/controllers/main.py3
-rw-r--r--addons/mass_mailing/models/mass_mailing.py2
-rw-r--r--addons/mass_mailing_event_track/views/event_views.xml1
-rw-r--r--addons/mrp/models/mrp_workcenter.py3
-rw-r--r--addons/mrp/views/ir_attachment_view.xml4
-rw-r--r--addons/mrp/wizard/mrp_product_produce_views.xml2
-rw-r--r--addons/mrp_repair/wizard/mrp_repair_make_invoice.py3
-rw-r--r--addons/pad/models/pad.py10
-rw-r--r--addons/pad/static/tests/pad_tests.js33
-rw-r--r--addons/payment/models/payment_acquirer.py6
-rw-r--r--addons/payment_adyen/models/payment.py8
-rw-r--r--addons/payment_authorize/models/authorize_request.py6
-rw-r--r--addons/payment_authorize/models/payment.py2
-rw-r--r--addons/payment_authorize/tests/test_authorize.py8
-rw-r--r--addons/payment_buckaroo/controllers/main.py3
-rw-r--r--addons/payment_buckaroo/models/payment.py5
-rw-r--r--addons/payment_ogone/models/payment.py7
-rw-r--r--addons/payment_payumoney/models/payment.py2
-rw-r--r--addons/payment_sips/models/payment.py2
-rw-r--r--addons/payment_stripe/models/payment.py13
-rw-r--r--addons/payment_stripe/static/src/js/stripe.js9
-rw-r--r--addons/payment_stripe/static/src/xml/stripe_templates.xml21
-rw-r--r--addons/point_of_sale/models/pos_order.py10
-rw-r--r--addons/point_of_sale/static/src/js/tests.js3
-rw-r--r--addons/portal/controllers/portal.py10
-rw-r--r--addons/portal/views/portal_templates.xml2
-rw-r--r--addons/portal/wizard/portal_wizard_views.xml2
-rw-r--r--addons/pos_restaurant/static/src/js/multiprint.js11
-rw-r--r--addons/pos_restaurant/static/src/xml/multiprint.xml15
-rw-r--r--addons/product/models/product.py2
-rw-r--r--addons/product/models/product_pricelist.py8
-rw-r--r--addons/product/models/product_template.py6
-rw-r--r--addons/product_expiry/models/production_lot.py3
-rw-r--r--addons/product_extended/views/product_views.xml1
-rw-r--r--addons/product_margin/models/product_product.py3
-rw-r--r--addons/project/controllers/portal.py3
-rw-r--r--addons/project/models/project.py2
-rw-r--r--addons/purchase/controllers/portal.py3
-rw-r--r--addons/rating/models/rating.py7
-rw-r--r--addons/rating_project/models/project.py5
-rw-r--r--addons/resource/models/resource_mixin.py6
-rw-r--r--addons/sale/models/account_invoice.py3
-rw-r--r--addons/sale/models/sale.py8
-rw-r--r--addons/sale/models/sale_analytic.py3
-rw-r--r--addons/sale/tests/test_sale_order.py11
-rw-r--r--addons/sale/views/sale_views.xml4
-rw-r--r--addons/sale_stock/models/sale_order.py2
-rw-r--r--addons/sale_stock/tests/test_sale_stock.py5
-rw-r--r--addons/sale_timesheet/controllers/main.py2
-rw-r--r--addons/sale_timesheet/models/account.py25
-rw-r--r--addons/sale_timesheet/tests/test_timesheet_revenue.py10
-rw-r--r--addons/sale_timesheet/views/project_task_views.xml14
-rw-r--r--addons/sales_team/models/crm_team.py2
-rw-r--r--addons/stock/models/procurement.py5
-rw-r--r--addons/stock/models/product.py4
-rw-r--r--addons/stock/models/stock_inventory.py19
-rw-r--r--addons/stock/models/stock_move.py13
-rw-r--r--addons/stock/models/stock_move_line.py5
-rw-r--r--addons/stock/models/stock_picking.py4
-rw-r--r--addons/stock/models/stock_quant.py5
-rw-r--r--addons/stock/models/stock_warehouse.py12
-rw-r--r--addons/stock/views/stock_inventory_views.xml60
-rw-r--r--addons/stock/wizard/make_procurement.py5
-rw-r--r--addons/stock_landed_costs/models/stock_landed_cost.py8
-rw-r--r--addons/survey/controllers/main.py2
-rw-r--r--addons/survey/models/survey.py8
-rw-r--r--addons/survey/tests/test_survey.py10
-rw-r--r--addons/survey/wizard/survey_email_compose_message.py3
-rw-r--r--addons/utm/models/utm.py3
-rw-r--r--addons/web/controllers/main.py52
-rw-r--r--addons/web/models/ir_http.py3
-rw-r--r--addons/web/static/src/js/chrome/search_inputs.js2
-rw-r--r--addons/web/static/src/js/chrome/view_manager.js2
-rw-r--r--addons/web/static/src/js/core/session.js15
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js10
-rw-r--r--addons/web/static/src/js/fields/field_utils.js15
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js53
-rw-r--r--addons/web/static/src/js/fields/special_fields.js2
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js4
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js207
-rw-r--r--addons/web/static/src/js/views/basic/basic_view.js27
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_model.js53
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_renderer.js5
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_controller.js13
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_model.js22
-rw-r--r--addons/web/static/src/js/views/list/list_renderer.js12
-rw-r--r--addons/web/static/src/js/views/view_dialogs.js2
-rw-r--r--addons/web/static/src/js/widgets/data_export.js2
-rw-r--r--addons/web/static/src/js/widgets/debug_manager.js112
-rw-r--r--addons/web/static/src/xml/base.xml4
-rw-r--r--addons/web/static/src/xml/kanban.xml4
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js197
-rw-r--r--addons/web/static/tests/fields/field_utils_tests.js28
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js236
-rw-r--r--addons/web/static/tests/helpers/test_utils.js7
-rw-r--r--addons/web/static/tests/views/basic_model_tests.js78
-rw-r--r--addons/web/static/tests/views/calendar_tests.js271
-rw-r--r--addons/web/static/tests/views/form_tests.js58
-rw-r--r--addons/web/static/tests/views/kanban_tests.js78
-rw-r--r--addons/web/static/tests/views/list_tests.js65
-rw-r--r--addons/web/static/tests/views/view_dialogs_tests.js31
-rw-r--r--addons/web/static/tests/widgets/data_export_tests.js84
-rw-r--r--addons/web_diagram/controllers/main.py7
-rw-r--r--addons/web_editor/controllers/main.py21
-rw-r--r--addons/web_editor/models/ir_qweb.py30
-rw-r--r--addons/web_editor/models/ir_ui_view.py4
-rw-r--r--addons/web_editor/static/src/js/widgets/ace.js10
-rw-r--r--addons/web_editor/tests/test_converter.py2
-rw-r--r--addons/website/controllers/main.py23
-rw-r--r--addons/website/models/ir_qweb.py3
-rw-r--r--addons/website/models/ir_ui_view.py7
-rw-r--r--addons/website/models/website.py14
-rw-r--r--addons/website/tests/test_converter.py4
-rw-r--r--addons/website/tests/test_views.py24
-rw-r--r--addons/website_blog/controllers/main.py4
-rw-r--r--addons/website_blog/models/website_blog.py2
-rw-r--r--addons/website_crm_partner_assign/controllers/main.py3
-rw-r--r--addons/website_crm_partner_assign/data/crm_partner_assign_data.xml2
-rw-r--r--addons/website_crm_partner_assign/models/crm_lead.py3
-rw-r--r--addons/website_crm_partner_assign/wizard/crm_forward_to_partner.py3
-rw-r--r--addons/website_crm_partner_assign/wizard/crm_forward_to_partner_view.xml2
-rw-r--r--addons/website_event/controllers/main.py11
-rw-r--r--addons/website_event_questions/controllers/main.py4
-rw-r--r--addons/website_event_sale/controllers/main.py3
-rw-r--r--addons/website_event_track/controllers/main.py4
-rw-r--r--addons/website_form/controllers/main.py6
-rw-r--r--addons/website_form/models/models.py5
-rw-r--r--addons/website_forum/controllers/main.py4
-rw-r--r--addons/website_forum/models/res_users.py5
-rw-r--r--addons/website_hr_recruitment/controllers/main.py4
-rw-r--r--addons/website_hr_recruitment/security/website_hr_recruitment_security.xml2
-rw-r--r--addons/website_livechat/controllers/main.py3
-rw-r--r--addons/website_mail/controllers/main.py1
-rw-r--r--addons/website_mail_channel/models/mail_channel.py2
-rw-r--r--addons/website_quote/controllers/main.py3
-rw-r--r--addons/website_sale/controllers/main.py16
-rw-r--r--addons/website_sale/models/sale_order.py3
-rw-r--r--addons/website_sale/models/website_config_settings.py2
-rw-r--r--addons/website_sale/tests/test_website_sale_pricelist.py9
-rw-r--r--addons/website_sale_digital/controllers/main.py6
-rw-r--r--addons/website_sale_options/controllers/main.py4
-rw-r--r--addons/website_slides/controllers/main.py2
-rw-r--r--addons/website_slides/models/slides.py20
-rw-r--r--doc/_extensions/github_link.py4
-rw-r--r--doc/_extensions/odoo_ext/translator.py10
-rw-r--r--doc/cla/corporate/vauxoo.md3
-rw-r--r--doc/cla/individual/NothingCtrl.md9
-rw-r--r--doc/cla/individual/borni-dhifi.md9
-rw-r--r--doc/cla/individual/repodevs.md10
-rw-r--r--doc/python3.rst131
-rw-r--r--doc/reference/cmdline.rst6
-rw-r--r--doc/reference/guidelines.rst5
-rw-r--r--doc/reference/views.rst8
-rw-r--r--odoo/addons/base/ir/ir_actions_report.py11
-rw-r--r--odoo/addons/base/ir/ir_attachment.py26
-rw-r--r--odoo/addons/base/ir/ir_config_parameter.py6
-rw-r--r--odoo/addons/base/ir/ir_fields.py20
-rw-r--r--odoo/addons/base/ir/ir_http.py8
-rw-r--r--odoo/addons/base/ir/ir_mail_server.py77
-rw-r--r--odoo/addons/base/ir/ir_model.py36
-rw-r--r--odoo/addons/base/ir/ir_qweb/__init__.py2
-rw-r--r--odoo/addons/base/ir/ir_qweb/assetsbundle.py23
-rw-r--r--odoo/addons/base/ir/ir_qweb/fields.py36
-rw-r--r--odoo/addons/base/ir/ir_qweb/ir_qweb.py16
-rw-r--r--odoo/addons/base/ir/ir_qweb/qweb.py84
-rw-r--r--odoo/addons/base/ir/ir_sequence.py3
-rw-r--r--odoo/addons/base/ir/ir_translation.py8
-rw-r--r--odoo/addons/base/ir/ir_ui_view.py44
-rw-r--r--odoo/addons/base/ir/ir_values.py22
-rw-r--r--odoo/addons/base/module/module.py11
-rw-r--r--odoo/addons/base/module/report/ir_module_reference_print.py3
-rw-r--r--odoo/addons/base/res/ir_property.py2
-rw-r--r--odoo/addons/base/res/report_paperformat_views.xml2
-rw-r--r--odoo/addons/base/res/res_bank.py5
-rw-r--r--odoo/addons/base/res/res_company.py7
-rw-r--r--odoo/addons/base/res/res_config.py12
-rw-r--r--odoo/addons/base/res/res_lang.py3
-rw-r--r--odoo/addons/base/res/res_partner.py6
-rw-r--r--odoo/addons/base/res/res_users.py12
-rw-r--r--odoo/addons/base/static/img/public_user-image.pngbin3193 -> 0 bytes
-rw-r--r--odoo/addons/base/tests/test_api.py4
-rw-r--r--odoo/addons/base/tests/test_expression.py6
-rw-r--r--odoo/addons/base/tests/test_func.py4
-rw-r--r--odoo/addons/base/tests/test_ir_actions.py2
-rw-r--r--odoo/addons/base/tests/test_ir_attachment.py10
-rw-r--r--odoo/addons/base/tests/test_ir_http.py2
-rw-r--r--odoo/addons/base/tests/test_mail.py14
-rw-r--r--odoo/addons/base/tests/test_mail_examples.py84
-rw-r--r--odoo/addons/base/tests/test_mimetypes.py12
-rw-r--r--odoo/addons/base/tests/test_orm.py2
-rw-r--r--odoo/addons/base/tests/test_qweb.py6
-rw-r--r--odoo/addons/base/tests/test_res_config.py4
-rw-r--r--odoo/addons/base/tests/test_search.py5
-rw-r--r--odoo/addons/base/tests/test_translate.py2
-rw-r--r--odoo/addons/base/tests/test_view_validation.py12
-rw-r--r--odoo/addons/base/tests/test_views.py2
-rw-r--r--odoo/addons/test_converter/tests/test_html.py22
-rw-r--r--odoo/addons/test_impex/models.py7
-rw-r--r--odoo/addons/test_impex/tests/test_export.py4
-rw-r--r--odoo/addons/test_impex/tests/test_load.py4
-rw-r--r--odoo/addons/test_new_api/tests/test_new_fields.py6
-rw-r--r--odoo/addons/test_pylint/tests/test_pylint.py10
-rw-r--r--odoo/api.py18
-rw-r--r--odoo/cli/server.py2
-rw-r--r--odoo/fields.py77
-rw-r--r--odoo/http.py49
-rw-r--r--odoo/loglevels.py19
-rw-r--r--odoo/models.py162
-rw-r--r--odoo/modules/graph.py11
-rw-r--r--odoo/modules/loading.py9
-rw-r--r--odoo/modules/migration.py5
-rw-r--r--odoo/modules/module.py2
-rw-r--r--odoo/modules/registry.py14
-rw-r--r--odoo/osv/expression.py35
-rw-r--r--odoo/osv/orm.py6
-rw-r--r--odoo/service/db.py9
-rw-r--r--odoo/service/model.py7
-rw-r--r--odoo/service/server.py16
-rw-r--r--odoo/service/wsgi_server.py70
-rw-r--r--odoo/sql_db.py6
-rw-r--r--odoo/tests/common.py74
-rw-r--r--odoo/tools/__init__.py2
-rw-r--r--odoo/tools/appdirs.py9
-rw-r--r--odoo/tools/cache.py8
-rw-r--r--odoo/tools/config.py30
-rw-r--r--odoo/tools/convert.py155
-rw-r--r--odoo/tools/float_utils.py19
-rwxr-xr-xodoo/tools/graph.py16
-rw-r--r--odoo/tools/image.py31
-rw-r--r--odoo/tools/lru.py6
-rw-r--r--odoo/tools/mail.py47
-rw-r--r--odoo/tools/mimetypes.py39
-rw-r--r--odoo/tools/misc.py38
-rw-r--r--odoo/tools/pycompat.py104
-rw-r--r--odoo/tools/safe_eval.py4
-rw-r--r--odoo/tools/test_reports.py31
-rw-r--r--odoo/tools/translate.py85
-rw-r--r--odoo/tools/yaml_import.py45
-rw-r--r--odoo/tools/yaml_tag.py4
-rw-r--r--requirements.txt2
-rwxr-xr-xsetup/package.py12
321 files changed, 4025 insertions, 2210 deletions
diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index 21bc3ff9a43..16ee2744892 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_bank_statement.py b/addons/account/models/account_bank_statement.py
index d565b07e54c..3ba6dcb1399 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -305,9 +305,9 @@ class AccountBankStatement(models.Model):
st_lines_left = self.env['account.bank.statement.line'].browse([line.get('id') for line in self.env.cr.dictfetchall()])
#try to assign partner to bank_statement_line
- stl_to_assign_partner = [stl.id for stl in st_lines_left if not stl.partner_id]
- refs = list(set([st.name for st in st_lines_left if not stl.partner_id]))
- if st_lines_left and stl_to_assign_partner and refs\
+ stl_to_assign = st_lines_left.filtered(lambda stl: not stl.partner_id)
+ refs = {stl_to_assign.mapped('name')}
+ if stl_to_assign and refs\
and st_lines_left[0].journal_id.default_credit_account_id\
and st_lines_left[0].journal_id.default_debit_account_id:
@@ -327,7 +327,7 @@ class AccountBankStatement(models.Model):
params = (self.env.user.company_id.id, (st_lines_left[0].journal_id.default_credit_account_id.id, st_lines_left[0].journal_id.default_debit_account_id.id), tuple(refs))
if statements:
sql_query += 'AND stl.id IN %s'
- params += (tuple(stl_to_assign_partner),)
+ params += (tuple(stl_to_assign.ids),)
self.env.cr.execute(sql_query, params)
results = self.env.cr.dictfetchall()
st_line = self.env['account.bank.statement.line']
@@ -499,7 +499,6 @@ class AccountBankStatementLine(models.Model):
'st_line': st_line.get_statement_line_for_reconciliation_widget(),
'reconciliation_proposition': rp
})
-
return ret
def get_statement_line_for_reconciliation_widget(self):
diff --git a/addons/account/models/account_config_settings.py b/addons/account/models/account_config_settings.py
index c70bb30bf4e..4eb6fc1c559 100644
--- a/addons/account/models/account_config_settings.py
+++ b/addons/account/models/account_config_settings.py
@@ -82,10 +82,8 @@ class AccountConfigSettings(models.TransientModel):
self.env.ref('base.group_user').write({'implied_ids': [(4, self.env.ref('product.group_sale_pricelist').id)]})
""" Set the product taxes if they have changed """
ir_values_obj = self.env['ir.values']
- if self.default_sale_tax_id:
- ir_values_obj.sudo().set_default('product.template', "taxes_id", [self.default_sale_tax_id.id], for_all_users=True, company_id=self.company_id.id)
- if self.default_purchase_tax_id:
- ir_values_obj.sudo().set_default('product.template', "supplier_taxes_id", [self.default_purchase_tax_id.id], for_all_users=True, company_id=self.company_id.id)
+ ir_values_obj.sudo().set_default('product.template', "taxes_id", [self.default_sale_tax_id.id] if self.default_sale_tax_id else False, for_all_users=True, company_id=self.company_id.id)
+ ir_values_obj.sudo().set_default('product.template', "supplier_taxes_id", [self.default_purchase_tax_id.id] if self.default_purchase_tax_id else False, for_all_users=True, company_id=self.company_id.id)
""" install a chart of accounts for the given company (if required) """
if self.chart_template_id and self.chart_template_id != self.company_id.chart_template_id:
wizard = self.env['wizard.multi.charts.accounts'].create({
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 3d7924bab47..cc96c01bbd5 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -341,9 +341,9 @@ class AccountInvoice(models.Model):
related='partner_id.commercial_partner_id', store=True, readonly=True,
help="The commercial entity that will be used on Journal Entries for this invoice")
- outstanding_credits_debits_widget = fields.Text(compute='_get_outstanding_info_JSON')
- payments_widget = fields.Text(compute='_get_payment_info_JSON')
- has_outstanding = fields.Boolean(compute='_get_outstanding_info_JSON')
+ outstanding_credits_debits_widget = fields.Text(compute='_get_outstanding_info_JSON', groups="account.group_account_invoice")
+ payments_widget = fields.Text(compute='_get_payment_info_JSON', groups="account.group_account_invoice")
+ has_outstanding = fields.Boolean(compute='_get_outstanding_info_JSON', groups="account.group_account_invoice")
#fields use to set the sequence, on the first invoice of the journal
sequence_number_next = fields.Char(string='Next Number', compute="_get_sequence_prefix", inverse="_set_sequence_next")
@@ -411,7 +411,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)()
@@ -532,7 +532,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
@@ -551,7 +551,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
@@ -799,7 +799,7 @@ class AccountInvoice(models.Model):
self.ensure_one()
credit_aml = self.env['account.move.line'].browse(credit_aml_id)
if not credit_aml.currency_id and self.currency_id != self.company_id.currency_id:
- credit_aml.with_context(allow_amount_currency=True).write({
+ credit_aml.with_context(allow_amount_currency=True, check_move_validity=False).write({
'amount_currency': self.company_id.currency_id.with_context(date=credit_aml.date).compute(credit_aml.balance, self.currency_id),
'currency_id': self.currency_id.id})
if credit_aml.payment_id:
@@ -944,7 +944,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
@@ -1137,7 +1137,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':
@@ -1307,7 +1307,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
@@ -1408,7 +1408,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
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index 311cc20f93b..db70e42b8f9 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -987,26 +987,10 @@ class AccountMoveLine(models.Model):
vals['amount_currency'] = sign * abs(sum([r.amount_residual_currency for r in self]))
# Writeoff line in the account of self
- first_line_dict = vals.copy()
- first_line_dict['account_id'] = self[0].account_id.id
- if 'analytic_account_id' in first_line_dict:
- del first_line_dict['analytic_account_id']
- if 'tax_ids' in first_line_dict:
- tax_ids = []
- #vals['tax_ids'] is a list of commands [[4, tax_id, None], ...]
- for tax_id in vals['tax_ids']:
- tax_ids.append(tax_id[1])
- amount = first_line_dict['credit'] - first_line_dict['debit']
- amount_tax = self.env['account.tax'].browse(tax_ids).compute_all(amount)['total_included']
- first_line_dict['credit'] = amount_tax > 0 and amount_tax or 0.0
- first_line_dict['debit'] = amount_tax < 0 and abs(amount_tax) or 0.0
- del first_line_dict['tax_ids']
+ first_line_dict = self._prepare_writeoff_first_line_values(vals)
# Writeoff line in specified writeoff account
- second_line_dict = vals.copy()
- second_line_dict['debit'], second_line_dict['credit'] = second_line_dict['credit'], second_line_dict['debit']
- if 'amount_currency' in vals:
- second_line_dict['amount_currency'] = -second_line_dict['amount_currency']
+ second_line_dict = self._prepare_writeoff_second_line_values(vals)
# Create the move
writeoff_move = self.env['account.move'].with_context(apply_taxes=True).create({
@@ -1020,6 +1004,32 @@ class AccountMoveLine(models.Model):
# Return the writeoff move.line which is to be reconciled
return writeoff_move.line_ids.filtered(lambda r: r.account_id == self[0].account_id)
+ @api.multi
+ def _prepare_writeoff_first_line_values(self, values):
+ line_values = values.copy()
+ line_values['account_id'] = self[0].account_id.id
+ if 'analytic_account_id' in line_values:
+ del line_values['analytic_account_id']
+ if 'tax_ids' in line_values:
+ tax_ids = []
+ # vals['tax_ids'] is a list of commands [[4, tax_id, None], ...]
+ for tax_id in values['tax_ids']:
+ tax_ids.append(tax_id[1])
+ amount = line_values['credit'] - line_values['debit']
+ amount_tax = self.env['account.tax'].browse(tax_ids).compute_all(amount)['total_included']
+ line_values['credit'] = amount_tax > 0 and amount_tax or 0.0
+ line_values['debit'] = amount_tax < 0 and abs(amount_tax) or 0.0
+ del line_values['tax_ids']
+ return line_values
+
+ @api.multi
+ def _prepare_writeoff_second_line_values(self, values):
+ line_values = values.copy()
+ line_values['debit'], line_values['credit'] = line_values['credit'], line_values['debit']
+ if 'amount_currency' in values:
+ line_values['amount_currency'] = -line_values['amount_currency']
+ return line_values
+
def force_full_reconcile(self):
""" After running the manual reconciliation wizard and making full reconciliation, we need to run this method to create
potentially exchange rate entries that will balance the remaining amount_residual_currency (possibly several aml in
@@ -1031,7 +1041,7 @@ class AccountMoveLine(models.Model):
partial_rec_set = self.env['account.partial.reconcile']
maxdate = '0000-00-00'
- #gather the max date for the move creation, and all aml that are unbalanced
+ # gather the max date for the move creation, and all aml that are unbalanced
for aml in self:
maxdate = max(aml.date, maxdate)
if aml.amount_residual_currency:
@@ -1771,6 +1781,6 @@ class AccountFullReconcile(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 > company.fiscalyear_lock_date:
+ if move_date > (company.fiscalyear_lock_date or '0000-00-00'):
res['date'] = move_date
return res
diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py
index 8c59f15fad8..19989561f86 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_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/static/src/js/account_payment_field.js b/addons/account/static/src/js/account_payment_field.js
index 4745f23e648..1e5171e1f44 100644
--- a/addons/account/static/src/js/account_payment_field.js
+++ b/addons/account/static/src/js/account_payment_field.js
@@ -46,7 +46,7 @@ var ShowPaymentLineWidget = AbstractField.extend({
k.index = v;
k.amount = field_utils.format.float(k.amount, {digits: k.digits});
if (k.date){
- k.date = field_utils.format.date(field_utils.parse.date(k.date, {isUTC: true}));
+ k.date = field_utils.format.date(field_utils.parse.date(k.date, {}, {isUTC: true}));
}
});
this.$el.html(QWeb.render('ShowPaymentInfo', {
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_action.js b/addons/account/static/src/js/reconciliation/reconciliation_action.js
index 51175be7794..8763d5acaa1 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_action.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_action.js
@@ -30,6 +30,7 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
validate_all_balanced: '_onValidate',
change_name: '_onChangeName',
close_statement: '_onCloseStatement',
+ load_more: '_onLoadMore',
},
config: {
// used to instanciate the model
@@ -71,6 +72,7 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
'bank_statement_id': self.model.bank_statement_id,
'valuenow': self.model.valuenow,
'valuemax': self.model.valuemax,
+ 'defaultDisplayQty': self.model.defaultDisplayQty,
'title': self.title,
});
});
@@ -89,12 +91,7 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
this.update_control_panel({breadcrumbs: breadcrumbs, search_view_hidden: true}, {clear: true});
this.renderer.prependTo(self.$('.o_form_sheet'));
- _.each(this.model.lines, function (line, handle) {
- var widget = new self.config.LineRenderer(self, self.model, line);
- widget.handle = handle;
- self.widgets.push(widget);
- widget.appendTo(self.$('.o_reconciliation_lines'));
- });
+ this._renderLines();
this._openFirstLine();
},
@@ -135,6 +132,15 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
},
/**
+ *
+ */
+ _loadMore: function(qty) {
+ var self = this;
+ return this.model.loadMore(qty).then(function () {
+ self._renderLines();
+ });
+ },
+ /**
* sitch to 'match' the first available line
*
* @private
@@ -153,6 +159,24 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
}
return handle;
},
+ /**
+ * render line widget and append to view
+ *
+ * @private
+ */
+ _renderLines: function () {
+ var self = this;
+ var linesToDisplay = this.model.getStatementLines();
+ _.each(linesToDisplay, function (line, handle) {
+ var widget = new self.config.LineRenderer(self, self.model, line);
+ widget.handle = handle;
+ self.widgets.push(widget);
+ widget.appendTo(self.$('.o_reconciliation_lines'));
+ });
+ if (this.model.hasMoreLines() === false) {
+ this.renderer.hideLoadMoreButton();
+ }
+ },
//--------------------------------------------------------------------------
// Handlers
@@ -224,6 +248,14 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
});
},
/**
+ * Load more statement and render them
+ *
+ * @param {OdooEvent} event
+ */
+ _onLoadMore: function (event) {
+ return this._loadMore(this.model.defaultDisplayQty);
+ },
+ /**
* call 'validate' or 'autoReconciliation' model method then destroy the
* validated lines and update the action renderer with the new status bar
* values and notifications then open the first available line
@@ -246,7 +278,15 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
});
_.each(result.handles, function (handle) {
self._getWidget(handle).destroy();
+ var index = _.findIndex(self.widgets, function (widget) {return widget.handle===handle;});
+ self.widgets.splice(index, 1);
});
+ // Get number of widget and if less than constant and if there are more to laod, load until constant
+ if (self.widgets.length < self.model.defaultDisplayQty
+ && self.model.valuemax - self.model.valuenow >= self.model.defaultDisplayQty) {
+ var toLoad = self.model.defaultDisplayQty - self.widgets.length;
+ self._loadMore(toLoad);
+ }
self._openFirstLine();
});
},
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js
index e12f8f21009..019e761f3d3 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_model.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js
@@ -100,6 +100,8 @@ var StatementModel = BasicModel.extend({
this.lines = {};
this.valuenow = 0;
this.valuemax = 0;
+ this.alreadyDisplayed = [];
+ this.defaultDisplayQty = 10;
},
//--------------------------------------------------------------------------
@@ -230,10 +232,10 @@ var StatementModel = BasicModel.extend({
var line = this.getLine(handle);
line.st_line.partner_id = partner && partner.id;
line.st_line.partner_name = partner && partner.display_name || '';
- return this._changePartner(handle, partner.id)
+ return $.when(partner && this._changePartner(handle, partner.id))
.then(function() {
line.reconciliation_proposition = [];
- return self._performMoveLine(handle)
+ return self.changeMode(handle, 'match');
})
.then(function () {
if (line.mode === 'create') {
@@ -253,7 +255,7 @@ var StatementModel = BasicModel.extend({
args: [self.bank_statement_id.id],
})
.then(function () {
- return self.bank_statement_id.id
+ return self.bank_statement_id.id;
});
},
/**
@@ -284,6 +286,34 @@ var StatementModel = BasicModel.extend({
return this.context;
},
/**
+ * Return the lines that needs to be displayed by the widget
+ *
+ * @returns {Object} lines that are loaded and not yet displayed
+ */
+ getStatementLines: function () {
+ var self = this;
+ var linesToDisplay = _.pick(this.lines, function(value, key, object) {
+ if (value.visible === true && self.alreadyDisplayed.indexOf(key) === -1) {
+ self.alreadyDisplayed.push(key);
+ return object;
+ }
+ });
+ return linesToDisplay;
+ },
+ /**
+ * Return a boolean telling if load button needs to be displayed or not
+ *
+ * @returns {boolean} true if load more button needs to be displayed
+ */
+ hasMoreLines: function () {
+ var self = this;
+ var notDisplayed = _.filter(this.lines, function(line) { return !line.visible; });
+ if (notDisplayed.length > 0) {
+ return true;
+ }
+ return false;
+ },
+ /**
* get the line data for this handle
*
* @param {OdooEvent} event
@@ -355,15 +385,43 @@ var StatementModel = BasicModel.extend({
line.reconcileModels = self.reconcileModels;
});
var ids = _.pluck(self.lines, 'id');
- return self._rpc({
- model: 'account.bank.statement.line',
- method: 'get_data_for_reconciliation_widget',
- args: [ids],
- })
- .then(self._formatLine.bind(self));
+ ids = ids.splice(0, self.defaultDisplayQty);
+ self.pagerIndex = ids.length;
+ return self.loadData(ids, []);
});
},
/**
+ * Load more bank statement line
+ *
+ * @param {integer} quantity to load
+ * @returns {Deferred}
+ */
+ loadMore: function(qty) {
+ if (qty === undefined) {
+ qty = this.defaultDisplayQty;
+ }
+ var ids = _.pluck(this.lines, 'id');
+ ids = ids.splice(this.pagerIndex, qty);
+ this.pagerIndex += qty;
+ return this.loadData(ids, this._getExcludedIds());
+ },
+ /**
+ * RPC method to load informations on lines
+ *
+ * @param {Array} ids of bank statement line passed to rpc call
+ * @param {Array} list of move_line ids that needs to be excluded from search
+ * @returns {Deferred}
+ */
+ loadData: function(ids, excluded_ids) {
+ var self = this;
+ return self._rpc({
+ model: 'account.bank.statement.line',
+ method: 'get_data_for_reconciliation_widget',
+ args: [ids, excluded_ids],
+ })
+ .then(self._formatLine.bind(self));
+ },
+ /**
* Add lines into the propositions from the reconcile model
* Can add 2 lines, and each with its taxes. The second line become editable
* in the create mode.
@@ -770,6 +828,7 @@ var StatementModel = BasicModel.extend({
var line = _.find(self.lines, function (l) {
return l.id === data.st_line.id;
});
+ line.visible = true;
_.extend(line, data);
self._formatLineProposition(line, line.reconciliation_proposition);
if (!line.reconciliation_proposition.length) {
@@ -851,6 +910,26 @@ var StatementModel = BasicModel.extend({
return prop;
},
/**
+ * Return list of account_move_line that has been selected and needs to be removed
+ * from other calls.
+ *
+ * @private
+ * @returns {Array} list of excluded ids
+ */
+ _getExcludedIds: function () {
+ var excludedIds = [];
+ _.each(this.lines, function(line) {
+ if (line.reconciliation_proposition) {
+ _.each(line.reconciliation_proposition, function(prop) {
+ if (parseInt(prop['id'])) {
+ excludedIds.push(prop['id']);
+ }
+ })
+ }
+ });
+ return excludedIds;
+ },
+ /**
* Defined whether the line is to be displayed or not. Here, we only display
* the line if it comes from the server or if an account is defined when it
* is created
@@ -1168,7 +1247,8 @@ var ManualModel = StatementModel.extend({
'filter': "",
'reconcileModels': [],
'account_id': this._formatNameGet([data.account_id, data.account_name]),
- 'st_line': data
+ 'st_line': data,
+ 'visible': true
});
this._formatLineProposition(line, line.reconciliation_proposition);
if (!line.reconciliation_proposition.length) {
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
index e0ecf61d354..2530a6043c2 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
@@ -24,6 +24,7 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
"click *[rel='do_action']": "_onDoAction",
'click button.button_back_to_statement': '_onGoToBankStatement',
'click button.button_close_statement': '_onCloseBankStatement',
+ 'click button.js_load_more': '_onLoadMore',
},
/**
* @override
@@ -87,6 +88,12 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
+ /*
+ * hide the button to load more statement line
+ */
+ hideLoadMoreButton: function () {
+ this.$('.js_load_more').hide();
+ },
/**
* update the statement rendering
*
@@ -215,6 +222,14 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
});
},
/**
+ * Load more statement lines for reconciliation
+ * @private
+ * @param {MouseEvent} event
+ */
+ _onLoadMore: function (e) {
+ this.trigger_up('load_more');
+ },
+ /**
* @private
*/
_onValidateName: function () {
diff --git a/addons/account/static/src/less/account_reconciliation.less b/addons/account/static/src/less/account_reconciliation.less
index 4ecf02c8416..66b39301fbb 100644
--- a/addons/account/static/src/less/account_reconciliation.less
+++ b/addons/account/static/src/less/account_reconciliation.less
@@ -166,7 +166,6 @@
vertical-align: top;
padding-right: 10px;
font-weight: bold;
- white-space: nowrap;
}
}
@@ -209,7 +208,7 @@
.o-transition(max-height, 250ms);
}
&[data-mode="match"] > .match {
- max-height: 130px;
+ max-height: none;
.o-transition(max-height, 400ms);
}
&[data-mode="create"] > .create {
diff --git a/addons/account/static/src/xml/account_reconciliation.xml b/addons/account/static/src/xml/account_reconciliation.xml
index e73a42c785b..e41bb97cc3d 100644
--- a/addons/account/static/src/xml/account_reconciliation.xml
+++ b/addons/account/static/src/xml/account_reconciliation.xml
@@ -25,6 +25,9 @@
<div class="notification_area"/>
</div>
<div class="o_reconciliation_lines"/>
+ <div t-if="widget._initialState.valuemax > widget._initialState.defaultDisplayQty">
+ <button class="btn btn-default js_load_more">Load more</button>
+ </div>
<div class="pull-right text-muted">Tip: Hit CTRL-Enter to reconcile all the balanced items in the sheet.</div>
</div>
<div t-else="" class="o_view_nocontent">
@@ -203,7 +206,16 @@
<td class="cell_action"><span class="fa fa-add-remove"/></td>
<td class="cell_account_code"><t t-esc="line.account_code"/></td>
<td class="cell_due_date"><t t-esc="line.date_maturity === false ? line.date : line.date_maturity"/></td>
- <td class="cell_label"><t t-if="line.partner_id &amp;&amp; line.partner_id !== state.st_line.partner_id"><t t-if="line.partner_name.length"><t t-esc="line.partner_name"/>: </t><t t-esc="line.label || line.name"/><t t-if="line.ref &amp;&amp; line.ref.length"> : </t><t t-esc="line.ref"/></t><t t-else=""><t t-esc="line.ref &amp;&amp; line.ref.length ? line.ref : line.label || line.name"/></t></td>
+ <td class="cell_label">
+ <t t-if="line.partner_id &amp;&amp; line.partner_id !== state.st_line.partner_id">
+ <t t-if="line.partner_name.length">
+ <t t-esc="line.partner_name"/>:
+ </t>
+ </t>
+ <t t-esc="line.label || line.name"/>
+ <t t-if="line.ref &amp;&amp; line.ref.length"> : </t>
+ <t t-esc="line.ref"/>
+ </td>
<td class="cell_left"><t t-if="line.amount &lt; 0"><span t-if="line.amount_currency_str" t-attf-class="o_multi_currency o_multi_currency_color_#{line.currency_id%8} line_info_button fa fa-money" t-att-data-content="line.amount_currency_str"/><t t-raw="line.amount_str"/></t></td>
<td class="cell_right"><t t-if="line.amount &gt; 0"><span t-if="line.amount_currency_str" t-attf-class="o_multi_currency o_multi_currency_color_#{line.currency_id%8} line_info_button fa fa-money" t-att-data-content="line.amount_currency_str"/><t t-raw="line.amount_str"/></t></td>
<td class="cell_info_popover"></td>
diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js
index 0eeb805112c..511656906d9 100644
--- a/addons/account/static/tests/reconciliation_tests.js
+++ b/addons/account/static/tests/reconciliation_tests.js
@@ -1,7 +1,9 @@
odoo.define('account.reconciliation_tests.data', function () {
"use strict";
-var data = {
+var Datas = {};
+
+var db = {
'res.partner': {
fields: {
id: {string: "ID", type: 'integer'},
@@ -58,7 +60,7 @@ var data = {
{id: 7, display_name: "Tax 10.00% include", amount: 10, price_include: true, account_id: 288},
],
json_friendly_compute_all: function (args) {
- var tax = _.find(data['account.tax'].records, {'id': args[0][0]});
+ var tax = _.find(db['account.tax'].records, {'id': args[0][0]});
var amount = args[1];
var tax_base = tax.price_include ? amount*100/(100+tax.amount) : amount;
return $.when({
@@ -103,7 +105,7 @@ var data = {
'account.bank.statement': {
fields: {},
reconciliation_widget_preprocess: function () {
- return $.when(data_preprocess);
+ return $.when(Datas.used.data_preprocess);
},
},
'account.bank.statement.line': {
@@ -122,19 +124,19 @@ var data = {
var partner_id = args.splice(1, 1)[0];
var excluded_ids = args.splice(1, 1)[0];
var key = JSON.stringify(args);
- if (!mv_lines[key]) {
+ if (!Datas.used.mv_lines[key]) {
throw new Error("Unknown parameters for get_move_lines_for_reconciliation_widget: '"+ key + "'");
}
- return $.when(_.filter(mv_lines[key], function (line) {
+ return $.when(_.filter(Datas.used.mv_lines[key], function (line) {
return excluded_ids.indexOf(line.id) === -1 && (!partner_id || partner_id === line.partner_id);
}));
},
get_data_for_reconciliation_widget: function (args) {
var ids = args[0];
- return $.when(_.filter(data_widget, function (w) {return _.contains(ids, w.st_line.id);}));
+ return $.when(_.filter(Datas.used.data_widget, function (w) {return _.contains(ids, w.st_line.id);}));
},
reconciliation_widget_auto_reconcile: function () {
- return $.when(auto_reconciliation);
+ return $.when(Datas.used.auto_reconciliation);
},
process_reconciliations: function (args) {
var datas = args[1];
@@ -142,8 +144,8 @@ var data = {
ids = ids.concat(_.flatten(_.pluck(datas, 'payment_aml_ids')));
ids = _.compact(ids);
- for (var key in move_lines_for_manual_reconciliation) {
- move_lines_for_manual_reconciliation[key] = _.filter(move_lines_for_manual_reconciliation[key], function (mv_line) {
+ for (var key in Datas.used.move_lines_for_manual_reconciliation) {
+ Datas.used.move_lines_for_manual_reconciliation[key] = _.filter(Datas.used.move_lines_for_manual_reconciliation[key], function (mv_line) {
return ids.indexOf(mv_line.id) === -1;
});
}
@@ -154,18 +156,18 @@ var data = {
fields: {},
get_data_for_manual_reconciliation_widget: function (args) {
var key = JSON.stringify(args);
- if (!data_for_manual_reconciliation_widget[key]) {
+ if (!Datas.used.data_for_manual_reconciliation_widget[key]) {
throw new Error("Unknown parameters for get_data_for_manual_reconciliation_widget: '"+ key + "'");
}
- return $.when(data_for_manual_reconciliation_widget[key]);
+ return $.when(Datas.used.data_for_manual_reconciliation_widget[key]);
},
get_move_lines_for_manual_reconciliation: function (args) {
var excluded_ids = args.splice(2, 1)[0];
var key = JSON.stringify(args);
- if (!move_lines_for_manual_reconciliation[key]) {
+ if (!Datas.used.move_lines_for_manual_reconciliation[key]) {
throw new Error("Unknown parameters for get_move_lines_for_manual_reconciliation: '"+ key + "'");
}
- return $.when(_.filter(move_lines_for_manual_reconciliation[key], function (line) {
+ return $.when(_.filter(Datas.used.move_lines_for_manual_reconciliation[key], function (line) {
return excluded_ids.indexOf(line.id) === -1;
}));
},
@@ -174,8 +176,8 @@ var data = {
var datas = args[0];
for (var i in datas) {
var data = datas[i];
- for (var key in move_lines_for_manual_reconciliation) {
- move_lines_for_manual_reconciliation[key] = _.filter(move_lines_for_manual_reconciliation[key], function (mv_line) {
+ for (var key in Datas.used.move_lines_for_manual_reconciliation) {
+ Datas.used.move_lines_for_manual_reconciliation[key] = _.filter(Datas.used.move_lines_for_manual_reconciliation[key], function (mv_line) {
return data.mv_line_ids.indexOf(mv_line.id) === -1;
});
}
@@ -367,7 +369,12 @@ var mv_lines = {
'[7,"",0,6]': [
{'account_type': "receivable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-07", 'date': "2017-01-08", 'total_amount_str': "$ 650.00", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0012", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 133, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 650.00", 'debit': 650.0, 'account_id': [287, "101200 Account Receivable"], 'account_code': "101200", 'ref': "", 'already_paid': false},
{'account_type': "receivable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-28", 'date': "2017-01-01", 'total_amount_str': "$ 4,610.00", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0001", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 106, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 4,610.00", 'debit': 4610.0, 'account_id': [287, "101200 Account Receivable"], 'account_code': "101200", 'ref': "", 'already_paid': false},
- {'account_type': "payable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-28", 'date': "2017-01-01", 'total_amount_str': "$ 10,000.00", 'partner_id': 12, 'account_name': "Account Payable", 'name': "BILL/2017/0001", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 114, 'credit': 10000.0, 'journal_id': [2, "Vendor Bills"], 'amount_str': "$ 10,000.00", 'debit': 0.0, 'account_id': [284, "101110 Stock Valuation Account"], 'account_code': "111100", 'ref': "", 'already_paid': false}
+ {'account_type': "payable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-28", 'date': "2017-01-01", 'total_amount_str': "$ 10,000.00", 'partner_id': 12, 'account_name': "Account Payable", 'name': "BILL/2017/0001", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 114, 'credit': 10000.0, 'journal_id': [2, "Vendor Bills"], 'amount_str': "$ 10,000.00", 'debit': 0.0, 'account_id': [284, "101110 Stock Valuation Account"], 'account_code': "111100", 'ref': "", 'already_paid': false},
+ {'account_type': "liquidity", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-01-23", 'date': "2017-01-23", 'total_amount_str': "$ 376.00", 'partner_id': 7, 'account_name': "Bank", 'name': "BNK1/2017/0002: SUPP.OUT/2017/0002", 'partner_name': "ASUSTeK", 'total_amount_currency_str': "", 'id': 392, 'credit': 376.0, 'journal_id': "Bank", 'amount_str': "$ 376.00", 'debit': 0.0, 'account_code': "101401", 'ref': "BILL/2017/0003", 'already_paid': true},
+ {'account_type': "liquidity", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-01-23", 'date': "2017-01-23", 'total_amount_str': "$ 100.00", 'partner_id': 8, 'account_name': "Bank", 'name': "BNK1/2017/0003: CUST.IN/2017/0001", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 394, 'credit': 0.0, 'journal_id': "Bank", 'amount_str': "$ 100.00", 'debit': 100.0, 'account_code': "101401", 'ref': "", 'already_paid': true},
+ {'account_type': "liquidity", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-01-23", 'date': "2017-01-23", 'total_amount_str': "$ 525.50", 'partner_id': 8, 'account_name': "Bank", 'name': "BNK1/2017/0004: CUST.IN/2017/0002", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 396, 'credit': 0.0, 'journal_id': "Bank", 'amount_str': "$ 525.50", 'debit': 525.5, 'account_code': "101401", 'ref': "INV/2017/0003", 'already_paid': true},
+ {'account_type': "receivable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-07", 'date': "2017-01-08", 'total_amount_str': "$ 650.00", 'partner_id': 8, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0002", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 109, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 650.00", 'debit': 650.0, 'account_code': "101200", 'ref': "", 'already_paid': false},
+ {'account_type': "receivable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-22", 'date': "2017-01-23", 'total_amount_str': "$ 525.00", 'partner_id': 8, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0004", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 399, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 525.00", 'debit': 525.0, 'account_code': "101200", 'ref': "", 'already_paid': false},
],
'[8,"",0,6]': [],
};
@@ -468,23 +475,22 @@ var options = {
}
};
-return {
- params: {
- data: data,
- data_preprocess: data_preprocess,
- data_widget: data_widget,
- mv_lines: mv_lines,
- auto_reconciliation: auto_reconciliation,
- data_for_manual_reconciliation_widget: data_for_manual_reconciliation_widget,
- move_lines_for_manual_reconciliation: move_lines_for_manual_reconciliation,
- session: session,
- options: options,
- },
+Datas.params = {
+ data: db,
+ data_preprocess: data_preprocess,
+ data_widget: data_widget,
+ mv_lines: mv_lines,
+ auto_reconciliation: auto_reconciliation,
+ data_for_manual_reconciliation_widget: data_for_manual_reconciliation_widget,
+ move_lines_for_manual_reconciliation: move_lines_for_manual_reconciliation,
+ session: session,
+ options: options,
+};
// this is the main function for this module. Its job is to export (and clone) all data for a test.
- getParams: function () {
- return $.extend(true, {}, this.params);
- }
+Datas.getParams = function () {
+ return (this.used = $.extend(true, {}, this.params));
};
+return Datas;
});
odoo.define('account.reconciliation_tests', function (require) {
@@ -534,7 +540,6 @@ QUnit.module('account', {
clientAction.destroy();
});
-
QUnit.test('Reconciliation fields', function (assert) {
assert.expect(6);
@@ -561,7 +566,6 @@ QUnit.module('account', {
clientAction.destroy();
});
-
QUnit.test('Reconciliation basic data', function (assert) {
assert.expect(14);
@@ -630,7 +634,6 @@ QUnit.module('account', {
clientAction.destroy();
});
-
QUnit.test('Reconciliation partial', function (assert) {
assert.expect(10);
@@ -701,7 +704,6 @@ QUnit.module('account', {
clientAction.destroy();
});
-
QUnit.test('Reconciliation currencies', function (assert) {
assert.expect(2);
@@ -736,7 +738,7 @@ QUnit.module('account', {
});
QUnit.test('Reconciliation change partner', function (assert) {
- assert.expect(4);
+ assert.expect(10);
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
@@ -754,7 +756,21 @@ QUnit.module('account', {
$('.ui-autocomplete .ui-menu-item a:contains(Camptocamp)').trigger('mouseenter').trigger('click');
clientAction._onAction({target: widget, name: 'change_partner', data: {data: {display_name: 'Camptocamp', id: 12}}, stopped: false});
assert.strictEqual(widget.$('.o_input_dropdown input').val(), "Camptocamp", "the partner many2one should display Camptocamp");
- assert.strictEqual(widget.$('.match table tr').length, 3, "camptocamp should have 3 propositions for reconciliation");
+ assert.strictEqual(widget.$('.match table tr.mv_line').length, 3, "camptocamp should have 3 propositions for reconciliation");
+
+ widget = clientAction.widgets[2];
+ widget.$('.accounting_view thead td:first').trigger('click');
+ widget.$('.accounting_view .mv_line .cell_label').trigger('click');
+ assert.strictEqual(widget.$('.o_input_dropdown input').val(), "Camptocamp", "the partner many2one should display agrolait");
+ assert.strictEqual(widget.$('.match table tr').length, 3, "Camptocamp should have 3 propositions for reconciliation");
+ assert.strictEqual(widget.$('.match .match_controls .fa:not(.disabled)').length, 0, "should not display the right arrow");
+
+ // Simulate remove partner
+ widget.$('.o_input_dropdown input').trigger('click');
+ widget.$('.o_input_dropdown input').val('').trigger('keyup').trigger('blur');
+ assert.strictEqual(widget.$('.o_input_dropdown input').val(), "", "the partner many2one should be empty");
+ assert.strictEqual(widget.$('.match table tr.mv_line').length, 5, "should have 5 propositions for reconciliation if partner is false");
+ assert.strictEqual(widget.$('.match .match_controls .fa:not(.disabled)').length, 1, "should not display the right arrow");
clientAction.destroy();
});
@@ -839,7 +855,6 @@ QUnit.module('account', {
clientAction.destroy();
});
-
QUnit.test('Reconciliation create line with taxes', function (assert) {
assert.expect(13);
@@ -883,7 +898,6 @@ QUnit.module('account', {
clientAction.destroy();
});
-
QUnit.test('Reconciliation create line from reconciliation model', function (assert) {
assert.expect(6);
@@ -900,7 +914,7 @@ QUnit.module('account', {
widget.$('.create .quick_add button:contains(ATOS)').trigger('click');
assert.strictEqual(widget.$('.accounting_view tbody .cell_label, .accounting_view tbody .cell_right').text().replace(/[\n\r\s$,]+/g, ' '),
- "ATOS Banque1145.62Tax 20.00%229.12ATOS Frais26.71Tax 10.00% include2.67", "should display 4 lines");
+ " ATOS Banque 1145.62 Tax 20.00% 229.12 ATOS Frais 26.71 Tax 10.00% include 2.67", "should display 4 lines");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s$,]+/g, ' '),
"Create Write-off229.12", "should display the 'Create Write-off' line with value in left column");
@@ -926,7 +940,6 @@ QUnit.module('account', {
clientAction.destroy();
});
-
QUnit.test('Reconciliation auto reconciliation', function (assert) {
assert.expect(6);
@@ -958,7 +971,6 @@ QUnit.module('account', {
clientAction.destroy();
});
-
QUnit.test('Reconciliation manual', function (assert) {
assert.expect(8);
@@ -992,5 +1004,229 @@ QUnit.module('account', {
clientAction.destroy();
});
+
+
+ QUnit.test('Reconciliation pager', function (assert) {
+ assert.expect(6);
+
+ this.params.data['account.bank.statement.line'].records.push(
+ {id: 9, display_name: "add 9"},
+ {id: 10, display_name: "add 10"},
+ {id: 11, display_name: "add 11"},
+ {id: 12, display_name: "add 12"},
+ {id: 13, display_name: "add 13"},
+ {id: 14, display_name: "add 14"},
+ {id: 15, display_name: "add 15"},
+ {id: 16, display_name: "add 16"}
+ );
+
+ var data_preprocess = this.params.data_preprocess;
+ data_preprocess.st_lines_ids.push(9, 10, 11, 12, 13, 14, 15, 16);
+
+ this.params.data_widget.push(
+ {
+ 'st_line': {
+ 'currency_id': 3,
+ 'communication_partner_name': false,
+ 'open_balance_account_id': 287,
+ 'name': "add 9",
+ 'partner_name': "Camptocamp",
+ 'partner_id': 12,
+ 'has_no_partner': false,
+ 'journal_id': 84,
+ 'account_name': "Bank",
+ 'note': "",
+ 'amount': 650.0,
+ 'amount_str': "$ 650.00",
+ 'amount_currency_str': "",
+ 'date': "2017-01-01",
+ 'account_code': "101401",
+ 'ref': "",
+ 'id': 9,
+ 'statement_id': 2
+ },
+ 'reconciliation_proposition': []
+ },
+ {
+ 'st_line': {
+ 'currency_id': 3,
+ 'communication_partner_name': false,
+ 'open_balance_account_id': 287,
+ 'name': "add 10",
+ 'partner_name': "Camptocamp",
+ 'partner_id': 12,
+ 'has_no_partner': false,
+ 'journal_id': 84,
+ 'account_name': "Bank",
+ 'note': "",
+ 'amount': 650.0,
+ 'amount_str': "$ 650.00",
+ 'amount_currency_str': "",
+ 'date': "2017-01-01",
+ 'account_code': "101401",
+ 'ref': "",
+ 'id': 10,
+ 'statement_id': 2
+ },
+ 'reconciliation_proposition': []
+ },
+ {
+ 'st_line': {
+ 'currency_id': 3,
+ 'communication_partner_name': false,
+ 'open_balance_account_id': 287,
+ 'name': "add 11",
+ 'partner_name': "Camptocamp",
+ 'partner_id': 12,
+ 'has_no_partner': false,
+ 'journal_id': 84,
+ 'account_name': "Bank",
+ 'note': "",
+ 'amount': 650.0,
+ 'amount_str': "$ 650.00",
+ 'amount_currency_str': "",
+ 'date': "2017-01-01",
+ 'account_code': "101401",
+ 'ref': "",
+ 'id': 11,
+ 'statement_id': 2
+ },
+ 'reconciliation_proposition': []
+ },
+ {
+ 'st_line': {
+ 'currency_id': 3,
+ 'communication_partner_name': false,
+ 'open_balance_account_id': 287,
+ 'name': "add 12",
+ 'partner_name': "Camptocamp",
+ 'partner_id': 12,
+ 'has_no_partner': false,
+ 'journal_id': 84,
+ 'account_name': "Bank",
+ 'note': "",
+ 'amount': 650.0,
+ 'amount_str': "$ 650.00",
+ 'amount_currency_str': "",
+ 'date': "2017-01-01",
+ 'account_code': "101401",
+ 'ref': "",
+ 'id': 12,
+ 'statement_id': 2
+ },
+ 'reconciliation_proposition': []
+ },
+ {
+ 'st_line': {
+ 'currency_id': 3,
+ 'communication_partner_name': false,
+ 'open_balance_account_id': 287,
+ 'name': "add 13",
+ 'partner_name': "Camptocamp",
+ 'partner_id': 12,
+ 'has_no_partner': false,
+ 'journal_id': 84,
+ 'account_name': "Bank",
+ 'note': "",
+ 'amount': 650.0,
+ 'amount_str': "$ 650.00",
+ 'amount_currency_str': "",
+ 'date': "2017-01-01",
+ 'account_code': "101401",
+ 'ref': "",
+ 'id': 13,
+ 'statement_id': 2
+ },
+ 'reconciliation_proposition': []
+ },
+ {
+ 'st_line': {
+ 'currency_id': 3,
+ 'communication_partner_name': false,
+ 'open_balance_account_id': 287,
+ 'name': "add 14",
+ 'partner_name': "Camptocamp",
+ 'partner_id': 12,
+ 'has_no_partner': false,
+ 'journal_id': 84,
+ 'account_name': "Bank",
+ 'note': "",
+ 'amount': 650.0,
+ 'amount_str': "$ 650.00",
+ 'amount_currency_str': "",
+ 'date': "2017-01-01",
+ 'account_code': "101401",
+ 'ref': "",
+ 'id': 14,
+ 'statement_id': 2
+ },
+ 'reconciliation_proposition': []
+ },
+ {
+ 'st_line': {
+ 'currency_id': 3,
+ 'communication_partner_name': false,
+ 'open_balance_account_id': 287,
+ 'name': "add 15",
+ 'partner_name': "Camptocamp",
+ 'partner_id': 12,
+ 'has_no_partner': false,
+ 'journal_id': 84,
+ 'account_name': "Bank",
+ 'note': "",
+ 'amount': 650.0,
+ 'amount_str': "$ 650.00",
+ 'amount_currency_str': "",
+ 'date': "2017-01-01",
+ 'account_code': "101401",
+ 'ref': "",
+ 'id': 15,
+ 'statement_id': 2
+ },
+ 'reconciliation_proposition': []
+ },
+ {
+ 'st_line': {
+ 'currency_id': 3,
+ 'communication_partner_name': false,
+ 'open_balance_account_id': 287,
+ 'name': "add 16",
+ 'partner_name': "Camptocamp",
+ 'partner_id': 12,
+ 'has_no_partner': false,
+ 'journal_id': 84,
+ 'account_name': "Bank",
+ 'note': "",
+ 'amount': 650.0,
+ 'amount_str': "$ 650.00",
+ 'amount_currency_str': "",
+ 'date': "2017-01-01",
+ 'account_code': "101401",
+ 'ref': "",
+ 'id': 16,
+ 'statement_id': 2
+ },
+ 'reconciliation_proposition': []
+ }
+ );
+
+ var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
+ testUtils.addMockEnvironment(clientAction, {
+ 'data': this.params.data,
+ });
+
+ clientAction.appendTo($('#qunit-fixture'));
+
+ assert.strictEqual(clientAction.$('.o_reconciliation_line').length, 10, "should display 10 lines");
+ assert.strictEqual(clientAction.$('.js_load_more:visible').length, 1, "should display the load more button");
+ clientAction.$('.o_reconcile:visible').click();
+ assert.strictEqual(clientAction.$('.o_reconcile:visible').length, 0, "should remove the reconciled line");
+ assert.strictEqual(clientAction.$('.o_reconciliation_line').length, 10, "should load one line to complete the 10");
+ clientAction.$('.js_load_more').click();
+ assert.strictEqual(clientAction.$('.o_reconciliation_line').length, 11, "should load the last record");
+ assert.strictEqual(clientAction.$('.js_load_more:visible').length, 0, "should hide the load more button");
+
+ clientAction.destroy();
+ });
});
});
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/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml
index e1a0c6a3334..d73af20294d 100644
--- a/addons/account/views/account_invoice_view.xml
+++ b/addons/account/views/account_invoice_view.xml
@@ -223,7 +223,7 @@
<button name="action_invoice_draft" states="cancel" string="Set to Draft" type="object" groups="account.group_account_invoice"/>
<field name="state" widget="statusbar" statusbar_visible="draft,open,paid" />
</header>
- <div class="alert alert-info" role="alert" style="margin-bottom:0px;" attrs="{'invisible': [('has_outstanding','=',False)]}">
+ <div groups="account.group_account_invoice" class="alert alert-info" role="alert" style="margin-bottom:0px;" attrs="{'invisible': [('has_outstanding','=',False)]}">
You have <bold><a class="alert-link" href="#outstanding" role="button">outstanding debits</a></bold> for this supplier. You can allocate them to mark this bill as paid.
</div>
<field name="has_outstanding" invisible="1"/>
@@ -363,7 +363,7 @@
<button name="action_invoice_draft" states="cancel" string="Reset to Draft" type="object" groups="account.group_account_invoice"/>
<field name="state" widget="statusbar" nolabel="1" statusbar_visible="draft,open,paid"/>
</header>
- <div class="alert alert-info" role="alert" style="margin-bottom:0px;" attrs="{'invisible': [('has_outstanding','=',False)]}">
+ <div groups="account.group_account_invoice" class="alert alert-info" role="alert" style="margin-bottom:0px;" attrs="{'invisible': [('has_outstanding','=',False)]}">
You have <bold><a class="alert-link" href="#outstanding" role="button">outstanding payments</a></bold> for this customer. You can allocate them to mark this invoice as paid.
</div>
<field name="has_outstanding" invisible="1"/>
diff --git a/addons/account/wizard/account_invoice_refund.py b/addons/account/wizard/account_invoice_refund.py
index f0840aa7856..4fdd998f215 100644
--- a/addons/account/wizard/account_invoice_refund.py
+++ b/addons/account/wizard/account_invoice_refund.py
@@ -74,7 +74,7 @@ class AccountInvoiceRefund(models.TransientModel):
for tmpline in refund.move_id.line_ids:
if tmpline.account_id.id == inv.account_id.id:
to_reconcile_lines += tmpline
- to_reconcile_lines.filtered(lambda l: l.reconciled == False).reconcile()
+ to_reconcile_lines.filtered(lambda l: l.reconciled == False).reconcile()
if mode == 'modify':
invoice = inv.read(inv_obj._get_refund_modify_read_fields())
invoice = invoice[0]
diff --git a/addons/account_asset/models/account_asset.py b/addons/account_asset/models/account_asset.py
index 4c2855d216e..2a4432ace9e 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 = '<span>%s</span>' % message_description
- for name, values in pycompat.items(tracked_values):
+ for name, values in tracked_values.items():
message += '<div> &nbsp; &nbsp; &bull; <b>%s</b>: ' % name
message += '%s</div>' % 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..02256abaa22 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):
@@ -394,7 +393,7 @@ class AccountVoucherLine(models.Model):
if product.description_purchase:
values['name'] += '\n' + product.description_purchase
else:
- values['price_unit'] = product.lst_price
+ values['price_unit'] = price_unit or product.lst_price
taxes = product.taxes_id or account.tax_ids
if product.description_sale:
values['name'] += '\n' + product.description_sale
@@ -404,7 +403,7 @@ class AccountVoucherLine(models.Model):
if company and currency:
if company.currency_id != currency:
if type == 'purchase':
- values['price_unit'] = product.standard_price
+ values['price_unit'] = price_unit or product.standard_price
values['price_unit'] = values['price_unit'] * currency.rate
return {'value': values, 'domain': {}}
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..c86db44e26c 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,12 +653,18 @@ class Import(models.TransientModel):
except ValueError as error:
return [{
'type': 'error',
- 'message': unicode(error),
+ 'message': pycompat.text_type(error),
'record': False,
}]
_logger.info('importing %d rows...', len(data))
- import_result = self.env[self.res_model].with_context(import_file=True).load(import_fields, data)
+
+ model = self.env[self.res_model].with_context(import_file=True)
+ defer_parent_store = self.env.context.get('defer_parent_store_computation', True)
+ if defer_parent_store and model._parent_store:
+ model = model.with_context(defer_parent_store_computation=True)
+
+ import_result = model.load(import_fields, data)
_logger.info('done')
# If transaction aborted, RELEASE SAVEPOINT is going to raise
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/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 <me@syrusakbary.com>
-# Extended from (c) 2011 Noel Bush <noel@aitools.org>
-# 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 2c347c09783..7ca677d137b 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'
@@ -272,7 +270,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) will expire soon and should be renewed and/or closed!') % value)
nearly_expired_contracts.write({'state': 'diesoon'})
diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py
index 8a1b02d65fe..140576da889 100644
--- a/addons/gamification/models/challenge.py
+++ b/addons/gamification/models/challenge.py
@@ -503,8 +503,10 @@ class Challenge(models.Model):
line_data['own_goal_id'] = False,
line_data['goals'] = []
-
- goals = Goals.search(domain, order="completeness desc, current desc")
+ if line.condition=='higher':
+ goals = Goals.search(domain, order="completeness desc, current desc")
+ else:
+ goals = Goals.search(domain, order="completeness desc, current asc")
for ranking, goal in enumerate(goals):
if user and goal.user_id == user:
line_data['own_goal_id'] = goal.id
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_account/models/google_service.py b/addons/google_account/models/google_service.py
index 44972435da5..040521a08bd 100644
--- a/addons/google_account/models/google_service.py
+++ b/addons/google_account/models/google_service.py
@@ -9,6 +9,7 @@ import requests
from werkzeug import urls
from odoo import api, fields, models, registry, _
+from odoo.exceptions import UserError
from odoo.http import request
@@ -125,6 +126,9 @@ class GoogleService(models.TransientModel):
client_id = get_param('google_%s_client_id' % (service,), default=False)
client_secret = get_param('google_%s_client_secret' % (service,), default=False)
+ if not client_id or not client_secret:
+ raise UserError(_("The account for the Google service '%s' is not configured") % service)
+
headers = {"content-type": "application/x-www-form-urlencoded"}
data = {
'refresh_token': refresh_token,
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 dc49f6f2022..654cc3543a1 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')
@@ -254,7 +253,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 6b23dc91b1e..dca8da92fb2 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 bf6a1fb8242..3a237e9f283 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/hw_proxy/controllers/main.py b/addons/hw_proxy/controllers/main.py
index 0521b72c89b..02f083110e7 100644
--- a/addons/hw_proxy/controllers/main.py
+++ b/addons/hw_proxy/controllers/main.py
@@ -5,6 +5,7 @@ from __future__ import print_function
import logging
import subprocess
import time
+import subprocess
from threading import Lock
@@ -23,7 +24,7 @@ BANNED_DEVICES = {
}
-# drivers modules must add to drivers an object with a get_status() method
+# drivers modules must add to drivers an object with a get_status() method
# so that 'status' can return the status of all active drivers
drivers = {}
@@ -108,7 +109,7 @@ class Proxy(http.Controller):
if not (device_id in BANNED_DEVICES):
resp += "<div class='device' data-device='"+device+"'>"+device_name+"</div>\n"
count += 1
-
+
if count == 0:
resp += "<div class='device'>No USB Device Found</div>"
@@ -126,7 +127,7 @@ class Proxy(http.Controller):
""" % subprocess.check_output('lsusb -v', shell=True)
return request.make_response(resp,{
- 'Cache-Control': 'no-cache',
+ 'Cache-Control': 'no-cache',
'Content-Type': 'text/html; charset=utf-8',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET',
@@ -167,7 +168,7 @@ class Proxy(http.Controller):
@http.route('/hw_proxy/payment_request', type='json', auth='none', cors='*')
def payment_request(self, price):
"""
- The PoS will activate the method payment
+ The PoS will activate the method payment
"""
print("payment_request: price:%s" % price)
return 'ok'
@@ -175,7 +176,7 @@ class Proxy(http.Controller):
@http.route('/hw_proxy/payment_status', type='json', auth='none', cors='*')
def payment_status(self):
print("payment_status")
- return { 'status':'waiting' }
+ return { 'status':'waiting' }
@http.route('/hw_proxy/payment_cancel', type='json', auth='none', cors='*')
def payment_cancel(self):
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 de968b64374..d78d57e6c66 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..0ffaad046a0 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,9 +12,9 @@ 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:)([^\'"]+)[\'"])'
+URL_REGEX = r'(\bhref=[\'"](?!mailto:|tel:|sms:)([^\'"]+)[\'"])'
def VALIDATE_URL(url):
if urls.url_parse(url).scheme not in ('http', 'https', 'ftp', 'ftps'):
@@ -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 cfa5654b0cd..c1b4433e74a 100644
--- a/addons/mail/controllers/main.py
+++ b/addons/mail/controllers/main.py
@@ -13,7 +13,7 @@ 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__)
@@ -101,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:
@@ -119,7 +119,7 @@ class MailController(http.Controller):
follower_id = None
follower_recs = request.env['mail.followers'].sudo().browse(follower_ids)
res_ids = follower_recs.mapped('res_id')
- request.env[res_model].browse(res_ids).check_access_rule("write")
+ request.env[res_model].browse(res_ids).check_access_rule("read")
for follower in follower_recs:
is_uid = partner_id == follower.partner_id
follower_id = follower.id if is_uid else follower_id
@@ -182,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..32d2e66b028 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):
@@ -24,7 +23,7 @@ class Followers(models.Model):
index=True, required=True, ondelete='cascade',
help='Model of the followed resource')
res_model = fields.Char(
- 'Related Document Model Name', index=True, readonly=True, related='res_model_id.model', store=True)
+ 'Related Document Model Name', index=True, readonly=True, related='res_model_id.model', compute_sudo=True, store=True)
res_id = fields.Integer(
'Related Document ID', index=True, help='Id of the followed resource')
partner_id = fields.Many2one(
@@ -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 583f0320a40..425745142f0 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 = '<div>%s</div>' % html
- root = lxml.html.fromstring(html)
+ html = u'<div>%s</div>' % 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('<div>') and html.endswith('</div>'):
+ if html.startswith(u'<div>') and html.endswith(u'</div>'):
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 60c40a69aa9..6e097196a1b 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/static/src/xml/client_action.xml b/addons/mail/static/src/xml/client_action.xml
index 936f8a91be9..952b93af1a0 100644
--- a/addons/mail/static/src/xml/client_action.xml
+++ b/addons/mail/static/src/xml/client_action.xml
@@ -84,7 +84,7 @@
<t t-name="mail.chat.SidebarItems">
<div t-if="!disable_add_channel" class="o_mail_add_channel" t-attf-data-type="#{channel_type}">
<span t-if="display_hash">#</span>
- <input type="text" t-attf-placeholder="#{input_placeholder}"/>
+ <input type="text" class="o_input" t-attf-placeholder="#{input_placeholder}"/>
</div>
<t t-foreach="channels" t-as="channel">
<t t-set="counter" t-value="channel.is_chat ? channel.unread_counter : channel.needaction_counter"/>
@@ -134,6 +134,6 @@
<!-- Invite dialog's content -->
<div t-name="mail.PartnerInviteDialog">
- <input type="text" class="o_mail_chat_partner_invite_input" id="mail_search_partners"/>
+ <input type="text" class="o_input o_mail_chat_partner_invite_input" id="mail_search_partners"/>
</div>
</templates>
diff --git a/addons/mail/static/src/xml/composer.xml b/addons/mail/static/src/xml/composer.xml
index 93a1ea51e46..032a71edbcd 100644
--- a/addons/mail/static/src/xml/composer.xml
+++ b/addons/mail/static/src/xml/composer.xml
@@ -5,7 +5,7 @@
<div class="o_composer_attachments_list"/>
<t t-if="widget.extended">
<div class="o_composer_subject">
- <input tabindex="1" type="text" placeholder="Subject"></input>
+ <input tabindex="1" type="text" class="o_input" placeholder="Subject"></input>
<div class="btn-group o_composer_buttons">
<button tabindex="3" class="btn btn-sm btn-primary o_composer_button_send" type="button"><t t-esc="widget.options.send_text"/></button>
<button tabindex="4" class="btn btn-sm btn-icon fa fa-smile-o o_composer_button_emoji" type="button" data-toggle="popover"/>
@@ -15,7 +15,7 @@
</t>
<div t-attf-class="o_composer #{widget.extended ? 'o_extended_composer' : ''}">
<div class="o_composer_input">
- <textarea class="o_composer_text_field" tabindex="2" placeholder="Write something..."/>
+ <textarea class="o_input o_composer_text_field" tabindex="2" placeholder="Write something..."/>
</div>
<t t-if="!widget.extended">
<div class="btn-group o_composer_buttons">
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 = '<p>Test Body</p>', '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/views/mail_message_views.xml b/addons/mail/views/mail_message_views.xml
index 75ffea35a5e..19ad37d44a5 100644
--- a/addons/mail/views/mail_message_views.xml
+++ b/addons/mail/views/mail_message_views.xml
@@ -148,11 +148,11 @@
<field name="name"/>
</div>
<div t-if="record.type.raw_value == 'binary'">
- <a t-attf-href="/web/content/#{record.id.value}?download=1">
+ <a t-attf-href="/web/content/#{record.id.raw_value}?download=1">
<t t-set="webimage" t-value="new RegExp('image.*(gif|jpeg|jpg|png)').test(record.mimetype.value)"/>
<div t-if="!webimage" class="o_image" t-att-data-mimetype="record.mimetype.value"/>
<div t-if="webimage" class="o_attachment_image">
- <img t-attf-src="/web/image/#{record.id.value}?download=1"/>
+ <img t-attf-src="/web/image/#{record.id.raw_value}?download=1"/>
</div>
</a>
</div>
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index 76412ce041c..0d91879224e 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:
@@ -295,8 +295,11 @@ class MailComposer(models.TransientModel):
'mail_server_id': self.mail_server_id.id,
'mail_activity_type_id': self.mail_activity_type_id.id,
}
+
# mass mailing: rendering override wizard static values
if mass_mail_mode and self.model:
+ if self.model in self.env and hasattr(self.env[self.model], 'message_get_email_values'):
+ mail_values.update(self.env[self.model].browse(res_id).message_get_email_values())
# keep a copy unless specifically requested, reset record name (avoid browsing records)
mail_values.update(notification=not self.auto_delete_message, model=self.model, res_id=res_id, record_name=False)
# auto deletion of mail_mail
@@ -338,7 +341,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/mass_mailing_event_track/views/event_views.xml b/addons/mass_mailing_event_track/views/event_views.xml
index cf057244f95..6e3e60fc781 100644
--- a/addons/mass_mailing_event_track/views/event_views.xml
+++ b/addons/mass_mailing_event_track/views/event_views.xml
@@ -11,7 +11,6 @@
string="Mail Track Speakers"
icon="fa-envelope-o"
attrs="{'invisible': [('track_count', '=', 0)]}">
- Mail Track Speakers
</button>
</button>
</field>
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/views/ir_attachment_view.xml b/addons/mrp/views/ir_attachment_view.xml
index 49fdf2c6037..7d3e5d47daa 100644
--- a/addons/mrp/views/ir_attachment_view.xml
+++ b/addons/mrp/views/ir_attachment_view.xml
@@ -19,11 +19,11 @@
<div class="oe_attachment">
<div class="row">
<div class="col-xs-4 o_kanban_primary_left" t-if="record.type.raw_value == 'binary'">
- <a t-attf-href="/web/content/#{record.id.value}?download=1">
+ <a t-attf-href="/web/content/#{record.id.raw_value}?download=1">
<t t-set="webimage" t-value="new RegExp('image.*(gif|jpeg|jpg|png)').test(record.mimetype.value)"/>
<div t-if="!webimage" class="o_image" t-att-data-mimetype="record.mimetype.value"/>
<div t-if="webimage" class="o_attachment_image">
- <img t-attf-src="/web/image/#{record.id.value}?download=1"/>
+ <img t-attf-src="/web/image/#{record.id.raw_value}?download=1"/>
</div>
</a>
</div>
diff --git a/addons/mrp/wizard/mrp_product_produce_views.xml b/addons/mrp/wizard/mrp_product_produce_views.xml
index 9a48db5300e..4675092417a 100644
--- a/addons/mrp/wizard/mrp_product_produce_views.xml
+++ b/addons/mrp/wizard/mrp_product_produce_views.xml
@@ -26,7 +26,7 @@
<field name="product_id" readonly="1"/>
<field name="product_qty" readonly="1"/>
<field name="qty_done"/>
- <field name="lot_id" context="{'default_product_id': product_id}"/>
+ <field name="lot_id" context="{'default_product_id': product_id}" domain="[('product_id', '=', product_id)]"/>
<field name="move_id" invisible="1"/>
</tree>
</field>
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/pad/static/tests/pad_tests.js b/addons/pad/static/tests/pad_tests.js
index dedf799db66..f2ab3a42dfa 100644
--- a/addons/pad/static/tests/pad_tests.js
+++ b/addons/pad/static/tests/pad_tests.js
@@ -230,4 +230,37 @@ QUnit.module('pad widget', {
delete FieldPad.prototype.isPadConfigured;
});
+ QUnit.test('record should be discarded properly even if only pad has changed', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'task',
+ data: this.data,
+ arch:'<form>' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="description" widget="pad"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (!args.method) {
+ return $.when(true);
+ }
+ return this._super.apply(this, arguments);
+ },
+ session: {
+ userName: "batman",
+ },
+ });
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$buttons.find('.o_form_button_cancel').click();
+ assert.strictEqual(form.$('.oe_pad_readonly').text(), this.data.task.pad_get_content(),
+ "pad content should not have changed");
+ form.destroy();
+ delete FieldPad.prototype.isPadConfigured;
+ });
+
});
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="<provider>"' attribute, then it
required if record.provider is <provider>. """
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 7d787a54caf..ad792a08987 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/payment_stripe/models/payment.py b/addons/payment_stripe/models/payment.py
index 1bb4ea9aad8..55cd3d690bb 100644
--- a/addons/payment_stripe/models/payment.py
+++ b/addons/payment_stripe/models/payment.py
@@ -17,8 +17,8 @@ STRIPE_HEADERS = {'Stripe-Version': '2016-03-07'}
# The following currencies are integer only, see https://stripe.com/docs/currencies#zero-decimal
INT_CURRENCIES = [
- 'BIF', 'XAF', 'XPF', 'CLP', 'KMF', 'DJF', 'GNF', 'JPY', 'MGA', 'PYGรญ', 'RWF', 'KRW', 'VUV',
- 'VND', 'XOF'
+ u'BIF', u'XAF', u'XPF', u'CLP', u'KMF', u'DJF', u'GNF', u'JPY', u'MGA', u'PYGรญ', u'RWF', u'KRW',
+ u'VUV', u'VND', u'XOF'
];
@@ -154,10 +154,13 @@ class PaymentTransactionStripe(models.Model):
def _stripe_form_get_tx_from_data(self, data):
""" Given a data dict coming from stripe, verify it and find the related
transaction record. """
- reference = data['metadata']['reference']
+ reference = data.get('metadata', {}).get('reference')
if not reference:
- error_msg = _('Stripe: invalid reply received from provider, missing reference')
- _logger.error(error_msg, data['metadata'])
+ error_msg = _(
+ 'Stripe: invalid reply received from provider, missing reference. Additional message: %s'
+ % data.get('error', {}).get('message', '')
+ )
+ _logger.error(error_msg)
raise ValidationError(error_msg)
tx = self.search([('reference', '=', reference)])
if not tx:
diff --git a/addons/payment_stripe/static/src/js/stripe.js b/addons/payment_stripe/static/src/js/stripe.js
index 3a736330657..93dcc606d38 100644
--- a/addons/payment_stripe/static/src/js/stripe.js
+++ b/addons/payment_stripe/static/src/js/stripe.js
@@ -2,6 +2,11 @@ odoo.define('payment_stripe.stripe', function(require) {
"use strict";
var ajax = require('web.ajax');
+ var core = require('web.core');
+ var _t = core._t;
+ var qweb = core.qweb;
+ ajax.loadXML('/payment_stripe/static/src/xml/stripe_templates.xml', qweb);
+
// The following currencies are integer only, see
// https://stripe.com/docs/currencies#zero-decimal
var int_currencies = [
@@ -33,6 +38,10 @@ odoo.define('payment_stripe.stripe', function(require) {
}).done(function(data){
handler.isTokenGenerate = false;
window.location.href = data;
+ }).fail(function(){
+ var msg = arguments && arguments[1] && arguments[1].data && arguments[1].data.message;
+ var wizard = $(qweb.render('stripe.error', {'msg': msg || _t('Payment error')}));
+ wizard.appendTo($('body')).modal({'keyboard': true});
});
},
});
diff --git a/addons/payment_stripe/static/src/xml/stripe_templates.xml b/addons/payment_stripe/static/src/xml/stripe_templates.xml
new file mode 100644
index 00000000000..c5966fe6274
--- /dev/null
+++ b/addons/payment_stripe/static/src/xml/stripe_templates.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates id="template" xml:space="preserve">
+<t t-name="stripe.error">
+ <div class="modal fade">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&amp;times;</button>
+ <h4 class="modal-title">Error</h4>
+ </div>
+ <div class="modal-body">
+ <t t-esc="msg"></t>
+ </div>
+ <div class="modal-footer">
+ <a href="#" class="btn btn-link btn-xs" data-dismiss="modal">Close</a>
+ </div>
+ </div>
+ </div>
+ </div>
+</t>
+</templates>
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index c7281942cf5..a0097b7ccd3 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
@@ -334,7 +334,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'])
@@ -352,7 +352,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
@@ -1033,7 +1033,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,
@@ -1042,7 +1042,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/point_of_sale/static/src/js/tests.js b/addons/point_of_sale/static/src/js/tests.js
index ec6ed65e3fe..36fd7a49f8a 100644
--- a/addons/point_of_sale/static/src/js/tests.js
+++ b/addons/point_of_sale/static/src/js/tests.js
@@ -315,7 +315,8 @@ odoo.define('point_of_sale.tour.acceptance', function (require) {
trigger: ".header-button",
}, {
content: "confirm closing the frontend",
- trigger: ".header-button",
+ trigger: ".header-button.confirm",
+ run: function() {}, //it's a check,
}]);
Tour.register('pos_basic_order', { test: true, url: '/pos/web' }, steps);
diff --git a/addons/portal/controllers/portal.py b/addons/portal/controllers/portal.py
index 1f5019a9810..b8e01be96c3 100644
--- a/addons/portal/controllers/portal.py
+++ b/addons/portal/controllers/portal.py
@@ -129,11 +129,13 @@ class CustomerPortal(Controller):
@route(['/my/account'], type='http', auth='user', website=True)
def account(self, redirect=None, **post):
+ values = self._prepare_portal_layout_values()
partner = request.env.user.partner_id
- values = {
+ values.update({
'error': {},
- 'error_message': []
- }
+ 'error_message': [],
+ 'hide_current_user': True,
+ })
if post:
error, error_message = self.details_form_validate(post)
@@ -192,7 +194,7 @@ class CustomerPortal(Controller):
error["vat"] = 'error'
# error message for empty required fields
- if [err for err in tools.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/portal/views/portal_templates.xml b/addons/portal/views/portal_templates.xml
index 87e566b874e..8c4e99809c5 100644
--- a/addons/portal/views/portal_templates.xml
+++ b/addons/portal/views/portal_templates.xml
@@ -85,7 +85,7 @@
<div class="o_my_contact" t-if="sales_user">
<t t-call="portal.portal_contact"/>
</div>
- <div class="o_portal_my_details">
+ <div class="o_portal_my_details" t-if="not hide_current_user and user">
<h3 class="page-header">Your Details <a href="/my/account" class="btn btn-default btn-xs">Change</a></h3>
<div t-field="user_id.partner_id" t-options='{"widget": "contact", "fields": ["email", "phone", "address", "name"]}'/>
</div>
diff --git a/addons/portal/wizard/portal_wizard_views.xml b/addons/portal/wizard/portal_wizard_views.xml
index b1cb31e63ba..909b822cf8d 100644
--- a/addons/portal/wizard/portal_wizard_views.xml
+++ b/addons/portal/wizard/portal_wizard_views.xml
@@ -25,7 +25,7 @@
</div>
<field name="user_ids">
<tree string="Contacts" editable="bottom" create="false" delete="false">
- <field name="partner_id"/>
+ <field name="partner_id" force_save="1"/>
<field name="email"/>
<field name="in_portal"/>
</tree>
diff --git a/addons/pos_restaurant/static/src/js/multiprint.js b/addons/pos_restaurant/static/src/js/multiprint.js
index 3f2d482c1e9..dac0a906850 100644
--- a/addons/pos_restaurant/static/src/js/multiprint.js
+++ b/addons/pos_restaurant/static/src/js/multiprint.js
@@ -191,7 +191,12 @@ models.Order = models.Order.extend({
var product_id = line.get_product().id;
if (typeof resume[line_hash] === 'undefined') {
- resume[line_hash] = { qty: qty, note: note, product_id: product_id };
+ resume[line_hash] = {
+ qty: qty,
+ note: note,
+ product_id: product_id,
+ product_name_wrapped: line.generate_wrapped_product_name(),
+ };
} else {
resume[line_hash].qty += qty;
}
@@ -222,6 +227,7 @@ models.Order = models.Order.extend({
add.push({
'id': curr.product_id,
'name': this.pos.db.get_product_by_id(curr.product_id).display_name,
+ 'name_wrapped': curr.product_name_wrapped,
'note': curr.note,
'qty': curr.qty,
});
@@ -229,6 +235,7 @@ models.Order = models.Order.extend({
add.push({
'id': curr.product_id,
'name': this.pos.db.get_product_by_id(curr.product_id).display_name,
+ 'name_wrapped': curr.product_name_wrapped,
'note': curr.note,
'qty': curr.qty - old.qty,
});
@@ -236,6 +243,7 @@ models.Order = models.Order.extend({
rem.push({
'id': curr.product_id,
'name': this.pos.db.get_product_by_id(curr.product_id).display_name,
+ 'name_wrapped': curr.product_name_wrapped,
'note': curr.note,
'qty': old.qty - curr.qty,
});
@@ -248,6 +256,7 @@ models.Order = models.Order.extend({
rem.push({
'id': old.product_id,
'name': this.pos.db.get_product_by_id(old.product_id).display_name,
+ 'name_wrapped': old.product_name_wrapped,
'note': old.note,
'qty': old.qty,
});
diff --git a/addons/pos_restaurant/static/src/xml/multiprint.xml b/addons/pos_restaurant/static/src/xml/multiprint.xml
index 4f3d0cf7379..282d07421c2 100644
--- a/addons/pos_restaurant/static/src/xml/multiprint.xml
+++ b/addons/pos_restaurant/static/src/xml/multiprint.xml
@@ -8,6 +8,15 @@
</span>
</t>
+ <t t-name="NameWrapped">
+ <t t-foreach="change.name_wrapped.slice(1)" t-as="wrapped_line">
+ <line>
+ <left></left>
+ <right><t t-esc="wrapped_line"/></right>
+ </line>
+ </t>
+ </t>
+
<t t-name="OrderChangeReceipt">
<receipt
align='center'
@@ -33,8 +42,9 @@
<t t-foreach="changes.cancelled" t-as="change">
<line>
<left><value><t t-esc="change.qty" /></value></left>
- <right><t t-esc="change.name" /></right>
+ <right><t t-esc="change.name_wrapped[0]" /></right>
</line>
+ <t t-call="NameWrapped"/>
<t t-if="change.note">
<line>
<left>NOTE</left>
@@ -55,8 +65,9 @@
<t t-foreach="changes.new" t-as="change">
<line>
<left><value><t t-esc="change.qty" /></value></left>
- <right><t t-esc="change.name" /></right>
+ <right><t t-esc="change.name_wrapped[0]" /></right>
</line>
+ <t t-call="NameWrapped"/>
<t t-if="change.note">
<line>
<left>NOTE</left>
diff --git a/addons/product/models/product.py b/addons/product/models/product.py
index 3466edc33ed..8437bf16d74 100644
--- a/addons/product/models/product.py
+++ b/addons/product/models/product.py
@@ -154,7 +154,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 646a813c63c..38fb9d4d1e1 100644
--- a/addons/product/models/product_template.py
+++ b/addons/product/models/product_template.py
@@ -170,7 +170,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])
@@ -417,9 +417,9 @@ class ProductTemplate(models.Model):
variants_to_activate = self.env['product.product']
variants_to_unlink = self.env['product.product']
for product_id in tmpl_id.product_variant_ids:
- if not product_id.active and product_id.attribute_value_ids in variant_matrix:
+ if not product_id.active and product_id.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant) in variant_matrix:
variants_to_activate |= product_id
- elif product_id.attribute_value_ids not in variant_matrix:
+ elif product_id.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant) not in variant_matrix:
variants_to_unlink |= product_id
if variants_to_activate:
variants_to_activate.write({'active': True})
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_extended/views/product_views.xml b/addons/product_extended/views/product_views.xml
index 2867a33f868..4e065e19604 100644
--- a/addons/product_extended/views/product_views.xml
+++ b/addons/product_extended/views/product_views.xml
@@ -23,6 +23,7 @@
<field name="model">product.product</field>
<field name="priority">4</field>
<field name="inherit_id" ref="product.product_normal_form_view"/>
+ <field name="groups_id" eval="[(4, ref('mrp.group_mrp_user'))]"/>
<field name="arch" type="xml">
<div name="standard_price_uom" position="after">
<button name="compute_price"
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/controllers/portal.py b/addons/project/controllers/portal.py
index 76079d9bdff..621113a385b 100644
--- a/addons/project/controllers/portal.py
+++ b/addons/project/controllers/portal.py
@@ -8,7 +8,6 @@ from odoo.http import request
from odoo.addons.portal.controllers.portal import get_records_pager, CustomerPortal, pager as portal_pager
from odoo.osv.expression import OR
-from odoo.tools import pycompat
class CustomerPortal(CustomerPortal):
@@ -157,7 +156,7 @@ class CustomerPortal(CustomerPortal):
'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("project.portal_my_tasks", values)
diff --git a/addons/project/models/project.py b/addons/project/models/project.py
index b2b9fccab1c..756283820d9 100644
--- a/addons/project/models/project.py
+++ b/addons/project/models/project.py
@@ -583,7 +583,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/controllers/portal.py b/addons/purchase/controllers/portal.py
index 2e8c77bbfa3..4a169daba44 100644
--- a/addons/purchase/controllers/portal.py
+++ b/addons/purchase/controllers/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.portal.controllers.portal import get_records_pager, pager as portal_pager, CustomerPortal
@@ -88,7 +87,7 @@ class CustomerPortal(CustomerPortal):
'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/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 265e72b9179..36ad3a2cfba 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
@@ -73,13 +72,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/resource/models/resource_mixin.py b/addons/resource/models/resource_mixin.py
index 0d45ec646a1..f1d20cc6772 100644
--- a/addons/resource/models/resource_mixin.py
+++ b/addons/resource/models/resource_mixin.py
@@ -4,7 +4,7 @@
from datetime import timedelta
from odoo import api, fields, models
-from odoo.tools import pycompat
+from odoo.tools import float_utils
class ResourceMixin(models.AbstractModel):
_name = "resource.mixin"
@@ -47,7 +47,7 @@ class ResourceMixin(models.AbstractModel):
theoric_hours = self.get_day_work_hours_count(day_intervals[0][0].date(), calendar=calendar)
work_time = sum((interval[1] - interval[0] for interval in day_intervals), timedelta())
total_work_time += work_time
- days_count += pycompat.round((work_time.total_seconds() / 3600 / theoric_hours) * 4) / 4
+ days_count += float_utils.round((work_time.total_seconds() / 3600 / theoric_hours) * 4) / 4
return {
'days': days_count,
'hours': total_work_time.total_seconds() / 3600,
@@ -70,7 +70,7 @@ class ResourceMixin(models.AbstractModel):
for day_intervals in calendar._iter_leave_intervals(from_datetime, to_datetime, self.resource_id.id):
theoric_hours = self.get_day_work_hours_count(day_intervals[0][0].date(), calendar=calendar)
leave_time = sum((interval[1] - interval[0] for interval in day_intervals), timedelta())
- days_count += pycompat.round((leave_time.total_seconds() / 3600 / theoric_hours) * 4) / 4
+ days_count += float_utils.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 b10d8d407fd..1cf260da62e 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 fbfe45bcdb1..02f8710da6f 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -9,7 +9,7 @@ from werkzeug.urls import url_encode
from odoo import api, fields, models, _
from odoo.exceptions import UserError, AccessError
-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
@@ -404,7 +404,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
@@ -421,7 +421,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):
@@ -561,7 +561,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/views/sale_views.xml b/addons/sale/views/sale_views.xml
index 7cdcfbdb229..eafe4222161 100644
--- a/addons/sale/views/sale_views.xml
+++ b/addons/sale/views/sale_views.xml
@@ -289,9 +289,9 @@
<div>
<field name="customer_lead" class="oe_inline"/> days
</div>
- <label for="analytic_tag_ids"/>
+ <label for="analytic_tag_ids" groups="analytic.group_analytic_accounting"/>
<div>
- <field name="analytic_tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
+ <field name="analytic_tag_ids" widget="many2many_tags" groups="analytic.group_analytic_accounting" options="{'color_field': 'color'}"/>
</div>
</group>
</group>
diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py
index 5c0c8091dbe..16c122cb9af 100644
--- a/addons/sale_stock/models/sale_order.py
+++ b/addons/sale_stock/models/sale_order.py
@@ -63,7 +63,7 @@ class SaleOrder(models.Model):
@api.multi
def action_cancel(self):
- self.order_line.mapped('procurement_ids').cancel()
+ self.mapped('order_line').mapped('procurement_ids').cancel()
return super(SaleOrder, self).action_cancel()
@api.multi
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/sale_timesheet/controllers/main.py b/addons/sale_timesheet/controllers/main.py
index 7aff42ff99d..975b79523a9 100644
--- a/addons/sale_timesheet/controllers/main.py
+++ b/addons/sale_timesheet/controllers/main.py
@@ -59,7 +59,7 @@ class SaleTimesheetController(http.Controller):
values['dashboard'] = dashboard_values
# -- Time Repartition (per employee)
- repartition_domain = domain + [('timesheet_invoice_type', '!=', False)] # force billable type
+ repartition_domain = domain + [('employee_id', '!=', False), ('timesheet_invoice_type', '!=', False)] # force billable type
repartition_data = request.env['account.analytic.line'].read_group(repartition_domain, ['employee_id', 'timesheet_invoice_type', 'unit_amount'], ['employee_id', 'timesheet_invoice_type'], lazy=False)
# set repartition per type per employee
diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py
index 155246b1c0c..eec7637be31 100644
--- a/addons/sale_timesheet/models/account.py
+++ b/addons/sale_timesheet/models/account.py
@@ -27,9 +27,9 @@ class AccountAnalyticLine(models.Model):
@api.multi
def write(self, values):
# prevent to update invoiced timesheets
- if self.filtered(lambda timesheet: timesheet.timesheet_invoice_id):
- if any([field_name in values for field_name in ['unit_amount', 'employee_id', 'task_id', 'timesheet_revenue', 'so_line', 'amount', 'date']]):
- raise UserError(_('You can not modify already invoiced timesheets.'))
+ #if self.filtered(lambda timesheet: timesheet.timesheet_invoice_id):
+ # if any([field_name in values for field_name in ['unit_amount', 'employee_id', 'task_id', 'timesheet_revenue', 'so_line', 'amount', 'date']]):
+ # raise UserError(_('You can not modify already invoiced timesheets.'))
so_lines = self.mapped('so_line')
if values.get('task_id'):
@@ -97,26 +97,35 @@ class AccountAnalyticLine(models.Model):
if self.env.context.get('create'): # avoid bad loop
return result
+ # find the timesheet UoM
+ timesheet_uom = self.product_uom_id
+ if values.get('product_uom_id'):
+ timesheet_uom = self.env['product.uom'].browse(values['product_uom_id'])
+ if not timesheet_uom: # fallback on default company timesheet UoM
+ timesheet_uom = self.env.user.company_id.project_time_mode_id
+
unit_amount = values.get('unit_amount', 0.0) or self.unit_amount
billable_type = 'non_billable'
revenue = 0.0
# set the revenue and billable type according to the product and the SO line
so_line_id = values.get('so_line') or self.so_line.id
- so_line = self.env['sale.order.line'].browse(so_line_id) if so_line_id else self.env['sale.order.line'].browse()
+ so_line = self.env['sale.order.line'].browse(so_line_id).sudo() if so_line_id else self.env['sale.order.line'].browse().sudo()
if so_line.product_id.type == 'service':
# find the analytic account to convert revenue into its currency
account_id = values.get('account_id') or self.account_id.id
analytic_account = self.env['account.analytic.account'].browse(account_id)
+ # convert the unit of mesure into hours
+ sale_price_hour = so_line.product_uom._compute_price(so_line.price_unit, timesheet_uom)
+ sale_price = so_line.currency_id.compute(sale_price_hour, analytic_account.currency_id) # amount from SO should be convert into analytic account currency
# calculate the revenue on the timesheet
if so_line.product_id.invoice_policy == 'delivery':
- sale_price_unit = so_line.currency_id.compute(so_line.price_unit, analytic_account.currency_id) # amount from SO should be convert into analytic account currency
- revenue = analytic_account.currency_id.round(unit_amount * sale_price_unit * (1-so_line.discount))
+ revenue = analytic_account.currency_id.round(unit_amount * sale_price * (1-(so_line.discount/100)))
billable_type = 'billable_time'
elif so_line.product_id.invoice_policy == 'order' and so_line.product_id.track_service == 'task':
+ quantity_hour = so_line.product_uom._compute_quantity(so_line.product_uom_qty, timesheet_uom)
# compute the total revenue the SO since we are in fixed price
- sale_price_unit = so_line.currency_id.compute(so_line.price_unit, analytic_account.currency_id)
- total_revenue_so = analytic_account.currency_id.round(so_line.product_uom_qty * sale_price_unit * (1-so_line.discount))
+ total_revenue_so = analytic_account.currency_id.round(quantity_hour * sale_price * (1-(so_line.discount/100)))
# compute the total revenue already existing (without the current timesheet line)
domain = [('so_line', '=', so_line.id)]
if self.ids:
diff --git a/addons/sale_timesheet/tests/test_timesheet_revenue.py b/addons/sale_timesheet/tests/test_timesheet_revenue.py
index af7d1f6dbda..9c5ceb79bac 100644
--- a/addons/sale_timesheet/tests/test_timesheet_revenue.py
+++ b/addons/sale_timesheet/tests/test_timesheet_revenue.py
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.sale.tests.test_sale_common import TestSale
-from odoo.tools import float_compare
+from odoo.tools import float_compare, float_repr
class TestSaleTimesheet(TestSale):
@@ -283,10 +283,10 @@ class TestSaleTimesheet(TestSale):
invoice.action_invoice_open()
# check concrete revenue
- self.assertEquals(float_compare(timesheet1.timesheet_revenue, 385.85, precision_digits=2), 0, "Revenue computation on invoice validation does not return the correct revenue !")
- self.assertEquals(float_compare(timesheet2.timesheet_revenue, 154.35, precision_digits=2), 0, "Revenue computation on invoice validation does not return the correct revenue !")
- self.assertEquals(float_compare(timesheet3.timesheet_revenue, 114.5, precision_digits=2), 0, "Revenue computation on invoice validation does not return the correct revenue !")
- self.assertEquals(float_compare(timesheet4.timesheet_revenue, 152.68, precision_digits=2), 0, "Revenue computation on invoice validation does not return the correct revenue !")
+ self.assertEquals(float_repr(timesheet1.timesheet_revenue, precision_digits=2), '385.85', "Revenue computation on invoice validation does not return the correct revenue !")
+ self.assertEquals(float_repr(timesheet2.timesheet_revenue, precision_digits=2), '154.35', "Revenue computation on invoice validation does not return the correct revenue !")
+ self.assertEquals(float_repr(timesheet3.timesheet_revenue, precision_digits=2), '114.50', "Revenue computation on invoice validation does not return the correct revenue !")
+ self.assertEquals(float_repr(timesheet4.timesheet_revenue, precision_digits=2), '152.68', "Revenue computation on invoice validation does not return the correct revenue !")
# check the invoice is well set
self.assertEquals(timesheet1.timesheet_invoice_id, invoice)
diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml
index b8b02461c44..56a88c7a28d 100644
--- a/addons/sale_timesheet/views/project_task_views.xml
+++ b/addons/sale_timesheet/views/project_task_views.xml
@@ -32,4 +32,18 @@
</field>
</field>
</record>
+
+ <record id="project_task_view_form_inherit_sale_timesheet" model="ir.ui.view">
+ <field name="name">project.task.form.inherit.timesheet</field>
+ <field name="model">project.task</field>
+ <field name="inherit_id" ref="hr_timesheet.view_task_form2_inherited"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='timesheet_ids']/tree" position="attributes">
+ <attribute name="decoration-muted">timesheet_invoice_id != False</attribute>
+ </xpath>
+ <xpath expr="//field[@name='timesheet_ids']/tree" position="inside">
+ <field name="timesheet_invoice_id" invisible="1"/>
+ </xpath>
+ </field>
+ </record>
</odoo>
diff --git a/addons/sales_team/models/crm_team.py b/addons/sales_team/models/crm_team.py
index 5aeaba6aeec..7a17403933b 100644
--- a/addons/sales_team/models/crm_team.py
+++ b/addons/sales_team/models/crm_team.py
@@ -239,7 +239,7 @@ class CrmTeam(models.Model):
for data_item in graph_data:
index = int((data_item.get('x_value') - start_date.isocalendar()[1]) % weeks_in_start_year)
- values[index][y_field] = int(data_item.get('y_value'))
+ values[index][y_field] = data_item.get('y_value')
elif self.dashboard_graph_group == 'month':
for month in range(0, (end_date.month - start_date.month) % 12 + 1):
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..55e63bb4f9f 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):
@@ -202,6 +202,19 @@ class Inventory(models.Model):
prepare_inventory = action_start
@api.multi
+ def action_inventory_line_tree(self):
+ action = self.env.ref('stock.action_inventory_line_tree').read()[0]
+ action['context'] = {
+ 'default_location_id': self.location_id.id,
+ 'default_product_id': self.product_id.id,
+ 'default_prod_lot_id': self.lot_id.id,
+ 'default_package_id': self.package_id.id,
+ 'default_partner_id': self.partner_id.id,
+ 'default_inventory_id': self.id,
+ }
+ return action
+
+ @api.multi
def _get_inventory_lines_values(self):
# TDE CLEANME: is sql really necessary ? I don't think so
locations = self.env['stock.location'].search([('id', 'child_of', [self.location_id.id])])
@@ -251,7 +264,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']:
@@ -334,6 +347,8 @@ class InventoryLine(models.Model):
theoretical_qty = fields.Float(
'Theoretical Quantity', compute='_compute_theoretical_qty',
digits=dp.get_precision('Product Unit of Measure'), readonly=True, store=True)
+ inventory_location_id = fields.Many2one(
+ 'stock.location', 'Location', related='inventory_id.location_id', related_sudo=False)
@api.one
@api.depends('location_id', 'product_id', 'package_id', 'product_uom_id', 'company_id', 'prod_lot_id', 'partner_id')
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index 3d09ff08949..6379289bfe6 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
@@ -211,7 +211,8 @@ class StockMove(models.Model):
@api.one
@api.depends('product_id', 'product_uom', 'product_uom_qty')
def _compute_product_qty(self):
- self.product_qty = self.product_uom._compute_quantity(self.product_uom_qty, self.product_id.uom_id)
+ rounding_method = self._context.get('rounding_method', 'UP')
+ self.product_qty = self.product_uom._compute_quantity(self.product_uom_qty, self.product_id.uom_id, rounding_method=rounding_method)
def _get_move_lines(self):
""" This will return the move lines to consider when applying _quantity_done_compute on a stock.move.
@@ -595,7 +596,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 +768,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 = {k: v for k, v in pycompat.items(available_move_lines) if v}
+ available_move_lines = dict((k, v) for k, v in available_move_lines.items() if v)
if not available_move_lines:
continue
@@ -954,13 +955,13 @@ class StockMove(models.Model):
# TDE CLEANME: remove context key + add as parameter
if self.env.context.get('source_location_id'):
defaults['location_id'] = self.env.context['source_location_id']
- new_move = self.copy(defaults)
+ new_move = self.with_context(rounding_method='HALF-UP').copy(defaults)
# ctx = context.copy()
# TDE CLEANME: used only in write in this file, to clean
# ctx['do_not_propagate'] = True
# FIXME: pim fix your crap
- self.with_context(do_not_propagate=True, do_not_unreserve=True).write({'product_uom_qty': self.product_uom_qty - uom_qty})
+ self.with_context(do_not_propagate=True, do_not_unreserve=True, rounding_method='HALF-UP').write({'product_uom_qty': self.product_uom_qty - uom_qty})
# if self.move_dest_id and self.propagate and self.move_dest_id.state not in ('done', 'cancel'):
# new_move_prop = self.move_dest_id.split(qty)
diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py
index fd2e084f969..124f67ef6e2 100644
--- a/addons/stock/models/stock_move_line.py
+++ b/addons/stock/models/stock_move_line.py
@@ -96,6 +96,11 @@ class StockMoveLine(models.Model):
res = {'domain': {'product_uom_id': []}}
return res
+ @api.onchange('lot_name', 'lot_id')
+ def onchange_serial_number(self):
+ if self.product_id.tracking == 'serial':
+ self.qty_done = 1
+
@api.model
def create(self, vals):
vals['ordered_qty'] = vals.get('product_uom_qty')
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 785cff1db58..582d24d1033 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
@@ -268,13 +267,15 @@ class QuantPackage(models.Model):
def _compute_package_info(self):
for package in self:
values = {'location_id': False, 'company_id': self.env.user.company_id.id, 'owner_id': False}
+ if self.quant_ids:
+ values['location_id'] = self.quant_ids[0].location_id
package.location_id = values['location_id']
package.company_id = values['company_id']
package.owner_id = values['owner_id']
@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/views/stock_inventory_views.xml b/addons/stock/views/stock_inventory_views.xml
index 9348886f2e1..a7a63adaea2 100644
--- a/addons/stock/views/stock_inventory_views.xml
+++ b/addons/stock/views/stock_inventory_views.xml
@@ -14,6 +14,40 @@
</field>
</record>
+ <record id="stock_inventory_line_tree2" model="ir.ui.view">
+ <field name="name">stock.inventory.line.tree2</field>
+ <field name="model">stock.inventory.line</field>
+ <field name="arch" type="xml">
+ <tree editable="top" string="Inventory Details" decoration-info="product_qty != theoretical_qty" decoration-danger="theoretical_qty &lt; 0">
+ <field name="product_id" domain="[('type','=','product')]"/>
+ <field name="product_uom_id" string="UoM" groups="product.group_uom"/>
+ <field name="location_id" domain="[('id', 'child_of', inventory_location_id)]" groups="stock.group_stock_multi_locations"/>
+ <field name="prod_lot_id" domain="[('product_id', '=', product_id)]" context="{'default_product_id': product_id}" groups="stock.group_production_lot"/>
+ <field name="package_id" domain="['|', ('location_id','=', False), ('location_id', '=', location_id)]" groups="stock.group_tracking_lot"/>
+ <field name="partner_id" groups="stock.group_tracking_owner"/>
+ <field name="theoretical_qty" readonly="1"/>
+ <field name="product_qty" string="Real Quantity"/>
+ <field name="state" invisible="1"/>
+ <field name="inventory_id" invisible="1"/>
+ <field name="inventory_location_id" invisible="1"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="stock_inventory_line_search" model="ir.ui.view">
+ <field name="name">stock.inventory.line.search</field>
+ <field name="model">stock.inventory.line</field>
+ <field name="arch" type="xml">
+ <search string="Search Inventory Lines">
+ <field name="product_id"/>
+ <field name="location_id" groups="stock.group_stock_multi_locations"/>
+ <field name="prod_lot_id" groups="stock.group_production_lot"/>
+ <field name="package_id" groups="stock.group_tracking_lot"/>
+ <field name="partner_id" groups="stock.group_tracking_owner"/>
+ </search>
+ </field>
+ </record>
+
<record id="view_inventory_filter" model="ir.ui.view">
<field name="name">stock.inventory.filter</field>
<field name="model">stock.inventory</field>
@@ -87,6 +121,19 @@
<field name="state" widget="statusbar" statusbar_visible="draft,confirm,done"/>
</header>
<sheet>
+ <div class="oe_button_box" name="button_box">
+ <button name="action_inventory_line_tree"
+ class="oe_stat_button"
+ icon="fa-building-o"
+ type="object"
+ help="List view of lines"
+ groups="base.group_no_one"
+ states="confirm">
+ <div class="o_form_field o_stat_info">
+ <span class="o_stat_text">Details</span>
+ </div>
+ </button>
+ </div>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1><field name="name" placeholder="e.g. Annual inventory"/></h1>
@@ -213,5 +260,16 @@
</p>
</field>
</record>
+
+ <record id="action_inventory_line_tree" model="ir.actions.act_window">
+ <field name="name">Inventory Lines</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">stock.inventory.line</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree</field>
+ <field name="view_id" ref="stock_inventory_line_tree2"/>
+ <field name="domain">[('inventory_id', '=', active_id)]</field>
+ </record>
+
<menuitem action="action_inventory_form" id="menu_action_inventory_form" parent="menu_stock_inventory_control" sequence="30"/>
-</odoo> \ No newline at end of file
+</odoo>
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_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 77eff220e0d..99faaaa5b8e 100644
--- a/addons/survey/models/survey.py
+++ b/addons/survey/models/survey.py
@@ -12,8 +12,6 @@ from werkzeug import urls
from odoo import api, fields, models, tools, SUPERUSER_ID, _
from odoo.addons.http_routing.models.ir_http import slug
from odoo.exceptions import UserError, ValidationError
-from odoo.tools import pycompat
-
email_validator = re.compile(r"[^@]+@[^@]+\.[^@]+")
_logger = logging.getLogger(__name__)
@@ -23,7 +21,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):
@@ -237,7 +235,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':
@@ -637,7 +635,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 a6c0443f160..c767b2ece4d 100644
--- a/addons/survey/tests/test_survey.py
+++ b/addons/survey/tests/test_survey.py
@@ -12,8 +12,6 @@ from odoo import _
from odoo.addons.http_routing.models.ir_http import slug
from odoo.exceptions import UserError
from odoo.tests.common import TransactionCase
-from odoo.tools import pycompat
-
class TestSurvey(TransactionCase):
@@ -178,7 +176,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))
@@ -210,8 +208,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({
@@ -264,7 +262,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 4e37cb482fd..f0a6650f3a6 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)
@@ -964,7 +964,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/<string:xmlid>',
@@ -1137,8 +1137,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:
@@ -1217,7 +1217,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 = []
@@ -1228,7 +1228,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
@@ -1320,7 +1320,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
@@ -1388,32 +1388,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
@@ -1446,7 +1436,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
@@ -1454,7 +1444,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/models/ir_http.py b/addons/web/models/ir_http.py
index 36ffe593acb..23a87f4cd02 100644
--- a/addons/web/models/ir_http.py
+++ b/addons/web/models/ir_http.py
@@ -5,8 +5,6 @@ import json
from odoo import models
from odoo.http import request
-import pytz
-import datetime
import odoo
@@ -26,7 +24,6 @@ class Http(models.AbstractModel):
version_info = odoo.service.common.exp_version()
return {
"session_id": request.session.sid,
- "tzOffset": self._context.get('tz') and datetime.datetime.now(pytz.timezone(self._context['tz'])).utcoffset().total_seconds() / 60 or False,
"uid": request.session.uid,
"is_system": request.env.user._is_system(),
"is_superuser": request.env.user._is_superuser(),
diff --git a/addons/web/static/src/js/chrome/search_inputs.js b/addons/web/static/src/js/chrome/search_inputs.js
index 2a265ff0fdd..2dba4c0dd99 100644
--- a/addons/web/static/src/js/chrome/search_inputs.js
+++ b/addons/web/static/src/js/chrome/search_inputs.js
@@ -232,7 +232,7 @@ var IntegerField = NumberField.extend(/** @lends instance.web.search.IntegerFiel
error_message: _t("not a valid integer"),
parse: function (value) {
try {
- return field_utils.parse.interger(value);
+ return field_utils.parse.integer(value);
} catch (e) {
return NaN;
}
diff --git a/addons/web/static/src/js/chrome/view_manager.js b/addons/web/static/src/js/chrome/view_manager.js
index 5962bb28acb..2dae906c6db 100644
--- a/addons/web/static/src/js/chrome/view_manager.js
+++ b/addons/web/static/src/js/chrome/view_manager.js
@@ -26,7 +26,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
search: function(event) {
var d = event.data;
_.extend(this.env, this._process_search_data(d.domains, d.contexts, d.groupbys));
- this.active_view.controller.reload(this.env);
+ this.active_view.controller.reload(_.extend({}, this.env));
},
switch_view: function(event) {
if ('res_id' in event.data) {
diff --git a/addons/web/static/src/js/core/session.js b/addons/web/static/src/js/core/session.js
index c21a855bab5..065b3b360e5 100644
--- a/addons/web/static/src/js/core/session.js
+++ b/addons/web/static/src/js/core/session.js
@@ -290,10 +290,6 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
var result = _.extend({}, window.odoo.session_info);
delete result.session_id;
_.extend(this, result);
- if (this.tzOffset === false) {
- // send by server to have the user timezone and not the browser timezone
- this.tzOffset = -new Date().getTimezoneOffset();
- }
return $.when();
},
check_session_id: function () {
@@ -415,6 +411,17 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
}) ? '' : this.prefix;
return prefix + path + qs;
},
+ /**
+ * Returns the time zone difference (in minutes) from the current locale
+ * (host system settings) to UTC, for a given date. The offset is positive
+ * if the local timezone is behind UTC, and negative if it is ahead.
+ *
+ * @param {string || moment} date a valid string date or moment instance
+ * @returns {integer}
+ */
+ getTZOffset: function (date) {
+ return -new Date(date).getTimezoneOffset();
+ },
});
return Session;
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 2d18a0ddb56..a1f87a38fd2 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -341,6 +341,9 @@ var FieldChar = InputField.extend(TranslatableFieldMixin, {
*/
_renderEdit: function () {
var def = this._super.apply(this, arguments);
+ if (this.field.size && this.field.size > 0) {
+ this.$el.attr('maxlength', this.field.size);
+ }
this.$el = this.$el.add(this._renderTranslateButton());
return def;
},
@@ -450,7 +453,7 @@ var FieldDateTime = FieldDate.extend({
*/
_getValue: function () {
var value = this.datewidget.getValue();
- return value && value.add(-this.getSession().tzOffset, 'minutes');
+ return value && value.add(-this.getSession().getTZOffset(value), 'minutes');
},
/**
@@ -460,7 +463,7 @@ var FieldDateTime = FieldDate.extend({
* @private
*/
_makeDatePicker: function () {
- var value = this.value && this.value.clone().add(this.getSession().tzOffset, 'minutes');
+ var value = this.value && this.value.clone().add(this.getSession().getTZOffset(this.value), 'minutes');
return new datepicker.DateTimeWidget(this, {defaultDate: value});
},
@@ -471,7 +474,7 @@ var FieldDateTime = FieldDate.extend({
* @private
*/
_renderEdit: function () {
- var value = this.value && this.value.clone().add(this.getSession().tzOffset, 'minutes');
+ var value = this.value && this.value.clone().add(this.getSession().getTZOffset(this.value), 'minutes');
this.datewidget.setValue(value);
this.$input = this.datewidget.$input;
},
@@ -791,6 +794,7 @@ var FieldFloatTime = FieldFloat.extend({
var FieldText = InputField.extend(TranslatableFieldMixin, {
className: 'o_field_text',
supportedFieldTypes: ['text'],
+ tagName: 'span',
/**
* @constructor
diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js
index 9d3ae75af92..cbb2a856337 100644
--- a/addons/web/static/src/js/fields/field_utils.js
+++ b/addons/web/static/src/js/fields/field_utils.js
@@ -92,8 +92,10 @@ function formatDate(value, field, options) {
if (value === false) {
return "";
}
- if (!options || !('timezone' in options) || options.timezone) {
- value = value.clone().add(session.tzOffset < 0 ? -1 : 0, 'days');
+ if (field && field.type === 'datetime') {
+ if (!options || !('timezone' in options) || options.timezone) {
+ value = value.clone().add(session.getTZOffset(value), 'minutes');
+ }
}
var l10n = core._t.database.parameters;
var date_format = time.strftime_to_moment_format(l10n.date_format);
@@ -118,7 +120,7 @@ function formatDateTime(value, field, options) {
return "";
}
if (!options || !('timezone' in options) || options.timezone) {
- value = value.clone().add(session.tzOffset, 'minutes');
+ value = value.clone().add(session.getTZOffset(value), 'minutes');
}
var l10n = core._t.database.parameters;
var date_format = time.strftime_to_moment_format(l10n.date_format);
@@ -354,11 +356,8 @@ function parseDate(value, field, options) {
date = moment.utc(value);
} else {
date = moment.utc(value, [datePattern, datePatternWoZero, moment.ISO_8601], true);
- if (options && options.timezone) {
- date.add(session.tzOffset > 0 ? -1 : 0, 'days');
- }
}
- if (date.isValid() && date.year() >= 1900) {
+ if (date.isValid()) {
if (date.year() === 0) {
date.year(moment.utc().year());
}
@@ -402,7 +401,7 @@ function parseDateTime(value, field, options) {
} else {
datetime = moment.utc(value, [pattern1, pattern2, moment.ISO_8601], true);
if (options && options.timezone) {
- datetime.add(-session.tzOffset, 'minutes');
+ datetime.add(-session.getTZOffset(datetime), 'minutes');
}
}
if (datetime.isValid()) {
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 0dce6ec2bb4..e05a53194c3 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -21,7 +21,6 @@ var dialogs = require('web.view_dialogs');
var core = require('web.core');
var data = require('web.data');
var Dialog = require('web.Dialog');
-var field_utils = require('web.field_utils');
var KanbanRenderer = require('web.KanbanRenderer');
var ListRenderer = require('web.ListRenderer');
var Pager = require('web.Pager');
@@ -226,7 +225,7 @@ var FieldMany2One = AbstractField.extend({
var self = this;
var def = $.Deferred();
var slowCreate = function () {
- var dialog = self._searchCreatePopup.bind(self, "form", false, self._createContext(name));
+ var dialog = self._searchCreatePopup("form", false, self._createContext(name));
dialog.on('closed', self, def.resolve.bind(def));
};
if (this.nodeOptions.quick_create) {
@@ -290,7 +289,7 @@ var FieldMany2One = AbstractField.extend({
_reset: function () {
this._super.apply(this, arguments);
this.floating = false;
- this.m2o_value = field_utils.format.many2one(this.value);
+ this.m2o_value = this._formatValue(this.value);
},
/**
* @private
@@ -394,7 +393,7 @@ var FieldMany2One = AbstractField.extend({
*/
_searchCreatePopup: function (view, ids, context) {
var self = this;
- new dialogs.SelectCreateDialog(this, _.extend({}, this.nodeOptions, {
+ return new dialogs.SelectCreateDialog(this, _.extend({}, this.nodeOptions, {
res_model: this.field.relation,
domain: this.record.getDomain({fieldName: this.name}),
context: _.extend({}, this.record.getContext(this.recordParams), context || {}),
@@ -567,7 +566,7 @@ var KanbanFieldMany2One = AbstractField.extend({
tagName: 'span',
init: function () {
this._super.apply(this, arguments);
- this.m2o_value = field_utils.format.many2one(this.value);
+ this.m2o_value = this._formatValue(this.value);
},
//--------------------------------------------------------------------------
@@ -2023,7 +2022,8 @@ var FieldRadio = FieldSelection.extend({
* `name_get` as data).
*/
var FieldReference = FieldMany2One.extend({
- supportedFieldTypes: ['reference'],
+ specialData: "_fetchSpecialReference",
+ supportedFieldTypes: ['char', 'reference'],
template: 'FieldReference',
events: _.extend({}, FieldMany2One.prototype.events, {
'change select': '_onSelectionChange',
@@ -2036,22 +2036,8 @@ var FieldReference = FieldMany2One.extend({
// needs to be copied as it is an unmutable object
this.field = _.extend({}, this.field);
- if (this.value) {
- var model = null;
- // The value of a Reference field can either be a string in the form
- // of model,record_id or an object
- if (typeof this.value === "string") {
- var value = this.value.split(',');
- model = value[0];
- this.value = {
- res_id: parseInt(value[1]),
- };
- } else {
- model = this.value.model;
- }
- this._setRelation(model);
- }
+ this._setState();
},
/**
* @override
@@ -2071,7 +2057,13 @@ var FieldReference = FieldMany2One.extend({
* @override
*/
_formatValue: function () {
- return this.recordData.display_name;
+ var value;
+ if (this.field.type === 'char') {
+ value = this.record.specialData[this.name];
+ } else {
+ value = this.value;
+ }
+ return value && value.data && value.data.display_name || '';
},
/**
@@ -2097,8 +2089,10 @@ var FieldReference = FieldMany2One.extend({
* @private
*/
_reset: function () {
+ var value = this.$('select').val();
+ this._setState();
this._super.apply(this, arguments);
- this._setRelation(this.value && this.value.model);
+ this.$('select').val(this.value && this.value.model || value);
},
/**
* Set `relation` key in field properties.
@@ -2111,6 +2105,19 @@ var FieldReference = FieldMany2One.extend({
this.field.relation = model;
},
/**
+ * @private
+ */
+ _setState: function () {
+ if (this.field.type === 'char') {
+ // in this case, the value is stored in specialData instead
+ this.value = this.record.specialData[this.name];
+ }
+
+ if (this.value) {
+ this._setRelation(this.value.model);
+ }
+ },
+ /**
* @override
* @private
*/
diff --git a/addons/web/static/src/js/fields/special_fields.js b/addons/web/static/src/js/fields/special_fields.js
index 69647902afe..7a0bb9ed092 100644
--- a/addons/web/static/src/js/fields/special_fields.js
+++ b/addons/web/static/src/js/fields/special_fields.js
@@ -54,7 +54,7 @@ var FieldTimezoneMismatch = FieldSelection.extend({
}
var offset = this.recordData.tz_offset.match(/([+-])([0-9]{2})([0-9]{2})/);
offset = (offset[1] === '-' ? -1 : 1) * (parseInt(offset[2])*60 + parseInt(offset[3]));
- var datetime = field_utils.format.datetime(moment.utc().add(offset, 'minutes'), {timezone: false});
+ var datetime = field_utils.format.datetime(moment.utc().add(offset, 'minutes'), this.field, {timezone: false});
var content = this.$option.html().split(' ')[0];
content += ' ('+ datetime + ')';
this.$option.html(content);
diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js
index 6ba9dbea477..cfdee6793dd 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -329,9 +329,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
if (options && options.readonlyIfRealDiscard && !needDiscard) {
return;
}
- if (needDiscard) { // Just some optimization
- self.model.discardChanges(recordID);
- }
+ self.model.discardChanges(recordID);
if (self.model.isNew(recordID)) {
self._abandonRecord(recordID);
return;
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 8755805f993..0d95c292070 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -186,6 +186,29 @@ var BasicModel = AbstractModel.extend({
});
},
/**
+ * Onchange RPCs may return values for fields that are not in the current
+ * view. Those fields might even be unknown when the onchange returns (e.g.
+ * in x2manys, we only know the fields that are used in the inner view, but
+ * not those used in the potential form view opened in a dialog when a sub-
+ * record is clicked). When this happens, we can't infer their type, so the
+ * given value can't be processed. It is instead stored in the '_rawChanges'
+ * key of the record, without any processing. Later on, if this record is
+ * displayed in another view (e.g. the user clicked on it in the x2many
+ * list, and the record opens in a dialog), those changes that were left
+ * behind must be applied. This function applies changes stored in
+ * '_rawChanges' for a given viewType.
+ *
+ * @param {string} id local resource id of a record
+ * @param {string} viewType the current viewType
+ * @returns {Deferred -> string} resolves to the id of the record
+ */
+ applyRawChanges: function (recordID, viewType) {
+ var record = this.localData[recordID];
+ return this._applyOnChange(record._rawChanges, record, viewType).then(function () {
+ return record.id;
+ });
+ },
+ /**
* Delete a list of records, then, if the records have a parent, reload it.
*
* @todo we should remove the deleted records from the localData
@@ -754,14 +777,15 @@ var BasicModel = AbstractModel.extend({
if (newValue instanceof Array) {
rec._savePoint = newValue.slice(0);
} else {
- // save the viewType of edition, so that the correct readonly modifiers
- // can be evaluated when the record will be saved
- for (var fieldName in (rec._changes || {})) {
- rec._editionViewType[fieldName] = options.viewType;
- }
rec._savePoint = _.extend({}, newValue);
}
});
+
+ // save the viewType of edition, so that the correct readonly modifiers
+ // can be evaluated when the record will be saved
+ _.each((record._changes || {}), function (value, fieldName) {
+ record._editionViewType[fieldName] = options.viewType;
+ });
}
var shouldReload = 'reload' in options ? options.reload : true;
var method = self.isNew(record_id) ? 'create' : 'write';
@@ -896,18 +920,23 @@ var BasicModel = AbstractModel.extend({
* @returns {Deferred -> string} resolves to the group id
*/
toggleGroup: function (groupId) {
+ var self = this;
var group = this.localData[groupId];
if (group.isOpen) {
group.isOpen = false;
group.data = [];
+ group.res_ids = [];
group.offset = 0;
+ this._updateParentResIDs(group);
return $.when(groupId);
}
if (!group.isOpen) {
group.isOpen = true;
var def;
if (group.count > 0) {
- def = this._load(group);
+ def = this._load(group).then(function () {
+ self._updateParentResIDs(group);
+ });
}
return $.when(def).then(function () {
return groupId;
@@ -1093,21 +1122,33 @@ var BasicModel = AbstractModel.extend({
* @param {Object} values the result of the onchange RPC (a mapping of
* fieldnames to their value)
* @param {Object} record
+ * @param {string} [viewType] current viewType. If not set, we will assume
+ * main viewType from the record
* @returns {Deferred}
*/
- _applyOnChange: function (values, record) {
+ _applyOnChange: function (values, record, viewType) {
var self = this;
var defs = [];
var rec;
+ viewType = viewType || record.viewType;
record._changes = record._changes || {};
_.each(values, function (val, name) {
var field = record.fields[name];
if (!field) {
- return; // ignore changes of unknown fields
+ // this field is unknown so we can't process it for now (it is not
+ // in the current view anyway, otherwise it wouldn't be unknown.
+ // we store its value without processing it, so that if we later
+ // on switch to another view in which this field is displayed,
+ // we could process it as we would know its type then.
+ // use case: an onchange sends a create command for a one2many,
+ // in the dict of values, there is a value for a field that is
+ // not in the one2many list, but that is in the one2many form.
+ record._rawChanges[name] = val;
+ return;
}
-
- if (field.type === 'many2one' ) {
- var id = false;
+ var id;
+ if (field.type === 'many2one') {
+ id = false;
// in some case, the value returned by the onchange can
// be false (no value), so we need to avoid creating a
// local record for that.
@@ -1126,13 +1167,27 @@ var BasicModel = AbstractModel.extend({
id = rec.id;
}
record._changes[name] = id;
+ } else if (field.type === 'reference') {
+ id = false;
+ if (val) {
+ var ref = val.split(',');
+ rec = self._makeDataPoint({
+ context: record.context,
+ data: {id: parseInt(ref[1])},
+ modelName: ref[0],
+ parentID: record.id,
+ });
+ defs.push(self._fetchNameGet(rec));
+ id = rec.id;
+ }
+ record._changes[name] = id;
} else if (field.type === 'one2many' || field.type === 'many2many') {
var listId = record._changes[name] || record.data[name];
var list;
if (listId) {
list = self.localData[listId];
} else {
- var fieldInfo = record.fieldsInfo[record.viewType][name];
+ var fieldInfo = record.fieldsInfo[viewType][name];
if (!fieldInfo) {
return; //ignore changes of x2many not in view
}
@@ -1142,7 +1197,7 @@ var BasicModel = AbstractModel.extend({
parentID: record.id,
static: true,
type: 'list',
- viewtype: fieldInfo.viewType,
+ viewType: fieldInfo.viewType,
});
}
record._changes[name] = list.id;
@@ -1535,15 +1590,17 @@ var BasicModel = AbstractModel.extend({
var self = this;
var model;
var records = [];
+ var ids = [];
list = this._applyX2ManyOperations(list);
- var ids = _.map(list.data, function (localId) {
+ _.each(list.data, function (localId) {
var record = self.localData[localId];
var data = record._changes || record.data;
var many2oneId = data[fieldName];
+ if (!many2oneId) { return; }
var many2oneRecord = self.localData[many2oneId];
records.push(many2oneRecord);
+ ids.push(many2oneRecord.res_id);
model = many2oneRecord.model;
- return many2oneRecord.res_id;
});
return this._rpc({
model: model,
@@ -1600,6 +1657,39 @@ var BasicModel = AbstractModel.extend({
});
},
/**
+ * Fetch the `name_get` for a reference field.
+ *
+ * @private
+ * @param {Object} record
+ * @param {string} fieldName
+ * @returns {Deferred}
+ */
+ _fetchReference: function (record, fieldName) {
+ var self = this;
+ var def;
+ var value = record._changes && record._changes[fieldName] || record.data[fieldName];
+ var model = value && value.split(',')[0];
+ var resID = value && parseInt(value.split(',')[1]);
+ if (model && model !== 'False' && resID) {
+ def = self._rpc({
+ model: model,
+ method: 'name_get',
+ args: [resID],
+ context: record.getContext({fieldName: fieldName}),
+ }).then(function (result) {
+ return self._makeDataPoint({
+ data: {
+ id: result[0][0],
+ display_name: result[0][1],
+ },
+ modelName: model,
+ parentID: record.id,
+ });
+ });
+ }
+ return $.when(def);
+ },
+ /**
* Fetch the extra data (`name_get`) for the reference fields of the record
* model.
*
@@ -1614,28 +1704,12 @@ var BasicModel = AbstractModel.extend({
_.each(fieldNames, function (fieldName) {
var field = record.fields[fieldName];
if (field.type === 'reference') {
- var value = record.data[fieldName];
- if (value) {
- var model = value.split(',')[0];
- var resID = parseInt(value.split(',')[1]);
- var def = self._rpc({
- model: model,
- method: 'name_get',
- args: [resID],
- context: record.getContext({fieldName: fieldName}),
- }).then(function (result) {
- var referenceDp = self._makeDataPoint({
- data: {
- id: result[0][0],
- display_name: result[0][1],
- },
- modelName: model,
- parentID: record.id,
- });
- record.data[fieldName] = referenceDp.id;
- });
- defs.push(def);
- }
+ var def = self._fetchReference(record, fieldName).then(function (dataPoint) {
+ if (dataPoint) {
+ record.data[fieldName] = dataPoint.id;
+ }
+ });
+ defs.push(def);
}
});
return $.when.apply($, defs);
@@ -1906,6 +1980,25 @@ var BasicModel = AbstractModel.extend({
});
},
/**
+ * Fetches the `name_get` associated to the reference widget if the field is
+ * a `char` (which is a supported case).
+ *
+ * @private
+ * @param {Object} record - an element from the localData
+ * @param {Object} fieldName - the name of the field
+ * @returns {Deferred}
+ */
+ _fetchSpecialReference: function (record, fieldName) {
+ var def;
+ var field = record.fields[fieldName];
+ if (field.type === 'char') {
+ // if the widget reference is set on a char field, the name_get
+ // needs to be fetched a posteriori
+ def = this._fetchReference(record, fieldName);
+ }
+ return $.when(def);
+ },
+ /**
* Fetches all the m2o records associated to the given fieldName. If the
* given fieldName is not a m2o field, nothing is done. The difference with
* _fetchSpecialMany2ones is that the field given by options.fold_field is
@@ -2712,6 +2805,7 @@ var BasicModel = AbstractModel.extend({
_cache: type === 'list' ? {} : undefined,
_changes: null,
_domains: {},
+ _rawChanges: {},
aggregateValues: params.aggregateValues || {},
context: params.context || {},
count: params.count || res_ids.length,
@@ -2826,14 +2920,25 @@ var BasicModel = AbstractModel.extend({
var field = params.fields[name];
data[name] = null;
record._changes = record._changes || {};
+ var dp;
if (field.type === 'many2one' && result[name]) {
- var rec = self._makeDataPoint({
+ dp = self._makeDataPoint({
context: record.context,
data: {id: result[name]},
modelName: field.relation,
parentID: record.id,
});
- record._changes[name] = rec.id;
+ record._changes[name] = dp.id;
+ } else if (field.type === 'reference' && result[name]) {
+ var ref = result[name].split(',');
+ dp = self._makeDataPoint({
+ context: record.context,
+ data: {id: parseInt(ref[1])},
+ modelName: ref[0],
+ parentID: record.id,
+ });
+ defs.push(self._fetchNameGet(dp));
+ record._changes[name] = dp.id;
} else if (field.type === 'one2many' || field.type === 'many2many') {
var fieldInfo = record.fieldsInfo[record.viewType][name];
var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode];
@@ -2859,6 +2964,7 @@ var BasicModel = AbstractModel.extend({
x2manyList._changes = [];
var many2ones = {};
var r;
+ result[name] = result[name] || []; // handle false value
var isCommandList = result[name].length && _.isArray(result[name][0]);
if (!isCommandList) {
result[name] = [[6, false, result[name]]];
@@ -2885,7 +2991,7 @@ var BasicModel = AbstractModel.extend({
r._changes = _.defaults(value[2], r.data);
for (var name in r._changes) {
- if (r._changes[name] === null) {
+ if (!r._changes[name]) {
continue;
}
var isFieldInView = name in r.fields;
@@ -3318,6 +3424,7 @@ var BasicModel = AbstractModel.extend({
list.data = data;
list.res_ids = ids;
}
+ self._updateParentResIDs(list);
return list;
});
},
@@ -3364,6 +3471,26 @@ var BasicModel = AbstractModel.extend({
}
},
/**
+ * Updates the res_ids of the parent of a given element of type list.
+ *
+ * After some operations (e.g. loading more records, folding/unfolding a
+ * group), the res_ids list of an element may be updated. When this happens,
+ * the res_ids of its ancestors need to be updated as well. This is the
+ * purpose of this function.
+ *
+ * @param {Object} element
+ */
+ _updateParentResIDs: function (element) {
+ var self = this;
+ if (element.parentID) {
+ var parent = this.localData[element.parentID];
+ parent.res_ids = _.flatten(_.map(parent.data, function (dataPointID) {
+ return self.localData[dataPointID].res_ids;
+ }));
+ this._updateParentResIDs(parent);
+ }
+ },
+ /**
* Helper method. Recursively traverses the data, starting from the element
* record (or list), then following all relations. This is useful when one
* want to determine a property for the current record.
diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js
index 736416dea27..bdc250011f0 100644
--- a/addons/web/static/src/js/views/basic/basic_view.js
+++ b/addons/web/static/src/js/views/basic/basic_view.js
@@ -63,6 +63,7 @@ var BasicView = AbstractView.extend({
*/
_loadData: function () {
if (this.recordID) {
+ var self = this;
var record = this.model.get(this.recordID);
var viewType = this.viewType;
var viewFields = Object.keys(record.fieldsInfo[viewType]);
@@ -107,15 +108,27 @@ var BasicView = AbstractView.extend({
}
}
});
- if (fieldNames.length && !this.model.isNew(record.id)) {
- return this.model.reload(this.recordID, {
- fieldNames: fieldNames,
- keepChanges: true,
- viewType: viewType,
+
+ var def;
+ if (fieldNames.length) {
+ // Some fields in the new view weren't in the previous one, so
+ // we might have stored changes for them (e.g. coming from
+ // onchange RPCs), that we haven't been able to process earlier
+ // (because those fields were unknow at that time). So we ask
+ // the model to process them.
+ def = this.model.applyRawChanges(record.id, viewType).then(function () {
+ if (!self.model.isNew(record.id)) {
+ return self.model.reload(record.id, {
+ fieldNames: fieldNames,
+ keepChanges: true,
+ viewType: viewType,
+ });
+ }
});
- } else {
- return $.when(this.recordID);
}
+ return $.when(def).then(function () {
+ return record.id;
+ });
}
return this._super.apply(this, arguments);
},
diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js
index d2b0c02c524..c8b9fd574b2 100644
--- a/addons/web/static/src/js/views/calendar/calendar_model.js
+++ b/addons/web/static/src/js/views/calendar/calendar_model.js
@@ -38,39 +38,47 @@ return AbstractModel.extend({
var start = event.start.clone();
var end = event.end && event.end.clone();
- if (!end || end.diff(start) < 0) {
- end = start.clone().add(1, 'h');
+ // Detects allDay events (86400000 = 1 day in ms)
+ if (event.allDay || (end && end.diff(start) % 86400000 === 0)) {
+ event.allDay = true;
}
- if (event.allDay || end.diff(start) % 86400000 === 0) {
- event.allDay = true;
+ // Set end date if not existing
+ if (!end || end.diff(start) < 0) { // undefined or invalid end date
+ if (event.allDay) {
+ end = start.clone();
+ } else {
+ // in week mode or day mode, convert allday event to event
+ end = start.clone().add(2, 'h');
+ }
+ } else if (event.allDay) {
+ // For an "allDay", FullCalendar gives the end day as the
+ // next day at midnight (instead of 23h59).
+ end.add(-1, 'days');
+ }
- if (this.scale === 'month') {
+ // An "allDay" event without the "all_day" option is not considered
+ // as a 24h day. It's just a part of the day (by default: 7h-19h).
+ if (event.allDay) {
+ if (!this.mapping.all_day) {
if (event.r_start) {
start.hours(event.r_start.hours())
.minutes(event.r_start.minutes())
.seconds(event.r_start.seconds())
.utc();
- end.add(-1, 'days')
- .hours(event.r_end.hours())
+ end.hours(event.r_end.hours())
.minutes(event.r_end.minutes())
.seconds(event.r_end.seconds())
.utc();
} else {
- start.utc();
- end.utc();
+ // default hours in the user's timezone
+ start.hours(7).add(-this.getSession().getTZOffset(start), 'minutes');
+ end.hours(19).add(-this.getSession().getTZOffset(end), 'minutes');
}
- } else if (this.mapping.all_day) {
- start.startOf('day');
- end.startOf('day').add(-1, 'days');
- } else {
- // default hours in the user's timezone
- start.hours(7).add(-this.getSession().tzOffset, 'minutes');
- end.hours(19).add(-this.getSession().tzOffset, 'minutes');
}
} else {
- start.add(-this.getSession().tzOffset, 'minutes');
- end.add(-this.getSession().tzOffset, 'minutes');
+ start.add(-this.getSession().getTZOffset(start), 'minutes');
+ end.add(-this.getSession().getTZOffset(end), 'minutes');
}
if (this.mapping.all_day) {
@@ -365,6 +373,7 @@ return AbstractModel.extend({
monthNamesShort: moment.monthsShort(),
dayNames: moment.weekdays(),
dayNamesShort: moment.weekdaysShort(),
+ firstDay: moment().startOf('week').isoWeekday(),
};
},
_getRangeDomain: function () {
@@ -599,8 +608,8 @@ return AbstractModel.extend({
date_stop = date_start.clone().add(date_delay,'hours');
}
- date_start.add(this.getSession().tzOffset, 'minutes');
- date_stop.add(this.getSession().tzOffset, 'minutes');
+ date_start.add(this.getSession().getTZOffset(date_start), 'minutes');
+ date_stop.add(this.getSession().getTZOffset(date_stop), 'minutes');
if (this.mapping.all_day && evt[this.mapping.all_day]) {
date_stop.add(1, 'days');
@@ -622,6 +631,10 @@ return AbstractModel.extend({
// r.start = date_start.format('YYYY-MM-DD');
// r.end = date_stop.format('YYYY-MM-DD');
} else if (this.data.scale === 'month' && this.fields[this.mapping.date_start].type !== 'date') {
+ // In month, FullCalendar gives the end day as the
+ // next day at midnight (instead of 23h59).
+ date_stop.add(1, 'days');
+
// allow to resize in month mode
r.reset_allday = r.allDay;
r.allDay = true;
diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js
index 5fb79cfa3a0..422258b4035 100644
--- a/addons/web/static/src/js/views/calendar/calendar_renderer.js
+++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js
@@ -331,12 +331,15 @@ return AbstractRenderer.extend({
}
element.find('.fc-content .fc-time').text(display_hour);
},
+ // Dirty hack to ensure a correct first render
+ eventAfterAllRender: function (view) {
+ window.dispatchEvent(new Event('resize'));
+ },
height: 'parent',
unselectAuto: false,
});
this.$calendar.fullCalendar(fc_options);
- window.dispatchEvent(new Event('resize'));
},
/**
* Initialize the mini calendar in the sidebar
diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js
index 459f8768c04..3fa3b76d601 100644
--- a/addons/web/static/src/js/views/kanban/kanban_controller.js
+++ b/addons/web/static/src/js/views/kanban/kanban_controller.js
@@ -107,7 +107,10 @@ var KanbanController = BasicController.extend({
* @returns {Deferred}
*/
_resequenceRecords: function (column_id, ids) {
- return this.model.resequence(this.modelName, ids, column_id);
+ var self = this;
+ return this.model.resequence(this.modelName, ids, column_id).then(function () {
+ self._updateEnv();
+ });
},
/**
* In grouped mode, set 'Create' button as btn-default if there is no column
@@ -181,6 +184,7 @@ var KanbanController = BasicController.extend({
.then(function (db_id) {
var data = self.model.get(db_id);
self.renderer.updateColumn(db_id, data);
+ self._updateEnv();
});
}
},
@@ -218,6 +222,7 @@ var KanbanController = BasicController.extend({
this.model.loadMore(column.db_id).then(function (db_id) {
var data = self.model.get(db_id);
self.renderer.updateColumn(db_id, data);
+ self._updateEnv();
});
},
/**
@@ -328,9 +333,12 @@ var KanbanController = BasicController.extend({
* @param {OdooEvent} event
*/
_onResequenceColumn: function (event) {
+ var self = this;
var state = this.model.get(this.handle, {raw: true});
var model = state.fields[state.groupedBy[0]].relation;
- this.model.resequence(model, event.data.ids, this.handle);
+ this.model.resequence(model, event.data.ids, this.handle).then(function () {
+ self._updateEnv();
+ });
},
/**
* @param {OdooEvent} event
@@ -341,6 +349,7 @@ var KanbanController = BasicController.extend({
this.model.toggleGroup(column.db_id).then(function (db_id) {
var data = self.model.get(db_id);
self.renderer.updateColumn(db_id, data);
+ self._updateEnv();
});
},
/**
diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js
index 71402fc92ec..e21a1376e51 100644
--- a/addons/web/static/src/js/views/kanban/kanban_model.js
+++ b/addons/web/static/src/js/views/kanban/kanban_model.js
@@ -35,12 +35,8 @@ var KanbanModel = BasicModel.extend({
group.count++;
// update the res_ids and count of the parent
- var self = this;
- var parent = this.localData[group.parentID];
- parent.res_ids = _.flatten(_.map(parent.data, function (dataPointID) {
- return self.localData[dataPointID].res_ids;
- }));
- parent.count++;
+ this.localData[group.parentID].count++;
+ this._updateParentResIDs(group);
return this._fetchRecord(new_record).then(function (result) {
return result.id;
@@ -151,6 +147,7 @@ var KanbanModel = BasicModel.extend({
return this.notifyChanges(recordID, changes).then(function () {
return self.save(recordID);
}).then(function () {
+ var resID = self.localData[recordID].res_id;
// Remove record from its current group
var old_group;
for (var i = 0; i < parent.data.length; i++) {
@@ -159,11 +156,14 @@ var KanbanModel = BasicModel.extend({
if (index >= 0) {
old_group.data.splice(index, 1);
old_group.count--;
+ old_group.res_ids = _.without(old_group.res_ids, resID);
+ self._updateParentResIDs(old_group);
break;
}
}
// Add record to its new group
new_group.data.push(recordID);
+ new_group.res_ids.push(resID);
new_group.count++;
return [old_group.id, new_group.id];
});
@@ -201,6 +201,16 @@ var KanbanModel = BasicModel.extend({
data.data = _.sortBy(data.data, function (d) {
return _.indexOf(resIDs, self.localData[d].res_id);
});
+ data.res_ids = [];
+ _.each(data.data, function (d) {
+ var dataPoint = self.localData[d];
+ if (dataPoint.type === 'record') {
+ data.res_ids.push(dataPoint.res_id);
+ } else {
+ data.res_ids = data.res_ids.concat(dataPoint.res_ids);
+ }
+ });
+ self._updateParentResIDs(data);
return parentID;
});
},
diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index 3ef48792f06..3327b1bb3e5 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -388,15 +388,15 @@ var ListRenderer = BasicRenderer.extend({
var $th = $('<th>')
.addClass('o_group_name')
.text(name + ' (' + group.count + ')');
- if (group.count > 0) {
- var $arrow = $('<span>')
+ var $arrow = $('<span>')
.css('padding-left', (groupLevel * 20) + 'px')
.css('padding-right', '5px')
- .addClass('fa')
- .toggleClass('fa-caret-right', !group.isOpen)
- .toggleClass('fa-caret-down', group.isOpen);
- $th.prepend($arrow);
+ .addClass('fa');
+ if (group.count > 0) {
+ $arrow.toggleClass('fa-caret-right', !group.isOpen)
+ .toggleClass('fa-caret-down', group.isOpen);
}
+ $th.prepend($arrow);
if (group.isOpen && !group.groupedBy.length && (group.count > group.data.length)) {
var $pager = this._renderGroupPager(group);
var $lastCell = $cells[$cells.length-1];
diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js
index 97b17f00413..99ffd1e1aa3 100644
--- a/addons/web/static/src/js/views/view_dialogs.js
+++ b/addons/web/static/src/js/views/view_dialogs.js
@@ -350,6 +350,7 @@ var SelectCreateDialog = ViewDialog.extend({
groupBy: searchResult.groupBy,
modelName: self.dataset.model,
hasSelectors: !self.options.disable_multiple_selection,
+ readonly: true,
}, self.options.list_view_options));
listView.setController(SelectCreateListController);
return listView.getController(self);
@@ -408,6 +409,7 @@ var SelectCreateDialog = ViewDialog.extend({
},
})).open();
dialog.on('closed', this, this.close);
+ return dialog;
},
});
diff --git a/addons/web/static/src/js/widgets/data_export.js b/addons/web/static/src/js/widgets/data_export.js
index fde9001f860..4b9f6a29a47 100644
--- a/addons/web/static/src/js/widgets/data_export.js
+++ b/addons/web/static/src/js/widgets/data_export.js
@@ -179,7 +179,7 @@ var DataExport = Dialog.extend({
var self = this;
this.exports.create({
name: value,
- resource: this.dataset.model,
+ resource: this.record.model,
export_fields: _.map(fields, function (field) {
return [0, 0, {name: field}];
}),
diff --git a/addons/web/static/src/js/widgets/debug_manager.js b/addons/web/static/src/js/widgets/debug_manager.js
index baa147d9a1f..b2c175fc028 100644
--- a/addons/web/static/src/js/widgets/debug_manager.js
+++ b/addons/web/static/src/js/widgets/debug_manager.js
@@ -348,7 +348,117 @@ DebugManager.include({
});
},
set_defaults: function() {
- this._active_view.controller.open_defaults_dialog();
+ var self = this;
+
+ var display = function (fieldInfo, value) {
+ var displayed = value;
+ if (value && fieldInfo.type === 'many2one') {
+ displayed = value.data.display_name;
+ value = value.data.id;
+ } else if (value && fieldInfo.type === 'selection') {
+ displayed = _.find(fieldInfo.selection, function (option) {
+ return option[0] === value;
+ })[1];
+ }
+ return [value, displayed];
+ };
+
+ var renderer = this._active_view.controller.renderer;
+ var fields = self._active_view.fields_view.fields;
+ var fieldsInfo = self._active_view.fields_view.fieldsInfo.form;
+ var fieldNamesInView = renderer.state.getFieldNames();
+ var fieldsValues = renderer.state.data;
+ var modifierDatas = {};
+ _.each(fieldNamesInView, function (fieldName) {
+ modifierDatas[fieldName] = _.find(renderer.allModifiersData, function (modifierdata) {
+ return modifierdata.node.attrs.name === fieldName;
+ });
+ });
+ this.fields = _.chain(fieldNamesInView)
+ .map(function (fieldName) {
+ var modifierData = modifierDatas[fieldName];
+ var invisibleOrReadOnly;
+ if (modifierData) {
+ var evaluatedModifiers = modifierData.evaluatedModifiers[renderer.state.id];
+ invisibleOrReadOnly = evaluatedModifiers.invisible || evaluatedModifiers.readonly;
+ }
+ var fieldInfo = fields[fieldName];
+ var valueDisplayed = display(fieldInfo, fieldsValues[fieldName]);
+ var value = valueDisplayed[0];
+ var displayed = valueDisplayed[1];
+ // ignore fields which are empty, invisible, readonly, o2m
+ // or m2m
+ if (!value || invisibleOrReadOnly || fieldInfo.type === 'one2many' ||
+ fieldInfo.type === 'many2many' || fieldInfo.type === 'binary' ||
+ fieldsInfo[fieldName].options.isPassword) {
+ return false;
+ }
+ return {
+ name: fieldName,
+ string: fieldInfo.string,
+ value: value,
+ displayed: displayed,
+ };
+ })
+ .compact()
+ .sortBy(function (field) { return field.string; })
+ .value();
+
+ var conditions = _.chain(fieldNamesInView)
+ .filter(function (fieldName) {
+ var fieldInfo = fields[fieldName];
+ return fieldInfo.change_default;
+ })
+ .map(function (fieldName) {
+ var fieldInfo = fields[fieldName];
+ var valueDisplayed = display(fieldInfo, fieldsValues[fieldName]);
+ var value = valueDisplayed[0];
+ var displayed = valueDisplayed[1];
+ return {
+ name: fieldName,
+ string: fieldInfo.string,
+ value: value,
+ displayed: displayed,
+ };
+ })
+ .value();
+ var d = new Dialog(this, {
+ title: _t("Set Default"),
+ buttons: [
+ {text: _t("Close"), close: true},
+ {text: _t("Save default"), click: function () {
+ var $defaults = d.$el.find('#formview_default_fields');
+ var fieldToSet = $defaults.val();
+ if (!fieldToSet) {
+ $defaults.parent().addClass('o_form_invalid');
+ return;
+ }
+ var allUsers = d.$el.find('#formview_default_all').is(':checked');
+ var condition = d.$el.find('#formview_default_conditions').val();
+ var value = _.find(self.fields, function (field) {
+ return field.name === fieldToSet;
+ }).value;
+ self._rpc({
+ model: 'ir.values',
+ method: 'set_default',
+ args: [
+ self._active_view.fields_view.model,
+ fieldToSet,
+ value,
+ allUsers,
+ true,
+ condition || false,
+ ],
+ }).done(function () { d.close(); });
+ }}
+ ]
+ });
+ d.args = {
+ fields: this.fields,
+ conditions: conditions,
+ };
+ d.template = 'FormView.set_default';
+ d.open();
},
fvg: function() {
var self = this;
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index f0a08b5dc62..b986c08ff4d 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -448,7 +448,7 @@
</label>
</td>
<td class="oe_form_required">
- <select id="formview_default_fields">
+ <select id="formview_default_fields" class="o_input">
<option value=""/>
<option t-foreach="args.fields" t-as="field"
t-att-value="field.name">
@@ -465,7 +465,7 @@
</label>
</td>
<td>
- <select id="formview_default_conditions">
+ <select id="formview_default_conditions" class="o_input">
<option value=""/>
<option t-foreach="args.conditions" t-as="cond"
t-att-value="cond.name + '=' + cond.value">
diff --git a/addons/web/static/src/xml/kanban.xml b/addons/web/static/src/xml/kanban.xml
index 5206393960f..8def5642a8b 100644
--- a/addons/web/static/src/xml/kanban.xml
+++ b/addons/web/static/src/xml/kanban.xml
@@ -48,7 +48,7 @@
<span class="o_kanban_title">Add new Column</span>
</div>
<div class="o_kanban_quick_create">
- <input type="text" placeholder="Add"/>
+ <input type="text" class="o_input" placeholder="Add"/>
<button class="btn btn-sm btn-primary o_kanban_add">Add</button>
<button class="btn btn-sm btn-default o_kanban_cancel">Discard</button>
</div>
@@ -57,7 +57,7 @@
<t t-name="KanbanView.QuickCreate">
<div class="o_kanban_quick_create">
- <input type="text" placeholder="Add"/>
+ <input type="text" class="o_input" placeholder="Add"/>
<button class="btn btn-sm btn-primary o_kanban_add">Add</button>
<button class="btn btn-sm btn-primary o_kanban_edit">Edit</button>
<button class="btn btn-sm btn-default o_kanban_cancel">Discard</button>
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index c2ab8121c16..b533f5056d2 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -736,6 +736,31 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test('char field with size attribute', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.foo.size = 5; // max length
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<group><field name="foo"/></group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual(form.$('input.o_field_widget').attr('maxlength'), '5',
+ "maxlength attribute should have been set correctly on the input");
+
+ form.destroy();
+ });
+
QUnit.test('char field in editable list view', function (assert) {
assert.expect(6);
@@ -1072,8 +1097,8 @@ QUnit.module('basic_fields', {
res_id: 1,
});
- assert.ok(form.$('div.o_field_text').length, "should have a text area");
- assert.strictEqual(form.$('div.o_field_text').text(), 'yop', 'should be "yop" in readonly');
+ assert.ok(form.$('.o_field_text').length, "should have a text area");
+ assert.strictEqual(form.$('.o_field_text').text(), 'yop', 'should be "yop" in readonly');
form.$buttons.find('.o_form_button_edit').click();
@@ -1089,7 +1114,7 @@ QUnit.module('basic_fields', {
form.$buttons.find('.o_form_button_save').click();
- assert.strictEqual(form.$('div.o_field_text').text(), 'hello world',
+ assert.strictEqual(form.$('.o_field_text').text(), 'hello world',
'should be "hello world" after save');
form.destroy();
});
@@ -1109,7 +1134,7 @@ QUnit.module('basic_fields', {
res_id: 1,
});
- var $field = form.$('div.o_field_text');
+ var $field = form.$('.o_field_text');
assert.strictEqual($field.outerHeight(), $field[0].scrollHeight,
"text field should not have a scroll bar");
@@ -1211,6 +1236,31 @@ QUnit.module('basic_fields', {
list.destroy();
});
+ // Firefox-specific
+ // Copying from <div style="white-space:pre-wrap"> does not keep line breaks
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1390115
+ QUnit.test('copying text fields in RO mode should preserve line breaks', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="txt"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // Copying from a div tag with white-space:pre-wrap doesn't work in Firefox
+ assert.strictEqual(form.$('[name="txt"]').prop("tagName").toLowerCase(), 'span',
+ "the field contents should be surrounded by a span tag");
+ });
+
QUnit.module('FieldBinary');
QUnit.test('binary fields are correctly rendered', function (assert) {
@@ -1563,7 +1613,7 @@ QUnit.module('basic_fields', {
form.destroy();
});
- QUnit.test('date field in form view', function (assert) {
+ QUnit.test('date field in form view (with positive time zone offset)', function (assert) {
assert.expect(8);
var form = createView({
@@ -1582,7 +1632,9 @@ QUnit.module('basic_fields', {
date_format: '%m/%d/%Y',
},
session: {
- tzOffset: 120
+ getTZOffset: function () {
+ return 120; // Should be ignored by date fields
+ },
},
});
@@ -1614,6 +1666,36 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test('date field in form view (with negative time zone offset)', function (assert) {
+ assert.expect(2);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="date"/></form>',
+ res_id: 1,
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ },
+ session: {
+ getTZOffset: function () {
+ return -120; // Should be ignored by date fields
+ },
+ },
+ });
+
+ assert.strictEqual(form.$('.o_field_date').text(), '02/03/2017',
+ 'the date should be correctly displayed in readonly');
+
+ // switch to edit mode
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.o_datepicker_input').val(), '02/03/2017',
+ 'the date should be correct in edit mode');
+
+ form.destroy();
+ });
+
QUnit.test('date field in editable list view', function (assert) {
assert.expect(8);
@@ -1628,7 +1710,9 @@ QUnit.module('basic_fields', {
date_format: '%m/%d/%Y',
},
session: {
- tzOffset: 0
+ getTZOffset: function () {
+ return 0;
+ },
},
});
@@ -1717,7 +1801,9 @@ QUnit.module('basic_fields', {
time_format: '%H:%M:%S',
},
session: {
- tzOffset: 120
+ getTZOffset: function () {
+ return 120;
+ },
},
});
@@ -1774,7 +1860,9 @@ QUnit.module('basic_fields', {
time_format: '%H:%M:%S',
},
session: {
- tzOffset: 120
+ getTZOffset: function () {
+ return 120;
+ },
},
});
@@ -1844,7 +1932,9 @@ QUnit.module('basic_fields', {
time_format: '%H:%M:%S',
},
session: {
- tzOffset: 120
+ getTZOffset: function () {
+ return 120;
+ },
},
});
@@ -1863,6 +1953,93 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test('datetime field with date/datetime widget (with day change)', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].p = [2];
+ this.data.partner.records[1].datetime = "2017-02-08 02:00:00"; // UTC
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="datetime"/>' +
+ '</tree>' +
+ '<form>' +
+ '<field name="datetime" widget="date"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ time_format: '%H:%M:%S',
+ },
+ session: {
+ getTZOffset: function () {
+ return -240;
+ },
+ },
+ });
+
+ var expectedDateString = "02/07/2017 22:00:00"; // local time zone
+ assert.strictEqual(form.$('.o_field_widget[name=p] .o_data_cell').text(), expectedDateString,
+ 'the datetime (datetime widget) should be correctly displayed in tree view');
+
+ // switch to form view
+ form.$('.o_field_widget[name=p] .o_data_row').click();
+ assert.strictEqual($('.modal .o_field_date[name=datetime]').text(), '02/07/2017',
+ 'the datetime (date widget) should be correctly displayed in form view');
+
+ form.destroy();
+ });
+
+ QUnit.test('datetime field with date/datetime widget (without day change)', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].p = [2];
+ this.data.partner.records[1].datetime = "2017-02-08 10:00:00"; // without timezone
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="datetime"/>' +
+ '</tree>' +
+ '<form>' +
+ '<field name="datetime" widget="date"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ time_format: '%H:%M:%S',
+ },
+ session: {
+ getTZOffset: function () {
+ return -240;
+ },
+ },
+ });
+
+ var expectedDateString = "02/08/2017 06:00:00"; // with timezone
+ assert.strictEqual(form.$('.o_field_widget[name=p] .o_data_cell').text(), expectedDateString,
+ 'the datetime (datetime widget) should be correctly displayed in tree view');
+
+ // switch to form view
+ form.$('.o_field_widget[name=p] .o_data_row').click();
+ assert.strictEqual($('.modal .o_field_date[name=datetime]').text(), '02/08/2017',
+ 'the datetime (date widget) should be correctly displayed in form view');
+
+ form.destroy();
+ });
QUnit.module('FieldMonetary');
diff --git a/addons/web/static/tests/fields/field_utils_tests.js b/addons/web/static/tests/fields/field_utils_tests.js
index a49197f6e65..a0284798553 100644
--- a/addons/web/static/tests/fields/field_utils_tests.js
+++ b/addons/web/static/tests/fields/field_utils_tests.js
@@ -4,6 +4,7 @@ odoo.define('web.field_utils_tests', function (require) {
var core = require('web.core');
var session = require('web.session');
var fieldUtils = require('web.field_utils');
+var testUtils = require('web.test_utils');
QUnit.module('fields', {}, function () {
@@ -59,10 +60,35 @@ QUnit.test("format_datetime", function (assert) {
var date_string = "2009-05-04 12:34:23";
var date = fieldUtils.parse.datetime(date_string, {}, {timezone: false});
- var str = fieldUtils.format.datetime(date, {timezone: false});
+ var str = fieldUtils.format.datetime(date, {}, {timezone: false});
assert.strictEqual(str, moment(date).format("MM/DD/YYYY HH:mm:ss"));
});
+QUnit.test("format_datetime (with different timezone offset)", function (assert) {
+ assert.expect(2);
+
+ // mock the date format to avoid issues due to localisation
+ var dateFormat = core._t.database.parameters.date_format;
+ core._t.database.parameters.date_format = '%m/%d/%Y';
+ session.getTZOffset = function (date) {
+ // simulate daylight saving time
+ var startDate = new Date(2017, 2, 26);
+ var endDate = new Date(2017, 9, 29);
+ if (startDate < date && date < endDate) {
+ return 120; // UTC+2
+ } else {
+ return 60; // UTC+1
+ }
+ };
+
+ var str = fieldUtils.format.datetime(moment.utc('2017-01-01T10:00:00Z'));
+ assert.strictEqual(str, '01/01/2017 11:00:00');
+ str = fieldUtils.format.datetime(moment.utc('2017-06-01T10:00:00Z'));
+ assert.strictEqual(str, '06/01/2017 12:00:00');
+
+ core._t.database.parameters.date_format = dateFormat;
+});
+
QUnit.test("format_many2one", function (assert) {
assert.expect(2);
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 29f8666455a..89f4601efc4 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -958,6 +958,45 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('list in form: create with one2many with many2one (version 2)', function (assert) {
+ // This test simulates the exact same scenario as the previous one,
+ // except that the value for the many2one is explicitely set to false,
+ // which is stupid, but this happens, so we have to handle it
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '<field name="trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'default_get') {
+ return $.when({p: [[0, 0, {display_name: 'new record', trululu: false}]]});
+ } else if (args.method === 'name_get') {
+ // This should not be called at all and thus is not accounted for
+ // in the assert.expect. If this is called, you broke this test.
+ assert.notOk(_.str.startsWith(args.args[0][0], 'virtual_'),
+ "should not call name_get for the m2o inside o2m which has no value");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual($('td.o_data_cell:first').text(), 'new record',
+ "should have created the new record in the o2m with the correct name");
+
+ form.destroy();
+ });
+
QUnit.test('list in form: default_get with x2many create', function (assert) {
assert.expect(3);
@@ -1250,6 +1289,38 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('slow create on a many2one', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form>' +
+ '<sheet>' +
+ '<field name="product_id" options="{\'quick_create\': False}"/>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ 'product,false,form':
+ '<form>' +
+ '<field name="name"/>' +
+ '</form>',
+ },
+ });
+
+ form.$('.o_field_many2one input').focus();
+ form.$('.o_field_many2one input').val('new partner').trigger('keyup').trigger('focusout');
+
+ $('.modal .modal-footer .btn-primary').first().click();
+
+ assert.strictEqual($('.modal:visible:last .o_form_view').length, 1,
+ 'a new modal should be opened and contain a form view');
+
+ form.destroy();
+ });
+
QUnit.test('no_create option on a many2one', function (assert) {
assert.expect(1);
@@ -1732,7 +1803,9 @@ QUnit.module('relational_fields', {
'</form>',
res_id: 1,
session: {
- tzOffset: 120
+ getTZOffset: function () {
+ return 120;
+ },
},
});
assert.strictEqual(form.$('td:eq(0)').text(), "01/25/2017",
@@ -5227,6 +5300,59 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('one2many: onchange that returns unknow field in list, but not in form', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.onchanges = {
+ name: function () {},
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="name"/>' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '<form string="Partners">' +
+ '<field name="display_name"/>' +
+ '<field name="timmy" widget="many2many_tags"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ return $.when({
+ value: {
+ p: [[5], [0, 0, {display_name: 'new', timmy: [[5], [4, 12]]}]],
+ },
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('.o_data_row').length, 1,
+ "the one2many should contain one row");
+ assert.strictEqual(form.$('.o_field_widget[name="timmy"]').length, 0,
+ "timmy should not be displayed in the list view");
+
+ form.$('.o_data_row td:first').click(); // open the record
+
+ assert.strictEqual($('.modal .o_field_many2manytags[name="timmy"]').length, 1,
+ "timmy should be displayed in the form view");
+ assert.strictEqual($('.modal .o_field_many2manytags[name="timmy"] .badge').length, 1,
+ "m2mtags should contain one tag");
+ assert.strictEqual($('.modal .o_field_many2manytags[name="timmy"] .o_badge_text').text(),
+ 'gold', "tag name should have been correctly loaded");
+
+ form.destroy();
+ });
+
+
QUnit.module('FieldMany2Many');
QUnit.test('many2many kanban: edition', function (assert) {
@@ -7214,7 +7340,7 @@ QUnit.module('relational_fields', {
"should do a do_action with correct parameters");
});
- assert.strictEqual(form.$('a.o_form_uri:contains(first record)').length, 1,
+ assert.strictEqual(form.$('a.o_form_uri:contains(xphone)').length, 1,
"should contain a link");
form.$('a.o_form_uri').click(); // click on the link in readonly mode (should trigger do_action)
@@ -7226,7 +7352,7 @@ QUnit.module('relational_fields', {
"should contain one many2one");
assert.strictEqual(form.$('.o_field_widget select').val(), "product",
"widget should contain one select with the model");
- assert.strictEqual(form.$('.o_field_widget input').val(), "first record",
+ assert.strictEqual(form.$('.o_field_widget input').val(), "xphone",
"widget should contain one input with the record");
var options = _.map(form.$('.o_field_widget select > option'), function (el) {
@@ -7251,12 +7377,114 @@ QUnit.module('relational_fields', {
$dropdown.find('li:first()').click();
form.$buttons.find('.o_form_button_save').click();
- assert.strictEqual(form.$('a.o_form_uri').length, 1,
+ assert.strictEqual(form.$('a.o_form_uri:contains(gold)').length, 1,
"should contain a link with the new value");
form.destroy();
});
+ QUnit.test('default_get and onchange with a reference field', function (assert) {
+ assert.expect(8);
+
+ this.data.partner.fields.reference.default = 'product,37';
+ this.data.partner.onchanges = {
+ int_field: function (obj) {
+ if (obj.int_field !== 0) {
+ obj.reference = 'partner_type,' + obj.int_field;
+ }
+ },
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="int_field"/>' +
+ '<field name="reference"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ viewOptions: {
+ mode: 'edit',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'name_get') {
+ assert.step(args.model);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ assert.verifySteps(['product'], "the first name_get should have been done");
+ assert.strictEqual(form.$('.o_field_widget[name="reference"] select').val(), "product",
+ "reference field model should be correctly set");
+ assert.strictEqual(form.$('.o_field_widget[name="reference"] input').val(), "xphone",
+ "reference field value should be correctly set");
+
+ // trigger onchange
+ form.$('.o_field_widget[name=int_field]').val(12).trigger('input');
+
+ assert.verifySteps(['product', 'partner_type'], "the second name_get should have been done");
+ assert.strictEqual(form.$('.o_field_widget[name="reference"] select').val(), "partner_type",
+ "reference field model should be correctly set");
+ assert.strictEqual(form.$('.o_field_widget[name="reference"] input').val(), "gold",
+ "reference field value should be correctly set");
+ form.destroy();
+ });
+
+ QUnit.test('widget reference on char field, reset by onchange', function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].foo = 'product,37';
+ this.data.partner.onchanges = {
+ int_field: function (obj) {
+ obj.foo = 'product,' + obj.int_field;
+ },
+ };
+
+ var nbNameGet = 0;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="int_field"/>' +
+ '<field name="foo" widget="reference" readonly="1"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ mockRPC: function (route, args) {
+ if (args.model === 'product' && args.method === 'name_get') {
+ nbNameGet++;
+ }
+ return this._super(route, args);
+ },
+ });
+
+ assert.strictEqual(nbNameGet, 1,
+ "the first name_get should have been done");
+ assert.strictEqual(form.$('a[name="foo"]').text(), "xphone",
+ "foo field should be correctly set");
+
+ // trigger onchange
+ form.$('.o_field_widget[name=int_field]').val(41).trigger('input');
+
+ assert.strictEqual(nbNameGet, 2,
+ "the second name_get should have been done");
+ assert.strictEqual(form.$('a[name="foo"]').text(), "xpad",
+ "foo field should have been updated");
+ form.destroy();
+ });
+
});
});
});
diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js
index fbfdb315fa4..343f04d9fad 100644
--- a/addons/web/static/tests/helpers/test_utils.js
+++ b/addons/web/static/tests/helpers/test_utils.js
@@ -257,8 +257,11 @@ function addMockEnvironment(widget, params) {
var initialDebounce = DebouncedField.prototype.DEBOUNCE;
DebouncedField.prototype.DEBOUNCE = params.fieldDebounce || 0;
var initialSession, initialConfig, initialParameters;
+ initialSession = _.extend({}, session);
+ session.getTZOffset = function () {
+ return 0; // by default, but may be overriden in specific tests
+ };
if ('session' in params) {
- initialSession = _.extend({}, session);
_.extend(session, params.session);
}
if ('config' in params) {
@@ -283,8 +286,8 @@ function addMockEnvironment(widget, params) {
for (key in session) {
delete session[key];
}
- _.extend(session, initialSession);
}
+ _.extend(session, initialSession);
if ('config' in params) {
for (key in config) {
delete config[key];
diff --git a/addons/web/static/tests/views/basic_model_tests.js b/addons/web/static/tests/views/basic_model_tests.js
index 8c23fc4f97c..318b715a504 100644
--- a/addons/web/static/tests/views/basic_model_tests.js
+++ b/addons/web/static/tests/views/basic_model_tests.js
@@ -1323,6 +1323,64 @@ QUnit.module('Views', {
model.destroy();
});
+ QUnit.test('default_get: fetch many2one with default (empty & not) inside x2manys', function (assert) {
+ assert.expect(4);
+
+ this.data.partner.fields.o2m = {
+ string: "O2M", type: 'one2many', relation: 'partner', default: [
+ [6, 0, []],
+ [0, 0, {category: false}],
+ [0, 0, {category: 12}],
+ ],
+ };
+ this.data.partner.fields.category.type = 'many2one';
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (args.method === 'name_get' && args.model === 'partner_type') {
+ assert.deepEqual(args.args, [[12]], "should name_get on category 12");
+ }
+ return this._super(route, args);
+ },
+ });
+
+ var params = {
+ fieldNames: ['o2m'],
+ fields: this.data.partner.fields,
+ fieldsInfo: {
+ form: {
+ o2m: {
+ relatedFields: this.data.partner.fields,
+ fieldsInfo: {
+ list: {
+ category: {
+ relatedFields: { display_name: {} },
+ },
+ },
+ },
+ viewType: 'list',
+ },
+ },
+ },
+ modelName: 'partner',
+ type: 'record',
+ viewType: 'form',
+ };
+
+ model.load(params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.o2m.count, 2, "o2m field should contain 2 records");
+ assert.strictEqual(record.data.o2m.data[0].data.category, false,
+ "first category field should be empty");
+ assert.strictEqual(record.data.o2m.data[1].data.category.data.display_name, "gold",
+ "second category field should have been correctly fetched");
+ });
+
+ model.destroy();
+ });
+
QUnit.test('default_get: fetch x2manys inside x2manys', function (assert) {
assert.expect(3);
@@ -2007,5 +2065,25 @@ QUnit.module('Views', {
model.destroy();
});
+ QUnit.test('default_get with value false for a one2many', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.product_ids.default = false;
+ this.params.fieldNames = ['product_ids'];
+ this.params.res_id = undefined;
+ this.params.type = 'record';
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.deepEqual(record.data.product_ids.data, [], "o2m default should be []");
+ });
+
+ model.destroy();
+ });
});});
diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js
index 6aa01daac5a..9f9be3a4042 100644
--- a/addons/web/static/tests/views/calendar_tests.js
+++ b/addons/web/static/tests/views/calendar_tests.js
@@ -440,7 +440,9 @@ QUnit.module('Views', {
initialDate: initialDate,
},
session: {
- tzOffset: 120
+ getTZOffset: function () {
+ return 120;
+ },
},
mockRPC: function (route, args) {
if (args.method === "create") {
@@ -507,7 +509,7 @@ QUnit.module('Views', {
});
QUnit.test('create event with timezone in week mode with formViewDialog', function (assert) {
- assert.expect(7);
+ assert.expect(8);
this.data.event.records = [];
this.data.event.onchanges = {
@@ -541,7 +543,9 @@ QUnit.module('Views', {
initialDate: initialDate,
},
session: {
- tzOffset: 120
+ getTZOffset: function () {
+ return 120;
+ },
},
mockRPC: function (route, args) {
if (args.method === "create") {
@@ -554,12 +558,8 @@ QUnit.module('Views', {
"should send the context to create events");
}
if (args.method === "write") {
- assert.deepEqual(args.args[1], {
- "allday": false,
- "start": "2016-12-12 06:00:00",
- "stop": "2016-12-12 08:00:00"
- },
- "should move the event");
+ assert.deepEqual(args.args[1], expectedEvent,
+ "should move the event");
}
return this._super(route, args);
},
@@ -631,17 +631,34 @@ QUnit.module('Views', {
left = pos.left + 5;
top = pos.top + 5;
+ // Mode this event to another day
+ var expectedEvent = {
+ "allday": false,
+ "start": "2016-12-12 06:00:00",
+ "stop": "2016-12-12 08:00:00"
+ };
testUtils.triggerPositionalMouseEvent(left, top, "mousedown");
left = calendar.$('.fc-day:eq(1)').offset().left + 5;
testUtils.triggerPositionalMouseEvent(left, top, "mousemove");
testUtils.triggerPositionalMouseEvent(left, top, "mouseup");
+ // Move to "All day"
+ expectedEvent = {
+ "allday": true,
+ "start": "2016-12-12 00:00:00",
+ "stop": "2016-12-12 00:00:00"
+ };
+ testUtils.triggerPositionalMouseEvent(left, top, "mousedown");
+ top = calendar.$('.fc-day:eq(1)').offset().top + 5;
+ testUtils.triggerPositionalMouseEvent(left, top, "mousemove");
+ testUtils.triggerPositionalMouseEvent(left, top, "mouseup");
+
calendar.destroy();
$view.remove();
});
- QUnit.test('create all day event', function (assert) {
- assert.expect(2);
+ QUnit.test('create all day event in week mode', function (assert) {
+ assert.expect(3);
this.data.event.records = [];
@@ -664,7 +681,9 @@ QUnit.module('Views', {
initialDate: initialDate,
},
session: {
- tzOffset: 120
+ getTZOffset: function () {
+ return 120;
+ },
},
});
@@ -690,6 +709,8 @@ QUnit.module('Views', {
assert.strictEqual($newevent.text().replace(/[\s\n\r]+/g, ''), "newevent",
"should display the new event with time and title");
+ assert.strictEqual($newevent.parent().attr('colspan'), "2",
+ "should appear over two days.");
assert.deepEqual($newevent.data('fcSeg').event.record,
{
@@ -706,6 +727,141 @@ QUnit.module('Views', {
$view.remove();
});
+ QUnit.test('create all day event in week mode (no quickCreate)', function (assert) {
+ assert.expect(1);
+
+ this.data.event.records = [];
+
+ var calendar = createView({
+ View: CalendarView,
+ model: 'event',
+ data: this.data,
+ arch:
+ '<calendar class="o_calendar_test" '+
+ 'date_start="start" '+
+ 'date_stop="stop" '+
+ 'mode="week" '+
+ 'quick_add="False" '+
+ 'readonly_form_view_id="1">'+
+ '<field name="name"/>'+
+ '</calendar>',
+ archs: archs,
+ viewOptions: {
+ initialDate: initialDate,
+ },
+ session: {
+ getTZOffset: function () {
+ return 120;
+ },
+ },
+ intercepts: {
+ do_action: function (event) {
+ assert.deepEqual(event.data.action.context, {
+ "default_name": null,
+ "default_start": "2016-12-14 05:00:00",
+ "default_stop": "2016-12-15 17:00:00",
+ },
+ "should send the correct data to create events");
+ },
+ },
+ });
+
+ var $view = $('#qunit-fixture').contents();
+ $view.prependTo('body'); // => select with click position
+
+
+ var pos = calendar.$('.fc-bg td:eq(4)').offset();
+ try {
+ testUtils.triggerPositionalMouseEvent(pos.left+15, pos.top+15, "mousedown");
+ } catch (e) {
+ calendar.destroy();
+ $view.remove();
+ throw new Error('The test fails to simulate a click in the screen. Your screen is probably too small or your dev tools is open.');
+ }
+ pos = calendar.$('.fc-bg td:eq(5)').offset();
+ testUtils.triggerPositionalMouseEvent(pos.left+15, pos.top+15, "mousemove");
+ testUtils.triggerPositionalMouseEvent(pos.left+15, pos.top+15, "mouseup");
+
+ calendar.destroy();
+ $view.remove();
+ });
+
+ QUnit.test('create event in month mode', function (assert) {
+ assert.expect(4);
+
+ this.data.event.records = [];
+
+ var calendar = createView({
+ View: CalendarView,
+ model: 'event',
+ data: this.data,
+ arch:
+ '<calendar class="o_calendar_test" '+
+ 'event_open_popup="true" '+
+ 'date_start="start" '+
+ 'date_stop="stop" '+
+ 'mode="month" '+
+ 'readonly_form_view_id="1">'+
+ '<field name="name"/>'+
+ '</calendar>',
+ archs: archs,
+ viewOptions: {
+ initialDate: initialDate,
+ },
+ session: {
+ getTZOffset: function () {
+ return 120;
+ },
+ },
+ mockRPC: function (route, args) {
+ if (args.method === "create") {
+ assert.deepEqual(args.args[0], {
+ "name": "new event",
+ "start": "2016-12-14 05:00:00",
+ "stop": "2016-12-15 17:00:00",
+ },
+ "should send the correct data to create events");
+ }
+ return this._super(route, args);
+ },
+ });
+
+ var $view = $('#qunit-fixture').contents();
+ $view.prependTo('body'); // => select with click position
+
+ var pos = calendar.$('.fc-bg td:eq(20)').offset();
+ try {
+ testUtils.triggerPositionalMouseEvent(pos.left+15, pos.top+15, "mousedown");
+ } catch (e) {
+ calendar.destroy();
+ $view.remove();
+ throw new Error('The test fails to simulate a click in the screen. Your screen is probably too small or your dev tools is open.');
+ }
+ pos = calendar.$('.fc-bg td:eq(21)').offset();
+ testUtils.triggerPositionalMouseEvent(pos.left+15, pos.top+15, "mousemove");
+ testUtils.triggerPositionalMouseEvent(pos.left+15, pos.top+15, "mouseup");
+
+ $('.modal input:first').val('new event').trigger('input');
+ $('.modal button.btn:contains(Create)').trigger('click');
+ var $newevent = calendar.$('.fc-event:contains(new event)');
+
+ assert.strictEqual($newevent.text().replace(/[\s\n\r]+/g, ''), "newevent",
+ "should display the new event with time and title");
+ assert.strictEqual($newevent.parent().attr('colspan'), "2",
+ "should appear over two days.");
+
+ assert.deepEqual($newevent.data('fcSeg').event.record, {
+ display_name: "new event",
+ start: fieldUtils.parse.datetime("2016-12-14 05:00:00", this.data.event.fields.start, {isUTC: true}),
+ stop: fieldUtils.parse.datetime("2016-12-15 17:00:00", this.data.event.fields.stop, {isUTC: true}),
+ name: "new event",
+ id: 1
+ }, "the new record should have the utc datetime (quickCreate)");
+
+ calendar.destroy();
+ $view.remove();
+ });
+
QUnit.test('use mini calendar', function (assert) {
assert.expect(12);
@@ -728,7 +884,9 @@ QUnit.module('Views', {
initialDate: initialDate,
},
session: {
- tzOffset: 120
+ getTZOffset: function () {
+ return 120;
+ },
},
});
@@ -866,6 +1024,62 @@ QUnit.module('Views', {
assert.strictEqual($('#ui-datepicker-div:empty').length, 0, "should have a clean body");
});
+ QUnit.test('create and edit event in month mode (all_day: false)', function (assert) {
+ assert.expect(2);
+
+ var calendar = createView({
+ View: CalendarView,
+ model: 'event',
+ data: this.data,
+ arch:
+ '<calendar class="o_calendar_test" '+
+ 'string="Events" ' +
+ 'date_start="start" '+
+ 'date_stop="stop" '+
+ 'mode="month" '+
+ 'readonly_form_view_id="1">'+
+ '<field name="name"/>'+
+ '</calendar>',
+ archs: archs,
+ viewOptions: {
+ initialDate: initialDate,
+ },
+ session: {
+ getTZOffset: function () {
+ return -240;
+ },
+ },
+ });
+
+ // create a new event and edit it
+ var $cell = calendar.$('.fc-day-grid .fc-row:eq(4) .fc-day:eq(2)');
+ testUtils.triggerMouseEvent($cell, "mousedown");
+ testUtils.triggerMouseEvent($cell, "mouseup");
+ $('.modal-body input:first').val('coucou').trigger('input');
+
+ testUtils.intercept(calendar, 'do_action', function (event) {
+ assert.deepEqual(event.data.action,
+ {
+ type: "ir.actions.act_window",
+ res_model: "event",
+ views: [[false, "form"]],
+ target: "current",
+ context: {
+ "default_name": "coucou",
+ "default_start": "2016-12-27 11:00:00", // 7:00 + 4h
+ "default_stop": "2016-12-27 23:00:00", // 19:00 + 4h
+ }
+ },
+ "should open the form view with the context default values");
+ });
+
+ $('.modal button.btn:contains(Edit)').trigger('click');
+
+ calendar.destroy();
+ assert.strictEqual($('#ui-datepicker-div:empty').length, 0, "should have a clean body");
+ });
+
+
QUnit.test('readonly date_start field', function (assert) {
assert.expect(4);
@@ -981,37 +1195,6 @@ QUnit.module('Views', {
calendar.destroy();
});
- QUnit.test('ensure calendar adapts to screen size', function (assert) {
- assert.expect(2);
-
- // We want to be able to restore changes on 'body'
- var bodyStyle = $('body').attr('style');
-
- // Force the view height to be smaller than the natural calendar height
- // (FullCalendar requires that the height be modified on body not $target)
- var height = 500;
- $('body').height(height);
-
- var calendar = createView({
- View: CalendarView,
- model: 'event',
- data: this.data,
- arch: '<calendar mode="week" date_start="start" />',
- });
-
- var $scroller = calendar.$('.fc-scroller')
- assert.strictEqual($scroller.height() < height, true,
- "the scroller should be smaller than the view height");
- assert.strictEqual($scroller.prop('scrollHeight') > height, true,
- "the scroller should have a scrollbar");
- calendar.destroy();
-
- $('body').removeAttr('style');
- if (bodyStyle) {
- $('body').attr('style', bodyStyle);
- }
- });
-
QUnit.test('ensure events are still shown if filters give an empty domain', function (assert) {
assert.expect(2);
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index b1ded45d9a2..c71ec8f2d93 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -2885,7 +2885,9 @@ QUnit.module('Views', {
'</form>',
res_id: 1,
session: {
- tzOffset: 120
+ getTZOffset: function () {
+ return 120;
+ },
},
});
@@ -3779,6 +3781,60 @@ QUnit.module('Views', {
form.destroy();
});
+ QUnit.test('one2manys (list editable) inside one2manys are saved correctly', function (assert) {
+ assert.expect(3);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="p"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ "partner,false,form": '<form>' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>'
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'create') {
+ assert.deepEqual(args.args[0].p,
+ [[0, false, {
+ p: [[0, false, {display_name: "xtv"}]],
+ }]],
+ "create should be called with the correct arguments");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // add a o2m subrecord
+ form.$('.o_field_x2many_list_row_add a').click();
+ $('.modal-body .o_field_one2many .o_field_x2many_list_row_add a').click();
+ $('.modal-body input').val('xtv').trigger('input');
+ $('.modal-footer button:first').click(); // save & close
+ assert.strictEqual($('.modal').length, 0,
+ "dialog should be closed");
+
+ var row = form.$('.o_field_one2many .o_list_view .o_data_row');
+ assert.strictEqual(row.children()[0].textContent, '1 record',
+ "the cell should contains the number of record: 1");
+
+ form.$buttons.find('.o_form_button_save').click();
+
+ form.destroy();
+ });
+
QUnit.test('*_view_ref in context are passed correctly', function (assert) {
var done = assert.async();
assert.expect(4);
diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js
index be8a8a496af..84091f91a08 100644
--- a/addons/web/static/tests/views/kanban_tests.js
+++ b/addons/web/static/tests/views/kanban_tests.js
@@ -136,11 +136,12 @@ QUnit.module('Views', {
});
QUnit.test('basic grouped rendering with active field', function (assert) {
- assert.expect(4);
+ assert.expect(6);
// add active field on partner model and make all records active
this.data.partner.fields.active = {string: 'Active', type: 'char', default: true};
+ var envIDs = [1, 2, 3, 4]; // the ids that should be in the environment during this test
var kanban = createView({
View: KanbanView,
model: 'partner',
@@ -152,6 +153,12 @@ QUnit.module('Views', {
'<div><field name="foo"/></div>' +
'</t></templates></kanban>',
groupBy: ['bar'],
+ intercepts: {
+ env_updated: function (event) {
+ assert.deepEqual(event.data.ids, envIDs,
+ "should notify the environment with the correct ids");
+ },
+ },
});
// check available actions in kanban header's config dropdown
@@ -163,6 +170,7 @@ QUnit.module('Views', {
// archive the records of the first column
assert.strictEqual(kanban.$('.o_kanban_group:last .o_kanban_record').length, 3,
"last column should contain 3 records");
+ envIDs = [4];
kanban.$('.o_kanban_group:last .o_column_archive').click(); // click on 'Archive'
assert.strictEqual(kanban.$('.o_kanban_group:last .o_kanban_record').length, 0,
"last column should contain no record");
@@ -546,8 +554,9 @@ QUnit.module('Views', {
});
QUnit.test('can drag and drop a record from one column to the next', function (assert) {
- assert.expect(7);
+ assert.expect(9);
+ var envIDs = [1, 3, 2, 4]; // the ids that should be in the environment during this test
this.data.partner.fields.sequence = {type: 'number', string: "Sequence"};
var kanban = createView({
View: KanbanView,
@@ -569,6 +578,12 @@ QUnit.module('Views', {
}
return this._super(route, args);
},
+ intercepts: {
+ env_updated: function (event) {
+ assert.deepEqual(event.data.ids, envIDs,
+ "should notify the environment with the correct ids");
+ },
+ },
});
assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 2,
"column should contain 2 record(s)");
@@ -578,6 +593,7 @@ QUnit.module('Views', {
assert.strictEqual(kanban.$('.thisiseditable').length, 4, "all records should be editable");
var $record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first');
var $group = kanban.$('.o_kanban_group:nth-child(2)');
+ envIDs = [3, 2, 4, 1]; // first record of first column moved to the bottom of second column
testUtils.dragAndDrop($record, $group);
assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 1,
@@ -676,6 +692,42 @@ QUnit.module('Views', {
kanban.destroy();
});
+ QUnit.test('environment is updated when (un)folding groups', function (assert) {
+ assert.expect(3);
+
+ var envIDs = [1, 3, 2, 4]; // the ids that should be in the environment during this test
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban>' +
+ '<field name="product_id"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates>' +
+ '</kanban>',
+ groupBy: ['product_id'],
+ intercepts: {
+ env_updated: function (event) {
+ assert.deepEqual(envIDs, event.data.ids,
+ "should notify the environment with the correct ids");
+ },
+ },
+ });
+
+ // fold the second group and check that the res_ids it contains are no
+ // longer in the environment
+ envIDs = [1, 3];
+ kanban.$('.o_kanban_group:last .o_kanban_toggle_fold').click();
+
+ // re-open the second group and check that the res_ids it contains are
+ // back in the environment
+ envIDs = [1, 3, 2, 4];
+ kanban.$('.o_kanban_group:last .o_kanban_toggle_fold').click();
+
+ kanban.destroy();
+ });
+
QUnit.test('create a column in grouped on m2o', function (assert) {
assert.expect(13);
@@ -1282,8 +1334,9 @@ QUnit.module('Views', {
});
QUnit.test('resequence columns in grouped by m2o', function (assert) {
- assert.expect(4);
+ assert.expect(7);
+ var envIDs = [1, 3, 2, 4]; // the ids that should be in the environment during this test
var kanban = createView({
View: KanbanView,
model: 'partner',
@@ -1301,6 +1354,12 @@ QUnit.module('Views', {
}
return this._super.apply(this, arguments);
},
+ intercepts: {
+ env_updated: function (event) {
+ assert.deepEqual(event.data.ids, envIDs,
+ "should notify the environment with the correct ids");
+ },
+ },
});
assert.ok(kanban.$('.o_kanban_view').hasClass('ui-sortable'),
@@ -1313,6 +1372,7 @@ QUnit.module('Views', {
// there is a 100ms delay on the d&d feature (jquery sortable) for
// kanban columns, making it hard to test. So we rather bypass the d&d
// for this test, and directly call the event handler
+ envIDs = [2, 4, 1, 3]; // the columns will be inverted
kanban._onResequenceColumn({data: {ids: [5, 3]}});
kanban.update({}, {reload: false}); // re-render without reloading
@@ -1441,8 +1501,9 @@ QUnit.module('Views', {
});
QUnit.test('load more records in column', function (assert) {
- assert.expect(9);
+ assert.expect(12);
+ var envIDs = [1, 2, 4]; // the ids that should be in the environment during this test
var kanban = createView({
View: KanbanView,
model: 'partner',
@@ -1462,12 +1523,19 @@ QUnit.module('Views', {
}
return this._super.apply(this, arguments);
},
+ intercepts:{
+ env_updated: function (event) {
+ assert.deepEqual(event.data.ids, envIDs,
+ "should notify the environment with the correct ids");
+ },
+ },
});
assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_record').length, 2,
"there should be 2 records in the column");
// load more
+ envIDs = [1, 2, 3, 4]; // id 3 will be loaded
kanban.$('.o_kanban_group:eq(1)').find('.o_kanban_load_more').click();
assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_record').length, 3,
@@ -1476,6 +1544,8 @@ QUnit.module('Views', {
assert.verifySteps([[2, undefined], [2, undefined], [2, 2]],
"the records should be correctly fetched");
+ // reload
+ envIDs = [1, 2, 4]; // first group is limited again to 2 records
kanban.reload();
assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_record').length, 2,
"there should be 2 records in the column after reload");
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index 2247a64fa3c..4b33af9f0a6 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -1271,7 +1271,9 @@ QUnit.module('Views', {
data: this.data,
arch: '<tree><field name="date"/><field name="datetime"/></tree>',
session: {
- tzOffset: 120
+ getTZOffset: function () {
+ return 120;
+ },
},
});
@@ -1322,12 +1324,13 @@ QUnit.module('Views', {
});
QUnit.test('list view with nested groups', function (assert) {
- assert.expect(37);
+ assert.expect(42);
this.data.foo.records.push({id: 5, foo: "blip", int_field: -7, m2o: 1});
this.data.foo.records.push({id: 6, foo: "blip", int_field: 5, m2o: 2});
var nbRPCs = {readGroup: 0, searchRead: 0};
+ var envIDs = []; // the ids that should be in the environment during this test
var list = createView({
View: ListView,
@@ -1352,10 +1355,14 @@ QUnit.module('Views', {
return this._super.apply(this, arguments);
},
intercepts: {
- switch_view: function (event) {
+ switch_view: function (event) {
assert.strictEqual(event.data.res_id, 4,
"'switch_view' event has been triggered");
},
+ env_updated: function (event) {
+ assert.deepEqual(event.data.ids, envIDs,
+ "should notify the environment with the correct ids");
+ },
},
});
@@ -1398,6 +1405,7 @@ QUnit.module('Views', {
// open subgroup
nbRPCs = {readGroup: 0, searchRead: 0};
+ envIDs = [4, 5]; // the opened subgroup contains these two records
$openGroup.find('.o_group_header:nth(2)').click();
assert.strictEqual(nbRPCs.readGroup, 0, "should have done no read_group");
assert.strictEqual(nbRPCs.searchRead, 1, "should have done one search_read");
@@ -1414,6 +1422,7 @@ QUnit.module('Views', {
// sort by int_field (ASC) and check that open groups are still open
nbRPCs = {readGroup: 0, searchRead: 0};
+ envIDs = [5, 4]; // order of the records changed
list.$('thead th:last').click();
assert.strictEqual(nbRPCs.readGroup, 2, "should have done two read_groups");
assert.strictEqual(nbRPCs.searchRead, 1, "should have done one search_read");
@@ -1427,6 +1436,7 @@ QUnit.module('Views', {
// close first level group
nbRPCs = {readGroup: 0, searchRead: 0};
+ envIDs = []; // the group being closed, there is no more record in the environment
list.$('.o_group_header:first').click();
assert.strictEqual(nbRPCs.readGroup, 0, "should have done no read_group");
assert.strictEqual(nbRPCs.searchRead, 0, "should have done no search_read");
@@ -2519,6 +2529,55 @@ QUnit.module('Views', {
list.destroy();
});
+
+ QUnit.test('grouped list view, indentation for empty group', function (assert) {
+ assert.expect(3);
+
+ this.data.foo.fields.priority = {
+ string: "Priority",
+ type: "selection",
+ selection: [[1, "Low"], [2, "Medium"], [3, "High"]],
+ default: 1,
+ };
+ this.data.foo.records.push({id: 5, foo: "blip", int_field: -7, m2o: 1, priority: 2});
+ this.data.foo.records.push({id: 6, foo: "blip", int_field: 5, m2o: 1, priority: 3});
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree><field name="id"/></tree>',
+ groupBy: ['priority', 'm2o'],
+ mockRPC: function (route, args) {
+ // Override of the read_group to display the row even if there is no record in it,
+ // to mock the behavihour of some fields e.g stage_id on the sale order.
+ if (args.method === 'read_group' && args.kwargs.groupby[0] === "m2o") {
+ return $.when([
+ {
+ id: 8,
+ m2o:[1,"Value 1"],
+ m2o_count: 0
+ }, {
+ id: 2,
+ m2o:[2,"Value 2"],
+ m2o_count: 1
+ }
+ ]);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // open the first group
+ list.$('.o_group_header:first').click();
+ assert.strictEqual(list.$('th.o_group_name').eq(1).children().length, 1,
+ "There should be an empty element creating the indentation for the subgroup.");
+ assert.strictEqual(list.$('th.o_group_name').eq(1).children().eq(0).hasClass('fa'), true,
+ "The first element of the row name should have the fa class");
+ assert.strictEqual(list.$('th.o_group_name').eq(1).children().eq(0).is('span'), true,
+ "The first element of the row name should be a span");
+ list.destroy();
+ });
});
});
diff --git a/addons/web/static/tests/views/view_dialogs_tests.js b/addons/web/static/tests/views/view_dialogs_tests.js
index 8c46d8fff46..43bf4ee6694 100644
--- a/addons/web/static/tests/views/view_dialogs_tests.js
+++ b/addons/web/static/tests/views/view_dialogs_tests.js
@@ -140,6 +140,35 @@ QUnit.module('Views', {
dialog.destroy();
});
+ QUnit.test('SelectCreateDialog list view in readonly', function (assert) {
+ assert.expect(1);
+
+ var parent = createParent({
+ data: this.data,
+ archs: {
+ 'partner,false,list':
+ '<tree string="Partner" editable="bottom">' +
+ '<field name="display_name"/>' +
+ '<field name="foo"/>' +
+ '</tree>',
+ 'partner,false,search':
+ '<search/>'
+ },
+ });
+
+ var dialog = new dialogs.SelectCreateDialog(parent, {
+ res_model: 'partner',
+ }).open();
+
+ // click on the first row to see if the list is editable
+ dialog.$('.o_list_view tbody tr:first td:not(.o_list_record_selector):first').click();
+
+ assert.equal(dialog.$('.o_list_view tbody tr:first td:not(.o_list_record_selector):first input').length, 0,
+ "list view should not be editable in a SelectCreateDialog");
+
+ dialog.destroy();
+ });
+
});
-}); \ No newline at end of file
+});
diff --git a/addons/web/static/tests/widgets/data_export_tests.js b/addons/web/static/tests/widgets/data_export_tests.js
index 83169a64e1f..d251e3dea15 100644
--- a/addons/web/static/tests/widgets/data_export_tests.js
+++ b/addons/web/static/tests/widgets/data_export_tests.js
@@ -4,6 +4,7 @@ odoo.define('web.data_export_tests', function (require) {
var framework = require('web.framework');
var ListView = require('web.ListView');
var testUtils = require('web.test_utils');
+var data = require('web.data');
var createView = testUtils.createView;
@@ -127,6 +128,89 @@ QUnit.module('widgets', {
]);
});
+ QUnit.test('saving fields list when exporting data', function (assert) {
+ assert.expect(6);
+
+ var create = data.DataSet.prototype.create;
+
+ data.DataSet.prototype.create = function (data, options) {
+ assert.step('create');
+ return $.when([]);
+ };
+
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree><field name="foo"/></tree>',
+ viewOptions: {
+ sidebar: true,
+ },
+ mockRPC: function (route) {
+ if (route === '/web/export/formats') {
+ return $.when([
+ {tag: 'csv', label: 'CSV'},
+ {tag: 'xls', label: 'Excel'},
+ ]);
+ }
+ if (route === '/web/export/get_fields') {
+ return $.when([
+ {
+ field_type: "one2many",
+ string: "Activities",
+ required: false,
+ value: "activity_ids/id",
+ id: "activity_ids",
+ params: {"model": "mail.activity", "prefix": "activity_ids", "name": "Activities"},
+ relation_field: "res_id",
+ children: true,
+ },
+ ]);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ list.getParent().searchview = {
+ build_search_data: function () {
+ assert.step('build_search_data');
+ return {
+ contexts: [],
+ domains: [],
+ groupbys: [],
+ };
+ },
+ };
+
+ // Open the export modal
+ list.$('thead th.o_list_record_selector input').click();
+ list.sidebar.$('a:contains(Export)').click();
+ assert.strictEqual($('.modal').length, 1,
+ "a modal dialog should be open");
+
+ // Select 'Activities' in fields to export
+ assert.strictEqual($('.modal select.o_fields_list option').length, 0,
+ "the fields list should be empty");
+ $('.modal .o_export_tree_item:contains(Activities)').click();
+ $('.modal button:contains(Add)').click();
+ assert.strictEqual($('.modal select.o_fields_list option').length, 1,
+ "there should be one item in the fields list");
+
+ // Save fields list
+ $('.modal a:contains(Save fields list)').click();
+ $('.modal .o_save_list > input').val('fields list').trigger('input');
+ $('.modal .o_save_list > button').click();
+ assert.verifySteps(['build_search_data', 'create'],
+ "create should have been called");
+
+ // Close the modal and destroy list
+ $('.modal button span:contains(Close)').click();
+ list.destroy();
+
+ // restore create function
+ data.DataSet.prototype.create = create;
+ });
+
});
});
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 """<script type='text/javascript'>
window.parent['%s'](%s, %s);
@@ -316,7 +315,7 @@ class Web_Editor(http.Controller):
content = None
if url_info["customized"]:
# If the file is already customized, the content is found in the corresponding attachment
- content = custom_attachments.filtered(lambda a: a.url == url).datas.decode("base64")
+ content = base64.b64decode(custom_attachments.filtered(lambda a: a.url == url).datas)
else:
# If the file is not yet customized, the content is found by reading the local less file
module = url_info["module"]
@@ -354,14 +353,14 @@ class Web_Editor(http.Controller):
custom_attachment = IrAttachment.search([("url", "=", custom_url)])
if custom_attachment:
# If it was already modified, simply override the corresponding attachment content
- custom_attachment.write({"datas": content.encode("utf-8").encode("base64")})
+ custom_attachment.write({"datas": base64.b64encode(content.encode("utf-8"))})
else:
# If not, create a new attachment to copy the original LESS file content, with its modifications
IrAttachment.create(dict(
name = custom_url,
type = "binary",
mimetype = "text/less",
- datas = content.encode("utf-8").encode("base64"),
+ datas = base64.b64encode(content.encode("utf-8")),
datas_fname = url.split("/")[-1],
url = custom_url, # Having an attachment of "binary" type with an non empty "url" field
# is quite of an hack. This allows to fetch the "datas" field by adding
diff --git a/addons/web_editor/models/ir_qweb.py b/addons/web_editor/models/ir_qweb.py
index 4cceec10421..f2132c596f0 100644
--- a/addons/web_editor/models/ir_qweb.py
+++ b/addons/web_editor/models/ir_qweb.py
@@ -9,7 +9,8 @@ Also, adds methods to convert values back to Odoo models.
"""
import ast
-import cStringIO
+import base64
+import io
import itertools
import json
import logging
@@ -47,7 +48,10 @@ class QWeb(models.AbstractModel):
el.set('t-call', el.attrib.pop('t-snippet'))
name = self.env['ir.ui.view'].search([('key', '=', el.attrib.get('t-call'))]).display_name
thumbnail = el.attrib.pop('t-thumbnail', "oe-thumbnail")
- div = u'<div name="%s" data-oe-type="snippet" data-oe-thumbnail="%s">' % (escape(ir_qweb.unicodifier(name)), escape(ir_qweb.unicodifier(thumbnail)))
+ div = u'<div name="%s" data-oe-type="snippet" data-oe-thumbnail="%s">' % (
+ escape(pycompat.to_text(name)),
+ escape(pycompat.to_text(thumbnail))
+ )
return [self._append(ast.Str(div))] + self._compile_node(el, options) + [self._append(ast.Str(u'</div>'))]
def _compile_directive_install(self, el, options):
@@ -57,7 +61,11 @@ class QWeb(models.AbstractModel):
return []
name = el.attrib.get('string') or 'Snippet'
thumbnail = el.attrib.pop('t-thumbnail', 'oe-thumbnail')
- div = u'<div name="%s" data-oe-type="snippet" data-module-id="%s" data-oe-thumbnail="%s"><section/></div>' % (escape(ir_qweb.unicodifier(name)), module.id, escape(ir_qweb.unicodifier(thumbnail)))
+ div = u'<div name="%s" data-oe-type="snippet" data-module-id="%s" data-oe-thumbnail="%s"><section/></div>' % (
+ escape(pycompat.to_text(name)),
+ module.id,
+ escape(pycompat.to_text(thumbnail))
+ )
return [self._append(ast.Str(div))]
else:
return []
@@ -201,7 +209,7 @@ class DateTime(models.AbstractModel):
def attributes(self, record, field_name, options, values):
attrs = super(DateTime, self).attributes(record, field_name, options, values)
value = record[field_name]
- if isinstance(value, basestring):
+ if isinstance(value, pycompat.string_types):
value = fields.Datetime.from_string(value)
if value:
# convert from UTC (server timezone) to user timezone
@@ -274,7 +282,7 @@ class HTML(models.AbstractModel):
content = []
if element.text:
content.append(element.text)
- content.extend(html.tostring(child)
+ content.extend(html.tostring(child, encoding='unicode')
for child in element.iterchildren(tag=etree.Element))
return '\n'.join(content)
@@ -307,7 +315,7 @@ class Image(models.AbstractModel):
if max_width or max_height:
max_size = '%sx%s' % (max_width, max_height)
- sha = hashlib.sha1(getattr(record, '__last_update')).hexdigest()[0:7]
+ sha = hashlib.sha1(getattr(record, '__last_update').encode('utf-8')).hexdigest()[0:7]
max_size = '' if max_size is None else '/%s' % max_size
src = '/web/image/%s/%s/%s%s?unique=%s' % (record._name, record.id, field_name, max_size, sha)
@@ -325,7 +333,7 @@ class Image(models.AbstractModel):
img = '<img class="%s" src="%s" style="%s"%s%s/>' % \
(classes, src, options.get('style', ''), ' alt="%s"' % alt if alt else '', ' data-zoom="1" data-zoom-image="%s"' % src_zoom if src_zoom else '')
- return ir_qweb.unicodifier(img)
+ return pycompat.to_text(img)
local_url_re = re.compile(r'^/(?P<module>[^]]+)/static/(?P<rest>.+)$')
@@ -374,7 +382,7 @@ class Image(models.AbstractModel):
image = I.open(f)
image.load()
f.seek(0)
- return f.read().encode('base64')
+ return base64.b64encode(f.read())
except Exception:
logger.exception("Failed to load local image %r", url)
return None
@@ -390,7 +398,7 @@ class Image(models.AbstractModel):
req = requests.get(url, timeout=REMOTE_CONNECTION_TIMEOUT)
# PIL needs a seekable file-like image so wrap result in IO buffer
- image = I.open(cStringIO.StringIO(req.content))
+ image = I.open(io.BytesIO(req.content))
# force a complete load of the image data to validate it
image.load()
except Exception:
@@ -399,9 +407,9 @@ class Image(models.AbstractModel):
# don't use original data in case weird stuff was smuggled in, with
# luck PIL will remove some of it?
- out = cStringIO.StringIO()
+ out = io.BytesIO()
image.save(out, image.format)
- return out.getvalue().encode('base64')
+ return base64.b64encode(out.getvalue())
class Monetary(models.AbstractModel):
diff --git a/addons/web_editor/models/ir_ui_view.py b/addons/web_editor/models/ir_ui_view.py
index 7cf9e2f3a27..977f02f4b44 100644
--- a/addons/web_editor/models/ir_ui_view.py
+++ b/addons/web_editor/models/ir_ui_view.py
@@ -91,7 +91,7 @@ class IrUiView(models.Model):
@api.model
def to_field_ref(self, el):
# filter out meta-information inserted in the document
- attributes = {k: v for k, v in pycompat.items(el.attrib)
+ attributes = {k: v for k, v in el.attrib.items()
if not k.startswith('data-oe-')}
attributes['t-field'] = el.get('data-oe-expression')
@@ -127,7 +127,7 @@ class IrUiView(models.Model):
@api.model
def _view_obj(self, view_id):
- if isinstance(view_id, basestring):
+ if isinstance(view_id, pycompat.string_types):
return self.env.ref(view_id)
elif isinstance(view_id, pycompat.integer_types):
return self.browse(view_id)
diff --git a/addons/web_editor/static/src/js/widgets/ace.js b/addons/web_editor/static/src/js/widgets/ace.js
index cdcf5167e4a..03c767328d1 100644
--- a/addons/web_editor/static/src/js/widgets/ace.js
+++ b/addons/web_editor/static/src/js/widgets/ace.js
@@ -224,7 +224,15 @@ var ViewEditor = Widget.extend({
initType = 'less';
}
if (!this.options.doNotLoadViews) {
- initResID = (typeof this.viewKey === 'number' ? this.viewKey : _.findWhere(this.views, {xml_id: this.viewKey}).id);
+ if (typeof this.viewKey === "number") {
+ initResID = this.viewKey;
+ } else {
+ var view = _.findWhere(this.views, {xml_id: this.viewKey});
+ if (!view) {
+ view = _.findWhere(this.views, {key: this.viewKey});
+ }
+ initResID = view.id;
+ }
initType = 'xml';
}
}
diff --git a/addons/web_editor/tests/test_converter.py b/addons/web_editor/tests/test_converter.py
index 91d4b578383..761de27515f 100644
--- a/addons/web_editor/tests/test_converter.py
+++ b/addons/web_editor/tests/test_converter.py
@@ -136,7 +136,7 @@ class TestConvertBack(common.TransactionCase):
converter = self.env[model] if model in self.env else self.env['ir.qweb.field']
value_back = converter.from_html(model, record._fields[field], element)
- if isinstance(expected, str):
+ if isinstance(expected, bytes):
expected = expected.decode('utf-8')
self.assertEqual(value_back, expected)
diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py
index 0ab03af4522..8f200b7bc25 100644
--- a/addons/website/controllers/main.py
+++ b/addons/website/controllers/main.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
+import base64
import datetime
from itertools import islice
import json
@@ -38,11 +38,11 @@ class QueryURL(object):
def __call__(self, path=None, path_args=None, **kw):
path = path or self.path
- for key, value in pycompat.items(self.args):
+ for key, value in self.args.items():
kw.setdefault(key, value)
path_args = OrderedSet(path_args or []) | self.path_args
paths, fragments = {}, []
- for key, value in pycompat.items(kw):
+ for key, value in kw.items():
if value and key in path_args:
if isinstance(value, models.BaseModel):
paths[key] = slug(value)
@@ -87,7 +87,7 @@ class Website(Home):
response = super(Website, self).web_login(redirect=redirect, *args, **kw)
if not redirect and request.params['login_success']:
if request.env['res.users'].browse(request.uid).has_group('base.group_user'):
- redirect = '/web?' + request.httprequest.query_string
+ redirect = b'/web?' + request.httprequest.query_string
else:
redirect = '/'
return http.redirect_with_hash(redirect)
@@ -114,7 +114,7 @@ class Website(Home):
}
# /page/website.XXX --> /page/XXX
if page.startswith('website.'):
- return request.redirect('/page/%s?%s' % (page[8:], request.httprequest.query_string), code=301)
+ return request.redirect(b'/page/%s?%s' % (page[8:].encode('utf-8'), request.httprequest.query_string), code=301)
elif '.' not in page:
page = 'website.%s' % page
@@ -149,7 +149,7 @@ class Website(Home):
def create_sitemap(url, content):
return Attachment.create({
- 'datas': content.encode('base64'),
+ 'datas': base64.b64encode(content),
'mimetype': mimetype,
'type': 'binary',
'name': url,
@@ -162,7 +162,7 @@ class Website(Home):
create_date = fields.Datetime.from_string(sitemap.create_date)
delta = datetime.datetime.now() - create_date
if delta < SITEMAP_CACHE_TIME:
- content = sitemap.datas.decode('base64')
+ content = base64.b64decode(sitemap)
if not content:
# Remove all sitemaps in ir.attachments as we're going to regenerated them
@@ -318,9 +318,14 @@ class Website(Home):
def get_view_ids(self, xml_ids):
ids = []
+ View = request.env["ir.ui.view"].with_context(active_test=False)
for xml_id in xml_ids:
if "." in xml_id:
- record_id = request.env.ref(xml_id).id
+ # Get website-specific view if possible
+ record_id = View.search([
+ ("website_id", "=", request.website.id),
+ ("key", "=", xml_id),
+ ]).id or request.env.ref(xml_id).id
else:
record_id = int(xml_id)
ids.append(record_id)
@@ -381,7 +386,7 @@ class Website(Home):
action = action_id = None
# find the action_id: either an xml_id, the path, or an ID
- if isinstance(path_or_xml_id_or_id, basestring) and '.' in path_or_xml_id_or_id:
+ if isinstance(path_or_xml_id_or_id, pycompat.string_types) and '.' in path_or_xml_id_or_id:
action = request.env.ref(path_or_xml_id_or_id, raise_if_not_found=False)
if not action:
action = ServerActions.search([('website_path', '=', path_or_xml_id_or_id), ('website_published', '=', True)], limit=1)
diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py
index e6cedfe07d2..a20db972c0b 100644
--- a/addons/website/models/ir_qweb.py
+++ b/addons/website/models/ir_qweb.py
@@ -5,7 +5,6 @@ import ast
from odoo import models
from odoo.http import request
-from odoo.tools import pycompat
class QWeb(models.AbstractModel):
@@ -82,7 +81,7 @@ class QWeb(models.AbstractModel):
atts = super(QWeb, self)._get_dynamic_att(tagName, atts, options, values)
if options.get('rendering_bundle'):
return atts
- for name, value in pycompat.items(atts):
+ for name, value in atts.items():
atts[name] = self._website_build_attribute(tagName, name, value, options, values)
return atts
diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py
index fcb7e3015f2..eec00e2e897 100644
--- a/addons/website/models/ir_ui_view.py
+++ b/addons/website/models/ir_ui_view.py
@@ -50,7 +50,7 @@ class View(models.Model):
@api.model
def _view_obj(self, view_id):
- if isinstance(view_id, basestring):
+ if isinstance(view_id, pycompat.string_types):
if 'website_id' in self._context:
domain = [('key', '=', view_id), '|', ('website_id', '=', False), ('website_id', '=', self._context.get('website_id'))]
order = 'website_id'
@@ -69,6 +69,11 @@ class View(models.Model):
return view_id
@api.model
+ def _get_inheriting_views_arch_domain(self, view_id, model):
+ domain = super(View, self)._get_inheriting_views_arch_domain(view_id, model)
+ return ['|', ('website_id', '=', False), ('website_id', '=', self.env.context.get('website_id'))] + domain
+
+ @api.model
@tools.ormcache_context('self._uid', 'xml_id', keys=('website_id',))
def get_view_id(self, xml_id):
if 'website_id' in self._context and not isinstance(xml_id, pycompat.integer_types):
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index bfd5e3e5fc5..3cbcab51647 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -249,7 +249,7 @@ class Website(models.Model):
def get_url_localized(router, lang):
arguments = dict(request.endpoint_arguments)
- for key, val in list(pycompat.items(arguments)):
+ for key, val in list(arguments.items()):
if isinstance(val, models.BaseModel):
arguments[key] = val.with_context(lang=lang)
return router.build(request.endpoint, arguments)
@@ -261,7 +261,7 @@ class Website(models.Model):
shorts.append(lg_codes[0])
uri = get_url_localized(router, code) if request.endpoint else request.httprequest.path
if req.query_string:
- uri += '?' + req.query_string
+ uri += u'?' + req.query_string.decode('utf-8')
lang = {
'hreflang': ('-'.join(lg_codes)).lower(),
'short': lg_codes[0],
@@ -327,7 +327,7 @@ class Website(models.Model):
endpoint = rule.endpoint
methods = endpoint.routing.get('methods') or ['GET']
- converters = list(pycompat.values(rule._converters))
+ converters = list(rule._converters.values())
if not ('GET' in methods
and endpoint.routing['type'] == 'http'
and endpoint.routing['auth'] in ('none', 'public')
@@ -372,7 +372,7 @@ class Website(models.Model):
values = [{}]
# converters with a domain are processed after the other ones
convitems = sorted(
- pycompat.items(converters),
+ converters.items(),
key=lambda x: hasattr(x[1], 'domain') and (x[1].domain != '[]'))
for (i, (name, converter)) in enumerate(convitems):
newval = []
@@ -388,7 +388,7 @@ class Website(models.Model):
for value in values:
domain_part, url = rule.build(value, append_unknown=False)
page = {'loc': url}
- for key, val in pycompat.items(value):
+ for key, val in value.items():
if key.startswith('__'):
page[key[2:]] = val
if url in ('/sitemap.xml',):
@@ -414,7 +414,7 @@ class Website(models.Model):
def image_url(self, record, field, size=None):
""" Returns a local url that points to the image field of a given browse record. """
sudo_record = record.sudo()
- sha = hashlib.sha1(getattr(sudo_record, '__last_update')).hexdigest()[0:7]
+ sha = hashlib.sha1(getattr(sudo_record, '__last_update').encode('utf-8')).hexdigest()[0:7]
size = '' if size is None else '/%s' % size
return '/web/image/%s/%s/%s%s?unique=%s' % (record._name, record.id, field, size, sha)
@@ -494,7 +494,7 @@ class Menu(models.Model):
self.browse(to_delete).unlink()
for menu in data['data']:
mid = menu['id']
- if isinstance(mid, basestring):
+ if isinstance(mid, pycompat.string_types):
new_menu = self.create({'name': menu['name']})
replace_id(mid, new_menu.id)
for menu in data['data']:
diff --git a/addons/website/tests/test_converter.py b/addons/website/tests/test_converter.py
index e5047971fa9..ce74f1009d4 100644
--- a/addons/website/tests/test_converter.py
+++ b/addons/website/tests/test_converter.py
@@ -4,8 +4,6 @@
import unittest
from odoo.addons.http_routing.models.ir_http import slugify, unslug
-from odoo.tools import pycompat
-
class TestUnslug(unittest.TestCase):
@@ -25,7 +23,7 @@ class TestUnslug(unittest.TestCase):
'foo1': (None, None),
}
- for slug, expected in pycompat.items(tests):
+ for slug, expected in tests.items():
self.assertEqual(unslug(slug), expected)
diff --git a/addons/website/tests/test_views.py b/addons/website/tests/test_views.py
index 6eb5040f539..7293eae4c56 100644
--- a/addons/website/tests/test_views.py
+++ b/addons/website/tests/test_views.py
@@ -11,11 +11,10 @@ from lxml import etree as ET, html
from lxml.html import builder as h
from odoo.tests import common
-from odoo.tools import pycompat
def attrs(**kwargs):
- return {'data-oe-%s' % key: str(value) for key, value in pycompat.items(kwargs)}
+ return {'data-oe-%s' % key: str(value) for key, value in kwargs.items()}
class TestViewSaving(common.TransactionCase):
@@ -48,7 +47,7 @@ class TestViewSaving(common.TransactionCase):
self.view_id = self.env['ir.ui.view'].create({
'name': "Test View",
'type': 'qweb',
- 'arch': ET.tostring(self.arch, encoding='utf-8').decode('utf-8')
+ 'arch': ET.tostring(self.arch, encoding='unicode')
})
def test_embedded_extraction(self):
@@ -160,7 +159,7 @@ class TestViewSaving(common.TransactionCase):
h.LI(h.SPAN("Acme Corporation", attrs(model='res.company', id=1, field='name', expression="bob", type='char'))),
h.LI(h.SPAN("+12 3456789", attrs(model='res.company', id=1, field='phone', expression="edmund", type='char'))),
)
- ), encoding='utf-8')
+ ), encoding='unicode')
self.view_id.save(value=replacement, xpath='/div/div[2]')
# the xml_id of the view should be flagged as 'noupdate'
@@ -170,7 +169,7 @@ class TestViewSaving(common.TransactionCase):
self.assertEqual(company.name, "Acme Corporation")
self.assertEqual(company.phone, "+12 3456789")
self.eq(
- ET.fromstring(self.view_id.arch.encode('utf-8')),
+ ET.fromstring(self.view_id.arch),
h.DIV(
h.DIV(
h.H3("Column 1"),
@@ -191,29 +190,29 @@ class TestViewSaving(common.TransactionCase):
def test_save_escaped_text(self):
""" Test saving html special chars in text nodes """
view = self.env['ir.ui.view'].create({
- 'arch': '<t t-name="dummy"><p><h1>hello world</h1></p></t>',
+ 'arch': u'<t t-name="dummy"><p><h1>hello world</h1></p></t>',
'type': 'qweb'
})
# script and style text nodes should not escaped client side
- replacement = '<script>1 && "hello & world"</script>'
+ replacement = u'<script>1 && "hello & world"</script>'
view.save(replacement, xpath='/t/p/h1')
self.assertIn(
- replacement.replace('&', '&amp;'),
+ replacement.replace(u'&', u'&amp;'),
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;amp; &amp;lt;b&amp;gt;cie'
+ replacement = u'world &amp;amp; &amp;lt;b&amp;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('&', '&amp;'),
+ view.render().decode('utf-8').replace(u'&', u'&amp;'),
'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 49ad2901ccc..a7ac33d7997 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 f9df60422b2..cb50a8682b3 100644
--- a/addons/website_blog/models/website_blog.py
+++ b/addons/website_blog/models/website_blog.py
@@ -136,7 +136,7 @@ class BlogPost(models.Model):
default='{"background-image": "none", "background-color": "oe_black", "opacity": "0.2", "resize_class": ""}')
blog_id = fields.Many2one('blog.blog', 'Blog', required=True, ondelete='cascade')
tag_ids = fields.Many2many('blog.tag', string='Tags')
- content = fields.Html('Content', default=_default_content, translate=html_translate, sanitize_attributes=False)
+ content = fields.Html('Content', default=_default_content, translate=html_translate, sanitize=False)
teaser = fields.Text('Teaser', compute='_compute_teaser', inverse='_set_teaser')
teaser_manual = fields.Text(string='Teaser Content')
diff --git a/addons/website_crm_partner_assign/controllers/main.py b/addons/website_crm_partner_assign/controllers/main.py
index bb973843018..63d4926d185 100644
--- a/addons/website_crm_partner_assign/controllers/main.py
+++ b/addons/website_crm_partner_assign/controllers/main.py
@@ -14,7 +14,6 @@ from odoo.addons.http_routing.models.ir_http import slug, unslug
from odoo.addons.portal.controllers.portal import CustomerPortal
from odoo.addons.website_partner.controllers.main import WebsitePartnerPage
-from odoo.tools import pycompat
from odoo.tools.translate import _
@@ -150,7 +149,7 @@ class WebsiteAccount(CustomerPortal):
'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/data/crm_partner_assign_data.xml b/addons/website_crm_partner_assign/data/crm_partner_assign_data.xml
index a6541946090..6944f68287c 100644
--- a/addons/website_crm_partner_assign/data/crm_partner_assign_data.xml
+++ b/addons/website_crm_partner_assign/data/crm_partner_assign_data.xml
@@ -43,7 +43,7 @@
<p>Thanks,</p>
-${ctx['partner_id'].user_id and ctx['partner_id'].user_id.signature or '' | safe}
+${ctx['partner_id'].user_id and ctx['partner_id'].user_id.signature | safe or ''}
% if not ctx['partner_id'].user_id:
PS: It looks like you do not have an account manager assigned to you, please contact us.
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_crm_partner_assign/wizard/crm_forward_to_partner_view.xml b/addons/website_crm_partner_assign/wizard/crm_forward_to_partner_view.xml
index 2c95ea01549..7adc57ec617 100644
--- a/addons/website_crm_partner_assign/wizard/crm_forward_to_partner_view.xml
+++ b/addons/website_crm_partner_assign/wizard/crm_forward_to_partner_view.xml
@@ -17,7 +17,7 @@
</group>
<field name="assignation_lines" attrs="{'invisible': [('forward_type', 'in', ['single',False])]}">
<tree create="false" editable="bottom">
- <field name="lead_id" readonly="1"/>
+ <field name="lead_id" readonly="1" force_save="1" />
<field name="lead_location" readonly="1"/>
<field name="partner_assigned_id"/>
<field name="partner_location" readonly="1"/>
diff --git a/addons/website_event/controllers/main.py b/addons/website_event/controllers/main.py
index 694a71959d2..ff1f9c00ce1 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.http_routing.models.ir_http 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/<model("event.event"):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 1864b28379c..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):
@@ -75,7 +75,7 @@ class WebsiteEventTrackController(http.Controller):
days = {}
tracks_by_days = {}
- for day, tracks in pycompat.items(days_tracks):
+ for day, tracks in days_tracks.items():
tracks_by_days[day] = tracks
days[day] = self._prepare_calendar(event, tracks)
diff --git a/addons/website_form/controllers/main.py b/addons/website_form/controllers/main.py
index cdf1034c7a9..978692e894a 100644
--- a/addons/website_form/controllers/main.py
+++ b/addons/website_form/controllers/main.py
@@ -10,7 +10,7 @@ from psycopg2 import IntegrityError
from odoo import http
from odoo.http import request
-from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, pycompat
+from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
from odoo.tools.translate import _
from odoo.exceptions import ValidationError
from odoo.addons.base.ir.ir_qweb.fields import nl2br
@@ -118,7 +118,7 @@ class WebsiteForm(http.Controller):
error_fields = []
- for field_name, field_value in pycompat.items(values):
+ for field_name, field_value in values.items():
# If the value of the field if a file
if hasattr(field_value, 'filename'):
# Undo file upload field name indexing
@@ -164,7 +164,7 @@ class WebsiteForm(http.Controller):
if hasattr(dest_model, "website_form_input_filter"):
data['record'] = dest_model.website_form_input_filter(request, data['record'])
- missing_required_fields = [label for label, field in pycompat.items(authorized_fields) if field['required'] and not label in data['record']]
+ missing_required_fields = [label for label, field in authorized_fields.items() if field['required'] and not label in data['record']]
if any(error_fields):
raise ValidationError(error_fields + missing_required_fields)
diff --git a/addons/website_form/models/models.py b/addons/website_form/models/models.py
index 654fbfa9ee4..67364d59dc2 100644
--- a/addons/website_form/models/models.py
+++ b/addons/website_form/models/models.py
@@ -5,7 +5,6 @@ import itertools
from odoo import models, fields, api
from odoo.http import request
-from odoo.tools import pycompat
class website_form_config(models.Model):
@@ -42,7 +41,7 @@ class website_form_model(models.Model):
])
}
return {
- k: v for k, v in pycompat.items(self.get_authorized_fields(self.model))
+ k: v for k, v in self.get_authorized_fields(self.model).items()
if k in included
}
@@ -52,7 +51,7 @@ class website_form_model(models.Model):
model = self.env[model_name]
fields_get = model.fields_get()
- for key, val in pycompat.items(model._inherits):
+ for key, val in model._inherits.items():
fields_get.pop(val, None)
# Unrequire fields with default values
diff --git a/addons/website_forum/controllers/main.py b/addons/website_forum/controllers/main.py
index 1344f641f3f..911e3eb6d93 100644
--- a/addons/website_forum/controllers/main.py
+++ b/addons/website_forum/controllers/main.py
@@ -577,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)
@@ -711,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_hr_recruitment/controllers/main.py b/addons/website_hr_recruitment/controllers/main.py
index e1faf43fdc7..4556976d9cf 100644
--- a/addons/website_hr_recruitment/controllers/main.py
+++ b/addons/website_hr_recruitment/controllers/main.py
@@ -49,9 +49,9 @@ class WebsiteHrRecruitment(http.Controller):
countries = set(o.country_id for o in offices if o.country_id)
if department:
- jobs = (j for j in jobs if j.department_id and j.department_id.id == department.id)
+ jobs = [j for j in jobs if j.department_id and j.department_id.id == department.id]
if office_id and office_id in [x.id for x in offices]:
- jobs = (j for j in jobs if j.address_id and j.address_id.id == office_id)
+ jobs = [j for j in jobs if j.address_id and j.address_id.id == office_id]
else:
office_id = False
diff --git a/addons/website_hr_recruitment/security/website_hr_recruitment_security.xml b/addons/website_hr_recruitment/security/website_hr_recruitment_security.xml
index 61fddcd5b3c..43ecfc36521 100644
--- a/addons/website_hr_recruitment/security/website_hr_recruitment_security.xml
+++ b/addons/website_hr_recruitment/security/website_hr_recruitment_security.xml
@@ -34,7 +34,7 @@
<record id="hr_department_public" model="ir.rule">
<field name="name">Job department: Public</field>
<field name="model_id" ref="hr.model_hr_department"/>
- <field name="domain_force">[('jobs_ids.website_published', '=', True)]</field>
+ <field name="domain_force">['|', ('jobs_ids.website_published', '=', True), ('child_ids', 'not in', [])]</field>
<field name="groups" eval="[(4, ref('base.group_public'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
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 ac73c0653c1..62533f08a91 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 odoo import http
from odoo.http import request
diff --git a/addons/website_mail_channel/models/mail_channel.py b/addons/website_mail_channel/models/mail_channel.py
index c8ef8f3414c..f0b0f3665e2 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_quote/controllers/main.py b/addons/website_quote/controllers/main.py
index 89d93a1854e..c0b0648bfd8 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 c5b62d8cf28..98e271dfde1 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
@@ -78,10 +76,10 @@ class TableCompute(object):
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]]
@@ -371,7 +369,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):
@@ -489,7 +487,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
@@ -497,7 +495,7 @@ class WebsiteSale(http.Controller):
def _checkout_form_save(self, mode, checkout, all_values):
Partner = request.env['res.partner']
if mode[0] == 'new':
- partner_id = Partner.sudo().create(checkout)
+ partner_id = Partner.sudo().create(checkout).id
elif mode[0] == 'edit':
partner_id = int(all_values.get('partner_id', 0))
if partner_id:
@@ -515,7 +513,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
@@ -678,7 +676,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:
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/models/website_config_settings.py b/addons/website_sale/models/website_config_settings.py
index 3cb2ead0846..8dc53454412 100644
--- a/addons/website_sale/models/website_config_settings.py
+++ b/addons/website_sale/models/website_config_settings.py
@@ -90,7 +90,7 @@ class WebsiteConfigSettings(models.TransientModel):
sale_show_tax = fields.Selection([
('total', 'Tax-Included Prices'),
('subtotal', 'Tax-Excluded Prices')],
- "Product Prices", default='total')
+ "Product Prices", default='subtotal')
@api.model
def get_values(self):
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 2bc81416f82..90f6de0c48a 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
@@ -100,7 +100,7 @@ class WebsiteSaleDigital(CustomerPortal):
else:
return request.not_found()
elif attachment["datas"]:
- data = StringIO(base64.standard_b64decode(attachment["datas"]))
- return http.send_file(data, filename=attachment['name'], as_attachment=True)
+ data = io.BytesIO(base64.standard_b64decode(attachment["datas"]))
+ return http.send_file(data, filename=attachment['name'].encode('utf-8'), 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/<model("slide.slide", "[('channel_id.can_see', '=', True), ('datas', '!=', False), ('slide_type', '=', 'presentation')]"):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 01a0fda6dc2..92dbc89f51a 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
@@ -12,7 +13,7 @@ from werkzeug import urls
from odoo import api, fields, models, SUPERUSER_ID, _
from odoo.addons.http_routing.models.ir_http import slug
-from odoo.tools import image, pycompat
+from odoo.tools import image
from odoo.tools.translate import html_translate
from odoo.exceptions import Warning
@@ -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'&amp;',
ord('<'): u'&lt;',
ord('"'): u'&quot;',
@@ -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/cla/corporate/vauxoo.md b/doc/cla/corporate/vauxoo.md
index 036e1a63620..096c835ef55 100644
--- a/doc/cla/corporate/vauxoo.md
+++ b/doc/cla/corporate/vauxoo.md
@@ -36,4 +36,5 @@ Luis Gonzรกlez lgonzalez@vauxoo.com https://github.com/luisg123v
Oriana Maita oriana@vauxoo.com https://github.com/maitaoriana
Gabriela Mogollon gmogollon@vauxoo.com https://github.com/GavyMG
Jesus Zapata jesus@vauxoo.com https://github.com/JesusZapata
-Germana Oliveira germana@vauxoo.com https://github.com/goliveirab \ No newline at end of file
+Germana Oliveira germana@vauxoo.com https://github.com/goliveirab
+Mariano Fernandez mariano@vauxoo.com https://github.com/Batuto
diff --git a/doc/cla/individual/NothingCtrl.md b/doc/cla/individual/NothingCtrl.md
new file mode 100644
index 00000000000..bf58b0034d3
--- /dev/null
+++ b/doc/cla/individual/NothingCtrl.md
@@ -0,0 +1,9 @@
+Vietnam, 2017-08-15
+
+I hereby agree to the terms of the Odoo Individual Contributor License Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this declaration.
+
+Signed,
+
+Duong Bao Thang nothingctrl@gmail.com https://github.com/NothingCtrl
diff --git a/doc/cla/individual/borni-dhifi.md b/doc/cla/individual/borni-dhifi.md
new file mode 100644
index 00000000000..429f208f918
--- /dev/null
+++ b/doc/cla/individual/borni-dhifi.md
@@ -0,0 +1,9 @@
+Tunisia, 2017-08-22
+
+I hereby agree to the terms of the Odoo Individual Contributor License Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this declaration.
+
+Signed,
+
+Borni Dhifi dhifi.borni@gmail.com https://github.com/borni-dhifi
diff --git a/doc/cla/individual/repodevs.md b/doc/cla/individual/repodevs.md
new file mode 100644
index 00000000000..06f8ec2f166
--- /dev/null
+++ b/doc/cla/individual/repodevs.md
@@ -0,0 +1,10 @@
+Indonesia, 2017-08-12
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+Edi Santoso repodevs@gmail.com https://github.com/repodevs
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/cmdline.rst b/doc/reference/cmdline.rst
index c8b1954f1a9..37b0e9db646 100644
--- a/doc/reference/cmdline.rst
+++ b/doc/reference/cmdline.rst
@@ -159,7 +159,11 @@ database
- ``%h`` is replaced by the whole hostname the request is made on.
- ``%d`` is replaced by the subdomain the request is made on, with the
exception of ``www`` (so domain ``odoo.com`` and ``www.odoo.com`` both
- match the database ``odoo``)
+ match the database ``odoo``).
+
+ These operations are case sensitive. Add option ``(?i)`` to match all
+ databases (so domain ``odoo.com`` using ``(?i)%d`` matches the database
+ ``Odoo``).
.. option:: --db-template <template>
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/doc/reference/views.rst b/doc/reference/views.rst
index 2a9b5ece4bc..d5e5b10c58c 100644
--- a/doc/reference/views.rst
+++ b/doc/reference/views.rst
@@ -846,17 +846,13 @@ attributes:
Possible children of the view element are:
``field``
- declares fields to aggregate or to use in kanban *logic*. If the field is
- simply displayed in the kanban view, it does not need to be pre-declared.
+ declares fields to use in kanban *logic*. If the field is simply displayed in
+ the kanban view, it does not need to be pre-declared.
Possible attributes are:
``name`` (required)
the name of the field to fetch
- ``sum``, ``avg``, ``min``, ``max``, ``count``
- displays the corresponding aggregation at the top of a kanban column, the
- field's value is the label of the aggregation (a string). Only one
- aggregate operation per field is supported.
``templates``
defines a list of :ref:`reference/qweb` templates. Cards definition may be
diff --git a/odoo/addons/base/ir/ir_actions_report.py b/odoo/addons/base/ir/ir_actions_report.py
index 768d8dfa84d..d7adaf42363 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
@@ -53,9 +54,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'
@@ -371,13 +372,13 @@ class IrActionsReport(models.Model):
temporary_files = []
if header:
head_file_fd, head_file_path = tempfile.mkstemp(suffix='.html', prefix='report.header.tmp.')
- 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(header)
temporary_files.append(head_file_path)
files_command_args.extend(['--header-html', head_file_path])
if footer:
foot_file_fd, foot_file_path = tempfile.mkstemp(suffix='.html', prefix='report.footer.tmp.')
- 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(footer)
temporary_files.append(foot_file_path)
files_command_args.extend(['--footer-html', foot_file_path])
@@ -386,7 +387,7 @@ class IrActionsReport(models.Model):
for i, body in enumerate(bodies):
prefix = '%s%d.' % ('report.body.tmp.', i)
body_file_fd, body_file_path = tempfile.mkstemp(suffix='.html', prefix=prefix)
- with closing(os.fdopen(body_file_fd, 'w')) as body_file:
+ with closing(os.fdopen(body_file_fd, 'wb')) as body_file:
body_file.write(body)
paths.append(body_file_path)
temporary_files.append(body_file_path)
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 32738298293..3b19add6ea9 100644
--- a/odoo/addons/base/ir/ir_http.py
+++ b/odoo/addons/base/ir/ir_http.py
@@ -131,7 +131,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()
@@ -154,7 +154,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
@@ -295,7 +295,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
@@ -331,7 +331,7 @@ class IrHttp(models.AbstractModel):
# cache
etag = bool(request) and request.httprequest.headers.get('If-None-Match')
- retag = '"%s"' % hashlib.md5(last_update).hexdigest()
+ 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 <text string>, 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..9f3ef47faa0 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
@@ -524,7 +519,8 @@ class IrModelFields(models.Model):
if field.state == 'manual' and field.ttype == 'many2many':
rel_name = field.relation_table or model._fields[field.name].relation
tables_to_drop.add(rel_name)
- model._pop_field(field.name)
+ if field.state == 'manual':
+ model._pop_field(field.name)
if tables_to_drop:
# drop the relation tables that are not used by other fields
@@ -625,7 +621,7 @@ class IrModelFields(models.Model):
if not self.search([('model_id', '=', vals['relation']), ('name', '=', vals['relation_field']), ('ttype', '=', 'many2one')]):
raise UserError(_("Many2one %s on model %s does not exist!") % (vals['relation_field'], vals['relation']))
- self.clear_caches()
+ self.clear_caches() # for _existing_field_data()
if vals['model'] in self.pool:
# setup models; this re-initializes model in registry
@@ -684,7 +680,7 @@ class IrModelFields(models.Model):
res = super(IrModelFields, self).write(vals)
- self.clear_caches()
+ self.clear_caches() # for _existing_field_data()
if column_rename:
# rename column in database, and its corresponding index if present
@@ -771,7 +767,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 +785,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 +868,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 +1130,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 +1279,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 +1376,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 +1431,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 +1450,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 +1470,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 +1496,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('<link href="%s" rel="stylesheet"/>' % url_for(attachment.url))
+ response.append(u'<link href="%s" rel="stylesheet"/>' % 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('<script %s type="text/javascript" src="%s"></script>' % (async and 'async="async"' or '', url_for(self.js().url)))
+ response.append(u'<script %s type="text/javascript" src="%s"></script>' % (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'<br>\n')
+ return pycompat.to_text(string).replace(u'\n', u'<br>\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('<img src="data:%s;base64,%s">' % (Image.MIME[image.format], value))
+ return u'<img src="data:%s;base64,%s">' % (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('<img src="data:%s;base64,%s">' % ('png', barcode.encode('base64')))
+ **{key: value for key, value in options.items() if key in ['width', 'height', 'humanreadable']})
+ return u'<img src="data:png;base64,%s">' % 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..8bb333ea3fe 100644
--- a/odoo/addons/base/ir/ir_qweb/ir_qweb.py
+++ b/odoo/addons/base/ir/ir_qweb/ir_qweb.py
@@ -66,7 +66,7 @@ class IrQWeb(models.AbstractModel, QWeb):
# assume cache will be invalidated by third party on write to ir.ui.view
def _get_template_cache_keys(self):
""" Return the list of context keys to use for caching ``_get_template``. """
- return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations']
+ return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations', 'website_id']
# apply ormcache_context decorator unless in dev mode...
@tools.conditional(
@@ -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:
@@ -189,14 +189,14 @@ class IrQWeb(models.AbstractModel, QWeb):
# in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear
# by restarting the server after updating the source code (or using the "Clear server cache" in debug tools)
'xml' not in tools.config['dev_mode'],
- tools.ormcache('xmlid', 'options.get("lang", "en_US")', 'css', 'js', 'debug', 'async'),
+ tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', 'css', 'js', 'debug', 'async', keys=("website_id",)),
)
def _get_asset(self, xmlid, options, css=True, js=True, debug=False, async=False, values=None):
files, remains = self._get_asset_content(xmlid, options)
asset = AssetsBundle(xmlid, files, remains, env=self.env)
return asset.to_html(css=css, js=js, debug=debug, async=async, url_for=(values or {}).get('url_for', lambda url: url))
- @tools.ormcache('xmlid', 'options.get("lang", "en_US")')
+ @tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', keys=("website_id",))
def _get_asset_content(self, xmlid, options):
options = dict(options,
inherit_branding=False, inherit_branding_auto=False,
@@ -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("<?xml"):
- element = etree.fromstring(document)
- else:
+ elif os.path.exists(document):
element = etree.parse(document).getroot()
+ else:
+ element = etree.fromstring(document)
+
for node in element:
if node.get('t-name') == str(template):
return (node, document)
@@ -527,12 +522,13 @@ class QWeb(object):
* itertools
Define:
* escape
- * unicodifier (empty string for a None or False, otherwise unicode string)
+ * to_text (empty string for a None or False, otherwise unicode string)
+ * string_types (replacement for basestring)
"""
return ast.parse(dedent("""
from collections import OrderedDict
- from odoo.tools import pycompat
- from odoo.addons.base.ir.ir_qweb.qweb import escape, unicodifier, foreach_iterator
+ from odoo.tools.pycompat import to_text, string_types
+ from odoo.addons.base.ir.ir_qweb.qweb import escape, foreach_iterator
"""))
def _create_def(self, options, body, prefix='fn', lineno=None):
@@ -748,7 +744,7 @@ class QWeb(object):
attrib = {}
# If `el` introduced new namespaces, write them as attribute by using the
# `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:
attrib['xmlns'] = ns_definition
else:
@@ -757,9 +753,9 @@ 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.
- ns = itertools.chain(pycompat.items(options['nsmap']), pycompat.items(el.nsmap))
+ ns = itertools.chain(options['nsmap'].items(), el.nsmap.items())
nsprefixmap = {v: k for k, v in ns}
- for key, value in pycompat.items(el.attrib):
+ for key, value in el.attrib.items():
attrib_qname = etree.QName(key)
if attrib_qname.namespace:
attrib['%s:%s' % (nsprefixmap[attrib_qname.namespace], attrib_qname.localname)] = value
@@ -777,7 +773,7 @@ class QWeb(object):
if unqualified_el_tag == 't':
return content
- tag = u'<%s%s' % (el_tag, u''.join([u' %s="%s"' % (name, escape(unicodifier(value))) for name, value in pycompat.items(attrib)]))
+ tag = u'<%s%s' % (el_tag, u''.join([u' %s="%s"' % (name, escape(pycompat.to_text(value))) for name, value in attrib.items()]))
if unqualified_el_tag in self._void_elements:
return [self._append(ast.Str(tag + '/>'))] + 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 343aa6f570a..aa347f9d2ee 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
@@ -234,7 +234,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:
@@ -270,7 +270,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'))]
@@ -320,7 +320,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']
@@ -439,6 +439,15 @@ actual arch.
# Inheritance mecanism
#------------------------------------------------------
@api.model
+ def _get_inheriting_views_arch_domain(self, view_id, model):
+ return [
+ ['inherit_id', '=', view_id],
+ ['model', '=', model],
+ ['mode', '=', 'extension'],
+ ['active', '=', True],
+ ]
+
+ @api.model
def get_inheriting_views_arch(self, view_id, model):
"""Retrieves the architecture of views that inherit from the given view, from the sets of
views that should currently be used in the system. During the module upgrade phase it
@@ -453,13 +462,8 @@ actual arch.
:return: [(view_arch,view_id), ...]
"""
user_groups = self.env.user.groups_id
+ conditions = self._get_inheriting_views_arch_domain(view_id, model)
- conditions = [
- ['inherit_id', '=', view_id],
- ['model', '=', model],
- ['mode', '=', 'extension'],
- ['active', '=', True],
- ]
if self.pool._init and not self._context.get('load_all_views'):
# Module init currently in progress, only consider views from
# modules whose code is already loaded
@@ -709,7 +713,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::
@@ -852,7 +856,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]
@@ -917,7 +921,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]
@@ -935,7 +939,7 @@ actual arch.
def _read_template_keys(self):
""" Return the list of context keys to use for caching ``_read_template``. """
- return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations']
+ return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations', 'website_id']
# apply ormcache_context decorator unless in dev mode...
@api.model
@@ -949,7 +953,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
@@ -1119,12 +1123,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)
@@ -1201,7 +1205,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 e192a2783d3..0fdb51ffce2 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)
@@ -647,7 +648,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':
@@ -696,7 +697,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
@@ -717,7 +718,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)
@@ -749,7 +750,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/report_paperformat_views.xml b/odoo/addons/base/res/report_paperformat_views.xml
index 94f5e94b247..1f48320d088 100644
--- a/odoo/addons/base/res/report_paperformat_views.xml
+++ b/odoo/addons/base/res/report_paperformat_views.xml
@@ -29,7 +29,7 @@
<field name="header_line" />
<field name="header_spacing" />
<field name="dpi" />
- <field name="report_ids" widget="many2many_tags"/>
+ <field name="report_ids" widget="many2many_tags" options="{'not_delete': True}"/>
</group>
</form>
</field>
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 64b2ee18f57..7fd426ed2c5 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__)
@@ -250,7 +250,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'])])
@@ -275,7 +275,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()
@@ -285,7 +285,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)
@@ -408,7 +408,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
@@ -445,7 +445,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 c3f52ed6302..6fa0e251311 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):
@@ -336,7 +336,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):
@@ -664,7 +664,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 1cc720b3696..f0a98285a85 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)
@@ -744,7 +744,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):
@@ -766,7 +766,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)
@@ -780,7 +780,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))
@@ -820,7 +820,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
--- a/odoo/addons/base/static/img/public_user-image.png
+++ b/odoo/addons/base/static/img/public_user-image.png
Binary files differ
diff --git a/odoo/addons/base/tests/test_api.py b/odoo/addons/base/tests/test_api.py
index ad2dd1f0e85..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'])
diff --git a/odoo/addons/base/tests/test_expression.py b/odoo/addons/base/tests/test_expression.py
index 2ee7429f90a..ded008afa07 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('<span data-o-mail-quote="1">%s' % misc.html_escape(ext.decode('utf-8')), html)
+ self.assertIn(u'<span data-o-mail-quote="1">%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('<span data-o-mail-quote="1">%s</span>' % misc.html_escape(ext.decode('utf-8')), html)
+ self.assertIn(u'<span data-o-mail-quote="1">%s</span>' % 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('<span data-o-mail-quote="1">%s</span>' % misc.html_escape(text), new_html)
+ self.assertIn(u'<span data-o-mail-quote="1">%s</span>' % 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('<span data-o-mail-quote="1">%s</span>' % misc.html_escape(ext), html)
+ self.assertIn(u'<span data-o-mail-quote="1">%s</span>' % 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('<span data-o-mail-quote="1">%s</span>' % misc.html_escape(ext), html)
+ self.assertIn(u'<span data-o-mail-quote="1">%s</span>' % 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('<span data-o-mail-quote="1">%s</span>' % misc.html_escape(ext), html)
+ self.assertIn(u'<span data-o-mail-quote="1">%s</span>' % 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('<span data-o-mail-quote="1">%s</span>' % misc.html_escape(ext.decode('utf-8')), html)
+ self.assertIn(u'<span data-o-mail-quote="1">%s</span>' % 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"""
<font size="2" style="color: rgb(31, 31, 31); font-family: monospace; font-variant: normal; line-height: normal; ">test1</font>
<div style="color: rgb(31, 31, 31); font-family: monospace; font-variant: normal; line-height: normal; font-size: 12px; font-style: normal; ">
<b>test2</b></div><div style="color: rgb(31, 31, 31); font-family: monospace; font-variant: normal; line-height: normal; font-size: 12px; ">
@@ -19,7 +19,7 @@ test12</font></div><div><font color="#1f1f1f" face="monospace" size="2"><br></fo
<a href="javascript:alert('malicious code')">test link</a>
"""
-EDI_LIKE_HTML_SOURCE = """<div style="font-family: 'Lucida Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: #FFF; ">
+EDI_LIKE_HTML_SOURCE = u"""<div style="font-family: 'Lucida Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: #FFF; ">
<p>Hello ${object.partner_id.name},</p>
<p>A new invoice is available for you: </p>
<p style="border-left: 1px solid #8e0000; margin-left: 30px;">
@@ -63,7 +63,7 @@ EDI_LIKE_HTML_SOURCE = """<div style="font-family: 'Lucida Grande', Ubuntu, Aria
# QUOTES
-QUOTE_BLOCKQUOTE = """<html>
+QUOTE_BLOCKQUOTE = u"""<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
</head>
@@ -103,14 +103,14 @@ web: <a class="moz-txt-link-freetext" href="https://opener.am">https://opener.am
</body>
</html>"""
-QUOTE_BLOCKQUOTE_IN = ["""<blockquote cite="mid:CAEJSRZvWvud8c6Qp=wfNG6O1+wK3i_jb33qVrF7XyrgPNjnyUA@mail.gmail.com" type="cite" data-o-mail-quote-node="1" data-o-mail-quote="1">"""]
-QUOTE_BLOCKQUOTE_OUT = ["""--
+QUOTE_BLOCKQUOTE_IN = [u"""<blockquote cite="mid:CAEJSRZvWvud8c6Qp=wfNG6O1+wK3i_jb33qVrF7XyrgPNjnyUA@mail.gmail.com" type="cite" data-o-mail-quote-node="1" data-o-mail-quote="1">"""]
+QUOTE_BLOCKQUOTE_OUT = [u"""--
Opener B.V. - Business solutions driven by open source collaboration
Stefan Rijnhart - Consultant/developer"""]
-QUOTE_THUNDERBIRD_HTML = """<html>
+QUOTE_THUNDERBIRD_HTML = u"""<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
</head>
@@ -137,13 +137,13 @@ Raoul Poilvache
</html>"""
-QUOTE_THUNDERBIRD_HTML_IN = ["""<blockquote cite="mid:CAP76m_WWFH2KVrbjOxbaozvkmbzZYLWJnQ0n0sy9XpGaCWRf1g@mail.gmail.com" type="cite" data-o-mail-quote-node="1" data-o-mail-quote="1">"""]
-QUOTE_THUNDERBIRD_HTML_OUT = ["""<pre class="moz-signature" cols="72"><span data-o-mail-quote="1">--
+QUOTE_THUNDERBIRD_HTML_IN = [u"""<blockquote cite="mid:CAP76m_WWFH2KVrbjOxbaozvkmbzZYLWJnQ0n0sy9XpGaCWRf1g@mail.gmail.com" type="cite" data-o-mail-quote-node="1" data-o-mail-quote="1">"""]
+QUOTE_THUNDERBIRD_HTML_OUT = [u"""<pre class="moz-signature" cols="72"><span data-o-mail-quote="1">--
Raoul Poilvache
</span></pre>"""]
-QUOTE_HOTMAIL_HTML = """
+QUOTE_HOTMAIL_HTML = u"""
<html>
<head>
<style><!--
@@ -170,12 +170,12 @@ To: tartelette@example.com grosbedon@example.com<br><br>
</div>-- <br><div class="ecxgmail_signature">
Raoul Poilvache</div>
</div></div></div></body></html>"""
-QUOTE_HOTMAIL_HTML_IN = ["""I don't like that.<br><br>"""]
+QUOTE_HOTMAIL_HTML_IN = [u"""I don't like that.<br><br>"""]
QUOTE_HOTMAIL_HTML_OUT = [
- """<hr id="stopSpelling" data-o-mail-quote="1">""",
- """<div dir="ltr" data-o-mail-quote="1"><b data-o-mail-quote="1"><i data-o-mail-quote="1">Test reply. The suite.</i></b>"""]
+ u"""<hr id="stopSpelling" data-o-mail-quote="1">""",
+ u"""<div dir="ltr" data-o-mail-quote="1"><b data-o-mail-quote="1"><i data-o-mail-quote="1">Test reply. The suite.</i></b>"""]
-QUOTE_THUNDERBIRD_1 = """<div>On 11/08/2012 05:29 PM,
+QUOTE_THUNDERBIRD_1 = u"""<div>On 11/08/2012 05:29 PM,
<a href="mailto:dummy@example.com">dummy@example.com</a> wrote:<br></div>
<blockquote>
<div>I contact you about our meeting for tomorrow. Here is the
@@ -203,15 +203,15 @@ Raoul Grosbedonn&#233;e
</pre>"""
QUOTE_THUNDERBIRD_1_IN = [
- '<a href="mailto:dummy@example.com">dummy@example.com</a> ',
- '<blockquote data-o-mail-quote-node="1" data-o-mail-quote="1">',
- 'Ok for me. I am replying directly below your mail, using Thunderbird, with a signature.']
-QUOTE_THUNDERBIRD_1_OUT = ["""--
+ u'<a href="mailto:dummy@example.com">dummy@example.com</a> ',
+ u'<blockquote data-o-mail-quote-node="1" data-o-mail-quote="1">',
+ u'Ok for me. I am replying directly below your mail, using Thunderbird, with a signature.']
+QUOTE_THUNDERBIRD_1_OUT = [u"""--
Raoul Grosbedonnรฉe
"""]
-TEXT_1 = """I contact you about our meeting tomorrow. Here is the schedule I propose:
+TEXT_1 = u"""I contact you about our meeting tomorrow. Here is the schedule I propose:
9 AM: brainstorming about our new amazing business app
9.45 AM: summary
10 AM: meeting with Ignasse to present our app
@@ -219,16 +219,16 @@ Is everything ok for you ?
--
MySignature"""
-TEXT_1_IN = ["""I contact you about our meeting tomorrow. Here is the schedule I propose:
+TEXT_1_IN = [u"""I contact you about our meeting tomorrow. Here is the schedule I propose:
9 AM: brainstorming about our new amazing business app
9.45 AM: summary
10 AM: meeting with Ignasse to present our app
Is everything ok for you ?"""]
-TEXT_1_OUT = ["""
+TEXT_1_OUT = [u"""
--
MySignature"""]
-TEXT_2 = """Salut Raoul!
+TEXT_2 = u"""Salut Raoul!
Le 28 oct. 2012 ร  00:02, Raoul Grosbedon a รฉcrit :
> I contact you about our meeting tomorrow. Here is the schedule I propose: (quote)
@@ -253,8 +253,8 @@ Bert TARTOPOILS
bert.tartopoils@miam.miam
"""
-TEXT_2_IN = ["Salut Raoul!", "Of course. This seems viable."]
-TEXT_2_OUT = ["""
+TEXT_2_IN = [u"Salut Raoul!", "Of course. This seems viable."]
+TEXT_2_OUT = [u"""
> I contact you about our meeting tomorrow. Here is the schedule I propose: (quote)""",
"""
> 2012/10/27 Bert Tartopoils :
@@ -272,17 +272,17 @@ TEXT_2_OUT = ["""
# MISC
-GMAIL_1 = """Hello,<div><br></div><div>Ok for me. I am replying directly in gmail, without signature.</div><div><br></div><div>Kind regards,</div><div><br></div><div>Demo.<br><br><div>On Thu, Nov 8, 2012 at 5:29 PM, <span>&lt;<a href="mailto:dummy@example.com">dummy@example.com</a>&gt;</span> wrote:<br><blockquote><div>I contact you about our meeting for tomorrow. Here is the schedule I propose:</div><div><ul><li>9 AM: brainstorming about our new amazing business app&lt;/span&gt;&lt;/li&gt;</li>
+GMAIL_1 = u"""Hello,<div><br></div><div>Ok for me. I am replying directly in gmail, without signature.</div><div><br></div><div>Kind regards,</div><div><br></div><div>Demo.<br><br><div>On Thu, Nov 8, 2012 at 5:29 PM, <span>&lt;<a href="mailto:dummy@example.com">dummy@example.com</a>&gt;</span> wrote:<br><blockquote><div>I contact you about our meeting for tomorrow. Here is the schedule I propose:</div><div><ul><li>9 AM: brainstorming about our new amazing business app&lt;/span&gt;&lt;/li&gt;</li>
<li>9.45 AM: summary</li><li>10 AM: meeting with Fabien to present our app</li></ul></div><div>Is everything ok for you ?</div>
<div><p>-- <br>Administrator</p></div>
<div><p>Log in our portal at: <a href="http://localhost:8069#action=login&amp;db=mail_1&amp;login=demo">http://localhost:8069#action=login&amp;db=mail_1&amp;login=demo</a></p></div>
</blockquote></div><br></div>"""
-GMAIL_1_IN = ['Ok for me. I am replying directly in gmail, without signature.', '<blockquote data-o-mail-quote-node="1" data-o-mail-quote="1">']
+GMAIL_1_IN = [u'Ok for me. I am replying directly in gmail, without signature.', '<blockquote data-o-mail-quote-node="1" data-o-mail-quote="1">']
GMAIL_1_OUT = []
-HOTMAIL_1 = """<div>
+HOTMAIL_1 = u"""<div>
<div dir="ltr"><br>
I have an amazing company, i'm learning OpenERP, it is a small company yet, but plannig to grow up quickly.
<br><br>Kindest regards,<br>xxx<br>
@@ -309,19 +309,19 @@ HOTMAIL_1 = """<div>
</div>
</div>
</div>"""
-HOTMAIL_1_IN = ["""<div dir="ltr"><br>
+HOTMAIL_1_IN = [u"""<div dir="ltr"><br>
I have an amazing company, i'm learning OpenERP, it is a small company yet, but plannig to grow up quickly.
<br><br>Kindest regards,<br>xxx<br>"""]
HOTMAIL_1_OUT = [
- """<hr id="stopSpelling" data-o-mail-quote="1">""",
- """<pre data-o-mail-quote="1">
+ u"""<hr id="stopSpelling" data-o-mail-quote="1">""",
+ u"""<pre data-o-mail-quote="1">
<a href="http://openerp.com" target="_blank" data-o-mail-quote="1">http://openerp.com</a>
Belgium: +32.81.81.37.00
U.S.: +1 (650) 307-6736
India: +91 (79) 40 500 100
</pre>"""]
-MSOFFICE_1 = """
+MSOFFICE_1 = u"""
<div>
<div class="WordSection1">
<p class="MsoNormal">
@@ -423,8 +423,8 @@ MSOFFICE_1 = """
</div>
</div>"""
-MSOFFICE_1_IN = ['Our requirements are simple. Just looking to replace some spreadsheets for tracking quotes and possibly using the timecard module.']
-MSOFFICE_1_OUT = ['I noticed you recently downloaded OpenERP.', 'Uou mentioned you wish to use OpenERP in your own company.', 'Belgium: +32.81.81.37.00']
+MSOFFICE_1_IN = [u'Our requirements are simple. Just looking to replace some spreadsheets for tracking quotes and possibly using the timecard module.']
+MSOFFICE_1_OUT = [u'I noticed you recently downloaded OpenERP.', 'Uou mentioned you wish to use OpenERP in your own company.', 'Belgium: +32.81.81.37.00']
# ------------------------------------------------------------
@@ -432,7 +432,7 @@ MSOFFICE_1_OUT = ['I noticed you recently downloaded OpenERP.', 'Uou mentioned y
# ------------------------------------------------------------
# bug: read more not apparent, strange message in read more span
-BUG1 = """<pre>Hi Migration Team,
+BUG1 = u"""<pre>Hi Migration Team,
Paragraph 1, blah blah blah blah blah blah blah blah blah blah blah blah
blah blah blah blah blah blah blah blah blah blah blah blah blah blah
@@ -472,10 +472,10 @@ Tel: +32.81.81.37.00
Web: http://www.openerp.com</pre>"""
BUG_1_IN = [
- 'Hi Migration Team',
- 'Paragraph 1'
+ u'Hi Migration Team',
+ u'Paragraph 1'
]
-BUG_1_OUT = ["""
+BUG_1_OUT = [u"""
--
Olivier Laurent
Migration Manager
@@ -486,7 +486,7 @@ Tel: +32.81.81.37.00
Web: http://www.openerp.com"""]
-REMOVE_CLASS = """
+REMOVE_CLASS = u"""
<div style="FONT-SIZE: 12pt; FONT-FAMILY: 'Times New Roman'; COLOR: #000000">
<div>Hello</div>
<div>I have just installed Odoo 9 and I've got the following error:</div>
@@ -510,9 +510,9 @@ REMOVE_CLASS = """
</div>
"""
REMOVE_CLASS_IN = [
- '<div style="font-size: 12pt; font-family: \'Times New Roman\'; color: #000000">',
- 'An error occured in a modal and I will send you back the html to try opening one on your end']
+ u'<div style="font-size: 12pt; font-family: \'Times New Roman\'; color: #000000">',
+ u'An error occured in a modal and I will send you back the html to try opening one on your end']
REMOVE_CLASS_OUT = [
- '<div class="modal-backdrop in">',
- '<div class="modal-content openerp">',
- '<div class="modal-header">']
+ u'<div class="modal-backdrop in">',
+ u'<div class="modal-content openerp">',
+ u'<div class="modal-header">']
diff --git a/odoo/addons/base/tests/test_mimetypes.py b/odoo/addons/base/tests/test_mimetypes.py
index 2ba8cc05972..b2de483f7c6 100644
--- a/odoo/addons/base/tests/test_mimetypes.py
+++ b/odoo/addons/base/tests/test_mimetypes.py
@@ -3,9 +3,9 @@ import unittest
from odoo.tools.mimetypes import guess_mimetype
-PNG = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC'
-GIF = "R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs="
-BMP = """Qk1+AAAAAAAAAHoAAABsAAAAAQAAAAEAAAABABgAAAAAAAQAAAATCwAAEwsAAAAAAAAAAAAAQkdScwAAAAAAAAAAAA
+PNG = b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC'
+GIF = b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs="
+BMP = b"""Qk1+AAAAAAAAAHoAAABsAAAAAQAAAAEAAAABABgAAAAAAAQAAAATCwAAEwsAAAAAAAAAAAAAQkdScwAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAD///8A"""
JPG = """/9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAP
//////////////////////////////////////////////////////////////////////////////////////2wBDAf///////
@@ -20,18 +20,18 @@ AE/IX//2gAMAwEAAgADAAAAEB//xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oACAEDAQE/EH//xAAUEQEAA
class test_guess_mimetype(unittest.TestCase):
def test_default_mimetype_empty(self):
- mimetype = guess_mimetype('')
+ mimetype = guess_mimetype(b'')
# odoo implementation returns application/octet-stream by default
# if available, python-magic returns application/x-empty
self.assertIn(mimetype, ('application/octet-stream', 'application/x-empty'))
def test_default_mimetype(self):
- mimetype = guess_mimetype('', default='test')
+ mimetype = guess_mimetype(b'', default='test')
# if available, python-magic returns application/x-empty
self.assertIn(mimetype, ('test', 'application/x-empty'))
def test_mimetype_octet_stream(self):
- mimetype = guess_mimetype('\0')
+ mimetype = guess_mimetype(b'\0')
self.assertEqual(mimetype, 'application/octet-stream')
def test_mimetype_png(self):
diff --git a/odoo/addons/base/tests/test_orm.py b/odoo/addons/base/tests/test_orm.py
index bf95bee08f7..c24758dc9bf 100644
--- a/odoo/addons/base/tests/test_orm.py
+++ b/odoo/addons/base/tests/test_orm.py
@@ -141,7 +141,7 @@ class TestORM(TransactionCase):
partner_ids_by_year = defaultdict(list)
partners = self.env['res.partner']
- for name, date in pycompat.items(partners_data):
+ for name, date in partners_data.items():
p = partners.create(dict(name=name, date=date))
partner_ids.append(p.id)
partner_ids_by_day[date].append(p.id)
diff --git a/odoo/addons/base/tests/test_qweb.py b/odoo/addons/base/tests/test_qweb.py
index 7a8f921af55..ea4ef849b75 100644
--- a/odoo/addons/base/tests/test_qweb.py
+++ b/odoo/addons/base/tests/test_qweb.py
@@ -11,7 +11,7 @@ from lxml import etree
from odoo.modules import get_module_resource
from odoo.tests.common import TransactionCase
from odoo.addons.base.ir.ir_qweb import QWebException
-from odoo.tools import pycompat, misc, ustr
+from odoo.tools import misc, ustr
class TestQWebTField(TransactionCase):
@@ -406,7 +406,7 @@ class TestQWebNS(TransactionCase):
('cac', 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2'),
('cbc', 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2'),
}
- self.assertEqual(set(pycompat.items(result_etree.nsmap)), expected_ns)
+ self.assertEqual(set(result_etree.nsmap.items()), expected_ns)
# check that the t-call did its work
cac_lines = result_etree.findall('.//cac:line', namespaces={'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2'})
@@ -484,7 +484,7 @@ class FileSystemLoader(object):
if node.get('t-name') == name:
root = etree.Element('templates')
root.append(deepcopy(node))
- arch = etree.tostring(root, encoding='utf-8', xml_declaration=True)
+ arch = etree.tostring(root, encoding='unicode')
return arch
diff --git a/odoo/addons/base/tests/test_res_config.py b/odoo/addons/base/tests/test_res_config.py
index 0be7dcc7c8d..1e30e1f1a24 100644
--- a/odoo/addons/base/tests/test_res_config.py
+++ b/odoo/addons/base/tests/test_res_config.py
@@ -46,7 +46,7 @@ class TestResConfig(TransactionCase):
# Check types
self.assertIsInstance(res, tuple)
self.assertEqual(len(res), 2, "The result should contain 2 elements")
- self.assertIsInstance(res[0], basestring)
+ self.assertIsInstance(res[0], pycompat.string_types)
self.assertIsInstance(res[1], pycompat.integer_types)
# Check returned values
@@ -58,7 +58,7 @@ class TestResConfig(TransactionCase):
res = self.ResConfig.get_option_name(self.full_field_name)
# Check type
- self.assertIsInstance(res, basestring)
+ self.assertIsInstance(res, pycompat.string_types)
# Check returned value
self.assertEqual(res, self.expected_name)
diff --git a/odoo/addons/base/tests/test_search.py b/odoo/addons/base/tests/test_search.py
index 97670fdd805..1c2e60291d6 100644
--- a/odoo/addons/base/tests/test_search.py
+++ b/odoo/addons/base/tests/test_search.py
@@ -2,7 +2,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase
-from odoo.tools import pycompat
class test_search(TransactionCase):
@@ -108,7 +107,7 @@ class test_search(TransactionCase):
user_ids[u] = Users.create({'name': u, 'login': u}).id
cron_ids[u] = Cron.create({'name': u, 'model_id': self.env.ref('base.model_res_partner').id, 'user_id': user_ids[u]}).id
- ids = Cron.search([('id', 'in', list(pycompat.values(cron_ids)))], order='user_id').ids
+ ids = Cron.search([('id', 'in', list(cron_ids.values()))], order='user_id').ids
expected_ids = [cron_ids[l] for l in 'ABC']
self.assertEqual(ids, expected_ids)
@@ -128,7 +127,7 @@ class test_search(TransactionCase):
create('F', parent_id=cat_ids['D'])
expected_ids = [cat_ids[x] for x in 'ADEFBC']
- found_ids = Cats.search([('id', 'in', list(pycompat.values(cat_ids)))]).ids
+ found_ids = Cats.search([('id', 'in', list(cat_ids.values()))]).ids
self.assertEqual(found_ids, expected_ids)
def test_13_m2o_order_loop_multi(self):
diff --git a/odoo/addons/base/tests/test_translate.py b/odoo/addons/base/tests/test_translate.py
index 6d54a0b7886..19405c3145d 100644
--- a/odoo/addons/base/tests/test_translate.py
+++ b/odoo/addons/base/tests/test_translate.py
@@ -9,6 +9,8 @@ from odoo.tests.common import TransactionCase
class TranslationToolsTestCase(unittest.TestCase):
+ def assertItemsEqual(self, a, b, msg=None):
+ self.assertEqual(sorted(a), sorted(b), msg)
def test_quote_unquote(self):
diff --git a/odoo/addons/base/tests/test_view_validation.py b/odoo/addons/base/tests/test_view_validation.py
index a408b993605..ff8ce85267a 100644
--- a/odoo/addons/base/tests/test_view_validation.py
+++ b/odoo/addons/base/tests/test_view_validation.py
@@ -11,7 +11,7 @@ from odoo.tools.view_validation import (
valid_field_in_graph, valid_field_in_tree,
)
-invalid_form = etree.parse(io.BytesIO('''\
+invalid_form = etree.parse(io.BytesIO(b'''\
<form>
<label></label>
<group>
@@ -34,7 +34,7 @@ invalid_form = etree.parse(io.BytesIO('''\
</form>
''')).getroot()
-valid_form = etree.parse(io.BytesIO('''\
+valid_form = etree.parse(io.BytesIO(b'''\
<form string="">
<field name=""></field>
<field name=""></field>
@@ -54,7 +54,7 @@ valid_form = etree.parse(io.BytesIO('''\
</form>
''')).getroot()
-invalid_graph = etree.parse(io.BytesIO('''\
+invalid_graph = etree.parse(io.BytesIO(b'''\
<graph>
<label/>
<group>
@@ -66,14 +66,14 @@ invalid_graph = etree.parse(io.BytesIO('''\
</graph>
''')).getroot()
-valid_graph = etree.parse(io.BytesIO('''\
+valid_graph = etree.parse(io.BytesIO(b'''\
<graph string="">
<field name=""></field>
<field name=""></field>
</graph>
''')).getroot()
-invalid_tree = etree.parse(io.BytesIO('''\
+invalid_tree = etree.parse(io.BytesIO(b'''\
<tree>
<group>
<div>
@@ -84,7 +84,7 @@ invalid_tree = etree.parse(io.BytesIO('''\
</tree>
''')).getroot()
-valid_tree = etree.parse(io.BytesIO('''\
+valid_tree = etree.parse(io.BytesIO(b'''\
<tree string="">
<field name=""></field>
<field name=""></field>
diff --git a/odoo/addons/base/tests/test_views.py b/odoo/addons/base/tests/test_views.py
index 3f007b5f71b..aba01b0aa32 100644
--- a/odoo/addons/base/tests/test_views.py
+++ b/odoo/addons/base/tests/test_views.py
@@ -144,7 +144,7 @@ class TestViewInheritance(ViewCase):
E.attribute(name, name='string'),
position='attributes'
)
- return etree.tostring(element)
+ return etree.tostring(element, encoding='unicode')
def makeView(self, name, parent=None, arch=None):
""" Generates a basic ir.ui.view with the provided name, parent and arch.
diff --git a/odoo/addons/test_converter/tests/test_html.py b/odoo/addons/test_converter/tests/test_html.py
index c9bb6769d08..e915785e333 100644
--- a/odoo/addons/test_converter/tests/test_html.py
+++ b/odoo/addons/test_converter/tests/test_html.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 os
@@ -123,7 +123,7 @@ class TestCurrencyExport(TestExport):
converted, u'<span class="oe_currency_value">\u20110.12</span>'
u'\N{NO-BREAK SPACE}{symbol}'.format(
obj=obj,
- symbol=currency.symbol.encode('utf-8')
+ symbol=currency.symbol
),)
def test_currency_pre(self):
@@ -136,9 +136,9 @@ class TestCurrencyExport(TestExport):
self.assertEqual(
converted,
u'{symbol}\N{NO-BREAK SPACE}'
- '<span class="oe_currency_value">0.12</span>'.format(
+ u'<span class="oe_currency_value">0.12</span>'.format(
obj=obj,
- symbol=currency.symbol.encode('utf-8')
+ symbol=currency.symbol
),)
def test_currency_precision(self):
@@ -151,10 +151,10 @@ class TestCurrencyExport(TestExport):
self.assertEqual(
converted,
- '<span class="oe_currency_value">0.12</span>'
+ u'<span class="oe_currency_value">0.12</span>'
u'\N{NO-BREAK SPACE}{symbol}'.format(
obj=obj,
- symbol=currency.symbol.encode('utf-8')
+ symbol=currency.symbol
),)
@@ -213,25 +213,23 @@ class TestBinaryExport(TestBasicExport):
with open(os.path.join(directory, 'test_vectors', 'image'), 'rb') as f:
content = f.read()
- encoded_content = content.encode('base64')
+ encoded_content = base64.b64encode(content)
value = converter.value_to_html(encoded_content, {})
self.assertEqual(
- value, '<img src="data:image/jpeg;base64,%s">' % (
- encoded_content
- ))
+ value, u'<img src="data:image/jpeg;base64,%s">' % encoded_content.decode('ascii'))
with open(os.path.join(directory, 'test_vectors', 'pdf'), 'rb') as f:
content = f.read()
with self.assertRaises(ValueError):
- converter.value_to_html(content.encode('base64'), {})
+ converter.value_to_html(base64.b64encode(content), {})
with open(os.path.join(directory, 'test_vectors', 'pptx'), 'rb') as f:
content = f.read()
with self.assertRaises(ValueError):
- converter.value_to_html(content.encode('base64'), {})
+ converter.value_to_html(base64.b64encode(content), {})
class TestSelectionExport(TestBasicExport):
diff --git a/odoo/addons/test_impex/models.py b/odoo/addons/test_impex/models.py
index f1e10b10abf..027739b2538 100644
--- a/odoo/addons/test_impex/models.py
+++ b/odoo/addons/test_impex/models.py
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
+from odoo.tools import pycompat
def selection_fn(model):
@@ -48,7 +49,7 @@ for name, field in MODELS:
@api.model
def name_search(self, name='', args=None, operator='ilike', limit=100):
- if isinstance(name, basestring) and name.split(':')[0] == self._name:
+ if isinstance(name, pycompat.string_types) and name.split(':')[0] == self._name:
records = self.search([('value', operator, int(name.split(':')[1]))])
return records.name_get()
else:
@@ -70,7 +71,7 @@ class One2ManyChild(models.Model):
@api.model
def name_search(self, name='', args=None, operator='ilike', limit=100):
- if isinstance(name, basestring) and name.split(':')[0] == self._name:
+ if isinstance(name, pycompat.string_types) and name.split(':')[0] == self._name:
records = self.search([('value', operator, int(name.split(':')[1]))])
return records.name_get()
else:
@@ -124,7 +125,7 @@ class Many2ManyChild(models.Model):
@api.model
def name_search(self, name='', args=None, operator='ilike', limit=100):
- if isinstance(name, basestring) and name.split(':')[0] == self._name:
+ if isinstance(name, pycompat.string_types) and name.split(':')[0] == self._name:
records = self.search([('value', operator, int(name.split(':')[1]))])
return records.name_get()
else:
diff --git a/odoo/addons/test_impex/tests/test_export.py b/odoo/addons/test_impex/tests/test_export.py
index 72a3b2a6f75..9c7311319e8 100644
--- a/odoo/addons/test_impex/tests/test_export.py
+++ b/odoo/addons/test_impex/tests/test_export.py
@@ -69,7 +69,7 @@ class test_integer_field(CreatorCase):
def test_huge(self):
self.assertEqual(
self.export(2**31-1),
- [[unicode(2**31-1)]])
+ [[pycompat.text_type(2**31-1)]])
class test_float_field(CreatorCase):
@@ -319,7 +319,7 @@ class test_m2o(CreatorCase):
record = self.env['export.integer'].create({'value': 42})
self.assertEqual(
self.export(record.id, fields=['value/.id', 'value/value']),
- [[unicode(record.id), u'42']])
+ [[pycompat.text_type(record.id), u'42']])
def test_external_id(self):
record = self.env['export.integer'].create({'value': 42})
diff --git a/odoo/addons/test_impex/tests/test_load.py b/odoo/addons/test_impex/tests/test_load.py
index d8c7e3d87d5..15331174d53 100644
--- a/odoo/addons/test_impex/tests/test_load.py
+++ b/odoo/addons/test_impex/tests/test_load.py
@@ -965,7 +965,7 @@ class test_o2m_multiple(ImporterCase):
class test_realworld(common.TransactionCase):
def test_bigfile(self):
- data = json.loads(pkgutil.get_data(self.__module__, 'contacts_big.json'))
+ data = json.loads(pkgutil.get_data(self.__module__, 'contacts_big.json').decode('utf-8'))
result = self.env['res.partner'].load(['name', 'mobile', 'email', 'image'], data)
self.assertFalse(result['messages'])
self.assertEqual(len(result['ids']), len(data))
@@ -973,7 +973,7 @@ class test_realworld(common.TransactionCase):
def test_backlink(self):
fnames = ["name", "type", "street", "city", "country_id", "category_id",
"supplier", "customer", "is_company", "parent_id"]
- data = json.loads(pkgutil.get_data(self.__module__, 'contacts.json'))
+ data = json.loads(pkgutil.get_data(self.__module__, 'contacts.json').decode('utf-8'))
result = self.env['res.partner'].load(fnames, data)
self.assertFalse(result['messages'])
self.assertEqual(len(result['ids']), len(data))
diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py
index 5fecc80f1ea..6a527b5759a 100644
--- a/odoo/addons/test_new_api/tests/test_new_fields.py
+++ b/odoo/addons/test_new_api/tests/test_new_fields.py
@@ -5,7 +5,7 @@ from datetime import date, datetime
from odoo.exceptions import AccessError, except_orm
from odoo.tests import common
-from odoo.tools import mute_logger, float_repr
+from odoo.tools import mute_logger, float_repr, pycompat
class TestFields(common.TransactionCase):
@@ -16,8 +16,8 @@ class TestFields(common.TransactionCase):
discussion = self.env.ref('test_new_api.discussion_0')
# read field as a record attribute or as a record item
- self.assertIsInstance(discussion.name, basestring)
- self.assertIsInstance(discussion['name'], basestring)
+ self.assertIsInstance(discussion.name, pycompat.string_types)
+ self.assertIsInstance(discussion['name'], pycompat.string_types)
self.assertEqual(discussion['name'], discussion.name)
# read it with method read()
diff --git a/odoo/addons/test_pylint/tests/test_pylint.py b/odoo/addons/test_pylint/tests/test_pylint.py
index 08a1b1110ca..86d193b161d 100644
--- a/odoo/addons/test_pylint/tests/test_pylint.py
+++ b/odoo/addons/test_pylint/tests/test_pylint.py
@@ -46,6 +46,7 @@ class TestPyLint(TransactionCase):
'old-ne-operator',
'old-octal-operator',
'parameter-unpacking',
+ 'invalid-string-codec',
'metaclass-assignment',
'deprecated-module',
@@ -75,6 +76,10 @@ class TestPyLint(TransactionCase):
'filter',
'zip',
+ 'basestring',
+ 'unichr',
+ 'unicode',
+
'file',
'reduce',
]
@@ -82,6 +87,8 @@ class TestPyLint(TransactionCase):
BAD_MODULES = [
'commands',
'cPickle',
+ 'csv',
+ 'cStringIO',
'md5',
'urllib',
'urllib2',
@@ -92,6 +99,7 @@ class TestPyLint(TransactionCase):
'htmlentitydefs',
'HTMLParser',
'Queue',
+ 'StringIO',
'UserDict',
'UserString',
'UserList',
@@ -139,4 +147,4 @@ class TestPyLint(TransactionCase):
else:
out, err = process.communicate()
if process.returncode:
- self.fail("pylint test failed:\n" + (out + "\n" + err).strip())
+ self.fail("pylint test failed:\n" + (b"\n" + out + b"\n" + err).decode('utf-8').strip())
diff --git a/odoo/api.py b/odoo/api.py
index dc51870f20a..4f4f93071fd 100644
--- a/odoo/api.py
+++ b/odoo/api.py
@@ -55,7 +55,7 @@ from weakref import WeakSet
from decorator import decorator
from werkzeug.local import Local, release_local
-from odoo.tools import frozendict, classproperty, pycompat
+from odoo.tools import frozendict, classproperty
_logger = logging.getLogger(__name__)
@@ -85,7 +85,7 @@ class Params(object):
params = []
for arg in self.args:
params.append(repr(arg))
- for item in sorted(pycompat.items(self.kwargs)):
+ for item in sorted(self.kwargs.items()):
params.append("%s=%r" % item)
return ', '.join(params)
@@ -100,7 +100,7 @@ class Meta(type):
# dummy parent class to catch overridden methods decorated with 'returns'
parent = type.__new__(meta, name, bases, {})
- for key, value in list(pycompat.items(attrs)):
+ for key, value in list(attrs.items()):
if not key.startswith('__') and callable(value):
# make the method inherit from decorators
value = propagate(getattr(parent, key, None), value)
@@ -155,6 +155,14 @@ def constrains(*args):
``@constrains`` only supports simple field names, dotted names
(fields of relational fields e.g. ``partner_id.customer``) are not
supported and will be ignored
+
+ ``@constrains`` will be triggered only if the declared fields in the
+ decorated method are included in the ``create`` or ``write`` call.
+ It implies that fields not present in a view will not trigger a call
+ during a record creation. A override of ``create`` is necessary to make
+ sure a constraint will always be triggered (e.g. to test the absence of
+ value).
+
"""
return attrsetter('_constrains', args)
@@ -938,13 +946,13 @@ class Environment(Mapping):
# make a full copy of the cache, and invalidate it
cache_dump = dict(
(field, dict(field_cache))
- for field, field_cache in pycompat.items(self.cache)
+ for field, field_cache in self.cache.items()
)
self.invalidate_all()
# re-fetch the records, and compare with their former cache
invalids = []
- for field, field_dump in pycompat.items(cache_dump):
+ for field, field_dump in cache_dump.items():
records = self[field.model_name].browse(f for f in field_dump if f)
for record in records:
try:
diff --git a/odoo/cli/server.py b/odoo/cli/server.py
index 180efcfb70f..d740dc7282a 100644
--- a/odoo/cli/server.py
+++ b/odoo/cli/server.py
@@ -12,7 +12,7 @@ GNU Public Licence.
"""
import atexit
-import csv
+import csv # pylint: disable=deprecated-module
import logging
import os
import signal
diff --git a/odoo/fields.py b/odoo/fields.py
index 18174f1d145..43ee8edbad1 100644
--- a/odoo/fields.py
+++ b/odoo/fields.py
@@ -11,6 +11,7 @@ import itertools
import logging
import pytz
+
try:
from xmlrpc.client import MAXINT
except ImportError:
@@ -19,12 +20,11 @@ except ImportError:
import psycopg2
-from odoo.sql_db import LazyCursor
-from odoo.tools import float_repr, float_round, frozendict, html_sanitize, human_size, pg_varchar, ustr, OrderedSet, pycompat
-from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT
-from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT
-from odoo.tools.translate import html_translate, _
-import odoo.tools.sql as sql
+from .sql_db import LazyCursor
+from .tools import float_repr, float_round, frozendict, html_sanitize, human_size, pg_varchar, ustr, OrderedSet, pycompat, sql
+from .tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT
+from .tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT
+from .tools.translate import html_translate, _
DATE_LENGTH = len(date.today().strftime(DATE_FORMAT))
DATETIME_LENGTH = len(datetime.now().strftime(DATETIME_FORMAT))
@@ -340,7 +340,7 @@ class Field(MetaField('DummyField', (object,), {})):
def __init__(self, string=Default, **kwargs):
kwargs['string'] = string
self._sequence = kwargs['_sequence'] = next(_global_seq)
- args = {key: val for key, val in pycompat.items(kwargs) if val is not Default}
+ args = {key: val for key, val in kwargs.items() if val is not Default}
self.args = args or EMPTY_DICT
self._setup_done = None
@@ -369,7 +369,7 @@ class Field(MetaField('DummyField', (object,), {})):
""" Set all field attributes at once (with slot defaults). """
# optimization: we assign slots only
assign = object.__setattr__
- for key, val in pycompat.items(self._slots):
+ for key, val in self._slots.items():
assign(self, key, attrs.pop(key, val))
if attrs:
assign(self, '_attrs', attrs)
@@ -498,7 +498,7 @@ class Field(MetaField('DummyField', (object,), {})):
def make_depends(deps):
return tuple(deps(model) if callable(deps) else deps)
- if isinstance(self.compute, basestring):
+ if isinstance(self.compute, pycompat.string_types):
# if the compute method has been overridden, concatenate all their _depends
self.depends = ()
for method in resolve_mro(model, self.compute, callable):
@@ -517,7 +517,7 @@ class Field(MetaField('DummyField', (object,), {})):
def _setup_related_full(self, model):
""" Setup the attributes of a related field. """
# fix the type of self.related if necessary
- if isinstance(self.related, basestring):
+ if isinstance(self.related, pycompat.string_types):
self.related = tuple(self.related.split('.'))
# determine the chain of fields, and make sure they are all set up
@@ -547,7 +547,7 @@ class Field(MetaField('DummyField', (object,), {})):
if not getattr(self, attr):
setattr(self, attr, getattr(field, prop))
- for attr, value in pycompat.items(field._attrs):
+ for attr, value in field._attrs.items():
if attr not in self._attrs:
setattr(self, attr, value)
@@ -655,7 +655,7 @@ class Field(MetaField('DummyField', (object,), {})):
model = model0.env.get(field.comodel_name)
# add self's model dependencies
- for mname, fnames in pycompat.items(model0._depends):
+ for mname, fnames in model0._depends.items():
model = model0.env[mname]
for fname in fnames:
field = model._fields[fname]
@@ -745,9 +745,7 @@ class Field(MetaField('DummyField', (object,), {})):
""" Convert ``value`` from the ``write`` format to the SQL format. """
if value is None or value is False:
return None
- if isinstance(value, unicode):
- return value.encode('utf8')
- return str(value)
+ return pycompat.to_native(value)
def convert_to_cache(self, value, record, validate=True):
""" Convert ``value`` to the cache format; ``value`` may come from an
@@ -973,7 +971,7 @@ class Field(MetaField('DummyField', (object,), {})):
for field in fields:
for record in records:
record._cache[field] = field.convert_to_cache(False, record, validate=False)
- if isinstance(self.compute, basestring):
+ if isinstance(self.compute, pycompat.string_types):
getattr(records, self.compute)()
else:
self.compute(records)
@@ -1047,14 +1045,14 @@ class Field(MetaField('DummyField', (object,), {})):
def determine_inverse(self, records):
""" Given the value of ``self`` on ``records``, inverse the computation. """
- if isinstance(self.inverse, basestring):
+ if isinstance(self.inverse, pycompat.string_types):
getattr(records, self.inverse)()
else:
self.inverse(records)
def determine_domain(self, records, operator, value):
""" Return a domain representing a condition on ``self``. """
- if isinstance(self.search, basestring):
+ if isinstance(self.search, pycompat.string_types):
return getattr(records, self.search)(operator, value)
else:
return self.search(records, operator, value)
@@ -1371,12 +1369,12 @@ class Char(_String):
return None
# we need to convert the string to a unicode object to be able
# to evaluate its length (and possibly truncate it) reliably
- return ustr(value)[:self.size].encode('utf8')
+ return pycompat.to_text(value)[:self.size]
def convert_to_cache(self, value, record, validate=True):
if value is None or value is False:
return False
- return ustr(value)[:self.size]
+ return pycompat.to_text(value)[:self.size]
class Text(_String):
@@ -1509,7 +1507,7 @@ class Date(Field):
def convert_to_cache(self, value, record, validate=True):
if not value:
return False
- if isinstance(value, basestring):
+ if isinstance(value, pycompat.string_types):
if validate:
# force parsing for validation
self.from_string(value)
@@ -1579,7 +1577,7 @@ class Datetime(Field):
def convert_to_cache(self, value, record, validate=True):
if not value:
return False
- if isinstance(value, basestring):
+ if isinstance(value, pycompat.string_types):
if validate:
# force parsing for validation
self.from_string(value)
@@ -1598,6 +1596,11 @@ class Datetime(Field):
assert record, 'Record expected'
return Datetime.to_string(Datetime.context_timestamp(record, Datetime.from_string(value)))
+# http://initd.org/psycopg/docs/usage.html#binary-adaptation
+# Received data is returned as buffer (in Python 2) or memoryview (in Python 3).
+_BINARY = memoryview
+if pycompat.PY2:
+ _BINARY = buffer #pylint: disable=buffer-builtin
class Binary(Field):
type = 'binary'
@@ -1619,11 +1622,15 @@ class Binary(Field):
# unicode in some circumstances, hence the str() cast here.
# This str() coercion will only work for pure ASCII unicode strings,
# on purpose - non base64 data must be passed as a 8bit byte strings.
- return psycopg2.Binary(str(value)) if value else None
+ if not value:
+ return None
+ if isinstance(value, bytes):
+ return psycopg2.Binary(value)
+ return psycopg2.Binary(pycompat.text_type(value).encode('ascii'))
def convert_to_cache(self, value, record, validate=True):
- if isinstance(value, buffer):
- return str(value)
+ if isinstance(value, _BINARY):
+ return bytes(value)
if isinstance(value, pycompat.integer_types) and \
(record._context.get('bin_size') or
record._context.get('bin_size_' + self.name)):
@@ -1724,14 +1731,14 @@ class Selection(Field):
if 'selection_add' in field.args:
# use an OrderedDict to update existing values
selection_add = field.args['selection_add']
- self.selection = list(pycompat.items(OrderedDict(self.selection + selection_add)))
+ self.selection = list(OrderedDict(self.selection + selection_add).items())
def _description_selection(self, env):
""" return the selection list (pairs (value, label)); labels are
translated according to context language
"""
selection = self.selection
- if isinstance(selection, basestring):
+ if isinstance(selection, pycompat.string_types):
return getattr(env[self.model_name], selection)()
if callable(selection):
return selection(env[self.model_name])
@@ -1748,7 +1755,7 @@ class Selection(Field):
def get_values(self, env):
""" return a list of the possible values """
selection = self.selection
- if isinstance(selection, basestring):
+ if isinstance(selection, pycompat.string_types):
selection = getattr(env[self.model_name], selection)()
elif callable(selection):
selection = selection(env[self.model_name])
@@ -1772,14 +1779,6 @@ class Selection(Field):
return item[1]
return False
- def convert_to_column(self, value, record, values=None):
- """ Convert ``value`` from the ``write`` format to the SQL format. """
- if value is None or value is False:
- return None
- if isinstance(value, unicode):
- return value.encode('utf8')
- return str(value)
-
class Reference(Selection):
type = 'reference'
@@ -1797,7 +1796,7 @@ class Reference(Selection):
if isinstance(value, BaseModel):
if not validate or (value._name in self.get_values(record.env) and len(value) <= 1):
return process(value._name, value.id) if value else False
- elif isinstance(value, basestring):
+ elif isinstance(value, pycompat.string_types):
res_model, res_id = value.split(',')
if record.env[res_model].browse(int(res_id)).exists():
return process(res_model, int(res_id))
@@ -1894,7 +1893,7 @@ class Many2one(_Relational):
super(Many2one, self)._setup_attrs(model, name)
# determine self.delegate
if not self.delegate:
- self.delegate = name in pycompat.values(model._inherits)
+ self.delegate = name in model._inherits.values()
def update_db(self, model, columns):
comodel = model.env[self.comodel_name]
@@ -2117,7 +2116,7 @@ class _RelationalMulti(_Relational):
self.depends += tuple(
self.name + '.' + arg[0]
for arg in self.domain
- if isinstance(arg, (tuple, list)) and isinstance(arg[0], basestring)
+ if isinstance(arg, (tuple, list)) and isinstance(arg[0], pycompat.string_types)
)
diff --git a/odoo/http.py b/odoo/http.py
index 13234c1044a..288f13be8e3 100644
--- a/odoo/http.py
+++ b/odoo/http.py
@@ -44,12 +44,12 @@ except ImportError:
psutil = None
import odoo
-from odoo.service.server import memory_info
-from odoo.service import security, model as service_model
-from odoo.tools.func import lazy_property
-from odoo.tools import ustr, consteq, frozendict, pycompat, unique
+from .service.server import memory_info
+from .service import security, model as service_model
+from .tools.func import lazy_property
+from .tools import ustr, consteq, frozendict, pycompat, unique
-from odoo.modules.module import module_manifest
+from .modules.module import module_manifest
_logger = logging.getLogger(__name__)
rpc_request = logging.getLogger(__name__ + '.rpc.request')
@@ -162,8 +162,12 @@ def redirect_with_hash(url, code=303):
# See extensive test page at http://greenbytes.de/tech/tc/httpredirects/
if request.httprequest.user_agent.browser in ('firefox',):
return werkzeug.utils.redirect(url, code)
+ # FIXME: decide whether urls should be bytes or text, apparently
+ # addons/website/controllers/main.py:91 calls this with a bytes url
+ # but addons/web/controllers/main.py:481 uses text... (blows up on login)
+ url = pycompat.to_text(url)
if urls.url_parse(url, scheme='http').scheme not in ('http', 'https'):
- url = 'http://' + url
+ url = u'http://' + url
url = url.replace("'", "%27").replace("<", "%3C")
return "<html><head><script>window.location = '%s' + location.hash;</script></head></html>" % url
@@ -286,7 +290,7 @@ class WebRequest(object):
def set_handler(self, endpoint, arguments, auth):
# is this needed ?
- arguments ={k: v for k, v in pycompat.items(arguments)
+ arguments ={k: v for k, v in arguments.items()
if not k.startswith("_ignored_")}
self.endpoint_arguments = arguments
self.endpoint = endpoint
@@ -301,7 +305,8 @@ class WebRequest(object):
and not isinstance(exception, werkzeug.exceptions.HTTPException):
odoo.tools.debugger.post_mortem(
odoo.tools.config, sys.exc_info())
- raise
+ # otherwise "no active exception to reraise"
+ raise exception
def _call_function(self, *args, **kwargs):
request = self
@@ -392,7 +397,7 @@ class WebRequest(object):
msg = '%s%s' % (token, max_ts)
secret = self.env['ir.config_parameter'].sudo().get_param('database.secret')
assert secret, "CSRF protection requires a configured database secret"
- hm = hmac.new(str(secret), msg, hashlib.sha1).hexdigest()
+ hm = hmac.new(secret.encode('ascii'), msg.encode('utf-8'), hashlib.sha1).hexdigest()
return '%so%s' % (hm, max_ts)
def validate_csrf(self, csrf):
@@ -416,7 +421,7 @@ class WebRequest(object):
msg = '%s%s' % (token, max_ts)
secret = self.env['ir.config_parameter'].sudo().get_param('database.secret')
assert secret, "CSRF protection requires a configured database secret"
- hm_expected = hmac.new(str(secret), msg, hashlib.sha1).hexdigest()
+ hm_expected = hmac.new(secret.encode('ascii'), msg.encode('utf-8'), hashlib.sha1).hexdigest()
return consteq(hm, hm_expected)
def route(route=None, **kw):
@@ -510,7 +515,7 @@ def route(route=None, **kw):
if isinstance(response, Response) or f.routing_type == 'json':
return response
- if isinstance(response, basestring):
+ if isinstance(response, (bytes, pycompat.text_type)):
return Response(response)
if isinstance(response, werkzeug.exceptions.HTTPException):
@@ -597,7 +602,7 @@ class JsonRequest(WebRequest):
request = self.session.pop('jsonp_request_%s' % (request_id,), '{}')
else:
# regular jsonrpc2
- request = self.httprequest.stream.read()
+ request = self.httprequest.stream.read().decode(self.httprequest.charset)
# Read POST content or POST Form Data named "request"
try:
@@ -761,7 +766,7 @@ class HttpRequest(WebRequest):
req = request.httprequest
if req.method == 'POST':
request.session.save_request_data()
- redirect = '/web/proxy/post{r.path}?{r.query_string}'.format(r=req)
+ redirect = '/web/proxy/post{r.full_path}'.format(r=req)
elif not request.params.get('noredirect'):
redirect = req.url
if redirect:
@@ -835,7 +840,7 @@ more details.
"""
response = Response(data, headers=headers)
if cookies:
- for k, v in pycompat.items(cookies):
+ for k, v in cookies.items():
response.set_cookie(k, v)
return response
@@ -876,7 +881,7 @@ class ControllerType(type):
super(ControllerType, cls).__init__(name, bases, attrs)
# flag old-style methods with req as first argument
- for k, v in pycompat.items(attrs):
+ for k, v in attrs.items():
if inspect.isfunction(v) and hasattr(v, 'original_func'):
# Set routing type on original functions
routing_type = v.routing.get('type')
@@ -1132,7 +1137,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
# NOTE we do not store files in the session itself to avoid loading them in memory.
# By storing them in the session store, we ensure every worker (even ones on other
# servers) can access them. It also allow stale files to be deleted by `session_gc`.
- for f in pycompat.values(req.files):
+ for f in req.files.values():
storename = 'werkzeug_%s_%s.file' % (self.sid, uuid.uuid4().hex)
path = os.path.join(root.session_store.path, storename)
with open(path, 'w') as fp:
@@ -1150,7 +1155,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
try:
if data:
# regenerate files filenames with the current session store
- for name, (storename, filename, content_type) in pycompat.items(data['files']):
+ for name, (storename, filename, content_type) in data['files'].items():
path = os.path.join(root.session_store.path, storename)
files.add(name, (path, filename, content_type))
yield werkzeug.datastructures.CombinedMultiDict([data['form'], files])
@@ -1158,7 +1163,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
yield None
finally:
# cleanup files
- for f, _, _ in pycompat.values(files):
+ for f, _, _ in files.values():
try:
os.unlink(f)
except IOError:
@@ -1380,13 +1385,13 @@ class Root(object):
if isinstance(result, Response) and result.is_qweb:
try:
result.flatten()
- except(Exception) as e:
+ except Exception as e:
if request.db:
result = request.registry['ir.http']._handle_exception(e)
else:
raise
- if isinstance(result, basestring):
+ if isinstance(result, (bytes, pycompat.text_type)):
response = Response(result, mimetype='text/html')
else:
response = result
@@ -1560,7 +1565,7 @@ def send_file(filepath_or_fp, mimetype=None, as_attachment=False, filename=None,
:param cache_timeout: the timeout in seconds for the headers.
"""
- if isinstance(filepath_or_fp, (str, unicode)):
+ if isinstance(filepath_or_fp, pycompat.string_types):
if not filename:
filename = os.path.basename(filepath_or_fp)
file = open(filepath_or_fp, 'rb')
@@ -1610,7 +1615,7 @@ def send_file(filepath_or_fp, mimetype=None, as_attachment=False, filename=None,
mtime,
size,
adler32(
- filename.encode('utf-8') if isinstance(filename, unicode)
+ filename.encode('utf-8') if isinstance(filename, pycompat.text_type)
else filename
) & 0xffffffff
))
diff --git a/odoo/loglevels.py b/odoo/loglevels.py
index 8c0242867d2..4380ff29427 100644
--- a/odoo/loglevels.py
+++ b/odoo/loglevels.py
@@ -37,13 +37,16 @@ def get_encodings(hint_encoding='utf-8'):
if prefenc:
yield prefenc
+# not using pycompat to avoid circular import: pycompat is in tools much of
+# which comes back to import loglevels
+text_type = type(u'')
def ustr(value, hint_encoding='utf-8', errors='strict'):
"""This method is similar to the builtin `unicode`, except
that it may try multiple encodings to find one that works
for decoding `value`, and defaults to 'utf-8' first.
- :param: value: the value to convert
- :param: hint_encoding: an optional encoding that was detecte
+ :param value: the value to convert
+ :param hint_encoding: an optional encoding that was detecte
upstream and should be tried first to decode ``value``.
:param str errors: optional `errors` flag to pass to the unicode
built-in to indicate how illegal character values should be
@@ -60,24 +63,24 @@ def ustr(value, hint_encoding='utf-8', errors='strict'):
# cases faster (isinstance/issubclass are significantly slower)
ttype = type(value)
- if ttype is unicode:
+ if ttype is text_type:
return value
# special short-circuit for str, as we still needs to support
# str subclasses such as `odoo.tools.unquote`
- if ttype is str or issubclass(ttype, str):
+ if ttype is bytes or issubclass(ttype, bytes):
# try hint_encoding first, avoids call to get_encoding()
# for the most common case
try:
- return unicode(value, hint_encoding, errors=errors)
+ return value.decode(hint_encoding, errors=errors)
except Exception:
pass
# rare: no luck with hint_encoding, attempt other ones
for ln in get_encodings(hint_encoding):
try:
- return unicode(value, ln, errors=errors)
+ return value.decode(ln, errors=errors)
except Exception:
pass
@@ -86,7 +89,7 @@ def ustr(value, hint_encoding='utf-8', errors='strict'):
# fallback for non-string values
try:
- return unicode(value)
+ return text_type(value)
except Exception:
raise UnicodeError('unable to convert %r' % (value,))
@@ -95,6 +98,6 @@ def exception_to_unicode(e):
if hasattr(e, 'args'):
return "\n".join((ustr(a) for a in e.args))
try:
- return unicode(e)
+ return text_type(e)
except Exception:
return u"Unknown message"
diff --git a/odoo/models.py b/odoo/models.py
index 81fba2863c7..d4e184ccdf2 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -148,7 +148,7 @@ class MetaModel(api.Meta):
self.module_to_models[self._module].append(self)
# check for new-api conversion error: leave comma after field definition
- for key, val in pycompat.items(attrs):
+ for key, val in attrs.items():
if type(val) is tuple and len(val) == 1 and isinstance(val[0], Field):
_logger.error("Trailing comma after field definition: %s.%s", self, key)
if isinstance(val, Field):
@@ -173,7 +173,7 @@ class NewId(object):
return False
__nonzero__ = __bool__
-IdType = pycompat.integer_types + (str, unicode, NewId)
+IdType = pycompat.integer_types + pycompat.string_types + (NewId,)
# maximum number of prefetched records
@@ -184,6 +184,7 @@ LOG_ACCESS_COLUMNS = ['create_uid', 'create_date', 'write_uid', 'write_date']
MAGIC_COLUMNS = ['id'] + LOG_ACCESS_COLUMNS
+@pycompat.implements_to_string
class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
""" Base class for Odoo models.
@@ -405,7 +406,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# determine inherited models
parents = cls._inherit
- parents = [parents] if isinstance(parents, basestring) else (parents or [])
+ parents = [parents] if isinstance(parents, pycompat.string_types) else (parents or [])
# determine the model's name
name = cls._name or (len(parents) == 1 and parents[0]) or cls.__name__
@@ -513,7 +514,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
cls._inherits.update(base._inherits)
- for mname, fnames in pycompat.items(base._depends):
+ for mname, fnames in base._depends.items():
cls._depends[mname] = cls._depends.get(mname, []) + fnames
for cons in base._constraints:
@@ -523,7 +524,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
cls._sql_constraints += base._sql_constraints
cls._sequence = cls._sequence or (cls._table + '_id_seq')
- cls._constraints = list(pycompat.values(cls._constraints))
+ cls._constraints = list(cls._constraints.values())
# update _inherits_children of parent models
for parent_name in cls._inherits:
@@ -769,7 +770,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# avoid broken transaction) and keep going
cr.execute('ROLLBACK TO SAVEPOINT model_load_save')
except Exception as e:
- message = (_('Unknown error during import:') + ' %s: %s' % (type(e), unicode(e)))
+ message = (_(u'Unknown error during import:') + u' %s: %s' % (type(e), e))
moreinfo = _('Resolve other errors first')
messages.append(dict(info, type='error', message=message, moreinfo=moreinfo))
# Failed for some reason, perhaps due to invalid data supplied,
@@ -874,7 +875,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
:returns: a list of triplets of (id, xid, record)
:rtype: list((int|None, str|None, dict))
"""
- field_names = {name: field.string for name, field in pycompat.items(self._fields)}
+ field_names = {name: field.string for name, field in self._fields.items()}
if self.env.lang:
field_names.update(self.env['ir.translation'].get_field_string(self._name))
@@ -886,7 +887,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# processing of response, but injects human readable in message
exc_vals = dict(base, record=record, field=field_names[field])
record = dict(base, type=type, record=record, field=field,
- message=unicode(exception.args[0]) % exc_vals)
+ message=pycompat.text_type(exception.args[0]) % exc_vals)
if len(exception.args) > 1 and exception.args[1]:
record.update(exception.args[1])
log(record)
@@ -1005,7 +1006,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
defaults = self._convert_to_write(defaults)
# add default values for inherited fields
- for model, names in pycompat.items(parent_fields):
+ for model, names in parent_fields.items():
defaults.update(self.env[model].default_get(names))
return defaults
@@ -1081,7 +1082,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
:rtype: etree._Element
"""
group = E.group(col="4")
- for fname, field in pycompat.items(self._fields):
+ for fname, field in self._fields.items():
if field.automatic:
continue
elif field.type in ('one2many', 'many2many', 'text', 'html'):
@@ -1263,7 +1264,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# fallback on default views methods if no ir.ui.view could be found
try:
arch_etree = getattr(self, '_get_default_%s_view' % view_type)()
- result['arch'] = etree.tostring(arch_etree, encoding='utf-8')
+ result['arch'] = etree.tostring(arch_etree, encoding='unicode')
result['type'] = view_type
result['name'] = 'default'
except AttributeError:
@@ -1503,14 +1504,14 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# avoid overriding inherited values when parent is set
avoid_models = {
parent_model
- for parent_model, parent_field in pycompat.items(self._inherits)
+ for parent_model, parent_field in self._inherits.items()
if parent_field in values
}
# compute missing fields
missing_defaults = {
name
- for name, field in pycompat.items(self._fields)
+ for name, field in self._fields.items()
if name not in values
if self._log_access and name not in MAGIC_COLUMNS
if not (field.inherited and field.related_field.model_name in avoid_models)
@@ -1521,7 +1522,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# override defaults with the provided values, never allow the other way around
defaults = self.default_get(list(missing_defaults))
- for name, value in pycompat.items(defaults):
+ for name, value in defaults.items():
if self._fields[name].type == 'many2many' and value and isinstance(value[0], pycompat.integer_types):
# convert a list of ids into a list of commands
defaults[name] = [(6, 0, value)]
@@ -1606,10 +1607,10 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
if field.relational and groups._fold_name in groups._fields:
fold = {group.id: group[groups._fold_name]
for group in groups.browse([key for key in result if key])}
- for key, line in pycompat.items(result):
+ for key, line in result.items():
line['__fold'] = fold.get(key, False)
- return list(pycompat.values(result))
+ return list(result.values())
@api.model
def _read_group_prepare(self, orderby, aggregated_fields, annotated_groupbys, query):
@@ -1714,7 +1715,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
value = False if value is None else value
gb = groupby_dict.get(key)
if gb and gb['type'] in ('date', 'datetime') and value:
- if isinstance(value, basestring):
+ if isinstance(value, pycompat.string_types):
dt_format = DEFAULT_SERVER_DATETIME_FORMAT if gb['type'] == 'datetime' else DEFAULT_SERVER_DATE_FORMAT
value = datetime.datetime.strptime(value, dt_format)
if gb['tz_convert']:
@@ -1821,7 +1822,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
"""
result = self._read_group_raw(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
- groupby = [groupby] if isinstance(groupby, basestring) else list(OrderedSet(groupby))
+ groupby = [groupby] if isinstance(groupby, pycompat.string_types) else list(OrderedSet(groupby))
dt = [
f for f in groupby
if self._fields[f.split(':')[0]].type in ('date', 'datetime')
@@ -1843,9 +1844,9 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
def _read_group_raw(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
self.check_access_rights('read')
query = self._where_calc(domain)
- fields = fields or [f.name for f in pycompat.values(self._fields) if f.store]
+ fields = fields or [f.name for f in self._fields.values() if f.store]
- groupby = [groupby] if isinstance(groupby, basestring) else list(OrderedSet(groupby))
+ groupby = [groupby] if isinstance(groupby, pycompat.string_types) else list(OrderedSet(groupby))
groupby_list = groupby[:1] if lazy else groupby
annotated_groupbys = [self._read_group_process_groupby(gb, query) for gb in groupby_list]
groupby_fields = [g['field'] for g in annotated_groupbys]
@@ -1917,7 +1918,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
self._read_group_resolve_many2one_fields(fetched_data, annotated_groupbys)
- data = ({k: self._read_group_prepare_data(k,v, groupby_dict) for k,v in pycompat.items(r)} for r in fetched_data)
+ data = ({k: self._read_group_prepare_data(k,v, groupby_dict) for k,v in r.items()} for r in fetched_data)
result = [self._read_group_format_result(d, annotated_groupbys, groupby, domain) for d in data]
if lazy:
# Right now, read_group only fill results in lazy mode (by default).
@@ -2023,7 +2024,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# fields which were required but have been removed (or will be added by
# another module)
cr = self._cr
- cols = [name for name, field in pycompat.items(self._fields)
+ cols = [name for name, field in self._fields.items()
if field.store and field.column_type]
cr.execute("SELECT a.attname, a.attnotnull"
" FROM pg_class c, pg_attribute a"
@@ -2123,7 +2124,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
recs = self.with_context(active_test=False).search([])
recs._recompute_todo(field)
- for field in pycompat.values(self._fields):
+ for field in self._fields.values():
if not field.store:
continue
@@ -2211,9 +2212,9 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
""" Determine inherited fields. """
# determine candidate inherited fields
fields = {}
- for parent_model, parent_field in pycompat.items(self._inherits):
+ for parent_model, parent_field in self._inherits.items():
parent = self.env[parent_model]
- for name, field in pycompat.items(parent._fields):
+ for name, field in parent._fields.items():
# inherited fields are implemented as related fields, with the
# following specific properties:
# - reading inherited fields should not bypass access rights
@@ -2226,13 +2227,13 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
)
# add inherited fields that are not redefined locally
- for name, field in pycompat.items(fields):
+ for name, field in fields.items():
if name not in self._fields:
self._add_field(name, field)
@api.model
def _inherits_check(self):
- for table, field_name in pycompat.items(self._inherits):
+ for table, field_name in self._inherits.items():
field = self._fields.get(field_name)
if not field:
_logger.info('Missing many2one field definition for _inherits reference "%s" in "%s", using default one.', field_name, self._name)
@@ -2245,7 +2246,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
field.ondelete = "cascade"
# reflect fields with delegate=True in dictionary self._inherits
- for field in pycompat.values(self._fields):
+ for field in self._fields.values():
if field.type == 'many2one' and not field.related and field.delegate:
if not field.required:
_logger.warning("Field %s with delegate=True must be required.", field)
@@ -2333,7 +2334,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# set up fields
bad_fields = []
- for name, field in pycompat.items(cls._fields):
+ for name, field in cls._fields.items():
try:
field.setup_full(self)
except Exception:
@@ -2352,11 +2353,11 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# map each field to the fields computed with the same method
groups = defaultdict(list)
- for field in pycompat.values(cls._fields):
+ for field in cls._fields.values():
if field.compute:
cls._field_computed[field] = group = groups[field.compute]
group.append(field)
- for fields in pycompat.values(groups):
+ for fields in groups.values():
compute_sudo = fields[0].compute_sudo
if not all(field.compute_sudo == compute_sudo for field in fields):
_logger.warning("%s: inconsistent 'compute_sudo' for computed fields: %s",
@@ -2369,7 +2370,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
if isinstance(self, Model):
# set up field triggers (on database-persisted models only)
- for field in pycompat.values(cls._fields):
+ for field in cls._fields.values():
# dependencies of custom fields may not exist; ignore that case
exceptions = (Exception,) if field.manual else ()
with tools.ignore(*exceptions):
@@ -2408,7 +2409,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
readonly = not (has_access('write') or has_access('create'))
res = {}
- for fname, field in pycompat.items(self._fields):
+ for fname, field in self._fields.items():
if allfields and fname not in allfields:
continue
if field.groups and not self.user_has_groups(field.groups):
@@ -2420,7 +2421,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
description['states'] = {}
if attributes:
description = {key: val
- for key, val in pycompat.items(description)
+ for key, val in description.items()
if key in attributes}
res[fname] = description
@@ -2531,7 +2532,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
if self._context.get('prefetch_fields', True) and field.prefetch:
fs.update(
f
- for f in pycompat.values(self._fields)
+ for f in self._fields.values()
# select fields that can be prefetched
if f.prefetch
# discard fields with groups that the user may not access
@@ -2986,7 +2987,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# split up fields into old-style and pure new-style ones
old_vals, new_vals, unknown = {}, {}, []
- for key, val in pycompat.items(vals):
+ for key, val in vals.items():
field = self._fields.get(key)
if field:
if field.store or field.inherited:
@@ -3060,7 +3061,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
direct = [] # list of direcly updated columns
has_trans = self.env.lang and self.env.lang != 'en_US'
single_lang = len(self.env['res.lang'].get_installed()) <= 1
- for name, val in pycompat.items(vals):
+ for name, val in vals.items():
field = self._fields[name]
if field and field.deprecated:
_logger.warning('Field %s.%s is deprecated: %s', self._name, name, field.deprecated)
@@ -3122,7 +3123,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# defaults in context must be removed when call a one2many or many2many
rel_context = {key: val
- for key, val in pycompat.items(self._context)
+ for key, val in self._context.items()
if not key.startswith('default_')}
# call the 'write' method of fields which are not columns
@@ -3135,7 +3136,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# write inherited fields on the corresponding parent records
unknown_fields = set(updend)
- for parent_model, parent_field in pycompat.items(self._inherits):
+ for parent_model, parent_field in self._inherits.items():
parent_ids = []
for sub_ids in cr.split_for_in_conditions(self.ids):
query = "SELECT DISTINCT %s FROM %s WHERE id IN %%s" % (parent_field, self._table)
@@ -3259,7 +3260,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# split up fields into old-style and pure new-style ones
old_vals, new_vals, unknown = {}, {}, []
- for key, val in pycompat.items(vals):
+ for key, val in vals.items():
field = self._fields.get(key)
if field:
if field.store or field.inherited:
@@ -3296,7 +3297,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# data of parent records to create or update, by model
tocreate = {
parent_model: {'id': vals.pop(parent_field, None)}
- for parent_model, parent_field in pycompat.items(self._inherits)
+ for parent_model, parent_field in self._inherits.items()
}
# list of column assignments defined as tuples like:
@@ -3311,7 +3312,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
upd_todo = []
unknown_fields = []
protected_fields = []
- for name, val in list(pycompat.items(vals)):
+ for name, val in list(vals.items()):
field = self._fields.get(name)
if not field:
unknown_fields.append(name)
@@ -3327,7 +3328,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
_logger.warning('No such field(s) in model %s: %s.', self._name, ', '.join(unknown_fields))
# create or update parent records
- for parent_model, parent_vals in pycompat.items(tocreate):
+ for parent_model, parent_vals in tocreate.items():
parent_id = parent_vals.pop('id')
if not parent_id:
parent_id = self.env[parent_model].create(parent_vals).id
@@ -3336,13 +3337,13 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
vals[self._inherits[parent_model]] = parent_id
# set boolean fields to False by default (to make search more powerful)
- for name, field in pycompat.items(self._fields):
+ for name, field in self._fields.items():
if field.type == 'boolean' and field.store and name not in vals:
vals[name] = False
# determine SQL values
self = self.browse()
- for name, val in pycompat.items(vals):
+ for name, val in vals.items():
field = self._fields[name]
if field.store and field.column_type:
column_val = field.convert_to_column(val, self, vals)
@@ -3374,7 +3375,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
if self.env.lang and self.env.lang != 'en_US':
# add translations for self.env.lang
- for name, val in pycompat.items(vals):
+ for name, val in vals.items():
field = self._fields[name]
if field.store and field.column_type and field.translate is True:
tname = "%s,%s" % (self._name, name)
@@ -3422,7 +3423,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# defaults in context must be removed when call a one2many or many2many
rel_context = {key: val
- for key, val in pycompat.items(self._context)
+ for key, val in self._context.items()
if not key.startswith('default_')}
# call the 'write' method of fields which are not columns
@@ -3727,11 +3728,11 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# build a black list of fields that should not be copied
blacklist = set(MAGIC_COLUMNS + ['parent_left', 'parent_right'])
- whitelist = set(name for name, field in pycompat.items(self._fields) if not field.inherited)
+ whitelist = set(name for name, field in self._fields.items() if not field.inherited)
def blacklist_given_fields(model):
# blacklist the fields that are given by inheritance
- for parent_model, parent_field in pycompat.items(model._inherits):
+ for parent_model, parent_field in model._inherits.items():
blacklist.add(parent_field)
if parent_field in default:
# all the fields of 'parent_model' are given by the record:
@@ -3740,17 +3741,17 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
else:
blacklist_given_fields(self.env[parent_model])
# blacklist deprecated fields
- for name, field in pycompat.items(model._fields):
+ for name, field in model._fields.items():
if field.deprecated:
blacklist.add(name)
blacklist_given_fields(self)
fields_to_copy = {name: field
- for name, field in pycompat.items(self._fields)
+ for name, field in self._fields.items()
if field.copy and name not in default and name not in blacklist}
- for name, field in pycompat.items(fields_to_copy):
+ for name, field in fields_to_copy.items():
if field.type == 'one2many':
# duplicate following the order of the ids because we'll rely on
# it later for copying translations in copy_translation()!
@@ -3788,7 +3789,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
old_wo_lang, new_wo_lang = (old + new).with_context(lang=None)
Translation = old.env['ir.translation']
- for name, field in pycompat.items(old._fields):
+ for name, field in old._fields.items():
if not field.copy:
continue
@@ -3971,7 +3972,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
"""
results = self._get_external_ids()
return {key: val[0] if val else ''
- for key, val in pycompat.items(results)}
+ for key, val in results.items()}
# backwards compatibility
get_xml_id = get_external_id
@@ -4344,7 +4345,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
target = self if update else self.browse([], self._prefetch)
return {
name: fields[name].convert_to_cache(value, target, validate=validate)
- for name, value in pycompat.items(values)
+ for name, value in values.items()
if name in fields
}
@@ -4354,14 +4355,14 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
"""
return {
name: self._fields[name].convert_to_record(value, self)
- for name, value in pycompat.items(values)
+ for name, value in values.items()
}
def _convert_to_write(self, values):
""" Convert the ``values`` dictionary into the format of :meth:`write`. """
fields = self._fields
result = {}
- for name, value in pycompat.items(values):
+ for name, value in values.items():
if name in fields:
field = fields[name]
value = field.convert_to_cache(value, self, validate=False)
@@ -4398,7 +4399,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
"""
if not func:
return self # support for an empty path of fields
- if isinstance(func, basestring):
+ if isinstance(func, pycompat.string_types):
recs = self
for name in func.split('.'):
recs = recs._mapped_func(operator.itemgetter(name))
@@ -4423,7 +4424,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
:param func: a function or a dot-separated sequence of field names
"""
- if isinstance(func, basestring):
+ if isinstance(func, pycompat.string_types):
name = func
func = lambda rec: any(rec.mapped(name))
return self.browse([rec.id for rec in self if func(rec)])
@@ -4440,7 +4441,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
if key is None:
recs = self.search([('id', 'in', self.ids)])
return self.browse(reversed(recs._ids)) if reverse else recs
- if isinstance(key, basestring):
+ if isinstance(key, pycompat.string_types):
key = itemgetter(key)
return self.browse(item.id for item in sorted(self, key=key, reverse=reverse))
@@ -4448,7 +4449,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
def update(self, values):
""" Update the records in ``self`` with ``values``. """
for record in self:
- for name, value in pycompat.items(values):
+ for name, value in values.items():
record[name] = value
#
@@ -4525,7 +4526,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
"""
if isinstance(item, BaseModel) and self._name == item._name:
return len(item) == 1 and item.id in self._ids
- elif isinstance(item, basestring):
+ elif isinstance(item, pycompat.string_types):
return item in self._fields
else:
raise TypeError("Mixing apples and oranges: %s in %s" % (item, self))
@@ -4618,11 +4619,8 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
def __str__(self):
return "%s%s" % (self._name, getattr(self, '_ids', ""))
-
- def __unicode__(self):
- return unicode(str(self))
-
- __repr__ = __str__
+ def __repr__(self):
+ return str(self)
def __hash__(self):
if hasattr(self, '_ids'):
@@ -4642,7 +4640,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
rs = inst[10:20] # subset of inst
nm = rs['name'] # name of first record in inst
"""
- if isinstance(key, basestring):
+ if isinstance(key, pycompat.string_types):
# important: one must call the field's getter
return self._fields[key].__get__(self, type(self))
elif isinstance(key, slice):
@@ -4696,7 +4694,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
if fnames is None:
if ids is None:
return self.env.invalidate_all()
- fields = list(pycompat.values(self._fields))
+ fields = list(self._fields.values())
else:
fields = [self._fields[n] for n in fnames]
@@ -4792,7 +4790,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
updates[frozendict(vals)].add(rec.id)
# update records in batch when possible
with recs.env.norecompute():
- for vals, ids in pycompat.items(updates):
+ for vals, ids in updates.items():
target = recs.browse(ids)
try:
target._write(dict(vals))
@@ -4832,7 +4830,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
if not result.get(names):
result[names] = node.attrib.get('on_change')
# traverse the subviews included in relational fields
- for subinfo in pycompat.values(info['fields'][name].get('views', {})):
+ for subinfo in info['fields'][name].get('views', {}).values():
process(etree.fromstring(subinfo['arch']), subinfo, names)
else:
for child in node:
@@ -4855,7 +4853,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
return
if res.get('value'):
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 res.get('domain'):
result.setdefault('domain', {}).update(res['domain'])
if res.get('warning'):
@@ -4984,7 +4982,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
record.mapped(field_seq)
# determine which fields have been modified
- for name, oldval in pycompat.items(values):
+ for name, oldval in values.items():
field = self._fields[name]
newval = record[name]
if newval != oldval or (
@@ -4995,7 +4993,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# At the moment, the client does not support updates on a *2many field
# while this one is modified by the user.
- if isinstance(field_name, basestring) and \
+ if isinstance(field_name, pycompat.string_types) and \
self._fields[field_name].type in ('one2many', 'many2many'):
dirty.discard(field_name)
@@ -5020,13 +5018,13 @@ class RecordCache(MutableMapping):
def contains(self, field):
""" Return whether `records[0]` has a value for ``field`` in cache. """
- if isinstance(field, basestring):
+ if isinstance(field, pycompat.string_types):
field = self._recs._fields[field]
return self._recs.id in self._recs.env.cache[field]
def __contains__(self, field):
""" Return whether `records[0]` has a regular value for ``field`` in cache. """
- if isinstance(field, basestring):
+ if isinstance(field, pycompat.string_types):
field = self._recs._fields[field]
dummy = SpecialValue(None)
value = self._recs.env.cache[field].get(self._recs.id, dummy)
@@ -5034,7 +5032,7 @@ class RecordCache(MutableMapping):
def get(self, field, default=None):
""" Return the cached, regular value of ``field`` for `records[0]`, or ``default``. """
- if isinstance(field, basestring):
+ if isinstance(field, pycompat.string_types):
field = self._recs._fields[field]
dummy = SpecialValue(None)
value = self._recs.env.cache[field].get(self._recs.id, dummy)
@@ -5042,14 +5040,14 @@ class RecordCache(MutableMapping):
def __getitem__(self, field):
""" Return the cached value of ``field`` for `records[0]`. """
- if isinstance(field, basestring):
+ if isinstance(field, pycompat.string_types):
field = self._recs._fields[field]
value = self._recs.env.cache[field][self._recs.id]
return value.get() if isinstance(value, SpecialValue) else value
def __setitem__(self, field, value):
""" Assign the cached value of ``field`` for all records in ``records``. """
- if isinstance(field, basestring):
+ if isinstance(field, pycompat.string_types):
field = self._recs._fields[field]
values = dict.fromkeys(self._recs._ids, value)
self._recs.env.cache[field].update(values)
@@ -5060,7 +5058,7 @@ class RecordCache(MutableMapping):
"""
if args and isinstance(args[0], SpecialValue):
values = dict.fromkeys(self._recs._ids, args[0])
- for name, field in pycompat.items(self._recs._fields):
+ for name, field in self._recs._fields.items():
if name != 'id':
self._recs.env.cache[field].update(values)
else:
@@ -5068,7 +5066,7 @@ class RecordCache(MutableMapping):
def __delitem__(self, field):
""" Remove the cached value of ``field`` for all ``records``. """
- if isinstance(field, basestring):
+ if isinstance(field, pycompat.string_types):
field = self._recs._fields[field]
field_cache = self._recs.env.cache[field]
for id in self._recs._ids:
@@ -5078,7 +5076,7 @@ class RecordCache(MutableMapping):
""" Iterate over the field names with a regular value in cache. """
cache, id = self._recs.env.cache, self._recs.id
dummy = SpecialValue(None)
- for name, field in pycompat.items(self._recs._fields):
+ for name, field in self._recs._fields.items():
if name != 'id' and not isinstance(cache[field].get(id, dummy), SpecialValue):
yield name
diff --git a/odoo/modules/graph.py b/odoo/modules/graph.py
index 731b6f6557a..0f2e329fab9 100644
--- a/odoo/modules/graph.py
+++ b/odoo/modules/graph.py
@@ -8,7 +8,6 @@ import logging
import odoo
import odoo.tools as tools
-from odoo.tools import pycompat
_logger = logging.getLogger(__name__)
@@ -36,7 +35,7 @@ class Graph(dict):
return
# update the graph with values from the database (if exist)
## First, we set the default values for each package in graph
- additional_data = {key: {'id': 0, 'state': 'uninstalled', 'dbdemo': False, 'installed_version': None} for key in pycompat.keys(self)}
+ additional_data = {key: {'id': 0, 'state': 'uninstalled', 'dbdemo': False, 'installed_version': None} for key in self.keys()}
## Then we get the values from the database
cr.execute('SELECT name, id, state, demo AS dbdemo, latest_version AS installed_version'
' FROM ir_module_module'
@@ -46,8 +45,8 @@ class Graph(dict):
## and we update the default values with values from the database
additional_data.update((x['name'], x) for x in cr.dictfetchall())
- for package in pycompat.values(self):
- for k, v in pycompat.items(additional_data[package.name]):
+ for package in self.values():
+ for k, v in additional_data[package.name].items():
setattr(package, k, v)
def add_module(self, cr, module, force=None):
@@ -102,9 +101,9 @@ class Graph(dict):
def __iter__(self):
level = 0
- done = set(pycompat.keys(self))
+ done = set(self.keys())
while done:
- level_modules = sorted((name, module) for name, module in pycompat.items(self) if module.depth==level)
+ level_modules = sorted((name, module) for name, module in self.items() if module.depth==level)
for name, module in level_modules:
done.remove(name)
yield module
diff --git a/odoo/modules/loading.py b/odoo/modules/loading.py
index bd58376a7b9..c06c219f7eb 100644
--- a/odoo/modules/loading.py
+++ b/odoo/modules/loading.py
@@ -20,7 +20,6 @@ import odoo.tools as tools
from odoo import api, SUPERUSER_ID
from odoo.modules.module import adapt_version, initialize_sys_path, load_openerp_module
-from odoo.tools import pycompat
_logger = logging.getLogger(__name__)
_test_logger = logging.getLogger('odoo.tests')
@@ -298,13 +297,13 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
_check_module_names(cr, itertools.chain(tools.config['init'], tools.config['update']))
- module_names = [k for k, v in pycompat.items(tools.config['init']) if v]
+ module_names = [k for k, v in tools.config['init'].items() if v]
if module_names:
modules = Module.search([('state', '=', 'uninstalled'), ('name', 'in', module_names)])
if modules:
modules.button_install()
- module_names = [k for k, v in pycompat.items(tools.config['update']) if v]
+ module_names = [k for k, v in tools.config['update'].items() if v]
if module_names:
modules = Module.search([('state', '=', 'installed'), ('name', 'in', module_names)])
if modules:
@@ -392,7 +391,7 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
getattr(py_module, uninstall_hook)(cr, registry)
Module = env['ir.module.module']
- Module.browse(pycompat.values(modules_to_remove)).module_uninstall()
+ Module.browse(modules_to_remove.values()).module_uninstall()
# Recursive reload, should only happen once, because there should be no
# modules to remove next time
cr.commit()
@@ -415,7 +414,7 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
_logger.info('Modules loaded.')
# STEP 8: call _register_hook on every model
- for model in pycompat.values(env):
+ for model in env.values():
model._register_hook()
# STEP 9: save installed/updated modules for post-install tests
diff --git a/odoo/modules/migration.py b/odoo/modules/migration.py
index 0fa6699cf24..8edc9586415 100644
--- a/odoo/modules/migration.py
+++ b/odoo/modules/migration.py
@@ -13,7 +13,6 @@ from os.path import join as opj
from odoo.modules.module import get_resource_path
import odoo.release as release
import odoo.tools as tools
-from odoo.tools import pycompat
from odoo.tools.parse_version import parse_version
@@ -90,8 +89,8 @@ class MigrationManager(object):
def _get_migration_versions(pkg):
versions = sorted({
ver
- for lv in pycompat.values(self.migrations[pkg.name])
- for ver, lf in pycompat.items(lv)
+ for lv in self.migrations[pkg.name].values()
+ for ver, lf in lv.items()
if lf
}, key=lambda k: parse_version(convert_version(k)))
return versions
diff --git a/odoo/modules/module.py b/odoo/modules/module.py
index f0836cbe041..b249f01c1d9 100644
--- a/odoo/modules/module.py
+++ b/odoo/modules/module.py
@@ -434,7 +434,7 @@ def get_test_modules(module):
mod = importlib.import_module('.tests', modpath)
except Exception as e:
# If module has no `tests` sub-module, no problem.
- if str(e) != 'No module named tests':
+ if not pycompat.text_type(e).startswith(u'No module named'):
_logger.exception('Can not `import %s`.', module)
return []
diff --git a/odoo/modules/registry.py b/odoo/modules/registry.py
index cade92c23ac..1efc6aa195f 100644
--- a/odoo/modules/registry.py
+++ b/odoo/modules/registry.py
@@ -17,7 +17,7 @@ import odoo
from .. import SUPERUSER_ID
from odoo.tools import (assertion_report, config, existing_tables,
lazy_classproperty, lazy_property, table_exists,
- topological_sort, OrderedSet, pycompat)
+ topological_sort, OrderedSet)
from odoo.tools.lru import LRU
_logger = logging.getLogger(__name__)
@@ -157,7 +157,7 @@ class Registry(Mapping):
def delete_all(cls):
""" Delete all the registries. """
with cls._lock:
- for db_name in list(pycompat.keys(cls.registries)):
+ for db_name in list(cls.registries.keys()):
cls.delete(db_name)
#
@@ -193,8 +193,8 @@ class Registry(Mapping):
# map fields on their dependents
dependents = {
field: set(dep for dep, _ in model._field_triggers[field] if dep != field)
- for model in pycompat.values(self)
- for field in pycompat.values(model._fields)
+ for model in self.values()
+ for field in model._fields.values()
}
# sort them topologically, and associate a sequence number to each field
mapping = {
@@ -262,7 +262,7 @@ class Registry(Mapping):
env['ir.model']._add_manual_models()
# prepare the setup on all models
- models = list(pycompat.values(env))
+ models = list(env.values())
for model in models:
model._prepare_setup()
@@ -316,7 +316,7 @@ class Registry(Mapping):
missing = {table2model[table] for table in missing_tables}
_logger.warning("Models have no table: %s.", ", ".join(missing))
# recreate missing tables following model dependencies
- deps = {name: model._depends for name, model in pycompat.items(env)}
+ deps = {name: model._depends for name, model in env.items()}
for name in topological_sort(deps):
if name in missing:
_logger.info("Recreate table of model %s.", name)
@@ -341,7 +341,7 @@ class Registry(Mapping):
""" Clear the caches associated to methods decorated with
``tools.ormcache`` or ``tools.ormcache_multi`` for all the models.
"""
- for model in pycompat.values(self.models):
+ for model in self.models.values():
model.clear_caches()
def setup_signaling(self):
diff --git a/odoo/osv/expression.py b/odoo/osv/expression.py
index 8df300a2bd4..69833f45de0 100644
--- a/odoo/osv/expression.py
+++ b/odoo/osv/expression.py
@@ -337,7 +337,7 @@ def generate_table_alias(src_table_alias, joined_tables=[]):
# We have to fit a crc32 hash and one underscore
# into a 63 character alias. The remaining space we can use to add
# a human readable prefix.
- alias_hash = hex(crc32(alias))[2:]
+ alias_hash = hex(crc32(alias.encode('utf-8')))[2:]
ALIAS_PREFIX_LENGTH = 63 - len(alias_hash) - 1
alias = "%s_%s" % (
alias[:ALIAS_PREFIX_LENGTH], alias_hash)
@@ -377,7 +377,7 @@ def normalize_leaf(element):
def is_operator(element):
""" Test whether an object is a valid domain operator. """
- return isinstance(element, basestring) and element in DOMAIN_OPERATORS
+ return isinstance(element, pycompat.string_types) and element in DOMAIN_OPERATORS
def is_leaf(element, internal=False):
@@ -398,7 +398,7 @@ def is_leaf(element, internal=False):
return (isinstance(element, tuple) or isinstance(element, list)) \
and len(element) == 3 \
and element[1] in INTERNAL_OPS \
- and ((isinstance(element[0], basestring) and element[0])
+ and ((isinstance(element[0], pycompat.string_types) and element[0])
or tuple(element) in (TRUE_LEAF, FALSE_LEAF))
@@ -697,9 +697,9 @@ class expression(object):
return the list of related ids
"""
names = []
- if isinstance(value, basestring):
+ if isinstance(value, pycompat.string_types):
names = [value]
- elif value and isinstance(value, (tuple, list)) and all(isinstance(item, basestring) for item in value):
+ elif value and isinstance(value, (tuple, list)) and all(isinstance(item, pycompat.string_types) for item in value):
names = value
elif isinstance(value, pycompat.integer_types):
return [value]
@@ -925,7 +925,7 @@ class expression(object):
domain = domain(model)
is_integer_m2o = comodel._fields[field.inverse_name].type == 'integer'
if right is not False:
- if isinstance(right, basestring):
+ if isinstance(right, pycompat.string_types):
op = {'!=': '=', 'not like': 'like', 'not ilike': 'ilike'}.get(operator, operator)
ids2 = [x[0] for x in comodel.name_search(right, domain or [], op, limit=None)]
if ids2:
@@ -988,12 +988,12 @@ class expression(object):
else:
subquery = 'SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % (rel_id1, rel_table, rel_id2)
# avoid flattening of argument in to_sql()
- subquery = cr.mogrify(subquery, [tuple(ids2)])
+ subquery = cr.mogrify(subquery, [tuple(ids2)]).decode('utf-8')
push(create_substitution_leaf(leaf, ('id', 'inselect', (subquery, [])), internal=True))
else:
call_null_m2m = True
if right is not False:
- if isinstance(right, basestring):
+ if isinstance(right, pycompat.string_types):
op = {'!=': '=', 'not like': 'like', 'not ilike': 'ilike'}.get(operator, operator)
domain = field.domain
if callable(domain):
@@ -1018,7 +1018,7 @@ class expression(object):
subop = 'not inselect' if operator in NEGATIVE_TERM_OPERATORS else 'inselect'
subquery = 'SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % (rel_id1, rel_table, rel_id2)
# avoid flattening of argument in to_sql()
- subquery = cr.mogrify(subquery, [tuple(it for it in res_ids if it)])
+ subquery = cr.mogrify(subquery, [tuple(it for it in res_ids if it)]).decode('utf-8')
push(create_substitution_leaf(leaf, ('id', subop, (subquery, [])), internal=True))
if call_null_m2m:
@@ -1051,8 +1051,8 @@ class expression(object):
res_ids.append(False) # TODO this should not be appended if False was in 'right'
return left, 'in', res_ids
# resolve string-based m2o criterion into IDs
- if isinstance(right, basestring) or \
- right and isinstance(right, (tuple, list)) and all(isinstance(item, basestring) for item in right):
+ if isinstance(right, pycompat.string_types) or \
+ right and isinstance(right, (tuple, list)) and all(isinstance(item, pycompat.string_types) for item in right):
push(create_substitution_leaf(leaf, _get_expression(comodel, left, right, operator), model))
else:
# right == [] or right == False and all other cases are handled by __leaf_to_sql()
@@ -1263,21 +1263,16 @@ class expression(object):
add_null = False
if need_wildcard:
- if isinstance(right, str):
- str_utf8 = right
- elif isinstance(right, unicode):
- str_utf8 = right.encode('utf-8')
- else:
- str_utf8 = str(right)
- params = '%%%s%%' % str_utf8
- add_null = not str_utf8
+ native_str = pycompat.to_native(right)
+ params = '%%%s%%' % native_str
+ add_null = not native_str
elif left in model:
params = model._fields[left].convert_to_column(right, model)
if add_null:
query = '(%s OR %s."%s" IS NULL)' % (query, table_alias, left)
- if isinstance(params, basestring):
+ if isinstance(params, pycompat.string_types):
params = [params]
return query, params
diff --git a/odoo/osv/orm.py b/odoo/osv/orm.py
index e4be2706c7f..f7cc7ae0701 100644
--- a/odoo/osv/orm.py
+++ b/odoo/osv/orm.py
@@ -46,12 +46,12 @@ def transfer_field_to_modifiers(field, modifiers):
for attr in ('invisible', 'readonly', 'required'):
state_exceptions[attr] = []
default_values[attr] = bool(field.get(attr))
- for state, modifs in pycompat.items(field.get("states",{})):
+ for state, modifs in field.get("states",{}).items():
for modif in modifs:
if default_values[modif[0]] != modif[1]:
state_exceptions[modif[0]].append(state)
- for attr, default_value in pycompat.items(default_values):
+ for attr, default_value in default_values.items():
if state_exceptions[attr]:
modifiers[attr] = [("state", "not in" if default_value else "in", state_exceptions[attr])]
else:
@@ -125,7 +125,7 @@ def setup_modifiers(node, field=None, context=None, in_tree_view=False):
def test_modifiers(what, expected):
modifiers = {}
- if isinstance(what, basestring):
+ if isinstance(what, pycompat.string_types):
node = etree.fromstring(what)
transfer_node_to_modifiers(node, modifiers)
simplify_modifiers(modifiers)
diff --git a/odoo/service/db.py b/odoo/service/db.py
index bdcf0e404d9..4c6c23822ab 100644
--- a/odoo/service/db.py
+++ b/odoo/service/db.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-
+import base64
import json
import logging
import os
@@ -23,6 +23,7 @@ import odoo.sql_db
import odoo.tools
from odoo.sql_db import db_connect
from odoo.release import version_info
+from odoo.tools import pycompat
_logger = logging.getLogger(__name__)
@@ -160,7 +161,7 @@ def exp_dump(db_name, format):
with tempfile.TemporaryFile() as t:
dump_db(db_name, t, format)
t.seek(0)
- return t.read().encode('base64')
+ return base64.b64encode(t.read())
def dump_db_manifest(cr):
pg_version = "%d.%d" % divmod(cr._obj.connection.server_version / 100, 100)
@@ -215,7 +216,7 @@ def dump_db(db_name, stream, backup_format='zip'):
def exp_restore(db_name, data, copy=False):
data_file = tempfile.NamedTemporaryFile(delete=False)
try:
- data_file.write(data.decode('base64'))
+ data_file.write(base64.b64decode(data))
data_file.close()
restore_db(db_name, data_file.name, copy=copy)
finally:
@@ -223,7 +224,7 @@ def exp_restore(db_name, data, copy=False):
return True
def restore_db(db, dump_file, copy=False):
- assert isinstance(db, basestring)
+ assert isinstance(db, pycompat.string_types)
if exp_db_exist(db):
_logger.info('RESTORE DB: %s already exists', db)
raise Exception("Database already exists")
diff --git a/odoo/service/model.py b/odoo/service/model.py
index 185d5ce72cc..47eeeab1af1 100644
--- a/odoo/service/model.py
+++ b/odoo/service/model.py
@@ -10,7 +10,6 @@ import time
import odoo
from odoo.exceptions import UserError, ValidationError, QWebException
from odoo.models import check_method_name
-from odoo.tools import pycompat
from odoo.tools.translate import translate
from odoo.tools.translate import _
@@ -68,10 +67,6 @@ def check(f):
except Exception:
pass
- uid = 1
- if args and isinstance(args[0], pycompat.integer_types):
- uid = args[0]
-
lang = ctx and ctx.get('lang')
if not (lang or hasattr(src, '__call__')):
return src
@@ -119,7 +114,7 @@ def check(f):
time.sleep(wait_time)
except IntegrityError as inst:
registry = odoo.registry(dbname)
- for key in pycompat.keys(registry._sql_error):
+ for key in registry._sql_error.keys():
if key in inst[0]:
raise ValidationError(tr(registry._sql_error[key], 'sql_constraint') or inst[0])
if inst.pgcode in (errorcodes.NOT_NULL_VIOLATION, errorcodes.FOREIGN_KEY_VIOLATION, errorcodes.RESTRICT_VIOLATION):
diff --git a/odoo/service/server.py b/odoo/service/server.py
index 56acaaed901..c3b523efe7e 100644
--- a/odoo/service/server.py
+++ b/odoo/service/server.py
@@ -40,7 +40,7 @@ from odoo.modules.module import run_unit_tests, runs_post_install
from odoo.modules.registry import Registry
from odoo.release import nt_service_name
from odoo.tools import config
-from odoo.tools import stripped_sys_argv, dumpstacks, log_ormcache_stats, pycompat
+from odoo.tools import stripped_sys_argv, dumpstacks, log_ormcache_stats
_logger = logging.getLogger(__name__)
@@ -216,7 +216,7 @@ class ThreadedServer(CommonServer):
time.sleep(SLEEP_INTERVAL + number) # Steve Reich timing style
registries = odoo.modules.registry.Registry.registries
_logger.debug('cron%d polling for jobs', number)
- for db_name, registry in pycompat.items(registries):
+ for db_name, registry in registries.items():
while registry.ready:
try:
acquired = ir_cron._acquire_job(db_name)
@@ -433,7 +433,7 @@ class PreforkServer(CommonServer):
def pipe_ping(self, pipe):
try:
- os.write(pipe[1], '.')
+ os.write(pipe[1], b'.')
except IOError as e:
if e.errno not in [errno.EAGAIN, errno.EINTR]:
raise
@@ -525,7 +525,7 @@ class PreforkServer(CommonServer):
def process_timeout(self):
now = time.time()
- for (pid, worker) in pycompat.items(self.workers):
+ for (pid, worker) in self.workers.items():
if worker.watchdog_timeout is not None and \
(now - worker.watchdog_time) >= worker.watchdog_timeout:
_logger.error("%s (%s) timeout after %ss",
@@ -546,8 +546,8 @@ class PreforkServer(CommonServer):
def sleep(self):
try:
# map of fd -> worker
- fds = {w.watchdog_pipe[0]: w for k, w in pycompat.items(self.workers)}
- fd_in = list(pycompat.keys(fds)) + [self.pipe[0]]
+ fds = {w.watchdog_pipe[0]: w for w in self.workers.values()}
+ fd_in = list(fds) + [self.pipe[0]]
# check for ping or internal wakeups
ready = select.select(fd_in, [], [], self.beat)
# update worker watchdogs
@@ -771,7 +771,7 @@ class WorkerHTTP(Worker):
client, addr = self.multi.socket.accept()
self.process_request(client, addr)
except socket.error as e:
- if e[0] not in (errno.EAGAIN, errno.ECONNABORTED):
+ if e.errno not in (errno.EAGAIN, errno.ECONNABORTED):
raise
def start(self):
@@ -887,7 +887,7 @@ def load_test_file_yml(registry, test_file):
def load_test_file_py(registry, test_file):
# Locate python module based on its filename and run the tests
test_path, _ = os.path.splitext(os.path.abspath(test_file))
- for mod_name, mod_mod in list(pycompat.items(sys.modules)):
+ for mod_name, mod_mod in list(sys.modules.items()):
if mod_mod:
mod_path, _ = os.path.splitext(getattr(mod_mod, '__file__', ''))
if test_path == mod_path:
diff --git a/odoo/service/wsgi_server.py b/odoo/service/wsgi_server.py
index ec699dc13e0..95276f336b1 100644
--- a/odoo/service/wsgi_server.py
+++ b/odoo/service/wsgi_server.py
@@ -12,12 +12,15 @@ import sys
import threading
import traceback
+
try:
from xmlrpc import client as xmlrpclib
except ImportError:
# pylint: disable=bad-python3-import
import xmlrpclib
+import werkzeug.exceptions
+import werkzeug.wrappers
import werkzeug.serving
import werkzeug.contrib.fixers
@@ -36,90 +39,54 @@ RPC_FAULT_CODE_WARNING = 2
RPC_FAULT_CODE_ACCESS_DENIED = 3
RPC_FAULT_CODE_ACCESS_ERROR = 4
-def xmlrpc_return(start_response, service, method, params, string_faultcode=False):
- """
- Helper to call a service's method with some params, using a wsgi-supplied
- ``start_response`` callback.
-
- This is the place to look at to see the mapping between core exceptions
- and XML-RPC fault codes.
- """
- # Map OpenERP core exceptions to XML-RPC fault codes. Specific exceptions
- # defined in ``odoo.exceptions`` are mapped to specific fault codes;
- # all the other exceptions are mapped to the generic
- # RPC_FAULT_CODE_APPLICATION_ERROR value.
- # This also mimics SimpleXMLRPCDispatcher._marshaled_dispatch() for
- # exception handling.
- try:
- result = odoo.http.dispatch_rpc(service, method, params)
- response = xmlrpclib.dumps((result,), methodresponse=1, allow_none=False, encoding=None)
- except Exception as e:
- if string_faultcode:
- response = xmlrpc_handle_exception_string(e)
- else:
- response = xmlrpc_handle_exception_int(e)
- start_response("200 OK", [('Content-Type','text/xml'), ('Content-Length', str(len(response)))])
- return [response]
-
def xmlrpc_handle_exception_int(e):
if isinstance(e, odoo.exceptions.UserError):
fault = xmlrpclib.Fault(RPC_FAULT_CODE_WARNING, odoo.tools.ustr(e.value))
- response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
elif isinstance(e, odoo.exceptions.RedirectWarning):
fault = xmlrpclib.Fault(RPC_FAULT_CODE_WARNING, str(e))
- response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
elif isinstance(e, odoo.exceptions.MissingError):
fault = xmlrpclib.Fault(RPC_FAULT_CODE_WARNING, str(e))
- response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
elif isinstance (e, odoo.exceptions.AccessError):
fault = xmlrpclib.Fault(RPC_FAULT_CODE_ACCESS_ERROR, str(e))
- response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
elif isinstance(e, odoo.exceptions.AccessDenied):
fault = xmlrpclib.Fault(RPC_FAULT_CODE_ACCESS_DENIED, str(e))
- response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
elif isinstance(e, odoo.exceptions.DeferredException):
info = e.traceback
# Which one is the best ?
formatted_info = "".join(traceback.format_exception(*info))
#formatted_info = odoo.tools.exception_to_unicode(e) + '\n' + info
fault = xmlrpclib.Fault(RPC_FAULT_CODE_APPLICATION_ERROR, formatted_info)
- response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
else:
info = sys.exc_info()
# Which one is the best ?
formatted_info = "".join(traceback.format_exception(*info))
#formatted_info = odoo.tools.exception_to_unicode(e) + '\n' + info
fault = xmlrpclib.Fault(RPC_FAULT_CODE_APPLICATION_ERROR, formatted_info)
- response = xmlrpclib.dumps(fault, allow_none=None, encoding=None)
- return response
+
+ return xmlrpclib.dumps(fault, allow_none=None)
def xmlrpc_handle_exception_string(e):
if isinstance(e, odoo.exceptions.UserError):
fault = xmlrpclib.Fault('warning -- %s\n\n%s' % (e.name, e.value), '')
- response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
elif isinstance(e, odoo.exceptions.RedirectWarning):
fault = xmlrpclib.Fault('warning -- Warning\n\n' + str(e), '')
elif isinstance(e, odoo.exceptions.MissingError):
fault = xmlrpclib.Fault('warning -- MissingError\n\n' + str(e), '')
- response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
elif isinstance(e, odoo.exceptions.AccessError):
fault = xmlrpclib.Fault('warning -- AccessError\n\n' + str(e), '')
- response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
elif isinstance(e, odoo.exceptions.AccessDenied):
fault = xmlrpclib.Fault('AccessDenied', str(e))
- response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
elif isinstance(e, odoo.exceptions.DeferredException):
info = e.traceback
formatted_info = "".join(traceback.format_exception(*info))
fault = xmlrpclib.Fault(odoo.tools.ustr(e), formatted_info)
- response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
#InternalError
else:
info = sys.exc_info()
formatted_info = "".join(traceback.format_exception(*info))
fault = xmlrpclib.Fault(odoo.tools.exception_to_unicode(e), formatted_info)
- response = xmlrpclib.dumps(fault, allow_none=None, encoding=None)
- return response
+
+ return xmlrpclib.dumps(fault, allow_none=None, encoding=None)
def wsgi_xmlrpc(environ, start_response):
""" Two routes are available for XML-RPC
@@ -136,14 +103,25 @@ def wsgi_xmlrpc(environ, start_response):
# Distinguish betweed the 2 faultCode modes
string_faultcode = True
+ service = environ['PATH_INFO'][len('/xmlrpc/'):]
if environ['PATH_INFO'].startswith('/xmlrpc/2/'):
- service = environ['PATH_INFO'][len('/xmlrpc/2/'):]
+ service = service[len('2/'):]
string_faultcode = False
- else:
- service = environ['PATH_INFO'][len('/xmlrpc/'):]
params, method = xmlrpclib.loads(data)
- return xmlrpc_return(start_response, service, method, params, string_faultcode)
+ try:
+ result = odoo.http.dispatch_rpc(service, method, params)
+ response = xmlrpclib.dumps((result,), methodresponse=1, allow_none=False)
+ except Exception as e:
+ if string_faultcode:
+ response = xmlrpc_handle_exception_string(e)
+ else:
+ response = xmlrpc_handle_exception_int(e)
+
+ return werkzeug.wrappers.Response(
+ response=response,
+ mimetype='text/xml',
+ )(environ, start_response)
def application_unproxied(environ, start_response):
""" WSGI entry point."""
@@ -168,9 +146,7 @@ def application_unproxied(environ, start_response):
return result
# We never returned from the loop.
- response = 'No handler found.\n'
- start_response('404 Not Found', [('Content-Type', 'text/plain'), ('Content-Length', str(len(response)))])
- return [response]
+ return werkzeug.exceptions.NotFound("No handler found.\n")(environ, start_response)
def application(environ, start_response):
if config['proxy_mode'] and 'HTTP_X_FORWARDED_HOST' in environ:
diff --git a/odoo/sql_db.py b/odoo/sql_db.py
index efc0e51972d..749abc69932 100644
--- a/odoo/sql_db.py
+++ b/odoo/sql_db.py
@@ -21,8 +21,6 @@ from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT, ISOLATION_LEVEL_READ
from psycopg2.pool import PoolError
from werkzeug import urls
-from .tools import pycompat
-
psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
_logger = logging.getLogger(__name__)
@@ -43,7 +41,7 @@ def undecimalize(symb, cr):
return None
return float(symb)
-for name, typeoid in pycompat.items(types_mapping):
+for name, typeoid in types_mapping.items():
psycopg2.extensions.register_type(psycopg2.extensions.new_type(typeoid, name, lambda x, cr: x))
psycopg2.extensions.register_type(psycopg2.extensions.new_type((700, 701, 1700,), 'float', undecimalize))
@@ -269,7 +267,7 @@ class Cursor(object):
sqllogs = {'from': self.sql_from_log, 'into': self.sql_into_log}
sum = 0
if sqllogs[type]:
- sqllogitems = pycompat.items(sqllogs[type])
+ sqllogitems = sqllogs[type].items()
_logger.debug("SQL LOG %s:", type)
for r in sorted(sqllogitems, key=lambda k: k[1]):
delay = timedelta(microseconds=r[1][1])
diff --git a/odoo/tests/common.py b/odoo/tests/common.py
index 5afa24a20c8..cf1c664d229 100644
--- a/odoo/tests/common.py
+++ b/odoo/tests/common.py
@@ -313,9 +313,11 @@ class HttpCase(TransactionCase):
Other lines are relayed to the test log.
"""
+ logger = _logger.getChild('phantomjs')
t0 = datetime.now()
td = timedelta(seconds=timeout)
buf = bytearray()
+ pid = phantom.stdout.fileno()
while True:
# timeout
self.assertLess(datetime.now() - t0, td,
@@ -323,7 +325,7 @@ class HttpCase(TransactionCase):
# read a byte
try:
- ready, _, _ = select.select([phantom.stdout], [], [], 0.5)
+ ready, _, _ = select.select([pid], [], [], 0.5)
except select.error as e:
# In Python 2, select.error has no relation to IOError or
# OSError, and no errno/strerror/filename, only a pair of
@@ -333,39 +335,35 @@ class HttpCase(TransactionCase):
continue
raise
- if ready:
- s = phantom.stdout.read(1)
- if not s:
- break
- buf.append(s)
+ if not ready:
+ continue
+
+ s = os.read(pid, 4096)
+ if not s:
+ self.fail("Ran out of data to read")
+ buf.extend(s)
# process lines
- if '\n' in buf and (not buf.startswith('<phantomLog>') or '</phantomLog>' in buf):
- if buf.startswith('<phantomLog>'):
- line = buf[12:buf.index('</phantomLog>')]
- buf = bytearray()
+ while b'\n' in buf and (not buf.startswith(b'<phantomLog>') or b'</phantomLog>' in buf):
+
+ if buf.startswith(b'<phantomLog>'):
+ line, buf = buf[12:].split(b'</phantomLog>\n', 1)
else:
- line, buf = buf.split('\n', 1)
- line = str(line)
+ line, buf = buf.split(b'\n', 1)
+ line = line.decode('utf-8')
lline = line.lower()
if lline.startswith(("error", "server application error")):
try:
# when errors occur the execution stack may be sent as a JSON
prefix = lline.index('error') + 6
- _logger.error("phantomjs: %s", pformat(json.loads(line[prefix:])))
+ self.fail(pformat(json.loads(line[prefix:])))
except ValueError:
- line_ = line.split('\n\n')
- _logger.error("phantomjs: %s", line_[0])
- # The second part of the log is for debugging
- if len(line_) > 1:
- _logger.info("phantomjs: \n%s", line.split('\n\n', 1)[1])
- pass
- break
+ self.fail(lline)
elif lline.startswith("warning"):
- _logger.warn("phantomjs: %s", line)
+ logger.warn(line)
else:
- _logger.info("phantomjs: %s", line)
+ logger.info(line)
if line == "ok":
return True
@@ -382,40 +380,40 @@ class HttpCase(TransactionCase):
phantom = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=None, close_fds=True)
except OSError:
raise unittest.SkipTest("PhantomJS not found")
- result = False
try:
result = self.phantom_poll(phantom, timeout)
+ self.assertTrue(
+ result,
+ "PhantomJS test completed without reporting success; "
+ "the log may contain errors or hints.")
finally:
# kill phantomjs if phantom.exit() wasn't called in the test
if phantom.poll() is None:
+ _logger.info("Terminating phantomjs")
phantom.terminate()
phantom.wait()
-
- # check PhantomJS health
- from signal import SIGSEGV
- _logger.info("Phantom JS return code: %d" % phantom.returncode)
- if phantom.returncode == -SIGSEGV:
- _logger.error("Phantom JS has crashed (segmentation fault) during testing; log may not be relevant")
+ else:
+ # if we had to terminate phantomjs its return code is
+ # always -15 so we don't care
+ # check PhantomJS health
+ from signal import SIGSEGV
+ _logger.info("Phantom JS return code: %d" % phantom.returncode)
+ if phantom.returncode == -SIGSEGV:
+ _logger.error("Phantom JS has crashed (segmentation fault) during testing; log may not be relevant")
self._wait_remaining_requests()
- # we ignore phantomjs return code as we kill it as soon as we have ok
- _logger.info("phantom_run execution finished")
- self.assertTrue(
- result,
- "PhantomJS test completed without reporting success; "
- "the log may contain errors or hints.")
def _wait_remaining_requests(self):
t0 = int(time.time())
for thread in threading.enumerate():
if thread.name.startswith('odoo.service.http.request.'):
- thread.join_retry_count = 10
+ join_retry_count = 10
while thread.isAlive():
# Need a busyloop here as thread.join() masks signals
# and would prevent the forced shutdown.
thread.join(0.05)
- thread.join_retry_count -= 1
- if thread.join_retry_count < 0:
+ join_retry_count -= 1
+ if join_retry_count < 0:
_logger.warning("Stop waiting for thread %s handling request for url %s",
thread.name, thread.url)
break
diff --git a/odoo/tools/__init__.py b/odoo/tools/__init__.py
index 4eb84a9de16..3feb0940ff6 100644
--- a/odoo/tools/__init__.py
+++ b/odoo/tools/__init__.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-import copy
+from . import pycompat
from . import win32
from . import appdirs
from . import pdf
diff --git a/odoo/tools/appdirs.py b/odoo/tools/appdirs.py
index 8617dabfa4f..6176e6bce75 100644
--- a/odoo/tools/appdirs.py
+++ b/odoo/tools/appdirs.py
@@ -13,6 +13,7 @@ from __future__ import print_function
# http://support.microsoft.com/default.aspx?scid=kb;en-us;310294#XSLTH3194121123120121120120
# - Mac OS X: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html
# - XDG spec for Un*x: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
+from . import pycompat
__version_info__ = (1, 3, 0)
__version__ = '.'.join(str(v) for v in __version_info__)
@@ -21,12 +22,6 @@ __version__ = '.'.join(str(v) for v in __version_info__)
import sys
import os
-PY3 = sys.version_info[0] == 3
-
-if PY3:
- unicode = str
-
-
def user_data_dir(appname=None, appauthor=None, version=None, roaming=False):
r"""Return full path to the user-specific data dir for this application.
@@ -397,7 +392,7 @@ def _get_win_folder_with_pywin32(csidl_name):
# not return unicode strings when there is unicode data in the
# path.
try:
- dir = unicode(dir)
+ dir = pycompat.text_type(dir)
# Downgrade to short path name if have highbit chars. See
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
diff --git a/odoo/tools/cache.py b/odoo/tools/cache.py
index 2554331ce9b..b6b23175773 100644
--- a/odoo/tools/cache.py
+++ b/odoo/tools/cache.py
@@ -8,7 +8,7 @@ from decorator import decorator
from inspect import formatargspec, getargspec
import logging
-from odoo.tools import pycompat
+from . import pycompat
unsafe_eval = eval
@@ -135,7 +135,7 @@ class ormcache_multi(ormcache):
def determine_key(self):
""" Determine the function that computes a cache key from arguments. """
assert self.skiparg is None, "ormcache_multi() no longer supports skiparg"
- assert isinstance(self.multi, basestring), "ormcache_multi() parameter multi must be an argument name"
+ assert isinstance(self.multi, pycompat.string_types), "ormcache_multi() parameter multi must be an argument name"
super(ormcache_multi, self).determine_key()
@@ -202,10 +202,10 @@ def log_ormcache_stats(sig=None, frame=None):
me = threading.currentThread()
me_dbname = getattr(me, 'dbname', 'n/a')
entries = defaultdict(int)
- for dbname, reg in pycompat.items(Registry.registries):
+ for dbname, reg in Registry.registries.items():
for key in reg.cache:
entries[(dbname,) + key[:2]] += 1
- for key, count in sorted(pycompat.items(entries)):
+ for key, count in sorted(entries.items()):
dbname, model_name, method = key
me.dbname = dbname
stat = STAT[key]
diff --git a/odoo/tools/config.py b/odoo/tools/config.py
index 375f55b4714..5a8f7efabfe 100644
--- a/odoo/tools/config.py
+++ b/odoo/tools/config.py
@@ -5,15 +5,13 @@ try:
import configparser as ConfigParser
except ImportError:
import ConfigParser
-
+
+import logging
import optparse
import os
import sys
import odoo
-import odoo.conf
-import odoo.loglevels as loglevels
-import logging
-import odoo.release as release
+from .. import release, conf, loglevels
from . import appdirs, pycompat
@@ -57,7 +55,7 @@ def _deduplicate_loggers(loggers):
# there are no duplicates within the output sequence
return (
'{}:{}'.format(logger, level)
- for logger, level in pycompat.items(dict(it.split(':') for it in loggers))
+ for logger, level in dict(it.split(':') for it in loggers).items()
)
@@ -398,10 +396,10 @@ class configmanager(object):
if getattr(opt, arg):
self.options[arg] = getattr(opt, arg)
# ... or keep, but cast, the config file value.
- elif isinstance(self.options[arg], basestring) and self.casts[arg].type in optparse.Option.TYPE_CHECKER:
+ elif isinstance(self.options[arg], pycompat.string_types) and self.casts[arg].type in optparse.Option.TYPE_CHECKER:
self.options[arg] = optparse.Option.TYPE_CHECKER[self.casts[arg].type](self.casts[arg], arg, self.options[arg])
- if isinstance(self.options['log_handler'], basestring):
+ if isinstance(self.options['log_handler'], pycompat.string_types):
self.options['log_handler'] = self.options['log_handler'].split(',')
self.options['log_handler'].extend(opt.log_handler)
@@ -433,7 +431,7 @@ class configmanager(object):
if getattr(opt, arg) is not None:
self.options[arg] = getattr(opt, arg)
# ... or keep, but cast, the config file value.
- elif isinstance(self.options[arg], basestring) and self.casts[arg].type in optparse.Option.TYPE_CHECKER:
+ elif isinstance(self.options[arg], pycompat.string_types) and self.casts[arg].type in optparse.Option.TYPE_CHECKER:
self.options[arg] = optparse.Option.TYPE_CHECKER[self.casts[arg].type](self.casts[arg], arg, self.options[arg])
self.options['root_path'] = os.path.abspath(os.path.expanduser(os.path.expandvars(os.path.join(os.path.dirname(__file__), '..'))))
@@ -471,9 +469,9 @@ class configmanager(object):
if opt.save:
self.save()
- odoo.conf.addons_paths = self.options['addons_path'].split(',')
+ conf.addons_paths = self.options['addons_path'].split(',')
- odoo.conf.server_wide_modules = [
+ conf.server_wide_modules = [
m.strip() for m in self.options['server_wide_modules'].split(',') if m.strip()
]
@@ -529,9 +527,9 @@ class configmanager(object):
def save(self):
p = ConfigParser.RawConfigParser()
- loglevelnames = dict(pycompat.izip(pycompat.values(self._LOGLEVELS), pycompat.keys(self._LOGLEVELS)))
+ loglevelnames = dict(pycompat.izip(self._LOGLEVELS.values(), self._LOGLEVELS))
p.add_section('options')
- for opt in sorted(pycompat.keys(self.options)):
+ for opt in sorted(self.options):
if opt in ('version', 'language', 'translate_out', 'translate_in', 'overwrite_existing_translations', 'init', 'update'):
continue
if opt in self.blacklist_for_save:
@@ -543,9 +541,9 @@ class configmanager(object):
else:
p.set('options', opt, self.options[opt])
- for sec in sorted(pycompat.keys(self.misc)):
+ for sec in sorted(self.misc):
p.add_section(sec)
- for opt in sorted(pycompat.keys(self.misc[sec])):
+ for opt in sorted(self.misc[sec]):
p.set(sec,opt,self.misc[sec][opt])
# try to create the directories and write the file
@@ -575,7 +573,7 @@ class configmanager(object):
def __setitem__(self, key, value):
self.options[key] = value
- if key in self.options and isinstance(self.options[key], basestring) and \
+ if key in self.options and isinstance(self.options[key], pycompat.string_types) and \
key in self.casts and self.casts[key].type in optparse.Option.TYPE_CHECKER:
self.options[key] = optparse.Option.TYPE_CHECKER[self.casts[key].type](self.casts[key], key, self.options[key])
diff --git a/odoo/tools/convert.py b/odoo/tools/convert.py
index 779abec9db1..57b692f7ae4 100644
--- a/odoo/tools/convert.py
+++ b/odoo/tools/convert.py
@@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
+import base64
import io
-import csv
import logging
import os.path
import re
@@ -16,7 +15,6 @@ import pytz
from lxml import etree, builder
import odoo
-import odoo.release
from . import assertion_report, pycompat
from .config import config
from .misc import file_open, unquote, ustr, SKIPPED_ELEMENT_TYPES
@@ -87,11 +85,11 @@ def _fix_multiple_roots(node):
def _eval_xml(self, node, env):
if node.tag in ('field','value'):
t = node.get('type','char')
- f_model = node.get('model', '').encode('utf-8')
+ f_model = node.get('model')
if node.get('search'):
- f_search = node.get("search",'').encode('utf-8')
- f_use = node.get("use",'id').encode('utf-8')
- f_name = node.get("name",'').encode('utf-8')
+ f_search = node.get("search")
+ f_use = node.get("use",'id')
+ f_name = node.get("name")
idref2 = {}
if f_search:
idref2 = _get_idref(self, env, f_model, self.idref)
@@ -108,7 +106,7 @@ def _eval_xml(self, node, env):
if isinstance(f_val, tuple):
f_val = f_val[0]
return f_val
- a_eval = node.get('eval','')
+ a_eval = node.get('eval')
if a_eval:
idref2 = _get_idref(self, env, f_model, self.idref)
try:
@@ -118,32 +116,40 @@ def _eval_xml(self, node, env):
'Could not eval(%s) for %s in %s', a_eval, node.get('name'), env.context)
raise
def _process(s):
- matches = re.finditer(r'[^%]%\((.*?)\)[ds]', s)
- done = []
+ matches = re.finditer(br'[^%]%\((.*?)\)[ds]', s)
+ done = set()
for m in matches:
found = m.group()[1:]
if found in done:
continue
- done.append(found)
- id = m.groups()[0]
+ done.add(found)
+ id = m.groups()[0].decode('utf-8')
if not id in self.idref:
self.idref[id] = self.id_get(id)
- s = s.replace(found, str(self.idref[id]))
- s = s.replace('%%', '%') # Quite wierd but it's for (somewhat) backward compatibility sake
+ # So funny story: in Python 3, bytes(n: int) returns a
+ # bytestring of n nuls. In Python 2 it obviously returns the
+ # stringified number, which is what we're expecting here
+ s = s.replace(found, pycompat.text_type(self.idref[id]).encode('utf-8'))
+ s = s.replace(b'%%', b'%') # Quite wierd but it's for (somewhat) backward compatibility sake
return s
if t == 'xml':
_fix_multiple_roots(node)
- return '<?xml version="1.0"?>\n'\
- +_process("".join([etree.tostring(n, encoding='utf-8') for n in node]))
+ return b'<?xml version="1.0"?>\n'\
+ +_process(b"".join(etree.tostring(n, encoding='utf-8') for n in node))
if t == 'html':
- return _process("".join([etree.tostring(n, encoding='utf-8') for n in node]))
+ return _process(b"".join(etree.tostring(n, encoding='utf-8') for n in node))
data = node.text
if node.get('file'):
with file_open(node.get('file'), 'rb') as f:
data = f.read()
+ if t == 'base64':
+ return base64.b64encode(data)
+
+ # after that, only text content makes sense
+ data = pycompat.to_text(data)
if t == 'file':
from ..modules import module
path = data.strip()
@@ -155,9 +161,6 @@ def _eval_xml(self, node, env):
if t == 'char':
return data
- if t == 'base64':
- return data.encode('base64')
-
if t == 'int':
d = data.strip()
if d == 'None':
@@ -176,7 +179,7 @@ def _eval_xml(self, node, env):
return res
elif node.tag == "function":
args = []
- a_eval = node.get('eval','')
+ a_eval = node.get('eval')
# FIXME: should probably be exclusive
if a_eval:
self.idref['ref'] = self.id_get
@@ -185,7 +188,7 @@ def _eval_xml(self, node, env):
return_val = _eval_xml(self, n, env)
if return_val is not None:
args.append(return_val)
- model = env[node.get('model', '')]
+ model = env[node.get('model')]
method = node.get('name')
# this one still depends on the old API
return odoo.api.call_kw(model, method, args, {})
@@ -212,8 +215,8 @@ class xml_import(object):
return self.noupdate or (len(data_node) and self.nodeattr2bool(data_node, 'noupdate', False))
def get_context(self, data_node, node, eval_dict):
- data_node_context = (len(data_node) and data_node.get('context','').encode('utf8'))
- node_context = node.get("context",'').encode('utf8')
+ data_node_context = (len(data_node) and data_node.get('context',''))
+ node_context = node.get("context")
context = {}
for ctx in (data_node_context, node_context):
if ctx:
@@ -252,7 +255,7 @@ form: module.record_id""" % (xml_id,)
def _tag_delete(self, rec, data_node=None, mode=None):
d_model = rec.get("model")
- d_search = rec.get("search",'').encode('utf-8')
+ d_search = rec.get("search")
d_id = rec.get("id")
records = self.env[d_model]
@@ -283,7 +286,7 @@ form: module.record_id""" % (xml_id,)
def _tag_report(self, rec, data_node=None, mode=None):
res = {}
for dest,f in (('name','string'),('model','model'),('report_name','name')):
- res[dest] = rec.get(f,'').encode('utf8')
+ res[dest] = rec.get(f)
assert res[dest], "Attribute %s of report is empty !" % (f,)
for field, dest in (('attachment', 'attachment'),
('attachment_use', 'attachment_use'),
@@ -294,7 +297,7 @@ form: module.record_id""" % (xml_id,)
('print_report_name', 'print_report_name'),
):
if rec.get(field):
- res[dest] = rec.get(field).encode('utf8')
+ res[dest] = rec.get(field)
if rec.get('auto'):
res['auto'] = safe_eval(rec.get('auto','False'))
if rec.get('header'):
@@ -302,7 +305,7 @@ form: module.record_id""" % (xml_id,)
res['multi'] = rec.get('multi') and safe_eval(rec.get('multi','False'))
- xml_id = rec.get('id','').encode('utf8')
+ xml_id = rec.get('id','')
self._test_xml_id(xml_id)
if rec.get('groups'):
@@ -346,20 +349,20 @@ form: module.record_id""" % (xml_id,)
return
def _tag_act_window(self, rec, data_node=None, mode=None):
- name = rec.get('name','').encode('utf-8')
- xml_id = rec.get('id','').encode('utf8')
+ name = rec.get('name')
+ xml_id = rec.get('id','')
self._test_xml_id(xml_id)
- type = rec.get('type','').encode('utf-8') or 'ir.actions.act_window'
+ type = rec.get('type') or 'ir.actions.act_window'
view_id = False
if rec.get('view_id'):
- view_id = self.id_get(rec.get('view_id','').encode('utf-8'))
- domain = rec.get('domain','').encode('utf-8') or '[]'
- res_model = rec.get('res_model','').encode('utf-8')
- src_model = rec.get('src_model','').encode('utf-8')
- view_type = rec.get('view_type','').encode('utf-8') or 'form'
- view_mode = rec.get('view_mode','').encode('utf-8') or 'tree,form'
- usage = rec.get('usage','').encode('utf-8')
- limit = rec.get('limit','').encode('utf-8')
+ view_id = self.id_get(rec.get('view_id'))
+ domain = rec.get('domain') or '[]'
+ res_model = rec.get('res_model')
+ src_model = rec.get('src_model')
+ view_type = rec.get('view_type') or 'form'
+ view_mode = rec.get('view_mode') or 'tree,form'
+ usage = rec.get('usage')
+ limit = rec.get('limit')
uid = self.uid
# Act_window's 'domain' and 'context' contain mostly literals
@@ -445,9 +448,9 @@ form: module.record_id""" % (xml_id,)
model = src_model
if isinstance(model, (list, tuple)):
model, res_id = model
- keyword = rec.get('key2','').encode('utf-8') or 'client_action_relate'
- value = 'ir.actions.act_window,'+str(id)
- replace = rec.get('replace','') or True
+ keyword = rec.get('key2') or 'client_action_relate'
+ value = 'ir.actions.act_window,%s' % id
+ replace = rec.get('replace') or True
self.env['ir.values'].set_action(xml_id, action_slot=keyword, model=model, action=value, res_id=res_id)
# TODO add remove ir.model.data
@@ -461,7 +464,7 @@ form: module.record_id""" % (xml_id,)
return
res = {}
for field in rec.findall('./field'):
- f_name = field.get("name",'').encode('utf-8')
+ f_name = field.get("name")
f_val = _eval_xml(self, field, self.env)
res[f_name] = f_val
ir_values = self.env['ir.values']
@@ -475,7 +478,7 @@ form: module.record_id""" % (xml_id,)
ir_values.set_action(res['name'], action_slot=res['key2'], model=model, action=res['value'], res_id=res_id)
def _tag_menuitem(self, rec, data_node=None, mode=None):
- rec_id = rec.get("id",'').encode('ascii')
+ rec_id = rec.get("id")
self._test_xml_id(rec_id)
# The parent attribute was specified, if non-empty determine its ID, otherwise
@@ -495,7 +498,7 @@ form: module.record_id""" % (xml_id,)
res = None
if rec.get('action'):
- a_action = rec.get('action','').encode('utf8')
+ a_action = rec.get('action')
# determine the type of action
action_type, action_id = self.model_id_get(a_action)
@@ -548,13 +551,13 @@ form: module.record_id""" % (xml_id,)
if self.isnoupdate(data_node) and self.mode != 'init':
return
- rec_model = rec.get("model",'').encode('ascii')
- rec_id = rec.get("id",'').encode('ascii')
+ rec_model = rec.get("model")
+ rec_id = rec.get("id")
self._test_xml_id(rec_id)
- rec_src = rec.get("search",'').encode('utf8')
+ rec_src = rec.get("search")
rec_src_count = rec.get("count")
- rec_string = rec.get("string",'').encode('utf8') or 'unknown'
+ rec_string = rec.get("string") or 'unknown'
records = None
eval_dict = {'ref': self.id_get}
@@ -587,7 +590,7 @@ form: module.record_id""" % (xml_id,)
globals_dict['ref'] = ref
globals_dict['_ref'] = ref
for test in rec.findall('./test'):
- f_expr = test.get("expr",'').encode('utf-8')
+ f_expr = test.get("expr",'')
env = self.env(user=uid, context=context)
expected_value = _eval_xml(self, test, env) or True
expression_value = safe_eval(f_expr, globals_dict)
@@ -597,16 +600,16 @@ form: module.record_id""" % (xml_id,)
' xmltag: %s\n' \
' expected value: %r\n' \
' obtained value: %r\n' \
- % (rec_string, etree.tostring(test), expected_value, expression_value)
+ % (rec_string, etree.tostring(test, encoding='unicode'), expected_value, expression_value)
_logger.error(msg)
return
else: # all tests were successful for this assertion tag (no break)
self.assertion_report.record_success()
def _tag_record(self, rec, data_node=None, mode=None):
- rec_model = rec.get("model").encode('ascii')
+ rec_model = rec.get("model")
model = self.env[rec_model]
- rec_id = rec.get("id",'').encode('ascii')
+ rec_id = rec.get("id",'')
rec_context = rec.get("context", {})
if rec_context:
rec_context = safe_eval(rec_context)
@@ -647,13 +650,13 @@ form: module.record_id""" % (xml_id,)
res = {}
for field in rec.findall('./field'):
#TODO: most of this code is duplicated above (in _eval_xml)...
- f_name = field.get("name").encode('utf-8')
- f_ref = field.get("ref",'').encode('utf-8')
- f_search = field.get("search",'').encode('utf-8')
- f_model = field.get("model",'').encode('utf-8')
+ f_name = field.get("name")
+ f_ref = field.get("ref")
+ f_search = field.get("search")
+ f_model = field.get("model")
if not f_model and f_name in model._fields:
f_model = model._fields[f_name].comodel_name
- f_use = field.get("use",'').encode('utf-8') or 'id'
+ f_use = field.get("use",'') or 'id'
f_val = False
if f_search:
@@ -683,7 +686,7 @@ form: module.record_id""" % (xml_id,)
f_val = int(f_val)
elif model._fields[f_name].type in ['float', 'monetary']:
f_val = float(f_val)
- elif model._fields[f_name].type == 'boolean' and isinstance(f_val, basestring):
+ elif model._fields[f_name].type == 'boolean' and isinstance(f_val, pycompat.string_types):
f_val = str2bool(f_val)
res[f_name] = f_val
@@ -696,7 +699,7 @@ form: module.record_id""" % (xml_id,)
def _tag_template(self, el, data_node=None, mode=None):
# This helper transforms a <template> element into a <record> and forwards it
- tpl_id = el.get('id', el.get('t-name', '')).encode('ascii')
+ tpl_id = el.get('id', el.get('t-name'))
full_tpl_id = tpl_id
if '.' not in full_tpl_id:
full_tpl_id = '%s.%s' % (self.module, tpl_id)
@@ -785,7 +788,7 @@ form: module.record_id""" % (xml_id,)
exc_info = sys.exc_info()
pycompat.reraise(
ParseError,
- ParseError(ustr(e), etree.tostring(rec).rstrip(), rec.getroottree().docinfo.URL, rec.sourceline),
+ ParseError(ustr(e), etree.tostring(rec, encoding='unicode').rstrip(), rec.getroottree().docinfo.URL, rec.sourceline),
exc_info[2]
)
return True
@@ -817,10 +820,9 @@ form: module.record_id""" % (xml_id,)
def convert_file(cr, module, filename, idref, mode='update', noupdate=False, kind=None, report=None, pathname=None):
if pathname is None:
pathname = os.path.join(module, filename)
- fp = file_open(pathname)
ext = os.path.splitext(filename)[1].lower()
- try:
+ with file_open(pathname, 'rb') as fp:
if ext == '.csv':
convert_csv_import(cr, module, pathname, fp.read(), idref, mode, noupdate)
elif ext == '.sql':
@@ -833,8 +835,6 @@ def convert_file(cr, module, filename, idref, mode='update', noupdate=False, kin
pass # .js files are valid but ignored here.
else:
raise ValueError("Can't load unknown file type %s.", filename)
- finally:
- fp.close()
def convert_sql_import(cr, fp):
cr.execute(fp.read())
@@ -845,28 +845,21 @@ def convert_csv_import(cr, module, fname, csvcontent, idref=None, mode='init',
quote: "
delimiter: ,
encoding: utf-8'''
- if not idref:
- idref={}
- model = ('.'.join(fname.split('.')[:-1]).split('-'))[0]
- #remove folder path from model
- head, model = os.path.split(model)
+ filename, _ext = os.path.splitext(os.path.basename(fname))
+ model = filename.split('-')[0]
- input = io.BytesIO(csvcontent) #FIXME
- reader = csv.reader(input, quotechar='"', delimiter=',')
+ reader = pycompat.csv_reader(io.BytesIO(csvcontent), quotechar='"', delimiter=',')
fields = next(reader)
if not (mode == 'init' or 'id' in fields):
_logger.error("Import specification does not contain 'id' and we are in init mode, Cannot continue.")
return
- datas = []
- for line in reader:
- if not (line and any(line)):
- continue
- try:
- datas.append([ustr(v) for v in line])
- except Exception:
- _logger.error("Cannot import the line: %s", line)
+ # filter out empty lines (any([]) == False) and lines containing only empty cells
+ datas = [
+ line for line in reader
+ if any(line)
+ ]
context = {
'mode': mode,
@@ -893,7 +886,7 @@ def convert_xml_import(cr, module, xmlfile, idref=None, mode='init', noupdate=Fa
if idref is None:
idref={}
- if isinstance(xmlfile, basestring):
+ if isinstance(xmlfile, pycompat.string_types):
xml_filename = xmlfile
else:
xml_filename = xmlfile.name
diff --git a/odoo/tools/float_utils.py b/odoo/tools/float_utils.py
index d71576adc92..93f3c79ef2a 100644
--- a/odoo/tools/float_utils.py
+++ b/odoo/tools/float_utils.py
@@ -6,6 +6,25 @@ import math
from odoo.tools import pycompat
+if not pycompat.PY2:
+ import builtins
+ def round(f):
+ # P3's builtin round differs from P2 in the following manner:
+ # * it rounds half to even rather than up (away from 0)
+ # * round(-0.) loses the sign (it returns -0 rather than 0)
+ # * round(x) returns an int rather than a float
+ #
+ # this compatibility shim implements Python 2's round in terms of
+ # Python 3's so that important rounding error under P3 can be
+ # trivially fixed, assuming the P2 behaviour to be debugged and
+ # correct.
+ roundf = builtins.round(f)
+ if builtins.round(f + 1) - roundf != 1:
+ return f + math.copysign(0.5, f)
+ # copysign ensures round(-0.) -> -0 *and* result is a float
+ return math.copysign(roundf, f)
+else:
+ round = round
def _float_check_precision(precision_digits=None, precision_rounding=None):
assert (precision_digits is not None or precision_rounding is not None) and \
diff --git a/odoo/tools/graph.py b/odoo/tools/graph.py
index 5e3edabf05a..d43088d97ab 100755
--- a/odoo/tools/graph.py
+++ b/odoo/tools/graph.py
@@ -5,8 +5,6 @@
import operator
import math
-from odoo.tools import pycompat
-
class graph(object):
def __init__(self, nodes, transitions, no_ancester=None):
@@ -259,7 +257,7 @@ class graph(object):
"""The ranks are normalized by setting the least rank to zero.
"""
- least_rank = min(x['x'] for x in pycompat.values(self.result.values))
+ least_rank = min(x['x'] for x in self.result.values.values())
if least_rank!=0:
for node in self.result:
@@ -383,7 +381,7 @@ class graph(object):
"""Finds actual-order of the nodes with respect to maximum number of nodes in a rank in component
"""
mid_pos = 0.0
- max_level = max(len(x) for x in pycompat.values(self.levels.values))
+ max_level = max(len(x) for x in self.levels.values.values())
for level in self.levels:
if level:
@@ -474,7 +472,7 @@ class graph(object):
"""
if self.Is_Cyclic:
- max_level = max(len(x) for x in pycompat.values(self.levels.values))
+ max_level = max(len(x) for x in self.levels.values.values())
if max_level%2:
self.result[self.start]['y'] = (max_level+1)/2 + self.max_order + (self.max_order and 1)
@@ -486,7 +484,7 @@ class graph(object):
else:
self.result[self.start]['y'] = 0
self.tree_order(self.start, 0)
- min_order = math.fabs(min(x['y'] for x in pycompat.values(self.result.values)))
+ min_order = math.fabs(min(x['y'] for x in self.result.values.values()))
index = self.start_nodes.index(self.start)
same = False
@@ -540,7 +538,7 @@ class graph(object):
self.result[start]['y'] = base + factor
factor += 1
- self.max_order = max(x['y'] for x in pycompat.values(self.result.values))
+ self.max_order = max(x['y'] for x in self.result.values.values())
def find_starts(self):
"""Finds other start nodes of the graph in the case when graph is disconneted
@@ -660,7 +658,7 @@ class graph(object):
for node in self.no_ancester:
for sec_node in self.transitions.get(node, []):
- if sec_node in pycompat.keys(self.partial_order):
+ if sec_node in self.partial_order:
self.transitions[self.start_nodes[0]].append(node)
break
@@ -746,7 +744,7 @@ if __name__=='__main__':
for node in nodes:
node_res[node] = result[node]
- for name,node in pycompat.items(node_res):
+ for name, node in node_res.items():
draw.arc( (int(node['y']-radius), int(node['x']-radius),int(node['y']+radius), int(node['x']+radius) ), 0, 360, (128,128,128))
draw.text( (int(node['y']), int(node['x'])), str(name), (128,128,128))
diff --git a/odoo/tools/image.py b/odoo/tools/image.py
index 791d1a978d3..0f830b0924c 100644
--- a/odoo/tools/image.py
+++ b/odoo/tools/image.py
@@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-try:
- import cStringIO as StringIO
-except ImportError:
- import StringIO
+import base64
+import codecs
+import io
from PIL import Image
from PIL import ImageEnhance
@@ -52,7 +51,7 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file
return False
if size == (None, None):
return base64_source
- image_stream = StringIO.StringIO(base64_source.decode(encoding))
+ image_stream = io.BytesIO(codecs.decode(base64_source, encoding))
image = Image.open(image_stream)
# store filetype here, as Image.new below will lose image.format
filetype = (filetype or image.format).upper()
@@ -77,9 +76,9 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file
if image.mode not in ["1", "L", "P", "RGB", "RGBA"] or (filetype == 'JPEG' and image.mode == 'RGBA'):
image = image.convert("RGB")
- background_stream = StringIO.StringIO()
+ background_stream = io.BytesIO()
image.save(background_stream, filetype)
- return background_stream.getvalue().encode(encoding)
+ return codecs.encode(background_stream.getvalue(), encoding)
def image_resize_and_sharpen(image, size, preserve_aspect_ratio=False, factor=2.0):
"""
@@ -130,7 +129,7 @@ def image_save_for_web(image, fp=None, format=None):
if fp:
image.save(fp, **opt)
else:
- img = StringIO.StringIO()
+ img = io.BytesIO()
image.save(img, **opt)
return img.getvalue()
@@ -172,8 +171,8 @@ def crop_image(data, type='top', ratio=False, thumbnail_ratio=None, image_format
"""
if not data:
return False
- image_stream = Image.open(StringIO.StringIO(data.decode('base64')))
- output_stream = StringIO.StringIO()
+ image_stream = Image.open(io.BytesIO(base64.b64decode(data)))
+ output_stream = io.BytesIO()
w, h = image_stream.size
new_h = h
new_w = w
@@ -199,10 +198,10 @@ def crop_image(data, type='top', ratio=False, thumbnail_ratio=None, image_format
raise ValueError('ERROR: invalid value for crop_type')
# TDE FIXME: should not have a ratio, makes no sense -> should have maximum width (std: 64; 256 px)
if thumbnail_ratio:
- thumb_image = Image.open(StringIO.StringIO(output_stream.getvalue()))
+ thumb_image = Image.open(io.BytesIO(output_stream.getvalue()))
thumb_image.thumbnail((new_w // thumbnail_ratio, new_h // thumbnail_ratio), Image.ANTIALIAS)
thumb_image.save(output_stream, image_format)
- return output_stream.getvalue().encode('base64')
+ return base64.b64encode(output_stream.getvalue())
# ----------------------------------------
# Colors
@@ -215,7 +214,7 @@ def image_colorize(original, randomize=True, color=(255, 255, 255)):
:param color: background-color, if not randomize
"""
# create a new image, based on the original one
- original = Image.open(StringIO.StringIO(original))
+ original = Image.open(io.BytesIO(original))
image = Image.new('RGB', original.size)
# generate the background color, past it as background
if randomize:
@@ -223,7 +222,7 @@ def image_colorize(original, randomize=True, color=(255, 255, 255)):
image.paste(color, box=(0, 0) + original.size)
image.paste(original, mask=original)
# return the new image
- buffer = StringIO.StringIO()
+ buffer = io.BytesIO()
image.save(buffer, 'PNG')
return buffer.getvalue()
@@ -286,6 +285,6 @@ if __name__=="__main__":
assert len(sys.argv)==3, 'Usage to Test: image.py SRC.png DEST.png'
- img = open(sys.argv[1],'rb').read().encode('base64')
+ img = base64.b64encode(open(sys.argv[1],'rb').read())
new = image_resize_image(img, (128,100))
- open(sys.argv[2], 'wb').write(new.decode('base64'))
+ open(sys.argv[2], 'wb').write(base64.b64decode(new))
diff --git a/odoo/tools/lru.py b/odoo/tools/lru.py
index 21ecaf6383a..119fce0e1ee 100644
--- a/odoo/tools/lru.py
+++ b/odoo/tools/lru.py
@@ -2,7 +2,6 @@
# taken from http://code.activestate.com/recipes/252524-length-limited-o1-lru-cache-implementation/
import threading
-from odoo.tools import pycompat
from .func import synchronized
__all__ = ['LRU']
@@ -110,12 +109,11 @@ class LRU(object):
@synchronized()
def itervalues(self):
- for i,j in pycompat.items(self):
- yield j
+ return iter(self.d.values())
@synchronized()
def keys(self):
- return list(pycompat.keys(self.d))
+ return list(self.d)
@synchronized()
def pop(self,key):
diff --git a/odoo/tools/mail.py b/odoo/tools/mail.py
index 530f3e63862..288defdcf1f 100644
--- a/odoo/tools/mail.py
+++ b/odoo/tools/mail.py
@@ -85,7 +85,7 @@ class _Cleaner(clean.Cleaner):
new_node.text = text
new_node.tail = tail
if attrs:
- for key, val in pycompat.items(attrs):
+ for key, val in attrs.items():
new_node.set(key, val)
return new_node
@@ -154,7 +154,7 @@ class _Cleaner(clean.Cleaner):
if style[0].lower() in self._style_whitelist:
valid_styles[style[0].lower()] = style[1]
if valid_styles:
- el.attrib['style'] = '; '.join('%s: %s' % (key, val) for (key, val) in pycompat.items(valid_styles))
+ el.attrib['style'] = '; '.join('%s: %s' % (key, val) for (key, val) in valid_styles.items())
else:
del el.attrib['style']
@@ -170,7 +170,7 @@ def html_sanitize(src, silent=True, sanitize_tags=True, sanitize_attributes=Fals
src = ustr(src, errors='replace')
# html: remove encoding attribute inside tags
doctype = re.compile(r'(<[^>]*\s)(encoding=(["\'][^"\']*?["\']|[^\s\n\r>]+)(\s[^>]*|/)?>)', re.IGNORECASE | re.DOTALL)
- src = doctype.sub(r"", src)
+ src = doctype.sub(u"", src)
logger = logging.getLogger(__name__ + '.html_sanitize')
@@ -178,10 +178,10 @@ def html_sanitize(src, silent=True, sanitize_tags=True, sanitize_attributes=Fals
part = re.compile(r"(<(([^a<>]|a[^<>\s])[^<>]*)@[^<>]+>)", re.IGNORECASE | re.DOTALL)
# remove results containing cite="mid:email_like@address" (ex: blockquote cite)
# cite_except = re.compile(r"^((?!cite[\s]*=['\"]).)*$", re.IGNORECASE)
- src = part.sub(lambda m: ('cite=' not in m.group(1) and 'alt=' not in m.group(1)) and misc.html_escape(m.group(1)) or m.group(1), src)
+ src = part.sub(lambda m: (u'cite=' not in m.group(1) and u'alt=' not in m.group(1)) and misc.html_escape(m.group(1)) or m.group(1), src)
# html encode mako tags <% ... %> to decode them later and keep them alive, otherwise they are stripped by the cleaner
- src = src.replace('<%', misc.html_escape('<%'))
- src = src.replace('%>', misc.html_escape('%>'))
+ src = src.replace(u'<%', misc.html_escape(u'<%'))
+ src = src.replace(u'%>', misc.html_escape(u'%>'))
kwargs = {
'page_structure': True,
@@ -222,33 +222,34 @@ def html_sanitize(src, silent=True, sanitize_tags=True, sanitize_attributes=Fals
# some corner cases make the parser crash (such as <SCRIPT/XSS SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT> in test_mail)
cleaner = _Cleaner(**kwargs)
cleaned = cleaner.clean_html(src)
+ assert isinstance(cleaned, pycompat.text_type)
# MAKO compatibility: $, { and } inside quotes are escaped, preventing correct mako execution
- cleaned = cleaned.replace('%24', '$')
- cleaned = cleaned.replace('%7B', '{')
- cleaned = cleaned.replace('%7D', '}')
- cleaned = cleaned.replace('%20', ' ')
- cleaned = cleaned.replace('%5B', '[')
- cleaned = cleaned.replace('%5D', ']')
- cleaned = cleaned.replace('%7C', '|')
- cleaned = cleaned.replace('&lt;%', '<%')
- cleaned = cleaned.replace('%&gt;', '%>')
+ cleaned = cleaned.replace(u'%24', u'$')
+ cleaned = cleaned.replace(u'%7B', u'{')
+ cleaned = cleaned.replace(u'%7D', u'}')
+ cleaned = cleaned.replace(u'%20', u' ')
+ cleaned = cleaned.replace(u'%5B', u'[')
+ cleaned = cleaned.replace(u'%5D', u']')
+ cleaned = cleaned.replace(u'%7C', u'|')
+ cleaned = cleaned.replace(u'&lt;%', u'<%')
+ cleaned = cleaned.replace(u'%&gt;', u'%>')
# html considerations so real html content match database value
- cleaned.replace(u'\xa0', '&nbsp;')
+ cleaned.replace(u'\xa0', u'&nbsp;')
except etree.ParserError as e:
- if 'empty' in str(e):
- return ""
+ if u'empty' in pycompat.text_type(e):
+ return u""
if not silent:
raise
- logger.warning('ParserError obtained when sanitizing %r', src, exc_info=True)
- cleaned = '<p>ParserError when sanitizing</p>'
+ logger.warning(u'ParserError obtained when sanitizing %r', src, exc_info=True)
+ cleaned = u'<p>ParserError when sanitizing</p>'
except Exception:
if not silent:
raise
- logger.warning('unknown error obtained when sanitizing %r', src, exc_info=True)
- cleaned = '<p>Unknown error when sanitizing</p>'
+ logger.warning(u'unknown error obtained when sanitizing %r', src, exc_info=True)
+ cleaned = u'<p>Unknown error when sanitizing</p>'
# this is ugly, but lxml/etree tostring want to put everything in a 'div' that breaks the editor -> remove that
- if cleaned.startswith('<div>') and cleaned.endswith('</div>'):
+ if cleaned.startswith(u'<div>') and cleaned.endswith(u'</div>'):
cleaned = cleaned[5:-6]
return cleaned
diff --git a/odoo/tools/mimetypes.py b/odoo/tools/mimetypes.py
index 3abdc22963d..59421cf8bb6 100644
--- a/odoo/tools/mimetypes.py
+++ b/odoo/tools/mimetypes.py
@@ -10,8 +10,6 @@ import logging
import re
import zipfile
-from odoo.tools import pycompat
-
__all__ = ['guess_mimetype']
_logger = logging.getLogger(__name__)
@@ -35,7 +33,7 @@ def _check_ooxml(data):
# then there is a directory whose name denotes the type of the file:
# word, pt (powerpoint) or xl (excel)
- for dirname, mime in pycompat.items(_ooxml_dirs):
+ for dirname, mime in _ooxml_dirs.items():
if any(entry.startswith(dirname) for entry in filenames):
return mime
@@ -51,13 +49,20 @@ _mime_validator = re.compile(r"""
(?:\+[\w-]+)? # optional structured syntax specifier
""", re.VERBOSE)
def _check_open_container_format(data):
+ # Open Document Format for Office Applications (OpenDocument) Version 1.2
+ #
+ # Part 3: Packages
+ # 3 Packages
+ # 3.3 MIME Media Type
with io.BytesIO(data) as f, zipfile.ZipFile(f) as z:
- # OCF zip files must contain a ``mimetype`` entry
+ # If a MIME media type for a document exists, then an OpenDocument
+ # package should contain a file with name "mimetype".
if 'mimetype' not in z.namelist():
return False
- # it holds the exact mimetype for the file
- marcel = z.read('mimetype')
+ # The content of this file shall be the ASCII encoded MIME media type
+ # associated with the document.
+ marcel = z.read('mimetype').decode('ascii')
# check that it's not too long (RFC6838 ยง 4.2 restricts type and
# subtype to 127 characters each + separator, strongly recommends
# limiting them to 64 but does not require it) and that it looks a lot
@@ -67,11 +72,11 @@ def _check_open_container_format(data):
return False
-_xls_pattern = re.compile("""
+_xls_pattern = re.compile(b"""
\x09\x08\x10\x00\x00\x06\x05\x00
| \xFD\xFF\xFF\xFF(\x10|\x1F|\x20|"|\\#|\\(|\\))
""", re.VERBOSE)
-_ppt_pattern = re.compile("""
+_ppt_pattern = re.compile(b"""
\x00\x6E\x1E\xF0
| \x0F\x00\xE8\x03
| \xA0\x46\x1D\xF0
@@ -87,11 +92,11 @@ def _check_olecf(data):
ignore that.
"""
offset = 0x200
- if data.startswith('\xEC\xA5\xC1\x00', offset):
+ if data.startswith(b'\xEC\xA5\xC1\x00', offset):
return 'application/msword'
# the _xls_pattern stuff doesn't seem to work correctly (the test file
# only has a bunch of \xf* at offset 0x200), that apparently works
- elif 'Microsoft Excel' in data:
+ elif b'Microsoft Excel' in data:
return 'application/vnd.ms-excel'
elif _ppt_pattern.match(data, offset):
return 'application/vnd.ms-powerpoint'
@@ -104,18 +109,18 @@ def _check_olecf(data):
_Entry = collections.namedtuple('_Entry', ['mimetype', 'signatures', 'discriminants'])
_mime_mappings = (
# pdf
- _Entry('application/pdf', ['%PDF'], []),
+ _Entry('application/pdf', [b'%PDF'], []),
# jpg, jpeg, png, gif, bmp
- _Entry('image/jpeg', ['\xFF\xD8\xFF\xE0', '\xFF\xD8\xFF\xE2', '\xFF\xD8\xFF\xE3', '\xFF\xD8\xFF\xE1'], []),
- _Entry('image/png', ['\x89PNG\r\n\x1A\n'], []),
- _Entry('image/gif', ['GIF87a', 'GIF89a'], []),
- _Entry('image/bmp', ['BM'], []),
+ _Entry('image/jpeg', [b'\xFF\xD8\xFF\xE0', b'\xFF\xD8\xFF\xE2', b'\xFF\xD8\xFF\xE3', b'\xFF\xD8\xFF\xE1'], []),
+ _Entry('image/png', [b'\x89PNG\r\n\x1A\n'], []),
+ _Entry('image/gif', [b'GIF87a', b'GIF89a'], []),
+ _Entry('image/bmp', [b'BM'], []),
# OLECF files in general (Word, Excel, PPT, default to word because why not?)
- _Entry('application/msword', ['\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1', '\x0D\x44\x4F\x43'], [
+ _Entry('application/msword', [b'\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1', b'\x0D\x44\x4F\x43'], [
_check_olecf
]),
# zip, but will include jar, odt, ods, odp, docx, xlsx, pptx, apk
- _Entry('application/zip', ['PK\x03\x04'], [_check_ooxml, _check_open_container_format]),
+ _Entry('application/zip', [b'PK\x03\x04'], [_check_ooxml, _check_open_container_format]),
)
def guess_mimetype(bin_data, default='application/octet-stream'):
""" Attempts to guess the mime type of the provided binary data, similar
diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py
index 8d3854aeb6d..9116e08cec8 100644
--- a/odoo/tools/misc.py
+++ b/odoo/tools/misc.py
@@ -12,6 +12,8 @@ import datetime
import subprocess
import io
import os
+
+import collections
import passlib.utils
import pickle as pickle_
import re
@@ -28,7 +30,6 @@ from lxml import etree
from .which import which
import traceback
-import csv
from operator import itemgetter
try:
@@ -274,16 +275,12 @@ def flatten(list):
>>> flatten(t)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
"""
-
- def isiterable(x):
- return hasattr(x, "__iter__")
-
r = []
for e in list:
- if isiterable(e):
- r.extend(flatten(e))
- else:
+ if isinstance(e, (bytes, pycompat.text_type)) or not isinstance(e, collections.Iterable):
r.append(e)
+ else:
+ r.extend(flatten(e))
return r
def reverse_enumerate(l):
@@ -405,14 +402,15 @@ def scan_languages():
csvpath = odoo.modules.module.get_resource_path('base', 'res', 'res.lang.csv')
try:
# read (code, name) from languages in base/res/res.lang.csv
- result = []
- with open(csvpath) as csvfile:
- reader = csv.reader(csvfile, delimiter=',', quotechar='"')
+ with open(csvpath, 'rb') as csvfile:
+ reader = pycompat.csv_reader(csvfile, delimiter=',', quotechar='"')
fields = next(reader)
code_index = fields.index("code")
name_index = fields.index("name")
- for row in reader:
- result.append((ustr(row[code_index]), ustr(row[name_index])))
+ result = [
+ (row[code_index], row[name_index])
+ for row in reader
+ ]
except Exception:
_logger.error("Could not read %s", csvpath)
result = []
@@ -465,7 +463,7 @@ def human_size(sz):
if not sz:
return False
units = ('bytes', 'Kb', 'Mb', 'Gb')
- if isinstance(sz,basestring):
+ if isinstance(sz,pycompat.string_types):
sz=len(sz)
s, i = float(sz), 0
while s >= 1024 and i < len(units)-1:
@@ -481,7 +479,7 @@ def logged(f):
vector = ['Call -> function: %r' % f]
for i, arg in enumerate(args):
vector.append(' arg %02d: %s' % (i, pformat(arg)))
- for key, value in pycompat.items(kwargs):
+ for key, value in kwargs.items():
vector.append(' kwarg %10s: %s' % (key, pformat(value)))
timeb4 = time.time()
@@ -798,7 +796,7 @@ class mute_logger(object):
def __enter__(self):
for logger in self.loggers:
- assert isinstance(logger, basestring),\
+ assert isinstance(logger, pycompat.string_types),\
"A logger name must be a string, got %s" % type(logger)
logging.getLogger(logger).addFilter(self)
@@ -910,7 +908,7 @@ def dumpstacks(sig=None, frame=None):
'dbname': getattr(th, 'dbname', 'n/a'),
'url': getattr(th, 'url', 'n/a')}
for th in threading.enumerate()}
- for threadId, stack in pycompat.items(sys._current_frames()):
+ for threadId, stack in sys._current_frames().items():
thread_info = threads_info.get(threadId, {})
code.append("\n# Thread: %s (id:%s) (db:%s) (uid:%s) (url:%s)" %
(thread_info.get('name', 'n/a'),
@@ -962,7 +960,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()))
class Collector(Mapping):
""" A mapping from keys to lists. This is essentially a space optimization
@@ -1073,7 +1071,7 @@ def formatLang(env, value, digits=None, grouping=True, monetary=False, dp=False,
if not digits and digits is not 0:
digits = DEFAULT_DIGITS
- if isinstance(value, (str, unicode)) and not value:
+ if isinstance(value, pycompat.string_types) and not value:
return ''
lang = env.user.company_id.partner_id.lang or 'en_US'
@@ -1108,7 +1106,7 @@ def format_date(env, value, lang_code=False, date_format=False):
return ''
if isinstance(value, datetime.datetime):
value = value.date()
- elif isinstance(value, basestring):
+ elif isinstance(value, pycompat.string_types):
if len(value) < DATE_LENGTH:
return ''
value = value[:DATE_LENGTH]
diff --git a/odoo/tools/pycompat.py b/odoo/tools/pycompat.py
index 19b821863b2..feb65da7a67 100644
--- a/odoo/tools/pycompat.py
+++ b/odoo/tools/pycompat.py
@@ -1,56 +1,82 @@
# -*- coding: utf-8 -*-
# to remove if we decide to add a dependency on six or future
# very strongly inspired by https://github.com/pallets/werkzeug/blob/master/werkzeug/_compat.py
+#pylint: disable=deprecated-module
+import csv
+import collections
+import io
import sys
+
PY2 = sys.version_info[0] == 2
+_Writer = collections.namedtuple('_Writer', 'writerow writerows')
if PY2:
- # pylint: disable=long-builtin,dict-iter-method
- integer_types = (int, long)
- round = round
+ # pylint: disable=long-builtin,unichr-builtin,unicode-builtin
+ unichr = unichr
+ text_type = unicode
+ string_types = (str, unicode)
+ def to_native(source, encoding='utf-8', falsy_empty=False):
+ if not source and falsy_empty:
+ return ''
+
+ if isinstance(source, text_type):
+ return source.encode(encoding)
- keys = lambda d: iter(d.iterkeys())
- values = lambda d: iter(d.itervalues())
- items = lambda d: iter(d.iteritems())
+ return str(source)
+
+ integer_types = (int, long)
# noinspection PyUnresolvedReferences
from itertools import imap, izip, ifilter
+ def implements_to_string(cls):
+ cls.__unicode__ = cls.__str__
+ cls.__str__ = lambda x: x.__unicode__().encode('utf-8')
+ return cls
+
def implements_iterator(cls):
cls.next = cls.__next__
del cls.__next__
return cls
exec ('def reraise(tp, value, tb=None):\n raise tp, value, tb')
+
+ def csv_reader(stream, **params):
+ for row in csv.reader(stream, **params):
+ yield [c.decode('utf-8') for c in row]
+ def csv_writer(stream, **params):
+ w = csv.writer(stream, **params)
+ return _Writer(
+ writerow=lambda r: w.writerow([c.encode('utf-8') for c in r]),
+ writerows=lambda rs: w.writerows(
+ [c.encode('utf-8') for c in r]
+ for r in rs
+ )
+ )
else:
- import builtins, math
# pylint: disable=bad-functions
+ unichr = chr
+ text_type = str
+ string_types = (str,)
+ def to_native(source, encoding='utf-8', falsy_empty=False):
+ if not source and falsy_empty:
+ return ''
+
+ if isinstance(source, bytes):
+ return source.decode(encoding)
+
+ return str(source)
+
integer_types = (int,)
- def round(f):
- # P3's builtin round differs from P2 in the following manner:
- # * it rounds half to even rather than up (away from 0)
- # * round(-0.) loses the sign (it returns -0 rather than 0)
- # * round(x) returns an int rather than a float
- #
- # this compatibility shim implements Python 2's round in terms of
- # Python 3's so that important rounding error under P3 can be
- # trivially fixed, assuming the P2 behaviour to be debugged and
- # correct.
- roundf = builtins.round(f)
- if builtins.round(f + 1) - roundf != 1:
- return f + math.copysign(0.5, f)
- # copysign ensures round(-0.) -> -0 *and* result is a float
- return math.copysign(roundf, f)
-
- keys = lambda d: iter(d.keys())
- values = lambda d: iter(d.values())
- items = lambda d: iter(d.items())
imap = map
izip = zip
ifilter = filter
+ def implements_to_string(cls):
+ return cls
+
def implements_iterator(cls):
return cls
@@ -58,3 +84,29 @@ else:
if value.__traceback__ != tb:
raise value.with_traceback(tb)
raise value
+
+ def csv_reader(stream, **params):
+ assert not isinstance(stream, io.TextIOBase),\
+ "For cross-compatibility purposes, csv_reader takes a bytes stream"
+ return csv.reader(io.TextIOWrapper(stream, encoding='utf-8'), **params)
+ def csv_writer(stream, **params):
+ assert not isinstance(stream, io.TextIOBase), \
+ "For cross-compatibility purposes, csv_writer takes a bytes stream"
+ return csv.writer(io.TextIOWrapper(stream, encoding='utf-8', line_buffering=True), **params)
+
+def to_text(source):
+ """ Generates a text value (an instance of text_type) from an arbitrary
+ source.
+
+ * False and None are converted to empty strings
+ * text is passed through
+ * bytes are decoded as UTF-8
+ * rest is textified via the current version's relevant data model method
+ """
+ if source is None or source is False:
+ return u''
+
+ if isinstance(source, bytes):
+ return source.decode('utf-8')
+
+ return text_type(source)
diff --git a/odoo/tools/safe_eval.py b/odoo/tools/safe_eval.py
index de767fb7453..fec89fac087 100644
--- a/odoo/tools/safe_eval.py
+++ b/odoo/tools/safe_eval.py
@@ -26,6 +26,7 @@ import werkzeug
from . import pycompat
from .misc import ustr
+from . import pycompat
import odoo
@@ -267,8 +268,9 @@ _BUILTINS = {
'True': True,
'False': False,
'None': None,
+ 'bytes': bytes,
'str': str,
- 'unicode': unicode,
+ 'unicode': pycompat.text_type,
'bool': bool,
'int': int,
'float': float,
diff --git a/odoo/tools/test_reports.py b/odoo/tools/test_reports.py
index cad44431c1d..a3588577c08 100644
--- a/odoo/tools/test_reports.py
+++ b/odoo/tools/test_reports.py
@@ -7,15 +7,14 @@
through the code of yaml tests.
"""
-import odoo
-import odoo.tools as tools
import logging
-
-from odoo.tools import pycompat
-from odoo.tools.safe_eval import safe_eval
-from subprocess import Popen, PIPE
import os
import tempfile
+from subprocess import Popen, PIPE
+
+from .. import api
+from . import pycompat, ustr, config
+from .safe_eval import safe_eval
_logger = logging.getLogger(__name__)
_test_logger = logging.getLogger('odoo.tests')
@@ -30,7 +29,7 @@ def try_report(cr, uid, rname, ids, data=None, context=None, our_module=None, re
context = {}
_test_logger.info(" - Trying %s.create(%r)", rname, ids)
- env = odoo.api.Environment(cr, uid, context)
+ env = api.Environment(cr, uid, context)
report_id = env['ir.actions.report'].search([('report_name', '=', rname)], limit=1)
if not report_id:
@@ -41,12 +40,12 @@ def try_report(cr, uid, rname, ids, data=None, context=None, our_module=None, re
if not res_data:
raise ValueError("Report %s produced an empty result!" % rname)
- if tools.config['test_report_directory']:
- open(os.path.join(tools.config['test_report_directory'], rname+ '.'+res_format), 'wb+').write(res_data)
+ if config['test_report_directory']:
+ open(os.path.join(config['test_report_directory'], rname+ '.'+res_format), 'wb+').write(res_data)
_logger.debug("Have a %s report for %s, will examine it", res_format, rname)
if res_format == 'pdf':
- if res_data[:5] != '%PDF-':
+ if res_data[:5] != b'%PDF-':
raise ValueError("Report %s produced a non-pdf header, %r" % (rname, res_data[:10]))
res_text = False
try:
@@ -56,7 +55,7 @@ def try_report(cr, uid, rname, ids, data=None, context=None, our_module=None, re
proc = Popen(['pdftotext', '-enc', 'UTF-8', '-nopgbrk', rfname, '-'], shell=False, stdout=PIPE)
stdout, stderr = proc.communicate()
- res_text = tools.ustr(stdout)
+ res_text = ustr(stdout)
os.unlink(rfname)
except Exception:
_logger.debug("Unable to parse PDF report: install pdftotext to perform automated tests.")
@@ -92,14 +91,14 @@ def try_report_action(cr, uid, action_id, active_model=None, active_ids=None,
Eg. 'OK' or 'fa-print'
:param our_module: the name of the calling module (string), like 'account'
"""
- if not our_module and isinstance(action_id, basestring):
+ if not our_module and isinstance(action_id, pycompat.string_types):
if '.' in action_id:
our_module = action_id.split('.', 1)[0]
context = dict(context or {})
# TODO context fill-up
- env = odoo.api.Environment(cr, uid, context)
+ env = api.Environment(cr, uid, context)
def log_test(msg, *args):
_test_logger.info(" - " + msg, *args)
@@ -113,7 +112,7 @@ def try_report_action(cr, uid, action_id, active_model=None, active_ids=None,
if not wiz_buttons:
wiz_buttons = []
- if isinstance(action_id, basestring):
+ if isinstance(action_id, pycompat.string_types):
if '.' in action_id:
_, act_xmlid = action_id.split('.', 1)
else:
@@ -138,7 +137,7 @@ def try_report_action(cr, uid, action_id, active_model=None, active_ids=None,
if datas.get('id',False):
context.update( {'active_id': datas.get('id',False), 'active_ids': datas.get('ids',[]), 'active_model': datas.get('model',False)})
context1 = action.get('context', {})
- if isinstance(context1, basestring):
+ if isinstance(context1, pycompat.string_types):
context1 = safe_eval(context1, dict(context))
context.update(context1)
env = env(context=context)
@@ -174,7 +173,7 @@ def try_report_action(cr, uid, action_id, active_model=None, active_ids=None,
view_data.update(wiz_data)
_logger.debug("View data is: %r", view_data)
- for fk, field in pycompat.items(view_res.get('fields',{})):
+ for fk, field in view_res.get('fields',{}).items():
# Default fields returns list of int, while at create()
# we need to send a [(6,0,[int,..])]
if field['type'] in ('one2many', 'many2many') \
diff --git a/odoo/tools/translate.py b/odoo/tools/translate.py
index 49aeb2864a8..c244f88e849 100644
--- a/odoo/tools/translate.py
+++ b/odoo/tools/translate.py
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import codecs
-import csv
import fnmatch
import inspect
import io
@@ -121,18 +120,17 @@ _LOCALE2WIN32 = {
ENGLISH_SMALL_WORDS = set("as at by do go if in me no of ok on or to up us we".split())
+# these direct uses of CSV are ok.
+import csv # pylint: disable=deprecated-module
class UNIX_LINE_TERMINATOR(csv.excel):
lineterminator = '\n'
csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
-#
-# Helper functions for translating fields
-#
+# FIXME: holy shit this whole thing needs to be cleaned up hard it's a mess
def encode(s):
- if isinstance(s, unicode):
- return s.encode('utf8')
+ assert isinstance(s, pycompat.text_type)
return s
# which elements are translated inline
@@ -260,7 +258,7 @@ def translate_xml_node(node, callback, parse, serialize):
append_content(result, translate_content(todo) if todo_has_text else todo)
# translate the required attributes
- for name, value in pycompat.items(result.attrib):
+ for name, value in result.attrib.items():
if name in TRANSLATED_ATTRS:
result.set(name, translate_text(value) or value)
@@ -280,18 +278,18 @@ def translate_xml_node(node, callback, parse, serialize):
def parse_xml(text):
- return etree.fromstring(encode(text))
+ return etree.fromstring(text)
def serialize_xml(node):
- return etree.tostring(node, method='xml', encoding='utf8').decode('utf8')
+ return etree.tostring(node, method='xml', encoding='unicode')
_HTML_PARSER = etree.HTMLParser(encoding='utf8')
def parse_html(text):
- return html.fragment_fromstring(encode(text), parser=_HTML_PARSER)
+ return html.fragment_fromstring(text, parser=_HTML_PARSER)
def serialize_html(node):
- return etree.tostring(node, method='html', encoding='utf8').decode('utf8')
+ return etree.tostring(node, method='html', encoding='unicode')
def xml_translate(callback, value):
@@ -307,7 +305,7 @@ def xml_translate(callback, value):
return serialize_xml(result)
except etree.ParseError:
# fallback for translated terms: use an HTML parser and wrap the term
- root = parse_html("<div>%s</div>" % value)
+ root = parse_html(u"<div>%s</div>" % value)
result = translate_xml_node(root, callback, parse_xml, serialize_xml)
# remove tags <div> and </div> from result
return serialize_xml(result)[5:-6]
@@ -643,14 +641,13 @@ class PoFile(object):
# only strings in python code are python formated
self.buffer.write(u"#, python-format\n")
- if not isinstance(trad, unicode):
- trad = unicode(trad, 'utf8')
- if not isinstance(source, unicode):
- source = unicode(source, 'utf8')
-
- msg = u"msgid %s\n" \
- u"msgstr %s\n\n" \
- % (quote(source), quote(trad))
+ msg = (
+ u"msgid %s\n"
+ u"msgstr %s\n\n"
+ ) % (
+ quote(pycompat.text_type(source)),
+ quote(pycompat.text_type(trad))
+ )
self.buffer.write(msg)
@@ -660,7 +657,7 @@ def trans_export(lang, modules, buffer, format, cr):
def _process(format, modules, rows, buffer, lang):
if format == 'csv':
- writer = csv.writer(buffer, 'UNIX')
+ writer = pycompat.csv_writer(buffer, dialect='UNIX')
# write header first
writer.writerow(("module","type","name","res_id","src","value","comments"))
for module, type, name, res_id, src, trad, comments in rows:
@@ -681,7 +678,7 @@ def trans_export(lang, modules, buffer, format, cr):
row.setdefault('tnrs', []).append((type, name, res_id))
row.setdefault('comments', set()).update(comments)
- for src, row in sorted(pycompat.items(grouped_rows)):
+ for src, row in sorted(grouped_rows.items()):
if not lang:
# translation template, so no translation value
row['translation'] = ''
@@ -695,7 +692,7 @@ def trans_export(lang, modules, buffer, format, cr):
module = row[0]
rows_by_module.setdefault(module, []).append(row)
tmpdir = tempfile.mkdtemp()
- for mod, modrows in pycompat.items(rows_by_module):
+ for mod, modrows in rows_by_module.items():
tmpmoddir = join(tmpdir, mod, 'i18n')
os.makedirs(tmpmoddir)
pofilename = (lang if lang else mod) + ".po" + ('t' if not lang else '')
@@ -810,9 +807,9 @@ def trans_generate(lang, modules, cr):
try:
# verify the minimal size without eventual xml tags
# wrap to make sure html content like '<a>b</a><c>d</c>' is accepted by lxml
- wrapped = "<div>%s</div>" % sanitized_term
+ wrapped = u"<div>%s</div>" % sanitized_term
node = etree.fromstring(wrapped)
- sanitized_term = etree.tostring(node, encoding='UTF-8', method='text')
+ sanitized_term = etree.tostring(node, encoding='unicode', method='text')
except etree.ParseError:
pass
# remove non-alphanumeric chars
@@ -847,12 +844,10 @@ def trans_generate(lang, modules, cr):
cr.execute(query, query_param)
for (xml_name, model, res_id, module) in cr.fetchall():
- module = encode(module)
- model = encode(model)
- xml_name = "%s.%s" % (module, encode(xml_name))
+ xml_name = "%s.%s" % (module, xml_name)
if model not in env:
- _logger.error("Unable to find object %r", model)
+ _logger.error(u"Unable to find object %r", model)
continue
record = env[model].browse(res_id)
@@ -861,14 +856,14 @@ def trans_generate(lang, modules, cr):
continue
if not record.exists():
- _logger.warning("Unable to find object %r with id %d", model, res_id)
+ _logger.warning(u"Unable to find object %r with id %d", model, res_id)
continue
- if model=='ir.model.fields':
+ if model==u'ir.model.fields':
try:
- field_name = encode(record.name)
+ field_name = record.name
except AttributeError as exc:
- _logger.error("name error in %s: %s", xml_name, str(exc))
+ _logger.error(u"name error in %s: %s", xml_name, str(exc))
continue
field_model = env.get(record.model)
if (field_model is None or not field_model._translate or
@@ -877,11 +872,11 @@ def trans_generate(lang, modules, cr):
field = field_model._fields[field_name]
if isinstance(getattr(field, 'selection', None), (list, tuple)):
- name = "%s,%s" % (encode(record.model), field_name)
+ name = "%s,%s" % (record.model, field_name)
for dummy, val in field.selection:
- push_translation(module, 'selection', name, 0, encode(val))
+ push_translation(module, 'selection', name, 0, val)
- for field_name, field in record._fields.iteritems():
+ for field_name, field in record._fields.items():
if field.translate:
name = model + "," + field_name
try:
@@ -889,13 +884,13 @@ def trans_generate(lang, modules, cr):
except Exception:
continue
for term in set(field.get_trans_terms(value)):
- push_translation(module, 'model', name, xml_name, encode(term))
+ push_translation(module, 'model', name, xml_name, term)
# End of data for ir.model.data query results
def push_constraint_msg(module, term_type, model, msg):
if not callable(msg):
- push_translation(encode(module), term_type, encode(model), 0, encode(msg))
+ push_translation(encode(module), term_type, encode(model), 0, msg)
def push_local_constraints(module, model, cons_type='sql_constraints'):
""" Climb up the class hierarchy and ignore inherited constraints from other modules. """
@@ -1005,7 +1000,7 @@ def trans_generate(lang, modules, cr):
def trans_load(cr, filename, lang, verbose=True, module_name=None, context=None):
try:
- with file_open(filename) as fileobj:
+ with file_open(filename, mode='rb') as fileobj:
_logger.info("loading %s", filename)
fileformat = os.path.splitext(filename)[-1][1:].lower()
result = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, module_name=module_name, context=context)
@@ -1038,11 +1033,9 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True,
# now, the serious things: we read the language file
fileobj.seek(0)
if fileformat == 'csv':
- reader = csv.reader(fileobj, quotechar='"', delimiter=',')
+ reader = pycompat.csv_reader(fileobj, quotechar='"', delimiter=',')
# read the first line of the file (it contains columns titles)
- for row in reader:
- fields = row
- break
+ fields = next(reader)
elif fileformat == 'po':
reader = PoFile(fileobj)
@@ -1060,7 +1053,7 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True,
addons_module, i18n_dir = os.path.split(addons_module_i18n)
addons, module = os.path.split(addons_module)
pot_handle = file_open(os.path.join(
- addons, module, i18n_dir, module + '.pot'))
+ addons, module, i18n_dir, module + '.pot'), mode='rb')
pot_reader = PoFile(pot_handle)
except:
pass
@@ -1109,7 +1102,7 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True,
return
if isinstance(res_id, pycompat.integer_types) or \
- (isinstance(res_id, basestring) and res_id.isdigit()):
+ (isinstance(res_id, pycompat.string_types) and res_id.isdigit()):
dic['res_id'] = int(res_id)
if module_name:
dic['module'] = module_name
@@ -1132,7 +1125,7 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True,
# Then process the entries implied by the POT file (which is more
# correct w.r.t. the targets) if some of them remain.
pot_rows = []
- for src, target in pycompat.items(pot_targets):
+ for src, target in pot_targets.items():
if target.value:
for type, name, res_id in target.targets:
pot_rows.append((type, name, res_id, src, target.value, target.comments))
diff --git a/odoo/tools/yaml_import.py b/odoo/tools/yaml_import.py
index 4cb2e7ea184..79f997045a2 100644
--- a/odoo/tools/yaml_import.py
+++ b/odoo/tools/yaml_import.py
@@ -12,8 +12,7 @@ from lxml import etree
import yaml
import odoo
-from . import assertion_report
-from . import yaml_tag, pycompat
+from . import assertion_report, pycompat, yaml_tag
from .config import config
from .misc import file_open, DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
from odoo import SUPERUSER_ID
@@ -25,9 +24,6 @@ from .safe_eval import safe_eval
_logger = logging.getLogger(__name__)
-def encode(s):
- return s.encode('utf8') if isinstance(s, unicode) else s
-
class YamlImportException(Exception):
pass
@@ -41,7 +37,7 @@ def _is_yaml_mapping(node, tag_constructor):
return value
def is_comment(node):
- return isinstance(node, basestring)
+ return isinstance(node, pycompat.string_types)
def is_assert(node):
return isinstance(node, yaml_tag.Assert) \
@@ -87,7 +83,7 @@ def is_ir_set(node):
return _is_yaml_mapping(node, yaml_tag.IrSet)
def is_string(node):
- return isinstance(node, basestring)
+ return isinstance(node, pycompat.string_types)
class RecordDictWrapper(dict):
"""
@@ -202,7 +198,7 @@ class YamlInterpreter(object):
def process_assert(self, node):
if isinstance(node, dict):
- assertion, expressions = next(pycompat.items(node))
+ assertion, expressions = list(node.items())[0]
else:
assertion, expressions = node, []
@@ -275,10 +271,10 @@ class YamlInterpreter(object):
return record_dict
def process_record(self, node):
- record, fields = next(pycompat.items(node))
+ record, fields = list(node.items())[0]
model = self.env[record.model]
view_id = record.view
- if view_id and (view_id is not True) and isinstance(view_id, basestring):
+ if view_id and (view_id is not True) and isinstance(view_id, pycompat.string_types):
if '.' not in view_id:
view_id = self.module + '.' + view_id
view_id = self.env.ref(view_id).id
@@ -358,7 +354,10 @@ class YamlInterpreter(object):
traverse(child, elems)
elems = OrderedDict()
- traverse(etree.fromstring(encode(view['arch'])), elems)
+ arch = view['arch']
+ if isinstance(arch, pycompat.text_type):
+ arch = arch.encode('utf-8')
+ traverse(etree.fromstring(arch), elems)
return elems
def is_readonly(field_elem):
@@ -377,7 +376,7 @@ class YamlInterpreter(object):
def process_vals(fg, vals):
""" sanitize the given field values """
result = {}
- for field_name, field_value in pycompat.items(vals):
+ for field_name, field_value in vals.items():
if field_name not in fg:
continue
if fg[field_name]['type'] == 'many2one' and isinstance(field_value, (tuple, list)):
@@ -398,7 +397,7 @@ class YamlInterpreter(object):
def post_process(fg, elems, vals):
""" filter out readonly fields from vals """
result = {}
- for field_name, field_value in pycompat.items(vals):
+ for field_name, field_value in vals.items():
if is_readonly(elems[field_name]):
continue
if fg[field_name]['type'] in ('one2many', 'many2many'):
@@ -444,7 +443,7 @@ class YamlInterpreter(object):
record_dict.update(process_vals(fg, result.get('value', {})))
# fill in fields, and execute onchange where necessary
- for field_name, field_elem in pycompat.items(elems):
+ for field_name, field_elem in elems.items():
assert field_name in fg, "The field '%s' is defined in the form view but not on the object '%s'!" % (field_name, model._name)
if is_readonly(field_elem):
# skip readonly fields
@@ -474,7 +473,7 @@ class YamlInterpreter(object):
result = recs.onchange(dict(record_dict, **parent_values), field_name, onchange_spec)
record_dict.update(process_vals(fg, {
key: val
- for key, val in pycompat.items(result.get('value', {}))
+ for key, val in result.get('value', {}).items()
if key not in fields # do not shadow values explicitly set in yaml
}))
@@ -483,7 +482,7 @@ class YamlInterpreter(object):
else:
record_dict = {}
- for field_name, expression in pycompat.items(fields):
+ for field_name, expression in fields.items():
if record_dict.get(field_name):
continue
field_value = self._eval_field(model, field_name, expression, parent=record_dict, default=False, context=context)
@@ -494,7 +493,7 @@ class YamlInterpreter(object):
# should not be sent to create. This bug appears with not stored function fields in the new API.
return {
key: val
- for key, val in pycompat.items(record_dict)
+ for key, val in record_dict.items()
for field in [model._fields[key].base_field]
if field.store or field.inverse
}
@@ -580,11 +579,11 @@ class YamlInterpreter(object):
self.sudo_env = self.env(user=SUPERUSER_ID)
def process_python(self, node):
- python, statements = next(pycompat.items(node))
+ python, statements = list(node.items())[0]
assert python.model or python.id, "!python node must have attribute `model` or `id`"
if python.id is None:
record = self.env[python.model]
- elif isinstance(python.id, basestring):
+ elif isinstance(python.id, pycompat.string_types):
record = self.get_record(python.id)
else:
record = self.env[python.model].browse(python.id)
@@ -640,7 +639,7 @@ class YamlInterpreter(object):
return args
def process_function(self, node):
- function, params = next(pycompat.items(node))
+ function, params = list(node.items())[0]
if self.isnoupdate(function) and self.mode != 'init':
return
model = self.env[function.model]
@@ -770,9 +769,9 @@ class YamlInterpreter(object):
def process_ir_set(self, node):
if not self.mode == 'init':
return False
- _, fields = next(pycompat.items(node))
+ _, fields = list(node.items())[0]
res = {}
- for fieldname, expression in pycompat.items(fields):
+ for fieldname, expression in fields.items():
if is_eval(expression):
value = safe_eval(expression.expression, self.eval_context)
else:
@@ -878,7 +877,7 @@ class YamlInterpreter(object):
elif not is_preceded_by_comment:
if isinstance(node, dict):
msg = "Creating %s\n with %s"
- args = next(pycompat.items(node))
+ args = list(node.items())[0]
self._log(msg, *args)
else:
self._log(node)
diff --git a/odoo/tools/yaml_tag.py b/odoo/tools/yaml_tag.py
index 2b7141b6693..9f57111bf37 100644
--- a/odoo/tools/yaml_tag.py
+++ b/odoo/tools/yaml_tag.py
@@ -1,8 +1,6 @@
import yaml
import logging
-from . import pycompat
-
class YamlTag(object):
"""
@@ -16,7 +14,7 @@ class YamlTag(object):
def __getattr__(self, attr):
return None
def __repr__(self):
- return "<%s %s>" % (self.__class__.__name__, sorted(pycompat.items(self.__dict__)))
+ return "<%s %s>" % (self.__class__.__name__, sorted(self.__dict__.items()))
class Assert(YamlTag):
def __init__(self, model, id=None, severity=logging.WARNING, string="NONAME", **kwargs):
diff --git a/requirements.txt b/requirements.txt
index 65a3a1640f0..1ef8ebd2252 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,7 +5,6 @@ ebaysdk==2.1.4
feedparser==5.2.1
gevent==1.1.2
greenlet==0.4.10
-jcconv==0.2.3
html2text==2016.9.19
Jinja2==2.8
lxml==3.5.0
@@ -37,7 +36,6 @@ suds-jurko==0.6
vatnumber==1.2
vobject==0.9.3
Werkzeug==0.11.11
-wsgiref==0.1.2
XlsxWriter==0.9.3
xlwt==1.1.2
xlrd==1.0.0
diff --git a/setup/package.py b/setup/package.py
index d7c712aba7e..c3eedeee3e7 100755
--- a/setup/package.py
+++ b/setup/package.py
@@ -133,7 +133,7 @@ class OdooDocker(object):
def end(self):
try:
_rpc_count_modules(port=str(self.port))
- except Exception, e:
+ except Exception as e:
print('Exception during docker execution: %s:' % str(e))
print('Error during docker execution: printing the bash output:')
with open(self.log_file.name) as f:
@@ -152,7 +152,7 @@ def docker(docker_image, build_dir, pub_dir):
_docker.start(docker_image, build_dir, pub_dir)
try:
yield _docker
- except Exception, e:
+ except Exception:
raise
finally:
_docker.end()
@@ -439,7 +439,7 @@ def main():
if not o.no_testing:
test_tgz(o)
published_files = publish(o, 'tarball', ['tar.gz', 'zip'])
- except Exception, e:
+ except Exception as e:
print("Won't publish the tgz release.\n Exception: %s" % str(e))
if not o.no_debian:
build_deb(o)
@@ -448,7 +448,7 @@ def main():
test_deb(o)
published_files = publish(o, 'debian', ['deb', 'dsc', 'changes', 'tar.gz'])
gen_deb_package(o, published_files)
- except Exception, e:
+ except Exception as e:
print("Won't publish the deb release.\n Exception: %s" % str(e))
if not o.no_rpm:
build_rpm(o)
@@ -457,7 +457,7 @@ def main():
test_rpm(o)
published_files = publish(o, 'redhat', ['noarch.rpm'])
gen_rpm_repo(o, published_files[0])
- except Exception, e:
+ except Exception as e:
print("Won't publish the rpm release.\n Exception: %s" % str(e))
if not o.no_windows:
_prepare_build_dir(o, win32=True)
@@ -466,7 +466,7 @@ def main():
if not o.no_testing:
test_exe(o)
published_files = publish(o, 'windows', ['exe'])
- except Exception, e:
+ except Exception as e:
print("Won't publish the exe release.\n Exception: %s" % str(e))
except:
pass