Dif. 7/10

AL Intermedio

Esta sección profundiza en el lenguaje AL con herramientas de productividad (IntelliSense, gestión de código fuente), funciones de conversión y formato, estructuras de control de flujo avanzadas, operaciones CRUD sobre registros, el concepto de Rec/xRec, funciones MARK y la comunicación entre objetos.

1. IntelliSense y Documentación Interna

VS Code con la extensión AL ofrece IntelliSense, que autocompleta nombres de objetos, campos, funciones y propiedades mientras escribes. Es la herramienta más importante para ser productivo en AL.

Atajo Función
Ctrl+Espacio Abre la lista de sugerencias de IntelliSense manualmente
Ctrl+Shift+Espacio Muestra los parámetros de la función actual (info de firma)
F12 Go to Definition — navega al código fuente de la función o tabla
Alt+F12 Peek Definition — muestra la definición en una ventana flotante
Shift+F12 Busca todas las referencias de un símbolo en el proyecto
Ctrl+K Ctrl+I Muestra documentación rápida (hover info) del símbolo bajo el cursor

Documentación XML en procedimientos

Puedes documentar tus procedimientos con comentarios XML. IntelliSense los mostrará como tooltip cuando otro desarrollador pase el ratón sobre la función.

/// <summary>
/// Calcula la multa por retraso en la devolución de un libro.
/// </summary>
/// <param name="DiasRetraso">Número de días de retraso.</param>
/// <returns>Importe de la multa en euros.</returns>
procedure CalcularMulta(DiasRetraso: Integer): Decimal
begin
    exit(DiasRetraso * 0.50);
end;

2. Gestión del Código Fuente

BC usa extensiones empaquetadas como ficheros .app. El código fuente se gestiona en ficheros .al dentro de un proyecto de VS Code. Para trabajar en equipo es imprescindible usar un sistema de control de versiones como Git.

Concepto Descripción
Repositorio Git Cada proyecto AL debe tener su propio repo. git init en la raíz del proyecto.
.gitignore Excluir .app, .alpackages/, .alcache/ y launch.json (datos locales).
Branches Usar ramas para cada funcionalidad nueva. Fusionar con merge o pull request.
app.json versionado Incrementar la versión en app.json antes de cada despliegue a producción.
Azure DevOps / GitHub BC se integra nativamente con Azure DevOps para CI/CD y validación automática de extensiones.
// Ejemplo de .gitignore para un proyecto AL de Business Central
*.app
.alpackages/
.alcache/
launch.json
rad.json
*.g.xlf

3. Funciones de Conversión y Formato: ROUND, FORMAT, EVALUATE

Estas funciones permiten transformar datos entre tipos, formatear valores para presentación y redondear números.

Función Qué hace Ejemplo
Round() Redondea un Decimal según precisión y dirección Round(15.456, 0.01) → 15.46
Format() Convierte cualquier valor a Text para mostrarlo Format(Today) → '08/03/2026'
Evaluate() Convierte un Text a otro tipo de dato Evaluate(MiFecha, '08/03/2026')
StrLen() Devuelve la longitud de una cadena StrLen('Hola') → 4
CopyStr() Extrae una subcadena CopyStr('ABCDE', 2, 3) → 'BCD'
UpperCase() / LowerCase() Convierte a mayúsculas/minúsculas UpperCase('hola') → 'HOLA'
Abs() Valor absoluto Abs(-15) → 15
Power() Potencia Power(2, 10) → 1024
// ROUND — redondeo controlado
var
    Precio: Decimal;
begin
    Precio := 19.999;
    Precio := Round(Precio, 0.01);           // 20.00 (2 decimales)
    Precio := Round(Precio, 0.01, '<');    // Redondeo hacia abajo
    Precio := Round(Precio, 0.01, '>');    // Redondeo hacia arriba
end;

// FORMAT — convertir a texto para mostrar
var
    FechaTexto: Text;
    ImporteTexto: Text;
begin
    FechaTexto   := Format(Today);            // '08/03/2026'
    ImporteTexto := Format(1234.50, 0, '<Integer>.<Decimals,2> €');
    Message('Fecha: %1 — Importe: %2', FechaTexto, ImporteTexto);
end;

// EVALUATE — convertir texto a otro tipo
var
    FechaDesdeTexto: Date;
    NumeroDesdeTexto: Integer;
begin
    Evaluate(FechaDesdeTexto, '08/03/2026');
    Evaluate(NumeroDesdeTexto, '42');
    Message('Fecha: %1 — Número: %2', FechaDesdeTexto, NumeroDesdeTexto);
end;

4. Funciones de Control de Flujo: REPEAT, WHILE, FOR, CASE

AL ofrece varias estructuras de bucle y selección múltiple para controlar el flujo de ejecución del código.

