openxava / documentación / Lección 25: Comportamiento y lógica de negocio

Curso: 1. Primeros pasos | 2. Modelo básico del dominio (1)  | 3. Modelo básico del dominio (2) | 4. Refinar la interfaz de usuario | 5. Desarrollo ágil | 6. Herencia de superclases mapedas | 7. Herencia de entidades | 8. Herencia de vistas | 9. Propiedades Java | 10. Propiedades calculadas | 11. @DefaultValueCalculator en colecciones | 12. @Calculation y totales de colección | 13. @DefaultValueCalculator desde archivo | 14. Evolución del esquema manual | 15. Cálculo de valor por defecto multiusuario | 16. Sincronizar propiedades persistentes y calculadas | 17. Lógica desde la base de datos | 18. Validando con @EntityValidator | 19. Alternativas de validación  | 20. Validación al borrar  21. Anotación Bean Validation propia | 22. Llamada REST desde una validación  | 23. Atributos en anotaciones  | 24. Refinar el comportamiento predefinido | 25. Comportamiento y lógica de negocio | 26. Referencias y colecciones | A. Arquitectura y filosofía | B. Java Persistence API | C. Anotaciones | D. Pruebas automáticas

Tabla de contenidos

Lección 25: Comportamiento y lógica de negocio
Lógica de negocio desde el modo detalle
Crear una acción para ejecutar lógica personalizada
Escribiendo la lógica de negocio real en la entidad
Escribe menos código usando Apache Commons BeanUtils
Excepciones de aplicación
Validar desde la acción
Evento OnChange para ocultar/mostrar una acción por código
Lógica de negocio desde el modo lista
Acción de lista con lógica propia
Lógica de negocio en el modelo sobre varias entidades
Mostrar un diálogo
Usar showDialog()
Definir las acciones del diálogo
Cerrar el diálogo
Vista plana en lugar de diálogo
Resumen
OpenXava no es simplemente un marco de trabajo para hacer mantenimientos (altas, bajas, modificaciones y consultas), más bien está concebido para desarrollar aplicaciones de gestión plenamente funcionales. Hasta ahora hemos aprendido como crear y refinar la aplicación para manejar los datos. Ahora vamos a posibilitar al usuario la ejecución de lógica de negocio específica.
En esta lección vamos a ver como escribir lógica de negocio en el modelo y llamar a esta lógica desde acciones personalizadas. Así podrás transformar tu aplicación de gestión de datos en una herramienta útil para el trabajo cotidiano de tu usuario.

Lógica de negocio desde el modo detalle

Empezaremos con el caso más simple: un botón en modo detalle para ejecutar cierta lógica. En este caso para crear la factura desde un pedido:
business-logic-behavior_es010.png
Aquí se muestra como esta nueva acción coge el pedido actual y crea una factura a partir de él. Simplemente copia todos los datos del pedido a la nueva factura, incluyendo las líneas de detalle. Se muestra un mensaje y la pestaña FACTURA del pedido visualizará la factura recién creada. Veamos como codificar este comportamiento.

Crear una acción para ejecutar lógica personalizada

Como ya sabes el primer paso para tener una acción personalizada en tu módulo es definir un controlador con esa acción. Por tanto, editemos controladores.xml y añadamos un nuevo controlador. El siguiente código muestra el controlador Pedido:
<controlador nombre="Pedido">
	<hereda-de controlador="Facturacion"/> <!-- Para tener las acciones estándar -->

	<accion nombre="crearFactura" modo="detail"
		clase="com.tuempresa.facturacion.acciones.CrearFacturaDesdePedido"/>
	<!-- modo="detail" : Sólo en modo detalle -->

</controlador>
Dado que hemos seguido la convención de dar al controlador el mismo nombre que a la entidad y el módulo, ya tenemos automáticamente esta nueva acción disponible para Pedido. El controlador Pedido desciende del controlador Facturacion. Recuerda que creamos un controlador Facturacion en la lección anterior. Es un refinamiento del controlador Typical.
Ahora hemos de escribir el código Java para la acción. Puedes verlo aquí:
package com.tuempresa.facturacion.acciones; // En el paquete 'acciones'

import org.openxava.actions.*;
import org.openxava.jpa.*;
import com.tuempresa.facturacion.modelo.*;

public class CrearFacturaDesdePedido
    extends ViewBaseAction { // Para usar getView()

    public void execute() throws Exception {
        Pedido pedido = XPersistence.getManager().find( // Usamos JPA para obtener la
            Pedido.class, // entidad Pedido visualizada en la vista
            getView().getValue("oid"));
        pedido.crearFactura(); // El trabajo de verdad lo delegamos en la entidad
        getView().refresh(); // Para ver la factura creada en la pestaña FACTURA
        addMessage("factura_creada_desde_pedido", // Mensaje de confirmación
            pedido.getFactura());
    }
}
Realmente simple. Buscamos la entidad Pedido, llamamos al método crearFactura(), refrescamos la vista y mostramos un mensaje. Fíjate como la acción es un mero intermediario entre la vista (la interfaz de usuario) y el modelo (la lógica de negocio).
Recuerda añadir el texto del mensaje en el archivo facturacion-messages_es.properties de la carpeta src/main/resources/i18n:
factura_creada_desde_pedido=Factura {0} creada a partir del pedido actual
Sin embargo, el mensaje tal cual está no se muestra de forma agradable, porque enviamos como argumento un objeto Factura. Necesitamos un toString() para Factura y Pedido que sea útil para el usuario. Sobrescribiremos toString() de DocumentoComercial (el padre de Factura y Pedido) para conseguirlo. Puedes ver este método toString():
abstract public class DocumentoComercial extends Eliminable { {

    ...

    public String toString() {
        return anyo + "/" + numero;
    }
}
El año y el número son perfectos para identificar una factura o pedido desde el punto de vista del usuario.
Esto es todo para la acción. Veamos la pieza restante, el método crearFactura() de la entidad Pedido.

Escribiendo la lógica de negocio real en la entidad

La lógica de negocio para crear una nueva Factura está en la entidad Pedido, no en la acción. Esto es la forma natural de hacerlo. El principio esencial de la Orientación a Objetos es que los objetos no son solo datos, sino datos y lógica. El código más bello es aquel cuyos objetos contienen la lógica para manejar sus propios datos. Si tus entidades son meros contenedores de datos (simples envoltorios de las tablas de la base de datos) y tus acciones tienen toda la lógica para manipularlos, en ese caso tu código es una perversión del objetivo original de la Orientación a Objetos.
Aparte de las razones espirituales, poner la lógica para crear una Factura dentro de Pedido es un enfoque pragmático, porque de esta forma podemos usar esta lógica desde otras acciones, proceso masivos, servicios web, etc.
Veamos el código del método crearFactura() de la clase Pedido:
public class Pedido extends DocumentoComercial {

    ...
	
