Frappe v16.35.0 + wkhtmltopdf: dynamic Page X/Y in footer-html

Hi,

I’m using ERPNext/Frappe v16.35.0 with the standard wkhtmltopdf PDF generator.

My wkhtmltopdf version is:

wkhtmltopdf 0.12.6.1 (with patched qt)

I have a custom Sales Invoice Print Format with a bank footer. I need this footer to appear on every page of the generated PDF.

I initially used a normal HTML element:

<div class="bank-footer">
    ...
</div>

but it only appeared on one page.

I then found the footer-html mechanism in frappe.utils.pdf.prepare_header_footer() and changed the Print Format to:

<div id="footer-html" class="bank-footer">
    ...
</div>

This works correctly. The footer is now passed to wkhtmltopdf as --footer-html and appears on every page of a multi-page PDF.

I verified this with an actual generated PDF, not the browser print preview:

PAGES: 2
PAGE 1: Bankverbindung=True, IBAN=True
PAGE 2: Bankverbindung=True, IBAN=True

So the repeating footer itself is working correctly.


The remaining problem is the page number.

I would like the footer to display:

Seite 1/2
Seite 2/2

I found the standard Frappe template:

frappe/templates/print_formats/pdf_header_footer.html

which contains JavaScript similar to:

function subst() {
    var vars = {};
    var x = window.location.search.substring(1).split('&');
    ...
    var x = ['frompage','topage','page','webpage','section','subsection','subsubsection'];
    ...
}

Therefore I tried:

Seite <span class="page"></span>/<span class="topage"></span>

However, the generated PDF contains empty values.

I checked the Frappe v16.35.0 source and found that frappe.utils.pdf.get_pdf() explicitly adds:

options.update({    "disable-javascript": "",    "disable-local-file-access": "",})

before calling:

pdfkit.from_string(    html,    options=options or {},    verbose=True)

So the subst() JavaScript from the standard Frappe footer template cannot execute.


My questions are:

  1. What is the recommended/supported way in Frappe v16.35.0 to display dynamic Page X/Y inside a footer-html footer while keeping JavaScript disabled?

  2. Can wkhtmltopdf’s native:

--footer-center "Seite [page]/[topage]"

be configured through Frappe’s pdf_options or another supported mechanism?

  1. If footer-html is already being used for a custom HTML bank footer, is there a supported way to combine the custom HTML footer with wkhtmltopdf’s native [page]/[topage] variables?

  2. Is there a Frappe hook or recommended customization point for this, without modifying Frappe core?

  3. Is switching the Print Format to the Chrome PDF Generator the recommended solution for dynamic page numbers in Frappe v16?

I would prefer not to globally re-enable JavaScript for PDF generation, and I would also prefer not to modify Frappe core.

The important requirement is that the result must be the actual PDF generated by ERPNext, because this PDF will subsequently be uploaded to an external accounting system.

Thank you.

I’ve invested a lot of hours battling with this issue. Coming from Odoo, it was quite easy to set an A4 background image and place the report information and “Page X/Y” on top of it. Since wkhtmltopdf is no longer supported, I decided to use Chrome as the renderer.
I tried --header-html and --footer-html, and the CSS way (@page { @bottom-right { content: "Seite " counter(page); font-size: 9pt; color: #333; } }) but it did not work — the letterhead didn’t work properly either.
So in the end I wrote a script that uses an A4 .svg as a background image and writes the page number with Python, perfectly aligned at the bottom right.
If you’re interested, I can upload the code.

Thanks a lot! If you don’t mind, I’d really appreciate it if you could upload the code. It might be very useful for my project as well. Thank you!

Servus/Hi Barrakuda,

I uploaded the here: pdf.py

This solution intercepts Frappe’s standard download_pdf to:

  1. Merge a corporate background letterhead (stationery.pdf) vectorially using pypdf.
  2. Cache the letterhead in RAM to prevent disk I/O bottlenecks across multi-page documents.
  3. Inject millimeter-accurate, right-aligned multilingual pagination (“Page X / Y”, “Seite X / Y”, “Página X / Y”) computed with Adobe Font Metrics (AFM).

1. Requirements

Install pypdf in your bench environment (bench --site your-site.local pip install pypdf>=4.0.0 ) or add “pypdf>=4.0.0” under dependencies in your app’s pyproject.toml.

2. Place your letterhead

Save your blank letterhead PDF to: <your_app>/public/stationery.pdf

3. Add the code to <your_app>/<your_app>/utils/pdf.py

4. Register the hook in <your_app>/hooks.py

override_whitelisted_methods = {
“frappe.utils.print_format.download_pdf”: “your_app.utils.pdf.download_pdf_with_stationery”
}

5. Restart bench after making these changes: bench restart

6. Please change pia_reports (is 2 times) and pia_ (once) in pdf.py for your app name. Mine is pia_reports

I think is all.

This workaround is handy for maintaining consistent pagination and a branded letterhead with Chrome. Be cautious about overriding Frappe’s standard download_pdf method, as it might require updates during version changes.

If you test it, try both single-page and multi-page documents, varying page lengths and non-English text, to ensure the letterhead and page numbers remain aligned. Also, test other PDF downloads to confirm the override doesn’t impact formats that don’t require the stationery.

Thank you for the tip about overriding download_pdf. We will see, I did just not found a better way to get it.

I tested it in German and Spanish with 1 and 3 pages, I made changes till I got that in all languages the “Page X/X” is properly aligned at the wished position. I tested it also on a few reports: invoice, delivery note, etc. All of them personalised. It has a filter so it only applies to the personalised reports on my module:

original_download_pdf(doctype, name, format=format, doc=doc, ...)

if hasattr(frappe.response, "filecontent") and frappe.response.filecontent:
if format and "pia_" in format:   # 👈 This is the key condition
frappe.response.filecontent = merge_stationery(frappe.response.filecontent, lang=lang)

Barrakuda: I forgot to say, you should adapt the high of each margin (top and bottom) to match the high of your header and footer.

That is understandable. I like the filter for pia_ formats since it prevents the override to impact other reports.

Good to see you have tested it on German and Spanish and in varied document lengths too. The header and footer margin adjustment is also useful – that’s probably something people could easily forget when adapting the code.