Skip to content

[Event Request] codeunit 7010 "Purch. Price Calc. Mgt." - CalcBestDirectUnitCost procedure #30264

@KajOsk

Description

@KajOsk

Why do you need this change?

1. Event Request: OnCalcBestDirectUnitCostOnAfterIsInMinQty

Please add a new integration event in the CalcBestDirectUnitCost procedure of codeunit 7010 "Purch. Price Calc. Mgt.", immediately after the IsInMinQty(...) call inside the price iteration loop, exposing the result as a var parameter that subscribers can override.

IsInMinQty checks whether the current Purchase Price record's minimum quantity condition is met before it is considered as a price candidate. The standard check compares MinQty against either QtyPerUOM * Qty or plain Qty depending on whether a unit of measure code is set.

In some business setups, the quantity that should be compared against the minimum is not the individual purchase line quantity but the total quantity across all lines in the document. Whether to use the line quantity or the order total depends on a field stored on the Purchase Price record itself.

The existing OnBeforeIsInMinQty event does not pass the PurchPrice record - it only receives UnitofMeasureCode and MinQty. Because of this, a subscriber cannot read fields on the price record and cannot decide which quantity to compare against. The new event needs to fire inside the CalcBestDirectUnitCost loop where the full PurchPrice record is available, and must expose the result of IsInMinQty as a var Boolean so subscribers can override it.


Alternatives Evaluated

The following existing events were evaluated:

  • OnBeforeIsInMinQty(Item, UnitofMeasureCode, MinQty, Result, IsHandled): Does not pass the PurchPrice record. A subscriber cannot read per-price-line fields and therefore cannot decide which quantity reference to use. This event cannot be used.

  • OnBeforeCalcBestDirectUnitCost(PurchPrice, BestPurchPrice, BestPurchPriceFound, IsHandled, SKU, Item): Fires once before the loop starts. A subscriber setting IsHandled := true would need to re-implement the entire price selection loop. This event cannot be used.

  • OnCalcBestDirectUnitCostOnBeforeConvertPriceToVAT(PurchPrice): Fires after IsInMinQty has already returned true and execution is already inside the if IsInMinQty then begin block. It cannot intercept a false result and cannot affect which records enter the candidate set. This event cannot be used.

  • SetUoM(Qty2, QtyPerUoM2): A subscriber could call SetUoM before CalcBestDirectUnitCost runs (for example via OnBeforeCalcBestDirectUnitCost) to set a different quantity - such as the order total - that IsInMinQty would then use. This works when all price records for the item should use the same quantity reference. It cannot be used when different price records in the same loop require different quantity references, because SetUoM sets a single global value that applies to every iteration. This event cannot be used.

None of the existing approaches allow a subscriber to change the quantity reference individually per price record based on a field on the PurchPrice row.


Performance Considerations

The event fires once per record in the Purchase Price temp table inside CalcBestDirectUnitCost. The number of price records per item/vendor/date is typically small. The overhead is the same as the existing OnCalcBestDirectUnitCostOnBeforeConvertPriceToVAT event, which already fires in the same loop. When no subscriber changes InMinQty, execution follows the standard IsInMinQty result unchanged.


Data Sensitivity Review

The event parameters expose:

  • var PurchPrice: Record "Purchase Price" - the record currently being evaluated; already in scope in the same loop iteration.
  • QtyPerUOM: Decimal - already used internally by IsInMinQty.
  • Qty: Decimal - already used internally by IsInMinQty.
  • var InMinQty: Boolean - the result of the IsInMinQty call; a Boolean with no sensitive data. Passed as var so subscribers can change it.

No data is exposed beyond what is already available inside CalcBestDirectUnitCost at this point. No IsHandled parameter is needed because IsInMinQty is a small local function with no side effects - there is no cost to always calling it first and letting the event override the result afterward.


Multi-Extension Interaction

Multiple extensions can subscribe to this event at the same time. If multiple subscribers change InMinQty, the last subscriber to run determines the final value. When no subscriber is active, InMinQty keeps the value returned by IsInMinQty and behaviour is unchanged.


