Tutorial - 25 min

Genera fatture in PDF

Questo tutorial aggiunge una fattura PDF stampabile per ogni Deal nel mini-CRM che hai costruito in Crea la tua prima app. Alla fine avrai un template di fattura che tira la company di un deal, le righe e i totali, renderizzato live nel pannello di anteprima dell'editor e chiamabile da sistemi esterni via REST.

Tempo totale: ~25 minuti. Toccherai i PDF Templates ed estenderai lo schema con una tabella di righe.

Limitazione del trial: il rendering PDF è disabilitato dentro l'ambiente trial per mantenere il pool condiviso di Chromium headless al riparo dal sovraccarico. Sia il pannello di anteprima live dell'editor (POST /api/v1/pdf-templates/{id}/preview) sia l'endpoint pubblico POST /api/v1/pdf-templates/generate/<name> ritornano 403 finché non sei su un ambiente a pagamento. Puoi comunque creare il template (data script, HTML, settings, parametri) e salvarlo, il pannello di anteprima mostrerà il 403 finché il rendering non viene riabilitato. Pianifica di verificare l'output renderizzato su un ambiente non-trial prima di farci affidamento.

Prerequisiti

Dovresti avere il mini-CRM da Crea la tua prima app deployato: tabelle company, contact, deal; le loro Business Entities; le tre pagine. Se hai saltato quel tutorial, il resto di questa pagina non avrà molto senso.

Passo 1 - Aggiungi righe allo schema (5 min)

Una fattura reale ha righe, non solo un singolo importo. Aggiungi una tabella deal_line.

  1. Apri Schema Designer -> Add Table -> nominalo deal_line.
  2. Nella tab Columns del pannello destro, aggiungi:
    • id - SERIAL, PK (auto)
    • deal_id - INTEGER, required
    • description - VARCHAR(300), required
    • quantity - DECIMAL, required, default 1
    • unit_price - DECIMAL, required, default 0
    • line_total - DECIMAL, required, default 0 - derivata, tenuta in sync sotto
  3. Passa alla tab Relations. Aggiungi una relazione: FK column deal_id, Relation type One-to-Many, References table deal, References column id. Fai clic su Add Relationship.
  4. Fai clic su Deploy, rivedi il Generated SQL, fai clic su Deploy in alto a destra.

Collega la BE, la pagina e un piccolo event

  1. Business Entities -> Create. Entity Name deal_line, Master Table deal_line, Label Column description. Salva.
  2. Apri la pagina Deals esistente in Page Editor. Nel form di dettaglio, fai clic su Add Tab, nomina la tab Lines, e aggiungi una sezione RelatedGrid collegata a deal_line con il filtro di join deal.id = current record's id. Salva e ripubblica.
  3. Business Events -> Create. Rule Name Calc deal_line.line_total. Enabled ON. Business Entity deal_line. Triggers: Before Create + Before Update. Nessuna condizione. Action: Execute Script con corpo:
    Entity.line_total = (decimal)(Entity.quantity ?? 0) * (decimal)(Entity.unit_price ?? 0);
    Salva.

Fai clic su un Deal, passa alla nuova tab Lines, e aggiungi due o tre righe. La colonna line_total dovrebbe popolarsi automaticamente ogni volta che salvi una riga.

Passo 2 - Scrivi il data script del PDF (5 min)

Apri PDF Templates -> fai clic su + Create. L'editor si apre con un campo Name in alto (digita DealInvoice), un campo Description, e cinque tab: HTML Template, Data Script, Settings, Params, JSON. Il pannello PDF Preview è sempre visibile sulla destra e si rigenera quando salvi.

Passa alla tab Params -> fai clic su + Add. Name deal_id, Type int, Required spuntato. (L'etichetta della tab si aggiorna a Params (1) una volta che ne hai definito uno.)

Passa alla tab Data Script, l'editor Monaco C# si apre sulla sinistra, con l'anteprima PDF ancora sulla destra. Incolla:

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
};

Lo script ritorna un oggetto anonimo. Qualunque cosa ritorni diventa il contesto dati per il template HTML, ogni proprietà è accessibile per nome nei placeholder Scriban.

Perché tutti i cast? Db.GetAsync e .ToListAsync() ritornano dynamic, che Scriban può a volte gestire male quando un valore è null o il suo tipo runtime non è quello che il template si aspetta. Fare il cast a un tipo concreto al confine (dove costruisci l'oggetto di ritorno) ti dà un comportamento prevedibile.

Passo 3 - Scrivi il template HTML (10 min)

Nell'editor HTML Template, incolla questo (lo styling è il grosso dei byte):

<!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>

Vedi il risultato

Fai clic sul pulsante verde Update (o Create su un template nuovo) in alto a destra per salvare. Il pannello PDF Preview sulla destra si rigenera. Una colonna di thumbnail sul bordo sinistro dell'anteprima mostra pagina 1, pagina 2, ecc.; il pannello principale mostra il render completo con una toolbar di viewer PDF integrata (zoom, ruota, download, stampa, altro). Itera modificando la tab HTML o Data Script e salvando di nuovo.

I pulsanti icona in alto a destra dell'editor (accanto a Cancel) ti permettono di alternare la visibilità del pannello di anteprima e passare alla modalità di editing a tutto schermo. L'icona di aiuto (?) apre un cheat-sheet inline per la sintassi Scriban.

Editor PDF Templates: campi Name e Description in alto, cinque tab (HTML Template, Data Script, Settings, Params, JSON), editor Monaco a sinistra, pannello PDF Preview sempre attivo a destra con una colonna di thumbnail e una fattura renderizzata.
Editor PDF Templates che mostra un template di fattura reale. In alto: campi Name + Description, più icone di toggle layout / aiuto / Cancel / Update verde a destra. Tabs: HTML Template (attiva qui) / Data Script / Settings / Params (con un badge di conteggio) / JSON. Pannello destro: PDF Preview sempre attivo con una colonna di thumbnail (pagina 1 selezionata, pagina 2 sotto), una toolbar di viewer integrata (zoom 47%, ruota, download, stampa, ecc.), e l'output renderizzato.
Cose che spesso hanno bisogno di un ritocco:
  • Formattazione dei numeri. I decimali tornano come numeri raw; se vuoi una visualizzazione fissa a 2 decimali, formatta nel data script (l.line_total.ToString("0.00")) e ritorna stringhe.
  • Simbolo di valuta. Hardcoded come euro sopra, esternalizzalo a un parametro o a un'impostazione Branding se servi più valute.
  • Page break. Per liste lunghe di righe, aggiungi page-break-inside: avoid a tr nel CSS così una riga non si divide tra pagine.
  • Font. Il renderer ha le famiglie Liberation, DejaVu, e Noto. Se referenzi Inter (come sopra) fa fallback su un sans di sistema. Per incorporare un font di brand, codifica in base64 un .woff2 e mettilo inline via @font-face.

Passo 4 - Genera il PDF da fuori dell'app (5 min)

Il template è ora invocabile per nome da qualsiasi client HTTP. Tre endpoint, tutti sotto /api/v1/pdf-templates:

  • POST /api/v1/pdf-templates/{id}/preview - renderizza il template per il suo ID di database e ritorna application/pdf. Usato dal pannello di anteprima dell'editor.
  • POST /api/v1/pdf-templates/generate/{name} - renderizza per nome e ritorna application/pdf (download del browser).
  • POST /api/v1/pdf-templates/generate/{name}/base64 - stesso ma ritorna { "data": "<base64>" }. Utile quando il chiamante vuole incorporare il PDF in un altro payload di risposta.

Da una shell, con un Bearer token dall'app admin:

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

Da un sistema esterno (un'integrazione Zapier, il flusso di conferma ordine di un partner), chiama lo stesso endpoint con i parametri nel body JSON. La piattaforma esegue il data script con quei parametri, renderizza l'HTML, e fa stream del PDF indietro.

Modalità trial: sia l'endpoint generate sia l'endpoint {id}/preview ritornano 403 nell'ambiente trial, quindi né la chiamata esterna curl né il pannello di anteprima dell'editor ritorneranno un PDF finché non sei su un ambiente a pagamento. I metadata del template (data script, HTML, settings) si salvano comunque normalmente, è il rendering che è controllato.

Riepilogo

  • Hai esteso lo schema con una tabella figlia (deal_line) e usato un trigger Before per tenere un campo derivato in sync.
  • Hai costruito un template PDF, un data script che tira le righe giuste, un body HTML che le renderizza con i placeholder Scriban.
  • Hai imparato gli endpoint REST per invocare il template da qualsiasi parte, incluso l'endpoint di preview che alimenta l'editor stesso.

Dove andare dopo

  • Reference dei PDF Templates - copre la superficie Scriban completa, la gestione dei font, trucchi per i page break, e la tab Settings dell'editor (margini, formato carta, orientamento).
  • Scheduled Events - chiama l'endpoint generate del PDF da uno Script Module e invia il risultato per email al customer su pianificazione mensile.