From 00ce3bd0b18d308970cbd0df369927d4f9ae7936 Mon Sep 17 00:00:00 2001 From: AaronHForgeFlow Date: Fri, 10 Jul 2026 19:08:18 +0200 Subject: [PATCH] [ADD] base_cron_weekday --- base_cron_weekday/README.rst | 108 +++++ base_cron_weekday/__init__.py | 1 + base_cron_weekday/__manifest__.py | 15 + base_cron_weekday/models/__init__.py | 1 + base_cron_weekday/models/ir_cron.py | 75 +++ base_cron_weekday/pyproject.toml | 3 + base_cron_weekday/readme/CONTRIBUTORS.md | 1 + base_cron_weekday/readme/DESCRIPTION.md | 11 + base_cron_weekday/readme/USAGE.md | 14 + .../static/description/index.html | 457 ++++++++++++++++++ base_cron_weekday/tests/__init__.py | 1 + base_cron_weekday/tests/test_cron_weekday.py | 72 +++ base_cron_weekday/views/ir_cron_views.xml | 31 ++ 13 files changed, 790 insertions(+) create mode 100644 base_cron_weekday/README.rst create mode 100644 base_cron_weekday/__init__.py create mode 100644 base_cron_weekday/__manifest__.py create mode 100644 base_cron_weekday/models/__init__.py create mode 100644 base_cron_weekday/models/ir_cron.py create mode 100644 base_cron_weekday/pyproject.toml create mode 100644 base_cron_weekday/readme/CONTRIBUTORS.md create mode 100644 base_cron_weekday/readme/DESCRIPTION.md create mode 100644 base_cron_weekday/readme/USAGE.md create mode 100644 base_cron_weekday/static/description/index.html create mode 100644 base_cron_weekday/tests/__init__.py create mode 100644 base_cron_weekday/tests/test_cron_weekday.py create mode 100644 base_cron_weekday/views/ir_cron_views.xml diff --git a/base_cron_weekday/README.rst b/base_cron_weekday/README.rst new file mode 100644 index 00000000000..efb4491f5a9 --- /dev/null +++ b/base_cron_weekday/README.rst @@ -0,0 +1,108 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +================= +Base Cron Weekday +================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:453890bf64f5ace83285eb727f27275145d393858cf5a9c1a08c2fb24c388865 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github + :target: https://github.com/OCA/server-tools/tree/19.0/base_cron_weekday + :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-19-0/server-tools-19-0-base_cron_weekday + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module lets you restrict a scheduled action (cron) from running on +specific weekdays. + +Every scheduled action gains a set of per-weekday checkboxes (all +enabled by default, so behaviour is unchanged until you deselect a day). +When a run would fall on a deselected weekday, it is deferred to the +next allowed day at the same time of day instead of executing. + +A common use case is skipping heavy or external-integration jobs on +weekends, when the remote service publishes nothing and the run has +nothing to do. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +1. Go to *Settings > Technical > Automation > Scheduled Actions* + (developer mode). +2. Open a scheduled action. +3. Under *Run on Weekdays*, deselect the days on which the action must + not run. +4. Save. At least one weekday must stay selected. + +Notes: + +- The weekday is evaluated in the timezone of the action's *Scheduler + User*. +- Only the scheduled cadence is affected. Running an action manually or + through a code trigger still executes it regardless of the selected + weekdays. + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* ForgeFlow + +Contributors +------------ + +- ForgeFlow + +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/base_cron_weekday/__init__.py b/base_cron_weekday/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/base_cron_weekday/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/base_cron_weekday/__manifest__.py b/base_cron_weekday/__manifest__.py new file mode 100644 index 00000000000..dcea87deaa6 --- /dev/null +++ b/base_cron_weekday/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2026 ForgeFlow +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0). + +{ + "name": "Base Cron Weekday", + "version": "19.0.1.0.0", + "category": "Technical Settings", + "summary": "Restrict scheduled actions from running on selected weekdays", + "author": "ForgeFlow, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/server-tools", + "license": "LGPL-3", + "depends": ["base"], + "data": ["views/ir_cron_views.xml"], + "installable": True, +} diff --git a/base_cron_weekday/models/__init__.py b/base_cron_weekday/models/__init__.py new file mode 100644 index 00000000000..9115592626a --- /dev/null +++ b/base_cron_weekday/models/__init__.py @@ -0,0 +1 @@ +from . import ir_cron diff --git a/base_cron_weekday/models/ir_cron.py b/base_cron_weekday/models/ir_cron.py new file mode 100644 index 00000000000..d4bea959924 --- /dev/null +++ b/base_cron_weekday/models/ir_cron.py @@ -0,0 +1,75 @@ +# Copyright 2026 ForgeFlow +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0). + +from datetime import timedelta, timezone + +from odoo import api, fields, models +from odoo.exceptions import ValidationError + +WEEKDAY_FIELDS = [ + ("run_monday", 0), + ("run_tuesday", 1), + ("run_wednesday", 2), + ("run_thursday", 3), + ("run_friday", 4), + ("run_saturday", 5), + ("run_sunday", 6), +] + + +class IrCron(models.Model): + _inherit = "ir.cron" + + run_monday = fields.Boolean(default=True) + run_tuesday = fields.Boolean(default=True) + run_wednesday = fields.Boolean(default=True) + run_thursday = fields.Boolean(default=True) + run_friday = fields.Boolean(default=True) + run_saturday = fields.Boolean(default=True) + run_sunday = fields.Boolean(default=True) + + @api.constrains(*[name for name, _weekday in WEEKDAY_FIELDS]) + def _check_run_weekdays(self): + for cron in self: + if not any(cron[name] for name, _weekday in WEEKDAY_FIELDS): + raise ValidationError( + self.env._( + "A scheduled action must be allowed to run on at least " + "one weekday." + ) + ) + + def _disallowed_weekdays(self): + """Return the set of Python weekday numbers (Mon=0..Sun=6) the cron + must not run on.""" + self.ensure_one() + return {weekday for name, weekday in WEEKDAY_FIELDS if not self[name]} + + def _weekday_in_tz(self, nextcall): + """Weekday of a naive-UTC ``nextcall`` evaluated in the cron's timezone.""" + return fields.Datetime.context_timestamp(self, nextcall).weekday() + + @api.model + def _reschedule_later(self, job): + super()._reschedule_later(job) + cron = self.browse(job["id"]) + disallowed = cron._disallowed_weekdays() + if not disallowed: + return + # core wrote nextcall via raw SQL, so drop the stale ORM value first. + cron.invalidate_recordset(["nextcall"]) + nextcall = cron.nextcall + # Defer one day at a time to the next allowed weekday, at the same time + # of day. Bounded to at most 6 steps since at least one weekday is + # always allowed. + guard = 0 + while cron._weekday_in_tz(nextcall) in disallowed and guard < 7: + nextcall = fields.Datetime.context_timestamp(cron, nextcall) + nextcall += timedelta(days=1) + nextcall = nextcall.astimezone(timezone.utc).replace(tzinfo=None) + guard += 1 + if nextcall != cron.nextcall: + self.env.cr.execute( + "UPDATE ir_cron SET nextcall = %s WHERE id = %s", + [nextcall, job["id"]], + ) diff --git a/base_cron_weekday/pyproject.toml b/base_cron_weekday/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/base_cron_weekday/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/base_cron_weekday/readme/CONTRIBUTORS.md b/base_cron_weekday/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..06f5bff3952 --- /dev/null +++ b/base_cron_weekday/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- ForgeFlow \<\> diff --git a/base_cron_weekday/readme/DESCRIPTION.md b/base_cron_weekday/readme/DESCRIPTION.md new file mode 100644 index 00000000000..5eecc38b59f --- /dev/null +++ b/base_cron_weekday/readme/DESCRIPTION.md @@ -0,0 +1,11 @@ +This module lets you restrict a scheduled action (cron) from running on +specific weekdays. + +Every scheduled action gains a set of per-weekday checkboxes (all +enabled by default, so behaviour is unchanged until you deselect a day). +When a run would fall on a deselected weekday, it is deferred to the +next allowed day at the same time of day instead of executing. + +A common use case is skipping heavy or external-integration jobs on +weekends, when the remote service publishes nothing and the run has +nothing to do. diff --git a/base_cron_weekday/readme/USAGE.md b/base_cron_weekday/readme/USAGE.md new file mode 100644 index 00000000000..641dbfdd4d5 --- /dev/null +++ b/base_cron_weekday/readme/USAGE.md @@ -0,0 +1,14 @@ +1. Go to *Settings \> Technical \> Automation \> Scheduled Actions* + (developer mode). +2. Open a scheduled action. +3. Under *Run on Weekdays*, deselect the days on which the action must + not run. +4. Save. At least one weekday must stay selected. + +Notes: + +- The weekday is evaluated in the timezone of the action's *Scheduler + User*. +- Only the scheduled cadence is affected. Running an action manually or + through a code trigger still executes it regardless of the selected + weekdays. diff --git a/base_cron_weekday/static/description/index.html b/base_cron_weekday/static/description/index.html new file mode 100644 index 00000000000..6c976f81bba --- /dev/null +++ b/base_cron_weekday/static/description/index.html @@ -0,0 +1,457 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Base Cron Weekday

