This commit is contained in:
Jesper Schulz-Wedde 2026-08-04 11:30:15 -07:00 committed by GitHub
commit 532c5e15e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 118 additions and 0 deletions

View file

@ -0,0 +1,24 @@
---
bc-version: [all]
domain: finance
keywords: [ledger-entry, immutable, reversal, audit-trail, correction, custentry-edit, non-financial-fields]
technologies: [al]
countries: [w1]
application-area: [finance]
---
# Correct posted ledger entries by reversing, not by editing or deleting them
## Description
A posted ledger entry is part of Business Central's permanent audit trail, and its **financial content** is immutable. The amounts, accounts, posting date, and quantities on `G/L Entry`, `Cust. Ledger Entry`, `Vendor Ledger Entry`, and similar tables must not change after posting, because registers, applications, VAT statements, and statutory reports all assume that content is append-only. Correcting financial content is therefore itself a posting: BC provides reversal (the `Reverse` / `Reverse Register` routines) and correcting documents (credit memos, correcting journals) that post a new, offsetting entry and leave the original intact. This immutability rule is about financial content — it is not a blanket ban on touching the entry (see Best Practice).
## Best Practice
To undo or correct a posting's **financial** content, post a reversing or correcting entry through the normal posting path so the offset is itself a balanced, dated, traceable transaction. The original entry stays in place and the two net to zero, preserving the audit trail.
Non-financial **operational** fields are a deliberate exception and are meant to be edited after posting. BC supports updating a defined set of post-posting fields — payment and application data such as due date, payment-discount dates, on-hold status, applies-to ID, and recipient/communication fields — and the Customer and Vendor Ledger Entries pages expose several of them as editable. Make those changes through the dedicated `CustEntry-Edit` / `VendEntry-Edit` routines (which those pages call), not a raw `Modify`, so the edit stays within the supported set and leaves the entry's financial content and audit trail intact.
## Anti Pattern
Calling `Modify` or `Delete` on a posted ledger entry to fix a mistake — changing an amount, repointing an account, or removing the row. Detection signal: `Modify`, `ModifyAll`, `Delete`, or `DeleteAll` on a `*Ledger Entry` or `G/L Entry` record outside a dedicated `*Entry-Edit` routine. This destroys the audit trail, desynchronizes the entry from its register and detailed entries, and corrupts any report or reconciliation that already consumed the original value.

View file

@ -0,0 +1,24 @@
// Demonstration only. Self-contained illustration, not derived from the
// Business Central base application source.
//
// BAD: insert straight into the ledger table and hand-compute Entry No.
// The row is unbalanced (no balancing entry), has no register, no resolved
// dimensions, and no VAT. FindLast + "+ 1" races under concurrency and will
// collide on the primary key. Reconciliation treats the result as corrupt.
codeunit 50100 "Post GL Adjustment"
{
procedure PostAdjustment(AccountNo: Code[20]; Amount: Decimal; PostingDate: Date)
var
GLEntry: Record "G/L Entry";
LastGLEntry: Record "G/L Entry";
begin
if LastGLEntry.FindLast() then;
GLEntry.Init();
GLEntry."Entry No." := LastGLEntry."Entry No." + 1;
GLEntry."G/L Account No." := AccountNo;
GLEntry.Amount := Amount;
GLEntry."Posting Date" := PostingDate;
GLEntry.Insert();
end;
}

View file

@ -0,0 +1,26 @@
// Demonstration only. Self-contained illustration, not derived from the
// Business Central base application source.
//
// GOOD: build a journal line and let the posting engine create the ledger
// entry. The platform assigns Entry No., writes the balancing entry, creates
// the register, and resolves dimensions and VAT.
codeunit 50100 "Post GL Adjustment"
{
procedure PostAdjustment(AccountNo: Code[20]; BalAccountNo: Code[20]; Amount: Decimal; PostingDate: Date)
var
GenJnlLine: Record "Gen. Journal Line";
GenJnlPostLine: Codeunit "Gen. Jnl.-Post Line";
begin
GenJnlLine.Init();
GenJnlLine."Posting Date" := PostingDate;
GenJnlLine."Document Type" := GenJnlLine."Document Type"::" ";
GenJnlLine."Account Type" := GenJnlLine."Account Type"::"G/L Account";
GenJnlLine.Validate("Account No.", AccountNo);
GenJnlLine."Bal. Account Type" := GenJnlLine."Bal. Account Type"::"G/L Account";
GenJnlLine.Validate("Bal. Account No.", BalAccountNo);
GenJnlLine.Validate(Amount, Amount);
GenJnlLine."Source Code" := 'ADJUST';
GenJnlPostLine.Run(GenJnlLine);
end;
}

View file

@ -0,0 +1,22 @@
---
bc-version: [all]
domain: finance
keywords: [posting, g-l-entry, ledger-entry, gen-jnl-post-line, journal-line, balancing, entry-no]
technologies: [al]
countries: [w1]
application-area: [finance]
---
# Post ledger entries through the posting engine, never by inserting them directly
## Description
Ledger entries — `G/L Entry`, `Cust. Ledger Entry`, `Vendor Ledger Entry`, `Item Ledger Entry`, and their detailed counterparts — are the output of Business Central's posting engine, not ordinary tables an extension writes to. The supported way to create them is to populate a journal line (for example `Gen. Journal Line`) and run the matching posting codeunit (`Gen. Jnl.-Post Line`, codeunit 12, for general-ledger postings). The posting routine enforces double-entry balancing, allocates `Entry No.` safely under concurrency, creates the register, resolves dimensions, applies VAT, and links source and application data. None of that happens when a row is inserted into the ledger table directly.
## Best Practice
Build the transaction as one or more journal lines and hand them to the posting codeunit. Let the engine assign `Entry No.`, create the `G/L Register`, and write the balancing entries. When you need a reusable entry point, wrap the journal-line setup in your own codeunit but still post through `Gen. Jnl.-Post Line` — or through the document-posting routines (sales, purchase, service) that ultimately call it. See sample: `post-ledger-entries-through-posting-codeunits.good.al`.
## Anti Pattern
Calling `Insert` on a ledger table — typically after a `FindLast` to guess the next `Entry No.` Detection signal: any `Insert` on a `*Ledger Entry` or `G/L Entry` record, or an `Entry No.` computed in AL rather than returned by the platform. Such code produces an unbalanced, registerless, dimensionless row that reconciliation and reporting will treat as corrupt, and the hand-computed `Entry No.` races under concurrency. See sample: `post-ledger-entries-through-posting-codeunits.bad.al`.

View file

@ -0,0 +1,22 @@
---
bc-version: [all]
domain: finance
keywords: [dimensions, dimension-set-id, dimension-set-entry, global-dimension, shortcut-dimension, dimensionmanagement]
technologies: [al]
countries: [w1]
application-area: [finance]
---
# Treat the Dimension Set ID as the source of truth for dimensions
## Description
Modern Business Central stores the dimensions of a record as a single `Dimension Set ID` that points at an immutable combination of `Dimension Set Entry` rows. The `Global Dimension 1 Code` / `Global Dimension 2 Code` fields and the `Shortcut Dimension 3..8` fields are denormalized projections the platform keeps in sync — they are not the source of truth, and they cover only a handful of the up to eight dimensions a set can hold. New or merged dimension combinations are obtained from codeunit `DimensionManagement` (for example `GetDimensionSetID`), which returns the `Dimension Set ID` to store on the record.
## Best Practice
When code sets, copies, or merges dimensions, work in terms of `Dimension Set ID` values and resolve or create them through `DimensionManagement`; assign the resulting set ID to the record and let the platform derive the global and shortcut projections. On records that expose shortcut-dimension fields (journal and document lines), call `Validate` on those fields — the field's logic updates the `Dimension Set ID` for you. To combine dimensions from several sources (document plus customer, header plus line) use the dimension-set combination routines instead of copying individual codes.
## Anti Pattern
Direct assignment (`:=`) to `Global Dimension 1 Code` or `Global Dimension 2 Code`, or reading those fields to determine "the dimensions," as if they were the record's dimension state. Detection signal: an assignment of a global/shortcut dimension field with no corresponding `Dimension Set ID` update, or analysis logic that branches on the global-dimension fields rather than the set entries. The projection silently disagrees with the set once a third dimension is involved, so posting and analysis-by-dimension produce wrong results.