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 conFindSet + 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 hacesModifydentro del bucle, puedes obtener errores de bloqueo. UsaFindSet(true)si vas a modificar. - Nunca uses
InsertoDeletedentro de un bucleFindSet + Nextsobre 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
truecuando necesites que se ejecute la lógica de negocio definida en los triggers de la tabla (validaciones, campos calculados, numeraciones automáticas). - Usa
falseen 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.
xRecno está disponible enOnInsert(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.