Вы находитесь на странице: 1из 28

Sistema de facturacin y control de Stock

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

Como ejemplo de aplicacin de las tcnicas de Bases de Datos y realizacin de programas, haremos un programa para llevar un control de Inventario (Stock) incluyendo la facturacin de los productos. Este ejemplo no pretende ser una implementacin de nivel comercial; simplemente demuestra tcnicas y herramientas de uso comn. Por lo tanto restringiremos nuestra atencin a un hipottico comercio -La Luz Mala S.A., Artculos de Iluminacin- y solamente trabajaremos con los datos de productos y clientes, que son necesarios para la facturacin. Algunas partes quedarn abiertas para que el lector las termine, de manera de tornar el ejemplo en una especie de taller prctico.

Requerimientos En lo que sigue, considerar que el lector posee cierto manejo de Delphi, en especial supondr que sabe cmo crear una tabla y conectarla desde la aplicacin. Tambin asumo que las nociones bsicas de diseo de Bases de Datos (Diagramas Entidad Relacin, tipos y cardinalidad de relaciones entre tablas, etc) son conocidas. Cualquier libro o curso bsico de diseo de Bases de Datos relacionales trata estos temas.

Desarrollaremos primero la aplicacin completa usando tablas de Paradox, para poner el nfasis en temas como validacin, trabajo con tablas dependientes, y otros temas que rara vez se encuentran aplicados a un problema concreto, aunque en la realidad aparecen en la mayora de los casos. Una vez que tengamos la aplicacin 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: utilizacin directa de aplicaciones a travs de Automatizacin OLE, poner procesos en hilos de ejecucin separados, etc. Este ejemplo est planeado para llenar un agujero en los cursos que normalmente se encuentran sobre programacin: la aplicacin de distintas tcnicas a una aplicacin 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 tambin 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 mo.

1) Diseo de la BD

Despus de noches de vigilia pensando en la mejor manera de almacenar los datos de este ejemplo, hemos llegado al siguiente diagrama entidad/relacin:

Vemos en el diagrama que tenemos cuatro entidades, relacionadas entre si. Este es el diagrama lgico, independiente del motor de Base de Datos que utilicemos. El motor a utilizar determinar los tipos de datos, aunque ya podemos (y debemos) definir cul campo ser numrico, cul de texto, etc. En el diagrama se ven los tipos que hemos seleccionado entre los disponibles para el diagrama lgico en el programa E/R Studio de Embarcadero SA. La eleccin del motor de Bases de Datos a utilizar no es trivial; de hecho, es una de las primeras decisiones importantes que tendremos que tomar. Todos tienen sus pros y sus contras; debemos hallar un punto medio entre la facilidad de implementacin, las posibilidades que nos brindan, la seguridad, el costo... Por suerte Delphi y la BDE nos permiten (hasta cierto punto) pasar de un formato a otro con un mnimo de inconvenientes. Como primera eleccin nos inclinaremos por el motor de Bases de Datos de Paradox, porque viene incluido con Delphi y por lo tanto es el ms simple de usar. Ms tarde

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

2) Diseo de la interface La aplicacin consta de una ventana principal desde la que se accede a las distintas opciones a travs de un men:

Archivo Salir Datos ABM Clientes... ABM Productos... Facturacin Alta... Anulacin... Consultas... Ayuda Acerca de...

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

Figura 1: la ventana principal

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

? Tienen dos paneles: uno con una lista de los datos ms tiles y otra con los datos detallados del registro
activo

Figura 2: estructura de la BD propuesta

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:

Figura 3: ficha base de las ABM

Las tablas sern colocadas en un DataModule, que llamaremos DM1. No lo incluimos en el USES de esta unit ya que no lo usamos todava, lo pondremos en las fichas descendientes.

Notaremos que hay dos botones de impresin. El de la izquierda generar un listado completo de la tabla que vemos 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 cdigo asociado, para aceptar o rechazar los cambios (notemos la referencia indirecta a la tabla, a travs de la grilla):

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

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 cdigo es el mismo para todas las ventanas de Altas, Bajas y Modificaciones y por eso se puede colocar en la ventana madre. Ms tarde agregaremos otras operaciones que tambin son generales. Para poder utilizar esta ficha como base para las otras ventanas, debemos primero guardar el modelo en el Almacn de Objetos (Repository) de Delphi. Teniendo la ficha visible, presionamos el botn derecho del ratn sobre la misma y seleccionamos Add to Repository... como indica la figura 3 A continuacin seleccionamos la pgina del almacn de objetos donde queremos que aparezca nuestra Figura 4: agregar la ficha al almacn de objetos ficha, le damos un nombre y una descripcin, y eventualmente seleccionamos un icono para representarlo (fig. 4). La ficha y su unidad asociada son ahora las fuentes de uno de los objetos del almacn. Notemos que si movemos o renombramos estos archivos no podremos utilizarlos desde el almacn, aunque la referencia siga estando all.

Ahora crearemos las ventanas de datos descendientes, una enlazada con la tabla de clientes y la otra con la tabla de productos.

Figura 5: propiedades de la ficha para agregar al almacn

Para ello, seleccionamos del men File la opcin New... y en el Almacn miramos en la pgina de nuestra aplicacin (en mi caso la llam Factura2). Seleccionamos la ventana maestra (en mi caso, llamada fABMMaster), comprobamos que est seleccionada la opcin inherit (heredar) y damos al OK (fig. 5). Logramos una copia idntica de la ventana principal de ABM, con todas sus caractersticas. La ventaja es que hemos heredado todas sus propiedades y mtodos (como los procedimientos de los botones Aceptar y Cancelar) pero podemos cambiar cualquier cosa. Llamemos a esta ficha fABMClientes y la grabamos con el nombre uABMClientes; podemos tambin poner un ttulo (Caption) ms indicativo que el que pone Delphi por defecto (por ejemplo, Datos de clientes). Realizamos la misma operacin para la ventana de ABM de Productos (fABMProductos).
Figura 6: crear una nueva ficha heredando las caractersticas de la ficha ABMMaster

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

necesitamos colocar las tablas en el proyecto.

Creamos entonces el Mdulo de Datos y ponemos las tablas y Fuentes de Datos necesarias. Adems, creamos los componentes de campo para cada tabla y agregamos los enlaces:

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

? Entre las tablas de Facturas y Clientes hay una relacin de Lookup: creamos un campo lookup que tome
el campo Cliente de Facturas y lo relacione con el campo IDCliente de Clientes, mostrando el campo NombreYApellido.

? Entre las tablas de Detalle y Productos tambin hay una relacin de Lookup: creamos un campo lookup
que tome el campo Producto de Detalle y lo relacione con el campo CodProd de Productos, mostrando el campo Descripcion.

Nos concentraremos ahora en las propiedades que hay que cambiar en la ficha de ABM de Clientes para diferenciarla de su madre. Primero, conectamos el navegador y la grilla con la fuente de datos de clientes que tenemos en el mdulo de datos (recordemos incluir la unit del mdulo de datos en la clusula USES). A continuacin ponemos controles de datos en el panel de la derecha para cada uno de los campos que podemos editar (podemos arrastrar y soltar los campos desde el editor de campos de la tabla de clientes):

Figura 7: ventana de ABM de clientes

He resaltado los ttulos (son simples etiquetas) poniendo el texto en Negrita. El campo IDCliente no es editable (es autonumrico), de manera que he utilizado un control DBText en lugar de un DBEdit. Adems, he colocado slo dos columnas en la grilla: NombreYApellido (con otro ttulo) y Telefonos. El botn de Propiedades por ahora no hace ms que poner el cursor (el foco de atencin del teclado) en el primero de los editores de la derecha:

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

Notemos la palabra inherited que escribi Delphi al principio del procedimiento. Esto significa que se llamar al procedimiento del mismo nombre definido en la ficha de la cual desciende la que estamos trabajando. En este caso no hay un procedimiento tal en la ficha FABMMaster, pero si alguna vez lo agregamos ser llamado automticamente. Podemos borrar esta lnea, o cambiarla de lugar dentro del procedimiento. Ya podemos probar la ventana, si agregamos en la ventana principal las instrucciones necesarias para que se muestre en respuesta a la opcin ABM Clientes del men Datos. Por ejemplo, podra ser algo como lo siguiente:

begin FABMClientes.Show; end;

Estoy suponiendo que permitimos a Delphi que cree automticamente la ventana al comenzar la aplicacin (Opciones del proyecto). En esta ventana ya podemos ver y modificar clientes ya existentes; sigamos expandiendo la funcionalidad escribiendo los manejadores de los otros botones. Podemos aprovechar la relacin de herencia que existe entre la ficha que guardamos en el almacn y nuestras fichas de ABM Clientes y ABM Productos; si no utilizamos referencias directas a una ficha particular en los procedimientos de respuesta de los botones, entonces podemos escribir el cdigo en la ficha madre y automticamente estar disponible en las dos fichas descendientes -y en cualquier otra que inventemos despus.

El botn de imprimir del panel de la izquierda debe hacer un listado simple de los datos de toda la tabla que se muestra en la grilla. Pues bien, hay una forma muy prctica de generar este listado: la funcin QRCreateList que nos brinda QuickReport en la unit QRExtra. Utilizando esta funcin y con un poco de cuidado, podemos inclusive poner este cdigo en el botn de la ventana maestra. De esta manera ser heredado por las ventanas descendientes. El cdigo sera 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;

Debemos incluir en la clusula USES un par de units: QRExtra y QuickRpt, donde estn definidas la funcin QRCreateList y la clase TcustomQuickRep respectivamente. Notemos que pasamos aqu las columnas de la grilla como columnas del reporte; es decir, el reporte impreso ser muy parecido a lo que se ve en la grilla. La generacin de una lista de la tabla completa es simple y se puede hacer en forma genrica como antes. Pero la impresin de la ficha con los datos de un registro es distinta para los clientes y para los productos, por lo que tendremos que hacerla a mano, generando un reporte para cada una que ser llamado en los botones de Ficha de cada ventana descendiente. Lo veremos luego. Primero hagamos la parte de bsqueda.

La bsqueda tambin es particular para cada descendiente, as que tendremos que codificarla por separado. Permitiremos la bsqueda por varios campos. Para la tabla de clientes podramos mostrar una ficha de datos como la siguiente:

Figura 8: ventana de bsqueda de Clientes

Al presionar el botn Buscar realizamos la bsqueda utilizando Locate para independizarnos de los ndices. Si encontramos una coincidencia cerramos la ventana de bsqueda poniendo un valor mrOK en la propiedad ModalResult de la ventana; el cursor se habr movido automticamente en la tabla, de manera que estaremos posicionados correctamente sobre el registro buscado. En caso que el texto buscado no se encuentre, mostramos un mensaje al usuario y no cerramos la ventana, para permitirle que siga buscando. El botn Cancelar cierra la ventana: tiene puesta la propiedad ModalResult en mrCancel de manera que este valor va a parar a la propiedad del mismo nombre de la ventana cuando se lo presiona. No hace falta ningn cdigo. El cdigo del botn 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');

end;

Y ya tenemos casi lista nuestra ventana de Altas Bajas y Modificaciones de Clientes: slo falta la impresin de la ficha. Vamos a ello. Para la impresin de los datos del cliente, creamos un QuickReport como el que se muestra en la fig. 8.

Figura 9: impresin de los datos de un cliente

Entonces el cdigo en el procedimiento de respuesta al botn Ficha podra ser algo como lo siguiente:
procedure TfABMClientes.BitBtn5Click(Sender: TObject); begin inherited; FFichaCliente.Preview; end;

Como dijimos antes, Delphi agrega automticamente la primera lnea para llamar al cdigo heredado antes de hacer nada ms. En nuestro caso no tenemos nada de cdigo en la ventana madre para este botn, as que podramos borrarla. No obstante la dejaremos porque si el da de maana agregamos algo en la ventana madre se ejecutar automticamente.

Hay un fallo en la lgica del cdigo anterior; cuando presionemos el botn de imprimir los datos del cliente que estamos viendo, se nos mostrar el reporte... con los datos de todos los clientes. Algo as como el listado que generamos antes, pero ms lindo. Este comportamiento no es el que deseamos; este botn debera imprimir solamente los datos del cliente actualmente seleccionado. Para eso debemos filtrar de alguna manera la tabla. Existen varios mtodos de filtrado que podemos usar. En este ejemplo usaremos la propiedad filter. El cdigo para el evento OnClick sobre el botn de impresin de datos personales queda ahora como el siguiente:
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;

end;

Notemos que despus 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? Debera pedirnos confirmacin. Podemos mostrar una caja de dilogo cuando presionamos el botn de borrar, pero as quedamos expuestos a que en una modificacin posterior agreguemos otra forma de borrar los registros -por ejemplo, en una grilla de consulta- y el control no se haga. El lugar ms conveniente para pedir la confirmacin es el evento BeforeDelete de la misma tabla, que se disparar 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 ningn momento necesitamos nombrar 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 la tabla de Productos el mismo procedimiento.

Hay otra consideracin que hacer con respecto al borrado en la tabla de facturas y su relacin con la de detalle, pero lo postergaremos hasta que veamos la facturacin.

Ahora s tenemos completa la ventana de Altas, Bajas y Modificaciones de clientes. Lo mismo hay que hacer para los productos, pero... lo harn Uds.

jercicio 2
Crear la ventana de ABM de productos heredando de ABMMaster, completa con todos los botones funcionando.

Facturacin
La parte de facturacin es la ms complicada de esta aplicacin, porque enlaza y utiliza todas las tablas a la vez. Veamos primero la ventana terminada:

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 controles para modificar los campos de la tabla de Facturas, y en la parte inferior tenemos una grilla que muestra y trabaja con los datos de la tabla Detalle (fig. 9). Estas dos tablas estn relacionadas en forma Maestro/Detalle, de manera que automticamente la tabla de Detalle se filtra para mostrar slo los registros que correspondan a la factura que se ve arriba. Delphi incluso toma en consideracin la relacin cuando agregamos registros a la tabla de detalle, poniendo automticamente los valores que corresponden en los campos 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 automticamente a los campos Nro de Factura y Tipo de Factura. El campo IDItem no es editable porque es de tipo autonumrico, y nos quedan solamente la cantidad, el cdigo del producto y el precio unitario. Posteriormente modificaremos la grilla para que la columna de cdigo nos deje elegir alguno de los productos de la tabla de Productos en una lista, antes que escribirlos directamente con las posibilidades de error que eso traera; adems, definiremos un nuevo campo virtual -no existente en la tabla fsica- para mostrar el subtotal, resultado de multiplicar la cantidad por el precio unitario. El precio unitario debe tomar como valor por defecto el precio indicado en la tabla de productos, pero se debe poder modificar. El primer problema que nos encontramos al trabajar con dos tablas relacionadas es que para agregar registros a la tabla de Detalle debemos tener un registro vlido seleccionado en la tabla Principal. Por consiguiente, en nuestra factura debemos asegurarnos que cada vez que el usuario va a modificar algo en la tabla de Detalle, la de Facturas no est en modo de insercin o edicin, porque podramos estar cambiando los valores de los campos de enlace. El cdigo es simple: si la tabla de Facturas est en estado de insercin o edicin, aceptamos los datos y listo. La pregunta del milln es: adnde colocamos el cdigo? Una primera idea sera en el evento OnExit del ltimo control de la parte de arriba. No obstante, si lo pensamos un poco ms vemos que un ratn en la mano de un usuario se transforma en un arma mortfera: es muy fcil modificar por ejemplo el nro. de factura y despus directamente pasar el foco a la grilla... tirando por el suelo nuestra estrategia ya que el usuario no entrara en el control donde pusimos nuestro cdigo. Debemos encontrar un evento que se produzca inequvocamente antes de modificar la tabla de detalle. La

10

nica manera de modificar los datos del Detalle en esta pantalla (y en la esta aplicacin) es la grilla de la ventana de Alta de Facturas, por lo que podramos controlar el estado de la factura al ganar el foco la grilla: el evento OnEnter de la grilla. El cdigo 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 a los campos NroFactura y Tipo (luego veremos cmo asignarles valores por defecto) y entramos a la grilla para agregar un detalle. Los campos de enlace tomarn valor solos. Sigamos trabajando sobre los controles de la tabla de Facturas.

Componentes de bsqueda (lookup)


El campo de Cliente tambin impone una condicin: dado que guardamos en la tabla de facturas solamente el ID del Cliente (de acuerdo con las reglas de normalizacin), tendramos que ingresar un nmero en este campo. 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 guardar el ID del que seleccionamos. Para lograr esto utilizamos un control DBLookupComboBox, con las siguientes propiedades:

? ? ? ? ?

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

El resultado se ve en la fig. ?. De esta manera el usuario siempre trabajar con el Nombre y Apellido del cliente, mientras internamente se maneja slo el nmero de identificacin. Luego veremos que este es tambin el caso del campo Producto de la tabla de Detalle. En general, casi siempre es conveniente trabajar con este sistema para los campos que referencian a otra tabla (claves externas).
Figura 11: seleccin de un cliente usando un DBLookupComboBox

Validaciones
Validar los datos significa comprobar que los mismos se ajustan a las restricciones que pueda haber definidas sobre ellos; por ejemplo, que no haya letras en un campo numrico. Hay tres momentos para hacer las validaciones:

? Al escribir (caracter a caracter)

11

? 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 restriccin: queremos que deje ingresar slo una letra, solamente A, B o C y en maysculas. Tratemos estos problemas uno por uno.

1) Solamente una letra Es fcil: ponemos la propiedad MaxLength del DBEdit correspondiente en 1. Claro que tambin est la restriccin de la definicin de la tabla, en la que asignamos una longitud 1 al campo Tipo, pero si podemos impedir que el usuario cometa un error, mejor. Notemos que tambin podramos haberlo hecho en la mscara de edicin del componente de campo.

2) En maysculas Tambin se puede hacer en el editor: ponemos la propiedad CharCase a ecUpperCase. Tambin podramos hacerlo en la mscara 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 comprobar tambin en el programa cliente para lograr una rpida respuesta al usuario; en lugar de esperar que el servidor de Bases de Datos nos devuelva el error, interpretarlo y mostrarlo, controlamos antes de enviarlo. Podemos hacerlo en el evento BeforePost de la tabla, en esta aplicacin simple; pero en una ms general tendramos que comprobar en este evento todos los campos que requieren verificacin y mostrar el mensaje correspondiente para cada uno. Es mejor utilizar los eventos de los componentes de datos, entre los cuales hay uno que es especficamente para realizar validaciones antes de enviar los datos a la BD: OnValidate. Colocamos entonces el siguiente cdigo en el evento OnValidate del componente del campo Tipo 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 (todava en memoria hasta que hagamos el Post). Si solamente mostramos el mensaje, los datos llegarn igualmente a la memoria intermedia del campo y podran ser rechazados por las validaciones de la Base de Datos, cuando hagamos el Post. Para que esto no suceda, debemos provocar una excepcin que corte el flujo del programa: la instruccin Abort hace justamente eso. El foco no sale del editor hasta que coloquemos un valor que pase la validacin o

12

cancelemos la edicin1. Podemos hacer lo mismo para la fecha, que no debera ser mayor que la actual. Queda como ejercicio.

jercicio 3
Realizar la validacin de la fecha de la factura. Se debe impedir que la fecha sea mayor que la actual; en caso que sea menor solamente mostrar una advertencia.

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

Tambin podemos validar la entrada caracter a caracter; por ejemplo, en el campo de fecha no deberamos permitir al usuario ingresar letras ni smbolos; igualmente en el nmero de factura. Para las validaciones caracter a caracter podramos escribir un procedimiento que compruebe cada pulsacin de tecla, por ejemplo en respuesta al evento OnKeyPress. Por suerte, Delphi tiene ya incorporado un mecanismo de validacin as: las mscaras de entrada de los componentes de campo. Los componentes de campo tienen una propiedad que permite especificar el formato genrico de la entrada; por ejemplo, que sern 6 nmeros o 2 nmeros, una barra, otros 2 nmeros, otra barra, cuatro nmeros. Desgraciadamente, esta propiedad no se llama igual ni utiliza los mismos cdigos en todos los componentes. Para los campos numricos se denomina EditFormat, mientras que para los campos de fecha y de caracteres se llama EditMask. Los cdigos para cada tipo se pueden ver en la ayuda en lnea. Para el campo de fecha de nuestra factura, requerimos al usuario que ingrese dos nmeros seguidos de una barra seguidos de otros dos nmeros seguidos de otra barra seguidos de cuatro nmeros. Notemos que todos los caracteres son obligatorios, por lo que no se permitir una entrada del tipo ' 1/1/00'. Sera posible permitirla, pero realmente no vale la pena. No creo que ningn usuario llame furioso a su casa a las 8 de la maana para quejarse que tiene que ingresar cuatro nmeros ms... La mscara sera 00/00/0000. Si consultamos la ayuda en lnea, vemos que el ' 0' indica que en ese lugar se espera un nmero, obligatoriamente. Si no se ingresa, Delphi nos reclamar amablemente que completemos la entrada o nos retiremos honrosamente presionando Escape. Y las barras? Las barras quedan como estn, 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 nmeros en secuencia y nada ms. Dgale eso si llama a la madrugada.

E
1

jercicio 4
Coloque una mscara de edicin al campo NroFactura, que permita 8 o menos nmeros.

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

13

Por ltimo, nos queda una validacin antes de dar por buenos los datos de la factura; la combinacin tipo/nmero de factura debe ser nica en toda la tabla. Esta comprobacin ya la hace la Base de Datos porque estos campos forman la Clave Primaria y uno de los requisitos de las claves primarias es justamente la unicidad. Podemos optar por dos caminos: enviar los datos a la tabla y si se produce el error de Violacin de Clave (Key Violation) mostrar un mensaje al usuario, o bien buscar antes de intentar ingresar los datos si el valor ya existe. Usaremos aqu la primera aproximacin. Las tablas tienen un evento llamado OnPostError que se produce cuando hay un error al intentar meter los datos a la tabla (post). En este evento Delphi nos brinda toda la informacin necesaria en los parmetros, entre ellos el objeto de la excepcin que se produce como consecuencia del error de la Base de Datos. Utilizando este objeto podemos reaccionar en forma sencilla al error:
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 nmero 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 error viene a travs de la BDE se produce una excepcin EDBEngineError, descendiente de EdatabaseError. La clase EdatabaseError no contiene otra informacin sobre el error que no sea el mensaje; y este mensaje puede cambiar si por ejemplo cambiamos el idioma, as que no nos sirve. Por suerte la BDE s presenta un cdigo de error diferente para cada error en la excepcin EDBEngineError, descendiente de EdatabaseError. Al producirse un error en una base de datos, tenemos varios cdigos: 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 estos errores consta de varias partes -cdigo nativo,cdigo de la BDE, subcdigo, texto, categora- y son modelados en Delphi con la clase TDBError. La excepcin EDBEngineError mantiene una lista de estos objetos llamada Errors, y un contador interno de la cantidad de entradas en la lista en la propiedad ErrorCount. El proceso del cdigo anterior comprueba que el primer error de la lista sea el correspondiente a la Violacin de Clave. La constante DBIERR_KEYVIOL est definida en la unit BDE, que tendremos que agregar a la clusula uses del DataModule.

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

Posteriormente tal vez necesitemos otras validaciones para la tabla de detalle, pero las veremos en su momento. Pasemos ahora a ver la generacin de valores por defecto para los campos en los que se pueda hacer.

Valores por defecto

14

