diff --git a/odoo/addons/base/tests/test_tz.py b/odoo/addons/base/tests/test_tz.py index 741a3575ea2..be3ca5ce011 100644 --- a/odoo/addons/base/tests/test_tz.py +++ b/odoo/addons/base/tests/test_tz.py @@ -1,33 +1,56 @@ +import datetime import pytz +from unittest.mock import patch from odoo.tests.common import TransactionCase +from odoo.tools._monkeypatches_pytz import _tz_mapping class TestTZ(TransactionCase): def test_tz_legacy(self): - #See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + d = datetime.datetime(1969, 7, 16) + # See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones def assertTZEqual(tz1, tz2): - self.assertEqual(tz1._utcoffset, tz2._utcoffset) - self.assertEqual(tz1._transition_info, tz2._transition_info) + self.assertEqual(tz1.localize(d).strftime('%z'), tz2.localize(d).strftime('%z')) - assertTZEqual(pytz.timezone('US/Eastern'), pytz.timezone('America/New_York')) - assertTZEqual(pytz.timezone('US/Central'), pytz.timezone('America/Chicago')) - assertTZEqual(pytz.timezone('US/Mountain'), pytz.timezone('America/Denver')) - assertTZEqual(pytz.timezone('US/Pacific'), pytz.timezone('America/Los_Angeles')) - assertTZEqual(pytz.timezone('US/Alaska'), pytz.timezone('America/Anchorage')) - assertTZEqual(pytz.timezone('US/Hawaii'), pytz.timezone('Pacific/Honolulu')) - assertTZEqual(pytz.timezone('Canada/Atlantic'), pytz.timezone('America/Halifax')) - assertTZEqual(pytz.timezone('Canada/Pacific'), pytz.timezone('America/Vancouver')) - assertTZEqual(pytz.timezone('Mexico/BajaNorte'), pytz.timezone('America/Tijuana')) - assertTZEqual(pytz.timezone('Mexico/General'), pytz.timezone('America/Mexico_City')) - assertTZEqual(pytz.timezone('Brazil/East'), pytz.timezone('America/Sao_Paulo')) - # This one is not correct for a strange reason - #assertTZEqual(pytz.timezone('Pacific/Midway'), pytz.timezone('Pacific/Pago_Pago')) + # in some version of tzdata the timezones are not symlink, as an example in 2023c-0ubuntu0.20.04.1 + # this as a side effect to have sligh difference in timezones seconds, breaking the following assertions + # in some cases: + # + # self.assertEqual(tz1._utcoffset, tz2._utcoffset) + # if hasattr(tz2, '_transition_info'): + # self.assertEqual(tz1._transition_info, tz2._transition_info) + # + # the first one is more robust + + for source, target in _tz_mapping.items(): + with self.subTest(source=source, target=target): + if source == 'Pacific/Enderbury': # this one was wrong in some version of tzdata + continue + assertTZEqual(pytz.timezone(source), pytz.timezone(target)) + + def test_dont_adapt_available_tz(self): + with patch.dict(_tz_mapping, { + 'DeprecatedUtc': 'UTC', + 'America/New_York': 'UTC', + }): + self.assertNotIn('DeprecatedUtc', pytz.all_timezones_set, 'DeprecatedUtc is not available') + self.assertEqual(pytz.timezone('DeprecatedUtc'), pytz.timezone('UTC'), 'DeprecatedUtc does not exist and should have been replaced with UTC') + self.assertIn('America/New_York', pytz.all_timezones_set, 'America/New_York is available') + self.assertNotEqual(pytz.timezone('America/New_York'), pytz.timezone('UTC'), 'America/New_York exists and should not have been replaced with UTC') def test_cannot_set_deprecated_timezone(self): # this should be ok self.env.user.tz = "America/New_York" if "US/Eastern" not in pytz.all_timezones: with self.assertRaises(ValueError): - self.env.user.tz = "US/Eastern" \ No newline at end of file + self.env.user.tz = "US/Eastern" + + def test_partner_with_old_tz(self): + # this test makes sence after ubuntu noble without tzdata-legacy installed + partner = self.env['res.partner'].create({'name': 'test', 'tz': 'UTC'}) + self.env.cr.execute("""UPDATE res_partner set tz='US/Eastern' WHERE id=%s""", (partner.id,)) + partner.invalidate_recordset() + self.assertEqual(partner.tz, 'US/Eastern') # tz was update despite selection not existing, but data was not migrated + self.assertEqual(partner.tz_offset, '-0400', "We don't expect pytz.timezone to fail if the timezone diseapeared when chaging os version") diff --git a/odoo/tools/__init__.py b/odoo/tools/__init__.py index 7150927960d..e1bf6c8da74 100644 --- a/odoo/tools/__init__.py +++ b/odoo/tools/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import _monkeypatches +from . import _monkeypatches_pytz from werkzeug import urls if not hasattr(urls, 'url_join'): diff --git a/odoo/tools/_monkeypatches_pytz.py b/odoo/tools/_monkeypatches_pytz.py new file mode 100644 index 00000000000..b1414dc5227 --- /dev/null +++ b/odoo/tools/_monkeypatches_pytz.py @@ -0,0 +1,132 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. +""" +In ubuntu noble, some timezone where removed leading to errors when trying to assign/access them. + +This was partially fixed in the code by removing all references to old timezones but one issue remains: +if a database contains timezones that are not defined in the os, the resolution will fail and break +at runtime. + +This patches proposes to alter timezone to fallback on the new canonical timezone if the timezone was removed. + +This list was generated by checking all symlink in /usr/share/zoneinfo in ubuntu 22.04 that disapeared in ubuntu 24.04 + +This solutions will work when moving a database from one server to another, even without migration. +This list could be improved for other purposes. + +""" + +import pytz + +_tz_mapping = { + "Africa/Asmera": "Africa/Nairobi", + "America/Argentina/ComodRivadavia": "America/Argentina/Catamarca", + "America/Buenos_Aires": "America/Argentina/Buenos_Aires", + "America/Cordoba": "America/Argentina/Cordoba", + "America/Fort_Wayne": "America/Indiana/Indianapolis", + "America/Indianapolis": "America/Indiana/Indianapolis", + "America/Jujuy": "America/Argentina/Jujuy", + "America/Knox_IN": "America/Indiana/Knox", + "America/Louisville": "America/Kentucky/Louisville", + "America/Mendoza": "America/Argentina/Mendoza", + "America/Rosario": "America/Argentina/Cordoba", + "Antarctica/South_Pole": "Pacific/Auckland", + "Asia/Ashkhabad": "Asia/Ashgabat", + "Asia/Calcutta": "Asia/Kolkata", + "Asia/Chungking": "Asia/Shanghai", + "Asia/Dacca": "Asia/Dhaka", + "Asia/Katmandu": "Asia/Kathmandu", + "Asia/Macao": "Asia/Macau", + "Asia/Rangoon": "Asia/Yangon", + "Asia/Saigon": "Asia/Ho_Chi_Minh", + "Asia/Thimbu": "Asia/Thimphu", + "Asia/Ujung_Pandang": "Asia/Makassar", + "Asia/Ulan_Bator": "Asia/Ulaanbaatar", + "Atlantic/Faeroe": "Atlantic/Faroe", + "Australia/ACT": "Australia/Sydney", + "Australia/LHI": "Australia/Lord_Howe", + "Australia/North": "Australia/Darwin", + "Australia/NSW": "Australia/Sydney", + "Australia/Queensland": "Australia/Brisbane", + "Australia/South": "Australia/Adelaide", + "Australia/Tasmania": "Australia/Hobart", + "Australia/Victoria": "Australia/Melbourne", + "Australia/West": "Australia/Perth", + "Brazil/Acre": "America/Rio_Branco", + "Brazil/DeNoronha": "America/Noronha", + "Brazil/East": "America/Sao_Paulo", + "Brazil/West": "America/Manaus", + "Canada/Atlantic": "America/Halifax", + "Canada/Central": "America/Winnipeg", + "Canada/Eastern": "America/Toronto", + "Canada/Mountain": "America/Edmonton", + "Canada/Newfoundland": "America/St_Johns", + "Canada/Pacific": "America/Vancouver", + "Canada/Saskatchewan": "America/Regina", + "Canada/Yukon": "America/Whitehorse", + "Chile/Continental": "America/Santiago", + "Chile/EasterIsland": "Pacific/Easter", + "Cuba": "America/Havana", + "Egypt": "Africa/Cairo", + "Eire": "Europe/Dublin", + "Europe/Kiev": "Europe/Kyiv", + "Europe/Uzhgorod": "Europe/Kyiv", + "Europe/Zaporozhye": "Europe/Kyiv", + "GB": "Europe/London", + "GB-Eire": "Europe/London", + "GMT+0": "Etc/GMT", + "GMT-0": "Etc/GMT", + "GMT0": "Etc/GMT", + "Greenwich": "Etc/GMT", + "Hongkong": "Asia/Hong_Kong", + "Iceland": "Africa/Abidjan", + "Iran": "Asia/Tehran", + "Israel": "Asia/Jerusalem", + "Jamaica": "America/Jamaica", + "Japan": "Asia/Tokyo", + "Kwajalein": "Pacific/Kwajalein", + "Libya": "Africa/Tripoli", + "Mexico/BajaNorte": "America/Tijuana", + "Mexico/BajaSur": "America/Mazatlan", + "Mexico/General": "America/Mexico_City", + "Navajo": "America/Denver", + "NZ": "Pacific/Auckland", + "NZ-CHAT": "Pacific/Chatham", + "Pacific/Enderbury": "Pacific/Kanton", + "Pacific/Ponape": "Pacific/Guadalcanal", + "Pacific/Truk": "Pacific/Port_Moresby", + "Poland": "Europe/Warsaw", + "Portugal": "Europe/Lisbon", + "PRC": "Asia/Shanghai", + "ROC": "Asia/Taipei", + "ROK": "Asia/Seoul", + "Singapore": "Asia/Singapore", + "Turkey": "Europe/Istanbul", + "UCT": "Etc/UTC", + "Universal": "Etc/UTC", + "US/Alaska": "America/Anchorage", + "US/Aleutian": "America/Adak", + "US/Arizona": "America/Phoenix", + "US/Central": "America/Chicago", + "US/Eastern": "America/New_York", + "US/East-Indiana": "America/Indiana/Indianapolis", + "US/Hawaii": "Pacific/Honolulu", + "US/Indiana-Starke": "America/Indiana/Knox", + "US/Michigan": "America/Detroit", + "US/Mountain": "America/Denver", + "US/Pacific": "America/Los_Angeles", + "US/Samoa": "Pacific/Pago_Pago", + "W-SU": "Europe/Moscow", + "Zulu": "Etc/UTC", +} + + +original_pytz_timezone = pytz.timezone + + +def timezone(name): + if name not in pytz.all_timezones_set and name in _tz_mapping: + name = _tz_mapping[name] + return original_pytz_timezone(name) + + +pytz.timezone = timezone diff --git a/odoo/tools/safe_eval.py b/odoo/tools/safe_eval.py index aae06ee4b66..72590f73d1a 100644 --- a/odoo/tools/safe_eval.py +++ b/odoo/tools/safe_eval.py @@ -65,6 +65,7 @@ _UNSAFE_ATTRIBUTES = { 'cr_await', 'cr_code', 'cr_frame', # Coroutine generators 'ag_await', 'ag_code', 'ag_frame', + }