Monday, March 13, 2017

Project Service Automation - Delegation UI and SDK Gotchas

A typical requirement for project management and time sheet systems is the ability to allow a manager to enter time sheets for team members. PSA supports this through the use of delegation records. A user can create a delegation record allowing another user to enter the hours they have worked on project tasks. Here I have created a delegation record for Joe Bloggs along with a  start and end date for when the delegation is allowed.

Now when I log on as Joe Bloggs from the Time Entry form I can switch users and add time entries for Joe Gill.

Delegation records are a bit cumbersome as the UI only allows a user to create delegation records for themselves and an administrator or project manager cannot create delegation records on behalf of another user. Also they can only be configured between two specifically named users. They cannot be configured in any type of generic or rule based fashion such as allowing a project manager to enter time worked for team members or allow membership of a team to give delegation rights. So entering delegate records for a large number of resources lends itself to improvement by writing some custom code using the SDK.

Creating delegation records using the SDK is easy as you can see from the code below. Now the interesting thing is that the Delegation From field must be populated and cannot be the same as the Delegation To field despite the fact that it gets overridden with the current users details (matching the UI experience). So if we run this code it will create a delegation record with the Delegation From set to the current user rather than the bookbale resource whose id is CF97C7F2-18DA-E611-80FB-3863BB349770.

Entity del = new Entity("msdyn_delegation");
del["msdyn_type"] = new OptionSetValue(192350000);
del["msdyn_startdate"] = DateTime.Parse("01/01/2017");
del["msdyn_enddate"] = DateTime.Parse("01/01/2018");
del["msdyn_delegationfrom"] = new EntityReference("bookableresource", Guid.Parse("CF97C7F2-18DA-E611-80FB-3863BB349770"));
del["msdyn_delegationto"] = new EntityReference("bookableresource", Guid.Parse("BD97C7F2-18DA-E611-80FB-3863BB349770"));
Guid id = crmService.Create(del);

The way to get around this is to use impersonation so that when the create code runs it runs as onbehalf of the Delegation From user. To demonstrate this I have written a custom workflow that runs when a new team member is added to a project. 

The custom workflow code is below and there a few steps it goes through before creating the delegation record. The first step it does is to check the project manager is a bookable resource of type user and gets the project managers userid as this will be used for impersonation later. It then checks that the Delegate To is a bookable resource of type user. The crucial line is where it creates the crmService object impersonating the Delegation From user id.

public class CreateDelegate : CodeActivity
[Input("Delegate From")]
public InArgument<EntityReference> DelegateFrom { get; set; }

[Input("Delegate To")]
public InArgument<EntityReference> DelegateTo { get; set; }

[Input("Delegation Start Date")]
public InArgument<DateTime> StartDate { get; set; }

[Input("Delegation End Date")]
public InArgument<DateTime> EndDate { get; set; }

protected override void Execute(CodeActivityContext executionContext)

ITracingService tracer = executionContext.GetExtension<ITracingService>();
IWorkflowContext context = executionContext.GetExtension<IWorkflowContext>();
IOrganizationServiceFactory serviceFactory = executionContext.GetExtension<IOrganizationServiceFactory>();
IOrganizationService crmService = serviceFactory.CreateOrganizationService(context.UserId);

EntityReference delegateFrom = DelegateFrom.Get(executionContext);
EntityReference userTo = DelegateTo.Get(executionContext);

// Get Delegate From User Details

QueryExpression query = new QueryExpression("bookableresource");
query.ColumnSet = new ColumnSet("userid");
FilterExpression qf = query.Criteria.AddFilter(LogicalOperator.And);
qf.Conditions.Add(new ConditionExpression("bookableresourceid", ConditionOperator.Equal, delegateFrom.Id));
qf.Conditions.Add(new ConditionExpression("resourcetype", ConditionOperator.Equal, 3));
EntityCollection ec = crmService.RetrieveMultiple(query);
if (ec.Entities.Count ==0)

tracer.Trace("The delegate from is not a bookableresource of resourcetype User");
Guid fromUserId = ec.Entities[0].GetAttributeValue<EntityReference>("userid").Id;

// Get Bookable Resource Id for Delegate To User

QueryExpression query2 = new QueryExpression("bookableresource");
query2.ColumnSet = new ColumnSet("userid");
FilterExpression qf2 = query2.Criteria.AddFilter(LogicalOperator.And);
qf2.Conditions.Add(new ConditionExpression("userid", ConditionOperator.Equal, userTo.Id.ToString()));
qf2.Conditions.Add(new ConditionExpression("resourcetype", ConditionOperator.Equal, 3));
EntityCollection ec2 = crmService.RetrieveMultiple(query2);
if (ec2.Entities.Count == 0)
{ tracer.Trace("The delegate to User is not a bookableresource of resourcetype User");
Guid delegateToId = ec2.Entities[0].Id ;

Entity del = new Entity("msdyn_delegation");
del["msdyn_type"] = new OptionSetValue(192350000);
del["msdyn_startdate"] = StartDate.Get<DateTime>(executionContext);
del["msdyn_enddate"] = EndDate.Get<DateTime>(executionContext);
del["msdyn_delegationfrom"] = new EntityReference("bookableresource", delegateFrom.Id);
del["msdyn_delegationto"] = new EntityReference("bookableresource", delegateToId);

// Impersonate the Delegate From User
crmService = serviceFactory.CreateOrganizationService(fromUserId);




No comments:

Post a Comment