6 de junio de 2008

Expandiendo expresiones con TextMerge()

La consulta

Hace unas semanas me consultaron de como poder combinar un archivo de plantilla con los datos de una tabla de VFP, para armar el texto personalizado de un correo electrónico. Mi respuesta rápida fue: "... con la función TextMerge() ...", y brindé el enlace a un artículo escrito por el amigo Esparta Palma sobre el tema:

-- Evolución del tratamiento de cadenas con TEXTMERGE --
http://comunidadvfp.blogspot.com/2014/10/evolucion-del-tratamiento-de-cadenas.html

La función TextMerge(), incorporada a partir de Visual FoxPro 7.0, y como bien lo indica Esparta en su artículo, tiene similares funcionalidades que los comandos SET TEXTMERGE y TEXT ... TEXTMERGE ... ENDTEXT, pero ... ¡¡¡ con anabólicos !!!, ya que TextMerge() permite la expansión de expresiones en forma anidada, y también permite recursividad si se le indica en su último parámetro.

La necesidad

La verdadera necesidad de quien me consultó, aparte de combinar simplemente los campos de la tabla donde estaban los datos de los destinatarios de los correos electrónicos, era incluir una tabla con datos de otra tabla, que obviamente era diferente por cada destinatario. Gracias a los anabólicos de la función TextMerge(), esto no sería inconveniente, ya que dentro de la expresión que se permite para la expansión, tranquilamente se puede incluir una función definida por el usuario. Como veremos en el ejemplo completo, se incluyen tres funciones definidad por el usuario que nos retornan la lista en cuestión, y resultados de cuentas y sumatorias.

La cosmética

Una vez logrado esto, también se deseaba que el correo electrónico enviado tenga formato HTML para una mejor apariencia. Entonces partiendo de un archivo de plantilla que contiene código HTML con las expresiones a expandir encerradas entre los delimitadores (por defecto "<<" para el inicio y ">>" para el final). Para el caso de la función que debia generar una lista, esta lo obtendiamos con una "tabla formateada" con código HTML. Para generar esta tabla utlizaríamos el código del artículo: "Convertir tablas a formato HTML".

El correo electrónico

Una vez obtenido el código HTML para cada correo electrónico, configuramos todas las propiedades del objeto que utilizamos para el envío de correos con sus respectivos valores. Generalmente la propiedad que se configura para darle formato HTML al cuerpo de un correo electónico es HtmlBody. Para mis pruebas de envío de correo electrónico utilicé CDO (Collaboration Data Objects) y el código del artículo "Mas sobre el envio de mensajes de correo electrónico desde Visual FoxPro" de este Blog.

La siguiente imagen nos muestra la apariencia del correo electrónico.



El código de ejemplo

A continuación se muestra un programa de ejemplo, en donde bien podemos generar el código HTML en tiempo de ejecución, o mejor aun, tomar ya un archivo HTML generado con algún editor que nos permitirá un mayor manejo en el formato de texto.

*-- Creo una Plantilla con código HTML
SET TEXTMERGE OFF
TEXT TO lcFile NOSHOW
<p><i><<ALLTRIM(Employees.TitleOfCourtesy)>><br>
<b><<ALLTRIM(Employees.FirstName)>> <<ALLTRIM(Employees.LastName)>></b><br>
<<ALLTRIM(Employees.Address)>><br>
<<ALLTRIM(Employees.City)>><br>
<<ALLTRIM(Employees.Country)>></i></p>
<hr>
<p>Tengo el agrado de hacerle llegar el listado del total de sus ordenes del año 1997 agrupadas por mes:</p>
<<AgregarListaResumen(Employees.EmployeeId)>>
<p>La cantidad total de sus ordenes son <b><<TotalOrdenesLista(Employees.EmployeeId)>></b> y 
suman un total de <b><<TotalFletesLista(Employees.EmployeeId)>></b> en concepto de fletes.</p>
<p>Lo saluda atentamente</p>
<p><u><i>El Gerente</i></u></p>
ENDTEXT
STRTOFILE(lcFile, FULLPATH("Template.htm"))

*-- Tomo la plantilla recien creada o bien una plantilla
*-- creada con alguna herramienta HTML 
lcFile = FILETOSTR(FULLPATH("Template.htm"))
OPEN DATABASE (HOME(2) + "Northwind\Northwind.dbc")
SELECT 0
USE Employees
SCAN ALL
 *-- Por cada Empleado genero el Mail con sus respectivas propiedades
 *-- ...
 *-- ...
 *-- Y genero el codigo HTML que ira en la propiedad BodyHtml del Mail
 lcBodyHtml = TEXTMERGE(lcFile)
 *-- Solo para este ejemplo guardo el archivo HTML para visualizarlo
 STRTOFILE(lcBodyHtml, FULLPATH(FORCEEXT("BodyHtml" + TRANSFORM(Employees.EmployeeId), "htm")))
