[16.0][MIG][REF] attachment_queue: Migration to 16.0
parent
b7bd5a8323
commit
178f40731a
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record model="ir.cron" id="cronjob_run_attachment_queue">
|
||||
<field name='name'>Run Attachments Queue</field>
|
||||
<field name='interval_number'>30</field>
|
||||
<field name='interval_type'>minutes</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="active">False</field>
|
||||
<field name="doall" eval="False" />
|
||||
<field name="model_id" ref="model_attachment_queue" />
|
||||
<field name="state">code</field>
|
||||
<field name="code">model.run_attachment_queue_scheduler()</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="attachment_queue_cron_batch_limit" model="ir.config_parameter">
|
||||
<field name="key">attachment_queue_cron_batch_limit</field>
|
||||
<field name="value">200</field>
|
||||
</record>
|
||||
</odoo>
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="attachment_queue_job_channel" model="queue.job.channel">
|
||||
<field name="name">Attachment queues</field>
|
||||
<field name="parent_id" ref="queue_job.channel_root" />
|
||||
</record>
|
||||
</odoo>
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="attachment_queue_demo" model="attachment.queue">
|
||||
<field name="datas">bWlncmF0aW9uIHRlc3Q=</field>
|
||||
<field name="name">attachment_queue_demo.doc</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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"))
|
|
@ -9,9 +9,16 @@
|
|||
<xpath expr="/form/*" position="before">
|
||||
<header>
|
||||
<button
|
||||
name="run"
|
||||
name="button_manual_run"
|
||||
states="pending,failed"
|
||||
string="Run"
|
||||
string="Manual run"
|
||||
type="object"
|
||||
class="oe_highlight"
|
||||
/>
|
||||
<button
|
||||
name="button_reschedule"
|
||||
states="done,failed"
|
||||
string="Reschedule"
|
||||
type="object"
|
||||
class="oe_highlight"
|
||||
/>
|
||||
|
|
Loading…
Reference in New Issue