I’ve been working on a fun problem for Force-Friend Nick (Hi Nick!).
The requirement:
- When an Opportunity is closed, workflow sends an email to the Accounts people (who don’t have a Salesforce login)
- The email is to include address details of the Primary Contact associated with the Opportunity
Some relevant information:
- Contacts are linked to Opportunity objects via the
OpportunityContactRoleobject, which contains anOpportunityId, aContactIdand anIsPrimaryflag (plus various other fields) - Cross-object formulas can’t span the Opportunity-Contact relationship
The Solution
There are several ways this could be implemented:
- Move the email generation from workflow to Apex, which would give full access to the object model (but lose the easy user interface)
- Create a Trigger on OpportunityContactRole to copy information onto the Opportunity (unfortunately, we discovered that you can’t create a Trigger on this object!)
- Create a Trigger on Opportunity to ‘pull’ the information from the Primary Contact onto the Opportunity record
We decided to go with the Trigger on Opportunity. The information is only wanted when the Opportunity is closed, so it means we could write the Trigger to fire on closure of the Opportunity. It then looks for the Primary Contact (as flagged by the IsPrimary field) and copies it to a Custom Field (Primary_Contact__C) on the Opportunity object, which can then be used by cross-formula functions within workflow.
Here’s the code for the trigger:
trigger SetPrimaryContact on Opportunity (before update) {
for (Opportunity o : Trigger.new) {
// Check if the Opportunity just closed
if (o.IsClosed && !Trigger.oldMap.get(o.id).IsClosed) {
OpportunityContactRole contactRole =
[select ContactID from OpportunityContactRole where IsPrimary = true and OpportunityId = :o.id];
if (contactRole != null) {
o.Primary_Contact__c = contactRole.ContactID;
}
}
}
}
Simple result, but took a bit of thinking to create it.
The only funny-looking code is !Trigger.oldMap.get(o.id).IsClosed, which retrieves the old value of the IsClosed field to see that it really changed. I use oldMap because I read somewhere that you should not rely on Trigger.old and Trigger.new being perfectly aligned (that is, for example, Trigger.old[4] and Trigger.new[4] do not necessarily refer to the same fields). However, I can’t find that reference anymore, so feel free to tell me if I’m right or wrong!
The Bottom Line
- Not every object permits a Trigger
- Cross-object formulas also have their limitations
- Sometimes a bit of Apex trickery saves having to buy licenses for extra users!
October 17th, 2008 at 6:11 am
The day after I wrote this blog post, a person on the Salesforce Community asked for exactly the same thing.
Ha, it’s amazing how the universe works sometimes!
October 17th, 2008 at 11:32 am
This is Apex code. If you are unfamiliar with Apex, please see An Introduction to Apex, which is an excellent introductory guide.
As to updating a field on a Contact when an Opportunity is won, I would recommend directing your question to the Force.com Discussion Boards. Please provide an explanation of what you are trying to accomplish, and please also explain WHY, since there might be a better way to achieve your goal.
October 2nd, 2009 at 3:08 am
I’ll preface with the clarification that I am a total newbie to Apex / Triggers.
Using your example above, I wrote a trigger to pull the Primary Contact information from the Related Contact list to the Contract. I was able to build a trigger that brings the Primary Contact ID onto the Contract from the Related Contact Object (using both the ContactID as part of a WIL for a custom email button – also need to pull the Contact Name for use in an email template – but that’s another issue)
I posted a question about using the ContactID populated from the trigger and another community member commented that I will run into Governor limits because the Select statement is in the For Loop. Your trigger above has that same format – how did you approach the Governor limits issue?
October 10th, 2009 at 6:38 am
Meghan,
Yes, this code is open to exceeding the Governor limits. It should only be used where you know it will not be triggered in bulk.
For example, if you’re just checking whether an end-user has updated some fields, it should be safe. However, if it can be triggered via, say, a bulk load via the Data Loader then you’ll need to code as few DML statements (eg SELECT, UPDATE) as possible — this takes a bit of thought, but can typically be done by loading lists and manipulating them in memory.
August 18th, 2010 at 10:32 pm
The following example works better: http://mattiasnordin.net/web08/t5/forum/forum_posts.asp?TID=18