Documentos e informes en WORD

Este artículo se lo dedico a Rubén.

Seguro que muchos habéis estado probando la solución de crear documentos Word desde PHP, para crear  (factura, registros, comunicaciones, etc.), en general, documentos con resultados muy buenos, pues éste es un método sencillo y muy potente, por la calidad del diseño de estos documentos.

Incluso, el usuario final o destinatario de la solución, puede diseñarnos el documento Word que podemos, con poquísimas adaptaciones, utilizar para el aplicativo.

Además, hace pocos meses salió la versión 1.0.0, con nuevas características y he pensado en hacer 2 ejemplos, para que podáis apreciar el potencial y sencillez de la solución.

Objetivo

Tengo 2 objetivos, a saber:

1.- Tener un ejemplo más completo de elaboración de documentos «factura». Posibilidad de obtener una a una o disponer de un único documento con múltiples facturas. También, como ejemplo, se incluye la elaboración dinámica de un QR con los datos de la factura y se incluye éste, en el  documento.

DEMO: https://fhumanes.com/invoice_word/

2.- Reproducir los informes que tengo en Excel, pero utilizando Word. Al igual que con Excel, he hecho un ejemplo e informe con un gráfico (en este caso de Word).

DEMO: https://fhumanes.com/reports_word/

Solución Técnica

Para los ejemplos he utilizado 3 bibliotecas de PHP:

Por requerimiento de estas librerías, la solución funciona con PHP 8.1 o superior.

En el primer ejemplo el aspecto de la solución es:En la página de visualización de las facturas he colocado un botón para obtener el Word de esa factura.

En este caso, en la página de listado, se puede selecciona 1 o varias facturas, para generar un único fichero que contenga cada una de las facturas seleccionadas.

Código para crear la factura (FacturaWord.php):

<?php
// Load the PHPWord library classes
require_once __DIR__ . '/phpword_1.0.0/autoload.php'; 
use PhpOffice\PhpWord\Element\TextRun;

// Load the QR library classes
require_once __DIR__ . '/qr-code_4.4.9/autoload.php'; 

use Endroid\QrCode\Color\Color;
use Endroid\QrCode\Encoding\Encoding;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelLow;
use Endroid\QrCode\QrCode;
use Endroid\QrCode\Label\Label;
use Endroid\QrCode\Logo\Logo;
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin;
use Endroid\QrCode\Writer\PngWriter;

function create_factura() 
{
// ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

$idfactura= $_SESSION['idfactura'] ; // identificación de factura a obtener

$decimal = new \NumberFormatter("es-ES", \NumberFormatter::DECIMAL); 
$decimal->setAttribute(\NumberFormatter::MIN_FRACTION_DIGITS, 2);
$decimal->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, 2); // by default some locales got max 2 fraction digits
$entero = new \NumberFormatter("es-ES", \NumberFormatter::DECIMAL);
$entero->setAttribute(\NumberFormatter::MIN_FRACTION_DIGITS, 0);
$entero->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, 0); 

// Template processor instance creation
$template_word = __DIR__.'/PlantillaFactura.docx';
$templateProcessor = new \PhpOffice\PhpWord\TemplateProcessor($template_word);

// -------------------- ^ cabecera necesaria para las plantillas de Word ------------------
$sql="SELECT Nif, NombreRazonSocial, Domicilio, RestoDomicilio, FechaFactura, TotalFactura FROM factura where idfactura = $idfactura";
$resql=db_query($sql,$conn);
$data=db_fetch_array($resql);

$nif = new TextRun();
$nif_arr = array ($data['Nif'],'dos');

for($i = 0; $i < count($nif_arr); ++$i) {
  $nif->addText($nif_arr[$i]);
  if ( $i+1 <> count($nif_arr)) { 
    $nif->addTextBreak(1);
  }
}

// Variables on different parts of document
$templateProcessor->setComplexValue('NIF', $nif);
// $templateProcessor->setValue('NIF', $data['Nif']); 
$templateProcessor->setValue('Nombre', $data['NombreRazonSocial']);  
$templateProcessor->setValue('Direccion1', $data['Domicilio']);
$templateProcessor->setValue('Direccion2', $data['RestoDomicilio']);
$templateProcessor->setValue('Total', $decimal->format($data['TotalFactura']));
$FechaFactura=$data['FechaFactura'];

// Set Image - Create QR
// Load the QR library classes

$writer = new PngWriter();

// Create QR code
$txt = "Factura: $idfactura";
$qrCode = QrCode::create($txt)
    ->setEncoding(new Encoding('UTF-8'))
    ->setErrorCorrectionLevel(new ErrorCorrectionLevelLow())
    ->setSize(200)
    ->setMargin(10)
    ->setRoundBlockSizeMode(new RoundBlockSizeModeMargin())
    ->setForegroundColor(new Color(0, 0, 0))
    ->setBackgroundColor(new Color(255, 255, 255));

// Create generic logo

$logo = Logo::create(__DIR__.'/fernando.png')
    ->setResizeToWidth(50);

// Create generic label

$label = Label::create('fhumanes')
    ->setTextColor(new Color(255, 0, 0));

$result = $writer->write($qrCode, $logo, $label);


// Create name file
$file = tempnam(sys_get_temp_dir(), 'PNG');
unlink($file);
$file = str_replace('.','_',$file).'.png';

// Save it to a file
$result->saveToFile($file);

$image = $file;
$templateProcessor->setImageValue('foto', array('path' => $image, 'width' => 100, 'height' => 100, 'ratio' => true));
unlink($file);

$sql="SELECT count(*) Lineas FROM linea_factura where factura_idfactura= $idfactura";
$resql=db_query($sql,$conn);
$data=db_fetch_array($resql);
$NumeroLineas= $data['Lineas']; // número de líneaas de factura
// Simple table
$templateProcessor->cloneRow('rowArticulo', $NumeroLineas);

$sql="SELECT Nombre, Precio, Cantidad, Valor  FROM linea_factura where factura_idfactura= $idfactura ";
$rsSql=db_query($sql,$conn);
$countLines=0;
while ($data2 = db_fetch_array($rsSql)){
    $countLines=$countLines+1;
    $templateProcessor->setValue('rowArticulo#'.$countLines, $data2['Nombre']);
    $templateProcessor->setValue('rowPrecio#'.$countLines, $decimal->format($data2['Precio']));
    $templateProcessor->setValue('rowCantidad#'.$countLines, $entero->format($data2['Cantidad']));
    $templateProcessor->setValue('rowValor#'.$countLines, $decimal->format($data2['Valor']));
    // $templateProcessor->setImageValue('rowFoto#'.$countLines, array('path' => $image, 'width' => 50, 'height' => 50, 'ratio' => true));
  }

// Date Local completed

$date = DateTime::createFromFormat('Y-m-d', $FechaFactura);
$formatter = new IntlDateFormatter('es_ES', IntlDateFormatter::LONG, IntlDateFormatter::LONG);
$formatter->setPattern("d 'de' MMMM 'de' yyyy");
$mydate = $formatter->format($date);
$templateProcessor->setValue('FechaDeHoyCompleta', $mydate);

// $templateProcessor->setValue('FechaDeHoyCompleta', $FechaFactura);

// -------------------- v pie para salvar el nuevo documento Word ------------------
$temp_file = tempnam(sys_get_temp_dir(), 'Word');
$templateProcessor->saveAS($temp_file);

return $temp_file;
}
?>

Código para obtener un único fichero de todas las facturas (factura_word_list.php):

<?php 
@ini_set("display_errors","1");
@ini_set("display_startup_errors","1");

require_once __DIR__ . '/MyCode/docx_merge_1.2.0/autoload.php';
use DocxMerge\DocxMerge;

require_once("include/dbcommon.php");
require_once __DIR__ ."/MyCode/FacturaWord.php"; 

custom_error(2,"Iniciación Print múltiple"); // To debug 

$id_arr = $_SESSION['ID_SELECT'];

custom_error(3,"ID's seleccionados: ".implode(",", $id_arr)); // To debug

$file_arr = array();
 
foreach ($id_arr as &$id) {
    $_SESSION['idfactura'] = $id;
    $temp_file = create_factura();
    $file_arr[] = $temp_file;
} 
custom_error(4,"files creados : ".implode(",", $file_arr)); // To debug

$result_file = tempnam(sys_get_temp_dir(), 'WOR');
unlink($result_file);
$result_file = str_replace('.','_',$result_file).'.docx';

custom_error(5,"files result : ".$result_file); // To debug

$dm = new DocxMerge();
$dm->merge($file_arr,
    $result_file,
    true
 );

foreach ($file_arr as &$file_temp) {  // Delete temporal file
    unlink($file_temp);
}

$documento = file_get_contents($result_file);

custom_error(6,"files result : ".$result_file.' Contenido: '.strlen($documento)); // To debug

unlink($result_file);  // delete file tmp

header("Content-Disposition: attachment; filename= factura.docx");
header('Content-Type: application/docx');
echo $documento;

 

En el segundo ejemplo, el aspecto de la salida es:

La rapidez (hay informes de 300 páginas), sencillez y potencia que se alcanza con esta solución es impresionante. También es muy importante la calidad de estos informes.

El código de este informe es:

<?php
// Required by PHPRunner (security)
@ini_set("display_errors","1");
@ini_set("display_startup_errors","1");
require_once("../../include/dbcommon.php");

// Load the PHPWord library classes
require_once __DIR__ . '/../phpword_1.0.0/autoload.php'; 
use PhpOffice\PhpWord\Element\TextRun;


function listado_001() 
{
// ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

$decimal = new \NumberFormatter("es-ES", \NumberFormatter::DECIMAL); 
$decimal->setAttribute(\NumberFormatter::MIN_FRACTION_DIGITS, 2);
$decimal->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, 2); // by default some locales got max 2 fraction digits
$entero = new \NumberFormatter("es-ES", \NumberFormatter::DECIMAL);
$entero->setAttribute(\NumberFormatter::MIN_FRACTION_DIGITS, 0);
$entero->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, 0); 

// Template processor instance creation
$template_word = __DIR__.'/template.docx';
$templateProcessor = new \PhpOffice\PhpWord\TemplateProcessor($template_word);

// -------------------- ^ cabecera necesaria para las plantillas de Word ------------------
$formatDate = date ( 'd/m/Y' ); // Fecha del informe
 
$templateProcessor->setValue('fecha',$formatDate); // Fecha del informe

// Gráficos
$categories = array();
$series1 = array();
$sql = 
"SELECT idrp_provincia, rp_provincia.CodigoProvincia, NombreProvincia, COUNT(*) municipios
FROM rp_provincia left JOIN  rp_municipio on ( rp_provincia.CodigoProvincia = rp_municipio.CodigoProvincia )
Group by 1,2,3
Order By 4 desc";

$rs = DB::Query($sql);
for ($i = 1; $i <= 10; $i++) {
    $data = $rs->fetchAssoc();
  $categories[] = $data['NombreProvincia'];
  $series1[]= $data['municipios'];
}


// TYPE: 'pie', 'doughnut', 'line', 'bar', 'stacked_bar', 'percent_stacked_bar', 'column', 'stacked_column', 'percent_stacked_column', 'area', 'radar', 'scatter'
$chart = new PhpOffice\PhpWord\Element\Chart('column', $categories, $series1, Null, 'Municipios');
// 
$chart->getStyle()->setWidth(PhpOffice\PhpWord\Shared\Converter::inchToEmu(7))->setHeight(PhpOffice\PhpWord\Shared\Converter::inchToEmu(4));

$chart->getStyle()->setShowGridX(true);
$chart->getStyle()->setShowGridY(true);
$chart->getStyle()->setShowAxisLabels(true);
$chart->getStyle()->setShowLegend(false);
$chart->getStyle()->setDataLabelOptions(
[	'showVal' => true, // value
  'showCatName' => false, // category name
  'showLegendKey' => false, //show the cart legend
  'showSerName' => false, // series name
  'showPercent' => false,
  'showLeaderLines' => false,
  'showBubbleSize' => false
]);
/**
     * Set the valueLabelPosition setting
     * "none" - skips writing labels
     * "nextTo" - sets labels next to the value
     * "low" - sets labels are below the graph
     * "high" - sets labels above the graph.
**/
// $chart->getStyle()->setValueLabelPosition('high');



// r = right, l = left, t = top, b = bottom, tr = top right
$chart->getStyle()->setLegendPosition('t');

$templateProcessor->setChart('grafico', $chart);

// -------------------------------------------------------------------------------------------

$sql = "SELECT count(*)  FROM rp_provincia";
$rs = DB::Query($sql);

$count_lines = $rs->value(0); // número de líneaas del informe
$templateProcessor->cloneRow('id', $count_lines);

$sql = 
"SELECT idrp_provincia, rp_provincia.CodigoProvincia, NombreProvincia, COUNT(*) municipios
FROM rp_provincia left JOIN  rp_municipio on ( rp_provincia.CodigoProvincia = rp_municipio.CodigoProvincia )
Group by 1,2,3
Order By 4 desc";

$rs = DB::Query($sql);
$countLines=0;
while ($data = $rs->fetchAssoc()) {
    $countLines=$countLines+1;
    $templateProcessor->setValue('id#'.$countLines, $data['idrp_provincia']);
    $templateProcessor->setValue('codigo#'.$countLines, $entero->format($data['CodigoProvincia']));
    $templateProcessor->setValue('nombre#'.$countLines, $data['NombreProvincia']);
    $templateProcessor->setValue('municipios#'.$countLines, $data['municipios']);
    
  }



// -------------------- v pie para salvar el nuevo documento Word ------------------
$temp_file = tempnam(sys_get_temp_dir(), 'Word');
$templateProcessor->saveAS($temp_file);

return $temp_file;
}

// ------------------ Operation with file result -------------------------------------------
$temp_file = listado_001();  // Ejecución del listado
$documento = file_get_contents($temp_file);
unlink($temp_file);  // delete file tmp
header("Content-Disposition: attachment; filename= municipios.docx");
header('Content-Type: application/docx');
echo $documento;
?>

Este es el más complejo por tener la parte del gráfico.

Para cualquier duda o explicación, comunicadme vuestra necesidad a través de mi email [email protected]

Os dejo el proyecto para que lo podáis instalar en vuestros PC’s. En el proyecto están todas las bibliotecas de PHP que he utilizado.

Adjuntos

Archivo Tamaño de archivo Descargas
zip invoice_word (PHPRunner 10.7 + backup BD + Libraries) 14 MB 252
zip reports_word (PHPRunner 10.7 + backup BD) 847 KB 200

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