Sistema de Facturacion

download Sistema de Facturacion

of 28

Transcript of Sistema de Facturacion

Sistema de facturacin y control de Stock

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

1

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

E

jercicio 1Implemente 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

2

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

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

3

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

4

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 laspropiedades 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 tomeel 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 lookupque 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:

5

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;

6

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');

7

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;

8

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.

E

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

?

FacturacinLa 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:

9

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

ValidacionesValidar 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.

E

jercicio 3Realizar 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.

E1

jercicio 4Coloque 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 globalesEn 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 cuestinCuando 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 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 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

E

jercicio 5Colocar 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 virtualesLa 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 tablaProductos. 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 valorpor 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 PrecioUnitario. 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 latabla 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 aceptamoslos 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 nosdar 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 tablaComo 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.

2

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