-
Notifications
You must be signed in to change notification settings - Fork 687
Description
Why do you need this change?
-
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; }
-
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;
-
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);
-
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
- 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;- 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;- 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;- 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