SettingsOrganisation

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

VariableWhat it inserts
LOGO_IMGYour company logo — pre-rendered <img> tag on standard templates; raw base64 on custom templates (see Using the logo)
OPERATOR_NAMEYour company name
OPERATOR_EMAILYour company email
OPERATOR_PHONEYour company phone
OPERATOR_LOCATIONYour company location
GST_NUMBERYour GST / VAT number
PAYMENT_INFORMATIONBank / payment details
OPERATOR_NOTESNotes added to the order

Order

VariableWhat it inserts
ORDER_NUMBEROrder number
ORDER_DATEDate the order was placed
ESTIMATE_DATEDate the quote was sent
CONFIRMATION_DATEDate the customer confirmed
PAYMENT_DATEDate payment was received
TARGET_DELIVERY_DATEEstimated delivery date
PURCHASE_ORDER_NUMBERCustomer's PO number

Customer

VariableWhat it inserts
CUSTOMER_NAMECustomer organisation name
CUSTOMER_GSTCustomer's GST / tax number
CUSTOMER_IDCustomer contact name
CUSTOMERRaw customer object — access individual fields (see below)
BILLING_ADDRESSPre-formatted billing address block
BILLING_ADDRESS_DATARaw billing address — access individual fields (see below)
SHIPPING_ADDRESSPre-formatted shipping address block
SHIPPING_ADDRESS_DATARaw 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

VariableWhat it inserts
SUB_TOTALParts total before extras
SHIPPING_FEEShipping charge
DISCOUNTDiscount amount
DISCOUNT_PERCENTAGEDiscount percentage
TAXESTax breakdown
PRICE_BEFORE_TAXTotal before tax
FINAL_PRICETotal amount due
TOP_UP_PRICETop-up to reach minimum order
LINE_ITEM_NAMESPricing line item names
LINE_ITEM_PRICESPricing line item amounts

Parts and expenses

VariableWhat it inserts
PARTS_TABLEBuilt-in parts table (ready to drop in)
PARTSParts list — iterate to build your own table (see below)
EXPENSES_TABLEBuilt-in expenses table (ready to drop in)
EXPENSESExpenses list — iterate to build your own table (see below)

Production (Group Traveller only)

VariableWhat it inserts
COMPANY_LOGOCompany logo — raw base64 string (see Using the logo)
GROUP_NAMEProduction group name
GROUP_DATEDate the group was created
MACHINEMachine used
MATERIALMaterial used
MATERIAL_BATCHBatch identifier
DUE_DATEGroup due date
EXPORT_DATEDate the document was generated
LINE_ITEMSParts list — iterate to list parts in the group
ALL_WORKFLOW_STEPSList of all workflow step names
LINE_ITEMS_WITH_MAPSParts paired with workflow step data — use to build routing tables

Orders (Order Traveller only)

VariableWhat it inserts
COMPANY_LOGOCompany logo — raw base64 string (see Using the logo)
EXPORT_DATEDate the document was generated
ORDERSList of orders — iterate to list each order with its parts and workflow steps

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>

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

Fontfont-family value
Poppins (default)Poppins
ArimoArimo
Gentium PlusGentium
Source Serif ProSourceSerif

āš ļø For templates in Russian, Ukrainian, Bulgarian, or Greek use Arimo to 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.

SizeWidth Ɨ Height
A4 (default)210 Ɨ 297 mm
A3297 Ɨ 420 mm
A5148 Ɨ 210 mm
US Letter215.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