Copied from :http://www.opfocus.com/2013/07/create-task-assigned-to-lead-owner-of-new-web-lead/
Apex
I recently got a
request from a client to write a trigger that creates a task when a lead is
created from Web-to-Lead (Lead Source = “Web”). The Task should have Due
Date = Today, WhoId is the Lead.Id, and Status = New.
I thought that was a
simple trigger. I originally designed the trigger to be an after insert
trigger on the Lead object, which checked for LeadSource = ‘Web’ and created
new Task like this:
Task t = new Task();
t.WhoId = l.Id;
t.Subject = 'Web Lead';
t.OwnerId = l.OwnerId
t.ActivityDate = date.today();
Then I added the new
Task (t) to lstNewTasks and insert.
Simple, right? No, no,
no! Not until the Lead Assignment rule was implemented. Here is the
scenario:
·
In Lead Settings, the default lead owner is “Jack Bauer”
·
Lead Assignment rule has State = MA, assign to “Sydney Bristow”
A new lead came in
from the website with State = MA. Who would be the Task Owner?
Sydney, of course. But the actual Task Owner was Jack Bauer.
Why?
According to
Salesforce’s Triggers and Order of Execution, on the server, Salesforce
executes all after triggers before assignment rules. Wow, I didn’t expect
that.
What
should I do to assign the Task Owner to the same Owner as the Lead? I
thought it would be nice to have a Task Assignment Rule for this particular
case. Then I began my search for some good ideas from fellow developers
and I found this blog article Opportunity Assignment
Rules in Salesforce.com via Apex & Imagination. MJ Kahn introduced to me the way to
convert Lead Assignment Rules to Opportunity Assignment Rules, so I thought
“Why not? I can use this same tactic with my Task Assignment Rules, too.”
My approach is a little bit easier than MJ’s Opportunity Assignment Rules
and here are the steps:
1.
Add a checkbox field called “From WebLead” in Activity Custom
Field
2.
Tweak the Lead Trigger to not set the Task Owner Id and set From
WebLead = true.
3.
Write an Apex trigger that fires when a Task is created.
The trigger will:
o
Query for the default Lead Assignment rule.
o
Check if the Task is from web lead (From WebLead = true)
and WhoId starts with ’00Q’.
o
Set Database Savepoint.
o
Insert a new Lead that has all the important fields equal
to the same values in the Lead that the Task was created from, except FirstName
is the Lead’s Id. (Oh. You might not want to copy the Lead’s email
from the web lead, since some people like to put an invalid email address and
that would cause the Web to Lead to fail to insert the original lead to the
database.) Apply the default Lead Assignment Rule to the new Lead.
o
Determine which user the default Lead Assignment Rule
assigned to the new Lead and the Task.From Weblead is true, then change the
Task to be owned by that user.
o
Roll back the database to the state it was in when you set the
Savepoint, so we don’t save the new Lead.
o
Allow the creation of the Task to continue. The Task will
be created and we now have Task Owner equals to the Lead Owner based on the
default Lead Assignment Rule!
Let’s see the Task
trigger below.
trigger Task on Task
(before insert) {
if (Trigger.isBefore && Trigger.isInsert) {
AssignmentRule rule = [select id from AssignmentRule where SObjectType = 'Lead'
and Active = true limit 1];
Database.DmlOptions dmo = new Database.DmlOptions();
dmo.assignmentRuleHeader.assignmentRuleId = rule.id;
Set<Id>
setLeadIds =
new Set<Id>();
Map<Id, Lead> mapWebLeads = new Map<Id,
Lead>();
Map<Id, Lead> mapLeadsbyWhoId = new Map<Id, Lead>();
for (Task t : Trigger.new) {
String whoId;
whoId = t.WhoId;
if (t.WhoId == null) continue;
if (whoId.contains('00Q')) setLeadIds.add(t.WhoId);
}
// Get the current information from the Lead that we create a task for
mapWebLeads = new Map<Id, Lead>([select LastName, OwnerId, Email,
Company, LeadSource, Country, State,
Status, AZ_Auctioneer_ID__c
from Lead
where Id IN:setLeadIds ]);
if (!mapWebLeads.isEmpty()) {
for (Task t : Trigger.new) {
String whoId;
whoId = t.WhoId;
if (whoId.contains('00Q') && t.From_WebLead__c==true) {
Lead newLead = new Lead();
newLead.OwnerId =
mapWebLeads.get(t.WhoId).OwnerId;
newLead.LastName =
mapWebLeads.get(t.WhoId).LastName;
newLead.FirstName = t.WhoId;
newLead.Company =
mapWebLeads.get(t.WhoId).Company;
newLead.LeadSource =
mapWebLeads.get(t.WhoId).LeadSource;
// flag the Lead to be assigned by
our Assignment Rule
newLead.setOptions(dmo);
mapLeadsbyWhoId.put(t.WhoId,
newLead);
}
}
}
Savepoint sp = Database.setSavepoint();
// Insert the newLeads (and trigger the reassignment)
insert mapLeadsbyWhoId.values();
// Create a map of Leads, indexed by their Ids
in the FirstName
Map<Id, Lead> mapLeadsByWhoIdNew = new
Map<Id, Lead>();
for (Lead ld : [select id, FirstName, Email,
OwnerId from Lead where id in :mapLeadsbyWhoId.values()]) {
mapLeadsByWhoIdNew.put((Id)ld.FirstName, ld);
}
// Rollback database so the temp newLeads are not saved to real database
Database.rollback(sp);
//
mapLeadsByWhoIdNew should now contain all of the Leads in Trigger.new
for (Task t : Trigger.new) {
String whoId;
whoId = t.WhoId;
if (t.WhoId == null) continue;
if (whoId.contains('00Q') && t.From_WebLead__c==true) {
// Reassign the Task
t.OwnerId = mapLeadsByWhoIdNew.get(t.WhoId).OwnerId;
}
}
}
}
Have you reassigned a Task based on Lead
Assignment rule before? What approach did you use?
Salesforce provides a simple mechanism for
setting business hours in your organization. Just navigate to:
Setup ->
Administration Setup -> Company Profile -> Business Hours
Which brings you to
this screen:
You set the hours for
each day of the week that the company is available. I’m using a single
Business Hours record named Default. You can have multiple business hours
in multiple time zones if you have a complex support organization. With
business hours you can work in specific time zones and locations with:
·
Cases
·
Case escalation rules
·
Case milestones in entitlement processes
This
is accessible through Apex, but you will likely need to customize the logic to
your needs. Salesforce thoughtfully provides a BusinessHours class, but it is very basic, just 3 methods.
Since I needed to check if today was a business day and a few other
things I had to write it myself.
Here is the Apex code:
1 public class BusinessDays {
2
3 private List<Boolean> businessDay =
new Boolean[7];
4 private List<Time> startHours = new
Time [7];
5 private List<Time> endHours = new Time
[7];
6 private Date knownSunday =
date.newInstance(2013, 1, 6);
7
8 //
Constructor creates businessDay array
9
public BusinessDays() {
10
11 BusinessHours bh =
12 [Select
13 SundayStartTime, MondayStartTime,
TuesdayStartTime,
14 WednesdayStartTime, ThursdayStartTime,
FridayStartTime,
15 SaturdayStartTime, SundayEndTime,
MondayEndTime,TuesdayEndTime,
16 WednesdayEndTime, ThursdayEndTime,
FridayEndTime,SaturdayEndTime
17 From BusinessHours
18 Where IsDefault=true];
19
20 businessDay[0] = (bh.SundayStartTime !=
null);
21 businessDay[1] = (bh.MondayStartTime != null);
22 businessDay[2] = (bh.TuesdayStartTime !=
null);
23 businessDay[3] = (bh.WednesdayStartTime !=
null);
24 businessDay[4] = (bh.ThursdayStartTime !=
null);
25 businessDay[5] = (bh.FridayStartTime !=
null);
26 businessDay[6] = (bh.SaturdayStartTime !=
null);
28
29 startHours[0] = bh.SundayStartTime;
30 startHours[1] = bh.MondayStartTime;
31 startHours[2] = bh.TuesdayStartTime;
32 startHours[3] = bh.WednesdayStartTime;
33 startHours[4] = bh.ThursdayStartTime;
34 startHours[5] = bh.FridayStartTime;
35 startHours[6] = bh.SaturdayStartTime;
36
37 endHours[0] = bh.SundayEndTime;
38 endHours[1] = bh.MondayEndTime;
39 endHours[2] = bh.TuesdayEndTime;
40 endHours[3] = bh.WednesdayEndTime;
41 endHours[4] = bh.ThursdayEndTime;
42 endHours[5] = bh.FridayEndTime;
43 endHours[6] = bh.SaturdayEndTime;
44
45 }
46
47 // Check if today is a business day
48 public Boolean isBusinessDay(Date inputDate)
{
49 // index i is index into the businessDay
array based on inputDate
50 Integer i =
Math.mod(Math.abs(this.knownSunday.daysBetween(inputDate)),7);
51 return (businessDay[i]);
52 }
53
54 // Get the start time
53 public Time getStartTime(Date inputDate) {
54 Integer i =
Math.mod(Math.abs(this.knownSunday.daysBetween(inputDate.date)),7);
55 return (startHours[i]);
56 }
57
58 // Gets next business day, skipping non
business days
59 public Date nextBusinessDay(Datetime
inputDatetime) {
60 Integer i =
61 Math.mod(Math.abs(this.knownSunday.daysBetween(inputDatetime.date())),7);
62 Datetime returnDate = inputDatetime;
63 while (!businessDay[Math.mod(i, 7)]) {
64 i++;
65 returnDate = returnDate.addDays(1);
66 }
67 return returnDate.date;
68 }
69
70 }
I build 3 arrays.
One to hold the days of the week the company is open, one to hold the
start time for each day, and one to hold the end time of each day. I’m
not using the end time array at the moment, it is available just in case.
Notice the variable
knowSunday on line 6. You could also use the method Date.toStartOfWeek(),
but you would have to match the indexes to the locale of your company.
According to the Apex reference: ”…the start of a week is Sunday in
the United States locale, and Monday in European locales.” I usually work
for US companies, but I’ve gotten in trouble before by making assumptions about
locale dependent information when working for customers in Europe.
If you want to know if
we are open for business today:
if
(busDays.isBusinessDay(Date.today())) { ...
If
you want to know when we open the doors for customers:
if
(busDays.getStartTime(Date.today())) { ...
If you want to know
what the next day we are open:
Datetime
nextBusDay = busDays.nextBusinessDay(Datetime.now());
These are just a few of the possibilities.
The customers I’ve worked for always seem to have their own unique rules
for determining when to send the invoice, how to calculate the date in that
custom field, and so on. The bad news is that almost nothing is pre-built
in the API. The good news is that we can use Apex to implement whatever
rules our customers dream up.
When I started
programming using web frameworks like Visualforce, it wasn’t entirely clear
what was happening on the server vs the client web browser. I routinely see
questions about this in the Force.com developer forums, so I created a simple
example to illustrate the round trip from the server, to the browser, to the
server and back.
Here is a VF page. It
displays a table of information about the Users. It also has a text input
field, an HTML button, and an output field. When you enter a value in the input
field and click Validate, a popup dialog displays the text, then refreshes part
of the page putting the entered text in the Text Field page block. The entered
text is read and processed by the JavaScript function checkField first. If it
passes validation, in this case a simple check for null, then it is sent to the
server via an actionFunction. Even though the same field is bound in both
places, why doesn’t it update in the outputText (2) after I type it in the
inputText (1)? The answer lies in the page lifecycle as I will explain below.
Here is the page:
We
entered some text, clicked Validate:
JavaScript popup
Voila! The
outputText shows the text:
outputField Updated
Now
lets look at the code. First the VF. The page uses a standard controller for
the User object and a controller extension. There is an apex:pageBlockTable on
line 19 for listing the details of the Users. The field testField is bound in 2
places, lines 29 and 38. When the page is initialized, the apex:inputText and
apex:outputText show the value from the server, null. When you type some text in the inputText, it
is just sitting there in the browser. Its bound to a variable in the
controller, but until the form is submitted, the server won’t be updated.
I could have submitted it to the server with an apex:commandButton, but I
used an HTML button to show that in the browser VF tags are really just your
standard HTML/CSS/JavaScript. When we click the button on line 30, we
call the JavaScript function on line 4. After a check for null, we
finally send the entered text to the server using an apex:actionFunction on
line 31. The server side variable is updated with the entered text,
returned to the server, and the apex:outputPanel on line 35 gets re-rendered.
Finally the apex:outputField displays the text we entered.
1 <apex:page
standardController="User"
extensions="HelloWorldUsers_ConExt"
recordSetVar="users">
2 <script>
3 //
JavaScript function to check the value entered in the inputField
4
function checkField() {
5
var val = document.getElementById('{!$Component.theForm.thePageBlock.theFieldInput}').value;
6 if
(val != null && val != '') {
7
alert('val is '+val);
8
// call the actionFunction, sends the value to the server
9
fieldUpdateFunction(val);
10 }
11 else alert('Please enter some text');
12 }
13 </script>
14
15 <apex:form
id="theForm" >
16
17
<apex:outputPanel id="phoneListPanel" >
18 <apex:pageBlock
title="Phone List">
19
<apex:pageBlockTable value="{!users}" var="user">
20 <apex:column
value="{!user.Name}" />
21 <apex:column
value="{!user.Phone}" />
22 <apex:column
value="{!user.MobilePhone}" />
23 <apex:column
value="{!user.Fax}" />
24
</apex:pageBlockTable>
25
</apex:pageBlock>
26
</apex:outputPanel>
27
28 <apex:pageBlock
id="thePageBlock" title="Test Validation">
29 <apex:inputText
id="theFieldInput" value="{!testField}" />
30 <input
type="button" value="Validate"
onclick="checkField();" />
31
<apex:actionFunction name="fieldUpdateFunction"
32
action="{!fieldUpdate}" rerender="phoneListPanel,fieldPanel"
/>
33
</apex:pageBlock>
34
35
<apex:outputPanel id="fieldPanel">
36 <apex:pageBlock
id="anotherPageBlock" title="Text Field">
37
<apex:pageBlockSection >
38 <apex:outputText
label="Field value from server" value="{!testField}"
/>
39
</apex:pageBlockSection>
40
</apex:pageBlock>
41
</apex:outputPanel>
42
43 </apex:form>
44
45 </apex:page>
There is not much to
it, but lets take a look at the server code, the controller extension:
1 // Controller Extension for HelloWorldUsers
2 public with sharing class
HelloWorldUsers_ConExt {
3
4 //
Field to bind to the inputText component
5
public String testField {public get; public set;}
6
7 //
Simple contructor
8
public HelloWorldUsers_ConExt(ApexPages.StandardSetController
stdController) {
9 }
10
11 // Method called by the actionFunction on
the HelloWorldUsers page
12 public PageReference fieldUpdate() {
13 System.debug('=============>>>
testField: '+testField);
14
15 // return null to keep us on the page
16 return null;
17 }
18
19 }
We have a String
textField on line 5 that is bound to the inputText and outputText. The method
fieldUpdate is called by the actionFunction. On line 16 it returns null so that
when it returns to the browser it stays in the same page.
Knowing the difference
between the server and browser begs the question: do you want to do the
processing in the browser or on the server? Typically you want to do what
you can in the browser. Its ideal to do simple validation, like
name/address/phone format, especially if you don’t need to connect with another
service. You can do more on the server, but it takes a least one round
trip so its usually slower. A complex integration may involve AJAX to the
server, connecting with RESTful web services, and lots of other fun
machinations that are beyond the scope of this post.
I hope this article helps illustrate the
difference between the server and the browser. The minutia of programming
is hard enough, so its critical to understand the bigger picture like the
lifecycle of a Visualforce page.
Clicks, clicks, and
more clicks! Everyone wants the computer to do the work, clicking the
mouse is just too hard! So let’s make the UI in our Salesforce org
shoulder some of the work for our customer. Here I’ll describe how to use
a Visualforce actionSupport tag to automatically populate the Opportunity
probability percentage value when the customer selects a StageName from a
picklist.
First lets look at the
objects we are working with: Opportunity and OpportunityStage. The
sales and marketing folks tell us that Opportunities have distinct Stages.
As they progress through each Stage, chatting up a prospective client,
the chance of landing the Opportunity increases, such that if they make it to
negotiating the contract they have a 90% probability of successfully turning
the Opportunity into a Contract. So they say. There are 10 default
Stage values for a Salesforce org are shown below:
We can represent the
Probability with a Visualforce inputField and have our customer type it in, but
here we will do the work for them.
First the Visualforce.
Here is a snippet of a page with an apex:inputField for the Stage Name
and another apex:inputField for the Probability:
1 <table>
2
<tr>
3
<th>Opportunity Name</th>
4
<td><apex:inputField value="{!thisOpp.Name}"
/></td>
5
</tr>
6
<tr>
7
<th>Opportunity Stage</th>
8
<td>
9
<apex:actionRegion >
10 <apex:inputField
value="{!thisOpp.StageName}" >
11 <apex:actionSupport
event="onchange" action="{!changeStageName}"
12 rerender="probability,messages"
/>
13 </apex:inputField>
14 </apex:actionRegion>
15 </td>
16 </tr>
17 <tr>
18 <th>Probability (%)</th>
19 <td>
20 <apex:inputField
value="{!thisOpp.Probability}" id="probability" />
21 </td>
22 </tr>
23 </table>
The Stage Name will be
rendered in the browser as a picklist. The Probability is rendered as a
text input:
We use an
apex:actionSupport tag to invoke the changeStageName method in our controller
when the picklist value changes. Notice the rerender on line 12. We
don’t want the whole Visualforce page to reload. If there are other
dynamic elements then managing their interactions can get very complex.
We wrap an apex:actionRegion around the picklist so that only the
picklist value is submitted to the server. Once again, if this is part of
a large complex page with other dynamic elements we probably do not want them
all updating with each change. For example, this could cause a problem if
the form needs to be validated only when the entire form is saved.
Now let’s look at the
Apex code in the controller. The actionSupport element action
changeStageName is bound to an identically name method in the controller:
1 // The user changed the Opportunity
StageName. Set the Probability to the
2 // correct value, based on the defaults set
up in the OpportunityStage object.
3 public transient Map<String, Decimal>
probabilityStageNameMap;
4
5 public PageReference changeStageName() {
6
7 if
(probabilityStageNameMap == null) {
8
probabilityStageNameMap = new Map<String, Decimal>();
9
for (OpportunityStage oppStage : [Select MasterLabel, DefaultProbability
10 From
OpportunityStage]) {
11 probabilityStageNameMap.put(oppStage.MasterLabel,
oppStage.DefaultProbability);
12 }
13 }
14
15 if
(probabilityStageNameMap.containsKey(thisOpp.StageName)) {
16 thisOpp.Probability =
probabilityStageNameMap.get(thisOpp.StageName);
17 }
18
19 return null;
20 }
We only create a Map
of StageName to probability once, the first time this method is invoked as you
can see on line 3. Also, notice we return null – we are not navigating to
a new page, we are just quietly updating one field:
The end result is that your customer selects a
StageName from a picklist and automagically the percentage field is populated.
Your customer can manually input a different Probability percentage value
if they choose, but more than likely they want the default value and they don’t
want the extra clicking and typing. The use of dynamic elements like this
will make your interface faster to use and more engaging to your customers.
One of the cardinal rules for programming with Apex is never
make a SOQL query in a loop. Even when you’re 110% sure that the loop
will make no more than 2 passes, you migrate your code from the sandbox to
production and… BOOM! SOQL query limit exceeded! This is particularly bad in code that
is not customer facing because it may be some time before the failure is
apparent.
The answer is to
always bulkify your queries. As an example, you want to get all the Accounts
assigned to the current User, then you want all the Contacts for each Account.
First, the non-bulkified example:
// We want a Map of
Account IDs to all associated Contact IDs
Map <ID, List
<ID>> accountContactMap = new Map <ID, List <ID>>();
// Select all Accounts
owned by the current User
List <Account>
accountList =
[Select ID, Name From Account Where OwnerId
=:UserInfo.getUserId()];
// For each Account
get the associated Contacts
for (Account
nextAccount : accountList) {
// Bad! Very bad! No SOQL in a
loop! What was I thinking!?!??
// Get the Contacts associated with this
Account
List <Contact> nextContacts =
[Select ID, Name From Contact Where
AccountId =:nextAccount.ID];
System.debug('Account '+nextAccount.Name+'
has '+nextContacts.size()+' Contacts');
// Create a List of all the associated
Contacts
List <ID> contactIDs = new List
<ID> ();
for (Contact nextContact : nextContacts) {
contactIDs.add(nextContact.ID);
}
// Add them to the Map
accountContactMap.put(nextAccount.ID,
contactIDs);
}
Now the bulkified
version:
1 // We
want a Map of Account IDs to all associated Contact IDs
2 Map
<ID, List <ID>> accountContactMap = new Map <ID, List
<ID>>();
3
4 //
Select all Accounts owned by the current User
5 List
<Account> accountList =
6
[Select ID, Name From Account Where OwnerId
=:UserInfo.getUserId()];
7
8 //
Make a List of Account IDs
9 List
<ID> accountIDList = new List <ID> ();
10 for (Account nextAccount : accountList) {
11 accountIDList.add(nextAccount.ID);
12 }
13
14 // Now get the Contacts associated with the
Accounts
15 List <Contact> allContactList =
16 [Select ID, Name
17 From Contact
18 Where AccountId in
:accountIDList
19 order by AccountId];
20 if (allContactList.size() == 0) {
20 return;
21 }
22
23 List <Contact> contactList = new List
<Contact> ();
24 ID previousAcctID = contactList[0].AccountID;
25
26 for (Contact nextContact : contactList) {
27 if (nextContact.AccountID !=
previousAcctID) {
28 accountContactMap.put(previousAcctID,
contactList);
29 contactList = new List
<Contact> ();
30 previousAcctID =
nextContact.AccountID;
31 }
32 contactList.add(nextContact.ID);
33 }
34 accountContactMap.put(previousAcctID,
contactList);
The new code is longer
and not as readable, but we’ve gotten rid of the dangerous SOQL in the for
loop.
Notice
the order by AccountId on line 19. This is critical to insure
that we make a clean transition from Contacts of one Account to another.
If you are using this pattern for more dimensions than just ID, you may
have to order by multiple fields. For example, let’s say you have a
custom field Last_Ping_Date__c on Contact. You need the Map of Account
IDs to Contact IDs and you will be charting them by date. This is simple
to do in the query as shown here:
// Now get the Contacts associated with the
Accounts
// Order by date so the chart is properly
ordered
List <Contact> allContactList =
[Select ID, Name
From Contact
Where AccountId in :accountIDList
order by AccountId,
Last_Ping_Date__c];
Don’t forget to test
for nulls and empty collections. The return statement on line 20 is
required because we are referencing field 0 in the List on line 24.
Finally, notice the
second accountContactMap.put() on line 34. We can’t forget to add the
final Account ID and its Contacts to the Map.
One
consequence of this pattern is a generous use of script statements. Of
course, that may run into a different governor limit of a maximum 200,000
script statements. Apex programming with large data sets can be a
juggling act – staying under some limits may bump into others. However,
with only 100 SOQL statements to work with, bulkifying your queries should
always be your first step.
No comments:
Post a Comment