---
title: Templates
path: concepts/templates
status: published
---

# Templates

ScaiControl ships a visual template designer for invoices and emails. Templates are stored in the `invoice_templates` table, edited via a GrapesJS-based canvas in the admin UI, rendered with Jinja2 + WeasyPrint (PDFs) or Jinja2 alone (emails).

## What is a template

A template is identified by the triple `(name, document_type, language)`:

- **`name`** — operator-chosen, e.g. "ScaiLabs Standard Invoice". Same name spans multiple language variants.
- **`document_type`** — one of:
  - `invoice` — PDF
  - `credit_note` — PDF (rendered the same way, but `credit_note_default.html` is the fallback)
  - `email_invoice` — HTML email
  - `email_credit_note` — HTML email
- **`language`** — ISO 639-1 lowercase code (`en`, `nl`, `de`, `fr`, `es`, `it`) or NULL for the any-language fallback.

A template carries:
- `subject` — only for email types
- `html_content` — the GrapesJS-saved body markup
- `css_content` — the CSS authored in the canvas
- `editor_data` — opaque GrapesJS project state (not used at render time but stored for round-trips)
- `version` — incremented on every save

## Resolution chain

When ScaiControl needs to render an invoice or email, it walks this chain (highest priority first):

1. Tenant's `invoice_template_name` (or `email_template_name`) + tenant's `preferred_language`
2. Tenant's template name + the any-language (NULL) variant
3. Partner's template name + tenant's preferred_language
4. Partner's template name + any-language variant
5. Built-in filesystem template (`services/billing/templates/invoice_default.html`, `credit_note_default.html`)

The first hit wins. This means a tenant can override its partner, and a partner can override the platform default.

## Designer UX

`/admin/billing/templates` lists every template, grouped by `(name, document_type)`. Inside each row, every language variant is listed; clicking opens the designer at `/admin/billing/templates/designer?name=…&type=…&lang=…`.

The designer:
- Drag-and-drop blocks for sections (header, invoice details, line items table, totals, bank info, footer).
- Variable picker — every Jinja variable that's in scope at render time, browsable by group (Seller, Buyer, Document, Loops, Conditionals).
- Live preview that renders the canvas with sample data through the same backend pipeline that finalisation uses.
- Page-format picker (A4, Letter, A5) for PDFs.
- "Send test email" button for email types.
- Language switcher — load a different language variant; "Copy current to another language" for fast localisation.

## Jinja round-trip

GrapesJS is HTML-aware, not Jinja-aware. Naively storing `{% if x %}…{% endif %}` in the canvas breaks the HTML5 parser (table foster-parenting moves block-level Jinja tags out of `<tbody>`), and the designer's auto-classification of text components misbehaves when it sees raw Jinja tokens.

To work around both issues, ScaiControl **encodes Jinja control syntax as HTML comments** before loading into the canvas and decodes on save:

```
{% if x %}foo{% endif %}        →   <!--JS:if x-->foo<!--JS:endif-->
{# comment #}                   →   <!--JC:comment-->
```

`{{ var }}` (expressions, no side effects) survive as-is.

In the designer canvas:
- HTML comments are invisible, so the user sees the wrapped content rendered statically.
- The auto-applied `data-jinja-cond="if x"` decoration draws an amber dashed border around any element wrapped in `{% if %}` / `{% for %}` blocks, with a small label. This is canvas-only (stripped at save time).

The frontend validation calls `POST /admin/billing/templates/designer/validate` on every change (debounced); the backend compiles the HTML through Jinja2 and returns errors (line + reason) so the admin sees broken syntax before saving.

## Page format and WeasyPrint quirks

PDF templates are rendered with WeasyPrint via `services/billing/template_renderer.py:render_pdf`. Things to know:

- **Blogger Sans is embedded as base64 data URLs** in the `@font-face` declarations. This avoids a runtime fetch and ensures consistent rendering offline.
- **SVG images with `width="100%" height="100%"` get those attributes stripped** by a custom WeasyPrint `url_fetcher`. Without this, WeasyPrint scales the SVG to fill its container instead of honouring the author's CSS `width`.
- **`@media (max-width: …)`-wrapped rules are not part of print rendering**. GrapesJS occasionally wraps style rules in media queries when the user resizes elements on the non-default device — those rules don't match in WeasyPrint's print media and silently disappear. The designer config now sets `widthMedia: ''` on every device so future edits stay unscoped, but already-saved templates may need a one-time SQL fix.

## Email rendering

Email templates render via `services/billing/template_renderer.py:render_email_template`. Both subject and HTML are Jinja-rendered with the same context shape as invoices (`seller`, `buyer`, `document`, `total`, `line_items`, `vat_breakdown`, `notes`, `reverse_charge`). Plain-text fallback is auto-derived from the HTML by stripping tags + `<style>` / `<script>` contents.

The CSS injected into emails is the designer's own — no equivalent of base.css gets layered on top (it would have for PDFs early on; same fix applied to both).

## Default formatting

Localisable bits:
- **Dates**: `%d-%m-%Y` (European convention). `2026-05-04` → `04-05-2026`.
- **Currency**: European thousands/decimal — `€ 1.234,56`. Currency-symbol lookup table covers EUR/USD/GBP; other currencies fall back to a three-letter prefix.
- **Legal notes**: localised at render time per `preferred_language`.

## Validation on save

The `POST /admin/billing/templates/designer` upsert endpoint compiles both `subject` and `html_content` through Jinja2 BEFORE writing to the DB. A `TemplateSyntaxError` returns 400 with `Invalid Jinja syntax in <field> (line N): <message>`. This means a broken template can't be silently saved and later blow up at PDF generation time.

The CLI also has a validator for ad-hoc / CI use:

```bash
scaicontrol admin templates validate
scaicontrol admin templates validate --lang nl
scaicontrol admin templates validate --name "ScaiLabs Standard Invoice"
```

Non-zero exit on any failure.
