Salesforce CreatedDate to LocalOrg Time Zone

folder_openSalesforce APEX
Small Time zonez clocks

Hello people,

It turns out that Salesfroce keeps all DateTime fields saved in GMT (Greenwich Mean Time).
This makes sense because the time is formatted when needed to the Users settings.
User > Settings > Language & Time Zone > Time Zone


Because of that when we try to use the field “CreatedDate” in a formula field like that “HOUR(CreatedDate)”,
we would get the value in GMT and not in User TimeZone or Company TimeZone.

In order to solve issue I have created APEX action that can be used to get the needed value.
In our case we want to get the Hour value from “DateCreated” in the Time Zone of our Company, which is set in
Setup > Company Information > Default Time Zone
Company information TimeZone
This field is DST( Daylight Saving Time ) aware and it is adjusted accordingly, that is why we can trust it.


Here is the Apex action and Test Code.
This code works on any sObject and Custom or Standart text field for storing the result!

There are plenty of comments for details!

Have fun!

APEX action class

public class action_HourInOrgTimeAbstract {
	static final public String CUSTOM_ERR_OBJ_TYPE = 'More than one object type found in one batch. Unexpected behavior in action_HourInOrgTimeAbstract';
	static final public String CUSTOM_ERR_FIELD = 'More than one target field found in one batch. Unexpected behavior in action_HourInOrgTimeAbstract';
	static final public String CUSTOM_ERR_FIELD_OR_OBJECT_NOT_FOUND = 'The object or target field does not exist: ';
	static final public String OUTPUT_FORMAT = 'HH';

	//This Action is to be used as apex action in "Process Builder" so that is why no Database.Batchable functionality is included.
	@InvocableMethod(label = 'Fill OrgHour' description = 'Fills target text field (from created date) in the organization timezone in the field Hour_Org_Time__c' category = 'Case')
	public static void fillOrgHour(List<Input> inputs) {
		List<String> ids = new List<String>();
		String queryInClause;
		String objectApiNameConsolidated;
		String targetFieldApiNameConsolidated;

		//Preparations and validations
		for (Input singleInput : inputs) {
			//bulking for query
			ids.add(singleInput.objectId);

			//query building help
			queryInClause += '\'' + singleInput.objectId + '\',';

			//getting Object Name from Id
			String objectApiName = singleInput.objectId.getSObjectType().getDescribe().getName();

			//Checking if all object types are uniform across input
			if (objectApiNameConsolidated == null) {
				objectApiNameConsolidated = objectApiName;
			} else if (objectApiNameConsolidated != objectApiName) {
				throw new SObjectException(CUSTOM_ERR_OBJ_TYPE);
			}

			//Checking if all target fields are uniform across input
			if (targetFieldApiNameConsolidated == null) {
				targetFieldApiNameConsolidated = singleInput.targetField;
			} else if (targetFieldApiNameConsolidated != singleInput.targetField) {
				throw new SObjectException(CUSTOM_ERR_FIELD);
			}

		}

		//Checking if all target field exist
		if (! doesFieldExist(objectApiNameConsolidated, targetFieldApiNameConsolidated)) {
			throw new SObjectException(CUSTOM_ERR_FIELD_OR_OBJECT_NOT_FOUND + objectApiNameConsolidated + ' OR '+ targetFieldApiNameConsolidated );
		}

		// remove last additional comma from string and first null
		queryInClause = queryInClause.subString(4, queryInClause.length() - 1);

		String queryString = 'Select Id, CreatedDate, ' + string.escapeSingleQuotes(targetFieldApiNameConsolidated);
		queryString += ' FROM ' + string.escapeSingleQuotes(objectApiNameConsolidated);
		queryString += ' WHERE Id IN (' + queryInClause + ')';

		List<SObject> sObjects = Database.query(queryString);

		//Getting the organization Time Zone Key
		Organization orgInfo = [Select TimezoneSidKey FROM Organization];

		//Preparing for bulk update
		List<SObject> updatedObjects = new List<SObject>();

		//doing the actual work finally
		for (SObject singleObj : sObjects) {

			//a bit shady conversion but that is the only working solution that I found
			Object timeObject = singleObj.get('CreatedDate');
			String jsonTime = JSON.serialize(timeObject);

			DateTime dateTimeSF = (DateTime) JSON.deserialize(jsonTime, DateTime.class);

			//Getting the Hour in Org time zone using the standard format function
			//The format function can show time in different time zones
			String hourInOrgTime = dateTimeSF.format(OUTPUT_FORMAT, orgInfo.TimeZoneSidKey);
			System.debug('hourInOrgTime: ' + hourInOrgTime);

			singleObj.put(targetFieldApiNameConsolidated, hourInOrgTime);

			updatedObjects.add(singleObj);
		}

		update updatedObjects;

	}


	public class Input {

		@InvocableVariable(label = 'Object Id' description = 'Id of the sObject' required = true)
		public Id objectId;

		@InvocableVariable(label = 'Target Field' description = 'API name of the target field' required = true)
		public String targetField;

	}

