diff --git a/addons/account/models/ir_actions_report.py b/addons/account/models/ir_actions_report.py
index cd9ea71b45f..fa61961eed4 100644
--- a/addons/account/models/ir_actions_report.py
+++ b/addons/account/models/ir_actions_report.py
@@ -1,58 +1,42 @@
# -*- coding: utf-8 -*-
+import io
+
+from collections import OrderedDict
from odoo import models, _
from odoo.exceptions import UserError
-from odoo.tools import pdf
class IrActionsReport(models.Model):
_inherit = 'ir.actions.report'
- def retrieve_attachment(self, record):
- # get the original bills through the message_main_attachment_id field of the record
- if self.report_name == 'account.report_original_vendor_bill' and record.message_main_attachment_id:
- if record.message_main_attachment_id.mimetype == 'application/pdf' or \
- record.message_main_attachment_id.mimetype.startswith('image'):
- return record.message_main_attachment_id
- return super(IrActionsReport, self).retrieve_attachment(record)
+ def _render_qweb_pdf_prepare_streams(self, data, res_ids=None):
+ # Custom behavior for 'account.report_original_vendor_bill'.
+ if self.report_name != 'account.report_original_vendor_bill':
+ return super()._render_qweb_pdf_prepare_streams(data, res_ids=res_ids)
- def _post_pdf(self, save_in_attachment, pdf_content=None, res_ids=None):
- # don't include the generated dummy report
- if self.report_name == 'account.report_original_vendor_bill':
- pdf_content = None
- res_ids = None
- if not save_in_attachment:
- raise UserError(_("No original vendor bills could be found for any of the selected vendor bills."))
- return super(IrActionsReport, self)._post_pdf(save_in_attachment, pdf_content=pdf_content, res_ids=res_ids)
+ invoices = self.env['account.move'].browse(res_ids)
+ if any(x.move_type not in ('in_invoice', 'in_receipt') for x in invoices):
+ raise UserError(_("You can only print the original document for vendor bills."))
- def _postprocess_pdf_report(self, record, buffer):
- # don't save the 'account.report_original_vendor_bill' report as it's just a mean to print existing attachments
- if self.report_name == 'account.report_original_vendor_bill':
- return None
- res = super(IrActionsReport, self)._postprocess_pdf_report(record, buffer)
- if self.model == 'account.move' and record.state == 'posted' and record.is_sale_document(include_receipts=True):
- attachment = self.retrieve_attachment(record)
- if attachment:
- attachment.register_as_main_attachment(force=False)
- return res
+ original_attachments = invoices.message_main_attachment_id
+ if not original_attachments:
+ raise UserError(_("No original vendor bills could be found for any of the selected vendor bills."))
+
+ collected_streams = OrderedDict()
+ for invoice in invoices:
+ if invoice.message_main_attachment_id:
+ collected_streams[invoice.id] = {
+ 'stream': io.BytesIO(invoice.message_main_attachment_id.raw),
+ 'attachment': invoice.message_main_attachment_id,
+ }
+ return collected_streams
def _render_qweb_pdf(self, res_ids=None, data=None):
- # Overridden so that the print > invoices actions raises an error
- # when trying to print a miscellaneous operation instead of an invoice.
- if self.model == 'account.move' and res_ids:
- invoice_reports = (self.env.ref('account.account_invoices_without_payment'), self.env.ref('account.account_invoices'))
- if self in invoice_reports:
- moves = self.env['account.move'].browse(res_ids)
- if any(not move.is_invoice(include_receipts=True) for move in moves):
- raise UserError(_("Only invoices could be printed."))
+ # Check for reports only available for invoices.
+ if self.report_name in ('account.report_invoice_with_payments', 'account.report_invoice'):
+ invoices = self.env['account.move'].browse(res_ids)
+ if any(x.move_type == 'entry' for x in invoices):
+ raise UserError(_("Only invoices could be printed."))
return super()._render_qweb_pdf(res_ids=res_ids, data=data)
-
- def _retrieve_stream_from_attachment(self, attachment):
- # Overridden in order to add a banner in the upper right corner of the exported Vendor Bill PDF.
- stream = super()._retrieve_stream_from_attachment(attachment)
- vendor_bill_export = self.env.ref('account.action_account_original_vendor_bill')
- if self == vendor_bill_export and attachment.mimetype == 'application/pdf':
- record = self.env[attachment.res_model].browse(attachment.res_id)
- return pdf.add_banner(stream, record.name, logo=True)
- return stream
diff --git a/addons/account_edi/models/ir_actions_report.py b/addons/account_edi/models/ir_actions_report.py
index fd0c767974f..611849d34e9 100644
--- a/addons/account_edi/models/ir_actions_report.py
+++ b/addons/account_edi/models/ir_actions_report.py
@@ -9,24 +9,36 @@ from odoo.tools.pdf import OdooPdfFileReader, OdooPdfFileWriter
class IrActionsReport(models.Model):
_inherit = 'ir.actions.report'
- def _post_pdf(self, save_in_attachment, pdf_content=None, res_ids=None):
- # OVERRIDE to embed some EDI documents inside the PDF.
- if self.model == 'account.move' and res_ids and len(res_ids) == 1 and pdf_content:
+ def _render_qweb_pdf_prepare_streams(self, data, res_ids=None):
+ # EXTENDS base
+ collected_streams = super()._render_qweb_pdf_prepare_streams(data, res_ids=res_ids)
+
+ if collected_streams \
+ and res_ids \
+ and len(res_ids) == 1 \
+ and self.report_name in ('account.report_invoice_with_payments', 'account.report_invoice'):
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:
edi_document.edi_format_id._prepare_invoice_report(writer, edi_document)
- buffer = io.BytesIO()
- writer.write(buffer)
- pdf_content = buffer.getvalue()
- reader_buffer.close()
- buffer.close()
- return super(IrActionsReport, self)._post_pdf(save_in_attachment, pdf_content=pdf_content, res_ids=res_ids)
+ # 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/addons/l10n_be_edi/models/ir_actions_report.py b/addons/l10n_be_edi/models/ir_actions_report.py
index da85872ef5c..057e7bd5c39 100644
--- a/addons/l10n_be_edi/models/ir_actions_report.py
+++ b/addons/l10n_be_edi/models/ir_actions_report.py
@@ -11,43 +11,56 @@ from xml.sax.saxutils import escape, quoteattr
class IrActionsReport(models.Model):
_inherit = 'ir.actions.report'
- def _postprocess_pdf_report(self, record, buffer):
- '''Add the pdf report in the e-fff XML as base64 string.
- '''
- result = super()._postprocess_pdf_report(record, buffer)
+ def _l10n_be_edi_add_pdf_into_invoice_xml(self, invoice, stream_data):
+ edi_attachment = invoice.edi_document_ids.filtered(lambda d: d.edi_format_id.code == 'efff_1').attachment_id
+ if not edi_attachment:
+ return
- if record._name == 'account.move':
- edi_attachment = record.edi_document_ids.filtered(lambda d: d.edi_format_id.code == 'efff_1').attachment_id
- if edi_attachment:
- old_xml = base64.b64decode(edi_attachment.with_context(bin_size=False).datas, validate=True)
- tree = etree.fromstring(old_xml)
- anchor_elements = tree.xpath("//*[local-name()='AccountingSupplierParty']")
- additional_document_elements = tree.xpath("//*[local-name()='AdditionalDocumentReference']")
- if anchor_elements and not additional_document_elements:
- pdf = base64.b64encode(buffer.getvalue()).decode()
- pdf_name = '%s.pdf' % record._get_efff_name()
- to_inject = '''
-
- %s
-
-
- %s
-
-
-
- ''' % (escape(pdf_name), quoteattr(pdf_name), pdf)
+ tree = etree.fromstring(edi_attachment.raw)
+ anchor_elements = tree.xpath("//*[local-name()='AccountingSupplierParty']")
+ additional_document_elements = tree.xpath("//*[local-name()='AdditionalDocumentReference']")
+ if anchor_elements and not additional_document_elements:
+ pdf_stream = stream_data['stream']
- anchor_index = tree.index(anchor_elements[0])
- tree.insert(anchor_index, etree.fromstring(to_inject))
- new_xml = etree.tostring(tree, pretty_print=True)
- edi_attachment.write({
- 'res_model': 'account.move',
- 'res_id': record.id,
- 'datas': base64.b64encode(new_xml),
- 'mimetype': 'application/xml',
- })
+ # Read pdf content.
+ pdf_content_b64 = base64.b64encode(pdf_stream.getvalue()).decode()
+ pdf_name = f'{invoice._get_efff_name()}.pdf'
- return result
+ # Inject the newly generated PDF.
+ to_inject = '''
+
+ %s
+
+
+ %s
+
+
+
+ ''' % (escape(pdf_name), quoteattr(pdf_name), pdf_content_b64)
+
+ anchor_index = tree.index(anchor_elements[0])
+ tree.insert(anchor_index, etree.fromstring(to_inject))
+ new_xml = etree.tostring(tree, pretty_print=True)
+ edi_attachment.write({
+ 'res_model': 'account.move',
+ 'res_id': invoice.id,
+ 'raw': new_xml,
+ 'mimetype': 'application/xml',
+ })
+
+ def _render_qweb_pdf_prepare_streams(self, data, res_ids=None):
+ # EXTENDS base
+ # Add the pdf report in the e-fff XML as base64 string.
+ collected_streams = super()._render_qweb_pdf_prepare_streams(data, res_ids=res_ids)
+
+ if collected_streams \
+ and res_ids \
+ and self.report_name in ('account.report_invoice_with_payments', 'account.report_invoice'):
+ for res_id, stream_data in collected_streams.items():
+ invoice = self.env['account.move'].browse(res_id)
+ self._l10n_be_edi_add_pdf_into_invoice_xml(invoice, stream_data)
+
+ return collected_streams
diff --git a/addons/l10n_be_edi/tests/test_efff_export.py b/addons/l10n_be_edi/tests/test_efff_export.py
index a4a28f267c7..dd5e79f6e7d 100644
--- a/addons/l10n_be_edi/tests/test_efff_export.py
+++ b/addons/l10n_be_edi/tests/test_efff_export.py
@@ -59,7 +59,8 @@ class TestUBLBE(AccountEdiTestCommon):
pdf_writer = PdfFileWriter()
pdf_writer.addBlankPage(42, 42)
pdf_writer.write(pdf_buffer)
- self.env.ref('account.account_invoices_without_payment')._postprocess_pdf_report(invoice, pdf_buffer)
+ stream_data = {'stream': pdf_buffer}
+ self.env.ref('account.account_invoices_without_payment')._l10n_be_edi_add_pdf_into_invoice_xml(invoice, stream_data)
pdf_buffer.close()
attachment = invoice._get_edi_attachment(self.edi_format)
diff --git a/odoo/addons/base/models/ir_actions_report.py b/odoo/addons/base/models/ir_actions_report.py
index d3be54967db..8a954087d4c 100644
--- a/odoo/addons/base/models/ir_actions_report.py
+++ b/odoo/addons/base/models/ir_actions_report.py
@@ -7,11 +7,9 @@ from odoo.exceptions import UserError, AccessError
from odoo.tools.safe_eval import safe_eval, time
from odoo.tools.misc import find_in_path
from odoo.tools import config, is_html_empty
-from odoo.sql_db import TestCursor
from odoo.http import request
from odoo.osv.expression import NEGATIVE_TERM_OPERATORS, FALSE_DOMAIN
-import base64
import io
import logging
import os
@@ -188,16 +186,6 @@ class IrActionsReport(models.Model):
#--------------------------------------------------------------------------
# Main report methods
#--------------------------------------------------------------------------
- def _retrieve_stream_from_attachment(self, attachment):
- #This import is needed to make sure a PDF stream can be saved in Image
- from PIL import PdfImagePlugin
- if attachment.mimetype.startswith('image'):
- stream = io.BytesIO(base64.b64decode(attachment.datas))
- img = Image.open(stream)
- output_stream = io.BytesIO()
- img.convert("RGB").save(output_stream, format="pdf")
- return output_stream
- return io.BytesIO(base64.decodebytes(attachment.datas))
def retrieve_attachment(self, record):
'''Retrieve an attachment for a specific record.
@@ -214,34 +202,6 @@ class IrActionsReport(models.Model):
('res_id', '=', record.id)
], limit=1)
- def _postprocess_pdf_report(self, record, buffer):
- '''Hook to handle post-processing during the pdf report generation.
-
- The basic behavior consists of creating a new attachment containing the
- pdf base64 encoded.
-
- :param record: The record that will own the attachment.
- :param io.BytesIO buffer: The PDF content as a bytes buffer.
- :return: The input buffer.
- '''
- attachment_name = safe_eval(self.attachment, {'object': record, 'time': time})
- if not attachment_name:
- return None
- attachment_vals = {
- 'name': attachment_name,
- 'raw': buffer.getvalue(),
- 'res_model': self.model,
- 'res_id': record.id,
- 'type': 'binary',
- }
- try:
- self.env['ir.attachment'].create(attachment_vals)
- except AccessError:
- _logger.info("Cannot save PDF report %r as attachment", attachment_vals['name'])
- else:
- _logger.info('The PDF document %s is now saved in the database', attachment_vals['name'])
- return buffer
-
@api.model
def get_wkhtmltopdf_state(self):
'''Get the current state of wkhtmltopdf: install, ok, upgrade, workers or broken.
@@ -641,154 +601,22 @@ class IrActionsReport(models.Model):
)
return view_obj._render_template(template, values).encode()
- def _post_pdf(self, save_in_attachment, pdf_content=None, res_ids=None):
- '''Merge the existing attachments by adding one by one the content of the attachments
- and then, we add the pdf_content if exists. Create the attachments for each record individually
- if required.
-
- :param save_in_attachment: The retrieved attachments as map record.id -> attachment_id.
- :param pdf_content: The pdf content newly generated by wkhtmltopdf.
- :param res_ids: the ids of record to allow postprocessing.
- :return: The pdf content of the merged pdf.
- '''
-
- def close_streams(streams):
- for stream in streams:
- try:
- stream.close()
- except Exception:
- pass
-
- # Check special case having only one record with existing attachment.
- # In that case, return directly the attachment content.
- # In that way, we also ensure the embedded files are well preserved.
- if len(save_in_attachment) == 1 and not pdf_content:
- return list(save_in_attachment.values())[0].getvalue()
-
- # Create a list of streams representing all sub-reports part of the final result
- # in order to append the existing attachments and the potentially modified sub-reports
- # by the _postprocess_pdf_report calls.
- streams = []
-
- # In wkhtmltopdf has been called, we need to split the pdf in order to call the postprocess method.
- if pdf_content:
- pdf_content_stream = io.BytesIO(pdf_content)
- # Build a record_map mapping id -> record
- record_map = {r.id: r for r in self.env[self.model].browse([res_id for res_id in res_ids if res_id])}
-
- # If no value in attachment or no record specified, only append the whole pdf.
- if not record_map or not self.attachment:
- streams.append(pdf_content_stream)
- else:
- if len(res_ids) == 1:
- # Only one record, so postprocess directly and append the whole pdf.
- if res_ids[0] in record_map and not res_ids[0] in save_in_attachment:
- new_stream = self._postprocess_pdf_report(record_map[res_ids[0]], pdf_content_stream)
- # If the buffer has been modified, mark the old buffer to be closed as well.
- if new_stream and new_stream != pdf_content_stream:
- close_streams([pdf_content_stream])
- pdf_content_stream = new_stream
- streams.append(pdf_content_stream)
- else:
- # In case of multiple docs, we need to split the pdf according the records.
- # To do so, we split the pdf based on top outlines computed by wkhtmltopdf.
- # An outline is a html tag found on the document. To retrieve this table,
- # we look on the pdf structure using pypdf to compute the outlines_pages from
- # the top level heading in /Outlines.
- reader = PdfFileReader(pdf_content_stream)
- root = reader.trailer['/Root']
- if '/Outlines' in root and '/First' in root['/Outlines']:
- outlines_pages = []
- node = root['/Outlines']['/First']
- while True:
- outlines_pages.append(root['/Dests'][node['/Dest']][0])
- if '/Next' not in node:
- break
- node = node['/Next']
- outlines_pages = sorted(set(outlines_pages))
- # There should be only one top-level heading by document
- assert len(outlines_pages) == len(res_ids)
- # There should be a top-level heading on first page
- assert outlines_pages[0] == 0
- for i, num in enumerate(outlines_pages):
- to = outlines_pages[i + 1] if i + 1 < len(outlines_pages) else reader.numPages
- attachment_writer = PdfFileWriter()
- for j in range(num, to):
- attachment_writer.addPage(reader.getPage(j))
- stream = io.BytesIO()
- attachment_writer.write(stream)
- if res_ids[i] and res_ids[i] not in save_in_attachment:
- new_stream = self._postprocess_pdf_report(record_map[res_ids[i]], stream)
- # If the buffer has been modified, mark the old buffer to be closed as well.
- if new_stream and new_stream != stream:
- close_streams([stream])
- stream = new_stream
- streams.append(stream)
- close_streams([pdf_content_stream])
- else:
- # If no outlines available, do not save each record
- streams.append(pdf_content_stream)
-
- # If attachment_use is checked, the records already having an existing attachment
- # are not been rendered by wkhtmltopdf. So, create a new stream for each of them.
- if self.attachment_use:
- for stream in save_in_attachment.values():
- streams.append(stream)
-
- # Build the final pdf.
- # If only one stream left, no need to merge them (and then, preserve embedded files).
- if len(streams) == 1:
- result = streams[0].getvalue()
- else:
- try:
- result = self._merge_pdfs(streams)
- except utils.PdfReadError:
- raise UserError(_("One of the documents you are trying to merge is encrypted"))
-
- # We have to close the streams after PdfFileWriter's call to write()
- close_streams(streams)
- return result
-
- def _get_unreadable_pdfs(self, streams):
- unreadable_streams = []
-
- for stream in streams:
- writer = PdfFileWriter()
- result_stream = io.BytesIO()
- try:
- reader = PdfFileReader(stream)
- writer.appendPagesFromReader(reader)
- writer.write(result_stream)
- except utils.PdfReadError:
- unreadable_streams.append(stream)
-
- return unreadable_streams
-
- def _raise_on_unreadable_pdfs(self, streams, stream_record):
- unreadable_pdfs = self._get_unreadable_pdfs(streams)
- if unreadable_pdfs:
- records = [stream_record[s].name for s in unreadable_pdfs if s in stream_record]
- raise UserError(_(
- "Odoo is unable to merge the PDFs attached to the following records:\n"
- "%s\n\n"
- "Please exclude them from the selection to continue. It's possible to "
- "still retrieve those PDFs by selecting each of the affected records "
- "individually, which will avoid merging.") % "\n".join(records))
-
+ @api.model
def _merge_pdfs(self, streams):
writer = PdfFileWriter()
for stream in streams:
- reader = PdfFileReader(stream)
- writer.appendPagesFromReader(reader)
+ try:
+ reader = PdfFileReader(stream)
+ writer.appendPagesFromReader(reader)
+ except utils.PdfReadError:
+ raise UserError(_("Odoo is unable to merge the generated PDFs."))
result_stream = io.BytesIO()
streams.append(result_stream)
writer.write(result_stream)
- return result_stream.getvalue()
+ return result_stream
- def _render_qweb_pdf(self, res_ids=None, data=None):
- """
- :rtype: bytes
- """
+ def _render_qweb_pdf_prepare_streams(self, data, res_ids=None):
+ self.ensure_one()
if not data:
data = {}
data.setdefault('report_type', 'pdf')
@@ -796,88 +624,202 @@ class IrActionsReport(models.Model):
# access the report details with sudo() but evaluation context as sudo(False)
self_sudo = self.sudo()
+ collected_streams = OrderedDict()
+
+ # Fetch the existing attachments from the database for later use.
+ # Reload the stream from the attachment in case of 'attachment_use'.
+ if res_ids:
+ records = self.env[self_sudo.model].browse(res_ids)
+ for record in records:
+ stream = None
+ attachment = None
+ if self_sudo.attachment:
+ attachment = self_sudo.retrieve_attachment(record)
+
+ # Extract the stream from the attachment.
+ if attachment and self_sudo.attachment_use:
+ stream = io.BytesIO(attachment.raw)
+
+ # Ensure the stream can be saved in Image.
+ if attachment.mimetype.startswith('image'):
+ img = Image.open(stream)
+ new_stream = io.BytesIO()
+ img.convert("RGB").save(new_stream, format="pdf")
+ stream.close()
+ stream = new_stream
+
+ collected_streams[record.id] = {
+ 'stream': stream,
+ 'attachment': attachment,
+ }
+
+ # Call 'wkhtmltopdf' to generate the missing streams.
+ res_ids_wo_stream = [res_id for res_id, stream_data in collected_streams.items() if not stream_data['stream']]
+ is_whtmltopdf_needed = not res_ids or res_ids_wo_stream
+
+ if is_whtmltopdf_needed:
+
+ if self.get_wkhtmltopdf_state() == 'install':
+ # wkhtmltopdf is not installed
+ # the call should be catched before (cf /report/check_wkhtmltopdf) but
+ # if get_pdf is called manually (email template), the check could be
+ # bypassed
+ raise UserError(_("Unable to find Wkhtmltopdf on this system. The PDF can not be created."))
+
+ # Disable the debug mode in the PDF rendering in order to not split the assets bundle
+ # into separated files to load. This is done because of an issue in wkhtmltopdf
+ # failing to load the CSS/Javascript resources in time.
+ # Without this, the header/footer of the reports randomly disappear
+ # because the resources files are not loaded in time.
+ # https://github.com/wkhtmltopdf/wkhtmltopdf/issues/2083
+ additional_context = {'debug': False}
+
+ # As the assets are generated during the same transaction as the rendering of the
+ # templates calling them, there is a scenario where the assets are unreachable: when
+ # you make a request to read the assets while the transaction creating them is not done.
+ # Indeed, when you make an asset request, the controller has to read the `ir.attachment`
+ # table.
+ # This scenario happens when you want to print a PDF report for the first time, as the
+ # assets are not in cache and must be generated. To workaround this issue, we manually
+ # commit the writes in the `ir.attachment` table. It is done thanks to a key in the context.
+ if not config['test_enable']:
+ additional_context['commit_assetsbundle'] = True
+
+ html = self_sudo.with_context(**additional_context)._render_qweb_html(res_ids_wo_stream, data=data)[0]
+
+ bodies, html_ids, header, footer, specific_paperformat_args = self._prepare_html(html, report_model=self_sudo.model)
+
+ if self_sudo.attachment and set(res_ids) != set(html_ids):
+ raise UserError(_(
+ "The report's template %r is wrong, please contact your administrator. \n\n"
+ "Can not separate file to save as attachment because the report's template does not contains the"
+ " attributes 'data-oe-model' and 'data-oe-id' on the div with 'article' classname.",
+ self.name,
+ ))
+
+ pdf_content = self._run_wkhtmltopdf(
+ bodies,
+ header=header,
+ footer=footer,
+ landscape=self._context.get('landscape'),
+ specific_paperformat_args=specific_paperformat_args,
+ set_viewport_size=self._context.get('set_viewport_size'),
+ )
+ pdf_content_stream = io.BytesIO(pdf_content)
+
+ # Printing a PDF report without any records. The content could be returned directly.
+ if not res_ids:
+ return {
+ False: {
+ 'stream': pdf_content_stream,
+ 'attachment': None,
+ }
+ }
+
+ # Split the pdf for each record using the PDF outlines.
+
+ if len(res_ids_wo_stream) == 1:
+ # Only one record: append the whole PDF.
+ collected_streams[res_ids_wo_stream[0]]['stream'] = pdf_content_stream
+ else:
+ # In case of multiple docs, we need to split the pdf according the records.
+ # To do so, we split the pdf based on top outlines computed by wkhtmltopdf.
+ # An outline is a html tag found on the document. To retrieve this table,
+ # we look on the pdf structure using pypdf to compute the outlines_pages from
+ # the top level heading in /Outlines.
+ reader = PdfFileReader(pdf_content_stream)
+ root = reader.trailer['/Root']
+ has_valid_outlines = '/Outlines' in root and '/First' in root['/Outlines']
+ if not has_valid_outlines:
+ return {False: {
+ 'report_action': self,
+ 'stream': pdf_content_stream,
+ 'attachment': None,
+ }}
+
+ outlines_pages = []
+ node = root['/Outlines']['/First']
+ while True:
+ outlines_pages.append(root['/Dests'][node['/Dest']][0])
+ if '/Next' not in node:
+ break
+ node = node['/Next']
+ outlines_pages = sorted(set(outlines_pages))
+ # There should be only one top-level heading by document
+ assert len(outlines_pages) == len(res_ids_wo_stream)
+ # There should be a top-level heading on first page
+ assert outlines_pages[0] == 0
+ for i, num in enumerate(outlines_pages):
+ to = outlines_pages[i + 1] if i + 1 < len(outlines_pages) else reader.numPages
+ attachment_writer = PdfFileWriter()
+ for j in range(num, to):
+ attachment_writer.addPage(reader.getPage(j))
+ stream = io.BytesIO()
+ attachment_writer.write(stream)
+ collected_streams[res_ids_wo_stream[i]]['stream'] = stream
+
+ return collected_streams
+
+ def _render_qweb_pdf(self, res_ids=None, data=None):
+ self.ensure_one()
+ if not data:
+ data = {}
+ data.setdefault('report_type', 'pdf')
+ self_sudo = self.sudo()
+
# In case of test environment without enough workers to perform calls to wkhtmltopdf,
# fallback to render_html.
if (tools.config['test_enable'] or tools.config['test_file']) and not self.env.context.get('force_report_rendering'):
return self_sudo._render_qweb_html(res_ids, data=data)
- # As the assets are generated during the same transaction as the rendering of the
- # templates calling them, there is a scenario where the assets are unreachable: when
- # you make a request to read the assets while the transaction creating them is not done.
- # Indeed, when you make an asset request, the controller has to read the `ir.attachment`
- # table.
- # This scenario happens when you want to print a PDF report for the first time, as the
- # assets are not in cache and must be generated. To workaround this issue, we manually
- # commit the writes in the `ir.attachment` table. It is done thanks to a key in the context.
- context = dict(self.env.context)
- if not config['test_enable']:
- context['commit_assetsbundle'] = True
+ collected_streams = self._render_qweb_pdf_prepare_streams(data, res_ids=res_ids)
- # Disable the debug mode in the PDF rendering in order to not split the assets bundle
- # into separated files to load. This is done because of an issue in wkhtmltopdf
- # failing to load the CSS/Javascript resources in time.
- # Without this, the header/footer of the reports randomly disappear
- # because the resources files are not loaded in time.
- # https://github.com/wkhtmltopdf/wkhtmltopdf/issues/2083
- context['debug'] = False
+ # Generate the ir.attachment if needed.
+ if self.attachment:
+ attachment_vals_list = []
+ for res_id, stream_data in collected_streams.items():
+ # An attachment already exists.
+ if stream_data['attachment']:
+ continue
+
+ record = self.env[self_sudo.model].browse(res_id)
+ attachment_name = safe_eval(self.attachment, {'object': record, 'time': time})
+
+ # Unable to compute a name for the attachment.
+ if not attachment_name:
+ continue
+
+ attachment_vals_list.append({
+ 'name': attachment_name,
+ 'raw': stream_data['stream'].getvalue(),
+ 'res_model': self.model,
+ 'res_id': record.id,
+ 'type': 'binary',
+ })
+
+ if attachment_vals_list:
+ attachment_names = ', '.join(x['name'] for x in attachment_vals_list)
+ try:
+ self.env['ir.attachment'].create(attachment_vals_list)
+ except AccessError:
+ _logger.info("Cannot save PDF report %r attachments for user %r", attachment_names, self.env.user.display_name)
+ else:
+ _logger.info("The PDF documents %r are now saved in the database", attachment_names)
+
+ # Merge all streams together for a single record.
+ streams_to_merge = [x['stream'] for x in collected_streams.values()]
+ if len(streams_to_merge) == 1:
+ pdf_content = streams_to_merge[0].getvalue()
+ else:
+ with self._merge_pdfs(streams_to_merge) as pdf_merged_stream:
+ pdf_content = pdf_merged_stream.getvalue()
+
+ for stream in streams_to_merge:
+ stream.close()
- save_in_attachment = OrderedDict()
- # Maps the streams in `save_in_attachment` back to the records they came from
- stream_record = dict()
if res_ids:
- # Dispatch the records by ones having an attachment and ones requesting a call to
- # wkhtmltopdf.
- Model = self.env[self_sudo.model]
- record_ids = Model.browse(res_ids)
- wk_record_ids = Model
- if self_sudo.attachment:
- for record_id in record_ids:
- attachment = self_sudo.retrieve_attachment(record_id)
- if attachment:
- stream = self_sudo._retrieve_stream_from_attachment(attachment)
- save_in_attachment[record_id.id] = stream
- stream_record[stream] = record_id
- if not self_sudo.attachment_use or not attachment:
- wk_record_ids += record_id
- else:
- wk_record_ids = record_ids
- res_ids = wk_record_ids.ids
+ _logger.info("The PDF report has been generated for model: %s, records %s.", self_sudo.model, str(res_ids))
- # A call to wkhtmltopdf is mandatory in 2 cases:
- # - The report is not linked to a record.
- # - The report is not fully present in attachments.
- if save_in_attachment and not res_ids:
- _logger.info('The PDF report has been generated from attachments.')
- self._raise_on_unreadable_pdfs(save_in_attachment.values(), stream_record)
- return self_sudo._post_pdf(save_in_attachment), 'pdf'
-
- if self.get_wkhtmltopdf_state() == 'install':
- # wkhtmltopdf is not installed
- # the call should be catched before (cf /report/check_wkhtmltopdf) but
- # if get_pdf is called manually (email template), the check could be
- # bypassed
- raise UserError(_("Unable to find Wkhtmltopdf on this system. The PDF can not be created."))
-
- html = self_sudo.with_context(context)._render_qweb_html(res_ids, data=data)[0]
-
- bodies, html_ids, header, footer, specific_paperformat_args = self.with_context(context)._prepare_html(html, report_model=self_sudo.model)
-
- if self_sudo.attachment and set(res_ids) != set(html_ids):
- raise UserError(_("The report's template '%s' is wrong, please contact your administrator. \n\n"
- "Can not separate file to save as attachment because the report's template does not contains the attributes 'data-oe-model' and 'data-oe-id' on the div with 'article' classname.") % self.name)
-
- pdf_content = self._run_wkhtmltopdf(
- bodies,
- header=header,
- footer=footer,
- landscape=context.get('landscape'),
- specific_paperformat_args=specific_paperformat_args,
- set_viewport_size=context.get('set_viewport_size'),
- )
- if res_ids:
- self._raise_on_unreadable_pdfs(save_in_attachment.values(), stream_record)
- _logger.info('The PDF report has been generated for model: %s, records %s.' % (self_sudo.model, str(res_ids)))
- return self_sudo._post_pdf(save_in_attachment, pdf_content=pdf_content, res_ids=html_ids), 'pdf'
return pdf_content, 'pdf'
@api.model