Category: Salesforce.com

Click on theSourceCodetab above to view the full sourcecode for the application or to install a complete working version into your environment.

In the last blog post I went into some of the details behind the Mass Contact Transfer VisualForce page. In this post, I’ll delve into the page Controller and supporting classes that contain the business logic for the page.

The transferContacts class is the controller for the VisualForce page. The controller has four main functions:

Provide the get/set methods for fields used on the VisualForce page

Create picklists of users for the To and From user fields

A doSearch() method that looks for records based on the criteria entered

A doTransfer() method to transfer the selected contacts to the new users

Get/Set methods

Generally these are very simple methods to either set the value of a public variable or return that value. Get/Set methods are always preceded by the word get or set followed by the name of the variable. On the VisualForce page these are referenced as {!fieldname}. For example, using value=”!fromUserID}” on a VisualForce page will call the getfromUserID() method to retrieve the value and setfromUserId() to set the value when the form is submitted.

The From and To User picklist fields

Rather than require users to use a search lookup for users on the transfer form, I implemented this using two picklists for the From and To users. The FromUsers list includes all Users while the ToUsers lists only includes Active users. The setup behind the two picklists fields is very straight forward.

VisualForce page code for a picklist. The codeblock below accesses the getFromUsers() method to fill the values in the tag.

Apex Controller code to return the picklist of values. The getFromUsers() method returns a List object of Users sorted by Name. The SelectOption list type accepts two values – an ID and a display name. When a FromUser name is selected from the list, the Apex Controller will be able to access the ID of that user by referencing the toUserID variable. This is set by the page to the ID of the selected when the form is submitted.

Searching for Contacts

The doSearch() method is called when the [Find] button is clicked on the page.

<apex:commandButton title="Find" value="Find" action="{!doSearch}"/>

The overall goal of the method is to build an SOQL string, run the Query, and display a list of contacts on the page. As discussed in Part 1, the searchCriteria() class contains the methods necessary to generate the actual WHERE clause parts to build the query string. The logic behind this is shown below.

To start, a base SOQL string is built selecting all the fields in the Contact object along with key fields from the Account, Owner, Account.Owner, CreatedBy.Owner, and LastModifiedBy.Owner relationships. The initial contents of the WHERE exclude Contacts where the current owner is the owner to transfer contacts to (in other words, they own the contacts already).

If a To Owner was selected, add to the WHERE clause to restrict the list of contacts owned by the To User ID.

// If a From User was selected, add this to the criteria
if (fromUserID <> null) cSOQL += ' AND OwnerID = \'' + fromUserID + '\' ';

For each line of Criteria, call the BuildWhereClause() method in the searchCriteria class.

// For each criteria line item, call the method to build the where clause component
for (searchCriteria cl : criteriaLine) {
cSOQL += cl.buildWhereClause(DebugMode);
}

Finally, order the list by Account Name and then Contact Name and limit the list to the first 250 contacts.

// Sort the results and limit to the first 250 rows
cSOQL += ' ORDER BY Account.Name, Name LIMIT 250' ;

searchCriteria class

The BuildWhereClause() to build the WHERE clause component for each of the search criteria lines is where it got complex. This method had to generate code to handle different field types (Text, PickList, Boolean, Number, Date, DateTime, etc.), different operators (=, >, <, contains, IN, etc.) and different types of values. Most of the logic is fairly straight forward, checking the field type and the operator and generating errors where the two are not compatible (BOOLEAN and ‘Less Than’, for example).

The complex part of the logic is to handle Date and DateTime field types. The most recent version of the application now supports the date format of the current user when parsing the value entered. Where this became tedious was for DateTime types. For example, you might expect the following SOQL WHERE block to only retrieve Contacts created on April 1, 2009: WHERE CreatedDate = 2009-04-01. However, CreatedDate is a DateTime field and requires a timestamp, not just a date. In order to properly query Contacts created on 4/1/09 the actual WHERE clause needs to looks like: WHERE (CreatedDate >= 2009-04-01T00:00:00Z AND CreatedDate <= 2009-04-01T23:59:59Z)

Transferring the Contacts

The final step in the process is to transfer the selected contacts to the ‘To User’. The toTransfer() method starts by building a list of Contact ID’s for selected Contacts. Part 1 of this blog post goes into the checkbox on each Contact. Only checked contacts are transferred. The list of ID’s is passed to a Query to select the Contacts. Finally, the OwnerID for each Contact is changed and the Database.Update() method is called to do the actual transfer.