	public static boolean doesFieldExist(String targetObject, string targetField) {
                try {
                        //Describe methods have no limits so we are good here
                        SObject so = Schema.getGlobalDescribe().get(targetObject).newSObject();
                        return so.getSobjectType().getDescribe().fields.getMap().containsKey(targetField);
                } catch (Exception ex) {
                        System.debug('Exception type caught: ' + ex.getTypeName());
                        System.debug('Message: ' + ex.getMessage());
                        System.debug('Cause: ' + ex.getCause());
                        System.debug('Line number: ' + ex.getLineNumber());
                        System.debug('Stack trace: ' + ex.getStackTraceString());
                        
                        return false;
                }
        }
}

APEX test class

@IsTest
private class action_HourInOrgTimeAbstractTest {
	static final String CASE_SUBJECT = 'test Case';
	static final String CASE_ORIGIN = 'Email';
	static final String CON_FIRST_NAME = 'First Test';
	static final String CON_LAST_NAME = 'Last Test';
	static final Integer OBJECT_COUNT = 500;
	//The tests are not SObject orinated there are some hardcoded objects in the test. This could be the next version
	static final String TARGET_FIELD = 'Subject';
	static final String TARGET_FIELD_FAIL = 'iAmNonexistingField123';

	@testSetup
	static void setupTestData() {
		List<Case> cases = new List<Case>();

		for (Integer i = 0; i < OBJECT_COUNT; i++) {
			Case testCase = new Case();
			testCase.Subject = CASE_SUBJECT + i;
			testCase.Origin = CASE_ORIGIN;
			cases.add(testCase);
		}

		insert cases;

		Contact con = new Contact();
		con.FirstName = CON_FIRST_NAME;
		con.LastName = CON_LAST_NAME;

		insert con;
	}


	//This Action is to be used as apex action in "Process Builder" so that is why no Database.Batchable functionality is included.
	@isTest(SeeAllData = false)
	static void testFillOrgHourSuccess() {
		List<Case> updatedCases = new List<Case>();
		List<Id> caseIds = new List<Id>();

		List<Case> cases = [SELECT Id, Subject FROM Case WHERE Origin = :CASE_ORIGIN LIMIT :OBJECT_COUNT];

		List<action_HourInOrgTimeAbstract.Input> inputs = new List<action_HourInOrgTimeAbstract.Input>();
		for (Case singleObject : cases){
			action_HourInOrgTimeAbstract.Input input = new action_HourInOrgTimeAbstract.Input();
			input.objectId = singleObject.Id;
			input.targetField = TARGET_FIELD;
			inputs.add(input);

			caseIds.add(singleObject.Id);
		}

		action_HourInOrgTimeAbstract.fillOrgHour(inputs);

		updatedCases = [SELECT Id, Subject, CreatedDate FROM CASE WHERE Id IN :caseIds];

		Organization orgInfo = [Select TimezoneSidKey FROM Organization];

		for (Case singleUpdatedObject : updatedCases){
			String hourInOrgTime = singleUpdatedObject.CreatedDate.format(action_HourInOrgTimeAbstract.OUTPUT_FORMAT, orgInfo.TimeZoneSidKey);

			System.assertEquals(singleUpdatedObject.Subject, hourInOrgTime);
		}
	}

	@isTest(SeeAllData = false)
	static void testFillOrgFail() {
		List<Id> caseIds = new List<Id>();

		List<Case> cases = [SELECT Id, Subject FROM Case WHERE Origin = :CASE_ORIGIN LIMIT :OBJECT_COUNT];

		List<action_HourInOrgTimeAbstract.Input> inputs = new List<action_HourInOrgTimeAbstract.Input>();
		for (Case singleObject : cases){
			action_HourInOrgTimeAbstract.Input input = new action_HourInOrgTimeAbstract.Input();
			input.objectId = singleObject.Id;
			input.targetField = TARGET_FIELD_FAIL;
			inputs.add(input);

			caseIds.add(singleObject.Id);
		}

		try {
			action_HourInOrgTimeAbstract.fillOrgHour(inputs);
		} catch (Exception ex) {
			System.assert(ex.getMessage().contains(action_HourInOrgTimeAbstract.CUSTOM_ERR_FIELD_OR_OBJECT_NOT_FOUND), TRUE );
		}
	}

	@isTest(SeeAllData = false)
	static void testFillOrgFailObjectConsolidation() {
		List<Id> objectIds = new List<Id>();

		List<Case> cases = [SELECT Id, Subject FROM Case WHERE Origin = :CASE_ORIGIN LIMIT :OBJECT_COUNT];
		Contact contact = [SELECT Id FROM Contact LIMIT 1];

		List<action_HourInOrgTimeAbstract.Input> inputs = new List<action_HourInOrgTimeAbstract.Input>();
		for (Case singleObject : cases){
			action_HourInOrgTimeAbstract.Input input = new action_HourInOrgTimeAbstract.Input();
			input.objectId = singleObject.Id;
			input.targetField = TARGET_FIELD;
			inputs.add(input);

			objectIds.add(singleObject.Id);
		}

		action_HourInOrgTimeAbstract.Input input = new action_HourInOrgTimeAbstract.Input();
		input.objectId = contact.Id;
		input.targetField = 'FirstName';
		inputs.add(input);

		try {
			action_HourInOrgTimeAbstract.fillOrgHour(inputs);
		} catch (SObjectException ex) {
			System.debug(ex.getMessage());
			System.assert(ex.getMessage().contains(action_HourInOrgTimeAbstract.CUSTOM_ERR_OBJ_TYPE));
		}
	}
}
Menu