A Poor Man’s Product Configurator

I was recently contacted by one of my blog readers to help him with a project. It sounded like an interesting challenge, so I agreed to help him with a little guidance and also post an overview of what we did so others could learn as well.

The business problem: let’s say your company makes custom widgets. A part of the sales process is creating a quote request. You don’t want your sales people to have a generic quote request form to fill out. Instead, you want to only show them the options based on the type of  product for which they are creating the quote request.

There are a few ways to accomplish this. I originally proposed a fairly complicated, but extensible model using related lists so you could configure an unlimited number of options. It turned out that the product model was a bit simpler and we could do it with just the product object and some Visualforce and a controller extension. My example here also uses field sets to make it easily configurable once it is in production.

To set up for this, I created a custom product object and a quote request object. On the custom product, I created a picklist field with options such as Group1, Group2, etc. On the quote request object, I created all the options as fields and then created field sets to group them together for display on the quote request entry page. The benefit of using the field set is that if I decide I need to add a new option to a product group, I can do it without having to add code. I just add the field to the relevant field set.

Here’s the Visualforce page. You can see the repeaters with the field sets.

<apex:page standardController="QuoteRequest__c" extensions="QuoteRequestExtension">
  <apex:form >
    <apex:pageBlock >
      <apex:pageMessages />
      <apex:pageBlockButtons >
        <apex:commandButton value="Save" action="{!save}"/>
        <apex:commandButton value="Cancel" action="{!Cancel}"/>
      </apex:pageBlockButtons>
      <apex:pageBlockSection title="Custom Product" collapsible="false">
        <apex:outputField value="{!QuoteRequest__c.CustomProduct__c}"/>
      </apex:pageBlockSection>
      <apex:pageBlockSection columns="1" title="Options" collapsible="false">
        <apex:repeat value="{!$ObjectType.QuoteRequest__c.FieldSets.Group1}" var="f" > 
            <apex:inputfield value="{!QuoteRequest__c[f]}" rendered="{!CustomProduct.ConfigGroup__c == 'Group1'}"/>
        </apex:repeat>
        <apex:repeat value="{!$ObjectType.QuoteRequest__c.FieldSets.Group2}" var="f" > 
            <apex:inputfield value="{!QuoteRequest__c[f]}" rendered="{!CustomProduct.ConfigGroup__c == 'Group2'}"/>
        </apex:repeat>
      </apex:pageBlockSection>
    </apex:pageBlock>
  </apex:form>
</apex:page>

And here’s the Apex controller extension. It takes the product ID passed as a parameter to the page and queries the configuration group on the custom product to display on the Visualforce page.

public with sharing class QuoteRequestExtension {

    public CustomProduct__c CustomProduct {get; set;}
    private final QuoteRequest__c qr;

    public QuoteRequestExtension(ApexPages.StandardController controller) {
        this.qr = (QuoteRequest__c)controller.getRecord();
        try {
            CustomProduct = [select id, ConfigGroup__c from CustomProduct__c where id = :ApexPages.currentPage().getParameters().get('product')];
        } catch(Exception e) {
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.FATAL, 'Error Getting Product Info'));
        }
        if (CustomProduct != null)
            this.qr.CustomProduct__c = CustomProduct.Id;
    }
}

The last ingredient to this is to add a lookup field on the Opportunity object. We can then add a custom button on to the Opportunity that takes the user to the quote request page and passes in the custom product ID from the opportunity.

My Next Adventure

I’m taking a break from my regular technical topics to share a bit of philosophy. I recently gave my notice at my current job and will be starting a new job at the end of May as a Salesforce.com Business Analyst at Zions Bancorporation. I’m really excited about this opportunity because it will give me the chance to focus on two things I love: solving business problems and implementing those problems in Salesforce. It is also going to give me the experience of working at a really large company.

I’ve worked at Petzl for 12.5 years (10 more years than I thought I would when I started). Although it is an amazing place to work, I felt that I had hit the ceiling with my professional growth there. I’ve always sought out challenges and have never been pleased with the status quo. In addition to my job description duties, I felt a commitment to try and make Petzl a better place to work: I organized blood drives, planned camping and climbing trips, and advocated for employee friendly policies. I could have just come in, done my job and gone home. But I feel that since we spend so many of our waking hours at work, we should really enjoy working there.

Another thing I’ve always believed is that work should be challenging and fulfilling. Here’s an excerpt from the email I sent to my coworkers:

There are all sorts of things each of you can do to grow professionally and build your career.  Don’t get too comfortable with your daily tasks. Never pass up anything interesting to you because it is different or unknown. Seek out those new challenges and let your manager know what floats your boat. You might be surprised what you can do. I want to thank Petzl for the opportunity they have given me to grow and develop as an IT professional. I’m excited to step out of my comfort zone and jump into this new adventure.

I’m looking forward to sharing the new things I learn in the coming months. Stay tuned!

Apex and Object References

I learned something today that I’m not sure how I made it this far without knowing. It was confusing the heck out of me, so I thought I’d share. When you assign one object to another through the use of the “=” operator, it doesn’t actually make a copy of the object. Instead, it makes a reference to the object. What does this mean? Let me try to explain with some code:

Account a = new Account(Name = 'Test');
Account b = a; //here we set a reference to a
b.Name = 'Changed'; //changing b also changes a
System.assertEquals('Changed', a.Name);

If you want to make a copy of the object, then you need to use the clone method to make a copy of the object:

Account a = new Account(Name = 'Test');
Account b = a.clone();
b.Name = 'Changed';
System.assertEquals('Test', a.Name);

Deleting Salesforce Contacts You Don’t Own (Part 2)

Back in October, I posted a way to allow users to delete contacts that they don’t own. The biggest problem I had with the method was that there was no way to test if the user requesting the delete should really be able to delete it (I wanted to say if the user had at lease edit permissions, then she could delete it). With the Spring release of Salesforce, there was a new object quietly exposed to Apex that makes this so easy. That object is the UserRecordAccess object. You can now query this object with SOQL to find out a user’s access to a specific record.

Let’s take a look. In its simplest form, you can query to see what the maximum access a user has to a particular object:

SELECT RecordId, MaxAccessLevel FROM UserRecordAccess WHERE UserId = [single ID] AND RecordId = [single ID]

Very elegant, right? Before we had access to this object, we’d have to do some major hackery by wrapping some dml in a try…catch statement to see what was possible.

So, how does this help us? We can now check to see if the record Id passed to the class can be edited by the user and if so, delete it because the class uses the without sharing keyword, so it ignores these permissions. The new and improved code!


global without sharing class ContactUtil {
    webService static String deleteContact(Id id) {
        Contact c = new Contact(id = id);
        try {
            UserRecordAccess ura = [select RecordId, HasEditAccess from UserRecordAccess where UserId = :UserInfo.getUserId() and RecordId = :id];
            if (ura.HasEditAccess)
                delete c;
            else
                throw new deleteException('Error deleting');
            return '';
        } catch (Exception e) {
            return e.getMessage();
        }
    }
}

Everything from the original post works exactly the same. As a bonus, here’s a link to an unmanaged package with the code (with test class!) and button: https://login.salesforce.com/packaging/installPackage.apexp?p0=04tU0000000URcs.

Salesforce Dashboard Filters in Action

A few releases ago, Salesforce introduced Dashboard Filters. This allowed users to use one dashboard and dynamically change it using a drop down filter. This is an awesome feature because it means you need to maintain less reports and dashboards to deliver desired functionality. I hadn’t moved my reports over to this functionality yet, but I’m now glad I waited until the Spring ’12 release. With this release, they’ve made dashboard filters even more powerful.

First let me try to explain my data model and why I had a zillion reports to accomplish my dashboard requirements. We have reps who might cover a set of territories. The rep owns all the accounts, and we want to be able to report by territory (which is a custom field on the account) on the dashboards. Before filtering was introduced, I couldn’t do dynamic dashboards in this case and just have the dashboard display as the running user. It wouldn’t show the breakout by territory. As a result, for each territory, I had to create a separate set of reports to be used on a dashboard for each territory. Each dashboard had 13 components on it, so let’s do some quick math: 20  territories with 13 components each = 260 reports to maintain!

