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; } } }
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!
Hi Nathan,
ReplyDeleteNice 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
Hi Christopher,
DeleteThanks 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.
I get an error when i click the cloneRecord button it says
Delete{1faultcode:soap:Client fault string no operation available for request UtilsDeeperClone clone. Please check the wsdl
You right!
ReplyDeleteHe 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"
Did you delete this from the top of the button script?
Delete{!requireScript("/soap/ajax/29.0/connection.js")}
{!requireScript("/soap/ajax/29.0/apex.js")}
if so... add it back to the top...
Are you sure you correctly input the child object(s) in the custom setting?
DeleteFor 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...
DeleteDear Nathan,
DeleteI 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.
Should be:
DeleteName: "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?
Hi Nathan,
DeleteI have changed as you mention. but still not cloning the ChildRelatioinship. just cloning the Master object.
Maybe i missed anything else?
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)...
DeleteWhat is the "StageName" in the "FieldDefault"? in step # 8.
ReplyDelete"StageName" is irrelevant here, it has to do with Opportunity table only...
DeleteHi,
ReplyDeleteGreat 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 :)
Hi Prateek,
DeleteHave 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...
Hi Prateek,
DeleteIt's just a typo, change the name of the class in the button from utilDeeperClone to utilsDeeperClone.
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.
DeleteBizarre, but I'm glad it's working for you. I've never had any issues using "Id originalId"...
DeleteWhat 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!
DeleteDid you try what Prateek said also helped work, which is changing to "String" and back to "Id":
DeleteI 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.
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!!!
DeleteBizarre, 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...
DeleteJust 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.
DeleteThanks for sharing, Prateek, and I'm glad you got it all working!
DeleteThis comment has been removed by the author.
DeleteHi 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?
DeleteHi Nathan,
ReplyDeleteI 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.
Hey Sat,
DeleteI 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...
Also is it possible to redirect to the cloned record at the end of the process ?
ReplyDeleteHi Sat,
DeleteWere you able to achieve this?
Hey Sat,
DeleteIt 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);
I did it using this way:
ReplyDeletevar result =
sforce.apex.execute("UtilsDeeperClone","clone",{originalId:"{!Opportunity.Id}",
chatterPost:true });
window.open('/'+result+'/e?retURL=%2F'+result);
Hi this tool is great!!!
ReplyDeleteCould you help me understand how to clone child objects like OpportunityLineItem QuoteLineItem Notes and Attachments, Activities / History?
Hi Nathan,
ReplyDeleteNice 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
Checking prior to the Database.Insert command would certainly work, especially if this is a one-off.
DeleteI 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
ReplyDeleteOf 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...
DeleteHi Nathan,
ReplyDeleteThanks 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?
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...
DeleteHi Nathan,
ReplyDeleteThanks 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
Sure. From a trigger, it would be something like:
Deletefor (Sales__c sale : newList) {
if (sale.Rev__c != oldMap.get(sale.Id).Rev__c) {
UtilsDeeperClone.clone(sale.Id, true);
}
}
Thanks Nathan for such a quick reply.
DeleteI 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?
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"
DeleteThis comment has been removed by the author.
DeleteThanks 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?
DeleteThat would take a little creativity on your part :)
DeleteI 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...
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.
DeleteI 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/
DeleteAs 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
Thanks again Nathan.
DeleteHappy to help! I hope everything you implemented is working out!
DeleteThanks for Sharing this knowledge... & great modular programming approach (Y)
ReplyDeleteThis comment has been removed by the author.
ReplyDeleteHi Nathan,
ReplyDeleteI 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
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:
Delete... 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...
Hi Nathan,
DeleteI am not using that Field Exculsion custom settings. I commented that code. even though I am getting this error.
Regards
Rdahika.Y
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 :)
DeleteHi 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.
ReplyDeleteAh, 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...
DeleteNice Post. Really helpful code
ReplyDeleteim getting a unexpected token error when i press the button.
ReplyDeleteAny more clarity you can add? I'd be happy to help troubleshoot, but this doesn't really give me anywhere to start...
DeleteHi,
ReplyDeleteEverything 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.
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...
Deleteif (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));
}
Also what about if i'm cloning Parent Account, how the child account will be cloned.
ReplyDeleteWhat will be the relationship name in this case??
Thanks
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...
DeleteThanks a lot for replying and the effort.
ReplyDeleteOne 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.
I tried using all child relationship combinations in custom settings like:
ReplyDeleteChildAccounts.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
Simple. Just set this up in the `ChildRelationship` table:
DeletesObjectType: "Account"
ChildRelationship: "Contacts"
It'll clone the contacts of level 0 contacts, i want to clone ChildAccount's Contacts in the 0th Level Account
ReplyDeleteIt should do both... There's no way to only do it for the lowest level (childaccount) only in my configuration...
DeleteHi Nathan,
ReplyDeleteYou 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
This comment has been removed by the author.
ReplyDeleteHi Nathan,
ReplyDeleteI want to change the name of the newly cloned record using a popup window. How can this be achieved?
Thanks in Advance
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.
DeleteError while clicking on button it says :-
ReplyDelete{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',}
were u able to resolve this if so, help me out i am facing same issue
DeleteI'm thinking you need to extend "UtilsDeeper" to be "UtilsDeeperClone" in your js button. Could you give that a shot and let me know?
DeleteHi Nathen,
ReplyDeleteI 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
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.
DeleteHello, I recently found this code and found it very useful.
ReplyDeleteOne 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
Hey JW,
DeleteSince 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.
Hi Nathan,
ReplyDeleteError 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
Based on where your brackets are in the UtilsDeeper}clone area there, could you double check your setup?
DeleteHi Nathan,
ReplyDeleteThanks 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
Hello Parvathi,
DeleteIs it consistent criteria per child object? Or does it require user input (not always consistent)?
Nathan- Do you have an option that isn't a javascript button? We are working in lightning.
ReplyDeleteHello 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:
Deletehttps://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/controllers_server_actions_call.htm
Let me know how it goes!
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"
ReplyDeleteSObjectType 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}'
});
Hey Olivia, a couple quick thoughts:
Delete- 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!
Thank you Nathan- I am not getting the following error:
ReplyDeleterabine--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!!!
Can you ensure your UtilsDeeperClone class is properly set up and has a method named "clone" that's globally exposed?
DeleteThank 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:
ReplyDeleteDesign (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-
Glad it's working for you!
DeletePersonally, 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.
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.
ReplyDeleteFor some reason it's not cloning my child objects
DeleteCould you please share the test class for UtilsDeeperClone class
ReplyDeleteIt is not working for Grand Children ?
ReplyDelete