EJERCICIO 11 · REPORTS Y DATAITEMS
Reports y DataItems
Dificultad: 8/10Ejercicios anteriores
¿Qué tienes construido hasta ahora?
- Modelo completo: tablas, triggers, FlowFields, FlowFilters, TableRelations y Queries.
- Interfaz completa: pages de tipo List, Card, ListPart, CardPart con SubPageLink y FactBoxes.
- Queries
"Ranking Alquileres Maquinaria"y"Resumen Historial Maquinaria"que calculan totales de todas las máquinas a la vez. - Codeunit
"Maquinaria Stats"con comparativa trimestral y ranking por alquileres. - Lo que aún no tienes: un documento imprimible que el responsable de flota pueda entregar o archivar.
Las Queries del ejercicio anterior dan al desarrollador los datos que necesita para procesar. Pero el responsable de flota necesita algo diferente: un PDF con el listado completo de maquinaria, sus alquileres activos y el historial de estados para entregarlo en reuniones o archivarlo. Para eso existen los Reports.
Antes de empezar, revisa cómo se organizan los tres niveles del Report que vas a construir:
report 50100 "Informe Flota
Maquinaria"
objeto report
trigger OnPreReport() — valida fechas del
requestpage
1 vez al inicio
dataitem(Maquinaria; "Maquinaria")
bucle × máquina
trigger OnPreDataItem() — SetCurrentKey, filtro
estado
1 vez por nivel
trigger OnAfterGetRecord() — calcular
DiasDesdeAlta
× cada registro
dataitem(CabeceraAlquiler; "Cabecera
Alquiler")
bucle × alquiler
DataItemLink = "No. Maquina Principal" = FIELD("No.")
enlace padre→hijo
trigger OnPostDataItem() — acumula
TotalAlquileresMaquina
1 vez al cerrar nivel
trigger OnPostReport() — mensaje de
confirmación
1 vez al final
rendering { layout(RDLCLayout) { ... } }
plantilla visual
Ejercicio 01
Report · DataItem · OnPreDataItem · OnAfterGetRecord
Crea el Report de flota con DataItem raíz
Objetivo: Definir la estructura base del report
"Informe Flota Maquinaria"
con el dataset raíz sobre "Maquinaria", ordenado por descripción, con un campo
calculado que muestre los días que lleva la máquina en el parque desde su fecha de alta.
Instrucciones
- Crea
InformeFlota.Report.alcon ID50100, nombre"Informe Flota Maquinaria",UsageCategory = ReportsAndAnalysisyApplicationArea = All. - En el
dataset, declara undataitem(Maquinaria; "Maquinaria")con las columnas:"No."(aliasNo_Maquina),"Descripcion","Categoria","Estado","Fecha Alta"y una columna calculadaDiasEnParqueque apunte a la variable global del mismo nombre. - En
OnPreDataItem(): llama aSetCurrentKey("Descripcion")para ordenar alfabéticamente. - En
OnAfterGetRecord(): calculaDiasEnParque := Today() - "Fecha Alta". Si"Fecha Alta"es0Dasigna0para evitar valores negativos. - Declara
DiasEnParquecomo variable global de tipoInteger. - Añade un bloque
renderingvacío (sin layout real por ahora): solo declara el tipoRDLCcon un nombre de archivo de referencia.
Pista de Código
AL — InformeFlota.Report.al (estructura base)
report 50100 "Informe Flota Maquinaria"
{
Caption = 'Informe de Flota de Maquinaria';
UsageCategory = ReportsAndAnalysis;
ApplicationArea = All;
dataset
{
dataitem(Maquinaria; "Maquinaria")
{
column(No_Maquina; "No.") { Caption = 'Nº Máquina'; }
column(Descripcion; "Descripcion") { Caption = 'Descripción'; }
column(Categoria; "Categoria") { Caption = 'Categoría'; }
column(Estado; "Estado") { Caption = 'Estado'; }
column(FechaAlta; "Fecha Alta") { Caption = 'Fecha de Alta'; }
column(DiasEnParque; DiasEnParque) { Caption = 'Días en el Parque'; }
trigger OnPreDataItem()
begin
SetCurrentKey("Descripcion"); // ordenamos A → Z
end;
trigger OnAfterGetRecord()
begin
if "Fecha Alta" = 0D then
DiasEnParque := 0
else
DiasEnParque := Today() - "Fecha Alta";
end;
}
}
rendering
{
layout(RDLCLayout)
{
Type = RDLC;
LayoutFile = 'layouts/InformeFlota.rdlc';
}
}
var
DiasEnParque: Integer; // variable global del Report
}
💡 Recuerda: Un DataItem es un bucle automático: BC
recorre todos los registros de
"Maquinaria" sin que escribas
FindSet ni Next. El OnAfterGetRecord se ejecuta
en cada iteración. Las variables globales del Report mantienen su valor entre iteraciones,
lo que las hace útiles para acumular totales.
Ejercicio 02
DataItem anidado · DataItemLink · OnPostDataItem
Añade el nivel de alquileres anidado
Objetivo: Anidar un segundo
dataitem de
"Cabecera Alquiler" dentro del de "Maquinaria", enlazado
por DataItemLink, y usar OnPostDataItem() para acumular
el total de alquileres de cada máquina en una variable.
Instrucciones
- Dentro del
dataitem(Maquinaria; ...), añade un segundodataitem(CabeceraAlquiler; "Cabecera Alquiler"). - Configura
DataItemLink = "No. Maquina Principal" = FIELD("No.")para que solo aparezcan los alquileres de la máquina activa en cada iteración del padre. - Añade las columnas:
"No."(aliasAlquiler_No),"Fecha Inicio","Fecha Fin","Estado"y"No. Lineas"(el FlowField que ya tienes). - En
OnPreDataItem()del hijo: ordena por"Fecha Inicio"descendente conSetAscending("Fecha Inicio", false). - Declara la variable global
TotalAlquileresMaquina(Integer). EnOnAfterGetRecord()del hijo incrementa el contador:TotalAlquileresMaquina += 1. - En
OnPostDataItem()del hijo, resetea el contador a 0 para la siguiente máquina:TotalAlquileresMaquina := 0.
Pista de Código
AL — fragmento dataset con DataItem anidado
dataitem(Maquinaria; "Maquinaria")
{
// ... columnas del nivel padre (Ejercicio 01)
column(TotalAlq; TotalAlquileresMaquina) { Caption = 'Total Alquileres'; }
// ── DataItem hijo: un nivel más adentro ─────────────────────
dataitem(CabeceraAlquiler; "Cabecera Alquiler")
{
DataItemLink = "No. Maquina Principal" = FIELD("No.");
column(Alquiler_No; "No.") { Caption = 'Nº Alquiler'; }
column(Alq_FechaIni; "Fecha Inicio") { Caption = 'Fecha Inicio'; }
column(Alq_FechaFin; "Fecha Fin") { Caption = 'Fecha Fin'; }
column(Alq_Estado; "Estado") { Caption = 'Estado'; }
column(Alq_NoLineas; "No. Lineas") { Caption = 'Nº Líneas'; }
trigger OnPreDataItem()
begin
SetCurrentKey("Fecha Inicio");
SetAscending("Fecha Inicio", false); // más recientes primero
end;
trigger OnAfterGetRecord()
begin
TotalAlquileresMaquina += 1; // cuenta alquileres de esta máquina
end;
trigger OnPostDataItem()
begin
TotalAlquileresMaquina := 0; // reset para la siguiente máquina
end;
}
// ... triggers del nivel padre
}
⚠️ Ojo con el reset: Si no reseteas
TotalAlquileresMaquina
en OnPostDataItem(), el contador acumulará todos los alquileres de todas
las máquinas en una sola cifra. El reset en OnPostDataItem garantiza que
la variable empiece en 0 para cada nueva máquina del nivel padre.
Ejercicio 03
OnPreReport · DataItemTableFilter · OnPostReport
Filtros fijos, validación y mensaje final
Objetivo: Añadir los triggers globales del Report para validar que
el informe tiene sentido antes de ejecutarse, aplicar un filtro fijo que excluya
las máquinas retiradas sin que el usuario pueda modificarlo, y mostrar un resumen
al terminar.
Instrucciones
- Declara dos variables globales de tipo
Integer:TotalMaquinasyTotalAlquileresFinal. - En el
dataitem(Maquinaria; ...), añade la propiedadDataItemTableFilter = "Estado" = filter(<> Retirado)para excluir máquinas retiradas de forma fija e invisible para el usuario. - En
OnAfterGetRecord()del nivelMaquinaria, incrementa tambiénTotalMaquinas += 1. - En
OnAfterGetRecord()del nivelCabeceraAlquiler, incrementa tambiénTotalAlquileresFinal += 1(además del contador por máquina del ejercicio anterior). - Escribe el trigger
OnPreReport(): si tantoTotalMaquinascomo el filtro de estado producen 0 registros (lo detectarás conMaquinaria.Count() = 0), lanza unErrorcon el mensaje 'No hay máquinas activas que incluir en el informe.' - Escribe el trigger
OnPostReport(): muestra unMessagecon el total de máquinas procesadas y el total de alquileres incluidos.
Pista de Código
AL — InformeFlota.Report.al (triggers globales y filtro
fijo)
dataset
{
dataitem(Maquinaria; "Maquinaria")
{
// Filtro fijo: el usuario no puede ver ni cambiar esto
DataItemTableFilter = "Estado" = filter(<> Retirado);
// ... columnas y DataItem hijo del ejercicio anterior
trigger OnAfterGetRecord()
begin
TotalMaquinas += 1;
if "Fecha Alta" = 0D then
DiasEnParque := 0
else
DiasEnParque := Today() - "Fecha Alta";
end;
}
}
// ── Trigger global: se ejecuta UNA VEZ antes de todo ────────────
trigger OnPreReport()
begin
Maquinaria.SetFilter("Estado", '<>%1', Maquinaria."Estado"::Retirado);
if Maquinaria.IsEmpty() then
Error('No hay máquinas activas que incluir en el informe.');
end;
// ── Trigger global: se ejecuta UNA VEZ al terminar todo ─────────
trigger OnPostReport()
begin
Message(
'Informe generado.\nMáquinas procesadas: %1\nAlquileres incluidos: %2',
TotalMaquinas,
TotalAlquileresFinal
);
end;
var
DiasEnParque : Integer;
TotalAlquileresMaquina: Integer;
TotalMaquinas : Integer;
TotalAlquileresFinal : Integer;
💡 Recuerda:
DataItemTableFilter aplica un filtro
permanente e invisible al DataItem, igual que SourceTableView
en una page. El usuario no puede verlo ni eliminarlo desde el requestpage.
Es la forma correcta de restringir el alcance del informe a un subconjunto de datos
sin depender de que el usuario aplique los filtros correctos.
💡 Bonus: Ahora que el Report existe, puedes añadir una acción
ImprimirInformeFlota en "Maquinaria List" con el trigger
OnAction que llame a
Report.Run(Report::"Informe Flota Maquinaria"). El usuario podrá
generar el PDF directamente desde la lista de maquinaria con un solo clic,
cerrando así el ciclo completo del proyecto: datos → lógica → interfaz → documento.