Custom workflows in Microsoft Dynamics 365 for Operations

 A workflow is a system that provides functionality to create business processes. It defines how a document flows through the system by showing the steps needed to process and approve it along with who needs to process it. The steps for creating a workflow in Microsoft Dynamics 365 for Finance and Operations is mostly unchanged from Dynamics AX 2012. The major difference is that Dynamics 365 workflows include support for state machines (a topic for a future post).

Note: These steps address creating a customized workflow in the same model as the document table. For cases, the model is not the same, it should be noted that, as of the time of this post, it is impossible to implement parts of this without overlaying. Specifically, we will need to overlay the document table to implement the canSubmitToWorkflow() method.

The following steps are for creating a custom workflow for a document table called MK_WFDocument.

Workflow status enumeration

The custom workflow will use a base enumeration to define its status. For this example, the enumeration will have basic statuses: draft, submitted, change request, rejected, and approved. In this example, the enumeration’s name is MK_WFDocumentStatus.

We will need to create a field on the document table using the enumeration. We should make it read-only as the workflow updates it. Optionally, we should edit the document’s form to display the new field.

Table methods

We need to override the canSubmitToWorkflow() method on our document table. This method implements the logic to determine if a record is valid for submission to workflow. For this example, the record will be valid if it has in the Draft status.

public boolean canSubmitToWorkflow(str _workflowType = '')
{
    boolean ret = super(_workflowType);
    
    ret = this.WorkflowStatus == MK_WFDocumentStatus::Draft;
    
    return ret;
}

Optionally, we should add a method for updating the workflow status using a record identifier. This method is mainly for convenience but it could be useful in implementing other business logic.

public static void updateWorkflowStatus(RecId _documentRecId, MK_WFDocumentStatus _status)
{
    ttsbegin;
    
    MK_WFDocument document;
    
    update_recordset document
        setting WorkflowStatus = _status
        where document.RecId == _documentRecId;
    
    ttscommit;
}

Query

The query defines the tables and field that the workflow will use. For this example, we will define a query that returns all fields of our document table. Note: Remember to set the Dynamic fields property to Yes.

Workflow category

Workflow uses the category to determine in which module the workflow will appear. If appropriate, we can use an existing category but most of the time, we will want to create a new one. Creating a workflow category is just creating it and setting the module property (in this example, the category will use fleet management).

Note: When creating a workflow for a new project, it is necessary to extend the module to the base enumeration behind the module property. Additionally, there will need to be a workflow configuration form. Fortunately, these changes are easy to implement (link).

Workflow type

The workflow type describes the workflow and the set of elements available to the workflow. When creating a workflow type, there is a wizard to simplify its creation.

  • Category: Select the workflow category from the previous step
  • Query: Select the query created for the document table
  • Document menu item: Select the menu item for the document table’s form.

After completing the wizard, Visual Studio creates objects in our project.

The workflow type that the wizard creates has several properties. Most of them are properties that indicate what the workflow should call for certain events. In general, the only thing we need to do is to change the label and help text to something meaningful. The other objects that the wizard creates are the following (referenced by the suffix that the wizard gives).

  • Document class: This class implements a method that returns a reference to our query. Most of the time, we will not change this class.
  • EventHandler class: This class handles the type-specific events. The wizard creates methods to which we can add our business logic. We will be adding code in a later step.
  • SubmitManager class: Workflow calls this class when submitting a document to workflow. My preference is to scrap this class and replace it with a generalized class that handles both submission and resubmission of the document. We will replace it in the next step.
  • CancelMenuItem action menu item: Workflow uses this menu item for canceling the workflow on a document. We need to update label and help text to something meaningful.
  • SubmitMenuItem action menu item: Workflow uses this menu item for submitting a document to the workflow. This menu item calls the SubmitManager class. Since we will be replacing that class, we will need to change it. For now, we need to update the label and help text to something meaningful.

Submission manager

The submission manager class is responsible for creating the dialog for submitting a document to the workflow. In sophisticated workflows, there may be many classes to handle submission and resubmission. Rather than have multiple classes that implement similar logic, my preference is to have a generalized class that handles both scenarios (this class is based on the BankReconciliationApprovalWorkflow class).

class MK_WFDocumentSubmitManager
{
    private MK_WFDocument document;
    private WorkflowVersionTable versionTable;
    private WorkflowComment comment;
    private WorkflowWorkItemTable workItem;
    private SysUserId userId;
    private boolean isSubmission;
    private WorkflowTypeName workflowType;

    /// <summary>
    /// Main method
    /// </summary>
    /// <param name = "_args">calling arguments</param>
    public static void main(Args _args)
    {
        if (_args.record().TableId != tableNum(MK_WFDocument))
        {
            throw error('Error attempting to submit document');
        }

        MK_WFDocument document = _args.record();
        FormRun caller = _args.caller() as FormRun;
        boolean isSubmission = _args.parmEnum();
        MenuItemName menuItem = _args.menuItemName();

        MK_WFDocumentSubmitManager manager = MK_WFDocumentSubmitManager::construct();
        manager.init(document, isSubmission, caller.getActiveWorkflowConfiguration(), caller.getActiveWorkflowWorkItem());

        if (manager.openSubmitDialog(menuItem))
        {
            manager.performSubmit(menuItem);
        }

        caller.updateWorkflowControls();
    }

    /// <summary>
    /// Construct method
    /// </summary>
    /// <returns>new instance of submission manager</returns>
    public static MK_WFDocumentSubmitManager construct()
    {
        return new MK_WFDocumentSubmitManager();
    }

    /// <summary>
    /// parameter method for document
    /// </summary>
    /// <param name = "_document">new document value</param>
    /// <returns>current document</returns>
    public MK_WFDocument parmDocument(MK_WFDocument _document = document)
    {
        document = _document;

        return document;
    }

    /// <summary>
    /// parameter method for version
    /// </summary>
    /// <param name = "_versionTable">new version table value</param>
    /// <returns>current version table</returns>
    public WorkflowVersionTable parmVersionTable(WorkflowVersionTable _versionTable = versionTable)
    {
        versionTable = _versionTable;

        return versionTable;
    }

    /// <summary>
    /// parameter method for comment
    /// </summary>
    /// <param name = "_comment">new comment value</param>
    /// <returns>current comment value</returns>
    public WorkflowComment parmComment(WorkflowComment _comment = comment)
    {
        comment = _comment;

        return comment;
    }

    /// <summary>
    /// parameter method for work item
    /// </summary>
    /// <param name = "_workItem">new work item value</param>
    /// <returns>current work item value</returns>
    public WorkflowWorkItemTable parmWorkItem(WorkflowWorkItemTable _workItem = workItem)
    {
        workItem = _workItem;

        return workItem;
    }

    /// <summary>
    /// parameter method for user
    /// </summary>
    /// <param name = "_userId">new user value</param>
    /// <returns>current user value</returns>
    public SysUserId parmUserId(SysUserId _userId = userId)
    {
        userId = _userId;

        return userId;
    }

    /// <summary>
    /// parameter method for isSubmission flag
    /// </summary>
    /// <param name = "_isSubmission">flag value</param>
    /// <returns>current flag value</returns>
    public boolean parmIsSubmission(boolean _isSubmission = isSubmission)
    {
        isSubmission = _isSubmission;

        return isSubmission;
    }

    /// <summary>
    /// parameter method for workflow type
    /// </summary>
    /// <param name = "_workflowType">new workflow type value</param>
    /// <returns>current workflow type</returns>
    public WorkflowTypeName parmWorkflowType(WorkflowTypeName _workflowType = workflowType)
    {
        workflowType = _workflowType;

        return workflowType;
    }

    /// <summary>
    /// Opens the submit dialog and returns result
    /// </summary>
    /// <returns>true if dialog closed okay</returns>
    protected boolean openSubmitDialog(MenuItemName _menuItemName)
    {
        if (isSubmission)
        {
            return this.openSubmitDialogSubmit();
        }
        else
        {
            return this.openSubmitDialogResubmit(_menuItemName);
        }
    }

    /// <summary>
    /// Open submission dialog
    /// </summary>
    /// <returns>true if dialog closed okay</returns>
    private boolean openSubmitDialogSubmit()
    {
        WorkflowSubmitDialog submitDialog = WorkflowSubmitDialog::construct(this.parmVersionTable());
        submitDialog.run();
        this.parmComment(submitDialog.parmWorkflowComment());
        
        return submitDialog.parmIsClosedOK();
    }

    /// <summary>
    /// Open resubmit dialog
    /// </summary>
    /// <returns>true if dialog closed okay</returns>
    private boolean openSubmitDialogResubmit(MenuItemName _menuItemName)
    {
        WorkflowWorkItemActionDialog actionDialog = WorkflowWorkItemActionDialog::construct(workItem, WorkflowWorkItemActionType::Resubmit, new MenuFunction(_menuItemName, MenuItemType::Action));
        actionDialog.run();
        this.parmComment(actionDialog.parmWorkflowComment());
        this.parmUserId(actionDialog.parmTargetUser());

        return actionDialog.parmIsClosedOK();
    }

    /// <summary>
    /// initializes manager
    /// </summary>
    /// <param name = "_document">document</param>
    /// <param name = "_menuItem">calling menu item</param>
    /// <param name = "_versionTable">workflow version</param>
    /// <param name = "_workItem">workflow item</param>
    protected void init(MK_WFDocument _document, boolean _isSubmission, WorkflowVersionTable _versionTable, WorkflowWorkitemTable _workItem)
    {
        this.parmDocument(_document);
        this.parmIsSubmission(_isSubmission);
        this.parmVersionTable(_versionTable);
        this.parmWorkItem(_workItem);
        this.parmWorkflowType(this.parmVersionTable().WorkflowTable().TemplateName);
    }

    /// <summary>
    /// perform workflow submission
    /// </summary>
    protected void performSubmit(MenuItemName _menuItemName)
    {
        if (isSubmission)
        {
            this.performSubmitSubmit();
        }
        else
        {
            this.performSubmitResubmit(_menuItemName);
        }
    }

    /// <summary>
    /// perform workflow submit
    /// </summary>
    private void performSubmitSubmit()
    {
        if (this.parmWorkflowType() && MK_WFDocument::findRecId(document.RecId).WorkflowStatus == MK_WFDocumentStatus::Draft)
        {
            Workflow::activateFromWorkflowType(workflowType, document.RecId, comment, NoYes::No);
            MK_WFDocument::updateWorkflowStatus(document.RecId, MK_WFDocumentStatus::Submitted);
        }
    }

    /// <summary>
    /// perform workflow resubmit
    /// </summary>
    private void performSubmitResubmit(MenuItemName _menuItemName)
    {
        if (this.parmWorkItem())
        {
            WorkflowWorkItemActionManager::dispatchWorkItemAction(workItem, comment, userId, WorkflowWorkItemActionType::Resubmit, _menuItemName);
            MK_WFDocument::updateWorkflowStatus(document.RecId, MK_WFDocumentStatus::Submitted);
        }
    }

}

The key thing to remember about using this class is that the submission action menu item needs to have a value of NoYes::Yes in the enum value parameter. Likewise, any resubmission action menu items will need to have a value of NoYes::No in the enum value parameter. Also, we need to change the target of the SubmitMenuItem action menu item to call this class. Since we do not need the SubmitManager class, we can delete it.

Workflow type events

We need to handle some events for the workflow type. We will be implementing logic in all three methods.

public void started(WorkflowEventArgs _workflowEventArgs)
{
    RecId documentRecId = _workflowEventArgs.parmWorkflowContext().parmRecId();
    MK_WFDocument::updateWorkflowStatus(documentRecId, MK_WFDocumentStatus::Submitted);
}

