diff --git a/attachment_zipped_download/__init__.py b/attachment_zipped_download/__init__.py new file mode 100644 index 00000000..9e5827f9 --- /dev/null +++ b/attachment_zipped_download/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import controllers +from . import models diff --git a/attachment_zipped_download/__openerp__.py b/attachment_zipped_download/__openerp__.py new file mode 100644 index 00000000..f0324661 --- /dev/null +++ b/attachment_zipped_download/__openerp__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Attachment Zipped Download", + "version": "8.0.1.0.0", + "category": "Tools", + "website": "https://github.com/OCA/knowledge", + "author": "Tecnativa, Odoo Community Association (OCA)", + "license": "AGPL-3", + "depends": ["base"], + "data": [ + "views/ir_attachment_view.xml", + ], + "installable": True, +} diff --git a/attachment_zipped_download/controllers/__init__.py b/attachment_zipped_download/controllers/__init__.py new file mode 100644 index 00000000..757b12a1 --- /dev/null +++ b/attachment_zipped_download/controllers/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import main diff --git a/attachment_zipped_download/controllers/main.py b/attachment_zipped_download/controllers/main.py new file mode 100644 index 00000000..1ebda6fd --- /dev/null +++ b/attachment_zipped_download/controllers/main.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 César Fernández Domínguez +# Copyright 2022 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from openerp import _, http +from openerp.http import request + + +class AttachmentZippedDownloadController(http.Controller): + + @http.route("/web/attachment/download_zip", type="http", auth="user") + def download_zip(self, ids=None, debug=0): + ids = [] if not ids else ids + if len(ids) == 0: + return + list_ids = map(int, ids.split(",")) + out_file = request.env["ir.attachment"].browse(list_ids)._create_temp_zip() + return http.send_file( + filepath_or_fp=out_file, + mimetype="application/zip", + as_attachment=True, + filename=_("attachments.zip"), + ) diff --git a/attachment_zipped_download/models/__init__.py b/attachment_zipped_download/models/__init__.py new file mode 100644 index 00000000..dca3fe04 --- /dev/null +++ b/attachment_zipped_download/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import ir_attachment +from . import ir_attachment_action_download diff --git a/attachment_zipped_download/models/ir_attachment.py b/attachment_zipped_download/models/ir_attachment.py new file mode 100644 index 00000000..6d49dfb3 --- /dev/null +++ b/attachment_zipped_download/models/ir_attachment.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 César Fernández Domínguez +# Copyright 2022 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +import base64 +import zipfile +from io import BytesIO + +from openerp import _, api, models +from openerp.exceptions import Warning as UserError + + +class IrAttachment(models.Model): + _inherit = "ir.attachment" + + @api.multi + def action_attachments_download(self): + items = self.filtered(lambda x: x.type == "binary") + if not items: + raise UserError( + _("None attachment selected. Only binary attachments allowed.") + ) + item_names = [it._compute_zip_file_name() for it in items] + if len(item_names) != len(set(item_names)): + raise UserError( + _("All file names must be unique.") + ) + ids = ",".join(map(str, items.ids)) + return { + "type": "ir.actions.act_url", + "url": "/web/attachment/download_zip?ids=%s" % (ids), + "target": "self", + } + + @api.multi + def _create_temp_zip(self): + zip_buffer = BytesIO() + with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file: + for attachment in self: + attachment.check("read") + zip_file.writestr( + attachment._compute_zip_file_name(), + base64.b64decode(attachment.datas), + ) + zip_file.close() + return zip_buffer + + @api.multi + def _compute_zip_file_name(self): + """Give a chance of easily changing the name of the file inside the ZIP.""" + self.ensure_one() + return self.name diff --git a/attachment_zipped_download/models/ir_attachment_action_download.py b/attachment_zipped_download/models/ir_attachment_action_download.py new file mode 100644 index 00000000..4218a607 --- /dev/null +++ b/attachment_zipped_download/models/ir_attachment_action_download.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Copyright 2023 Foodles (https://www.foodles.com/) +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from openerp import _, api, models + + +class IrAttachmentActionDownloadMixin(models.AbstractModel): + _name = "ir.attachment.action_download" + _description = """ + Mixin to help download attachments linked to record(s). + """ + + @api.multi + def _get_downloadable_attachments(self): + """Give a chance to easily overwrite this method + on sub modules to limit restict attachement able to downloads + + In some case we probably want the user download some specific + document that are probably related to the current model + + By default return all attachment link the the record. + """ + return self.env["ir.attachment"].search( + [("res_model", "=", self._name), ("res_id", "in", self.ids)] + ) + + @api.multi + def action_download_attachments(self): + """Return action to: + * emit a warning message if no attachment found + * download a file if only 1 file found + * zip and download the list of attachment returns by `_get_downloadable_attachments` + """ + attachments = self._get_downloadable_attachments() + if not attachments: + title = _("No attachment!") + text = _("There is no document found to download.") + return { + "type": "ir.actions.client", + "tag": "action_warn", + "params": { + "title": title, + "text": text, + "sticky": True, + }, + } + + if len(attachments) == 1: + return { + "target": "self", + "type": "ir.actions.act_url", + "url": "/web/binary/saveas?model=ir.attachment&field=datas&filename_field=name&id=%s" % attachments.id, + } + else: + return attachments.action_attachments_download() diff --git a/attachment_zipped_download/readme/CONTRIBUTORS.rst b/attachment_zipped_download/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..4debd5c6 --- /dev/null +++ b/attachment_zipped_download/readme/CONTRIBUTORS.rst @@ -0,0 +1,12 @@ +* César Fernández Domínguez + +* `Tecnativa `_: + + * Víctor Martínez + * Pedro M. Baeza + +* Pierre Verkest + +* `Ecosoft `_: + + * Tharathip Chaweewongphan diff --git a/attachment_zipped_download/readme/DESCRIPTION.rst b/attachment_zipped_download/readme/DESCRIPTION.rst new file mode 100644 index 00000000..38725937 --- /dev/null +++ b/attachment_zipped_download/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +This module allows downloading multiple attachments as a zip file. + +This also provide a helper class `IrAttachmentActionDownloadMixin` +to be used by developer to add action method on models. diff --git a/attachment_zipped_download/readme/USAGE.rst b/attachment_zipped_download/readme/USAGE.rst new file mode 100644 index 00000000..e6abba29 --- /dev/null +++ b/attachment_zipped_download/readme/USAGE.rst @@ -0,0 +1,86 @@ +#. Go to *Settings > Technical > Database Structure > Attachments* and select some files. +#. Go to *More > Download* and a zip file containing the selected files will be downloaded. + +## For developer + +You can reuse the `IrAttachmentActionDownloadMixin` on your +favorite models:: + + from openerp import models + + + class StockPicking(models.Model): + _name = "stock.picking" + _inherit = ["stock.picking", "ir.attachment.action_download"] + + +Then you can add an action button on list view line or on the action button +(when multiple lines are selected) to download all files:: + + + + + + stock.picking.tree download attachments + stock.picking + + +