Dif. 5/10

Introducción a la Programación AL

Esta sección cubre los fundamentos del lenguaje AL: desde las variables y los procedimientos hasta la sintaxis básica, los operadores y las estructuras condicionales. Dominar estos conceptos es imprescindible antes de avanzar hacia temas intermedios y avanzados.

1. Variables Especiales de Almacenamiento de Trabajo

Además de las variables que tú declaras, AL proporciona variables especiales que el sistema crea automáticamente y que están siempre disponibles dentro de ciertos contextos. Estas variables facilitan el acceso a datos del registro actual, la sesión, el usuario y otros elementos del entorno de ejecución.

Variable Tipo Descripción
Rec Record Referencia al registro actual que se está procesando. Disponible en triggers de tabla y página.
xRec Record Copia del registro antes de la modificación. Permite comparar valores antiguos y nuevos.
CurrPage Page Referencia a la página actual en ejecución. Permite cerrarla, actualizarla o modificar controles.
CurrReport Report Referencia al informe actual. Permite saltar registros (Skip), cancelar, etc.
UserId Función Devuelve el identificador del usuario que ha iniciado sesión en BC.
CompanyName Función Devuelve el nombre de la empresa activa en la sesión de BC.
Today Date Devuelve la fecha actual del sistema.
Time Time Devuelve la hora actual del sistema.
WorkDate Date Devuelve la fecha de trabajo configurada por el usuario (puede diferir de Today).
// Ejemplo: usar variables especiales en un trigger de tabla
trigger OnInsert()
begin
    // Registrar quién y cuándo creó el registro
    Rec."Creado Por"     := UserId;
    Rec."Fecha Creacion" := Today;

    // Mostrar un mensaje con info del entorno
    Message('Usuario: %1, Empresa: %2, Fecha trabajo: %3',
            UserId, CompanyName, WorkDate);
end;
// Ejemplo: CurrPage y CurrReport

// En una página: cerrar la página tras guardar
trigger OnAfterValidate()
begin
    CurrPage.Update(false); // refresca la página sin guardar
end;

// En un informe: saltar registros que no cumplan condición
trigger OnAfterGetRecord()
begin
    if Rec.Cantidad = 0 then
        CurrReport.Skip(); // no incluir este registro en el informe
end;

2. Procedimientos No Modificables y Modificables

En AL, los procedimientos (funciones) se clasifican según si el código que ejecutan puede ser reemplazado por otra extensión o no. Esta distinción es fundamental en un ecosistema donde múltiples extensiones conviven en el mismo entorno.

Tipo Palabra clave ¿Se puede sobrescribir? ¿Cuándo usarlo?
No modificable (sealed) procedure (por defecto) No. Otras extensiones no pueden alterar su comportamiento. Lógica interna que no debe ser interceptada.
Modificable (integrable) [IntegrationEvent] / [BusinessEvent] Sí. Otras extensiones pueden suscribirse y ejecutar código adicional. Puntos de extensión para que otros desarrolladores puedan personalizar el flujo.
// Procedimiento NO modificable — nadie puede interceptarlo
local procedure CalcularMulta(DiasRetraso: Integer): Decimal
begin
    exit(DiasRetraso * 0.50); // 0,50 € por día de retraso
end;

// Procedimiento con evento de integración — otras extensiones
// pueden suscribirse para añadir lógica antes o después
[IntegrationEvent(false, false)]
local procedure OnBeforeRegistrarPrestamo(var Prestamo: Record Prestamo)
begin
    // Vacío: el evento solo define el punto de extensión
end;

// Suscripción desde otra extensión
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Gestion Prestamos",
                  'OnBeforeRegistrarPrestamo', '', false, false)]
local procedure MiLogicaExtra(var Prestamo: Record Prestamo)
begin
    // Añadir validación personalizada
    Prestamo.TestField("No. Socio");
end;

3. Procedimientos Personalizados y su Creación

En AL puedes crear tus propias funciones (procedimientos) que encapsulan lógica de negocio reutilizable. Un procedimiento puede ser local (solo accesible dentro del objeto) o público (accesible desde otros objetos).

Sintaxis de un procedimiento

// Procedimiento local sin retorno
local procedure MostrarSaludo(NombreUsuario: Text[50])
begin
    Message('¡Bienvenido, %1!', NombreUsuario);
end;

// Procedimiento público con valor de retorno
procedure ObtenerEdad(FechaNacimiento: Date): Integer
var
    Edad: Integer;
begin
    Edad := Date2DMY(Today, 3) - Date2DMY(FechaNacimiento, 3);
    exit(Edad);
end;

// Procedimiento con parámetro por referencia (var)
local procedure AplicarDescuento(var Precio: Decimal; PctDescuento: Decimal)
begin
    Precio := Precio * (1 - PctDescuento / 100);
    // Al usar 'var', el cambio se refleja en la variable original
end;

💡 local vs. público

  • Usa local procedure por defecto. Solo expón un procedimiento como público si otros objetos necesitan invocarlo.
  • Usa var en los parámetros cuando necesites que el procedimiento modifique la variable original del llamador.
  • Un procedimiento sin valor de retorno simplemente no tiene la parte : TipoRetorno después de los paréntesis.

4. Sintaxis AL: Asignación, Puntuación y Expresiones

AL tiene una sintaxis inspirada en Pascal. Conocer sus convenciones de puntuación y asignación evita errores de compilación y facilita la lectura del código.

Elemento Símbolo Ejemplo
Asignación := Precio := 29.99;
Comparación de igualdad = if Estado = Estado::Activo then
Fin de sentencia ; (punto y coma) Obligatorio al final de cada instrucción
Cadena de texto ' ' (comillas simples) Nombre := 'Juan Pérez';
Nombre de campo con espacios " " (comillas dobles) Rec."Fecha Creacion"
Comentario de línea // // Esto es un comentario
Bloque de código begin ... end Agrupa varias sentencias en un solo bloque
Concatenación + NombreCompleto := Nombre + ' ' + Apellido;
Acceso a miembro de enum :: Estado::Activo
Acceso a campo . (punto) Socio.Nombre
// Ejemplos de sintaxis AL — asignación y expresiones
var
    Nombre: Text[50];
    Precio: Decimal;
    Cantidad: Integer;
    Total: Decimal;
    Activo: Boolean;
begin
    // Asignación simple
    Nombre   := 'Biblioteca Municipal';
    Precio   := 15.50;
    Cantidad := 3;

    // Expresión aritmética
    Total := Precio * Cantidad;

    // Concatenación de cadenas
    Message('Total para ' + Nombre + ': %1 €', Total);

    // Asignación booleana
    Activo := (Cantidad > 0);
end;

⚠️ Errores comunes de sintaxis

  • Usar = para asignar en vez de :=. En AL, = es solo para comparación.
  • Olvidar el ; al final de una instrucción. El compilador dará error en la línea siguiente.
  • Usar comillas dobles para cadenas de texto. Las cadenas van entre comillas simples (' '). Las comillas dobles son solo para nombres de campo con espacios.

5. Operadores: Aritméticos, Booleanos y Relacionales

Operadores aritméticos

Operador Operación Ejemplo Resultado
+ Suma / Concatenación 5 + 3 8
- Resta 10 - 4 6
* Multiplicación 6 * 7 42
/ División 15 / 4 3.75
div División entera 15 div 4 3
mod Módulo (resto) 15 mod 4 3

Operadores relacionales (de comparación)

Operador Significado Ejemplo
= Igual a if A = B then
<> Distinto de if A <> B then
< Menor que if Precio < 100 then
> Mayor que if Cantidad > 0 then
<= Menor o igual if Edad <= 18 then
>= Mayor o igual if Saldo >= 0 then
in Contenido en conjunto if Estado in [Estado::Activo, Estado::Pendiente] then

Operadores booleanos (lógicos)

Operador Significado Ejemplo
and Y lógico — ambas deben ser true if (A > 0) and (B > 0) then
or O lógico — al menos una true if (A = 0) or (B = 0) then
not Negación if not Activo then
xor O exclusivo — solo una true if A xor B then

6. Precedencia de Operadores

Cuando una expresión combina varios operadores, AL los evalúa siguiendo un orden de prioridad (de mayor a menor). Usar paréntesis siempre es recomendable para evitar ambigüedades.

Prioridad Operadores Descripción
1 (más alta) not, - (unario) Negación lógica y cambio de signo
2 *, /, div, mod, and Multiplicación, división y AND lógico
3 +, -, or Suma, resta, concatenación y OR lógico
4 (más baja) =, <>, <, >, <=, >=, in Comparaciones
// ¡Cuidado con la precedencia!

// INCORRECTO — da error de compilación: and se evalúa antes que >
if Cantidad > 0 and Precio > 10 then  // ERROR

// CORRECTO — los paréntesis fuerzan el orden deseado
if (Cantidad > 0) and (Precio > 10) then
    Message('Condición cumplida');

💡 Regla de oro

  • Siempre usa paréntesis en condiciones compuestas con and, or, not. En AL, and tiene mayor precedencia que las comparaciones, así que A > 0 and B > 0 se interpreta como A > (0 and B) > 0, lo cual da error.

7. Procedimientos AL de Uso Frecuente

AL incluye una serie de funciones integradas para interactuar con el usuario, mostrar mensajes, lanzar errores y pedir confirmación. Son las herramientas básicas de comunicación entre el código y el usuario.

Función Qué hace ¿Detiene la ejecución?
Message() Muestra un mensaje informativo al usuario No (el código sigue ejecutándose)
Error() Muestra un error y revierte la transacción actual — detiene todo y hace rollback
Confirm() Muestra un diálogo Sí/No y devuelve un booleano Espera la respuesta del usuario
StrMenu() Muestra un menú de opciones y devuelve el número elegido Espera la respuesta del usuario
Dialog.Open() Abre una ventana de progreso (ej. barra de progreso) No (se actualiza manualmente)
// MESSAGE — información al usuario
Message('El préstamo %1 ha sido registrado.', Prestamo."No.");

// ERROR — detiene la ejecución y revierte
if Socio."No." = '' then
    Error('Debe seleccionar un socio antes de continuar.');

// CONFIRM — pregunta Sí/No
if not Confirm('¿Desea eliminar el préstamo %1?', false, Prestamo."No.") then
    exit; // El usuario dijo No, salimos

// STRMENU — menú de opciones
var
    Opcion: Integer;
begin
    Opcion := StrMenu('Renovar,Devolver,Cancelar', 1, 'Acción sobre el préstamo:');
    case Opcion of
        1: Message('Renovando...');
        2: Message('Devolviendo...');
        3: Message('Operación cancelada.');
    end;
end;

// DIALOG — barra de progreso
var
    Ventana: Dialog;
    i: Integer;
begin
    Ventana.Open('Procesando registro #1##########');
    for i := 1 to 100 do begin
        Ventana.Update(1, i);
        Sleep(50);
    end;
    Ventana.Close();
end;

8. Procedimientos de Registro

Estas funciones permiten buscar, filtrar y navegar registros en una tabla. Son la base de cualquier operación de lectura de datos en AL.

Función Qué hace Devuelve
Get() Busca un registro por su clave primaria Boolean (true si lo encuentra)
Find('-') Busca el primer registro según los filtros y el orden actual Boolean
Find('+') Busca el último registro Boolean
FindSet() Busca el primer registro y prepara el cursor para iterar con Next Boolean
FindFirst() Busca el primer registro (optimizado, sin preparar cursor) Boolean
FindLast() Busca el último registro Boolean
Next() Avanza al siguiente registro del conjunto Integer (0 si no hay más)
SetRange() Establece un filtro de rango en un campo
SetFilter() Establece un filtro con expresión (más flexible que SetRange)
Reset() Elimina todos los filtros del registro
Count Devuelve el número de registros que cumplen los filtros Integer
IsEmpty Comprueba si hay registros (más eficiente que Count = 0) Boolean
// GET — buscar por clave primaria
var
    Socio: Record Socio;
begin
    if Socio.Get('S-0001') then
        Message('Socio encontrado: %1', Socio.Nombre)
    else
        Error('No existe el socio S-0001');
end;

// SETRANGE + FINDSET — iterar sobre registros filtrados
var
    Prestamo: Record Prestamo;
begin
    Prestamo.SetRange(Estado, Prestamo.Estado::Activo);
    if Prestamo.FindSet() then
        repeat
            Message('Préstamo: %1 - Socio: %2',
                    Prestamo."No.", Prestamo."No. Socio");
        until Prestamo.Next() = 0;
end;

// SETFILTER — filtro con expresión flexible
var
    Libro: Record Libro;
begin
    Libro.SetFilter(Titulo, '@*central*'); // @ = case-insensitive, * = comodín
    Message('Libros encontrados: %1', Libro.Count);
end;

💡 FindSet vs. Find('-') vs. FindFirst

  • FindSet(): usar cuando vas a iterar con repeat...until Next() = 0. Es el más eficiente para bucles.
  • FindFirst() / FindLast(): cuando solo necesitas un registro y no vas a iterar.
  • Find('-') / Find('+'): forma antigua, equivalente a FindFirst/FindLast. Preferir las versiones First/Last.

9. Declaraciones Condicionales: BEGIN-END, IF-THEN-ELSE

Las estructuras condicionales permiten ejecutar código de forma selectiva según se cumplan o no ciertas condiciones. AL usa if...then...else y bloques begin...end para agrupar múltiples sentencias.

IF-THEN simple

// Una sola sentencia — no necesita begin...end
if Prestamo.Estado = Prestamo.Estado::Vencido then
    Message('Este préstamo está vencido.');

IF-THEN-ELSE

// Con rama alternativa
if Socio."No." <> '' then
    Message('Socio seleccionado: %1', Socio.Nombre)
else
    Error('Debe seleccionar un socio.');

// NOTA: NO hay punto y coma antes de 'else'. Esto es un error muy común.

BEGIN-END para múltiples sentencias

// Cuando hay más de una sentencia, se necesita begin...end
if (Prestamo."Fecha Devolucion" < Today) and
   (Prestamo.Estado = Prestamo.Estado::Activo) then
begin
    Prestamo.Estado := Prestamo.Estado::Vencido;
    Prestamo.Modify(true);
    Message('Préstamo %1 marcado como vencido.', Prestamo."No.");
end else begin
    Message('Préstamo %1 sigue activo.', Prestamo."No.");
end;

IF anidados

// IFs anidados — se evalúan en cascada
if DiasRetraso = 0 then
    Message('Devuelto a tiempo.')
else if DiasRetraso <= 7 then
    Message('Retraso leve: %1 días.', DiasRetraso)
else if DiasRetraso <= 30 then
    Message('Retraso moderado: %1 días.', DiasRetraso)
else
    Error('Retraso grave: %1 días. Se bloqueará la cuenta.', DiasRetraso);

⚠️ El error del punto y coma antes de ELSE

  • En AL nunca debe haber ; justo antes de else. Si lo pones, el compilador interpreta que la sentencia IF terminó ahí y que else está suelto.
  • Incorrecto: if A then B; else C;
  • Correcto: if A then B else C;

10. Probando el Informe Completo

Después de crear un informe en AL, es fundamental probarlo para verificar que los datos, el diseño y la lógica funcionan correctamente. Business Central ofrece varias formas de ejecutar y depurar un informe.

Pasos para probar un informe

Paso Acción Qué verificar
1 Compilar y desplegar la extensión (Ctrl+Shift+BF5) Sin errores de compilación
2 En BC, buscar el informe por nombre en la barra de búsqueda El informe aparece si UsageCategory y ApplicationArea están definidos
3 Configurar los filtros en la RequestPage Los filtros reducen adecuadamente los datos mostrados
4 Seleccionar "Vista previa" o "Imprimir" Los datos son correctos y el layout tiene el formato esperado
5 Probar con datos vacíos (sin registros) El informe no genera error; idealmente muestra un mensaje o un layout vacío
6 Probar con muchos registros Rendimiento aceptable y paginación correcta

Ejecutar un informe desde código AL

// Ejecutar un informe desde un botón de acción en una página
action(ImprimirInforme)
{
    Caption = 'Imprimir Informe de Socios';
    ApplicationArea = All;
    Image = Report;

    trigger OnAction()
    var
        InformeSocios: Report "Listado de Socios";
    begin
        InformeSocios.Run(); // abre la RequestPage y ejecuta el informe
    end;
}

// Ejecutar con filtros predefinidos (sin RequestPage)
trigger OnAction()
var
    Socio: Record Socio;
    InformeSocios: Report "Listado de Socios";
begin
    Socio.SetRange("No.", 'S-0001', 'S-0050');
    InformeSocios.SetTableView(Socio); // aplica el filtro al informe
    InformeSocios.RunModal(); // RunModal = espera a que el usuario cierre
end;

💡 Consejos para depurar informes

  • Usa Message() en los triggers OnPreReport, OnAfterGetRecord y OnPostReport para inspeccionar valores mientras desarrollas.
  • Si el informe aparece en blanco, comprueba que los nombres de las columnas del dataset coinciden con los marcadores del layout.
  • Recuerda que Run() es no modal (el código sigue) y RunModal() es modal (el código espera).
  • Si el informe no aparece en la búsqueda de BC, verifica que UsageCategory no esté vacío.
← Volver a Teoría