Untitled static subpage: Thursday, 25 June 2026, 19:44

Date: 2026-06-25 00:08 Title: KontrolesERP — Guía de Despliegue y Ecosistema Type: page Description: Arquitectura completa, post-inicialización de base de datos, configuración DTE y checklist para nueva instalación o sincronización

KontrolesERP — Guía de Despliegue y Ecosistema

Versión: 2026-06-03
Stack: KontrolesERP · PostgreSQL 14 · Spring Boot 17 · PHP 8.2 · Docker Compose


ÍNDICE

  1. Ecosistema — Arquitectura completa
  2. Post-inicialización de BD — Configuración DTE
  3. Checklist — Nueva instalación o sincronización

1. ECOSISTEMA

1.1 Mapa de contenedores y flujos

                        INTERNET
                            │
                     puerto 80/443
                            │
                    ┌───────▼────────┐
                    │  nginx_proxy   │  jc21/nginx-proxy-manager
                    │  (NPM)         │  Admin UI: puerto 81
                    └───────┬────────┘
                            │  SSL / reverse proxy por subdominio
          ┌─────────────────┼─────────────────────────────────┐
          │                 │                 │                │
   /erp ──▼──        /admin─▼─         /files▼─         /pos,/rta,/api
┌──────────────┐  ┌──────────────┐  ┌──────────┐   ┌──────────────────┐
│   erp_app    │  │  dte-worker  │  │filebrowser│   │  kontrolesapi    │
│  (KontrolesERP) │  │ Spring Boot  │  │  :80     │   │   PHP+Apache     │
│   :8888      │  │   :8080      │  └────┬─────┘   └───────┬──────────┘
└──────┬───────┘  └──────┬───────┘       │                 │
       │                 │        ./dtes  │          ┌──────┴──────┐
       │  R/W            │  R/W  (volume) │          │  pos_pwa    │
       └────────┬────────┘               │          │  :3000      │
                │                        │          ├─────────────┤
         ┌──────▼──────┐                 │          │  rta_pwa    │
         │  PostgreSQL  │                │          │  :3001      │
         │     :5432    │                │          └─────────────┘
         │db00_kontroles│                │
         └──────────────┘         ┌──────▼──────┐
                                  │  ./dtes/    │
                                  │{clientId}/  │
                                  │ pdfs/{yyyyMM}│
                                  │ json/{yyyyMM}│
                                  └─────────────┘

              dte-worker ─── HTTPS ──► MH (api.dtes.mh.gob.sv)

1.2 Servicios — descripción detallada

Contenedor Imagen Puerto interno Descripción
administrador-db-1 postgres:14-alpine 5432 BD principal. BD: db00_kontroleserp, schema: adempiere
erp_app rocarsadecv/inrocardevsuite:latest 8888, 4444 KontrolesERP — Tomcat embebido.
nginx_proxy jc21/nginx-proxy-manager:latest 80, 443, 81 Proxy reverso, SSL automático Let's Encrypt, panel UI en :81
dte-worker rocarsadecv/dte-worker:latest 8080 Firmador DTE El Salvador. Polls c_invoice PENDING, firma con PKCS12, envía a MH
kontrolesapi rocarsadecv/kontrolesapi:latest 8090→80 API REST PHP. Endpoints: /socios, /ventas, /catalogo, /auth
kontrolespos_pwa nginx:alpine 3001→80 PWA Punto de Venta. Proxy a kontrolesapi
kontrolesrta_pwa nginx:alpine 3000→80 PWA Rutas / Ventas. Proxy a kontrolesapi
filebrowser filebrowser/filebrowser:latest interno→80 Gestor de archivos DTE (PDF + JSON) por empresa
kontrolesweb rocarsadecv/kontrolesweb:latest 8099→80 Sitio corporativo HTMLy CMS

1.3 Volúmenes y directorios clave

