Why do you need this change?
We need four events for the table "Sales Line":
- OnBeforeInitOnValidateNo, in order to be able to manage an if statement before run the Init() procedure.
Here the IsHandled parameter is necessary because it is the only way to skip the Init() procedure if a particular custom condition is not satisfied.
- OnAfterAssignValuesFromGLAccount, in order to manage custom values for the fields to which the procedure has already assigned a value. The aim is to overwrite this values according to custom logics and to do it before running the subsequent procedure InitDeferralCode.
- OnBeforeCalcBaseQty2, in order to manage the Result of the procedure CalcBaseQty according to custom logics.
Here the IsHandled parameter is necessary since, without it my custom calculation will be overwritten by standard procedure exit(UOMMgt.CalcBaseQty(
"No.", "Variant Code", "Unit of Measure Code", Qty, "Qty. per Unit of Measure", "Qty. Rounding Precision (Base)", FieldCaption("Qty. Rounding Precision"), FromFieldName, ToFieldName));
So, this is the only way to modify the CalcBaseQty calculation specifacally for Sales Line, without exploiting more general and already existing events inside codeunit 5402.
- OnBeforeUpdateUnitPriceByField, in order to skip the procedure UpdateUnitPriceByField, when a particular condition is satisfied. I want to test my condition before running IsPriceCalcCalledByField. That's why I cannot use the already existing event OnBeforeUpdateUnitPrice.
Describe the request
In trigger OnValidate of field(6; "No."; Code[20]) of table 37 "Sales Line" we need one event:
[IntegrationEvent(false, false)]
local procedure OnBeforeInitOnValidateNo(var Ishandled: Boolean; CurrFieldNo: Integer; Rec: Record "Sales Line"; xRec: Record "Sales Line")
begin
end;
Changes between **:
trigger OnValidate()
var
TempSalesLine: Record "Sales Line" temporary;
IsHandled: Boolean;
ShouldStopValidation: Boolean;
begin
IsHandled := false;
OnBeforeValidateNo(Rec, xRec, CurrFieldNo, IsHandled);
if IsHandled then
exit;
GetSalesSetup();
Rec."No." := FindOrCreateRecordByNo(Rec."No.");
TestJobPlanningLine();
TestStatusOpen();
CheckItemAvailable(FieldNo("No."));
if (xRec."No." <> "No.") and (Quantity <> 0) then begin
TestField("Qty. to Asm. to Order (Base)", 0);
CalcFields("Reserved Qty. (Base)");
TestField("Reserved Qty. (Base)", 0);
if Type = Type::Item then
SalesWarehouseMgt.SalesLineVerifyChange(Rec, xRec);
OnValidateNoOnAfterVerifyChange(Rec, xRec);
if CurrFieldNo = Rec.FieldNo("No.") then
CheckWarehouse(false);
end;
TestField("Qty. Shipped Not Invoiced", 0);
TestField("Quantity Shipped", 0);
TestField("Shipment No.", '');
TestField("Prepmt. Amt. Inv.", 0);
TestField("Return Qty. Rcd. Not Invd.", 0);
TestField("Return Qty. Received", 0);
TestField("Return Receipt No.", '');
if "No." = '' then
ATOLink.DeleteAsmFromSalesLine(Rec);
CheckAssocPurchOrder(FieldCaption("No."));
CheckReceiptOrderStatus();
OnValidateNoOnBeforeInitRec(Rec, xRec, CurrFieldNo);
TempSalesLine := Rec;
**ishandled := false;
OnBeforeInitOnValidateNo(Ishandled, CurrFieldNo, Rec, xRec)
if not ishandled then**
Init();
SystemId := TempSalesLine.SystemId;
"Automatically Generated" := TempSalesLine."Automatically Generated";
if xRec."Line Amount" <> 0 then
"Recalculate Invoice Disc." := xRec."Allow Invoice Disc.";
Type := TempSalesLine.Type;
"No." := TempSalesLine."No.";
OnValidateNoOnCopyFromTempSalesLine(Rec, TempSalesLine, xRec, CurrFieldNo);
ShouldStopValidation := "No." = '';
OnValidateNoOnAfterCalcShouldStopValidation(Rec, xRec, CurrFieldNo, ShouldStopValidation);
if ShouldStopValidation then
exit;
if HasTypeToFillMandatoryFields() then begin
Quantity := TempSalesLine.Quantity;
"Outstanding Qty. (Base)" := TempSalesLine."Outstanding Qty. (Base)";
end;
"System-Created Entry" := TempSalesLine."System-Created Entry";
GetSalesHeader();
OnValidateNoOnBeforeInitHeaderDefaults(SalesHeader, Rec, TempSalesLine);
InitHeaderDefaults(SalesHeader);
OnValidateNoOnAfterInitHeaderDefaults(SalesHeader, TempSalesLine, Rec);
CalcFields("Substitution Available");
"Promised Delivery Date" := SalesHeader."Promised Delivery Date";
"Requested Delivery Date" := SalesHeader."Requested Delivery Date";
IsHandled := false;
OnValidateNoOnBeforeCalcShipmentDateForLocation(IsHandled, Rec);
if not IsHandled then
CalcShipmentDateForLocation();
IsHandled := false;
OnValidateNoOnBeforeUpdateDates(Rec, xRec, SalesHeader, CurrFieldNo, IsHandled, TempSalesLine);
if not IsHandled then
UpdateDates();
OnAfterAssignHeaderValues(Rec, SalesHeader);
case Type of
Type::" ":
CopyFromStandardText();
Type::"G/L Account":
CopyFromGLAccount(TempSalesLine);
Type::Item:
CopyFromItem();
Type::Resource:
CopyFromResource();
Type::"Fixed Asset":
CopyFromFixedAsset();
Type::"Charge (Item)":
CopyFromItemCharge();
end;
OnAfterAssignFieldsForNo(Rec, xRec, SalesHeader);
IsHandled := false;
OnValidateNoOnBeforeCheckPostingSetups(Rec, IsHandled);
if not IsHandled then
if Type <> Type::" " then
if not IsTemporary() then begin
PostingSetupMgt.CheckGenPostingSetupSalesAccount("Gen. Bus. Posting Group", "Gen. Prod. Posting Group");
PostingSetupMgt.CheckGenPostingSetupCOGSAccount("Gen. Bus. Posting Group", "Gen. Prod. Posting Group");
PostingSetupMgt.CheckVATPostingSetupSalesAccount("VAT Bus. Posting Group", "VAT Prod. Posting Group");
end;
if HasTypeToFillMandatoryFields() and (Type <> Type::"Fixed Asset") then
ValidateVATProdPostingGroup();
UpdatePrepmtSetupFields();
"Refers to Period" := SalesHeader."Refers to Period";
if VATPostingSetup.IsEUService("VAT Bus. Posting Group", "VAT Prod. Posting Group") then
"Service Tariff No." := SalesHeader."Service Tariff No."
else
if "Service Tariff No." <> '' then
"Service Tariff No." := '';
if HasTypeToFillMandatoryFields() then begin
PlanPriceCalcByField(FieldNo("No."));
ValidateUnitOfMeasureCodeFromNo();
if Quantity <> 0 then begin
OnValidateNoOnBeforeInitOutstanding(Rec, xRec);
InitOutstanding();
if IsCreditDocType() then
InitQtyToReceive()
else
InitQtyToShip();
InitQtyToAsm();
UpdateWithWarehouseShip();
end;
end;
IsHandled := false;
OnValidateNoOnBeforeCreateDimFromDefaultDim(Rec, IsHandled, TempSalesLine);
if not IsHandled then
CreateDimFromDefaultDim(Rec.FieldNo("No."));
OnValidateNoOnAfterCreateDimFromDefaultDim(Rec, xRec, SalesHeader, CurrFieldNo);
if "No." <> xRec."No." then begin
if Type = Type::Item then begin
if (Quantity <> 0) and ItemExists(xRec."No.") then begin
VerifyChangeForSalesLineReserve(FieldNo("No."));
SalesWarehouseMgt.SalesLineVerifyChange(Rec, xRec);
end;
CheckItemCanBeAddedToSalesLine();
end;
GetDefaultBin();
Rec.AutoAsmToOrder();
DeleteItemChargeAssignment("Document Type", "Document No.", "Line No.");
if Type = Type::"Charge (Item)" then
DeleteChargeChargeAssgnt("Document Type", "Document No.", "Line No.");
end;
UpdateItemReference(FieldNo("No."));
UpdateUnitPriceByField(FieldNo("No."));
OnValidateNoOnAfterUpdateUnitPrice(Rec, xRec, TempSalesLine);
end;
In procedure CopyFromGLAccount of table 37 "Sales Line" we need an event:
[IntegrationEvent(false, false)]
local procedure OnAfterAssignValuesFromGLAccount(var SalesLine: Record "Sales Line"; GLAccount: Record "G/L Account"; SalesHeader: Record "Sales Header"; var TempSalesLine: Record "Sales Line" temporary; CurrFieldNo: Integer)
begin
end;
Changes between **:
local procedure CopyFromGLAccount(var TempSalesLine: Record "Sales Line" temporary)
var
**IsHandled: boolean;
begin
GLAcc.Get("No.");
GLAcc.CheckGLAcc();
TestDirectPosting();
Description := GLAcc.Name;
"Gen. Prod. Posting Group" := GLAcc."Gen. Prod. Posting Group";
"VAT Prod. Posting Group" := GLAcc."VAT Prod. Posting Group";
"Tax Group Code" := GLAcc."Tax Group Code";
"Allow Invoice Disc." := false;
"Allow Item Charge Assignment" := false;
**OnAfterAssignValuesFromGLAccount(Rec, GLAcc, SalesHeader, TempSalesLine, CurrFieldNo);**
InitDeferralCode();
SetDefaultGLAccountQuantity();
OnAfterAssignGLAccountValues(Rec, GLAcc, SalesHeader, TempSalesLine);
end;
In procedure CalcBaseQty of table 37 "Sales Line" we need an event:
[IntegrationEvent(false, false)]
local procedure OnBeforeCalcBaseQty2(var Ishandled: Boolean; var Result: Decimal; var SalesLine: Record "Sales Line"; Qty: Decimal; FromFieldName: Text; ToFieldName: Text)
begin
end;
Changes between **:
procedure CalcBaseQty(Qty: Decimal; FromFieldName: Text; ToFieldName: Text): Decimal
var
**Ishandled: boolean;
Result: Decimal;**
begin
**ishandled := false;
OnBeforeCalcBaseQty2(Ishandled, Result, Rec, Qty, FromFieldName, ToFieldName);
if ishandled then
exit(Result);**
OnBeforeCalcBaseQty(Rec, Qty, FromFieldName, ToFieldName);
exit(UOMMgt.CalcBaseQty(
"No.", "Variant Code", "Unit of Measure Code", Qty, "Qty. per Unit of Measure", "Qty. Rounding Precision (Base)", FieldCaption("Qty. Rounding Precision"), FromFieldName, ToFieldName));
end;
In procedure UpdateUnitPriceByField of table 37 "Sales Line" we need an event:
[IntegrationEvent(false, false)]
local procedure OnBeforeUpdateUnitPriceByField(var Ishandled: Boolean; CalledByFieldNo: Integer; CurrFieldNo: Integer)
begin
end;
Changes between **:
procedure UpdateUnitPriceByField(CalledByFieldNo: Integer)
var
BlanketOrderSalesLine: Record "Sales Line";
IsHandled: Boolean;
PriceCalculation: Interface "Price Calculation";
begin
**IsHandled := false;
OnBeforeUpdateUnitPriceByField(IsHandled, CalledByFieldNo, CurrFieldNo);
if IsHandled then
exit;**
if not IsPriceCalcCalledByField(CalledByFieldNo) then
exit;
IsHandled := false;
OnBeforeUpdateUnitPrice(Rec, xRec, CalledByFieldNo, CurrFieldNo, IsHandled);
if IsHandled then
exit;
GetSalesHeader();
TestField("Qty. per Unit of Measure");
case Type of
Type::"G/L Account",
Type::Item,
Type::Resource:
begin
IsHandled := false;
OnUpdateUnitPriceOnBeforeFindPrice(SalesHeader, Rec, CalledByFieldNo, CurrFieldNo, IsHandled, xRec);
if not IsHandled then
if not BlanketOrderIsRelated(BlanketOrderSalesLine) then begin
GetPriceCalculationHandler(PriceType::Sale, SalesHeader, PriceCalculation);
if not ("Copied From Posted Doc." and IsCreditDocType()) then begin
PriceCalculation.ApplyDiscount();
ApplyPrice(CalledByFieldNo, PriceCalculation);
end else
CalcUnitPriceUsingUOMCoef();
end else
CopyUnitPriceAndLineDiscountPct(BlanketOrderSalesLine, CalledByFieldNo);
OnUpdateUnitPriceByFieldOnAfterFindPrice(SalesHeader, Rec, CalledByFieldNo, CurrFieldNo);
end;
end;
ShowUnitPriceChangedMsg();
IsHandled := false;
OnUpdateUnitPriceByFieldOnBeforeValidateUnitPrice(Rec, xRec, CalledByFieldNo, CurrFieldNo, IsHandled);
if not IsHandled then
Validate("Unit Price");
ClearFieldCausedPriceCalculation();
OnAfterUpdateUnitPrice(Rec, xRec, CalledByFieldNo, CurrFieldNo);
end;
Performance considerations
The requested events are introduced in the Sales Line table, which is a frequently used table. However, all proposed events only add a single event invocation and do not introduce any additional database operations by themselves.
OnBeforeInitOnValidateNo is executed only during validation of field "No.".
OnAfterAssignValuesFromGLAccount is executed only when the sales line type is "G/L Account".
OnBeforeCalcBaseQty2 is executed only when CalcBaseQty() is called for Sales Line.
OnBeforeUpdateUnitPriceByField is executed only when a unit price recalculation is triggered.
Therefore, the expected performance impact is negligible. Any additional processing cost depends entirely on the subscriber implementation.
Data sensitivity/security review
The proposed events expose only records and variables that are already available within the Sales Line business process:
Sales Line
Sales Header
G/L Account
temporary Sales Line
quantities, field numbers and Boolean flags.
No personally identifiable information, credentials, secrets, or new categories of sensitive data are exposed by these events. The proposed changes only provide additional extensibility points in existing business logic.
Multi-extension interaction/conflict risk
OnBeforeInitOnValidateNo
This event uses the IsHandled pattern because there is currently no extensibility point that allows partners to prevent the execution of Init() under specific conditions.
As with any IsHandled event, multiple extensions could set IsHandled := true. In that case, the standard Init() logic would be skipped. This risk is acceptable because only extensions intentionally replacing the initialization behavior should set IsHandled := true.
Without this event, partners are forced to duplicate the entire "No." validation logic.
OnBeforeCalcBaseQty2
This event also requires IsHandled because the result returned by:
exit(UOMMgt.CalcBaseQty(...));
cannot be modified afterwards.
If multiple extensions subscribe and set IsHandled := true, the final value assigned to Result depends on the subscriber execution order. This is acceptable because the event is intended for scenarios where an extension completely replaces the standard Sales Line base quantity calculation.
The existing events in Codeunit 5402 are too generic because they affect quantity calculations globally, while the requirement is to customize the calculation specifically for Sales Line.
OnBeforeUpdateUnitPriceByField
IsHandled is required because the existing OnBeforeUpdateUnitPrice event is raised after the call to IsPriceCalcCalledByField, which is too late for this scenario.
If multiple extensions set IsHandled := true, the standard price update logic is skipped. This is an acceptable risk because only extensions intentionally replacing the standard price update behavior should use the bypass functionality.
OnAfterAssignValuesFromGLAccount
This event does not use the IsHandled pattern and therefore does not introduce any special multi-extension risks.
Multiple extensions can safely subscribe to the event and modify the assigned values. The only possible interaction is that a subsequent subscriber may overwrite values assigned by a previous subscriber, which is the normal and accepted behavior of integration events exposing record variables.
Why do you need this change?
We need four events for the table "Sales Line":
Here the IsHandled parameter is necessary because it is the only way to skip the Init() procedure if a particular custom condition is not satisfied.
Here the IsHandled parameter is necessary since, without it my custom calculation will be overwritten by standard procedure exit(UOMMgt.CalcBaseQty(
"No.", "Variant Code", "Unit of Measure Code", Qty, "Qty. per Unit of Measure", "Qty. Rounding Precision (Base)", FieldCaption("Qty. Rounding Precision"), FromFieldName, ToFieldName));
So, this is the only way to modify the CalcBaseQty calculation specifacally for Sales Line, without exploiting more general and already existing events inside codeunit 5402.
Describe the request
In trigger OnValidate of field(6; "No."; Code[20]) of table 37 "Sales Line" we need one event:
Changes between **:
In procedure CopyFromGLAccount of table 37 "Sales Line" we need an event:
Changes between **:
In procedure CalcBaseQty of table 37 "Sales Line" we need an event:
Changes between **:
In procedure UpdateUnitPriceByField of table 37 "Sales Line" we need an event:
Changes between **:
Performance considerations
The requested events are introduced in the Sales Line table, which is a frequently used table. However, all proposed events only add a single event invocation and do not introduce any additional database operations by themselves.
OnBeforeInitOnValidateNo is executed only during validation of field "No.".
OnAfterAssignValuesFromGLAccount is executed only when the sales line type is "G/L Account".
OnBeforeCalcBaseQty2 is executed only when CalcBaseQty() is called for Sales Line.
OnBeforeUpdateUnitPriceByField is executed only when a unit price recalculation is triggered.
Therefore, the expected performance impact is negligible. Any additional processing cost depends entirely on the subscriber implementation.
Data sensitivity/security review
The proposed events expose only records and variables that are already available within the Sales Line business process:
Sales Line
Sales Header
G/L Account
temporary Sales Line
quantities, field numbers and Boolean flags.
No personally identifiable information, credentials, secrets, or new categories of sensitive data are exposed by these events. The proposed changes only provide additional extensibility points in existing business logic.
Multi-extension interaction/conflict risk
OnBeforeInitOnValidateNo
This event uses the IsHandled pattern because there is currently no extensibility point that allows partners to prevent the execution of Init() under specific conditions.
As with any IsHandled event, multiple extensions could set IsHandled := true. In that case, the standard Init() logic would be skipped. This risk is acceptable because only extensions intentionally replacing the initialization behavior should set IsHandled := true.
Without this event, partners are forced to duplicate the entire "No." validation logic.
OnBeforeCalcBaseQty2
This event also requires IsHandled because the result returned by:
exit(UOMMgt.CalcBaseQty(...));
cannot be modified afterwards.
If multiple extensions subscribe and set IsHandled := true, the final value assigned to Result depends on the subscriber execution order. This is acceptable because the event is intended for scenarios where an extension completely replaces the standard Sales Line base quantity calculation.
The existing events in Codeunit 5402 are too generic because they affect quantity calculations globally, while the requirement is to customize the calculation specifically for Sales Line.
OnBeforeUpdateUnitPriceByField
IsHandled is required because the existing OnBeforeUpdateUnitPrice event is raised after the call to IsPriceCalcCalledByField, which is too late for this scenario.
If multiple extensions set IsHandled := true, the standard price update logic is skipped. This is an acceptable risk because only extensions intentionally replacing the standard price update behavior should use the bypass functionality.
OnAfterAssignValuesFromGLAccount
This event does not use the IsHandled pattern and therefore does not introduce any special multi-extension risks.
Multiple extensions can safely subscribe to the event and modify the assigned values. The only possible interaction is that a subsequent subscriber may overwrite values assigned by a previous subscriber, which is the normal and accepted behavior of integration events exposing record variables.