Guía R-007 – Hacer informes con PDFMAKE

En esta búsqueda de completar las funcionalidades de los desarrollos de React me faltaba disponer de una solución para generar informes.

Siempre pensé que de todas las soluciones que dispongo para realizar informes en PHP (publicado para PHPRunner), serían suficientes y así en parte lo es, porque todas ellas se pueden utilizar, pero deseaba disponer de alguna en React, sencilla, fácil de utilizar y muy rápida, y creo que la he conseguido. Este ejemplo es un caso sencillo de hacer la simulación de una factura con el producto PDFMake.

Objetivo

Disponer de una solución para crear informes, documentos, etc., principalmente en PDF desde el entorno de React.

DEMO:  https://fhumanes.com/factura-pdfmake/

Solución Técnica

Como os he indicado, he utilizado el producto PDFMake. En React me costó mucho incorporar las fuentes para utilizar en el PDF, pero con la IA DeepSeek y muchas pruebas de las soluciones que me aportaba, conseguí incorporar las fuentes y a partir de ahí, todo han sido «alegrías», ya que ví lo sencillo y potente de la solución, aunque no sirve para todo tipo de informe.

El ejemplo es básico, un botón y la elaboración de la factura cuyos datos son fijos, pero muy fácil de hacerlos dinámicos.

Una de las aportaciones que hace esta solución es que puedes «diseñar» o escribir la definición del informe en interactivo en este enlace (http://pdfmake.org/playground.html) y una vez que lo tengas «ajustado» puedes llevar esa definición al código de la aplicación. Esta alternativa ayuda mucho a las personas que van a realizar pocos informes. La documentación es buena y tiene muchos ejemplos .

El código que produce la factura es este:

import { React, useEffect } from 'react';
import  * as pdfMake from 'pdfmake/build/pdfmake';
import { loadFonts } from './utils/loadFonts';

// Formateado de los valores númericos en el informe
let decimal = new Intl.NumberFormat('de-DE', {
    style: 'currency',
    currency: 'eur',
    minimumFractionDigits: 2,
    maximumFractionDigits: 2
});

const Factura = () => {

  // Datos de la Factura
  const facturaData = {
    numero: 'F-2023-001',
    fecha: new Date().toLocaleDateString(),
    cliente: 'Juan Pérez',
    nit: '123456789',
    items: [
      { descripcion: 'Laptop HP', cantidad: 1, precio: 1200 },
      { descripcion: 'Mouse inalámbrico', cantidad: 2, precio: 25 },
      { descripcion: 'Teclado mecánico', cantidad: 1, precio: 80 },
      { descripcion: 'Laptop HP', cantidad: 1, precio: 1200 },
      { descripcion: 'Mouse inalámbrico', cantidad: 2, precio: 25 },
      { descripcion: 'Teclado mecánico', cantidad: 1, precio: 80 },
      { descripcion: 'Laptop HP', cantidad: 1, precio: 1200 },
      { descripcion: 'Mouse inalámbrico', cantidad: 2, precio: 25 },
      { descripcion: 'Teclado mecánico', cantidad: 1, precio: 80 },
      { descripcion: 'Laptop HP', cantidad: 1, precio: 1200 },
      { descripcion: 'Mouse inalámbrico', cantidad: 2, precio: 25 },
      { descripcion: 'Teclado mecánico', cantidad: 1, precio: 80 },
      { descripcion: 'Laptop HP', cantidad: 10, precio: 1200 },
      { descripcion: 'Mouse inalámbrico', cantidad: 20, precio: 25 },
      { descripcion: 'Teclado mecánico', cantidad: 30, precio: 80 },
      { descripcion: 'Laptop HP', cantidad: 1, precio: 1200 },
      { descripcion: 'Mouse inalámbrico', cantidad: 2, precio: 25 },
      { descripcion: 'Teclado mecánico', cantidad: 1, precio: 80 },
      { descripcion: 'Laptop HP', cantidad: 1, precio: 1200 },
      { descripcion: 'Mouse inalámbrico', cantidad: 2, precio: 25 },
      { descripcion: 'Teclado mecánico', cantidad: 1, precio: 80 },
    ],
  };

    useEffect(() => {
      loadFonts(); // Precarga las fuentes al montar el componente
    }, []);

  const generarFactura = async () => {
    await loadFonts(); // Asegura que las fuentes estén cargadas

    const subtotal = facturaData.items.reduce((sum, item) => sum + (item.precio * item.cantidad), 0);
    const iva = subtotal * 0.16;
    const total = subtotal + iva;

    // Definición del Informe
    const docDefinition = {
        // a string or { width: number, height: number }
        pageSize: 'A5',
        // by default we use portrait, you can change it to landscape if you wish
        pageOrientation: 'landscape',
        // [left, top, right, bottom] or [horizontal, vertical] or just a number for equal margins
        pageMargins: [ 50, 30, 20, 40 ],
        
        footer: function(currentPage, pageCount) { 
            return [
                {text: 'Página: '+currentPage.toString() + ' de ' + pageCount ,margin: [30,10,10,0],alignment: 'right' }
                ]
        },
        header: function(currentPage, pageCount, pageSize) {
            // you can apply any logic and return any valid pdfmake element
            return [
            { text: 'simple text',margin: [30,10,30,10], alignment: (currentPage % 2) ? 'left' : 'right' },
            { canvas: [ { type: 'rect', x: 170, y: 32, w: pageSize.width - 170, h: 40 } ] }
            ]
        },
            
      content: [
        {
            layout: 'noBorders', // optional
            table: {
              widths: ['auto', '*', 'auto'],
              body: [
                  [ 
                    { image: 'data:image/png;base64,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' ,
                         width: 75 },
                  { text: '\nFACTURA/INVOICE', style: 'header' },
                  { text: `\nNúmero: ${facturaData.numero}`, margin: [0, 10], color: 'blue'},
                  ]
                  ]
            },
          },
          { text: '\n\n' },
        { text: `Fecha: ${facturaData.fecha}` },
        { text: `Cliente: ${facturaData.cliente}` },
        { text: `NIT: ${facturaData.nit}`, margin: [0, 0, 0, 20] },
        
        {
          layout: 'lightHorizontalLines', // optional
          table: {
            headerRows: 1,
            widths: ['*', 'auto', 'auto', 'auto'],
            body: [
                [
                    { text: 'Descripción', style: 'tableHeader' },
                    { text: 'Cantidad', style: 'tableHeader', alignment: 'center' },
                    { text: 'Precio', style: 'tableHeader', alignment: 'right' },
                    { text: 'Total', style: 'tableHeader', alignment: 'right' }
                ],
              ...facturaData.items.map(item => 
                [
                    item.descripcion,
                    { text: item.cantidad ,alignment: 'center'},
                    { text: decimal.format(item.precio),alignment: 'right'} ,
                    { text: decimal.format(item.cantidad * item.precio),alignment: 'right'} ,
                ]),
                  // Asegúrate de que cada fila tenga exactamente 4 elementos:
                [{ text: 'Subtotal:', alignment: 'right', colSpan: 3 },'', '',  { text: decimal.format(subtotal),alignment: 'right'}],
                [{ text: 'IVA (16%):', alignment: 'right', colSpan: 3 },'', '',  { text: decimal.format(iva),alignment: 'right'}],
                [{ text: 'Total:', alignment: 'right', colSpan: 3, bold: true },'', '',  { text: decimal.format(total),alignment: 'right', bold: true}],

            ],
          },
        },
    
      ],
      styles: {
        header: { 
            fontSize: 18, 
            font: 'Roboto', 
            bold: true,
            alignment: 'center' },
        tableHeader: {
            font: 'Roboto', 
            bold: true,
            fontSize: 13,
            fillColor: '#dddddd'
        } 
      },
    };

    pdfMake.createPdf(docDefinition).download('factura.pdf'); // Descarga directa
  };

  return (
    <div>
      <h2>Generador de Factura</h2>
      <button onClick={generarFactura}>Descargar Factura</button>
    </div>
  );
};

export default Factura;

En el ejemplo podréis ver imágenes (en este caso embebida en el documento), definición de cabeceras y pies de página con su contador de páginas, también distintas ediciones de números y textos.

Hay otras muchas características que no he incluido como gráficos QR, protección y cifrado del documento PDF, definición de características del documento PDF, etc.

Creo que lo he transmitido en el texto, me asombra la sencillez de la definición del documento y lo rápido y eficiente, que produce el documento, más cuando todo está desarrollado en JavaScript, que al principio, parecía que era para «mover texto en el navegador».

Como siempre , os dejo el ejemplo para que lo podáis descargar en vuestros PC’s y podáis hacer lo ajustes y pruebas que consideréis adecuado.

Adjuntos

Archivo Tamaño de archivo Descargas
zip factura-pdfmake - REACT 528 KB 23

Blog personal para facilitar soporte gratuito a usuarios de React y PHPRunner