Monday 8 October 2012

Making all approval comments mandatory in a multi-step approval process.

A month ago, I produced a blog post that outlined how a picklist field and apex trigger could be used to make rejection comments in approval processes mandatory.

More recently, a question appeared on the force.com stack overflow relating to this post. The user was wondering if it was possible to make comments mandatory for all steps in an approval process that includes multiple approval steps.

Referring back to the previous post, I made comments mandatory on rejections by adding a field update action on final rejection. This field update would be trapped inside a before update trigger, which would go on to check the latest step object in the approval process for a comment. If no comment existed, it would produce an error asking the user to enter a reason for the rejection.

This works well for single step approvals or rejections, but multiple steps are more complex. Approval and rejection actions can be associated with individual steps inside an approval process, like so:



However, it is not quite that simple. The Salesforce order of execution complicates matters. The final approval / rejections work because the actions, such as field update, occur after the latest approval step has been stored, so we can use a trigger to look to check the most recent entry for comments.

This is not the case for individual approval step actions. The field updates occur before the latest approval step object is stored. This means that when the trigger tries to check for the comment, it can't retrieve the latest step object. This made me think that it may not be possible to accomplish. Then I found the re-trigger workflow option on the field update edit page:



If the update could cause workflow to fire again, then I realized it would be possible by adding a workflow rule that caused another update to the same object. This would avoid the order of execution problems, because the workflow and resulting update would occur after the insertion of the step, not before like the previous update.

So it is possible to create mandatory comments for each step, using a double step trigger. Here are the steps to make this possible:

1) Create a new picklist field on your approval process object called "Approval Comment Check". Assign two picklist values "Required" and "Requested", with no default, like this:


2) Create a new workflow field update action, call it "Approval Comment Required" and configure it so that your new Approval Comment Check field is updated to the "Required" value. Check the "Re-evaluate Workflow Rules after Field Change" checkbox.



 3) Create another field update action, called "Approval Comment Requested". This update should change the Approval Comment Check field to the "Requested" value. This time, do not check the "Re-evaluate Workflow Rules after Field Change" option.


4) Create a new workflow rule, on your object going through the approval process, called "Approval Comment Flag". Set the evaluation criteria to "When a record is created, or when a record is edited and did not previously meet the rule criteria". The rule criteria should be defined as when the Approval Comment Check field equals "Required". When you have finished, click Save & Next


5) On the new workflow summary page, click Add Workflow Action  underneath Immediate Workflow Actions. From the drop menu, click on Select Existing Action. Add the "Approval Comment Requested" rule you created in step 3 and then click Done. Your Summary Screen should look something like this. 

!!!!Don't forget to activate your workflow rule before continuing!!!! 



6) Create the following before update trigger on your approval process object.

trigger RequireApprovalComments on Invoice_Statement__c (before update) 
{
  // Create a map that stores all the objects that require editing 
  Map<Id, Invoice_Statement__c> approvalStatements = 
  new Map<Id, Invoice_Statement__c>{};

  for(Invoice_Statement__c inv: trigger.new)
  {
    // Put all objects for update that require a comment check in a map,
    // so we only have to use 1 SOQL query to do all checks
    
    if (inv.Approval_Comment_Check__c == 'Requested')
    { 
      approvalStatements.put(inv.Id, inv);
      // Reset the field value to null, 
      // so that the check is not repeated,
      // next time the object is updated
      inv.Approval_Comment_Check__c = null; 
    }
  }  
   
  if (!approvalStatements.isEmpty())  
  {
    // UPDATE 2/1/2014: Get the most recent process instance for the approval.
    // If there are some approvals to be reviewed for approval, then
    // get the most recent process instance for each object.
    List<Id> processInstanceIds = new List<Id>{};
    
    for (Invoice_Statement__c invs : [SELECT (SELECT ID
                                              FROM ProcessInstances
                                              ORDER BY CreatedDate DESC
                                              LIMIT 1)
                                      FROM Invoice_Statement__c
                                      WHERE ID IN :approvalStatements.keySet()])
    {
        processInstanceIds.add(invs.ProcessInstances[0].Id);
    }
      
    // Now that we have the most recent process instances, we can check
    // the most recent process steps for comments.  
    for (ProcessInstance pi : [SELECT TargetObjectId,
                                   (SELECT Id, StepStatus, Comments 
                                    FROM Steps
                                    ORDER BY CreatedDate DESC
                                    LIMIT 1 )
                               FROM ProcessInstance
                               WHERE Id IN :processInstanceIds
                               ORDER BY CreatedDate DESC])
    {
      // If no comment exists, then prevent the object from saving.                 
      if ((pi.Steps[0].Comments == null || 
           pi.Steps[0].Comments.trim().length() == 0))
      {
        approvalStatements.get(pi.TargetObjectId).addError(
         'Operation Cancelled: Please provide a reason ' + 
         'for your approval / rejection!');
      }
    }                                       
  }
}

7) Finally in your approval process, at each step you want a comment to be mandatory, add the "Approval Comment Required" field update created in step 2 to the approval actions. You can also add this update to the rejection action to make a comment mandatory for rejection at a particular step, although as a tip, if you want to make a comment mandatory on all rejections, simply add the field update to the final rejection actions (note this only works if the rejection behaviour is 'Final Rejection' rather than 'Go Back 1 Step'.



I admit that this is not the prettiest of solutions, but it does accomplish what was intended, all approvals do now have to be commented. One thing to be mindful of is that this approval method uses a two step update process, so if you have any update triggers for the approval object, they will be fired twice every time an approval step is completed.

Please vote up this community idea to accomplish this through the standard menu. As much as I enjoy working out how to accomplish feats like this with force.com, I would prefer it came out of the box :D
UPDATE 2/1/2014 sample test method:
/*
    A sample test class for the Require Approval Comments trigger
    Obviously adapt it to your own approval processes.
*/
@isTest
public class RequireApprovalCommentsTest
{
    /*
        For this first test, create an object for approval, then
        simulate rejeting the approval with an added comment for explanation.
        
        The rejection should be processed normally without being interrupted.
    */
    private static testmethod void testRejectionWithComment()
    {
        // Generate sample work item using utility method.
        Id testWorkItemId = generateAndSubmitObject();
        
        // Reject the submitted request, providing a comment.
        Approval.ProcessWorkitemRequest testRej = new Approval.ProcessWorkitemRequest();
        testRej.setComments('Rejecting request with a comment.');
        testRej.setAction  ('Reject');
        testRej.setWorkitemId(testWorkItemId);
    
        Test.startTest();        
            // Process the rejection
            Approval.ProcessResult testRejResult =  Approval.process(testRej);
        Test.stopTest();
        
        // Verify the rejection results
        System.assert(testRejResult.isSuccess(), 'Rejections that include comments should be permitted');
        System.assertEquals('Rejected', testRejResult.getInstanceStatus(), 
          'Rejections that include comments should be successful and instance status should be Rejected');
    }
    
    /*
        For this test, create an object for approval, then reject the request,
        without a comment explaining why. The rejection should be halted, and
        and an apex page message should be provided to the user.
    */
    private static testmethod void testRejectionWithoutComment()
    {
        // Generate sample work item using utility method.
        Id testWorkItemId = generateAndSubmitObject();
        
        // Reject the submitted request, without providing a comment.
        Approval.ProcessWorkitemRequest testRej = new Approval.ProcessWorkitemRequest();
        testRej.setComments('');
        testRej.setAction  ('Reject');      
        testRej.setWorkitemId(testWorkItemId);
    
        Test.startTest();        
        // Attempt to process the rejection
        try
        {
          Approval.ProcessResult testRejResult =  Approval.process(testRej);
          system.assert(false, 'A rejection with no comment should cause an exception');
        }
        catch(DMLException e)
        {
          system.assertEquals(
             'Operation Cancelled: Please provide a reason for your approval / rejection!', 
             e.getDmlMessage(0), 
             'error message should be Operation Cancelled: Please provide a rejection reason!'); 
        }
        Test.stopTest();
    }
    
    /*
        When an approval is approved instead of rejected, a comment is also required.
        Mark an approval as approved with a comment, it should be successful.
    */
    private static testmethod void testApprovalWithComment()
    {
        // Generate sample work item using utility method.
        Id testWorkItemId = generateAndSubmitObject();
        
        // approve the submitted request, providing a comment.
        Approval.ProcessWorkitemRequest testApp = new Approval.ProcessWorkitemRequest();
        testApp.setComments ('Sample approval comment');
        testApp.setAction   ('Approve');
        testApp.setWorkitemId(testWorkItemId);
    
        Test.startTest();        
            // Process the approval
            Approval.ProcessResult testAppResult =  Approval.process(testApp);
        Test.stopTest();
        
        // Verify the approval results
        System.assert(testAppResult.isSuccess(), 
                      'Approvals that include comments should still be permitted');
    }
    
    /*
        When an approval is approved instead of rejected, a comment is also required.
        Mark an approval as approved without a comment, it should be rejected and held back.
    */
    private static testmethod void testApprovalWithoutComment()
    {
        // Generate sample work item using utility method.
        Id testWorkItemId = generateAndSubmitObject();
        
        // approve the submitted request, without providing a comment.
        Approval.ProcessWorkitemRequest testApp = new Approval.ProcessWorkitemRequest();
        testApp.setComments ('');
        testApp.setAction   ('Approve');
        testApp.setWorkitemId(testWorkItemId);
        
        // Verify the approval results
        Test.startTest();        
       // Attempt to process the approval
        try
        {
          Approval.ProcessResult testAppResult =  Approval.process(testApp);
          system.assert(false, 'An with no comment should cause an exception');
        }
        catch(DMLException e)
        {
          system.assertEquals(
             'Operation Cancelled: Please provide a reason for your approval / rejection!', 
             e.getDmlMessage(0), 
             'error message should be Operation Cancelled: Please provide a rejection reason!'); 
        }
        Test.stopTest();        
        
    }    
    
    /*
        Put many objects through the approval process, some rejected, some approved,
        some with comments, some without. Only approvals and rejctions without comments should be
        prevented from being saved.
    */
    private static testmethod void testBatchRejctions()
    {
        List<Invoice_Statement__c> testBatchIS = new List<Invoice_Statement__c>{};
        for (Integer i = 0; i < 200; i++)
        {
            testBatchIS.add(new Invoice_Statement__c());
        }   
           
        insert testBatchIS;
        
        List<Approval.ProcessSubmitRequest> testReqs = new List<Approval.ProcessSubmitRequest>{}; 
        for(Invoice_Statement__c testinv : testBatchIS)
        {
            Approval.ProcessSubmitRequest testReq = new Approval.ProcessSubmitRequest();
            testReq.setObjectId(testinv.Id);
            testReqs.add(testReq);
        }
        
        List<Approval.ProcessResult> reqResults = Approval.process(testReqs);
        
        for (Approval.ProcessResult reqResult : reqResults)
        {
            System.assert(reqResult.isSuccess(), 
                         'Unable to submit new batch invoice statement record for approval');
        }
        
        List<Approval.ProcessWorkitemRequest> testAppRejs = new List<Approval.ProcessWorkitemRequest>{};
        
        for (Integer i = 0; i < 50 ; i++)
        {
            Approval.ProcessWorkitemRequest testRejWithComment = new Approval.ProcessWorkitemRequest();
            testRejWithComment.setComments  ('Rejecting request with a comment.');
            testRejWithComment.setAction    ('Reject');
            testRejWithComment.setWorkitemId(reqResults[i*4].getNewWorkitemIds()[0]);
            
            testAppRejs.add(testRejWithComment);
            
            Approval.ProcessWorkitemRequest testRejWithoutComment = new Approval.ProcessWorkitemRequest();
            testRejWithoutComment.setAction    ('Reject');
            testRejWithoutComment.setWorkitemId(reqResults[(i*4)+1].getNewWorkitemIds()[0]);
            
            testAppRejs.add(testRejWithoutComment);
            
            Approval.ProcessWorkitemRequest testAppWithComment = new Approval.ProcessWorkitemRequest();
            testAppWithComment.setComments  ('Approving request with a comment.');
            testAppWithComment.setAction    ('Approve');
            testAppWithComment.setWorkitemId(reqResults[(i*4)+2].getNewWorkitemIds()[0]);
            
            testAppRejs.add(testAppWithComment);
            
            Approval.ProcessWorkitemRequest testAppWithoutComment = new Approval.ProcessWorkitemRequest();
            testAppWithoutComment.setAction    ('Approve');
            testAppWithoutComment.setWorkitemId(reqResults[(i*4)+3].getNewWorkitemIds()[0]);
            
            testAppRejs.add(testAppWithoutComment);            
        }
            
        Test.startTest();        
            // Process the approvals and rejections
            try
            {
                List<Approval.ProcessResult> testAppRejResults =  Approval.process(testAppRejs);
                system.assert(false, 'Any rejections without comments should cause an exception');
            }
            catch(DMLException e)
            {
                system.assertEquals(100, e.getNumDml());
                
                for(Integer i = 0; i < 50 ; i++)
                {
                  system.assertEquals((i*4) + 1, e.getDmlIndex(i * 2));
                  system.assertEquals(
                    'Operation Cancelled: Please provide a reason for your approval / rejection!', 
                    e.getDmlMessage((i * 2)));
                  system.assertEquals((i*4) + 3, e.getDmlIndex((i * 2) + 1 ));
                  system.assertEquals(
                    'Operation Cancelled: Please provide a reason for your approval / rejection!', 
                    e.getDmlMessage((i * 2) + 1 ));
                }
            }    
        Test.stopTest();
    }
    
    /*
        Utility method for creating single object, and submitting for approval.
        
        The method should return the Id of the work item generated as a result of the submission.
    */
    private static Id generateAndSubmitObject()
    {
        // Create a sample invoice statement object and then submit it for approval.
        Invoice_Statement__c testIS = new Invoice_Statement__c();
        insert testIS;
        
        Approval.ProcessSubmitRequest testReq = new Approval.ProcessSubmitRequest();
        testReq.setObjectId(testIS.Id);
        Approval.ProcessResult reqResult = Approval.process(testReq);
        
        System.assert(reqResult.isSuccess(),'Unable to submit new invoice statement record for approval');
        
        return reqResult.getNewWorkitemIds()[0];
    }
}

36 comments:

  1. Hi Christopher,

    First of all, thanks for the tips.

    I did try to use the Approval Comment Required field update on a specific approved step but it does nothing, but using the Approval Comment Requested on the final rejection did work for all the approval rejection.
    I'm not sure how the required field update is supposed to work alone since the trigger is only checking for the 'requested' value.

    Could you help ?

    Regards,

    Pierre

    ReplyDelete
    Replies
    1. Hi Pierre,

      Thanks very much for your question. After reading your comment, I realised I had missed a couple of steps in my explanation. A workflow rule needs to be defined that recognises when the approval comment check field is set to "Required" and sets it "Requested" instead, before updating the record. This is actually the part of the process that performs the double update necessary to read the approval step information in the trigger.

      I have modified the post above to include these steps, which if you follow, should allow you to define approvals at individual steps as required. Sorry about that, thanks so much for bringing it to my attention!

      If you have any problems let me know :)

      Delete
  2. Hello Christopher,

    Thanks for this post!

    Have noticed a slight issue with this solution..

    If an Approval Step Rejection does not go to Final Rejection Step. ( i.e. one goes back a step during rejection )

    -- The processInstanceStep for that rejection is not inserted within the context of the current trigger. So one cannot determine if a comment had been entered and hence you always fail validation when trying to reject an approval step. ( even if comment is entered)

    The methodology above works fine when a rejection step goes to "Final Rejection Steps" but has issues when one has an approval step that returns to the previous step.

    I can see that my trigger is being fired during a "go back a step rejection" but cannot see that processInstanceStep rejection to check if comments exist.

    Have you seen this? Any ideas on a solution besides removing any steps that go back ( not a good solution )

    Thanks, Benjamin

    ReplyDelete
    Replies
    1. Hi Benjamin,

      Please try using the below. I guess It will work for the all the steps even if the rejection step goes to previous step.
      if ((pi.Steps[0].Comments == null || pi.Steps[0].Comments.trim().length() == 0)&&(pi.Steps[0].StepStatus =='Rejected'))

      Thanks,
      Prabhu.

      Delete
    2. Hi Benjamin and Parda,

      I have recreated your scenario, and modified the code (see code sample above as part of the post) to accommodate stepping back one step, please update and let me know if this helps,

      If you are still experiencing difficulties, can you please be more specific about your approval configuration and I can attempt to recreate the problem.

      Regards,
      CAL

      Delete
  3. Hi Christopher,

    Thank you for the useful post.
    Kindly post the Test cases for this trigger as well.

    Thanks,
    Prabhu

    ReplyDelete
  4. Hi Pardha,

    I have included a test class in the post above, please note that it is only a guide, you will need to adapt it to your own specific process.

    Cheers,
    CAL

    ReplyDelete
  5. Hi Christopher,

    Thank you very much for the updates.
    I have also worked on the same way and was able to solve the issue.

    Thanks,
    Pardha.

    ReplyDelete
    Replies
    1. No problem Pardha! Glad you solved the issue.

      Cheers,
      CAL

      Delete
  6. i am getting "no applicable approval process found" error

    ReplyDelete
    Replies
    1. Hi Vivekh,

      There are two common reasons for receiving such an error:

      1) The object you are submitting for for approval does not match the entry criteria for any of your approval processes. If it does not match any criteria, but the request needs to be approved, then you need to review your approval processes.
      2) If (1) is not true, check that the rule where the entry criteria matches is active. If not activate it and try again.

      let me know how you get on.

      Cheers,
      Chris

      Delete
  7. Hi Chris

    Can i add a link "return to the previous page" in addition to the current massage? how?....
    (since right now i gwt a brand new page with this massage)

    Thanks!

    ReplyDelete
    Replies
    1. Hi Mattan,

      This is possible if you are using a custom Visualforce Page to submit your requests for approval. You can get the previous page URL to return to by grabbing the current "retURL" parameter from the page in apex (ApexPages.currentPage().getParameters().get('retURL')), then insert the link in the message using standard "a" link tags in the message definition. Bear in mind that you may have to do this inside a wrapped controller for the page rather than directly in the trigger.

      I'm not sure this is entirely possible on the standard approval page, as in order for the link to render correctly in the page message, you need to place the escape="false" argument into the apex:pagemessages tag, which you have no control over in the standard page.

      Let me know how you get on...

      Regards,
      CAL

      Delete
  8. Hi Christopher,

    Thanks for your post. I have followed all the steps. When I approve/Reject the approval process, from somewhere I am getting value for comments (if ((pi.Steps[0].Comments) as "Please validate details and provide approva;" in trigger even though I didnt enter any comments. Can you please let me know from where I am getting comment value.

    ReplyDelete
  9. This comment has been removed by the author.

    ReplyDelete
  10. This comment has been removed by the author.

    ReplyDelete
  11. Great post!

    I have a similar questions regarding the approval layout. I know we are able to edit the layout and add fields from the related object on the approval layout. However, how can I accomplish not only adding this checkbox to the layout but also having the ability for the approver to check this box if needed? Once checked the workflow rule i already created will fire a task to the appropriate user. We cannot fire this task based on approval only, because not all approvals require this task; only when this checkbox is checked.

    ReplyDelete
  12. Hi Christopher, this is not working in Lightning, could you please check and help.

    ReplyDelete
    Replies
    1. Hey Rafi Md,

      Did you got any solution for this in Lightning?

      Thanks in Advance,
      Vamsy M

      Delete
    2. Hi

      Any solution how to implement in lightning?

      Thanks
      Mani Sai Gundumogula

      Delete
  13. Hi Christopher

    I implemented this successfully for an individual approver. However, when the approval request is assigned to a queue, the validation error is thrown even when approval/rejection comments are mentioned.

    Any idea what could be the issue?

    ReplyDelete
  14. This is not working in Lightning. Could you please help..!!

    ReplyDelete
    Replies
    1. Hi, Did you find a solution for lightning?

      Delete
    2. Did you get the Solve around for Lightning!!

      Delete
  15. It is showing the error properly if comments are missing but the order status is still updated to rejected.

    ReplyDelete
    Replies
    1. Approval is also rejected after showin the error message.

      Delete
  16. Right off the bat, it is significant that the shading and state of your logo is one of its sort contrasted with that of other organization logos that advance comparable items and administrations. logo design service

    ReplyDelete
  17. Hi All,
    I tried the above solution in lightning but for ist level of approval it works well but the status field gets updated to Approved.How can we update this field to Pending.When the second approver tries to approve the record the reocrd get approved even if the comments are not entered.Please guide.
    Thanks

    ReplyDelete
  18. Hi Christopher!!

    Thia blog ia Indeed Life saver and wonderful.

    T had implemented the above solution, But It is not woking in Lighning,!!!

    Kindly help, Please!!

    ReplyDelete
  19. This is very interesting content! I have thoroughly enjoyed reading your points and have come to the conclusion that you are right about many of them. You are great. ralevant backlink

    ReplyDelete
  20. I personally like your post; you have shared good insights and experiences. Keep it up. get backlinks

    ReplyDelete
  21. is there any solution for this to work in lightning?

    ReplyDelete
  22. Hey what if I want to add the comments which I entered to a custom field?
    Suppose I have a custom field "Reason for Rejection", I need this field to get the value I entered in the comments for rejection. How do I achieve this?

    ReplyDelete
  23. Hi, I was wondering if I could require comments by the submitter in the intial submission actions? I tried used the field update action approval comment required, but all it does is first submit for approval without any comments and tells me I need to enter comments, but problem is it submits for approval anyways without me enter comments.

    ReplyDelete