2

I am currently working on a lightning quick action which will update OrderItems on demand from a specified quote. If an OrderItem has no corresponding QuoteLineItem, the line item will be deleted. This is where something strange happens.

Previously, we were not referencing the QuoteLineItemId field when generating OrderItems. I have just recently written code which now references a QuoteLineItemId for every OrderItem. Curiously, when I attempt to sync old OrderItems (in a partial sandbox) I receive the following error.

ResyncOrderLinesfromQuote.AuraHandledException: Something went wrong while deleting orderItems. Error Message: Delete failed. First exception on row 0 with id 8021J000005bcVCQAY; first error: INSUFFICIENT_ACCESS_OR_READONLY, insufficient access rights on object id: []

I do not receive this error when attempting to sync Order Items generated with new code. I suspect the error may have something to do with the fact that the QuoteLineItemId is not referenced on the old OrderItems. but I am at my wits end trying to identify what exactly is causing the problem and how to fix it. I have scoured the internet for answers but haven't found anything yet.

Note: I have full Admin/Modify All Data rights to all involved objects.

I am a new, self taught programmer. Any constructive criticism is greatly appreciated. Thanks!

Class

public without sharing class ResyncOrderLinesfromQuote {
//Purpose: From Button on Order record page, sync's QuoteLineItems>OrderItems one way. Deletes OrderItems without corresponding QuoteLineItems in batch function.

@AuraEnabled
public static Void syncLineItems(Id orderId) {
    Order ord = [SELECT Id, QuoteId FROM Order WHERE Order.Id =: orderId]; //query order here with orderId
    Map<ID, OrderItem> orderItemMap = getOrderItems(ord.Id);
    //System.debug('orderItemMap: ' + orderItemMap);
    //Two main maps below 
    Map<ID, QuoteLineItem> quoteLineItemMap = getQuoteLineItemMap(ord.QuoteId);
    //System.debug('quoteLineItemMap: ' + quoteLineItemMap);
    ///The keys are the QuoteLineItemId fields of (value:) OrderItems. Nulls are ommitted.
    Map<ID, OrderItem> qliIdsToOrderItemMap = new Map<Id, OrderItem>();

    ///OrderItemIds for orders which have null for the QuoteLineItemId field
    Set<Id> orphanOIs = new Set<Id>();

    for(OrderItem line : orderItemMap.values()) {
        if(line.QuoteLineItemId != null) {
            System.debug('line.QuoteLineItemId != null');
            qliIdsToOrderItemMap.put(line.QuoteLineItemId, line);
        } else {
            orphanOIs.add(line.Id);
        }
    }
    System.debug('quoteLineItemMap: ' + quoteLineItemMap);
    System.debug('qliIdsToOrderItemMap: ' + qliIdsToOrderItemMap);
    System.debug('orphanOIs: ' + orphanOIs);
    Set<Id> orphanQuoteIds = new Set<Id>(quoteLineItemMap.keySet());
    orphanQuoteIds.removeAll(qliIdsToOrderItemMap.keySet());
    System.debug('quoteLineItemMap2: ' + quoteLineItemMap);
    List<OrderItem> orderItemsToUpdate = processOrderItemsForUpdates(quoteLineItemMap, qliIdsToOrderItemMap);
    List<OrderItem> orderItemsToInsert = createOrderItemsForOrphanQuoteLineItems(quoteLineItemMap, orphanQuoteIds, orderId);
    List<OrderItem> orderItemsToDelete = [SELECT id FROM OrderItem WHERE Id IN :orphanOIs];
    System.debug('orderItemsToDelete: ' + orderItemsToDelete);
    try {
        delete orderItemsToDelete;
    } catch (DmlException e) {
        System.debug(e);
        throw new AuraHandledException('Something went wrong while deleting orderItems. Error Message: ' + e.getMessage());
    }

    //orderItemsToUpdate.addAll(orderItemsToInsert);
    update orderItemsToUpdate;
    system.debug('orderItemsToInsert: ' + orderItemsToInsert);
    system.debug('orderItemsToUpdate: ' + orderItemsToUpdate);
    // try {
    // insert orderItemsToInsert;
    // } catch (DmlException e) {
    //     System.debug(e.getMessage());
    // }
    Database.SaveResult[] srList = Database.insert(orderItemsToInsert, false);
    // Iterate through each returned result
    for (Database.SaveResult sr : srList) {
        if (!sr.isSuccess()) {
            // Operation failed, so get all errors                
            for(Database.Error err : sr.getErrors()) {
                System.debug('The following error has occurred.');                    
                System.debug(err.getStatusCode() + ': ' + err.getMessage());
                System.debug('Fields that affected this error: ' + err.getFields());
            }
        }
    }
}

public static List<OrderItem> processOrderItemsForUpdates(Map<Id, QuoteLineItem> qliMap, Map<Id, OrderItem> qliIdsToOrderItemMap) {
    System.debug('processOrderItemsForUpdates: qliMap - ' + qliMap);
    System.debug('processOrderItemsForUpdates: qliIdsToOrderItemMap - ' + qliIdsToOrderItemMap);
    List<OrderItem> orderItemsToUpdate = new List<OrderItem>();
    ///QuoteLineItemIds for QuoteLineItems paired/referenced with/by OrderItems.
    Set<ID> matchedQuoteLineItemIDs = new Set<Id>(qliIdsToOrderItemMap.keySet());
    ///   qliRefInOIs ∩ quoteLineItemMap.keySet()
    matchedQuoteLineItemIDs.retainAll(qliMap.keySet());
    Boolean recheckOrderStarDate = false;
    Date newOrderStartDate = Date.today();
    Id currentOrderId = null;

    for (Id qliId : matchedQuoteLineItemIDs) {
        QuoteLineItem qli = qliMap.get(qliId);
        OrderItem oi = qliIdsToOrderItemMap.get(qliId);
        System.debug(qli.LastModifiedDate);
        System.debug(oi.LastModifiedDate);
        //do stuff: copy changes from qli to oi
        if(oi.Discount__c != qli.Discount) {
            oi.Discount__c = qli.Discount;
        }
        if(oi.Distributor__c != qli.Distributor__c) {
            oi.Distributor__c = qli.Distributor__c;
        }
        if(oi.NSP_Price__c != qli.NSP_Price__c) {
            oi.NSP_Price__c = qli.NSP_Price__c;
        }
        if(qli.Quote.Override_Product_Description__c == true) {
            System.debug('Override_Product_Description__c: ' + qli.Quote.Override_Product_Description__c);
            String qlidesc = RecordGenerator.formatLineItemDescription(qli.Description);
            system.debug('qlidesc: ' + qlidesc);
            system.debug('oi.Description: ' + oi.Description);
            if(oi.Description != qlidesc) {
                oi.Description = qlidesc;
            }
        } else {
            String qlidesc = RecordGenerator.formatLineItemDescription(qli.Product2.Description);
            system.debug('qlidesc: ' + qlidesc);
            system.debug('oi.Description: ' + oi.Description);
            if(oi.Description != qlidesc) {
                oi.Description = qlidesc;
            } 
        }        
        if(oi.Quantity != qli.Quantity) {
            oi.Quantity = qli.Quantity;
        }
        if(oi.Serial_Number__c != qli.Serial_Number__c) {
            oi.Serial_Number__c = qli.Serial_Number__c;
        }
        //if recheckOrderStarDate == true - will trip an update of the order StartDate upon completion of the list iteration 
        if(oi.ServiceDate != qli.Start_Date__c){
            if(oi.Order.EffectiveDate > qli.Start_Date__c) {
                if (recheckOrderStarDate == true) {
                    if(qli.Start_Date__c < newOrderStartDate) {
                        newOrderStartDate = qli.Start_Date__c;
                    }    
                } else {
                        recheckOrderStarDate = true;
                        newOrderStartDate = qli.Start_Date__c;
                        currentOrderId = oi.OrderId;
                }
            }
            oi.ServiceDate = qli.Start_Date__c;
        }
        if(oi.EndDate != qli.End_Date__c) {
            oi.EndDate = qli.End_Date__c;
        }
        if(oi.UnitPrice != qli.UnitPrice) {
            oi.UnitPrice = qli.UnitPrice;
        }
        if(oi.PricebookEntryId != qli.PricebookEntryId) {
            oi.PricebookEntryId = qli.PricebookEntryId;
        }
        orderItemsToUpdate.add(oi);
        System.debug(qli.LastModifiedDate);
        System.debug(oi.LastModifiedDate);                
    }
    if (recheckOrderStarDate == true) {
        Order currentOrder = [SELECT Id, EffectiveDate FROM Order WHERE Id =: currentOrderId][0];
        currentOrder.EffectiveDate = newOrderStartDate;
        update currentOrder;
    }
    return orderItemsToUpdate;
}

public static List<OrderItem> createOrderItemsForOrphanQuoteLineItems(Map<Id, QuoteLineItem> qliMap, Set<Id> orphanQuoteIds, Id orderId) {
    List<OrderItem> newItems = new List<OrderItem>();
    for (Id quoteLineItemId : orphanQuoteIds) {
        QuoteLineItem quoteLineItemToCopy = qliMap.get(quoteLineItemId);
        System.debug('quoteLineItemToCopy: ' + quoteLineItemToCopy);
        OrderItem newMatchingOrderItem = RecordGenerator.generateOrderItemFromQuoteLineItem(quoteLineItemToCopy);
        newMatchingOrderItem.orderId = orderId;
        newItems.add(newMatchingOrderItem);    
    }
    return newItems;
}

public static Map<ID, QuoteLineItem> getQuoteLineItemMap(Id syncedQuoteId) {
    Map<ID, QuoteLineItem> quoteLineItemMap = new Map<Id, QuoteLineItem>([SELECT Id,
                                                                            Discount,
                                                                            Distributor__c,
                                                                            NSP_Price__c,
                                                                            PricebookEntry.ProductCode,
                                                                            Description,
                                                                            Product2.Description,
                                                                            Quantity,
                                                                            Serial_Number__c,
                                                                            Start_Date__c,
                                                                            End_Date__c,                                                
                                                                            UnitPrice,
                                                                            PricebookEntryId,
                                                                            LastModifiedDate,
                                                                            Quote.Override_Product_Description__C
                                                                        FROM QuoteLineItem WHERE QuoteLineItem.QuoteId =: syncedQuoteId]);
    return quoteLineItemMap;
}
public static Map<ID, OrderItem> getOrderItems(Id currentOrderId) {
    Map<ID, OrderItem> orderItemMap = new Map<ID, OrderItem>([SELECT OrderId,
                                        Discount__c,
                                        Distributor__c,
                                        NSP_Price__c,
                                        Description,
                                        Quantity,
                                        Serial_Number__c,
                                        ServiceDate,
                                        EndDate,
                                        UnitPrice,
                                        PricebookEntryId,
                                        QuoteLineItemId,
                                        LastModifiedDate,
                                        Order.EffectiveDate
                                    FROM OrderItem WHERE OrderItem.OrderId =: currentOrderId]);
    return orderItemMap;
}
public class AuraHandledException extends Exception {}

}