2. Event Request: OnCalcBestDirectUnitCostOnBeforeCaseStatement

Please add a new integration event with an IsHandled parameter in the CalcBestDirectUnitCost procedure of codeunit 7010 "Purch. Price Calc. Mgt.", immediately after the three price conversions (ConvertPriceToVAT, ConvertPriceToUoM, ConvertPriceLCYToFCY) and before the case true of block that decides whether the current price replaces the current best price.

The case true of block applies two rules:

  1. Specificity preference: a price with a currency code or variant code always takes priority over a generic one.
  2. Cost comparison: among equally specific prices, pick the one with the lowest effective amount.

Some business setups need a different rule for the cost comparison step. For example, selecting the price from the highest applicable minimum-quantity tier rather than the lowest cost, or preferring the most recently started price as a tiebreaker. These strategies cannot be implemented by subscribing to any existing event because no event exposes both the current candidate price (PurchPrice, already converted) and the current best price (BestPurchPrice) simultaneously - at a point where the selection decision can still be changed.


Alternatives Evaluated

The following existing events were evaluated:

  • OnCalcBestDirectUnitCostOnBeforeConvertPriceToVAT(PurchPrice): Fires before the three price conversions. The prices have not yet been normalised to the same VAT, unit of measure and currency basis, so comparing them is not meaningful. BestPurchPrice is also not accessible here. This event cannot be used.

  • OnAfterCalcBestDirectUnitCostFound(PurchPrice, BestPurchPriceFound, IsHandled): Fires after the entire loop has finished. The selection is already final and BestPurchPrice is not passed, so it is not possible to change which price was chosen. This event cannot be used.

  • OnBeforeCalcBestDirectUnitCost(PurchPrice, BestPurchPrice, BestPurchPriceFound, IsHandled, SKU, Item): A subscriber setting IsHandled := true must re-implement the complete loop including all conversion calls and selection logic. This event cannot be used.

  • An event after the case true of block: Considered as a simpler alternative - the standard rules would always run and the subscriber could override the result afterward. However, this has a critical edge case: when the standard second rule determines the current candidate is better and sets BestPurchPrice := PurchPrice, the event fires with BestPurchPrice and PurchPrice pointing to the same record. The subscriber has lost the previous best price and cannot restore it. For use cases that need to replace the cost comparison with a different criterion (e.g. pick by minimum-quantity tier), the subscriber must compare the old best against the current candidate - which is only possible before the case true of runs.

None of the existing events allow a subscriber to compare the unconverted current candidate against the current best price before the selection is made.


Performance Considerations

The event fires once per price record that passes the min-quantity check, inside the CalcBestDirectUnitCost loop. The overhead is the same as the existing OnCalcBestDirectUnitCostOnBeforeConvertPriceToVAT event, which already fires at the same frequency in the same loop. When no subscriber sets IsHandled := true, the standard case true of logic runs unchanged.


Data Sensitivity Review

The event parameters expose:

  • var PurchPrice: Record "Purchase Price" - the current candidate record, already converted; already in scope at this point.
  • var BestPurchPrice: Record "Purchase Price" - the current best price found so far; a temporary record local to CalcBestDirectUnitCost. Passed as var so subscribers can assign to it when they select a different winner.
  • var BestPurchPriceFound: Boolean - must be var so a subscriber that replaces the case true of block can also mark that a selection was made.
  • var IsHandled: Boolean - standard skip flag.
  • LineDiscPerCent: Decimal - the line discount percentage set via SetLineDisc before the loop began. Required so subscribers can replicate the effective-amount calculation used by the standard second rule: "Direct Unit Cost" * (1 - LineDiscPerCent / 100). The local CalcLineAmount procedure that performs this calculation is not accessible to subscribers.

All values are already in scope within the procedure at this point. BestPurchPrice is a temporary record that is not persisted. No sensitive data is introduced.


Multi-Extension Interaction

Multiple extensions can subscribe at the same time. The IsHandled pattern is standard across codeunit 7010. If multiple subscribers set IsHandled := true, each one runs in sequence and the last value written to BestPurchPrice is used. Extensions that only need to read BestPurchPrice as context without replacing the standard logic should leave IsHandled := false.


3. Event Request: Extend OnCalcBestDirectUnitCostOnBeforeNoPriceFound with PriceInSKU and SKU Parameters

Please extend the existing integration event OnCalcBestDirectUnitCostOnBeforeNoPriceFound in codeunit 7010 "Purch. Price Calc. Mgt." to include var PriceInSKU: Boolean and var SKU: Record "Stockkeeping Unit" as additional parameters.

When no purchase price is found for an item/vendor combination, CalcBestDirectUnitCost falls back to either SKU."Last Direct Cost" or Item."Last Direct Cost", depending on whether PriceInSKU evaluates to true. Some setups need to intercept this fallback - for example, to set the cost to 0 instead of using the item's last direct cost when a setup flag says that falling back to item cost is not allowed.

The existing event fires before PriceInSKU is recalculated on the line PriceInSKU := PriceInSKU and (SKU."Last Direct Cost" <> 0). A subscriber that sets IsHandled := true needs to make the same PriceInSKU evaluation itself to decide whether to use the SKU cost or apply custom logic. Without the current value of PriceInSKU and access to SKU."Last Direct Cost", the subscriber cannot make this decision correctly and is forced to duplicate internal logic or hardcode assumptions.

Adding PriceInSKU and SKU as var parameters gives subscribers the information they need without having to duplicate the internal state.


Alternatives Evaluated

The following existing events were evaluated:

  • OnCalcBestDirectUnitCostOnAfterSetUnitCost(var BestPurchPrice: Record "Purchase Price"): Fires after the fallback cost has been assigned and all three conversion procedures (ConvertPriceToVAT, ConvertPriceToUoM, ConvertPriceLCYToFCY) have already run against it. Changing the cost here does not undo those conversions. A subscriber cannot cleanly set the cost to 0 at this point. This event cannot be used.

  • OnBeforeCalcBestDirectUnitCost(PurchPrice, BestPurchPrice, BestPurchPriceFound, IsHandled, SKU, Item): A subscriber setting IsHandled := true must re-implement the complete price selection loop, all min-quantity checks, all conversions, and the fallback logic. This is not a viable targeted intercept for a change that affects only the fallback branch.

  • Adding a new second event after the PriceInSKU update line: This would work technically, but adds an extra event for a case that the existing OnCalcBestDirectUnitCostOnBeforeNoPriceFound already targets. Extending the existing event's parameters is simpler and less disruptive.

None of the existing events allow a subscriber to intercept the fallback cost assignment with full knowledge of what PriceInSKU will be.


Performance Considerations

The event fires at most once per CalcBestDirectUnitCost call, and only when no price was found (not BestPurchPriceFound). When purchase prices are configured for the item/vendor, this branch is never reached. Passing two additional parameters (Boolean and Record) to an existing event has no measurable impact. When a price is found, the if not BestPurchPriceFound then block is never entered and the event never fires..


Data Sensitivity Review

The two new parameters expose:

  • var PriceInSKU: Boolean - a computed flag indicating whether a stockkeeping unit with a non-zero last direct cost was matched. No sensitive data. Passed as var so a subscriber can update it if needed before the standard if not IsHandled then block runs.
  • var SKU: Record "Stockkeeping Unit" - the matched SKU record (if any). Already exposed by OnBeforeCalcBestDirectUnitCost in the same codeunit, setting the same precedent. Contains standard SKU fields including "Last Direct Cost".

Both values are already in scope within CalcBestDirectUnitCost at the event's insertion point and are used in the lines immediately following the event call. No sensitive data is introduced beyond what the procedure already handles.


Multi-Extension Interaction

The IsHandled pattern is unchanged. If multiple subscribers set IsHandled := true, each runs in sequence and the last value written to BestPurchPrice."Direct Unit Cost" is used. Because PriceInSKU is var, one subscriber's update to it is visible to subsequent subscribers in the same event firing, which is consistent with how var parameters work for integration events in Business Central.