At my previous employer one of my regularly tedious tasks was to mass re-assign contacts based on various conditions. Many times it was as simple as a group of Accounts were transferred to new Account Managers and the default behavior of SalesForce.com is to only transfer contacts owned by the Account owner. The result would be a group of Contacts needed to be transferred manually. I handled these requests using an exported report of Contacts with Contact ID with Ron Hess’s Excel Connector to update the OwnerID column for the contacts. Clearly this required a better way – and VisualForce provided the platform. Creating the Mass Contact Transfer VisualForce page was for the most part fairly straight forward. It’s essentially a two part page:

Search Criteria

Search Results

Building the Search Criteria Portion

Retrieving the From and To User ID’s.

To simplify the process of selecting the From and To users, this was constructed using two picklist fields instead of two text fields with a lookup. The “From” picklist includes all Users – active or in-active; the “To” picklist only lists active users.

The tough part was getting the red required bar next to the “To” user field. You would expect that setting the Required attribute on the apex:selectList tag would tell SalesForce.com to use the standard red bar for the field, but that is not the case. Instead you have to wrap the field with an apex:outputPanel tag and have another apex:outputPanel tag within the first that closes itself. This code is highlighed above in red. The syntax should work for any field that you want to show the SalesForce.com standard red bar to indicate a required field.

Search Criteria Section

Next was to build the Selection Criteria section of the page. Initially it was very simple. I wanted five lines for selection criteria to match the standard Transfer pages. The first draft of the page had five repeated blocks of code in the VisualForce page for the Selection Criteria (field, operator, value). Clearly this was not the most efficient way to write the page, but it gave me a feel for the look and what was needed. The more efficient way is to use a separate class for Selection Criteria. The Page Controller constructor method creates a List with 5 instances of the searchCriteria class and the VisualForce page can use an apex:DataTable tag to display however many rows are in that list. The resulting page looks like this:

searchValue: A simple inputText field to capture the search criteria value.

When the user clicks the [Find] button on the page, the Page Controller Class is able to loop through the criteria lines and process each one individually. Since the Search Criteria section is written to be independent of the “Mass Contact Transfer” page, there is a method in the searchCriteria class to build the Where clause portion. The doSearch() method in the Page Controller Class appends each of the Where clause parts together to create a single SOQL statement to query the contacts based on the From UserID, To UserID, and user defined criteria. The Query is run and the results are appended to a searchResults List for display.

Search Results

At the bottom of the page there is a section that starts as hidden using the style attribute on the apex:outputPanel tag. {!ShowBlockIfResults} returns either “display: block;” to show the block (only if there are search results) or “display: none;” to hide the block. In the list of Contacts, a checkbox in the first column is used to allow the user to select which contacts should be transferred. This is checked by default. To simulate how the standard SalesForce Transfer pages work, the user can check or uncheck all Contacts by clicking the checkbox in the column header. This is accomplished using some simple JavaScript code linked to onClick event on the checkbox in the column header. The styleClass, rowClasses, onrowmouseout, and onrowmouseover attributes of the apex:DataTable tag are used to format the results table so it looks like the standard SalesForce Transfer pages.

Finally, when the user clicks the [Transfer Selected] button, the doTransfer() method on the Page Controller loops through the searchResults list to build a list of Contacts whose OwnerID should be changed. Any errors in the Database.Update call are displayed in the messages section at the top of the page. The last step is to re-run the doSearch() method to query any remaining contacts not transferred the first time (if they were not checked, there was an error, or the original query returned more than 250 rows).

Next week, Part 2 of this blog post will go into the Apex code in the Page Controller and the two supporting classes. In the mean time you are welcome to download the source code or install the AppExchange package by clicking on the SourceCode tab at the top of the page.

Note: The code for all triggers, classes, and pages can be downloaded as an archived Eclipse IDE Project from here, or you can install this as a package into your Development or Sandbox environment from here.

The Business Issues:

As an administrator, there are times when I wanted to monitor records deleted, especially for certain users who had a habit of inadvertently or even purposely deleting records.

The Recycle bin does not list Opportunities and Contacts deleted when their parent account is deleted. There were times when I was contacted to search for the “missing” Contact or Opportunity, but it was nearly impossible to locate because the Account associated with the record was deleted and the Contact/Opportunity did not show up in the Recycle bin.

The Solution:

Create Apex Triggers on key objects in SalesForce.com to log to a custom object when records were Deleted and Undeleted. To log deletes on your custom objects, simply use the IDE to create new Trigger on your custom object and then copy the trigger code from one of the existing standard object triggers. You’ll also need to modify the DeleteLog_Test class to add test coverage for your new trigger. The test code can likely just be copied for an existing standard object and then modified with your object name.

Build logic into the code to also log deleted child records, especially for Contacts and Opportunities. It is fairly straight forward to extend this logic to log other child records such as Contracts or Assets by modifying the query in the commitInsert() method in the DeleteLog class.

Technical Issues:

Avoid hitting the maximum number of SOQL and Script Execution Statement limits in bulk deletes, such as when using the “Mass Delete Accounts” function. After some experimenting, the key to avoiding this issue involved re-writing the processing of Notes/Attachments/Tasks for records being deleted so that it first read in all of the items in a single query and then processed them in a more efficient loop using Map[]’s instead of Lists to avoid using a For loop to process the QueryResults in List[] form.

Optionally avoid logging of huge mass deletes by the administrator or by integrated applications. In my past role as an Administrator there were occasions where I would mass-delete a bunch of records, almost always to correct an import or Outlook Add-in sync problem with a user. We also had a couple of integrated applications that would upload and sometimes mass-delete hundreds of records on a daily basis. To avoid logging all of these in the DeleteLog table, there is a custom field added to the User object to allow the logging functionality to be disabled for a specific userID. If you install the package or use the archived IDE project for this, be sure to manually add the “Do Not Log Deleted Records” field to the User Page Layout.

It starts by creating a new instance of the DeleteLog class and then passes in the records from the Trigger.old[] collection to the LogDeletedRecord() or LogUnDeletedRecord() method depending on the type of action taken by the user. Finally, it calls the appropriate Commit() method to write the new records to the custom DeleteLog__c object.

The DeleteLog class has four main methods:

logDeletedRecord()

logUnDeletedRecord()

commitInsert()

commitRecoveredUpdates()

The logDeletedRecord() method essentially adds a new item to a class list of type DeleteLog__c. It also adds the ID of the object being deleted to an oID list. This will be used later to query all child records for the records being deleted. One key part of the method is that it forces a commit() at 150 records. The maximum number of records that can be inserted at one time is 200.

The commitInsert() method is where the SOQL logic had to be more creative to avoid hitting the governor limits for bulk operations. commitInsert() has three main steps:

Count the number of Activities, Notes, and Attachments associated with each record being deleted.

Write the DeleteLog records to the DeleteLog__c object

If the object(s) being deleted are Accounts, find Contacts and Opportunities associated with the Accounts and add them to the DeleteLog__c object as well.

Finally, I added VisualForce page associated with viewing the DeleteLog__c object with an [Undelete] button. Though it is true that the button could have also been done using a JavaScript button on the standard page layout. What is nice about looking at a Deleted record, such as a Deleted Account, from this page is that we can see the Account along with the deleted child records listed.

For most medium to large SalesForce.com implementations, the following two statements are probably true:

One of the favorites of most SalesForce.com users is the ease of creating new custom reports.

One of the headaches of most administrators is dealing with the hundreds or thousands of reports that get created, saved, and then are never used again.

Before the Force.com IDE, the only way I found to deal with unused reports was to manually move then, one at a time by using SAVE, to an “Unused Reports” folder. This is by no means a short task. Moving 50 reports can take hours. There is an Idea on the Idea board for a “Mass Move” feature.

In the mean time, the Force.com IDE can be used to mass archive and delete old reports. Note that if you plan on keeping the archived reports around you must mass-delete them in a different Force.com IDE project so that you are able to keep your archived copies.

What the IDE cannot do for reports:

Sync non-working reports down to the IDE. For example, if a report no longer runs because the object or fields in the report no longer exist it’s not possible to archive the report via the IDE. At that point you can either fix the report in the UI or just delete it.

Move reports between Folders. This would have been a great feature!

Copy reports to another Folder.

The how-to steps

Archiving Reports via the IDE:

Create a New Force.com Project in the Eclipse IDE (FILE->New->Force.com Project)

Name the new Project, for example “Archived Reports” and click [Next]

Click on “Selected Meta Data Components” and click [Choose]

Scroll down the Reports section, expand it and select, one at a time, the reports you wish to Archive. Click [OK]

