From 811d606d2e366598bd4a9843195123cbf1f54f69 Mon Sep 17 00:00:00 2001 From: hparfr Date: Thu, 26 Apr 2018 14:18:12 +0200 Subject: [PATCH 01/14] Add cron daylight saving time resistant --- .../README.rst | 83 +++++++++++++++++++ .../__init__.py | 1 + .../__openerp__.py | 19 +++++ .../models/__init__.py | 1 + .../models/ir_cron.py | 56 +++++++++++++ .../views/cron.xml | 13 +++ 6 files changed, 173 insertions(+) create mode 100644 cron_daylight_saving_time_resistant/README.rst create mode 100644 cron_daylight_saving_time_resistant/__init__.py create mode 100644 cron_daylight_saving_time_resistant/__openerp__.py create mode 100644 cron_daylight_saving_time_resistant/models/__init__.py create mode 100644 cron_daylight_saving_time_resistant/models/ir_cron.py create mode 100644 cron_daylight_saving_time_resistant/views/cron.xml diff --git a/cron_daylight_saving_time_resistant/README.rst b/cron_daylight_saving_time_resistant/README.rst new file mode 100644 index 00000000000..6ccf4be857c --- /dev/null +++ b/cron_daylight_saving_time_resistant/README.rst @@ -0,0 +1,83 @@ +.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: https://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +=================================== +Cron daylight saving time resistant +=================================== + +This module adjust cron to run at fixed hours, local time. + + +Without this module, when a daylight saving time change occur, the cron will not take +the hour change in account. + +With this module, when a daylight saving time change occur, the offset (+1 or -1 hour) +will be applied. + + +Usage +===== + +To use this module, you need to edit a cron, and check the option, +"Daylight saving time resistant". + +#. Go to ... + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/149/10.0 + + +Known issues / Roadmap +====================== + +* Write tests +* Edge cases like run every 5 minutes + dst resistant may behave +incorrectly during the time change. + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smash it by providing detailed and welcomed feedback. + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Raphaël Reverdy https://akretion.com + +Do not contact contributors directly about support or help with technical issues. + +Funders +------- + +The development of this module has been financially supported by: + +* Akretion + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/cron_daylight_saving_time_resistant/__init__.py b/cron_daylight_saving_time_resistant/__init__.py new file mode 100644 index 00000000000..9a7e03eded3 --- /dev/null +++ b/cron_daylight_saving_time_resistant/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/cron_daylight_saving_time_resistant/__openerp__.py b/cron_daylight_saving_time_resistant/__openerp__.py new file mode 100644 index 00000000000..331eb94227a --- /dev/null +++ b/cron_daylight_saving_time_resistant/__openerp__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Copyright <2018> +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Cron daylight saving time resistant", + "summary": "Run cron on fixed hours", + "version": "9.0.1.0.0", + "category": "Tools", + "website": "https://github.com/OCA/server-tools", + "author": "akretion, Odoo Community Association (OCA)", + "license": "AGPL-3", + "installable": True, + "depends": [ + "base", + ], + "data": [ + "views/cron.xml", + ], +} diff --git a/cron_daylight_saving_time_resistant/models/__init__.py b/cron_daylight_saving_time_resistant/models/__init__.py new file mode 100644 index 00000000000..cd483538a20 --- /dev/null +++ b/cron_daylight_saving_time_resistant/models/__init__.py @@ -0,0 +1 @@ +from . import ir_cron \ No newline at end of file diff --git a/cron_daylight_saving_time_resistant/models/ir_cron.py b/cron_daylight_saving_time_resistant/models/ir_cron.py new file mode 100644 index 00000000000..fb538f5c586 --- /dev/null +++ b/cron_daylight_saving_time_resistant/models/ir_cron.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# © 2018 Akretion - Raphaël Reverdy +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging +import pytz +from datetime import datetime + +from openerp import api, fields, models +from openerp.osv import fields as osv_fields +from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT +from openerp.addons.base.ir.ir_cron import _intervalTypes +_logger = logging.getLogger(__name__) + + +class IrCron(models.Model): + _inherit = "ir.cron" + + daylight_saving_time_resistant = fields.Boolean( + help="Adjust interval to run at the same hour after and before" + "daylight saving time change. It's used twice a year") + + def _process_job(cls, job_cr, job, cron_cr): + """Add or remove the Daylight saving offset when needed.""" + with api.Environment.manage(): + now = osv_fields.datetime.context_timestamp( + job_cr, job['user_id'], datetime.now()) + nextcall = osv_fields.datetime.context_timestamp( + job_cr, job['user_id'], + datetime.strptime( + job['nextcall'], + DEFAULT_SERVER_DATETIME_FORMAT) + ) + delta = _intervalTypes[job['interval_type']]( + job['interval_number']) + + if (nextcall + delta) < now: + before_offset = (nextcall + delta).utcoffset() + after_offset = now.utcoffset() + elif nextcall < now: + before_offset = nextcall.utcoffset() + after_offset = (nextcall + delta).utcoffset() + else: + before_offset = 0 + after_offset = 0 + + diff_offset = after_offset - before_offset + + if diff_offset and job['daylight_saving_time_resistant']: + numbercall = job['numbercall'] + if nextcall < now and numbercall: + nextcall -= diff_offset + modified_next_call = fields.Datetime.to_string( + nextcall.astimezone(pytz.UTC)) + job['nextcall'] = modified_next_call + super(IrCron, cls)._process_job(job_cr, job, cron_cr) diff --git a/cron_daylight_saving_time_resistant/views/cron.xml b/cron_daylight_saving_time_resistant/views/cron.xml new file mode 100644 index 00000000000..3e4c802df78 --- /dev/null +++ b/cron_daylight_saving_time_resistant/views/cron.xml @@ -0,0 +1,13 @@ + + + + ir.cron.resist_dst + ir.cron + + + + + + + + From ee2511fce4d75339c96c75b661d5968d37d78cd4 Mon Sep 17 00:00:00 2001 From: hparfr Date: Thu, 26 Apr 2018 14:23:06 +0200 Subject: [PATCH 02/14] Add newlines at end of files --- cron_daylight_saving_time_resistant/README.rst | 2 +- cron_daylight_saving_time_resistant/__init__.py | 2 +- cron_daylight_saving_time_resistant/models/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cron_daylight_saving_time_resistant/README.rst b/cron_daylight_saving_time_resistant/README.rst index 6ccf4be857c..d38395d55f5 100644 --- a/cron_daylight_saving_time_resistant/README.rst +++ b/cron_daylight_saving_time_resistant/README.rst @@ -26,7 +26,7 @@ To use this module, you need to edit a cron, and check the option, .. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/149/10.0 + :target: https://runbot.odoo-community.org/runbot/149/9.0 Known issues / Roadmap diff --git a/cron_daylight_saving_time_resistant/__init__.py b/cron_daylight_saving_time_resistant/__init__.py index 9a7e03eded3..0650744f6bc 100644 --- a/cron_daylight_saving_time_resistant/__init__.py +++ b/cron_daylight_saving_time_resistant/__init__.py @@ -1 +1 @@ -from . import models \ No newline at end of file +from . import models diff --git a/cron_daylight_saving_time_resistant/models/__init__.py b/cron_daylight_saving_time_resistant/models/__init__.py index cd483538a20..9115592626a 100644 --- a/cron_daylight_saving_time_resistant/models/__init__.py +++ b/cron_daylight_saving_time_resistant/models/__init__.py @@ -1 +1 @@ -from . import ir_cron \ No newline at end of file +from . import ir_cron From e7dc0f8622ec705520a0ca082bc7a41afb1dc6af Mon Sep 17 00:00:00 2001 From: hparfr Date: Mon, 7 May 2018 10:04:49 +0200 Subject: [PATCH 03/14] Fix issue when now+delta is in future --- .../models/ir_cron.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/cron_daylight_saving_time_resistant/models/ir_cron.py b/cron_daylight_saving_time_resistant/models/ir_cron.py index fb538f5c586..d16c051fba7 100644 --- a/cron_daylight_saving_time_resistant/models/ir_cron.py +++ b/cron_daylight_saving_time_resistant/models/ir_cron.py @@ -34,20 +34,20 @@ def _process_job(cls, job_cr, job, cron_cr): delta = _intervalTypes[job['interval_type']]( job['interval_number']) - if (nextcall + delta) < now: - before_offset = (nextcall + delta).utcoffset() - after_offset = now.utcoffset() - elif nextcall < now: - before_offset = nextcall.utcoffset() - after_offset = (nextcall + delta).utcoffset() - else: - before_offset = 0 - after_offset = 0 + numbercall = job['numbercall'] + future_call = nextcall + while future_call < now and numbercall: + if numbercall > 0: + numbercall -= 1 + if numbercall: + future_call += delta + + after_offset = future_call.utcoffset() + before_offset = nextcall.utcoffset() diff_offset = after_offset - before_offset if diff_offset and job['daylight_saving_time_resistant']: - numbercall = job['numbercall'] if nextcall < now and numbercall: nextcall -= diff_offset modified_next_call = fields.Datetime.to_string( From e2f2efce2c954e060ddf9993d8e97769d9218354 Mon Sep 17 00:00:00 2001 From: hparfr Date: Mon, 7 May 2018 16:52:10 +0200 Subject: [PATCH 04/14] Add tests --- .../models/ir_cron.py | 32 +++++--- .../tests/__init__.py | 1 + .../tests/test_dst.py | 81 +++++++++++++++++++ 3 files changed, 101 insertions(+), 13 deletions(-) create mode 100644 cron_daylight_saving_time_resistant/tests/__init__.py create mode 100644 cron_daylight_saving_time_resistant/tests/test_dst.py diff --git a/cron_daylight_saving_time_resistant/models/ir_cron.py b/cron_daylight_saving_time_resistant/models/ir_cron.py index d16c051fba7..c6a69964013 100644 --- a/cron_daylight_saving_time_resistant/models/ir_cron.py +++ b/cron_daylight_saving_time_resistant/models/ir_cron.py @@ -20,6 +20,22 @@ class IrCron(models.Model): help="Adjust interval to run at the same hour after and before" "daylight saving time change. It's used twice a year") + def _calculate_daylight_offset(self, nextcall, delta, numbercall, now): + + tz = nextcall.tzinfo + before_offset = tz.normalize(nextcall).utcoffset() + + while nextcall < now and numbercall: + if numbercall > 0: + numbercall -= 1 + if numbercall: + nextcall += delta + + after_offset = tz.normalize(nextcall).utcoffset() + + diff_offset = after_offset - before_offset + return diff_offset + def _process_job(cls, job_cr, job, cron_cr): """Add or remove the Daylight saving offset when needed.""" with api.Environment.manage(): @@ -31,21 +47,11 @@ def _process_job(cls, job_cr, job, cron_cr): job['nextcall'], DEFAULT_SERVER_DATETIME_FORMAT) ) + numbercall = job['numbercall'] delta = _intervalTypes[job['interval_type']]( job['interval_number']) - - numbercall = job['numbercall'] - future_call = nextcall - while future_call < now and numbercall: - if numbercall > 0: - numbercall -= 1 - if numbercall: - future_call += delta - - after_offset = future_call.utcoffset() - before_offset = nextcall.utcoffset() - - diff_offset = after_offset - before_offset + diff_offset = cls._calculate_daylight_offset( + nextcall, delta, numbercall, now) if diff_offset and job['daylight_saving_time_resistant']: if nextcall < now and numbercall: diff --git a/cron_daylight_saving_time_resistant/tests/__init__.py b/cron_daylight_saving_time_resistant/tests/__init__.py new file mode 100644 index 00000000000..cec914727c8 --- /dev/null +++ b/cron_daylight_saving_time_resistant/tests/__init__.py @@ -0,0 +1 @@ +from . import test_dst \ No newline at end of file diff --git a/cron_daylight_saving_time_resistant/tests/test_dst.py b/cron_daylight_saving_time_resistant/tests/test_dst.py new file mode 100644 index 00000000000..991d7f2665d --- /dev/null +++ b/cron_daylight_saving_time_resistant/tests/test_dst.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# © 2018 Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from openerp.tests.common import TransactionCase +from datetime import datetime, timedelta +from pytz import timezone +import pytz +print "hi !" +class TestDST(TransactionCase): + print "test ?" + def test_dst(self): + """First test, caching some data.""" + cron = self.env['ir.cron'] + brux = timezone('Europe/Brussels') + ncall = -1 + winter_jan_12 = datetime(2018, 1, 1, 12, 0) + winter_feb_0 = datetime(2018, 1, 2, 0, 0) + summer_june_12 = datetime(2018, 6, 15, 12, 0) + summer_sep_3 = datetime(2018, 9, 17, 3, 0) + winter_jan_next_year = datetime(2019, 2,3, 0, 0) + tests = [{ + 'nextcall': brux.localize(winter_jan_12), + 'delta': timedelta(days=5), + 'now': brux.localize(winter_feb_0), + 'expected': timedelta(hours=0), + }, { + 'nextcall': brux.localize(winter_jan_12), + 'delta': timedelta(days=6 * 30), + 'now': brux.localize(winter_feb_0), + 'expected': timedelta(hours=1), + }, { + 'nextcall': brux.localize(winter_jan_12), + 'delta': timedelta(days=5), + 'now': brux.localize(summer_june_12), + 'expected': timedelta(hours=1), + }, { + 'nextcall': brux.localize(winter_jan_12), + 'delta': timedelta(days=6 * 30), + 'now': brux.localize(summer_june_12), + 'expected': timedelta(hours=1), + }, { + 'nextcall': brux.localize(winter_jan_12), + 'delta': timedelta(days=6 * 365), + 'now': brux.localize(winter_jan_next_year), + 'expected': timedelta(hours=0), + }] + tests = tests + [{ + 'nextcall': brux.localize(summer_june_12), + 'delta': timedelta(days=5), + 'now': brux.localize(winter_jan_next_year), + 'expected': timedelta(hours=-1), + }, { + 'nextcall': brux.localize(summer_june_12), + 'delta': timedelta(days=4 * 30), + 'now': brux.localize(winter_jan_next_year), + 'expected': timedelta(hours=-1), + }, { + 'nextcall': brux.localize(summer_june_12), + 'delta': timedelta(days=5), + 'now': brux.localize(summer_june_12), + 'expected': timedelta(hours=0), + }, { + 'nextcall': brux.localize(summer_june_12), + 'delta': timedelta(days=6 * 30), + 'now': brux.localize(summer_sep_3), + 'expected': timedelta(hours=-1), + }, { + 'nextcall': brux.localize(summer_june_12), + 'delta': timedelta(days=6 * 365), + 'now': brux.localize(summer_sep_3), + 'expected': timedelta(hours=0), + }] + test = tests[0] + for test in tests: + res = cron._calculate_daylight_offset( + test['nextcall'], + test['delta'], + ncall, + test['now']) + self.assertEqual(res, test['expected']) + From 0e2a308e92ed2af5d42b13c32928f654a2c8afed Mon Sep 17 00:00:00 2001 From: hparfr Date: Wed, 9 May 2018 17:09:21 +0200 Subject: [PATCH 05/14] Pylint --- cron_daylight_saving_time_resistant/README.rst | 2 +- cron_daylight_saving_time_resistant/tests/test_dst.py | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/cron_daylight_saving_time_resistant/README.rst b/cron_daylight_saving_time_resistant/README.rst index d38395d55f5..73b1095c38d 100644 --- a/cron_daylight_saving_time_resistant/README.rst +++ b/cron_daylight_saving_time_resistant/README.rst @@ -34,7 +34,7 @@ Known issues / Roadmap * Write tests * Edge cases like run every 5 minutes + dst resistant may behave -incorrectly during the time change. + incorrectly during the time change. Bug Tracker diff --git a/cron_daylight_saving_time_resistant/tests/test_dst.py b/cron_daylight_saving_time_resistant/tests/test_dst.py index 991d7f2665d..fb12becfc2e 100644 --- a/cron_daylight_saving_time_resistant/tests/test_dst.py +++ b/cron_daylight_saving_time_resistant/tests/test_dst.py @@ -4,10 +4,10 @@ from openerp.tests.common import TransactionCase from datetime import datetime, timedelta from pytz import timezone -import pytz -print "hi !" + + class TestDST(TransactionCase): - print "test ?" + def test_dst(self): """First test, caching some data.""" cron = self.env['ir.cron'] @@ -17,7 +17,7 @@ def test_dst(self): winter_feb_0 = datetime(2018, 1, 2, 0, 0) summer_june_12 = datetime(2018, 6, 15, 12, 0) summer_sep_3 = datetime(2018, 9, 17, 3, 0) - winter_jan_next_year = datetime(2019, 2,3, 0, 0) + winter_jan_next_year = datetime(2019, 2, 3, 0, 0) tests = [{ 'nextcall': brux.localize(winter_jan_12), 'delta': timedelta(days=5), @@ -78,4 +78,3 @@ def test_dst(self): ncall, test['now']) self.assertEqual(res, test['expected']) - From 1358dd92d561c964a82c3a40df38802ac0952e5c Mon Sep 17 00:00:00 2001 From: Florian da Costa Date: Tue, 3 Dec 2019 13:12:40 +0100 Subject: [PATCH 06/14] Fix cron hour in case we loose an hour --- .../models/ir_cron.py | 59 ++++++++++++------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/cron_daylight_saving_time_resistant/models/ir_cron.py b/cron_daylight_saving_time_resistant/models/ir_cron.py index c6a69964013..7c57d0c3ffb 100644 --- a/cron_daylight_saving_time_resistant/models/ir_cron.py +++ b/cron_daylight_saving_time_resistant/models/ir_cron.py @@ -38,25 +38,42 @@ def _calculate_daylight_offset(self, nextcall, delta, numbercall, now): def _process_job(cls, job_cr, job, cron_cr): """Add or remove the Daylight saving offset when needed.""" - with api.Environment.manage(): - now = osv_fields.datetime.context_timestamp( - job_cr, job['user_id'], datetime.now()) - nextcall = osv_fields.datetime.context_timestamp( - job_cr, job['user_id'], - datetime.strptime( - job['nextcall'], - DEFAULT_SERVER_DATETIME_FORMAT) - ) - numbercall = job['numbercall'] - delta = _intervalTypes[job['interval_type']]( - job['interval_number']) - diff_offset = cls._calculate_daylight_offset( - nextcall, delta, numbercall, now) - - if diff_offset and job['daylight_saving_time_resistant']: - if nextcall < now and numbercall: - nextcall -= diff_offset - modified_next_call = fields.Datetime.to_string( - nextcall.astimezone(pytz.UTC)) - job['nextcall'] = modified_next_call super(IrCron, cls)._process_job(job_cr, job, cron_cr) + # changing the date has to be after the super, else, e may add a hour + # to next call, and the super will no run the cron, (because now will + # be 1 hour too soon) and the date will just be incremented of 1 + # hour, each hour...until the changes time really occurs... + # if need to test this, use freeze_gun lib. + if job['daylight_saving_time_resistant']: + with api.Environment.manage(): + now = osv_fields.datetime.context_timestamp( + job_cr, job['user_id'], datetime.now()) + nextcall = osv_fields.datetime.context_timestamp( + job_cr, job['user_id'], + datetime.strptime( + job['nextcall'], # original nextcall + DEFAULT_SERVER_DATETIME_FORMAT) + ) + numbercall = job['numbercall'] + delta = _intervalTypes[job['interval_type']]( + job['interval_number']) + diff_offset = cls._calculate_daylight_offset( + nextcall, delta, numbercall, now) + if diff_offset and nextcall < now and numbercall: + cron_cr.execute(""" + SELECT nextcall FROM ir_cron WHERE id = %s + """, (job['id'],)) + res_sql = cron_cr.fetchall() + new_nextcall = res_sql and res_sql[0][0] + new_nextcall = osv_fields.datetime.context_timestamp( + job_cr, job['user_id'], + datetime.strptime( + new_nextcall, # original nextcall + DEFAULT_SERVER_DATETIME_FORMAT) + ) + new_nextcall -= diff_offset + modified_next_call = fields.Datetime.to_string( + new_nextcall.astimezone(pytz.UTC)) + cron_cr.execute("UPDATE ir_cron SET nextcall=%s WHERE id=%s", + (modified_next_call, job['id'])) + cron_cr.commit() From 81a98a27e0efda7c561e6730b57b02f335199334 Mon Sep 17 00:00:00 2001 From: Florian da Costa Date: Tue, 15 Feb 2022 11:49:53 +0100 Subject: [PATCH 07/14] [IMP] cron_daylight_saving_time_resistant: pre-commit execution --- .../__openerp__.py | 1 - .../models/ir_cron.py | 52 ++++--- .../tests/__init__.py | 2 +- .../tests/test_dst.py | 133 ++++++++++-------- .../views/cron.xml | 6 +- 5 files changed, 107 insertions(+), 87 deletions(-) diff --git a/cron_daylight_saving_time_resistant/__openerp__.py b/cron_daylight_saving_time_resistant/__openerp__.py index 331eb94227a..9791485a74b 100644 --- a/cron_daylight_saving_time_resistant/__openerp__.py +++ b/cron_daylight_saving_time_resistant/__openerp__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright <2018> # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). { diff --git a/cron_daylight_saving_time_resistant/models/ir_cron.py b/cron_daylight_saving_time_resistant/models/ir_cron.py index 7c57d0c3ffb..3d84b311b72 100644 --- a/cron_daylight_saving_time_resistant/models/ir_cron.py +++ b/cron_daylight_saving_time_resistant/models/ir_cron.py @@ -1,15 +1,15 @@ -# -*- coding: utf-8 -*- # © 2018 Akretion - Raphaël Reverdy # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). import logging -import pytz from datetime import datetime +import pytz from openerp import api, fields, models +from openerp.addons.base.ir.ir_cron import _intervalTypes from openerp.osv import fields as osv_fields from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT -from openerp.addons.base.ir.ir_cron import _intervalTypes + _logger = logging.getLogger(__name__) @@ -18,7 +18,8 @@ class IrCron(models.Model): daylight_saving_time_resistant = fields.Boolean( help="Adjust interval to run at the same hour after and before" - "daylight saving time change. It's used twice a year") + "daylight saving time change. It's used twice a year" + ) def _calculate_daylight_offset(self, nextcall, delta, numbercall, now): @@ -44,36 +45,47 @@ def _process_job(cls, job_cr, job, cron_cr): # be 1 hour too soon) and the date will just be incremented of 1 # hour, each hour...until the changes time really occurs... # if need to test this, use freeze_gun lib. - if job['daylight_saving_time_resistant']: + if job["daylight_saving_time_resistant"]: with api.Environment.manage(): now = osv_fields.datetime.context_timestamp( - job_cr, job['user_id'], datetime.now()) + job_cr, job["user_id"], datetime.now() + ) nextcall = osv_fields.datetime.context_timestamp( - job_cr, job['user_id'], + job_cr, + job["user_id"], datetime.strptime( - job['nextcall'], # original nextcall - DEFAULT_SERVER_DATETIME_FORMAT) + job["nextcall"], # original nextcall + DEFAULT_SERVER_DATETIME_FORMAT, + ), ) - numbercall = job['numbercall'] - delta = _intervalTypes[job['interval_type']]( - job['interval_number']) + numbercall = job["numbercall"] + delta = _intervalTypes[job["interval_type"]](job["interval_number"]) diff_offset = cls._calculate_daylight_offset( - nextcall, delta, numbercall, now) + nextcall, delta, numbercall, now + ) if diff_offset and nextcall < now and numbercall: - cron_cr.execute(""" + cron_cr.execute( + """ SELECT nextcall FROM ir_cron WHERE id = %s - """, (job['id'],)) + """, + (job["id"],), + ) res_sql = cron_cr.fetchall() new_nextcall = res_sql and res_sql[0][0] new_nextcall = osv_fields.datetime.context_timestamp( - job_cr, job['user_id'], + job_cr, + job["user_id"], datetime.strptime( new_nextcall, # original nextcall - DEFAULT_SERVER_DATETIME_FORMAT) + DEFAULT_SERVER_DATETIME_FORMAT, + ), ) new_nextcall -= diff_offset modified_next_call = fields.Datetime.to_string( - new_nextcall.astimezone(pytz.UTC)) - cron_cr.execute("UPDATE ir_cron SET nextcall=%s WHERE id=%s", - (modified_next_call, job['id'])) + new_nextcall.astimezone(pytz.UTC) + ) + cron_cr.execute( + "UPDATE ir_cron SET nextcall=%s WHERE id=%s", + (modified_next_call, job["id"]), + ) cron_cr.commit() diff --git a/cron_daylight_saving_time_resistant/tests/__init__.py b/cron_daylight_saving_time_resistant/tests/__init__.py index cec914727c8..ddfe571ecc3 100644 --- a/cron_daylight_saving_time_resistant/tests/__init__.py +++ b/cron_daylight_saving_time_resistant/tests/__init__.py @@ -1 +1 @@ -from . import test_dst \ No newline at end of file +from . import test_dst diff --git a/cron_daylight_saving_time_resistant/tests/test_dst.py b/cron_daylight_saving_time_resistant/tests/test_dst.py index fb12becfc2e..4f0ff92f39f 100644 --- a/cron_daylight_saving_time_resistant/tests/test_dst.py +++ b/cron_daylight_saving_time_resistant/tests/test_dst.py @@ -1,80 +1,89 @@ -# -*- coding: utf-8 -*- # © 2018 Akretion # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openerp.tests.common import TransactionCase from datetime import datetime, timedelta + +from openerp.tests.common import TransactionCase from pytz import timezone class TestDST(TransactionCase): - def test_dst(self): """First test, caching some data.""" - cron = self.env['ir.cron'] - brux = timezone('Europe/Brussels') + cron = self.env["ir.cron"] + brux = timezone("Europe/Brussels") ncall = -1 winter_jan_12 = datetime(2018, 1, 1, 12, 0) winter_feb_0 = datetime(2018, 1, 2, 0, 0) summer_june_12 = datetime(2018, 6, 15, 12, 0) summer_sep_3 = datetime(2018, 9, 17, 3, 0) winter_jan_next_year = datetime(2019, 2, 3, 0, 0) - tests = [{ - 'nextcall': brux.localize(winter_jan_12), - 'delta': timedelta(days=5), - 'now': brux.localize(winter_feb_0), - 'expected': timedelta(hours=0), - }, { - 'nextcall': brux.localize(winter_jan_12), - 'delta': timedelta(days=6 * 30), - 'now': brux.localize(winter_feb_0), - 'expected': timedelta(hours=1), - }, { - 'nextcall': brux.localize(winter_jan_12), - 'delta': timedelta(days=5), - 'now': brux.localize(summer_june_12), - 'expected': timedelta(hours=1), - }, { - 'nextcall': brux.localize(winter_jan_12), - 'delta': timedelta(days=6 * 30), - 'now': brux.localize(summer_june_12), - 'expected': timedelta(hours=1), - }, { - 'nextcall': brux.localize(winter_jan_12), - 'delta': timedelta(days=6 * 365), - 'now': brux.localize(winter_jan_next_year), - 'expected': timedelta(hours=0), - }] - tests = tests + [{ - 'nextcall': brux.localize(summer_june_12), - 'delta': timedelta(days=5), - 'now': brux.localize(winter_jan_next_year), - 'expected': timedelta(hours=-1), - }, { - 'nextcall': brux.localize(summer_june_12), - 'delta': timedelta(days=4 * 30), - 'now': brux.localize(winter_jan_next_year), - 'expected': timedelta(hours=-1), - }, { - 'nextcall': brux.localize(summer_june_12), - 'delta': timedelta(days=5), - 'now': brux.localize(summer_june_12), - 'expected': timedelta(hours=0), - }, { - 'nextcall': brux.localize(summer_june_12), - 'delta': timedelta(days=6 * 30), - 'now': brux.localize(summer_sep_3), - 'expected': timedelta(hours=-1), - }, { - 'nextcall': brux.localize(summer_june_12), - 'delta': timedelta(days=6 * 365), - 'now': brux.localize(summer_sep_3), - 'expected': timedelta(hours=0), - }] + tests = [ + { + "nextcall": brux.localize(winter_jan_12), + "delta": timedelta(days=5), + "now": brux.localize(winter_feb_0), + "expected": timedelta(hours=0), + }, + { + "nextcall": brux.localize(winter_jan_12), + "delta": timedelta(days=6 * 30), + "now": brux.localize(winter_feb_0), + "expected": timedelta(hours=1), + }, + { + "nextcall": brux.localize(winter_jan_12), + "delta": timedelta(days=5), + "now": brux.localize(summer_june_12), + "expected": timedelta(hours=1), + }, + { + "nextcall": brux.localize(winter_jan_12), + "delta": timedelta(days=6 * 30), + "now": brux.localize(summer_june_12), + "expected": timedelta(hours=1), + }, + { + "nextcall": brux.localize(winter_jan_12), + "delta": timedelta(days=6 * 365), + "now": brux.localize(winter_jan_next_year), + "expected": timedelta(hours=0), + }, + ] + tests = tests + [ + { + "nextcall": brux.localize(summer_june_12), + "delta": timedelta(days=5), + "now": brux.localize(winter_jan_next_year), + "expected": timedelta(hours=-1), + }, + { + "nextcall": brux.localize(summer_june_12), + "delta": timedelta(days=4 * 30), + "now": brux.localize(winter_jan_next_year), + "expected": timedelta(hours=-1), + }, + { + "nextcall": brux.localize(summer_june_12), + "delta": timedelta(days=5), + "now": brux.localize(summer_june_12), + "expected": timedelta(hours=0), + }, + { + "nextcall": brux.localize(summer_june_12), + "delta": timedelta(days=6 * 30), + "now": brux.localize(summer_sep_3), + "expected": timedelta(hours=-1), + }, + { + "nextcall": brux.localize(summer_june_12), + "delta": timedelta(days=6 * 365), + "now": brux.localize(summer_sep_3), + "expected": timedelta(hours=0), + }, + ] test = tests[0] for test in tests: res = cron._calculate_daylight_offset( - test['nextcall'], - test['delta'], - ncall, - test['now']) - self.assertEqual(res, test['expected']) + test["nextcall"], test["delta"], ncall, test["now"] + ) + self.assertEqual(res, test["expected"]) diff --git a/cron_daylight_saving_time_resistant/views/cron.xml b/cron_daylight_saving_time_resistant/views/cron.xml index 3e4c802df78..66737ce1602 100644 --- a/cron_daylight_saving_time_resistant/views/cron.xml +++ b/cron_daylight_saving_time_resistant/views/cron.xml @@ -1,12 +1,12 @@ - + ir.cron.resist_dst ir.cron - + - + From 8af4df7d56e75fc7b035c31faa131cb6551977d6 Mon Sep 17 00:00:00 2001 From: Florian da Costa Date: Wed, 16 Feb 2022 11:47:32 +0100 Subject: [PATCH 08/14] [MIG] cron_daylight_saving_time_resistant: Migration to 14.0 --- .../README.rst | 83 ----------- .../{__openerp__.py => __manifest__.py} | 5 +- .../models/ir_cron.py | 86 ++++++----- .../readme/CONFIGURE.rst | 2 + .../readme/CONTRIBUTORS.rst | 2 + .../readme/DESCRIPTION.rst | 8 + .../tests/test_dst.py | 137 +++++++----------- .../views/{cron.xml => ir_cron_views.xml} | 3 +- 8 files changed, 111 insertions(+), 215 deletions(-) delete mode 100644 cron_daylight_saving_time_resistant/README.rst rename cron_daylight_saving_time_resistant/{__openerp__.py => __manifest__.py} (83%) create mode 100644 cron_daylight_saving_time_resistant/readme/CONFIGURE.rst create mode 100644 cron_daylight_saving_time_resistant/readme/CONTRIBUTORS.rst create mode 100644 cron_daylight_saving_time_resistant/readme/DESCRIPTION.rst rename cron_daylight_saving_time_resistant/views/{cron.xml => ir_cron_views.xml} (75%) diff --git a/cron_daylight_saving_time_resistant/README.rst b/cron_daylight_saving_time_resistant/README.rst deleted file mode 100644 index 73b1095c38d..00000000000 --- a/cron_daylight_saving_time_resistant/README.rst +++ /dev/null @@ -1,83 +0,0 @@ -.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png - :target: https://www.gnu.org/licenses/agpl - :alt: License: AGPL-3 - -=================================== -Cron daylight saving time resistant -=================================== - -This module adjust cron to run at fixed hours, local time. - - -Without this module, when a daylight saving time change occur, the cron will not take -the hour change in account. - -With this module, when a daylight saving time change occur, the offset (+1 or -1 hour) -will be applied. - - -Usage -===== - -To use this module, you need to edit a cron, and check the option, -"Daylight saving time resistant". - -#. Go to ... - -.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas - :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/149/9.0 - - -Known issues / Roadmap -====================== - -* Write tests -* Edge cases like run every 5 minutes + dst resistant may behave - incorrectly during the time change. - - -Bug Tracker -=========== - -Bugs are tracked on `GitHub Issues -`_. In case of trouble, please -check there if your issue has already been reported. If you spotted it first, -help us smash it by providing detailed and welcomed feedback. - -Credits -======= - -Images ------- - -* Odoo Community Association: `Icon `_. - -Contributors ------------- - -* Raphaël Reverdy https://akretion.com - -Do not contact contributors directly about support or help with technical issues. - -Funders -------- - -The development of this module has been financially supported by: - -* Akretion - -Maintainer ----------- - -.. image:: https://odoo-community.org/logo.png - :alt: Odoo Community Association - :target: https://odoo-community.org - -This module is maintained by the OCA. - -OCA, or the Odoo Community Association, is a nonprofit organization whose -mission is to support the collaborative development of Odoo features and -promote its widespread use. - -To contribute to this module, please visit https://odoo-community.org. diff --git a/cron_daylight_saving_time_resistant/__openerp__.py b/cron_daylight_saving_time_resistant/__manifest__.py similarity index 83% rename from cron_daylight_saving_time_resistant/__openerp__.py rename to cron_daylight_saving_time_resistant/__manifest__.py index 9791485a74b..99cf20395d4 100644 --- a/cron_daylight_saving_time_resistant/__openerp__.py +++ b/cron_daylight_saving_time_resistant/__manifest__.py @@ -1,9 +1,8 @@ -# Copyright <2018> # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). { "name": "Cron daylight saving time resistant", "summary": "Run cron on fixed hours", - "version": "9.0.1.0.0", + "version": "14.0.1.0.0", "category": "Tools", "website": "https://github.com/OCA/server-tools", "author": "akretion, Odoo Community Association (OCA)", @@ -13,6 +12,6 @@ "base", ], "data": [ - "views/cron.xml", + "views/ir_cron_views.xml", ], } diff --git a/cron_daylight_saving_time_resistant/models/ir_cron.py b/cron_daylight_saving_time_resistant/models/ir_cron.py index 3d84b311b72..0a2c5651e7e 100644 --- a/cron_daylight_saving_time_resistant/models/ir_cron.py +++ b/cron_daylight_saving_time_resistant/models/ir_cron.py @@ -1,14 +1,13 @@ -# © 2018 Akretion - Raphaël Reverdy # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). import logging from datetime import datetime import pytz -from openerp import api, fields, models -from openerp.addons.base.ir.ir_cron import _intervalTypes -from openerp.osv import fields as osv_fields -from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT + +from odoo import api, fields, models + +from odoo.addons.base.models.ir_cron import _intervalTypes _logger = logging.getLogger(__name__) @@ -37,55 +36,52 @@ def _calculate_daylight_offset(self, nextcall, delta, numbercall, now): diff_offset = after_offset - before_offset return diff_offset + @classmethod def _process_job(cls, job_cr, job, cron_cr): """Add or remove the Daylight saving offset when needed.""" - super(IrCron, cls)._process_job(job_cr, job, cron_cr) + super()._process_job(job_cr, job, cron_cr) # changing the date has to be after the super, else, e may add a hour # to next call, and the super will no run the cron, (because now will # be 1 hour too soon) and the date will just be incremented of 1 # hour, each hour...until the changes time really occurs... - # if need to test this, use freeze_gun lib. if job["daylight_saving_time_resistant"]: with api.Environment.manage(): - now = osv_fields.datetime.context_timestamp( - job_cr, job["user_id"], datetime.now() - ) - nextcall = osv_fields.datetime.context_timestamp( - job_cr, - job["user_id"], - datetime.strptime( - job["nextcall"], # original nextcall - DEFAULT_SERVER_DATETIME_FORMAT, - ), - ) - numbercall = job["numbercall"] - delta = _intervalTypes[job["interval_type"]](job["interval_number"]) - diff_offset = cls._calculate_daylight_offset( - nextcall, delta, numbercall, now - ) - if diff_offset and nextcall < now and numbercall: - cron_cr.execute( - """ - SELECT nextcall FROM ir_cron WHERE id = %s - """, - (job["id"],), - ) - res_sql = cron_cr.fetchall() - new_nextcall = res_sql and res_sql[0][0] - new_nextcall = osv_fields.datetime.context_timestamp( + try: + cron = api.Environment( job_cr, job["user_id"], - datetime.strptime( - new_nextcall, # original nextcall - DEFAULT_SERVER_DATETIME_FORMAT, - ), - ) - new_nextcall -= diff_offset - modified_next_call = fields.Datetime.to_string( - new_nextcall.astimezone(pytz.UTC) - ) - cron_cr.execute( - "UPDATE ir_cron SET nextcall=%s WHERE id=%s", - (modified_next_call, job["id"]), + {"lastcall": fields.Datetime.from_string(job["lastcall"])}, + )[cls._name] + now = fields.Datetime.context_timestamp(cron, datetime.now()) + # original nextcall + nextcall = fields.Datetime.context_timestamp(cron, job["nextcall"]) + numbercall = job["numbercall"] + delta = _intervalTypes[job["interval_type"]](job["interval_number"]) + diff_offset = cron._calculate_daylight_offset( + nextcall, delta, numbercall, now ) + if diff_offset and nextcall < now and numbercall: + cron_cr.execute( + """ + SELECT nextcall FROM ir_cron WHERE id = %s + """, + (job["id"],), + ) + res_sql = cron_cr.fetchall() + new_nextcall = res_sql and res_sql[0][0] + new_nextcall = fields.Datetime.context_timestamp( + cron, new_nextcall + ) + new_nextcall -= diff_offset + modified_next_call = fields.Datetime.to_string( + new_nextcall.astimezone(pytz.UTC) + ) + cron_cr.execute( + "UPDATE ir_cron SET nextcall=%s WHERE id=%s", + (modified_next_call, job["id"]), + ) + cron.flush() + cron.invalidate_cache() + finally: + job_cr.commit() cron_cr.commit() diff --git a/cron_daylight_saving_time_resistant/readme/CONFIGURE.rst b/cron_daylight_saving_time_resistant/readme/CONFIGURE.rst new file mode 100644 index 00000000000..8fc14e7c142 --- /dev/null +++ b/cron_daylight_saving_time_resistant/readme/CONFIGURE.rst @@ -0,0 +1,2 @@ +* Go to the menu Settings => Technical => Automation => Scheduled Actions + Then you can check the check box Daylight Saving Time Resistant diff --git a/cron_daylight_saving_time_resistant/readme/CONTRIBUTORS.rst b/cron_daylight_saving_time_resistant/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..3444e783702 --- /dev/null +++ b/cron_daylight_saving_time_resistant/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Raphaël Reverdy https://akretion.com +* Florian da Costa diff --git a/cron_daylight_saving_time_resistant/readme/DESCRIPTION.rst b/cron_daylight_saving_time_resistant/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..c43f98341a0 --- /dev/null +++ b/cron_daylight_saving_time_resistant/readme/DESCRIPTION.rst @@ -0,0 +1,8 @@ +This module adjust cron to run at fixed hours, local time. + + +Without this module, when a daylight saving time change occur, the cron will not take +the hour change in account. + +With this module, when a daylight saving time change occur, the offset (+1 or -1 hour) +will be applied. diff --git a/cron_daylight_saving_time_resistant/tests/test_dst.py b/cron_daylight_saving_time_resistant/tests/test_dst.py index 4f0ff92f39f..2541bfa5f21 100644 --- a/cron_daylight_saving_time_resistant/tests/test_dst.py +++ b/cron_daylight_saving_time_resistant/tests/test_dst.py @@ -1,89 +1,62 @@ -# © 2018 Akretion # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from datetime import datetime, timedelta -from openerp.tests.common import TransactionCase -from pytz import timezone +from freezegun import freeze_time + +import odoo +from odoo import fields +from odoo.tests.common import TransactionCase +from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT class TestDST(TransactionCase): - def test_dst(self): - """First test, caching some data.""" - cron = self.env["ir.cron"] - brux = timezone("Europe/Brussels") - ncall = -1 - winter_jan_12 = datetime(2018, 1, 1, 12, 0) - winter_feb_0 = datetime(2018, 1, 2, 0, 0) - summer_june_12 = datetime(2018, 6, 15, 12, 0) - summer_sep_3 = datetime(2018, 9, 17, 3, 0) - winter_jan_next_year = datetime(2019, 2, 3, 0, 0) - tests = [ - { - "nextcall": brux.localize(winter_jan_12), - "delta": timedelta(days=5), - "now": brux.localize(winter_feb_0), - "expected": timedelta(hours=0), - }, - { - "nextcall": brux.localize(winter_jan_12), - "delta": timedelta(days=6 * 30), - "now": brux.localize(winter_feb_0), - "expected": timedelta(hours=1), - }, - { - "nextcall": brux.localize(winter_jan_12), - "delta": timedelta(days=5), - "now": brux.localize(summer_june_12), - "expected": timedelta(hours=1), - }, - { - "nextcall": brux.localize(winter_jan_12), - "delta": timedelta(days=6 * 30), - "now": brux.localize(summer_june_12), - "expected": timedelta(hours=1), - }, - { - "nextcall": brux.localize(winter_jan_12), - "delta": timedelta(days=6 * 365), - "now": brux.localize(winter_jan_next_year), - "expected": timedelta(hours=0), - }, - ] - tests = tests + [ - { - "nextcall": brux.localize(summer_june_12), - "delta": timedelta(days=5), - "now": brux.localize(winter_jan_next_year), - "expected": timedelta(hours=-1), - }, - { - "nextcall": brux.localize(summer_june_12), - "delta": timedelta(days=4 * 30), - "now": brux.localize(winter_jan_next_year), - "expected": timedelta(hours=-1), - }, - { - "nextcall": brux.localize(summer_june_12), - "delta": timedelta(days=5), - "now": brux.localize(summer_june_12), - "expected": timedelta(hours=0), - }, - { - "nextcall": brux.localize(summer_june_12), - "delta": timedelta(days=6 * 30), - "now": brux.localize(summer_sep_3), - "expected": timedelta(hours=-1), - }, - { - "nextcall": brux.localize(summer_june_12), - "delta": timedelta(days=6 * 365), - "now": brux.localize(summer_sep_3), - "expected": timedelta(hours=0), - }, - ] - test = tests[0] - for test in tests: - res = cron._calculate_daylight_offset( - test["nextcall"], test["delta"], ncall, test["now"] + def setUp(self): + super().setUp() + self.registry.enter_test_mode(self.env.cr) + + def tearDown(self): + self.registry.leave_test_mode() + super().tearDown() + + def _check_cron_date_after_run(self, cron, datetime_str): + # add 10 sec to make sure cron will run + datetime_current = datetime.strptime( + datetime_str, DEFAULT_SERVER_DATETIME_FORMAT + ) + timedelta(seconds=10) + datetime_current_str = datetime_current.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + with freeze_time(datetime_current_str): + cron.write( + {"nextcall": datetime_str, "daylight_saving_time_resistant": True} ) - self.assertEqual(res, test["expected"]) + cron.flush() + self.env.cr.execute("SELECT * FROM ir_cron WHERE id = %s", (cron.id,)) + job = self.env.cr.dictfetchall()[0] + timezone_date_orig = fields.Datetime.context_timestamp(cron, cron.nextcall) + with odoo.registry(self.env.cr.dbname).cursor() as new_cr: + registry = odoo.registry(new_cr.dbname) + registry["ir.cron"]._process_job(new_cr, job, new_cr) + day_after_date_orig = (timezone_date_orig + timedelta(days=1)).day + timezone_date_after = fields.Datetime.context_timestamp(cron, cron.nextcall) + # check the cron is really planned the next day (which mean it has run + # then check the planned hour is the same even in case of change of time + # (brussels summer time/ brussels winter time + self.assertEqual(day_after_date_orig, timezone_date_after.day) + self.assertEqual(timezone_date_orig.hour, timezone_date_after.hour) + + def test_cron(self): + cron = self.env["ir.cron"].create( + { + "name": "TestCron", + "model_id": self.env.ref("base.model_res_partner").id, + "state": "code", + "code": "model.search([])", + "interval_number": 1, + "interval_type": "days", + "numbercall": -1, + "doall": False, + } + ) + # from summer time to winter time + self._check_cron_date_after_run(cron, "2021-10-30 15:00:00") + # from winter time to summer time + self._check_cron_date_after_run(cron, "2021-03-27 15:00:00") diff --git a/cron_daylight_saving_time_resistant/views/cron.xml b/cron_daylight_saving_time_resistant/views/ir_cron_views.xml similarity index 75% rename from cron_daylight_saving_time_resistant/views/cron.xml rename to cron_daylight_saving_time_resistant/views/ir_cron_views.xml index 66737ce1602..0f79894dd7c 100644 --- a/cron_daylight_saving_time_resistant/views/cron.xml +++ b/cron_daylight_saving_time_resistant/views/ir_cron_views.xml @@ -1,9 +1,8 @@ - ir.cron.resist_dst ir.cron - + From 9b9224a5b482c6a13b068d928f846e329a607d67 Mon Sep 17 00:00:00 2001 From: Florian da Costa Date: Tue, 13 Dec 2022 17:29:37 +0100 Subject: [PATCH 09/14] [IMP] cron_daylight_saving_time_resistant: black, isort, prettier --- .../odoo/addons/cron_daylight_saving_time_resistant | 1 + setup/cron_daylight_saving_time_resistant/setup.py | 6 ++++++ 2 files changed, 7 insertions(+) create mode 120000 setup/cron_daylight_saving_time_resistant/odoo/addons/cron_daylight_saving_time_resistant create mode 100644 setup/cron_daylight_saving_time_resistant/setup.py diff --git a/setup/cron_daylight_saving_time_resistant/odoo/addons/cron_daylight_saving_time_resistant b/setup/cron_daylight_saving_time_resistant/odoo/addons/cron_daylight_saving_time_resistant new file mode 120000 index 00000000000..61296a7d37f --- /dev/null +++ b/setup/cron_daylight_saving_time_resistant/odoo/addons/cron_daylight_saving_time_resistant @@ -0,0 +1 @@ +../../../../cron_daylight_saving_time_resistant \ No newline at end of file diff --git a/setup/cron_daylight_saving_time_resistant/setup.py b/setup/cron_daylight_saving_time_resistant/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/cron_daylight_saving_time_resistant/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 86df925ff9c8a39322c6d941e2a95ca0982837e5 Mon Sep 17 00:00:00 2001 From: Florian da Costa Date: Tue, 13 Dec 2022 17:41:31 +0100 Subject: [PATCH 10/14] [16.0][MIG] Migrate cron_daylight_saving_time_resistant to v16 --- .../README.rst | 87 ++++ .../__manifest__.py | 2 +- .../models/ir_cron.py | 10 +- .../static/description/index.html | 432 ++++++++++++++++++ .../tests/test_dst.py | 13 +- 5 files changed, 535 insertions(+), 9 deletions(-) create mode 100644 cron_daylight_saving_time_resistant/README.rst create mode 100644 cron_daylight_saving_time_resistant/static/description/index.html diff --git a/cron_daylight_saving_time_resistant/README.rst b/cron_daylight_saving_time_resistant/README.rst new file mode 100644 index 00000000000..82709ce50f6 --- /dev/null +++ b/cron_daylight_saving_time_resistant/README.rst @@ -0,0 +1,87 @@ +=================================== +Cron daylight saving time resistant +=================================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github + :target: https://github.com/OCA/server-tools/tree/16.0/cron_daylight_saving_time_resistant + :alt: OCA/server-tools +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-tools-16-0/server-tools-16-0-cron_daylight_saving_time_resistant + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/webui/builds.html?repo=OCA/server-tools&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adjust cron to run at fixed hours, local time. + + +Without this module, when a daylight saving time change occur, the cron will not take +the hour change in account. + +With this module, when a daylight saving time change occur, the offset (+1 or -1 hour) +will be applied. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +* Go to the menu Settings => Technical => Automation => Scheduled Actions + Then you can check the check box Daylight Saving Time Resistant + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* akretion + +Contributors +~~~~~~~~~~~~ + +* Raphaël Reverdy https://akretion.com +* Florian da Costa + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/server-tools `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/cron_daylight_saving_time_resistant/__manifest__.py b/cron_daylight_saving_time_resistant/__manifest__.py index 99cf20395d4..856aca84e52 100644 --- a/cron_daylight_saving_time_resistant/__manifest__.py +++ b/cron_daylight_saving_time_resistant/__manifest__.py @@ -2,7 +2,7 @@ { "name": "Cron daylight saving time resistant", "summary": "Run cron on fixed hours", - "version": "14.0.1.0.0", + "version": "16.0.1.0.0", "category": "Tools", "website": "https://github.com/OCA/server-tools", "author": "akretion, Odoo Community Association (OCA)", diff --git a/cron_daylight_saving_time_resistant/models/ir_cron.py b/cron_daylight_saving_time_resistant/models/ir_cron.py index 0a2c5651e7e..62273647a41 100644 --- a/cron_daylight_saving_time_resistant/models/ir_cron.py +++ b/cron_daylight_saving_time_resistant/models/ir_cron.py @@ -37,15 +37,15 @@ def _calculate_daylight_offset(self, nextcall, delta, numbercall, now): return diff_offset @classmethod - def _process_job(cls, job_cr, job, cron_cr): + def _process_job(cls, db, cron_cr, job): """Add or remove the Daylight saving offset when needed.""" - super()._process_job(job_cr, job, cron_cr) + res = super()._process_job(db, cron_cr, job) # changing the date has to be after the super, else, e may add a hour # to next call, and the super will no run the cron, (because now will # be 1 hour too soon) and the date will just be incremented of 1 # hour, each hour...until the changes time really occurs... if job["daylight_saving_time_resistant"]: - with api.Environment.manage(): + with cls.pool.cursor() as job_cr: try: cron = api.Environment( job_cr, @@ -80,8 +80,6 @@ def _process_job(cls, job_cr, job, cron_cr): "UPDATE ir_cron SET nextcall=%s WHERE id=%s", (modified_next_call, job["id"]), ) - cron.flush() - cron.invalidate_cache() finally: - job_cr.commit() cron_cr.commit() + return res diff --git a/cron_daylight_saving_time_resistant/static/description/index.html b/cron_daylight_saving_time_resistant/static/description/index.html new file mode 100644 index 00000000000..6caa9399129 --- /dev/null +++ b/cron_daylight_saving_time_resistant/static/description/index.html @@ -0,0 +1,432 @@ + + + + + + +Cron daylight saving time resistant + + + +
+

Cron daylight saving time resistant

+ + +

Beta License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runboat

+

This module adjust cron to run at fixed hours, local time.

+

Without this module, when a daylight saving time change occur, the cron will not take +the hour change in account.

+

With this module, when a daylight saving time change occur, the offset (+1 or -1 hour) +will be applied.

+

Table of contents

+ +
+

Configuration

+
    +
  • Go to the menu Settings => Technical => Automation => Scheduled Actions +Then you can check the check box Daylight Saving Time Resistant
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • akretion
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/server-tools project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/cron_daylight_saving_time_resistant/tests/test_dst.py b/cron_daylight_saving_time_resistant/tests/test_dst.py index 2541bfa5f21..75c16c5137c 100644 --- a/cron_daylight_saving_time_resistant/tests/test_dst.py +++ b/cron_daylight_saving_time_resistant/tests/test_dst.py @@ -28,13 +28,19 @@ def _check_cron_date_after_run(self, cron, datetime_str): cron.write( {"nextcall": datetime_str, "daylight_saving_time_resistant": True} ) - cron.flush() + cron.flush_recordset() self.env.cr.execute("SELECT * FROM ir_cron WHERE id = %s", (cron.id,)) job = self.env.cr.dictfetchall()[0] timezone_date_orig = fields.Datetime.context_timestamp(cron, cron.nextcall) + # ensure Paris time zone is taken into account. If we only work in UTC + # there is not change of hour and the test will be green even if it does + # nothing at all... + self.assertEqual(timezone_date_orig.tzinfo.zone, "Europe/Paris") with odoo.registry(self.env.cr.dbname).cursor() as new_cr: registry = odoo.registry(new_cr.dbname) - registry["ir.cron"]._process_job(new_cr, job, new_cr) + registry["ir.cron"]._process_job(new_cr.dbname, new_cr, job) + # since it is updated as a sql query in module + cron.invalidate_recordset() day_after_date_orig = (timezone_date_orig + timedelta(days=1)).day timezone_date_after = fields.Datetime.context_timestamp(cron, cron.nextcall) # check the cron is really planned the next day (which mean it has run @@ -44,6 +50,9 @@ def _check_cron_date_after_run(self, cron, datetime_str): self.assertEqual(timezone_date_orig.hour, timezone_date_after.hour) def test_cron(self): + user = self.env.ref("base.user_root") + user.write({"tz": "Europe/Paris"}) + user.invalidate_recordset() cron = self.env["ir.cron"].create( { "name": "TestCron", From af8f577ddb0bfe9c26d1b104ffe557bab4f9841d Mon Sep 17 00:00:00 2001 From: clementmbr Date: Wed, 24 Jan 2024 11:40:39 -0300 Subject: [PATCH 11/14] [FIX] test cron_daylight_saving_time_resistant --- cron_daylight_saving_time_resistant/tests/test_dst.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cron_daylight_saving_time_resistant/tests/test_dst.py b/cron_daylight_saving_time_resistant/tests/test_dst.py index 75c16c5137c..25ab5d81bca 100644 --- a/cron_daylight_saving_time_resistant/tests/test_dst.py +++ b/cron_daylight_saving_time_resistant/tests/test_dst.py @@ -38,7 +38,9 @@ def _check_cron_date_after_run(self, cron, datetime_str): self.assertEqual(timezone_date_orig.tzinfo.zone, "Europe/Paris") with odoo.registry(self.env.cr.dbname).cursor() as new_cr: registry = odoo.registry(new_cr.dbname) - registry["ir.cron"]._process_job(new_cr.dbname, new_cr, job) + db = odoo.sql_db.db_connect(new_cr.dbname) + + registry["ir.cron"]._process_job(db, new_cr, job) # since it is updated as a sql query in module cron.invalidate_recordset() day_after_date_orig = (timezone_date_orig + timedelta(days=1)).day From fa3dfab394ac464965dd17a24d858f93fe04e207 Mon Sep 17 00:00:00 2001 From: Florian da Costa Date: Mon, 29 Jan 2024 09:55:32 +0100 Subject: [PATCH 12/14] cron_daylight_saving_time_resistant : add maintainers --- cron_daylight_saving_time_resistant/__manifest__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cron_daylight_saving_time_resistant/__manifest__.py b/cron_daylight_saving_time_resistant/__manifest__.py index 856aca84e52..07f6c8bd629 100644 --- a/cron_daylight_saving_time_resistant/__manifest__.py +++ b/cron_daylight_saving_time_resistant/__manifest__.py @@ -8,6 +8,7 @@ "author": "akretion, Odoo Community Association (OCA)", "license": "AGPL-3", "installable": True, + "maintainers": ["florian-dacosta"], "depends": [ "base", ], From 2b952bdb10733b43a80962bdcd184bc6eb999469 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Mon, 29 Jan 2024 09:17:02 +0000 Subject: [PATCH 13/14] [UPD] Update cron_daylight_saving_time_resistant.pot --- .../cron_daylight_saving_time_resistant.pot | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 cron_daylight_saving_time_resistant/i18n/cron_daylight_saving_time_resistant.pot diff --git a/cron_daylight_saving_time_resistant/i18n/cron_daylight_saving_time_resistant.pot b/cron_daylight_saving_time_resistant/i18n/cron_daylight_saving_time_resistant.pot new file mode 100644 index 00000000000..60d4b3d421c --- /dev/null +++ b/cron_daylight_saving_time_resistant/i18n/cron_daylight_saving_time_resistant.pot @@ -0,0 +1,36 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * cron_daylight_saving_time_resistant +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: cron_daylight_saving_time_resistant +#: model:ir.model.fields,help:cron_daylight_saving_time_resistant.field_ir_cron__daylight_saving_time_resistant +msgid "" +"Adjust interval to run at the same hour after and beforedaylight saving time" +" change. It's used twice a year" +msgstr "" + +#. module: cron_daylight_saving_time_resistant +#: model:ir.model.fields,field_description:cron_daylight_saving_time_resistant.field_ir_cron__daylight_saving_time_resistant +msgid "Daylight Saving Time Resistant" +msgstr "" + +#. module: cron_daylight_saving_time_resistant +#: model:ir.model,name:cron_daylight_saving_time_resistant.model_ir_cron +msgid "Scheduled Actions" +msgstr "" + +#. module: cron_daylight_saving_time_resistant +#: model:ir.model.fields,field_description:cron_daylight_saving_time_resistant.field_ir_cron__smart_search +msgid "Smart Search" +msgstr "" From 7c431ece9be61f47d9984b437f1fbf56b414dce4 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 29 Jan 2024 09:23:34 +0000 Subject: [PATCH 14/14] [BOT] post-merge updates --- README.md | 1 + .../README.rst | 19 +++++++-- .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 39 ++++++++++-------- setup/_metapackage/VERSION.txt | 2 +- setup/_metapackage/setup.py | 1 + 6 files changed, 39 insertions(+), 23 deletions(-) create mode 100644 cron_daylight_saving_time_resistant/static/description/icon.png diff --git a/README.md b/README.md index da53efb40e6..c3182bb7ca0 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ addon | version | maintainers | summary [base_technical_user](base_technical_user/) | 16.0.1.0.0 | | Add a technical user parameter on the company [base_time_window](base_time_window/) | 16.0.1.0.0 | | Base model to handle time windows [base_view_inheritance_extension](base_view_inheritance_extension/) | 16.0.1.1.0 | | Adds more operators for view inheritance +[cron_daylight_saving_time_resistant](cron_daylight_saving_time_resistant/) | 16.0.1.0.0 | [![florian-dacosta](https://github.com/florian-dacosta.png?size=30px)](https://github.com/florian-dacosta) | Run cron on fixed hours [database_cleanup](database_cleanup/) | 16.0.1.0.1 | | Database cleanup [dbfilter_from_header](dbfilter_from_header/) | 16.0.1.0.0 | | Filter databases with HTTP headers [excel_import_export](excel_import_export/) | 16.0.1.1.0 | [![kittiu](https://github.com/kittiu.png?size=30px)](https://github.com/kittiu) | Base module for developing Excel import/export/report diff --git a/cron_daylight_saving_time_resistant/README.rst b/cron_daylight_saving_time_resistant/README.rst index 82709ce50f6..3aeeab441db 100644 --- a/cron_daylight_saving_time_resistant/README.rst +++ b/cron_daylight_saving_time_resistant/README.rst @@ -2,10 +2,13 @@ Cron daylight saving time resistant =================================== -.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:fdc494dd40cb7c08648cb1ecb755f4a0ba5580eef66adf55482ee787e47c2afb + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status @@ -20,10 +23,10 @@ Cron daylight saving time resistant :target: https://translation.odoo-community.org/projects/server-tools-16-0/server-tools-16-0-cron_daylight_saving_time_resistant :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/webui/builds.html?repo=OCA/server-tools&target_branch=16.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=16.0 :alt: Try me on Runboat -|badge1| |badge2| |badge3| |badge4| |badge5| +|badge1| |badge2| |badge3| |badge4| |badge5| This module adjust cron to run at fixed hours, local time. @@ -50,7 +53,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. -If you spotted it first, help us smashing it by providing a detailed and welcomed +If you spotted it first, help us to smash it by providing a detailed and welcomed `feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -82,6 +85,14 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. +.. |maintainer-florian-dacosta| image:: https://github.com/florian-dacosta.png?size=40px + :target: https://github.com/florian-dacosta + :alt: florian-dacosta + +Current `maintainer `__: + +|maintainer-florian-dacosta| + This module is part of the `OCA/server-tools `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/cron_daylight_saving_time_resistant/static/description/icon.png b/cron_daylight_saving_time_resistant/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/cron_daylight_saving_time_resistant/static/description/index.html b/cron_daylight_saving_time_resistant/static/description/index.html index 6caa9399129..5a29a1f2b17 100644 --- a/cron_daylight_saving_time_resistant/static/description/index.html +++ b/cron_daylight_saving_time_resistant/static/description/index.html @@ -1,20 +1,19 @@ - - + Cron daylight saving time resistant