Extension
"Extend the extension" is a pattern comparable to Matryoshka dolls, where each doll fits inside a larger one, decreasing in size progressively. Using functionality that was developed previously, you can extend that extension with your own logic. That way you get a stack of functionality that only contains the business logic required to serve one particular purpose.
The extension for Dime.Scheduler in Business Central is wafer thin and only adds the necessary components to get you started quickly. While it is a batteries included experience, this extension was made for customizations by our partners.
Requirements
To extend the extension, you must create a blank extension or use the one you already have. To do that, you must have:
- A Business Central tenant to develop and test
- VS Code
- The AL Language extension
Whilst not strictly necessary, we also recommend the following two:
Adding the dependency
The app.json
file is the place to be to add the standard Dime.Scheduler extension as a dependency. Specifically, in the dependencies
array, add Dime.Scheduler:
"dependencies": [{
"id": "f15bbe6d-c2a3-4828-aa98-5a77cff4cd41",
"publisher": "Dime",
"name": "Dime.Scheduler",
"version": "2.3.0.0"
}
]
Here's a typical example of the entire app.json
that we create for our customers:
{
"id": "d9c75381-bb78-4de7-81d3-7db784177465",
"name": "Customer XYZ Dime.Scheduler Customization",
"publisher": "Dime Software",
"version": "1.0.0.0",
"brief": "This extension adds custom behavior for Customer XYZ to the standard Business Central Dime.Scheduler connector",
"description": "This extension adds custom behavior for Customer XYZ to the standard Business Central Dime.Scheduler connector",
"privacyStatement": "https://www.dimescheduler.com/privacy",
"EULA": "https://docs.dimescheduler.com/",
"help": "https://docs.dimescheduler.com/",
"url": "https://www.dimescheduler.com/",
"logo": "./assets/logo.png",
"dependencies": [{
"id": "f15bbe6d-c2a3-4828-aa98-5a77cff4cd41",
"publisher": "Dime",
"name": "Dime.Scheduler",
"version": "2.3.0.0"
}
],
"screenshots": [],
"platform": "21.0.0.0",
"application": "21.0.0.0",
"idRanges": [
{
"from": 50000,
"to": 50200
}
],
"contextSensitiveHelpUrl": "https://docs.dimescheduler.com/",
"resourceExposurePolicy": {
"allowDebugging": true,
"allowDownloadingSource": false,
"includeSourceInSymbolFile": false
},
"runtime":"10.0"
}
Custom development
What you want to do next is entirely up to you. This may vary from adding new tables, extending existing ones, or simply adding your own code unit to handle incoming appointments from Dime.Scheduler. In the next subsections, we'll go over some examples to illustrate how you can extend the extension.
Extend Dime.Scheduler artifacts
The standard extension for Dime.Scheduler in Business Central adds few new objects to Business Central. One essential part is the setup page, which exposes standard functionality to quickly link tables from BC to Dime.Scheduler. This table can be customized with your own fields. It is not required to extend Dime.Scheduler's artifacts, but you are free to do so as you see fit.
Here's an example that adds a few fields to the service order module in the setup page:
tableextension 50161 "MYCUSTOMER DS Setup" extends "Dime DS Setup"
{
fields
{
field(50000; "MYCUSTOMER DS Initial Repair Status"; Code[10])
{
Caption = 'Initial Repair Status Code';
TableRelation = "Repair Status".Code;
ValidateTableRelation = true;
}
field(50001; "MYCUSTOMER DS Planned Repair Status"; Code[10])
{
Caption = 'Planned Repair Status Code';
TableRelation = "Repair Status".Code;
ValidateTableRelation = true;
}
field(50002; "MYCUSTOMER DS Replan Repair Status"; Code[10])
{
Caption = 'Replan Repair Status Code';
TableRelation = "Repair Status".Code;
ValidateTableRelation = true;
}
}
}
pageextension 50161 "MYCUSTOMER DS Setup" extends "Dime DS Setup"
{
layout
{
addafter(Testing)
{
group(MYCUSTOMER)
{
Caption = 'MYCUSTOMER';
field("MYCUSTOMER DS Initial Repair Status"; Rec."MYCUSTOMER DS Initial Repair Status")
{
ApplicationArea = All;
}
field("MYCUSTOMER DS Planned Repair Status"; Rec."MYCUSTOMER DS Planned Repair Status")
{
ApplicationArea = All;
}
field("MYCUSTOMER DS Replan Repair Status"; Rec."MYCUSTOMER DS Replan Repair Status")
{
ApplicationArea = All;
}
}
}
}
}
This, by itself, won't change the message flow from BC to Dime.Scheduler. You'll have to tap into the right events to send the additional information to Dime.Scheduler. See here for the list of events and below for an example of how to use an event listener to send additional information to Dime.Scheduler.
Extend standard tables
A common pattern is to add additional configuration to standard objects like the Resource Card
.
pageextension 50160 "MYCUSTOMER DS Resource Card" extends "Resource Card"
{
layout
{
addafter("Dime DS Enable Exchange")
{
field("MYCUSTOMER DS Planning Duration"; Rec."MYCUSTOMER DS Planning Duration")
{
ApplicationArea = All;
}
}
}
}
Subscribe to events
Here's a code unit that listens to a couple of events that Dime.Scheduler raises:
OnBeforeSendResourceAsResourceToDimeScheduler
adds logic to control which appointments are to be sent to Dime.Scheduler.OnBeforeSendServiceHeaderAsJobToDimeScheduler
maps custom fields to Dime.Scheduler free fields.OnBeforeSendServiceItemLineAsTaskToDimeScheduler
adds a field to one of Dime.Scheduler's free fields.
codeunit 50305 "DS MYCUSTOMER Events Subscriber"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Dime DS Events", 'OnBeforeSendResourceAsResourceToDimeScheduler', '', false, false)]
local procedure FilterOnResourceGroup(var Resource: Record Resource; var DsWebServMgt: Codeunit "Dime DS Web Service Management")
var
ResourceGroup: Record "Resource Group";
Employee: Record "Employee";
DimeDSDimeSchedulerMgt: Codeunit "Dime DS Dime.Scheduler Mgt.";
NotSendResourceGroup: Boolean;
begin
if ResourceGroup.Get(Resource."Resource Group No.") then
NotSendResourceGroup := not (ResourceGroup."DS MYCUSTOMER Send To DS")
else
NotSendResourceGroup := true;
DsWebServMgt.ReplaceParameter('DoNotShow', DSWebServMgt.HandleBool((Resource.Blocked) or (Resource."Dime DS Hide In Planning Board") or (NotSendResourceGroup)));
// If there's no employee, use the resource information
Employee.SetRange("Resource No.", Resource."No.");
if not Employee.FindFirst() then begin
DSWebServMgt.ReplaceParameter('HomeAddress', Resource.Address + ', ' + Resource."Address 2" + ', ' + Resource."Post Code" + ', ' + Resource.City);
DSWebServMgt.ReplaceParameter('HomeCountry', DimeDSDimeSchedulerMgt.GetCountryCode(Resource."Country/Region Code"));
DSWebServMgt.ReplaceParameter('InServiceFrom', DSWebServMgt.HandleDate(Resource."Employment Date"));
DsWebServMgt.ReplaceParameter('Email', Resource."DS MYCUSTOMER Exchange Email");
DsWebServMgt.ReplaceParameter('ExchangeIntegrationEnabled', DSWebServMgt.HandleBool(Resource."DS MYCUSTOMER Enable Exchange"));
end;
end;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Dime DS Events", 'OnBeforeSendServiceHeaderAsJobToDimeScheduler', '', false, false)]
local procedure SendServiceHeader(var DsWebServMgt: Codeunit "Dime DS Web Service Management"; var ServiceHeader: Record "Service Header")
begin
DsWebServMgt.ReplaceParameter('FreeText1', ServiceHeader.SomeCustomField);
DsWebServMgt.ReplaceParameter('FreeDate1', DsWebServMgt.HandleDate(ServiceHeader."Some Random Date"));
end;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Dime DS Events", 'OnBeforeSendServiceItemLineAsTaskToDimeScheduler', '', false, false)]
local procedure SendServiceItemLine(var DsWebServMgt: Codeunit "Dime DS Web Service Management"; var ServiceItemLine: Record "Service Item Line")
begin
DsWebServMgt.ReplaceParameter('FreeText1', ServiceItemLine."Service Item Group Code");
end;
}
Handling incoming appointments
Getting data from Dime.Scheduler back into BC requires very little knowledge of Dime.Scheduler, in fact. When the planner changes the schedule, Dime.Scheduler will post the appointment to the Dime.Scheduler Appointments
table. There are event listeners ready to pounce and process the data into the right table.
Here's some skeleton code that outlines the initial stages of the development of a code unit that processes incoming appointments from Dime.Scheduler. This one is setup to process appointments as service order allocation lines, but you can do whatever you want to do with it.
codeunit 50094 "MYCUSTOMER DS Functions"
{
TableNo = "Dime DS Appointment";
trigger OnRun()
begin
PerformAllocation(Rec);
end;
local procedure PerformAllocation(DimeDSAppointment: Record "Dime DS Appointment")
var
DimeDSAppointmentResource: Record "Dime DS Appointment Resource";
DimeDSServOrderAllocLnk: Record "Dime DS Serv. Order Alloc. Lnk";
DimeDSSetup: Record "Dime DS Setup";
ServHeader: Record "Service Header";
ServItemLine: Record "Service Item Line";
ServOrdAllocation: Record "Service Order Allocation";
ServAllocMgt: Codeunit ServAllocationManagement;
DimeDSHandleTimeSheet: Codeunit "Dime DS Handle Time Sheet";
LineNo: Integer;
OrderNotFoundErr: Label 'Service Order "(%1)" could not be found.', Comment = '%1 = Service Order No.';
OrderStatusErr: Label 'You cannot allocate a resource to the service order %1 because it is %2.', Comment = '%1 = Service Order Number ; %2 = Status';
AllocationExists: Boolean;
begin
// YOUR LOGIC GOES HERE
// TYPICAL STRUCTURE
// 1. VALIDATE ITEM IN BC
// 2. CHECK WHAT KIND OF PLANNING WAS DONE: DimeDSAppointment."Database Action" CAN BE "I" (insert), "U" (update), or "D" (delete)
// 3. PROCESS ACCORDINGLY SUCH AS CREATING AN ALLOCATION LINE, OR WHEN THAT ALREADY EXISTS, ENSURE A LINK IS ESTABLISHED BETWEEN THE ITEM IN BC AND DIME.SCHEDULER (e.g. "Dime DS Job Planning Line Link","Dime DS Serv. Order Alloc. Lnk")
end;
}
You may have to/want to pair this code unit with a permission set:
permissionset 50002 "MYCUSTOMER DS ALL"
{
Assignable = true;
Caption = 'Dime.Scheduler MYCUSTOMER customizations', Locked = true;
Permissions = codeunit "MYCUSTOMER DS Functions" = X;
}
The Dime.Scheduler Web Service code unit
All of the calls to the Dime.Scheduler API is done through the Dime DS Web Service Management
code unit, as you may have seen from earlier code samples. When you use the the default workflow and require only minor customizations, then you could end up with up tiny solutions like this:
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Dime DS Events", 'OnBeforeSendServiceItemLineAsTaskToDimeScheduler', '', false, false)]
local procedure SendServiceItemLine(var DsWebServMgt: Codeunit "Dime DS Web Service Management"; var ServiceItemLine: Record "Service Item Line")
begin
// Replace
DsWebServMgt.ReplaceParameter('FreeText1', ServiceItemLine."Service Item Group Code");
// Add
DSWebservMgt.AddParameter('BillNo', ServiceHeader."Bill-to Customer No.");
end;
Simply subscribe to the right event, locate the field that you want to modify, and make the necessary change in code.
It's the same if you want to deviate from the standard connector altogether. Fetch the data from BC and use the Dime DS Dime.Scheduler Mgt.
code unit. For each call to AddParameter
, the code unit will add a key/value pair to the API call to Dime.Scheduler and passes it to the import procedure of choice that you must specify in the CallDimeSchedulerWS
method call.
See below for a realistic example that overrides the standard logic and maps its custom fields to Dime.Scheduler's free fields (see highlighted rows), as well as planning on service item levels (as opposed to the default header level in service orders).
procedure SendServiceItemLineAsTask(var ServiceItemLine: Record "Service Item Line")
var
ServiceHeader: Record "Service Header";
ServContractHdr: Record "Service Contract Header";
ServItem: Record "Service Item";
Customer: Record Customer;
DimeDSDocFilterValueMgt: Codeunit "Dime DS Doc. Filter Value Mgt.";
DimeDSDimeSchedulerMgt: Codeunit "Dime DS Dime.Scheduler Mgt.";
DSMYCUSTOMERMaterialStatusMgt: Codeunit "DS MYCUSTOMER Material Status Mgt.";
RecRef: RecordRef;
ProjectObject: Record "MYCUSTOMER Project Object";
ContactName: Text[100];
ContactPhone: Text[30];
ContactEmail: Text[80];
begin
DimeDSConnectorSetup.Get();
DimeDSSetup.Get();
if not ServiceHeader.Get(ServiceItemLine."Document Type", ServiceItemLine."Document No.") then
exit;
if not ServContractHdr.Get(ServContractHdr."Contract Type"::Contract, ServiceItemLine."Contract No.") then
ServContractHdr.Init();
if not ServItem.Get(ServiceItemLine."Service Item No.") then
ServItem.Init();
// Transfer the Filter Values for the Task - uses the DS Doc. Filter Value Sources setup
RecRef.GetTable(ServiceItemLine);
RecRef.SetRecFilter();
DimeDSDocFilterValueMgt.TransferDocFilterValues(RecRef);
DSWebservMgt.InitParameters();
// Mandatory fields
DSWebservMgt.AddParameter('SourceApp', DimeDSConnectorSetup."Source App");
DSWebservMgt.AddParameter('SourceType', DimeDSDimeSchedulerMgt.GetSourceType(DATABASE::"Service Header"));
DSWebservMgt.AddParameter('TaskNo', Format(ServiceItemLine."Line No.", 0, 9));
DSWebservMgt.AddParameter('JobNo', ServiceItemLine."Document No." + '_' + format(ServiceItemLine."Line No."));
DSWebservMgt.AddParameter('ShortDescription', '');
// Map the category visual indicator
case DimeDSSetup."Service - Default Category" of
DimeDSSetup."Service - Default Category"::" ":
DSWebservMgt.AddParameter('Category', '');
DimeDSSetup."Service - Default Category"::"Repair Status":
DSWebservMgt.AddParameter('Category', ServiceItemLine."Repair Status Code");
DimeDSSetup."Service - Default Category"::"Service Order Type":
DSWebservMgt.AddParameter('Category', ServiceHeader."Service Order Type");
DimeDSSetup."Service - Default Category"::"Service Zone":
DSWebservMgt.AddParameter('Category', ServiceHeader."Service Zone Code");
end;
// Map the time marker visual indicator
case DimeDSSetup."Service - Default Timemarker" of
DimeDSSetup."Service - Default Timemarker"::" ":
DSWebservMgt.AddParameter('TimeMarker', '');
DimeDSSetup."Service - Default Timemarker"::"Repair Status":
DSWebservMgt.AddParameter('TimeMarker', ServiceItemLine."Repair Status Code");
DimeDSSetup."Service - Default Timemarker"::"Service Order Type":
DSWebservMgt.AddParameter('TimeMarker', ServiceHeader."Service Order Type");
DimeDSSetup."Service - Default Timemarker"::"Service Zone":
DSWebservMgt.AddParameter('TimeMarker', ServiceHeader."Service Zone Code");
end;
DSWebservMgt.AddParameter('Subject', '');
DSWebservMgt.AddParameter('Body', '');
DSWebservMgt.AddParameter('AppointmentTemplate', DimeDSDimeSchedulerMgt.GetAppointmentTemplate(DATABASE::"Service Header"));
// Fields that affect the color of the pin on the map when displaying the address
DSWebservMgt.AddParameter('Pin', '');
DSWebservMgt.AddParameter('Description', ServiceItemLine.Description);
DSWebservMgt.AddParameter('Type', ServiceItemLine."Service Item Group Code");
DSWebservMgt.AddParameter('ServiceNo', ServiceItemLine."Service Item No.");
DSWebservMgt.AddParameter('ServiceGroup', ServItem."Service Item Group Code");
DSWebservMgt.AddParameter('ServiceClass', ServItem."Service Price Group Code");
DSWebservMgt.AddParameter('ServiceSerialNo', ServiceItemLine."Serial No.");
DSWebservMgt.AddParameter('ServiceName', ServItem."Item Description");
DSWebservMgt.AddParameter('IRISFault', ServiceItemLine."Fault Code");
DSWebservMgt.AddParameter('IRISSymptom', ServiceItemLine."Symptom Code");
DSWebservMgt.AddParameter('IRISArea', ServiceItemLine."Fault Area Code");
DSWebservMgt.AddParameter('IRISReason', ServiceItemLine."Fault Reason Code");
DSWebservMgt.AddParameter('IRISResolution', ServiceItemLine."Resolution Code");
DSWebservMgt.AddParameter('ContractNo', ServiceItemLine."Contract No.");
DSWebservMgt.AddParameter('ContractType', ServContractHdr."Serv. Contract Acc. Gr. Code");
DSWebservMgt.AddParameter('ContractDescription', ServContractHdr.Description);
DSWebservMgt.AddParameter('ContractStartDate', DSWebservMgt.HandleDate(ServContractHdr."Starting Date"));
DSWebservMgt.AddParameter('ContractEndDate', DSWebservMgt.HandleDate(ServContractHdr."Expiration Date"));
DSWebservMgt.AddParameter('PartsWarrantyStartDate', DSWebservMgt.HandleDate(ServiceItemLine."Warranty Starting Date (Parts)"));
DSWebservMgt.AddParameter('PartsWarrantyEndDate', DSWebservMgt.HandleDate(ServiceItemLine."Warranty Ending Date (Parts)"));
DSWebservMgt.AddParameter('LaborWarrantyStartDate', DSWebservMgt.HandleDate(ServiceItemLine."Warranty Starting Date (Labor)"));
DSWebservMgt.AddParameter('LaborWarrantyEndDate', DSWebservMgt.HandleDate(ServiceItemLine."Warranty Ending Date (Labor)"));
// Map importance Indicator to custom "Material Status" field
DsWebServMgt.AddParameter('Importance', Format(DSMYCUSTOMERMaterialStatusMgt.ConvertMaterialStatusIntoImportance(ServiceItemLine."Material Status")));
DSWebservMgt.AddParameter('Status', ServiceItemLine."Repair Status Code");
DSWebservMgt.AddParameter('ExpectedResponseDateTime', DSWebservMgt.HandleDateTime(CreateDateTime(ServiceItemLine."Response Date", 0T)));
DSWebservMgt.AddParameter('ActualResponseDateTime', DSWebservMgt.HandleDateTime(CreateDateTime(ServiceItemLine."Starting Date", 0T)));
DSWebservMgt.AddParameter('RequestedStartDate', DSWebservMgt.HandleDateTime(0DT));
DSWebservMgt.AddParameter('RequestedEndDate', DSWebservMgt.HandleDateTime(0DT));
DSWebservMgt.AddParameter('ConfirmedStartDate', DSWebservMgt.HandleDateTime(0DT));
DSWebservMgt.AddParameter('ConfirmedEndDate', DSWebservMgt.HandleDateTime(0DT));
DSWebservMgt.AddParameter('ActualStartDate', DSWebservMgt.HandleDateTime(0DT));
DSWebservMgt.AddParameter('ActualEndDate', DSWebservMgt.HandleDateTime(0DT));
DSWebservMgt.AddParameter('LocationDescription', ServItem."Location of Service Item");
// Fields that affect default duration and capacity
DSWebservMgt.AddParameter('DurationInSeconds', DimeDSDimeSchedulerMgt.ConvertDecimaltoSeconds(DimeDSSetup."Default Duration Service Line"));
DSWebservMgt.AddParameter('PlanningUOM', '');
DSWebservMgt.AddParameter('PlanningUOMConversion', Format(0, 0, 9));
DSWebservMgt.AddParameter('PlanningQty', Format(0, 0, 9));
DSWebservMgt.AddParameter('UseFixPlanningQty', DSWebservMgt.HandleBool(false));
DSWebservMgt.AddParameter('RoundToUOM', DSWebservMgt.HandleBool(false));
// Fields that control if the task is an open task
DSWebservMgt.AddParameter('IsComplete', DSWebservMgt.HandleBool(false));
DSWebservMgt.AddParameter('InfiniteTask', DSWebservMgt.HandleBool(false));
DSWebservMgt.AddParameter('TaskOpenAsOf', DSWebservMgt.HandleDateTime(0DT));
DSWebservMgt.AddParameter('TaskOpenTill', DSWebservMgt.HandleDateTime(0DT));
DSWebservMgt.AddParameter('RequiredTotalDurationInSeconds', Format(0, 0, 9));
DSWebservMgt.AddParameter('RequiredNoResources', Format(0, 0, 9));
DSWebservMgt.AddParameter('DoNotCountAppointmentResource', DSWebservMgt.HandleBool(false));
// Fields to define Resource Certificates
DSWebservMgt.AddParameter('CertificateNo', '');
DSWebservMgt.AddParameter('AppointmentEarliestAllowed', DSWebservMgt.HandleDateTime(0DT));
DSWebservMgt.AddParameter('AppointmentLatestAllowed', DSWebservMgt.HandleDateTime(0DT));
DSWebservMgt.AddParameter('FreeText1', ServiceItemLine."MYCUSTOMER Project Object No.");
DSWebservMgt.AddParameter('FreeText2', format(ServiceItemLine."MYCUSTOMER Project Object Type"));
DSWebservMgt.AddParameter('FreeText3', ServiceItemLine."Item No.");
DSWebservMgt.AddParameter('FreeText4', ServiceItemLine."MYCUSTOMER VIN");
ContactName := ServiceHeader."Ship-to Contact";
ContactPhone := ServiceHeader."Ship-to Phone";
ContactEmail := ServiceHeader."Ship-to E-Mail";
if ServiceItemLine."MYCUSTOMER Project Object No." <> '' then begin
ProjectObject.Get(ServiceItemLine."MYCUSTOMER Project Object No.");
if ProjectObject."Contact Name" <> '' then
ContactName := ProjectObject."Contact Name";
if ProjectObject."Contact Phone No." <> '' then
ContactPhone := ProjectObject."Contact Phone No.";
if ProjectObject."Contact E-mail" <> '' then
ContactEmail := ProjectObject."Contact E-mail";
end;
DSWebservMgt.AddParameter('FreeText6', ContactName);
DSWebservMgt.AddParameter('FreeText7', ContactPhone);
DSWebservMgt.AddParameter('FreeText8', ContactEmail);
// Sending Links with the Task - up to 3 are available in mboc_upsertTask, use mboc_upsertTaskUrl for more
RecRef.GetTable(ServiceHeader);
DSWebservMgt.AddParameter('url1', DimeDSDimeSchedulerMgt.GeneratePageUrl(RecRef, 5900));
DSWebservMgt.AddParameter('urldesc1', 'Service Order');
// DSWebservMgt.AddParameter('url2', DimeDSDimeSchedulerMgt.GeneratePageUrlWeb(RecRef,5900));
// DSWebservMgt.AddParameter('urldesc2', 'Service Order (Web)');
if Customer.Get(ServiceItemLine."Customer No.") then begin
RecRef.GetTable(Customer);
DSWebservMgt.AddParameter('url3', DimeDSDimeSchedulerMgt.GeneratePageUrl(RecRef, 21));
DSWebservMgt.AddParameter('urldesc3', 'Customer card');
end;
// Call the Dime.Scheduler web service will insert or update the Task
DSWebservMgt.CallDimeSchedulerWS('mboc_upsertTask');
end;
The data is there in Business Central, Dime.Scheduler can accommodate that data and provides you with the tools to easily get that data across. Up to you to identify and map the data so your customer can schedule with the information that they need.
This is the signature of this code unit:
The code unit takes care of authentication (by default, a JSON Web Token is created and renewed when you run the FastTrack wizard), composing the body of the request, and posting the request to Dime.Scheduler's API. All you need do is specify the right parameters and call the right procedure name, and the code unit will take care of the rest.