Update: @hammnick pointed out that you can in fact do this using roll-up summaries. I swear I checked this before hand and it wasn’t working. Regardless, I will keep this post up as I think it provides a useful learning tool. This would be more useful in a situation where you do not have a Master-Detail relationship and still want to gather counts. I am glad I didn’t spend too much time in this!
I was reading through @nikpanter‘s blog a little while ago and came across this post about learning Apex: http://www.xlerate.ca/nik/?p=165. In that post Nik mentioned writing a trigger to count the number of opportunities tied to an account as well as the number of won and number of closed opportunities. I reached out to Nik and confirmed that he hadn’t written this trigger and then proceeded to write it myself.
Since we are keeping track of opportunity counts on the account, we need to remember to update the account whenever an opportunity is inserted, updated, deleted or undeleted. We can calculate the stats after each of these actions are done so this will be an ‘After’ trigger for each of those events on the opportunity object.
We will need three custom fields on the Account to hold the values. I created the following fields:
- Total # of Opportunities (Total_Number_of_Opportunities__c)
- # of Closed Opportunities (Num_of_Closed_Opportunities__c)
- # of Won Opportunities (Num_of_Won_Opportunities__c)
trigger updateAccountOpportunityCounts on Opportunity (after insert, after update, after delete, after undelete) {
////////////////////////////////////////////////////
//Set up Map, a Set and a List
//Map will hold accounts and all opportunities tied to it
//Set will hold Ids of accounts for the opportunities in the trigger
//List to hold accounts which we will be updating
////////////////////////////////////////////////////
Map<Account, List<Opportunity>> totalCountMap = new Map<Account, List<Opportunity>>();
Set<Id> accountIdSet = new Set<Id>();
List<Account> accountUpdateList = new List<Account>();
////////////////////////////////////////////////////
//Create the set of Account IDs.
//If this is a delete trigger, pull them the trigger.old, otherwise from trigger.new
////////////////////////////////////////////////////
if(Trigger.isDelete)
for(Opportunity o : trigger.old)
accountIdSet.add(o.AccountId);
else
for(Opportunity o : trigger.new)
accountIdSet.add(o.AccountId);
//Query for all of the accounts assoicated with the opportunities in the trigger using the above created set of Ids
Map<Id, Account> accountMap = new Map<Id, Account>([select Id, Name, Total_Number_of_Opportunities__c, Num_of_Won_Opportunities__c, Num_of_Closed_Opportunities__c from Account where Id IN :accountIdSet]);
////////////////////////////////////////////////////
//loop through each of the opportunities associated with the above accounts
//add them to lists in the totalCountMap
//**Note that in a delete trigger (since this is after delete), any deleted opportunities will NOT appear in the query
////////////////////////////////////////////////////
for(Opportunity o: [select Id, AccountId, IsWon, IsClosed from Opportunity where AccountId IN :accountMap.keySet()]){
//temporarily list to either get existing list from Map or insert new list to map
List<Opportunity> tempList = new List<Opportunity>();
//if this account already exists, get the list of opportunities so we can add this opportunity to it.
if(totalCountMap.containsKey(accountMap.get(o.AccountId)))
tempList = totalCountMap.get(accountMap.get(o.AccountId));
//add the opportunity to the list (whether it is new or existing)
tempList.add(o);
//Add the Account and list of opportunites into the Map. This will replace a map entry if it already exists
totalCountMap.put(accountMap.get(o.AccountId),tempList);
}
////////////////////////////////////////////////////
//loop through all of the accounts in the map and get the counts for total number of opps,
//count of closed and count of won opps.
////////////////////////////////////////////////////
for(Account a : totalCountMap.keySet()){
//initialize our counters
Integer wonOpps = 0;
Integer closedOpps = 0;
//loop through the associated opportunities and count up the opportunities
for(Opportunity o : totalCountMap.get(a)){
if(o.IsClosed){
closedOpps++;
if(o.IsWon) wonOpps++; //can't be won without being closed, reduce script statments by checking this here
}
}
//set the counts
a.Total_Number_of_Opportunities__c = totalCountMap.get(a).size();
a.Num_of_Won_Opportunities__c = wonOpps;
a.Num_of_Closed_Opportunities__c = closedOpps;
//add the account to the list
accountUpdateList.add(a);
}
//update all of the accounts if the list isn't empty
if(!accountUpdateList.IsEmpty()) update accountUpdateList;
}
@isTest
private class testUpdateAccountOpportunityCounts {
static testMethod void myUnitTest() {
//Create an account
Account a = new Account(Name='My Freakin Awesome Test Account');
insert a;
//create, say, 200 opportunites attached to said account
List<Opportunity> oList = new List<Opportunity>();
for(Integer i=1; i<201; i++){
oList.add(new Opportunity(Name='Opportunity ' + i,
AccountId=a.id,
StageName='Qualification',
closeDate=Date.newInstance(2100,01,01)
)
);
}
insert oList;
//Make sure we actually have 200 opportunities and none of them are closed or closed won.
a = [select Total_Number_of_Opportunities__c, Num_of_Won_Opportunities__c, Num_of_Closed_Opportunities__c from Account where Id = :a.id limit 1];
System.assertEquals(200, a.Total_Number_of_Opportunities__c);
System.assertEquals(0, a.Num_of_Won_Opportunities__c);
System.assertEquals(0, a.Num_of_Closed_Opportunities__c);
//Now let's close half of the opportunities, 50 of those won, other 50 lost
for(Integer i=0; i<100; i++){
if(i<50)
oList[i].StageName='Closed Won';
else
oList[i].StageName='Closed Lost';
}
update oList;
a = [select Total_Number_of_Opportunities__c, Num_of_Won_Opportunities__c, Num_of_Closed_Opportunities__c from Account where Id = :a.id limit 1];
System.assertEquals(200, a.Total_Number_of_Opportunities__c);
System.assertEquals(50, a.Num_of_Won_Opportunities__c);
System.assertEquals(100, a.Num_of_Closed_Opportunities__c);
//Let's delete some opportunities now (going to delete every other in the list)
List<Opportunity> deleteList = new List<Opportunity>();
for(Integer i=0; i<200; i++){
deleteList.add(oList[i]);
i++; //to skip every other
}
delete deleteList;
a = [select Total_Number_of_Opportunities__c, Num_of_Won_Opportunities__c, Num_of_Closed_Opportunities__c from Account where Id = :a.id limit 1];
System.assertEquals(100, a.Total_Number_of_Opportunities__c);
System.assertEquals(25, a.Num_of_Won_Opportunities__c);
System.assertEquals(50, a.Num_of_Closed_Opportunities__c);
//Finally, let's undelete the deleted opportunities
undelete deleteList;
a = [select Total_Number_of_Opportunities__c, Num_of_Won_Opportunities__c, Num_of_Closed_Opportunities__c from Account where Id = :a.id limit 1];
System.assertEquals(200, a.Total_Number_of_Opportunities__c);
System.assertEquals(50, a.Num_of_Won_Opportunities__c);
System.assertEquals(100, a.Num_of_Closed_Opportunities__c);
}
}
You can install this trigger and class along with the above three fields. You will need to add the fields to your page layouts if you choose too.
https://login.salesforce.com/packaging/installPackage.apexp?p0=04tE00000000Rnx