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!
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.
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
Hi IIse, sorry I havent had much experience with dealing with a report with multiple pages. But I think this link might help you – https://blog.logrocket.com/generating-pdfs-react/
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
Hi Vladimir,
I have some experience of converting reports into PDF’s but it might be slightly different to what you are expecting. https://www.daymandynamics.com/pcf-email-report/ I am converting a SSRS report into a PDF attachment. Not sure if this will help you be you can inspect the source code for ideas