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 procedurepor defecto. Solo expón un procedimiento como público si otros objetos necesitan invocarlo. - Usa
varen 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
: TipoRetornodespué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,andtiene mayor precedencia que las comparaciones, así queA > 0 and B > 0se interpreta comoA > (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 | Sí — 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 conrepeat...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 deelse. Si lo pones, el compilador interpreta que la sentencia IF terminó ahí y queelseestá 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+B → F5) | 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 triggersOnPreReport,OnAfterGetRecordyOnPostReportpara 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) yRunModal()es modal (el código espera). - Si el informe no aparece en la búsqueda de BC, verifica que
UsageCategoryno esté vacío.