Lead Related List to Account/Contact/Opportunity Mapping

Salesforce have a dubious reputation for focusing on things new and shiny to the detriment of improving and fixing core functionality. One of these within Sales Cloud is its inability for custom object related records to take the journey to the new Account, Contact and Opportunity records after the lead has been converted. You create records within custom objects against your Leads, and naturally you want to retain this information when they become a fully-fledged customer, but on Salesforce, all this valuable data is stuck on your Lead and not easily accessible. This is frustrating for many users who need to retain information gathered during the Lead process.

In defence of Salesforce, it can never be as easy as simply giving users the option within setup to select which objects should move to the new Account, Contact or Opportunity record. It’s also going to need to know which fields to use to connect to those objects, and if there are multiple lookups for any of these, things become even more unclear. So why not allow the user to specify the fields?

Let’s fix it ourselves then

Yes, let’s. We need a custom object to hold the API names of the object we want to map as well as the field names of the Lead, Account, Contact and Opportunity lookups. Our custom object will have one record per object we want to take the journey from Lead to the converted records. Then, we need some code to run on the Lead trigger when the Lead is converted which will check all the records in our custom object and use this information to find all the records associated with our Lead, then populate the various lookup fields with the newly created ID(s). The Lead field should be required, but the Account, Contact and Opportunity fields will be optional, as sometimes you want to connect your related records to only one or two of these.

OK, makes sense – how can I set this up for myself?‚Äč

There is just one step you’ll ideally amend for yourself, and that’s the part of the test code which creates a record on your custom object. It’s not tricky, but if coding really makes you break out in a cold sweat, you can create a simple custom object instead and use my test code. I’ll take you through every step. If you’re an accomplished coder, you’ll breeze through all of this; for you, this will be a time-saver and you can take the provided code and integrate it with your existing classes rather than putting all the code into the trigger. I’ve deliberately kept it simple – I didn’t want to turn this into a tutorial on how to run code from a trigger; the important thing was to get something that worked for everyone as easily as possible.

Step 1: Create the custom object – Lead Convert Object

First, dive into setup and create an object like below. Rather than do it after, it’s also a good idea to create a tab at this point by clicking the “Launch New Custom Tab Wizard after saving this custom object” box at the bottom. Finally, click Save.

Now that you’ve saved your object and created a tab, you’ll need some fields to hold the API names of your Lead, Account, Contact and Opportunity lookup fields. Create these as below:

Field LabelField NameData Type
Account Lookup Field NameAccount_Lookup_Field_Name__cText(80)
Contact Lookup Field NameContact_Lookup_Field_Name__cText(80)
Opportunity Lookup Field NameOpportunity_Lookup_Field_Name__cText(80)
Lead Lookup Field NameLead_Lookup_Field_Name__cText(80)

Step 2: Create Lead trigger code

Now it’s time to write the trigger code. Start up the Developer Console, which you’ll find on the Gear icon at the top, assuming you have the privileges. Click File > New > Apex Trigger. In the Name field, type “LeadTrigger”, and in the sObject field, select “Lead”. Finally, remove all the text and replace with the code below.

trigger LeadTrigger on Lead (before update) {
    
    // Create Maps so we can easily get the Account, Contact and Opportunity IDs via the Lead ID
    Map<ID,ID> LeadAccMap = new Map<ID,ID>();
    Map<ID,ID> LeadContactMap = new Map<ID,ID>();
    Map<ID,ID> LeadOppMap = new Map<ID,ID>();

    for(Lead l :trigger.new){
        if(l.IsConverted){
            LeadAccMap.put(l.id,l.ConvertedAccountId);
            LeadContactMap.put(l.id,l.ConvertedContactId);
            LeadOppMap.put(l.id,l.ConvertedOpportunityId);
        }
    }

    // Get a list of all the objects to be moved to the new Account and/or Contact records
    List<Lead_Convert_Object__c> lcoList = new List<Lead_Convert_Object__c>([
        SELECT id,Name,Account_Lookup_Field_Name__c,Contact_Lookup_Field_Name__c,Opportunity_Lookup_Field_Name__c,Lead_Lookup_Field_Name__c
        FROM Lead_Convert_Object__c
    ]);
    
    // Create a string containing all the Lead IDs to be used in IN clause of the queries
    // Typically there will be just one, but we should keep everything batchable
    List<ID> LeadList = new List<ID>(LeadAccMap.keySet());
    string keysetin = '(\'' + string.join(LeadList,'\',\'') + '\')';
    
    // Get all the records to be updated - a series of SELECT statements from each of the objects
    // One of the rare occasions we can't avoid DML within the loop as we can't query multiple objects with one statement
    List<sObject> UpdateList = new List<sObject>();
    for(Lead_Convert_Object__c lco :lcoList){
        string sqlfields = 'id,' + lco.Lead_Lookup_Field_Name__c;
    	if(lco.Account_Lookup_Field_Name__c != null) sqlfields += ',' + lco.Account_Lookup_Field_Name__c;
        if(lco.Contact_Lookup_Field_Name__c != null) sqlfields += ',' + lco.Contact_Lookup_Field_Name__c;
        if(lco.Opportunity_Lookup_Field_Name__c != null) sqlfields += ',' + lco.Opportunity_Lookup_Field_Name__c;
        string sql='SELECT ' + sqlfields
            	+ ' FROM ' + lco.Name 
            	+ ' WHERE ' + lco.Lead_Lookup_Field_Name__c + ' IN ' + keysetin;
        List<sObject> ConvertList = database.query(sql);
        for(sObject rec :ConvertList){
            ID LeadId = (ID)rec.get(lco.Lead_Lookup_Field_Name__c);
            if(lco.Account_Lookup_Field_Name__c != null){
                ID AccId = LeadAccMap.get(LeadId);
                rec.put(lco.Account_Lookup_Field_Name__c,AccId);
            }
            if(lco.Contact_Lookup_Field_Name__c != null){
                ID ContactId = LeadContactMap.get(LeadId);
                rec.put(lco.Contact_Lookup_Field_Name__c,ContactId);
            }
            if(lco.Opportunity_Lookup_Field_Name__c != null){
                ID OppId = LeadOppMap.get(LeadId);
                rec.put(lco.Opportunity_Lookup_Field_Name__c,OppId);
            }
        }
        UpdateList.addAll(ConvertList);
    }
    update UpdateList;
}

Step 3: Create a test object

This step will let us test the whole process and will also allow us to get 100% code coverage on the test class later. If you really know what you’re doing, you can use one of your own objects instead, which you currently link to both Leads and one or more of Accounts, Contacts or Opportunities. If you do this, you’ll need to make a small amendment to the test code further down so that it uses your object rather than this test one.

So, create your new object like below. This time, you won’t need a tab as you’ll be able to create your test from the related list on the Lead. Follow this up by adding Lead, Account, Contact and Opportunity lookup fields. During the usual wizard process, add the related lists to their respective page layouts.

Field LabelField NameData Type
AccountAccount__cLookup (Account)
ContactContact__cLookup (Contact)
OpportunityOpportunity__cLookup (Opportunity)
LeadLead__cLookup (Lead)

Step 4: Let’s test!

First thing to do is to go to your new Lead Convert Object tab and create a new record:
Object name: Sticky__c
Lead Lookup Field Name: Lead__c
Account Lookup Field Name: Account__c
Contact Lookup Field Name: Contact__c
Opportunity Lookup Field Name: Opportunity__c

If you’re using your own object, you’ll use the API names for your object and respective Lead and Account/Contact/Opportunity lookup fields.

Now, create a Lead as normal, save it, then go to your new Sticky related list and create a new record. The Lead field will already be populated, so just type some text in the Sticky Name field and save (you should leave the Account, Contact and Opportunity lookup fields blank – these will be auto-populated by the new process). Finally, convert your lead. When you go to your newly created records, take a look at the Sticky related list. All being well, your record has followed the conversion process and is sitting right there.

When you come to apply this to your own objects, you will probably leave one or two of Lookup Field Name fields blank, depending on which of Account/Contact/Opportunity you are mapping to.

Step 5: Test code

You can’t deploy to your production environment without some test code of course. If you’re transferring the Sticky object, you can use this test code as-is. If you’d rather not clutter up your instance with an unneeded object, you’ll need to modify the test code to use one of yours instead. If the test code won’t run, it’s probably because some mandatory fields on your Lead object are needed. If that’s the case, simply add them to the Lead creation part after line 7.

Create a new class using the Developer Console (File > New > Apex Class) and enter a sensible name (e.g. LeadUnitTest) and add the following code.

@IsTest(seeAllData = false)
public class LeadUnitTest {

    static TestMethod void testLeadConvert(){
        Lead l = new Lead(
            LastName = 'Test',
            Company = 'TestCompany'
        );
        insert l;
        
        // If you're not using the test Sticky object, change this section to create a record on an object which will move to the Account, Contact or Opportunity from the Lead
        Sticky__c stickyrec = new Sticky__c(
            Name = 'Test sticky',
            Lead__c = l.id
        );
        insert stickyrec;
        
        Lead_Convert_Object__c lco = new Lead_Convert_Object__c(
            Name = 'Sticky__c',
            Lead_Lookup_Field_Name__c = 'Lead__c',
            Account_Lookup_Field_Name__c = 'Account__c',
            Contact_Lookup_Field_Name__c = 'Contact__c',
            Opportunity_Lookup_Field_Name__c = 'Opportunity__c'
        );
        insert lco;
        
        Database.LeadConvert lc = new Database.LeadConvert();
        lc.setLeadId(l.id);
        LeadStatus convertStatus = [SELECT Id, MasterLabel FROM LeadStatus WHERE IsConverted=true LIMIT 1];
        lc.setConvertedStatus(convertStatus.MasterLabel);
        
        test.startTest();
        
        Database.convertLead(lc);
        
        test.stopTest();
        
        // Good practice to test the record has been correctly updated
        // Not required for code coverage
        Lead newlead = [SELECT id,ConvertedAccountId FROM Lead WHERE id=:l.id];
        Sticky__c sticky = [SELECT id,Account__c FROM Sticky__c WHERE Lead__c=:l.id];
        system.assertEquals(sticky.Account__c,newlead.ConvertedAccountId);
    }
}

Step 6: Deploy and enjoy

Deploy the new objects, trigger and class into production in your usual way, then set up records on the Lead Convert Object as required; one record per object to follow Lead records into Account/Contact/Opportunity records. It’s a good idea to do a live test right after this to make sure you’ve entered all your API names correctly.

Last words

If you have any suggestions on improving anything you’ve read above or you’ve noticed any mistakes, please leave a comment. Any feedback is appreciated and will be approved shortly as long as it’s constructive. If you don’t want to leave a comment on this post but do have something to say to me, head to the Contact section to send me a direct message instead.

Leave a Reply

Close Menu