Skip to content

[19.0][IMP] edi_queue_oca: add timezone-aware daily ETA scheduling#312

Open
Ricardoalso wants to merge 2 commits into
OCA:19.0from
camptocamp:edi_queue_oca_eta
Open

[19.0][IMP] edi_queue_oca: add timezone-aware daily ETA scheduling#312
Ricardoalso wants to merge 2 commits into
OCA:19.0from
camptocamp:edi_queue_oca_eta

Conversation

@Ricardoalso

@Ricardoalso Ricardoalso commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

This pull request introduces an enhancement to the EDI Queue OCA module by allowing EDI exchange jobs to be scheduled and accumulated for release at a specific daily time, in addition to existing per-type channel and priority configuration.

New job scheduling and accumulation features:

  • Added fields to edi.exchange.type for enabling ETA scheduling, configuring execution hour, minute, and timezone, and logic to compute the next scheduled runtime in UTC. This allows all jobs for a given exchange type to be held and released together at a fixed time each day, supporting off-peak processing and trading partner requirements.
  • Modified job dispatch logic so that, if ETA scheduling is enabled, jobs are queued with the calculated ETA; otherwise, jobs are dispatched immediately as before.

Accumulating jobs until a fixed daily time

Enabling ETA scheduling on an exchange type causes every job created for that type to be held in the queue until the configured time of day, rather than being processed immediately. All jobs that arrive during the day accumulate and are released together at that scheduled time.

Typical use cases:

  • A trading partner's receiving system processes incoming files only during a specific nightly window (e.g., 22:00).
  • Resource-intensive EDI operations (large exports, heavy transformations) are deferred to off-peak hours to avoid competing with daytime workloads.
  • An operational preference to send a batch of documents at a predictable daily time instead of dispatching them one by one in real time.

The execution time is configured using three fields:

  • Hour — hour of the day (00–23)
  • Minute — minute of the hour (00–59)
  • Timezone — the timezone in which the hour and minute are interpreted; defaults to the current user's timezone

At runtime, the configured time is converted to the next matching UTC datetime and set as the queue job ETA. If the target time for the current day has already passed, the job is automatically scheduled for the same time on the next day.

When ETA scheduling is disabled, jobs are dispatched immediately as usual.

@Ricardoalso Ricardoalso force-pushed the edi_queue_oca_eta branch 4 times, most recently from d376ffd to e36337d Compare June 22, 2026 09:54
Comment thread edi_queue_oca/models/edi_exchange_record.py Outdated
Comment thread edi_queue_oca/models/edi_exchange_type.py Outdated
Comment thread edi_queue_oca/models/edi_exchange_type.py Outdated

@simahawk simahawk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to the existing remarks

Comment thread edi_queue_oca/models/edi_exchange_record.py Outdated
job_priority = fields.Integer()
eta_time = fields.Float(
string="Execute at (timezone aware)",
help="Hour of the day (decimal) at which jobs for this type should be "

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that for a certain type you want jobs to accumulate till a certain hour, correct?
What is the use-case? It would be nice to mention that in the readme as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly, my specific business use-case is to accumulate resource-intensive EDI work into off-peak hours.
I pushed a fixup commit to document it

@SilvioC2C SilvioC2C left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is good, but I would go with a cleaner approach:

  • use separate fields for hours and minutes instead of parsing strings
  • let the user define the ETA TZ
  • don't add utils, let the record itself compute its own ETA

"fixed daily window, or to concentrate resource-intensive EDI work in "
"off-peak hours.",
)
eta_time = fields.Float(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I'd split this field into 2 different fields, one for hours (values from 0 to 23) and another for minutes (values from 0 to 59), and add a field eta_tz that defines the timezone; then, simply have a computed, non-stored field to compute the ETA:

from datetime import datetime
from dateutil.relativedelta import relativedelta
from pytz import timezone, utc

from odoo import api, fields, models
from odoo.addons.base.models.res_partner import _tzs as timezone_selection


class EdiExchangeType(models.Model):
    [...]

    eta_hour = fields.Selection(
        list((x, str(x).zfill(2)) for x in range(24))),
        string="Execution time - hour",
        help="Hour of the day at which jobs for this type should be scheduled.",
        required=True,
        default="00",
    )
    eta_minute = fields.Selection(
        list((x, str(x).zfill(2)) for x in range(60))),
        string="Execution time - minute",
        help="Minute of the hour at which jobs for this type should be scheduled.",
        required=True,
        default="00",
    )
    eta_tz = fields.Selection(
        timezone_selection,
        string="Execution time - timezone",
        help="ETA's timezone for jobs of this type",
        required=True,
        default=lambda self: self._get_default_eta_tz(),
    )

    @api.model
    def _get_default_eta_tz(self):
        return self.env.user.tz or (timezone_selection and timezone_selection[0]) or "UTC"

    def _get_eta(self):
        """Returns the ETA for the current type as timezone-naive datetime"""
        self.ensure_one()
        now = utc.localize(datetime.now())
        eta = timezone(self.eta_time_tz).localize(datetime(now.year, now.month, now.day, self.eta_time_hour, self.eta_time_minute)).astimezone(utc)
        if eta < now:
            eta += relativedelta(days=1)
        return eta.replace(tzinfo=None)

