I have a Visualforce Page for QuoteLineItem multiple edit. It works as a shopping cart: the top section has the items already in the Quote, and in the bottom there are the available products from the selected Price Book. The user can add/remove items from the shopping cart, edit some custom fields and then Save everything and return to the Quote.
But I'm getting a weird behavior in a certain sequence of steps (red arrow is where I will click next).
There are already items in the cart. I will add some random item.
The new item is added, without any of the custom visible fields populated. I will now remove one of the existing items.
The recently added line has now only one of the custom fields populated with whatever was in the (n-1) position. See in the viewState that it has NOT that field populated in the object.
If I try to save it, or add a new item afterwards, that dropbox gets back to not being filled, as it should be.
Oh, and that field (New/Renewal) should not even be possible to populate before setting something in "Fee Type" field, it has a field dependency.
So, I have no idea what is going on. From everything I look in code, it seems OK. I have even written an apex test that replicates these steps and it works. I guess it is something UI related that I am messing up.
Visualforce page:
<apex:page standardController="Quote" extensions="QuoteLineItemEntryExtension" action="{!priceBookCheck}" cache="False">
<c:LoadingBox />
<apex:sectionHeader Title="Manage {!$ObjectType.Product2.LabelPlural}" subtitle="{!quote.Name}"/>
<apex:messages style="color:red"/>
<style>
.search{
font-size:14pt;
margin-right: 20px;
}
.fyi{
color:red;
font-style:italic;
}
.label{
margin-right:10px;
font-weight:bold;
}
</style>
<script type='text/javascript'>
// This script assists the search bar functionality
// It will execute a search only after the user has stopped typing for more than 1 second
// To raise the time between when the user stops typing and the search, edit the following variable:
var waitTime = 0.4;
var countDown = waitTime;
var started = false;
function resetTimer(){
countDown=waitTime;
if(started==false){
started=true;
runCountDown();
}
}
function runCountDown(){
countDown -= 0.2;
if(countDown<=0){
fetchResults();
started=false;
}
else{
window.setTimeout(runCountDown,200);
}
}
</script>
<apex:form >
<apex:outputPanel id="mainBody">
<apex:outputLabel styleClass="label">PriceBook: </apex:outputLabel>
<apex:outputText value="{!theBook.Name}"/>
<apex:commandLink action="{!changePricebook}" value="change" immediate="true"/>
<br/>
<!-- not everyone is using multi-currency, so this section may or may not show -->
<apex:outputPanel rendered="{!multipleCurrencies}">
<apex:outputLabel styleClass="label">Currency: </apex:outputLabel>
<apex:outputText value="{!chosenCurrency}"/>
<br/>
</apex:outputPanel>
<br/>
<!-- this is the upper table... a.k.a. the "Shopping Cart"-->
<!-- notice we use a lot of $ObjectType merge fields... I did that because if you have changed the labels of fields or objects it will reflect your own lingo -->
<apex:pageBlock title="Selected {!$ObjectType.Product2.LabelPlural}" id="selected_items">
<apex:variable var="index" value="{!0}"/>
<apex:pageblockTable value="{!shoppingCart}" var="s">
<apex:column headerValue="Index" value="{!index}"/>
<apex:column>
<apex:commandButton value="Remove" action="{!removeFromShoppingCart}" reRender="selected_items,searchResults" immediate="true" status="loadStatus">
<!-- this param is how we send an argument to the controller, so it knows which row we clicked 'remove' on -->
<apex:param value="{!index}" assignTo="{!toUnselect}" name="toUnselect"/>
</apex:commandButton>
<!-- Increment our index counter -->
<apex:variable var="index" value="{!index +1}"/>
</apex:column>
<apex:column headerValue="{!$ObjectType.Product2.LabelPlural}" value="{!s.PriceBookEntry.Product2.Name}"/>
<apex:column headerValue="{!$ObjectType.QuoteLineItem.Fields.Fee_Type__c.Label}">
<apex:inputField value="{!s.Fee_Type__c}" required="true"/>
</apex:column>
<apex:column headerValue="{!$ObjectType.QuoteLineItem.Fields.Quantity.Label}">
<apex:inputField value="{!s.Quantity}" style="width:70px" required="true"/>
</apex:column>
<!--
<apex:column headerValue="{!$ObjectType.QuoteLineItem.Fields.UnitPrice.Label}">
<apex:inputField value="{!s.UnitPrice}" style="width:70px" required="true"/>
</apex:column>
-->
<apex:column headerValue="{!$ObjectType.QuoteLineItem.Fields.License_category__c.Label}">
<apex:inputField value="{!s.License_category__c}" required="true"/>
</apex:column>
<apex:column headerValue="{!$ObjectType.QuoteLineItem.Fields.License_start_date__c.Label}">
<apex:inputField value="{!s.License_start_date__c}"/>
</apex:column>
<apex:column headerValue="{!$ObjectType.QuoteLineItem.Fields.License_end_date__c.Label}">
<apex:inputField value="{!s.License_end_date__c}" />
</apex:column>
<apex:column headerValue="{!$ObjectType.QuoteLineItem.Fields.Uplift__c.Label}">
<apex:inputField value="{!s.Uplift__c}" style="width:70px" />
</apex:column>
<apex:column headerValue="{!$ObjectType.QuoteLineItem.Fields.SPF__c.Label}">
<apex:inputField value="{!s.SPF__c}" style="width:70px" required="true"/>
</apex:column>
<apex:column headerValue="{!$ObjectType.QuoteLineItem.Fields.Major_Account_Discount__c.Label}">
<apex:inputField value="{!s.Major_Account_Discount__c}" style="width:70px" required="false"/>
</apex:column>
<!-- Services related fields -->
<apex:column headerValue="{!$ObjectType.QuoteLineItem.Fields.Service_description__c.Label} (Services)">
<apex:inputField value="{!s.Service_description__c}" required="false"/>
</apex:column>
<apex:column headerValue="{!$ObjectType.QuoteLineItem.Fields.Start_date__c.Label} (Services)">
<apex:inputField value="{!s.Start_date__c}" required="false"/>
</apex:column>
<apex:column headerValue="{!$ObjectType.QuoteLineItem.Fields.Duration__c.Label} (Services)">
<apex:inputField value="{!s.Duration__c}" required="false"/>
</apex:column>
</apex:pageblockTable>
<apex:pageBlockButtons >
<apex:commandButton action="{!onSave}" value="Save" status="loadStatus"/>
<apex:commandButton action="{!onCancel}" value="Cancel" immediate="true" status="loadStatus"/>
</apex:pageBlockButtons>
</apex:pageBlock>
<!-- this is the lower table: search bar and search results -->
<apex:pageBlock >
<apex:outputPanel styleClass="search">
Search for {!$ObjectType.Product2.LabelPlural}:
</apex:outputPanel>
<apex:actionRegion renderRegionOnly="false" immediate="true">
<apex:actionFunction name="fetchResults" action="{!updateAvailableList}" reRender="searchResults" status="searchStatus"/>
<!-- here we invoke the scripting to get out fancy 'no button' search bar to work -->
<apex:inputText value="{!searchString}" onkeydown="if(event.keyCode==13){this.blur();}else{resetTimer();}" style="width:300px"/>
<i>
<!-- actionStatus component makes it easy to let the user know when a search is underway -->
<apex:actionStatus id="searchStatus" startText="searching..." stopText=" "/>
</i>
</apex:actionRegion>
<br/>
<br/>
<apex:outputPanel id="searchResults">
<apex:pageBlockTable value="{!AvailableProducts}" var="a">
<apex:column headerValue="{!$ObjectType.Product2.Fields.Name.Label}" value="{!a.Product2.Name}" />
<apex:column headerValue="{!$ObjectType.Product2.Fields.Family.Label}" value="{!a.Product2.Family}"/>
<apex:column headerValue="{!$ObjectType.Product2.Fields.Legacy__c.Label}" value="{!a.Product2.Legacy__c}"/>
<apex:column headerValue="Sales Price" value="{!a.UnitPrice}"/>
<apex:column headerValue="{!$ObjectType.Product2.Fields.Description.Label}" value="{!a.Product2.Description}"/>
<apex:column >
<!-- command button in a column... neato -->
<apex:commandButton value="Select" action="{!addToShoppingCart}" reRender="selected_items,searchResults" immediate="true" status="loadStatus">
<!-- again we use apex:param to be able to tell the controller which row we are working with -->
<apex:param value="{!a.Id}" assignTo="{!toSelect}" name="toSelect"/>
</apex:commandButton>
</apex:column>
</apex:pageBlockTable>
<!-- We put up a warning if results exceed 100 rows -->
<apex:outputPanel styleClass="fyi" rendered="{!overLimit}">
<br/>
Your search returned over 100 results, use a more specific search string if you do not see the desired {!$ObjectType.Product2.Label}.
<br/>
</apex:outputPanel>
</apex:outputPanel>
</apex:pageBlock>
</apex:outputPanel>
</apex:form>
</apex:page>
Add and remove function excerpts, Apex code (controller):
public with sharing class QuoteLineItemEntryExtension {
public Quote theQuote {get;set;}
public String searchString {get;set;}
public quoteLineItem[] shoppingCart {get;set;}
public priceBookEntry[] AvailableProducts {get;set;}
public Pricebook2 theBook {get;set;}
public String toSelect {get; set;}
public String toUnselect {get; set;}
public Decimal Total {get;set;}
public Boolean overLimit {get;set;}
public Boolean multipleCurrencies {get; set;}
private Boolean forcePricebookSelection = false;
private quoteLineItem[] forDeletion = new quoteLineItem[]{};
private void Initialize(ApexPages.StandardController controller) {
// Need to know if org has multiple currencies enabled
multipleCurrencies = UserInfo.isMultiCurrencyOrganization();
// Get information about the quote being worked on
if (multipleCurrencies) {
theQuote = database.query('select Id, Pricebook2Id, Pricebook2.Name, CurrencyIsoCode from Quote where Id = \'' + controller.getRecord().Id + '\' limit 1');
}
else {
theQuote = [select Id, Pricebook2Id, PriceBook2.Name from Quote where Id = :controller.getRecord().Id limit 1];
}
// If products were previously selected need to put them in the "selected products" section to start with
shoppingCart = [select Id, Quantity, TotalPrice, UnitPrice, Description, License_start_date__c, License_end_date__c, SPF__c,
License_category__c, Fee_Type__c, Uplift__c, Major_Account_Discount__c, Service_description__c, Start_date__c, Duration__c,
PriceBookEntryId, PriceBookEntry.Name, PriceBookEntry.IsActive, PriceBookEntry.Product2Id, PriceBookEntry.Product2.Name,
PriceBookEntry.PriceBook2Id from quoteLineItem where QuoteId=:theQuote.Id ORDER BY SortOrder ASC];
// Check if Opp has a pricebook associated yet
if(theQuote.Pricebook2Id == null){
Pricebook2[] activepbs = [select Id, Name from Pricebook2 where isActive = true limit 2];
if(activepbs.size() == 2){
forcePricebookSelection = true;
theBook = new Pricebook2();
}
else{
theBook = activepbs[0];
}
}
else{
theBook = theQuote.Pricebook2;
}
if(!forcePricebookSelection) {
updateAvailableList();
}
}
public QuoteLineItemEntryExtension(ApexPages.StandardController controller)
{
Initialize(controller);
}
public PageReference addToShoppingCart() {
for(PricebookEntry d : AvailableProducts){
String entry_id = (String) d.Id;
if (entry_id.equals(toSelect)) {
QuoteLineItem new_item = new QuoteLineItem(
QuoteId=theQuote.Id,
PriceBookEntry=d,
PriceBookEntryId=d.Id,
UnitPrice=d.UnitPrice,
Quantity=1,
SPF__c=0
);
shoppingCart.add(new_item);
break;
}
}
return null;
}
public PageReference removeFromShoppingCart() {
QuoteLineItem to_remove = shoppingCart.remove(Integer.valueOf(toUnselect));
if (to_remove.Id != null) {
forDeletion.add(to_remove);
}
return null;
}
public PageReference onSave()
{
// If previously selected products are now removed, we need to delete them
try {
if(forDeletion.size()>0) {
delete(forDeletion);
}
} catch (System.DmlException e) {
if (e.getMessage().contains('ENTITY_IS_DELETED')) {
ApexPages.addMessage(
new ApexPages.Message(
ApexPages.Severity.WARNING,
'Tried to delete the same item more than once. Please check if the Quote now contains the desired items and repeat the delete operation if necessary.'
)
);
return null;
}
else {
ApexPages.addMessages(e);
return null;
}
}
// Previously selected products may have new quantities and amounts, and we may have new products listed, so we use upsert here
try{
if(shoppingCart.size()>0) {
upsert(shoppingCart);
}
}
catch(Exception e){
ApexPages.addMessages(e);
return null;
}
// After save return the user to the quote
return new PageReference('/' + ApexPages.currentPage().getParameters().get('Id'));
}
public PageReference onCancel()
{
// If user hits cancel we commit no changes and return them to the quote
return new PageReference('/' + ApexPages.currentPage().getParameters().get('Id'));
}
public void updateAvailableList()
{
// We dynamically build a query string
// The original code excludes items already in the shopping cart, but we removed this feature
String qString = 'select Id, Pricebook2Id, IsActive, Product2.Name, Product2.Family, Product2.Legacy__c, Product2.IsActive, Product2.Description, UnitPrice from PricebookEntry where IsActive=true and Pricebook2Id = \'' + theBook.Id + '\'';
if(multipleCurrencies) {
qstring += ' and CurrencyIsoCode = \'' + theQuote.get('currencyIsoCode') + '\'';
}
// note that we are looking for the search string entered by the user in the name OR description
// modify this to search other fields if desired
if (searchString!=null) {
qString+= ' and (Product2.Name like \'%' + searchString + '%\' or Product2.Description like \'%' + searchString + '%\')';
}
qString+= ' order by Product2.Name';
qString+= ' limit 201';
system.debug('qString:' +qString);
AvailableProducts = database.query(qString);
// We only display up to 200 results... if there are more than we let the user know (see vf page)
if (AvailableProducts.size()==201) {
AvailableProducts.remove(200);
overLimit = true;
}
else{
overLimit=false;
}
}
}
immediate=true
, it does not fire any getters/setters. I wonder if that is the issue here. Can you include the backend for the relevant properties as well?