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 |