Sophisticated DateTime “Formula Fields” with Apex and Field-Level Security

What do you do when you want to calculate a formula-like field but a regular formula won't work?

Salesforce CRM's formulas handle dates very well. If you want to enter a date value and have formula fields display, for instance, mydate__c + 21 days, that's simple. Just use mydate__c + 21.

Side note: If you try going the long way around and use DATE( YEAR( mydate__c ), MONTH( mydate__c ), DAY( mydate__c ) + 21 ) and mydate__c = 09/17/2010, Salesforce returns #Error! because there's no date 09/38/2010. Similarly, adding three months to a date like 1/31/2010 will also give an error. More about this in a future post.

DateTime fields are like Date fields, but they include... wait for it... a time component (and can be created in the running user's local time zone or in GMT).

Here's a use-case for a DateTime formula field:

A photography studio schedules photo shoots, and different packages include different durations. Similarly, we could use a hair salon which offers different services, each with a different duration, a dentist... you get the idea.

Requirements:

  1. Enter a DateTime for an appointment start time (starttime__c)
  2. Enter a duration (though in a production system, I'd include a value on the Product2 sObject, we'll just enter a value here) (minutes__c)
  3. Display a read-only DateTime field with the end time (endtime__c)
  4. The end time must be read-only to all users, like any formula field

Here's what won't work:

  • A formula field won't work because there are no MINUTE(), HOUR(), SECOND() formula functions
  • Workflow won't work because it depends on formulas to fill new values for date/datetime fields

That leaves Apex. First, the configuration:

  1. Create DateTime field starttime__c
  2. Create DateTime field endtime__c
  3. Set endtime__c field-level security to Read-Only for all profiles
  4. Create Number (18,0) field minutes__c
  5. Create a trigger on the sobject

Here's the trigger:

trigger timeTrigger on TestObject__c (before insert, before update) {
    for (TestObject__c t : Trigger.New){
    	if(t.StartTime__c != null && t.minutes__c != null){
        datetime myDateT = t.StartTime__c;
        double d = t.minutes__c;
        Integer shootmins = d.intValue();
        if(mydateT != null && shootmins != null)
        	t.EndTime__c = myDateT.addminutes(integer.valueof(shootmins));
       	}
    }
} 

Regular readers will note that I do usually split triggers into a trigger and a class, but I've not done so here purely for the sake of brevity.

Here's the test code:

public without sharing class shootTimesTriggerTest {

    private static testMethod void ShootCalculateEndTime_PositiveTestCases() {
        TestObject__c to;
        TestObject__c l;    
        test.starttest();
        l = new TestObject__c (name = 'test');
        datetime myDateTime = datetime.newInstance(2008, 12, 1, 12, 30, 2);
        l.StartTime__c = myDateTime;
        l.minutes__c = 90;
        insert l;
        to = [SELECT id, EndTime__c FROM TestObject__c WHERE id = :l.id];
        datetime newDateTime = datetime.newInstance(2008, 12, 1, 14, 0, 2);
        system.assertequals(to.EndTime__c, newDateTime);
        l.minutes__c = 45;
        update l;        
        to = [SELECT id, EndTime__c FROM TestObject__c WHERE id = :l.id];
        newDateTime = datetime.newInstance(2008, 12, 1, 13, 15, 2);
        system.assertequals(to.EndTime__c, newDateTime);
        test.stoptest();
    }

    private static testMethod void OppCalculateEndTime_NegativeTestCases() {
        test.starttest();
        TestObject__c l = new TestObject__c (name = 'test');
        l.minutes__c = null;
        insert l;
        system.assertequals(l.EndTime__c, null);
        test.stoptest();
    } 
}

A few points about how this works:

  • Triggers run in System mode, so they don't respect field-level security. Thus, we can set a field to read-only for all profiles, and the EndTime__c field will still be updated.
  • The test code runs in System mode as well, avoiding any potential problems if the field were set to invisible to a profile and we used System.RunAs() to test for various profiles.
  • Although I'm not a fan of using SOQL queries this often, I used these in the interest of saving time. Keep in mind that if you had quite a few queries in your regular code, adding these two might put you over the limit, so use queries sparingly!
  • This is the only way I know of to add minutes to a DateTime.

Did I miss anything? Please let me know in the comments.

Computer Associates Plans To Release Agile Development Tool On Force.com Platform

November 19, 2009 · Filed Under Development, Partners · 1 Comment 

Today, at Dreamforce 2009, Computer Associates plans to release CA Agile Planner(http://www.ca.com/agile), a tool to manage agile development teams.
CA Agile Planner will be integrated with CA Clarity PPM (http://www.ca.com/ppm).

Although it was not clear how much of the demonstrated app will run on Force.com and how much will run on the CA servers (though it appeared to be 100% on Force.com), it was clear that the application was accessed via a force.com URL.

For more information, see http://www.ca.com/agile.

A release date for the product was not mentioned, but the earliest planned release of any newly-announced feature thus far is Spring 2010, so one might assume that this will not be available until then.

New Developer Library Released

Today, Developer Force (http://developer.force.com) released its new library. Here are a few of them. All can be found at http://wiki.developerforce.com/index.php/Documentation.

Workbook
http://www.salesforce.com/us/developer/docs/workbook/index.htm

Fundamentals
http://www.salesforce.com/us/developer/docs/fundamentals/index.htm

Cookbook
http://www.salesforce.com/us/developer/docs/cookbook/index.htm

Apex Advanced Code Example
http://www.salesforce.com/us/developer/docs/apexcode/Content/apex_shopping_cart_example.htm
https://sites.secure.force.com/appexchange/listingDetail?listingId=a0N30000001saDCEAY

And many more to come!

A Mention in the Developers Challenge

October 2, 2009 · Filed Under Development, X-Squared On Demand, salesforce.com · Comment 

The salesforce.com Developer Force Challenge has concluded, and the team of Force Squared and The Enforcer won a mention!

Our Daily Shinro site was listed “for sheer exuberance!”

I’m really proud of the site, though the lion’s share of the kudos go to John for the concept and site design. I just coded whatever he told me to code; he’s the creative one!

So if anyone is looking for a custom Force.com Site or website integration to Salesforce, contact us and let’s discuss your needs!

Overload Apex Class to be Controller AND Extension

July 24, 2009 · Filed Under Apex, Development, Visualforce, X-Squared On Demand · 5 Comments 

Wow - today brought an interesting discovery. Here's the situation:

Coding the new premium version of Mass Update Contacts (details to come), I replaced the two parts of the page with Apex Components. This will allow the app to support custom address fields and international address formats.

I didn't want to write one ControllerExtension for the main page, a CustomController for the view section component, and another CustomController for the pageblocktable component. So here is the overloaded class constructor. Note that this works because an extension passes the StandardController to the constructor, and a CustomController passes nothing:

public with sharing class VersatileClass {

private Account account;

public VersatileClass(){
	system.debug('OPERATING AS CONTROLLER');
		if(System.currentPageReference().getParameters().get('id')==null){
			//Include error checking here
		} else{
			string AId = System.currentPageReference().getParameters().get('id');
			account = [select id, name from Account where id = :AId];
			//And whatever else you want to do
		}
}

public VersatileClass(ApexPages.StandardController controller) {
	system.debug('OPERATING AS EXTENSION');
		if(System.currentPageReference().getParameters().get('id')==null){
			//Include error checking here
		} else{
			this.account = (Account)controller.getRecord();
			//And whatever else you want to do
		}
	}
}

Enjoy! This should save people a lot of time.

Next Page »