Genera facturas en PDF
Este tutorial añade una factura en PDF imprimible para cada Deal en el mini-CRM que construiste en Construye tu primera aplicación. Al final tendrás una plantilla de factura que extrae la empresa de una deal, las líneas y los totales, renderizada en vivo en el panel de vista previa del editor e invocable desde sistemas externos vía REST.
Tiempo total: ~25 minutos. Tocarás PDF Templates y extenderás el esquema con una tabla de líneas.
Limitación de la prueba: el renderizado de PDF está desactivado dentro del entorno de prueba para evitar que el pool compartido de Chromium headless se sobrecargue. Tanto el panel de vista previa en vivo del editor (POST /api/v1/pdf-templates/{id}/preview) como el endpoint públicoPOST /api/v1/pdf-templates/generate/<name>devuelven 403 hasta que estés en un entorno de pago. Puedes seguir creando la plantilla (data script, HTML, ajustes, parámetros) y guardarla, el panel de vista previa mostrará el 403 hasta que se reactive el renderizado. Planifica verificar la salida renderizada en un entorno no de prueba antes de confiar en ella.
Prerrequisitos
Deberías tener el mini-CRM de Construye tu primera aplicación
desplegado: tablas company, contact, deal; sus Business
Entities; las tres páginas. Si te saltaste ese tutorial, el resto de esta página no tendrá
mucho sentido.
Paso 1 - Añade líneas al esquema (5 min)
Una factura real tiene líneas, no solo un único importe. Añade una tabla deal_line.
- Abre Schema Designer -> Add Table -> nómbrala
deal_line. -
En la pestaña Columns del panel derecho, añade:
id· SERIAL, PK (auto)deal_id· INTEGER, obligatoriodescription· VARCHAR(300), obligatorioquantity· DECIMAL, obligatorio, default1unit_price· DECIMAL, obligatorio, default0line_total· DECIMAL, obligatorio, default0- derivado, se mantiene sincronizado abajo
-
Cambia a la pestaña Relations. Añade una relación: columna FK
deal_id, Relation type One-to-Many, References tabledeal, References columnid. Haz clic en Add Relationship. - Haz clic en Deploy, revisa el Generated SQL, haz clic en Deploy arriba a la derecha.
Conecta la BE, la página y un pequeño evento
- Business Entities -> Create. Entity Name
deal_line, Master Tabledeal_line, Label Columndescription. Guarda. -
Abre la página Deals existente en Page Editor. En el formulario de detalle,
haz clic en Add Tab, nombra la pestaña Lines, y añade una sección
RelatedGrid vinculada a
deal_linecon el filtro de join deal.id = id del registro actual. Guarda y vuelve a publicar. - Business Events -> Create. Rule Name
Calc deal_line.line_total. Enabled activado. Business Entitydeal_line. Triggers: Before Create + Before Update. Sin condiciones. Acción: Execute Script con cuerpo:
Guarda.Entity.line_total = (decimal)(Entity.quantity ?? 0) * (decimal)(Entity.unit_price ?? 0);
Haz clic en una Deal, cambia a la nueva pestaña Lines y añade dos o tres líneas. La columna
line_total debería rellenarse automáticamente cada vez que guardes una línea.
Paso 2 - Escribe el data script del PDF (5 min)
Abre PDF Templates -> haz clic en + Create. El editor se abre con un
campo Name arriba (escribe DealInvoice), un campo Description y cinco pestañas:
HTML Template, Data Script, Settings,
Params, JSON. El panel PDF Preview es
siempre visible a la derecha y se vuelve a renderizar cuando guardas.
Cambia a la pestaña Params -> haz clic en + Add. Name
deal_id, Type int, Required marcado. (La etiqueta de la pestaña se
actualiza a Params (1) una vez has definido uno.)
Cambia a la pestaña Data Script, el editor Monaco C# se abre en la izquierda, con la vista previa del PDF aún a la derecha. Pega:
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
}; El script devuelve un objeto anónimo. Lo que devuelvas se convierte en el contexto de datos para la plantilla HTML, cada propiedad es accesible por nombre en los marcadores Scriban.
¿Por qué todos los casts?Db.GetAsyncy.ToListAsync()devuelvendynamic, lo que Scriban a veces puede manejar mal cuando un valor es null o su tipo en tiempo de ejecución no es lo que la plantilla espera. Hacer cast a un tipo concreto en el límite (donde construyes el objeto de retorno) te da un comportamiento predecible.
Paso 3 - Escribe la plantilla HTML (10 min)
En el editor HTML Template, pega esto (los estilos son el grueso de los 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> Mira el resultado
Haz clic en el botón verde Update (o Create en una plantilla nueva) arriba a la derecha para guardar. El panel PDF Preview a la derecha se vuelve a renderizar. Una columna de miniaturas en el borde izquierdo de la vista previa muestra la página 1, la página 2, etc.; el panel principal muestra el renderizado completo con una barra de herramientas de visor PDF integrada (zoom, rotar, descargar, imprimir, más). Itera editando la pestaña HTML o Data Script y guardando de nuevo.
Los botones con icono en la parte superior derecha del editor (junto a Cancel) te permiten conmutar la visibilidad del panel de vista previa y cambiar al modo de edición a pantalla completa. El icono de ayuda (?) abre una chuleta en línea para la sintaxis Scriban.
Cosas que a menudo necesitan un ajuste:
- Formato numérico. Los decimales vuelven como números crudos; si quieres una visualización fija de 2 decimales, formatea en el data script (
l.line_total.ToString("0.00")) y devuelve strings.- Símbolo de moneda. Codificado como € arriba, externalízalo a un parámetro o a un ajuste de Branding si sirves múltiples monedas.
- Saltos de página. Para listas de líneas largas, añade
page-break-inside: avoidatren el CSS para que una fila no se parta entre páginas.- Fuentes. El renderer tiene las familias Liberation, DejaVu y Noto. Si referencias Inter (como arriba) cae a una sans del sistema. Para embeber una fuente de marca, codifica en base64 un
.woff2e inclúyelo vía@font-face.
Paso 4 - Genera el PDF desde fuera de la aplicación (5 min)
La plantilla ya es invocable por nombre desde cualquier cliente HTTP. Tres endpoints, todos bajo
/api/v1/pdf-templates:
-
POST /api/v1/pdf-templates/{id}/preview- renderiza la plantilla por su ID de base de datos y devuelveapplication/pdf. Usado por el panel de vista previa del editor. -
POST /api/v1/pdf-templates/generate/{name}- renderiza por nombre y devuelveapplication/pdf(descarga del navegador). -
POST /api/v1/pdf-templates/generate/{name}/base64- igual pero devuelve{ "data": "<base64>" }. Útil cuando el llamador quiere embeber el PDF en otro payload de respuesta.
Desde un shell, con un token Bearer de la aplicación de administración:
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 Desde un sistema externo (una integración Zapier, el flujo de confirmación de pedido de un socio), llama al mismo endpoint con los parámetros en el cuerpo JSON. La plataforma ejecuta el data script con esos parámetros, renderiza el HTML y transmite el PDF de vuelta.
Modo de prueba: tanto el endpointgeneratecomo el endpoint{id}/previewdevuelven 403 en el entorno de prueba, así que ni la llamada externacurlni el panel de vista previa del editor devolverán un PDF hasta que estés en un entorno de pago. Los metadatos de la plantilla (data script, HTML, ajustes) siguen guardándose con normalidad, lo que está restringido es el renderizado.
Resumen
- Extendiste el esquema con una tabla hija (
deal_line) y usaste un trigger Before para mantener un campo derivado sincronizado. - Construiste una plantilla PDF, un data script que extrae las filas correctas, un cuerpo HTML que las renderiza con marcadores Scriban.
- Aprendiste los endpoints REST para invocar la plantilla desde cualquier lugar, incluido el endpoint de vista previa que alimenta al propio editor.
Hacia dónde ir después
- Referencia de PDF Templates - cubre la superficie Scriban completa, el manejo de fuentes, trucos de saltos de página y la pestaña Settings del editor (márgenes, formato de papel, orientación).
- Scheduled Events - llama al endpoint generate del PDF desde un Script Module y envía el resultado por email al cliente con un horario mensual.