Thursday, September 19, 2013

Deeper Clone: Clone Salesforce objects with all levels of children

During development, I ran into a requirement to be able to take any record and clone not only it, but all its children, grand-children, great-grand-children, etc.  As you may already know, Salesforce has already provided a generic "clone" method which literally copies the specified sObjects and casts that new record to a new sObject awaiting insertion.  This method also provides the capability to maintain relationships opt_isDeepClone.  However, there are two major limitations of this method.  First, the generic clone method only pays attention to the fields stored in-memory for the original sObject.  Second, the generic clone method does not pay any attention to child relationships and records, which limits the real-world applicability.

I will address both challenges here.


In order to circumvent the first "in-memory" issue and also prevent code modification with each field addition, we'll generate a dynamic list of fields for the sObjects in question, then run and store a SoQL query with the resulting fields.  Another trick I've included is the ability to exclude specific fields for a given sObject.  This may be important if there is a specific field that should be reset upon cloning (for instance, maybe you want to reset the "Stage" field when you clone an existing, completed Opportunity).

To address the children, grand-children, great-grand-children, etc I've employed two inner classes, which will serve as a storage medium while we iterate through and identify all levels of parents and children we wish to clone.  These classes will also help house the relationship information for later use, such as the name of the child object and the name of the field which is used to tie it to the parent (an example here would be the OpportunityId field for the OpportunityLineItem table).

Without further ado, let's get to it!

1. Create a new Custom Setting (Setup > Develop > Custom Settings > New) and create a setting named ApexMessage with one field (ApexMessage):


2. Once created, click "Manage" for the ApexMessage Custom Setting and then click "New" to enter a new message:

**Feel free to re-word the message to your liking, but please note the Name must be exact.

3. Create a new Custom Setting (Setup > Develop > Custom Settings > New) and create a setting named ChildRelationship with two fields (ChildRelationship, sObjectType):


4. Once created, click "Manage" for the ChildRelationship Custom Setting and then click "New" to enter a new relationship:

**The ChildRelationship field contains the actual child relationship name from the lookup

5. Create a new Custom Setting (Setup > Develop > Custom Settings > New) and create a setting named FieldExclusion with two fields (FieldExclusion, sObjectType):


6. Once created, click "Manage" for the FieldExclusion Custom Setting and then click "New" to enter a new field exclusion:

**The FieldExclusion field contains the actual API name for the fields you wish to exclude

7. Create a new Custom Setting (Setup > Develop > Custom Settings > New) and create a setting named FieldDefault with three fields (FieldDefault, FieldValue, sObjectType):


8. Once created, click "Manage" for the FieldDefault Custom Setting and then click "New" to enter a new field default:

**The FieldDefault field contains the actual API name for the fields you wish to exclude

9. Create a new Button (Setup > Customize > Opportunities > Buttons, Links, and Actions > New Button or Link) and create a button:

**You may choose to create this button on any other table, that's the beauty of the tool!

10. Add the Button from step nine to your page layout and enjoy!

11. Create a new Apex Class (Setup > Develop > Apex Classes > New) and paste the following code:

global class UtilsCustomSetting {
    
    private static final Map<String, String> mapApexMessages = mapApexMessages();
    private static final Map<String, Set<String>> mapChildRelationships = mapChildRelationships();
    private static final Map<String, Set<String>> mapFieldExclusions = mapFieldExclusions();
    private static final Map<String, Map<String, String>> mapFieldDefaults = mapFieldDefaults();
    
    global static Map<String, String> mapApexMessages() {
        List<ApexMessage__c> msgs = ApexMessage__c.getAll().values();
        Map<String, String> options = new Map<String, String>();
        for (ApexMessage__c msg : msgs) {
            options.put(msg.Name, msg.ApexMessage__c);
        }
        return options;
    }
    
    global static Map<String, Set<String>> mapChildRelationships() {
        List<ChildRelationship__c> excls = ChildRelationship__c.getAll().values();
        Map<String, Set<String>> mapOf = new Map<String, Set<String>>();
        for (ChildRelationship__c excl : excls) {
            if (!mapOf.containsKey(excl.sObjectType__c)) {
                mapOf.put(excl.sObjectType__c, new Set<String>());
            }
            mapOf.get(excl.sObjectType__c).add(excl.ChildRelationship__c);
        }
        return mapOf;
    }
    
    global static Map<String, Set<String>> mapFieldExclusions() {
        List<FieldExclusion__c> excls = FieldExclusion__c.getAll().values();
        Map<String, Set<String>> mapOf = new Map<String, Set<String>>();
        for (FieldExclusion__c excl : excls) {
            if (!mapOf.containsKey(excl.sObjectType__c)) {
                mapOf.put(excl.sObjectType__c, new Set<String>());
            }
            mapOf.get(excl.sObjectType__c).add(excl.FieldExclusion__c.toLowerCase());
        }
        return mapOf;
    }
    
    global static Map<String, Map<String, String>> mapFieldDefaults() {
        List<FieldDefault__c> defs = FieldDefault__c.getAll().values();
        Map<String, Map<String, String>> mapOf = new Map<String, Map<String, String>>();
        for (FieldDefault__c def : defs) {
            if (!mapOf.containsKey(def.sObjectType__c)) {
                mapOf.put(def.sObjectType__c, new Map<String, String>());
            }
            mapOf.get(def.sObjectType__c).put(def.FieldDefault__c.toLowerCase(), def.FieldValue__c);
        }
        return mapOf;
    }
    
    //** Returns a message for use in APEX error handling, chatter posting, etc
    global static String getApexMessage(String msgName) {
        return mapApexMessages.get(msgName);
    }
    
    //** Returns a set of child relationships to include in the cloning query
    global static Set<String> getChildRelationships(String relName) {
        Set<String> setOf = new Set<String>();
        if (mapChildRelationships.containsKey(relName)) {
            setOf.addAll(mapChildRelationships.get(relName));
        }
        return setOf;
    }
    
    //** Returns a set of fields to exclude from the cloning query
    global static Set<String> getFieldExclusions(String esclName) {
        Set<String> setOf = new Set<String>();
        if (mapFieldExclusions.containsKey(esclName)) {
            setOf.addAll(mapFieldExclusions.get(esclName));
        }
        return setOf;
    }
    
    //** Returns a set of fields to exclude from the cloning query
    global static Set<String> getFieldDefaults(String defName) {
        Set<String> setOf = new Set<String>();
        if (mapFieldDefaults.containsKey(defName)) {
            setOf.addAll(mapFieldDefaults.get(defName).keySet());
        }
        return setOf;
    }
    
    //** Returns a set of fields to exclude from the cloning query
    global static String getFieldDefault(String defName, String defValue) {
        if (mapFieldDefaults.containsKey(defName)) {
            if (mapFieldDefaults.get(defName).containsKey(defValue)) {
                return mapFieldDefaults.get(defName).get(defValue);
            }
        }
        return null;
    }
    
}

12. Create a new Apex Class (Setup > Develop > Apex Classes > New) and paste the following code:

global without sharing class UtilsVariables {
    
    global static Map<String, String> mapApexMessages() {
        List<ApexMessage__c> msgs = ApexMessage__c.getAll().values();
        Map<String, String> options = new Map<String, String>();
        for (ApexMessage__c msg : msgs) {
            options.put(msg.Name, msg.ApexMessage__c);
        }
        return options;
    }
    
    global static Map<String, Set<String>> mapChildRelationships() {
        List<ChildRelationship__c> excls = ChildRelationship__c.getAll().values();
        Map<String, Set<String>> mapOf = new Map<String, Set<String>>();
        for (ChildRelationship__c excl : excls) {
            if (!mapOf.containsKey(excl.sObjectType__c)) {
                mapOf.put(excl.sObjectType__c, new Set<String>());
            }
            mapOf.get(excl.sObjectType__c).add(excl.ChildRelationship__c);
        }
        return mapOf;
    }
    
    global static Map<String, Set<String>> mapFieldExclusions() {
        List<FieldExclusion__c> excls = FieldExclusion__c.getAll().values();
        Map<String, Set<String>> mapOf = new Map<String, Set<String>>();
        for (FieldExclusion__c excl : excls) {
            if (!mapOf.containsKey(excl.sObjectType__c)) {
                mapOf.put(excl.sObjectType__c, new Set<String>());
            }
            mapOf.get(excl.sObjectType__c).add(excl.FieldExclusion__c.toLowerCase());
        }
        return mapOf;
    }
    
    global static Map<String, Map<String, String>> mapFieldDefaults() {
        List<FieldDefault__c> defs = FieldDefault__c.getAll().values();
        Map<String, Map<String, String>> mapOf = new Map<String, Map<String, String>>();
        for (FieldDefault__c def : defs) {
            if (!mapOf.containsKey(def.sObjectType__c)) {
                mapOf.put(def.sObjectType__c, new Map<String, String>());
            }
            mapOf.get(def.sObjectType__c).put(def.FieldDefault__c.toLowerCase(), def.FieldValue__c);
        }
        return mapOf;
    }
    
    //** Maps, held privately so as to only pull once per call
    private static final Map<String, String> mapApexMessages = UtilsCustomSetting.mapApexMessages();
    private static final Map<String, Set<String>> mapChildRelationships = UtilsCustomSetting.mapChildRelationships();
    private static final Map<String, Set<String>> mapFieldExclusions = UtilsCustomSetting.mapFieldExclusions();
    private static final Map<String, Map<String, String>> mapFieldDefaults = UtilsCustomSetting.mapFieldDefaults();
    
    //** Returns a message for use in APEX error handling, chatter posting, etc
    global static String getApexMessage(String msgName) {
        return mapApexMessages.get(msgName);
    }
    
    //** Returns a set of child relationships to include in the cloning query
    global static Set<String> getChildRelationships(String relName) {
        Set<String> setOf = new Set<String>();
        if (mapChildRelationships.containsKey(relName)) {
            setOf.addAll(mapChildRelationships.get(relName));
        }
        return setOf;
    }
    
    //** Returns a set of fields to exclude from the cloning query
    global static Set<String> getFieldExclusions(String esclName) {
        Set<String> setOf = new Set<String>();
        if (mapFieldExclusions.containsKey(esclName)) {
            setOf.addAll(mapFieldExclusions.get(esclName));
        }
        return setOf;
    }
    
    //** Returns a set of fields to exclude from the cloning query
    global static Set<String> getFieldDefaults(String defName) {
        Set<String> setOf = new Set<String>();
        if (mapFieldDefaults.containsKey(defName)) {
            setOf.addAll(mapFieldDefaults.get(defName).keySet());
        }
        return setOf;
    }
    
    //** Returns a set of fields to exclude from the cloning query
    global static String getFieldDefault(String defName, String defValue) {
        if (mapFieldDefaults.containsKey(defName)) {
            if (mapFieldDefaults.get(defName).containsKey(defValue)) {
                return mapFieldDefaults.get(defName).get(defValue);
            }
        }
        return null;
    }
    
}

13. Create a new Apex Class (Setup > Develop > Apex Classes > New) and paste the following code:

global class UtilsGeneral {
    
    global static sObject newBlankSObject(sObject so) {
        return so.getSObjectType().newSObject();
    }
    
}

14. Create a new Apex Class (Setup > Develop > Apex Classes > New) and paste the following code:

global with sharing class UtilsDeeperClone {
    
    //** Deeply clones a list of original records, and all related/requested children
    //** Original list must include the Id field
    global static List<Id> clone(List<sObject> originalObjects, Boolean chatterPost) {
        //** Method variables
        Map<Id, Id> mapIdOldNew = new Map<Id, Id>();
        Map<String, List<Id>> mapObjectIds = new Map<String, List<Id>>();
        Map<String, String> mapRelationshipFields = new Map<String, String>();
        Map<String, Map<Id, sObject>> mapObjectQueries = new Map<String, Map<Id, sObject>>();
        Map<Integer, Map<String, List<sObject>>> mapObject = new Map<Integer, Map<String, List<sObject>>>();
        List<sObjectChildRelationship> myChildRelationships = new List<sObjectChildRelationship>();
        //** Add Level 0 (originalObjects)
        Map<String, List<sObject>> mapObjectCurrent = new Map<String, List<sObject>>();
        List<sObjectChildRelationshipRow> myChildRelationshipRows = new List<sObjectChildRelationshipRow>();
        //** Process input sObjects by sObjectType
        Set<String> origObjectTypeNames = new Set<String>();
        for (sObject originalObject : originalObjects) {
            if (!origObjectTypeNames.contains(originalObject.getSObjectType().getDescribe().getName())) {
                origObjectTypeNames.add(originalObject.getSObjectType().getDescribe().getName());
            }
        }
        for (String origObjectTypeName : origObjectTypeNames) {
            for (sObject originalObject : originalObjects) {
                if (origObjectTypeName == originalObject.getSObjectType().getDescribe().getName()) {
                    //** Add sObject to myChildRelationshipRows
                    myChildRelationshipRows.add(new sObjectChildRelationshipRow(originalObject));
                    //** Populate Map<String, List<Id>> mapIdObjectTypeCurrent
                    if (mapObjectCurrent.containsKey(origObjectTypeName)) {
                        mapObjectCurrent.get(origObjectTypeName).add(originalObject);
                    } else {
                        mapObjectCurrent.put(origObjectTypeName, new List<sObject>{originalObject});
                    }
                }
            }
            //** Add myChildRelationshipRows to a myChildRelationships record with relationship information
            if (!myChildRelationshipRows.isEmpty()) {
                myChildRelationships.add(new sObjectChildRelationship(0, origObjectTypeName, null, myChildRelationshipRows));
            }
        }
        //** Populate Map<Integer, Map<String, List<Id>>> mapObject
        mapObject.put(0, mapObjectCurrent);
        //** Loop through levels 1-n (children)
        for (Integer currentLevel = 1 ; currentLevel < 20 ; currentLevel++) {
            mapObjectCurrent = new Map<String, List<sObject>>();
            if (mapObject.size() == currentLevel) {
                //** Loop through all tables
                for (String objType : mapObject.get(currentLevel-1).keySet()) {
                    List<sObject> sObjectOriginals = mapObject.get(currentLevel-1).get(objType);
                    //** Get complete list of all child relationships for the given table 'objType'
                    List<Schema.ChildRelationship> childRelationships =
                        sObjectOriginals.get(0).getSObjectType().getDescribe().getChildRelationships();
                    //** Exit loop once there are no more childRelationships
                    if (!childRelationships.isEmpty()) {
                        //** Loop through all child relationships
                        for (Schema.ChildRelationship childRelationship : childRelationships) {
                            //** Only run script when the child relationships is acceptable (Custom Setting)
                            if (UtilsVariables.getChildRelationships(objType).contains(
                                    childRelationship.getRelationshipName())) {
                                myChildRelationshipRows = new List<sObjectChildRelationshipRow>();
                                //** Name of the Child Table
                                String childObjectTypeName = childRelationship.getChildSObject().getDescribe().getName();
                                //** Name of the Child Field which stores the Parent sObject Id
                                String childObjectFieldName = childRelationship.getField().getDescribe().getName();
                                //** Build query to return all children in this relationship
                                String childRelationshipQuery = 'SELECT ID';
                                childRelationshipQuery += ' FROM ' + childObjectTypeName;
                                childRelationshipQuery += ' WHERE ' + childObjectFieldName;
                                childRelationshipQuery += ' IN (\'' + sObjectOriginals.get(0).Id + '\'';
                                for (Integer i = 1 ; i < sObjectOriginals.size() ; i++) {
                                    childRelationshipQuery += ', \'' + sObjectOriginals.get(i).Id + '\'';
                                }
                                childRelationshipQuery += ')';
                                //** Executive query to return all children in this relationship
                                List<sObject> childRelationshipQueryResults = Database.query(childRelationshipQuery);
                                //** Loop through all queried children
                                for (sObject childObject : childRelationshipQueryResults) {
                                    //** Add sObject to myChildRelationshipRows
                                    myChildRelationshipRows.add(new sObjectChildRelationshipRow(childObject));
                                    //** Populate Map<String, List<Id>> mapIdObjectTypeCurrent
                                    if (mapObjectCurrent.containsKey(childObjectTypeName)) {
                                        mapObjectCurrent.get(childObjectTypeName).add(childObject);
                                    } else {
                                        mapObjectCurrent.put(childObjectTypeName, new List<sObject>{childObject});
                                    }
                                }
                                //** Add myChildRelationshipRows to a myChildRelationships record with relationship information
                                if (!myChildRelationshipRows.isEmpty()) {
                                    myChildRelationships.add(new sObjectChildRelationship(currentLevel,
                                        childObjectTypeName,
                                        childObjectFieldName,
                                        myChildRelationshipRows));
                                }
                            }
                        }
                    }
                }
            }
            //** Populate Map<Integer, Map<String, List<Id>>> mapObject
            if (!mapObjectCurrent.isEmpty()) {
                mapObject.put(currentLevel, mapObjectCurrent);
            }
        }
        //** Establish a list of Ids per each sObjectType for the result queries
        for (sObjectChildRelationship rel : myChildRelationships) {
            for (sObjectChildRelationshipRow row : rel.myChildRelationshipRowList) {
                if (!mapObjectIds.containsKey(rel.relationshipName)) {
                    mapObjectIds.put(rel.relationshipName, new List<Id>());
                }
                mapObjectIds.get(rel.relationshipName).add(row.sObjectOriginal.Id);
                if (!mapRelationshipFields.containsKey(rel.relationshipName)) {
                    mapRelationshipFields.put(rel.relationshipName, rel.relationshipFieldName);
                }
            }
        }
        //** Loop through each sObjectType to query the results for use later
        for (String objName : mapObjectIds.keySet()) {
            //** List of all Ids for the records to be cloned
            List<Id> sObjectIds = new List<Id>();
            sObjectIds = mapObjectIds.get(objName);
            //** List of all fields for the records to be cloned 
            List<String> sObjectFields = new List<String>();
            //** sObjectType
            Schema.SObjectType sObjectType = sObjectIds.get(0).getSObjectType();
            //** Get all current fields from the object
            if (sObjectType != null) {
                Map<String, Schema.SObjectField> fieldMap = sObjectType.getDescribe().fields.getMap();
                for (String fieldName : fieldMap.keySet()) {
                    if (fieldMap.get(fieldName).getDescribe().isCreateable()) {
                        sObjectFields.add(fieldName);
                    }
                }
            }
            for (Integer i = 0; i < sObjectFields.size(); i++) {
                if (UtilsVariables.getFieldExclusions(sObjectType.getDescribe().getName()).contains(sObjectFields.get(i))) {
                    sObjectFields.remove(i);
                }
            }
            //** If there are no records sent into the method, then return an empty list
            if (sObjectIds != null && !sObjectIds.isEmpty() && !sObjectFields.isEmpty()) {
                //** Construct a SOQL query to get all field values of all records (sort Id ascending)
                String sObjectFieldsQuery = 'SELECT ' + sObjectFields.get(0);
                for (Integer i = 1; i < sObjectFields.size(); i++) {
                    sObjectFieldsQuery += ', ' + sObjectFields.get(i);
                }
                sObjectFieldsQuery += ' FROM ' + sObjectType.getDescribe().getName();
                sObjectFieldsQuery += ' WHERE Id IN (\'' + sObjectIds.get(0) + '\'';
                for (Integer i = 1 ; i < sObjectIds.size() ; i++) {
                    sObjectFieldsQuery += ', \'' + sObjectIds.get(i) + '\'';
                }
                sObjectFieldsQuery += ')';
                System.debug('##### sObjectFieldsQuery: ' + sObjectFieldsQuery);
                List<sObject> sObjectFieldsQueryResults = Database.query(sObjectFieldsQuery);
                Map<Id, sObject> mapObjectFieldsQueryResults = new Map<Id, sObject>();
                for (sObject obj : sObjectFieldsQueryResults) {
                    mapObjectFieldsQueryResults.put(obj.Id, obj);
                }
                mapObjectQueries.put(objName, mapObjectFieldsQueryResults);
            }
        }
        //** Loop through each level to insert while adding the correct parent identification
        for (Integer currentLevel = 0 ; currentLevel < 20 ; currentLevel++) {
            List<sObject> sObjectsToClone = new List<sObject>();
            List<Id> listIdOld = new List<Id>();
            List<Id> listIdNew = new List<Id>();
            for (sObjectChildRelationship cloneChildRelationship : myChildRelationships) {
                if (cloneChildRelationship.relationshipLevel == currentLevel) {
                    sObjectsToClone = new List<sObject>();
                    for (sObjectChildRelationshipRow cloneChildRelationshipRow :
                            cloneChildRelationship.myChildRelationshipRowList) {
                        listIdOld.add(cloneChildRelationshipRow.sObjectOriginal.Id);
                        sObject orig = mapObjectQueries.get(cloneChildRelationship.relationshipName).get(
                            cloneChildRelationshipRow.sObjectOriginal.Id);
                        sObject clone = UtilsGeneral.newBlankSObject(orig);
                        Map<String, Schema.SObjectField> fieldMap = clone.getSObjectType().getDescribe().fields.getMap();
                        System.debug('##### Clone: Before = ' + clone);
                        for (String fieldName : fieldMap.keySet()) {
                            if (fieldName != 'Id' && fieldMap.get(fieldName).getDescribe().isCreateable() &&
                                    !UtilsVariables.getFieldExclusions(clone.getSObjectType().getDescribe().getName()).contains(
                                    fieldName)) {
                                clone.put(fieldName, orig.get(fieldName));
                                System.debug('##### Clone: Field Clone = ' + fieldName + ': ' + orig.get(fieldName));
                            }
                            if (fieldName != 'Id' && fieldMap.get(fieldName).getDescribe().isCreateable() &&
                                    UtilsVariables.getFieldDefaults(clone.getSObjectType().getDescribe().getName()).contains(
                                    fieldName)) {
                                clone.put(fieldName,
                                    UtilsVariables.getFieldDefault(clone.getSObjectType().getDescribe().getName(), fieldName));
                                System.debug('##### Clone: Field Default = ' + fieldName + ': ' + UtilsVariables.getFieldDefault(
                                    clone.getSObjectType().getDescribe().getName(), fieldName));
                            }
                        }
                        if (cloneChildRelationship.relationshipFieldName != null) {
                            clone.put(cloneChildRelationship.relationshipFieldName, mapIdOldNew.get((Id) mapObjectQueries.get(
                                cloneChildRelationship.relationshipName).get(cloneChildRelationshipRow.sObjectOriginal.Id).get(
                                cloneChildRelationship.relationshipFieldName)));
                            System.debug('##### Clone: Field Relationship = ' + cloneChildRelationship.relationshipFieldName +
                                ': ' + mapIdOldNew.get((Id) mapObjectQueries.get(cloneChildRelationship.relationshipName).get(
                                cloneChildRelationshipRow.sObjectOriginal.Id).get(
                                cloneChildRelationship.relationshipFieldName)));
                        }
                        System.debug('##### Clone: After = ' + clone);
                        sObject cloned = clone.clone(false, true);
                        sObjectsToClone.add(cloned);
                        System.debug('##### Clone: Cloned = ' + cloned);
                    }
                    //** Insert cloned records
                    insert sObjectsToClone;
                    //** Populate list with cloned (new) Ids [assumption is it's the same order as listIdOld]
                    for (sObject newObject : sObjectsToClone) {
                        listIdNew.add(newObject.Id);
                    }
                    //** Fail gracefully if listOldId.size() != listNewId.size()
                    System.assertEquals(listIdNew.size(), listIdOld.size());
                    //** Map the original (old) Ids to the cloned (new) Ids
                    for (Integer i = 0 ; i < listIdOld.size() ; i++) {
                        mapIdOldNew.put(listIdOld.get(i), listIdNew.get(i));
                        if (chatterPost && currentLevel == 0) {
                            UtilsChatter.postChatterFeedItem(ConnectApi.FeedType.Record,
                                listIdNew.get(i),
                                UtilsVariables.getAPEXMessage('CloneChatterMessage') + listIdOld.get(i));
                        }
                    }
                }
            }
        }
        List<Id> clonedObjectIds = new List<Id>();
        for (sObject originalObject : originalObjects) {
            clonedObjectIds.add(mapIdOldNew.get(originalObject.Id));
        }
        return clonedObjectIds;
    }
    
    //** Allow pages and external services to trigger a clone from a single object with or without a Chatter post
    webService static Id clone(Id originalId, Boolean chatterPost) {
        //** Build query to return the full object from an Id
        String originalObjectQuery = 'SELECT ID';
        originalObjectQuery += ' FROM ' + originalId.getSObjectType().getDescribe().getName();
        originalObjectQuery += ' WHERE Id = \'' + originalId + '\'';
        //** Executive query to return the full object from an Id
        List<sObject> originalObject = Database.query(originalObjectQuery);
        //** Execute clone()
        List<Id> clonedIds = clone(originalObject, chatterPost);
        return clonedIds[0];
    }
    
    //** Houses the relationship information with the list of related records
    public class sObjectChildRelationship {
        //** What level of child is it?  0=Parent 1=Child 2=Grand-Child 3=Great-Grand-Child etc...
        public Integer relationshipLevel {get; set;}
        //** What table are the related records stored in?
        public String relationshipName {get; set;}
        //** What is the field name that stores the parent-child relationship?
        public String relationshipFieldName {get; set;}
        //** Houses the list of actual records
        public List<sObjectChildRelationshipRow> myChildRelationshipRowList {get; set;}
        //** Initialize
        public sObjectChildRelationship(Integer relationshipLevel,
                String relationshipName,
                String relationshipFieldName,
                List<sObjectChildRelationshipRow> myChildRelationshipRowList) {
            this.relationshipLevel = relationshipLevel;
            this.relationshipName = relationshipName;
            this.relationshipFieldName = relationshipFieldName;
            this.myChildRelationshipRowList = myChildRelationshipRowList;
        }
    }
    
    //** Houses the actual record
    public class sObjectChildRelationshipRow {
        //** What record am I cloning?
        public sObject sObjectOriginal {get; set;}
        //** Initialize
        public sObjectChildRelationshipRow(sObject sObjectOriginal) {
            this.sObjectOriginal = sObjectOriginal;
        }
    }
    
}
  **Credit goes to Christopher Alun Lewis (blog) for coming up with the inner class idea which I modified to my own requirement here.

A few notes:
  • In order to clone master detail relationships, the option to re-parent must be selected on the field setup
  • You can get the utilChatter class here: utilChatter
  • If Opportunity is one of your child objects, you need to add these field exclusions at minimum: Amount
  • If OpportunityLineItem is one of your child objects, you need to add these field exclusions at minimum: isDeleted,SortOrder,TotalPrice,ListPrice,Subtotal,CreatedDate,CreatedById,LastModifiedDate,LastModifiedById,SystemModStamp
Happy Coding!

97 comments :

  1. Hi Nathan,

    Nice Solution, good work! I had often thought about how I could tackle multiple levels of cloning, and this does the job well. I wouldn't of considered using the sforce.apex.execute javascript, you learn something new every day :D. Also a big fan of using custom settings for establishing the child relationships and exclusions, much more manageable than the in button definitions I use in clone plus.

    Just a few things I noticed while building up the solution:
    1) The process needs to start with building the custom settings, then build the utilCustomSetting class then the utilDeepClone class. They are currently the opposite way round, which causes dependency errors when you start to implement.
    2) You explain which custom settings to build by name, but not which fields to add (like message_body__c field and child_relationship__c field, I had to work it out by looking at the pictures and back tracking from there.
    3) In order to clone master detail relationships, the option to re-parent must be selected on the field setup.
    4) I had two level 1 relationships, and it was complaining that I was trying to insert an item with an id. To fix this I had to move List sObjectsToClone = new List to inside the "if (cloneChildRelationship.relationshipLevel == currentLevel) {" loop.

    I hope you continue to run with the idea, looking forward to seeing how you progress it.

    Cheers,
    Christopher Alun Lewis

    ReplyDelete
    Replies
    1. Hi Christopher,

      Thanks for taking the time to dig into this and I'm glad you found it useful!

      I have re-tooled the post to flow more naturally. I also re-copied the class since I had made a few modifications, such as the ability to set a particular field to a default. For instance, if you want to always set "StageName" back to "Open" or whatever else your business requirements need. I also modified the Custom Settings to be more scalable since the original could only hold 255 characters per sObjectType.

      Delete
    2. I get an error when i click the cloneRecord button it says
      {1faultcode:soap:Client fault string no operation available for request UtilsDeeperClone clone. Please check the wsdl

      Delete
  2. You right!
    He did the cloning.. but - only for the Master object. he didn't clone any of the child and grand child object.

    So i have changed the button code as per you suggest and i get an error:

    "A problem with the OnClick JavaScript for this button or link was encountered:

    sforce is not defined"

    ReplyDelete
    Replies
    1. Did you delete this from the top of the button script?

      {!requireScript("/soap/ajax/29.0/connection.js")}
      {!requireScript("/soap/ajax/29.0/apex.js")}

      if so... add it back to the top...

      Delete
    2. Are you sure you correctly input the child object(s) in the custom setting?

      Delete
    3. For the child object(s), the values in the custom setting may be wrong. If your child is called "My_Child_Object__c" you don't put "My_Child_Object__c" in the custom setting. Look for the "Child Relationship Name" and use that instead. In this case it is probably "My_Child_Objects", in which case you should put "My_Child_Objects__r" (always attach __r to custom object child relationships) in the custom setting. Notice it is plural and has __r (you can google "Child Relationship Name" if you need more help on this...

      Delete
    4. Dear Nathan,
      I must thank you for your very kind patience..

      My Master object is "Tickets__C"
      Child Objects is:
      1. "Certificate__C
      2. Visit__C

      So after i clicked "Manage" in the "ChildRelationship" custom setting, i added:
      Name: "Certificate".
      ChildRelationship: "Certificate__r" / "Visit__r".
      sObject Type: "Tickets__r".

      I didn't added any excluded fields as i want all of them to be cloned.

      Anything missing?
      How do i can add a picture here? then you can see it.

      Delete
    5. Should be:

      Name: "Tickets__c1"
      ChildRelationship: "Certificate__r"
      sObject Type: "Tickets__c"

      Name: "Tickets__c2"
      ChildRelationship: "Visit__r"
      sObject Type: "Tickets__c"

      "Name" is irrelevant, but is really just used for organization (hence adding "1" and "2" at the end)
      "ChildRelationship" is the unique child relationship
      "sObjectType" is the name of the parent sObject (not always the master... consider if you have parent, child, grandchild, the child would be the sObjectType and the grandchild would be the childrelationship in one of the entries while the parent would be the sObjectType and the child would be the chidlrelationship in another... Make sense?

      Delete
    6. Hi Nathan,

      I have changed as you mention. but still not cloning the ChildRelatioinship. just cloning the Master object.
      Maybe i missed anything else?

      Delete
    7. I'd need to see the rest of your code and screenshots of your lookup fields (that would be used as parent/child relationships during cloning)...

      Delete
  3. What is the "StageName" in the "FieldDefault"? in step # 8.

    ReplyDelete
    Replies
    1. "StageName" is irrelevant here, it has to do with Opportunity table only...

      Delete
  4. Hi,

    Great Post, tried couple of times yesterday, but receiving same error in both developer and pro orgs on button click.

    A problem with the OnClick JavaScript for this button or link was encountered:

    {faultcode:'soapenv:Client', faultstring:'No service available for class 'utilDeeperClone'', }

    I am trying to clone opportunity(having custom child and grandchild), logged in through system admin profile and org doesn't have any namespace.

    Thanks a Ton :)

    ReplyDelete
    Replies
    1. Hi Prateek,

      Have you made sure to set the class as "global" and the method as "public"? If not, please post the class that holds the utilDeeperClone method...

      Delete
    2. Hi Prateek,

      It's just a typo, change the name of the class in the button from utilDeeperClone to utilsDeeperClone.

      Delete
    3. Thanks for this, I changed the datatype in "webService static Id clone(Id originalId, Boolean chatterPost)" to "String originalId " and it worked and then I rolled it back still working. Unusual Hmmm.

      Delete
    4. Bizarre, but I'm glad it's working for you. I've never had any issues using "Id originalId"...

      Delete
    5. What was the solution to the "A problem with the OnClick JavaScript for this button or link was encountered:{faultcode:'soapenv:Client', faultstring:'No service available for class 'utilsDeeperClone'', }" issue? I fixed the typo in the button script and still no go. Thanks!

      Delete
    6. Did you try what Prateek said also helped work, which is changing to "String" and back to "Id":

      I changed the datatype in "webService static Id clone(Id originalId, Boolean chatterPost)" to "String originalId " and it worked and then I rolled it back still working.

      Delete
    7. I'm not sure what it is, on a clean org it works fine, but in my dev org (has a namespace) it does not. I shouldn't have to include a namespace prefix as far as I can tell. Thanks for the quick reply!!!

      Delete
    8. Bizarre, I haven't done it in a dev org with a namespace. This was in a traditional sandbox/production environment... Not sure how to help you on that one...

      Delete
    9. Just FYI, There were couple of other things , like few custom fields were not getting defaulted due to few custom validations I had in Place so I had to write a small before insert trigger, I created a field in the backend and on using custom clone button default it to true in custom settings and used it as a criteria in my trigger so that it doesn't fire while creating the Opportunity Normally.

      Delete
    10. Thanks for sharing, Prateek, and I'm glad you got it all working!

      Delete
    11. This comment has been removed by the author.

      Delete
    12. Hi Prateek, were you able to fix the Opportunity_Product_Group__c issue? I think I remember an issue that occurred when the lookup was set to "not allow re-parenting"... Perhaps your field [Opportunity_Product__c.Opportunity_Product_Group__c] is set to not allow re-parenting?

      Delete
  5. Hi Nathan,

    I have a question about error message stored in custom settings. How are they triggered ?

    They are put into a map in both UtilsVariables and UtilsCustomSettings but it seems like they are used until then.

    ReplyDelete
    Replies
    1. Hey Sat,

      I use "get" those from the getApexMessage method inside any sObject.addError call. No different than simply writing your own error messages, except that you don't have to push code updates to update the verbiage...

      Delete
  6. Also is it possible to redirect to the cloned record at the end of the process ?

    ReplyDelete
    Replies
    1. Hi Sat,

      Were you able to achieve this?

      Delete
    2. Hey Sat,

      It looks like Prateek replied in another thread, so just re-posting here so you are sure to see it. This is exactly what I had done!

      I did it using this way:


      var result =
      sforce.apex.execute("UtilsDeeperClone","clone",{originalId:"{!Opportunity.Id}",
      chatterPost:true });

      window.open('/'+result+'/e?retURL=%2F'+result);

      Delete
  7. I did it using this way:


    var result =
    sforce.apex.execute("UtilsDeeperClone","clone",{originalId:"{!Opportunity.Id}",
    chatterPost:true });

    window.open('/'+result+'/e?retURL=%2F'+result);

    ReplyDelete
  8. Hi this tool is great!!!

    Could you help me understand how to clone child objects like OpportunityLineItem QuoteLineItem Notes and Attachments, Activities / History?

    ReplyDelete
  9. Hi Nathan,
    Nice solution!!!
    I have a requirement wherein i have to copy the record with its child objects. Have created a custom clone button.

    I have to ignore some of the child records based on some condition.

    Suppose the Parent Object is TestAcc and child Object is TestProj. TestAcc has 3 TestProjects associated with it. I want to ignore the TestProject record where the ProjectManager is same as logged in User.

    public void testclone(String dId, List childRecords, String query) {

    for(ChildRelationship child : childRecords) {
    List existingRecords = Database.query(query);
    List newRecords = existingRecords.deepClone(false);

    for(SObject obj : newRecords) {
    obj.put(child.getFieldName(), dId);
    }

    Database.SaveResult[] newData = Database.Insert(newRecords, true);//Insert newly cloned records

    }

    }

    I want to ignore the TestProject records where TestProject.ProjectManager = UserInfo.getUserId.

    could we just check the sObject’s in the newRecords list before the Database.Insert command? We could remove the TestProject record where the ProjectManager is the current user

    ReplyDelete
    Replies
    1. Checking prior to the Database.Insert command would certainly work, especially if this is a one-off.

      Delete
  10. I am using Deeper Clone functionality and it is working perfectly fine, Thank you for posting the detailed code along with test class. I wanted to find out if it is possible to include some additional text on a text field during cloning. for example if original record has a comment text field and I want to add new text "comments from previous opportunity" and also include the actual comments in a clone

    ReplyDelete
    Replies
    1. Of course, anything is possible. The way that immediately comes to mind is setting up a custom setting with sObjectType, Field, and PrecedingText as fields and when looping through the fields in the clone too, you check if it matches the sObjectType and Field requirements, and if so, add PrecedingText + originalText... That would give you more scalability if you wanted to use it for other fields in the future...

      Delete
  11. Hi Nathan,
    Thanks a lot for your reply, I did create the custom object as you suggested, but wanted to find out if I have to do any manipulation with code? if so, which class/or classes and where?

    ReplyDelete
    Replies
    1. You will need to modify code in the UtilsDeeperClone class after "System.debug('##### Clone: Before = ' + clone);" and after "System.debug('##### Clone: After = ' + clone);" during the loops. As it loops through each field, you'll want to call your list and check whether the current field is in your list and if so, append the text...

      Delete
  12. Hi Nathan,
    Thanks for your awesome post.
    Can you show an example to call it from a on-change trigger of a field "Rev__c" for the custom object lets say "sales__c".
    Thanks

    ReplyDelete
    Replies
    1. Sure. From a trigger, it would be something like:

      for (Sales__c sale : newList) {
      if (sale.Rev__c != oldMap.get(sale.Id).Rev__c) {
      UtilsDeeperClone.clone(sale.Id, true);
      }
      }

      Delete
    2. Thanks Nathan for such a quick reply.
      I have workflows designed around the custom object which fires whenever a new record is created and fires subsequently based on other conditions. I believe cloning will trigger it too. How to avoid it?any ideas?

      Delete
    3. How you could avoid the workflows is to have a custom field "TriggeredByClone__c" that you set to "true" only on the clone... Your workflow rules could then just implement logic "AND TriggeredByClone__c = false"

      Delete
    4. This comment has been removed by the author.

      Delete
    5. Thanks again Nathan for your patience . I have another question which is,While cloning the record with related list, is it possible to just keep the child with new cloned record and not with parent record?

      Delete
    6. That would take a little creativity on your part :)

      I would suggest something along the lines of:

      UtilsDeeperClone.clone(sale.Id, true);
      delete [SELECT Id FROM Child__c WHERE Sales__c = :sale.Id];

      ... that would effectively delete all the children for a specific child table after the cloning process is complete...

      Delete
    7. cool. You are a great helper. Just a general question, I have just started exploring salesforce couple of months ago, there are so many black boxes. Is there any encyclopedia of salesforce that covers in depth working of different aspects of it.

      Delete
    8. I would recommend utilizing a site like the salesforce stackexchange, which has a community of developers who are always happy to answer questions: http://salesforce.stackexchange.com/

      As far as tutorials and all that, check out https://developer.salesforce.com/ where you can find pretty indepth details about whatever you are looking for.

      More often than not, I just google something. Not sure what the new "Orders" object is all about? Just google "salesforce order object" and the right salesforce documentation:

      http://lmgtfy.com/?q=salesforce+order+object

      ... which brings up...

      https://help.salesforce.com/apex/HTViewHelpDoc?id=order_overview.htm&language=en_US

      Delete
    9. Happy to help! I hope everything you implemented is working out!

      Delete
  13. Thanks for Sharing this knowledge... & great modular programming approach (Y)

    ReplyDelete
  14. Hi Nathan,

    I am trying the same but I am getting error like required field missing Name,CloseDate. I want these 2 fields clone from OriginalId only.

    Any help.

    Regards
    Radhika.Y

    ReplyDelete
    Replies
    1. Ah ok. This tool is set up to clone everything EXCEPT what is listed in the FieldExclusion table and it looks like you are trying to use that as the list of fields to ONLY use. In that case, you'll need to make a modification:

      ... find the row that looks like:

      if (UtilsVariables.getFieldExclusions(sObjectType.getDescribe().getName()).contains(sObjectFields.get(i))) {

      ... replace it with:

      if (!UtilsVariables.getFieldExclusions(sObjectType.getDescribe().getName()).contains(sObjectFields.get(i))) {

      (the difference being one exclamation point at the beginning of UtilsVariables, which will effectively turn that "FieldExclusions" custom setting into a "FieldInclusions" custom setting...

      Delete
    2. Hi Nathan,

      I am not using that Field Exculsion custom settings. I commented that code. even though I am getting this error.

      Regards
      Rdahika.Y

      Delete
    3. In that case, please post more detailed error information, like which line is erroring and any debug info you have available. I was taking a guess based on the little information I had :)

      Delete
  15. Hi Nathan, I am using http://christopheralunlewis.blogspot.com/2012/04/clone-plus-clone-salesforce-objects.html code as i just want to clone child object and parent object but not grand child. It is working perfectly fine. But what i want is after clicking clone button it should go to standard edit page first from where record should get cloned only when user clicks save and it should not get clone if user clicks on cancel. Currently its just rediecting to detail page of record but i want to change some field values before saving. Please guide.

    ReplyDelete
    Replies
    1. Ah, this is where you'll need a very custom flow that will take some significant work outside of Christopher's (or my) solution. You'll need to create a custom visualforce page, where the save function is the cloning function, and where the id you pass in ?id=xxxxxxxx is used to populate the record you display. You'll display it by querying the record where Id=xxxxxxx then doing a .clone() then displaying the .clone() (so it's pre-save)... There isn't an easy change you can make to get this to happen...

      Delete
  16. Nice Post. Really helpful code

    ReplyDelete
  17. im getting a unexpected token error when i press the button.

    ReplyDelete
    Replies
    1. Any more clarity you can add? I'd be happy to help troubleshoot, but this doesn't really give me anywhere to start...

      Delete
  18. Hi,

    Everything working awesome. Need one thing might be it's not being covered or i'm missing.

    How can i add a dynamic value in cloned object. e.g. Source Object has a field Ext-Id and has value 123456 and now i want Ext-id PR-123456 in cloned object.

    ReplyDelete
    Replies
    1. What I would do is during cloning, have a check that says if the field is "Ext-Id" then add the PR- in front, otherwise just clone...

      if (fieldName != 'Id' && fieldMap.get(fieldName).getDescribe().isCreateable() && !UtilsVariables.getFieldExclusions(clone.getSObjectType().getDescribe().getName()).contains(fieldName)) {
      if (fieldName == 'Ext-Id') {
      clone.put(fieldName, 'PR-' + orig.get(fieldName));
      } else {
      clone.put(fieldName, orig.get(fieldName));
      }
      System.debug('##### Clone: Field Clone = ' + fieldName + ': ' + orig.get(fieldName));
      }

      Delete
  19. Also what about if i'm cloning Parent Account, how the child account will be cloned.

    What will be the relationship name in this case??

    Thanks

    ReplyDelete
    Replies
    1. The child relationship for this would be `ChildAccounts`... you can always see this by clicking on the field "Parent Account" in salesforce and look for the value in the `Child Relationship Name` field...

      Delete
  20. Thanks a lot for replying and the effort.

    One more challenge that i'm facing for the same is:

    I want to clone Parent Account and want to clone the Contacts of Child Account's Contacts in the cloned Account.

    Like : ParentAccount has ChildAccount, and ChildAccount has ChildContact1.

    Want i want is to Clone ParentAccount and in the cloned Account there should ChildContact1 in the related list of ClonedAccount.

    ReplyDelete
  21. I tried using all child relationship combinations in custom settings like:

    ChildAccounts.Contacts
    ChildAccount.Contacts
    Accounts.ChildContacts
    Account.ChildContacts

    No success yet, please help out if it can be done using custom settings or need to tweak in the code

    ReplyDelete
    Replies
    1. Simple. Just set this up in the `ChildRelationship` table:

      sObjectType: "Account"
      ChildRelationship: "Contacts"

      Delete
  22. It'll clone the contacts of level 0 contacts, i want to clone ChildAccount's Contacts in the 0th Level Account

    ReplyDelete
    Replies
    1. It should do both... There's no way to only do it for the lowest level (childaccount) only in my configuration...

      Delete
  23. Hi Nathan,

    You are a magician...Great Work!! I implemented this logic in my Dev Org. Works Perfectly..Custom buttons are not allowed in lightning detail pages So, I have implemented the button logic by using a lightning component so that I can place it on Object Detail Page in Lifghtning View.

    Thanks,
    Raviteja

    ReplyDelete
  24. This comment has been removed by the author.

    ReplyDelete
  25. Hi Nathan,

    I want to change the name of the newly cloned record using a popup window. How can this be achieved?

    Thanks in Advance

    ReplyDelete
    Replies
    1. Using my framework, I'd likely force a popup using javascript and feed the result into the following sforce.apex.execute() to be used as part of the process. I am not sure how this would work using Lightning Views.

      Delete
  26. Error while clicking on button it says :-
    {faultcode:'soapenv.Client',faultstring:'No operation is available for request {http://soap.sforce.com/schemas/package/UtilsDeeper}clone,please check the WSDL for the service',}

    ReplyDelete
    Replies
    1. were u able to resolve this if so, help me out i am facing same issue

      Delete
    2. I'm thinking you need to extend "UtilsDeeper" to be "UtilsDeeperClone" in your js button. Could you give that a shot and let me know?

      Delete
  27. Hi Nathen,

    I am trying to clone a Level 2 child, and used the format you specified with __r doesnt seems to work, can you please give some help

    thanks
    g

    ReplyDelete
    Replies
    1. Hey there, my apologies on the delayed response, I took some time away to travel the world! What exactly is the issue you are running into? I'm not sure how to help with the info you gave.

      Delete
  28. Hello, I recently found this code and found it very useful.

    One issue I did encounter is that there is that there a case where I have a parent and childObject1 that both have a look up in childObject2. Is there a way to get the code to create a clone of childObject2 and set both the lookups to the newly cloned parent and childObject1?


    Parent<-Child1
    ^_______^
    Child2

    ReplyDelete
    Replies
    1. Hey JW,

      Since you will need both set up prior, I'd do it from Child1 and then set Child2.Parent to equal Child1.Parent on clone. That's a broad-strokes explanation, let me know if you need more detail.

      Delete
  29. Hi Nathan,

    Error while clicking on button it says :-
    {faultcode:'soapenv.Client',faultstring:'No operation is available for request {http://soap.sforce.com/schemas/package/UtilsDeeper}clone,please check the WSDL for the service',}

    could you please help resolving this

    ReplyDelete
    Replies
    1. Based on where your brackets are in the UtilsDeeper}clone area there, could you double check your setup?

      Delete
  30. Hi Nathan,

    Thanks for your great effort. DeepClone works like a charm when only one child or grand child records are inserted but fails to satisfy validations while multiple child records are inserted. one more thing to add it would be of great use if it gets saved only on selection of save button.Now the new record is being generated as soon as deepclone is selected inspite of validation errors.I have a requirement to check child records and clone only those with particular field value how can i achieve this.i have around 10 child objects each of them with particular criteria to be checked while cloning.could you please help at which part of the code i can modify

    ReplyDelete
    Replies
    1. Hello Parvathi,

      Is it consistent criteria per child object? Or does it require user input (not always consistent)?

      Delete
  31. Nathan- Do you have an option that isn't a javascript button? We are working in lightning.

    ReplyDelete
    Replies
    1. Hello Olivia, I have not worked much with lightning of yet (I took some time off between jobs), but you can look here for some more details on how that might look:

      https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/controllers_server_actions_call.htm

      Let me know how it goes!

      Delete
  32. Thank you so much for the quick response! I will look into the referenced link! I am trying to get it to work in classic and am running into the following error: "sforce not defined"

    SObjectType with Relationships:
    Name, sObjectType, ChildRelationship
    Design1, Design__c, Zone__r
    Design2, Design__c, RPA_Annual_Budget__r

    Java button:
    {!requireScript("/soap/ajax/29.0/connectio.js")}
    {!requireScript("soap/ajax/29.0/apex.js")}

    sforce.apex.execute("utilDeeperClone", "clone",
    {
    originalId:'{Design__c.Id}'
    });

    ReplyDelete
    Replies
    1. Hey Olivia, a couple quick thoughts:

      - it seems you are missing an "n" at the end of your first line. It should read "connection.js" instead of "connectio.js"
      - to be safe, I'd also add a "/" before "soap" on the second line
      - you'll also need to put an "!" before "Design__c" in the execute function -- that's what tells the script to use a dynamic value instead of the literal text string you've entered

      Let me know if you are still having trouble!

      Delete
  33. Thank you Nathan- I am not getting the following error:
    rabine--fullsand.cs96.my.salesforce.com says:
    A problem with the OnClick JavaScript for this button or link was encountered:

    {faultcode:'soapenv:Client', faultstring:'No operation available for request {http://soap.sforce.com/schemas/package/UtilsDeeperClone}clone, please check the WSDL for the service.', }

    Javascript button updated:
    {!requireScript("/soap/ajax/29.0/connection.js")}
    {!requireScript("/soap/ajax/29.0/apex.js")}

    sforce.apex.execute("UtilsDeeperClone", "clone",
    {
    originalId:'{!Design__c.Id}'
    });

    Really appreciate your help!!!

    ReplyDelete
    Replies
    1. Can you ensure your UtilsDeeperClone class is properly set up and has a method named "clone" that's globally exposed?

      Delete
  34. Thank you Nathan! I was able to get this working! I was able to successfully populate 3 levels of data off of the parent account. I have another questions for you- I have a grandchild object that is also related directly to the parent account. The way my object is set up is as follows:
    Design (parent)
    Budget(Child)
    Repair(grandchild-masterdetail relationship to budget)
    Repair (Child, look up to the design)

    The reason why we set it up this way is so we can view by year or aggregate all repairs by design. The issue that I am having is when i clone the grandchild master detail it still associates with the Old design I cloned from. Is there a way to pull in the new id, maybe through field defaults? Originally i had it set up with a filter on that field so you couldn't associate a design with a repair that wasn't the same design as the budget, but to make the clone button work, I had to turn off that filter. Any ideas would be greatly appreciated!!

    Thanks again-

    ReplyDelete
    Replies
    1. Glad it's working for you!

      Personally, I'd probably write a before insert trigger that, upon insert, would automatically populate the lookup to Design with whatever the value is on the master (Budget) lookup to Design. That way, you set it automatically everywhere, regardless of new (fresh) or clone, then just exclude that field from the cloning process entirely.

      Delete
  35. Hello Nathan, my clone button is working but to clone the child and Parent object - How can I clone the child and grandchild records with a button the the parent object? Please help.

    ReplyDelete
    Replies
    1. For some reason it's not cloning my child objects

      Delete
  36. Could you please share the test class for UtilsDeeperClone class

    ReplyDelete
  37. It is not working for Grand Children ?

    ReplyDelete