Apex “Deep Clone” Controller
November 19th, 2009
I wrote the following code at the Admin To Hero App Building Workshop and it was very popular. I think we used the code on 3 or 4 projects that day so I thought I’d post it to help everyone out.
Essentially it is a Visualforce page and Apex Controller that allows you to do a “deep clone” of an object and it’s line items for a master-detail relationship. So I created a “Clone with Items” custom button on a page layout that invokes the Visualforce page that clones a purchase order header and its line items.
PurchaseOrderClone – Visualforce page
1 2 3 4 5 6 |
<apex:page standardController="Purchase_Order__c"
extensions="PurchaseOrderCloneWithItemsController"
action="{!cloneWithItems}">
<apex:pageMessages />
</apex:page> |
PurchaseOrderCloneWithItemsController – Apex controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
public class PurchaseOrderCloneWithItemsController {
//added an instance varaible for the standard controller
private ApexPages.StandardController controller {get; set;}
// add the instance for the variables being passed by id on the url
private Purchase_Order__c po {get;set;}
// set the id of the record that is created -- ONLY USED BY THE TEST CLASS
public ID newRecordId {get;set;}
// initialize the controller
public PurchaseOrderCloneWithItemsController(ApexPages.StandardController controller) {
//initialize the stanrdard controller
this.controller = controller;
// load the current record
po = (Purchase_Order__c)controller.getRecord();
}
// method called from the VF's action attribute to clone the po
public PageReference cloneWithItems() {
// setup the save point for rollback
Savepoint sp = Database.setSavepoint();
Purchase_Order__c newPO;
try {
//copy the purchase order - ONLY INCLUDE THE FIELDS YOU WANT TO CLONE
po = [select Id, Name, Ship_To__c, PO_Number__c, Supplier__c, Supplier_Contact__c, Date_Needed__c, Status__c, Type_of_Purchase__c, Terms__c, Shipping__c, Discount__c from Purchase_Order__c where id = :po.id];
newPO = po.clone(false);
insert newPO;
// set the id of the new po created for testing
newRecordId = newPO.id;
// copy over the line items - ONLY INCLUDE THE FIELDS YOU WANT TO CLONE
List<Purchased_Item__c> items = new List<Purchased_Item__c>();
for (Purchased_Item__c pi : [Select p.Id, p.Unit_Price__c, p.Quantity__c, p.Memo__c, p.Description__c From Purchased_Item__c p where Purchase_Order__c = :po.id]) {
Purchased_Item__c newPI = pi.clone(false);
newPI.Purchase_Order__c = newPO.id;
items.add(newPI);
}
insert items;
} catch (Exception e){
// roll everything back in case of error
Database.rollback(sp);
ApexPages.addMessages(e);
return null;
}
return new PageReference('/'+newPO.id+'/e?retURL=%2F'+newPO.id);
}
} |
TestPurchaseOrderCloneWithController – Test class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
@isTest
private class TestPurchaseOrderCloneWithController {
static testMethod void testPOCloneController() {
// setup a reference to the page the controller is expecting with the parameters
PageReference pref = Page.PurchaseOrderClone;
Test.setCurrentPage(pref);
// setup a ship to account
Account shipTo = new Account();
shipTo.Name = 'PSAV 6FOO';
shipTo.Type = 'Supplier';
insert shipTo;
// create new po record
Purchase_Order__c po = new Purchase_Order__c();
po.Date_Needed__c = Date.newInstance(2020,01,01);
po.Ship_To__c = shipTo.id;
insert po;
// create a line item for the po
Purchased_Item__c pi1 = new Purchased_Item__c();
pi1.Description__c = 'My item';
pi1.Purchase_Order__c = po.id;
pi1.Quantity__c = 1;
pi1.Unit_Price__c = 10;
insert pi1;
// Construct the standard controller
ApexPages.StandardController con = new ApexPages.StandardController(po);
// create the controller
PurchaseOrderCloneWithItemsController ext = new PurchaseOrderCloneWithItemsController(con);
// Switch to test context
Test.startTest();
// call the cloneWithItems method
PageReference ref = ext.cloneWithItems();
// create the matching page reference
PageReference redir = new PageReference('/'+ext.newRecordId+'/e?retURL=%2F'+ext.newRecordId);
// make sure the user is sent to the correct url
System.assertEquals(ref.getUrl(),redir.getUrl());
// check that the new po was created successfully
Purchase_Order__c newPO = [select id from Purchase_Order__c where id = :ext.newRecordId];
System.assertNotEquals(newPO, null);
// check that the line item was created
List<Purchased_Item__c> newItems = [Select p.Id From Purchased_Item__c p where Purchase_Order__c = :newPO.id];
System.assertEquals(newItems.size(),1);
// Switch back to runtime context
Test.stopTest();
}
} |
Categories: Apex, Code Sample, Salesforce, Visualforce













This is great, thanks for sharing.
How difficult would it be to extend this to deep clone Opportunities?
@Don, it’s essentially the same concept. Here are some details: http://community.salesforce.com/t5/Apex-Code-Development/Trigger-to-clone-opportunity-and-opportunity-products/m-p/117353
Hi,
Im getting only 84% test coverage. I want 100%. The problem code is:
catch (Exception e){
// roll everything back in case of error
Database.rollback(sp);
ApexPages.addMessages(e);
return null;
Any suggestions?
@Neil, you’ll probably want to set up a test case that throws an error and asserts that the records were rolled back (i.e., query for the records and assert that none were found) and assert that the page messages contained the specified error from the Exception.
Jeff,
Just want to say thanks for sharing — I came across this page (w/source code), and with a little modification was able to re-purpose to knock of an item on my to-do list.
S
@Stephan, glad to hear it helped!
Hi Jeff,
Hi,
We need to clone “Open Activities” of Campaign when a particular Campaign is cloned.
Could you assist me on how the code can be leveraged for this feature?
Thanks,
Vimal
@Vimal, the code should be very similar to my example code. You would write a controller that queries for all open activities for a campaign and moves/copies them to the new campaign.
@Jeff this is great…it looks like it would almost work for me…i am trying to clone a price book and all its related price book entries (which number up to 2100+)…with your code i know i would hit the governor limits…can you think of a way i can do this without using @future? basically need to do more in the code after this is done…and with @future, i cant do that.
@Travis, you’ll probably want to use a SOQL For Loop to process these records.
Thanks for the post. We are looking to do a similar master-detail deep clone feature, and my understanding is that you have to query all field that you want to copy both for the master and for the details. This is a bit concerning, since we have a detail object with a large field list. The maintenance of this seems somewhat time consuming. Is there any way to grab all fields in the master and detail objects without explicitly querying for them?
Kevin, you should look at Apex Describe feature and see if you can build that list dynamically and use Dynamic SOQL for the query.
Can this be re purposed to clone the related records of another related object?
Specifically, I have a template of related records related to a parent (Object 1). I want to relate that parent record (Object 1 and Object 2) and copy the related records into another related list of records that is a child of Object 2.
thank you for any input you can provide
Great Post Jeff, This is very helpful for my application.
I have one more question, can we do deeper clone up to three levels?
If so do we hit the governor limits for SOQL Queries?
Hi Jeff, Thanks for your posts, I always find them very useful. I have shamelessly copied your code to custom clone opportunities however, the savepoint is not working for me. If my user clicks Cancel, they are returned to the original record but there is still a clone of the opportunity. Any ideas what I could be missing? Thanks!
Hi Jeff,
Can we extend this code to do the deeper clone like
Account –> opportunity –> opportunitylineitems
Sure! It shouldn’t be a problem. Just add the additional query once you create the opportunity.
Jeff,
Thank you for your efforts and the education. I love reading and learning from these posts. I’m needing to clone opportunities and a custom related object (not master-detail but rather a lookup relationship). It seems that this method would work well for that. Anything I’m missing?
Check this out for easy deep cloning:
http://blogs.developerforce.com/developer-relations/2009/02/cloning-objects-in-salesforcecom.html
Hi Jeff, thanks for the code.The problem I am facing is after clicking the clone button a new record is created even if i click cancel button also this is because of the statement insert newPO but I am not getting how to solve this problem.Can you please help me…
@Subha, this is intended functionality. If you want to have them preview the new record before creating it then you will have to write a controller and Visulforce page that preloads all of the info, allows them to make edits and then saves the record.
Hi Jeff,
I have utilized your deep clone functionality to fit our organizations needs.
Basically we have the following scenario :
We have a Quote Request, which has Vehicle Models related to it and you have accesories related to the models.
Below is the code we are using. The issue we are facing is if a quote has more than one Model it fails with the error :
Record ID: cannot specify Id in an insert call
The Code works if there is 1 model with 1 or more accessories attached to it. The issue comes in when there is 2 or more vehicle models attached to a quote.
Is there any solution to the above issue we are facing?
===============================================
public class QuotationRequestCloneWithItemsController {
//added an instance varaible for the standard controller
private ApexPages.StandardController controller {get; set;}
// add the instance for the variables being passed by id on the url
private Quotation_Request__c qReq {get;set;}
// set the id of the record that is created — ONLY USED BY THE TEST CLASS
public ID newRecordId {get;set;}
// initialize the controller
public QuotationRequestCloneWithItemsController(ApexPages.StandardController controller) {
//initialize the stanrdard controller
this.controller = controller;
// load the current record
qReq = (Quotation_Request__c)controller.getRecord();
}
// method called from the VF’s action attribute to clone the Quote request
public PageReference cloneWithItems() {
// setup the save point for rollback
Savepoint sp = Database.setSavepoint();
Quotation_Request__c newqReq;
try {
//copy the quote request – ONLY INCLUDE THE FIELDS YOU WANT TO CLONE
qReq = [select Account__c,
Arrears_Deal__c,
Call_Report__c,
Call_Report_Vehicle__c,
Canopies_Comments__c,
Canopies_Door__c,
Canopies_Material__c,
Canopies_Sliding_Windows__c,
Canopies_Windows__c,
Checked__c,
Contact_Person__c,
Customer__c,
Customer_Nominated_Accessory_Supplier__c,
Customer_Nominated_Supplier__c,
Date__c,
Deposit_Ex_VAT__c,
FAM__c,
Fuel_Cards__c,
Insurance__c,
Interest_Rate__c,
Lead__c,
License__c,
Medical_Aid_Kit__c,
Nominated_Residual__c,
Personalised_Number_Plates__c,
Product__c,
Prospects__c,
Quotation_Pricing_Method__c,
Name,
RV__c,
ID,
Registered_Driver_Proxy__c,
Supplier_Name_Accessory__c,
Supplier_Name__c,
System_Parameters__c,
Tracking_Unit__c,
Tracking_Unit_Installation__c,
Tracking_Unit_Subscription__c,
Value_Add_Comment__c
from Quotation_Request__c where id = :qReq.id];
newqReq = qReq.clone(false);
insert newqReq;
//set the id of the new Quote Request to be inserted
newRecordId = newqReq.id;
// copy over the models – ONLY INCLUDE THE FIELDS YOU WANT TO CLONE —
List Models = new List();
List ModelAccessory = new List();
for (Quote_Model__c qm : [Select Models__c,
name,
make_description__c,
X1_Contract_Months__c,
X1_KM__c,
X2_Contract_Months__c,
X2_KM__c,
X3_Contract_Months__c,
X3_KM__c,
X4_Contract_Months__c,
X4_KM__c,No_of_Units__c,
m_m_number__c,
Required_Delivery_Date__c,
Delivery_Province__c,
Availability__c,
Required_Delivery_Area__c,
Comments__c,
Discontinued_Date__c,
Barred_Date__c
from Quote_Model__c
where Quotation_Request__c = :qReq.id])
{
Quote_Model__c newQM = qm.clone(false);
newQM.Quotation_Request__c = newqReq.id;
Models.add(newQM);
insert Models;
//get accessories per model
for (Accessory__c qmAcc : [SELECT Accessory_Number__c,
Canopies_Comments__c,Canopies_Door__c,
Quotation_Request__c,Quote_Model__c,
Accessory_Type__c,
Accessory_Details__c,
Accessory_Comments__c,
item_selection__c,
Other_Accessories__c FROM Accessory__c
where Quote_Model__c = :qm.id])
{
Accessory__c clonedAccessory = qmAcc.clone(false);
clonedAccessory.Quote_Model__c = newQM.id;
ModelAccessory.add(clonedAccessory);
}
}
insert ModelAccessory;
} catch (Exception e){
// roll everything back in case of error
Database.rollback(sp);
ApexPages.addMessages(e);
return null;
}
return new PageReference(‘/’+newqReq.id+’/e?retURL=%2F’+newqReq.id);
}
}