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:

ContextoLlamada
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étodoQué 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:

ColumnaContenido
model_name / model_codeModelo firmado y su clave primaria. Índice único: una firma por documento.
pathRuta del png dentro de MyFiles.
nickUsuario del ERP que firmó. Vacío si se firmó desde el enlace público.
signer_nameNombre escrito por el firmante externo. Vacío si firmó un usuario del ERP.
creation_date, ip, observacionesTraza 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ónResultado
(ninguna)Página con el PDF y, si no está firmado, el formulario de firma.
action=pdfMuestra el PDF en el navegador.
action=pdf-downloadDescarga 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.

PipeArgumentosQué permite
pdfContent$modelDevolver el binario del PDF completo. Si responde, no se ejecuta nada más.
pdfOptions$modelArray con option, title, idformat y lang para ExportManager::newDoc().
pdfBefore$exportManager, $modelTocar el ExportManager antes de generar (setCompany(), setOrientation()). Devolviendo false se corta la generación por defecto.
pdfXmlView$modelNameNombre del XMLView del que salen las columnas de la ficha.
pdfColumns$columns, $modelFiltrar o reordenar esas columnas.
pdfTitle$modelTítulo del documento.
pdfFileName$modelNombre 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.php → Dinamic\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