Skip to content

[Event Requests] - table 37 Sales Line #29574

@exnihiloo

Description

@exnihiloo

Why do you need this change?

  1. We need to assign new Shipment Date value on validation trigger (field10 "Shipment Date") but before assigning it we need to get HideValidationDialog which is possible with global procedure and also we need to get variable HasBeenShown value which is not available right now because there is only setter procedure. We would like to ask for this variable on event publisher OnValidateShipmentDateOnAfterSalesLineVerifyChange OR for a new global procedure GetHasBeedShown.

    Alternatives Evaluation: Event OnValidateShipmentDateOnAfterSalesLineVerifyChange can be used, we just need additional parameter inside this event var HasBeenShown

    IsHandled Justification: Do not need for this case.

    Performance Considerations: The Shipment Date fieldΓÇÖs OnValidate trigger is executed very frequently because it is touched during normal sales line entry, recalculation of related dates, Copy Document operations, and automated integrations. In typical implementations, a single sales order may contain 20ΓÇô200 lines, and each line may trigger multiple validations due to dependencies between Shipment Date, Planned Shipment Date, Planned Delivery Date, Shipping Time, and Requested Delivery Date. Automated integrations (EDI, eΓÇæcommerce, APIs) can generate thousands of sales lines per day, resulting in this validation running tens of thousands of times daily.
    The standard validation includes availability checks, warehouse conflict checks, and date conflict logic, all of which are timingΓÇæcritical because they run inside the UI validation pipeline and posting processes. Any unnecessary execution directly affects lineΓÇæentry responsiveness, posting performance, and integration throughput. Allowing the logic to be bypassed when overridden by an extension prevents redundant execution of these heavy routines and reduces performance overhead in highΓÇævolume scenarios.

    Data Sensitivity Review: We need only one parameter HasBeenShown which wonΓÇÖt expose any sensitive data.

    Example of custom code :

    field(10; "Shipment Date"; Date)
    {
        ...
    
        trigger OnValidate()
        var
            ...
        begin
            IsHandled := false;
            OnBeforeValidateShipmentDate(IsHandled, Rec, xRec, CurrFieldNo);
            if IsHandled then
                exit;
    
            TestStatusOpen();
            SalesWarehouseMgt.SalesLineVerifyChange(Rec, xRec);
            DoCheckReceiptOrderStatus := CurrFieldNo <> 0;
            //OnValidateShipmentDateOnAfterSalesLineVerifyChange(Rec, CurrFieldNo, DoCheckReceiptOrderStatus); //Standard Code
            OnValidateShipmentDateOnAfterSalesLineVerifyChange(Rec, CurrFieldNo, DoCheckReceiptOrderStatus, HasBeenShown ); //New parameter
            if DoCheckReceiptOrderStatus then
                CheckReceiptOrderStatus();
    
            if "Shipment Date" <> 0D then begin
                //Custom code begin <<
                if SalesHeader."Shipment Date" <> 0D then begin
                    if "Shipment Date" <> SalesHeader."Shipment Date" then                            
                        "Shipment Date" := SalesHeader."Shipment Date";                                 
                end else begin                                                                      
                    if ("Shipment Date" < WorkDate()) and (Type <> Type::" ") then
                        if not (HideValidationDialog or HasBeenShown) then begin
                            "Shipment Date" := WorkDate();
                            MESSAGE(hm006,WorkDate());
                            HasBeenShown := true;
                        end;
                end;
                //Custom code end >>
                if CurrFieldNo in [
                                   FieldNo("Planned Shipment Date"),
                                   FieldNo("Planned Delivery Date"),
                                   FieldNo("Shipment Date"),
                                   FieldNo("Shipping Time"),
                                   FieldNo("Outbound Whse. Handling Time"),
                                   FieldNo("Requested Delivery Date")]
                then
                    CheckItemAvailable(FieldNo("Shipment Date"));
    
                CheckShipmentDateBeforeWorkDate();
            end;
            ...
        end;
    }
  2. The Sales Line OnInsert trigger contains a mandatory LockTable() call that extensions cannot influence. This creates avoidable performance issues and potential deadlocks in highΓÇævolume scenarios. Adding an event publisher before the lock would allow extensions to decide whether locking is necessary, improving scalability without changing standard behavior.

    Alternatives Evaluation:
    The Sales Line OnInsert trigger does not expose an IsHandled pattern, and none of the existing integration events in the insert trigger allow us to:prevent the standard LockTable() call, replace the standard logic, inject custom logic before the standard logic executes. The available events (OnBeforeVerifyReservedQty, OnInsertOnAfterCheckInventoryConflict, OnAfterInsertOnAfterUpdateDeferralAmounts, etc.) are postΓÇæprocessing events only. They allow adding logic after standard behavior, but they do not allow skipping or overriding the standard insert logic.
    Because the LockTable() call happens inside the trigger itself, and not inside a procedure with an IsHandled event, there is no event that allows us to prevent the lock from being taken.

    IsHandled Justification:
    The standard LockTable() call causes unnecessary table locking in highΓÇævolume environments.
    We need to replace the standard insert logic with a custom version that does not lock the table.
    Without IsHandled, the standard logic always executes, and we cannot prevent the lock from being taken.
    Adding custom logic after the insert is not sufficient, because the lock has already been taken by that point.

    Data Sensitivity Review:
    The event would only expose the Sales Line record (Rec and xRec), which is already accessible to any extension that subscribes to existing events on the table.
    The data involved includes: Item No., Quantity, Type, Deferral Code, Dimensions
    None of these fields contain personal data or sensitive financial information. They are standard operational fields already exposed through many existing events.
    No additional sensitive data would be exposed by adding an IsHandled event.

    Multi Extension Interaction:
    Extensions that rely on this event must coordinate via dependency declarations. Partners can use EventSubscriberInstance = Manual to control execution order. The pattern is consistent with other core IsHandled events, so developers are familiar with the implications.

    Example of custom code :

    trigger OnInsert()
    begin
        TestStatusOpen();
        if Quantity <> 0 then begin
            OnBeforeVerifyReservedQty(Rec, xRec, 0);
            SalesLineReserve.VerifyQuantity(Rec, xRec);
        end;
        //Custom code begin <<
        //LockTable(); //Standard Line
        if ns.GET('VKENTRYNR') then begin
            EVALUATE("Entry No.",NoSeriesMgt.GetNextNo('VKENTRYNR',TODAY,TRUE));
        end else begin
            SalesLineExist.RESET;
            SalesLineExist.SETCURRENTKEY("Entry No.");
    
            if SalesLineExist.FindLast() then Ok := true else Ok := false;
            if Ok = true then
            "Entry No." := SalesLineExist."Entry No." + 1
            else
            "Entry No." := 1;
        end;
        if "Document Type" = "Document Type"::Order then begin
            SalesHeader_l.GET("Document Type","Document No.");
            "Shortcut Dimension 1 Code" := SalesHeader_l."Shortcut Dimension 1 Code";
            DimMgt.ValidateShortcutDimValues(1,"Shortcut Dimension 1 Code","Dimension Set ID");
        end;
        //Custom code end >>
        SalesHeader."No." := '';
        //Custom code begin <<
        if not Naturalrabatt then
            VerkRabattZeile.Hole_Aktuelle_Rabattzeilen(Rec);
    
        "Orig. Belegart" := "Document Type";
        "Orig. Belegnr"  := "Document No.";
        "Orig. Zeilennr" := "Line No.";
        //Custom code end >>
        if (Type = Type::Item) and ("No." <> '') then
            CheckInventoryPickConflict();
        OnInsertOnAfterCheckInventoryConflict(Rec, xRec, SalesLine2);
        if ("Deferral Code" <> '') and (GetDeferralAmount() <> 0) then
            UpdateDeferralAmounts();
        OnAfterInsertOnAfterUpdateDeferralAmounts(Rec, CurrFieldNo);
    end;
  3. The UpdateVATOnLines procedure always executes SalesLine.LockTable(). Adding an additional IsHandled before the SalesLine.LockTable() call would allow extensions to fully override the standard logic when needed, prevent unnecessary locking avoid the risk of deadlocks and improve scalability without changing default behavior for existing customers.

    Alternatives Evaluation:
    We evaluated all existing events in and around the UpdateVATOnLines procedure:
    OnUpdateVATOnLinesOnAfterCurrencyInitialize
    OnUpdateVATOnLinesOnAfterSalesLineSetFilter
    OnCalcVATAmountLinesOnBeforeGetDeferralAmount
    None of these events allow us to:
    prevent the standard SalesLine.LockTable() call
    All available events are postΓÇæprocessing events. They allow adding logic after the standard behavior, but they do not allow suppressing or replacing the standard logic.
    Because the locking happens inside the procedure itself, and not inside a replaceable function, there is no event that allows us to avoid the lock or override the logic.
    It would be great if event OnUpdateVATOnLinesOnAfterSalesLineSetFilter would have additional parameter IsHandled.

    IsHandled Justification:
    The extension must avoid the standard LockTable() and replace the VAT calculation logic. Without IsHandled, the standard logic always executes, causing locking and performance issues.

    Data Sensitivity Review:
    Data does not contain sensitive information, we only need additional parameter IsHandled for the existing event.

    Multi Extension Interaction:
    Extensions that rely on this event must coordinate via dependency declarations. Partners can use EventSubscriberInstance = Manual to control execution order. The pattern is consistent with other core IsHandled events, so developers are familiar with the implications.

    Example of custom code :

    procedure UpdateVATOnLines(QtyType: Option General,Invoicing,Shipping; var SalesHeader: Record "Sales Header"; var SalesLine: Record "Sales Line"; var VATAmountLine: Record "VAT Amount Line") LineWasModified: Boolean
    var
        ...
    begin
        if IsUpdateVATOnLinesHandled(SalesHeader, SalesLine, VATAmountLine, QtyType, LineWasModified) then
            exit(LineWasModified);
    
        LineWasModified := false;
        if QtyType = QtyType::Shipping then
            exit;
    
        Currency.Initialize(SalesHeader."Currency Code");
        OnUpdateVATOnLinesOnAfterCurrencyInitialize(Rec, SalesHeader, Currency);
    
        TempVATAmountLineRemainder.DeleteAll();
    
        SalesLine.SetRange("Document Type", SalesHeader."Document Type");
        SalesLine.SetRange("Document No.", SalesHeader."No.");
        SetLoadFieldsForInvDiscoundCalculation(SalesLine);
        OnUpdateVATOnLinesOnAfterSalesLineSetFilter(SalesLine);
        //SalesLine.LockTable(); //Custom avoids LockTable
        if SalesLine.FindSet() then
            repeat
                if not SalesLine.ZeroAmountLine(QtyType) then begin
                    OnCalcVATAmountLinesOnBeforeGetDeferralAmount(SalesLine);
  4. We need an event at the beginning of SumVATAmountLine because we use a different VAT calculation method and canΓÇÖt change the standard logic. The current event is only at the end of the procedure, which is too late ΓÇö the values are already calculated. An event with IsHandled at the start would let us skip the standard calculation and apply our own, without modifying base code.

    IsHandled Justification:
    The existing event OnSumVATAmountLineOnBeforeModify fires only after all VAT calculations are already performed, so it cannot prevent or replace the standard logic. Our customization needs to override the calculation for the Invoicing case, which requires skipping the standard code entirely. An IsHandled event at the start of the procedure is the only way to stop the standard logic from running and execute our custom calculation instead.

    Performance Considerations:
    SumVATAmountLine runs very frequently during posting, VAT recalculation, invoice discount updates, and line modifications. A single document may trigger dozens of calls, and highΓÇævolume customers may execute this procedure thousands of times per day. Because it loops through VAT Amount Lines and performs rounding and discount calculations, it is timingΓÇæcritical. Allowing the logic to be bypassed when overridden avoids unnecessary VAT recalculation and improves posting performance.

    Data Sensitivity Review:
    The data involved (line amounts, VAT amounts, invoice discount amounts, quantities, flags) is operational accounting data and does not include personal or sensitive information. All fields are already accessible through existing events and APIs, so no new sensitive data is exposed.

    Multi Extension Interaction:
    Extensions that rely on this event must coordinate via dependency declarations. Partners can use EventSubscriberInstance = Manual to control execution order. The pattern is consistent with other core IsHandled events, so developers are familiar with the implications.

    Example of custom code :

    local procedure SumVATAmountLine(var SalesHeader: Record "Sales Header"; var SalesLine: Record "Sales Line"; var VATAmountLine: Record "VAT Amount Line"; QtyType: Option General,Invoicing,Shipping; AmtToHandle: Decimal; QtyToHandle: Decimal)
    begin
        case QtyType of
            QtyType::General:
                begin
                    ...
                end;
            QtyType::Invoicing:
                //Custom code begin <<
                //if SalesHeader."Invoice Discount Calculation" <> SalesHeader."Invoice Discount Calculation"::Amount then begin
                    //VATAmountLine."Line Amount" += AmtToHandle;
                    //if SalesLine."Allow Invoice Disc." then
                    //    VATAmountLine."Inv. Disc. Base Amount" += AmtToHandle;
                    //VATAmountLine."Invoice Discount Amount" += Round(SalesLine."Inv. Discount Amount" * QtyToHandle / SalesLine.Quantity, Currency."Amount Rounding Precision");
                //end else begin
                 //Custom code end<<
                    VATAmountLine."Line Amount" += AmtToHandle;
                    if SalesLine."Allow Invoice Disc." then
                        VATAmountLine."Inv. Disc. Base Amount" += AmtToHandle;
                    VATAmountLine."Invoice Discount Amount" += SalesLine."Inv. Disc. Amount to Invoice";
                end;
            QtyType::Shipping:
            ...

Describe the request

  1. Field 10 "Shipment Date", OnValidate trigger
field(10; "Shipment Date"; Date)
        {
            ...
            trigger OnValidate()
            var
                CheckDateConflict: Codeunit "Reservation-Check Date Confl.";
                DoCheckReceiptOrderStatus: Boolean;
                IsHandled: Boolean;
            begin
                ...
                OnValidateShipmentDateOnAfterSalesLineVerifyChange(Rec, CurrFieldNo, DoCheckReceiptOrderStatus, HasBeenShown); //New parameter HasBeenShown
                if DoCheckReceiptOrderStatus then
                    CheckReceiptOrderStatus();

                if "Shipment Date" <> 0D then begin
                    if CurrFieldNo in [
                                       FieldNo("Planned Shipment Date"),
                                       FieldNo("Planned Delivery Date"),
                                       FieldNo("Shipment Date"),
                                       FieldNo("Shipping Time"),
                                       FieldNo("Outbound Whse. Handling Time"),
                                       FieldNo("Requested Delivery Date")]
                    then
                        CheckItemAvailable(FieldNo("Shipment Date"));

                    CheckShipmentDateBeforeWorkDate();
                end;
[IntegrationEvent(false, false)]
local procedure OnValidateShipmentDateOnAfterSalesLineVerifyChange(var SalesLine: Record "Sales Line"; CurrentFieldNo: Integer; var DoCheckReceiptOrderStatus: Boolean; var HasBeenShown: Boolean;)
begin
end;
procedure GetHasBeenShown(): Boolean
begin
    exit(HasBeenShown);
end;
  1. Trigger OnInsert
trigger OnInsert()
var
    IsHandled : Boolean; //New local variable
begin
    TestStatusOpen();
    if Quantity <> 0 then begin
        OnBeforeVerifyReservedQty(Rec, xRec, 0);
        SalesLineReserve.VerifyQuantity(Rec, xRec);
    end;
    //Code change - Start
    OnInsertOnBeforeLockTable(Rec, xRec, SalesHeader, IsHandled);
    if not IsHandled then
        LockTable();
    //Code change - End
    SalesHeader."No." := '';
    ...
end;
[IntegrationEvent(false, false)]
local procedure OnInsertOnBeforeLockTable(var SalesLine: Record "Sales Line"; xSalesLine: Record "Sales Line"; var SalesHeader Record "Sales Header"; var IsHandled: Boolean;)
begin
end;
  1. Procedure UpdateVATOnLines
procedure UpdateVATOnLines(QtyType: Option General,Invoicing,Shipping; var SalesHeader: Record "Sales Header"; var SalesLine: Record "Sales Line"; var VATAmountLine: Record "VAT Amount Line") LineWasModified: Boolean
var
  ...
  IsHandled : Boolean; //New local variable
begin
  if IsUpdateVATOnLinesHandled(SalesHeader, SalesLine, VATAmountLine, QtyType, LineWasModified) then
      exit(LineWasModified);

  LineWasModified := false;
  if QtyType = QtyType::Shipping then
      exit;

  Currency.Initialize(SalesHeader."Currency Code");
  OnUpdateVATOnLinesOnAfterCurrencyInitialize(Rec, SalesHeader, Currency);

  TempVATAmountLineRemainder.DeleteAll();

  SalesLine.SetRange("Document Type", SalesHeader."Document Type");
  SalesLine.SetRange("Document No.", SalesHeader."No.");
  SetLoadFieldsForInvDiscoundCalculation(SalesLine);
  //Code change - Start
  OnUpdateVATOnLinesOnAfterSalesLineSetFilter(SalesLine, IsHandled);
  if not IsHandled then
      SalesLine.LockTable();
  //Code change - End
  if SalesLine.FindSet() then
      repeat
         ...
end;
[IntegrationEvent(false, false)]
local procedure OnUpdateVATOnLinesOnAfterSalesLineSetFilter(var SalesLine: Record "Sales Line", var IsHandled: Boolean)
begin
end;
  1. Procedure SumVATAmountLine
local procedure SumVATAmountLine(var SalesHeader: Record "Sales Header"; var SalesLine: Record "Sales Line"; var VATAmountLine: Record "VAT Amount Line"; QtyType: Option General,Invoicing,Shipping; AmtToHandle: Decimal; QtyToHandle: Decimal)
var
    IsHandled: Boolean; //New local variable
begin
    //Code change - Start
    IsHandled := false;
    OnBeforeSumVATAmountLine(SalesHeader, SalesLine, VATAmountLine,QtyType, AmtToHandle, QtyToHandle)
    if not IsHandled then
   //Code change - End
      case QtyType of
          QtyType::General:
              begin
                  VATAmountLine."Line Amount" += SalesLine."Line Amount";
                  if SalesLine."Allow Invoice Disc." then
                      VATAmountLine."Inv. Disc. Base Amount" += SalesLine."Line Amount";
                  VATAmountLine."Invoice Discount Amount" += SalesLine."Inv. Discount Amount";
              end;
          QtyType::Invoicing:
              if SalesHeader."Invoice Discount Calculation" <> SalesHeader."Invoice Discount Calculation"::Amount then begin
                  VATAmountLine."Line Amount" += AmtToHandle;
                  if SalesLine."Allow Invoice Disc." then
                      VATAmountLine."Inv. Disc. Base Amount" += AmtToHandle;
                  VATAmountLine."Invoice Discount Amount" += Round(SalesLine."Inv. Discount Amount" * QtyToHandle / SalesLine.Quantity, Currency."Amount Rounding Precision");
              end else begin
                  VATAmountLine."Line Amount" += AmtToHandle;
                  if SalesLine."Allow Invoice Disc." then
                      VATAmountLine."Inv. Disc. Base Amount" += AmtToHandle;
                  VATAmountLine."Invoice Discount Amount" += SalesLine."Inv. Disc. Amount to Invoice";
              end;
  ...
end;
[IntegrationEvent(false, false)]
local procedure OnBeforeSumVATAmountLine(var SalesHeader: Record "Sales Header"; var SalesLine: Record "Sales Line"; var VATAmountLine: Record "VAT Amount Line"; QtyType: Option General,Invoicing,Shipping; AmtToHandle: Decimal; QtyToHandle: Decimal)
begin
end;

Internal work item: AB#619489

Metadata

Metadata

Assignees

No one assigned

    Labels

    SCMGitHub request for SCM areaevent-requestRequest for adding an event

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions