Artículo técnico

Generación de reportes Excel con plantillas en Delphi usando HotXLS

Un servicio de facturación que ayudé a depurar llenaba su plantilla de factura escribiendo en coordenadas fijas: nombre del cliente en B3, fecha de factura en E3, primera línea de detalle en la fila 10. Una tarde, una contadora insertó un bloque de dirección de pago encima de la sección de detalle del archivo de plantilla compartido. El generador siguió escribiendo en la fila 10, cada guardado reportó éxito, y durante dos días las facturas salientes llevaron partidas estampadas encima del texto de dirección mientras la fila de totales sumaba celdas vacías. Nada se cayó y ninguna línea de log se puso roja. La salida simplemente estaba mal, y solo un cliente lo notó.

Ese incidente es el problema de diseño central de los reportes basados en plantillas: las plantillas las editan personas que nunca leen su código fuente. HotXLS, una biblioteca nativa para Delphi y C++Builder que lee y escribe libros XLS y XLSX sin automatización de Excel, aporta las tres primitivas que un generador necesita para sobrevivir a ediciones de plantilla: búsqueda de texto de celda, copias de rango que preservan estilos e inserción de filas que desplaza estructuras dependientes. El resto de este artículo trata sobre cómo conectar esas primitivas para que una celda movida haga fallar la compilación ruidosamente en lugar de corromper facturas en silencio.

Por qué las coordenadas fijas fallan con la primera edición de plantilla

Una plantilla de hoja de cálculo es una interfaz de usuario propiedad del equipo de finanzas u operaciones, y la van a cambiar: una fila de logo más alta, una línea fiscal adicional, un bloque de dirección reordenado. Cualquier generador que direcciona celdas por números literales de fila y columna codifica una revisión específica de ese diseño y no tiene manera de detectar que la revisión cambió debajo. El formato de archivo no ayuda aquí: un guardado BIFF u OOXML tiene éxito tanto si sus números siguen significando lo mismo que el trimestre pasado como si no.

La corrección duradera es un contrato de placeholders. El autor de la plantilla escribe tokens como {{CUSTOMER}}, {{DATE}} y {{DETAIL_START}} en las celdas que el generador debe tocar, y el generador deriva cada coordenada en tiempo de ejecución a partir de dónde encuentra realmente esos tokens. Igual de importante es la regla de falla: si falta un token requerido, el trabajo debe detenerse antes de escribir un solo valor de datos de cliente. Una plantilla desviada debe producir un ticket de trabajo fallido, nunca un archivo entregado.

Ubicar placeholders con FindText y ReplaceText

Ambas familias de clases de HotXLS exponen búsqueda a nivel de hoja. FindText devuelve la fila y columna de la primera celda cuyo texto coincide, con una sobrecarga que agrega sensibilidad a mayúsculas; ReplaceText reemplaza todas las ocurrencias y devuelve el conteo de reemplazos. En el lado XLSX, anclar el llenado de una plantilla se ve así:

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  R, C: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open('invoice-template.xlsx') <> 1 then
      raise Exception.Create('Cannot open invoice template');
    Sheet := Book.Sheets[0];               // TXLSXSheets.Items is 0-based

    if not Sheet.FindText('{{CUSTOMER}}', R, C) then
      raise Exception.Create('Template drift: {{CUSTOMER}} anchor missing');
    Sheet.Cells[R, C].Value := 'ACME Corp';

    if Sheet.ReplaceText('{{DATE}}',
         FormatDateTime('yyyy-mm-dd', Date)) = 0 then
      raise Exception.Create('Template drift: {{DATE}} token missing');
    // detail expansion and save follow below
  finally
    Book.Free;
  end;
end;

Dos detalles importan. Primero, FindText y ReplaceText coinciden con el valor de texto de una celda; un token incrustado dentro de una cadena de fórmula es invisible para ellos, así que los tokens placeholder pertenecen a celdas simples, nunca dentro de fórmulas. Segundo, el conteo de reemplazos es su detector de desvío. Una plantilla que debería contener exactamente un token {{DATE}} pero reporta cero reemplazos fue editada, y lanzar una excepción en ese momento es precisamente lo que convierte el desplazamiento silencioso de diseño en una falla visible.

Clonar la fila de detalle sin perder estilos ni fórmulas

La sección de detalle de una factura crece con los datos, y el enfoque ingenuo, escribir valores en filas vacías, descarta los bordes, formatos numéricos y fórmulas por fila que el diseñador preparó. El patrón robusto mantiene en la plantilla una fila de muestra completamente formateada y la clona: CopyRange duplica estilos y fórmulas juntos, y después el generador sobrescribe solo las celdas de valor.

const
  DetailRow = 10;            // the formatted sample row in the template
var
  I: Integer;
begin
  // Open space before the totals block first, so the SUM range
  // below the detail band stretches together with the data.
  if Length(Items) > 1 then
    Sheet.InsertRows(DetailRow + 1, Length(Items) - 1);

  for I := 0 to High(Items) do
  begin
    if I > 0 then              // clone styles + formulas from the sample row
      Sheet.CopyRange(DetailRow, 1, DetailRow, 5, DetailRow + I, 1);
    Sheet.Cells[DetailRow + I, 1].Value := Items[I].Name;
    Sheet.Cells[DetailRow + I, 2].Value := Items[I].Qty;
    Sheet.Cells[DetailRow + I, 3].Value := Items[I].UnitPrice;
    Sheet.Cells[DetailRow + I, 4].Formula :=
      Format('B%d*C%d', [DetailRow + I, DetailRow + I]);  // no '=' prefix
  end;
end;

Observen con cuidado la asignación de fórmula: la propiedad Formula de XLSX recibe la expresión sin signo igual inicial, mientras que la fachada XLS espera '=B10*C10' asignado mediante Value. Mezclar las dos convenciones es el error de portabilidad más común entre ambas familias de clases, y falla en silencio: la celda simplemente contiene una cadena literal. Si la plantilla decora la banda de detalle con filas de título combinadas, recuerden que solo la celda superior izquierda de un área combinada lleva valor; las reglas de diseño en nuestro artículo sobre celdas combinadas en plantillas de reporte guiadas por diseño explican por qué las regiones combinadas deben mantenerse fuera de la banda de datos por completo.

Qué desplaza InsertRows automáticamente, y qué nunca desplazará

Insertar filas delante del bloque de totales es lo que mantiene un rango SUM estirándose a medida que crece la sección de detalle. En el lado XLSX, InsertRows desplaza rangos combinados, alturas de fila, hipervínculos, comentarios, paneles congelados, rangos de autofiltro, formatos condicionales, validaciones de datos, tablas, nombres definidos y anclas de imágenes y gráficos junto con las celdas. Sin embargo, la reescritura de fórmulas cubre solo referencias dentro de la misma hoja. Una fórmula en una hoja de resumen que apunta a la región movida conserva sus coordenadas antiguas, y precisamente por eso los totales consumidos entre hojas son más seguros expresados mediante nombres a nivel de libro; vean el artículo complementario sobre nombres definidos y fórmulas entre hojas para ese patrón.

El formato XLS heredado agrega un borde más filoso. HotXLS preserva tablas dinámicas, tablas de consulta y conexiones de datos externas en archivos BIFF como bloques de bytes sin procesar: hacen round trip por apertura y guardado sin cambios, pero no están modeladas, así que la inserción de filas no las mueve. Una plantilla que mantiene una tabla dinámica debajo de un bloque de detalle expansible se guardará sin advertencia mientras el rectángulo fuente de la tabla dinámica queda obsoleto en silencio. Mantengan contenido de tablas dinámicas y consultas en hojas donde el generador nunca inserta, y el problema desaparece por construcción.

Recalcular antes de entregar, o decidir explícitamente no hacerlo

HotXLS no evalúa fórmulas durante SaveAs. Excel recalcula cuando se abre el archivo (la fachada XLS expone CalculationMode y RecalcOnSave para dirigirlo), así que un reporte entregado a una persona no necesita un paso adicional. El cálculo cambia cuando el libro alimenta una máquina: la exportación CSV emite fórmulas como texto literal sin evaluarlas, y cualquier parser posterior que lea valores en caché verá resultados obsoletos o vacíos. Para esas rutas, evalúen en el servidor con Calculate, que parsea y calcula una expresión arbitraria contra el libro cargado:

var
  Total: Variant;
  LastDetail: Integer;
begin
  LastDetail := DetailRow + Length(Items) - 1;
  Total := Book.Calculate(Format('SUM(Invoice!D%d:D%d)',
    [DetailRow, LastDetail]));
  if (not VarIsNumeric(Total)) or
     (Abs(Total - ExpectedTotal) > 0.005) then
    raise Exception.Create('Invoice total does not match the order record');

  if Book.SaveAs('invoice-2026-0611.xlsx') <> 1 then
    raise Exception.Create('Save failed: check output path and permissions');
end;

Comparar el total calculado con el registro de negocio antes de guardar es un seguro barato. Convierte una factura incorrecta en un trabajo fallido, y un trabajo fallido es algo que un operador puede reintentar; una factura incorrecta en el buzón de un cliente es algo por lo que un gerente de cuenta debe disculparse.

Preguntas frecuentes

¿Una sola ruta de código puede llenar plantillas .xls y .xlsx?

No literalmente. HotXLS incluye dos familias de clases independientes: TXLSWorkbook se basa en interfaces y conteo de referencias con indexación de hojas basada en 1, mientras que TXLSXWorkbook es un objeto normal que deben liberar, con indexación de hojas basada en 0 y una convención de fórmulas separada. FindText, ReplaceText, CopyRange e InsertRows existen en ambos lados, así que el algoritmo se porta limpiamente, pero comprométanse con un formato por canalización u oculten las diferencias detrás de un adaptador propio delgado.

¿Cuántas filas de detalle puede manejar este patrón?

Clonar una fila con estilo unos cuantos miles de veces no es notable en hardware actual. Cuando las bandas de detalle llegan a conteos de seis cifras, la ruta de guardado se vuelve el cuello de botella; activar StreamingWrite transmite el XML de hoja directamente al paquete de salida, como se cubre en nuestro artículo sobre escrituras en streaming para trabajos batch de servidor.

¿Qué pasa con un gráfico anclado debajo del bloque de detalle?

En el lado XLSX, tanto el ancla del gráfico como sus referencias de serie se desplazan cuando InsertRows se ejecuta encima, así que un gráfico bajo la fila de totales permanece unido a los datos correctos. En el lado XLS, los gráficos viven en hojas de gráfico dedicadas y las estructuras no modeladas, como tablas dinámicas, no se desplazan en absoluto; otra razón para separar hojas de presentación de la hoja que el generador expande.

La generación basada en plantillas es la forma de mayor apalancamiento para producir libros pulidos desde Delphi, porque permite que los diseñadores sean dueños de la apariencia mientras el código es dueño de los datos. Las primitivas de búsqueda, copia e inserción mostradas aquí se entregan todas con HotXLS Component, junto con el motor de fórmulas usado para validación previa a la entrega.