diff --git a/src/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSIntTableSubscriber.Codeunit.al b/src/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSIntTableSubscriber.Codeunit.al index 60d8d99d3dd..15f68a31e97 100644 --- a/src/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSIntTableSubscriber.Codeunit.al +++ b/src/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSIntTableSubscriber.Codeunit.al @@ -284,7 +284,7 @@ codeunit 6610 "FS Int. Table Subscriber" end; [EventSubscriber(ObjectType::Codeunit, Codeunit::"Integration Rec. Synch. Invoke", 'OnAfterTransferRecordFields', '', true, false)] - local procedure OnAfterTransferRecordFields(SourceRecordRef: RecordRef; var DestinationRecordRef: RecordRef; var AdditionalFieldsWereModified: Boolean) + local procedure OnAfterTransferRecordFields(SourceRecordRef: RecordRef; var DestinationRecordRef: RecordRef; var AdditionalFieldsWereModified: Boolean; DestinationIsInserted: Boolean) var FSConnectionSetup: Record "FS Connection Setup"; FSWorkOrderProduct: Record "FS Work Order Product"; @@ -358,6 +358,35 @@ codeunit 6610 "FS Int. Table Subscriber" end; end; + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Integration Record Synch.", 'OnBeforeIsFieldModified', '', true, false)] + local procedure OnBeforeIsFieldModified(var SourceFieldRef: FieldRef; var DestinationFieldRef: FieldRef; var Result: Boolean; var IsHandled: Boolean) + begin + HandleOnBeforeIsFieldModified(SourceFieldRef, DestinationFieldRef, Result, IsHandled); + end; + + internal procedure HandleOnBeforeIsFieldModified(var SourceFieldRef: FieldRef; var DestinationFieldRef: FieldRef; var Result: Boolean; var IsHandled: Boolean) + var + FSConnectionSetup: Record "FS Connection Setup"; + CRMProduct: Record "CRM Product"; + ExistingCRMProduct: Record "CRM Product"; + DestinationRecordRef: RecordRef; + ItemIsManaged: Boolean; + begin + if not FSConnectionSetup.IsEnabled() then + exit; + + if not IsItemCouplingToCustomerAssetConversion(SourceFieldRef, DestinationFieldRef) then + exit; + + ItemIsManaged := SourceFieldRef.Value(); + DestinationRecordRef := DestinationFieldRef.Record(); + DestinationRecordRef.SetTable(CRMProduct); + ExistingCRMProduct.SetLoadFields(ConvertToCustomerAsset); + ExistingCRMProduct.Get(CRMProduct.ProductId); + Result := ExistingCRMProduct.ConvertToCustomerAsset <> GetCustomerAssetConversion(ItemIsManaged); + IsHandled := true; + end; + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Integration Record Synch.", 'OnTransferFieldData', '', true, false)] local procedure OnTransferFieldData(SourceFieldRef: FieldRef; DestinationFieldRef: FieldRef; var NewValue: Variant; var IsValueFound: Boolean; var NeedsConversion: Boolean) var @@ -380,6 +409,7 @@ codeunit 6610 "FS Int. Table Subscriber" QuantityToTransferToInvoice: Decimal; QuantityCurrentlyConsumed: Decimal; QuantityCurrentlyInvoiced: Decimal; + ItemIsManaged: Boolean; NotCoupledCRMUomErr: Label 'The unit is not coupled to a unit of measure.'; begin if not FSConnectionSetup.IsEnabled() then @@ -392,6 +422,14 @@ codeunit 6610 "FS Int. Table Subscriber" if SourceFieldRef.Record().Number() = DestinationFieldRef.Record().Number() then exit; + if IsItemCouplingToCustomerAssetConversion(SourceFieldRef, DestinationFieldRef) then begin + ItemIsManaged := SourceFieldRef.Value(); + NewValue := GetCustomerAssetConversion(ItemIsManaged); + IsValueFound := true; + NeedsConversion := false; + exit; + end; + if (SourceFieldRef.Record().Number = Database::"Service Header") and (DestinationFieldRef.Record().Number = Database::"FS Work Order") then case DestinationFieldRef.Name() of @@ -672,6 +710,23 @@ codeunit 6610 "FS Int. Table Subscriber" exit(MaxQuantity); end; + local procedure IsItemCouplingToCustomerAssetConversion(SourceFieldRef: FieldRef; DestinationFieldRef: FieldRef): Boolean + var + Item: Record Item; + CRMProduct: Record "CRM Product"; + begin + exit( + (SourceFieldRef.Record().Number() = Database::Item) and + (SourceFieldRef.Number() = Item.FieldNo("Coupled to Dataverse")) and + (DestinationFieldRef.Record().Number() = Database::"CRM Product") and + (DestinationFieldRef.Number() = CRMProduct.FieldNo(ConvertToCustomerAsset))); + end; + + internal procedure GetCustomerAssetConversion(ItemIsManaged: Boolean): Boolean + begin + exit(not ItemIsManaged); + end; + [EventSubscriber(ObjectType::Codeunit, Codeunit::"CRM Int. Table. Subscriber", 'OnFindNewValueForCoupledRecordPK', '', true, false)] local procedure OnFindNewValueForCoupledRecordPK(IntegrationTableMapping: Record "Integration Table Mapping"; SourceFieldRef: FieldRef; DestinationFieldRef: FieldRef; var NewValue: Variant; var IsValueFound: Boolean) var @@ -1444,7 +1499,7 @@ codeunit 6610 "FS Int. Table Subscriber" end; [EventSubscriber(ObjectType::Codeunit, Codeunit::"CRM Setup Defaults", 'OnResetItemProductMappingOnAfterInsertFieldsMapping', '', false, false)] - local procedure AddFieldServiceProductTypeFieldMapping(var Sender: Codeunit "CRM Setup Defaults"; IntegrationTableMappingName: Code[20]) + local procedure AddFieldServiceProductMappings(var Sender: Codeunit "CRM Setup Defaults"; IntegrationTableMappingName: Code[20]) var FSConnectionSetup: Record "FS Connection Setup"; Item: Record Item; @@ -1461,6 +1516,14 @@ codeunit 6610 "FS Int. Table Subscriber" CRMProduct.FieldNo(FieldServiceProductType), IntegrationFieldMapping.Direction::ToIntegrationTable, '', false, false); + + // Coupled Business Central items are managed by Field Service customer assets. + Sender.InsertIntegrationFieldMapping( + IntegrationTableMappingName, + Item.FieldNo("Coupled to Dataverse"), + CRMProduct.FieldNo(ConvertToCustomerAsset), + IntegrationFieldMapping.Direction::ToIntegrationTable, + '', false, false); end; local procedure UpdateCorrelatedJobJournalLine(var SourceRecordRef: RecordRef; var DestinationRecordRef: RecordRef) @@ -2518,8 +2581,6 @@ codeunit 6610 "FS Int. Table Subscriber" IgnoreArchievedServiceOrdersOnQueryPostFilterIgnoreRecord(SourceRecordRef, IgnoreRecord); Database::"FS Work Order": IgnoreArchievedCRMWorkOrdersOnQueryPostFilterIgnoreRecord(SourceRecordRef, IgnoreRecord); - Database::"Service Item": - IgnoreServiceItemsByConvertToCustomerAssetFlag(SourceRecordRef, IgnoreRecord); end; if FSConnectionSetup.IsEnabled() then @@ -2685,6 +2746,8 @@ codeunit 6610 "FS Int. Table Subscriber" IgnoreRecord := true; end; +#pragma warning disable AS0105 + [Obsolete('Remove calls to this procedure. Service items are always synchronized to Field Service customer assets; item-product synchronization disables customer asset conversion.', '30.0')] internal procedure IgnoreServiceItemsByConvertToCustomerAssetFlag(SourceRecordRef: RecordRef; var IgnoreRecord: Boolean) var FSConnectionSetup: Record "FS Connection Setup"; @@ -2718,6 +2781,7 @@ codeunit 6610 "FS Int. Table Subscriber" if not CRMProduct.ConvertToCustomerAsset then IgnoreRecord := true; end; +#pragma warning restore AS0105 [EventSubscriber(ObjectType::Codeunit, Codeunit::"Integration Table Synch.", 'OnAfterInitSynchJob', '', true, true)] local procedure LogTelemetryOnAfterInitSynchJob(ConnectionType: TableConnectionType; IntegrationTableID: Integer) diff --git a/src/Apps/W1/FieldServiceIntegration/test library/src/FSIntegrationTestLibrary.Codeunit.al b/src/Apps/W1/FieldServiceIntegration/test library/src/FSIntegrationTestLibrary.Codeunit.al index fbb03b04162..1a7e247c0a1 100644 --- a/src/Apps/W1/FieldServiceIntegration/test library/src/FSIntegrationTestLibrary.Codeunit.al +++ b/src/Apps/W1/FieldServiceIntegration/test library/src/FSIntegrationTestLibrary.Codeunit.al @@ -58,6 +58,20 @@ codeunit 139205 "FS Integration Test Library" FSIntTableSubscriber.UpdateQuantities(FSBookableResourceBooking, ServiceLine); end; + procedure GetCustomerAssetConversion(ItemIsManaged: Boolean): Boolean + var + FSIntTableSubscriber: Codeunit "FS Int. Table Subscriber"; + begin + exit(FSIntTableSubscriber.GetCustomerAssetConversion(ItemIsManaged)); + end; + + procedure HandleOnBeforeIsFieldModified(var SourceFieldRef: FieldRef; var DestinationFieldRef: FieldRef; var Result: Boolean; var IsHandled: Boolean) + var + FSIntTableSubscriber: Codeunit "FS Int. Table Subscriber"; + begin + FSIntTableSubscriber.HandleOnBeforeIsFieldModified(SourceFieldRef, DestinationFieldRef, Result, IsHandled); + end; + procedure IgnorePostedJobJournalLinesOnQueryPostFilterIgnoreRecord(SourceRecordRef: RecordRef; var IgnoreRecord: Boolean) var FSIntTableSubscriber: Codeunit "FS Int. Table Subscriber"; @@ -79,12 +93,17 @@ codeunit 139205 "FS Integration Test Library" FSIntTableSubscriber.IgnoreArchievedCRMWorkOrdersOnQueryPostFilterIgnoreRecord(SourceRecordRef, IgnoreRecord); end; + /// + /// Retained for compatibility. Service items are now always synchronized to Field Service customer assets, so this procedure leaves the synchronization decision unchanged. + /// + /// A reference to the service item to evaluate. + /// The existing synchronization decision, which is left unchanged. +#pragma warning disable AS0105 + [Obsolete('Remove calls to this procedure. Service items are always synchronized to Field Service customer assets; item-product synchronization disables customer asset conversion.', '30.0')] procedure IgnoreServiceItemsByConvertToCustomerAssetFlag(SourceRecordRef: RecordRef; var IgnoreRecord: Boolean) - var - FSIntTableSubscriber: Codeunit "FS Int. Table Subscriber"; begin - FSIntTableSubscriber.IgnoreServiceItemsByConvertToCustomerAssetFlag(SourceRecordRef, IgnoreRecord); end; +#pragma warning restore AS0105 procedure MarkArchivedServiceOrder(ServiceHeader: Record "Service Header") var diff --git a/src/Apps/W1/FieldServiceIntegration/test/src/FSIntegrationTest.Codeunit.al b/src/Apps/W1/FieldServiceIntegration/test/src/FSIntegrationTest.Codeunit.al index 94c69e79ff9..3c70255f726 100644 --- a/src/Apps/W1/FieldServiceIntegration/test/src/FSIntegrationTest.Codeunit.al +++ b/src/Apps/W1/FieldServiceIntegration/test/src/FSIntegrationTest.Codeunit.al @@ -23,7 +23,6 @@ using Microsoft.Purchases.Vendor; using Microsoft.Sales.Customer; using Microsoft.Service.Archive; using Microsoft.Service.Document; -using Microsoft.Service.Item; using Microsoft.Service.Setup; using Microsoft.Service.Test; using Microsoft.TestLibraries.DynamicsFieldService; @@ -1578,109 +1577,107 @@ codeunit 139204 "FS Integration Test" end; [Test] - procedure IgnoreServiceItemWhenConvertToCustomerAssetIsFalse() + [TransactionModel(TransactionModel::AutoCommit)] + procedure ItemSynchronizationDisablesCustomerAssetConversion() var Item: Record Item; - TempServiceItem: Record "Service Item" temporary; CRMProduct: Record "CRM Product"; - CRMIntegrationRecord: Record "CRM Integration Record"; - RecordRef: RecordRef; - IgnoreRecord: Boolean; - ProductId: Guid; + IntegrationTableMapping: Record "Integration Table Mapping"; + CRMIntegrationTableSynch: Codeunit "CRM Integration Table Synch."; + CRMSetupDefaults: Codeunit "CRM Setup Defaults"; begin - // [FEATURE] [Service Item Mapping] - // [SCENARIO] Service Item is skipped when linked CRM Product has Convert to Customer Asset = No. + // [FEATURE] [Item-Product Mapping] + // [SCENARIO] Synchronizing an item disables native Field Service customer asset creation. Initialize(); + LibraryCRMIntegration.CreateCRMConnectionSetup('', '@@test@@', true); InitSetup(true, ''); - Item.Get(CreateItem()); - TempServiceItem."Item No." := Item."No."; - RecordRef.GetTable(TempServiceItem); - - ProductId := CreateGuid(); - CRMProduct.ProductId := ProductId; - CRMProduct.ConvertToCustomerAsset := false; - CRMProduct.Insert(false); + // [GIVEN] A coupled item and product where Convert to Customer Asset is Yes. + CRMSetupDefaults.ResetItemProductMapping('ITEM-PRODUCT', false); + LibraryCRMIntegration.CreateCoupledItemAndProduct(Item, CRMProduct); + CRMProduct.ConvertToCustomerAsset := true; + CRMProduct.Modify(); - CRMIntegrationRecord.CoupleCRMIDToRecordID(ProductId, Item.RecordId()); + IntegrationTableMapping.Get('ITEM-PRODUCT'); - FSIntegrationTestLibrary.IgnoreServiceItemsByConvertToCustomerAssetFlag(RecordRef, IgnoreRecord); + // [WHEN] The item is synchronized to the Field Service product. + CRMIntegrationTableSynch.SynchRecord(IntegrationTableMapping, Item.RecordId(), true, false); - Assert.IsTrue(IgnoreRecord, 'Service Item should be ignored when Convert to Customer Asset is false.'); + // [THEN] Native Field Service customer asset creation is disabled. + CRMProduct.Get(CRMProduct.ProductId); + Assert.IsFalse(CRMProduct.ConvertToCustomerAsset, 'Convert to Customer Asset should be disabled.'); end; [Test] - procedure DoNotIgnoreServiceItemWhenConvertToCustomerAssetIsTrue() - var - Item: Record Item; - TempServiceItem: Record "Service Item" temporary; - CRMProduct: Record "CRM Product"; - CRMIntegrationRecord: Record "CRM Integration Record"; - RecordRef: RecordRef; - IgnoreRecord: Boolean; - ProductId: Guid; + [TransactionModel(TransactionModel::AutoRollback)] + procedure CustomerAssetConversionReflectsItemManagement() begin - // [FEATURE] [Service Item Mapping] - // [SCENARIO] Service Item is not skipped when linked CRM Product has Convert to Customer Asset = Yes. - Initialize(); - InitSetup(true, ''); - - Item.Get(CreateItem()); - TempServiceItem."Item No." := Item."No."; - RecordRef.GetTable(TempServiceItem); + // [FEATURE] [Item-Product Mapping] + // [SCENARIO] Customer asset conversion is disabled only for items managed through synchronization. - ProductId := CreateGuid(); - CRMProduct.ProductId := ProductId; - CRMProduct.ConvertToCustomerAsset := true; - CRMProduct.Insert(false); - - CRMIntegrationRecord.CoupleCRMIDToRecordID(ProductId, Item.RecordId()); + // [WHEN] An item is not managed through synchronization. + // [THEN] Native Field Service customer asset conversion remains enabled. + Assert.IsTrue(FSIntegrationTestLibrary.GetCustomerAssetConversion(false), 'An unmanaged item should allow customer asset conversion.'); - FSIntegrationTestLibrary.IgnoreServiceItemsByConvertToCustomerAssetFlag(RecordRef, IgnoreRecord); - - Assert.IsFalse(IgnoreRecord, 'Service Item should not be ignored when Convert to Customer Asset is true.'); + // [WHEN] An item is managed through synchronization. + // [THEN] Native Field Service customer asset conversion is disabled. + Assert.IsFalse(FSIntegrationTestLibrary.GetCustomerAssetConversion(true), 'A managed item should disable customer asset conversion.'); end; [Test] - procedure DoNotIgnoreServiceItemWhenItemNoIsBlank() + [TransactionModel(TransactionModel::AutoRollback)] + procedure ItemProductComparisonIgnoredWhenFieldServiceIsDisabled() var - TempServiceItem: Record "Service Item" temporary; - RecordRef: RecordRef; - IgnoreRecord: Boolean; - begin - // [FEATURE] [Service Item Mapping] - // [SCENARIO] Service Item with blank Item No. is not skipped by this filter. - Initialize(); - InitSetup(true, ''); + Item: Record Item; + CRMProduct: Record "CRM Product"; + SourceRecordRef: RecordRef; + DestinationRecordRef: RecordRef; + SourceFieldRef: FieldRef; + DestinationFieldRef: FieldRef; + Result: Boolean; + IsHandled: Boolean; + begin + // [FEATURE] [Item-Product Mapping] + // [SCENARIO] Field Service does not handle synchronization comparisons while its integration is disabled. + ResetFSEnvironment(); - TempServiceItem."Item No." := ''; - RecordRef.GetTable(TempServiceItem); + // [GIVEN] The fields used to derive customer asset conversion and no Field Service connection setup. + SourceRecordRef.GetTable(Item); + SourceFieldRef := SourceRecordRef.Field(Item.FieldNo("Coupled to Dataverse")); + DestinationRecordRef.GetTable(CRMProduct); + DestinationFieldRef := DestinationRecordRef.Field(CRMProduct.FieldNo(ConvertToCustomerAsset)); - FSIntegrationTestLibrary.IgnoreServiceItemsByConvertToCustomerAssetFlag(RecordRef, IgnoreRecord); + // [WHEN] The synchronization engine compares those fields. + FSIntegrationTestLibrary.HandleOnBeforeIsFieldModified(SourceFieldRef, DestinationFieldRef, Result, IsHandled); - Assert.IsFalse(IgnoreRecord, 'Service Item with blank Item No. should not be ignored by this filter.'); + // [THEN] Field Service leaves the comparison to the synchronization engine. + Assert.IsFalse(IsHandled, 'Field Service should not handle synchronization comparisons while its integration is disabled.'); end; [Test] - procedure DoNotIgnoreServiceItemWhenItemIsNotCoupled() + [TransactionModel(TransactionModel::AutoCommit)] + procedure ItemProductMappingDisablesCustomerAssetConversion() var Item: Record Item; - TempServiceItem: Record "Service Item" temporary; - RecordRef: RecordRef; - IgnoreRecord: Boolean; + CRMProduct: Record "CRM Product"; + IntegrationFieldMapping: Record "Integration Field Mapping"; + CRMSetupDefaults: Codeunit "CRM Setup Defaults"; begin - // [FEATURE] [Service Item Mapping] - // [SCENARIO] Service Item with uncoupled Item is not skipped by this filter. + // [FEATURE] [Item-Product Mapping] + // [SCENARIO] The item-product mapping derives native Field Service customer asset creation from item management. Initialize(); InitSetup(true, ''); - Item.Get(CreateItem()); - TempServiceItem."Item No." := Item."No."; - RecordRef.GetTable(TempServiceItem); - - FSIntegrationTestLibrary.IgnoreServiceItemsByConvertToCustomerAssetFlag(RecordRef, IgnoreRecord); + // [WHEN] The default item-product mapping is reset. + CRMSetupDefaults.ResetItemProductMapping('ITEM-PRODUCT', false); - Assert.IsFalse(IgnoreRecord, 'Service Item with uncoupled Item should not be ignored by this filter.'); + // [THEN] Convert to Customer Asset is mapped from the item's Dataverse coupling state in the outbound direction. + IntegrationFieldMapping.SetRange("Integration Table Mapping Name", 'ITEM-PRODUCT'); + IntegrationFieldMapping.SetRange("Integration Table Field No.", CRMProduct.FieldNo(ConvertToCustomerAsset)); + Assert.IsTrue(IntegrationFieldMapping.FindFirst(), 'The Convert to Customer Asset mapping should exist.'); + Assert.AreEqual(Item.FieldNo("Coupled to Dataverse"), IntegrationFieldMapping."Field No.", 'The mapping should use the item coupling state.'); + Assert.AreEqual(IntegrationFieldMapping.Direction::ToIntegrationTable, IntegrationFieldMapping.Direction, 'The mapping should be outbound.'); + Assert.AreEqual('', IntegrationFieldMapping."Constant Value", 'The mapping should not use a constant value.'); end; local procedure Initialize()