Controller:
public class ClonePlusController { public List<relatedObjects> objectChildren { get; set; } public String objectTypeName { get; set; } public String objectName { get; set; } private SObject headSObject, headClone; // Initialisation method called when the clone plus page is loaded. // Use the id page parameter to find out what object type to clone. // Then load the object from the database. // Finally call the populateObjectChildren method to public void initialiseObjectsForCloning() { // Here we generate a keyprefixmap using the global describe // Then compare that to our object to determine type. Map<String, Schema.SObjectType> gd = Schema.getGlobalDescribe(); Map<String,String> keyPrefixMap = new Map<String,String>{}; for(String sObj : gd.keySet()){ Schema.DescribeSObjectResult r = gd.get(sObj).getDescribe(); keyPrefixMap.put(r.getKeyPrefix(), r.getName()); } String objectID = ApexPages.currentPage().getParameters().get('id'); String objectTypeKey = objectId.subString(0,3); objectTypeName = keyPrefixMap.get(objectTypeKey); String primaryObjectQueryString = 'SELECT Id, Name FROM ' + objectTypeName + ' WHERE Id = \'' + objectId + '\''; headSObject = Database.query(primaryObjectQueryString); objectName = '' + headSObject.get('Name'); populateObjectChildren(); } // Get all of the children of the current object that have a object // type contained in the child object types page parameter. // Not restricting the child objects to particular types results in // many uncloneable system objects being added to the possibiilites. // Making these object type choices also allows us // pick and chose the specific kinds of objects we want to clone. public void populateObjectChildren() { objectChildren = new List<relatedObjects>{}; Set<String> childObjectTypes = new Set<String>{}; // read the object types from the page parameter. childObjectTypes.addAll( ApexPages.currentPage().getParameters() .get('childobjecttypes').split(',') ); // Use the sobjecttype describe method to retrieve all // child relationships for the object to be cloned. Schema.DescribeSObjectResult headDescribe = headsObject.getSObjectType().getDescribe(); List<Schema.ChildRelationship> childRelationships = headDescribe.getChildRelationships(); // Iterate through each relationship, retrieve related objects. for (Schema.ChildRelationship childRelationship : childRelationships) { Schema.SObjectType childObjectType = childRelationship.getChildSObject(); // Only retrieve the objects if their type is // one of those we are interested in. if (childObjectTypes.contains( childObjectType.getDescribe().getName())) { List<relatedObjectRow> relatedObjects = new List<relatedObjectRow>{}; Schema.SObjectField childObjectField = childRelationship.getField(); String relatedChildSObjectsquery = 'SELECT ID FROM ' + childObjectType.getDescribe().getName() + ' WHERE ' + childObjectField.getDescribe().getName() + ' = \'' + headsObject.Id + '\''; for (SObject childObject : Database.query(relatedChildSObjectsquery)) { relatedObjects.add(new relatedObjectRow(childObject)); } if (!relatedObjects.isEmpty()) { objectChildren.add(new relatedObjects(relatedObjects, childObjectType.getDescribe().getLabelPlural(), childObjectField.getDescribe().getName())); } } } } // Perform the cloning process. // First clone the parent, then all of the child objects. // Then redirect the user to the new object page. public PageReference doClone() { headClone = cloneObjects(new List<sObject>{headSObject}).get(0); insert headClone; cloneSelectedObjects(); return new PageReference('/' + headClone.Id); } // Clone the selected child objects. // Associate the cloned objects with the new cloned parent object. public void cloneSelectedObjects() { List<sObject> clonedObjects = new List<sObject>{}; List<sObject> selectedRelatedObjects; for (relatedObjects relatedObject : objectChildren) { selectedRelatedObjects = new List<sObject>{}; clonedObjects = new List<sObject>{}; for (relatedObjectRow row : relatedObject.objectRows) { if (row.selected) { selectedRelatedObjects.add(row.obj); } } if (!selectedRelatedObjects.isEmpty()) { clonedObjects = cloneObjects(selectedRelatedObjects); for (sObject clone : clonedObjects) { clone.put(relatedObject.relatedFieldName, headClone.Id); } insert clonedObjects; } } } // Clone a list of objects to a particular object type // Parameters // - List<sObject> sObjects - the list of objects to be cloned // The sObjects you pass in must include the ID field, // and the object must exist already in the database, // otherwise the method will not work. public static List<sObject> cloneObjects(List<sObject> sObjects){ Schema.SObjectType objectType = sObjects.get(0).getSObjectType(); // A list of IDs representing the objects to clone List<Id> sObjectIds = new List<Id>{}; // A list of fields for the sObject being cloned List<String> sObjectFields = new List<String>{}; // A list of new cloned sObjects List<sObject> clonedSObjects = new List<sObject>{}; // Get all the fields from the selected object type using // the get describe method on the object type. if(objectType != null) { for (Schema.SObjectField objField : objectType.getDescribe().fields.getMap().values()) { Schema.DescribeFieldResult fieldDesc = objField.getDescribe(); // If the field type is location, then do not include it, // otherwise it will cause a soql exception. // Note that excluding the field does not stop the location from // being copied to the new cloned object. if(fieldDesc.getType() != DisplayType.LOCATION) { sObjectFields.add(fieldDesc.Name); } } } // If there are no objects sent into the method, // then return an empty list if (sObjects != null || sObjects.isEmpty() || sObjectFields.isEmpty()){ // Strip down the objects to just a list of Ids. for (sObject objectInstance: sObjects){ sObjectIds.add(objectInstance.Id); } /* Using the list of sObject IDs and the object type, we can construct a string based SOQL query to retrieve the field values of all the objects.*/ String allSObjectFieldsQuery = 'SELECT ' + String.join(sObjectFields,','); allSObjectFieldsQuery += ' FROM ' + objectType.getDescribe().getName() + ' WHERE ID IN (\'' + sObjectIds.get(0) + '\''; for (Integer i=1 ; i < sObjectIds.size() ; i++){ allSObjectFieldsQuery += ', \'' + sObjectIds.get(i) + '\''; } allSObjectFieldsQuery += ')'; system.debug('allSObjectFieldsQuery: ' + allSObjectFieldsQuery); try{ // Execute the query. For every result returned, // use the clone method on the generic sObject // and add to the collection of cloned objects for (SObject sObjectFromDatabase: Database.query(allSObjectFieldsQuery)){ clonedSObjects.add(sObjectFromDatabase.clone(false,true)); } } catch (exception e){ } } return clonedSObjects; } // Related objects data construct - // used to store a collection of child objects // connected to the head object through // the same relationship field. public class relatedObjects { public List<relatedObjectRow> objectRows { get; set; } public String pluralLabel { get; set; } public String relatedFieldName { get; set; } public relatedObjects(List<relatedObjectRow> objectRows, String pluralLabel, String relatedFieldName) { this.objectRows = objectRows; this.pluralLabel = pluralLabel; this.relatedFieldName = relatedFieldName; } } // An indidual child object row. // It simple contains the object definition, // and a checkbox to select the row for cloning // on the clone plus page. public class relatedObjectRow { public sObject obj { get; set; } public Boolean selected { get; set; } public relatedObjectRow(Sobject obj) { this.obj = obj; // All object rows are selected by default. this.selected = true; } public String getId(){ try{ return '' + obj.get('Id'); } catch (Exception e){ return ''; } } } }
Page:
<apex:page controller="ClonePlusController" action="{!initialiseObjectsForCloning}"> <apex:sectionHeader title="Clone Plus: {!objectName}"/> <apex:form id="theform" > Please select the child objects you would like to clone.<br/><br/> <apex:repeat value="{!objectChildren}" var="child"> <apex:PageBlock title="{!child.pluralLabel}"> <apex:pageBlockTable value="{!child.objectRows}" var="objectRow"> <apex:column headerValue="Clone" width="10%"> <apex:inputCheckbox value="{!objectRow.selected}"/> </apex:column> <apex:column headerValue="Id" value="{!objectRow.Id}" width="45%"/> </apex:pageBlockTable> </apex:PageBlock> </apex:repeat> <apex:PageBlock > <apex:commandButton action="{!doClone}" value="Clone"/> </apex:PageBlock> </apex:form> </apex:page>
At this current point in time, my plan is to develop the next release of clone plus on the AppExchange as a free app, with a much better management facility, so you can control what you are cloning much more effectively. I'll keep you posted!
Hi,
ReplyDeletethis is realy cool thing thank you. Want to add that this peace of code can be replaced with join() method of String.
String allSObjectFieldsQuery = 'SELECT ' + sObjectFields.get(o);
for (Integer i=1 ; i < sObjectFields.size() ; i++){
allSObjectFieldsQuery += ', ' + sObjectFields.get(i);
}
ACE! nice one dimmys, I had no idea that method existed, certainly makes adding list values to a string easy, great find.
DeleteThanks for sharing!
Chris
Oh btw, I have added this method to the code example above where you suggested.
DeleteYes, I also had no idea about it, until my friend drew my eye on it. ;)
Deletei get the error when cloning:
ReplyDeleteSystem.UnexpectedException: No such column 'geo_location__c' on entity 'Account'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.
Error is in expression '{!doClone}' in component in page cloneplus
Class.ClonePlusController.cloneObjects: line 224, column 1
Class.ClonePlusController.doClone: line 117, column 1
can you help in any way -- all i need to do it clone the standard object "Account" and make it a custom object to be named "Network" -- all i need is all the fields. Please someone help
Hello Harry,
DeleteSorry for the late reply, as the error message suggests, it is trying to process a column called geo_location__c on account, is this field on your Network object?
Also, before we go any further, can I ask why you are trying to copy data between the two object types?
It still is not showing any related objects on the opportunity. If you go ahead an click clone it will clone the opportunity but none of the related list records. Any ideas why?
ReplyDeleteThanks,
Amanda
I got it. I was having a brain blip and named the Opportunity Products by their setup name instead of their API Name.
ReplyDeleteHi Amanda,
DeleteGlad to see you sorted out the issue. Unfortunately the object name is simply passed as text argument, and at the moment just ignores the child name if it does not exist, I'll add some error messages into the next update!
Any more problems let me know!
Hey Christopher,
ReplyDeleteMy org had a requirement to clone all levels of a structure, so I took your idea and ran with it. I also added in some of my own magic, such as auto-posting a chatter message to the original sObject stating where it came from! Check it out:
Deeper Clone: Clone Salesforce objects with all levels of children
Take any record and clone not only it, but all its children, grand-children, great-grand-children, etc...
http://nwilliamsscu.blogspot.com/2013/09/deeper-clone-clone-salesforce-objects.html
Happy coding!
Thanks Chris. The solution helped me to save a lot of time for my SF users.
ReplyDeleteHi Christopher,
ReplyDeleteThanks for this code..just one ask for me...is there an easy way to add ' - CLONE' to the name field of the cloned object? We need to identify that its a clone so it can be easily identified and then edited.
p.s. Meaning Primary Object
DeleteAs (Ray Ban Outlet Store) soon as you have walk-through the door, You can be shoulder blades towards a military with of females excessively with glassed little eyes and as a result hands filled up with delicacies. You will also (Michael Kors Outlet Store) find an occasional call wife stashed with the website's corners, Continuing to keep a container looking just a bit confused. But if you love competitive, Cool engagement rings(And that we tend to might) Together with showy (Ray Ban Outlet) sequined purses and handbags(And this also our organization conduct), You have to fighting with each other why (Cheap Jordan Shoes Websites) all throngs of people makes it worth while.
ReplyDeleteYou should possibly not even look at to take care of the (Michael Kors Outlet Online) culprit over the past. We can undoubtedly search for tp (Cheap Yeezy Shoes Sale) prepare (Jordan Shoes For Sale Online) the culprit within the past. We want to definitely appear for to attach the culprit within the past. Head lines of late would certainly center over
Thank you for introducing this tool. keep it updated.
ReplyDeleteLearn Salesforce CPQ
Salesforce CPQ Online Training