{deploy_dir}/                          # directorio base en el servidor
├── KontrolesERPEnv.properties            # conexión BD para erp_app
├── docker-compose.yml
├── .env                               # PG_VOLUME_NAME, JWT_SECRET
├── nginx.conf                         # nginx_proxy (si aplica)
├── init/                              # solo en primer deploy (--with-db)
│   ├── 01_init_db.sql                 # crea usuario adempiere, extensiones
│   └── KontrolesERP_pg.dmp               # backup de la BD seed
├── reports/                           # ← montado en erp_app Y dte-worker
│   └── dtes/
│       └── generic/                   # JRXMLs genéricos por tipo DTE
│           ├── dte_01.jrxml           # FAC
│           ├── dte_03.jrxml           # CCF
│           ├── dte_04.jrxml           # NR
│           ├── dte_05.jrxml           # NC
│           ├── dte_06.jrxml           # ND
│           ├── dte_07.jrxml           # CR
│           ├── dte_08.jrxml           # CL
│           ├── dte_09.jrxml           # DCL
│           ├── dte_11.jrxml           # FEX
│           ├── dte_14.jrxml           # FSE
│           ├── dte_15.jrxml           # CD
│           └── dte_generic.jrxml      # fallback genérico
├── dtes/                              # ← montado en dte-worker Y filebrowser
│   └── {ad_client_id}/               # ej: 1000000
│       ├── pdfs/{yyyyMM}/             # PDFs autorizados
│       └── json/{yyyyMM}/             # JSONs autorizados
├── firmadordtes/
│   └── logs/
├── filebrowser/
│   ├── settings.json                  # baseURL=/files, root=/srv, port=80
│   └── filebrowser.db                 # base de datos interna filebrowser
├── kontrolesrta/
│   ├── frontend/                      # build de la PWA
│   └── nginx.conf
├── kontrolespos/
│   ├── frontend/
│   └── nginx.conf
└── kontrolesweb/
    ├── content/
    └── config/

1.4 Flujo de vida de un DTE

KontrolesERP (erp_app)
    │
    │  1. Usuario completa factura → docstatus='CO'
    │     Trigger emh_trg_invoice_pending() → emh_status='PENDING'
    │
    ▼
dte-worker (polling cada 30s)
    │
    │  2. SELECT c_invoice WHERE emh_status='PENDING'
    │            AND processed='Y' AND docstatus='CO'
    │
    │  3. emh_dtesjson(invoiceId) → genera JSON + inserta emh_docs
    │
    │  4. FirmadorService → firma PKCS12 + RSA-SHA512
    │
    │  5. POST api.dtes.mh.gob.sv/fesv/recepciondte
    │
    │  6. Respuesta MH → emh_docsline (codigoMsg, selloRecibido)
    │
    │  7. codigoMsg IN ('001','002') AND selloRecibido ≠ ''
    │     → emh_status='SENT'   (sino → 'ERROR')
    │
    ▼
Email scheduler (polling cada 60s)
    │
    │  8. Busca SENT con sello válido y sin email previo
    │
    │  9. PdfService.generateAndSave() → PDF con QR MH
    │     QR URL: admin.factura.gob.sv/consultaPublica?ambiente=&codGen=&fechaEmi=
    │
    │  10. EmailService.send() → PDF + JSON al receptor
    │
    └─► Archivos en ./dtes/{clientId}/pdfs/{yyyyMM}/ y json/{yyyyMM}/

1.5 Subdominio y URLs por cliente

URL Destino Notas
{empresa}.kontroles.com erp_app:8888 Panel ERP KontrolesERP
{empresa}.kontroles.com/admin dte-worker:8080 Configuración DTE, credenciales MH
{empresa}.kontroles.com/files filebrowser:80 PDFs y JSONs autorizados
{empresa}.kontroles.com (POS) kontrolespos_pwa:3001 PWA Punto de Venta
{empresa}.kontroles.com (RTA) kontrolesrta_pwa:3000 PWA Rutas/Ventas
host.kontroles.com erp_app / dte-worker Panel administrador INROCAR

2. POST-INIT BD — Configuración DTE

2.1 Paso 0 — Ejecutar schema DTE

# En el servidor, dentro del contenedor PostgreSQL:
docker exec -i administrador-db-1 psql -U postgres -d db00_kontroleserp \
  < emh_setup_completo.sql

Este script es idempotente (usa IF NOT EXISTS). Crea: - Columnas adicionales en tablas KontrolesERP (emh_status, emh_tipodoc, etc.) - Tablas propias: emh_credentials, emh_seqs, emh_docs, emh_docsline, emh_invalidacion, emh_contingencia, emh_emailconfig, emh_emaillog, emh_fileshare_config, c_bp_documents - Funciones: emh_dtesjson(), emh_anulacion_json(), emh_contingencia_json(), emh_numero_a_letras() - Vistas: emh_v_emisor, emh_v_receptor, emh_v_cuerpo - Trigger: emh_trg_invoice_pending (marca PENDING al completar factura)

2.2 Paso 1 — Datos del Emisor (AD_OrgInfo)

UPDATE adempiere.ad_orginfo SET
    emh_nrc              = '2926478',          -- NRC sin guiones
    emh_codactividad     = '74900',            -- Código CAT-019 MH
    emh_descactividad    = 'OTRAS ACTIVIDADES PROFESIONALES...',
    emh_nombrecomercial  = 'INROCAR',
    emh_tipoestablec     = '02',               -- 01=Sucursal 02=Casa Matriz 07=Bodega
    emh_correo           = 'no-reply@empresa.com',
    emh_codestablemh     = 'M001',             -- Asignado por MH (4 dígitos)
    emh_codestable       = '0001',             -- Código interno
    emh_codpuntoventamh  = 'P001',             -- Asignado por MH (4 dígitos)
    emh_codpuntoventa    = '0001'
WHERE ad_client_id = {AD_CLIENT_ID} AND ad_org_id = {AD_ORG_ID};

El NIT se obtiene de ad_org.value (debe estar en formato sin guiones).

2.3 Paso 2 — Credenciales MH

-- Subir certificado P12 como bytea:
INSERT INTO adempiere.emh_credentials (
    ad_client_id, ad_org_id,
    mh_user, mh_pass,
    cert_file,         -- bytea del archivo .p12
    cert_pass,
    ambiente           -- '00'=pruebas  '01'=producción
) VALUES (
    {AD_CLIENT_ID}, {AD_ORG_ID},
    '{usuario_mh}', '{password_mh}',
    pg_read_binary_file('/ruta/al/certificado.p12'),
    '{password_certificado}',
    '00'
);

Desde el panel admin: https://host.kontroles.com/admin → sección Credenciales.

2.4 Paso 3 — Secuencias DTE (emh_seqs)

Una fila por cada tipo de DTE que la empresa emita:

INSERT INTO adempiere.emh_seqs (ad_client_id, ad_org_id, tipodoc, serie, currentnext)
VALUES
    ({CLIENT}, {ORG}, '01', 'M001P001', 1),   -- FAC  (Factura)
    ({CLIENT}, {ORG}, '03', 'M001P001', 1),   -- CCF  (Crédito Fiscal)
    ({CLIENT}, {ORG}, '04', 'M001P001', 1),   -- NR   (Nota Remisión)
    ({CLIENT}, {ORG}, '05', 'M001P001', 1),   -- NC   (Nota Crédito)
    ({CLIENT}, {ORG}, '06', 'M001P001', 1),   -- ND   (Nota Débito)
    ({CLIENT}, {ORG}, '07', 'M001P001', 1),   -- CR   (Retención)
    ({CLIENT}, {ORG}, '08', 'M001P001', 1),   -- CL   (Liquidación)
    ({CLIENT}, {ORG}, '09', 'M001P001', 1),   -- DCL  (Doc. Contable Liq.)
    ({CLIENT}, {ORG}, '11', 'M001P001', 1),   -- FEX  (Exportación)
    ({CLIENT}, {ORG}, '14', 'M001P001', 1),   -- FSE  (Sujeto Excluido)
    ({CLIENT}, {ORG}, '15', 'M001P001', 1)    -- CD   (Donación)
ON CONFLICT (ad_client_id, ad_org_id, tipodoc) DO NOTHING;

serie = codEstableMH (4) || codPuntoVentaMH (4).
El campo currentnext se incrementa automáticamente con cada DTE emitido.

2.5 Paso 4 — DocTypes KontrolesERP ↔ Tipos DTE

-- Mapear cada c_doctype con su tipo DTE
UPDATE adempiere.c_doctype SET emh_tipodoc = '01'
WHERE ad_client_id={CLIENT} AND name ILIKE '%factura%'   AND issotrx='Y';

UPDATE adempiere.c_doctype SET emh_tipodoc = '03'
WHERE ad_client_id={CLIENT} AND name ILIKE '%crédito fiscal%';

UPDATE adempiere.c_doctype SET emh_tipodoc = '05'
WHERE ad_client_id={CLIENT} AND name ILIKE '%nota de crédito%';

UPDATE adempiere.c_doctype SET emh_tipodoc = '06'
WHERE ad_client_id={CLIENT} AND name ILIKE '%nota de débito%';

-- Verificar resultado:
SELECT c_doctype_id, name, emh_tipodoc
FROM adempiere.c_doctype
WHERE ad_client_id={CLIENT} AND emh_tipodoc IS NOT NULL;

2.6 Paso 5 — Departamentos (c_region.emh_deptcode)

UPDATE adempiere.c_region SET emh_deptcode='01' WHERE name ILIKE '%Ahuachapán%'    AND c_country_id=173;
UPDATE adempiere.c_region SET emh_deptcode='02' WHERE name ILIKE '%Santa Ana%'     AND c_country_id=173;
UPDATE adempiere.c_region SET emh_deptcode='03' WHERE name ILIKE '%Sonsonate%'     AND c_country_id=173;
UPDATE adempiere.c_region SET emh_deptcode='04' WHERE name ILIKE '%Chalatenango%'  AND c_country_id=173;
UPDATE adempiere.c_region SET emh_deptcode='05' WHERE name ILIKE '%Cuscatlán%'     AND c_country_id=173;
UPDATE adempiere.c_region SET emh_deptcode='06' WHERE name ILIKE '%La Libertad%'   AND c_country_id=173;
UPDATE adempiere.c_region SET emh_deptcode='07' WHERE name ILIKE '%San Salvador%'  AND c_country_id=173;
UPDATE adempiere.c_region SET emh_deptcode='08' WHERE name ILIKE '%Cabañas%'       AND c_country_id=173;
UPDATE adempiere.c_region SET emh_deptcode='09' WHERE name ILIKE '%San Vicente%'   AND c_country_id=173;
UPDATE adempiere.c_region SET emh_deptcode='10' WHERE name ILIKE '%La Paz%'        AND c_country_id=173;
UPDATE adempiere.c_region SET emh_deptcode='11' WHERE name ILIKE '%Usulután%'      AND c_country_id=173;
UPDATE adempiere.c_region SET emh_deptcode='12' WHERE name ILIKE '%San Miguel%'    AND c_country_id=173;
UPDATE adempiere.c_region SET emh_deptcode='13' WHERE name ILIKE '%Morazán%'       AND c_country_id=173;
UPDATE adempiere.c_region SET emh_deptcode='14' WHERE name ILIKE '%La Unión%'      AND c_country_id=173;

2.7 Paso 6 — Unidades de Medida MH (c_uom.emh_mhcode)

-- Valores más comunes (catálogo MH)
UPDATE adempiere.c_uom SET emh_mhcode=59 WHERE uomsymbol='Und';    -- Unidad
UPDATE adempiere.c_uom SET emh_mhcode=58 WHERE uomsymbol='Kg';     -- Kilogramo
UPDATE adempiere.c_uom SET emh_mhcode=57 WHERE uomsymbol='L';      -- Litro
UPDATE adempiere.c_uom SET emh_mhcode=99 WHERE emh_mhcode IS NULL; -- Otro (fallback)

