Skip to content

[Event Request] table 37 "Sales Line" #30225

Description

@Simon110394

Why do you need this change?

We need four events for the table "Sales Line":

  1. 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.
  2. 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.
  3. 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.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    missing-infoThe issue misses information that prevents it from completion.

    Type

    No fields configured for Task.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions