Un proyecto paso a paso
Ya estamos en nuestro último capitulo, en este, pondremos en practica la mayoría de los conocimientos adquiridos. El proyecto que vamos a desarrollar, es un sistema básico pero funcional de facturación que se llamará, por supuesto; Factura Libre.
Dentro del desarrollo de software, existe un tema primario e importante que se llama Análisis y diseño de sistemas, que son las herramientas y conocimientos necesarios para desarrollar, lo más óptimamente posible, un programa. Aun y cuando seas un programador novel o autodidacta (como yo), no lo tomes como un tema menor, en tu buscador favorito encontraras amplia documentación al respecto, como este tema sale fuera del ámbito (y alcance) de estas notas, para los fines de nuestro proyecto, usaremos mucho más, al que dicen llamar, el menos común de los sentidos, el sentido común.
Tratemos de responder las tres preguntas siguientes: ¿qué?, ¿para qué? y ¿para quién?. ¿Qué?, un sistema de facturación. ¿Para qué?, para llevar el control completo del proceso de facturación. ¿Para quién?, usuario básico de computadora. El qué y para que pueden convertirse sin problema en el objetivo general de tu sistema, que no debes de perder de vista nunca, a lo largo del desarrollo de tu programa. Aun y cuando tu vayas a ser el usuario del sistema, es importante que tengas presente y consideres que un programa lo usa, siempre, un usuario final, reitero, aun y cuando tú seas la misma persona, imagínate que no es así. Parte de la idea de facilitarle la vida (informática) al usuario (no de complicársela), en la medida de lo posible, el programador tiene que cubrir las necesidades del usuario no al revés, no lo olvides, he visto no pocos sistemas, de muchos tamaños, donde el usuario tiene que cambiar sus procesos de trabajo, por responsabilidad (por decirlo de forma suave) de los programadores. Tu estas al servicio del usuario, no el al tuyo, reitero, no lo olvides.
Ya tenemos el objetivo de nuestro sistema, ahora, tratemos de ver el ¿como?. Para responder esta pregunta, es relevante saber con que recursos contamos. Primero los más importantes, los recursos humanos. En el desarrollo de un sistema, dependiendo de su tamaño y complejidad, pueden participar muchas personas; analistas de sistemas, diseñadores de interfaces, normalizadores de bases de datos, programadores, documentadores, implantadores, entre otros muchos especialistas. Para nuestro sistema, trataremos de asumir algunos de estos roles, por ahora, estamos como analistas del sistema y hemos determinado que en recursos humanos contamos con dos personas, tu y yo. Un recurso humano de suma importancia para el éxito de un sistema, es el usuario final, su conocimiento de la necesidad a cubrir por el sistema, puede ser crucial para su buen termino, puede llegar a ser tu recurso más valioso.
Ahora, pensemos en los recursos materiales a nuestra disposición, principalmente, el hardware donde se implementará el sistema, no pienses en el hardware donde se desarrollará, si no donde se utilizará, puede que tu máquina sea moderna y rápida, la del usuario tal vez no. En los recursos materiales, considera, también, el sistema o sistemas operativos donde se usará el sistema así como las versiones de estos, para nuestro caso, puede ser importante que consideres la versión de OpenOffice.org con que cuenta el usuario final.
El siguiente punto a analizar, serían los objetivos particulares del sistema, es decir, los alcances de este, que hará y que no, por ejemplo:
- Control de clientes
- Control de facturas
- Control de pagos
Tienes que establecer la prioridad de estos alcances, en primer instancia, aquellos sin los cuales, el sistema deja de cumplir su objetivo general, por ejemplo, clientes y facturas, el control de usuarios, de pagos e inventario, pueden ser primarios o secundarios, según, las necesidades de tu usuario, el análisis del sistema y por supuesto, el presupuesto del cliente. Toma en cuenta que los recursos humanos y materiales pueden determinar algunos alcances del sistema, pero a su vez, los alcances pueden influir en las necesidades mínimas de estos recursos, es decir, tienes que tener una visión global de todos estos aspectos, pues unos infieren en los otros.
Nuestra siguiente tarea, es hacer un pseudo-código en lenguaje natural, por ejemplo:
- Abrir programa.
- Solicitar contraseña.
- Verificar contraseña.
- Si es correcta dar acceso.
- Si no es correcta solicitarla otra vez.
- Crear una nueva factura.
- Seleccionar cliente.
- Si no existe, dar de alta al cliente.
- Seleccionar fecha.
- Seleccionar productos a facturar.
- Establecer cantidades a facturar.
- Guardar factura.
- Imprimir factura.
- Reiniciar el proceso.
- Salir del programa.
Estas instrucciones pueden ser tan genéricas o detalladas como lo consideres, su utilidad es que te van dando la pauta para el flujo de los datos, su relación entre ellos y la jerarquía de los mismos. En siguiente paso es diseñar un diagrama de flujo que refleje lo más certero el pseudo-código del paso anterior. Un diagrama de flujo es la representación gráfica de un proceso o un conjunto de procesos, para la solución de un problema, puede ser tan sencillo como el que te muestro de ejemplo, o tan complejo para representar el algoritmo completo de un sistema. Generalmente, hay un conjunto mínimo de símbolos estándar usados para representar cualquier diagrama de flujo, en tu buscador favorito encontraras suficiente documentación al respecto.
El siguiente paso es determinar que datos son los que guardará el programa, como primera aproximación tenemos la siguiente lista genérica.
- Los datos de los clientes
- Los datos de las facturas
- El detalle de las facturas
Ahora, hagámosla más detallada de modo que sean casi definitivos los datos a guardar. En este punto es recomendable una conversación clara y extensa con el usuario final, de hecho, es recomendable una excelente comunicación con el usuario final a lo largo de todo el desarrollo de tu sistema, veras que será mucho mejor para los dos.
¿Que datos de los clientes y de las facturas guardaremos?, esto dependerá del alcance de tu sistema, previo acuerdo con el usuario y de las necesidades contables, que sabemos, varían de región en región. Nos avocaremos a las solicitadas en México, aunque espero sean lo suficientemente ilustrativas para sea posible adaptarlo a cualquier otra latitud o a tus necesidades particulares. Para nuestro caso, los datos mínimos que requerimos guardar para poder elaborar una factura valida son:
- El nombre del cliente, también llamado Razón Social.
- Su dirección fiscal completa, es decir, la declarada frente a la autoridad.
- Su RFC, que en México es una secuencia especial alfanumérica que identifica de forma univoca a cada contribuyente.
- La fecha y lugar de elaboración de la factura.
- Detalle de los conceptos facturados, cantidad, descripción, importe, subtotal.
- Los impuestos respectivos de acuerdo a su estatus fiscal y área de servicios.
Ahora, desglosaremos cada uno de estos puntos en unidades de información más pequeñas, lo más pequeñas posibles:
- Nombre (Razón Social del cliente)
- Calle
- Número
- Colonia
- Delegación o Municipio
- CP (Código Postal)
- Ciudad o Estado
- RFC
- Número de factura
- Cliente al que se le factura
- Fecha de la factura
- Cantidad
- Descripción
- PU (Precio Unitario)
- Importe
- Subtotal
- Impuestos
- Total
Observa que algunos de estos datos (Importe, subtotal, impuestos, total) son susceptibles de calcularse, máxime si estamos en una hoja de calculo, en un sistema pequeñito como el que estamos enfrentando, no veras mucha diferencia de desempeño entre guardar todos los datos ya calculados o calcularlos cada vez que son mostrados al usuario, en sistemas más grandes puede ser crucial esta diferencia. Por ahora podemos darnos el lujo de usar uno u otro método, pero no olvides esta importante diferencia.
Ahora, tratemos de agrupar los datos en grupos lógicos, es decir, los que tengan relación directa, unos con otros, primero los datos de los clientes:
Clientes |
---|
Nombre |
Calle |
Número |
Colonia |
Delegación o Municipio |
Código Postal |
Ciudad o Estado |
RFC |
Ahora, los datos de las facturas:
Facturas |
---|
Número |
Cliente |
Fecha |
Subtotal |
Impuesto |
Total |
Estado |
Observa que hemos agregado un dato, Estado, este nos servirá como mínimo, para saber si una factura ya fue pagada o no, aunque puede tener muchos otros estatus, tantos como tu criterio te dicte.
Ahora, el detalle de la factura:
Detalle Factura |
---|
Número de Factura |
Cantidad |
Descripción |
Precio |
Importe |
A cada grupo puedes agregar los datos que tu necesites, por ejemplo; a los clientes puedes agregarle datos como teléfonos o personas de contacto, a las facturas algunos clientes te piden que agregues números de pedido, remisión o algún otro dato, en el detalle tal vez necesites un dato para la unidad. Quita o agrega según te dicte tu experiencia y necesidad.
A cada uno de estos grupos le llamaremos tabla, al conjunto de todas nuestras tablas le llamaremos base de datos. A cada dato de cada tabla le llamaremos campo y será el encabezado de cada columna, cada fila de datos que guardemos con todos sus campos, le llamaremos registro.
Abre Base para que te muestre el Asistente para base de datos donde seleccionaremos Crear nueva base de datos para dar clic en el botón de comando Siguiente.
El siguiente paso le indicaremos que si queremos registrar la base de datos para después dar clic en el botón de comando Finalizar.
En el ultimo paso, selecciona el nombres de tu nueva base de datos (1), asegúrate de ir guardando todo en un solo directorio (2), para finalizar da clic en Guardar (3).
Crearemos una nueva tabla en vista diseño.
Agrega los siguiente campos con las siguientes características para la tabla Clientes.
Campo | Tipo | Tamaño |
---|---|---|
Id | Entero (INTEGER) | |
Nombre | Texto (VARCHAR_IGNORECASE) | 100 |
Calle | Texto (VARCHAR_IGNORECASE) | 100 |
Numero | Texto (VARCHAR) | 50 |
Colonia | Texto (VARCHAR_IGNORECASE) | 50 |
Delegacion | Texto (VARCHAR_IGNORECASE) | 50 |
Cp | Texto (VARCHAR) | 5 |
Ciudad | Texto (VARCHAR_IGNORECASE) | 50 |
Rfc | Texto (VARCHAR_IGNORECASE) | 13 |
Nota que a esta tabla le hemos agregado un campo llamado Id, puedes nombrarlo también Clave o algún otro que consideres. La utilidad de este campo es identificar de forma unívoca a cada registro de nuestra tabla, en bases de datos a este campo suele llamarse clave primaria, que en las tablas de Base distingues por el icono de la llave a la izquierda del nombre del campo, esta propiedad se establece de forma automática en cada campo que establezcas como entero (Integer) y establezcas que sea Valor automático. Procura agregar al menos un campo que sea clave primaria en todas tus tablas.
Como siguiente paso guardar la nueva tabla con el nombre Clientes.
En el caso de la tabla Facturas, el número de factura es único, por lo que este puede fungir como clave primaria, hay casos (en México) en los que las facturas llevan una letra adicional cuando el emisor cuenta con sucursales, en este caso tienes que ingeniártelas para solventar esto. Los campos para esta tabla son:
Campo | Tipo | Tamaño |
---|---|---|
Numero | Entero grande (BIGINT) | |
Cliente | Entero (INTEGER) | |
Fecha | Fecha (DATE) | |
Subtotal | Doble (DOUBLE) | |
Impuesto | Doble (DOUBLE) | |
Total | Doble (DOUBLE) | |
Estado | Texto (VARCHAR) | 20 |
Es importante que no establezcas el campo Numero como valor automático porqué, salvo en contadas ocasiones, este número no empezará en cero, también observa que el campo Cliente, lo hemos establecido como entero (integer), esto es importante pues con este campo relacionaremos esta tabla con la tabla Clientes. Si tu régimen fiscal te obliga a aplicar más u otros impuestos, establécelos en esta tabla. Así mismo, si tu cliente te pide agregar algún dato como Pedido, también hazlo en esta tabla.
Guarda la tabla con el nombre Facturas.
Te queda de tarea agregar los campos para la tercer tabla llamada Detalles, cuidado con esta tabla, puede tener muchas variantes, usa la más sencilla.
Ahora crea y guarda un nuevo archivo de Calc que nos servirá como interfaz para el usuario, en el, agregaremos los controles necesarios para manipular los datos. La separación de los datos y la interfaz, es una buena practica de programación. En este punto tenemos muchas posibilidades, usaremos una combinación de formularios y cuadros de diálogo para resolverlo. Si bien los formularios tienen incorporadas propiedades simples y útiles para enlazarse y manipular bases de datos, para fines didácticos, procuráremos hacer uso de cuadros de diálogo que nos obligarán a hacer uso de más código pero en compensación repasaremos muchos de los conceptos tratados en este libro. La mayor parte del código esta suficientemente comentado, nos detendremos solo en aquellos que requieran algún comentario en especial. Cada cuadro de diálogo tendrá asociado un modulo de código, además incluye uno que se llame Utilidades para agregar las macros y funciones genéricas que nos usaremos en otros módulos.
Lo primero que tenemos que garantizar, es que los datos existan, para ello, al abrir nuestro archivo verificamos que estén registrados con la siguiente macro que tienes que asociar al evento Abrir documento de este archivo.
Option Explicit Sub Inicio() Dim oDatos As Object 'Verificamos si los datos existen oDatos = ExistenDatos() If IsNull( oDatos ) Then MsgBox "No se encontró la base de datos, el programa se cerrara", 16,"Error grave" 'Cerramos este archivo ThisComponent.Close( True ) End If End Sub
La función ExistenDatos tiene el siguiente código.
'Función para verificar que existan los datos, si existen devolvemos la referencia Function ExistenDatos() As Object Dim oDBC As Object Dim oBD As Object 'El servicio de bases de datos oDBC = createUnoService("com.sun.star.sdb.DatabaseContext") 'Nos aseguramos de que esten registrados If oDBC.hasByName( DATOS ) Then 'Accedemos a ellos oBD = oDBC.getByName( DATOS ) 'Los regresamos ExistenDatos = oBD End If End Function
Observa que en esta función estamos usando una constante; DATOS, te sugiero agregar un nuevo modulo para ir declarando, tanto las variables como las constantes globales que vayamos necesitando. El establecer una constante para el nombre registrado de la base de datos, nos permite cambiar en un solo lugar este nombre y usarlo a todo lo largo de nuestro programa.
Option Explicit 'Cambia aquí el nombre de tu conexión Global Const DATOS As String = "datos"
El siguiente paso es crear el cuadro de diálogo para manipular los datos de la tabla Clientes, agrega un nuevo cuadro de diálogo, agrega un control cuadro de lista (ListBox), controles cuadro de texto (TextBox), uno por cada campo de nuestra tabla, botones de comando (CommandButton) y etiquetas (Label) de modo que te quede lo más parecido a la siguiente imagen.
Todos los cuadros de texto (TextBox) tiene su propiedad Solo lectura = Si, los botones de comando (CommandButton), están desactivados, excepto Nuevo y Salir, el botón de comando Salir tiene la propiedad Tipo de botón = Cancelar.
Renombra la primer hoja del archivo a Inicio, agrega un botón de comando y asóciale la siguiente macro a su evento Ejecutar una acción.
Option Explicit Sub Clientes() Dim oDlg As Object 'Cargamos el cuadro de diálogo oDlg = CargarDialogo( "Standard","dlgClientes" ) 'Nos aseguramos de que sea válido If Not IsNull(oDlg) Then 'Lo mostramos oDlg.execute() 'Lo liberamos oDlg.dispose() Else MsgBox "No se pudo mostrar el cuadro de diálogo de Clientes" End If End Sub
El código de la función CargarDialogo es el siguiente.
'Función para cargar un cuadro de dialogo en memoria y regresar el cuadro de dialogo Function CargarDialogo(Libreria As String, Nombre As String) As Object Dim oLibreria as Object 'Verificamos que la libreria exista If DialogLibraries.hasByName( Libreria ) Then 'Cargamos la libreria en memoria DialogLibraries.LoadLibrary( Libreria ) 'Referencia a la libreria oLibreria = DialogLibraries.getByName( Libreria ) 'Verificamos que exista el cuadro de diálogo If oLibreria.hasByName( Nombre ) Then 'Creamos el cuadro de diálogo CargarDialogo = CreateUnoDialog( oLibreria.getByName( Nombre ) ) End If End If End Function
En este punto, desde la interfaz de usuario que estamos construyendo, ya deberías poder mostrar y cerrar el cuadro de diálogo.
El botón de comando Salir, debe funcionar aun sin código pues establecimos su propiedad Tipo de botón en Cancelar, con lo cual podemos cerrar el cuadro de diálogo.
Si bien verificamos que existieran los datos al abrir el documento, no es mala idea volver a verificarlo antes de cargar el cuadro de diálogo. Usaremos una variable a nivel de modulo para guardar la conexión a la base de datos, también, cada control que se vaya a manipular, debe de referenciarse en una variable, por ello agregaremos las variables necesarias en el mismo nivel del modulo, de modo que sean visibles para todos los eventos y todas las macros que desarrollemos en este modulo.
Option Explicit Dim oDatos As Object Dim lstClientes As Object Dim txtClave As Object Dim txtNombre As Object Dim txtCalle As Object Dim txtNumero As Object Dim txtColonia As Object Dim txtDelegacion As Object Dim txtCp As Object Dim txtCiudad As Object Dim txtRfc As Object Dim cmdNuevo As Object Dim cmdGuardar As Object Dim cmdEditar As Object Dim cmdActualizar As Object Dim cmdEliminar As Object Dim cmdCancelar As Object Dim cmdSalir As Object
Todos los controles los cargaremos en una sola macro que llamaremos CargarControles y cuyo código es el siguiente.
Sub CargarControles( Dialogo As Object) With Dialogo lstClientes = .getControl("lstClientes") txtClave = .getControl("txtClave") txtNombre = .getControl("txtNombre") txtCalle = .getControl("txtCalle") txtNumero = .getControl("txtNumero") txtColonia = .getControl("txtColonia") txtDelegacion = .getControl("txtDelegacion") txtCp = .getControl("txtCp") txtCiudad = .getControl("txtCiudad") txtRfc = .getControl("txtRfc") cmdNuevo = .getControl( "cmdNuevo" ) cmdGuardar = .getControl( "cmdGuardar" ) cmdEditar = .getControl( "cmdEditar" ) cmdActualizar = .getControl( "cmdActualizar" ) cmdEliminar = .getControl( "cmdEliminar" ) cmdCancelar = .getControl( "cmdCancelar" ) cmdSalir = .getControl( "cmdSalir" ) End With End Sub
La macro Clientes, ya con la verificación de los datos y la carga de controles, queda de la siguiente manera.
Sub Clientes() Dim oDlg As Object 'Cargamos el cuadro de diálogo oDlg = CargarDialogo( "Standard","dlgClientes" ) 'Nos aseguramos de que sea válido If Not IsNull(oDlg) Then 'Nos aseguramos de que existan los datos oDatos = ExistenDatos() If Not ISNull( oDatos ) Then 'Cargamos los controles en memoria Call CargarControles( oDlg ) 'Lo mostramos oDlg.execute() 'Lo liberamos oDlg.dispose() Else MsgBox "No se encontró la base de datos", 16,"Error grave" End If Else MsgBox "No se pudo mostrar el cuadro de diálogo de Clientes" End If End Sub
De aquí en adelante, cuando modifiquemos una macro, solo te indicare las líneas necesarias donde hacer el cambio, de este modo no repetiremos tanto código.
Lo siguiente que haremos no es indispensable, pero a parte de que se ve bonito, nos muestra la versatilidad de OOo Basic, haremos que el color de fondo de los cuadros de texto (TextBox), cambie al color que quieras cuando recibe el foco y regrese a blanco cuando sale de el, de este modo, le indicamos visualmente al usuario en que campo esta actualmente, las macros que logran esto son las siguientes.
'Cambia el color de fondo al recibir el foco Sub Control_RecibeFoco(Evento As Object) Evento.Source.Model.Border = 0 Call CambiaColorFF( Evento.Source, AMARILLO_PASTEL, 0 ) End Sub 'Cambia el color de fondo al perder el foco Sub Control_PierdeFoco(Evento As Object) Evento.Source.Model.Border = 1 Call CambiaColorFF( Evento.Source, BLANCO, 0 ) End Sub 'Procedimiento que cambia el color de fondo y fuente de un control Sub CambiaColorFF(Control As Object, Fondo As Long, Fuente As Long) Control.Model.BackgroundColor = Fondo Control.Model.TextColor = Fuente End Sub
Observa que en la macro CambiaColorFF podemos cambiar tanto el color de fondo como la fuente, de este modo puedes jugar con la combinación de colores que más te guste, yo siempre dejo la fuente en negro porque tengo poca imaginación para eso de los colores. Observa que al recibir el foco uso la constante AMARILLO_PASTEL y al perder el foco uso BLANCO, estas constantes están declaradas a nivel global y tienen los siguiente valores.
Global Const AMARILLO_PASTEL As Long = 16777164 Global Const BLANCO As Long = 16777215
Ahora, para verlas trabajando, solo te resta asociar las macros respectivas en los eventos correspondientes de los cuadros de texto.
Asocia las mismas macros a todos los cuadros de texto (TextBox), si, las mismas a todos los controles, de hecho, mientras un control, no importa cual, soporte estas propiedades, puedes asociarle estas macros, esto es una característica muy poderosa de OOo Basic.
Mencionamos que los cuadros de texto (TextBox) son de solo lectura, esto es para que controlemos la adición y edición de los datos, también, los botones de comando (CommandButton) están desactivados de forma predeterminada y solo activamos los necesarios en el momento indicado, por ejemplo, si la tabla de clientes esta vacía, solo debe estar activado el botón de Nuevo, pero si ya hay datos, podemos activar además: Editar y Eliminar. De modo que los botones de comando se activarán y desactivaran de acuerdo a la acción que seleccione el usuario, la siguiente tabla nos ayudará a controlar el estado de cada botón.
Nuevo | Guardar | Editar | Actualizar | Eliminar | Cancelar | |
---|---|---|---|---|---|---|
Nuevo | ||||||
Guardar | ||||||
Editar | ? | ? | ||||
Actualizar | ||||||
Eliminar | ? | ? | ||||
Cancelar | ||||||
Salir |
El signo de interrogación nos indica que estos botones pueden o no pueden estar activados, ¿de que depende? Dependerá de la existencia o no de más registros.
Cuando abrimos nuestro cuadro de diálogo, tenemos que traer los clientes existentes, si los hay y mostrarlos en el cuadro de lista (ListBox), para ello, agregamos la siguiente línea a la macro Clientes, justo después de cargar los controles.
'Cargamos los controles en memoria Call CargarControles( oDlg ) 'Traemos los nombres de los clientes si lo hay Call TraerClientes( oDatos )
La macro TraerClientes tiene el siguiente código.
Sub TraerClientes( oDatos ) Dim oConsulta As Object Dim sSQL As String Dim mClientes() Dim lTotalRegistros As Long 'Obtenemos el total de clientes lTotalRegistros = TotalRegistros( oDatos, "Clientes", "Id" ) 'Si no hay registros If lTotalRegistros = 0 Then MsgBox "El catalogo de Clientes esta vacio, usa el botón Nuevo para agregar uno", 64,"Clientes" Else 'Si hay registros construimos la consulta para regresar los clientes ordenados por Nombre sSQL = "SELECT Nombre FROM Clientes ORDER BY Nombre" 'Hacemos la consulta oConsulta = HacerConsulta( oDatos, sSQL ) 'Movemos los datos a una matriz mClientes = CampoAMatriz( oConsulta, lTotalRegistros, 1 ) 'Y los agregamos al cuadro de lista Call MatrizACuadroLista( mClientes(), lstClientes ) 'Como si hay datos, activamos Editar y Eliminar cmdEditar.setEnable( True ) cmdEliminar.setEnable( True ) 'Seleccionamos el primer cliente lstClientes.selectItemPos( 0, True ) End If End Sub
Tenemos varias funciones subrutinas nuevas; TotalRegistros, HacerConsulta, CampoAMatriz y MatrizACuadroLista, cuyo código es el siguiente.
'Función para enviar una consulta SQL a una base de datos Function HacerConsulta( BaseDatos As Object, SQL As String) As Object Dim oConexion As Object Dim oDeclaracion As Object Dim oResultado As Object 'Creamos una conexion a la base de datos oConexion = BaseDatos.getConnection( "","" ) 'Creamos un objeto para las instrucciones SQL oDeclaracion = oConexion.createStatement() 'Ejecutamos la consulta oResultado = oDeclaracion.executeQuery( SQL ) 'Si hay resultados If Not IsNull( oResultado ) Then oResultado.next() HacerConsulta = oResultado End If End Function 'Función que devuelve el total de registros de un determinado campo y tabla Function TotalRegistros( BaseDatos As Object, Tabla As String, Campo As String) As Long Dim sSQL As String Dim oConsulta As Object 'Construimos la consulta sSQL = "SELECT COUNT( " & Campo & " ) FROM " & Tabla 'Hacemos la consulta oConsulta = HacerConsulta( BaseDatos, sSQL ) 'Obtenemos y regresamos el valor TotalRegistros = oConsulta.getLong(1) End Function 'Función para mover los registros que regresa una consulta a una matriz Function CampoAMatriz( Consulta As Object, NumReg As Long, Tipo As Integer ) Dim mDatos() Dim co1 As Long 'Pasandole el número de registros hacemos un ciclo 'determinado en vez de uno indeterminado NumReg = NumReg - 1 'Redimencionamos la matriz Redim mDatos( NumReg ) For co1 = 0 To NumReg 'El tipo de campo es importante para usar el método correcto Select Case Tipo Case 1 : mDatos( co1 ) = Consulta.getString(1) Case 2 : mDatos( co1 ) = Consulta.getInt(1) 'Aquí iremos agregando los tipos necesarios End Select 'Nos movemos al siguiente registro Consulta.next() Next co1 CampoAMatriz = mDatos() End Function 'Macro para agregar una matriz a un cuadro de lista 'el método addItem es muy lento, por ello es mejor 'agregar la matriz completa Sub MatrizACuadroLista( mDatos(), CuadroLista As Object ) CuadroLista.addItems( mDatos(), 0 ) End Sub
Empecemos con el código de nuestro botones, por supuesto el primero sera Nuevo, el cual es muy simple pues solo tiene que preparar el cuadro de diálogo para recibir los datos del usuario, vamos a activar y vaciar los cuadros de texto y a establecer el estado de los demás botones de acuerdo a la tabla vista anteriormente.
Sub Nuevo_Clic() 'Activamos los cuadros de texto Call ActivarCuadrosTexto( True ) 'Los vaciamos Call LimpiarCuadrosTexto( True ) 'Botones activados cmdGuardar.setEnable( True ) cmdCancelar.setEnable( True ) 'Botones desactivados cmdNuevo.setEnable( False ) cmdEditar.setEnable( False ) cmdActualizar.setEnable( False ) cmdEliminar.setEnable( False ) cmdSalir.setEnable( False ) 'Desactivamos el cuadro de lista lstClientes.setEnable( False ) 'Enviamos el cursor al primer campo txtNombre.setFocus() End Sub
Observa que también desactivamos el cuadro de lista con los nombres de los clientes, esto es importante porque a este control le agregaremos su evento Clic, el cual nos traerá los datos del cliente seleccionado, lo desactivamos para que el usuario no se le ocurra dar clic en el mientras esta agregando uno nuevo o mientras edita otro, ya ves como son los usuarios. El código de las nuevas subrutinas es el siguiente.
Sub ActivarCuadrosTexto( Activar As Boolean ) 'Recuerda que la clave la establecimos autonúmerico por ello este cuadro nunca lo activamos txtNombre.setEditable( Activar ) txtCalle.setEditable( Activar ) txtNumero.setEditable( Activar ) txtColonia.setEditable( Activar ) txtDelegacion.setEditable( Activar ) txtCp.setEditable( Activar ) txtCiudad.setEditable( Activar ) txtRfc.setEditable( Activar ) End Sub Sub LimpiarCuadrosTexto( Nuevo As Boolean ) If Nuevo Then txtClave.setText( "<Nuevo>" ) Else txtClave.setText( "" ) End If txtNombre.setText( "" ) txtCalle.setText( "" ) txtNumero.setText( "" ) txtColonia.setText( "" ) txtDelegacion.setText( "" ) txtCp.setText( "" ) txtCiudad.setText( "" ) txtRfc.setText( "" ) End Sub
El código del botón Cancelar es trivial.
Sub cmdCancelar_Clic() Dim iClientes As Integer 'Verificamos cuantos clientes hay iClientes = lstClientes.getItemCount() If iClientes = 0 Then 'Si no hay clientes limpiamos todo Call LimpiarCuadrosTexto( False ) Else 'Si hay clientes activamos los controles necesarios cmdEditar.setEnable( True ) cmdEliminar.setEnable( True ) lstClientes.setEnable( True ) 'Mostramos los datos del cliente seleccionado actualmente Call lstClientes_Clic() End If 'Activamos y desactivamos los demás controles necesarios cmdNuevo.setEnable( True ) cmdSalir.setEnable( True ) cmdGuardar.setEnable( False ) cmdActualizar.setEnable( False ) cmdCancelar.setEnable( False ) Call ActivarCuadrosTexto( False ) End Sub
Observa que si hay datos, llamamos al evento Clic del control cuadro de lista que aun no codificamos porque primero haremos el código del botón Guardar, el cual es el siguiente.
Sub cmdGuardar_Clic() Dim mDatos() Dim co1 As Integer Dim sTmp As String Dim sSQL As String 'Obtenemos los datos de todos los cuadros de texto mDatos = Array( txtNombre.getText, txtCalle.getText, txtNumero.getText, txtColonia.getText, txtDelegacion.getText, txtCp.getText, txtCiudad.getText, txtRfc.getText, ) 'Agregamos las comillas simples For co1 = LBound(mDatos) To UBound(mDatos) mDatos(co1) = "'" & mDatos(co1) & "'" Next co1 'Juntamos y separamos por coma sTmp = Join( mDatos(), ",") 'Construimos la instrucción SQL de inserción sSQL = "INSERT INTO ""Clientes"" (""Nombre"",""Calle"",""Numero"",""Colonia"",""Delegacion"",""Cp"",""Ciudad"",""Rfc"") VALUES (" & sTmp & ")" 'Insertamos los datos Call ActualizarDatos( oDatos, sSQL ) 'Activamos y desactivamos los demás controles necesarios cmdEditar.setEnable( True ) cmdEliminar.setEnable( True ) lstClientes.setEnable( True ) cmdNuevo.setEnable( True ) cmdSalir.setEnable( True ) cmdGuardar.setEnable( False ) cmdCancelar.setEnable( False ) 'Desactivamos los cuadros de texto Call ActivarCuadrosTexto( False ) End Sub
¿Que nos falto?, algo en lo que he hecho mucho énfasis a todo lo largo de estas notas, claro, no hemos validado ningún dato, lo cual es un pésimo, si no es que el peor hábito de programación. La instrucción SQL debe estar perfectamente construida, si no es así, el método executeUpdate usado en la macro ActualizarDatos fallará.
'Ejecuta la consulta de actualización pasada Sub ActualizarDatos( BaseDatos As Object, SQL As String) Dim oConexion As Object Dim oDeclaracion As Object Dim oResultado As Object 'Creamos una conexion a la base de datos oConexion = BaseDatos.getConnection( "","" ) 'Creamos un objeto para las instrucciones SQL oDeclaracion = oConexion.createStatement() 'Ejecutamos la inserción de datos oDeclaracion.executeUpdate( SQL ) End Sub
Modifiquemos la macro para validar los datos, hay muchas formas de hacer esto, todo dependerá de los tipos de datos y de las necesidades de tu proyecto. Reitero, la validación es un tema central en muchos programas, valida a conciencia, siempre.
Usaremos una función para validar todos los datos.
'Función para validar los datos que capture el usuario Function ValidarDatos() As Boolean Dim sDato As String Dim co1 As Byte Dim sLetra As String 'Obtenemos el valor del cuadro de texto y le quitamos los espacios sobrantes sDato = Trim(txtNombre.getText()) 'Validamos que si esta vacio, si es un número o una fecha If sDato = "" Or IsNumeric( sDato ) Or IsDate( sDato )Then MsgBox "El campo NOMBRE no puede estar vacío, es un número o una fecha", 16, "Error en campo" 'Vaciamos el contenido del control txtNombre.setText( "" ) 'Enviamos el cursor al control txtNombre.setFocus() 'Salimos de la función, ValidarDatos = False Exit Function Else 'Si el valor es correcto, lo establecemos en el control para su posterior guardado 'aquí puedes por ejemplo; convertir en mayusculas txtNombre.setText( sDato ) End If 'Lo mismo para todos los restantes campos sDato = Trim(txtCalle.getText()) If sDato = "" Or IsNumeric( sDato ) Or IsDate( sDato )Then MsgBox "El campo CALLE no puede estar vacío, es un número o una fecha", 16, "Error en campo" txtCalle.setText( "" ) txtCalle.setFocus() Exit Function Else txtCalle.setText( sDato ) End If 'Tal vez en este campo quieras aceptar números, yo creo que es mejor que no y obligar a capturar 'por ejemplo: Nº 7, Mz 43 Lt 7, Edif 8 Depto 6 sDato = Trim(txtNumero.getText()) If sDato = "" Or IsNumeric( sDato ) Or IsDate( sDato )Then MsgBox "El campo NUMERO no puede estar vacío, es un número o una fecha", 16, "Error en campo" txtNumero.setText( "" ) txtNumero.setFocus() Exit Function Else txtNumero.setText( sDato ) End If sDato = Trim(txtColonia.getText()) If sDato = "" Or IsNumeric( sDato ) Or IsDate( sDato )Then MsgBox "El campo COLONIA no puede estar vacío, es un número o una fecha", 16, "Error en campo" txtColonia.setText( "" ) txtColonia.setFocus() Exit Function Else txtColonia.setText( sDato ) End If sDato = Trim(txtDelegacion.getText()) If sDato = "" Or IsNumeric( sDato ) Or IsDate( sDato )Then MsgBox "El campo DELEGACION no puede estar vacío, es un número o una fecha", 16, "Error en campo" txtDelegacion.setText( "" ) txtDelegacion.setFocus() Exit Function Else txtDelegacion.setText( sDato ) End If 'Este es un caso especial, en México el CP esta en formato 00000, validaremos 'que todos los caracteres capturados sean digitos sDato = Trim(txtCp.getText()) If sDato = "" Or IsDate( sDato )Then MsgBox "El campo C.P. no puede estar vacío o una fecha", 16, "Error en campo" txtCp.setText( "" ) txtCp.setFocus() Exit Function Else 'Iteramos en cada letra For co1 = 1 To Len(sDato) 'Extraemos cada letra sLetra = Mid( sDato, co1, 1 ) 'Verificamos que sea un digito If InStr( 1, "0123456789", sLetra ) = 0 Then MsgBox "El campo C.P. solo acepta dígitos", 16, "Error en campo" txtCp.setText( "" ) txtCp.setFocus() Exit Function End If Next co1 'Si son puros dígitos, le damos el formato correcto sDato = Format( Val(sDato), "00000" ) txtCp.setText( sDato ) End If sDato = Trim(txtCiudad.getText()) If sDato = "" Or IsNumeric( sDato ) Or IsDate( sDato )Then MsgBox "El campo CIUDAD no puede estar vacío, es un número o una fecha", 16, "Error en campo" txtCiudad.setText( "" ) txtCiudad.setFocus() Exit Function Else txtCiudad.setText( sDato ) End If 'Este es otro caso especial, el RFC es una combinación de letras y números que identifica 'de forma única a cada contribuyente, para personas morales es de 12 caracteres y para las 'personas físicas es de 13 con el formato [L]LLLDDDDDDAAA sDato = Trim(txtRfc.getText()) If sDato = "" Or IsNumeric( sDato ) Or IsDate( sDato )Then MsgBox "El campo RFC no puede estar vacío, es un número o una fecha", 16, "Error en campo" txtRfc.setText( "" ) txtRfc.setFocus() Exit Function Else 'Esta es tu tarea, validar que este bien capturado el RFC txtRfc.setText( sDato ) End If 'Si todos los datos están correctos ValidarDatos = True End Function
¿Recuerdas que comentamos que muchas veces la validación nos lleva muchas más líneas de código que otros procesos?, aquí una prueba. Entre las propiedades de los controles, por ejemplo el cuadro de texto para el campo CP, lo hemos limitado a solo permitir cinco caracteres, las propiedades de tu tabla y la validación por código, deberías de tener completo control sobre los datos capturados por el usuario, un refuerzo más podría ser un controlador de errores como hemos aprendido ya, queda a tu criterio el establecerlo o no. La validación para el RFC te queda de tarea, en el archivo que acompaña a estos apuntes puedes ver mi propuesta, pero por supuesto, intenta antes hacer la tuya, y claro, no tienen que coincidir plenamente, recuerda que en informática (y en casi todo) hay más de una manera de resolver los problemas. Hay otro caso, dentro de la validación que tienes que considerar, pero este es tan obvio que también te queda de tarea averiguarlo.
Ahora, modifica la macro cmdGuardar_Clic para integrar la validación anterior.
Sub cmdGuardar_Clic() Dim mDatos() Dim co1 As Integer Dim sTmp As String Dim sSQL As String If ValidarDatos() Then 'Obtenemos los datos de todos los cuadros de texto mDatos = Array( txtNombre.getText, txtCalle.getText, txtNumero.getText, txtColonia.getText, txtDelegacion.getText, txtCp.getText, txtCiudad.getText, txtRfc.getText, ) 'Agregamos las comillas simples For co1 = LBound(mDatos) To UBound(mDatos) mDatos(co1) = "'" & mDatos(co1) & "'" Next co1 'Juntamos y separamos por coma sTmp = Join( mDatos(), ",") 'Construimos la instrucción SQL de inserción sSQL = "INSERT INTO ""Clientes"" (""Nombre"",""Calle"",""Numero"",""Colonia"",""Delegacion"",""Cp"",""Ciudad"",""Rfc"") VALUES (" & sTmp & ")" 'Insertamos los datos Call ActualizarDatos( oDatos, sSQL ) 'Activamos y desactivamos los demás controles necesarios cmdEditar.setEnable( True ) cmdEliminar.setEnable( True ) lstClientes.setEnable( True ) cmdNuevo.setEnable( True ) cmdSalir.setEnable( True ) cmdGuardar.setEnable( False ) cmdCancelar.setEnable( False ) 'Desactivamos los cuadros de texto Call ActivarCuadrosTexto( False ) End If End Sub
Ahora si, veamos el código del evento Clic para el cuadro de lista (ListBox) de los clientes, que nos traerá los datos completos del cliente seleccionado por el usuario.
Sub lstClientes_Clic() Dim sCliente As String Dim sSQL As String Dim oConsulta As Object 'El cliente seleccionado sCliente = lstClientes.getSelectedItem 'Construimos la consulta SQL sSQL = "SELECT * FROM Clientes WHERE Nombre='" & sCliente & "'" 'Hacemos la consulta oConsulta = HacerConsulta( oDatos, sSQL ) 'Mostramos los datos en sus respectivos controles With oConsulta txtClave.setText( .getInt(1) ) txtNombre.setText( .getString(2) ) txtCalle.setText( .getString(3) ) txtNumero.setText( .getString(4) ) txtColonia.setText( .getString(5) ) txtDelegacion.setText( .getString(6) ) txtCp.setText( .getString(7) ) txtCiudad.setText( .getString(8) ) txtRfc.setText( .getString(9) ) End With End Sub
El siguiente botón que vamos a programar es el de Eliminar, que queda así.
Sub cmdEliminar_Clic() Dim sClave As String Dim sNombre As String Dim sInfo As String Dim iRes As Integer Dim sSQL As String Dim iSel As Integer Dim iNum As Integer 'Posición del cliente seleccionado iSel = lstClientes.getSelectedItemPos 'La clave del cliente sClave = txtClave.getText 'El nombre del cliente sNombre = txtNombre.getText 'Construimos el mensaje a mostrar sInfo = "Se eliminará al cliente " & sNombre & " con clave " & sClave & Chr(13) & Chr(13) & "¿Estás seguro de continuar?" & Chr(13) & Chr(13) & "ESTA ACCIÓN NO SE PUEDE DESHACER" 'Mostramos y preguntamos al usuario iRes = MsgBox( sInfo, 36,"Eliminar cliente" ) 'Si el usuario responde Si If iRes = 6 Then 'Construimos la instrucción SQL de borrado sSQL = "DELETE FROM ""Clientes"" WHERE ""Id""=" & sClave 'Actualizamos la base de datos Call ActualizarDatos( oDatos, sSQL ) 'Quitamos al cliente de la lista lstClientes.removeItems( iSel,1) 'Obtenemos el número de clientes iNum = lstClientes.getItemCount Select Case iNum Case 0 'Si ya no hay clientes cmdEditar.setEnable( False ) cmdEliminar.setEnable( False ) lstClientes.setEnable( False ) Call LimpiarCuadrosTexto( False ) txtNombre.setFocus() Case 1 'Si al menos hay un cliente lo seleccionamos lstClientes.selectItemPos( 0, True ) 'Traemos sus datos Call lstClientes_Clic() Case Else 'Solo si se eliminó el ultimo cliente If iNum = iSel Then iSel = iSel - 1 'Seleccionamos el inmediato anterior lstClientes.selectItemPos( iSel, True ) 'Traemos sus datos Call lstClientes_Clic() End Select End If End Sub
Siempre y perdona el absoluto, siempre pregunta al usuario y trata de informarle lo más claro posible, cuando se esta apunto de realizar una acción que no es posible deshacer, dependiendo del contexto, tal vez hasta dos veces hay que preguntar. En el código anterior nos hace falta una validación antes de eliminar al cliente, ¿cual?.
El botón de Editar tiene un comportamiento muy similar al botón Nuevo, con la diferencia de que no vaciamos los cuadros de texto y activamos otros botones de comando.
Sub cmdEditar_Clic() 'Activamos los cuadros de texto Call ActivarCuadrosTexto( True ) 'Botones activados cmdActualizar.setEnable( True ) cmdCancelar.setEnable( True ) 'Botones desactivados cmdNuevo.setEnable( False ) cmdEditar.setEnable( False ) cmdGuardar.setEnable( False ) cmdEliminar.setEnable( False ) cmdSalir.setEnable( False ) 'Desactivamos el cuadro de lista lstClientes.setEnable( False ) 'Enviamos el cursor al primer campo txtNombre.setFocus() End Sub
El código del botón Actualizar, es similar al del botón Guardar, pero claro, hay que hacer algunas validaciones un poco diferentes. Te queda de tarea terminarlo, en el archivo anexo de este libro esta mi propuesta, pero de nuevo, no hagas trampa y trata de terminarlo primero, después solo compara recordando que es solo una propuesta.
El siguiente cuadro de diálogo que haremos será el de la facturación, en este ya estarán involucradas dos tablas diferentes, pero su manipulación es muy similar al que acabamos de terminar aunque veras que necesitaremos muchas más líneas de código para poder controlarlo, diseña tu cuadro de dialogo de acuerdo a la siguiente imagen.
En el archivo anexo podrás ver el detalle de los controles usados, así como las principales propiedades usadas en ellos.
En nuestra hoja de calculo de inicio, agrega un segundo botón de comando que abrirá este cuadro de diálogo.
Todo el código de este cuadro de diálogo estará en un nuevo modulo, así que agrega uno al archivo desde el IDE como ya sabes. Lo primero que tenemos que hacer es declarar las variables a nivel de modulo que serán visibles para todas las macros y funciones que desarrollemos en él.
Option Explicit Dim oDatos As Object Dim cboClientes As Object Dim txtClave As Object Dim txtFecha As Object Dim txtFactura As Object Dim txtCantidad As Object Dim txtDescripcion As Object Dim txtPu As Object Dim txtImporte As Object Dim txtSubtotal As Object Dim txtImpuesto As Object Dim txtTotal As Object Dim lstDetalle As Object Dim lstLeyendas As Object Dim lblImpuesto As Object Dim lblEstado As Object Dim cmdNuevaFactura As Object Dim cmdAgregar As Object Dim cmdGuardar As Object Dim cmdReservar As Object Dim cmdUsarReservada As Object Type Articulo Cantidad As Double Descripcion As String Precio As Double Importe As Double End Type Dim EsReservada As Boolean Dim dSubtotal As Double Dim mDetalle() As New Articulo
La siguiente macro será la macro de inicio y la que se tiene que asociar al botón de comando agregado a la interfaz del usuario. Observa que hacemos uso de varias macros ya vistas en el cuadro de diálogo anterior, como; CargarDialogo, ExistenDatos y TotalRegistros.
Sub Facturacion() Dim oDlg As Object Dim lFactura As Long Dim sFactura As String 'Cargamos el cuadro de diálogo oDlg = CargarDialogo( "Standard","dlgFacturacion" ) 'Nos aseguramos de que sea válido If Not IsNull(oDlg) Then 'Nos aseguramos de que existan los datos oDatos = ExistenDatos() If Not IsNull( oDatos ) Then 'Nos aseguramos de que haya clientes registrados If TotalRegistros( oDatos, "Clientes", "Id" ) > 0 Then 'Cargamos los controles en memoria Call CargarControles( oDlg ) txtDescripcion.setText( "" ) cboClientes.setText( "" ) lblImpuesto.setText( "IVA " & Format(IVA,"0 %" ) 'Traemos los nombres de los clientes si lo hay Call TraerClientes( oDatos ) 'Establecemos la fecha actual txtFecha.Date = Format(Now,"YYYYMMDD") 'Consultamos la ultima factura lFactura = UltimaFactura( oDatos ) 'Si es la primer factura pedimos él número If lFactura = 0 Then Do sFactura = InputBox( "Es la primer factura que haces, ¿escribe el número de factura?" ) Loop Until IsNumeric(sFactura) lFactura = Val( sFactura ) Else 'Si ya hay solo aumentamos en uno lFactura = lFactura + 1 End If 'Si hay al menos una factura reservada activamos el botón de comando If Not ExisteValor( oDatos, "Facturas", "Estado", "Reservada") Then cmdUsarReservada.setEnable( False ) End If 'Establecemos el número de la nueva factura txtFactura.setText( lFactura ) txtClave.setFocus() 'Mostramos el cuadro de diálogo oDlg.execute() 'Lo liberamos oDlg.dispose() Else MsgBox "No hay clientes registrados, al menos registra uno", 16, "Sin clientes" End If Else MsgBox "No se encontró la base de datos", 16, "Error grave" End If Else MsgBox "No se pudo mostrar el cuadro de diálogo de Clientes" End If End Sub
La macro CargarControles es igual pero claro, en esta cargamos los controles usado en este cuadro de diálogo, no te confundas, son dos macros con igual nombre pero contenido diferente, al estar en módulos diferentes, el IDE sabe a cual llamar.
'Para inicializar las variables de todos los controles usados Sub CargarControles( Dialogo As Object) With Dialogo cboClientes = .getControl("cboClientes") txtClave = .getControl("txtClave") txtFecha = .getControl("txtFecha") txtFactura = .getControl("txtFactura") txtCantidad = .getControl("txtCantidad") txtDescripcion = .getControl("txtDescripcion") txtPu = .getControl("txtPu") txtImporte = .getControl("txtImporte") lstDetalle = .getControl("lstDetalle") lstLeyendas = .getControl("lstLeyendas") txtSubtotal = .getControl("txtSubtotal") txtImpuesto = .getControl("txtImpuesto") txtTotal = .getControl("txtTotal") lblImpuesto = .getControl("lblImpuesto") lblEstado = .getControl("lblEstado") cmdNuevaFactura = .getControl("cmdNuevaFactura") cmdAgregar = .getControl("cmdAgregar") cmdGuardar = .getControl("cmdGuardar") cmdReservar = .getControl("cmdReservar") cmdUsarReservada = .getControl("cmdUsarReservada") End With End Sub
Observa que en la siguiente línea.
lblImpuesto.setText( "IVA " & Format(IVA,"0 %" )
Establecemos el texto de una etiqueta (Label), y usamos el valor de una constante llamada IVA, esta constante la declaramos en el mismo modulo de las otras constantes. Nota las nuevas constantes que usaremos, por supuesto, no son limitativas, agrega todas las que consideres que usaras en tú sistema.
Option Explicit 'Cambia aquí el nombre de tu conexión Global Const DATOS As String = "datos" Global Const AMARILLO_PASTEL As Long = 16777164 Global Const BLANCO As Long = 16777215 Global Const IVA As Single = 0.16 Global Const COPIAS As Byte = 3 Global Const MAX_COPIAS As Byte = 5
La macro TraerClientes de este modulo, es un poco diferente.
'Traemos todos los clientes de la base de datos Sub TraerClientes( oDatos ) Dim oConsulta As Object Dim sSQL As String Dim mClientes() Dim lTotalRegistros As Long 'Obtenemos el total de clientes lTotalRegistros = TotalRegistros( oDatos, "Clientes", "Id" ) 'Construimos la consulta para regresar los clientes ordenados por Nombre sSQL = "SELECT Nombre FROM Clientes ORDER BY Nombre" 'Hacemos la consulta oConsulta = HacerConsulta( oDatos, sSQL ) 'Movemos los datos a una matriz mClientes = CampoAMatriz( oConsulta, lTotalRegistros, 1 ) 'Y los agregamos al cuadro de lista Call MatrizACuadroLista( mClientes(), cboClientes ) End Sub
La función UltimaFactura tiene el siguiente código.
'Para obtener el número de la ultima factura guardada Function UltimaFactura( oDatos ) As Long Dim sSQL As String Dim oConsulta As Object sSQL = "SELECT MAX(Numero) FROM Facturas" oConsulta = HacerConsulta( oDatos, sSQL ) UltimaFactura = oConsulta.getLong( 1 ) End Function
Observa que obtenemos el máximo del campo Numero, esto no necesariamente tiene que ser así, tal ves la estructura de tú tabla te permita obtener solo el ultimo valor agregado, esto tú lo determinas. La función ExisteValor tiene el siguiente código.
'Función para saber si un registro ya fue dado de alta Function ExisteValor( BaseDatos As Object, Tabla As String, Campo As String, Valor As String) As Boolean Dim sSQL As String Dim oConsulta As Object 'Construimos la consulta sSQL = "SELECT " & Campo & " FROM " & Tabla & " WHERE " & Campo & " = '" & Valor & "'" 'Hacemos la consulta oConsulta = HacerConsulta( BaseDatos, sSQL ) 'Verificamos si existe If oConsulta.getRow = 1 Then ExisteValor = True End If End Function
El resto del código creo que es autoexplicativo.
El primero control que codificaremos será el cuadro de texto txtClave, usaremos su evento Tecla Pulsada y buscaremos el cliente con dicha clave cuando el usuario presione la tecla ↵ Enter . Esta información de ayuda, puedes establecerla en la propiedad Texto de ayuda de cada control, si tu sistema no viene (que debería) acompañado de un archivo de ayuda, esta propiedad puede ser muy útil para mostrarle al usuario que se espera que capture o que haga y se muestra en cuanto el cursor entra en el área del control.
Que mostrará en tiempo de ejecución.
El código de este evento es.
'Cuando el usuario presiona una tecla en el cuadro texto Clave del cliente Sub txtClave_TeclaPulsada( Evento ) Dim lClave As Long Dim sCliente As String 'Si la tecla presiona es la tecla ENTER buscamos al cliente por clave If Evento.KeyCode = 1280 Then lClave = Val( txtClave.getText ) txtClave.setText( Format(lClave) ) sCliente = ExisteCliente( oDatos, Format(lClave), True ) cboClientes.setText( sCliente ) If sCliente = "" Then MsgBox "El cliente con la clave " & Format(lClave) & " no existe", 16,"Facturacion" Else txtCantidad.setFocus() End If End If End Sub
El código de la tecla ↵ Enter es 1280, tomamos el valor del cuadro de texto, lo convertimos a valor y buscamos esta clave de cliente, el código de la función ExisteCliente es el siguiente. Observa que dependiendo del tercer argumento (Clave) podemos hacer la búsqueda por la clave o por el nombre del cliente, en los dos casos regresamos una cadena con el nombre o con la clave del cliente encontrado o una cadena vacía si no es encontrado.
'Para saber si un cliente existe, podemos buscar por cliente o por clave Function ExisteCliente( oDatos, Dato As String, Clave As Boolean ) As String Dim sSQL As String Dim oConsulta As Object 'Construimos la consulta correcta If Clave Then sSQL = "SELECT Nombre FROM Clientes WHERE Id=" & Dato Else sSQL = "SELECT Id FROM Clientes WHERE Nombre='" & Dato & "'" End If oConsulta = HacerConsulta( oDatos, sSQL ) 'Si hay una fila al menos se encontró el cliente If oConsulta.getRow > 0 Then ExisteCliente = oConsulta.getString( 1 ) End If End Function
A parte de la clave, ponemos a disposición del usuario, la posibilidad de seleccionar al cliente desde el control cuadro combinado (ComboBox), usamos un cuadro combinado, por qué este combinado las virtudes de un cuadro de texto para permitir escribir el nombre directamente o seleccionarlo desde la lista desplegable, por esto, tenemos que controlar tanto el evento Estado modificado, que es llamado cuando el usuario selecciona algún elemento de la lista, como el evento Al perder el foco, podemos usar también el evento Texto modificado, pero al ser un campo donde puede haber muchos caracteres implicados nos decantamos por otros eventos, cuyo código es el siguiente.
'Cuando sale el foco del cuadro combinado de los clientes Sub cboClientes_PierdeFoco( Evento ) Dim sCliente As String Dim sClave As String sCliente = Trim(Evento.Source.getText) If sCliente <> "" Then sClave = ExisteCliente( oDatos, sCliente, False ) If sClave = "" Then MsgBox "El cliente " & sCliente & " no esta registrado", 16,"Cliente inexistente" txtClave.setText( "" ) Else txtClave.setText( sClave ) End If End If End Sub 'Este evento es llamado cuando se usa el raton para cambiar el elemento dentro del cuadro combinado Sub cboClientes_Cambia( Evento ) Dim sCliente As String Dim sClave As String 'Obtenemos el texto y bucamos al cliente por nombre sCliente = Evento.Source.getText sClave = ExisteCliente( oDatos, sCliente, False ) If sClave = "" Then MsgBox "El cliente " & sCliente & " no esta registrado", 16,"Cliente inexistente" txtClave.setText( "" ) Else txtClave.setText( sClave ) txtCantidad.setFocus() End If End Sub
Observando el código de los controles txtClave y cboClientes, puedes deducir fácilmente que uno se complementa con el otro, si el usuario escribe una clave correcta el nombre es mostrado, si el usuario escribe un nombre directamente o selecciona uno existente de la lista desplegable, es mostrada su clave correspondiente.
Para el control de la fecha, hemos usado un control fecha (DateField) en donde, al abrir el cuadro de diálogo, mostramos la fecha actual del sistema y las únicas validaciones que hacemos es informarle al usuario si esta usando una fecha anterior o posterior a la actual, en algunos casos tal ves sea necesario evitar que el usuario seleccione una fecha futura, esto, lo dejo a tu criterio, por ahora, lo dejamos a criterio del usuario y solo se lo informamos en la función de validación que implementaremos más adelante.
El botón de comando Reservar, lo usamos para cambiar el estatus de la factura actual a reservada, no se guarda ningún otro dato.
El código de este botón es.
'Para marcar una factura como reservada para su uso posterior Sub cmdReservar_Clic() Dim iRes As Integer Dim sSQL As String iRes = MsgBox( "Esta acción solo marcara esta factura: " & txtFactura.getText() & " como reservada para uso posterior. No se guardará ningún otro dato" & _ Chr(10) & Chr(10) & "¿Estás seguro de marcarla como reservada?", 36, "Reservar factura" ) If iRes= 6 Then 'Solo nuevas facturas se pueden marcar como reservadas sSQL = "INSERT INTO ""Facturas"" (""Numero"",""Estado"") VALUES (" & txtFactura.getText() & ",'Reservada')" Call ActualizarDatos( oDatos, sSQL ) cmdUsarReservada.setEnable( True ) Call cmdNuevaFactura_Clic() End If End Sub
Como usaremos el botón de comando Nueva Factura, mostramos su apariencia, recuerda que de forma predeterminada esta desactivado, más adelante te muestro en que caso queda activado.
Y su código.
'Para preparar el ingreso de una nueva factura Sub cmdNuevaFactura_Clic() Dim lFactura As Long dSubtotal = 0 EsReservada = False Erase mDetalle Call ActualizarTotal( 0 ) lFactura = UltimaFactura( oDatos ) + 1 txtFactura.setText( Format(lFactura) ) txtClave.setText( "" ) cboClientes.setText( "" ) txtCantidad.setText( "" ) txtDescripcion.setText( "" ) txtPu.setText( "" ) lstDetalle.removeItems( 0, lstDetalle.getItemCount ) lstLeyendas.removeItems( 0, lstLeyendas.getItemCount ) cmdNuevaFactura.setEnable( False ) cmdAgregar.setEnable( True ) cmdGuardar.setEnable( True ) cmdReservar.setEnable( True ) cmdUsarReservada.setEnable( True ) lblEstado.setText( "Nueva Factura" ) txtClave.setFocus() End Sub
El botón de comando Cancelar, nos sirve para establecer el estatus de la factura actual como cancelada, podemos cancelar tanto una factura nueva como una existente.
El código de este botón es.
'Botón de comando Cancelar Sub cmdCancelar_Clic() Dim iRes As Integer Dim sSQL As String iRes = MsgBox( "Esta acción NO SE PUEDE DESHACER la factura: " & txtFactura.getText() & " se marcará como CANCELADA." & Chr(10) & Chr(10) & "¿Estás seguro de CANCELAR esta factura?", 36, "Cancelar factura" ) 'Nos aseguramos de la respuesta del usuario If iRes= 6 Then If ExisteValor( oDatos, "Facturas", "Numero", txtFactura.getText() ) Then 'Si la factura ya existe solo actualizamos su estado sSQL = "UPDATE ""Facturas"" SET ""Estado""='Cancelada' WHERE ""Numero""=" & txtFactura.getText() Else 'Si no existe la insertamos como nueva sSQL = "INSERT INTO ""Facturas"" (""Numero"",""Estado"") VALUES (" & txtFactura.getText() & ",'Cancelada')" End If 'Hacemos la consulta de actualización Call ActualizarDatos( oDatos, sSQL ) 'Preparamos para una nueva factura Call cmdNuevaFactura_Clic() End If End Sub
El botón Usar Reservada, nos servirá para permitirle al usuario seleccionar y usar una factura previamente marcada como reservada.
Y su código.
'Botón para mostrar las facturas reservadas Sub cmdUsarReservada_Clic() Dim oDlg As Object Dim sSQL As String Dim oConsulta As Object Dim mReservadas() 'Verificamos si hay facturas reservadas If ExisteValor( oDatos, "Facturas", "Estado", "Reservada") then 'Cargamos el cuadro de diálogo oDlg = CargarDialogo( "Standard","dlgReservadas" ) 'Traemos las facturas reservadas sSQL = "SELECT Numero FROM Facturas WHERE Estado='Reservada'" oConsulta = HacerConsulta( oDatos, sSQL ) 'Contamos cuantas son sSQL = "SELECT COUNT(Estado) FROM Facturas WHERE Estado='Reservada'" mReservadas = CampoAMatriz( oConsulta, TotalRegistrosConsulta( oDatos, sSQL ), 3 ) Call MatrizACuadroLista( mReservadas(), oDlg.getControl( "lstReservadas" ) ) 'Mostramos el cuadro de diálogo oDlg.execute() oDlg.dispose() Else MsgBox "No hay facturas reservadas", 16, "Sin facturas reservadas" txtClave.setFocus() End If End Sub
La macro anterior usa la siguiente macro nueva.
'Función que devuelve el total de registros de una consulta Function TotalRegistrosConsulta( BaseDatos As Object, SQL As String) As Long Dim oConsulta As Object oConsulta = HacerConsulta( BaseDatos, SQL ) TotalRegistrosConsulta = oConsulta.getLong(1) End Function
Observa que mostramos las facturas en un cuadro de lista de un nuevo cuadro de diálogo que solo tiene dicho cuadro de lista, una etiqueta y un botón de comando para salir.
Con doble clic sobre la factura deseada, la seleccionamos, salimos del cuadro de diálogo y establecemos esta factura como actual en el cuadro de diálogo de facturación. El código de este evento es el siguiente.
'Evento doble clic para seleccionar una factura reservada Sub lstReservadas_Doble_Clic( Evento ) Dim sFactura As String 'Contamos el número de clics y si hay un elemento seleccionado If Evento.ClickCount = 2 And ( Evento.Source.getSelectedItemPos > -1 ) Then sFactura = Evento.Source.getSelectedItem txtFactura.setText( sFactura ) lblEstado.setText( "Reservada" ) cmdReservar.setEnable( False ) cmdNuevaFactura.setEnable( True ) 'Finalizamos el cuadro de diálogo Evento.Source.getContext.endExecute() EsReservada = True End If End Sub
Observa que le mostramos al usuario que es una factura reservada, actualizando el texto de la etiqueta lblEstado, también desactivamos el botón Reservar.
Los campos Cantidad y P.U. son controles numéricos (NumericField), por lo que solo permiten la introducción de números, como el cambio de cualquiera de ellos afecta al importe, es decir a su producto, haremos una sola macro para los dos que asignaremos a su evento Texto modificado y cuyo código es el siguiente.
'Cada vez que cambia la cantidad o el precio 'actualizamos el importe Sub Importe_Modificado( Evento ) Dim dCantidad As Double Dim dPU As Double Dim dImporte As Double dCantidad = txtCantidad.getValue dPU = txtPu.getValue dImporte = dCantidad * dPU 'Nos aseguramos de que el resultado tenga solo dos decimales dImporte = FuncionCalc( "ROUND", Array(dImporte,2) ) txtImporte.setValue( dImporte ) End Sub
La función FuncionCalc, nos permite llamar a cualquier función integrada de Calc, pasarle los argumentos respectivos y obtener su resultado, esta función ya la hemos visto pero dado que es muy breve la repetimos aquí.
'Función para llamar a función incorporada de Calc, es importante 'pasarle los argumentos correctamente en una matriz de datos y 'usar la función deseada con su nombre en ingles Function FuncionCalc( Nombre As String, Datos() ) Dim oSFA As Object oSFA = createUnoService( "com.sun.star.sheet.FunctionAccess" ) FuncionCalc = oSFA.callFunction( Nombre, Datos() ) End Function
Para mostrar el importe, usamos un control moneda (CurrencyField) por lo que solo establecemos el valor, el formato lo establecemos como propiedad de este control.
El botón de comando Agregar que puedes ver en la imagen anterior, nos sirve para agregar el artículo o producto a la lista de detalle de nuestra factura, su código es el siguiente.
'Para agregar un artículo al detalle de la factura Sub cmdAgregar_Clic() Dim dCantidad As Double Dim dPU As Double Dim dImporte As Double Dim sDescripcion As String Dim sInfo As String 'La cantidad, el precio y el importe dCantidad = txtCantidad.getValue dPU = txtPu.getValue dImporte = txtImporte.getValue 'La descripcion sDescripcion = Trim( txtDescripcion.getText ) 'Como en el cuadro de texto permitimos varias líneas, el usuario puede introducir 'saltos de líneas innecesarios al inicio y al final de la cadena sDescripcion = QuitarSaltosInicio( sDescripcion ) sDescripcion = QuitarSaltosFin( sDescripcion ) 'Establecemos la cadena ya limpia txtDescripcion.setText( sDescripcion ) 'La cantidad y el precio no pueden ser cero If dCantidad > 0 Then If dPU > 0 Then 'La descripción no puede estar vacia If sDescripcion <> "" Then 'Compañero si facturas artículos por más de un millón 'que esperas para hacerte mi mecenas If dImporte < 1000000 Then 'Agregamos el artículo y actualizamos el total Call AgregarArticulo( dCantidad, sDescripcion, dPU, dImporte) Call ActualizarTotal( dImporte ) 'Limpiamos los campos para un nuevo ingreso txtCantidad.setText( "" ) txtDescripcion.setText( "" ) txtPu.setText( "" ) txtCantidad.setFocus() Else MsgBox "El importe es muy alto, verificalo", "16", "Importe" txtPu.setFocus End If Else MsgBox "La descripción no puede estar vacía", 16, "Descripción" txtDescripcion.setFocus End If Else MsgBox "El P.U. no puede ser cero", 16, "Precio Unitario" txtPu.setFocus End If Else MsgBox "La cantidad no puede ser cero", 16, "Cantidad" txtCantidad.setFocus End If End Sub
Las nuevas funciones y macros usados en este evento son.
'Función recursiva para quitar los saltos de línea 'que estén al inicio de la cadena pasada Function QuitarSaltosInicio( Cadena ) As String Dim co1 As Integer 'Si la cadena esta vacia salimos de la función If Len(Cadena) = 0 Then QuitarSaltosInicio = "" Exit Function 'Si el primer caracter de la izquiera NO es un salto de línea 'regresamos la cadena y salimos de la función ElseIf Left(Cadena,1) <> Chr(10) Then QuitarSaltosInicio = Cadena Exit Function Else 'Si es un salto de línea, lo omitimos y regresamos el resto de la cadena Cadena = Mid( Cadena,2,Len(Cadena) ) 'Volvemos a llamar a la función QuitarSaltosInicio = QuitarSaltosInicio(Cadena) End If End Function 'Función recursiva igual a la anterior 'pero los quita al final de la cadena pasada Function QuitarSaltosFin( Cadena ) As String Dim co1 As Integer If Len(Cadena) = 0 Then QuitarSaltosFin = "" Exit Function ElseIf Right(Cadena,1) <> Chr(10) Then QuitarSaltosFin = Cadena Exit Function Else Cadena = Left( Cadena,Len(Cadena)-1 ) QuitarSaltosFin = QuitarSaltosFin(Cadena) End If End Function
Ten cuidado con las funciones recursivas, establece siempre como primera o primeras condiciones aquellas con las que salimos de la función. Con un poco de ingenio puedes juntar estas dos funciones en una sola, esa es tu tarea. Las macros restantes son.
'Para agregar un artículo al cuadro de lista Sub AgregarArticulo( Cantidad, Descripcion, Precio, Importe) Dim sTmp As String Dim iIndice As Integer Dim sLinea As String Dim mTmp() 'Obtenemos el nuevo indice para la matriz If IsNull( mDetalle ) Then iIndice = 0 Else iIndice = UBound(mDetalle)+1 End If 'y redimensionamos con este indice Redim Preserve mDetalle( iIndice ) 'Guardamos los datos en el nuevo elemento de la matriz With mDetalle( iIndice ) .Cantidad = Cantidad .Descripcion = Descripcion .Precio = Precio .Importe = Importe End With 'Formateamos la línea de texto para agregarla al cuadro de lista a cada campo 'le asignamos un espacio concreto y rellenamos con espacio en blanco el resto sTmp = Format( Cantidad, "#,##0.00" ) sLinea = Space(15-Len(sTmp)) & sTmp & " | " 'Cuando la descripción es muy larga, la recortamos solo para mostrarla, la cadena 'completa original queda guardada correctamente en la matriz de detalle sTmp = Left(Descripcion,47) 'Averiguamos si hay saltos de línea mTmp = Split( sTmp, Chr(10) ) If Len(sTmp) >= 47 Then 'Como los saltos los cuenta pero no se ven, los aumentamos como espacios sTmp = sTmp & Space(UBound(mTmp)) & "..." Else sTmp = sTmp & Space( 50-Len(sTmp) ) & Space(UBound(mTmp)) End If sLinea = sLinea & sTmp & " | " 'Quince espacios para el precio sTmp = Format( Precio, "#,##0.00" ) sLinea = sLinea & Space(15-Len(sTmp)) & sTmp & " | " 'Veinte para el importe sTmp = Format( Importe, "$ #,##0.00" ) sLinea = sLinea & Space(20-Len(sTmp)) & sTmp 'Agregamos la línea formateada completa lstDetalle.addItem( sLinea, lstDetalle.getItemCount ) End Sub 'Para actualizar el total de la factura, hacemos las operaciones necesarios 'damos el formato correcto y los mostramos en los controles correctos Sub ActualizarTotal( Importe ) Dim dImpuesto As Double Dim dTotal As Double dSubtotal = dSubtotal + Importe txtSubtotal.setText( Format( dSubtotal, "$ #,##0.00" ) ) 'Nos aseguramos de que el resultado tenga solo dos decimales dImpuesto = FuncionCalc( "ROUND", Array( dSubtotal*IVA, 2 ) ) txtImpuesto.setText( Format( dImpuesto, "$ #,##0.00" ) ) dTotal = dSubtotal + dImpuesto txtTotal.setText( Format( dTotal, "$ #,##0.00" ) ) End Sub
Con el código anterior ya debes de poder agregar artículos. Hay todavía un caso en el que al agregar un artículo, falle el guardado de la factura, tu tarea es encontrarlo, como pista, concéntrate en lo que permites guardar en el campo Descripción.
Por supuesto debemos de permitir al usuario quitarlos, para ello usamos el evento Clic del cuadro de lista, pero tiene que dar doble clic.
'Para quitar un articulo del detalle Sub lstDetalle_Doble_Clic( Evento ) Dim Pos As Integer If Evento.ClickCount = 2 And ( Evento.Source.getSelectedItemPos > -1 ) Then Pos = Evento.Source.getSelectedItemPos Evento.Source.removeItems( Pos, 1 ) Call QuitarArticulo( Pos ) End If End Sub
La macro QuitarArticulo tiene el siguiente código.
'Al quitar un articulo hay que actualizar el total y quitarlo de la matriz Sub QuitarArticulo( Indice ) Dim co1 As Integer Dim dImporte As Double Dim mTmp() As New Articulo 'Obtenemos el importe y lo ponemos en negativo dImporte = mDetalle( Indice ).Importe * -1 'Actualizamos el total Call ActualizarTotal( dImporte ) 'Si hay más de un artículo If UBound(mDetalle) > 0 Then 'Redimencionamos la matriz temporal Redim Preserve mTmp( UBound(mDetalle)-1 ) 'Movemos los datos a esta matriz For co1 = LBound( mTmp ) To UBound( mTmp ) 'Los que están antes del actual los pasamos tal cual If co1 < Indice Then mTmp(co1).Cantidad = mDetalle(co1).Cantidad mTmp(co1).Descripcion = mDetalle(co1).Descripcion mTmp(co1).Precio = mDetalle(co1).Precio mTmp(co1).Importe = mDetalle(co1).Importe Else 'Los que están después del actual los movemos del inmediato posterior mTmp(co1).Cantidad = mDetalle(co1+1).Cantidad mTmp(co1).Descripcion = mDetalle(co1+1).Descripcion mTmp(co1).Precio = mDetalle(co1+1).Precio mTmp(co1).Importe = mDetalle(co1+1).Importe End If Next co1 'Redimenciona la matriz original Redim Preserve mDetalle( UBound(mTmp) ) 'Movemos los datos correctos For co1 = LBound( mTmp ) To UBound( mTmp ) mDetalle(co1).Cantidad = mTmp(co1).Cantidad mDetalle(co1).Descripcion = mTmp(co1).Descripcion mDetalle(co1).Precio = mTmp(co1).Precio mDetalle(co1).Importe = mTmp(co1).Importe Next co1 Else 'Si no quedan elementos borramos la matriz Erase mDetalle End If End Sub
Todo ese código para mover los datos entre las matrices es por la limitada forma que tienen los lenguajes Basic en la manipulación de matrices.
Verifica que ya puedas agregar y quitar artículos o productos al detalle de la factura y que el importe total se actualice correctamente.
Nuestra siguiente tarea es permitirle al usuario agregar leyendas a la factura, estas, son útiles para agregar notas de cualquier tipo. El código del botón Leyenda es el siguiente.
'Para agregar una nueva leyenda a la factura Sub cmdLeyenda_Clic() Dim sLeyenda As String 'Cambia aquí el limite de leyendas que quieras permitir, te recomiendo un número pequeño 'tienes que adaptar el cuadro de lista del formato de impresión para que visualice 'correctamente este número de líneas If lstLeyendas.getItemCount < 4 Then sLeyenda = Trim(InputBox( "Introduce la nueva leyenda para esta factura" )) If sLeyenda <> "" Then lstLeyendas.addItem( sLeyenda, lstLeyendas.getItemCount ) End If Else MsgBox "Solo se permiten cuatro líneas de leyendas", 16,"Leyendas" End If End Sub
Y por supuesto el código para quitarlas.
'Con doble clic borramos las leyendas del cuadro de lista Sub lstLeyendas_Doble_Clic( Evento ) Dim Pos As Integer If Evento.ClickCount = 2 And ( Evento.Source.getSelectedItemPos > -1 ) Then Pos = Evento.Source.getSelectedItemPos Evento.Source.removeItems( Pos, 1 ) End If End Sub
Solo nos faltan tres botones por codificar, veamos el primero que es Guardar.
Y su código.
'Para guardar una factura Sub cmdGuardar_Clic() Dim sFactura As String Dim sCliente As String Dim sFecha As String Dim sSubtotal As String Dim sImpuesto As String Dim sTotal As String Dim sSQL As String Dim sTmp As String Dim co1 As Integer Dim sLeyendas As String Dim iRes As Integer 'Validamos todos los datos If ValidarDatos() Then iRes = MsgBox( "Todos los datos con correctos, ¿guardar la factura?", 36, "Guardar factura" ) 'si el usuarios responde que si If iRes = 6 Then 'Obtenemos los datos de la factura, la fecha y los valores los convertimos a texto para 'su uso correcto en la cadena SQL de inserción sFactura = txtFactura.getText() sCliente = txtClave.getText() sFecha = Format(txtFecha.getDate()) sFecha = "'" & Left(sFecha,4) & "-" & Mid(sFecha,5,2) & "-" & Right(sFecha,2) & "'" sSubtotal = Format(dSubtotal) sImpuesto = Format( FuncionCalc( "ROUND", Array( dSubtotal*IVA, 2 ) ) ) sTotal = Format( dSubtotal + Val(sImpuesto) ) 'Si hay leyendas las juntamos en una sola cadena separa por el caracter pipe (|) If lstLeyendas.getItemCount > 0 Then sLeyendas = "'" & Join( lstLeyendas.getItems(), "|" ) & "'" Else 'Si no hay leyendas, se pasa una cadena vacía, observa el par de comillas simples sLeyendas = "''" End If 'Si la factura es reservada ya esta guardada por lo que solo actualizamos los campos restantes If EsReservada Then sTmp = Chr(34) & "Cliente" & Chr(34) & "=" & sCliente & "," sTmp = sTmp & Chr(34) & "Fecha" & Chr(34) & "=" & sFecha & "," sTmp = sTmp & Chr(34) & "Subtotal" & Chr(34) & "=" & sSubtotal & "," sTmp = sTmp & Chr(34) & "Impuesto" & Chr(34) & "=" & sImpuesto & "," sTmp = sTmp & Chr(34) & "Total" & Chr(34) & "=" & sTotal & "," sTmp = sTmp & Chr(34) & "Estado" & Chr(34) & "='Elaborada'," sTmp = sTmp & Chr(34) & "Leyendas" & Chr(34) & "=" & sLeyendas sSQL = "UPDATE ""Facturas"" SET " & sTmp & " WHERE ""Numero""=" & sFactura Else 'Si es nueva usamos todos los campos sTmp = sFactura & "," & sCliente & "," & sFecha & "," & sSubtotal & "," & sImpuesto & "," & sTotal & ",'Elaborada'," & sLeyendas sSQL = "INSERT INTO ""Facturas"" ( ""Numero"",""Cliente"",""Fecha"",""Subtotal"",""Impuesto"",""Total"",""Estado"",""Leyendas"" ) VALUES (" & sTmp & ")" End If 'Insertamos los datos de la factura Call ActualizarDatos( oDatos, sSQL ) 'Insertamos el detalle de la factura For co1 = LBound( mDetalle ) To UBound( mDetalle ) sTmp = sFactura & "," & mDetalle(co1).Cantidad & ",'" & mDetalle(co1).Descripcion & "'," & mDetalle(co1).Precio & "," & mDetalle(co1).Importe sSQL = "INSERT INTO ""Detalle"" ( ""Factura"",""Cantidad"",""Descripcion"",""Precio"",""Importe"" ) VALUES (" & sTmp & ")" Call ActualizarDatos( oDatos, sSQL ) Next 'Prepara todo para una nueva factura Call cmdNuevaFactura_Clic() End If End If End Sub
El código de la función ValidarDatos para este cuadro de diálogo.
'Función para validar todos los datos Function ValidarDatos() As Boolean Dim sClave As String Dim sFecha As String Dim dFecha As Date Dim iRes As Integer 'La clave del cliente sClave = Format(Val(txtClave.getText())) 'Como usamos Val y después Format, en ves de vacía, si no hay nada siempre da 0 'cuidado, tal vez, dependiendo de tu tabla Clientes, tal vez si puedas tener un 'cliente con la clave 0 If sClave <> "0" Then 'Verificamos que exista el cliente If ExisteCliente( oDatos, sClave, True ) <> "" Then 'Verificamos que se hallan agregado productos If lstDetalle.getItemCount > 0 Then 'Obtenemos la fecha sFecha = Format(txtFecha.getDate) dFecha = DateSerial( Val(Left(sFecha,4)), Val(Mid(sFecha,5,2)), Val(Right(sFecha,2)) ) 'Si es menor a la fecha actual, se puede usar pero le avisamos al usuario If dFecha < Date() Then iRes = MsgBox( "La fecha " & Format(dFecha,"dd-mmm-yy") & " es una fecha pasada" & Chr(10) & Chr(10) & "¿Estás seguro de usar esta fecha?", 36, "Fecha pasada") If iRes <> 6 Then txtFecha.setFocus() Exit Function End If End If 'Si la fecha es mayor, también se puede usar pero avisamos If dFecha > Date() Then iRes = MsgBox( "La fecha " & Format(dFecha,"dd-mmm-yy") & " es una fecha futura" & Chr(10) & Chr(10) & "¿Estás seguro de usar esta fecha?", 36, "Fecha futura") If iRes <> 6 Then txtFecha.setFocus() Exit Function End If End If 'Si la factura es reservada avisamos If EsReservada Then iRes = MsgBox( "Esta factura " & txtFactura.getText() & " tiene estatus de RESERVADA" & Chr(10) & Chr(10) & "¿Estás seguro de usarla?", 36, "Guardar factura" ) If iRes <> 6 Then Exit Function End If End If 'Si todas las pruebas se pasaron regresamos Verdadero, cualquier validación que necesites 'agregala ANTES de esta línea ValidarDatos = True Else txtCantidad.setFocus() MsgBox "No hay artículos a facturar, agrega al menos uno", 16, "Sin artículos" End If Else cboClientes.setText( "" ) MsgBox "El cliente con la clave " & sClave & " no existe", 16,"Facturacion" txtClave.setFocus() End If Else cboClientes.setText( "" ) MsgBox "Escribe una clave de cliente a buscar", 16,"Facturacion" txtClave.setFocus() End If End Function
Antes ver el código del botón de comando Imprimir, es importante que ya tengas preparado el formato donde se vaciará la información de la factura a imprimir, tener perfectamente determinadas e identificadas las celdas que usaras para cada dato. En mi propuesta usaremos además, algunos controles de formulario que te muestro en la siguiente imagen.
Un cuadro de lista (1) para las leyendas, un cuadro de texto (2) para la cantidad con letra con su etiqueta respectiva, y otros tres cuadros de texto (3) para los totales. Para evitarnos el manipular directamente estos controles, los relacionaremos con celdas de la hoja de calculo y será en estas donde vaciaremos los datos respectivos. Es importante que todos estos controles los ancles a la página, no a la celda, con esto logramos que, independientemente del número de líneas que ocupe un concepto a facturar, la posición de estos controles no cambie y siempre se impriman en el lugar correcto. Por supuesto tienes que adaptar estas posiciones al formato físico de tu factura. Agrega además, una línea horizontal (1) a la altura de la ultima fila a imprimir, esta línea estará invisible (2), es importante que este anclada a la celda, de este modo, conforme insertemos conceptos, sé desplazará, la moveremos a la distancia que queramos por código y al estar anclada a la celda, sabremos en donde establecer el área de impresión.
Para ubicarla rápidamente por indice, asegúrate de enviarla al fondo de todos los elementos, de este modo sabemos que tendrá como indice el valor cero.
El código del botón de comando Imprimir es el siguiente.
'Para imprimir la factura actual Sub cmdImprimir_Clic() Dim iRes As Integer 'Validamos que todo este bien If ValidarDatos() Then iRes = MsgBox( "Todos los datos con correctos, esta factura no esta guardada, puedes imprimirla pero NO SE GUARDARÁ ningún dato, ¿deseas imprimirla?", 36, "Imprimir factura" ) If iRes = 6 Then 'Imprimimos tomando los datos actuales del cuadro de diálogo Call ImprimirFactura( "" ) End If End If End Sub
La nueva macro ImprimirFactura tiene el código siguiente.
'Macro para vaciar los datos al formato de impresion Sub ImprimirFactura( NumeroFactura As String ) Dim sCliente As String Dim sFecha As String Dim dFecha As Date Dim oFecha As Object Dim sLinea As String Dim sSubtotal As String Dim sImpuesto As String Dim sTotal As String Dim sEstado As String Dim oDatosCliente As Object Dim oDatosFactura As Object Dim oDatosDetalle As Object Dim sSQL As String Dim mLeyendas() Dim HayLeyendas As Boolean Dim co1 As Integer Dim sCelda As String Dim iCopias As Integer Dim lTotalDetalle As Integer 'Borramos las celdas donde vaciamos las leyendas Call BorrarDatosRango( ThisComponent, "Impresion", "A51:A54" ) 'Las celdas donde mostramos el detalle Call BorrarDatosRango( ThisComponent, "Impresion", "A11:D30" ) Call BorrarAreasImpresion( ThisComponent, "Impresion" ) 'Copias a imprimir iCopias = 1 'Tomamos los datos del cuadro de diálogo, con esto permitimos hacer pruebas 'e imprimir las llamadas pre-facturas If NumeroFactura = "" Then sCliente = txtClave.getText() sFecha = Format(txtFecha.getDate()) dFecha = DateSerial( Val(Left(sFecha,4)), Val(Mid(sFecha,5,2)), Val(Right(sFecha,2)) ) sSubtotal = txtSubtotal.getText() sImpuesto = txtImpuesto.getText() sTotal = txtTotal.getText() 'Si tenemos leyendas If lstLeyendas.getItemCount > 0 Then mLeyendas = lstLeyendas.getItems() HayLeyendas = True End If 'Vaciamos el detalle de la factura For co1 = LBound( mDetalle ) To UBound( mDetalle ) sCelda = "A" & Format(11+co1) Call EscribirEnCelda( ThisComponent, "Impresion", sCelda, 2, mDetalle(co1).Cantidad ) sCelda = "B" & Format(11+co1) Call EscribirEnCelda( ThisComponent, "Impresion", sCelda, 1, mDetalle(co1).Descripcion ) sCelda = "C" & Format(11+co1) Call EscribirEnCelda( ThisComponent, "Impresion", sCelda, 2, mDetalle(co1).Precio ) sCelda = "D" & Format(11+co1) Call EscribirEnCelda( ThisComponent, "Impresion", sCelda, 2, mDetalle(co1).Importe ) Next co1 Else 'Tomamos los datos de la base de datos sSQL = "SELECT * FROM Facturas WHERE Numero=" & NumeroFactura oDatosFactura = HacerConsulta( oDatos, sSQL ) sCliente = oDatosFactura.getString( 2 ) 'getDate nos devuelve una estructura com.sun.star.util.Date oFecha = oDatosFactura.getDate( 3 ) dFecha = DateSerial( oFecha.Year, oFecha.Month, oFecha.Day ) sSubtotal = Format( oDatosFactura.getDouble( 4 ), "$ #,##0.00" ) sImpuesto = Format( oDatosFactura.getDouble( 5 ), "$ #,##0.00" ) sTotal = Format( oDatosFactura.getDouble( 6 ), "$ #,##0.00" ) sEstado = oDatosFactura.getString( 7 ) If sEstado = "Elaborada" Then 'Cambiamos el número de copias predeterminadas a imprimir, generalmente las 'facturas se imprimen en juegos iCopias = COPIAS End If 'Si hay leyendas If oDatosFactura.getString( 8 ) <> "" Then 'Las obtenemos y llenamos la matriz mLeyendas = Split( oDatosFactura.getString( 8 ), "|" ) HayLeyendas = True End If 'El detalle de la factura sSQL = "SELECT * FROM Detalle WHERE Factura=" & NumeroFactura oDatosDetalle = HacerConsulta( oDatos, sSQL ) sSQL = "SELECT COUNT(Factura) FROM Detalle WHERE Factura=" & NumeroFactura lTotalDetalle = TotalRegistrosConsulta( oDatos, sSQL ) For co1 = 1 To lTotalDetalle sCelda = "A" & Format(10+co1) Call EscribirEnCelda( ThisComponent, "Impresion", sCelda, 2, oDatosDetalle.getFloat(2) ) sCelda = "B" & Format(10+co1) Call EscribirEnCelda( ThisComponent, "Impresion", sCelda, 1, oDatosDetalle.getString(3) ) sCelda = "C" & Format(10+co1) Call EscribirEnCelda( ThisComponent, "Impresion", sCelda, 2, oDatosDetalle.getFloat(4) ) sCelda = "D" & Format(10+co1) Call EscribirEnCelda( ThisComponent, "Impresion", sCelda, 2, oDatosDetalle.getDouble(5) ) oDatosDetalle.next() Next co1 End If 'Obtenemos y vaciamos los datos del cliente sSQL = "SELECT * FROM Clientes WHERE Id=" & sCliente oDatosCliente = HacerConsulta( oDatos, sSQL ) sLinea = oDatosCliente.getString( 2 ) Call EscribirEnCelda( ThisComponent, "Impresion", "A1", 1, sLinea ) sLinea = oDatosCliente.getString( 3 ) & " " & oDatosCliente.getString( 4 ) & ", Col. " & oDatosCliente.getString( 5 ) Call EscribirEnCelda( ThisComponent, "Impresion", "A2", 1, sLinea ) sLinea = oDatosCliente.getString( 6 ) & ", " & oDatosCliente.getString( 8 ) & ", C.P. " & oDatosCliente.getString( 7 ) Call EscribirEnCelda( ThisComponent, "Impresion", "A3", 1, sLinea ) sLinea = "R.F.C.: " & oDatosCliente.getString( 9 ) Call EscribirEnCelda( ThisComponent, "Impresion", "A4", 1, sLinea ) 'La fecha de la factura sLinea = "México, D.F., a " & Format( dFecha, "dd \d\e mmmm \d\e\l yyyy" ) Call EscribirEnCelda( ThisComponent, "Impresion", "D5", 1, sLinea ) 'Los importes Call EscribirEnCelda( ThisComponent, "Impresion", "D50", 1, sSubtotal ) Call EscribirEnCelda( ThisComponent, "Impresion", "D51", 1, sImpuesto ) Call EscribirEnCelda( ThisComponent, "Impresion", "D52", 1, sTotal ) 'La cantidad con letra sLinea = Numeros_Letras( Val(Mid(sTotal,3,50)), "peso", False, "", "(", "/100 m.n.)", 1) Call EscribirEnCelda( ThisComponent, "Impresion", "A50", 1, sLinea ) 'Si hay leyendas If HayLeyendas Then Call EscribirEnRango( ThisComponent, "Impresion", "A51", 1, mLeyendas() ) End If 'Establecemos el área de impresión e imprimimos Call EstablecerAreaImpresion(iCopias, sEstado, NumeroFactura) End Sub
Tenemos algunas macros nuevas, para borrar el contenido de un rango.
'Macro que borra cualquier valor, texto o formula del rango pasado Sub BorrarDatosRango( Documento As Object, Hoja As String, Rango As String ) Dim oRango As Object oRango = Documento.getSheets.getByName( Hoja ).getCellRangeByname( Rango ) oRango.clearContents( 21 ) End Sub
Para borrar todas las áreas de impresión.
'Borrar todas las áreas de impresión de la hoja Sub BorrarAreasImpresion( Documento As Object, Hoja As String ) Dim mAI() 'Borramos todas las áreas de impresión de la hoja Documento.getSheets.getByName( Hoja ).setPrintAreas( mAI() ) End Sub
Para escribir en una celda determinada.
'Vaciamos el dato pasado en la Celda pasada, hay que establecer el Tipo correcto Sub EscribirEnCelda( Documento As Object, Hoja As String, Celda As String, Tipo As Byte, Dato ) Dim oCelda As Object oCelda = Documento.getSheets.getByName( Hoja ).getCellRangeByname( Celda ) Select Case Tipo Case 1 : oCelda.setString( Dato ) Case 2 : oCelda.setValue( Dato ) Case 3 : oCelda.setFormula( Dato ) End Select End Sub
Para vaciar una matriz unidimensional en un rango, puedes mejorar esta macro de modo que acepte matrices de dos dimensiones y en ves de usar un ciclo usas el método setDataArray para vaciar toda la matriz.
'Escribe una matriz en un rango, solo hay que pasarle la celda de inicio Sub EscribirEnRango( Documento As Object, Hoja As String, Celda As String, Tipo As Byte, Datos() ) Dim oCelda As Object Dim co1 As Integer Dim Fil As Long, Col As Long oCelda = Documento.getSheets.getByName( Hoja ).getCellRangeByname( Celda ) 'fila y columna de inicio Fil = oCelda.getRangeAddress.StartRow Col = oCelda.getRangeAddress.StartColumn For co1 = LBound( Datos ) To UBound( Datos ) oCelda = Documento.getSheets.getByName( Hoja ).getCellByPosition( Col, Fil+co1 ) Select Case Tipo Case 1 : oCelda.setString( Datos(co1) ) Case 2 : oCelda.setValue( Datos(co1) ) Case 3 : oCelda.setFormula( Datos(co1) ) End Select Next co1 End Sub
La macro para establecer el área de impresión y por fin, imprimir. Asumimos que se envía a la impresora predeterminada, si usas otra impresora, usa las propiedades aprendidas en el capítulo de impresión.
'Establece el área de impresión e imprime Sub EstablecerAreaImpresion( NumCopias As Integer, Estado As String, NumeroFactura As String ) Dim mAI(0) As New com.sun.star.table.CellRangeAddress Dim oDir As New com.sun.star.table.CellAddress Dim oDoc As Object Dim oHoja As Object Dim oPaginaDibujo As Object Dim oLinea As Object Dim oAncla As object Dim oPos As New com.sun.star.awt.Point Dim mOpc(0) As New com.sun.star.beans.PropertyValue Dim oGlobal As Object Dim sSQL As String 'Servicio global de configuración de Calc oGlobal = createUnoService( "com.sun.star.sheet.GlobalSheetSettings" ) oDoc = ThisComponent oHoja = oDoc.getSheets.getByName( "Impresion" ) oPaginaDibujo = oHoja.getDrawPage() 'Esta línea oculta nos auxilia para obtener el área de impresión oLinea = oPaginaDibujo.getByINdex( 0 ) oPos.X = 0 'La posicionamos a 18 cm de la parte superior que es el área de impresión disponible oPos.Y = 18000 oLinea.setPosition( oPos ) 'Obtenemos la dirección de la celda donde haya quedado oDir = oLinea.Anchor.getCellAddress 'Construimos el área de impresión correctamente 'Rango A1:D ? mAI(0).Sheet = oDir.Sheet mAI(0).StartColumn = 0 mAI(0).StartRow = 0 mAI(0).EndColumn = 3 mAI(0).EndRow = oDir.Row 'Agregamos el área de impresión oHoja.setPrintAreas( mAI() ) 'Preguntamos cuantas copias se imprimirán NumCopias = Val( InputBox( "¿Cuantas copias se imprimirán?" & Chr(10) & Chr(10) & "Asegurate de que la impresora este lista para imprimir", "Imprimir factura", NumCopias ) ) 'Tienes que ser mayor a cero If NumCopias > 0 Then 'El número de copias mOpc(0).Name = "CopyCount" mOpc(0).Value = NumCopias 'Enviamos a imprimir, dado que imprimiremos una hoja diferente a la seleccionada 'es importante activar esta propiedad, si no, no imprimirá nada oGlobal.PrintAllSheets = True 'Imprimimos oDoc.Print( mOpc() ) 'Desactivamos la propiedad oGlobal.PrintAllSheets = False 'Actualizamos el estado de la factura, solo si su estado anterior es Elaborada If Estado = "Elaborada" Then sSQL = "UPDATE ""Facturas"" SET ""Estado""='Impresa' WHERE ""Numero""=" & NumeroFactura Call ActualizarDatos( oDatos, sSQL ) End If Else MsgBox "Proceso cancelado, no se imprimió la factura", 0, "Factura Libre" End If End Sub
Solo nos resta hacer el código del botón de comando Guardar e Imprimir que es el siguiente, esta macro usa las mismas subrutinas anteriores. Como le pasamos el número de factura a la macro ImprimirFactura, toma los datos de la factura de la base de datos.
'Guarda e imprime la factura Sub cmdGuardarImprimir_Clic() Dim sFactura As String Dim iRes As Integer 'Guardamos el número de factura actual sFactura = txtFactura.getText() 'Intentamos guardar Call cmdGuardar_Clic() 'Si cambia el número de factura esta se guardó correctamente If sFactura <> txtFactura.getText() Then iRes = MsgBox( "La factura " & sFactura & " se guardó correctamente, puedes imprimirla ahora, ¿deseas imprimirla?", 36, "Imprimir factura" ) If iRes = 6 Then 'Imprimimos la factura guardada Call ImprimirFactura( sFactura ) End If End If End Sub
Mira que bien esta quedando.
Tal vez pensaras que se me olvido el código del botón Ver Facturas, pero no, no se me olvido si no que es tu tarea escribirlo, la idea de este botón es mostrar en un nuevo cuadro de diálogo, las facturas guardadas, darle la posibilidad al usuario de filtrar por varias formas, de ver el detalle de la factura que quiera y de cambiar el estatus de las mismas, este cuadro de diálogo tiene que ser capaz también, de imprimir o reimprimir la factura que sea, así como de mostrar los reportes necesarios de la facturación. No me decepciones e intenta resolverlo antes de ver mi propuesta en los archivos de código que acompañan a este libro.
Posibles mejoras que te quedan de tarea:
- Si la base de datos no esta registrada, pero el archivo ODB esta en el mismo directorio del archivo usado como interfaz, podría registrarse automáticamente o preguntarle al usuario si quiere usar este archivo como base de datos fuente.
- Evitar que el usuario cierre los cuadros de diálogo con el botón de cierre, obligarlo a usar siempre el botón Salir.
- Puedes cambiar los controles de texto por controles de formato donde se requiera, por ejemplo: el C.P. Y el RFC.
- Puedes permitir al usuario moverse entre los campos con la tecla ↵ Enter .
- ¿Se te ocurre alguna otra?
Solo me resta decirte; para no cometer errores, tienes que convertirte en un experimentado programador, para convertirte en un programador experimentado, tienes que cometer muchos errores...
¡¡Feliz programación!!
Si tienes dudas acerca de lo aquí explicado, tienes algún problema con AOO, o quieres ampliar la información, no dudes en dirigirte al Foro Oficial en español de Apache OpenOffice para Macros y API UNO |