Demo

The eConnect API demo project is small C# project that explains how to use the eConnect API. It starts with creating a new activity and then continuous on to explain all the steps you have to do in order to complete the activity. This includes adding and handling parts, cost lines, comments and so on. You can find a downloadable version of the project here

The project consist of two C# files, eConnectApi and Program.cs

Login

Before you can start using any other API function you need to obtain a valid login ticket. This is done by calling one of the Logon functions. We have chosen to login using a user name and password. The user in the sample is a sub contractor user, you will have to change user name and password to the user name and password you have received from InfoCare in order to get the sample to working.


        private static void Login()
        {
            //Change user name and password to appropriate values
            dynamic userModel = api.Logon("bubba@subbus.no", "Subbus1324").Result;
            //set the ticket id so we can use it from the other functions
            _ticketId = userModel.TicketId.ToString();

            Console.WriteLine("Ticket : " + _ticketId);
        }
        
    

The actual work is api.Logon method which sets up the argument model and calls the API function


        public async Task<dynamic> Logon(string id, string verification)
        {
            HttpResponseMessage response = await _httpClient.PostAsync(_httpClient.BaseAddress + "/api/Account/Logon?id=" + id + 
                "&verification=" + verification, null);

            if (response.IsSuccessStatusCode)
            {
                string jsonString = await response.Content.ReadAsStringAsync();
                return JsonConvert.DeserializeObject(jsonString);
            }
            else
                throw new HttpResponseException(response);
        }
        
    

Running this code should display your ticket id in the console


        Ticket : 071c8517-76ad-45a7-9343-10621032a4bd
        
    

In a real life application you would store this ticket away somewhere safe as the ticket can be reused for at least several days. The ticket expiration date is part of the information returned with the ticket id, see UserModel for more details.

Create activity

In this demo we will start by creating a new activity, but keep in mind that not all activities are created through the API. So in many cases you will probably start by calling GetActivity to get an activity that has been created elsewhere. If you look at the CreateActivityArgumentModel you can see that you only have to fill out the TicketId. If you do not fill out the combined contract line id, a default contract line id will be picked for you. Calling create activity does NOT actually create the activity, you can think of calling create activity as getting an template on how to create an activity.

If we look at first few lines of the CreateActivity method you can see that we first the api function to get the create template from the server, we that extract the contract line id and the complete template as a list. Working with list with dynamic that contains other dynamics is a bit cumbersome so it is easier to convert to list right away.


        //first we need to create the activity
        dynamic createReturnModel = api.CreateActivity(_ticketId).Result;
        //extract contract line id
        string contractLineId = createReturnModel.CombinedContractLineId.ToString();
        //make a copy of the activity model
        _activityModel = ((Newtonsoft.Json.Linq.JArray)createReturnModel.ActivityUpdates).Values<dynamic>().ToList();
        //dump content to console
        PrintActivityModel();
        
    

The content of template is then printed to the console


Field: ProblemDescription,  IsRequired: True , IsEnabled: True, Lookup : None , Type 6 , Value:
        Field: SolutionDescription,  IsRequired: False , IsEnabled: True, Lookup : Optional , Type 6 , Value:
        Field: InvoiceReference,  IsRequired: False , IsEnabled: True, Lookup : None , Type 6 , Value:
        Field: SpecialReference,  IsRequired: False , IsEnabled: True, Lookup : None , Type 6 , Value:
        Field: WarrantyReference,  IsRequired: False , IsEnabled: True, Lookup : None , Type 6 , Value:
        Field: CustomerReference,  IsRequired: False , IsEnabled: True, Lookup : None , Type 6 , Value:
        Field: Country,  IsRequired: False , IsEnabled: True, Lookup : None , Type 6 , Value: NO
        Field: Contract.ContractId,  IsRequired: False , IsEnabled: False, Lookup : None , Type 6 , Value: c200
        Field: Contract.Name,  IsRequired: False , IsEnabled: False, Lookup : None , Type 6 , Value: Time and material
        Field: SiteCustomer.Name,  IsRequired: True , IsEnabled: True, Lookup : None , Type 6 , Value: Bents company AS
        Field: SiteCustomer.AdditionalInfo,  IsRequired: False , IsEnabled: True, Lookup : None , Type 6 , Value:
Field: SiteCustomer.Address,  IsRequired: True , IsEnabled: True, Lookup : None , Type 6 , Value:
        Field: SiteCustomer.ZipCode,  IsRequired: True , IsEnabled: True, Lookup : None , Type 1 , Value: 4841
        Field: SiteCustomer.City,  IsRequired: True , IsEnabled: True, Lookup : None , Type 6 , Value: ARENDAL
        Field: SiteCustomer.PhoneNumber,  IsRequired: True , IsEnabled: True, Lookup : None , Type 10 , Value: 98002840
        Field: SiteCustomer.WorkPhone,  IsRequired: False , IsEnabled: True, Lookup : None , Type 10 , Value:
        Field: SiteCustomer.ContactPerson,  IsRequired: True , IsEnabled: True, Lookup : None , Type 6 , Value: Bubba Subbus
        Field: SiteCustomer.eMail,  IsRequired: True , IsEnabled: True, Lookup : None , Type 11 , Value: hkbwa@infocare.no
        Field: SiteCustomer.MobileNumber,  IsRequired: False , IsEnabled: True, Lookup : None , Type 10 , Value: 98002840
        Field: Product.Name,  IsRequired: False , IsEnabled: True, Lookup : None , Type 6 , Value: SERVICE
        Field: Product.Brand,  IsRequired: True , IsEnabled: True, Lookup : None , Type 6 , Value: DIV
        Field: Product.Description,  IsRequired: True , IsEnabled: True, Lookup : None , Type 6 , Value: Chargeable Time/material Onsit
        Field: Product.SerialNumber,  IsRequired: False , IsEnabled: True, Lookup : None , Type 6 , Value:
        Field: Product.SerialNumberCustomer,  IsRequired: False , IsEnabled: True, Lookup : None , Type 6 , Value:
        Field: Product.SerialNumberOut,  IsRequired: False , IsEnabled: False, Lookup : None , Type 6 , Value:
        Field: Product.PurchaseDate,  IsRequired: False , IsEnabled: True, Lookup : None , Type 3 , Value:
        Field: CustomProperty.506886bc-de91-45e2-a864-f6e2118e58ab,  IsRequired: False , IsEnabled: True, Lookup : Required , Type 6 , Value:
Field: CustomProperty.07963655-c5c5-4dfc-9e28-aef9ff9abde4,  IsRequired: True , IsEnabled: True, Lookup : None , Type 3 , Value:
Field: CustomProperty.5782cc4f-0874-4201-b81d-5f2635713e6f,  IsRequired: True , IsEnabled: True, Lookup : Required , Type 6 , Value:
        Field: CustomProperty.97840156-f1d0-41e4-99f2-1e9d32b3d1d8,  IsRequired: False , IsEnabled: True, Lookup : Required , Type 6 , Value:
Field: CustomProperty.763578e2-6d6e-4e07-8de8-0137dd97d687,  IsRequired: True , IsEnabled: True, Lookup : None , Type 6 , Value:
        Field: CustomProperty.851bc5ae-6c29-4728-8a15-9fbbfddae447,  IsRequired: False , IsEnabled: True, Lookup : None , Type 6 , Value:
        Field: CustomProperty.100505de-f7b4-4aa6-86c6-014f30e8687d,  IsRequired: False , IsEnabled: True, Lookup : None , Type 8 , Value:
Allowable actions: Add comments Add cost lines
    
    

In order to actually create the activity we must fill out all required fields that does not have a a default value. I have highlighted these fields in the sample above. Note that one of the fields require that the value is picked from a lookup, but I happen to know that "Bytt PC" is a valid value from the lookup in question so I will just use this for now. You will find samples on how to use lookups later in this document. Also note the last highlighted line, this is a printout AllowableActions attribute, it tells us that we can add new cost lines and comments to this activity at this stage. We will however not do this just now, we will wait until we have created the activity.

I have used this little helper function to construct a field attribute model.


        private static dynamic CreateFieldUpdate(string fieldName, object value)
        {
            return new 
            {
                AttributeAction = 1, //1 is Modified
                AttributeType = 0, //0 is FieldModel
                AttributeName = fieldName, //Attribute name for fields are the field name
                AttributeData = new
                {
                    FieldName = fieldName,
                    FieldValue = value
                }
            };
        }
    
  

So all I have to is to construct a list of these five updates and then send it together with the contract line id in order to actually create the activity


        List<dynamic> updates = new List<dynamic>();
        //set field values for required fields that does not have a value
        updates.Add(CreateFieldUpdate("ProblemDescription", "PC stopped working, nothing happens when I turn it on"));
        updates.Add(CreateFieldUpdate("SiteCustomer.Address", "Parkway Blvd 2345"));
        //by checking the field type (FieldType =3) on this custom property I know it is date so I will just set it to now
        updates.Add(CreateFieldUpdate("CustomProperty.07963655-c5c5-4dfc-9e28-aef9ff9abde4", DateTime.Now));
        //I know that "Bytt PC" is valid lookup value in the lookup this fields is supposed to use
        updates.Add(CreateFieldUpdate("CustomProperty.5782cc4f-0874-4201-b81d-5f2635713e6f", "Bytt PC"));
        //The last custom property is a regular string property (FieldType=6)
        updates.Add(CreateFieldUpdate("CustomProperty.763578e2-6d6e-4e07-8de8-0137dd97d687", "Hello eConnect API"));
            
        //update the activity with our new values
        dynamic updateReturnModel=api.UpdateActivity(_ticketId, contractLineId, null, updates).Result;
        //set the combined activity id
        _combinedActivityId = updateReturnModel.CombinedActivityId.ToString();
        //merge returned activity model with what we originally got from create to get a complete and updated activity model
        MergeActivityModel(updateReturnModel);
        PrintActivityModel();
    

I then take the result I receive from UpdateActivity and merge it together with what I received from CreateActivity so that I get a updated activity model which I then print to the console.


        Field: ProblemDescription,  IsRequired: True , IsEnabled: True, Lookup : None , Type 6 , Value: PC stopped working, nothing happens when I turn it on
        Field: SolutionDescription,  IsRequired: False , IsEnabled: True, Lookup : Optional , Type 6 , Value:
        Field: InvoiceReference,  IsRequired: False , IsEnabled: False, Lookup : None , Type 6 , Value:
        Field: SpecialReference,  IsRequired: False , IsEnabled: False, Lookup : None , Type 6 , Value:
        Field: WarrantyReference,  IsRequired: False , IsEnabled: False, Lookup : None , Type 6 , Value:
        Field: CustomerReference,  IsRequired: False , IsEnabled: False, Lookup : None , Type 6 , Value:
        Field: Country,  IsRequired: False , IsEnabled: False, Lookup : None , Type 6 , Value: NO
        Field: Contract.ContractId,  IsRequired: False , IsEnabled: False, Lookup : None , Type 6 , Value: c200
        Field: Contract.Name,  IsRequired: False , IsEnabled: False, Lookup : None , Type 6 , Value: Time and material
        Field: SiteCustomer.Name,  IsRequired: True , IsEnabled: False, Lookup : None , Type 6 , Value: Bents company AS
        Field: SiteCustomer.AdditionalInfo,  IsRequired: False , IsEnabled: False, Lookup : None , Type 6 , Value:
        Field: SiteCustomer.Address,  IsRequired: True , IsEnabled: True, Lookup : None , Type 6 , Value: Parkway Blvd 2345
        Field: SiteCustomer.ZipCode,  IsRequired: True , IsEnabled: True, Lookup : None , Type 1 , Value: 4841
        Field: SiteCustomer.City,  IsRequired: True , IsEnabled: True, Lookup : None , Type 6 , Value: ARENDAL
        Field: SiteCustomer.PhoneNumber,  IsRequired: True , IsEnabled: True, Lookup : None , Type 10 , Value: 98002840
        Field: SiteCustomer.WorkPhone,  IsRequired: False , IsEnabled: True, Lookup : None , Type 10 , Value:
        Field: SiteCustomer.ContactPerson,  IsRequired: True , IsEnabled: True, Lookup : None , Type 6 , Value: Bubba Subbus
        Field: SiteCustomer.eMail,  IsRequired: True , IsEnabled: False, Lookup : None , Type 11 , Value: hkbwa@infocare.no
        Field: SiteCustomer.MobileNumber,  IsRequired: False , IsEnabled: True, Lookup : None , Type 10 , Value: 98002840
        Field: Product.Name,  IsRequired: False , IsEnabled: True, Lookup : None , Type 6 , Value: SERVICE
        Field: Product.Brand,  IsRequired: True , IsEnabled: True, Lookup : None , Type 6 , Value: DIV
        Field: Product.Description,  IsRequired: True , IsEnabled: True, Lookup : None , Type 6 , Value: Chargeable Time/material Onsit
        Field: Product.SerialNumber,  IsRequired: False , IsEnabled: True, Lookup : None , Type 6 , Value:
        Field: Product.SerialNumberCustomer,  IsRequired: False , IsEnabled: False, Lookup : None , Type 6 , Value:
        Field: Product.SerialNumberOut,  IsRequired: False , IsEnabled: True, Lookup : None , Type 6 , Value:
        Field: Product.PurchaseDate,  IsRequired: False , IsEnabled: False, Lookup : None , Type 3 , Value:
        Field: CustomProperty.506886bc-de91-45e2-a864-f6e2118e58ab,  IsRequired: False , IsEnabled: True, Lookup : Required , Type 6 , Value:
        Field: CustomProperty.07963655-c5c5-4dfc-9e28-aef9ff9abde4,  IsRequired: True , IsEnabled: True, Lookup : None , Type 3 , Value: 03.04.2017 00:00:00
        Field: CustomProperty.5782cc4f-0874-4201-b81d-5f2635713e6f,  IsRequired: True , IsEnabled: True, Lookup : Required , Type 6 , Value: Bytt PC
        Field: CustomProperty.97840156-f1d0-41e4-99f2-1e9d32b3d1d8,  IsRequired: False , IsEnabled: True, Lookup : Required , Type 6 , Value:
        Field: CustomProperty.763578e2-6d6e-4e07-8de8-0137dd97d687,  IsRequired: True , IsEnabled: True, Lookup : None , Type 6 , Value: Hello eConnect API
        Field: CustomProperty.851bc5ae-6c29-4728-8a15-9fbbfddae447,  IsRequired: False , IsEnabled: True, Lookup : None , Type 6 , Value:
        Field: CustomProperty.100505de-f7b4-4aa6-86c6-014f30e8687d,  IsRequired: False , IsEnabled: True, Lookup : None , Type 8 , Value:
        Field: CombinedActivityId,  IsRequired: False , IsEnabled: False, Lookup : None , Type 6 , Value: sesam10:1042042
        Field: Originator,  IsRequired: False , IsEnabled: False, Lookup : None , Type 6 , Value: sunflower
        Field: ServiceDescription,  IsRequired: False , IsEnabled: False, Lookup : None , Type 6 , Value: C200 Chargeable Time/material Onsit
        Field: SecurityCode,  IsRequired: False , IsEnabled: False, Lookup : None , Type 6 , Value: 879246
        Field: AssignedGroup,  IsRequired: True, IsEnabled: False, Lookup : None , Type 6 , Value: PilotGroup
        Field: AssignedResource,  IsRequired: True, IsEnabled: False, Lookup : None , Type 6 , Value: 7a282cf6-6469-40bc-b365-9236c8b75c9d        
        Field: SubAssignedResource,  IsRequired: False, IsEnabled: True, Lookup : None , Type 6 , Value:        
        Schedule: 134120
        Personal deadline: 04/10/2017 13:28:00
        Official deadline: 04/10/2017 13:28:00
        Delays:
        Fails:
        Schedule ends
        Attachment: CustomerCopy, Type: 3 (application/pdf)
        CustomeEvent:Oppdraget er mottatt
        WorkflowStep: fab81133-3b60-4fc8-8b93-210a1a93b1eb:Assign
        WorkflowStep: 273c4b90-9c17-4fce-a025-2c3c6906ac75:Create
        Allowable actions: Add comments Add cost lines Add parts
    

As you can see from the sample above so have several fields changed one or more properties. If you look for example at the reference fields like SpecialReference or InvoiceReference you can see that the IsEnabled flag has changed from true to false. This means that these fields no longer can be changed (this may change when we continue pushing the activity trough the activity workflow). We have also received some additional fields; CombinedActivityId, Originator, ServiceDescription, SecurityCode, AssignedGroup, AssignedResource and SubAssignedResource. CombinedActivityId, Originator, ServiceDescription and SecurityCode are read-only fields and will remain so throughout the life cycle of the activity. Whether the assign step will be modifiable will depend upon the user and the current workflow step. The sample activity was created using a subcontractor that has turned on both automatic assignment and sub-assignment. This means that when this subcontractor creates an activity the activity will automatically be assigned to the subcontractor and the subcontractor will be allowed to sub-assign the activity to one of the technicians working for the subcontractor. We will show you how this is done later on. An InfoCare employee opening up this activity may be allowed to change both assigned resource and assigned group provided that the activity is in an assign step.

Finally you can see that we have received a two WorkflowSteps, a Schedule, an Attachment and a Custom Event and the allowable actions has changed. A schedule tells us when the activity has to reach a certain state. It is possible to modify a schedule using the API, you will find a sample on how to this later. An attachment is a file associated with the activity. In this case it is a report, i.e. print out of the activity (Type=3). A custom event is record of "important" events that has happened to the activity, in this case the we have received an event that tells us that the activity has been created. The workflow steps tells us that we have done the create and assign step so far.

Adding comments

After creating an activity you may want to add additional information about the activity, like for example that technician should call customer one hour before arriving or that it the third time the customer have reported this problem. You can do this by adding comments to the activity.

When adding a comment you only have to set the comment text the other values can automatically set by the server, so creating a attribute model for adding a comment boils down to this:


        private static dynamic CreateNewComment(string commentText)
        {
           
            //do not have to set attribute name as this will be changed by the server to be equal to the id of the comment
            return new
            {
                AttributeAction = 0, //0 is Add
                AttributeType = 2, //2 is CommentModel                
                AttributeData = new
                {
                    Fields = new dynamic[] 
                    {
                        new
                        {
                            AttributeAction = 0, //0 is Add
                            AttributeType = 0, //0 is FieldModel
                            AttributeName = "Comment", //Attribute name for fields are the field name
                            AttributeData = new
                            {
                                FieldName ="Comment",
                                FieldValue = commentText
                            }
                        }
                    }
                }
            };
        }
    

This code sets up the call, merges the result and print the result to the console


    private static void AddComment()
    {
        List<dynamic> updates = new List<dynamic>();
        //create comment
        updates.Add(CreateNewComment("Please ask for Anton Kruger when you reach the customer site."));
        //send updates to the api
        dynamic updateReturnModel = api.UpdateActivity(_ticketId, null, _combinedActivityId, updates).Result;
        //merge result with out activity model
        MergeActivityModel(updateReturnModel);
        //print activity model to console
        PrintActivityModel();
    }
    

In the output below I have only included the two new lines we got from adding this comment


    Comment: Please ask for Anton Kruger when you reach the customer site.
    Created by: bubba@subbus.no, at: 04.04.2017 09:45:25
    

Adding parts

Ordering and handling spare parts in intrinsic factor when it comes to working with activities. You would often try to order spare parts as early as possible in the process as it can take some time before the spare is available if it has to be ordered from an external supplier. Adding a spare part is done by adding a new RequisitionLineModel to the activity. I have used the CreateAttributeTemplate to get an initial list of fields I need to add for a requisition line.


     //Lets us the handy CreateAttributeTemplate method to get a list fields we need to fill out when adding a new part
    dynamic attributeTemplateReturnModel = api.CreateAttributeTemplate(_ticketId, _combinedActivityId, 5 /* 5 is attribute type RequisitionLine */).Result;
    List fields= ((Newtonsoft.Json.Linq.JArray)attributeTemplateReturnModel.TemplateFields).Values().ToList();
    Console.WriteLine("Required fields for adding a requisition lines");
    PrintAttributeModel(fields);  
    

As we can see from the printout below there are 3 fields that must be set in order to add a new part, we must of course specify the part (PartNumber) and the number of units we want (Quantity), but we must also specify RequisitionType. As you can see from the definition so is this a lookup value, hence I should have made a call to GetLookupValues in order to get a list of available values. However, I happen to know that requisition type "1" is valid for this activity, so we will skip this step.


    Required fields for adding a requisition lines
    Field: RequisitionType,  IsRequired: True, IsEnabled: True, Lookup : Required , Type 6 , Value:
    Field: PartNumber,  IsRequired: True, IsEnabled: True, Lookup : None , Type 6 , Value:
    Field: Quantity,  IsRequired: True, IsEnabled: True, Lookup : None , Type 1 , Value: 0
    

So with this in mind we can set correct values and apply the update


    //set values
    fields.FirstOrDefault(p => p.AttributeData.FieldName == "PartNumber").AttributeData.FieldValue = "d200";
    fields.FirstOrDefault(p => p.AttributeData.FieldName == "Quantity").AttributeData.FieldValue = 1;
    fields.FirstOrDefault(p => p.AttributeData.FieldName == "RequisitionType").AttributeData.FieldValue = "1";
    //must wrap the fields in a proper requisition line update
    List activityUpdates = new List();
    activityUpdates.Add(new
    {
        AttributeAction = 0, //0 - Add
        AttributeType = 5, //RequisitionLine
        AttributeData = new
        {
            RequisitionLineUpdates=fields
        }
    });
    dynamic updateReturnModel = api.UpdateActivity(_ticketId, null, _combinedActivityId, activityUpdates).Result;
    //merge result with out activity model
    MergeActivityModel(updateReturnModel);
    //get allowable actions
    _allowableActions = updateReturnModel.AllowableActions;
    //print activity model to console
    PrintActivityModel();
    

In the printout below I have only include the new values we have received from adding the part


    Requisition: 472679:1
    Field: PartNumber,  IsRequired: False, IsEnabled: False, Lookup : None , Type 6 , Value: d200
    Field: PartName,  IsRequired: False, IsEnabled: False, Lookup : None , Type 6 , Value: Nikon D200
    Field: Quantity,  IsRequired: False, IsEnabled: False, Lookup : None , Type 1 , Value: 1
    Field: Stock,  IsRequired: False, IsEnabled: False, Lookup : None , Type 6 , Value: 1040
    Field: StockDisplayName,  IsRequired: False, IsEnabled: False, Lookup : None , Type 6 , Value: IC Kr.sand
    Field: RequisitionType,  IsRequired: False, IsEnabled: False, Lookup : None , Type 6 , Value: 1
    Field: LowestCompletedZone,  IsRequired: False, IsEnabled: False, Lookup : None , Type 1 , Value: Ordered
    Field: EstimatedDeliveryStatus,  IsRequired: False, IsEnabled: False, Lookup : None , Type 6 , Value:
    Field: EstimatedDeliveryDate,  IsRequired: False, IsEnabled: False, Lookup : None , Type 6 , Value:
    Field: DeliveryCustomerName,  IsRequired: False, IsEnabled: False, Lookup : None , Type 6 , Value: INFOCARE ASA
    Field: DeliveryCustomerAdditionalInfo,  IsRequired: False, IsEnabled: False, Lookup : None , Type 6 , Value: INFOCARE KRISTIANSAND
    Field: DeliveryCustomerAddress,  IsRequired: False, IsEnabled: False, Lookup : None , Type 6 , Value: SKIBÅSEN 42A
    Field: DeliveryCustomerZip,  IsRequired: False, IsEnabled: False, Lookup : None , Type 6 , Value: 4636
    Field: DeliveryCustomerCity,  IsRequired: False, IsEnabled: False, Lookup : None , Type 6 , Value: KRISTIANSAND S
    Line event : order
    Line event : orderLocal
    

There is couple of things we would like to draw your attention to. First notice the requisition id, 472679:1. This is the unique identifier for this requisition line (part). Then have a look at all the additional fields we have received. When we created the part we only had to specify PartName, Quantity and RequisitionType, but as you can see from the printout there are several more fields we could have specified. Finally you can see that there as been added two requisition line events, the first event "order" tells us that the part has been ordered, the second line "orderLocal" tells us that the line has been ordered locally. The next logical step would be to pick the part up from local stock, but before do that we should assign the activity to a technician.

Changing the deadline

Sometimes you may want to change a deadline, it may be that the customer cannot meet you at the appointed time, some prerequisites may have delayed the activity, traffic may be congested and so on. A schedule is changed by adding a ScheduleDelay to the schedule Delays list as shown in this example.


    private static void ChangeSchedule()
    {
        //we need a schedule to change first, just get the first we find
        dynamic schedule =_activityModel.FirstOrDefault(p => p.AttributeType == 3 /* 3 is Schedule */).AttributeData;
        //start by getting a delay template using the lookup controller
        dynamic returnModel =  api.CreateAttributeTemplate(_ticketId, _combinedActivityId, 12, schedule.ScheduleId.ToString()).Result;
        List templateFields= ((Newtonsoft.Json.Linq.JArray)returnModel.TemplateFields).Values().ToList();
        //loop through the template fields
        foreach (dynamic attribute in templateFields)
        {
            if (attribute.AttributeName == "PersonalDeadline")
                //set deadline to be two hours into the future
                attribute.AttributeData.FieldValue = DateTime.Now.AddHours(2);
            else if (attribute.AttributeName == "DelayReason")
                //should have used the lookup specified in the DelayReason field, but I happen to now that 4 is a valid value
                attribute.AttributeData.FieldValue = "4";
            else if (attribute.AttributeName=="DelayDescription")
            {
                //this is of type comment
                dynamic commentModel = attribute.AttributeData;
                List commentFields = ((Newtonsoft.Json.Linq.JArray)commentModel.Fields).Values().ToList();
                //so get field that represents the actual comment text
                dynamic commentText = commentFields.FirstOrDefault(p => p.AttributeName == "Comment");
                commentText.AttributeData.FieldValue = "Talked to customer, she wants to meet in two hours";
            }
        }            
            
        //create delay model using the template fields
        dynamic delayModel = new
        {
            Fields = templateFields
        };
        //create attribute add model
        dynamic attributeAddDelay = new
        {
            AttributeAction = 0,
            AttributeType = 12, //Delay
            AttributeData = delayModel
        };
        //create schedule  update 
        dynamic scheduleModel = new
        {
            ScheduleId = schedule.ScheduleId,
            Delays = new List()
        };
        //add delay to delay list
        scheduleModel.Delays.Add(attributeAddDelay);
        // then we need to wrap this into an attribute update model
        List activityUpdates = new List();
        activityUpdates.Add(new
        {
            AttributeAction = 1, //1 - Modify
            AttributeType = 3, //Schedule
            AttributeData = scheduleModel

        });
        dynamic updateReturnModel = api.UpdateActivity(_ticketId, null, _combinedActivityId, activityUpdates).Result;
        //merge result with out activity model
        MergeActivityModel(updateReturnModel);
        //get allowable actions
        _allowableActions = updateReturnModel.AllowableActions;
        //print activity model to console
        PrintActivityModel();

    }
    

In the print out we can see that the schedule has changed and we got the beginning change history for this schedule.

    
    Schedule: 148993
    Personal deadline: 29/09/2017 15:28:54
    Official deadline: 29/09/2017 15:28:54
    Delays:
    Field: CreatedBy,  IsRequired: False, IsEnabled: False , Type  , Value: alban@subbus.no
    Field: CreatedAt,  IsRequired: False, IsEnabled: False , Type  , Value: 29/09/2017 13:28:54
    Field: PersonalDeadline,  IsRequired: False, IsEnabled: False , Type  , Value: 29/09/2017 15:28:54
    Field: OfficialDeadline,  IsRequired: False, IsEnabled: False , Type  , Value: 29/09/2017 15:28:54
    Field: DelayReason,  IsRequired: True, IsEnabled: False, Lookup : Required , Type 6 , Value: 4, DisplayValue: 4: Kundens ønske/årsak
    CommentId: 1465811
    Field: CreatedAt,  IsRequired: False, IsEnabled: False , Type 4 , Value: 29/09/2017 13:28:54
    Field: CreatedBy,  IsRequired: False, IsEnabled: False , Type 6 , Value: alban@subbus.no
    Field: Comment,  IsRequired: True, IsEnabled: False , Type 6 , Value: Talked to customer, she wants to meet in two hours
    Fails:
    Schedule ends
    

Assigning activity to technician

In this demo we will use a technique call sub-assignment. The activity has already been automatically assigned to the company so we should sub-assign the activity to a technician working for the company. We would normally use Lookup.GetLookupValues setting LookupType="SubAssignees" to get a list of valid users, but I know that "bubba@subbus.no" is a valid technician for our demo. So all I need to in order to sub-assign the activity to this user is to construct a field update for "SubAssignedResource" field where I set the value to "bubba@subbus.no".


    private static void SubAssignActivity()
    {
        //create update list
        List updates = new List();
        //set field values for required fields that does not have a value
        updates.Add(CreateFieldUpdate("SubAssignedResurce", "bubba@subbus.no"));
        //update the activity with our new values
        dynamic updateReturnModel = api.UpdateActivity(_ticketId, null, _combinedActivityId, updates).Result;
        //merge result with out activity model
        MergeActivityModel(updateReturnModel);
        //get allowable actions
        _allowableActions = updateReturnModel.AllowableActions;
        //print activity model to console
        PrintActivityModel();
    }
    

As we can see from the printout excerpt below we have successfully changed the sub-assigned resource


    Field: AssignedGroup,  IsRequired: True, IsEnabled: False, Lookup : None , Type 6 , Value: PilotGroup
    Field: AssignedResource,  IsRequired: True, IsEnabled: False, Lookup : None , Type 6 , Value: 7a282cf6-6469-40bc-b365-9236c8b75c9d
Field: SubAssignedResource,  IsRequired: False, IsEnabled: True, Lookup : None , Type 6 , Value: bubba@subbus.no
    

Going through the workflow

After we have added all information we need to the activity it is about time we move to the next step workflow, but before we can do that we need to know which steps that are available and what the requirements are for going there. So lets start by calling the Lookup.GetAvailableWorkflowSteps

The GoNextStep function calls GetAvailableWorkflowSteps to get all available steps and print it out to the console


    private static void GoNextStep()
    {
        Console.WriteLine("Getting available steps start");
        dynamic availableWorkflowStepsReturnModel = api.GetAvailableWorkflowSteps(_ticketId, _combinedActivityId).Result;
        foreach (var item in availableWorkflowStepsReturnModel.Items)
        {
            Console.WriteLine("Id: " + item.Id + ", Name: " + item.Name + ", IsDefault : " + item.IsDefault);
            Console.WriteLine("   Min start time:  " + item.EarliestPossibleStartTime + ", Min req level: " + item.MinumumRequisitionCompletePercentage + ", Complete cost lines:  " + item.CostLinesMustBeCompleted);
            if (!HasProperty(item, "TemplateFields") || item.TemplateFields==null || item.TemplateFields.Count==0)
                Console.WriteLine("   Additional fields required : None");
            else
            {
                List templateFields = ((Newtonsoft.Json.Linq.JArray)item.TemplateFields).Values().ToList();
                Console.WriteLine("   Additional fields  required: " + templateFields.Count);
                PrintAttributeModel(templateFields);
            }
        }
        Console.WriteLine("Getting available steps complete");

        List updates = new List();
        //Add a workflow step
        updates.Add(CreateNewWorkflowStep("bda2d848-d61e-783d-f44e-6e61f3f9ec4b"));
        //send updates to the api
        dynamic updateReturnModel = api.UpdateActivity(_ticketId, null, _combinedActivityId, updates).Result;
        //merge result with out activity model
        MergeActivityModel(updateReturnModel);
        //print activity model to console
        PrintActivityModel();
    }
    

As we can see from the printout below there are five possible actions we can perform.


    Getting available steps start
    Id: bda2d848-d61e-783d-f44e-6e61f3f9ec4b, Name: Ready, IsDefault : True
       Min start time:  29/09/2017 15:42:00, Min req level: , Complete cost lines:  False
       Additional fields  required: 2
            Field: CreatedAt,  IsRequired: False, IsEnabled: True , Type 4 , Value: 29/09/2017 15:44:31
            CommentId: 0
                Field: Comment,  IsRequired: False, IsEnabled: False, Lookup : Optional , Type 6 , Value:
        Id: DBD, Name: DBD, IsDefault : False
           Min start time:  , Min req level: , Complete cost lines:  False
           Additional fields required : None
        Id: Wait, Name: Wait, IsDefault : False
           Min start time:  , Min req level: , Complete cost lines:  False
           Additional fields required : None
        Id: WArr, Name: WArr, IsDefault : False
           Min start time:  , Min req level: , Complete cost lines:  False
           Additional fields required : None
        Id: WBuyBack, Name: WBuyBack, IsDefault : False
           Min start time:  , Min req level: , Complete cost lines:  False
           Additional fields required : None
    Getting available steps complete
    

None of the steps requires any special requirements other than that all required fields be filled out. As you recall from the last time we printed out activity model that all required fields does indeed have values. We are going to go to the ready step (Id=bda2d848-d61e-783d-f44e-6e61f3f9ec4b), so let us create a new workflow step update. This step supports two optional fields; CreateAt and Comment. Since they are optional we are not going to fill them out at this time.

    
    private static dynamic CreateNewWorkflowStep(string workflowSetId)
    {
        return new
        {
            AttributeAction = 0, //0 is Add
            AttributeType = 8, //2 is WorkflowStepModel
            AttributeData = new
            {
                Id = workflowSetId
            }
        };
    }        
    

Going from assign to ready does not change the activity much, the only difference is that AssignedGroup and AssignedResource are now locked as you can see from the printout below

    
    Field: AssignedGroup,  IsRequired: False, IsEnabled: False, Lookup : None , Type 6 , Value: PilotGroup
    Field: AssignedResource,  IsRequired: False, IsEnabled: False, Lookup : None , Type 6 , Value: 7a282cf6-6469-40bc-b365-9236c8b75c9d    
    

and we have of course gotten a new workflow step


    WorkflowStep: fab81133-3b60-4fc8-8b93-210a1a93b1eb:Assign
    WorkflowStep: 273c4b90-9c17-4fce-a025-2c3c6906ac75:Create
    WorkflowStep: 34f3d376-276b-497b-911d-23cbc6deec85:Ready
    

Handling parts

The activity has now been sub-assigned to a technician and been moved the ready step. This means that the technician can start working on the activity. One of the first things a technician would do is to get hold of the parts that has been registered on the activity. Lets start by getting a list of the actions that can be performed on the part we ordered earlier. This is done by using the GetAvailableFunctions.


    private static void HandlePart()
    {
        //We only have one requisition line so doing a FirstOrDefault query should be sufficient
        string requisitionId=_activityModel.FirstOrDefault(p=>p.AttributeType==5).AttributeData.CombinedRequisitionLineId;
        //Lets start by getting a list of available functions
        dynamic availableFunctionReturnModel = api.GetAvailableFunctions(_ticketId, _combinedActivityId, requisitionId).Result;
        //print information about each available function
        Console.WriteLine("Available functions start");
        foreach (var availableFunction in availableFunctionReturnModel.AvailableFunctions)
        {
            PrintAvailableFunction(availableFunction);
        }
        Console.WriteLine("Available functions ends");
    }
    

The code for sending the request to server looks like this


    public async Task GetAvailableFunctions(string ticketId, string combinedActivityId, string combinedRequisitionLineId)
    {
        dynamic requisitionLineArgumenModel = new
        {
            TicketId = ticketId,
            CombinedActivityId = combinedActivityId,
            CombinedRequisitionLineId = combinedRequisitionLineId,
        };
        return await ExecuteApiFunction(requisitionLineArgumenModel, "Lookup", "GetAvailableFunctions");
    }
    

Lets look at the details about each function. Note that the printout does not print out all rules regarding a requisition function, for a complete discussion of all the rules see AvailableFunctionReturnModel.


     Available functions start
     stocOkToTecOk:Rcv from Local Stock
        Possible status values: ok
        Suggested status: ok
        Possible locations: ToLocationRule (0)
        stock: 1040(subno01)
        Suggested location: 1040(subno01)
        Can part number be changed : False
        Suggested part number : d200
        Max quantity : 1
     Rollback:Undo last function
        Possible status values:
        Suggested status:
        Possible locations: ToLocationRule (0)
        Suggested location: ()
        Can part number be changed : False
        Suggested part number : d200
        Max quantity : 1
     Available functions ends
    

Applying a requisition function is really about changing one of these 4 key elements

A function definition tells us if which of these fields we can change and how we are allowed to change them. In the printout about we have two allowable functions, stockokToTec and Rollback. The last function is not really an actual function, applying this function will rollback the previous function that was applied. The other function however, stockokToTec (Receive from local stock), is a real function. You can see that status can only be set to "ok" this is also the suggested value. This part is only available in one stock (1040) and in one location (subno01). The suggested location has been set to this value. You are not allowed to change the part number. Since we only ordered one part maximum quantity is 1.

This means that in order to register that we have received the part from local stock we need to add RequisitionLineEventModel that sets PartNummer to d200, ToLoaction to stock 1040 and location to subno01, Status to "ok" and Quantity to 1

Handling cost lines

An activity can accumulate various costs and the Cost Line module can be used to register and update these costs. By using the lookup GetAvailableCostSetDefinitions we can find a cost definition, or "template" which contains information about what type of cost it is, service and line codes, and other info. The returned model contains a collection of Cost Set Definition Models which in turn contains a collection of Cost Line Definition Models


        private static void AddCostLine()
        {
            //Start by getting all available cost definitions or cost "templates" to base the new cost line on
            dynamic allCostSetDefinitions = api.GetAvailableCostSetDefinitions(_ticketId, _combinedActivityId).Result;

            //Printing info for each set and its 5 most used lines
            foreach (var set in allCostSetDefinitions.CostSetDefinitionModels) { 
                Console.WriteLine("Got cost set definition " + set.CostSetDefinitionName + ". It has " + 
                    set.CostLineDefinitions.Count + " different lines to choose from");
                for (int a = 0; a < 5; a++) { 
                    Console.WriteLine("   Got line: " + set.CostLineDefinitions[a].CostLineDefinitionName +
                        ", Type " + set.CostLineDefinitions[a].CostLineType +
                        ((bool)set.CostLineDefinitions[a].IsDescriptionRequired ? ", Description is required" : "")  +
                        ", used " + set.CostLineDefinitions[a].UsageCount + " times before");
                }
            }
            //Lets find the definition for the Expenses cost in the set "Recall - 500"
            dynamic costSetDefinition = ((IEnumerable)allCostSetDefinitions.CostSetDefinitionModels)
                .FirstOrDefault(a => a.CostSetDefinitionName.Value.Contains("500"));
            dynamic costLineDefinition = ((IEnumerable)costSetDefinition.CostLineDefinitions)
                .FirstOrDefault(a => a.CostLineDefinitionName.Value.Contains("Expenses"));

            Console.WriteLine("Making an update to add the cost " + costLineDefinition.CostLineDefinitionName + 
                " with the definition Id " + costLineDefinition.CostLineDefinitionId);

            //Making an update list with one update
            List update = new List() {
                new {
                    AttributeAction = 0, //Add
                    AttributeType = 4, //Cost line
                    AttributeData = new
                    {
                        CostLineDefinitionId = costLineDefinition.CostLineDefinitionId //The definition Id for the line to be added
                    }
                }
            };

            //Sending the update and saving the result
            dynamic result = api.UpdateActivity(_ticketId, null, _combinedActivityId, update).Result;

            Console.WriteLine("Successfully added cost line");
            PrintAttributeModel(((IEnumerable)result.ActivityUpdates).ToList());

            MergeActivityModel(result);
        }
    

The top 5 lines for each set in this example is


        Got cost set definition C200 - Chargeable Time/material Onsite. It has 101 different lines to choose from
            Got line: Reise dagtid 08-16 (h15), Type 5, used 38 times before
            Got line: Kjørte kilometer (k11), Type 9, Description is required, used 38 times before
            Got line: Arbeid, dagtid (h11), Type 5, used 38 times before
            Got line: Arbeid, dagtid, kontrakt (h11k), Type 5, used 9 times before
            Got line: Diverse materiell (f1134), Type 1, used 8 times before
        Got cost set definition 500 - Recall. It has 44 different lines to choose from
            Got line: Arbeid dagtid 08-16 (tn01), Type 5, used 4 times before
            Got line: Expenses (u021), Type 2, used 1 times before
            Got line: Deler u/faktura (d01), Type 1, used 1 times before
            Got line: Deleforbruk u/retur u/SerNr. (d03), Type 1, used 0 times before
            Got line: Diettgodtj,døgn, over 12 timer (u002), Type 2, used 0 times before
        Got cost set definition 138 - Vedlikeh. EDT Automatnett. It has 45 different lines to choose from
            Got line: Kjørte kilometer  -30km (NCR) (k12), Type 9, used 5 times before
            Got line: Reise dagtid ma-fre (ed45s), Type 5, used 5 times before
            Got line: Deleforbruk u/retur u/SerNr. (d03), Type 1, used 0 times before
            Got line: Diettgodtgj,døgn,8-12 t, fakt (u101), Type 2, used 0 times before
            Got line: Diettgodtj,døgn,over 12 t,fakt (u102), Type 2, used 0 times before
    