REPEAT...UNTIL

// Ejecuta al menos una vez. El patrón más usado con FindSet().
var
    Prestamo: Record Prestamo;
begin
    Prestamo.SetRange(Estado, Prestamo.Estado::Activo);
    if Prestamo.FindSet() then
        repeat
            Message('Préstamo: %1', Prestamo."No.");
        until Prestamo.Next() = 0;
end;

WHILE...DO

// Evalúa la condición ANTES de entrar. Puede no ejecutarse nunca.
var
    Intentos: Integer;
begin
    Intentos := 0;
    while Intentos < 5 do begin
        Intentos += 1;
        Message('Intento %1 de 5', Intentos);
    end;
end;

FOR...TO / FOR...DOWNTO

// Bucle con contador — número fijo de iteraciones
var
    i: Integer;
    Suma: Integer;
begin
    Suma := 0;
    for i := 1 to 10 do
        Suma += i;
    Message('Suma de 1 a 10: %1', Suma); // 55

    // Cuenta atrás
    for i := 5 downto 1 do
        Message('%1...', i);
end;

CASE...OF

// Selección múltiple — alternativa limpia a IF-ELSE anidados
var
    DiaSemana: Integer;
begin
    DiaSemana := Date2DWY(Today, 1); // 1=Lun, 7=Dom
    case DiaSemana of
        1: Message('Lunes');
        2: Message('Martes');
        3: Message('Miércoles');
        4: Message('Jueves');
        5: Message('Viernes');
        6, 7: Message('Fin de semana');
        else
            Error('Día inválido: %1', DiaSemana);
    end;
end;

💡 ¿Cuál elegir?

  • repeat...until: para iterar registros con FindSet + Next.
  • while...do: cuando no sabes cuántas iteraciones habrá y la condición puede ser false desde el inicio.
  • for...to: cuando conoces el número exacto de iteraciones.
  • case...of: cuando comparas una variable contra múltiples valores concretos (sustituye a if-else if-else if...).

5. Funciones EXIT, BREAK, QUIT, SKIP

Estas funciones permiten interrumpir el flujo normal de ejecución de formas diferentes.

Función Ámbito Qué hace ¿Revierte datos?
exit Procedimiento Sale del procedimiento actual. Puede devolver un valor: exit(valor). No
break Bucle (trigger de DataItem) Sale del bucle repeat/while/for. En informes, interrumpe el DataItem actual. No
CurrReport.Quit() Informe completo Detiene el informe entero de forma inmediata. No
CurrReport.Skip() Registro actual (informe) Salta el registro actual y pasa al siguiente sin incluirlo en la salida. No
// EXIT — salir de una función con valor de retorno
procedure EsSocioPremium(NoSocio: Code[20]): Boolean
var
    Socio: Record Socio;
begin
    if not Socio.Get(NoSocio) then
        exit(false); // no existe → no es premium
    exit(Socio.Tipo = Socio.Tipo::Premium);
end;

// SKIP — en un informe, excluir registros sin préstamos
trigger OnAfterGetRecord()
begin
    if Socio."Total Prestamos" = 0 then
        CurrReport.Skip(); // no aparecerá en el informe
end;

// BREAK — salir de un bucle anticipadamente
var
    Libro: Record Libro;
    Encontrado: Boolean;
begin
    Encontrado := false;
    if Libro.FindSet() then
        repeat
            if Libro.Titulo = 'El Quijote' then begin
                Encontrado := true;
                break; // salimos del repeat
            end;
        until Libro.Next() = 0;
end;

6. Funciones NEXT con FIND / FINDSET

El patrón FindSet() + repeat...until Next() = 0 es la forma estándar de recorrer un conjunto de registros en AL. Entender cuándo usar cada variante de Find es clave para escribir código eficiente.

Patrón Cuándo usarlo Rendimiento
FindSet(false) + Next Solo leer registros (no modificar) Óptimo — lectura secuencial rápida
FindSet(true) + Next Leer y modificar registros dentro del bucle Bloquea los registros para actualización
FindFirst() Solo necesitas un registro (el primero) Más rápido que FindSet si no iteras
FindLast() Solo necesitas el último registro Eficiente para obtener el máximo
Next(n) Avanzar n posiciones (n puede ser negativo) Normal
// Patrón COMPLETO: filtrar → buscar → iterar → modificar
var
    Prestamo: Record Prestamo;
    Contador: Integer;
begin
    Prestamo.SetRange(Estado, Prestamo.Estado::Activo);
    Prestamo.SetFilter("Fecha Devolucion", '..%1', Today - 30); // vencidos hace 30+ días

    Contador := 0;
    if Prestamo.FindSet(true) then // true = vamos a modificar
        repeat
            Prestamo.Estado := Prestamo.Estado::Vencido;
            Prestamo.Modify(true);
            Contador += 1;
        until Prestamo.Next() = 0;

    Message('Se actualizaron %1 préstamos.', Contador);
end;

⚠️ Error frecuente con FindSet

  • Si usas FindSet(false) y luego haces Modify dentro del bucle, puedes obtener errores de bloqueo. Usa FindSet(true) si vas a modificar.
  • Nunca uses Insert o Delete dentro de un bucle FindSet + Next sobre la misma tabla. Puedes romper el cursor de iteración.

7. Funciones INSERT, MODIFY, DELETE, MODIFYALL, DELETEALL

Estas son las operaciones CRUD (Crear, Leer, Actualizar, Eliminar) sobre registros en AL. Cada una tiene un parámetro booleano que controla si se ejecutan los triggers de tabla.

Función Qué hace Parámetro RunTrigger
Insert(true/false) Inserta un nuevo registro en la tabla true → ejecuta OnInsert
Modify(true/false) Actualiza el registro actual en la tabla true → ejecuta OnModify
Delete(true/false) Elimina el registro actual de la tabla true → ejecuta OnDelete
ModifyAll(Campo, Valor, true/false) Actualiza un campo en todos los registros filtrados de una vez Operación masiva en SQL — muy eficiente
DeleteAll(true/false) Elimina todos los registros filtrados de una vez Operación masiva
Init() Inicializa los campos del registro con sus valores por defecto (InitValue)
// INSERT — crear un nuevo socio
var
    Socio: Record Socio;
begin
    Socio.Init();                        // inicializa campos con valores por defecto
    Socio."No."   := 'S-0100';
    Socio.Nombre := 'Ana García';
    Socio.Email  := 'ana@example.com';
    Socio.Insert(true);                  // true = ejecutar trigger OnInsert
end;

// MODIFY — actualizar un registro existente
var
    Socio: Record Socio;
begin
    if Socio.Get('S-0100') then begin
        Socio.Email := 'nuevo@example.com';
        Socio.Modify(true);
    end;
end;

// DELETE — eliminar un registro
var
    Socio: Record Socio;
begin
    if Socio.Get('S-0100') then
        Socio.Delete(true);
end;

// MODIFYALL — actualización masiva
var
    Prestamo: Record Prestamo;
begin
    Prestamo.SetRange(Estado, Prestamo.Estado::Activo);
    Prestamo.SetFilter("Fecha Devolucion", '..%1', Today);
    Prestamo.ModifyAll(Estado, Prestamo.Estado::Vencido, false);
    // Actualiza TODOS los registros filtrados de golpe. Mucho más rápido que un bucle.
end;

// DELETEALL — borrado masivo
var
    LogTemp: Record "Log Temporal";
begin
    LogTemp.SetFilter("Fecha", '..%1', Today - 90);
    LogTemp.DeleteAll(false); // borrar logs de más de 90 días
end;

💡 ¿true o false en RunTrigger?

  • Usa true cuando necesites que se ejecute la lógica de negocio definida en los triggers de la tabla (validaciones, campos calculados, numeraciones automáticas).
  • Usa false en migraciones de datos masivas, procesos batch o cuando ya has validado los datos manualmente. Es más rápido.
  • Regla general: en operaciones del usuario, true. En procesos internos del sistema, evalúa caso a caso.

8. Rec y xRec

Rec y xRec son dos variables implícitas que BC crea automáticamente en los triggers de tabla y de página. Rec contiene los valores actuales del registro (después de la edición del usuario) y xRec contiene los valores anteriores (antes de la edición).

Variable Contiene Uso típico
Rec Valores actuales (lo que el usuario acaba de escribir) Acceder al valor nuevo de un campo
xRec Valores anteriores (lo que había antes de la edición) Comparar si un campo ha cambiado
// En un trigger OnValidate de campo: comparar valor viejo y nuevo
field(10; Estado; Enum "Estado Prestamo")
{
    trigger OnValidate()
    begin
        // Detectar si el estado ha cambiado
        if Rec.Estado <> xRec.Estado then begin
            Message('Estado cambió de %1 a %2',
                    xRec.Estado, Rec.Estado);

            // Impedir volver a estado Activo desde Vencido
            if (xRec.Estado = xRec.Estado::Vencido) and
               (Rec.Estado = Rec.Estado::Activo) then
                Error('No se puede reactivar un préstamo vencido.');
        end;
    end;
}

// En un trigger OnModify de tabla: registrar cambios
trigger OnModify()
begin
    if Rec.Nombre <> xRec.Nombre then
        Message('El nombre del socio cambió de "%1" a "%2"',
                xRec.Nombre, Rec.Nombre);
end;

