diff --git a/.travis.yml b/.travis.yml index ea881ba1c..fa0c7a785 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,21 +13,25 @@ addons: env: global: - - VERSION="12.0" TESTS="0" LINT_CHECK="0" MAKEPOT="0" + - VERSION="12.0" TESTS="0" LINT_CHECK="0" MAKEPOT="0" MQT_DEP="PIP" - COUNTRY="US" matrix: - LINT_CHECK="1" - - TESTS="1" ODOO_REPO="OCA/OCB" EXCLUDE="database_cleanup" + - TESTS="1" ODOO_REPO="OCA/OCB" EXCLUDE="database_cleanup,module_analysis" - TESTS="1" ODOO_REPO="OCA/OCB" INCLUDE="database_cleanup" - - TESTS="1" ODOO_REPO="odoo/odoo" EXCLUDE="database_cleanup" MAKEPOT="1" + - TESTS="1" ODOO_REPO="OCA/OCB" INCLUDE="module_analysis" + - TESTS="1" ODOO_REPO="odoo/odoo" EXCLUDE="database_cleanup,module_analysis" MAKEPOT="1" - TESTS="1" ODOO_REPO="odoo/odoo" INCLUDE="database_cleanup" MAKEPOT="1" + - TESTS="1" ODOO_REPO="odoo/odoo" INCLUDE="module_analysis" MAKEPOT="1" install: - git clone --depth=1 https://github.com/OCA/maintainer-quality-tools.git ${HOME}/maintainer-quality-tools - export PATH=${HOME}/maintainer-quality-tools/travis:${PATH} - travis_install_nightly + # Requirements to test server_environment modules + - printf '[options]\n\nrunning_env = dev\n' > ${HOME}/.openerp_serverrc script: - travis_run_tests diff --git a/attachment_synchronize/README.rst b/attachment_synchronize/README.rst new file mode 100644 index 000000000..4da14ae51 --- /dev/null +++ b/attachment_synchronize/README.rst @@ -0,0 +1,123 @@ +====================== +Attachment Synchronize +====================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fserver--tools-lightgray.png?logo=github + :target: https://github.com/OCA/server-tools/tree/12.0/attachment_synchronize + :alt: OCA/server-tools +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-tools-12-0/server-tools-12-0-attachment_synchronize + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/149/12.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to **import/export files** from/to backend servers. + +A backend server is defined by the basic `storage_backend `_ OCA module, while it can be configured (amazon S3, sftp,...) with additional modules from the `storage `_ repository. + +The imported files (and the files to be exported) are stored in Odoo as ``attachment.queue`` objects, defined by the `attachment_queue `_ module while the importation itself (resp. exportation) is realized by **"Attachments Import Tasks"** (resp. "Attachments Export Tasks") defined by this current module. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +As importation and exportation are different processes, they are triggered in different ways : + +**To import files**, you need to create an *"Attachment Import Task"* (menu *Settings > Technical > Attachments Import Tasks*) which defines : + +- where to find the files to import from the backend server (path to the files, selection pattern) +- what to do with the source files in the backend server (avoid duplicates, delete/rename after import...) +- how the files will be processed once imported (through the **File Type** field). + +.. image:: https://raw.githubusercontent.com/OCA/server-tools/12.0/attachment_synchronize/static/description/import_task.png + +.. epigraph:: + + 🔎 The **File Type** options are defined by other modules built to process the Attachments Queues with the same "File Type". + +**To export files**, you need first to register them as *"Attachments Queues"* objects linked to an *"Attachment Export Task"* (which set automatically their **File Type** to *"Export File (External Location)"*). + +Then, you can export one file at a time from the *Attachment Queue*'s form view, or export all the *Attachments Queues* in a pending state related to the same *Export Task* from the given *Export Task* form view (menu *Settings > Technical > Attachments Export Tasks*) : + +.. image:: https://raw.githubusercontent.com/OCA/server-tools/12.0/attachment_synchronize/static/description/export_task.png + +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 smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Akretion + +Contributors +~~~~~~~~~~~~ + +`Akretion `_ : + +- Valentin CHEMIERE +- Mourad EL HADJ MIMOUNE +- Florian DA COSTA +- Clément MOMBEREAU + +GS Lab: + +- Giovanni SERRA + +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. + +.. |maintainer-florian-dacosta| image:: https://github.com/florian-dacosta.png?size=40px + :target: https://github.com/florian-dacosta + :alt: florian-dacosta +.. |maintainer-GSLabIt| image:: https://github.com/GSLabIt.png?size=40px + :target: https://github.com/GSLabIt + :alt: GSLabIt +.. |maintainer-bealdav| image:: https://github.com/bealdav.png?size=40px + :target: https://github.com/bealdav + :alt: bealdav + +Current `maintainers `__: + +|maintainer-florian-dacosta| |maintainer-GSLabIt| |maintainer-bealdav| + +This module is part of the `OCA/server-tools `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/attachment_synchronize/__init__.py b/attachment_synchronize/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/attachment_synchronize/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/attachment_synchronize/__manifest__.py b/attachment_synchronize/__manifest__.py new file mode 100644 index 000000000..c816b1f71 --- /dev/null +++ b/attachment_synchronize/__manifest__.py @@ -0,0 +1,28 @@ +# @ 2016 florian DA COSTA @ Akretion +# © 2016 @author Mourad EL HADJ MIMOUNE +# @ 2020 Giovanni Serra @ GSlab.it +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Attachment Synchronize", + "version": "12.0.1.0.0", + "author": "Akretion,Odoo Community Association (OCA)", + "website": "https://github.com/oca/server-tools", + "maintainers": ["florian-dacosta", "sebastienbeau", "GSLabIt", "bealdav"], + "license": "AGPL-3", + "category": "Generic Modules", + "depends": [ + "attachment_queue", + "storage_backend", # https://github.com/OCA/storage + ], + "data": [ + "views/attachment_queue_views.xml", + "views/attachment_synchronize_task_views.xml", + "views/storage_backend_views.xml", + "data/cron.xml", + "security/ir.model.access.csv", + ], + "demo": ["demo/attachment_synchronize_task_demo.xml"], + "installable": True, + "development_status": "Beta", +} diff --git a/attachment_synchronize/data/cron.xml b/attachment_synchronize/data/cron.xml new file mode 100644 index 000000000..57b5e766e --- /dev/null +++ b/attachment_synchronize/data/cron.xml @@ -0,0 +1,16 @@ + + + + + Run attachment tasks import + 30 + minutes + -1 + False + + + code + model.run_task_import_scheduler() + + + diff --git a/attachment_synchronize/demo/attachment_synchronize_task_demo.xml b/attachment_synchronize/demo/attachment_synchronize_task_demo.xml new file mode 100644 index 000000000..f8e9879eb --- /dev/null +++ b/attachment_synchronize/demo/attachment_synchronize_task_demo.xml @@ -0,0 +1,25 @@ + + + + TEST Import + + import + delete + test_import + + + + TEST Export + + export + test_export + + + + bWlncmF0aW9uIHRlc3Q= + attachment_queue_imported_demo.doc + attachment_queue_imported_demo.doc + + + + diff --git a/attachment_synchronize/models/__init__.py b/attachment_synchronize/models/__init__.py new file mode 100644 index 000000000..afcda8bde --- /dev/null +++ b/attachment_synchronize/models/__init__.py @@ -0,0 +1 @@ +from . import attachment_queue, attachment_synchronize_task, storage_backend diff --git a/attachment_synchronize/models/attachment_queue.py b/attachment_synchronize/models/attachment_queue.py new file mode 100644 index 000000000..48327c76c --- /dev/null +++ b/attachment_synchronize/models/attachment_queue.py @@ -0,0 +1,39 @@ +# @ 2016 Florian DA COSTA @ Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import os +from odoo import api, models, fields + + +class AttachmentQueue(models.Model): + _inherit = "attachment.queue" + + task_id = fields.Many2one("attachment.synchronize.task", string="Task") + method_type = fields.Selection(related="task_id.method_type") + storage_backend_id = fields.Many2one( + "storage.backend", + string="Storage Backend", + related="task_id.backend_id", + store=True, + ) + file_type = fields.Selection( + selection_add=[("export", "Export File (External location)")] + ) + + def _run(self): + super()._run() + if self.file_type == "export": + path = os.path.join(self.task_id.filepath, self.datas_fname) + self.storage_backend_id._add_b64_data(path, self.datas) + + def _get_failure_emails(self): + res = super()._get_failure_emails() + if self.task_id.failure_emails: + res = self.task_id.failure_emails + return res + + @api.onchange("task_id") + def onchange_task_id(self): + for attachment in self: + if attachment.task_id.method_type == "export": + attachment.file_type = "export" diff --git a/attachment_synchronize/models/attachment_synchronize_task.py b/attachment_synchronize/models/attachment_synchronize_task.py new file mode 100644 index 000000000..436c79a6a --- /dev/null +++ b/attachment_synchronize/models/attachment_synchronize_task.py @@ -0,0 +1,209 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import datetime +import logging +import os + +import odoo +from odoo import api, fields, models, tools +from odoo.osv import expression + +_logger = logging.getLogger(__name__) + + +try: + # We use a jinja2 sandboxed environment to render mako templates. + # Note that the rendering does not cover all the mako syntax, in particular + # arbitrary Python statements are not accepted, and not all expressions are + # allowed: only "public" attributes (not starting with '_') of objects may + # be accessed. + # This is done on purpose: it prevents incidental or malicious execution of + # Python code that may break the security of the server. + from jinja2.sandbox import SandboxedEnvironment + + mako_template_env = SandboxedEnvironment( + variable_start_string="${", + variable_end_string="}", + line_statement_prefix="%", + trim_blocks=True, # do not output newline after blocks + ) + mako_template_env.globals.update( + { + "str": str, + "datetime": datetime, + "len": len, + "abs": abs, + "min": min, + "max": max, + "sum": sum, + "filter": filter, + "map": map, + "round": round, + } + ) +except ImportError: + _logger.warning("jinja2 not available, templating features will not work!") + + +class AttachmentSynchronizeTask(models.Model): + _name = "attachment.synchronize.task" + _description = "Attachment synchronize task" + + name = fields.Char(required=True) + method_type = fields.Selection( + [("import", "Import Task"), ("export", "Export Task")], required=True + ) + pattern = fields.Char( + string="Selection Pattern", + help="Pattern used to select the files to be imported following the 'fnmatch' " + "special characters (e.g. '*.txt' to catch all the text files).\n" + "If empty, import all the files found in 'File Path'.", + ) + filepath = fields.Char( + string="File Path", help="Path to imported/exported files in the Backend" + ) + backend_id = fields.Many2one("storage.backend", string="Backend") + attachment_ids = fields.One2many("attachment.queue", "task_id", string="Attachment") + move_path = fields.Char( + string="Move Path", help="Imported File will be moved to this path" + ) + new_name = fields.Char( + string="New Name", + help="Imported File will be renamed to this name.\n" + "New Name can use 'mako' template where 'obj' is the original file's name.\n" + "For instance : ${obj.name}-${obj.create_date}.csv", + ) + after_import = fields.Selection( + selection=[ + ("rename", "Rename"), + ("move", "Move"), + ("move_rename", "Move & Rename"), + ("delete", "Delete"), + ], + help="Action after import a file", + ) + file_type = fields.Selection( + selection=[], + string="File Type", + help="Used to fill the 'File Type' field in the imported 'Attachments Queues'." + "\nFurther operations will be realized on these Attachments Queues depending " + "on their 'File Type' value.", + ) + enabled = fields.Boolean("Enabled", default=True) + avoid_duplicated_files = fields.Boolean( + string="Avoid importing duplicated files", + help="If checked, a file will not be imported if an Attachment Queue with the " + "same name already exists.", + ) + failure_emails = fields.Char( + string="Failure Emails", + help="Used to fill the 'Failure Emails' field in the 'Attachments Queues' " + "related to this task.\nAn alert will be sent to these emails if any operation " + "on these Attachment Queue's file type fails.", + ) + + def _prepare_attachment_vals(self, data, filename): + self.ensure_one() + vals = { + "name": filename, + "datas": data, + "datas_fname": filename, + "task_id": self.id, + "file_type": self.file_type or False, + } + return vals + + @api.model + def _template_render(self, template, record): + try: + template = mako_template_env.from_string(tools.ustr(template)) + except Exception: + _logger.exception("Failed to load template '{}'".format(template)) + + variables = {"obj": record} + try: + render_result = template.render(variables) + except Exception: + _logger.exception( + "Failed to render template '{}'' using values '{}'".format( + template, variables + ) + ) + render_result = u"" + if render_result == u"False": + render_result = u"" + return render_result + + @api.model + def run_task_import_scheduler(self, domain=None): + if domain is None: + domain = [] + domain = expression.AND( + [domain, [("method_type", "=", "import"), ("enabled", "=", True)]] + ) + for task in self.search(domain): + task.run_import() + + def run_import(self): + self.ensure_one() + attach_obj = self.env["attachment.queue"] + backend = self.backend_id + filepath = self.filepath or "" + filenames = backend._list(relative_path=filepath, pattern=self.pattern) + if self.avoid_duplicated_files: + filenames = self._file_to_import(filenames) + total_import = 0 + for file_name in filenames: + with api.Environment.manage(): + with odoo.registry(self.env.cr.dbname).cursor() as new_cr: + new_env = api.Environment(new_cr, self.env.uid, self.env.context) + try: + full_absolute_path = os.path.join(filepath, file_name) + data = backend._get_b64_data(full_absolute_path) + attach_vals = self._prepare_attachment_vals(data, file_name) + attachment = attach_obj.with_env(new_env).create(attach_vals) + new_full_path = False + if self.after_import == "rename": + new_name = self._template_render(self.new_name, attachment) + new_full_path = os.path.join(filepath, new_name) + elif self.after_import == "move": + new_full_path = os.path.join(self.move_path, file_name) + elif self.after_import == "move_rename": + new_name = self._template_render(self.new_name, attachment) + new_full_path = os.path.join(self.move_path, new_name) + if new_full_path: + backend._add_b64_data(new_full_path, data) + if self.after_import in ( + "delete", + "rename", + "move", + "move_rename", + ): + backend._delete(full_absolute_path) + total_import += 1 + except Exception as e: + new_env.cr.rollback() + raise e + else: + new_env.cr.commit() + _logger.info("Run import complete! Imported {0} files".format(total_import)) + + def _file_to_import(self, filenames): + imported = ( + self.env["attachment.queue"] + .search([("name", "in", filenames)]) + .mapped("name") + ) + return list(set(filenames) - set(imported)) + + def run_export(self): + for task in self: + task.attachment_ids.filtered(lambda a: a.state == "pending").run() + + def button_toogle_enabled(self): + for rec in self: + rec.enabled = not rec.enabled + + def button_duplicate_record(self): + self.ensure_one() + self.copy({"enabled": False}) diff --git a/attachment_synchronize/models/storage_backend.py b/attachment_synchronize/models/storage_backend.py new file mode 100644 index 000000000..8bee3cf93 --- /dev/null +++ b/attachment_synchronize/models/storage_backend.py @@ -0,0 +1,67 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class StorageBackend(models.Model): + _inherit = "storage.backend" + + synchronize_task_ids = fields.One2many( + "attachment.synchronize.task", "backend_id", string="Tasks" + ) + import_task_count = fields.Integer( + "Import Tasks", compute="_compute_import_task_count" + ) + export_task_count = fields.Integer( + "Export Tasks", compute="_compute_export_task_count" + ) + + def _compute_import_task_count(self): + for rec in self: + rec.import_task_count = len( + rec.synchronize_task_ids.filtered(lambda t: t.method_type == "import") + ) + + def _compute_export_task_count(self): + for rec in self: + rec.export_task_count = len( + rec.synchronize_task_ids.filtered(lambda t: t.method_type == "export") + ) + + def action_related_import_task(self): + self.ensure_one() + + act_window_xml_id = "attachment_synchronize.action_attachment_import_task" + act_window = self.env.ref(act_window_xml_id).read()[0] + domain = [ + ("id", "in", self.synchronize_task_ids.ids), + ("method_type", "=", "import"), + ] + act_window["domain"] = domain + if self.import_task_count == 1: + form = self.env.ref("attachment_synchronize.view_attachment_task_form") + act_window["views"] = [(form.id, "form")] + act_window["res_id"] = ( + self.env["attachment.synchronize.task"].search(domain).id + ) + + return act_window + + def action_related_export_task(self): + self.ensure_one() + + act_window_xml_id = "attachment_synchronize.action_attachment_export_task" + act_window = self.env.ref(act_window_xml_id).read()[0] + domain = [ + ("id", "in", self.synchronize_task_ids.ids), + ("method_type", "=", "export"), + ] + act_window["domain"] = domain + if self.export_task_count == 1: + form = self.env.ref("attachment_synchronize.view_attachment_task_form") + act_window["views"] = [(form.id, "form")] + act_window["res_id"] = ( + self.env["attachment.synchronize.task"].search(domain).id + ) + + return act_window diff --git a/attachment_synchronize/readme/CONTRIBUTORS.rst b/attachment_synchronize/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..f48f581a3 --- /dev/null +++ b/attachment_synchronize/readme/CONTRIBUTORS.rst @@ -0,0 +1,10 @@ +`Akretion `_ : + +- Valentin CHEMIERE +- Mourad EL HADJ MIMOUNE +- Florian DA COSTA +- Clément MOMBEREAU + +GS Lab: + +- Giovanni SERRA diff --git a/attachment_synchronize/readme/DESCRIPTION.rst b/attachment_synchronize/readme/DESCRIPTION.rst new file mode 100644 index 000000000..a161774b6 --- /dev/null +++ b/attachment_synchronize/readme/DESCRIPTION.rst @@ -0,0 +1,5 @@ +This module allows to **import/export files** from/to backend servers. + +A backend server is defined by the basic `storage_backend `_ OCA module, while it can be configured (amazon S3, sftp,...) with additional modules from the `storage `_ repository. + +The imported files (and the files to be exported) are stored in Odoo as ``attachment.queue`` objects, defined by the `attachment_queue `_ module while the importation itself (resp. exportation) is realized by **"Attachments Import Tasks"** (resp. "Attachments Export Tasks") defined by this current module. diff --git a/attachment_synchronize/readme/USAGE.rst b/attachment_synchronize/readme/USAGE.rst new file mode 100644 index 000000000..f4714693d --- /dev/null +++ b/attachment_synchronize/readme/USAGE.rst @@ -0,0 +1,19 @@ +As importation and exportation are different processes, they are triggered in different ways : + +**To import files**, you need to create an *"Attachment Import Task"* (menu *Settings > Technical > Attachments Import Tasks*) which defines : + +- where to find the files to import from the backend server (path to the files, selection pattern) +- what to do with the source files in the backend server (avoid duplicates, delete/rename after import...) +- how the files will be processed once imported (through the **File Type** field). + +.. image:: ../static/description/import_task.png + +.. epigraph:: + + 🔎 The **File Type** options are defined by other modules built to process the Attachments Queues with the same "File Type". + +**To export files**, you need first to register them as *"Attachments Queues"* objects linked to an *"Attachment Export Task"* (which set automatically their **File Type** to *"Export File (External Location)"*). + +Then, you can export one file at a time from the *Attachment Queue*'s form view, or export all the *Attachments Queues* in a pending state related to the same *Export Task* from the given *Export Task* form view (menu *Settings > Technical > Attachments Export Tasks*) : + +.. image:: ../static/description/export_task.png diff --git a/attachment_synchronize/security/ir.model.access.csv b/attachment_synchronize/security/ir.model.access.csv new file mode 100644 index 000000000..742f94e2f --- /dev/null +++ b/attachment_synchronize/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_attachment_synchronize_task_manager,attachment.synchronize.task.manager,model_attachment_synchronize_task,base.group_system,1,1,1,1 diff --git a/attachment_synchronize/static/description/export_task.png b/attachment_synchronize/static/description/export_task.png new file mode 100644 index 000000000..8e8bc8834 Binary files /dev/null and b/attachment_synchronize/static/description/export_task.png differ diff --git a/attachment_synchronize/static/description/icon.png b/attachment_synchronize/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/attachment_synchronize/static/description/icon.png differ diff --git a/attachment_synchronize/static/description/import_task.png b/attachment_synchronize/static/description/import_task.png new file mode 100644 index 000000000..9aee76f57 Binary files /dev/null and b/attachment_synchronize/static/description/import_task.png differ diff --git a/attachment_synchronize/static/description/index.html b/attachment_synchronize/static/description/index.html new file mode 100644 index 000000000..f378652c7 --- /dev/null +++ b/attachment_synchronize/static/description/index.html @@ -0,0 +1,448 @@ + + + + + + +Attachment Synchronize + + + +
+

Attachment Synchronize

+ + +

Beta License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runbot

+

This module allows to import/export files from/to backend servers.

+

A backend server is defined by the basic storage_backend OCA module, while it can be configured (amazon S3, sftp,…) with additional modules from the storage repository.

+

The imported files (and the files to be exported) are stored in Odoo as attachment.queue objects, defined by the attachment_queue module while the importation itself (resp. exportation) is realized by “Attachments Import Tasks” (resp. “Attachments Export Tasks”) defined by this current module.

+

Table of contents

+ +
+

Usage

+

As importation and exportation are different processes, they are triggered in different ways :

+

To import files, you need to create an “Attachment Import Task” (menu Settings > Technical > Attachments Import Tasks) which defines :

+
    +
  • where to find the files to import from the backend server (path to the files, selection pattern)
  • +
  • what to do with the source files in the backend server (avoid duplicates, delete/rename after import…)
  • +
  • how the files will be processed once imported (through the File Type field).
  • +
+https://raw.githubusercontent.com/OCA/server-tools/12.0/attachment_synchronize/static/description/import_task.png +
+🔎 The File Type options are defined by other modules built to process the Attachments Queues with the same “File Type”.
+

To export files, you need first to register them as “Attachments Queues” objects linked to an “Attachment Export Task” (which set automatically their File Type to “Export File (External Location)”).

