diff --git a/Apps/W1/FieldServiceIntegration/app/app.json b/Apps/W1/FieldServiceIntegration/app/app.json index 0b22936add..ad974c62c8 100644 --- a/Apps/W1/FieldServiceIntegration/app/app.json +++ b/Apps/W1/FieldServiceIntegration/app/app.json @@ -11,7 +11,14 @@ "contextSensitiveHelpUrl": "https://learn.microsoft.com/dynamics365/business-central/", "url": "https://go.microsoft.com/fwlink/?LinkId=724011", "logo": "ExtensionLogo.png", - "dependencies": [], + "dependencies": [ + { + "id": "10cb69d9-bc8a-4d27-970a-9e110e9db2a5", + "name": "_Exclude_APIV2_", + "publisher": "Microsoft", + "version": "25.0.0.0" + } + ], "internalsVisibleTo": [ { "id": "41b3ab6e-3f20-47c7-a67f-feccc4d58a55", diff --git a/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSArchivedServiceOrdersJob.Codeunit.al b/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSArchivedServiceOrdersJob.Codeunit.al new file mode 100644 index 0000000000..bf9ffdfad5 --- /dev/null +++ b/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSArchivedServiceOrdersJob.Codeunit.al @@ -0,0 +1,147 @@ +// ------------------------------------------------------------------------------------------------ +// 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.DynamicsFieldService; + +using Microsoft.Integration.Dataverse; +using Microsoft.Integration.SyncEngine; +using Microsoft.Service.Document; +using System.Threading; +using Microsoft.Service.Archive; + +codeunit 6618 "FS Archived Service Orders Job" +{ + TableNo = "Job Queue Entry"; + + trigger OnRun() + begin + UpdateOrders(Rec.GetLastLogEntryNo()); + end; + + var + ArchivedOrdersUpdatedMsg: Label 'Archived service orders have been synchronized.'; + + local procedure UpdateOrders(JobLogEntryNo: Integer) + var + FSConnectionSetup: Record "FS Connection Setup"; + begin + if not FSConnectionSetup.IsEnabled() then + exit; + + Codeunit.Run(Codeunit::"CRM Integration Management"); + UpdateArchivedOrders(JobLogEntryNo); + end; + + local procedure UpdateArchivedOrders(JobLogEntryNo: Integer) + var + CRMIntegrationRecord: Record "CRM Integration Record"; + CRMIntegrationRecord2: Record "CRM Integration Record"; + FSWorkOrder: Record "FS Work Order"; + IntegrationTableSynch: Codeunit "Integration Table Synch."; + SynchActionType: Option "None",Insert,Modify,ForceModify,IgnoreUnchanged,Fail,Skip,Delete; + ModifyCounter: Integer; + begin + IntegrationTableSynch.BeginIntegrationSynchJobLoging(TableConnectionType::CRM, Codeunit::"FS Archived Service Orders Job", JobLogEntryNo, Database::"Service Header"); + + CRMIntegrationRecord.SetRange("Archived Service Order", true); + CRMIntegrationRecord.SetRange("Archived Service Order Updated", false); + if CRMIntegrationRecord.FindSet() then + repeat + if FSWorkOrder.Get(CRMIntegrationRecord."CRM ID") then + if UpdateFromServiceHeader(FSWorkOrder) then begin + CRMIntegrationRecord2.GetBySystemId(CRMIntegrationRecord.SystemId); + CRMIntegrationRecord2."Archived Service Order Updated" := true; + CRMIntegrationRecord2.Modify(); + ModifyCounter += 1; + end; + until CRMIntegrationRecord.Next() = 0; + + IntegrationTableSynch.UpdateSynchJobCounters(SynchActionType::Modify, ModifyCounter); + IntegrationTableSynch.EndIntegrationSynchJobWithMsg(ArchivedOrdersUpdatedMsg); + end; + + [TryFunction] + local procedure UpdateFromServiceHeader(var FSWorkOrder: Record "FS Work Order") + begin + ResetFSWorkOrderLineFromServiceOrderLine(FSWorkOrder); + MarkPosted(FSWorkOrder); + end; + + local procedure ResetFSWorkOrderLineFromServiceOrderLine(var FSWorkOrder: Record "FS Work Order") + var + ServiceLineArchive: Record "Service Line Archive"; + FSWorkOrderProduct: Record "FS Work Order Product"; + FSWorkOrderService: Record "FS Work Order Service"; + CRMIntegrationRecord: Record "CRM Integration Record"; + begin + FSWorkOrderProduct.SetRange(WorkOrder, FSWorkOrder.WorkOrderId); + if FSWorkOrderProduct.FindSet() then + repeat + if CRMIntegrationRecord.FindByCRMID(FSWorkOrderProduct.WorkOrderProductId) then + if ServiceLineArchive.GetBySystemId(CRMIntegrationRecord."Archived Service Line Id") then + UpdateWorkOrderProduct(ServiceLineArchive, FSWorkOrderProduct); + until FSWorkOrderProduct.Next() = 0; + + FSWorkOrderService.SetRange(WorkOrder, FSWorkOrder.WorkOrderId); + if FSWorkOrderService.FindSet() then + repeat + if CRMIntegrationRecord.FindByCRMID(FSWorkOrderService.WorkOrderServiceId) then + if ServiceLineArchive.GetBySystemId(CRMIntegrationRecord."Archived Service Line Id") then + UpdateWorkOrderService(ServiceLineArchive, FSWorkOrderService); + until FSWorkOrderService.Next() = 0; + end; + + local procedure MarkPosted(var FSWorkOrder: Record "FS Work Order") + begin + FSWorkOrder.SystemStatus := FSWorkOrder.SystemStatus::Posted; + FSWorkOrder.Modify(); + end; + + internal procedure UpdateWorkOrderProduct(ServiceLineArchive: Record "Service Line Archive"; var FSWorkOrderProduct: Record "FS Work Order Product") + var + Modified: Boolean; + begin + if FSWorkOrderProduct.QuantityShipped <> (ServiceLineArchive."Quantity Shipped" + ServiceLineArchive."Qty. to Ship") then begin + FSWorkOrderProduct.QuantityShipped := ServiceLineArchive."Quantity Shipped" + ServiceLineArchive."Qty. to Ship"; + Modified := true; + end; + + if FSWorkOrderProduct.QuantityInvoiced <> (ServiceLineArchive."Quantity Invoiced" + ServiceLineArchive."Qty. to Invoice") then begin + FSWorkOrderProduct.QuantityInvoiced := ServiceLineArchive."Quantity Invoiced" + ServiceLineArchive."Qty. to Invoice"; + Modified := true; + end; + + if FSWorkOrderProduct.QuantityConsumed <> ServiceLineArchive."Quantity Consumed" + ServiceLineArchive."Qty. to Consume" then begin + FSWorkOrderProduct.QuantityConsumed := ServiceLineArchive."Quantity Consumed" + ServiceLineArchive."Qty. to Consume"; + Modified := true; + end; + + if Modified then + FSWorkOrderProduct.Modify(); + end; + + internal procedure UpdateWorkOrderService(ServiceLineArchive: Record "Service Line Archive"; var FSWorkOrderService: Record "FS Work Order Service") + var + Modified: Boolean; + begin + if FSWorkOrderService.DurationShipped <> (ServiceLineArchive."Quantity Shipped" + ServiceLineArchive."Qty. to Ship") then begin + FSWorkOrderService.DurationShipped := (ServiceLineArchive."Quantity Shipped" + ServiceLineArchive."Qty. to Ship") * 60; + Modified := true; + end; + + if FSWorkOrderService.DurationInvoiced <> (ServiceLineArchive."Quantity Invoiced" + ServiceLineArchive."Qty. to Invoice") then begin + FSWorkOrderService.DurationInvoiced := (ServiceLineArchive."Quantity Invoiced" + ServiceLineArchive."Qty. to Invoice") * 60; + Modified := true; + end; + + if FSWorkOrderService.DurationConsumed <> ServiceLineArchive."Quantity Consumed" + ServiceLineArchive."Qty. to Consume" then begin + FSWorkOrderService.DurationConsumed := (ServiceLineArchive."Quantity Consumed" + ServiceLineArchive."Qty. to Consume") * 60; + Modified := true; + end; + + if Modified then + FSWorkOrderService.Modify(); + end; +} + diff --git a/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSIntTableSubscriber.Codeunit.al b/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSIntTableSubscriber.Codeunit.al index 58f202d072..05911e7661 100644 --- a/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSIntTableSubscriber.Codeunit.al +++ b/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSIntTableSubscriber.Codeunit.al @@ -9,6 +9,7 @@ using Microsoft.Projects.Project.Job; using Microsoft.Foundation.NoSeries; using Microsoft.Foundation.UOM; using Microsoft.Projects.Project.Setup; +using Microsoft.Service.Item; using Microsoft.Integration.SyncEngine; using Microsoft.Inventory.Setup; using Microsoft.Sales.Customer; @@ -18,9 +19,12 @@ using Microsoft.Projects.Project.Journal; using Microsoft.Projects.Resources.Resource; using Microsoft.Integration.D365Sales; using Microsoft.Inventory.Item; +using Microsoft.Service.Archive; +using Microsoft.Service.Document; using Microsoft.Projects.Project.Planning; using Microsoft.Projects.Project.Ledger; using Microsoft.Sales.History; +using Microsoft.Service.Setup; codeunit 6610 "FS Int. Table Subscriber" { @@ -117,6 +121,30 @@ codeunit 6610 "FS Int. Table Subscriber" end; end; + [EventSubscriber(ObjectType::Table, Database::"Integration Table Mapping", 'OnBeforeModifyEvent', '', false, false)] + local procedure IntegrationTableMappingOnBeforeModifyEvent(var Rec: Record "Integration Table Mapping"; RunTrigger: Boolean) + var + FSConnectionSetup: Record "FS Connection Setup"; + ServiceItem: Record "Service Item"; + MandatoryFilterErr: Label '"%1" must be included in the filter. If you need this behavior, please contact your partner for assistance.', Comment = '%1 = a field caption'; + begin + if not FSConnectionSetup.IsEnabled() then + exit; + if not RunTrigger then + exit; + if Rec.IsTemporary() then + exit; + + if Rec."Table ID" <> Database::"Service Item" then + exit; + + ServiceItem.SetView(Rec.GetTableFilter()); + if ServiceItem.GetFilter(SystemId) <> '' then + exit; + if ServiceItem.GetFilter("Service Item Components") = '' then + Error(MandatoryFilterErr, ServiceItem.FieldCaption("Service Item Components")); + end; + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Integration Rec. Synch. Invoke", 'OnBeforeTransferRecordFields', '', false, false)] local procedure OnBeforeTransferRecordFields(SourceRecordRef: RecordRef; var DestinationRecordRef: RecordRef) var @@ -124,14 +152,24 @@ codeunit 6610 "FS Int. Table Subscriber" FSProjectTask: Record "FS Project Task"; FSWorkOrderProduct: Record "FS Work Order Product"; FSWorkOrderService: Record "FS Work Order Service"; + FSBookableResourceBooking: Record "FS Bookable Resource Booking"; CRMIntegrationRecord: Record "CRM Integration Record"; + FSWorkOrderIncident: Record "FS Work Order Incident"; + FSIncident: Record "FS Incident Type"; JobJournalLine: Record "Job Journal Line"; JobTask: Record "Job Task"; + ServiceHeader: Record "Service Header"; + ServiceItemLine: Record "Service Item Line"; + ServiceLine: Record "Service Line"; + DefaultIncidentLbl: Label 'Service Order Incident'; + SourceDestCode: Text; begin if not FSConnectionSetup.IsEnabled() then exit; - case GetSourceDestCode(SourceRecordRef, DestinationRecordRef) of + SourceDestCode := GetSourceDestCode(SourceRecordRef, DestinationRecordRef); + + case SourceDestCode of 'FS Work Order Product-Job Journal Line', 'FS Work Order Service-Job Journal Line': begin @@ -155,6 +193,164 @@ codeunit 6610 "FS Int. Table Subscriber" JobJournalLine."Job Task No." := JobTask."Job Task No."; DestinationRecordRef.GetTable(JobJournalLine); end; + 'FS Work Order Incident-Service Item Line': + begin + SourceRecordRef.SetTable(FSWorkOrderIncident); + DestinationRecordRef.SetTable(ServiceItemLine); + + if ServiceItemLine."Document No." <> '' then + exit; + + if CRMIntegrationRecord.FindByCRMID(FSWorkOrderIncident.WorkOrder) then + ServiceHeader.GetBySystemId(CRMIntegrationRecord."Integration ID"); + + ServiceItemLine."Document Type" := ServiceItemLine."Document Type"::Order; + ServiceItemLine."Document No." := ServiceHeader."No."; + ServiceItemLine."Line No." := GetNextLineNo(ServiceItemLine); + + if IsNullGuid(FSWorkOrderIncident.CustomerAsset) then + if FSIncident.Get(FSWorkOrderIncident.IncidentType) then + ServiceItemLine.Description := FSIncident.Name + else + ServiceItemLine.Description := DefaultIncidentLbl; + + DestinationRecordRef.GetTable(ServiceItemLine); + end; + 'FS Work Order Product-Service Line': + begin + SourceRecordRef.SetTable(FSWorkOrderProduct); + DestinationRecordRef.SetTable(ServiceLine); + + if ServiceLine."Document No." <> '' then + exit; + + if CRMIntegrationRecord.FindByCRMID(FSWorkOrderProduct.WorkOrder) then + ServiceHeader.GetBySystemId(CRMIntegrationRecord."Integration ID"); + + ServiceLine."Document Type" := ServiceLine."Document Type"::Order; + ServiceLine."Document No." := ServiceHeader."No."; + ServiceLine."Line No." := GetNextLineNo(ServiceLine); + ServiceLine."Service Item Line No." := ServiceItemLine."Line No."; + ServiceLine."Service Item No." := ServiceItemLine."Service Item No."; + ServiceLine.Type := ServiceLine.Type::Item; + + DestinationRecordRef.GetTable(ServiceLine); + end; + 'FS Work Order Service-Service Line': + begin + SourceRecordRef.SetTable(FSWorkOrderService); + DestinationRecordRef.SetTable(ServiceLine); + + if ServiceLine."Document No." <> '' then + exit; + + if CRMIntegrationRecord.FindByCRMID(FSWorkOrderService.WorkOrder) then + ServiceHeader.GetBySystemId(CRMIntegrationRecord."Integration ID"); + + ServiceLine."Document Type" := ServiceLine."Document Type"::Order; + ServiceLine."Document No." := ServiceHeader."No."; + ServiceLine."Line No." := GetNextLineNo(ServiceLine); + ServiceLine."Service Item Line No." := ServiceItemLine."Line No."; + ServiceLine."Service Item No." := ServiceItemLine."Service Item No."; + ServiceLine.Type := ServiceLine.Type::Item; + + DestinationRecordRef.GetTable(ServiceLine); + end; + 'FS Bookable Resource Booking-Service Line': + begin + SourceRecordRef.SetTable(FSBookableResourceBooking); + DestinationRecordRef.SetTable(ServiceLine); + + if ServiceLine."Document No." <> '' then + exit; + + if CRMIntegrationRecord.FindByCRMID(FSBookableResourceBooking.WorkOrder) then + ServiceHeader.GetBySystemId(CRMIntegrationRecord."Integration ID"); + + ServiceLine."Document Type" := ServiceLine."Document Type"::Order; + ServiceLine."Document No." := ServiceHeader."No."; + ServiceLine."Line No." := GetNextLineNo(ServiceLine); + ServiceLine.Type := ServiceLine.Type::Resource; + + DestinationRecordRef.GetTable(ServiceLine); + end; + end; + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Integration Rec. Synch. Invoke", 'OnAfterTransferRecordFields', '', false, false)] + local procedure OnAfterTransferRecordFields(SourceRecordRef: RecordRef; var DestinationRecordRef: RecordRef; var AdditionalFieldsWereModified: 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"; + ServiceLine: Record "Service Line"; + SourceDestCode: Text; + begin + if not FSConnectionSetup.IsEnabled() then + exit; + + SourceDestCode := GetSourceDestCode(SourceRecordRef, DestinationRecordRef); + + case SourceDestCode of + 'FS Work Order Product-Service Line': + begin + SourceRecordRef.SetTable(FSWorkOrderProduct); + DestinationRecordRef.SetTable(ServiceLine); + + UpdateQuantities(FSWorkOrderProduct, ServiceLine, false); + AdditionalFieldsWereModified := true; + + SourceRecordRef.GetTable(FSWorkOrderProduct); + DestinationRecordRef.GetTable(ServiceLine); + end; + 'Service Line-FS Work Order Product': + begin + SourceRecordRef.SetTable(ServiceLine); + DestinationRecordRef.SetTable(FSWorkOrderProduct); + + UpdateQuantities(FSWorkOrderProduct, ServiceLine, true); + AdditionalFieldsWereModified := true; + + SourceRecordRef.GetTable(ServiceLine); + DestinationRecordRef.GetTable(FSWorkOrderProduct); + end; + + + 'FS Work Order Service-Service Line': + begin + SourceRecordRef.SetTable(FSWorkOrderService); + DestinationRecordRef.SetTable(ServiceLine); + + UpdateQuantities(FSWorkOrderService, ServiceLine, false); + AdditionalFieldsWereModified := true; + + SourceRecordRef.GetTable(FSWorkOrderService); + DestinationRecordRef.GetTable(ServiceLine); + end; + 'Service Line-FS Work Order Service': + begin + SourceRecordRef.SetTable(ServiceLine); + DestinationRecordRef.SetTable(FSWorkOrderService); + + UpdateQuantities(FSWorkOrderService, ServiceLine, true); + AdditionalFieldsWereModified := true; + + SourceRecordRef.GetTable(ServiceLine); + DestinationRecordRef.GetTable(FSWorkOrderService); + end; + + + 'FS Bookable Resource Booking-Service Line': + begin + SourceRecordRef.SetTable(FSBookableResourceBooking); + DestinationRecordRef.SetTable(ServiceLine); + + UpdateQuantities(FSBookableResourceBooking, ServiceLine); + AdditionalFieldsWereModified := true; + + DestinationRecordRef.GetTable(ServiceLine); + end; end; end; @@ -162,21 +358,29 @@ codeunit 6610 "FS Int. Table Subscriber" local procedure OnTransferFieldData(SourceFieldRef: FieldRef; DestinationFieldRef: FieldRef; var NewValue: Variant; var IsValueFound: Boolean; var NeedsConversion: Boolean) var FSConnectionSetup: Record "FS Connection Setup"; + CRMIntegrationRecord: Record "CRM Integration Record"; + FSWorkOrder: Record "FS Work Order"; FSWorkOrderService: Record "FS Work Order Service"; FSWorkOrderProduct: Record "FS Work Order Product"; FSBookableResourceBooking: Record "FS Bookable Resource Booking"; JobJournalLine: Record "Job Journal Line"; + ServiceHeader: Record "Service Header"; + ServiceLine: Record "Service Line"; + ItemUnitOfMeasure: Record "Item Unit of Measure"; SourceRecordRef: RecordRef; DestinationRecordRef: RecordRef; + NAVItemUomRecordId: RecordId; DurationInHours: Decimal; DurationInMinutes: Decimal; Quantity: Decimal; QuantityToTransferToInvoice: Decimal; QuantityCurrentlyConsumed: Decimal; QuantityCurrentlyInvoiced: Decimal; + NotCoupledCRMUomErr: Label 'The unit is not coupled to a unit of measure.'; begin if not FSConnectionSetup.IsEnabled() then exit; + if IsValueFound then exit; @@ -184,17 +388,110 @@ codeunit 6610 "FS Int. Table Subscriber" if SourceFieldRef.Record().Number() = DestinationFieldRef.Record().Number() then exit; - if SourceFieldRef.Record().Number = Database::"FS Work Order Service" then + if (SourceFieldRef.Record().Number = Database::"Service Header") and + (DestinationFieldRef.Record().Number = Database::"FS Work Order") then + case DestinationFieldRef.Name() of + FSWorkOrder.FieldName(SystemStatus): + begin + SourceFieldRef.Record().SetTable(ServiceHeader); + DestinationFieldRef.Record().SetTable(FSWorkOrder); + NewValue := FSWorkOrder.SystemStatus; // default -> no update + IsValueFound := true; + NeedsConversion := false; + end; + end; + if (SourceFieldRef.Record().Number = Database::"FS Work Order") and + (DestinationFieldRef.Record().Number = Database::"Service Header") then + case DestinationFieldRef.Name() of + ServiceHeader.FieldName(Status): + begin + SourceFieldRef.Record().SetTable(FSWorkOrder); + DestinationFieldRef.Record().SetTable(ServiceHeader); + + case FSWorkOrder.SystemStatus of + FSWorkOrder.SystemStatus::Unscheduled, + FSWorkOrder.SystemStatus::Scheduled: + NewValue := ServiceHeader.Status::Pending; + FSWorkOrder.SystemStatus::InProgress: + NewValue := ServiceHeader.Status::"In Process"; + FSWorkOrder.SystemStatus::Completed: + NewValue := ServiceHeader.Status::Finished; + else + NewValue := ServiceHeader.Status; // default -> no update + end; + + IsValueFound := true; + NeedsConversion := false; + end; + end; + + if (SourceFieldRef.Record().Number = Database::"Service Line") and + (DestinationFieldRef.Record().Number = Database::"FS Work Order Service") then + case DestinationFieldRef.Name() of + FSWorkOrderService.FieldName(DurationShipped): + begin + SourceRecordRef := SourceFieldRef.Record(); + SourceRecordRef.SetTable(ServiceLine); + DurationInHours := ServiceLine."Quantity Shipped"; + DurationInMinutes := DurationInHours * 60; + NewValue := DurationInMinutes; + IsValueFound := true; + NeedsConversion := false; + end; + FSWorkOrderService.FieldName(DurationInvoiced): + begin + SourceRecordRef := SourceFieldRef.Record(); + SourceRecordRef.SetTable(ServiceLine); + DurationInHours := ServiceLine."Quantity Invoiced"; + DurationInMinutes := DurationInHours * 60; + NewValue := DurationInMinutes; + IsValueFound := true; + NeedsConversion := false; + end; + FSWorkOrderService.FieldName(DurationConsumed): + begin + SourceRecordRef := SourceFieldRef.Record(); + SourceRecordRef.SetTable(ServiceLine); + DurationInHours := ServiceLine."Quantity Consumed"; + DurationInMinutes := DurationInHours * 60; + NewValue := DurationInMinutes; + IsValueFound := true; + NeedsConversion := false; + end; + end; + + if (SourceFieldRef.Record().Number = Database::"FS Work Order") then case SourceFieldRef.Name() of + FSWorkOrder.FieldName(CreatedOn): + begin + SourceRecordRef := SourceFieldRef.Record(); + SourceRecordRef.SetTable(FSWorkOrder); + NewValue := DT2Date(FSWorkOrder.CreatedOn); + IsValueFound := true; + NeedsConversion := false; + end; + end; + + if (SourceFieldRef.Record().Number = Database::"FS Work Order Service") then + case SourceFieldRef.Name() of + FSWorkOrderService.FieldName(EstimateDuration): + begin + SourceRecordRef := SourceFieldRef.Record(); + SourceRecordRef.SetTable(FSWorkOrderService); + DurationInMinutes := FSWorkOrderService.EstimateDuration; + DurationInHours := (DurationInMinutes / 60); + NewValue := DurationInHours; + IsValueFound := true; + NeedsConversion := false; + end; FSWorkOrderService.FieldName(Duration), FSWorkOrderService.FieldName(DurationToBill): begin SourceRecordRef := SourceFieldRef.Record(); SourceRecordRef.SetTable(FSWorkOrderService); - SetCurrentProjectPlanningQuantities(SourceRecordRef, QuantityCurrentlyConsumed, QuantityCurrentlyInvoiced); - DestinationRecordRef := DestinationFieldRef.Record(); - DestinationRecordRef.SetTable(JobJournalLine); - if SourceFieldRef.Name() = FSWorkOrderService.FieldName(Duration) then begin + if DestinationFieldRef.Record().Number = Database::"Job Journal Line" then + SetCurrentProjectPlanningQuantities(SourceRecordRef, QuantityCurrentlyConsumed, QuantityCurrentlyInvoiced); + if SourceFieldRef.Name() in [FSWorkOrderService.FieldName(Duration)] then begin DurationInMinutes := FSWorkOrderService.Duration; DurationInHours := (DurationInMinutes / 60); NewValue := DurationInHours - QuantityCurrentlyConsumed; @@ -202,10 +499,14 @@ codeunit 6610 "FS Int. Table Subscriber" if SourceFieldRef.Name() = FSWorkOrderService.FieldName(DurationToBill) then begin DurationInMinutes := FSWorkOrderService.DurationToBill; DurationInHours := (DurationInMinutes / 60); - if JobJournalLine."Line Type" in [JobJournalLine."Line Type"::Budget, JobJournalLine."Line Type"::" "] then - NewValue := 0 - else - NewValue := DurationInHours - QuantityCurrentlyInvoiced; + NewValue := DurationInHours - QuantityCurrentlyInvoiced; + + if (DestinationFieldRef.Record().Number = Database::"Job Journal Line") then begin + DestinationRecordRef := DestinationFieldRef.Record(); + DestinationRecordRef.SetTable(JobJournalLine); + if JobJournalLine."Line Type" in [JobJournalLine."Line Type"::Budget, JobJournalLine."Line Type"::" "] then + NewValue := 0; + end; end; IsValueFound := true; NeedsConversion := false; @@ -213,6 +514,9 @@ codeunit 6610 "FS Int. Table Subscriber" end; FSWorkOrderService.FieldName(Description): begin + if (DestinationFieldRef.Record().Number <> Database::"Job Journal Line") then + exit; + SourceRecordRef := SourceFieldRef.Record(); SourceRecordRef.SetTable(FSWorkOrderService); DestinationRecordRef := DestinationFieldRef.Record(); @@ -237,14 +541,15 @@ codeunit 6610 "FS Int. Table Subscriber" end; end; - if SourceFieldRef.Record().Number = Database::"FS Work Order Product" then + if (SourceFieldRef.Record().Number = Database::"FS Work Order Product") then case SourceFieldRef.Name() of FSWorkOrderProduct.FieldName(Quantity), FSWorkOrderProduct.FieldName(QtyToBill): begin SourceRecordRef := SourceFieldRef.Record(); SourceRecordRef.SetTable(FSWorkOrderProduct); - SetCurrentProjectPlanningQuantities(SourceRecordRef, QuantityCurrentlyConsumed, QuantityCurrentlyInvoiced); + if DestinationFieldRef.Record().Number = Database::"Job Journal Line" then + SetCurrentProjectPlanningQuantities(SourceRecordRef, QuantityCurrentlyConsumed, QuantityCurrentlyInvoiced); if SourceFieldRef.Name() = FSWorkOrderProduct.FieldName(Quantity) then begin Quantity := FSWorkOrderProduct.Quantity - QuantityCurrentlyConsumed; NewValue := Quantity; @@ -267,6 +572,239 @@ codeunit 6610 "FS Int. Table Subscriber" NeedsConversion := false; exit; end; + FSWorkOrderProduct.FieldName(Unit): + begin + SourceRecordRef := SourceFieldRef.Record(); + SourceRecordRef.SetTable(FSWorkOrderProduct); + + if not CRMIntegrationRecord.FindRecordIDFromID(FSWorkOrderProduct.Unit, Database::"Item Unit of Measure", NAVItemUomRecordId) then + Error(NotCoupledCRMUomErr); + + if not ItemUnitOfMeasure.Get(NAVItemUomRecordId) then + Error(NotCoupledCRMUomErr); + + NewValue := ItemUnitOfMeasure.Code; + IsValueFound := true; + NeedsConversion := false; + exit; + end; + end; + end; + + internal procedure UpdateQuantities(var FSWorkOrderProduct: Record "FS Work Order Product"; var ServiceLine: Record "Service Line"; ToFieldService: Boolean) + begin + if FSWorkOrderProduct.LineStatus = FSWorkOrderProduct.LineStatus::Estimated then begin + if ToFieldService then + FSWorkOrderProduct.EstimateQuantity := ServiceLine.Quantity + else + ServiceLine.Validate(Quantity, FSWorkOrderProduct.EstimateQuantity); + + ServiceLine.Validate("Qty. to Ship", 0); + ServiceLine.Validate("Qty. to Invoice", 0); + ServiceLine.Validate("Qty. to Consume", 0); + + if ToFieldService then + ServiceLine.Modify(true); + end else begin + ServiceLine.Validate(Quantity, GetMaxQuantity(FSWorkOrderProduct.Quantity, FSWorkOrderProduct.QtyToBill)); + ServiceLine.Validate("Qty. to Ship", GetMaxQuantity(FSWorkOrderProduct.Quantity, FSWorkOrderProduct.QtyToBill) - ServiceLine."Quantity Shipped"); + ServiceLine.Validate("Qty. to Invoice", FSWorkOrderProduct.QtyToBill - ServiceLine."Quantity Invoiced"); + end; + end; + + internal procedure UpdateQuantities(var FSWorkOrderService: Record "FS Work Order Service"; var ServiceLine: Record "Service Line"; ToFieldService: Boolean) + begin + if FSWorkOrderService.LineStatus = FSWorkOrderService.LineStatus::Estimated then begin + if ToFieldService then + FSWorkOrderService.EstimateDuration := ServiceLine.Quantity * 60 + else + ServiceLine.Validate(Quantity, FSWorkOrderService.EstimateDuration / 60); + + ServiceLine.Validate("Qty. to Ship", 0); + ServiceLine.Validate("Qty. to Invoice", 0); + ServiceLine.Validate("Qty. to Consume", 0); + + if ToFieldService then + ServiceLine.Modify(true); + end else begin + ServiceLine.Validate(Quantity, GetMaxQuantity(FSWorkOrderService.Duration, FSWorkOrderService.DurationToBill) / 60); + ServiceLine.Validate("Qty. to Ship", GetMaxQuantity(FSWorkOrderService.Duration, FSWorkOrderService.DurationToBill) / 60 - ServiceLine."Quantity Shipped"); + ServiceLine.Validate("Qty. to Invoice", FSWorkOrderService.DurationToBill / 60 - ServiceLine."Quantity Invoiced"); + end; + end; + + internal procedure UpdateQuantities(FSBookableResourceBooking: Record "FS Bookable Resource Booking"; var ServiceLine: Record "Service Line") + begin + if ServiceLine."Qty. to Consume" <> 0 then + ServiceLine.Validate("Qty. to Consume", 0); + ServiceLine.Validate(Quantity, FSBookableResourceBooking.Duration / 60); + ServiceLine.Validate("Qty. to Consume", ServiceLine.Quantity - ServiceLine."Quantity Consumed"); + end; + + procedure GetMaxQuantity(Quantity1: Decimal; Quantity2: Decimal): Decimal + var + MaxQuantity: Decimal; + begin + MaxQuantity := Quantity1; + + if Quantity2 > MaxQuantity then + MaxQuantity := Quantity2; + + exit(MaxQuantity); + end; + + procedure GetMaxQuantity(Quantity1: Decimal; Quantity2: Decimal; Quantity3: Decimal): Decimal + var + MaxQuantity: Decimal; + begin + MaxQuantity := Quantity1; + + if Quantity2 > MaxQuantity then + MaxQuantity := Quantity2; + + if Quantity3 > MaxQuantity then + MaxQuantity := Quantity3; + + exit(MaxQuantity); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"CRM Int. Table. Subscriber", 'OnFindNewValueForCoupledRecordPK', '', false, false)] + local procedure OnFindNewValueForCoupledRecordPK(IntegrationTableMapping: Record "Integration Table Mapping"; SourceFieldRef: FieldRef; DestinationFieldRef: FieldRef; var NewValue: Variant; var IsValueFound: Boolean) + var + FSConnectionSetup: Record "FS Connection Setup"; + CRMIntegrationRecord: Record "CRM Integration Record"; + FSWorkOrderService: Record "FS Work Order Service"; + FSWorkOrderProduct: Record "FS Work Order Product"; + ServiceItemLine: Record "Service Item Line"; + ServiceLine: Record "Service Line"; + ItemUnitOfMeasure: Record "Item Unit of Measure"; + CRMUom: Record "CRM Uom"; + SourceRecordRef: RecordRef; + NAVItemUomRecordId: RecordId; + WorkOrderIncidentId: Guid; + NAVItemUomId: Guid; + NotCoupledCRMUomErr: Label 'The unit is not coupled to a unit of measure.'; + begin + if not FSConnectionSetup.IsEnabled() then + exit; + + if (SourceFieldRef.Record().Number = Database::"FS Work Order Product") and + (DestinationFieldRef.Record().Number = Database::"Service Line") then + case SourceFieldRef.Name() of + FSWorkOrderProduct.FieldName(WorkOrderIncident): + begin + SourceFieldRef.Record().SetTable(FSWorkOrderProduct); + DestinationFieldRef.Record().SetTable(ServiceLine); + if CRMIntegrationRecord.FindByCRMID(FSWorkOrderProduct.WorkOrderIncident) then begin + ServiceItemLine.GetBySystemId(CRMIntegrationRecord."Integration ID"); + NewValue := ServiceItemLine."Line No."; + IsValueFound := true; + end; + end; + + FSWorkOrderProduct.FieldName(Unit): + begin + SourceRecordRef := SourceFieldRef.Record(); + SourceRecordRef.SetTable(FSWorkOrderProduct); + + if not CRMIntegrationRecord.FindRecordIDFromID(FSWorkOrderProduct.Unit, Database::"Item Unit of Measure", NAVItemUomRecordId) then + Error(NotCoupledCRMUomErr); + + if not ItemUnitOfMeasure.Get(NAVItemUomRecordId) then + Error(NotCoupledCRMUomErr); + + NewValue := ItemUnitOfMeasure.Code; + IsValueFound := true; + exit; + end; + end; + if (SourceFieldRef.Record().Number = Database::"FS Work Order Service") and + (DestinationFieldRef.Record().Number = Database::"Service Line") then + case SourceFieldRef.Name() of + FSWorkOrderProduct.FieldName(WorkOrderIncident): + begin + SourceFieldRef.Record().SetTable(FSWorkOrderService); + DestinationFieldRef.Record().SetTable(ServiceLine); + if CRMIntegrationRecord.FindByCRMID(FSWorkOrderService.WorkOrderIncident) then begin + ServiceItemLine.GetBySystemId(CRMIntegrationRecord."Integration ID"); + NewValue := ServiceItemLine."Line No."; + IsValueFound := true; + end; + end; + + FSWorkOrderService.FieldName(Unit): + begin + SourceRecordRef := SourceFieldRef.Record(); + SourceRecordRef.SetTable(FSWorkOrderService); + + if not CRMIntegrationRecord.FindRecordIDFromID(FSWorkOrderService.Unit, Database::"Item Unit of Measure", NAVItemUomRecordId) then + Error(NotCoupledCRMUomErr); + + if not ItemUnitOfMeasure.Get(NAVItemUomRecordId) then + Error(NotCoupledCRMUomErr); + + NewValue := ItemUnitOfMeasure.Code; + IsValueFound := true; + exit; + end; + end; + + if (SourceFieldRef.Record().Number = Database::"Service Line") and + (DestinationFieldRef.Record().Number = Database::"FS Work Order Product") then + case SourceFieldRef.Name() of + ServiceLine.FieldName("Service Item Line No."): + begin + SourceFieldRef.Record().SetTable(ServiceLine); + if CRMIntegrationRecord.FindIDFromRecordID(GetServiceOrderItemLineRecordId(ServiceLine."Document No.", ServiceLine."Service Item Line No."), WorkOrderIncidentId) then begin + NewValue := WorkOrderIncidentId; + IsValueFound := true; + end; + end; + end; + + if (SourceFieldRef.Record().Number = Database::"Service Line") and + (DestinationFieldRef.Record().Number = Database::"FS Work Order Service") then + case SourceFieldRef.Name() of + ServiceLine.FieldName("Service Item Line No."): + begin + SourceFieldRef.Record().SetTable(ServiceLine); + if CRMIntegrationRecord.FindIDFromRecordID(GetServiceOrderItemLineRecordId(ServiceLine."Document No.", ServiceLine."Service Item Line No."), WorkOrderIncidentId) then begin + NewValue := WorkOrderIncidentId; + IsValueFound := true; + end; + end; + end; + + if (SourceFieldRef.Record().Number = Database::"Service Line") and + (DestinationFieldRef.Record().Number in [Database::"FS Work Order Product", Database::"FS Work Order Service"]) then + case SourceFieldRef.Name() of + ServiceLine.FieldName("Service Item Line No."): + begin + SourceFieldRef.Record().SetTable(ServiceLine); + if CRMIntegrationRecord.FindByCRMID(FSWorkOrderProduct.WorkOrderIncident) then begin + ServiceItemLine.GetBySystemId(CRMIntegrationRecord."Integration ID"); + NewValue := ServiceItemLine."Line No."; + IsValueFound := true; + end; + end; + ServiceLine.FieldName("Unit of Measure Code"): + begin + SourceRecordRef := SourceFieldRef.Record(); + SourceRecordRef.SetTable(ServiceLine); + + if not ItemUnitOfMeasure.Get(ServiceLine."No.", ServiceLine."Unit of Measure Code") then + Error(NotCoupledCRMUomErr); + + if not CRMIntegrationRecord.FindIDFromRecordID(ItemUnitOfMeasure.RecordId, NAVItemUomId) then + Error(NotCoupledCRMUomErr); + + if not CRMUom.Get(NAVItemUomId) then + Error(NotCoupledCRMUomErr); + + NewValue := CRMUom.UoMId; + IsValueFound := true; + exit; + end; end; end; @@ -282,6 +820,7 @@ codeunit 6610 "FS Int. Table Subscriber" SalesInvoiceHeader: Record "Sales Invoice Header"; JobPlanningLineInvoice: Record "Job Planning Line Invoice"; JobUsageLink: Record "Job Usage Link"; + ArchivedServiceOrders: List of [Code[20]]; SourceDestCode: Text; begin if not FSConnectionSetup.IsEnabled() then @@ -342,6 +881,20 @@ codeunit 6610 "FS Int. Table Subscriber" end; until JobPlanningLineInvoice.Next() = 0; end; + 'FS Work Order-Service Header': + begin + ValidateServiceHeaderAfterInsert(DestinationRecordRef); + ResetServiceOrderItemLineFromFSWorkOrderIncident(SourceRecordRef, DestinationRecordRef, ArchivedServiceOrders); + ResetServiceOrderLineFromFSWorkOrderProduct(SourceRecordRef, DestinationRecordRef, ArchivedServiceOrders); + ResetServiceOrderLineFromFSWorkOrderService(SourceRecordRef, DestinationRecordRef, ArchivedServiceOrders); + ResetServiceOrderLineFromFSBookableResourceBooking(SourceRecordRef, DestinationRecordRef, ArchivedServiceOrders); + end; + 'Service Header-FS Work Order': + begin + ResetFSWorkOrderIncidentFromServiceOrderItemLine(SourceRecordRef, DestinationRecordRef); + ResetFSWorkOrderProductFromServiceOrderLine(SourceRecordRef, DestinationRecordRef); + ResetFSWorkOrderServiceFromServiceOrderLine(SourceRecordRef, DestinationRecordRef); + end; end; end; @@ -360,6 +913,8 @@ codeunit 6610 "FS Int. Table Subscriber" JobJournalLine: Record "Job Journal Line"; FSWorkOrderProduct: Record "FS Work Order Product"; FSWorkOrderService: Record "FS Work Order Service"; + ServiceHeader: Record "Service Header"; + ArchivedServiceOrders: List of [Code[20]]; SourceDestCode: Text; begin if not FSConnectionSetup.IsEnabled() then @@ -376,18 +931,464 @@ codeunit 6610 "FS Int. Table Subscriber" end; 'FS Work Order Service-Job Journal Line': begin - SourceRecordRef.SetTable(FSWorkOrderService); - DestinationRecordRef.SetTable(JobJournalLine); - UpdateCorrelatedJobJournalLine(SourceRecordRef, DestinationRecordRef); - ConditionallyPostJobJournalLine(FSConnectionSetup, FSWorkOrderService, JobJournalLine); + SourceRecordRef.SetTable(FSWorkOrderService); + DestinationRecordRef.SetTable(JobJournalLine); + UpdateCorrelatedJobJournalLine(SourceRecordRef, DestinationRecordRef); + ConditionallyPostJobJournalLine(FSConnectionSetup, FSWorkOrderService, JobJournalLine); + end; + 'FS Work Order-Service Header': + begin + ResetServiceOrderItemLineFromFSWorkOrderIncident(SourceRecordRef, DestinationRecordRef, ArchivedServiceOrders); + ResetServiceOrderLineFromFSWorkOrderProduct(SourceRecordRef, DestinationRecordRef, ArchivedServiceOrders); + ResetServiceOrderLineFromFSWorkOrderService(SourceRecordRef, DestinationRecordRef, ArchivedServiceOrders); + ResetServiceOrderLineFromFSBookableResourceBooking(SourceRecordRef, DestinationRecordRef, ArchivedServiceOrders); + end; + 'Service Header-FS Work Order': + begin + ProofAllServiceItemLinesAssigned(ServiceHeader); + ResetFSWorkOrderIncidentFromServiceOrderItemLine(SourceRecordRef, DestinationRecordRef); + ResetFSWorkOrderProductFromServiceOrderLine(SourceRecordRef, DestinationRecordRef); + ResetFSWorkOrderServiceFromServiceOrderLine(SourceRecordRef, DestinationRecordRef); + end; + end; + end; + + local procedure ProofAllServiceItemLinesAssigned(ServiceHeader: Record "Service Header") + var + ServiceLine: Record "Service Line"; + begin + ServiceLine.SetRange("Document Type", ServiceLine."Document Type"::Order); + ServiceLine.SetRange("Document No.", ServiceHeader."No."); + ServiceLine.SetRange("Service Item Line No.", 0); + if ServiceLine.FindFirst() then + ServiceLine.TestField("Service Item Line No."); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Integration Rec. Synch. Invoke", 'OnAfterUnchangedRecordHandled', '', false, false)] + local procedure HandleOnAfterUnchangedRecordHandled(SourceRecordRef: RecordRef; DestinationRecordRef: RecordRef) + var + FSConnectionSetup: Record "FS Connection Setup"; + ArchivedServiceOrders: List of [Code[20]]; + SourceDestCode: Text; + begin + if not FSConnectionSetup.IsEnabled() then + exit; + + SourceDestCode := GetSourceDestCode(SourceRecordRef, DestinationRecordRef); + + case SourceDestCode of + 'FS Work Order-Service Header': + begin + ResetServiceOrderItemLineFromFSWorkOrderIncident(SourceRecordRef, DestinationRecordRef, ArchivedServiceOrders); + ResetServiceOrderLineFromFSWorkOrderProduct(SourceRecordRef, DestinationRecordRef, ArchivedServiceOrders); + ResetServiceOrderLineFromFSWorkOrderService(SourceRecordRef, DestinationRecordRef, ArchivedServiceOrders); + ResetServiceOrderLineFromFSBookableResourceBooking(SourceRecordRef, DestinationRecordRef, ArchivedServiceOrders); + end; + 'Service Header-FS Work Order': + begin + ResetFSWorkOrderIncidentFromServiceOrderItemLine(SourceRecordRef, DestinationRecordRef); + ResetFSWorkOrderProductFromServiceOrderLine(SourceRecordRef, DestinationRecordRef); + ResetFSWorkOrderServiceFromServiceOrderLine(SourceRecordRef, DestinationRecordRef); + end; + end; + end; + + local procedure ValidateServiceHeaderAfterInsert(DestinationRecordRef: RecordRef) + var + ServiceHeader: Record "Service Header"; + begin + DestinationRecordRef.SetTable(ServiceHeader); + ServiceHeader.Validate("Customer No."); // explicit recalculation, as InitRecord() was called after setting the customer + ServiceHeader.Modify(true); + DestinationRecordRef.GetTable(ServiceHeader); + end; + + local procedure ResetServiceOrderItemLineFromFSWorkOrderIncident(SourceRecordRef: RecordRef; DestinationRecordRef: RecordRef; var ArchivedServiceOrders: List of [Code[20]]) + var + ServiceHeader: Record "Service Header"; + ServiceItemLine: Record "Service Item Line"; + ServiceItemLineToDelete: Record "Service Item Line"; + CRMIntegrationRecord: Record "CRM Integration Record"; + FSWorkOrder: Record "FS Work Order"; + FSWorkOrderIncident: Record "FS Work Order Incident"; + FSWorkOrderIncident2: Record "FS Work Order Incident"; + CRMIntegrationTableSynch: Codeunit "CRM Integration Table Synch."; + CRMSalesorderdetailRecordRef: RecordRef; + CRMSalesorderdetailId: Guid; + FSWorkOrderIncidentIdList: List of [Guid]; + FSWorkOrderIncidentIdFilter: Text; + begin + SourceRecordRef.SetTable(FSWorkOrder); + DestinationRecordRef.SetTable(ServiceHeader); + + ServiceItemLine.SetRange("Document Type", ServiceItemLine."Document Type"::Order); + ServiceItemLine.SetRange("Document No.", ServiceHeader."No."); + if ServiceItemLine.FindSet() then + repeat + CRMIntegrationRecord.SetRange("Integration ID", ServiceItemLine.SystemId); + CRMIntegrationRecord.SetRange("Table ID", Database::"Service Item Line"); + if CRMIntegrationRecord.FindFirst() then begin + FSWorkOrderIncident.SetRange(WorkOrderIncidentId, CRMIntegrationRecord."CRM ID"); + if FSWorkOrderIncident.IsEmpty() then begin + CRMIntegrationRecord.Delete(); + ArchiveServiceOrder(ServiceHeader, ArchivedServiceOrders); + if ServiceItemLineToDelete.GetBySystemId(ServiceItemLine.SystemId) then begin + DeleteServiceLines(ServiceItemLine); + ServiceItemLineToDelete.Delete(true); + end; + end; + end; + until ServiceItemLine.Next() = 0; + + FSWorkOrderIncident.Reset(); + FSWorkOrderIncident.SetRange(WorkOrder, FSWorkOrder.WorkOrderId); + if FSWorkOrderIncident.FindSet() then begin + repeat + if not SkipReimport(FSWorkOrderIncident.WorkOrderIncidentId, FSWorkOrderIncident.ModifiedOn) then + FSWorkOrderIncidentIdList.Add(FSWorkOrderIncident.WorkOrderIncidentId) + until FSWorkOrderIncident.Next() = 0; + + foreach CRMSalesorderdetailId in FSWorkOrderIncidentIdList do + FSWorkOrderIncidentIdFilter += CRMSalesorderdetailId + '|'; + FSWorkOrderIncidentIdFilter := FSWorkOrderIncidentIdFilter.TrimEnd('|'); + + FSWorkOrderIncident2.SetFilter(WorkOrderIncidentId, FSWorkOrderIncidentIdFilter); + CRMSalesorderdetailRecordRef.GetTable(FSWorkOrderIncident2); + CRMIntegrationTableSynch.SynchRecordsFromIntegrationTable(CRMSalesorderdetailRecordRef, Database::"Service Item Line", false, false); + end; + end; + + local procedure DeleteServiceLines(ServiceItemLine: Record "Service Item Line") + var + ServiceLine: Record "Service Line"; + begin + ServiceLine.SetRange("Document Type", ServiceItemLine."Document Type"); + ServiceLine.SetRange("Document No.", ServiceItemLine."Document No."); + ServiceLine.SetRange("Service Item Line No.", ServiceItemLine."Line No."); + if not ServiceLine.IsEmpty() then + ServiceLine.DeleteAll(true); + end; + + local procedure ResetServiceOrderLineFromFSWorkOrderProduct(SourceRecordRef: RecordRef; DestinationRecordRef: RecordRef; var ArchivedServiceOrders: List of [Code[20]]) + var + ServiceHeader: Record "Service Header"; + ServiceLine: Record "Service Line"; + ServiceLineToDelete: Record "Service Line"; + CRMIntegrationRecord: Record "CRM Integration Record"; + FSWorkOrder: Record "FS Work Order"; + FSWorkOrderProduct: Record "FS Work Order Product"; + FSWorkOrderProduct2: Record "FS Work Order Product"; + CRMIntegrationTableSynch: Codeunit "CRM Integration Table Synch."; + FSWorkOrderProductRecordRef: RecordRef; + FSWorkOrderProductId: Guid; + FSWorkOrderProductIdList: List of [Guid]; + FSWorkOrderProductIdFilter: Text; + begin + SourceRecordRef.SetTable(FSWorkOrder); + DestinationRecordRef.SetTable(ServiceHeader); + + ServiceLine.SetRange("Document Type", ServiceLine."Document Type"::Order); + ServiceLine.SetRange("Document No.", ServiceHeader."No."); + ServiceLine.SetRange(Type, ServiceLine.Type::Item); + ServiceLine.SetRange("Item Type", ServiceLine."Item Type"::Inventory); + if ServiceLine.FindSet() then + repeat + CRMIntegrationRecord.SetRange("Integration ID", ServiceLine.SystemId); + CRMIntegrationRecord.SetRange("Table ID", Database::"Service Line"); + if CRMIntegrationRecord.FindFirst() then begin + FSWorkOrderProduct.SetRange(WorkOrderProductId, CRMIntegrationRecord."CRM ID"); + if FSWorkOrderProduct.IsEmpty() then begin + CRMIntegrationRecord.Delete(); + ArchiveServiceOrder(ServiceHeader, ArchivedServiceOrders); + if ServiceLineToDelete.GetBySystemId(ServiceLine.SystemId) then + ServiceLineToDelete.Delete(true); + end; + end; + until ServiceLine.Next() = 0; + + FSWorkOrderProduct.Reset(); + FSWorkOrderProduct.SetRange(WorkOrder, FSWorkOrder.WorkOrderId); + if FSWorkOrderProduct.FindSet() then begin + repeat + if not SkipReimport(FSWorkOrderProduct.WorkOrderProductId, FSWorkOrderProduct.ModifiedOn) then + FSWorkOrderProductIdList.Add(FSWorkOrderProduct.WorkOrderProductId); + until FSWorkOrderProduct.Next() = 0; + + foreach FSWorkOrderProductId in FSWorkOrderProductIdList do + FSWorkOrderProductIdFilter += FSWorkOrderProductId + '|'; + FSWorkOrderProductIdFilter := FSWorkOrderProductIdFilter.TrimEnd('|'); + + FSWorkOrderProduct2.SetFilter(WorkOrderProductId, FSWorkOrderProductIdFilter); + FSWorkOrderProductRecordRef.GetTable(FSWorkOrderProduct2); + CRMIntegrationTableSynch.SynchRecordsFromIntegrationTable(FSWorkOrderProductRecordRef, Database::"Service Line", false, false); + end; + end; + + local procedure ResetServiceOrderLineFromFSWorkOrderService(SourceRecordRef: RecordRef; DestinationRecordRef: RecordRef; var ArchivedServiceOrders: List of [Code[20]]) + var + ServiceHeader: Record "Service Header"; + ServiceLine: Record "Service Line"; + ServiceLineToDelete: Record "Service Line"; + CRMIntegrationRecord: Record "CRM Integration Record"; + FSWorkOrder: Record "FS Work Order"; + FSWorkOrderService: Record "FS Work Order Service"; + FSWorkOrderService2: Record "FS Work Order Service"; + CRMIntegrationTableSynch: Codeunit "CRM Integration Table Synch."; + FSWorkOrderServiceRecordRef: RecordRef; + FSWorkOrderServiceId: Guid; + FSWorkOrderServiceIdList: List of [Guid]; + FSWorkOrderServiceIdFilter: Text; + begin + SourceRecordRef.SetTable(FSWorkOrder); + DestinationRecordRef.SetTable(ServiceHeader); + + ServiceLine.SetRange("Document Type", ServiceLine."Document Type"::Order); + ServiceLine.SetRange("Document No.", ServiceHeader."No."); + ServiceLine.SetRange(Type, ServiceLine.Type::Item); + ServiceLine.SetFilter("Item Type", '%1|%2', ServiceLine."Item Type"::Service, ServiceLine."Item Type"::"Non-Inventory"); + if ServiceLine.FindSet() then + repeat + CRMIntegrationRecord.SetRange("Integration ID", ServiceLine.SystemId); + CRMIntegrationRecord.SetRange("Table ID", Database::"Service Line"); + if CRMIntegrationRecord.FindFirst() then begin + FSWorkOrderService.SetRange(WorkOrderServiceId, CRMIntegrationRecord."CRM ID"); + if FSWorkOrderService.IsEmpty() then begin + CRMIntegrationRecord.Delete(); + ArchiveServiceOrder(ServiceHeader, ArchivedServiceOrders); + if ServiceLineToDelete.GetBySystemId(ServiceLine.SystemId) then + ServiceLineToDelete.Delete(true); + end; + end; + until ServiceLine.Next() = 0; + + FSWorkOrderService.Reset(); + FSWorkOrderService.SetRange(WorkOrder, FSWorkOrder.WorkOrderId); + if FSWorkOrderService.FindSet() then begin + repeat + if not SkipReimport(FSWorkOrderService.WorkOrderServiceId, FSWorkOrderService.ModifiedOn) then + FSWorkOrderServiceIdList.Add(FSWorkOrderService.WorkOrderServiceId); + until FSWorkOrderService.Next() = 0; + + foreach FSWorkOrderServiceId in FSWorkOrderServiceIdList do + FSWorkOrderServiceIdFilter += FSWorkOrderServiceId + '|'; + FSWorkOrderServiceIdFilter := FSWorkOrderServiceIdFilter.TrimEnd('|'); + + FSWorkOrderService2.SetFilter(WorkOrderServiceId, FSWorkOrderServiceIdFilter); + FSWorkOrderServiceRecordRef.GetTable(FSWorkOrderService2); + CRMIntegrationTableSynch.SynchRecordsFromIntegrationTable(FSWorkOrderServiceRecordRef, Database::"Service Line", false, false); + end; + end; + + local procedure ResetServiceOrderLineFromFSBookableResourceBooking(SourceRecordRef: RecordRef; DestinationRecordRef: RecordRef; var ArchivedServiceOrders: List of [Code[20]]) + var + ServiceHeader: Record "Service Header"; + ServiceLine: Record "Service Line"; + ServiceLineToDelete: Record "Service Line"; + CRMIntegrationRecord: Record "CRM Integration Record"; + FSWorkOrder: Record "FS Work Order"; + FSBookableResourceBooking: Record "FS Bookable Resource Booking"; + FSBookableResourceBooking2: Record "FS Bookable Resource Booking"; + CRMIntegrationTableSynch: Codeunit "CRM Integration Table Synch."; + FSIntegrationMgt: Codeunit "FS Integration Mgt."; + FSBookableResourceBookingRecordRef: RecordRef; + FSBookableResourceBookingId: Guid; + FSBookableResourceBookingIdList: List of [Guid]; + FSBookableResourceBookingIdFilter: Text; + begin + SourceRecordRef.SetTable(FSWorkOrder); + DestinationRecordRef.SetTable(ServiceHeader); + + ServiceLine.SetRange("Document Type", ServiceLine."Document Type"::Order); + ServiceLine.SetRange("Document No.", ServiceHeader."No."); + ServiceLine.SetRange(Type, ServiceLine.Type::Resource); + if ServiceLine.FindSet() then + repeat + CRMIntegrationRecord.SetRange("Integration ID", ServiceLine.SystemId); + CRMIntegrationRecord.SetRange("Table ID", Database::"Service Line"); + if CRMIntegrationRecord.FindFirst() then begin + FSBookableResourceBooking.SetRange(BookableResourceBookingId, CRMIntegrationRecord."CRM ID"); + if FSBookableResourceBooking.IsEmpty() then begin + CRMIntegrationRecord.Delete(); + ArchiveServiceOrder(ServiceHeader, ArchivedServiceOrders); + if ServiceLineToDelete.GetBySystemId(ServiceLine.SystemId) then begin + DeleteServiceItemLineForBooking(ServiceLine); + ServiceLineToDelete.Delete(true); + end; + end; + end; + until ServiceLine.Next() = 0; + + FSBookableResourceBooking.Reset(); + FSBookableResourceBooking.SetRange(WorkOrder, FSWorkOrder.WorkOrderId); + FSBookableResourceBooking.SetRange(BookingStatus, FSIntegrationMgt.GetBookingStatusCompleted()); + if FSBookableResourceBooking.FindSet() then begin + repeat + if not SkipReimport(FSBookableResourceBooking.BookableResourceBookingId, FSBookableResourceBooking.ModifiedOn) then + FSBookableResourceBookingIdList.Add(FSBookableResourceBooking.BookableResourceBookingId); + until FSBookableResourceBooking.Next() = 0; + + foreach FSBookableResourceBookingId in FSBookableResourceBookingIdList do + FSBookableResourceBookingIdFilter += FSBookableResourceBookingId + '|'; + FSBookableResourceBookingIdFilter := FSBookableResourceBookingIdFilter.TrimEnd('|'); + + FSBookableResourceBooking2.SetFilter(BookableResourceBookingId, FSBookableResourceBookingIdFilter); + FSBookableResourceBookingRecordRef.GetTable(FSBookableResourceBooking2); + CRMIntegrationTableSynch.SynchRecordsFromIntegrationTable(FSBookableResourceBookingRecordRef, Database::"Service Line", false, false); + end; + end; + + local procedure SkipReimport(CRMId: Guid; CurrentModifyTimeStamp: DateTime): Boolean + var + CRMIntegrationRecord: Record "CRM Integration Record"; + begin + // check if skipped (because of deletion) + CRMIntegrationRecord.SetRange("CRM ID", CRMId); + if not CRMIntegrationRecord.FindFirst() then + exit(false); + if not CRMIntegrationRecord."Skip Reimport" then + exit(false); + if (CRMIntegrationRecord."Skip Reimport") and (CRMIntegrationRecord."Last Synch. Modified On" > CurrentModifyTimeStamp) then + exit(true); + + CRMIntegrationRecord.Delete(); // there was an update in field service -> start import again with new coupling + exit(false); + end; + + local procedure ResetFSWorkOrderIncidentFromServiceOrderItemLine(SourceRecordRef: RecordRef; DestinationRecordRef: RecordRef) + var + ServiceHeader: Record "Service Header"; + FSWorkOrder: Record "FS Work Order"; + ServiceItemLine: Record "Service Item Line"; + CRMIntegrationTableSynch: Codeunit "CRM Integration Table Synch."; + ServiceItemLineRecordRef: RecordRef; + begin + SourceRecordRef.SetTable(ServiceHeader); + DestinationRecordRef.SetTable(FSWorkOrder); + + ServiceItemLine.SetRange("Document Type", ServiceHeader."Document Type"); + ServiceItemLine.SetRange("Document No.", ServiceHeader."No."); + ServiceItemLine.SetRange("FS Bookings", false); + if not ServiceItemLine.IsEmpty() then begin + ServiceItemLineRecordRef.GetTable(ServiceItemLine); + CRMIntegrationTableSynch.SynchRecordsToIntegrationTable(ServiceItemLineRecordRef, false, false); + end; + end; + + local procedure ResetFSWorkOrderProductFromServiceOrderLine(SourceRecordRef: RecordRef; DestinationRecordRef: RecordRef) + var + ServiceHeader: Record "Service Header"; + FSWorkOrder: Record "FS Work Order"; + ServiceLine: Record "Service Line"; + ServiceLineRecordRef: RecordRef; + begin + SourceRecordRef.SetTable(ServiceHeader); + DestinationRecordRef.SetTable(FSWorkOrder); + + ServiceLine.Reset(); + ServiceLine.SetRange("Document Type", ServiceHeader."Document Type"); + ServiceLine.SetRange("Document No.", ServiceHeader."No."); + ServiceLine.SetRange("Type", ServiceLine.Type::Item); + ServiceLine.SetFilter("Item Type", '%1|%2', ServiceLine."Item Type"::Inventory, ServiceLine."Item Type"::"Non-Inventory"); + if not ServiceLine.IsEmpty() then begin + ServiceLineRecordRef.GetTable(ServiceLine); + SynchRecordsToIntegrationTable(ServiceLineRecordRef, Database::"FS Work Order Product", false, false); + end; + end; + + local procedure ResetFSWorkOrderServiceFromServiceOrderLine(SourceRecordRef: RecordRef; DestinationRecordRef: RecordRef) + var + ServiceHeader: Record "Service Header"; + FSWorkOrder: Record "FS Work Order"; + ServiceLine: Record "Service Line"; + ServiceLineRecordRef: RecordRef; + begin + SourceRecordRef.SetTable(ServiceHeader); + DestinationRecordRef.SetTable(FSWorkOrder); + + ServiceLine.Reset(); + ServiceLine.SetRange("Document Type", ServiceHeader."Document Type"); + ServiceLine.SetRange("Document No.", ServiceHeader."No."); + ServiceLine.SetRange("Type", ServiceLine.Type::Item); + ServiceLine.SetRange("Item Type", ServiceLine."Item Type"::Service); + if not ServiceLine.IsEmpty() then begin + ServiceLineRecordRef.GetTable(ServiceLine); + SynchRecordsToIntegrationTable(ServiceLineRecordRef, Database::"FS Work Order Service", false, false); + end; + end; + + procedure SynchRecordsToIntegrationTable(var RecordsToSynchRecordRef: RecordRef; TargetTable: Integer; IgnoreChanges: Boolean; IgnoreSynchOnlyCoupledRecords: Boolean) JobID: Guid + var + IntegrationTableMapping: Record "Integration Table Mapping"; + IntegrationTableSynch: Codeunit "Integration Table Synch."; + IntegrationRecordRef: RecordRef; + SynchronizeEmptySetErr: Label 'Attempted to synchronize an empty set of records.'; + begin + IntegrationTableMapping.SetRange("Table ID", RecordsToSynchRecordRef.Number); + IntegrationTableMapping.SetRange("Integration Table ID", TargetTable); + IntegrationTableMapping.SetRange("Delete After Synchronization", false); + if not IntegrationTableMapping.FindFirst() then + Error(SynchronizeEmptySetErr); + + RecordsToSynchRecordRef.Ascending(false); + if not RecordsToSynchRecordRef.FindSet() then + Error(SynchronizeEmptySetErr); + + JobID := + IntegrationTableSynch.BeginIntegrationSynchJob( + TableConnectionType::CRM, IntegrationTableMapping, RecordsToSynchRecordRef.Number); + if not IsNullGuid(JobID) then begin + repeat + IntegrationTableSynch.Synchronize(RecordsToSynchRecordRef, IntegrationRecordRef, IgnoreChanges, IgnoreSynchOnlyCoupledRecords) + until RecordsToSynchRecordRef.Next() = 0; + IntegrationTableSynch.EndIntegrationSynchJob(); + end; + end; + + internal procedure ArchiveServiceOrder(ServiceHeader: Record "Service Header"; ArchivedServiceOrders: List of [Code[20]]) + var + ServiceMgtSetup: Record "Service Mgt. Setup"; + ServiceDocumentArchiveMgmt: Codeunit "Service Document Archive Mgmt."; + begin + if ArchivedServiceOrders.Contains(ServiceHeader."No.") then + exit; + + ServiceMgtSetup.Get(); + if not ServiceMgtSetup."Archive Orders" then + exit; + + ArchivedServiceOrders.Add(ServiceHeader."No."); + ServiceDocumentArchiveMgmt.ArchServiceDocumentNoConfirm(ServiceHeader); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Integration Rec. Synch. Invoke", 'OnBeforeModifyRecord', '', false, false)] + local procedure HandleOnBeforeModifyRecord(IntegrationTableMapping: Record "Integration Table Mapping"; SourceRecordRef: RecordRef; var DestinationRecordRef: RecordRef) + var + FSConnectionSetup: Record "FS Connection Setup"; + ServiceHeader: Record "Service Header"; + SourceDestCode: Text; + begin + if not FSConnectionSetup.IsEnabled() then + exit; + + SourceDestCode := GetSourceDestCode(SourceRecordRef, DestinationRecordRef); + + case SourceDestCode of + 'FS Work Order Service-Job Journal Line': + UpdateCorrelatedJobJournalLine(SourceRecordRef, DestinationRecordRef); + 'Service Header-FS Work Order': + begin + SourceRecordRef.SetTable(ServiceHeader); + ProofAllServiceItemLinesAssigned(ServiceHeader); + SetCompanyId(DestinationRecordRef); end; end; end; - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Integration Rec. Synch. Invoke", 'OnBeforeModifyRecord', '', false, false)] - local procedure HandleOnBeforeModifyRecord(IntegrationTableMapping: Record "Integration Table Mapping"; SourceRecordRef: RecordRef; var DestinationRecordRef: RecordRef) + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Integration Rec. Synch. Invoke", 'OnUpdateIntegrationRecordCoupling', '', false, false)] + local procedure HandleOnUpdateIntegrationRecordCoupling(IntegrationTableMapping: Record "Integration Table Mapping"; SourceRecordRef: RecordRef; var DestinationRecordRef: RecordRef) var FSConnectionSetup: Record "FS Connection Setup"; + ServiceHeader: Record "Service Header"; SourceDestCode: Text; begin if not FSConnectionSetup.IsEnabled() then @@ -396,11 +1397,16 @@ codeunit 6610 "FS Int. Table Subscriber" SourceDestCode := GetSourceDestCode(SourceRecordRef, DestinationRecordRef); case SourceDestCode of - 'FS Work Order Service-Job Journal Line': - UpdateCorrelatedJobJournalLine(SourceRecordRef, DestinationRecordRef); + 'Service Header-FS Work Order': + begin + SourceRecordRef.SetTable(ServiceHeader); + ProofAllServiceItemLinesAssigned(ServiceHeader); + SetCompanyId(DestinationRecordRef); + end; end; end; + [EventSubscriber(ObjectType::Table, Database::"Inventory Setup", 'OnAfterValidateEvent', 'Location Mandatory', false, false)] local procedure AfterValidateLocationMandatory(var Rec: Record "Inventory Setup"; var xRec: Record "Inventory Setup"; CurrFieldNo: Integer) var @@ -514,6 +1520,7 @@ codeunit 6610 "FS Int. Table Subscriber" JobJournalLine: Record "Job Journal Line"; FSWorkOrderProduct: Record "FS Work Order Product"; FSWorkOrderService: Record "FS Work Order Service"; + ArchivedServiceOrders: List of [Code[20]]; SourceDestCode: Text; begin if not FSConnectionSetup.IsEnabled() then @@ -534,6 +1541,19 @@ codeunit 6610 "FS Int. Table Subscriber" DestinationRecordRef.SetTable(JobJournalLine); ConditionallyPostJobJournalLine(FSConnectionSetup, FSWorkOrderService, JobJournalLine); end; + 'FS Work Order-Service Header': + begin + ResetServiceOrderItemLineFromFSWorkOrderIncident(SourceRecordRef, DestinationRecordRef, ArchivedServiceOrders); + ResetServiceOrderLineFromFSWorkOrderProduct(SourceRecordRef, DestinationRecordRef, ArchivedServiceOrders); + ResetServiceOrderLineFromFSWorkOrderService(SourceRecordRef, DestinationRecordRef, ArchivedServiceOrders); + ResetServiceOrderLineFromFSBookableResourceBooking(SourceRecordRef, DestinationRecordRef, ArchivedServiceOrders); + end; + 'Service Header-FS Work Order': + begin + ResetFSWorkOrderIncidentFromServiceOrderItemLine(SourceRecordRef, DestinationRecordRef); + ResetFSWorkOrderProductFromServiceOrderLine(SourceRecordRef, DestinationRecordRef); + ResetFSWorkOrderServiceFromServiceOrderLine(SourceRecordRef, DestinationRecordRef); + end; end; end; @@ -611,11 +1631,20 @@ codeunit 6610 "FS Int. Table Subscriber" Resource: Record Resource; FSBookableResource: Record "FS Bookable Resource"; LastJobJournalLine: Record "Job Journal Line"; + FSWorkOrderIncident: Record "FS Work Order Incident"; + FSWorkOrderProduct: Record "FS Work Order Product"; + FSWorkOrderService: Record "FS Work Order Service"; + ServiceHeader: Record "Service Header"; + ServiceItemLine: Record "Service Item Line"; + ServiceLine: Record "Service Line"; CRMProductName: Codeunit "CRM Product Name"; + NoSeries: Codeunit "No. Series"; + FSIntegrationMgt: Codeunit "FS Integration Mgt."; RecID: RecordId; SourceDestCode: Text; BillingAccId: Guid; ServiceAccId: Guid; + WorkOrderId: Guid; Handled: Boolean; begin if not FSConnectionSetup.IsEnabled() then @@ -624,6 +1653,8 @@ codeunit 6610 "FS Int. Table Subscriber" SourceDestCode := GetSourceDestCode(SourceRecordRef, DestinationRecordRef); case SourceDestCode of + 'Location-FS Warehouse': + SetCompanyId(DestinationRecordRef); 'Service Item-FS Customer Asset': SetCompanyId(DestinationRecordRef); 'FS Customer Asset-Service Item': @@ -713,6 +1744,190 @@ codeunit 6610 "FS Int. Table Subscriber" end; DestinationRecordRef.GetTable(JobJournalLine); end; + 'FS Bookable Resource Booking-Service Line': + begin + DestinationRecordRef.SetTable(ServiceLine); + GenerateServiceItemLineForBooking(ServiceLine); + DestinationRecordRef.GetTable(ServiceLine); + end; + 'Service Order Type-FS Work Order Type': + SetCompanyId(DestinationRecordRef); + 'Service Header-FS Work Order': + begin + SourceRecordRef.SetTable(ServiceHeader); + ProofAllServiceItemLinesAssigned(ServiceHeader); + SetCompanyId(DestinationRecordRef); + end; + 'Service Item Line-FS Work Order Incident': + begin + SetCompanyId(DestinationRecordRef); + + SourceRecordRef.SetTable(ServiceItemLine); + DestinationRecordRef.SetTable(FSWorkOrderIncident); + if CRMIntegrationRecord.FindIDFromRecordID(GetServiceOrderRecordId(ServiceItemLine."Document No."), WorkOrderId) then + FSWorkOrderIncident.WorkOrder := WorkOrderId; + FSWorkOrderIncident.IncidentType := FSIntegrationMgt.GetDefaultWorkOrderIncident(); + DestinationRecordRef.GetTable(FSWorkOrderIncident); + end; + 'Service Line-FS Work Order Product': + begin + SetCompanyId(DestinationRecordRef); + + SourceRecordRef.SetTable(ServiceLine); + DestinationRecordRef.SetTable(FSWorkOrderProduct); + if CRMIntegrationRecord.FindIDFromRecordID(GetServiceOrderRecordId(ServiceLine."Document No."), WorkOrderId) then + FSWorkOrderProduct.WorkOrder := WorkOrderId; + DestinationRecordRef.GetTable(FSWorkOrderProduct); + end; + 'Service Line-FS Work Order Service': + begin + SetCompanyId(DestinationRecordRef); + + SourceRecordRef.SetTable(ServiceLine); + DestinationRecordRef.SetTable(FSWorkOrderService); + if CRMIntegrationRecord.FindIDFromRecordID(GetServiceOrderRecordId(ServiceLine."Document No."), WorkOrderId) then + FSWorkOrderService.WorkOrder := WorkOrderId; + DestinationRecordRef.GetTable(FSWorkOrderService); + end; + end; + end; + + local procedure GenerateServiceItemLineForBooking(var ServiceLine: Record "Service Line") + var + ServiceItemLine: Record "Service Item Line"; + Resource: Record Resource; + begin + if not GetServiceItemLine(ServiceLine, ServiceItemLine) then begin + ServiceItemLine.Validate("Document Type", ServiceLine."Document Type"); + ServiceItemLine.Validate("Document No.", ServiceLine."Document No."); + ServiceItemLine.Validate("Line No.", GetNextLineNo(ServiceItemLine)); + ServiceItemLine.Validate(Description, Resource.TableCaption()); + ServiceItemLine.Validate("FS Bookings", true); + ServiceItemLine.Insert(true); + end; + + ServiceLine.Validate("Service Item Line No.", ServiceItemLine."Line No."); + end; + + local procedure DeleteServiceItemLineForBooking(ServiceLineToDelete: Record "Service Line") + var + ServiceItemLine: Record "Service Item Line"; + ServiceLine: Record "Service Line"; + begin + // other service line exists? + ServiceLine.SetRange("Document Type", ServiceLineToDelete."Document Type"); + ServiceLine.SetRange("Document No.", ServiceLineToDelete."Document No."); + ServiceLine.SetRange("Service Item Line No.", ServiceLineToDelete."Service Item Line No."); + ServiceLine.SetFilter("Line No.", '<>%1', ServiceLineToDelete."Line No."); + if not ServiceLine.IsEmpty() then + exit; + + // delete service item line -> only if no other service line exists + ServiceItemLine.SetRange("Document Type", ServiceLineToDelete."Document Type"); + ServiceItemLine.SetRange("Document No.", ServiceLineToDelete."Document No."); + ServiceItemLine.SetRange("Line No.", ServiceLineToDelete."Service Item Line No."); + if not ServiceItemLine.IsEmpty() then + ServiceItemLine.DeleteAll() + end; + + local procedure GetServiceItemLine(ServiceLine: Record "Service Line"; var ServiceItemLine: Record "Service Item Line"): Boolean + begin + ServiceItemLine.SetRange("Document Type", ServiceLine."Document Type"); + ServiceItemLine.SetRange("Document No.", ServiceLine."Document No."); + ServiceItemLine.SetRange("FS Bookings", true); + exit(ServiceItemLine.FindFirst()); + end; + + local procedure GetNextLineNo(ServiceItemLine: Record "Service Item Line"): Integer + var + ServiceItemLineSearch: Record "Service Item Line"; + begin + ServiceItemLineSearch.SetRange("Document Type", ServiceItemLine."Document Type"); + ServiceItemLineSearch.SetRange("Document No.", ServiceItemLine."Document No."); + if ServiceItemLineSearch.FindLast() then + exit(ServiceItemLineSearch."Line No." + 10000); + exit(10000); + end; + + local procedure GetNextLineNo(ServiceLine: Record "Service Line"): Integer + var + ServiceLineSearch: Record "Service Line"; + begin + ServiceLineSearch.SetRange("Document Type", ServiceLine."Document Type"); + ServiceLineSearch.SetRange("Document No.", ServiceLine."Document No."); + if ServiceLineSearch.FindLast() then + exit(ServiceLineSearch."Line No." + 10000); + exit(10000); + end; + + local procedure GetServiceOrderRecordId(DocumentNo: Code[20]): RecordId + var + ServiceHeader: Record "Service Header"; + begin + if ServiceHeader.Get(ServiceHeader."Document Type"::Order, DocumentNo) then + exit(ServiceHeader.RecordId); + end; + + local procedure GetServiceOrderItemLineRecordId(DocumentNo: Code[20]; LineNo: Integer): RecordId + var + ServiceItemLine: Record "Service Item Line"; + begin + if ServiceItemLine.Get(ServiceItemLine."Document Type"::Order, DocumentNo, LineNo) then + exit(ServiceItemLine.RecordId); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"CRM Integration Table Synch.", 'OnSynchNAVTableToCRMOnBeforeCheckLatestModifiedOn', '', true, false)] + local procedure OnSynchNAVTableToCRMOnBeforeCheckLatestModifiedOn(var SourceRecordRef: RecordRef; IntegrationTableMapping: Record "Integration Table Mapping") + var + ServiceHeader: Record "Service Header"; + ServiceHeaderToSync: Record "Service Header"; + ServiceItemLine: Record "Service Item Line"; + ServiceLine: Record "Service Line"; + CRMIntegrationTableSynch: Codeunit "CRM Integration Table Synch."; + ServiceOrderRecordRef: RecordRef; + ServiceOrdersToIgnore: List of [Code[20]]; + ServiceOrdersToSync: List of [Code[20]]; + ServiceOrderNo: Code[20]; + begin + if SourceRecordRef.Number() <> Database::"Service Header" then + exit; + + // sync of service header should triggered by changes in service item lines and service lines: + // search for modified service orders and start sync -> only if not already synced (via service header). + + // already synced service orders should not be triggered again + SourceRecordRef.SetTable(ServiceHeader); + if ServiceHeader.FindSet() then + repeat + ServiceOrdersToIgnore.Add(ServiceHeader."No."); + until ServiceHeader.Next() = 0; + + // search for modified service (item) lines and start sync + ServiceItemLine.SetRange("Document Type", ServiceItemLine."Document Type"::Order); + ServiceItemLine.SetFilter(SystemModifiedAt, ServiceHeader.GetFilter(SystemModifiedAt)); + if ServiceItemLine.FindSet() then + repeat + if not ServiceOrdersToIgnore.Contains(ServiceItemLine."Document No.") then + if not ServiceOrdersToSync.Contains(ServiceItemLine."Document No.") then + ServiceOrdersToSync.Add(ServiceItemLine."Document No."); + until ServiceItemLine.Next() = 0; + + ServiceLine.SetRange("Document Type", ServiceItemLine."Document Type"::Order); + ServiceLine.SetFilter(SystemModifiedAt, ServiceHeader.GetFilter(SystemModifiedAt)); + if ServiceLine.FindSet() then + repeat + if not ServiceOrdersToIgnore.Contains(ServiceLine."Document No.") then + if not ServiceOrdersToSync.Contains(ServiceLine."Document No.") then + ServiceOrdersToSync.Add(ServiceLine."Document No."); + until ServiceLine.Next() = 0; + + // start sync for found service orders + foreach ServiceOrderNo in ServiceOrdersToSync do begin + ServiceHeaderToSync.Reset(); + ServiceHeaderToSync.SetRange("Document Type", ServiceHeaderToSync."Document Type"::Order); + ServiceHeaderToSync.SetRange("No.", ServiceOrderNo); + ServiceOrderRecordRef.GetTable(ServiceHeaderToSync); + CRMIntegrationTableSynch.SynchRecordsToIntegrationTable(ServiceOrderRecordRef, false, false); end; end; @@ -874,6 +2089,7 @@ codeunit 6610 "FS Int. Table Subscriber" local procedure SetCurrentProjectPlanningQuantities(var SourceRecordRef: RecordRef; var QuantityCurrentlyConsumed: Decimal; var QuantityCurrentlyInvoiced: Decimal) var + FSConnectionSetup: Record "FS Connection Setup"; FSWorkOrderProduct: Record "FS Work Order Product"; FSWorkOrderService: Record "FS Work Order Service"; FSBookableResourceBooking: Record "FS Bookable Resource Booking"; @@ -885,6 +2101,10 @@ codeunit 6610 "FS Int. Table Subscriber" begin QuantityCurrentlyConsumed := 0; QuantityCurrentlyInvoiced := 0; + + if not FSConnectionSetup.IsIntegrationTypeProjectEnabled() then + exit; + case SourceRecordRef.Number() of Database::"FS Work Order Product": begin @@ -1200,6 +2420,16 @@ codeunit 6610 "FS Int. Table Subscriber" if IgnoreRecord then exit; + case SourceRecordRef.Number() of + Database::"FS Work Order Product", + Database::"FS Work Order Service": + IgnorePostedJobJournalLinesOnQueryPostFilterIgnoreRecord(SourceRecordRef, IgnoreRecord); + Database::"Service Header": + IgnoreArchievedServiceOrdersOnQueryPostFilterIgnoreRecord(SourceRecordRef, IgnoreRecord); + Database::"FS Work Order": + IgnoreArchievedCRMWorkOrdersOnQueryPostFilterIgnoreRecord(SourceRecordRef, IgnoreRecord); + end; + if FSConnectionSetup.IsEnabled() then exit; @@ -1232,6 +2462,103 @@ codeunit 6610 "FS Int. Table Subscriber" end; end; + internal procedure IgnorePostedJobJournalLinesOnQueryPostFilterIgnoreRecord(SourceRecordRef: RecordRef; var IgnoreRecord: Boolean) + var + FSConnectionSetup: Record "FS Connection Setup"; + FSWorkOrder: Record "FS Work Order"; + FSWorkOrderProduct: Record "FS Work Order Product"; + FSWorkOrderService: Record "FS Work Order Service"; + QuantityCurrentlyConsumed: Decimal; + QuantityCurrentlyInvoiced: Decimal; + FSQuantityToConsume: Decimal; + FSQuantityToInvoice: Decimal; + IntegrateToService: Boolean; + begin + if not FSConnectionSetup.IsIntegrationTypeProjectEnabled() then + exit; + if IgnoreRecord then + exit; + if FSConnectionSetup."Line Synch. Rule" <> "FS Work Order Line Synch. Rule"::LineUsed then + exit; + + case SourceRecordRef.Number() of + Database::"FS Work Order Product": + begin + SourceRecordRef.SetTable(FSWorkOrderProduct); + if FSWorkOrderProduct.LineStatus = FSWorkOrderProduct.LineStatus::Used then begin + FSQuantityToConsume := FSWorkOrderProduct.Quantity; + FSQuantityToInvoice := FSWorkOrderProduct.QtyToBill; + end; + if FSWorkOrder.Get(FSWorkOrderProduct.WorkOrder) then + IntegrateToService := FSWorkOrder.IntegrateToService; + end; + Database::"FS Work Order Service": + begin + SourceRecordRef.SetTable(FSWorkOrderService); + if FSWorkOrderService.LineStatus = FSWorkOrderService.LineStatus::Used then begin + FSQuantityToConsume := FSWorkOrderService.Duration / 60; + FSQuantityToInvoice := FSWorkOrderService.DurationToBill / 60; + end; + if FSWorkOrder.Get(FSWorkOrderService.WorkOrder) then + IntegrateToService := FSWorkOrder.IntegrateToService; + end; + end; + + SetCurrentProjectPlanningQuantities(SourceRecordRef, QuantityCurrentlyConsumed, QuantityCurrentlyInvoiced); + + if IntegrateToService then + IgnoreRecord := true; + + if (QuantityCurrentlyConsumed = FSQuantityToConsume) and (QuantityCurrentlyInvoiced = FSQuantityToInvoice) then + IgnoreRecord := true; + end; + + internal procedure IgnoreArchievedServiceOrdersOnQueryPostFilterIgnoreRecord(SourceRecordRef: RecordRef; var IgnoreRecord: Boolean) + var + FSConnectionSetup: Record "FS Connection Setup"; + ServiceHeader: Record "Service Header"; + CRMIntegrationRecord: Record "CRM Integration Record"; + begin + if not FSConnectionSetup.IsEnabled() then + exit; + + if IgnoreRecord then + exit; + + SourceRecordRef.SetTable(ServiceHeader); + if not CRMIntegrationRecord.FindByRecordID(ServiceHeader.RecordId) then + exit; + + if CRMIntegrationRecord."Archived Service Order" then + IgnoreRecord := true; + end; + + internal procedure IgnoreArchievedCRMWorkOrdersOnQueryPostFilterIgnoreRecord(SourceRecordRef: RecordRef; var IgnoreRecord: Boolean) + var + FSConnectionSetup: Record "FS Connection Setup"; + FSWorkOrder: Record "FS Work Order"; + FSWorkOrderIncident: Record "FS Work Order Incident"; + CRMIntegrationRecord: Record "CRM Integration Record"; + begin + if not FSConnectionSetup.IsEnabled() then + exit; + + if IgnoreRecord then + exit; + + SourceRecordRef.SetTable(FSWorkOrder); + + // at least one work order incident should exist + FSWorkOrderIncident.SetRange(WorkOrder, FSWorkOrder.WorkOrderId); + if FSWorkOrderIncident.IsEmpty() then + IgnoreRecord := true; + + // skip archived work orders + if CRMIntegrationRecord.FindByCRMID(FSWorkOrder.WorkOrderId) then + if CRMIntegrationRecord."Archived Service Order" then + IgnoreRecord := true; + end; + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Integration Table Synch.", 'OnAfterInitSynchJob', '', true, true)] local procedure LogTelemetryOnAfterInitSynchJob(ConnectionType: TableConnectionType; IntegrationTableID: Integer) var @@ -1289,6 +2616,227 @@ codeunit 6610 "FS Int. Table Subscriber" exit(''); 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 + IntegrationMgt: Codeunit "FS Integration Mgt."; + begin + IntegrationMgt.TestOneServiceItemLinePerOrderModificationIsAllowed(Rec); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"CRM Integration Management", 'OnIsCRMIntegrationRecord', '', false, false)] + local procedure HandleOnIsCRMIntegrationRecord(TableID: Integer; var isIntegrationRecord: Boolean) + begin + if TableID = Database::"Service Header Archive" then + isIntegrationRecord := true; + end; + + [EventSubscriber(ObjectType::Table, Database::"Service Header", 'OnAfterDeleteEvent', '', false, false)] + local procedure HandleOnAfterDeleteServiceHeader(var Rec: Record "Service Header") + begin + if Rec.IsTemporary() then + exit; + + MarkArchivedServiceOrder(Rec); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Service Document Archive Mgmt.", 'OnAfterStoreServiceLineArchive', '', false, false)] + local procedure OnAfterStoreServiceLineArchive(var ServiceLine: Record "Service Line"; var ServiceLineArchive: Record "Service Line Archive") + begin + MarkArchivedServiceOrderLine(ServiceLine, ServiceLineArchive); + end; + + [EventSubscriber(ObjectType::Table, Database::"Service Item Line", 'OnAfterDeleteEvent', '', false, false)] + local procedure HandleOnAfterDeleteServiceItemLine(var Rec: Record "Service Item Line") + begin + if Rec.IsTemporary() then + exit; + + UncoupleRecord(Rec); + end; + + [EventSubscriber(ObjectType::Table, Database::"Service Line", 'OnAfterDeleteEvent', '', false, false)] + local procedure HandleOnAfterDeleteServiceLine(var Rec: Record "Service Line") + begin + if Rec.IsTemporary() then + exit; + + UncoupleRecord(Rec); + end; + + local procedure UncoupleRecord(ServiceItemLine: Record "Service Item Line") + var + FSConnectionSetup: Record "FS Connection Setup"; + CRMIntegrationRecord: Record "CRM Integration Record"; + begin + if not FSConnectionSetup.IsIntegrationTypeServiceEnabled() then + exit; + + CRMIntegrationRecord.SetCurrentKey("Integration ID"); + CRMIntegrationRecord.SetRange("Integration ID", ServiceItemLine.SystemId); + if not CRMIntegrationRecord.FindFirst() then + exit; + + CRMIntegrationRecord."Last Synch. CRM Modified On" := CurrentDateTime(); + CRMIntegrationRecord."Last Synch. CRM Result" := CRMIntegrationRecord."Last Synch. CRM Result"::Success; + CRMIntegrationRecord."Last Synch. Modified On" := CurrentDateTime(); + CRMIntegrationRecord."Last Synch. Result" := CRMIntegrationRecord."Last Synch. Result"::Success; + CRMIntegrationRecord.Skipped := true; + CRMIntegrationRecord."Skip Reimport" := true; + CRMIntegrationRecord.Modify(); + end; + + local procedure UncoupleRecord(ServiceLine: Record "Service Line") + var + FSConnectionSetup: Record "FS Connection Setup"; + CRMIntegrationRecord: Record "CRM Integration Record"; + begin + if not FSConnectionSetup.IsIntegrationTypeServiceEnabled() then + exit; + + CRMIntegrationRecord.SetCurrentKey("Integration ID"); + CRMIntegrationRecord.SetRange("Integration ID", ServiceLine.SystemId); + if not CRMIntegrationRecord.FindFirst() then + exit; + + CRMIntegrationRecord."Last Synch. CRM Modified On" := CurrentDateTime(); + CRMIntegrationRecord."Last Synch. CRM Result" := CRMIntegrationRecord."Last Synch. CRM Result"::Success; + CRMIntegrationRecord."Last Synch. Modified On" := CurrentDateTime(); + CRMIntegrationRecord."Last Synch. Result" := CRMIntegrationRecord."Last Synch. Result"::Success; + CRMIntegrationRecord.Skipped := true; + CRMIntegrationRecord."Skip Reimport" := true; + CRMIntegrationRecord.Modify(); + end; + + internal procedure MarkArchivedServiceOrder(ServiceHeader: Record "Service Header") + var + FSConnectionSetup: Record "FS Connection Setup"; + CRMIntegrationRecord: Record "CRM Integration Record"; + begin + if not FSConnectionSetup.IsIntegrationTypeServiceEnabled() then + exit; + + CRMIntegrationRecord.SetRange("Table ID", Database::"Service Header"); + CRMIntegrationRecord.SetRange("Integration ID", ServiceHeader.SystemId); + if CRMIntegrationRecord.FindFirst() then begin + CRMIntegrationRecord."Archived Service Header Id" := GetLatestServiceOrderArchiveSystemId(ServiceHeader."No."); + CRMIntegrationRecord."Archived Service Order" := true; + CRMIntegrationRecord.Modify(); + end; + end; + + internal procedure MarkArchivedServiceOrderLine(var ServiceLine: Record "Service Line"; var ServiceLineArchive: Record "Service Line Archive") + var + FSConnectionSetup: Record "FS Connection Setup"; + CRMIntegrationRecord: Record "CRM Integration Record"; + begin + if not FSConnectionSetup.IsIntegrationTypeServiceEnabled() then + exit; + + CRMIntegrationRecord.SetRange("Table ID", Database::"Service Line"); + CRMIntegrationRecord.SetRange("Integration ID", ServiceLine.SystemId); + if CRMIntegrationRecord.FindFirst() then begin + CRMIntegrationRecord."Archived Service Line Id" := ServiceLineArchive.SystemId; + CRMIntegrationRecord.Modify(); + end; + end; + + local procedure GetLatestServiceOrderArchiveSystemId(OrderNo: Code[20]): Guid + var + ServiceHeaderArchive: Record "Service Header Archive"; + EmptyGuid: Guid; + begin + ServiceHeaderArchive.SetRange("Document Type", ServiceHeaderArchive."Document Type"::Order); + ServiceHeaderArchive.SetRange("No.", OrderNo); + if not ServiceHeaderArchive.FindLast() then + exit(EmptyGuid); + + exit(ServiceHeaderArchive.SystemId); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"CRM Integration Management", 'OnBeforeOpenCoupledNavRecordPage', '', false, false)] + local procedure OnBeforeOpenCoupledNavRecordPage(CRMID: Guid; CRMEntityTypeName: Text; var Result: Boolean; var IsHandled: Boolean) + begin + if IsHandled or Result or (CRMEntityTypeName <> 'msdyn_workorder') then + exit; + + if not OnlyServiceHeaderArchiveExists(CRMID) then + exit; + Result := OpenServiceHeaderArchive(CRMID); + + IsHandled := Result; + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"CRM Integration Management", 'OnBeforeOpenRecordCardPage', '', false, false)] + local procedure OnBeforeOpenRecordCardPage(RecordID: RecordID; var IsHandled: Boolean) + var + ServiceHeader: Record "Service Header"; + RecordRef: RecordRef; + Result: Boolean; + begin + RecordRef := RecordID.GetRecord(); + case RecordID.TableNo of + Database::"Service Header": + begin + RecordRef.SetTable(ServiceHeader); + Page.Run(Page::"Service Order", ServiceHeader); + IsHandled := true; + end; + end; + end; + + local procedure OnlyServiceHeaderArchiveExists(CRMID: Guid): Boolean + var + CRMIntegrationRecord: Record "CRM Integration Record"; + ServiceHeader: Record "Service Header"; + ServiceHeaderArchive: Record "Service Header Archive"; + begin + CRMIntegrationRecord.SetRange("Table ID", Database::"Service Header"); + CRMIntegrationRecord.SetRange("CRM ID", CRMID); + if not CRMIntegrationRecord.FindFirst() then + exit(false); + + if ServiceHeader.GetBySystemId(CRMIntegrationRecord."Integration ID") then + exit(false); + + if ServiceHeaderArchive.GetBySystemId(CRMIntegrationRecord."Archived Service Header Id") then + exit(true); + end; + + local procedure OpenServiceHeader(CRMID: Guid): Boolean + var + CRMIntegrationRecord: Record "CRM Integration Record"; + ServiceHeader: Record "Service Header"; + begin + CRMIntegrationRecord.SetRange("Table ID", Database::"Service Header"); + CRMIntegrationRecord.SetRange("CRM ID", CRMID); + if not CRMIntegrationRecord.FindFirst() then + exit(false); + + if not ServiceHeader.GetBySystemId(CRMIntegrationRecord."Integration ID") then + exit(false); + + Page.Run(Page::"Service Order", ServiceHeader); + exit(true); + end; + + local procedure OpenServiceHeaderArchive(CRMID: Guid): Boolean + var + CRMIntegrationRecord: Record "CRM Integration Record"; + ServiceHeaderArchive: Record "Service Header Archive"; + begin + CRMIntegrationRecord.SetRange("Table ID", Database::"Service Header"); + CRMIntegrationRecord.SetRange("CRM ID", CRMID); + if not CRMIntegrationRecord.FindFirst() then + exit(false); + + if not ServiceHeaderArchive.GetBySystemId(CRMIntegrationRecord."Archived Service Header Id") then + exit(false); + + Page.Run(Page::"Service Order Archive", ServiceHeaderArchive); + exit(true); + end; + [IntegrationEvent(false, false)] local procedure OnSetUpNewLineOnNewLine(var JobJournalLine: Record "Job Journal Line"; var JobJournalTemplate: Record "Job Journal Template"; var JobJournalBatch: Record "Job Journal Batch"; var Handled: Boolean); begin diff --git a/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSIntegrationMgt.Codeunit.al b/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSIntegrationMgt.Codeunit.al index e8c803b058..3631106739 100644 --- a/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSIntegrationMgt.Codeunit.al +++ b/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSIntegrationMgt.Codeunit.al @@ -6,8 +6,10 @@ namespace Microsoft.Integration.DynamicsFieldService; using Microsoft.Integration.Dataverse; using Microsoft.Integration.D365Sales; +using Microsoft.Service.Setup; using System; using Microsoft.Utilities; +using Microsoft.Foundation.NoSeries; codeunit 6615 "FS Integration Mgt." { @@ -187,6 +189,122 @@ codeunit 6615 "FS Integration Mgt." exit(GuidVar); end; + internal procedure EnableServiceOrderArchive() + var + ServiceMgtSetup: Record "Service Mgt. Setup"; + begin + ServiceMgtSetup.Get(); + if ServiceMgtSetup."Archive Orders" then + exit; + + ServiceMgtSetup.Validate("Archive Orders", true); + ServiceMgtSetup.Modify(true); + end; + + internal procedure TestManualServiceOrderNoSeriesFlag(IntegrationType: Enum "FS Integration Type") + var + ServiceMgtSetup: Record "Service Mgt. Setup"; + NoSeries: Codeunit "No. Series"; + CannotInvoiceErrorInfo: ErrorInfo; + NoManualNoSeriesTitleErr: Label 'Service Order No. Series must be set to manual.'; + NoManualNoSeriesErr: Label 'Please make sure that the No. Series setup is correct.'; + SetToManualLbl: Label 'Set to Manual'; + OpenNoSeriesListLbl: Label 'Open No. Series. list'; + begin + if not (IntegrationType in [IntegrationType::Service, IntegrationType::Both]) then + exit; + + ServiceMgtSetup.Get(); + if not NoSeries.IsManual(ServiceMgtSetup."Service Order Nos.") then begin + CannotInvoiceErrorInfo.Title := NoManualNoSeriesTitleErr; + CannotInvoiceErrorInfo.Message := NoManualNoSeriesErr; + + if ServiceMgtSetup."Service Order Nos." <> '' then + CannotInvoiceErrorInfo.AddAction( + SetToManualLbl, + Codeunit::"FS Integration Mgt.", + 'SetServiceOrderNoSeriesToManual' + ); + + CannotInvoiceErrorInfo.PageNo := Page::"No. Series"; + CannotInvoiceErrorInfo.AddNavigationAction(OpenNoSeriesListLbl); + Error(CannotInvoiceErrorInfo); + end; + end; + + procedure SetServiceOrderNoSeriesToManual(ErrorInfo: ErrorInfo) + var + ServiceMgtSetup: Record "Service Mgt. Setup"; + NoSeries: Record "No. Series"; + begin + ServiceMgtSetup.Get(); + NoSeries.Get(ServiceMgtSetup."Service Order Nos."); + NoSeries.Validate("Manual Nos.", true); + NoSeries.Modify(true); + end; + + internal procedure TestOneServiceItemLinePerOrder(IntegrationType: Enum "FS Integration Type") + var + ServiceMgtSetup: Record "Service Mgt. Setup"; + begin + if not (IntegrationType in [IntegrationType::Service, IntegrationType::Both]) then + exit; + + ServiceMgtSetup.Get(); + ServiceMgtSetup.TestField("One Service Item Line/Order", false); + end; + + internal procedure TestOneServiceItemLinePerOrderModificationIsAllowed(ServiceMgtSetup: Record "Service Mgt. Setup") + var + ConnectionSetup: Record "FS Connection Setup"; + begin + if not ServiceMgtSetup."One Service Item Line/Order" then + exit; + + if not ConnectionSetup.IsIntegrationTypeServiceEnabled() then + exit; + + ConnectionSetup.TestField("Is Enabled", false); + end; + + internal procedure GetDefaultWorkOrderIncident(): Guid + var + ConnectionSetup: Record "FS Connection Setup"; + begin + ConnectionSetup.Get(); + + if IsNullGuid(ConnectionSetup."Default Work Order Incident ID") then begin + ConnectionSetup."Default Work Order Incident ID" := GenerateDefaultWorkOrderIncident(); + ConnectionSetup.Modify(); + end; + + exit(ConnectionSetup."Default Work Order Incident ID"); + end; + + internal procedure GenerateDefaultWorkOrderIncident(): Guid + var + IncidentType: Record "FS Incident Type"; + IncidentTypeNameLbl: Label 'Business Central - Default Incident Type'; + begin + IncidentType.IncidentTypeId := CreateGuid(); + IncidentType.Name := IncidentTypeNameLbl; + IncidentType.Insert(); + + exit(IncidentType.IncidentTypeId); + end; + + internal procedure GetBookingStatusCompleted(): Guid + var + FSBookingStatus: Record "FS Booking Status"; + EmptyGuid: Guid; + begin + FSBookingStatus.SetRange(FieldServiceStatus, FSBookingStatus.FieldServiceStatus::Completed); + if not FSBookingStatus.FindFirst() then + exit(EmptyGuid); + + exit(FSBookingStatus.BookingStatusId); + end; + [EventSubscriber(ObjectType::Table, Database::"Service Connection", 'OnRegisterServiceConnection', '', false, false)] local procedure RegisterFSConnectionOnRegisterServiceConnection(var ServiceConnection: Record "Service Connection") var diff --git a/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSLookupFSTables.Codeunit.al b/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSLookupFSTables.Codeunit.al index 85d78bb769..83e33bc666 100644 --- a/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSLookupFSTables.Codeunit.al +++ b/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSLookupFSTables.Codeunit.al @@ -26,6 +26,12 @@ codeunit 6612 "FS Lookup FS Tables" Database::"FS Customer Asset": if LookupFSCustomerAsset(SavedCRMId, CRMId, IntTableFilter) then Handled := true; + Database::"FS Work Order": + if LookupFSWorkOrder(SavedCRMId, CRMId, IntTableFilter) then + Handled := true; + Database::"FS Work Order Type": + if LookupFSWorkOrderType(SavedCRMId, CRMId, IntTableFilter) then + Handled := true; end; end; @@ -78,4 +84,54 @@ codeunit 6612 "FS Lookup FS Tables" end; exit(false); end; + + local procedure LookupFSWorkOrderType(SavedCRMId: Guid; var CRMId: Guid; IntTableFilter: Text): Boolean + var + FSWorkOrderType: Record "FS Work Order Type"; + OriginalFSWorkOrderType: Record "FS Work Order Type"; + FSWorkOrderTypes: Page "FS Work Order Types"; + begin + if not IsNullGuid(CRMId) then begin + if FSWorkOrderType.Get(CRMId) then + FSWorkOrderTypes.SetRecord(FSWorkOrderType); + if not IsNullGuid(SavedCRMId) then + if OriginalFSWorkOrderType.Get(SavedCRMId) then + FSWorkOrderTypes.SetCurrentlyCoupledFSWorkOrderType(OriginalFSWorkOrderType); + end; + FSWorkOrderType.SetView(IntTableFilter); + FSWorkOrderTypes.SetTableView(FSWorkOrderType); + FSWorkOrderTypes.LookupMode(true); + Commit(); + if FSWorkOrderTypes.RunModal() = Action::LookupOK then begin + FSWorkOrderTypes.GetRecord(FSWorkOrderType); + CRMId := FSWorkOrderType.WorkOrderTypeId; + exit(true); + end; + exit(false); + end; + + local procedure LookupFSWorkOrder(SavedCRMId: Guid; var CRMId: Guid; IntTableFilter: Text): Boolean + var + FSWorkOrderType: Record "FS Work Order"; + OriginalFSWorkOrder: Record "FS Work Order"; + FSWorkOrders: Page "FS Work Orders"; + begin + if not IsNullGuid(CRMId) then begin + if FSWorkOrderType.Get(CRMId) then + FSWorkOrders.SetRecord(FSWorkOrderType); + if not IsNullGuid(SavedCRMId) then + if OriginalFSWorkOrder.Get(SavedCRMId) then + FSWorkOrders.SetCurrentlyCoupledFSWorkOrder(OriginalFSWorkOrder); + end; + FSWorkOrderType.SetView(IntTableFilter); + FSWorkOrders.SetTableView(FSWorkOrderType); + FSWorkOrders.LookupMode(true); + Commit(); + if FSWorkOrders.RunModal() = Action::LookupOK then begin + FSWorkOrders.GetRecord(FSWorkOrderType); + CRMId := FSWorkOrderType.WorkOrderId; + exit(true); + end; + exit(false); + end; } \ No newline at end of file diff --git a/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSSetupDefaults.Codeunit.al b/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSSetupDefaults.Codeunit.al index 031c16c5c4..e432f99a05 100644 --- a/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSSetupDefaults.Codeunit.al +++ b/Apps/W1/FieldServiceIntegration/app/src/Codeunits/FSSetupDefaults.Codeunit.al @@ -6,10 +6,13 @@ namespace Microsoft.Integration.DynamicsFieldService; using Microsoft.Integration.Dataverse; using Microsoft.Integration.D365Sales; +using Microsoft.Service.Setup; using Microsoft.Integration.SyncEngine; using Microsoft.Inventory.Location; +using Microsoft.Service.Document; using Microsoft.Inventory.Setup; using Microsoft.Utilities; +using Microsoft.Inventory.Item; using System.Threading; using Microsoft.Projects.Project.Job; using Microsoft.Service.Item; @@ -31,6 +34,7 @@ codeunit 6611 "FS Setup Defaults" internal procedure ResetConfiguration(var FSConnectionSetup: Record "FS Connection Setup") var CDSIntegrationMgt: Codeunit "CDS Integration Mgt."; + FSIntegrationMgt: Codeunit "FS Integration Mgt."; CRMSetupDefault: Codeunit "CRM Setup Defaults"; IsHandled: Boolean; begin @@ -42,13 +46,33 @@ codeunit 6611 "FS Setup Defaults" CDSIntegrationMgt.RegisterConnection(); CDSIntegrationMgt.ActivateConnection(); - ResetProjectTaskMapping(FSConnectionSetup, 'PROJECTTASK', true); - ResetProjectJournalLineWOProductMapping(FSConnectionSetup, 'PJLINE-WORDERPRODUCT', true); - ResetProjectJournalLineWOServiceMapping(FSConnectionSetup, 'PJLINE-WORDERSERVICE', true); + if FSConnectionSetup."Integration Type" in [FSConnectionSetup."Integration Type"::Project, + FSConnectionSetup."Integration Type"::Both] then begin + ResetProjectTaskMapping(FSConnectionSetup, 'PROJECTTASK', true); + ResetProjectJournalLineWOProductMapping(FSConnectionSetup, 'PJLINE-WORDERPRODUCT', true); + ResetProjectJournalLineWOServiceMapping(FSConnectionSetup, 'PJLINE-WORDERSERVICE', true); + end; + ResetServiceItemCustomerAssetMapping(FSConnectionSetup, 'SVCITEM-CUSTASSET', true); ResetResourceBookableResourceMapping(FSConnectionSetup, 'RESOURCE-BOOKABLERSC', true); ResetLocationMapping(FSConnectionSetup, 'LOCATION', true, false); CRMSetupDefault.ResetItemProductMapping('ITEM-PRODUCT', true); + + if FSConnectionSetup."Integration Type" in [FSConnectionSetup."Integration Type"::Service, + FSConnectionSetup."Integration Type"::Both] then begin + ResetServiceOrderTypeMapping(FSConnectionSetup, 'SRVORDERTYPE', true); + ResetServiceOrderMapping(FSConnectionSetup, 'SRVORDER', true); + ResetServiceOrderItemLineMapping(FSConnectionSetup, 'SRVORDERITEMLINE', true); + ResetServiceOrderLineItemMapping(FSConnectionSetup, 'SRVORDERLINE-ITEM', true); + ResetServiceOrderLineServiceItemMapping(FSConnectionSetup, 'SRVORDERLINE-SERVICE', true); + ResetServiceOrderLineResourceMapping(FSConnectionSetup, 'SRVORDERLINE-RESOURC', true); + + if IsNullGuid(FSConnectionSetup."Default Work Order Incident ID") then + FSConnectionSetup."Default Work Order Incident ID" := FSIntegrationMgt.GenerateDefaultWorkOrderIncident(); + + FSIntegrationMgt.EnableServiceOrderArchive(); + end; + SetCustomIntegrationsTableMappings(FSConnectionSetup); end; @@ -112,6 +136,7 @@ codeunit 6611 "FS Setup Defaults" FSWorkOrderProduct: Record "FS Work Order Product"; JobJournalLine: Record "Job Journal Line"; CDSCompany: Record "CDS Company"; + InventorySetup: Record "Inventory Setup"; CDSIntegrationMgt: Codeunit "CDS Integration Mgt."; IsHandled: Boolean; EmptyGuid: Guid; @@ -126,7 +151,7 @@ codeunit 6611 "FS Setup Defaults" FSWorkOrderProduct.SetFilter(ProjectTask, '<>' + Format(EmptyGuid)); case FSConnectionSetup."Line Synch. Rule" of "FS Work Order Line Synch. Rule"::LineUsed: - FSWorkOrderProduct.SetRange(LineStatus, FSWorkOrderProduct.LineStatus::Used); + FSWorkOrderProduct.SetFilter(LineStatus, Format(FSWorkOrderProduct.LineStatus::Estimated) + '|' + Format(FSWorkOrderProduct.LineStatus::Used)); "FS Work Order Line Synch. Rule"::WorkOrderCompleted: FSWorkOrderProduct.SetFilter(WorkOrderStatus, Format(FSWorkOrderProduct.WorkOrderStatus::Completed) + '|' + Format(FSWorkOrderProduct.WorkOrderStatus::Posted)); end; @@ -143,6 +168,7 @@ codeunit 6611 "FS Setup Defaults" IntegrationTableMapping.SetIntegrationTableFilter( GetTableFilterFromView(Database::"FS Work Order Product", FSWorkOrderProduct.TableCaption(), FSWorkOrderProduct.GetView())); IntegrationTableMapping."Dependency Filter" := 'CUSTOMER|ITEM-PRODUCT'; + IntegrationTableMapping."Deletion-Conflict Resolution" := IntegrationTableMapping."Deletion-Conflict Resolution"::"Restore Records"; IntegrationTableMapping.Modify(); InsertIntegrationFieldMapping( @@ -214,7 +240,7 @@ codeunit 6611 "FS Setup Defaults" FSWorkOrderService.SetFilter(ProjectTask, '<>' + Format(EmptyGuid)); case FSConnectionSetup."Line Synch. Rule" of "FS Work Order Line Synch. Rule"::LineUsed: - FSWorkOrderService.SetRange(LineStatus, FSWorkOrderService.LineStatus::Used); + FSWorkOrderService.SetFilter(LineStatus, Format(FSWorkOrderService.LineStatus::Estimated) + '|' + Format(FSWorkOrderService.LineStatus::Used)); "FS Work Order Line Synch. Rule"::WorkOrderCompleted: FSWorkOrderService.SetFilter(WorkOrderStatus, Format(FSWorkOrderService.WorkOrderStatus::Completed) + '|' + Format(FSWorkOrderService.WorkOrderStatus::Posted)); end; @@ -230,6 +256,7 @@ codeunit 6611 "FS Setup Defaults" GetTableFilterFromView(Database::"FS Work Order Service", FSWorkOrderService.TableCaption(), FSWorkOrderService.GetView())); IntegrationTableMapping."Dependency Filter" := 'CUSTOMER|ITEM-PRODUCT|RESOURCE-BOOKABLERSC'; + IntegrationTableMapping."Deletion-Conflict Resolution" := IntegrationTableMapping."Deletion-Conflict Resolution"::"Restore Records"; IntegrationTableMapping.Modify(); InsertIntegrationFieldMapping( @@ -364,6 +391,7 @@ codeunit 6611 "FS Setup Defaults" ServiceItem.Reset(); ServiceItem.SetRange(Blocked, ServiceItem.Blocked::" "); + ServiceItem.SetRange("Service Item Components", false); InsertIntegrationTableMapping( IntegrationTableMapping, IntegrationTableMappingName, @@ -411,6 +439,8 @@ codeunit 6611 "FS Setup Defaults" InventorySetup: Record "Inventory Setup"; Location: Record Location; FSWarehouse: Record "FS Warehouse"; + CDSCompany: Record "CDS Company"; + CDSIntegrationMgt: Codeunit "CDS Integration Mgt."; begin if not SkipLocationMandatoryCheck then if InventorySetup.Get() and (not InventorySetup."Location Mandatory") then @@ -424,6 +454,9 @@ codeunit 6611 "FS Setup Defaults" Format(Location."Asm. Consump. Whse. Handling"::"Warehouse Pick (optional)") + '''|''' + Format(Location."Asm. Consump. Whse. Handling"::"Inventory Movement") + ''''); + if CDSIntegrationMgt.GetCDSCompany(CDSCompany) then + FSWarehouse.SetRange(CompanyId, CDSCompany.CompanyId); + InsertIntegrationTableMapping( IntegrationTableMapping, IntegrationTableMappingName, Database::Location, Database::"FS Warehouse", @@ -458,6 +491,543 @@ codeunit 6611 "FS Setup Defaults" RecreateJobQueueEntryFromIntTableMapping(IntegrationTableMapping, 1, ShouldRecreateJobQueueEntry, 5); end; + local procedure ResetServiceOrderTypeMapping(var FSConnectionSetup: Record "FS Connection Setup"; IntegrationTableMappingName: Code[20]; ShouldRecreateJobQueueEntry: Boolean) + var + IntegrationTableMapping: Record "Integration Table Mapping"; + IntegrationFieldMapping: Record "Integration Field Mapping"; + ServiceOrderType: Record "Service Order Type"; + FSWorkOrderType: Record "FS Work Order Type"; + CDSCompany: Record "CDS Company"; + CDSIntegrationMgt: Codeunit "CDS Integration Mgt."; + IsHandled: Boolean; + begin + IsHandled := false; + OnBeforeResetServiceOrderTypeMapping(IntegrationTableMappingName, ShouldRecreateJobQueueEntry, IsHandled); + if IsHandled then + exit; + + if not (FSConnectionSetup."Integration Type" in [FSConnectionSetup."Integration Type"::Service, + FSConnectionSetup."Integration Type"::Both]) then + exit; + + FSWorkOrderType.Reset(); + FSWorkOrderType.SetRange(StateCode, FSWorkOrderType.StateCode::Active); + FSWorkOrderType.SetRange(IntegrateToService, true); + if CDSIntegrationMgt.GetCDSCompany(CDSCompany) then + FSWorkOrderType.SetRange(CompanyId, CDSCompany.CompanyId); + + InsertIntegrationTableMapping( + IntegrationTableMapping, IntegrationTableMappingName, + Database::"Service Order Type", Database::"FS Work Order Type", + FSWorkOrderType.FieldNo(WorkOrderTypeId), FSWorkOrderType.FieldNo(ModifiedOn), + '', '', false); + + IntegrationTableMapping.SetIntegrationTableFilter( + GetTableFilterFromView(Database::"FS Work Order Type", FSWorkOrderType.TableCaption(), FSWorkOrderType.GetView())); + IntegrationTableMapping.Modify(); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceOrderType.FieldNo(Code), + FSWorkOrderType.FieldNo(Code), + IntegrationFieldMapping.Direction::Bidirectional, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceOrderType.FieldNo(Description), + FSWorkOrderType.FieldNo(Name), + IntegrationFieldMapping.Direction::Bidirectional, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + 0, + FSWorkOrderType.FieldNo(IntegrateToService), + IntegrationFieldMapping.Direction::Bidirectional, + 'true', true, false); + + RecreateJobQueueEntryFromIntTableMapping(IntegrationTableMapping, 1, ShouldRecreateJobQueueEntry, 30); + end; + + local procedure ResetServiceOrderMapping(var FSConnectionSetup: Record "FS Connection Setup"; IntegrationTableMappingName: Code[20]; ShouldRecreateJobQueueEntry: Boolean) + var + IntegrationTableMapping: Record "Integration Table Mapping"; + IntegrationFieldMapping: Record "Integration Field Mapping"; + ServiceOrder: Record "Service Header"; + FSWorkOrder: Record "FS Work Order"; + CDSCompany: Record "CDS Company"; + CDSIntegrationMgt: Codeunit "CDS Integration Mgt."; + CRMSetupDefaults: Codeunit "CRM Setup Defaults"; + ArchivedServiceOrdersSynchJobDescTxt: Label 'Archived Service Orders - %1 synchronization job', Comment = '%1 = CRM product name'; + IsHandled: Boolean; + begin + IsHandled := false; + OnBeforeResetServiceOrderMapping(IntegrationTableMappingName, ShouldRecreateJobQueueEntry, IsHandled); + if IsHandled then + exit; + + if not (FSConnectionSetup."Integration Type" in [FSConnectionSetup."Integration Type"::Service, + FSConnectionSetup."Integration Type"::Both]) then + exit; + + ServiceOrder.Reset(); + ServiceOrder.SetRange("Document Type", ServiceOrder."Document Type"::Order); + + FSWorkOrder.Reset(); + FSWorkOrder.SetRange(IntegrateToService, true); + FSWorkOrder.SetFilter(SystemStatus, '%1|%2|%3|%4', + FSWorkOrder.SystemStatus::Unscheduled, + FSWorkOrder.SystemStatus::Scheduled, + FSWorkOrder.SystemStatus::InProgress, + FSWorkOrder.SystemStatus::Completed + ); + if CDSIntegrationMgt.GetCDSCompany(CDSCompany) then + FSWorkOrder.SetRange(CompanyId, CDSCompany.CompanyId); + + InsertIntegrationTableMapping( + IntegrationTableMapping, IntegrationTableMappingName, + Database::"Service Header", Database::"FS Work Order", + FSWorkOrder.FieldNo(WorkOrderId), FSWorkOrder.FieldNo(ModifiedOn), + '', '', false); + + IntegrationTableMapping.SetTableFilter( + GetTableFilterFromView(Database::"Service Header", ServiceOrder.TableCaption(), ServiceOrder.GetView())); + IntegrationTableMapping.SetIntegrationTableFilter( + GetTableFilterFromView(Database::"FS Work Order", FSWorkOrder.TableCaption(), FSWorkOrder.GetView())); + IntegrationTableMapping."Dependency Filter" := 'CUSTOMER|SRVORDERTYPE'; + IntegrationTableMapping.Modify(); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceOrder.FieldNo("Document Type"), + 0, IntegrationFieldMapping.Direction::FromIntegrationTable, + 'Order', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceOrder.FieldNo("No."), + FSWorkOrder.FieldNo(Name), + IntegrationFieldMapping.Direction::Bidirectional, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceOrder.FieldNo("Customer No."), + FSWorkOrder.FieldNo(ServiceAccount), + IntegrationFieldMapping.Direction::Bidirectional, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceOrder.FieldNo("Order Date"), + FSWorkOrder.FieldNo(CreatedOn), + IntegrationFieldMapping.Direction::FromIntegrationTable, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceOrder.FieldNo("Service Order Type"), + FSWorkOrder.FieldNo(WorkOrderType), + IntegrationFieldMapping.Direction::Bidirectional, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceOrder.FieldNo("Work Description"), + FSWorkOrder.FieldNo(WorkOrderSummary), + IntegrationFieldMapping.Direction::Bidirectional, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceOrder.FieldNo(Status), + FSWorkOrder.FieldNo(SystemStatus), + IntegrationFieldMapping.Direction::Bidirectional, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + 0, + FSWorkOrder.FieldNo(IntegrateToService), + IntegrationFieldMapping.Direction::Bidirectional, + 'true', true, false); + + RecreateJobQueueEntryFromIntTableMapping(IntegrationTableMapping, 1, ShouldRecreateJobQueueEntry, 30); + CRMSetupDefaults.RecreateJobQueueEntry(ShouldRecreateJobQueueEntry, Codeunit::"FS Archived Service Orders Job", 30, StrSubstNo(ArchivedServiceOrdersSynchJobDescTxt, CRMProductName.SHORT()), false) + end; + + local procedure ResetServiceOrderItemLineMapping(var FSConnectionSetup: Record "FS Connection Setup"; IntegrationTableMappingName: Code[20]; ShouldRecreateJobQueueEntry: Boolean) + var + IntegrationTableMapping: Record "Integration Table Mapping"; + IntegrationFieldMapping: Record "Integration Field Mapping"; + ServiceItemLine: Record "Service Item Line"; + FSWorkOrderIncident: Record "FS Work Order Incident"; + CDSCompany: Record "CDS Company"; + CDSIntegrationMgt: Codeunit "CDS Integration Mgt."; + IsHandled: Boolean; + begin + IsHandled := false; + OnBeforeResetServiceOrderItemLineMapping(IntegrationTableMappingName, ShouldRecreateJobQueueEntry, IsHandled); + if IsHandled then + exit; + + if not (FSConnectionSetup."Integration Type" in [FSConnectionSetup."Integration Type"::Service, + FSConnectionSetup."Integration Type"::Both]) then + exit; + + ServiceItemLine.Reset(); + ServiceItemLine.SetRange("Document Type", ServiceItemLine."Document Type"::Order); + ServiceItemLine.SetRange("FS Bookings", false); + + FSWorkOrderIncident.Reset(); + if CDSIntegrationMgt.GetCDSCompany(CDSCompany) then + FSWorkOrderIncident.SetRange(CompanyId, CDSCompany.CompanyId); + + InsertIntegrationTableMapping( + IntegrationTableMapping, IntegrationTableMappingName, + Database::"Service Item Line", Database::"FS Work Order Incident", + FSWorkOrderIncident.FieldNo(WorkOrderIncidentId), FSWorkOrderIncident.FieldNo(ModifiedOn), + '', '', false); + + IntegrationTableMapping.SetTableFilter( + GetTableFilterFromView(Database::"Service Item Line", ServiceItemLine.TableCaption(), ServiceItemLine.GetView())); + IntegrationTableMapping.SetIntegrationTableFilter( + GetTableFilterFromView(Database::"FS Work Order Incident", FSWorkOrderIncident.TableCaption(), FSWorkOrderIncident.GetView())); + IntegrationTableMapping."Dependency Filter" := 'SRVORDER|SVCITEM-CUSTASSET'; + IntegrationTableMapping."Update-Conflict Resolution" := IntegrationTableMapping."Update-Conflict Resolution"::"Get Update from Integration"; + IntegrationTableMapping.Modify(); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceItemLine.FieldNo("Document Type"), + 0, IntegrationFieldMapping.Direction::FromIntegrationTable, + 'Order', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceItemLine.FieldNo("Service Item No."), + FSWorkOrderIncident.FieldNo(CustomerAsset), + IntegrationFieldMapping.Direction::Bidirectional, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceItemLine.FieldNo(Description), + FSWorkOrderIncident.FieldNo(Description), + IntegrationFieldMapping.Direction::Bidirectional, + '', true, false); + end; + + local procedure ResetServiceOrderLineItemMapping(var FSConnectionSetup: Record "FS Connection Setup"; IntegrationTableMappingName: Code[20]; ShouldRecreateJobQueueEntry: Boolean) + var + IntegrationTableMapping: Record "Integration Table Mapping"; + IntegrationFieldMapping: Record "Integration Field Mapping"; + ServiceLine: Record "Service Line"; + FSWorkOrderProduct: Record "FS Work Order Product"; + CDSCompany: Record "CDS Company"; + InventorySetup: Record "Inventory Setup"; + CDSIntegrationMgt: Codeunit "CDS Integration Mgt."; + EmptyGuid: Guid; + IsHandled: Boolean; + begin + IsHandled := false; + OnBeforeResetServiceOrderLineItemMapping(IntegrationTableMappingName, ShouldRecreateJobQueueEntry, IsHandled); + if IsHandled then + exit; + + if not (FSConnectionSetup."Integration Type" in [FSConnectionSetup."Integration Type"::Service, + FSConnectionSetup."Integration Type"::Both]) then + exit; + + ServiceLine.Reset(); + ServiceLine.SetRange("Document Type", ServiceLine."Document Type"::Order); + ServiceLine.SetRange(Type, ServiceLine.Type::Item); + ServiceLine.SetFilter("Item Type", '%1|%2', ServiceLine."Item Type"::Inventory, ServiceLine."Item Type"::"Non-Inventory"); + FSWorkOrderProduct.Reset(); + FSWorkOrderProduct.SetFilter(WorkOrderIncident, '<>%1', EmptyGuid); + if CDSIntegrationMgt.GetCDSCompany(CDSCompany) then + FSWorkOrderProduct.SetRange(CompanyId, CDSCompany.CompanyId); + + InsertIntegrationTableMapping( + IntegrationTableMapping, IntegrationTableMappingName, + Database::"Service Line", Database::"FS Work Order Product", + FSWorkOrderProduct.FieldNo(WorkOrderProductId), FSWorkOrderProduct.FieldNo(ModifiedOn), + '', '', false); + + IntegrationTableMapping.SetTableFilter( + GetTableFilterFromView(Database::"Service Line", ServiceLine.TableCaption(), ServiceLine.GetView())); + IntegrationTableMapping.SetIntegrationTableFilter( + GetTableFilterFromView(Database::"FS Work Order Product", FSWorkOrderProduct.TableCaption(), FSWorkOrderProduct.GetView())); + IntegrationTableMapping."Dependency Filter" := 'SRVORDER|SRVORDERITEMLINE'; + IntegrationTableMapping."Update-Conflict Resolution" := IntegrationTableMapping."Update-Conflict Resolution"::"Get Update from Integration"; + IntegrationTableMapping.Modify(); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceLine.FieldNo("Document Type"), + 0, IntegrationFieldMapping.Direction::FromIntegrationTable, + 'Order', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceLine.FieldNo("Service Item Line No."), + FSWorkOrderProduct.FieldNo(WorkOrderIncident), + IntegrationFieldMapping.Direction::Bidirectional, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceLine.FieldNo("No."), + FSWorkOrderProduct.FieldNo(Product), + IntegrationFieldMapping.Direction::Bidirectional, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceLine.FieldNo("No."), + FSWorkOrderProduct.FieldNo(ProductId), + IntegrationFieldMapping.Direction::ToIntegrationTable, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceLine.FieldNo(Description), + FSWorkOrderProduct.FieldNo(Name), + IntegrationFieldMapping.Direction::Bidirectional, + '', true, false); + + if InventorySetup.Get() and (InventorySetup."Location Mandatory") then begin + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceLine.FieldNo("Location Code"), + FSWorkOrderProduct.FieldNo(WarehouseId), + IntegrationFieldMapping.Direction::Bidirectional, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceLine.FieldNo("Location Code"), + FSWorkOrderProduct.FieldNo(LocationCode), + IntegrationFieldMapping.Direction::ToIntegrationTable, + '', true, false); + end; + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceLine.FieldNo("Unit of Measure Code"), + FSWorkOrderProduct.FieldNo(Unit), + IntegrationFieldMapping.Direction::Bidirectional, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceLine.FieldNo("Quantity Shipped"), + FSWorkOrderProduct.FieldNo(QuantityShipped), + IntegrationFieldMapping.Direction::ToIntegrationTable, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceLine.FieldNo("Quantity Invoiced"), + FSWorkOrderProduct.FieldNo(QuantityInvoiced), + IntegrationFieldMapping.Direction::ToIntegrationTable, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceLine.FieldNo("Quantity Consumed"), + FSWorkOrderProduct.FieldNo(QuantityConsumed), + IntegrationFieldMapping.Direction::ToIntegrationTable, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + 0, + FSWorkOrderProduct.FieldNo(IntegrateToService), + IntegrationFieldMapping.Direction::Bidirectional, + 'true', true, false); + end; + + local procedure ResetServiceOrderLineServiceItemMapping(var FSConnectionSetup: Record "FS Connection Setup"; IntegrationTableMappingName: Code[20]; ShouldRecreateJobQueueEntry: Boolean) + var + IntegrationTableMapping: Record "Integration Table Mapping"; + IntegrationFieldMapping: Record "Integration Field Mapping"; + ServiceLine: Record "Service Line"; + FSWorkOrderService: Record "FS Work Order Service"; + CDSCompany: Record "CDS Company"; + CDSIntegrationMgt: Codeunit "CDS Integration Mgt."; + EmptyGuid: Guid; + IsHandled: Boolean; + begin + IsHandled := false; + OnBeforeResetServiceOrderLineItemMapping(IntegrationTableMappingName, ShouldRecreateJobQueueEntry, IsHandled); + if IsHandled then + exit; + + if not (FSConnectionSetup."Integration Type" in [FSConnectionSetup."Integration Type"::Service, + FSConnectionSetup."Integration Type"::Both]) then + exit; + + ServiceLine.Reset(); + ServiceLine.SetRange("Document Type", ServiceLine."Document Type"::Order); + ServiceLine.SetRange(Type, ServiceLine.Type::Item); + ServiceLine.SetRange("Item Type", ServiceLine."Item Type"::Service); + FSWorkOrderService.Reset(); + FSWorkOrderService.SetFilter(WorkOrderIncident, '<>%1', EmptyGuid); + if CDSIntegrationMgt.GetCDSCompany(CDSCompany) then + FSWorkOrderService.SetRange(CompanyId, CDSCompany.CompanyId); + + InsertIntegrationTableMapping( + IntegrationTableMapping, IntegrationTableMappingName, + Database::"Service Line", Database::"FS Work Order Service", + FSWorkOrderService.FieldNo(WorkOrderServiceId), FSWorkOrderService.FieldNo(ModifiedOn), + '', '', false); + + IntegrationTableMapping.SetTableFilter( + GetTableFilterFromView(Database::"Service Line", ServiceLine.TableCaption(), ServiceLine.GetView())); + IntegrationTableMapping.SetIntegrationTableFilter( + GetTableFilterFromView(Database::"FS Work Order Service", FSWorkOrderService.TableCaption(), FSWorkOrderService.GetView())); + IntegrationTableMapping."Dependency Filter" := 'SRVORDER|SRVORDERITEMLINE'; + IntegrationTableMapping."Update-Conflict Resolution" := IntegrationTableMapping."Update-Conflict Resolution"::"Get Update from Integration"; + IntegrationTableMapping.Modify(); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceLine.FieldNo("Document Type"), + 0, IntegrationFieldMapping.Direction::FromIntegrationTable, + 'Order', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceLine.FieldNo("Service Item Line No."), + FSWorkOrderService.FieldNo(WorkOrderIncident), + IntegrationFieldMapping.Direction::Bidirectional, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceLine.FieldNo("No."), + FSWorkOrderService.FieldNo(Service), + IntegrationFieldMapping.Direction::Bidirectional, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceLine.FieldNo(Description), + FSWorkOrderService.FieldNo(Name), + IntegrationFieldMapping.Direction::Bidirectional, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceLine.FieldNo("Unit of Measure Code"), + FSWorkOrderService.FieldNo(Unit), + IntegrationFieldMapping.Direction::Bidirectional, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceLine.FieldNo("Quantity Shipped"), + FSWorkOrderService.FieldNo(DurationShipped), + IntegrationFieldMapping.Direction::ToIntegrationTable, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceLine.FieldNo("Quantity Invoiced"), + FSWorkOrderService.FieldNo(DurationInvoiced), + IntegrationFieldMapping.Direction::ToIntegrationTable, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceLine.FieldNo("Quantity Consumed"), + FSWorkOrderService.FieldNo(DurationConsumed), + IntegrationFieldMapping.Direction::ToIntegrationTable, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + 0, + FSWorkOrderService.FieldNo(IntegrateToService), + IntegrationFieldMapping.Direction::Bidirectional, + 'true', true, false); + end; + + local procedure ResetServiceOrderLineResourceMapping(var FSConnectionSetup: Record "FS Connection Setup"; IntegrationTableMappingName: Code[20]; ShouldRecreateJobQueueEntry: Boolean) + var + IntegrationTableMapping: Record "Integration Table Mapping"; + IntegrationFieldMapping: Record "Integration Field Mapping"; + ServiceLine: Record "Service Line"; + FSBookableResourceBooking: Record "FS Bookable Resource Booking"; + CDSCompany: Record "CDS Company"; + CDSIntegrationMgt: Codeunit "CDS Integration Mgt."; + FSIntegrationMgt: Codeunit "FS Integration Mgt."; + IsHandled: Boolean; + EmptyGuid: Guid; + begin + IsHandled := false; + OnBeforeResetServiceOrderLineResourceMapping(IntegrationTableMappingName, ShouldRecreateJobQueueEntry, IsHandled); + if IsHandled then + exit; + + if not (FSConnectionSetup."Integration Type" in [FSConnectionSetup."Integration Type"::Service, + FSConnectionSetup."Integration Type"::Both]) then + exit; + + ServiceLine.Reset(); + ServiceLine.SetRange("Document Type", ServiceLine."Document Type"::Order); + ServiceLine.SetRange(Type, ServiceLine.Type::Resource); + + FSBookableResourceBooking.Reset(); + FSBookableResourceBooking.SetFilter(WorkOrder, '<>%1', EmptyGuid); + FSBookableResourceBooking.SetRange(BookingStatus, FSIntegrationMgt.GetBookingStatusCompleted()); + if CDSIntegrationMgt.GetCDSCompany(CDSCompany) then + FSBookableResourceBooking.SetRange(CompanyId, CDSCompany.CompanyId); + + InsertIntegrationTableMapping( + IntegrationTableMapping, IntegrationTableMappingName, + Database::"Service Line", Database::"FS Bookable Resource Booking", + FSBookableResourceBooking.FieldNo(BookableResourceBookingId), FSBookableResourceBooking.FieldNo(ModifiedOn), + '', '', false); + + IntegrationTableMapping.SetTableFilter( + GetTableFilterFromView(Database::"Service Line", ServiceLine.TableCaption(), ServiceLine.GetView())); + IntegrationTableMapping.SetIntegrationTableFilter( + GetTableFilterFromView(Database::"FS Bookable Resource Booking", FSBookableResourceBooking.TableCaption(), FSBookableResourceBooking.GetView())); + IntegrationTableMapping."Dependency Filter" := 'SRVORDER|SRVORDERITEMLINE|RESOURCE-BOOKABLERSC'; + IntegrationTableMapping."Update-Conflict Resolution" := IntegrationTableMapping."Update-Conflict Resolution"::"Get Update from Integration"; + IntegrationTableMapping.Modify(); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceLine.FieldNo("Document Type"), + 0, IntegrationFieldMapping.Direction::FromIntegrationTable, + 'Order', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceLine.FieldNo("No."), + FSBookableResourceBooking.FieldNo(Resource), + IntegrationFieldMapping.Direction::FromIntegrationTable, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + ServiceLine.FieldNo(Description), + FSBookableResourceBooking.FieldNo(Name), + IntegrationFieldMapping.Direction::FromIntegrationTable, + '', true, false); + + InsertIntegrationFieldMapping( + IntegrationTableMappingName, + 0, + FSBookableResourceBooking.FieldNo(IntegrateToService), + IntegrationFieldMapping.Direction::Bidirectional, + 'true', true, false); + end; + local procedure ShouldResetServiceItemMapping(): Boolean var ApplicationAreaMgmtFacade: Codeunit "Application Area Mgmt. Facade"; @@ -580,6 +1150,10 @@ codeunit 6611 "FS Setup Defaults" case BCTableNo of Database::Resource: CDSTableNo := Database::"FS Bookable Resource"; + Database::"Service Order Type": + CDSTableNo := Database::"FS Work Order Type"; + Database::"Service Header": + CDSTableNo := Database::"FS Work Order"; Database::"Service Item": CDSTableNo := Database::"FS Customer Asset"; Database::"Job Task": @@ -619,6 +1193,12 @@ codeunit 6611 "FS Setup Defaults" CRMSetupDefaults.AddEntityTableMapping('msdyn_warehouse', Database::Location, TempNameValueBuffer); CRMSetupDefaults.AddEntityTableMapping('msdyn_warehouse', Database::"FS Warehouse", TempNameValueBuffer); + CRMSetupDefaults.AddEntityTableMapping('msdyn_workordertype', Database::"Service Order Type", TempNameValueBuffer); + CRMSetupDefaults.AddEntityTableMapping('msdyn_workordertype', Database::"FS Work Order Type", TempNameValueBuffer); + + CRMSetupDefaults.AddEntityTableMapping('msdyn_workorder', Database::"Service Header", TempNameValueBuffer); + CRMSetupDefaults.AddEntityTableMapping('msdyn_workorder', Database::"FS Work Order", TempNameValueBuffer); + TempNameValueBuffer.SetRange(Name, 'product'); TempNameValueBuffer.SetRange(Value, Format(Database::Resource)); if TempNameValueBuffer.FindFirst() then @@ -630,9 +1210,13 @@ codeunit 6611 "FS Setup Defaults" local procedure ReturnNameFieldNoOnBeforeGetNameFieldNo(TableId: Integer; var FieldNo: Integer) var FSConnectionSetup: Record "FS Connection Setup"; + ServiceOrderType: Record "Service Order Type"; + ServiceOrder: Record "Service Header"; ServiceItem: Record "Service Item"; FSCustomerAsset: Record "FS Customer Asset"; FSBookableResource: Record "FS Bookable Resource"; + FSWorkOrderType: Record "FS Work Order Type"; + FSWorkOrder: Record "FS Work Order"; FSWorkOrderProduct: Record "FS Work Order Product"; FSWorkOrderService: Record "FS Work Order Service"; FSProjectTask: Record "FS Project Task"; @@ -645,12 +1229,20 @@ codeunit 6611 "FS Setup Defaults" exit; case TableId of + Database::"Service Order Type": + FieldNo := ServiceOrderType.FieldNo(Code); + Database::"Service Header": + FieldNo := ServiceOrder.FieldNo("No."); Database::"Service Item": FieldNo := ServiceItem.FieldNo("No."); Database::"FS Customer Asset": FieldNo := FSCustomerAsset.FieldNo(Name); Database::"FS Bookable Resource": FieldNo := FSBookableResource.FieldNo(Name); + Database::"FS Work Order Type": + FieldNo := FSWorkOrderType.FieldNo(Code); + Database::"FS Work Order": + FieldNo := FSWorkOrder.FieldNo(Name); Database::"FS Work Order Product": FieldNo := FSWorkOrderProduct.FieldNo(Name); Database::"FS Work Order Service": @@ -673,6 +1265,8 @@ codeunit 6611 "FS Setup Defaults" IntegrationTableMapping: Record "Integration Table Mapping"; begin case NAVTableID of + Database::"Service Order Type", + Database::"Service Header", Database::"Service Item", Database::"Work Type", Database::"Resource": @@ -789,6 +1383,31 @@ codeunit 6611 "FS Setup Defaults" begin end; + [IntegrationEvent(false, false)] + local procedure OnBeforeResetServiceOrderTypeMapping(IntegrationTableMappingName: Code[20]; ShouldRecreateJobQueueEntry: Boolean; var IsHandled: Boolean) + begin + end; + + [IntegrationEvent(false, false)] + local procedure OnBeforeResetServiceOrderMapping(IntegrationTableMappingName: Code[20]; ShouldRecreateJobQueueEntry: Boolean; var IsHandled: Boolean) + begin + end; + + [IntegrationEvent(false, false)] + local procedure OnBeforeResetServiceOrderItemLineMapping(IntegrationTableMappingName: Code[20]; ShouldRecreateJobQueueEntry: Boolean; var IsHandled: Boolean) + begin + end; + + [IntegrationEvent(false, false)] + local procedure OnBeforeResetServiceOrderLineItemMapping(IntegrationTableMappingName: Code[20]; ShouldRecreateJobQueueEntry: Boolean; var IsHandled: Boolean) + begin + end; + + [IntegrationEvent(false, false)] + local procedure OnBeforeResetServiceOrderLineResourceMapping(IntegrationTableMappingName: Code[20]; ShouldRecreateJobQueueEntry: Boolean; var IsHandled: Boolean) + begin + end; + [IntegrationEvent(false, false)] local procedure OnCreateJobQueueEntryOnBeforeJobQueueEnqueue(var JobQueueEntry: Record "Job Queue Entry"; var IntegrationTableMapping: Record "Integration Table Mapping"; JobCodeunitId: Integer; JobDescription: Text) begin diff --git a/Apps/W1/FieldServiceIntegration/app/src/Enums/FSIntegrationType.Enum.al b/Apps/W1/FieldServiceIntegration/app/src/Enums/FSIntegrationType.Enum.al new file mode 100644 index 0000000000..421361466f --- /dev/null +++ b/Apps/W1/FieldServiceIntegration/app/src/Enums/FSIntegrationType.Enum.al @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------------ +// 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.DynamicsFieldService; + +enum 6612 "FS Integration Type" +{ + Extensible = true; + + value(0; Project) + { + Caption = 'Projects (default)'; + } + value(1; Service) + { + Caption = 'Service'; + } + value(2; Both) + { + Caption = 'Both'; + } +} diff --git a/Apps/W1/FieldServiceIntegration/app/src/Page Extensions/FSServiceLines.PageExt.al b/Apps/W1/FieldServiceIntegration/app/src/Page Extensions/FSServiceLines.PageExt.al new file mode 100644 index 0000000000..8043227e5a --- /dev/null +++ b/Apps/W1/FieldServiceIntegration/app/src/Page Extensions/FSServiceLines.PageExt.al @@ -0,0 +1,38 @@ +// ------------------------------------------------------------------------------------------------ +// 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.DynamicsFieldService; + +using Microsoft.Service.Document; +using Microsoft.Integration.Dataverse; + +pageextension 6628 "FS Service Lines" extends "Service Lines" +{ + layout + { + addlast(Control1) + { + field("Coupled to FS"; Rec."Coupled to FS") + { + ApplicationArea = All; + ToolTip = 'Specifies if the entity is coupled to an entity in Field Service.'; + Visible = FSIntegrationEnabled; + } + } + } + + var + FSIntegrationEnabled: Boolean; + CRMIntegrationEnabled: Boolean; + + trigger OnOpenPage() + var + FSConnectionSetup: Record "FS Connection Setup"; + CRMIntegrationManagement: Codeunit "CRM Integration Management"; + begin + CRMIntegrationEnabled := CRMIntegrationManagement.IsCRMIntegrationEnabled(); + if CRMIntegrationEnabled then + FSIntegrationEnabled := FSConnectionSetup.IsEnabled(); + end; +} diff --git a/Apps/W1/FieldServiceIntegration/app/src/Page Extensions/FSServiceOrder.PageExt.al b/Apps/W1/FieldServiceIntegration/app/src/Page Extensions/FSServiceOrder.PageExt.al new file mode 100644 index 0000000000..eb701cec29 --- /dev/null +++ b/Apps/W1/FieldServiceIntegration/app/src/Page Extensions/FSServiceOrder.PageExt.al @@ -0,0 +1,231 @@ +// ------------------------------------------------------------------------------------------------ +// 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.DynamicsFieldService; + +using Microsoft.Service.Document; +using Microsoft.Integration.Dataverse; + +pageextension 6614 "FS Service Order" extends "Service Order" +{ + layout + { + modify("Service Order Type") + { + ShowMandatory = FSIntegrationTypeServiceEnabled; + } + addafter(Status) + { + group("Work Description") + { + Caption = 'Work Description'; + field(WorkDescription; WorkDescription) + { + ApplicationArea = Service; + Caption = 'Work Description'; + ShowCaption = false; + ToolTip = 'Specifies the products or service being offered.'; + MultiLine = true; + Importance = Additional; + + trigger OnValidate() + begin + Rec.SetWorkDescription(WorkDescription); + end; + } + } + } + } + + actions + { + addlast(navigation) + { + group(ActionFS) + { + Caption = 'Dynamics 365 Field Service'; + Enabled = FSIntegrationEnabled; + + action(GoToProductFS) + { + ApplicationArea = Suite; + Caption = 'Work Order'; + Image = ViewServiceOrder; + ToolTip = 'Open the coupled Dynamics 365 Field Service record.'; + + trigger OnAction() + var + CRMIntegrationManagement: Codeunit "CRM Integration Management"; + begin + CRMIntegrationManagement.ShowCRMEntityFromRecordID(Rec.RecordId); + end; + } + action(SynchronizeNowFS) + { + AccessByPermission = TableData "CRM Integration Record" = IM; + ApplicationArea = Suite; + Caption = 'Synchronize'; + Image = Refresh; + ToolTip = 'Send updated data to Dynamics 365 Field Service.'; + + trigger OnAction() + var + ServiceHeader: Record "Service Header"; + CRMIntegrationManagement: Codeunit "CRM Integration Management"; + ServiceHeaderRecordRef: RecordRef; + begin + CurrPage.SetSelectionFilter(ServiceHeader); + ServiceHeader.Next(); + + if ServiceHeader.Count = 1 then + CRMIntegrationManagement.UpdateOneNow(ServiceHeader.RecordId) + else begin + ServiceHeaderRecordRef.GetTable(ServiceHeader); + CRMIntegrationManagement.UpdateMultipleNow(ServiceHeaderRecordRef); + end + end; + } + group(CouplingFS) + { + Caption = 'Coupling', Comment = 'Coupling is a noun'; + Image = LinkAccount; + ToolTip = 'Create, change, or delete a coupling between the Business Central record and a Dynamics 365 Field Service record.'; + action(ManageCouplingFS) + { + AccessByPermission = TableData "CRM Integration Record" = IM; + ApplicationArea = Suite; + Caption = 'Set Up Coupling'; + Image = LinkAccount; + ToolTip = 'Create or modify the coupling to a Dynamics 365 Field Service record.'; + + trigger OnAction() + var + CRMIntegrationManagement: Codeunit "CRM Integration Management"; + begin + CRMIntegrationManagement.DefineCoupling(Rec.RecordId); + end; + } + action(FSMatchBasedCoupling) + { + AccessByPermission = TableData "CRM Integration Record" = IM; + ApplicationArea = Suite; + Caption = 'Match-Based Coupling'; + Image = CoupledItem; + ToolTip = 'Couple service orders to work orders in Dynamics 365 Field Service record based on matching criteria.'; + + trigger OnAction() + var + ServiceHeader: Record "Service Header"; + CRMIntegrationManagement: Codeunit "CRM Integration Management"; + RecRef: RecordRef; + begin + CurrPage.SetSelectionFilter(ServiceHeader); + RecRef.GetTable(ServiceHeader); + CRMIntegrationManagement.MatchBasedCoupling(RecRef); + end; + } + action(DeleteCouplingFS) + { + AccessByPermission = TableData "CRM Integration Record" = D; + ApplicationArea = Suite; + Caption = 'Delete Coupling'; + Enabled = CRMIsCoupledToRecord; + Image = UnLinkAccount; + ToolTip = 'Delete the coupling to a Dynamics 365 Field Service record.'; + + trigger OnAction() + var + ServiceHeader: Record "Service Header"; + CRMCouplingManagement: Codeunit "CRM Coupling Management"; + ServiceHeaderRecordRef: RecordRef; + begin + CurrPage.SetSelectionFilter(ServiceHeader); + ServiceHeaderRecordRef.GetTable(ServiceHeader); + CRMCouplingManagement.RemoveCoupling(ServiceHeaderRecordRef); + end; + } + } + action(FSShowLog) + { + ApplicationArea = Suite; + Caption = 'Synchronization Log'; + Image = Log; + ToolTip = 'View integration synchronization jobs for the resource table.'; + + trigger OnAction() + var + CRMIntegrationManagement: Codeunit "CRM Integration Management"; + begin + CRMIntegrationManagement.ShowLog(Rec.RecordId); + end; + } + } + } + addlast(Promoted) + { + group(Category_FS_Synchronize) + { + Caption = 'Synchronize'; + Visible = FSIntegrationEnabled; + + group(Category_FS_Coupling) + { + Caption = 'Coupling'; + ShowAs = SplitButton; + + actionref(ManageCouplingFS_Promoted; ManageCouplingFS) + { + } + actionref(DeleteCouplingFS_Promoted; DeleteCouplingFS) + { + } + actionref(FSMatchBasedCoupling_Promoted; FSMatchBasedCoupling) + { + } + } + actionref(SynchronizeNowFS_Promoted; SynchronizeNowFS) + { + } + actionref(GoToProductFS_Promoted; GoToProductFS) + { + } + actionref(FSShowLog_Promoted; FSShowLog) + { + } + } + } + } + + var + WorkDescription: Text; + FSIntegrationEnabled: Boolean; + FSIntegrationTypeServiceEnabled: Boolean; + CRMIsCoupledToRecord: Boolean; + CRMIntegrationEnabled: Boolean; + + trigger OnAfterGetRecord() + begin + WorkDescription := Rec.GetWorkDescription(); + end; + + trigger OnAfterGetCurrRecord() + var + CRMCouplingManagement: Codeunit "CRM Coupling Management"; + begin + if FSIntegrationEnabled then + CRMIsCoupledToRecord := CRMCouplingManagement.IsRecordCoupledToCRM(Rec.RecordId); + end; + + trigger OnOpenPage() + var + FSConnectionSetup: Record "FS Connection Setup"; + CRMIntegrationManagement: Codeunit "CRM Integration Management"; + begin + CRMIntegrationEnabled := CRMIntegrationManagement.IsCRMIntegrationEnabled(); + if CRMIntegrationEnabled then begin + FSIntegrationEnabled := FSConnectionSetup.IsEnabled(); + FSIntegrationTypeServiceEnabled := FSConnectionSetup.IsIntegrationTypeServiceEnabled(); + end; + end; +} diff --git a/Apps/W1/FieldServiceIntegration/app/src/Page Extensions/FSServiceOrderSubform.PageExt.al b/Apps/W1/FieldServiceIntegration/app/src/Page Extensions/FSServiceOrderSubform.PageExt.al new file mode 100644 index 0000000000..7165f0db58 --- /dev/null +++ b/Apps/W1/FieldServiceIntegration/app/src/Page Extensions/FSServiceOrderSubform.PageExt.al @@ -0,0 +1,38 @@ +// ------------------------------------------------------------------------------------------------ +// 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.DynamicsFieldService; + +using Microsoft.Service.Document; +using Microsoft.Integration.Dataverse; + +pageextension 6627 "FS Service Order Subform" extends "Service Order Subform" +{ + layout + { + addlast(Control1) + { + field("Coupled to FS"; Rec."Coupled to FS") + { + ApplicationArea = All; + ToolTip = 'Specifies if the entity is coupled to an entity in Field Service.'; + Visible = FSIntegrationEnabled; + } + } + } + + var + FSIntegrationEnabled: Boolean; + CRMIntegrationEnabled: Boolean; + + trigger OnOpenPage() + var + FSConnectionSetup: Record "FS Connection Setup"; + CRMIntegrationManagement: Codeunit "CRM Integration Management"; + begin + CRMIntegrationEnabled := CRMIntegrationManagement.IsCRMIntegrationEnabled(); + if CRMIntegrationEnabled then + FSIntegrationEnabled := FSConnectionSetup.IsEnabled(); + end; +} diff --git a/Apps/W1/FieldServiceIntegration/app/src/Page Extensions/FSServiceOrderTypes.PageExt.al b/Apps/W1/FieldServiceIntegration/app/src/Page Extensions/FSServiceOrderTypes.PageExt.al new file mode 100644 index 0000000000..50d73f7d00 --- /dev/null +++ b/Apps/W1/FieldServiceIntegration/app/src/Page Extensions/FSServiceOrderTypes.PageExt.al @@ -0,0 +1,206 @@ +// ------------------------------------------------------------------------------------------------ +// 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.DynamicsFieldService; + +using Microsoft.Service.Setup; +using Microsoft.Integration.Dataverse; + +pageextension 6625 "FS Service Order Types" extends "Service Order Types" +{ + layout + { + addlast(Control1) + { + field("Coupled to FS"; Rec."Coupled to FS") + { + ApplicationArea = All; + ToolTip = 'Specifies if the entity is coupled to an entity in Field Service.'; + Visible = FSIntegrationEnabled; + } + } + } + + actions + { + addlast(navigation) + { + group(ActionFS) + { + Caption = 'Dynamics 365 Field Service'; + Enabled = FSIntegrationEnabled; + + action(GoToProductFS) + { + ApplicationArea = Suite; + Caption = 'Work Order Type'; + Image = ViewServiceOrder; + ToolTip = 'Open the coupled Dynamics 365 Field Service record.'; + + trigger OnAction() + var + CRMIntegrationManagement: Codeunit "CRM Integration Management"; + begin + CRMIntegrationManagement.ShowCRMEntityFromRecordID(Rec.RecordId); + end; + } + action(SynchronizeNowFS) + { + AccessByPermission = TableData "CRM Integration Record" = IM; + ApplicationArea = Suite; + Caption = 'Synchronize'; + Image = Refresh; + ToolTip = 'Send updated data to Dynamics 365 Field Service.'; + + trigger OnAction() + var + ServiceOrderType: Record "Service Order Type"; + CRMIntegrationManagement: Codeunit "CRM Integration Management"; + ServiceHeaderRecordRef: RecordRef; + begin + CurrPage.SetSelectionFilter(ServiceOrderType); + ServiceOrderType.Next(); + + if ServiceOrderType.Count = 1 then + CRMIntegrationManagement.UpdateOneNow(ServiceOrderType.RecordId) + else begin + ServiceHeaderRecordRef.GetTable(ServiceOrderType); + CRMIntegrationManagement.UpdateMultipleNow(ServiceHeaderRecordRef); + end + end; + } + group(CouplingFS) + { + Caption = 'Coupling', Comment = 'Coupling is a noun'; + Image = LinkAccount; + ToolTip = 'Create, change, or delete a coupling between the Business Central record and a Dynamics 365 Field Service record.'; + action(ManageCouplingFS) + { + AccessByPermission = TableData "CRM Integration Record" = IM; + ApplicationArea = Suite; + Caption = 'Set Up Coupling'; + Image = LinkAccount; + ToolTip = 'Create or modify the coupling to a Dynamics 365 Field Service record.'; + + trigger OnAction() + var + CRMIntegrationManagement: Codeunit "CRM Integration Management"; + begin + CRMIntegrationManagement.DefineCoupling(Rec.RecordId); + end; + } + action(FSMatchBasedCoupling) + { + AccessByPermission = TableData "CRM Integration Record" = IM; + ApplicationArea = Suite; + Caption = 'Match-Based Coupling'; + Image = CoupledItem; + ToolTip = 'Couple service order types to work order types in Dynamics 365 Field Service record based on matching criteria.'; + + trigger OnAction() + var + ServiceOrderType: Record "Service Order Type"; + CRMIntegrationManagement: Codeunit "CRM Integration Management"; + RecRef: RecordRef; + begin + CurrPage.SetSelectionFilter(ServiceOrderType); + RecRef.GetTable(ServiceOrderType); + CRMIntegrationManagement.MatchBasedCoupling(RecRef); + end; + } + action(DeleteCouplingFS) + { + AccessByPermission = TableData "CRM Integration Record" = D; + ApplicationArea = Suite; + Caption = 'Delete Coupling'; + Enabled = CRMIsCoupledToRecord; + Image = UnLinkAccount; + ToolTip = 'Delete the coupling to a Dynamics 365 Field Service record.'; + + trigger OnAction() + var + ServiceOrderType: Record "Service Order Type"; + CRMCouplingManagement: Codeunit "CRM Coupling Management"; + ServiceHeaderRecordRef: RecordRef; + begin + CurrPage.SetSelectionFilter(ServiceOrderType); + ServiceHeaderRecordRef.GetTable(ServiceOrderType); + CRMCouplingManagement.RemoveCoupling(ServiceHeaderRecordRef); + end; + } + } + action(FSShowLog) + { + ApplicationArea = Suite; + Caption = 'Synchronization Log'; + Image = Log; + ToolTip = 'View integration synchronization jobs for the resource table.'; + + trigger OnAction() + var + CRMIntegrationManagement: Codeunit "CRM Integration Management"; + begin + CRMIntegrationManagement.ShowLog(Rec.RecordId); + end; + } + } + } + addlast(Promoted) + { + group(Category_FS_Synchronize) + { + Caption = 'Synchronize'; + Visible = FSIntegrationEnabled; + + group(Category_FS_Coupling) + { + Caption = 'Coupling'; + ShowAs = SplitButton; + + actionref(ManageCouplingFS_Promoted; ManageCouplingFS) + { + } + actionref(DeleteCouplingFS_Promoted; DeleteCouplingFS) + { + } + actionref(FSMatchBasedCoupling_Promoted; FSMatchBasedCoupling) + { + } + } + actionref(SynchronizeNowFS_Promoted; SynchronizeNowFS) + { + } + actionref(GoToProductFS_Promoted; GoToProductFS) + { + } + actionref(FSShowLog_Promoted; FSShowLog) + { + } + } + } + } + + var + FSIntegrationEnabled: Boolean; + CRMIsCoupledToRecord: Boolean; + CRMIntegrationEnabled: Boolean; + + trigger OnAfterGetCurrRecord() + var + CRMCouplingManagement: Codeunit "CRM Coupling Management"; + begin + if FSIntegrationEnabled then + CRMIsCoupledToRecord := CRMCouplingManagement.IsRecordCoupledToCRM(Rec.RecordId); + end; + + trigger OnOpenPage() + var + FSConnectionSetup: Record "FS Connection Setup"; + CRMIntegrationManagement: Codeunit "CRM Integration Management"; + begin + CRMIntegrationEnabled := CRMIntegrationManagement.IsCRMIntegrationEnabled(); + if CRMIntegrationEnabled then + FSIntegrationEnabled := FSConnectionSetup.IsEnabled(); + end; +} diff --git a/Apps/W1/FieldServiceIntegration/app/src/Page Extensions/FSServiceOrders.PageExt.al b/Apps/W1/FieldServiceIntegration/app/src/Page Extensions/FSServiceOrders.PageExt.al new file mode 100644 index 0000000000..75d6df5e08 --- /dev/null +++ b/Apps/W1/FieldServiceIntegration/app/src/Page Extensions/FSServiceOrders.PageExt.al @@ -0,0 +1,206 @@ +// ------------------------------------------------------------------------------------------------ +// 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.DynamicsFieldService; + +using Microsoft.Service.Document; +using Microsoft.Integration.Dataverse; + +pageextension 6624 "FS Service Orders" extends "Service Orders" +{ + layout + { + addlast(Control1) + { + field("Coupled to FS"; Rec."Coupled to FS") + { + ApplicationArea = All; + ToolTip = 'Specifies if the entity is coupled to an entity in Field Service.'; + Visible = FSIntegrationEnabled; + } + } + } + + actions + { + addlast(navigation) + { + group(ActionFS) + { + Caption = 'Dynamics 365 Field Service'; + Enabled = FSIntegrationEnabled; + + action(GoToProductFS) + { + ApplicationArea = Suite; + Caption = 'Work Order'; + Image = ViewServiceOrder; + ToolTip = 'Open the coupled Dynamics 365 Field Service record.'; + + trigger OnAction() + var + CRMIntegrationManagement: Codeunit "CRM Integration Management"; + begin + CRMIntegrationManagement.ShowCRMEntityFromRecordID(Rec.RecordId); + end; + } + action(SynchronizeNowFS) + { + AccessByPermission = TableData "CRM Integration Record" = IM; + ApplicationArea = Suite; + Caption = 'Synchronize'; + Image = Refresh; + ToolTip = 'Send updated data to Dynamics 365 Field Service.'; + + trigger OnAction() + var + ServiceHeader: Record "Service Header"; + CRMIntegrationManagement: Codeunit "CRM Integration Management"; + ServiceHeaderRecordRef: RecordRef; + begin + CurrPage.SetSelectionFilter(ServiceHeader); + ServiceHeader.Next(); + + if ServiceHeader.Count = 1 then + CRMIntegrationManagement.UpdateOneNow(ServiceHeader.RecordId) + else begin + ServiceHeaderRecordRef.GetTable(ServiceHeader); + CRMIntegrationManagement.UpdateMultipleNow(ServiceHeaderRecordRef); + end + end; + } + group(CouplingFS) + { + Caption = 'Coupling', Comment = 'Coupling is a noun'; + Image = LinkAccount; + ToolTip = 'Create, change, or delete a coupling between the Business Central record and a Dynamics 365 Field Service record.'; + action(ManageCouplingFS) + { + AccessByPermission = TableData "CRM Integration Record" = IM; + ApplicationArea = Suite; + Caption = 'Set Up Coupling'; + Image = LinkAccount; + ToolTip = 'Create or modify the coupling to a Dynamics 365 Field Service record.'; + + trigger OnAction() + var + CRMIntegrationManagement: Codeunit "CRM Integration Management"; + begin + CRMIntegrationManagement.DefineCoupling(Rec.RecordId); + end; + } + action(FSMatchBasedCoupling) + { + AccessByPermission = TableData "CRM Integration Record" = IM; + ApplicationArea = Suite; + Caption = 'Match-Based Coupling'; + Image = CoupledItem; + ToolTip = 'Couple service orders to work orders in Dynamics 365 Field Service record based on matching criteria.'; + + trigger OnAction() + var + ServiceHeader: Record "Service Header"; + CRMIntegrationManagement: Codeunit "CRM Integration Management"; + RecRef: RecordRef; + begin + CurrPage.SetSelectionFilter(ServiceHeader); + RecRef.GetTable(ServiceHeader); + CRMIntegrationManagement.MatchBasedCoupling(RecRef); + end; + } + action(DeleteCouplingFS) + { + AccessByPermission = TableData "CRM Integration Record" = D; + ApplicationArea = Suite; + Caption = 'Delete Coupling'; + Enabled = CRMIsCoupledToRecord; + Image = UnLinkAccount; + ToolTip = 'Delete the coupling to a Dynamics 365 Field Service record.'; + + trigger OnAction() + var + ServiceHeader: Record "Service Header"; + CRMCouplingManagement: Codeunit "CRM Coupling Management"; + ServiceHeaderRecordRef: RecordRef; + begin + CurrPage.SetSelectionFilter(ServiceHeader); + ServiceHeaderRecordRef.GetTable(ServiceHeader); + CRMCouplingManagement.RemoveCoupling(ServiceHeaderRecordRef); + end; + } + } + action(FSShowLog) + { + ApplicationArea = Suite; + Caption = 'Synchronization Log'; + Image = Log; + ToolTip = 'View integration synchronization jobs for the resource table.'; + + trigger OnAction() + var + CRMIntegrationManagement: Codeunit "CRM Integration Management"; + begin + CRMIntegrationManagement.ShowLog(Rec.RecordId); + end; + } + } + } + addlast(Promoted) + { + group(Category_FS_Synchronize) + { + Caption = 'Synchronize'; + Visible = FSIntegrationEnabled; + + group(Category_FS_Coupling) + { + Caption = 'Coupling'; + ShowAs = SplitButton; + + actionref(ManageCouplingFS_Promoted; ManageCouplingFS) + { + } + actionref(DeleteCouplingFS_Promoted; DeleteCouplingFS) + { + } + actionref(FSMatchBasedCoupling_Promoted; FSMatchBasedCoupling) + { + } + } + actionref(SynchronizeNowFS_Promoted; SynchronizeNowFS) + { + } + actionref(GoToProductFS_Promoted; GoToProductFS) + { + } + actionref(FSShowLog_Promoted; FSShowLog) + { + } + } + } + } + + var + FSIntegrationEnabled: Boolean; + CRMIsCoupledToRecord: Boolean; + CRMIntegrationEnabled: Boolean; + + trigger OnAfterGetCurrRecord() + var + CRMCouplingManagement: Codeunit "CRM Coupling Management"; + begin + if FSIntegrationEnabled then + CRMIsCoupledToRecord := CRMCouplingManagement.IsRecordCoupledToCRM(Rec.RecordId); + end; + + trigger OnOpenPage() + var + FSConnectionSetup: Record "FS Connection Setup"; + CRMIntegrationManagement: Codeunit "CRM Integration Management"; + begin + CRMIntegrationEnabled := CRMIntegrationManagement.IsCRMIntegrationEnabled(); + if CRMIntegrationEnabled then + FSIntegrationEnabled := FSConnectionSetup.IsEnabled(); + end; +} diff --git a/Apps/W1/FieldServiceIntegration/app/src/Pages/FSConnectionSetup.Page.al b/Apps/W1/FieldServiceIntegration/app/src/Pages/FSConnectionSetup.Page.al index bb88b441a1..cf86da7f01 100644 --- a/Apps/W1/FieldServiceIntegration/app/src/Pages/FSConnectionSetup.Page.al +++ b/Apps/W1/FieldServiceIntegration/app/src/Pages/FSConnectionSetup.Page.al @@ -115,12 +115,14 @@ page 6612 "FS Connection Setup" ApplicationArea = Suite; ShowMandatory = true; ToolTip = 'Specifies the project journal template in which project journal lines will be created and coupled to work order products and work order services.'; + Editable = EditableProjectSettings; } field("Job Journal Batch"; Rec."Job Journal Batch") { ApplicationArea = Suite; ShowMandatory = true; ToolTip = 'Specifies the project journal batch in which project journal lines will be created and coupled to work order products and work order services.'; + Editable = EditableProjectSettings; } field("Hour Unit of Measure"; Rec."Hour Unit of Measure") { @@ -128,6 +130,22 @@ page 6612 "FS Connection Setup" ShowMandatory = true; ToolTip = 'Specifies the unit of measure that corresponds to the ''hour'' unit that is used on Dynamics 365 Field Service bookable resources.'; } + group(IntegrationTypeService) + { + ShowCaption = false; + + field("Integration Type"; Rec."Integration Type") + { + ApplicationArea = Service; + Editable = not Rec."Is Enabled"; + ToolTip = 'Specifies the type of integration between Business Central and Dynamics 365 Field Service.'; + + trigger OnValidate() + begin + UpdateIntegrationTypeEditable(); + end; + } + } field("Enable Invt. Availability"; Rec."Enable Invt. Availability") { ApplicationArea = Suite; @@ -143,6 +161,7 @@ page 6612 "FS Connection Setup" { ApplicationArea = Suite; ToolTip = 'Specifies when to synchronize work order products and work order services.'; + Editable = EditableProjectSettings; trigger OnValidate() var @@ -173,6 +192,7 @@ page 6612 "FS Connection Setup" { ApplicationArea = Suite; ToolTip = 'Specifies when to post project journal lines that are coupled to work order products and work order services.'; + Editable = EditableProjectSettings; } } } @@ -451,6 +471,7 @@ page 6612 "FS Connection Setup" IsEditable: Boolean; IsCdsIntegrationEnabled: Boolean; CRMVersionStatus: Boolean; + EditableProjectSettings: Boolean; VirtualTableAppInstalled: Boolean; local procedure RefreshData() @@ -459,6 +480,7 @@ page 6612 "FS Connection Setup" RefreshSynchJobsData(); UpdateEnableFlags(); SetStyleExpr(); + UpdateIntegrationTypeEditable(); end; local procedure RefreshSynchJobsData() @@ -516,5 +538,10 @@ page 6612 "FS Connection Setup" begin Rec.Validate("Proxy Version", CRMIntegrationManagement.GetLastProxyVersionItem()); end; + + local procedure UpdateIntegrationTypeEditable() + begin + EditableProjectSettings := Rec."Integration Type" in [Rec."Integration Type"::Project, Rec."Integration Type"::Both]; + end; } diff --git a/Apps/W1/FieldServiceIntegration/app/src/Pages/FSConnectionSetupWizard.Page.al b/Apps/W1/FieldServiceIntegration/app/src/Pages/FSConnectionSetupWizard.Page.al index 251d2b283f..14e092faec 100644 --- a/Apps/W1/FieldServiceIntegration/app/src/Pages/FSConnectionSetupWizard.Page.al +++ b/Apps/W1/FieldServiceIntegration/app/src/Pages/FSConnectionSetupWizard.Page.al @@ -134,12 +134,14 @@ page 6613 "FS Connection Setup Wizard" ApplicationArea = Suite; ShowMandatory = true; ToolTip = 'Specifies the project journal template in which project journal lines will be created and coupled to work order products and work order services.'; + Editable = EditableProjectSettings; } field("Job Journal Batch"; Rec."Job Journal Batch") { ApplicationArea = Suite; ShowMandatory = true; ToolTip = 'Specifies the project journal batch in which project journal lines will be created and coupled to work order products and work order services.'; + Editable = EditableProjectSettings; } field("Hour Unit of Measure"; Rec."Hour Unit of Measure") { @@ -151,11 +153,23 @@ page 6613 "FS Connection Setup Wizard" { ApplicationArea = Suite; ToolTip = 'Specifies when to synchronize work order products and work order services.'; + Editable = EditableProjectSettings; } field("Line Post Rule"; Rec."Line Post Rule") { ApplicationArea = Suite; ToolTip = 'Specifies when to post project journal lines that are coupled to work order products and work order services.'; + Editable = EditableProjectSettings; + } + field("Integration Type"; Rec."Integration Type") + { + ApplicationArea = Service; + ToolTip = 'Specifies the type of integration between Business Central and Dynamics 365 Field Service.'; + + trigger OnValidate() + begin + UpdateIntegrationTypeEditable(); + end; } } group("Advanced Settings") @@ -460,6 +474,7 @@ page 6613 "FS Connection Setup Wizard" VirtualTableAppInstalled: Boolean; [NonDebuggable] Password: Text; + EditableProjectSettings: Boolean; ConnectionNotSetUpQst: Label 'The %1 connection has not been set up.\\Are you sure you want to exit?', Comment = '%1 = CRM product name'; CRMURLShouldNotBeEmptyErr: Label 'You must specify the URL of your %1 solution.', Comment = '%1 = CRM product name'; CRMSynchUserCredentialsNeededErr: Label 'You must specify the credentials for the user account for synchronization with %1.', Comment = '%1 = CRM product name'; @@ -555,18 +570,22 @@ page 6613 "FS Connection Setup Wizard" EnableFSConnectionEnabled := Rec."Server Address" <> ''; Rec."Authentication Type" := Rec."Authentication Type"::Office365; + if FSConnectionSetup.Get() then begin EnableFSConnection := true; EnableFSConnectionEnabled := not FSConnectionSetup."Is Enabled"; ImportSolution := true; if FSConnectionSetup."Is FS Solution Installed" then ImportFSSolutionEnabled := false; + Rec."Integration Type" := FSConnectionSetup."Integration Type"; end else begin if ImportFSSolutionEnabled then ImportSolution := true; if EnableFSConnectionEnabled then EnableFSConnection := true; end; + + UpdateIntegrationTypeEditable(); end; local procedure ShowItemAvailabilityStep() @@ -641,6 +660,11 @@ page 6613 "FS Connection Setup Wizard" Rec.InitializeDefaultTemplateAndBatch(); end; + local procedure UpdateIntegrationTypeEditable() + begin + EditableProjectSettings := Rec."Integration Type" in [Rec."Integration Type"::Project, Rec."Integration Type"::Both]; + end; + local procedure GetVirtualTablesAppSourceLink(): Text var UserSettingsRecord: Record "User Settings"; diff --git a/Apps/W1/FieldServiceIntegration/app/src/Pages/FSPostedServInvLinesAPI.Page.al b/Apps/W1/FieldServiceIntegration/app/src/Pages/FSPostedServInvLinesAPI.Page.al new file mode 100644 index 0000000000..8546a8b3a2 --- /dev/null +++ b/Apps/W1/FieldServiceIntegration/app/src/Pages/FSPostedServInvLinesAPI.Page.al @@ -0,0 +1,180 @@ +// ------------------------------------------------------------------------------------------------ +// 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.DynamicsFieldService; + +using Microsoft.Finance.GeneralLedger.Account; +using Microsoft.Inventory.Item; +using Microsoft.Inventory.Location; +using Microsoft.Foundation.UOM; +using Microsoft.Service.History; +using Microsoft.API.V2; + +page 6617 "FS Posted Serv. Inv. Lines API" +{ + APIVersion = 'v2.0'; + EntityCaption = 'Posted Service Invoice Line'; + EntitySetCaption = 'Posted Service Invoice Lines'; + PageType = API; + ODataKeyFields = SystemId; + EntityName = 'postedServiceInvoiceLine'; + EntitySetName = 'postedServiceInvoiceLines'; + SourceTable = "Service Invoice Line"; + InsertAllowed = false; + ModifyAllowed = false; + DeleteAllowed = false; + Extensible = false; + + layout + { + area(content) + { + repeater(Group) + { + field(id; Rec.SystemId) + { + Caption = 'Id'; + Editable = false; + } + field(documentId; ServiceInvoiceHeader.SystemId) + { + Caption = 'Document Id'; + } + field(sequence; Rec."Line No.") + { + Caption = 'Sequence'; + } + field(itemId; Item.SystemId) + { + Caption = 'Item Id'; + } + field(accountId; GLAccount.SystemId) + { + Caption = 'Account Id'; + } + field(lineType; Rec.Type) + { + Caption = 'Line Type'; + } + field(lineObjectNumber; Rec."No.") + { + Caption = 'Line Object No.'; + } + field(description; Rec.Description) + { + Caption = 'Description'; + } + field(description2; Rec."Description 2") + { + Caption = 'Description 2'; + } + field(unitOfMeasureId; UnitOfMeasure.SystemId) + { + Caption = 'Unit Of Measure Id'; + } + field(unitOfMeasureCode; Rec."Unit of Measure Code") + { + Caption = 'Unit Of Measure Code'; + } + field(quantity; Rec.Quantity) + { + Caption = 'Quantity'; + } + field(unitPrice; Rec."Unit Price") + { + Caption = 'Unit Price'; + } + field(discountAmount; Rec."Line Discount Amount") + { + Caption = 'Discount Amount'; + } + field(discountPercent; Rec."Line Discount %") + { + Caption = 'Discount Percent'; + } + field(discountAppliedBeforeTax; Rec."Line Discount Amount") + { + Caption = 'Discount Applied Before Tax'; + Editable = false; + } + field(amountExcludingTax; Rec."Line Amount") + { + Caption = 'Amount Excluding Tax'; + Editable = false; + } + field(taxCode; Rec."VAT %") + { + Caption = 'Tax Code'; + } + field(taxPercent; Rec."VAT %") + { + Caption = 'Tax Percent'; + Editable = false; + } + field(amountIncludingTax; Rec."Amount Including VAT") + { + Caption = 'Amount Including Tax'; + Editable = false; + } + field(netAmount; Rec.Amount) + { + Caption = 'Net Amount'; + Editable = false; + } + field(netAmountIncludingTax; Rec."Amount Including VAT") + { + Caption = 'Net Amount Including Tax'; + Editable = false; + } + field(itemVariantId; ItemVariant.SystemId) + { + Caption = 'Item Variant Id'; + } + field(locationId; Location.SystemId) + { + Caption = 'Location Id'; + } + part(dimensionSetLines; "APIV2 - Dimension Set Lines") + { + Caption = 'Dimension Set Lines'; + EntityName = 'dimensionSetLine'; + EntitySetName = 'dimensionSetLines'; + SubPageLink = "Parent Id" = field(SystemId); + } + } + } + } + + trigger OnAfterGetRecord() + begin + if not ServiceInvoiceHeader.Get(Rec."Document No.") then + clear(ServiceInvoiceHeader); + + Clear(Item); + Clear(GLAccount); + case Rec.Type of + Rec.Type::Item: + if not Item.Get(Rec."No.") then + Clear(Item); + Rec.Type::"G/L Account": + if not GLAccount.Get(Rec."No.") then + Clear(GLAccount); + end; + + if not UnitOfMeasure.Get(Rec."Unit of Measure Code") then + Clear(UnitOfMeasure); + if not Location.Get(Rec."Location Code") then + Clear(Location); + if not ItemVariant.Get(Item."No.", Rec."Variant Code") then + Clear(ItemVariant); + end; + + var + ServiceInvoiceHeader: Record "Service Invoice Header"; + Item: Record Item; + GLAccount: Record "G/L Account"; + UnitOfMeasure: Record "Unit of Measure"; + ItemVariant: Record "Item Variant"; + Location: Record Location; +} \ No newline at end of file diff --git a/Apps/W1/FieldServiceIntegration/app/src/Pages/FSPostedServiceInvoiceAPI.Page.al b/Apps/W1/FieldServiceIntegration/app/src/Pages/FSPostedServiceInvoiceAPI.Page.al new file mode 100644 index 0000000000..39cf297207 --- /dev/null +++ b/Apps/W1/FieldServiceIntegration/app/src/Pages/FSPostedServiceInvoiceAPI.Page.al @@ -0,0 +1,337 @@ +// ------------------------------------------------------------------------------------------------ +// 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.DynamicsFieldService; + +using Microsoft.Sales.Customer; +using Microsoft.Finance.Currency; +using Microsoft.Service.History; +using Microsoft.Foundation.PaymentTerms; +using Microsoft.Foundation.Shipping; +using Microsoft.Integration.Graph; +using Microsoft.Service.Document; +using Microsoft.API.V2; +using Microsoft.Integration.Entity; +using Microsoft.Sales.Receivables; + +page 6618 "FS Posted Service Invoice API" +{ + APIVersion = 'v2.0'; + EntityCaption = 'Posted Service Invoice'; + EntitySetCaption = 'Posted Service Invoices'; + EntityName = 'postedServiceInvoice'; + EntitySetName = 'postedServiceInvoices'; + ODataKeyFields = SystemId; + PageType = API; + SourceTable = "Service Invoice Header"; + InsertAllowed = false; + ModifyAllowed = false; + DeleteAllowed = false; + Extensible = false; + + layout + { + area(content) + { + repeater(Group) + { + field(id; Rec.SystemId) + { + Caption = 'Id'; + } + field(number; Rec."No.") + { + Caption = 'No.'; + } + field(externalDocumentNumber; Rec."External Document No.") + { + Caption = 'External Document No.'; + } + field(invoiceDate; Rec."Document Date") + { + Caption = 'Invoice Date'; + } + field(postingDate; Rec."Posting Date") + { + Caption = 'Posting Date'; + } + field(dueDate; Rec."Due Date") + { + Caption = 'Due Date'; + } + field(customerPurchaseOrderReference; Rec."Your Reference") + { + Caption = 'Customer Purchase Order Reference',; + } + field(customerId; SellToCustomer.SystemId) + { + Caption = 'Customer Id'; + } + field(customerNumber; Rec."Customer No.") + { + Caption = 'Customer No.'; + } + field(customerName; Rec.Name) + { + Caption = 'Customer Name'; + Editable = false; + } + field(billToName; Rec."Bill-to Name") + { + Caption = 'Bill-To Name'; + Editable = false; + } + field(billToCustomerId; BillToCustomer.SystemId) + { + Caption = 'Bill-To Customer Id'; + } + field(billToCustomerNumber; Rec."Bill-to Customer No.") + { + Caption = 'Bill-To Customer No.'; + } + field(shipToName; Rec."Ship-to Name") + { + Caption = 'Ship-to Name'; + } + field(shipToContact; Rec."Ship-to Contact") + { + Caption = 'Ship-to Contact'; + } + field(sellToAddressLine1; Rec.Address) + { + Caption = 'Sell-to Address Line 1'; + } + field(sellToAddressLine2; Rec."Address 2") + { + Caption = 'Sell-to Address Line 2'; + } + field(sellToCity; Rec.City) + { + Caption = 'Sell-to City'; + } + field(sellToCountry; Rec."Country/Region Code") + { + Caption = 'Sell-to Country/Region Code'; + } + field(sellToState; Rec.County) + { + Caption = 'Sell-to State'; + } + field(sellToPostCode; Rec."Post Code") + { + Caption = 'Sell-to Post Code'; + } + field(billToAddressLine1; Rec."Bill-To Address") + { + Caption = 'Bill-to Address Line 1'; + Editable = false; + } + field(billToAddressLine2; Rec."Bill-To Address 2") + { + Caption = 'Bill-to Address Line 2'; + Editable = false; + } + field(billToCity; Rec."Bill-To City") + { + Caption = 'Bill-to City'; + Editable = false; + } + field(billToCountry; Rec."Bill-To Country/Region Code") + { + Caption = 'Bill-to Country/Region Code'; + Editable = false; + } + field(billToState; Rec."Bill-To County") + { + Caption = 'Bill-to State'; + Editable = false; + } + field(billToPostCode; Rec."Bill-To Post Code") + { + Caption = 'Bill-to Post Code'; + Editable = false; + } + field(shipToAddressLine1; Rec."Ship-to Address") + { + Caption = 'Ship-to Address Line 1'; + } + field(shipToAddressLine2; Rec."Ship-to Address 2") + { + Caption = 'Ship-to Address Line 2'; + } + field(shipToCity; Rec."Ship-to City") + { + Caption = 'Ship-to City'; + } + field(shipToCountry; Rec."Ship-to Country/Region Code") + { + Caption = 'Ship-to Country/Region Code'; + } + field(shipToState; Rec."Ship-to County") + { + Caption = 'Ship-to State'; + } + field(shipToPostCode; Rec."Ship-to Post Code") + { + Caption = 'Ship-to Post Code'; + } + field(currencyId; Currency.SystemId) + { + Caption = 'Currency Id'; + } + field(shortcutDimension1Code; Rec."Shortcut Dimension 1 Code") + { + Caption = 'Shortcut Dimension 1 Code'; + } + field(shortcutDimension2Code; Rec."Shortcut Dimension 2 Code") + { + Caption = 'Shortcut Dimension 2 Code'; + } + field(currencyCode; CurrencyCode) + { + Caption = 'Currency Code'; + } + field(orderId; ServiceOrder.SystemId) + { + Caption = 'Order Id'; + Editable = false; + } + field(orderNumber; Rec."Order No.") + { + Caption = 'Order No.'; + Editable = false; + } + field(paymentTermsId; PaymentTerms.SystemId) + { + Caption = 'Payment Terms Id'; + } + field(shipmentMethodId; ShipmentMethod.SystemId) + { + Caption = 'Shipment Method Id'; + } + field(salesperson; Rec."Salesperson Code") + { + Caption = 'Salesperson'; + } + field(pricesIncludeTax; Rec."Prices Including VAT") + { + Caption = 'Prices Include Tax'; + Editable = false; + } + part(dimensionSetLines; "APIV2 - Dimension Set Lines") + { + Caption = 'Dimension Set Lines'; + EntityName = 'dimensionSetLine'; + EntitySetName = 'dimensionSetLines'; + SubPageLink = "Parent Id" = field(SystemId); + } + part(postedServiceInvoiceLines; "FS Posted Serv. Inv. Lines API") + { + Caption = 'Lines'; + EntityName = 'postedServiceInvoiceLine'; + EntitySetName = 'postedServiceInvoiceLines'; + SubPageLink = "Document No." = field("No."); + } + part(pdfDocument; "APIV2 - PDF Document") + { + Caption = 'PDF Document'; + Multiplicity = ZeroOrOne; + EntityName = 'pdfDocument'; + EntitySetName = 'pdfDocument'; + SubPageLink = "Document Id" = field(SystemId); + } + field(totalAmountExcludingTax; Rec.Amount) + { + Caption = 'Total Amount Excluding Tax'; + Editable = false; + } + field(totalAmountIncludingTax; Rec."Amount Including VAT") + { + Caption = 'Total Amount Including Tax'; + Editable = false; + } + field(status; Status) + { + Caption = 'Status'; + Editable = false; + } + field(lastModifiedDateTime; Rec.SystemModifiedAt) + { + Caption = 'Last Modified Date'; + Editable = false; + } + field(phoneNumber; Rec."Phone No.") + { + Caption = 'Phone No.'; + } + field(email; Rec."E-Mail") + { + Caption = 'Email'; + } + part(attachments; "APIV2 - Attachments") + { + Caption = 'Attachments'; + EntityName = 'attachment'; + EntitySetName = 'attachments'; + SubPageLink = "Document Id" = field(SystemId); + } + part(documentAttachments; "APIV2 - Document Attachments") + { + Caption = 'Document Attachments'; + EntityName = 'documentAttachment'; + EntitySetName = 'documentAttachments'; + SubPageLink = "Document Id" = field(SystemId); + } + } + } + } + + trigger OnAfterGetRecord() + var + GraphMgtGeneralTools: Codeunit "Graph Mgt - General Tools"; + begin + if not SellToCustomer.Get(Rec."Customer No.") then + Clear(SellToCustomer); + if not BillToCustomer.Get(Rec."Bill-to Customer No.") then + Clear(BillToCustomer); + CurrencyCode := GraphMgtGeneralTools.TranslateNAVCurrencyCodeToCurrencyCode(CachedCurrencyCode, Rec."Currency Code"); + if not Currency.Get(CurrencyCode) then + Clear(Currency); + if not ServiceOrder.Get(ServiceOrder."Document Type"::Order, Rec."Order No.") then + Clear(ServiceOrder); + if not PaymentTerms.Get(Rec."Payment Terms Code") then + Clear(PaymentTerms); + if not ShipmentMethod.Get(Rec."Shipment Method Code") then + Clear(ShipmentMethod); + CalculateStatus(); + end; + + var + SellToCustomer: Record "Customer"; + BillToCustomer: Record "Customer"; + Currency: Record "Currency"; + ServiceOrder: Record "Service Header"; + PaymentTerms: Record "Payment Terms"; + ShipmentMethod: Record "Shipment Method"; + CurrencyCode: Code[10]; + CachedCurrencyCode: Code[10]; + Status: Enum "Invoice Entity Aggregate Status"; + + local procedure CalculateStatus() + var + CustLedgerEntry: Record "Cust. Ledger Entry"; + begin + CustLedgerEntry.SetCurrentKey("Document No."); + CustLedgerEntry.SetRange("Document No.", Rec."No."); + CustLedgerEntry.SetRange("Document Type", CustLedgerEntry."Document Type"::Invoice); + CustLedgerEntry.SetRange("Posting Date", Rec."Posting Date"); + CustLedgerEntry.SetRange(Open, true); + + if CustLedgerEntry.IsEmpty() then + Status := Status::Paid + else + Status := Status::Open; + end; + +} \ No newline at end of file diff --git a/Apps/W1/FieldServiceIntegration/app/src/Pages/FSWorkOrderTypes.Page.al b/Apps/W1/FieldServiceIntegration/app/src/Pages/FSWorkOrderTypes.Page.al new file mode 100644 index 0000000000..f2f732cf40 --- /dev/null +++ b/Apps/W1/FieldServiceIntegration/app/src/Pages/FSWorkOrderTypes.Page.al @@ -0,0 +1,156 @@ +// ------------------------------------------------------------------------------------------------ +// 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.DynamicsFieldService; + +using Microsoft.Integration.Dataverse; +using Microsoft.Service.Setup; +using System.Environment.Configuration; + +page 6615 "FS Work Order Types" +{ + ApplicationArea = Service; + Caption = 'Work Order Types - Dynamics 365 Field Service'; + Editable = false; + PageType = List; + SourceTable = "FS Work Order Type"; + UsageCategory = Lists; + + layout + { + area(content) + { + repeater(General) + { + field(Code; Rec.Code) + { + ApplicationArea = Service; + StyleExpr = FirstColumnStyle; + ToolTip = 'Specifies the work order type code.'; + } + field(Name; Rec.Name) + { + ApplicationArea = Service; + ToolTip = 'Specifies the work order type name.'; + } + } + } + } + + actions + { + area(processing) + { + action(CreateFromFS) + { + ApplicationArea = Suite; + Caption = 'Create in Business Central'; + Image = NewItemNonStock; + ToolTip = 'Generate the entity from the Field Service work order.'; + Visible = ShowCreateInBC; + + trigger OnAction() + var + FSWorkOrderType: Record "FS Work Order Type"; + CRMIntegrationManagement: Codeunit "CRM Integration Management"; + begin + CurrPage.SetSelectionFilter(FSWorkOrderType); + CRMIntegrationManagement.CreateNewRecordsFromSelectedCRMRecords(FSWorkOrderType); + end; + } + action(ShowOnlyUncoupled) + { + ApplicationArea = Suite; + Caption = 'Hide Coupled Records'; + Image = FilterLines; + ToolTip = 'Do not show coupled records.'; + + trigger OnAction() + begin + Rec.MarkedOnly(true); + end; + } + action(ShowAll) + { + ApplicationArea = Suite; + Caption = 'Show Coupled Records'; + Image = ClearFilter; + ToolTip = 'Show coupled records.'; + + trigger OnAction() + begin + Rec.MarkedOnly(false); + end; + } + } + area(Promoted) + { + group(Category_Process) + { + Caption = 'Process'; + + actionref(CreateFromFS_Promoted; CreateFromFS) + { + } + actionref(ShowOnlyUncoupled_Promoted; ShowOnlyUncoupled) + { + } + actionref(ShowAll_Promoted; ShowAll) + { + } + } + } + } + + trigger OnInit() + begin + Codeunit.Run(Codeunit::"CRM Integration Management"); + end; + + trigger OnOpenPage() + var + ApplicationAreaMgmtFacade: Codeunit "Application Area Mgmt. Facade"; + LookupCRMTables: Codeunit "Lookup CRM Tables"; + begin + Rec.FilterGroup(4); + Rec.SetView(LookupCRMTables.GetIntegrationTableMappingView(Database::"FS Work Order Type")); + Rec.FilterGroup(0); + ShowCreateInBC := ApplicationAreaMgmtFacade.IsPremiumExperienceEnabled(); + end; + + trigger OnAfterGetRecord() + var + CRMIntegrationRecord: Record "CRM Integration Record"; + RecordID: RecordID; + begin + if CRMIntegrationRecord.FindRecordIDFromID(Rec.WorkOrderTypeId, Database::"Service Order Type", RecordID) then + if CurrentlyCoupledFSWorkOrderType.WorkOrderTypeId = Rec.WorkOrderTypeId then begin + Coupled := 'Current'; + FirstColumnStyle := 'Strong'; + Rec.Mark(true); + end else begin + Coupled := 'Yes'; + FirstColumnStyle := 'Subordinate'; + Rec.Mark(false); + end + else begin + Coupled := 'No'; + FirstColumnStyle := 'None'; + Rec.Mark(true); + end; + end; + + + var + CurrentlyCoupledFSWorkOrderType: Record "FS Work Order Type"; + Coupled: Text; + FirstColumnStyle: Text; + ShowCreateInBC: Boolean; + + procedure SetCurrentlyCoupledFSWorkOrderType(FSWorkOrderType: Record "FS Work Order Type") + begin + CurrentlyCoupledFSWorkOrderType := FSWorkOrderType; + end; +} + diff --git a/Apps/W1/FieldServiceIntegration/app/src/Pages/FSWorkOrders.Page.al b/Apps/W1/FieldServiceIntegration/app/src/Pages/FSWorkOrders.Page.al new file mode 100644 index 0000000000..0c324e8995 --- /dev/null +++ b/Apps/W1/FieldServiceIntegration/app/src/Pages/FSWorkOrders.Page.al @@ -0,0 +1,260 @@ +// ------------------------------------------------------------------------------------------------ +// 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.DynamicsFieldService; + +using Microsoft.Integration.Dataverse; +using System.Environment.Configuration; +using Microsoft.Integration.D365Sales; +using Microsoft.Service.Document; + +page 6616 "FS Work Orders" +{ + ApplicationArea = Service; + Caption = 'Work Orders - Dynamics 365 Field Service'; + Editable = false; + PageType = List; + SourceTable = "FS Work Order"; + UsageCategory = Lists; + + layout + { + area(content) + { + repeater(General) + { + field(Name; Rec.Name) + { + ApplicationArea = Service; + StyleExpr = FirstColumnStyle; + ToolTip = 'Specifies the work order type code.'; + } + field(WorkOrderType; Rec.WorkOrderType) + { + ApplicationArea = Service; + Visible = false; + ToolTip = 'Specifies the work order type.'; + } + field(WorkOrderTypeName; FSWorkOrderType.Name) + { + Caption = 'Work Order Type'; + ApplicationArea = Service; + ToolTip = 'Specifies the work order type.'; + } + field(StatusCode; Rec.StatusCode) + { + ApplicationArea = Service; + ToolTip = 'Specifies the status of the work order.'; + } + field(CreatedOn; Rec.CreatedOn) + { + ApplicationArea = Service; + ToolTip = 'Specifies the date and time when the work order was created.'; + } + field(ServiceAccount; Rec.ServiceAccount) + { + ApplicationArea = Service; + Visible = false; + ToolTip = 'Specifies the service account for the work order.'; + } + field(ServiceAccountName; CRMAccountService.Name) + { + Caption = 'Service Account'; + ApplicationArea = Service; + ToolTip = 'Specifies the service account for the work order.'; + } + field(Address1; Rec.Address1) + { + ApplicationArea = Service; + Visible = false; + ToolTip = 'Specifies the address for the work order.'; + } + field(Address2; Rec.Address2) + { + ApplicationArea = Service; + Visible = false; + ToolTip = 'Specifies the address for the work order.'; + } + field(Address3; Rec.Address3) + { + ApplicationArea = Service; + Visible = false; + ToolTip = 'Specifies the address for the work order.'; + } + field(PostalCode; Rec.PostalCode) + { + ApplicationArea = Service; + ToolTip = 'Specifies the address for the work order.'; + Visible = false; + } + field(City; Rec.City) + { + ApplicationArea = Service; + ToolTip = 'Specifies the address for the work order.'; + Visible = false; + } + field(BillingAccount; Rec.BillingAccount) + { + ApplicationArea = Service; + Visible = false; + ToolTip = 'Specifies the billing account for the work order.'; + } + field(BillingAccountName; CRMAccountBilling.Name) + { + Caption = 'Billing Account'; + ApplicationArea = Service; + ToolTip = 'Specifies the billing account for the work order.'; + } + } + } + } + + actions + { + area(processing) + { + action(CreateFromFS) + { + ApplicationArea = Suite; + Caption = 'Create in Business Central'; + Image = NewItemNonStock; + ToolTip = 'Generate the entity from the Field Service work order.'; + Visible = ShowCreateInBC; + + trigger OnAction() + var + FSWorkOrder: Record "FS Work Order"; + CRMIntegrationManagement: Codeunit "CRM Integration Management"; + begin + CurrPage.SetSelectionFilter(FSWorkOrder); + CRMIntegrationManagement.CreateNewRecordsFromSelectedCRMRecords(FSWorkOrder); + end; + } + action(OpenInBC) + { + ApplicationArea = Suite; + Caption = 'Open in Business Central'; + Image = Document; + ToolTip = 'Opens the entity in Business Central.'; + Visible = ShowOpenInBC; + + trigger OnAction() + var + ServiceHeader: Record "Service Header"; + begin + RecordID.GetRecord().SetTable(ServiceHeader); + Page.Run(Page::"Service Order", ServiceHeader); + end; + } + action(ShowOnlyUncoupled) + { + ApplicationArea = Suite; + Caption = 'Hide Coupled Records'; + Image = FilterLines; + ToolTip = 'Do not show coupled records.'; + + trigger OnAction() + begin + Rec.MarkedOnly(true); + end; + } + action(ShowAll) + { + ApplicationArea = Suite; + Caption = 'Show Coupled Records'; + Image = ClearFilter; + ToolTip = 'Show coupled records.'; + + trigger OnAction() + begin + Rec.MarkedOnly(false); + end; + } + } + area(Promoted) + { + group(Category_Process) + { + Caption = 'Process'; + + actionref(CreateFromFS_Promoted; CreateFromFS) + { + } + actionref(OpenInBC_Promoted; OpenInBC) + { + } + actionref(ShowOnlyUncoupled_Promoted; ShowOnlyUncoupled) + { + } + actionref(ShowAll_Promoted; ShowAll) + { + } + } + } + } + + trigger OnInit() + begin + Codeunit.Run(Codeunit::"CRM Integration Management"); + end; + + trigger OnOpenPage() + var + ApplicationAreaMgmtFacade: Codeunit "Application Area Mgmt. Facade"; + LookupCRMTables: Codeunit "Lookup CRM Tables"; + begin + Rec.FilterGroup(4); + Rec.SetView(LookupCRMTables.GetIntegrationTableMappingView(Database::"FS Work Order")); + Rec.FilterGroup(0); + ShowCreateInBC := ApplicationAreaMgmtFacade.IsPremiumExperienceEnabled(); + end; + + trigger OnAfterGetRecord() + var + CRMIntegrationRecord: Record "CRM Integration Record"; + begin + if CRMIntegrationRecord.FindRecordIDFromID(Rec.WorkOrderId, Database::"Service Header", RecordID) then begin + if CurrentlyCoupledFSWorkOrder.WorkOrderId = Rec.WorkOrderId then begin + Coupled := 'Current'; + FirstColumnStyle := 'Strong'; + Rec.Mark(true); + end else begin + Coupled := 'Yes'; + FirstColumnStyle := 'Subordinate'; + Rec.Mark(false); + end; + ShowOpenInBC := true; + end else begin + Coupled := 'No'; + FirstColumnStyle := 'None'; + ShowOpenInBC := false; + Rec.Mark(true); + end; + + if not CRMAccountService.Get(Rec.ServiceAccount) then + Clear(CRMAccountService); + if not CRMAccountBilling.Get(Rec.ServiceAccount) then + Clear(CRMAccountBilling); + if not FSWorkOrderType.Get(Rec.WorkOrderType) then + Clear(FSWorkOrderType); + end; + + + var + CurrentlyCoupledFSWorkOrder: Record "FS Work Order"; + FSWorkOrderType: Record "FS Work Order Type"; + CRMAccountService: Record "CRM Account"; + CRMAccountBilling: Record "CRM Account"; + RecordID: RecordID; + Coupled: Text; + FirstColumnStyle: Text; + ShowCreateInBC: Boolean; + ShowOpenInBC: Boolean; + + procedure SetCurrentlyCoupledFSWorkOrder(FSWorkOrder: Record "FS Work Order") + begin + CurrentlyCoupledFSWorkOrder := FSWorkOrder; + end; +} + diff --git a/Apps/W1/FieldServiceIntegration/app/src/Permissions/FSEdit.PermissionSet.al b/Apps/W1/FieldServiceIntegration/app/src/Permissions/FSEdit.PermissionSet.al index 70b9c6083f..93dffb31ab 100644 --- a/Apps/W1/FieldServiceIntegration/app/src/Permissions/FSEdit.PermissionSet.al +++ b/Apps/W1/FieldServiceIntegration/app/src/Permissions/FSEdit.PermissionSet.al @@ -26,5 +26,7 @@ permissionset 6610 "FS - Edit" tabledata "FS Work Order Product" = IMD, tabledata "FS Work Order Service" = IMD, tabledata "FS Work Order Substatus" = IMD, - tabledata "FS Work Order Type" = IMD; + tabledata "FS Work Order Type" = IMD, + tabledata "FS Booking Status" = IMD, + tabledata "FS Incident Type" = IMD; } diff --git a/Apps/W1/FieldServiceIntegration/app/src/Permissions/FSObjects.PermissionSet.al b/Apps/W1/FieldServiceIntegration/app/src/Permissions/FSObjects.PermissionSet.al index 0c7556efd2..f658f8093d 100644 --- a/Apps/W1/FieldServiceIntegration/app/src/Permissions/FSObjects.PermissionSet.al +++ b/Apps/W1/FieldServiceIntegration/app/src/Permissions/FSObjects.PermissionSet.al @@ -10,7 +10,8 @@ permissionset 6612 "FS - Objects" Assignable = false; Caption = 'Field Service - Objects'; - Permissions = codeunit "FS Assisted Setup Subscriber" = X, + Permissions = codeunit "FS Archived Service Orders Job" = X, + codeunit "FS Assisted Setup Subscriber" = X, codeunit "FS Data Classification" = X, codeunit "FS Install" = X, codeunit "FS Upgrade" = X, @@ -23,13 +24,19 @@ permissionset 6612 "FS - Objects" page "FS Connection Setup Wizard" = X, page "FS Customer Asset List" = X, page "FS Item Avail. by Location" = X, + page "FS Posted Serv. Inv. Lines API" = X, + page "FS Posted Service Invoice API" = X, + page "FS Work Order Types" = X, + page "FS Work Orders" = X, query "FS Item Avail. by Location" = X, table "FS Bookable Resource" = X, table "FS Bookable Resource Booking" = X, table "FS BookableResourceBookingHdr" = X, + table "FS Booking Status" = X, table "FS Connection Setup" = X, table "FS Customer Asset" = X, table "FS Customer Asset Category" = X, + table "FS Incident Type" = X, table "FS Project Task" = X, table "FS Resource Pay Type" = X, table "FS Warehouse" = X, diff --git a/Apps/W1/FieldServiceIntegration/app/src/Permissions/FSRead.PermissionSet.al b/Apps/W1/FieldServiceIntegration/app/src/Permissions/FSRead.PermissionSet.al index 1f4dda8f14..37019b5b1d 100644 --- a/Apps/W1/FieldServiceIntegration/app/src/Permissions/FSRead.PermissionSet.al +++ b/Apps/W1/FieldServiceIntegration/app/src/Permissions/FSRead.PermissionSet.al @@ -26,5 +26,7 @@ permissionset 6611 "FS - Read" tabledata "FS Work Order Product" = R, tabledata "FS Work Order Service" = R, tabledata "FS Work Order Substatus" = R, - tabledata "FS Work Order Type" = R; + tabledata "FS Work Order Type" = R, + tabledata "FS Booking Status" = R, + tabledata "FS Incident Type" = R; } diff --git a/Apps/W1/FieldServiceIntegration/app/src/Table Extensions/FSIntegrationRecord.TableExt.al b/Apps/W1/FieldServiceIntegration/app/src/Table Extensions/FSIntegrationRecord.TableExt.al new file mode 100644 index 0000000000..3447fb6577 --- /dev/null +++ b/Apps/W1/FieldServiceIntegration/app/src/Table Extensions/FSIntegrationRecord.TableExt.al @@ -0,0 +1,49 @@ +// ------------------------------------------------------------------------------------------------ +// 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.DynamicsFieldService; + +using Microsoft.Integration.Dataverse; + +tableextension 6618 "FS Integration Record" extends "CRM Integration Record" +{ + fields + { + field(12000; "Archived Service Order"; Boolean) + { + Caption = 'Archived Service Order'; + DataClassification = SystemMetadata; + } + field(12001; "Archived Service Order Updated"; Boolean) + { + Caption = 'Archived Service Order Updated'; + DataClassification = SystemMetadata; + } + field(12002; "Archived Service Header Id"; Guid) + { + Caption = 'Archived Service Header Id'; + DataClassification = SystemMetadata; + } + field(12003; "Archived Service Line Id"; Guid) + { + Caption = 'Archived Service Line Id'; + DataClassification = SystemMetadata; + } + field(12004; "Skip Reimport"; Boolean) + { + Caption = 'Skip Reimport'; + DataClassification = SystemMetadata; + } + } + + keys + { + key(Key1; "Archived Service Header Id") + { + } + key(Key2; "Archived Service Line Id") + { + } + } +} diff --git a/Apps/W1/FieldServiceIntegration/app/src/Table Extensions/FSServiceHeader.TableExt.al b/Apps/W1/FieldServiceIntegration/app/src/Table Extensions/FSServiceHeader.TableExt.al new file mode 100644 index 0000000000..ec1d3a4a03 --- /dev/null +++ b/Apps/W1/FieldServiceIntegration/app/src/Table Extensions/FSServiceHeader.TableExt.al @@ -0,0 +1,49 @@ +// ------------------------------------------------------------------------------------------------ +// 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.DynamicsFieldService; + +using Microsoft.Service.Document; +using System.Reflection; +using Microsoft.Integration.Dataverse; + +tableextension 6615 "FS Service Header" extends "Service Header" +{ + fields + { + field(12000; "Work Description"; BLOB) + { + Caption = 'Work Description'; + } + + field(12001; "Coupled to FS"; Boolean) + { + FieldClass = FlowField; + Caption = 'Coupled to Field Service'; + Editable = false; + CalcFormula = exist("CRM Integration Record" where("Integration ID" = field(SystemId), "Table ID" = const(Database::"Service Header"))); + } + } + + procedure SetWorkDescription(NewWorkDescription: Text) + var + OutStream: OutStream; + begin + Clear("Work Description"); + Rec."Work Description".CreateOutStream(OutStream, TextEncoding::UTF8); + OutStream.WriteText(NewWorkDescription); + Modify(); + end; + + procedure GetWorkDescription() WorkDescription: Text + var + TypeHelper: Codeunit "Type Helper"; + InStream: InStream; + begin + Rec.CalcFields("Work Description"); + Rec."Work Description".CreateInStream(InStream, TextEncoding::UTF8); + exit(TypeHelper.TryReadAsTextWithSepAndFieldErrMsg(InStream, TypeHelper.LFSeparator(), Rec.FieldName("Work Description"))); + end; + +} diff --git a/Apps/W1/FieldServiceIntegration/app/src/Table Extensions/FSServiceItemLine.TableExt.al b/Apps/W1/FieldServiceIntegration/app/src/Table Extensions/FSServiceItemLine.TableExt.al new file mode 100644 index 0000000000..7ff724ee65 --- /dev/null +++ b/Apps/W1/FieldServiceIntegration/app/src/Table Extensions/FSServiceItemLine.TableExt.al @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------------------------ +// 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.DynamicsFieldService; + +using Microsoft.Service.Document; +using Microsoft.Integration.Dataverse; + +tableextension 6620 "FS Service Item Line" extends "Service Item Line" +{ + fields + { + field(12000; "Coupled to FS"; Boolean) + { + FieldClass = FlowField; + Caption = 'Coupled to Field Service'; + Editable = false; + CalcFormula = exist("CRM Integration Record" where("Integration ID" = field(SystemId), "Table ID" = const(Database::"Service Item Line"))); + } + field(12001; "FS Bookings"; Boolean) + { + Caption = 'Field Service Bookings'; + DataClassification = CustomerContent; + } + } +} diff --git a/Apps/W1/FieldServiceIntegration/app/src/Table Extensions/FSServiceLine.TableExt.al b/Apps/W1/FieldServiceIntegration/app/src/Table Extensions/FSServiceLine.TableExt.al new file mode 100644 index 0000000000..642805796c --- /dev/null +++ b/Apps/W1/FieldServiceIntegration/app/src/Table Extensions/FSServiceLine.TableExt.al @@ -0,0 +1,29 @@ +// ------------------------------------------------------------------------------------------------ +// 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.DynamicsFieldService; + +using Microsoft.Service.Document; +using Microsoft.Inventory.Item; +using Microsoft.Integration.Dataverse; + +tableextension 6616 "FS Service Line" extends "Service Line" +{ + fields + { + field(12000; "Item Type"; Enum "Item Type") + { + Caption = 'Item Type'; + FieldClass = FlowField; + CalcFormula = lookup(Item.Type where("No." = field("No."))); + } + field(12001; "Coupled to FS"; Boolean) + { + FieldClass = FlowField; + Caption = 'Coupled to Field Service'; + Editable = false; + CalcFormula = exist("CRM Integration Record" where("Integration ID" = field(SystemId), "Table ID" = const(Database::"Service Line"))); + } + } +} diff --git a/Apps/W1/FieldServiceIntegration/app/src/Table Extensions/FSServiceOrderType.TableExt.al b/Apps/W1/FieldServiceIntegration/app/src/Table Extensions/FSServiceOrderType.TableExt.al new file mode 100644 index 0000000000..471ae52c40 --- /dev/null +++ b/Apps/W1/FieldServiceIntegration/app/src/Table Extensions/FSServiceOrderType.TableExt.al @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------------------------------ +// 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.DynamicsFieldService; + +using Microsoft.Service.Setup; +using Microsoft.Integration.Dataverse; + +tableextension 6619 "FS Service Order Type" extends "Service Order Type" +{ + fields + { + field(12000; "Coupled to FS"; Boolean) + { + FieldClass = FlowField; + Caption = 'Coupled to Field Service'; + Editable = false; + CalcFormula = exist("CRM Integration Record" where("Integration ID" = field(SystemId), "Table ID" = const(Database::"Service Order Type"))); + } + } +} diff --git a/Apps/W1/FieldServiceIntegration/app/src/Tables/FSBookableResourceBooking.Table.al b/Apps/W1/FieldServiceIntegration/app/src/Tables/FSBookableResourceBooking.Table.al index f566b60040..3419b5c75b 100644 --- a/Apps/W1/FieldServiceIntegration/app/src/Tables/FSBookableResourceBooking.Table.al +++ b/Apps/W1/FieldServiceIntegration/app/src/Tables/FSBookableResourceBooking.Table.al @@ -616,6 +616,32 @@ table 6611 "FS Bookable Resource Booking" ExternalType = 'String'; ExternalAccess = Read; } + field(110; BookingStatus; Guid) + { + ExternalName = 'bookingstatus'; + ExternalType = 'Lookup'; + ExternalAccess = Read; + Description = 'Unique identifier of the booking status.'; + Caption = 'Booking Status'; + TableRelation = "FS Booking Status".BookingStatusId; + DataClassification = SystemMetadata; + } + field(120; CompanyId; GUID) + { + ExternalName = 'bcbi_company'; + ExternalType = 'Lookup'; + Description = 'Business Central Company'; + Caption = 'Company Id'; + TableRelation = "CDS Company".CompanyId; + DataClassification = SystemMetadata; + } + field(121; IntegrateToService; Boolean) + { + ExternalName = 'bcbi_integratetoervice'; + ExternalType = 'Boolean'; + Caption = 'Integrate to Service'; + DataClassification = SystemMetadata; + } } keys { diff --git a/Apps/W1/FieldServiceIntegration/app/src/Tables/FSBookingStatus.Table.al b/Apps/W1/FieldServiceIntegration/app/src/Tables/FSBookingStatus.Table.al new file mode 100644 index 0000000000..c38f0f3103 --- /dev/null +++ b/Apps/W1/FieldServiceIntegration/app/src/Tables/FSBookingStatus.Table.al @@ -0,0 +1,97 @@ +// ------------------------------------------------------------------------------------------------ +// 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.DynamicsFieldService; + +using Microsoft.Integration.D365Sales; + +table 6627 "FS Booking Status" +{ + ExternalName = 'bookingstatus'; + TableType = CRM; + Description = 'Booking Status in CRM.'; + DataClassification = SystemMetadata; + + fields + { + field(1; BookingStatusId; GUID) + { + ExternalName = 'bookingstatusid'; + ExternalType = 'Uniqueidentifier'; + ExternalAccess = Read; + Description = 'Unique identifier of the booking state.'; + Caption = 'Booking Status Id'; + DataClassification = SystemMetadata; + } + field(2; CreatedOn; Datetime) + { + ExternalName = 'CreatedOn'; + ExternalType = 'DateTime'; + ExternalAccess = Read; + Description = 'Date and time when the record was created.'; + Caption = 'Created On'; + DataClassification = SystemMetadata; + } + field(3; CreatedBy; GUID) + { + ExternalName = 'CreatedBy'; + ExternalType = 'Lookup'; + ExternalAccess = Read; + Description = 'Unique identifier of the user who created the record.'; + Caption = 'Created By'; + TableRelation = "CRM Systemuser".SystemUserId; + DataClassification = SystemMetadata; + } + field(4; ModifiedOn; Datetime) + { + ExternalName = 'ModifiedOn'; + ExternalType = 'DateTime'; + ExternalAccess = Read; + Description = 'Date and time when the record was modified.'; + Caption = 'Modified On'; + DataClassification = SystemMetadata; + } + field(5; ModifiedBy; GUID) + { + ExternalName = 'ModifiedBy'; + ExternalType = 'Lookup'; + ExternalAccess = Read; + Description = 'Unique identifier of the user who modified the record.'; + Caption = 'Modified By'; + TableRelation = "CRM Systemuser".SystemUserId; + DataClassification = SystemMetadata; + } + field(10; Name; Text[100]) + { + ExternalName = 'name'; + ExternalType = 'String'; + Description = 'Type the name of the booking status.'; + Caption = 'Name'; + DataClassification = SystemMetadata; + } + field(11; FieldServiceStatus; Option) + { + ExternalName = 'msdyn_fieldservicestatus'; + ExternalType = 'Picklist'; + Description = 'Status in Field Service.'; + OptionMembers = "",Scheduled,Traveling," On Break","In Progress",Completed,Canceled; + OptionOrdinalValues = -1, 690970000, 690970001, 690970002, 690970003, 690970004, 690970005; + Caption = 'Field Service Status'; + DataClassification = SystemMetadata; + } + } + keys + { + key(PK; BookingStatusId) + { + Clustered = true; + } + } + fieldgroups + { + fieldgroup(DropDown; Name) + { + } + } +} \ No newline at end of file diff --git a/Apps/W1/FieldServiceIntegration/app/src/Tables/FSConnectionSetup.Table.al b/Apps/W1/FieldServiceIntegration/app/src/Tables/FSConnectionSetup.Table.al index 178a9f05b6..fdd571b600 100644 --- a/Apps/W1/FieldServiceIntegration/app/src/Tables/FSConnectionSetup.Table.al +++ b/Apps/W1/FieldServiceIntegration/app/src/Tables/FSConnectionSetup.Table.al @@ -95,8 +95,10 @@ table 6623 "FS Connection Setup" CRMConnectionSetupPage: Page "CRM Connection Setup"; begin if "Is Enabled" then begin - TestField("Job Journal Template"); - TestField("Job Journal Batch"); + if "Integration Type" <> "Integration Type"::Service then begin + TestField("Job Journal Template"); + TestField("Job Journal Batch"); + end; if not CRMConnectionSetup.IsEnabled() then Error(CRMConnSetupMustBeEnabledErr, CRMConnectionSetupPage.Caption()); if "Hour Unit of Measure" = '' then @@ -244,6 +246,24 @@ table 6623 "FS Connection Setup" DataClassification = SystemMetadata; Caption = 'Automatically post project journal lines'; } + field(300; "Integration Type"; Enum "FS Integration Type") + { + DataClassification = SystemMetadata; + Caption = 'Integration Type'; + + trigger OnValidate() + var + IntegrationMgt: Codeunit "FS Integration Mgt."; + begin + IntegrationMgt.TestManualServiceOrderNoSeriesFlag(Rec."Integration Type"); + IntegrationMgt.TestOneServiceItemLinePerOrder(Rec."Integration Type"); + end; + } + field(301; "Default Work Order Incident ID"; Guid) + { + DataClassification = SystemMetadata; + Caption = 'Default Work Order Incident ID'; + } } keys @@ -985,6 +1005,14 @@ table 6623 "FS Connection Setup" JobQueueEntry.SetStatus(NewStatus); until JobQueueEntry.Next() = 0; until IntegrationTableMapping.Next() = 0; + + JobQueueEntry.Reset(); + JobQueueEntry.SetRange("Object Type to Run"); + JobQueueEntry.SetRange("Object ID to Run", Codeunit::"FS Archived Service Orders Job"); + if JobQueueEntry.FindSet() then + repeat + JobQueueEntry.SetStatus(NewStatus); + until JobQueueEntry.Next() = 0; end; internal procedure GetConnectionStringAsStoredInSetup() ConnectionString: Text @@ -1027,6 +1055,16 @@ table 6623 "FS Connection Setup" exit("Is Enabled"); end; + internal procedure IsIntegrationTypeServiceEnabled(): Boolean + begin + exit(IsEnabled() and ("Integration Type" in ["Integration Type"::Service, "Integration Type"::Both])); + end; + + internal procedure IsIntegrationTypeProjectEnabled(): Boolean + begin + exit(IsEnabled() and ("Integration Type" in ["Integration Type"::Project, "Integration Type"::Both])); + end; + internal procedure GetProxyVersion(): Integer var EnvironmentInformation: Codeunit "Environment Information"; diff --git a/Apps/W1/FieldServiceIntegration/app/src/Tables/FSIncidentType.Table.al b/Apps/W1/FieldServiceIntegration/app/src/Tables/FSIncidentType.Table.al new file mode 100644 index 0000000000..8379e4b673 --- /dev/null +++ b/Apps/W1/FieldServiceIntegration/app/src/Tables/FSIncidentType.Table.al @@ -0,0 +1,90 @@ +// ------------------------------------------------------------------------------------------------ +// 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.DynamicsFieldService; + +using Microsoft.Integration.D365Sales; + +table 6625 "FS Incident Type" +{ + ExternalName = 'msdyn_incidenttype'; + TableType = CRM; + Description = 'Incident Types in CRM.'; + DataClassification = SystemMetadata; + + fields + { + field(1; IncidentTypeId; GUID) + { + ExternalName = 'msdyn_incidenttypeid'; + ExternalType = 'Uniqueidentifier'; + ExternalAccess = Insert; + Description = 'Unique identifier of the resource.'; + Caption = 'Incident Type Id'; + DataClassification = SystemMetadata; + } + field(2; CreatedOn; Datetime) + { + ExternalName = 'CreatedOn'; + ExternalType = 'DateTime'; + ExternalAccess = Read; + Description = 'Date and time when the record was created.'; + Caption = 'Created On'; + DataClassification = SystemMetadata; + } + field(3; CreatedBy; GUID) + { + ExternalName = 'CreatedBy'; + ExternalType = 'Lookup'; + ExternalAccess = Read; + Description = 'Unique identifier of the user who created the record.'; + Caption = 'Created By'; + TableRelation = "CRM Systemuser".SystemUserId; + DataClassification = SystemMetadata; + } + field(4; ModifiedOn; Datetime) + { + ExternalName = 'ModifiedOn'; + ExternalType = 'DateTime'; + ExternalAccess = Read; + Description = 'Date and time when the record was modified.'; + Caption = 'Modified On'; + DataClassification = SystemMetadata; + } + field(5; ModifiedBy; GUID) + { + ExternalName = 'ModifiedBy'; + ExternalType = 'Lookup'; + ExternalAccess = Read; + Description = 'Unique identifier of the user who modified the record.'; + Caption = 'Modified By'; + TableRelation = "CRM Systemuser".SystemUserId; + DataClassification = SystemMetadata; + } + field(10; Name; Text[100]) + { + ExternalName = 'msdyn_name'; + ExternalType = 'String'; + Description = 'Type the name of the incident.'; + Caption = 'Name'; + DataClassification = SystemMetadata; + } + } + keys + { + key(PK; IncidentTypeId) + { + Clustered = true; + } + key(Name; Name) + { + } + } + fieldgroups + { + fieldgroup(DropDown; Name) + { + } + } +} \ No newline at end of file diff --git a/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWarehouse.Table.al b/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWarehouse.Table.al index a36c232f06..fe16069679 100644 --- a/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWarehouse.Table.al +++ b/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWarehouse.Table.al @@ -195,7 +195,7 @@ table 6624 "FS Warehouse" } field(33; CompanyId; GUID) { - ExternalName = 'bcbi_companyid'; + ExternalName = 'bcbi_company'; ExternalType = 'Lookup'; Description = 'The unique identifier of the company associated with the location.'; Caption = 'Company'; diff --git a/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWorkOrder.Table.al b/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWorkOrder.Table.al index f9dbed9ba5..22fa8a93a4 100644 --- a/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWorkOrder.Table.al +++ b/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWorkOrder.Table.al @@ -886,6 +886,13 @@ table 6617 "FS Work Order" TableRelation = "CDS Company".CompanyId; DataClassification = SystemMetadata; } + field(165; IntegrateToService; Boolean) + { + ExternalName = 'bcbi_integratetoervice'; + ExternalType = 'Boolean'; + Caption = 'Integrate to Service'; + DataClassification = SystemMetadata; + } } keys { diff --git a/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWorkOrderIncident.Table.al b/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWorkOrderIncident.Table.al index 6837e3621c..59d73fc2cd 100644 --- a/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWorkOrderIncident.Table.al +++ b/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWorkOrderIncident.Table.al @@ -292,6 +292,23 @@ table 6618 "FS Work Order Incident" ExternalType = 'String'; ExternalAccess = Read; } + field(60; IncidentType; Guid) + { + ExternalName = 'msdyn_incidenttype'; + ExternalType = 'Lookup'; + Caption = 'Incident Type'; + TableRelation = "FS Incident Type".IncidentTypeId; + DataClassification = SystemMetadata; + } + field(70; CompanyId; GUID) + { + ExternalName = 'bcbi_company'; + ExternalType = 'Lookup'; + Description = 'Business Central Company'; + Caption = 'Company Id'; + TableRelation = "CDS Company".CompanyId; + DataClassification = SystemMetadata; + } } keys { diff --git a/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWorkOrderProduct.Table.al b/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWorkOrderProduct.Table.al index aff2eaf9fd..9605841d5a 100644 --- a/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWorkOrderProduct.Table.al +++ b/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWorkOrderProduct.Table.al @@ -706,6 +706,35 @@ table 6619 "FS Work Order Product" Description = 'Quantity invoiced in Dynamics 365 Business Central. When this value is different than 0, you can no longer edit the work order product.'; Caption = 'Quantity Invoiced'; } + field(113; QuantityShipped; Decimal) + { + ExternalName = 'bcbi_quantityshipped'; + ExternalType = 'Float'; + Description = 'Quantity shipped in Dynamics 365 Business Central. When this value is different than 0, you can no longer edit the work order product.'; + Caption = 'Quantity Shipped'; + } + field(120; IntegrateToService; Boolean) + { + ExternalName = 'bcbi_integratetoervice'; + ExternalType = 'Boolean'; + Caption = 'Integrate to Service'; + DataClassification = SystemMetadata; + } + field(121; LocationCode; Code[10]) + { + ExternalName = 'bcbi_locationcode'; + ExternalType = 'String'; + Description = 'Unique identifier of the warehouse associated with the entity.'; + Caption = 'Location Code'; + ExternalAccess = Read; + } + field(122; ProductId; Code[20]) + { + ExternalName = 'bcbi_productid'; + ExternalType = 'String'; + Description = 'Unique identifier of the product associated with the entity.'; + Caption = 'Product Id'; + } } keys { diff --git a/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWorkOrderService.Table.al b/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWorkOrderService.Table.al index 4830d0383b..fc76add2d2 100644 --- a/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWorkOrderService.Table.al +++ b/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWorkOrderService.Table.al @@ -747,6 +747,28 @@ table 6620 "FS Work Order Service" Description = 'Duration invoiced in Dynamics 365 Business Central. When this value is different than 0, you can no longer edit the work order service.'; Caption = 'Duration Invoiced'; } + field(114; DurationShipped; Integer) + { + ExternalName = 'bcbi_durationshipped'; + ExternalType = 'Integer'; + Description = 'Duration shipped in Dynamics 365 Business Central. When this value is different than 0, you can no longer edit the work order service.'; + Caption = 'Duration Shipped'; + } + field(120; IntegrateToService; Boolean) + { + ExternalName = 'bcbi_integratetoervice'; + ExternalType = 'Boolean'; + Caption = 'Integrate to Service'; + DataClassification = SystemMetadata; + } + field(121; LocationCode; Code[10]) + { + ExternalName = 'bcbi_locationcode'; + ExternalType = 'String'; + Description = 'Unique identifier of the warehouse associated with the entity.'; + Caption = 'Location Code'; + ExternalAccess = Read; + } } keys { diff --git a/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWorkOrderType.Table.al b/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWorkOrderType.Table.al index 5929311a8b..34276899dc 100644 --- a/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWorkOrderType.Table.al +++ b/Apps/W1/FieldServiceIntegration/app/src/Tables/FSWorkOrderType.Table.al @@ -219,6 +219,31 @@ table 6622 "FS Work Order Type" Caption = 'Taxable'; DataClassification = SystemMetadata; } + field(40; Code; Text[10]) + { + ExternalName = 'bcbi_code'; + ExternalType = 'String'; + Description = 'Type the code of the work order type.'; + Caption = 'Code'; + DataClassification = SystemMetadata; + } + field(41; IntegrateToService; Boolean) + { + ExternalName = 'bcbi_integratetoservice'; + ExternalType = 'Boolean'; + Description = 'Select whether work orders of this type are integrated to service.'; + Caption = 'Integrate To Service'; + DataClassification = SystemMetadata; + } + field(50; CompanyId; GUID) + { + ExternalName = 'bcbi_company'; + ExternalType = 'Lookup'; + Description = 'Business Central Company'; + Caption = 'Company Id'; + TableRelation = "CDS Company".CompanyId; + DataClassification = SystemMetadata; + } } keys { diff --git a/Apps/W1/FieldServiceIntegration/test library/src/FSIntegrationTestLibrary.Codeunit.al b/Apps/W1/FieldServiceIntegration/test library/src/FSIntegrationTestLibrary.Codeunit.al index dcc5054f25..a445e2d306 100644 --- a/Apps/W1/FieldServiceIntegration/test library/src/FSIntegrationTestLibrary.Codeunit.al +++ b/Apps/W1/FieldServiceIntegration/test library/src/FSIntegrationTestLibrary.Codeunit.al @@ -5,6 +5,8 @@ namespace Microsoft.TestLibraries.DynamicsFieldService; using Microsoft.Integration.DynamicsFieldService; +using Microsoft.Service.Document; +using Microsoft.Service.Archive; codeunit 139205 "FS Integration Test Library" { @@ -27,4 +29,88 @@ codeunit 139205 "FS Integration Test Library" begin FSConnectionSetup.PerformTestConnection(); end; + + procedure ResetConfiguration(var FSConnectionSetup: Record "FS Connection Setup") + var + FSSetupDefaults: Codeunit "FS Setup Defaults"; + begin + FSSetupDefaults.ResetConfiguration(FSConnectionSetup); + end; + + procedure UpdateQuantities(FSWorkOrderProduct: Record "FS Work Order Product"; var ServiceLine: Record "Service Line"; ToFieldService: Boolean) + var + FSIntTableSubscriber: Codeunit "FS Int. Table Subscriber"; + begin + FSIntTableSubscriber.UpdateQuantities(FSWorkOrderProduct, ServiceLine, ToFieldService); + end; + + procedure UpdateQuantities(FSWorkOrderService: Record "FS Work Order Service"; var ServiceLine: Record "Service Line"; ToFieldService: Boolean) + var + FSIntTableSubscriber: Codeunit "FS Int. Table Subscriber"; + begin + FSIntTableSubscriber.UpdateQuantities(FSWorkOrderService, ServiceLine, ToFieldService); + end; + + procedure UpdateQuantities(FSBookableResourceBooking: Record "FS Bookable Resource Booking"; var ServiceLine: Record "Service Line") + var + FSIntTableSubscriber: Codeunit "FS Int. Table Subscriber"; + begin + FSIntTableSubscriber.UpdateQuantities(FSBookableResourceBooking, ServiceLine); + end; + + procedure IgnorePostedJobJournalLinesOnQueryPostFilterIgnoreRecord(SourceRecordRef: RecordRef; var IgnoreRecord: Boolean) + var + FSIntTableSubscriber: Codeunit "FS Int. Table Subscriber"; + begin + FSIntTableSubscriber.IgnorePostedJobJournalLinesOnQueryPostFilterIgnoreRecord(SourceRecordRef, IgnoreRecord); + end; + + procedure IgnoreArchievedServiceOrdersOnQueryPostFilterIgnoreRecord(SourceRecordRef: RecordRef; var IgnoreRecord: Boolean) + var + FSIntTableSubscriber: Codeunit "FS Int. Table Subscriber"; + begin + FSIntTableSubscriber.IgnoreArchievedServiceOrdersOnQueryPostFilterIgnoreRecord(SourceRecordRef, IgnoreRecord); + end; + + procedure IgnoreArchievedCRMWorkOrdersOnQueryPostFilterIgnoreRecord(SourceRecordRef: RecordRef; var IgnoreRecord: Boolean) + var + FSIntTableSubscriber: Codeunit "FS Int. Table Subscriber"; + begin + FSIntTableSubscriber.IgnoreArchievedCRMWorkOrdersOnQueryPostFilterIgnoreRecord(SourceRecordRef, IgnoreRecord); + end; + + procedure MarkArchivedServiceOrder(ServiceHeader: Record "Service Header") + var + FSIntTableSubscriber: Codeunit "FS Int. Table Subscriber"; + begin + FSIntTableSubscriber.MarkArchivedServiceOrder(ServiceHeader); + end; + + procedure MarkArchivedServiceOrderLine(var ServiceLine: Record "Service Line"; var ServiceLineArchive: Record "Service Line Archive") + var + FSIntTableSubscriber: Codeunit "FS Int. Table Subscriber"; + begin + FSIntTableSubscriber.MarkArchivedServiceOrderLine(ServiceLine, ServiceLineArchive); + end; + + procedure ArchiveServiceOrder(ServiceHeader: Record "Service Header"; ArchivedServiceOrders: List of [Code[20]]) + var + FSIntTableSubscriber: Codeunit "FS Int. Table Subscriber"; + begin + FSIntTableSubscriber.ArchiveServiceOrder(ServiceHeader, ArchivedServiceOrders); + end; + + procedure UpdateWorkOrderProduct(SalesLineArchive: Record "Service Line Archive"; var FSWorkOrderProduct: Record "FS Work Order Product") + var + FSArchivedServiceOrdersJob: Codeunit "FS Archived Service Orders Job"; + begin + FSArchivedServiceOrdersJob.UpdateWorkOrderProduct(SalesLineArchive, FSWorkOrderProduct); + end; + + procedure UpdateWorkOrderService(SalesLineArchive: Record "Service Line Archive"; var FSWorkOrderService: Record "FS Work Order Service") + var + FSArchivedServiceOrdersJob: Codeunit "FS Archived Service Orders Job"; + begin + FSArchivedServiceOrdersJob.UpdateWorkOrderService(SalesLineArchive, FSWorkOrderService); + end; } \ No newline at end of file diff --git a/Apps/W1/FieldServiceIntegration/test/src/FSIntegrationTest.Codeunit.al b/Apps/W1/FieldServiceIntegration/test/src/FSIntegrationTest.Codeunit.al index 5d206f6fa7..b44bd42025 100644 --- a/Apps/W1/FieldServiceIntegration/test/src/FSIntegrationTest.Codeunit.al +++ b/Apps/W1/FieldServiceIntegration/test/src/FSIntegrationTest.Codeunit.al @@ -20,8 +20,14 @@ using Microsoft.CRM.Contact; using Microsoft.Sales.Customer; using Microsoft.CRM.Team; using Microsoft.Purchases.Vendor; +using Microsoft.Service.Setup; +using Microsoft.Foundation.NoSeries; using System.Security.AccessControl; using Microsoft.TestLibraries.DynamicsFieldService; +using Microsoft.Service.Document; +using Microsoft.Service.Test; +using Microsoft.Service.Item; +using Microsoft.Service.Archive; codeunit 139204 "FS Integration Test" { @@ -40,6 +46,10 @@ codeunit 139204 "FS Integration Test" FSIntegrationTestLibrary: Codeunit "FS Integration Test Library"; Assert: Codeunit Assert; LibraryCRMIntegration: Codeunit "Library - CRM Integration"; + LibrarySales: Codeunit "Library - Sales"; + LibraryService: Codeunit "Library - Service"; + LibraryInventory: Codeunit "Library - Inventory"; + LibraryResource: Codeunit "Library - Resource"; ConnectionErr: Label 'The connection setup cannot be validated. Verify the settings and try again.'; ConnectionSuccessMsg: Label 'The connection test was successful'; JobQueueEntryStatusReadyErr: Label 'Job Queue Entry status should be Ready.'; @@ -119,6 +129,27 @@ codeunit 139204 "FS Integration Test" Assert.ExpectedError(FSConnectionSetup.FieldCaption("Job Journal Template")); end; + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure JournalTemplateNameNotRequiredToEnable() + var + FSConnectionSetup: Record "FS Connection Setup"; + begin + // [FEATURE] [UT] Service Order Integration + // [SCENARIO] Journal Template is not required to enable the Service integration type. + // [GIVEN] FS Connection Setup, where "Is Enabled" = No. + Initialize(); + InitSetup(false, ''); + + // [GIVEN] Setup without Job Journal Template and Integration Type = Service. + FSConnectionSetup."Integration Type" := FSConnectionSetup."Integration Type"::Service; + FSConnectionSetup.Modify(); + + // [THEN] Validate that the connection is enabled without error message (regarding job journal). + asserterror FSConnectionSetup.Validate("Is Enabled", true); + Assert.ExpectedError('You must enable the connection in page Dynamics 365 Sales Integration Setup'); + end; + [Test] [TransactionModel(TransactionModel::AutoRollback)] procedure JournalBatchRequiredToEnable() @@ -145,6 +176,34 @@ codeunit 139204 "FS Integration Test" JobJournalTemplate.Delete(); end; + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure JournalBatchNotRequiredToEnable() + var + FSConnectionSetup: Record "FS Connection Setup"; + JobJournalTemplate: Record "Job Journal Template"; + LibraryJob: Codeunit "Library - Job"; + begin + // [FEATURE] [UT] Service Order Integration + // [SCENARIO] Journal Batch is not required to enable the Service integration type. + // [GIVEN] FS Connection Setup, where "Is Enabled" = No. + Initialize(); + InitSetup(false, ''); + + // [GIVEN] Setup without Job Journal Batch and Integration Type = Service. + LibraryJob.CreateJobJournalTemplate(JobJournalTemplate); + JobJournalTemplate.Insert(); + FSConnectionSetup."Job Journal Template" := JobJournalTemplate.Name; + FSConnectionSetup."Integration Type" := FSConnectionSetup."Integration Type"::Service; + FSConnectionSetup.Modify(); + + // [THEN] Validate that the connection is enabled without error message (regarding job journal). + asserterror FSConnectionSetup.Validate("Is Enabled", true); + Assert.ExpectedError('You must enable the connection in page Dynamics 365 Sales Integration Setup'); + if JobJournalTemplate.Find() then + JobJournalTemplate.Delete(); + end; + [Test] [TransactionModel(TransactionModel::AutoRollback)] procedure HourUOMRequiredToEnable() @@ -177,6 +236,162 @@ codeunit 139204 "FS Integration Test" JobJournalTemplate.Delete(); end; + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure ManualNoSeriesRequiredToSelectService() + var + FSConnectionSetup: Record "FS Connection Setup"; + begin + // [FEATURE] [UI] Service Order Integration. + // [SCENARIO] User selects "Service" in "Integration Type" field, but "Manual No. Series" is not selected. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + + // [GIVEN] No Series of Service Order is not set to manual. + InitServiceManagementSetup(false, false, false); + + // [WHEN] Set Integration Type to "Service". + // [THEN] Error message "Manual No. Series is required for Service integration." appears. + FSConnectionSetup.Get(); + asserterror FSConnectionSetup.Validate("Integration Type", FSConnectionSetup."Integration Type"::Service); + Assert.ExpectedError('Please make sure that the No. Series setup is correct.'); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure ManualNoSeriesNotRequiredToSelectProject() + var + FSConnectionSetup: Record "FS Connection Setup"; + begin + // [FEATURE] [UI] Service Order Integration. + // [SCENARIO] User selects "Project" in "Integration Type" field, but "Manual No. Series" is not selected. + Initialize(); + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + InitSetup(true, ''); + // [GIVEN] No Series of Service Order is not set to manual. + InitServiceManagementSetup(false, false, false); + + // [WHEN] Set Integration Type to "Project". + // [THEN] No error message appears. + FSConnectionSetup.Get(); + FSConnectionSetup.Validate("Integration Type", FSConnectionSetup."Integration Type"::Project); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure ArchiveOfServiceOrdersIsAutomaticallyEnabled() + var + FSConnectionSetup: Record "FS Connection Setup"; + ServiceMgtSetup: Record "Service Mgt. Setup"; + begin + // [FEATURE] [UI] Service Order Integration. + // [SCENARIO] Archive Orders should be enabled for Service integration type. + // [GIVEN] Disabled FS Connection Setup. + Initialize(); + + // [GIVEN] Service Managment Archive Flag is set to false. + InitServiceManagementSetup(false, false, false); + ServiceMgtSetup.Get(); + Assert.IsFalse(ServiceMgtSetup."Archive Orders", 'Archive Orders should be disabled.'); + + // [WHEN] Field Service Integration is enabled. + InitSetup(true, ''); + FSConnectionSetup.Get(); + FSConnectionSetup."Integration Type" := FSConnectionSetup."Integration Type"::Service; + FSConnectionSetup.Modify(false); + FSIntegrationTestLibrary.ResetConfiguration(FSConnectionSetup); + + // [THEN] Service Managment Archive Flag is set to true. + ServiceMgtSetup.Get(); + Assert.IsTrue(ServiceMgtSetup."Archive Orders", 'Archive Orders should be enabled.'); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure ArchiveOfServiceOrdersIsNotAutomaticallyEnabled() + var + FSConnectionSetup: Record "FS Connection Setup"; + ServiceMgtSetup: Record "Service Mgt. Setup"; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] Archive Orders should not be enabled for default integration type. + // [GIVEN] Disabled FS Connection Setup. + Initialize(); + + // [GIVEN] Service Managment Archive Flag is set to false. + InitServiceManagementSetup(false, false, false); + ServiceMgtSetup.Get(); + Assert.IsFalse(ServiceMgtSetup."Archive Orders", 'Archive Orders should be disabled.'); + + // [WHEN] Field Service Integration is enabled. + InitSetup(true, ''); + FSConnectionSetup.Get(); + FSIntegrationTestLibrary.ResetConfiguration(FSConnectionSetup); + + // [THEN] Service Managment Archive Flag is set to false for default integration type. + ServiceMgtSetup.Get(); + Assert.IsFalse(ServiceMgtSetup."Archive Orders", 'Archive Orders should be enabled.'); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure OneServiceItemLinePerOrderIsDisabled() + var + FSConnectionSetup: Record "FS Connection Setup"; + ServiceMgtSetup: Record "Service Mgt. Setup"; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] One Service Item Line Per Order becomes enabled and this is not allowed. + // [GIVEN] Disabled FS Connection Setup. + Initialize(); + InitSetup(true, ''); + FSConnectionSetup.Get(); + FSConnectionSetup."Integration Type" := FSConnectionSetup."Integration Type"::Service; + FSConnectionSetup.Modify(false); + + // [GIVEN] Service Managment Flag is set to false. + InitServiceManagementSetup(false, false, false); + + // [WHEN] One Service Item Line Per Order becomes enabled. + // [THEN] Error message "One Service Item Line Per Order is not allowed for Field Service Integration." appears. + ServiceMgtSetup.Get(); + asserterror ServiceMgtSetup.Validate("One Service Item Line/Order", true); + Assert.ExpectedError(FSConnectionSetup.FieldCaption("Is Enabled")); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure ServiceItemComponentsFilterMandatory() + var + FSConnectionSetup: Record "FS Connection Setup"; + IntegrationTableMapping: Record "Integration Table Mapping"; + ServiceItem: Record "Service Item"; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] Service Item Components Filter is mandatory for Service integration. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + FSConnectionSetup.Get(); + FSConnectionSetup."Integration Type" := FSConnectionSetup."Integration Type"::Service; + FSConnectionSetup.Modify(false); + FSIntegrationTestLibrary.ResetConfiguration(FSConnectionSetup); + + // [WHEN] Service Item Components filter is removed. + IntegrationTableMapping.SetRange("Table ID", Database::"Service Item"); + IntegrationTableMapping.FindFirst(); + + ServiceItem.SetView(IntegrationTableMapping.GetTableFilter()); + ServiceItem.SetRange("Service Item Components"); + IntegrationTableMapping.SetTableFilter(ServiceItem.GetView(false)); + IntegrationTableMapping.Modify(false); // force blob update without validation + + // [THEN] Error message "Service Item Components Filter is mandatory for Service integration." appears. + asserterror IntegrationTableMapping.Modify(true); + Assert.ExpectedError(ServiceItem.FieldCaption("Service Item Components")); + end; + [Test] [TransactionModel(TransactionModel::AutoRollback)] procedure WorkingConnectionRequiredToEnable() @@ -535,6 +750,823 @@ codeunit 139204 "FS Integration Test" CRMConnectionSetup.Delete(); end; + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure UpdateWorkOrderProductDefaultEstimated() + var + WorkOrderProduct: Record "FS Work Order Product"; + ServiceHeader: Record "Service Header"; + ServiceLine: Record "Service Line"; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] User updates quantities on work order lines that are not posted in BC. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + + // [GIVEN] Existing Service Line. + LibraryService.CreateServiceHeader(ServiceHeader, ServiceHeader."Document Type"::Order, LibrarySales.CreateCustomerNo()); + LibraryService.CreateServiceLine(ServiceLine, ServiceHeader, ServiceLine.Type::Item, LibraryInventory.CreateItemNo()); + + // [GIVEN] Quantities on work order lines. + WorkOrderProduct.LineStatus := WorkOrderProduct.LineStatus::Estimated; + WorkOrderProduct.EstimateQuantity := 3; + WorkOrderProduct.Quantity := 2; + WorkOrderProduct.QtyToBill := 1; + + // [WHEN] Update quantities on work order lines that are not posted in BC. + FSIntegrationTestLibrary.UpdateQuantities(WorkOrderProduct, ServiceLine, false); + + // [THEN] Quantities should be updated accordingly. + Assert.AreEqual(WorkOrderProduct.EstimateQuantity, ServiceLine.Quantity, 'Quantity should be ' + Format(WorkOrderProduct.EstimateQuantity)); + Assert.AreEqual(0, ServiceLine."Qty. to Ship", 'Qty. to Ship should be 0'); + Assert.AreEqual(0, ServiceLine."Qty. to Invoice", 'Qty. to Invoice should be 0'); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure UpdateWorkOrderProductDefaultUsed() + var + WorkOrderProduct: Record "FS Work Order Product"; + ServiceHeader: Record "Service Header"; + ServiceLine: Record "Service Line"; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] User updates quantities on work order lines that are not posted in BC. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + + // [GIVEN] Existing Service Line. + LibraryService.CreateServiceHeader(ServiceHeader, ServiceHeader."Document Type"::Order, LibrarySales.CreateCustomerNo()); + LibraryService.CreateServiceLine(ServiceLine, ServiceHeader, ServiceLine.Type::Item, LibraryInventory.CreateItemNo()); + + // [GIVEN] Quantities on work order lines. + WorkOrderProduct.LineStatus := WorkOrderProduct.LineStatus::Used; + WorkOrderProduct.EstimateQuantity := 3; + WorkOrderProduct.Quantity := 2; + WorkOrderProduct.QtyToBill := 1; + + // [WHEN] Update quantities on work order lines that are not posted in BC. + FSIntegrationTestLibrary.UpdateQuantities(WorkOrderProduct, ServiceLine, false); + + // [THEN] Quantities should be updated accordingly. + Assert.AreEqual(WorkOrderProduct.Quantity, ServiceLine.Quantity, 'Quantity should be ' + Format(WorkOrderProduct.Quantity)); + Assert.AreEqual(WorkOrderProduct.Quantity, ServiceLine."Qty. to Ship", 'Qty. to Ship should be ' + Format(WorkOrderProduct.Quantity)); + Assert.AreEqual(WorkOrderProduct.QtyToBill, ServiceLine."Qty. to Invoice", 'Qty. to Invoice should be ' + Format(WorkOrderProduct.QtyToBill)); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure UpdateWorkOrderProductToShipHigherThanExpected() + var + WorkOrderProduct: Record "FS Work Order Product"; + ServiceHeader: Record "Service Header"; + ServiceLine: Record "Service Line"; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] User updates quantities on work order lines that are not posted in BC. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + + // [GIVEN] Existing Service Line. + LibraryService.CreateServiceHeader(ServiceHeader, ServiceHeader."Document Type"::Order, LibrarySales.CreateCustomerNo()); + LibraryService.CreateServiceLine(ServiceLine, ServiceHeader, ServiceLine.Type::Item, LibraryInventory.CreateItemNo()); + + // [GIVEN] Quantities on work order lines. + WorkOrderProduct.LineStatus := WorkOrderProduct.LineStatus::Used; + WorkOrderProduct.EstimateQuantity := 3; + WorkOrderProduct.Quantity := 10; + WorkOrderProduct.QtyToBill := 1; + + // [WHEN] Update quantities on work order lines that are not posted in BC. + FSIntegrationTestLibrary.UpdateQuantities(WorkOrderProduct, ServiceLine, false); + + // [THEN] Quantities should be updated accordingly. Qty to Ship increases Quantity. + Assert.AreEqual(WorkOrderProduct.Quantity, ServiceLine.Quantity, 'Quantity should be ' + Format(WorkOrderProduct.Quantity)); + Assert.AreEqual(WorkOrderProduct.Quantity, ServiceLine."Qty. to Ship", 'Qty. to Ship should be ' + Format(WorkOrderProduct.Quantity)); + Assert.AreEqual(WorkOrderProduct.QtyToBill, ServiceLine."Qty. to Invoice", 'Qty. to Invoice should be ' + Format(WorkOrderProduct.QtyToBill)); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure UpdateWorkOrderProductToInvoiceHigherThanExpected() + var + WorkOrderProduct: Record "FS Work Order Product"; + ServiceHeader: Record "Service Header"; + ServiceLine: Record "Service Line"; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] User updates quantities on work order lines that are not posted in BC. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + + // [GIVEN] Existing Service Line. + LibraryService.CreateServiceHeader(ServiceHeader, ServiceHeader."Document Type"::Order, LibrarySales.CreateCustomerNo()); + LibraryService.CreateServiceLine(ServiceLine, ServiceHeader, ServiceLine.Type::Item, LibraryInventory.CreateItemNo()); + + // [GIVEN] Quantities on work order lines. + WorkOrderProduct.LineStatus := WorkOrderProduct.LineStatus::Used; + WorkOrderProduct.EstimateQuantity := 3; + WorkOrderProduct.Quantity := 2; + WorkOrderProduct.QtyToBill := 10; + + // [WHEN] Update quantities on work order lines that are not posted in BC. + FSIntegrationTestLibrary.UpdateQuantities(WorkOrderProduct, ServiceLine, false); + + // [THEN] Quantities should be updated accordingly. Qty to Invoice increases all other quantities. + Assert.AreEqual(WorkOrderProduct.QtyToBill, ServiceLine.Quantity, 'Quantity should be ' + Format(WorkOrderProduct.QtyToBill)); + Assert.AreEqual(WorkOrderProduct.QtyToBill, ServiceLine."Qty. to Ship", 'Qty. to Ship should be ' + Format(WorkOrderProduct.QtyToBill)); + Assert.AreEqual(WorkOrderProduct.QtyToBill, ServiceLine."Qty. to Invoice", 'Qty. to Invoice should be ' + Format(WorkOrderProduct.QtyToBill)); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure UpdateWorkOrderProductAlreadyPosted() + var + WorkOrderProduct: Record "FS Work Order Product"; + ServiceHeader: Record "Service Header"; + ServiceLine: Record "Service Line"; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] User updates quantities on work order lines that are not posted in BC. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + + // [GIVEN] Existing Service Line. + LibraryService.CreateServiceHeader(ServiceHeader, ServiceHeader."Document Type"::Order, LibrarySales.CreateCustomerNo()); + LibraryService.CreateServiceLine(ServiceLine, ServiceHeader, ServiceLine.Type::Item, LibraryInventory.CreateItemNo()); + + // [GIVEN] Quantities on work order lines. + WorkOrderProduct.LineStatus := WorkOrderProduct.LineStatus::Used; + WorkOrderProduct.EstimateQuantity := 5; + WorkOrderProduct.Quantity := 3; + WorkOrderProduct.QtyToBill := 2; + ServiceLine."Quantity Shipped" := 2; + ServiceLine."Quantity Invoiced" := 1; + + // [WHEN] Update quantities on work order lines that are partly posted in BC. + FSIntegrationTestLibrary.UpdateQuantities(WorkOrderProduct, ServiceLine, false); + + // [THEN] Quantities should be updated accordingly. Posted quantities are considered. + Assert.AreEqual(WorkOrderProduct.Quantity, ServiceLine.Quantity, 'Quantity should be ' + Format(WorkOrderProduct.Quantity)); + Assert.AreEqual(WorkOrderProduct.Quantity - ServiceLine."Quantity Shipped", ServiceLine."Qty. to Ship", 'Qty. to Ship should be ' + Format(WorkOrderProduct.Quantity - ServiceLine."Quantity Shipped")); + Assert.AreEqual(WorkOrderProduct.QtyToBill - ServiceLine."Quantity Invoiced", ServiceLine."Qty. to Invoice", 'Qty. to Invoice should be ' + Format(WorkOrderProduct.QtyToBill - ServiceLine."Quantity Invoiced")); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure UpdateWorkOrderServiceDefaultEstimated() + var + WorkOrderService: Record "FS Work Order Service"; + ServiceHeader: Record "Service Header"; + ServiceLine: Record "Service Line"; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] User updates quantities on work order lines that are not posted in BC. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + + // [GIVEN] Existing Service Line. + LibraryService.CreateServiceHeader(ServiceHeader, ServiceHeader."Document Type"::Order, LibrarySales.CreateCustomerNo()); + LibraryService.CreateServiceLine(ServiceLine, ServiceHeader, ServiceLine.Type::Item, LibraryInventory.CreateItemNo()); + + // [GIVEN] Quantities on work order lines. + WorkOrderService.LineStatus := WorkOrderService.LineStatus::Estimated; + WorkOrderService.EstimateDuration := 180; + WorkOrderService.Duration := 120; + WorkOrderService.DurationToBill := 60; + + // [WHEN] Update quantities on work order lines that are not posted in BC. + FSIntegrationTestLibrary.UpdateQuantities(WorkOrderService, ServiceLine, false); + + // [THEN] Quantities should be updated accordingly. + Assert.AreEqual(WorkOrderService.EstimateDuration / 60, ServiceLine.Quantity, 'Quantity should be ' + Format(WorkOrderService.EstimateDuration / 60)); + Assert.AreEqual(0, ServiceLine."Qty. to Ship", 'Qty. to Ship should be 0'); + Assert.AreEqual(0, ServiceLine."Qty. to Invoice", 'Qty. to Invoice should be 0'); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure UpdateWorkOrderServiceDefault() + var + WorkOrderService: Record "FS Work Order Service"; + ServiceHeader: Record "Service Header"; + ServiceLine: Record "Service Line"; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] User updates quantities on work order lines that are not posted in BC. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + + // [GIVEN] Existing Service Line. + LibraryService.CreateServiceHeader(ServiceHeader, ServiceHeader."Document Type"::Order, LibrarySales.CreateCustomerNo()); + LibraryService.CreateServiceLine(ServiceLine, ServiceHeader, ServiceLine.Type::Item, LibraryInventory.CreateItemNo()); + + // [GIVEN] Quantities on work order lines. + WorkOrderService.LineStatus := WorkOrderService.LineStatus::Used; + WorkOrderService.EstimateDuration := 180; + WorkOrderService.Duration := 120; + WorkOrderService.DurationToBill := 60; + + // [WHEN] Update quantities on work order lines that are not posted in BC. + FSIntegrationTestLibrary.UpdateQuantities(WorkOrderService, ServiceLine, false); + + // [THEN] Quantities should be updated accordingly. + Assert.AreEqual(WorkOrderService.Duration / 60, ServiceLine.Quantity, 'Quantity should be ' + Format(WorkOrderService.Duration / 60)); + Assert.AreEqual(WorkOrderService.Duration / 60, ServiceLine."Qty. to Ship", 'Qty. to Ship should be ' + Format(WorkOrderService.Duration / 60)); + Assert.AreEqual(WorkOrderService.DurationToBill / 60, ServiceLine."Qty. to Invoice", 'Qty. to Invoice should be ' + Format(WorkOrderService.DurationToBill / 60)); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure UpdateWorkOrderServiceToShipHigherThanExpected() + var + WorkOrderService: Record "FS Work Order Service"; + ServiceHeader: Record "Service Header"; + ServiceLine: Record "Service Line"; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] User updates quantities on work order lines that are not posted in BC. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + + // [GIVEN] Existing Service Line. + LibraryService.CreateServiceHeader(ServiceHeader, ServiceHeader."Document Type"::Order, LibrarySales.CreateCustomerNo()); + LibraryService.CreateServiceLine(ServiceLine, ServiceHeader, ServiceLine.Type::Item, LibraryInventory.CreateItemNo()); + + // [GIVEN] Quantities on work order lines. + WorkOrderService.LineStatus := WorkOrderService.LineStatus::Used; + WorkOrderService.EstimateDuration := 180; + WorkOrderService.Duration := 240; + WorkOrderService.DurationToBill := 60; + + // [WHEN] Update quantities on work order lines that are not posted in BC. + FSIntegrationTestLibrary.UpdateQuantities(WorkOrderService, ServiceLine, false); + + // [THEN] Quantities should be updated accordingly. Qty to Ship increases Quantity. + Assert.AreEqual(WorkOrderService.Duration / 60, ServiceLine.Quantity, 'Duration should be ' + Format(WorkOrderService.Duration / 60)); + Assert.AreEqual(WorkOrderService.Duration / 60, ServiceLine."Qty. to Ship", 'Qty. to Ship should be ' + Format(WorkOrderService.Duration / 60)); + Assert.AreEqual(WorkOrderService.DurationToBill / 60, ServiceLine."Qty. to Invoice", 'Qty. to Invoice should be ' + Format(WorkOrderService.DurationToBill / 60)); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure UpdateWorkOrderServiceToInvoiceHigherThanExpected() + var + WorkOrderService: Record "FS Work Order Service"; + ServiceHeader: Record "Service Header"; + ServiceLine: Record "Service Line"; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] User updates quantities on work order lines that are not posted in BC. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + + // [GIVEN] Existing Service Line. + LibraryService.CreateServiceHeader(ServiceHeader, ServiceHeader."Document Type"::Order, LibrarySales.CreateCustomerNo()); + LibraryService.CreateServiceLine(ServiceLine, ServiceHeader, ServiceLine.Type::Item, LibraryInventory.CreateItemNo()); + + // [GIVEN] Quantities on work order lines. + WorkOrderService.LineStatus := WorkOrderService.LineStatus::Used; + WorkOrderService.EstimateDuration := 180; + WorkOrderService.Duration := 120; + WorkOrderService.DurationToBill := 240; + + // [WHEN] Update quantities on work order lines that are not posted in BC. + FSIntegrationTestLibrary.UpdateQuantities(WorkOrderService, ServiceLine, false); + + // [THEN] Quantities should be updated accordingly. Qty to Invoice increases all other quantities. + Assert.AreEqual(WorkOrderService.DurationToBill / 60, ServiceLine.Quantity, 'Quantity should be ' + Format(WorkOrderService.DurationToBill / 60)); + Assert.AreEqual(WorkOrderService.DurationToBill / 60, ServiceLine."Qty. to Ship", 'Qty. to Ship should be ' + Format(WorkOrderService.DurationToBill / 60)); + Assert.AreEqual(WorkOrderService.DurationToBill / 60, ServiceLine."Qty. to Invoice", 'Qty. to Invoice should be ' + Format(WorkOrderService.DurationToBill / 60)); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure UpdateWorkOrderServiceAlreadyPosted() + var + WorkOrderService: Record "FS Work Order Service"; + ServiceHeader: Record "Service Header"; + ServiceLine: Record "Service Line"; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] User updates quantities on work order lines that are not posted in BC. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + + // [GIVEN] Existing Service Line. + LibraryService.CreateServiceHeader(ServiceHeader, ServiceHeader."Document Type"::Order, LibrarySales.CreateCustomerNo()); + LibraryService.CreateServiceLine(ServiceLine, ServiceHeader, ServiceLine.Type::Item, LibraryInventory.CreateItemNo()); + + // [GIVEN] Quantities on work order lines. + WorkOrderService.LineStatus := WorkOrderService.LineStatus::Used; + WorkOrderService.EstimateDuration := 300; + WorkOrderService.Duration := 180; + WorkOrderService.DurationToBill := 120; + ServiceLine."Quantity Shipped" := 2; + ServiceLine."Quantity Invoiced" := 1; + + // [WHEN] Update quantities on work order lines that are partly posted in BC. + FSIntegrationTestLibrary.UpdateQuantities(WorkOrderService, ServiceLine, false); + + // [THEN] Quantities should be updated accordingly. Posted Quantities are considered. + Assert.AreEqual(WorkOrderService.Duration / 60, ServiceLine.Quantity, 'Quantity should be ' + Format(WorkOrderService.Duration / 60)); + Assert.AreEqual(WorkOrderService.Duration / 60 - ServiceLine."Quantity Shipped", ServiceLine."Qty. to Ship", 'Qty. to Ship should be ' + Format(WorkOrderService.Duration / 60 - ServiceLine."Quantity Shipped")); + Assert.AreEqual(WorkOrderService.DurationToBill / 60 - ServiceLine."Quantity Invoiced", ServiceLine."Qty. to Invoice", 'Qty. to Invoice should be ' + Format(WorkOrderService.DurationToBill / 60 - ServiceLine."Quantity Invoiced", ServiceLine."Qty. to Invoice")); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure UpdateFSBookableResourceBookingDefault() + var + FSBookableResourceBooking: Record "FS Bookable Resource Booking"; + ServiceHeader: Record "Service Header"; + ServiceLine: Record "Service Line"; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] User updates quantities on booking lines that are not posted in BC. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + + // [GIVEN] Existing Service Line. + LibraryService.CreateServiceHeader(ServiceHeader, ServiceHeader."Document Type"::Order, LibrarySales.CreateCustomerNo()); + LibraryService.CreateServiceLine(ServiceLine, ServiceHeader, ServiceLine.Type::Item, LibraryInventory.CreateItemNo()); + + // [GIVEN] Quantities on booking lines. + FSBookableResourceBooking.Duration := 120; + + // [WHEN] Update quantities on booking lines that are not posted in BC. + FSIntegrationTestLibrary.UpdateQuantities(FSBookableResourceBooking, ServiceLine); + + // [THEN] Quantities should be updated accordingly. + Assert.AreEqual(FSBookableResourceBooking.Duration / 60 - ServiceLine."Quantity Consumed", ServiceLine."Qty. to Consume", 'Qty. to Consume should be ' + Format(FSBookableResourceBooking.Duration / 60)); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure UpdateFSBookableResourceBookingAlreadyExistingQuantities() + var + FSBookableResourceBooking: Record "FS Bookable Resource Booking"; + ServiceHeader: Record "Service Header"; + ServiceLine: Record "Service Line"; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] User updates quantities on booking lines that are not posted in BC. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + + // [GIVEN] Existing Service Line. + LibraryService.CreateServiceHeader(ServiceHeader, ServiceHeader."Document Type"::Order, LibrarySales.CreateCustomerNo()); + LibraryService.CreateServiceLine(ServiceLine, ServiceHeader, ServiceLine.Type::Resource, LibraryResource.CreateResourceNo()); + + // [GIVEN] Quantities on booking lines. + FSBookableResourceBooking.Duration := 180; + ServiceLine.Validate(Quantity, 5); + ServiceLine.Validate("Qty. to Consume", 3); + + // [WHEN] Update quantities on booking lines that are not posted in BC. + FSIntegrationTestLibrary.UpdateQuantities(FSBookableResourceBooking, ServiceLine); + + // [THEN] Quantities should be updated accordingly. Existing Quantities are reset. + Assert.AreEqual(FSBookableResourceBooking.Duration / 60 - ServiceLine."Quantity Consumed", ServiceLine."Qty. to Consume", 'Qty. to Consume should be ' + Format(FSBookableResourceBooking.Duration / 60 - ServiceLine."Quantity Consumed")); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure IgnorePostedJobJournalLinesInFilterForProductEstimatedQuantity() + var + FSConnectionSetup: Record "FS Connection Setup"; + FSWorkOrderProduct: Record "FS Work Order Product"; + RecordRef: RecordRef; + IgnoreRecord: Boolean; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] User adds quantity with LineStatus=Estimated in FS. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + FSConnectionSetup.Get(); + FSConnectionSetup."Integration Type" := "FS Integration Type"::Project; + FSConnectionSetup.Modify(false); + + // [GIVEN] Existing Work Order Line + FSWorkOrderProduct.LineStatus := FSWorkOrderProduct.LineStatus::Estimated; + FSWorkOrderProduct.EstimateQuantity := 5; + FSWorkOrderProduct.Quantity := 5; + + // [GIVEN] Existing Work Order Line as reference + RecordRef.GetTable(FSWorkOrderProduct); + + // [WHEN] Filter is build -> ignore estimated lines + FSIntegrationTestLibrary.IgnorePostedJobJournalLinesOnQueryPostFilterIgnoreRecord(RecordRef, IgnoreRecord); + + // [THEN] Record should be ignored. + Assert.IsTrue(IgnoreRecord, 'Record should be ignored.'); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure IgnorePostedJobJournalLinesInFilterForServiceEstimatedQuantity() + var + FSConnectionSetup: Record "FS Connection Setup"; + FSWorkOrderService: Record "FS Work Order Service"; + RecordRef: RecordRef; + IgnoreRecord: Boolean; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] User adds quantity with LineStatus=Estimated in FS. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + FSConnectionSetup.Get(); + FSConnectionSetup."Integration Type" := "FS Integration Type"::Project; + FSConnectionSetup.Modify(false); + + // [GIVEN] Existing Work Order Line + FSWorkOrderService.LineStatus := FSWorkOrderService.LineStatus::Estimated; + FSWorkOrderService.EstimateDuration := 300; + FSWorkOrderService.Duration := 300; + + // [GIVEN] Existing Work Order Line as reference + RecordRef.GetTable(FSWorkOrderService); + + // [WHEN] Filter is build -> ignore estimated lines + FSIntegrationTestLibrary.IgnorePostedJobJournalLinesOnQueryPostFilterIgnoreRecord(RecordRef, IgnoreRecord); + + // [THEN] Record should be ignored. + Assert.IsTrue(IgnoreRecord, 'Record should be ignored.'); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure IgnorePostedJobJournalLinesInFilterForProductUsedQuantity() + var + FSConnectionSetup: Record "FS Connection Setup"; + FSWorkOrderProduct: Record "FS Work Order Product"; + RecordRef: RecordRef; + IgnoreRecord: Boolean; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] User adds quantity with LineStatus=Used in FS. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + FSConnectionSetup.Get(); + FSConnectionSetup."Integration Type" := "FS Integration Type"::Project; + FSConnectionSetup.Modify(false); + + // [GIVEN] Existing Work Order Line + FSWorkOrderProduct.LineStatus := FSWorkOrderProduct.LineStatus::Used; + FSWorkOrderProduct.EstimateQuantity := 5; + FSWorkOrderProduct.Quantity := 5; + + // [GIVEN] Existing Work Order Line as reference + RecordRef.GetTable(FSWorkOrderProduct); + + // [WHEN] Filter is build + FSIntegrationTestLibrary.IgnorePostedJobJournalLinesOnQueryPostFilterIgnoreRecord(RecordRef, IgnoreRecord); + + // [THEN] Record should not be ignored. + Assert.IsFalse(IgnoreRecord, 'Record should not be ignored.'); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure IgnorePostedJobJournalLinesInFilterForServiceUsedQuantity() + var + FSConnectionSetup: Record "FS Connection Setup"; + FSWorkOrderService: Record "FS Work Order Service"; + RecordRef: RecordRef; + IgnoreRecord: Boolean; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] User adds quantity with LineStatus=Used in FS. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + FSConnectionSetup.Get(); + FSConnectionSetup."Integration Type" := "FS Integration Type"::Project; + FSConnectionSetup.Modify(false); + + // [GIVEN] Existing Work Order Line + FSWorkOrderService.LineStatus := FSWorkOrderService.LineStatus::Used; + FSWorkOrderService.EstimateDuration := 300; + FSWorkOrderService.Duration := 300; + + // [GIVEN] Existing Work Order Line as reference + RecordRef.GetTable(FSWorkOrderService); + + // [WHEN] Filter is build -> consider used lines + FSIntegrationTestLibrary.IgnorePostedJobJournalLinesOnQueryPostFilterIgnoreRecord(RecordRef, IgnoreRecord); + + // [THEN] Record should not be ignored. + Assert.IsFalse(IgnoreRecord, 'Record should not be ignored.'); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure IgnoreArchivedServiceOrdersInFilterNotArchivedYet() + var + FSConnectionSetup: Record "FS Connection Setup"; + FSWorkOrder: Record "FS Work Order"; + ServiceHeader: Record "Service Header"; + ServiceLine: Record "Service Line"; + CRMIntegrationRecord: Record "CRM Integration Record"; + RecordRef: RecordRef; + IgnoreRecord: Boolean; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] Ignore Archived Service Orders in Filter. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + FSConnectionSetup.Get(); + FSConnectionSetup."Integration Type" := "FS Integration Type"::Service; + FSConnectionSetup.Modify(false); + + // [GIVEN] Existing Service Header + LibraryService.CreateServiceHeader(ServiceHeader, ServiceHeader."Document Type"::Order, LibrarySales.CreateCustomerNo()); + LibraryService.CreateServiceLine(ServiceLine, ServiceHeader, ServiceLine.Type::Item, LibraryInventory.CreateItemNo()); + + // [GIVEN] Existing Work Order Line as reference + FSWorkOrder.WorkOrderId := CreateGuid(); + CRMIntegrationRecord.CoupleCRMIDToRecordID(FSWorkOrder.WorkOrderId, ServiceHeader.RecordId()); + + // [GIVEN] Existing Work Order Line as reference + RecordRef.GetTable(ServiceHeader); + + // [WHEN] Filter is build -> consider used lines + FSIntegrationTestLibrary.IgnoreArchievedServiceOrdersOnQueryPostFilterIgnoreRecord(RecordRef, IgnoreRecord); + + // [THEN] Servie Order should be ignored + Assert.IsFalse(IgnoreRecord, 'Record should not be ignored.'); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure MarkArchivedServiceOrder() + var + FSConnectionSetup: Record "FS Connection Setup"; + ServiceHeader: Record "Service Header"; + CRMIntegrationRecord: Record "CRM Integration Record"; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] Service Header becomes linked to archive. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + FSConnectionSetup.Get(); + FSConnectionSetup."Integration Type" := "FS Integration Type"::Service; + FSConnectionSetup.Modify(false); + + // [GIVEN] Archive Service Orders is enabled + InitServiceManagementSetup(true, true, false); + + // [GIVEN] Existing Service Header + LibraryService.CreateServiceDocumentForCustomerNo(ServiceHeader, ServiceHeader."Document Type"::Order, LibrarySales.CreateCustomerNo()); + + // [GIVEN] Existing Work Order as reference. + CRMIntegrationRecord."Table ID" := Database::"Service Header"; + CRMIntegrationRecord."Integration ID" := ServiceHeader.SystemId; + CRMIntegrationRecord.Insert(false); + + // [WHEN] Marked as archived. + FSIntegrationTestLibrary.MarkArchivedServiceOrder(ServiceHeader); + + // [THEN] Integration Record should be marked as archived. + Clear(CRMIntegrationRecord); + CRMIntegrationRecord.SetRange("Table ID", Database::"Service Header"); + CRMIntegrationRecord.SetRange("Integration ID", ServiceHeader.SystemId); + CRMIntegrationRecord.FindFirst(); + + Assert.IsTrue(CRMIntegrationRecord."Archived Service Order", 'Record should be marked as archived.'); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure MarkArchivedServiceOrderLine() + var + FSConnectionSetup: Record "FS Connection Setup"; + ServiceHeader: Record "Service Header"; + ServiceLine: Record "Service Line"; + ServiceLineArchive: Record "Service Line Archive"; + CRMIntegrationRecord: Record "CRM Integration Record"; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] Service Line becomes linked to archive. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + FSConnectionSetup.Get(); + FSConnectionSetup."Integration Type" := "FS Integration Type"::Service; + FSConnectionSetup.Modify(false); + + // [GIVEN] Archive Service Orders is enabled + InitServiceManagementSetup(true, true, false); + + // [GIVEN] Existing Service Header + LibraryService.CreateServiceDocumentForCustomerNo(ServiceHeader, ServiceHeader."Document Type"::Order, LibrarySales.CreateCustomerNo()); + ServiceLine.SetRange("Document Type", ServiceHeader."Document Type"); + ServiceLine.SetRange("Document No.", ServiceHeader."No."); + ServiceLine.FindFirst(); + + // [GIVEN] Existing Work Order as reference. + CRMIntegrationRecord."Table ID" := Database::"Service Line"; + CRMIntegrationRecord."Integration ID" := ServiceLine.SystemId; + CRMIntegrationRecord.Insert(false); + + // [WHEN] Marked as archived. + ServiceLineArchive."Document Type" := ServiceLine."Document Type"; + ServiceLineArchive."Document No." := ServiceLine."Document No."; + ServiceLineArchive."Line No." := ServiceLine."Line No."; + ServiceLineArchive.Insert(false); + FSIntegrationTestLibrary.MarkArchivedServiceOrderLine(ServiceLine, ServiceLineArchive); + + // [THEN] Service Line and Service Line Archive should be linked. + Clear(CRMIntegrationRecord); + CRMIntegrationRecord.SetRange("Table ID", Database::"Service Line"); + CRMIntegrationRecord.SetRange("Integration ID", ServiceLine.SystemId); + CRMIntegrationRecord.FindFirst(); + + Assert.AreEqual(CRMIntegrationRecord."Archived Service Line Id", ServiceLineArchive.SystemId, 'Archived Service Line Id should be ' + Format(ServiceLineArchive.SystemId)); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure ArchiveServiceOrder() + var + FSConnectionSetup: Record "FS Connection Setup"; + ServiceHeader: Record "Service Header"; + ArchivedServiceOrders: List of [Code[20]]; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] Service Header becomes archived. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + FSConnectionSetup.Get(); + FSConnectionSetup."Integration Type" := "FS Integration Type"::Service; + FSConnectionSetup.Modify(false); + + // [GIVEN] Archive Service Orders is enabled + InitServiceManagementSetup(true, true, false); + + // [GIVEN] Existing Service Header + LibraryService.CreateServiceDocumentForCustomerNo(ServiceHeader, ServiceHeader."Document Type"::Order, LibrarySales.CreateCustomerNo()); + + // [WHEN] Archive Service Order. + FSIntegrationTestLibrary.ArchiveServiceOrder(ServiceHeader, ArchivedServiceOrders); + + // [THEN] Version should increase + ServiceHeader.Get(ServiceHeader."Document Type"::Order, ServiceHeader."No."); + ServiceHeader.CalcFields("No. of Archived Versions"); + Assert.AreEqual(1, ServiceHeader."No. of Archived Versions", 'Record should be marked as archived.'); + end; + + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure ArchiveServiceOrderWithDisabledSetupFlag() + var + FSConnectionSetup: Record "FS Connection Setup"; + ServiceHeader: Record "Service Header"; + ArchivedServiceOrders: List of [Code[20]]; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] Service Header becomes archived but setup flag is disabled. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + FSConnectionSetup.Get(); + FSConnectionSetup."Integration Type" := "FS Integration Type"::Service; + FSConnectionSetup.Modify(false); + + // [GIVEN] Archive Service Orders is disabled + InitServiceManagementSetup(true, false, false); + + // [GIVEN] Existing Service Header + LibraryService.CreateServiceDocumentForCustomerNo(ServiceHeader, ServiceHeader."Document Type"::Order, LibrarySales.CreateCustomerNo()); + + // [WHEN] Archive Service Order. + FSIntegrationTestLibrary.ArchiveServiceOrder(ServiceHeader, ArchivedServiceOrders); + + // [THEN] Version should not increase + ServiceHeader.Get(ServiceHeader."Document Type"::Order, ServiceHeader."No."); + ServiceHeader.CalcFields("No. of Archived Versions"); + Assert.AreEqual(0, ServiceHeader."No. of Archived Versions", 'Record should not be marked as archived.'); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure ArchiveServiceOrderMultiple() + var + FSConnectionSetup: Record "FS Connection Setup"; + ServiceHeader: Record "Service Header"; + ArchivedServiceOrders: List of [Code[20]]; + I: Integer; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] Service Header becomes archived multiple times. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + FSConnectionSetup.Get(); + FSConnectionSetup."Integration Type" := "FS Integration Type"::Service; + FSConnectionSetup.Modify(false); + + // [GIVEN] Archive Service Orders is enabled + InitServiceManagementSetup(true, true, false); + + // [GIVEN] Existing Service Header + LibraryService.CreateServiceDocumentForCustomerNo(ServiceHeader, ServiceHeader."Document Type"::Order, LibrarySales.CreateCustomerNo()); + + // [WHEN] Archive Service Order. + for I := 1 to 5 do + FSIntegrationTestLibrary.ArchiveServiceOrder(ServiceHeader, ArchivedServiceOrders); + + // [THEN] Version should increase + ServiceHeader.Get(ServiceHeader."Document Type"::Order, ServiceHeader."No."); + ServiceHeader.CalcFields("No. of Archived Versions"); + Assert.AreEqual(1, ServiceHeader."No. of Archived Versions", 'Record should be marked as archived.'); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure UpdateWorkOrderProduct() + var + ServiceLineArchive: Record "Service Line Archive"; + FSWorkOrderProduct: Record "FS Work Order Product"; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] Archive Service Orders transfer to FS. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + + // [GIVEN] Existing Service Line Archive. + ServiceLineArchive."Qty. to Ship" := 1; + ServiceLineArchive."Quantity Shipped" := 2; + ServiceLineArchive."Qty. to Invoice" := 3; + ServiceLineArchive."Quantity Invoiced" := 4; + ServiceLineArchive."Qty. to Consume" := 5; + ServiceLineArchive."Quantity Consumed" := 6; + + // [GIVEN] ExistingWork Order Product. + FSWorkOrderProduct.Insert(); + + // [WHEN] Update quantities on booking lines that are not posted in BC. + FSIntegrationTestLibrary.UpdateWorkOrderProduct(ServiceLineArchive, FSWorkORderProduct); + + // [THEN] Quantities should be updated accordingly. + Assert.AreEqual(ServiceLineArchive."Qty. to Ship" + ServiceLineArchive."Quantity Shipped", FSWorkOrderProduct.QuantityShipped, 'Quantity should be ' + Format(ServiceLineArchive."Qty. to Ship" + ServiceLineArchive."Quantity Shipped")); + Assert.AreEqual(ServiceLineArchive."Qty. to Invoice" + ServiceLineArchive."Quantity Invoiced", FSWorkOrderProduct.QuantityInvoiced, 'Qty. to Invoice should be ' + Format(ServiceLineArchive."Qty. to Invoice" + ServiceLineArchive."Quantity Invoiced")); + Assert.AreEqual(ServiceLineArchive."Qty. to Consume" + ServiceLineArchive."Quantity Consumed", FSWorkOrderProduct.QuantityConsumed, 'Qty. to Consume should be ' + Format(ServiceLineArchive."Qty. to Consume" + ServiceLineArchive."Quantity Consumed")); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure UpdateWorkOrderService() + var + ServiceLineArchive: Record "Service Line Archive"; + FSWorkOrderService: Record "FS Work Order Service"; + begin + // [FEATURE] [UI] Service Order Integration + // [SCENARIO] Archive Service Orders transfer to FS. + // [GIVEN] FS Connection Setup, where "Is Enabled" = Yes. + Initialize(); + InitSetup(true, ''); + + // [GIVEN] Existing Service Line Archive. + ServiceLineArchive."Qty. to Ship" := 1; + ServiceLineArchive."Quantity Shipped" := 2; + ServiceLineArchive."Qty. to Invoice" := 3; + ServiceLineArchive."Quantity Invoiced" := 4; + ServiceLineArchive."Qty. to Consume" := 5; + ServiceLineArchive."Quantity Consumed" := 6; + + // [GIVEN] ExistingWork Order Service. + FSWorkOrderService.Insert(); + + // [WHEN] Update quantities on booking lines that are not posted in BC. + FSIntegrationTestLibrary.UpdateWorkOrderService(ServiceLineArchive, FSWorkOrderService); + + // [THEN] Quantities should be updated accordingly. + Assert.AreEqual((ServiceLineArchive."Qty. to Ship" + ServiceLineArchive."Quantity Shipped") * 60, FSWorkOrderService.DurationShipped, 'Duration should be ' + Format(ServiceLineArchive."Qty. to Ship" + ServiceLineArchive."Quantity Shipped")); + Assert.AreEqual((ServiceLineArchive."Qty. to Invoice" + ServiceLineArchive."Quantity Invoiced") * 60, FSWorkOrderService.DurationInvoiced, 'Duration Invoiced should be ' + Format(ServiceLineArchive."Qty. to Invoice" + ServiceLineArchive."Quantity Invoiced")); + Assert.AreEqual((ServiceLineArchive."Qty. to Consume" + ServiceLineArchive."Quantity Consumed") * 60, FSWorkOrderService.DurationConsumed, 'Duration Consumed should be ' + Format(ServiceLineArchive."Qty. to Consume" + ServiceLineArchive."Quantity Consumed")); + end; + local procedure Initialize() var AssistedSetupTestLibrary: Codeunit "Assisted Setup Test Library"; @@ -674,6 +1706,37 @@ codeunit 139204 "FS Integration Test" FSIntegrationTestLibrary.RegisterConnection(FSConnectionSetup); end; + procedure InitServiceManagementSetup(ManualNoSeries: Boolean; ArchiveOrdersEnabled: Boolean; OneServiceItemLinePerOrder: Boolean) + var + NoSeries: Record "No. Series"; + NoSeriesLine: Record "No. Series Line"; + + ServiceMgtSetup: Record "Service Mgt. Setup"; + NewNoSeries: Code[20]; + begin + NewNoSeries := 'ServiceOrder'; + + // create new No. Series + NoSeries.Code := NewNoSeries; + NoSeries."Manual Nos." := ManualNoSeries; + NoSeries."Default Nos." := true; + NoSeries.Insert(true); + + // create new No. Series Line + NoSeriesLine."Series Code" := NoSeries.Code; + NoSeriesLine."Starting Date" := 20100101D; + NoSeriesLine."Starting No." := '00001'; + NoSeriesLine."Increment-by No." := 1; + NoSeriesLine.Insert(true); + + // update ServiceMgtSetup record + ServiceMgtSetup.Get(); + ServiceMgtSetup."Service Order Nos." := NewNoSeries; + ServiceMgtSetup."Archive Orders" := ArchiveOrdersEnabled; + ServiceMgtSetup."One Service Item Line/Order" := OneServiceItemLinePerOrder; + ServiceMgtSetup.Modify(true); + end; + local procedure InsertJobQueueEntries() var JobQueueEntry: Record "Job Queue Entry";