Untitled static subpage: Thursday, 25 June 2026, 19:44
Date: 2026-06-25 00:03 Title: FirmadorDtes — Documentación Técnica Type: page Description: Arquitectura, base de datos, API REST, flujo completo y resolución de problemas del sistema de facturación electrónica DTE
FirmadorDtes — Documentación Técnica y de Administración
Sistema de firma y transmisión de Documentos Tributarios Electrónicos (DTEs) al Ministerio de Hacienda de El Salvador, integrado con KontrolesERP (KontrolesERP).
Tabla de Contenidos
- Descripción General
- Arquitectura
- Stack Tecnológico
- Componentes del Sistema
- Flujo Completo de un DTE
- Tipos de Documento Soportados
- Esquema de Base de Datos
- Generación Dinámica de JSON
- API REST de Administración
- Interfaz Web de Administración
- Despliegue
- Configuración Inicial Post-Despliegue
- Operación y Monitoreo
- Invalidaciones
- Contingencias
- Evento de Retorno (ERET)
- Generación de PDFs y Envío de Correo
- Ambiente de Pruebas y Certificación MH
- Resolución de Problemas
1. Descripción General
FirmadorDtes es un servicio autónomo (worker) que:
- Observa la base de datos PostgreSQL en busca de facturas pendientes de envío (
emh_status = 'PENDING') - Genera el JSON del DTE dinámicamente según los esquemas del MH v2026
- Firma el documento con el certificado PKCS12 de la empresa usando RSA-SHA512 (JWS)
- Transmite al API del Ministerio de Hacienda (ambiente pruebas o producción)
- Registra la respuesta (sello de recepción o error)
- Genera el PDF con código QR y lo envía por correo al receptor
El sistema es multi-empresa: cada empresa (ad_client_id) y sucursal (ad_org_id) tiene sus propias credenciales, certificado y configuración.
2. Arquitectura
┌─────────────────────────────────────────────────────────────┐
│ KontrolesERP │
│ Usuario crea Factura → c_invoice (emh_status='PENDING') │
└────────────────────────────┬────────────────────────────────┘
│ PostgreSQL (schema: adempiere)
│ db00_kontroleserp
┌────────────────────────────▼────────────────────────────────┐
│ FirmadorDtes (dte-worker) │
│ │
│ WorkerScheduler (polling cada 30s) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ DatabaseService → emh_preparar_dte() │ │
│ │ ↓ JSON crudo │ │
│ │ FirmadorService (JOSE4J + PKCS12) │ │
│ │ ↓ JWS firmado │ │
│ │ HaciendaService → POST /fesv/recepciondte │ │
│ │ ↓ Respuesta {codigoMsg, selloRecibido} │ │
│ │ DatabaseService → emh_docsline + c_invoice.status │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ EmailScheduler (polling cada 60s) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ PdfService (JasperReports + ZXing QR) │ │
│ │ EmailService (SMTP dinámico por empresa) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ AdminController → GET/POST /admin/* │
│ (Interfaz web de configuración) │
└────────────────────────────┬────────────────────────────────┘
│ HTTPS
┌────────────────────────────▼────────────────────────────────┐
│ API Ministerio de Hacienda El Salvador │
│ Pruebas: https://apitest.dtes.mh.gob.sv/fesv/... │
│ Producción: https://api.dtes.mh.gob.sv/fesv/... │
└─────────────────────────────────────────────────────────────┘
3. Stack Tecnológico
| Componente | Tecnología | Versión |
|---|---|---|
| Framework | Spring Boot | 2.7.18 (Java 17) |
| Base de datos | PostgreSQL | 14 |
| Firma JWS | JOSE4J | 0.9.3 |
| Generación PDF | JasperReports | 6.21.3 |
| Código QR | ZXing | 3.5.3 |
| Parsing JSON | org.json | 20231013 |
| Correo electrónico | Spring Mail (JavaMail) | dinámico |
| Contenedor | Docker | (multi-stage JDK 17 Alpine) |
4. Componentes del Sistema
4.1 WorkerScheduler
Jobs programados que ejecutan el ciclo de transmisión:
| Job | Intervalo | Función |
|---|---|---|
procesarCola() |
30 s | Procesa DTEs PENDING |
procesarInvalidaciones() |
30 s | Procesa eventos de anulación PENDING |
procesarContingencias() |
30 s | Procesa eventos de contingencia PENDING |
procesarPendientesEmail() |
60 s | Genera PDF y envía correo de facturas SENT |
4.2 FirmadorService
Firma el JSON del DTE con el certificado PKCS12 de la empresa:
- Carga
KeyStoredesde los bytes del.p12almacenado enemh_credentials.cert_file - Extrae la
PrivateKeycon la contraseña del certificado - Crea un
JsonWebSignaturecon algoritmoRSA_USING_SHA512(requisito MH) - Retorna el JWS:
header.payload.signature(cadena compacta)
4.3 HaciendaService
Comunicación con el API del MH:
obtenerToken(user, pass, ambiente)— POST a/seguridad/auth; token cacheado 4 horasenviarDte(jws, user, pass, ambiente)— POST a/fesv/recepciondtecon envelope:json { "ambiente": "00", "idEnvio": 1, "version": 1, "tipoDte": "01", "documento": "<JWS>" }anularDte(jws, ...)— POST a/fesv/anulardteenviarContingencia(jws, nit, ...)— POST a/fesv/contingencia
Códigos de respuesta MH:
- 001 = Recibido
- 002 = Procesado → estado SENT
- Cualquier otro código → estado ERROR
4.4 DatabaseService
Todas las consultas JDBC al schema adempiere:
- Polling:
findPendingInvoices(),findPendingInvalidaciones(),findPendingContingencias(),findPendingEmails() - Generación JSON: llama a
emh_preparar_dte(c_invoice_id)yemh_preparar_eret(emh_eret_id) - Credenciales:
getCredentials(clientId, orgId)desdeemh_credentials - Logging:
logResponse()inserta enemh_docsline - Estado:
updateInvoiceStatus(),markAsProcessing()
4.5 PdfService
Genera el PDF de representación gráfica del DTE:
- Plantillas
.jrxml(JasperReports) en/app/reports/dtes/generic/dte_{tipodoc}.jrxml - Código QR apuntando a
https://admin.factura.gob.sv/consultaPublica?ambiente={00|01}&codGen={UUID}&fechaEmi={fecha} - Soporta todos los tipos: FAC, CCF, NR, NC, ND, CR, FEX, FSE, CD
- Detección automática de tipo por estructura del JSON (cuerpoDocumento)
- Guarda el PDF y el JSON autorizado en
/app/dtes/{clientId}/{YYYY-MM}/
4.6 EmailService
Envío de correo con SMTP dinámico (configurable por empresa en emh_emailconfig):
- Busca email del receptor en
c_bpartner_locationoad_user - Adjunta PDF del DTE y JSON autorizado (
{UUID}.json) - Asunto:
{numeroControl} autorizado por el Ministerio de Hacienda - JSON adjunto incluye sección
autorizacion: { codigoGeneracion, numeroControl, selloRecibido, fhProcesamiento }
4.7 AdminController
API REST y UI de administración accesible en http://host:8585/.
5. Flujo Completo de un DTE
KontrolesERP: Usuario completa factura (docstatus='CO', processed='Y')
└─ emh_status = 'PENDING' (automático)
WorkerScheduler.procesarCola() — cada 30 segundos:
│
├─ 1. SELECT c_invoice WHERE emh_status='PENDING' AND docstatus='CO' (LIMIT 10)
├─ 2. UPDATE emh_status = 'PROCESSING'
├─ 3. SELECT emh_credentials WHERE ad_client_id=? AND ad_org_id=?
│
├─ 4. SELECT emh_preparar_dte(c_invoice_id) ← PostgreSQL
│ └─ emh_generarjson_v2(): lee emh_config_dte, ejecuta SQL dinámico
│ └─ Construye JSON: identificacion, emisor, receptor, cuerpoDocumento, resumen
│ └─ INSERT INTO emh_docs (numeroControl, codigoGeneracion, ws_json_send)
│ └─ RETURNS jsonb
│
├─ 5. FirmadorService.firmarDte(json, certBytes, certPass)
│ └─ KeyStore.load(PKCS12) → extrae PrivateKey
│ └─ JsonWebSignature RSA-SHA512
│ └─ RETURNS "eyJ...header.payload.signature"
│
├─ 6. HaciendaService.obtenerToken(user, pass, ambiente)
│ └─ POST /seguridad/auth → Bearer token (cache 4h)
│
├─ 7. HaciendaService.enviarDte(jws, ...)
│ └─ POST /fesv/recepciondte
│ └─ RETURNS { codigoMsg, descripcionMsg, selloRecibido }
│
├─ 8. INSERT emh_docsline (jws enviado, json respuesta, código, sello)
│
└─ 9. UPDATE c_invoice SET emh_status = 'SENT' | 'ERROR', emh_msg = descripcionMsg
WorkerScheduler.procesarPendientesEmail() — cada 60 segundos:
├─ SELECT facturas SENT con sello válido y sin email enviado
├─ PdfService.generateAndSave(invoiceId) → PDF con QR
├─ EmailService.send(smtp, toEmail, pdf, jsonAutorizado)
└─ INSERT emh_emaillog (status='SENT')
6. Tipos de Documento Soportados
| Código | Nombre | Aplica sobre | Notas |
|---|---|---|---|
01 |
Factura Electrónica (FAC) | Consumidor final | |
03 |
Comprobante de Crédito Fiscal (CCF) | Empresas con NRC | |
04 |
Nota de Remisión (NR) | CCF | Traslado de bienes |
05 |
Nota de Crédito (NC) | CCF | Descuentos/devoluciones |
06 |
Nota de Débito (ND) | CCF | Cargos adicionales |
07 |
Comprobante de Retención (CR) | — | Desde tabla emh_retencion |
11 |
Factura de Exportación (FEX) | Exterior | Con recinto fiscal/régimen |
14 |
Factura de Sujeto Excluido (FSE) | Sujetos excluidos | Campo compra en vez de ventaGravada |
15 |
Comprobante de Donación (CD) | — | Donante/donatario |
18 |
Evento de Retorno (ERET) | FAC/FEX/FSE validadas | Tabla propia emh_eret; no usa c_invoice |
Los tipos 01–15 usan c_invoice como fuente y se generan vía emh_generarjson_v2().
El tipo 18 (ERET) usa tablas propias y se genera vía emh_generarjson_eret().
7. Esquema de Base de Datos
Tablas propias del sistema EMH
emh_credentials
Credenciales por empresa/sucursal para conectarse al API del MH.
| Columna | Tipo | Descripción |
|---|---|---|
ad_client_id |
NUMERIC | ID empresa KontrolesERP |
ad_org_id |
NUMERIC | ID sucursal |
mh_user |
VARCHAR(20) | Usuario API MH (generalmente el NIT) |
mh_pass |
VARCHAR(100) | Contraseña API MH |
cert_file |
BYTEA | Certificado PKCS12 (.p12) en binario |
cert_pass |
VARCHAR(100) | Contraseña del certificado |
ambiente |
CHAR(2) | 00=Pruebas, 01=Producción |
emh_seqs
Secuencias del correlativo numeroControl por tipo DTE.
| Columna | Tipo | Descripción |
|---|---|---|
tipodoc |
VARCHAR(2) | 01, 03, 04, etc. |
serie |
VARCHAR(8) | codEstable(4) + codPuntoVenta(4) p.ej. 00010001 |
currentnext |
INTEGER | Próximo número a usar |
El numeroControl resultante tiene el formato: DTE-{tipodoc}-{codEstableMH}{codPuntoVentaMH}-{año}{correlativo15}.
Ejemplo: DTE-01-00010001-260000000000001
emh_docs
Un registro por factura: contiene el JSON enviado al MH.
| Columna | Tipo | Descripción |
|---|---|---|
c_invoice_id |
NUMERIC | Referencia a la factura |
ws_tipodoc |
VARCHAR(2) | Tipo DTE |
ws_numerocontrol |
VARCHAR(40) | Número de control completo |
ws_codigogeneracion |
VARCHAR(36) | UUID único del DTE |
ws_json_send |
JSONB | JSON del DTE sin firmar |
contingencia |
CHAR(1) | Y=emitido en contingencia |
emh_docsline
Un registro por intento de transmisión al MH.
| Columna | Tipo | Descripción |
|---|---|---|
emh_docs_id |
INTEGER | Referencia a emh_docs |
ws_json_send |
TEXT | JWS firmado que se envió |
ws_json_receipt |
JSONB | Respuesta del MH |
ws_codigomsg |
VARCHAR(10) | 001=Recibido, 002=Procesado, otros=Error |
ws_descripcionmsg |
VARCHAR(500) | Mensaje de respuesta |
ws_sellorecibido |
VARCHAR(200) | UUID sello del MH (si autorizado) |
emh_config_dte
Tabla de configuración para el constructor dinámico de JSON. Define qué campos JSON genera cada tipo de DTE.
| Columna | Tipo | Descripción |
|---|---|---|
tipdoc |
CHAR(3) | Tipo DTE: '01 ', '03 ', 'ALL'=compartido |
seccion |
VARCHAR(30) | Sección JSON: identificacion, emisor, receptor, etc. |
campo |
VARCHAR(50) | Nombre del campo JSON; 'RAIZ'=metadato de sección |
condicion |
CHAR(1) | R=requerido, O=opcional, N=siempre null |
tipodato |
VARCHAR(10) | text, integer, number, boolean, array, null, nullable |
orden |
SMALLINT | Orden del campo dentro de la sección |
orden_seccion |
SMALLINT | Orden de la sección en el JSON final |
sqltxt |
TEXT | Expresión SQL o cláusula FROM para el campo |
emh_invalidacion
Eventos de anulación de DTEs.
emh_contingencia / emh_contingencialine
Eventos de contingencia (transmisión diferida por fuerza mayor).
emh_emailconfig
Configuración SMTP por empresa/sucursal.
| Columna | Descripción |
|---|---|
smtp_host |
Servidor SMTP (p.ej. smtp.gmail.com) |
smtp_port |
Puerto (587=TLS, 465=SSL, 25=sin cifrado) |
smtp_user / smtp_pass |
Credenciales SMTP |
from_email / from_name |
Remitente |
use_tls |
Y/N |
reply_to |
Correo de respuesta (opcional) |
emh_emaillog
Historial de correos enviados. Estados: PENDING, SENT, ERROR, NO_EMAIL, NO_CONFIG.
emh_eret / emh_eret_ref / emh_eret_line
Evento de Retorno (ver sección 16).
Columnas EMH en tablas KontrolesERP
| Tabla | Columna | Descripción |
|---|---|---|
c_invoice |
emh_status |
PENDING → PROCESSING → SENT / ERROR |
c_invoice |
emh_msg |
Último mensaje de error o descripción MH |
c_doctype |
emh_tipodoc |
Código DTE: 01, 03, 04... 15; NULL=no aplica FE |
ad_orginfo |
emh_nrc |
NRC del emisor (sin guiones) |
ad_orginfo |
emh_codactividad |
Código actividad económica CAT-019 |
ad_orginfo |
emh_descactividad |
Descripción de la actividad |
ad_orginfo |
emh_nombrecomercial |
Nombre comercial (opcional) |
ad_orginfo |
emh_tipoestablec |
01=Sucursal, 02=Casa Matriz, 07=Bodega |
ad_orginfo |
emh_codestablemh |
Código establecimiento asignado por MH (4 dígitos) |
ad_orginfo |
emh_codpuntoventamh |
Código punto de venta asignado por MH (4 dígitos) |
ad_orginfo |
emh_codestable |
Código establecimiento interno (4 dígitos) |
ad_orginfo |
emh_codpuntoventa |
Código punto de venta interno (4 dígitos) |
c_region |
emh_deptcode |
Código departamento MH (01–14) |
c_location |
emh_municipiocode |
Código municipio MH (01–33 según dpto.) |
c_bpartner |
emh_codactividad |
Actividad económica del receptor (para CCF) |
8. Generación Dinámica de JSON
El JSON de cada DTE se construye en PostgreSQL mediante la función emh_generarjson_v2(p_c_invoice_id).
Cómo funciona
- Lee todas las filas de
emh_config_dtepara eltipdocde la factura (más las filas'ALL'compartidas conemisor) - Agrupa por
seccion(ordenada pororden_seccion) - Para cada sección, evalúa cada campo según su
tipodato:null→ emite"seccion": nullnullable→ emite null si la consulta no retorna datosarray→ ejecuta elsqltxtcomo subconsulta y serializa como array JSONB- otros → ejecuta
sqltxtcomo expresión SQL en el contextoFROM c_invoice i JOIN ...
- Construye el árbol JSON y lo retorna como
JSONB
Configurar un campo nuevo
Para agregar o modificar un campo en el JSON de un tipo DTE:
INSERT INTO adempiere.emh_config_dte
(tipdoc, seccion, campo, condicion, tipodato, orden, orden_seccion, sqltxt)
VALUES
('01 ', 'receptor', 'telefono', 'O', 'text', 15, 4,
'bp.phone')
ON CONFLICT (tipdoc, seccion, campo) DO UPDATE
SET sqltxt = EXCLUDED.sqltxt, condicion = EXCLUDED.condicion;
tipdocdebe ser CHAR(3):'01 '(con espacio),'03 ', o'ALL'condicion = 'R'lanza error si el valor es NULLcondicion = 'O'omite el campo si es NULLcondicion = 'N'siempre emitenull
9. API REST de Administración
Base URL: http://host:8585/admin
Credenciales MH
| Método | Ruta | Descripción |
|---|---|---|
GET |
/credenciales |
Lista credenciales por empresa (sin datos del cert) |
POST |
/credenciales |
Crea o actualiza credencial; acepta .p12 como multipart/form-data |
DELETE |
/credenciales/{id} |
Desactiva credencial |
Campos POST: adClientId, adOrgId, mhUser, mhPass, certPass, ambiente (00/01), certFile (archivo .p12, opcional si ya existe).
Empresa / Emisor (ad_orginfo)
| Método | Ruta | Descripción |
|---|---|---|
GET |
/empresas |
Lista datos de emisor por organización |
POST |
/empresas |
Actualiza datos del emisor |
Campos: adOrgId, taxid (NIT), emhNrc, emhCodActividad, emhDescActividad, emhNombreComercial, emhTipoEstablec, emhCodEstableMH, emhCodEstable, emhCodPuntoVentaMH, emhCodPuntoVenta, emhCorreo.
Tipos DTE (c_doctype)
| Método | Ruta | Descripción |
|---|---|---|
GET |
/doctypes |
Lista tipos de documento con su emh_tipodoc |
GET |
/doctypes/conteos |
Estadísticas de envíos por tipo (aprobados / total) |
POST |
/doctypes/{id} |
Asigna emh_tipodoc a un doctype |
Secuencias de Numeración
| Método | Ruta | Descripción |
|---|---|---|
GET |
/seqs |
Lista correlativas actuales por tipo DTE |
POST |
/seqs |
Crea o actualiza una secuencia |
POST |
/seqs/init |
Inicializa secuencias para todos los tipos configurados |
Campos POST: adClientId, adOrgId, tipodoc, serie (p.ej. 00010001), currentnext.
Invalidaciones
| Método | Ruta | Descripción |
|---|---|---|
GET |
/invalidaciones |
Lista últimas 50 invalidaciones |
POST |
/invalidaciones |
Crea evento de anulación (worker lo procesa en ~30s) |
Contingencias
| Método | Ruta | Descripción |
|---|---|---|
GET |
/contingencias |
Lista últimas 50 contingencias |
POST |
/contingencias |
Crea evento de contingencia |
POST |
/contingencias/{id}/linea |
Agrega factura a un evento de contingencia |
Correo Electrónico
| Método | Ruta | Descripción |
|---|---|---|
GET |
/emailconfig |
Lista configuraciones SMTP |
POST |
/emailconfig |
Crea o actualiza configuración SMTP |
GET |
/emaillog |
Historial de correos (?limit=50) |
Facturas
| Método | Ruta | Descripción |
|---|---|---|
GET |
/invoices/{id}/pdf |
Genera y descarga el PDF de una factura |
POST |
/invoices/{id}/reenviar-email |
Reenvía el correo de una factura SENT |
Generación de Pruebas (Certificación)
| Método | Ruta | Descripción |
|---|---|---|
POST |
/prueba/generar |
Genera documentos de prueba PENDING para certificación MH |
Parámetros: tipodoc (01, 03, 05, etc.), cantidad, monto.
Crea automáticamente c_invoice + c_invoiceline con el BPartner adecuado según el tipo.
Para CR(07) referencia CCFs previamente aprobados de emh_retencion.
Filebrowser (Gestor de Archivos DTE)
| Método | Ruta | Descripción |
|---|---|---|
GET |
/fileshare |
Lista configuración de acceso por empresa |
POST |
/fileshare |
Crea usuario en Filebrowser + registra en BD |
DELETE |
/fileshare/{id} |
Desactiva acceso |
10. Interfaz Web de Administración
Acceder en http://host:8585/ (o el dominio configurado).
La UI tiene pestañas para cada sección del API:
| Pestaña | Función |
|---|---|
| Credenciales | Cargar certificado .p12 y configurar usuario/contraseña MH |
| Secuencias | Ver y ajustar el correlativo actual por tipo DTE |
| Empresa / Emisor | Configurar NRC, actividad económica, códigos de establecimiento |
| Tipos DTE | Asignar emh_tipodoc a cada tipo de documento KontrolesERP |
| Invalidaciones | Crear y monitorear eventos de anulación |
| Contingencias | Crear eventos de contingencia y agregar facturas |
| Configurar SMTP y ver historial de correos | |
| Pruebas | Generar documentos de prueba para certificación MH |
11. Despliegue
Variables de entorno clave
SPRING_DATASOURCE_URL=jdbc:postgresql://host.docker.internal:5432/db00_kontroleserp?currentSchema=adempiere
SPRING_DATASOURCE_USERNAME=adempiere
SPRING_DATASOURCE_PASSWORD=4dm1n!3RP
DTE_POLLING_INTERVAL=30000
DTE_EMAIL_POLLING_INTERVAL=60000
DTE_PDF_OUTPUT_DIR=/app/dtes
DTE_REPORTS_DIR=/app/reports/dtes
docker-compose.yml (extracto)
dte-worker:
image: rocarsadecv/dte-worker:latest
ports:
- "8585:8080"
volumes:
- ./dtes:/app/dtes
- ./reports:/app/reports
- ./logs:/app/logs
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- SPRING_DATASOURCE_URL=...
Aplicar DDL en base de datos nueva
Ejecutar en orden:
psql -U postgres -d db00_kontroleserp -f src/main/java/com/kontroles/sqlscript/emh_tables_ddl.sql
psql -U postgres -d db00_kontroleserp -f src/main/java/com/kontroles/sqlscript/emh_views_functions.sql
psql -U postgres -d db00_kontroleserp -f src/main/java/com/kontroles/sqlscript/emh_cat_ubicacion.sql
psql -U postgres -d db00_kontroleserp -f src/main/java/com/kontroles/sqlscript/emh_generarjson_v2.sql
psql -U postgres -d db00_kontroleserp -f src/main/java/com/kontroles/sqlscript/emh_generarjson_eret.sql
Nota: Los scripts de DDL son idempotentes (
IF NOT EXISTS). Se pueden ejecutar múltiples veces sin efectos secundarios.
12. Configuración Inicial Post-Despliegue
Paso 1: Datos del emisor
En la pestaña Empresa / Emisor:
- NIT: sin guiones (p.ej.
06071708201010) - NRC: sin guiones
- Código actividad: código de 6 dígitos del CAT-019 MH (p.ej.
451002) - Descripción actividad: descripción textual de la actividad
- Tipo establecimiento:
01=Sucursal/Agencia,02=Casa Matriz,07=Bodega - Código establecimiento MH (
codEstableMH): código de 4 dígitos asignado por MH (p.ej.0001) - Código punto de venta MH (
codPuntoVentaMH): código de 4 dígitos asignado por MH (p.ej.0001)
Paso 2: Credenciales MH
En la pestaña Credenciales:
- Seleccionar empresa/sucursal
- Ingresar el usuario API del MH (generalmente el NIT)
- Ingresar la contraseña API del MH
- Subir el archivo
.p12del certificado firmado por el MH - Ingresar la contraseña del certificado
.p12 - Seleccionar ambiente:
00=Pruebas,01=Producción
Paso 3: Tipos DTE
En la pestaña Tipos DTE, asignar el código emh_tipodoc a cada tipo de documento de KontrolesERP:
| Doctype KontrolesERP | emh_tipodoc |
|---|---|
| Factura Consumidor Final | 01 |
| Comprobante de Crédito Fiscal | 03 |
| Nota de Remisión | 04 |
| Nota de Crédito | 05 |
| Nota de Débito | 06 |
| Comprobante de Retención | 07 |
| Factura de Exportación | 11 |
| Factura Sujeto Excluido | 14 |
| Comprobante de Donación | 15 |
Los tipos sin asignación no serán procesados por el worker.
Paso 4: Secuencias
En la pestaña Secuencias, usar el botón Inicializar secuencias o crear manualmente:
- Serie: concatenación de
codEstable(4)+codPuntoVenta(4), p.ej.00010001 - Correlativo inicial: normalmente
1(para ambiente de pruebas puede reiniciarse)
Paso 5: Configuración de correo (opcional)
En la pestaña Email:
- Servidor SMTP, puerto, usuario, contraseña
- Activar TLS si aplica
- Correo y nombre del remitente
13. Operación y Monitoreo
Estados de una factura
PENDING → PROCESSING → SENT
└→ ERROR (reintentable manualmente)
- PENDING: recién creada en KontrolesERP, esperando al worker
- PROCESSING: el worker la tomó, en proceso de firma/envío
- SENT: MH retornó código
001o002con sello de recepción - ERROR: MH rechazó el documento; ver
emh_msgen la factura
Consultar estado de una factura
SELECT i.documentno, i.emh_status, i.emh_msg,
d.ws_numerocontrol, d.ws_codigogeneracion,
dl.ws_codigomsg, dl.ws_descripcionmsg, dl.ws_sellorecibido
FROM adempiere.c_invoice i
LEFT JOIN adempiere.emh_docs d ON d.c_invoice_id = i.c_invoice_id
LEFT JOIN adempiere.emh_docsline dl ON dl.emh_docs_id = d.emh_docs_id
WHERE i.c_invoice_id = <ID>;
Forzar reintento
Para reintentar una factura en estado ERROR:
UPDATE adempiere.c_invoice
SET emh_status = 'PENDING', emh_msg = NULL
WHERE c_invoice_id = <ID>;
El worker la tomará en el próximo ciclo de 30 segundos.
Ver el JSON transmitido
SELECT ws_json_send FROM adempiere.emh_docs WHERE c_invoice_id = <ID>;
Ver respuesta del MH
SELECT ws_codigomsg, ws_descripcionmsg, ws_sellorecibido, ws_json_receipt
FROM adempiere.emh_docsline
WHERE emh_docs_id = (SELECT emh_docs_id FROM adempiere.emh_docs WHERE c_invoice_id = <ID>)
ORDER BY emh_docsline_id DESC LIMIT 1;
14. Invalidaciones
Para anular un DTE ya autorizado por el MH:
Vía API
POST /admin/invalidaciones
Content-Type: application/json
{
"cInvoiceId": 60001,
"tipoInvalidacion": 1,
"motivoInvalidacion": "Error en monto",
"nombreResponsable": "Juan Pérez",
"tipDocResponsable": "36",
"numDocResponsable": "06141234567890",
"nombreSolicita": "Ana García",
"tipDocSolicita": "13",
"numDocSolicita": "01234567-8"
}
Tipos de invalidación: 1=Error en documento, 2=Reemplaza documento existente, 3=Otro.
El worker procesará la invalidación en ~30 segundos y enviará al endpoint /fesv/anulardte.
Restricciones del MH
- No se puede invalidar una FAC/CCF si tiene una NC/ND relacionada con estado validado (primero invalidar la NC/ND)
- No se puede invalidar un FEX/FSE si tiene un ERET activo (primero invalidar el ERET)
- Los Eventos (EOP, ERET) tienen sus propias reglas de invalidación
15. Contingencias
La contingencia aplica cuando el sistema del MH no está disponible y el emisor debe seguir operando offline.
Proceso
Momento 1 — Ocurre la contingencia:
- El sistema detecta que el MH no responde
- Se siguen emitiendo DTEs con tipoModelo=2 (Diferido) y tipoOperacion=2 (Contingencia)
- Se registra el tipo de contingencia (catálogo CAT-005)
Momento 2 — Recuperada la conectividad (máx. 24h):
1. Crear el evento de contingencia en la pestaña Contingencias
2. Agregar todos los DTEs emitidos durante la contingencia al evento
3. El worker envía el evento a /fesv/contingencia
4. MH retorna sello de recepción del evento
Momento 3 — Transmitir los DTEs (máx. 72h desde sello del evento):
- Los DTEs en estado PENDING con contingencia='Y' se envían normalmente
- El worker los procesa automáticamente
16. Evento de Retorno (ERET)
El Evento de Retorno (tipoEvento=18, esquema fe-eret-v1.json) se usa para retornos de bienes en operaciones amparadas en FE, FEXE o FSEE.
Cuándo aplica
- Reembolsos al cliente
- Devolución de envases/empaques
- Para FEXE: disminución de exportación
- Para FSEE: disminución de la compra
No aplica sobre CCF — para CCF usar Nota de Crédito (NC, tipo 05).
Tablas
emh_eret — Cabecera del evento
emh_eret_ref — DTEs relacionados (máx. 50; tipo_documento + codigo_generacion)
emh_eret_line — Cuerpo: detalle de bienes/servicios devueltos
Crear un ERET (SQL directo)
-- 1. Cabecera
INSERT INTO adempiere.emh_eret (
ad_client_id, ad_org_id, fec_emi, hor_emi,
doc_tipo_documento, doc_num_documento, doc_nombre,
doc_telefono, doc_correo
) VALUES (
1000000, 1000000, CURRENT_DATE, CURRENT_TIME,
'DUI', '01234567-8', 'Juan Pérez',
'78901234', 'juan@email.com'
) RETURNING emh_eret_id;
-- 2. DTE al que aplica el retorno
INSERT INTO adempiere.emh_eret_ref (emh_eret_id, c_invoice_id, tipo_documento, codigo_generacion, fecha_emision)
VALUES (1, 60026, '01', 'C9D311ED-A610-49D2-AA79-81B01D11E021', '2026-06-11');
-- 3. Detalle del retorno
INSERT INTO adempiere.emh_eret_line (
emh_eret_id, num_item, tipo_item, codigo_generacion,
cantidad, precio_uni, descripcion, uni_medida,
monto_descu, cod_tributo, venta_gravada, iva_item, tributos
) VALUES (
1, 1, 1, 'C9D311ED-A610-49D2-AA79-81B01D11E021',
1, 100.00, 'Devolución de producto', 59,
0, '20', 100.00, 13.00, ARRAY['20']::CHAR(2)[]
);
-- 4. Generar y verificar el JSON
SELECT adempiere.emh_preparar_eret(1);
Restricciones del MH
- Plazo: 3 meses desde el sello de recepción del DTE original
- Para actividades de turismo (códigos 01282, 21001, 46484, 47721): 2 años
- Máximo 50 DTEs por evento
- Todos los DTEs deben corresponder al mismo Emisor + Receptor + Tercero
- Se pueden aplicar múltiples ERET a un DTE siempre que la suma no supere el valor original
17. Generación de PDFs y Envío de Correo
Plantillas PDF
Ubicación en el contenedor: /app/reports/dtes/generic/dte_{tipodoc}.jrxml
El sistema selecciona automáticamente la plantilla según el tipo de DTE.
Características comunes de todas las plantillas:
- Código QR en esquina superior derecha vinculado al portal de consulta del MH
- Sello de recepción (ws_sellorecibido)
- Número de control
- Datos completos del emisor y receptor
Descarga manual de PDF
GET /admin/invoices/{c_invoice_id}/pdf
Genera el PDF en tiempo real y lo descarga directamente sin enviar correo.
Reenvío de correo
POST /admin/invoices/{c_invoice_id}/reenviar-email
Reenvía el correo con PDF y JSON autorizado a la dirección del receptor.
Estructura del JSON autorizado (adjunto en correo)
{
"identificacion": { ... },
"emisor": { ... },
"receptor": { ... },
"cuerpoDocumento": [ ... ],
"resumen": { ... },
"autorizacion": {
"codigoGeneracion": "UUID-del-DTE",
"numeroControl": "DTE-01-00010001-260000000000001",
"selloRecibido": "UUID-sello-MH",
"fhProcesamiento": "2026-06-11T10:00:00"
}
}
18. Ambiente de Pruebas y Certificación MH
El MH requiere transmitir un mínimo de documentos de cada tipo antes de certificar para producción.
Mínimos requeridos (referencia INROCAR)
| Tipo | Mínimo | Estado |
|---|---|---|
| FAC (01) | 90 | ✓ |
| CCF (03) | 75 | ✓ |
| NC (05) | 50 | ✓ |
| NR (04) | 6 | ✓ |
| CR (07) | 6 | ✓ |
| FEX (11) | 6 | ✓ |
| FSE (14) | 6 | ✓ |
| CD (15) | 2 | ✓ |
Generar documentos de prueba
POST /admin/prueba/generar?tipodoc=01&cantidad=10&monto=100
Crea documentos con estado PENDING que el worker transmitirá automáticamente.
Diferencias entre ambiente de pruebas y producción
| Aspecto | Pruebas (00) |
Producción (01) |
|---|---|---|
| URL base | apitest.dtes.mh.gob.sv |
api.dtes.mh.gob.sv |
| Credenciales | Usuario/pass de prueba MH | Credenciales oficiales |
| Certificado | .p12 de pruebas |
.p12 oficial firmado por MH |
ambiente en JSON |
"00" |
"01" |
Para cambiar de pruebas a producción: actualizar ambiente en emh_credentials.
19. Resolución de Problemas
Factura en ERROR: cómo diagnosticar
Ver el mensaje de error:
SELECT emh_msg FROM adempiere.c_invoice WHERE c_invoice_id = <ID>;Ver la respuesta completa del MH:
SELECT ws_codigomsg, ws_descripcionmsg, ws_json_receipt FROM adempiere.emh_docsline dl JOIN adempiere.emh_docs d ON d.emh_docs_id = dl.emh_docs_id WHERE d.c_invoice_id = <ID> ORDER BY dl.emh_docsline_id DESC LIMIT 1;Ver el JSON que se intentó enviar:
SELECT ws_json_send FROM adempiere.emh_docs WHERE c_invoice_id = <ID>;
Errores comunes del MH
| Código/Error | Causa | Solución |
|---|---|---|
Token inválido |
Credenciales incorrectas | Verificar mh_user/mh_pass en emh_credentials |
NIT no autorizado |
NIT no registrado en MH | Verificar que el NIT esté activo en el portal MH |
Número de control duplicado |
Correlativo ya usado | Revisar emh_seqs.currentnext y avanzar al siguiente |
Campo X requerido |
JSON incompleto | Revisar emh_config_dte para ese campo; verificar datos del BP |
codEstableMH inválido |
Código de establecimiento incorrecto | Actualizar emh_codestablemh en ad_orginfo con el valor del MH |
204.1 ventaTercero |
DTE relacionado no corresponde | Para CL/DCL: verificar que los CCFs referenciados sean del tercero correcto |
El worker no procesa facturas
- Verificar que el contenedor esté corriendo:
docker ps | grep dte-worker - Ver logs:
docker logs dte-worker --tail=50 - Verificar conectividad a BD: el log mostrará errores de conexión
- Verificar que
docstatus='CO'yprocessed='Y'en la factura
Reiniciar el procesamiento de todos los errores
UPDATE adempiere.c_invoice
SET emh_status = 'PENDING', emh_msg = NULL
WHERE emh_status = 'ERROR'
AND ad_client_id = 1000000;
Precaución: solo hacer esto si se ha corregido la causa del error; de lo contrario se volverán a rechazar.
Versión del documento: junio 2026 — Compatible con Manual de Transmisión MH v2.0 (05/2026)