+

Then, you can export one file at a time from the Attachment Queue’s form view, or export all the Attachments Queues in a pending state related to the same Export Task from the given Export Task form view (menu Settings > Technical > Attachments Export Tasks) :

+https://raw.githubusercontent.com/OCA/server-tools/12.0/attachment_synchronize/static/description/export_task.png +
+
+

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 smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+
+

Contributors

+

Akretion :

+ +

GS Lab:

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

Current maintainers:

+

florian-dacosta GSLabIt bealdav

+

This module is part of the OCA/server-tools project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/attachment_synchronize/tests/__init__.py b/attachment_synchronize/tests/__init__.py new file mode 100644 index 000000000..3845a51ae --- /dev/null +++ b/attachment_synchronize/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_import +from . import test_export diff --git a/attachment_synchronize/tests/common.py b/attachment_synchronize/tests/common.py new file mode 100644 index 000000000..ddaa26da0 --- /dev/null +++ b/attachment_synchronize/tests/common.py @@ -0,0 +1,43 @@ +# Copyright 2020 Akretion (http://www.akretion.com). +# @author SĂ©bastien BEAU +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import mock +import os +from odoo.addons.storage_backend.tests.common import Common + + +class SyncCommon(Common): + def _clean_testing_directory(self): + for test_dir in [ + self.directory_input, + self.directory_output, + self.directory_archived, + ]: + for filename in self.backend._list(test_dir): + self.backend._delete(os.path.join(test_dir, filename)) + + def _create_test_file(self): + self.backend._add_b64_data( + os.path.join(self.directory_input, "bar.txt"), + self.filedata, + mimetype=u"text/plain", + ) + + def setUp(self): + super().setUp() + self.env.cr.commit = mock.Mock() + self.registry.enter_test_mode(self.env.cr) + self.directory_input = "test_import" + self.directory_output = "test_export" + self.directory_archived = "test_archived" + self._clean_testing_directory() + self._create_test_file() + self.task = self.env.ref( + "attachment_synchronize.import_from_filestore" + ) + + def tearDown(self): + self.registry.leave_test_mode() + self._clean_testing_directory() + super().tearDown() diff --git a/attachment_synchronize/tests/test_export.py b/attachment_synchronize/tests/test_export.py new file mode 100644 index 000000000..18ce825a3 --- /dev/null +++ b/attachment_synchronize/tests/test_export.py @@ -0,0 +1,42 @@ +# Copyright 2020 Akretion (http://www.akretion.com). +# @author SĂ©bastien BEAU +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import mock +from .common import SyncCommon +from odoo.tools import mute_logger + + +def raising_side_effect(*args, **kwargs): + raise Exception("Boom") + + +class TestExport(SyncCommon): + def setUp(self): + super().setUp() + self.task = self.env.ref("attachment_synchronize.export_to_filestore") + self.attachment = self.env["attachment.queue"].create( + { + "name": "foo.txt", + "datas_fname": "foo.txt", + "task_id": self.task.id, + "file_type": "export", + "datas": self.filedata, + } + ) + + def test_export(self): + self.attachment.run() + result = self.backend._list("test_export") + self.assertEqual(result, ["foo.txt"]) + + @mute_logger("odoo.addons.attachment_queue.models.attachment_queue") + def test_failing_export(self): + with mock.patch.object( + type(self.backend), + "_add_b64_data", + side_effect=raising_side_effect, + ): + self.attachment.run() + self.assertEqual(self.attachment.state, "failed") + self.assertEqual(self.attachment.state_message, "Boom") diff --git a/attachment_synchronize/tests/test_import.py b/attachment_synchronize/tests/test_import.py new file mode 100644 index 000000000..611555763 --- /dev/null +++ b/attachment_synchronize/tests/test_import.py @@ -0,0 +1,83 @@ +# Copyright 2020 Akretion (http://www.akretion.com). +# @author SĂ©bastien BEAU +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from .common import SyncCommon + + +class TestImport(SyncCommon): + @property + def archived_files(self): + return self.backend._list(self.directory_archived) + + @property + def input_files(self): + return self.backend._list(self.directory_input) + + def _check_attachment_created(self, count=1): + attachment = self.env["attachment.queue"].search([("name", "=", "bar.txt")]) + self.assertEqual(len(attachment), count) + + def test_import_with_rename(self): + self.task.write({"after_import": "rename", "new_name": "foo.txt"}) + self.task.run_import() + self._check_attachment_created() + self.assertEqual(self.input_files, ["foo.txt"]) + self.assertEqual(self.archived_files, []) + + def test_import_with_move(self): + self.task.write({"after_import": "move", "move_path": self.directory_archived}) + self.task.run_import() + self._check_attachment_created() + self.assertEqual(self.input_files, []) + self.assertEqual(self.archived_files, ["bar.txt"]) + + def test_import_with_move_and_rename(self): + self.task.write( + { + "after_import": "move_rename", + "new_name": "foo.txt", + "move_path": self.directory_archived, + } + ) + self.task.run_import() + self._check_attachment_created() + self.assertEqual(self.input_files, []) + self.assertEqual(self.archived_files, ["foo.txt"]) + + def test_import_with_delete(self): + self.task.write({"after_import": "delete"}) + self.task.run_import() + self._check_attachment_created() + self.assertEqual(self.input_files, []) + self.assertEqual(self.archived_files, []) + + def test_import_twice(self): + self.task.write({"after_import": "delete"}) + self.task.run_import() + self._check_attachment_created(count=1) + + self._create_test_file() + self.task.run_import() + self._check_attachment_created(count=2) + + def test_import_twice_no_duplicate(self): + self.task.write( + {"after_import": "delete", "avoid_duplicated_files": True} + ) + self.task.run_import() + self._check_attachment_created(count=1) + + self._create_test_file() + self.task.run_import() + self._check_attachment_created(count=1) + + def test_running_cron(self): + self.task.write({"after_import": "delete"}) + self.env["attachment.synchronize.task"].run_task_import_scheduler() + self._check_attachment_created(count=1) + + def test_running_cron_disable_task(self): + self.task.write({"after_import": "delete", "enabled": False}) + self.env["attachment.synchronize.task"].run_task_import_scheduler() + self._check_attachment_created(count=0) diff --git a/attachment_synchronize/views/attachment_queue_views.xml b/attachment_synchronize/views/attachment_queue_views.xml new file mode 100644 index 000000000..b62863594 --- /dev/null +++ b/attachment_synchronize/views/attachment_queue_views.xml @@ -0,0 +1,35 @@ + + + + + attachment.queue + + + + + + + + + + + + + attachment.queue + + + + state == 'done' + + + + + + + + + diff --git a/attachment_synchronize/views/attachment_synchronize_task_views.xml b/attachment_synchronize/views/attachment_synchronize_task_views.xml new file mode 100644 index 000000000..7fe1d7999 --- /dev/null +++ b/attachment_synchronize/views/attachment_synchronize_task_views.xml @@ -0,0 +1,177 @@ + + + + attachment.synchronize.task + +
+
+
+
+
+ +
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + attachment.synchronize.task + + + + + + + + + + + + + + + + +