Codeunit 60 Sales-Calc. Discount, source in 29

Source29

src/Layers/W1/BaseApp/Sales/Document/SalesCalcDiscount.Codeunit.al439 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.Sales.Document;

using Microsoft.Finance.Currency;
using Microsoft.Finance.VAT.Calculation;
using Microsoft.Sales.Customer;
using Microsoft.Sales.Pricing;
using Microsoft.Sales.Setup;
using Microsoft.Utilities;

/// <summary>
/// Calculates invoice discounts for sales documents based on customer discount settings.
/// </summary>
codeunit 60 "Sales-Calc. Discount"
{
    Permissions = tabledata "Sales Header" = rm,
                  tabledata "Sales Line" = rm;
    TableNo = "Sales Line";

    trigger OnRun()
    var
        IsHandled: Boolean;
    begin
        SalesLine.Copy(Rec);

        TempSalesHeader.Get(Rec."Document Type", Rec."Document No.");
        UpdateHeader := true;

        IsHandled := false;
        OnRunOnBeforeCalculateInvoiceDiscount(SalesLine, TempSalesHeader, TempSalesLine, UpdateHeader, IsHandled);
        if not IsHandled then
            CalculateInvoiceDiscount(TempSalesHeader, TempSalesLine);

        if Rec.Get(SalesLine."Document Type", SalesLine."Document No.", SalesLine."Line No.") then;
    end;

    var
        TempSalesHeader: Record "Sales Header";
        SalesLine: Record "Sales Line";
        TempSalesLine: Record "Sales Line";
        CustInvDisc: Record "Cust. Invoice Disc.";
        CustPostingGr: Record "Customer Posting Group";
        Currency: Record Currency;
        InvDiscBase: Decimal;
        ChargeBase: Decimal;
        CurrencyDate: Date;
        UpdateHeader: Boolean;
        CustInvDiscFound: Boolean;

#pragma warning disable AA0074
        Text000: Label 'Service Charge';
#pragma warning restore AA0074

    local procedure CalculateInvoiceDiscount(var SalesHeader: Record "Sales Header"; var SalesLine2: Record "Sales Line")
    var
        TempVATAmountLine: Record "VAT Amount Line" temporary;
        SalesSetup: Record "Sales & Receivables Setup";
        TempServiceChargeLine: Record "Sales Line" temporary;
        SalesCalcDiscountByType: Codeunit "Sales - Calc Discount By Type";
        DiscountNotificationMgt: Codeunit "Discount Notification Mgt.";
        ShouldGetCustInvDisc: Boolean;
        IsHandled: Boolean;
    begin
        OnBeforeCalculateInvoiceDiscount(SalesHeader, SalesLine2, UpdateHeader);

        SalesSetup.Get();
        if UpdateHeader then
            SalesHeader.Find(); // To ensure we have the latest - otherwise update fails.

        IsHandled := false;
        OnBeforeCalcSalesDiscount(SalesHeader, IsHandled, SalesLine2, UpdateHeader);
        if IsHandled then
            exit;

        SalesLine.LockTable();
        SalesHeader.TestField("Customer Posting Group");
        CustPostingGr.Get(SalesHeader."Customer Posting Group");

        SalesLine2.Reset();
        SalesLine2.SetRange("Document Type", SalesLine."Document Type");
        SalesLine2.SetRange("Document No.", SalesLine."Document No.");
        SalesLine2.SetRange("System-Created Entry", true);
        SalesLine2.SetRange(Type, SalesLine2.Type::"G/L Account");
        SalesLine2.SetRange("No.", CustPostingGr."Service Charge Acc.");
        SalesLine2.SetLoadFields("Unit Price", "Shipment No.", "Qty. Shipped Not Invoiced");
        if SalesLine2.FindSet(true) then
            repeat
                SalesLine2."Unit Price" := 0;
                SalesLine2.Modify();
                TempServiceChargeLine := SalesLine2;
                TempServiceChargeLine.Insert();
            until SalesLine2.Next() = 0;

        SalesLine2.Reset();
        SalesLine2.SetLoadFields();
        SalesLine2.SetRange("Document Type", SalesLine."Document Type");
        SalesLine2.SetRange("Document No.", SalesLine."Document No.");
        SalesLine2.SetFilter(Type, '<>0');
        OnCalculateInvoiceDiscountOnBeforeSalesLine2FindFirst(SalesLine2);
        if SalesLine2.FindFirst() then;
        SalesLine2.CalcVATAmountLines(0, SalesHeader, SalesLine2, TempVATAmountLine);
        InvDiscBase :=
          TempVATAmountLine.GetTotalInvDiscBaseAmount(
            SalesHeader."Prices Including VAT", SalesHeader."Currency Code");
        ChargeBase :=
          TempVATAmountLine.GetTotalLineAmount(
            SalesHeader."Prices Including VAT", SalesHeader."Currency Code");

        if UpdateHeader then
            SalesHeader.Modify();

        if SalesHeader."Posting Date" = 0D then
            CurrencyDate := WorkDate()
        else
            CurrencyDate := SalesHeader."Posting Date";

        CustInvDiscFound := CustInvDisc.GetRecord(SalesHeader."Invoice Disc. Code", SalesHeader."Currency Code", CurrencyDate, ChargeBase);

        OnCalculateInvoiceDiscountOnBeforeCheckCustInvDiscServiceCharge(CustInvDisc, SalesHeader, CurrencyDate, ChargeBase, CustInvDiscFound);
        if CustInvDiscFound and (CustInvDisc."Service Charge" <> 0) then begin
            OnCalculateInvoiceDiscountOnBeforeCurrencyInitialize(CustPostingGr);
            Currency.Initialize(SalesHeader."Currency Code");
            if not UpdateHeader then
                SalesLine2.SetSalesHeader(SalesHeader);
            if not TempServiceChargeLine.IsEmpty() then begin
                TempServiceChargeLine.FindLast();
                SalesLine2.Get(SalesLine."Document Type", SalesLine."Document No.", TempServiceChargeLine."Line No.");
                SetSalesLineServiceCharge(SalesHeader, SalesLine2);
                SalesLine2.Modify();
            end else begin
                IsHandled := false;
                OnCalculateInvoiceDiscountOnBeforeUpdateSalesLine2(SalesHeader, SalesLine2, UpdateHeader, CustInvDisc, IsHandled);
                if not IsHandled then begin
                    SalesLine2.Reset();
                    SalesLine2.SetRange("Document Type", SalesLine."Document Type");
                    SalesLine2.SetRange("Document No.", SalesLine."Document No.");
                    SalesLine2.FindLast();
                    SalesLine2.Init();
                    if not UpdateHeader then
                        SalesLine2.SetSalesHeader(SalesHeader);
                    SalesLine2."Line No." := SalesLine2."Line No." + 10000;
                    SalesLine2."System-Created Entry" := true;
                    SalesLine2.Type := SalesLine2.Type::"G/L Account";
                    SalesLine2.Validate("No.", CustPostingGr.GetServiceChargeAccount());
                    SalesLine2.Description := Text000;
                    SalesLine2.Validate(Quantity, 1);

                    OnAfterValidateSalesLine2Quantity(SalesHeader, SalesLine2, CustInvDisc);

                    if SalesLine2."Document Type" in
                        [SalesLine2."Document Type"::"Return Order", SalesLine2."Document Type"::"Credit Memo"]
                    then
                        SalesLine2.Validate("Return Qty. to Receive", SalesLine2.Quantity)
                    else
                        SalesLine2.Validate("Qty. to Ship", SalesLine2.Quantity);
                    SetSalesLineServiceCharge(SalesHeader, SalesLine2);
                    OnCalculateInvoiceDiscountOnBeforeSalesLineInsert(SalesLine2, SalesHeader);
                    SalesLine2.Insert();
                end;
            end;
            SalesLine2.CalcVATAmountLines(0, SalesHeader, SalesLine2, TempVATAmountLine);
        end else
            if TempServiceChargeLine.FindSet(false) then
                repeat
                    if (TempServiceChargeLine."Shipment No." = '') and (TempServiceChargeLine."Qty. Shipped Not Invoiced" = 0) then begin
                        SalesLine2.Get(SalesLine."Document Type", SalesLine."Document No.", TempServiceChargeLine."Line No.");
                        IsHandled := false;
                        OnCalculateInvoiceDiscountOnBeforeSalesLine2DeleteTrue(UpdateHeader, SalesLine2, IsHandled);
                        if not IsHandled then
                            SalesLine2.Delete(true);
                    end;
                until TempServiceChargeLine.Next() = 0;

        IsHandled := false;
        OnCalculateInvoiceDiscountOnBeforeCustInvDiscRecExists(SalesHeader, SalesLine2, UpdateHeader, InvDiscBase, ChargeBase, TempVATAmountLine, IsHandled);
        if IsHandled then
            exit;

        if CustInvDiscRecExists(SalesHeader."Invoice Disc. Code") then begin
            ShouldGetCustInvDisc := InvDiscBase <> ChargeBase;
            OnAfterCustInvDiscRecExists(SalesHeader, CustInvDisc, InvDiscBase, ChargeBase, ShouldGetCustInvDisc);
            if ShouldGetCustInvDisc then
                CustInvDisc.GetRecord(
                  SalesHeader."Invoice Disc. Code", SalesHeader."Currency Code", CurrencyDate, InvDiscBase);

            DiscountNotificationMgt.NotifyAboutMissingSetup(
              SalesSetup.RecordId, SalesHeader."Gen. Bus. Posting Group", SalesLine2."Gen. Prod. Posting Group",
              SalesSetup."Discount Posting", SalesSetup."Discount Posting"::"Line Discounts");

            UpdateSalesHeaderInvoiceDiscount(SalesHeader, TempVATAmountLine, SalesSetup."Calc. Inv. Disc. per VAT ID");

            SalesLine2.SetSalesHeader(SalesHeader);
            SalesLine2.UpdateVATOnLines(0, SalesHeader, SalesLine2, TempVATAmountLine);
            UpdatePrepmtLineAmount(SalesHeader);
        end;

        SalesCalcDiscountByType.ResetRecalculateInvoiceDisc(SalesHeader);
        OnAfterCalcSalesDiscount(SalesHeader, TempVATAmountLine, SalesLine2);
    end;

    local procedure UpdateSalesHeaderInvoiceDiscount(var SalesHeader: Record "Sales Header"; var TempVATAmountLine: Record "VAT Amount Line" temporary; CalcInvDiscPerVATID: Boolean)
    var
        IsHandled: Boolean;
    begin
        IsHandled := false;
        OnBeforeUpdateSalesHeaderInvoiceDiscount(CustInvDisc, SalesHeader, TempVATAmountLine, UpdateHeader, IsHandled);
        if IsHandled then
            exit;

        SalesHeader."Invoice Discount Calculation" := SalesHeader."Invoice Discount Calculation"::"%";
        SalesHeader."Invoice Discount Value" := CustInvDisc."Discount %";
        if UpdateHeader then
            SalesHeader.Modify();

        TempVATAmountLine.SetInvoiceDiscountPercent(
          CustInvDisc."Discount %", SalesHeader."Currency Code",
          SalesHeader."Prices Including VAT", CalcInvDiscPerVATID,
          SalesHeader."VAT Base Discount %");
    end;

    local procedure SetSalesLineServiceCharge(SalesHeader: Record "Sales Header"; var SalesLine: Record "Sales Line")
    var
        IsHandled: Boolean;
    begin
        IsHandled := false;
        OnBeforeSetSalesLineServiceCharge(SalesHeader, SalesLine, CustInvDisc, IsHandled);
        if IsHandled then
            exit;

        if SalesHeader."Prices Including VAT" then
            SalesLine.Validate(
                "Unit Price",
                Round((1 + SalesLine."VAT %" / 100) * CustInvDisc."Service Charge", Currency."Unit-Amount Rounding Precision"))
        else
            SalesLine.Validate("Unit Price", CustInvDisc."Service Charge");
    end;

    /// <summary>
    /// Checks whether a customer invoice discount record exists for the specified code.
    /// </summary>
    /// <param name="InvDiscCode">The invoice discount code to check.</param>
    /// <returns>True if a customer invoice discount record exists; otherwise, false.</returns>
    procedure CustInvDiscRecExists(InvDiscCode: Code[20]) Result: Boolean
    var
        CustInvDisc: Record "Cust. Invoice Disc.";
        IsHandled: Boolean;
    begin
        IsHandled := false;
        OnBeforeCustInvDiscRecExists(InvDiscCode, Result, IsHandled);
        if IsHandled then
            exit(Result);

        CustInvDisc.SetRange(Code, InvDiscCode);
        exit(CustInvDisc.FindFirst());
    end;

    /// <summary>
    /// Calculates invoice discount using the provided sales header without updating it.
    /// </summary>
    /// <param name="TempSalesHeader">The sales header to use for discount calculation.</param>
    /// <param name="TempSalesLine">The sales lines to calculate discounts for.</param>
    procedure CalculateWithSalesHeader(var TempSalesHeader: Record "Sales Header"; var TempSalesLine: Record "Sales Line")
    var
        FilterSalesLine: Record "Sales Line";
        IsHandled: Boolean;
    begin
        IsHandled := false;
        OnBeforeCalculateWithSalesHeader(SalesLine, TempSalesHeader, TempSalesLine, UpdateHeader, IsHandled); // <-- NEW EVENT
        if IsHandled then
            exit;

        FilterSalesLine.Copy(TempSalesLine);
        SalesLine := TempSalesLine;

        UpdateHeader := false;
        CalculateInvoiceDiscount(TempSalesHeader, TempSalesLine);

        TempSalesLine.Copy(FilterSalesLine);
    end;

    /// <summary>
    /// Calculates and applies invoice discount to a single sales line.
    /// </summary>
    /// <param name="SalesLineToUpdate">The sales line to calculate and update with invoice discount.</param>
    procedure CalculateInvoiceDiscountOnLine(var SalesLineToUpdate: Record "Sales Line")
    begin
        SalesLine.Copy(SalesLineToUpdate);

        TempSalesHeader.Get(SalesLine."Document Type", SalesLine."Document No.");
        UpdateHeader := false;
        CalculateInvoiceDiscount(TempSalesHeader, SalesLine);

        if SalesLineToUpdate.Get(SalesLineToUpdate."Document Type", SalesLineToUpdate."Document No.", SalesLineToUpdate."Line No.") then;
    end;

    /// <summary>
    /// Calculates invoice discount for a sales header if automatic calculation is enabled.
    /// </summary>
    /// <param name="TempSalesHeader">The sales header to calculate invoice discount for.</param>
    procedure CalculateIncDiscForHeader(var TempSalesHeader: Record "Sales Header")
    var
        SalesSetup: Record "Sales & Receivables Setup";
        IsHandled: Boolean;
    begin
        IsHandled := false;
        OnBeforeCalculateIncDiscForHeader(TempSalesHeader, IsHandled, SalesLine, TempSalesLine, UpdateHeader);
        if IsHandled then
            exit;

        SalesSetup.Get();
        if not SalesSetup."Calc. Inv. Discount" then
            exit;

        SalesLine."Document Type" := TempSalesHeader."Document Type";
        SalesLine."Document No." := TempSalesHeader."No.";
        UpdateHeader := true;
        CalculateInvoiceDiscount(TempSalesHeader, TempSalesLine);
    end;

    /// <summary>
    /// Updates prepayment line amounts when invoice discount would exceed remaining non-prepaid amount.
    /// </summary>
    /// <param name="SalesHeader">The sales header containing lines to update.</param>
    procedure UpdatePrepmtLineAmount(SalesHeader: Record "Sales Header")
    var
        SalesLine: Record "Sales Line";
    begin
        if (SalesHeader."Invoice Discount Calculation" = SalesHeader."Invoice Discount Calculation"::"%") and
           (SalesHeader."Prepayment %" > 0) and (SalesHeader."Invoice Discount Value" > 0) and
           (SalesHeader."Invoice Discount Value" + SalesHeader."Prepayment %" >= 100)
        then begin
            SalesLine.SetRange("Document Type", SalesHeader."Document Type");
            SalesLine.SetRange("Document No.", SalesHeader."No.");
            SalesLine.SetLoadFields(Type, Quantity, "Unit Price", "Qty. to Invoice", "Prepayment %", "Prepmt. Line Amount", Amount);
            if SalesLine.FindSet(true) then
                repeat
                    if not SalesLine.ZeroAmountLine(0) and (SalesLine."Prepayment %" = SalesHeader."Prepayment %") then begin
                        SalesLine."Prepmt. Line Amount" := SalesLine.Amount;
                        SalesLine.Modify();
                    end;
                until SalesLine.Next() = 0;
        end;
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeCalcSalesDiscount(var SalesHeader: Record "Sales Header"; var IsHandled: Boolean; var SalesLine: Record "Sales Line"; var UpdateHeader: Boolean)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnAfterCalcSalesDiscount(var SalesHeader: Record "Sales Header"; var TempVATAmountLine: Record "VAT Amount Line" temporary; var SalesLine2: Record "Sales Line")
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnAfterCustInvDiscRecExists(var SalesHeader: Record "Sales Header"; var CustInvDisc: Record "Cust. Invoice Disc."; InvDiscBase: Decimal; ChargeBase: Decimal; var ShouldGetCustInvDisc: Boolean)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnAfterValidateSalesLine2Quantity(SalesHeader: Record "Sales Header"; var SalesLine: Record "Sales Line"; CustInvoiceDisc: Record "Cust. Invoice Disc.")
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeCalculateIncDiscForHeader(var TempSalesHeader: Record "Sales Header" temporary; var IsHandled: Boolean; var SalesLine: Record "Sales Line"; var TempSalesLine: Record "Sales Line" temporary; var UpdateHeader: Boolean)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeSetSalesLineServiceCharge(SalesHeader: Record "Sales Header"; var SalesLine: Record "Sales Line"; CustInvoiceDisc: Record "Cust. Invoice Disc."; var IsHandled: Boolean)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeUpdateSalesHeaderInvoiceDiscount(var CustInvoiceDisc: Record "Cust. Invoice Disc."; var SalesHeader: Record "Sales Header"; var TempVATAmountLine: Record "VAT Amount Line" temporary; var UpdateHeader: Boolean; var IsHandled: Boolean)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnCalculateInvoiceDiscountOnBeforeCurrencyInitialize(var CustomerPostingGroup: Record "Customer Posting Group")
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnCalculateInvoiceDiscountOnBeforeCheckCustInvDiscServiceCharge(var CustInvoiceDisc: Record "Cust. Invoice Disc."; var SalesHeader: Record "Sales Header"; CurrencyDate: Date; ChargeBase: Decimal; var CustInvDiscFound: Boolean)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnCalculateInvoiceDiscountOnBeforeCustInvDiscRecExists(var SalesHeader: Record "Sales Header"; var SalesLine2: Record "Sales Line"; var UpdateHeader: Boolean; var InvDiscBase: Decimal; var ChargeBase: Decimal; var TempVATAmountLine: Record "VAT Amount Line" temporary; var IsHandled: Boolean)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnCalculateInvoiceDiscountOnBeforeSalesLine2FindFirst(var SalesLine2: Record "Sales Line")
    begin
    end;

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

    [IntegrationEvent(false, false)]
    local procedure OnBeforeCalculateWithSalesHeader(var SalesLine: Record "Sales Line"; var TempSalesHeader: Record "Sales Header" temporary; var TempSalesLine: Record "Sales Line"; var UpdateHeader: Boolean; var IsHandled: Boolean)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnCalculateInvoiceDiscountOnBeforeSalesLine2DeleteTrue(UpdateHeader: Boolean; var SalesLine2: Record "Sales Line"; var IsHandled: Boolean)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnRunOnBeforeCalculateInvoiceDiscount(var SalesLine: Record "Sales Line"; var TempSalesHeader: Record "Sales Header" temporary; var TempSalesLine: Record "Sales Line" temporary; var UpdateHeader: Boolean; var IsHandled: Boolean)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnCalculateInvoiceDiscountOnBeforeUpdateSalesLine2(var SalesHeader: Record "Sales Header"; var SalesLine2: Record "Sales Line"; UpdateHeader: Boolean; CustInvDisc: Record "Cust. Invoice Disc."; var IsHandled: Boolean)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeCustInvDiscRecExists(InvDiscCode: Code[20]; var Result: Boolean; var IsHandled: Boolean)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeCalculateInvoiceDiscount(var SalesHeader: Record "Sales Header"; var SalesLine: Record "Sales Line"; var UpdateHeader: Boolean)
    begin
    end;
}