61597879 sistema de facturacion

28
1 Sistema de facturación y control de Stock <<<NOTA: este apunte no está terminado, a medida que vaya completandolo será actualizado. Ernesto Cullen>>> Como ejemplo de aplicación de las técnicas de Bases de Datos y realización de programas, haremos un programa para llevar un control de Inventario (Stock) incluyendo la facturación de los productos. Este ejemplo no pretende ser una implementación de nivel comercial; simplemente demuestra técnicas y herramientas de uso común. Por lo tanto restringiremos nuestra atención a un hipotético comercio -La Luz Mala S.A., Artículos de Iluminación- y solamente trabajaremos con los datos de productos y clientes, que son necesarios para la facturación. Algunas partes quedarán abiertas para que el lector las termine, de manera de tornar el ejemplo en una especie de taller práctico. Requerimientos En lo que sigue, consideraré que el lector posee cierto manejo de Delphi, en especial supondré que sabe cómo crear una tabla y conectarla desde la aplicación. También asumo que las nociones básicas de diseño de Bases de Datos (Diagramas Entidad Relación, tipos y cardinalidad de relaciones entre tablas, etc) son conocidas. Cualquier libro o curso básico de diseño de Bases de Datos relacionales trata estos temas. Desarrollaremos primero la aplicación completa usando tablas de Paradox, para poner el énfasis en temas como validación, trabajo con tablas dependientes, y otros temas que rara vez se encuentran aplicados a un problema concreto, aunque en la realidad aparecen en la mayoría de los casos. Una vez que tengamos la aplicación completa y funcionando migraremos los datos a un servidor SQL (Interbase) y nos centraremos en los problemas que pueden surgir como consecuencia. Por último, agregaremos algunos “extras”: utilización directa de aplicaciones a través de Automatización OLE, poner procesos en hilos de ejecución separados, etc. Este ejemplo está planeado para llenar un agujero en los cursos que normalmente se encuentran sobre programación: la aplicación de distintas técnicas a una aplicación “de verdad”, completa y funcional. A casi todos nos ha pasado cuando empezamos a crear programas reales que nos encontramos con problemas muy particulares, distintos a los que se tratan en los ejemplos del libro de Delphi que compramos. El proceso de encontrar soluciones a esos problemas es apasionante e instructivo, pero también lleva su tiempo y esfuerzo. Si este ejemplo los ayuda a ganar un poco de ese tiempo sin quitar la parte instructiva, entonces habrá cumplido su objetivo -y yo el mío. 1) Diseño de la BD

Upload: javier-cedeno

Post on 30-Nov-2015

58 views

Category:

Documents


10 download

TRANSCRIPT

Page 1: 61597879 Sistema de Facturacion

1

Sistema de facturación y control deStock

<<<NOTA: este apunte no está terminado, a medida que vaya completandolo será actualizado. ErnestoCullen>>>

Como ejemplo de aplicación de las técnicas de Bases de Datos y realización de programas, haremos unprograma para llevar un control de Inventario (Stock) incluyendo la facturación de los productos.

Este ejemplo no pretende ser una implementación de nivel comercial; simplemente demuestra técnicas yherramientas de uso común. Por lo tanto restringiremos nuestra atención a un hipotético comercio -La LuzMala S.A., Artículos de Iluminación- y solamente trabajaremos con los datos de productos y clientes, que sonnecesarios para la facturación. Algunas partes quedarán abiertas para que el lector las termine, de manerade tornar el ejemplo en una especie de taller práctico.

Requerimientos

En lo que sigue, consideraré que el lector posee cierto manejo de Delphi, en especial supondré que sabecómo crear una tabla y conectarla desde la aplicación. También asumo que las nociones básicas de diseño deBases de Datos (Diagramas Entidad Relación, tipos y cardinalidad de relaciones entre tablas, etc) sonconocidas. Cualquier libro o curso básico de diseño de Bases de Datos relacionales trata estos temas.

Desarrollaremos primero la aplicación completa usando tablas de Paradox, para poner el énfasis en temascomo validación, trabajo con tablas dependientes, y otros temas que rara vez se encuentran aplicados a unproblema concreto, aunque en la realidad aparecen en la mayoría de los casos. Una vez que tengamos laaplicación completa y funcionando migraremos los datos a un servidor SQL (Interbase) y nos centraremosen los problemas que pueden surgir como consecuencia. Por último, agregaremos algunos “extras”:utilización directa de aplicaciones a través de Automatización OLE, poner procesos en hilos de ejecuciónseparados, etc.

Este ejemplo está planeado para llenar un agujero en los cursos que normalmente se encuentran sobreprogramación: la aplicación de distintas técnicas a una aplicación “de verdad”, completa y funcional. A casitodos nos ha pasado cuando empezamos a crear programas reales que nos encontramos con problemas muyparticulares, distintos a los que se tratan en los ejemplos del libro de Delphi que compramos. El proceso deencontrar soluciones a esos problemas es apasionante e instructivo, pero también lleva su tiempo y esfuerzo.Si este ejemplo los ayuda a ganar un poco de ese tiempo sin quitar la parte instructiva, entonces habrácumplido su objetivo -y yo el mío.

1) Diseño de la BD

Page 2: 61597879 Sistema de Facturacion

2

Figura 1: la ventana principal

Después de noches de vigilia pensando en la mejor manera de almacenar los datos de este ejemplo, hemosllegado al siguiente diagrama entidad/relación:

Vemos en el diagrama que tenemos cuatro entidades, relacionadas entre si. Este es el diagrama lógico,independiente del motor de Base de Datos que utilicemos. El motor a utilizar determinará los tipos de datos,aunque ya podemos (y debemos) definir cuál campo será numérico, cuál de texto, etc. En el diagrama se venlos tipos que hemos seleccionado entre los disponibles para el diagrama lógico en el programa E/R Studio deEmbarcadero SA.

La elección del motor de Bases de Datos a utilizar no es trivial; de hecho, es una de las primeras decisionesimportantes que tendremos que tomar. Todos tienen sus pros y sus contras; debemos hallar un punto medioentre la facilidad de implementación, las posibilidades que nos brindan, la seguridad, el costo... Por suerteDelphi y la BDE nos permiten (hasta cierto punto) pasar de un formato a otro con un mínimo deinconvenientes.

Como primera elección nos inclinaremos por el motor de Bases de Datos de Paradox, porque viene incluidocon Delphi y por lo tanto es el más simple de usar. Más tarde

Ejercicio 1Implemente las tablas correspondientes al diagrama E/R anterior. Cree un alias apuntando a laBase de Datos generada.

?

2) Diseño de la interface

La aplicación consta de una ventana principal desde la que se accede a las distintas opciones a través de unmenú:

ArchivoSalir

DatosABM Clientes...ABM Productos...

FacturaciónAlta...Anulación...Consultas...

AyudaAcerca de...

Las pantallas de ABM (Altas, Bajas,Modificaciones) de datos para clientes yproductos tienen ciertas similitudes:

? Tienen botones para Cerrar, Imprimir, Buscar, Agregar, Borrar, Propiedades

Page 3: 61597879 Sistema de Facturacion

3

Figura 2: estructura de la BD propuesta

Figura 3: ficha base de las ABM

? Tienen dos paneles: uno con una lista de los datos más útiles y otra con los datos detallados del registroactivo

Para aprovechar estas semejanzas, crearemos una ficha maestra con los controles y propiedades comunes y luego heredaremos de ésta las fichas para cada caso particular.

La ficha general es:

Las tablas serán colocadas en un DataModule, que llamaremos DM1. No lo incluimos en el USES de estaunit ya que no lo usamos todavía, lo pondremos en las fichas descendientes.

Notaremos que hay dos botones de impresión. El de la izquierda generará un listado completo de la tabla quevemos en la grilla, mientras que el de la derecha se refiere a los datos del registro actual.

Los botones de Aceptar y Cancelar tienen ya un código asociado, para aceptar o rechazar los cambios(notemos la referencia indirecta a la tabla, a través de la grilla):

procedure TForm2.bAceptarClick(Sender: TObject);begin if DBGrid1.Datasource.Dataset.State in dsEditModes then

Page 4: 61597879 Sistema de Facturacion

4

Figura 4: agregar la ficha al almacén de objetos

Figura 5: propiedades de la ficha para agregar alalmacén

Figura 6: crear una nueva ficha heredando lascaracterísticas de la ficha ABMMaster

DBGrid1.Datasource.Dataset.Post;end;

procedure TForm2.bCancelarClick(Sender: TObject);begin if DBGrid1.Datasource.Dataset.State in dsEditModes then DBGrid1.Datasource.Dataset.Cancel;end;

