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
Alex MerwinAlex Merwin 

APEX Trigger Advice: Cross Populate Fields between Opportunity & OpportunityLineItem standard objects

Hello - 

I'm new to APEX and struggling with what should be a pretty easy ask I think. 

When a user updates the StageName field on the Opportunity standard object to 'Notify Demand', I need all OpportunityLineItems (standard object) associated with this Opportunity to have a picklist custom field named 'Campaign Status' updated to 'Buyer Approval Needed'. 

It is my intent to schedule a workflow rule to trigger based off the update of the OpportunityLineItem object, with a lot of merged fields from that object. So, you update the StageName at the Opportunity level, and a series of customized emails deploys, one per OpportunityLineItem. 

Here's what I have so far for the Trigger: 
 
trigger update on Opportunity (before insert, before update){

  List<ID> OppIds = New List<ID>();

  for(Opportunity o : Trigger.new){
    if(o.StageName == 'Notify Demand' && o.OpportunityLineItem__c != null){
      OppIds.add(o.OpportunityLineItem__c);
    }
  }

  List<OpportunityLineItem__c> oppList = [SELECT id, Campaign_Status__c FROM OpportunityLineItem WHERE id in :OppIds];
  for(integer i = 0 ; i < oppList.size(); i++){
    oppList[i].Campaign_Status__c = ‘Buyer Approval Needed’;
  }

  update oppList;
}

 
Best Answer chosen by Alex Merwin
pconpcon
Sorry, I had a typo.  The code below should fix that issue.
 
trigger OLIUpdate on Opportunity (after update) {
    Set<Id> oppIds = new Set<Id>();

    for (Opportunity opp : Trigger.new) {
        Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
        if (
            opp.StageName == 'Notify Demand' &&
            oldOpp.StageName != opp.StageName
        ) {
            oppIds.add(opp.Id);
        }
    }

    if (!oppIds.isEmpty()) {
        List<OpportunityLineItem> oliList = [
            select Campaign_Status__c
            from OpportunityLineItem
            where OpportunityId in :oppIds
        ];

        for (OpportunityLineItem oli : oliList) {
            oli.Campaign_Status__c = 'Buyer Approval Needed';
        }

        update oliList;
    }
}

 

All Answers

pconpcon
You've got the logic kinda right but not 100%. Below is the updated trigger that will do what you want
 
trigger OLIUpdate on Opportunity (after update) {
    Set<Id> oppIds = new Set<Id>();

    for (Opportunity opp : Trigger.new) {
        Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
        if (
            opp.StageName == 'Notify Demand' &&
            oldOpp.StageName != opp.StageName
        ) {
            oppIds.add(opp.Id);
        }
    }

    if (!oppIds.isEmpty()) {
        List<OpportunityLineItem> oliList = [
            select Campaign_Status__c
            from OpportunityLineItem
            where OpportunityId in :oppIds
        ];

        for (OpportunityLineItem oli : oliList) {
            oli.Campaign_Status__c = 'Buyer Approval Needed'
        }

        update olList;
    }
}
NOTE: This code has not been tested and may contain typographical or logical errors

I changed this to be on update only (since you cannot have an Opportunity with Line Items on create.  Then we get all of the Opportunities that the StageName changed to Notify Demand.  The way you had it before it would fire everytime any Opportunity was update with that and reset the status of all your line items.  Then we query all of your line items and then update their status.
Alex MerwinAlex Merwin
@Pcon - thanks for your help! I'm getting a compile error: 

Error: Compile Error: expecting a semi-colon, found '}' at line 23 column 8

I then added a semi-colon:
trigger OLIUpdate on Opportunity (after update) {
    Set<Id> oppIds = new Set<Id>();

    for (Opportunity opp : Trigger.new) {
        Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
        if (
            opp.StageName == 'Notify Demand' &&
            oldOpp.StageName != opp.StageName
        ) {
            oppIds.add(opp.Id);
        }
    }

    if (!oppIds.isEmpty()) {
        List<OpportunityLineItem> oliList = [
            select Campaign_Status__c
            from OpportunityLineItem
            where OpportunityId in :oppIds
        ];

        for (OpportunityLineItem oli : oliList) {
            oli.Campaign_Status__c = 'Buyer Approval Needed'
        ;}

        update olList;
    }
}

And got the following additional compile error: 

Error: Compile Error: Variable does not exist: olList at line 25 column 16    

Got any advice on how I can remedy? 

This is a huge help! I have a few other use cases I think we'll use this snippet for as well once we get it working. 

Thanks a lot. 
pconpcon
Sorry, I had a typo.  The code below should fix that issue.
 
trigger OLIUpdate on Opportunity (after update) {
    Set<Id> oppIds = new Set<Id>();

    for (Opportunity opp : Trigger.new) {
        Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
        if (
            opp.StageName == 'Notify Demand' &&
            oldOpp.StageName != opp.StageName
        ) {
            oppIds.add(opp.Id);
        }
    }

    if (!oppIds.isEmpty()) {
        List<OpportunityLineItem> oliList = [
            select Campaign_Status__c
            from OpportunityLineItem
            where OpportunityId in :oppIds
        ];

        for (OpportunityLineItem oli : oliList) {
            oli.Campaign_Status__c = 'Buyer Approval Needed';
        }

        update oliList;
    }
}

 
This was selected as the best answer
Alex MerwinAlex Merwin
That worked! Thanks!
Alex MerwinAlex Merwin
@Pcon - I hit a snag with some logic I didn't consider in the original ask. 
  1. Is there a way to restrict the Trigger to only fire when Opportunity.RecordTypeID has a certain value? 
  2. Is there a way to populate the Campaign_Status__c field with 2 potential values, determined by a checkbox field on the OpportunityLineItem object called INTL_Demand_Buyer_Approved__c ? 
So, something like: 
 
trigger OLIUpdate_SAP_SendLaunchTickets on Opportunity (after update) {
    Set<Id> oppIds = new Set<Id>();

    for (Opportunity opp : Trigger.new) {
        Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
        if (
            opp.StageName == 'Deploy Launch Tickets' &&
            oldOpp.StageName != opp.StageName &&
            opp.RECORDTYPEID == '012500000001tbkAAA'
        ) {
            oppIds.add(opp.Id);
        }
    }

    if (!oppIds.isEmpty()) {
        List<OpportunityLineItem> oliList1 = [
            select Campaign_Status__c
            from OpportunityLineItem
            where OpportunityId in :oppIds &&
            opp.INTL_Demand_Approval__c != 'Local Demand must approve buyer before Launch Ticket can be deployed'
        ];
        
    if (!oppIds.isEmpty()) {
        List<OpportunityLineItem> oliList2 = [
            select Campaign_Status__c
            from OpportunityLineItem
            where OpportunityId in :oppIds &&
            opp.INTL_Demand_Approval__c == 'Local Demand must approve buyer before Launch Ticket can be deployed' &&
            opp.INTL_Demand_Buyer_Approved__c == 'FALSE'
        ];

        for (OpportunityLineItem oli : oliList1) {
            oli.Campaign_Status__c = 'Buyer Approved, Ready to Launch';
        }
        
        for (OpportunityLineItem oli : oliList2) {
            oli.Campaign_Status__c = 'Buyer Approved, Pending Local Demand Approval';
        }

        update oliList;
    }
}

 
pconpcon
Sure, you just need to query the RecordType (don't use the Id directly because so that it works better in Sandboxes).  Then compare it.  Then we add the field to the OpportunityLineItem query and check it in your loop.
 
trigger OLIUpdate on Opportunity (after update) {
    Set<Id> oppIds = new Set<Id>();

    RecordType rt = [
        select Id
        from RecordType
        where Name = 'Record Type Name'
    ];

    for (Opportunity opp : Trigger.new) {
        Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
        if (
            opp.StageName == 'Notify Demand' &&
            oldOpp.StageName != opp.StageName &&
            opp.RecordTypeId = rt.Id
        ) {
            oppIds.add(opp.Id);
        }
    }

    if (!oppIds.isEmpty()) {
        List<OpportunityLineItem> oliList = [
            select Campaign_Status__c,
                INTL_Demand_Buyer_Approved__c
            from OpportunityLineItem
            where OpportunityId in :oppIds
        ];

        for (OpportunityLineItem oli : oliList) {
            if (oli.INTL_Demand_Buyer_Approved__c) {
                oli.Campaign_Status__c = 'Buyer Approved, Ready to Launch';
            } else {
                oli.Campaign_Status__c = 'Buyer Approved, Pending Local Demand Approval';
            }
        }

        update oliList;
    }
}
Alex MerwinAlex Merwin
Thanks! Can you let me know what I should tweak to get rid of this error? 

Error: Compile Error: AND operator can only be applied to Boolean expressions at line 13 column 13

Also, the logic for whether to go to 'Buyer Approved, Ready to Launch' or 'Buyer Approved, Pending Local Demand Approval' is based on a text field on the Opportunity Product object called INTL_Demand_Approval__c. My bad for missing that on my original inquiry! I tweaked the code as follows, but still getting the above compile error. 
 
trigger OLIUpdate on Opportunity (after update) {
    Set<Id> oppIds = new Set<Id>();

    RecordType rt = [
        select Id
        from RecordType
        where Name = 'Strategic Audience Plan'
    ];

    for (Opportunity opp : Trigger.new) {
        Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
        if (
            opp.StageName == 'Notify Demand' &&
            oldOpp.StageName != opp.StageName &&
            opp.RecordTypeId = rt.Id
        ) {
            oppIds.add(opp.Id);
        }
    }

    if (!oppIds.isEmpty()) {
        List<OpportunityLineItem> oliList = [
            select Campaign_Status__c,
                INTL_Demand_Approval__c
            from OpportunityLineItem
            where OpportunityId in :oppIds
        ];

        for (OpportunityLineItem oli : oliList) {
            if (oli.INTL_Demand_Approval__c != 'Local Demand must approve buyer before Launch Ticket can be deployed') {
                oli.Campaign_Status__c = 'Buyer Approved, Ready to Launch';
            } else {
                oli.Campaign_Status__c = 'Buyer Approved, Pending Local Demand Approval';
            }
        }

        update oliList;
    }
}

 
pconpcon
On line 15 that should read == instead of just =.  Since OpportunityProduct is a many to one relationship with Opportunity, do all OpportunityProducts have to have the INTL_Demand_Approval__c field set?  What is iit's value for Ready to launch vs the other status?

 
Alex MerwinAlex Merwin
Thanks for this. Unfortunately I'm getting the following error when promoting the trigger to Production: 
The following triggers have 0% code coverage. Each trigger must have at least 1% code coverage.OLIUpdate_SAP_BuyerApproval
OLIUpdate_SAP_SendLaunchTickets
OLIUpdate


It's strange because I'm only including the OLIUpdate trigger in the Outbound Change set, yet the error references the other two triggers associated with the Opportunity object. I tried a second change set including all three triggers from Sandbox but it's throwing the same error. 

Here's the code for the three triggers: 

OLIUpdate
trigger OLIUpdate on Opportunity (after update) {
    Set<Id> oppIds = new Set<Id>();

    for (Opportunity opp : Trigger.new) {
        Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
        if (
            opp.StageName == 'Notify Demand Teams' &&
            oldOpp.StageName != opp.StageName
        ) {
            oppIds.add(opp.Id);
        }
    }

    if (!oppIds.isEmpty()) {
        List<OpportunityLineItem> oliList = [
            select Campaign_Status__c
            from OpportunityLineItem
            where OpportunityId in :oppIds
        ];

        for (OpportunityLineItem oli : oliList) {
            oli.Campaign_Status__c = 'Buyer Approval Needed';
        }

        update oliList;
    }
}



OLIUpdate_SAP_SendLaunchTickets
trigger OLIUpdate_SAP_SendLaunchTickets on Opportunity (after update) {
    Set<Id> oppIds = new Set<Id>();

    for (Opportunity opp : Trigger.new) {
        Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
        if (
            opp.StageName == 'Deploy Launch Tickets' &&
            oldOpp.StageName != opp.StageName 
        ) {
            oppIds.add(opp.Id);
        }
    }

    if (!oppIds.isEmpty()) {
        List<OpportunityLineItem> oliList = [
            select Campaign_Status__c
            from OpportunityLineItem
            where OpportunityId in :oppIds
        ];

        for (OpportunityLineItem oli : oliList) {
            oli.Campaign_Status__c = 'Buyer Approved, Ready to Launch';
        }

        update oliList;
    }
}



OLIUpdate_SAP_BuyerApproval
 
trigger OLIUpdate_SAP_BuyerApproval on Opportunity (after update) {
    Set<Id> oppIds = new Set<Id>();

    for (Opportunity opp : Trigger.new) {
        Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
        if (
            opp.StageName == 'Responded to Buyer' &&
            oldOpp.StageName != opp.StageName
        ) {
            oppIds.add(opp.Id);
        }
    }

    if (!oppIds.isEmpty()) {
        List<OpportunityLineItem> oliList = [
            select Campaign_Status__c
            from OpportunityLineItem
            where OpportunityId in :oppIds
        ];

        for (OpportunityLineItem oli : oliList) {
            oli.Campaign_Status__c = 'Buyer Approval Needed';
        }

        update oliList;
    }
}


 
pconpcon
Did you write any test code to cover these triggers?
Alex MerwinAlex Merwin
Unfortunately this is the first time I've worked with APEX and I don't know! I promoted the 1st round of triggers without any additional testing code written and it worked fine. Only having issues now with updating the triggers that are already out there. Is there an article or something I can consult to learn how to get these updated triggers promoted to our production org? Thanks again for all the help!
 
pconpcon
I would recommend that you do some reading on testing [1] [2] [3] [4] to get a better understanding.  Each of your individual tests should only tests one specific portion of you class (ie a constructor test, sendEmail test, contactSelected test, etc).  You should also have both a postitive (everything works perfectly) and a negative (things are not right) test.

Each test should follow the following structure:
  • Setup of test data. This includes creation of any data needed by your class.  Account, Contacts etc
  • Starting the test. This is calling Test.startTest() to reset the governor limits
  • Calling your class / method
  • Stopping the test.This is calling Test.stopTest() to reset the governor limits and allow for any async jobs to finish
  • Asserting that your changes have worked
    • If you have inserted/updated/deleted data, you need to query for the updates
    • Run System.assert, System.assertEquals, System.assertNotEquals to verify that you got the correct data back
If you have any specific problems with your tests, feel free to create a new post with the part of the class you are trying to test and your current test method, and you will more likely get a better response then asking for someone to essentially write an entire test class for you.

[1] http://www.sfdc99.com/2013/05/14/how-to-write-a-test-class/
[2] http://pcon.github.io/presentations/testing/
[3] http://blog.deadlypenguin.com/blog/2014/07/23/intro-to-apex-auto-converting-leads-in-a-trigger/
[4] http://blog.deadlypenguin.com/blog/testing/strategies/
Alex MerwinAlex Merwin
Thanks again for your help. I've given it a go and opened a new thread here. Appreciate it!

https://developer.salesforce.com/forums?state=id#!/feedtype=SINGLE_QUESTION_DETAIL&dc=Apex_Code_Development&criteria=OPENQUESTIONS&id=906F0000000MHLdIAO