From 7e53bd296f31103c3f23610d098639d5597f0a39 Mon Sep 17 00:00:00 2001 From: "Abdelouahab (abla)" Date: Wed, 29 Mar 2023 10:35:54 +0000 Subject: [PATCH] [FIX] account: origianl bills with encrypted PDF To reproduce ============ - Accounting > Vendor Bills > create a vendor bill and upload the pdf (attached on this ticket) - back to list view, select the created bill and print the original bills a user error is raised Problem ======= when trying to add the banner we read the PDF and we found out that it's encrypted so we raise the error Solution ======== if the PDF is encrypted, we log warning and carry on downloading the original bill. opw-3221796 closes odoo/odoo#118134 X-original-commit: 900418d9bc7bd260cac67769be77f8e818eed814 Signed-off-by: John Laterre (jol) Signed-off-by: abla001 --- addons/account/models/ir_actions_report.py | 6 +- addons/account/tests/__init__.py | 1 + .../account/tests/test_ir_actions_report.py | 98 +++++++++++++++++++ 3 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 addons/account/tests/test_ir_actions_report.py diff --git a/addons/account/models/ir_actions_report.py b/addons/account/models/ir_actions_report.py index edd0ec1812c..50312a1bf61 100644 --- a/addons/account/models/ir_actions_report.py +++ b/addons/account/models/ir_actions_report.py @@ -2,9 +2,9 @@ from collections import OrderedDict try: - from PyPDF2.errors import PdfStreamError + from PyPDF2.errors import PdfStreamError, PdfReadError except ImportError: - from PyPDF2.utils import PdfStreamError + from PyPDF2.utils import PdfStreamError, PdfReadError from odoo import models, _ from odoo.exceptions import UserError @@ -36,7 +36,7 @@ class IrActionsReport(models.Model): record = self.env[attachment.res_model].browse(attachment.res_id) try: stream = pdf.add_banner(stream, record.name, logo=True) - except (ValueError, PdfStreamError): + except (ValueError, PdfStreamError, PdfReadError, TypeError): record._message_log(body=_( "There was an error when trying to add the banner to the original PDF.\n" "Please make sure the source file is valid." diff --git a/addons/account/tests/__init__.py b/addons/account/tests/__init__.py index 7882338da67..b81052eff79 100644 --- a/addons/account/tests/__init__.py +++ b/addons/account/tests/__init__.py @@ -38,3 +38,4 @@ from . import test_payment_term from . import test_account_payment_register from . import test_tour from . import test_early_payment_discount +from . import test_ir_actions_report diff --git a/addons/account/tests/test_ir_actions_report.py b/addons/account/tests/test_ir_actions_report.py new file mode 100644 index 00000000000..bd9c8bae32f --- /dev/null +++ b/addons/account/tests/test_ir_actions_report.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +import base64 +import io + +from odoo.addons.account.tests.common import AccountTestInvoicingCommon +from odoo.exceptions import UserError +from odoo.tools import pdf +from odoo.tests import tagged +from odoo.tools import file_open + +from PyPDF2 import PdfFileReader, PdfFileWriter + +@tagged('post_install', '-at_install') +class TestIrActionsReport(AccountTestInvoicingCommon): + + def setUp(self): + super().setUp() + self.file = file_open('base/tests/minimal.pdf', 'rb').read() + self.minimal_reader_buffer = io.BytesIO(self.file) + self.minimal_pdf_reader = pdf.OdooPdfFileReader(self.minimal_reader_buffer) + + def test_download_one_corrupted_pdf(self): + """ + PyPDF2 is not flawless. We can upload a PDF that can be previsualised but that cannot be merged by PyPDF2. + In the case of "Print Original Bills", we want to be able to download the pdf from the list view. + We test that, when selecting one record, it can be printed (downloaded) without error. + """ + attach_name = 'original_vendor_bill.pdf' + + in_invoice_1 = self.env['account.move'].create({ + 'move_type': 'in_invoice', + 'date': '2017-01-01', + 'invoice_date': '2017-01-01' + }) + + in_invoice_1.message_main_attachment_id = self.env['ir.attachment'].create({ + 'datas': base64.b64encode(self.file), + 'name': attach_name, + 'mimetype': 'application/pdf', + 'res_model': 'account.move', + 'res_id': in_invoice_1.id, + }) + test_record_report = self.env['ir.actions.report'].with_context(force_report_rendering=True)._render_qweb_pdf('account.action_account_original_vendor_bill', res_ids=in_invoice_1.id) + self.assertTrue(test_record_report, "The PDF should have been generated") + + def test_download_one_encrypted_pdf(self): + """ + Same as test_download_one_corrupted_pdf + but for encrypted pdf with no password and set encryption type to 5 (not known by PyPDF2) + """ + attach_name = 'original_vendor_bill.pdf' + # we need to encrypt the file + with file_open('base/tests/minimal.pdf', 'rb') as pdf_file: + pdf_reader = PdfFileReader(pdf_file) + pdf_writer = PdfFileWriter() + for page_num in range(pdf_reader.getNumPages()): + pdf_writer.addPage(pdf_reader.getPage(page_num)) + # Encrypt the PDF + pdf_writer.encrypt('', use_128bit=True) + # Get the binary + output_buffer = io.BytesIO() + pdf_writer.write(output_buffer) + encrypted_file = output_buffer.getvalue() + + # we need to change the encryption value from 4 to 5 to simulate an encryption not used by PyPDF2 + encrypt_start = encrypted_file.find(b'/Encrypt') + encrypt_end = encrypted_file.find(b'>>', encrypt_start) + encrypt_version = encrypted_file[encrypt_start : encrypt_end] + encrypted_file = encrypted_file.replace(encrypt_version, encrypt_version.replace(b'4', b'5')) + + in_invoice_1 = self.env['account.move'].create({ + 'move_type': 'in_invoice', + 'date': '2017-01-01', + 'invoice_date': '2017-01-01' + }) + + in_invoice_1.message_main_attachment_id = self.env['ir.attachment'].create({ + 'datas': base64.b64encode(encrypted_file), + 'name': attach_name, + 'mimetype': 'application/pdf', + 'res_model': 'account.move', + 'res_id': in_invoice_1.id, + }) + test_record_report = self.env['ir.actions.report'].with_context(force_report_rendering=True)._render_qweb_pdf('account.action_account_original_vendor_bill', res_ids=in_invoice_1.id) + self.assertTrue(test_record_report, "The PDF should have been generated") + + in_invoice_2 = in_invoice_1.copy() + + in_invoice_2.message_main_attachment_id = self.env['ir.attachment'].create({ + 'datas': base64.b64encode(self.file), + 'name': attach_name, + 'mimetype': 'application/pdf', + 'res_model': 'account.move', + 'res_id': in_invoice_2.id, + }) + # trying to merge with a corrupted attachment should not work + with self.assertRaises(UserError): + self.env['ir.actions.report'].with_context(force_report_rendering=True)._render_qweb_pdf('account.action_account_original_vendor_bill', res_ids=[in_invoice_1.id, in_invoice_2.id])