Making an Cost Line Model update to add the cost Expenses (u021) with the definition Id "9831df12-6062-9cad-11df-b1ae15c924b6"


        new {
            AttributeAction = 0, //Add
            AttributeType = 4, //Cost line
            AttributeData = new
            {
                CostLineDefinitionId = costLineDefinition.CostLineDefinitionId //The definition Id for the line to be added
            }
        }
    

When sent the result returned was


        Successfully added cost line
        CostLineId: 7ca6bec9-bc63-468e-a352-ec4bebcf726c
        CostLineDefinitionId: 9831df12-6062-9cad-11df-b1ae15c924b6
        IsDeletable: False
        Field: CreatedAt,  IsRequired: True, IsEnabled: True , Type 3 , Value: 07.11.2017 10.59.15
        Field: CreatedBy,  IsRequired: True, IsEnabled: False , Type 6 , Value: alban@subbus.no, DisplayValue: Alban Subbus
        Field: Name,  IsRequired: True, IsEnabled: False , Type 6 , Value: Expenses (u021)
        Field: Description,  IsRequired: False, IsEnabled: True , Type 6 , Value:
        Field: Quantity,  IsRequired: True, IsEnabled: True , Type 2 , Value: 0
    

Lets say our new cost line is missing some details, and construct an update to modify it using the same Cost Line Model. The cost line id was "7ca6bec9-bc63-468e-a352-ec4bebcf726c"


        private static void EditCostLine()
        {
            //Lets add a description to the new line
            string newDescription = "This is a test expense";
            //And an amount
            double newQuantity = 1234.567;

            //We need to reference the cost line by it's Id
            dynamic costLineModel = _activityModel.FirstOrDefault(a => a.AttributeName.Value.Contains("CostLine")).AttributeData;
            Console.WriteLine("Modifying CostLineId: " + costLineModel.CostLineId);

            //Extracting the Fields collection
            List fields = ((IEnumerable)costLineModel.Fields).ToList();
            //Set our new values as field updates in the costLineModel.Fields list
            fields.FirstOrDefault(p => p.AttributeData.FieldName == "Description").AttributeData.FieldValue = newDescription;
            fields.FirstOrDefault(p => p.AttributeData.FieldName == "Quantity").AttributeData.FieldValue = newQuantity;

            //Preparing an update list with one cost line update
            List update = new List() {
                new {
                    AttributeAction = 1, //Modify
                    AttributeType = 4, //Cost line
                    AttributeData = new
                    {
                        CostLineId = costLineModel.CostLineId, //The Id for the line to be edited
                        Fields = fields
                    }
                }
            };

            //Sending the update and saving the result
            dynamic result = api.UpdateActivity(_ticketId, null, _combinedActivityId, update).Result;

            Console.WriteLine("Successfully modified the cost line");
            PrintAttributeModel(((IEnumerable)result.ActivityUpdates).ToList());

            //The result will only contain the two fields that was changed, so we need to merge these changes back to our costLineModel.
            //  It's especially important to update our local quantity info since the server has rounding rules and will modify our input.
            foreach (dynamic fieldUpdate in ((IEnumerable)result.ActivityUpdates[0].AttributeData.Fields))
            {
                //Copy the fieldUpdate's .AttributeData into the corresponding field in costLineModel.Fields
                fields.FirstOrDefault(p => p.AttributeData.FieldName == fieldUpdate.AttributeData.FieldName.ToString())
                    .AttributeData = fieldUpdate.AttributeData;
            }

            //Pushing the updated costLineModel back into our _activityModel collection
            _activityModel.FirstOrDefault(a => a.AttributeName == "CostLine." + costLineModel.CostLineId).AttributeData = costLineModel;

            PrintActivityModel();
        }
    

The results is printed to the console


        Modifying CostLineId: 7ca6bec9-bc63-468e-a352-ec4bebcf726c
        Successfully modified the cost line
        CostLineId: 7ca6bec9-bc63-468e-a352-ec4bebcf726c
        CostLineDefinitionId: 9831df12-6062-9cad-11df-b1ae15c924b6
        IsDeletable: False
        Field: CreatedAt,  IsRequired: True, IsEnabled: True , Type 3 , Value: 07.11.2017 10.59.15
        Field: Description,  IsRequired: False, IsEnabled: True , Type 6 , Value: This is a test expense
        Field: Quantity,  IsRequired: True, IsEnabled: True , Type 2 , Value: 1234,57
    

Adding attachment

Adding attachments is a two step process. First you have to upload the file using the UploadFile to the server. This method will return a AttachmentModel which you can use to construct a normal activity update.

So lets have a look at the code


        private static void UploadFile()
        {
            //start by uploading a file
            dynamic result=api.UploadFile(_ticketId, "MyTest", "This is a nice description", "c:\\temp\\uploadme.txt").Result;
            //get the attachment id
            Console.WriteLine("AttachmentId: " + result.AttachmentId.ToString());
            //do an activity update to associate the attachment with your activity.
            List  updates = new List();
            //create attachment model
            updates.Add(CreateNewAttachment(result));
            dynamic updateReturnModel = api.UpdateActivity(_ticketId, null, _combinedActivityId, updates).Result;
            //merge result with out activity model
            MergeActivityModel(updateReturnModel);
            //get allowable actions
            _allowableActions = updateReturnModel.AllowableActions;
            //print activity model to console
            PrintActivityModel();
         }
    

The method that changes the result from UploadFile to an activity update model looks like this


        private static dynamic CreateNewAttachment(dynamic attachmentModel)
        {
            return new
            {
                AttributeAction = 0, //0 is Add
                AttributeType = 9, //9 is AttachmentModel
                AttributeData = attachmentModel
            };
        }
    

The results is printed to the console. The highlighted line show the added attachment.

    
        Attachment: CustomerCopy, Type: 3 (application/pdf)
        CustomeEvent:ActivityClosedRework
        CustomeEvent:Oppdraget er mottatt
        WorkflowStep: ec6a2d26-fbbc-4fca-81b8-95f3feaf37d7:Create
        WorkflowStep: 52db9ef7-1546-4a37-a74c-549985e64258:Assign
        Attachment: MyTest, Type: 0 (text/plain)
        Allowable actions: Add comments Add cost lines Add parts
        ------------------------ Upload file complete ----------------------------