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

  1. Descripción General
  2. Arquitectura
  3. Stack Tecnológico
  4. Componentes del Sistema
  5. Flujo Completo de un DTE
  6. Tipos de Documento Soportados
  7. Esquema de Base de Datos
  8. Generación Dinámica de JSON
  9. API REST de Administración
  10. Interfaz Web de Administración
  11. Despliegue
  12. Configuración Inicial Post-Despliegue
  13. Operación y Monitoreo
  14. Invalidaciones
  15. Contingencias
  16. Evento de Retorno (ERET)
  17. Generación de PDFs y Envío de Correo
  18. Ambiente de Pruebas y Certificación MH
  19. 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 KeyStore desde los bytes del .p12 almacenado en emh_credentials.cert_file
  • Extrae la PrivateKey con la contraseña del certificado
  • Crea un JsonWebSignature con algoritmo RSA_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 horas
  • enviarDte(jws, user, pass, ambiente) — POST a /fesv/recepciondte con envelope: json { "ambiente": "00", "idEnvio": 1, "version": 1, "tipoDte": "01", "documento": "<JWS>" }
  • anularDte(jws, ...) — POST a /fesv/anulardte
  • enviarContingencia(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) y emh_preparar_eret(emh_eret_id)
  • Credenciales: getCredentials(clientId, orgId) desde emh_credentials
  • Logging: logResponse() inserta en emh_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_location o ad_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 0115 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 PENDINGPROCESSINGSENT / 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 (0114)
c_location emh_municipiocode Código municipio MH (0133 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

  1. Lee todas las filas de emh_config_dte para el tipdoc de la factura (más las filas 'ALL' compartidas con emisor)
  2. Agrupa por seccion (ordenada por orden_seccion)
  3. Para cada sección, evalúa cada campo según su tipodato:
    • null → emite "seccion": null
    • nullable → emite null si la consulta no retorna datos
    • array → ejecuta el sqltxt como subconsulta y serializa como array JSONB
    • otros → ejecuta sqltxt como expresión SQL en el contexto FROM c_invoice i JOIN ...
  4. 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;
  • tipdoc debe ser CHAR(3): '01 ' (con espacio), '03 ', o 'ALL'
  • condicion = 'R' lanza error si el valor es NULL
  • condicion = 'O' omite el campo si es NULL
  • condicion = 'N' siempre emite null

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
Email 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:

  1. Seleccionar empresa/sucursal
  2. Ingresar el usuario API del MH (generalmente el NIT)
  3. Ingresar la contraseña API del MH
  4. Subir el archivo .p12 del certificado firmado por el MH
  5. Ingresar la contraseña del certificado .p12
  6. 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 001 o 002 con sello de recepción
  • ERROR: MH rechazó el documento; ver emh_msg en 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

  1. Ver el mensaje de error:

    SELECT emh_msg FROM adempiere.c_invoice WHERE c_invoice_id = <ID>;
    
  2. 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;
    
  3. 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

  1. Verificar que el contenedor esté corriendo: docker ps | grep dte-worker
  2. Ver logs: docker logs dte-worker --tail=50
  3. Verificar conectividad a BD: el log mostrará errores de conexión
  4. Verificar que docstatus='CO' y processed='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)