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

标签: salesforce  Salesforce

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:
 

uploading.4e448015.gif转存失败重新上传取消



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:
 

uploading.4e448015.gif转存失败重新上传取消

 

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.

uploading.4e448015.gif转存失败重新上传取消

 

 

 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.

uploading.4e448015.gif转存失败重新上传取消

 

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

uploading.4e448015.gif转存失败重新上传取消

 

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!!!! 

uploading.4e448015.gif转存失败重新上传取消

 

 

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'.
 

uploading.4e448015.gif转存失败重新上传取消



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];
    }
}
版权声明:本文为zhang44429824原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zhang44429824/article/details/109380858

智能推荐

SpringBoot 使用freemarker 处理文档,找不到文件位置(报错:basePackagePath=““ /* relatively to resourceLoaderClass pkg)

在Spring Boot中加载word的文档的时候,加载ftl文档的位置应该是从 target目录下面去加载的(不太确定),不是像大多数情况这样根据类的路径去加载。SpringBoot加载的位置应该是从 “resources”文件下面开始,如果放到“resources”的根目录下面需要加一道“/”斜线。 类似于: config...

剑指offer 合并两个排序的链表

题目 链接:https://leetcode-cn.com/problems/he-bing-liang-ge-pai-xu-de-lian-biao-lcof/ 思路 我想的是,与合并两个有序数组一样的思维,新建一个链表,然后判断谁的值大,进而在新的链表上面进行插入。 看书思路 合并链表是一个递归问题:合并一个节点后可以转化为一个子问题。终止条件是其中一个链表为空 代码 链表反转也可以用递归解决...

Java编程思想 第三章:操作符

Java中的操作符和c/c++中的操作符基本一致,因为我之前学习过C语言和C++,所以本章的内容大部分都已熟知,下面只做简单的介绍。 Java操作符及优先级 Java中的操作符包括算术操作符,关系操作符,逻辑操作符,位运算符、自操作运算符、移位运算符、赋值运算符和其他运算符。 算术操作符:包括加减乘除和取余(%),优先级乘除取余高于加减,都是双元运算符,其中加法(+)可以用来连接两个字符串,比如:...

JetBrains 系列开发工具,如何配置 `SCSS` `File Watcher` ,相关输出配置参数详解:webStorm phpStorm IDEA

JetBrains 系列开发工具,如何配置 SCSS File Watcher ,相关输出配置参数详解:webStorm phpStorm IDEA 前言 你目前已经了解了如何使用 SCSS 进行开发,了解了该文章的内容:『 SCSS 日常用法 』 在 JetBrains 系列开发工具中通过 FileWatcher 进行编译的 SCSS 文件都是通过 sass 这个程序进行的。『 如何添加 Fil...

C语言小函数—二进制与十六进制

测试如下 “` int main() { long int num = 15; } “`...

猜你喜欢

仿微博或微信的文章多图显示(自定义MultiImageView)

按照一般的规矩,先上张图来供大伙看看 如果大致是大伙们需要实现的功能,不烦一观 自定义MultiImageView 工具类 具体使用 app.gradle中添加依赖 implementation 'com.github.bumptech.glide:glide:4.8.0' AndroidManifest.xml中配置联网权限 <uses-permission android:name=&q...

经典进程同步和互斥问题

经典进程同步与互斥问题 前言 一、生产者-消费者问题 1.问题描述 2.问题分析 3.代码 二、读者-写者问题 1.问题描述&&分析 2.代码 三、哲学家进餐问题 1.问题描述&&分析 2.代码 四、理发师问题 1.问题描述&&分析 2.代码 前言 在多道程序设计环境中,进程同步是一个非常重要的问题,下面讨论几个经典的进程同步问题。 一、生产者-消费...

java设计模式——ThreadLocal线程单例

1、定义一个ThreadLocal线程单例,代码如下: 2、定义一个多线程类,代码如下: 3、定义一个测试类,代码如下: 4、输出结果,如下图:...

【tensorflow】线性模型实战

线性模型:y = 1.477 * x + 0.089   1. 采样数据 采样噪声eps在均值0,方差0.01的高斯分布中,而后在均匀分布U(0,1)中,区间[-10,10]进行n=100次随机采样:   2. 计算误差 循环计算每个点的预测值与真是值之间差的平方并累加,从而获得训练集上的均芳误差损失值。   3. 计算梯度   4. 梯度更新 对权重w和偏...

常见损失函数和评价指标总结(附公式&代码)

网上看到一篇很实用的帖子关于常见损失函数和评价指标,收藏下来 本文转载于https://zhuanlan.zhihu.com/p/91511706 ------------------------------------------------------------------------------------------------------------------------------...