From 507ab67620d7a97401f82b9689babe77dbdc0d50 Mon Sep 17 00:00:00 2001 From: Holger Brunn Date: Fri, 21 Mar 2025 07:16:52 +0100 Subject: [PATCH 01/17] [ADD] base_export_async: make attachment accessible to portal users --- base_export_async/models/delay_export.py | 4 ++++ .../tests/test_base_export_async.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/base_export_async/models/delay_export.py b/base_export_async/models/delay_export.py index 549d44a1e1..ee0ae25ab0 100644 --- a/base_export_async/models/delay_export.py +++ b/base_export_async/models/delay_export.py @@ -109,6 +109,10 @@ def export(self, params): attachment.name, ) + if any(user.has_group("base.group_portal") for user in users): + attachment.generate_access_token() + url += f"&access_token={attachment.access_token}" + time_to_live = ( self.env["ir.config_parameter"].sudo().get_param("attachment.ttl", 7) ) diff --git a/base_export_async/tests/test_base_export_async.py b/base_export_async/tests/test_base_export_async.py index d10ca04281..4a2771d0c1 100644 --- a/base_export_async/tests/test_base_export_async.py +++ b/base_export_async/tests/test_base_export_async.py @@ -98,3 +98,22 @@ def test_cron_delete(self): # The attachment must be deleted self.assertFalse(new_attachment.exists()) + + def test_portal_export(self): + """Check that we make attachments externally accessible for portal users""" + portal_user = self.env["res.users"].create( + { + "login": "base_export_async_portal_user", + "name": "base_export_async_portal_user", + "groups_id": self.env.ref("base.group_portal").ids, + } + ) + params = json.loads(data_csv.get("data")) + params["user_ids"] = portal_user.ids + attachments = self.env["ir.attachment"].search([]) + mails = self.env["mail.mail"].search([]) + self.delay_export_obj.export(params) + new_attachment = self.env["ir.attachment"].search([]) - attachments + self.assertTrue(new_attachment.access_token) + new_mail = self.env["mail.mail"].search([]) - mails + self.assertIn("&access_token=", new_mail.body) From 09c3aea3da043e140b1ebdf81316c1f89bc1b94b Mon Sep 17 00:00:00 2001 From: Vincent Hatakeyama Date: Fri, 24 Jan 2025 11:32:02 +0100 Subject: [PATCH 02/17] [FIX] queue_job: indicate that run_job need a read/write connection Without this fix, if db_replica_host is set, Odoo might pass a readonly database cursor and the FOR UPDATE in the method would fail. --- queue_job/controllers/main.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/queue_job/controllers/main.py b/queue_job/controllers/main.py index d1e56e8f77..9ba18e8d29 100644 --- a/queue_job/controllers/main.py +++ b/queue_job/controllers/main.py @@ -74,7 +74,13 @@ def _enqueue_dependent_jobs(self, env, job): else: break - @http.route("/queue_job/runjob", type="http", auth="none", save_session=False) + @http.route( + "/queue_job/runjob", + type="http", + auth="none", + save_session=False, + readonly=False, + ) def runjob(self, db, job_uuid, **kw): http.request.session.db = db env = http.request.env(user=SUPERUSER_ID) From e8ebc9ac1ad0108162b10aaaec40a2196a5e8864 Mon Sep 17 00:00:00 2001 From: Florent Xicluna Date: Fri, 17 Jan 2025 18:40:27 +0100 Subject: [PATCH 03/17] [IMP] queue_job: perform_enqueued_jobs should filter the context --- queue_job/tests/common.py | 8 ++++++++ test_queue_job/tests/test_delay_mocks.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/queue_job/tests/common.py b/queue_job/tests/common.py index 13f1f5f832..51e4fec832 100644 --- a/queue_job/tests/common.py +++ b/queue_job/tests/common.py @@ -256,6 +256,7 @@ def _add_job(self, *args, **kwargs): if not job.identity_key or all( j.identity_key != job.identity_key for j in self.enqueued_jobs ): + self._prepare_context(job) self.enqueued_jobs.append(job) patcher = mock.patch.object(job, "store") @@ -274,6 +275,13 @@ def _add_job(self, *args, **kwargs): ) return job + def _prepare_context(self, job): + # pylint: disable=context-overridden + job_model = job.job_model.with_context({}) + field_records = job_model._fields["records"] + # Filter the context to simulate store/load of the job + job.recordset = field_records.convert_to_write(job.recordset, job_model) + def __enter__(self): return self diff --git a/test_queue_job/tests/test_delay_mocks.py b/test_queue_job/tests/test_delay_mocks.py index 240fb474be..4be7121518 100644 --- a/test_queue_job/tests/test_delay_mocks.py +++ b/test_queue_job/tests/test_delay_mocks.py @@ -268,6 +268,22 @@ def test_trap_jobs_perform(self): self.assertEqual(logs[2].message, "test_trap_jobs_perform graph 3") self.assertEqual(logs[3].message, "test_trap_jobs_perform graph 1") + def test_trap_jobs_prepare_context(self): + # pylint: disable=context-overridden + with trap_jobs() as trap: + model1 = self.env["test.queue.job"].with_context({"config_key": 42}) + model2 = self.env["test.queue.job"].with_context( + {"config_key": 42, "lang": "it_IT"} + ) + model1.with_delay().testing_method("0", "K", return_context=1) + model2.with_delay().testing_method("0", "K", return_context=1) + + [job1, job2] = trap.enqueued_jobs + trap.perform_enqueued_jobs() + + self.assertEqual(job1.result, {"job_uuid": mock.ANY}) + self.assertEqual(job2.result, {"job_uuid": mock.ANY, "lang": "it_IT"}) + def test_mock_with_delay(self): with mock_with_delay() as (delayable_cls, delayable): self.env["test.queue.job"].button_that_uses_with_delay() From b389557aa8f15d59a1bbdde8a712afac9390915d Mon Sep 17 00:00:00 2001 From: Florent Xicluna Date: Tue, 4 Feb 2025 22:12:28 +0100 Subject: [PATCH 04/17] [IMP] queue_job: explain context in docstring --- queue_job/README.rst | 1 + test_queue_job/tests/test_delay_mocks.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/queue_job/README.rst b/queue_job/README.rst index 70a6f226a5..5c05ce9440 100644 --- a/queue_job/README.rst +++ b/queue_job/README.rst @@ -425,6 +425,7 @@ a job, is transferred to the job according to an allow-list. The default allow-list is `("tz", "lang", "allowed_company_ids", "force_company", "active_test")`. It can be customized in ``Base._job_prepare_context_before_enqueue_keys``. + **Bypass jobs on running Odoo** When you are developing (ie: connector modules) you might want diff --git a/test_queue_job/tests/test_delay_mocks.py b/test_queue_job/tests/test_delay_mocks.py index 4be7121518..7427ca3087 100644 --- a/test_queue_job/tests/test_delay_mocks.py +++ b/test_queue_job/tests/test_delay_mocks.py @@ -269,6 +269,12 @@ def test_trap_jobs_perform(self): self.assertEqual(logs[3].message, "test_trap_jobs_perform graph 1") def test_trap_jobs_prepare_context(self): + """Context is transferred to the job according to an allow-list. + + Default allow-list is: + ("tz", "lang", "allowed_company_ids", "force_company", "active_test") + It can be customized in ``Base._job_prepare_context_before_enqueue_keys``. + """ # pylint: disable=context-overridden with trap_jobs() as trap: model1 = self.env["test.queue.job"].with_context({"config_key": 42}) From 312a24cdc770c44ba195d7ee8c25f6a30656ce93 Mon Sep 17 00:00:00 2001 From: Florent Xicluna Date: Mon, 2 Jun 2025 13:50:28 +0200 Subject: [PATCH 05/17] [IMP] queue_job: use __slots__ for ChannelJob It decreases memory footprint when there's thousands of jobs to prioritize. --- queue_job/jobrunner/channels.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/queue_job/jobrunner/channels.py b/queue_job/jobrunner/channels.py index 468fb5760d..e1c0d0b84f 100644 --- a/queue_job/jobrunner/channels.py +++ b/queue_job/jobrunner/channels.py @@ -173,6 +173,8 @@ class ChannelJob: """ + __slots__ = ("db_name", "channel", "uuid", "seq", "date_created", "priority", "eta") + def __init__(self, db_name, channel, uuid, seq, date_created, priority, eta): self.db_name = db_name self.channel = channel From b07868f25ab918e2509ecfae31fb11d63053c3a2 Mon Sep 17 00:00:00 2001 From: Florent Xicluna Date: Tue, 3 Jun 2025 15:08:57 +0200 Subject: [PATCH 06/17] [FIX] queue_job: missing slot for __weakref__ --- queue_job/jobrunner/channels.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/queue_job/jobrunner/channels.py b/queue_job/jobrunner/channels.py index e1c0d0b84f..064ed8e95c 100644 --- a/queue_job/jobrunner/channels.py +++ b/queue_job/jobrunner/channels.py @@ -173,7 +173,16 @@ class ChannelJob: """ - __slots__ = ("db_name", "channel", "uuid", "seq", "date_created", "priority", "eta") + __slots__ = ( + "db_name", + "channel", + "uuid", + "seq", + "date_created", + "priority", + "eta", + "__weakref__", + ) def __init__(self, db_name, channel, uuid, seq, date_created, priority, eta): self.db_name = db_name From d9876864a63ffaa4741f0f7fbf38bd7d32f4b5bd Mon Sep 17 00:00:00 2001 From: Florent Xicluna Date: Mon, 2 Jun 2025 09:45:50 +0200 Subject: [PATCH 07/17] [REF] queue_job: remove deprecated and not used methods --- queue_job/job.py | 71 ------------------------------------------------ 1 file changed, 71 deletions(-) diff --git a/queue_job/job.py b/queue_job/job.py index d8e50e5124..790e07d90e 100644 --- a/queue_job/job.py +++ b/queue_job/job.py @@ -9,7 +9,6 @@ import uuid import weakref from datetime import datetime, timedelta -from functools import total_ordering from random import randint import odoo @@ -104,7 +103,6 @@ def identity_exact_hasher(job_): return hasher -@total_ordering class Job: """A Job is a task to execute. It is the in-memory representation of a job. @@ -367,65 +365,6 @@ def job_record_with_same_identity_key(self): ) return existing - # TODO to deprecate (not called anymore) - @classmethod - def enqueue( - cls, - func, - args=None, - kwargs=None, - priority=None, - eta=None, - max_retries=None, - description=None, - channel=None, - identity_key=None, - ): - """Create a Job and enqueue it in the queue. Return the job uuid. - - This expects the arguments specific to the job to be already extracted - from the ones to pass to the job function. - - If the identity key is the same than the one in a pending job, - no job is created and the existing job is returned - - """ - new_job = cls( - func=func, - args=args, - kwargs=kwargs, - priority=priority, - eta=eta, - max_retries=max_retries, - description=description, - channel=channel, - identity_key=identity_key, - ) - return new_job._enqueue_job() - - # TODO to deprecate (not called anymore) - def _enqueue_job(self): - if self.identity_key: - existing = self.job_record_with_same_identity_key() - if existing: - _logger.debug( - "a job has not been enqueued due to having " - "the same identity key (%s) than job %s", - self.identity_key, - existing.uuid, - ) - return Job._load_from_db_record(existing) - self.store() - _logger.debug( - "enqueued %s:%s(*%r, **%r) with uuid: %s", - self.recordset, - self.method_name, - self.args, - self.kwargs, - self.uuid, - ) - return self - @staticmethod def db_record_from_uuid(env, job_uuid): # TODO remove in 15.0 or 16.0 @@ -749,16 +688,6 @@ def __eq__(self, other): def __hash__(self): return self.uuid.__hash__() - def sorting_key(self): - return self.eta, self.priority, self.date_created, self.seq - - def __lt__(self, other): - if self.eta and not other.eta: - return True - elif not self.eta and other.eta: - return False - return self.sorting_key() < other.sorting_key() - def db_record(self): return self.db_records_from_uuids(self.env, [self.uuid]) From 71befa5a47fe20dc75a5f50c5a7bdb1e1ae53827 Mon Sep 17 00:00:00 2001 From: Pierre Verkest Date: Fri, 14 Mar 2025 00:18:38 +0100 Subject: [PATCH 08/17] =?UTF-8?q?[FIX]=C2=A0queue=5Fjob:=20job=20runner=20?= =?UTF-8?q?open=20pipe=20that=20are=20never=20closed=20properly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit this make a lot of open file descriptors that we got the limit while instentiate jobrunner from queue_job_cron_jobrunner in the current channel implementation https://github.com/OCA/queue/pull/750 --- queue_job/jobrunner/runner.py | 11 ++++++ queue_job/tests/test_runner_runner.py | 49 +++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/queue_job/jobrunner/runner.py b/queue_job/jobrunner/runner.py index 7f935c63d7..8738f6b10a 100644 --- a/queue_job/jobrunner/runner.py +++ b/queue_job/jobrunner/runner.py @@ -431,6 +431,17 @@ def __init__( self._stop = False self._stop_pipe = os.pipe() + def __del__(self): + # pylint: disable=except-pass + try: + os.close(self._stop_pipe[0]) + except OSError: + pass + try: + os.close(self._stop_pipe[1]) + except OSError: + pass + @classmethod def from_environ_or_config(cls): scheme = os.environ.get("ODOO_QUEUE_JOB_SCHEME") or queue_job_config.get( diff --git a/queue_job/tests/test_runner_runner.py b/queue_job/tests/test_runner_runner.py index c6486e27ef..131ce6322d 100644 --- a/queue_job/tests/test_runner_runner.py +++ b/queue_job/tests/test_runner_runner.py @@ -3,8 +3,57 @@ # pylint: disable=odoo-addons-relative-import # we are testing, we want to test as we were an external consumer of the API +import os + +from odoo.tests import BaseCase, tagged + from odoo.addons.queue_job.jobrunner import runner from .common import load_doctests load_tests = load_doctests(runner) + + +@tagged("-at_install", "post_install") +class TestRunner(BaseCase): + @classmethod + def _is_open_file_descriptor(cls, fd): + try: + os.fstat(fd) + return True + except OSError: + return False + + def test_runner_file_descriptor(self): + a_runner = runner.QueueJobRunner.from_environ_or_config() + + read_fd, write_fd = a_runner._stop_pipe + self.assertTrue(self._is_open_file_descriptor(read_fd)) + self.assertTrue(self._is_open_file_descriptor(write_fd)) + + del a_runner + + self.assertFalse(self._is_open_file_descriptor(read_fd)) + self.assertFalse(self._is_open_file_descriptor(write_fd)) + + def test_runner_file_closed_read_descriptor(self): + a_runner = runner.QueueJobRunner.from_environ_or_config() + + read_fd, write_fd = a_runner._stop_pipe + os.close(read_fd) + + del a_runner + + self.assertFalse(self._is_open_file_descriptor(read_fd)) + self.assertFalse(self._is_open_file_descriptor(write_fd)) + + def test_runner_file_closed_write_descriptor(self): + a_runner = runner.QueueJobRunner.from_environ_or_config() + + read_fd, write_fd = a_runner._stop_pipe + os.close(write_fd) + + del a_runner + + self.assertFalse(self._is_open_file_descriptor(read_fd)) + self.assertFalse(self._is_open_file_descriptor(write_fd)) From 59a15d1ed603fb6d94b7b3c244473f0bd78d7398 Mon Sep 17 00:00:00 2001 From: Florent Xicluna Date: Mon, 26 May 2025 15:24:51 +0200 Subject: [PATCH 09/17] [IMP] queue_job: add Priority to Group-By and search --- queue_job/models/queue_job.py | 2 +- queue_job/views/queue_job_views.xml | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/queue_job/models/queue_job.py b/queue_job/models/queue_job.py index 075c8f0501..3587f5499c 100644 --- a/queue_job/models/queue_job.py +++ b/queue_job/models/queue_job.py @@ -91,7 +91,7 @@ class QueueJob(models.Model): func_string = fields.Char(string="Task", readonly=True) state = fields.Selection(STATES, readonly=True, required=True, index=True) - priority = fields.Integer() + priority = fields.Integer(group_operator=False) exc_name = fields.Char(string="Exception", readonly=True) exc_message = fields.Char(string="Exception Message", readonly=True, tracking=True) exc_info = fields.Text(string="Exception Info", readonly=True) diff --git a/queue_job/views/queue_job_views.xml b/queue_job/views/queue_job_views.xml index 1dc77c5e0b..695aa30a2b 100644 --- a/queue_job/views/queue_job_views.xml +++ b/queue_job/views/queue_job_views.xml @@ -168,6 +168,7 @@ /> + @@ -212,6 +213,7 @@ + @@ -284,6 +286,11 @@ string="State" context="{'group_by': 'state'}" /> + Date: Tue, 3 Jun 2025 08:58:27 +0200 Subject: [PATCH 10/17] [IMP] queue_job: more efficient ChannelJob sorting --- queue_job/jobrunner/channels.py | 64 +++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/queue_job/jobrunner/channels.py b/queue_job/jobrunner/channels.py index 064ed8e95c..7fcabbd07b 100644 --- a/queue_job/jobrunner/channels.py +++ b/queue_job/jobrunner/channels.py @@ -2,6 +2,7 @@ # Copyright 2015-2016 Camptocamp SA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) import logging +from collections import namedtuple from functools import total_ordering from heapq import heappop, heappush from weakref import WeakValueDictionary @@ -10,6 +11,7 @@ from ..job import CANCELLED, DONE, ENQUEUED, FAILED, PENDING, STARTED, WAIT_DEPENDENCIES NOT_DONE = (WAIT_DEPENDENCIES, PENDING, ENQUEUED, STARTED, FAILED) +JobSortingKey = namedtuple("SortingKey", "eta priority date_created seq") _logger = logging.getLogger(__name__) @@ -108,7 +110,7 @@ class ChannelJob: job that are necessary to prioritise them. Channel jobs are comparable according to the following rules: - * jobs with an eta come before all other jobs + * jobs with an eta cannot be compared with jobs without * then jobs with a smaller eta come first * then jobs with a smaller priority come first * then jobs with a smaller creation time come first @@ -135,14 +137,18 @@ class ChannelJob: >>> j3 < j1 True - j4 and j5 comes even before j3, because they have an eta + j4 and j5 have an eta, they cannot be compared with j3 >>> j4 = ChannelJob(None, None, 4, ... seq=0, date_created=4, priority=9, eta=9) >>> j5 = ChannelJob(None, None, 5, ... seq=0, date_created=5, priority=9, eta=9) - >>> j4 < j5 < j3 + >>> j4 < j5 True + >>> j4 < j3 + Traceback (most recent call last): + ... + TypeError: '<' not supported between instances of 'int' and 'NoneType' j6 has same date_created and priority as j5 but a smaller eta @@ -153,7 +159,7 @@ class ChannelJob: Here is the complete suite: - >>> j6 < j4 < j5 < j3 < j1 < j2 + >>> j6 < j4 < j5 and j3 < j1 < j2 True j0 has the same properties as j1 but they are not considered @@ -173,25 +179,13 @@ class ChannelJob: """ - __slots__ = ( - "db_name", - "channel", - "uuid", - "seq", - "date_created", - "priority", - "eta", - "__weakref__", - ) + __slots__ = ("db_name", "channel", "uuid", "_sorting_key", "__weakref__") def __init__(self, db_name, channel, uuid, seq, date_created, priority, eta): self.db_name = db_name self.channel = channel self.uuid = uuid - self.seq = seq - self.date_created = date_created - self.priority = priority - self.eta = eta + self._sorting_key = JobSortingKey(eta, priority, date_created, seq) def __repr__(self): return "" % self.uuid @@ -202,18 +196,36 @@ def __eq__(self, other): def __hash__(self): return id(self) + def set_no_eta(self): + self._sorting_key = JobSortingKey(None, *self._sorting_key[1:]) + + @property + def seq(self): + return self._sorting_key.seq + + @property + def date_created(self): + return self._sorting_key.date_created + + @property + def priority(self): + return self._sorting_key.priority + + @property + def eta(self): + return self._sorting_key.eta + def sorting_key(self): - return self.eta, self.priority, self.date_created, self.seq + # DEPRECATED + return self._sorting_key def sorting_key_ignoring_eta(self): - return self.priority, self.date_created, self.seq + return self._sorting_key[1:] def __lt__(self, other): - if self.eta and not other.eta: - return True - elif not self.eta and other.eta: - return False - return self.sorting_key() < other.sorting_key() + # Do not compare job where ETA is set with job where it is not + # If one job 'eta' is set, and the other is None, it raises TypeError + return self._sorting_key < other._sorting_key class ChannelQueue: @@ -323,7 +335,7 @@ def remove(self, job): def pop(self, now): while self._eta_queue and self._eta_queue[0].eta <= now: eta_job = self._eta_queue.pop() - eta_job.eta = None + eta_job.set_no_eta() self._queue.add(eta_job) if self.sequential and self._eta_queue and self._queue: eta_job = self._eta_queue[0] From 8cb59283215b769f903ecd4623099c9954dd570c Mon Sep 17 00:00:00 2001 From: Florent Xicluna Date: Wed, 11 Jun 2025 11:52:51 +0200 Subject: [PATCH 11/17] [IMP] queue_job: filter for retried jobs --- queue_job/views/queue_job_views.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/queue_job/views/queue_job_views.xml b/queue_job/views/queue_job_views.xml index 695aa30a2b..3984c39c85 100644 --- a/queue_job/views/queue_job_views.xml +++ b/queue_job/views/queue_job_views.xml @@ -255,6 +255,12 @@ domain="[('state', '=', 'cancelled')]" /> + + Date: Wed, 11 Jun 2025 12:13:08 +0200 Subject: [PATCH 12/17] [IMP] queue_job: set the columns optional in list view --- queue_job/views/queue_job_views.xml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/queue_job/views/queue_job_views.xml b/queue_job/views/queue_job_views.xml index 3984c39c85..7f374d8ba8 100644 --- a/queue_job/views/queue_job_views.xml +++ b/queue_job/views/queue_job_views.xml @@ -157,7 +157,7 @@ decoration-muted="state == 'done'" > - + - - + + - - - - + + + + From bd48ae612db8922b199ce532d510313eb5c9225c Mon Sep 17 00:00:00 2001 From: Florent Xicluna Date: Wed, 25 Jun 2025 09:24:52 +0200 Subject: [PATCH 13/17] [IMP] queue_job: add index for efficient autovacuum --- queue_job/models/queue_job.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/queue_job/models/queue_job.py b/queue_job/models/queue_job.py index 3587f5499c..df33e2c7c5 100644 --- a/queue_job/models/queue_job.py +++ b/queue_job/models/queue_job.py @@ -6,7 +6,7 @@ from datetime import datetime, timedelta from odoo import _, api, exceptions, fields, models -from odoo.tools import config, html_escape +from odoo.tools import config, html_escape, index_exists from odoo.addons.base_sparse_field.models.fields import Serialized @@ -130,16 +130,21 @@ class QueueJob(models.Model): worker_pid = fields.Integer(readonly=True) def init(self): - self._cr.execute( - "SELECT indexname FROM pg_indexes WHERE indexname = %s ", - ("queue_job_identity_key_state_partial_index",), - ) - if not self._cr.fetchone(): + index_1 = "queue_job_identity_key_state_partial_index" + index_2 = "queue_job_channel_date_done_date_created_index" + if not index_exists(self._cr, index_1): + # Used by Job.job_record_with_same_identity_key self._cr.execute( "CREATE INDEX queue_job_identity_key_state_partial_index " "ON queue_job (identity_key) WHERE state in ('pending', " "'enqueued', 'wait_dependencies') AND identity_key IS NOT NULL;" ) + if not index_exists(self._cr, index_2): + # Used by .autovacuum + self._cr.execute( + "CREATE INDEX queue_job_channel_date_done_date_created_index " + "ON queue_job (channel, date_done, date_created);" + ) @api.depends("records") def _compute_record_ids(self): @@ -408,6 +413,7 @@ def autovacuum(self): ("date_cancelled", "<=", deadline), ("channel", "=", channel.complete_name), ], + order="date_done, date_created", limit=1000, ) if jobs: From b68be0eea7f6665b9ca02d836d7f011f63ebd8ec Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 2 Jan 2026 16:48:20 +0000 Subject: [PATCH 14/17] [BOT] post-merge updates --- README.md | 2 +- base_export_async/README.rst | 8 ++++-- base_export_async/__manifest__.py | 2 +- .../static/description/index.html | 28 +++++++++++-------- 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index aa4e119ecb..660b868040 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Available addons ---------------- addon | version | maintainers | summary --- | --- | --- | --- -[base_export_async](base_export_async/) | 16.0.1.1.0 | | Asynchronous export with job queue +[base_export_async](base_export_async/) | 16.0.1.2.0 | | Asynchronous export with job queue [base_import_async](base_import_async/) | 16.0.1.2.0 | | Import CSV files in the background [queue_job](queue_job/) | 16.0.2.11.5 | guewen | Job Queue [queue_job_batch](queue_job_batch/) | 16.0.1.0.1 | | Job Queue Batch diff --git a/base_export_async/README.rst b/base_export_async/README.rst index 0863ca6273..3cf70f08d2 100644 --- a/base_export_async/README.rst +++ b/base_export_async/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ================= Base Export Async ================= @@ -7,13 +11,13 @@ Base Export Async !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:85ce335207ce3505a7676a8a78743155f9f8c01d1c6f777fe2308c5e77f00e5a + !! source digest: sha256:da52a05130a89cd0000a39c9dd6315821387de2c310f9cf7cc1f7e6e8541fe55 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |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 +.. |badge2| image:: https://img.shields.io/badge/license-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%2Fqueue-lightgray.png?logo=github diff --git a/base_export_async/__manifest__.py b/base_export_async/__manifest__.py index 04975d5bf3..f43ed29632 100644 --- a/base_export_async/__manifest__.py +++ b/base_export_async/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Base Export Async", "summary": "Asynchronous export with job queue", - "version": "16.0.1.1.0", + "version": "16.0.1.2.0", "license": "AGPL-3", "author": "ACSONE SA/NV, Odoo Community Association (OCA)", "website": "https://github.com/OCA/queue", diff --git a/base_export_async/static/description/index.html b/base_export_async/static/description/index.html index 6aa89b8a93..147c0c8c2b 100644 --- a/base_export_async/static/description/index.html +++ b/base_export_async/static/description/index.html @@ -3,7 +3,7 @@ -Base Export Async +README.rst -
-

Base Export Async

+
+ + +Odoo Community Association + +
+

Base Export Async

-

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

+

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

Standard Export can be delayed in asynchronous jobs executed in the background and then send by email to the user.

Table of contents

@@ -385,7 +390,7 @@

Base Export Async

-

Usage

+

Usage

The user is presented with a new checkbox “Asynchronous export” in the export screen. When selected, the export is delayed in a background job.

@@ -393,7 +398,7 @@

Usage

to the user who execute the export.

-

Bug Tracker

+

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 @@ -401,22 +406,22 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • ACSONE SA/NV
-

Contributors

+

Contributors

  • Arnaud Pineux (ACSONE SA/NV) authored the initial prototype.
  • Guewen Baconnier (Camptocamp)
-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -429,5 +434,6 @@

Maintainers

+
From 6caf9e50acc89b22952305698412725b5e10e62d Mon Sep 17 00:00:00 2001 From: oca-ci Date: Fri, 2 Jan 2026 17:02:29 +0000 Subject: [PATCH 15/17] [UPD] Update queue_job.pot --- queue_job/i18n/queue_job.pot | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/queue_job/i18n/queue_job.pot b/queue_job/i18n/queue_job.pot index a64310a5c6..3a3462af74 100644 --- a/queue_job/i18n/queue_job.pot +++ b/queue_job/i18n/queue_job.pot @@ -661,6 +661,7 @@ msgstr "" #. module: queue_job #: model:ir.model.fields,field_description:queue_job.field_queue_job__priority +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search msgid "Priority" msgstr "" @@ -887,6 +888,11 @@ msgstr "" msgid "Time required to execute this job in seconds. Average when grouped." msgstr "" +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Tried many times" +msgstr "" + #. module: queue_job #: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration msgid "Type of the exception activity on record." From 19e2e22c624046c6d1425528fd573b0f7cec2d51 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 2 Jan 2026 17:04:28 +0000 Subject: [PATCH 16/17] [BOT] post-merge updates --- README.md | 4 ++-- queue_job/README.rst | 3 +-- queue_job/__manifest__.py | 2 +- queue_job/static/description/index.html | 2 +- test_queue_job/__manifest__.py | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 660b868040..a71cc330cf 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,13 @@ addon | version | maintainers | summary --- | --- | --- | --- [base_export_async](base_export_async/) | 16.0.1.2.0 | | Asynchronous export with job queue [base_import_async](base_import_async/) | 16.0.1.2.0 | | Import CSV files in the background -[queue_job](queue_job/) | 16.0.2.11.5 | guewen | Job Queue +[queue_job](queue_job/) | 16.0.2.12.0 | guewen | Job Queue [queue_job_batch](queue_job_batch/) | 16.0.1.0.1 | | Job Queue Batch [queue_job_cron](queue_job_cron/) | 16.0.2.1.0 | | Scheduled Actions as Queue Jobs [queue_job_cron_jobrunner](queue_job_cron_jobrunner/) | 16.0.1.1.0 | ivantodorovich | Run jobs without a dedicated JobRunner [queue_job_subscribe](queue_job_subscribe/) | 16.0.1.1.0 | | Control which users are subscribed to queue job notifications [queue_job_web_notify](queue_job_web_notify/) | 16.0.1.0.0 | | This module allows to display a notification to the related user of a failed job. It uses the web_notify notification feature. -[test_queue_job](test_queue_job/) | 16.0.2.3.0 | | Queue Job Tests +[test_queue_job](test_queue_job/) | 16.0.2.4.0 | | Queue Job Tests [test_queue_job_batch](test_queue_job_batch/) | 16.0.1.0.0 | | Test Job Queue Batch diff --git a/queue_job/README.rst b/queue_job/README.rst index ea17db8255..f22fd7bc10 100644 --- a/queue_job/README.rst +++ b/queue_job/README.rst @@ -11,7 +11,7 @@ Job Queue !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:d14c52037a007ed26c3868531df1cef148fcc85fb5cee8b35e34f3522879cc0f + !! source digest: sha256:b92d06dbbf161572f2bf02e0c6a59282cea11cc5e903378094bead986f0125de !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Mature-brightgreen.png @@ -429,7 +429,6 @@ a job, is transferred to the job according to an allow-list. The default allow-list is `("tz", "lang", "allowed_company_ids", "force_company", "active_test")`. It can be customized in ``Base._job_prepare_context_before_enqueue_keys``. - **Bypass jobs on running Odoo** When you are developing (ie: connector modules) you might want diff --git a/queue_job/__manifest__.py b/queue_job/__manifest__.py index c7d9138637..f32b20e2e2 100644 --- a/queue_job/__manifest__.py +++ b/queue_job/__manifest__.py @@ -2,7 +2,7 @@ { "name": "Job Queue", - "version": "16.0.2.11.5", + "version": "16.0.2.12.0", "author": "Camptocamp,ACSONE SA/NV,Odoo Community Association (OCA)", "website": "https://github.com/OCA/queue", "license": "LGPL-3", diff --git a/queue_job/static/description/index.html b/queue_job/static/description/index.html index a194f69546..82bed11d0f 100644 --- a/queue_job/static/description/index.html +++ b/queue_job/static/description/index.html @@ -372,7 +372,7 @@

Job Queue

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:d14c52037a007ed26c3868531df1cef148fcc85fb5cee8b35e34f3522879cc0f +!! source digest: sha256:b92d06dbbf161572f2bf02e0c6a59282cea11cc5e903378094bead986f0125de !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Mature License: LGPL-3 OCA/queue Translate me on Weblate Try me on Runboat

This addon adds an integrated Job Queue to Odoo.

diff --git a/test_queue_job/__manifest__.py b/test_queue_job/__manifest__.py index e19d2cd87d..c3a29bf0c5 100644 --- a/test_queue_job/__manifest__.py +++ b/test_queue_job/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Queue Job Tests", - "version": "16.0.2.3.0", + "version": "16.0.2.4.0", "author": "Camptocamp,Odoo Community Association (OCA)", "license": "LGPL-3", "category": "Generic Modules", From bed49b23a75e8b661a4261b9ebcb8d8cc156e0fe Mon Sep 17 00:00:00 2001 From: Weblate Date: Fri, 2 Jan 2026 17:06:36 +0000 Subject: [PATCH 17/17] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: queue-16.0/queue-16.0-queue_job Translate-URL: https://translation.odoo-community.org/projects/queue-16-0/queue-16-0-queue_job/ --- queue_job/i18n/ca.po | 6 ++++++ queue_job/i18n/de.po | 10 ++++++++-- queue_job/i18n/es.po | 6 ++++++ queue_job/i18n/fr.po | 6 ++++++ queue_job/i18n/it.po | 6 ++++++ queue_job/i18n/tr.po | 6 ++++++ queue_job/i18n/zh_CN.po | 6 ++++++ 7 files changed, 44 insertions(+), 2 deletions(-) diff --git a/queue_job/i18n/ca.po b/queue_job/i18n/ca.po index 9d6362e6a9..1a4ae1c1f5 100644 --- a/queue_job/i18n/ca.po +++ b/queue_job/i18n/ca.po @@ -671,6 +671,7 @@ msgstr "Pendent" #. module: queue_job #: model:ir.model.fields,field_description:queue_job.field_queue_job__priority +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search msgid "Priority" msgstr "Prioritat" @@ -904,6 +905,11 @@ msgid "Time required to execute this job in seconds. Average when grouped." msgstr "" "Temps necessari per executar el treball en segons. Mitjana quan s'agrupa." +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Tried many times" +msgstr "" + #. module: queue_job #: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration msgid "Type of the exception activity on record." diff --git a/queue_job/i18n/de.po b/queue_job/i18n/de.po index 761b5f46f1..2717a70447 100644 --- a/queue_job/i18n/de.po +++ b/queue_job/i18n/de.po @@ -683,6 +683,7 @@ msgstr "Ausstehend" #. module: queue_job #: model:ir.model.fields,field_description:queue_job.field_queue_job__priority +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search msgid "Priority" msgstr "Priorität" @@ -929,6 +930,11 @@ msgstr "" "Benötigte Zeit zur Ausführung dieses Jobs in Sekunden. Bei Gruppierung wird " "der Durchschnitt ermittelt." +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Tried many times" +msgstr "" + #. module: queue_job #: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration msgid "Type of the exception activity on record." @@ -951,8 +957,8 @@ msgid "" msgstr "" "Unerwartetes Format der zugehörigen Aktion für {}.\n" "Beispiel für ein gültiges Format:\n" -"{{\"enable\": True, \"func_name\": \"related_action_foo\", \"kwargs\" {{" -"\"limit\": 10}}}}" +"{{\"enable\": True, \"func_name\": \"related_action_foo\", " +"\"kwargs\" {{\"limit\": 10}}}}" #. module: queue_job #. odoo-python diff --git a/queue_job/i18n/es.po b/queue_job/i18n/es.po index 35547c9138..0580170ec6 100644 --- a/queue_job/i18n/es.po +++ b/queue_job/i18n/es.po @@ -679,6 +679,7 @@ msgstr "Pendiente" #. module: queue_job #: model:ir.model.fields,field_description:queue_job.field_queue_job__priority +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search msgid "Priority" msgstr "Prioridad" @@ -923,6 +924,11 @@ msgstr "" "Tiempo requerido para ejecutar este trabajo en segundos. Promedio cuando se " "agrupa." +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Tried many times" +msgstr "" + #. module: queue_job #: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration msgid "Type of the exception activity on record." diff --git a/queue_job/i18n/fr.po b/queue_job/i18n/fr.po index ab7eeb1b6b..0a03d545ef 100644 --- a/queue_job/i18n/fr.po +++ b/queue_job/i18n/fr.po @@ -678,6 +678,7 @@ msgstr "En attente" #. module: queue_job #: model:ir.model.fields,field_description:queue_job.field_queue_job__priority +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search msgid "Priority" msgstr "Priorité" @@ -923,6 +924,11 @@ msgstr "" "Temps requis pour exécuter cette tâche en seconde. Moyenne lors des " "regroupements." +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Tried many times" +msgstr "" + #. module: queue_job #: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration msgid "Type of the exception activity on record." diff --git a/queue_job/i18n/it.po b/queue_job/i18n/it.po index 25373a1f35..4f66a2f2a4 100644 --- a/queue_job/i18n/it.po +++ b/queue_job/i18n/it.po @@ -678,6 +678,7 @@ msgstr "In attesa" #. module: queue_job #: model:ir.model.fields,field_description:queue_job.field_queue_job__priority +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search msgid "Priority" msgstr "Priorità" @@ -920,6 +921,11 @@ msgid "Time required to execute this job in seconds. Average when grouped." msgstr "" "Tempo in secondi richiesto per eseguire il lavoro. Medio quando raggruppati." +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Tried many times" +msgstr "" + #. module: queue_job #: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration msgid "Type of the exception activity on record." diff --git a/queue_job/i18n/tr.po b/queue_job/i18n/tr.po index a76e2c1ac6..ba8e6bff69 100644 --- a/queue_job/i18n/tr.po +++ b/queue_job/i18n/tr.po @@ -678,6 +678,7 @@ msgstr "Beklemede" #. module: queue_job #: model:ir.model.fields,field_description:queue_job.field_queue_job__priority +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search msgid "Priority" msgstr "Öncelik" @@ -920,6 +921,11 @@ msgstr "" "Saniye cinsinden bu işi yapmak için gereken süre. Gruplandığında ortalaması " "alınır." +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Tried many times" +msgstr "" + #. module: queue_job #: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration msgid "Type of the exception activity on record." diff --git a/queue_job/i18n/zh_CN.po b/queue_job/i18n/zh_CN.po index 610288b6d8..f75cca8176 100644 --- a/queue_job/i18n/zh_CN.po +++ b/queue_job/i18n/zh_CN.po @@ -670,6 +670,7 @@ msgstr "等待" #. module: queue_job #: model:ir.model.fields,field_description:queue_job.field_queue_job__priority +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search msgid "Priority" msgstr "优先级" @@ -906,6 +907,11 @@ msgstr "" msgid "Time required to execute this job in seconds. Average when grouped." msgstr "" +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Tried many times" +msgstr "" + #. module: queue_job #: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration msgid "Type of the exception activity on record."