Estándares de Desarrollo | Documentación Técnica
Introducción
Sección titulada «Introducción»La presente guía de estándares de desarrollo en AL para Virtual Office Group C.A. establece un marco técnico unificado que garantiza la calidad, escalabilidad y compatibilidad de nuestras soluciones dentro del ecosistema Microsoft Dynamics 365 Business Central.
Al adoptar las directrices oficiales de Microsoft, nuestra organización asegura el desarrollo de extensiones cloud-ready que minimizan conflictos técnicos y facilitan actualizaciones continuas. Este compromiso optimiza el mantenimiento y garantiza una experiencia de usuario coherente y profesional.
Lenguaje Técnico y Nomenclatura
Sección titulada «Lenguaje Técnico y Nomenclatura»Se establece como norma obligatoria que la sintaxis y nomenclatura técnica dentro del código fuente se realice estrictamente en inglés.
Esta directriz abarca:
-
Identificadores de Objetos: (Tables, Pages, Codeunits, Reports, Enums, Queries).
-
Identificadores de Campos: En tablas, extensiones de tablas, paginas, extensiones de paginas, queries y reportes.
-
Lógica de Programación: Nombres de variables, constantes,Errores,Texto, y métodos (procedimientos).
-
Arquitectura de Eventos: Nombres de suscriptores y publicadores de eventos.
Excepciones Permitidas (Uso de Español)
Sección titulada «Excepciones Permitidas (Uso de Español)»Capa de usuario (UI)
Sección titulada «Capa de usuario (UI)»Propiedades Caption y ToolTip, y variables de tipo Label.
Comentarios de posición (Label)
Sección titulada «Comentarios de posición (Label)»utilizar los comentarios de los marcadores de posición para identificar los valores a utilizar
PostConfirmQst: Label '¿Desea registrar la factura %1 del cliente %2?', Comment = '%1 = No. Factura, %2 = Nombre Cliente';Descripción de campos en tablas
Sección titulada «Descripción de campos en tablas»Se utiliza para notas técnicas sobre el propósito del campo, redactadas en español.
field(2; "Saldo Pendiente"; Decimal){ Description = 'Campo calculado para el total de la deuda';}Comentarios
Sección titulada «Comentarios»Se permite el español para comentarios internos para facilitar la comunicación inmediata entre el equipo.
Estructura de la extensión
Sección titulada «Estructura de la extensión»Para garantizar la mantenibilidad, los proyectos deben seguir una estructura jerárquica organizada. Se prohíbe el uso de estructuras planas en proyectos medianos o grandes.
Organización de la Raíz del Proyecto
Sección titulada «Organización de la Raíz del Proyecto»El directorio raíz de la extensión debe contener los archivos de configuración y las carpetas principales de recursos:
.alpackages/: Dependencias y símbolos (símbolos).
src/: Directorio contenedor de todo el código fuente.
test/: Unidades de prueba (Test Codeunits).
app.json: Manifiesto obligatorio de la extensión.
logo.jpg: Imagen representativa de la extensión (formato 150x150 recomendado).
README.md: Documentación general del proyecto.
.gitignore: (Obligatorio) Configuración de exclusión para Git.
.vscode/: Carpeta que contiene la configuración del entorno (ej. launch.json y settings.json).
AppsourceCop.json: Archivo de configuración para validaciones .
Translations/: (Opciónal) Configuración de traducciones.
Ejemplo de uso correcto
Sección titulada «Ejemplo de uso correcto»Directory.alpackages/
- …
Directory.vscode/
- launch.json
- settings.json
- package.json
Directorysrc/
- …
Directorytest/
- …
- logo.jpg
- app.json
- .gitignore
- README.md
- AppsourceCop.json
Estructura de la Carpeta /src (Agrupación Funcional)
Sección titulada «Estructura de la Carpeta /src (Agrupación Funcional)»Los objetos deben agruparse en subcarpetas dentro de src/ basándose en su area de negocio. Una estructura estándar es:
Ejemplo de uso correcto 1
Sección titulada «Ejemplo de uso correcto 1»Directory.alpackages/
- …
Directory.vscode/
- …
Directorysrc
DirectoryBank
DirectoryPage
- VOGBankCard.Page.al
- VOGBankList.Page.al
DirectoryTable
- VOGBank.Table.al
DirectoryPageExtension
- VOGBankCard.PageExt.al
- VOGBankList.PageExt.al
Directorytest/
- …
- logo.jpg
- app.json
- .gitignore
- README.md
- AppsourceCop.json
Ejemplo de uso correcto 2
Sección titulada «Ejemplo de uso correcto 2»Directory.alpackages/
- …
Directory.vscode/
- …
Directorysrc
DirectoryOCR Document
DirectoryPage
- IncomingDocumetList.Page.al
- IncomingDocumentDetail.Page.al
DirectoryTableExtension
- IncomingDocument.TableExt.al
DirectoryCodeunit
- OCRSend.Codeunit.al
Directorytest/
- …
- logo.jpg
- app.json
- .gitignore
- README.md
- AppsourceCop.json
Los objetos dentro de cada módulo funcional se clasificarán en subcarpetas por tipo. Se requiere la siguiente estructura de directorios:
TableTableExtensionPagePageExtensionCodeunitReportReportExtensionQueryQueryExtensionEnum
Traducciones
Sección titulada «Traducciones»Los archivos de traducción deben residir en una carpeta llamada Translations en la raíz de tu proyecto.
Directory.alpackages/
- …
Directory.vscode/
- …
Directorysrc
DirectoryOCR Document
DirectoryPage
- IncomingDocumetList.Page.al
- IncomingDocumentDetail.Page.al
DirectoryTableExtension
- IncomingDocument.TableExt.al
DirectoryCodeunit
- OCRSend.Codeunit.al
Directorytest/
- …
- logo.jpg
DirectoryTranslation
- example.es-ES.xlf
- app.json
- .gitignore
- README.md
- AppsourceCop.json
Uso de prefijo (VOG)
Sección titulada «Uso de prefijo (VOG)»Todas las extensiones deben utilizar el prefijo VOG para garantizar la unicidad y evitar conflictos con objetos de terceros o de Microsoft.
El prefijo se aplicara de la siguiente manera:
Objetos
Sección titulada «Objetos»//Enumeracionesenum 50009 VOGTimeExecUpdateCurrency{}
// Paginapage 50009 VOGConfUpdateCurrency{}
// Tablatable 50009 VOGConfigUpdateCurrency{}
// Codeunitcodeunit 50009 VOGCurrencyUpdater{}
// reportesreport 50009 VOGCurrencyUpdateReport{}
//queriesquery 50009 VOGCurrencyQuery{}
//PermisosPermissionSet 50009 VOGCurrencyPermissionSet{}Nombre de extensiones de campos
Sección titulada «Nombre de extensiones de campos»// Extensiones de tablastableextension 50009 VOGCustomerExtension extends Customer{ fields { field(50000; VOGPreferredLanguage; Code[10]) { DataClassification = CustomerContent; } }}//Extensión de paginapageextension 50009 VOGCustomerCard extends CustomerCard{ fields { field(50000; VOGPreferredLanguage; Code[10]) { DataClassification = CustomerContent; } }}// Extensión de reportereportextension 50100 "StandardSalesInvoiceExt" extends "Standard Sales - Invoice"{ dataset { // Agregamos un campo a la sección del encabezado (Header) add(Header) { column(CustomTrackingNumber_Lbl; CustomTrackingNumberLbl) { } column(CustomTrackingNumber; Rec."Custom Tracking Number") { } } }}Variables
Sección titulada «Variables» var VOGTempCustomer: Record Customer temporary; VOGGenJnlPostLine: Codeunit "Gen. Jnl.-Post Line";Procedimientos
Sección titulada «Procedimientos» local procedure VOGCalculateDiscount(Amount: Decimal): Decimal begin endNomenclatura de archivos
Sección titulada «Nomenclatura de archivos»El nombre del archivo debe coincidir exactamente con el nombre del objeto interno (excluyendo la extensión .al).
Cada nombre de archivo debe contener el prefijo establecido, nombre del objeto (usando solo los caracteres A-Z, a-z y 0-9), el tipo de objeto y la extensión del archivo.
Estructura del nombre
Sección titulada «Estructura del nombre»| Objetos Nuevos | Extensiones de objetos |
|---|---|
VOG<nombre>.<NombreDeObjecto>.al | VOG<nombre>.<NombreDeObjecto>Ext.al |
Ejemplo
Sección titulada «Ejemplo»| Objetos | Extensiones de objeto |
|---|---|
VOGCustomerCard.Page.al | VOGCustomerCard.PageExt.al |
Para el tipo de objeto en la nomenclatura del archivo, se deben utilizar las siguientes abreviaturas:
| Objetos | Abbreviación |
|---|---|
| Page | Page |
| Page Extension | PageExt |
| Page Customization | PageCust |
| Codeunit | Codeunit |
| Table | Table |
| Table Extension | TableExt |
| XML Port | Xmlport |
| Report | Report |
| Request Page | RequestPage |
| Query | Query |
| Enum | Enum |
| Enum Extension | EnumExt |
| Control Add-ins | ControlAddin |
| Dotnet | Dotnet |
| Profile | Profile |
| Interface | Interface |
| Permission Set | PermissionSet |
| Permission Set Extension | PermissionSetExt |
Nombre de Objetos
Sección titulada «Nombre de Objetos»En el caso de los objetos, estos ejemplos muestran cómo asignar un nombre a los archivos.
Ejemplo
Sección titulada «Ejemplo»| Nombre del objeto | Nombre de archivo |
|---|---|
| codeunit 70000000 VOGSalesperson | VOGSalesPerson.Codeunit.al |
| páge 70000000 VOGVendor | VOGVendor.Page.al |
| pageextension 70000000 VOGSalesperson extends “Vendor Card” | VOGSalesperson.PageExt.al |
Formateo del codigo
Sección titulada «Formateo del codigo»- Utilice todas las letras minúsculas para las palabras clave del idioma reservado.
- Utilice 4 espacios para la sangría.
- Los corchetes siempre deben comenzar en una nueva línea. Si solo hay una propiedad, se puede colocar en una sola línea.
- Escribir en
PascalCasepara nombres de objetos, variables y métodos.
Ejemplo
Sección titulada «Ejemplo»page 123 PageName{ actions { area(Processing) { action(ActionName) { trigger OnAction() begin end; } } }
var VOGTempCustomer: Record Customer temporary;
[EventSubscriber(ObjectType::Page, Page::"Item Card", 'OnAfterGetCurrRecordEvent', '', false, false)] local procedure VOGOnOpenItemCard(var rec: Record Item) var OnRecord: Option " ", Item, Contact; begin EnablePictureAnalyzerNotification(rec."No.", OnRecord::Item); end;}Estructura de Objetos
Sección titulada «Estructura de Objetos»Dentro de un archivo de código .al, la estructura de todos los objetos debe seguir la secuencia:
- Propiedades del objeto
- Las construcciones específicas del objeto, como
- Campos de tabla
- Diseño de página
- Acciones
- Desencadenantes (triggers)
- Variables globales
- Etiquetas
- Variables globales
- Métodos u Procedimientos
Declaración de Namespace
Sección titulada «Declaración de Namespace»Todos los objetos deben declararse dentro de un Namespace. Esta práctica es obligatoria para:
- Evitar conflictos de nombres con desarrollos estándar o extensiones de terceros (AppSource).
- Optimizar la mantenibilidad, agrupando los objetos de forma lógica según su funcionalidad o área de negocio.”
Estructura del namespace:
Sección titulada «Estructura del namespace:» namespace <Empresa>.<Proyecto>.<Modulo>
- Empresa: Prefijo de la orginanización (VOG). Por defecto el valor siempre sera VOG. - Proyecto: Nombre del proyecto o solución al que pertenece la extensión. - Modulo: (Opcional) Nombre del módulo o área funcional dentro del proyecto.Ejemplo de uso correcto
Sección titulada «Ejemplo de uso correcto»namespace VOG.OfficeManagement.RoomBooking
table 50100 VOGOfficeRoom { DataClassification = CustomerContent;
fields { field(1;RoomName; Text[100]) { DataClassification = CustomerContent; }
field(2;Capacity; Integer) { DataClassification = CustomerContent; MinValue = 1; BlankZero = true; } } }Estructura con dependencia externas:
Sección titulada «Estructura con dependencia externas:»Cuando se utilicen objetos de otros namespaces o dependencias externas, se debe emplear la directiva using al inicio del archivo. Esto permite referenciar objetos externos de manera clara.
Ejemplo de uso correcto con dependencias
Sección titulada «Ejemplo de uso correcto con dependencias»namespace VOG.OfficeManagement.RoomBooking;
using VOG.Common.Utilities;using VOG.Customer.Management;
table 50100 VOGOfficeRoom {
fields { field(1;RoomName; Text[100]) { DataClassification = CustomerContent; }
field(2;Capacity; Integer) { DataClassification = CustomerContent; MinValue = 1; BlankZero = true; } }
var VOGTempCustomer: Record Customer; }En el caso anterior, se declaran una dependencia externas que se utilizarara para el objeto. (Record Customer);
Nomenclatura de campos en tablas
Sección titulada «Nomenclatura de campos en tablas»- Todas los campos en extensiones de tabla deben utilizar el prefijo
VOG. - Un campo debe comenzar con una letra mayúscula.
- Se deben omitir los espacios en blanco, los puntos, caracteres numericos y otros caracteres (como paréntesis) que harían necesarias las comillas alrededor de una variable.
- Si un campo es un compuesto de dos o más palabras o abreviaturas, cada palabra o abreviatura debe comenzar con una letra mayúscula
- No utilizar nombre dentros de comillas.
Ejemplo de uso correcto
Sección titulada «Ejemplo de uso correcto»// Uso de nomenclatura correcta en campos de tabla
table 50100 VOGOfficeRoom{ DataClassification = CustomerContent;
fields { field(1;RoomName; Text[100]) { DataClassification = CustomerContent; }
field(2;Capacity; Integer) { DataClassification = CustomerContent; MinValue = 1; BlankZero = true; } }}
table 50101 VOGMeetingRoomExtension extends "Meeting Room"{ fields { field(50000;VOGAvailableEquipment; Text[250]) { DataClassification = CustomerContent; } }}Ejemplo de uso incorrecto
Sección titulada «Ejemplo de uso incorrecto»// Uso de nomenclatura incorrecta en campos de tabla
table 50100 VOGOfficeRoom{ DataClassification = CustomerContent;
fields { // Se utilizo caracteres especiales field(1;room_name; Text[100]) { DataClassification = CustomerContent;
} // No se comenzo con letra mayuscula field(2;capacity; Integer) { DataClassification = CustomerContent; MinValue = 1; BlankZero = true; } }}
table 50101 VOGMeetingRoomExtension extends "Meeting Room"{ fields { // No se uso el prefijo VOG field(50000;AvailableEquipment; Text[250]) { DataClassification = CustomerContent; } }}Nomenclatura de variables
Sección titulada «Nomenclatura de variables»- Todas las variables globales o protegidas deben utilizar el prefijo
VOG. Se exceptuan las variables locales dentro de los métodos u procedimientos. - Las variables que hacen referencia a un objeto AL deben contener el nombre del objeto, abreviado cuando sea necesario.
- Una variable debe comenzar con una letra mayúscula.
- Se deben omitir los espacios en blanco, los puntos y otros caracteres (como paréntesis) que harían necesarias las comillas alrededor de una variable.
- Si una variable es un compuesto de dos o más palabras o abreviaturas, cada palabra o abreviatura debe comenzar con una letra mayúscula
- No utilizar nombre dentros de comillas.
- El nombre de las variables global y locales no deben ser identicos.
- Evite repetir el nombre en campos,metodos o acciones.
Ejemplo de uso correcto 1
Sección titulada «Ejemplo de uso correcto 1»VOGWIPBuffer: Record "Job WIP Buffer"Ejemplo de uso correcto 2
Sección titulada «Ejemplo de uso correcto 2»VOGPostline: Codeunit "Gen. Jnl.-Post Line";VOGGenJnlPostLine: Codeunit "Gen. Jnl.-Post Line";Ejemplo de uso correcto 3
Sección titulada «Ejemplo de uso correcto 3»VOGAmountLCY: Decimal;Variables de objetos temporales
Sección titulada «Variables de objetos temporales»Para las variables que hacen referencia a objetos temporales, utilice el sufijo Temporary después del nombre de la variable. En el nombre de la variable debe incluirse el prefijo Temp.
Ejemplo de uso incorrecto
Sección titulada «Ejemplo de uso incorrecto» VOGCustomer: Record Customer temporary; // Variable de objeto temporal. Debe incluir la palabra `Temp` en el nombre. TempVOGCustomer: Record Customer ; // Variable de objeto persistente. No debe incluir la palabra `Temp` en el nombre. TempCustomer: Decimal // No debe incluir la palabra `Temp` en el nombre, no es una variable de un objeto.Ejemplo de uso correcto
Sección titulada «Ejemplo de uso correcto» VOGTempCustomer: Record Customer temporary; // Variable de objeto temporal VOGCustomer: Record Customer ; // Variable de objeto persistenteComentarios XML
Sección titulada «Comentarios XML»Los comentarios deben utilizarse para explicar el propósito y la lógica del código. Se establecera comentarios XML con las siguiente directrices
- Se utilizaran las etiquetas
<summary>,<param>,<returns>y<remarks>para detallar la funciones o procedimientos. - Evitar el uso de comentarios en funcionalidades redundantes o evidentes.
Estructura del comentario
Sección titulada «Estructura del comentario»-
summary (Resumen) Es la etiqueta más importante. Define de forma breve y concisa qué es el objeto o qué hace el procedimiento. Es lo primero que aparece en el IntelliSense de VS Code.
-
param (Parámetro) Se utiliza para documentar cada una de las variables de entrada de un procedimiento. Requiere el atributo name, que debe coincidir exactamente con el nombre de la variable en la firma de la función.
-
returns (Retorno) Se utiliza exclusivamente en funciones que devuelven un valor.
-
remarks (Observaciones / Comentarios adicionales) Es una etiqueta para información extendida que no cabe en el resumen.
Ejemplo de uso incorrecto
Sección titulada «Ejemplo de uso incorrecto»// Este procedimiento calcula el descuento basado en la cantidad - Uso de comentario simple documentarlocal procedure VOGCalculateDiscountForQuantity(Amount: Decimal): Decimalbegin // Lógica de cálculo de descuentoendEjemplo de uso correcto
Sección titulada «Ejemplo de uso correcto»/// <summary>/// Calcula el descuento por cantidad/// </summary>/// <param name="Amount">Monto a calcular</param>/// <returns>El descuento total calculado como Decimal.</returns>/// <remarks>/// Este método configuración de ventas/// </remarks>local procedure VOGCalculateDiscountForQuantity(Amount: Decimal): Decimalbegin
endMensajes y Errores
Sección titulada «Mensajes y Errores»-
Los mensajes de error, advertencia e informacion se deben establecer con las siguientes propiedades:
-
Todos los mensajes deben comenzar con el prefijo
VOG. -
Debe utilizar la nomenclatura
Error,WarningoInformationseguidas del texto descriptivo. -
El texto se debe crear en una variable de tipo
Labelpara facilitar la traducción y mantenimiento. -
Utiliza el sufijo
Lblen la variales que contienen mensajes. -
En el caso de incluir se debe documentar los marcadores de posición.
Ejemplo de uso incorrecto
Sección titulada «Ejemplo de uso incorrecto» var VOGErrorCustomerNotFoundMessageLbl: Label 'El cliente %1 no fue encontrado en el sistema.'; // No establecio comentario su placeholder VOGErrorCustomerNotFoundMessage: Label 'El cliente %1 no fue encontrado en el sistema.',Comment = '%1 - Número de cliente que no se encontró'; ; // No utili el sufijo Lbl VOGErrorLbl: Label 'El cliente %1 no fue encontrado en el sistema.',Comment = '%1 - Número de cliente que no se encontró'; ; // No tiene nombre descriptivo del mensajeEjemplo de uso correcto
Sección titulada «Ejemplo de uso correcto» var VOGErrorCustomerNotFoundMessageLbl: Label 'El cliente %1 no fue encontrado en el sistema.',Comment = '%1 - Número de cliente que no se encontró'; ;Declaración de método o procedimientos
Sección titulada «Declaración de método o procedimientos»Para declarar un método:
- El nombre del procedimiento deben utilizar el prefijo
VOG. - Incluir un espacio después de un punto y coma al declarar varios argumentos.
- El punto y coma se debe utilizar al final del end.
- Los métodos deben nombrarse usando PascalCase, como las variables.
- Debe haber una línea en blanco entre las declaraciones de método.
- Evite asignar el mismo nombre que campos u acciones en el mismo ambito.
Ejemplo de uso correcto
Sección titulada «Ejemplo de uso correcto»local procedure VOGMyProcedure(Customer: Record Customer; Int: Integer)beginend;
// Blank line between methods
local procedure VOGMyProcedure2(Customer: Record Customer; Int: Integer)beginend;Métodos de llamada
Sección titulada «Métodos de llamada»Al llamar a un método, incluya un espacio después de cada comando si va a pasar varios parámetros.
Los paréntesis deben especificarse cuando se realiza una llamada al método o al sistema, como:
Ejemplo de uso correcto
Sección titulada «Ejemplo de uso correcto»VOGMyProcedure();VOGMyProcedure(1);VOGMyProcedure(1, 2);Declaración de publicador de eventos (EventPublisher)
Sección titulada «Declaración de publicador de eventos (EventPublisher)»para declarar un publicador de eventos:
- Todo evento debe estar precedido por el atributo
[IntegrationEvent(false, false)] - Los eventos deben declararse como local procedure a menos que se requiera acceso desde fuera del objeto.
- El cuerpo del procedimiento debe estar siempre vacío (begin end;). No se permite lógica dentro de un publisher.
- La nomenclatura debe comenzar con el prefijo
VOGseguido de una descripción del evento que se está publicando. La descripcion debe indicar el momento en que se produce el evento. Ejemplo:VOGOnBeforeValidateCustomerNo,VOGOnAfterInsertSalesHeader, etc.
Ejemplo de uso correcto
Sección titulada «Ejemplo de uso correcto»codeunit 50105 VOGBookingEvents{ // Este publicador se ejecuta cuando se valida el número de cliente en una cabecera de ventas [EventPublisher(ObjectType::Table, Database::"Sales Header", 'OnAfterValidateEvent', 'Sell-to Customer No.', false, false)] procedure VOGOnAfterValidateSellToCustomerNo(var Rec: Record "Sales Header"; var xRec: Record "Sales Header") begin end;}Declaración de suscriptor de eventos (EventSubscriber)
Sección titulada «Declaración de suscriptor de eventos (EventSubscriber)»Para declara un suscriptor de eventos:
- Nombrar con el prefijo
VOGseguido de una descripción del evento al que se está suscribiendo. - Los procedimientos de eventos deben ser locales (local procedure). No hay razón para que otra Codeunit llame manualmente a un suscriptor de eventos.
Ejemplo de uso correcto
Sección titulada «Ejemplo de uso correcto»codeunit 50105 VOGBookingEvents{ // Este suscriptor se ejecuta cuando se valida el número de cliente en una cabecera de ventas [EventSubscriber(ObjectType::Table, Database::"Sales Header", 'OnAfterValidateEvent', 'Sell-to Customer No.', false, false)] local procedure VOGOnAfterValidateSellToCustomerNo(var Rec: Record "Sales Header"; var xRec: Record "Sales Header") var OfficeMgt: Codeunit VOGOfficeManagement; begin // La lógica debe estar encapsulada o llamar a otros métodos para mantener el suscriptor limpio if Rec."Sell-to Customer No." <> '' then OfficeMgt.CheckCustomerOfficeAvailability(Rec."Sell-to Customer No."); end;}Permisos
Sección titulada «Permisos»Los permisos en AL permiten a los usuarios dar permiso a otros usuarios en función de sus necesidades particulares.
Tipos de Permisos
Sección titulada «Tipos de Permisos»| Valores de Permisos | Representacion | Descripción |
|---|---|---|
| R o r | Leer | R para letura directa, r para acceso de lectura indirecta. |
| I o i | Insertar | I para permiso de insercion directa, i para permiso de insercion indirecta. |
| M o m | Modificar | M para permiso de modificacion directa, m para permiso de modificacion indirecta. |
| D o d | Eliminar | D para permiso de eliminacion directa, d para permiso de eliminacion indirecta. |
| X or x | Execute (Run) | X para ejecutar permisos directamente, x para permisos de ejecucion indirecta. |
Ejemplo de uso correcto
PermissionSet 50145 VOGPermissionSet{ Permissions = TableData "VOGOfficeRoom" = RIMD, TableData "VOGMeetingRoomExtension" = RIMD;}Paginas de API
Sección titulada «Paginas de API»Las páginas de API son diferentes de las páginas de interfaz de usuario. Requieren diferentes propiedades y no se comportan de la misma manera. Dado que las páginas de API se utilizan para la integración con aplicaciones externas, deben tratarse como contratos. Para lograr esto, los siguientes temas son importantes.
- Aplicación de API independiente
- Propiedades de la página
- Control de versiones
- Propiedades de campo
- Campos predeterminadoS
Es una buena práctica desarrollar páginas de API en una aplicación independiente en lugar de combinarlas en una solución. Al hacerlo, proporciona una mejor capacidad de mantenimiento y es una buena manera de separar las preocupaciones
Las propiedades que se deben definir son:
APIPublisher
Sección titulada «APIPublisher»El nombre del editor de la API suele ser la empresa que crea la API. Es la primera parte personalizada de la dirección URL de un punto de conexión determinado. El valor no distingue entre mayúsculas y minúsculas.
APIPublisher = 'contoso';APIGroup
Sección titulada «APIGroup»Establece el grupo del punto de conexión de la API en el que se expone la página o la consulta. En la dirección URL, APIGroup viene después de APIPublisher. Se puede usar para distinguir diferentes aplicaciones de API o grupos de API entre sí. El valor no distingue entre mayúsculas y minúsculas.
APIGroup = 'app1';APIVersion
Sección titulada «APIVersion»Establece las versiones del punto de conexión de la API en el que se expone la página o la consulta. Esta propiedad no es obligatoria. Si no se especifica, las API se expondrán como versión ‘beta’.
La APIVersion se puede establecer en ‘beta’ o tener el formato ‘vx.y’. Ejemplo:
APIVersion = 'beta';Múltiples versiones de API
Sección titulada «Múltiples versiones de API»Nunca debe romper las versiones existentes. Cualquier cambio importante requiere la creación de una nueva versión.
Es posible exponer una API en varias versiones:
APIVersion = 'beta', 'v1.0';Esto permite publicar una nueva versión de una aplicación API sin copiar todos los objetos individuales y actualizar los números de versión. Solo es necesario copiar los objetos de API que se modifican en una nueva versión. Los demás objetos solo necesitan una adición a la propiedad APIVersion para estar disponibles en el punto de conexión de la nueva versión
EntitySetName
Sección titulada «EntitySetName»EntitySetName es el nombre de la entidad en plural. Piense en ello como el nombre de la colección de entidades. Se recomienda utilizar camelCasing para esta propiedad. ¡El valor distingue entre mayúsculas y minúsculas!
EntitySetName = 'itemCategories';EntityName (Nombre de la entidad)
Sección titulada «EntityName (Nombre de la entidad)»EntityName establece el nombre de entidad singular para la página o consulta de la API. Este nombre no se utiliza en la URL. En su lugar, se usa EntityName en la información de metadatos. Se recomienda utilizar camelCasing para esta propiedad.
EntityName = 'itemCategory';DelayedInsert
Sección titulada «DelayedInsert»Esta propiedad es necesaria en una página de API editable. No se aplica a un objeto de consulta de API. Si se establece en la página de la API, DelayedInsert no es necesario. Todas las páginas de API aplican el comportamiento para especificar primero todos los valores de campo y, a continuación, insertar el registro a la vez.Editable = false
DelayedInsert = true;Ejemplo Completo
Sección titulada «Ejemplo Completo»PageType = API;APIPublisher = 'contoso';APIGroup = 'app1';APIVersion = 'v1.0';EntitySetName = 'itemCategories';EntityName = 'itemCategory';DelayedInsert = true;Propiedades de campo
Sección titulada «Propiedades de campo»La estructura base de una página de API es similar a una página de lista de interfaz de usuario
layout{ area(Content) { repeater(records) { ... } }}Al especificar los campos, hay que tener en cuenta algunas consideraciones.
- No hay propiedades obligatorias. La propiedad no desempeña ningún papel en las páginas de la API, por lo que se puede omitir.
- La propiedad también es opcional y solo se debe usar en caso de que la aplicación externa requiera subtítulos y el título debe ser diferente del título estándar.
- El nombre del campo, debe definirse en camelCasing. No puede contener espacios, puntos u otros caracteres especiales
Campos obligatorios
Sección titulada «Campos obligatorios»- SystemId
- Este campo debe exponerse con el nombre id
- SystemModifiedAt
- Este campo debe exponerse con el nombre . Si eliges un nombre diferente, la funcionalidad del webhook no funcionará correctamente.
layout{ area(Content) { repeater(records) { field(id; Rec.SystemId) { } field(lastModifiedDateTime; Rec.SystemModifiedAt) { } } }}Accesibilidad y analisis de buenas practicas.
Sección titulada «Accesibilidad y analisis de buenas practicas.»Los siguientes puntos deben ser consideradas para el desarrollo de extensiones en AL.
Clasificacion de campos con DataClassification
Sección titulada «Clasificacion de campos con DataClassification»Debido a los requisitos de las leyes y normativas de privacidad, los campos de la clase de campo deben usar la propiedad DataClassification y su valor debe ser diferente de ToBeClassified. Esto se aplica a los campos de las tablas y las extensiones de tabla.
En la tabla siguiente se describen los niveles de confidencialidad de datos que se pueden asignar.
| Sensibilidad | Descripción |
|---|---|
| AccountData | Información de facturación del cliente e información del instrumento de pago, incluida la información de contacto del administrador, como el nombre, la dirección o el número de teléfono del administrador del inquilino. |
| CustomerContent | Contenido proporcionado o creado directamente por administradores y usuarios. El valor predeterminado es |
| SystemMetadata | Datos generados mientras se ejecuta el servicio o programa que no se pueden vincular a un usuario o inquilino. |
| EndUserIdentifiableInformation | (EUII) Datos que identifican o podrían utilizarse para identificar al usuario de un servicio de Microsoft. EUII no incluye contenido del Cliente. |
| EndUserPseudonymousIdentifiers | (EUPI) Un identificador creado por Microsoft y vinculado al usuario de un servicio de Microsoft. Al combinarse con otra información, como una tabla de mapeo, EUPI identifica al usuario final. EUPI no contiene información cargada ni creada por el cliente (contenido del cliente o EUII). |
Ejemplo de uso incorrecto
Sección titulada «Ejemplo de uso incorrecto»table 50100 VOGOfficeRoom{ fields { field(1;RoomName; Text[100]) { DataClassification = ToBeClassified; } }}
tableextension 50100 VOGOfficeRoom extends "VOG Office Room"{ fields { field(;RoomName; Text[100]) { DataClassification = ToBeClassified; } }}Ejemplo de uso incorrecto
Sección titulada «Ejemplo de uso incorrecto»table 50100 VOGOfficeRoom{ fields { field(1;RoomName; Text[100]) { DataClassification = AccountData; } }}
tableextension 50100 VOGOfficeRoom extends "VOG Office Room"{ fields { field(50100;RoomName; Text[100]) { DataClassification = AccountData; } }}Uso de ApplicationArea
Sección titulada «Uso de ApplicationArea»Ningún campo o acción es visible en Business Central sin la propiedad ApplicationArea. Se debe configurar como All por defecto, a menos que el requerimiento especifique lo contrario.
Se aplica a:
- Page Label
- Page Field
- Page Part
- Page System Part
- Page Chart Part
- Page Action
- Page Custom Action
- Page File Upload Action
- Page User Control
- Page
- Report
Tipo de Valores
Sección titulada «Tipo de Valores»| Valores | Descripcion |
|---|---|
| All | Se va a aplicar a todas las versiones |
| Basic | Solo se Aplica a la version basica de business central |
| Suite | Solo se Aplica a la version premium de business central |
Ejemplo de uso correcto
Sección titulada «Ejemplo de uso correcto» field(VOGModel; Rec."Model ID") { Caption = 'Modelo ID'; Editable = false; ApplicationArea = All; }Uso de UsageCategory
Sección titulada «Uso de UsageCategory»Para que una página o un reporte sea accesible a través del buscador de Dynamics 365 Business Central (conocido como Tell Me), es obligatorio declarar la propiedad UsageCategory.
Existen los siguientes valores para la propiedad UsageCategory:
| Valores | Descripcion |
|---|---|
| None | La página, el informe o la consulta no se incluyen en una búsqueda. |
| Lists | La pagina, el informa o la consulta se enumeran como Listas, en la categoria Paginas y Tareas |
| Tasks | La pagina, el informa o la consulta aparecen como Tareas, en la categoria Paginas y Tareas |
| ReportsAndAnalysis | La pagina, el informa o la consulta aparecen como Informes, en la categoria Informes y analisis |
| Documents | La pagina, el informa o la consulta aparecen como Informes y analisis, en la categoria Informes y analisis |
| History | La pagina, el informe o la consulta aparecen como Archivo en la categoria Informes y analisis |
| Administration | La consulta aparece como Administracion en la categoria Pagina y tareas |
Ejemplo de uso correcto
page 50100 VOGOfficeRoomList{ PageType = List; UsageCategory = Lists; ApplicationArea = All;}Uso del Tooltip
Sección titulada «Uso del Tooltip»Se debe establecer el uso de la propiedad ToolTip en todos los campos, acciones y partes de una página. Esta propiedad proporciona una descripción clara de la función o el propósito del elemento.
- Campos de página
- Acciones de página
- Parametro de Reportes;
Ejemplo de uso correcto
Sección titulada «Ejemplo de uso correcto» field(VOGModel; Rec."Model ID") { Caption = 'Modelo ID'; Editable = false; ApplicationArea = All; ToolTip = 'Identificador unico del modelo del producto.'; }Uso del this en variables globales
Sección titulada «Uso del this en variables globales»Al referenciar variables globales dentro de métodos o procedimientos, utilice siempre la palabra clave this para mejorar la claridad del código y evitar ambigüedades.
Ejemplo de uso incorrecto
Sección titulada «Ejemplo de uso incorrecto»var TotalAmount: Decimal; LineAmount: Decimal;
local procedure VOGCalculateTotal()begin TotalAmount := TotalAmount + LineAmount;end;Ejemplo de uso correcto
Sección titulada «Ejemplo de uso correcto»var TotalAmount: Decimal; LineAmount: Decimal;
local procedure VOGCalculateTotal()begin this.TotalAmount := this.TotalAmount + this.LineAmount;end;Uso de Labels en Objetos Report
Sección titulada «Uso de Labels en Objetos Report»Para todo texto estático que deba aparecer en el diseño del reporte (títulos de columnas, nombres de campos, leyendas),
es obligatorio el uso de las variables Labels en el objeto Report en lugar de variables enviadas al dataset.
- a diferencia de la variable tipo
Textse envían una sola vez en la estructura del reporte, reduciendo drásticamente el peso del dataset. - Están diseñados para ser capturados por los archivos de traducción (.xlf).
Ejemplo Incorrecto
Sección titulada «Ejemplo Incorrecto» column(InvoiceNoCaption; 'Nº Factura') // Esto se repite en cada fila { }Ejemplo incorrecto
Sección titulada «Ejemplo incorrecto»report 50100 "StandardInvoice"{ dataset { dataitem(Header; "Sales Invoice Header") { column(No_; "No.") { } } }
labels { InvoiceTitleCaption = 'Factura', Comment = 'Título del reporte'; CustomerNoCaption = 'Nº Cliente'; PageCaption = 'Página'; }}Especificar el Caption en campos de pagina
Sección titulada «Especificar el Caption en campos de pagina»Todos los campos que se muestran en un objeto de página deben tener definida la propiedad Caption
Ejemplo de uso incorrecto
page 50100 MyCustomerPage { layout { area(content) { field(CustomerName; Customer.Name) { ApplicationArea = All; // Falta Caption → advertencia AA0225 } } } }Ejemplo de uso correcto
page 50100 MyCustomerPage { layout { area(content) { field(CustomerName; Customer.Name) { Caption = 'Customer Name'; ApplicationArea = All; } } } }Uso del AutoFormatType en campos decimales
Sección titulada «Uso del AutoFormatType en campos decimales»Se debe aplicar la propiedad AutoFormatType, la cual determina el formato de visualización de los datos decimales en la interfaz de usuario.
Trabaja en conjunto con AutoFormatExpression para aplicar reglas de redondeo y símbolos de moneda.
El tipo de dato AutoFormatType solo se aplica a campos de tipo Decimal.
Ejemplo
Sección titulada «Ejemplo» field(VOGUnitPrice; Rec."Unit Price") { Caption = 'Precio Unitario'; AutoFormatType = 1; AutoFormatExpression = '0.00'; ApplicationArea = All; }Uso de Begin.. End
Sección titulada «Uso de Begin.. End»Solo use begin.. end para encerrar instrucciones compuestas
Ejemplo uso 1
Sección titulada «Ejemplo uso 1»// Ejemplo de uso incorrecto. El uso de begin.. end no es necesario para una sola instrucción
if FindSet() then begin repeat ... until next() = 0;end;// Ejemplo de uso correcto.
if FindSet() then repeat ... until next() = 0;Excepción del caso
if X then begin if Y then //Hacer Algoend else (not X)No utilice líneas en blanco:
- al principio o al final de cualquier función (después y antes de
begin end) - dentro de la expresión multilínea
- después de las líneas en blanco
Espaciado de comentarios
Sección titulada «Espaciado de comentarios»Comience siempre los comentarios con // seguido de un carácter de espacio
Ejemplo de uso incorrecto
Sección titulada «Ejemplo de uso incorrecto»RowNo += 1000; //Move way below the budgetEjemplo de uso correcto
Sección titulada «Ejemplo de uso correcto»RowNo += 1000; // Move way below the budgetEspaciado de CASE
Sección titulada «Espaciado de CASE»Una acción CASE debe comenzar en una línea después de la posibilidad.
Ejemplo de uso incorrecto
Sección titulada «Ejemplo de uso incorrecto»case Letter of 'A': Letter2 := '10'; 'B': Letter2 := '11'; end;Ejemplo de uso correcto
Sección titulada «Ejemplo de uso correcto»case Letterof 'A': Letter2:= '10'; 'B': Letter2:= '11';end;Orden de las declaraciones de variables
Sección titulada «Orden de las declaraciones de variables»Las declaraciones de variables deben ordenarse por tipo. En general, los tipos de objetos y variables complejas se enumeran primero, seguidos de las variables simples. El orden debe ser:
- Record
- Report
- Codeunit
- XmlPort
- Page
- Query
- Notification
- BigText
- DateFormula
- RecordId
- RecordRef
- FieldRef
- FilterPageBuilder
Ejemplo de uso incorrecto
Sección titulada «Ejemplo de uso incorrecto» StartingDateFilter: Text; Vendor: Record Vendor;
//IncorrectoEjemplo de uso correcto
Sección titulada «Ejemplo de uso correcto» Vendor: Record Vendor; StartingDateFilter: Text;
//CorrectoVerdadero/falso innecesario
Sección titulada «Verdadero/falso innecesario»No utilice palabras clave o innecesariamente si la expresión ya es una expresión lógica.true / false
Ejemplo de uso 1
Sección titulada «Ejemplo de uso 1»if IsPositive() = true then //Incorrectoif IsPositive() then //CorrectoEjemplo de uso 2
Sección titulada «Ejemplo de uso 2»if Complete <> true then //Incorrecto if not Complete then //CorrectoInvocaciones de objetos con nombre
Sección titulada «Invocaciones de objetos con nombre»Al llamar a un objeto de forma estática, utilice el nombre del objeto, no el identificador del objeto
Ejemplo de uso
Sección titulada «Ejemplo de uso» Page.RunModal(525, SalesShptLine); //Incorrecto Page.RunModal(Page::"Posted Sales Shipment Lines", SalesShptLine); //CorrectoVariables referenciadas en procedimientos
Sección titulada «Variables referenciadas en procedimientos»Cuando se pasa una variable por referencia a un procedimiento, utilice siempre la palabra clave var en la declaración del procedimiento.
Ejemplo de uso incorrecto
Sección titulada «Ejemplo de uso incorrecto»local procedure VOGUpdateCustomer(Customer: Record Customer)begin Customer.Name := 'New Name'; Customer.Modify();end;Ejemplo de uso correcto
Sección titulada «Ejemplo de uso correcto»local procedure VOGUpdateCustomer(var Customer: Record Customer)begin Customer.Name := 'New Name'; Customer.Modify();end;Uso de Constantes sin concantenaciones
Sección titulada «Uso de Constantes sin concantenaciones»No utilice la concatenación de cadenas para crear constantes. En su lugar, utilice placeholders para insertar valores dinamicos.
Ejemplo de uso incorrecto
Sección titulada «Ejemplo de uso incorrecto» // Uso de concatenación para crear un mensaje de error. No esta permitido.
procedure VOGShowCustomerMessage(CustomerName: Text; Balance: Decimal) begin Error('El cliente ' + CustomerName + ' tiene un saldo de ' + Format(Balance)); end;Ejemplo de uso correcto
Sección titulada «Ejemplo de uso correcto» // Uso de una variable Label con placeholders para crear un mensaje de error.
var CustomerMessageLbl: Label 'El cliente %1 tiene un saldo de %2';
procedure VOGShowCustomerMessage(CustomerName: Text; Balance: Decimal) begin Error(CustomerMessageLbl, CustomerName, Balance); end;Objetos obsoletos
Sección titulada «Objetos obsoletos»Cuando se utilice un objeto/campo obsoleto, se debe agregar la Propiedades:
obsoleteState: con un valor (Pending|Removed)obsoleteReason: con un comentario que explique por qué se está utilizando y cuándo se eliminará el uso del objeto obsoleto.
Ejemplo de uso correcto
Sección titulada «Ejemplo de uso correcto»tableextension 50100 VOGCustomerExt extends Customer{ fields { field(50100; VOGOldField; Text[50]) { Caption = 'Old Field'; ObsoleteState = Pending; ObsoleteReason = 'Este campo se reemplaza por NewField'; ObsoleteTag = 'v20.0'; // Campo Opcional } }}Ejemplo de uso incorrecto
Sección titulada «Ejemplo de uso incorrecto»field(50100; OldField; Text[50]){ Caption = 'Old Field'; ObsoleteState = Pending; // Falta ObsoleteReason → Error AA0213}Rendimiento y optimización
Sección titulada «Rendimiento y optimización»El cumplimiento de los siguientes puntos es obligatorio para garantizar la escalabilidad y eficiencia de las extensiones. Estas prácticas minimizan la carga en el Service Tier (NST),optimizar las consultas hacia el SQL Server y el consumo de recursos.
Uso del DeleteAll
Sección titulada «Uso del DeleteAll»Cuando realizas un DeleteAll en una tabla vacía, se produce un bloqueo de tabla. Por lo tanto, es buena práctica comprobar siempre si la tabla está vacía al realizar un DeleteAll.
Ejemplo de uso incorrecto
Sección titulada «Ejemplo de uso incorrecto» EmptyTableWLD.SetRange(Code, 'AJ'); EmptyTableWLD.DeleteAll(true);Ejemplo de uso correcto
Sección titulada «Ejemplo de uso correcto» EmptyTableWLD.SetRange(Code, 'AJ'); if not EmptyTableWLD.isEmpty() then EmptyTableWLD.DeleteAll(true);En el caso de cumplir
Uso de la propiedad SetLoadFields
Sección titulada «Uso de la propiedad SetLoadFields»Para el rendimiento del código, es importante que utilice SetLoadFields tanto como sea posible.
Si desea recuperar un registro de la base de datos para comprobar si el registro está disponible, utilice siempre SetLoadFields en los campos de clave principal de la tabla, de modo que solo se recuperen esos campos de la base de datos.
Ejemplo
Sección titulada «Ejemplo»if not Item.Get(ItemNo) then //Incorrecto exit();Item.SetLoadFields("No.");if not Item.Get(ItemNo) then //Correcto exit();No usar el OnFindRecord / OnNextRecord
Sección titulada «No usar el OnFindRecord / OnNextRecord»El uso de los triggers OnFindRecord y OnNextRecord a nivel de página es una de las prácticas más peligrosas para el rendimiento en Business Central si no se maneja con extremo cuidado.
Estos triggers deben considerarse como el último recurso. Su implementación incorrecta anula todas las optimizaciones nativas de SQL (como el paginado y el almacenamiento en caché) y puede congelar la interfaz de usuario.
Caso de degradación
Sección titulada «Caso de degradación»avegación del usuario en una lista.
Problema: Business Central carga registros en “bloques” (por ejemplo, de 50 en 50). Al escribir código en OnFindRecord o OnNextRecord, obligas al servidor a ejecutar lógica personalizada cada vez que el usuario hace scroll.
Impacto: Si la lógica interna incluye búsquedas en otras tablas o cálculos, el desplazamiento por la lista tendrá un retraso (lag) perceptible, aumentando el consumo de CPU en el NST.
Excepción
Sección titulada «Excepción»Solo se permite su uso en los siguientes casos:
Páginas basadas en Tablas Temporales: Cuando los datos no residen en SQL sino que se generan al vuelo (ej. una integración con un API externo que se muestra en una lista).
Buffers de integración: Cuando se necesita filtrar datos de una manera que el motor de SQL no puede procesar nativamente.
Uso de ReadIsolation en consultas de lecturas
Sección titulada «Uso de ReadIsolation en consultas de lecturas»Al realizar operaciones de lectura en registros, es fundamental utilizar la propiedad ReadIsolation para garantizar la consistencia y la integridad de los datos. Esta práctica ayuda a evitar problemas relacionados con lecturas sucias o inconsistentes, especialmente en entornos concurrentes donde múltiples procesos pueden acceder y modificar los mismos datos simultáneamente.
Se debe utilizar ReadIsolation siempre que se realice una lectura de datos que no tenga intención de modificar registros inmediatamente.
Niveles de Aislamiento Permitidos
Sección titulada «Niveles de Aislamiento Permitidos»| Nivel de Aislamiento | Descripción | Casos de Uso Recomendados |
|---|---|---|
| Default | El sistema utiliza el aislamiento por defecto de Business Central (generalmente ReadCommitted). | Consultas estándar sin requisitos especiales. |
| ReadCommitted | Garantiza que la transacción solo lea datos que han sido confirmados (commit). Evita “lecturas sucias”. | Operaciones financieras o contables donde la precisión es obligatoria. |
| ReadUncommitted | El nivel más permisivo. No emite bloqueos compartidos y no respeta bloqueos exclusivos. Puede leer datos modificados pero no confirmados. | Dashboards, Estadísticas y Reports. Ideal para contar registros disponibles sin bloquear el procesos. |
| RepeatableRead | Asegura que si se lee un registro dos veces en la misma transacción, los datos serán iguales. Bloquea los registros leídos. | Procesos de validación complejos donde los datos no deben cambiar durante el cálculo. |
| UpdLock | Lee el registro y coloca un bloqueo de actualización inmediato. Ninguna otra transacción puede obtener un bloqueo de actualización o exclusivo. Prevención de condiciones de carrera. | Úsalo antes de un Modify para asegurar que nadie toque el registro mientras lo procesas. |
Ejemplo 1
Sección titulada «Ejemplo 1»// Estándar para consultas de solo lectura (Reporting/Dashboards)procedure VOGGetTotalAvailableRooms(): Integervar OfficeRoom: Record "VOF Office Room";begin // Evita bloqueos en la tabla mientras otros usuarios modifican OfficeRoom.ReadIsolation := IsolationLevel::ReadUncommitted; OfficeRoom.SetRange(Status, OfficeRoom.Status::Available); exit(OfficeRoom.Count());end;Ejemplo 2
Sección titulada «Ejemplo 2»// Estándar para preparación de actualizaciónprocedure VOGMarkRoomAsOccupied(RoomNo: Code[20])var OfficeRoom: Record "VOF Office Room";begin // Bloquea el registro específicamente para esta transacción OfficeRoom.ReadIsolation := IsolationLevel::UpdLock; if OfficeRoom.Get(RoomNo) then begin OfficeRoom.Status := OfficeRoom.Status::Occupied; OfficeRoom.Modify(true); end;end;Uso lockTable en la modificación de registros
Sección titulada «Uso lockTable en la modificación de registros»El método LockTable se utiliza para asegurar que los datos no cambien entre el momento en que se leen y el momento en que se modifican. Su uso debe ser cuidadoso y seguir el principio de “bloquear lo más tarde posible”.
Debe seguir los siguientes lineamientos al utilizar LockTable:
- Siempre debe llamarse antes de la función de lectura (Get, Find, FindFirst). Si se llama después de leer, el bloqueo no garantiza que los datos leídos sigan siendo los mismos.
- El bloqueo dura hasta que la transacción termina. Por ello, se prohíbe poner un LockTable al inicio de un proceso largo si no es estrictamente necesario.
- No usar LockTable en tablas maestras (como Customer o Item) a menos que sea estrictamente necesario, ya que detiene el trabajo de todos los usuarios de la empresa.
- Nunca mostrar un Confirm o un Error largo después de un LockTable. La tabla quedará bloqueada para el resto de la empresa.
- Antes de usar LockTable, considera si puedes usar
ReadIsolation := IsolationLevel::UpdLock, que es más moderno y preciso.
// Estándar para actualización de estado de oficinaprocedure VOGFinalizeVofReservation(RoomNo: Code[20])var OfficeRoom: Record VOGOfficeRoom;begin // 1. Declarar el bloqueo antes de la lectura OfficeRoom.LockTable();
// 2. Obtener el registro (ahora bloqueado para otros) if OfficeRoom.Get(RoomNo) then begin // 3. Lógica de modificación OfficeRoom.Status := OfficeRoom.Status::Occupied; OfficeRoom.Modify(true); end; // El bloqueo se libera automáticamente al terminar el método (End)end;Evitar el uso innecesario de CalcFields
Sección titulada «Evitar el uso innecesario de CalcFields»El método CalcFields puede ser costoso en términos de rendimiento, especialmente si se utiliza en grandes conjuntos de datos o dentro de bucles. Por ello, su uso debe ser limitado a situaciones donde sea absolutamente necesario calcular campos específicos.
El estándar moderno exige sustituirlo por técnicas de carga anticipada.
Se deben seguir las siguientes directrices:
- Utilice
SetAutoCalFieldspara especificar qué campos calculados se deben cargar antes de realizar una operación de lectura masiva (FindSet). Esto reduce la cantidad de llamadas a la base de datos. - Si un campo calculado se consulta con demasiada frecuencia y afecta al rendimiento, se debe evaluar convertirlo en un campo físico que se actualice mediante lógica de eventos (reduciendo la carga de cálculo en tiempo real).
- Está estrictamente prohibido usar CalcFields dentro de un bucle repeat until. En su lugar, carga los campos necesarios fuera del bucle.
Ejemplo 1: Uso innecesario de CalcFields
Sección titulada «Ejemplo 1: Uso innecesario de CalcFields»// Realiza múltiples llamadas a SQL por cada registroif OfficeRoom.FindSet() then repeat OfficeRoom.CalcFields(VOGOccupancy Rate); // Llamada extra por cada iteración TotalRate += OfficeRoom.VOGOccupancyRate; until OfficeRoom.Next() = 0;Ejemplo 2: Uso recomendado con SetLoadFields
Sección titulada «Ejemplo 2: Uso recomendado con SetLoadFields»// Optimiza la lectura en una sola consulta SQLOfficeRoom.SetAutoCalFields(VOGOccupancyRate,VOGRoomName);if OfficeRoom.FindSet() then repeat // El campo ya está cargado en memoria desde el FindSet TotalRate += OfficeRoom.VOGOccupancyRate; until OfficeRoom.Next() = 0;Ejemplo 2: Uso recomendado con OnOpenPage
Sección titulada «Ejemplo 2: Uso recomendado con OnOpenPage»// Optimiza la lectura en una sola consulta SQL
page 50100 "VOGCustomerBalanceList"{ PageType = List; ApplicationArea = All; UsageCategory = Lists; SourceTable = Customer; Caption = 'Lista de Balance de Cliente';
layout { area(Content) { repeater(GroupName) { field("No."; Rec."No.") { ApplicationArea = All; } field(Balance; Rec.Balance) { ApplicationArea = All; } } } }
trigger OnOpenPage() begin // Preparamos al registro anl momento de ingresar en la pagina Rec.SetAutoCalcFields(Balance); end;
}Uso del Find() y FindSet()
Sección titulada «Uso del Find() y FindSet()»Los métodos FindSet() o Find() deben usarse solo en relación con el método Next().
No se debe usar el valor Get() FindFirst() o FindLast() con el metodo Next() .Si no, estás desperdiciando CPU y ancho de banda porque se cargan varios registros pero solo usas uno.
Ejemplo incorrecto
codeunit 1 MyCodeunit{ var VOGcustomer: Record Customer;
procedure VOGFoo() begin if customer.FindFirst() then repeat ... until customer.Next() = 0; end;}Ejemplo correcto
codeunit 1 MyCodeunit{ var VOGcustomer: Record Customer;
procedure VOGFoo() begin if customer.FindSet() then repeat ... until customer.Next() = 0; end;}Priorizar el uso del get()
Sección titulada «Priorizar el uso del get()»A diferencia de otros métodos de búsqueda, Get accede directamente a la clave primaria, lo que permite al motor de SQL optimizar la búsqueda al máximo.
Es obligatorio utilizar el método Get siempre que se conozca la clave primaria del registro. Se debe evitar el uso de filtros (SetRange/SetFilter) seguidos de FindFirst para recuperar un único registro conocido.
Caso de uso
Sección titulada «Caso de uso»// Busquedad por llave primariaGLEntry(1234) //Excelente
// Busqueda por indiceGLEntry.SetRange("Document No",'INV11020') //BuenaGLEntry.FindFirst();
// Escaneo de indiceGLEntry.SetRange("Global Dimensión 2 Code",'INV11020') //DeficienteGLEntry.FindFirst();
// Escaneo de la tablaGLEntry.SetRange("FA Entry No.",1234) //DeficienteGLEntry.FindFirst();Iterar datos eficientemente
Sección titulada «Iterar datos eficientemente»- No cambiar filtros/loadfields/autocalfields una vez haya comenzado la interación
- evitar
SetCurrentKeyen los campos siendo modficados - Mejorar la actualización de los registro iterando
Ejemplo de uso incorrecto
Sección titulada «Ejemplo de uso incorrecto»procedure Slow()var rec,rec2: Record Mytablebegin if rec.findSet() then repeat rec2 := rec; rec.MyField := rec2.MyField + 1 rec.Modify(); until rec.next() = 0endEjemplo de uso correcto
Sección titulada «Ejemplo de uso correcto»procedure Fast()var rec,rec2: Record Mytablebegin if rec.findSet() then repeat rec.MyField := rec.MyField + 1 rec.Modify(); until rec.next() = 0endIndexar campos FlowField()
Sección titulada «Indexar campos FlowField()»Puedes aumentar potencialmente el rendimiento si los campos usados en FlowFields se añaden a SumIndexedFields de la clave correspondiente.
Cuando hay problemas de rendimiento en las páginas de lista, la causa raíz suele ser que muestran FlowFields definidos sobre tablas que no están suficientemente indexadas.
Ejemplo de uso incorrecto
Sección titulada «Ejemplo de uso incorrecto»table 18 Customer{... fields {... field(97; "Debit Amount"; Decimal) { ... CalcFormula = Sum ("Detailed Cust. Ledg. Entry"."Debit Amount" WHERE("Customer No." = FIELD("No."), "Entry Type" = FILTER(<> Application), "Initial Entry Global Dim. 1" = FIELD("Global Dimension 1 Filter"), "Initial Entry Global Dim. 2" = FIELD("Global Dimension 2 Filter"), "Posting Date" = FIELD("Date Filter"), "Currency Code" = FIELD("Currency Filter"))); FieldClass = FlowField; ... } ... } keys { ... } ...}table 379 "Detailed Cust. Ledg. Entry"{... fields { ... } keys { key(Key1; "Entry No.") { Clustered = true; // Sin indexar campo "Debit Amount" } } ...}Ejemplo de uso correcto
Sección titulada «Ejemplo de uso correcto»table 18 Customer{... fields {... field(97; "Debit Amount"; Decimal) { ... CalcFormula = Sum ("Detailed Cust. Ledg. Entry"."Debit Amount" WHERE("Customer No." = FIELD("No."), "Entry Type" = FILTER(<> Application), "Initial Entry Global Dim. 1" = FIELD("Global Dimension 1 Filter"), "Initial Entry Global Dim. 2" = FIELD("Global Dimension 2 Filter"), "Posting Date" = FIELD("Date Filter"), "Currency Code" = FIELD("Currency Filter"))); FieldClass = FlowField; ... } ... } keys { ... } ...}table 379 "Detailed Cust. Ledg. Entry"{... fields { ... } keys { key(Key1; "Entry No.") { Clustered = true; } key(Key2; "Customer No.", "Entry Type", "Initial Entry Global Dim. 1", "Initial Entry Global Dim. 2", "Posting Date", "Currency Code") { SumIndexFields = "Debit Amount" // Declaracion del campo indexado } } ...}Evitar bloqueadores de operaciones masivas
Sección titulada «Evitar bloqueadores de operaciones masivas»Evitar bloqueadores de operaciones masivas:
La presencia de suscriptores a eventos, disparadores de base de datos o filtros de seguridad puede convertir operaciones rápidas de ModifyAll o DeleteAll en procesos lentos fila por fila
Casos de uso
Sección titulada «Casos de uso»Ejemplo incorrecto
Sección titulada «Ejemplo incorrecto»// Uso de Triggers (Disparadores)
// Si la tabla "Item" tiene mucho código en su trigger OnModify...Item.SetRange("Item Category Code", 'HARDWARE');// El 'true' hará que por cada producto se ejecute TODA la lógica de la tabla Item.Item.ModifyAll("Safety Lead Time", '2D', true);
// Uso de Suscriber Event
[EventSubscriber(ObjectType::Table, Database::"Sales Line", 'OnAfterModifyEvent', '', false, false)]local procedure OnAfterModifySalesLine(var Rec: Record "Sales Line")begin // Este código se ejecutará 10,000 veces si modificas 10,000 líneas. // SQL no puede hacer un UPDATE masivo porque tiene que enviarle cada 'Rec' a AL. VOGLogMgt.LogChange(Rec."Document No.", Rec."Line No.");end;
SalesLine.SetRange("Document Type", SalesLine."Document Type"::Order);SalesLine.ModifyAll("Shipment Date", Today, false); // <--- DEGRADADO A FILA POR FILAOptimización de busqueda de Texto
Sección titulada «Optimización de busqueda de Texto»Si estás diseñando una tabla en tu extensión y sabes que los usuarios buscarán descripciones largas, debes habilitar la propiedad OptimizeForTextSearch
Ejemplo
Sección titulada «Ejemplo»field(20; Description; Text[100]){ // Optimiza el campo para búsquedas de texto parcial en SQL OptimizeForTextSearch = true;}Uso del Query.SaveAsJson
Sección titulada «Uso del Query.SaveAsJson»El método Query.SaveAsJson en AL es una función eficiente para exportar datos estructurados directamente desde un objeto Query de Business Central a un formato JSON.
A diferencia de recorrer un Record y construir un JSON manualmente con JsonObject.Add, esta función delega el trabajo al servidor, lo que la hace mucho más rápida para grandes volúmenes de datos.
Sintaxis
Sección titulada «Sintaxis»Query.SaveAsJson(OutStream);
-
OutStream: Es el flujo de salida donde se escribirá el contenido JSON.
-
Retorno: Devuelve un booleano (True si tuvo éxito).
Ejemplo Practico
Sección titulada «Ejemplo Practico»codeunit 50100 "ExportDataToJSON"{ procedure ExportCustomers() var MyCustQuery: Query "My Customer Query"; // Tu objeto Query TempBlob: Codeunit "Temp Blob"; OutStr: OutStream; InStr: InStream; FileName: Text; begin FileName := 'ClientesExport.json';
// 1. Crear el OutStream usando Temp Blob TempBlob.CreateOutStream(OutStr);
// 2. Ejecutar y guardar el Query como JSON if MyCustQuery.SaveAsJson(OutStr) then begin
// 3. Descargar el archivo para el usuario TempBlob.CreateInStream(InStr); DownloadFromStream(InStr, 'Descargar JSON', '', '', FileName);
end else Error('No se pudo generar el JSON.'); end;}Usar colecciones nativas (Listas/Diccionarios) en cambio de tablas temporales
Sección titulada «Usar colecciones nativas (Listas/Diccionarios) en cambio de tablas temporales»El uso de estructuras de datos en memoria debe priorizar el rendimiento y la simplicidad. Se debe evitar la creación de tablas temporales.
- No usar tablas temporales en para operaciones que pueden resolverse con tipos de datos nativos (
List\Disctionary). - No Usar tablas temporarias para copiar grandes sets de datos de una tabla física a menos que el tamaño del conjunto esté estrictamente controlado y justificado.
- Antes de usar una tabla temporal para “clonar” datos, evalúe si puede filtrar el registro original o utilizar un objeto Query para procesar la información directamente desde SQL.
Uso del DataTransfer
Sección titulada «Uso del DataTransfer»El objeto DataTransfer debe utilizarse obligatoriamente cuando se requiera mover o copiar datos entre tablas. Por ejemplo, de una tabla a otra o de un campo a otro en una actualización de extensión,sin necesidad de aplicar lógica de validación de AL.
Caso de uso
Sección titulada «Caso de uso»Migración de datos entre tablas con la misma estructura o actualización masiva de campos tras un cambio de esquema.
internal procedure MigrarDatosEficientemente()var DataTransfer: DataTransfer; Origen: Record "Old Table"; Destino: Record "New Table";begin // Definimos el mapeo de campos DataTransfer.SetTables(Database::"Old Table", Database::"New Table"); DataTransfer.AddFieldValue(Origen.FieldNo("Old Code"), Destino.FieldNo("New Code")); DataTransfer.AddFieldValue(Origen.FieldNo("Old Name"), Destino.FieldNo("New Name"));
// Filtramos si es necesario DataTransfer.AddSourceFilter(Origen.FieldNo(Status), '=%1', Origen.Status::Active);
// Ejecución en SQL (sin triggers de AL) DataTransfer.CopyRows();end;Uso de Funciones Vinculadas a Campos (Expression Fields)
Sección titulada «Uso de Funciones Vinculadas a Campos (Expression Fields)»Se debe priorizar el uso de funciones directamente vinculadas, en lugar de realizar cálculos masivos en el trigger OnAfterGetRecord.
Caso de uso
Sección titulada «Caso de uso»Mostrar un dato calculado (ej. un estado de crédito dinámico o un semáforo de colores) en una lista.
-
Mala Práctica: Calcular el valor para todos los registros dentro de
OnAfterGetRecordy guardarlo en una variable global de la página. -
Impacto: Business Central ejecuta
OnAfterGetRecordpara cada fila que se procesa en el dataset, incluso si el usuario no ha llegado a ver esas filas en el scroll. Esto consume ciclos de CPU del NST de forma innecesaria.
trigger OnAfterGetRecord()begin // Esto se ejecuta para CADA fila del dataset al cargar EstadoCliente := CustomLogic.CalcularEstado(Rec);end;-
Solución: Crear una función local que devuelva el valor deseado y asignarla directamente al campo en el diseño de la página.
-
Justificación: Al vincular una función a un campo, el cliente de Business Central solo invoca la función para los registros que son actualmente visibles en la pantalla del usuario. Si el usuario no hace scroll, el cálculo para los registros inferiores nunca se ejecuta.
field(Estado; VOGObtenerEstadoDinamico()) // La función se vincula directamente{ ApplicationArea = All; Caption = 'Estado de Crédito';}
// En la sección de procedimientos:local procedure VOGObtenerEstadoDinamico(): Textbegin // Este código SOLO se ejecuta para las filas visibles en pantalla exit(CustomLogic.CalcularEstado(Rec));end;Consideraciones
Sección titulada «Consideraciones»-
Variables Globales: El uso de funciones en SourceExpr reduce la necesidad de declarar variables globales en la página, limpiando la memoria de la sesión.
-
Propiedad Editable: Los campos vinculados a funciones deben tener siempre la propiedad Editable = false.
-
Cálculos Pesados: Si la función vinculada requiere consultas SQL complejas, se debe asegurar que los campos necesarios para esa consulta hayan sido cargados previamente mediante SetLoadFields o SetAutoCalcFields.
Evitar logica pesada en los suscriptores de OnOpenCompany
Sección titulada «Evitar logica pesada en los suscriptores de OnOpenCompany»Se prohíbe la inclusión de lógica de negocio compleja, procesos de sincronización masiva o cálculos de datos pesados dentro del trigger OnOpenCompany. Este trigger debe reservarse exclusivamente para verificaciones de configuración mínimas y ligeras.
Si el trigger contiene consultas a tablas con millones de registros, bucles de validación o llamadas a APIs externas, el usuario verá una pantalla en blanco o el círculo de carga durante varios segundos (o incluso minutos) antes de poder trabajar.
Causa de degradación
Sección titulada «Causa de degradación»Si 100 usuarios inician sesión al mismo tiempo, el servidor intentará ejecutar 100 veces esa lógica pesada, saturando los recursos de CPU y memoria de la instancia.
Consideraciones
Sección titulada «Consideraciones»-
¿Hay llamadas externas? (HTTP Requests): Prohibido. El inicio de sesión no debe depender de la disponibilidad de un servicio externo.
-
¿Hay bucles repeat..until? (Iteración de tablas): Prohibido. Cualquier búsqueda debe ser por clave primaria (Get).
-
¿Se modifican registros? (Escrituras en base de datos): Evitar. Las escrituras durante el login pueden causar bloqueos de tabla (Table Locking) si muchos usuarios entran a la vez.
Usar PageBackground Tasks para calculos pesados
Sección titulada «Usar PageBackground Tasks para calculos pesados»El uso de Page Background Tasks (PBT) permite que la interfaz de usuario permanezca interactiva mientras el servidor realiza cálculos complejos en segundo plano.
Caso de uso
Sección titulada «Caso de uso»Una Role Center o una Ficha de Cliente que muestra KPIs complejos (ej. “Total de Ventas año actual vs año anterior”).
-
El Problema: Si estos cálculos se ejecutan en el OnAfterGetRecord o OnOpenPage, el usuario no puede hacer clic en nada ni navegar por la página hasta que el cálculo termine.
-
La Solución: PBT dispara una sesión hija asíncrona que realiza el cálculo. El usuario puede empezar a trabajar inmediatamente, y los resultados aparecen en pantalla en cuanto el proceso termina.
//Esta Codeunit hace el "trabajo sucio". No puede modificar datos, solo calcular y devolver resultados.
codeunit 50100 "CustomerKPIProcessor"{ trigger OnRun() var CustNo: Code[20]; Results: Dictionary of [Text, Text]; TotalSales: Decimal; AvgDays: Decimal; begin // Leer parámetros enviados desde la página CustNo := Page.GetBackgroundParameters().Get('CustNo');
// Lógica pesada: Simulación de cálculos complejos TotalSales := CalculateLastYearSales(CustNo); AvgDays := CalculateAvgPaymentDays(CustNo);
// Empaquetar resultados Results.Add('TotalSales', Format(TotalSales, 0, 9)); Results.Add('AvgDays', Format(AvgDays, 0, 9));
Page.SetBackgroundTaskResult(Results); end;
local procedure CalculateLastYearSales(CustNo: Code[20]): Decimal var CustLedgerEntry: Record "Cust. Ledger Entry"; begin CustLedgerEntry.SetRange("Customer No.", CustNo); CustLedgerEntry.SetRange("Document Type", CustLedgerEntry."Document Type"::Invoice); CustLedgerEntry.SetRange("Posting Date", CalcDate('<-1Y>', Today), Today); CustLedgerEntry.CalcSums("Sales (LCY)"); exit(CustLedgerEntry."Sales (LCY)"); end;
local procedure CalculateAvgPaymentDays(CustNo: Code[20]): Decimal begin // Aquí iría una lógica compleja de iteración... Sleep(2000); // Simulamos un retraso de 2 segundos exit(45.5); end;}// Aquí disparamos la tarea y recibimos los datos sin bloquear al usuario.
pageextension 50101 "CustomerKPICardExt" extends "Customer Card"{ layout { addlast(General) { group(PerformanceKPIs) { Caption = 'Indicadores de Rendimiento (Segundo Plano)';
field(AvgPaymentDays; AvgPaymentDaysVar) { ApplicationArea = All; Caption = 'Promedio Días Pago'; Editable = false; } field(LastYearSales; LastYearSalesVar) { ApplicationArea = All; Caption = 'Ventas Año Pasado'; Editable = false; } } } }
var AvgPaymentDaysVar: Decimal; LastYearSalesVar: Decimal;
trigger OnAfterGetCurrRecord() var TaskParameters: Dictionary of [Text, Text]; begin // Reiniciar variables para que el usuario vea que se están cargando AvgPaymentDaysVar := 0; LastYearSalesVar := 0;
TaskParameters.Add('CustNo', Rec."No.");
// Encolar la tarea CurrPage.EnqueueBackgroundTask(Codeunit::"CustomerKPIProcessor", TaskParameters, 10000, PageBackgroundTaskPriority::Normal); end;
trigger OnPageBackgroundTaskCompleted(TaskId: Integer; Results: Dictionary of [Text, Text]) var ResultValue: Text; begin // Recuperar y evaluar los resultados del diccionario if Results.Get('AvgDays', ResultValue) then Evaluate(AvgPaymentDaysVar, ResultValue);
if Results.Get('TotalSales', ResultValue) then Evaluate(LastYearSalesVar, ResultValue); end;
trigger OnPageBackgroundTaskError(TaskId: Integer; ErrorCode: Text; ErrorText: Text; ErrorCallStack: Text; var IsHandled: Boolean) begin // Si algo falla, evitamos que el error bloquee la página IsHandled := true; Message('No se pudieron cargar los KPIs: %1', ErrorText); end;}Aquí disparamos la tarea y recibimos los datos sin bloquear al usuario.
Sección titulada «Aquí disparamos la tarea y recibimos los datos sin bloquear al usuario.»Se prohíbe realizar llamadas externas mediante HttpClient de forma sincrónica dentro de triggers que afecten la interfaz de usuario
(como OnOpenPage, OnValidate o OnAfterGetRecord).
Estas llamadas deben delegarse a procesos asíncronos para proteger la disponibilidad del servicio.
Caso de uso
Sección titulada «Caso de uso»El sistema consulta un servicio de logística externo para obtener el estado de un paquete al abrir la Factura de Venta.
-
El Problema: Si el servicio externo tarda 10 segundos en responder (o está caído), la sesión del usuario en Business Central se congela totalmente durante ese tiempo.
-
Impacto en el servidor: El servidor de Business Central tiene un número limitado de hilos (threads). Si muchos usuarios ejecutan llamadas sincrónicas lentas simultáneamente, se puede agotar la capacidad del servidor, provocando una caída del servicio para todos los usuarios.
Soluciones
Sección titulada «Soluciones»A. Uso de Page Background Tasks (PBT) - Para Consultas Visuales Si el dato externo solo se necesita para visualizarlo en pantalla (ej. consultar un saldo en un portal externo), utiliza un PBT como vimos anteriormente.
Documentación del código
Sección titulada «Documentación del código»El archivo README.md en la raíz del proyecto debe contener una descripción clara y concisa de la extensión, incluyendo su propósito, funcionalidades principales, requisitos previos y cualquier otra información relevante para los desarrolladores y usuarios finales.
La estructura recomendada para el archivo README.md es la siguiente:
Nombre de la extensión
Sección titulada «Nombre de la extensión»Debe ser el título principal del archivo README.md
Descripción
Sección titulada «Descripción»Una breve descripción de la extensión, su propósito y las funcionalidades que ofrece.
Rangos de Objetos
Sección titulada «Rangos de Objetos»El rango de id de los objetos utilizados en la extensión.
Descripcion de objetos
Sección titulada «Descripcion de objetos»Una lista de los objetos principales incluidos en la extensión, con una breve descripción de cada uno. En el caso de las tabla y las paginas, incluir una lista de los campos principales y su descripción.
Requisitos Previos
Sección titulada «Requisitos Previos»Instrucciones sobre los requisitos previos necesarios para instalar y utilizar la extensión, como versiones específicas de Business Central o dependencias de otras extensiones. Esta sección es opcional.
Uso Funcional
Sección titulada «Uso Funcional»Instrucciones sobre cómo utilizar la extensión, incluyendo ejemplos de uso si es necesario.
Ejemplo de README.md
Sección titulada «Ejemplo de README.md»Se puede encontrar un ejemplo de archivo README.md siguiendo este estándar en el siguiente repositorio de GitHub: