BC Way - Add Approval workflows on custom tables





Alguna vez te has preguntado cómo hace Business Central para que un pedido de venta pase por "Enviar a aprobación", "Pendiente", "Aprobado" o "Rechazado"? Pues bien, toda esa maquinaria (el motor de Workflows + el módulo de Approvals) "ya existe" en la plataforma, pero solo viene cableada para las tablas estándar (Sales Header, Purchase Header, Customer, etc.).

La buena noticia es que ese motor es extensible. Si tienes una tabla propia, en nuestro caso la `Statistical Account`; puedes "engancharla" al mismo sistema y conseguir exactamente la misma experiencia: botones de aprobación, estados, notificaciones, sin reinventar nada.

En este documento te cuento, sin tecnicismos innecesarios qué piezas montamos y para qué sirve cada una. La idea es que cualquiera que lea esto entienda el "por qué" detrás de cada paso, no solo el "qué".

1- Contexto funcional:
Imagina que las Cuentas Estadísticas son registros importantes que no deberían modificarse a la ligera. Queremos que, antes de darlas por buenas, alguien con autoridad las revise y las apruebe. Es decir, queremos el clásico flujo de "cuatro ojos".

Lo que el usuario final espera ver es esto:

- Un campo Estado de aprobación visible en la ficha y en la lista, con colores (gris = abierto, amarillo = pendiente, verde = aprobado, rojo = rechazado).
- Un botón Enviar solicitud de aprobación que dispara el flujo.
- Un botón Cancelar solicitud de aprobación por si nos arrepentimos.
- Que mientras está Pendiente o Aprobado, el registro quede bloqueado para edición y para borrado (no queremos que alguien cambie algo ya aprobado a escondidas).
- Que el aprobador reciba su tarea en su bandeja de "Solicitudes de aprobación" de siempre, igual que con cualquier documento estándar.

En resumen: que la Cuenta Estadística se comporte como un documento "de primera clase" dentro de BC, con su ciclo de vida de aprobación completo.

2- Contexto técnico:
Para conseguirlo nos apoyamos en tres grandes piezas estándar de Business Central:
SubsistemaCodeunit estándarUso principal
Motor de WorkflowsWorkflow Management, Workflow Setup, Workflow Event HandlingDefinir eventos, respuestas y plantillas de flujo
Módulo de ApprovalsApprovals Mgmt.Crear solicitudes de aprobación y gestionar aprobadores
Respuestas de WorkflowWorkflow Response HandlingEjecutar acciones cuando el flujo avanza

La integración gira alrededor del patrón publisher / subscriber.

Nosotros publicamos nuestros propios eventos, como “enviar Cuenta Estadística a aprobación”, y después nos suscribimos a eventos estándar de Business Central para indicarle cómo debe tratar nuestra tabla.

El patrón habitual dentro de los subscribers será algo como esto:
case RecRef.Number of
    Database::"Statistical Account":
        // Aquí va nuestra lógica
end;

Ese case es fundamental. Business Central lanza muchos eventos de forma genérica, pero nosotros solo queremos reaccionar cuando el registro pertenece a nuestra tabla.

Los objetos principales que necesitaremos son:

ObjetoUso
Enum BCS Stat. Acc. Appr. StatusDefine los estados del flujo
Table Extension BCS Statistical AccountAñade el campo de estado y reglas de bloqueo
Codeunit BCS Stat. Acc. Approval Mgmt.Contiene la lógica de aprobación
Codeunit BCS Stat. Acc. Workflow SetupRegistra categoría, plantilla y relaciones
Page ExtensionsAñaden botones, colores y control de edición

3- Desarrollo:
Nunca prometí que esta funcionalidad fuese rapida de hacer, porque no lo es. La tenemos dividida en 12 pasos

Paso 1 - Crear el Enum de estado

Lo primero es definir los posibles estados de aprobación.

enum 60702 "BCS Stat. Acc. Appr. Status"
{
    Extensible = true;
    value(0; "Open") { Caption = 'Open'; }
    value(1; "Pending Approval") { Caption = 'Pending Approval'; }
    value(2; "Rejected") { Caption = 'Rejected'; }
    value(3; "Approved") { Caption = 'Approved'; }
}

Este enum actúa como la fuente de verdad del ciclo de vida del registro.

Lo dejamos como Extensible = true por si más adelante necesitamos estados adicionales (aunque es mas improbable que alguien lo extienda de lo que seria que nos den un mes más de vacaciones de gratis).


Paso 2 - Añadir el campo a la tabla

Después añadimos el campo de estado a la tabla mediante una extensión.

field(60701; "BCS Approval Status"; Enum "BCS Stat. Acc. Appr. Status")
{
    Caption = 'Approval Status';
    DataClassification = CustomerContent;
    Editable = false; // el usuario no lo modifica manualmente
}

El campo no debería ser editable directamente por el usuario. Lo moverá el flujo de aprobación.


Paso 3 - Proteger el registro

Importante que controlemos el bloqueo de modificación y borrado según estado

trigger OnBeforeDelete()
var
    ApprovalsMgmt: Codeunit "Approvals Mgmt.";
begin
    BCSCheckApprovalStatus();
    ApprovalsMgmt.DeleteApprovalEntries(Rec.RecordId);
    BCSAttachmentManagement.DeleteRelatedDocumentAttachments(Rec."No.");
end;

local procedure BCSCheckApprovalStatus()
begin
    if Rec."BCS Approval Status" in
        [Rec."BCS Approval Status"::Approved,
         Rec."BCS Approval Status"::"Pending Approval"]
    then
        Error(StatusErr, Rec."BCS Approval Status");
end;

procedure ApprovalStatusAllowModify(): Boolean
begin
    exit(not (Rec."BCS Approval Status" in
        [Rec."BCS Approval Status"::Approved,
         Rec."BCS Approval Status"::"Pending Approval"]));
end;

Este bloque evita que alguien borre o modifique un registro que está pendiente o aprobado.

Además, al borrar un registro permitido, se limpian las entradas de aprobación relacionadas para evitar basura funcional.


Paso 4 - Publicar eventos propios

Vamos a necesitar crear eventos propios de aprobación

[IntegrationEvent(false, false)]
procedure OnSendStatAccForApproval(var StatisticalAccount: Record "Statistical Account")
begin
end;

[IntegrationEvent(false, false)]
procedure OnCancelStatAccApprovalRequest(var StatisticalAccount: Record "Statistical Account")
begin
end;

Estos eventos serán invocados desde las acciones de la página.

La ventaja de usar eventos es que dejamos la solución abierta a extensiones futuras. Otro módulo podría suscribirse a estos eventos sin tocar nuestro código.


Paso 5 - Validar que existe un workflow activo

Antes de enviar el registro a aprobación, conviene comprobar que realmente hay un flujo configurado.

procedure CheckStatAccApprovalPossible(var StatisticalAccount: Record "Statistical Account"): Boolean
begin
    if not IsStatAccApprovalWorkflowEnabled(StatisticalAccount) then
        Error(NoWorkflowEnabledErr);
    exit(true);
end;

procedure RunWorkflowOnSendStatAccForApprovalCode(): Code[128]
begin
    exit(UpperCase('RunWorkflowOnSendStatAccForApproval'));
end;

Esto evita la típica situación de “he pulsado el botón y no ha pasado nada”.

Si no hay workflow habilitado, mostramos un error claro.


Paso 6 - Conectar nuestro evento con el motor de Workflows

Este es nuestro puente entre evento custom y workflow estándar

[EventSubscriber(ObjectType::Codeunit, Codeunit::"BCS Stat. Acc. Approval Mgmt.",
    'OnSendStatAccForApproval', '', true, true)]
local procedure RunWorkflowOnSendStatAccForApproval(var StatisticalAccount: Record "Statistical Account")
begin
    WorkflowManagement.HandleEvent(
        RunWorkflowOnSendStatAccForApprovalCode(), StatisticalAccount);
end;

Este subscriber traduce nuestro evento custom a un evento que el motor estándar de Workflows entiende.

A partir de aquí, Business Central ya puede arrancar el flujo configurado.


Paso 7 - Registrar eventos en la biblioteca de workflows

[EventSubscriber(ObjectType::Codeunit, Codeunit::"Workflow Event Handling",
    'OnAddWorkflowEventsToLibrary', '', false, false)]
local procedure OnAddWorkflowEventsToLibrary()
begin
    WorkflowEventHandling.AddEventToLibrary(
        RunWorkflowOnSendStatAccForApprovalCode(),
        Database::"Statistical Account", SendForApprovalEventDescTxt, 0, false);
end;

Esto permite que nuestros eventos aparezcan en la configuración estándar de Workflows (y si no aparecen busca el boton de "Reset Microsoft Templates" en la página Workflow Templates).

Es decir, un consultor podrá seleccionarlos desde la interfaz de Business Central igual que haría con eventos estándar.


Paso 8 - Rellenar la entrada de aprobación

También debemos de poblar registros de Approval Entry para una tabla custom

[EventSubscriber(ObjectType::Codeunit, Codeunit::"Approvals Mgmt.",
    'OnPopulateApprovalEntryArgument', '', false, false)]
local procedure OnPopulateApprovalEntryArgument(
    var ApprovalEntryArgument: Record "Approval Entry"; var RecRef: RecordRef;
    WorkflowStepInstance: Record "Workflow Step Instance")
var
    StatisticalAccount: Record "Statistical Account";
begin
    case RecRef.Number of
        Database::"Statistical Account":
            begin
                RecRef.SetTable(StatisticalAccount);
                ApprovalEntryArgument."Document No." := StatisticalAccount."No.";
                ApprovalEntryArgument."Table ID" := Database::"Statistical Account";
            end;
    end;
end;

Cuando Business Central crea una solicitud de aprobación, necesita saber qué datos mostrar.

Aquí le indicamos que el número de documento debe ser el No. de la Cuenta Estadística.


Paso 9 - Actualizar el estado según avance el flujo

El campo BCS Approval Status debe sincronizarse con las acciones reales del workflow.

EventoEstado resultante
Enviar a aprobaciónPending Approval
AprobarApproved
RechazarRejected
Cancelar / reabrirOpen

El detalle importante está en la aprobación: solo deberíamos marcar como Approved cuando no queden aprobadores pendientes.

Esto permite soportar flujos con varias personas aprobando en cadena.


Paso 10 - Resolver la Card Page del registro

Ajustaremos código para redirigir a la página de ficha para una tabla custom

[EventSubscriber(ObjectType::Codeunit, Codeunit::"Page Management",
    'OnConditionalCardPageIDNotFound', '', true, true)]
local procedure OnConditionalCardPageIDNotFound(RecordRef: RecordRef; var CardPageID: Integer)
begin
    case RecordRef.Number of
        Database::"Statistical Account":
            CardPageID := Page::"Statistical Account Card";
    end;
end;

Esto permite que, desde la solicitud de aprobación, el aprobador pueda abrir directamente la ficha correcta del registro.

Sin este subscriber, Business Central puede no saber qué página debe abrir para nuestra tabla custom.


Paso 11 - Registrar relaciones de tabla

Crearemos relaciones entre tabla custom y Approval Entry

[EventSubscriber(ObjectType::Codeunit, Codeunit::"Workflow Setup",
    'OnAfterInsertApprovalsTableRelations', '', true, true)]
local procedure OnAfterInsertApprovalsTableRelations()
var
    ApprovalEntry: Record "Approval Entry";
    StatisticalAccount: Record "Statistical Account";
    WorkflowWebhookEntry: Record "Workflow Webhook Entry";
begin
    WorkflowSetup.InsertTableRelation(
        Database::"Statistical Account", 0,
        Database::"Approval Entry", ApprovalEntry.FieldNo("Record ID to Approve"));
    WorkflowSetup.InsertTableRelation(
        Database::"Statistical Account", StatisticalAccount.FieldNo("SystemId"),
        Database::"Workflow Webhook Entry", WorkflowWebhookEntry.FieldNo("Data ID"));
end;

Este bloque suele ser uno de los más importantes y también uno de los más fáciles de olvidar.

Sin estas relaciones, Business Central puede crear aprobaciones, pero no enlazarlas correctamente con el registro original.


Paso 12 - Añadir acciones y colores en página

Crearemos las acciones de enviar/cancelar aprobación

action("BCS BCS_SendApprovalRequest")
{
    Caption = 'Send Approval Request';
    Enabled = not OpenApprovalEntriesExist;
    trigger OnAction()
    var
        BCSApprovalMgmt: Codeunit "BCS Stat. Acc. Approval Mgmt.";
    begin
        if BCSApprovalMgmt.CheckStatAccApprovalPossible(Rec) then
            BCSApprovalMgmt.OnSendStatAccForApproval(Rec);
        CurrPage.Update(false);
    end;
}

Y para el semáforo visual:

trigger OnAfterGetRecord()
begin
    OpenApprovalEntriesExist := ApprovalsMgmt.HasOpenApprovalEntries(Rec.RecordId);
    CanCancelApprovalForRecord := ApprovalsMgmt.CanCancelApprovalForRecord(Rec.RecordId);
    BCSSetApprovalStatusStyle();
    GPageEditable := Rec.ApprovalStatusAllowModify();
end;

local procedure BCSSetApprovalStatusStyle()
begin
    case Rec."BCS Approval Status" of
        Rec."BCS Approval Status"::Open: BCSApprovalStatusStyleTxt := 'Standard';
        Rec."BCS Approval Status"::"Pending Approval": BCSApprovalStatusStyleTxt := 'Ambiguous';
        Rec."BCS Approval Status"::Approved: BCSApprovalStatusStyleTxt := 'Favorable';
        Rec."BCS Approval Status"::Rejected: BCSApprovalStatusStyleTxt := 'Unfavorable';
    end;
end;

Este bloque mejora muchísimo la experiencia de usuario.

El usuario entiende de un vistazo si el registro está abierto, pendiente, aprobado o rechazado.

4- Funcionalidad:
El flujo completo queda así:
  1. El usuario abre una Cuenta Estadística.
  2. Pulsa Enviar solicitud de aprobación.
  3. La página valida que exista un workflow activo.
  4. Se publica el evento custom.
  5. El subscriber lo convierte en un evento del motor de Workflows.
  6. Business Central crea la entrada de aprobación.
  7. El estado cambia a Pending Approval.
  8. El registro queda bloqueado para edición y borrado.
  9. El aprobador recibe la solicitud en su bandeja estándar.
  10. El aprobador abre la ficha del registro.
  11. Si aprueba, el estado pasa a Approved.
  12. Si rechaza, el estado pasa a Rejected.
  13. Si se cancela la solicitud, el estado vuelve a Open.

Lo más importante es que no hemos creado una pantalla nueva de aprobaciones.

Estamos reutilizando la experiencia estándar de Business Central:

  • Bandeja estándar de aprobaciones.
  • Eventos estándar de workflow.
  • Respuestas estándar.
  • Integración con Power Automate.
  • Comportamiento consistente con documentos estándar.
5- Conclusión:
La clave de este desarrollo es entender que no estamos programando un sistema de aprobaciones desde cero.

Lo que hacemos es enseñarle al motor estándar de Business Central que nuestra tabla custom existe y que debe tratarla como un documento aprobable.

El patrón que has podido ver se puede reutilizar en muchas otras tablas personalizadas.

Si quieres probar a reutilizarlo a través de Github Copilot con skills siéntete libre de probar AL Copilot Skills Collection, donde tienes esta skill y muchas más para implementar.


Nos vemos en la próxima parada del BC Scout Path 🧭

Remember: Talk is cheap, show me the Skills!

Post a Comment

Previous Post Next Post