Es muy prctico (y reduce grandemente los errores de entrada) asignar valores a los campos antes de que el usuario pueda meter la mano; estos valores se podrn cambiar, pero trataremos de elegir los datos que se ingresan la mayora 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 asignacin de valores por defecto a los campos de un registro: el evento OnNewRecord. Este evento se produce cuando recin se genera la copia en memoria del registro, para comenzar a introducir valores. Tcnicamente se produce despus de entrar la tabla en estado dsInsert y por lo tanto despus del evento BeforeInsert, pero antes del evento AfterInsert. La ventaja de esta secuencia es que los valores colocados en el evento OnNewRecord no marcan el registro como modificado, de manera que se puede cancelar la insercin con slo moverse a otro registro. En nuestra factura pondremos tres valores por defecto: el nmero de factura (que extraemos de un archivo INI), el tipo de la factura (tambin 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 cdigo anterior hemos usado otra forma de acceder a los datos, mediante la propiedad FieldValues de la tabla. Esta propiedad es un array de Variants, uno por cada campo, que se acceden a travs del nombre del campo. No es necesario especificar FieldValues despus del nombre de la tabla porque es la propiedad por defecto de esta ltima. Este mtodo puede ser un poco ms lento que usar las propiedades .AsXXX de los componentes de campo, pero es ms simple de escribir y de entender. Cuando no se procesan grandes cantidades de datos la demora es imperceptible.

En las lneas anteriores hay unas cuantas variables que no hemos definido. Hagmoslo ahora, antes que nos olvidemos.

Definicin de una unit para las declaraciones globales


En casi todos los programas que tengan ms de dos units necesitaremos algunas constantes y posiblemente variables de alcance global, es decir accesibles desde todas las units del proyecto. Es una prctica comn definir una nueva unit que contendr nicamente las declaraciones de todas estas variables y constantes; de esta manera, para tener acceso a las mismas solamente hay que agregar una referencia a esta unit en la clusula uses del archivo desde donde queremos utilizarla. En nuestro ejemplo, la unit se llama uGlobales.pas y contiene las declaraciones de las constantes asociadas con el archivo INI de configuracin, as como la variable que representa a la instancia del archivo mismo. A continuacin va el listado completo de esta unit:
unit uGlobales; interface uses IniFiles;

15

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 hasta ahora: SeIngresoUnafactura. El nombre es bastante sugerente: simplemente indicar si se ha ingresado ya una factura o no. La veremos en la siguiente seccin. En esta unit global, adems de declaraciones tenemos dos secciones que son poco vistas en los programas: la seccin de inicializacin y la de finalizacin de una unit. La seccin de inicializacin de una unit -se puede poner en cualquiera- se distingue por la palabra reservada Initialization y se ejecuta apenas se crea la unit: al principio mismo del programa, antes de cualquier evento OnCreate. Es el momento ideal para dar valores a variables globales, como nuestra instancia de TiniFile que trabajar con el archivo de configuracin. De la misma manera, la seccin Finalization se ejecuta al final de la aplicacin, despus de destruir todas las ventanas. Aqu es cuando cerramos el archivo INI y liberamos los recursos ocupados por la instancia de TiniFile.

Aceptar o cancelar, esa es la cuestin


Cuando se acepta la factura, no nos queda ms que cerrar la ventana verdad? No. Falso. Debemos comprobar antes de cerrar que las tablas estn en buen estado; por ejemplo, el ltimo registro del detalle no estar totalmente ingresado (a menos que nos hayamos movido a otra lnea) dado que no hemos hecho el Post. Lo mismo puede suceder con la tabla de facturas, ya que el usuario tiene en sus manos el arma mortfera. Por lo tanto, debemos asegurarnos antes de cerrar la ventana que todos los registros son ingresados correctamente:
//Botn Aceptar procedure TFAltaFactura.BitBtn1Click(Sender: TObject); begin if dm1.TabFacturas.State in dsEditModes then dm1.tabFacturas.Post;

16

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 cdigo, despus de aceptar las eventuales modificaciones que pueda haber en las tablas actualizamos el archivo INI con el nuevo nmero de factura. nicamente si todos estos pasos se terminan completamente, se cierra la ventana colocando el valor mrOk en la propiedad ModalResult del cuadro de dilogo para indicar al programa principal que se acept la factura. Esta es la parte fcil. Ahora qu sucede cuando el usuario, despus de ingresar los datos de la factura y varias lneas de detalle, decide cancelar el ingreso? Resulta que tenemos ya en la Base de Datos varios registros; debemos borrarlos. Bueno, pero no es gran cosa, dirn Uds: borramos el registro de factura y los de detalles que pertenezcan a esa factura, y ya. Estn en lo cierto, pero hay que tener cuidado con el orden de las operaciones. Estamos ante el tpico caso en que debemos preservar la Integridad Referencial de los datos: no pueden quedar registros de detalle sin el correspondiente registro de factura. Este control se puede hacer en la Base de Datos (muy recomendado). Por lo tanto, llegamos a la conclusin que hay que borrar primero los registros del detalle y despus el de la factura para preservar la Integridad Relacional. Esta accin se denomina borrado en cascada, ya que al borrar 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 borrar un registro de Facturas -ya sea por cancelar un alta o en alguna otra pantalla que nos permita hacerlo- se deben eliminar en cascada todos los registros de la tabla Detalle. El lugar adecuado para codificar es el evento 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, pequeo saltamontes: recuerda que la tabla Detalle est enlazada en una relacin Maestro/Detalle a la tabla Facturas, por lo que se encuentra filtrada. Los nicos registros visibles son los que corresponden a la factura actual, y eso es lo que debemos borrar.

Entonces, el cdigo en el botn de Cancelar quedara como sigue:


//Boton Cancelar procedure 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;

17

dm1.tabFacturas.delete; end;

Pero todava queda un caso patolgico: cuando no se ha ingresado nada todava y se cancela nada ms entrar a la ventana, si borramos la factura actual estaremos borrando la que sea que tenga la desgracia de que el cursor de la tabla Facturas est sobre ella. Este problema se puede solucionar de varias formas: para citar slo 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 sobre
esta tabla.

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

Hemos utilizado aqu el segundo mtodo. En el momento en que el registro de Facturas ya est seguro en la tabla ponemos una variable global de tipo lgico -la variable SeIngresoUnaFactura que vimos declarada en la unit global- y solamente borramos el registro si esta variable tiene valor verdadero. Este tipo de variables que indican que se ha alcanzado cierto estado en el procesamiento se denominan Banderas. Es un tpico mtodo de los programas pre-objetos; lo usamos aqu como muestra de la posibilidad de mezcla de tcnicas que brinda el Object Pascal. La bandera toma valor verdadero en el evento AfterPost de la tabla de facturas -que est en el DataModule- y se comprueba en el evento anterior, al momento de presionar Cancelar en la ventana de Alta de 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 botn Cancelar de la ventana de Alta de Facturas ejecuta el siguiente cdigo corregido:


//Boton Cancelar procedure 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 para el ingreso de la forma de pago. En este campo se puede almacenar prcticamente cualquier cosa, pero ofreceremos al usuario algunas ya prefedinidas: por ejemplo Contado, Adelanto y 30 das, Tarjeta de crdito. Estas cadenas predefinidas se colocan en la propiedad Items del ComboBox, como si fuera uno comn. La diferencia estriba en que el texto que se encuentre en el Combo -ya sea seleccionado de la lista o escrito a mano- ir a parar a la tabla de Facturas al campo FormaDePago (indicado por las propiedades DataSource y DataField). Queda como ejercicio completar esta parte.

18

jercicio 5
Colocar un DBComboBox para entrar la forma de pago, con las tres opciones comentadas ms arriba.

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

Detalle de la factura: campos virtuales


La informacin del detalle de las facturas se extrae mayormente de la tabla de Productos. Lo primero que haremos es determinar cmo se acceder a esa informacin 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 el cdigo o el nombre- y el precio unitario.

? La cantidad es un nmero que se debe poder ingresar libremente; dejaremos pues la columna como est. ? Para indicar el producto sera bueno poder elegir de una lista que muestre todos los registros de la tabla
Productos. Ms todava, pediremos que se pueda seleccionar un item por nombre o por cdigo a eleccin del usuario.

? El precio unitario debe poder ingresarse en cada caso particular; no obstante, debera mostrar como valor
por defecto el que figura en la tabla de Productos.

? Por cada lnea se desea tambin ver un subtotal, resultado de multiplicar la cantidad por el Precio
Unitario. Todas estas acciones se pueden llevar a cabo fcilmente en Delphi, gracias a los componentes de campo. Para elegir los datos usaremos campos de bsqueda (lookup). Son equivalentes a los controles DBLookupComboBox como el que utilizamos para seleccionar el Cliente en la Factura (ver ms arriba), con la diferencia que lo que crearemos ahora son Componentes de campo o sea que se integran en la definicin de la tabla. No quiere decir que estos campos existan; de hecho, no tocamos para nada la definicin de la tabla fsica. nicamente en memoria, para el acceso normal a las Bases de Datos, tendremos definidos algunos campos ms.

Los campos de bsqueda 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 estos componentes:

? Traer al frente el Editor de Campos (doble click en la


tabla o seleccionar la opcin correspondiente del men contextual)

? En el men contextual del Editor de Campos,


seleccionar New Field (Nuevo Campo). Se nos presenta la ventana de definicin de campos.

? Completamos los datos del nuevo campo, y aceptamos


los cambios. Se agrega un nuevo componente de campo a la tabla; para el programa, se ha creado un campo nuevo como cualquier otro. En la fig. 10 Se ve la definicin del campo que muestra el cdigo de producto.

Figura 12: creacin del campo de bsqueda de Cdigo

19

? Si el campo es de tipo lookup (bsqueda), lo veremos en la grilla como un ComboBox. Al desplegarlo nos
dar la informacin de la tabla de Productos, pero el dato que seleccionemos quedar guardado en la tabla de Detalle de Facturas. Podemos definir ms de un campo de tipo lookup; de hecho, en esta aplicacin sera prctico tener dos campos as, uno para el cdigo de producto y otro para la descripcin. Dado que estos campos acceden a la misma tabla de productos, se mantendrn siempre sincronizados mostrando el mismo registro (el actual de la tabla de productos). En la fig. 11 se ve la definicin del campo que muestra la descripcin (pero almacena el cdigo).

Figura 13: definicin del campo de bsqueda de producto por descripcin

Si probamos ahora la aplicacin podemos ya seleccionar productos con las listas que se despliegan al entrar a la celda correspondiente (fig. 12). Nos falta ahora hacer que se calcule automticamente el subtotal de cada lnea, multiplicando la cantidad por el Precio Unitario.

La columna Subtotal tambin es un campo virtual, slo que en este caso no buscamos ninguna informacin en otra tabla; el valor que mostraremos es resultado de un clculo. Para definir un campo calculado procedemos de la misma manera que con los campos de bsqueda: en el editor de campos seleccionamos New Field... y completamos los datos. Esta vez el tipo elegido ser por supuesto Calculated (fig. 13). Notemos que al seleccionar Calculated para el tipo de campo se deshabilitan los controles de la parte inferior de la ventana. El valor Figura 14: el campo de bsqueda (lookup) de productos por descripcin en accin de este campo resulta de un clculo, dijimos... pero adnde ponemos la expresin? La expresin no se coloca en el campo, sino en la tabla. El componente Ttable tiene un evento especial para dar valor a todos los campos de tipo Calculado: previsiblemente, se llama OnCalcFields.
Figura 15: definicin del campo calculado Subtotal

20

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 edicin ? 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 actualizados los campos calculados es mucha carga para el programa; en esas ocasiones, podemos poner la propiedad de la tabla llamada AutoCalcFields en Falso. Entonces el evento no se producir al modificar datos del mismo registro, recin veremos los resultados de los clculos cuando nos movamos a otro registro2. Para la mayora de las aplicaciones, dejaremos la propiedad AutoCalcFields en Verdadero. Y finalmente cmo codificamos la expresin del clculo? 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 expresin al componente de campo.

Si prueban la aplicacin ahora, podrn ingresar ya facturas con su detalle, y vern el subtotal de cada lnea ni bien modifiquen cualquiera de los campos.

Nos queda un agregado por hacer a la grilla; dijimos que cuando seleccionamos un producto en cualquiera de los campos de bsqueda deba colocarse como Precio Unitario por defecto el que figuraba en la tabla de Productos. Encontrar este valor no es difcil: simplemente tenemos que buscar el producto que acabamos de seleccionar -y nos encontramos con que ya est seleccionado! Dado que los campos Lookup muestran el contenido de la tabla Productos, al seleccionar uno ya estamos posicionando el cursor de esta tabla en ese registro. Lo que debemos determinar es donde ponemos el cdigo que tome el valor de productos y lo ponga en Detalle. Hay varios lugares posibles; en esta aplicacin actuaremos en respuesta al cambio en el campo Codigo de la tabla de Detalle. Necesitamos un evento que se produzca cuando se cambia el contenido del campo. En Delphi representamos a los campos con los componentes de campo, por lo que es lgico 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 cdigo:
procedure TDM1.tabDetalleCodigoChange(Sender: TField); begin tabDetallePrUnit.AsCurrency:= tabProductosPrUnit.AsCurrency; end;

Este comportamiento parece afectar tambin a los campos de tipo lookup, que no se actualizan en la pantalla hasta que movemos el cursor a otro registro.

21

As de fcil. El valor que colocamos en el campo PrUnit (a travs del componente de campo tabDetallePrUnit) se ver en la columna correspondiente de la grilla inmediatamente, pero el usuario puede cambiarlo con slo escribir encima. Ahora s, tenemos una factura funcional... o casi. Nos falta, claro, la actualizacin del stock por cada producto vendido. La operatoria es simple: buscamos el producto que corresponde a cada lnea del detalle; le restamos la cantidad facturada cuando agregamos una lnea de detalle y le sumamos la cantidad cuando estamos borrando la lnea. 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, agregar el producto a una tabla de pedidos, salir corriendo sin que nos vean... Para evitar que realmente tengamos que salir corriendo con nuestro cliente atrs blandiendo un hacha, haremos que se le presente al usuario la opcin de cancelar esa lnea 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 su ingreso a la tabla: el evento BeforePost de la tabla Detalle. Para cancelar un ingreso en trmite, provocamos una excepcin: la instruccin Abort fue creada justamente para eso. El cdigo queda como sigue:
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 en existencia.'+ #13'Indique qu desea hacer',[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 montn de cosas que antes no estaban... Qu es esa TabPedidos? Y el 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 tabla Pedidos es una nueva tabla que almacenar los datos de productos que hay que pedir a nuestro proveedor. En una aplicacin ms completa habra que almacenar tambin a qu proveedor debemos pedir cada producto; por ahora simplemente los almacenamos en la tabla a la espera de la creacin del pedido.

22

Veamos la estructura que utilizaremos para la tabla Pedidos:

Vemos que los pedidos mantienen una referencia a la factura que les dio origen, a travs del Nro. y tipo de factura. De esta manera podremos rastrear si hemos pedido todo lo que faltaba en una factura en cualquier momento. El otro cambio es el campo EnStock de la tabla Detalle. Este campo es necesario para llevar bien las cantidades en existencia, como mostramos con un ejemplo a continuacin: Supongamos que vendemos 3 unidades de un determinado producto, y hay 6 en existencia. Al ingresar la lnea de detalle se deben restar 3 unidades al campo Existencia de la tabla de Productos; hasta ac todo bien. Ahora supongamos que antes de terminar de introducir la factura el usuario decide cancelarla. Como ya vimos, se borra la factura y el detalle correspondiente. Y entonces hay que actualizar nuevamente la existencia del producto sumndole esta vez la cantidad pedida. Todo fantstico hasta ac. Ahora bien, resulta que en lugar de 3 unidades el usuario pide 15. El sistema le presentar la opcin de marcar la diferencia (15-6=9 unidades) para un pedido al proveedor. El usuario, obedientemente, acepta los cambios. Cmo 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 lnea, y ponemos directamente la existencia en 0. Pero nuestro 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 ms, para guardar lo que realmente estamos vendiendo del stock. Los cambios se hacen efectivos en los procedimientos que se ejecutan despus de aceptar una lnea o despus de borrarla. Veamos la implementacin 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 lnea, la situacin es un poco ms complicada porque necesitamos conocer la cantidad restada de stock y el cdigo del producto... datos que acabamos de borrar! Necesitamos guardar estos datos en algn lugar mientras se produce el borrado. El lugar ideal es el evento BeforeDelete:

23

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

Vemos que al cdigo de actualizacin del total general de la factura se suman las dos lneas necesarias para guardar los valores del cdigo de producto y la cantidad descontada de la existencia en variables internas; despus de borrado el registro (en el evento AfterDelete) actualizamos el stock sumando al producto correspondiente 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 ms sencilla por supuesto que podemos seguir utilizando el mismo cdigo visto arriba, pero tenemos otra opcin. Tambin hay otra forma de hacer estas actualizaciones complejas en cualquier tipo de Base de Datos, utilizando Actualizaciones diferidas (Cached Updates). Veremos esta tcnica dentro de poco, por las ventajas que aporta al momento de trabajar en red. Por ahora terminaremos con el programa agregando la posibilidad de anulacin de facturas, consultas e impresiones.

Consulta de facturas
La consulta que proponemos aqu es muy simple: solamente poder seleccionar una factura y ver el detalle correspondiente. Luego haremos consultas ms sofisticadas incluyendo condiciones de filtro (facturas que se hicieron en tal fecha, a tal o cual cliente, que superen tal monto, etc. etc) Una vez definido el alcance de la operacin que acometemos, vemos que en Delphi podemos resolverlo fcilmente: la relacin master/detail entre las tablas de Facturas y Detalle nos mantiene filtrada la segunda en base al registro seleccionado en la primera, por lo que solamente debemos mostrar las dos tablas. Usaremos dos grillas: en la primera mostraremos al usuario la tabla de Facturas, donde podr navegar sin cambiar nada. En la segunda se mostrar la tabla de Detalle que como dijimos, ya est filtrada mostrando solamente los registros que corresponden a la factura seleccionada. Como detalle, agregaremos la posibilidad de bsqueda de factura por nmero. La pantalla de consulta queda entonces como sigue: <<<Grfico de la pantalla de consulta de facturas>>>

24

Y esto es todo con respecto a la consulta. Sin embargo, vamos a pedirle a Delphi algo ms: que nos muestre con otro color las facturas que estn anuladas. Esto implica tomar el control del redibujo de la grilla; pero veamos primero cmo anular una factura.

Anulacin de facturas
Las facturas no se pueden borrar; una vez impresas, la nica posibilidad es anularlas. Para ello hemos previsto una opcin en el men principal y lo que es ms importante, un campo en la tabla Facturas. Este campo (Anulada) es de tipo lgico (booleano), es decir que solamente puede tomar los valores True o False. Un valor True indicara que la factura est anulada. Entonces lo nico que tenemos que hacer es seleccionar la factura deseada (para lo cual sera bueno ver todo el detalle) y poner valor True en el campo Anulada. Lo hacemos en una ventana igual a la que usamos para la consulta, solamente que ahora tenemos dos botones: Anular y Activar. Les trae algo a la memoria? Si se estn preguntando lo que yo pienso que se estn preguntando, la respuesta es s: se puede reutilizar el cdigo de la ventana de consultas haciendo que la nueva ventana de anulacin herede las caractersticas de la otra. Como ya lo hemos hecho anteriormente, se los dejo como ejercicio. nicamente les mostrar la ventana ya terminada, con los dos botones (fig. ???)

<<<Grfico de la ventana de anulacin>>>

E E

jercicio 6
Realice la ventana de anulacin de facturas, heredndola de la ventana de consulta. Agregue los botones de Anular y Activar

?
Los nuevos botones, como habrn adivinado, colocan los valores True y False respectivamente en el campo Anulada de la factura. El cdigo es muy simple, y nuevamente queda como ejercicio.

jercicio 7
Escriba el cdigo para los botones Anular y Cancelar.

NOTA: estas operaciones estarn normalmente restringidas para ser ejecutadas slo por usuarios con jerarqua suficiente (gerentes o responsables de rea). Veremos luego cmo asignar permisos de ejecucin diferentes en funcin del nivel del usuario.

Bueno, ya casi estamos llegando al final? Nooooo... faltan muchos detalles para que esta aplicacin pueda considerarse completa. En primer lugar, volvamos al diseo de la Base de Datos. Siempre es conveniente validar los datos por encima de la aplicacin, es decir, en la estructura misma de la BD. Por qu? Porque si no, puede aparecer la mano negra que nadie conoce pero todos sufren alguna vez, y tocar los datos de la tabla sin usar

25

nuestro programa. Por ejemplo: a travs 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 con servidores SQL, es el ingreso a la Base de Datos en s lo que est restringido, y es obligatorio validar el usuario y su clave para poder acceder a los datos. En nuestro ejemplo no vamos a poner clave a las tablas mientras trabajamos en Paradox. El caso que s vamos a contemplar en cualquier base de datos es el de la Integridad referencial de los datos. Por Integridad Referencial entendemos ciertas reglas que impiden que los datos queden hurfanos en una tabla; es en cierta manera un trabajo social ;-) El problema viene por el lado del modelo relacional: cuando tenemos dos tablas en relacin maestro/detalle, habr algn campo o conjunto de campos que definen la relacin entre los registros del detalle y los de la tabla principal. En otras palabras, los valores de esos campos en la tabla detalle deben corresponder a algn registro de la tabla principal. Si no, tenemos por ejemplo un IDCliente y no tenemos el nombre... lo cual nos sirve poco y nada. A estos registros que quedan descolgados se les llama hurfanos. Hay que asegurarse entonces que los datos de las tablas de detalle en una relacin de esas no queden hurfanos. 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 estos casos. Tomemos el ejemplo de las facturas de nuestra aplicacin: hay tres casos en los que necesitaremos definir reglas 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 aplicacin (Facturas). Definamos entonces las reglas de Integridad Referencial entre las tablas de Facturas y Clientes:

? Modificacin: permitiremos la modificacin de los datos de los clientes, incluso de los campos que forman
el enlace con la factura; pero si se modifican estos ltimos campos, tendremos que modificar en forma acorde los datos de la factura. Por ejemplo: enlazamos por un campo ID y tenemos una factura que corresponde a un cliente de ID = 5. A continuacin el usuario modifica el valor del campo ID en la tabla de clientes y coloca un valor 6; entonces tendremos que modificar el campo de la factura para que diga 6. Esta operacin se denomina actualizacin en cascada, y la realiza automticamente la Base de Datos.

? 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 la principal. Para definir las reglas de la tabla de Facturas, entramos a la utilidad de restructuracin de tablas del Database Desktop y seleccionamos en el ComboBox de las propiedades la opcin Referential Integrity (fig. ???) <<<Imagen de la restructuracin de la tabla Facturas, seleccionando la opcin REf. Integrity>>>

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

26

nombre de la tabla externa. NOTA: por algn error en la implementacin del Database Desktop, nicamente se muestran como tablas pasibles de ser referenciadas las que estn en el directorio especificado por el alias :WORK:, que se selecciona en el men File|Working Directory. Para nuestro ejemplo de la tabla de Facturas referenciando a la tabla de clientes, podemos seleccionar el alias en uso como Working Directory. Una vez seleccionados los campos, la informacin de Integridad Referencial queda como se ve en la figura ???? <<<Grfico de la pantalla de Integridad Referencial mostrando las selecciones para el enlace entre Facturas y Clientes>>>

Cuando aceptamos la informacin ingresada, debemos dar un nombre a la Regla de Integridad; con este nombre se guardar la regla en el archivo .VAL.

Puntos para tener en cuenta:

? ? Aceptar todo: si hay registros pendientes, aceptar todo. Aqu tendra que actualizar el archivo INI con
el nuevo nmero 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 el
OnChange de Producto)

? ? Detalle: Subtotal calculado ? ? Factura: Total autoactualizable ? ? tabFacturas: valores por defecto. En especial, poner el nmero de factura en un archivo .INI o en una
tabla de secuencias

? ? TabFacturas: la condicin de pago se puede ingresar con un componente DBComboBox o un


DBRadioGroup

? ? Aclarar bien el tema de Cancelacin de facturas... la necesidad de una variable bandera global, o una
edicin falsa.

? ? Qu pasa cuando no alcanza el stock ? ? Validacin del tipo de factura, la fecha, etc. OK ? ? Error Key violation si ponen un Nro que ya est OK ? ? Considerar en el INI los nmeros 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

? Anulacin de facturas

27

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

Toques finales

? Logotipo de la empresa en la pantalla principal y en la pantalla de carga ? Creacin 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

28

Вам также может понравиться