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
sfdclearnsfdclearn 

Trigger to copy opportunity product name to account custom field

trigger productname on Account(after insert,after update)
{
   
    
        List<Account> lstAccounts = [Select ProductNameCopy__c from Account ];
        List<Opportunity> lstopptys = [SELECT Id, Name FROM Opportunity];
        
        List<Id> oppIds = new List<Id>();
        List<OpportunityLineItem> lstopptylineitem  = [SELECT Id, PricebookEntry.Product2.Name FROM OpportunityLineItem WHERE OpportunityId IN :oppIds];
        for(Opportunity o : lstopptys)
        {
           oppIds.add(o.Id);
         }
         
        
            for(Account objAccount : lstAccounts)
            {
               for(Opportunity objoppty : lstopptys)
               {
                for(opportunitylineitem objopplineitem : lstopptylineitem)
                {
                objAccount.ProductNameCopy__c=objopplineitem.PricebookEntry.Product2.Name;
                update objAccount;
                
                }
                
         }
         }
         }
I am using the above trigger to copy the opportunity product names in a custom field in the account object.

This trigger is saved without error. 

But doesn't copy the product name in the Account's custom field(ProductNameCopy).

 

Please advise!!!

Best Answer chosen by Admin (Salesforce Developers) 
crop1645crop1645

trigger MyAccountTrigger on Account(before update, before insert) {

Map<ID,Set<String>> aIdToUniqueProductSetMap = new Map<ID,Set<String>> ();

// Go through each Oppo and collect a unique set of Product names from all OLI
for (Opportunity o : [select id, accountId,
                        (select id, PricebookEntry.Product2.Name from OpportunityLineItems)
                      from Opportunity where accountId IN :Trigger.new] ) {
  Set<String> productsInOppoSet = new Set<String> (); // reset our set for this Oppo
  for (OpportunityLineItem oli : o.opportunityLineItems)
     productsInOppoSet.add(oli.PricebookEntry.Product2.Name);
  // With the unique set, add it into our Map associating each account to its unique product set
  if (aIdToUniqueProductSetMap.containsKey(o.accountId)) {
      Set<String> uniqueProdSet = aIdToUniqueProductSetMap.get(o.accountId); // add this Oppo's unique set to whatever we have so far
      uniqueProdSet.addAll(productsInOppoSet);
      aIdToUniqueProductSetMap.put(o.accountId,uniqueProdSet); // put it back in the Map
  }    
  else
      aIdToUniqueProductSetMap.put(o.accountId,productsInOppoSet); // first Oppo for this Account; simply do a put

}

// All done with Oppos, now put back into each triggered Account
for (Account a: Trigger.new) {
  if (aIdToUniqueProductSetMap.containsKey(a.id)) {  // only for those Accounts that had Oppos with products
     String concatenatedProducts = '';
     for (String p : aIdToUniqueProductSetMap.get(a.id))   
         concatenatedProducts = concatenatedProducts + (concatenatedProducts.length() == 0 ? '' : ' ') + p; // separate with spaces; other delims possible
     a.description = concatenatedProducts ; // when the trigger completes, the triggered Accounts get updated
  }


}
}

All Answers

crop1645crop1645

sfdclearn:

 

First of all, use your old friend System.debug(..) to understand what is going on in your code.

 

This trigger has a few conceptual issues

 

1. lstAccounts is retrieving all Accounts in the database; you need to use Trigger.new to get the list of Accounts in the trigger list

2. lstOpptys should be a list of only the Opportunities that are children of the triggered Accounts

3 - There can be many Opportunities for a given Account and they cna have many products. How can you only place one product on the Account?

4 - The trigger will only fire if the Account is updated, it will not fire when you add an Opportunity product or modify an Opportunity

 

Check out the APEX documentation on Triggers, especially the examples to understand how to write triggers.

sfdclearnsfdclearn

Hi,

 

   Thanks for your response! I want this trigger to trigger only When the Account status is changed(though i haven't added that condition in the trigger, for now its in the Account insert/update).When the Account status is changed i want all the product

name(opp products) associated to the account to be copied in the Custom text field in the account. 

  We have a workflow rule that sends an email out when Account status is changed.If we have product name copied on the custom text field we can merge the field in the email template so email template specifically says the product names associated to the account.

sfdclearnsfdclearn

Also I have made modifications in trigger.

 

trigger productname on Account(after insert,after update)
{
   
    
        List<Account> lstAccounts = [Select ProductNameCopy__c from Account];
        List<Id> AccIds =new List<id>();
        for(Account a:lstAccounts)
        {
         AccIds.add(a.Id);
         }
         system.debug(1);
        List<Opportunity> lstopptys = [SELECT Id, Name FROM Opportunity WHERE AccountId IN : AccIds];
        List<Id> oppIds = new List<Id>();
        for(Opportunity o : lstopptys)
        {
           oppIds.add(o.Id);
         }
        system.debug(2);
        List<OpportunityLineItem> lstopptylineitem  = [SELECT Id, PricebookEntry.Product2.Name FROM OpportunityLineItem WHERE OpportunityId IN :oppIds];
        
        for(Account objAccount : trigger.new)
            {
               for(Opportunity objoppty : lstopptys)
               {
                for(opportunitylineitem objopplineitem : lstopptylineitem)
                {
                if(objAccount.AccountStatus__c=='New')
                {
                objAccount.ProductNameCopy__c=objAccount.productNameCopy__c + objopplineitem.PricebookEntry.Product2.Name;
                system.debug(3);
               
                }
                 }
                
         }
          update objAccount;
         }
         }

Error: Invalid Data. 
Review all error messages below to correct your data.
Apex trigger productname caused an unexpected exception, contact your administrator: productname: execution of AfterUpdate caused by: System.FinalException: Record is read-only: Trigger.productname: line 29, column 1
   
   But it throws when i save an account

SammyComesHereSammyComesHere

You are trying to work on Trigger.New Object which is read only in after insert/update. you need to Query the database and get the results and then work on it .

 

Updating Trigger.New Object in After update/insert is not allowed.

 

This is the spolier----

 for(Account objAccount : trigger.new)

 objAccount.ProductNameCopy__c=objAccount.productNameCopy__c + objopplineitem.PricebookEntry.Product2.Name;

crop1645crop1645

My personal preference is to do updates on fields in SObject X within the before trigger on SObject X rather than after triggers. Use after triggers to do updates on related records.  So...ignoring whether the Account qualifies as having its status Changed...

 

trigger AccountTrigger on Account(before update, before insert) {

Map<ID,String> aIdToConcatenatedProductMap = new Map<ID,String> (); for (Opportunity o : [select id, accountId, (select id, PricebookEntry.Product2.Name from OpportunityLineItems) from Opportunity where accountId IN : Trigger.new] ) { String concatenatedProducts = ''; for (OpportunityLineItem oli : o.opportunityLineItems) concatenatedproducts = concatenatedProducts + oli.PricebookEntry.Product2.Name + ' '; if (aIdToConcatenatedProductMap.containskey(o.accountId)) {
String concatSoFar = aIdToConcatenatedProductMap.get(o.accountId);
aIdToConcatenatedProductMap.put(o.accountId,concatSoFar + ' ' + concatenatedProducts);
else
aIdToConcatenatedProductMap.put(o.accountId,concatenatedProducts);
}
for (Account a: Trigger.new)
if (aIdToConcatenatedProductMap.containsKey(a.id))
a.productNameCopy__c = aIdToConcatenatedProductMap.get(a.id);
}

 1 SOQL call instead of 3, no DML calls (Trigger.new changes are implictly inserted/updated by SFDC). Works for bulk operations, gets product names from all of each Account (in trigger list) 's Opportunities that have products.

 

I still feel this would be better done by an after update trigger on Opportunity as whenever an Opportunity productId is added/deleted (change is impossible) in an OpportunityLineItem, the parent Oppo is triggered (because the Amount rolls up) - then your Account is always sync'ed when Oppos changed rather than sync'ing when the account changes.

sfdclearnsfdclearn

Thanks so much!!!

this trigger worked and it brings the product name in the account.

The reason why i am writing the trigger in the account is because 

we have a workflow rule in the Account object that triggers an email template when Account Status is changed to 'New' to customers.

 

we would like to include all the product names Associated to this account's opp in the email as merge field.

 

like 
Thank you for purchasing following products

{!ProductNameCopy__c}.

 

In this scenario we need a custom text field to hold all the product names in the custom text field.

 

Thats the reason i wrote the trigger in the after update so i was assuming i can check the

Account status condition.

 

For example, if account status='New'

then copy all the product names in the custom text field ProductNameCopy__c).

 

 

 

crop1645crop1645

sfdclearn :

 

If you would be so kind, please mark as solution.

 

BTW --

 

1> You can check the Account.status value in before triggers as the values set by workflow field updates will be available to the before trigger in Trigger.new

2> Doing the sync from the Oppo to the Account has other benefits --

 

> the Account always has the list of products, regardless of status. This might be useful to you for other use cases.

> if an Oppo or Opportunity Product is changed/added, the Account won't pick up the changes until the next account update

sfdclearnsfdclearn

Thank you so much again !!!

just wanted to clarify a small thing.

some of the product name is copied twice.

 

Account NamePhone
 
ProductNameCopy
SLA: Gold
GenWatt Gasoline 750kW
GenWatt Gasoline 750kW
xyz
Installation: Industrial - High
Installation: Industrial - High
Installation: Industrial - Low
Fax
 
AccountStatus
 
Website
 
Parent Account
 
Ticker Symbol
 
Account Number
 
Ownership
 
Account Site
 
Employees
 
Type
 
SIC Code
 
Industry
 
  

 

could you please let me know why this some of product names are repeating twice.

 

Thanks!!!!

 

P 

 

 

 

 

 

crop1645crop1645

sfdclearn:

 

well, the code I provided didn't do de-dup'ing, this was left as an exercise for the reader <g>

 

A reason dups are occurring is that an account x has >1 Oppo, each with the same products; or one Oppo has 2+ line items with same products

 

To de-dup, you'll need to modify the Map to be a Map<ID,Set<String>>, then as each OLI is processed, you add to the set. SFDC will ensure the set is unique. Then, before setting the Account's list of products custom field, run through the Set and concatenate -- you can also copy the set into a list and then sort before concatenating

sfdclearnsfdclearn

 

trigger AccountTrigger on Account(before update, before insert)
{

Map<ID,String> aIdToConcatenatedProductMap = new Map<ID,String> ();
Set<string> pb=new Set<String>();
for (Opportunity o : [select id, accountId, (select id, PricebookEntry.Product2.Name from OpportunityLineItems) from Opportunity where accountId IN : Trigger.new] )
{
  String concatenatedProducts = '';
  for (OpportunityLineItem oli : o.opportunityLineItems) 
  {
  concatenatedproducts =   concatenatedProducts + oli.PricebookEntry.Product2.Name + '\n ';
  if (aIdToConcatenatedProductMap.containskey(o.accountId)) 
  {
     
   
     String concatSoFar=aIdToConcatenatedProductMap.get(o.accountId);
     pb.add(concatSoFar);
     aIdToConcatenatedProductMap.put(o.accountId ,pb  + ' ' + concatenatedProducts);
     
  }
  else
  {
     aIdToConcatenatedProductMap.put(o.accountId, concatenatedproducts );
   }
    }
}
for (Account a: Trigger.new)
{
  if (aIdToConcatenatedProductMap.containsKey(a.id))
    a.productNameCopy__c = aIdToConcatenatedProductMap.get(a.id);
    }

}

And the product name is now copied as

{SLA: Gold
, {SLA: Gold
, {SLA: Gold
, {SLA: Gold
} GenWatt Gasoline 750kW
} GenWatt Gasoline 750kW
xyz
, {SLA: Gold
} GenWatt Gasoline 750kW
} Installation: Industrial - High
, {SLA: Gold
, {SLA: Gold
} GenWatt Gasoline 750kW
} GenWatt Gasoline 750kW
xyz
, {SLA: Gold
} GenWatt Gasoline 750kW
} Installation: Industrial - High
Installation: Industrial - Low

 

 

could you please advise?

 

I know my code change might be bad as i am new to apex trigger.

 

Thanks so much!!!

crop1645crop1645

trigger MyAccountTrigger on Account(before update, before insert) {

Map<ID,Set<String>> aIdToUniqueProductSetMap = new Map<ID,Set<String>> ();

// Go through each Oppo and collect a unique set of Product names from all OLI
for (Opportunity o : [select id, accountId,
                        (select id, PricebookEntry.Product2.Name from OpportunityLineItems)
                      from Opportunity where accountId IN :Trigger.new] ) {
  Set<String> productsInOppoSet = new Set<String> (); // reset our set for this Oppo
  for (OpportunityLineItem oli : o.opportunityLineItems)
     productsInOppoSet.add(oli.PricebookEntry.Product2.Name);
  // With the unique set, add it into our Map associating each account to its unique product set
  if (aIdToUniqueProductSetMap.containsKey(o.accountId)) {
      Set<String> uniqueProdSet = aIdToUniqueProductSetMap.get(o.accountId); // add this Oppo's unique set to whatever we have so far
      uniqueProdSet.addAll(productsInOppoSet);
      aIdToUniqueProductSetMap.put(o.accountId,uniqueProdSet); // put it back in the Map
  }    
  else
      aIdToUniqueProductSetMap.put(o.accountId,productsInOppoSet); // first Oppo for this Account; simply do a put

}

// All done with Oppos, now put back into each triggered Account
for (Account a: Trigger.new) {
  if (aIdToUniqueProductSetMap.containsKey(a.id)) {  // only for those Accounts that had Oppos with products
     String concatenatedProducts = '';
     for (String p : aIdToUniqueProductSetMap.get(a.id))   
         concatenatedProducts = concatenatedProducts + (concatenatedProducts.length() == 0 ? '' : ' ') + p; // separate with spaces; other delims possible
     a.description = concatenatedProducts ; // when the trigger completes, the triggered Accounts get updated
  }


}
}

This was selected as the best answer
sfdclearnsfdclearn

I tried the above code.

It shows the following error.

 

Error: Compile Error: expecting a semi-colon, found 'uniqueProdSet' at line 14 column 10

crop1645crop1645

oops -- I corrected line 14

sfdclearnsfdclearn

I correct this but again it shows error on same line

Error: Compile Error: Illegal assignment from Boolean to SET<String> at line 14 column 7

crop1645crop1645

teach me to type in code while watching a baseball game ... this time, i verified it compiles (reposted above)

sfdclearnsfdclearn

thanks a lot ! that worked.

sfdclearnsfdclearn

Hi,

 

   I am trying to include a small logic. that is..

 

  if there is more than opportunity product associated to the a single Account's opportunities then use

 only one common name for that.

 

trigger MyAccountTrigger1 on Account(before update, before insert) {

Map<ID,Set<String>> aIdToUniqueProductSetMap = new Map<ID,Set<String>> ();
Set<String> uniqueProdSet= new Set<String>();

// Go through each Oppo and collect a unique set of Product names from all OLI
for (Opportunity o : [select id, accountId,
(select id, PricebookEntry.Product2.Name from OpportunityLineItems)
from Opportunity where accountId IN :Trigger.new] ) {
Set<String> productsInOppoSet = new Set<String> ();
//reset our set for this Oppo
for (OpportunityLineItem oli : o.opportunityLineItems)
productsInOppoSet.add(oli.PricebookEntry.Product2.Name);
// With the unique set, add it into our Map associating each account to its unique product set
if (aIdToUniqueProductSetMap.containsKey(o.accountId)) {
uniqueProdSet = aIdToUniqueProductSetMap.get(o.accountId); // add this Oppo's unique set to whatever we have so far
uniqueProdSet.addAll(productsInOppoSet);
aIdToUniqueProductSetMap.put(o.accountId,uniqueProdSet); // put it back in the Map
}
else
aIdToUniqueProductSetMap.put(o.accountId,productsInOppoSet); // first Oppo for this Account; simply do a put

}
if(uniqueProdSet.size()>1)

{
for(Account a: Trigger.new)
{a.productNameCopy__c='Academic';}
}
else
{
// All done with Oppos, now put back into each triggered Account
for (Account a: Trigger.new)
{



if (aIdToUniqueProductSetMap.containsKey(a.id)) { // only for those Accounts that had Oppos with products
String concatenatedProducts = '';
for (String p : aIdToUniqueProductSetMap.get(a.id))
concatenatedProducts = concatenatedProducts + (concatenatedProducts.length() == 0 ? '' : ' ') + p; // separate with spaces; other delims possible
a.ProductNameCopy__c = concatenatedProducts ; // when the trigger completes, the triggered Accounts get updated
}
}

 

this works some time and and some time it copies all the product names.

Nazia Pathan 4Nazia Pathan 4
Hi @crop1645,

I am looking to populate both Product names and Product families in two different fields on account could you please guide me on this?

Regards,
Nazia