ENDSCAN
USE IN SELECT("Employees")

PROCEDURE AgregarListaResumen(tnId)
 LOCAL lcHtml, loHtml
 *-- Listado de ordenes por Empleado
 SELECT MONTH(OrderDate) AS mes, COUNT(OrderId) AS Cant_Ordenes, SUM(Freight) AS Importe_Flete ;
  FROM Orders ;
  WHERE EmployeeId = tnId AND YEAR(OrderDate) = 1997 ;
  GROUP BY Mes ;
  ORDER BY Mes ;
  INTO CURSOR Lista
 loHtml = CREATEOBJECT("ClaseHtml")
 lcHtml = loHtml.AgregarTabla("Lista") + CHR(13) + CHR(10)
 RELEASE loHtml
 USE IN SELECT("Lista")
 RETURN lcHtml
ENDPROC

PROCEDURE TotalOrdenesLista(tnId)
 LOCAL lcHtml
 *-- Total de Ordenes por Empleado
 SELECT COUNT(OrderId) AS Tot_Ordenes ;
  FROM Orders ;
  WHERE EmployeeId = tnId AND YEAR(OrderDate) = 1997 ;
  INTO CURSOR TOTAL
 lcHtml = TRANSFORM(TOTAL.Tot_Ordenes)
 USE IN SELECT("Total")
 RETURN lcHtml
ENDPROC

PROCEDURE TotalFletesLista(tnId)
 LOCAL lcHtml
 *-- Total de Fletes por Empleado
 SELECT SUM(Freight) AS Tot_Flete ;
  FROM Orders ;
  WHERE EmployeeId = tnId AND YEAR(OrderDate) = 1997 ;
  INTO CURSOR TOTAL
 lcHtml = TRANSFORM(TOTAL.Tot_Flete)
 USE IN SELECT("Total")
 RETURN lcHtml
ENDPROC


*--------------------------------
DEFINE CLASS ClaseHtml AS CUSTOM
 *--
 *-- Clase para convertir una Tabla a formato HTML
 *-- Código original del artículo:
 *-- "Convertir tablas a formato HTML"
 *-- http://comunidadvfp.blogspot.com/2005/10/convertir-tablas-formato-html.html
 *--
 #DEFINE CR CHR(13)
 #DEFINE LF CHR(10)
 #DEFINE CR_LF CR + LF
 *-- Propiedades
 DIMENSION aCampos(1,1)
 nCampos = 0
 nRegistros = 0
 cNombreAlias = ""
 cArchivoHtml = ""
 cHtml = ""
 cColorBody = "#DDDDDD"
 cColorTitle = "#FFFFDD"
 cColorTable = "#FFFFFF"
 *--
 PROCEDURE AgregarTags(tcTexto, tcTag, tcOpciones)
  RETURN [<] + LOWER(tcTag) + IIF(EMPTY(tcOpciones),[],[ ] + tcOpciones) + [>] + ;
    ALLTRIM(tcTexto) + [</] + LOWER(tcTag) + [>]
 ENDPROC
 *--
 PROCEDURE IniciarTag(tcTag, tcOpciones)
  RETURN [<] + LOWER(tcTag) +  ;
    IIF(EMPTY(tcOpciones),[],[ ] + tcOpciones) + [>]
 ENDPROC
 *--
 PROCEDURE TerminarTag(tcTag)
  RETURN [</] + LOWER(tcTag) + [>]
 ENDPROC
 *--
 PROCEDURE AgregarTabla(tcNombreAlias)

  THIS.cNombreAlias = tcNombreAlias
  THIS.nCampos = AFIELDS(THIS.aCampos,THIS.cNombreAlias)

  LOCAL lcTexto, ln
  lcTexto = THIS.IniciarTag([table],[border="1" cellpadding="2" cellspacing="0"] + ;
    IIF(EMPTY(THIS.cColorTable),[],[ bgcolor="] + THIS.cColorTable + ["])) + CR_LF
  *-- Fila cabecera
  lcTexto = lcTexto + THIS.IniciarTag([tr], ;
    IIF(EMPTY(THIS.cColorTitle),[],[bgcolor="] + THIS.cColorTitle + ["])) + CR_LF
  *-- Recorro campos
  FOR ln = 1 TO THIS.nCampos
    lcTexto = lcTexto + THIS.AgregarTags(THIS.aCampos(ln,1),[th]) + CR_LF
  ENDFOR
  lcTexto = lcTexto + THIS.TerminarTag([tr]) + CR_LF
  *-- Filas de registros
  SELECT (THIS.cNombreAlias)
  SCAN ALL
    THIS.nRegistros = THIS.nRegistros + 1
    *-- Inicio fila
    lcTexto = lcTexto + THIS.IniciarTag([tr]) + CR_LF
    *-- Recorro los campos
    FOR ln = 1 TO THIS.nCampos
      lcTexto = lcTexto + THIS.AgregarTags(THIS.TomarValorCampo(ln),[td], ;
        THIS.AlinearSoloNumeros(ln)) + CR_LF
    ENDFOR
    *-- Fin fila
    lcTexto = lcTexto + THIS.TerminarTag([tr]) + CR_LF
  ENDSCAN
  lcTexto = lcTexto + THIS.TerminarTag([table])
  RETURN lcTexto
 ENDPROC
 *--
 PROCEDURE TomarValorCampo(tn)
  LOCAL lcTexto, lcTipo
  *-- Tipo de campo
  lcTipo = THIS.aCampos(tn,2)
  DO CASE
    CASE INLIST(lcTipo,"C","M") && Caracter y Memo
      lcTexto = ALLTRIM(EVALUATE(THIS.aCampos(tn,1)))
    CASE INLIST(lcTipo,"D","T","I","L")
      lcTexto = TRANSFORM(EVALUATE(THIS.aCampos(tn,1)))
    CASE INLIST(lcTipo,"Y") && Monetario
      *-- Quitar esta opción si no se desar el símbolo monetario
      *-- Este tipo también está contemplado en la opcion siguiente
      lcTexto = TRANSFORM(EVALUATE(THIS.aCampos(tn,1)))
    CASE INLIST(lcTipo,"N","F","B","Y")
      IF THIS.aCampos(tn,4) = 0
        lcTexto = ALLTRIM(STR(EVALUATE(THIS.aCampos(tn,1))))
      ELSE
        lcTexto = ALLTRIM(STR(EVALUATE(THIS.aCampos(tn,1)), ;
          THIS.aCampos(tn,3) + THIS.aCampos(tn,4) + 1, THIS.aCampos(tn,4)))
      ENDIF
    CASE INLIST(lcTipo,"G") && General
      lcTexto = [gen]
    CASE INLIST(lcTipo,"Q","V","W") && Nuevos tipos de VFP 9.0
      lcTexto = TRANSFORM(EVALUATE(THIS.aCampos(tn,1)))
    OTHERWISE
      lcTexto = ""
  ENDCASE
  lcTexto = THIS.TransformarTexto(lcTexto)
  IF lcTipo = "M" && Si es campo Memo
    *-- Transformo retornos de carro y saltos de lineas
    lcTexto = STRTRAN(lcTexto, CR_LF, [<br>])
    lcTexto = STRTRAN(lcTexto, CR, [<br>])
    lcTexto = STRTRAN(lcTexto, LF, [<br>])
  ELSE && Si no es campo Memo
    *-- Transformo espacios [ ] por [&nbsp;]
    lcTexto = STRTRAN(lcTexto, [ ], [&nbsp;])
  ENDIF
  RETURN lcTexto
 ENDPROC
 *--
 PROCEDURE TransformarTexto(tcTexto)
  *-- Transformo algunos caracteres "\<>&
  tcTexto = STRTRAN(tcTexto, ["], [&quot;])
  tcTexto = STRTRAN(tcTexto, [\], [&#092;])
  tcTexto = STRTRAN(tcTexto, [<], [&lt;])
  tcTexto = STRTRAN(tcTexto, [>], [&gt;])
  tcTexto = STRTRAN(tcTexto, [&], [&amp;])
  *-- Si es vacio o nulo
  IF EMPTY(tcTexto) OR ISNULL(tcTexto)
    tcTexto = [&nbsp;]
  ENDIF
  RETURN tcTexto
 ENDPROC
 *--
 PROCEDURE AlinearSoloNumeros(tn)
  LOCAL lcTexto, lcTipo
  lcTipo = THIS.aCampos(tn,2)
  DO CASE
    CASE INLIST(lcTipo,"N","F","B","Y","I")
      *-- Alinea a la derecha solo campos numéricos
      lcTexto = [align="right"]
    OTHERWISE
      lcTexto = []
  ENDCASE
  RETURN lcTexto
 ENDPROC

ENDDEFINE
*--------------------------------

El final

Espero que esto los ayude a encontrar diversos usos que se le puede dar a la función TextMerge(). Lo positivo de este artículo y esta solución, es que para toda ella utilizamos código publicado en este Blog.

A raíz de otras consultas recibidas, próximamente ampliaré sobre el tema del envio de correos electrónicos con CDO, con la inclusión de formato HTML, archivos adjuntos, y de imágenes y sonidos embebidos.

Hasta la próxima.

Luis María Guayán

No hay comentarios. :

Publicar un comentario

Los comentarios son moderados, por lo que pueden demorar varias horas para su publicación.