EJERCICIO 11 · REPORTS Y DATAITEMS

Reports y DataItems

Dificultad: 8/10
Ejercicios 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.al con ID 50100, nombre "Informe Flota Maquinaria", UsageCategory = ReportsAndAnalysis y ApplicationArea = All.
  • En el dataset, declara un dataitem(Maquinaria; "Maquinaria") con las columnas: "No." (alias No_Maquina), "Descripcion", "Categoria", "Estado", "Fecha Alta" y una columna calculada DiasEnParque que apunte a la variable global del mismo nombre.
  • En OnPreDataItem(): llama a SetCurrentKey("Descripcion") para ordenar alfabéticamente.
  • En OnAfterGetRecord(): calcula DiasEnParque := Today() - "Fecha Alta". Si "Fecha Alta" es 0D asigna 0 para evitar valores negativos.
  • Declara DiasEnParque como variable global de tipo Integer.
  • Añade un bloque rendering vacío (sin layout real por ahora): solo declara el tipo RDLC con 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 segundo dataitem(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." (alias Alquiler_No), "Fecha Inicio", "Fecha Fin", "Estado" y "No. Lineas" (el FlowField que ya tienes).
  • En OnPreDataItem() del hijo: ordena por "Fecha Inicio" descendente con SetAscending("Fecha Inicio", false).
  • Declara la variable global TotalAlquileresMaquina (Integer). En OnAfterGetRecord() 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: TotalMaquinas y TotalAlquileresFinal.
  • En el dataitem(Maquinaria; ...), añade la propiedad DataItemTableFilter = "Estado" = filter(<> Retirado) para excluir máquinas retiradas de forma fija e invisible para el usuario.
  • En OnAfterGetRecord() del nivel Maquinaria, incrementa también TotalMaquinas += 1.
  • En OnAfterGetRecord() del nivel CabeceraAlquiler, incrementa también TotalAlquileresFinal += 1 (además del contador por máquina del ejercicio anterior).
  • Escribe el trigger OnPreReport(): si tanto TotalMaquinas como el filtro de estado producen 0 registros (lo detectarás con Maquinaria.Count() = 0), lanza un Error con el mensaje 'No hay máquinas activas que incluir en el informe.'
  • Escribe el trigger OnPostReport(): muestra un Message con 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.
← Volver a Ejercicios