Este código es el mismo para todas las ventanas deAltas, Bajas y Modificaciones y por eso se puedecolocar en la ventana madre. Más tarde agregaremosotras operaciones que también son generales.

Para poder utilizar esta ficha como base para las otrasventanas, debemos primero guardar el modelo en elAlmacén de Objetos (Repository) de Delphi.

Teniendo la ficha visible, presionamos el botón derechodel ratón sobre la misma y seleccionamos “Add toRepository...” como indica la figura 3

A continuación seleccionamos la página del almacénde objetos donde queremos que aparezca nuestraficha, le damos un nombre y una descripción, yeventualmente seleccionamos un icono para representarlo (fig.4).

La ficha y su unidad asociada son ahora las fuentes de uno delos objetos del almacén. Notemos que si movemos orenombramos estos archivos no podremos utilizarlos desde elalmacén, aunque la referencia siga estando allí.

Ahora crearemos las ventanas de datos descendientes, unaenlazada con la tabla de clientes y la otra con la tabla deproductos.

Para ello, seleccionamos del menú File la opción New... y en el Almacén miramos en la página de nuestraaplicación (en mi caso la llamé Factura2). Seleccionamos la ventana maestra (en mi caso, llamada

fABMMaster), comprobamos que esté seleccionada la opcióninherit (heredar) y damos al OK (fig. 5).

Logramos una copia idéntica de la ventana principal de ABM,con todas sus características. La ventaja es que hemosheredado todas sus propiedades y métodos (como losprocedimientos de los botones Aceptar y Cancelar) peropodemos cambiar cualquier cosa. Llamemos a esta fichafABMClientes y la grabamos con el nombre uABMClientes;podemos también poner un título (Caption) más indicativo que elque pone Delphi por defecto (por ejemplo, “Datos de clientes”).

Realizamos la misma operación para la ventana de ABM deProductos (fABMProductos).

Ahora tenemos que darles vida a las ventanas nuevas: para eso

Page 5: 61597879 Sistema de Facturacion

5

Figura 7: ventana de ABM de clientes

necesitamos colocar las tablas en el proyecto.

Creamos entonces el Módulo de Datos y ponemos las tablas y Fuentes de Datos necesarias. Además,creamos los componentes de campo para cada tabla y agregamos los enlaces:

? Entre las tablas de Facturas y Detalle hay una relación Master/Detail: en la tabla de Detalle ponemos laspropiedades MasterSource al DSFacturas y MasterFields a NroFactura-Factura

? Entre las tablas de Facturas y Clientes hay una relación de Lookup: creamos un campo lookup que tomeel campo Cliente de Facturas y lo relacione con el campo IDCliente de Clientes, mostrando el campoNombreYApellido.

? Entre las tablas de Detalle y Productos también hay una relación de Lookup: creamos un campo lookupque tome el campo Producto de Detalle y lo relacione con el campo CodProd de Productos, mostrando elcampo Descripcion.

Nos concentraremos ahora en las propiedades que hay que cambiar en la ficha de ABM de Clientes paradiferenciarla de su “madre”.

Primero, conectamos el navegador y la grilla con la fuente de datos de clientes que tenemos en el módulo dedatos (recordemos incluir la unit del módulo de datos en la cláusula USES). A continuación ponemoscontroles de datos en el panel de la derecha para cada uno de los campos que podemos editar (podemosarrastrar y soltar los campos desde el editor de campos de la tabla de clientes):

He resaltado los títulos (son simples etiquetas) poniendo el texto en Negrita. El campo IDCliente no eseditable (es autonumérico), de manera que he utilizado un control DBText en lugar de un DBEdit. Además,he colocado sólo dos columnas en la grilla: NombreYApellido (con otro título) y Telefonos.

El botón de “Propiedades” por ahora no hace más que poner el cursor (el foco de atención del teclado) en elprimero de los editores de la derecha:

Page 6: 61597879 Sistema de Facturacion

6

procedure TfABMClientes.BitBtn3Click(Sender: TObject);begin inherited; DBEdit2.SetFocus;end;

Notemos la palabra “inherited” que escribió Delphi al principio del procedimiento. Esto significa que sellamará al procedimiento del mismo nombre definido en la ficha de la cual desciende la que estamostrabajando. En este caso no hay un procedimiento tal en la ficha FABMMaster, pero si alguna vez loagregamos será llamado automáticamente. Podemos borrar esta línea, o cambiarla de lugar dentro delprocedimiento.

Ya podemos probar la ventana, si agregamos en la ventana principal las instrucciones necesarias para que semuestre en respuesta a la opción “ABM Clientes” del menú “Datos”. Por ejemplo, podría ser algo como losiguiente:

beginFABMClientes.Show;

end;

Estoy suponiendo que permitimos a Delphi que cree automáticamente la ventana al comenzar la aplicación(Opciones del proyecto).

En esta ventana ya podemos ver y modificar clientes ya existentes; sigamos expandiendo la funcionalidadescribiendo los manejadores de los otros botones.

Podemos aprovechar la relación de herencia que existe entre la ficha que guardamos en el almacén ynuestras fichas de ABM Clientes y ABM Productos; si no utilizamos referencias directas a una fichaparticular en los procedimientos de respuesta de los botones, entonces podemos escribir el código en la fichamadre y automáticamente estará disponible en las dos fichas descendientes -y en cualquier otra queinventemos después.

El botón de imprimir del panel de la izquierda debe hacer un listado simple de los datos de toda la tabla que semuestra en la grilla. Pues bien, hay una forma muy práctica de generar este listado: la función QRCreateListque nos brinda QuickReport en la unit QRExtra. Utilizando esta función y con un poco de cuidado, podemosinclusive poner este código en el botón de la ventana maestra. De esta manera será heredado por lasventanas descendientes. El código sería algo como lo siguiente:

procedure TfABMMaster.BitBtn1Click(Sender: TObject);var q:tCustomQuickRep; l: tStringList; i: integer;begin l:= tStringList.Create; q:= nil; for i:= 1 to DBGrid1.Columns.count do l.Add(DBGrid1.Columns[i-1].FieldName); QRCreateList(q, Self, DBGrid1.DataSource.Dataset,'Listado de '+Caption, l); q.Preview; l.Free; q.Free;end;

Page 7: 61597879 Sistema de Facturacion

7

Figura 8: ventana de búsqueda de Clientes

Debemos incluir en la cláusula USES un par de units: QRExtra y QuickRpt, donde están definidas la funciónQRCreateList y la clase TcustomQuickRep respectivamente.

Notemos que pasamos aquí las columnas de la grilla como columnas del reporte; es decir, el reporte impresoserá muy parecido a lo que se ve en la grilla.

La generación de una lista de la tabla completa es simple y se puede hacer en forma genérica como antes.Pero la impresión de la ficha con los datos de un registro es distinta para los clientes y para los productos, porlo que tendremos que hacerla “a mano”, generando un reporte para cada una que será llamado en losbotones de “Ficha” de cada ventana descendiente.

Lo veremos luego. Primero hagamos la parte de búsqueda.

La búsqueda también es particular para cada descendiente, así que tendremos que codificarla por separado.Permitiremos la búsqueda por varios campos. Para la tabla de clientes podríamos mostrar una ficha de datoscomo la siguiente:

Al presionar el botón “Buscar” realizamos la búsqueda utilizando Locate para independizarnos de los índices.Si encontramos una coincidencia cerramos la ventana de búsqueda poniendo un valor mrOK en la propiedadModalResult de la ventana; el cursor se habrá movido automáticamente en la tabla, de manera queestaremos posicionados correctamente sobre el registro buscado. En caso que el texto buscado no seencuentre, mostramos un mensaje al usuario y no cerramos la ventana, para permitirle que siga buscando.

El botón “Cancelar” cierra la ventana: tiene puesta la propiedad ModalResult en mrCancel de manera queeste valor va a parar a la propiedad del mismo nombre de la ventana cuando se lo presiona. No hace faltaningún código.

El código del botón “Buscar” es el siguiente:

procedure TFBuscarCliente.BitBtn1Click(Sender: TObject);var s: string;begin //seleccionamos el campo a buscar if RadioGroup1.ItemIndex = 0 then s:= 'NombreYApellido' else s:= 'Telefonos'; //Busca, y si encuentra cierra la ventana con OK if DM1.tabClientes.Locate(s,edit1.text,[loPartialKey]) then ModalResult:= mrOk else ShowMessage('No se encuentra el cliente solicitado');

Page 8: 61597879 Sistema de Facturacion

8

Figura 9: impresión de los datos de un cliente

end;

Y ya tenemos casi lista nuestra ventana de Altas Bajas y Modificaciones de Clientes: sólo falta la impresiónde la ficha. Vamos a ello.

Para la impresión de los datos del cliente, creamos un QuickReport como el que se muestra en la fig. 8.

Entonces el código en el procedimiento de respuesta al botón “Ficha” podría ser algo como lo siguiente:

procedure TfABMClientes.BitBtn5Click(Sender: TObject);begin inherited; FFichaCliente.Preview;end;

Como dijimos antes, Delphi agrega automáticamente la primera línea para llamar al código heredado antes dehacer nada más. En nuestro caso no tenemos nada de código en la ventana madre para este botón, así quepodríamos borrarla. No obstante la dejaremos porque si el día de mañana agregamos algo en la ventanamadre se ejecutará automáticamente.

Hay un fallo en la lógica del código anterior; cuando presionemos el botón de imprimir los datos del clienteque estamos viendo, se nos mostrará el reporte... con los datos de todos los clientes. Algo así como el listadoque generamos antes, pero más lindo.

Este comportamiento no es el que deseamos; este botón debería imprimir solamente los datos del clienteactualmente seleccionado. Para eso debemos filtrar de alguna manera la tabla.

Existen varios métodos de filtrado que podemos usar. En este ejemplo usaremos la propiedad filter.

El código para el evento OnClick sobre el botón de impresión de datos personales queda ahora como elsiguiente:

procedure TfABMClientes.BitBtn5Click(Sender: TObject);begin inherited; DM1.tabClientes.Filter:= 'IDCliente=' + DM1.tabClientes.FieldByName('IDCliente').AsString; DM1.tabClientes.Filtered:= true; FFichaCliente.Preview; DM1.tabClientes.Filtered:= false;

Page 9: 61597879 Sistema de Facturacion

9

end;

Notemos que después de mostrar el reporte sacamos el filtrado a la tabla.

Faltan algunos detalles: por ejemplo, ¿qué sucede si queremos borrar un registro de la tabla de clientes?Debería pedirnos confirmación. Podemos mostrar una caja de diálogo cuando presionamos el botón deborrar, pero así quedamos expuestos a que en una modificación posterior agreguemos otra forma de borrarlos registros -por ejemplo, en una grilla de consulta- y el control no se haga.

El lugar más conveniente para pedir la confirmación es el evento BeforeDelete de la misma tabla, que sedisparará siempre cualquiera sea la forma de borrar el registro:

procedure TDM1.tabClientesBeforeDelete(DataSet: TDataSet);begin if MessageDlg('Se va a borrar el registro. ¿Continuar?',mtConfirmation, mbYesNo,0)=mrNo then abort;end;

El procedimiento es el mismo para la tabla de productos (notemos que en ningún momento necesitamosnombrar la tabla). Delphi nos permite indicar a esta tabla que llame al mismo procedimiento anterior;simplemente, en el Inspector de Objetos abrimos la lista y seleccionamos para el evento BeforeDelete de latabla de Productos el mismo procedimiento.

Hay otra consideración que hacer con respecto al borrado en la tabla de facturas y su relación con la dedetalle, pero lo postergaremos hasta que veamos la facturación.

Ahora sí tenemos completa la ventana de Altas, Bajas y Modificaciones de clientes. Lo mismo hay quehacer para los productos, pero... lo harán Uds.

Ejercicio 2Crear la ventana de ABM de productos heredando de ABMMaster, completa con todos losbotones funcionando.

?

Facturación

La parte de facturación es la más complicada de esta aplicación, porque enlaza y utiliza todas las tablas a lavez. Veamos primero la ventana terminada:

Page 10: 61597879 Sistema de Facturacion

10

Facturas.db

Detalle.db

Figura 10: ventana de alta de facturas

En esta ventana trabajamos sobre dos tablas: Facturas y Detalle. En la parte de arriba tenemos controlespara modificar los campos de la tabla de Facturas, y en la parte inferior tenemos una grilla que muestra ytrabaja con los datos de la tabla Detalle (fig. 9). Estas dos tablas están relacionadas en formaMaestro/Detalle, de manera que automáticamente la tabla de Detalle se filtra para mostrar sólo los registrosque correspondan a la factura que se ve arriba. Delphi incluso toma en consideración la relación cuandoagregamos registros a la tabla de detalle, poniendo automáticamente los valores que corresponden en loscampos de enlace (en este caso, Nro y tipo de factura).

Podemos ver el comportamiento anterior si dejamos en la grilla todas las columnas de la tabla Detalle.Notaremos que al momento de insertar un registro nuevo Delphi da valor automáticamente a los campos Nrode Factura y Tipo de Factura. El campo IDItem no es editable porque es de tipo autonumérico, y nos quedansolamente la cantidad, el código del producto y el precio unitario. Posteriormente modificaremos la grilla paraque la columna de código nos deje elegir alguno de los productos de la tabla de Productos en una lista, antesque escribirlos directamente con las posibilidades de error que eso traería; además, definiremos un nuevocampo virtual -no existente en la tabla física- para mostrar el subtotal, resultado de multiplicar la cantidad porel precio unitario. El precio unitario debe tomar como valor por defecto el precio indicado en la tabla deproductos, pero se debe poder modificar.

El primer problema que nos encontramos al trabajar con dos tablas relacionadas es que para agregarregistros a la tabla de Detalle debemos tener un registro válido seleccionado en la tabla Principal. Porconsiguiente, en nuestra factura debemos asegurarnos que cada vez que el usuario va a modificar algo en latabla de Detalle, la de Facturas no esté en modo de inserción o edición, porque podríamos estar cambiandolos valores de los campos de enlace. El código es simple: si la tabla de Facturas está en estado de inserción oedición, aceptamos los datos y listo. La pregunta del millón es: ¿adónde colocamos el código?

Una primera idea sería en el evento OnExit del último control de la parte de arriba. No obstante, si lopensamos un poco más vemos que un ratón en la mano de un usuario se transforma en un arma mortífera: esmuy fácil modificar por ejemplo el nro. de factura y después directamente pasar el foco a la grilla... tirandopor el suelo nuestra estrategia ya que el usuario no entraría en el control donde pusimos nuestro código.

Debemos encontrar un evento que se produzca inequívocamente antes de modificar la tabla de detalle. La

Page 11: 61597879 Sistema de Facturacion

11

Figura 11: selección de un cliente usando unDBLookupComboBox

única manera de modificar los datos del Detalle en esta pantalla (y en la esta aplicación) es la grilla de laventana de Alta de Facturas, por lo que podríamos controlar el estado de la factura al ganar el foco la grilla:el evento OnEnter de la grilla. El código queda como sigue:

procedure TFAltaFactura.DBGrid1Enter(Sender: TObject);begin if dm1.tabFacturas.State in dsEditModes then dm1.tabFacturas.Post;end;

Ahora tenemos la tabla en el estado correcto: podemos probarlo si corremos el programa, ponemos valores alos campos NroFactura y Tipo (luego veremos cómo asignarles valores por defecto) y entramos a la grillapara agregar un detalle. Los campos de enlace tomarán valor solos.

Sigamos trabajando sobre los controles de la tabla de Facturas.

Componentes de búsqueda (lookup)

El campo de Cliente también impone una condición: dado que guardamos en la tabla de facturas solamenteel ID del Cliente (de acuerdo con las reglas de normalización), tendríamos que ingresar un número en estecampo. Pero no es necesario obligar al usuario a recordar los identificadores internos de los clientes;podemos mostrar una lista con los nombres y apellidos y decirle a Delphi que en realidad queremos guardarel ID del que seleccionamos. Para lograr esto utilizamos un control DBLookupComboBox, con las siguientespropiedades:

? DataSource: dm1.dsFacturas? DataField: Cliente? ListSource: dm1.dsClientes? ListField: NombreYApellido? KeyField: IDCliente

El resultado se ve en la fig. ?. De esta manera el usuariosiempre trabajará con el Nombre y Apellido del cliente,mientras internamente se maneja sólo el número deidentificación.

Luego veremos que este es también el caso del campoProducto de la tabla de Detalle. En general, casi siempre esconveniente trabajar con este sistema para los campos quereferencian a otra tabla (claves externas).

Validaciones

Validar los datos significa comprobar que los mismos se ajustan a las restricciones que pueda haber definidassobre ellos; por ejemplo, que no haya letras en un campo numérico. Hay tres momentos para hacer lasvalidaciones:

? Al escribir (caracter a caracter)

Page 12: 61597879 Sistema de Facturacion

12

? Al introducir el valor en el campo

? Al introducir el registro en la Base de Datos (Post)

Aplicaremos en este ejemplo los tres tipos de validaciones.

El campo de Tipo de Factura pone una restricción: queremos que deje ingresar sólo una letra, solamente “A”,“B” o “C” y en mayúsculas. Tratemos estos problemas uno por uno.

1) Solamente una letra

Es fácil: ponemos la propiedad MaxLength del DBEdit correspondiente en 1. Claro que también está larestricción de la definición de la tabla, en la que asignamos una longitud 1 al campo Tipo, pero si podemosimpedir que el usuario cometa un error, mejor. Notemos que también podríamos haberlo hecho en lamáscara de edición del componente de campo.

2) En mayúsculas

También se puede hacer en el editor: ponemos la propiedad CharCase a ecUpperCase. Tambiénpodríamos hacerlo en la máscara del componente de campo.

3) Solamente “A”, “B” o “C”

Nuevamente aquí tenemos un control que se tiene que hacer en la tabla, pero conviene comprobartambién en el programa cliente para lograr una rápida respuesta al usuario; en lugar de esperar que elservidor de Bases de Datos nos devuelva el error, interpretarlo y mostrarlo, controlamos antes deenviarlo.

Podemos hacerlo en el evento BeforePost de la tabla, en esta aplicación simple; pero en una más generaltendríamos que comprobar en este evento todos los campos que requieren verificación y mostrar elmensaje correspondiente para cada uno. Es mejor utilizar los eventos de los componentes de datos, entrelos cuales hay uno que es específicamente para realizar validaciones antes de enviar los datos a la BD:OnValidate. Colocamos entonces el siguiente código en el evento OnValidate del componente del campoTipo de la tabla Facturas:

procedure TDM1.tabFacturasTipoValidate(Sender: TField);begin if (Sender.AsString<>'A') and (Sender.AsString<>'B') and (Sender.AsString<>'C') then begin ShowMessage('El tipo de factura debe ser ''A'', ''B'' o ''C'''); Abort; end;end;

Este evento se ejecuta cuando Delphi intenta introducir los datos en el campo (todavía en memoria hasta quehagamos el Post). Si solamente mostramos el mensaje, los datos llegarán igualmente a la memoria intermediadel campo y podrían ser rechazados por las validaciones de la Base de Datos, cuando hagamos el Post. Paraque esto no suceda, debemos provocar una excepción que corte el flujo del programa: la instrucción Aborthace justamente eso. El foco no sale del editor hasta que coloquemos un valor que pase la validación o

Page 13: 61597879 Sistema de Facturacion

1 En algunas aplicaciones conviene dejar que el usuario salga del editor, mostrando simplemente elmensaje. Y en otras es indispensable, cuando el valor de un campo se corresponde con los valores de otroscampos.

13

cancelemos la edición1.

Podemos hacer lo mismo para la fecha, que no debería ser mayor que la actual. Queda como ejercicio.

Ejercicio 3Realizar la validación de la fecha de la factura. Se debe impedir que la fecha sea mayor que laactual; en caso que sea menor solamente mostrar una advertencia.

?

Las validaciones a nivel de campo pueden ser codificadas también en las propiedades de los componentes decampo, como restricciones (constraints). No las usaremos en el presente ejemplo.

También podemos validar la entrada caracter a caracter; por ejemplo, en el campo de fecha no deberíamospermitir al usuario ingresar letras ni símbolos; igualmente en el número de factura.

Para las validaciones caracter a caracter podríamos escribir un procedimiento que compruebe cada pulsaciónde tecla, por ejemplo en respuesta al evento OnKeyPress. Por suerte, Delphi tiene ya incorporado unmecanismo de validación así: las máscaras de entrada de los componentes de campo.

Los componentes de campo tienen una propiedad que permite especificar el formato genérico de la entrada;por ejemplo, que serán 6 números o 2 números, una barra, otros 2 números, otra barra, cuatro números.Desgraciadamente, esta propiedad no se llama igual ni utiliza los mismos códigos en todos los componentes.Para los campos numéricos se denomina EditFormat, mientras que para los campos de fecha y decaracteres se llama EditMask. Los códigos para cada tipo se pueden ver en la ayuda en línea.

Para el campo de fecha de nuestra factura, requerimos al usuario que ingrese dos números seguidos de unabarra seguidos de otros dos números seguidos de otra barra seguidos de cuatro números. Notemos que todoslos caracteres son obligatorios, por lo que no se permitirá una entrada del tipo ' 1/1/00'. Sería posiblepermitirla, pero realmente no vale la pena. No creo que ningún usuario llame furioso a su casa a las 8 de lamañana para quejarse que tiene que ingresar cuatro números más...

La máscara sería 00/00/0000. Si consultamos la ayuda en línea, vemos que el ' 0' indica que en ese lugar seespera un número, obligatoriamente. Si no se ingresa, Delphi nos reclamará amablemente que completemosla entrada o nos retiremos honrosamente presionando Escape. ¿Y las barras? Las barras quedan comoestán, el cursor les pasa por encima como si no existieran y el usuario puede hacer lo mismo. En definitiva,simplemente tiene que escribir los ocho números en secuencia y nada más. Dígale eso si llama a lamadrugada.

Ejercicio 4Coloque una máscara de edición al campo NroFactura, que permita 8 o menos números.

?

Page 14: 61597879 Sistema de Facturacion

14

NOTA: en la unit BDE se define una constante llamada abort -si, el mismo nombre que la función de launidad SysUtils. Por consiguiente, para que el compilador pueda diferenciarlas, debemos calificar lallamada a la función: en lugar de escribir simplemente abort escribimos sysutils.abort .

Por último, nos queda una validación antes de dar por buenos los datos de la factura; la combinacióntipo/número de factura debe ser única en toda la tabla. Esta comprobación ya la hace la Base de Datosporque estos campos forman la Clave Primaria y uno de los requisitos de las claves primarias es justamentela unicidad. Podemos optar por dos caminos: enviar los datos a la tabla y si se produce el error de Violaciónde Clave (Key Violation) mostrar un mensaje al usuario, o bien buscar antes de intentar ingresar los datos siel valor ya existe.

Usaremos aquí la primera aproximación. Las tablas tienen un evento llamado OnPostError que se producecuando hay un error al intentar meter los datos a la tabla (post). En este evento Delphi nos brinda toda lainformación necesaria en los parámetros, entre ellos el objeto de la excepción que se produce comoconsecuencia del error de la Base de Datos. Utilizando este objeto podemos reaccionar en forma sencilla alerror:

procedure TDM1.tabFacturasPostError(DataSet: TDataSet; E: EDatabaseError; var Action: TDataAction);begin if e is EDBEngineError then if EDBEngineError(e).Errors[0].ErrorCode = DBIERR_KEYVIOL then DatabaseError('El número de factura ya existe');end;

Como podemos ver, el tratamiento de errores de la Base de Datos no es tan sencillo. En particular, si el errorviene a través de la BDE se produce una excepción EDBEngineError, descendiente de EdatabaseError.

La clase EdatabaseError no contiene otra información sobre el error que no sea el mensaje; y este mensajepuede cambiar si por ejemplo cambiamos el idioma, así que no nos sirve. Por suerte la BDE sí presenta uncódigo de error diferente para cada error en la excepción EDBEngineError, descendiente deEdatabaseError.

Al producirse un error en una base de datos, tenemos varios códigos: uno por el motor de bases de datos,posiblemente uno por el driver, uno de la BDE... depende del motor que utilicemos. Cada uno de estoserrores consta de varias partes -código nativo,código de la BDE, subcódigo, texto, categoría- y sonmodelados en Delphi con la clase TDBError. La excepción EDBEngineError mantiene una lista de estosobjetos llamada Errors, y un contador interno de la cantidad de entradas en la lista en la propiedadErrorCount.

El proceso del código anterior comprueba que el primer error de la lista sea el correspondiente a la Violaciónde Clave. La constante DBIERR_KEYVIOL está definida en la unit BDE, que tendremos que agregar a lacláusula uses del DataModule.

Posteriormente tal vez necesitemos otras validaciones para la tabla de detalle, pero las veremos en sumomento.

Pasemos ahora a ver la generación de valores por defecto para los campos en los que se pueda hacer.

Valores por defecto

Page 15: 61597879 Sistema de Facturacion

15

Es muy práctico (y reduce grandemente los errores de entrada) asignar valores a los campos antes de que elusuario pueda meter la mano; estos valores se podrán cambiar, pero trataremos de elegir los datos que seingresan la mayoría de las veces en un uso normal. Toda vez que el valor a introducir ya esté en el campo,el usuario solamente tendrá que pasarlo por alto.

Existe un evento especial en los Datasets de Delphi que permite la asignación de valores por defecto a loscampos de un registro: el evento OnNewRecord.

Este evento se produce cuando recién se genera la copia en memoria del registro, para comenzar a introducirvalores. Técnicamente se produce después de entrar la tabla en estado dsInsert y por lo tanto después delevento BeforeInsert , pero antes del evento AfterInsert . La ventaja de esta secuencia es que los valorescolocados en el evento OnNewRecord no marcan el registro como modificado, de manera que se puedecancelar la inserción con sólo moverse a otro registro.

En nuestra factura pondremos tres valores por defecto: el número de factura (que extraemos de un archivoINI), el tipo de la factura (también del INI, para que pueda configurarse) y la fecha actual:

procedure TDM1.tabFacturasNewRecord(DataSet: TDataSet);begin tabFacturas['NroFactura']:= ArchIni.readInteger(secFactura,idNroFactura,defNroFactura); tabFacturas['Tipo']:= ArchIni.ReadString(secFactura,idTipoFactura,defTipoFactura); tabFacturas['Fecha']:= date;end;

En el código anterior hemos usado otra forma de acceder a los datos, mediante la propiedad FieldValues dela tabla. Esta propiedad es un array de Variants, uno por cada campo, que se acceden a través del nombredel campo. No es necesario especificar FieldValues después del nombre de la tabla porque es la propiedadpor defecto de esta última. Este método puede ser un poco más lento que usar las propiedades .AsXXX delos componentes de campo, pero es más simple de escribir y de entender. Cuando no se procesan grandescantidades de datos la demora es imperceptible.

En las líneas anteriores hay unas cuantas variables que no hemos definido. Hagámoslo ahora, antes que nosolvidemos.

Definición de una unit para las declaraciones globales

En casi todos los programas que tengan más de dos units necesitaremos algunas constantes y posiblementevariables de alcance global, es decir accesibles desde todas las units del proyecto. Es una práctica comúndefinir una nueva unit que contendrá únicamente las declaraciones de todas estas variables y constantes; deesta manera, para tener acceso a las mismas solamente hay que agregar una referencia a esta unit en lacláusula uses del archivo desde donde queremos utilizarla.

En nuestro ejemplo, la unit se llama uGlobales.pas y contiene las declaraciones de las constantes asociadascon el archivo INI de configuración, así como la variable que representa a la instancia del archivo mismo. Acontinuación va el listado completo de esta unit:

unit uGlobales;

interface

uses IniFiles;

Page 16: 61597879 Sistema de Facturacion

16

const ArchLogo = 'logo.emf'; NombreIni = 'Factura2.ini';

//Identificadores para el archivo INI secFactura = 'Facturacion'; idNroFactura = 'Nro'; defNroFactura = 1; idTipoFactura = 'Tipo'; defTipoFactura = 'B';

var SeIngresoUnaFactura: boolean; ArchIni: TIniFile;

implementation

Initialization ArchIni:= TIniFile.Create(NombreIni);

Finalization ArchIni.Free;

end.

Bueno, hay aquí algunas cositas que explicar ¿no?. Existe una variable declarada que no hemos visto hastaahora: ‘SeIngresoUnafactura’. El nombre es bastante sugerente: simplemente indicará si se ha ingresadoya una factura o no. La veremos en la siguiente sección.

En esta unit global, además de declaraciones tenemos dos secciones que son poco vistas en los programas: lasección de inicialización y la de finalización de una unit.

La sección de inicialización de una unit -se puede poner en cualquiera- se distingue por la palabra reservadaInitialization y se ejecuta apenas se crea la unit: al principio mismo del programa, antes de cualquier eventoOnCreate. Es el momento ideal para dar valores a variables globales, como nuestra instancia de TiniFile quetrabajará con el archivo de configuración.

De la misma manera, la sección Finalization se ejecuta al final de la aplicación, después de destruir todaslas ventanas. Aquí es cuando cerramos el archivo INI y liberamos los recursos ocupados por la instancia deTiniFile.

Aceptar o cancelar, esa es la cuestión

Cuando se acepta la factura, no nos queda más que cerrar la ventana ¿verdad? No. Falso. Debemoscomprobar antes de cerrar que las tablas estén en buen estado; por ejemplo, el último registro del detalle noestará totalmente ingresado (a menos que nos hayamos movido a otra línea) dado que no hemos hecho elPost. Lo mismo puede suceder con la tabla de facturas, ya que el usuario tiene en sus manos el armamortífera. Por lo tanto, debemos asegurarnos antes de cerrar la ventana que todos los registros soningresados correctamente:

//Botón Aceptarprocedure TFAltaFactura.BitBtn1Click(Sender: TObject);begin if dm1.TabFacturas.State in dsEditModes then dm1.tabFacturas.Post;

Page 17: 61597879 Sistema de Facturacion

17

if dm1.tabDetalle.state in dsEditModes then dm1.tabDetalle.Post; //Guarda el siguiente nro de factura en el archivo INI ArchIni.WriteInteger(secFactura,idNroFactura,dm1.tabFacturasNroFactura.AsInteger+1); //Unicamente si llega hasta aca, cierro la ventana ModalResult:= mrOk;end;

Como vemos en el código, después de aceptar las eventuales modificaciones que pueda haber en las tablasactualizamos el archivo INI con el nuevo número de factura. Únicamente si todos estos pasos se terminancompletamente, se cierra la ventana colocando el valor mrOk en la propiedad ModalResult del cuadro dediálogo para indicar al programa principal que se aceptó la factura.

Esta es la parte fácil.

Ahora ¿qué sucede cuando el usuario, después de ingresar los datos de la factura y varias líneas de detalle,decide cancelar el ingreso?

Resulta que tenemos ya en la Base de Datos varios registros; debemos borrarlos. Bueno, pero no es grancosa, dirán Uds: borramos el registro de factura y los de detalles que pertenezcan a esa factura, y ya.

Están en lo cierto, pero hay que tener cuidado con el orden de las operaciones. Estamos ante el típico casoen que debemos preservar la Integridad Referencial de los datos: no pueden quedar registros de detalle sin elcorrespondiente registro de factura. Este control se puede hacer en la Base de Datos (muy recomendado).

Por lo tanto, llegamos a la conclusión que hay que borrar primero los registros del detalle y después el de lafactura para preservar la Integridad Relacional. Esta acción se denomina borrado en cascada, ya que alborrar un registro de la tabla maestra se eliminan todos los correspondientes de la tabla detalle.

Queremos que este comportamiento sea siempre el mismo entre las tablas de facturas y de detalle; al borrarun registro de Facturas -ya sea por cancelar un alta o en alguna otra pantalla que nos permita hacerlo- sedeben eliminar en cascada todos los registros de la tabla Detalle. El lugar adecuado para codificar es elevento BeforeDelete de la tabla de Facturas:

procedure TDM1.tabFacturasBeforeDelete(DataSet: TDataSet);begin//Borrado en cascada while tabDetalle.RecordCount>0 do tabDetalle.Delete;end;

Pero, pero... ¿esto no borra todos los registros de la tabla Detalle? No, pequeño saltamontes: recuerda que latabla Detalle está enlazada en una relación Maestro/Detalle a la tabla Facturas, por lo que se encuentrafiltrada. Los únicos registros visibles son los que corresponden a la factura actual, y eso es lo que debemosborrar.

Entonces, el código en el botón de Cancelar quedaría como sigue:

//Boton Cancelarprocedure TFAltaFactura.BitBtn2Click(Sender: TObject);begin if dm1.tabDetalle.state in dsEditModes then dm1.tabDetalle.Cancel; if dm1.TabFacturas.State in dsEditModes then dm1.tabFacturas.Cancel;

Page 18: 61597879 Sistema de Facturacion

18

dm1.tabFacturas.delete;end;

Pero todavía queda un caso patológico: cuando no se ha ingresado nada todavía y se cancela nada másentrar a la ventana, si borramos la factura actual estaremos borrando la que sea que tenga la desgracia deque el cursor de la tabla Facturas esté sobre ella.

Este problema se puede solucionar de varias formas: para citar sólo dos que se vienen a la mente enseguida,podemos

? Agregar siempre un registro a la tabla de facturas; apenas entramos, hacemos Post y luego Edit sobreesta tabla.

? Usar una variable que nos indique cuando se ha ingresado realmente un registro en la tabla de facturas.

Hemos utilizado aquí el segundo método. En el momento en que el registro de Facturas ya está seguro en latabla ponemos una variable global de tipo lógico -la variable SeIngresoUnaFactura que vimos declarada enla unit global- y solamente borramos el registro si esta variable tiene valor verdadero. Este tipo de variablesque indican que se ha alcanzado cierto estado en el procesamiento se denominan Banderas. Es un típicométodo de los programas pre-objetos; lo usamos aquí como muestra de la posibilidad de mezcla de técnicasque brinda el Object Pascal.

La bandera toma valor verdadero en el evento AfterPost de la tabla de facturas -que está en elDataModule- y se comprueba en el evento anterior, al momento de presionar Cancelar en la ventana de Altade Facturas. Por este motivo fue necesario declarar la variable como global.

