Desarrollo de un modelo PDF completamente personalizado para Dolibarr orientado a la generación automática de contratos y documentos de encargo inmobiliario.
El objetivo del proyecto era sustituir las plantillas estándar del ERP por documentos corporativos totalmente adaptados a la imagen de la empresa, incorporando datos dinámicos del cliente, contratos, importes, cláusulas legales, firmas y diseño personalizado.
/* Copyright (C) ... (mantengo tu cabecera) */
/**
* \file htdocs/core/modules/contract/doc/pdf_loremipsum.modules.php
* \ingroup ficheinter
* \brief Strato contracts template class file (modificado para Documento Lorem Ipsum)
*/
require_once DOL_DOCUMENT_ROOT.'/core/modules/contract/modules_contract.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/pdf.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php';
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/functions2.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
/**
* Class to build contracts documents with model Strato
*/
class pdf_doccompradores extends ModelePDFContract
{
/** @var DoliDB */
public $db;
public $entity;
public $name;
public $description;
public $update_main_doc_field;
public $type;
public $version = 'dolibarr';
/** @var Societe */
public $recipient;
public function __construct($db)
{
global $langs, $mysoc;
$this->db = $db;
$this->name = 'doccompradores'; // mantenemos el nombre para que te siga apareciendo "strato" en el selector
$this->description = "Documento lorem ipsum";
$this->update_main_doc_field = 1;
$this->type = 'pdf';
$formatarray = pdf_getFormat();
$this->page_largeur = $formatarray['width'];
$this->page_hauteur = $formatarray['height'];
$this->format = array($this->page_largeur, $this->page_hauteur);
$this->marge_gauche = getDolGlobalInt('MAIN_PDF_MARGIN_LEFT', 20);
$this->marge_droite = getDolGlobalInt('MAIN_PDF_MARGIN_RIGHT', 20);
$this->marge_haute = getDolGlobalInt('MAIN_PDF_MARGIN_TOP', 20);
$this->marge_basse = getDolGlobalInt('MAIN_PDF_MARGIN_BOTTOM', 20);
$this->corner_radius = getDolGlobalInt('MAIN_PDF_FRAME_CORNER_RADIUS', 0);
$this->option_logo = 1;
if ($mysoc === null) {
dol_syslog(get_class($this).'::__construct() Global $mysoc should not be null.'. getCallerInfoString(), LOG_ERR);
return;
}
$this->emetteur = $mysoc;
if (empty($this->emetteur->country_code)) {
$this->emetteur->country_code = substr($langs->defaultlang, -2);
}
}
// Helper: bloque de campo con fondo gris y líneas de escritura
private function drawFieldBlock($pdf, $labelA, $labelB, $x, $y, $page_width, $margin_right, $labelW = 42, $numLines = 4, $lineH = 10)
{
// Colores
$bg = [242,242,242];
$line = [120,120,120];
$text = [40,40,40];
$boxX = $x + $labelW;
$boxW = $page_width - $margin_right - $boxX;
$boxH = $numLines * $lineH;
// Fondo gris
$pdf->SetFillColor($bg[0], $bg[1], $bg[2]);
$pdf->Rect($boxX, 40, $boxW, 43.5, 'F');
// Etiqueta
$pdf->SetTextColor($text[0], $text[1], $text[2]);
$pdf->SetXY($x+5, 41);
$pdf->Cell($labelW - 6, 7, $labelA, 0, 0, 'R');
// Etiqueta
$pdf->SetTextColor($text[0], $text[1], $text[2]);
$pdf->SetXY($x+5, 67.5);
$pdf->Cell($labelW - 6, 7, $labelB, 0, 0, 'R');
// Líneas
$pdf->SetDrawColor($line[0], $line[1], $line[2]);
$pdf->SetLineWidth(0.3);
for ($i = 1; $i <= $numLines; $i++) {
$yy = 42 + $i * $lineH - 2;
$pdf->Line($boxX + 0, $yy, $boxX + $boxW - 0, $yy);
}
return $y + $boxH + 6;
}
// =====================================================================
// MÉTODO REESCRITO: genera el “Documento Lorem Ipsum” directamente
// =====================================================================
public function write_file($object, $outputlangs, $srctemplatepath = '', $hidedetails = 0, $hidedesc = 0, $hideref = 0)
{
global $user, $langs, $conf, $hookmanager;
if (!is_object($outputlangs)) $outputlangs = $langs;
if (getDolGlobalString('MAIN_USE_FPDF')) {
$this->error = 'This template requires TCPDF. Disable MAIN_USE_FPDF in Setup → PDF.';
return -1;
}
$object->fetch_thirdparty();
$this->recipient = $object->thirdparty;
$client_name = $this->recipient->name;
$client_dni = $this->recipient->idprof1; // en España se suele usar idprof1 = NIF/DNI
$client_addr = $this->recipient->address;
$client_zip = $this->recipient->zip;
$client_town = $this->recipient->town;
$client_country = $this->recipient->country; // objeto país (opcional)
// Componer dirección completa
$client_full_address = trim($client_addr);
$outputlangs->loadLangs(array('main','contracts','companies'));
// --------- Definir carpeta/archivo de salida ----------
if (empty($conf->contract->multidir_output[$conf->entity])) {
$this->error = $langs->transnoentities("ErrorConstantNotDefined","CONTRACT_OUTPUTDIR");
return 0;
}
$objectref = dol_sanitizeFileName($object->ref);
$dir = getMultidirOutput($object)."/".$objectref;
$file = $dir."/".$objectref.".pdf";
if (!file_exists($dir) && dol_mkdir($dir) < 0) {
$this->error = $langs->transnoentitiesnoconv("ErrorCanNotCreateDir", $dir);
return 0;
}
// --------- Hooks before PDF ----------
if (!is_object($hookmanager)) {
include_once DOL_DOCUMENT_ROOT.'/core/class/hookmanager.class.php';
$hookmanager = new HookManager($this->db);
}
$hookmanager->initHooks(array('pdfgeneration'));
global $action;
$action = ''; // debe ser variable, no literal
$parameters = array('file'=>$file,'object'=>$object,'outputlangs'=>$outputlangs);
$reshook = $hookmanager->executeHooks('beforePDFCreation', $parameters, $object, $action);
if ($reshook < 0) { $this->error = $hookmanager->error; return 0; }
// --------- Crear PDF ----------
$pdf = pdf_getInstance(array($this->page_largeur, $this->page_hauteur));
$default_font = pdf_getPDFFont($outputlangs);
$default_font_size = pdf_getPDFFontSize($outputlangs);
if (class_exists('TCPDF')) { $pdf->setPrintHeader(false); $pdf->setPrintFooter(false); }
$pdf->SetCreator("Dolibarr ".DOL_VERSION);
$pdf->SetAuthor($outputlangs->convToOutputCharset($user->getFullName($outputlangs)));
$pdf->SetTitle($outputlangs->convToOutputCharset($object->ref));
$pdf->SetSubject($outputlangs->transnoentities("Contract"));
if (getDolGlobalString('MAIN_DISABLE_PDF_COMPRESSION')) $pdf->SetCompression(false);
// Márgenes
$pdf->SetMargins($this->marge_gauche, $this->marge_haute, $this->marge_droite);
$pdf->SetAutoPageBreak(true, $this->marge_basse);
$pdf->SetFont($default_font, '', $default_font_size);
// --------- Página ----------
$pdf->AddPage();
// (Opcional) Logo arriba centrado
if (!getDolGlobalString('PDF_DISABLE_MYCOMPANY_LOGO') && !empty($this->emetteur->logo)) {
$logodir = !empty($conf->mycompany->multidir_output[$object->entity]) ? $conf->mycompany->multidir_output[$object->entity] : $conf->mycompany->dir_output;
$logo = $logodir.'/logos/'.$this->emetteur->logo;
if (is_readable($logo)) {
$w = 45; // ancho deseado
$x = ($this->page_largeur - $this->marge_gauche - $this->marge_droite - $w)/2 + $this->marge_gauche;
$pdf->Image($logo, $x, $this->marge_haute, $w, 0);
}
}
// ======== CONTENIDO DEL DOCUMENTO ========
// ==== Cabecera estilizada a todo el ancho ====
// Requisitos previos: $pdf, $default_font, $default_font_size, $this->marge_gauche, $this->marge_droite, $this->page_largeur
// Colores
$bg = array(34,45,55); // fondo
$white = array(255,255,255); // texto
// Geometría del banner
$X = 0;
$Y = 0; // pegado arriba
$W = $this->page_largeur; // ancho total
$H = 33; // alto de la franja
$padX = $this->marge_gauche; // “padding” lateral para el texto
$padY = 6; // “padding” superior para primera línea
// 1) Fondo
$pdf->SetFillColor($bg[0], $bg[1], $bg[2]);
$pdf->Rect($X, $Y, $W, $H, 'F');
// 2) Título y subtítulo en blanco
$pdf->SetTextColor($white[0], $white[1], $white[2]);
// Título
$pdf->SetFont($default_font, 'B', $default_font_size + 8);
$pdf->SetXY($padX, $Y + $padY);
$pdf->Cell(0, 6, $outputlangs->convToOutputCharset('Lorem Ipsum Dolor Sit Amet'), 0, 1, 'L');
// Subtítulo (línea siguiente, estilo fino)
$pdf->SetFont($default_font, '', $default_font_size + 16);
$pdf->SetXY($padX, $Y + $padY + 8);
$pdf->Cell(0, 6, $outputlangs->convToOutputCharset('loremipsum.example.com'), 0, 1, 'L');
// 3) Logo a la derecha dentro de la franja
// (ajusta $logoPath si lo cargas desde Dolibarr)
$logoW = 36; // ancho deseado del logo en mm
$logoPadRight = $this->marge_droite; // padding derecho
$logoX = $W - $logoPadRight - $logoW; // colocación a la derecha
$logoY = $Y + 4; // alineación vertical dentro del banner
// === Logo a la derecha dentro del banner ===
if (!getDolGlobalString('PDF_DISABLE_MYCOMPANY_LOGO')) {
$logodir = !empty($conf->mycompany->multidir_output[$object->entity])
? $conf->mycompany->multidir_output[$object->entity]
: $conf->mycompany->dir_output;
// Candidatos de logo (grande y miniatura)
$logo_large = $logodir.'/logos/'.$this->emetteur->logo; // ej: mycompany/xxx/logos/mi_logo.png
$logo_small = $logodir.'/logos/thumbs/'.$this->emetteur->logo_small; // ej: .../thumbs/mi_logo_small.png
// Elige el primero legible
$logo_path = '';
if (!empty($this->emetteur->logo) && is_readable($logo_large)) {
$logo_path = $logo_large;
} elseif (!empty($this->emetteur->logo_small) && is_readable($logo_small)) {
$logo_path = $logo_small;
} else {
dol_syslog(__METHOD__.': logo no encontrado: '.$logo_large.' o '.$logo_small, LOG_WARNING);
}
if ($logo_path) {
$logoW = 30; // ancho deseado
$logoX = $this->page_largeur - $this->marge_droite - $logoW;
$logoY = 4; // dentro de la franja
$pdf->Image($logo_path, $logoX, $logoY, $logoW, 0); // alto proporcional
}
}
// 4) Restaurar color de texto y dejar el cursor debajo del banner
$pdf->SetTextColor(0,0,0);
$pdf->SetY($Y + $H + 6); // cursor listo para el contenido siguiente
// ============================================
$x = $this->marge_gauche;
$y = max($this->marge_haute + 15, 0);
$pdf->SetXY($x, $y);
$pdf->SetFont($default_font, '', $default_font_size + 1);
$x = $this->marge_gauche;
$y = /* donde empieces tus campos, p.ej. */ $pdf->GetY() + 10;
// Nombre/s (4 líneas)
$y = $this->drawFieldBlock(
$pdf,
$outputlangs->convToOutputCharset('Lorem:'),
$outputlangs->convToOutputCharset('Ipsum:'),
$x,
$y,
$this->page_largeur,
$this->marge_droite,
15,
5,
8.8
);
$pdf->SetFont($default_font, '', $default_font_size + 4);
// Nombre
$pdf->SetXY($x + 17, $y - 53);
$pdf->Cell(100, 6, $outputlangs->convToOutputCharset($client_name), 0, 1, 'L');
// DNI
$pdf->SetXY($x + 17, $y - 27);
$pdf->Cell(100, 6, $outputlangs->convToOutputCharset($client_dni), 0, 1, 'L');
// (opcional) volver a negro para el resto del documento
$pdf->SetTextColor(0,0,0);
// Enunciado
$pdf->SetXY($x, $y-7);
$pdf->SetFont($default_font, 'B', $default_font_size + 2);
// Color grisáceo
$pdf->SetTextColor(80, 80, 80); // prueba con 80-80-80 o 100-100-100 según quieras más claro/oscuro
// Juntar letras (tracking negativo)
$pdf->setFontSpacing(-0.3); // valores entre -0.2 y -0.5 funcionan bien
// Texto en mayúsculas, centrado
$pdf->Cell(
0, 8,
$outputlangs->convToOutputCharset('LOREM IPSUM DOLOR SIT AMET CONSECTETUR:'),
0, 1, 'C'
);
// Restaurar tracking para el resto del documento
$pdf->setFontSpacing(0);
// Línea subrayada (del ancho del texto)
$curY = $pdf->GetY();
$pdf->Line($this->marge_gauche+10, $curY-2, $this->page_largeur - $this->marge_droite-9, $curY-2);
$y = $curY + 3; // espacio después
$pdf->SetTextColor(0,0,0);
$W = $this->page_largeur - $this->marge_gauche - $this->marge_droite;
$par = function($html) use ($pdf, $x, &$y, $W, $outputlangs) {
$pdf->writeHTMLCell($W, 0, $x, $y, $outputlangs->convToOutputCharset($html), 0, 1, false, true, 'L');
$y = $pdf->GetY() + 2;
};
// Dirección
$pdf->SetFont($default_font, '', $default_font_size);
$pdf->SetXY($x + 70, $y + 4.4);
$pdf->MultiCell(
130,
1,
$outputlangs->convToOutputCharset($client_full_address),
0,
'L'
);
// Ciudad
$pdf->SetFont($default_font, '', $default_font_size);
$pdf->SetXY($x + 25, $y + 4.4);
$pdf->MultiCell(
130,
1,
$outputlangs->convToOutputCharset($client_town),
0,
'L'
);
$pdf->SetFont($default_font, '', $default_font_size);
$pdf->SetXY($x + 36, $y + 9);
$pdf->MultiCell(
130,
1,
$outputlangs->convToOutputCharset("Lorem Ipsum Empresa S.L."),
0,
'L'
);
// PRIMERO
$pdf->SetFont($default_font, 'B', $default_font_size + 1);
$pdf->SetXY($x, $y);
$pdf->SetFont($default_font, '', $default_font_size);
$par('PRIMERO.- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non lorem vitae augue facilisis placerat. Sed vitae lorem at ipsum malesuada consequat.');
// SEGUNDO
if (!empty($object->lines)) {
$line = $object->lines[0]; // primera línea del contrato
$unitPriceHT = $line->subprice; // precio unitario sin impuestos
} else {
$unitPriceHT = 0;
}
$pdf->SetFont($default_font,'',$default_font_size);
$curX = $pdf->GetX();
$curY = $pdf->GetY();
$html = ''.price($unitPriceHT).'';
$pdf->writeHTMLCell(0, 20, 130, $curY + 15, $html, 0, 0, false, true, 'L');
$pdf->SetFont($default_font, 'B', $default_font_size + 1);
$pdf->SetXY($x, $y);
$pdf->SetFont($default_font, '', $default_font_size);
$par('SEGUNDO.- Que ha sido informada por la agencia de la posibilidad de utilizar un procedimiento de compra mediante subasta a través del portal web “loremipsum.example.com”, que consiste en la exposición en el mismo de la vivienda objeto del encargo para proceder a su venta mediante el sistema de LOREM IPSUM partiendo, para este inmueble, de un PRECIO DE SALIDA de __________________ €.');
// TERCERO
$pdf->SetFont($default_font, 'B', $default_font_size + 1);
$pdf->SetXY($x, $y);
$pdf->SetFont($default_font, '', $default_font_size);
$par('TERCERO.- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse potenti:');
$leftIndent = 6; $x2 = $x + $leftIndent; $W2 = $W - $leftIndent;
$pdf->writeHTMLCell($W2, 0, $x2, $y, $outputlangs->convToOutputCharset(
'1. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vitae neque sed lorem tincidunt dictum.'
), 0, 1, false, true, 'L');
$y = $pdf->GetY() + 2;
$pdf->writeHTMLCell($W2, 0, $x2, $y, $outputlangs->convToOutputCharset(
'2. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non justo sed risus tempor pretium.'
), 0, 1, false, true, 'L');
$y = $pdf->GetY() + 2;
$pdf->writeHTMLCell($W2, 0, $x2, $y, $outputlangs->convToOutputCharset(
'3. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent feugiat, lorem at facilisis tincidunt, neque neque vulputate eros, vitae consequat ipsum lorem nec lorem.'
), 0, 1, false, true, 'L');
$y = $pdf->GetY() + 3;
$pdf->writeHTMLCell($W2, 0, $x2, $y, $outputlangs->convToOutputCharset(
'4. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur tempus, lorem non tempor gravida, ipsum erat congue lorem, sed cursus ipsum lorem vitae lorem.'
), 0, 1, false, true, 'L');
$y = $pdf->GetY() + 4;
// Lugar y fecha
$pdf->SetXY($x, $y);
$pdf->Cell(
0, 6,
$outputlangs->convToOutputCharset("En _______________________ a _______ de _______________________ de _____________"),
0, 1, 'C'
);
$pdf->SetY($pdf->GetY() - 6);
$pdf->SetX(50);
$pdf->Cell(0, 6, $outputlangs->convToOutputCharset("Lorem Ipsum"), 0, 1, 'L');
$pdf->SetY($pdf->GetY() - 6);
$pdf->SetX(88);
$pdf->Cell(0, 6, $outputlangs->convToOutputCharset(dol_print_date(dol_now(), '%d')), 0, 1, 'L');
$pdf->SetY($pdf->GetY() - 6);
$pdf->SetX(115);
$pdf->Cell(0, 6, $outputlangs->convToOutputCharset(dol_print_date(dol_now(), '%B')), 0, 1, 'L');
$pdf->SetY($pdf->GetY() - 6);
$pdf->SetX(161);
$pdf->Cell(0, 6, $outputlangs->convToOutputCharset(dol_print_date(dol_now(), '%Y')), 0, 1, 'L');
$y += 18;
// === Bloque de firmas (2 columnas) ===
$labelGray = array(130,130,130);
$boxBg = array(247,247,247); // gris muy claro
$lineGray = array(150,150,150); // línea inferior
$footerBar = array(238,169,89); // banda inferior (naranja suave)
// Geometría
$gap = 28; // separación entre columnas
$colW = 100; // ancho de cada columna
$leftX = $x;
$rightX= 100;
$yRow = $y-10; // punto de inicio
// Etiquetas grises y centradas
$pdf->SetFont($default_font, '', $default_font_size); // no negrita, como en la imagen
$pdf->SetTextColor($labelGray[0], $labelGray[1], $labelGray[2]);
$pdf->SetXY($leftX, $yRow);
$pdf->Cell($colW, 6, $outputlangs->convToOutputCharset('Lorem ipsum'), 0, 0, 'C');
$pdf->SetXY($rightX, $yRow);
$pdf->Cell($colW, 6, $outputlangs->convToOutputCharset('Lorem dolor'), 0, 1, 'C');
$yRow += 8; // pequeño margen debajo del texto
// Cajas para firmar (relleno claro + línea inferior)
$padX = 18; // padding lateral dentro de la columna
$boxW = $colW - ($padX * 2); // ancho de la caja
$boxH = 28; // alto de la caja (ajusta si quieres más/menos)
$leftBoxX = $leftX + $padX;
$rightBoxX = $rightX + $padX;
$boxY = $yRow;
$pdf->SetFillColor($boxBg[0], $boxBg[1], $boxBg[2]);
$pdf->Rect($leftBoxX, $boxY, $boxW, $boxH, 'F');
$pdf->Rect($rightBoxX, $boxY, $boxW, $boxH, 'F');
// Línea fina sólo en la parte inferior de cada caja
$pdf->SetDrawColor($lineGray[0], $lineGray[1], $lineGray[2]);
$pdf->SetLineWidth(0.3);
$pdf->Line($leftBoxX, $boxY + $boxH, $leftBoxX + $boxW, $boxY + $boxH);
$pdf->Line($rightBoxX, $boxY + $boxH, $rightBoxX + $boxW, $boxY + $boxH);
// Avanza Y para continuar el documento
$y = $boxY + $boxH + 14;
// === Banda de color al pie de página, a todo el ancho ===
$barH = 8; // alto de la banda
$barY = $this->page_hauteur - $this->marge_basse - $barH+25;
$pdf->SetFillColor($footerBar[0], $footerBar[1], $footerBar[2]);
$pdf->Rect(0, $barY, $this->page_largeur, $barH, 'F');
// (opcional) devuelve color de texto a negro para lo siguiente
$pdf->SetTextColor(0,0,0);
// --------- Guardar ----------
$pdf->Close();
$pdf->Output($file, 'F');
dolChmod($file);
// Hooks after
$hookmanager->initHooks(array('pdfgeneration'));
$parameters = array('file'=>$file,'object'=>$object,'outputlangs'=>$outputlangs);
$hookmanager->executeHooks('afterPDFCreation', $parameters, $object, $action);
$this->result = array('fullpath'=>$file);
return 1;
}
// ---------------------------------------------------------------------------------
// El resto de métodos de tu archivo (tabla, firmas por defecto, cabecera/pie genéricos)
// no son necesarios para este modelo, pero los dejamos por compatibilidad.
// ---------------------------------------------------------------------------------
protected function _tableau(&$pdf, $tab_top, $tab_height, $nexY, $outputlangs, $hidetop = 0, $hidebottom = 0)
{
$hidebottom = 0;
if ($hidetop) $hidetop = -1;
$this->printRoundedRect($pdf, $this->marge_gauche, $tab_top, $this->page_largeur - $this->marge_gauche - $this->marge_droite, $tab_height + 3, $this->corner_radius, $hidetop, $hidebottom, 'D');
}
protected function tabSignature(&$pdf, $tab_top, $tab_height, $outputlangs)
{
$pdf->SetDrawColor(128,128,128);
$posmiddle = $this->marge_gauche + round(($this->page_largeur - $this->marge_gauche - $this->marge_droite) / 2);
$posy = $tab_top + $tab_height + 6;
$pdf->SetXY($this->marge_gauche, $posy);
$pdf->MultiCell($posmiddle - $this->marge_gauche - 5, 5, $outputlangs->transnoentities("ContactNameAndSignature", $this->emetteur->name), 0, 'L', 0);
$pdf->SetXY($posmiddle + 5, $posy);
$pdf->MultiCell($this->page_largeur - $this->marge_droite - $posmiddle - 5, 5, $outputlangs->transnoentities("ContactNameAndSignature", $this->recipient->name), 0, 'L', 0);
}
protected function _pagehead(&$pdf, $object, $showaddress, $outputlangs, $outputlangsbis = null, $titlekey = "Contract")
{
// No usamos esta cabecera en este modelo (dibujamos manual en write_file),
// pero la dejamos por compatibilidad si se llamara desde fuera.
return 0;
}
protected function _pagefoot(&$pdf, $object, $outputlangs, $hidefreetext = 0)
{
// Pie estándar si alguna vez lo necesitas:
return pdf_pagefoot($pdf, $outputlangs, 'CONTRACT_FREE_TEXT', $this->emetteur, $this->marge_basse, $this->marge_gauche, $this->page_hauteur, $object, getDolGlobalInt('MAIN_GENERATE_DOCUMENTS_SHOW_FOOT_DETAILS', 0), $hidefreetext, $this->page_largeur, $this->watermark);
}
}
Uno de los aspectos más importantes fue la construcción manual de toda la maquetación PDF mediante TCPDF. El documento genera cabeceras corporativas, bloques de información, campos dinámicos, zonas de firma y estilos visuales personalizados sin depender de plantillas externas.
Además del diseño visual, el sistema recupera automáticamente información de contratos, terceros y productos registrados en Dolibarr, permitiendo generar documentación completamente personalizada sin intervención manual.
A nivel técnico, este desarrollo demuestra capacidad para extender sistemas ERP, trabajar con TCPDF, generar documentos complejos desde código, integrar datos empresariales dinámicos y construir soluciones documentales adaptadas a procesos reales de negocio.