Precarga de formularios web con Java Servlets, Thymeleaf y JavaScript: un caso práctico de facturación electrónica

En el desarrollo de aplicaciones web empresariales, uno de los desafíos habituales consiste en preparar formularios que necesitan información procedente de distintas entidades de una base de datos.

Un ejemplo es la emisión de una factura electrónica. Antes de comenzar a ingresar productos, el sistema necesita disponer de clientes, bodegas, productos, condiciones de venta, modalidades de despacho y parámetros tributarios.

Actualmente, muchas aplicaciones resuelven este problema utilizando JavaScript y varias solicitudes a una API REST. Sin embargo, existe otra alternativa: recuperar la información desde el servidor y generar el formulario HTML con los datos previamente cargados.

En este artículo explicaré cómo implementé este mecanismo en AustralPOS, un sistema de punto de venta y facturación electrónica desarrollado con Java Servlets, Thymeleaf, JDBC, JavaScript y Bootstrap.

1. El problema: preparar el formulario

Antes de mostrar la pantalla de emisión, el sistema debe recuperar clientes registrados, productos, bodegas de la empresa, modalidades de despacho y traslado, tipo de documento tributario, condiciones comerciales y tasa de IVA. Algunos valores, además, quedan establecidos por defecto.

Como buena parte de estos datos se necesita al abrir el formulario, opté por realizar la precarga en el servidor. El flujo es sencillo:

Navegador (HTTP GET)
        |
        v
  Servlet Factura
        |
        v
 Capa de servicios
        |
        v
   Base de datos
        |
        v
 Contexto Thymeleaf
        |
        v
   Plantilla HTML
        |
        v
     Navegador

Este modelo se conoce como Server-Side Rendering (SSR): el servidor genera el HTML antes de entregarlo al navegador.

2. El servlet como controlador

El servlet Factura extiende HttpServlet y atiende las solicitudes para abrir el formulario. El motor de plantillas se inicializa en init() para reutilizarlo en posteriores peticiones.

public class Factura extends HttpServlet {
    private TemplateEngine templateEngine;

    @Override
    public void init() {
        ThymeleafConfig config = new ThymeleafConfig();
        templateEngine = config.createTemplateEngine();
    }

    @Override
    public void doGet(HttpServletRequest request,
                      HttpServletResponse response)
            throws ServletException, IOException {
        // Precarga de datos y generación de la vista.
    }
}

El servlet coordina la solicitud: no necesita construir etiquetas HTML ni incluir directamente consultas SQL.

3. Recuperación de información mediante servicios

En AustralPOS, las operaciones comerciales se realizan dentro del contexto de una empresa. El identificador se recupera de la sesión HTTP:

int empresaid = (int) request.getSession()
        .getAttribute("empresaid");

Con ese valor, el controlador solicita clientes, bodegas y productos a sus servicios respectivos:

CliProvService cliprovService = new CliProvService(empresaid);
ArrayList<CliProv> listacliprov = cliprovService.obtenerClientes();

BodegaService bodegaService = new BodegaService(empresaid);
ArrayList<Bodega> listaBodega = bodegaService.listBodega();

ProductoService productoService = new ProductoService(empresaid);
ArrayList<Producto> arrayproducto = productoService.obtenerProductos();

Esta separación organiza las responsabilidades: el servlet coordina, los servicios obtienen información y Thymeleaf presenta la vista. El acceso debe seguir validando la sesión y los permisos del usuario sobre la empresa solicitada.

4. Parámetros comerciales y tributarios

Además de los catálogos, el formulario requiere modalidades de despacho, tipos de traslado y valores predeterminados:

DespachoService despachoService = new DespachoService();
ArrayList<Despacho> listadespacho = despachoService.listDespacho();
int despachoId = despachoService.getDespachoId(1);

TrasladoService trasladoService = new TrasladoService();
ArrayList<Traslado> listtraslado = trasladoService.listTraslado();
int trasladoId = trasladoService.getIdTraslado(1);

DocumentoService documentoService = new DocumentoService();
int tpoDocumentoId = documentoService.getId(33);

El código tributario 33 corresponde a la factura electrónica afecta en Chile. En este ejemplo, el servicio recupera el identificador interno con el que la aplicación representa ese tipo de documento.

También se obtienen las condiciones comerciales:

TpoVentaService tpoVentaService = new TpoVentaService();
int tpoVentaId = tpoVentaService.getTpoVentaId(3);

ReferenciaService referenciaService = new ReferenciaService();
int referenciaId = referenciaService.getReferenciaId(0);

FPagoService fpagoService = new FPagoService();
int fpagoId = fpagoService.getFagoId(1);

ParametroService parametroService = new ParametroService();
double tasaIva = parametroService.getTasaIva();

Con esto, la tasa de IVA y otros parámetros se obtienen desde los servicios y no quedan escritos directamente en múltiples formularios. Los valores definitivos deben validarse nuevamente en el servidor antes de registrar la factura; para cálculos monetarios conviene utilizar BigDecimal.

5. Transferencia de datos al contexto de Thymeleaf

La clase Context permite poner las colecciones y valores a disposición de la plantilla:

Context context = new Context();
context.setVariable("listacliprov", listacliprov);
context.setVariable("listaBodega", listaBodega);
context.setVariable("arrayproducto", arrayproducto);
context.setVariable("listadespacho", listadespacho);
context.setVariable("listtraslado", listtraslado);

context.setVariable("despachoId", despachoId);
context.setVariable("trasladoId", trasladoId);
context.setVariable("tpoDocumentoId", tpoDocumentoId);
context.setVariable("tpoVentaId", tpoVentaId);
context.setVariable("referenciaId", referenciaId);
context.setVariable("fpagoId", fpagoId);
context.setVariable("tasaIva", tasaIva);

El controlador entrega datos a la plantilla sin necesidad de decidir cómo se distribuyen visualmente los controles.

6. Generación del HTML en el servidor

El servlet termina procesando la plantilla de la factura:

response.setContentType("text/html;charset=UTF-8");
String contenido = templateEngine.process(
        "/movimientoview/factura", context);
response.getWriter().write(contenido);

En el HTML, las bodegas se muestran mediante un selector que recorre la colección precargada:

<select id="Bodega" name="Bodega" class="form-select">
    <option th:each="bodega : ${listaBodega}"
            th:value="${bodega.bodegaid}"
            th:text="${bodega.bodeganom}">
    </option>
</select>

Thymeleaf convierte cada registro en una opción HTML. Por ejemplo, si la empresa tiene tres bodegas, el resultado podría ser:

<select id="Bodega" name="Bodega" class="form-select">
    <option value="1">Casa Matriz</option>
    <option value="2">Sucursal Norte</option>
    <option value="3">Sucursal Sur</option>
</select>

Las bodegas del ejemplo son ilustrativas. Lo relevante es que el navegador ya recibe las opciones incorporadas al HTML, sin efectuar otra solicitud para poblar inicialmente ese control.

7. Fragmentos reutilizables para seleccionar clientes y productos

Thymeleaf permite incorporar componentes definidos en otros archivos HTML. En el formulario utilizo fragmentos para las ventanas modales de clientes y productos:

<!-- Modal Productos -->
<div th:replace="productoview/listaproducto :: modalProductos"></div>

<!-- Modal Clientes -->
<div th:replace="cliprovview/listaclientes :: modalClientes"></div>

La instrucción th:replace incorpora el fragmento durante el procesamiento de la plantilla. Así, el formulario principal se mantiene separado de los componentes de búsqueda y selección. Los fragmentos pueden utilizar las variables preparadas en el mismo contexto.

8. Parámetros iniciales disponibles para JavaScript

No todos los valores se muestran directamente al usuario. Algunos se incorporan en campos ocultos para que JavaScript los consulte:

<input type="hidden" id="tasaIva" name="tasaIva"
       th:value="${tasaIva}">
<input type="hidden" id="tpoDocumentoId" name="tpoDocumentoId"
       th:value="${tpoDocumentoId}">
<input type="hidden" id="tpoVentaId" name="tpoVentaId"
       th:value="${tpoVentaId}">

Luego, en JavaScript, puedo recuperar uno de esos valores:

const tasaIva = Number(
    document.getElementById("tasaIva").value
);

Importante: un campo oculto puede ser modificado desde el navegador. No debe utilizarse como fuente confiable para determinar impuestos, precios ni autorizaciones al guardar el documento.

9. Operaciones dinámicas con JavaScript

La precarga resuelve el estado inicial del formulario, pero el usuario todavía necesita agregar productos, indicar cantidades y visualizar los totales. Para ello utilizo JavaScript.

El botón para añadir una línea llama a la función AgregaDetalle():

<button type="button" class="btn btn-success"
        onclick="AgregaDetalle(
            ProductoId.value,
            ProductoNom.value,
            PrecioUnidad.value,
            Cantidad.value,
            AfectoExento.value,
            PrecioBruto.value
        );">
    Agregar
</button>