Describe the request

Requested changes in CalcBestDirectUnitCost:

  1. Event Request: OnCalcBestDirectUnitCostOnAfterIsInMinQty
procedure CalcBestDirectUnitCost(var PurchPrice: Record "Purchase Price")
var
    BestPurchPrice: Record "Purchase Price";
    BestPurchPriceFound: Boolean;
    IsHandled: Boolean;
    InMinQty: Boolean;  //>> new local variable
begin
    IsHandled := false;
    OnBeforeCalcBestDirectUnitCost(PurchPrice, BestPurchPrice, BestPurchPriceFound, IsHandled, SKU, Item);
    if IsHandled then
        exit;

    FoundPurchPrice := PurchPrice.Find('-');
    if FoundPurchPrice then
        repeat
            //>> code change start
            InMinQty := IsInMinQty(PurchPrice."Unit of Measure Code", PurchPrice."Minimum Quantity");
            OnCalcBestDirectUnitCostOnAfterIsInMinQty(PurchPrice, QtyPerUOM, Qty, InMinQty);
            if InMinQty then begin
            //<< code change end
                OnCalcBestDirectUnitCostOnBeforeConvertPriceToVAT(PurchPrice);
                ConvertPriceToVAT(
                  Vend."Prices Including VAT", Item."VAT Prod. Posting Group",
                  Vend."VAT Bus. Posting Group", PurchPrice."Direct Unit Cost");
                ConvertPriceToUoM(PurchPrice."Unit of Measure Code", PurchPrice."Direct Unit Cost");
                ConvertPriceLCYToFCY(PurchPrice."Currency Code", PurchPrice."Direct Unit Cost");

                case true of
                    ((BestPurchPrice."Currency Code" = '') and (PurchPrice."Currency Code" <> '')) or
                    ((BestPurchPrice."Variant Code" = '') and (PurchPrice."Variant Code" <> '')):
                        begin
                            BestPurchPrice := PurchPrice;
                            BestPurchPriceFound := true;
                        end;
                    ((BestPurchPrice."Currency Code" = '') or (PurchPrice."Currency Code" <> '')) and
                  ((BestPurchPrice."Variant Code" = '') or (PurchPrice."Variant Code" <> '')):
                        if (BestPurchPrice."Direct Unit Cost" = 0) or
                           (CalcLineAmount(BestPurchPrice) > CalcLineAmount(PurchPrice))
                        then begin
                            BestPurchPrice := PurchPrice;
                            BestPurchPriceFound := true;
                        end;
                end;
            end;
        until PurchPrice.Next() = 0;

    // ... remainder of procedure unchanged
end;

New event declaration:

[IntegrationEvent(false, false)]
local procedure OnCalcBestDirectUnitCostOnAfterIsInMinQty(var PurchPrice: Record "Purchase Price"; QtyPerUOM: Decimal; Qty: Decimal; var InMinQty: Boolean)
begin
end;

Subscriber example:

[EventSubscriber(ObjectType::Codeunit, Codeunit::"Purch. Price Calc. Mgt.", 'OnCalcBestDirectUnitCostOnAfterIsInMinQty', '', false, false)]
local procedure OnAfterIsInMinQtyHandler(var PurchPrice: Record "Purchase Price"; QtyPerUOM: Decimal; Qty: Decimal; var InMinQty: Boolean)
var
    OrderQty: Decimal;
begin
    // "EXT Qty. Reference" is a custom field on Purchase Price that controls
    // whether the minimum quantity is checked against the individual line
    // quantity or the total order quantity.
    case PurchPrice."EXT Qty. Reference" of
        PurchPrice."EXT Qty. Reference"::Order:
            begin
                OrderQty := GetEXTOrderTotalQty(PurchPrice."Item No.", PurchPrice."Unit of Measure Code");
                if PurchPrice."Unit of Measure Code" = '' then
                    InMinQty := PurchPrice."Minimum Quantity" <= QtyPerUOM * OrderQty
                else
                    InMinQty := PurchPrice."Minimum Quantity" <= OrderQty;
            end;
        PurchPrice."EXT Qty. Reference"::Line:
            ; // keep the result from the standard IsInMinQty call
    end;
end;

2. Event Request: OnCalcBestDirectUnitCostOnBeforeCaseStatement

procedure CalcBestDirectUnitCost(var PurchPrice: Record "Purchase Price")
var
    BestPurchPrice: Record "Purchase Price";
    BestPurchPriceFound: Boolean;
    IsHandled: Boolean;
begin
    IsHandled := false;
    OnBeforeCalcBestDirectUnitCost(PurchPrice, BestPurchPrice, BestPurchPriceFound, IsHandled, SKU, Item);
    if IsHandled then
        exit;

    FoundPurchPrice := PurchPrice.Find('-');
    if FoundPurchPrice then
        repeat
            if IsInMinQty(PurchPrice."Unit of Measure Code", PurchPrice."Minimum Quantity") then begin
                OnCalcBestDirectUnitCostOnBeforeConvertPriceToVAT(PurchPrice);
                ConvertPriceToVAT(
                  Vend."Prices Including VAT", Item."VAT Prod. Posting Group",
                  Vend."VAT Bus. Posting Group", PurchPrice."Direct Unit Cost");
                ConvertPriceToUoM(PurchPrice."Unit of Measure Code", PurchPrice."Direct Unit Cost");
                ConvertPriceLCYToFCY(PurchPrice."Currency Code", PurchPrice."Direct Unit Cost");

                //>> code change start
                IsHandled := false;
                OnCalcBestDirectUnitCostOnBeforeCaseStatement(PurchPrice, BestPurchPrice, BestPurchPriceFound, IsHandled, LineDiscPerCent);
                if not IsHandled then
                //<< code change end
                    case true of
                        ((BestPurchPrice."Currency Code" = '') and (PurchPrice."Currency Code" <> '')) or
                        ((BestPurchPrice."Variant Code" = '') and (PurchPrice."Variant Code" <> '')):
                            begin
                                BestPurchPrice := PurchPrice;
                                BestPurchPriceFound := true;
                            end;
                        ((BestPurchPrice."Currency Code" = '') or (PurchPrice."Currency Code" <> '')) and
                      ((BestPurchPrice."Variant Code" = '') or (PurchPrice."Variant Code" <> '')):
                            if (BestPurchPrice."Direct Unit Cost" = 0) or
                               (CalcLineAmount(BestPurchPrice) > CalcLineAmount(PurchPrice))
                            then begin
                                BestPurchPrice := PurchPrice;
                                BestPurchPriceFound := true;
                            end;
                    end;
            end;
        until PurchPrice.Next() = 0;

    // ... remainder of procedure unchanged
end;

New event declaration:

[IntegrationEvent(false, false)]
local procedure OnCalcBestDirectUnitCostOnBeforeCaseStatement(var PurchPrice: Record "Purchase Price"; var BestPurchPrice: Record "Purchase Price"; var BestPurchPriceFound: Boolean; var IsHandled: Boolean; LineDiscPerCent: Decimal)
begin
end;

Subscriber example:

[EventSubscriber(ObjectType::Codeunit, Codeunit::"Purch. Price Calc. Mgt.", 'OnCalcBestDirectUnitCostOnBeforeCaseStatement', '', false, false)]
local procedure OnBeforeCaseStatementHandler(var PurchPrice: Record "Purchase Price"; var BestPurchPrice: Record "Purchase Price"; var BestPurchPriceFound: Boolean; var IsHandled: Boolean; LineDiscPerCent: Decimal)
var
    BestEffectiveAmt: Decimal;
    CurrEffectiveAmt: Decimal;
begin
    // "EXT Best Price" is a custom setup flag. When off, select by
    // minimum-quantity tier and starting date instead of lowest cost.
    // When on, replicate the standard cost comparison using LineDiscPerCent
    // since CalcLineAmount is local and not accessible here.
    if not GetEXTBestPriceActive() then begin
        if (BestPurchPrice."Direct Unit Cost" = 0) or
           (BestPurchPrice."Minimum Quantity" <= PurchPrice."Minimum Quantity") or
           (BestPurchPrice."Starting Date" <= PurchPrice."Starting Date")
        then begin
            BestPurchPrice := PurchPrice;
            BestPurchPriceFound := true;
        end;
        IsHandled := true;
    end else begin
        BestEffectiveAmt := BestPurchPrice."Direct Unit Cost" * (1 - LineDiscPerCent / 100);
        CurrEffectiveAmt := PurchPrice."Direct Unit Cost" * (1 - LineDiscPerCent / 100);
        if (BestPurchPrice."Direct Unit Cost" = 0) or (BestEffectiveAmt > CurrEffectiveAmt) then begin
            BestPurchPrice := PurchPrice;
            BestPurchPriceFound := true;
        end;
        IsHandled := true;
    end;
end;

3. Event Request: Extend OnCalcBestDirectUnitCostOnBeforeNoPriceFound with PriceInSKU and SKU Parameters

[IntegrationEvent(false, false)]
local procedure OnCalcBestDirectUnitCostOnBeforeNoPriceFound(var BestPurchPrice: Record "Purchase Price"; Item: Record Item; var IsHandled: Boolean)
begin
end;

Updated event declaration:

[IntegrationEvent(false, false)]
local procedure OnCalcBestDirectUnitCostOnBeforeNoPriceFound(var BestPurchPrice: Record "Purchase Price"; Item: Record Item; var IsHandled: Boolean; var PriceInSKU: Boolean; var SKU: Record "Stockkeeping Unit") //>> PriceInSKU and SKU are new parameters
begin
end;

Requested change in CalcBestDirectUnitCost (call site only - no structural change):

    // No price found in agreement
    if not BestPurchPriceFound then begin
        IsHandled := false;
        //>> code change start
        OnCalcBestDirectUnitCostOnBeforeNoPriceFound(BestPurchPrice, Item, IsHandled, PriceInSKU, SKU);
        //<< code change end
        if not IsHandled then begin
            PriceInSKU := PriceInSKU and (SKU."Last Direct Cost" <> 0);
            if PriceInSKU then
                BestPurchPrice."Direct Unit Cost" := SKU."Last Direct Cost"
            else
                BestPurchPrice."Direct Unit Cost" := Item."Last Direct Cost";
        end;
        ConvertPriceToVAT(false, Item."VAT Prod. Posting Group", '', BestPurchPrice."Direct Unit Cost");
        ConvertPriceToUoM('', BestPurchPrice."Direct Unit Cost");
        ConvertPriceLCYToFCY('', BestPurchPrice."Direct Unit Cost");
        OnCalcBestDirectUnitCostOnAfterSetUnitCost(BestPurchPrice);
    end;

Subscriber example:

[EventSubscriber(ObjectType::Codeunit, Codeunit::"Purch. Price Calc. Mgt.", 'OnCalcBestDirectUnitCostOnBeforeNoPriceFound', '', false, false)]
local procedure OnBeforeNoPriceFoundHandler(var BestPurchPrice: Record "Purchase Price"; Item: Record Item; var IsHandled: Boolean; var PriceInSKU: Boolean; var SKU: Record "Stockkeeping Unit")
var
    EXTPurchSetup: Record "EXT Purchases Setup";
begin
    PriceInSKU := PriceInSKU and (SKU."Last Direct Cost" <> 0);
    if PriceInSKU then
        // SKU has a last direct cost - use it as usual.
        BestPurchPrice."Direct Unit Cost" := SKU."Last Direct Cost"
    else begin
        EXTPurchSetup.Get();
        if EXTPurchSetup."Use Item Last Direct Cost" then
            // Setup allows falling back to the item's last direct cost.
            BestPurchPrice."Direct Unit Cost" := Item."Last Direct Cost"
        else
            // Setup does not allow the fallback - force cost to 0 so
            // the buyer is required to enter the price manually.
            BestPurchPrice."Direct Unit Cost" := 0;
    end;
    IsHandled := true;
end;

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