Enter dashboard filtering. Now with dashboard filtering, I can have 13 reports that are used by the dashboards. I can also reduce the number of dashboards to the number of reps we have. If a rep covers 4 territories, now I just need a single dashboard for that rep. The dashboard then has a filter so when you first pull up the dashboard, you see a roll up of the all territories covered by the rep. You can then drill in using the filter to see individual territories. Now I could get greedy here and switch to one dashboard that is a dynamic one based on the logged-in user. I chose not to go this route because I find it easier for all users to understand and they can also take advantage of the Chatter Feed for the dashboard to discuss a specific rep’s performance without everyone seeing it.

Creating a Dashboard Filter

Dashboard Filters Example

The Spring ’12 release gave us many more features that I’m sure I’ll use soon:

  • You can now create three filters per dashboard.
  • You can now add filters to dynamic dashboards.
  • You can post snapshots of filtered dashboard components to Chatter.
  • You can now filter by the usual operators (starts with, contains, etc)

Some caveats regarding dashboard filters:

  • You can’t filter dashboards with s-controls or Visualforce components.
  • You can only have 10 options for each filter, but according to the release notes, you can increase it to 50 by contacting Salesforce.
  • You can’t filter on bucketed fields.
  • The iPad Dashboard App doesn’t support filters yet so users will only see the first view of data.

Take a good hard look at the new dashboard filters features. They could save you a lot of headaches!

Integrating Salesforce with Box

I’ve been working on a project to integrate Salesforce and Box. This has been an interesting project to work on since it involves a bunch of brand new concepts to me such as future and batch apex and callouts. The goal of this project is to create a robust library to interact with Box using Salesforce apex code. I’ve posted my code on Github (also a first for me). Please take a look at my Github project, fork, make enhancements and contribute back. The code is free to use and modify. I haven’t attached an open source license to it yet, but feel free to do what you want with it.

Here’s the link: https://github.com/dhoechst/ApexBox

I’ll continue refining the code and hope to post some walk throughs of some of the code and how to use it.

 

Posting Files to Box.com using Jitterbit

I’ve been working on a big project to launch Box (www.box.com) at my company. Box is a fantastic web based file sharing service. I won’t go into details here, but check them out if you need to share files securely with others. My next few blog posts will focus on some of the aspects of that project.

One of the requirements was to automatically load files to Box shared folders. These files needed to included data from our ERP. For example, one file needed to list inventory levels and needed to be updated on a frequent basis. I definitely didn’t want to do this manually, so I turned to some automation. Jitterbit (www.jitterbit.com)  is a tool we use for all sorts for data integration, and it came through in this project as well.

