Template editor writing guide
A practical guide to customising your document templates in Phasio
You don't need to be a developer to customise your templates. This guide covers inserting data, showing or hiding fields, and building custom tables for parts and expenses.
š” The fastest way to get a custom template is to use the AI writing assistant at the bottom of this page.
Start from the default
Every template comes pre-filled with a working layout. Open the Source tab to edit the HTML directly, or use the Visual tab for click-to-edit formatting.
Custom Document, Group Traveller, and Order Traveller templates start with a fully annotated starter template ā it includes a page header, footer, logo, and a parts table with comments explaining each section. Edit it to match your layout and branding.
ā ļø Do not remove
xmlns:th="http://www.thymeleaf.org"from the<html>tag ā it enables all variable and conditional features. Standard HTML5 is supported; no strict XHTML syntax required.
Insert a variable
Click Variables in the toolbar to browse and insert any variable ā it is placed at your cursor automatically with the correct syntax.
To type a variable directly in the Source tab:
[(${VARIABLE_NAME})]Example:
<p>Order: [(${ORDER_NUMBER})]</p>
<p>Customer: [(${CUSTOMER_NAME})]</p>
<p>Total: [(${FINAL_PRICE})]</p>Variable reference
Your company
| Variable | What it inserts |
|---|---|
LOGO_IMG | Your company logo ā pre-rendered <img> tag on standard templates; raw base64 on custom templates (see Using the logo) |
OPERATOR_NAME | Your company name |
OPERATOR_EMAIL | Your company email |
OPERATOR_PHONE | Your company phone |
OPERATOR_LOCATION | Your company location |
GST_NUMBER | Your GST / VAT number |
PAYMENT_INFORMATION | Bank / payment details |
OPERATOR_NOTES | Notes added to the order |
Order
| Variable | What it inserts |
|---|---|
ORDER_NUMBER | Order number |
ORDER_DATE | Date the order was placed |
ESTIMATE_DATE | Date the quote was sent |
CONFIRMATION_DATE | Date the customer confirmed |
PAYMENT_DATE | Date payment was received |
TARGET_DELIVERY_DATE | Estimated delivery date |
PURCHASE_ORDER_NUMBER | Customer's PO number |
Customer
| Variable | What it inserts |
|---|---|
CUSTOMER_NAME | Customer organisation name |
CUSTOMER_GST | Customer's GST / tax number |
CUSTOMER_ID | Customer contact name |
CUSTOMER | Raw customer object ā access individual fields (see below) |
BILLING_ADDRESS | Pre-formatted billing address block |
BILLING_ADDRESS_DATA | Raw billing address ā access individual fields (see below) |
SHIPPING_ADDRESS | Pre-formatted shipping address block |
SHIPPING_ADDRESS_DATA | Raw shipping address ā access individual fields (see below) |
CUSTOMER fields: CUSTOMER.organisationName, CUSTOMER.firstName, CUSTOMER.lastName, CUSTOMER.email, CUSTOMER.phone
BILLING_ADDRESS_DATA / SHIPPING_ADDRESS_DATA fields: street1, street2, city, state, country, zip, name, company, email, phone
<!-- Use the pre-formatted block for a quick address block -->
<td th:utext="${BILLING_ADDRESS}"></td>
<!-- Or access individual fields for a custom layout -->
<td>
<div th:text="${BILLING_ADDRESS_DATA.name}"></div>
<div th:text="${BILLING_ADDRESS_DATA.street1}"></div>
<div th:text="${BILLING_ADDRESS_DATA.city + ', ' + BILLING_ADDRESS_DATA.country}"></div>
</td>Pricing
| Variable | What it inserts |
|---|---|
SUB_TOTAL | Parts total before extras |
SHIPPING_FEE | Shipping charge |
DISCOUNT | Discount amount |
DISCOUNT_PERCENTAGE | Discount percentage |
TAXES | Tax breakdown |
PRICE_BEFORE_TAX | Total before tax |
FINAL_PRICE | Total amount due |
TOP_UP_PRICE | Top-up to reach minimum order |
LINE_ITEM_NAMES | Pricing line item names |
LINE_ITEM_PRICES | Pricing line item amounts |
Parts and expenses
| Variable | What it inserts |
|---|---|
PARTS_TABLE | Built-in parts table (ready to drop in) |
PARTS | Parts list ā iterate to build your own table (see below) |
EXPENSES_TABLE | Built-in expenses table (ready to drop in) |
EXPENSES | Expenses list ā iterate to build your own table (see below) |
Production (Group Traveller only)
| Variable | What it inserts |
|---|---|
COMPANY_LOGO | Company logo ā raw base64 string (see Using the logo) |
GROUP_NAME | Production group name |
GROUP_DATE | Date the group was created |
MACHINE | Machine used |
MATERIAL | Material used |
MATERIAL_BATCH | Batch identifier |
DUE_DATE | Group due date |
EXPORT_DATE | Date the document was generated |
LINE_ITEMS | Parts list ā iterate to list parts in the group |
ALL_WORKFLOW_STEPS | List of all workflow step names |
LINE_ITEMS_WITH_MAPS | Parts paired with workflow step data ā use to build routing tables |
Orders (Order Traveller only)
| Variable | What it inserts |
|---|---|
COMPANY_LOGO | Company logo ā raw base64 string (see Using the logo) |
EXPORT_DATE | Date the document was generated |
ORDERS | List of orders ā iterate to list each order with its parts and workflow steps |
Using the logo
LOGO_IMG behaves differently depending on the template type:
Standard templates (Order Invoice, Order Estimate, Order Confirmation, Traveller Sheet, Consignment Label) ā LOGO_IMG is a pre-rendered <img> tag from the server. Output it directly:
<!-- In a table cell -->
<td th:utext="${LOGO_IMG}"></td>
<!-- Inline -->
[(${LOGO_IMG})]Custom templates (Custom Document, Group Traveller, Order Traveller) ā LOGO_IMG and COMPANY_LOGO are raw base64 strings. Use both th:src and th:attr for reliable rendering:
<img th:if="${LOGO_IMG != null and !#strings.isEmpty(LOGO_IMG)}"
th:src="${'data:image/png;base64,' + LOGO_IMG}"
th:attr="src=|data:image/png;base64,${LOGO_IMG}|"
style="max-height: 40px; width: auto;"
alt="" />Show a field only when it has a value
Use th:if to hide a block when the variable is empty. This is useful for optional fields like PO number or notes.
<p th:if="${PURCHASE_ORDER_NUMBER}">
PO: [(${PURCHASE_ORDER_NUMBER})]
</p>
<p th:if="${OPERATOR_NOTES}">
Notes: [(${OPERATOR_NOTES})]
</p>This works on any element ā <p>, <div>, <tr>, and so on.
Visual indicator: In the Visual tab, elements with th:if show a blue outline and label so you can see them at a glance without reading the source.
Build a custom parts table
Use PARTS to build your own layout. Each part has: name, technology, material, color, quantity, unitPrice, processingPrice, thumbnailImg (raw base64), postProcessings (a list with name and price), and physical dimensions: volume (mm³), height, width, length (mm), area (mm²). shrinkWrapVolume and minimumWallThickness are available on Pro and Factory Floor plans.
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr>
<th style="text-align: left; padding: 6px;">Part</th>
<th style="text-align: left; padding: 6px;">Material</th>
<th style="text-align: left; padding: 6px;">Colour</th>
<th style="text-align: right; padding: 6px;">Qty</th>
<th style="text-align: right; padding: 6px;">Total</th>
</tr>
</thead>
<tbody>
<tr th:each="part : ${PARTS}">
<td style="padding: 6px;">[(${part.name})]</td>
<td style="padding: 6px;">[(${part.technology})], [(${part.material})]</td>
<td style="padding: 6px;" th:text="${part.color}"></td>
<td style="padding: 6px;">[(${part.width})] Ć [(${part.height})] Ć [(${part.length})] mm</td>
<td style="text-align: right; padding: 6px;">[(${part.quantity})]</td>
<td style="text-align: right; padding: 6px;">[(${part.processingPrice})]</td>
</tr>
</tbody>
</table>To include a thumbnail image:
<img th:if="${part.thumbnailImg != null and !#strings.isEmpty(part.thumbnailImg)}"
th:src="${'data:image/png;base64,' + part.thumbnailImg}"
th:attr="src=|data:image/png;base64,${part.thumbnailImg}|"
style="max-width: 40px; max-height: 40px;" alt="" />Visual indicator: In the Visual tab, rows with th:each show an amber outline and label so loop blocks are easy to spot.
Build a custom expenses table
Use EXPENSES to build your own layout. Each expense has name, description, price, and type (FIXED or HOURLY). Hourly expenses also have ratePerHour and hours.
<table style="width: 100%; border-collapse: collapse;">
<tr th:each="expense : ${EXPENSES}">
<td style="padding: 6px;">
[(${expense.name})]
<div th:if="${expense.description != null and !#strings.isEmpty(expense.description)}"
style="font-size: 0.9em; color: #666;">
[(${expense.description})]
</div>
</td>
<td style="padding: 6px;" th:if="${expense.type.name() == 'HOURLY'}">
[(${expense.hours})] hrs Ć [(${expense.ratePerHour})]
</td>
<td style="text-align: right; padding: 6px;">[(${expense.price})]</td>
</tr>
</table>Group Traveller ā parts and workflow routing
Use LINE_ITEMS to list parts in the group. Each item has: partName, customerName, quantity, thumbnailImg, purchaseOrderNumber (the PO number of the order this part belongs to), and orderNumber.
Use LINE_ITEMS_WITH_MAPS and ALL_WORKFLOW_STEPS together to build a routing matrix ā rows are parts, columns are workflow steps:
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr>
<th style="text-align: left; padding: 6px;">Part</th>
<th th:each="step : ${ALL_WORKFLOW_STEPS}" style="padding: 6px; text-align: center;">
[(${step})]
</th>
</tr>
</thead>
<tbody>
<tr th:each="entry : ${LINE_ITEMS_WITH_MAPS}">
<td style="padding: 6px;">[(${entry['lineItem'].partName})]</td>
<td th:each="step : ${ALL_WORKFLOW_STEPS}" style="padding: 6px; text-align: center;">
<span th:if="${entry['stepMap'].containsKey(step) and entry['stepMap'].get(step).completed}">ā</span>
<span th:unless="${entry['stepMap'].containsKey(step) and entry['stepMap'].get(step).completed}">ā</span>
</td>
</tr>
</tbody>
</table>Order Traveller ā iterating orders
Use ORDERS to iterate over each order. Each order has: orderNumber, orderDate, customerName, customerGst, billingAddress, shippingAddress, purchaseOrderNumber, finalPrice, currency, allWorkflowSteps, and lineItemsWithMaps. Line items within each order also carry purchaseOrderNumber and orderNumber.
<div th:each="order : ${ORDERS}">
<h3>Order [(${order.orderNumber})] ā [(${order.customerName})]</h3>
<p>[(${order.orderDate})]</p>
<p th:if="${order.purchaseOrderNumber}">PO: [(${order.purchaseOrderNumber})]</p>
<p th:if="${order.finalPrice}">Total: [(${order.currency})] [(${order.finalPrice})]</p>
<!-- use order.lineItemsWithMaps and order.allWorkflowSteps the same way as Group Traveller above -->
</div>Repeating page header and footer
Add id="page-header" or id="page-footer" to repeat an element on every page:
<div id="page-header">
[(${OPERATOR_NAME})] | Order [(${ORDER_NUMBER})]
</div>
<div id="page-footer">
Page <span class="page-current"></span> of <span class="page-total"></span>
</div>Side-by-side columns
The PDF renderer (OpenPDF) does not support flexbox or CSS Grid. Use <table> for multi-column layouts:
<table style="width: 100%;">
<tr>
<td style="vertical-align: top;">
<img th:if="${LOGO_IMG != null and !#strings.isEmpty(LOGO_IMG)}"
th:src="${'data:image/png;base64,' + LOGO_IMG}"
style="max-height: 40px;" alt="" />
</td>
<td style="vertical-align: top; text-align: right;">
[(${OPERATOR_NAME})]<br/>
[(${OPERATOR_EMAIL})]
</td>
</tr>
</table>Fonts
| Font | font-family value |
|---|---|
| Poppins (default) | Poppins |
| Arimo | Arimo |
| Gentium Plus | Gentium |
| Source Serif Pro | SourceSerif |
ā ļø For templates in Russian, Ukrainian, Bulgarian, or Greek use
Arimoto ensure all characters render correctly.
Page sizes
Standard templates default to A4. Custom Document, Group Traveller, and Order Traveller templates let you set any size in millimetres when creating the template.
| Size | Width Ć Height |
|---|---|
| A4 (default) | 210 Ć 297 mm |
| A3 | 297 Ć 420 mm |
| A5 | 148 Ć 210 mm |
| US Letter | 215.9 Ć 279.4 mm |
Write templates with AI
Describe what you want and an AI assistant will write the HTML for you. Use the buttons below to open a new chat with everything it needs already loaded.
Example prompts:
- "Create an invoice with logo on the left and company details on the right."
- "Build a parts table showing thumbnail, name, dimensions, and quantity."
- "Add a PO number row that only shows when a PO number is set."
- "Build a Group Traveller with a routing matrix showing which workflow steps each part goes through."
AI writing assistant
Opens a new chat and copies the context to your clipboard ā paste it in to get started.
Last updated on