@Ricardoalso Ricardoalso Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestions! I initially took the more user-friendly approach using float_time, which is also used across core Odoo modules.
But I like the simplicity of having both eta_hour and eta_minute fields. Since this is linked to a technical setting, I think we can trade user-friendliness for a simpler and more efficient design.

Comment thread edi_queue_oca/models/edi_exchange_type.py Outdated

@grindtildeath grindtildeath left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small things

Comment thread edi_queue_oca/models/edi_exchange_type.py Outdated
Comment thread edi_queue_oca/utils.py Outdated
Comment on lines +34 to +37
# Use timedelta from midnight rather than datetime.replace(hour=...) so that
# values near 24.0 (e.g. 23.992 → 1440 total minutes) wrap cleanly to 00:00
# the next day instead of crashing with ValueError: hour must be in 0..23.
total_minutes = round(eta_time * 60) % 1440

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it would be cleaner to build a datetime.time object that you can use to combine with datetime object. cf https://github.com/OCA/server-tools/blob/19.0/base_time_window/models/time_window_mixin.py#L125-L128 (to convert the float_time to time object)

@Ricardoalso Ricardoalso Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this code out ! 🚀
I finally decided to take the approach suggested by Silvio #312 (comment)

@grindtildeath grindtildeath left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks much better 👍

Comment thread edi_queue_oca/models/edi_exchange_type.py Outdated
Comment thread edi_queue_oca/tests/test_record.py
Comment thread edi_queue_oca/tests/test_record.py Outdated
self.exchange_type_in.job_eta_hour = "22"
self.exchange_type_in.job_eta_minute = "00"
# job_eta_enabled defaults to False
record = self._make_record()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpicking: the record do not have any impact on this feature, it seems useless to create one record all the times. It could be done at class setup.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, I took the opportunity to make a small improvement while squashing 😉

@simahawk

Copy link
Copy Markdown
Contributor

squash pls

@OCA-git-bot

Copy link
Copy Markdown
Contributor

This PR has the approved label and has been created more than 5 days ago. It should therefore be ready to merge by a maintainer (or a PSC member if the concerned addon has no declared maintainer). 🤖

Add a configurable daily execution time on EDI exchange types and inject the
corresponding ETA into queued exchange record jobs.

Added fields to `edi.exchange.type` for enabling ETA scheduling, configuring execution hour, minute, and timezone, and logic to compute the next scheduled runtime in UTC. This allows all jobs for a given exchange type to be held and released together at a fixed time each day, supporting off-peak processing and trading partner requirements.

Enabling ETA scheduling on an exchange type causes every job created for that type to be held in the queue until the configured time of day, rather than being processed immediately. All jobs that arrive during the day accumulate and are released together at that scheduled time.

When ETA scheduling is disabled, jobs are dispatched immediately as usual
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants