Codeunit 5348 CRM Quote to Sales Quote, source in 29

Source29

src/Layers/W1/BaseApp/Integration/D365Sales/CRMQuotetoSalesQuote.Codeunit.al719 lines, Copyright (c) Microsoft Corporation. MIT

// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------
namespace Microsoft.Integration.D365Sales;

using Microsoft.Integration.Dataverse;
using Microsoft.Integration.SyncEngine;
using Microsoft.Inventory.Item;
using Microsoft.Projects.Resources.Resource;
using Microsoft.Sales.Customer;
using Microsoft.Sales.Document;
using Microsoft.Sales.Setup;
using Microsoft.Utilities;
using System.Environment.Configuration;
using System.Utilities;

codeunit 5348 "CRM Quote to Sales Quote"
{
    TableNo = "CRM Quote";

    trigger OnRun()
    var
        CDSCompany: Record "CDS Company";
        SalesHeader: Record "Sales Header";
        CDSIntegrationMgt: Codeunit "CDS Integration Mgt.";
        EmptyGuid: Guid;
    begin
        if CDSIntegrationMgt.GetCDSCompany(CDSCompany) then
            if (Rec.CompanyId <> EmptyGuid) and (Rec.CompanyId <> CDSCompany.CompanyId) then
                exit;

        ProcessInNAV(Rec, SalesHeader);
    end;

    var
        CRMProductName: Codeunit "CRM Product Name";
        CRMSynchHelper: Codeunit "CRM Synch. Helper";
        LastSalesLineNo: Integer;

        UnableToFindOrderErr: Label 'Converting the sales quote to a sales order failed.';
        UnableToFindCrmOrderErr: Label 'Unable to find Dynamics 365 Sales order that corresponds to this quote.';
        UnableToFindOrderTelemetryErr: Label 'Converting the sales quote to a sales order failed.', Locked = true;
        CRMOrderFoundButUnsubmittedTelemetryMsg: Label 'Dynamics 365 Sales order %1 corresponding to the quote %2 found, but it is not submitted.', Locked = true;
        OrderCreatedFromQuoteTelemetryTxt: Label 'Converting the sales quote to a sales order succeeded.', Locked = true;
        UnableToFindCrmOrderTelemetryErr: Label 'Unable to find Dynamics 365 Sales order that corresponds to this quote.', Locked = true;
        UpdatedQuoteNoOnExistingOrderTelemetryTxt: Label 'Updated Quote No. on the existing order that corresponds to this quote.', Locked = true;
        CannotCreateSalesQuoteInNAVTxt: Label 'The sales quote cannot be created.';
        CannotFindCRMAccountForQuoteErr: Label 'The %2 account for %2 sales quote %1 does not exist.', Comment = '%1=Dataverse Sales Order Name, %2 - Dataverse service name';
        ItemDoesNotExistErr: Label '%1 The item %2 does not exist.', Comment = '%1= the text: "The sales order cannot be created.", %2=product name';
        ItemUnitOfMeasureDoesNotExistErr: Label '%1 The item unit of measure %2 does not exist.', Comment = '%1= the text: "The sales order cannot be created.", %2=item unit of measure name';
        ResourceUnitOfMeasureDoesNotExistErr: Label '%1 The resource unit of measure %2 does not exist.', Comment = '%1= the text: "The sales order cannot be created.", %2=resource unit of measure name';
        NoCustomerErr: Label '%1 There is no potential customer defined on the %3 sales quote %2.', Comment = '%1= the text: "The sales quote cannot be created.", %2=sales order title, %3 - Dataverse service name';
        NotCoupledCustomerErr: Label '%1 There is no customer coupled to %3 account %2.', Comment = '%1= the text: "The sales quote cannot be created.", %2=account name, %3 - Dataverse service name';
        NotCoupledCRMProductErr: Label '%1 The %3 product %2 is not coupled to an item.', Comment = '%1= the text: "The sales quote cannot be created.", %2=product name, %3 - Dataverse service name';
        NotCoupledCRMResourceErr: Label '%1 The %3 resource %2 is not coupled to a resource.', Comment = '%1= the text: "The sales quote cannot be created.", %2=resource name, %3 - Dataverse service name';
        NotCoupledCRMUomErr: Label '%1 The %3 unit %2 is not coupled to a unit of measure.', Comment = '%1= the text: "The sales quote cannot be created.", %2=unit name, %3 - Dataverse service name';
        AccountNotCustomerErr: Label '%1 The selected type of the %2 %3 account is not customer.', Comment = '%1= the text: "The sales order cannot be created.", %2=account name, %3=Dataverse service name';
        AccountNotCustomerTelemetryMsg: Label '%1 The selected type of the %2 %3 account is not customer.', Locked = true;
        ResourceDoesNotExistErr: Label '%1 The resource %2 does not exist.', Comment = '%1= the text: "The sales quote cannot be created.", %2=product name';
        UnexpectedProductTypeErr: Label '%1 Unexpected value of product type code for product %2. The supported values are: sales inventory, services.', Comment = '%1= the text: "The sales quote cannot be created.", %2=product name';
        MissingWriteInProductNoErr: Label '%1 %2 %3 contains a write-in product. You must choose the default write-in product in Sales & Receivables Setup window.', Comment = '%1 - Dataverse service name,%2 - document type (order or quote), %3 - document number';
        MisingWriteInProductTelemetryMsg: Label 'The user is missing a default write-in product when creating a sales quote from a %1 quote.', Locked = true;
        CrmTelemetryCategoryTok: Label 'AL CRM Integration', Locked = true;
        SuccessfullyCoupledSalesQuoteTelemetryMsg: Label 'The user successfully coupled quote %2 to %1 quote %3.', Locked = true;
        SkippingProcessQuoteConnectionDisabledMsg: Label 'Skipping creation of quote header from %1 quote %2. The %1 integration is not enabled.', Locked = true;
        SuccessfullyAppliedSalesQuoteDiscountsTelemetryMsg: Label 'Successfully applied discounts from %1 quote %3 to quote %2.', Locked = true;
        StartingToApplySalesQuoteDiscountsTelemetryMsg: Label 'Starting to appliy discounts from %1 quote %3 to quote %2.', Locked = true;
        OverwriteCRMDiscountQst: Label 'There is a discount on the %2 quote, which will be overwritten by %1 settings. You will have the possibility to update the discounts directly on the quote, after it is created. Do you want to continue?', Comment = '%1 - product name, %2 - Dataverse service name';

    local procedure ApplyQuoteDiscounts(CRMQuote: Record "CRM Quote"; var SalesHeader: Record "Sales Header")
    var
        SalesCalcDiscountByType: Codeunit "Sales - Calc Discount By Type";
        CRMDiscountAmount: Decimal;
    begin
        // No discounts to apply
        if (CRMQuote.DiscountAmount = 0) and (CRMQuote.DiscountPercentage = 0) then
            exit;

        Session.LogMessage('0000HVK', StrSubstNo(StartingToApplySalesQuoteDiscountsTelemetryMsg, CRMProductName.CDSServiceName(), SalesHeader.SystemId, CRMQuote.QuoteId), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CrmTelemetryCategoryTok);

        // Attempt to set the discount, if NAV general and customer settings allow it
        // Using CRM discounts
        CRMDiscountAmount := CRMQuote.TotalLineItemAmount - CRMQuote.TotalAmountLessFreight;
        SalesCalcDiscountByType.ApplyInvDiscBasedOnAmt(CRMDiscountAmount, SalesHeader);

        // NAV settings (in G/L Setup as well as per-customer discounts) did not allow using the CRM discounts
        // Using NAV discounts
        // But the user will be able to manually update the discounts after the order is created in NAV
        if GuiAllowed() then
            if not HideQuoteDiscountsDialog() then
                if not Confirm(StrSubstNo(OverwriteCRMDiscountQst, PRODUCTNAME.Short(), CRMProductName.CDSServiceName()), true) then
                    Error('');

        Session.LogMessage('0000HVL', StrSubstNo(SuccessfullyAppliedSalesQuoteDiscountsTelemetryMsg, CRMProductName.CDSServiceName(), SalesHeader.SystemId, CRMQuote.QuoteId), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CrmTelemetryCategoryTok);
    end;

    local procedure HideQuoteDiscountsDialog() Hide: Boolean;
    begin
        OnHideQuoteDiscountsDialog(Hide);
    end;

    procedure ProcessInNAV(CRMQuote: Record "CRM Quote"; var SalesHeader: Record "Sales Header"): Boolean
    var
        CRMConnectionSetup: Record "CRM Connection Setup";
    begin
        if not CRMConnectionSetup.IsEnabled() then begin
            Session.LogMessage('0000EU6', StrSubstNo(SkippingProcessQuoteConnectionDisabledMsg, CRMProductName.SHORT(), CRMQuote.QuoteId), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CrmTelemetryCategoryTok);
            exit;
        end;

        if CRMQuote.StateCode = CRMQuote.StateCode::Active then
            exit(ProcessActiveQuote(CRMQuote, SalesHeader));

        exit(ProcessWonQuote(CRMQuote, SalesHeader));
    end;

    local procedure ProcessActiveQuote(CRMQuote: Record "CRM Quote"; var SalesHeader: Record "Sales Header"): Boolean
    var
        CRMIntegrationRecord: Record "CRM Integration Record";
        RevisionedCRMQuote: Record "CRM Quote";
        RecordId: RecordID;
        OpType: Option Create,Update;
    begin
        if CRMIntegrationRecord.FindRecordIDFromID(CRMQuote.QuoteId, DATABASE::"Sales Header", RecordId) then
            exit(false);

        if CRMQuote.RevisionNumber = 0 then
            exit(CreateOrUpdateNAVQuote(CRMQuote, SalesHeader, OpType::Create));

        RevisionedCRMQuote.Reset();
        RevisionedCRMQuote.SetRange(QuoteNumber, CRMQuote.QuoteNumber);
        RevisionedCRMQuote.SetRange(StateCode, RevisionedCRMQuote.StateCode::Closed);
        RevisionedCRMQuote.SetRange(StatusCode, RevisionedCRMQuote.StatusCode::Revised);
        if RevisionedCRMQuote.FindSet() then
            repeat
                if CRMIntegrationRecord.FindRecordIDFromID(RevisionedCRMQuote.QuoteId, DATABASE::"Sales Header", RecordId) then begin
                    GetSalesHeaderByRecordId(RecordId, SalesHeader);
                    CRMIntegrationRecord.Get(RevisionedCRMQuote.QuoteId, SalesHeader.SystemId);
                    CRMIntegrationRecord.Delete(true);
                    exit(CreateOrUpdateNAVQuote(CRMQuote, SalesHeader, OpType::Update));
                end;
            until RevisionedCRMQuote.Next() = 0;

        exit(CreateOrUpdateNAVQuote(CRMQuote, SalesHeader, OpType::Create));
    end;

    local procedure ProcessWonQuote(CRMQuote: Record "CRM Quote"; var SalesHeader: Record "Sales Header"): Boolean
    var
        QuoteCRMIntegrationRecord: Record "CRM Integration Record";
        WonQuoteCRMIntegrationRecord: Record "CRM Integration Record";
        OrderCRMIntegrationRecord: Record "CRM Integration Record";
        CRMSalesOrder: Record "CRM Salesorder";
        OrderSalesHeader: Record "Sales Header";
        ArchiveManagement: Codeunit ArchiveManagement;
        BlankGuid: Guid;
        OpType: Option Create,Update;
        IsOrderCreated: Boolean;
        IsCRMOrderSubmitted: Boolean;
        YourReferenceFilter: Text;
    begin
        if CRMQuote.StateCode = CRMQuote.StateCode::Won then begin
            QuoteCRMIntegrationRecord.Reset();
            QuoteCRMIntegrationRecord.SetRange("CRM ID", CRMQuote.QuoteId);
            if not QuoteCRMIntegrationRecord.FindFirst() then begin
                CreateOrUpdateNAVQuote(CRMQuote, SalesHeader, OpType::Create);
                QuoteCRMIntegrationRecord.Get(CRMQuote.QuoteId, SalesHeader.SystemId)
            end;
            if not WonQuoteCRMIntegrationRecord.Get(CRMQuote.QuoteId, BlankGuid) then begin
                CRMSalesOrder.SetRange(QuoteId, CRMQuote.QuoteId);
                if not CRMSalesOrder.FindFirst() then begin
                    Session.LogMessage('0000D6P', UnableToFindCrmOrderTelemetryErr, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CrmTelemetryCategoryTok);
                    Error(UnableToFindCrmOrderErr);
                end;

                IsOrderCreated := OrderCRMIntegrationRecord.FindByCRMID(CRMSalesOrder.SalesOrderId);
                IsCRMOrderSubmitted := (CRMSalesOrder.StateCode = CRMSalesOrder.StateCode::Submitted);

                if SalesHeader.GetBySystemId(QuoteCRMIntegrationRecord."Integration ID") then begin
                    if not IsOrderCreated then
                        if IsCRMOrderSubmitted then begin
                            CODEUNIT.Run(CODEUNIT::"CRM Sales Order to Sales Order", CRMSalesOrder);
                            YourReferenceFilter := CopyStr(CRMSalesOrder.orderNumber, 1, MaxStrLen(OrderSalesHeader."Your Reference"));
                            OrderSalesHeader.SetRange("Your Reference", YourReferenceFilter);
                            OnBeforeFindOrderSalesHeader(OrderSalesHeader, CRMSalesOrder);
                            if not OrderSalesHeader.FindFirst() then begin
                                Session.LogMessage('0000D6L', UnableToFindOrderTelemetryErr, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CrmTelemetryCategoryTok);
                                Error(UnableToFindOrderErr)
                            end else begin
                                IsOrderCreated := true;
                                Session.LogMessage('0000D6M', OrderCreatedFromQuoteTelemetryTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CrmTelemetryCategoryTok);
                            end
                        end else
                            Session.LogMessage('0000DI3', StrSubstNo(CRMOrderFoundButUnsubmittedTelemetryMsg, CRMQuote.QuoteId, CRMSalesOrder.SalesOrderId), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CrmTelemetryCategoryTok)
                    else
                        OrderSalesHeader.GetBySystemId(OrderCRMIntegrationRecord."Integration ID");

                    if IsOrderCreated then begin
                        if OrderSalesHeader."Quote No." <> SalesHeader."No." then begin
                            OrderSalesHeader."Quote No." := SalesHeader."No.";
                            OrderSalesHeader.Modify();
                            Session.LogMessage('0000D6N', UpdatedQuoteNoOnExistingOrderTelemetryTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CrmTelemetryCategoryTok);
                        end;
                        ArchiveManagement.ArchSalesDocumentNoConfirm(SalesHeader);
                        OnAfterArchSalesDocumentNoConfirm(SalesHeader);
                    end else begin
                        SalesHeader.Status := SalesHeader.Status::Released;
                        SalesHeader.Modify();
                    end;
                end;

                WonQuoteCRMIntegrationRecord.Init();
                WonQuoteCRMIntegrationRecord.Validate("CRM ID", CRMQuote.QuoteId);
                WonQuoteCRMIntegrationRecord.Validate("Integration ID", BlankGuid);
                WonQuoteCRMIntegrationRecord.Insert(true);
            end;
            exit(true)
        end;
        exit(false)
    end;

    local procedure CreateOrUpdateNAVQuote(CRMQuote: Record "CRM Quote"; var SalesHeader: Record "Sales Header"; OpType: Option Create,Update): Boolean
    var
        CRMIntegrationRecord: Record "CRM Integration Record";
    begin
        if IsNullGuid(CRMQuote.QuoteId) then
            exit;

        if OpType = OpType::Update then
            ManageSalesQuoteArchive(SalesHeader);

        CreateOrUpdateSalesQuoteHeader(CRMQuote, SalesHeader, OpType);
        CreateOrUpdateSalesQuoteLines(CRMQuote, SalesHeader);
        CreateOrUpdateSalesQuoteNotes(CRMQuote, SalesHeader);
        ApplyQuoteDiscounts(CRMQuote, SalesHeader);
        CRMIntegrationRecord.CoupleRecordIdToCRMID(SalesHeader.RecordId, CRMQuote.QuoteId);
        OnCreateOrUpdateNAVQuoteOnAfterCoupleRecordIdToCRMID(CRMQuote, SalesHeader);
        if OpType = OpType::Create then
            Session.LogMessage('0000839', StrSubstNo(SuccessfullyCoupledSalesQuoteTelemetryMsg, CRMProductName.CDSServiceName(), SalesHeader.SystemId, CRMQuote.QuoteId), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CrmTelemetryCategoryTok);
        exit(true);
    end;

    local procedure CreateOrUpdateSalesQuoteHeader(CRMQuote: Record "CRM Quote"; var SalesHeader: Record "Sales Header"; OpType: Option Create,Update)
    var
        Customer: Record Customer;
        IntegrationRecordSynch: Codeunit "Integration Record Synch.";
        SourceRecordRef: RecordRef;
        DestinationRecordRef: RecordRef;
        SourceFieldRef: FieldRef;
        DestinationFieldRef: FieldRef;
    begin
        if OpType = OpType::Create then begin
            SalesHeader.Init();
            SalesHeader.Validate("Document Type", SalesHeader."Document Type"::Quote);
            SalesHeader.Validate(Status, SalesHeader.Status::Open);
            SalesHeader.InitInsert();
        end else
            if SalesHeader.Status = SalesHeader.Status::Released then
                SalesHeader.Validate(Status, SalesHeader.Status::Open);

        GetCoupledCustomer(CRMQuote, Customer);
        SalesHeader.Validate("Sell-to Customer No.", Customer."No.");
        SalesHeader.Validate("Your Reference", CopyStr(CRMQuote.QuoteNumber, 1, MaxStrLen(SalesHeader."Your Reference")));
        SalesHeader.Validate("Currency Code", CRMSynchHelper.GetNavCurrencyCode(CRMQuote.TransactionCurrencyId));
        SalesHeader.Validate("Requested Delivery Date", CRMQuote.RequestDeliveryBy);
        CopyBillToInformationIfNotEmpty(CRMQuote, SalesHeader);
        CopyShipToInformationIfNotEmpty(CRMQuote, SalesHeader);
        CopyCRMOptionFields(CRMQuote, SalesHeader);
        SalesHeader.Validate("External Document No.", CopyStr(CRMQuote.Name, 1, MaxStrLen(SalesHeader."External Document No.")));
        SalesHeader.Validate("Quote Valid Until Date", CRMQuote.EffectiveTo);
        SourceRecordRef.GetTable(CRMQuote);
        DestinationRecordRef.GetTable(SalesHeader);
        SourceFieldRef := SourceRecordRef.Field(CRMQuote.FieldNo(Description));
        DestinationFieldRef := DestinationRecordRef.Field(SalesHeader.FieldNo("Work Description"));
        IntegrationRecordSynch.SetTextValue(DestinationFieldRef, IntegrationRecordSynch.GetTextValue(SourceFieldRef));
        DestinationRecordRef.SetTable(SalesHeader);

        OnCreateOrUpdateSalesQuoteHeaderOnBeforeInsertOrModify(CRMQuote, SalesHeader, OpType);
        if OpType = OpType::Create then
            SalesHeader.Insert(true)
        else
            SalesHeader.Modify(true);
    end;

    local procedure CreateOrUpdateSalesQuoteNotes(CRMQuote: Record "CRM Quote"; var SalesHeader: Record "Sales Header")
    var
        CRMAnnotation: Record "CRM Annotation";
    begin
        CRMAnnotation.SetRange(ObjectId, CRMQuote.QuoteId);
        CRMAnnotation.SetRange(IsDocument, false);
        if CRMAnnotation.FindSet() then
            repeat
                CreateNote(SalesHeader, CRMAnnotation);
            until CRMAnnotation.Next() = 0;
    end;

    local procedure CreateOrUpdateSalesQuoteLines(CRMQuote: Record "CRM Quote"; SalesHeader: Record "Sales Header")
    var
        CRMQuotedetail: Record "CRM Quotedetail";
        SalesLine: Record "Sales Line";
    begin
        // If any of the products on the lines are not found in NAV, err
        CRMQuotedetail.SetRange(QuoteId, CRMQuote.QuoteId); // Get all sales quote lines
        CRMQuotedetail.SetCurrentKey(SequenceNumber);
        CRMQuotedetail.Ascending(true);

        if CRMQuotedetail.FindSet() then
            repeat
                InitializeSalesQuoteLine(CRMQuotedetail, SalesHeader, SalesLine);
                SalesLine.Insert(true);
                if SalesLine."Qty. to Assemble to Order" <> 0 then
                    SalesLine.Validate("Qty. to Assemble to Order");
            until CRMQuotedetail.Next() = 0
        else begin
            SalesLine.Validate("Document Type", SalesHeader."Document Type");
            SalesLine.Validate("Document No.", SalesHeader."No.");
        end;

        SalesLine.InsertFreightLine(CRMQuote.FreightAmount);
    end;

    procedure GetCoupledCustomer(CRMQuote: Record "CRM Quote"; var Customer: Record Customer) Result: Boolean
    var
        CRMAccount: Record "CRM Account";
        CRMIntegrationRecord: Record "CRM Integration Record";
        NAVCustomerRecordId: RecordID;
        CRMAccountId: Guid;
        IsHandled: Boolean;
    begin
        IsHandled := false;
        OnBeforeGetCoupledCustomer(CRMQuote, Customer, Result, IsHandled);
        if IsHandled then
            exit(Result);

        if IsNullGuid(CRMQuote.CustomerId) then
            Error(NoCustomerErr, CannotCreateSalesQuoteInNAVTxt, CRMQuote.Description, CRMProductName.CDSServiceName());

        // Get the ID of the CRM Account associated to the sales quote. Works for both CustomerType(s): account, contact
        if not GetCRMAccountOfCRMQuote(CRMQuote, CRMAccount) then
            Error(CannotFindCRMAccountForQuoteErr, CRMQuote.Name, CRMProductName.CDSServiceName());
        CRMAccountId := CRMAccount.AccountId;

        if not CRMIntegrationRecord.FindRecordIDFromID(CRMAccountId, DATABASE::Customer, NAVCustomerRecordId) then
            Error(NotCoupledCustomerErr, CannotCreateSalesQuoteInNAVTxt, CRMAccount.Name, CRMProductName.CDSServiceName());

        exit(Customer.Get(NAVCustomerRecordId));
    end;

    procedure GetCRMAccountOfCRMQuote(CRMQuote: Record "CRM Quote"; var CRMAccount: Record "CRM Account") Result: Boolean
    var
        CRMContact: Record "CRM Contact";
        IsHandled: Boolean;
    begin
        IsHandled := false;
        OnBeforeGetCRMAccountOfCRMQuote(CRMQuote, CRMAccount, Result, IsHandled);
        if IsHandled then
            exit(Result);

        if CRMQuote.CustomerIdType = CRMQuote.CustomerIdType::account then
            if CRMAccount.Get(CRMQuote.CustomerId) then
                if CRMAccount.CustomerTypeCode <> CRMAccount.CustomerTypeCode::Customer then begin
                    Session.LogMessage('0000DMO', StrSubstNo(AccountNotCustomerTelemetryMsg, CannotCreateSalesQuoteInNAVTxt, CRMQuote.CustomerId, CRMProductName.CDSServiceName()), Verbosity::Warning, DataClassification::CustomerContent, TelemetryScope::All, 'Category', CrmTelemetryCategoryTok);
                    Error(AccountNotCustomerErr, CannotCreateSalesQuoteInNAVTxt, CRMAccount.Name, CRMProductName.CDSServiceName());
                end else
                    exit(true);

        if CRMQuote.CustomerIdType = CRMQuote.CustomerIdType::contact then
            if CRMContact.Get(CRMQuote.CustomerId) then
                exit(CRMAccount.Get(CRMContact.ParentCustomerId));
        exit(false);
    end;

    local procedure CopyCRMOptionFields(CRMQuote: Record "CRM Quote"; var SalesHeader: Record "Sales Header")
    var
        CRMAccount: Record "CRM Account";
        CRMOptionMapping: Record "CRM Option Mapping";
    begin
        if CRMOptionMapping.FindRecordID(
             DATABASE::"CRM Account", CRMAccount.FieldNo(Address1_ShippingMethodCodeEnum), CRMQuote.ShippingMethodCodeEnum.AsInteger())
        then
            SalesHeader.Validate(
              "Shipping Agent Code",
              CopyStr(CRMOptionMapping.GetRecordKeyValue(), 1, MaxStrLen(SalesHeader."Shipping Agent Code")));

        if CRMOptionMapping.FindRecordID(
             DATABASE::"CRM Account", CRMAccount.FieldNo(PaymentTermsCodeEnum), CRMQuote.PaymentTermsCodeEnum.AsInteger())
        then
            SalesHeader.Validate(
              "Payment Terms Code",
              CopyStr(CRMOptionMapping.GetRecordKeyValue(), 1, MaxStrLen(SalesHeader."Payment Terms Code")));

        if CRMOptionMapping.FindRecordID(
             DATABASE::"CRM Account", CRMAccount.FieldNo(Address1_FreightTermsCodeEnum), CRMQuote.FreightTermsCodeEnum.AsInteger())
        then
            SalesHeader.Validate(
              "Shipment Method Code",
              CopyStr(CRMOptionMapping.GetRecordKeyValue(), 1, MaxStrLen(SalesHeader."Shipment Method Code")));
    end;

    local procedure CopyBillToInformationIfNotEmpty(CRMQuote: Record "CRM Quote"; var SalesHeader: Record "Sales Header")
    begin
        // If the Bill-To fields in CRM are all empty, then let NAV keep its standard behavior (takes Bill-To from the Customer information)
        if ((CRMQuote.BillTo_Line1 = '') and
            (CRMQuote.BillTo_Line2 = '') and
            (CRMQuote.BillTo_City = '') and
            (CRMQuote.BillTo_PostalCode = '') and
            (CRMQuote.BillTo_Country = '') and
            (CRMQuote.BillTo_StateOrProvince = ''))
        then
            exit;

        SalesHeader.Validate("Bill-to Address", CopyStr(CRMQuote.BillTo_Line1, 1, MaxStrLen(SalesHeader."Bill-to Address")));
        SalesHeader.Validate("Bill-to Address 2", CopyStr(CRMQuote.BillTo_Line2, 1, MaxStrLen(SalesHeader."Bill-to Address 2")));
        SalesHeader.Validate("Bill-to City", CopyStr(CRMQuote.BillTo_City, 1, MaxStrLen(SalesHeader."Bill-to City")));
        SalesHeader.Validate("Bill-to Post Code", CopyStr(CRMQuote.BillTo_PostalCode, 1, MaxStrLen(SalesHeader."Bill-to Post Code")));
        SalesHeader.Validate(
          "Bill-to Country/Region Code", CopyStr(CRMQuote.BillTo_Country, 1, MaxStrLen(SalesHeader."Bill-to Country/Region Code")));
        SalesHeader.Validate("Bill-to County", CopyStr(CRMQuote.BillTo_StateOrProvince, 1, MaxStrLen(SalesHeader."Bill-to County")));
    end;

    local procedure InitNewSalesLine(SalesHeader: Record "Sales Header"; var SalesLine: Record "Sales Line")
    begin
        SalesLine.Init();
        SalesLine.Validate("Document Type", SalesHeader."Document Type");
        SalesLine.Validate("Document No.", SalesHeader."No.");
        LastSalesLineNo := LastSalesLineNo + 10000;
        SalesLine.Validate("Line No.", LastSalesLineNo);
    end;

    local procedure InitializeWriteInQuoteLine(var SalesLine: Record "Sales Line")
    var
        SalesSetup: Record "Sales & Receivables Setup";
    begin
        SalesSetup.Get();
        if SalesSetup."Write-in Product No." = '' then begin
            Session.LogMessage('000083A', StrSubstNo(MisingWriteInProductTelemetryMsg, CRMProductName.CDSServiceName()), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CrmTelemetryCategoryTok);
            Error(MissingWriteInProductNoErr, CRMProductName.CDSServiceName(), SalesLine."Document Type", SalesLine.GetSalesHeader()."Your Reference");
        end;
        SalesSetup.Validate("Write-in Product No.");
        case SalesSetup."Write-in Product Type" of
            SalesSetup."Write-in Product Type"::Item:
                SalesLine.Validate(Type, SalesLine.Type::Item);
            SalesSetup."Write-in Product Type"::Resource:
                SalesLine.Validate(Type, SalesLine.Type::Resource);
        end;
        SalesLine.Validate("No.", SalesSetup."Write-in Product No.");
    end;

    local procedure InitializeSalesQuoteLineFromItem(CRMProduct: Record "CRM Product"; var SalesLine: Record "Sales Line")
    var
        CRMIntegrationRecord: Record "CRM Integration Record";
        Item: Record Item;
        NAVItemRecordID: RecordID;
    begin
        // Attempt to find the coupled item
        if not CRMIntegrationRecord.FindRecordIDFromID(CRMProduct.ProductId, DATABASE::Item, NAVItemRecordID) then
            Error(NotCoupledCRMProductErr, CannotCreateSalesQuoteInNAVTxt, CRMProduct.Name, CRMProductName.CDSServiceName());

        if not Item.Get(NAVItemRecordID) then
            Error(ItemDoesNotExistErr, CannotCreateSalesQuoteInNAVTxt, CRMProduct.ProductNumber);
        SalesLine.Validate(Type, SalesLine.Type::Item);
        SalesLine.Validate("No.", Item."No.");
    end;

    local procedure InitializeSalesQuoteLineFromResource(CRMProduct: Record "CRM Product"; var SalesLine: Record "Sales Line")
    var
        CRMIntegrationRecord: Record "CRM Integration Record";
        Resource: Record Resource;
        NAVResourceRecordID: RecordID;
    begin
        // Attempt to find the coupled resource
        if not CRMIntegrationRecord.FindRecordIDFromID(CRMProduct.ProductId, DATABASE::Resource, NAVResourceRecordID) then
            Error(NotCoupledCRMResourceErr, CannotCreateSalesQuoteInNAVTxt, CRMProduct.Name, CRMProductName.CDSServiceName());

        if not Resource.Get(NAVResourceRecordID) then
            Error(ResourceDoesNotExistErr, CannotCreateSalesQuoteInNAVTxt, CRMProduct.ProductNumber);
        SalesLine.Validate(Type, SalesLine.Type::Resource);
        SalesLine.Validate("No.", Resource."No.");
    end;

    local procedure InitializeSalesQuoteLine(CRMQuotedetail: Record "CRM Quotedetail"; SalesHeader: Record "Sales Header"; var SalesLine: Record "Sales Line")
    var
        CRMProduct: Record "CRM Product";
        CRMIntegrationManagement: Codeunit "CRM Integration Management";
    begin
        InitNewSalesLine(SalesHeader, SalesLine);

        if IsNullGuid(CRMQuotedetail.ProductId) then
            InitializeWriteInQuoteLine(SalesLine)
        else begin
            CRMProduct.Get(CRMQuotedetail.ProductId);
            CRMProduct.TestField(StateCode, CRMProduct.StateCode::Active);
            case CRMProduct.ProductTypeCode of
                CRMProduct.ProductTypeCode::SalesInventory:
                    InitializeSalesQuoteLineFromItem(CRMProduct, SalesLine);
                CRMProduct.ProductTypeCode::Services:
                    InitializeSalesQuoteLineFromResource(CRMProduct, SalesLine);
                else
                    Error(UnexpectedProductTypeErr, CannotCreateSalesQuoteInNAVTxt, CRMProduct.ProductNumber);
            end;
        end;
        SetLineDescription(SalesHeader, SalesLine, CRMQuoteDetail);

        if CRMIntegrationManagement.IsUnitGroupMappingEnabled() then
            UpdateSalesLineUnitOfMeasure(CRMQuotedetail, CRMProduct, SalesLine);

        SalesLine.Validate(Quantity, CRMQuotedetail.Quantity);
        SalesLine.Validate("Unit Price", CRMQuotedetail.PricePerUnit);
        SalesLine.Validate(Amount, CRMQuotedetail.BaseAmount);
        SalesLine.Validate(
          "Line Discount Amount",
          CRMQuotedetail.Quantity * CRMQuotedetail.VolumeDiscountAmount +
          CRMQuotedetail.ManualDiscountAmount);

        OnAfterInitializeSalesQuoteLine(CRMQuotedetail, SalesHeader, SalesLine);
    end;

    local procedure UpdateSalesLineUnitOfMeasure(CRMQuotedetail: Record "CRM Quotedetail"; CRMProduct: Record "CRM Product"; var SalesLine: Record "Sales Line")
    var
        CRMIntegrationRecord: Record "CRM Integration Record";
        ItemUnitOfMeasure: Record "Item Unit of Measure";
        ResourceUnitOfMeasure: Record "Resource Unit of Measure";
        NAVItemUomRecordId: RecordID;
        NAVResourceUomRecordId: RecordID;
    begin
        if IsNullGuid(CRMProduct.ProductId) then
            exit;

        case CRMProduct.ProductTypeCode of
            CRMProduct.ProductTypeCode::SalesInventory:
                begin
                    if not CRMIntegrationRecord.FindRecordIDFromID(CRMQuotedetail.UoMId, Database::"Item Unit of Measure", NAVItemUomRecordId) then
                        Error(NotCoupledCRMUomErr, CannotCreateSalesQuoteInNAVTxt, CRMQuotedetail.UoMIdName, CRMProductName.CDSServiceName());

                    if not ItemUnitOfMeasure.Get(NAVItemUomRecordId) then
                        Error(ItemUnitOfMeasureDoesNotExistErr, CannotCreateSalesQuoteInNAVTxt, CRMQuotedetail.UoMIdName);

                    SalesLine.Validate("Unit of Measure Code", ItemUnitOfMeasure.Code);
                end;
            CRMProduct.ProductTypeCode::Services:
                begin
                    if not CRMIntegrationRecord.FindRecordIDFromID(CRMQuotedetail.UoMId, Database::"Resource Unit of Measure", NAVResourceUomRecordId) then
                        Error(NotCoupledCRMUomErr, CannotCreateSalesQuoteInNAVTxt, CRMQuotedetail.UoMIdName, CRMProductName.CDSServiceName());

                    if not ResourceUnitOfMeasure.Get(NAVResourceUomRecordId) then
                        Error(ResourceUnitOfMeasureDoesNotExistErr, CannotCreateSalesQuoteInNAVTxt, CRMQuotedetail.UoMIdName);

                    SalesLine.Validate("Unit of Measure Code", ResourceUnitOfMeasure.Code);
                end;
        end;
    end;

    local procedure CopyShipToInformationIfNotEmpty(CRMQuote: Record "CRM Quote"; var SalesHeader: Record "Sales Header")
    begin
        // If the Ship-To fields in CRM are all empty, then let NAV keep its standard behavior (takes Bill-To from the Customer information)
        if ((CRMQuote.ShipTo_Line1 = '') and
            (CRMQuote.ShipTo_Line2 = '') and
            (CRMQuote.ShipTo_City = '') and
            (CRMQuote.ShipTo_PostalCode = '') and
            (CRMQuote.ShipTo_Country = '') and
            (CRMQuote.ShipTo_StateOrProvince = ''))
        then
            exit;

        SalesHeader.Validate("Ship-to Address", CopyStr(CRMQuote.ShipTo_Line1, 1, MaxStrLen(SalesHeader."Ship-to Address")));
        SalesHeader.Validate("Ship-to Address 2", CopyStr(CRMQuote.ShipTo_Line2, 1, MaxStrLen(SalesHeader."Ship-to Address 2")));
        SalesHeader.Validate("Ship-to City", CopyStr(CRMQuote.ShipTo_City, 1, MaxStrLen(SalesHeader."Ship-to City")));
        SalesHeader.Validate("Ship-to Post Code", CopyStr(CRMQuote.ShipTo_PostalCode, 1, MaxStrLen(SalesHeader."Ship-to Post Code")));
        SalesHeader.Validate(
          "Ship-to Country/Region Code", CopyStr(CRMQuote.ShipTo_Country, 1, MaxStrLen(SalesHeader."Ship-to Country/Region Code")));
        SalesHeader.Validate("Ship-to County", CopyStr(CRMQuote.ShipTo_StateOrProvince, 1, MaxStrLen(SalesHeader."Ship-to County")));
        OnAfterCopyShipToInformationIfNotEmpty(CRMQuote, SalesHeader);
    end;

    local procedure CreateNote(SalesHeader: Record "Sales Header"; CRMAnnotation: Record "CRM Annotation")
    var
        RecordLink: Record "Record Link";
        CRMAnnotationCoupling: Record "CRM Annotation Coupling";
        RecordLinkManagement: Codeunit "Record Link Management";
        InStream: InStream;
        AnnotationText: Text;
    begin
        RecordLink."Record ID" := SalesHeader.RecordId;
        RecordLink.Type := RecordLink.Type::Note;
        RecordLink.Description := CRMAnnotation.Subject;
        CRMAnnotation.CalcFields(NoteText);

        CRMAnnotation.NoteText.CreateInStream(InStream, TEXTENCODING::UTF16);
        InStream.Read(AnnotationText);

        RecordLinkManagement.WriteNote(RecordLink, CRMAnnotationCoupling.ExtractNoteText(AnnotationText));
        RecordLink.Created := CRMAnnotation.CreatedOn;
        RecordLink.Company := CompanyName;
        RecordLink.Insert(true);
    end;

    local procedure SetLineDescription(SalesHeader: Record "Sales Header"; var SalesLine: Record "Sales Line"; var CRMQuoteDetail: Record "CRM Quotedetail");
    var
        SalesReceivablesSetup: Record "Sales & Receivables Setup";
        LineDescriptionInStream: InStream;
        ExtendedDescription: Text;
        CRMQuoteLineDescription: Text;
    begin
        CRMQuotedetail.CalcFields(Description);
        CRMQuotedetail.Description.CreateInStream(LineDescriptionInStream, TEXTENCODING::UTF16);
        LineDescriptionInStream.ReadText(CRMQuoteLineDescription);
        if CRMQuoteLineDescription = '' then
            CRMQuoteLineDescription := CRMQuotedetail.ProductDescription;
        ExtendedDescription := CRMQuoteLineDescription;

        // in case of write-in product - write the description directly in the main sales line description
        if SalesReceivablesSetup.Get() then
            if SalesLine."No." = SalesReceivablesSetup."Write-in Product No." then begin
                SalesLine.Description := CopyStr(CRMQuoteLineDescription, 1, MaxStrLen(SalesLine.Description));
                if StrLen(CRMQuoteLineDescription) > MaxStrLen(SalesLine.Description) then
                    ExtendedDescription := CopyStr(CRMQuoteLineDescription, MaxStrLen(SalesLine.Description) + 1)
                else
                    ExtendedDescription := '';
            end;

        // in case of inventory item - write the item name in the main line and create extended lines with the extended description
        CreateExtendedDescriptionQuoteLines(SalesHeader, ExtendedDescription, SalesLine."Line No.");

        // in case of line descriptions with multple lines, add all lines of the line descirption
        while not LineDescriptionInStream.EOS() do begin
            LineDescriptionInStream.ReadText(ExtendedDescription);
            CreateExtendedDescriptionQuoteLines(SalesHeader, ExtendedDescription, SalesLine."Line No.");
        end;
    end;

    procedure CreateExtendedDescriptionQuoteLines(SalesHeader: Record "Sales Header"; FullDescription: Text; QuoteLineNo: Integer)
    var
        SalesLine: Record "Sales Line";
    begin
        while StrLen(FullDescription) > 0 do begin
            InitNewSalesLine(SalesHeader, SalesLine);

            SalesLine.Validate(Description, CopyStr(FullDescription, 1, MaxStrLen(SalesLine.Description)));
            SalesLine."Attached to Line No." := QuoteLineNo;
            SalesLine.Insert();
            FullDescription := CopyStr(FullDescription, MaxStrLen(SalesLine.Description) + 1);
        end;
    end;

    [Scope('OnPrem')]
    procedure ManageSalesQuoteArchive(var SalesHeader: Record "Sales Header")
    var
        SalesLine: Record "Sales Line";
        RecordLink: Record "Record Link";
        ArchiveManagement: Codeunit ArchiveManagement;
    begin
        ArchiveManagement.ArchSalesDocumentNoConfirm(SalesHeader);
        SalesLine.SetRange("Document Type", SalesLine."Document Type"::Quote);
        SalesLine.SetRange("Document No.", SalesHeader."No.");
        SalesLine.DeleteAll();
        RecordLink.SetRange("Record ID", SalesHeader.RecordId);
        RecordLink.SetRange(Type, RecordLink.Type::Note);
        RecordLink.DeleteAll();
        if SalesHeader.Find() then begin
            SalesHeader.Status := SalesHeader.Status::Released;
            SalesHeader.Modify();
        end;
    end;

    local procedure GetSalesHeaderByRecordId(RecordID: RecordID; var SalesHeader: Record "Sales Header")
    var
        RecRef: RecordRef;
    begin
        RecRef := RecordID.GetRecord();
        RecRef.SetTable(SalesHeader);
        SalesHeader.Find();
    end;

    [IntegrationEvent(false, false)]
    local procedure OnAfterInitializeSalesQuoteLine(CRMQuotedetail: Record "CRM Quotedetail"; SalesHeader: Record "Sales Header"; var SalesLine: Record "Sales Line")
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnAfterCopyShipToInformationIfNotEmpty(CRMQuote: Record "CRM Quote"; var SalesHeader: Record "Sales Header")
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnCreateOrUpdateNAVQuoteOnAfterCoupleRecordIdToCRMID(CRMQuote: Record "CRM Quote"; SalesHeader: Record "Sales Header")
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnCreateOrUpdateSalesQuoteHeaderOnBeforeInsertOrModify(CRMQuote: Record "CRM Quote"; var SalesHeader: Record "Sales Header"; OpType: Option)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnHideQuoteDiscountsDialog(var Hide: Boolean)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeGetCRMAccountOfCRMQuote(CRMQuote: Record "CRM Quote"; var CRMAccount: Record "CRM Account"; var Result: Boolean; var IsHandled: Boolean)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnAfterArchSalesDocumentNoConfirm(var SalesHeader: Record "Sales Header")
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeFindOrderSalesHeader(var OrderSalesHeader: Record "Sales Header"; CRMSalesorder: Record "CRM Salesorder")
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeGetCoupledCustomer(CRMQuote: Record "CRM Quote"; var Customer: Record Customer; var Result: Boolean; var IsHandled: Boolean)
    begin
    end;
}