El detalle se presenta en una tabla que inicialmente no contiene filas:

<table id="TablaDetalle" class="table table-bordered table-hover">
    <thead>
        <tr>
            <th>Código</th>
            <th>Producto</th>
            <th>Cantidad</th>
            <th>Precio</th>
            <th>Total</th>
            <th></th>
        </tr>
    </thead>
    <tbody></tbody>
</table>

El principio es el siguiente: Thymeleaf genera el estado inicial; JavaScript administra las modificaciones posteriores en el navegador. En el proyecto, los comportamientos se distribuyen en archivos como DetallePos.js y CalculoTotal.js.

10. Envío de la factura mediante HTTP POST

Cuando el usuario termina de completar el formulario, la información se envía al controlador encargado del procesamiento de la operación:

<form action="addMovimiento"
      method="POST"
      id="formfactura"
      name="formfactura">
    <!-- Campos de la factura -->
</form>

Esto permite distinguir claramente dos momentos: GET prepara y muestra el formulario; POST recibe y procesa los datos de la factura. El servidor debe verificar los datos y las reglas del negocio antes de registrar el documento.

11. ¿Por qué no cargar todo con AJAX?

Las solicitudes asíncronas son útiles para buscar información bajo demanda o actualizar partes de la interfaz. Sin embargo, no siempre resulta necesario utilizarlas para completar todos los controles iniciales.

En este caso, la precarga permite incorporar directamente las bodegas, las opciones comerciales y los parámetros iniciales al HTML. Así se evitan solicitudes HTTP adicionales para obtener esos datos después de abrir la página.

La estrategia también tiene límites: si el catálogo contiene miles de productos, recuperar todos los registros en cada apertura del formulario puede aumentar el tiempo de respuesta y el tamaño del documento. En ese escenario, conviene considerar una búsqueda dinámica o paginada para productos, manteniendo precargada la información más pequeña y estable.

SSR y AJAX no son enfoques excluyentes: pueden combinarse dentro de una misma aplicación según las necesidades de cada pantalla.

12. Rendimiento y mantenimiento

En una pantalla que consulta varios servicios, el tiempo inicial depende de la eficiencia de las consultas y de la cantidad de registros recuperados. Si la preparación del formulario crece, puede resultar útil encapsular la operación en un servicio especializado:

// Ejemplo de posible refactorización:
FacturaFormService service = new FacturaFormService(empresaid);
FacturaFormDTO datos = service.cargarFormulario();

Este fragmento es una propuesta de evolución, no parte del servlet mostrado. También es recomendable gestionar los errores de los servicios con respuestas HTTP adecuadas y usar caché únicamente cuando la naturaleza de los datos lo permita.

13. ¿Es obligatorio incorporar un framework frontend?

Frameworks como React, Angular o Vue ofrecen herramientas valiosas para interfaces complejas y aplicaciones altamente interactivas. Sin embargo, no son obligatorios para todos los sistemas web.

En aplicaciones empresariales centradas en formularios, consultas y operaciones comerciales, una combinación de Java Servlets, Thymeleaf y JavaScript puede ofrecer una solución clara: el servidor prepara los datos, la plantilla genera HTML y el navegador gestiona la interacción posterior.

Conclusión

La precarga de formularios con Java Servlets y Thymeleaf demuestra que el renderizado del lado del servidor continúa siendo una alternativa válida para desarrollar aplicaciones empresariales.

En AustralPOS, el servlet obtiene datos comerciales y tributarios a través de la capa de servicios, Thymeleaf los incorpora a la vista y JavaScript permite completar el detalle de productos y otras operaciones dinámicas.

No todas las operaciones de una aplicación web necesitan resolverse mediante una API REST, ni toda la interfaz necesita construirse desde JavaScript. Lo importante es elegir qué responsabilidades ejecutar en el servidor y cuáles en el navegador, según los requerimientos del sistema.


Tecnologías utilizadas: Java, Jakarta Servlets, Thymeleaf, JDBC, JavaScript, HTML5 y Bootstrap 5.
Proyecto: AustralPOS — Sistema de punto de venta y facturación electrónica.

Etiquetas sugeridas para Blogger: Java, Servlets, Thymeleaf, JavaScript, SSR, MVC, Facturación Electrónica, AustralPOS.

Comentarios

Entradas populares de este blog

Boleta Electrónica en Chile: Los 3 Ambientes del SII para Desarrolladores

Configurando Servlets y JSP en Jetty

GUIA GENERAL DE GENERACION DE DOCUMENTOS TRIBUTARIOS ELECTRONICOS