From 31702e7006ce975cf08f3296c89402dd52ef01de Mon Sep 17 00:00:00 2001 From: Ernad Husremovic Date: Tue, 2 Sep 2025 19:31:34 +0200 Subject: [PATCH] fix PyPDF2 3.x page copying + rename documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added explicit page copying after cloneReaderDocumentRoot() calls - Renamed PATCH_PDFWRITER.md to PATCH_PYPDF2_PDFWRITER.md - Prevents 327-byte empty PDFs in PyPDF2 3.x 🤖 assisted by claude --- .../account_edi/models/ir_actions_report.py | 4 ++ .../models/ir_actions_report.py.backup | 47 +++++++++++++++++++ ...PDFWRITER.md => PATCH_PYPDF2_PDFWRITER.md} | 26 ++++++---- 3 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 odoo-bringout-oca-ocb-account_edi/account_edi/models/ir_actions_report.py.backup rename odoo-bringout-oca-ocb-account_edi/doc/{PATCH_PDFWRITER.md => PATCH_PYPDF2_PDFWRITER.md} (71%) diff --git a/odoo-bringout-oca-ocb-account_edi/account_edi/models/ir_actions_report.py b/odoo-bringout-oca-ocb-account_edi/account_edi/models/ir_actions_report.py index 2831321..5880fa6 100644 --- a/odoo-bringout-oca-ocb-account_edi/account_edi/models/ir_actions_report.py +++ b/odoo-bringout-oca-ocb-account_edi/account_edi/models/ir_actions_report.py @@ -32,6 +32,10 @@ class IrActionsReport(models.Model): # Post-process and embed the additional files. writer = OdooPdfFileWriter() writer.cloneReaderDocumentRoot(reader) + # Copy all pages from the reader to the writer (required for PyPDF2 3.x) + for page_num in range(reader.getNumPages()): + page = reader.getPage(page_num) + writer.addPage(page) for edi_document in to_embed: # The attachements on the edi documents are only system readable # because they don't have res_id and res_model, here we are sure that diff --git a/odoo-bringout-oca-ocb-account_edi/account_edi/models/ir_actions_report.py.backup b/odoo-bringout-oca-ocb-account_edi/account_edi/models/ir_actions_report.py.backup new file mode 100644 index 0000000..2831321 --- /dev/null +++ b/odoo-bringout-oca-ocb-account_edi/account_edi/models/ir_actions_report.py.backup @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +import io + +from odoo import models +from odoo.tools.pdf import OdooPdfFileReader, OdooPdfFileWriter + + +class IrActionsReport(models.Model): + _inherit = 'ir.actions.report' + + def _render_qweb_pdf_prepare_streams(self, report_ref, data, res_ids=None): + # EXTENDS base + collected_streams = super()._render_qweb_pdf_prepare_streams(report_ref, data, res_ids=res_ids) + + if collected_streams \ + and res_ids \ + and len(res_ids) == 1 \ + and self._is_invoice_report(report_ref): + invoice = self.env['account.move'].browse(res_ids) + if invoice.is_sale_document() and invoice.state != 'draft': + to_embed = invoice.edi_document_ids + # Add the attachments to the pdf file + if to_embed: + pdf_stream = collected_streams[invoice.id]['stream'] + + # Read pdf content. + pdf_content = pdf_stream.getvalue() + reader_buffer = io.BytesIO(pdf_content) + reader = OdooPdfFileReader(reader_buffer, strict=False) + + # Post-process and embed the additional files. + writer = OdooPdfFileWriter() + writer.cloneReaderDocumentRoot(reader) + for edi_document in to_embed: + # The attachements on the edi documents are only system readable + # because they don't have res_id and res_model, here we are sure that + # the user has access to the invoice and edi document + edi_document.edi_format_id._prepare_invoice_report(writer, edi_document) + + # Replace the current content. + pdf_stream.close() + new_pdf_stream = io.BytesIO() + writer.write(new_pdf_stream) + collected_streams[invoice.id]['stream'] = new_pdf_stream + + return collected_streams diff --git a/odoo-bringout-oca-ocb-account_edi/doc/PATCH_PDFWRITER.md b/odoo-bringout-oca-ocb-account_edi/doc/PATCH_PYPDF2_PDFWRITER.md similarity index 71% rename from odoo-bringout-oca-ocb-account_edi/doc/PATCH_PDFWRITER.md rename to odoo-bringout-oca-ocb-account_edi/doc/PATCH_PYPDF2_PDFWRITER.md index b2027cd..6be81d0 100644 --- a/odoo-bringout-oca-ocb-account_edi/doc/PATCH_PDFWRITER.md +++ b/odoo-bringout-oca-ocb-account_edi/doc/PATCH_PYPDF2_PDFWRITER.md @@ -22,26 +22,34 @@ The account_edi module uses PyPDF2 for: ## Solution -**This package requires NO direct patches** because it uses: -1. `OdooPdfFileWriter` from `odoo.tools.pdf` (oca-ocb-base) -2. `OdooPdfFileReader` from `odoo.tools.pdf` (oca-ocb-base) +**This package includes direct fixes** for PyPDF2 3.x page copying issue: +1. Uses `OdooPdfFileWriter` from `odoo.tools.pdf` (oca-ocb-base) for compatibility +2. **CRITICAL FIX**: Added explicit page copying after `cloneReaderDocumentRoot()` -The main compatibility layer in `oca-ocb-base` handles all PyPDF2 version compatibility automatically. +In PyPDF2 3.x, `cloneReaderDocumentRoot()` only copies document structure, not content pages. This was causing 327-byte empty PDFs. The fix includes explicit page copying: + +```python +writer.cloneReaderDocumentRoot(reader) +# Copy all pages from the reader to the writer (required for PyPDF2 3.x) +for page_num in range(reader.getNumPages()): + page = reader.getPage(page_num) + writer.addPage(page) +``` ## Files Using PyPDF2 ### `account_edi/models/ir_actions_report.py` - Uses `OdooPdfFileWriter` for PDF generation (automatically compatible) -- Calls `writer.cloneReaderDocumentRoot(reader)` +- **FIXED**: Added explicit page copying after `writer.cloneReaderDocumentRoot(reader)` - Embeds EDI XML attachments in PDFs ## Implementation Details -**No code changes needed** in this package. Compatibility is achieved through: +**Direct code changes applied** in this package: -1. **Dependency**: Requires `oca-ocb-base` with `pdfwrite` branch -2. **Automatic compatibility**: `OdooPdfFileWriter` handles all PyPDF2 version differences -3. **Method compatibility**: Methods like `cloneReaderDocumentRoot()` are automatically wrapped +1. **Dependency**: Requires `oca-ocb-base` with `pdfwrite` branch for compatibility classes +2. **Page copying fix**: Added explicit page copying loop in `_render_qweb_pdf_prepare_streams()` +3. **Method compatibility**: Methods like `cloneReaderDocumentRoot()` are automatically wrapped by base classes ## Testing