lunes, 24 de septiembre de 2018

¿Cómo usar dos tablas de distintas bases de datos en la misma consulta?

Para adjuntar una tabla a un DataSet, en este caso TZQuery, y que no produzca un error, la solución que no encontré en ningún lado, la descubrí mediante el sistema de prueba y error hasta que salga. Y como suele suceder cuando no se encuentra algo en toda la web, es porque ese algo, es muy obvio y este caso no fue la excepción.

Primero establecemos la conexión con una de las dos bases de datos mediante el componente TZConnection. Luego hacemos una consulta para adjuntar la base de datos, la abrimos y la cerramos, listo, ya está adjuntada. Luego escribimos la consulta que necesitemos.

Ejemplo:

ZQ.Close;
ZQ.SQL.Text:='ATTACH DATABASE '+QuotedStr(strDB)+' AS realgestdb;';
ZQ.Open;
ZQ.Close;
ZQ.SQL.Text:='SELECT cfecha, cprovid, nombre, ccomp, realgestdb.comprob.ccomprobalias, cletracomp, '+
'cnrocomp, ccaeocai, ccai, cuit, cnetogravado, cnogravado, cimpinternos, cpercib, cperciva, ctasaiva, '+
'civa, cnetogravado1, ctasaiva1, civa1, cnetogravado2, ctasaiva2, civa2, ctotal '+
'FROM ccompras '+
'INNER JOIN cprov ON cprovid=provid '+
'INNER JOIN realgestdb.comprob ON ccomp=realgestdb.comprob.id '+
'WHERE cfecha BETWEEN '+QuotedStr(desde)+' AND '+QuotedStr(hasta)+
'ORDER BY cfecha, cprovid ;';
ZQ.Open;


Las tres primera lineas realizan el ATTACH DATABE y ya queda disponible para cualquier consulta que se realice en el mismo dataset, hasta que se des adjunte, para ello:

ZQ.Close;
ZQ.SQL.Text:='DETACH DATABASE '+QuotedStr('realgestdb')+';';
ZQ.Open;
ZQ.Close;


Es importante para adjuntar, enviar el path completo de la base de datos y entre comillas simples, para eso nada más cómodo que la función QuotedStr. En este caso strDB es una variable del tipo string que contiene el path completo de la base de datos a adjuntar. Luego con AS le establecemos un alias para luego referenciarla en las consultas SQL. El alias puede ser cualquier nombre.
Pero ojo, que para realizar el DETACH DATABASE se utiliza el Alias, no el path completo de la base de datos. Esto se debe que, al menos SQLite, permite adjuntar variar veces una misma base de datos bajo distintos Alias (AS).

La cuarta línea cierra la consulta. La quinta, cambia la consulta y para acceder al campo ccomprobalias de la tabla comprob de la base de datos adjuntada bajo el alias de realgestdb lo hacemos de la forma Alias.tabla.campo.

LazReport: cambiar el alto de una banda en tiempo de ejecución.


En este caso se trata de un reporte del libro I.V.A. y necesito cambiar el alto de la banda de los datos maestros según tengan 1, 2 o 3 discriminaciones o tasas de IVA en un mismo comprobante, para no desperdiciar espacio, entonces si un comprobante tiene un solo IVA el alto (height) será de 15, si son dos, será de 30 y si son tres será de 45 pixeles.
Como se ve en la imagen, durante el diseño del reporte se toma el máximo, será luego en run time que variará el valor de la banda MasterData1.

Para ello nos valemos del evento del reporte TfrReport OnBeginBand y su nombre lo indica todo: tareas a realizar al comienzo de la banda. Lo resolví muy simple de la siguiente manera:

procedure TFLibCompras.frRepBeginBand(Band: TfrBand);
begin
if ((Band.Name='MasterData1') and (ZQ.FieldByName('cnetogravado2').AsCurrency<>0)) then
  begin
    Band.Height:=45;
    Exit;