💡 Cuándo usar xRec

  • Para validar cambios: impedir transiciones de estado no permitidas.
  • Para auditoría: registrar qué cambió, quién y cuándo.
  • Para recalcular: si un campo que afecta a otros cambia, recalcular los dependientes.
  • xRec no está disponible en OnInsert (no hay "valor anterior" al crear un registro).

9. Funciones MARK, CLEARMARKS, MARKEDONLY, RESET

El sistema de marcado de AL permite "etiquetar" registros individuales en memoria y luego filtrar para trabajar solo con los marcados. Es útil cuando necesitas seleccionar registros con lógica compleja que no se puede expresar con SetFilter.

Función Qué hace
Mark(true/false) Marca o desmarca el registro actual
Mark() (sin parámetro) Devuelve si el registro actual está marcado (Boolean)
MarkedOnly(true/false) Activa/desactiva el filtro para mostrar solo registros marcados
ClearMarks() Elimina todas las marcas de todos los registros
Reset() Elimina todos los filtros (pero NO las marcas)
// Ejemplo: marcar socios que tienen préstamos vencidos
var
    Socio: Record Socio;
    Prestamo: Record Prestamo;
begin
    // Paso 1: recorrer todos los socios y marcar los que tengan vencidos
    if Socio.FindSet() then
        repeat
            Prestamo.Reset();
            Prestamo.SetRange("No. Socio", Socio."No.");
            Prestamo.SetRange(Estado, Prestamo.Estado::Vencido);
            if not Prestamo.IsEmpty then
                Socio.Mark(true); // marcamos este socio
        until Socio.Next() = 0;

    // Paso 2: filtrar para trabajar solo con los marcados
    Socio.MarkedOnly(true);
    Message('Socios con préstamos vencidos: %1', Socio.Count);

    // Ya puedes recorrer solo los socios marcados
    if Socio.FindSet() then
        repeat
            Message('Socio moroso: %1', Socio.Nombre);
        until Socio.Next() = 0;

    // Limpiar
    Socio.ClearMarks();
    Socio.MarkedOnly(false);
end;

10. Comunicación entre Objetos: Datos, Parámetros y Llamadas

En una extensión real, la lógica se reparte entre tablas, páginas, codeunits e informes. Estos objetos necesitan comunicarse para compartir datos y ejecutar lógica de negocio. Hay varias formas de hacerlo en AL.

Llamar a un procedimiento de un Codeunit

// Codeunit con lógica de negocio
codeunit 50100 "Gestion Prestamos"
{
    procedure RegistrarPrestamo(NoSocio: Code[20]; NoLibro: Code[20])
    var
        Prestamo: Record Prestamo;
    begin
        Prestamo.Init();
        Prestamo."No. Socio" := NoSocio;
        Prestamo."No. Libro" := NoLibro;
        Prestamo."Fecha Prestamo" := Today;
        Prestamo.Insert(true);
    end;
}

// Llamar desde una página
action(NuevoPrestamo)
{
    Caption = 'Registrar Préstamo';
    ApplicationArea = All;

    trigger OnAction()
    var
        GestionPrestamos: Codeunit "Gestion Prestamos";
    begin
        GestionPrestamos.RegistrarPrestamo(Rec."No.", 'L-0001');
    end;
}

Pasar registros entre objetos

// Pasar un registro como parámetro por referencia
procedure ValidarSocio(var Socio: Record Socio): Boolean
begin
    if Socio.Nombre = '' then begin
        Error('El nombre del socio es obligatorio.');
        exit(false);
    end;
    exit(true);
end;

// Abrir una página con filtros desde código
var
    PaginaSocios: Page "Lista Socios";
    Socio: Record Socio;
begin
    Socio.SetRange(Tipo, Socio.Tipo::Premium);
    PaginaSocios.SetTableView(Socio);
    PaginaSocios.RunModal(); // abre la página filtrada por socios premium
end;

// Obtener el registro seleccionado en un LookupMode
var
    PaginaSocios: Page "Lista Socios";
    SocioSeleccionado: Record Socio;
begin
    PaginaSocios.LookupMode(true);
    if PaginaSocios.RunModal() = Action::LookupOK then begin
        PaginaSocios.GetRecord(SocioSeleccionado);
        Message('Socio seleccionado: %1', SocioSeleccionado.Nombre);
    end;
end;

💡 Principio de responsabilidad única

  • Las tablas almacenan datos y validan campos (triggers).
  • Las páginas muestran datos y gestionan la interacción con el usuario.
  • Los codeunits contienen la lógica de negocio reutilizable.
  • Los informes generan documentos y procesan datos masivos.
  • Una página nunca debería contener lógica de negocio compleja. Delega siempre en un codeunit.
← Volver a Teoría