Click [Finish]

The result of the above is a “Project” in Eclipse with backed up copies of the report meta data. This is all you’ll need to recreate the reports in SFC if they’re ever needed again.

Deleting the Archived Reports via the IDE. You can also delete the reports using the SalesForce.com UI, but that can be time consuming if you have a lot of reports and they’re in different folders. This method works well to delete multiple reports at one time, though you cannot delete the folders the reports are in using this method.

In the “Package Explorer” area, right-click on the “Archived Reports” project you created above and select COPY

Right-click in the white space in the “Package Explorer” area and select PASTE.

When prompted name the project. For example, enter “Deleted Reports” and click [OK]

Right-click on the new Project name and select Properties

Update the Username and Password for the project if they did not carry over from the original project

Expand the Project Explorer area down to SRC->REPORTS.

Expand all of the Report folder

Use Shift-Click on Control-Click to select ALL of the reports to be deleted. Do not select the report folders. They can only be deleted through the web UI (unless you empty your Recycle bin first).

Press the [Del] key, or click on EDIT->DELETE

Click [Yes] when prompted to confirm the delete

Click [Yes] when prompted to confirm deleting from the server.

At this point the “Deleted Reports” project is no longer required and can be deleted.

Restoring an archived report back into SalesForce.com from the IDE is easy if the folder that the report was in still exists.

Open the “Archived Reports” project created earlier

Expand the project to the SRC->REPORTS section and find the report to restore.

Right-click on the report and select FORCE.COM->SAVE TO SERVER

Click [Yes] when prompted to save the report to the server.

If the folder does not exist in SalesForce.com you will need to copy the report metadata into a new file and upload it separately.

Create a new Report Folder using the SalesForce.com Web UI. Do not give it the same name as the deleted folder.

In the Force.com IDE, right-click on the Project name and select FORCE.COM->ADD/REMOVE META-DATA COMPONENTS.

Click [Add/Remove]

Refresh the metadata if required

Expand the Reports section and check the new folder.

Click [OK]

You will be prompted to refresh data for the reports/folders in your existing project, even if they do not exist in SalesForce.com any longer. Click [Yes] for these.

Double-click on the report file (ex: report_name.report) in the Package Explorer to view the contents of the file.

Select the entire contents of the file (Control-A) and copy into the clipboard (Control-C)

Right-click on the new folder that you created in SalesForce.com (see step 5) and select NEW->FILE

Type in a filename for the report. Note that the filename cannot have any spaces and must have end in .report (example: report_name.report). The filename does not have to match the report name.

Paste the contents of the report and save the file. Ignore the “Premature end of file” error.

Right-click on the new .report file and select FORCE.COM->SAVE TO SERVER

Click [Yes] when prompted to save the report to the server.

Yes, there are lots of steps here and hopefully SalesForce.com will give us administrators better tools to manage outdated reports. In the mean time, the Force.com IDE can be used to clean up the massive list of reports that may be in your SalesForce.com instance.

Sorry for the long delay between blog posts. Between project work, vacation and being sick I just haven’t had the time to dedicate to writing a new post. But that changes now.

Since my last post I’ve starting using Twitter. Click the Twitter link to the right to see my Twitter page and who I’m following. Some of you may say why or even huh? For those who haven’t heard of or used Twitter – Twitter is basically a social networking experiment gone haywire. When you use Twitter, you post (aka “tweet”) short blurbs about what you are doing, thinking, watching, working on, or might have a question about. The blurbs of text can only be 140 characters long so write concisely. Other Twitter users that are “following” you will probably respond to your question, that is if you have followers.

Yea, OK. So what’s the point? Who cares what I’m making for dinner?

The real value to Twitter is it’s immediateness. It’s like IRC on steroids. I could post a question to the SalesForce.com developer boards and will likely get an answer back in the next day or so. However, using Twitter is a great alternative for a quick question or even just an FYI to other SalesForce.com administrators/developers. More than likely I’ll get a response within a few minutes especially if I tag it correctly.

Hash tags (#) are a way of tagging Tweets with a specific topic name for easy searching. Any tweet that has #SalesForce in it can be easily tracked by searching for this tag (http://search.twitter.com/search?q=%23salesforce). If you had a question about something SalesForce.com related, you could pose it to the Twitter community by adding the #SalesForce tag to your tweet. Anyone else monitoring #SalesForce related tweets would see that and hopefully respond. Be sure to check out HashTags.org as well. This site tracks all of the realtime tags in use by Twitter users.

Another key to successfully using Twitter is to Follow other Twitter users that you want to follow you. Generally you will find that if you Follow another Twitter user they will follow you back. So, start by searching for #SaleForce.com or SalesForce in Twitter to see who else is tweeting about SalesForce (or any other topic you’re interested in) and then Follow them.

David Pogue’s video on Twitter (below) is a cute way of explaining the Twitter, though keep in mind that David has thousands of followers (people who want to read every single thing he “tweets”). We’re not going to get that lucky, but by using hash tags there is a way to target our tweets and questions to the right audience. Give it a try.

What I did find unusual, is that twitter.com/php was suspended. Up until recently, if you sent a tweet with @PHP and the name of a PHP Function it would return back to you the definition. I just went over to twitter.com/php and saw the standard “Account Suspended due to Strange Activity” message. That’s a real shame.

As SalesForce prepares the Spring ’09 release, I wanted to touch on what I think is an area they need to focus on – Currency Management.

In my opinion one of the biggest missing components of SalesForce.com has been around Currency Management, specifically around viewing amounts in different currencies using the correct exchange rate. Though SalesForce.com supports multi-currency and even added dated exchange rates in the Summer ’07, there are some severe limitations around this feature that can make it difficult for organizations to utilize Salesforce.com for more financial-type reporting.

From the SalesForce.com help on the advanced currency features:

Advanced Currency Management Considerations

Dated exchange rates are used for opportunities, opportunity products, opportunity product schedules, campaign opportunity fields, and reports related to these objects and fields. Dated exchange rates are not used in forecasting, currency fields in other objects, or currency fields in other types of reports.

If you enable advanced currency management, you cannot create roll-up summary fields that calculate currency on the opportunity object. All existing currency-related roll-up summary fields on the opportunity object will be disabled and their values will no longer be calculated. If your organization enables advanced currency management, you should delete any currency roll-up summary fields using opportunities and accounts or opportunities and custom objects.

Campaign opportunity fields use dated exchange rates when calculating the amount in the campaign currency, but are not used when converting those amounts to the user currency.

Dated exchange rates only used on limited objects; specifically no custom objects.

Roll-ups on the Opportunity currency fields no longer work.

If a company wanted to leverage the power of the SalesForce.com platform to use for a system that required more accurate currency reporting, such as a Project Accounting system, these are major limitations. Luckily, SalesForce.com does a great job listening to their customers.Searching through the posted ideas, I found some roughly similar thoughts, but nothing that specifically addressed dated exchange rates on custom objects. My long term preference for this is for SalesForce.com to add a Formula Function to convert any value to any currency using an effective date: ConvertCurrency(Amount, From_ISOCode, To_ISOCode, Effective_Date).I posted this idea on the Force.com Ideas board. If you agree with my recommendation, please review and promote my idea.

While we wait for SalesForce.com to address these issues in their base functionality, there is a way to work around this using Apex triggers. The general idea is to create converted currency field(s) on the necessary objects and use the trigger to update the values on these. I’ll avoid using code to keep the post short, but tThe functionality would work as follows. The trigger code can be found at the end of this post:

Create an Amount_XXX(where Amount is a field name and XXX is the currency) field on objects where you need to be able to roll-up or display amounts in XXX currency using the dated exchange rate. For example, Amount_USD would contain the Amount field converted USD.

Create an Insert/Update Trigger on the object to set this field value based on a specific date field on the object. The trigger would lookup the exchange rate based on a specific date field on the object, do the conversion and then update the Amount_XXX field

The key downside to this methodology is that you’d need to have one custom field (and field-specific trigger code) per field and currency that requires conversion. However, if you really must be able to roll-up, display, and/or report on a custom object using dated exchange rates this is a viable work-around.

Fixing the inherent problems with multi-currency would enable vendors to develop powerful financials related applications using the Force.com API. I’m not necessarily suggesting that the full financial software (AR, AP, GL) be moved to the SaaS model. That has its own set of potential problems – especially around auditing and security. What I am noting is that there are other uses for strong financial capabilities outside of the standard AR/AP/GL model. Even if you wanted to do something simple like generate Invoices from sold SalesForce.com Opportunities, not being able to track the exact amount invoiced in the correct currency is a big concern.

trigger trg_Invoice on Custom_Invoice__c (before insert, before update) {
// ----------------------------------------------------------------------
// 26 Jan 2009: M.Smith
// http://www.sfcnmore.com/
//
// Update a custom currency converted field using dated exchange rages
// This works around limitations with dated exchange rates on custom objects
// ----------------------------------------------------------------------
/* ......................................................................
* The following variables should be filled in for the code to work properly
* The Trigger is designed to operate generically based on the values set below
* Note: The nDecimalPlaces variable must be adjusted for the object & field as well
* ....................................................................... */
String ToCurrencyCode = 'USD';
String FIELD_CURRENCYCODE = 'CurrencyIsoCode';
String FIELD_AMOUNT = 'Invoice_Amount__c';
String FIELD_DATE = 'Invoice_Date__c';
String FIELD_CONVERTED_AMOUNT = 'Invoice_Amount_USD__c';
// --- The following two need to be set to the actual object (table) for trigger
// Have to adjust the Object and Fieldname below in order to get the decimal places.
Integer nDecimalPlaces =
Custom_Invoice__c.Invoice_Amount_USD__c.getDescribe().getScale();
* ....................................................................... */
try {
// A list of the Currency codes to retrieve from the DatedConversionRates table
List<String> CurrCodes = new List<String>();
// A list of item numbers to process from the Trigger.New[] collection
List<Integer> TriggerItem = new List<Integer>();
// Trigger.new is a list of the 'Objects' that were updated
// This loop iterates over the list, and adds any that have a
// change in the 3 key fields
for (Integer i = 0; i < Trigger.new.size(); i++) {
// Check to see if any of the following fields have changed:
// - AMOUNT field
// - DATE field (the date used for currency conversion)
// If either has changed (of if this is a new record), remember
// this record ID
if (Trigger.IsInsert) {
// Build a list of objects to be processed
TriggerItem.add (i);
CurrCodes.add( (string)Trigger.new[i].Get(FIELD_CURRENCYCODE));
} else {
if ((Trigger.old[i].Get(FIELD_AMOUNT) != Trigger.new[i].Get(FIELD_AMOUNT))
|| ( Trigger.old[i].Get(FIELD_DATE) != Trigger.new[i].Get(FIELD_DATE))
|| ( Trigger.old[i].Get(FIELD_CURRENCYCODE) != Trigger.new[i].Get(FIELD_CURRENCYCODE)) )
{
// Build a list of objects to be processed
TriggerItem.add (i);
CurrCodes.add( (string)Trigger.new[i].Get(FIELD_CURRENCYCODE));
}
}
}
// To allow this to work for BULK operations (> 20 records), query all of
// the conversion rates for the currencies that are used in the records being
// processed. This acts as a Cache to avoid querying the table for each record
// being inserted/updated. Loop through these in memory each time to find the
// correct rate to use
List<DatedConversionRate> = [SELECT ISOCode, ConversionRate,
StartDate, NextStartDate FROM DatedConversionRate
WHERE ISOCode in :CurrCodes];
// For each record being inserted/updated
for ( integer i : TriggerItem)
{
// Retrieve the proper DatedConversionRate based on the Date fields value
// field. Change this field name to adjust to the appropriate Custom object.
date dDateFieldValue = (date)Trigger.new[i].Get(FIELD_DATE);
double nRate = -1;
for (DatedConversionRate exchRate : ExchRateCache) {
// Looping through the cached DatedConversionRate object
if (exchRate.ISOCode == Trigger.new[i].Get(FIELD_CURRENCYCODE)
&& exchRate.StartDate <= dDateFieldValue
&& exchRate.NextStartDate > dDateFieldValue) {
// Look for a match for CURRENCYCODE and within the Date Ranges
nRate = exchRate.ConversionRate;
break;
}
}
System.assertNotEquals(nRate, -1, 'A Rate was not found for the ' +
dDateFieldValue + ' & ' + Trigger.new[i].Get(FIELD_CURRENCYCODE));
/* System.Debug('Exchange Rate = ' + nRate); */
// Convert the AMOUNT field to the Target Currency
// Use .setScale() to set the decimal places accordingly
Decimal nOriginalAmt =
((Decimal)Trigger.new[i].Get(FIELD_AMOUNT)).setScale(nDecimalPlaces);
Decimal nAmount = (nOriginalAmt / nRate).setScale(nDecimalPlaces);
// Update the field (BEFORE INSERT/UPDATE ONLY)
Trigger.new[i].put(FIELD_CONVERTED_AMOUNT, nAmount);
}
} catch (Exception e) {
// Log any errors in the debug logs
System.Debug('Error Detected...Logic Skipped: ' + e.getMessage()
+ '/' + e.getCause());
}
}

I sat through the Force.com Spring ’09 Release Preview Webinar yesterday. As usual, it looks like SalesForce has expanded the capability of their platform and improved some of their core administrative functions as well.

At the Force.com/Administrative level, some of the new features that really caught my attention are:

Enhanced Page Layout Editor. I have to admit, I never liked the old page layout editor. I always found it cumbersome, and it never worked right for me in Google Chrome (which is my preferred SalesForce.com these days browser because of its JavaScript performance). The new editor and the ability to add blank spaces to the page layout should certainly help both the administrator and the end users.

Analytic Improvements.

The recently added Analytic Snapshots feature in SalesForce.com was a big plus for administrators; however it had some missing features. The newly added ability to create Snapshots using Summary reports is one of them. The problem with Tabular reports was that it had too much data. You had to filter down the datasets to under 2000 records to get the snapshot to work properly. Now, we’ll be able to create Snapshots on summarized data, for example Pipeline History by Sales Manager that could avoid the 2000 record limitation.

The second big enhancement is the two new Grouping functions for reports: ParentGroupVal() and PrevGroupVal(). These will turn out to be powerful tools for creating more meaningful reports and dashboards.

GetUpdated() and GetDeleted() API Methods on more objects. It’s great to see that SFC has added these two functions to more of their standard objects to further improve data-replication services for those who are using them.

Updates to the OfficeToolkit COM API. I’ve used the COM API heavily in some of the integrations developed at my previous company, particularly those that could not be done in VB.NET. The new properties and methods added to the key objects will make it easier to use the Toolkit with some of the newer features of SFC. If you are using the OfficeToolkit COM API currently, I recommend reviewing the changes to determine if an update to your code is warranted.

Cross-Object Workflow for Custom Objects. Workflow rules can now perform Cross-Object Field Updates on related objects. Could be helpful for updating a related Parent record when the Child is updated. It would be nice if it could do the reverse – update all related Child record when a Parent is updated.

Text() function.Finally!!! The Text() function allows you to retrieve the contents of a Picklist field.This is so much better than using a long nested set of IsPickVal() functions.

Some of the exciting new end-user features are:

Add/Remove Campaign Members or Status. Marketing users can now add or remove Campaign members individually on the Campaign screen. Be sure to add the “Campaign Members” related list to the Campaign page-layout.

Finding Similar Opportunities. Administrators can add a related list to the Opportunity screen that lists other Opportunities that share similar attributes to the Opportunity currently displayed.

Full Text Search on Documents. Seems to support most common document types, except ZIP files. Note that the release notes do not state that existing documents will be indexed.

Dashboard Finder. Users can now enter part of the name of a Dashboard in the Dashboard Picklist field to quickly find the dashboard they want. When an organization has dozens of dashboards, this will be a huge timesaver.

Firefox 3.0 Support? I’ve been using Firefox with SalesForce.com for years. They did not specify what specific areas were made to be Firefox 3.0 compatible, but I am curious.

I haven’t signed up for the pre-release trial, but I will be looking at the new features as they’re released into production.

I just noticed that SalesForce.com released an update to their Winter ’09 IDE. If you are an SFC developer and you’re not yet using the Eclipse IDE with the Force.com Perspective, I highly recommend it. The IDE is a must if you’re doing anything with Apex Classes or Triggers. Sure, you can create and edit them online directly in SFC and even deploy by hand using ANT, but why? It’s so much easier to do it all through Eclipse.

I had really just starting using Eclipse heavily before leaving KT. I wrote and deployed my first Trigger using it. Speaking of triggers, I remember complaining about SFC’s lack of “business logic” capability years ago. They really have come along way since I first started working them back in 2001. Between Triggers, Workflow Actions, S-Controls, and outbound messaging it is possible to embed almost any business logic you want into SFC. I digress.

Another really great use for the Force.com IDE (Eclipse) is to backup or copy customizations between instances. For example, you can build a series of custom objects, tabs, etc. in your sandbox or development edition and then deploy to Production very easiliy via the IDE. While still at KT I took advantage of the IDE to backup 100+ unused Reports and then delete them from SFC. This way if someone came to me later and said “where is my report” I could still recover it from the IDE Project File.