My data structure:

Opportunity -> SH_Quote__c -> SH_Quote_Line__c

Apex trigger at SH_Quote_Line__c level

public without sharing class QuotationLineItemController {
  //After Update Trigger Handler
  public static void AfterUpdate (List<SH_Quote_Line__c> records){

    if(records.size() > 0){
        Boolean checkResponded = True;

        for (SH_Quote_Line__c ql : records){
            SH_Quote__c quoteObj = [SELECT Id, Status__c, OwnerId, OwnerEmail__c FROM SH_Quote__c WHERE Id = :ql.SH_Quote__c ];

            List<SH_Quote_Line__c> quoteLineObjs = new List<SH_Quote_Line__c>(
                [SELECT id, Name, Status__c FROM SH_Quote_Line__c WHERE SH_Quote__c = :quoteObj.id]);

            for(SH_Quote_Line__c child : quoteLineObjs){
                System.debug('quoteLineObjs.id =  ' +child.id);
                System.debug('quoteLineObjs.Status__c  =  ' +child.Status__c);

                if(child.Status__c == 'Notified'){
                    checkResponded &= False;
                    quoteObj.Status__c = 'Notified';

                if(child.Status__c == 'In Progress') {
                    checkResponded &= False;
                    quoteObj.Status__c = 'In Progress';

                if(child.Status__c == 'Responded'){
                    checkResponded &= True;
                    quoteObj.Status__c = 'In Progress';

                System.debug('checkResponded =  ' +checkResponded);

            System.debug('checkResponded FINAL =  ' +checkResponded);

            if (checkResponded == True){
                quoteObj.Status__c = 'Completed';

            update quoteObj;

            quoteObj = [SELECT Id, Status__c, OwnerId, OwnerEmail__c FROM SH_Quote__c WHERE Id = :ql.SH_Quote__c ];

            if (quoteObj.Status__c == 'Completed'){

                List<EmailTemplate> lstEmailTemplates = [SELECT Id, Body, Subject from EmailTemplate where DeveloperName = 'Quotation_VF_Notify_Email'];

                //Pick any dummy contact
                Contact c = [select id, Email from Contact where Email != null limit 1];

                Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
                mail.setTargetObjectId(c.id); //Set to dummy contact
                mail.setTreatTargetObjectAsRecipient(false); //dont send to dummy contact
                mail.setToAddresses(new String[] {quoteObj.OwnerEmail__c}); //Send to Quotation Owner under additional email
                System.debug('quoteObj :' + quoteObj); 

                Messaging.SendEmailResult[] resultMail = Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
                System.debug('SendEmailResult :' + resultMail); 

To explain briefly what my code does is on SH_Quote_Line__c level, when I update a record such that quoteObj.Status__c = 'Completed', I want it to send an email to the SH_Quote__c Owner.

This trigger logic works completely fine as a System Admin however when I use a different profile (Sales Manager profile) to update a record in SH_Quote_Line__c I get the error below:

We can't save this record because the “[SH_Quote_Line_c] Update Status logic” process failed. Give your Salesforce admin these details. This error occurred when the flow tried to update records: CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY: QuoteLineTrigger: execution of AfterUpdate caused by: System.EmailException: SendEmail failed. First exception on row 0; first error: INVALID_CROSS_REFERENCE_KEY, invalid cross reference id: [] Class.QuotationLineItemController.AfterUpdate: line 101, column 1 Trigger.QuoteLineTrigger: line 30, column 1

My Sales Manager profile has Read & Edit permissions to all 3 objects so I cannot undersand why this is not working for that particular profile???


  1. If I uncomment "mail.setWhatId(quoteObj.id);" there is no error but it just gives an email without any merge fields attached to my VF Email Template
  2. I have tried using Process Builder and Workflow rules to send an email when Status__c = 'Completed' but I still get the same error (that's why I thought apex could do the trick - seems it can't)
  • add a Sharing button to the quote and see if that record has been shared to the sales manager (explicitly or implicitly via sharing rules)
    – cropredy
    Dec 4, 2019 at 16:48
  • I can't seem to make the sharing button visible on the quote detail record even when adding to "Salesforce Mobile and Lightning Experience Actions"
    – compski
    Dec 4, 2019 at 17:01
  • It is a detail page button; switch to classic if you have to
    – cropredy
    Dec 4, 2019 at 17:51
  • But why are you recommending this method? this has to be rolled out to like 200 users, I can't simply share every single quote this same way
    – compski
    Dec 5, 2019 at 1:17
  • 1
    I'm recommending the sharing button as a diagnostic tool so you can adjust your sharing model
    – cropredy
    Dec 5, 2019 at 2:59

1 Answer 1


In the Org Hierarchy, since both the Quote and Quote Line was set as Private. Had to use Apex Managed Sharing to execute at the After Insert trigger in Quote Line level to enable the non-admins to send email otherwise the "mail.setWhatId(quoteObj.id)" will complain.

public static void grantUserQuoteRecordAccessRight(List<SH_Quote_Line__c> objlist){

List <SH_Quote_Line__Share> qlObjList = new List<SH_Quote_Line__Share>();
List <SH_Quote__Share> quoteShareList = new List<SH_Quote__Share>();

for(SH_Quote_Line__c ql : objlist){
    SH_Quote__c quoteObj = [SELECT Id, Status__c, OwnerId, OwnerEmail__c FROM SH_Quote__c WHERE Id = :ql.SH_Quote__c ];

    for(User user : [Select Id, email, Name, UserRole.Name, Username from user where IsActive=true and Id != :ql.OwnerId]){
        SH_Quote_Line__Share qlObj = new SH_Quote_Line__Share();
        SH_Quote__Share qObj = new SH_Quote__Share(); 

        qlObj.ParentId = obj.Id;
        qlObj.AccessLevel = 'Edit';
        qlObj.RowCause = Schema.SH_Quote_Line__Share.RowCause.Manual;
        qlObj.UserOrGroupId = user.Id;

        qObj.ParentId = obj.Id;
        qobj.AccessLevel = 'Edit';
        qobj.RowCause = Schema.SH_Quote__Share.RowCause.Manual;
        qObj.UserOrGroupId = user.Id;
    Database.SaveResult[] result = Database.Insert(newobjlist, false);
    Database.SaveResult[] result1 = Database.Insert(quoteShareList, false);
}catch(Exception ex){
    System.debug(' ######## ERROR ############ ' + ex.getMessage());
    System.debug(' ######## ERROR ############ ' + ex.getStackTraceString());

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .