Crear factura con Setasign PDF (nueva versión)

La versión anterior ha tenido bastante éxito, pero era un ejemplo bastante limitado y he tenido varias preguntas sobre todo, por la limitación de la factura a una única página.

En este nuevo ejemplo intento que sea mucho más funcional, resuelvo que sólo sea una página y hago que todo sea mucho más sencillo de utilizar.

Objetivo

Hacer una factura (informe) en PDF con la mayor calidad de documento PDF, utilizando una plantilla y de muy poco código PHP para hacer el informe.

Utilización de la librería FPDI de SETASIGN , que es 100% PHP y que su ejecución es muy rápida.

DEMO: https://fhumanes.com/invoice_pdf

DEMO: https://fhumanes.com/reports_setasign/  (informes hecho con esta solución)

Solución Técnica.

Para la elaboración de la plantilla de PDF he utilizado Excel (facilito ejemplo).

Para medir los puntos donde se debe rellenar el documento PDF, he utilizado el  programa PDFill ( Editor basic) 

Para utilizar este producto lo he configurado para medir las distancias por «puntos».

Las medidas de los puntos se obtienen:

(1).- Se selecciona la optación de añadir texto al documento PDF.

(2).- Con el ratón se posiciona (sin hacer clic) en el punto en donde deseamos fijar el contenido variable.

(3).- Informa del tamaño de la página.

(4).- Informa de las coordenadas del punto que ha marcado con el ratón y valores que tenemos que trasladar a nuestro programa.

Para simplificar la impresiones he creado una función que dependiendo del tipo de datos hace la «impresión» en la página del PDF.

PDF_functions.php
<?php
/*
Param:

Type = char | number | integer | date | dateTime | time | dateLong | boolean | memo
NumberDecimal = 0
Align = L left | R right | C center
FontStyle = 'B' | 'I' | 'U' | ''
Font = 'helvetica'
FontSize = 10
ColorRGB = "0,0,0"
RightMargin = 0  Especial fields MEMO

*/
function print_pdf($point_x = 1, $point_y = 1, $value = 0, $type = 'char', $numberDecimal = 0 ,
                   $align = 'L', $fontStyle = '', $font = 'helvetica', $fontSize = 11, $colorRGB = "0,0,0", $rightMargin = 0 )
{
    $pdf = $GLOBALS['pdf'];
    $coef_x = $GLOBALS['coef_x'];
    $coef_y = $GLOBALS['coef_y'];
    $wPt  = $GLOBALS['wPt'];
    /*
    $numeric_symbol_of_thousands = $GLOBALS['numeric_symbol_of_thousands'];
    $numeric_decimal_symbol = $GLOBALS['numeric_decimal_symbol'];
    $date_format = $GLOBALS['date_format'];
    $date_and_time_format = $GLOBALS['date_and_time_format'];
    $time_format = $GLOBALS['time_format'];
    $long_Date_Format = $GLOBALS['long_Date_Format'];
    */

    date_default_timezone_set( $GLOBALS['date_default_timezone_set'] );
    setlocale(LC_TIME, $GLOBALS['setlocale_LC_TIME']);

    if ( $align == 'R' ) {  
        if ($type == 'number' || $type == 'integer') {
        $rightMargin =  $wPt - $point_x ;  // Obtain the distance from the right margin from the osition of the field.
      }
    }

    $pdf->SetXY($point_x/$coef_x, $point_y/$coef_y);             // Positioning on the page
    $pdf->SetMargins($point_x/$coef_x,5,$rightMargin/$coef_x);   // For fields MEMO
    $pdf->SetFont($font,$fontStyle,$fontSize);                   //  Font, type and size
    $FontColor = explode(",", $colorRGB);                      
    $pdf->SetTextColor($FontColor[0],$FontColor[1],$FontColor[2]); // Color in R, G, B

    switch ($type) {
    case 'char': // Char
        $pdf->Cell(0,0,$value,0,1,$align);
        break;
    case 'number': // Num
        $value = number_format($value, $numberDecimal ,$GLOBALS['numeric_decimal_symbol'] ,$GLOBALS['numeric_symbol_of_thousands']);
        $pdf->Cell(0,0,$value,0,1,$align);
         break;
    case 'integer': // Integer
        $pdf->Cell(0,0,$value,0,1,$align);
         break;
    case 'date': // Date
        $date = new DateTimeImmutable($value);
        $value = $date->format($GLOBALS['date_format']);
        $pdf->Cell(0,0,$value,0,1,$align);
        break;
     case 'time': // Time
        $date = new DateTimeImmutable($value);
        $value = $date->format($GLOBALS['time_format']);
        $pdf->Cell(0,0,$value,0,1,$align);
        break;
    case 'dateTime': // DateTime
        $date = new DateTimeImmutable($value);
        $value = $date->format($GLOBALS['date_and_time_format']);
        $pdf->Cell(0,0,$value,0,1,$align);
        break;   
    case 'dateLong': // Date Long
        $date = new DateTimeImmutable($value);
        $value = $date->format($GLOBALS['long_Date_Format']);
        $pdf->Cell(0,0,$value,0,1,$align);
        break;   
    case 'boolean': // Boolean
        $pdf->SetFont('ZapfDingbats',$fontStyle,$fontSize);     //  Font, type and size
        if ($value == 1) {$value = '4';} else {$value = '';}    // 'l' Punto negro
        $pdf->Cell(0,0,$value,0,1,$align);
        break;   
    case 'memo': // Memo
        $pdf->Write(5, $value);
        break;   
    default:
        $pdf->Cell(0,0,$value,0,1,$align);
    }
}

El código que crea la factura ha quedado bastante sencillo, siendo este.

InvoicePDF.php
<?php
// use setasign\Fpdi\Fpdi;
use setasign\Fpdi\Tcpdf\Fpdi;

require_once __DIR__ . '/fpdi_2.3.7/autoload.php';

require_once __DIR__ . '/PDF_functions.php';   // Function Print into PDF

// initiate FPDI
$pdf = new Fpdi();

// Very important
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(false);

// set the source file
$template_pdf = __DIR__.'/TemplateInvoice.pdf';
$pdf->setSourceFile($template_pdf);
// import page 1
$tplIdx = $pdf->importPage(1);
// get the size of the imported page
$size = $pdf->getTemplateSize($tplIdx);
// add a page with the same orientation and size
$pdf->AddPage($size['orientation'], $size);
// use the imported page
$pdf->useTemplate($tplIdx);

// Obtain measures from the page for the transformation of the Points
$pdf->SetXY(1, 1);
$wPt = 595.28; //  Measures in points of the page
$w   = $pdf->GetPageWidth();
$hPt = 841.89; //  Measures in points of the page
$h   = $pdf->GetPageHeight();
$coef_x = $wPt/$w; // X axis transformation coefficient
$coef_y = $hPt/$h; // Y axis transformation coefficient

// -----------------------------------------------Recover invoice data-------------------------------------------------------------------------------------------------------
$idfactura= $_SESSION['idfactura'] ; // invoice identification to obtain

$sql="SELECT Nif, NombreRazonSocial, Domicilio, RestoDomicilio, FechaFactura, TotalFactura FROM factura where idfactura = $idfactura";
$resql=DB::Query($sql);
$data = $resql->fetchAssoc() ;

// Variables on different parts of document

print_pdf(322, 81, '#'.$idfactura, 'char', 0 , 'L', '', 'helvetica', 16, "247, 172, 8", 0 ); // Number invoce
print_pdf(454, 81,$data['FechaFactura'], 'date', 0 , 'L', '', 'helvetica', 16, "247, 172, 8", 0 ); //  Date invoce
print_pdf(91,113,$data['Nif'], 'char', 0 , 'L', 'B', 'helvetica', 10, "0,0,0", 0 ); //  NIF
print_pdf(91,127,$data['NombreRazonSocial'], 'char', 0 , 'L', 'B', 'helvetica', 10, "68,68,68", 0 ); // Nombre
// $string = iconv("UTF-8", "ISO-8859-1//TRANSLIT", $data['Domicilio']); // Convert UTf8
print_pdf(91,141,$data['Domicilio'], 'char', 0 , 'L', '', 'helvetica', 10, "68,68,68", 0 ); // Domicilio
print_pdf(91,154, $data['RestoDomicilio'], 'char', 0 , 'L', '', 'helvetica', 10, "68,68,68", 0 ); // Resto Domicilio
$page_number = 1;
print_pdf(483, 156, $page_number,'number', 0 , 'R', 'B', 'helvetica', 12, "0,0,0", 0 ); // Number page

print_pdf(531,649,$data['TotalFactura'], 'number', 2 , 'R', 'B', 'helvetica', 10, "0,0,0", 0 ); // TotalFactura
print_pdf(531,679,$data['TotalFactura'], 'number', 2 , 'R', 'B', 'helvetica', 10, "0,0,0", 0 ); // TotalFactura

// --------------------------------------------------------------
// Cálculo de páginas de la factura
$sql="SELECT count(*) total_records   FROM linea_factura where factura_idfactura= $idfactura ";
$resql=DB::Query($sql);
$data3 = $resql->fetchAssoc() ;
$total_records = $data3['total_records'];
if ( $total_records < 32 ) {
  $total_pages = 1;
} else {
  $total_pages = intdiv(($total_records - 31),39);
  $total_pages += 1;
  if ((($total_records - 31) % 39) > 0 ) {
    $total_pages += 1;
  }
}

print_pdf(165,663, $total_pages, 'number', 0 , 'R', '', 'helvetica', 10, "0,0,0", 0 );  //  Nunber Page
print_pdf(260, 663, $total_records, 'number', 0 , 'R', '', 'helvetica', 10, "0,0,0", 0 );  //  Nunber Record

// ------------------------------------------------------------
// Insert image
$pdf->Image(__DIR__.'/QR_prueba.png' , 180 , 5, 20 , 20,'PNG', 'https://fhumanes.com/');

// --------------------------------------------------------------
$sql="SELECT producto_idproducto, Nombre, Precio, Cantidad, Valor   FROM linea_factura where factura_idfactura= $idfactura ";
$rsSql=DB::Query($sql);
$countLines=0;
$num_record = 1;
while( $data2 = $rsSql->fetchAssoc() ){
  if ( $num_record < 32 ) {

    print_pdf(81, 200+($countLines*14.55), $num_record, 'number', 0 , 'R', '', 'helvetica', 10, "0,0,0", 0 );  //  Nunber Record		
    print_pdf(139, 200+($countLines*14.55),$data2['producto_idproducto'], 'number', 0 , 'R', '', 'helvetica', 10, "0,0,0", 0 );  //  Id Producto
    print_pdf(145.5, 200+($countLines*14.55),$data2['Nombre'], 'char', 0 , 'L', '', 'helvetica', 10, "0,0,0", 0 );  //  Nombre
    print_pdf(401, 200+($countLines*14.55),$data2['Precio'], 'number', 2 , 'R', '', 'helvetica', 10, "0,0,0", 0 );  //  Precio
    print_pdf(460, 200+($countLines*14.55),$data2['Cantidad'], 'number', 0 , 'R', '', 'helvetica', 10, "0,0,0", 0 );  //  Cantidad 
    print_pdf(533, 200+($countLines*14.55),$data2['Valor'], 'number', 2 , 'R', '', 'helvetica', 10, "0,0,0", 0 );  //  Valor	
    $countLines=$countLines+1;
    $num_record += 1;
  } else {
  $new_record = ($num_record - 32) % 39 ; // para control de nueva página
  if ($new_record == 0 ) { // Nueva página 
    $countLines=0;
    $page_number += 1;
    // import page 1
    $tplIdx = $pdf->importPage(2);
    // get the size of the imported page
    $size = $pdf->getTemplateSize($tplIdx);
    // add a page with the same orientation and size
    $pdf->AddPage($size['orientation'], $size);
    // use the imported page
    $pdf->useTemplate($tplIdx);
    print_pdf(496, 108, $page_number,'number', 0 , 'R', 'B', 'helvetica', 12, "0,0,0", 0 ); // Number page
  }

  print_pdf(81, 151+($countLines*14.55), $num_record, 'number', 0 , 'R', '', 'helvetica', 10, "0,0,0", 0 );  //  Nunber Record		
  print_pdf(139, 151+($countLines*14.55),$data2['producto_idproducto'], 'number', 0 , 'R', '', 'helvetica', 10, "0,0,0", 0 );  //  Id Producto
  print_pdf(145.5, 151+($countLines*14.55),$data2['Nombre'], 'char', 0 , 'L', '', 'helvetica', 10, "0,0,0", 0 );  //  Nombre
  print_pdf(401, 151+($countLines*14.55),$data2['Precio'], 'number', 2 , 'R', '', 'helvetica', 10, "0,0,0", 0 );  //  Precio
  print_pdf(460, 151+($countLines*14.55),$data2['Cantidad'], 'number', 0 , 'R', '', 'helvetica', 10, "0,0,0", 0 );  //  Cantidad 
  print_pdf(533, 151+($countLines*14.55),$data2['Valor'], 'number', 2 , 'R', '', 'helvetica', 10, "0,0,0", 0 );  //  Valor	
  $countLines=$countLines+1;
  $num_record += 1;

  }
}

// -----------------------------------------------------------------------------------------
//  adding the second page of the template
$tplIdx = $pdf->importPage(3);
// get the size of the imported page
$size = $pdf->getTemplateSize($tplIdx);
// add a page with the same orientation and size
$pdf->AddPage($size['orientation'], $size);
// use the imported page
// $pdf->useTemplate($tplIdx);
$pdf->useImportedPage($tplIdx);

// ---------------------------------------------------------
// $pdf->Output('I','Factura.pdf');
$pdf->Output('Factura.pdf', 'I');
// $pdf->Output();
/*
// --------------------  foot to save the new PDF document ------------------
$temp_file = tempnam(sys_get_temp_dir(), 'PDF');
$pdf->Output('F',$temp_file);

// ------------------ Operation with file result -------------------------------------------
$documento = file_get_contents($temp_file);
unlink($temp_file);  // delete file tmp
header("Content-Disposition: attachment; filename= factura.pdf");
header('Content-Type: application/pdf');
echo $documento;
*/

?>

Fijarse que en el apartado de conexión a la base de datos está además de la conexión, cargando el fichero de configuración donde se indica los formatos de las fechas y los símbolos para la edición de los valores numéricos.

config.php
<?php
$GLOBALS['numeric_symbol_of_thousands']  = '.';
$GLOBALS['numeric_decimal_symbol'] = ',';
$GLOBALS['date_format'] = 'd/m/Y';
$GLOBALS['date_and_time_format'] = 'd/m/Y H:i:s';
$GLOBALS['time_format'] = 'H:i:s';
$GLOBALS['long_Date_Format']= 'l, d de F del Y';
$GLOBALS['date_default_timezone_set'] = 'Europe/Madrid';
$GLOBALS['setlocale_LC_TIME'] = 'spanish';

He intentado documentar el código para explicar qué es cada punto, no obstante, es posible que algún detalle no quede suficientemente explicado. Decídmelo para mejorar la documentación y explicároslo.

Os dejo todos los fuentes del proyecto para que lo instaléis en vuestros PC y hagáis todas las pruebas que requiráis. 

Para lo cualquier duda o lo que necesitéis, me lo indicáis a través de mi email [email protected].

Actualización del 14/06/2023

He realizado los mismos informes que hice para Excel y Word, con esta solución y me ha parecido una forma sencilla y bastante rápida, por lo que os aconsejo que veáis el resultado y si os gusta, probéis a revisar el código.

Los gráficos de este ejemplo se han hecho con jpGraph, muy potente es solución.

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