El evento AfterPost de la tabla de facturas luce así:

procedure TDM1.tabFacturasAfterPost(DataSet: TDataSet);begin SeIngresoUnaFactura:= true;end;

y el botón Cancelar de la ventana de Alta de Facturas ejecuta el siguiente código corregido:

//Boton Cancelarprocedure TFAltaFactura.BitBtn2Click(Sender: TObject);begin if dm1.tabDetalle.state in dsEditModes then dm1.tabDetalle.Cancel; if dm1.TabFacturas.State in dsEditModes then dm1.tabFacturas.Cancel; if SeIngresoUnafactura then dm1.tabFacturas.delete;end;

Ya casi terminamos con la tabla de Facturas; como último detalle, pondremos un control DBComboBox parael ingreso de la forma de pago. En este campo se puede almacenar prácticamente cualquier cosa, peroofreceremos al usuario algunas ya prefedinidas: por ejemplo ‘Contado’, ‘Adelanto y 30 días’, ‘Tarjeta decrédito’. Estas cadenas predefinidas se colocan en la propiedad Items del ComboBox, como si fuera unocomún. La diferencia estriba en que el texto que se encuentre en el Combo -ya sea seleccionado de la lista oescrito a mano- irá a parar a la tabla de Facturas al campo FormaDePago (indicado por las propiedadesDataSource y DataField). Queda como ejercicio completar esta parte.

Page 19: 61597879 Sistema de Facturacion

19

Figura 12: creación del campo de búsqueda de Código

Ejercicio 5Colocar un DBComboBox para entrar la forma de pago, con las tres opciones comentadas másarriba.

?

Ahora sí, ya podemos pasar a discutir las necesidades de la parte de Detalles.

Detalle de la factura: campos virtuales

La información del detalle de las facturas se extrae mayormente de la tabla de Productos. Lo primero queharemos es determinar cómo se accederá a esa información desde el punto de vista del usuario.

El usuario debe ingresar tres datos por cada fila del detalle: la cantidad, el producto -ya sea mediante elcódigo o el nombre- y el precio unitario.

? La cantidad es un número que se debe poder ingresar libremente; dejaremos pues la columna como está.

? Para indicar el producto sería bueno poder elegir de una lista que muestre todos los registros de la tablaProductos. Más todavía, pediremos que se pueda seleccionar un item por nombre o por código a eleccióndel usuario.

? El precio unitario debe poder ingresarse en cada caso particular; no obstante, debería mostrar como valorpor defecto el que figura en la tabla de Productos.

? Por cada línea se desea también ver un subtotal, resultado de multiplicar la cantidad por el PrecioUnitario.

Todas estas acciones se pueden llevar a cabo fácilmente en Delphi, gracias a los componentes de campo.

Para elegir los datos usaremos campos de búsqueda (lookup). Son equivalentes a los controlesDBLookupComboBox como el que utilizamos para seleccionar el Cliente en la Factura (ver más arriba), conla diferencia que lo que crearemos ahora son Componentes de campo o sea que se integran en la definiciónde la tabla. No quiere decir que estos campos existan; de hecho, no tocamos para nada la definición de latabla física. Únicamente en memoria, para el acceso normal a las Bases de Datos, tendremos definidosalgunos campos más.

Los campos de búsqueda tienen la habilidad de mostrar una lista de opciones para el valor del campo,trayendo esta lista desde otra tabla o consulta. Recordemos los pasos necesarios para crear uno de estoscomponentes:

? Traer al frente el Editor de Campos (doble click en latabla o seleccionar la opción correspondiente del menúcontextual)

? En el menú contextual del Editor de Campos,seleccionar “New Field” (Nuevo Campo). Se nos presentala ventana de definición de campos.

? Completamos los datos del nuevo campo, y aceptamoslos cambios. Se agrega un nuevo componente de campoa la tabla; para el programa, se ha creado un camponuevo como cualquier otro. En la fig. 10 Se ve ladefinición del campo que muestra el código de producto.

Page 20: 61597879 Sistema de Facturacion

20

Figura 13: definición del campo de búsqueda de productopor descripción

Figura 14: el campo de búsqueda (lookup) de productos pordescripción en acción

Figura 15: definición del campo calculado Subtotal

? Si el campo es de tipo lookup (búsqueda), lo veremos en la grilla como un ComboBox. Al desplegarlo nosdará la información de la tabla de Productos, pero el dato que seleccionemos quedará guardado en latabla de Detalle de Facturas.

Podemos definir más de un campo de tipo lookup; de hecho, en esta aplicación sería práctico tener doscampos así, uno para el código de producto y otro para la descripción. Dado que estos campos acceden a lamisma tabla de productos, se mantendrán siempre sincronizados mostrando el mismo registro (el actual de latabla de productos). En la fig. 11 se ve la definición del campo que muestra la descripción (pero almacena elcódigo).

Si probamos ahora la aplicación podemos ya seleccionar productos con las listas que se despliegan al entrar ala celda correspondiente (fig. 12). Nos falta ahora hacer que se calcule automáticamente el subtotal de cadalínea, multiplicando la cantidad por el Precio Unitario.

La columna Subtotal también es un campo virtual,sólo que en este caso no buscamos ningunainformación en otra tabla; el valor que mostraremoses resultado de un cálculo.

Para definir un campo calculado procedemos de lamisma manera que con los campos de búsqueda: enel editor de campos seleccionamos “New Field...” ycompletamos los datos. Esta vez el tipo elegido serápor supuesto “Calculated” (fig. 13).

Notemos que al seleccionar “Calculated” para el tipode campo se deshabilitan los controles de la parteinferior de la ventana.

Elvalordeeste campo resulta de un cálculo, dijimos... pero ¿adóndeponemos la expresión? La expresión no se coloca en elcampo, sino en la tabla.

El componente Ttable tiene un evento especial para dar valora todos los campos de tipo Calculado: previsiblemente, sellama OnCalcFields.

Page 21: 61597879 Sistema de Facturacion

2 Este comportamiento parece afectar también a los campos de tipo lookup, que no se actualizan en lapantalla hasta que movemos el cursor a otro registro.

21

Este evento se produce normalmente cuando:

? nos movemos de un control de datos a otro, o de una columna a otra en una grilla.

? la tabla entra en modo de edición

? se abre la tabla

? se recupera un registro desde la tabla

Como vemos, se llama a cada rato. Hay veces que este exceso de celo de la tabla por mantener actualizadoslos campos calculados es mucha carga para el programa; en esas ocasiones, podemos poner la propiedad dela tabla llamada AutoCalcFields en Falso. Entonces el evento no se producirá al modificar datos del mismoregistro, recién veremos los resultados de los cálculos cuando nos movamos a otro registro2. Para la mayoríade las aplicaciones, dejaremos la propiedad AutoCalcFields en Verdadero.

Y finalmente ¿cómo codificamos la expresión del cálculo? En simple y puro Pascal:

procedure TDM1.tabDetalleCalcFields(DataSet: TDataSet);begin tabDetalleSubtotal.AsCurrency:=

tabDetalleCantidad.AsInteger*tabDetallePrUnit.AsCurrency;end;

Como vemos, le asignamos directamente el resultado de la expresión al componente de campo.

Si prueban la aplicación ahora, podrán ingresar ya facturas con su detalle, y verán el subtotal de cada línea nibien modifiquen cualquiera de los campos.

Nos queda un agregado por hacer a la grilla; dijimos que cuando seleccionamos un producto en cualquiera delos campos de búsqueda debía colocarse como Precio Unitario por defecto el que figuraba en la tabla deProductos. Encontrar este valor no es difícil: simplemente tenemos que buscar el producto que acabamos deseleccionar -y nos encontramos con que ya está seleccionado! Dado que los campos Lookup muestran elcontenido de la tabla Productos, al seleccionar uno ya estamos posicionando el cursor de esta tabla en eseregistro. Lo que debemos determinar es donde ponemos el código que tome el valor de productos y lo pongaen Detalle.

Hay varios lugares posibles; en esta aplicación actuaremos en respuesta al cambio en el campo Codigo de latabla de Detalle.

Necesitamos un evento que se produzca cuando se cambia el contenido del campo. En Delphi representamosa los campos con los componentes de campo, por lo que es lógico que se encuentre allí. En efecto,buscamos en los componentes de campo de la tabla Detalle y en el correspondiente al campo “Codigo”tomamos el evento OnChange para escribir el siguiente código:

procedure TDM1.tabDetalleCodigoChange(Sender: TField);begin tabDetallePrUnit.AsCurrency:= tabProductosPrUnit.AsCurrency;end;

Page 22: 61597879 Sistema de Facturacion

22

