Skip to content

Instantly share code, notes, and snippets.

@AndersonKatharineTNC
Last active October 11, 2021 20:54
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AndersonKatharineTNC/6b04db1f68540c729b9fbd15ac92ec13 to your computer and use it in GitHub Desktop.
Save AndersonKatharineTNC/6b04db1f68540c729b9fbd15ac92ec13 to your computer and use it in GitHub Desktop.
New Submit for Approval Button (Salesforce)
/*
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 ;
}
}
/*
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();
}
}
<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/>&nbsp;<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>
/*
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