· 11 min read · By Jason Dorn

How to Build a Dynamic Invoice Template with Line Items in Word

Build one Word invoice template that handles any number of line items, calculates totals automatically, and generates PDFs in bulk from a CSV. No plugins, no Excel formulas — just Jinja2 inside your document.


A freelance designer I know bills 15 clients a month. Until last year, she had 15 different Word files — one per client — because her invoices had different numbers of line items and her mail merge couldn't handle variable-length tables. Every month, she opened each file, updated the dates, changed the amounts, and exported to PDF. Two hours, gone.

What she actually needed was one template that could handle any number of line items, calculate the totals, and generate the whole batch from a spreadsheet. That's exactly what this post walks through.

If you're stuck maintaining a dozen near-identical invoice templates, or if your current mail merge can't handle the "how many line items does this one have" problem, read on. We'll build a single invoice template from scratch, test it with a sample CSV, and you'll walk away with something you can use for real work today.

Key Takeaways

  • One Word template can handle 1-line-item invoices and 50-line-item invoices with zero changes
  • The {% for %} loop in Jinja2 repeats a table row for every line item in your data
  • Totals are calculated automatically using filters like | sum and | round(2)
  • Conditional tax fields let the same template work across jurisdictions
  • Generate 100 invoices in under 3 minutes from a single CSV

Why Word mail merge falls short for invoices

Microsoft Word's built-in mail merge works fine for simple letters: name, address, greeting, body. But the moment your document needs a variable-length table — like line items on an invoice, or attendees on a certificate, or products on a receipt — mail merge breaks down.

Mail merge fills in individual fields. It can't repeat a table row. So if your invoice has 3 line items this month and 12 next month, you either:

  1. Maintain 12 different templates (one per max-row-count), or
  2. Manually delete blank rows after the merge, or
  3. Give up and do it all by hand

None of these are good answers. The real fix is a templating engine that understands loops — and Jinja2, which has been the templating standard for Python projects like Ansible and Flask for over a decade, does this trivially inside a .docx file.

Step 1: Build your invoice template in Word

Open Microsoft Word (or Google Docs, if you prefer — just export as .docx when you're done). Create a normal invoice layout: your logo, your business details, "Bill To" section, invoice number, date, due date.

Then insert a table for the line items. Give it headers like this:

| Description | Quantity | Rate | Subtotal |

This is where most mail merge tutorials stop. Here's where Jinja2 picks up.

Add the loop tags around the repeating row

In your table, right under the headers, add ONE row that looks like this:

| {{ item.description }} | {{ item.qty }} | {{ item.rate }} | {{ item.subtotal }} |

Now this is the magic part. Put a {% for item in line_items %} tag in the cell BEFORE this row (or above the table), and a {% endfor %} tag in the cell AFTER. In practice, most people use a dedicated row for each tag so they don't mess up the table cell alignment.

Here's the full markup in the table area:

{% for item in line_items %}
| {{ item.description }} | {{ item.qty }} | {{ item.rate }} | {{ item.subtotal }} |
{% endfor %}

What this tells EZdoc: "For every entry in the line_items list, repeat this table row with the item's values filled in." If your CSV has 3 items, you get 3 rows. If it has 12, you get 12. Same template.

Add the total row below the loop

Underneath the {% endfor %}, add a row for the total:

| | | **Total:** | {{ total }} |

You can either pass total in from your data directly (easiest) or calculate it inside the template using a Jinja2 filter. If you want the template to do the math, replace {{ total }} with:

{{ line_items | sum(attribute='subtotal_numeric') | round(2) }}

This adds up the subtotal_numeric field from every row in line_items and rounds to 2 decimal places. Note: you'd want to store the numeric value (like 1500.00) separately from the formatted display string (like "$1,500.00"), because | sum works on numbers, not strings.

Step 2: Structure your CSV

The CSV is where people usually get confused. You've got a problem: each invoice has a variable number of line items, but a CSV is flat — one row per record.

There are two ways to solve this in EZdoc, and the right choice depends on how technical your data source is.

Option A: JSON array in a single cell (recommended)

For most use cases, the cleanest pattern is to put the line items as a JSON array inside one cell of your CSV. Each row represents one invoice, and the line_items column holds all that invoice's line items as structured data.

Here's what a CSV would look like:

client_name invoice_number invoice_date due_date line_items total
Acme Corp INV-001 2026-04-01 2026-05-01 [{"description":"Logo design","qty":1,"rate":"$800","subtotal":"$800","subtotal_numeric":800}] $800
Beta LLC INV-002 2026-04-01 2026-05-01 [{"description":"Website copy","qty":4,"rate":"$150","subtotal":"$600","subtotal_numeric":600},{"description":"SEO audit","qty":1,"rate":"$350","subtotal":"$350","subtotal_numeric":350}] $950

Yes, the JSON gets ugly in a spreadsheet. But: you only write it once when you're setting up your workflow, and you can generate it with a formula in Google Sheets or Excel. (We'll cover that in a follow-up post.)

