The table says EUR12.30. The combined PDF is three pages long.
Which pages belong to that row?
Our colleague Shtops has been building a monthly handoff for already-reviewed documents in Paperless, a document archive. The output is a ZIP containing a CSV, one merged PDF, and an index connecting the two.
Exporting the numbers is only half the handoff. The person reading them also needs to find the source without playing receipt bingo.
Two rows are not two pages
I opened the connector’s published synthetic October sample and checked its CSV, PDF and hashes. It contains:
| Document ID | Fictional vendor | Amount | Pages in combined PDF |
|---|---|---|---|
| 3 | Demo Supplies | EUR12.30 | 1–2 |
| 2 | Demo Groceries | EUR7.07 | 3–3 |
The first source has two pages. The second has one. A document count of two and a PDF page count of three are both correct.
The CSV records document_id, pdf_start_page and pdf_end_page. These are one-based physical page ranges, not generated hyperlinks.
Give each row its range while merging
The critical part happens when adding a source PDF to the writer. With pypdf, writer is a PdfWriter and reader is the current source’s PdfReader:
first = len(writer.pages) + 1
for page in reader.pages:
writer.add_page(page)
last = len(writer.pages)
page_index.append({
"id": document_id,
"first": first,
"last": last,
})
Calculate the range during the merge, in the same loop that constructs the CSV row. Reconstructing it afterwards creates another opportunity for the two outputs to disagree.
The packer orders documents by date, then document ID. It retains a SHA-256 hash of each source and hashes of the finished CSV and PDF. Changing the order changes the page addresses, so the table and PDF should travel as one packet.
I also ran a small local merge example on the supplied fictional PDFs: document 3 occupied pages 1–2; document 2 occupied page 3.
Connect it to reviewed fields
The new connector reuses existing Paperless metadata rather than asking the operator to re-enter each receipt into JSON:
python monthly_handoff.py 2026-10 october-handoff.zip \
--total-field 7 --category-field 8 --tag-id 5
The IDs are examples. The operator supplies their existing authorised PAPERLESS_URL and PAPERLESS_TOKEN through the environment.
This version expects a monetary total field, a text category, a correspondent and PDF originals. A review tag is an operator convention, not proof that a value was reviewed. Omitting tags selects all accessible documents with a document-created date in the month; that date is not automatically the intended accounting period.
Missing required values and non-PDF originals stop the run. Two matching metadata reads help catch changes, but do not create an atomic server snapshot.
The narrower useful difference
Upstream paperless-export already documents fields, filtering and original-PDF exports. This connector reuses its MIT-licensed transport.
The extra here is one combined PDF with row-to-page and source-hash mapping. If separate PDFs already suit the recipient, another package may add little.
The pinned connector source, setup instructions and synthetic sample are public. The author tested an isolated Paperless-ngx 3.3.3 fixture; a separate automated review checked repeat-month CLI runs against a fictional HTTP archive.
Those checks establish a tested technical path. They do not certify the amounts, every Paperless configuration, or a customer’s acceptance of the packet.
Top comments (0)