Workflow API en Acumatica ERP: Automatización de flujos paso a paso

Workflow API en Acumatica

¿Qué es el Workflow API en Acumatica ERP?

El Workflow API en Acumatica ERP es una representación de cómo cambian los estados de los documentos en el sistema, como resultado de la interacción que tienen los usuarios y otros eventos. Puedes crear estos flujos de trabajo en las pantallas que sean personalizadas a través de Acumatica Framework o extender los que ya existen en las pantallas nativas.

Casos de uso de Workflow API en Acumatica ERP

En Acumatica cuando se genera un documento por ejemplo Orden de venta, este documento para por diferentes estados durante su procesamiento como “Retenido” -> “Abierto” -> “En embarque” -> “Completado”.

El Workflow API en Acumatica ERP permite a los desarrolladores definir estos flujos de cambio de estados, las transiciones entre ellos y las condiciones y/o eventos que deben cumplir para generar esta transición.

Con esta API, los programadores pueden realizar lo siguiente:

  • Agregar nuevos estados.
  • Ocultar o modificar acciones en función del estado actual.
  • Automatizar decisiones según reglas de negocio.
  • Aplicar flujos personalizados en pantallas estándar o personalizadas.

Si la empresa tiene una forma definida de manejar algún proceso o documento como las ordenes de compra, las facturas de gastos o las ventas, el Workflow API en Acumatica permite adaptar estos flujos predefinidos para que se alineen con ese proceso quitando, agregando o reorganizando los pasos, las condiciones y botones.

Componentes del Workflow API en Acumatica

Estados y Transiciones

Un Workflow puede describirse como una máquina de estados con transiciones mostrando las etapas de un registro mientras se procesa en el sistema. Ejemplos de estados serían “Retenido”, “Abierto”, “Aprobado” y las transiciones son los cambios por ejemplo de Retenido -> Abierto o de “Retenido” -> “Pendiente de Pago”.

Acciones y Campos

Es posible configura acciones y las propiedades de los campos en pantalla para que dependiendo de las condiciones sean habilitados o visibles.

Condiciones

Las condiciones son sentencias en lenguaje BQL que permite a los campos y acciones en una pantalla determinar si deben cambiar su propiedad, determinar si una acción debe ser ejecutada de manera automática o determinar si una transición a otro status debe ser realizada.

Eventos

Un evento en el Workflow API en Acumatica es utilizado para realizar transiciones de manera automática. Estos eventos permiten la interacción entre diferentes graphs, por lo tanto entre las diferentes pantallas o incluso diferentes tipos de registro.

Cuadros de Diálogo

Cuando los flujos de trabajo involucren la interacción con el usuario de manera que se espere un dato de su parte, podemos utilizar estos cuadros de diálogo y recolectar la respuesta. Con este valor en la lógica de negocio podemos continuar el trabajo con los registros.

Categorías de Acciones

Las categorías son etiquetas que nos permiten organizar las acciones en las secciones correspondientes en el menú de Acumatica ERP.

FlowStates

Esta entidad dentro del Workflow API en Acumatica unifica las acciones, campos y eventos que estarán presentes cuando el registro se encuentra en ese estado.

Componentes de Screen Configuration muestran los elementos de Workflow API en Acumatica ERP
Componentes de Workflow API en Acumatica 

Agregar un flujo de trabajo en una pantalla personalizada

Pre-requisitos

El código en el Workflow API en Acumatica es declarativo. Lo cual provoca que la manera normal de hacer debug en el entorno de desarrollo no funciona. Para poder verificar cuando los errores en un flujo de trabajo ocurran, es necesario modificar el archivo Web.config en la instancia. De esta manera podemos ver los errores en la pantalla System Monitor (SM201530) en la sección System Events.

La sección a modificar en el archivo Web.config es appSettings y se debe agregar o editar el valor de la llave EnableWorkflowValidationOnStartup.

Debe verse de la siguiente manera:

<add key="EnableWorkflowValidationOnStartup" value="True" />

Agregar Workflow predeterminado

A partir de una libreria de extensión en el IDE Visual Studio, es donde se define una clase para definir el comportamiento de las transacciones. Para este ejemplo se tiene un módulo desarrollado por la certificación de Application Developer de Acumatica.

Un flujo de trabajo en una pantalla personalizada funciona como una extensión del graph original de la transacción, por lo cual su definición se realiza heredando desde PXGraphExtension.

De manera gráfica, el flujo de trabajo a definir es el siguiente:

Flujo de trabajo desarrollado con Workflow API en Acumatica.

Para el siguiente ejemplo se agrega un flujo de trabajo a un graph llamado RSSVWorkOrderEntry. Este código se coloca dentro de un nuevo archivo llamado RSSVWorkOrderWorkflow.cs con el siguiente contenido:

using PX.Data;
using PX.Data.WorkflowAPI;
using static PX.Data.WorkflowAPI.BoundedTo<PhoneRepairShop.RSSVWorkOrderEntry,
PhoneRepairShop.RSSVWorkOrder>;
namespace PhoneRepairShop
{
public class RSSVWorkOrderWorkflow : PX.Data.PXGraphExtension<RSSVWorkOrderEntry>
{
}
}

Agregar Estados

Se deben definir los status con los que va a trabajar la pantalla en una clase y referenciar tanto constantes simples tipo string como constantes BQL. dentro de la clase RSSVWorkOrderWorkflow.

 #region Constants
        public static class States
        {
//Constants 
            public const string OnHold = WorkOrderStatusConstants.OnHold;
            public const string ReadyForAssignment =
              WorkOrderStatusConstants.ReadyForAssignment;
            public const string PendingPayment =
              WorkOrderStatusConstants.PendingPayment;
            public const string Assigned = WorkOrderStatusConstants.Assigned;
            public const string Completed = WorkOrderStatusConstants.Completed;
            public const string Paid = WorkOrderStatusConstants.Paid;
//Constans BQL
            public class onHold : PX.Data.BQL.BqlString.Constant<onHold>
            {
                public onHold() : base(OnHold) { }
            }
            public class readyForAssignment :
              PX.Data.BQL.BqlString.Constant<readyForAssignment>
            {
                public readyForAssignment() : base(ReadyForAssignment) { }
            }
            public class pendingPayment :
              PX.Data.BQL.BqlString.Constant<pendingPayment>
            {
                public pendingPayment() : base(PendingPayment) { }
            }
            public class assigned : PX.Data.BQL.BqlString.Constant<assigned>
            {
                public assigned() : base(Assigned) { }
            }
            public class completed : PX.Data.BQL.BqlString.Constant<completed>
            {
                public completed() : base(Completed) { }
            }
            public class paid : PX.Data.BQL.BqlString.Constant<paid>
            {
                public paid() : base(Paid) { }
            }
        }
        #endregion

Para adjuntar este flujo de trabajo a la pantalla, es necesario obtener y sobreescribir el método Configure en 2 niveles.

  • Un método estático para inicializar
  • Un método sobrescrito Configure(PXScreenConfifuration config), donde se aplican los cambios a la pantalla especificada.
public sealed override void Configure (PXScreenConfiguration config)
{
  Configure(config.GetScreenConfigurationContext<RSSVWorkOrderEntry,
                                                           RSSVWorkOrder>())
}
protected static void Configure(WorkflowContext<RSSVWorkOrderEntry,                                                         RSSVWorkOrder> context){}

Dentro del método estático agrega la plantilla para definir el flujo de trabajo predeteminado con la siguiente instrucción:

