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
Qingsong LiQingsong Li 

Why 0% Prabability Opportunity Trigger been fired twice

I have a trigger on opportunity level, and then a trigger on opportunity line item level, the trigger is working well for all pipeline stage with non 0% prabability when the line items less than 100 because the DML update statement limited,
The trigger in opportunity level is:
Code:
trigger OpportuntiStageUpdate on Opportunity (after update) {
    //Determine the opportuntiy been updated
   ID[] sid=New ID[trigger.new.size()]; 
   for(Opportunity s : trigger.new){
         sid.add(s.id);
   }
   OpportunityAfterTrigger.OppsUpdate(sid);
}

 
the trigger in line items is:
 
Code:
trigger OpportuntiLineItemUpdate on OpportunityLineItem (before update, before insert) {
    // Determine the ooportuntiyLineItem been updated
    // Get the opportuntiy ID which will be updated
    Set<ID> pbeIds = new Set<ID>();
    Set<ID> prodIds = new Set<ID>();
    Set<String> currCode = new Set<String>();
    for (OpportunityLineItem oli1 : Trigger.new){
        pbeIds.add(oli1.OPPORTUNITYID);
        prodIds.add(oli1.pricebookentryid);
        currCode.add(oli1.CURRENCYISOCODE);
        }
    //Now map the closedate,probability,channels
        Map<Id, Opportunity> opps= new Map<Id, Opportunity>(
            [select CLOSEDATE,
                    PROBABILITY,
                    Channel__c,
                    Manual_or_Derived__c
                    from Opportunity
                    where id In : pbeIds]);
        Map<Id, PricebookEntry> entries = new Map<Id, PricebookEntry> (
            [select product2.Description
                    from pricebookentry
                    where id In : prodIds]);
        Map<String, FX__C> fxTable = new Map<String, FX__C> ();
        for (FX__C newFX :[select Name, Exchange_Rate__C from FX__C
                    where To_Currency__C In : currCode]){
                 fxTable .put(newFX.Name,newFX);
        }

    for (OpportunityLineItem oli : Trigger.new){
        //Pending for DRI Release 1.0
        //Set BP Number to Null if Non-Derived Opps
        if (Trigger.isInsert && opps.get(oli.OPPORTUNITYID).Manual_or_Derived__c=='M'){
           oli.BP_Number__C='';
        }
        if (oli.Applicaton__C > 100 || oli.Business__C>100){
            oli.Applicaton__C.addError('Application % and Business Number % must less than or equal to 100%');
        }
        //Update Quantity for Derived Opps
        If (opps.get(oli.OPPORTUNITYID).Manual_or_Derived__c=='D' &&
            oli.Vechile_Volume__C <>0 && oli.Applicaton__C<>Null && oli.Business__C <>Null &&
            oli.Number_Per_Application__C <>0){
            oli.Quantity=math.Round(oli.Vechile_Volume__C * oli.Applicaton__C  * oli.Business__C /10000*oli.Number_Per_Application__C) ;
        }
        

        //Below code should be disable when DRI release to production
        if (Trigger.isInsert){
            oli.BP_Number__C='';
        }

        //Update Description
        oli.Description = entries.get(oli.pricebookentryid).Product2.Description;
        if (oli.BOOK_EXPECTED_CLOSED_DATE__C == null) {
            oli.BOOK_EXPECTED_CLOSED_DATE__C = opps.get(oli.OPPORTUNITYID).CLOSEDATE;
        }
                 
        //If no probability, pull the probability from header level
        if (oli.PROBABILITY__C == null) {
            oli.PROBABILITY__C = opps.get(oli.OPPORTUNITYID).PROBABILITY;
        }
        //Set All Probability to 100% when marked to win
        Set<String> won = new Set<String>();
        won.add('Win');
        won.add('Win-Carryover');
        won.add('Win-Conquest');
        won.add('Win-Extended Bookings');
        if (won.contains(oli.STAGE__C)){
            oli.PROBABILITY__C=100;
            //Check Project OI for non penetration channel
            if (oli.Projected_OI__c == null) {
                if (opps.get(oli.OPPORTUNITYID).Channel__c <> 'EEA - Penetration') {
                    oli.Projected_OI__c.addError('Project OI is required when close a opportunity!');
                }
            }
        }
        //Set All Probability to 0% when marked to lost/canceled/no quote
        Set<String> lost= new Set<String>();
        lost.add('Lost');
        lost.add('Lost-Business');
        lost.add('Lost-Opportunity');
        lost.add('Canceled');
        lost.add('No Quote');
        if (lost.contains(oli.STAGE__C)){
            oli.PROBABILITY__C=0;
        } 

        //Lost opportunity sets
        Set<String> lose= new Set<String>();
        lose.add('Lost');
        lose.add('Lost-Business');
        lose.add('Lost-Opportunity');       
        if (lose.contains(oli.STAGE__C) && oli.Other_Competitor__c == null){
            oli.Other_Competitor__c.addError('Other competitor must be fulfilled for lost opportunity');
        }
        if (lose.contains(oli.STAGE__C) && oli.Winner__c == null){
            oli.Winner__c.addError('Winner must be fulfilled for lost opportunity');
        }

        //Win opportunity sets
        Set<String> win = new Set<String>();
        win.add('Win');
        win.add('Win-Carryover');
        win.add('Win-Conquest');
        win.add('Win-Extended Bookings');        
        if (win.contains(oli.STAGE__C) && oli.Other_Competitor__c == null){
            oli.Other_Competitor__c.addError('Other competitor must be fulfilled for won opportunity');
        }
        if (win.contains(oli.STAGE__C) && oli.Projected_OI__c == null){
            oli.Projected_OI__c.addError('Project OI % must be fulfilled for won opportunity');
        }

        //Non open stage sets
        Set<String> open = new Set<String>();
        open.add('Win');
        open.add('Win-Carryover');
        open.add('Win-Conquest');
        open.add('Win-Extended Bookings');
        open.add('Lost');
        open.add('Lost-Business');
        open.add('Lost-Opportunity');
        open.add('Canceled');
        open.add('No Quote'); 
        if (oli.Decision_Factors_1__c == null && open.contains(oli.STAGE__C)) {
            oli.Decision_Factors_1__c.addError('Close a opportunity must have Decision factor');
        }
        if (oli.Incumbent_Supplier__c == null && open.contains(oli.STAGE__C)) {
            oli.Incumbent_Supplier__c.addError('Close a opportunity must have Incumbent Supplier');
        }    
        oli.CLOSE_REASON_DESCRIPTION__C = 'trigger from items';
        //Update Exchange Rate base on the Calendar Year
        if (oli.CURRENCYISOCODE =='USD'){
            oli.Exchange_Rate__C = 1.0;
        } else {
            String fromcurrCode ='USD';
            String tocurrCode = oli.CURRENCYISOCODE;
            String calendarYear = '2017';
            If (oli.CALENDAR_YEAR__C.year()<=2017) {
                calendarYear = ''+(oli.CALENDAR_YEAR__C.year());
            }
            String fxName = fromcurrCode + '-' + tocurrCode + '-' + calendarYear;
            //FX__C[] fx = [Select Exchange_Rate__C FROM FX__C Where From_Currency__C=: fromcurrCode and To_Currency__C =: tocurrCode and Year__C =: calendarYear];
            //if (fx.size()>0) {
            //    oli.Exchange_Rate__C = 1 / fx[0].Exchange_Rate__C;
            //}
            if(fxTable.containsKey(fxName)){
                oli.Exchange_Rate__C = 1 / fxTable.get(fxName).Exchange_Rate__C;
            }
        }       
    }
}

 
and then apex class is:
Code:
public class OpportunityAfterTrigger{
    public static Boolean inLineItemTrigger;
    Static testMethod void sampleOppsTestMethod() { 
        Opportunity testOpps = [SELECT Id FROM Opportunity LIMIT 1];
        ID[] sid=New ID[]{testOpps.Id}; 
       //sid.add(testOpps.id[0]);
       OpportunityAfterTrigger.OppsUpdate(sid);
    } 

    //@future (callout = true)
    public static void OppsUpdate(ID[] sid){
    //Service__c sd = new Service__c();
 
     for(Opportunity nw : [Select Id,StageName,Probability,CloseDate,Additional_Comments__C,
                            INCUMBENT_SUPPLIER__C,OTHER_COMPETITOR__C,WINNER__C,DECISION_FACTORS_1__C,PROJECTED_OI__C
                            From Opportunity
                            Where Id in :sid]){ 
                             
       //Get Opportunity ID
        Set<ID> iOppsID = New Set<ID>();
        iOppsID.add(nw.Id);
        
        //Lost stage sets
        Set<String> lost= new Set<String>();
        lost.add('Lost');
        lost.add('Lost-Business');
        lost.add('Lost-Opportunity');
        lost.add('Canceled');
        lost.add('No Quote'); 
        
        //Win stage sets
        Set<String> won = new Set<String>();
        won.add('Win');
        won.add('Win-Carryover');
        won.add('Win-Conquest');
        won.add('Win-Extended Bookings');
        
        //Anticipated stage set
        Set<String> Anticipated= new Set<String>();
        Anticipated.add('Anticipated');
        
        //Open Line Items for update
        OpportunityLineItem[] items = [select STAGE__C,PROBABILITY__C,Id,
                        OpportunityID,PRICEBOOKENTRYID,NUMBER_PER_APPLICATION__C,
                        Applicaton__c,BUSINESS__C, INCUMBENT_SUPPLIER__C,
                        OTHER_COMPETITOR__C,WINNER__C,DECISION_FACTORS_1__C,PROJECTED_OI__C   
            FROM OpportunityLineItem where OpportunityId In : iOppsID];        

        for (OpportunityLineItem a: items){
            //Set all product line items to lost/canceled/no quote if header level marked to lost
            if (lost.contains(nw.StageName)){
                a.STAGE__C = nw.StageName;
                a.PROBABILITY__C = nw.Probability;
                a.CLOSE_REASON_DESCRIPTION__C  = nw.Additional_Comments__C;
                a.BOOK_EXPECTED_CLOSED_DATE__C = nw.CloseDate;                  
                } 
                //Set open product line itmes to win if header level marked to win
                else if (won.contains(nw.StageName)){
                        if (a.STAGE__C == 'Anticipated') {
                            a.STAGE__C = nw.StageName;
                            a.PROBABILITY__C = nw.Probability;
                            a.CLOSE_REASON_DESCRIPTION__C  = nw.Additional_Comments__C; 
                            a.BOOK_EXPECTED_CLOSED_DATE__C = nw.CloseDate;                          
                            } 
                        }
                        //Set open product line items
                        else if (a.STAGE__C == 'Anticipated') {  
                                a.PROBABILITY__C = nw.Probability;
                                a.BOOK_EXPECTED_CLOSED_DATE__C = nw.CloseDate;
                        }
            if (a.INCUMBENT_SUPPLIER__C == null && nw.INCUMBENT_SUPPLIER__C != null)
                {a.INCUMBENT_SUPPLIER__C=nw.INCUMBENT_SUPPLIER__C;}
            if (a.OTHER_COMPETITOR__C == null)
                {a.OTHER_COMPETITOR__C=nw.OTHER_COMPETITOR__C;}
            if (a.WINNER__C == null)
                {a.WINNER__C=nw.WINNER__C;}
            if (a.DECISION_FACTORS_1__C == null)
                {a.DECISION_FACTORS_1__C=nw.DECISION_FACTORS_1__C;}
            if (a.PROJECTED_OI__C == null)
                {a.PROJECTED_OI__C=nw.PROJECTED_OI__C;}          
            }
        try {
        update items;
        } 
        catch (System.StringException e){
            System.debug(e);
        }
       }
    }
}

 the problems is when I change the stage to lost or cancelled (the probability 0%), the trigger will be fired twice, below is the debug information:
Code:
*** Beginning OpportuntiBPFlagUpdate on Opportunity trigger event BeforeUpdate for 006S0000002FF3p

20081103142806.509:Trigger.OpportuntiBPFlagUpdate: line 6, column 5: SelectLoop:LIST:SOBJECT:Opportunity

Cumulative resource usage:

Resource usage for namespace: (default)
Number of SOQL queries: 0 out of 20
Number of query rows: 0 out of 1000
Number of SOSL queries: 0 out of 0
Number of DML statements: 0 out of 20
Number of DML rows: 0 out of 100
Number of script statements: 46 out of 10200
Maximum heap size: 0 out of 100000
Number of callouts: 0 out of 10
Number of Email Invocations: 0 out of 10
Number of fields describes: 0 out of 10
Number of record type describes: 0 out of 10
Number of child relationships describes: 0 out of 10
Number of picklist describes: 0 out of 10
Number of future calls: 0 out of 10
Number of find similar calls: 0 out of 0
Number of System.runAs() invocations: 0 out of 0

Total email recipients queued to be sent : 0

*** Ending OpportuntiBPFlagUpdate on Opportunity trigger event BeforeUpdate for 006S0000002FF3p

*** Beginning OpportuntiStageUpdate on Opportunity trigger event AfterUpdate for 006S0000002FF3p

.........................
No profiling information for SOSL operations.

1 most expensive DML operations:
Class.OpportunityAfterTrigger.OppsUpdate: line 83, column 9: Update: LIST:SOBJECT:OpportunityLineItem: executed 2 times in 449 ms

1 most expensive method invocations:
Class.OpportunityAfterTrigger: line 11, column 24: public static void OppsUpdate(LIST:Id): executed 2 times in 525 ms


 
so this problem is caused the lost/cancelled opportunity can not have line items more than 50, my question is there any reason to cause the trigger been fire twice just for lost/canclled oppotunity? how to skip duplicated fire trigger, any suggestion on the code improvement? Thanks a lot