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

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

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

El campo IDCliente no es editable (es autonumérico). Primero.necesitamos colocar las tablas en el proyecto. Además. de manera que he utilizado un control DBText en lugar de un DBEdit. mostrando el campo NombreYApellido. 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). 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. 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. he colocado sólo dos columnas en la grilla: NombreYApellido (con otro título) y Telefonos. 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”. ? 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. Creamos entonces el Módulo de Datos y ponemos las tablas y Fuentes de Datos necesarias. 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 .

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

nombre de la tabla externa. que se selecciona en el menú File|Working Directory. la necesidad de una variable bandera global.. aceptar todo.VAL. En especial.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. porque ya se aceptó y no hay forma de borrarla. con este nombre se guardará la regla en el archivo . 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. solamente para ver las facturas y sus detalles ? Anulación de facturas 27 . debemos dar un nombre a la Regla de Integridad. 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. únicamente se muestran como tablas pasibles de ser referenciadas las que están en el directorio especificado por el alias :WORK:.. o una edición falsa. NOTA: por algún error en la implementación del Database Desktop. ? ? Qué pasa cuando no alcanza el stock ? ? Validación del tipo de factura. podemos seleccionar el alias en uso como Working Directory. etc. poner el número de factura en un archivo . ? ? 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. Para nuestro ejemplo de la tabla de Facturas referenciando a la tabla de clientes. la fecha. Puntos para tener en cuenta: ? ? Aceptar todo: si hay registros pendientes. Aquí tendría que actualizar el archivo INI con el nuevo número de factura. Una vez seleccionados los campos.

Se puede heredar de la anterior. pero con un botón para anular... con lista de acciones para compartir con el menú. 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.Igual que la de consulta. con indicador de progreso de la carga ? Barra de herramientas. Se puede agregar al final 28 .

Sign up to vote on this title
UsefulNot useful