[SalesForce] Updating parent record based on child record

I have a trigger on release(Child of Opportunity) record. Logic for trigger is in handler class. I am updating checkbox to true on Opportunity(Parent) whenever release record edited or inserted. if any record inserted/updated on release object has Release__c.Test__c value start with other than ‘HIGH%’ then checkbox needs to set to False, checkbox will be set to true only when all related record has Test__c = ‘HIGH%’ else set to false. I created below logic but not working as expected. I do not want to use another For loop for child is it possible in single loop, Below is my code for trigger.

trigger updateCheckbocOpp on Release__c (after insert, after update)
{
    List<Opportunity> lstOpptoUpdate = new List<Opportunity>();
    for(Release__c relRec:trigger.new)
    {
        system.debug('relRec.Test__c -'+ relRec.Test__c);
        system.debug('relRec.Opportunity__c -'+ relRec.Opportunity__c);
        if(!relRec.Test__c.startsWith('HIGH%'))
        {
            lstOpptoUpdate.add(new Opportunity(Id = relRec.Opportunity__c,
                                               Set_Rel__c=false));
            system.debug('Setting to False');

        }
    }
    system.debug('Testing - trigger'+lstOpptoUpdate);
    update lstOpptoUpdate;

}

Updated code: Suggested by Carlos –

trigger updateCheckbocOpp1 on Release__c (after insert, after update){ 
    Map<ID, Opportunity > OppForUpdate = new Map<ID, Opportunity>(); 
    List<Id> listIds = new List<Id>();

    for (Release__c relRec: trigger.new) 
    {
        listIds.add(relRec.Opportunity__c);
        system.debug('Arvind>>'+ listIds);
    }
    OppForUpdate = new Map<Id, Opportunity>([SELECT id, Set_Rel__c 
                                           FROM Opportunity 
                                           WHERE ID IN :listIds]);
    system.debug(OppForUpdate.size());

    for (Release__c rel: trigger.new)
    {

        Opportunity myParentOpp = OppForUpdate.get(rel.Opportunity__c);
        system.debug('myParentOpp>>'+myParentOpp);

        if(rel.Test__c.startsWith('HIGH%'))    
        {
            myParentOpp.Set_Rel__c = true;
        }
        else
        {
            myParentOpp.Set_Rel__c = false;
        }
    }   update OppForUpdate.values();
}

Best Answer

I haven't tested, but you can try something similar to this:

trigger updateCheckbocOpp on Release__c (after insert, after update) 
{ 
    Map<ID, Opportunity> parentOpps = new Map<ID, Opportunity>(); 
    List<Id> listIds = new List<Id>();

    for (Release__c relRec: trigger.new) 
    {
    listIds.add(relRec.OpportunityId);
    }

    parentOpps = new Map<Id, Opportunity>([SELECT id, Set_Rel__c 
                                            FROM Opportunity 
                                             WHERE ID IN :listIds]);

    for (Release__c r: Trigger:new){
    Opportunity myParentOpp = parentOpps.get(r.Opportunity);

    if(r.Test__c.startsWith('HIGH%'))
    {
     myParentOpp.Set_Rel__c = true;
    }
    else{
     myParentOpp.Set_Rel__c = false;
    }
 }

  update parentOpps.values();
}

Normally you shouldn't use the trigger as the place where you add your apex logic /code. As a good practice, you should think about using a trigger handler. There are many options there like Kevin's O'Hara trigger framework. Just google Kevin's O'Hara trigger handler for more information. Using trigger handlers is the way to go.

TEST----------------------------- Tested on my developer console using a different logic and it works. Added a checkbox field on the Parent Object(Opportunity) called PrimaryContact__c. So if an OpportunityContactRole is primary this checkbox should be set to true. Is a similar approach.

  List<OpportunityContactRole > likeTrigger = [Select Id, IsPrimary, 
  OpportunityId FROM OpportunityContactRole ]; 
  Map<ID, Opportunity > parentOpps = new Map<ID, Opportunity>(); 
  List<Id> listIds = new List<Id>();

   for (OpportunityContactRole relRec: likeTrigger) 
   {
      listIds.add(relRec.OpportunityId);
   }

   parentOpps = new Map<Id, Opportunity>([SELECT id, PrimaryContact__c 
          FROM Opportunity  
             WHERE ID IN :listIds]);

    system.debug(parentOpps.size());

   for (OpportunityContactRole r: likeTrigger){
   Opportunity myParentOpp = parentOpps.get(r.OpportunityId);

   if(r.IsPrimary ==true)
   {
    myParentOpp.PrimaryContact__c = true;
   }
   else{
    myParentOpp.PrimaryContact__c = false;
   }
 }

   update parentOpps.values();

You can run this code in your developer console after adding the PrimaryContact__c checkbox to your opportunity object. This is just an idea on how you can get your code working.

****** UPDATE *******

I think that if what you want is to loop through your Release__c records looking up to Opportunities as the parent in that relationship. And after that set a value on the parent if all related records have the same value on a field, you shouldn't think about a trigger. You can do this with Process Builder and Flows. Check this site to give you an idea on how to do this.

The other option is to use something like Rollup Summary support to count those child records and use formulas or even a trigger to get the rest of your update (to the Opportunity) done.

This is a way to create custom summary fields with Process Builder and Flows.

You can use a roll up an app like this one If you want to start looking on how to build you rollup summary classes you can check this out

The point is that you need to count somehow all the child records for the opportunity object. For this, you can use some of the approaches I mentioned here. I assume that the relation between Opportunities and Release__c objects are a lookup and not a master relationship. If you have a master relationship between the Opportunity standard object and the custom Release__c object you could easily solve this by using stander roll-up summary fields.... but I assume that you don't have it, right?

****** UPDATE *******

I was thinking that you could do something like this:

Create 2 fields in you Opportunity object: Total_Tests__c and Total_Releases__c. They both need to be of data type Number. We are going to use these two fields to count releases with LIKE 'HIGH%' and a total number of releases related to an Opportunity.

After that, you need to use a utility class created by Anthony Victorio that will take care of the counting operations. Create one class and copy paste the code below:

public class RollUpSummaryUtility {

    //the following class will be used to house the field names
    //and desired operations
    public class fieldDefinition {
        public String operation {get;set;}
        public String childField {get;set;}
        public String parentField {get;set;}

        public fieldDefinition (String o, String c, String p) {
            operation = o;
            childField = c;
            parentField = p;
        }
    }

    public static void rollUpTrigger(list<fieldDefinition> fieldDefinitions,
    list<sObject> records, String childObject, String childParentLookupField, 
    String parentObject, String queryFilter) {

        //Limit the size of list by using Sets which do not contain duplicate
        //elements prevents hitting governor limits
        set<Id> parentIds = new set<Id>();

        for(sObject s : records) {
            parentIds.add((Id)s.get(childParentLookupField));
        }

        //populate query text strings to be used in child aggregrator and 
        //parent value assignment
        String fieldsToAggregate = '';
        String parentFields = '';

        for(fieldDefinition d : fieldDefinitions) {
            fieldsToAggregate += d.operation + '(' + d.childField + ') ' + 
            ', ';
            parentFields += d.parentField + ', ';
        }

        //Using dynamic SOQL with aggergate results to populate parentValueMap
        String aggregateQuery = 'Select ' + fieldsToAggregate + 
        childParentLookupField + ' from ' + childObject + ' where  ' + 
        childParentLookupField + ' IN :parentIds ' + queryFilter + ' ' +
        ' group by ' + childParentLookupField;

        //Map will contain one parent record Id per one aggregate object
        map<Id, AggregateResult> parentValueMap = 
        new map <Id, AggregateResult>();

        for(AggregateResult q : Database.query(aggregateQuery)){
            parentValueMap.put((Id)q.get(childParentLookupField), q);
        }

        //list of parent object records to update
        list<sObject> parentsToUpdate = new list<sObject>();

        String parentQuery = 'select ' + parentFields + ' Id ' +
         ' from ' + parentObject + ' where Id IN :parentIds';

        //for each affected parent object, retrieve aggregate results and 
        //for each field definition add aggregate value to parent field
        for(sObject s : Database.query(parentQuery)) {

            Integer row = 0; //row counter reset for every parent record
            for(fieldDefinition d : fieldDefinitions) {
                String field = 'expr' + row.format();
                AggregateResult r = parentValueMap.get(s.Id);
                //r will be null if no records exist 
                //(e.g. last record deleted)
                if(r != null) { 
                    Decimal value = ((Decimal)r.get(field) == null ) ? 0 : 
                        (Decimal)r.get(field);
                    s.put(d.parentField, value);
                } else {
                    s.put(d.parentField, 0);
                }
                row += 1; //plus 1 for every field definition after first
            }
            parentsToUpdate.add(s);
        }

        //if parent records exist, perform update of all parent records 
        //with a single DML statement
        if(parentsToUpdate.Size() > 0) {
            try{
                update parentsToUpdate;
            }catch(Exception e){
                ApexPages.addmessage(new ApexPages.message(ApexPages.severity.ERROR,e.getDMLMessage(0)));
                System.debug('Exception'+e.getDMLMessage(0));
            }
        }

    }

}

When you are done with these two phases we need to create the trigger that will fire our RollUpSummaryUtility class and insert the correct values in our two custom fields, Total_Tests__c and Total_Releases__c.

Based on your data model I created this trigger, just copy and paste it into your trigger body:

 trigger OppCheckBoxUpdate on Release__c(after delete, after insert, after update, after undelete){

  if(trigger.isInsert || trigger.isUpdate || trigger.isUnDelete){


    // This will count Release__c records with Test__c LIKE 'HIGH%'
    list<RollUpSummaryUtility.fieldDefinition> fieldDefinitions = 
    new list<RollUpSummaryUtility.fieldDefinition> {
        new RollUpSummaryUtility.fieldDefinition('COUNT', 'Test__c', 
        'Total_Tests__c')
    };

    RollUpSummaryUtility.rollUpTrigger(fieldDefinitions, trigger.new, 
    'Release__c', 'Opportunity__c', 'Opportunity', 'And Test__c Like \'14%\'');


    // This will count all Release__c records related to their Opportunity      
   list<RollUpSummaryUtility.fieldDefinition> fieldDefinitions2 = 
    new list<RollUpSummaryUtility.fieldDefinition> {
        new RollUpSummaryUtility.fieldDefinition('COUNT', 'Release__c.Id', 
        'Total_Releases__c')
    };

    RollUpSummaryUtility.rollUpTrigger(fieldDefinitions2, trigger.new, 
    'Release__c', 'Opportunity__c', 'Opportunity', '');

}   

if(trigger.isDelete){

    list<RollUpSummaryUtility.fieldDefinition> fieldDefinitions = 
    new list<RollUpSummaryUtility.fieldDefinition> {
        new RollUpSummaryUtility.fieldDefinition('COUNT', 'Test__c', 
        'Total_Tests__c')
    };

    RollUpSummaryUtility.rollUpTrigger(fieldDefinitions, trigger.old, 
    'Release__c', 'Opportunity__c', 'Opportunity', 'And Test__c Like \'14%\'');


    list<RollUpSummaryUtility.fieldDefinition> fieldDefinitions2 = 
    new list<RollUpSummaryUtility.fieldDefinition> {
        new RollUpSummaryUtility.fieldDefinition('COUNT', 'Release__c.Id', 
        'Total_Releases__c')
    };

    RollUpSummaryUtility.rollUpTrigger(fieldDefinitions2, trigger.new, 
    'Release__c', 'Opportunity__c', 'Opportunity', '');
  } 
}

Now we need to build a couple of workflows that are going to set or remove the value on the opportunity checkbox based on the following.

IF((Total_Tests__c== Total_Releases__c),true, false) --> For the True value. This means that if the number of releases with value Test__c.startsWith('HIGH%') is equal to the number of Release__c records related to that Opportunity, the tick box on the Parent Opportunity should be set to True.

We need to do the same but on the other way around to set that tick box back to False in or logic changes. IF((Total_Tests__c != Total_Releases__c),true, false). Make sure that you set the correct value for the tick box in each workflow rule.

Go to Setup and Search for "Workflow Rules"--> New Workflow Rule --> Select Opportunity as the object, click Next.

Give a name to the workflow rule. Tick on "created, and every time it's edited" Run this rule if the "formula evaluates to true" In the formula field insert this: IF((Total_Tests__c== Total_Releases__c),true, false)

Then save it and you will have a workflow. You should be back to the All Workflow Rules page. Click on the new workflow we created and click on the Edit button under the "Workflow Actions". You should be on a new page where you can see a button "Add Workflow Actions". Click on it and select "New Field Update". Give it a name and select the checkbox from the Field to Update options. After you selected the right field you can set the value if the workflow rules is met. In this case because we are using the IF((Total_Tests__c== Total_Releases__c),true, false) we need to set that value under "Specify New Field Value" to True.

Save it all and when back to the main "All Workflow Rules" page, make sure that you click on "Active" on the Action column for our new rule.

Do the same for our:

IF((Total_Tests__c != Total_Releases__c),true, false)

but this time set to False the value in "Specify New Field Value".

This is all.

There are things to consider like making your trigger more robust by adding a trigger handler but this should help you to set this logic in your org.

When testing, make sure you refresh the Opportunity page if you don't see the changes you are expecting. Let me know if all works for you, it is quite a bit, but is fun if you get what you want after all the work :) ...