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!
- Apex Controller Class
- VisualForce Components
- The comments block
- The approval directions block
- The quote header block
- The quote lines block
- VisualForce Email Templates
- Needs Approval
- Approved
- Rejected
- Recalled
- 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!
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.
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.