Field Service Technician Service Reporting Development Guide

Microsoft have documentation covering their service reporting functionality – Field Service technician service reporting | Microsoft Docs. You will first need to download the reporting solution provided by Microsoft and install it into your environment. Next you will need to download the PCF Reporting solution and extract it into a local folder of your choosing. (I will be using this source code as a base within this blog post)


PCF Development


What I use to develop PCF’s is the PCF Builder on the XRMToolBox. Once you have the tool downloaded you will need to select the ‘ReportPCF’ folder


Once the folder is selected press the ‘Reload Details’ button


If you press the ‘Build’ button you will find that this error message appears in the console window.

To fix this you need to press the ‘Open in VS Code’. This will open VS Code if you have it installed.


Within the Terminal enter the command ‘npm install’. This should add a new folder called ‘node_modules’ within the PCF directory.


If you now press ‘Build’, it should now succeed.


Below I will give examples on how to retrieve data and display it on a report. I would recommend to use the Microsoft Report provided as a base.


Displaying data from a Lookup


Using retrieveRecord


GetReportData.ts

public getResourceInfo = async (): Promise < Resource > => {
    let resourceInfo;
    if (!this.resourceID) return resourceInfo;
    const data = await this.context.webAPI.retrieveRecord(
        "bookableresource",
        this.resourceID,
        `?$select=msdyn_hourlyrate,name,resourcetype`
    )

    if (data) {
        resourceInfo = {
            name: data.name,
            msdyn_hourlyrate: data.msdyn_hourlyrate,
            resourcetype: data["resourcetype@OData.Community.Display.V1.FormattedValue"]
        };
    }
    return resourceInfo;
}

SampleReport.tsx

<SectionTitle>Resource Lookup Information</SectionTitle>
<FieldInfo name="Name" value={resourceInfo?.name} valueStyles={styles.singleColValue}></FieldInfo>
<FieldInfo name="Type" value={resourceInfo?.resourcetype} valueStyles={styles.singleColValue}></FieldInfo>
<FieldInfo name="Hourly Rate" value={resourceInfo?.msdyn_hourlyrate} valueStyles={styles.singleColValue}></FieldInfo>

Using retrieveRecord with Expand


GetReportData.ts

public getBookingData = async (): Promise < Booking > => {
    let booking;
    const data = await this.context.webAPI.retrieveRecord(
        "bookableresourcebooking",
        this.bookingID,
        "?$expand=Resource($select=msdyn_hourlyrate,name,resourcetype)"
    )

    if (data) {
        booking = {
            name: data.name,
            starttime: data.starttime,
            endtime: data.endtime,
            duration: data.duration,
            resourcename: data?.Resource?.name,
            msdyn_hourlyrate: data.Resource?.msdyn_hourlyrate,
            resourcetype: data.Resource?.["resourcetype@OData.Community.Display.V1.FormattedValue"]
        };
    }

    return booking;
}

SampleReport.tsx

<SectionTitle>Resource Expand Information</SectionTitle>
<FieldInfo name="Name" value={booking?.resourcename} valueStyles={styles.singleColValue}></FieldInfo>
<FieldInfo name="Type" value={booking?.resourcetype} valueStyles={styles.singleColValue}></FieldInfo>
<FieldInfo name="Hourly Rate" value={booking?.msdyn_hourlyrate} valueStyles={styles.singleColValue}></FieldInfo>

Output:

Displaying a list of data


Using retrieveMultipleRecords:


GetReportData.ts

public getBookingTimestamps = async (): Promise < Array < BookingTimestamps >> => {
    let timestamps = [];

    const timestampsData = await this.context.webAPI.retrieveMultipleRecords(
        "msdyn_bookingtimestamp",
        "?$select=msdyn_bookingtimestampid,msdyn_systemstatus,msdyn_timestamptime&$filter=_msdyn_booking_value eq " + this.bookingID
    )

    if (timestampsData) {
        timestamps = timestampsData.entities;
        timestamps.map((timestamp) => ({
            msdyn_bookingtimestampid: timestamp.msdyn_bookingtimestampid,
            msdyn_systemstatus: timestamp["msdyn_systemstatus@OData.Community.Display.V1.FormattedValue"],
            msdyn_timestamptime: timestamp["msdyn_timestamptime@OData.Community.Display.V1.FormattedValue"]
        }))
    }

    return timestamps;
}

