Monday, April 15, 2024

Create a custom approval workflow in D365 F&O

 This is a step by step procedure to create a custom approval workflow.


STEP 1 : Creating a workflow template

To start with creating a workflow, we need to first define it’s type/template.
Know more about workflow types/templates :
https://technet.microsoft.com/en-us/library/dd362043.aspx


1. Create new workflow template :

2. A wizard opens which needs Category, Document Class, Query as inputs to create the template.

3. Specifying template properties :

https://docs.microsoft.com/en-us/previous-versions/dynamicsax-2009/developer/cc594095(v%3dax.50)

4. Once a workflow template is created, following objects get created :

Action menu items : CancelMenuItem, SubmitMenuItem
Classes : DocumentClass, WorkflowTypEventHandler, WorkflowTypeSubmitManager and accordingly they are specified in the properties of the workflow type.

STEP 2 : Creating Workflow Approval

1.       Creating new workflow approval :

 https://msdn.microsoft.com/en-us/library/cc596847.aspx

2.       After it’s creation, new objects get created :

Classes : WorkflowApprovalEventHandler
                 WorkflowResubmitActnMgr

Action Menu items : WorkflowApprovalResubmitMenuItem
                                      WorkflowApprovalDelegateMenuItem
                                      WorkflowApprovalApprove
                                      WorkflowApprovalDeny
                                      WorkflowApprovalRequestChange
                                      WorkflowApprovalReject

3.      In the Outcomes node, there are 4 options : Approve, Deny, Reject and RequestChange. They can be enables/disabled using properties as per requirement. Corresponding to each outcome, a menu item is auto created once the Workflow Approval is created. (As explained above)


STEP 3 : Configure workflow

      
1.  Go to the setup in the module in which the workflow is required.
             Taking example of Accounts Receivable module :


2. Select the Type/template for the new workflow that has to be created. The workflow template/type created in Step 1 will be there in the module to which it’s category is linked.


3. A wizard opens where you can configure your workflow by simple drag and drop.
   Steps explained :
https://docs.microsoft.com/en-us/dynamics365/unified-operations/fin-and-ops/organization-administration/configure-approval-process-workflow

STEP 4 : Enabling workflow on UI

1. Check the properties of the form in which WF is required. To follow this tutorial,
              WF Data Source - Specify accordingly
              WF Enabled            - No
              WF Type                 - Specify accordingly
If you are creating a form extension, you can use OnInitialized event handler to specify the properties as follows :          

sender.formRun().design().workflowDatasource(_WFDataSource_);

sender.formRun().design().workflowEnabled(false);

sender.formRun().design().workflowType(_WorkflowType_);

2. On the form in which workflow button is required, add DropDialogButtonGroup in the Action Pane. For reference you can consider the one in PurchTable.

3. The drop dialog button highlighted in the above image links to a Drop Dialog Form that will appear on clicking the Workflow button. That is a new form that has to be created.

4. The WorkflowDropDialog form that has to be created is exactly same as the PuchTableWorkflowDropDialog form. You can refer both the design as well as code from this form to create your own.

STEP 5 : Functionality of Workflow

Now, the workflow framework is ready and we can work on the functionality part i.e. what will happen on every action related to the workflow.
I’ll explain this using an example to make things easier.  

Example : Creating a workflow for Vendor Approval.

For all the below mentioned menu items :
SubmitMenuItem, WorkflowApproval, ResubmitMenuItem, WorkflowApprovalDelegateMenuItem,  WorkflowApprovalApprove, WorkflowApprovalDeny, WorkflowApprovalRequestChange, WorkflowApprovalReject

Object is set as : VendorWorkflowActionManager

Functionality is defined using following created functions :
a). Main
      
Constructing workflowActionManager and validating the arguments before taking the required action. Then it calls the Run() function.

b).Run
     Depending upon which menu item is clicked, it will execute a function. It handles actions for : Submit, Approve, Reject and RequestChange actions.

c). parmByPassDialog, parmWorkflowComment and parmCanceledAction
      These methods are for setting value of CanceledAction and BypassDialog.
#DEFINE.BypassDialog('BypassDialog') This is the Macro used.

d). dialogOK

      To opem a workflow Submit dialog for confirming the submission of workflow.

e). submitToWorkflow

      This is called when someone submits a workflow. The workflow configuration and the vendor status is updated. For updating vendor status, updateStatus function is   called.

f). updateStatus

     On submitting the workflow, the vendor status is updated to InReview. Depending upon what action the workflow approver takes, the status of Vendor is updated to Approved/ Rejected. 
g). validateArgsObject
      Validating passed arguments

Code snippet :

public class VendorWorkflowActionManager

{

    WorkflowComment         workflowComment;

    WorkflowVersionTable    workflowVersionTable;

    boolean bypassDialog;

    boolean canceledAction;

    #DEFINE.BypassDialog('BypassDialog')

 

    public static void main(Args _args)

    {

        VendorWorkflowActionManager  workflowActionManager = VendorWorkflowActionManager::construct();

 

        workflowActionManager.validateArgsObject(_args, funcName());

 

        workflowActionManager.run(_args);

  

    }

  private void validateArgsObject(Args _args, str _funcName)

    {

        if (!_args)

        {

            throw error(Error::wrongUseOfFunction(_funcname));

        }

    }

  public WorkflowComment parmWorkflowComment(WorkflowComment _workflowComment = workflowComment)

    {

        workflowComment = _workflowComment;

 

        return workflowComment;

    }

    public void run(Args _args)

