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..9144d360dcb 100644 --- a/src/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSIntTableSubscriber.Codeunit.al +++ b/src/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSIntTableSubscriber.Codeunit.al @@ -284,12 +284,14 @@ 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"; FSWorkOrderService: Record "FS Work Order Service"; FSBookableResourceBooking: Record "FS Bookable Resource Booking"; + CRMProduct: Record "CRM Product"; + ExistingCRMProduct: Record "CRM Product"; ServiceLine: Record "Service Line"; SourceDestCode: Text; begin @@ -299,6 +301,17 @@ codeunit 6610 "FS Int. Table Subscriber" SourceDestCode := GetSourceDestCode(SourceRecordRef, DestinationRecordRef); case SourceDestCode of + 'Item-CRM Product': + begin + if not DestinationIsInserted then + exit; + + DestinationRecordRef.SetTable(CRMProduct); + ExistingCRMProduct.SetLoadFields(ConvertToCustomerAsset); + ExistingCRMProduct.Get(CRMProduct.ProductId); + if ExistingCRMProduct.ConvertToCustomerAsset then + AdditionalFieldsWereModified := true; + end; 'FS Work Order Product-Service Line': begin SourceRecordRef.SetTable(FSWorkOrderProduct); @@ -1383,6 +1396,8 @@ codeunit 6610 "FS Int. Table Subscriber" SourceDestCode := GetSourceDestCode(SourceRecordRef, DestinationRecordRef); case SourceDestCode of + 'Item-CRM Product': + DisableCustomerAssetConversion(DestinationRecordRef); 'FS Work Order Service-Job Journal Line': UpdateCorrelatedJobJournalLine(SourceRecordRef, DestinationRecordRef); 'Service Header-FS Work Order': @@ -1444,7 +1459,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 +1476,14 @@ codeunit 6610 "FS Int. Table Subscriber" CRMProduct.FieldNo(FieldServiceProductType), IntegrationFieldMapping.Direction::ToIntegrationTable, '', false, false); + + // Business Central service items are the source for Field Service customer assets. + Sender.InsertIntegrationFieldMapping( + IntegrationTableMappingName, + 0, + CRMProduct.FieldNo(ConvertToCustomerAsset), + IntegrationFieldMapping.Direction::ToIntegrationTable, + 'false', false, false); end; local procedure UpdateCorrelatedJobJournalLine(var SourceRecordRef: RecordRef; var DestinationRecordRef: RecordRef) @@ -1627,7 +1650,7 @@ codeunit 6610 "FS Int. Table Subscriber" end; [EventSubscriber(ObjectType::Codeunit, Codeunit::"Integration Rec. Synch. Invoke", 'OnBeforeInsertRecord', '', true, false)] - local procedure HandleOnBeforeInsertRecord(SourceRecordRef: RecordRef; DestinationRecordRef: RecordRef) + local procedure HandleOnBeforeInsertRecord(SourceRecordRef: RecordRef; var DestinationRecordRef: RecordRef) var FSProjectTask: Record "FS Project Task"; Customer: Record Customer; @@ -1662,6 +1685,8 @@ codeunit 6610 "FS Int. Table Subscriber" SourceDestCode := GetSourceDestCode(SourceRecordRef, DestinationRecordRef); case SourceDestCode of + 'Item-CRM Product': + DisableCustomerAssetConversion(DestinationRecordRef); 'Location-FS Warehouse': SetCompanyId(DestinationRecordRef); 'Service Item-FS Customer Asset': @@ -2518,8 +2543,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 +2708,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 +2743,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) @@ -2780,6 +2806,15 @@ codeunit 6610 "FS Int. Table Subscriber" exit(''); end; + local procedure DisableCustomerAssetConversion(var DestinationRecordRef: RecordRef) + var + CRMProduct: Record "CRM Product"; + begin + DestinationRecordRef.SetTable(CRMProduct); + CRMProduct.ConvertToCustomerAsset := false; + DestinationRecordRef.GetTable(CRMProduct); + end; + [EventSubscriber(ObjectType::Table, Database::"Service Mgt. Setup", 'OnBeforeValidateEvent', 'One Service Item Line/Order', true, false)] local procedure ServiceMgtSetupOnBeforeValidateOneServiceItemLinePerOrder(var Rec: Record "Service Mgt. Setup") var 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..4e1517aeb99 100644 --- a/src/Apps/W1/FieldServiceIntegration/test library/src/FSIntegrationTestLibrary.Codeunit.al +++ b/src/Apps/W1/FieldServiceIntegration/test library/src/FSIntegrationTestLibrary.Codeunit.al @@ -79,12 +79,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..da32168176d 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,103 @@ codeunit 139204 "FS Integration Test" end; [Test] - procedure IgnoreServiceItemWhenConvertToCustomerAssetIsFalse() + [TransactionModel(TransactionModel::AutoCommit)] + procedure InitialItemSynchronizationDisablesCustomerAssetConversion() var - Item: Record Item; - TempServiceItem: Record "Service Item" temporary; - CRMProduct: Record "CRM Product"; CRMIntegrationRecord: Record "CRM Integration Record"; - RecordRef: RecordRef; - IgnoreRecord: Boolean; - ProductId: Guid; + CRMProduct: Record "CRM Product"; + CRMTransactioncurrency: Record "CRM Transactioncurrency"; + CRMUom: Record "CRM Uom"; + Currency: Record Currency; + IntegrationTableMapping: Record "Integration Table Mapping"; + Item: Record Item; + CRMIntegrationTableSynch: Codeunit "CRM Integration Table Synch."; + CRMSetupDefaults: Codeunit "CRM Setup Defaults"; + JobID: Guid; 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] The first synchronization of an item disables native Field Service customer asset creation. Initialize(); - InitSetup(true, ''); - - Item.Get(CreateItem()); - TempServiceItem."Item No." := Item."No."; - RecordRef.GetTable(TempServiceItem); + InitializeItemProductSynchronization(); - ProductId := CreateGuid(); - CRMProduct.ProductId := ProductId; - CRMProduct.ConvertToCustomerAsset := false; - CRMProduct.Insert(false); + // [GIVEN] An uncoupled item whose unit of measure and currency are coupled to Dataverse. + CRMSetupDefaults.ResetItemProductMapping('ITEM-PRODUCT', false); + CreateItemWithCoupledUnitOfMeasure(Item, CRMUom); + LibraryCRMIntegration.CreateCoupledCurrencyAndTransactionCurrency(Currency, CRMTransactioncurrency); + IntegrationTableMapping.Get('ITEM-PRODUCT'); - CRMIntegrationRecord.CoupleCRMIDToRecordID(ProductId, Item.RecordId()); + // [WHEN] The item is synchronized to a new Field Service product. + JobID := CRMIntegrationTableSynch.SynchRecord(IntegrationTableMapping, Item.RecordId(), true, true); - FSIntegrationTestLibrary.IgnoreServiceItemsByConvertToCustomerAssetFlag(RecordRef, IgnoreRecord); - - Assert.IsTrue(IgnoreRecord, 'Service Item should be ignored when Convert to Customer Asset is false.'); + // [THEN] Native Field Service customer asset creation is disabled on the new product. + VerifySuccessfulIntegrationSynchJob(JobID, 1, 0); + Assert.IsTrue(CRMIntegrationRecord.FindByRecordID(Item.RecordId()), 'The item should be coupled to a product.'); + CRMProduct.Get(CRMIntegrationRecord."CRM ID"); + Assert.IsFalse(CRMProduct.ConvertToCustomerAsset, 'Convert to Customer Asset should be disabled.'); end; [Test] - procedure DoNotIgnoreServiceItemWhenConvertToCustomerAssetIsTrue() + [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; + CRMUom: Record "CRM Uom"; + IntegrationFieldMapping: Record "Integration Field Mapping"; + IntegrationTableMapping: Record "Integration Table Mapping"; + CRMIntegrationTableSynch: Codeunit "CRM Integration Table Synch."; + CRMSetupDefaults: Codeunit "CRM Setup Defaults"; + JobID: Guid; begin - // [FEATURE] [Service Item Mapping] - // [SCENARIO] Service Item is not skipped when linked CRM Product has Convert to Customer Asset = Yes. + // [FEATURE] [Item-Product Mapping] + // [SCENARIO] Synchronizing an item disables native Field Service customer asset creation. Initialize(); - InitSetup(true, ''); - - Item.Get(CreateItem()); - TempServiceItem."Item No." := Item."No."; - RecordRef.GetTable(TempServiceItem); + InitializeItemProductSynchronization(); - ProductId := CreateGuid(); - CRMProduct.ProductId := ProductId; + // [GIVEN] A coupled item and product where Convert to Customer Asset is Yes and the existing mapping has no conversion rule. + CRMSetupDefaults.ResetItemProductMapping('ITEM-PRODUCT', false); + CreateItemWithCoupledUnitOfMeasure(Item, CRMUom); + LibraryCRMIntegration.CoupleItem(Item, CRMUom, CRMProduct); CRMProduct.ConvertToCustomerAsset := true; - CRMProduct.Insert(false); - - CRMIntegrationRecord.CoupleCRMIDToRecordID(ProductId, Item.RecordId()); - - FSIntegrationTestLibrary.IgnoreServiceItemsByConvertToCustomerAssetFlag(RecordRef, IgnoreRecord); - - Assert.IsFalse(IgnoreRecord, 'Service Item should not be ignored when Convert to Customer Asset is true.'); - end; - - [Test] - procedure DoNotIgnoreServiceItemWhenItemNoIsBlank() - 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, ''); + CRMProduct.Modify(); - TempServiceItem."Item No." := ''; - RecordRef.GetTable(TempServiceItem); + IntegrationFieldMapping.SetRange("Integration Table Mapping Name", 'ITEM-PRODUCT'); + IntegrationFieldMapping.SetRange("Integration Table Field No.", CRMProduct.FieldNo(ConvertToCustomerAsset)); + IntegrationFieldMapping.DeleteAll(); + IntegrationTableMapping.Get('ITEM-PRODUCT'); - FSIntegrationTestLibrary.IgnoreServiceItemsByConvertToCustomerAssetFlag(RecordRef, IgnoreRecord); + // [WHEN] The item is synchronized to the Field Service product. + JobID := CRMIntegrationTableSynch.SynchRecord(IntegrationTableMapping, Item.RecordId(), true, false); - Assert.IsFalse(IgnoreRecord, 'Service Item with blank Item No. should not be ignored by this filter.'); + // [THEN] Native Field Service customer asset creation is disabled. + VerifySuccessfulIntegrationSynchJob(JobID, 0, 1); + CRMProduct.Get(CRMProduct.ProductId); + Assert.IsFalse(CRMProduct.ConvertToCustomerAsset, 'Convert to Customer Asset should be 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 always disables native Field Service customer asset creation. Initialize(); InitSetup(true, ''); - Item.Get(CreateItem()); - TempServiceItem."Item No." := Item."No."; - RecordRef.GetTable(TempServiceItem); + // [WHEN] The default item-product mapping is reset. + CRMSetupDefaults.ResetItemProductMapping('ITEM-PRODUCT', false); - FSIntegrationTestLibrary.IgnoreServiceItemsByConvertToCustomerAssetFlag(RecordRef, IgnoreRecord); - - Assert.IsFalse(IgnoreRecord, 'Service Item with uncoupled Item should not be ignored by this filter.'); + // [THEN] Convert to Customer Asset is mapped to constant false 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(0, IntegrationFieldMapping."Field No.", 'The mapping should use a constant value.'); + Assert.AreEqual(IntegrationFieldMapping.Direction::ToIntegrationTable, IntegrationFieldMapping.Direction, 'The mapping should be outbound.'); + Assert.AreEqual('false', IntegrationFieldMapping."Constant Value", 'The mapping should disable Convert to Customer Asset.'); end; local procedure Initialize() @@ -1712,6 +1705,51 @@ codeunit 139204 "FS Integration Test" SetTenantLicenseStateToTrial(); end; + local procedure VerifySuccessfulIntegrationSynchJob(JobID: Guid; ExpectedInserted: Integer; ExpectedModified: Integer) + var + IntegrationSynchJob: Record "Integration Synch. Job"; + IntegrationSynchJobErrors: Record "Integration Synch. Job Errors"; + begin + IntegrationSynchJob.Get(JobID); + IntegrationSynchJobErrors.SetRange("Integration Synch. Job ID", JobID); + if IntegrationSynchJobErrors.FindFirst() then + Assert.Fail('Unexpected synchronization error: ' + IntegrationSynchJobErrors.Message); + IntegrationSynchJob.TestField(Failed, 0); + IntegrationSynchJob.TestField(Inserted, ExpectedInserted); + IntegrationSynchJob.TestField(Modified, ExpectedModified); + end; + + local procedure CreateItemWithCoupledUnitOfMeasure(var Item: Record Item; var CRMUom: Record "CRM Uom") + var + ItemUnitOfMeasure: Record "Item Unit of Measure"; + UnitGroup: Record "Unit Group"; + CRMUomschedule: Record "CRM Uomschedule"; + begin + LibraryCRMIntegration.CreateCoupledItemUnitGroupAndUomSchedule(UnitGroup, CRMUomschedule); + Item.GetBySystemId(UnitGroup."Source Id"); + ItemUnitOfMeasure.Get(Item."No.", Item."Base Unit of Measure"); + LibraryCRMIntegration.CoupleItemUnitOfMeasure(ItemUnitOfMeasure, CRMUomschedule, CRMUom); + end; + + local procedure InitializeItemProductSynchronization() + var + CDSCompany: Record "CDS Company"; + CDSConnectionSetup: Record "CDS Connection Setup"; + CRMConnectionSetup: Record "CRM Connection Setup"; + begin + InitSetup(true, ''); + LibraryCRMIntegration.ConfigureCRM(); + LibraryCRMIntegration.CreateCRMOrganization(); + CRMConnectionSetup.Get(); + CRMConnectionSetup.Validate("Is Enabled", false); + CRMConnectionSetup.Modify(true); + CRMConnectionSetup.Validate("Is Enabled", true); + CRMConnectionSetup.Modify(true); + CDSConnectionSetup.Get(); + CDSConnectionSetup.SetBaseCurrencyData(); + LibraryCRMIntegration.EnsureCDSCompany(CDSCompany); + end; + procedure ResetFSEnvironment() var FSConnectionSetup: Record "FS Connection Setup"; @@ -2018,4 +2056,3 @@ codeunit 139204 "FS Integration Test" InventorySetup.Modify(true); end; } -