    public void crearFactura() throws Exception { // throws Exception para tener
                                              // un código más simple, de momento
        Factura factura = new Factura(); // Instancia una factura
        BeanUtils.copyProperties(factura, this); // y copia el estado del pedido actual
        factura.setOid(null); // Para que JPA sepa que esta entidad todavía no existe
        factura.setFecha(LocalDate.now()); // La fecha para la nueva factura es hoy
        factura.setDetalles(new ArrayList<>(getDetalles())); // Clona la colección detalles
        XPersistence.getManager().persist(factura);
        this.factura = factura; // Siempre después de persist()
    }
}
La lógica consiste en crear un nuevo objeto Factura, copiar los datos desde el Pedido actual a él y asignar la entidad resultante a la referencia factura del Pedido actual.
Hay tres sutiles detalles aquí. Primero, has de escribir factura.setOid(null), si no la nueva Factura tendría la misma identidad que el Pedido original, además a JPA no le gusta persistir los objetos con el id autogenerado rellenado de antemano. Segundo, has de asignar la nueva Factura al actual Pedido (this.factura = factura) después de llamar a persist(factura), si no obtendrás un error de JPA (algo así como "object references an unsaved transient instance". Tercero, hemos de envolver la colección detalles con un new ArrayList(), para que sea una colección nueva aunque con los mismos elementos, porque JPA no quiere la misma colección asignada a dos entidades.

Escribe menos código usando Apache Commons BeanUtils

Observa como hemos usado BeanUtils.copyProperties() para copiar todas las propiedades del actual Pedido a la nueva Factura. Este método copia todas las propiedades con el mismo nombre de un objeto a otro, incluso si los objetos son de clases diferentes. Esta utilidad pertenece al proyecto de apache Commons BeanUtils. El jar para esta utilidad, commons-beanutils.jar, ya está incluido en tu proyecto.
El siguiente código muestra como usando BeanUtils escribes menos:
BeanUtils.copyProperties(factura, this);
// Es lo mismo que
factura.setOid(getOid());
factura.setAnyo(getAnyo());
factura.setNumero(getNumero());
factura.setFecha(getFecha());
factura.setEliminado(isEliminado());
factura.setCliente(getCliente());
factura.setPorcentajeIVA(getPorcentajeIVA());
factura.setIva(getIva());
factura.setImporteTotal(getImporteTotal());
factura.setObservaciones(getObservaciones());
factura.setDetalles(getDetalles());
Sin embargo, la principal ventaja de usar BeanUtils no es ahorrar tiempo de tecleo, sino que obtienes un código más resistente a los cambios. Porque, si añades, quitas o renombras alguna propiedad de DocumentoComercial (el padre de Factura y Pedido), si estás copiando las propiedades a mano tienes que cambiar el código, mientras que si estás usando BeanUtils.copyProperties() el código funcionará siempre bien, sin tener que cambiarlo.

Excepciones de aplicación

Recuerda la frase: "La excepción que confirma la regla". Las reglas, la vida y el software están llenos de excepciones. Y nuestro método crearFactura() no es una excepción. Hemos escrito código que funciona en los casos más comunes. Pero, ¿qué ocurre si el pedido no está listo para ser facturado o si hay algún problema para acceder a la base de datos? Obviamente, en este caso necesitamos tomar caminos diferentes.
Es decir, el simple throws Exception que hemos escrito para el método crearFactura() no es suficiente para un comportamiento refinado. Deberiamos crear nuestra propia excepción, hagámoslo:
package com.tuempresa.facturacion.modelo; // En el paquete 'modelo'

import org.openxava.util.*;

public class CrearFacturaException extends Exception { // No RuntimeException

    public CrearFacturaException(String mensaje) {
        // El XavaResources es para traducir el mensaje desde el id en i18n
        super(XavaResources.getString(mensaje));
    }
	
}
Ahora podemos usar nuestra CrearFacturaException en lugar de Exception en el método crearFactura() de Pedido:
public void crearFactura()
    throws CrearFacturaException // Una excepción de aplicación (1)
{
    if (this.factura != null) { // Si ya tiene una factura no podemos crearla
        throw new CrearFacturaException( 
            "pedido_ya_tiene_factura"); // Admite un id de 18n como argumento
    }
    if (!isEntregado()) { // Si el pedido no está entregado no podemos crear la factura
        throw new CrearFacturaException("pedido_no_entregado");
    }
    try {
        Factura factura = new Factura(); 
        BeanUtils.copyProperties(factura, this); 
        factura.setOid(null); 
        factura.setFecha(LocalDate.now()); 
        factura.setDetalles(new ArrayList<>(getDetalles())); 
        XPersistence.getManager().persist(factura);
        this.factura = factura; 
    }
    catch (Exception ex) { // Cualquier excepción inesperada (2)
        throw new SystemException( // Se lanza una excepción runtime (3)
            "imposible_crear_factura", ex);
    }
}
Ahora declaramos explícitamente las excepciones de aplicación que este método lanza (1). Una excepción de aplicación es una excepción chequeada que indica un comportamiento especial pero esperado del método. Una excepción de aplicación está relacionada con la lógica de negocio del método. Puedes crear una excepción de aplicación para cada posible caso. Por ejemplo, podrías crear una PedidoYaTieneFacturaException y una PedidoNoEntregadoException. Esto te permitiría tratar cada caso de forma diferente desde el código que usa el método. Aunque, esto no es necesario en nuestro caso, por tanto nosotros simplemente usamos nuestra CrearFacturaException, una excepción de aplicación genérica para este método.
También hemos de enfrentarnos a problemas inesperados (2). Los problemas inesperados incluyen errores del sistema (acceso a base de datos, la red o problemas de hardware) o errores de programación (NullPointerException, IndexOutOfBoundsException, etc). Cuando nos encontramos con cualquier problema inesperado lanzamos una RuntimeException (3). En este caso hemos lanzado una SystemException, una RuntimeException incluida en OpenXava por comodidad, pero puedes lanzar la RuntimeException que quieras.
No necesitas modificar el código de la acción. Si tu acción no atrapa las excepciones, OpenXava lo hace automáticamente. Muestra los mensajes de las excepciones de aplicación al usuario; y para las excepciones runtime, muestra un mensaje de error genérico y aborta la transacción.
Para rematar, añadimos el mensaje para la excepción en los archivos i18n. Edita el archivo facturacion-messages_es.properties de la carpeta src/main/resources/i18n añadiendo las siguientes entradas:
pedido_ya_tiene_factura=El pedido ya tiene una factura
pedido_no_entregado=El pedido todavía no está entregado
imposible_crear_factura=Imposible crear factura
Hay cierto debate en la comunidad de desarrolladores sobre la manera correcta de usar las excepciones en Java. El enfoque usado en esta sección es la forma clásica de trabajar con excepciones en el mundo Java empresarial.

Validar desde la acción

Usualmente el mejor lugar para las validaciones es el modelo, es decir, las entidades. Sin embargo, a veces es necesario poner lógica de validación en las acciones. Por ejemplo, si quieres preguntar por el estado actual de la interfaz gráfica has de hacer la validación en la acción.
En nuestro caso si el usuario pulsa en CREAR FACTURA cuando está creando un nuevo pedido que todavía no ha grabado, fallará. Falla porque es imposible crear una factura desde un pedido inexistente. El usuario ha de grabar el pedido primero.
Modificamos el método execute() de CrearFacturaDesdePedido para validar que la factura visualizada actualmente esté grabada:
public void execute() throws Exception {
    // Añade la siguiente condición
    if (getView().getValue("oid") == null) { 
        // Si el oid es nulo el pedido actual no se ha grabado todavía (1)
        addError("imposible_crear_factura_pedido_no_existe");
        return;
    }
    
    ...
    
}
La validación consiste en verificar que el oid es nulo (1), en cuyo caso el usuario está introduciendo un pedido nuevo, pero todavía no lo ha grabado. En este caso se muestra un mensaje y se aborta la creación de la factura.
Aquí también tenemos un mensaje para añadir al archivo i18n. Edita el archivo facturacion-messages_es.properties de la carpeta src/main/resources/i18n añadiendo la siguiente entrada:
imposible_crear_factura_pedido_no_existe=Imposible crea factura: El pedido no existe todavía
Las validaciones le dicen al usuario que ha hecho algo mal. Esto es necesario, por supuesto, pero es mejor aún crear una aplicación que ayude al usuario a evitar hacer las cosas mal. Veamos una forma de hacerlo en la siguiente sección.

Evento OnChange para ocultar/mostrar una acción por código

Nuestro actual código es suficientemente robusto como para prevenir que equivocaciones del usuario estropeen los datos. Vamos a ir un paso más allá, impidiendo que el usuario se equivoque. Ocultaremos la acción para crear una nueva factura cuando el pedido no esté listo para ello.
OpenXava permite ocultar y mostrar acciones automáticamente. También permite ejecutar una acción cuando cierta propiedad sea cambiada por el usuario en la interfaz de usuario. Con estos dos ingredientes podemos mostrar el botón sólo cuando la acción esté lista para ser usada.
Recuerda que una factura puede ser generada desde un pedido si el pedido ha sido entregado y no tiene factura todavía. Por tanto, tenemos que vigilar los cambios en la referencia factura y la propiedad entregado de la entidad Pedido. Lo primero será crear la acción que oculta o muestra la acción para crear una factura desde un pedido, MostrarOcultarCrearFactura, con este código:
package com.tuempresa.facturacion.acciones; // En el paquete 'acciones'

import org.openxava.actions.*; // Necesario para usar OnChangePropertyAction,

public class MostrarOcultarCrearFactura
    extends OnChangePropertyBaseAction { // Necesario para las acciones @OnChange (1)

    public void execute() throws Exception {
        if (estaPedidoCreado() && estaEntregado() && !tieneFactura()) { // (2)
            addActions("Pedido.crearFactura");
        }
        else {
            removeActions("Pedido.crearFactura");
        }
    }
	
    private boolean estaPedidoCreado() {
        return getView().getValue("oid") != null; // Leemos el valor de la vista
    }
	
    private boolean estaEntregado() {
        Boolean entregado = (Boolean)
            getView().getValue("entregado"); // Leemos el valor de la vista
        return entregado == null?false:entregado;
    }

    private boolean tieneFactura() {
        return getView().getValue("factura.oid") != null; // Leemos el valor de la vista
    } 	
}
Después anotamos factura y entregado en Pedido con @OnChange para que cuando el usuario cambie el valor de entregado o factura en la pantalla, la acción MostrarOcultarCrearFactura se ejecute:
public class Pedido extends DocumentoComercial {

    ...
    @OnChange(MostrarOcultarCrearFactura.class) // Añade esto
    Factura factura;

    ...
    @OnChange(MostrarOcultarCrearFactura.class) // Añade esto
    boolean entregado;

    ...
}
MostrarOcultarCrearFactura es una acción convencional con un método execute(), aunque extiende de OnChangePropertyBaseAction (1). Todas las acciones anotadas con @OnChange tienen que implementar IOnChangePropertyAction, aunque es más fácil extender de OnChangePropertyBaseAction la cual lo implementa. Desde esta acción puedes usar getNewValue() y getChangedProperty(), aunque en este caso concreto no los necesitamos.
El método execute() pregunta si el pedido visualizado está grabado, entregado y todavía no tiene una factura (2), en cuyo caso muestra la acción con addActions("Pedido.crearFactura"), en caso contrario oculta la acción con removeActions("Pedido.crearFactura"). Así, ocultamos o mostramos la acción Pedido.crearFactura, mostrándola solo cuando proceda. Los métodos add/removeActions() permiten especificar varias acciones a mostrar u ocultar separadas por comas.
Ahora cuando marcas o desmarcas la casilla entregado o escoges una factura, el botón para la acción se muestra u oculta. También, cuando el usuario pulsa en Nuevo para crear un nuevo pedido el botón para crear la factura se oculta. Sin embargo, al editar un pedido ya existente, el botón estará siempre presente, aunque el pedido no cumpla los requisitos. Esto es porque cuando un objeto se busca y visualiza las acciones @OnChange no se ejecutan por defecto. Podemos cambiar esto con una pequeña modificación en BuscarExcluyendoEliminados:
public class BuscarExcluyendoEliminados
    // extends SearchByViewKeyAction {
    extends SearchExecutingOnChangeAction { // Usa ésta como clase base
La acción de búsqueda por defecto, es decir, SearchByViewKeyAction no ejecuta las acciones @OnChange por defecto, por tanto cambiamos nuestra acción de buscar para que extienda de SearchExecutingOnChangeAction. SearchExecutingOnChangeAction se comporta exactamente igual que SearchByViewKeyAction pero ejecutando los eventos OnChange. De esta forma cuando el usuario escoge un pedido la acción MostrarOcultarCrearFactura se ejecuta.
Nos queda un pequeño detalle para que todo esto sea perfecto: cuando el usuario pulsa en CREAR FACTURA después de que la factura se haya creado el botón se tiene que ocultar. El usuario no puede crear la factura otra vez. Podemos implementar esta funcionalidad con un ligero refinamiento de CrearFacturaDesdePedido, así:
public void execute() throws Exception {

    ...

    // Todo ha ido bien, por tanto ocultamos la acción
    removeActions("Pedido.crearFactura"); 
}
Como puedes ver simplemente añadimos removeActions("Pedido.crearFactura") al final del método execute().
Mostrar y ocultar acciones no es un sustituto para la validación en el modelo. Las validaciones siguen siendo necesarias porque las entidades pueden ser usadas desde cualquier otra parte de la aplicación, no solo de los módulos de mantenimiento. Sin embargo, el truco de ocultar y mostrar acciones mejora la experiencia del usuario.

Lógica de negocio desde el modo lista

En la lección anterior aprendiste como crear acciones de lista. Las acciones de lista son una herramienta utilísima para dar al usuario la posibilidad de aplicar lógica a varios objetos a la vez. En nuestro caso, podemos añadir una acción en el modo lista para crear una nueva factura automáticamente a partir de varios pedidos seleccionados en la lista, de esta manera:
business-logic-behavior_es020.png
Aquí se muestra como esta acción de lista coge los pedidos seleccionados y crea una factura a partir de ellos. Simplemente copia los datos del pedido en la nueva factura, añadiendo las línea de detalle de todos los pedidos en una única factura. También se muestra un mensaje. Veamos como codificar este comportamiento.

Acción de lista con lógica propia

Como ya sabes, el primer paso para tener una acción propia en tu módulo es añadirla a un controlador. Por tanto, editemos controladores.xml añadiendo una nueva acción al controlador Pedido:
<controlador nombre="Pedido">

    ...
    
    <!-- La nueva acción -->
    <accion nombre="crearFacturaDesdePedidosSeleccionados"
        modo="list"
        clase="com.tuempresa.facturacion.acciones.CrearFacturaDesdePedidosSeleccionados"/>
	<!-- modo="list": Sólo se muestra en modo lista -->

</controlador>
Solo con esto ya tienes una nueva acción disponible para Pedido en modo lista.
Ahora hemos de escribir el código Java para la acción:
package com.tuempresa.facturacion.acciones;

import java.util.*;
import javax.ejb.*;
import org.openxava.actions.*;
import org.openxava.model.*;
import com.tuempresa.facturacion.modelo.*;

public class CrearFacturaDesdePedidosSeleccionados
    extends TabBaseAction { // Tipico de acciones de lista. Permite usar getTab() (1)

    public void execute() throws Exception {
        Collection<Pedido> pedidos = getPedidosSeleccionados(); // (2)
        Factura factura = Factura.crearDesdePedidos(pedidos); // (3)
        addMessage("factura_creada_desde_pedidos", factura, pedidos); // (4)
    }

    private Collection<Pedido> getPedidosSeleccionados() // (5)
        throws FinderException
    {
        Collection<Pedido> pedidos = new ArrayList<>();
        for (Map key: getTab().getSelectedKeys()) { // (6)
            Pedido pedido = (Pedido) MapFacade.findEntity("Pedido", key); // (7)
            pedidos.add(pedido);
        }
        return pedidos;
    }
}
Realmente sencillo. Obtenemos la lista de los pedidos marcados en la lista (2), llamamos al método estático crearDesdePedidos() (3) de Factura y mostramos un mensaje (4). En este caso también ponemos la lógica real en la clase del modelo, no en la acción. Dado que la lógica aplica a varios pedidos y crea una nueva factura, el lugar natural para ponerlo es en un método estático de la clase Factura.
El método getPedidosSeleccionados() (5) devuelve una colección con las entidades Pedido marcadas por el usuario en la lista. Para hacerlo, el método usa getTab() (6), disponible en TabBaseAction (1), que devuelve un objeto org.openxava.tab.Tab. El objeto Tab te permite manejar los datos tabulares de la lista. En este caso usamos getSelectedKeys() (6) que devuelve una colección con las claves de las filas seleccionadas. Dado que esas claves están en formato Map usamos MapFacade.findEntity() (7) para convertirlas en entidades Pedido.
Acuérdate de añadir el texto del mensaje al fichero facturacion-messages_es.properties en la carpeta src/main/resources/i18n:
factura_creada_desde_pedidos=Factura {0} creada a partir de los pedidos: {1}
Eso es todo para la acción. Veamos la pieza que falta, el método crearDesdePedidos() de la entidad Factura.

Lógica de negocio en el modelo sobre varias entidades

La lógica de negocio para crear una nueva Factura a partir de varias entidades Pedido está en la capa del modelo, es decir, en las entidades, no en la acción. No podemos poner el método en la clase Pedido, porque el proceso se hace a partir de varios pedidos, no de uno. No podemos usar un método de instancia en Factura porque todavía no existe el objeto Factura, de hecho lo que queremos es crearlo. Por lo tanto, vamos a crear un método de factoría estático en la clase Factura para crear una nueva Factura a partir de varios pedidos. Puedes ver este método aquí:
public class Factura extends DocumentoComercial {

    ...
	
    public static Factura crearDesdePedidos(Collection<Pedido> pedidos)
        throws CrearFacturaException
    {
        Factura factura = null;
        for (Pedido pedido: pedidos) {
            if (factura == null) { // El primero pedido
                pedido.crearFactura(); // Reutilizamos la lógica para crear una
                                       // factura desde un pedido
                factura = pedido.getFactura(); // y usamos la factura creada
            }
            else { // Para el resto de los pedidos la factura ya está creada
                pedido.setFactura(factura); // Asigna la factura
                pedido.copiarDetallesAFactura(); // Un método de Pedido para copiar las lineas
            } 
        } 
        if (factura == null) { // Si no hay pedidos
            throw new CrearFacturaException("pedidos_no_especificados");
        }
        return factura;
    }
}
Usamos el primer Pedido para crear una nueva Factura usando el método ya existente crearFactura() de Pedido. Entonces llamamos a copiarDetallesAFactura() de Pedido para copiar las líneas de los pedidos restantes a la nueva Factura acumulando en ella el iva e importeTotal de los pedidos. Además, asignamos la nueva Factura como la factura de los pedidos de la colección.
Si factura es nulo al final del proceso, es porque la colección pedidos está vacía. En este caso lanzamos una CrearFacturaException, ya que la acción no atrapa las excepciones, OpenXava muestra el mensaje de la excepción al usuario. Esto está bien. Si el usuario no marca los pedido y pulsa en el botón para crear la factura, le aparecerá ese mensaje de error.
Todavía nos queda añadir el método copiarDetallesAFactura() a Pedido:
public class Pedido extends DocumentoComercial {

    ...
	
    public void copiarDetallesAFactura() { 
        factura.getDetalles().addAll(getDetalles()); // Copia las líneas
        factura.setIva(factura.getIva().add(getIva())); // Acumula el IVA
        factura.setImporteTotal( // y el importe total
		    factura.getImporteTotal().add(getImporteTotal()));
    }
}
Como puedes ver, copia los detalles del pedido actual a la factura y acumula el iva y el importeTotal.
Acuérdate de añadir los textos para los mensajes en el archivo facturacion-messages_es.properties de la carpeta src/main/resources/i18n:
pedidos_no_especificados=Pedidos no especificados
Este no es el único error con el que el usuario puede encontrarse. Todas las validaciones que hemos escrito para Factura y Pedido hasta ahora se aplican automáticamente, por lo tanto el usuario ha de escoger pedidos ya entregados y sin factura. La validación del modelo impide que el usuario cree una factura desde pedidos no apropiados.

Mostrar un diálogo

Después de crear una factura a partir de varios pedidos, sería práctico para el usuario ver y posiblemente editar la nueva factura. Una forma de conseguir esto es sacando un diálogo que permite ver y editar la recién creada factura. De esta forma:
business-logic-behavior_es030.png
Veamos como implementar este comportamiento.

Usar showDialog()

El primer paso es modificar CrearFacturaDesdePedidosSeleccionados para mostrar un diálogo después de crear la factura, con sólo añadir unas poca línea al final de execute() es suficiente:
public void execute() throws Exception {
    Collection<Pedido> pedidos = getPedidosSeleccionados(); 
    Factura factura = Factura.crearDesdePedidos(pedidos); 
    addMessage("factura_creada_desde_pedidos", factura, pedidos);

    // Añade las siguientes líneas para mostrar el diálogo
    showDialog(); // (1)
    // A partir de ahora getView() es el diálogo
    getView().setModel(factura); // Visualiza la factura en el diálogo (2)
    getView().setKeyEditable(false); // Para indicar que el objeto ya existe (3)
    setControllers("EdicionFactura"); // Las acciones del diálogo (4)
}
Llamamos a showDialog() (1), lo que saca un diálogo y a partir de ese momento cuando usamos getView() referencia a la vista del diálogo no a la vista principal del módulo. Después de showDialog() el diálogo está en blanco, hasta que asignamos nuestra factura a la vista con getView().setModel(factura) (2), ahora la factura se visualiza en el diálogo. La siguiente línea, getView().setKeyEditable(false) (3), es para indicar que la factura ya está grabada y así más adelante la acción de grabar correspondiente sepa como comportarse. Finalmente, usamos setControllers("EdicionFactura") para definir el controlador con las acciones a presentar en el diálogo, es decir los botones de abajo del diálogo. Fíjate como setControllers() es una alternativa a addActions().
Obviamente, esto no funcionará hasta que tengamos el controlador EdicionFactura definido. Haremos esto en la siguiente sección.

Definir las acciones del diálogo

El diálogo permite al usuario cambiar la factura y grabar los cambios o simplemente cerrar la factura después de examinarla. Estas acciones se definen en el controlador EdicionFactura en controladores.xml:
<controlador nombre="EdicionFactura">

    <accion nombre="grabar"
        clase="com.tuempresa.facturacion.acciones.GrabarFactura"
        atajo-de-teclado="Control S"/>
		
    <accion nombre="cerrar"
        clase="org.openxava.actions.CancelAction"/>
		
</controlador>
Las dos acciones de este controlador representan los dos botones, GRABAR y CERRAR que viste en la imagen anterior.

Cerrar el diálogo

GrabarFactura contiene sólo una extensión menor de la acción estándar SaveAction de OpenXava:
package com.tuempresa.facturacion.acciones;

import org.openxava.actions.*;

public class GrabarFactura
    extends SaveAction { // Acción estándar de OpenXava para 
                         // grabar el contenido de la vista	             
    public void execute() throws Exception {
        super.execute(); // La lógica estándar de grabación (1)
        closeDialog(); // (2)
    }
}
La acción extiende SaveAction sobrescribiendo el método execute() para simplemente llamar a la lógica estándar, con super.execute() (1), y después cerrar el diálogo con closeDialog() (2). De esta forma, cuando el usuario pulsa en GRABAR, los datos de la factura se graban y el diálogo se cierra volviendo a la lista de pedidos, listo para continuar creando facturas a partir de pedidos.
Para el botón CERRAR usamos CancelAction, una acción incluida en OpenXava que simplemente cierra el diálogo.

Vista plana en lugar de diálogo

A veces en lugar de sacar un diálogo encima:
business-logic-behavior_es040.png
Podriamos preferir reemplazar la vista actual por la nueva, así:
business-logic-behavior_es050.png
Esto puede ser útil cuando la cantidad de información a mostrar es muy grande y en un diálogo queda mal. Usar una vista plana en vez de un diálogo es tan fácil como cambiar esta línea de tu CrearFacturaDesdePedidosSeleccionados:
showDialog();
Por esta otra:
showNewView();
No hace falta nada más. Bueno, quizás cambiar el nombre de la acción "cerrar" por "volver" en el controlador EdicionFactura en controllers.xml.

El trabajo está terminado. Puedes probar el módulo Pedido: escoge varios pedidos y pulsa en el botón CREAR FACTURA DESDE PEDIDOS SELECCIONADOS. Entonces verás la factura creada en un diálogo.

Resumen

La sal de tu aplicación son las acciones y los métodos. Gracias a ellos puedes convertir una simple aplicación de gestión de datos en una herramienta útil. En este caso, por ejemplo, hemos provisto al usuario con una forma de crear automáticamente facturas desde pedidos.
Has aprendido como crear métodos de lógica de negocio tanto estáticos como de instancia, y como llamarlos desde acciones de modo detalle y modo lista. Por el camino has visto como ocultar y mostrar acciones, usar excepciones, validar en las acciones, mostrar diálogos y cómo hacer las pruebas automáticas de todo esto.
Todavía nos quedan muchas cosas interesantes por aprender, por ejemplo en la siguiente lección vamos a refinar el comportamiento de las referencias y colecciones.

Descargar código fuente de esta lección

¿Problemas con la lección? Pregunta en el foro ¿Ha ido bien? Ve a la lección 26