P. 1
Sistema de Facturacion

Sistema de Facturacion

|Views: 2.190|Likes:

More info:

Published by: Franklin Quispe Arpi on Aug 04, 2011
Copyright:Attribution Non-commercial

Availability:

Read on Scribd mobile: iPhone, iPad and Android.
download as PDF, TXT or read online from Scribd
See more
See less

06/07/2013

pdf

text

original

Sistema de facturación y control de Stock

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

Como ejemplo de aplicación de las técnicas de Bases de Datos y realización de programas, haremos un programa para llevar un control de Inventario (Stock) incluyendo la facturación de los productos. Este ejemplo no pretende ser una implementación de nivel comercial; simplemente demuestra técnicas y herramientas de uso común. Por lo tanto restringiremos nuestra atención a un hipotético comercio -La Luz Mala S.A., Artículos de Iluminación- y solamente trabajaremos con los datos de productos y clientes, que son necesarios para la facturación. Algunas partes quedarán abiertas para que el lector las termine, de manera de tornar el ejemplo en una especie de taller práctico.

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

Desarrollaremos primero la aplicación completa usando tablas de Paradox, para poner el énfasis en temas como validación, trabajo con tablas dependientes, y otros temas que rara vez se encuentran aplicados a un problema concreto, aunque en la realidad aparecen en la mayoría de los casos. Una vez que tengamos la aplicación completa y funcionando migraremos los datos a un servidor SQL (Interbase) y nos centraremos en los problemas que pueden surgir como consecuencia. Por último, agregaremos algunos “extras”: utilización directa de aplicaciones a través de Automatización OLE, poner procesos en hilos de ejecución separados, etc. Este ejemplo está planeado para llenar un agujero en los cursos que normalmente se encuentran sobre programación: la aplicación de distintas técnicas a una aplicación “de verdad”, completa y funcional. A casi todos nos ha pasado cuando empezamos a crear programas reales que nos encontramos con problemas muy particulares, distintos a los que se tratan en los ejemplos del libro de Delphi que compramos. El proceso de encontrar soluciones a esos problemas es apasionante e instructivo, pero también lleva su tiempo y esfuerzo. Si este ejemplo los ayuda a ganar un poco de ese tiempo sin quitar la parte instructiva, entonces habrá cumplido su objetivo -y yo el mío.

1) Diseño de la BD

1

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

Vemos en el diagrama que tenemos cuatro entidades, relacionadas entre si. Este es el diagrama lógico, independiente del motor de Base de Datos que utilicemos. El motor a utilizar determinará los tipos de datos, aunque ya podemos (y debemos) definir cuál campo será numérico, cuál de texto, etc. En el diagrama se ven los tipos que hemos seleccionado entre los disponibles para el diagrama lógico en el programa E/R Studio de Embarcadero SA. La elección 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 implementación, 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 mínimo de inconvenientes. Como primera elección nos inclinaremos por el motor de Bases de Datos de Paradox, porque viene incluido con Delphi y por lo tanto es el más simple de usar. Más tarde

E

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

?

2) Diseño de la interface La aplicación consta de una ventana principal desde la que se accede a las distintas opciones a través de un menú:

Archivo Salir Datos ABM Clientes... ABM Productos... Facturación Alta... Anulación... 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

