Módulo de Dolibarr para la integración con el sistema VeriFactu de la Agencia Estatal de Administración Tributaria (AEAT), en cumplimiento de la Ley 11/2021 (Ley Antifraude) y el Real Decreto 1007/2023 que establece los requisitos técnicos para los sistemas de facturación en España.
Este proyecto tiene su origen en el código abierto verifactu desarrollado originalmente por Alberto SuperAdmin (Alberto Luque Rivas) de easysoft.es y distribuido bajo licencia GPL v3.
Este módulo fue desarrollado a partir del módulo original licenciado bajo GPL v3 (GNU General Public License version 3). Sobre esta base:
- Se reorganizó y modularizó el código para mejorar la mantenibilidad
- Se tradujeron la documentación y los comentarios al inglés
- Se creó una nueva librería interna (OpenAEAT\Billing)
Este fork mantiene todo el código bajo GPL v3, respetando la licencia original y los derechos de los autores originales, añadiendo la atribución correspondiente en todos los archivos.
VeriFactu es el sistema de verificación de facturas de la AEAT que permite:
- Envío automático de facturas a la AEAT
- Generación de códigos QR de verificación
- Encadenamiento criptográfico de facturas (SHA-256)
- Consulta del estado de facturas enviadas
- Anulación de facturas
- Gestión de facturas rectificativas
- Dolibarr 13.0 o superior
- PHP 7.4 o superior
- Extensión PHP SOAP habilitada
- Extensión PHP OpenSSL habilitada
- Extensión PHP GD habilitada (para QR)
- Certificado digital válido (FNMT o equivalente)
- Copiar la carpeta
verifactuen el directoriohtdocs/custom/de Dolibarr - Ir a Configuración > Módulos en Dolibarr
- Buscar "VeriFactu" en la lista de módulos
- Activar el módulo
Ir a Configuración > Módulos > VeriFactu > Configuración
- Entorno: Seleccionar Pruebas o Producción
- NIF Emisor: Número de identificación fiscal de la empresa
- Nombre/Razón Social: Nombre de la empresa
Ir a Configuración > Módulos > VeriFactu > Certificados
- Subir el certificado en formato PFX/P12
- Introducir la contraseña del certificado
- Verificar que el certificado es válido
El certificado debe ser:
- Certificado de persona jurídica (empresa)
- Emitido por una CA reconocida (FNMT, etc.)
- Válido y no revocado
Configurar los datos del sistema de facturación:
- NIF Desarrollador: NIF del desarrollador del software
- Nombre del Sistema: Nombre del sistema de facturación
- ID del Sistema: Identificador único del sistema
- Versión: Versión del software
El módulo descarga y cachea localmente los esquemas WSDL y XSD necesarios para la comunicación SOAP con la AEAT. Esto evita dependencias de servidores externos (AEAT, W3C) y previene errores de rate-limiting al procesar muchas facturas seguidas.
- Los esquemas se descargan automáticamente la primera vez que se envía una factura
- En Configuración > Módulos > VeriFactu > Configuración se muestra el estado de los esquemas
- Usar el botón Descargar/Actualizar esquemas para forzar la re-descarga si la AEAT actualiza los esquemas
Cuando la fecha en que se efectúa una operación es distinta de la fecha en que se expide la factura, la que determina el devengo del IVA es la primera. Es el caso de una factura emitida en enero por un servicio prestado en diciembre.
En Configuración → Transmitir la fecha de operación se habilita el envío de
ese dato a la AEAT en el campo FechaOperacion. Al activarlo:
- Se habilita el campo de fecha Impuestos en la factura, que es de donde se
toma (Dolibarr lo controla con la constante
INVOICE_POINTOFTAX_DATE). - La fecha solo se transmite cuando difiere de la fecha de factura. Si ambas coinciden, el registro sale igual que antes.
Rellenando ese campo, la factura se expide con su fecha y el IVA se imputa al periodo de la operación:
La fecha de operación no interviene en la huella ni en el código QR, así que informarla no afecta al encadenamiento.
Los bloques Datos VeriFactu y Campos fiscales aparecen plegados en la ficha de la factura. Se despliegan con un clic y Dolibarr recuerda la elección.
En los listados, las columnas de detalle fiscal están disponibles en el selector de columnas, pero no se muestran de entrada: a la vista queda el estado VeriFactu. Cada usuario activa las que necesite.
El distintivo de estado junto al número de factura se controla con la constante
VERIFACTU_STATUS_BADGE_ON_REF:
| Valor | Efecto |
|---|---|
card |
Por defecto. En la ficha de la factura sí, en los listados no |
always |
En todas partes, como hasta la versión 2.2.0 |
never |
No se muestra |
- Crear una factura en Dolibarr
- Validar la factura
- En la pestaña "VeriFactu" de la factura:
- Hacer clic en "Enviar a AEAT"
- Verificar el estado de la respuesta
La validación masiva de facturas procesa cada factura en una transacción independiente:
- Si una factura falla, las demás continúan procesándose normalmente
- Se detectan automáticamente conflictos de fechas con VeriFactu (orden cronológico)
- Si hay conflictos, se muestra un diálogo de confirmación antes de ajustar las fechas
- Las facturas se procesan ordenadas por fecha (de menor a mayor)
Al validar una factura individual (PROV), el sistema:
- Ajusta automáticamente la fecha a
max(hoy, última_factura_validada)para respetar el orden cronológico de VeriFactu - Muestra un aviso en el diálogo de confirmación si la fecha fue ajustada, indicando la fecha original, la nueva fecha y la referencia de la última factura validada
- Ir a Facturación > VeriFactu > Consulta AEAT
- Seleccionar los filtros de búsqueda:
- Período de imputación (año/mes)
- Rango de fechas
- Contraparte (NIF del cliente)
- Hacer clic en "Consultar"
- Acceder a la factura enviada
- En la pestaña "VeriFactu":
- Hacer clic en "Anular en AEAT"
- Seleccionar el tipo de anulación
- Confirmar la operación
El código QR se genera automáticamente al enviar la factura y se muestra en:
- La vista de la factura
- El PDF de la factura (si está configurado)
- El ticket de TPV (si está habilitado)
| Código | Descripción |
|---|---|
| F1 | Factura completa |
| F2 | Factura simplificada |
| F3 | Factura sustitutiva de simplificadas |
| R1 | Rectificativa (error fundado en derecho) |
| R2 | Rectificativa (Art. 80.3) |
| R3 | Rectificativa (Art. 80.4) |
| R4 | Rectificativa (otras) |
| R5 | Rectificativa en factura simplificada |
- Español (es_ES)
- Catalán (ca_ES)
- Euskera (eu_ES)
- Gallego (gl_ES)
- Inglés (en_US)
El módulo incluye un sistema completo de Declaración Responsable en cumplimiento con la normativa española vigente:
- RD 1007/2023 - Reglamento VeriFactu
- RD 254/2025 - Modificaciones y plazos
- Orden HAC/1177/2024 - Especificaciones técnicas
- Art. 29.2.j) Ley 58/2003 - Ley General Tributaria
La configuración se encuentra en conf/declaracion_responsable.conf.php e incluye:
| Sección | Contenido |
|---|---|
| Datos del Productor | NIF, razón social, dirección, contacto |
| Datos del Sistema | Nombre, IdSistemaInformatico, versión |
| Componentes | Software y hardware requeridos |
| Especificaciones Técnicas | Tipo de firma (XAdES), algoritmo hash (SHA-256) |
| Integridad | Hash del módulo calculado dinámicamente |
| Cumplimiento | Declaración según Art. 29.2.j) LGT |
| Suscripción | Fecha, lugar y firmante |
// Obtener configuración completa con hash calculado
$declaracion = obtenerDeclaracionResponsable(true);
// Validar campos obligatorios
$errores = validarDeclaracionResponsable();
// Calcular hash SHA-256 del módulo
$hash = calcularHashModuloVerifactu();
// Exportar en formato JSON
$json = exportarDeclaracionJSON();La declaración responsable está disponible en:
- Menú: VeriFactu > Declaración Responsable
- Exportación JSON:
/verifactu/views/declaration_json.php
Para más detalles, ver docs/responsible_declaration.md.
verifactu/
├── admin/ # Páginas de administración
│ ├── setup.php # Configuración general
│ ├── managecertificates.php # Gestión de certificados
│ └── uploadcertificates.php # Subida de certificados
├── class/ # Clases PHP
│ ├── actions_verifactu.class.php # Hooks y acciones
│ ├── api_verifactu.class.php # API REST
│ └── verifactu.utils.php # Utilidades
├── conf/ # Configuración
│ └── declaracion_responsable.conf.php # Declaración responsable
├── core/
│ ├── modules/ # Descriptor del módulo
│ └── triggers/ # Triggers automáticos
│ ├── interface_900_modVerifactu_BillRestrictions.class.php
│ └── interface_999_modVerifactu_VerifactuTriggers.class.php
├── docs/ # Documentación
│ └── responsible_declaration.md # Doc. declaración responsable
├── lib/
│ ├── newfenix/ # Librería OpenAEAT Billing
│ │ ├── src/ # Clases principales
│ │ │ ├── SchemaManager.php # Gestión de esquemas WSDL/XSD
│ │ │ ├── SoapClient.php # Transporte SOAP
│ │ │ ├── Config.php # Configuración
│ │ │ ├── Manager.php # Orquestador
│ │ │ ├── Invoice.php # Modelo de factura
│ │ │ ├── Cancellation.php # Modelo de anulación
│ │ │ └── Query.php # Modelo de consulta
│ │ └── vendor/ # Dependencias (QRCode)
│ └── functions/ # Funciones del módulo
│ ├── functions.submission.php # Envío de facturas
│ ├── functions.query.php # Consultas AEAT
│ ├── functions.cancellation.php # Anulación
│ ├── functions.certificates.php # Certificados
│ ├── functions.compatibility.php # Funciones de compatibilidad
│ ├── functions.qr.php # Generación QR
│ └── functions.response.php # Respuestas AEAT
├── tests/ # Tests
│ ├── MassValidateInterceptionTest.php # Tests validación masiva
│ └── SchemaManagerTest.php # Tests gestión de esquemas
├── views/ # Vistas y páginas
│ ├── list.facture.php # Lista de facturas
│ ├── query.facture.php # Consulta AEAT
│ ├── tabVERIFACTU.facture.php # Pestaña VeriFactu
│ ├── declaration.php # Declaración responsable
│ ├── declaration_json.php # Exportación JSON declaración
│ ├── faq.php # Ayuda y FAQ
│ ├── pos.facture.php # Ticket TPV
│ └── documentation.php # Documentación
├── langs/ # Archivos de idioma
├── css/ # Estilos CSS
├── js/ # JavaScript
└── README.md # Este archivo
El módulo expone una API REST para integración externa. La ruta base es
verifactuapi (el núcleo de Dolibarr localiza la clase VerifactuApi por el
primer segmento de la URL):
GET /api/index.php/verifactuapi/integrity # público, como integrity.php
POST /api/index.php/verifactuapi/toProduction # clave de API (DOLAPIKEY) de un administrador
POST /api/index.php/verifactuapi/toTest # clave de API (DOLAPIKEY) de un administrador
Este error indica un problema con la estructura del mensaje SOAP. Verificar:
- Formato de fecha (DD-MM-AAAA)
- Datos del emisor y destinatario
- Configuración del certificado
Si aparecen errores al crear el SoapClient al procesar muchas facturas seguidas, es porque PHP intenta descargar xmldsig-core-schema.xsd desde w3.org y este servidor aplica rate-limiting. Solución:
- Ir a Configuración > Módulos > VeriFactu > Configuración
- Pulsar el botón Descargar/Actualizar esquemas para cachear los WSDL/XSD localmente
- Los esquemas se descargan automáticamente en el primer uso, pero se puede forzar desde aquí
Si el certificado no es reconocido:
- Verificar que el certificado no ha expirado
- Comprobar que la contraseña es correcta
- Asegurarse de que el certificado es de persona jurídica
Si al subir un .p12 de la FNMT (habitual en autónomos y personas físicas) el
módulo responde que la contraseña no es correcta aunque sí lo sea, lo más probable
es que el problema no sea la contraseña sino el cifrado del contenedor PKCS#12.
La FNMT ha protegido históricamente estos ficheros con algoritmos legacy
(RC2-40-CBC y PBE-SHA1-3DES). OpenSSL 3 —el que traen Debian 12 y la imagen
oficial dolibarr/dolibarr— desactiva estos algoritmos salvo que se active el
legacy provider, por lo que openssl_pkcs12_read() falla con
error:0308010C: digital envelope routines::unsupported.
Desde la versión 2.0.0 el módulo:
- Reintenta automáticamente la extracción con
openssl pkcs12 -legacy, de modo que en la mayoría de servidores el certificado se acepta sin tocar nada. - Distingue en el mensaje de error entre contraseña incorrecta y algoritmo de cifrado legacy no soportado, en lugar de culpar siempre a la contraseña.
En muchos alojamientos compartidos (Loading.es, Hostinger y similares) tanto
exec() como proc_open() están en disable_functions, por lo que el módulo no
puede invocar el binario openssl del sistema.
Desde la versión 2.0.0 esto ya no impide usar certificados: la extracción intenta
primero las funciones OpenSSL propias de PHP (openssl_pkcs12_read()), que no
necesitan lanzar ningún proceso, y solo recurre al binario externo cuando hace
falta. En la práctica:
| Certificado | Hosting normal | Hosting compartido sin proc_open |
|---|---|---|
| Cifrado moderno | funciona | funciona |
| Cifrado legacy (FNMT antiguo) | funciona (vía -legacy) |
no es posible |
El único caso que no tiene solución desde el módulo es un certificado con cifrado
legacy en un servidor sin acceso al binario openssl ni a openssl.cnf: ahí hay
que reexportar el certificado con un algoritmo moderno (opción B más abajo) desde
otra máquina.
Si el binario openssl del servidor no tiene el proveedor legacy disponible, hay
dos soluciones:
Opción A — activar el legacy provider en el servidor. Editar
/etc/ssl/openssl.cnf y dejar la sección de proveedores así:
[provider_sect]
default = default_sect
legacy = legacy_sect
[default_sect]
activate = 1
[legacy_sect]
activate = 1Opción B — reexportar el certificado con un algoritmo moderno, en una máquina donde sí se pueda abrir:
openssl pkcs12 -legacy -in certificado_fnmt.p12 -nodes -out temporal.pem -password pass:TU_CLAVE
openssl pkcs12 -export -in temporal.pem -out certificado_moderno.p12 -password pass:TU_CLAVE
rm temporal.pemDespués subir certificado_moderno.p12 desde la pestaña Certificados.
Si aparece una pantalla en blanco al ver facturas:
- Revisar los logs de PHP en busca de errores
- Verificar que todas las extensiones PHP están instaladas
El módulo calcula correctamente el ImporteTotal para VeriFactu excluyendo la retención IRPF:
ImporteTotal= Base Imponible + IVA + Recargo de Equivalencia (sin deducir IRPF)- El código QR refleja este importe correcto
- Las consultas a AEAT comparan con el importe correcto
- Versión: 2.2.3
- Autor: Germán Luis Aracil Boned
- Email: garacilb@gmail.com
- Licencia: GPL-3.0-or-later
- Dedicado a: Mi compañero y amigo Ildefonso González Rodríguez
Este módulo mejora gracias a quien se toma el trabajo de reproducir un fallo, leer la normativa y contarlo con detalle. Nominalmente, y por lo que aportó cada uno:
- @hisie (Diego Cebrián) — revisión de conformidad con el RD 1007/2023 y la Orden HAC/1177/2024 (issue #30), con referencia al articulado punto por punto. De ahí salieron la identidad del sistema informático declarada y transmitida, el hash de integridad que dejaba ficheros fuera, la exclusión de las proformas y el tipo rectificativo que se mostraba distinto del que se enviaba. Y en la 2.1.0, su análisis de la condición de carrera en el encadenamiento y del envío no garantizado en la validación: al ir a resolverlo apareció además que la cadena no se construía en absoluto sobre PostgreSQL, así que ese hallazgo se le debe también a él.
- @braito4 — ocho propuestas de corrección
normativa (PR #34 a #41): respuestas parciales de la AEAT, semántica de
RechazoPrevio, validación del registro contra el esquema, control de flujo del servicio, validez del certificado e identidad fiscal, aislamiento entre entidades MultiCompany y la columna de fecha de creación. Cada una venía con su test. - @hippo16214 — los certificados
.p12de la FNMT rechazados por OpenSSL 3 (issue #33), con el comando exacto para generar un contenedor legacy reproducible, que hoy se ejecuta en cada pasada de la suite de regresión; y elTypeErrordel generador de QR que rompía la vista de factura en PHP 8 (issue #32), diagnosticado hasta la línea. - @jose73wrc (José Antonio) — el error fatal
del informe de pagos (issue #31), con la traza y la causa raíz ya
identificadas; y el aviso de que la corrección de los certificados no servía
en alojamientos compartidos donde
proc_openestá desactivado, que es lo que llevó a leer el.p12con las funciones nativas de PHP antes que con el binario.
Atiende la issue #51, propuesta por @xabitrigo, y las observaciones sobre la interfaz que planteó en el mismo hilo.
La fecha en que se efectúa la operación es la que determina el devengo del IVA, mientras que la fecha de expedición es la que ordena la numeración de las facturas (Art. 10.1.d del RD 1007/2023, Art. 6.1.f del RD 1619/2012). Hasta ahora el módulo solo transmitía la de expedición.
SuministroInformacion.xsd declara FechaOperacion como opcional, de tipo
sf:fecha —el mismo formato dd-mm-aaaa de FechaExpedicionFactura— y la sitúa
justo antes de DescripcionOperacion, que es donde se inserta. No interviene
en la huella, que se calcula sobre IDEmisorFactura, NumSerieFactura,
FechaExpedicionFactura, TipoFactura, CuotaTotal, ImporteTotal, la huella
anterior y FechaHoraHusoGenRegistro; se ha comprobado generando el registro con
y sin ella.
Se toma de date_pointoftax, el campo «Impuestos» que Dolibarr muestra en la
factura, y solo se transmite cuando difiere de la fecha de expedición.
Hay una opción nueva en la configuración del módulo, Transmitir la fecha de
operación, desactivada por defecto porque cambia lo que se remite a la AEAT. Al
activarla se habilita también INVOICE_POINTOFTAX_DATE, la constante de Dolibarr
que muestra ese campo en la factura y que de otro modo hay que fijar a mano.
Los separadores de los bloques fiscales se declaraban desplegados, de modo que
ocupaban 730 px de los 1870 de la página y las líneas de factura quedaban
fuera de la vista. Dolibarr admite separadores plegados —la clave 2 del array
de opciones, que showSeparator() interpreta—, así que pasan a declararse así.
La página baja a 1209 px y quien los despliegue mantiene su preferencia,
porque el propio núcleo la guarda en una cookie.
El módulo añadía 16 columnas a los listados de facturas, casi todas vacías, que dejaban la tabla en 8153 px de ancho. Con la visibilidad en negativo —patrón del propio Dolibarr— la columna sigue disponible en el selector de columnas pero no aparece marcada: el listado baja a 2190 px y 15 columnas, con el estado VeriFactu a la vista.
El distintivo de estado que se pegaba a cada número de factura lo gobierna ahora
VERIFACTU_STATUS_BADGE_ON_REF: card por defecto (en la ficha sí, en los
listados no), always para el comportamiento anterior y never para retirarlo.
Una instalación que actualice recibe estos mismos ajustes: addExtraField() no
reescribe un campo que ya existe, así que alignExtraFieldDisplay() los aplica
sobre los campos ya creados, respetando los que el administrador haya cambiado
por su cuenta.
Sobre una instalación real de Dolibarr 19.0.3 con PostgreSQL 16, con el módulo activado y facturas de prueba:
| Comprobación | Antes | Después |
|---|---|---|
| Alto de la ficha de factura | 1870 px | 1209 px |
| Ancho del listado de facturas | 8153 px | 2190 px |
| Columnas por fila en el listado | 30 | 15 |
FechaOperacion en el registro |
no existía | se transmite y no altera la huella |
Correcciones propias del autor del módulo, @garacil (Germán Luis Aracil Boned), detectadas al revisar el código fuente para redactar la wiki del proyecto.
class/api_verifactu.class.php declaraba @access public en la clase y en sus
tres métodos. Restler, el framework de la API de Dolibarr, solo autentica los
métodos con un nivel de acceso superior a público, así que toProduction y
toTest podían invocarse sin clave de API ni permiso alguno: cualquiera que
alcanzase la URL de la API podía decidir a qué entorno de la AEAT se remitían
los registros de facturación de una entidad.
Ahora ambos métodos se declaran @access protected, como los de la API del
núcleo de Dolibarr, de modo que exigen una clave de API válida (DOLAPIKEY), y
además rechazan con HTTP 403 a cualquier usuario que no sea administrador, antes
de tocar la configuración. El hash de integridad sigue siendo público, igual que
integrity.php. La ruta base de la API es verifactuapi y no verifactu como
indicaba este README; queda corregida en la sección «API REST».
En Dolibarr 17 la API del módulo no llega a ejecutarse: el Restler que incluye esa versión del núcleo termina con
Access to undeclared static property Composer\Autoload\ClassLoader::$loaderen cuanto hay registrado un autoloader de Composer, como el delib/newfenix/vendor. Es un fallo del núcleo, corregido en versiones posteriores de Dolibarr, anterior a esta versión e independiente de ella. Donde la API sí funciona (Dolibarr 19 y posteriores), la corrección cierra el acceso sin credenciales.
El bloque catch de execVERIFACTUCall() vaciaba verifactu_huella y
verifactu_csv_factura fuera cual fuera la operación que había fallado. Si una
anulación o una subsanación terminaba con una excepción (sin conexión, error de
certificado, bloqueo de la cadena ocupado…), la factura perdía en Dolibarr la
huella y el CSV de un registro que la AEAT sí había aceptado: getLastInvoiceHash()
dejaba de verla, la siguiente factura se encadenaba sobre un registro anterior al
real y el código QR desaparecía de una factura que seguía registrada.
Ahora una excepción nunca toca la huella ni el CSV. Si la factura ya tiene un
registro aceptado, conserva además su estado «Enviada» y solo se anota el error de
la operación fallida en verifactu_error y verifactu_ultima_salida, de modo que
la baja o la subsanación pueden repetirse. Si nunca llegó a aceptarse, se muestra
el estado «Error» como hasta ahora. La decisión se toma sobre los campos guardados
en la base de datos, no sobre lo que la operación fallida hubiera dejado en memoria,
y está en la nueva función buildVerifactuExceptionErrorData().
El código pedía varios textos con un nombre de clave distinto del que definían
los ficheros de idioma, y Dolibarr mostraba la clave en bruto: los avisos de
factura no modificable y de datos obligatorios del cliente
(VERIFACTU_INVOICE_CAN_NOT_BE_*), el estado «Anulada», el aviso de factura ya
anulada, los tipos de anulación del diálogo de baja, los avisos de NIF de tercero
no modificable, los valores fiscales aplicados desde el tercero, el título «Factura
simplificada» del PDF, la etiqueta del campo «Cliente de factura simplificada» y
los textos de ayuda de varios campos. Además, los textos de la validación masiva,
el aviso de ajuste de fecha de la validación individual y el título de la guía de
ayuda solo existían en español e inglés.
Se añaden en los cinco idiomas las claves que faltaban, como alias con el mismo texto que su clave equivalente, y las traducciones que no existían en catalán, euskera y gallego.
Tres tests nuevos, que fallan con el código de la 2.2.1 y pasan con esta versión:
tests/ApiAccessTest.php (acceso de cada método de la API, rechazo 403 antes de
cambiar la configuración, ningún método público que escriba),
tests/ExceptionKeepsRecordTest.php (la excepción conserva huella, CSV y estado de
un registro aceptado) y tests/LanguageKeysTest.php (toda clave del módulo que pide
el código existe y no está vacía en los cinco idiomas). La suite completa, 15
ficheros, pasa.
Corrección propia del autor del módulo, @garacil (Germán Luis Aracil Boned), detectada al actualizar a la 2.2.0 una instalación en producción sobre PostgreSQL.
Desde la 2.1.0, al abrir la entrada de menú «VeriFactu» (verifactuindex.php)
PHP terminaba con Call to undefined function dol_get_first_day(). El gráfico
de facturas por mes se reescribió en esa versión para que funcionase también en
PostgreSQL, sustituyendo MONTH() y YEAR() por dol_get_first_day() y
dol_get_last_day(); pero esos dos ayudantes viven en core/lib/date.lib.php,
que Dolibarr no carga en todas las páginas, y la página no lo incluía. La
configuración del módulo seguía abriéndose, así que el fallo se percibía como
«el módulo ha dejado de funcionar», aunque la validación, el envío a la AEAT y
la pestaña VeriFactu de la factura no usan esos ayudantes y no estaban
afectados.
La página carga ahora la librería junto al resto de sus dependencias, y la
suite de regresión comprueba que cualquier fichero del módulo que use un
ayudante de date.lib.php lo incluya antes de llamarlo.
Resuelve los dos últimos PR abiertos de @braito4, el #34 y el #39. En ambos casos se aplica el fondo del hallazgo, no la implementación propuesta, y el motivo está explicado en cada PR.
El PR proponía impedir que un registro de anulación fuese el primero de la
cadena. El esquema oficial dice lo contrario:
RegistroFacturacionAnulacionType declara Encadenamiento como un choice
entre PrimerRegistro y RegistroAnterior, y existe SinRegistroPrevioType
precisamente para anular un registro que nunca se remitió a la AEAT.
Pero el PR apuntaba a algo real: sin registro anterior, el módulo no
configuraba el encadenamiento en absoluto, así que validate() fallaba con un
error genérico. Ahora la anulación se marca explícitamente como primer registro,
que es lo que el esquema admite y lo que el propio módulo ya ofrecía con el tipo
de anulación «sin registro previo».
Dos entidades de Multicompany con VeriFactu activado son dos obligados tributarios, cada uno con su cadena. El mismo NIF en las dos significa dos cadenas para un solo obligado.
La comprobación se hace ahora al guardar la configuración, y solo con tablas
del núcleo (llx_const), sin leer la estructura interna de Multicompany, que
no forma parte de Dolibarr y cambia entre versiones. Falla en abierto: un error
de consulta no bloquea la pantalla de configuración, porque lo que protege una
cadena ya existente es el bloqueo del NIF que entró en la 2.0.0.
Nada de esto se ejecuta en el camino del envío. Ahí un false revierte la
factura a borrador, de modo que un fallo transitorio de base de datos habría
des-validado facturas ya emitidas.
Además, si Multicompany está compartiendo la numeración de facturas, la pantalla de configuración lo advierte: dos obligados tomarían su serie del mismo contador y VeriFactu no puede conciliarlo. Se lee de la constante de configuración del propio módulo, no de sus tablas.
No se incorpora la unicidad de la huella del certificado que proponía el mismo PR: un certificado de representante o de colaborador social puede firmar legítimamente por varios obligados tributarios, así que bloquear su reutilización entre entidades impediría un caso de uso válido.
Corrige el punto 4 de la issue #30, planteado por @hisie, y un defecto que apareció al verificarlo y que afecta a toda instalación sobre PostgreSQL.
getLastInvoiceHash() leía el registro anterior con
DATE_FORMAT(f.datef, '%d-%m-%Y'). Esa función solo existe en MySQL y
Dolibarr no la traduce: convertSQLFromMysql() no la contempla, de modo que en
PostgreSQL la consulta falla con function date_format(date, unknown) does not exist.
El fallo era silencioso. La función solo comprobaba si había filas, así que un
error de consulta y una cadena vacía eran indistinguibles: devolvía null y
cada factura se encadenaba como primer registro, incumpliendo el Art. 13 de
la Orden HAC/1177/2024.
- La fecha se formatea ahora en PHP con
$db->jdate(), con el mismo formatod-m-Yque se usa al construir el envío, de modo que ambos extremos de la cadena llevan la misma cadena de fecha. - Un error de lectura lanza una excepción en lugar de pasar por «no hay registro anterior». Es preferible no enviar a enviar con la cadena rota.
- El panel tenía el mismo problema con
MONTH()yYEAR(): el gráfico mensual quedaba vacío en PostgreSQL. Ahora filtra por rango de fechas y agrupa en PHP, sin funciones específicas de ningún motor.
Si tu instalación usa PostgreSQL, conviene revisar los registros ya enviados: es posible que se remitieran como primer registro de cadena.
Entre leer la huella anterior y guardar la nueva hay una llamada al servicio de la AEAT que dura segundos. Dos validaciones simultáneas leían el mismo registro anterior y encadenaban ambas facturas sobre él.
La sección crítica se serializa ahora con un bloqueo de sesión de base de datos:
GET_LOCK() en MySQL y un advisory lock en PostgreSQL, por entidad y entorno,
con espera configurable en VERIFACTU_CHAIN_LOCK_TIMEOUT (30 segundos por
defecto). El bloqueo vive en la sesión de base de datos, así que se libera solo
si el proceso muere, y se libera igualmente cuando el envío lanza una excepción.
Con VERIFACTU_DIRECT_CALL_ON_VALIDATE desactivado y una factura que no venía
de TakePOS, la validación terminaba sin dejar ningún rastro VeriFactu. Ese
indicador decide cómo se remite el registro, no si existe: el Art. 6 del
RD 1007/2023 lo exige en el momento de la expedición.
Ahora la factura queda marcada como pendiente, con Incidencia = 'S', y la cola
de reintentos la recoge, igual que ocurre cuando falla la conexión.
getDeclaredSystemIdentity() aplicaba los límites del esquema con substr()
sin avisar, de modo que una declaración responsable con valores demasiado largos
volvía a transmitir algo distinto de lo declarado, que es justo lo que corrigió
la 2.0.0. El recorte se registra ahora en el log como error, y se hace con
mb_substr() para contar caracteres, como hace el esquema, sin partir por la
mitad un carácter multibyte.
La segunda parte pendiente de la issue #30 —configurar los datos del productor
desde la pantalla de administración— no se implementa, y no por descuido: en
multiempresa, lo que identifica al obligado tributario ante la AEAT
(VERIFACTU_HOLDER_NIF y VERIFACTU_HOLDER_COMPANY_NAME) ya es configuración
por entidad, porque Dolibarr carga llx_const con WHERE entity IN (0, n). Lo
que quedaría por parametrizar son los datos del productor del software, que
es quien suscribe la declaración responsable; quien redistribuya el módulo bajo
su nombre edita conf/declaracion_responsable.conf.php, que existe para eso.
| Comprobación | Resultado |
|---|---|
| Consulta de la cadena, antigua y nueva, contra PostgreSQL 16 real | la antigua falla, la nueva devuelve el registro |
convertSQLFromMysql() de Dolibarr 19.0.3 sobre la consulta antigua |
deja DATE_FORMAT sin traducir |
| Exclusión mutua del advisory lock con dos sesiones concurrentes | la segunda espera hasta que la primera libera |
| Suite de regresión completa | 10 ficheros, 100% |
El número de versión que declaraba el módulo llevaba desde diciembre de 2025
congelado en 1.0.3: los paquetes publicados como 1.0.4 y 1.1 seguían
instalándose y mostrándose en Dolibarr como 1.0.3, porque las etiquetas se
movieron pero modVerifactu.class.php no. Esto importa más allá de lo cosmético,
porque ese mismo valor es el que se transmite a la AEAT en el campo Version
del bloque SistemaInformatico (Art. 15.2.c de la Orden HAC/1177/2024).
Esta versión se numera 2.0.0. Además de dejar atrás sin ambigüedad la etiqueta 1.1 ya publicada —para que quien la tenga instalada no vea un retroceso al actualizar—, el salto de versión mayor corresponde a lo que cambia de comportamiento: registros que antes se transmitían ahora se rechazan localmente por incumplir los límites del esquema, las consultas dejan de abarcar las entidades con las que se comparten facturas, un envío parcialmente correcto con líneas rechazadas deja de darse por enviado y el NIF del obligado tributario queda fijado en cuanto existe la primera huella. El número está unificado en los tres sitios donde aparecía descoordinado: la clase del módulo, la declaración responsable y este README.
-
Fatal error en el informe de pagos (issue #31, reportada por @jose73wrc):
beforePDFCreation()dabaCall to undefined method pdf_paiement_fourn::fetch_optionals()al generar el informe de pagos de facturas de cliente o de proveedor. Ese hook no lo disparan solo los modelos PDF de factura: los modelos de informe (pdf_paiement,pdf_paiement_fourn) también lo lanzan, y lo que pasan como$objectno es una factura. En Dolibarr 22.x pasan el propio modelo PDF, que extiendeCommonDocGeneratory no tienefetch_optionals(); en Dolibarr 17.x pasan una variable$objectque nunca se asigna enwrite_file(), es decirnull. En ambos casos la llamada sin comprobación era un error fatal. Añadido un guard que sale limpiamente cuando el objeto no es un objeto de negocio. De paso, se deja de acceder al extrafield cuando no existe, lo que además eliminaba un warning de PHP 8. -
TypeError en el QR con PHP 8 (issue #32, reportada por @hippo16214):
QRGenerator::renderQrCode()declarabaint $outputType, pero las constantesQRCode::OUTPUT_IMAGE_PNGyQRCode::OUTPUT_MARKUP_SVGdechillerlan/php-qrcodeson strings. Condeclare(strict_types=1), PHP 8 rechazaba toda llamada y rompía la pestaña VeriFactu y la vista de factura (el QR del PDF no se veía afectado porque usaTCPDF::write2DBarcode()). Corregido el tipo del parámetro astring. -
Certificados en hosting compartido (feedback del PR #42): en alojamientos donde
exec()yproc_open()están ambos endisable_functionsel módulo no podía leer el certificado en absoluto, y por tanto no podía comunicarse con la AEAT. La extracción usa ahora primero las funciones OpenSSL propias de PHP (openssl_pkcs12_read()), que no lanzan ningún proceso, y solo recurre al binario externo cuando hace falta (contenedores legacy). Se ha eliminado también el requisito duro deproc_openen la conversión a PEM. Los certificados con cifrado moderno funcionan ya de forma transparente en esos servidores; los legacy siguen necesitando el binario, lo que se comunica con un mensaje explícito. -
Certificados FNMT
.p12con cifrado legacy (issue #33, reportada por @hippo16214, con la aportación de @jose73wrc sobre alojamientos compartidos): los contenedores PKCS#12 protegidos conRC2-40-CBC/PBE-SHA1-3DES—los habituales de la FNMT— fallaban bajo OpenSSL 3 y el módulo lo notificaba como "Verifique la contraseña", que es engañoso. Ahora la extracción reintenta automáticamente conopenssl pkcs12 -legacyy el mensaje de error distingue entre contraseña incorrecta, algoritmo legacy no soportado y fallo genérico. Documentado en la sección de resolución de problemas. -
Identidad del sistema declarada vs. transmitida (issue #30, puntos 1 y 6, revisión de @hisie): la declaración responsable declaraba
IdSistemaInformatico=VERIFACTU-DOLIBARR-OSSy nombreVeriFactu para Dolibarr ERP/CRM, mientras que a la AEAT se enviabanDVyDolibarr Verifactu Module. El Art. 15.2 de la Orden HAC/1177/2024 exige que coincidan. Se ha unificado tomando como válidos los valores que admite el esquema oficial:SuministroInformacion.xsddefineIdSistemaInformaticocomosf:TextMax2Type(máximo 2 caracteres) yNombreSistemaInformaticocomosf:TextMax30Type(máximo 30), por lo que los valores largos que se declaraban serían rechazados por el webservice.getSystemConfig()lee ahora la identidad de la propia declaración responsable, de modo que ya no pueden divergir, y aplica los límites del esquema. Además el campoVersiontransmitíaDOL_VERSION(la versión de Dolibarr) en lugar de la del módulo, que es lo que exige el Art. 15.2.c. -
Hash de integridad incompleto (issue #30, punto 2, revisión de @hisie): la lista
ficheros_verificadosreferenciabainterface_99_...en vez deinterface_999_...yclass/verifactu.class.php, que no existe. ComocalcularHashModuloVerifactu()ignoraba en silencio los ficheros ausentes, el trigger principal quedaba fuera del cálculo de integridad. Corregida la lista, ampliada con los ficheros críticos de hash, envío y anulación, y ahora la función registra los ficheros no encontrados enintegridad.ficheros_no_encontradosy en el log en lugar de callarlos. -
Facturas proforma enviadas a la AEAT como F1 (issue #30, punto 3, revisión de @hisie): una proforma no es una factura expedida legalmente y no debe generar registro de facturación (Art. 6 RD 1007/2023). Nueva función
isVerifactuApplicableInvoice()que las excluye tanto en el trigger de validación como enexecVERIFACTUCall(), con lo que quedan fuera de todas las vías de envío (validación, pestaña VeriFactu, listado, envío masivo y reintentos). Las proformas siguen validándose con normalidad en Dolibarr. -
Tipo rectificativo divergente (issue #30, punto 5, revisión de @hisie):
billCreate()almacenaba R2 para abonos y facturas de sustitución, pero el envío transmitía R1. La pestaña VeriFactu mostraba por tanto un tipo que nunca era el enviado. Unificado a R1, que es lo que realmente se transmite.
-
La contraseña del certificado ya no se interpola en la línea de comandos de
openssl. Las llamadas usanproc_opencon argumentos en array y pasan la contraseña por variable de entorno (-password env:), lo que elimina la posibilidad de inyección de comandos a través de la contraseña y evita que quede visible en la lista de procesos del servidor. -
prepareLocalCertificate()ya no hacedie()ante un fallo de extracción (tumbaba la petición entera): lanza una excepción que el llamador convierte en el mensaje de error habitual del módulo.
class/actions_verifactu.class.php- Guard enbeforePDFCreation()lib/newfenix/src/QRGenerator.php- Tipo del parámetro$outputTypelib/functions/functions.certificates.php- Reintento-legacy, clasificación de errores y llamadas seguras a OpenSSLadmin/managecertificates.php- Mensajes de error diferenciadoslib/functions/functions.configuration.php-getDeclaredSystemIdentity()ygetSystemConfig()lib/functions/functions.compatibility.php- Nueva funciónisVerifactuApplicableInvoice()lib/functions/functions.submission.php- Exclusión de proformascore/triggers/interface_999_modVerifactu_VerifactuTriggers.class.php- Exclusión de proformas y tipos rectificativosconf/declaracion_responsable.conf.php- Identidad del sistema y lista de integridadcore/modules/modVerifactu.class.php- Versión del módulo- Todos los archivos de idioma (es_ES, en_US, ca_ES, eu_ES, gl_ES)
Integradas a partir de las propuestas de @braito4,
verificadas una a una contra el esquema oficial SuministroInformacion.xsd, el
de respuesta RespuestaSuministro.xsd y el código de Dolibarr 19.0.3.
- Respuestas parciales de la AEAT (PR #36): un envío
ParcialmenteCorrectopuede contener líneas conEstadoRegistro = Incorrecto. El módulo aceptaba el envío completo mirando solo el estado global, de modo que una factura rechazada quedaba almacenada como enviada. AhoraisAEATResponseAccepted()exige que todas las líneas estén aceptadas, tanto en el alta como en la anulación. RechazoPrevio(PR #38): el esquema distingueS(la AEAT rechazó el registro antes) deX(el registro nunca llegó a la AEAT, por ejemplo al acogerse a Veri*factu desde un sistema que no lo usaba). El módulo transmitía siempreX.- Validación del registro antes de enviarlo (PR #40): tipo de factura,
longitudes de
NombreRazonEmisoryDescripcionOperacion, número de destinatarios y de líneas de desglose, encadenamiento y formato de las huellas se comprueban contra los límites del XSD antes de llamar al servicio. - Control de flujo de la AEAT (PR #37): se registra el
TiempoEsperaEnvioque devuelve cada respuesta y lo respeta el reintento automático de pendientes. No se aplica en la validación, donde abortar el envío revierte la factura a borrador, ni en los envíos que el usuario pide explícitamente. - Certificados y identidad fiscal (PR #41): se rechaza un certificado fuera
de su periodo de validez X.509 con mensaje propio, los bundles PEM se
escriben con permisos
0600, y el NIF del obligado tributario queda fijado en cuanto la entidad tiene su primera huella. El certificado sigue pudiendo sustituirse, que es lo que hace falta al renovarlo. - Aislamiento entre entidades MultiCompany (PR #34): las consultas de
VeriFactu usaban
getEntity('invoice'), que incluye las entidades con las que se comparten facturas. En la cadena fiscal eso encadena registros de otro obligado tributario. Ahora cada consulta se acota a la entidad activa, igual que la activación del módulo,integrity.phpy la búsqueda de la vista de consulta. - Columna de fecha de creación (PR #35): la vista de consulta seleccionaba
date_creation, que es una propiedad del objetoFacturey no una columna dellx_facture; la columna esdatec.
Quedan fuera de esta versión, para tratarlas con sus autores: las comprobaciones de compartición, unicidad de NIF y huella de certificado entre entidades del PR #34, que inspeccionan la estructura interna del módulo MultiCompany y bloquean activación y validación; y el PR #39, que impide que un registro de anulación sea el primero de la cadena, algo que el esquema oficial admite explícitamente.
Los cambios se han verificado sobre instalaciones reales, no solo en aislamiento:
| Entorno | Resultado |
|---|---|
| Dolibarr 22.0.5 + PHP 8.4 + PostgreSQL 16 | 22/22 comprobaciones en vivo |
| Dolibarr 17.0.2 + PHP 8.2 + PostgreSQL 16 | 20/20 comprobaciones en vivo |
| Dolibarr 17.0.2 + PHP 8.4 + PostgreSQL 16 | 20/20 comprobaciones en vivo |
| Dolibarr 17.0.2 + PHP 7.4 (mínimo declarado) | 20/20 comprobaciones en vivo |
| Dolibarr 19.0.3 + PHP 8.4 (fuente oficial) | 69/69, constantes y hooks contrastados |
| Suite de regresión completa (9 ficheros) | 100% |
En todas ellas se activa el módulo, se dispara el hook beforePDFCreation con
el objeto que realmente pasa cada una, se generan los QR y se comprueban las
constantes de tipo de factura contra la clase Facture real.
Sobre Dolibarr 19.0.3 se ha contrastado además el esquema de llx_facture, el
objeto que recibe beforePDFCreation (también ahí es el modelo PDF, así que el
fallo de la issue #31 se daba igualmente) y que las 186 funciones del núcleo que
invoca el módulo existen en esa rama.
El reintento con openssl pkcs12 -legacy está condicionado a que el binario sea
OpenSSL 3 o superior: en servidores con OpenSSL 1.1.1 o LibreSSL, donde ese
modificador no existe, no se añade (añadirlo convertiría una extracción correcta
en un error de "opción desconocida").
tests/IssueFixesTest.php- 65 tests de regresión para los issues #30, #31, #32 y #33
-
Retención IRPF en VeriFactu: Corregido el cálculo de
ImporteTotalen el código QR y en las consultas a AEAT. Las facturas con retención IRPF ahora envían correctamenteImporteTotal = Base + IVA + Recargosin deducir el IRPF. Creada función helpergetVerifactuImporteTotal()para centralizar el cálculo. -
Validación masiva con transacciones independientes: Reescrito completamente el sistema de validación masiva de facturas. Cada factura se procesa en su propia transacción de base de datos, de modo que si una falla, las demás continúan procesándose. Se intercepta la acción estándar de Dolibarr
massvalidationcon procesamiento individual por factura. -
Orden cronológico de fechas en validación masiva: Antes de validar masivamente, el sistema detecta si alguna factura tiene fecha anterior a la última factura validada en VeriFactu. Si hay conflicto, muestra un diálogo de confirmación y ajusta las fechas automáticamente. Las facturas se procesan ordenadas por fecha ascendente y la fecha mínima se va actualizando conforme se procesan.
-
Orden cronológico de fechas en validación individual: Al validar una factura individual (PROV), la fecha se ajusta a
max(hoy, última_factura_validada)en vez de solo ahoy. Si la fecha se ajustó, se muestra un aviso en el diálogo de confirmación. -
Envío masivo desde lista de facturas: Corregido el problema por el que al confirmar el envío masivo desde la lista de facturas no se transmitían los IDs seleccionados (el diálogo
formconfirmno soporta arrays). Se usa ahora un campo ocultoverifactu_toselectcon IDs separados por comas. Las facturas ya enviadas se filtran automáticamente. -
Archivos PEM al renovar certificado: Al subir un nuevo certificado P12/PFX, los archivos PEM antiguos no se eliminaban, provocando que el sistema siguiera usando el certificado anterior (potencialmente expirado). Añadida función
deleteExistingPemFiles()que limpia los PEM antes de procesar el nuevo certificado. Añadida visualización de la fecha de expiración del certificado con indicadores de color (expirado/próximo a expirar/válido).
- Caché local de esquemas WSDL/XSD: Nueva clase
SchemaManagerque descarga los 7 archivos de esquema (1 WSDL + 5 XSD de AEAT + 1 XSD de W3C) localmente y reescribe las referenciasschemaLocationpara eliminar dependencias externas. Esto previene errores de rate-limiting de w3.org al procesar muchas facturas. Los esquemas se descargan automáticamente en el primer uso y pueden actualizarse desde la página de configuración.
class/actions_verifactu.class.php- Validación masiva e individual con transacciones independientes y control de fechaslib/functions/functions.compatibility.php- Nueva funcióngetVerifactuImporteTotal()lib/functions/functions.qr.php- Corrección importes QR con IRPFlib/functions/functions.query.php- Corrección comparación importes AEATlib/functions/functions.submission.php- Integración SchemaManagerlib/newfenix/src/SchemaManager.php- Nueva clase gestión de esquemaslib/newfenix/src/SoapClient.php- Uso de WSDL local con fallback remotolib/newfenix/src/Config.php- PropiedadlocalWsdlPathlib/newfenix/src/Manager.php- IntegraciónsetSchemasDir()admin/setup.php- Sección estado de esquemas con botón de descarga + expiración de certificadoadmin/managecertificates.php- Limpieza de PEM al subir nuevo certificadoadmin/uploadcertificates.php- Limpieza de PEM al subir nuevo certificadolib/functions/functions.certificates.php- Nueva funcióndeleteExistingPemFiles()views/list.facture.php- Corrección transmisión IDs en envío masivo- Todos los archivos de idioma (es_ES, en_US, ca_ES, eu_ES, gl_ES)
tests/MassValidateInterceptionTest.php- 52 tests para validación masiva, transacciones, fechastests/SchemaManagerTest.php- 19 tests para gestión de esquemas
- Fechas obligatorias VeriFactu actualizadas: Ajustadas las fechas de transición automática a producción de 2026 a 2027 según el anuncio del Gobierno:
- Sociedades: 1 de enero de 2027 (antes 1 de enero de 2026)
- Autónomos: 1 de julio de 2027 (antes 1 de julio de 2026)
- Documentación del proyecto simplificada: Simplificada la sección de historia del proyecto en el README manteniendo la atribución GPL v3 al autor original
- Declaración responsable actualizada: Configurados los datos del productor con la información de 7Kas Servicios de Internet SL (CIF B98515273, Benicasim, Castellón)
lib/functions/functions.configuration.php- Lógica de cambio de entornoadmin/setup.php- Fechas de visualización de estadoconf/declaracion_responsable.conf.php- Datos del productor 7Kas Servicios de Internet SL- Todos los archivos de idioma (es_ES, en_US, ca_ES, eu_ES, gl_ES)
- Declaración Responsable configurable: Añadido archivo de configuración
conf/declaracion_responsable.conf.phpque cumple con la normativa española (RD 1007/2023, Orden HAC/1177/2024):- Datos del productor (NIF, dirección, contacto)
- Datos del sistema informático (IdSistemaInformatico, versión)
- Especificaciones técnicas (firma XAdES, hash SHA-256)
- Hash de integridad del módulo calculado dinámicamente
- Declaración de cumplimiento con referencias legales
- Exportación JSON: Nuevo endpoint
/views/declaration_json.phppara exportar la declaración responsable en formato JSON - Documentación: Añadido
docs/responsible_declaration.mdcon guía de configuración completa
- Actualizada la página de Declaración Responsable para usar los datos del archivo de configuración
- Validación automática de campos obligatorios con mensajes de aviso
- Visualización del hash de integridad del módulo en la declaración
- Gestión de errores en validación masiva de facturas: Corregido el problema por el que facturas con errores de VeriFactu cancelaban todo el proceso de validación por lotes. Ahora, cuando una factura falla al enviar a VeriFactu:
- La factura permanece como borrador con referencia PROV en lugar de ser validada y luego revertida
- Las demás facturas del lote continúan procesándose normalmente
- Mejorada la gestión de conexiones PostgreSQL para evitar errores "connection already closed"
- Los mensajes de error se muestran correctamente al usuario

