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
SpunkySpunky 

Copy Primary Contact to Opportunity custom field

I've written a trigger in my sandbox to copy the primary contact Role and ID values into 2 opportunity custom fields but it's not firing and I just cant figure out why.

 

Any help would be appreciated, I'm sure it's an oversight on my part.

 

Trigger PrimaryContactCopy on Opportunity(before insert,before update){
OpportunityContactRole Roles = new OpportunityContactRole();
for(Opportunity opp:Trigger.new){
Roles = [select Id,IsPrimary,ContactId, Role from OpportunityContactRole where IsPrimary=True AND opportunity.id=:opp.Id];
opp.Name__c = Roles.ContactId;
opp.Email__c = Roles.Role;
update opp;
 } 
}

 

Best Answer chosen by Admin (Salesforce Developers) 
sfdcfoxsfdcfox

 

trigger PrimaryContactCopy on Opportunity(after insert, after update) {
  if(!ContactCopyUtil.isRecursive)
    ContactCopyUtil.copyFrom(Trigger.newmap.keyset());
}

public class ContactCopyUtil {
  public static boolean isRecursive = false;

  @future
  public static void copyFrom(set<id> oppids) {
    isRecursive = true;
    List<Opportunity> opps = new List<opportunity>();
    for(Opportunity o:[select id,primary_contact__c,(select id,role,contactid from opportunitycontactroles where isprimary=true) from opportunity where id in :oppids])
      if(o.opportunitycontactroles<>null &&
         o.opportunitycontactroles.size()==1 &&
         o.opportunitycontactroles[0].contactid<>o.name__c)
        opps.add(new Opportunity(id=o.id,
          email__c=o.opportunitycontactroles[0].role,
          name__c=o.opportunitycontactroles[0].contactid));
    update opps;
  }
}

Best I could say is that you'd want to do this in a @future method, because OpportunityContactRole won't be available in a before insert trigger (as stated before, they would not exist because the record itself does not yet exist). Of course, we have to prevent recursion or there'll be trouble, so we do that with a boolean flag. Fairly standard procedure here, just a bit tricky because the child object can not have a trigger.

 

All Answers

BritishBoyinDCBritishBoyinDC

general comment - putting the SOQL in the loop will quickly break the limits...you need to query outside the loop and then use maps to perform some of sort of reference lookup...

 

Re this code, I am not sure it can work, if this is being triggered by the UI...the contact role data is written to the database after the opportunity is committed (it has to be, or the foreign key wouldn't work...) - so it might work if you tried to use an after insert/after update on opportunity, but I don't think you could ever guarantee that you are querying the right data...?

 

Also - how do you handle someone updating the Contact Role table. The lack of triggers on Opportunity Contact Role is one that continues to make us sad...

sfdcfoxsfdcfox

 

trigger PrimaryContactCopy on Opportunity(after insert, after update) {
  if(!ContactCopyUtil.isRecursive)
    ContactCopyUtil.copyFrom(Trigger.newmap.keyset());
}

public class ContactCopyUtil {
  public static boolean isRecursive = false;

  @future
  public static void copyFrom(set<id> oppids) {
    isRecursive = true;
    List<Opportunity> opps = new List<opportunity>();
    for(Opportunity o:[select id,primary_contact__c,(select id,role,contactid from opportunitycontactroles where isprimary=true) from opportunity where id in :oppids])
      if(o.opportunitycontactroles<>null &&
         o.opportunitycontactroles.size()==1 &&
         o.opportunitycontactroles[0].contactid<>o.name__c)
        opps.add(new Opportunity(id=o.id,
          email__c=o.opportunitycontactroles[0].role,
          name__c=o.opportunitycontactroles[0].contactid));
    update opps;
  }
}

Best I could say is that you'd want to do this in a @future method, because OpportunityContactRole won't be available in a before insert trigger (as stated before, they would not exist because the record itself does not yet exist). Of course, we have to prevent recursion or there'll be trouble, so we do that with a boolean flag. Fairly standard procedure here, just a bit tricky because the child object can not have a trigger.

 

This was selected as the best answer
*werewolf**werewolf*

Quick note, I think the class has some weird references in it to name__c; it should be amended as follows:

 

public class ContactCopyUtil {
  public static boolean isRecursive = false;

  @future
  public static void copyFrom(set<id> oppids) {
    isRecursive = true;
    List<Opportunity> opps = new List<opportunity>();
    for(Opportunity o:[select id,primary_contact__c,(select id,role,contactid from opportunitycontactroles where isprimary=true) from opportunity where id in :oppids])
      if(o.opportunitycontactroles<>null &&
         o.opportunitycontactroles.size()==1 &&
         o.opportunitycontactroles[0].contactid<>o.primary_contact__c)
        opps.add(new Opportunity(id=o.id,
          primary_contact__c=o.opportunitycontactroles[0].contactid));
    update opps;
  }
}

 

kcrawfordIMSkcrawfordIMS

Thank for the great question and of course the answer. I'm VERY new at all this and am running into an issue with this trigger. I keep getting the error:

 

ErrorError: Compile Error: unexpected token: public at line 6 column 0



what am I missing here?

Thanks

*werewolf**werewolf*

If you copied sfdcfox's code verbatim into a trigger then you'll get that error because he mashed up 2 things in one.  The part with the trigger, you copy into a trigger:

 

trigger PrimaryContactCopy on Opportunity(after insert, after update) {
  if(!ContactCopyUtil.isRecursive)
    ContactCopyUtil.copyFrom(Trigger.newmap.keyset());
}



The part with the class you copy into a class (here's my amended version):

public class ContactCopyUtil {
  public static boolean isRecursive = false;

  @future
  public static void copyFrom(set<id> oppids) {
    isRecursive = true;
    List<Opportunity> opps = new List<opportunity>();
    for(Opportunity o:[select id,primary_contact__c,(select id,role,contactid from opportunitycontactroles where isprimary=true) from opportunity where id in :oppids])
      if(o.opportunitycontactroles<>null &&
         o.opportunitycontactroles.size()==1 &&
         o.opportunitycontactroles[0].contactid<>o.primary_contact__c)
        opps.add(new Opportunity(id=o.id,
          primary_contact__c=o.opportunitycontactroles[0].contactid));
    update opps;
  }
}



 

kcrawfordIMSkcrawfordIMS

Thanks,

 

Now it worked. If I wanted to improve on this and copy the email from the contact role to the primary_contact__c, what would I have to tweak? 

*werewolf**werewolf*

Now that you have a link to the contact on your opportunity, you no longer need to touch the trigger to add things from the contact to the opportunity.  Just add a formula field, and you can reference information from the contact in your formula field (use the Insert Field button of the formula builder to help you out with the references)

Bobby BoslerBobby Bosler

Sorry if this is a dumb question, but I cannot get this class and trigger to deploy without tests. I have not clue even where to start to code a test to get this thing going. Any help?

*werewolf**werewolf*

See here for an intro on writing test cases.

Bobby BoslerBobby Bosler

Guess I'll learn to fish then! Thanks!

Bobby BoslerBobby Bosler

Do you have a test case already written that I could use for this?

 

I'd work it out myself, but I have no background in Apex or C++. I'd have to learn a whole new coding language just to test and deploy this class/trigger.

sfdcfoxsfdcfox

You'll have to do something similar to the following:

 

public class TestMyCode {
  public static void testMethod test() {
    Account a = new account(...);
    insert a;
    Contact c = new Contact(accountid=a.id,...);
    insert c;
    Opportunity o = new Opportunity(accountid=a.id,...);
    insert o;
    OpportunityContactRole ocr = new OpportunityContactRole(opportunityid=o.id,contactid=c.id,...);
    insert ocr;
    Test.startTest();
    update o;
    Test.stopTest();
    o = [select id,primary_contact__c from opportunity where id = :o.id];
    system.assertEquals(o.primary_contact__c,c.id,'Contact ID did not populate correctly');
  }
}

You can fill in the "..." in the code with other field assignments that may be required for your organization's data set or that I have otherwise intentionally omitted for brevity, but this is essentially what your code would look like.

 

I don't have time until at least later this week to perform a full testing cycle in my test organization, but if you're not able to get it working soon, let me know and I'll see what I can do.

JamuraiJamurai

I was able to take this and write a workable test class. Certain values are specific to my org, but I think you ought to be able to take this and modify as necessary.

 

@isTest
private class TestCopyPrimaryContact {
  static testMethod void test() {
    Account a = new account(Name='Fake Account');
    insert a;
    Contact c = new Contact(AccountId=a.id,LastName='Daikoku');
    insert c;
    Opportunity o = new Opportunity(AccountId=a.id,Name='Test Opp',CloseDate=System.today(),StageName='Prospecting');
    insert o;
    OpportunityContactRole ocr = new OpportunityContactRole(opportunityid=o.id,contactid=c.id,IsPrimary=true,Role='Decision Maker');
    insert ocr;
    Test.startTest();
    update o;
    Test.stopTest();
    o = [select id,Primary_Contact__c from opportunity where id = :o.id];
    system.assertEquals(o.Primary_Contact__c,c.id,'Contact ID did not populate correctly');
  }
}

 

CloudyRituCloudyRitu

Create a PrimaryContact__c look up on Opportunity and use below method in after insert, after update event to populate the primary contact role on opportunity.

 

private void updatePrimaryContact(){    
List<Opportunity> lstopportunity = new List<Opportunity>();     
for(Opportunity op:[select id,PrimaryContact__c, (select ContactId from OpportunityContactRoles where IsPrimary=true) from Opportunity where id In :Trigger.newMap.keyset()]){    op.PrimaryContact__c= op.OpportunityContactRoles[0].ContactId;         
lstopportunity.add(op);
}     
update lstopportunity;  
}
K.G.K.G.
Thanks all for this invaluable information, but what a shame that triggers can't be fired on OpportunityContactRole.  Firing a trigger on Opportunity every time something completely unrelated about the Opportunity is updated is inefficient.  And, ironically, it seems to me that if you simply deleted an Opportunity Contact Role or updated its details, that doesn't count as an update to Opportunity and this trigger wouldn't be fired at all.  I certainly hope one day we will be able to write this trigger on OpportunityContactRole itself - I just upvoted allowing triggers on that object at https://success.salesforce.com/ideaView?id=08730000000BrdvAAC&sort=2
Francesco Danieli 1Francesco Danieli 1
Thanks to sfdcfox and werewolf for the great help.

Altough i've managed to get the opp custom field populated with the Contact Role id, i'm still at loss with populating another custom field in opportunity with the email of the role. 

I've tried as werewolf suggested to "Just add a formula field" but I can't see how to reference information from the contact in your formula field.
 
Could someone please share some lights?
K.G.K.G.
@Francesco Danieli 1, if the name of your custom field with a Contact lookup in it is "My_PrimContact__c", then you will make a formula field of type "text" whose formula is simply "My_PrimContact__r.Email" (note how the "c" changes to an "r").
Francesco Danieli 1Francesco Danieli 1
@K.G Can't thank you enough :)
To reiterate what K.G. said and make it even more obvious, the custom field that needs to be originally create must be a lookup field.
In this way it can be referenced in the formula field.

 
Sfdc Guide 7Sfdc Guide 7

I really appreciate that someone has written and passed along this code for us to make use of. However, I had a really hard time understanding exactly what was going on due in part to the syntax and nature of the code itself. So I figured I would respond with my version, which is hopefully a little more easily understood. 

First, make a lookup field on your Opportunity called "Primary Contact" - the API name will be Primary_Contact__c.

Then create a new apex class, paste this:

 

public class ContactCopyUtil {
  public static boolean isRecursive = false;

  @future
  public static void copyFrom(Set<Id> oppIds) {

    isRecursive = true;
    List<Opportunity> oppsToUpdate = new List<opportunity>();

    for(Opportunity o: [SELECT Id, Primary_Contact__c, (SELECT Id, Role, ContactId FROM OpportunityContactRoles where IsPrimary = true) FROM Opportunity WHERE Id in :oppIds]) {


        if(o.OpportunityContactRoles != null && o.OpportunityContactRoles.size() == 1 && o.OpportunityContactRoles[0].ContactId != o.Primary_Contact__c)
         
            o.Primary_Contact__c = o.opportunitycontactroles[0].ContactId;
            oppsToUpdate.add(o);          
    
        }

    update oppsToUpdate;
    
  }
}


Save that - it should save without errors if your fields have the same name. Note the "IF" statement which asks that two things be true: 

1. There is an OpportunityContactRole
2. We haven't already set the Primary Contact field on this record

If both of those are true, it will go ahead and populate the field. 

Now create a trigger, just as you saw before:

trigger PrimaryContactCopy on Opportunity (after insert, after update) {
  if(!ContactCopyUtil.isRecursive)
    ContactCopyUtil.copyFrom(Trigger.newmap.keyset());
}

That's all! I hope this slightly refactored code is easier to understand. 
Abhishek ChopraAbhishek Chopra
Hi All, thanks for your comment, very helpful to understand.

i have come across a situation where if an opportunity has two Contact role associated and if i change the "is primary" from one Contact to another (to populate custom field from primary contact to opportunity) how will i trigger the Process Builder/Trigger as there is no triggering point for Opportunity though the last modified gets changed. I have gone through almost all the blog posts available in the community but no Luck. People have advised me to write a Batch or Scheduled Apex. Any help here or ideas to get around this scenario is highly appreciated.
K.G.K.G.
@Abishek Chopra 2, you're right that there is nothing happening in Salesforce that would tell Salesforce to "fire" any sort of Process Builder or Trigger, since the Opportunity isn't getting changed, but instead the OpportunityContactRole is getting changed.  That's precisely why people hate the fact that there are no OpportunityContactRole PBs/triggers allowed.

Therefore, the only thing you can do is have code stored in Salesforce sweep the entire database on a schedule, looking for things that have "fallen out of sync" and fixing them if it finds any.  This is the kind of code that a combination of Batch Apex (to do the actual fixing work) and Scheduled Apex (to run the Batch Apex) are perfect for.

To learn more about HOW to write good Scheduled Apex and Batch Apex, check out Dan Applebaum's Advanced Apex Programming.