2.8 Paso 7 — Configuración Email SMTP

INSERT INTO adempiere.emh_emailconfig (
    ad_client_id, ad_org_id,
    smtp_host, smtp_port, smtp_user, smtp_pass,
    from_email, from_name,
    use_tls
) VALUES (
    {CLIENT}, {ORG},
    'smtp.empresa.com', 587,
    'usuario@empresa.com', 'password',
    'facturacion@empresa.com', 'Empresa SA de CV',
    'Y'
)
ON CONFLICT (ad_client_id, ad_org_id) DO UPDATE SET
    smtp_host=EXCLUDED.smtp_host, smtp_port=EXCLUDED.smtp_port,
    smtp_user=EXCLUDED.smtp_user, smtp_pass=EXCLUDED.smtp_pass,
    from_email=EXCLUDED.from_email, from_name=EXCLUDED.from_name;

Desde el panel admin: https://host.kontroles.com/admin → sección Email Config.

2.9 Paso 8 — Filebrowser (gestor archivos DTE)

-- Registrar empresa en filebrowser_config
-- Esto crea el usuario en Filebrowser vía API POST /files/api/users
INSERT INTO adempiere.emh_fileshare_config (
    ad_client_id, username, password, scope
) VALUES (
    {CLIENT}, '{subdomain}', '{Password12chars!}', '/{CLIENT}'
);

La contraseña Filebrowser debe tener mínimo 12 caracteres.
El scope controla qué carpeta ve el usuario (generalmente /{ad_client_id}).

2.10 Paso 9 — NIT/NRC de clientes (c_bp_documents)

Para que los DTEs lleven los datos fiscales correctos del receptor:

-- NIT de un cliente
INSERT INTO adempiere.c_bp_documents (c_bpartner_id, ad_client_id, doc_type, doc_value)
VALUES ({BP_ID}, {CLIENT}, 'NIT', '00000000000000')  -- 14 dígitos sin guiones
ON CONFLICT DO NOTHING;

-- NRC de un cliente
INSERT INTO adempiere.c_bp_documents (c_bpartner_id, ad_client_id, doc_type, doc_value)
VALUES ({BP_ID}, {CLIENT}, 'NRC', '0000000')
ON CONFLICT DO NOTHING;

-- Actividad económica del cliente (opcional, mejora calidad del DTE)
UPDATE adempiere.c_bpartner SET
    emh_codactividad  = '45301',
    emh_descactividad = 'Venta de partes y piezas...'
WHERE c_bpartner_id = {BP_ID};

2.11 Paso 10 — Nginx Proxy Manager (NPM)

  1. Acceder a http://{servidor}:81
  2. Credenciales iniciales: admin@example.com / changeme → cambiar inmediatamente
  3. Crear Proxy Hosts para cada servicio:
Subdominio Tipo Forward Host Forward Port SSL
{empresa}.kontroles.com HTTP erp_app 8888 Let's Encrypt
{empresa}.kontroles.com/admin HTTP dte-worker 8080 mismo cert
{empresa}.kontroles.com/files HTTP filebrowser 80 mismo cert
{empresa}.kontroles.com (POS) HTTP kontrolespos_pwa 80 mismo cert
{empresa}.kontroles.com (RTA) HTTP kontrolesrta_pwa 80 mismo cert

2.12 Resumen del orden de ejecución

1. docker compose up -d                     ← levantar stack
2. psql < emh_setup_completo.sql            ← schema DTE (idempotente)
3. UPDATE ad_orginfo ...                    ← datos emisor
4. INSERT emh_credentials ...               ← credenciales MH
5. INSERT emh_seqs ...                      ← secuencias por tipo DTE
6. UPDATE c_doctype SET emh_tipodoc ...     ← mapeo doctypes
7. UPDATE c_region SET emh_deptcode ...     ← departamentos MH
8. UPDATE c_uom SET emh_mhcode ...          ← unidades medida MH
9. INSERT emh_emailconfig ...               ← SMTP
10. INSERT emh_fileshare_config ...         ← Filebrowser
11. Configurar NPM (subdominio + SSL)
12. Reiniciar dte-worker                    ← toma la config nueva

3. ACTUALIZACIÓN DE UN SERVIDOR EXISTENTE

Para aplicar mejoras de código, correcciones de SQL o nuevos diseños de PDF a un servidor ya instalado (TEST u otro cliente como JAASA), usar update_dte.sh:

Flujo completo de actualización
────────────────────────────────────────────────────────────────
PASO A (local): Aplicar cambios de código Java
    Editar PdfService.java, WorkerScheduler.java, etc.

PASO B (local): Construir y publicar nueva imagen Docker
    cd despliegue_cliente/
    ./build_all.sh dte          ← compila firmadorDtes y sube a DockerHub

PASO C (remoto): Aplicar al servidor destino
    ./update_dte.sh 192.168.1.99            ← TEST
    ./update_dte.sh 190.86.183.194          ← JAASA de CV

    Flags opcionales:
      --skip-sql    — no actualiza emh_config_dte (si solo cambió código)
      --skip-image  — no toca el contenedor (si solo cambiaron JRXML/SQL)
      --user root   — usuario SSH diferente al default (administrador)
────────────────────────────────────────────────────────────────

update_dte.sh ejecuta tres pasos en orden: 1. Sincroniza reports/dtes/ → el servidor (JRXMLs por tipo DTE y por cliente) 2. Aplica SQL (idempotente): emh_tables_ddlemh_views_functionsemh_generarjson_v2 - Crea columnas/tablas nuevas si aún no existen - Actualiza funciones y vistas - Actualiza filas de emh_config_dte con ON CONFLICT DO UPDATE - No toca: facturas, socios, credenciales MH, secuencias, ni datos del cliente 3. Actualiza imagen: docker compose pull dte-worker + docker compose up -d --no-deps dte-worker

Reportes JRXML personalizados por cliente

Colocar el diseño custom en reports/dtes/{ad_client_id}/dte_{tipodoc}.jrxml:

despliegue_cliente/
└── reports/
    └── dtes/
        ├── generic/           ← diseño genérico para todos
        │   ├── dte_01.jrxml
        │   ├── dte_03.jrxml
        │   └── ...
        └── 1000001/           ← diseño exclusivo de ad_client_id=1000001
            └── dte_01.jrxml   ← sobreescribe generic/dte_01.jrxml solo para este cliente

El worker detecta el JRXML modificado automáticamente (compara lastModified), sin necesidad de reiniciar el contenedor.


4. CHECKLIST — NUEVA INSTALACIÓN O SINCRONIZACIÓN

4.1 Infraestructura

  • [ ] Servidor Linux con Docker + Docker Compose instalados
  • [ ] Puertos abiertos: 80, 443, 81 (NPM), 22 (SSH), 5432 (opcional, solo LAN)
  • [ ] DNS: CNAME {empresa}.kontroles.com → IP del servidor (o dominio base)
  • [ ] Espacio en disco: mínimo 20 GB para BD + PDFs/JSONs
  • [ ] RAM: mínimo 4 GB (recomendado 8 GB para múltiples clientes)

4.2 Repositorios y secretos

  • [ ] Clonar o actualizar despliegue_cliente/ en el servidor
  • [ ] Crear .env con PG_VOLUME_NAME y JWT_SECRET
  • [ ] Verificar credenciales DockerHub en dte-secrets.properties (para push, no para pull)
  • [ ] Certificados MH (.p12) disponibles y accesibles
  • [ ] kontroles-secrets.properties con passwords de DockerHub

4.3 Imágenes Docker (DockerHub: rocarsadecv)

  • [ ] rocarsadecv/inrocardevsuite:latest — ERP
  • [ ] rocarsadecv/dte-worker:latest — Firmador DTE
  • [ ] rocarsadecv/kontrolesapi:latest — API REST
  • [ ] rocarsadecv/kontrolesweb:latest — Sitio web
docker pull rocarsadecv/inrocardevsuite:latest
docker pull rocarsadecv/dte-worker:latest
docker pull rocarsadecv/kontrolesapi:latest
docker pull rocarsadecv/kontrolesweb:latest

4.4 Base de Datos

Instalación nueva (primer deploy)

  • [ ] Preparar backup seed: KontrolesERP_pg.dmp en init/
  • [ ] Preparar init/01_init_db.sql (crea usuario adempiere, extensiones pgcrypto, uuid-ossp)
  • [ ] Ejecutar deploy.sh --with-db {ip_servidor} (crea BD desde el seed)
  • [ ] Verificar volumen: docker volume ls | grep postgres

Sincronización con instalación existente

  • [ ] Hacer backup de la BD origen: bash docker exec administrador-db-1 pg_dump -U postgres -Fc db00_kontroleserp \ > backup_$(date +%Y%m%d).dump
  • [ ] Copiar backup al nuevo servidor via SCP
  • [ ] Restaurar: bash docker exec -i administrador-db-1 pg_restore -U postgres -d db00_kontroleserp \ --no-owner --role=postgres < backup.dump
  • [ ] Verificar usuario adempiere tiene permisos en schema adempiere

Post-restauración siempre

  • [ ] Ejecutar emh_setup_completo.sql (idempotente, aplica cualquier schema nuevo)
  • [ ] Verificar emh_dtesjson function existe: sql SELECT proname FROM pg_proc WHERE proname='emh_dtesjson';
  • [ ] Verificar trigger emh_trg_invoice_pending activo en c_invoice

4.5 Configuración DTE por cliente (nuevo cliente)

  • [ ] Ejecutar Paso 1-10 del Sección 2
  • [ ] Verificar que ad_orginfo tiene NIT (en ad_org.value), NRC, codEstableMH, codPuntoVentaMH
  • [ ] Subir certificado .p12 a emh_credentials
  • [ ] Confirmar ambiente='00' para pruebas, '01' para producción
  • [ ] Verificar secuencias emh_seqs para todos los tipos DTE que usará el cliente
  • [ ] Probar envío manual: crear factura de prueba → docstatus='CO' → verificar emh_status='SENT'

