Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

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:

django
1
2
{% 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-0404-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
1
2
3
scaicontrol admin templates validate
scaicontrol admin templates validate --lang nl
scaicontrol admin templates validate --name "ScaiLabs Standard Invoice"

Non-zero exit on any failure.

Updated 2026-05-18 01:48:39 View source (.md) rev 2