The first step was to create an “application” in Box. There are detailed instructions here: http://developers.box.net/w/page/12923956/ApiOverview. Once you have your API key, you are ready for the next step of getting an authentication token as outlined at http://developers.box.net/w/page/12923915/ApiAuthentication. It is pretty easy to just use REST API calls right from a browser to get the token:

  1. Get a ticket by entering the following into your web browser (replace <apikey> with the one you obtained above: https://www.box.net/api/1.0/rest?action=get_ticket&api_key=<apikey>
  2. Use the ticket retrieved to authorize your application to your account: https://www.box.net/api/1.0/auth/<ticket>
  3. Finally retrieve the auth token: https://www.box.net/api/1.0/rest?action=get_auth_token&api_key=<apikey>ticket=<ticket>

Now that you have your auth token, you are ready to setup an upload process in Jitterbit. I’m assuming you already have some experience with Jitterbit and transformations. If not, Jitterbit has some great examples in their application help and online. You’ll need an http target setup with the following parameters:

  • URL (replace <authtoken> with your actual auth token, leave [folderid] as is: https://upload.box.net/api/1.0/upload/<authtoken>/[folderid]

Next, create a script to set some variables. $folderid is the id of the folder you want to upload to. You can get this by browsing to the folder in Box and looking for the 9 digit number in the URL. The filename should be changed to what you want.

$folderid = ’123456789′;
$jitterbit.target.http.form_data = true;
$jitterbit.target.http.form_data.filename = “File Name.csv”;

Put this all together into an operation and run it. It should look something like Script->Source->Transformation->Target in the graphical editor. When it  runs, the transformation will be performed and the file added to the Box folder.

I’m a Salesforce MVP!

This week I received an email welcoming me to the Salesforce MVP program. This email came as quite a shock since I didn’t even know I had been nominated. I’m just a few days into the program, and I’m already really excited about it. I’ve had a lot of interaction with other MVPs on Twitter and I’m honored to join their ranks.

The Salesforce community is one of the reasons I love working with Salesforce so much. If I don’t know the answer to something, I can quickly jump on Twitter and use the #askforce hashtag, I can go over to Answers and post my question, I can go to the forums, or I can search the many blogs. All of these channels have fantastic contributors in them, and I can often find the answer in minutes. The only way this could work is having a dedicated group of people willing to volunteer their time. The MVP program is a great way to cultivate and encourage contributors and I think Salesforce has executed this program well.

Becoming an MVP has made me even more motivated to contribute to the community (of course, I’m sure that is the whole point of the program). I’m always looking for ideas for this blog. If you have a topic you’d like to see me cover, please comment here. Thanks to whoever nominated me. I think it is going to be a fun year!

Printing a Salesforce Record to a DYMO Printer

Sometimes you might need to print records in Salesforce onto label. You have a few options. You could print them in batches using a custom Visualforce page, or a merge tool such as CongaMerge or Drawloop. For my use case, though, I needed to be able to print one-off labels on demand. DYMO has a JavaScript library that can interact directly with DYMO printers installed on the computer. To use, it, you must first install the DYMO software onto your computer. After installing it, make sure to close your browser completely and then open it again – watch out for background processes – they caused me a lot of grief because I couldn’t get it to work until I found all the background Chrome processes running.

You can get the latest version of the DYMO JavaScript Library here: http://labelwriter.com/software/dls/sdk/js/DYMO.Label.Framework.latest.js. DYMO doesn’t recommend using that URL for production use. Instead, you should download it and upload it as a static resource in Salesforce.

I wanted to create a custom button on a Case that when clicked would print a label with the case number in a barcode format. Accessing static resources from custom JavaScript button is a little tricky, but I found this hack to get it to work: http://jevonearth.blogspot.com/2011/08/using-static-resources-with-salesforce.html.

The code for the JavaScript is fairly simple. It builds the label xml, then prints it to the first DYMO label printer that it finds. You can change what is printed by modifying the label xml. There are a lot of examples on the DYMO blog.


{!REQUIRESCRIPT('/resource/' & LEFT(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(TEXT(NOW()),':',''),'-',''),' ',''),10) & '000/DymoLib')}

try
{
// open label

var labelXml = '<?xml version="1.0" encoding="utf-8"?>' +
'<DieCutLabel Version="8.0" Units="twips">' +
'<PaperOrientation>Landscape</PaperOrientation>' +
'<Id>Address</Id>' +
'<PaperName>30252 Address</PaperName>' +
'<DrawCommands>' +
'<RoundRectangle X="0" Y="0" Width="1581" Height="5040" Rx="270" Ry="270" />' +
'</DrawCommands>' +
'<ObjectInfo>' +
'<BarcodeObject>' +
'<Name>Barcode</Name>' +
'<ForeColor Alpha="255" Red="0" Green="0" Blue="0" />' +
'<BackColor Alpha="0" Red="255" Green="255" Blue="255" />' +
'<LinkedObjectName></LinkedObjectName>' +
'<Rotation>Rotation0</Rotation>' +
'<IsMirrored>False</IsMirrored>' +
'<IsVariable>True</IsVariable>' +
'<Text>' + '{!Case.CaseNumber}' + '</Text>' +
'<Type>Code39</Type>' +
'<Size>Medium</Size>' +
'<TextPosition>Bottom</TextPosition>' +
'<TextFont Family="Arial" Size="7.3125" Bold="False" Italic="False" Underline="False" Strikeout="False" />' +
'<CheckSumFont Family="Arial" Size="7.3125" Bold="False" Italic="False" Underline="False" Strikeout="False" />' +
'<TextEmbedding>None</TextEmbedding>' +
'<ECLevel>0</ECLevel>' +
'<HorizontalAlignment>Center</HorizontalAlignment>' +
'<QuietZonesPadding Left="0" Top="0" Right="0" Bottom="0" />' +
'</BarcodeObject>' +
'<Bounds X="331" Y="345.600006103516" Width="4386.5" Height="720" />' +
'</ObjectInfo>' +
'</DieCutLabel>';
var label = dymo.label.framework.openLabelXml(labelXml);

// select printer to print on
// for simplicity sake just use the first LabelWriter printer
var printers = dymo.label.framework.getPrinters();
if (printers.length == 0)
throw "No DYMO printers are installed. Install DYMO printers.";

var printerName = "";
for (var i = 0; i < printers.length; ++i)
{
var printer = printers[i];
if (printer.printerType == "LabelWriterPrinter")
{
printerName = printer.name;
break;
}
}

if (printerName == "")
throw "No LabelWriter printers found. Install LabelWriter printer";

// finally print the label
label.print(printerName);
}
catch(e)
{
alert(e.message || e);
}

Basic JavaScript Debugging with Firebug

A lot of my posts have been JavaScript heavy and I get a lot of questions from readers trying to adapt the code. The most frequent problems are with errors in the JavaScript syntax. Unlike Apex and Visualforce markup, you can save pages with embedded JavaScript with syntax errors and have no save errors. It is then maddening when you try to view your fresh new page and nothing happens. The JavaScript fails silently in the background and you have no idea what happened. This is when I turn to one of the developer tools such as the one built into Chrome or my personal favorite, Firebug.

To use Firebug, you first need Firefox installed. If you don’t have it, go get it now: http://www.mozilla.org/en-US/firefox/new/. I’ll wait. OK, next you need to install Firebug: http://getfirebug.com/. There are a million sites you can use to learn about Firebug, so I’ll just go over the basics. In the upper right of Firefox, you should a little bug. If it is gray, then Firebug isn’t active for the page; just click on the bug to give it some color and make it active. A panel will probably open at the bottom of your browser. This is the console and where you can see all the messages.

Now let’s debug some code! Here is a little Visualforce page that will cause you some problems:

<apex:page >
<script src="/soap/ajax/19.0/connection.js" type="text/javascript" />

<script type = "text/javascript">
function tryConsole() {
console.log("Link clicked");
sforce.connection.sessionId = '{!$Api.Session_ID}';
var result = sforce.connection.query("Select id, name" +
"from Account limit 1");
var it = new sforce.QueryResultIterator(result);
while(itt.hasNext()) {
var record = it.next();
console.log(record);
}
 }
</script>
 <a href="javascript:tryConsole()">click here</a>
</apex:page>

Make a new Visualforce page and copy the code above into it. When you click the “click here” link, the tryConsole JavaScript function will run. Give it a try so you can look at the Firebug console. It should look something like this:

So what happened? First it logged a message to the console using the code console.log(“Link clicked”). This is more preferable to doing an alert with a message in it because it doesn’t interrupt the flow of the page. Then we see the big whammy. There was something wrong with my SOQL query. There was a malformed query because I’m concatenating two strings together and forgot to leave a space between the two strings. I can see the error message right in the console and make some changes to the code to fix it. To fix this problem, just add a space between name and the closing double quote. Make the change, save the page and try clicking on the link again.

Oops, we still have an error:

This time, it looks like a typo in the code. The variable itt isn’t defined. Notice that there is a link in the console that takes you directly to the offending line of JavaScript. We can see that the variable is actually defined as it, so let’s change the code to say “while(it.hasNext())” and save the page again. Third time’s the charm – let’s try clicking the link one more time and taking a look at the console:

Ah, much better. Now we can see that the console is showing us the record that was selected by the SOQL. This is because of the line of code “console.log(record)”. It allows us to inspect the values and see if they are what we expect and could help us with other trouble shooting.

This is by no means an exhaustive tutorial on debugging JavaScript, but I hope it gets you on your way to finding errors and squashing bugs!

Follow

Get every new post delivered to your Inbox.