4.6 Reportes y Templates

  • [ ] Sincronizar reports/dtes/generic/*.jrxml (11 archivos) al servidor: bash rsync -az despliegue_cliente/reports/ {servidor}:{deploy_dir}/reports/
  • [ ] Si hay reportes customizados por cliente, colocar en: reports/dtes/{ad_client_id}/dte_{tipodoc}.jrxml (tiene prioridad sobre generic/)
  • [ ] Reportes Jasper KontrolesERP (balances, libros de venta, etc.) en /opt/erp/reports/ dentro del contenedor erp_app: bash docker cp reporte.jrxml erp_app:/opt/erp/reports/

4.7 Frontends (PWAs)

  • [ ] Construir PWAs: build_all.sh (genera kontrolesrta/frontend/ y kontrolespos/frontend/)
  • [ ] Verificar kontrolesrta/nginx.conf con la URL correcta de kontrolesapi
  • [ ] Verificar kontrolespos/nginx.conf con la URL correcta de kontrolesapi
  • [ ] La API URL en las PWAs apunta a {empresa}.kontroles.com/api (relativa, vía proxy NPM)

4.8 Nginx Proxy Manager

  • [ ] NPM operativo: curl -s http://{servidor}:81
  • [ ] Exportar/importar configuración NPM si es sincronización:
    • Datos en ./nginx-proxy-manager/data/ (sqlite) — copiar volumen completo
  • [ ] Para instalación nueva: crear proxy hosts manualmente (ver tabla Paso 10 §2.11)
  • [ ] SSL: verificar Let's Encrypt activo para cada subdominio
  • [ ] Probar HTTPS: curl -I https://{empresa}.kontroles.com

4.9 Filebrowser

  • [ ] Verificar filebrowser/settings.json existe con baseURL=/files
  • [ ] Primer acceso: https://{empresa}.kontroles.com/files → admin/admin → cambiar password
  • [ ] Ejecutar INSERT en emh_fileshare_config para crear usuario por empresa (§2.9)
  • [ ] Verificar carpetas ./dtes/{ad_client_id}/ con permisos correctos

4.10 Validación final end-to-end

1. Acceder al ERP: https://{empresa}.kontroles.com
   └─ Login con usuario/password de la empresa

2. Crear una Factura (FAC):
   └─ Sales → Invoices → New
   └─ Completar (docstatus → CO)
   └─ Verificar emh_status PENDING → SENT en BD:
      SELECT documentno, emh_status FROM c_invoice ORDER BY created DESC LIMIT 1;

3. Verificar PDF generado:
   └─ GET https://{empresa}.kontroles.com/admin/invoices/{id}/pdf
   └─ Verificar QR con portal MH: admin.factura.gob.sv/consultaPublica?...

4. Verificar email enviado:
   └─ SELECT * FROM emh_emaillog ORDER BY created DESC LIMIT 1;

5. Verificar archivo en Filebrowser:
   └─ https://{empresa}.kontroles.com/files

4.11 Diferencias empresa vs empresa (multi-tenant)

Cada empresa en KontrolesERP se separa por ad_client_id. Los datos que son exclusivos por empresa:

Objeto Separación
emh_credentials ad_client_id + ad_org_id
emh_seqs ad_client_id + ad_org_id + tipodoc
emh_emailconfig ad_client_id + ad_org_id
emh_fileshare_config ad_client_id
c_doctype.emh_tipodoc ad_client_id
ad_orginfo (datos emisor) ad_org_id
Reportes custom reports/dtes/{ad_client_id}/
Archivos DTE ./dtes/{ad_client_id}/
Subdominio NPM Configurado manualmente por empresa

APÉNDICE — Scripts de verificación rápida

Verificar estado DTE de un cliente

-- Estado general de envíos del día
SELECT dt.emh_tipodoc, inv.emh_status, COUNT(*) AS n
FROM adempiere.c_invoice inv
JOIN adempiere.c_doctype dt ON dt.c_doctype_id = inv.c_doctype_id
WHERE inv.ad_client_id = {CLIENT}
  AND inv.dateinvoiced = CURRENT_DATE
  AND dt.emh_tipodoc IS NOT NULL
GROUP BY dt.emh_tipodoc, inv.emh_status
ORDER BY dt.emh_tipodoc, inv.emh_status;

-- Últimos errores
SELECT inv.documentno, inv.emh_msg, inv.updated
FROM adempiere.c_invoice inv
WHERE inv.ad_client_id = {CLIENT}
  AND inv.emh_status = 'ERROR'
ORDER BY inv.updated DESC LIMIT 10;

-- Secuencias actuales
SELECT tipodoc, serie, currentnext
FROM adempiere.emh_seqs
WHERE ad_client_id = {CLIENT}
ORDER BY tipodoc;

Reintentar documentos en ERROR

-- Solo si el error fue transitorio (red, timeout):
UPDATE adempiere.c_invoice
SET emh_status = 'PENDING', updated = NOW()
WHERE c_invoice_id IN (
    SELECT inv.c_invoice_id
    FROM adempiere.c_invoice inv
    WHERE inv.emh_status = 'ERROR'
      AND inv.ad_client_id = {CLIENT}
      AND inv.emh_msg ILIKE '%timeout%'  -- ajustar según el error
);

Forzar regeneración de PDF

curl -o documento.pdf \
  "https://{empresa}.kontroles.com/admin/invoices/{id}/pdf"

Documento generado para KontrolesERP — SaaS ERP El Salvador
Actualizado: 2026-06-03