Salesforce Rollup Summary Fields using Apex Code
by Colin Loretz. Average Reading Time: about 6 minutes.
Rollup Summary Fields on any field/object using APEX Code
Level: Intermediate
Rollup Summary Fields
The Rollup Summary fields provided by Salesforce offer a great way to summarize relationship data. If you have two objects, “Timesheet” and “Time Entry”, a rollup summary field can total the number of hours assigned to any given timesheet.
What problem does this address?
Standard rollup summary fields require a master-detail relationship. Currently, you can not setup a master-detail-detail+ relationship so what happens when you have more than 2 objects that you want to summarize? You won’t be able to.
The following Apex code helps solve this situation by allowing you to summarize information on objects that are related by either a Master-Detail relationship or a Lookup relationship field. You can even summarize information in a cascading fashion, by linking more than 2 objects together. I’ve been working on a project management application on the Force.com platform and we have Projects->Tasks->Time Entries. The time entries are summarized by hours and dollars at both the Task and Project level.
In tackling this problem, there are five use cases we must consider:
1. Creating a new record that rolls up to the parent record
2. Deleting an existing record that currently rolls up to the parent record
3. Updating a record with a new value and remains under the same parent record
4. Updating a record with a new value and changing the parent record that it rolls up to
5. The child value remains the same and we change the parent record that is being rolled up to
Disclaimer & Warning:
There are other ways to accomplish this type of functionality so please feel free to critique the code in the comments. The code has been written for processing bulk records but has not been extensively tested for excessively large record sets. IE: A parent object that summarizes 100+ child records that summarizes another 100+ child records below that. The more objects you have cascading under one another, the greater the chances of running into the Salesforce governor limits. Please feel free to provide feedback and recommendations on this as you encounter them.
GETTING STARTED
Step 1: Create the Parent and Child objects
1. Create a parent object:
- Object Label: Weekly Timesheet
- Object Name: Weekly_Timesheet
- Save
2. In the Custom Fields & Relationships area of the new object, click New.
3. Create a new field to store the summarized data:
- Field Type: Number
- Field Label: Total Hours
- Field Name: Total_Hours
- Length: 16
- Decimals: 2
- Save
4. Create a child object:
- Object Label: Time Entry
- Object Name: Time_Entry
- Save
5. In the Custom Fields & Relationships area of the new object, click New.
6. Create two fields on the child object:
- Field Type: Number
- Field Label: Hours
- Field Name: Hours
- Length: 16
- Decimals: 2
- Follow prompts and Save
- Field Type: Lookup Relationship
- Field Label: Timesheet
- Field Name: Timesheet
- Follow prompts and Save
7. Create a trigger (Download formatted code here)
trigger rollupHoursToTimesheet on Time_Entry__c (after insert, after update, after delete) {
double sumTotalHours = 0.0;
Weekly_Timesheet__c [] sheetsToUpdate = new Weekly_Timesheet__c[]{};
//***********************************************
//Code for updating existing records and new records
//***********************************************
if(Trigger.isInsert)
{
Time_Entry__c [] teNew = trigger.new;
for(Time_Entry__c te : teNew)
{
for (Weekly_Timesheet__c timesheet : [select Id, Name, Total_Hours__c from Weekly_Timesheet__c where Id = :te.Timesheet__c])
{
//Sum all the timesheet entries
for (Time_Entry__c timeEntries: [select Id, Hours__c from Time_Entry__c where Timesheet__c = :timesheet.id])
{
sumTotalHours += timeEntries.Hours__c;
}
timesheet.Total_Hours__c = sumTotalHours;
//add timesheet to list to be updated outside of the loop
sheetsToUpdate.add(timesheet);
}
}
//commit the changes to Salesforce
update sheetsToUpdate;
}
//***********************************************
//Code for updating when a record is updated
//***********************************************
else if(Trigger.isUpdate)
{
//sum total both old and new
Time_Entry__c [] oldTime = Trigger.old;
Time_Entry__c [] newTime = Trigger.new;
Double newSum = 0.0;
Double oldSum = 0.0;
for(Time_Entry__c newTe: newTime)
{
for(Time_Entry__c oldTe : oldTime)
{
Weekly_Timesheet__c oldTimesheet = [Select Id, Name, Total_Hours__c from Weekly_Timesheet__c where Id = :oldTe.Timesheet__c];
Weekly_Timesheet__c newTimesheet = [Select Id, Name, Total_Hours__c from Weekly_Timesheet__c where Id = :newTe.Timesheet__c];
Time_Entry__c [] newSumHours = [Select Id, Name, Hours__c from Time_Entry__c where Timesheet__c = :newTimesheet.Id];
Time_Entry__c [] oldSumHours = [Select Id, Name, Hours__c from Time_Entry__c where Timesheet__c = :oldTimesheet.Id];
//sum premiums from child objects
for(Time_Entry__c oldSumHour : oldSumHours)
{
oldSum += oldSumHour.Hours__c;
}
for(Time_Entry__c newSumHour : newSumHours)
{
newSum += newSumHour.Hours__c;
}
newTimesheet.Total_Hours__c = newSum;
oldTimesheet.Total_Hours__c = oldSum;
sheetsToUpdate.add(newTimesheet);
sheetsToUpdate.add(oldTimesheet);
}
}
update sheetsToUpdate;
}
//***********************************************
//Code for updating when a record is deleted
//***********************************************
else if(Trigger.isDelete)
{
Time_Entry__c [] teOld = trigger.old;
for(Time_Entry__c te: teOld)
{
for (Weekly_Timesheet__c timesheet: [select Id, Name, Total_Hours__c from Weekly_Timesheet__c where Id = :te.Timesheet__c])
{
for (Time_Entry__c timeEntries: [select Id, Hours__c from Time_Entry__c where Timesheet__c = :timesheet.id])
{
sumTotalHours += timeEntries.Hours__c;
}
timesheet.Total_Hours__c = sumTotalHours;
sheetsToUpdate.add(timesheet);
}
}
update sheetsToUpdate;
}
}
trigger rollupHoursToTimesheet on Time_Entry__c (after insert, after update, after delete) {
double sumTotalHours = 0.0;
Weekly_Timesheet__c [] sheetsToUpdate = new Weekly_Timesheet__c[]{};
//***********************************************
//Code for updating existing records and new records
//***********************************************
if(Trigger.isInsert)
{
Time_Entry__c [] teNew = trigger.new;
for(Time_Entry__c te : teNew)
{
for (Weekly_Timesheet__c timesheet : [select Id, Name, Total_Hours__c from Weekly_Timesheet__c where Id = :te.Timesheet__c])
{
//Sum all the timesheet entries
for (Time_Entry__c timeEntries: [select Id, Hours__c from Time_Entry__c where Timesheet__c = :timesheet.id])
{
sumTotalHours += timeEntries.Hours__c;
}
timesheet.Total_Hours__c = sumTotalHours;
//add timesheet to list to be updated outside of the loop
sheetsToUpdate.add(timesheet);
}
}
//commit the changes to Salesforce
update sheetsToUpdate;
}
//***********************************************
//Code for updating when a record is updated
//***********************************************
else if(Trigger.isUpdate)
{
//sum total both old and new
Time_Entry__c [] oldTime = Trigger.old;
Time_Entry__c [] newTime = Trigger.new;
Double newSum = 0.0;
Double oldSum = 0.0;
for(Time_Entry__c newTe: newTime)
{
for(Time_Entry__c oldTe : oldTime)
{
Weekly_Timesheet__c oldTimesheet = [Select Id, Name, Total_Hours__c from Weekly_Timesheet__c where Id = :oldTe.Timesheet__c];
Weekly_Timesheet__c newTimesheet = [Select Id, Name, Total_Hours__c from Weekly_Timesheet__c where Id = :newTe.Timesheet__c];
Time_Entry__c [] newSumHours = [Select Id, Name, Hours__c from Time_Entry__c where Timesheet__c = :newTimesheet.Id];
Time_Entry__c [] oldSumHours = [Select Id, Name, Hours__c from Time_Entry__c where Timesheet__c = :oldTimesheet.Id];
//sum premiums from child objects
for(Time_Entry__c oldSumHour : oldSumHours)
{
oldSum += oldSumHour.Hours__c;
}
for(Time_Entry__c newSumHour : newSumHours)
{
newSum += newSumHour.Hours__c;
}
newTimesheet.Total_Hours__c = newSum;
oldTimesheet.Total_Hours__c = oldSum;
sheetsToUpdate.add(newTimesheet);
sheetsToUpdate.add(oldTimesheet);
}
}
update sheetsToUpdate;
}
//***********************************************
//Code for updating when a record is deleted
//***********************************************
else if(Trigger.isDelete)
{
Time_Entry__c [] teOld = trigger.old;
for(Time_Entry__c te: teOld)
{
for (Weekly_Timesheet__c timesheet: [select Id, Name, Total_Hours__c from Weekly_Timesheet__c where Id = :te.Timesheet__c])
{
for (Time_Entry__c timeEntries: [select Id, Hours__c from Time_Entry__c where Timesheet__c = :timesheet.id])
{
sumTotalHours += timeEntries.Hours__c;
}
timesheet.Total_Hours__c = sumTotalHours;
sheetsToUpdate.add(timesheet);
}
}
update sheetsToUpdate;
}
}
8. Create the unit test for the trigger in an Apex class
[COMING SOON]

Colin-
This is interesting code. Great job thinking through all the possibilities. You’ve definitely one-uppped the Quote/Line Item example in the Apex Cookbook.
The only critique I would make is to change the old/new nomenclature. I finally figured out that you’re summing the children of the old target, and then summing the children of the new target – but the words Old and New are too close to Trigger.old and Trigger.new… and I was confused.
Aside from that (minor) comment, I love it.
Thanks very much for sharing this with us.
-David
Colin,
I may have found a bug in the Update section of the trigger. I had to add an IF statement to the last section:
sheetsToUpdate.add(newTimesheet);
if(newTimesheet.Id != oldTimesheet.Id){
sheetsToUpdate.add(oldTimesheet);
}
Otherwise, it was giving me duplicate line errors.
Thanks!
David
I needed to create time entries first and then at a point in the future, assign it to a weekly timesheet. When I tried this it gave me an error trying to total the old value of the weekly timesheet since it didn’t exist until i assigned it. I took out all the calculations for the old value sum and it worked fine. What is the reason for totaling the old value?
@Jeff Rheel:
Part of the issue may be the naming conventions I used (as mentioned by David in his comment).
1. Everytime a time sheet entry is added to a timesheet, we grab that timesheet
2. Instead of incrementing or decrementing the time entry’s value onto the parent object (Timesheet), I sum all the children objects. I did this because I wanted to make sure the value was accurate every time.
3. That sum replaces the Total Hours field on the Timesheet.
I will be revisiting this code this week and changing the variable names to make more sense, as well as cleaning up the code.
My use case is actually a bit unique in that I have a ProjectTask__c object that is related to itself. A ProjectTask can have subtasks, each of which have subtasks. The code as it is written know will only work up for up to 3 levels of subtasks before hitting the server governor limits.
Updated code and test coverage code will be available soon.
Very nice implementation, thanks.
Hello Collin, I think it’s great that you share this with the rest of the world, thank you. However your implementation has some drawbacks, because during a batch operation too many soql queries will be executed. I will recommend you to follow the next implementation:
if(Trigger.isAfter){
Double sumTotalHours = 0;
Map TimeSheetsToUpdate = new Map();
Set TimeSheetsIDs = new Set();
Map TimeSheets = new Map();
//***********************************************
//Code for updating existing records and new records
//***********************************************
if(Trigger.isInsert){
for(Time_Entry__c te : trigger.new){
if(te.TimeSheet__c != null){
if(!TimeSheetsIDs.contains(te.TimeSheet__c)){
TimeSheetsIDs.add(te.TimeSheet__c);
}
}
}
for (TimeSheet__c ts : [select Id, Name, Total_Hours__c,
(select Id, Hours__c from Time_Entries__r)
from TimeSheet__c where Id in :TimeSheetsIDs]){
TimeSheetsToUpdate.put(ts.id,ts);
}
for(TimeSheet__c ts: TimeSheetsToUpdate.values()){
for (Time_Entry__c te: ts.Time_Entries__r){
sumTotalHours += te.Hours__c;
}
ts.Total_Hours__c = sumTotalHours;
}
//commit the changes to Salesforce
update TimeSheetsToUpdate.values();
}
//***********************************************
//Code for updating when a record is updated
//***********************************************
else if(Trigger.isUpdate){
for(Time_Entry__c te : trigger.old){
if(te.TimeSheet__c != null){
if(!TimeSheetsIDs.contains(te.TimeSheet__c)){
TimeSheetsIDs.add(te.TimeSheet__c);
}
}
}
for(Time_Entry__c te : trigger.new){
if(te.TimeSheet__c != null){
if(!TimeSheetsIDs.contains(te.TimeSheet__c)){
TimeSheetsIDs.add(te.TimeSheet__c);
}
}
}
for (TimeSheet__c ts : [select Id, Name, Total_Hours__c,
(select Id, Hours__c from Time_Entries__r)
from TimeSheet__c where Id in :TimeSheetsIDs]){
TimeSheetsToUpdate.put(ts.id,ts);
}
for(TimeSheet__c ts: TimeSheetsToUpdate.values()){
for (Time_Entry__c te: ts.Time_Entries__r){
sumTotalHours += te.Hours__c;
}
ts.Total_Hours__c = sumTotalHours;
}
//commit the changes to Salesforce
update TimeSheetsToUpdate.values();
}
//***********************************************
//Code for updating when a record is deleted
//***********************************************
else if(Trigger.isDelete){
for(Time_Entry__c te : trigger.old){
if(te.TimeSheet__c != null){
if(!TimeSheetsIDs.contains(te.TimeSheet__c)){
TimeSheetsIDs.add(te.TimeSheet__c);
}
}
}
for (TimeSheet__c ts : [select Id, Name, Total_Hours__c,
(select Id, Hours__c from Time_Entries__r)
from TimeSheet__c where Id in :TimeSheetsIDs]){
TimeSheetsToUpdate.put(ts.id,ts);
}
for(TimeSheet__c ts: TimeSheetsToUpdate.values()){
for (Time_Entry__c te: ts.Time_Entries__r){
sumTotalHours += te.Hours__c;
}
ts.Total_Hours__c = sumTotalHours;
}
//commit the changes to Salesforce
update TimeSheetsToUpdate.values();
}
}
Hello again, sorry just realized that I had a lot of redundant code. Here’s a much more cleaner version.
Cheers!
Map TimeSheetsToUpdate = new Map();
Set TimeSheetsIDs = new Set();
//************************************************
//Code for updating existing records and new records
//************************************************
if(Trigger.isInsert || Trigger.isUpdate){
for(Time_Entry__c te : trigger.new){
if(te.TimeSheet__c != null){
if(!TimeSheetsIDs.contains(te.TimeSheet__c)){
TimeSheetsIDs.add(te.TimeSheet__c);
}
}
}
}
if(Trigger.isDelete || Trigger.isUpdate){
for(Time_Entry__c te : trigger.old){
if(te.TimeSheet__c != null){
if(!TimeSheetsIDs.contains(te.TimeSheet__c)){
TimeSheetsIDs.add(te.TimeSheet__c);
}
}
}
}
if(LineItemsIDs.size() > 0){
for (TimeSheet__c ts : [select Id, Name, Total_Hours__c,
(select Id, Hours__c from Time_Entries__r)
from TimeSheet__c where Id in :TimeSheetsIDs]){
TimeSheetsToUpdate.put(li.id,li);
}
for(TimeSheet__c ts: TimeSheetsToUpdate.values()){
Double SumOfHours = 0;
for (Time_Entry__c te: ts.Time_Entries__r){
SumOfHours += te.Hours__c ;
}
ts.Total_Hours__c = SumOfHours;
}
//commit the changes to Salesforce
update TimeSheetsToUpdate.values();
}
thanks for the clean and clear code. Helpful to learn with and provides a solid solution to rollup summary not being available for certain object relationships.
To those that utilised this code….
I’m completely new to Apex and this functionality is exactly what I’m looking for.
I’ve completed all the above steps, taking on board the comments and I have the trigger sat in Eclipse ready to deploy.
However, the Deploy wizard tells me the trigger needs to be tested.
I’m wondering if anyone can provide me with a class they used to test this trigger?
Thanks,
James
Colin, you are a stud. Thanks so much for publishing this!
This looks very useful… I will test it….
When can we have the test code? :)
Thanks.
@CK – I plan to revisit this at some point very soon and incorporate some better bulk processing and hopefully that will include posting the test code. However, for now you will have to run it on a developer org :D
Yes, please post soon!! I need this!
The 2 place decimal doesn’t act as minutes though. This would be useful
if you entry 0.90 as one of the hours would be converted to 1.30
I’ve been trying to implement this but have been having problems with the cleaner version. I keep getting map errors : Unexpected token at line 2. I am very new to triggers and I am sure that if I were more familar with them I could probably figure this out. The first few lines of the clean code is this:
trigger rollupHoursToTimesheet on Time_Entry__c (after insert, after update, after delete) {
if(Trigger.isAfter){
Double sumTotalHours = 0;
Map TimeSheetsToUpdate = new Map();
Set TimeSheetsIDs = new Set();
//************************************************
//Code for updating existing records and new records
//************************************************
Thanks for posting your code! Being new to APEX this provided a great example. Did you ever get around to writing the Test Unit code for this by chance? I was able to take your example code and get it working for our objects, but unfortunately not skilled enough in APEX yet to write my own test units.
I guess this code is not working after salesforce update, does anyone can simply provide a sample trigger with test please?
Thank you in advance
[...] Salesforce Rollup Summary Fields using Apex Code – Colin Loretz [...]
Thanks for the code Colin! I have a revised version of the trigger on my blog that uses aggregate queries and maps in order limit exceptions that might be thrown out due to governor limits.
http://www.anthonyvictorio.com/salesforce/roll-up-summary-trigger/
I hope it helps, its definitely dated and is probably something I should revisit. Force.com has changed quite a bit since 2008 and I know this could be done more efficiently. Hope it helped with direction.
It very much helped, I wouldn’t have known were to start without your code. I’m far better at pulling things apart than at creating new things from scratch =P