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.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/PDFDocumentExtension/Lib/PlantillasPDF/BaseTemplate
Es decir, funciona tanto con las plantillas PDF clásicas como con las del plugin PlantillasPDF.