# Document Templating

## Getting Started

Realpad CRM can generate documents (contracts, invoices, protocols, etc.) from Word templates.\
You create a `.docx` file with special placeholder markers, upload it to the system, and the CRM fills in the real data automatically.

The system uses [XDocReport](https://github.com/opensagres/xdocreport) with the [Apache Velocity](https://velocity.apache.org/engine/2.4.1/user-guide.html) template engine under the hood. You don't need to understand these libraries in depth — this guide covers everything you need to create templates.

***

## Document types & their placeholders documentation

* [**Deal documents**](/placeholders/document-templating/deal-documents.md): reservation, purchase, ... contracts and everything that needs to be created from the context of a single Deal. These placeholders are also available in other document types related to / extending the Deal context.
* [**Invoices**](/placeholders/document-templating/invoices.md): documents created from a single Payment on a Deal.
* [List of inspection defects](/placeholders/document-templating/list-of-inspection-defects.md), [Reclamation documents](/placeholders/document-templating/reclamation-documents.md), [Defect list report](/placeholders/document-templating/defects-list-report.md).

***

## Step-by-Step: Creating a Template

{% stepper %}
{% step %}

### Open Microsoft Word

Create or open a `.docx` file. The template is a normal Word document — you can use all standard formatting (bold, tables, headers, page breaks, etc.).
{% endstep %}

{% step %}

### Insert Placeholders Using Merge Fields (Recommended)

The recommended way to insert placeholders is through Word **Merge Fields**. This prevents a common issue where Word silently splits your placeholder text across internal formatting runs, breaking it (see Common Pitfalls for details).

How to insert a Merge Field:

1. Place your cursor where you want the placeholder.
2. Press `Ctrl+F9` (Windows) or `Cmd+F9` (Mac) to insert a field — you will see gray braces `{ }`.
3. Inside the field braces, type: `MERGEFIELD $placeholderName` (for example: `MERGEFIELD $dealId`), or `MERGEFIELD $placeholderName \* MERGEFORMAT`.
4. Press `Alt+F9` (or `Shift+F9` on Mac) to update the field display — it will now show the placeholder name as a preview.

{% hint style="warning" %}
Important: Do NOT type the braces `{ }` manually — they must be inserted by Word using `Ctrl+F9` / `Cmd+F9`. Manually typed curly braces look similar but are not the same thing and will not work.
{% endhint %}
{% endstep %}

{% step %}

### Placeholders Typed Directly (forbidden)

While it is in theory possible to insert placeholders directly into the document, this breaks our ability to inspect them, and thus is forbidden.
{% endstep %}

{% step %}

### Upload and Test

1. Upload the template to the CRM (in the Document Templates section)
2. Generate a test document for a real Deal to verify all placeholders are filled in correctly
3. Adjust formatting and placeholders as needed
   {% endstep %}
   {% endstepper %}

***

## Placeholder Syntax

### Simple Placeholders

A simple placeholder starts with `$` followed by the name:

```
Deal status: $status
Created on: $createdOn
```

### Accessing Properties of Objects

Inside a loop, each item is an object with multiple properties. Use a dot `.` to access them:

```
#foreach($customer in $customers)
  Customer name: $customer.fullName
  Customer email: $customer.email
#end
```

This also works for other objects — for example, units inside a `#foreach($unit in $units)` loop:

```
Unit area: $unit.area
Unit floor: $unit.floorNo
```

### Loops (Repeating Content)

To repeat a section for each item in a list, use `#foreach` ... `#end`:

```
#foreach($unit in $units)
  Unit: $unit.internalId — $unit.type, Floor: $unit.floorNo
  Area: $unit.area m², Price: $unit.finalPriceVat
#end
```

This generates one block of text for each Unit on the Deal. Everything between `#foreach` and `#end` is repeated.

Loops in tables: A common pattern is to put the `#foreach` and `#end` in the first column of a table row. The entire row gets repeated for each item:

| #                                            | Unit               | Type         | Price                        |
| -------------------------------------------- | ------------------ | ------------ | ---------------------------- |
| `#foreach($unit in $units)` `$foreach.count` | `$unit.internalId` | `$unit.type` | `$unit.finalPriceVat` `#end` |

Note: `$foreach.count` is a built-in Velocity variable that gives you the current iteration number (1, 2, 3, ...). You can also use `$foreach.index` for zero-based numbering (0, 1, 2, ...) and `$foreach.hasNext` to check if there are more items.

### Conditional Content

To show content only when a condition is true, use `#if` ... `#end`:

```
#if($isMortgage)
  Mortgage bank: $mortgageBankName
  Mortgage amount: $mortgageAmount
#end
```

You can also use `#else` and `#elseif`:

```
#if($isOneCustomerOnly)
  Sole buyer: $customer1Name
#else
  Multiple buyers — see the list below.
#end
```

Checking if a value exists: Many placeholders come with a boolean companion (prefixed with `is`). Use it to check before displaying:

```
#if($isCurrentUserEmail)
  Contact: $currentUserEmail
#end
```

You can also use the `$Utils.isSet()` helper to check any value:

```
#if($Utils.isSet($mortgageAmount))
  Mortgage: $mortgageAmount
#end
```

Comparing values: You can compare numbers and strings inside `#if`:

```
#if($dealPriceVAT.getAsNumber() > 1000000)
  High-value Deal
#end

#if($status == "Active")
  This Deal is active.
#end
```

Note: To compare numbers, use `.getAsNumber()` first — otherwise you'd be comparing the formatted text, not the numeric value.

### Variables

You can define temporary variables with `#set` to store computed values or simplify repeated expressions:

```
#set($totalArea = $Utils.sum($unit.area, $unit.areaBalcony, $unit.areaTerrace))
Total area: $totalArea m²
```

### Nested Loops

Loops can be nested. For example, to list defects for each room during an inspection:

```
#foreach($room in $inspection_rooms)
Room: $room.name
  #foreach($defect in $room.defects)
  - $defect.description
  #end
#end
```

### Velocity Reference

The template engine is [Apache Velocity](https://velocity.apache.org/engine/2.4.1/user-guide.html). The most commonly used features are covered in this guide, but for the full syntax reference, see the [Velocity User Guide](https://velocity.apache.org/engine/2.4.1/user-guide.html). The key directives are `#if`/`#elseif`/`#else`/`#end`, `#foreach`/`#end`, and `#set`.

***

## Data Types and Methods

Every placeholder has a data type that determines how it is displayed and what methods you can call on it.

### String

Plain text values. Rendered as-is in the document.

```
$customer1Name          →  "Jan Novák"
$status                 →  "Active"
```

Cyrillic Transliteration (RS developers only): For Serbian (RS) developers, String placeholders support `.toCyrillic()`. This is typically used inside a `#foreach` loop:

```
$customer.fullName              →  "Марко Марковић"  (if already Cyrillic)
$customer.fullName.toCyrillic() →  "Марко Марковић"  (converts Latin to Cyrillic)
```

Important: `.toCyrillic()` is only available for RS developers. For all other developers, String values are plain text and calling `.toCyrillic()` will output the method call literally. Templates that use `.toCyrillic()` should only be used by RS developers.

### DateWrapper

Formatted dates that respect the user's locale settings.

```
$createdOn              →  "23.12.2025"  (default format)
```

Available methods:

| Method               | Description                                                                                                                    | Example                                                    |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- |
| `.format("pattern")` | Custom date format using [Joda-Time patterns](https://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html) | `$createdOn.format("dd. MMMM yyyy")` → "23. December 2025" |
| `.addDays(n)`        | Add days to the date                                                                                                           | `$createdOn.addDays(14)` → date + 14 days                  |
| `.addWeeks(n)`       | Add weeks to the date                                                                                                          | `$createdOn.addWeeks(2)` → date + 2 weeks                  |
| `.addMonths(n)`      | Add months to the date                                                                                                         | `$createdOn.addMonths(3)` → date + 3 months                |
| `.isSet()`           | Check if date has a value                                                                                                      | `#if($createdOn.isSet()) ... #end`                         |

Common date format patterns:

| Pattern              | Result                     |
| -------------------- | -------------------------- |
| `dd.MM.yyyy`         | 23.12.2025                 |
| `d. MMMM yyyy`       | 23. December 2025          |
| `dd/MM/yyyy`         | 23/12/2025                 |
| `yyyy-MM-dd`         | 2025-12-23                 |
| `EEEE, d. MMMM yyyy` | Tuesday, 23. December 2025 |

Note: The month and day names (e.g. "December", "Tuesday") are automatically localized to the user's language.

### NumberWrapper

Formatted numbers that respect the user's locale (decimal separator, thousands grouping, currency symbol).

```
$dealPriceVAT           →  "1 250 000,00 CZK"  (default formatting)
$unit.area              →  "72,50"               (area formatting)
```

Available methods:

| Method           | Description                           | Example                                         |
| ---------------- | ------------------------------------- | ----------------------------------------------- |
| `.add(number)`   | Add to the value                      | `$dealPriceVAT.add($otherPrice)`                |
| `.sub(number)`   | Subtract from the value               | `$dealPriceVAT.sub($discount)`                  |
| `.mul(number)`   | Multiply the value                    | `$unit.area.mul(2)`                             |
| `.div(number)`   | Divide the value                      | `$dealPriceVAT.div(2)`                          |
| `.round()`       | Round to whole number                 | `$dealPriceVAT.round()`                         |
| `.roundFloor()`  | Round down to whole number            | `$dealPriceVAT.roundFloor()`                    |
| `.toInt()`       | Convert to integer (truncate)         | `$unit.area.toInt()`                            |
| `.strip()`       | Remove trailing zeros from decimals   | `$dealPriceVAT.strip()` → "1 250 000 CZK"       |
| `.keep()`        | Always show decimal places            | `$dealPriceVAT.keep()` → "1 250 000,00 CZK"     |
| `.dash()`        | Show dashes instead of trailing zeros | `$dealPriceVAT.dash()` → "1 250 000,— CZK"      |
| `.cz()`          | Number written in Czech words         | `$dealPriceVAT.cz()`                            |
| `.sk()`          | Number written in Slovak words        | `$dealPriceVAT.sk()`                            |
| `.isSet()`       | Check if number has a value           | `#if($dealPriceVAT.isSet()) ... #end`           |
| `.getAsNumber()` | Raw number for comparisons            | `#if($dealPriceVAT.getAsNumber() > 0) ... #end` |

Tip: Many number placeholders also come with pre-computed `_cz` and `_sk` word variants as separate placeholders (e.g. `$dealPriceVAT_cz`). These are equivalent to calling `.cz()` on the base placeholder.

Arithmetic: Methods accept both other placeholders and plain numbers, and can be chained:

```
Subtotal: $dealPriceVAT.sub($dealDiscountVat).round()
Double area: $unit.area.mul(2)
Custom calculation: $dealPriceVAT.mul(0.1).round().dash()
```

### Boolean

True/false values, used in `#if` conditions:

```
#if($isMortgage)
  This Deal uses a mortgage.
#end

#if($isOneCustomerOnly)
  Single buyer.
#else
  Multiple buyers.
#end
```

### $Utils Helper Object

A utility object (`DealDocumentUtils`) available in all document templates — Deal Documents, Invoices, Reclamation Protocols, and others.

| Method                    | Description                                          | Example                                     |
| ------------------------- | ---------------------------------------------------- | ------------------------------------------- |
| `$Utils.isSet(value)`     | Check if any value is set (works with all types)     | `#if($Utils.isSet($mortgageAmount))`        |
| `$Utils.sum(n1, n2, ...)` | Sum multiple NumberWrapper values                    | `$Utils.sum($unit.area, $unit.areaBalcony)` |
| `$Utils.wrap(number)`     | Create a NumberWrapper from a raw `long` or `double` | `$Utils.wrap(100)`                          |

`$Utils.isSet()` returns `false` for null values, empty collections, numbers/dates that have no value, and missing images. It is the most reliable way to check whether a placeholder has data.

**Usage example:**

```velocity

## Sum areas and conditionally display
#set($totalArea = $Utils.sum($unit.area, $unit.areaBalcony, $unit.areaTerrace))
Total area: $totalArea m²

## Check before displaying optional values
#if($Utils.isSet($mortgageAmount))
  Mortgage: $mortgageAmount
#end

## Wrap a raw number for arithmetic
#set($deposit = $dealPriceVAT.mul(0.1))
#set($remaining = $dealPriceVAT.sub($deposit))
```

### Method Chaining

Both DateWrapper and NumberWrapper methods return new wrapper objects, so you can chain multiple operations:

```
$createdOn.addDays(14).format("dd.MM.yyyy")
$dealPriceVAT.sub($dealDiscountVat).round().dash()
$Utils.sum($unit.area, $unit.areaBalcony).strip()
```

***

## Embedding Images

Some placeholders provide images (for example, Unit floor plans or Inspection defect photos). These work similarly to text placeholders but require a different insertion method in the Word template — you use **bookmarks** instead of Merge Fields or typed text.

### How to add an image placeholder

1. Insert a placeholder image into your Word document (any small image will do — it serves as a sizing reference)
2. Select the image
3. Go to **Insert → Bookmark**
4. In the Bookmark name field, type the image placeholder name — but replace dots with underscores (for example, for the placeholder `$unit.plan`, type `unit_plan`)
5. Click **Add** to create the bookmark
6. Resize the placeholder image to the approximate dimensions you want the real image to have in the generated document — the generated image will match this size

When the document is generated, the bookmarked placeholder image is replaced with the actual image from the CRM. If no image is available for a given placeholder, the placeholder image is removed from the output (it will not appear as a broken image).

Important: Bookmark names cannot contain dots (`.`) or dollar signs (`$`). Always replace dots with underscores: `$unit.plan` → bookmark name `unit_plan`, `$defect.image` → bookmark name `defect_image`.

### Image placeholders in loops

Images work inside `#foreach` loops too. For example, to show a photo for each inspection defect, you would insert a placeholder image inside the loop body and bookmark it with the image field name (using underscores). The image is then replaced for each iteration of the loop.

### Available image placeholders

Image placeholders are listed in the placeholder reference for each document type. Look for placeholders marked as **Image** type. Common examples include floor plan images on Units and defect attachment photos on Inspection documents.

***

## Common Pitfalls

A best practice to investigate a problematic template, especially if it is based on a working one, is to split it approximately in half, remove one half and:

* If the smaller part works, the problem was in the other part.
* If the smaller part doesn't work, the problem is in it.

In both cases, split down the problematic part further, until you discover the problematic placeholder or control structure.

<details>

<summary><strong>1. Broken Placeholders (Word Formatting Runs)</strong></summary>

The most common issue. Word internally splits text into "runs" based on formatting. If you type `$dealId` and then go back to fix a typo or apply formatting, Word may store it internally as `$deal` + `Id` in separate runs. The template engine then cannot recognize the placeholder.

How to fix:

* Select the entire placeholder text, cut it, and paste it back using **Paste as Plain Text** (`Ctrl+Shift+V`)
* Or use Merge Fields instead (see Step 2 — Insert Placeholders Using Merge Fields)
* Or type the placeholder in one go without going back to edit it

How to diagnose: If a placeholder appears literally in the generated document (e.g., you see `$dealId` instead of a number), this is almost certainly the cause.

</details>

<details>

<summary><strong>2. Missing `$` Sign</strong></summary>

Every placeholder must start with `$`. If you write `dealId` instead of `$dealId`, it will appear as literal text.

</details>

<details>

<summary><strong>3. Unclosed `#foreach` or `#if` Blocks</strong></summary>

Every `#foreach` needs a matching `#end`, and every `#if` needs a matching `#end`. A missing `#end` will cause the template generation to fail.

Example:

WRONG:

```
#foreach($unit in $units)
  $unit.internalId
(missing #end!)
```

CORRECT:

```
#foreach($unit in $units)
  $unit.internalId
#end
```

</details>

<details>

<summary><strong>4. Using a Placeholder That Doesn't Exist</strong></summary>

If you use a placeholder name that the system doesn't recognize, it will appear literally in the generated document (e.g., `$nonExistentField`). Double-check the name against the placeholder reference for your document type.

</details>

<details>

<summary><strong>5. Placeholder from Wrong Document Type</strong></summary>

Each document type has its own set of available placeholders. For example, Invoice-specific placeholders like `$paymentType` are not available in Reclamation documents. Check which document type you are creating the template for.

</details>

<details>

<summary><strong>6. Loops Inside Tables</strong></summary>

When using `#foreach` in a table, the `#foreach` and `#end` tags must be in the **same table row**. Placing `#foreach` in one row and `#end` in another will cause unexpected results.

</details>

<details>

<summary><strong>7. Whitespace and Newlines</strong></summary>

`#foreach`, `#if`, and `#end` directives take up a line in the output. If you don't want extra blank lines, place them on the same line as adjacent content, or put them at the start/end of a paragraph.

</details>

***

## Quick Example

Here is a minimal Deal Document template:

```
                        RESERVATION CONTRACT

Date: $now.format("d. MMMM yyyy")

Buyer:    $customer1Name

Units:
#foreach($unit in $units)
  $unit.internalId, Building $unit.building, Floor $unit.floorNo, Area: $unit.area m²
#end

Purchase Price: $dealPriceVAT ($dealPriceVAT_cz in words)

#if($isMortgage)
The purchase will be partially financed by a mortgage from $mortgageBankName
in the amount of $mortgageTotal.
#end

Payment Schedule:
#foreach($payment in $paymentsPrescribed)
  $foreach.count. $payment.type — $payment.amountVat — due: $payment.deadline
#end

Signed by: $currentUser
Date: $nowDMYdot
```

A more complete example using the `$customers` list:

```
Buyers:
#foreach($customer in $customers)
  $foreach.count. $customer.fullName, born $customer.birthDate
     Address: $customer.permanentAddress
#end
```

***


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://dev.realpadsoftware.com/placeholders/document-templating.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