SampleReport.tsx

<SectionTitle>Booking Timestamps</SectionTitle>;
{
  bookingTimestamps.map((bookingTimestamp) => (
    <FieldInfo
      key={bookingTimestamp.msdyn_bookingtimestampid}
      name={`${bookingTimestamp["msdyn_timestamptime@OData.Community.Display.V1.FormattedValue"]} - ${bookingTimestamp["msdyn_systemstatus@OData.Community.Display.V1.FormattedValue"]}`}
      hideValue={true}
    ></FieldInfo>
  ));
}

Using retrieveRecord with Expand:


GetReportData.ts

public getBookingTimestampsExpand = async (): Promise < Array < BookingTimestampsExpand >> => {
    let timestamps = [];
    if (!this.bookingID) return timestamps;

    const timestampsData = await this.context.webAPI.retrieveRecord(
        "bookableresourcebooking",
        this.bookingID,
        "?$expand=msdyn_bookableresourcebooking_msdyn_bookingtimestamp_Booking($select=msdyn_bookingtimestampid,msdyn_systemstatus,msdyn_timestamptime)"
    )

    if (timestampsData && timestampsData.msdyn_bookableresourcebooking_msdyn_bookingtimestamp_Booking) {
        timestamps = timestampsData.msdyn_bookableresourcebooking_msdyn_bookingtimestamp_Booking;
        timestamps = timestamps.map((timestamp) => ({
            msdyn_bookingtimestampid: timestamp.msdyn_bookingtimestampid,
            msdyn_systemstatus: timestamp["msdyn_systemstatus@OData.Community.Display.V1.FormattedValue"],
            msdyn_timestamptime: timestamp["msdyn_timestamptime@OData.Community.Display.V1.FormattedValue"]
        }))
    }
    return timestamps;
}

SampleReport.tsx

<SectionTitle>Booking Timestamps Expand</SectionTitle>;
{
  bookingTimestampsExpand.map((bookingTimestamp) => (
    <FieldInfo
      key={bookingTimestamp.msdyn_bookingtimestampid}
      name={`${bookingTimestamp.msdyn_timestamptime} - ${bookingTimestamp.msdyn_systemstatus}`}
      hideValue={true}
    ></FieldInfo>
  ));
}

Output:


Displaying an Image


Using retrieveMultipleRecords


Bookable Resource Booking Quick Notes

Microsoft have added a new entity called ‘Bookable Resource Booking Quick Notes’, which is a note adding experience made for Field Service Mobile. If you are adding notes in this section it will not be added to the Timeline or the Notes entity. This is a separate entity called msdyn_bookableresourcebookingquicknote


GetReportData.ts

public getNotes = async (): Promise < Array < Notes >> => {
    let notes = [];

    const notesData = await this.context.webAPI.retrieveMultipleRecords(
        "msdyn_bookableresourcebookingquicknote",
        "?$select=msdyn_bookableresourcebookingquicknoteid,msdyn_imageid,msdyn_text&$filter=_msdyn_quicknote_lookup_entity_value eq " + this.bookingID
    )

    if (notesData) {
        notes = notesData.entities;
        notes.map((note) => ({
            msdyn_bookableresourcebookingquicknoteid: note.msdyn_bookableresourcebookingquicknoteid,
            msdyn_imageid: note.msdyn_imageid,
            msdyn_text: note.msdyn_text
        }))
    }

    return notes;
}

SampleReport.tsx

I have added another argument in the ‘FieldInfo’ constant called ‘image’ which will allow us to display the quick note images on the report.


As the image is stored as a url string we will need to create an img tag like below:

<img src={ (image != null) ? image+"&Full=true" : ""}/>

const FieldInfo = ({ name, value, valueStyles, nameStyles, hideValue, image, annotation, formimage}: FieldInfoProps) => (
  <div style={styles.fieldBox}>
    <span style={nameStyles || (hideValue !== true) ? styles.name : styles.value}>
      {name}{((hideValue !== true)) && ":"}</span>
    {hideValue !== true && (<span style={valueStyles || styles.value}>{value}</span>)}

    <img src={ (image != null) ? image+"&Full=true" : ""}/>
    <img src={ (annotation != null) ? `data:image/png;base64,${annotation}` : ""}/>
    <img src={ (formimage != null) ? formimage : ""}/>
  </div>
);

<SectionTitle>Notes Quick Notes</SectionTitle>;
{
  notes.map((note) => (
    <FieldInfo
      key={note.msdyn_bookableresourcebookingquicknoteid}
      name={note.msdyn_text}
      image={note.msdyn_image_url}
      hideValue={true}
    ></FieldInfo>
  ));
}

Using retrieveMultipleRecords


Notes/Annotations

Below is an example of reading and displaying regular annotations from a timeline.


GetReportData.ts

public getAnnotations = async (): Promise < Array < Annotations >> => {
    let annotations = [];

    const annotationsData = await this.context.webAPI.retrieveMultipleRecords(
        "annotation",
        "?$select=annotationid,documentbody,notetext&$filter=_objectid_value eq " + this.bookingID
    )

    if (annotationsData) {
        annotations = annotationsData.entities;
        annotations.map((note) => ({
            annotationid: note.annotationid,
            documentbody: note.documentbody,
            notetext: note.notetext
        }))
    }

    return annotations;
}

SampleReport.tsx

I have added another argument in the ‘FieldInfo’ constant called ‘annotation’ which will allow us to display annotations on the report.

As the image is stored as a base64 string we will need to create an img tag like below:

<img src={ (annotation != null) ? `data:image/png;base64,${annotation}` : ""}/>

const FieldInfo = ({ name, value, valueStyles, nameStyles, hideValue, image, annotation, formimage}: FieldInfoProps) => (
  <div style={styles.fieldBox}>
    <span style={nameStyles || (hideValue !== true) ? styles.name : styles.value}>
      {name}{((hideValue !== true)) && ":"}</span>
    {hideValue !== true && (<span style={valueStyles || styles.value}>{value}</span>)}

    <img src={ (image != null) ? image+"&Full=true" : ""}/>
    <img src={ (annotation != null) ? `data:image/png;base64,${annotation}` : ""}/>
    <img src={ (formimage != null) ? formimage : ""}/>
  </div>
);

<SectionTitle>Notes Annotations</SectionTitle>;
{
  annotations.map((annotation) => (
    <FieldInfo
      key={annotation.annotationid}
      name={annotation.notetext}
      annotation={annotation.documentbody}
      hideValue={true}
    ></FieldInfo>
  ));
}

Output:


Field type Cheat Sheet


Below is a list of all the field types within Dynamics 365. It will give examples on how to read and display data in TypeScript for each field type:

I have created a new entity called ‘Field Type’ and have added each type of field to the entity. Below I am fetching each field type and displaying them to the report.


GetReportData.ts

public getFieldTypes = async (): Promise < FieldType > => {
    let fieldTypes;
    const data = await this.context.webAPI.retrieveRecord(
        "new_fieldtype",
        this.fieldType,
        `?$select=new_currency,_new_customer_value,new_dateandtime,new_dateonly,new_decimalnumber,new_floatingpointnumber,new_imageid,_new_lookup_value,new_multiplelinesoftext,new_name,new_optionset,new_singlelineoftext,new_twooptions,new_wholenumber,statecode,statuscode,new_multiselectoptionset,new_fieldtypeid`
    )

    if (data) {
        fieldTypes = {

            //Currency
            new_currency: data.new_currency,
            new_currency_formatted: data["new_currency@OData.Community.Display.V1.FormattedValue"],

            //Customer
            _new_customer_value: data["_new_customer_value"],
            _new_customer_value_formatted: data["_new_customer_value@OData.Community.Display.V1.FormattedValue"],
            _new_customer_value_lookuplogicalname: data["_new_customer_value@Microsoft.Dynamics.CRM.lookuplogicalname"],

            //Date Time
            new_dateandtime: data.new_dateandtime,
            new_dateandtime_formatted: data["new_dateandtime@OData.Community.Display.V1.FormattedValue"],

            //Date Only
            new_dateonly: data.new_dateonly,
            new_dateonly_formatted: data["new_dateonly@OData.Community.Display.V1.FormattedValue"],

            //Decimal
            new_decimalnumber: data.new_decimalnumber,
            new_decimalnumber_formatted: data["new_decimalnumber@OData.Community.Display.V1.FormattedValue"],

            //Floating Point
            new_floatingpointnumber: data.new_floatingpointnumber,
            new_floatingpointnumber_formatted: data["new_floatingpointnumber@OData.Community.Display.V1.FormattedValue"],

            //Image
            new_imageid: data.new_imageid,

            //Lookup
            _new_lookup_value: data._new_lookup_value,
            _new_lookup_value_formatted: data["_new_lookup_value@OData.Community.Display.V1.FormattedValue"],
            _new_lookup_value_lookuplogicalname: data["_new_lookup_value@Microsoft.Dynamics.CRM.lookuplogicalname"],

            //Multiple Lines of Text
            new_multiplelinesoftext: data.new_multiplelinesoftext,

            //Option Set
            new_optionset: data.new_optionset,
            new_optionset_formatted: data["new_optionset@OData.Community.Display.V1.FormattedValue"],

            //Single Line of Text
            new_singlelineoftext: data.new_singlelineoftext,

            //Two Options
            new_twooptions: data.new_twooptions,
            new_twooptions_formatted: data["new_twooptions@OData.Community.Display.V1.FormattedValue"],

            //Whole Number
            new_wholenumber: data.new_wholenumber,
            new_wholenumber_formatted: data["new_wholenumber@OData.Community.Display.V1.FormattedValue"],

            //Status Reason
            statecode: data.statecode,
            statecode_formatted: data["statecode@OData.Community.Display.V1.FormattedValue"],

            //State
            statuscode: data.statuscode,

            //MultiSelect Option Set
            new_multiselectoptionset: data.new_multiselectoptionset,
            new_multiselectoptionset_formatted: data["new_multiselectoptionset@OData.Community.Display.V1.FormattedValue"],

            //Field Type Id
            new_fieldtypeid: data.new_fieldtypeid
        };
    }
    return fieldTypes;
}

SampleReport.tsx

<SectionTitle>Field Type Information</SectionTitle>
<FieldInfo name="Currency" value={FieldType?.new_currency} valueStyles={styles.singleColValue}></FieldInfo>
<FieldInfo name="Currency Formatted" value={FieldType?.new_currency_formatted} valueStyles={styles.singleColValue}></FieldInfo>

<FieldInfo name="Customer" value={FieldType?._new_customer_value} valueStyles={styles.singleColValue}></FieldInfo>
<FieldInfo name="Customer Formatted" value={FieldType?._new_customer_value_formatted} valueStyles={styles.singleColValue}></FieldInfo>
<FieldInfo name="Customer Logical Name" value={FieldType?._new_customer_value_lookuplogicalname} valueStyles={styles.singleColValue}></FieldInfo>

<FieldInfo name="Date and Time" value={FieldType?.new_dateandtime} valueStyles={styles.singleColValue}></FieldInfo>
<FieldInfo name="Date and Time Formatted" value={FieldType?.new_dateandtime_formatted} valueStyles={styles.singleColValue}></FieldInfo>

<FieldInfo name="Date Only" value={FieldType?.new_dateonly} valueStyles={styles.singleColValue}></FieldInfo>
<FieldInfo name="Date Only Formatted" value={FieldType?.new_dateonly_formatted} valueStyles={styles.singleColValue}></FieldInfo>

<FieldInfo name="Decimal Number" value={FieldType?.new_decimalnumber} valueStyles={styles.singleColValue}></FieldInfo>
<FieldInfo name="Decimal Number Formatted" value={FieldType?.new_decimalnumber_formatted} valueStyles={styles.singleColValue}></FieldInfo>

<FieldInfo name="Floating Point Number" value={FieldType?.new_floatingpointnumber} valueStyles={styles.singleColValue}></FieldInfo>
<FieldInfo name="Floating Point Number Formatted" value={FieldType?.new_floatingpointnumber_formatted} valueStyles={styles.singleColValue}></FieldInfo>

<FieldInfo name="Image" value={FieldType?.new_imageid} valueStyles={styles.singleColValue}></FieldInfo>
<FieldInfo name="Image Formatted" formimage={"/Image/download.aspx?Entity=new_fieldtype&Attribute=new_image&Id=" + FieldType?.new_fieldtypeid} valueStyles={styles.singleColValue}></FieldInfo>

<FieldInfo name="Lookup" value={FieldType?._new_lookup_value} valueStyles={styles.singleColValue}></FieldInfo>
<FieldInfo name="Lookup Formatted" value={FieldType?._new_lookup_value_formatted} valueStyles={styles.singleColValue}></FieldInfo>
<FieldInfo name="Lookup Logical Name" value={FieldType?._new_lookup_value_lookuplogicalname} valueStyles={styles.singleColValue}></FieldInfo>

<FieldInfo name="Multiple Lines of Text" value={FieldType?.new_multiplelinesoftext} valueStyles={styles.singleColValue}></FieldInfo>

<FieldInfo name="Option Set" value={FieldType?.new_optionset} valueStyles={styles.singleColValue}></FieldInfo>
<FieldInfo name="Option Set Formatted" value={FieldType?.new_optionset_formatted} valueStyles={styles.singleColValue}></FieldInfo>

<FieldInfo name="Single Line of Text" value={FieldType?.new_singlelineoftext} valueStyles={styles.singleColValue}></FieldInfo>

<FieldInfo name="Two Options" value={FieldType?.new_twooptions} valueStyles={styles.singleColValue}></FieldInfo>
<FieldInfo name="Two Options Formatted" value={FieldType?.new_twooptions_formatted} valueStyles={styles.singleColValue}></FieldInfo>

<FieldInfo name="Whole Number" value={FieldType?.new_wholenumber} valueStyles={styles.singleColValue}></FieldInfo>
<FieldInfo name="Whole Number Formatted" value={FieldType?.new_wholenumber_formatted} valueStyles={styles.singleColValue}></FieldInfo>

<FieldInfo name="State Code" value={FieldType?.statecode} valueStyles={styles.singleColValue}></FieldInfo>
<FieldInfo name="State Code Formatted" value={FieldType?.statecode_formatted} valueStyles={styles.singleColValue}></FieldInfo>

<FieldInfo name="Status Code" value={FieldType?.statuscode} valueStyles={styles.singleColValue}></FieldInfo>

<FieldInfo name="MultiSelect Option Set" value={FieldType?.new_multiselectoptionset} valueStyles={styles.singleColValue}></FieldInfo>
<FieldInfo name="MultiSelect Option Set Formatted" value={FieldType?.new_multiselectoptionset_formatted} valueStyles={styles.singleColValue}></FieldInfo>

Below is the report output:

If you followed my guide closely, you may have noticed that Visual Studio Code will display errors telling you that you are missing certain code. I have not added all the source code needed to this post as it would be unnecessary. I would suggest understanding the code to get a better understanding or you can ask for help in the comments below and I will answer it as soon as I can.

Hopefully this helped you!

5 thoughts on “Field Service Technician Service Reporting Development Guide

  1. This is an amazing post, thanks for sharing your experience. In almost every second implementation of Dynamics 365 Field Service, there is a requirement to generate some kind of report. Its really awesome that this is now out-of-the-box and also supports further changes/customisation.

  2. Hey, many thanks for this information about the report, we have a slight problem, as we need this report to be send to the customer, we would like it to look ok.. but on printing the report to pdf it just cuts of on the page break even just in the middle of the signature or a picutre. do you have any idea on how to implement a flexible page break or even a static one to make the picture a bit nicer? kind regards, Ilse

  3. Hi Thomas,
    Have you found any information about saving the PCF report to PDF?

    There is an example in demo solution from Microsoft site, but there is no documentation about it, and it is a managed solution, so we are unable to take code from it

Leave a Reply to Thomas Dayman Cancel reply

Your email address will not be published. Required fields are marked *