+ +

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

+

This module lets you restrict a scheduled action (cron) from running on +specific weekdays.

+

Every scheduled action gains a set of per-weekday checkboxes (all +enabled by default, so behaviour is unchanged until you deselect a day). +When a run would fall on a deselected weekday, it is deferred to the +next allowed day at the same time of day instead of executing.

+

A common use case is skipping heavy or external-integration jobs on +weekends, when the remote service publishes nothing and the run has +nothing to do.

+

Table of contents

+ +
+

Usage

+
    +
  1. Go to Settings > Technical > Automation > Scheduled Actions +(developer mode).
  2. +
  3. Open a scheduled action.
  4. +
  5. Under Run on Weekdays, deselect the days on which the action must +not run.
  6. +
  7. Save. At least one weekday must stay selected.
  8. +
+

Notes:

+
    +
  • The weekday is evaluated in the timezone of the action’s Scheduler +User.
  • +
  • Only the scheduled cadence is affected. Running an action manually or +through a code trigger still executes it regardless of the selected +weekdays.
  • +
+
+
+

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 to smash it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • ForgeFlow
  • +
+
+ +
+

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/base_cron_weekday/tests/__init__.py b/base_cron_weekday/tests/__init__.py new file mode 100644 index 00000000000..1f30c1e7edf --- /dev/null +++ b/base_cron_weekday/tests/__init__.py @@ -0,0 +1 @@ +from . import test_cron_weekday diff --git a/base_cron_weekday/tests/test_cron_weekday.py b/base_cron_weekday/tests/test_cron_weekday.py new file mode 100644 index 00000000000..f7989ffa7f1 --- /dev/null +++ b/base_cron_weekday/tests/test_cron_weekday.py @@ -0,0 +1,72 @@ +# Copyright 2026 ForgeFlow +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0). + +from datetime import datetime, timedelta + +from odoo import fields +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase + + +class TestCronWeekday(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.cron = cls.env["ir.cron"].create( + { + "name": "Test Weekday Cron", + "model_id": cls.env.ref("base.model_ir_cron").id, + "state": "code", + "code": "records = model", + "interval_number": 1, + "interval_type": "days", + "nextcall": "2100-01-01 12:00:00", + } + ) + + def _make_job(self, nextcall): + return { + "id": self.cron.id, + "nextcall": nextcall, + "interval_type": self.cron.interval_type, + "interval_number": self.cron.interval_number, + } + + def test_at_least_one_weekday_required(self): + with self.assertRaises(ValidationError): + self.cron.write( + { + "run_monday": False, + "run_tuesday": False, + "run_wednesday": False, + "run_thursday": False, + "run_friday": False, + "run_saturday": False, + "run_sunday": False, + } + ) + + def test_defer_off_disallowed_weekday(self): + """A run landing on a deselected weekday moves to the next allowed day.""" + self.cron.write({"run_saturday": False, "run_sunday": False}) + saturday = datetime(2100, 1, 1, 12, 0, 0) + while saturday.weekday() != 5: + saturday += timedelta(days=1) + self.cron._reschedule_later(self._make_job(saturday)) + self.cron.invalidate_recordset(["nextcall"]) + weekday = fields.Datetime.context_timestamp( + self.cron, self.cron.nextcall + ).weekday() + self.assertNotIn(weekday, (5, 6), "nextcall still lands on a weekend") + self.assertEqual(weekday, 0, "Saturday run should defer to Monday") + + def test_allowed_weekday_unchanged(self): + """When all days are allowed, rescheduling behaves like core.""" + self.assertFalse(self.cron._disallowed_weekdays()) + past = datetime(2000, 1, 1, 12, 0, 0) + now = self.env.cr.now() + self.cron._reschedule_later(self._make_job(past)) + self.cron.invalidate_recordset(["nextcall"]) + self.assertGreater( + self.cron.nextcall, now, "nextcall should advance into the future" + ) diff --git a/base_cron_weekday/views/ir_cron_views.xml b/base_cron_weekday/views/ir_cron_views.xml new file mode 100644 index 00000000000..2966965c02d --- /dev/null +++ b/base_cron_weekday/views/ir_cron_views.xml @@ -0,0 +1,31 @@ + + + + + ir.cron.form.weekday + ir.cron + + + + + + +