La ficha general es: Figura 3: ficha base de las ABM Las tablas serán colocadas en un DataModule. mientras que el de la derecha se refiere a los datos del registro actual. lo pondremos en las fichas descendientes.Dataset. No lo incluimos en el USES de esta unit ya que no lo usamos todavía.State in dsEditModes then 3 .bAceptarClick(Sender: TObject). a través de la grilla): procedure TForm2. Los botones de Aceptar y Cancelar tienen ya un código asociado. crearemos una ficha maestra con los controles y propiedades comunes y luego heredaremos de ésta las fichas para cada caso particular. para aceptar o rechazar los cambios (notemos la referencia indirecta a la tabla. Notaremos que hay dos botones de impresión. El de la izquierda generará un listado completo de la tabla que vemos en la grilla.? Tienen dos paneles: uno con una lista de los datos más útiles y otra con los datos detallados del registro activo Figura 2: estructura de la BD propuesta Para aprovechar estas semejanzas.Datasource. que llamaremos DM1. begin if DBGrid1.

comprobamos que esté seleccionada la opción inherit (heredar) y damos al OK (fig. Seleccionamos la ventana maestra (en mi caso. una enlazada con la tabla de clientes y la otra con la tabla de productos. podemos también poner un título (Caption) más indicativo que el que pone Delphi por defecto (por ejemplo.Dataset. La ventaja es que hemos heredado todas sus propiedades y métodos (como los procedimientos de los botones Aceptar y Cancelar) pero podemos cambiar cualquier cosa.Post.Dataset.Cancel.. begin if DBGrid1. presionamos el botón derecho del ratón sobre la misma y seleccionamos “Add to Repository.” como indica la figura 3 A continuación seleccionamos la página del almacén de objetos donde queremos que aparezca nuestra Figura 4: agregar la ficha al almacén de objetos ficha. seleccionamos del menú File la opción New. aunque la referencia siga estando allí.. Realizamos la misma operación para la ventana de ABM de Productos (fABMProductos). end. Este código es el mismo para todas las ventanas de Altas. end.Datasource. Para poder utilizar esta ficha como base para las otras ventanas. Más tarde agregaremos otras operaciones que también son generales.Datasource. procedure TForm2. Ahora crearemos las ventanas de datos descendientes.bCancelarClick(Sender: TObject). Bajas y Modificaciones y por eso se puede colocar en la ventana madre. Logramos una copia idéntica de la ventana principal de ABM.. y en el Almacén miramos en la página de nuestra aplicación (en mi caso la llamé Factura2). Figura 5: propiedades de la ficha para agregar al almacén Para ello. Llamemos a esta ficha fABMClientes y la grabamos con el nombre uABMClientes. La ficha y su unidad asociada son ahora las fuentes de uno de los objetos del almacén. y eventualmente seleccionamos un icono para representarlo (fig.DBGrid1.Datasource.Dataset. con todas sus características. Notemos que si movemos o renombramos estos archivos no podremos utilizarlos desde el almacén. “Datos de clientes”). le damos un nombre y una descripción.State in dsEditModes then DBGrid1. 5). 4). llamada fABMMaster). Figura 6: crear una nueva ficha heredando las características de la ficha ABMMaster Ahora tenemos que darles vida a las ventanas nuevas: para eso 4 .. debemos primero guardar el modelo en el Almacén de Objetos (Repository) de Delphi. Teniendo la ficha visible.

El botón de “Propiedades” por ahora no hace más que poner el cursor (el foco de atención del teclado) en el primero de los editores de la derecha: 5 . Primero. de manera que he utilizado un control DBText en lugar de un DBEdit. conectamos el navegador y la grilla con la fuente de datos de clientes que tenemos en el módulo de datos (recordemos incluir la unit del módulo de datos en la cláusula USES). he colocado sólo dos columnas en la grilla: NombreYApellido (con otro título) y Telefonos. mostrando el campo NombreYApellido.necesitamos colocar las tablas en el proyecto. ? Entre las tablas de Detalle y Productos también hay una relación de Lookup: creamos un campo lookup que tome el campo Producto de Detalle y lo relacione con el campo CodProd de Productos. Nos concentraremos ahora en las propiedades que hay que cambiar en la ficha de ABM de Clientes para diferenciarla de su “madre”. Además. Creamos entonces el Módulo de Datos y ponemos las tablas y Fuentes de Datos necesarias. mostrando el campo Descripcion. El campo IDCliente no es editable (es autonumérico). Además. creamos los componentes de campo para cada tabla y agregamos los enlaces: ? Entre las tablas de Facturas y Detalle hay una relación Master/Detail: en la tabla de Detalle ponemos las propiedades MasterSource al DSFacturas y MasterFields a NroFactura-Factura ? Entre las tablas de Facturas y Clientes hay una relación de Lookup: creamos un campo lookup que tome el campo Cliente de Facturas y lo relacione con el campo IDCliente de Clientes. A continuación 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 títulos (son simples etiquetas) poniendo el texto en Negrita.

En esta ventana ya podemos ver y modificar clientes ya existentes. Podemos borrar esta línea. Ya podemos probar la ventana.Columns.DataSource. podría ser algo como lo siguiente: begin FABMClientes. Utilizando esta función y con un poco de cuidado. Podemos aprovechar la relación de herencia que existe entre la ficha que guardamos en el almacén y nuestras fichas de ABM Clientes y ABM Productos.SetFocus. pero si alguna vez lo agregamos será llamado automáticamente. podemos inclusive poner este código en el botón de la ventana maestra. q.procedure TfABMClientes. hay una forma muy práctica de generar este listado: la función QRCreateList que nos brinda QuickReport en la unit QRExtra.Add(DBGrid1. DBEdit2. Self. q. begin l:= tStringList. si no utilizamos referencias directas a una ficha particular en los procedimientos de respuesta de los botones. l: tStringList. end.Columns[i-1].Dataset. En este caso no hay un procedimiento tal en la ficha FABMMaster.Show. Notemos la palabra “inherited” que escribió Delphi al principio del procedimiento. Estoy suponiendo que permitimos a Delphi que cree automáticamente la ventana al comenzar la aplicación (Opciones del proyecto). DBGrid1. Por ejemplo. begin inherited. i: integer.FieldName). entonces podemos escribir el código en la ficha madre y automáticamente estará disponible en las dos fichas descendientes -y en cualquier otra que inventemos después. si agregamos en la ventana principal las instrucciones necesarias para que se muestre en respuesta a la opción “ABM Clientes” del menú “Datos”.'Listado de '+Caption. 6 . El código sería algo como lo siguiente: procedure TfABMMaster. De esta manera será heredado por las ventanas descendientes. QRCreateList(q. end. El botón 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. for i:= 1 to DBGrid1. end.count do l. l.Free. q:= nil. l). var q:tCustomQuickRep.BitBtn3Click(Sender: TObject).BitBtn1Click(Sender: TObject). sigamos expandiendo la funcionalidad escribiendo los manejadores de los otros botones.Free.Preview.Create. Esto significa que se llamará al procedimiento del mismo nombre definido en la ficha de la cual desciende la que estamos trabajando. o cambiarla de lugar dentro del procedimiento.

es decir.Debemos incluir en la cláusula USES un par de units: QRExtra y QuickRpt. Para la tabla de clientes podríamos mostrar una ficha de datos como la siguiente: Figura 8: ventana de búsqueda de Clientes Al presionar el botón “Buscar” realizamos la búsqueda utilizando Locate para independizarnos de los índices.[loPartialKey]) then ModalResult:= mrOk else ShowMessage('No se encuentra el cliente solicitado'). el cursor se habrá movido automáticamente en la tabla. Pero la impresión de la ficha con los datos de un registro es distinta para los clientes y para los productos. En caso que el texto buscado no se encuentre. Notemos que pasamos aquí las columnas de la grilla como columnas del reporte.text. donde están definidas la función QRCreateList y la clase TcustomQuickRep respectivamente. El botón “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. La generación de una lista de la tabla completa es simple y se puede hacer en forma genérica como antes. y si encuentra cierra la ventana con OK if DM1. //Busca. No hace falta ningún código. de manera que estaremos posicionados correctamente sobre el registro buscado. begin //seleccionamos el campo a buscar if RadioGroup1.edit1. mostramos un mensaje al usuario y no cerramos la ventana. var s: string.ItemIndex = 0 then s:= 'NombreYApellido' else s:= 'Telefonos'. por lo que tendremos que hacerla “a mano”. Permitiremos la búsqueda por varios campos. el reporte impreso será muy parecido a lo que se ve en la grilla. Si encontramos una coincidencia cerramos la ventana de búsqueda poniendo un valor mrOK en la propiedad ModalResult de la ventana.Locate(s. para permitirle que siga buscando. así que tendremos que codificarla por separado. Primero hagamos la parte de búsqueda.tabClientes. 7 . La búsqueda también es particular para cada descendiente.BitBtn1Click(Sender: TObject). Lo veremos luego. El código del botón “Buscar” es el siguiente: procedure TFBuscarCliente. generando un reporte para cada una que será llamado en los botones de “Ficha” de cada ventana descendiente.

Filtered:= false. FFichaCliente. Hay un fallo en la lógica del código anterior.tabClientes.Preview. Existen varios métodos de filtrado que podemos usar.Filter:= 'IDCliente=' + DM1. DM1. Figura 9: impresión de los datos de un cliente Entonces el código en el procedimiento de respuesta al botón “Ficha” podría ser algo como lo siguiente: procedure TfABMClientes. Como dijimos antes. cuando presionemos el botón de imprimir los datos del cliente que estamos viendo. Para eso debemos filtrar de alguna manera la tabla.tabClientes. 8 .BitBtn5Click(Sender: TObject).BitBtn5Click(Sender: TObject). Este comportamiento no es el que deseamos.end. No obstante la dejaremos porque si el día de mañana agregamos algo en la ventana madre se ejecutará automáticamente. FFichaCliente. Algo así como el listado que generamos antes. este botón debería imprimir solamente los datos del cliente actualmente seleccionado. begin inherited..Preview. DM1.tabClientes.Filtered:= true.. 8. creamos un QuickReport como el que se muestra en la fig. así que podríamos borrarla. end.AsString. DM1. Y ya tenemos casi lista nuestra ventana de Altas Bajas y Modificaciones de Clientes: sólo falta la impresión de la ficha. Para la impresión de los datos del cliente. El código para el evento OnClick sobre el botón de impresión de datos personales queda ahora como el siguiente: procedure TfABMClientes. begin inherited.tabClientes. Vamos a ello. con los datos de todos los clientes.FieldByName('IDCliente'). pero más lindo. En nuestro caso no tenemos nada de código en la ventana madre para este botón. En este ejemplo usaremos la propiedad filter. se nos mostrará el reporte. Delphi agrega automáticamente la primera línea para llamar al código heredado antes de hacer nada más.

? Facturación La parte de facturación es la más complicada de esta aplicación.0)=mrNo then abort. Veamos primero la ventana terminada: 9 . E jercicio 2 Crear la ventana de ABM de productos heredando de ABMMaster. Bajas y Modificaciones de clientes.. simplemente. ¿Continuar?'. Podemos mostrar una caja de diálogo cuando presionamos el botón de borrar.tabClientesBeforeDelete(DataSet: TDataSet). que se disparará siempre cualquiera sea la forma de borrar el registro: procedure TDM1. mbYesNo. ¿qué sucede si queremos borrar un registro de la tabla de clientes? Debería pedirnos confirmación. pero lo postergaremos hasta que veamos la facturación.. pero así quedamos expuestos a que en una modificación posterior agreguemos otra forma de borrar los registros -por ejemplo. Ahora sí tenemos completa la ventana de Altas. Notemos que después de mostrar el reporte sacamos el filtrado a la tabla. completa con todos los botones funcionando.mtConfirmation. El lugar más conveniente para pedir la confirmación es el evento BeforeDelete de la misma tabla. begin if MessageDlg('Se va a borrar el registro. pero. en una grilla de consulta. en el Inspector de Objetos abrimos la lista y seleccionamos para el evento BeforeDelete de la tabla de Productos el mismo procedimiento.y el control no se haga. Faltan algunos detalles: por ejemplo. lo harán Uds. Delphi nos permite indicar a esta tabla que llame al mismo procedimiento anterior.end. Hay otra consideración que hacer con respecto al borrado en la tabla de facturas y su relación con la de detalle. porque enlaza y utiliza todas las tablas a la vez. end. El procedimiento es el mismo para la tabla de productos (notemos que en ningún momento necesitamos nombrar la tabla). Lo mismo hay que hacer para los productos.

db Detalle. el código del producto y el precio unitario. en nuestra factura debemos asegurarnos que cada vez que el usuario va a modificar algo en la tabla de Detalle. Estas dos tablas están relacionadas en forma Maestro/Detalle. Posteriormente modificaremos la grilla para que la columna de código nos deje elegir alguno de los productos de la tabla de Productos en una lista. tirando por el suelo nuestra estrategia ya que el usuario no entraría en el control donde pusimos nuestro código. aceptamos los datos y listo. Por consiguiente. El código es simple: si la tabla de Facturas está en estado de inserción o edición. Podemos ver el comportamiento anterior si dejamos en la grilla todas las columnas de la tabla Detalle. pero se debe poder modificar.db Figura 10: ventana de alta de facturas En esta ventana trabajamos sobre dos tablas: Facturas y Detalle.para mostrar el subtotal. porque podríamos estar cambiando los valores de los campos de enlace. antes que escribirlos directamente con las posibilidades de error que eso traería. definiremos un nuevo campo virtual -no existente en la tabla física. No obstante.. Delphi incluso toma en consideración la relación cuando agregamos registros a la tabla de detalle. si lo pensamos un poco más vemos que un ratón en la mano de un usuario se transforma en un arma mortífera: es muy fácil modificar por ejemplo el nro.. 9).Facturas. Nro y tipo de factura). Notaremos que al momento de insertar un registro nuevo Delphi da valor automáticamente a los campos Nro de Factura y Tipo de Factura. El precio unitario debe tomar como valor por defecto el precio indicado en la tabla de productos. la de Facturas no esté en modo de inserción o edición. La pregunta del millón es: ¿adónde colocamos el código? Una primera idea sería en el evento OnExit del último control de la parte de arriba. La 10 . además. El campo IDItem no es editable porque es de tipo autonumérico. de manera que automáticamente la tabla de Detalle se filtra para mostrar sólo los registros que correspondan a la factura que se ve arriba. poniendo automáticamente los valores que corresponden en los campos de enlace (en este caso. Debemos encontrar un evento que se produzca inequívocamente antes de modificar la tabla de detalle. y nos quedan solamente la cantidad. resultado de multiplicar la cantidad por el precio unitario. de factura y después directamente pasar el foco a la grilla. 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 válido seleccionado en la tabla Principal. y en la parte inferior tenemos una grilla que muestra y trabaja con los datos de la tabla Detalle (fig. En la parte de arriba tenemos controles para modificar los campos de la tabla de Facturas.

con las siguientes propiedades: ? ? ? ? ? DataSource: dm1. begin if dm1. Luego veremos que este es también el caso del campo Producto de la tabla de Detalle. Hay tres momentos para hacer las validaciones: ? Al escribir (caracter a caracter) 11 . que no haya letras en un campo numérico. ponemos valores a los campos NroFactura y Tipo (luego veremos cómo asignarles valores por defecto) y entramos a la grilla para agregar un detalle. ?.tabFacturas.dsClientes ListField: NombreYApellido KeyField: IDCliente El resultado se ve en la fig. Figura 11: selección 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. Sigamos trabajando sobre los controles de la tabla de Facturas.Post.State in dsEditModes then dm1. tendríamos que ingresar un número en este campo. Para lograr esto utilizamos un control DBLookupComboBox.tabFacturas. Ahora tenemos la tabla en el estado correcto: podemos probarlo si corremos el programa. Pero no es necesario obligar al usuario a recordar los identificadores internos de los clientes.dsFacturas DataField: Cliente ListSource: dm1. De esta manera el usuario siempre trabajará con el Nombre y Apellido del cliente.DBGrid1Enter(Sender: TObject). podemos mostrar una lista con los nombres y apellidos y decirle a Delphi que en realidad queremos guardar el ID del que seleccionamos. Componentes de búsqueda (lookup) El campo de Cliente también impone una condición: dado que guardamos en la tabla de facturas solamente el ID del Cliente (de acuerdo con las reglas de normalización). casi siempre es conveniente trabajar con este sistema para los campos que referencian a otra tabla (claves externas). Los campos de enlace tomarán valor solos. por ejemplo. por lo que podríamos controlar el estado de la factura al ganar el foco la grilla: el evento OnEnter de la grilla. En general. El código queda como sigue: procedure TFAltaFactura. mientras internamente se maneja sólo el número de identificación. end.única manera de modificar los datos del Detalle en esta pantalla (y en la esta aplicación) es la grilla de la ventana de Alta de Facturas.

en lugar de esperar que el servidor de Bases de Datos nos devuelva el error.AsString<>'C') then begin ShowMessage('El tipo de factura debe ser ''A''. entre los cuales hay uno que es específicamente para realizar validaciones antes de enviar los datos a la BD: OnValidate. 1) Solamente una letra Es fácil: ponemos la propiedad MaxLength del DBEdit correspondiente en 1. 2) En mayúsculas También se puede hacer en el editor: ponemos la propiedad CharCase a ecUpperCase. pero en una más general tendríamos que comprobar en este evento todos los campos que requieren verificación y mostrar el mensaje correspondiente para cada uno. Claro que también está la restricción de la definición de la tabla. en esta aplicación simple. Colocamos entonces el siguiente código en el evento OnValidate del componente del campo Tipo de la tabla Facturas: procedure TDM1. mejor. cuando hagamos el Post. 3) Solamente “A”. Este evento se ejecuta cuando Delphi intenta introducir los datos en el campo (todavía en memoria hasta que hagamos el Post). pero si podemos impedir que el usuario cometa un error. El foco no sale del editor hasta que coloquemos un valor que pase la validación o 12 .? Al introducir el valor en el campo ? Al introducir el registro en la Base de Datos (Post) Aplicaremos en este ejemplo los tres tipos de validaciones.tabFacturasTipoValidate(Sender: TField). end. Notemos que también podríamos haberlo hecho en la máscara de edición del componente de campo.AsString<>'B') and (Sender. Tratemos estos problemas uno por uno. controlamos antes de enviarlo. ''B'' o ''C''').AsString<>'A') and (Sender. debemos provocar una excepción que corte el flujo del programa: la instrucción Abort hace justamente eso. end. solamente “A”. Para que esto no suceda. Abort. Es mejor utilizar los eventos de los componentes de datos. “B” o “C” Nuevamente aquí tenemos un control que se tiene que hacer en la tabla. También podríamos hacerlo en la máscara del componente de campo. begin if (Sender. interpretarlo y mostrarlo. pero conviene comprobar también en el programa cliente para lograr una rápida respuesta al usuario. “B” o “C” y en mayúsculas. Podemos hacerlo en el evento BeforePost de la tabla. en la que asignamos una longitud 1 al campo Tipo. El campo de Tipo de Factura pone una restricción: queremos que deje ingresar sólo una letra. Si solamente mostramos el mensaje. los datos llegarán igualmente a la memoria intermedia del campo y podrían ser rechazados por las validaciones de la Base de Datos.

Dígale eso si llama a la madrugada. el cursor les pasa por encima como si no existieran y el usuario puede hacer lo mismo. mostrando simplemente el mensaje. como restricciones (constraints). Los componentes de campo tienen una propiedad que permite especificar el formato genérico de la entrada. igualmente en el número de factura. Los códigos para cada tipo se pueden ver en la ayuda en línea. Desgraciadamente. ¿Y las barras? Las barras quedan como están. Para las validaciones caracter a caracter podríamos escribir un procedimiento que compruebe cada pulsación de tecla. 13 . obligatoriamente. También podemos validar la entrada caracter a caracter. requerimos al usuario que ingrese dos números seguidos de una barra seguidos de otros dos números seguidos de otra barra seguidos de cuatro números. Podemos hacer lo mismo para la fecha. por ejemplo. por lo que no se permitirá una entrada del tipo ' 1/1/00'. mientras que para los campos de fecha y de caracteres se llama EditMask.cancelemos la edición1. Queda como ejercicio. ? Las validaciones a nivel de campo pueden ser codificadas también en las propiedades de los componentes de campo. Para los campos numéricos se denomina EditFormat. que serán 6 números o 2 números. ? En algunas aplicaciones conviene dejar que el usuario salga del editor. cuando el valor de un campo se corresponde con los valores de otros campos.. La máscara sería 00/00/0000. En definitiva. Notemos que todos los caracteres son obligatorios. otra barra. vemos que el ' 0' indica que en ese lugar se espera un número. Sería posible permitirla. que permita 8 o menos números. otros 2 números. E jercicio 3 Realizar la validación de la fecha de la factura. Por suerte. Delphi nos reclamará amablemente que completemos la entrada o nos retiremos honrosamente presionando Escape.. en caso que sea menor solamente mostrar una advertencia. esta propiedad no se llama igual ni utiliza los mismos códigos en todos los componentes. pero realmente no vale la pena. Si consultamos la ayuda en línea. una barra. por ejemplo en respuesta al evento OnKeyPress. E 1 jercicio 4 Coloque una máscara de edición al campo NroFactura. No las usaremos en el presente ejemplo. que no debería ser mayor que la actual. cuatro números. No creo que ningún usuario llame furioso a su casa a las 8 de la mañana para quejarse que tiene que ingresar cuatro números más. Para el campo de fecha de nuestra factura. Delphi tiene ya incorporado un mecanismo de validación así: las máscaras de entrada de los componentes de campo. Y en otras es indispensable. simplemente tiene que escribir los ocho números en secuencia y nada más. Si no se ingresa. Se debe impedir que la fecha sea mayor que la actual. por ejemplo. en el campo de fecha no deberíamos permitir al usuario ingresar letras ni símbolos.

. debemos calificar la llamada a la función: en lugar de escribir simplemente abort escribimos sysutils. la combinación tipo/número de factura debe ser única en toda la tabla. descendiente de EdatabaseError. end. subcódigo. uno de la BDE. Podemos optar por dos caminos: enviar los datos a la tabla y si se produce el error de Violación de Clave (Key Violation) mostrar un mensaje al usuario.Por último. La clase EdatabaseError no contiene otra información sobre el error que no sea el mensaje. Utilizando este objeto podemos reaccionar en forma sencilla al error: procedure TDM1. posiblemente uno por el driver.tabFacturasPostError(DataSet: TDataSet. var Action: TDataAction). y un contador interno de la cantidad de entradas en la lista en la propiedad ErrorCount.y son modelados en Delphi con la clase TDBError. E: EDatabaseError. depende del motor que utilicemos. o bien buscar antes de intentar ingresar los datos si el valor ya existe. Como podemos ver. En particular. y este mensaje puede cambiar si por ejemplo cambiamos el idioma. si el error viene a través de la BDE se produce una excepción EDBEngineError. Usaremos aquí la primera aproximación. así que no nos sirve.código de la BDE. el tratamiento de errores de la Base de Datos no es tan sencillo. pero las veremos en su momento. Esta comprobación 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. para que el compilador pueda diferenciarlas.abort. el mismo nombre que la función de la unidad SysUtils. Por consiguiente. nos queda una validación antes de dar por buenos los datos de la factura. Pasemos ahora a ver la generación de valores por defecto para los campos en los que se pueda hacer. La constante DBIERR_KEYVIOL está definida en la unit BDE.Errors[0]. NOTA: en la unit BDE se define una constante llamada abort -si. Por suerte la BDE sí presenta un código de error diferente para cada error en la excepción EDBEngineError. begin if e is EDBEngineError then if EDBEngineError(e). que tendremos que agregar a la cláusula uses del DataModule. categoría. Cada uno de estos errores consta de varias partes -código nativo. Valores por defecto 14 ..ErrorCode = DBIERR_KEYVIOL then DatabaseError('El número de factura ya existe'). Posteriormente tal vez necesitemos otras validaciones para la tabla de detalle. El proceso del código anterior comprueba que el primer error de la lista sea el correspondiente a la Violación de Clave. Al producirse un error en una base de datos. Las tablas tienen un evento llamado OnPostError que se produce cuando hay un error al intentar meter los datos a la tabla (post). texto. tenemos varios códigos: uno por el motor de bases de datos. En este evento Delphi nos brinda toda la información necesaria en los parámetros. entre ellos el objeto de la excepción que se produce como consecuencia del error de la Base de Datos. La excepción EDBEngineError mantiene una lista de estos objetos llamada Errors. descendiente de EdatabaseError.

pero trataremos de elegir los datos que se ingresan la mayoría de las veces en un uso normal. estos valores se podrán cambiar. antes que nos olvidemos. uno por cada campo. pero antes del evento AfterInsert. En las líneas anteriores hay unas cuantas variables que no hemos definido.idNroFactura. interface uses IniFiles. Técnicamente se produce después de entrar la tabla en estado dsInsert y por lo tanto después del evento BeforeInsert. En nuestra factura pondremos tres valores por defecto: el número de factura (que extraemos de un archivo INI). No es necesario especificar FieldValues después del nombre de la tabla porque es la propiedad por defecto de esta última. así como la variable que representa a la instancia del archivo mismo. begin tabFacturas['NroFactura']:= ArchIni. 15 . pero es más simple de escribir y de entender.defTipoFactura).defNroFactura). el usuario solamente tendrá que pasarlo por alto. Existe un evento especial en los Datasets de Delphi que permite la asignación de valores por defecto a los campos de un registro: el evento OnNewRecord.Es muy práctico (y reduce grandemente los errores de entrada) asignar valores a los campos antes de que el usuario pueda meter la mano. tabFacturas['Tipo']:= ArchIni. En el código anterior hemos usado otra forma de acceder a los datos. para comenzar a introducir valores. Toda vez que el valor a introducir ya esté en el campo. Este método puede ser un poco más lento que usar las propiedades .tabFacturasNewRecord(DataSet: TDataSet). end. Cuando no se procesan grandes cantidades de datos la demora es imperceptible. Hagámoslo ahora. para que pueda configurarse) y la fecha actual: procedure TDM1. Esta propiedad es un array de Variants. el tipo de la factura (también del INI. para tener acceso a las mismas solamente hay que agregar una referencia a esta unit en la cláusula uses del archivo desde donde queremos utilizarla. que se acceden a través del nombre del campo. mediante la propiedad FieldValues de la tabla. La ventaja de esta secuencia es que los valores colocados en el evento OnNewRecord no marcan el registro como modificado.AsXXX de los componentes de campo. Este evento se produce cuando recién se genera la copia en memoria del registro. Es una práctica común definir una nueva unit que contendrá únicamente las declaraciones de todas estas variables y constantes. En nuestro ejemplo. de manera que se puede cancelar la inserción con sólo moverse a otro registro. la unit se llama uGlobales. de esta manera.readInteger(secFactura. A continuación va el listado completo de esta unit: unit uGlobales.pas y contiene las declaraciones de las constantes asociadas con el archivo INI de configuración. es decir accesibles desde todas las units del proyecto. tabFacturas['Fecha']:= date. Definición de una unit para las declaraciones globales En casi todos los programas que tengan más de dos units necesitaremos algunas constantes y posiblemente variables de alcance global.idTipoFactura.ReadString(secFactura.

El nombre es bastante sugerente: simplemente indicará si se ha ingresado ya una factura o no.Create(NombreIni).se distingue por la palabra reservada Initialization y se ejecuta apenas se crea la unit: al principio mismo del programa. Falso. después de destruir todas las ventanas. Debemos comprobar antes de cerrar que las tablas estén en buen estado. idTipoFactura = 'Tipo'. defNroFactura = 1. hay aquí algunas cositas que explicar ¿no?. debemos asegurarnos antes de cerrar la ventana que todos los registros son ingresados correctamente: //Botón Aceptar procedure TFAltaFactura. esa es la cuestión Cuando se acepta la factura. NombreIni = 'Factura2.State in dsEditModes then dm1. ArchIni: TIniFile. no nos queda más que cerrar la ventana ¿verdad? No. Es el momento ideal para dar valores a variables globales. además de declaraciones tenemos dos secciones que son poco vistas en los programas: la sección de inicialización y la de finalización de una unit. el último registro del detalle no estará totalmente ingresado (a menos que nos hayamos movido a otra línea) dado que no hemos hecho el Post. La sección de inicialización de una unit -se puede poner en cualquiera. defTipoFactura = 'B'. por ejemplo. var SeIngresoUnaFactura: boolean. //Identificadores para el archivo INI secFactura = 'Facturacion'. La veremos en la siguiente sección. begin if dm1. implementation Initialization ArchIni:= TIniFile.ini'. end.Free.Post. antes de cualquier evento OnCreate.tabFacturas. Por lo tanto. Aquí es cuando cerramos el archivo INI y liberamos los recursos ocupados por la instancia de TiniFile.BitBtn1Click(Sender: TObject). Finalization ArchIni. Existe una variable declarada que no hemos visto hasta ahora: ‘SeIngresoUnafactura’.TabFacturas. Lo mismo puede suceder con la tabla de facturas. idNroFactura = 'Nro'. ya que el usuario tiene en sus manos el arma mortífera.const ArchLogo = 'logo. Aceptar o cancelar.emf'. Bueno. 16 . la sección Finalization se ejecuta al final de la aplicación. En esta unit global. como nuestra instancia de TiniFile que trabajará con el archivo de configuración. De la misma manera.

Delete. ya que al borrar un registro de la tabla maestra se eliminan todos los correspondientes de la tabla detalle. pero hay que tener cuidado con el orden de las operaciones. y ya.AsInteger+1). después de aceptar las eventuales modificaciones que pueda haber en las tablas actualizamos el archivo INI con el nuevo número de factura..State in dsEditModes then dm1.Cancel.RecordCount>0 do tabDetalle.tabFacturasNroFactura. decide cancelar el ingreso? Resulta que tenemos ya en la Base de Datos varios registros.se deben eliminar en cascada todos los registros de la tabla Detalle. Los únicos registros visibles son los que corresponden a la factura actual. Este control se puede hacer en la Base de Datos (muy recomendado). Pero.WriteInteger(secFactura. Ahora ¿qué sucede cuando el usuario. pero no es gran cosa. después de ingresar los datos de la factura y varias líneas de detalle. debemos borrarlos.tabFacturasBeforeDelete(DataSet: TDataSet).Cancel. Únicamente si todos estos pasos se terminan completamente.state in dsEditModes then dm1. y eso es lo que debemos borrar.idNroFactura. El lugar adecuado para codificar es el evento BeforeDelete de la tabla de Facturas: procedure TDM1. se cierra la ventana colocando el valor mrOk en la propiedad ModalResult del cuadro de diálogo para indicar al programa principal que se aceptó la factura. pequeño saltamontes: recuerda que la tabla Detalle está enlazada en una relación Maestro/Detalle a la tabla Facturas. Esta es la parte fácil. end. Bueno.Post. Entonces. dirán Uds: borramos el registro de factura y los de detalles que pertenezcan a esa factura.tabFacturas. llegamos a la conclusión que hay que borrar primero los registros del detalle y después el de la factura para preservar la Integridad Relacional. Estamos ante el típico caso en que debemos preservar la Integridad Referencial de los datos: no pueden quedar registros de detalle sin el correspondiente registro de factura. Esta acción se denomina borrado en cascada.state in dsEditModes then dm1. Por lo tanto.tabDetalle. //Guarda el siguiente nro de factura en el archivo INI ArchIni. end.if dm1. Queremos que este comportamiento sea siempre el mismo entre las tablas de facturas y de detalle.BitBtn2Click(Sender: TObject). pero. el código en el botón de Cancelar quedaría como sigue: //Boton Cancelar procedure TFAltaFactura.TabFacturas.tabDetalle.. al borrar un registro de Facturas -ya sea por cancelar un alta o en alguna otra pantalla que nos permita hacerlo. begin if dm1.dm1.tabDetalle. Están en lo cierto. cierro la ventana ModalResult:= mrOk. Como vemos en el código. //Unicamente si llega hasta aca.tabDetalle. ¿esto no borra todos los registros de la tabla Detalle? No. 17 . por lo que se encuentra filtrada. if dm1. begin //Borrado en cascada while tabDetalle.

end.delete. lo usamos aquí como muestra de la posibilidad de mezcla de técnicas que brinda el Object Pascal. al momento de presionar Cancelar en la ventana de Alta de Facturas. Pero todavía queda un caso patológico: cuando no se ha ingresado nada todavía y se cancela nada más entrar a la ventana.tabFacturasAfterPost(DataSet: TDataSet). pero ofreceremos al usuario algunas ya prefedinidas: por ejemplo ‘Contado’. end. 18 . if SeIngresoUnafactura then dm1. ‘Adelanto y 30 días’. podemos ? Agregar siempre un registro a la tabla de facturas. pondremos un control DBComboBox para el ingreso de la forma de pago. Queda como ejercicio completar esta parte. apenas entramos. hacemos Post y luego Edit sobre esta tabla. como último detalle. ? Usar una variable que nos indique cuando se ha ingresado realmente un registro en la tabla de facturas.tabDetalle.State in dsEditModes then dm1.y solamente borramos el registro si esta variable tiene valor verdadero.tabFacturas.dm1.delete. 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. end. La diferencia estriba en que el texto que se encuentre en el Combo -ya sea seleccionado de la lista o escrito a mano. Hemos utilizado aquí el segundo método. y el botón Cancelar de la ventana de Alta de Facturas ejecuta el siguiente código corregido: //Boton Cancelar procedure TFAltaFactura. Es un típico método de los programas pre-objetos.y se comprueba en el evento anterior.TabFacturas.Cancel. begin SeIngresoUnaFactura:= true. Estas cadenas predefinidas se colocan en la propiedad Items del ComboBox. Este problema se puede solucionar de varias formas: para citar sólo dos que se vienen a la mente enseguida.tabFacturas. if dm1. El evento AfterPost de la tabla de facturas luce así: procedure TDM1. Ya casi terminamos con la tabla de Facturas.BitBtn2Click(Sender: TObject). Este tipo de variables que indican que se ha alcanzado cierto estado en el procesamiento se denominan Banderas.tabFacturas. En el momento en que el registro de Facturas ya está seguro en la tabla ponemos una variable global de tipo lógico -la variable SeIngresoUnaFactura que vimos declarada en la unit global. En este campo se puede almacenar prácticamente cualquier cosa.state in dsEditModes then dm1. La bandera toma valor verdadero en el evento AfterPost de la tabla de facturas -que está en el DataModule. ‘Tarjeta de crédito’. Por este motivo fue necesario declarar la variable como global.irá a parar a la tabla de Facturas al campo FormaDePago (indicado por las propiedades DataSource y DataField).tabDetalle. como si fuera uno común. begin if dm1.Cancel.

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 opción correspondiente del menú contextual) ? En el menú contextual del Editor de Campos. no tocamos para nada la definición de la tabla física. seleccionar “New Field” (Nuevo Campo). Detalle de la factura: campos virtuales La información del detalle de las facturas se extrae mayormente de la tabla de Productos. Son equivalentes a los controles DBLookupComboBox como el que utilizamos para seleccionar el Cliente en la Factura (ver más arriba). con las tres opciones comentadas más arriba. ? Para indicar el producto sería bueno poder elegir de una lista que muestre todos los registros de la tabla Productos.y el precio unitario. de hecho. para el programa. tendremos definidos algunos campos más. pediremos que se pueda seleccionar un item por nombre o por código a elección del usuario. Todas estas acciones se pueden llevar a cabo fácilmente en Delphi. debería mostrar como valor por defecto el que figura en la tabla de Productos. 10 Se ve la definición del campo que muestra el código de producto. dejaremos pues la columna como está. ? Completamos los datos del nuevo campo. para el acceso normal a las Bases de Datos. El usuario debe ingresar tres datos por cada fila del detalle: la cantidad. ? Por cada línea se desea también ver un subtotal.E jercicio 5 Colocar un DBComboBox para entrar la forma de pago. Más todavía. ? Ahora sí. Para elegir los datos usaremos campos de búsqueda (lookup). se ha creado un campo nuevo como cualquier otro. ? La cantidad es un número que se debe poder ingresar libremente. Se nos presenta la ventana de definición de campos. el producto -ya sea mediante el código o el nombre. gracias a los componentes de campo. no obstante. Los campos de búsqueda tienen la habilidad de mostrar una lista de opciones para el valor del campo. ? El precio unitario debe poder ingresarse en cada caso particular. trayendo esta lista desde otra tabla o consulta. y aceptamos los cambios. Se agrega un nuevo componente de campo a la tabla. ya podemos pasar a discutir las necesidades de la parte de Detalles. No quiere decir que estos campos existan. Únicamente en memoria. con la diferencia que lo que crearemos ahora son Componentes de campo o sea que se integran en la definición de la tabla. resultado de multiplicar la cantidad por el Precio Unitario. Figura 12: creación del campo de búsqueda de Código 19 . Lo primero que haremos es determinar cómo se accederá a esa información desde el punto de vista del usuario. En la fig.

Al desplegarlo nos dará la información de la tabla de Productos. en esta aplicación sería práctico tener dos campos así. En la fig. sino en la tabla. sólo que en este caso no buscamos ninguna información en otra tabla. 13).. pero el dato que seleccionemos quedará guardado en la tabla de Detalle de Facturas. 11 se ve la definición del campo que muestra la descripción (pero almacena el código). El componente Ttable tiene un evento especial para dar valor a todos los campos de tipo Calculado: previsiblemente. lo veremos en la grilla como un ComboBox. Esta vez el tipo elegido será por supuesto “Calculated” (fig. Notemos que al seleccionar “Calculated” para el tipo de campo se deshabilitan los controles de la parte inferior de la ventana. La columna Subtotal también es un campo virtual. de hecho. se llama OnCalcFields.. Podemos definir más de un campo de tipo lookup. El valor Figura 14: el campo de búsqueda (lookup) de productos por descripción en acción de este campo resulta de un cálculo. dijimos. Figura 13: definición del campo de búsqueda de producto por descripción Si probamos ahora la aplicación podemos ya seleccionar productos con las listas que se despliegan al entrar a la celda correspondiente (fig.” y completamos los datos.? Si el campo es de tipo lookup (búsqueda). Nos falta ahora hacer que se calcule automáticamente el subtotal de cada línea.. multiplicando la cantidad por el Precio Unitario. uno para el código de producto y otro para la descripción. pero ¿adónde ponemos la expresión? La expresión no se coloca en el campo. Dado que estos campos acceden a la misma tabla de productos. el valor que mostraremos es resultado de un cálculo. se mantendrán siempre sincronizados mostrando el mismo registro (el actual de la tabla de productos).. Figura 15: definición del campo calculado Subtotal 20 . 12). Para definir un campo calculado procedemos de la misma manera que con los campos de búsqueda: en el editor de campos seleccionamos “New Field.

dejaremos la propiedad AutoCalcFields en Verdadero. Y finalmente ¿cómo codificamos la expresión del cálculo? En simple y puro Pascal: procedure TDM1.tabDetalleCalcFields(DataSet: TDataSet). que no se actualizan en la pantalla hasta que movemos el cursor a otro registro. en esas ocasiones. ? la tabla entra en modo de edición ? se abre la tabla ? se recupera un registro desde la tabla Como vemos. En Delphi representamos a los campos con los componentes de campo. Hay varios lugares posibles. Como vemos.AsCurrency:= tabDetalleCantidad. begin tabDetalleSubtotal. Este comportamiento parece afectar también a los campos de tipo lookup. end.AsCurrency. dijimos que cuando seleccionamos un producto en cualquiera de los campos de búsqueda debía colocarse como Precio Unitario por defecto el que figuraba en la tabla de Productos. Lo que debemos determinar es donde ponemos el código que tome el valor de productos y lo ponga en Detalle. En efecto. Para la mayoría de las aplicaciones. podrán ingresar ya facturas con su detalle. 2 21 .tabDetalleCodigoChange(Sender: TField). Nos queda un agregado por hacer a la grilla. Hay veces que este exceso de celo de la tabla por mantener actualizados los campos calculados es mucha carga para el programa. le asignamos directamente el resultado de la expresión al componente de campo. podemos poner la propiedad de la tabla llamada AutoCalcFields en Falso. por lo que es lógico que se encuentre allí. en esta aplicación actuaremos en respuesta al cambio en el campo Codigo de la tabla de Detalle. end. Necesitamos un evento que se produzca cuando se cambia el contenido del campo. Encontrar este valor no es difícil: 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.AsCurrency. begin tabDetallePrUnit. Si prueban la aplicación ahora.Este evento se produce normalmente cuando: ? nos movemos de un control de datos a otro. y verán el subtotal de cada línea ni bien modifiquen cualquiera de los campos. buscamos en los componentes de campo de la tabla Detalle y en el correspondiente al campo “Codigo” tomamos el evento OnChange para escribir el siguiente código: procedure TDM1. se llama a cada rato. o de una columna a otra en una grilla.AsCurrency:= tabProductosPrUnit. Entonces el evento no se producirá al modificar datos del mismo registro. recién veremos los resultados de los cálculos cuando nos movamos a otro registro2.AsInteger*tabDetallePrUnit.

tenemos una factura funcional. 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. ¿Qué es esa ‘TabPedidos’? ¿Y el campo EnStock que se adivina por el componente de campo tabDetalleEnStock? Bueno. Para cancelar un ingreso en trámite.Así de fácil.tabDetalleProducto..AsInteger:= tabDetalleCantidad. por ahora simplemente los almacenamos en la tabla a la espera de la creación del pedido. Para evitar que realmente tengamos que salir corriendo con nuestro cliente atrás blandiendo un hacha. la actualización del stock por cada producto vendido.Insert.Label1.AsInteger. pero el usuario puede cambiarlo con sólo escribir encima. tabPedidos['Cantidad']:= -dif.ShowModal=mrCancel then sysutils. hacer la vista gorda y no avisar nada. var dif: integer. haremos que se le presente al usuario la opción de cancelar esa línea de factura o agregar el mismo para un pedido posterior al proveedor.abort else begin tabPedidos. ¡Un momento. tabPedidos.AsInteger. tabProductosExistencia. tabPedidos['Codigo']:= tabDetalle['Codigo'].. pero solamente hay %d en existencia.[]).AsInteger. salir corriendo sin que nos vean.AsString. end. end.Caption:= Format('Se han solicitado %d %s.'+ #13'Indique qué desea hacer'. tabPedidos['Factura']:= tabDetalle['Factura']. Ahora sí. provocamos una excepción: la instrucción Abort fue creada justamente para eso. La tabla Pedidos es una nueva tabla que almacenará los datos de productos que hay que pedir a nuestro proveedor. colega! Aquí hay un montón de cosas que antes no estaban..tabDetalleCodigo.AsInteger-tabDetalleCantidad.. La operatoria es simple: buscamos el producto que corresponde a cada línea del detalle.. El valor que colocamos en el campo PrUnit (a través del componente de campo tabDetallePrUnit) se verá en la columna correspondiente de la grilla inmediatamente.AsInteger]). En una aplicación más completa habría que almacenar también a qué proveedor debemos pedir cada producto. end else tabDetalleEnStock. agregar el producto a una tabla de pedidos. El código queda como sigue: procedure TDM1.[tabDetalleCantidad. if dif<0 then begin FNoHayStock. tabPedidosFechaPedido. begin tabProductos.tabDetalleBeforePost(DataSet: TDataSet). 22 .AsInteger:= tabProductosExistencia. Nos falta. tabPedidos['Tipo']:= tabDetalle['Tipo']. if FNoHayStock.Clear. le restamos la cantidad facturada cuando agregamos una línea de detalle y le sumamos la cantidad cuando estamos borrando la línea.Post. claro. forman parte de los cambios que hay que implementar para lograr nuestro cometido. tabDetalleEnStock. o casi.AsString.Locate('CodProd'. dif:= tabProductosExistencia.. 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á.asInteger.

para guardar lo que realmente estamos vendiendo del stock. y debemos sumar a la existencia lo que sacamos. actualizamos el stock tabProductos. hasta acá todo bien. Como ya vimos. Los cambios se hacen efectivos en los procedimientos que se ejecutan después de aceptar una línea o después de borrarla.asInteger. var difCant: integer. Es por esto que necesitamos un campo más. tabProductos. obedientemente. resulta que en lugar de 3 unidades el usuario pide 15. como mostramos con un ejemplo a continuación: Supongamos que vendemos 3 unidades de un determinado producto. acepta los cambios.tabDetalleCodigo.Post. es decir no se descuenta de stock lo que figura en el campo Cantidad). //Actualizamos el total general de la factura difST:= tabDetalleSubtotal. ¡datos que acabamos de borrar! Necesitamos guardar estos datos en algún lugar mientras se produce el borrado. la situación es un poco más complicada porque necesitamos conocer la cantidad restada de stock y el código del producto.Edit. El otro cambio es el campo “EnStock” de la tabla Detalle. a través del Nro. y hay 6 en existencia.Locate('CodProd'...AsInteger-tabDetalleCantidad. begin //actualizamos la cantidad en stock tabProductos. end. y tipo de factura. //Al llegar aquí ya hemos aceptado la diferencia. se borra la factura y el detalle correspondiente. Todo fantástico hasta acá. De esta manera podremos rastrear si hemos pedido todo lo que faltaba en una factura en cualquier momento. y ponemos directamente la existencia en 0. difST: currency. Lo podemos hacer en el momento de aceptar la línea. El lugar ideal es el evento BeforeDelete: 23 . end. El usuario.. no lo que figura como “Cantidad”. Y entonces hay que actualizar nuevamente la existencia del producto sumándole esta vez la cantidad pedida. Ahora supongamos que antes de terminar de introducir la factura el usuario decide cancelarla. ¿Cómo actualizamos el stock? Debemos restar solamente la cantidad que realmente hay en stock (que difiere de la Cantidad Facturada. Este campo es necesario para llevar bien las cantidades en existencia. Ahora bien. Veamos la implementación del evento AfterPost: procedure TDM1.Edit. Cuando borramos una línea. if difCant<0 then tabProductos['Existencia']:= 0 else tabProductos['Existencia']:= difCant. tabFacturas['Total']:= tabFacturas['Total']+difST. Pero nuestro querido usuario decide luego cancelar la factura. difCant:= tabProductosExistencia.[]).AsCurrency-SubtotalAnterior.AsString.tabDetalleAfterPost(DataSet: TDataSet). El sistema le presentará la opción de marcar la diferencia (15-6=9 unidades) para un pedido al proveedor. if difST<>0 then begin if not (tabFacturas.Veamos la estructura que utilizaremos para la tabla Pedidos: Vemos que los pedidos mantienen una referencia a la factura que les dio origen.State in dsEditModes) then tabFacturas. Al ingresar la línea de detalle se deben restar 3 unidades al campo “Existencia” de la tabla de Productos..

por lo que solamente debemos mostrar las dos tablas. begin if not (tabFacturas.state in dsEditModes) then tabFacturas. También hay otra forma de hacer estas actualizaciones complejas en cualquier tipo de Base de Datos.ProdABorrar. veremos que esta operatoria se torna mucho más sencilla por supuesto que podemos seguir utilizando el mismo código visto arriba. Como detalle. end. if tabProductos. etc) Una vez definido el alcance de la operación que acometemos. después de borrado el registro (en el evento AfterDelete) actualizamos el stock sumando al producto correspondiente la cantidad almacenada: procedure TDM1. vemos que en Delphi podemos resolverlo fácilmente: la relación master/detail entre las tablas de Facturas y Detalle nos mantiene filtrada la segunda en base al registro seleccionado en la primera. por las ventajas que aporta al momento de trabajar en red. Usaremos dos grillas: en la primera mostraremos al usuario la tabla de Facturas. a tal o cual cliente.tabDetalleBeforeDelete(DataSet: TDataSet). SubtotalAnterior:= tabDetalle['Subtotal']. pero tenemos otra opción.Post. tabFacturas['Total']:= tabFacturas['Total']-SubtotalAnterior. consultas e impresiones. ya está filtrada mostrando solamente los registros que corresponden a la factura seleccionada. utilizando un servidor SQL que soporte triggers. Cuando pasemos a un entorno Cliente/Servidor. end.Locate('CodProd'.Edit.Edit. Vemos que al código de actualización del total general de la factura se suman las dos líneas necesarias para guardar los valores del código de producto y la cantidad descontada de la existencia en variables internas. donde podrá navegar sin cambiar nada.[]) then //Actualizamos el Stock begin tabProductos. La pantalla de consulta queda entonces como sigue: <<<Gráfico de la pantalla de consulta de facturas>>> 24 . CantBorrada:= Dataset['EnStock']. end. Veremos esta técnica dentro de poco. que superen tal monto. Ahora si tenemos actualizado el stock de nuestros productos. tabProductos. Por ahora terminaremos con el programa agregando la posibilidad de anulación de facturas. Luego haremos consultas más sofisticadas incluyendo condiciones de filtro (facturas que se hicieron en tal fecha. En la segunda se mostrará la tabla de Detalle que como dijimos. Consulta de facturas La consulta que proponemos aquí es muy simple: solamente poder seleccionar una factura y ver el detalle correspondiente. utilizando Actualizaciones diferidas (Cached Updates). agregaremos la posibilidad de búsqueda de factura por número. etc. tabProductos['Existencia']:= tabProductos['Existencia']+CantBorrada. begin ProdABorrar:= Dataset['Codigo'].procedure TDM1.tabDetalleAfterDelete(DataSet: TDataSet).

jercicio 7 Escriba el código para los botones “Anular” y “Cancelar”. ? NOTA: estas operaciones estarán normalmente restringidas para ser ejecutadas sólo por usuarios con jerarquía suficiente (gerentes o responsables de área). Veremos luego cómo asignar permisos de ejecución diferentes en función del nivel del usuario. con los dos botones (fig. Para ello hemos previsto una opción en el menú principal y lo que es más importante. es decir que solamente puede tomar los valores True o False. una vez impresas. pero veamos primero cómo anular una factura. volvamos al diseño de la Base de Datos. Agregue los botones de “Anular” y “Activar” ? Los nuevos botones. vamos a pedirle a Delphi algo más: que nos muestre con otro color las facturas que están anuladas. se los dejo como ejercicio. y tocar los datos de la tabla sin usar 25 . puede aparecer la mano negra que nadie conoce pero todos sufren alguna vez. Un valor True indicaría que la factura está anulada. En primer lugar. en la estructura misma de la BD.Y esto es todo con respecto a la consulta. Únicamente les mostraré la ventana ya terminada. solamente que ahora tenemos dos botones: Anular y Activar. heredándola de la ventana de consulta. ya casi estamos llegando al ¿final? Nooooo. Siempre es conveniente validar los datos “por encima” de la aplicación. y nuevamente queda como ejercicio. colocan los valores True y False respectivamente en el campo Anulada de la factura. ¿Les trae algo a la memoria? Si se están preguntando lo que yo pienso que se están preguntando. Lo hacemos en una ventana igual a la que usamos para la consulta. ???) <<<Gráfico de la ventana de anulación>>> E E jercicio 6 Realice la ventana de anulación de facturas. Bueno. ¿Por qué? Porque si no. la respuesta es sí: se puede reutilizar el código de la ventana de consultas haciendo que la nueva ventana de anulación herede las características de la otra. Este campo (Anulada) es de tipo lógico (booleano). Anulación de facturas Las facturas no se pueden borrar. un campo en la tabla Facturas. Como ya lo hemos hecho anteriormente. es decir. El código es muy simple. Entonces lo único que tenemos que hacer es seleccionar la factura deseada (para lo cual sería bueno ver todo el detalle) y poner valor True en el campo Anulada. faltan muchos detalles para que esta aplicación pueda considerarse completa.. Sin embargo. la única posibilidad es anularlas. como habrán adivinado.. Esto implica tomar el control del redibujo de la grilla.

Si no. entonces tendremos que modificar el campo de la factura para que diga “6”. tendremos que modificar en forma acorde los datos de la factura. Hay que asegurarse entonces que los datos de las tablas de detalle en una relación de esas no queden huérfanos. Recordemos que las reglas de Integridad Referencial se definen en la tabla de Detalle. ? Borrado: no permitiremos el borrado de un cliente si éste figura en alguna factura. lo cual nos sirve poco y nada. ? Cuando se modifican en la tabla principal los campos que forman el enlace. tenemos por ejemplo un IDCliente y no tenemos el nombre. podemos poner una clave a las tablas. A continuación el usuario modifica el valor del campo ID en la tabla de clientes y coloca un valor 6. En otras palabras. Definamos entonces las reglas de Integridad Referencial entre las tablas de Facturas y Clientes: ? Modificación: permitiremos la modificación de los datos de los clientes. y es obligatorio validar el usuario y su clave para poder acceder a los datos. El caso que sí vamos a contemplar en cualquier base de datos es el de la Integridad referencial de los datos. y el 26 . Definir las reglas de Integridad Referencial es decirle a la Base de Datos qué tiene que hacer en estos casos.. ???) <<<Imagen de la restructuración de la tabla Facturas. habrá algún campo o conjunto de campos que definen la relación entre los registros del detalle y los de la tabla principal.. Por ejemplo: enlazamos por un campo “ID” y tenemos una factura que corresponde a un cliente de ID = 5. En nuestro ejemplo no vamos a poner clave a las tablas mientras trabajamos en Paradox. los valores de esos campos en la tabla detalle deben corresponder a algún registro de la tabla principal. pero si se modifican estos últimos campos. es el ingreso a la Base de Datos en sí lo que está restringido. no se puede borrar una factura ¿cierto? pero no hay nada que impida ese borrado desde el Database Desktop. Y entonces.nuestro programa. al trabajar con servidores SQL. y la realiza automáticamente la Base de Datos. Por Integridad Referencial entendemos ciertas reglas que impiden que los datos queden huérfanos en una tabla. Trabajando con Bases de Datos locales com oParadox. Por ejemplo: a través de nuestro programa. Integrity”>>> En la pantalla que aparece se nos piden los campos que referenciarán a la tabla externa (clave externa). Para definir las reglas de la tabla de Facturas. Tomemos el ejemplo de las facturas de nuestra aplicación: 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 aplicación (Facturas).. Esta operación se denomina “actualización en cascada”. seleccionando la opción “REf. es en cierta manera un trabajo social . Hay dos situaciones en las cuales podemos tener problemas: ? Cuando se borra un registro de la tabla principal que tenga detalles enlazados.-) El problema viene por el lado del modelo relacional: cuando tenemos dos tablas en relación maestro/detalle. A estos registros que quedan “descolgados” se les llama huérfanos.. entramos a la utilidad de restructuración de tablas del Database Desktop y seleccionamos en el ComboBox de las propiedades la opción “Referential Integrity” (fig. incluso de los campos que forman el enlace con la factura. por ejemplo. referenciando a la principal.

Aquí tendría que actualizar el archivo INI con el nuevo número de factura. Una vez seleccionados los campos. que se selecciona en el menú File|Working Directory. podemos seleccionar el alias en uso como Working Directory. debemos dar un nombre a la Regla de Integridad. etc. solamente para ver las facturas y sus detalles ? Anulación de facturas 27 . ? ? Qué pasa cuando no alcanza el stock ? ? Validación del tipo de factura. ? ? 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. poner el número de factura en un archivo . o una edición falsa. Puntos para tener en cuenta: ? ? Aceptar todo: si hay registros pendientes.VAL. OK ? ? Error “Key violation” si ponen un Nro que ya esté OK ? ? Considerar en el INI los números de factura para los distintos tipos ? Integridad Referencial entre las tablas ? Consulta de facturas Una ficha simple con dos DBGrid y un navegador. la fecha. En especial. únicamente se muestran como tablas pasibles de ser referenciadas las que están en el directorio especificado por el alias :WORK:.INI o en una tabla de secuencias ? ? TabFacturas: la condición de pago se puede ingresar con un componente DBComboBox o un DBRadioGroup ? ? Aclarar bien el tema de Cancelación de facturas. con este nombre se guardará la regla en el archivo .. la necesidad de una variable bandera global. NOTA: por algún error en la implementación del Database Desktop. aceptar todo.. Para nuestro ejemplo de la tabla de Facturas referenciando a la tabla de clientes. porque ya se aceptó y no hay forma de borrarla.nombre de la tabla externa. la información de Integridad Referencial queda como se ve en la figura ???? <<<Gráfico de la pantalla de Integridad Referencial mostrando las selecciones para el enlace entre Facturas y Clientes>>> Cuando aceptamos la información ingresada.

Igual que la de consulta.. con lista de acciones para compartir con el menú. con indicador de progreso de la carga ? Barra de herramientas. Toques finales ? Logotipo de la empresa en la pantalla principal y en la pantalla de carga ? Creación de una unit para las declaraciones globales ? Pantalla de Splash. Se puede heredar de la anterior.. pero con un botón para anular. Se puede agregar al final 28 .

You're Reading a Free Preview

Descarga
scribd
/*********** DO NOT ALTER ANYTHING BELOW THIS LINE ! ************/ var s_code=s.t();if(s_code)document.write(s_code)//-->