[SalesForce] INSUFFICIENT_ACCESS_OR_READONLY, when deleting OrderItems in Apex

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;
    }
}    

}

Best Answer

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!

Related Topic