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
Developer.mikie.Apex.StudentDeveloper.mikie.Apex.Student 

Help with LREngine Filtering and Bug

Hey there,

I have a roll up tool that I found on the internet called LREngine, created by someone named: abhinavguptas. The trigger is amazing and is the most reliable trigger for rolling up values through a lookup I have come across, especially when you are rolling up more than one field. However, this trigger has two bugs. One is that it doesn't like to accept filtering methods and the other is that...should the rolluped records be deleted one by one, the value will deduct each records number worth, until the last record where it will keep the last figure in the rollup field.

E.G. Parent: Rolled up field = 6, child fields = 2 per record. Deleted one by one and when all child records are deleted, rolled up field will still equal 2.

Thank you for any help in advance. Below I will post the Class and the standard trigger with the bits I have changed or added as bold:

Class:

/*
Copyright (c) 2012 tgerm.com
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

1. Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in the
   documentation and/or other materials provided with the distribution.
3. The name of the author may not be used to endorse or promote products
   derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
    LREngine("L"ookup "R"ollup Engine) : This class simplifies rolling up on the child records in lookup relationship.
*/
public class LREngine {

    /*
        Tempalte tokens
            0 : Fields to project
            1 : Object to query
            2 : Optional WHERE clause filter to add
            3 : Group By field name
    */
    static String SOQL_TEMPLATE = 'SELECT {0} FROM {1} {2} GROUP BY {3}';

    /**
        Key driver method that rolls up lookup fields based on the context. This is specially useful in Trigger context.

        @param ctx Context the complete context required to rollup
        @param detailRecordsFromTrigger child/detail records which are modified/created/deleted during the Trigger
        @returns Array of in memory master objects. These objects are not updated back to the database
                because we want client or calling code to have this freedom to do some post processing and update when required.
    */
    public static Sobject[] rollUp(Context ctx, Sobject[] detailRecordsFromTrigger) {

        // API name of the lookup field on detail sobject
        String lookUpFieldName = ctx.lookupField.getName();

        Set<Id> masterRecordIds = new Set<Id>();
        for (Sobject kid : detailRecordsFromTrigger) {
            masterRecordIds.add((Id)kid.get(lookUpFieldName));
        }
        return rollUp(ctx, masterRecordIds);
    }

    /**
        Key driver method that rolls up lookup fields based on the context. This is meant to be called from non trigger contexts like
        scheduled/batch apex, where we want to rollup on some master record ids.

        @param Context the complete context required to rollup
        @param masterIds Master record IDs whose child records should be rolled up.
        @returns Array of in memory master objects. These objects are not updated back to the database
                because we want client or calling code to have this freedom to do some post processing and update when required.
    */
    public static Sobject[] rollUp(Context ctx,  Set<Id> masterIds) {
        // K: Id of master record
        // V: Empty sobject with ID field, this will be used for updating the masters
        Map<Id, Sobject> masterRecordsMap = new Map<Id, Sobject>();
        for (Id mId : masterIds) {
            masterRecordsMap.put(mId, ctx.master.newSobject(mId));
        }

        // #0 token : SOQL projection
        String soqlProjection = ctx.lookupField.getName();
        // k: detail field name, v: master field name
        Map<String, String> detail2MasterFldMap = new Map<String, String>();
        // k: detail field name, v: detail field small alias
        Map<String, String> detailFld2AliasMap = new Map<String, String>();
        Integer aliasCounter = 1;
        for (RollupSummaryField rsf : ctx.fieldsToRoll) {
            /* Keeping alias short i.e. d1, d2...dn to allow more chars in SOQL to avoid SOQL char limit.
                It also helps in avoiding this exception when field names are more than 25 chars i.e.
                System.QueryException: alias is too long, maximum of 25 characters: Annualized_Recurring_Revenue__c
               
                A Sample query with new alias format will look like following
                SELECT AccountId, Avg(Amount) d1, Count(CloseDate) d2 FROM Opportunity  GROUP BY AccountId
            */
            String aliasName = 'd' + aliasCounter++;
            // create aggreate projection with alias for easy fetching via AggregateResult class
            // i.e. SUM(Amount) Amount
            soqlProjection += ', ' + rsf.operation + '(' + rsf.detail.getName() + ') ' + aliasName;
            detail2MasterFldMap.put(rsf.detail.getName(), rsf.master.getName());
            // store the alias for the detail field name, it will be used for loading up later
            detailFld2AliasMap.put(rsf.detail.getName(), aliasName);
        }

        // #1 token for SOQL_TEMPLATE
        String detailTblName = ctx.detail.getDescribe().getName();
       
        // #2 Where clause
        String whereClause = '';
        if (ctx.detailWhereClause != null && ctx.detailWhereClause.trim().length() > 0) {
            whereClause = 'WHERE ' + ctx.detailWhereClause ;
        }
       
        // #3 Group by field
        String grpByFld = ctx.lookupField.getName();
       
        String soql = String.format(SOQL_TEMPLATE, new String[]{soqlProjection, detailTblName, whereClause, grpByFld});
       
        // aggregated results
        List<AggregateResult> results = Database.query(soql);
       
        for (AggregateResult res : results){
            Id masterRecId = (Id)res.get(grpByFld);
            Sobject masterObj = masterRecordsMap.get(masterRecId);
            if (masterObj == null) {
                System.debug(Logginglevel.WARN, 'No master record found for ID :' + masterRecId);
                continue;
            }

            for (String detailFld : detail2MasterFldMap.keySet()) {
                // Load the alias name to fetch the value from the aggregated result
                String aliasName = detailFld2AliasMap.get(detailFld);
                Object aggregatedDetailVal = res.get(aliasName);
                masterObj.put(detail2MasterFldMap.get(detailFld), aggregatedDetailVal);
            }          
        }
       
        return masterRecordsMap.values();  
    }




