Codeunit 5360 CDS Int. Table Couple
- App
- Base Application
- Namespace
- Microsoft.Integration.Dataverse
- Versions
- 19-28
Versions171819202122232425262728
Source29
Source in 29
src/Layers/W1/BaseApp/Integration/Dataverse/CDSIntTableCouple.Codeunit.al376 lines, Copyright (c) Microsoft Corporation. MIT
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------
namespace Microsoft.Integration.Dataverse;
using Microsoft.Integration.D365Sales;
using Microsoft.Integration.SyncEngine;
codeunit 5360 "CDS Int. Table Couple"
{
TableNo = "Integration Table Mapping";
trigger OnRun()
var
ConnectionName: Text;
Handled: Boolean;
begin
OnBeforeRun(Rec, Handled);
if Handled then
exit;
ConnectionName := CRMIntegrationTableSynch.InitConnection();
CRMIntegrationTableSynch.TestConnection();
PerformScheduledCoupling(Rec);
CRMIntegrationTableSynch.CloseConnection(ConnectionName);
end;
var
CRMIntegrationTableSynch: Codeunit "CRM Integration Table Synch.";
SynchActionType: Option "None",Insert,Modify,ForceModify,IgnoreUnchanged,Fail,Skip,Delete,Uncouple,Couple;
NoMatchingCriteriaDefinedErr: Label 'You must specify which fields on the table mapping ''%1'' should be used for match-based coupling.', Comment = '%1 - integration table mapping name';
NoMatchFoundErr: Label 'Failed to couple %2 record(s), because no unique uncoupled matching entity was found in %1 with the specified matching criteria.', Comment = '%1 - comma-separated list of field names, %2 - A URL, %3 - an integer, number of records';
NoMatchFoundTelemetryErr: Label 'No matching entity was found for %1 in %3 by matching on following fields: %2.', Locked = true;
SingleMatchAlreadyCoupledTelemetryErr: Label 'Single matching entity was found for %1 in %3 by matching on following fields: %2, but it is already coupled.', Locked = true;
MultipleMatchesFoundTelemetryErr: Label 'Multiple matching entities found for %1 in %3 by matching on following fields: %2.', Locked = true;
NoMatchingCriteriaDefinedTelemetryErr: Label 'User is trying to schedule match based coupling for integration table mapping %1 without having specified the matchin criteria.', Locked = true;
NoConflictResolutionStrategyDefinedTelemetryErr: Label 'User is trying to schedule match based coupling for integration table mapping %1 without having specified the conflict resolution strategy.', Locked = true;
SkippingPostCouplingSynchTelemetryUserChoiceMsg: Label 'Skipping post-coupling synchronization for integration table mapping %1, because the user chose not to run it.', Locked = true;
SkippingPostCouplingSynchTelemetryNoneCoupledMsg: Label 'Skipping post-coupling synchronization for integration table mapping %1, because no records were coupled.', Locked = true;
StartingPostCouplingSynchTelemetryMsg: Label 'Starting post-coupling synchronization for integration table mapping %1, for %2 coupled records.', Locked = true;
SchedulingPostCouplingSynchForBatchTelemetryMsg: Label 'Scheduling post-coupling synchronization for integration table mapping %1, for a batch of %2 coupled records.', Locked = true;
CategoryTok: Label 'AL Dataverse Integration', Locked = true;
CouplingMsg: Label 'Coupling records...\\Processing record #1##########', Comment = '#1 place holder for record number';
MappingNameWithParentTxt: Label '%1 (%2)', Locked = true;
DataverseOrgURL: Text;
internal procedure PerformScheduledCoupling(var IntegrationTableMapping: Record "Integration Table Mapping")
var
CRMFullSynchReviewLine: Record "CRM Full Synch. Review Line";
IntegrationTableSynch: Codeunit "Integration Table Synch.";
JobId: Guid;
begin
JobId := IntegrationTableSynch.BeginIntegrationCoupleJob(TableConnectionType::CRM, IntegrationTableMapping, IntegrationTableMapping."Table ID");
if not IsNullGuid(JobId) then begin
if IntegrationTableMapping.Direction in [IntegrationTableMapping.Direction::Bidirectional, IntegrationTableMapping.Direction::FromIntegrationTable] then
CRMFullSynchReviewLine.FullSynchStarted(IntegrationTableMapping, JobId, IntegrationTableMapping.Direction::FromIntegrationTable);
if IntegrationTableMapping.Direction in [IntegrationTableMapping.Direction::Bidirectional, IntegrationTableMapping.Direction::ToIntegrationTable] then
CRMFullSynchReviewLine.FullSynchStarted(IntegrationTableMapping, JobId, IntegrationTableMapping.Direction::ToIntegrationTable);
CoupleRecords(IntegrationTableMapping, IntegrationTableSynch);
IntegrationTableSynch.EndIntegrationSynchJob();
if IntegrationTableMapping.Direction in [IntegrationTableMapping.Direction::Bidirectional, IntegrationTableMapping.Direction::FromIntegrationTable] then
CRMFullSynchReviewLine.FullSynchFinished(IntegrationTableMapping, IntegrationTableMapping.Direction::FromIntegrationTable);
if IntegrationTableMapping.Direction in [IntegrationTableMapping.Direction::Bidirectional, IntegrationTableMapping.Direction::ToIntegrationTable] then
CRMFullSynchReviewLine.FullSynchFinished(IntegrationTableMapping, IntegrationTableMapping.Direction::ToIntegrationTable);
end;
end;
local procedure CoupleRecords(var IntegrationTableMapping: Record "Integration Table Mapping"; var IntegrationTableSynch: Codeunit "Integration Table Synch.")
var
IntegrationFieldMapping: Record "Integration Field Mapping";
TempMatchingIntegrationFieldMapping: Record "Integration Field Mapping" temporary;
CRMIntegrationRecord: Record "CRM Integration Record";
CRMIntegrationManagement: Codeunit "CRM Integration Management";
IntegrationRecordSynch: Codeunit "Integration Record Synch.";
LocalRecordRef: RecordRef;
IntegrationRecordRef: RecordRef;
EmptyRecordRef: RecordRef;
MatchingIntegrationRecordFieldRef: FieldRef;
MatchingLocalFieldRef: FieldRef;
CoupledToCRMFieldRef: FieldRef;
IntegrationRecordIdFieldRef: FieldRef;
IntegrationRecordKeyRef: KeyRef;
SetMatchingFieldFilterHandled: Boolean;
MatchingFieldCount: Integer;
MatchCount: Integer;
UnmatchedSystemIdsDictionary: Dictionary of [Code[20], List of [Guid]];
UnmatchedSystemIds: List of [Guid];
CoupledSystemIds: List of [Guid];
CoupledCRMIds: List of [Guid];
LocalRecordSystemId: Guid;
RecordNumber: Integer;
Dialog: Dialog;
TableFilter: Text;
FilterList: List of [Text];
MatchPriorityList: List of [Integer];
MatchPriority: Integer;
begin
// collect the matching criteria fields in a temporary record
IntegrationFieldMapping.SetRange("Integration Table Mapping Name", IntegrationTableMapping.Name);
IntegrationFieldMapping.SetRange("Use For Match-Based Coupling", true);
IntegrationFieldMapping.SetCurrentKey("Match Priority");
IntegrationFieldMapping.SetAscending("Match Priority", true);
if IntegrationFieldMapping.FindSet() then
repeat
TempMatchingIntegrationFieldMapping.Init();
TempMatchingIntegrationFieldMapping.TransferFields(IntegrationFieldMapping);
TempMatchingIntegrationFieldMapping.Insert();
if not MatchPriorityList.Contains(TempMatchingIntegrationFieldMapping."Match Priority") then
MatchPriorityList.Add(TempMatchingIntegrationFieldMapping."Match Priority");
until IntegrationFieldMapping.Next() = 0
else begin
Session.LogMessage('0000EZF', StrSubstNo(NoMatchingCriteriaDefinedTelemetryErr, GetMappingNameWithParent(IntegrationTableMapping)), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
IntegrationTableSynch.LogMatchBasedCouplingError(LocalRecordRef, StrSubstNo(NoMatchingCriteriaDefinedErr, IntegrationTableMapping.GetUserFriendlyMappingName()));
exit;
end;
if GuiAllowed() then begin
Dialog.Open(CouplingMsg);
Dialog.Update(1, '');
end;
// iterate through uncoupled records and for each of them try to find a match in Dataverse
LocalRecordRef.Open(IntegrationTableMapping."Table ID");
IntegrationRecordSynch.SplitLocalTableFilter(IntegrationTableMapping, FilterList);
foreach TableFilter in FilterList do begin
if TableFilter <> '' then
LocalRecordRef.SetView(TableFilter);
if CRMIntegrationManagement.FindCoupledToCRMField(LocalRecordRef, CoupledToCRMFieldRef) then begin
CoupledToCRMFieldRef.CalcField();
CoupledToCRMFieldRef.SetRange(false);
end;
if LocalRecordRef.FindSet() then
repeat
if GuiAllowed() then begin
RecordNumber += 1;
Dialog.Update(1, RecordNumber);
end;
LocalRecordSystemId := LocalRecordRef.Field(LocalRecordRef.SystemIdNo()).Value();
// re-check that the record is uncoupled as it could just be coupled by another job
if CRMIntegrationRecord.IsIntegrationIdCoupled(LocalRecordSystemId, LocalRecordRef.Number) then begin
Session.LogMessage('0000F7F', GetSingleMatchAlreadyCoupledTelemetryErrorMessage(LocalRecordRef, TempMatchingIntegrationFieldMapping), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
CoupledSystemIds.Add(LocalRecordSystemId)
end else begin
Clear(IntegrationRecordRef);
IntegrationRecordRef.Open(IntegrationTableMapping."Integration Table ID");
IntegrationTableMapping.SetIntRecordRefFilter(IntegrationRecordRef);
// this inner loop is looping through a temporary record set with a handful of user-chosen matching fields - not a performance concern as such
foreach MatchPriority in MatchPriorityList do
if not CoupledSystemIds.Contains(LocalRecordSystemId) then begin
MatchingFieldCount := 0;
MatchCount := 0;
TempMatchingIntegrationFieldMapping.Reset();
TempMatchingIntegrationFieldMapping.SetRange("Match Priority", MatchPriority);
TempMatchingIntegrationFieldMapping.FindSet();
repeat
// initialize the fields that we should match on
MatchingIntegrationRecordFieldRef := IntegrationRecordRef.Field(TempMatchingIntegrationFieldMapping."Integration Table Field No.");
MatchingLocalFieldRef := LocalRecordRef.Field(TempMatchingIntegrationFieldMapping."Field No.");
// raise an event so that custom filtering logic can be implemented (depending on which record and which fields are chosen as the matching field)
SetMatchingFieldFilterHandled := false;
OnBeforeSetMatchingFilter(IntegrationRecordRef, MatchingIntegrationRecordFieldRef, LocalRecordRef, MatchingLocalFieldRef, SetMatchingFieldFilterHandled);
// if nobody implemented custom filtering, apply default filtering logic
// and that is: set the filter on the integration table field with the value of the local field (case sensitive if specified by user)
if not SetMatchingFieldFilterHandled then
case MatchingLocalFieldRef.Type of
FieldType::Code,
FieldType::Text:
if Format(MatchingLocalFieldRef.Value()) <> '' then begin
if not TempMatchingIntegrationFieldMapping."Case-Sensitive Matching" then
MatchingIntegrationRecordFieldRef.SetFilter('''@' + Format(MatchingLocalFieldRef.Value()).Replace('''', '''''') + '''')
else
MatchingIntegrationRecordFieldRef.SetRange(MatchingLocalFieldRef.Value());
MatchingFieldCount += 1;
end;
else begin
MatchingIntegrationRecordFieldRef.SetRange(MatchingLocalFieldRef.Value());
MatchingFieldCount += 1;
end;
end;
until TempMatchingIntegrationFieldMapping.Next() = 0;
// if there is exactly one match, and it is not coupled, couple it. otherwise - log a synch error
if (MatchingFieldCount > 0) or SetMatchingFieldFilterHandled then
MatchCount := IntegrationRecordRef.Count();
case MatchCount of
0:
begin
Session.LogMessage('0000EZG', GetNoMatchFoundTelemetryErrorMessage(LocalRecordRef, TempMatchingIntegrationFieldMapping), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
if not UnmatchedSystemIds.Contains(LocalRecordSystemId) then
UnmatchedSystemIds.Add(LocalRecordSystemId);
end;
1:
begin
IntegrationRecordRef.FindFirst();
if CRMIntegrationRecord.IsCRMRecordRefCoupled(IntegrationRecordRef) then begin
Session.LogMessage('0000EZH', GetSingleMatchAlreadyCoupledTelemetryErrorMessage(LocalRecordRef, TempMatchingIntegrationFieldMapping), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
if not UnmatchedSystemIds.Contains(LocalRecordSystemId) then
UnmatchedSystemIds.Add(LocalRecordSystemId);
end else
if IntegrationTableSynch.Couple(LocalRecordRef, IntegrationRecordRef) then begin
CoupledSystemIds.Add(LocalRecordSystemId);
if UnmatchedSystemIds.Contains(LocalRecordSystemId) then
UnmatchedSystemIds.Remove(LocalRecordSystemId);
IntegrationRecordKeyRef := IntegrationRecordRef.KeyIndex(1); // Primary Key
IntegrationRecordIdFieldRef := IntegrationRecordKeyRef.FieldIndex(1);
CoupledCRMIds.Add(IntegrationRecordIdFieldRef.Value());
end;
end;
else begin
Session.LogMessage('0000EZI', GetMultipleMatchesFoundTelemetryErrorMessage(LocalRecordRef, TempMatchingIntegrationFieldMapping), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
if not UnmatchedSystemIds.Contains(LocalRecordSystemId) then
UnmatchedSystemIds.Add(LocalRecordSystemId);
end;
end;
end;
end;
until LocalRecordRef.Next() = 0;
if GuiAllowed then
Dialog.Update(1, RecordNumber);
end;
if GuiAllowed then
Dialog.Close();
// if the user chose so, create new entities in Dataverse for records that couldn't be matched
if UnmatchedSystemIds.Count() > 0 then
if ShouldCreateNewRecordsInCaseOfNoMatch(IntegrationTableMapping) then begin
UnmatchedSystemIdsDictionary.Add(IntegrationTableMapping.Name, UnmatchedSystemIds);
CRMIntegrationManagement.CreateNewRecordsInCRM(UnmatchedSystemIdsDictionary);
end else begin
IntegrationTableSynch.UpdateSynchJobCounters(SynchActionType::Fail, UnmatchedSystemIds.Count());
IntegrationTableSynch.LogSynchError(EmptyRecordRef, EmptyRecordRef, GetNoMatchFoundErrorMessage(UnmatchedSystemIds.Count()), false);
end;
// schedule synch job of coupled records, if user chose to do so
if IntegrationTableMapping."Synch. After Bulk Coupling" then
SynchronizeCoupledRecords(IntegrationTableMapping, CoupledSystemIds, CoupledCRMIds)
else
Session.LogMessage('0000EZJ', StrSubstNo(SkippingPostCouplingSynchTelemetryUserChoiceMsg, GetMappingNameWithParent(IntegrationTableMapping)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
end;
local procedure ShouldCreateNewRecordsInCaseOfNoMatch(var IntegrationTableMapping: Record "Integration Table Mapping"): Boolean
var
CRMConnectionSetup: Record "CRM Connection Setup";
Handled: Boolean;
ShouldCreateNewRecord: Boolean;
begin
OnShouldCreateNewRecordInCaseOfNoMatch(IntegrationTableMapping, ShouldCreateNewRecord, Handled);
if Handled then
exit(ShouldCreateNewRecord);
case IntegrationTableMapping.Name of
'SALESPEOPLE':
exit(false);
'SALESORDER-ORDER':
exit(CRMConnectionSetup.IsBidirectionalSalesOrderIntEnabled() and IntegrationTableMapping."Create New in Case of No Match");
else
exit(IntegrationTableMapping."Create New in Case of No Match")
end;
end;
local procedure SynchronizeCoupledRecords(var IntegrationTableMapping: Record "Integration Table Mapping"; var CoupledSystemIds: List of [Guid]; var CoupledCRMIds: List of [Guid])
var
CRMIntegrationManagement: Codeunit "CRM Integration Management";
Direction: Integer;
begin
if CoupledSystemIds.Count() = 0 then begin
Session.LogMessage('0000EZK', StrSubstNo(SkippingPostCouplingSynchTelemetryNoneCoupledMsg, GetMappingNameWithParent(IntegrationTableMapping)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
exit;
end;
Session.LogMessage('0000EZL', StrSubstNo(StartingPostCouplingSynchTelemetryMsg, GetMappingNameWithParent(IntegrationTableMapping), CoupledSystemIds.Count()), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
Direction := IntegrationTableMapping.Direction;
if IntegrationTableMapping.Direction = IntegrationTableMapping.Direction::Bidirectional then
case IntegrationTableMapping."Update-Conflict Resolution" of
IntegrationTableMapping."Update-Conflict Resolution"::"None":
Session.LogMessage('0000EZM', StrSubstNo(NoConflictResolutionStrategyDefinedTelemetryErr, GetMappingNameWithParent(IntegrationTableMapping)), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
IntegrationTableMapping."Update-Conflict Resolution"::"Get Update from Integration":
Direction := IntegrationTableMapping.Direction::FromIntegrationTable;
IntegrationTableMapping."Update-Conflict Resolution"::"Send Update to Integration":
Direction := IntegrationTableMapping.Direction::ToIntegrationTable;
end;
Session.LogMessage('0000EZN', StrSubstNo(SchedulingPostCouplingSynchForBatchTelemetryMsg, GetMappingNameWithParent(IntegrationTableMapping), CoupledSystemIds.Count()), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
CRMIntegrationManagement.EnqueueSyncJob(IntegrationTableMapping, CoupledSystemIds, CoupledCRMIds, Direction, true);
end;
local procedure GetNoMatchFoundErrorMessage(ErrorCount: Integer): Text
begin
exit(StrSubstNo(NoMatchFoundErr, GetDataverseOrgURL(), ErrorCount));
end;
local procedure GetNoMatchFoundTelemetryErrorMessage(var LocalRecordRef: RecordRef; var MatchIntegrationFieldMapping: Record "Integration Field Mapping" temporary): Text
var
MatchingFieldNameList: Text;
begin
MatchingFieldNameList := GetMatchingFieldNameList(LocalRecordRef, MatchIntegrationFieldMapping);
exit(StrSubstNo(NoMatchFoundTelemetryErr, Format(LocalRecordRef.Field(LocalRecordRef.SystemIdNo).Value()), MatchingFieldNameList, GetDataverseOrgURL()));
end;
local procedure GetMultipleMatchesFoundTelemetryErrorMessage(var LocalRecordRef: RecordRef; var MatchIntegrationFieldMapping: Record "Integration Field Mapping" temporary): Text
var
MatchingFieldNameList: Text;
begin
MatchingFieldNameList := GetMatchingFieldNameList(LocalRecordRef, MatchIntegrationFieldMapping);
exit(StrSubstNo(MultipleMatchesFoundTelemetryErr, Format(LocalRecordRef.Field(LocalRecordRef.SystemIdNo).Value()), MatchingFieldNameList, GetDataverseOrgURL()));
end;
local procedure GetSingleMatchAlreadyCoupledTelemetryErrorMessage(var LocalRecordRef: RecordRef; var MatchIntegrationFieldMapping: Record "Integration Field Mapping" temporary): Text
var
MatchingFieldNameList: Text;
begin
MatchingFieldNameList := GetMatchingFieldNameList(LocalRecordRef, MatchIntegrationFieldMapping);
exit(StrSubstNo(SingleMatchAlreadyCoupledTelemetryErr, Format(LocalRecordRef.Field(LocalRecordRef.SystemIdNo).Value()), MatchingFieldNameList, GetDataverseOrgURL()));
end;
local procedure GetMappingNameWithParent(var IntegrationTableMapping: Record "Integration Table Mapping"): Text
begin
if IntegrationTableMapping."Parent Name" <> '' then
exit(StrSubstNo(MappingNameWithParentTxt, IntegrationTableMapping.Name, IntegrationTableMapping."Parent Name"));
exit(IntegrationTableMapping.Name);
end;
local procedure GetMatchingFieldNameList(var LocalRecordRef: RecordRef; var MatchIntegrationFieldMapping: Record "Integration Field Mapping" temporary) MatchingFieldNameList: Text
begin
MatchIntegrationFieldMapping.FindSet();
repeat
if MatchingFieldNameList = '' then
MatchingFieldNameList := LocalRecordRef.Field(MatchIntegrationFieldMapping."Field No.").Name()
else
MatchingFieldNameList += ', ' + LocalRecordRef.Field(MatchIntegrationFieldMapping."Field No.").Name()
until MatchIntegrationFieldMapping.Next() = 0;
end;
local procedure GetDataverseOrgURL(): Text
var
CDSConnectionSetup: Record "CDS Connection Setup";
CRMConnectionSetup: Record "CRM Connection Setup";
begin
if DataverseOrgURL <> '' then
exit(DataverseOrgURL);
if CRMConnectionSetup.IsEnabled() then
DataverseOrgURL := CRMConnectionSetup."Server Address"
else
if CDSConnectionSetup.Get() then
if CDSConnectionSetup."Is Enabled" then
DataverseOrgURL := CDSConnectionSetup."Server Address";
exit(DataverseOrgURL);
end;
[IntegrationEvent(false, false)]
local procedure OnBeforeRun(IntegrationTableMapping: Record "Integration Table Mapping"; var Handled: Boolean)
begin
end;
[IntegrationEvent(false, false)]
local procedure OnBeforeSetMatchingFilter(var IntegrationRecordRef: RecordRef; var MatchingIntegrationRecordFieldRef: FieldRef; var LocalRecordRef: RecordRef; var MatchingLocalFieldRef: FieldRef; var SetMatchingFilterHandled: Boolean)
begin
end;
[IntegrationEvent(false, false)]
local procedure OnShouldCreateNewRecordInCaseOfNoMatch(var IntegrationTableMapping: Record "Integration Table Mapping"; var ShouldCreateNewRecord: Boolean; var Handled: Boolean)
begin
end;
}