function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
Boom B OpFocusBoom B OpFocus 

Trigger to create an opportunity team member from lead conversion

I wrote a Trigger on Lead that creates an Opportunity Team Member when a Lead is converted and it has ConvertedOpportunityId.  The Opportunity Team Member should have UserId = Lead OwnerId and has a specific role (for example - "Account Manager").  

 

I tested converting a Lead that was owned by me and the Opportunity was created with an Opportunity Team Member = me and role was Account Manager, so I thought the Trigger worked fine.  Then I tested converting a Lead that was owned by another user.  The Opportunity was created with the same owner as the Lead Owner, but there was no Opportunity Team Member.  I added some debug statements to catch if the Opportunity Team Member was really created and it did (at least) in the debug log.  One of the debugs showed that the Opportunity Team Member was created from the Trigger and it had me (the person who converted the Lead) as the Member.  Another debug showed that I was the Opportunity Owner, but when I checked in the UI, the Opportunity Owner was actually the other user.  

 

Here is my Lead Trigger.

Trigger Lead on Lead (before insert, before update) {
	 	
	if (Trigger.isBefore) {
		
		if (Trigger.isUpdate) {	
					
			Set<Id> setLeadIds     = new Set<Id>();  
			Set<Id> setConvertedOppIds   = new Set<Id>();  
			
			for (Lead ld : Trigger.new) {

				// Find all converted Leads with Opportunitiy and add ConvertedOpportunityId to setConvertedOppIds
				if (ld.isConverted && ld.ConvertedOpportunityId != null 
					&& Trigger.oldMap.get(ld.Id).ConvertedOpportunityId != ld.ConvertedOpportunityId) 
					setConvertedOppIds.add(ld.ConvertedOpportunityId);			
			}		

			if (!setConvertedOppIds.isEmpty()) {
				List<OpportunityTeamMember> lstOppTeams = new List<OpportunityTeamMember>();
				// Get Opportunities that created from the Lead conversion
				List<Opportunity> lstConvertedOpps = [select Id, OwnerId from Opportunity where Id in :setConvertedOppIds];
				if (lstConvertedOpps.size() > 0) {					
					for (Opportunity opp : lstConvertedOpps) {
						OpportunityTeamMember otm = new OpportunityTeamMember(
								   TeamMemberRole = 'Account Manager', 
						           OpportunityId  = opp.Id,
						           UserId         = opp.OwnerId);
						lstOppTeams.add(otm);
					}
					// Insert Opportunity Team
					if (!lstOppTeams.isEmpty()) {
						for (Opportunity opp : lstConvertedOpps) {
							System.debug('===== before inserting OTMs, opp = ' + opp);
						}
						System.debug('===== inserting OTMs: ' + lstOppTeams);
						
						insert lstOppTeams;
						
						for (OpportunityTeamMember otm : [select id, UserId, TeamMemberRole from OpportunityTeamMember where OpportunityId in :setConvertedOppIds])
							System.debug('===== OTM after insert: ' + otm);
						
						lstConvertedOpps = [select Id, OwnerId from Opportunity where Id in :setConvertedOppIds];
						for (Opportunity opp : lstConvertedOpps) {
							System.debug('===== after inserting OTMs, opp = ' + opp);
						}
					}
				}
			}
		}		
	} 	
}

 

 

So, I wonder if we can really write a Trigger on Lead that creates an Opportunity Team Member that belongs to a different user?

Also, can we catch what happens during the Lead Conversion, like why there seems to be some opportunity owner changing during that time but we couldn't really see that?

 

Out of the curiousity, I wrote a Trigger on Opportunity just for debug to see if I could catch the opportunity change.  Strangely, I was unable to do that.  The debugs only showed that I was the opportunity owner right before Insert and also after insert.  Nothing showed that the other user was the opportunity owner until I checked in the UI.

 

Here is the opportunity Trigger.

Trigger Opportunity on Opportunity (after insert, after update, before insert, before update) {

	String str = '';
	str += (Trigger.isBefore) ? 'Before ' : 'After ';
	if (Trigger.isInsert) str += 'Insert: ';
	if (Trigger.isUpdate) str += 'Update: ';

	for (Opportunity opp : Trigger.new) {
		System.debug('===== ' + str + ' Opp Id=' + opp.id + '   OwnerId=' + opp.OwnerId);
	}

}

 

Best Answer chosen by Admin (Salesforce Developers) 
Boom B OpFocusBoom B OpFocus

I contacted Salesforce support and he gave me this resolution below:

 

Resolution:
The Order of operations for Lead Convert is something like this:
• Account created, triggers/workflow fire • Contact created, triggers/workflow fire • Opportunity created, triggers/workflow fire • Owners are reparented

The workaround is to use an @future method for the new opportunity, as @future fires after the correct owner is assigned for the Opportunity. 

 


I updated my code to use the @future method and it worked!  A caveat from using @future is we can run into the governor limit that only allows 200 @future method calls per one full license user, per 24 hours.

 

Class for creating Opportunity Team Member:

public class OpportunityTeamProcessor {
	@future
	public static void createOpportunityTeamMember(Set<Id> oppIds) {
		// List to store OpportunityTeamMember to create
		List<OpportunityTeamMember> lstOppTeams = new List<OpportunityTeamMember>();
		// Iterate through the list of Opportunities that created from the Lead conversion
		for (Opportunity opp : [select Id, OwnerId from Opportunity where Id in :oppIds]) {
			OpportunityTeamMember otm = new OpportunityTeamMember(
					   TeamMemberRole = 'Sales Manager', 
			           OpportunityId  = opp.Id,
			           UserId         = opp.OwnerId);
			lstOppTeams.add(otm);
			
		}
		// Insert Opportunity Team
		if (!lstOppTeams.isEmpty()) {
			// Remember that Future method is being called
			Statics.inFutureContext = true;
			insert lstOppTeams;			
		}
	}
}

 Trigger that calls the class above:

trigger Lead on Lead (before insert, before update) {
	 	
	if (Trigger.isBefore) {
		
		if (Trigger.isUpdate) {	
					
			Set<Id> setLeadIds     = new Set<Id>();  
			Set<Id> setConvertedOppIds   = new Set<Id>();  
			
			for (Lead ld : Trigger.new) {

				// Find all converted Leads with Opportunitiy and add ConvertedOpportunityId to setConvertedOppIds
				if (ld.isConverted && ld.ConvertedOpportunityId != null 
					&& Trigger.oldMap.get(ld.Id).ConvertedOpportunityId != ld.ConvertedOpportunityId) 
					setConvertedOppIds.add(ld.ConvertedOpportunityId);			
			}		

			if (!Statics.inFutureContext) {
				if (!setConvertedOppIds.isEmpty()) OpportunityTeamProcessor.createOpportunityTeamMember(setConvertedOppIds);
			}				
		}		
	} 	
}

 I also used static variable to store the state of the trigger processing that prevents recursive future method calls (thanks to Jeff Douglas's blog).

public class Statics {
	// inFutureContext is true when the trigger (that uses this variable) is being called from the future method
	public static Boolean inFutureContext = false;
}

 

All Answers

Chris760Chris760

Funny, we just finished getting the bugs out of a trigger as well that automatically builds an opportunity team on the opportunity based on a lookup on the opportunity called "Team", which references a custom object I made called "Team" with a child object called "Team Members" where you can create all the team members on your team (each team member record references a user) and that way you can set your team on the lead, and once the opportunity is made, it automatically builds all the members who are a part of your team and sets the access level defined in their Team Member record, along with their default role, etc.

 

Some advice I should probably give you based on a lot of frustration, is that by default, any Opportunity Team Member records you create will have Read-Only permissions.  If you wish to give team members Read/Write permissions, then you need to insert a record in to the OpportunityShare object with an access level of "Edit" for that user.  Also, if the opportunity owner also happens to be one of the OpportunityTeamMembers that are getting added, you need to leave out the OpportunityShare record that gets created for that team member or else the trigger will error (because salesforce adds an OpportunityShare record automatically for the opportunity owner, which will conflict with the OpportunityShare record that the trigger tries to create if the Opportunity Owner is also one of the Team Members).  It was kind of tricky but eventaully we got all the bugs out.

 

I don't know if it's of any help, but here's the final trigger we made:

 

trigger OpportunityTeamMembersAssignment on Opportunity (after insert, after update) {

	if (TriggerFlags.allowOpportunityDML) {
		//Opportunity->Team
		Map<Id, Id> MOpportunityTeam = new Map<Id, Id>();
		Map<Id, Id> MOpportunityOwners = new MAp<Id, Id>();
		
		for (Opportunity o : [
								SELECT Id, Team__c, OwnerId 
								FROM Opportunity
								WHERE Id IN: Trigger.newMap.keySet()
							  ]) {
								MOpportunityTeam.put(o.Id, o.Team__c);
								MOpportunityOwners.put(o.Id, o.OwnerId);
		}
		
		//Team->UserIDs													
		Map<Id, List<Team_Member__c>> MTeamTeamMembers = new Map<Id, List<Team_Member__c>>();
		
		//Collect Team Members' User Ids
		for (Team_Member__c tm : [	SELECT Team__c, User__c, Default_Role__c, Share_Permission__c, Active__c 
									FROM Team_Member__c
									WHERE Team__c IN: MOpportunityTeam.values() 
								  ]) {
								  
								  //get set of team's users
								  List<Team_Member__c> teamUsers = MTeamTeamMembers.get(tm.Team__c) != null ?
								  										MTeamTeamMembers.get(tm.Team__c) :
								  										new List<Team_Member__c>();
								  //add team member to team
								  teamUsers.add(tm);
								  
								  //update team map
								  MTeamTeamMembers.put(tm.Team__c, teamUsers);
		}
		
		List<OpportunityTeamMember> OTMs 	= new List<OpportunityTeamMember>();
		List<OpportunityShare> OSs			= new List<OpportunityShare>();
		 
		//Create OpportunityTeamMemberRecords
		for (Id oId : MOpportunityTeam.keySet() ) {			
			//get team
			Id teamId 					= MOpportunityTeam.get(oId);
			
			//get team's team_members
			List<Team_Member__c> TMs 	= MTeamTeamMembers.get(teamId) != null ?
											MTeamTeamMembers.get(teamId) :
											new List<Team_Member__c>();
											
			for (Team_Member__c tm : TMs) {
				//create opportunity team member
				OpportunityTeamMember otm 	= new OpportunityTeamMember();
				otm.OpportunityId 			= oId;
				otm.UserId					= tm.User__c;
				otm.TeamMemberRole			= tm.Default_Role__c;			
				OTMs.add(otm); 			
				
				//create opportunity share
				OpportunityShare os 		= new OpportunityShare();
				os.OpportunityId			= oId;
				os.UserOrGroupId			= tm.User__c;
				os.OpportunityAccessLevel	= tm.Share_Permission__c;
				if (MOpportunityOwners.get(oId) != os.UserOrGroupId ) 
					OSs.add(os);
			}
			
		}
		
		//DML
		insert OTMs;
		insert OSs;
	}	
}

 

Test Class:

public with sharing class TestTriggerOpportunityTeamMembers {

	public static testmethod void test_1() {
		if (TestUtility.isActiveTrigger('OpportunityTeamMembersAssignment')) {
			//create test users
			User john 	= TestUtility.createTestUser();
			User paul 	= TestUtility.createTestUser();
			User george = TestUtility.createTestUser();
			User ringo 	= TestUtility.createTestUser();
			
			//create teams
			Team__c beatles 	= new Team__c();
			beatles.Name 		= 'The Cloud Beatles';
			insert beatles;
			
			Team__c wings 		= new Team__c();
			wings.name			= 'Wings';
			insert wings;
			
			//create team members		
				//john
				Team_Member__c tm1	= new Team_Member__c();
				tm1.Team__c 		= beatles.Id;
				tm1.User__c			= john.Id;
				tm1.Default_Role__c	= 'Marketing Director';
				tm1.Access__c		= 'Read/Write';
				insert tm1;
				
				//paul
				Team_Member__c tm2	= new Team_Member__c();
				tm2.Team__c 		= beatles.Id;
				tm2.User__c			= paul.Id;
				tm2.Default_Role__c	= 'Marketing Consultant';
				tm2.Access__c		= 'Read/Write';
				insert tm2;
				
				//george
				Team_Member__c tm3	= new Team_Member__c();
				tm3.Team__c 		= beatles.Id;
				tm3.User__c			= george.Id;
				tm3.Default_Role__c	= 'Marketing Consultant';
				tm3.Access__c		= 'Read/Write';
				insert tm3;
				
				//ringo
				Team_Member__c tm4	= new Team_Member__c();
				tm4.Team__c 		= beatles.Id;
				tm4.User__c			= ringo.Id;
				tm4.Default_Role__c	= 'Marketing Consultant';
				tm4.Access__c		= 'Read/Write';
				insert tm4;
			
				//paul and linda
				Team_Member__c tm5	= new Team_Member__c();
				tm5.Team__c 		= wings.Id;
				tm5.User__c			= paul.Id;
				tm5.Default_Role__c	= 'Marketing Consultant';
				tm5.Access__c		= 'Read/Write';
				insert tm5;
			
			//create test account
			Account a 	= new Account();
			a.name		= 'Teston';
			insert a;
			
			//create test opportunity	
			Opportunity o 	= new Opportunity();
			o.Name			= 'Test Oppty';
			o.AccountId		= a.Id;
			o.CloseDate		= Date.today();
			o.StageName		= 'Offer';
			o.Team__c		= beatles.Id;
			o.OwnerId		= john.Id;		
			insert o;
			
			
			List<OpportunityTeamMember> OTMs = [SELECT id, UserId, OPPORTUNITYACCESSLEVEL
												FROM OpportunityTeamMember
												WHERE OpportunityId =: o.Id
												];
		
			Set<Id> BeatlesUserIDs = new Set<Id>{ 	john.Id, 
													paul.Id,
													george.Id,
													ringo.Id
												 };
												 
			system.assertEquals(4, OTMs.size() );
			system.assert( BeatlesUserIDs.contains( OTMs[0].UserId ) );
			system.assert( BeatlesUserIDs.contains( OTMs[1].UserId ) );
			system.assert( BeatlesUserIDs.contains( OTMs[2].UserId ) );
			system.assert( BeatlesUserIDs.contains( OTMs[3].UserId ) );

		
			//update the opportunity
			User peter = TestUtility.createTestUser();
			
			//add the 5th beatle to list
			BeatlesUserIDs.add(peter.Id);
			
			//add the 5th beatle team member
			Team_Member__c tm6	= new Team_Member__c();
			tm6.Team__c 		= beatles.Id;
			tm6.User__c			= peter.Id;
			tm6.Default_Role__c	= 'Marketing Director';
			tm6.Access__c		= 'Read/Write';
			insert tm6;
			
			//update O to execute the trigger
			update o;
			
			//re-query
			OTMs = [SELECT id, UserId, OPPORTUNITYACCESSLEVEL
					FROM OpportunityTeamMember
					WHERE OpportunityId =: o.Id
					];
			
			//new test
			system.assertEquals(5, OTMs.size() );
			system.assert( BeatlesUserIDs.contains( OTMs[0].UserId ) );
			system.assert( BeatlesUserIDs.contains( OTMs[1].UserId ) );
			system.assert( BeatlesUserIDs.contains( OTMs[2].UserId ) );
			system.assert( BeatlesUserIDs.contains( OTMs[3].UserId ) );
			system.assert( BeatlesUserIDs.contains( OTMs[4].UserId ) );

					
		}
	}
	
}

 

Boom B OpFocusBoom B OpFocus

I contacted Salesforce support and he gave me this resolution below:

 

Resolution:
The Order of operations for Lead Convert is something like this:
• Account created, triggers/workflow fire • Contact created, triggers/workflow fire • Opportunity created, triggers/workflow fire • Owners are reparented

The workaround is to use an @future method for the new opportunity, as @future fires after the correct owner is assigned for the Opportunity. 

 


I updated my code to use the @future method and it worked!  A caveat from using @future is we can run into the governor limit that only allows 200 @future method calls per one full license user, per 24 hours.

 

Class for creating Opportunity Team Member:

public class OpportunityTeamProcessor {
	@future
	public static void createOpportunityTeamMember(Set<Id> oppIds) {
		// List to store OpportunityTeamMember to create
		List<OpportunityTeamMember> lstOppTeams = new List<OpportunityTeamMember>();
		// Iterate through the list of Opportunities that created from the Lead conversion
		for (Opportunity opp : [select Id, OwnerId from Opportunity where Id in :oppIds]) {
			OpportunityTeamMember otm = new OpportunityTeamMember(
					   TeamMemberRole = 'Sales Manager', 
			           OpportunityId  = opp.Id,
			           UserId         = opp.OwnerId);
			lstOppTeams.add(otm);
			
		}
		// Insert Opportunity Team
		if (!lstOppTeams.isEmpty()) {
			// Remember that Future method is being called
			Statics.inFutureContext = true;
			insert lstOppTeams;			
		}
	}
}

 Trigger that calls the class above:

trigger Lead on Lead (before insert, before update) {
	 	
	if (Trigger.isBefore) {
		
		if (Trigger.isUpdate) {	
					
			Set<Id> setLeadIds     = new Set<Id>();  
			Set<Id> setConvertedOppIds   = new Set<Id>();  
			
			for (Lead ld : Trigger.new) {

				// Find all converted Leads with Opportunitiy and add ConvertedOpportunityId to setConvertedOppIds
				if (ld.isConverted && ld.ConvertedOpportunityId != null 
					&& Trigger.oldMap.get(ld.Id).ConvertedOpportunityId != ld.ConvertedOpportunityId) 
					setConvertedOppIds.add(ld.ConvertedOpportunityId);			
			}		

			if (!Statics.inFutureContext) {
				if (!setConvertedOppIds.isEmpty()) OpportunityTeamProcessor.createOpportunityTeamMember(setConvertedOppIds);
			}				
		}		
	} 	
}

 I also used static variable to store the state of the trigger processing that prevents recursive future method calls (thanks to Jeff Douglas's blog).

public class Statics {
	// inFutureContext is true when the trigger (that uses this variable) is being called from the future method
	public static Boolean inFutureContext = false;
}

 

This was selected as the best answer
Rafael Sanchez 4Rafael Sanchez 4
Hello, and how about the test class for the lead trigger, can you please share it?