Hola Germán,
He comenzado a usar tu módulo Verifactu porque es el que me pareció tener la mejor base para mis necesidades particulares. No obstante he visto elementos mejorables.
He realizado una revisión técnica del módulo VeriFactu para Dolibarr con el objetivo de verificar su conformidad con el RD 1007/2023 y la Orden HAC/1177/2024. A continuación te detallo los hallazgos y propongo las correcciones necesarias, así como una nueva funcionalidad que considero importante para entornos multi-instalación.
Para ello he creado un fork de tu repositorio donde tengo intención de ir implementando todas las correcciones y mejoras que se describen en este correo. Soy consciente de que no todos los puntos que planteo tienen el mismo nivel de urgencia o quizás no todos sean pertinentes según el criterio del mantenedor principal, por lo que estoy completamente abierto a debatirlos contigo antes de proceder. Una vez consensuados, estaré encantado de abrir Pull Requests contra tu repositorio para que estas mejoras estén disponibles para toda la comunidad de usuarios del módulo.
— CRÍTICO / ALTO
1. Discrepancia en IdSistemaInformatico (functions.configuration.php:129)
getSystemConfig() envía a la AEAT 'IdSistemaInformatico' => 'DV', mientras que la Declaración Responsable declara 'id_sistema_informatico' => 'VERIFACTU-DOLIBARR-OSS'. Ambos valores deben ser idénticos. El Art. 15.2.b de la Orden HAC/1177/2024 exige que el identificador declarado coincida exactamente con el que se transmite en cada registro.
Propuesta de corrección básica:
// functions.configuration.php:129
'IdSistemaInformatico' => 'VERIFACTU-DOLIBARR-OSS',
'NombreSistemaInformatico' => 'VeriFactu para Dolibarr ERP/CRM',
Ver propuesta de parametrización de Declaración responsable para una mejor solución.
2. Nombre de fichero incorrecto en el hash de integridad (declaracion_responsable.conf.php:328)
El array ficheros_verificados referencia:
'core/triggers/interface_99_modVerifactu_VerifactuTriggers.class.php'
pero el fichero real es interface_**999**_modVerifactu_VerifactuTriggers.class.php. La función calcularHashModuloVerifactu() ignora silenciosamente los ficheros que no existen, por lo que el trigger principal queda excluido del cálculo de integridad del sistema.
Propuesta de corrección:
// declaracion_responsable.conf.php:328
'core/triggers/interface_999_modVerifactu_VerifactuTriggers.class.php',
3. Las facturas proforma se envían a la AEAT como F1 (interface_999 trigger:175-178)
TYPE_PROFORMA se mapea a TYPE_STANDARD (F1) sin ninguna exclusión del flujo de envío a VeriFactu. Las facturas proforma no son facturas emitidas legalmente y no deben generar un registro de facturación.
Propuesta de corrección:
case $object::TYPE_PROFORMA:
// Las facturas proforma NO deben enviarse a VeriFactu
$object->array_options['options_verifactu_excluir'] = 1;
break;
Y en billValidate, añadir al inicio:
if (!empty($object->array_options['options_verifactu_excluir'])) {
return 1; // Excluida del flujo VeriFactu
}
**4. Envío obligatorio en validación y condición de carrera en el encadenamiento
El módulo presenta dos problemas relacionados que deben resolverse conjuntamente, ya que la solución de uno afecta directamente al otro.
Problema A — Envío no garantizado en validación (interface_999 trigger:308): si el flag VERIFACTU_DIRECT_CALL_ON_VALIDATE está desactivado y la factura no proviene de TakePOS, la factura se valida correctamente sin que se genere ningún registro VeriFactu. La normativa exige que el registro de facturación exista en el momento de la expedición (Art. 6 RD 1007/2023), por lo que este flag no debería
controlar si se crea el registro, sino cómo.
Problema B — Condición de carrera en la cadena (functions.hash.php:33): getLastInvoiceHash() lee el último hash sin ningún mecanismo de bloqueo. En un entorno multiusuario, dos validaciones simultáneas pueden leer el mismo hash anterior y enviar ambos registros con el mismo RegistroAnterior, rompiendo la cadena. Esto viola el Art. 13 de la Orden HAC/1177/2024, que exige una cadena estrictamente secuencial.
Ambos problemas no pueden abordarse de forma independiente: hacer el envío obligatorio en validación sin resolver antes la condición de carrera agravaría el problema de concurrencia. Y un bloqueo a nivel de base de datos (SELECT ... FOR UPDATE) tampoco es la solución adecuada, porque el bloqueo quedaría activo durante toda la llamada al webservice de la AEAT, que puede durar varios segundos, bloqueando cualquier otra validación concurrente en ese tiempo.
Solución conjunta propuesta: implementar una cola de envío serializada mediante una tabla dedicada (llx_verifactu_queue) con las siguientes características:
- En el momento de validación, se inserta el registro en la cola de forma atómica dentro de la transacción principal de Dolibarr, asignando un número de secuencia incremental. Esto garantiza que toda factura validada tiene su puesto en la cola, resolviendo el problema A.
- Un proceso de cola consume los registros en orden estricto de secuencia, leyendo el hash anterior y enviando a la AEAT de forma serializada, sin concurrencia posible. Esto resuelve el problema B sin necesidad de bloqueos de larga duración.
- Si el envío falla (error AEAT, sin conexión), el registro queda en cola con estado pendiente y se reintenta en la siguiente validación o de forma periódica, manteniendo el mecanismo de Incidencia='S' ya implementado.
Esta arquitectura resuelve ambos problemas a la vez, es compatible con el mecanismo de reintento existente, y escala correctamente en instalaciones de alto volumen.
---### MEDIO
5. Discrepancia en el tipo de factura rectificativa entre creación y envío
En billCreate (trigger:167-169): TYPE_CREDIT_NOTE → R2 (TYPE_CREDIT_NOTE_80_3).
En el envío (submission.php:299): TYPE_CREDIT_NOTE → R1 (TYPE_CREDIT_NOTE_LEGAL) para facturas no TakePOS.
El tipo almacenado por defecto (R2) y el que se envía realmente (R1) son distintos. La pestaña VeriFactu muestra R2, lo que induce a error al usuario sobre lo que se transmite a la AEAT.
Propuesta de corrección: Unificar el tipo inicial en billCreate a R1 para facturas normales, o bien permitir al usuario seleccionar el tipo rectificativo explícitamente en la pestaña VeriFactu antes del envío.
6. NombreSistemaInformatico inconsistente (functions.configuration.php:128)
Se envía a la AEAT como 'Dolibarr Verifactu Module', pero la Declaración Responsable lo declara como 'VeriFactu para Dolibarr ERP/CRM'. Deben coincidir (ver también corrección del punto 1).
Nueva funcionalidad propuesta: Personalización de la Declaración Responsable por instalación / tenant
En entornos con múltiples entidades (multiempresa) o distribuciones del módulo a terceros, es necesario que cada instalación o tenant pueda configurar sus propios datos de productor sin modificar el fichero declaracion_responsable.conf.php base.
Propuesta de implementación:
Añadir en la pantalla de administración del módulo (admin/setup.php) una nueva sección "Declaración Responsable" con los siguientes campos configurables por tenant (almacenados en llx_const con el prefijo VERIFACTU_DR_):
┌────────────────────────────┬─────────────────────────────────────┬────────────────────┐
│ Campo │ Clave configuración │ Art. HAC/1177/2024 │
├────────────────────────────┼─────────────────────────────────────┼────────────────────┤
│ Razón social del productor │ VERIFACTU_DR_PRODUCTOR_RAZON_SOCIAL │ Art. 15.1.a │
├────────────────────────────┼─────────────────────────────────────┼────────────────────┤
│ NIF del productor │ VERIFACTU_DR_PRODUCTOR_NIF │ Art. 15.1.b │
├────────────────────────────┼─────────────────────────────────────┼────────────────────┤
│ Dirección del productor │ VERIFACTU_DR_PRODUCTOR_DIRECCION │ Art. 15.1.c │
├────────────────────────────┼─────────────────────────────────────┼────────────────────┤
│ Email de contacto │ VERIFACTU_DR_PRODUCTOR_EMAIL │ Recomendado │
├────────────────────────────┼─────────────────────────────────────┼────────────────────┤
│ Nombre del sistema │ VERIFACTU_DR_SISTEMA_NOMBRE │ Art. 15.2.a │
├────────────────────────────┼─────────────────────────────────────┼────────────────────┤
│ IdSistemaInformatico │ VERIFACTU_DR_SISTEMA_ID │ Art. 15.2.b │
├────────────────────────────┼─────────────────────────────────────┼────────────────────┤
│ Versión del sistema │ VERIFACTU_DR_SISTEMA_VERSION │ Art. 15.2.c │
├────────────────────────────┼─────────────────────────────────────┼────────────────────┤
│ Fecha de la declaración │ VERIFACTU_DR_FECHA_SUSCRIPCION │ Art. 15.4 │
├────────────────────────────┼─────────────────────────────────────┼────────────────────┤
│ Nombre del firmante │ VERIFACTU_DR_FIRMANTE_NOMBRE │ Art. 15.4 │
└────────────────────────────┴─────────────────────────────────────┴────────────────────┘
La función obtenerDeclaracionResponsable() debería modificarse para cargar primero los valores del fichero base y sobreescribir con los valores de configuración de base de datos cuando existan:
function obtenerDeclaracionResponsable($calcularHash = true)
{
global $declaracionResponsable, $conf;
// Sobreescribir con valores configurados por tenant
$overrides = [
'productor.razon_social' => 'VERIFACTU_DR_PRODUCTOR_RAZON_SOCIAL',
'productor.nif' => 'VERIFACTU_DR_PRODUCTOR_NIF',
'productor.contacto.email' => 'VERIFACTU_DR_PRODUCTOR_EMAIL',
'sistema.nombre' => 'VERIFACTU_DR_SISTEMA_NOMBRE',
'sistema.id_sistema_informatico' => 'VERIFACTU_DR_SISTEMA_ID',
'sistema.version' => 'VERIFACTU_DR_SISTEMA_VERSION',
'suscripcion.fecha' => 'VERIFACTU_DR_FECHA_SUSCRIPCION',
'suscripcion.firmante.nombre' => 'VERIFACTU_DR_FIRMANTE_NOMBRE',
];
foreach ($overrides as $path => $confKey) {
$value = $conf->global->$confKey ?? '';
if (!empty($value)) {
setNestedArrayValue($declaracionResponsable, explode('.', $path), $value);
}
}
// Sincronizar IdSistemaInformatico con getSystemConfig()
if (!empty($conf->global->VERIFACTU_DR_SISTEMA_ID)) {
define('VERIFACTU_SISTEMA_ID_OVERRIDE', $conf->global->VERIFACTU_DR_SISTEMA_ID);
}
if ($calcularHash) {
calcularHashModuloVerifactu();
}
$declaracionResponsable['sistema']['numero_instalacion'] = generarIdInstalacion();
return $declaracionResponsable;
}
Y en getSystemConfig(), leer el identificador del sistema desde la configuración, garantizando que lo declarado y lo transmitido a la AEAT sean siempre coherentes:
function getSystemConfig()
{
global $conf, $dolibarr_main_instance_unique_id;
$sistemaId = $conf->global->VERIFACTU_DR_SISTEMA_ID ?? 'VERIFACTU-DOLIBARR-OSS';
$sistemaNombre = $conf->global->VERIFACTU_DR_SISTEMA_NOMBRE ?? 'VeriFactu para Dolibarr ERP/CRM';
$issuerName = $conf->global->VERIFACTU_HOLDER_COMPANY_NAME ?? '';
$issuerNif = $conf->global->VERIFACTU_HOLDER_NIF ?? '';
return [
'NombreRazon' => $issuerName,
'NIF' => $issuerNif,
'NombreSistemaInformatico' => $sistemaNombre,
'IdSistemaInformatico' => $sistemaId,
'Version' => $conf->global->VERIFACTU_DR_SISTEMA_VERSION ?? DOL_VERSION,
'NumeroInstalacion' => $dolibarr_main_instance_unique_id . '_' . $conf->entity,
'TipoUsoPosibleSoloVerifactu' => 'S',
'TipoUsoPosibleMultiOT' => 'N',
'IndicadorMultiplesOT' => 'N',
];
}
De esta forma, el fichero declaracion_responsable.conf.php actúa como plantilla con los valores del productor original (7Kas), y cada instalación o tenant puede sobreescribir sus propios datos desde la interfaz de administración, garantizando que lo que se declara y lo que se envía a la AEAT siempre estén sincronizados.
Como te comentaba al inicio, he abierto un fork del repositorio donde iré implementando estas correcciones de forma progresiva. No obstante, antes de proceder con ningún cambio, me gustaría que debatiéramos cuáles de estos puntos consideras necesarios o pertinentes, ya que eres quien mejor conoce el contexto y las decisiones de diseño del módulo. Mi objetivo no es imponer criterios sino colaborar para mejorar el cumplimiento normativo del módulo en beneficio de todos los usuarios. Si llegamos a un acuerdo, estaré encantado de abrir los Pull Requests correspondientes contra tu repositorio principal para que estas mejoras estén disponibles para toda la comunidad.
Quedo a tu disposición para cualquier aclaración o para una llamada si lo prefieres.
Un saludo,
Diego Cebrián
Hola Germán,
He comenzado a usar tu módulo Verifactu porque es el que me pareció tener la mejor base para mis necesidades particulares. No obstante he visto elementos mejorables.
He realizado una revisión técnica del módulo VeriFactu para Dolibarr con el objetivo de verificar su conformidad con el RD 1007/2023 y la Orden HAC/1177/2024. A continuación te detallo los hallazgos y propongo las correcciones necesarias, así como una nueva funcionalidad que considero importante para entornos multi-instalación.
Para ello he creado un fork de tu repositorio donde tengo intención de ir implementando todas las correcciones y mejoras que se describen en este correo. Soy consciente de que no todos los puntos que planteo tienen el mismo nivel de urgencia o quizás no todos sean pertinentes según el criterio del mantenedor principal, por lo que estoy completamente abierto a debatirlos contigo antes de proceder. Una vez consensuados, estaré encantado de abrir Pull Requests contra tu repositorio para que estas mejoras estén disponibles para toda la comunidad de usuarios del módulo.
— CRÍTICO / ALTO
1. Discrepancia en IdSistemaInformatico (functions.configuration.php:129)
getSystemConfig()envía a la AEAT'IdSistemaInformatico' => 'DV', mientras que la Declaración Responsable declara'id_sistema_informatico' => 'VERIFACTU-DOLIBARR-OSS'. Ambos valores deben ser idénticos. El Art. 15.2.b de la Orden HAC/1177/2024 exige que el identificador declarado coincida exactamente con el que se transmite en cada registro.Propuesta de corrección básica:
Ver propuesta de parametrización de Declaración responsable para una mejor solución.
2. Nombre de fichero incorrecto en el hash de integridad (declaracion_responsable.conf.php:328)
El array ficheros_verificados referencia:
'core/triggers/interface_99_modVerifactu_VerifactuTriggers.class.php'pero el fichero real es
interface_**999**_modVerifactu_VerifactuTriggers.class.php. La funcióncalcularHashModuloVerifactu()ignora silenciosamente los ficheros que no existen, por lo que el trigger principal queda excluido del cálculo de integridad del sistema.Propuesta de corrección:
3. Las facturas proforma se envían a la AEAT como F1 (interface_999 trigger:175-178)
TYPE_PROFORMAse mapea aTYPE_STANDARD(F1) sin ninguna exclusión del flujo de envío a VeriFactu. Las facturas proforma no son facturas emitidas legalmente y no deben generar un registro de facturación.Propuesta de corrección:
**4. Envío obligatorio en validación y condición de carrera en el encadenamiento
El módulo presenta dos problemas relacionados que deben resolverse conjuntamente, ya que la solución de uno afecta directamente al otro.
Problema A — Envío no garantizado en validación (interface_999 trigger:308): si el flag VERIFACTU_DIRECT_CALL_ON_VALIDATE está desactivado y la factura no proviene de TakePOS, la factura se valida correctamente sin que se genere ningún registro VeriFactu. La normativa exige que el registro de facturación exista en el momento de la expedición (Art. 6 RD 1007/2023), por lo que este flag no debería
controlar si se crea el registro, sino cómo.
Problema B — Condición de carrera en la cadena (functions.hash.php:33): getLastInvoiceHash() lee el último hash sin ningún mecanismo de bloqueo. En un entorno multiusuario, dos validaciones simultáneas pueden leer el mismo hash anterior y enviar ambos registros con el mismo RegistroAnterior, rompiendo la cadena. Esto viola el Art. 13 de la Orden HAC/1177/2024, que exige una cadena estrictamente secuencial.
Ambos problemas no pueden abordarse de forma independiente: hacer el envío obligatorio en validación sin resolver antes la condición de carrera agravaría el problema de concurrencia. Y un bloqueo a nivel de base de datos (SELECT ... FOR UPDATE) tampoco es la solución adecuada, porque el bloqueo quedaría activo durante toda la llamada al webservice de la AEAT, que puede durar varios segundos, bloqueando cualquier otra validación concurrente en ese tiempo.
Solución conjunta propuesta: implementar una cola de envío serializada mediante una tabla dedicada (llx_verifactu_queue) con las siguientes características:
Esta arquitectura resuelve ambos problemas a la vez, es compatible con el mecanismo de reintento existente, y escala correctamente en instalaciones de alto volumen.
---### MEDIO
5. Discrepancia en el tipo de factura rectificativa entre creación y envío
En
billCreate(trigger:167-169):TYPE_CREDIT_NOTE→ R2 (TYPE_CREDIT_NOTE_80_3).En el envío (submission.php:299):
TYPE_CREDIT_NOTE→ R1 (TYPE_CREDIT_NOTE_LEGAL) para facturas no TakePOS.El tipo almacenado por defecto (R2) y el que se envía realmente (R1) son distintos. La pestaña VeriFactu muestra R2, lo que induce a error al usuario sobre lo que se transmite a la AEAT.
Propuesta de corrección: Unificar el tipo inicial en
billCreatea R1 para facturas normales, o bien permitir al usuario seleccionar el tipo rectificativo explícitamente en la pestaña VeriFactu antes del envío.6. NombreSistemaInformatico inconsistente (functions.configuration.php:128)
Se envía a la AEAT como 'Dolibarr Verifactu Module', pero la Declaración Responsable lo declara como 'VeriFactu para Dolibarr ERP/CRM'. Deben coincidir (ver también corrección del punto 1).
Nueva funcionalidad propuesta: Personalización de la Declaración Responsable por instalación / tenant
En entornos con múltiples entidades (multiempresa) o distribuciones del módulo a terceros, es necesario que cada instalación o tenant pueda configurar sus propios datos de productor sin modificar el fichero declaracion_responsable.conf.php base.
Propuesta de implementación:
Añadir en la pantalla de administración del módulo (admin/setup.php) una nueva sección "Declaración Responsable" con los siguientes campos configurables por tenant (almacenados en llx_const con el prefijo VERIFACTU_DR_):
┌────────────────────────────┬─────────────────────────────────────┬────────────────────┐
│ Campo │ Clave configuración │ Art. HAC/1177/2024 │
├────────────────────────────┼─────────────────────────────────────┼────────────────────┤
│ Razón social del productor │ VERIFACTU_DR_PRODUCTOR_RAZON_SOCIAL │ Art. 15.1.a │
├────────────────────────────┼─────────────────────────────────────┼────────────────────┤
│ NIF del productor │ VERIFACTU_DR_PRODUCTOR_NIF │ Art. 15.1.b │
├────────────────────────────┼─────────────────────────────────────┼────────────────────┤
│ Dirección del productor │ VERIFACTU_DR_PRODUCTOR_DIRECCION │ Art. 15.1.c │
├────────────────────────────┼─────────────────────────────────────┼────────────────────┤
│ Email de contacto │ VERIFACTU_DR_PRODUCTOR_EMAIL │ Recomendado │
├────────────────────────────┼─────────────────────────────────────┼────────────────────┤
│ Nombre del sistema │ VERIFACTU_DR_SISTEMA_NOMBRE │ Art. 15.2.a │
├────────────────────────────┼─────────────────────────────────────┼────────────────────┤
│ IdSistemaInformatico │ VERIFACTU_DR_SISTEMA_ID │ Art. 15.2.b │
├────────────────────────────┼─────────────────────────────────────┼────────────────────┤
│ Versión del sistema │ VERIFACTU_DR_SISTEMA_VERSION │ Art. 15.2.c │
├────────────────────────────┼─────────────────────────────────────┼────────────────────┤
│ Fecha de la declaración │ VERIFACTU_DR_FECHA_SUSCRIPCION │ Art. 15.4 │
├────────────────────────────┼─────────────────────────────────────┼────────────────────┤
│ Nombre del firmante │ VERIFACTU_DR_FIRMANTE_NOMBRE │ Art. 15.4 │
└────────────────────────────┴─────────────────────────────────────┴────────────────────┘
La función obtenerDeclaracionResponsable() debería modificarse para cargar primero los valores del fichero base y sobreescribir con los valores de configuración de base de datos cuando existan:
Y en
getSystemConfig(), leer el identificador del sistema desde la configuración, garantizando que lo declarado y lo transmitido a la AEAT sean siempre coherentes:De esta forma, el fichero declaracion_responsable.conf.php actúa como plantilla con los valores del productor original (7Kas), y cada instalación o tenant puede sobreescribir sus propios datos desde la interfaz de administración, garantizando que lo que se declara y lo que se envía a la AEAT siempre estén sincronizados.
Como te comentaba al inicio, he abierto un fork del repositorio donde iré implementando estas correcciones de forma progresiva. No obstante, antes de proceder con ningún cambio, me gustaría que debatiéramos cuáles de estos puntos consideras necesarios o pertinentes, ya que eres quien mejor conoce el contexto y las decisiones de diseño del módulo. Mi objetivo no es imponer criterios sino colaborar para mejorar el cumplimiento normativo del módulo en beneficio de todos los usuarios. Si llegamos a un acuerdo, estaré encantado de abrir los Pull Requests correspondientes contra tu repositorio principal para que estas mejoras estén disponibles para toda la comunidad.
Quedo a tu disposición para cualquier aclaración o para una llamada si lo prefieres.
Un saludo,
Diego Cebrián