    /**
        Exception throwed if Rollup Summary field is in bad state
    */
    public class BadRollUpSummaryStateException extends Exception {}

   /**
       Which rollup operation you want to perform
    */
    public enum RollupOperation {
        Sum, Max, Min, Avg, Count
    }
   
    /**
        Represents a "Single" roll up field, it contains
        - Master field where the rolled up info will be saved
        - Detail field that will be rolled up via any operation i.e. sum, avg etc
        - Operation to perform i.e. sum, avg, count etc
           
    */
    public class RollupSummaryField {
        public Schema.Describefieldresult master;
        public Schema.Describefieldresult detail;
        public RollupOperation operation;
       
        // derived fields, kept like this to save script lines later, by saving the same
        // computations over and over again
        public boolean isMasterTypeNumber;
        public boolean isDetailTypeNumber;
        public boolean isMasterTypeDateOrTime;
        public boolean isDetailTypeDateOrTime;
       
        public RollupSummaryField(Schema.Describefieldresult m,
                                         Schema.Describefieldresult d, RollupOperation op) {
            this.master = m;
            this.detail = d;
            this.operation = op;
            // caching these derived attrbutes for once
            // as their is no view state involved here
            // and this caching will lead to saving in script lines later on
            this.isMasterTypeNumber = isNumber(master.getType());
            this.isDetailTypeNumber = isNumber(detail.getType());
            this.isMasterTypeDateOrTime = isDateOrTime(master.getType());
            this.isDetailTypeDateOrTime = isDateOrTime(detail.getType());
            // validate if field is good to work on later
            validate();
        }  
       
        void validate() {
            if (master == null || detail == null || operation == null)
                throw new BadRollUpSummaryStateException('All of Master/Detail Describefieldresult and RollupOperation info is mandantory');

            if ( (!isMasterTypeDateOrTime && !isMasterTypeNumber)
                  || (!isDetailTypeDateOrTime && !isDetailTypeNumber)) {
                    throw new BadRollUpSummaryStateException('Only Date/DateTime/Time/Numeric fields are allowed');
            }
           
            if (isMasterTypeDateOrTime && (RollupOperation.Sum == operation || RollupOperation.Avg == operation)) {
                throw new BadRollUpSummaryStateException('Sum/Avg doesnt looks like valid for dates ! Still want, then implement the IRollerCoaster yourself and change this class as required.');
            }
        }
       
        boolean isNumber (Schema.Displaytype dt) {
            return dt == Schema.Displaytype.Currency
                   || dt == Schema.Displaytype.Integer
                   || dt == Schema.Displaytype.Percent
                   || dt == Schema.Displaytype.Double;
        }
       
        boolean isDateOrTime(Schema.DisplayType dt) {
            return dt == Schema.Displaytype.Time
                   || dt == Schema.Displaytype.Date
                   || dt == Schema.Displaytype.Datetime;
        }
    }
   
    /**
        Context having all the information about the rollup to be done.
        Please note : This class encapsulates many rollup summary fields with different operations.
    */
    public class Context {
        // Master Sobject Type
        public Schema.Sobjecttype master;
        // Child/Details Sobject Type
        public Schema.Sobjecttype detail;
        // Lookup field on Child/Detail Sobject
        public Schema.Describefieldresult lookupField;
        // various fields to rollup on
        public List<RollupSummaryField> fieldsToRoll;

        // Where clause or filters to apply while aggregating detail records
        public String detailWhereClause;           

        public Context(Schema.Sobjecttype m, Schema.Sobjecttype d,
                           Schema.Describefieldresult lf) {
            this(m, d, lf, '');                            
        }
       
        public Context(Schema.Sobjecttype m, Schema.Sobjecttype d,
                           Schema.Describefieldresult lf, String detailWhereClause) {
            this.master = m;
            this.detail = d;
            this.lookupField = lf;
            this.detailWhereClause = detailWhereClause;
            this.fieldsToRoll = new List<RollupSummaryField>();
        }

        /**
            Adds new rollup summary fields to the context
        */
        public void add(RollupSummaryField fld) {
            this.fieldsToRoll.add(fld);
        }
    }   
}

Trigger:

trigger OffInvRollup on Office_Invoice__c (after insert, after update,
                                        after delete, after undelete) {



   // modified objects whose parent records should be updated
     Office_Invoice__c[] objects = null;  

     if (Trigger.isDelete) {
         objects = Trigger.old;
     } else {
        /*
            Handle any filtering required, specially on Trigger.isUpdate event. If the rolled up fields
            are not changed, then please make sure you skip the rollup operation.
            We are not adding that for sake of similicity of this illustration.
        */
        objects = Trigger.new;
     }
    

         List<Office_Invoice__c > objects1 = new List<Office_Invoice__c >();
    for(Office_Invoice__c m : objects)
        if(m.Payment_Method__c == 'Commission Deduction')
            objects1.add(m);

    
    

     /*
      First step is to create a context for LREngine, by specifying parent and child objects and
      lookup relationship field name
     */
     LREngine.Context ctx = new LREngine.Context(Office_Commission__c.SobjectType, // parent object
                                            Office_Invoice__c.SobjectType,  // child object
                                            Schema.SObjectType.Office_Invoice__c.fields.Office_Commission__c);    
     /*
      Next, one can add multiple rollup fields on the above relationship.
      Here specify
       1. The field to aggregate in child object
       2. The field to which aggregated value will be saved in master/parent object
       3. The aggregate operation to be done i.e. SUM, AVG, COUNT, MIN/MAX
     */

     ctx.add(
            new LREngine.RollupSummaryField(
                                            Schema.SObjectType.Office_Commission__c.fields.Office_Invoice_Amount_Total__c,
                                            Schema.SObjectType.Office_Invoice__c.fields.Office_Invoice_Amount__c,
                                            LREngine.RollupOperation.Sum
                                         ));
                                        
                                 ctx.add(
            new LREngine.RollupSummaryField(
                                            Schema.SObjectType.Office_Commission__c.fields.Office_Invoice_GST_Amount_Total__c,
                                            Schema.SObjectType.Office_Invoice__c.fields.Office_Invoice_GST_Amount__c,
                                            LREngine.RollupOperation.Sum
                                         ));
                                        
          
     /*
      Calling rollup method returns in memory master objects with aggregated values in them.
      Please note these master records are not persisted back, so that client gets a chance
      to post process them after rollup
      */
     Sobject[] masters = LREngine.rollUp(ctx, objects1);   

     // Persiste the changes in master
   try{     update masters;
   } catch(DmlException e) {
                for(integer i=0;i<e.getNumDml();i++) {
                    System.debug(e.getDmlMessage(i));
                }
                }
}

Thank you for your help
Best Answer chosen by Developer.mikie.Apex.Student
CheyneCheyne
To get it to calculate the rollups when the last record is deleted, try making the following additions to the Apex class (in bold):

Class:

/*
Copyright (c) 2012 tgerm.com
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

1. Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in the
   documentation and/or other materials provided with the distribution.
3. The name of the author may not be used to endorse or promote products
   derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
    LREngine("L"ookup "R"ollup Engine) : This class simplifies rolling up on the child records in lookup relationship.
*/
public class LREngine {

    /*
        Tempalte tokens
            0 : Fields to project
            1 : Object to query
            2 : Optional WHERE clause filter to add
            3 : Group By field name
    */
    static String SOQL_TEMPLATE = 'SELECT {0} FROM {1} {2} GROUP BY {3}';
    static String COUNT_SOQL_TEMPLATE = 'SELECT Id FROM {0} WHERE Id NOT IN (SELECT {1} FROM {2})';

    /**
        Key driver method that rolls up lookup fields based on the context. This is specially useful in Trigger context.

        @param ctx Context the complete context required to rollup
        @param detailRecordsFromTrigger child/detail records which are modified/created/deleted during the Trigger
        @returns Array of in memory master objects. These objects are not updated back to the database
                because we want client or calling code to have this freedom to do some post processing and update when required.
    */
    public static Sobject[] rollUp(Context ctx, Sobject[] detailRecordsFromTrigger) {

        // API name of the lookup field on detail sobject
        String lookUpFieldName = ctx.lookupField.getName();

        Set<Id> masterRecordIds = new Set<Id>();
        for (Sobject kid : detailRecordsFromTrigger) {
            masterRecordIds.add((Id)kid.get(lookUpFieldName));
        }
        return rollUp(ctx, masterRecordIds);
    }

    /**
        Key driver method that rolls up lookup fields based on the context. This is meant to be called from non trigger contexts like
        scheduled/batch apex, where we want to rollup on some master record ids.

        @param Context the complete context required to rollup
        @param masterIds Master record IDs whose child records should be rolled up.
        @returns Array of in memory master objects. These objects are not updated back to the database
                because we want client or calling code to have this freedom to do some post processing and update when required.
    */
    public static Sobject[] rollUp(Context ctx,  Set<Id> masterIds) {
        // K: Id of master record
        // V: Empty sobject with ID field, this will be used for updating the masters
        Map<Id, Sobject> masterRecordsMap = new Map<Id, Sobject>();
        for (Id mId : masterIds) {
            masterRecordsMap.put(mId, ctx.master.newSobject(mId));
        }

        // #0 token : SOQL projection
        String soqlProjection = ctx.lookupField.getName();
        // k: detail field name, v: master field name
        Map<String, String> detail2MasterFldMap = new Map<String, String>();
        // k: detail field name, v: detail field small alias
        Map<String, String> detailFld2AliasMap = new Map<String, String>();
        Integer aliasCounter = 1;
        for (RollupSummaryField rsf : ctx.fieldsToRoll) {
            /* Keeping alias short i.e. d1, d2...dn to allow more chars in SOQL to avoid SOQL char limit.
                It also helps in avoiding this exception when field names are more than 25 chars i.e.
                System.QueryException: alias is too long, maximum of 25 characters: Annualized_Recurring_Revenue__c
              
                A Sample query with new alias format will look like following
                SELECT AccountId, Avg(Amount) d1, Count(CloseDate) d2 FROM Opportunity  GROUP BY AccountId
            */
            String aliasName = 'd' + aliasCounter++;
            // create aggreate projection with alias for easy fetching via AggregateResult class
            // i.e. SUM(Amount) Amount
            soqlProjection += ', ' + rsf.operation + '(' + rsf.detail.getName() + ') ' + aliasName;
            detail2MasterFldMap.put(rsf.detail.getName(), rsf.master.getName());
            // store the alias for the detail field name, it will be used for loading up later
            detailFld2AliasMap.put(rsf.detail.getName(), aliasName);
        }

        // #1 token for SOQL_TEMPLATE
        String detailTblName = ctx.detail.getDescribe().getName();
      
        // #2 Where clause
        String whereClause = '';
        if (ctx.detailWhereClause != null && ctx.detailWhereClause.trim().length() > 0) {
            whereClause = 'WHERE ' + ctx.detailWhereClause ;
        }
      
        // #3 Group by field
        String grpByFld = ctx.lookupField.getName();
      
        String soql = String.format(SOQL_TEMPLATE, new String[]{soqlProjection, detailTblName, whereClause, grpByFld});
      
        // aggregated results
        List<AggregateResult> results = Database.query(soql);
      
        for (AggregateResult res : results){
            Id masterRecId = (Id)res.get(grpByFld);
            Sobject masterObj = masterRecordsMap.get(masterRecId);
            if (masterObj == null) {
                System.debug(Logginglevel.WARN, 'No master record found for ID :' + masterRecId);
                continue;
            }

            for (String detailFld : detail2MasterFldMap.keySet()) {
                // Load the alias name to fetch the value from the aggregated result
                String aliasName = detailFld2AliasMap.get(detailFld);
                Object aggregatedDetailVal = res.get(aliasName);
                masterObj.put(detail2MasterFldMap.get(detailFld), aggregatedDetailVal);
            }         
        }

        //since the aggregate query above won't return any results when there are zero child records,
        //we need to do another query to get these records
        String masterTblName = ctx.master.getDescribe().getName();
        soql = String.format(COUNT_SOQL_TEMPLATE, new String[]{masterTblName, grpByFld, detailTblName});
        List<SObject> zeroChildrenRes = Database.query(soql);
        for (SObject obj : zeroChildrenRes) {
            Id masterRecId = obj.Id;
            Sobject masterObj = masterRecordsMap.get(masterRecId);
            if (masterObj == null) {
                System.debug(Logginglevel.WARN, 'No master record found for ID :' + masterRecId);
                continue;
            }
     
            //Since none of these master records have any children, just null this rollup
            for (String detailFld : detail2MasterFldMap.keySet()) {
                masterObj.put(detail2MasterFldMap.get(detailFld), null);
            }
        }

      
        return masterRecordsMap.values(); 
    }




    /**
        Exception throwed if Rollup Summary field is in bad state
    */
    public class BadRollUpSummaryStateException extends Exception {}

   /**
       Which rollup operation you want to perform
    */
    public enum RollupOperation {
        Sum, Max, Min, Avg, Count
    }
  
    /**
        Represents a "Single" roll up field, it contains
        - Master field where the rolled up info will be saved
        - Detail field that will be rolled up via any operation i.e. sum, avg etc
        - Operation to perform i.e. sum, avg, count etc
          
    */
    public class RollupSummaryField {
        public Schema.Describefieldresult master;
        public Schema.Describefieldresult detail;
        public RollupOperation operation;
      
        // derived fields, kept like this to save script lines later, by saving the same
        // computations over and over again
        public boolean isMasterTypeNumber;
        public boolean isDetailTypeNumber;
        public boolean isMasterTypeDateOrTime;
        public boolean isDetailTypeDateOrTime;
      
        public RollupSummaryField(Schema.Describefieldresult m,
                                         Schema.Describefieldresult d, RollupOperation op) {
            this.master = m;
            this.detail = d;
            this.operation = op;
            // caching these derived attrbutes for once
            // as their is no view state involved here
            // and this caching will lead to saving in script lines later on
            this.isMasterTypeNumber = isNumber(master.getType());
            this.isDetailTypeNumber = isNumber(detail.getType());
            this.isMasterTypeDateOrTime = isDateOrTime(master.getType());
            this.isDetailTypeDateOrTime = isDateOrTime(detail.getType());
            // validate if field is good to work on later
            validate();
        } 
      
        void validate() {
            if (master == null || detail == null || operation == null)
                throw new BadRollUpSummaryStateException('All of Master/Detail Describefieldresult and RollupOperation info is mandantory');

            if ( (!isMasterTypeDateOrTime && !isMasterTypeNumber)
                  || (!isDetailTypeDateOrTime && !isDetailTypeNumber)) {
                    throw new BadRollUpSummaryStateException('Only Date/DateTime/Time/Numeric fields are allowed');
            }
          
            if (isMasterTypeDateOrTime && (RollupOperation.Sum == operation || RollupOperation.Avg == operation)) {
                throw new BadRollUpSummaryStateException('Sum/Avg doesnt looks like valid for dates ! Still want, then implement the IRollerCoaster yourself and change this class as required.');
            }
        }
      
        boolean isNumber (Schema.Displaytype dt) {
            return dt == Schema.Displaytype.Currency
                   || dt == Schema.Displaytype.Integer
                   || dt == Schema.Displaytype.Percent
                   || dt == Schema.Displaytype.Double;
        }
      
        boolean isDateOrTime(Schema.DisplayType dt) {
            return dt == Schema.Displaytype.Time
                   || dt == Schema.Displaytype.Date
                   || dt == Schema.Displaytype.Datetime;
        }
    }
  
    /**
        Context having all the information about the rollup to be done.
        Please note : This class encapsulates many rollup summary fields with different operations.
    */
    public class Context {
        // Master Sobject Type
        public Schema.Sobjecttype master;
        // Child/Details Sobject Type
        public Schema.Sobjecttype detail;
        // Lookup field on Child/Detail Sobject
        public Schema.Describefieldresult lookupField;
        // various fields to rollup on
        public List<RollupSummaryField> fieldsToRoll;

        // Where clause or filters to apply while aggregating detail records
        public String detailWhereClause;          

        public Context(Schema.Sobjecttype m, Schema.Sobjecttype d,
                           Schema.Describefieldresult lf) {
            this(m, d, lf, '');                           
        }
      
        public Context(Schema.Sobjecttype m, Schema.Sobjecttype d,
                           Schema.Describefieldresult lf, String detailWhereClause) {
            this.master = m;
            this.detail = d;
            this.lookupField = lf;
            this.detailWhereClause = detailWhereClause;
            this.fieldsToRoll = new List<RollupSummaryField>();
        }

        /**
            Adds new rollup summary fields to the context
        */
        public void add(RollupSummaryField fld) {
            this.fieldsToRoll.add(fld);
        }
    }  
}

All Answers

CheyneCheyne
To get it to calculate the rollups when the last record is deleted, try making the following additions to the Apex class (in bold):

Class:

/*
Copyright (c) 2012 tgerm.com
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

1. Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in the
   documentation and/or other materials provided with the distribution.
3. The name of the author may not be used to endorse or promote products
   derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
    LREngine("L"ookup "R"ollup Engine) : This class simplifies rolling up on the child records in lookup relationship.
*/
public class LREngine {

    /*
        Tempalte tokens
            0 : Fields to project
            1 : Object to query
            2 : Optional WHERE clause filter to add
            3 : Group By field name
    */
    static String SOQL_TEMPLATE = 'SELECT {0} FROM {1} {2} GROUP BY {3}';
    static String COUNT_SOQL_TEMPLATE = 'SELECT Id FROM {0} WHERE Id NOT IN (SELECT {1} FROM {2})';

    /**
        Key driver method that rolls up lookup fields based on the context. This is specially useful in Trigger context.

        @param ctx Context the complete context required to rollup
        @param detailRecordsFromTrigger child/detail records which are modified/created/deleted during the Trigger
        @returns Array of in memory master objects. These objects are not updated back to the database
                because we want client or calling code to have this freedom to do some post processing and update when required.
    */
    public static Sobject[] rollUp(Context ctx, Sobject[] detailRecordsFromTrigger) {

        // API name of the lookup field on detail sobject
        String lookUpFieldName = ctx.lookupField.getName();

        Set<Id> masterRecordIds = new Set<Id>();
        for (Sobject kid : detailRecordsFromTrigger) {
            masterRecordIds.add((Id)kid.get(lookUpFieldName));
        }
        return rollUp(ctx, masterRecordIds);
    }

    /**
        Key driver method that rolls up lookup fields based on the context. This is meant to be called from non trigger contexts like
        scheduled/batch apex, where we want to rollup on some master record ids.

        @param Context the complete context required to rollup
        @param masterIds Master record IDs whose child records should be rolled up.
        @returns Array of in memory master objects. These objects are not updated back to the database
                because we want client or calling code to have this freedom to do some post processing and update when required.
    */
    public static Sobject[] rollUp(Context ctx,  Set<Id> masterIds) {
        // K: Id of master record
        // V: Empty sobject with ID field, this will be used for updating the masters
        Map<Id, Sobject> masterRecordsMap = new Map<Id, Sobject>();
        for (Id mId : masterIds) {
            masterRecordsMap.put(mId, ctx.master.newSobject(mId));
        }

        // #0 token : SOQL projection
        String soqlProjection = ctx.lookupField.getName();
        // k: detail field name, v: master field name
        Map<String, String> detail2MasterFldMap = new Map<String, String>();
        // k: detail field name, v: detail field small alias
        Map<String, String> detailFld2AliasMap = new Map<String, String>();
        Integer aliasCounter = 1;
        for (RollupSummaryField rsf : ctx.fieldsToRoll) {
            /* Keeping alias short i.e. d1, d2...dn to allow more chars in SOQL to avoid SOQL char limit.
                It also helps in avoiding this exception when field names are more than 25 chars i.e.
                System.QueryException: alias is too long, maximum of 25 characters: Annualized_Recurring_Revenue__c
              
                A Sample query with new alias format will look like following
                SELECT AccountId, Avg(Amount) d1, Count(CloseDate) d2 FROM Opportunity  GROUP BY AccountId
            */
            String aliasName = 'd' + aliasCounter++;
            // create aggreate projection with alias for easy fetching via AggregateResult class
            // i.e. SUM(Amount) Amount
            soqlProjection += ', ' + rsf.operation + '(' + rsf.detail.getName() + ') ' + aliasName;
            detail2MasterFldMap.put(rsf.detail.getName(), rsf.master.getName());
            // store the alias for the detail field name, it will be used for loading up later
            detailFld2AliasMap.put(rsf.detail.getName(), aliasName);
        }

        // #1 token for SOQL_TEMPLATE
        String detailTblName = ctx.detail.getDescribe().getName();
      
        // #2 Where clause
        String whereClause = '';
        if (ctx.detailWhereClause != null && ctx.detailWhereClause.trim().length() > 0) {
            whereClause = 'WHERE ' + ctx.detailWhereClause ;
        }
      
        // #3 Group by field
        String grpByFld = ctx.lookupField.getName();
      
        String soql = String.format(SOQL_TEMPLATE, new String[]{soqlProjection, detailTblName, whereClause, grpByFld});
      
        // aggregated results
        List<AggregateResult> results = Database.query(soql);
      
        for (AggregateResult res : results){
            Id masterRecId = (Id)res.get(grpByFld);
            Sobject masterObj = masterRecordsMap.get(masterRecId);
            if (masterObj == null) {
                System.debug(Logginglevel.WARN, 'No master record found for ID :' + masterRecId);
                continue;
            }

            for (String detailFld : detail2MasterFldMap.keySet()) {
                // Load the alias name to fetch the value from the aggregated result
                String aliasName = detailFld2AliasMap.get(detailFld);
                Object aggregatedDetailVal = res.get(aliasName);
                masterObj.put(detail2MasterFldMap.get(detailFld), aggregatedDetailVal);
            }         
        }

        //since the aggregate query above won't return any results when there are zero child records,
        //we need to do another query to get these records
        String masterTblName = ctx.master.getDescribe().getName();
        soql = String.format(COUNT_SOQL_TEMPLATE, new String[]{masterTblName, grpByFld, detailTblName});
        List<SObject> zeroChildrenRes = Database.query(soql);
        for (SObject obj : zeroChildrenRes) {
            Id masterRecId = obj.Id;
            Sobject masterObj = masterRecordsMap.get(masterRecId);
            if (masterObj == null) {
                System.debug(Logginglevel.WARN, 'No master record found for ID :' + masterRecId);
                continue;
            }
     
            //Since none of these master records have any children, just null this rollup
            for (String detailFld : detail2MasterFldMap.keySet()) {
                masterObj.put(detail2MasterFldMap.get(detailFld), null);
            }
        }

      
        return masterRecordsMap.values(); 
    }




    /**
        Exception throwed if Rollup Summary field is in bad state
    */
    public class BadRollUpSummaryStateException extends Exception {}

   /**
       Which rollup operation you want to perform
    */
    public enum RollupOperation {
        Sum, Max, Min, Avg, Count
    }
  
    /**
        Represents a "Single" roll up field, it contains
        - Master field where the rolled up info will be saved
        - Detail field that will be rolled up via any operation i.e. sum, avg etc
        - Operation to perform i.e. sum, avg, count etc
          
    */
    public class RollupSummaryField {
        public Schema.Describefieldresult master;
        public Schema.Describefieldresult detail;
        public RollupOperation operation;
      
        // derived fields, kept like this to save script lines later, by saving the same
        // computations over and over again
        public boolean isMasterTypeNumber;
        public boolean isDetailTypeNumber;
        public boolean isMasterTypeDateOrTime;
        public boolean isDetailTypeDateOrTime;
      
        public RollupSummaryField(Schema.Describefieldresult m,
                                         Schema.Describefieldresult d, RollupOperation op) {
            this.master = m;
            this.detail = d;
            this.operation = op;
            // caching these derived attrbutes for once
            // as their is no view state involved here
            // and this caching will lead to saving in script lines later on
            this.isMasterTypeNumber = isNumber(master.getType());
            this.isDetailTypeNumber = isNumber(detail.getType());
            this.isMasterTypeDateOrTime = isDateOrTime(master.getType());
            this.isDetailTypeDateOrTime = isDateOrTime(detail.getType());
            // validate if field is good to work on later
            validate();
        } 
      
        void validate() {
            if (master == null || detail == null || operation == null)
                throw new BadRollUpSummaryStateException('All of Master/Detail Describefieldresult and RollupOperation info is mandantory');

            if ( (!isMasterTypeDateOrTime && !isMasterTypeNumber)
                  || (!isDetailTypeDateOrTime && !isDetailTypeNumber)) {
                    throw new BadRollUpSummaryStateException('Only Date/DateTime/Time/Numeric fields are allowed');
            }
          
            if (isMasterTypeDateOrTime && (RollupOperation.Sum == operation || RollupOperation.Avg == operation)) {
                throw new BadRollUpSummaryStateException('Sum/Avg doesnt looks like valid for dates ! Still want, then implement the IRollerCoaster yourself and change this class as required.');
            }
        }
      
        boolean isNumber (Schema.Displaytype dt) {
            return dt == Schema.Displaytype.Currency
                   || dt == Schema.Displaytype.Integer
                   || dt == Schema.Displaytype.Percent
                   || dt == Schema.Displaytype.Double;
        }
      
        boolean isDateOrTime(Schema.DisplayType dt) {
            return dt == Schema.Displaytype.Time
                   || dt == Schema.Displaytype.Date
                   || dt == Schema.Displaytype.Datetime;
        }
    }
  
    /**
        Context having all the information about the rollup to be done.
        Please note : This class encapsulates many rollup summary fields with different operations.
    */
    public class Context {
        // Master Sobject Type
        public Schema.Sobjecttype master;
        // Child/Details Sobject Type
        public Schema.Sobjecttype detail;
        // Lookup field on Child/Detail Sobject
        public Schema.Describefieldresult lookupField;
        // various fields to rollup on
        public List<RollupSummaryField> fieldsToRoll;

        // Where clause or filters to apply while aggregating detail records
        public String detailWhereClause;          

        public Context(Schema.Sobjecttype m, Schema.Sobjecttype d,
                           Schema.Describefieldresult lf) {
            this(m, d, lf, '');                           
        }
      
        public Context(Schema.Sobjecttype m, Schema.Sobjecttype d,
                           Schema.Describefieldresult lf, String detailWhereClause) {
            this.master = m;
            this.detail = d;
            this.lookupField = lf;
            this.detailWhereClause = detailWhereClause;
            this.fieldsToRoll = new List<RollupSummaryField>();
        }

        /**
            Adds new rollup summary fields to the context
        */
        public void add(RollupSummaryField fld) {
            this.fieldsToRoll.add(fld);
        }
    }  
}
This was selected as the best answer
CheyneCheyne
Try adding the filter into the Context constructor. Format it like it's the where clause of a SOQL query

LREngine.Context ctx = new LREngine.Context(Office_Commission__c.SobjectType, // parent object
                                            Office_Invoice__c.SobjectType,  // child object
                                            Schema.SObjectType.Office_Invoice__c.fields.Office_Commission__c,
                                           'Payment_Method__c = \'Commission Deduction\'');
Developer.mikie.Apex.StudentDeveloper.mikie.Apex.Student
Cheyne! You are a genius, thank you sooo much my friend. You have saved me a world of trouble!
ManzManz

Hello,
I am getting below Error when ever I am trying to use the below syntax
"LREngine.Context ctx = new LREngine.Context(Parent_API.SobjectType, // parent object
                                                                               Child_API.SobjectType,  // child object
                                                                               Schema.SObjectType.Child.fields.Relationship_Field_API // relationship field name
    );
"

Error: Compile Error: Invalid type: LREngine.Context at line 18 column 32

Can you please help me fix this?
I tried in many blogs but I didnt receive any reply from others.

Thanks,
MR