context.AddScreenConfigurationFor(screen => screen
.StateIdentifierIs()
.AddDefaultFlow(flow => …)

Definir Acciones

Antes de definir las transiciones entre los status se deben definir las acciones que disparan esa transición.

Para este ejemplo se agregará la transición de estado OnHold hacia Ready for Assigment. En el graph RSSVWorkOrderEntry se define un PXAction.

 public PXAction<RSSVWorkOrder> ReleaseFromHold;
        [PXButton(), PXUIField(DisplayName = "Remove Hold",
          MapEnableRights = PXCacheRights.Select,
          MapViewRights = PXCacheRights.Select)]
        protected virtual IEnumerable releaseFromHold(PXAdapter adapter) => adapter.Get();

Para organizar correctamente este botón en la pantalla, se definir Categorias obteniendo las que Workflow API en Acumatica ya integra de la siguiente manera en el método Configure

#region Categories
            var commonCategories = CommonActionCategories.Get(context);
            var processingCategory = commonCategories.Processing;
#endregion

El campo dentro del DAC que indicará el estado de la transacción debe agregarse con la instrucción StateIdentifier.

.StateIdentifierIs<RSSVWorkOrder.status>()

Estas categorías deben agregarse al parámetro screen del método AddDefaultWorkflow como se muestra a continuación con el método WithCategories.

  context.AddScreenConfigurationFor(screen =>
                screen
                .StateIdentifierIs<RSSVWorkOrder.status>()
                .AddDefaultFlow(flow => ...)
                .WithCategories(categories =>
                {
                    categories.Add(processingCategory);
                })			
                	
            );

Las acciones se agregan al Workflow con el método WithActions y se indica la categoría donde se va a ubicar en el menú de Acumatica ERP.

context.AddScreenConfigurationFor(screen =>
                screen
                .StateIdentifierIs<RSSVWorkOrder.status>()
                .AddDefaultFlow(flow => ...)
                .WithCategories(categories =>
                {
                    categories.Add(processingCategory);
                })				
                .WithActions(actions => 
                {
                    actions.Add(g => g.ReleaseFromHold, c => c
                      .WithCategory(processingCategory));
                })				
            );

Crear FlowStates

Para definir el estado inicial de la transacción en un flowstate. A continuación se agrega un ejemplo con 2 flowstates, el inicial y el que sigue en el flujo de trabajo planteado.

 context.AddScreenConfigurationFor(screen =>
                screen
                .StateIdentifierIs<RSSVWorkOrder.status>()				
                .AddDefaultFlow(flow => flow
                    .WithFlowStates(fss =>
                    {
                        fss.Add<States.onHold>(flowState =>
                        {
                            return flowState
                            .IsInitial()
                            .WithActions(actions =>
                            {
                                actions.Add(g => g.ReleaseFromHold, a => a
                                .IsDuplicatedInToolbar()
                                .WithConnotation(ActionConnotation.Success));
                            });
                        });
                        fss.Add<States.readyForAssignment>(flowState =>
                        {
                            return flowState
                            .WithFieldStates(states =>
                            {
                                states.AddField<RSSVWorkOrder.customerID>(state
                                    => state.IsDisabled());
                                states.AddField<RSSVWorkOrder.serviceID>(state
                                    => state.IsDisabled());
                                states.AddField<RSSVWorkOrder.deviceID>(state
                                    => state.IsDisabled());
                            }); ;
                        });
                    }))				
                .WithCategories(categories =>
                {
                    categories.Add(processingCategory);
                })
                .WithActions(actions => 
                {
                    actions.Add(g => g.ReleaseFromHold, c => c
                      .WithCategory(processingCategory));
                })
            );

En el segundo flowstate, se definen con el método FieldStates los campos que serán deshabilitados cuando se realiza la transición a Ready for assigment

Se utiliza el método IsDuplicatedInToolbar para que el usuario vea el la acción directamente en pantalla si accionar el menú más Acciones en Acumatica ERP.

Para definir transiciones se puede realizar de 2 maneras: simple o agrupada.

En el caso de existir más de un estado destino para la transición, se utiliza un grupo de transiciones. De lo contrario es posible utilizar una transición simple.

Para este ejemplo las transiciones definidas desde el estado OnHold pueden ser Ready for Assigment o Awaiting Payment.

Agregar Condiciones

Se deben declarar las condiciones en un condition pack, después se define cada condición con el método GetOrCreate, que toma como parámetro una setencia BQL. Las condiciones nos ayudarán en el grupo de transiciones a decidir hacia qué estado debe ir la transacción.

Dentro de la clase RSSVWorkOrderWorkflow se define la clase Condition. Para este ejemplo se definen 2 condiciones basándonos en si requiere un anticipo para realizar el servicio.

public class Conditions : Condition.Pack
        {
            public Condition RequiresPrepayment => GetOrCreate(b => b.FromBql<
              Where<Selector<RSSVWorkOrder.serviceID, RSSVRepairService.prepayment>,
              Equal<True>>>());
            public Condition DoesNotRequirePrepayment => GetOrCreate(b => b.FromBql<
              Where<Selector<RSSVWorkOrder.serviceID, RSSVRepairService.prepayment>,
              Equal<False>>>());
        }

Definir Grupos de Transiciones

Ya definidas las condiciones, se agrega el grupo de transiciones con el método WithTransitions, además de definir el flowstate del estado Pending Payment

 context.AddScreenConfigurationFor(screen =>
                screen
                .StateIdentifierIs<RSSVWorkOrder.status>()
                .AddDefaultFlow(flow => flow
                    .WithFlowStates(fss =>
                    {
                        fss.Add<States.onHold>(flowState =>
                        {
                            return flowState
                            .IsInitial()
                            .WithActions(actions =>
                            {
                                actions.Add(g => g.ReleaseFromHold, a => a
                                .IsDuplicatedInToolbar()
                                .WithConnotation(ActionConnotation.Success));
                            });
                        });
                        fss.Add<States.readyForAssignment>(flowState =>
                        {
                            return flowState
                            .WithFieldStates(states =>
                            {
                                states.AddField<RSSVWorkOrder.customerID>(state
                                    => state.IsDisabled());
                                states.AddField<RSSVWorkOrder.serviceID>(state
                                    => state.IsDisabled());
                                states.AddField<RSSVWorkOrder.deviceID>(state =>
                                    state.IsDisabled());
                            }); ;
                        });
                        fss.Add<States.pendingPayment>(flowState =>
                        {
                            return flowState
                              .WithFieldStates(states =>
                              {
                                  states.AddField<RSSVWorkOrder.customerID>(state => state.IsDisabled());
                                  states.AddField<RSSVWorkOrder.serviceID>(state => state.IsDisabled());
                                  states.AddField<RSSVWorkOrder.deviceID>(state => state.IsDisabled());
                              }); ;
                        });
                    })
					
                    .WithTransitions(transitions =>
                    {
                        transitions.AddGroupFrom<States.onHold>(ts =>
                        {
                            ts.Add(t => t.To<States.readyForAssignment>()
                                .IsTriggeredOn(g => g.ReleaseFromHold)
                                .When(conditions.DoesNotRequirePrepayment));
                            ts.Add(t => t.To<States.pendingPayment>()
                                .IsTriggeredOn(g => g.ReleaseFromHold)
                                .When(conditions.RequiresPrepayment));
                        });
                    })
					
                )
                .WithCategories(categories =>
                {
                    categories.Add(processingCategory);
                })
                .WithActions(actions => 
                {
                    actions.Add(g => g.ReleaseFromHold, c => c
                      .WithCategory(processingCategory));
                })
            );

Hasta el momento la pantalla se vería de esta manera: Un botón con una transición.

Transición con Workflow API en Acumatica

Agregar cuadros de diálogo

Para la transición al estado Assigned se requiere un cuadro de diálogo preguntando el dato del empleado a asignar. Además la lógica de este cuadro, asignará el valor seleccionado al campo Assignee.

En el método Configure se define el form que se mostrará al hacer clic en una acción en pantalla.

El método forms.Create es el que inicializa el cuadro de diálogo. Dentro de este método se define el título del form y con el método Prompt se define el título que verá el usuario. Además la instrucción WithFields definirá los campos que tendrá esta ventana emergente. Para este ejemplo es un selector definido en el campo RSSVWorkOrder.assignee.

 var formAssign = context.Forms.Create("FormAssign", form =>
                form.Prompt("Assign").WithFields(fields =>
                {
                    fields.Add("Assignee", field => field
                        .WithSchemaOf<RSSVWorkOrder.assignee>()
                        .IsRequired()
                        .Prompt("Assignee"));
                }));

En el graph RSSVWorkOrderEntry se agrega la acción que llamará a este cuadro de diálogo como se muestra a continuación

 public PXAction<RSSVWorkOrder> Assign;
 [PXButton]
 [PXUIField(DisplayName = "Assign", Enabled = false)]
 protected virtual IEnumerable assign(PXAdapter adapter) => adapter.Get();

Para asociar el botón Assign con el cuadro formAssign se utiliza el método WithForms al agregar la acción en el método WithActions del workflow. Al utilizar la instrucción WithFieldAssigments podemos asignar dónde se guardará el valor que ingresa el usuario en el cuadro de diálogo.

 actions.Add(g => g.Assign, c => c
                      .WithCategory(processingCategory)
                      .WithForm(formAssign)
                      .WithFieldAssignments(fields => {
                      fields.Add<RSSVWorkOrder.assignee>(f =>
                        f.SetFromFormField(formAssign, "Assignee"));
                      }));

Recuerda agregar el flowstate que corresponda al estado Assigned y este nuevo botón a los estados previos a Assigned. Como se muestra en el código

fss.Add<States.assigned>(flowState =>
{
       return flowState
       .WithFieldStates(states =>
       {
          states.AddField<RSSVWorkOrder.customerID>(state => state.IsDisabled());
          states.AddField<RSSVWorkOrder.serviceID>(state => state.IsDisabled());
          states.AddField<RSSVWorkOrder.deviceID>(state => state.IsDisabled());
       });
});

Y para que la transición hacia el estado Assigned funcione, se debe registrar en el método WithTransition desde dónde se puede llegar al estado Assigned.

 transitions.AddGroupFrom<States.readyForAssignment>(ts =>
         {
            ts.Add(t => t.To<States.onHold>().IsTriggeredOn(g => g.PutOnHold));
            ts.Add(t => t.To<States.assigned>().IsTriggeredOn(g => g.Assign));
 });

Completar las acciones y transiciones por acción

Repitiendo los pasos aprendidos, se registran acciones adicionales en la pantalla como Complete y CreateInvoice para agregarlos en los flowstates que corresponden, quedando de esta manera el método Configure

  protected static void Configure(WorkflowContext<RSSVWorkOrderEntry, 
                                                        RSSVWorkOrder> context)
        {
            var formAssign = context.Forms.Create("FormAssign", form =>
                form.Prompt("Assign").WithFields(fields =>
                {
                    fields.Add("Assignee", field => field
                        .WithSchemaOf<RSSVWorkOrder.assignee>()
                        .IsRequired()
                        .Prompt("Assignee"));
                }));
            var conditions = context.Conditions.GetPack<Conditions>();
            #region Categories
            var commonCategories = CommonActionCategories.Get(context);
            var processingCategory = commonCategories.Processing;
            #endregion
            context.AddScreenConfigurationFor(screen =>
                screen
                .StateIdentifierIs<RSSVWorkOrder.status>()
                .AddDefaultFlow(flow => flow
                    .WithFlowStates(fss =>
                    {
                        fss.Add<States.onHold>(flowState =>
                        {
                            return flowState
                            .IsInitial()
                            .WithActions(actions =>
                            {
                                actions.Add(g => g.ReleaseFromHold, a => a
                                .IsDuplicatedInToolbar()
                                .WithConnotation(ActionConnotation.Success));
                            });
                        });
                        fss.Add<States.readyForAssignment>(flowState =>
                        {
                            return flowState
                              .WithFieldStates(states =>
                              {
                                states.AddField<RSSVWorkOrder.customerID>(state
                                  => state.IsDisabled());
                                states.AddField<RSSVWorkOrder.serviceID>(state
                                  => state.IsDisabled());
                                states.AddField<RSSVWorkOrder.deviceID>(state 
                                  => state.IsDisabled());
                              })
                              .WithActions(actions =>
                              {
                                  actions.Add(g => g.PutOnHold, a => a.IsDuplicatedInToolbar());
                                  actions.Add(g => g.Assign, a => a
                                      .IsDuplicatedInToolbar()
                                      .WithConnotation(ActionConnotation.Success));
                              });
                        });
                        fss.Add<States.pendingPayment>(flowState =>
                        {
                            return flowState
                              .WithFieldStates(states =>
                              {
                                states.AddField<RSSVWorkOrder.customerID>(state 
                                  => state.IsDisabled());
                                states.AddField<RSSVWorkOrder.serviceID>(state 
                                  => state.IsDisabled());
                                states.AddField<RSSVWorkOrder.deviceID>(state 
                                  => state.IsDisabled());
                              })
                              .WithActions(actions =>
                              {
                                actions.Add(g => g.PutOnHold, a => a.IsDuplicatedInToolbar());
                              });
                        });
						
                        fss.Add<States.assigned>(flowState =>
                        {
                            return flowState
                                .WithFieldStates(states =>
                                {
                                    states.AddField<RSSVWorkOrder.customerID>(state 
                                      => state.IsDisabled());
                                    states.AddField<RSSVWorkOrder.serviceID>(state 
                                      => state.IsDisabled());
                                    states.AddField<RSSVWorkOrder.deviceID>(state 
                                      => state.IsDisabled());
                                })
                                
                                .WithActions(actions =>
                                {
                                    actions.Add(g => g.Complete, a => a
                                      .IsDuplicatedInToolbar()
                                      .WithConnotation(ActionConnotation.Success));
                                });
                        });						
                    })
                    .WithTransitions(transitions =>
                    {
                        transitions.AddGroupFrom<States.onHold>(ts =>
                        {
                            ts.Add(t => t.To<States.readyForAssignment>()
                                .IsTriggeredOn(g => g.ReleaseFromHold)
                                .When(conditions.DoesNotRequirePrepayment));
                            ts.Add(t => t.To<States.pendingPayment>()
                                .IsTriggeredOn(g => g.ReleaseFromHold)
                                .When(conditions.RequiresPrepayment));
                        });
                        transitions.AddGroupFrom<States.readyForAssignment>(ts =>
                        {
                            ts.Add(t => t.To<States.onHold>().IsTriggeredOn(g => g.PutOnHold));
                            ts.Add(t => t.To<States.assigned>().IsTriggeredOn(g => g.Assign));
                        });
                        transitions.AddGroupFrom<States.pendingPayment>(ts =>
                        {
                            ts.Add(t => t.To<States.onHold>().IsTriggeredOn(g => g.PutOnHold));
                        });
                    })
                )
                .WithCategories(categories =>
                {
                    categories.Add(processingCategory);
                })
                .WithActions(actions => 
                {
                    actions.Add(g => g.ReleaseFromHold, c => c
                      .WithCategory(processingCategory));
                    actions.Add(g => g.PutOnHold, c => c
                      .WithCategory(processingCategory));
                    actions.Add(g => g.Assign, c => c
                      .WithCategory(processingCategory)
                      .WithForm(formAssign)
                      .WithFieldAssignments(fields => {
                      fields.Add<RSSVWorkOrder.assignee>(f =>
                        f.SetFromFormField(formAssign, "Assignee")
                        .);
                      }));                   
                    actions.Add(g => g.Complete, c => c
                      .WithCategory(processingCategory, Placement.Last)
                      .WithFieldAssignments(fas => fas
                        .Add<RSSVWorkOrder.dateCompleted>(f =>
                          f.SetFromToday())));                    
                })
                .WithForms(forms => forms.Add(formAssign))
            );
}

Cuando existen pantallas con flujos de trabajo que interactúan entre si, se pueden definir eventos para que las transiciones dependan entre si.

Un evento se define dentro de un DAC. Por ejemplo para monitorear el pago de una WorkOrder se define la siguiente clase y evento.

public class MyEvents : PXEntityEvent<ARRegister>.Container<MyEvents>
{
   public PXEntityEvent<ARRegister> InvoiceGotPrepaid;
}

En el graph RSSVWorOrderEntry se define el event handler.

 public PXWorkflowEventHandler<RSSVWorkOrder, ARInvoice> OnCloseDocument;

Y este event handler se debe registrar con el método WithHandlers en el método Configure

 .WithHandlers(handlers =>
                {
                    handlers.Add(handler => handler
                        .WithTargetOf<ARInvoice>()
                        .OfEntityEvent<ARInvoice.Events>(e => e.CloseDocument)
                        .Is(g => g.OnCloseDocument)
                        .UsesPrimaryEntityGetter<
                        SelectFrom<RSSVWorkOrder>
                               .Where<RSSVWorkOrder.invoiceNbr
                               .IsEqual<ARRegister.refNbr.FromCurrent>>>());

Como este evento será disparado desde el proceso de liberación de la factura asociada, se modifica la clase ARReleaseProcess para que transicione al estado Paid

  public class ARReleaseProcess_Extension : PXGraphExtension<ARReleaseProcess>
    {
		public SelectFrom<RSSVWorkOrder>.View UpdWorkOrder;
		
		[PXOverride]
		public void PerformPersist(PXGraph.IPersistPerformer persister,
		            Action<PXGraph.IPersistPerformer> base_PerformPersist)
		{
			base_PerformPersist(persister);
			persister.Update<RSSVWorkOrder>();
		}
    }

Con esto la transición al estado queda completada al momento de cerrar la factura asociada.

Los Workflow API en acumatica de una transición con evento

Para finalizar este flujo de trabajo con Workflow API en Acumatica, agrega las transiciones correspondientes y los flowstates faltantes.

De esta manera queda el flujo de trabajo finalizado:

protected static void Configure(WorkflowContext<RSSVWorkOrderEntry, 
                                                        RSSVWorkOrder> context)
        {
            var formAssign = context.Forms.Create("FormAssign", form =>
                form.Prompt("Assign").WithFields(fields =>
                {
                    fields.Add("Assignee", field => field
                        .WithSchemaOf<RSSVWorkOrder.assignee>()
                        .IsRequired()
                        .Prompt("Assignee"));
                }));
            var conditions = context.Conditions.GetPack<Conditions>();
            #region Categories
            var commonCategories = CommonActionCategories.Get(context);
            var processingCategory = commonCategories.Processing;
            #endregion
            context.AddScreenConfigurationFor(screen =>
                screen
                .StateIdentifierIs<RSSVWorkOrder.status>()
                .AddDefaultFlow(flow => flow
                    .WithFlowStates(fss =>
                    {
                        fss.Add<States.onHold>(flowState =>
                        {
                            return flowState
                            .IsInitial()
                            .WithActions(actions =>
                            {
                                actions.Add(g => g.ReleaseFromHold, a => a
                                .IsDuplicatedInToolbar()
                                .WithConnotation(ActionConnotation.Success));
                            });
                        });
                        fss.Add<States.readyForAssignment>(flowState =>
                        {
                            return flowState
                              .WithFieldStates(states =>
                              {
                                states.AddField<RSSVWorkOrder.customerID>(state
                                  => state.IsDisabled());
                                states.AddField<RSSVWorkOrder.serviceID>(state
                                  => state.IsDisabled());
                                states.AddField<RSSVWorkOrder.deviceID>(state 
                                  => state.IsDisabled());
                              })
                              .WithActions(actions =>
                              {
                                  actions.Add(g => g.PutOnHold, a => a.IsDuplicatedInToolbar());
                                  actions.Add(g => g.Assign, a => a
                                      .IsDuplicatedInToolbar()
                                      .WithConnotation(ActionConnotation.Success));
                              });
                        });
                        fss.Add<States.pendingPayment>(flowState =>
                        {
                            return flowState
                              .WithFieldStates(states =>
                              {
                                states.AddField<RSSVWorkOrder.customerID>(state 
                                  => state.IsDisabled());
                                states.AddField<RSSVWorkOrder.serviceID>(state 
                                  => state.IsDisabled());
                                states.AddField<RSSVWorkOrder.deviceID>(state 
                                  => state.IsDisabled());
                              })
                              .WithActions(actions =>
                              {
                                actions.Add(g => g.PutOnHold, 
                                  a => a.IsDuplicatedInToolbar());
                                actions.Add(g => g.CreateInvoiceAction, a => a
                                  .IsDuplicatedInToolbar()
                                  .WithConnotation(ActionConnotation.Success));
                              })
                              // Add the OnInvoiceGotPrepaid event handler
                              .WithEventHandlers(handlers =>
                              {
                                  handlers.Add(g => g.OnInvoiceGotPrepaid);
                              });
                        });						
                        fss.Add<States.assigned>(flowState =>
                        {
                            return flowState
                                .WithFieldStates(states =>
                                {
                                    states.AddField<RSSVWorkOrder.customerID>(state
                                      => state.IsDisabled());
                                    states.AddField<RSSVWorkOrder.serviceID>(state
                                      => state.IsDisabled());
                                    states.AddField<RSSVWorkOrder.deviceID>(state
                                      => state.IsDisabled());
                                })
                                .WithActions(actions =>
                                {
                                    actions.Add(g => g.Complete, a => a
                                      .IsDuplicatedInToolbar()
                                      .WithConnotation(ActionConnotation.Success));
                                });
                        });
                        fss.Add<States.completed>(flowState =>
                        {
                            return flowState
                                .WithFieldStates(states =>
                                {
                                    states.AddField<RSSVWorkOrder.customerID>(state =>
                                        state.IsDisabled());
                                    states.AddField<RSSVWorkOrder.serviceID>(state =>
                                        state.IsDisabled());
                                    states.AddField<RSSVWorkOrder.deviceID>(state =>
                                        state.IsDisabled());
                                })
                                .WithActions(actions =>
                                {
                                    actions.Add(g => g.CreateInvoiceAction, a => a
                                    .IsDuplicatedInToolbar()
                                    .WithConnotation(ActionConnotation.Success));
                                })
                                .WithEventHandlers(handlers =>
                                {
                                    handlers.Add(g => g.OnCloseDocument);
                                });
                        });
                        fss.Add<States.paid>(flowState =>
                        {
                            return flowState
                                .WithFieldStates(states =>
                                {
                                    states.AddField<RSSVWorkOrder.customerID>(state =>
                                        state.IsDisabled());
                                    states.AddField<RSSVWorkOrder.serviceID>(state =>
                                        state.IsDisabled());
                                    states.AddField<RSSVWorkOrder.deviceID>(state =>
                                        state.IsDisabled());
                                });
                        });
                    })
                    .WithTransitions(transitions =>
                    {
                        transitions.AddGroupFrom<States.onHold>(ts =>
                        {
                            ts.Add(t => t.To<States.readyForAssignment>()
                                .IsTriggeredOn(g => g.ReleaseFromHold)
                                .When(conditions.DoesNotRequirePrepayment));
                            ts.Add(t => t.To<States.pendingPayment>()
                                .IsTriggeredOn(g => g.ReleaseFromHold)
                                .When(conditions.RequiresPrepayment));
                        });
                        transitions.AddGroupFrom<States.readyForAssignment>(ts =>
                        {
                            ts.Add(t => t.To<States.onHold>().IsTriggeredOn(g => g.PutOnHold));
                            ts.Add(t => t.To<States.assigned>().IsTriggeredOn(g => g.Assign));
                        });
                        transitions.AddGroupFrom<States.pendingPayment>(ts =>
                        {
                            ts.Add(t => t.To<States.onHold>().IsTriggeredOn(g => g.PutOnHold));                            
                            ts.Add(t => t.To<States.readyForAssignment>()
                              .IsTriggeredOn(g => g.OnInvoiceGotPrepaid));
                        });
                        transitions.AddGroupFrom<States.assigned>(ts =>
                        {
                            ts.Add(t => t.To<States.completed>().IsTriggeredOn(g =>
                                g.Complete));
                        });
                        transitions.AddGroupFrom<States.completed>(ts =>
                        {
                            ts.Add(t => t.To<States.paid>().IsTriggeredOn(g => g.OnCloseDocument));
                        });
                    })
                )
                .WithCategories(categories =>
                {
                    categories.Add(processingCategory);
                })
                .WithActions(actions => 
                {
                    actions.Add(g => g.ReleaseFromHold, c => c
                      .WithCategory(processingCategory));
                    actions.Add(g => g.PutOnHold, c => c
                      .WithCategory(processingCategory));
                    actions.Add(g => g.Assign, c => c
                      .WithCategory(processingCategory)
                      .WithForm(formAssign)
                      .WithFieldAssignments(fields => {
                      fields.Add<RSSVWorkOrder.assignee>(f =>
                        f.SetFromFormField(formAssign, "Assignee"));
                      }));
                    actions.Add(g => g.Complete, c => c
                      .WithCategory(processingCategory, Placement.Last)
                      .WithFieldAssignments(fas => fas
                        .Add<RSSVWorkOrder.dateCompleted>(f =>
                        f.SetFromToday())));
                    actions.Add(g => g.CreateInvoiceAction, c => c
                      .WithCategory(processingCategory)
                      .IsDisabledWhen(conditions.DisableCreateInvoice));
                })
                .WithForms(forms => forms.Add(formAssign))
                .WithHandlers(handlers =>
                {
                    handlers.Add(handler => handler
                        .WithTargetOf<ARInvoice>()
                        .OfEntityEvent<ARInvoice.Events>(e => e.CloseDocument)
                        .Is(g => g.OnCloseDocument)
                        .UsesPrimaryEntityGetter<
                        SelectFrom<RSSVWorkOrder>.
                        Where<RSSVWorkOrder.invoiceNbr.IsEqual<ARRegister.refNbr.FromCurrent>>>());
                    
                    handlers.Add(handler => handler
                      .WithTargetOf<ARRegister>()
                      .OfEntityEvent<RSSVWorkOrder.MyEvents>(e => e.InvoiceGotPrepaid)
                      .Is(g => g.OnInvoiceGotPrepaid)
                      .UsesPrimaryEntityGetter<
                        SelectFrom<RSSVWorkOrder>.
                        Where<RSSVWorkOrder.invoiceNbr
                        .IsEqual<ARRegister.refNbr.FromCurrent>>>());                    
                })
            );
        }

Cómo extender un flujo de trabajo existente

Al usar la Workflow API en Acumatica ERP, no solo se puede crear flujos de trabajo propios, sino también personalizar los flujos definidos en el código fuente de Acumatica ERP. El Workflow API proporciona un conjunto de métodos para actualizar y eliminar flujos de trabajo y sus elementos, como los estados y las transiciones.

Similar a los métodos AddDefaultWorkflow y AddScreenConfiguration, el Workflow API en Acumatica tiene los métodos UpdateScreenConfigurationFor y UpdateDefaultFlow.

Al actualizar un flujo existente, se permite el uso de los mismos métodos encadenados que son empleados al crear uno nuevo, tales como:

  • WithFlowStates() – para que los estados del flujo sean modificados.
  • WithActions() – para que las acciones sean agregadas o ajustadas.
  • WithTransitions() – para que las transiciones entre estados sean definidas.

Gracias a este enfoque, se facilita la personalización de flujos de trabajo, ya que una estructura de código familiar es reutilizada por los desarrolladores.

En el siguiente ejemplo se agrega una acción a la pantalla de facturas y se modifica el flujo de trabajo para que esta acción esté disponible

 public class SOInvoiceOrder_Workflow : PXGraphExtension<SOInvoiceEntry_Workflow,
        SOInvoiceEntry>
    {
        public sealed override void Configure(PXScreenConfiguration config)
        {
            Configure(config.GetScreenConfigurationContext<SOInvoiceEntry, 
                                                           ARInvoice>());
        }
        protected static void Configure(WorkflowContext<SOInvoiceEntry,
                                                        ARInvoice> context)
        {            
            var repairCategory = context.Categories.CreateNew(
                ActionCategories.RepairCategoryID,
                category => category.DisplayName(
                ActionCategories.DisplayNames.RepairOrders));
            var viewOrder = context.ActionDefinitions
              .CreateExisting<SOInvoiceEntry_Extension>(g => g.ViewOrder,
                a => a.WithCategory(repairCategory));
            context.UpdateScreenConfigurationFor(screen => screen
                .UpdateDefaultFlow(flow =>
                {
                    return flow
                        .WithFlowStates(flowStates =>
                        {
                            flowStates.Update<ARDocStatus.open>(flowState =>
                            {
                                return flowState.WithActions(actions =>
                                    actions.Add(viewOrder));
                            });
                        });
                })
                .WithCategories(categories =>
                {
                    categories.Add(repairCategory);
                })
                .WithActions(actions =>
                {
                    actions.Add(viewOrder);
                })
            );
            
        }
    }
   public static class ActionCategories
    {
        public const string RepairCategoryID = "Repair Orders Category";
        [PXLocalizable]
        public static class DisplayNames
        {
            public const string RepairOrders = "Repair Orders";
        }
    }
    
}

Con esta modificación, la pantalla de facturas quedaría agregada la nueva acción

Acción agregada con Workflow API en Acumatica

Si deseas descargar el proyecto de visual estudio aquí tienes el repositorio completo del curso T270 – Workflow API en Acumatica o bien si deseas aprender cómo desarrollar módulos completos en Acumatica, puedes consultar la ruta de aprendizaje Application Developer.

Conclusiones de Workflow API en Acumatica ERP

Al definir flujos de trabajos personalizados, es posible adaptar Acumatica ERP para los procesos de cada empresa. Así sean flujos nativos o módulos desarrollados especializados en una industria, Workflow API en Acumatica otorga la flexibilidad que muchas compañías buscan sin tener que recurrir a soluciones hechas a medida, sino con una solución ERP con estándares internacionales que está respaldada por partners especialistas como ClandBus.

Estos estados se pueden aprovechar para el diseño de KPI’s, tableros de control, consultas personalizadas, reportes o eventos de negocio que automaticen tareas dentro de la compañía, enfocando a los usuario en tareas de alto valor, aumentando su productividad y evitando errores operativos.

¿Te interesa transformar los procesos clave de tu empresa con Acumatica ERP?
Contáctanos para una demo personalizada y descubre todo lo que puedes lograr.

Si esto le pasa por tener la información en varios lados, esto se resuelve de raíz con un ERP. Vea cómo funciona el ERP para la dirección de operaciones que implementamos en México.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *