diff --git a/attachment_queue/__init__.py b/attachment_queue/__init__.py
new file mode 100644
index 000000000..0650744f6
--- /dev/null
+++ b/attachment_queue/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/attachment_queue/__manifest__.py b/attachment_queue/__manifest__.py
new file mode 100644
index 000000000..7581c49f5
--- /dev/null
+++ b/attachment_queue/__manifest__.py
@@ -0,0 +1,23 @@
+# Copyright 2015 Florian DA COSTA @ Akretion
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+{
+ "name": "Attachment Queue",
+ "version": "12.0.1.0.0",
+ "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"],
+ "data": [
+ "views/attachment_queue_view.xml",
+ "security/ir.model.access.csv",
+ "data/cron.xml",
+ "data/ir_config_parameter.xml",
+ "data/mail_template.xml",
+ ],
+ "demo": ["demo/attachment_queue_demo.xml"],
+ "installable": True,
+}
diff --git a/attachment_queue/data/cron.xml b/attachment_queue/data/cron.xml
new file mode 100644
index 000000000..ad4e6540a
--- /dev/null
+++ b/attachment_queue/data/cron.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ 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
new file mode 100644
index 000000000..5b723255a
--- /dev/null
+++ b/attachment_queue/data/ir_config_parameter.xml
@@ -0,0 +1,7 @@
+
+
+
+ attachment_queue_cron_batch_limit
+ 200
+
+
diff --git a/attachment_queue/data/mail_template.xml b/attachment_queue/data/mail_template.xml
new file mode 100644
index 000000000..4eec88492
--- /dev/null
+++ b/attachment_queue/data/mail_template.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ ${object.failure_emails}
+ Attachment Failure notification
+ The attachment ${object.name} has failed
+
+ Hello,
+ The attachment ${object.name} has failed with the following error message :
${object.state_message}
+ Regards,
+ ]]>
+
+
+
diff --git a/attachment_queue/demo/attachment_queue_demo.xml b/attachment_queue/demo/attachment_queue_demo.xml
new file mode 100644
index 000000000..ef69be60a
--- /dev/null
+++ b/attachment_queue/demo/attachment_queue_demo.xml
@@ -0,0 +1,10 @@
+
+
+
+
+ bWlncmF0aW9uIHRlc3Q=
+ attachment_queue_demo.doc
+ attachment_queue_demo.doc
+
+
+
diff --git a/attachment_queue/models/__init__.py b/attachment_queue/models/__init__.py
new file mode 100644
index 000000000..89333838a
--- /dev/null
+++ b/attachment_queue/models/__init__.py
@@ -0,0 +1 @@
+from . import attachment_queue
diff --git a/attachment_queue/models/attachment_queue.py b/attachment_queue/models/attachment_queue.py
new file mode 100644
index 000000000..4c058dac3
--- /dev/null
+++ b/attachment_queue/models/attachment_queue.py
@@ -0,0 +1,111 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+import logging
+
+from odoo import api, fields, models, registry
+
+_logger = logging.getLogger(__name__)
+
+
+class AttachmentQueue(models.Model):
+ _name = "attachment.queue"
+ _inherits = {"ir.attachment": "attachment_id"}
+ _inherit = ["mail.thread"]
+
+ attachment_id = fields.Many2one(
+ "ir.attachment",
+ required=True,
+ ondelete="cascade",
+ help="Link to ir.attachment model ",
+ )
+ file_type = fields.Selection(
+ selection=[],
+ help="The file type determines an import method to be used "
+ "to parse and transform data before their import in ERP or an export",
+ )
+ date_done = fields.Datetime()
+ state = fields.Selection(
+ [("pending", "Pending"), ("failed", "Failed"), ("done", "Done")],
+ readonly=False,
+ required=True,
+ default="pending",
+ )
+ 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",
+ )
+
+ def _compute_failure_emails(self):
+ for attach in self:
+ attach.failure_emails = attach._get_failure_emails()
+
+ def _get_failure_emails(self):
+ # to be overriden in submodules implementing the file_type
+ 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
+ """
+ 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, self.env.uid, 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
+
+ def _run(self):
+ self.ensure_one()
+ _logger.info("Starting processing of attachment queue id %d", self.id)
+
+ def set_done(self):
+ """
+ Manually set to done
+ """
+ message = "Manually set to done by %s" % self.env.user.name
+ self.write({"state_message": message, "state": "done"})
diff --git a/attachment_queue/readme/CONTRIBUTORS.rst b/attachment_queue/readme/CONTRIBUTORS.rst
new file mode 100644
index 000000000..1394f35aa
--- /dev/null
+++ b/attachment_queue/readme/CONTRIBUTORS.rst
@@ -0,0 +1,4 @@
+* Valentin CHEMIERE
+* Florian da Costa
+* Angel Moya
+* Dan Kiplangat
diff --git a/attachment_queue/readme/DESCRIPTION.rst b/attachment_queue/readme/DESCRIPTION.rst
new file mode 100644
index 000000000..57bac75fd
--- /dev/null
+++ b/attachment_queue/readme/DESCRIPTION.rst
@@ -0,0 +1,3 @@
+This module adds async processing capabilities to attachments by implementing a new model attachment.queue that wraps attachments and stores additional information so that it can be processed in an asynchronous way.
+
+A use case of this module can be found in the attachment_synchronize module.
diff --git a/attachment_queue/readme/USAGE.rst b/attachment_queue/readme/USAGE.rst
new file mode 100644
index 000000000..a97a271e5
--- /dev/null
+++ b/attachment_queue/readme/USAGE.rst
@@ -0,0 +1,17 @@
+Go the menu Settings > Technical > Database Structure > Attachments Queue
+
+You can create / see standard attachments with additional fields
+
+Configure the batch limit for attachments that can be sync by the cron task at a go:
+
+Settings > Technical > System parameters > attachment_queue_cron_batch_limit
+
+
+image:: ../static/description/tree.png
+
+
+This module can be used in combination with attachment_synchronize to control file processing workflow
+
+
+image:: ../static/description/form.png
+
diff --git a/attachment_queue/security/ir.model.access.csv b/attachment_queue/security/ir.model.access.csv
new file mode 100644
index 000000000..e1e0880a3
--- /dev/null
+++ b/attachment_queue/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_attachment_queue_user,attachment.queue.user,model_attachment_queue,,1,0,0,0
+access_attachment_queue_manager,attachment.queue.manager,model_attachment_queue,base.group_no_one,1,1,1,1
diff --git a/attachment_queue/static/description/form.png b/attachment_queue/static/description/form.png
new file mode 100644
index 000000000..fbc9f6aa6
Binary files /dev/null and b/attachment_queue/static/description/form.png differ
diff --git a/attachment_queue/static/description/icon.png b/attachment_queue/static/description/icon.png
new file mode 100644
index 000000000..3a0328b51
Binary files /dev/null and b/attachment_queue/static/description/icon.png differ
diff --git a/attachment_queue/static/description/tree.png b/attachment_queue/static/description/tree.png
new file mode 100644
index 000000000..d4dcc92fe
Binary files /dev/null and b/attachment_queue/static/description/tree.png differ
diff --git a/attachment_queue/tests/__init__.py b/attachment_queue/tests/__init__.py
new file mode 100644
index 000000000..7f25150f6
--- /dev/null
+++ b/attachment_queue/tests/__init__.py
@@ -0,0 +1,3 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from . import test_attachment_queue
diff --git a/attachment_queue/tests/test_attachment_queue.py b/attachment_queue/tests/test_attachment_queue.py
new file mode 100644
index 000000000..74d368f2b
--- /dev/null
+++ b/attachment_queue/tests/test_attachment_queue.py
@@ -0,0 +1,39 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+import odoo
+from odoo import api
+from odoo.tests.common import TransactionCase
+
+
+class TestAttachmentBaseQueue(TransactionCase):
+ 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"
+ )
+
+ def tearDown(self):
+ self.registry.leave_test_mode()
+ super().tearDown()
+
+ 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_set_done(self):
+ """Test set_done manually
+ """
+ self.assertEqual(self.attachment.state, "pending")
+ self.attachment.set_done()
+ self.assertEqual(self.attachment.state, "done")
diff --git a/attachment_queue/views/attachment_queue_view.xml b/attachment_queue/views/attachment_queue_view.xml
new file mode 100644
index 000000000..f83f60a72
--- /dev/null
+++ b/attachment_queue/views/attachment_queue_view.xml
@@ -0,0 +1,106 @@
+
+
+
+
+ attachment.queue
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ attachment.queue
+
+
+
+
+
+
+
+
+
+
+
+
+
+ attachment.queue
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Attachments Queue
+ ir.actions.act_window
+ attachment.queue
+ form
+ tree,form
+
+
+
+
+
+
+ tree
+
+
+
+
+
+
+ form
+
+
+
+
+
+
+