Github Link : https://github.com/AourLegacy/AourFactory/tree/Cloning
Salesforce, a leading CRM platform, is equipped with myriad features that enable streamlined data management. Among these features is the capability to clone records, a function crucial for preserving data integrity and boosting workflow efficiency. This article explores two distinct use cases of cloning within Salesforce, employing both Flows and Apex code to achieve precise data replication.
In this use case, the objective is to clone a quote along with its associated quote lines using a combination of a Flow and Apex code.
The process commences with a Flow that clones the quote then triggers the Apex code designed to clone the quote lines. The Flow acts as the orchestrator, managing the sequence of operations, while the Apex code performs the actual cloning.
Code Snippet :
public with sharing class QLIsCustomClone {
/**
* Clone related Quote lines with Quantity != 0 and Family != 'Travaux'
* @param : new Quote sent by the flow FL_QUO_CloneQuoteForBPU
*/
@InvocableMethod(label='clone filtered QLIs' description='clone QuoteLines with the right requireBy')
public static void customCloneQlis(List<SBQQ__Quote__c> newQuotes) {
System.debug('SLA asyncCustomCloneQlis Start');
if(newQuotes != null && !newQuotes.isEmpty()) {
String newQuoteId = newQuotes.get(0).Id;
String srcQuoteId = newQuotes.get(0).SBQQ__Source__c;
System.debug('SLA newQuotes: '+newQuotes);
SBQQ.TriggerControl.disable();
try {
List<SBQQ__QuoteLine__c> newQLIs = new List<SBQQ__QuoteLine__c>();
//Clone the quote lines
List<SBQQ__QuoteLine__c> oldQLis = [SELECT Id, SBQQ__Quote__c, SBQQ__BillingFrequency__c, SBQQ__BillingType__c, SBQQ__BlockPrice__c, SBQQ__Bundle__c, SBQQ__BundledQuantity__c, SBQQ__Bundled__c, SBQQ__CarryoverLine__c, SBQQ__ChargeType__c, SBQQ__ComponentCost__c, SBQQ__ComponentDiscountedByPackage__c, SBQQ__ComponentListTotal__c, SBQQ__ComponentSubscriptionScope__c, SBQQ__ComponentTotal__c, SBQQ__ComponentUpliftedByPackage__c, SBQQ__CompoundDiscountRate__c, SBQQ__ConfigurationRequired__c, SBQQ__ContractedPrice__c, SBQQ__CostEditable__c, SBQQ__Cost__c, SBQQ__CustomerPrice__c, SBQQ__Description__c, SBQQ__Dimension__c, SBQQ__DiscountScheduleType__c, SBQQ__DiscountSchedule__c, SBQQ__DiscountTier__c, SBQQ__Discount__c, SBQQ__DistributorDiscount__c, SBQQ__DynamicOptionId__c, SBQQ__EarliestValidAmendmentStartDate__c, SBQQ__EndDate__c, SBQQ__Existing__c, SBQQ__Favorite__c, SBQQ__GenerateContractedPrice__c, SBQQ__GrossProfit__c, SBQQ__Group__c, SBQQ__Guidance__c, SBQQ__HasConsumptionSchedule__c, SBQQ__Hidden__c, SBQQ__Incomplete__c, SBQQ__ListPrice__c, SBQQ__MarkupAmount__c, SBQQ__MarkupRate__c, SBQQ__MaximumPrice__c, SBQQ__MinimumPrice__c, SBQQ__NetPrice__c, SBQQ__NonDiscountable__c, SBQQ__NonPartnerDiscountable__c, SBQQ__Number__c, SBQQ__OptionDiscountAmount__c, SBQQ__OptionDiscount__c, SBQQ__OptionLevel__c, SBQQ__OptionType__c, SBQQ__Optional__c, SBQQ__OriginalPrice__c, SBQQ__OriginalQuoteLineId__c, SBQQ__OriginalUnitCost__c, SBQQ__PackageProductCode__c, SBQQ__PackageProductDescription__c, SBQQ__PartnerDiscount__c, SBQQ__PartnerPrice__c, SBQQ__PreviousSegmentPrice__c, SBQQ__PreviousSegmentUplift__c, SBQQ__PriceEditable__c, SBQQ__PricebookEntryId__c, SBQQ__PricingMethodEditable__c, SBQQ__PricingMethod__c, SBQQ__PriorQuantity__c, SBQQ__ProductOption__c, SBQQ__Product__c, SBQQ__ProrateMultiplier__c, SBQQ__ProratedListPrice__c, SBQQ__ProratedPrice__c, SBQQ__Quantity__c, SBQQ__RegularPrice__c, SBQQ__Renewal__c, SBQQ__RenewedAsset__c, SBQQ__RenewedSubscription__c, SBQQ__RequiredBy__c, SBQQ__SegmentIndex__c, SBQQ__SegmentKey__c, SBQQ__SegmentLabel__c, SBQQ__SpecialPriceDescription__c, SBQQ__SpecialPriceType__c, SBQQ__SpecialPrice__c, SBQQ__StartDate__c, SBQQ__SubscribedAssetIds__c, SBQQ__SubscriptionBase__c, SBQQ__SubscriptionCategory__c, SBQQ__SubscriptionPercent__c, SBQQ__SubscriptionPricing__c, SBQQ__SubscriptionScope__c, SBQQ__SubscriptionTargetPrice__c, SBQQ__SubscriptionTerm__c, SBQQ__TaxCode__c, SBQQ__Taxable__c, SBQQ__TermDiscountSchedule__c, SBQQ__TermDiscountTier__c, SBQQ__TermDiscount__c, SBQQ__UnitCost__c, SBQQ__UnproratedNetPrice__c, SBQQ__UpgradedAsset__c, SBQQ__UpgradedQuantity__c, SBQQ__UpgradedSubscription__c, SBQQ__UpliftAmount__c, SBQQ__Uplift__c, SBQQ__VolumeDiscount__c, SBQQ__AdditionalDiscount__c, SBQQ__ComponentVisibility__c, SBQQ__CustomerTotal__c, SBQQ__EffectiveEndDate__c, SBQQ__EffectiveQuantity__c, SBQQ__EffectiveStartDate__c, SBQQ__EffectiveSubscriptionTerm__c, SBQQ__ListTotal__c, SBQQ__Markup__c, SBQQ__NetTotal__c, SBQQ__PackageCost__c, SBQQ__PackageListTotal__c, SBQQ__PackageTotal__c, SBQQ__PartnerTotal__c, SBQQ__ProductCode__c, SBQQ__ProductFamily__c, SBQQ__ProductName__c, SBQQ__RegularTotal__c, SBQQ__TotalDiscountAmount__c FROM SBQQ__QuoteLine__c WHERE SBQQ__Quote__c=:srcQuoteId];
for(SBQQ__QuoteLine__c qli : oldQLis){
SBQQ__QuoteLine__c nQli = qli.clone(false, false, false, false);
nQli.SBQQ__RequiredBy__c = null;
nQli.SBQQ__Quote__c = newQuoteId;
nQli.SBQQ__Source__c = qli.Id;
newQLIs.add(nQli);
}
if(newQLIs!= null && !newQLIs.isEmpty()) Insert newQLIs;
Set<Id> newIds = new Set<Id>();
for(SBQQ__QuoteLine__c newQli : newQLIs) {
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;
System.debug('SLA newLineToRequiredBySrcMap: '+newLineToRequiredBySrcMap);
System.debug('SLA srcReqMap: '+srcReqMap);
} finally {
SBQQ.TriggerControl.enable();
}
}
System.debug('SLA asyncCustomCloneQlis End');
}
}
Upon execution, the new cloned Quote Lines are seamlessly attached to the new cloned Quote, illustrating the power of combining Flows with Apex code to manage complex data operations.
Use Case 2: Generalized Cloning of Any Object using its ID
This use case broadens the cloning functionality to encompass any object within Salesforce using its unique ID. A callable Apex code is designed to be triggered from a Flow, enabling the cloning of any object based on its ID. The process utilizes custom metadata for mapping each object to its children, ensuring accurate cloning.
public without sharing class CloneObjectWithRelated {
public class CloneObjectWithRelatedException extends Exception {
}
public static String throwExceptionValue = '';
public static Map<String, Map<String, Schema.SObjectField>> describeObjects = new Map<String, Map<String, Schema.SObjectField>>();
@InvocableMethod(label='Invocable method to clone' description='Returns the new Id' category='Quote')
public static List<string> cloneQuote(List<ID> ids) {
List<string> retMsg=new List<String>();
if(ids.size()>0){
retMsg.Add(cloneAnySobjet(ids[0]));
}
return retMsg;
}
/**
*
* @description Method that clones an SObject according to it's ID (recordId as parameter)
*
* @param recordId
* @param relateToClonedRecord
* @param mainRecordFieldName
* @param relatedChildsAPIName
* @param parentFieldInChild
*
* @return
*/
@AuraEnabled
public static String cloneAnySobjet(Id recordId) {
String msg = '';
Savepoint sp = Database.setSavepoint(); //to make rollback in case of Error
Map<String, Schema.SObjectType> schemaMap = Schema.getGlobalDescribe();
String objectAPIName = String.valueOf(recordId.getSobjectType());
String soqlQuery = 'SELECT ';
if (describeObjects.get(objectAPIName) == null) {
describeObjects.put(
objectAPIName,
schema.getGlobalDescribe()
.get(objectAPIName)
.getDescribe()
.fields.getMap()
);
}
Set<String> fieldMap = describeObjects.get(objectAPIName).keySet();
for (String s : fieldMap) {
if (
describeObjects.get(objectAPIName).get(s).getDescribe().isAccessible()
) {
soqlQuery += +s + ',';
}
}
soqlQuery = soqlQuery.removeEnd(',');
soqlQuery += ' FROM ' + objectAPIName + ' WHERE ID = \'' + recordId + '\'';
System.debug('soqlQuery 1 = ' + soqlQuery);
SObject record = Database.query(soqlQuery);
SObject clonedParentRecordID = record.clone(false, true, false, false);
String mtdQuery = 'SELECT Id,MasterLabel,CloneChilds__c,LinkClonedToMainRecord__c,ListChildsToClone__c,LookupFieldToParent__c,StaticMappingFields__c FROM CloneObjectOptions__mdt WHERE MasterLabel = \''+objectAPIName+ '\' LIMIT 1';
List<CloneObjectOptions__mdt> configs = Database.query(mtdQuery);
List<CloneObjectOptions__mdt> config = [SELECT
Id,
MasterLabel,
CloneChilds__c,
LinkClonedToMainRecord__c,
ListChildsToClone__c,
LookupFieldToParent__c,
StaticMappingFields__c
FROM CloneObjectOptions__mdt
WHERE MasterLabel = :objectAPIName
LIMIT 1
];
try {
System.debug('@@configClone: ' + config);
if (config.size() > 0) {
Map<String, Object> staticValuesParse = new Map<String, Object>();
List<Object> listChilds = new List<Object>();
if (config.get(0).LinkClonedToMainRecord__c)
clonedParentRecordID.put(
config.get(0).LookupFieldToParent__c,
recordId
);
if (
config.get(0).StaticMappingFields__c != null &&
config.get(0).StaticMappingFields__c.trim() != ''
) {
staticValuesParse = (Map<String, Object>) JSON.deserializeUntyped(
config.get(0).StaticMappingFields__c.trim()
);
for (String fieldName : staticValuesParse.keySet()) {
if (staticValuesParse.get(fieldName).equals(''))
clonedParentRecordID.put(fieldName, null);
else
clonedParentRecordID.put(
fieldName,
staticValuesParse.get(fieldName)
);
}
}
System.debug('@@Here before');
insert clonedParentRecordID;
System.debug('@@@obj: ' + clonedParentRecordID);
if (
config.get(0).CloneChilds__c &&
config.get(0).ListChildsToClone__c != null &&
config.get(0).ListChildsToClone__c.trim() != ''
) {
listChilds = (List<Object>) JSON.deserializeUntyped(
config.get(0).ListChildsToClone__c.trim()
);
for (Object oneChild : listChilds) {
Map<String, Object> convChild = (Map<String, Object>) oneChild;
// OLD
// msg = cloneRelatedChilds(recordId, clonedParentRecordID.id, (String)convChild.get('ChildApiName'), (String)convChild.get('ParentFieldInChild'), sp);
// if (msg.contains('KO:')) {
// Database.rollback(sp);
// return msg ;
// }
// OLD
// NEW
try {
msg = cloneRelatedChilds(
recordId,
clonedParentRecordID.id,
(String) convChild.get('ChildApiName'),
(String) convChild.get('ParentFieldInChild'),
sp
);
System.debug('MSG ***** = ' + msg);
if (
Test.isRunningTest() && throwExceptionValue == 'NOTHING_CLONED'
) {
throw new CloneObjectWithRelatedException(
'Nothing cloned and No Id returned.'
);
}
return msg;
} catch (Exception e) {
Database.rollback(sp);
return msg;
}
// NEW
}
}
}
System.debug('config.size = ' + config.size());
System.debug('configs.size = ' + configs.size());
System.debug('ObjectName = ' + objectAPIName);
if (
Test.isRunningTest() &&
throwExceptionValue == 'CONFIG_NULL' &&
config.size() == 0
) {
throw new CloneObjectWithRelatedException('No config found.');
}
return msg;
} catch (Exception e) {
System.debug('@@error in Parent Object : ' + e.getMessage());
Database.rollback(sp);
return 'KO: ' + e.getMessage();
}
}
//Methode used to link new Opp and new case to the cloned Quote
@AuraEnabled
public static void updateQuote(Id quoteId,Id opportunityId){
SBQQ__Quote__c clonedQuote=[SELECT id,SBQQ__Primary__c From SBQQ__Quote__c where id = :quoteId];
if(clonedQuote!=null){
if(opportunityId!=null){
clonedQuote.SBQQ__Opportunity2__c=opportunityId;
clonedQuote.SBQQ__Primary__c = true;
}
if(opportunityId!=null )update clonedQuote;
}
}
/**
* @description Method that clones the Child Objects of an SObject
*/
public static String cloneRelatedChilds(
Id parentSchedule,
Id newParentSchedule,
String sObjectChildName,
String lookupFieldName,
Savepoint sp
) {
String parentObjectAPIName = String.valueOf(
parentSchedule.getSobjectType()
);
Map<Id, Id> childsFirstLevel = new Map<Id, Id>();
String msg = newParentSchedule;
String soqlQuery = 'SELECT ';
if (describeObjects.get(sObjectChildName) == null) {
describeObjects.put(
sObjectChildName,
schema.getGlobalDescribe()
.get(sObjectChildName)
.getDescribe()
.fields.getMap()
);
}
Set<String> fieldMap = describeObjects.get(sObjectChildName).keySet();
for (String s : fieldMap) {
if (
describeObjects.get(sObjectChildName)
.get(s)
.getDescribe()
.isAccessible()
) {
soqlQuery += +s + ',';
}
}
soqlQuery = soqlQuery.removeEnd(',');
soqlQuery +=
' FROM ' +
sObjectChildName +
' WHERE ' +
lookupFieldName +
'= \'' +
parentSchedule +
'\'';
System.debug('soqlQuery 2 = ' + soqlQuery);
List<SObject> clonedChilds = new List<SObject>();
for (SObject obj : Database.query(soqlQuery)) {
SObject cloned = obj.clone();
cloned.put(lookupFieldName, newParentSchedule);
if (sObjectChildName == 'SBQQ__QuoteLine__c')
cloned.put('SBQQ__Group__c', null);
clonedChilds.add(cloned);
System.debug('@@quote: ' + obj.Id);
}
try {
System.debug('@@ childs: ' + clonedChilds);
if (
Test.isRunningTest() && throwExceptionValue == 'CLONE_EXCEPTION'
) {
throw new CloneObjectWithRelatedException(
'Cloning Exception.'
);
}else{
insert clonedChilds;
}
} catch (Exception e) {
System.debug('@@error in childs: ' + e.getMessage());
Database.rollback(sp);
msg = 'KO: ' + e.getMessage();
}
return msg;
}
}
The custom metadata is instrumental in mapping each object to its children, facilitating a flawless cloning process. This setup demonstrates the extensibility of Salesforce's cloning functionality, showcasing how it can be tailored to meet diverse data management requirements.
These use cases elucidate the potential of Salesforce's cloning functionality when melded with the robustness of Apex and the orchestration capabilities of Flows. The ability to accurately clone data, be it specific objects like quotes or a more generalized set of objects, is a testament to the flexibility and power of Salesforce as a CRM platform.
Comments