Github Link : https://github.com/AourLegacy/AourFactory/tree/QuoteReference
Salesforce CPQ (Configure, Price, Quote) is a robust tool that can automate even the most complex quoting processes. But sometimes, businesses need to extend its capabilities to cater to specific needs. In this article, we'll explore how we used Apex in tandem with Salesforce Flows to achieve a unique quoting mechanism, specifically tailored for large sets of data from a CSV.
The Challenge:
The task was multi-faceted:
1. Data Ingestion: Read a CSV containing Account Names and their respective Contact Names.
2. Quote Cloning: For each Account, clone a given Quote multiple times. The number of clones depends on the number of Quote lines divided by a batch size (e.g., 200).Or duplacte the original quote.
3. Quote Line Cloning: Each cloned Quote would then have a subset of the original Quote lines.
4. Dynamic Behavior: The logic must be flexible based on a `Choice` field.
The Solution:
1. Data Ingestion using Flows: We utilized Salesforce Flows to read the CSV file. This allowed us to map the CSV columns to Salesforce fields seamlessly. The Flow would then send this data, along with the `Choice` field and a `recordId` (representing a Quote Id), to an Apex class for further processing.
2. Apex Class for Processing: We created an Apex class named `QuoteCloner` to handle the bulk of the logic.
Here's a breakdown:
- Data Collection: The method `cloneQuotesByBatch` accepts the original Quote Id, a map of Account Ids to Contact Ids, and the `Choice` string.
- Quote & Quote Line Retrieval: The original Quote and its Quote lines are fetched from Salesforce.
- Batch Calculation: The number of batches (or cloned Quotes) is determined by dividing the number of Quote lines by our batch size. - Switch Logic: Based on the `Choice` field's value, different logic can be executed, offering dynamic behavior.
- Quote & Quote Line Cloning: For each Account-Contact pair, the original Quote is cloned as per the batch calculation. Each cloned Quote then gets a subset of the original Quote lines.
public class QuoteReference {
@InvocableMethod(callout=true label='QuoteReference')
public static void QuoteRefenerenceMain(List<FlowInput> inputs ) {
List<String> accountNames = new List<String>();
List<String> ContactNames = new List<String>();
Map<String, String> nameToContactNameMap = new Map<String, String>();
List<Account> ids = new List<Account>(inputs[0].ids) ;
Id recordId = inputs[0].recordId;
String Choice = inputs[0].Choice;
for(Account acc : ids) { // assuming you're only interested in the first inner list
accountNames.add(acc.Name);
ContactNames.add(acc.ContactName__c);
nameToContactNameMap.put(acc.Name, acc.ContactName__c);
}
Map<String, Id> nameToAccountIdMap = new Map<String, Id>();
// Fetch the account Ids based on the account names
for(Account acc : [SELECT Id, Name FROM Account WHERE Name IN :accountNames]) {
nameToAccountIdMap.put(acc.Name, acc.Id);
}
Map<String, Id> nameToContactIdMap = new Map<String, Id>();
// Fetch the account Ids based on the account names
for(Contact cont : [SELECT Id, Name FROM Contact WHERE Name IN :ContactNames]) {
nameToContactIdMap.put(cont.Name, cont.Id);
}
Map<Id, Id> accountIdToContactIdMap = new Map<Id, Id>();
for(String accountName : nameToContactNameMap.keySet()) {
Id accountId = nameToAccountIdMap.get(accountName);
String contactName = nameToContactNameMap.get(accountName);
Id contactId = nameToContactIdMap.get(contactName);
if(accountId != null && contactId != null) {
accountIdToContactIdMap.put(accountId, contactId);
}
}
switch on Choice {
when 'Dupliquer' {
cloneQuoteWithAccountsAndContacts(recordId,accountIdToContactIdMap);
}
when 'Diviser' {
cloneQuotesByBatch(recordId,accountIdToContactIdMap);
}
}
}
public static void cloneQuoteWithAccountsAndContacts(Id quoteId, Map<Id, Id> accountIdToContactIdMap) {
// Retrieve the quote and its quote lines
SBQQ__Quote__c originalQuote = [SELECT Id, BillingAdresse__c, BundleDiscountAmount__c, BundleDiscountPercentage__c, DiscountAmount__c, Discount1__c, DifficultytoMaintainCustomer__c, DeltaDiscount__c, CreatedDate, CreatedById, IsDeleted, NBbundle__c, NetAfterTva__c, NombreDevisiteur__c, NombreUtilisateur__c, Objectif__c, Optional__c, OwnerId, PartnerLevel__c, Pricehold__c, PricingMethods__c, PrixNegocie__c, Process__c, SBQQ__AdditionalDiscountAmount__c, SBQQ__Account__c, Promotion__c, SBQQ__AverageCustomerDiscount__c, SBQQ__AveragePartnerDiscount__c, SBQQ__Distributor__c, SBQQ__DeliveryMethod__c, SBQQ__DefaultTemplate__c, SBQQ__DaysQuoteOpen__c, SBQQ__CustomerDiscount__c, SBQQ__CustomerAmount__c, SBQQ__ContractingMethod__c, SBQQ__ConsumptionRateOverride__c, SBQQ__DistributorDiscount__c, SBQQ__EndDate__c, SBQQ__ExpirationDate__c, SBQQ__FirstSegmentTermEndDate__c, SBQQ__MarkupRate__c, SBQQ__ListAmount__c, SBQQ__LineItemsPrinted__c, SBQQ__LineItemsGrouped__c, SBQQ__LineItemCount__c, SBQQ__MasterEvergreenContract__c, SBQQ__NetAmount__c, SBQQ__Notes__c, SBQQ__Opportunity2__c, SBQQ__OrderBy__c, SBQQ__OrderByQuoteLineGroup__c, SBQQ__Ordered__c, SBQQ__Primary__c, SBQQ__PricebookId__c, SBQQ__PriceBook__c, SBQQ__PaymentTerms__c, SBQQ__PartnerDiscount__c, SBQQ__Partner__c, SBQQ__OriginalQuote__c, SBQQ__PrimaryContact__c, SBQQ__RegularAmount__c, SBQQ__RenewalTerm__c, SBQQ__StartDate__c, SBQQ__Source__c , SBQQ__SalesRep__c, SBQQ__Status__c, SBQQ__SubscriptionTerm__c, SBQQ__Type__c, SBQQ__Uncalculated__c FROM SBQQ__Quote__c WHERE Id = :quoteId LIMIT 1];
List<SBQQ__QuoteLine__c> originalQuoteLines = [SELECT SBQQ__MaximumPrice__c, SBQQ__Bundle__c, SBQQ__Bundled__c, SBQQ__BundledQuantity__c, SBQQ__AdditionalDiscountAmount__c, SBQQ__AdditionalDiscount__c, SBQQ__AdditionalQuantity__c, SBQQ__Product__c, SBQQ__Quote__c, SBQQ__NetPrice__c, SBQQ__ListPrice__c, SBQQ__ListTotal__c, SBQQ__OriginalPrice__c FROM SBQQ__QuoteLine__c WHERE SBQQ__Quote__c = :quoteId];
List<SBQQ__Quote__c> clonedQuotes = new List<SBQQ__Quote__c>();
List<SBQQ__QuoteLine__c> clonedQuoteLines = new List<SBQQ__QuoteLine__c>();
for(Id accountId : accountIdToContactIdMap.keySet()) {
Id contactId = accountIdToContactIdMap.get(accountId);
// Clone the quote and link to the account and contact
SBQQ__Quote__c clonedQuote = originalQuote.clone(false, true,false,false);
clonedQuote.SBQQ__Account__c = accountId;
clonedQuote.SBQQ__PrimaryContact__c = contactId;
clonedQuote.SBQQ__Primary__c = false;
clonedQuotes.add(clonedQuote);
}
for(SBQQ__Quote__c q : clonedQuotes){
System.debug('@@@@'+q);
}
// Insert the cloned quotes
insert clonedQuotes;
for(SBQQ__Quote__c newQuote : clonedQuotes) {
for(SBQQ__QuoteLine__c originalQuoteLine : originalQuoteLines) {
SBQQ__QuoteLine__c clonedQuoteLine = originalQuoteLine.clone(false, true);
clonedQuoteLine.SBQQ__Quote__c = newQuote.Id;
clonedQuoteLines.add(clonedQuoteLine);
}
}
// Insert the cloned quote lines
if(clonedQuoteLines!= null && !clonedQuoteLines.isEmpty()) Insert clonedQuoteLines;
Set<Id> newIds = new Set<Id>();
for(SBQQ__QuoteLine__c newQli : clonedQuoteLines) {
newIds.add(newQli.Id);
}
List<SBQQ__QuoteLine__c> clonedQlis = [Select Id, SBQQ__Source__c, SBQQ__Source__r.SBQQ__RequiredBy__c , SBQQ__RequiredBy__c FROM SBQQ__QuoteLine__c WHERE Id In :newIds];
Map<Id, Id> newLineToRequiredBySrcMap = new Map<Id, Id>();
Map<Id, Id> srcReqMap = new Map<Id, Id>();
for(SBQQ__QuoteLine__c newQli : clonedQlis) {
newLineToRequiredBySrcMap.put(newQli.Id, newQli.SBQQ__Source__r.SBQQ__RequiredBy__c);
srcReqMap.put(newQli.SBQQ__Source__c, newQli.Id);
}
for(SBQQ__QuoteLine__c newQli : clonedQlis) {
newQli.SBQQ__RequiredBy__c = srcReqMap.get(newLineToRequiredBySrcMap.get(newQli.Id));
}
if(clonedQlis!= null && !clonedQlis.isEmpty()) update clonedQlis;
}
public static void cloneQuotesByBatch(Id originalQuoteId, Map<Id, Id> accountIdToContactIdMap) {
// Constants
Integer batchSize = 200;
// Retrieve the original quote and its quote lines
SBQQ__Quote__c originalQuote = [SELECT Id, BillingAdresse__c, BundleDiscountAmount__c, BundleDiscountPercentage__c, DiscountAmount__c, Discount1__c, DifficultytoMaintainCustomer__c, DeltaDiscount__c, CreatedDate, CreatedById, IsDeleted, NBbundle__c, NetAfterTva__c, NombreDevisiteur__c, NombreUtilisateur__c, Objectif__c, Optional__c, OwnerId, PartnerLevel__c, Pricehold__c, PricingMethods__c, PrixNegocie__c, Process__c, SBQQ__AdditionalDiscountAmount__c, SBQQ__Account__c, Promotion__c, SBQQ__AverageCustomerDiscount__c, SBQQ__AveragePartnerDiscount__c, SBQQ__Distributor__c, SBQQ__DeliveryMethod__c, SBQQ__DefaultTemplate__c, SBQQ__DaysQuoteOpen__c, SBQQ__CustomerDiscount__c, SBQQ__CustomerAmount__c, SBQQ__ContractingMethod__c, SBQQ__ConsumptionRateOverride__c, SBQQ__DistributorDiscount__c, SBQQ__EndDate__c, SBQQ__ExpirationDate__c, SBQQ__FirstSegmentTermEndDate__c, SBQQ__MarkupRate__c, SBQQ__ListAmount__c, SBQQ__LineItemsPrinted__c, SBQQ__LineItemsGrouped__c, SBQQ__LineItemCount__c, SBQQ__MasterEvergreenContract__c, SBQQ__NetAmount__c, SBQQ__Notes__c, SBQQ__Opportunity2__c, SBQQ__OrderBy__c, SBQQ__OrderByQuoteLineGroup__c, SBQQ__Ordered__c, SBQQ__Primary__c, SBQQ__PricebookId__c, SBQQ__PriceBook__c, SBQQ__PaymentTerms__c, SBQQ__PartnerDiscount__c, SBQQ__Partner__c, SBQQ__OriginalQuote__c, SBQQ__PrimaryContact__c, SBQQ__RegularAmount__c, SBQQ__RenewalTerm__c, SBQQ__StartDate__c, SBQQ__Source__c , SBQQ__SalesRep__c, SBQQ__Status__c, SBQQ__SubscriptionTerm__c, SBQQ__Type__c, SBQQ__Uncalculated__c FROM SBQQ__Quote__c WHERE Id = :originalQuoteId LIMIT 1];
List<SBQQ__QuoteLine__c> originalQuoteLines = [SELECT SBQQ__MaximumPrice__c, SBQQ__Bundle__c, SBQQ__Bundled__c, SBQQ__BundledQuantity__c, SBQQ__AdditionalDiscountAmount__c, SBQQ__AdditionalDiscount__c, SBQQ__AdditionalQuantity__c, SBQQ__Product__c, SBQQ__Quote__c, SBQQ__NetPrice__c, SBQQ__ListPrice__c, SBQQ__ListTotal__c, SBQQ__OriginalPrice__c FROM SBQQ__QuoteLine__c WHERE SBQQ__Quote__c = :originalQuoteId];
// Calculate number of batches
Integer numberOfBatches = (Integer) Math.ceil(originalQuoteLines.size() / (Decimal) batchSize);
// Loop through each Account and Contact pair in the map
for(Id accountId : accountIdToContactIdMap.keySet()) {
Id contactId = accountIdToContactIdMap.get(accountId);
// Clone the Quote for the number of batches
for(Integer i = 0; i < numberOfBatches; i++) {
SBQQ__Quote__c clonedQuote = originalQuote.clone(false, true);
clonedQuote.SBQQ__Account__c = accountId;
clonedQuote.SBQQ__PrimaryContact__c = contactId;
// Insert the cloned Quote
insert clonedQuote;
// Determine start and end indexes for the slice of Quote lines
Integer startIndex = i * batchSize;
Integer endIndex = Math.min(startIndex + batchSize, originalQuoteLines.size());
List<SBQQ__QuoteLine__c> batchedQuoteLines = new List<SBQQ__QuoteLine__c>();
for(Integer j = startIndex; j < endIndex; j++) {
SBQQ__QuoteLine__c clonedQuoteLine = originalQuoteLines[j].clone(false, true);
clonedQuoteLine.SBQQ__Quote__c = clonedQuote.Id;
batchedQuoteLines.add(clonedQuoteLine);
}
// Insert the cloned Quote lines for this batch
insert batchedQuoteLines;
Set<Id> newIds = new Set<Id>();
for(SBQQ__QuoteLine__c newQli : batchedQuoteLines) {
newIds.add(newQli.Id);
}
List<SBQQ__QuoteLine__c> clonedQlis = [Select Id, SBQQ__Source__c, SBQQ__Source__r.SBQQ__RequiredBy__c , SBQQ__RequiredBy__c FROM SBQQ__QuoteLine__c WHERE Id In :newIds];
Map<Id, Id> newLineToRequiredBySrcMap = new Map<Id, Id>();
Map<Id, Id> srcReqMap = new Map<Id, Id>();
for(SBQQ__QuoteLine__c newQli : clonedQlis) {
newLineToRequiredBySrcMap.put(newQli.Id, newQli.SBQQ__Source__r.SBQQ__RequiredBy__c);
srcReqMap.put(newQli.SBQQ__Source__c, newQli.Id);
}
for(SBQQ__QuoteLine__c newQli : clonedQlis) {
newQli.SBQQ__RequiredBy__c = srcReqMap.get(newLineToRequiredBySrcMap.get(newQli.Id));
}
if(clonedQlis!= null && !clonedQlis.isEmpty()) update clonedQlis;
}
}
}
public class FlowInput {
@InvocableVariable(label='Record ID' required=true)
public Id recordId;
@InvocableVariable(label='dataDoct' required=true)
public List<Account> ids;
@InvocableVariable(label='Choice Value' required=true)
public String Choice ;
}
}
3. Connecting Apex and Flows:
The key to this solution is the interplay between Apex and Flows. The Flow acts as the interface, ingesting the CSV and passing the necessary data to Apex. The Apex class, with its robust processing capabilities, then acts on this data, creating the required records in Salesforce.
To connect the Flow and Apex, we used the `@InvocableMethod` annotation. This makes our Apex method accessible from the Flow, allowing for a seamless integration.
Conclusion:
Salesforce CPQ is powerful, but every now and then, businesses might find the need to extend its capabilities. With the combination of Flows and Apex, we can tailor the CPQ process to fit even the most unique requirements. In our case, we managed to automate the cloning of Quotes and Quote lines based on data from a CSV, all while offering dynamic behavior through the `Choice` field.
Remember, while this solution works for our specific scenario, always consider your organization's needs and test thoroughly in a sandbox or developer environment before deploying to production.
Comments