Merge PR #520 into 18.0

Signed-off-by pedrobaeza
This commit is contained in:
OCA-git-bot 2025-02-18 08:49:00 +00:00
commit b86f27f7af
27 changed files with 1491 additions and 0 deletions

View File

@ -0,0 +1,174 @@
==========================
Attachment Zipped Download
==========================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:63602db7f612916c91796c11238e746b60396e495f8df6bb7f4bcbff442e56d1
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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%2Fknowledge-lightgray.png?logo=github
:target: https://github.com/OCA/knowledge/tree/18.0/attachment_zipped_download
:alt: OCA/knowledge
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/knowledge-18-0/knowledge-18-0-attachment_zipped_download
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/knowledge&target_branch=18.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
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.
**Table of contents**
.. contents::
:local:
Usage
=====
1. Go to *Settings > Technical > Database Structure > Attachments* and
select some files.
2. Go to *Actions > Download* and a zip file containing the selected
files will be downloaded.
## For developer
You can reuse the IrAttachmentActionDownloadMixin on your favorite
models:
::
from odoo 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:
::
<odoo>
<!--
add a button on list view to download all attachement present
on the given transfert
-->
<record id="vpicktree" model="ir.ui.view">
<field name="inherit_id" ref="stock.vpicktree"/>
<field name="name">stock.picking.tree download attachments</field>
<field name="model">stock.picking</field>
<field name="arch" type="xml">
<field name="json_popover" position="after">
<button name="action_download_attachments"
type="object"
icon="fa-download"
string="Download attachment(s)"
aria-label="Download Proof documents"
class="float-right"/>
</field>
</field>
</record>
<!--
Add "Download attachments" item in the Action menu when
multiple records are selected
-->
<record id="action_download_picking_attachements" model="ir.actions.server">
<field name="name">Download attachments</field>
<field name="model_id" ref="stock.model_stock_picking"/>
<field name="binding_model_id" ref="stock.model_stock_picking"/>
<field name="binding_view_types">list</field>
<field name="state">code</field>
<field name="code">
action = records.action_download_attachments()
</field>
</record>
</odoo>
.. note::
Even you will be able to generate a zip file with multiple document
with the same name it's advice to overwrite \_compute_zip_file_name
to improve the name. When a slash (/) is present in the path it will
create a directory. This example will create a directory per
stock.picking using its name:
::
class IrAttachment(models.Model):
_inherit = "ir.attachment"
def _compute_zip_file_name(self):
self.ensure_one()
if self.res_model and self.res_model == "stock.picking":
return (
self.env[self.res_model]
.browse(self.res_id)
.display_name.replace("/", "-")
+ "/"
+ self.name
)
return super()._compute_zip_file_name()
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/knowledge/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/knowledge/issues/new?body=module:%20attachment_zipped_download%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
-------
* Tecnativa
Contributors
------------
- César Fernández Domínguez <cesfernandez@outlook.com>
- `Tecnativa <https://www.tecnativa.com>`__:
- Víctor Martínez
- Pedro M. Baeza
- Pierre Verkest <pierreverkest@gmail.com>
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.
This module is part of the `OCA/knowledge <https://github.com/OCA/knowledge/tree/18.0/attachment_zipped_download>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@ -0,0 +1,2 @@
from . import controllers
from . import models

View File

@ -0,0 +1,15 @@
# Copyright 2022 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Attachment Zipped Download",
"version": "18.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,
}

View File

@ -0,0 +1 @@
from . import main

View File

@ -0,0 +1,23 @@
# Copyright 2019 César Fernández Domínguez <cesfernandez@outlook.com>
# Copyright 2022 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from odoo import _, http
from odoo.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()
stream = http.Stream(
type="data",
data=out_file.getvalue(),
mimetype="application/zip",
as_attachment=True,
download_name=_("attachments.zip"),
)
return stream.get_response()

View File

@ -0,0 +1,60 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * attachment_zipped_download
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 17.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: attachment_zipped_download
#: model:ir.model,name:attachment_zipped_download.model_ir_attachment_action_download
msgid ""
"\n"
" Mixin to help download attachments linked to record(s).\n"
" "
msgstr ""
#. module: attachment_zipped_download
#: model:ir.model,name:attachment_zipped_download.model_ir_attachment
msgid "Attachment"
msgstr ""
#. module: attachment_zipped_download
#: model:ir.actions.server,name:attachment_zipped_download.action_attachments_download
msgid "Download"
msgstr ""
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/models/ir_attachment_action_download.py:0
#, python-format
msgid "No attachment!"
msgstr ""
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/models/ir_attachment.py:0
#, python-format
msgid "None attachment selected. Only binary attachments allowed."
msgstr ""
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/models/ir_attachment_action_download.py:0
#, python-format
msgid "There is no document found to download."
msgstr ""
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/controllers/main.py:0
#, python-format
msgid "attachments.zip"
msgstr ""

View File

@ -0,0 +1,70 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * attachment_zipped_download
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-03-15 14:04+0000\n"
"PO-Revision-Date: 2023-09-07 16:37+0000\n"
"Last-Translator: Ivorra78 <informatica@totmaterial.es>\n"
"Language-Team: \n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.17\n"
#. module: attachment_zipped_download
#: model:ir.model,name:attachment_zipped_download.model_ir_attachment_action_download
msgid ""
"\n"
" Mixin to help download attachments linked to record(s).\n"
" "
msgstr ""
"\n"
" Mixin para ayudar a descargar archivos adjuntos vinculados a "
"registros.\n"
" "
#. module: attachment_zipped_download
#: model:ir.model,name:attachment_zipped_download.model_ir_attachment
msgid "Attachment"
msgstr "Adjunto"
#. module: attachment_zipped_download
#: model:ir.actions.server,name:attachment_zipped_download.action_attachments_download
msgid "Download"
msgstr "Descargar"
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/models/ir_attachment_action_download.py:0
#, python-format
msgid "No attachment!"
msgstr "¡No hay adjuntos!"
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/models/ir_attachment.py:0
#, python-format
msgid "None attachment selected. Only binary attachments allowed."
msgstr ""
"No se seleccionó ningún archivo adjunto. Solo se permiten archivos adjuntos "
"binarios."
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/models/ir_attachment_action_download.py:0
#, python-format
msgid "There is no document found to download."
msgstr "No se encontró ningún documento para descargar."
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/controllers/main.py:0
#, python-format
msgid "attachments.zip"
msgstr "adjuntos.zip"

View File

@ -0,0 +1,65 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * attachment_zipped_download
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: attachment_zipped_download
#: model:ir.model,name:attachment_zipped_download.model_ir_attachment_action_download
msgid ""
"\n"
" Mixin to help download attachments linked to record(s).\n"
" "
msgstr ""
"Mixin pour faciliter le téléchargements des pièces jointes lié aux "
"enregistrements."
#. module: attachment_zipped_download
#: model:ir.model,name:attachment_zipped_download.model_ir_attachment
msgid "Attachment"
msgstr "Pièce jointe"
#. module: attachment_zipped_download
#: model:ir.actions.server,name:attachment_zipped_download.action_attachments_download
msgid "Download"
msgstr "Télécharger"
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/models/ir_attachment_action_download.py:0
#, python-format
msgid "No attachment!"
msgstr "Aucune pièce jointe !"
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/models/ir_attachment.py:0
#, python-format
msgid "None attachment selected. Only binary attachments allowed."
msgstr ""
"Aucune pièce jointe sélectionnée. Seul les picèces jointes de type Binaire "
"sont permises."
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/models/ir_attachment_action_download.py:0
#, python-format
msgid "There is no document found to download."
msgstr "Aucune pièce jointe téléchargeable trouvé."
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/controllers/main.py:0
#, python-format
msgid "attachments.zip"
msgstr "pieces-jointes.zip"

View File

@ -0,0 +1,64 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * attachment_zipped_download
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2023-02-15 21:23+0000\n"
"Last-Translator: Bole <bole@dajmi5.com>\n"
"Language-Team: none\n"
"Language: hr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 4.14.1\n"
#. module: attachment_zipped_download
#: model:ir.model,name:attachment_zipped_download.model_ir_attachment_action_download
msgid ""
"\n"
" Mixin to help download attachments linked to record(s).\n"
" "
msgstr ""
#. module: attachment_zipped_download
#: model:ir.model,name:attachment_zipped_download.model_ir_attachment
msgid "Attachment"
msgstr "Prilog"
#. module: attachment_zipped_download
#: model:ir.actions.server,name:attachment_zipped_download.action_attachments_download
msgid "Download"
msgstr "Preuzimanje"
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/models/ir_attachment_action_download.py:0
#, python-format
msgid "No attachment!"
msgstr ""
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/models/ir_attachment.py:0
#, python-format
msgid "None attachment selected. Only binary attachments allowed."
msgstr "Nema odabranih priloga. Samo binarni prilozi su dozvoljeni."
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/models/ir_attachment_action_download.py:0
#, python-format
msgid "There is no document found to download."
msgstr ""
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/controllers/main.py:0
#, python-format
msgid "attachments.zip"
msgstr "attachments.zip"

View File

@ -0,0 +1,66 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * attachment_zipped_download
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2023-09-20 19:38+0000\n"
"Last-Translator: mymage <stefano.consolaro@mymage.it>\n"
"Language-Team: none\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.17\n"
#. module: attachment_zipped_download
#: model:ir.model,name:attachment_zipped_download.model_ir_attachment_action_download
msgid ""
"\n"
" Mixin to help download attachments linked to record(s).\n"
" "
msgstr ""
"\n"
" Mixin per aiutare a scaricare gli allegati collegati ai record.\n"
" "
#. module: attachment_zipped_download
#: model:ir.model,name:attachment_zipped_download.model_ir_attachment
msgid "Attachment"
msgstr "Allegato"
#. module: attachment_zipped_download
#: model:ir.actions.server,name:attachment_zipped_download.action_attachments_download
msgid "Download"
msgstr "Scarica"
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/models/ir_attachment_action_download.py:0
#, python-format
msgid "No attachment!"
msgstr "Nessun allegato!"
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/models/ir_attachment.py:0
#, python-format
msgid "None attachment selected. Only binary attachments allowed."
msgstr "Nessun allegato selezionato. Consentiti solo allegati binari."
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/models/ir_attachment_action_download.py:0
#, python-format
msgid "There is no document found to download."
msgstr "Non sono stati trovati documenti da scaricare."
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/controllers/main.py:0
#, python-format
msgid "attachments.zip"
msgstr "attachments.zip"

View File

@ -0,0 +1,66 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * attachment_zipped_download
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-02-28 21:33+0000\n"
"Last-Translator: Peter Romão <peterromao@yahoo.co.uk>\n"
"Language-Team: none\n"
"Language: pt\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 4.17\n"
#. module: attachment_zipped_download
#: model:ir.model,name:attachment_zipped_download.model_ir_attachment_action_download
msgid ""
"\n"
" Mixin to help download attachments linked to record(s).\n"
" "
msgstr ""
"\n"
" Mixin para ajudar a baixar anexos vinculados ao(s) registo(s).\n"
" "
#. module: attachment_zipped_download
#: model:ir.model,name:attachment_zipped_download.model_ir_attachment
msgid "Attachment"
msgstr "Anexo"
#. module: attachment_zipped_download
#: model:ir.actions.server,name:attachment_zipped_download.action_attachments_download
msgid "Download"
msgstr "Transferir"
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/models/ir_attachment_action_download.py:0
#, python-format
msgid "No attachment!"
msgstr "Sem Anexo!"
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/models/ir_attachment.py:0
#, python-format
msgid "None attachment selected. Only binary attachments allowed."
msgstr "Nenhum anexo selecionado. Somente anexos binários permitidos."
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/models/ir_attachment_action_download.py:0
#, python-format
msgid "There is no document found to download."
msgstr "Não há nenhum documento encontrado para transferir."
#. module: attachment_zipped_download
#. odoo-python
#: code:addons/attachment_zipped_download/controllers/main.py:0
#, python-format
msgid "attachments.zip"
msgstr "anexos.zip"

View File

@ -0,0 +1,2 @@
from . import ir_attachment
from . import ir_attachment_action_download

View File

@ -0,0 +1,43 @@
# Copyright 2019 César Fernández Domínguez <cesfernandez@outlook.com>
# Copyright 2022 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
import zipfile
from io import BytesIO
from odoo import models
from odoo.exceptions import UserError
class IrAttachment(models.Model):
_inherit = "ir.attachment"
def action_attachments_download(self):
items = self.filtered(lambda x: x.type == "binary")
if not items:
raise UserError(
self.env._("None attachment selected. Only binary attachments allowed.")
)
ids = ",".join(map(str, items.ids))
return {
"type": "ir.actions.act_url",
"url": f"/web/attachment/download_zip?ids={ids}",
"target": "self",
}
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(),
attachment.raw,
)
zip_buffer.seek(0)
zip_file.close()
return zip_buffer
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

View File

@ -0,0 +1,55 @@
# Copyright 2023 Foodles (https://www.foodles.com/)
# @author Pierre Verkest <pierreverkest84@gmail.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import models
class IrAttachmentActionDownloadMixin(models.AbstractModel):
_name = "ir.attachment.action_download"
_description = """
Mixin to help download attachments linked to record(s).
"""
def _get_downloadable_attachments(self):
"""Give a chance to easily overwrite this method
on sub modules to limit restict attachement able to downlaods
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)]
)
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 = self.env._("No attachment!")
message = self.env._("There is no document found to download.")
return {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"type": "warning",
"title": title,
"message": message,
"sticky": True,
},
}
if len(attachments) == 1:
return {
"target": "self",
"type": "ir.actions.act_url",
"url": f"/web/content/{attachments.id}?download=1",
}
else:
return attachments.action_attachments_download()

View File

@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View File

@ -0,0 +1,5 @@
- César Fernández Domínguez \<<cesfernandez@outlook.com>\>
- [Tecnativa](https://www.tecnativa.com):
- Víctor Martínez
- Pedro M. Baeza
- Pierre Verkest \<<pierreverkest@gmail.com>\>

View File

@ -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.

View File

@ -0,0 +1,78 @@
1. Go to *Settings \> Technical \> Database Structure \> Attachments*
and select some files.
2. Go to *Actions \> Download* and a zip file containing the selected
files will be downloaded.
\## For developer
You can reuse the IrAttachmentActionDownloadMixin on your favorite
models:
from odoo 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:
<odoo>
<!--
add a button on list view to download all attachement present
on the given transfert
-->
<record id="vpicktree" model="ir.ui.view">
<field name="inherit_id" ref="stock.vpicktree"/>
<field name="name">stock.picking.tree download attachments</field>
<field name="model">stock.picking</field>
<field name="arch" type="xml">
<field name="json_popover" position="after">
<button name="action_download_attachments"
type="object"
icon="fa-download"
string="Download attachment(s)"
aria-label="Download Proof documents"
class="float-right"/>
</field>
</field>
</record>
<!--
Add "Download attachments" item in the Action menu when
multiple records are selected
-->
<record id="action_download_picking_attachements" model="ir.actions.server">
<field name="name">Download attachments</field>
<field name="model_id" ref="stock.model_stock_picking"/>
<field name="binding_model_id" ref="stock.model_stock_picking"/>
<field name="binding_view_types">list</field>
<field name="state">code</field>
<field name="code">
action = records.action_download_attachments()
</field>
</record>
</odoo>
> [!NOTE]
> Even you will be able to generate a zip file with multiple document
> with the same name it's advice to overwrite \_compute_zip_file_name to
> improve the name. When a slash (/) is present in the path it will
> create a directory. This example will create a directory per
> stock.picking using its name:
>
> class IrAttachment(models.Model):
> _inherit = "ir.attachment"
>
> def _compute_zip_file_name(self):
> self.ensure_one()
> if self.res_model and self.res_model == "stock.picking":
> return (
> self.env[self.res_model]
> .browse(self.res_id)
> .display_name.replace("/", "-")
> + "/"
> + self.name
> )
> return super()._compute_zip_file_name()

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -0,0 +1,516 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Attachment Zipped Download</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="attachment-zipped-download">
<h1 class="title">Attachment Zipped Download</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:63602db7f612916c91796c11238e746b60396e495f8df6bb7f4bcbff442e56d1
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/knowledge/tree/18.0/attachment_zipped_download"><img alt="OCA/knowledge" src="https://img.shields.io/badge/github-OCA%2Fknowledge-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/knowledge-18-0/knowledge-18-0-attachment_zipped_download"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/knowledge&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module allows downloading multiple attachments as a zip file.</p>
<p>This also provide a helper class IrAttachmentActionDownloadMixin to be
used by developer to add action method on models.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-2">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-3">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-4">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-5">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-6">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
<ol class="arabic simple">
<li>Go to <em>Settings &gt; Technical &gt; Database Structure &gt; Attachments</em> and
select some files.</li>
<li>Go to <em>Actions &gt; Download</em> and a zip file containing the selected
files will be downloaded.</li>
</ol>
<p>## For developer</p>
<p>You can reuse the IrAttachmentActionDownloadMixin on your favorite
models:</p>
<pre class="literal-block">
from odoo import models
class StockPicking(models.Model):
_name = &quot;stock.picking&quot;
_inherit = [&quot;stock.picking&quot;, &quot;ir.attachment.action_download&quot;]
</pre>
<p>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:</p>
<pre class="literal-block">
&lt;odoo&gt;
&lt;!--
add a button on list view to download all attachement present
on the given transfert
--&gt;
&lt;record id=&quot;vpicktree&quot; model=&quot;ir.ui.view&quot;&gt;
&lt;field name=&quot;inherit_id&quot; ref=&quot;stock.vpicktree&quot;/&gt;
&lt;field name=&quot;name&quot;&gt;stock.picking.tree download attachments&lt;/field&gt;
&lt;field name=&quot;model&quot;&gt;stock.picking&lt;/field&gt;
&lt;field name=&quot;arch&quot; type=&quot;xml&quot;&gt;
&lt;field name=&quot;json_popover&quot; position=&quot;after&quot;&gt;
&lt;button name=&quot;action_download_attachments&quot;
type=&quot;object&quot;
icon=&quot;fa-download&quot;
string=&quot;Download attachment(s)&quot;
aria-label=&quot;Download Proof documents&quot;
class=&quot;float-right&quot;/&gt;
&lt;/field&gt;
&lt;/field&gt;
&lt;/record&gt;
&lt;!--
Add &quot;Download attachments&quot; item in the Action menu when
multiple records are selected
--&gt;
&lt;record id=&quot;action_download_picking_attachements&quot; model=&quot;ir.actions.server&quot;&gt;
&lt;field name=&quot;name&quot;&gt;Download attachments&lt;/field&gt;
&lt;field name=&quot;model_id&quot; ref=&quot;stock.model_stock_picking&quot;/&gt;
&lt;field name=&quot;binding_model_id&quot; ref=&quot;stock.model_stock_picking&quot;/&gt;
&lt;field name=&quot;binding_view_types&quot;&gt;list&lt;/field&gt;
&lt;field name=&quot;state&quot;&gt;code&lt;/field&gt;
&lt;field name=&quot;code&quot;&gt;
action = records.action_download_attachments()
&lt;/field&gt;
&lt;/record&gt;
&lt;/odoo&gt;
</pre>
<div class="admonition note">
<p class="first admonition-title">Note</p>
<p>Even you will be able to generate a zip file with multiple document
with the same name its advice to overwrite _compute_zip_file_name
to improve the name. When a slash (/) is present in the path it will
create a directory. This example will create a directory per
stock.picking using its name:</p>
<pre class="last literal-block">
class IrAttachment(models.Model):
_inherit = &quot;ir.attachment&quot;
def _compute_zip_file_name(self):
self.ensure_one()
if self.res_model and self.res_model == &quot;stock.picking&quot;:
return (
self.env[self.res_model]
.browse(self.res_id)
.display_name.replace(&quot;/&quot;, &quot;-&quot;)
+ &quot;/&quot;
+ self.name
)
return super()._compute_zip_file_name()
</pre>
</div>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/knowledge/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/knowledge/issues/new?body=module:%20attachment_zipped_download%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-3">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-4">Authors</a></h2>
<ul class="simple">
<li>Tecnativa</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-5">Contributors</a></h2>
<ul class="simple">
<li>César Fernández Domínguez &lt;<a class="reference external" href="mailto:cesfernandez&#64;outlook.com">cesfernandez&#64;outlook.com</a>&gt;</li>
<li><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:<ul>
<li>Víctor Martínez</li>
<li>Pedro M. Baeza</li>
</ul>
</li>
<li>Pierre Verkest &lt;<a class="reference external" href="mailto:pierreverkest&#64;gmail.com">pierreverkest&#64;gmail.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-6">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>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.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/knowledge/tree/18.0/attachment_zipped_download">OCA/knowledge</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,4 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from . import test_attachment_zipped_download
from . import test_ir_attachment_action_download

View File

@ -0,0 +1,9 @@
# Copyright 2023 Foodles (https://www.foodles.com/)
# @author Pierre Verkest <pierreverkest84@gmail.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import models
class ResPartner(models.Model):
_name = "res.partner"
_inherit = ["res.partner", "ir.attachment.action_download"]

View File

@ -0,0 +1,76 @@
# Copyright 2022-2024 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import base64
from odoo.exceptions import AccessError
from odoo.tests import HttpCase, new_test_user
from odoo.addons.base.tests.common import BaseCommon
class TestAttachmentZippedDownloadBase(BaseCommon):
@classmethod
def _create_attachment(cls, user, name, model=False, res_id=False):
return (
cls.env["ir.attachment"]
.with_user(user)
.create(
{
"name": name,
"datas": base64.b64encode(b"\xff data"),
"res_model": model,
"res_id": res_id,
}
)
)
class TestAttachmentZippedDownload(HttpCase, TestAttachmentZippedDownloadBase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user = new_test_user(cls.env, login="test-user")
test_1 = cls._create_attachment(cls.user, "test1.txt")
test_2 = cls._create_attachment(cls.user, "test2.txt")
cls.attachments = test_1 + test_2
def test_action_attachments_download(self):
self.authenticate("test-user", "test-user")
res = self.attachments.action_attachments_download()
response = self.url_open(res["url"], timeout=20)
self.assertEqual(response.status_code, 200)
class TestAttachmentZipped(TestAttachmentZippedDownloadBase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user = new_test_user(
cls.env,
login="test-user",
password="test-user",
groups="base.group_user,base.group_partner_manager",
)
test_1 = cls._create_attachment(cls.user, "test1.txt")
test_2 = cls._create_attachment(cls.user, "test2.txt")
test_3 = cls._create_attachment(
cls.user,
"test3.txt",
model="res.partner",
res_id=cls.user.partner_id.id,
)
cls.attachments = test_1 + test_2 + test_3
def test_create_temp_zip(self):
res = self.attachments._create_temp_zip()
self.assertTrue(res)
def test_create_temp_zip_access_denined(self):
attachments = self.attachments | self._create_attachment(
self.uid,
"test4.txt",
model="ir.ui.view",
res_id=self.env.ref("base.view_view_form").id,
)
with self.assertRaises(AccessError):
attachments._create_temp_zip()

View File

@ -0,0 +1,76 @@
# Copyright 2023 Foodles (https://www.foodles.com/)
# @author Pierre Verkest <pierreverkest84@gmail.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo_test_helper import FakeModelLoader
from .test_attachment_zipped_download import TestAttachmentZippedDownloadBase
class TestMixin(TestAttachmentZippedDownloadBase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.loader = FakeModelLoader(cls.env, cls.__module__)
cls.addClassCleanup(cls.loader.restore_registry)
cls.loader.backup_registry()
# Imported Test model must be done after the backup_registry
from .models.res_partner import ResPartner
cls.loader.update_registry((ResPartner,))
cls.partner_1 = cls.env["res.partner"].create({"name": "Test partner 1"})
cls.partner_2 = cls.env["res.partner"].create({"name": "Test partner 2"})
cls.partner_3 = cls.env["res.partner"].create({"name": "Test partner 3"})
cls.partner_1_f1 = cls._create_attachment(
cls.env.uid,
"partner_1-f1.txt",
model="res.partner",
res_id=cls.partner_1.id,
)
cls.partner_1_f2 = cls._create_attachment(
cls.env.uid,
"partner_1-f2.txt",
model="res.partner",
res_id=cls.partner_1.id,
)
cls.partner_2_f1 = cls._create_attachment(
cls.env.uid,
"partner_2-f1.txt",
model="res.partner",
res_id=cls.partner_2.id,
)
def test_action_download_attachments_no_attachment(self):
action = self.partner_3.action_download_attachments()
self.assertEqual(action["type"], "ir.actions.client")
self.assertEqual(action["tag"], "display_notification")
def test_action_download_attachments_one_attachment(self):
action = (self.partner_2 | self.partner_3).action_download_attachments()
self.assertEqual(action["type"], "ir.actions.act_url")
self.assertEqual(action["target"], "self")
self.assertEqual(
action["url"], f"/web/content/{self.partner_2_f1.id}?download=1"
)
def test_action_download_attachments_two_attachment_one_record(self):
action = (self.partner_1).action_download_attachments()
self.assertEqual(action["type"], "ir.actions.act_url")
self.assertEqual(action["target"], "self")
self.assertTrue(action["url"].startswith("/web/attachment/download_zip?ids="))
ids = sorted(map(int, action["url"].split("=")[1].split(",")))
self.assertEqual(ids, (self.partner_1_f1 | self.partner_1_f2).ids)
def test_action_download_attachments_three_attachment_n_records(self):
action = (
self.partner_1 | self.partner_2 | self.partner_3
).action_download_attachments()
self.assertEqual(action["type"], "ir.actions.act_url")
self.assertEqual(action["target"], "self")
self.assertTrue(action["url"].startswith("/web/attachment/download_zip?ids="))
ids = sorted(map(int, action["url"].split("=")[1].split(",")))
self.assertEqual(
ids, (self.partner_1_f1 + self.partner_1_f2 + self.partner_2_f1).ids
)

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="action_attachments_download" model="ir.actions.server">
<field name="name">Download</field>
<field name="model_id" ref="base.model_ir_attachment" />
<field name="binding_model_id" ref="base.model_ir_attachment" />
<field name="state">code</field>
<field name="code">
if records:
action = records.action_attachments_download()
</field>
</record>
</odoo>

1
test-requirements.txt Normal file
View File

@ -0,0 +1 @@
odoo_test_helper