En este post de programación con JavaScript del lado del servidor con Node.js y un poco de express te mostraré cómo crear una factura, ticket o recibo PDF usando estas tecnologías.
Voy a enseñarte dos ejemplos. En el primero verás cómo crear un recibo PDF y guardarlo en el almacenamiento, mientras que en el segundo ejemplo te enseñaré mostrar el ticket PDF con Express.
Esto solo es una continuación a mi post de cómo crear un PDF con Node. Recuerda que tú eres libre de cambiar los estilos y modificar la plantilla, lo que aquí te muestro es mi ejemplo.
Diseño de la plantilla
Primero vamos a diseñar la plantilla HTML y después le vamos a pasar los datos desde Node, específicamente le pasaremos la lista de productos y los totales.
Al inicio se ve así:
<!DOCTYPE html>
<html lang="es">
<head>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css"
integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Factura</title>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-xs-10 ">
<h1>Factura</h1>
</div>
<div class="col-xs-2">
<img class="img img-responsive" src="https://github.com/parzibyte.png" alt="Logotipo">
</div>
</div>
<hr>
<div class="row">
<div class="col-xs-10">
<h1 class="h6">Luis Cabrera Benito</h1>
<h1 class="h6">https://parzibyte.me/blog</h1>
</div>
<div class="col-xs-2 text-center">
<strong>Fecha</strong>
<br>
2021-05-03 <br>
<strong>Factura No.</strong>
<br>
1
</div>
</div>
<hr>
<div class="row text-center" style="margin-bottom: 2rem;">
<div class="col-xs-6">
<h1 class="h2">Cliente</h1>
<strong>Luis Cabrera Benito</strong>
</div>
<div class="col-xs-6">
<h1 class="h2">Remitente</h1>
<strong>Luis Cabrera Benito</strong>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<table class="table table-condensed table-bordered table-striped">
<thead>
<tr>
<th>Descripción</th>
<th>Cantidad</th>
<th>Precio unitario</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{{tablaProductos}}
</tbody>
<tfoot>
<tr>
<td colspan="3" class="text-right">Subtotal</td>
<td>{{subtotal}}</td>
</tr>
<tr>
<td colspan="3" class="text-right">Descuento</td>
<td>{{descuento}}</td>
</tr>
<tr>
<td colspan="3" class="text-right">Subtotal con descuento</td>
<td>{{subtotalConDescuento}}</td>
</tr>
<tr>
<td colspan="3" class="text-right">Impuestos</td>
<td>{{impuestos}}</td>
</tr>
<tr>
<td colspan="3" class="text-right">
<h4>Total</h4>
</td>
<td>
<h4>{{total}}</h4>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="row">
<div class="col-xs-12 text-center">
<p class="h5">Gracias por su compra</p>
</div>
</div>
</div>
</body>
</html>
Si te fijas estoy usando el framework Bootstrap 3 para demostrar que podemos usar cualquier hoja de estilos. Obviamente puedes usar cualquier hoja de estilos o no usar estilos.
También fíjate en que estoy colocando variables como {{total}}
o {{tablaProductos}}
, mismas que vamos a pasar desde JavaScript del lado del servidor más adelante.
Creando factura con Node
Ahora simplemente vamos a leer el HTML de la plantilla, pasarle los valores (que al final será HTML) y luego convertirla a PDF. Te repito que aquí estoy suponiendo que ya leíste mi otro post sobre la creación de PDFs con JS.
/**
* https://parzibyte.me/blog
*/
const pdf = require("html-pdf");
const fs = require("fs");
const ubicacionPlantilla = require.resolve("./factura.html");
let contenidoHtml = fs.readFileSync(ubicacionPlantilla, 'utf8')
// Estos productos podrían venir de cualquier lugar
const productos = [
{
descripcion: "Nintendo Switch",
cantidad: 2,
precio: 9000,
},
{
descripcion: "Videojuego: Hollow Knight",
cantidad: 1,
precio: 150,
},
{
descripcion: "Audífonos HyperX",
cantidad: 5,
precio: 1500,
},
];
// Nota: el formateador solo es para, valga la redundancia, formatear el dinero. No es requerido, solo que quiero que se vea bonito
// https://parzibyte.me/blog/2021/05/03/formatear-dinero-javascript/
const formateador = new Intl.NumberFormat("en", { style: "currency", "currency": "MXN" });
// Generar el HTML de la tabla
let tabla = "";
let subtotal = 0;
for (const producto of productos) {
// Aumentar el total
const totalProducto = producto.cantidad * producto.precio;
subtotal += totalProducto;
// Y concatenar los productos
tabla += `<tr>
<td>${producto.descripcion}</td>
<td>${producto.cantidad}</td>
<td>${formateador.format(producto.precio)}</td>
<td>${formateador.format(totalProducto)}</td>
</tr>`;
}
const descuento = 0;
const subtotalConDescuento = subtotal - descuento;
const impuestos = subtotalConDescuento * 0.16
const total = subtotalConDescuento + impuestos;
// Remplazar el valor {{tablaProductos}} por el verdadero valor
contenidoHtml = contenidoHtml.replace("{{tablaProductos}}", tabla);
// Y también los otros valores
contenidoHtml = contenidoHtml.replace("{{subtotal}}", formateador.format(subtotal));
contenidoHtml = contenidoHtml.replace("{{descuento}}", formateador.format(descuento));
contenidoHtml = contenidoHtml.replace("{{subtotalConDescuento}}", formateador.format(subtotalConDescuento));
contenidoHtml = contenidoHtml.replace("{{impuestos}}", formateador.format(impuestos));
contenidoHtml = contenidoHtml.replace("{{total}}", formateador.format(total));
pdf.create(contenidoHtml).toFile("salida.pdf", (error) => {
if (error) {
console.log("Error creando PDF: " + error)
} else {
console.log("PDF creado correctamente");
}
});
Los comentarios y el código en sí ayudan a explicar toda esta generación del recibo de compra creado con Node.
Primero leemos el contenido del HTML y creamos el HTML necesario de la tabla de productos, mismo que será incluido en la cadena HTML usando replace
.
Luego calculamos el total, impuestos, descuentos, etcétera y los formateamos como moneda. Finalmente convertimos el PDF a un archivo e imprimimos si la operación fue correcta.
Aquí puedes ver el PDF que resulta: Factura de ejemplo creada con Node.js
Sirviendo factura con Express
Para terminar este tutorial te voy a enseñar cómo mostrar esta factura PDF creada con Node.js directamente en el navegador usando Express. En lugar de invocar a toFile
, invocamos a toStream
así:
/**
* Tomado de https://parzibyte.me/blog/2019/05/27/node-js-express-ejemplo-creacion-proyecto/
* By Parzibyte
*/
// Importar dependencias
const express = require("express"),
app = express(),
pdf = require("html-pdf"),
fs = require("fs");
// Constantes propias del programa
const ubicacionPlantilla = require.resolve("./factura.html"),
puerto = 3000;
let contenidoHtml = fs.readFileSync(ubicacionPlantilla, 'utf8')
// Estos productos podrían venir de cualquier lugar
const productos = [
{
descripcion: "Nintendo Switch",
cantidad: 2,
precio: 9000,
},
{
descripcion: "Videojuego: Hollow Knight",
cantidad: 1,
precio: 150,
},
{
descripcion: "Audífonos HyperX",
cantidad: 5,
precio: 1500,
},
];
// Nota: el formateador solo es para, valga la redundancia, formatear el dinero. No es requerido, solo que quiero que se vea bonito
// https://parzibyte.me/blog/2021/05/03/formatear-dinero-javascript/
const formateador = new Intl.NumberFormat("en", { style: "currency", "currency": "MXN" });
// Definir rutas
app.get('/', (peticion, respuesta) => {
// Generar el HTML de la tabla
let tabla = "";
let subtotal = 0;
for (const producto of productos) {
// Aumentar el total
const totalProducto = producto.cantidad * producto.precio;
subtotal += totalProducto;
// Y concatenar los productos
tabla += `<tr>
<td>${producto.descripcion}</td>
<td>${producto.cantidad}</td>
<td>${formateador.format(producto.precio)}</td>
<td>${formateador.format(totalProducto)}</td>
</tr>`;
}
const descuento = 0;
const subtotalConDescuento = subtotal - descuento;
const impuestos = subtotalConDescuento * 0.16
const total = subtotalConDescuento + impuestos;
// Remplazar el valor {{tablaProductos}} por el verdadero valor
contenidoHtml = contenidoHtml.replace("{{tablaProductos}}", tabla);
// Y también los otros valores
contenidoHtml = contenidoHtml.replace("{{subtotal}}", formateador.format(subtotal));
contenidoHtml = contenidoHtml.replace("{{descuento}}", formateador.format(descuento));
contenidoHtml = contenidoHtml.replace("{{subtotalConDescuento}}", formateador.format(subtotalConDescuento));
contenidoHtml = contenidoHtml.replace("{{impuestos}}", formateador.format(impuestos));
contenidoHtml = contenidoHtml.replace("{{total}}", formateador.format(total));
pdf.create(contenidoHtml).toStream((error, stream) => {
if (error) {
respuesta.end("Error creando PDF: " + error)
} else {
respuesta.setHeader("Content-Type", "application/pdf");
stream.pipe(respuesta);
}
});
});
// Una vez definidas nuestras rutas podemos iniciar el servidor
app.listen(puerto, err => {
if (err) {
// Aquí manejar el error
console.error("Error escuchando: ", err);
return;
}
// Si no se detuvo arriba con el return, entonces todo va bien ;)
console.log(`Escuchando en el puerto :${puerto}`);
});
Ahora al ejecutar el script y visitar localhost:3000 tenemos el PDF mostrado:
Poniendo todo junto
Ya te mostré el código, pero si quieres tener el código completo incluyendo al package.json
para que puedas instalar las dependencias con un npm install
te lo dejo en GitHub.
Igualmente te dejo con más posts de JavaScript.
Como puedo lograr que el pdf sea “horizontal” ? llevo varias buscando info con esta libreria y no puedo :/
Hola. Si tiene alguna consulta puede hacérmela llegar en https://parzibyte.me/#contacto
Saludos!
pregunta, se puede crear un voucher o ticket de venta con mas 20 item, sin necesidad de que salga hoja x hoja solo en una sola tira?
Yo creo que sí