Book a call
Home  /  Insights  /  Templates

Advanced PDF templates: the FreeMarker patterns worth knowing

Six patterns that cover most of what clients ask for in transaction templates, including the page-break behaviour nobody documents.

A
Atsovia NetSuite team
12 August 2026 · 6 min read

Advanced PDF templates cover most transaction document requirements once you know a handful of patterns. The trouble is that the patterns are not really documented anywhere as patterns, they are scattered across forum answers, and the rendering engine has behaviour that surprises people who know HTML.

These are the six that come up in almost every template we build.

1. Conditional sections that collapse properly

Showing a block only when it has content is straightforward; the mistake is leaving the container behind when it is empty, so the document has a mysterious gap.

<#if record.shipaddress?has_content>
<table class="addr">
<tr><td>${record.shipaddress}</td></tr>
</table>
</#if>
Put the #if outside the table, not inside it. Wrapping only the row leaves an empty table that still takes up its margins.

Use ?has_content rather than testing for an empty string. It handles a missing field, a null and an empty value in one test, which matters because which of those three you get depends on the record type.

2. Grouping a sublist

Line items grouped by category, by location or by any other column is one of the most requested layouts and has no built-in support. The reliable approach is to sort the rows in a saved search, then detect the change in the template.

<#assign lastGroup = "">
<#list record.item as item>
<#if item.custcol_group != lastGroup>
<tr><td colspan="4" class="grouphdr">${item.custcol_group}</td></tr>
<#assign lastGroup = item.custcol_group>
</#if>
<tr><td>${item.item}</td><td>${item.quantity}</td></tr>
</#list>
The rows must already be in group order. The template detects the change; it cannot sort.

3. Running totals

Subtotals per group, or any total the record does not expose as a field, are accumulated the same way, declare before the loop, add inside it.

<#assign groupTotal = 0>
<#list record.item as item>
<#assign groupTotal = groupTotal + item.amount?number>
</#list>
<td>${groupTotal?string("#,##0.00")}</td>
The ?number coercion matters, amounts arrive as formatted strings and adding them without it concatenates.
Most template bugs are type bugs. The value looked like a number and was a string.

4. Page break control

This is the one that catches everybody, because the renderer behind Advanced PDF is not a browser and CSS page-break behaviour is only partly honoured. Two things are worth knowing.

First, a table that spans pages will not repeat its header unless you put the header rows inside <thead>. This is the single most common complaint about long invoices, and the fix is a tag most people leave out because it makes no visible difference on page one.

Second, keeping a block together across a break is best done by making it a table and setting the break behaviour on the table, rather than trying to control the rows. Fighting it at row level tends to produce a template that looks right on your test record and breaks on a longer one.

<table>
<thead>
<tr><th>Item</th><th>Qty</th><th>Amount</th></tr>
</thead>
<tbody>
<#list record.item as item>
<tr><td>${item.item}</td><td>${item.quantity}</td><td>${item.amount}</td></tr>
</#list>
</tbody>
</table>
thead is what makes the header repeat on page two. Always test with a record long enough to need one.

Always test with a transaction that runs to at least three pages. A template validated on a two-line order will ship with page-break bugs nobody sees until a customer receives a forty-line invoice.

5. Logos and branding per subsidiary

In a multi-subsidiary account, one template serving every entity is far easier to maintain than one template per entity. Drive the differences from the subsidiary record rather than duplicating the file.

<#if record.subsidiary?has_content>
<img src="${subsidiary.logo@url}" style="height: 44px;" />
</#if>
One template, branding driven by data. Six templates that differ only in a logo will drift apart within a year.

6. Dates, numbers and locale

Formatting is where templates leak inconsistency. A date rendered raw in one place and formatted in another looks careless on a document going to a customer.

Need
Pattern
Note
Date
${record.trandate}
Honours user preference
Explicit date
?string("dd MMM yyyy")
Fixed regardless of user
Currency
?string("#,##0.00")
Apply after ?number
Fallback
!": "
Prints a dash, not an error

The last row is worth adopting as a habit. An unhandled missing field can fail the render outright, and a template that throws on one record in five hundred is a support ticket that arrives months after the project closed and takes a day to reproduce.

Before you ship a template

Test four records, not one: the shortest possible, one long enough to span three pages, one with every optional field empty, and one in a second currency or subsidiary if the account has them. Nearly every template defect we have been called in to fix would have been caught by the second or third of those.

Try it on your own numbers

Implementation scope estimator

Six questions about your business, and an indicative phase plan with the arithmetic shown.

Open the calculator
A
Atsovia NetSuite team
Implementation and development

Years of building on NetSuite: SuiteScript, SuiteFlow, SuiteQL, SuiteTalk, Advanced PDF templates and Celigo integrations. We write these up when a problem comes round for the third time.

More about the team → Get in touch →

Keep reading

SuiteScript
Why your Map/Reduce keeps hitting SSS_USAGE_LIMIT_EXCEEDED
9 min read
Administration
Cutting NetSuite file cabinet costs without deleting anything
7 min read
SuiteQL
SuiteQL or saved search: how to decide, with the governance maths
11 min read
Next step

Book a free consultation

Tell us what you are running today and where it is breaking down. Most calls run 20 to 30 minutes and end with a clear next step. We reply within one business day.

Talk to a consultant