Add SCM functional knowledge domain

Introduce nine source-backed rules with original AL sample pairs, bounded SCM review routing, and complete positive/clean evaluation coverage.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Jesper Schulz-Wedde 2026-09-17 15:54:23 +02:00
parent b91443beec
commit 9ac62e3967
34 changed files with 1244 additions and 10 deletions

View file

@ -0,0 +1,18 @@
namespace BCQuality.SCM.Samples;
using Microsoft.Inventory.Tracking;
using Microsoft.Sales.Document;
codeunit 50106 "SCM Cancel Reservation Bad"
{
procedure CancelSalesReservation(ReservationEntryNo: Integer)
var
ReservationEntry: Record "Reservation Entry";
begin
ReservationEntry.Get(ReservationEntryNo, false);
ReservationEntry.TestField("Source Type", Database::"Sales Line");
ReservationEntry.TestField("Reservation Status", ReservationEntry."Reservation Status"::Reservation);
ReservationEntry.Delete(true);
end;
}

View file

@ -0,0 +1,19 @@
namespace BCQuality.SCM.Samples;
using Microsoft.Inventory.Tracking;
using Microsoft.Sales.Document;
codeunit 50107 "SCM Cancel Reservation Good"
{
procedure CancelSalesReservation(ReservationEntryNo: Integer)
var
ReservationEntry: Record "Reservation Entry";
ReservationEngineMgt: Codeunit "Reservation Engine Mgt.";
begin
ReservationEntry.Get(ReservationEntryNo, false);
ReservationEntry.TestField("Source Type", Database::"Sales Line");
ReservationEntry.TestField("Reservation Status", ReservationEntry."Reservation Status"::Reservation);
ReservationEngineMgt.CancelReservation(ReservationEntry);
end;
}

View file

@ -0,0 +1,39 @@
---
bc-version: [all]
domain: scm
keywords: [reservation-entry, cancelreservation, reservation-engine-mgt, reservation-status, order-tracking, disallow-cancellation]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Cancel reservations through reservation management
## Description
Persistent `"Reservation Entry"` rows are not disposable allocation markers. Reservation and Tracking links use an entry-number pair with opposite `Positive` values, while Surplus and Prospect entries can legitimately be unpaired. Cancelling a binding reservation must handle its counterpart and any remaining item tracking or order tracking, not just remove a row.
## Best Practice
For explicit cancellation of an existing binding reservation, use `"Reservation Engine Mgt.".CancelReservation`. It checks the reservation status and `"Disallow Cancellation"`, handles the counterpart, and preserves or retracks the remaining source quantities as appropriate. For source-line quantity changes, use the source-specific reservation management path rather than deleting its reservation rows yourself.
Do not require every Reservation Entry to have a partner or identical lot/serial values on both sides: Surplus/Prospect entries and supported late-binding scenarios need different treatment. Temporary buffers, engine-owned updates, and supported publisher metadata are not independent cancellation. Cancelling a reservation is also different from intentionally removing an item-tracking assignment.
The samples retrieve the negative side of a persistent sales-line reservation and cancel only the binding. They do not delete the sales line or remove its tracking specifications.
## Anti Pattern
Report `Delete(true)`, `DeleteAll`, or a status/source rewrite on persistent `"Reservation Entry"` records used as a replacement for cancelling a reservation. Deleting both sides is not a sufficient repair: it can still discard tracking that should survive and omit retracking.
Require cancellation intent and a binding reservation in the visible context. Do not flag normal processing of temporary Prospect/Surplus buffers or diagnose every single row as an orphan.
## Samples
- [`cancel-reservations-through-reservation-management.bad.al`](cancel-reservations-through-reservation-management.bad.al)
- [`cancel-reservations-through-reservation-management.good.al`](cancel-reservations-through-reservation-management.good.al)
## References
- [Reservation, order tracking, and action messaging](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-reservation-order-tracking-and-action-messaging)
- [Item tracking and reservations](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-item-tracking-and-reservations)
- [BaseApp reservation cancellation](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Tracking/ReservationEngineMgt.Codeunit.al#L51-L90)

View file

@ -0,0 +1,53 @@
namespace BCQuality.SCM.Samples;
using Microsoft.Inventory.Requisition;
using Microsoft.Purchases.Document;
using Microsoft.Sales.Document;
codeunit 50116 "SCM Requisition Action Bad"
{
procedure CarryOutAcceptedNewPurchase(TemplateName: Code[10]; BatchName: Code[10]; LineNo: Integer; OrderDate: Date; PostingDate: Date; ReceiptDate: Date; CutoffDate: Date)
var
RequisitionLine: Record "Requisition Line";
PurchaseHeader: Record "Purchase Header";
PurchaseLine: Record "Purchase Line";
begin
if (OrderDate = 0D) or (PostingDate = 0D) or (ReceiptDate = 0D) or (CutoffDate = 0D) then
Error(PlanningDatesErr);
RequisitionLine.Get(TemplateName, BatchName, LineNo);
RequisitionLine.TestField(Type, RequisitionLine.Type::Item);
RequisitionLine.TestField("Replenishment System", RequisitionLine."Replenishment System"::Purchase);
RequisitionLine.TestField("Action Message", RequisitionLine."Action Message"::New);
RequisitionLine.TestField("Accept Action Message", true);
RequisitionLine.TestField("Demand Type", Database::"Sales Line");
RequisitionLine.TestField("Demand Order No.");
RequisitionLine.TestField("Vendor No.");
RequisitionLine.SetRecFilter();
PurchaseHeader.Init();
PurchaseHeader."Document Type" := PurchaseHeader."Document Type"::Order;
PurchaseHeader.Insert(true);
PurchaseHeader.Validate("Buy-from Vendor No.", RequisitionLine."Vendor No.");
PurchaseHeader.Validate("Order Date", OrderDate);
PurchaseHeader.Validate("Posting Date", PostingDate);
PurchaseHeader.Validate("Expected Receipt Date", ReceiptDate);
PurchaseHeader.Modify(true);
PurchaseLine.Init();
PurchaseLine."Document Type" := PurchaseHeader."Document Type";
PurchaseLine."Document No." := PurchaseHeader."No.";
PurchaseLine."Line No." := 10000;
PurchaseLine.Validate(Type, PurchaseLine.Type::Item);
PurchaseLine.Validate("No.", RequisitionLine."No.");
PurchaseLine.Validate("Location Code", RequisitionLine."Location Code");
PurchaseLine.Validate("Variant Code", RequisitionLine."Variant Code");
PurchaseLine.Validate("Unit of Measure Code", RequisitionLine."Unit of Measure Code");
PurchaseLine.Validate(Quantity, RequisitionLine.Quantity);
PurchaseLine.Insert(true);
RequisitionLine.Delete(true);
end;
var
PlanningDatesErr: Label 'Supply explicit order, posting, receipt, and cutoff dates.';
}

View file

@ -0,0 +1,37 @@
namespace BCQuality.SCM.Samples;
using Microsoft.Inventory.Requisition;
using Microsoft.Purchases.Document;
using Microsoft.Sales.Document;
codeunit 50117 "SCM Requisition Action Good"
{
procedure CarryOutAcceptedNewPurchase(TemplateName: Code[10]; BatchName: Code[10]; LineNo: Integer; OrderDate: Date; PostingDate: Date; ReceiptDate: Date; CutoffDate: Date)
var
RequisitionLine: Record "Requisition Line";
PurchaseHeaderDefaults: Record "Purchase Header";
ReqWkshMakeOrder: Codeunit "Req. Wksh.-Make Order";
begin
if (OrderDate = 0D) or (PostingDate = 0D) or (ReceiptDate = 0D) or (CutoffDate = 0D) then
Error(PlanningDatesErr);
RequisitionLine.Get(TemplateName, BatchName, LineNo);
RequisitionLine.TestField(Type, RequisitionLine.Type::Item);
RequisitionLine.TestField("Replenishment System", RequisitionLine."Replenishment System"::Purchase);
RequisitionLine.TestField("Action Message", RequisitionLine."Action Message"::New);
RequisitionLine.TestField("Accept Action Message", true);
RequisitionLine.TestField("Demand Type", Database::"Sales Line");
RequisitionLine.TestField("Demand Order No.");
RequisitionLine.TestField("Vendor No.");
RequisitionLine.SetRecFilter();
PurchaseHeaderDefaults."Order Date" := OrderDate;
PurchaseHeaderDefaults."Posting Date" := PostingDate;
PurchaseHeaderDefaults."Expected Receipt Date" := ReceiptDate;
ReqWkshMakeOrder.Set(PurchaseHeaderDefaults, CutoffDate, false);
ReqWkshMakeOrder.SetSuppressCommit(true);
ReqWkshMakeOrder.CarryOutBatchAction(RequisitionLine);
end;
var
PlanningDatesErr: Label 'Supply explicit order, posting, receipt, and cutoff dates.';
}

View file

@ -0,0 +1,44 @@
---
bc-version: [all]
domain: scm
keywords: [requisition-line, action-message, accept-action-message, req-wksh-make-order, carryoutbatchaction, demand-order-no, planning-flexibility]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Carry out requisition actions through the standard workflow
## Description
A requisition/planning line is a pending change to a supply/demand network, not just a template for a purchase line. Carry-out interprets New, change-quantity, reschedule, and cancel actions, preserves referenced supply and planning flexibility, and moves reservation/tracking ownership before finalizing the proposal. Creating a plausible purchase order and deleting the requisition line can leave new supply unrelated to the demand that caused it.
## Best Practice
For requisition batch carry-out, initialize `"Req. Wksh.-Make Order"` with `Set` and invoke `CarryOutBatchAction` on the intended accepted lines. Supply the order/posting/receipt defaults separately from the ending-order-date cutoff, and preserve the selected worksheet/batch/line filters. A plain `Run` or a single order-line insertion helper is not a replacement for this batch initialization and finalization.
Use the standard `"Carry Out Action"` dispatch for broader planning output and its configured purchase, transfer, assembly, or manufacturing choices. Do not turn every action into a new purchase order, bypass source-specific reservation transfer, or delete proposals before the owning workflow has completed their supply change.
Ordinary manual purchase creation that does not consume planning output is outside this rule. Users may reject or delete unwanted proposals without creating supply; temporary planning simulations, pre-carry-out enrichment, and engine-owned cleanup are also legitimate. `Delete(true)` on a requisition line is not intrinsically a defect.
The samples select an existing accepted New/Purchase item proposal with sales-demand context. Dates are explicit, the source selection remains bounded, and the clean sample leaves order creation and reservation handoff to the standard workflow; it is not a complete planning-run generator.
## Anti Pattern
Report code that consumes accepted persistent `"Requisition Line"` action messages, manually creates or changes supply from a subset of fields, and then deletes or marks the proposal handled without the standard carry-out/source-reservation handoff. Running purchase-field validation and the requisition delete trigger does not first move the proposal's demand links to the new purchase line.
Require both proposal-consumption intent and a visible supply conversion. Do not flag an isolated deletion of an unwanted suggestion, an ordinary purchase-order API, or the standard carry-out engine's own insert/delete sequence.
## Samples
- [`carry-out-requisition-actions-through-the-standard-workflow.bad.al`](carry-out-requisition-actions-through-the-standard-workflow.bad.al)
- [`carry-out-requisition-actions-through-the-standard-workflow.good.al`](carry-out-requisition-actions-through-the-standard-workflow.good.al)
## References
- [Perform planning action messages](https://learn.microsoft.com/en-us/dynamics365/business-central/production-how-to-run-mps-and-mrp#to-perform-action-messages)
- [Planning functionality](https://learn.microsoft.com/en-us/dynamics365/business-central/production-about-planning-functionality)
- [Reservation, order tracking, and action messaging](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-reservation-order-tracking-and-action-messaging)
- [BaseApp carry-out caller and date defaults](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Requisition/CarryOutActionMsgReq.Report.al#L116-L133)
- [Batch initialization and selection](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Requisition/ReqWkshMakeOrder.Codeunit.al#L116-L215)
- [Reservation handoff before supply finalization](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Requisition/ReqWkshMakeOrder.Codeunit.al#L663-L743)

View file

@ -0,0 +1,37 @@
namespace BCQuality.SCM.Samples;
using Microsoft.Inventory.Ledger;
codeunit 50104 "SCM Item Application Bad"
{
procedure ChangeSalesQuantityApplication(ApplicationEntryNo: Integer; NewInboundEntryNo: Integer)
var
ItemApplicationEntry: Record "Item Application Entry";
OutboundItemLedgerEntry: Record "Item Ledger Entry";
InboundItemLedgerEntry: Record "Item Ledger Entry";
begin
ItemApplicationEntry.Get(ApplicationEntryNo);
ItemApplicationEntry.TestField(Quantity);
ItemApplicationEntry.TestField("Inbound Item Entry No.");
ItemApplicationEntry.TestField("Outbound Item Entry No.");
ItemApplicationEntry.TestField("Transferred-from Entry No.", 0);
if ItemApplicationEntry.CostApplication() then
Error(QuantityApplicationErr);
OutboundItemLedgerEntry.Get(ItemApplicationEntry."Outbound Item Entry No.");
OutboundItemLedgerEntry.TestField("Entry Type", OutboundItemLedgerEntry."Entry Type"::Sale);
OutboundItemLedgerEntry.TestField(Positive, false);
OutboundItemLedgerEntry.TestField("Drop Shipment", false);
OutboundItemLedgerEntry.TestField(Correction, false);
InboundItemLedgerEntry.Get(NewInboundEntryNo);
InboundItemLedgerEntry.TestField(Positive, true);
InboundItemLedgerEntry.TestField("Item No.", OutboundItemLedgerEntry."Item No.");
InboundItemLedgerEntry.TestField("Variant Code", OutboundItemLedgerEntry."Variant Code");
InboundItemLedgerEntry.TestField("Location Code", OutboundItemLedgerEntry."Location Code");
ItemApplicationEntry."Inbound Item Entry No." := NewInboundEntryNo;
ItemApplicationEntry.Modify(true);
end;
var
QuantityApplicationErr: Label 'Select an ordinary quantity application, not a cost application.';
}

View file

@ -0,0 +1,45 @@
namespace BCQuality.SCM.Samples;
using Microsoft.Inventory.Ledger;
using Microsoft.Inventory.Posting;
codeunit 50105 "SCM Item Application Good"
{
procedure ChangeSalesQuantityApplication(ApplicationEntryNo: Integer; NewInboundEntryNo: Integer)
var
ItemApplicationEntry: Record "Item Application Entry";
OutboundItemLedgerEntry: Record "Item Ledger Entry";
InboundItemLedgerEntry: Record "Item Ledger Entry";
ItemJnlPostLine: Codeunit "Item Jnl.-Post Line";
OutboundEntryNo: Integer;
begin
ItemApplicationEntry.Get(ApplicationEntryNo);
ItemApplicationEntry.TestField(Quantity);
ItemApplicationEntry.TestField("Inbound Item Entry No.");
ItemApplicationEntry.TestField("Outbound Item Entry No.");
ItemApplicationEntry.TestField("Transferred-from Entry No.", 0);
if ItemApplicationEntry.CostApplication() then
Error(QuantityApplicationErr);
OutboundEntryNo := ItemApplicationEntry."Outbound Item Entry No.";
OutboundItemLedgerEntry.Get(OutboundEntryNo);
OutboundItemLedgerEntry.TestField("Entry Type", OutboundItemLedgerEntry."Entry Type"::Sale);
OutboundItemLedgerEntry.TestField(Positive, false);
OutboundItemLedgerEntry.TestField("Drop Shipment", false);
OutboundItemLedgerEntry.TestField(Correction, false);
InboundItemLedgerEntry.Get(NewInboundEntryNo);
InboundItemLedgerEntry.TestField(Positive, true);
InboundItemLedgerEntry.TestField("Item No.", OutboundItemLedgerEntry."Item No.");
InboundItemLedgerEntry.TestField("Variant Code", OutboundItemLedgerEntry."Variant Code");
InboundItemLedgerEntry.TestField("Location Code", OutboundItemLedgerEntry."Location Code");
ItemJnlPostLine.UnApply(ItemApplicationEntry);
OutboundItemLedgerEntry.Get(OutboundEntryNo);
ItemJnlPostLine.ReApply(OutboundItemLedgerEntry, NewInboundEntryNo);
ItemJnlPostLine.RedoApplications();
ItemJnlPostLine.CostAdjust();
ItemJnlPostLine.ClearApplicationLog();
end;
var
QuantityApplicationErr: Label 'Select an ordinary quantity application, not a cost application.';
}

View file

@ -0,0 +1,40 @@
---
bc-version: [all]
domain: scm
keywords: [item-application-entry, inbound-item-entry-no, unapply, reapply, redoapplications, costadjust, application-worksheet]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Change item applications through posting routines
## Description
An `"Item Application Entry"` connects quantity application to cost flow; changing its inbound entry number is not merely fixing a foreign key. Unapplication/reapplication also affects ledger remaining quantities, open states, valuation, and entries needing cost adjustment. Direct edits can leave a plausible application row attached to inconsistent inventory and costs.
## Best Practice
Prefer the Application Worksheet for interactive corrections. For a narrowly controlled programmatic correction of an ordinary quantity application, use the same `"Item Jnl.-Post Line"` instance for `UnApply`, reload the affected outbound item entry, then `ReApply` it to the compatible inbound entry. Complete the application's `RedoApplications`, `CostAdjust`, and `ClearApplicationLog` lifecycle; do not commit a half-completed replacement.
Respect the posting routines' inventory-period, correction, transfer, and drop-shipment restrictions rather than bypassing them. `"Transferred-from Entry No."`, outbound transfers, and special application types are not permission to reuse the ordinary-sales sample unchecked. Do not enable application-check bypasses or borrow the worksheet's multi-step recovery flags for a standalone transaction.
`CostAdjust` honors automatic-cost-adjustment setup; calling it does not promise that all costs are settled when adjustment is disabled or deferred. Retain the required scheduled/manual adjustment process. Temporary application projections, extension metadata, and source-document reservation/order-tracking changes are not edits to the persistent item-application graph.
## Anti Pattern
Report independent `Modify`, `Delete`, or replacement `Insert` operations on persistent `"Item Application Entry"` rows used to repoint a receipt/shipment application, including a change to `"Inbound Item Entry No."` that leaves remaining quantities and cost propagation untouched. Valid item numbers, matching quantities, or running table triggers do not complete reapplication.
Also report a visibly incomplete custom unapply/reapply transaction that omits finalization or commits between the two operations. Do not flag code merely because the standard posting/application workflow internally writes these tables.
## Samples
- [`change-item-applications-through-posting-routines.bad.al`](change-item-applications-through-posting-routines.bad.al)
- [`change-item-applications-through-posting-routines.good.al`](change-item-applications-through-posting-routines.good.al)
## References
- [Item application design](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-item-application)
- [Cost adjustment design](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-cost-adjustment)
- [BaseApp application finalization sequence](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Ledger/ApplicationWorksheet.Page.al#L495-L503)
- [BaseApp reapplication and cost-adjustment lifecycle](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Posting/ItemJnlPostLine.Codeunit.al#L5464-L5538)

View file

@ -0,0 +1,37 @@
namespace BCQuality.SCM.Samples;
using Microsoft.Inventory.Journal;
using Microsoft.Inventory.Ledger;
codeunit 50100 "SCM Stock Adjustment Bad"
{
procedure PostPreparedPositiveAdjustment(ItemJournalLine: Record "Item Journal Line"; NewEntryNo: Integer)
var
ItemLedgerEntry: Record "Item Ledger Entry";
begin
ItemJournalLine.TestField("Entry Type", ItemJournalLine."Entry Type"::"Positive Adjmt.");
ItemJournalLine.TestField("Value Entry Type", ItemJournalLine."Value Entry Type"::"Direct Cost");
ItemJournalLine.TestField("Item No.");
ItemJournalLine.TestField("Posting Date");
ItemJournalLine.TestField("Quantity (Base)");
if ItemJournalLine."Quantity (Base)" < 0 then
Error(PositiveQuantityErr);
ItemLedgerEntry.Init();
ItemLedgerEntry."Entry No." := NewEntryNo;
ItemLedgerEntry."Item No." := ItemJournalLine."Item No.";
ItemLedgerEntry."Entry Type" := ItemJournalLine."Entry Type";
ItemLedgerEntry."Posting Date" := ItemJournalLine."Posting Date";
ItemLedgerEntry."Document No." := ItemJournalLine."Document No.";
ItemLedgerEntry."Location Code" := ItemJournalLine."Location Code";
ItemLedgerEntry."Variant Code" := ItemJournalLine."Variant Code";
ItemLedgerEntry.Quantity := ItemJournalLine."Quantity (Base)";
ItemLedgerEntry."Remaining Quantity" := ItemLedgerEntry.Quantity;
ItemLedgerEntry.Positive := true;
ItemLedgerEntry.Open := true;
ItemLedgerEntry.Insert(true);
end;
var
PositiveQuantityErr: Label 'The prepared adjustment must increase inventory.';
}

View file

@ -0,0 +1,25 @@
namespace BCQuality.SCM.Samples;
using Microsoft.Inventory.Journal;
using Microsoft.Inventory.Posting;
codeunit 50101 "SCM Stock Adjustment Good"
{
procedure PostPreparedPositiveAdjustment(var ItemJournalLine: Record "Item Journal Line")
var
ItemJnlPostLine: Codeunit "Item Jnl.-Post Line";
begin
ItemJournalLine.TestField("Entry Type", ItemJournalLine."Entry Type"::"Positive Adjmt.");
ItemJournalLine.TestField("Value Entry Type", ItemJournalLine."Value Entry Type"::"Direct Cost");
ItemJournalLine.TestField("Item No.");
ItemJournalLine.TestField("Posting Date");
ItemJournalLine.TestField("Quantity (Base)");
if ItemJournalLine."Quantity (Base)" < 0 then
Error(PositiveQuantityErr);
ItemJnlPostLine.RunWithCheck(ItemJournalLine);
end;
var
PositiveQuantityErr: Label 'The prepared adjustment must increase inventory.';
}

View file

@ -0,0 +1,43 @@
---
bc-version: [all]
domain: scm
keywords: [item-ledger-entry, value-entry, item-journal-line, item-jnl-post-line, runwithcheck, inventory-posting]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Post item ledger changes through item journals
## Description
An item ledger entry is not an independently insertable stock balance. Posting connects its quantity to item applications, reservations, tracking, and one or more value entries; expected cost, invoicing, revaluation, and later cost adjustment can produce different value entries for the same item entry. Running a table's insert trigger does not run this posting workflow or its configured inventory-to-G/L integration.
## Best Practice
For a prepared standalone item-journal movement, enter through codeunit `"Item Jnl.-Post Line".RunWithCheck`. For a persisted journal batch, use `"Item Jnl.-Post Batch"`; for a sales, purchase, transfer, assembly, or production transaction, retain that workflow's owning document/posting orchestration rather than replacing it with a naked journal call. Let the posting engine create the ledger, value, and application records and perform its checks.
Extend supported posting events and pass validated journal/source data into the owning workflow. Read-only ledger queries, temporary previews, extension-owned metadata fields, and supported publisher parameters consumed by the poster are not independent ledger posting and must not be flagged merely because they assign record fields. A publisher's `var` parameter or `IsHandled` flag is not blanket authorization to recreate quantity/cost state. A change inside the posting engine itself requires tracing that engine's surrounding invariants, not a ban on its own inserts.
Do not require every value entry to point to an item ledger entry: capacity and production WIP have their own supported posting relationships. Assembly and manufacturing posting retain order/component/routing and capacity context; one bare output/consumption call is not full order completion.
The clean sample takes an already prepared positive-adjustment journal line. It is not a substitute for journal preparation, batch revaluation, warehouse reconciliation, or source-document posting.
## Anti Pattern
Report extension code that independently inserts/deletes persistent `"Item Ledger Entry"` or `"Value Entry"` transaction rows, or overwrites posted quantity, remaining quantity, application identity, or cost amounts to implement a receipt, shipment, adjustment, or cost correction. `Insert(true)`, `Modify(true)`, and balanced-looking quantities do not supply the missing posting orchestration.
Require evidence of a persistent transaction mutation and its business purpose; a table declaration or a write to a custom annotation field is insufficient. For a more specific revaluation or application defect, prefer the corresponding SCM article rather than reporting the same correction twice.
## Samples
- [`post-item-ledger-changes-through-item-journals.bad.al`](post-item-ledger-changes-through-item-journals.bad.al)
- [`post-item-ledger-changes-through-item-journals.good.al`](post-item-ledger-changes-through-item-journals.good.al)
## References
- [Inventory posting design](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-inventory-posting)
- [Item application design](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-item-application)
- [BaseApp item-journal posting entry point](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Posting/ItemJnlPostLine.Codeunit.al#L162-L179)
- [Assembly-order posting context](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-assembly-order-posting)
- [Production-order posting context](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-production-order-posting)

View file

@ -0,0 +1,22 @@
namespace BCQuality.SCM.Samples;
using Microsoft.Inventory.Journal;
using Microsoft.Inventory.Posting;
codeunit 50102 "SCM Revaluation Batch Bad"
{
procedure PostCalculatedRevaluationBatch(TemplateName: Code[10]; BatchName: Code[10])
var
ItemJournalLine: Record "Item Journal Line";
ItemJnlPostLine: Codeunit "Item Jnl.-Post Line";
begin
ItemJournalLine.SetRange("Journal Template Name", TemplateName);
ItemJournalLine.SetRange("Journal Batch Name", BatchName);
ItemJournalLine.FindSet();
repeat
ItemJournalLine.TestField("Value Entry Type", ItemJournalLine."Value Entry Type"::Revaluation);
ItemJournalLine.TestField("Inventory Value Per");
ItemJnlPostLine.RunWithCheck(ItemJournalLine);
until ItemJournalLine.Next() = 0;
end;
}

View file

@ -0,0 +1,21 @@
namespace BCQuality.SCM.Samples;
using Microsoft.Inventory.Journal;
using Microsoft.Inventory.Posting;
codeunit 50103 "SCM Revaluation Batch Good"
{
procedure PostCalculatedRevaluationBatch(TemplateName: Code[10]; BatchName: Code[10])
var
ItemJournalLine: Record "Item Journal Line";
ItemJnlPostBatch: Codeunit "Item Jnl.-Post Batch";
begin
ItemJournalLine.SetRange("Journal Template Name", TemplateName);
ItemJournalLine.SetRange("Journal Batch Name", BatchName);
ItemJournalLine.FindFirst();
ItemJournalLine.TestField("Value Entry Type", ItemJournalLine."Value Entry Type"::Revaluation);
ItemJournalLine.TestField("Inventory Value Per");
ItemJnlPostBatch.Run(ItemJournalLine);
end;
}

View file

@ -0,0 +1,39 @@
---
bc-version: [all]
domain: scm
keywords: [revaluation, inventory-value-per, partial-revaluation, item-jnl-post-batch, item-journal-line, runwithcheck, standard-cost]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Post calculated revaluation through the item journal batch
## Description
A calculated revaluation line with nonblank `"Inventory Value Per"` represents an aggregate, not a finalized posting against one item ledger entry. Codeunit `"Item Jnl.-Post Batch"` distributes that value over eligible entries, handles rounding, and coordinates Item/SKU standard-cost updates. Calling the line poster directly skips that batch work even though the input is a valid `"Item Journal Line"`.
## Best Practice
Post a prepared revaluation batch through `"Item Jnl.-Post Batch"`. Keep the calculated line's valuation date, aggregation scope, location/variant filters, and revaluation fields intact. The batch expands summarized values into per-entry postings and checks that the eligible inventory has not changed; for partial revaluation it also rechecks remaining quantity before posting.
Do not treat the public `"Item Jnl.-Post Line".RunWithCheck` API as a replacement for that orchestration. It remains legitimate for finalized individual-entry revaluation lines within a workflow that already supplies the necessary checks; the batch itself uses the line poster. A call to that API without evidence of summarized or partial revaluation is not this defect.
The samples explicitly require `"Value Entry Type" = Revaluation` and a nonblank `"Inventory Value Per"` in an existing calculated journal batch. They demonstrate posting, not how to calculate a new valuation or choose a standard cost.
## Anti Pattern
Report a loop that sends calculated aggregate revaluation lines straight to `"Item Jnl.-Post Line"`, or a custom partial-revaluation workflow that bypasses the remaining-quantity recheck visible in the standard batch. A loop over the journal is not equivalent to distributing the aggregate over its underlying item entries.
Do not recommend directly editing existing `"Value Entry"` cost amounts or the Item's unit cost to repair the result. Use the revaluation/cost-adjustment workflow appropriate to the correction.
## Samples
- [`post-revaluation-through-the-item-journal-batch.bad.al`](post-revaluation-through-the-item-journal-batch.bad.al)
- [`post-revaluation-through-the-item-journal-batch.good.al`](post-revaluation-through-the-item-journal-batch.good.al)
## References
- [Revaluation design](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-revaluation)
- [Inventory posting design](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-inventory-posting)
- [BaseApp summarized revaluation and remaining-quantity checks](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Posting/ItemJnlPostBatch.Codeunit.al#L510-L714)

View file

@ -0,0 +1,29 @@
namespace BCQuality.SCM.Samples;
using Microsoft.Inventory.Journal;
using Microsoft.Inventory.Location;
using Microsoft.Inventory.Posting;
using Microsoft.Inventory.Transfer;
codeunit 50112 "SCM Transfer Posting Bad"
{
procedure ShipTransferOrder(TransferOrderNo: Code[20]; var ItemJournalLine: Record "Item Journal Line")
var
TransferHeader: Record "Transfer Header";
Location: Record Location;
ItemJnlPostLine: Codeunit "Item Jnl.-Post Line";
begin
TransferHeader.Get(TransferOrderNo);
TransferHeader.TestField("Direct Transfer", false);
TransferHeader.TestField("In-Transit Code");
Location.Get(TransferHeader."Transfer-from Code");
Location.TestField("Require Shipment", false);
ItemJournalLine.TestField("Entry Type", ItemJournalLine."Entry Type"::Transfer);
ItemJournalLine.TestField("Location Code", TransferHeader."Transfer-from Code");
ItemJournalLine.TestField("New Location Code", TransferHeader."In-Transit Code");
ItemJnlPostLine.RunWithCheck(ItemJournalLine);
TransferHeader."Last Shipment No." := ItemJournalLine."Document No.";
TransferHeader.Modify(true);
end;
}

View file

@ -0,0 +1,37 @@
namespace BCQuality.SCM.Samples;
using Microsoft.Inventory.Location;
using Microsoft.Inventory.Transfer;
codeunit 50113 "SCM Transfer Posting Good"
{
procedure ShipTransferOrder(TransferOrderNo: Code[20])
var
TransferHeader: Record "Transfer Header";
Location: Record Location;
TransferOrderPostShipment: Codeunit "TransferOrder-Post Shipment";
begin
TransferHeader.Get(TransferOrderNo);
TransferHeader.TestField("Direct Transfer", false);
TransferHeader.TestField("In-Transit Code");
Location.Get(TransferHeader."Transfer-from Code");
Location.TestField("Require Shipment", false);
TransferOrderPostShipment.Run(TransferHeader);
end;
procedure ReceiveTransferOrder(TransferOrderNo: Code[20])
var
TransferHeader: Record "Transfer Header";
Location: Record Location;
TransferOrderPostReceipt: Codeunit "TransferOrder-Post Receipt";
begin
TransferHeader.Get(TransferOrderNo);
TransferHeader.TestField("Direct Transfer", false);
TransferHeader.TestField("In-Transit Code");
Location.Get(TransferHeader."Transfer-to Code");
Location.TestField("Require Receive", false);
TransferOrderPostReceipt.Run(TransferHeader);
end;
}

View file

@ -0,0 +1,42 @@
---
bc-version: [all]
domain: scm
keywords: [transfer-header, transfer-line, transferorder-post-shipment, transferorder-post-receipt, in-transit-code, last-shipment-no, item-application-entry]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Post transfers through shipment and receipt codeunits
## Description
A two-step transfer order preserves a continuous quantity, reservation, and cost/application lineage from the source through in-transit to the destination. Its shipment and receipt workflows also own posted documents and derived lines for partial receipt. Posting an item-journal movement and manually marking a transfer header or line as shipped is not equivalent, even if the total quantities balance.
## Best Practice
For a prepared non-direct transfer order without required warehouse documents, use `"TransferOrder-Post Shipment".Run` at shipment and `"TransferOrder-Post Receipt".Run` at receipt, passing the actual `"Transfer Header"`. Validate the intended quantities to ship/receive through the source document; do not assign posted quantity counters as preparation.
When warehouse shipment or receipt is required, use the warehouse document posting workflow that invokes the transfer poster with its real source context. Retain the configured standard direct-transfer workflow for direct transfers; the two-step sample's in-transit guard is not a universal requirement.
Standalone item reclassification journals and bin movements are legitimate separate operations. Do not demand a fixed number of item ledger entries, or a nonzero `"Transferred-from Entry No."` on every transfer application: tracking/application splits and average-cost transfer handling differ. Require evidence that code is replacing completion of an existing transfer order, not merely moving stock through another supported process.
## Anti Pattern
Report ad-hoc item postings, independent positive/negative adjustments, manually created posted-transfer rows, or changes to source shipment/receipt counters used to stand in for transfer-order posting. Updating `"Last Shipment No."` after a bare item-journal call does not create the posted shipment, source-line progress, or transfer application lineage.
Do not repair this by changing an existing item ledger entry's location or inventing application links. Route the source transaction through its owning shipment/receipt or configured direct-transfer workflow. Metadata enrichment inside that workflow is not itself a posting bypass.
## Samples
- [`post-transfers-through-shipment-and-receipt-codeunits.bad.al`](post-transfers-through-shipment-and-receipt-codeunits.bad.al)
- [`post-transfers-through-shipment-and-receipt-codeunits.good.al`](post-transfers-through-shipment-and-receipt-codeunits.good.al)
## References
- [Transfer inventory between locations](https://learn.microsoft.com/en-us/dynamics365/business-central/inventory-how-transfer-between-locations)
- [Item application design](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-item-application)
- [Cost adjustment design](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-cost-adjustment)
- [BaseApp shipment journal/source linkage](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Transfer/TransferOrderPostShipment.Codeunit.al#L292-L336)
- [Partial-receipt derived-line handling](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Transfer/TransferOrderPostReceipt.Codeunit.al#L457-L529)
- [Transfer application and average-cost branches](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Posting/ItemJnlPostLine.Codeunit.al#L1911-L1976)

View file

@ -0,0 +1,44 @@
namespace BCQuality.SCM.Samples;
using Microsoft.Inventory.Item;
using Microsoft.Inventory.Journal;
using Microsoft.Inventory.Location;
using Microsoft.Warehouse.Journal;
codeunit 50110 "SCM Warehouse Adjustment Bad"
{
procedure ReconcileRegisteredWarehouseAdjustment(ItemNo: Code[20]; LocationCode: Code[10]; TemplateName: Code[10]; BatchName: Code[10]; PostingDate: Date; DocumentNo: Code[20]): Boolean
var
Item: Record Item;
ItemJournalBatch: Record "Item Journal Batch";
ItemJournalLine: Record "Item Journal Line";
Location: Record Location;
CalculateWhseAdjustment: Report "Calculate Whse. Adjustment";
begin
Location.Get(LocationCode);
Location.TestField("Directed Put-away and Pick", true);
Location.TestField("Adjustment Bin Code");
ItemJournalBatch.Get(TemplateName, BatchName);
ItemJournalLine.SetRange("Journal Template Name", TemplateName);
ItemJournalLine.SetRange("Journal Batch Name", BatchName);
if not ItemJournalLine.IsEmpty() then
Error(EmptyBatchErr);
Item.Get(ItemNo);
Item.SetRecFilter();
Item.SetRange("Location Filter", LocationCode);
ItemJournalLine."Journal Template Name" := TemplateName;
ItemJournalLine."Journal Batch Name" := BatchName;
CalculateWhseAdjustment.SetItemJnlLine(ItemJournalLine);
CalculateWhseAdjustment.SetTableView(Item);
CalculateWhseAdjustment.InitializeRequest(PostingDate, DocumentNo);
CalculateWhseAdjustment.SetHideValidationDialog(true);
CalculateWhseAdjustment.UseRequestPage(false);
CalculateWhseAdjustment.RunModal();
exit(true);
end;
var
EmptyBatchErr: Label 'Use an empty, dedicated item journal batch for warehouse reconciliation.';
}

View file

@ -0,0 +1,50 @@
namespace BCQuality.SCM.Samples;
using Microsoft.Inventory.Item;
using Microsoft.Inventory.Journal;
using Microsoft.Inventory.Location;
using Microsoft.Inventory.Posting;
using Microsoft.Warehouse.Journal;
codeunit 50111 "SCM Warehouse Adjustment Good"
{
procedure ReconcileRegisteredWarehouseAdjustment(ItemNo: Code[20]; LocationCode: Code[10]; TemplateName: Code[10]; BatchName: Code[10]; PostingDate: Date; DocumentNo: Code[20]): Boolean
var
Item: Record Item;
ItemJournalBatch: Record "Item Journal Batch";
ItemJournalLine: Record "Item Journal Line";
Location: Record Location;
CalculateWhseAdjustment: Report "Calculate Whse. Adjustment";
ItemJnlPostBatch: Codeunit "Item Jnl.-Post Batch";
begin
Location.Get(LocationCode);
Location.TestField("Directed Put-away and Pick", true);
Location.TestField("Adjustment Bin Code");
ItemJournalBatch.Get(TemplateName, BatchName);
ItemJournalLine.SetRange("Journal Template Name", TemplateName);
ItemJournalLine.SetRange("Journal Batch Name", BatchName);
if not ItemJournalLine.IsEmpty() then
Error(EmptyBatchErr);
Item.Get(ItemNo);
Item.SetRecFilter();
Item.SetRange("Location Filter", LocationCode);
ItemJournalLine."Journal Template Name" := TemplateName;
ItemJournalLine."Journal Batch Name" := BatchName;
CalculateWhseAdjustment.SetItemJnlLine(ItemJournalLine);
CalculateWhseAdjustment.SetTableView(Item);
CalculateWhseAdjustment.InitializeRequest(PostingDate, DocumentNo);
CalculateWhseAdjustment.SetHideValidationDialog(true);
CalculateWhseAdjustment.UseRequestPage(false);
CalculateWhseAdjustment.RunModal();
if ItemJournalLine.FindFirst() then begin
ItemJournalLine.TestField("Warehouse Adjustment", true);
ItemJnlPostBatch.Run(ItemJournalLine);
end;
exit(true);
end;
var
EmptyBatchErr: Label 'Use an empty, dedicated item journal batch for warehouse reconciliation.';
}

View file

@ -0,0 +1,41 @@
---
bc-version: [all]
domain: scm
keywords: [warehouse-adjustment, calculate-whse-adjustment, adjustment-bin-code, directed-put-away-and-pick, item-journal-line, warehouse-entry]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Reconcile warehouse adjustments with the item ledger
## Description
At a Directed Put-away and Pick location, registering an ordinary warehouse quantity or physical-inventory adjustment and synchronizing it to inventory are distinct steps. The warehouse registration balances quantity through the adjustment bin. An additional ordinary item-journal increase/decrease is not the same as consuming that pending warehouse adjustment.
## Best Practice
After the warehouse adjustment has been registered, run `"Calculate Whse. Adjustment"` for the intended item/location and prepared item-journal batch, then post the generated lines through `"Item Jnl.-Post Batch"`. The calculation derives the reconciliation by location, variant, units of measure, and tracking, marks the lines `"Warehouse Adjustment"`, and accounts for already prepared unposted adjustments.
Keep reconciliation separate from source-document posting: warehouse receipts/shipments use their document workflows. Intentional warehouse-only staging is valid when a separately owned reconciliation step completes the process; do not flag the registration call just because that later job is outside the diff.
Do not generalize this rule to every warehouse operation. Bin movements need not change total inventory, and warehouse tracking/expiration reclassification has a standard batch path that can also post item-journal entries. Standard reclassification and basic-location item adjustments are not this ordinary advanced-warehouse quantity-adjustment case.
The samples start after warehouse quantity registration and report whether inventory reconciliation completed. The clean sample calculates and posts into an empty dedicated batch; it is not a complete warehouse physical-count workflow.
## Anti Pattern
Report a workflow that claims to reconcile a registered advanced-warehouse quantity adjustment by posting a manually mirrored ordinary item-journal line, or that marks reconciliation complete after only warehouse registration or adjustment calculation. Calculation prepares journal lines; it does not post those lines. Require explicit synchronization intent and location/workflow evidence.
Do not repair the defect by inventing positive/negative quantities or flipping `"Warehouse Adjustment"` on an arbitrary line. Use the calculation step so the adjustment-bin balance and the actual tracked quantities drive inventory reconciliation.
## Samples
- [`reconcile-warehouse-adjustments-with-the-item-ledger.bad.al`](reconcile-warehouse-adjustments-with-the-item-ledger.bad.al)
- [`reconcile-warehouse-adjustments-with-the-item-ledger.good.al`](reconcile-warehouse-adjustments-with-the-item-ledger.good.al)
## References
- [Synchronize adjusted warehouse entries with item ledger entries](https://learn.microsoft.com/en-us/dynamics365/business-central/inventory-how-count-adjust-reclassify#to-synchronize-the-adjusted-warehouse-entries-with-the-related-item-ledger-entries)
- [BaseApp warehouse-adjustment calculation](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Warehouse/Journal/CalculateWhseAdjustment.Report.al#L298-L384)
- [Warehouse reclassification exception](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Warehouse/Journal/WhseJnlRegisterBatch.Codeunit.al#L196-L210)

View file

@ -0,0 +1,35 @@
namespace BCQuality.SCM.Samples;
using Microsoft.Inventory.Tracking;
using Microsoft.Sales.Document;
codeunit 50108 "SCM Tracking Transfer Bad"
{
procedure TransferBlanketOrderTracking(var SourceBlanketOrderLine: Record "Sales Line"; var DestinationSalesOrderLine: Record "Sales Line"; QuantityBaseToTransfer: Decimal)
var
ReservationEntry: Record "Reservation Entry";
begin
SourceBlanketOrderLine.TestField("Document Type", SourceBlanketOrderLine."Document Type"::"Blanket Order");
SourceBlanketOrderLine.TestField(Type, SourceBlanketOrderLine.Type::Item);
DestinationSalesOrderLine.TestField("Document Type", DestinationSalesOrderLine."Document Type"::Order);
DestinationSalesOrderLine.TestField(Type, DestinationSalesOrderLine.Type::Item);
DestinationSalesOrderLine.TestField("No.", SourceBlanketOrderLine."No.");
if QuantityBaseToTransfer <= 0 then
Error(PositiveQuantityErr);
ReservationEntry.SetRange("Source Type", Database::"Sales Line");
ReservationEntry.SetRange("Source Subtype", SourceBlanketOrderLine."Document Type".AsInteger());
ReservationEntry.SetRange("Source ID", SourceBlanketOrderLine."Document No.");
ReservationEntry.SetRange("Source Ref. No.", SourceBlanketOrderLine."Line No.");
ReservationEntry.SetRange(Positive, false);
ReservationEntry.FindFirst();
ReservationEntry."Source Subtype" := DestinationSalesOrderLine."Document Type".AsInteger();
ReservationEntry."Source ID" := DestinationSalesOrderLine."Document No.";
ReservationEntry."Source Ref. No." := DestinationSalesOrderLine."Line No.";
ReservationEntry.Validate("Quantity (Base)", -QuantityBaseToTransfer);
ReservationEntry.Modify(true);
end;
var
PositiveQuantityErr: Label 'The base quantity to transfer must be positive.';
}

View file

@ -0,0 +1,24 @@
namespace BCQuality.SCM.Samples;
using Microsoft.Sales.Document;
codeunit 50109 "SCM Tracking Transfer Good"
{
procedure TransferBlanketOrderTracking(var SourceBlanketOrderLine: Record "Sales Line"; var DestinationSalesOrderLine: Record "Sales Line"; QuantityBaseToTransfer: Decimal)
var
SalesLineReserve: Codeunit "Sales Line-Reserve";
begin
SourceBlanketOrderLine.TestField("Document Type", SourceBlanketOrderLine."Document Type"::"Blanket Order");
SourceBlanketOrderLine.TestField(Type, SourceBlanketOrderLine.Type::Item);
DestinationSalesOrderLine.TestField("Document Type", DestinationSalesOrderLine."Document Type"::Order);
DestinationSalesOrderLine.TestField(Type, DestinationSalesOrderLine.Type::Item);
DestinationSalesOrderLine.TestField("No.", SourceBlanketOrderLine."No.");
if QuantityBaseToTransfer <= 0 then
Error(PositiveQuantityErr);
SalesLineReserve.TransferSaleLineToSalesLine(SourceBlanketOrderLine, DestinationSalesOrderLine, QuantityBaseToTransfer);
end;
var
PositiveQuantityErr: Label 'The base quantity to transfer must be positive.';
}

View file

@ -0,0 +1,42 @@
---
bc-version: [all]
domain: scm
keywords: [reservation-entry, tracking-specification, sales-line-reserve, transfersalelinetosalesline, transferreserventry, copyitemtracking, quantity-base]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Transfer item tracking through source reservation codeunits
## Description
Moving lot/serial tracking between document lines is a source-ownership operation, not a copy of visible tracking fields. Partial movement must retain the old source's remainder, destination units of measure, quantities to handle/invoice, status, sign, and any reservation counterpart. Repointing a `"Reservation Entry"` loses this coordination; inserting a `"Tracking Specification"` row alone does not book the destination's source tracking.
## Best Practice
Use the reservation codeunit for the source workflow. For the tracking portion of blanket-sales-order or quote conversion to a sales order, `"Sales Line-Reserve".TransferSaleLineToSalesLine` takes the existing source line, prepared destination line, and quantity to transfer in **base units**. It delegates the source/status and quantity movement to `"Create Reserv. Entry".TransferReservEntry`.
The caller still owns document conversion and destination-line preparation; this method does not create a sales order. Keep item, variant, location, and source identity consistent, and use other source-specific wrappers for purchases, transfers, assembly, or production instead of reusing a sales wrapper indiscriminately.
`"Item Tracking Management".CopyItemTracking` serves a different purpose: it creates Prospect copies, not a transfer of reservation ownership. That is valid for its intended copy workflow. Temporary Tracking Specification processing is also normal, and persisted historical tracking specifications are not forbidden; distinguish the working/historic representation from the current source booking.
## Anti Pattern
Report direct rewrites of persistent `"Reservation Entry"` source type/subtype, ID, reference number, or quantities to implement source-line conversion or a partial tracking transfer. Changing only `"Quantity (Base)"` and source keys can drop the remainder or leave the other tracking/reservation quantities attached to the wrong source.
Also report use of a tracking copy as a replacement for moving an existing binding reservation when that intent is explicit. Do not flag a legitimate Prospect copy, temporary tracking buffer, historical tracking read, or source-specific engine call merely because it uses these tables.
## Samples
- [`transfer-item-tracking-through-source-reservation-codeunits.bad.al`](transfer-item-tracking-through-source-reservation-codeunits.bad.al)
- [`transfer-item-tracking-through-source-reservation-codeunits.good.al`](transfer-item-tracking-through-source-reservation-codeunits.good.al)
## References
- [Item Tracking Lines window design](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-item-tracking-lines-window)
- [Active versus historic item-tracking entries](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-active-versus-historic-item-tracking-entries)
- [Item tracking and reservations](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-item-tracking-and-reservations)
- [BaseApp sales tracking transfer](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Sales/Document/SalesLineReserve.Codeunit.al#L532-L578)
- [Base-unit conversion caller](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Sales/Document/BlanketSalesOrdertoOrder.Codeunit.al#L195-L198)
- [Prospect-copy API](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Tracking/ItemTrackingManagement.Codeunit.al#L575-L657)

View file

@ -0,0 +1,25 @@
namespace BCQuality.SCM.Samples;
using Microsoft.Inventory.Item;
codeunit 50114 "SCM Additional Promise Bad"
{
procedure CanPromiseAdditionalDemand(ItemNo: Code[20]; LocationCode: Code[10]; VariantCode: Code[10]; ShipmentDate: Date; RequestedAdditionalQuantityBase: Decimal; LookaheadDateFormula: DateFormula): Boolean
var
Item: Record Item;
begin
if (ShipmentDate = 0D) or (RequestedAdditionalQuantityBase <= 0) then
Error(DemandInputErr);
Item.Get(ItemNo);
Item.TestField(Type, Item.Type::Inventory);
Item.SetRange("Location Filter", LocationCode);
Item.SetRange("Variant Filter", VariantCode);
Item.SetRange("Date Filter", 0D, ShipmentDate);
Item.CalcFields(Inventory);
exit(Item.Inventory >= RequestedAdditionalQuantityBase);
end;
var
DemandInputErr: Label 'Enter a shipment date and a positive additional base quantity.';
}

View file

@ -0,0 +1,33 @@
namespace BCQuality.SCM.Samples;
using Microsoft.Foundation.Enums;
using Microsoft.Inventory.Availability;
using Microsoft.Inventory.Item;
codeunit 50115 "SCM Additional Promise Good"
{
procedure CanPromiseAdditionalDemand(ItemNo: Code[20]; LocationCode: Code[10]; VariantCode: Code[10]; ShipmentDate: Date; RequestedAdditionalQuantityBase: Decimal; LookaheadDateFormula: DateFormula): Boolean
var
Item: Record Item;
AvailableToPromise: Codeunit "Available to Promise";
GrossRequirement: Decimal;
ScheduledReceipt: Decimal;
PromisableQuantityBase: Decimal;
begin
if (ShipmentDate = 0D) or (RequestedAdditionalQuantityBase <= 0) then
Error(DemandInputErr);
Item.Get(ItemNo);
Item.TestField(Type, Item.Type::Inventory);
Item.SetRange("Location Filter", LocationCode);
Item.SetRange("Variant Filter", VariantCode);
Item.SetRange("Date Filter", 0D, ShipmentDate);
PromisableQuantityBase := AvailableToPromise.CalcQtyAvailableToPromise(
Item, GrossRequirement, ScheduledReceipt, ShipmentDate,
Enum::"Analysis Period Type"::Day, LookaheadDateFormula);
exit(PromisableQuantityBase >= RequestedAdditionalQuantityBase);
end;
var
DemandInputErr: Label 'Enter a shipment date and a positive additional base quantity.';
}

View file

@ -0,0 +1,40 @@
---
bc-version: [all]
domain: scm
keywords: [available-to-promise, calcqtyavailabletopromise, inventory, shipment-date, gross-requirement, scheduled-receipt, location-filter, variant-filter]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Use date-aware availability for promising
## Description
`Item.Inventory` is an on-hand quantity, not an available-to-promise answer. Promising additional demand must account for the requested date, location and variant, reservations, scheduled receipts, existing requirements, and demand within the configured lookahead. An on-hand comparison can promise inventory already committed elsewhere and miss incoming supply.
## Best Practice
For an additional demand not already recorded on a source line, use `"Available to Promise".CalcQtyAvailableToPromise` with the Item's location/variant filters, date range ending on the shipment date, and the configured period/lookahead horizon. Compare in base units. Use a fresh calculation context or the codeunit's recalculation support rather than carrying cached quantities between unrelated items or requests.
For an existing sales-line change, retain the source-aware order-promising/availability workflow, which accounts for the line's own quantity or delta; blindly applying an additional-demand calculation can double-count that line. Assembly and production requirements/supply likewise need the standard availability context, not just a sales-only stock subtraction.
An ATP result is not a reservation or a guarantee of warehouse pickability. Lot/serial constraints, bins, warehouse activity, and later concurrent changes still need their own checks. Conversely, an on-hand display, valuation report, or deliberately immediate-stock-only check is allowed to use `Item.Inventory`; do not replace its distinct business question with ATP.
## Anti Pattern
Report `CalcFields(Inventory)` or an equivalent sum of item ledger quantities used as the complete decision for a dated additional-demand promise, including code that applies location/variant filters but ignores other demand and supply. Require explicit promising intent; an Inventory FlowField read alone is not a finding.
Also report a visible loss of location, variant, date, or source-line context in that calculation. Do not invent missing demand in an unseen caller or require this exact API when a visible supported workflow already supplies the correct availability semantics.
## Samples
- [`use-date-aware-availability-for-promising.bad.al`](use-date-aware-availability-for-promising.bad.al)
- [`use-date-aware-availability-for-promising.good.al`](use-date-aware-availability-for-promising.good.al)
## References
- [Calculate order-promising dates](https://learn.microsoft.com/en-us/dynamics365/business-central/sales-how-to-calculate-order-promising-dates)
- [Availability in the warehouse](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-availability-in-the-warehouse)
- [BaseApp ATP calculation](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Availability/AvailabletoPromise.Codeunit.al#L52-L184)
- [Forward-demand lookahead](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Availability/AvailabletoPromise.Codeunit.al#L295-L356)