diff --git a/attachment_queue/__manifest__.py b/attachment_queue/__manifest__.py
index 7522a464d..45cd06bac 100644
--- a/attachment_queue/__manifest__.py
+++ b/attachment_queue/__manifest__.py
@@ -3,21 +3,19 @@
{
"name": "Attachment Queue",
- "version": "14.0.1.0.1",
+ "version": "16.0.1.0.1",
"author": "Akretion,Odoo Community Association (OCA)",
"summary": "Base module adding the concept of queue for processing files",
"website": "https://github.com/OCA/server-tools",
"maintainers": ["florian-dacosta", "sebastienbeau"],
"license": "AGPL-3",
"category": "Generic Modules",
- "depends": ["base", "mail"],
+ "depends": ["base", "mail", "queue_job"],
"data": [
"views/attachment_queue_view.xml",
"security/ir.model.access.csv",
- "data/cron.xml",
- "data/ir_config_parameter.xml",
"data/mail_template.xml",
+ "data/queue_job_channel.xml",
],
- "demo": ["demo/attachment_queue_demo.xml"],
"installable": True,
}
diff --git a/attachment_queue/data/cron.xml b/attachment_queue/data/cron.xml
deleted file mode 100644
index fa8130f4b..000000000
--- a/attachment_queue/data/cron.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
- Run Attachments Queue
- 30
- minutes
- -1
- False
-
-
- code
- model.run_attachment_queue_scheduler()
-
-
-
diff --git a/attachment_queue/data/ir_config_parameter.xml b/attachment_queue/data/ir_config_parameter.xml
deleted file mode 100644
index ecf34bf8c..000000000
--- a/attachment_queue/data/ir_config_parameter.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
- attachment_queue_cron_batch_limit
- 200
-
-
diff --git a/attachment_queue/data/queue_job_channel.xml b/attachment_queue/data/queue_job_channel.xml
new file mode 100644
index 000000000..5e635cb03
--- /dev/null
+++ b/attachment_queue/data/queue_job_channel.xml
@@ -0,0 +1,7 @@
+
+
+
+ Attachment queues
+
+
+
diff --git a/attachment_queue/demo/attachment_queue_demo.xml b/attachment_queue/demo/attachment_queue_demo.xml
deleted file mode 100644
index c1d8460e8..000000000
--- a/attachment_queue/demo/attachment_queue_demo.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
- bWlncmF0aW9uIHRlc3Q=
- attachment_queue_demo.doc
-
-
-
diff --git a/attachment_queue/models/attachment_queue.py b/attachment_queue/models/attachment_queue.py
index b7d279c8c..64f39f1f3 100644
--- a/attachment_queue/models/attachment_queue.py
+++ b/attachment_queue/models/attachment_queue.py
@@ -2,10 +2,19 @@
import logging
-from odoo import SUPERUSER_ID, api, fields, models, registry
+from odoo import api, fields, models
+from odoo.exceptions import UserError
+
+from odoo.addons.queue_job.exception import RetryableJobError
_logger = logging.getLogger(__name__)
+DEFAULT_ETA_FOR_RETRY = 60 * 60
+STR_ERR_ATTACHMENT_RUNNING = (
+ "The attachment is currently flagged as being in processing"
+)
+STR_ERROR_DURING_PROCESSING = "Error during processing of attachment_queue id {}: \n"
+
class AttachmentQueue(models.Model):
_name = "attachment.queue"
@@ -34,10 +43,38 @@ class AttachmentQueue(models.Model):
state_message = fields.Text()
failure_emails = fields.Char(
compute="_compute_failure_emails",
- string="Failure Emails",
help="Comma-separated list of email addresses to be notified in case of"
"failure",
)
+ running_lock = fields.Boolean()
+
+ @property
+ def _eta_for_retry(self):
+ return DEFAULT_ETA_FOR_RETRY
+
+ @property
+ def _job_attrs(self):
+ return {"channel": "Attachment queues"}
+
+ def _schedule_jobs(self):
+ for el in self:
+ el.with_delay(**self._job_attrs).run()
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ res = super().create(vals_list)
+ res._schedule_jobs()
+ return res
+
+ def button_reschedule(self):
+ self.state = "pending"
+ self.state_message = ""
+ self._schedule_jobs()
+
+ def button_manual_run(self):
+ if self.running_lock:
+ raise UserError(STR_ERR_ATTACHMENT_RUNNING)
+ self.run()
def _compute_failure_emails(self):
for attach in self:
@@ -48,51 +85,41 @@ class AttachmentQueue(models.Model):
self.ensure_one()
return ""
- @api.model
- def run_attachment_queue_scheduler(self, domain=None):
- if domain is None:
- domain = [("state", "=", "pending")]
- batch_limit = self.env.ref(
- "attachment_queue.attachment_queue_cron_batch_limit"
- ).value
- if batch_limit and batch_limit.isdigit():
- limit = int(batch_limit)
- else:
- limit = 200
- attachments = self.search(domain, limit=limit)
- if attachments:
- return attachments.run()
- return True
-
def run(self):
"""
- Run the process for each attachment queue
+ Run the process for an individual attachment queue
"""
- failure_tmpl = self.env.ref("attachment_queue.attachment_failure_notification")
- for attachment in self:
- with api.Environment.manage():
- with registry(self.env.cr.dbname).cursor() as new_cr:
- new_env = api.Environment(new_cr, SUPERUSER_ID, self.env.context)
- attach = attachment.with_env(new_env)
- try:
- attach._run()
- # pylint: disable=broad-except
- except Exception as e:
- attach.env.cr.rollback()
- _logger.exception(str(e))
- attach.write({"state": "failed", "state_message": str(e)})
- emails = attach.failure_emails
- if emails:
- failure_tmpl.send_mail(attach.id)
- attach.env.cr.commit()
- else:
- vals = {
- "state": "done",
- "date_done": fields.Datetime.now(),
- }
- attach.write(vals)
- attach.env.cr.commit()
- return True
+ if self.state != "pending":
+ return
+ if self.running_lock is True:
+ raise RetryableJobError(
+ STR_ERR_ATTACHMENT_RUNNING, seconds=self._eta_for_retry
+ )
+ self.running_lock = True
+ self.flush_recordset()
+ try:
+ with self.env.cr.savepoint():
+ self._run()
+ except Exception as e:
+ _logger.warning(STR_ERROR_DURING_PROCESSING.format(self.id) + str(e))
+ self.write(
+ {"state": "failed", "state_message": str(e), "running_lock": False}
+ )
+ emails = self.failure_emails
+ if emails:
+ self.env.ref(
+ "attachment_queue.attachment_failure_notification"
+ ).send_mail(self.id)
+ return False
+ else:
+ self.write(
+ {
+ "state": "done",
+ "date_done": fields.Datetime.now(),
+ "running_lock": False,
+ }
+ )
+ return True
def _run(self):
self.ensure_one()
diff --git a/attachment_queue/tests/test_attachment_queue.py b/attachment_queue/tests/test_attachment_queue.py
index 3d8910be3..346841b42 100644
--- a/attachment_queue/tests/test_attachment_queue.py
+++ b/attachment_queue/tests/test_attachment_queue.py
@@ -1,35 +1,111 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-import odoo
-from odoo import api
+from unittest import mock
+
+from odoo_test_helper import FakeModelLoader
+
+from odoo.exceptions import UserError
from odoo.tests.common import TransactionCase
+from odoo.addons.queue_job.exception import RetryableJobError
+from odoo.addons.queue_job.tests.common import trap_jobs
+
+DUMMY_AQ_VALS = {
+ "datas": "",
+ "name": "dummy_aq.doc",
+}
+MOCK_PATH_RUN = (
+ "odoo.addons.attachment_queue.models.attachment_queue.AttachmentQueue._run"
+)
+
class TestAttachmentBaseQueue(TransactionCase):
+ def _create_dummy_attachment(self, override=False, no_job=False):
+ override = override or {}
+ vals = DUMMY_AQ_VALS.copy()
+ vals.update(override)
+ if no_job:
+ return (
+ self.env["attachment.queue"].with_context(test_queue_job_no_delay=True)
+ ).create(vals)
+ return self.env["attachment.queue"].create(vals)
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.loader = FakeModelLoader(cls.env, cls.__module__)
+ cls.loader.backup_registry()
+ from .test_models import AttachmentQueue
+
+ cls.loader.update_registry((AttachmentQueue,))
+
+ @classmethod
+ def tearDownClass(cls):
+ super().tearDownClass()
+ cls.loader.restore_registry()
+ return super().tearDownClass()
+
def setUp(self):
super().setUp()
- self.registry.enter_test_mode(self.env.cr)
- self.env = api.Environment(
- self.registry.test_cr, self.env.uid, self.env.context
- )
- self.attachment = self.env.ref("attachment_queue.attachment_queue_demo")
+ self.aq_model = self.env["attachment.queue"]
- def tearDown(self):
- self.registry.leave_test_mode()
- super().tearDown()
+ def test_job_created(self):
+ with trap_jobs() as trap:
+ self._create_dummy_attachment()
+ trap.assert_enqueued_job(
+ self.env["attachment.queue"].run,
+ )
- def test_attachment_queue(self):
- """Test run_attachment_queue_scheduler to ensure set state to done"""
- self.assertEqual(self.attachment.state, "pending")
- self.env["attachment.queue"].run_attachment_queue_scheduler()
- self.env.cache.invalidate()
- with odoo.registry(self.env.cr.dbname).cursor() as new_cr:
- new_env = api.Environment(new_cr, self.env.uid, self.env.context)
- attach = self.attachment.with_env(new_env)
- self.assertEqual(attach.state, "done")
+ def test_aq_locked_job(self):
+ """If an attachment is already running, and a job tries to run it, retry later"""
+ with self.assertRaises(RetryableJobError):
+ self._create_dummy_attachment({"running_lock": True}, no_job=True)
+
+ def test_aq_locked_button(self):
+ """If an attachment is already running, and a user tries to run it manually,
+ raise error window"""
+ attachment = self._create_dummy_attachment(no_job=True)
+ attachment.running_lock = True
+ with self.assertRaises(UserError):
+ attachment.button_manual_run()
+
+ def test_run_ok(self):
+ """Attachment queue should have correct state and result"""
+ partners_initial = len(self.env["res.partner"].search([]))
+ with mock.patch.object(
+ type(self.aq_model),
+ "_run",
+ self.env["attachment.queue"].mock_run_create_partners,
+ ):
+ attachment = self._create_dummy_attachment(no_job=True)
+ partners_after = len(self.env["res.partner"].search([]))
+ self.assertEqual(partners_after, partners_initial + 10)
+ self.assertEqual(attachment.state, "done")
+
+ def test_run_fails(self):
+ """Attachment queue should have correct state/error message"""
+ with mock.patch.object(
+ type(self.aq_model), "_run", self.env["attachment.queue"].mock_run_fail
+ ):
+ attachment = self._create_dummy_attachment(no_job=True)
+ self.assertEqual(attachment.state, "failed")
+ self.assertEqual(attachment.state_message, "boom")
+
+ def test_run_fails_rollback(self):
+ """In case of failure, no side effects should occur"""
+ partners_initial = len(self.env["res.partner"].search([]))
+ with mock.patch.object(
+ type(self.aq_model),
+ "_run",
+ self.env["attachment.queue"].mock_run_create_partners_and_fail,
+ ):
+ self._create_dummy_attachment(no_job=True)
+ partners_after = len(self.env["res.partner"].search([]))
+ self.assertEqual(partners_after, partners_initial)
def test_set_done(self):
"""Test set_done manually"""
- self.assertEqual(self.attachment.state, "pending")
- self.attachment.set_done()
- self.assertEqual(self.attachment.state, "done")
+ attachment = self._create_dummy_attachment()
+ self.assertEqual(attachment.state, "pending")
+ attachment.set_done()
+ self.assertEqual(attachment.state, "done")
diff --git a/attachment_queue/tests/test_models.py b/attachment_queue/tests/test_models.py
new file mode 100644
index 000000000..d84c8eaad
--- /dev/null
+++ b/attachment_queue/tests/test_models.py
@@ -0,0 +1,20 @@
+# Copyright 2023 Akretion
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+from odoo import _, models
+from odoo.exceptions import UserError
+
+
+class AttachmentQueue(models.Model):
+ _inherit = "attachment.queue"
+ _name = "attachment.queue"
+
+ def mock_run_fail(self):
+ raise UserError(_("boom"))
+
+ def mock_run_create_partners(self):
+ for x in range(10):
+ self.env["res.partner"].create({"name": str(x)})
+
+ def mock_run_create_partners_and_fail(self):
+ self.mock_run_create_partners()
+ raise UserError(_("boom"))
diff --git a/attachment_queue/views/attachment_queue_view.xml b/attachment_queue/views/attachment_queue_view.xml
index 381099026..89eddcc66 100644
--- a/attachment_queue/views/attachment_queue_view.xml
+++ b/attachment_queue/views/attachment_queue_view.xml
@@ -9,9 +9,16 @@