From acca2f71ad8386352396c46b24a759be8cefe8ed Mon Sep 17 00:00:00 2001 From: Julien Van Roy Date: Mon, 16 Oct 2023 17:04:50 +0200 Subject: [PATCH] [FIX] account,portal: fix 'download' button in account portal The 'download' button should download *all* the attachments linked to the move. Depending on the localization, an xml might be needed in addition to the PDF (e.g. the CFDI and the signed PDF in Mexico). To do that, we open a headless Send & Print wizard with only the `download` checkbox set to True. Secondly, do not show the 'Download' button in the portal view if no official attachment is linked to the move (the print button still appears). task-3542881 Part-of: odoo/odoo#138206 --- addons/account/controllers/download_docs.py | 27 ++++++++----------- addons/account/controllers/portal.py | 19 +++++++++++-- addons/account/models/account_move.py | 9 ++++--- addons/account/models/ir_attachment.py | 9 +++++++ addons/account/wizard/account_move_send.py | 5 +++- addons/portal/controllers/portal.py | 16 ++++++----- .../snailmail_account/models/account_move.py | 4 +-- 7 files changed, 58 insertions(+), 31 deletions(-) diff --git a/addons/account/controllers/download_docs.py b/addons/account/controllers/download_docs.py index 67038111377..749ea8227e0 100644 --- a/addons/account/controllers/download_docs.py +++ b/addons/account/controllers/download_docs.py @@ -1,12 +1,19 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import io -import zipfile from odoo import http, _ from odoo.http import request, content_disposition +def _get_zip_headers(content, filename): + return [ + ('Content-Type', 'zip'), + ('X-Content-Type-Options', 'nosniff'), + ('Content-Length', len(content)), + ('Content-Disposition', content_disposition(filename)), + ] + + class AccountDocumentDownloadController(http.Controller): @http.route('/account/export_zip_documents', type='http', auth='user') def export_zip_documents(self, **args): @@ -16,18 +23,6 @@ class AccountDocumentDownloadController(http.Controller): attachments = request.env['ir.attachment'].browse(ids) attachments.check_access_rights('read') attachments.check_access_rule('read') - - # Create zip file - buffer = io.BytesIO() - with zipfile.ZipFile(buffer, 'w', compression=zipfile.ZIP_DEFLATED) as zipfile_obj: - for attachment in attachments: - zipfile_obj.writestr(attachment.display_name, attachment.raw) - content = buffer.getvalue() - - headers = [ - ('Content-Type', 'zip'), - ('X-Content-Type-Options', 'nosniff'), - ('Content-Length', len(content)), - ('Content-Disposition', content_disposition(filename)), - ] + content = attachments._build_zip_from_attachments() + headers = _get_zip_headers(content, filename) return request.make_response(content, headers) diff --git a/addons/account/controllers/portal.py b/addons/account/controllers/portal.py index 0bc04fb9076..9ef44cad8dc 100644 --- a/addons/account/controllers/portal.py +++ b/addons/account/controllers/portal.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from collections import OrderedDict + from odoo import http, _ from odoo.osv import expression from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager +from odoo.addons.account.controllers.download_docs import _get_zip_headers from odoo.exceptions import AccessError, MissingError -from collections import OrderedDict from odoo.http import request @@ -121,7 +123,20 @@ class PortalAccount(CustomerPortal): except (AccessError, MissingError): return request.redirect('/my') - if report_type in ('html', 'pdf', 'text'): + if report_type == 'pdf' and download: + # Send & Print wizard with only the 'download' checkbox to get the official attachment(s) + template = request.env.ref(invoice_sudo._get_mail_template()) + attachment_ids = invoice_sudo._generate_pdf_and_send_invoice(template, checkbox_send_mail=False, checkbox_download=True) + attachments = request.env['ir.attachment'].browse(attachment_ids) + if len(attachments) > 1: + filename = invoice_sudo._get_invoice_report_filename(extension='zip') + zip_content = attachments.sudo()._build_zip_from_attachments() + headers = _get_zip_headers(zip_content, filename) + return request.make_response(zip_content, headers) + headers = self._get_http_headers(invoice_sudo, report_type, attachments.raw, download) + return request.make_response(attachments.raw, list(headers.items())) + + elif report_type in ('html', 'pdf', 'text'): return self._show_report(model=invoice_sudo, report_type=report_type, report_ref='account.account_invoices', download=download) values = self._invoice_get_page_view_values(invoice_sudo, access_token, **kw) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 9f5d9e3564b..d0ac5700c06 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -4294,15 +4294,16 @@ class AccountMove(models.Model): return rslt - def _get_pdf_and_send_invoice_vals(self, template): + def _get_pdf_and_send_invoice_vals(self, template, **kwargs): return { 'mail_template_id': template.id, 'move_ids': self.ids, 'checkbox_send_mail': True, 'checkbox_download': False, + **kwargs, } - def _generate_pdf_and_send_invoice(self, template, from_cron=True, allow_fallback_pdf=True): + def _generate_pdf_and_send_invoice(self, template, from_cron=True, allow_fallback_pdf=True, **kwargs): """ Generate the pdf for the current invoices and send them by mail using the send & print wizard. :param from_cron: Flag indicating if the method is called from a cron. In that case, we avoid raising any @@ -4310,11 +4311,11 @@ class AccountMove(models.Model): :param allow_fallback_pdf: In case of error when generating the documents for invoices, generate a proforma PDF report instead. """ - composer_vals = self._get_pdf_and_send_invoice_vals(template) + composer_vals = self._get_pdf_and_send_invoice_vals(template, **kwargs) composer = self.env['account.move.send'].create(composer_vals) # from_cron=True to log errors in chatter instead of raise - composer.action_send_and_print(from_cron=from_cron, allow_fallback_pdf=allow_fallback_pdf) + return composer.action_send_and_print(from_cron=from_cron, allow_fallback_pdf=allow_fallback_pdf) def get_invoice_pdf_report_attachment(self): if len(self) < 2 and self.invoice_pdf_report_id: diff --git a/addons/account/models/ir_attachment.py b/addons/account/models/ir_attachment.py index b0701fce0ff..201deb20131 100644 --- a/addons/account/models/ir_attachment.py +++ b/addons/account/models/ir_attachment.py @@ -10,6 +10,7 @@ except ImportError: from PyPDF2.utils import PdfReadError import io import logging +import zipfile _logger = logging.getLogger(__name__) @@ -17,6 +18,14 @@ _logger = logging.getLogger(__name__) class IrAttachment(models.Model): _inherit = 'ir.attachment' + def _build_zip_from_attachments(self): + """ Return the zip bytes content resulting from compressing the attachments in `self`""" + buffer = io.BytesIO() + with zipfile.ZipFile(buffer, 'w', compression=zipfile.ZIP_DEFLATED) as zipfile_obj: + for attachment in self: + zipfile_obj.writestr(attachment.display_name, attachment.raw) + return buffer.getvalue() + # ------------------------------------------------------------------------- # EDI # ------------------------------------------------------------------------- diff --git a/addons/account/wizard/account_move_send.py b/addons/account/wizard/account_move_send.py index facf14c1d04..4aca179eedd 100644 --- a/addons/account/wizard/account_move_send.py +++ b/addons/account/wizard/account_move_send.py @@ -646,7 +646,10 @@ class AccountMoveSend(models.Model): # _get_invoice_extra_attachments retrieves invoice PDF and other possible xml, etc. attachment_ids += self._get_invoice_extra_attachments(move).ids or moves_data.get(move).get('proforma_pdf_attachment').ids if attachment_ids: - return self._download(attachment_ids) + if from_cron: + return attachment_ids + else: + return self._download(attachment_ids) return {'type': 'ir.actions.act_window_close'} diff --git a/addons/portal/controllers/portal.py b/addons/portal/controllers/portal.py index 77254a0ccbe..b4a147bacb4 100644 --- a/addons/portal/controllers/portal.py +++ b/addons/portal/controllers/portal.py @@ -489,14 +489,18 @@ class CustomerPortal(Controller): method_name = '_render_qweb_%s' % (report_type) report = getattr(ReportAction, method_name)(report_ref, list(model.ids), data={'report_type': report_type})[0] - reporthttpheaders = [ - ('Content-Type', 'application/pdf' if report_type == 'pdf' else 'text/html'), - ('Content-Length', len(report)), - ] + headers = self._get_http_headers(model, report_type, report, download) + return request.make_response(report, headers=list(headers.items())) + + def _get_http_headers(self, model, report_type, report, download): + headers = { + 'Content-Type': 'application/pdf' if report_type == 'pdf' else 'text/html', + 'Content-Length': len(report), + } if report_type == 'pdf' and download: filename = "%s.pdf" % (re.sub('\W+', '-', model._get_report_base_filename())) - reporthttpheaders.append(('Content-Disposition', content_disposition(filename))) - return request.make_response(report, headers=reporthttpheaders) + headers['Content-Disposition'] = content_disposition(filename) + return headers def get_error(e, path=''): """ Recursively dereferences `path` (a period-separated sequence of dict diff --git a/addons/snailmail_account/models/account_move.py b/addons/snailmail_account/models/account_move.py index 5e623fade6d..5f3fe326d94 100644 --- a/addons/snailmail_account/models/account_move.py +++ b/addons/snailmail_account/models/account_move.py @@ -7,8 +7,8 @@ from odoo import models class AccountMove(models.Model): _inherit = "account.move" - def _get_pdf_and_send_invoice_vals(self, template): + def _get_pdf_and_send_invoice_vals(self, template, **kwargs): # EXTENDS account - vals = super()._get_pdf_and_send_invoice_vals(template) + vals = super()._get_pdf_and_send_invoice_vals(template, **kwargs) vals['checkbox_send_by_post'] = False return vals