Skip to content
Ax1zzPublic

About

Batch-generate Word and PDF documents from a spreadsheet - with a preflight check that catches mistakes before you generate 500 broken files.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

11 Commits

Folders and files

Repository files navigation

mergemill

Turn a spreadsheet and a Word template into hundreds of documents β€” and catch the mistakes before you generate them, not after.

License: MIT Python 3.9+ Tests Offline

πŸ‡¬πŸ‡§ English Β· πŸ‡·πŸ‡Ί Русский


Word Mail Merge cannot save one file per record, cannot name files from your data, and cannot export one PDF per record. So people pay freelancers $60–$600 to write a throwaway VBA macro instead. mergemill is that macro, done properly β€” and it refuses to run until your template and your data actually agree.

pip install mergemill
mergemill init                      # writes a working example
mergemill check letter.docx clients.xlsx
mergemill run   letter.docx clients.xlsx -o out --name "{{ client_name }}"

The part nobody else does: preflight

Generating documents is easy. Generating 500 correct documents is the hard part, and every failure is silent β€” a placeholder that never matched a column leaves a blank in a contract; two rows that produce the same filename overwrite each other and you quietly end up with 480 files instead of 500.

mergemill check writes nothing. It just tells you what would go wrong.

mergemill preflight in the terminal

Real output above: two typo'd placeholders with did-you-mean suggestions, a filename collision naming the exact source rows, a client called CON that Windows would reject, unused columns, and blank cells in a column the template prints. Nothing was generated. Exit code 2.

Not a terminal person?

mergemill ui opens a local page in your browser. Same engine, no command line, and the files never leave your machine β€” the "server" is your own computer.

mergemill web UI showing a clean preflight

When something is wrong, it says so before you can click Generate:

mergemill web UI showing preflight errors

Why this exists

These are real people, quoted verbatim, describing the exact gap this fills.

"How can I get this document to save as 20 individual record files? I really want the entire 30 page document to save as a single .pdf record, by school name." β€” learn.microsoft.com/answers #5053938

"I found a macro that lets me split the merged document after it's made, but it names the documents 1, 2, 3 etc. I have hundreds of documents I need to make, and renaming them manually is not workable." β€” superuser.com/q/842306

"I just can not export each pages to a pdf file separated. Is there any way to print/export files using direct mail on microsoft word? Preferential using some Merge Field as file name?" β€” superuser.com/q/1220625

"I'd like to use my Excel data to merge with Word to produce a letter for each ID which summarises each individual exam activity in tabular form (for which there is a separate record entered in Excel)." β€” learn.microsoft.com/answers #4800820

That last one is the many-to-one merge β€” one document per customer containing all of their line items. Word cannot do it at all. mergemill does it with one flag.

Quick start

1. Write a template in Word

Type placeholders directly into the document. No MERGEFIELD, no field codes β€” just text.

Dear {{ contact_first_name }},

Thank you for instructing {{ firm_name }} in connection with {{ matter }}.
Our fee is {{ fee | num(2) }} {{ currency }}, payable within {{ payment_terms }} days.

{% if retainer %}A retainer of {{ retainer | num(2) }} is payable before work begins.{% endif %}

A column headed Contact First Name in your spreadsheet resolves to {{ contact_first_name }}. Case, spaces and punctuation are handled for you.

2. Check before you generate

mergemill check letter.docx clients.xlsx --name "{{ client_name }}"

3. Generate

mergemill run letter.docx clients.xlsx -o out --name "{{ client_name }}-{{ matter }}"

Examples

# One PDF per row, no Word or LibreOffice required
mergemill run letter.docx clients.xlsx -o out --format pdf --name "{{ client_name }}"

# One invoice per customer, containing all of their line items
mergemill run invoice.docx lines.xlsx -o out \
    --group-by invoice_no --name "{{ group_value }}"

# Only German clients over 1000, and show me what would happen first
mergemill run letter.docx clients.xlsx -o out \
    --filter "country == 'DE' and fee > 1000" --dry-run

# Re-run a batch without touching what already exists
mergemill run letter.docx clients.xlsx -o out --skip-existing

# Use it as a CI gate: fail the build if a template drifts from its data
mergemill check letter.docx clients.xlsx --strict --json

Template syntax

Standard Jinja2 via docxtpl, plus a few filters for spreadsheet data.

You write You get
{{ client_name }} the cell value; a blank cell renders as nothing, never None
{{ start_date }} 2026-06-01 β€” never 2026-06-01 00:00:00
{{ fee | num(2) }} 1234.50
{{ fee | num(2, ' ') }} 1 234.50
{{ start_date | date('%d %B %Y') }} 01 June 2026
{{ middle_name | blank('N/A') }} N/A when the cell is empty
{% if vip %}…{% endif %} conditional sections

Always available, whatever is in your spreadsheet: {{ row_number }} (the actual spreadsheet line), {{ row_index }}, {{ total_rows }}, {{ today }}, {{ source_file }}. With --group-by: {{ group_value }}, {{ group_count }} and {{ rows }}.

Repeating table rows

Put {%tr for line in rows %} in its own table row and {%tr endfor %} in another β€” the rows between them repeat.

{%tr for line in rows %}
{{ line.description }} β€” {{ line.quantity }}
{%tr endfor %}

Compared with the alternatives

Per-record files Filename from data PDF per record Many-to-one Runs offline Price
mergemill βœ… βœ… βœ… βœ… βœ… Free, MIT
Word Mail Merge ❌ ❌ ❌ ❌ βœ… Bundled with Word
Merge Tools add-in (VBA) βœ… βœ… βœ… partial βœ… Free, Windows + Word only
docxtpl ❌ library ❌ ❌ βœ… βœ… Free β€” you write the batch loop
Carbone βœ… βœ… βœ… βœ… on-prem tier $29–$159/mo, on-prem $1,500/yr
docxtemplater βœ… βœ… paid module βœ… βœ… Free core; modules €500–€3,000/yr
A Fiverr freelancer βœ… βœ… βœ… βœ… n/a $60–$600, once, unmaintainable

Prices were taken from each vendor's public pricing page. None of them preflight.

docxtpl deserves a specific note: it is excellent, and mergemill is built on it. But it is a library that renders one document from one dictionary. Spreadsheet input, the batch loop, filename templating, PDF output, grouping and validation are all left to you. That is the gap.

Privacy

Documents contain salaries, contracts and client names, so:

  • No network calls, ever. Not for telemetry, not for updates, not for fonts.
  • The web UI binds to 127.0.0.1 only and refuses to bind to a public interface.
  • Uploads in the UI land in a temporary directory that is deleted when the request ends.
  • PDF conversion is local β€” no Word, no LibreOffice, no cloud converter.

Install

pip install mergemill          # core: .docx output
pip install 'mergemill[pdf]'   # adds PDF output

Python 3.9+. Runtime dependencies are just docxtpl and openpyxl; the web UI uses only the standard library.

Command reference

Command Does
mergemill init [dir] Write a runnable example: two templates, two spreadsheets
mergemill inspect T.docx List every placeholder the template uses
mergemill check T.docx D.xlsx Preflight only β€” never writes documents
mergemill run T.docx D.xlsx -o out Preflight, then generate
mergemill ui Local browser interface

Useful flags: --name (filename pattern) Β· --group-by Β· --format docx\|pdf\|both Β· --filter Β· --limit Β· --dry-run Β· --skip-existing Β· --strict Β· --force Β· --json Β· --sheet Β· --date-format

Exit codes: 0 clean Β· 1 some documents failed Β· 2 preflight errors.

Use it as a library

from mergemill import data, template, preflight, render

dataset = data.load("clients.xlsx")
info = template.inspect("letter.docx")

result = preflight.run(dataset, info, "{{ client_name }}", output_dir="out")
for finding in result.findings:
    print(finding.code, finding.severity, finding.title)

if result.ok:
    plan = render.build_plan(dataset, info.path, "{{ client_name }}", "out")
    outcome = render.execute(plan)
    render.write_manifest(plan, outcome, "out/manifest.json")

Every run can write a JSON manifest mapping each output file back to the spreadsheet rows that produced it β€” useful when someone asks six months later where a document came from.

Preflight rules

Code Level Meaning
E001 error Template has a Jinja syntax error
E002 error Placeholder has no matching column (with did-you-mean)
E003 error Filename pattern references an unknown field
E004 error Two or more rows produce the same filename
E005 error Filename is illegal on Windows or macOS
E006 error Data source has no rows
E007 error Two columns normalise to the same identifier
E008 error --group-by column does not exist
W101 warning Column present in the data but never used
W102 warning Blank cells in a column the template prints
W103 warning Template contains no placeholders
W104 warning Output folder already holds files that would be overwritten
W105 warning Grouping has no effect β€” every group has one row
W106 warning A filename needed automatic fixing
W107 warning A column is shadowed by a built-in name and is unreachable

Roadmap

  • .pptx and .xlsx templates
  • Images per record ({{ logo }} from a file-path column)
  • A mergemill.toml so a project's flags live in the repo
  • Watch mode: regenerate when the spreadsheet changes
  • Google Sheets input via an explicit, opt-in fetch step
  • Pre-built binaries so non-Python users can skip pip

Have a use case that does not fit? Open an issue β€” real workflows are more useful than feature requests in the abstract.

Contributing

See CONTRIBUTING.md. The short version: pip install -e '.[dev]', pytest, and new preflight rules are the most welcome contribution of all.

Licence

MIT β€” see LICENSE.

About

Batch-generate Word and PDF documents from a spreadsheet - with a preflight check that catches mistakes before you generate 500 broken files.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages