From a5101891d46833ff78daff14fc35676a1c36b140 Mon Sep 17 00:00:00 2001
From: Simone Orsi
Date: Wed, 11 Feb 2026 18:40:20 +0100
Subject: [PATCH 1/4] Add queue_job_profiler
---
queue_job_profiler/README.rst | 96 ++++
queue_job_profiler/__init__.py | 2 +
queue_job_profiler/__manifest__.py | 18 +
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 | 43 ++
.../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, 888 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..4b27f4dd8f
--- /dev/null
+++ b/queue_job_profiler/__manifest__.py
@@ -0,0 +1,18 @@
+# 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)",
+ "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..cc2ddc7e89
--- /dev/null
+++ b/queue_job_profiler/models/queue_job.py
@@ -0,0 +1,43 @@
+# Copyright 2019 Creu Blanca
+# 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",
+ )
+
+ 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"]
+ 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..312f6a1ce2
--- /dev/null
+++ b/queue_job_profiler/models/queue_job_function.py
@@ -0,0 +1,48 @@
+# Copyright 2019 Creu Blanca
+# 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..b820290dcb
--- /dev/null
+++ b/queue_job_profiler/tests/test_job_profiler.py
@@ -0,0 +1,134 @@
+# copyright 2026 Camptocamp sa (https://www.camptocamp.com).
+# 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 7818e8fb64234c21ec3f37cc297d6a25de30a7dc Mon Sep 17 00:00:00 2001
From: Simone Orsi
Date: Thu, 12 Feb 2026 11:07:47 +0100
Subject: [PATCH 2/4] 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 312f6a1ce2..6f1d20fcb7 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 b820290dcb..1e4ce70be9 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 3/4] 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 6f1d20fcb7..9118d05cd2 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.