EJERCICIO 07 · TRIGGERS DE TABLA

Triggers de Tabla

Dificultad: 6/10
Ejercicios anteriores

¿Qué tienes construido hasta ahora?

  • Enum "Rental Status" con los estados de maquinaria, extensible.
  • Tabla "Maquinaria" (50100): FlowFields con "Date Filter" conectado a "No. Alquileres", "Tiene Historial" y "Ultimo Cambio Estado".
  • Tabla "Cabecera Alquiler" (50103): FlowFields "No. Lineas" y "Descripcion Maquina".
  • Tabla "Linea Alquiler" (50101): PK compuesta, TableRelation hacia ambas tablas.
  • Tabla "Historial Estado Maquina" (50102): AutoIncrement, TableRelation condicional según "Tipo Origen".
  • Codeunit "Maquinaria Stats" (50100) con comparativa trimestral usando SetRange + CalcFields.
  • Pages "Maquinaria List", "Maquinaria Card" y "Cabecera Alquiler Card".

Hasta ahora el modelo de datos está completo: tablas, relaciones, FlowFields y FlowFilters. Pero las tablas no reaccionan a nada todavía. Si alguien crea un alquiler, la tabla no asigna ningún valor automáticamente. Si alguien borra una cabecera con líneas activas, BC lo permite sin protestar.

En este ejercicio vas a añadir los triggers de tabla que dan vida al proyecto, repartidos entre las tres tablas principales. El diagrama muestra qué trigger cubre cada momento del ciclo de vida de un alquiler:

OnInsert
Asignar fecha y estado inicial
OnValidate
Calcular días, validar fechas
OnModify
Auditar usuario y registrar cambio en historial
OnDelete
Impedir borrar con líneas activas
OnRename
Bloquear cambio de PK
Ejercicio 01
OnInsert · OnModify

Inicialización y auditoría en Cabecera Alquiler

Objetivo: Hacer que "Cabecera Alquiler" se autocomplete al crearse (fecha de inicio = hoy, estado = Disponible) y que registre el usuario que hizo el último cambio cada vez que se modifique.

Instrucciones

  • Abre CabeceraAlquiler.Table.al y añade dos campos físicos nuevos: "Creado Por" (Code[50]) y "Modificado Por" (Code[50]), ambos con Editable = false.
  • Escribe el trigger OnInsert() al nivel raíz de la tabla (fuera de fields y keys): asigna Rec."Fecha Inicio" := Today, Rec."Estado" := Rec."Estado"::Disponible y Rec."Creado Por" := UserId.
  • Escribe el trigger OnModify(): asigna Rec."Modificado Por" := UserId.

Pista de Código

AL — CabeceraAlquiler.Table.al (triggers raíz)
    // ── Campos nuevos (dentro de fields) ────────────────────────
        field(9;  "Creado Por";    Code[50]) { Editable = false; }
        field(10; "Modificado Por"; Code[50]) { Editable = false; }

    // ── Triggers raíz (fuera de fields y keys) ──────────────────
    trigger OnInsert()
    begin
        Rec."Fecha Inicio" := Today;
        Rec."Estado"       := Rec."Estado"::Disponible;
        Rec."Creado Por"   := UserId;
    end;

    trigger OnModify()
    begin
        Rec."Modificado Por" := UserId;
    end;
⚠️ Recuerda: Los triggers de tabla se ejecutan siempre, independientemente de desde qué pantalla llegue la operación. El OnModify se disparará también desde el codeunit "Maquinaria Stats" o desde cualquier otro proceso que modifique la tabla.
Ejercicio 02
OnValidate · Cascada

Validación de fechas y cálculo de días en Linea Alquiler

Objetivo: Añadir a la tabla "Linea Alquiler" un campo "Dias Alquiler" que se calcule automáticamente cuando el usuario introduzca la fecha de fin, y que impida fechas de fin anteriores a la de inicio.

Instrucciones

  • Abre LineaAlquiler.Table.al y añade el campo "Dias Alquiler" (Integer) con Editable = false.
  • En el campo "Fecha Fin", añade el trigger OnValidate():
    • Si Rec."Fecha Fin" < Rec."Fecha Inicio" lanza un Error con el mensaje 'La fecha de fin no puede ser anterior a la de inicio.'
    • Si la validación pasa, calcula: Rec."Dias Alquiler" := Rec."Fecha Fin" - Rec."Fecha Inicio".
  • Añade también un OnValidate() en "Fecha Inicio" que dispare en cascada la revalidación de "Fecha Fin" si este ya tiene valor: if Rec."Fecha Fin" <> 0D then Rec.Validate("Fecha Fin").

Pista de Código

AL — LineaAlquiler.Table.al (campos y triggers)
        // ── Campo nuevo ─────────────────────────────────────────────
        field(6; "Dias Alquiler"; Integer) { Editable = false; }

        // ── OnValidate en Fecha Inicio: cascada hacia Fecha Fin ─────
        field(4; "Fecha Inicio"; Date)
        {
            trigger OnValidate()
            begin
                if Rec."Fecha Fin" <> 0D then
                    Rec.Validate("Fecha Fin"); // revalida para recalcular días
            end;
        }

        // ── OnValidate en Fecha Fin: validación + cálculo ───────────
        field(5; "Fecha Fin"; Date)
        {
            trigger OnValidate()
            begin
                if Rec."Fecha Fin" < Rec."Fecha Inicio" then
                    Error('La fecha de fin no puede ser anterior a la de inicio.');

                Rec."Dias Alquiler" := Rec."Fecha Fin" - Rec."Fecha Inicio";
            end;
        }
💡 Recuerda: Rec.Validate("Fecha Fin") no solo asigna el valor: dispara el OnValidate del campo destino completo, incluyendo la validación de integridad referencial y cualquier otra lógica que tenga. Úsalo siempre que quieras que un cambio en un campo reaccione correctamente a través de la cadena de validaciones.
Ejercicio 03
OnDelete

Protege la cabecera contra borrados peligrosos

Objetivo: Impedir que se borre una "Cabecera Alquiler" que tenga líneas asociadas en "Linea Alquiler", y aprovechar el FlowField "No. Lineas" que ya tienes para hacer la comprobación de forma elegante.

Instrucciones

  • En CabeceraAlquiler.Table.al, añade el trigger OnDelete().
  • Dentro del trigger, llama primero a Rec.CalcFields("No. Lineas") para calcular el FlowField.
  • Si Rec."No. Lineas" > 0 lanza un Error con el mensaje: 'No se puede eliminar una cabecera con %1 línea(s) asociada(s).', pasando Rec."No. Lineas" como parámetro.
  • Añade también el trigger OnRename() que bloquee cualquier cambio de PK con el mensaje: 'Los alquileres no pueden ser renombrados.'

Pista de Código

AL — CabeceraAlquiler.Table.al (triggers OnDelete y OnRename)
    trigger OnDelete()
    begin
        Rec.CalcFields("No. Lineas"); // forzamos el cálculo del FlowField
        if Rec."No. Lineas" > 0 then
            Error(
                'No se puede eliminar una cabecera con %1 línea(s) asociada(s).',
                Rec."No. Lineas"
            );
    end;

    trigger OnRename()
    begin
        Error('Los alquileres no pueden ser renombrados.');
    end;
⚠️ Ojo: Dentro de un trigger OnDelete, los FlowFields no se calculan automáticamente: debes llamar a CalcFields explícitamente antes de leer su valor, igual que harías desde un codeunit.
Ejercicio 04
OnModify · Historial automático

Registra cada cambio de estado en el historial automáticamente

Objetivo: Completar el ciclo del proyecto conectando la tabla "Maquinaria" con la tabla "Historial Estado Maquina" mediante el trigger OnModify(), de modo que cualquier cambio en el campo "Estado" genere una fila de historial de forma completamente automática.

Instrucciones

  • Abre Maquinaria.Table.al y añade el trigger OnModify().
  • Dentro del trigger, declara una variable de tipo Record "Historial Estado Maquina" y otra de tipo Record "Maquinaria" llamada MaquinaAntes.
  • Usa MaquinaAntes.Get(Rec."No.") para leer el estado antes del cambio (el registro aún no ha sido guardado en este punto).
  • Si MaquinaAntes."Estado" <> Rec."Estado", inserta un nuevo registro en "Historial Estado Maquina" con: "No. Maquina", "Estado Anterior" = MaquinaAntes."Estado", "Estado Nuevo" = Rec."Estado" y "Fecha Cambio" = CurrentDateTime.

Pista de Código

AL — Maquinaria.Table.al (trigger OnModify)
    trigger OnModify()
    var
        Historial    : Record "Historial Estado Maquina";
        MaquinaAntes : Record "Maquinaria";
    begin
        // Leemos el estado guardado ANTES de este modify
        MaquinaAntes.Get(Rec."No.");

        // Solo insertamos historial si el estado ha cambiado realmente
        if MaquinaAntes."Estado" <> Rec."Estado" then begin
            Historial.Init();
            Historial."No. Maquina"     := Rec."No.";
            Historial."Estado Anterior" := MaquinaAntes."Estado";
            Historial."Estado Nuevo"    := Rec."Estado";
            Historial."Fecha Cambio"    := CurrentDateTime;
            Historial."Tipo Origen"     := Historial."Tipo Origen"::Manual;
            Historial.Insert(true); // true para ejecutar el OnInsert de la tabla
        end;
    end;
💡 Recuerda: Dentro de OnModify, Rec contiene los valores nuevos que están a punto de guardarse. Para leer los valores anteriores debes hacer un Get explícito sobre el mismo registro, ya que en este momento el registro viejo aún existe en la base de datos sin modificar.
💡 Bonus: Ahora que el historial se inserta automáticamente desde OnModify, el trigger OnValidate del campo "Estado" en MaquinariaCard.Page.al ya no necesita hacerlo manualmente. Puedes simplificarlo para que solo muestre el aviso de Retirado, y dejar toda la lógica de persistencia en la tabla donde siempre debió estar.
← Volver a Ejercicios