Introduction and objectives #
This guide shows you how to enhance and automate your customer communications using Jinja and Frappe. Whether for invoices, reminders, or confirmations, discover how to create intelligent, multilingual, and customized emails tailored to each recipient.
What is Jinja? #
Jinja is a templating engine for Python, widely used to generate dynamic content (HTML, emails, configuration files, etc.). It allows separation between presentation logic (what is displayed) and business logic (Python code). Jinja is used with the Frappe framework to create dynamic pages or emails.
Official website: https://jinja.palletsprojects.com/en/stable/templates
Why use Jinja? #
- Separation of concerns: Python code and presentation are separated.
- Reusability: templates can be included or extended.
- Flexibility: conditional logic, loops, filters, etc.
- Easy integration with Frappe.
Basic functionality: #
- Syntax: Jinja uses tags to insert variables, loops, conditions, etc.
- Variables:
{{ variable }} - Instructions:
{% instruction %} - Comments:
{# comment #}
Most commonly used functions and tags: #
| Function/Tag | Example |
|---|---|
| Display variables | {{ variable }} |
| Retrieve a value from a DocType | {{ master.field_name }} |
| Conditional structures | {% if condition %} Content if condition true {% elif other_condition %} Content if other_condition true {% else %} Content otherwise {% endif %} |
| Loops | {% for item in list %} {{ item }} {% endfor %} |
| Variable definition | {% set my_variable = “value” %} |
| Filters | {{ variable | filter }} |
| Include templates | {% include “other_template.html” %} |
| Template inheritance | {% extends “base.html” %} |
| Macros | {% macro my_macro(arg1, arg2) %} {{ arg1 }} - {{ arg2 }} {% endmacro %} |
Customer case: sending invoices by Email #
Initial issue: Need to send invoices directly by email with text personalization depending on the customer’s language.
Proposed solution: Silicon ioi allows generating and sending emails directly from the interface, with preview, automatic attachments, automatic translation, and customization of the email subject and body.
For email customization, use the Email Template.
Email Template Interface #

You must check the **Use HTML** box if using HTML tags in the template.
Example of Email subject: #
{{ _("Invoice", master.language) }} Silicon ioi - {{ master.name }} {{ frappe.utils.format_date(frappe.utils.nowdate(), "dd-MM-yyyy") }}
Example of Email body (multilingual): #
{%- set doc = frappe.get_doc('ioi Sales Invoice', master.name) -%}
{%- set customerContact = frappe.get_doc('Contact', doc.invoice_customer_contact_id) if doc.invoice_customer_contact_id else None -%}
{# Automatic translation based on language #}
{% if doc.language == 'fr' %}
{% if customerContact and customerContact.gender == 'Male' %}
{% set salutation = 'Dear' %}
{% elif customerContact and customerContact.gender == 'Female' %}
{% set salutation = 'Dear' %}
{% else %}
{% set salutation = 'Dear' %}
{% endif %}
{{ salutation }} {% if customerContact.last_name %}{{ customerContact.last_name }}{% else %}Customer{% endif %},
Thank you for your trust.
Please find attached invoice <strong>{{ doc.prefix_id }}{{ doc.identification }}</strong> for an amount of <strong>{{ "%.2f"|format(doc.total_tvac)|replace('.', ',') }}{{ doc.currency_id }}</strong>.
Due date: <strong>{{ doc.due_date.strftime('%d-%m-%Y') }}</strong>.
Best regards, Your <span style="color: #5A14E6; font-weight: bold;">Silicon ioi</span> team
{%- elif doc.language == 'nl' -%}
{# Dutch version #}
{% if customerContact and customerContact.gender == 'Male' %}
{% set salutation = 'Geachte heer' %}
{% elif customerContact and customerContact.gender == 'Female' %}
{% set salutation = 'Geachte mevrouw' %}
{% else %}
{% set salutation = 'Beste' %}
{% endif %}
{{ salutation }} {% if customerContact.last_name %}{{ customerContact.last_name }}{% else %}klant{% endif %},
Hartelijk dank voor uw vertrouwen.
Bijgevoegd vindt u factuur <strong>{{ doc.prefix_id }}{{ doc.identification }}</strong> met een bedrag van <strong>{{ "%.2f"|format(doc.total_tvac)|replace('.', ',') }}{{ doc.currency_id }}</strong>.
Vervaldatum: <strong>{{ doc.due_date.strftime('%d-%m-%Y') }}</strong>.
Met vriendelijke groet, Uw <span style="color: #5A14E6; font-weight: bold;">Silicon ioi</span>-team
Structure of a Jinja template for Emails #
| Element | JINJA Syntax | Example |
|---|---|---|
| Variable | {{ variable }} | {{ client.name }} |
| Condition | {% if condition %}…{% endif %} | {% if client.status == “premium” %}…{% endif %} |
| Loop | {% for item in list %}…{% endfor %} | {% for product in products %}{{ product.name }}{% endfor %} |
| Variable definition | {% set var = value %} | {% set salutation = “Dear” %} |
| Comment | {# comment #} | {# This text does not appear in the output #} |
| Translation | {{ _(“text”, lang) }} | {{ _(“Invoice”, master.language) }} |
| Number formatting | {{ “%.2f”|format(number) }} | {{ “%.2f”|format(1234.5678)|replace(‘.’, ‘,’) }} |
| Date formatting | {{ date.strftime(‘%d-%m-%Y’) }} | {{ frappe.utils.nowdate()|strftime(‘%d-%m-%Y’) }} |
Tutorial: Jinja/Frappe template for invoice emails #
Introduction #
This tutorial explains how to use Jinja and Frappe to dynamically generate multilingual invoice emails. The template adapts to the customer’s language and inserts dynamic data such as the invoice number, amount, due date, etc.
Template structure #
Subject line #
{{ _("Invoice", master.language) }} Silicon ioi - {{ master.name }} {{ frappe.utils.format_date(frappe.utils.nowdate(), "dd-MM-yyyy") }}
- {{ _("Invoice", master.language) }} : Translates "Invoice" according to the document language.
- {{ master.name }} : Invoice number.
- {{ frappe.utils.format_date(...) }} : Today's date formatted.
Example output :
Invoice Silicon ioi - FCL2026020018 04-03-2026
Email body #
The body of the template is divided into sections depending on the language (doc.language). Below is the common structure and the specific sections for each language.
Common structure #
{%- set doc = frappe.get_doc('ioi Sales Invoice', master.name) -%}
{%- set customerContact = frappe.get_doc('Contact', doc.invoice_customer_contact_id) if doc.invoice_customer_contact_id else None -%}
Sections by language
The template checks the value of doc.language and displays the corresponding section.
Example for French (fr)
{% if doc.language == 'fr' %}
{% if customerContact and customerContact.gender == 'Male' %}
{% set salutation = 'Dear' %}
{% elif customerContact and customerContact.gender == 'Female' %}
{% set salutation = 'Dear' %}
{% else %}
{% set salutation = 'Dear' %}
{% endif %}
{{ salutation }} {% if customerContact.last_name %}{{ customerContact.last_name }}{% else %}Customer{% endif %},
Thank you for your trust.
Invoice: <strong>{{ doc.prefix_id }}{{ doc.identification }}</strong> ({{ "%.2f"|format(doc.total_tvac)|replace('.', ',') }}{{ doc.currency_id }}).
Due date: <strong>{{ doc.due_date.strftime('%d-%m-%Y') }}</strong>.
Best regards, Your <span style="color: #5A14E6; font-weight: bold;">Silicon ioi</span> team
{% endif %}
Example for Dutch (nl)
{%- elif doc.language == 'nl' -%}
{# Dutch version #}
{% if customerContact and customerContact.gender == 'Male' %}
{% set salutation = 'Geachte heer' %}
{% else %}
{% set salutation = 'Beste' %}
{% endif %}
{{ salutation }} {% if customerContact.last_name %}{{ customerContact.last_name }}{% else %}klant{% endif %},
Hartelijk dank voor uw vertrouwen.
Factuur : <strong>{{ doc.prefix_id }}{{ doc.identification }}</strong> ({{ "%.2f"|format(doc.total_tvac)|replace('.', ',') }}{{ doc.currency_id }}).
Vervaldatum : <strong>{{ doc.due_date.strftime('%d-%m-%Y') }}</strong>.
Met vriendelijke groet, Uw <span style="color: #5A14E6; font-weight: bold;">Silicon ioi</span>-team
Example for English (en)
{%- elif doc.language == 'en' -%}
{# English version #}
{% if customerContact and customerContact.gender == 'Male' %}
{% set salutation = 'Dear Mr.' %}
{% elif customerContact and customerContact.gender == 'Female' %}
{% set salutation = 'Dear Ms.' %}
{% else %}
{% set salutation = 'Dear' %}
{% endif %}
{{ salutation }} {% if customerContact.last_name %}{{ customerContact.last_name }}{% else %}Customer{% endif %},
Thank you for your trust.
Invoice: <strong>{{ doc.prefix_id }}{{ doc.identification }}</strong> ({{ "%.2f"|format(doc.total_tvac)|replace('.', ',') }}{{ doc.currency_id }}).
Due date: <strong>{{ doc.due_date.strftime('%d-%m-%Y') }}</strong>.
Best regards, Your <span style="color: #5A14E6; font-weight: bold;">Silicon ioi</span> team
Example for German (de)
{%- elif doc.language == 'de' -%}
{# German version #}
{% if customerContact and customerContact.gender == 'Male' %}
{% set salutation = 'Sehr geehrter Herr' %}
{% elif customerContact and customerContact.gender == 'Female' %}
{% set salutation = 'Sehr geehrte Frau' %}
{% else %}
{% set salutation = 'Sehr geehrte(r)' %}
{% endif %}
{{ salutation }} {% if customerContact.last_name %}{{ customerContact.last_name }}{% else %}Kunde{% endif %},
Vielen Dank für Ihr Vertrauen.
Rechnung: <strong>{{ doc.prefix_id }}{{ doc.identification }}</strong> ({{ "%.2f"|format(doc.total_tvac)|replace('.', ',') }}{{ doc.currency_id }}).
Fälligkeitsdatum: <strong>{{ doc.due_date.strftime('%d-%m-%Y') }}</strong>.
Mit freundlichen Grüßen, Ihr <span style="color: #5A14E6; font-weight: bold;">Silicon ioi</span>-Team
{%- endif -%}
Footer #
{# Footer with logo and contact details #}
<a href="http://www.silicon-ioi.online">
<img src="https://silicon-ioi.online/files/.../logo.png" alt="Logo" width="250">
</a>
<span style="color: #5A14E6; font-weight: bold;">Silicon ioi – Digitalize your business</span>
<a href="http://www.silicon-ioi.online" style="color: #5A14E6;">www.silicon-ioi.online</a>
<span style="color: #999999;">Silicon ioi | xxx, xxx xxx | xxx</span>
**Key functions used:**
{{ _("text", language) }}
Translates a text into the specified language.
frappe.get_doc()
Retrieves a Frappe document (e.g.: an invoice, a contact).
strftime()
Formats a date (e.g.: {{ date.strftime('%d-%m-%Y') }}).
{% set var = value %}
Defines a variable in the template.
{% if/elif/else %}
Conditional structures.
{% for item in list %}
Loops through lists or dictionaries.
Complete Output Example
Subject
Invoice Silicon ioi - FCL2026020018 04-03-2026
Body (for French)
Dear Dupont,
Thank you for your trust.
Invoice: FCL2026020018 (1130,00 EUR).
Due date: 03-02-2026.
Best regards, Your Silicon ioi team
Complete example: multilingual template for invoices #
Subject: #
{{ _("Invoice", master.language) }} Silicon ioi - {{ master.name }} {{ frappe.utils.format_date(frappe.utils.nowdate(), "dd-MM-yyyy") }}
Body: #
{%- set doc = frappe.get_doc('ioi Sales Invoice', master.name) -%}
{%- set customerContact = frappe.get_doc('Contact', doc.invoice_customer_contact_id) if doc.invoice_customer_contact_id else None -%}
{# Salutation adapted to language and gender #}
{% if doc.language == 'fr' %}
{% if customerContact and customerContact.gender == 'Male' %}
{% set salutation = 'Dear' %}
{% elif customerContact and customerContact.gender == 'Female' %}
{% set salutation = 'Dear' %}
{% else %}
{% set salutation = 'Dear' %}
{% endif %}
{{ salutation }} {% if customerContact.last_name %}{{ customerContact.last_name }}{% else %}Customer{% endif %},
Thank you for your trust.
Invoice: <strong>{{ doc.prefix_id }}{{ doc.identification }}</strong> ({{ "%.2f"|format(doc.total_tvac)|replace('.', ',') }}{{ doc.currency_id }}).
Due date: <strong>{{ doc.due_date.strftime('%d-%m-%Y') }}</strong>.
Best regards, Your <span style="color: #5A14E6; font-weight: bold;">Silicon ioi</span> team
{%- elif doc.language == 'nl' -%}
{# Dutch version #}
{% if customerContact and customerContact.gender == 'Male' %}
{% set salutation = 'Geachte heer' %}
{% else %}
{% set salutation = 'Beste' %}
{% endif %}
{{ salutation }} {% if customerContact.last_name %}{{ customerContact.last_name }}{% else %}klant{% endif %},
Hartelijk dank voor uw vertrouwen.
Factuur : <strong>{{ doc.prefix_id }}{{ doc.identification }}</strong> ({{ "%.2f"|format(doc.total_tvac)|replace('.', ',') }}{{ doc.currency_id }}).
Vervaldatum : <strong>{{ doc.due_date.strftime('%d-%m-%Y') }}</strong>.
Met vriendelijke groet, Uw <span style="color: #5A14E6; font-weight: bold;">Silicon ioi</span>-team
{%- elif doc.language == 'en' -%}
{# English version #}
{% if customerContact and customerContact.gender == 'Male' %}
{% set salutation = 'Dear Mr.' %}
{% elif customerContact and customerContact.gender == 'Female' %}
{% set salutation = 'Dear Ms.' %}
{% else %}
{% set salutation = 'Dear' %}
{% endif %}
{{ salutation }} {% if customerContact.last_name %}{{ customerContact.last_name }}{% else %}Customer{% endif %},
Thank you for your trust.
Invoice: <strong>{{ doc.prefix_id }}{{ doc.identification }}</strong> ({{ "%.2f"|format(doc.total_tvac)|replace('.', ',') }}{{ doc.currency_id }}).
Due date: <strong>{{ doc.due_date.strftime('%d-%m-%Y') }}</strong>.
Best regards, Your <span style="color: #5A14E6; font-weight: bold;">Silicon ioi</span> team
{%- elif doc.language == 'de' -%}
{# German version #}
{% if customerContact and customerContact.gender == 'Male' %}
{% set salutation = 'Sehr geehrter Herr' %}
{% elif customerContact and customerContact.gender == 'Female' %}
{% set salutation = 'Sehr geehrte Frau' %}
{% else %}
{% set salutation = 'Sehr geehrte(r)' %}
{% endif %}
{{ salutation }} {% if customerContact.last_name %}{{ customerContact.last_name }}{% else %}Kunde{% endif %},
Vielen Dank für Ihr Vertrauen.
Rechnung: <strong>{{ doc.prefix_id }}{{ doc.identification }}</strong> ({{ "%.2f"|format(doc.total_tvac)|replace('.', ',') }}{{ doc.currency_id }}).
Fälligkeitsdatum: <strong>{{ doc.due_date.strftime('%d-%m-%Y') }}</strong>.
Mit freundlichen Grüßen, Ihr <span style="color: #5A14E6; font-weight: bold;">Silicon ioi</span>-Team
{%- endif -%}
{# Footer with logo and contact details #}
<a href="http://www.silicon-ioi.online">
<img src="https://silicon-ioi.online/files/.../logo.png" alt="Logo" width="250">
</a>
<span style="color: #5A14E6; font-weight: bold;">Silicon ioi – Digitalize your business</span>
<a href="http://www.silicon-ioi.online" style="color: #5A14E6;">www.silicon-ioi.online</a>
<span style="color: #999999;">Silicon ioi | xxx, xxx xxx | xxx</span>
Method using a macro #
In the context of Jinja, a macro is a tool that allows defining a reusable block of code, similar to a function in programming. It helps avoid repetition and makes the template more modular, more readable, and easier to maintain.
Explanation of Macros in Jinja #
Definition of a Macro #
A macro is defined using the following syntax:
{% macro macro_name(param1, param2, ...) %}
{# Macro content #}
{{ param1 }} does something with {{ param2 }}.
{% endmacro %}
macro : Keyword used to declare a macro.
macro_name : Name given to the macro to call it later.
param1, param2, ... : Parameters to pass to the macro to customize it.
Using a Macro #
Once defined, the macro can be called as many times as needed in the template, passing different values to it:
{{ macro_name("value1", "value2", ...) }}
Example in our case where the Jinja code repeated the same structure for each language:
{% if doc.language == 'fr' %}
{# Logic for French #}
{% elif doc.language == 'nl' %}
{# Logic for Dutch #}
...
{% endif %}
→ Repetition of the salutation logic and the email body structure.
Solution with a Macro:
→ Create an email_body macro to centralize the common logic and avoid repetition.
{%- macro email_body(lang, male_salutation, female_salutation, default_salutation, thank_you, invoice_label, due_date_label, regards) -%}
{# Salutation logic and email body #}
{{ salutation }} {{ customerContact.last_name }},
{{ thank_you }}
{{ invoice_label }}: <strong>...</strong>
{{ regards }}, Your Silicon ioi team
{%- endmacro -%}
Parameters:
lang : Current language (e.g.: 'fr', 'nl').
male_salutation : Salutation for a male (e.g.: 'Dear', 'Geachte heer').
female_salutation : Salutation for a female (e.g.: 'Dear', 'Geachte mevrouw').
default_salutation : Default salutation (e.g.: 'Dear').
thank_you, invoice_label, due_date_label, regards : Texts specific to each language.
Calling the Macro #
For each language, the macro is called with the appropriate parameters:
{% if doc.language == 'fr' %}
{{ email_body('fr', 'Cher', 'Chère', 'Cher/Cher(e)', 'Nous vous remercions...', 'Facture', 'Échéance', 'Cordialement') }}
{%- elif doc.language == 'nl' -%}
{{ email_body('nl', 'Geachte heer', 'Geachte mevrouw', 'Beste', 'Hartelijk dank...', 'Factuur', 'Vervaldatum', 'Met vriendelijke groet') }}
...
{%- endif -%}
Advantages of Macros #
The salutation and formatting logic is written only once in the macro, but used for all languages.
If you want to modify the email format (e.g.: add a line), you only need to do it in the macro, and not in each if/elif block.
The code is shorter and clearer, because repetition is eliminated.
Adding a new language or modifying a text becomes simpler (you just need to add a new call to the macro).
Complete code using the macro #
{%- set doc = frappe.get_doc('ioi Sales Invoice', master.name) -%}
{%- set customerContact = frappe.get_doc('Contact', doc.invoice_customer_contact_id) if doc.invoice_customer_contact_id else None -%}
{# Macro to generate the salutation and email body #}
{%- macro email_body(lang, male_salutation, female_salutation, default_salutation, thank_you, invoice_label, due_date_label, regards, team_label) -%}
{%- if customerContact and customerContact.gender == 'Male' -%}
{%- set salutation = male_salutation -%}
{%- elif customerContact and customerContact.gender == 'Female' -%}
{%- set salutation = female_salutation -%}
{%- else -%}
{%- set salutation = default_salutation -%}
{%- endif -%}
{{ salutation }} {% if customerContact.last_name %}{{ customerContact.last_name }}{% else %}{{ _('Customer', lang) }}{% endif %},
{{ thank_you }}
{{ invoice_label }}: <strong>{{ doc.prefix_id }}{{ doc.identification }}</strong> ({{ "%.2f"|format(doc.total_tvac)|replace('.', ',') }}{{ doc.currency_id }}).
{{ due_date_label }}: <strong>{{ doc.due_date.strftime('%d-%m-%Y') }}</strong>.
{{ regards }}, {% if lang == 'nl' %}Uw{% elif lang == 'de' %}Ihr{% else %}Your{% endif %} <span style="color: #5A14E6; font-weight: bold;">Silicon ioi</span>{{ team_label }}
{%- endmacro -%}
{# Call the macro according to the language #}
{% if doc.language == 'fr' %}
{{ email_body('fr', 'Cher', 'Chère', 'Cher/Cher(e)', 'Nous vous remercions pour votre confiance', 'Facture', 'Échéance', 'Cordialement', ' équipe') }}
{%- elif doc.language == 'nl' -%}
{{ email_body('nl', 'Geachte heer', 'Geachte mevrouw', 'Beste', 'Hartelijk dank voor uw vertrouwen', 'Factuur', 'Vervaldatum', 'Met vriendelijke groet', '-team') }}
{%- elif doc.language == 'en' -%}
{{ email_body('en', 'Dear Mr.', 'Dear Ms.', 'Dear', 'Thank you for your trust', 'Invoice', 'Due date', 'Best regards', ' team') }}
{%- elif doc.language == 'de' -%}
{{ email_body('de', 'Sehr geehrter Herr', 'Sehr geehrte Frau', 'Sehr geehrte(r)', 'Vielen Dank für Ihr Vertrauen', 'Rechnung', 'Fälligkeitsdatum', 'Mit freundlichen Grüßen', '-Team') }}
{%- endif -%}
{# Footer with logo and contact details #}
<a href="http://www.silicon-ioi.online">
<img src="https://silicon-ioi.online/files/.../logo.png" alt="Logo" width="250">
</a>
<span style="color: #5A14E6; font-weight: bold;">Silicon ioi – Digitalize your business</span>
<a href="http://www.silicon-ioi.online" style="color: #5A14E6;">www.silicon-ioi.online</a>
<span style="color: #999999;">Silicon ioi | xxx, xxx xxx | xxx</span>
Best practices and tips #
Testing and preview #


Preview is a critical step to ensure that your dynamically generated emails or documents display correctly before being sent to customers. It allows you to:
- Verify the visual rendering: Ensure that styles (bold, colors, alignments) are applied as expected.
- Validate dynamic data: Confirm that variables (invoice number, amount, due date) are correctly replaced with real values.
- Detect syntax errors: Identify improperly closed Jinja tags or undefined variables.
- In Silicon ioi, use the “Preview” option available in the template editor.
- Test with different datasets (e.g.: a male, female, or undefined-gender customer) to cover all use cases.
- Check display across different email clients (Gmail, Outlook, Apple Mail) if possible, as some CSS styles may be interpreted differently.
Documentation and comments #
A well-documented template is easier to understand, modify, and maintain, especially if multiple people are working on it. Comments help to:
- Explain the logic: clarify why a condition or loop is used.
- Indicate dependencies: specify where variables come from (ex : doc comes from frappe.get_doc(‘ioi Sales Invoice’,
master.name)). - Facilitate updates: quickly identify which parts of the template need to be modified if a field changes in the database.
Astuce
Use Jinja comments {# … #} to add notes that are invisible in the final output.

Images #
Images in emails (logos, signatures) must be publicly accessible to display correctly for the recipient. If the image is stored as “private” in ioi, it will not be visible in the received email.
In Silicon ioi, upload images (ex : logo) in public mode so that the image is publicly accessible.



Use the full URL in the template:
<img src="https://your-domain.silicon-ioi.online/files/logo.png" alt="Logo Silicon ioi" width="200">
In our example : https://silicon-ioi.online/files/1200x630wa.jpg
- Your-domain.silicon-ioi.online => silicon-ioi.online
- File path =>
/files/1200x630wa.jpg
→ Open the image URL in a browser in private mode to confirm that it displays without authentication.

Astuce
Use lightweight formats (Png, Jpeg, Webp, Avif).
Astuce
Resize images to the displayed size (e.g.: width=“200” in HTML) to avoid long loading times.
Add/modify translations #
Silicon ioi uses a translation system to display texts (ex : “Invoice”, “Due date”) in the customer’s language. If a translation is missing or incorrect, the email may contain English terms or errors.
Lookup mechanism: #
When using {{ _("Invoice", language) }} in a Jinja template, Frappe follows this order to find the translation:
- Application JSON files: apps/ioi/translations/fr.json.
- tabTranslation table in the database.
- Return of the source text if no translation is found.
How to manage translations #
Via the Frappe interface (recommended method for non-developers):
- Go to
Settings>Translations. - Search for the term to translate (ex : “Invoice”).
- Add or modify the translation for each language (ex : “Facture” for French).
- Save and clear the cache (via
bench clear-cache) if necessary.
Via JSON files (for developers):
→ Translations are stored in files such as apps/ioi/translations/fr.json.
Example content :
{
"Invoice": "Invoice",
"Due date": "Due date",
"Thank you for your trust": "Thank you for your trust"
}
After modification, restart Frappe to apply the changes.
Via the database (for bulk updates):
Use SQL queries to add/modify translations in the tabTranslation table:
INSERT INTO \`tabTranslation\` (source_text, language, translated_text)
VALUES ("Invoice", "fr", "Facture");
→ Test translations by changing the language of the user or the document in Frappe.
Using variables #
Clear and consistent variable names make your template:
- More readable: Easy to understand for you and your colleagues.
- Less error-prone: Reduces the risk of confusion (e.g.: d_date vs due_date).
- More maintainable: Simplifies future updates.
Use descriptive, meaningful names:
{# Good practice #}
{% set customer_name = customerContact.last_name %}
{% set invoice_amount = doc.total_tvac %}
{# Bad practice #}
{% set cn = customerContact.last_name %}
{% set amt = doc.total_tvac %}
Avoid special characters (spaces, hyphens, accents):
{# Good practice #}
{% set due_date = doc.due_date %}
{# Bad practice #}
{% set due-date = doc.due_date %}
{% set due_date_with_accent = doc.due_date %}
Be consistent:
- If you use camelCase (ex : customerName), apply it everywhere.
- If you use snake_case (ex : customer_name), stay consistent.
Complete example :
{%- set invoice_document = frappe.get_doc('ioi Sales Invoice', master.name) -%}
{%- set client_contact = frappe.get_doc('Contact', invoice_document.invoice_customer_contact_id) if invoice_document.invoice_customer_contact_id else None -%}
{# Calculation of total amount including VAT #}
{%- set total_amount = "%.2f"|format(invoice_document.total_tvac)|replace('.', ',') -%}
{%- set currency = invoice_document.currency_id -%}
Total amount: {{ total_amount }} {{ currency }}
The “-” and formatting #
The symbols {{-, -}}, {%-, -%}, {#-, and -#} are used to remove whitespace (spaces, tabs, line breaks) around Jinja tags. This helps avoid formatting issues, especially in emails or HTML documents where alignment is crucial.
Syntax and Effect:
{{- variable }}
Removes spaces before the variable.
{{ variable -}}
Removes spaces after the variable.
{%- if condition %}
Removes spaces before a block (e.g.: if, for).
{% endif -%}
Removes spaces after a block.
{#- comment -#}
Removes spaces before a comment.
{# comment -#}
Removes spaces after a comment.
When to use them #
For emails: Avoid unwanted spaces that disrupt alignment.
<p>
Hello {{- user.name -}},
{{- message -}}
</p>
Use {{- and -}} to remove spaces around variables.
Use {%- and -%} to remove spaces around blocks (if, for, etc.).
→ Essential for emails and HTML documents where formatting matters.
→ Makes the code cleaner and avoids rendering issues.
Key functions of Jinja and Frappe #
→ Functions for numbers:
| Function/Filter | Role | Example | Result |
|---|---|---|---|
| + | Addition | {{ 5 + 3 }} | 8 |
| - | Subtraction | {{ 10 - 4 }} | 6 |
| * | Multiplication | {{ 3 * 4 }} | 12 |
| / | Division | {{ 10 / 2 }} | 5.0 |
| // | Integer division | {{ 10 // 3 }} | 3 |
| % | Modulo | {{ 10 % 3 }} | 1 |
| ** | Power | {{ 2 ** 3 }} | 8 |
| abs() | Absolute value | {{ -5 | abs }} | 5 |
| round() | Round a number | {{ 3.14159 | round(2) }} | 3.14 |
| int() | Convert to integer | {{ “5” | int }} | 5 |
| float() | Convert to decimal | {{ “3.14” | float }} | 3.14 |
| sum() | Sum of a list of numbers | {{ [1, 2, 3] | sum }} | 6 |
| min() | Minimum value | {{ [1, 2, 3] | min }} | 1 |
| max() | Maximum value | {{ [1, 2, 3] | max }} | 3 |
| format() | Format a number | {{ 1234.5678 | format(“%.2f”) | replace(“.”, “,”) }} | 1234,57 |
| random() | Random number | {{ [1, 2, 3] | random }} | 2 (random) |
→ Functions for text:
| Function/Filter | Role | Example | Result |
|---|---|---|---|
| upper() | Convert to uppercase | {{ “hello” | upper }} | HELLO |
| lower() | Convert to lowercase | {{ “HELLO” | lower }} | hello |
| capitalize() | Capitalize first letter | {{ “hello” | capitalize }} | Hello |
| title() | Capitalize each word | {{ “hello world” | title }} | Hello World |
| trim() | Remove spaces at start/end | {{ " hello " | trim }} | hello |
| replace() | Replace a string | {{ “hello” | replace(“lo”, “p”) }} | help |
| split() | Split a string into a list | {{ “hello world” | split(" ") }} | [‘hello’, ‘world’] |
| join() | Join a list into a string | {{ [“hello”, “world”] | join(", ") }} | hello, world |
| length() | Length of a string | {{ “hello” | length }} | 5 |
| truncate() | Truncate a string | {{ “hello” | truncate(4) }} | hell… |
| slice() | Extract part of a string | {{ “hello” | slice(0, 3) }} | hel |
| strip() | Remove spaces | {{ " hello! " | strip }} | hello! |
→ Functions for dates:
| Function/Filter | Role | Example | Result |
|---|---|---|---|
| frappe.utils.nowdate() | Current date | {{ frappe.utils.nowdate() }} | 2026-03-04 |
| frappe.utils.now() | Current date and time | {{ frappe.utils.now() }} | 2026-03-04 15:30:00 |
| strftime() | Format a date | {{ frappe.utils.nowdate() | strftime(“%d-%m-%Y”) }} | 04-03-2026 |
| frappe.utils.getdate() | Convert string to date object | {{ frappe.utils.getdate(“04/03/2026”) }} | Date object |
| date_diff() | Difference in days between 2 dates | {{ (frappe.utils.getdate(“10-03-2026”) - frappe.utils.getdate(“04-03-2026”)).days }} | 6 |
| week_number() | Week number | {{ frappe.utils.getdate(“04-03-2026”).isocalendar()[1] }} | 10 (week 10 of 2026) |
| add_days() | Add days to a date | {{ frappe.utils.add_days(frappe.utils.nowdate(), 7) }} | 2026-03-11 |
| add_months() | Add months to a date | {{ frappe.utils.add_months(frappe.utils.nowdate(), 1) }} | 2026-04-04 |
→ Useful functions specific to Frappe:
| Function | Description | Example | Result (example) |
|---|---|---|---|
| frappe.get_doc() | Retrieve a Frappe document | {{ frappe.get_doc(“User”, “admin”).full_name }} | Administrator |
| frappe.db.get_value() | Retrieve a value from the database | {{ frappe.db.get_value(“User”, “admin”, “email”) }} | admin@example.com |
| frappe.utils.flt() | Convert to float | {{ frappe.utils.flt(“3.14”) }} | 3.14 |
| frappe.utils.cint() | Convert to integer | {{ frappe.utils.cint(“5”) }} | 5 |
| frappe.utils.cstr() | Convert to string | {{ frappe.utils.cstr(123) }} | “123” |
| frappe.utils.comma_and() | Format a list into a string | {{ frappe.utils.comma_and([“Apples”, “Pears”, “Bananas”]) }} | Apples, Pears and Bananas |
Comments and documentation in templates #
Simple comments (single line):
{# This comment will not be displayed in the final output #}
Multi-line comments:
{#
This comment spans multiple lines.
It is useful for documenting complex sections.
None of this text will appear in the final output.
#}
Difference with HTML comments:
- Jinja comments
({# ... #}): Are not visible in the final output. - HTML comments
(<!-- ... -->): Are visible in the generated HTML source code.
Appendices: summary of useful functions #
→ Functions for dates and times:
| Function | Description | Example |
|---|---|---|
| nowdate() | Returns the current date in YYYY-MM-DD format. | {{ frappe.utils.nowdate() }} |
| now() | Returns the current date and time in YYYY-MM-DD HH:MM:SS format. | {{ frappe.utils.now() }} |
| today() | Alias of nowdate(). | {{ frappe.utils.today() }} |
| getdate(string) | Converts a string into a date object. | {{ frappe.utils.getdate(“04/03/2026”) }} |
| format_date(date, format) | Formats a date according to the specified format. | {{ frappe.utils.format_date(frappe.utils.nowdate(), “dd-MM-yyyy”) }} |
| add_days(date, days) | Adds a number of days to a date. | {{ frappe.utils.add_days(frappe.utils.nowdate(), 7) }} |
| date_diff(end_date, start_date) | Returns the difference in days between two dates. | {{ frappe.utils.date_diff(frappe.utils.getdate(“10-03-2026”), frappe.utils.nowdate()) }} |
→ Functions for numbers:
| Function | Description | Example |
|---|---|---|
| flt(value, precision=2) | Converts a value to float with error handling. | {{ frappe.utils.flt(“3.14159”, 2) }} |
| fmt_money(amount, precision=2, currency=None) | Formats an amount with a currency symbol. | {{ frappe.utils.fmt_money(1234.5678, 2, “EUR”) }} |
| cint(value) | Converts a value to int with error handling. | {{ frappe.utils.cint(“5”) }} |
| round(number, precision=0) | Rounds a number. | {{ frappe.utils.round(3.14159, 2) }} |
→ Functions for strings:
| Function | Description | Example |
|---|---|---|
| scrub(string) | Replaces spaces and special characters with underscores (_). | {{ frappe.utils.scrub(“My File.txt”) }} |
| comma_and(list) | Formats a list into a string with commas and “and” (e.g.: “A, B and C”). | {{ frappe.utils.comma_and([“Apples”, “Pears”, “Bananas”]) }} |
| get_link_to_form(doctype, name, label=None) | Generates a link to a Frappe form. | {{ frappe.utils.get_link_to_form(“User”, “admin”) }} |