public void canceled(WorkflowEventArgs _workflowEventArgs)
{
    RecId documentRecId = _workflowEventArgs.parmWorkflowContext().parmRecId();
    MK_WFDocument::updateWorkflowStatus(documentRecId, MK_WFDocumentStatus::Rejected);
}

public void completed(WorkflowEventArgs _workflowEventArgs)
{
    RecId documentRecId = _workflowEventArgs.parmWorkflowContext().parmRecId();
    MK_WFDocument::updateWorkflowStatus(documentRecId, MK_WFDocumentStatus::Approved);
}

Workflow approval

An approval is a workflow element that we can add to a workflow. In this case, an approval is a step where one or more people need to approve something. There are other elements that we can add to a workflow. Like workflow types, creating a workflow approval opens a wizard.

Note: it may be necessary to build the project for previously created classes to be available.

  • Workflow document: Select the Document class that the workflow type wizard created.
  • Document preview field group: Select the field group for displaying identifying information of the document table
  • Document menu item: Select the menu item for the document table’s form

Just like the workflow type’s wizard, this wizard creates many objects in the project.

  • EventHandler class: This class handles the approval specific events. The wizard creates methods to which we can add our business logic. We will be adding code in a later step.
  • ResubmitActionMgr class: Workflow calls this class when the user resubmits the document. Like the SubmitManager class for the workflow type, we will use the generalized class instead of this class.
  • Approve action menu item: Workflow uses this menu item for approving the approval. We need to change the label and help text to something meaningful.
  • DelegateMenuItem action menu item: Workflow uses this menu item when delegating the approval. We need to change the label and help text to something meaningful.
  • Reject action menu item: Workflow uses this menu item when rejecting the approval. We need to change the label and help text to something meaningful.
  • RequestChange action menu item: Workflow uses this menu item when requesting a change. We need to change the label and help text to something meaningful.

We need to change the ResubmitMenuItem to use the generalized class. The changes are like the ones we made to the SubmitMenuItem menu item.

Workflow approval events

For this example, we will be adding logic to two of the event handling method

public void canceled(WorkflowElementEventArgs _workflowElementEventArgs)
{
    RecId documentRecId = _workflowElementEventArgs.parmWorkflowContext().parmRecId();
    MK_WFDocument::updateWorkflowStatus(documentRecId, MK_WFDocumentStatus::Rejected);
}

public void completed(WorkflowElementEventArgs _workflowElementEventArgs)
{
    RecId documentRecId = _workflowElementEventArgs.parmWorkflowContext().parmRecId();
    MK_WFDocument::updateWorkflowStatus(documentRecId, MK_WFDocumentStatus::Rejected);
}

Adding the approval to the type

To finish the workflow approval, we need to add it to the workflow type as a supported type. Under the supported elements, create a node for the workflow approval with the appropriate properties.

Enable the workflow on the form

The last step in creating a customized workflow is to enable it on the form. On the details node of the document table’s form, change the Workflow data source, Workflow enabled, and Workflow type properties.

Creating a new workflow from the customized workflow

After building the project, we need to create the workflow. We use the workflow configuration set up form in the module specified in the workflow category. In this example, we go to the Fleet Management module.

In the configuration form, we click the new button and select our workflow type from the list. When asked, we enter our credentials and the workflow editor will open. In the workflow editor, we add the workflow approval to the workflow. Next, we connect the start node to the approval and the approval to the end node. Finally, we resolve the warnings and click Save and close. When asked, activate the workflow.

Note: The workflow editor app only supports the Internet Explorer browser.

Now, when we create a record in our document table, the workflow button should appear in the menu bar of the form.

That concludes this post on workflow. The next post will discuss the state machine support that Microsoft added in D365FO.

Comments

Popular posts from this blog

Customization on Sales invoice Report in D365 F&O

75) COC - Create a coc of the table modified method

46) D365 FO: SHAREPOINT FILE UPLOAD USING X++