    {

        WorkflowWorkItemActionManager workflowWorkItemActionManager = new WorkflowWorkItemActionManager();

        VendTable            vendTable;

        FormDataSource callerFormDataSource;

        callerFormDataSource    = _args.record().dataSource();

       

        // Validate the args object isn't null.

        this.validateArgsObject(_args, funcName());

 

        // Set the local caller properties.

        Object caller = _args.caller();

        Common callerRecord = _args.record();

        vendTable   =   callerRecord as VendTable;

        str callerMenuItem = _args.menuItemName();

        str callerParm = _args.parm();

 

        if (callerParm == #bypassDialog)

        {

            this.parmBypassDialog(true);

        }

 

        this.parmCanceledAction(false);

 

        if (callerMenuItem == menuitemActionStr(VendorTemplateSubmit))

        {

            if(!this.submitToWorkflow(_args))

            {

                return;

            }

        }

 

        else

        {

            workflowWorkItemActionManager.parmArgs(_args);

            workflowWorkItemActionManager.parmCaller(caller);

            workflowWorkItemActionManager.run();

 

            this.parmCanceledAction(!workflowWorkItemActionManager.parmIsActionDialogClosedOK());

        }

 

        if (callerMenuItem == menuitemActionStr(VendorApprovalApprove) && !this.parmCanceledAction())

        {

            VendorWorkflowActionManager::updateStatus(vendTable,callerFormDataSource,VersioningDocumentState::Approved);

           

        }

        else if (callerMenuItem == menuitemActionStr(VendorApprovalResubmit) && !this.parmCanceledAction())

        {

            VendorWorkflowActionManager::updateStatus(vendTable,callerFormDataSource,VersioningDocumentState::InReview);

        }

 

        else if (callerMenuItem == menuitemActionStr(VendorApprovalReject) && !this.parmCanceledAction())

        {

            VendorWorkflowActionManager::updateStatus(vendTable,callerFormDataSource,VersioningDocumentState::Rejected);

        }

    }

 

public boolean parmBypassDialog(boolean _bypassDialog = bypassDialog)

    {

        bypassDialog = _bypassDialog;

 

        return bypassDialog;

    }

public boolean parmCanceledAction(boolean _canceledAction = canceledAction)

    {

        canceledAction = _canceledAction;

 

        return canceledAction;

    }

  public boolean dialogOk(boolean ok = false)

    {

        WorkflowSubmitDialog        workflowSubmitDialog;

 

        if (!ok)

        {

            workflowSubmitDialog = WorkflowSubmitDialog::construct(workflowVersionTable);

            workflowSubmitDialog.run();

 

            if (workflowSubmitDialog.parmIsClosedOK())

            {

                ok = true;

                workflowComment =  workflowSubmitDialog.parmWorkflowComment();

            }

            else

            {

                canceledAction = true;

            }

        }

 

        return ok;

    }

  public boolean submitToWorkflow(Args _args)

    {

        VendTable vendTable;

        FormDataSource callerFormDataSource;

        callerFormDataSource    = _args.record().dataSource();

 

       

        this.validateArgsObject(_args, funcName());

 

        Common callerRecord = _args.record();

 

        workflowVersionTable = Workflow::findWorkflowConfigToActivateForType(workFlowTypeStr(VendorTemplate),

                                                                             callerRecord.RecId,

                                                                             callerRecord.TableId);

 

        Debug::assert(workflowVersionTable.RecId != 0);

 

        vendTable = callerRecord as VendTable;

 

        Debug::assert(vendTable.RecId != 0);

 

        canceledAction = false;

 

        if (this.dialogOk(bypassDialog))

        {

            VendorWorkflowActionManager::updateStatus(vendTable,callerFormDataSource,VersioningDocumentState::InReview);

            Workflow::activateFromWorkflowConfigurationId(workflowVersionTable.ConfigurationId,

                                                          callerRecord.RecId,

                                                          this.parmWorkflowComment(),

                                                          NoYes::No);

 

        }

 

        return true;

    }

   public static void updateStatus(VendTable               _vendTable,

                                    FormDataSource          _callerForm,

                                    VersioningDocumentState _state)

    {

        if(_vendTable)

        {

            ttsbegin;

            _vendTable.selectForUpdate(true);

            _vendTable.VendorStatus = _state;

            switch(_state)

            {

                case VersioningDocumentState::Approved:

                    _vendTable.Blocked         = CustVendorBlocked::No;

                    if(_callerForm && _callerForm.name()   == formDataSourceStr(VendTable, VendTable))

                    {

                         _callerForm.allowEdit(true);

                    }

                    break;

                case VersioningDocumentState::InReview:

                    if(_callerForm && _callerForm.name()   == formDataSourceStr(VendTable, VendTable))

                    {

                         _callerForm.allowEdit(false);

                         _callerForm.allowDelete(false);

                    }

                    break;

                case VersioningDocumentState::Rejected:

                    if(_callerForm && _callerForm.name()   ==  formDataSourceStr(VendTable, VendTable))

                    {

                        _callerForm.allowEdit(true);

                    }

                    break;

                                                      

                default:

                    break;

            }

 

            _vendTable.doUpdate();

            ttscommit;

            if(_callerForm && _callerForm.name()   == formDataSourceStr(VendTable, VendTable))

            {

                _callerForm.refresh();

            }

        }

    }




Just to add on, to add any additional functionalities, you can use event handlers for different Approval Outcomes : Approve/Reject/Deny and Request Change. Event handlers of Approve and Reject outcome can be used to handle cases of auto approval and auto rejection respectively. 

I hope you'll be able to create a custom WF using this blog easily. All the best!