Así de fácil. El valor que colocamos en el campo PrUnit (a través del componente de campotabDetallePrUnit) se verá en la columna correspondiente de la grilla inmediatamente, pero el usuario puedecambiarlo con sólo escribir encima.

Ahora sí, tenemos una factura funcional... o casi. Nos falta, claro, la actualización del stock por cadaproducto vendido.

La operatoria es simple: buscamos el producto que corresponde a cada línea del detalle; le restamos lacantidad facturada cuando agregamos una línea de detalle y le sumamos la cantidad cuando estamosborrando la línea.

Y aquí nos encontramos con otra dificultad: ¿qué hacer cuando no alcanza el stock de un producto?

Hay varias opciones: avisar al usuario y dejar todo como está, hacer la vista gorda y no avisar nada, agregarel producto a una tabla de pedidos, salir corriendo sin que nos vean... Para evitar que realmente tengamosque salir corriendo con nuestro cliente atrás blandiendo un hacha, haremos que se le presente al usuario laopción de cancelar esa línea de factura o agregar el mismo para un pedido posterior al proveedor.

Podemos controlar la existencia de un producto a punto de ser facturado en el momento antes de aceptar suingreso a la tabla: el evento BeforePost de la tabla Detalle . Para cancelar un ingreso en trámite,provocamos una excepción: la instrucción Abort fue creada justamente para eso. El código queda comosigue:

procedure TDM1.tabDetalleBeforePost(DataSet: TDataSet);var dif: integer;begin tabProductos.Locate('CodProd',tabDetalleCodigo.AsString,[]); dif:= tabProductosExistencia.AsInteger-tabDetalleCantidad.asInteger; if dif<0 then begin FNoHayStock.Label1.Caption:= Format('Se han solicitado %d %s, pero solamente hay %d enexistencia.'+ #13'Indique qué deseahacer',[tabDetalleCantidad.AsInteger,tabDetalleProducto.AsString, tabProductosExistencia.AsInteger]); if FNoHayStock.ShowModal=mrCancel then sysutils.abort else begin tabPedidos.Insert; tabPedidos['Codigo']:= tabDetalle['Codigo']; tabPedidos['Cantidad']:= -dif; tabPedidos['Factura']:= tabDetalle['Factura']; tabPedidos['Tipo']:= tabDetalle['Tipo']; tabPedidosFechaPedido.Clear; tabPedidos.Post; tabDetalleEnStock.AsInteger:= tabProductosExistencia.AsInteger; end; end else tabDetalleEnStock.AsInteger:= tabDetalleCantidad.AsInteger;end;

¡Un momento, colega! Aquí hay un montón de cosas que antes no estaban... ¿Qué es esa ‘TabPedidos’? ¿Yel campo EnStock que se adivina por el componente de campo tabDetalleEnStock?

Bueno, forman parte de los cambios que hay que implementar para lograr nuestro cometido. La tablaPedidos es una nueva tabla que almacenará los datos de productos que hay que pedir a nuestro proveedor.En una aplicación más completa habría que almacenar también a qué proveedor debemos pedir cadaproducto; por ahora simplemente los almacenamos en la tabla a la espera de la creación del pedido.

Page 23: 61597879 Sistema de Facturacion

23

Veamos la estructura que utilizaremos para la tabla Pedidos:

Vemos que los pedidos mantienen una referencia a la factura que les dio origen, a través del Nro. y tipo defactura. De esta manera podremos rastrear si hemos pedido todo lo que faltaba en una factura en cualquiermomento.

El otro cambio es el campo “EnStock” de la tabla Detalle . Este campo es necesario para llevar bien lascantidades en existencia, como mostramos con un ejemplo a continuación:

Supongamos que vendemos 3 unidades de un determinado producto, y hay 6 en existencia. Al ingresar lalínea de detalle se deben restar 3 unidades al campo “Existencia” de la tabla de Productos; hasta acá todobien. Ahora supongamos que antes de terminar de introducir la factura el usuario decide cancelarla. Comoya vimos, se borra la factura y el detalle correspondiente. Y entonces hay que actualizar nuevamente laexistencia del producto sumándole esta vez la cantidad pedida. Todo fantástico hasta acá.

Ahora bien, resulta que en lugar de 3 unidades el usuario pide 15. El sistema le presentará la opción demarcar la diferencia (15-6=9 unidades) para un pedido al proveedor. El usuario, obedientemente, acepta loscambios. ¿Cómo actualizamos el stock? Debemos restar solamente la cantidad que realmente hay en stock(que difiere de la Cantidad Facturada, es decir no se descuenta de stock lo que figura en el campo Cantidad).Lo podemos hacer en el momento de aceptar la línea, y ponemos directamente la existencia en 0. Peronuestro querido usuario decide luego cancelar la factura... y debemos sumar a la existencia lo que sacamos,no lo que figura como “Cantidad”. Es por esto que necesitamos un campo más, para guardar lo querealmente estamos vendiendo del stock.

Los cambios se hacen efectivos en los procedimientos que se ejecutan después de aceptar una línea odespués de borrarla. Veamos la implementación del evento AfterPost:

procedure TDM1.tabDetalleAfterPost(DataSet: TDataSet);var difCant: integer; difST: currency;begin //actualizamos la cantidad en stock tabProductos.Locate('CodProd',tabDetalleCodigo.AsString,[]); difCant:= tabProductosExistencia.AsInteger-tabDetalleCantidad.asInteger; //Al llegar aquí ya hemos aceptado la diferencia, actualizamos el stock tabProductos.Edit; if difCant<0 then tabProductos['Existencia']:= 0 else tabProductos['Existencia']:= difCant; tabProductos.Post; //Actualizamos el total general de la factura difST:= tabDetalleSubtotal.AsCurrency-SubtotalAnterior; if difST<>0 then begin if not (tabFacturas.State in dsEditModes) then tabFacturas.Edit; tabFacturas['Total']:= tabFacturas['Total']+difST; end;end;

Cuando borramos una línea, la situación es un poco más complicada porque necesitamos conocer la cantidadrestada de stock y el código del producto... ¡datos que acabamos de borrar! Necesitamos guardar estos datosen algún lugar mientras se produce el borrado. El lugar ideal es el evento BeforeDelete:

Page 24: 61597879 Sistema de Facturacion

24

procedure TDM1.tabDetalleBeforeDelete(DataSet: TDataSet);begin ProdABorrar:= Dataset['Codigo']; CantBorrada:= Dataset['EnStock']; SubtotalAnterior:= tabDetalle['Subtotal'];end;

Vemos que al código de actualización del total general de la factura se suman las dos líneas necesarias paraguardar los valores del código de producto y la cantidad descontada de la existencia en variables internas;después de borrado el registro (en el evento AfterDelete) actualizamos el stock sumando al productocorrespondiente la cantidad almacenada:

procedure TDM1.tabDetalleAfterDelete(DataSet: TDataSet);begin if not (tabFacturas.state in dsEditModes) then tabFacturas.Edit; tabFacturas['Total']:= tabFacturas['Total']-SubtotalAnterior; if tabProductos.Locate('CodProd',ProdABorrar,[]) then //Actualizamos el Stock begin tabProductos.Edit; tabProductos['Existencia']:= tabProductos['Existencia']+CantBorrada; tabProductos.Post; end;end;

Ahora si tenemos actualizado el stock de nuestros productos. Cuando pasemos a un entorno Cliente/Servidor,utilizando un servidor SQL que soporte triggers, veremos que esta operatoria se torna mucho más sencilla -por supuesto que podemos seguir utilizando el mismo código visto arriba, pero tenemos otra opción. Tambiénhay otra forma de hacer estas actualizaciones complejas en cualquier tipo de Base de Datos, utilizandoActualizaciones diferidas (Cached Updates). Veremos esta técnica dentro de poco, por las ventajas queaporta al momento de trabajar en red.

Por ahora terminaremos con el programa agregando la posibilidad de anulación de facturas, consultas eimpresiones.

Consulta de facturas

La consulta que proponemos aquí es muy simple: solamente poder seleccionar una factura y ver el detallecorrespondiente. Luego haremos consultas más sofisticadas incluyendo condiciones de filtro (facturas que sehicieron en tal fecha, a tal o cual cliente, que superen tal monto, etc. etc)

Una vez definido el alcance de la operación que acometemos, vemos que en Delphi podemos resolverlofácilmente: la relación master/detail entre las tablas de Facturas y Detalle nos mantiene filtrada la segunda enbase al registro seleccionado en la primera, por lo que solamente debemos mostrar las dos tablas. Usaremosdos grillas: en la primera mostraremos al usuario la tabla de Facturas, donde podrá navegar sin cambiarnada. En la segunda se mostrará la tabla de Detalle que como dijimos, ya está filtrada mostrando solamentelos registros que corresponden a la factura seleccionada. Como detalle, agregaremos la posibilidad debúsqueda de factura por número.

La pantalla de consulta queda entonces como sigue:

<<<Gráfico de la pantalla de consulta de facturas>>>

Page 25: 61597879 Sistema de Facturacion

25

Y esto es todo con respecto a la consulta. Sin embargo, vamos a pedirle a Delphi algo más: que nos muestrecon otro color las facturas que están anuladas. Esto implica tomar el control del redibujo de la grilla; peroveamos primero cómo anular una factura.

Anulación de facturas

Las facturas no se pueden borrar; una vez impresas, la única posibilidad es anularlas. Para ello hemosprevisto una opción en el menú principal y lo que es más importante, un campo en la tabla Facturas.

Este campo (Anulada) es de tipo lógico (booleano), es decir que solamente puede tomar los valores True oFalse. Un valor True indicaría que la factura está anulada.

Entonces lo único que tenemos que hacer es seleccionar la factura deseada (para lo cual sería bueno vertodo el detalle) y poner valor True en el campo Anulada. Lo hacemos en una ventana igual a la que usamospara la consulta, solamente que ahora tenemos dos botones: Anular y Activar. ¿Les trae algo a la memoria?Si se están preguntando lo que yo pienso que se están preguntando, la respuesta es sí: se puede reutilizar elcódigo de la ventana de consultas haciendo que la nueva ventana de anulación herede las características dela otra. Como ya lo hemos hecho anteriormente, se los dejo como ejercicio. Únicamente les mostraré laventana ya terminada, con los dos botones (fig. ???)

<<<Gráfico de la ventana de anulación>>>

Ejercicio 6Realice la ventana de anulación de facturas, heredándola de la ventana de consulta. Agregue losbotones de “Anular” y “Activar”

?

Los nuevos botones, como habrán adivinado, colocan los valores True y False respectivamente en el campoAnulada de la factura. El código es muy simple, y nuevamente queda como ejercicio.

Ejercicio 7Escriba el código para los botones “Anular” y “Cancelar”.

?

NOTA: estas operaciones estarán normalmente restringidas para ser ejecutadas sólo por usuarios conjerarquía suficiente (gerentes o responsables de área). Veremos luego cómo asignar permisos de ejecucióndiferentes en función del nivel del usuario.

Bueno, ya casi estamos llegando al ¿final? Nooooo... faltan muchos detalles para que esta aplicación puedaconsiderarse completa.

En primer lugar, volvamos al diseño de la Base de Datos. Siempre es conveniente validar los datos “porencima” de la aplicación, es decir, en la estructura misma de la BD. ¿Por qué? Porque si no, puede aparecerla mano negra que nadie conoce pero todos sufren alguna vez, y tocar los datos de la tabla sin usar

Page 26: 61597879 Sistema de Facturacion

26

nuestro programa. Por ejemplo: a través de nuestro programa, no se puede borrar una factura ¿cierto?pero no hay nada que impida ese borrado desde el Database Desktop, por ejemplo. Y entonces...

Trabajando con Bases de Datos locales com oParadox, podemos poner una clave a las tablas; al trabajar conservidores SQL, es el ingreso a la Base de Datos en sí lo que está restringido, y es obligatorio validar elusuario y su clave para poder acceder a los datos. En nuestro ejemplo no vamos a poner clave a las tablasmientras trabajamos en Paradox.

El caso que sí vamos a contemplar en cualquier base de datos es el de la Integridad referencial de losdatos. Por Integridad Referencial entendemos ciertas reglas que impiden que los datos queden huérfanos enuna tabla; es en cierta manera un trabajo social ;-)

El problema viene por el lado del modelo relacional: cuando tenemos dos tablas en relación maestro/detalle,habrá algún campo o conjunto de campos que definen la relación entre los registros del detalle y los de latabla principal. En otras palabras, los valores de esos campos en la tabla detalle deben corresponder a algúnregistro de la tabla principal. Si no, tenemos por ejemplo un IDCliente y no tenemos el nombre... lo cual nossirve poco y nada. A estos registros que quedan “descolgados” se les llama huérfanos.

Hay que asegurarse entonces que los datos de las tablas de detalle en una relación de esas no quedenhuérfanos. Hay dos situaciones en las cuales podemos tener problemas:

? Cuando se borra un registro de la tabla principal que tenga detalles enlazados.

? Cuando se modifican en la tabla principal los campos que forman el enlace.

Definir las reglas de Integridad Referencial es decirle a la Base de Datos qué tiene que hacer en estoscasos.

Tomemos el ejemplo de las facturas de nuestra aplicación: hay tres casos en los que necesitaremos definirreglas de Integridad Referencial

? Entre la tabla de Facturas (detalle) y la de Clientes (principal)

? Entre la tabla de Detalle (detalle) y la de Facturas (principal)

? Entre la tabla de Detalle (detalle) y la de Productos (principal)

Notemos que una misma tabla puede tomar los dos roles en una misma aplicación (Facturas).

Definamos entonces las reglas de Integridad Referencial entre las tablas de Facturas y Clientes:

? Modificación: permitiremos la modificación de los datos de los clientes, incluso de los campos que formanel enlace con la factura; pero si se modifican estos últimos campos, tendremos que modificar en formaacorde los datos de la factura. Por ejemplo: enlazamos por un campo “ID” y tenemos una factura quecorresponde a un cliente de ID = 5. A continuación el usuario modifica el valor del campo ID en la tablade clientes y coloca un valor 6; entonces tendremos que modificar el campo de la factura para que diga“6”. Esta operación se denomina “actualización en cascada”, y la realiza automáticamente la Base deDatos.

? Borrado: no permitiremos el borrado de un cliente si éste figura en alguna factura.

Recordemos que las reglas de Integridad Referencial se definen en la tabla de Detalle, referenciando a laprincipal. Para definir las reglas de la tabla de Facturas, entramos a la utilidad de restructuración de tablasdel Database Desktop y seleccionamos en el ComboBox de las propiedades la opción “Referential Integrity”(fig. ???)

<<<Imagen de la restructuración de la tabla Facturas, seleccionando la opción “REf. Integrity”>>>

En la pantalla que aparece se nos piden los campos que referenciarán a la tabla externa (clave externa), y el

Page 27: 61597879 Sistema de Facturacion

27

nombre de la tabla externa.

NOTA: por algún error en la implementación del Database Desktop, únicamente se muestran como tablaspasibles de ser referenciadas las que están en el directorio especificado por el alias :WORK:, que seselecciona en el menú File|Working Directory.

Para nuestro ejemplo de la tabla de Facturas referenciando a la tabla de clientes, podemos seleccionar elalias en uso como Working Directory. Una vez seleccionados los campos, la información de IntegridadReferencial queda como se ve en la figura ????

<<<Gráfico de la pantalla de Integridad Referencial mostrando las selecciones para el enlace entre Facturasy Clientes>>>

Cuando aceptamos la información ingresada, debemos dar un nombre a la Regla de Integridad; con estenombre se guardará la regla en el archivo .VAL.

Puntos para tener en cuenta:

? ?Aceptar todo: si hay registros pendientes, aceptar todo. Aquí tendría que actualizar el archivo INI conel nuevo número de factura, porque ya se aceptó y no hay forma de borrarla.

? ?Cancelar: borrar la factura con todo su detalle

? ?Detalle: campos lookup enlazados (codigo y descripcion)

? ?Detalle: precio unitario debe dejar escribir otro numero pero proponer el del producto (Modificar en elOnChange de Producto)

? ?Detalle: Subtotal calculado

? ?Factura: Total autoactualizable

? ? tabFacturas: valores por defecto. En especial, poner el número de factura en un archivo .INI o en unatabla de secuencias

? ?TabFacturas: la condición de pago se puede ingresar con un componente DBComboBox o unDBRadioGroup

? ?Aclarar bien el tema de Cancelación de facturas... la necesidad de una variable bandera global, o unaedición falsa.

? ?Qué pasa cuando no alcanza el stock

? ?Validación del tipo de factura, la fecha, etc. OK

? ?Error “Key violation” si ponen un Nro que ya esté OK

? ?Considerar en el INI los números de factura para los distintos tipos

? Integridad Referencial entre las tablas

?Consulta de facturas

Una ficha simple con dos DBGrid y un navegador, solamente para ver las facturas y sus detalles

?Anulación de facturas

Page 28: 61597879 Sistema de Facturacion

28

Igual que la de consulta, pero con un botón para anular. Se puede heredar de la anterior...

Toques finales

? Logotipo de la empresa en la pantalla principal y en la pantalla de carga

? Creación de una unit para las declaraciones globales

? Pantalla de Splash, con indicador de progreso de la carga

? Barra de herramientas, con lista de acciones para compartir con el menú. Se puede agregar al final