end;
if ((Band.Name='MasterData1') and (ZQ.FieldByName('cnetogravado1').AsCurrency<>0)) then
  begin
    Band.Height:=30;
    Exit;
  end;
if ((Band.Name='MasterData1') and (ZQ.FieldByName('cnetogravado1').AsCurrency=0)) then
  begin
    Band.Height:=15;
    Exit;
  end;
end;


El código es el que tengo funcionando y lo expongo a modo de ejemplo, la condición para establecer el alto de la banda puede ser cualquiera. También en el mismo evento podrían modificarse otros componentes del reporte.


Y así queda parte del reporte, con valores de prueba.

viernes, 17 de agosto de 2018

TMemo: evitar el enter.

Si bien una de sus propiedades es WantReturns y le podemos asignar False para evitar la línea nueva, a veces no funciona, al menos con la versión 1.6 de Lazarus y 3.0 de FreePascal usando el widget Gtk-2.
No obstante la solución es muy simple utilizando el evento OnKeyPress de TMemo:

procedure TForm1.mmDetalleKeyPress(Sender: TObject; var Key: char);
begin
  if (Key in [#13,#10]) then Key:=#0;
end;


mmDetalle es la variable del tipo TMemo.
Si la tecla presionada es Enter se anula.

TEdit: rellenar con ceros u otro caracter.

La función AddChar incluida en la unidad StrUtils hace esto.

function AddChar(C: Char; const S: string; N: Integer): string; 

El primer parámetro es el caracter con el cual vamos a rellenar los espacios del string, el segundo parámetro es el string a rellenar y el tercero la cantidad de lugares a rellenar (lo normal es que sea la misma cantidad de caracteres del string).

Ejemplo: un TEdit para que el usuario ingrese el número de un comprobante y al abandonar el TEdit, el mismo complete con ceros a la izquierda. Nos valemos del método OnExit del TEdit:

procedure TForm1.edNroExit(Sender: TObject);
begin
  edNro.Text:=AddChar('0',edNro.Text,8);
end;


El edNro es la variable del tipo TEdit y la propiedad MaxLength es 8, el resto de las propiedades son las predefinidas por el IDE Lazarus.
En lugar de pasar el número 8 se podría pasar la propiedad edNro.MaxLength.

jueves, 16 de agosto de 2018

TDBLookupComboBox: Validar.

Validar un combo del tipo TDBLookupComboBox con las siguientes propiedades:

Style: cdDropDown
ArrowKeysTravers: True
AutoComplete: True

Es una configuración muy cómoda para el usuario, pero permite que deje el valor del combo en blanco o con un valor que no coincide con ningún elemento, por el motivo que sea (distracción o "a ver que pasa").
Nota: para combos con pocos elementos conviene utilizar el estilo (style) csDropDownList y deshabilitar la propiedad de autocompletado.
Volviendo al tema de inicio, la validación se realiza a través del evento OnExit:

procedure TFCRegCompras.cmbProvExit(Sender: TObject);
begin
  if cmbProv.KeyValue=Null then
  begin
    ShowMessage('Seleccione un proveedor.');
    cmbProv.SetFocus;
  end;

end;

Desde ya es muy conveniente antes de cargar y habilitar el combo, verificar que el dataset no esté vacío, si esto sucediese, el programa no se colgaría, pero una vez que el usuario entra al combo, no podría salir nunca porque KeyValue siempre sería Null y al utilizar SetFocus vuelve a entrar al combo. Es una validación muy útil pero deben tomarse ciertas precauciones. Por ejemplo, en un programa de registración de compras, donde otros combos y valores dependen del proveedor seleccionado, usando el evento OnChange del combo de proveedores, es fundamental tomar este tipo de medidas.

lunes, 13 de agosto de 2018

TDBGrid: ordenar por columnas ASC y DESC con flechas.



Anteriormente escribí acerca de ordenar las columnas de un DBGrid de forma simple y en un solo sentido. Ahora veremos como hacerlo de forma tal que además de poner en negrita el título de la columna que posee el orden, le agregue una flecha ascendente (arriba) o una descendente (abajo) y que si el usuario hace click en el título de una columna y la misma es la que está ordenando la grilla, entonces cambie el orden de la misma, es decir, si está ordenada ascendentemente pasará a ordenarse en sentido descendente.

Para las flechas necesitamos un TImageList.


Este componente se encuentra en la paleta Common Controls (el último que se ve en la imagen).


Desde el inspector de objetos estableceremos las propiedades Height (alto) y Width (ancho) de ImageList1 en 16 pixeles. Luego cargamos las imágenes de las flechas en dicho componente.

Éstas son las flechas utilizadas:


Ahora debemos conectar la lista de imágenes con la grilla.


Seleccionamos el DBGrid en el inspector de objetos y en TitleImageList asignamos ImageList1. Luego en "Eventos" creamos el procedimiento para OnTitleClick:

procedure TForm1.DBGrid1TitleClick(Column: TColumn);
var
  i:Integer;
  AscDes:String;
begin
  if Column.Title.ImageIndex=0 then AscDes:=' DESC ' else AscDes:=' ASC ';
  for i:=0 to DBGrid1.Columns.Count-1 do
  begin
    DBGrid1.Columns.Items[i].Title.Font.Style:=[];
    DBGrid1.Columns.Items[i].Title.ImageIndex:=-1;
  end;
  ZQuery1.Close;
  sqlOrder:=' ORDER BY '+Column.FieldName+AscDes+';';
  sqlgrid:=sqlSelect+sqlWhere+sqlOrder;
  ZQuery1.SQL.Text:=sqlgrid;
  ZQuery1.Open;
  Column.Title.Font.Style:=[fsBold];
  if AscDes=' DESC ' then Column.Title.ImageIndex:=1 else Column.Title.ImageIndex:=0;
end;


Las variables sqlOrder, sqlGrid, sqlSelect y sqlWhere las defino como privadas para la clase del formulario, son del tipo string y su utilización es más que obvia, permitiendo por ejemplo, ordenar manteniendo un filtro definido por el usuario (sqlWhere).
Atención a no confundir Column con Columns, la primera viene como parámetro, es la columna en la cual el usuario hizo click. En cambio Column"s" es miembro de TDBgrid.
Lo primero que hacemos es averiguar si la columna (variable Column del tipo TColumn) está ordenada de manera descendente, caso contrario la damos como ascendente y establecemos el valor correspondiente a la variable local AscDes que luego utilizaremos en la consulta SQL.
Lo siguiente es recorrer las columnas (Columns) del grid y quitarles el [bold] y la flecha.
Cerramos el Query, armamos el string de la consulta y reabrimos la misma.
Finalmente marcamos en negrita el título de la columna y se asignamos la imagen de la flecha que le corresponde.










jueves, 5 de julio de 2018

TListBox: buscar.


El siguiente código se ejecuta cuando se presiona el botón buscar, si la cadena de texto ingresada coincide con algún ítem de la lista, el mismo es seleccionado.

procedure TFCProv.BBuscarClick(Sender: TObject);
var
  nombre, item:String;
  i, cant:Integer;
begin
  nombre:=UpperCase(edBuscar.Text);
  cant:=Length(nombre);
  for i:=0 to lbProv.Count-1 do
  begin
    item:=UpperCase(lbProv.Items[i]);
    if nombre=LeftStr(item,cant) then
    begin
      lbProv.ItemIndex:=i;
      Break;
    end;
  end;
end;


En este caso la lista contiene nombres de proveedores, está ordenada, convierte a mayúsculas la cadena a buscar y el elemento a comparar. La variable entera cant se utiliza para saber la cantidad de caracteres que ingresó el usuario y hacer la búsqueda parcial, es decir, si el usuario ingresa "Ac" y hay en la lista un elemento "ACME" se compara "AC" con "AC". Como lo que se quiere es solo marcar el elemento en la lista (lbProv.ItemIndex), se hace y se utiliza break para salir.

miércoles, 4 de julio de 2018

TDBLookupComboBox validar e impedir NULL.

Lo más simple es definir la propiedad Style en csDropDownList pero no siempre es lo que necesitamos. Por ejemplo cuando son muchos los registros que se cargan en el combo, le damos al usuario la posibilidad de que escriba las primeras letras y le traiga coincidencias, esto se logra estableciendo a True la propiedad AutoComplete. La contra es que el usuario puede abandonar el combo sin ninguna coincidencia, dejando el KeyValue (que es del tipo Variant) en Null. Esto a su vez lanzará un error si la validación la hacemos con números enteros, por ejemplo if combo.KeyValue<0 then...

Opción 1: no permitir que el usuario abandone el combo sin la correcta selección de un elemento.

Definir el evento OnExit.

procedure TFCProv.cmbLocalidadExit(Sender: TObject);
begin
  if cmbLocalidad.KeyValue=Null then
  begin
    ShowMessage('Seleccione una localidad.');
    cmbLocalidad.SetFocus;
  end;
end;


Opción 2: en algún caso en que se permite no seleccionar nada en el combo y debemos guardar 0 (cero) en la base de datos.


Por ejemplo, las actividades de 2 a 5 son opcionales y al crear el Form se les estable el valor de KeyValue en 0 para que no muestre nada. (La normalización y desnormalización de bases de datos está fuera del alcance de este ejemplo.).

if cmbActividad2.KeyValue=Null then
  DMProv.ZQProv.FieldByName('actividad2').AsInteger:=0
else
  DMProv.ZQProv.FieldByName('actividad2').AsInteger:=cmbActividad2.KeyValue;


Esta validación se hace al momento de guardar el registro y en todos los combos excepto el 1, si KeyValue es Null se guarda 0, caso contrario el valor de KeyValue.

TEdit solo números.

Para que una variable del tipo TEdit acepte solo números hay que definir el evento OnKeyPress de esta forma:

procedure TFCProv.edit1KeyPress(Sender: TObject; var Key: char);
begin
if not (Key in ['0'..'9',#8, #9]) then Key:=#0;
end;


De esta forma el usuario solo podrá ingresar números y borrar.

Si bien el tipo TEdit tiene una propiedad NumbersOnly, la misma no funciona si se utiliza el widget GTK-2 en sistemas operativos GNU con kernel Linux.

#8 es para permitir borrar y #9 la tecla de tabulación, cualquier otra tecla se anula con Key:=#0.

miércoles, 20 de junio de 2018

LazReport: cambiar el valor de numeración de las hojas.

En LazReport podemos utilizar la variable propia del generador de reportes [PAGE#] para imprimir el número de página, generalmente lo hacemos en la banda pie de página. De esta forma la primera página será la número 1 y las siguientes incrementarán su valor en 1.
Si queremos que la primera página tenga otro valor, podemos definir una variable en el reporte, en este caso llamada hojanumero y en el evento GetValue pasarle el valor que deseamos. Luego sumamos esta variable a [PAGE#] y le restamos 1.

En Lazarus podemos valernos cómodamente de un TSpinEdit para que el usuario ingrese el número inicial y establecemos los valores mínimos y máximos con lo cual nos ahorramos código de validación.


El valor de SpinEdit1.Value lo pasamos a la variable del reporte hojanumero.

En el reporte, añadimos un cuadro de texto en la banda pie de página e ingresamos lo siguiente:

Hoja N°[ ( [PAGE#] + [hojanumero] -1) ]

Otra opción es restarle 1 al valor antes de enviarlo al reporte y quitar el -1 de la fórmula anterior.