Option B: Separate rows per line item (for source data you can't restructure)

If your invoices come out of an existing system as one row per line item, you can use that format too. The downside is you need some Ruby/Python/JS to group the rows by invoice number before feeding them into EZdoc. We recommend Option A whenever you have control over the data shape.

Step 3: Add conditional fields for taxes

One of the biggest wins of using a templating engine is conditional content. Say your invoices need to show a tax line, but only for clients in certain states. Instead of maintaining two templates, you add a single {% if %} block:

{% if tax_rate > 0 %}
| | | Tax ({{ tax_rate }}%): | {{ tax_amount }} |
{% endif %}
| | | **Total:** | {{ total }} |

Your CSV adds two columns: tax_rate and tax_amount. For clients without sales tax, leave them blank (or set tax_rate to 0). The row only renders when there's actually tax to charge.

Same template works for tax-exempt nonprofits, out-of-state clients, international billing — all from the same CSV. (For more conditional template tricks, see our CSV to PDF tutorial which covers if/elif/else in more depth.)

Step 4: Format currency properly

If your rates come out of a database as raw numbers (like 1500.00), your invoice will show 1500.00 instead of $1,500.00. That looks unprofessional. Jinja2 has filters for formatting, but the cleanest approach is to format the display values in your data source — whether that's your CRM, your time-tracking app, or a Google Sheets formula — and pass them into the template pre-formatted.

In your CSV:
- rate: "$150.00" (display-ready)
- rate_numeric: 150 (used only for math)

In your template, reference {{ item.rate }} for display and {{ item.rate_numeric }} only when you need to sum or multiply.

Step 5: Upload to EZdoc and generate

Save your Word file. Sign in to EZdoc, upload the .docx as a template, upload your CSV, and click Generate. You'll see each row process in real time, and within a minute or two you have a ZIP file with every invoice as a PDF.

The first time you run it, expect to iterate once or twice: a typo in the column header, a missing subtotal_numeric field, etc. After the second run, it just works.

Real-world example: a freelance designer's monthly billing

Let's walk through a concrete scenario. My friend the designer bills 15 clients. Before, she maintained 5 different Word templates to cover the common cases (1-2 line items, 3-5 line items, 6+ line items, etc.), plus a separate folder for international clients (different tax rules), plus another for retainer clients (different payment terms).

With Jinja2 inside her Word template, she consolidated everything into one template and one CSV:

  • The line_items column in the CSV is a JSON array, holding anywhere from 1 to 20 items per invoice
  • A client_tax_rate column sets the tax percentage (0% for most, 7.25% for California, 8.875% for NYC)
  • A payment_terms column sets the net period ("Net 30", "Net 60", "Due on receipt")
  • A conditional {% if retainer %} block at the top of the template adds a note to retainer clients about their contract

Her monthly billing now takes 5 minutes: she exports her time-tracking data, massages it into the CSV format, and generates the batch. The 2-hour manual process is gone, and she's never had to maintain multiple templates again.

Common pitfalls (and how to avoid them)

1. Forgetting to include the {% endfor %} tag. The template will render but the loop won't close properly and you'll get weird output. Always double-check every {% for %} has a matching {% endfor %}.

2. Putting loop tags inside table cells where they break formatting. Word and Google Docs sometimes auto-format table cells in weird ways. If you're having trouble, use a paragraph BEFORE the table row for {% for %} and a paragraph AFTER for {% endfor %}. The loop will still repeat the table row correctly.

3. Trying to do math on string values. "$1,500.00" | sum doesn't work — you need numeric values. Store display strings and numeric values as separate fields in your data.

4. Column header mismatches. Your CSV column headers must match your template's Jinja2 variable names exactly. {{ client_name }} in the template needs client_name in the CSV, not Client Name or clientName.

5. Escaping issues in JSON cells. Use straight quotes (") not curly quotes (") inside JSON. Spreadsheet apps sometimes auto-convert — turn off autocorrect for the line_items column if you're editing in Google Sheets.

FAQ

Does this work with Google Docs?

Yes. Create your template in Google Docs, add the Jinja2 tags exactly like you would in Word, then File → Download → Microsoft Word (.docx). Upload that file to EZdoc. Formatting is preserved.

Can I have different headers for different invoices?

Yes, but you'll need to use conditional blocks or multiple templates. For small variations (different company addresses, different logos), put the variation in your data. For dramatically different layouts, use multi-template mode and include a _template column in your CSV.

How do I calculate totals inside the template instead of in my CSV?

Use the | sum filter on a list of numeric values. Example:

{{ line_items | sum(attribute='subtotal_numeric') | round(2) }}

This is useful when you don't know the total ahead of time (like when line items come from an API). For pre-calculated totals, just store the total directly in your CSV — it's simpler.

What if some invoices have different currencies?

Add a currency column to your CSV and reference it in the template: {{ currency }}{{ amount }}. Or, if each client always uses the same currency, store the pre-formatted string including the symbol.

Is this more powerful than Word's built-in mail merge?

Yes, by a lot. Word mail merge does simple field substitution. Jinja2 does loops, conditionals, filters, calculations, and nested data — all inside a normal .docx file that non-technical users can still edit visually.

Start automating your invoices

The manual monthly billing grind is one of those problems that's small enough to ignore but big enough to be annoying forever. One weekend of template setup pays off every month after.

Try EZdoc free — 25 free pages per month, no credit card. That's enough to generate a month's worth of invoices for most freelancers.

See also: our step-by-step CSV to PDF tutorial for more Jinja2 examples, or the full docs for a complete reference of filters and syntax.

— Jason