Como añadir firma en una extensión o controlador de mi plugin

Este plugin trae de serie la pestaña de firma en presupuestos, pedidos, albaranes y facturas de cliente. Pero el mecanismo es genérico: cualquier modelo puede tener firma, porque el modelo FirmaDocumento guarda la referencia como model_name + model_code en la tabla firma_documentos.

Para añadir la firma a tus propios modelos tienes el trait FacturaScripts\Plugins\FirmarDocumentos\Lib\DocSignatureTrait.

Por qué todos los métodos devuelven un Closure

DocSignatureTrait está pensado para usarse indistintamente en una clase de Extension/ o en un controlador propio. Al registrar una extensión, el core recorre por reflexión todos los métodos públicos de la clase y exige que cada uno devuelva un Closure. Por eso el trait devuelve closures, y por eso la forma de invocarlos cambia según el contexto:

| Contexto | Llamada | |---|---| | Extensión de controlador | $this->createViewFirmaDocumento('firma'); | | Controlador propio | ($this->createViewFirmaDocumento())('firma'); |

En una extensión la llamada pasa por ExtensionsTrait::__call, que liga el Closure al controlador y lo ejecuta. En un controlador propio hay que invocar el Closure devuelto: ya viene ligado a $this y a la clase del controlador, así que puede llamar a addHtmlView(), $this->permissions, etc.

Métodos del trait

| Método | Qué hace | |---|---| | createViewFirmaDocumento() | Añade la pestaña con la plantilla Tab/FirmaDocumento y encola el JS del canvas (signature_pad + firma_canvas.js). Acepta el nombre de la vista, firma por defecto. | | loadDataFirmaDocumento() | Carga la firma del documento. Si el documento todavía no tiene clave primaria, elimina la pestaña. Si no hay firma, rellena model_name y model_code para que la vista pueda generar el enlace público con shareUrl(). | | saveFirmaAction() | Guarda la firma dibujada con el nick del usuario y la IP. Comprueba permissions->allowUpdate. Devuelve siempre true. | | deleteFirmaAction() | Borra la firma del documento. Comprueba permissions->allowDelete. Devuelve siempre true. |

saveFirmaAction() y deleteFirmaAction() devuelven true a propósito, para cortar el flujo normal del controlador después de procesar la acción.

Caso 1: extender un controlador existente

Es lo habitual cuando el controlador es del core o de otro plugin.

1. Crea la extensión

<?php
namespace FacturaScripts\Plugins\MiPlugin\Extension\Controller;

use Closure;
use FacturaScripts\Plugins\FirmarDocumentos\Lib\DocSignatureTrait;

/**
 * Añade la pestaña de firma a EditMiModelo.
 */
class EditMiModelo
{
    use DocSignatureTrait;

    public function createViews(): Closure
    {
        return function () {
            $this->createViewFirmaDocumento('firma');
        };
    }

    public function execPreviousAction(): Closure
    {
        return function ($action) {
            if ($action === 'save-firma') {
                return $this->saveFirmaAction();
            }

            if ($action === 'delete-firma') {
                return $this->deleteFirmaAction();
            }
        };
    }

    public function loadData(): Closure
    {
        return function ($viewName, $view) {
            $this->loadDataFirmaDocumento($viewName, $view);
        };
    }
}

Fíjate en que execPreviousAction no devuelve nada cuando la acción no es suya: así el core sigue con el flujo normal del controlador.

2. Regístrala en tu Init.php

public function init(): void
{
    $this->loadExtension(new Extension\Controller\EditMiModelo());
}

La ruta bajo Extension/ tiene que coincidir con la del controlador destino en Dinamic/.

Caso 2: un controlador propio

Si el controlador es tuyo, usa el trait directamente e invoca los closures:

<?php
namespace FacturaScripts\Plugins\MiPlugin\Controller;

use FacturaScripts\Core\Lib\ExtendedController\EditController;
use FacturaScripts\Plugins\FirmarDocumentos\Lib\DocSignatureTrait;

class EditMiModelo extends EditController
{
    use DocSignatureTrait;

    public function getModelClassName(): string
    {
        return 'MiModelo';
    }

    protected function createViews(): void
    {
        parent::createViews();

        ($this->createViewFirmaDocumento())('firma');
    }

    protected function execPreviousAction($action): bool
    {
        if ($action === 'save-firma') {
            return ($this->saveFirmaAction())();
        }

        if ($action === 'delete-firma') {
            return ($this->deleteFirmaAction())();
        }

        return parent::execPreviousAction($action);
    }

    protected function loadData($viewName, $view): void
    {
        parent::loadData($viewName, $view);

        ($this->loadDataFirmaDocumento())($viewName, $view);
    }
}

Con eso ya tienes la pestaña, el canvas, el guardado, el borrado, la validación de permisos y el enlace público funcionando.

El modelo FirmaDocumento

use FacturaScripts\Dinamic\Model\FirmaDocumento;

$firma = new FirmaDocumento();
if ($firma->loadFromModel('MiModelo', $codigo)) {
    echo $firma->getUrl();       // url de la imagen png, con token MyFiles
    echo $firma->signerLabel();  // nick del ERP, o nombre del firmante externo
    echo $firma->shareUrl();     // enlace público para firmar / consultar
    echo $firma->creation_date;
    echo $firma->ip;
}

Columnas relevantes:

| Columna | Contenido | |---|---| | model_name / model_code | Modelo firmado y su clave primaria. Índice único: una firma por documento. | | path | Ruta del png dentro de MyFiles. | | nick | Usuario del ERP que firmó. Vacío si se firmó desde el enlace público. | | signer_name | Nombre escrito por el firmante externo. Vacío si firmó un usuario del ERP. | | creation_date, ip, observaciones | Traza y notas de la firma. |

Para guardar una firma desde código, usa Lib\FirmaStorage::save(), que se encarga del png, del registro y de sobrescribir la firma anterior si existía.

El enlace público

Lib\FirmaToken cifra model_name + model_code con AES-256-CBC y añade un HMAC de integridad, con una clave derivada de los datos de conexión de la instalación. Es determinista: el enlace de un documento es siempre el mismo. Deja de ser válido si cambia la contraseña de la base de datos.

El controlador Controller\FirmaPublica atiende ese enlace con $requiresAuth = false y un getPageData() vacío, así que no aparece en el menú ni en la lista de permisos. Acciones soportadas por querystring:

| Acción | Resultado | |---|---| | (ninguna) | Página con el PDF y, si no está firmado, el formulario de firma. | | action=pdf | Muestra el PDF en el navegador. | | action=pdf-download | Descarga el PDF. |

Si el token es inválido o el documento ya no existe, muestra sólo un aviso genérico.

Personalizar el PDF del enlace público

El PDF lo genera Lib\FirmaPdf: usa el FormatoDocumento de FacturaScripts para presupuestos, pedidos, albaranes y facturas, y la ficha del modelo (las columnas de su XMLView/Edit<Modelo>.xml) para cualquier otro modelo.

FirmaPdf usa ExtensionsTrait, así que no hay que heredar de nada: se personaliza con una extensión y pipes.

| Pipe | Argumentos | Qué permite | |---|---|---| | pdfContent | $model | Devolver el binario del PDF completo. Si responde, no se ejecuta nada más. | | pdfOptions | $model | Array con option, title, idformat y lang para ExportManager::newDoc(). | | pdfBefore | $exportManager, $model | Tocar el ExportManager antes de generar (setCompany(), setOrientation()). Devolviendo false se corta la generación por defecto. | | pdfXmlView | $modelName | Nombre del XMLView del que salen las columnas de la ficha. | | pdfColumns | $columns, $model | Filtrar o reordenar esas columnas. | | pdfTitle | $model | Título del documento. | | pdfFileName | $model | Nombre del archivo. |

Devolver null significa "no me aplica": el pipe pasa a la siguiente extensión y, si ninguna responde, se usa el comportamiento por defecto. Comprueba siempre el modelo, para no cambiar el PDF de los demás.

<?php
namespace FacturaScripts\Plugins\MiPlugin\Extension\Lib;

use Closure;
use FacturaScripts\Dinamic\Model\MiModelo;

class FirmaPdf
{
    // opción sencilla: misma ficha, pero sólo con mis columnas
    public function pdfXmlView(): Closure
    {
        return function (string $modelName) {
            return $modelName === 'MiModelo' ? 'FirmaMiModelo' : null;
        };
    }

    // opción avanzada: genero yo el pdf entero
    public function pdfContent(): Closure
    {
        return function ($model) {
            if (false === $model instanceof MiModelo) {
                return null;
            }

            $export = new MiPdfExport();
            $export->newDoc('...');
            // ...
            return $export->getDoc();
        };
    }
}
// MiPlugin/Init.php
$this->loadExtension(new Extension\Lib\FirmaPdf());

Como en cualquier extensión, los métodos deben devolver un Closure y la ruta bajo Extension/ tiene que coincidir con la de la clase destino en Dinamic/ (Extension/Lib/FirmaPdf.phpDinamic\Lib\FirmaPdf).

Propagación al transformar documentos

Extension/Lib/BusinessDocumentGenerator copia la firma al documento destino cuando se transforma un documento de venta (presupuesto → pedido → albarán → factura). Se reutiliza el mismo path, así que FirmaDocumento::delete() sólo borra la imagen del disco cuando ya no queda ningún registro apuntando a ella.

Cómo se incrusta la firma en el PDF

Se hace con dos extensiones del core, que ya vienen en el plugin:

  • Extension/Lib/PDF/PDFDocument
  • Extension/Lib/PlantillasPDF/BaseTemplate

Es decir, funciona tanto con las plantillas PDF clásicas como con las del plugin PlantillasPDF.

Cookies
Usamos cookies necesarias para el funcionamiento del sitio y cookies opcionales para recordar tus preferencias y mejorar tu experiencia. Puedes aceptarlas todas, rechazarlas o configurar tus preferencias

Soporte