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..ece97f5a2
--- /dev/null
+++ b/attachment_queue/__manifest__.py
@@ -0,0 +1,28 @@
+# 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 that add the concept of queue for processing file",
+ '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..72e057ced
--- /dev/null
+++ b/attachment_queue/models/attachment_queue.py
@@ -0,0 +1,106 @@
+# 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="list of email (separated by comma) which should 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
+
+ @api.multi
+ def _run(self):
+ self.ensure_one()
+ _logger.info('Start to process attachment queue id %d', self.id)
+
+ @api.multi
+ 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..a5e20daee
--- /dev/null
+++ b/attachment_queue/readme/DESCRIPTION.rst
@@ -0,0 +1,5 @@
+This module implement a queue for processing file.
+File are stored in Odoo standard ir.attachment.
+The attachments will be processed depending on their type.
+
+An example of the use of this module, can be found in the module `attachment_synchronize`.
diff --git a/attachment_queue/readme/USAGE.rst b/attachment_queue/readme/USAGE.rst
new file mode 100644
index 000000000..ec276d247
--- /dev/null
+++ b/attachment_queue/readme/USAGE.rst
@@ -0,0 +1,8 @@
+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
+
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/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/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..fc29f902f
--- /dev/null
+++ b/attachment_queue/tests/test_attachment_queue.py
@@ -0,0 +1,50 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo.tests.common import TransactionCase
+import odoo
+from odoo import api
+
+
+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..f8bc7c9d3
--- /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
+
+
+
+
+
+
+