Code to generate OrderItems

global without sharing class RecordGenerator {
@TestVisible     
public static OrderItem generateOrderItemFromQuoteLineItem(QuoteLineItem lineItem) {
    OrderItem newOrderItem = new OrderItem();
    newOrderItem.Discount__c = lineItem.Discount;
    newOrderItem.Distributor__c = lineItem.Distributor__c;
    newOrderItem.EndDate = lineItem.End_Date__c;
    newOrderItem.NSP_Price__c = lineItem.NSP_Price__c;
    newOrderItem.Product2Id = lineItem.Product2Id;
    if(lineItem.Quote.Override_Product_Description__c == true) {
        newOrderItem.Description = formatLineItemDescription(lineItem.Description);
    } else {
        newOrderItem.Description = formatLineItemDescription(lineItem.Product2.Description);
    }
    newOrderItem.Quantity = lineItem.Quantity;
    newOrderItem.Serial_Number__c = lineItem.Serial_Number__c;
    newOrderItem.ServiceDate = lineItem.Start_Date__c;
    newOrderItem.UnitPrice = lineItem.UnitPrice;
    newOrderItem.PricebookEntryId = lineItem.PricebookEntryId;
    newOrderItem.QuoteLineItemId = lineItem.Id;
    return newOrderItem;
}  
public static String formatLineItemDescription(String productDescription) {
    if (productDescription != null) {
        if (productDescription.length() < 255) {
            return productDescription; 
        } else if (productDescription.length() >= 255) {
            String formatedDescription = productDescription.substring(0, Math.min(productDescription.length(), 254));
            return formatedDescription;
        } 
        return '';
    } else {
        return null;
    }
}    

}

  • Can you open this record "8021J000005bcVCQAY" in your browser? As you mentioned its a partial copy sandbox, it could be ghost record. – Pranay Jaiswal May 22 '18 at 21:12
  • Hi, thanks for responding! I can open the record and it is an OrderItem. I have tried this on multiple records, btw. – Destiny May 22 '18 at 21:19
  • Can you delete them via UI? – Pranay Jaiswal May 22 '18 at 21:19
  • I cannot. I receive the same error. I can, however delete new OrderItems made with the new RecordGenerator code via Apex. – Destiny May 22 '18 at 21:21
  • Exactly... my point they are ghost records.. I have updated answer for the same. New records are fresh records.. you will be able to delete them. – Pranay Jaiswal May 22 '18 at 21:22
1

Salesforce support took a few days to look over this issue. It turns out the problem was that the Order status was not set to 'Draft' while the code was making edits. You cannot edit an Order not set to Draft status.

I added code to check the order status and update if necessary. Everything runs as it should now. Hope this helps someone avoid my mistake!

0

As its a partial copy sandbox, few records which are modified during Sandbox refresh gets corrupted. They then appear in querry and you cant do any DML on those ghost record.

If users are unable to access a particular record or subset of records in a sandbox environment, first ensure that they have appropriate access granted via security controls.

If the affected user(s) clearly have appropriate access, via the View/Modify all data permissions for example, and are encountering an "Insufficient Privileges" message upon attempting to access the data through the user interface, or are receiving an error message stating, "insufficient access rights on object id" upon attempting to modify or delete records via the API:

Check the system audit fields on the affected records by exporting the data from the sandbox environment or check them in production if you cannot view/export the data from sandbox.

Compare the created or last modified system audit field's date stamps to see if they correspond to the affected sandbox's refresh date. If they correlate to one another the unexpected behavior may be the result of the affected records being created or modified during the sandbox refresh process.

Resolution: Contact SF support.

Source: https://help.salesforce.com/articleView?id=000170930&type=1

  • oh dear, partial sandbox refresh creates records with invalid data integrity relationships? Good to know – cropredy May 22 '18 at 22:00
  • @cropredy Yeah.. it sucks bigtime :( I have spent hours debugging this, thinking my apex code is wrong. It can also be present in full copy sandbox. – Pranay Jaiswal May 22 '18 at 22:03
  • Thank you so much for looking at this @PranayJaiswal. Salesforce support took a few days to look over this issue. It turns out the problem was that the Order status was not set to 'Draft' while the code was making edits. You cannot edit an Order not set to Draft status. I added code to check the order status and update if necessary. Everything runs as it should now. Hope this helps someone avoid my mistake! – Destiny May 29 '18 at 17:45
  • Sorry but that won't give you insufficient access. It will give exception as dml exception, validation exception etc.. Insufficient access is majorly due to ghost record – Pranay Jaiswal May 29 '18 at 17:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.