CPQ Standard Approvals – VisualForce Email Template Examples!

I’ve had these email templates for a while now and use them as starting points for any CPQ implementation where approvals are required, which is most implementations! These are provided as is and can be modified to suit your needs! There are 4 actual email templates (Needs Approval, Approved, Rejected, and Recalled). Here’s the list of things to support these templates!

  1. Apex Controller Class
  2. VisualForce Components
    • The comments block
    • The approval directions block
    • The quote header block
    • The quote lines block
  3. VisualForce Email Templates
    • Needs Approval
    • Approved
    • Rejected
    • Recalled
  4. Apex Controller Test Class

APEX Controller Class

Here’s the controller! It’s very simple! Get quote, get quote lines, get comments. That’s it.

public without sharing class QuoteApprovalsCmpController {
    public String quoteId;
    public SBQQ__Quote__c quote;
    public List<SBQQ__QuoteLine__c> quoteLines;
    
    public void setQuoteId(String Id) {
        quoteId = Id;
    }
    
    public String getQuoteId() {
        return quoteId;
    }
    
    public void setQuote(SBQQ__Quote__c q) {
        quote = q;
    }
    
    public SBQQ__Quote__c getQuote() {
        String soql = ''
            + ' select ' + String.join(new List<String>(SBQQ__Quote__c.SObjectType.getDescribe().fields.getMap().keySet()), ',')
            + ', SBQQ__SalesRep__r.Name, SBQQ__Account__r.Name, SBQQ__Opportunity2__r.Name from SBQQ__Quote__c'
            + ' where Id=\''+quoteId+'\' limit 1';
        quote = Database.query(soql);
        return quote;
    }
    
    public void setQuoteLines(List<SBQQ__QuoteLine__c> lines) {
        quoteLines = lines;
    }
    
    public List<SBQQ__QuoteLine__c> getQuoteLines() {
        String soql = ''
            + ' select ' + String.join(new List<String>(SBQQ__QuoteLine__c.SObjectType.getDescribe().fields.getMap().keySet()), ',')
            + ' from SBQQ__QuoteLine__c'
            + ' where SBQQ__Quote__c=\''+quoteId+'\' limit 200';
        quoteLines = Database.query(soql);
        return quoteLines;
    }
    
    public ProcessInstanceStep getCurrentProcessInstanceStep() {
        List<ProcessInstanceStep> steps = [SELECT ProcessInstance.CompletedDate, ActorId, Comments, CreatedById, CreatedDate, ElapsedTimeInDays, ElapsedTimeInHours, ElapsedTimeInMinutes, Id, OriginalActorId, ProcessInstanceId, StepNodeId, StepStatus, SystemModstamp
                                           FROM ProcessInstanceStep
                                           WHERE ProcessInstance.TargetObjectId = :quoteId AND ProcessInstance.CompletedDate = null
                                           ORDER BY SystemModstamp DESC];
        if (steps.size() > 0) return steps[0];
        else return null;
    }
}

VF Component – Comments

This block will display only if there are approval comments. It’s nice to have the comments thread in any email that is sent so that the person reading it has some context for what is going on with the given quote.

<apex:component controller="QuoteApprovalsCmpController" access="global">
	<apex:attribute name="cmpQuoteId" type="String" description="ID of the Quote" required="required" assignTo="{!quoteId}"/>
	<style>
		span {
			font-weight: bold;
		}
	</style>

	<apex:outputPanel rendered="{!if(CurrentProcessInstanceStep.Comments != null, true, false)}">
	<p><span>Comments:</span></p>
	<p>{!CurrentProcessInstanceStep.Comments}</p>
	</apex:outputPanel>
</apex:component>

VF Component – Approval Directions

This is a section where we say how to approve. There are a few different ways but you might want to change how this is worded or change it to include another way or exclude one that I’ve included.

<apex:component controller="QuoteApprovalsCmpController" access="global">
	<apex:attribute name="cmpQuoteId" type="String" description="ID of the Quote" required="required" assignTo="{!quoteId}"/>
	<style>
		span {
			font-weight: bold;
		}
	</style>

	<p>
		<span>Click the link below to approve or reject:</span>
		<a href="{!LEFT($Api.Partner_Server_URL_290,FIND('services',$Api.Partner_Server_URL_290)-1)}{!'lightning/r/ProcessInstanceHistory/' + cmpQuoteId + '/related/ProcessSteps/view'}">Approve or Reject</a>
	</p>
	
	<p>To Approve by email, simply reply to this email with "Approve" or "Reject" as the response.</p>
</apex:component>

VF Component – Quote Header

This is the bun of the quote! We include various fields from the quote record to give some detail on what someone is approving. This is where you might want to add more quote fields or remove some I’ve included.

<apex:component controller="QuoteApprovalsCmpController" access="global">
	<apex:attribute name="cmpQuoteId" type="String" description="ID of the Quote" required="required" assignTo="{!quoteId}"/>
	<table cellpadding="5" style="border-collapse: collapse" width="100%">
		<tr>
			<td align="right" style="border-right-style: solid; border-right-width: thick; border-right-color: #6EC0EE;" width="20%"><i>Quote</i></td>
			<td width="80%"><a href="{!LEFT($Api.Partner_Server_URL_140,FIND('.com',$Api.Partner_Server_URL_140)+4)+quote.Id}"><b>{!quote.Name}</b></a></td>
		</tr>
		<tr>
			<td align="right" style="border-right-style: solid; border-right-width: thick; border-right-color: #6EC0EE;" width="20%"><i>Opportunity</i></td>
			<td width="80%"><a href="{!LEFT($Api.Partner_Server_URL_140,FIND('.com',$Api.Partner_Server_URL_140)+4)+quote.SBQQ__Opportunity2__c}"><b>{!quote.SBQQ__Opportunity2__r.Name}</b></a></td>
		</tr>
		<tr>
			<td align="right" style="border-right-style: solid; border-right-width: thick; border-right-color: #6EC0EE;" width="20%"><i>Sales Rep</i></td>
			<td width="80%"><b>{!quote.SBQQ__SalesRep__r.Name}</b></td>
		</tr>
		<tr>
			<td align="right" style="border-right-style: solid; border-right-width: thick; border-right-color: #6EC0EE;" width="20%"><i>Account</i></td>
			<td width="80%"><b>{!quote.SBQQ__Account__r.Name}</b></td>
		</tr>
		<tr>
			<td align="right" style="border-right-style: solid; border-right-width: thick; border-right-color: #6EC0EE;" width="20%"><i>Amount</i></td>
			<td width="80%"><b><apex:outputText value="{0, Number, Currency}"><apex:param value="{!quote.SBQQ__ListAmount__c}" /></apex:outputText></b></td>
		</tr>
		<tr>
			<td align="right" style="border-right-style: solid; border-right-width: thick; border-right-color: #6EC0EE;" width="20%"><i>Status</i></td>
			<td width="80%"><b>{!quote.SBQQ__Status__c}</b></td>
		</tr>
	</table>
	<br />
</apex:component>

VF Component – Quote Lines

This is similar to the quote header but it’s the lines! Feel free to add and/or remove fields from this section.

<apex:component controller="QuoteApprovalsCmpController" access="global">
	<apex:attribute name="cmpQuoteId" type="String" description="ID of the Quote" required="required" assignTo="{!quoteId}"/>
	<table cellpadding="5" style="border-collapse: collapse" width="100%">
		<tr>
			<td style="background-color: #6EC0EE; color: #FFFFFF;"><b>Product Name</b></td>
			<td style="background-color: #6EC0EE; color: #FFFFFF; text-align:right;"><b>Unit Price</b></td>
			<td style="background-color: #6EC0EE; color: #FFFFFF; text-align:center;"><b>Quantity</b></td>
			<td style="background-color: #6EC0EE; color: #FFFFFF; text-align:right;"><b>Total Price</b></td>
		</tr> 
		<apex:repeat value="{!quoteLines}" var="lineItem">
		<tr>
			<td>{!lineItem.SBQQ__ProductName__c}</td>
			<td style="text-align:right;"><apex:outputText value="{0, Number, Currency}"><apex:param value="{!lineItem.SBQQ__ListPrice__c}" /></apex:outputText></td>
			<td style="text-align:center;"><apex:outputText value="{0, number, ###,###,###,###}"><apex:param value="{!lineItem.SBQQ__Quantity__c}"/></apex:outputText></td>
			<td style="text-align:right;"><apex:outputText value="{0, Number, Currency}"><apex:param value="{!lineItem.SBQQ__NetTotal__c}" /></apex:outputText></td>
		</tr>
		</apex:repeat>
	</table>
</apex:component>

VF Email Template – Needs Approval

This quote needs approval! You can see where we’re using the above components. This is the place to add any template specific information or wording. Below this one are the other three templates. You also don’t have to use all of these. Sometimes it doesn’t make sense to send a recalled email.

<messaging:emailTemplate subject="Quote {!relatedTo.Name} - Needs Approval" recipientType="User" relatedToType="SBQQ__Quote__c">
	<messaging:htmlEmailBody>
		<body style="font-family: 'Arial'">
			<p>Quote {!relatedTo.Name} - <b>Needs Approval</b></p>
				<hr style="border: solid thin #6EC0EE" />
				<br />
				<c:QuoteApprovalsCommentsCmp cmpQuoteId="{!relatedTo.Id}" />
				<c:QuoteApprovalsCmp cmpQuoteId="{!relatedTo.Id}" />
				<c:QuoteApprovalsHeaderCmp cmpQuoteId="{!relatedTo.Id}" />
				<c:QuoteApprovalsLinesCmp cmpQuoteId="{!relatedTo.Id}" />
			<p />
		</body>
	</messaging:htmlEmailBody>
</messaging:emailTemplate>

VF Email Template – Approved

Notice in the approved template, we don’t include the directions or the comments. If you’d like to have comments in any other template, just add the component line for that section from the needs approval template above to the approved template below.

<messaging:emailTemplate subject="Quote {!relatedTo.Name} - Approved" recipientType="User" relatedToType="SBQQ__Quote__c">
    <messaging:htmlEmailBody >
        <body style="font-family: 'Arial'">
            <p style="color:green;">Quote {!relatedTo.Name} has been <b>Approved</b></p>
            <hr style="border: solid thin #6EC0EE" />
            <br />
			<c:QuoteApprovalsHeaderCmp cmpQuoteId="{!relatedTo.Id}" />
			<c:QuoteApprovalsLinesCmp cmpQuoteId="{!relatedTo.Id}" />
        </body>
    </messaging:htmlEmailBody>
</messaging:emailTemplate>

VF Email Template – Rejected

<messaging:emailTemplate subject="Quote {!relatedTo.Name} - Rejected" recipientType="User" relatedToType="SBQQ__Quote__c">
    <messaging:htmlEmailBody >
        <body style="font-family: 'Arial'">
            <p style="color:red;">Quote {!relatedTo.Name} has been <b>Rejected</b></p>
            <hr style="border: solid thin #6EC0EE" />
            <br />
			<c:QuoteApprovalsCommentsCmp cmpQuoteId="{!relatedTo.Id}" />
			<c:QuoteApprovalsHeaderCmp cmpQuoteId="{!relatedTo.Id}" />
			<c:QuoteApprovalsLinesCmp cmpQuoteId="{!relatedTo.Id}" />
        </body>
    </messaging:htmlEmailBody>
</messaging:emailTemplate>

VF Email Template – Recalled

<messaging:emailTemplate subject="Quote {!relatedTo.Name} - Recalled" recipientType="User" relatedToType="SBQQ__Quote__c">
    <messaging:htmlEmailBody >
        <body style="font-family: 'Arial'">
            <p style="color:orange;">Quote {!relatedTo.Name} has been <b>Recalled</b></p>
            <hr style="border: solid thin #6EC0EE" />
            <br />
			<c:QuoteApprovalsHeaderCmp cmpQuoteId="{!relatedTo.Id}" />
			<c:QuoteApprovalsLinesCmp cmpQuoteId="{!relatedTo.Id}" />
        </body>
    </messaging:htmlEmailBody>
</messaging:emailTemplate>

APEX Controller Test Class

Of course you need a test class. Keep in mind that if you change anything in the controller, you’re gonna need to update this test class to keep your code coverage up to snuff.

@isTest
private class QuoteApprovalsCmpController_Test {
    @isTest
    static void test_getCurrentProcessInstanceStep() {
        SBQQ__Quote__c quote = new SBQQ__Quote__c();
        insert quote;
        
        List<SBQQ__QuoteLine__c> qls = new List<SBQQ__QuoteLine__c>{new SBQQ__QuoteLine__c(SBQQ__Quote__c=quote.id)};
        
        QuoteApprovalsCmpController cmpCtlr = new QuoteApprovalsCmpController();
        cmpCtlr.setQuoteId(quote.Id);
        cmpCtlr.setQuote(quote);
        cmpCtlr.setQuoteLines(qls);
        qls = cmpCtlr.getQuoteLines();
        quote = cmpCtlr.getQuote();
        cmpCtlr.getQuoteId();
        cmpCtlr.getCurrentProcessInstanceStep();
    }
}

Installer!

Yep. Same as always. Below are the installers!

Sandbox | Production

2 thoughts on “CPQ Standard Approvals – VisualForce Email Template Examples!

  1. Hi Dennis, when I install this package getting below error

    This package can’t be installed.
    There are problems that prevent this package from being installed.
    Custom Field Definitions(00N5e00000g10Oe) Duplicate RelationshipName
    The relationship name “Quotes__r” is already used by custom field SBQQ__Quote__c.SubmittedUser__c. Please rename existing relationship name.

  2. Dennis, this walkthrough was fantastic, thank you! For non-developers like myself, this was exactly what was needed. I was unable to install the package because of a similar error as Gaurang, but I manually cut/paste the code and it’s working at 99%.

    I am only running into a small issue I hope you can help me with. When submitted approvals are Approved or Rejected, the emails are not showing the Comments made by the approver. The Needs Approval email that is initially sent does include the submitters Comments perfectly fine. But when the manager accepts or rejects (via SF record or email reply), the Approve/Reject emails never show the comments. The only thing I could think is that a comment from a reply is a different field than {!CurrentProcessInstanceStep.Comments}??

    Any help would be greatly appreciated, TIA.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.