Gerar faturas em PDF
Este tutorial adiciona uma fatura imprimível em PDF para cada Deal no mini-CRM que construiu em Construa a sua primeira aplicação. No fim terá um template de fatura que puxa a empresa, as linhas e os totais de um deal, renderizado ao vivo no painel de pré-visualização do editor e invocável a partir de sistemas externos via REST.
Tempo total: ~25 minutos. Vai tocar em PDF Templates e estender o schema com uma tabela de linhas.
Limitação de trial: a renderização de PDF está desativada dentro do ambiente de trial para evitar sobrecarregar o pool partilhado de Chromium headless. Tanto o painel de pré-visualização ao vivo do editor (POST /api/v1/pdf-templates/{id}/preview) como o endpoint públicoPOST /api/v1/pdf-templates/generate/<name>devolvem 403 até estar num ambiente pago. Pode continuar a autorar o template (data script, HTML, definições, parâmetros) e guardá-lo, o painel de pré-visualização mostrará o 403 até a renderização ser reativada. Planeie verificar o output renderizado num ambiente não-trial antes de depender dele.
Pré-requisitos
Deverá ter o mini-CRM de Construa a sua primeira
aplicação deployado: tabelas company, contact, deal;
as suas Business Entities; as três páginas. Se saltou esse tutorial, o resto desta página não
fará muito sentido.
Passo 1 - Adicionar linhas ao schema (5 min)
Uma fatura a sério tem linhas, não apenas um único montante. Adicione uma tabela
deal_line.
- Abra Schema Designer → Add Table → nomeie-a
deal_line. -
No separador Columns do painel à direita, adicione:
id· SERIAL, PK (auto)deal_id· INTEGER, obrigatóriodescription· VARCHAR(300), obrigatórioquantity· DECIMAL, obrigatório, default1unit_price· DECIMAL, obrigatório, default0line_total· DECIMAL, obrigatório, default0- derivado, mantido em sincronização abaixo
-
Mude para o separador Relations. Adicione uma relação: FK column
deal_id, Relation type One-to-Many, References tabledeal, References columnid. Clique em Add Relationship. - Clique em Deploy, reveja o Generated SQL, clique em Deploy no canto superior direito.
Ligar a BE, a página e um pequeno evento
- Business Entities → Create. Entity Name
deal_line, Master Tabledeal_line, Label Columndescription. Guarde. -
Abra a página Deals existente no Page Editor. No formulário de detalhe,
clique em Add Tab, nomeie o separador Lines, e adicione uma secção
RelatedGrid ligada a
deal_linecom o filtro de join deal.id = id do registo atual. Guarde e volte a publicar. - Business Events → Create. Rule Name
Calc deal_line.line_total. Enabled ligado. Business Entitydeal_line. Triggers: Before Create + Before Update. Sem condições. Ação: Execute Script com o corpo:
Guarde.Entity.line_total = (decimal)(Entity.quantity ?? 0) * (decimal)(Entity.unit_price ?? 0);
Clique num Deal, mude para o novo separador Lines, e adicione duas ou três linhas. A coluna
line_total deverá ser preenchida automaticamente cada vez que guarda uma linha.
Passo 2 - Escrever o data script do PDF (5 min)
Abra PDF Templates → clique em + Create. O editor abre com um
campo Name no topo (escreva DealInvoice), um campo Description e cinco separadores:
HTML Template, Data Script, Settings,
Params, JSON. O painel PDF Preview está
sempre visível à direita e re-renderiza quando guarda.
Mude para o separador Params → clique em + Add. Name
deal_id, Type int, Required marcado. (A etiqueta do separador atualiza
para Params (1) assim que define um.)
Mude para o separador Data Script, o editor Monaco de C# abre à esquerda, com a pré-visualização de PDF ainda à direita. Cole:
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
}; O script devolve um objeto anónimo. O que quer que devolva torna-se o contexto de dados para o template HTML, cada propriedade é acessível pelo nome em placeholders Scriban.
Porquê todos os casts?Db.GetAsynce.ToListAsync()devolvemdynamic, que o Scriban pode por vezes manusear mal quando um valor é nulo ou o seu tipo em runtime não é o que o template espera. Fazer cast para um tipo concreto na fronteira (onde constrói o objeto de retorno) dá-lhe comportamento previsível.
Passo 3 - Escrever o template HTML (10 min)
No editor HTML Template, cole isto (o estilo é a maior parte dos 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> Ver o resultado
Clique no botão verde Update (ou Create num template novo) no canto superior direito para guardar. O painel PDF Preview à direita re-renderiza. Uma coluna de thumbnails no rebordo esquerdo da pré-visualização mostra a página 1, página 2, etc.; o painel principal mostra a renderização completa com uma barra de ferramentas de visualizador de PDF integrada (zoom, rotação, download, imprimir, mais). Itere editando o separador HTML ou Data Script e guardando de novo.
Os botões de ícone no canto superior direito do editor (junto a Cancel) permitem-lhe alternar a visibilidade do painel de pré-visualização e mudar para o modo de edição em ecrã inteiro. O ícone de ajuda (?) abre um cheat-sheet in-line para a sintaxe Scriban.
Coisas que muitas vezes precisam de ajuste:
- Formatação de números. Os decimais voltam como números em bruto; se quer apresentação fixa em 2 casas decimais, formate no data script (
l.line_total.ToString("0.00")) e devolva strings.- Símbolo de moeda. Hardcoded como € acima, externalize para um parâmetro ou definição de Branding se servir várias moedas.
- Quebras de página. Para listas de linhas longas, adicione
page-break-inside: avoidatrno CSS para que uma linha não se divida entre páginas.- Fontes. O renderizador tem as famílias Liberation, DejaVu e Noto. Se referencia Inter (como acima) cai para uma sans do sistema. Para embutir uma fonte de marca, codifique em base64 um
.woff2e coloque-o in-line via@font-face.
Passo 4 - Gerar o PDF a partir de fora da app (5 min)
O template é agora invocável pelo nome a partir de qualquer cliente HTTP. Três endpoints, todos
sob /api/v1/pdf-templates:
-
POST /api/v1/pdf-templates/{id}/preview- renderiza o template pelo seu ID em base de dados e devolveapplication/pdf. Usado pelo painel de pré-visualização do editor. -
POST /api/v1/pdf-templates/generate/{name}- renderiza por nome e devolveapplication/pdf(download de browser). -
POST /api/v1/pdf-templates/generate/{name}/base64- igual mas devolve{ "data": "<base64>" }. Útil quando o chamador quer embutir o PDF noutro payload de resposta.
A partir de uma shell, com um Bearer token da aplicação 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 A partir de um sistema externo (uma integração Zapier, o fluxo de confirmação de encomenda de um parceiro), chame o mesmo endpoint com os parâmetros no body JSON. A plataforma corre o data script com esses parâmetros, renderiza o HTML, e devolve o PDF em stream.
Modo trial: tanto o endpointgeneratecomo o endpoint{id}/previewdevolvem 403 no ambiente de trial, por isso nem a chamada externacurlnem o painel de pré-visualização do editor devolverão um PDF até estar num ambiente pago. Os metadados do template (data script, HTML, definições) continuam a ser guardados normalmente, a renderização é o que está bloqueado.
Recapitulação
- Estendeu o schema com uma tabela filha (
deal_line) e usou um trigger Before para manter um campo derivado em sincronização. - Construiu um PDF template, um data script que puxa as linhas certas, um corpo HTML que as renderiza com placeholders Scriban.
- Aprendeu os endpoints REST para invocar o template de qualquer lado, incluindo o endpoint de pré-visualização que alimenta o próprio editor.
Para onde ir a seguir
- Referência PDF Templates - cobre a superfície completa do Scriban, gestão de fontes, truques de quebra de página e o separador Settings do editor (margens, formato de papel, orientação).
- Scheduled Events - chame o endpoint de geração de PDF a partir de um Script Module e envie por email o resultado ao cliente num horário mensal.