💼 Business problem
Multi-brand/multi-subsidiary accounts need one invoice template that:
- Shows the correct logo & brand colors per subsidiary or per customer brand.
- Displays regional tax/VAT info dynamically.
- Adds watermarks (e.g., “ON CREDIT HOLD” or “PAST DUE”).
- Keeps totals/tax neatly aligned and consistent.
🧠 Approach
- Use Advanced PDF/HTML with FreeMarker to pick the logo & palette at runtime.
- Read branding from the Subsidiary (preferred) or from Customer fields.
- Keep layout modular: Header, Bill-To/Ship-To, Lines, Tax Summary, Totals.
- Optional UE script to pre-compute flags (e.g.,
is_past_due
, brand code) if you prefer.
📋 Prerequisites (recommend)
- Subsidiary custom fields:
custrecord_brand_logo_url
(Text, store file URL or external URL)custrecord_brand_color_hex
(Text, e.g.,#0A3D62
)custrecord_vat_label
(Text, e.g.,VAT
,GST/HST
, etc.)
- (Optional) Customer override fields with same IDs (so customer branding can supersede).
- (Optional) Custom body fields on Invoice if you want to override per transaction.
Tip: If you don’t want custom fields, you can fall back to
${companyInformation.logoUrl}
for a single global logo.
🧱 Header: dynamic logo + brand color
<#-- Resolve brand context: Customer override > Subsidiary > Company default -->
<#assign sub = record.subsidiary />
<#assign customer = record.entity />
<#assign logoUrl =
(customer.custentity_brand_logo_url?has_content)?then(customer.custentity_brand_logo_url,
(sub.custrecord_brand_logo_url?has_content)?then(sub.custrecord_brand_logo_url,
companyInformation.logoUrl)) />
<#assign brandColor =
(customer.custentity_brand_color_hex?has_content)?then(customer.custentity_brand_color_hex,
(sub.custrecord_brand_color_hex?has_content)?then(sub.custrecord_brand_color_hex,
"#222")) />
<table style="width:100%; border-collapse:collapse;">
<tr>
<td style="width:50%; padding:8px 0;">
<#if logoUrl?has_content>
<img src="${logoUrl}" style="max-height:60px;"/>
</#if>
</td>
<td style="width:50%; text-align:right;">
<div style="font-size:22px; font-weight:700; color:${brandColor};">INVOICE</div>
<div style="font-size:12px;">${record.tranid}</div>
<div style="font-size:12px;">${record.trandate}</div>
</td>
</tr>
<tr>
<td colspan="2" style="height:3px; background:${brandColor};"></td>
</tr>
</table>
🧾 Bill/Ship + references (clean block)
<table style="width:100%; margin-top:10px;">
<tr>
<td style="vertical-align:top; width:50%;">
<div style="font-weight:700;">Bill To</div>
${record.billaddress?html}
</td>
<td style="vertical-align:top; width:50%;">
<div style="font-weight:700;">Ship To</div>
${record.shipaddress?html}
<#if record.otherrefnum?has_content>
<div style="margin-top:6px;"><b>Customer PO:</b> ${record.otherrefnum}</div>
</#if>
</td>
</tr>
</table>
📦 Line table with subtle zebra & alignment
<style>
.tbl { width:100%; border-collapse:collapse; margin-top:14px; }
.th { font-size:11px; text-transform:uppercase; letter-spacing:.4px; padding:8px 6px; border-bottom:1px solid #ddd; }
.td { font-size:12px; padding:7px 6px; border-bottom:1px solid #eee; }
.num { text-align:right; white-space:nowrap; }
.zebra:nth-child(even) td { background:#fafafa; }
</style>
<table class="tbl">
<tr>
<th class="th" style="text-align:left;">Item</th>
<th class="th" style="text-align:left;">Description</th>
<th class="th num">Qty</th>
<th class="th num">Rate</th>
<th class="th num">Amount</th>
</tr>
<#list record.item as line>
<tr class="zebra">
<td class="td">${line.item@label}</td>
<td class="td">${line.description}</td>
<td class="td num">${line.quantity?number}</td>
<td class="td num">
<#if line.rate?has_content && (line.rate?string != "")>${line.rate}<#else>0.00</#if>
</td>
<td class="td num">${line.amount}</td>
</tr>
</#list>
</table>
Note the Qty/Rate guards so empty values don’t crash (common FreeMarker pitfall).
🧮 Tax summary (left) + totals (right) on one row
This keeps the layout tight (you asked a lot for this in earlier work).
<table style="width:100%; margin-top:12px;">
<tr>
<!-- LEFT: Tax Summary -->
<td style="width:55%; vertical-align:top; padding-right:8px;">
<#assign vatLabel =
(customer.custentity_vat_label?has_content)?then(customer.custentity_vat_label,
(sub.custrecord_vat_label?has_content)?then(sub.custrecord_vat_label, "Tax")) />
<#if record.taxsummary?has_content>
<table style="width:100%; border-collapse:collapse;">
<tr>
<th style="text-align:left; padding:6px 4px;">${vatLabel} Type</th>
<th style="text-align:left; padding:6px 4px;">Code</th>
<th class="num" style="padding:6px 4px;">Basis</th>
<th class="num" style="padding:6px 4px;">Rate</th>
<th class="num" style="padding:6px 4px;">Amount</th>
</tr>
<#list record.taxsummary as t>
<tr>
<td style="padding:6px 4px;">${t.taxtype@label}</td>
<td style="padding:6px 4px;">${t.taxcode@label}</td>
<td class="num" style="padding:6px 4px;">${t.taxbasis}</td>
<td class="num" style="padding:6px 4px;">${t.taxrate}</td>
<td class="num" style="padding:6px 4px;">${t.taxamount}</td>
</tr>
</#list>
</table>
</#if>
</td>
<!-- RIGHT: Totals -->
<td style="width:45%; vertical-align:top;">
<table style="width:100%; border-collapse:collapse;">
<tr>
<td style="padding:6px 4px;">Subtotal</td>
<td class="num" style="padding:6px 4px;">${record.subtotal}</td>
</tr>
<#if record.taxtotal?has_content>
<tr>
<td style="padding:6px 4px;">${vatLabel} Total</td>
<td class="num" style="padding:6px 4px;">${record.taxtotal}</td>
</tr>
</#if>
<#if record.shippingcost?has_content && record.shippingcost?string != "">
<tr>
<td style="padding:6px 4px;">Shipping</td>
<td class="num" style="padding:6px 4px;">${record.shippingcost}</td>
</tr>
</#if>
<tr>
<td style="padding:8px 4px; font-weight:700; border-top:1px solid #ddd;">Total</td>
<td class="num" style="padding:8px 4px; font-weight:700; border-top:1px solid #ddd;">${record.total}</td>
</tr>
<#if record.amountremaining?has_content>
<tr>
<td style="padding:6px 4px;">Amount Due</td>
<td class="num" style="padding:6px 4px;">${record.amountremaining}</td>
</tr>
</#if>
</table>
</td>
</tr>
</table>
🚩 Watermarks: credit hold / past-due
Show a big translucent banner based on record state.
<#-- Compute flags -->
<#assign isCreditHold = (record.entitycredithold?string('','')?upper_case == 'ON') />
<#assign isPastDue = (record.amountremaining?number > 0 && record.duedate?has_content && (record.duedate?date < .now?date)) />
<style>
.wm {
position:fixed; top:40%; left:50%; transform:translate(-50%,-50%);
font-size:64px; color:#000; opacity:.06; letter-spacing:8px;
border:6px solid #000; padding:18px 28px; text-transform:uppercase;
}
</style>
<#if isCreditHold>
<div class="wm">CREDIT HOLD</div>
<#elseif isPastDue>
<div class="wm">PAST DUE</div>
</#if>
💳 Payment note / e-Transfer (common request)
<#if record.custbody_payment_note?has_content>
<div style="margin-top:12px; font-size:12px; line-height:1.4;">
<b>Payment:</b> ${record.custbody_payment_note}
</div>
<#else>
<div style="margin-top:12px; font-size:12px; line-height:1.4;">
<b>Payment:</b> E-transfer accepted at accounts@yourbrand.com (include invoice # in memo).
</div>
</#if>
🧰 Optional UE helper (beforeLoad)
If you prefer to pre-compute flags or inject derived fields that the template reads:
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
* Title: UE | Invoice PDF helpers
*/
define(['N/record','N/format'], (record, format) => {
const beforeLoad = (ctx) => {
if (ctx.type !== ctx.UserEventType.VIEW && ctx.type !== ctx.UserEventType.PRINT) return;
const inv = ctx.newRecord;
// Example derived boolean: custbody_is_past_due
const due = inv.getValue('duedate');
const remaining = Number(inv.getValue('amountremaining') || 0);
let pastDue = false;
if (due && remaining > 0) {
const today = new Date();
const dueDate = format.parse({ value: due, type: format.Type.DATE });
pastDue = (dueDate < today);
}
inv.setValue({ fieldId: 'custbody_is_past_due', value: pastDue });
};
return { beforeLoad };
});
Then your FreeMarker can check ${record.custbody_is_past_due}
directly.
✅ Testing checklist
- Switch between two subsidiaries and confirm logo/color swap.
- Customer with override fields → their branding wins.
- Invoice with tax lines → tax table shows; non-taxed invoice → block hides gracefully.
- Aging test: set past due; verify PAST DUE watermark.
- Credit-hold customer → CREDIT HOLD watermark appears.
- Try missing qty/rate values to ensure no template crash.
🧪 Troubleshooting
- Logo not showing: confirm the URL is accessible to the template (File Cabinet files with
Available Without Login
or a hosted CDN URL), and variablelogoUrl
resolves. - Weird numbers: guard with
?has_content
and?number
where appropriate; avoid math on empty strings. - Totals alignment off: keep totals in a separate right-side cell (as above) and avoid nested tables with variable column counts.
- RTL languages: wrap address blocks in a container and set
direction:rtl; text-align:right;
conditionally.
🧠 Takeaways
- Centralize branding on the Subsidiary record; allow Customer overrides for brand-specific clients.
- Use FreeMarker guards to prevent null/empty crashes.
- Keep tax and totals in a two-column layout for consistent alignment.
- Add visual status (watermarks) to reduce AR back-and-forth.
Leave a Reply