Genereer PDF-facturen
Deze tutorial voegt een afdrukbare PDF-factuur toe voor elke Deal in het mini-CRM dat je in Bouw je eerste app hebt gebouwd. Aan het einde heb je een factuur-template die het bedrijf, de regelitems en totalen van een deal ophaalt, live gerenderd in het preview-paneel van de editor en aanroepbaar vanuit externe systemen via REST.
Totale tijd: ongeveer 25 minuten. Je raakt PDF Templates aan en breidt het schema uit met een regelitem-tabel.
Trial-beperking: PDF-rendering is uitgeschakeld binnen de trial-omgeving om de gedeelde headless-Chromium-pool niet te overbelasten. Zowel het live preview-paneel van de editor (POST /api/v1/pdf-templates/{id}/preview) als het publiekePOST /api/v1/pdf-templates/generate/<name>-endpoint geven 403 terug totdat je op een betaalde omgeving zit. Je kunt nog steeds de template schrijven (datascript, HTML, settings, parameters) en opslaan, het preview-paneel toont de 403 totdat rendering opnieuw wordt ingeschakeld. Plan om de gerenderde output op een niet-trial-omgeving te verifieren voordat je erop vertrouwt.
Vereisten
Je zou het mini-CRM uit Bouw je eerste app gedeployed
moeten hebben: company, contact, deal-tabellen; hun
Business Entities; de drie pagina's. Als je die tutorial hebt overgeslagen, zal de rest van
deze pagina weinig zin hebben.
Stap 1 - Voeg regelitems toe aan het schema (5 min)
Een echte factuur heeft regelitems, niet alleen een enkel bedrag. Voeg een
deal_line-tabel toe.
- Open Schema Designer, Add Table, noem hem
deal_line. -
Voeg in de Columns-tab van het rechterpaneel toe:
id- SERIAL, PK (auto)deal_id- INTEGER, requireddescription- VARCHAR(300), requiredquantity- DECIMAL, required, default1unit_price- DECIMAL, required, default0line_total- DECIMAL, required, default0- afgeleid, hieronder in sync gehouden
-
Schakel naar de Relations-tab. Voeg een relatie toe: FK column
deal_id, Relation type One-to-Many, References tabledeal, References columnid. Klik op Add Relationship. - Klik op Deploy, bekijk de Generated SQL, klik op Deploy rechtsboven.
Bedraad de BE, pagina en een klein event
- Business Entities, Create. Entity Name
deal_line, Master Tabledeal_line, Label Columndescription. Sla op. -
Open de bestaande Deals-pagina in Page Editor. Klik in het detailformulier
op Add Tab, noem de tab Lines, en voeg een RelatedGrid-
sectie toe gebonden aan
deal_linemet het joinfilter deal.id = current record's id. Sla op en publiceer opnieuw. - Business Events, Create. Rule Name
Calc deal_line.line_total. Enabled AAN. Business Entitydeal_line. Triggers: Before Create + Before Update. Geen condities. Actie: Execute Script met body:
Sla op.Entity.line_total = (decimal)(Entity.quantity ?? 0) * (decimal)(Entity.unit_price ?? 0);
Klik in een Deal, schakel naar de nieuwe Lines-tab, en voeg twee of drie regels toe. De
line_total-kolom zou elke keer dat je een regel opslaat automatisch moeten
worden gevuld.
Stap 2 - Schrijf het PDF-datascript (5 min)
Open PDF Templates, klik op + Create. De editor opent met
een Name-veld bovenaan (typ DealInvoice), een Description-veld, en vijf tabs:
HTML Template, Data Script, Settings,
Params, JSON. Het PDF Preview-paneel is
altijd zichtbaar rechts en rendert opnieuw wanneer je opslaat.
Schakel naar de Params-tab, klik op + Add. Name
deal_id, Type int, Required aangevinkt. (Het tab-label wordt
bijgewerkt naar Params (1) zodra je er een hebt gedefinieerd.)
Schakel naar de Data Script-tab, de Monaco C#-editor opent links, met de PDF-preview nog steeds rechts. Plak:
var deal = await Db.GetAsync("deal", deal_id);
if (deal == null) throw new Exception($"Deal {deal_id} not found");
var company = await Db.GetAsync("company", deal.company_id);
var lines = await Db.From("deal_line")
.Where("deal_id", "=", deal_id)
.OrderBy("id")
.ToListAsync();
decimal subtotal = 0;
foreach (var l in lines) subtotal += (decimal)(l.line_total ?? 0);
var vatRate = 0.21m; // Belgian standard VAT, adjust per customer
var vat = Math.Round(subtotal * vatRate, 2);
var total = subtotal + vat;
return new {
InvoiceNumber = $"INV-{deal.id:D6}",
InvoiceDate = DateTime.UtcNow.ToString("yyyy-MM-dd"),
Deal = new { Title = (string)deal.title, Stage = (string)deal.stage },
Company = new { Name = (string)company.name, Industry = (string?)company.industry },
Lines = lines.Select(l => new {
Description = (string)l.description,
Quantity = l.quantity,
UnitPrice = l.unit_price,
LineTotal = l.line_total
}),
Subtotal = subtotal,
VatRate = (int)(vatRate * 100),
Vat = vat,
Total = total
}; Het script geeft een anoniem object terug. Wat je ook teruggeeft wordt de data-context voor het HTML-template, elke property is op naam toegankelijk in Scriban-placeholders.
Waarom al die casts?Db.GetAsyncen.ToListAsync()gevendynamicterug, wat Scriban soms verkeerd kan behandelen wanneer een waarde null is of zijn runtime-type niet is wat het template verwacht. Casten naar een concreet type aan de grens (waar je het return-object bouwt) geeft je voorspelbaar gedrag.
Stap 3 - Schrijf het HTML-template (10 min)
Plak in de HTML Template-editor dit (de styling is het grootste deel van de bytes):
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>{{ InvoiceNumber }}</title>
<style>
@page { size: A4; margin: 24mm 18mm; }
body { font: 11pt/1.5 'Inter', sans-serif; color: #1e2838; }
h1 { font-size: 24pt; margin: 0 0 4pt; letter-spacing: -0.02em; }
.muted { color: #566277; font-size: 10pt; }
.row { display: flex; justify-content: space-between; margin-bottom: 24pt; }
.right { text-align: right; }
table { width: 100%; border-collapse: collapse; margin: 16pt 0; }
th { text-align: left; padding: 6pt 4pt; border-bottom: 2px solid #1e2838; font-size: 9pt; text-transform: uppercase; letter-spacing: 0.08em; }
td { padding: 8pt 4pt; border-bottom: 1px solid #e4e7ed; }
td.num { text-align: right; font-variant-numeric: tabular-nums; }
.totals { margin-left: auto; width: 60mm; }
.totals td { padding: 4pt 4pt; border: none; }
.totals .grand { border-top: 2px solid #1e2838; font-weight: 700; font-size: 13pt; }
footer { margin-top: 32pt; font-size: 9pt; color: #8c97aa; text-align: center; }
</style>
</head>
<body>
<div class="row">
<div>
<h1>Invoice</h1>
<div class="muted">{{ InvoiceNumber }} · {{ InvoiceDate }}</div>
</div>
<div class="right">
<strong>Bill to</strong><br>
{{ Company.Name }}<br>
<span class="muted">{{ Company.Industry }}</span>
</div>
</div>
<div><strong>{{ Deal.Title }}</strong> <span class="muted">· {{ Deal.Stage }}</span></div>
<table>
<thead>
<tr>
<th>Description</th>
<th class="num">Qty</th>
<th class="num">Unit price</th>
<th class="num">Line total</th>
</tr>
</thead>
<tbody>
{{ for line in Lines }}
<tr>
<td>{{ line.Description }}</td>
<td class="num">{{ line.Quantity }}</td>
<td class="num">€ {{ line.UnitPrice }}</td>
<td class="num">€ {{ line.LineTotal }}</td>
</tr>
{{ end }}
</tbody>
</table>
<table class="totals">
<tr>
<td>Subtotal</td>
<td class="num">€ {{ Subtotal }}</td>
</tr>
<tr>
<td>VAT ({{ VatRate }}%)</td>
<td class="num">€ {{ Vat }}</td>
</tr>
<tr class="grand">
<td>Total</td>
<td class="num">€ {{ Total }}</td>
</tr>
</table>
<footer>Thank you for your business.</footer>
</body>
</html> Bekijk het resultaat
Klik op de groene Update-knop (of Create bij een nieuw template) rechtsboven om op te slaan. Het PDF Preview-paneel rechts rendert opnieuw. Een thumbnail-kolom aan de linkerrand van de preview toont pagina 1, pagina 2, etc.; het hoofdpaneel toont de volledige render met een ingebouwde PDF-viewer-werkbalk (zoom, draaien, downloaden, printen, meer). Itereer door de HTML- of Data Script-tab te bewerken en opnieuw op te slaan.
De icoonknoppen rechtsboven in de editor (naast Cancel) laten je de zichtbaarheid van het preview-paneel toggelen en schakelen naar full-screen-editingmodus. Het help (?)-icoon opent een inline cheat-sheet voor Scriban-syntaxis.
Dingen die vaak een tweak nodig hebben:
- Nummerformattering. Decimalen komen terug als ruwe getallen; als je vast 2-decimalen-weergave wilt, formatteer in het datascript (
l.line_total.ToString("0.00")) en geef strings terug.- Valutasymbool. Hierboven hardcoded als euro, externaliseer naar een parameter of een Branding-instelling als je meerdere valuta's bedient.
- Paginabreuken. Voor lange regelitemlijsten, voeg
page-break-inside: avoidtoe aantrin de CSS zodat een rij niet over pagina's wordt gesplitst.- Lettertypes. De renderer heeft Liberation-, DejaVu- en Noto-families. Als je naar Inter verwijst (zoals hierboven) valt het terug op een systeem-sans. Om een merklettertype te embedden, base64-encodeer een
.woff2en inline het via@font-face.
Stap 4 - Genereer de PDF van buiten de app (5 min)
Het template is nu op naam aanroepbaar vanuit elke HTTP-client. Drie endpoints, allemaal
onder /api/v1/pdf-templates:
-
POST /api/v1/pdf-templates/{id}/preview- rendert de template op zijn database-ID en geeftapplication/pdfterug. Gebruikt door het preview-paneel van de editor. -
POST /api/v1/pdf-templates/generate/{name}- rendert op naam en geeftapplication/pdfterug (browser-download). -
POST /api/v1/pdf-templates/generate/{name}/base64- hetzelfde maar geeft{ "data": "<base64>" }terug. Handig wanneer de caller de PDF wil embedden in een andere response-payload.
Vanuit een shell, met een Bearer-token vanuit de adminapp:
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"deal_id": 1}' \
https://demo1.archestack.eu/api/v1/pdf-templates/generate/DealInvoice \
--output invoice.pdf Vanuit een extern systeem (een Zapier-integratie, een order-bevestigingsflow van een partner), roep hetzelfde endpoint aan met de parameters in de JSON-body. Het platform draait het datascript met die parameters, rendert de HTML, en streamt de PDF terug.
Trial-modus: zowel hetgenerate-endpoint als het{id}/preview-endpoint geven 403 terug in de trial-omgeving, dus noch de externecurl-aanroep noch het preview-paneel van de editor zal een PDF teruggeven totdat je op een betaalde omgeving zit. De template-metadata (datascript, HTML, settings) slaat nog steeds normaal op, het renderen is wat afgeschermd is.
Recap
- Je hebt het schema uitgebreid met een kindtabel (
deal_line) en een Before- trigger gebruikt om een afgeleid veld in sync te houden. - Je hebt een PDF-template gebouwd, een datascript dat de juiste rijen ophaalt, een HTML-body die ze rendert met Scriban-placeholders.
- Je hebt de REST-endpoints geleerd voor het aanroepen van de template van overal, inclusief het preview-endpoint dat de editor zelf aandrijft.
Waar nu naartoe
- PDF Templates-referentie - dekt het volledige Scriban-oppervlak, font-afhandeling, paginabreuk-trucs, en de Settings-tab van de editor (marges, papierformaat, orientatie).
- Scheduled Events - roep het PDF generate- endpoint aan vanuit een Script Module en e-mail het resultaat naar de klant op een maandelijks schema.