Clone a Quote Line Item with an Apex Controller

Jasper asked:

I’m in need of a custom clone button to clone single QuoteLineItems [with URL hacking].

I followed up with Jasper privately in email, committing to helping him. He confirmed in email that the following solution worked for him, so I’ll describe my process.

First, I tried the solution he described to make sure I could replicate the error he was seeing. He did a custom URL button displayed on a Quote Line Item with the following content:

/{!QuoteLineItem.Id}/e?clone=1&retURL={!QuoteLineItem.Id}

Confirmed the following error showed up:

Error: Invalid Data.
Review all error messages below to correct your data.
• You must enter a value (Related field: Price Book Entry)
• You must enter a value (Related field: Product)

Next, my thought was to see if we could actually utilize the URL Hacking post to populate all of the individual fields. I didn’t get too far with this approach, because I wasn’t able to specify the PricebookEntryId record on the URL query string, which happened to be vital.

Next, I did a quick proof of concept to ensure that I could simply clone it with the Salesforce sobject clone system method (as seen in the developer console):

QuoteLineItem ql = [select Id, QuoteId, PricebookEntryId,
                           Quantity, UnitPrice, Discount,
                           Description, ServiceDate, SortOrder,
                           ListPrice, Subtotal, TotalPrice
                    from QuoteLineItem
                    limit 1];
QuoteLineItem q2 = ql.clone();
insert q2;
System.debug('New Line Item: '+q2);

Once I inspected that the new record looked ok, I began to develop the Visualforce page wrapper for this logic:

<apex:page standardController="QuoteLineItem"
  extensions="QuoteCloneController" action="{!doClone}">

  <apex:pageMessages />
</apex:page>

Jasper mentioned that there were a number of custom fields on the Quote Line Item, so I made sure to have code which programmatically grabbed these fields to clone. Here’s the apex:

public with sharing class QuoteCloneController {
    private Id recordId;
    public QuoteCloneController(ApexPages.StandardController sc) {
        recordId = sc.getId();
    }

    public PageReference doClone() {
        if (recordId == null || (Id)recordId != recordId) {
            ApexPages.addMessage(new ApexPages.Message(ApexPages.SEVERITY.FATAL, 'QuoteLineId must be supplied.'));
        }
        try {
            String query = 'select Quoteid, PricebookEntryId, Quantity, UnitPrice, Discount, Description, ServiceDate, ListPrice, Subtotal, TotalPrice ';
            // get all of the custom, writeable fields
            Map<String, Schema.SObjectField> fieldMap = QuoteLineItem.getSObjectTypE().getDescribe().fields.getMap();
            for (String key : fieldMap.keySet()) {
                Schema.SObjectField field = fieldMap.get(key);
                Schema.DescribeFieldResult result = field.getDescribe();
                if (result.isCustom() && !result.isCalculated()) {
                    query += ' ,'+result.getName();
                }
            }
            query += ' from QuoteLineItem where Id = :recordId';

            QuoteLineItem ql = Database.query(query);
            QuoteLineItem q2 = ql.clone();
            insert q2;
            return new PageReference('/'+q2.Id);
        } catch(DMLException e) {
            ApexPages.addMessages(e);
            return null;
        }
    }
}

and its test class:

@isTest
private class QuoteCloneController_Tests {
    @isTest(SeeAllData=true) // required for using the standard pricebook, unfortunately
    private static void testClone() {
        Account a = new Account(Name = 'Test Account');
        insert a;

        Opportunity o = new Opportunity(Name = 'Test Opp', StageName = 'Test Stage', CloseDate = Date.today(), AccountId = a.Id);
        insert o;

        Pricebook2 pb = [select Id from Pricebook2 where IsStandard = true limit 1];

        Product2 p = new Product2(Name = 'Test Product', isActive = true);
        insert p;

        PricebookEntry pbe = new PricebookEntry(Pricebook2Id = pb.Id, Product2Id = p.Id, UnitPrice = 1, isActive = true);
        insert pbe;

        Quote q = new Quote(Name = 'Test Quote', OpportunityId = o.Id, PriceBook2Id = pb.Id);
        insert q;

        QuoteLineItem qli = new QuoteLineItem(QuoteId = q.Id, PriceBookEntryId = pbe.Id, Quantity = 1, UnitPrice = 100);
        insert qli;

        QuoteCloneController con = new QuoteCloneController(new ApexPages.StandardController(qli));
        System.assertNotEquals(null, con.doClone());
    }
}

and finally, the Detail page URL button for Quote Line Item:

/apex/QuoteClone?id={!QuoteLineItem.Id}

I bundled these changes into an unmanaged package, then I instructed Jasper to install it, add the correct security to the visualforce page, and add the button to the Quote Line Item page layout to confirm it worked. He did that later, so I’m sharing the solution here now.

Overall the solution took me about 45 minutes of discovery and 45 minutes of development. Hopefully it helps someone else also!

This entry was tagged , , , . Bookmark the permalink.

3 Responses to Clone a Quote Line Item with an Apex Controller

  1. Aussie Bob says:

    Hi,

    This is a gripe that has affected my deployment for some time now – the URL trick never worked for the Pricebook reason you described above.

    I knew that Apex etc would be able to provide a solution but I am not an experienced coder.

    I just deployed this solution and it worked for me, now have a user testing more thoroughly.

    Thank you for this :) :) :)

    Any tips before “going live”?

  2. Chirag Mehta says:

    Detailed post, nice!

  3. Nice. PricebookEntryId is kind of an odd field. I’m working on a generic clone utility, and the field doesn’t behave how I would expect.

Comments are closed.