Last active
October 11, 2021 20:54
-
-
Save AndersonKatharineTNC/6b04db1f68540c729b9fbd15ac92ec13 to your computer and use it in GitHub Desktop.
New Submit for Approval Button (Salesforce)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Written by: Katharine Anderson | |
Last updated: 04-18-2019 | |
Purpose: This is the controller behind the ApprovalProcessPreview visualforce page (which has lightning | |
styling in Lightning and classic styling in classic). | |
Workflow: Allow the approval process submitter to click the new 'submit for approval' button when | |
they're ready to submit. They will then see who the assigned approvers will be. If it doesn't meet | |
any entry criteria, then they would be able to go back and fix it instead of getting the standard | |
error message, which is generally not helpful. This also gives them the opportunity to change who | |
will be in the approval process if needed. | |
The comments field in the submission from the visualforce page will contain the list of assigned | |
approvers, in case anyone needs to reference it after submission. | |
If the record is already in an approval process, the page will show an error message telling the user | |
that it's already routing, and if the record doesn't meet any entry criteria they will get an error | |
message telling them to reach out to their operations partners or system administrators for assistance. | |
To add this functionality to an object in Saleforce, just add the following code to a custom detail | |
page button: | |
-Name: Submit for Approval (replace the standard 'Submit for Approval' button on the detail page layout.) | |
-Contact Source: URL | |
-Behavior: Display in existing window without sidebar or header): | |
-URL: {!URLFOR("/apex/ApprovalProcessPreview", {{{PUT OBJECT NAME HERE}}}.Id, [id={{{PUT OBJECT NAME HERE}}}.Id])} | |
NOTE: This will NOT replace the standard 'Submit for Approval' button on the Approval History related | |
list. This may be a good workaround if this issue comes up enough: | |
https://developer.salesforce.com/forums/?id=906F0000000BNxxIAG | |
*/ | |
public class ApprovalPreviewController { | |
//declare variables | |
public Id objectId {get;set;} | |
public string apEmpty {get;set;} | |
public string pendingPI {get;set;} | |
public id theApprovalUserId {get;set;} | |
public String theObjectType {get;set;} | |
public ApprovalProcess ap {get;set;} | |
public Approval.ProcessResult submitResult {get;set;} | |
public ApprovalPreviewController() { | |
objectId = String.escapeSingleQuotes(ApexPages.currentPage().getParameters().get('id')); | |
} | |
//declare public list where the approval steps will be stored (for putting into the comments field when submitting for approval.) | |
public list<approvalStep> stepsForComments = new list<approvalStep>(); | |
//create approvalProcess class | |
public class ApprovalProcess{ | |
public String processName {get;set;} | |
public String processDescription {get;set;} | |
public List<approvalStep> approvalSteps { | |
get{ | |
if(approvalSteps == null) approvalSteps = new List<ApprovalStep>(); | |
return approvalSteps; | |
} | |
set; | |
} | |
} | |
//create approvalStep class | |
public class ApprovalStep{ | |
public Integer stepNumber {get;set;} | |
public String stepName {get;set;} | |
public String approver {get;set;} | |
} | |
//method that creates the preview of the approval steps | |
public void runPreview(){ | |
//initalize submission variables | |
theObjectType = objectId.getSObjectType().getDescribe().getName(); | |
theApprovalUserId = UserInfo.getUserId(); | |
//set savepoint, so that any database changes during the try/catch block can get rolled back | |
system.savepoint sp1 = Database.setSavepoint(); | |
try{ | |
//submit approval | |
Approval.ProcessSubmitRequest req1 = new Approval.ProcessSubmitRequest(); | |
req1.setComments('Submitting request for approval.'); | |
req1.setObjectId(objectId); | |
req1.setSubmitterId(theApprovalUserId); | |
Approval.ProcessResult result = Approval.process(req1); | |
id newInstanceID = goThroughSteps(result); | |
system.debug('***NewInstanceID: '+newInstanceId); | |
//create 'approvalSteps' list and 'stepsForComments' list | |
List<ProcessInstance> newInstanceList = [SELECT Id, ProcessDefinition.Name, ProcessDefinition.Description, (SELECT Id,ProcessInstanceID,OriginalActorId,OriginalActor.Name,ProcessNodeID,ProcessNode.Name | |
FROM StepsAndWorkItems | |
WHERE StepStatus = 'Approved' | |
ORDER BY ID) | |
FROM ProcessInstance | |
WHERE ID =: newInstanceID | |
LIMIT 1]; | |
system.debug('***NewInstance: '+newInstanceList); | |
ProcessInstance newInstance = newInstanceList[0]; | |
ap = new ApprovalProcess(); | |
ap.processName = newInstance.ProcessDefinition.Name; | |
ap.processDescription = newInstance.ProcessDefinition.Description; | |
for(ProcessInstanceHistory pih : newInstance.StepsAndWorkitems ){ | |
ApprovalStep aStep = new ApprovalStep(); | |
aStep.stepNumber = ap.approvalSteps.size() + 1; | |
aStep.stepName = pih.ProcessNode.Name; | |
aStep.approver = pih.OriginalActor.Name; | |
system.debug('***aStep = '+aStep); | |
//only used within this method | |
ap.approvalSteps.add(aStep); | |
//public method (accessible by submitForApproval() below) | |
stepsForComments.add(aStep); | |
} | |
//catch exceptions and return error message to user | |
} catch(DmlException e) { | |
System.debug('The following exception has occurred: ' + e.getMessage()); | |
if(e.getMessage().contains('NO_APPLICABLE_PROCESS')){ | |
apEmpty = 'This record does not meet the entry criteria for any approval processes. Please reach out to your Operations partner or a System Administrator for assistance.'; | |
} if (e.getMessage().contains('ALREADY_IN_PROCESS')){ | |
pendingPI = 'This record is already in an approval process. For more information, please return to the detail page and review the Approval History.'; | |
} | |
} | |
Database.rollback(sp1); | |
} | |
//go through approval steps for preview | |
public id goThroughSteps(Approval.ProcessResult result){ | |
// First, get the ID of the newly created item | |
List<Id> newWorkItemIds = result.getNewWorkitemIds(); | |
ProcessInstanceWorkitem piw = [Select Id, ActorId, Actor.Name,ProcessInstanceId From ProcessInstanceWorkitem Where Id = :newWorkItemIds[0] ]; | |
system.debug('***PIW Actor ID: '+piw.actorId + ' '+piw.actor.Name); | |
List<Id> actorIDs = new List<ID>(); | |
// Instantiate the new ProcessWorkitemRequest object and populate it | |
Approval.ProcessWorkitemRequest req2 = new Approval.ProcessWorkitemRequest(); | |
req2.setComments('Approving request.'); | |
req2.setAction('Approve'); | |
// Use the ID from the newly created item to specify the item to be worked | |
req2.setWorkitemId(newWorkItemIds.get(0)); | |
// Submit the request for approval | |
Approval.ProcessResult result2 = Approval.process(req2); | |
system.debug('***result2=' + result2); | |
if(!result2.getNewWorkitemIds().isEmpty()){ | |
goThroughSteps(result2); | |
} | |
//return newInstanceID so that it can be used in the try/catch block of runPreview() | |
String newInstanceId = result2.getInstanceId(); | |
System.debug(result2.getInstanceStatus()); | |
return newInstanceId; | |
} | |
//visualforce page submit for approval button code | |
public PageReference submitForApproval(){ | |
//instantiate variables | |
id currentUserId = userInfo.getUserId(); | |
id recordID = String.escapeSingleQuotes(ApexPages.currentPage().getParameters().get('id')); | |
system.debug('***recordId: '+recordId); | |
//re-format list 'stepsForComments' to make it easier to read in the comments section | |
List<string> asStrings = new List<String>(); | |
system.debug('*StepsForComments: '+stepsForComments); | |
For(ApprovalStep aStep : stepsForComments){ | |
string approvalStepString = aStep.stepNumber +'. '+ aStep.stepName +' - '+ aStep.approver; | |
asStrings.add(approvalStepString); | |
} | |
String joined = String.join(asStrings, '\n'+' '); | |
//submit request for approval | |
Approval.ProcessSubmitRequest req = new Approval.ProcessSubmitRequest(); | |
req.setObjectId(recordID); | |
req.setComments('Submitting request for approval - assigned approvers: \n'+joined); | |
req.setSubmitterId(currentUserId); | |
submitResult = Approval.process(req); | |
//send user back to record detail page | |
PageReference pageRef = new PageReference('/'+recordID); | |
pageRef.setRedirect(true); | |
return pageRef ; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Written by: Katharine Anderson | |
Last updated: 04-18-2019 | |
Purpose: Test the ApprovalPreviewController class (the controller behind the ApprovalProcessPreview | |
visualforce page). | |
To make this work in another org, you will need to set up a few things first, both in Sandbox and in Production. | |
This way, if the actual approval processes in your org change (which they very likely will), you will continue | |
to get full test coverage: | |
1) Custom Object - | |
NAME: Approval_Process_Test__c | |
LABEL: ZDO NOT DELETE: Approval Process Test | |
DESCRIPTION: DO NOT DELETE!!! It's expected that there will be no records in this object. This is the object | |
used to create temporary test records to test the ApprovalPreviewController apex class. This is the class that | |
drives the 'Review and Submit for Approval' custom buttons. Code coverage will fail, and may block any new code | |
from being added to production if you delete this object without also replacing any references to it in the | |
following code and getting full test coverage: ApprovalPreviewControllerTest.apxc, TestDataFactory.apxc (which is | |
not advisable. If you want to remove the functionality entirely, you will need to delete: | |
ApprovalPreviewController.apxc, ApprovalPreviewControllerTest.apxc, ApprovalProcessPreview.vfp, and | |
remove the references to this object in the TestDataFactory.apxc before deleting this object. | |
2) Custom field on Approval_Process_Test__c: | |
FIELD TYPE: Checkbox | |
Default: Unchecked | |
NAME: Record_should_enter_AP__c | |
LABEL: Record should enter Approval Process | |
HELP TEXT: Check this box if the test record should meet the entry criteria for an approval process. | |
3) Approval Process on Approval_Process_Test__c: | |
NAME: Default Approval Process | |
UNIQUE NAME: Default_Approval_Process | |
ENTRY CRITERIA (Criteria are met): | |
Field - ZDO NOT DELETE: Approval Process Test: Record should enter Approval Process | |
Operator - equals | |
Value - True | |
4) Create TWO Approval Steps on the Approval Process: | |
Step 1) SELECT APPROVER: Automatically assign to approver(s): Related User - 'Created By' | |
Step 2) SELECT APPROVER:Automatically assign to approver(s): Related User - 'Last Modified By' | |
5) Activate the Approval Process (in both Sandbox AND Production) | |
*/ | |
@isTest | |
public class ApprovalPreviewControllerTest { | |
@testSetup | |
static void setupAPTs() { | |
//insert test ApprovalProcessTest records | |
List<Approval_Process_Test__c> aptList = TestDataFactory.createAPTs(50); | |
} | |
//test to make sure that the error message shows correctly if the record doesn't meet the entry criteria for any existing and active approval processes | |
@istest | |
static void ApprovalPreviewControllerRPemp() { | |
Map<Id, Approval_Process_Test__c> idMap = new Map<id,Approval_Process_Test__c>([SELECT Id, Name | |
FROM Approval_Process_Test__c | |
LIMIT 50]); | |
Set<id> idSet = idMap.keySet(); | |
//List<Id> ids = new List<Id>(idSet); | |
List<Approval_Process_Test__c> aptList = idMap.values(); | |
id currentUserId = userInfo.getUserId(); | |
system.debug('***startRPempTest'); | |
system.Test.startTest(); | |
Integer size = aptList.size(); | |
for(Integer i = 0; i < size; i++){ | |
ApexPages.currentPage().getParameters().put('id', aptList[i].id); | |
ApprovalPreviewController testAppPreview = new ApprovalPreviewController(); | |
testAppPreview.runPreview(); | |
string apEmpty = testAppPreview.apEmpty; | |
system.assert(apEmpty == 'This record does not meet the entry criteria for any approval processes. Please reach out to your Operations partner or a System Administrator for assistance.','***System Assert: apEmpty field should be filled in: '+apEmpty); | |
} | |
system.Test.stopTest(); | |
} | |
//test to make sure that the error message shows correctly if the record is already in an approval process | |
@istest | |
static void ApprovalPreviewControllerPI() { | |
Map<Id, Approval_Process_Test__c> idMap = new Map<id,Approval_Process_Test__c>([SELECT Id, Name | |
FROM Approval_Process_Test__c | |
LIMIT 25]); | |
Set<id> idSet = idMap.keySet(); | |
//List<Id> ids = new List<Id>(idSet); | |
List<Approval_Process_Test__c> aptList = idMap.values(); | |
id currentUserId = userInfo.getUserId(); | |
Integer size = aptList.size(); | |
for(Integer i = 0; i < size; i++){ | |
aptList[i].Record_should_enter_AP__c = TRUE; | |
} | |
update aptList; | |
system.debug('***startPIempTest'); | |
system.Test.startTest(); | |
for(Integer j = 0; j < size; j++){ | |
ApexPages.currentPage().getParameters().put('id', aptList[j].id); | |
ApprovalPreviewController testAppPreview = new ApprovalPreviewController(); | |
testAppPreview.submitForApproval(); | |
testAppPreview.runPreview(); | |
string pendingPI = testAppPreview.pendingPI; | |
system.assert(pendingPI == 'This record is already in an approval process. For more information, please return to the detail page and review the Approval History.','***System Assert: pendingPI field should be filled in: '+pendingPI); | |
} | |
system.Test.stopTest(); | |
} | |
//test to make sure that the preview works and that the submission with the correct comments works | |
@istest | |
static void ApprovalPreviewControllerSA() { | |
//errors with 10 records...but generally this code will only be called for one record | |
//at a time | |
Map<Id, Approval_Process_Test__c> idMap = new Map<id,Approval_Process_Test__c>([SELECT Id, Name | |
FROM Approval_Process_Test__c | |
LIMIT 5]); | |
Set<id> idSet = idMap.keySet(); | |
//List<Id> ids = new List<Id>(idSet); | |
List<Approval_Process_Test__c> aptList = idMap.values(); | |
id currentUserId = userInfo.getUserId(); | |
Integer size = aptList.size(); | |
for(Integer i = 0; i < size; i++){ | |
aptList[i].Record_should_enter_AP__c = TRUE; | |
} | |
update aptList; | |
system.debug('***startSATest'); | |
system.Test.startTest(); | |
for(Integer j = 0; j < size; j++){ | |
ApexPages.currentPage().getParameters().put('id', aptList[j].id); | |
ApprovalPreviewController testAppPreview = new ApprovalPreviewController(); | |
testAppPreview.runPreview(); | |
List<Object> stepsForComments = testAppPreview.stepsForComments; | |
system.assert(!stepsForComments.isEmpty(),'***System Assert: stepsForComments List should not be empty: '+stepsForComments); | |
testAppPreview.submitForApproval(); | |
String submitResult = string.valueof(testAppPreview.submitResult); | |
system.debug('***submitResult: '+submitResult); | |
system.assert(submitResult.contains('isSuccess=true'),'***System Assert: submitResult should be successful: '+submitResult); | |
} | |
system.Test.stopTest(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<apex:page showHeader="true" sidebar="true" controller="ApprovalPreviewController" title="Preview Approvals" action="{!runPreview}" lightningStylesheets="true"> | |
<p style="font-size: 18px" align="center"> | |
<b>Review List of Approvers and Submit for Approval</b> | |
</p> | |
<apex:form rendered="{!ap.processName == null}"> | |
<p style="font-size: 14px" align="center"> | |
<font color="#bb0000"> | |
<b>Error: </b> | |
</font> | |
<i> | |
{!apEmpty} | |
{!pendingPI} | |
</i> | |
</p> | |
</apex:form> | |
<apex:form rendered="{!ap.processName != null}"> | |
<apex:pageBlock > | |
<p style="font-size: 16px"> | |
<b><apex:outputLabel value="{!ap.processName}"/></b> | |
</p> | |
<p> | |
<apex:outputText value="Description: {!ap.processDescription}"/> | |
<br/> <br/> | |
</p> | |
<apex:pageBlockTable value="{!ap.approvalSteps}" var="as"> | |
<apex:column headerValue="Step Number" style="width:100px" value="{!as.stepNumber}"/> | |
<apex:column headerValue="Step Name" style="width:400px" value="{!as.stepName}"/> | |
<apex:column headerValue="Approver" value="{!as.approver}"/> | |
</apex:pageBlockTable> | |
</apex:pageBlock> | |
</apex:form> | |
<apex:form > | |
<script> | |
function submitForApprovalScr(){ | |
if ((Modal.confirm && Modal.confirm('Once you submit this record for approval, you might not be able to edit it or recall it from the approval process depending on your settings. Continue?')) || (!Modal.confirm && window.sfdcConfirm('Once you submit this record for approval, you might not be able to edit it or recall it from the approval process depending on your settings. Continue?'))) | |
{ | |
submitForApprovalFun(); | |
} | |
} | |
</script> | |
<apex:actionFunction name="submitForApprovalFun" action="{!submitForApproval}" /> | |
<div align="center"> | |
<apex:commandButton value="Submit for Approval" oncomplete="submitForApprovalScr()" rendered="{!ap.processName != null}"/> | |
<apex:commandButton value="Go Back" action="{!URLFOR($Action[theObjectType].View,ObjectId)}" reRender="two"/> | |
</div> | |
</apex:form> | |
</apex:page> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Written By: Katharine Anderson | |
Last Updated: 02-12-2019 | |
Purpose: Call these methods from test classes to create records to test against. Best practice as recommended | |
by Salesforce. Add methods as needed for records of different objects. | |
*/ | |
@isTest | |
public class TestDataFactory { | |
public static List<Approval_Process_Test__c> createAPTs (Integer numAPTs){ | |
List<Approval_Process_Test__c> aptList = new List<Approval_Process_Test__c>(); | |
For (integer i=0; i < numAPTs; i++){ | |
Approval_Process_Test__c apt = new Approval_Process_Test__c(); | |
apt.Name = 'Record'+i; | |
aptList.add(apt); | |
} | |
insert aptList; | |
system.debug('***aptList'+aptList); | |
Integer aptListSize = aptList.size(); | |
system.assert(aptListSize == numAPTs, '***System Assert: ' +aptListSize + ' records inserted, ' + numAPTs + ' records expected.'); | |
return aptList; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment