From 1dcdc23fbfc1449920143f9a00d97fd80f21568e Mon Sep 17 00:00:00 2001
From: Quentin Groulard
Date: Fri, 27 Feb 2026 12:15:27 +0100
Subject: [PATCH 01/27] [FIX] queue_job_batch: multi company rule
Adapt to the new multi company system.
---
queue_job_batch/security/security.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/queue_job_batch/security/security.xml b/queue_job_batch/security/security.xml
index 9443708ae0..a033a049c7 100644
--- a/queue_job_batch/security/security.xml
+++ b/queue_job_batch/security/security.xml
@@ -20,7 +20,7 @@
['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]
+ >[('company_id', 'in', company_ids + [False])]
From 4038ecb0dde1dd0fcc2cea6af5572f3fbf9ce892 Mon Sep 17 00:00:00 2001
From: Simone Orsi
Date: Wed, 11 Feb 2026 18:40:20 +0100
Subject: [PATCH 02/27] Add queue_job_profiler
---
queue_job_profiler/README.rst | 96 ++++
queue_job_profiler/__init__.py | 2 +
queue_job_profiler/__manifest__.py | 19 +
queue_job_profiler/controllers/__init__.py | 1 +
queue_job_profiler/controllers/main.py | 28 ++
queue_job_profiler/models/__init__.py | 2 +
queue_job_profiler/models/queue_job.py | 44 ++
.../models/queue_job_function.py | 48 ++
queue_job_profiler/pyproject.toml | 3 +
queue_job_profiler/readme/CONTRIBUTORS.md | 2 +
queue_job_profiler/readme/DESCRIPTION.md | 7 +
queue_job_profiler/readme/USAGE.md | 8 +
.../static/description/index.html | 445 ++++++++++++++++++
queue_job_profiler/tests/__init__.py | 1 +
queue_job_profiler/tests/test_job_profiler.py | 134 ++++++
.../views/queue_job_function_views.xml | 25 +
queue_job_profiler/views/queue_job_views.xml | 25 +
17 files changed, 890 insertions(+)
create mode 100644 queue_job_profiler/README.rst
create mode 100644 queue_job_profiler/__init__.py
create mode 100644 queue_job_profiler/__manifest__.py
create mode 100644 queue_job_profiler/controllers/__init__.py
create mode 100644 queue_job_profiler/controllers/main.py
create mode 100644 queue_job_profiler/models/__init__.py
create mode 100644 queue_job_profiler/models/queue_job.py
create mode 100644 queue_job_profiler/models/queue_job_function.py
create mode 100644 queue_job_profiler/pyproject.toml
create mode 100644 queue_job_profiler/readme/CONTRIBUTORS.md
create mode 100644 queue_job_profiler/readme/DESCRIPTION.md
create mode 100644 queue_job_profiler/readme/USAGE.md
create mode 100644 queue_job_profiler/static/description/index.html
create mode 100644 queue_job_profiler/tests/__init__.py
create mode 100644 queue_job_profiler/tests/test_job_profiler.py
create mode 100644 queue_job_profiler/views/queue_job_function_views.xml
create mode 100644 queue_job_profiler/views/queue_job_views.xml
diff --git a/queue_job_profiler/README.rst b/queue_job_profiler/README.rst
new file mode 100644
index 0000000000..9765639adc
--- /dev/null
+++ b/queue_job_profiler/README.rst
@@ -0,0 +1,96 @@
+==================
+Job Queue Profiler
+==================
+
+..
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! source digest: sha256:b6e605728ea05be0bf98dbe6e94c197bab21882e965286ef214f719e252b444e
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |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%2Fqueue-lightgray.png?logo=github
+ :target: https://github.com/OCA/queue/tree/18.0/queue_job_profiler
+ :alt: OCA/queue
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+ :target: https://translation.odoo-community.org/projects/queue-18-0/queue-18-0-queue_job_profiler
+ :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/queue&target_branch=18.0
+ :alt: Try me on Runboat
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+This addon adds profiling controls to queue job functions and wraps
+queue job execution in an Odoo profiler session when enabled.
+
+When profiling is enabled for a job function and the executing user
+matches the configured profiling user, the queue job runner records SQL
+and async stack traces via ``odoo.tools.profiler.Profiler`` and saves
+the results into ``ir_profile``.
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Usage
+=====
+
+1. Open the Queue Job Functions menu.
+2. Open the job function you want to profile.
+3. In the Profiler group, enable Profiling and set the Profiling user
+ and Profiling until.
+4. Run the job with the selected user (the queue job runner usually
+ executes as the superuser).
+5. Inspect the generated entries in ``ir_profile`` to review the
+ captured profiling data.
+
+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
+-------
+
+* Camptocamp
+
+Contributors
+------------
+
+- `Camptocamp `__:
+
+ - Simone Orsi
+
+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/queue `_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/queue_job_profiler/__init__.py b/queue_job_profiler/__init__.py
new file mode 100644
index 0000000000..91c5580fed
--- /dev/null
+++ b/queue_job_profiler/__init__.py
@@ -0,0 +1,2 @@
+from . import controllers
+from . import models
diff --git a/queue_job_profiler/__manifest__.py b/queue_job_profiler/__manifest__.py
new file mode 100644
index 0000000000..7db4b83454
--- /dev/null
+++ b/queue_job_profiler/__manifest__.py
@@ -0,0 +1,19 @@
+# Copyright 2026 Camptocamp SA (https://www.camptocamp.com).
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl
+
+{
+ "name": "Job Queue Profiler",
+ "version": "18.0.1.0.0",
+ "author": "Camptocamp, Odoo Community Association (OCA)",
+ "maintainers": ["simahawk"],
+ "website": "https://github.com/OCA/queue",
+ "license": "AGPL-3",
+ "category": "Generic Modules",
+ "depends": [
+ "queue_job",
+ ],
+ "data": [
+ "views/queue_job_function_views.xml",
+ "views/queue_job_views.xml",
+ ],
+}
diff --git a/queue_job_profiler/controllers/__init__.py b/queue_job_profiler/controllers/__init__.py
new file mode 100644
index 0000000000..12a7e529b6
--- /dev/null
+++ b/queue_job_profiler/controllers/__init__.py
@@ -0,0 +1 @@
+from . import main
diff --git a/queue_job_profiler/controllers/main.py b/queue_job_profiler/controllers/main.py
new file mode 100644
index 0000000000..08c8097634
--- /dev/null
+++ b/queue_job_profiler/controllers/main.py
@@ -0,0 +1,28 @@
+# Copyright 2026 Camptocamp SA (https://www.camptocamp.com).
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo.tools.profiler import Profiler
+
+from odoo.addons.queue_job.controllers.main import RunJobController as BaseController
+
+
+class RunJobController(BaseController):
+ def _try_perform_job(self, env, job):
+ if self._profiler_is_enabled(env, job):
+ return self._profiler_perform_job(env, job)
+ return super()._try_perform_job(env, job)
+
+ def _profiler_is_enabled(self, env, job):
+ func_id = job.job_config.job_function_id
+ job_function = env["queue.job.function"].browse(func_id)
+ return job_function.is_profiling_enabled()
+
+ def _profiler_perform_job(self, env, job):
+ with self._profiler_get(env, job):
+ result = super()._try_perform_job(env, job)
+ return result
+
+ def _profiler_get(self, env, job):
+ func_id = job.job_config.job_function_id
+ job_function = env["queue.job.function"].browse(func_id)
+ return Profiler(description=job_function._profile_make_name(job))
diff --git a/queue_job_profiler/models/__init__.py b/queue_job_profiler/models/__init__.py
new file mode 100644
index 0000000000..2cf4503488
--- /dev/null
+++ b/queue_job_profiler/models/__init__.py
@@ -0,0 +1,2 @@
+from . import queue_job_function
+from . import queue_job
diff --git a/queue_job_profiler/models/queue_job.py b/queue_job_profiler/models/queue_job.py
new file mode 100644
index 0000000000..04c1b893ee
--- /dev/null
+++ b/queue_job_profiler/models/queue_job.py
@@ -0,0 +1,44 @@
+# Copyright 2026 Camptocamp SA
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
+
+from odoo import fields, models
+
+
+class QueueJob(models.Model):
+ _inherit = "queue.job"
+
+ job_is_profiled = fields.Boolean(
+ string="Profiled",
+ default=False,
+ help="Whether this job has been profiled or not.",
+ compute="_compute_job_is_profiled",
+ compute_sudo=True,
+ )
+
+ def _compute_job_is_profiled(self):
+ for job in self:
+ # don't care about perf as this is loaded only on the job form view
+ profile_name = job.job_function_id._profile_make_name(job)
+ job.job_is_profiled = bool(job._profiler_get_record(profile_name))
+
+ def _profiler_get_record(self, profile_name):
+ IrProfile = self.env["ir.profile"].sudo()
+ return IrProfile.search(
+ [("name", "=", profile_name)],
+ limit=1,
+ )
+
+ def action_view_profile(self):
+ self.ensure_one()
+ profile_name = self.job_function_id._profile_make_name(self)
+ profile = self._profiler_get_record(profile_name)
+ if not profile:
+ return {"type": "ir.actions.act_window_close"}
+ return {
+ "type": "ir.actions.act_window",
+ "name": "Profile",
+ "res_model": "ir.profile",
+ "view_mode": "form",
+ "res_id": profile.id,
+ "target": "current",
+ }
diff --git a/queue_job_profiler/models/queue_job_function.py b/queue_job_profiler/models/queue_job_function.py
new file mode 100644
index 0000000000..312e0796e7
--- /dev/null
+++ b/queue_job_profiler/models/queue_job_function.py
@@ -0,0 +1,48 @@
+# Copyright 2026 Camptocamp SA
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
+
+from odoo import api, exceptions, fields, models
+
+
+class QueueJobFunction(models.Model):
+ _inherit = "queue.job.function"
+
+ profiling_enabled = fields.Boolean(
+ string="Profiling enabled",
+ help="Indicates whether profiling is enabled for this job function.",
+ )
+ profiling_user_id = fields.Many2one(
+ "res.users",
+ string="Profiling user",
+ help="The user allowed to perform profiling for this job function.",
+ )
+ profiling_until = fields.Datetime(
+ string="Profiling until",
+ help="The date and time until which profiling is enabled "
+ "for this job function.",
+ )
+
+ def is_profiling_enabled(self):
+ self.ensure_one()
+ return (
+ self.profiling_enabled
+ and self.profiling_user_id == self.env.user
+ and (self.profiling_until and self.profiling_until >= fields.Datetime.now())
+ )
+
+ @api.constrains("profiling_enabled")
+ def _check_profiling_setup(self):
+ for record in self:
+ if record.profiling_enabled and (
+ not record.profiling_user_id or not record.profiling_until
+ ):
+ raise exceptions.ValidationError(
+ self.env._(
+ "A profiling user and a profiling until date "
+ "must be set when profiling is enabled."
+ )
+ )
+
+ def _profile_make_name(self, job):
+ self.ensure_one()
+ return f"queue.job {job.uuid} - {self.name}"
diff --git a/queue_job_profiler/pyproject.toml b/queue_job_profiler/pyproject.toml
new file mode 100644
index 0000000000..4231d0cccb
--- /dev/null
+++ b/queue_job_profiler/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["whool"]
+build-backend = "whool.buildapi"
diff --git a/queue_job_profiler/readme/CONTRIBUTORS.md b/queue_job_profiler/readme/CONTRIBUTORS.md
new file mode 100644
index 0000000000..fc27d9f599
--- /dev/null
+++ b/queue_job_profiler/readme/CONTRIBUTORS.md
@@ -0,0 +1,2 @@
+- [Camptocamp](https://camptocamp.com):
+ - Simone Orsi \<\>
diff --git a/queue_job_profiler/readme/DESCRIPTION.md b/queue_job_profiler/readme/DESCRIPTION.md
new file mode 100644
index 0000000000..8e8814426c
--- /dev/null
+++ b/queue_job_profiler/readme/DESCRIPTION.md
@@ -0,0 +1,7 @@
+This addon adds profiling controls to queue job functions and wraps
+queue job execution in an Odoo profiler session when enabled.
+
+When profiling is enabled for a job function and the executing user
+matches the configured profiling user, the queue job runner records
+SQL and async stack traces via `odoo.tools.profiler.Profiler` and saves
+the results into `ir_profile`.
\ No newline at end of file
diff --git a/queue_job_profiler/readme/USAGE.md b/queue_job_profiler/readme/USAGE.md
new file mode 100644
index 0000000000..868db0ccbe
--- /dev/null
+++ b/queue_job_profiler/readme/USAGE.md
@@ -0,0 +1,8 @@
+1. Open the Queue Job Functions menu.
+2. Open the job function you want to profile.
+3. In the Profiler group, enable Profiling and set the Profiling user
+ and Profiling until.
+4. Run the job with the selected user (the queue job runner usually executes
+ as the superuser).
+5. Inspect the generated entries in `ir_profile` to review the captured
+ profiling data.
\ No newline at end of file
diff --git a/queue_job_profiler/static/description/index.html b/queue_job_profiler/static/description/index.html
new file mode 100644
index 0000000000..9ab72a5454
--- /dev/null
+++ b/queue_job_profiler/static/description/index.html
@@ -0,0 +1,445 @@
+
+
+
+
+
+Job Queue Profiler
+
+
+
+
+
Job Queue Profiler
+
+
+
+
This addon adds profiling controls to queue job functions and wraps
+queue job execution in an Odoo profiler session when enabled.
+
When profiling is enabled for a job function and the executing user
+matches the configured profiling user, the queue job runner records SQL
+and async stack traces via odoo.tools.profiler.Profiler and saves
+the results into ir_profile.
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.
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/queue project on GitHub.
+
+
diff --git a/queue_job_profiler/tests/__init__.py b/queue_job_profiler/tests/__init__.py
new file mode 100644
index 0000000000..f4cae97b5a
--- /dev/null
+++ b/queue_job_profiler/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_job_profiler
diff --git a/queue_job_profiler/tests/test_job_profiler.py b/queue_job_profiler/tests/test_job_profiler.py
new file mode 100644
index 0000000000..e579ad0885
--- /dev/null
+++ b/queue_job_profiler/tests/test_job_profiler.py
@@ -0,0 +1,134 @@
+# Copyright 2026 Camptocamp SA
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
+
+from datetime import timedelta
+from unittest.mock import patch
+
+from freezegun import freeze_time
+
+from odoo import exceptions, fields
+from odoo.tests import common, new_test_user
+
+from odoo.addons.queue_job_profiler.controllers.main import RunJobController
+
+
+class TestJobFunction(common.TransactionCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
+ cls.user_func1 = cls.env["queue.job.function"].create(
+ {"model_id": cls.env["ir.model"]._get_id("res.users"), "method": "read"}
+ )
+ cls.user_func2 = cls.env["queue.job.function"].create(
+ {"model_id": cls.env["ir.model"]._get_id("res.users"), "method": "read"}
+ )
+ cls.user1 = new_test_user(cls.env, login="user1", groups="base.group_user")
+ cls.user2 = new_test_user(cls.env, login="user2", groups="base.group_user")
+
+ def test_func_constraint(self):
+ with self.assertRaisesRegex(
+ exceptions.ValidationError,
+ "A profiling user and a profiling until date "
+ "must be set when profiling is enabled.",
+ ):
+ self.user_func1.profiling_enabled = True
+
+ def _enable_profiling(self, job_function, user, delta=timedelta(hours=1)):
+ job_function.profiling_user_id = user
+ job_function.profiling_until = fields.Datetime.now() + delta
+ job_function.profiling_enabled = True
+
+ def test_func_profiling_enabled(self):
+ # By default, profiling should be disabled for all users
+ for user in (self.env.user, self.user1, self.user2):
+ self.assertFalse(self.user_func1.with_user(user).is_profiling_enabled())
+ # Enable it for user1 and check that it's only enabled for that user only
+ self._enable_profiling(self.user_func1, self.user1)
+ self.assertTrue(self.user_func1.with_user(self.user1).is_profiling_enabled())
+ for user in (self.env.user, self.user2):
+ self.assertFalse(self.user_func1.with_user(user).is_profiling_enabled())
+ # Check for another job function and user
+ self._enable_profiling(self.user_func2, self.user2)
+ self.assertTrue(self.user_func2.with_user(self.user2).is_profiling_enabled())
+ for user in (self.env.user, self.user1):
+ self.assertFalse(self.user_func2.with_user(user).is_profiling_enabled())
+ with freeze_time(fields.Datetime.now() + timedelta(days=1)):
+ # After the profiling_until date, profiling should be disabled for all users
+ for user in (self.env.user, self.user1, self.user2):
+ self.assertFalse(self.user_func1.with_user(user).is_profiling_enabled())
+ self.assertFalse(self.user_func2.with_user(user).is_profiling_enabled())
+
+ def test_job_is_profiled(self):
+ job1 = self.env.user.with_delay().read(["id"])
+ job1.store()
+ job_rec1 = job1.db_record()
+ job2 = self.env.user.with_delay().read(["id"])
+ job2.store()
+ job_rec2 = job2.db_record()
+ jobs = job_rec1 | job_rec2
+ self.assertEqual(jobs.mapped("job_is_profiled"), [False, False])
+ profile_name1 = self.user_func1._profile_make_name(job_rec1)
+ profile_name2 = self.user_func2._profile_make_name(job_rec2)
+ prof1 = self.env["ir.profile"].create({"name": profile_name1})
+ jobs.invalidate_recordset()
+ self.assertEqual(jobs.mapped("job_is_profiled"), [True, False])
+ prof2 = self.env["ir.profile"].create({"name": profile_name2})
+ jobs.invalidate_recordset()
+ self.assertEqual(jobs.mapped("job_is_profiled"), [True, True])
+ self.assertEqual(job_rec1._profiler_get_record(profile_name1), prof1)
+ self.assertEqual(job_rec2._profiler_get_record(profile_name2), prof2)
+
+ def test_job_action_view_profile(self):
+ job = self.env.user.with_delay().read(["id"])
+ job.store()
+ job_rec = job.db_record()
+ self.assertEqual(
+ job_rec.action_view_profile(), {"type": "ir.actions.act_window_close"}
+ )
+ profile_name = self.user_func1._profile_make_name(job_rec)
+ prof = self.env["ir.profile"].create({"name": profile_name})
+ action = job_rec.action_view_profile()
+ self.assertEqual(action["type"], "ir.actions.act_window")
+ self.assertEqual(action["res_model"], "ir.profile")
+ self.assertEqual(action["res_id"], prof.id)
+
+ def _run_controller(self, job, user=None):
+ controller = RunJobController()
+ env = self.env(user=user) if user else self.env
+ with (
+ patch.object(self.env.cr, "commit"),
+ ):
+ controller._try_perform_job(env, job)
+
+ def test_controller(self):
+ job = self.env.user.with_delay().read(["id"])
+ self._enable_profiling(self.user_func1, self.user1)
+ # TODO @simahawk: I'd like to look for new `ir_profile` records
+ # but somehow looks very hard to find them
+ # since `Profiler` creates them in a separated db connection.
+ # All my attempts to mock the cr or de connection of `db_connect`
+ # failed poorly, so I prefer to not waste too much time aroudn this.
+ # I'm falling back to patching the controller method
+ # that should be called when profiling is enabled.
+ # The profiler init is tested in another method below.
+ # Not ideal, but better than no test :)
+ with patch.object(RunJobController, "_profiler_get") as mock_profiler_get:
+ self._run_controller(job)
+ mock_profiler_get.assert_not_called()
+ with patch.object(RunJobController, "_profiler_get") as mock_profiler_get:
+ self._run_controller(job, user=self.user1)
+ mock_profiler_get.assert_called_once()
+ with freeze_time(fields.Datetime.now() + timedelta(days=1)):
+ with patch.object(RunJobController, "_profiler_get") as mock_profiler_get:
+ self._run_controller(job)
+ mock_profiler_get.assert_not_called()
+
+ def test_controller_profiler_get(self):
+ # as we mocked it before, make sure it works
+ controller = RunJobController()
+ job = self.env.user.with_delay().read(["id"])
+ profiler = controller._profiler_get(self.env, job)
+ self.assertEqual(
+ profiler.description, f"queue.job {job.uuid} - {job.job_function_name}"
+ )
diff --git a/queue_job_profiler/views/queue_job_function_views.xml b/queue_job_profiler/views/queue_job_function_views.xml
new file mode 100644
index 0000000000..57ecf10081
--- /dev/null
+++ b/queue_job_profiler/views/queue_job_function_views.xml
@@ -0,0 +1,25 @@
+
+
+
+ queue.job.function.form.profiler
+ queue.job.function
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/queue_job_profiler/views/queue_job_views.xml b/queue_job_profiler/views/queue_job_views.xml
new file mode 100644
index 0000000000..7e379a3795
--- /dev/null
+++ b/queue_job_profiler/views/queue_job_views.xml
@@ -0,0 +1,25 @@
+
+
+
+ queue.job.form.profiler
+ queue.job
+
+
+
+
+
+
+
+
From c17a4ad8bcc9d2af27d04586f9f3ea65736269d1 Mon Sep 17 00:00:00 2001
From: Simone Orsi
Date: Thu, 12 Feb 2026 11:07:47 +0100
Subject: [PATCH 03/27] queue_job_profiler: make user note required
---
queue_job_profiler/models/queue_job_function.py | 9 +++------
queue_job_profiler/tests/test_job_profiler.py | 3 +--
queue_job_profiler/views/queue_job_function_views.xml | 6 +-----
3 files changed, 5 insertions(+), 13 deletions(-)
diff --git a/queue_job_profiler/models/queue_job_function.py b/queue_job_profiler/models/queue_job_function.py
index 312e0796e7..68e343b7bf 100644
--- a/queue_job_profiler/models/queue_job_function.py
+++ b/queue_job_profiler/models/queue_job_function.py
@@ -26,20 +26,17 @@ def is_profiling_enabled(self):
self.ensure_one()
return (
self.profiling_enabled
- and self.profiling_user_id == self.env.user
and (self.profiling_until and self.profiling_until >= fields.Datetime.now())
+ and (not self.profiling_user_id or self.profiling_user_id == self.env.user)
)
@api.constrains("profiling_enabled")
def _check_profiling_setup(self):
for record in self:
- if record.profiling_enabled and (
- not record.profiling_user_id or not record.profiling_until
- ):
+ if record.profiling_enabled and not record.profiling_until:
raise exceptions.ValidationError(
self.env._(
- "A profiling user and a profiling until date "
- "must be set when profiling is enabled."
+ "A profiling until date must be set when profiling is enabled."
)
)
diff --git a/queue_job_profiler/tests/test_job_profiler.py b/queue_job_profiler/tests/test_job_profiler.py
index e579ad0885..da7c6d8917 100644
--- a/queue_job_profiler/tests/test_job_profiler.py
+++ b/queue_job_profiler/tests/test_job_profiler.py
@@ -29,8 +29,7 @@ def setUpClass(cls):
def test_func_constraint(self):
with self.assertRaisesRegex(
exceptions.ValidationError,
- "A profiling user and a profiling until date "
- "must be set when profiling is enabled.",
+ "A profiling until date must be set when profiling is enabled.",
):
self.user_func1.profiling_enabled = True
diff --git a/queue_job_profiler/views/queue_job_function_views.xml b/queue_job_profiler/views/queue_job_function_views.xml
index 57ecf10081..c75bdc22eb 100644
--- a/queue_job_profiler/views/queue_job_function_views.xml
+++ b/queue_job_profiler/views/queue_job_function_views.xml
@@ -8,11 +8,7 @@
-
+
Date: Thu, 12 Feb 2026 15:35:59 +0100
Subject: [PATCH 04/27] queue_job_profiler: allow to select multiple users
---
queue_job_profiler/README.rst | 11 +++++-----
.../models/queue_job_function.py | 10 +++++----
queue_job_profiler/readme/DESCRIPTION.md | 3 ++-
queue_job_profiler/readme/USAGE.md | 4 ++--
.../static/description/index.html | 11 +++++-----
queue_job_profiler/tests/test_job_profiler.py | 22 ++++++++++++-------
.../views/queue_job_function_views.xml | 5 ++++-
7 files changed, 40 insertions(+), 26 deletions(-)
diff --git a/queue_job_profiler/README.rst b/queue_job_profiler/README.rst
index 9765639adc..a2530f7ec9 100644
--- a/queue_job_profiler/README.rst
+++ b/queue_job_profiler/README.rst
@@ -32,9 +32,10 @@ This addon adds profiling controls to queue job functions and wraps
queue job execution in an Odoo profiler session when enabled.
When profiling is enabled for a job function and the executing user
-matches the configured profiling user, the queue job runner records SQL
-and async stack traces via ``odoo.tools.profiler.Profiler`` and saves
-the results into ``ir_profile``.
+matches one of the configured profiling users (or no users are set), the
+queue job runner records SQL and async stack traces via
+``odoo.tools.profiler.Profiler`` and saves the results into
+``ir_profile``.
**Table of contents**
@@ -46,8 +47,8 @@ Usage
1. Open the Queue Job Functions menu.
2. Open the job function you want to profile.
-3. In the Profiler group, enable Profiling and set the Profiling user
- and Profiling until.
+3. In the Profiler group, enable Profiling and set the Profiling users
+ (optional) and Profiling until.
4. Run the job with the selected user (the queue job runner usually
executes as the superuser).
5. Inspect the generated entries in ``ir_profile`` to review the
diff --git a/queue_job_profiler/models/queue_job_function.py b/queue_job_profiler/models/queue_job_function.py
index 68e343b7bf..00550e15cb 100644
--- a/queue_job_profiler/models/queue_job_function.py
+++ b/queue_job_profiler/models/queue_job_function.py
@@ -11,10 +11,10 @@ class QueueJobFunction(models.Model):
string="Profiling enabled",
help="Indicates whether profiling is enabled for this job function.",
)
- profiling_user_id = fields.Many2one(
+ profiling_user_ids = fields.Many2many(
"res.users",
- string="Profiling user",
- help="The user allowed to perform profiling for this job function.",
+ string="Profiling users",
+ help="The users allowed to perform profiling for this job function.",
)
profiling_until = fields.Datetime(
string="Profiling until",
@@ -27,7 +27,9 @@ def is_profiling_enabled(self):
return (
self.profiling_enabled
and (self.profiling_until and self.profiling_until >= fields.Datetime.now())
- and (not self.profiling_user_id or self.profiling_user_id == self.env.user)
+ and (
+ not self.profiling_user_ids or self.env.user in self.profiling_user_ids
+ )
)
@api.constrains("profiling_enabled")
diff --git a/queue_job_profiler/readme/DESCRIPTION.md b/queue_job_profiler/readme/DESCRIPTION.md
index 8e8814426c..06cb1376de 100644
--- a/queue_job_profiler/readme/DESCRIPTION.md
+++ b/queue_job_profiler/readme/DESCRIPTION.md
@@ -2,6 +2,7 @@ This addon adds profiling controls to queue job functions and wraps
queue job execution in an Odoo profiler session when enabled.
When profiling is enabled for a job function and the executing user
-matches the configured profiling user, the queue job runner records
+matches one of the configured profiling users (or no users are set),
+the queue job runner records
SQL and async stack traces via `odoo.tools.profiler.Profiler` and saves
the results into `ir_profile`.
\ No newline at end of file
diff --git a/queue_job_profiler/readme/USAGE.md b/queue_job_profiler/readme/USAGE.md
index 868db0ccbe..a58ee8eeff 100644
--- a/queue_job_profiler/readme/USAGE.md
+++ b/queue_job_profiler/readme/USAGE.md
@@ -1,7 +1,7 @@
1. Open the Queue Job Functions menu.
2. Open the job function you want to profile.
-3. In the Profiler group, enable Profiling and set the Profiling user
- and Profiling until.
+3. In the Profiler group, enable Profiling and set the Profiling users
+ (optional) and Profiling until.
4. Run the job with the selected user (the queue job runner usually executes
as the superuser).
5. Inspect the generated entries in `ir_profile` to review the captured
diff --git a/queue_job_profiler/static/description/index.html b/queue_job_profiler/static/description/index.html
index 9ab72a5454..7a850cb7c3 100644
--- a/queue_job_profiler/static/description/index.html
+++ b/queue_job_profiler/static/description/index.html
@@ -373,9 +373,10 @@
Job Queue Profiler
This addon adds profiling controls to queue job functions and wraps
queue job execution in an Odoo profiler session when enabled.
When profiling is enabled for a job function and the executing user
-matches the configured profiling user, the queue job runner records SQL
-and async stack traces via odoo.tools.profiler.Profiler and saves
-the results into ir_profile.
+matches one of the configured profiling users (or no users are set), the
+queue job runner records SQL and async stack traces via
+odoo.tools.profiler.Profiler and saves the results into
+ir_profile.
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
@@ -412,15 +417,15 @@
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.