/** * @description This service exposes methods that contain logic to make cloning an opportunity * much easier and more functional. * @author Cloud Performer Inc. * @version 1.0 * @group Opportunity Cloning * @history 2021-04-02 - CJG - Created * 2021-04-12 - CJG - Added setting to restrict child records shown to a particular List */ public with sharing class OpportunityCloneService { /** * @description exception class that is thrown when something happens in the opportunity * clonde process. */ public class CloneException extends System.Exception {} /** * @description Map of Child relationship objects to the opportunity. */ public static Map opportunityChildrenMap; /** * @description static constructor that will populate the opportunityChildrenMap */ static { List childRelationshipList = Schema.Opportunity.getSObjectType().getDescribe().getChildRelationships(); opportunityChildrenMap = new Map(); List restrictedRelationshipList = new List(); String restrictedChildList = Label.SmartCloneChildList; if(String.isNotBlank(restrictedChildList)){ restrictedRelationshipList = restrictedChildList.split(';'); } for(Schema.ChildRelationship childRel:childRelationshipList) { if(String.isNotBlank(childRel.getRelationshipName())){ Schema.DescribeSObjectResult childObjRes = childRel.getChildSObject().getDescribe(); if(!restrictedRelationshipList.isEmpty()){ if(restrictedRelationshipList.contains(childObjRes.getName())){ opportunityChildrenMap.put(childRel.getRelationshipName(),childObjRes); } } else { opportunityChildrenMap.put(childRel.getRelationshipName(),childObjRes); } } } } /** * @description Lightning Exposed method that will provide a map of Child relationships * to an Opportunity that can be cloned. * @return Map Map where the key represents the relationship name and the value is the plural label * of the child object. */ @AuraEnabled(cacheable=true) public static Map ObtainOpporunityChildRelationships(){ //return new List(opportunityChildrenMap.keySet()); Map relationshipMap = new Map(); for(String key:opportunityChildrenMap.keySet()){ Schema.DescribeSObjectResult res = opportunityChildrenMap.get(key); if(res.isAccessible() && res.isCreateable()){ String pluralLabel = res.getLabelPlural(); relationshipMap.put(key,pluralLabel); } } return relationshipMap; } /** * @description Lightning Exposed method that will provide a Map of fields in a fieldset on the * opportunity. * @param fieldsetName The name of the fieldset to use * @return Map Map where the key is the apiFieldName and the value is whether the field is marked as * 'required' in the fieldset. */ @AuraEnabled(cacheable=true) public static Map ObtainCloneOpportunityFields(String fieldsetName){ Map fieldsToReturn = new Map(); Map fieldSetMap = Schema.Opportunity.getSObjectType().getDescribe().fieldsets.getMap(); Schema.FieldSet fieldSetItem = fieldSetMap.get(fieldsetName); if(fieldSetItem == null){ throw new OpportunityCloneService.CloneException('No field set named ' + fieldsetName + 'found on the Opportunity.'); } for(Schema.FieldSetMember fieldMember:fieldSetMap.get(fieldsetName).getFields()) { fieldsToReturn.put(fieldMember.getFieldPath(),fieldMember.getRequired()); } return fieldsToReturn; } @AuraEnabled public static Id CloneOpportunityWithChildren(Id oppId, Map newValues, List childRelationshipsToClone) { //dynamic query in order to find the original opportunity. String originalOppQueryString = ObtainQueryStringForOppAndChildren(oppId, childRelationshipsToClone); System.debug(LoggingLevel.INFO,'\n\n*****\nQuery: ' + originalOppQueryString + '\n*****\n\n'); List oppList = Database.query(originalOppQueryString); //if the original opp cannot be found, throw and exception if(oppList.isEmpty()){ throw new AuraHandledException('There was a problem obtaining original opportunity.'); } Opportunity originalOpp = oppList[0]; //clone the original opportunity Opportunity clonedOpp = originalOpp.clone(false,true,false,false); //replace the values on the Opportunity with the new ones for(String fieldName:newValues.keySet()){ clonedOpp.put(fieldName, CreateObjectValueFromString(fieldName,newValues.get(fieldName))); } //insert the new Opportunity try{ insert clonedOpp; } catch(System.Exception ex){ System.debug(LoggingLevel.INFO,'\n\n*****\nProblem creating new Opportunity: ' + ex.getMessage() + '\n*****\n\n'); throw new AuraHandledException('Unable to create new opportunity.'); } //now we try to loop through the children and clone the records List childRelationships = Schema.Opportunity.getSObjectType().getDescribe().getChildRelationships(); List childRecsToInsert = new List(); for(Schema.ChildRelationship childRel:childRelationships){ //if the child relationship name was passed in, get the records if(childRelationshipsToClone.contains(childRel.getRelationshipName())){ List childRecs = originalOpp.getSObjects(childRel.getRelationshipName()); childRecsToInsert.addAll(CloneAndReassociateRecords(childRecs, childRel.getField().getDescribe().getName(),clonedOpp.Id)); } } if(!childRecsToInsert.isEmpty()){ try{ insert childRecsToInsert; } catch(System.Exception ex) { System.debug(LoggingLevel.INFO,'\n\n*****\nError cloning children: ' + ex.getMessage() + '\n*****\n\n'); throw new AuraHandledException('Unable to clone children.'); } } return clonedOpp.Id; } public static String ObtainQueryStringForOppAndChildren(Id oppRecordId, List relationships) { String queryString = 'SELECT '; queryString += String.join(ObtainFieldsForObject('Opportunity'),','); List subqueryList = new List(); for(String relationship:relationships){ Schema.DescribeSObjectResult childObject = opportunityChildrenMap.get(relationship); String childQueryString = '(SELECT ' + String.join(ObtainFieldsForObject(childObject.getName()),',') + ' FROM '; childQueryString += relationship + ')'; subqueryList.add(childQueryString); } if(!subqueryList.isEmpty()){ queryString += ', ' + String.join(subqueryList,',') + 'FROM Opportunity WHERE Id =\'' + oppRecordId + '\''; } else { queryString += ' FROM Opportunity WHERE Id =\'' + oppRecordId + '\''; } return queryString; } /** * @description this method is responsible for getting the list of fields for a particular object */ private static List ObtainFieldsForObject(String objectName) { List fieldNameList; List objectListToDescribe = new List{objectName}; List describeItem = System.Schema.describeSObjects(objectListToDescribe); if(describeItem.isEmpty()) { throw new OpportunityCloneService.CloneException('Could not find info for ' + objectName + '.'); } Map fieldSObjectMapping = describeItem[0].fields.getMap(); fieldNameList = new List(); for(Schema.SObjectField fieldToken:fieldSObjectMapping.values()) { Schema.DescribeFieldResult fieldTokenDescribe = fieldToken.getDescribe(); if(fieldTokenDescribe.isCreateable() && fieldTokenDescribe.isUpdateable()) { fieldNameList.add(fieldTokenDescribe.getName()); } } //need to account for weird objects where not all of the fields come back if(objectName.toUpperCase() == 'OPPORTUNITYLINEITEM'){ fieldNameList.add('PricebookEntryId'); } return fieldNameList; } private static List CloneAndReassociateRecords(List recList, String parentFieldName, Id newParentId) { List newList = new List(); for(SObject rec:recList){ SObject newRec = rec.clone(false,true,false,false); newRec.put(parentFieldName, newParentId); if(newRec.getSObjectType().getDescribe().getName().toUpperCase() == 'OPPORTUNITYLINEITEM') { newRec.put('UnitPrice',null); } newList.add(newRec); } return newList; } private static Object CreateObjectValueFromString(String fieldName, String fieldValue) { Map fieldSObjectMapping = Schema.Opportunity.getSObjectType().getDescribe().fields.getMap(); Schema.DisplayType fieldType = fieldSObjectMapping.get(fieldName).getDescribe().getType(); if(String.isBlank(fieldValue)){ return null; } Object returnObj; switch on fieldType { when ADDRESS, COMBOBOX, EMAIL, ENCRYPTEDSTRING, MULTIPICKLIST, PHONE, PICKLIST, STRING, TEXTAREA,URL { returnObj = fieldValue; } when BOOLEAN { returnObj = Boolean.valueOf(fieldValue); } when CURRENCY, DOUBLE, PERCENT { returnObj = Double.valueOf(fieldValue); } when DATE { returnObj = Date.valueOf(fieldValue); } when DATETIME { returnObj = Datetime.parse(fieldValue); } when ID { returnObj = Id.valueOf(fieldValue); } when INTEGER { returnObj = Integer.valueOf(fieldValue); } when LONG { returnObj = Long.valueOf(fieldValue); } when TIME { returnObj = fieldValue; } when else { returnObj = fieldValue; } } return returnObj; } }