From 3cb00e4d2dd19ae86922607eb52e4b630d839ef0 Mon Sep 17 00:00:00 2001 From: Jesper Schulz-Wedde Date: Fri, 26 Jun 2026 10:40:50 +0200 Subject: [PATCH 1/4] Add Finance posting domain-knowledge pilot Adds three atomic domain-rule knowledge files under community/knowledge/finance/ as a pilot for Type-A (normative) Business Central domain knowledge: post through the posting engine, treat posted ledger entries as immutable, and treat the Dimension Set ID as the source of truth for dimensions. Includes a good/bad AL sample pair for the posting rule. These encode BC-specific invariants that LLMs reliably get wrong, fitting the existing remedial/atomic knowledge grain with no schema or contract changes. Passes the repo frontmatter validator and is discovered by the knowledge index. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...-modify-or-delete-posted-ledger-entries.md | 22 ++++++++++++++++ ...r-entries-through-posting-codeunits.bad.al | 24 +++++++++++++++++ ...-entries-through-posting-codeunits.good.al | 26 +++++++++++++++++++ ...edger-entries-through-posting-codeunits.md | 22 ++++++++++++++++ ...ite-dimensions-as-dimension-set-entries.md | 22 ++++++++++++++++ 5 files changed, 116 insertions(+) create mode 100644 community/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md create mode 100644 community/knowledge/finance/post-ledger-entries-through-posting-codeunits.bad.al create mode 100644 community/knowledge/finance/post-ledger-entries-through-posting-codeunits.good.al create mode 100644 community/knowledge/finance/post-ledger-entries-through-posting-codeunits.md create mode 100644 community/knowledge/finance/write-dimensions-as-dimension-set-entries.md diff --git a/community/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md b/community/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md new file mode 100644 index 0000000..8db10c8 --- /dev/null +++ b/community/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md @@ -0,0 +1,22 @@ +--- +bc-version: [all] +domain: finance +keywords: [ledger-entry, immutable, reversal, audit-trail, correction, reverse, custentry-edit] +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 an immutable record in Business Central's audit trail. The financial content of `G/L Entry`, `Cust. Ledger Entry`, `Vendor Ledger Entry`, and similar tables — amounts, accounts, posting date, quantities — must not change after posting, because registers, applications, VAT statements, and statutory reports all assume entries are append-only. Corrections are themselves postings: 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. + +## Best Practice + +To undo or correct a posting, 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. A narrow set of non-financial fields (for example an entry's `Open` / `On Hold` status or a due date) is editable through dedicated platform routines such as `CustEntry-Edit` and `VendEntry-Edit`; use those routines rather than a raw `Modify`. + +## 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. diff --git a/community/knowledge/finance/post-ledger-entries-through-posting-codeunits.bad.al b/community/knowledge/finance/post-ledger-entries-through-posting-codeunits.bad.al new file mode 100644 index 0000000..3d6fe51 --- /dev/null +++ b/community/knowledge/finance/post-ledger-entries-through-posting-codeunits.bad.al @@ -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; +} diff --git a/community/knowledge/finance/post-ledger-entries-through-posting-codeunits.good.al b/community/knowledge/finance/post-ledger-entries-through-posting-codeunits.good.al new file mode 100644 index 0000000..761b5a7 --- /dev/null +++ b/community/knowledge/finance/post-ledger-entries-through-posting-codeunits.good.al @@ -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; +} diff --git a/community/knowledge/finance/post-ledger-entries-through-posting-codeunits.md b/community/knowledge/finance/post-ledger-entries-through-posting-codeunits.md new file mode 100644 index 0000000..a78186a --- /dev/null +++ b/community/knowledge/finance/post-ledger-entries-through-posting-codeunits.md @@ -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`. diff --git a/community/knowledge/finance/write-dimensions-as-dimension-set-entries.md b/community/knowledge/finance/write-dimensions-as-dimension-set-entries.md new file mode 100644 index 0000000..a13d91c --- /dev/null +++ b/community/knowledge/finance/write-dimensions-as-dimension-set-entries.md @@ -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. From 5e779c702f183547612cc042e6e6b893c03eabe6 Mon Sep 17 00:00:00 2001 From: Jesper Schulz-Wedde Date: Mon, 29 Jun 2026 15:04:36 +0200 Subject: [PATCH 2/4] Clarify editable operational fields on posted ledger entries Addresses review feedback from @JeremyVyska on PR #57: the immutability rule applies to financial content, not the whole entry. Reframes the Description around financial content and gives the operational-field exception (payment/application data, on-hold, applies-to, communication fields edited via CustEntry-Edit/VendEntry-Edit and the ledger entry pages) its own paragraph in Best Practice instead of understating it as a narrow set. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../do-not-modify-or-delete-posted-ledger-entries.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/community/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md b/community/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md index 8db10c8..0f8eb7f 100644 --- a/community/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md +++ b/community/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md @@ -1,7 +1,7 @@ --- bc-version: [all] domain: finance -keywords: [ledger-entry, immutable, reversal, audit-trail, correction, reverse, custentry-edit] +keywords: [ledger-entry, immutable, reversal, audit-trail, correction, custentry-edit, non-financial-fields] technologies: [al] countries: [w1] application-area: [finance] @@ -11,11 +11,13 @@ application-area: [finance] ## Description -A posted ledger entry is an immutable record in Business Central's audit trail. The financial content of `G/L Entry`, `Cust. Ledger Entry`, `Vendor Ledger Entry`, and similar tables — amounts, accounts, posting date, quantities — must not change after posting, because registers, applications, VAT statements, and statutory reports all assume entries are append-only. Corrections are themselves postings: 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. +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, 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. A narrow set of non-financial fields (for example an entry's `Open` / `On Hold` status or a due date) is editable through dedicated platform routines such as `CustEntry-Edit` and `VendEntry-Edit`; use those routines rather than a raw `Modify`. +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 From fa63d5526c2912a949534faf662536f595be2981 Mon Sep 17 00:00:00 2001 From: Jesper Schulz-Wedde Date: Thu, 17 Sep 2026 16:09:17 +0200 Subject: [PATCH 3/4] Expand Finance pilot into a source-verified review domain Move Finance knowledge to the Microsoft-owned layer, add nine scoped rules with eighteen AL samples, and register bounded Finance review with complete paired evaluation coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/scripts/Test-SkillIndex.ps1 | 7 +- ...-modify-or-delete-posted-ledger-entries.md | 24 ---- ...r-entries-through-posting-codeunits.bad.al | 24 ---- ...-entries-through-posting-codeunits.good.al | 26 ---- ...edger-entries-through-posting-codeunits.md | 22 --- ...ite-dimensions-as-dimension-set-entries.md | 22 --- docs/using-bcquality.md | 14 +- evaluation/README.md | 10 ++ evaluation/review-fixtures.json | 14 ++ ...tries-through-application-codeunits.bad.al | 33 +++++ ...ries-through-application-codeunits.good.al | 31 +++++ ...r-entries-through-application-codeunits.md | 37 +++++ ...ledger-due-dates-through-entry-edit.bad.al | 21 +++ ...edger-due-dates-through-entry-edit.good.al | 21 +++ ...nge-ledger-due-dates-through-entry-edit.md | 35 +++++ .../do-not-edit-shared-dimension-sets.bad.al | 14 ++ .../do-not-edit-shared-dimension-sets.good.al | 18 +++ .../do-not-edit-shared-dimension-sets.md | 35 +++++ ...ify-or-delete-posted-ledger-entries.bad.al | 32 +++++ ...fy-or-delete-posted-ledger-entries.good.al | 32 +++++ ...-modify-or-delete-posted-ledger-entries.md | 35 +++++ ...mal-vat-journal-amount-includes-vat.bad.al | 56 ++++++++ ...al-vat-journal-amount-includes-vat.good.al | 56 ++++++++ .../normal-vat-journal-amount-includes-vat.md | 35 +++++ ...r-entries-through-posting-codeunits.bad.al | 60 ++++++++ ...-entries-through-posting-codeunits.good.al | 29 ++++ ...edger-entries-through-posting-codeunits.md | 36 +++++ ...erve-journal-batch-document-balance.bad.al | 51 +++++++ ...rve-journal-batch-document-balance.good.al | 49 +++++++ ...preserve-journal-batch-document-balance.md | 35 +++++ ...-transactions-by-transaction-number.bad.al | 18 +++ ...transactions-by-transaction-number.good.al | 18 +++ ...erse-transactions-by-transaction-number.md | 34 +++++ ...dimensions-as-dimension-set-entries.bad.al | 20 +++ ...imensions-as-dimension-set-entries.good.al | 19 +++ ...ite-dimensions-as-dimension-set-entries.md | 34 +++++ microsoft/skills/review/al-code-review.md | 1 + microsoft/skills/review/al-finance-review.md | 130 ++++++++++++++++++ 38 files changed, 1064 insertions(+), 124 deletions(-) delete mode 100644 community/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md delete mode 100644 community/knowledge/finance/post-ledger-entries-through-posting-codeunits.bad.al delete mode 100644 community/knowledge/finance/post-ledger-entries-through-posting-codeunits.good.al delete mode 100644 community/knowledge/finance/post-ledger-entries-through-posting-codeunits.md delete mode 100644 community/knowledge/finance/write-dimensions-as-dimension-set-entries.md create mode 100644 microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.bad.al create mode 100644 microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.good.al create mode 100644 microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.md create mode 100644 microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.bad.al create mode 100644 microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.good.al create mode 100644 microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.md create mode 100644 microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.bad.al create mode 100644 microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.good.al create mode 100644 microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.md create mode 100644 microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.bad.al create mode 100644 microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.good.al create mode 100644 microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md create mode 100644 microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.bad.al create mode 100644 microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.good.al create mode 100644 microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.md create mode 100644 microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.bad.al create mode 100644 microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.good.al create mode 100644 microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.md create mode 100644 microsoft/knowledge/finance/preserve-journal-batch-document-balance.bad.al create mode 100644 microsoft/knowledge/finance/preserve-journal-batch-document-balance.good.al create mode 100644 microsoft/knowledge/finance/preserve-journal-batch-document-balance.md create mode 100644 microsoft/knowledge/finance/reverse-transactions-by-transaction-number.bad.al create mode 100644 microsoft/knowledge/finance/reverse-transactions-by-transaction-number.good.al create mode 100644 microsoft/knowledge/finance/reverse-transactions-by-transaction-number.md create mode 100644 microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.bad.al create mode 100644 microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.good.al create mode 100644 microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.md create mode 100644 microsoft/skills/review/al-finance-review.md diff --git a/.github/scripts/Test-SkillIndex.ps1 b/.github/scripts/Test-SkillIndex.ps1 index ed07204..424532c 100644 --- a/.github/scripts/Test-SkillIndex.ps1 +++ b/.github/scripts/Test-SkillIndex.ps1 @@ -93,14 +93,15 @@ try { 'microsoft/skills/review/al-query-review.md', 'microsoft/skills/review/al-reporting-review.md', 'microsoft/skills/review/al-appsource-review.md', - 'microsoft/skills/review/al-telemetry-review.md' + 'microsoft/skills/review/al-telemetry-review.md', + 'microsoft/skills/review/al-finance-review.md' ) $review = @($skills | Where-Object id -eq 'al-code-review') if ($review.Count -ne 1) { throw "Expected exactly one al-code-review record, found $($review.Count)." } if ((@($review[0].subSkills) -join "`n") -cne ($expectedLeaves -join "`n")) { - throw 'al-code-review subSkills did not preserve the declared 17-leaf order.' + throw 'al-code-review subSkills did not preserve the declared 18-leaf order.' } foreach ($leafPath in $expectedLeaves) { $leaf = @($skills | Where-Object path -ceq $leafPath) @@ -238,4 +239,4 @@ finally { Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue } -Write-Output 'Skill-index check PASSED: deterministic, schema-valid, and all 17 review leaves preserved in order.' +Write-Output 'Skill-index check PASSED: deterministic, schema-valid, and all 18 review leaves preserved in order.' diff --git a/community/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md b/community/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md deleted file mode 100644 index 0f8eb7f..0000000 --- a/community/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -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. diff --git a/community/knowledge/finance/post-ledger-entries-through-posting-codeunits.bad.al b/community/knowledge/finance/post-ledger-entries-through-posting-codeunits.bad.al deleted file mode 100644 index 3d6fe51..0000000 --- a/community/knowledge/finance/post-ledger-entries-through-posting-codeunits.bad.al +++ /dev/null @@ -1,24 +0,0 @@ -// 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; -} diff --git a/community/knowledge/finance/post-ledger-entries-through-posting-codeunits.good.al b/community/knowledge/finance/post-ledger-entries-through-posting-codeunits.good.al deleted file mode 100644 index 761b5a7..0000000 --- a/community/knowledge/finance/post-ledger-entries-through-posting-codeunits.good.al +++ /dev/null @@ -1,26 +0,0 @@ -// 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; -} diff --git a/community/knowledge/finance/post-ledger-entries-through-posting-codeunits.md b/community/knowledge/finance/post-ledger-entries-through-posting-codeunits.md deleted file mode 100644 index a78186a..0000000 --- a/community/knowledge/finance/post-ledger-entries-through-posting-codeunits.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -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`. diff --git a/community/knowledge/finance/write-dimensions-as-dimension-set-entries.md b/community/knowledge/finance/write-dimensions-as-dimension-set-entries.md deleted file mode 100644 index a13d91c..0000000 --- a/community/knowledge/finance/write-dimensions-as-dimension-set-entries.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -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. diff --git a/docs/using-bcquality.md b/docs/using-bcquality.md index e55751f..3186fe7 100644 --- a/docs/using-bcquality.md +++ b/docs/using-bcquality.md @@ -183,7 +183,7 @@ using your normal compilation, analyzer, test, and human-review workflow. ## Coverage and limits -The Microsoft broad review composes the 17 Microsoft domains listed below. +The Microsoft broad review composes the 18 Microsoft domains listed below. The Community Agents review is a separate skill selected by the request, not a nested part of that coordinator. All current review leaves accept app folders, files, and diffs; request an Agent SDK review explicitly when that @@ -193,8 +193,15 @@ Available knowledge is **not** a promise that every rule will run. Selection depends on the task, target context, enabled layers, and source evidence. A whole-folder review is a current-state snapshot: detecting a published API removal or another comparison-only regression requires an actual baseline. -The corpus is technical AL guidance, not exhaustive functional validation or -AppSource certification. +The corpus includes technical AL guidance and targeted Finance posting +invariants, not exhaustive functional validation or AppSource certification. + +The Finance leaf reviews journal posting, financial ledger changes, +applications, and posting-linked dimension handling. It prunes unrelated code +at the leaf rather than changing broad-review orchestration. Its dimension +guidance can apply across application areas, but it does not own generic +custom-table or master Default Dimension wiring. Request a focused "Finance +posting review" when only this domain is needed. BCQuality intentionally does not duplicate mechanical diagnostics already enforced by the AL compiler or standard analyzers. Run the consuming app's @@ -215,6 +222,7 @@ Each article describes one concern. Where samples exist, use its linked | Data modeling | [Data modeling](../microsoft/knowledge/data-modeling/) | | Error handling | [Error handling](../microsoft/knowledge/error-handling/) | | Events | [Events](../microsoft/knowledge/events/) | +| Finance | [Finance](../microsoft/knowledge/finance/) | | Interfaces | [Interfaces](../microsoft/knowledge/interfaces/) | | Performance | [Performance](../microsoft/knowledge/performance/) | | Privacy | [Privacy](../microsoft/knowledge/privacy/) | diff --git a/evaluation/README.md b/evaluation/README.md index 2125ce3..f8c1877 100644 --- a/evaluation/README.md +++ b/evaluation/README.md @@ -6,6 +6,16 @@ The evaluation is convention-driven. The harness discovers every `/skills Model-facing preparation hashes case IDs, neutralizes `Good`/`Bad` object-name tokens, and removes full-line sample comments so neither the article slug, domain, nor expected outcome reveals the answer. +The Finance override deliberately covers every paired Finance article, not +only the first filename. Its shared context supplies the target version and +application area, not expected answers. Scenario prerequisites live in +executable AL: the document-balance cases check the template setting, and the +VAT cases encode the imported net/VAT/gross totals and applicable VAT mode. +Do not move these prerequisites into comments that preparation removes. +Clean samples also retain supported operational edits, temporary ledger/set +buffers, legitimate entry-number APIs, and reads of individual shortcut +dimensions so these exceptions are exercised rather than blanket-excluded. + ## Validate the corpus ```powershell diff --git a/evaluation/review-fixtures.json b/evaluation/review-fixtures.json index 90c42c6..93c0054 100644 --- a/evaluation/review-fixtures.json +++ b/evaluation/review-fixtures.json @@ -13,6 +13,20 @@ "events": { "article": "reset-ishandled-only-when-the-value-can-carry-over" }, + "finance": { + "articles": [ + "apply-ledger-entries-through-application-codeunits", + "change-ledger-due-dates-through-entry-edit", + "do-not-edit-shared-dimension-sets", + "do-not-modify-or-delete-posted-ledger-entries", + "normal-vat-journal-amount-includes-vat", + "post-ledger-entries-through-posting-codeunits", + "preserve-journal-batch-document-balance", + "reverse-transactions-by-transaction-number", + "write-dimensions-as-dimension-set-entries" + ], + "context": "Target Business Central 28, worldwide standard application (w1), application-area finance. Review each file as an independent source input." + }, "interfaces": { "article": "set-defaultimplementation-on-enum" }, diff --git a/microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.bad.al b/microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.bad.al new file mode 100644 index 0000000..1e605d3 --- /dev/null +++ b/microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.bad.al @@ -0,0 +1,33 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50104 "Customer Settlement Actions" +{ + procedure RequestApplication(CustomerEntryNo: Integer) + var + CustomerEntry: Record "Cust. Ledger Entry"; + begin + RequireInteractiveSession(); + CustomerEntry.Get(CustomerEntryNo); + CustomerEntry.TestField(Open, true); + CustomerEntry.Open := false; + CustomerEntry.Modify(true); + end; + + procedure RequestUnapplication(CustomerEntryNo: Integer) + var + DetailedCustomerEntry: Record "Detailed Cust. Ledg. Entry"; + begin + RequireInteractiveSession(); + DetailedCustomerEntry.SetRange("Cust. Ledger Entry No.", CustomerEntryNo); + DetailedCustomerEntry.SetRange("Entry Type", DetailedCustomerEntry."Entry Type"::Application); + DetailedCustomerEntry.ModifyAll(Unapplied, true); + end; + + local procedure RequireInteractiveSession() + begin + if not GuiAllowed() then + Error(InteractiveSessionErr); + end; + + var + InteractiveSessionErr: Label 'Request settlement from an interactive session.'; +} diff --git a/microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.good.al b/microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.good.al new file mode 100644 index 0000000..b33e2d2 --- /dev/null +++ b/microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.good.al @@ -0,0 +1,31 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50104 "Customer Settlement Actions" +{ + procedure RequestApplication(CustomerEntryNo: Integer) + var + CustomerEntry: Record "Cust. Ledger Entry"; + CustomerApplication: Codeunit "CustEntry-Apply Posted Entries"; + begin + RequireInteractiveSession(); + CustomerEntry.Get(CustomerEntryNo); + CustomerEntry.TestField(Open, true); + CustomerApplication.ApplyCustEntryFormEntry(CustomerEntry); + end; + + procedure RequestUnapplication(CustomerEntryNo: Integer) + var + CustomerApplication: Codeunit "CustEntry-Apply Posted Entries"; + begin + RequireInteractiveSession(); + CustomerApplication.UnApplyCustLedgEntry(CustomerEntryNo); + end; + + local procedure RequireInteractiveSession() + begin + if not GuiAllowed() then + Error(InteractiveSessionErr); + end; + + var + InteractiveSessionErr: Label 'Request settlement from an interactive session.'; +} diff --git a/microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.md b/microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.md new file mode 100644 index 0000000..dfcc874 --- /dev/null +++ b/microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.md @@ -0,0 +1,37 @@ +--- +bc-version: [all] +domain: finance +keywords: [cust-ledger-entry, vendor-ledger-entry, detailed-ledger-entry, remaining-amount, application, unapplication, open, closed-by-entry-no] +technologies: [al] +countries: [w1] +application-area: [finance] +--- + +# Apply and unapply entries through the application workflow, not status flags + +## Description + +Customer/vendor settlement is a posting operation involving detailed ledger entries, not just a change to `Open` on the main entry. Remaining amounts are calculated from detailed entries. Unapplication posts correcting entries and handles application-derived effects such as discounts and currency gains/losses; deleting details or changing `Unapplied` cannot reproduce that history. + +## Best Practice + +Use `"CustEntry-Apply Posted Entries"` / `"VendEntry-Apply Posted Entries"` and the supported application or unapplication workflow. Let it check application dates, entry state, and application ordering. The `ApplyCustEntryFormEntry` / `ApplyVendEntryFormEntry` and `UnApply...LedgEntry` methods are **interactive**: users select and confirm the operation. They are not unattended "mark paid" APIs. + +For programmatic posting, use the target version's public `Apply` / `PostUnApply...` APIs with properly prepared selection and `Apply Unapply Parameters`; handle cancellation and the workflow's transaction/commit behavior. `"Applying Entry"`, `"Applies-to ID"`, and `"Amount to Apply"` are legitimate application-preparation fields. Do not report their writes alone, temporary buffers, supported posting/compression internals, or unrelated operational/extension fields. + +See sample: [`apply-ledger-entries-through-application-codeunits.good.al`](apply-ledger-entries-through-application-codeunits.good.al). + +## Anti Pattern + +Implement payment matching, settlement, or reopening by directly persisting `Open`, `"Closed by Entry No."`, closure amounts/dates, or detailed-entry unapplication flags, or by deleting/rewriting detailed application amounts. Require confirmed writes to existing non-temporary records and settlement intent. Do not suggest assigning a `Remaining Amount` FlowField as a fix. + +This article owns fabricated application state. Use the [posted-financial-content rule](do-not-modify-or-delete-posted-ledger-entries.md) for original accounting-value corrections, not a second finding prescribing the same application fix. + +See sample: [`apply-ledger-entries-through-application-codeunits.bad.al`](apply-ledger-entries-through-application-codeunits.bad.al). + +## References + +- [Apply and unapply customer transactions](https://learn.microsoft.com/en-us/dynamics365/business-central/receivables-how-apply-sales-transactions-manually). +- [Apply and unapply vendor transactions](https://learn.microsoft.com/en-us/dynamics365/business-central/payables-how-apply-purchase-transactions-manually). +- [BCApps: customer application workflow](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Sales/Receivables/CustEntryApplyPostedEntries.Codeunit.al). +- [BCApps: vendor application workflow](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Purchases/Payables/VendEntryApplyPostedEntries.Codeunit.al). diff --git a/microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.bad.al b/microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.bad.al new file mode 100644 index 0000000..7a7bec6 --- /dev/null +++ b/microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.bad.al @@ -0,0 +1,21 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50105 "Update Ledger Due Dates" +{ + procedure UpdateCustomerDueDate(EntryNo: Integer; NewDueDate: Date) + var + CustomerEntry: Record "Cust. Ledger Entry"; + begin + CustomerEntry.Get(EntryNo); + CustomerEntry.Validate("Due Date", NewDueDate); + CustomerEntry.Modify(true); + end; + + procedure UpdateVendorDueDate(EntryNo: Integer; NewDueDate: Date) + var + VendorEntry: Record "Vendor Ledger Entry"; + begin + VendorEntry.Get(EntryNo); + VendorEntry.Validate("Due Date", NewDueDate); + VendorEntry.Modify(true); + end; +} diff --git a/microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.good.al b/microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.good.al new file mode 100644 index 0000000..540548e --- /dev/null +++ b/microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.good.al @@ -0,0 +1,21 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50105 "Update Ledger Due Dates" +{ + procedure UpdateCustomerDueDate(EntryNo: Integer; NewDueDate: Date) + var + CustomerEntry: Record "Cust. Ledger Entry"; + begin + CustomerEntry.Get(EntryNo); + CustomerEntry.Validate("Due Date", NewDueDate); + Codeunit.Run(Codeunit::"Cust. Entry-Edit", CustomerEntry); + end; + + procedure UpdateVendorDueDate(EntryNo: Integer; NewDueDate: Date) + var + VendorEntry: Record "Vendor Ledger Entry"; + begin + VendorEntry.Get(EntryNo); + VendorEntry.Validate("Due Date", NewDueDate); + Codeunit.Run(Codeunit::"Vend. Entry-Edit", VendorEntry); + end; +} diff --git a/microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.md b/microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.md new file mode 100644 index 0000000..01fc1ee --- /dev/null +++ b/microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.md @@ -0,0 +1,35 @@ +--- +bc-version: [all] +domain: finance +keywords: [due-date, initial-entry-due-date, cust-entry-edit, vend-entry-edit, detailed-ledger-entry, aging] +technologies: [al] +countries: [w1] +application-area: [finance] +--- + +# Change posted customer/vendor due dates through the entry-edit workflow + +## Description + +A posted customer or vendor due date is supported editable operational data, but it is also represented by `Initial Entry Due Date` on related detailed ledger entries. Validating `Due Date` and calling `Modify(true)` on the main entry does not perform all synchronization done by `"Cust. Entry-Edit"` or `"Vend. Entry-Edit"`. The main entry and due-date-based analysis can otherwise disagree. + +## Best Practice + +Fetch the existing entry, validate the proposed `Due Date`, and pass the changed record to the corresponding entry-edit codeunit, as the standard ledger pages do. Field validation enforces entry-state rules; the editor persists the supported change and synchronizes related detailed entries. Preserve both steps rather than treating table triggers as equivalent to the edit workflow. + +This is a due-date synchronization rule, not a prohibition on all operational edits after posting. Exclude temporary buffers, extension-only fields, supported editor internals, and code that demonstrably performs the equivalent synchronization under the supported workflow. Check the actual table/routine instead of assuming every `*Entry-Edit` accepts the same fields. + +See sample: [`change-ledger-due-dates-through-entry-edit.good.al`](change-ledger-due-dates-through-entry-edit.good.al). + +## Anti Pattern + +Change `Due Date` on an existing non-temporary `Cust. Ledger Entry` or `Vendor Ledger Entry` and persist it with `Modify`, `Modify(true)`, or `ModifyAll` without the edit workflow or equivalent related-entry update. A preceding `Validate("Due Date", ...)` is not sufficient evidence of synchronization. + +See sample: [`change-ledger-due-dates-through-entry-edit.bad.al`](change-ledger-due-dates-through-entry-edit.bad.al). + +## References + +- [Cust. Entry-Edit API](https://learn.microsoft.com/en-us/dynamics365/business-central/application/base-application/codeunit/microsoft.sales.receivables.cust.-entry-edit). +- [Vend. Entry-Edit API](https://learn.microsoft.com/en-us/dynamics365/business-central/application/base-application/codeunit/microsoft.purchases.payables.vend.-entry-edit). +- [BCApps: customer due-date synchronization](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Sales/Receivables/CustEntryEdit.Codeunit.al). +- [BCApps: vendor due-date synchronization](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Purchases/Payables/VendEntryEdit.Codeunit.al). diff --git a/microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.bad.al b/microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.bad.al new file mode 100644 index 0000000..2c5761c --- /dev/null +++ b/microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.bad.al @@ -0,0 +1,14 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50103 "Change Journal Dimension" +{ + procedure ChangeExistingDimension(TemplateName: Code[10]; BatchName: Code[10]; LineNo: Integer; DimensionCode: Code[20]; NewValue: Code[20]) + var + JournalLine: Record "Gen. Journal Line"; + DimensionSetEntry: Record "Dimension Set Entry"; + begin + JournalLine.Get(TemplateName, BatchName, LineNo); + DimensionSetEntry.Get(JournalLine."Dimension Set ID", DimensionCode); + DimensionSetEntry.Validate("Dimension Value Code", NewValue); + DimensionSetEntry.Modify(); + end; +} diff --git a/microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.good.al b/microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.good.al new file mode 100644 index 0000000..72d09b3 --- /dev/null +++ b/microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.good.al @@ -0,0 +1,18 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50103 "Change Journal Dimension" +{ + procedure ChangeExistingDimension(TemplateName: Code[10]; BatchName: Code[10]; LineNo: Integer; DimensionCode: Code[20]; NewValue: Code[20]) + var + JournalLine: Record "Gen. Journal Line"; + TempDimensionSetEntry: Record "Dimension Set Entry" temporary; + DimensionManagement: Codeunit DimensionManagement; + begin + JournalLine.Get(TemplateName, BatchName, LineNo); + DimensionManagement.GetDimensionSet(TempDimensionSetEntry, JournalLine."Dimension Set ID"); + TempDimensionSetEntry.Get(JournalLine."Dimension Set ID", DimensionCode); + TempDimensionSetEntry.Validate("Dimension Value Code", NewValue); + TempDimensionSetEntry.Modify(); + JournalLine.Validate("Dimension Set ID", DimensionManagement.GetDimensionSetID(TempDimensionSetEntry)); + JournalLine.Modify(true); + end; +} diff --git a/microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.md b/microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.md new file mode 100644 index 0000000..38182c3 --- /dev/null +++ b/microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.md @@ -0,0 +1,35 @@ +--- +bc-version: [all] +domain: finance +keywords: [dimension-set-entry, dimension-value-id, getdimensionset, getdimensionsetid, posted-dimensions, temporary] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Change a transaction's dimension set reference, not a shared set's membership + +## Description + +The same `Dimension Set ID` can be referenced by an unposted journal line and by many already-posted entries. Editing or deleting the persisted set's dimension/value rows therefore changes the meaning of unrelated transactions, including posting history. The dimension-set search tree also relies on those combinations remaining stable; a set is not a mutable child collection owned by one journal line. + +## Best Practice + +To change an unposted transaction's dimensions, load its set into a **temporary** `Dimension Set Entry` buffer with `DimensionManagement.GetDimensionSet`, change the buffer, and obtain a reusable ID with `GetDimensionSetID`. Validate dimension values in the buffer so `Dimension Value ID` matches the chosen value. Store the resulting ID on the transaction and synchronize its projections through that record's supported dimension validation. + +For already-posted G/L dimensions, use the supported dimension-correction workflow rather than changing shared rows. Read-only access, temporary buffers, and standard maintenance of projection metadata such as `Global Dimension No.` are not membership changes. This rule protects shared posting history; it does not prescribe custom-table/default-dimension wiring. + +See sample: [`do-not-edit-shared-dimension-sets.good.al`](do-not-edit-shared-dimension-sets.good.al). + +## Anti Pattern + +Follow a journal/document/ledger `Dimension Set ID` to a **persistent** `Dimension Set Entry` and modify, rename, or delete its dimension/value membership in order to change that one transaction. Inspect `IsTemporary` guards, aliases, and the fields written before reporting: the same operations on a temporary working copy are expected. + +See sample: [`do-not-edit-shared-dimension-sets.bad.al`](do-not-edit-shared-dimension-sets.bad.al). + +## References + +- [Dimension set entries overview](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-dimension-set-entries-overview). +- [Supported G/L dimension correction](https://learn.microsoft.com/en-us/dynamics365/business-central/finance-troubleshooting-correcting-dimensions). +- [DimensionManagement API](https://learn.microsoft.com/en-us/dynamics365/business-central/application/base-application/codeunit/microsoft.finance.dimension.dimensionmanagement). +- [BCApps: Dimension Set Entry and its set-ID resolver](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Finance/Dimension/DimensionSetEntry.Table.al). diff --git a/microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.bad.al b/microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.bad.al new file mode 100644 index 0000000..7160785 --- /dev/null +++ b/microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.bad.al @@ -0,0 +1,32 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50101 "Correct Posted Transaction" +{ + procedure RequestTransactionReversal(EntryNo: Integer) + var + GLEntry: Record "G/L Entry"; + begin + if not GuiAllowed() then + Error(InteractiveSessionErr); + GLEntry.Get(EntryNo); + GLEntry.TestField("Transaction No."); + GLEntry.Amount := -GLEntry.Amount; + GLEntry.Modify(true); + end; + + procedure UpdateDescription(EntryNo: Integer; NewDescription: Text[100]) + var + GLEntry: Record "G/L Entry"; + begin + GLEntry.Get(EntryNo); + GLEntry.Description := NewDescription; + Codeunit.Run(Codeunit::"G/L Entry-Edit", GLEntry); + end; + + procedure ClearSimulation(var TempGLEntry: Record "G/L Entry" temporary) + begin + TempGLEntry.DeleteAll(); + end; + + var + InteractiveSessionErr: Label 'Request the reversal from an interactive session.'; +} diff --git a/microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.good.al b/microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.good.al new file mode 100644 index 0000000..1aa2c8f --- /dev/null +++ b/microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.good.al @@ -0,0 +1,32 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50101 "Correct Posted Transaction" +{ + procedure RequestTransactionReversal(EntryNo: Integer) + var + GLEntry: Record "G/L Entry"; + ReversalEntry: Record "Reversal Entry"; + begin + if not GuiAllowed() then + Error(InteractiveSessionErr); + GLEntry.Get(EntryNo); + GLEntry.TestField("Transaction No."); + ReversalEntry.ReverseTransaction(GLEntry."Transaction No."); + end; + + procedure UpdateDescription(EntryNo: Integer; NewDescription: Text[100]) + var + GLEntry: Record "G/L Entry"; + begin + GLEntry.Get(EntryNo); + GLEntry.Description := NewDescription; + Codeunit.Run(Codeunit::"G/L Entry-Edit", GLEntry); + end; + + procedure ClearSimulation(var TempGLEntry: Record "G/L Entry" temporary) + begin + TempGLEntry.DeleteAll(); + end; + + var + InteractiveSessionErr: Label 'Request the reversal from an interactive session.'; +} diff --git a/microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md b/microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md new file mode 100644 index 0000000..eca3a5e --- /dev/null +++ b/microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md @@ -0,0 +1,35 @@ +--- +bc-version: [all] +domain: finance +keywords: [g-l-entry, ledger-entry, reversal, audit-trail, correction, financial-content, entry-edit] +technologies: [al] +countries: [w1] +application-area: [finance] +--- + +# Correct posted financial content through posting workflows, not row surgery + +## Description + +Changing a posted entry's original amount, account, posting date, or tax amounts in place does not correct the related ledger, register, or source document. Deleting one erroneous row has the same problem. Business Central provides reversing and correcting posting workflows that retain the relationship between the original transaction and its correction; this is not a blanket prohibition on every write to a posted table. + +## Best Practice + +Use a supported transaction/register reversal, credit memo, or correcting journal appropriate to the original posting and its current state. Request the standard reversal workflow rather than negating a single row or setting `Reversed` yourself. Let that workflow enforce eligibility; do not change source or application fields to make a rejected reversal pass. + +Supported operational edits are deliberate exceptions: for example, `"G/L Entry-Edit"` supports description changes, and `"Cust. Entry-Edit"` / `"Vend. Entry-Edit"` handle their table-specific editable fields. [Due-date synchronization](change-ledger-due-dates-through-entry-edit.md), [application/unapplication](apply-ledger-entries-through-application-codeunits.md), G/L dimension correction, and supported date compression have their own workflows. Do not flag their standard implementations, temporary simulation buffers, or extension-only metadata updates as financial row surgery. A subscriber is not exempt merely because it runs inside a supported workflow: inspect the fields it actually changes. + +See sample: [`do-not-modify-or-delete-posted-ledger-entries.good.al`](do-not-modify-or-delete-posted-ledger-entries.good.al). + +## Anti Pattern + +Persist a change to original financial content, delete posted rows, or fabricate reversal flags/links to repair or undo a transaction outside the supported correction/maintenance workflow. Require an existing, non-temporary standard ledger record and evidence of the fields or rows affected; a `Modify` token alone is insufficient. Settlement-state writes belong to the application article rather than a duplicate finding here. + +See sample: [`do-not-modify-or-delete-posted-ledger-entries.bad.al`](do-not-modify-or-delete-posted-ledger-entries.bad.al). + +## References + +- [Reverse journal postings](https://learn.microsoft.com/en-us/dynamics365/business-central/finance-how-reverse-journal-posting). +- [Correct G/L dimensions](https://learn.microsoft.com/en-us/dynamics365/business-central/finance-troubleshooting-correcting-dimensions). +- [BCApps: G/L Entry-Edit](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Finance/GeneralLedger/Ledger/GLEntryEdit.Codeunit.al). +- [BCApps: supported customer-ledger date compression](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Sales/Receivables/DateCompressCustomerLedger.Report.al). diff --git a/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.bad.al b/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.bad.al new file mode 100644 index 0000000..e86a246 --- /dev/null +++ b/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.bad.al @@ -0,0 +1,56 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50108 "Import Purchase Journal Total" +{ + procedure ImportExampleTotal(TemplateName: Code[10]; BatchName: Code[10]; LineNo: Integer) + var + JournalLine: Record "Gen. Journal Line"; + VATPostingSetup: Record "VAT Posting Setup"; + GeneralLedgerSetup: Record "General Ledger Setup"; + SourceInvoice: JsonObject; + NetToken: JsonToken; + VATToken: JsonToken; + GrossToken: JsonToken; + ImportedNet: Decimal; + ImportedVAT: Decimal; + ImportedGross: Decimal; + begin + SourceInvoice.ReadFrom('{"netAmount":100,"vatAmount":25,"grossAmount":125}'); + SourceInvoice.Get('netAmount', NetToken); + SourceInvoice.Get('vatAmount', VATToken); + SourceInvoice.Get('grossAmount', GrossToken); + ImportedNet := NetToken.AsValue().AsDecimal(); + ImportedVAT := VATToken.AsValue().AsDecimal(); + ImportedGross := GrossToken.AsValue().AsDecimal(); + if ImportedGross <> ImportedNet + ImportedVAT then + Error(TotalsErr); + + JournalLine.Get(TemplateName, BatchName, LineNo); + JournalLine.TestField("Account Type", JournalLine."Account Type"::"G/L Account"); + JournalLine.TestField("Bal. Account Type", JournalLine."Bal. Account Type"::"G/L Account"); + JournalLine.TestField("Account No."); + JournalLine.TestField("Bal. Account No."); + JournalLine.TestField("Currency Code", ''); + JournalLine.TestField("Gen. Posting Type", JournalLine."Gen. Posting Type"::Purchase); + JournalLine.TestField("VAT Posting", JournalLine."VAT Posting"::"Automatic VAT Entry"); + JournalLine.TestField("VAT Calculation Type", JournalLine."VAT Calculation Type"::"Normal VAT"); + JournalLine.TestField("VAT %", 25); + JournalLine.TestField("VAT Difference", 0); + JournalLine.TestField("Non-Deductible VAT %", 0); + JournalLine.TestField("Bal. Gen. Posting Type", JournalLine."Bal. Gen. Posting Type"::" "); + JournalLine.TestField("Bal. VAT %", 0); + VATPostingSetup.Get(JournalLine."VAT Bus. Posting Group", JournalLine."VAT Prod. Posting Group"); + VATPostingSetup.TestField("VAT Calculation Type", VATPostingSetup."VAT Calculation Type"::"Normal VAT"); + VATPostingSetup.TestField("VAT %", 25); + VATPostingSetup.TestField("Non-Deductible VAT %", 0); + VATPostingSetup.TestField("Unrealized VAT Type", VATPostingSetup."Unrealized VAT Type"::" "); + GeneralLedgerSetup.Get(); + GeneralLedgerSetup.TestField("Additional Reporting Currency", ''); + GeneralLedgerSetup.TestField("Amount Rounding Precision", 0.01); + + JournalLine.Validate(Amount, ImportedNet); + JournalLine.Modify(true); + end; + + var + TotalsErr: Label 'The invoice total must equal its net amount plus VAT.'; +} diff --git a/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.good.al b/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.good.al new file mode 100644 index 0000000..4a54dba --- /dev/null +++ b/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.good.al @@ -0,0 +1,56 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50108 "Import Purchase Journal Total" +{ + procedure ImportExampleTotal(TemplateName: Code[10]; BatchName: Code[10]; LineNo: Integer) + var + JournalLine: Record "Gen. Journal Line"; + VATPostingSetup: Record "VAT Posting Setup"; + GeneralLedgerSetup: Record "General Ledger Setup"; + SourceInvoice: JsonObject; + NetToken: JsonToken; + VATToken: JsonToken; + GrossToken: JsonToken; + ImportedNet: Decimal; + ImportedVAT: Decimal; + ImportedGross: Decimal; + begin + SourceInvoice.ReadFrom('{"netAmount":100,"vatAmount":25,"grossAmount":125}'); + SourceInvoice.Get('netAmount', NetToken); + SourceInvoice.Get('vatAmount', VATToken); + SourceInvoice.Get('grossAmount', GrossToken); + ImportedNet := NetToken.AsValue().AsDecimal(); + ImportedVAT := VATToken.AsValue().AsDecimal(); + ImportedGross := GrossToken.AsValue().AsDecimal(); + if ImportedGross <> ImportedNet + ImportedVAT then + Error(TotalsErr); + + JournalLine.Get(TemplateName, BatchName, LineNo); + JournalLine.TestField("Account Type", JournalLine."Account Type"::"G/L Account"); + JournalLine.TestField("Bal. Account Type", JournalLine."Bal. Account Type"::"G/L Account"); + JournalLine.TestField("Account No."); + JournalLine.TestField("Bal. Account No."); + JournalLine.TestField("Currency Code", ''); + JournalLine.TestField("Gen. Posting Type", JournalLine."Gen. Posting Type"::Purchase); + JournalLine.TestField("VAT Posting", JournalLine."VAT Posting"::"Automatic VAT Entry"); + JournalLine.TestField("VAT Calculation Type", JournalLine."VAT Calculation Type"::"Normal VAT"); + JournalLine.TestField("VAT %", 25); + JournalLine.TestField("VAT Difference", 0); + JournalLine.TestField("Non-Deductible VAT %", 0); + JournalLine.TestField("Bal. Gen. Posting Type", JournalLine."Bal. Gen. Posting Type"::" "); + JournalLine.TestField("Bal. VAT %", 0); + VATPostingSetup.Get(JournalLine."VAT Bus. Posting Group", JournalLine."VAT Prod. Posting Group"); + VATPostingSetup.TestField("VAT Calculation Type", VATPostingSetup."VAT Calculation Type"::"Normal VAT"); + VATPostingSetup.TestField("VAT %", 25); + VATPostingSetup.TestField("Non-Deductible VAT %", 0); + VATPostingSetup.TestField("Unrealized VAT Type", VATPostingSetup."Unrealized VAT Type"::" "); + GeneralLedgerSetup.Get(); + GeneralLedgerSetup.TestField("Additional Reporting Currency", ''); + GeneralLedgerSetup.TestField("Amount Rounding Precision", 0.01); + + JournalLine.Validate(Amount, ImportedGross); + JournalLine.Modify(true); + end; + + var + TotalsErr: Label 'The invoice total must equal its net amount plus VAT.'; +} diff --git a/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.md b/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.md new file mode 100644 index 0000000..80e3dec --- /dev/null +++ b/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.md @@ -0,0 +1,35 @@ +--- +bc-version: [all] +domain: finance +keywords: [normal-vat, automatic-vat-entry, gross-amount, net-amount, vat-posting-setup, gen-journal-line, purchase] +technologies: [al] +countries: [w1] +application-area: [finance] +--- + +# Supply a VAT-inclusive journal Amount for automatic Normal VAT + +## Description + +For a general-journal line using **Automatic VAT Entry** and **Normal VAT**, `Amount` includes VAT. The posting engine extracts tax from that total; it does not add tax to a VAT-exclusive expense imported into `Amount`. For an LCY purchase of net 100 plus 25 VAT, entering 100 produces an 80 expense and 20 VAT, rather than the intended 100 expense and 25 VAT from a total of 125. + +## Best Practice + +Map the source's VAT-inclusive total to journal `Amount` in this posting mode. Establish the intended account and posting-group combination before validating the final amount. Account-derived VAT defaults depend on `Copy VAT Setup to Jnl. Lines`; do not assume account selection always supplies the intended configuration. + +Require the actual input contract and calculation mode, not just a variable named `NetAmount`. The examples encode source net, VAT, and gross values plus a 25% Normal-VAT setup check. They target an LCY G/L purchase with 0.01 amount rounding and without balancing-side VAT, additional reporting currency, VAT differences, unrealized VAT, or non-deductible VAT. Other calculation types, Manual VAT Entry, reverse charge, Full VAT, sales/use tax, non-deductible or unrealized tax, and other currency/rounding contexts need their own analysis; this is not a universal gross-up formula or country-specific tax advice. + +See sample: [`normal-vat-journal-amount-includes-vat.good.al`](normal-vat-journal-amount-includes-vat.good.al). + +## Anti Pattern + +In the demonstrated automatic Normal-VAT configuration, put a provably VAT-exclusive source amount into journal `Amount` while expecting posting to add tax. An amount assignment alone, unknown setup, or a suggestive variable name is insufficient. Do not report the correctly supplied gross amount or automatically rewrite tax calculations outside this scope. + +See sample: [`normal-vat-journal-amount-includes-vat.bad.al`](normal-vat-journal-amount-includes-vat.bad.al). + +## References + +- [VAT posting setup combinations](https://learn.microsoft.com/en-us/dynamics365/business-central/finance-setup-vat#combine-vat-posting-groups-in-vat-posting-setups). +- [BCApps: journal amount validation](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Finance/GeneralLedger/Journal/GenJournalLine.Table.al). +- [BCApps: Normal VAT extraction during posting](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Finance/GeneralLedger/Posting/GenJnlPostLine.Codeunit.al). +- [BCApps: journal VAT amount regression cases](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/Tests/VAT/ERMVATOnGenJournalLine.Codeunit.al). diff --git a/microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.bad.al b/microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.bad.al new file mode 100644 index 0000000..948976b --- /dev/null +++ b/microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.bad.al @@ -0,0 +1,60 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50100 "Post Transfer Journal" +{ + procedure PostTransferBatch(TemplateName: Code[10]; BatchName: Code[10]) + var + JournalLine: Record "Gen. Journal Line"; + LastGLEntry: Record "G/L Entry"; + NextEntryNo: Integer; + begin + JournalLine.SetRange("Journal Template Name", TemplateName); + JournalLine.SetRange("Journal Batch Name", BatchName); + if JournalLine.Count() <> 1 then + Error(SingleTransferErr); + JournalLine.FindFirst(); + JournalLine.TestField("Account Type", JournalLine."Account Type"::"G/L Account"); + JournalLine.TestField("Bal. Account Type", JournalLine."Bal. Account Type"::"G/L Account"); + JournalLine.TestField("Account No."); + JournalLine.TestField("Bal. Account No."); + JournalLine.TestField("Posting Date"); + JournalLine.TestField("Document No."); + JournalLine.TestField(Amount); + JournalLine.TestField("Currency Code", ''); + JournalLine.TestField("Gen. Posting Type", JournalLine."Gen. Posting Type"::" "); + JournalLine.TestField("Bal. Gen. Posting Type", JournalLine."Bal. Gen. Posting Type"::" "); + + LastGLEntry.LockTable(); + if LastGLEntry.FindLast() then + NextEntryNo := LastGLEntry."Entry No." + 1 + else + NextEntryNo := 1; + InsertLedgerRow(JournalLine, NextEntryNo, JournalLine."Account No.", JournalLine.Amount); + InsertLedgerRow(JournalLine, NextEntryNo + 1, JournalLine."Bal. Account No.", -JournalLine.Amount); + end; + + local procedure InsertLedgerRow(JournalLine: Record "Gen. Journal Line"; EntryNo: Integer; AccountNo: Code[20]; Amount: Decimal) + var + GLEntry: Record "G/L Entry"; + begin + GLEntry.Init(); + GLEntry."Entry No." := EntryNo; + GLEntry."G/L Account No." := AccountNo; + GLEntry."Posting Date" := JournalLine."Posting Date"; + GLEntry."Document Type" := JournalLine."Document Type"; + GLEntry."Document No." := JournalLine."Document No."; + GLEntry."Source Code" := JournalLine."Source Code"; + GLEntry."Journal Batch Name" := JournalLine."Journal Batch Name"; + GLEntry."Dimension Set ID" := JournalLine."Dimension Set ID"; + GLEntry."Global Dimension 1 Code" := JournalLine."Shortcut Dimension 1 Code"; + GLEntry."Global Dimension 2 Code" := JournalLine."Shortcut Dimension 2 Code"; + GLEntry.Amount := Amount; + if Amount > 0 then + GLEntry."Debit Amount" := Amount + else + GLEntry."Credit Amount" := -Amount; + GLEntry.Insert(true); + end; + + var + SingleTransferErr: Label 'Use a journal batch containing exactly one self-balancing G/L transfer.'; +} diff --git a/microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.good.al b/microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.good.al new file mode 100644 index 0000000..4d3bee3 --- /dev/null +++ b/microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.good.al @@ -0,0 +1,29 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50100 "Post Transfer Journal" +{ + procedure PostTransferBatch(TemplateName: Code[10]; BatchName: Code[10]) + var + JournalLine: Record "Gen. Journal Line"; + PostBatch: Codeunit "Gen. Jnl.-Post Batch"; + begin + JournalLine.SetRange("Journal Template Name", TemplateName); + JournalLine.SetRange("Journal Batch Name", BatchName); + if JournalLine.Count() <> 1 then + Error(SingleTransferErr); + JournalLine.FindFirst(); + JournalLine.TestField("Account Type", JournalLine."Account Type"::"G/L Account"); + JournalLine.TestField("Bal. Account Type", JournalLine."Bal. Account Type"::"G/L Account"); + JournalLine.TestField("Account No."); + JournalLine.TestField("Bal. Account No."); + JournalLine.TestField("Posting Date"); + JournalLine.TestField("Document No."); + JournalLine.TestField(Amount); + JournalLine.TestField("Currency Code", ''); + JournalLine.TestField("Gen. Posting Type", JournalLine."Gen. Posting Type"::" "); + JournalLine.TestField("Bal. Gen. Posting Type", JournalLine."Bal. Gen. Posting Type"::" "); + PostBatch.Run(JournalLine); + end; + + var + SingleTransferErr: Label 'Use a journal batch containing exactly one self-balancing G/L transfer.'; +} diff --git a/microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.md b/microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.md new file mode 100644 index 0000000..e24856b --- /dev/null +++ b/microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.md @@ -0,0 +1,36 @@ +--- +bc-version: [all] +domain: finance +keywords: [g-l-entry, ledger-entry, gen-jnl-post-line, gen-jnl-post-batch, journal-line, register, insert] +technologies: [al] +countries: [w1] +application-area: [finance] +--- + +# Create financial ledger entries through the owning posting engine + +## Description + +Standard financial ledger entries are outputs of posting, not independent rows an extension manufactures. Even two manually inserted G/L rows with balanced amounts bypass posting checks, register bookkeeping, and transaction/source relationships. `G/L Entry.Insert(true)` runs the table trigger; it does not invoke the posting engine. + +## Best Practice + +Use the owning document or journal posting workflow. For a normal persisted general-journal batch, use `"Gen. Jnl.-Post Batch"`; the example posts an existing batch containing one self-balancing, non-VAT G/L transfer. Let posting allocate entries and maintain the register rather than reconstructing its tables. + +`"Gen. Jnl.-Post Line".RunWithCheck` is appropriate for a complete journal line inside a correctly owned posting lifecycle, but it does not invent a balancing account or document number, allocate numbering merely from `Posting No. Series`, or replace [batch document-balancing policy](preserve-journal-batch-document-balance.md). The line codeunit is stateful; its checked wrapper owns its start/continue/finish work. Normal batch posting owns its numbering and commits by default; do not imply these entry points are transaction-neutral. + +Exclude temporary buffers and the standard engine's own insertion points. A checked parent may legitimately use `RunWithoutCheck`; do not replace it without inspecting that parent. This rule concerns standard financial ledgers, not a custom table merely named `Ledger Entry` or a supported, specifically reviewed migration/repair workflow. + +See sample: [`post-ledger-entries-through-posting-codeunits.good.al`](post-ledger-entries-through-posting-codeunits.good.al). + +## Anti Pattern + +Create posted financial effects by directly inserting persistent `G/L Entry`, customer/vendor ledger, or VAT ledger rows outside their owning posting workflow. Resolve the actual record type and lifecycle. Balanced debit/credit values, copied dimensions, `Insert(true)`, and a lock around entry-number allocation do not turn raw inserts into a complete posting. + +See sample: [`post-ledger-entries-through-posting-codeunits.bad.al`](post-ledger-entries-through-posting-codeunits.bad.al). + +## References + +- [Posting engine structure](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-posting-engine-structure). +- [Gen. Jnl.-Post Line API](https://learn.microsoft.com/en-us/dynamics365/business-central/application/base-application/codeunit/microsoft.finance.generalledger.posting.gen.-jnl.-post-line). +- [BCApps: posting lifecycle and register maintenance](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Finance/GeneralLedger/Posting/GenJnlPostLine.Codeunit.al). diff --git a/microsoft/knowledge/finance/preserve-journal-batch-document-balance.bad.al b/microsoft/knowledge/finance/preserve-journal-batch-document-balance.bad.al new file mode 100644 index 0000000..40d4d43 --- /dev/null +++ b/microsoft/knowledge/finance/preserve-journal-batch-document-balance.bad.al @@ -0,0 +1,51 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50107 "Post Journal Allocation" +{ + procedure PostAllocation(TemplateName: Code[10]; BatchName: Code[10]; DebitAccount: Code[20]; CreditAccount: Code[20]; PostingDate: Date) + var + JournalTemplate: Record "Gen. Journal Template"; + JournalBatch: Record "Gen. Journal Batch"; + JournalLine: Record "Gen. Journal Line"; + PostLine: Codeunit "Gen. Jnl.-Post Line"; + begin + JournalTemplate.Get(TemplateName); + JournalTemplate.TestField(Recurring, false); + JournalTemplate.TestField("Force Doc. Balance", true); + JournalTemplate.TestField("Source Code"); + JournalBatch.Get(TemplateName, BatchName); + JournalBatch.TestField("No. Series", ''); + JournalBatch.TestField("Posting No. Series", ''); + JournalLine.SetRange("Journal Template Name", TemplateName); + JournalLine.SetRange("Journal Batch Name", BatchName); + if not JournalLine.IsEmpty() then + Error(EmptyBatchErr); + + AddAllocationLine(JournalTemplate, BatchName, 10000, DebitAccount, PostingDate, 'ALLOC-A', 90); + AddAllocationLine(JournalTemplate, BatchName, 20000, CreditAccount, PostingDate, 'ALLOC-B', -90); + JournalLine.FindSet(); + repeat + PostLine.RunWithCheck(JournalLine); + until JournalLine.Next() = 0; + end; + + local procedure AddAllocationLine(JournalTemplate: Record "Gen. Journal Template"; BatchName: Code[10]; LineNo: Integer; AccountNo: Code[20]; PostingDate: Date; DocumentNo: Code[20]; Amount: Decimal) + var + JournalLine: Record "Gen. Journal Line"; + begin + JournalLine.Init(); + JournalLine."Journal Template Name" := JournalTemplate.Name; + JournalLine."Journal Batch Name" := BatchName; + JournalLine."Line No." := LineNo; + JournalLine."Source Code" := JournalTemplate."Source Code"; + JournalLine.Validate("Posting Date", PostingDate); + JournalLine.Validate("Document No.", DocumentNo); + JournalLine.Validate("Account Type", JournalLine."Account Type"::"G/L Account"); + JournalLine.Validate("Account No.", AccountNo); + JournalLine.Validate("Gen. Posting Type", JournalLine."Gen. Posting Type"::" "); + JournalLine.Validate(Amount, Amount); + JournalLine.Insert(true); + end; + + var + EmptyBatchErr: Label 'Use an empty journal batch for this allocation.'; +} diff --git a/microsoft/knowledge/finance/preserve-journal-batch-document-balance.good.al b/microsoft/knowledge/finance/preserve-journal-batch-document-balance.good.al new file mode 100644 index 0000000..040269c --- /dev/null +++ b/microsoft/knowledge/finance/preserve-journal-batch-document-balance.good.al @@ -0,0 +1,49 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50107 "Post Journal Allocation" +{ + procedure PostAllocation(TemplateName: Code[10]; BatchName: Code[10]; DebitAccount: Code[20]; CreditAccount: Code[20]; PostingDate: Date) + var + JournalTemplate: Record "Gen. Journal Template"; + JournalBatch: Record "Gen. Journal Batch"; + JournalLine: Record "Gen. Journal Line"; + PostBatch: Codeunit "Gen. Jnl.-Post Batch"; + begin + JournalTemplate.Get(TemplateName); + JournalTemplate.TestField(Recurring, false); + JournalTemplate.TestField("Force Doc. Balance", true); + JournalTemplate.TestField("Source Code"); + JournalBatch.Get(TemplateName, BatchName); + JournalBatch.TestField("No. Series", ''); + JournalBatch.TestField("Posting No. Series", ''); + JournalLine.SetRange("Journal Template Name", TemplateName); + JournalLine.SetRange("Journal Batch Name", BatchName); + if not JournalLine.IsEmpty() then + Error(EmptyBatchErr); + + AddAllocationLine(JournalTemplate, BatchName, 10000, DebitAccount, PostingDate, 'ALLOC-A', 90); + AddAllocationLine(JournalTemplate, BatchName, 20000, CreditAccount, PostingDate, 'ALLOC-A', -90); + JournalLine.FindFirst(); + PostBatch.Run(JournalLine); + end; + + local procedure AddAllocationLine(JournalTemplate: Record "Gen. Journal Template"; BatchName: Code[10]; LineNo: Integer; AccountNo: Code[20]; PostingDate: Date; DocumentNo: Code[20]; Amount: Decimal) + var + JournalLine: Record "Gen. Journal Line"; + begin + JournalLine.Init(); + JournalLine."Journal Template Name" := JournalTemplate.Name; + JournalLine."Journal Batch Name" := BatchName; + JournalLine."Line No." := LineNo; + JournalLine."Source Code" := JournalTemplate."Source Code"; + JournalLine.Validate("Posting Date", PostingDate); + JournalLine.Validate("Document No.", DocumentNo); + JournalLine.Validate("Account Type", JournalLine."Account Type"::"G/L Account"); + JournalLine.Validate("Account No.", AccountNo); + JournalLine.Validate("Gen. Posting Type", JournalLine."Gen. Posting Type"::" "); + JournalLine.Validate(Amount, Amount); + JournalLine.Insert(true); + end; + + var + EmptyBatchErr: Label 'Use an empty journal batch for this allocation.'; +} diff --git a/microsoft/knowledge/finance/preserve-journal-batch-document-balance.md b/microsoft/knowledge/finance/preserve-journal-batch-document-balance.md new file mode 100644 index 0000000..27751f9 --- /dev/null +++ b/microsoft/knowledge/finance/preserve-journal-batch-document-balance.md @@ -0,0 +1,35 @@ +--- +bc-version: [all] +domain: finance +keywords: [force-doc-balance, gen-journal-template, gen-jnl-post-batch, runwithcheck, document-no, posting-date, balancing] +technologies: [al] +countries: [w1] +application-area: [finance] +--- + +# Preserve the journal batch's document-balancing policy + +## Description + +A balanced G/L total does not prove that a general-journal batch satisfies its template's document-balancing policy. `"Gen. Jnl.-Post Batch"` checks balances at posting-date boundaries and, when the **journal template's** `Force Doc. Balance` is enabled, document-type/document-number boundaries. A loop over `"Gen. Jnl.-Post Line".RunWithCheck` does not reproduce these batch-level checks. + +## Best Practice + +Post normal persisted general-journal batches through their owning batch workflow. Keep balancing lines in the appropriate document/date group when the template requires it. The examples use two LCY G/L lines: opposite amounts under different document numbers are not a document-balanced transfer when `Force Doc. Balance` is true; the good example groups them under one document and retains the batch checks. + +Do not claim every document must always balance: when that template option is false, the supported workflow can allow document imbalance while still checking the required aggregate balances. Standalone self-balancing line posting and purpose-built posting engines that demonstrably own equivalent aggregate policies are not prohibited. A `RunWithCheck` call or loop alone is not sufficient evidence of a defect. + +See sample: [`preserve-journal-batch-document-balance.good.al`](preserve-journal-batch-document-balance.good.al). + +## Anti Pattern + +Replace a normal persisted journal batch's posting path with per-line posting or only an aggregate-total check, bypassing a demonstrated template/document/date policy. For the document-imbalance finding, require evidence that `Force Doc. Balance` applies and that separate document groups can be unbalanced; do not infer the setting from its name or a comment alone. + +See sample: [`preserve-journal-batch-document-balance.bad.al`](preserve-journal-batch-document-balance.bad.al). + +## References + +- [Work with general journals](https://learn.microsoft.com/en-us/dynamics365/business-central/ui-work-general-journals). +- [Gen. Jnl.-Post Batch API](https://learn.microsoft.com/en-us/dynamics365/business-central/application/base-application/codeunit/microsoft.finance.generalledger.posting.gen.-jnl.-post-batch). +- [BCApps: batch balance checks](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Finance/GeneralLedger/Posting/GenJnlPostBatch.Codeunit.al). +- [BCApps: document-balance option regression cases](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/Tests/General%20Journal/ERMTestMultipleGenJnlLines.Codeunit.al). diff --git a/microsoft/knowledge/finance/reverse-transactions-by-transaction-number.bad.al b/microsoft/knowledge/finance/reverse-transactions-by-transaction-number.bad.al new file mode 100644 index 0000000..9f5d311 --- /dev/null +++ b/microsoft/knowledge/finance/reverse-transactions-by-transaction-number.bad.al @@ -0,0 +1,18 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50106 "Reverse Selected Posting" +{ + procedure RequestReversal(SelectedEntryNo: Integer) + var + GLEntry: Record "G/L Entry"; + ReversalEntry: Record "Reversal Entry"; + begin + if not GuiAllowed() then + Error(InteractiveSessionErr); + GLEntry.Get(SelectedEntryNo); + GLEntry.TestField("Transaction No."); + ReversalEntry.ReverseTransaction(GLEntry."Entry No."); + end; + + var + InteractiveSessionErr: Label 'Request the reversal from an interactive session.'; +} diff --git a/microsoft/knowledge/finance/reverse-transactions-by-transaction-number.good.al b/microsoft/knowledge/finance/reverse-transactions-by-transaction-number.good.al new file mode 100644 index 0000000..392d3cd --- /dev/null +++ b/microsoft/knowledge/finance/reverse-transactions-by-transaction-number.good.al @@ -0,0 +1,18 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50106 "Reverse Selected Posting" +{ + procedure RequestReversal(SelectedEntryNo: Integer) + var + GLEntry: Record "G/L Entry"; + ReversalEntry: Record "Reversal Entry"; + begin + if not GuiAllowed() then + Error(InteractiveSessionErr); + GLEntry.Get(SelectedEntryNo); + GLEntry.TestField("Transaction No."); + ReversalEntry.ReverseTransaction(GLEntry."Transaction No."); + end; + + var + InteractiveSessionErr: Label 'Request the reversal from an interactive session.'; +} diff --git a/microsoft/knowledge/finance/reverse-transactions-by-transaction-number.md b/microsoft/knowledge/finance/reverse-transactions-by-transaction-number.md new file mode 100644 index 0000000..d61f3a2 --- /dev/null +++ b/microsoft/knowledge/finance/reverse-transactions-by-transaction-number.md @@ -0,0 +1,34 @@ +--- +bc-version: [all] +domain: finance +keywords: [reversetransaction, reverseregister, transaction-no, entry-no, reversal-entry, g-l-register] +technologies: [al] +countries: [w1] +application-area: [finance] +--- + +# Pass the transaction number, not a ledger-entry number, to ReverseTransaction + +## Description + +`Reversal Entry.ReverseTransaction` expects a **transaction number**, while `ReverseRegister` expects a **G/L register number**. Neither parameter means a ledger `Entry No.`. All are integers, so the compiler accepts the wrong identity; a coincidentally matching integer can select another transaction instead of the posting the user intended to reverse. + +## Best Practice + +When starting from a `G/L Entry`, fetch that entry and pass **its** `Transaction No.` to `Reversal Entry.ReverseTransaction`, as the sample does. The same identity distinction applies to customer/vendor ledger entries when using their supported transaction-reversal path. If the starting point is a G/L register, use `Reversal Entry.ReverseRegister` with that register's number. The interactive workflow collects the participating entries and validates reversal eligibility before the user posts the reversal. + +Do not infer eligibility from `Open` alone or bypass a rejection by changing origin, application, or reversal fields. The supported path depends on source and state; some postings require unapplication or a correcting document first. A request to reverse is not a guarantee that reversal will be permitted. This rule concerns the two named `Reversal Entry` APIs, not routines such as `UnApplyCustLedgEntry` that legitimately accept a ledger entry number. + +See sample: [`reverse-transactions-by-transaction-number.good.al`](reverse-transactions-by-transaction-number.good.al). + +## Anti Pattern + +Pass a ledger entry's `Entry No.` or a register number into `ReverseTransaction`, or pass a ledger-entry/transaction number into `ReverseRegister`. Require visible value provenance, not merely a suspicious variable name or an arbitrary integer. A correctly sourced transaction number is valid even when the variable is poorly named. + +See sample: [`reverse-transactions-by-transaction-number.bad.al`](reverse-transactions-by-transaction-number.bad.al). + +## References + +- [Reversal Entry API](https://learn.microsoft.com/en-us/dynamics365/business-central/application/base-application/table/microsoft.finance.generalledger.reversal.reversal-entry). +- [Reverse journal postings](https://learn.microsoft.com/en-us/dynamics365/business-central/finance-how-reverse-journal-posting). +- [BCApps: reversal entry selection](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Finance/GeneralLedger/Reversal/ReversalEntry.Table.al). diff --git a/microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.bad.al b/microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.bad.al new file mode 100644 index 0000000..f54d42b --- /dev/null +++ b/microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.bad.al @@ -0,0 +1,20 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50102 "Copy Journal Dimensions" +{ + procedure CopyAllLineDimensions(TemplateName: Code[10]; BatchName: Code[10]; SourceLineNo: Integer; TargetLineNo: Integer) + var + SourceLine: Record "Gen. Journal Line"; + TargetLine: Record "Gen. Journal Line"; + begin + SourceLine.Get(TemplateName, BatchName, SourceLineNo); + TargetLine.Get(TemplateName, BatchName, TargetLineNo); + TargetLine."Shortcut Dimension 1 Code" := SourceLine."Shortcut Dimension 1 Code"; + TargetLine."Shortcut Dimension 2 Code" := SourceLine."Shortcut Dimension 2 Code"; + TargetLine.Modify(true); + end; + + procedure HasShortcutDimension1(JournalLine: Record "Gen. Journal Line"; DimensionValue: Code[20]): Boolean + begin + exit(JournalLine."Shortcut Dimension 1 Code" = DimensionValue); + end; +} diff --git a/microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.good.al b/microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.good.al new file mode 100644 index 0000000..330c0de --- /dev/null +++ b/microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.good.al @@ -0,0 +1,19 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50102 "Copy Journal Dimensions" +{ + procedure CopyAllLineDimensions(TemplateName: Code[10]; BatchName: Code[10]; SourceLineNo: Integer; TargetLineNo: Integer) + var + SourceLine: Record "Gen. Journal Line"; + TargetLine: Record "Gen. Journal Line"; + begin + SourceLine.Get(TemplateName, BatchName, SourceLineNo); + TargetLine.Get(TemplateName, BatchName, TargetLineNo); + TargetLine.Validate("Dimension Set ID", SourceLine."Dimension Set ID"); + TargetLine.Modify(true); + end; + + procedure HasShortcutDimension1(JournalLine: Record "Gen. Journal Line"; DimensionValue: Code[20]): Boolean + begin + exit(JournalLine."Shortcut Dimension 1 Code" = DimensionValue); + end; +} diff --git a/microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.md b/microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.md new file mode 100644 index 0000000..7286b6b --- /dev/null +++ b/microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.md @@ -0,0 +1,34 @@ +--- +bc-version: [all] +domain: finance +keywords: [dimension-set-id, shortcut-dimension, global-dimension, journal-line, posting, copy-dimensions] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Copy complete posting dimension sets, not only shortcut projections + +## Description + +When a journal line or posting document inherits dimensions, its `Dimension Set ID` identifies the complete combination of `Dimension Set Entry` rows. Global and shortcut dimensions expose selected dimensions, not the complete set; the eight shortcut dimensions are not a limit on set membership. Copying only these projections can leave the destination's posting dimensions unchanged or silently lose dimensions outside the shortcuts. + +## Best Practice + +For an intentional **complete** dimension transfer, copy the source set ID and synchronize the destination's projections through its supported validation or dimension-management routine. On `Gen. Journal Line`, `Validate("Dimension Set ID", SourceSetID)` updates the two shortcut fields. Do not assume another table has the same validation trigger. + +When line-specific dimensions must survive a header change, use the appropriate set-combination or delta routine instead of blindly replacing the line's entire set. Reading or filtering a known global dimension is legitimate; it is not a claim to enumerate every dimension. This rule owns dimension transfer through existing journal/document posting records, not custom-table or master `Default Dimension` wiring, which belongs to data modeling. + +See sample: [`write-dimensions-as-dimension-set-entries.good.al`](write-dimensions-as-dimension-set-entries.good.al). + +## Anti Pattern + +A routine intended to copy **all** posting dimensions copies only global/shortcut codes, or assigns a set ID without synchronizing the destination's stored projections. Require evidence of a complete-transfer intent and inspect surrounding validation; an explicit change to one selected dimension or a read-only filter is not this defect. + +See sample: [`write-dimensions-as-dimension-set-entries.bad.al`](write-dimensions-as-dimension-set-entries.bad.al). + +## References + +- [Dimension set entries overview](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-dimension-set-entries-overview). +- [DimensionManagement API](https://learn.microsoft.com/en-us/dynamics365/business-central/application/base-application/codeunit/microsoft.finance.dimension.dimensionmanagement). +- [BCApps: Gen. Journal Line dimension validation](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Finance/GeneralLedger/Journal/GenJournalLine.Table.al). diff --git a/microsoft/skills/review/al-code-review.md b/microsoft/skills/review/al-code-review.md index dc1f1db..4287a63 100644 --- a/microsoft/skills/review/al-code-review.md +++ b/microsoft/skills/review/al-code-review.md @@ -28,6 +28,7 @@ sub-skills: - microsoft/skills/review/al-reporting-review.md - microsoft/skills/review/al-appsource-review.md - microsoft/skills/review/al-telemetry-review.md + - microsoft/skills/review/al-finance-review.md --- # AL code review diff --git a/microsoft/skills/review/al-finance-review.md b/microsoft/skills/review/al-finance-review.md new file mode 100644 index 0000000..228086d --- /dev/null +++ b/microsoft/skills/review/al-finance-review.md @@ -0,0 +1,130 @@ +--- +kind: action-skill +id: al-finance-review +version: 1 +title: AL Finance review +description: Reviews financial journal posting, ledger corrections, applications, VAT handling, and posting-linked dimensions against BCQuality Finance guidance. +inputs: [pr-diff, file-path, folder-path] +outputs: [findings-report] +bc-version: [all] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# AL Finance review + +Reviews the `finance` knowledge domain. This is a leaf action skill composed by +`al-code-review`; it invokes no other skills. Application-area discovery is +broad enough for posting-linked dimensions, but source relevance is narrow. + +## Source + +Use READ's **Bounded retrieval for review skills** workflow with +`-Domain finance`. Consume every catalog page across enabled layers, preserving +each exact path and applicability metadata. Do not select a top-k catalog or +deduplicate by basename. Open complete bodies only for exact Worklist paths, +in stable chunks of at most eight, consuming every continuation. If the helper +or prepared index is unavailable or invalid, use READ's explicit path-discovery +and bounded native-read fallback; a retrieval error is not an empty corpus. + +## Relevance + +Apply READ's frontmatter matching rules using only known task dimensions. +Use the target application version from `app.json` when available; do not invent +a country or application area from a filename or UI `ApplicationArea` token. +Retain conditional articles only when configured, cap resulting confidence at +`medium`, and name every unknown dimension in the finding. + +Inspect the supplied AL scope and its enclosing declarations. Admit only +changed executable behavior involving at least one of these surfaces: + +- General-journal construction or posting, journal-batch processing, or an + event subscriber whose resolved publisher is in the financial posting path. +- Writes or correction/application/reversal calls involving standard financial + ledger records, detailed customer/vendor entries, VAT entries, or G/L registers. +- Dimension transfer or dimension-set mutation connected by visible data flow + to an existing journal, posting document, or financial ledger record. + +Return `not-applicable` when none is present. Imports, object names, comments, +read-only ledger displays, generic `Amount`/`Date`/`Open` fields, and calls to +`DimensionManagement` without posting-linked context do not establish relevance. +For a diff, retain surrounding variable types, field provenance, event +attributes, and reachable helpers; do not review isolated added lines without +the context needed to classify their record or call. + +## Worklist + +Match the complete relevant catalog's keywords, titles, and descriptions to +the admitted source surfaces. Add an exact catalog path only when its concern +maps to the changed behavior; generic financial vocabulary is not enough. +The following deterministic cues must select their named articles even if +keyword ranking would otherwise omit them: + +- Persistent standard financial ledger inserts reached from extension posting + code — `post-ledger-entries-through-posting-codeunits`. +- Persisted general-journal lines posted through a line-codeunit loop or custom + aggregate check, with visible template/document/date balancing context — + `preserve-journal-batch-document-balance`. +- Imported net/tax/gross values mapped into `Gen. Journal Line.Amount`, with + evidence of the VAT posting mode and posting-setup combination — + `normal-vat-journal-amount-includes-vat`. +- Persisted original accounting-value changes, deletion of posted rows, or + fabricated reversal flags/links — `do-not-modify-or-delete-posted-ledger-entries`. +- Settlement/reopening code writing `Open`, closure fields, detailed + application amounts, or unapplication flags — + `apply-ledger-entries-through-application-codeunits`. +- A due-date change persisted on an existing customer/vendor ledger entry — + `change-ledger-due-dates-through-entry-edit`. +- A `Reversal Entry.ReverseTransaction` or `ReverseRegister` argument with + visible ledger-entry, transaction, or register provenance — + `reverse-transactions-by-transaction-number`. +- A complete posting-dimension transfer represented by shortcut/global fields + or a `Dimension Set ID` assignment — + `write-dimensions-as-dimension-set-entries`. +- A dimension/value membership change on `Dimension Set Entry`, reached from + a journal/document/ledger set ID — `do-not-edit-shared-dimension-sets`. + +These are retrieval cues, not findings. Use the selected articles' normative +exceptions to classify standard workflows, temporary records, operational +edits, and extension fields. Finance does not own generic custom-table/master +dimension wiring, number-series API migration, or general AL validation, +locking, transaction, and event-style advice. Do not add those concerns as +Finance agent findings. + +Resolve actual normative conflicts across layers per READ and record suppressed +candidates per DO. Keep every remaining exact path in a stable worklist. +Return `no-knowledge` if no applicable Finance knowledge survives filtering or +configuration; return `completed` with no findings when applicable knowledge +exists but no article matches the admitted changes. + +## Action + +Evaluate every worklist article in full against the changed behavior and its +surrounding control flow. Establish record type, existing versus newly prepared +state, temporariness, fields actually persisted, argument provenance, and the +posting/edit API boundary before emitting a finding. Do not infer a financial +defect from a method name, missing external setup, or unsupported speculation +about callers. + +Use the most specific article for the correction: application-state, due-date, +and shared-dimension findings must not also become generic posted-row findings +for the same change. Emit `major` for a demonstrated financial-correctness +violation and reserve `blocker` for directly evidenced destructive corruption +under DO's severity rules. Applicability alone produces no finding. + +Set `high` confidence only for established source evidence and known matching +context. Domain-scoped agent findings follow DO's precision bar and remain +capped at `minor`/`medium`; do not broaden this pass into other AL domains. +Provide literal `suggested-code` for complete, local, unambiguous fixes. +Otherwise give `suggested-code-omission-reason`, particularly when selecting +the correct posting workflow requires business context. + +Follow DO's acceptance gate and outcome rules. Report `partial` rather than +silently dropping worklist items when a budget is reached, and `failed` for an +unrecoverable retrieval or evaluation error. + +## Output + +Output conforms to the DO findings-report contract. Every finding this skill +emits MUST set `findings[].domain` to `"Finance"`. From 7c17aa3ab8391af88a5121fa5d4ca6eb88ef1cfe Mon Sep 17 00:00:00 2001 From: Jesper Schulz-Wedde Date: Thu, 17 Sep 2026 18:10:21 +0200 Subject: [PATCH 4/4] Fix Finance review applicability and ownership boundaries Remove application-area gating and later VAT-field dependencies, align dynamic shared conventions, separate SCM ownership, and keep journal examples focused on the intended invariant. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/scripts/Test-SkillIndex.ps1 | 4 +- docs/using-bcquality.md | 11 ++-- evaluation/README.md | 4 +- evaluation/review-fixtures.json | 2 +- ...r-entries-through-application-codeunits.md | 4 +- ...nge-ledger-due-dates-through-entry-edit.md | 2 +- .../do-not-edit-shared-dimension-sets.md | 4 +- ...-modify-or-delete-posted-ledger-entries.md | 6 ++- ...mal-vat-journal-amount-includes-vat.bad.al | 2 - ...al-vat-journal-amount-includes-vat.good.al | 2 - .../normal-vat-journal-amount-includes-vat.md | 8 +-- ...edger-entries-through-posting-codeunits.md | 8 +-- ...erve-journal-batch-document-balance.bad.al | 8 +-- ...rve-journal-batch-document-balance.good.al | 4 +- ...preserve-journal-batch-document-balance.md | 2 +- ...erse-transactions-by-transaction-number.md | 2 +- ...ite-dimensions-as-dimension-set-entries.md | 2 +- microsoft/skills/review/al-finance-review.md | 54 ++++++++++++------- 18 files changed, 77 insertions(+), 52 deletions(-) diff --git a/.github/scripts/Test-SkillIndex.ps1 b/.github/scripts/Test-SkillIndex.ps1 index 424532c..e1fd87a 100644 --- a/.github/scripts/Test-SkillIndex.ps1 +++ b/.github/scripts/Test-SkillIndex.ps1 @@ -101,7 +101,7 @@ try { throw "Expected exactly one al-code-review record, found $($review.Count)." } if ((@($review[0].subSkills) -join "`n") -cne ($expectedLeaves -join "`n")) { - throw 'al-code-review subSkills did not preserve the declared 18-leaf order.' + throw "al-code-review subSkills did not preserve the declared $($expectedLeaves.Count)-leaf order." } foreach ($leafPath in $expectedLeaves) { $leaf = @($skills | Where-Object path -ceq $leafPath) @@ -239,4 +239,4 @@ finally { Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue } -Write-Output 'Skill-index check PASSED: deterministic, schema-valid, and all 18 review leaves preserved in order.' +Write-Output "Skill-index check PASSED: deterministic, schema-valid, and all $($expectedLeaves.Count) review leaves preserved in order." diff --git a/docs/using-bcquality.md b/docs/using-bcquality.md index 3186fe7..eae80fb 100644 --- a/docs/using-bcquality.md +++ b/docs/using-bcquality.md @@ -183,7 +183,7 @@ using your normal compilation, analyzer, test, and human-review workflow. ## Coverage and limits -The Microsoft broad review composes the 18 Microsoft domains listed below. +The Microsoft broad review composes the Microsoft domains listed below. The Community Agents review is a separate skill selected by the request, not a nested part of that coordinator. All current review leaves accept app folders, files, and diffs; request an Agent SDK review explicitly when that @@ -193,13 +193,16 @@ Available knowledge is **not** a promise that every rule will run. Selection depends on the task, target context, enabled layers, and source evidence. A whole-folder review is a current-state snapshot: detecting a published API removal or another comparison-only regression requires an actual baseline. -The corpus includes technical AL guidance and targeted Finance posting +The corpus combines technical AL guidance with targeted functional-domain invariants, not exhaustive functional validation or AppSource certification. The Finance leaf reviews journal posting, financial ledger changes, applications, and posting-linked dimension handling. It prunes unrelated code -at the leaf rather than changing broad-review orchestration. Its dimension -guidance can apply across application areas, but it does not own generic +at the leaf rather than changing broad-review orchestration. Finance articles +use `application-area: [all]` so missing application-area context does not +weaken applicable findings; resolved records and operations supply the +narrowing. Finance owns financial ledgers, not Item, Value, Capacity, Warehouse, +or inventory-application records owned by SCM. It also does not own generic custom-table or master Default Dimension wiring. Request a focused "Finance posting review" when only this domain is needed. diff --git a/evaluation/README.md b/evaluation/README.md index f8c1877..f460b08 100644 --- a/evaluation/README.md +++ b/evaluation/README.md @@ -8,7 +8,9 @@ Model-facing preparation hashes case IDs, neutralizes `Good`/`Bad` object-name t The Finance override deliberately covers every paired Finance article, not only the first filename. Its shared context supplies the target version and -application area, not expected answers. Scenario prerequisites live in +localization but deliberately omits application area, as production callers +often do. Finance applicability must come from its source-surface gate, not +an artificial evaluation-only area hint. Scenario prerequisites live in executable AL: the document-balance cases check the template setting, and the VAT cases encode the imported net/VAT/gross totals and applicable VAT mode. Do not move these prerequisites into comments that preparation removes. diff --git a/evaluation/review-fixtures.json b/evaluation/review-fixtures.json index 93c0054..f395139 100644 --- a/evaluation/review-fixtures.json +++ b/evaluation/review-fixtures.json @@ -25,7 +25,7 @@ "reverse-transactions-by-transaction-number", "write-dimensions-as-dimension-set-entries" ], - "context": "Target Business Central 28, worldwide standard application (w1), application-area finance. Review each file as an independent source input." + "context": "Target Business Central 28, worldwide standard application (w1). Review each file as an independent source input." }, "interfaces": { "article": "set-defaultimplementation-on-enum" diff --git a/microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.md b/microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.md index dfcc874..11f78cd 100644 --- a/microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.md +++ b/microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.md @@ -4,7 +4,7 @@ domain: finance keywords: [cust-ledger-entry, vendor-ledger-entry, detailed-ledger-entry, remaining-amount, application, unapplication, open, closed-by-entry-no] technologies: [al] countries: [w1] -application-area: [finance] +application-area: [all] --- # Apply and unapply entries through the application workflow, not status flags @@ -23,7 +23,7 @@ See sample: [`apply-ledger-entries-through-application-codeunits.good.al`](apply ## Anti Pattern -Implement payment matching, settlement, or reopening by directly persisting `Open`, `"Closed by Entry No."`, closure amounts/dates, or detailed-entry unapplication flags, or by deleting/rewriting detailed application amounts. Require confirmed writes to existing non-temporary records and settlement intent. Do not suggest assigning a `Remaining Amount` FlowField as a fix. +Implement customer/vendor payment matching, settlement, or reopening by directly persisting `Open`, `"Closed by Entry No."`, closure amounts/dates, or detailed-entry unapplication flags, or by deleting/rewriting detailed application amounts. Require confirmed writes to existing non-temporary customer/vendor or detailed customer/vendor entries and settlement intent. Item/inventory application records belong to SCM, not this rule. Do not suggest assigning a `Remaining Amount` FlowField as a fix. This article owns fabricated application state. Use the [posted-financial-content rule](do-not-modify-or-delete-posted-ledger-entries.md) for original accounting-value corrections, not a second finding prescribing the same application fix. diff --git a/microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.md b/microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.md index 01fc1ee..aca7b76 100644 --- a/microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.md +++ b/microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.md @@ -4,7 +4,7 @@ domain: finance keywords: [due-date, initial-entry-due-date, cust-entry-edit, vend-entry-edit, detailed-ledger-entry, aging] technologies: [al] countries: [w1] -application-area: [finance] +application-area: [all] --- # Change posted customer/vendor due dates through the entry-edit workflow diff --git a/microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.md b/microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.md index 38182c3..97b29f2 100644 --- a/microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.md +++ b/microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.md @@ -17,13 +17,13 @@ The same `Dimension Set ID` can be referenced by an unposted journal line and by To change an unposted transaction's dimensions, load its set into a **temporary** `Dimension Set Entry` buffer with `DimensionManagement.GetDimensionSet`, change the buffer, and obtain a reusable ID with `GetDimensionSetID`. Validate dimension values in the buffer so `Dimension Value ID` matches the chosen value. Store the resulting ID on the transaction and synchronize its projections through that record's supported dimension validation. -For already-posted G/L dimensions, use the supported dimension-correction workflow rather than changing shared rows. Read-only access, temporary buffers, and standard maintenance of projection metadata such as `Global Dimension No.` are not membership changes. This rule protects shared posting history; it does not prescribe custom-table/default-dimension wiring. +For already-posted G/L dimensions, use the supported dimension-correction workflow rather than changing shared rows. Read-only access, temporary buffers, and standard maintenance of projection metadata such as `Global Dimension No.` are not membership changes. This rule protects dimension sets reached from general-journal, financial-document, or Finance-ledger flows. It does not own Item, Value, Capacity, Warehouse, or inventory-application record writes, or prescribe custom-table/default-dimension wiring. See sample: [`do-not-edit-shared-dimension-sets.good.al`](do-not-edit-shared-dimension-sets.good.al). ## Anti Pattern -Follow a journal/document/ledger `Dimension Set ID` to a **persistent** `Dimension Set Entry` and modify, rename, or delete its dimension/value membership in order to change that one transaction. Inspect `IsTemporary` guards, aliases, and the fields written before reporting: the same operations on a temporary working copy are expected. +Follow a general-journal, financial-document, or Finance-ledger `Dimension Set ID` to a **persistent** `Dimension Set Entry` and modify, rename, or delete its dimension/value membership in order to change that one transaction. Inspect `IsTemporary` guards, aliases, and the fields written before reporting: the same operations on a temporary working copy are expected. See sample: [`do-not-edit-shared-dimension-sets.bad.al`](do-not-edit-shared-dimension-sets.bad.al). diff --git a/microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md b/microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md index eca3a5e..33a8029 100644 --- a/microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md +++ b/microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md @@ -4,7 +4,7 @@ domain: finance keywords: [g-l-entry, ledger-entry, reversal, audit-trail, correction, financial-content, entry-edit] technologies: [al] countries: [w1] -application-area: [finance] +application-area: [all] --- # Correct posted financial content through posting workflows, not row surgery @@ -19,11 +19,13 @@ Use a supported transaction/register reversal, credit memo, or correcting journa Supported operational edits are deliberate exceptions: for example, `"G/L Entry-Edit"` supports description changes, and `"Cust. Entry-Edit"` / `"Vend. Entry-Edit"` handle their table-specific editable fields. [Due-date synchronization](change-ledger-due-dates-through-entry-edit.md), [application/unapplication](apply-ledger-entries-through-application-codeunits.md), G/L dimension correction, and supported date compression have their own workflows. Do not flag their standard implementations, temporary simulation buffers, or extension-only metadata updates as financial row surgery. A subscriber is not exempt merely because it runs inside a supported workflow: inspect the fields it actually changes. +This rule covers G/L, customer/vendor/detailed, VAT, and financial-posting/register records. Item, Value, Capacity, Warehouse, inventory-application, and other inventory-posting records are SCM concerns. The financial-row leg of one inventory-posting bypass is outside this rule when the same inventory correction resolves it; an independently actionable financial defect remains in scope regardless of the containing module's name. + See sample: [`do-not-modify-or-delete-posted-ledger-entries.good.al`](do-not-modify-or-delete-posted-ledger-entries.good.al). ## Anti Pattern -Persist a change to original financial content, delete posted rows, or fabricate reversal flags/links to repair or undo a transaction outside the supported correction/maintenance workflow. Require an existing, non-temporary standard ledger record and evidence of the fields or rows affected; a `Modify` token alone is insufficient. Settlement-state writes belong to the application article rather than a duplicate finding here. +Persist a change to original financial content, delete posted rows, or fabricate reversal flags/links to repair or undo a transaction outside the supported correction/maintenance workflow. Require an existing, non-temporary Finance-owned record and evidence of the fields or rows affected; a `Modify` token or `*Ledger Entry` name alone is insufficient. Settlement-state writes belong to the application article rather than a duplicate finding here. See sample: [`do-not-modify-or-delete-posted-ledger-entries.bad.al`](do-not-modify-or-delete-posted-ledger-entries.bad.al). diff --git a/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.bad.al b/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.bad.al index e86a246..50ac8ff 100644 --- a/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.bad.al +++ b/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.bad.al @@ -35,13 +35,11 @@ codeunit 50108 "Import Purchase Journal Total" JournalLine.TestField("VAT Calculation Type", JournalLine."VAT Calculation Type"::"Normal VAT"); JournalLine.TestField("VAT %", 25); JournalLine.TestField("VAT Difference", 0); - JournalLine.TestField("Non-Deductible VAT %", 0); JournalLine.TestField("Bal. Gen. Posting Type", JournalLine."Bal. Gen. Posting Type"::" "); JournalLine.TestField("Bal. VAT %", 0); VATPostingSetup.Get(JournalLine."VAT Bus. Posting Group", JournalLine."VAT Prod. Posting Group"); VATPostingSetup.TestField("VAT Calculation Type", VATPostingSetup."VAT Calculation Type"::"Normal VAT"); VATPostingSetup.TestField("VAT %", 25); - VATPostingSetup.TestField("Non-Deductible VAT %", 0); VATPostingSetup.TestField("Unrealized VAT Type", VATPostingSetup."Unrealized VAT Type"::" "); GeneralLedgerSetup.Get(); GeneralLedgerSetup.TestField("Additional Reporting Currency", ''); diff --git a/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.good.al b/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.good.al index 4a54dba..961e234 100644 --- a/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.good.al +++ b/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.good.al @@ -35,13 +35,11 @@ codeunit 50108 "Import Purchase Journal Total" JournalLine.TestField("VAT Calculation Type", JournalLine."VAT Calculation Type"::"Normal VAT"); JournalLine.TestField("VAT %", 25); JournalLine.TestField("VAT Difference", 0); - JournalLine.TestField("Non-Deductible VAT %", 0); JournalLine.TestField("Bal. Gen. Posting Type", JournalLine."Bal. Gen. Posting Type"::" "); JournalLine.TestField("Bal. VAT %", 0); VATPostingSetup.Get(JournalLine."VAT Bus. Posting Group", JournalLine."VAT Prod. Posting Group"); VATPostingSetup.TestField("VAT Calculation Type", VATPostingSetup."VAT Calculation Type"::"Normal VAT"); VATPostingSetup.TestField("VAT %", 25); - VATPostingSetup.TestField("Non-Deductible VAT %", 0); VATPostingSetup.TestField("Unrealized VAT Type", VATPostingSetup."Unrealized VAT Type"::" "); GeneralLedgerSetup.Get(); GeneralLedgerSetup.TestField("Additional Reporting Currency", ''); diff --git a/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.md b/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.md index 80e3dec..6dda9ef 100644 --- a/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.md +++ b/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.md @@ -4,20 +4,22 @@ domain: finance keywords: [normal-vat, automatic-vat-entry, gross-amount, net-amount, vat-posting-setup, gen-journal-line, purchase] technologies: [al] countries: [w1] -application-area: [finance] +application-area: [all] --- # Supply a VAT-inclusive journal Amount for automatic Normal VAT ## Description -For a general-journal line using **Automatic VAT Entry** and **Normal VAT**, `Amount` includes VAT. The posting engine extracts tax from that total; it does not add tax to a VAT-exclusive expense imported into `Amount`. For an LCY purchase of net 100 plus 25 VAT, entering 100 produces an 80 expense and 20 VAT, rather than the intended 100 expense and 25 VAT from a total of 125. +For a general-journal line using **Automatic VAT Entry** and **Normal VAT**, `Amount` includes VAT. The posting engine extracts tax from that total; it does not add tax to a VAT-exclusive expense imported into `Amount`. An LCY invoice with net 100 and VAT 25 therefore needs a journal total of 125, not 100. ## Best Practice Map the source's VAT-inclusive total to journal `Amount` in this posting mode. Establish the intended account and posting-group combination before validating the final amount. Account-derived VAT defaults depend on `Copy VAT Setup to Jnl. Lines`; do not assume account selection always supplies the intended configuration. -Require the actual input contract and calculation mode, not just a variable named `NetAmount`. The examples encode source net, VAT, and gross values plus a 25% Normal-VAT setup check. They target an LCY G/L purchase with 0.01 amount rounding and without balancing-side VAT, additional reporting currency, VAT differences, unrealized VAT, or non-deductible VAT. Other calculation types, Manual VAT Entry, reverse charge, Full VAT, sales/use tax, non-deductible or unrealized tax, and other currency/rounding contexts need their own analysis; this is not a universal gross-up formula or country-specific tax advice. +Require the actual input contract and calculation mode, not just a variable named `NetAmount`. The examples encode source net, VAT, and gross values plus a 25% Normal-VAT setup check. They target an LCY G/L purchase with 0.01 amount rounding and without balancing-side VAT, additional reporting currency, VAT differences, or unrealized VAT. Other calculation types, Manual VAT Entry, reverse charge, Full VAT, sales/use tax, unrealized tax, and other currency/rounding contexts need their own analysis; this is not a universal gross-up formula or country-specific tax advice. + +The concern is the supplied transaction total, not its deductible/non-deductible allocation. Non-deductible VAT features can change the allocation of that total, not turn the source's net amount into its gross amount. Do not infer a particular expense or deductible-VAT split from this rule. The samples therefore do not depend on later-version non-deductible-VAT fields. See sample: [`normal-vat-journal-amount-includes-vat.good.al`](normal-vat-journal-amount-includes-vat.good.al). diff --git a/microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.md b/microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.md index e24856b..aefb6bb 100644 --- a/microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.md +++ b/microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.md @@ -4,7 +4,7 @@ domain: finance keywords: [g-l-entry, ledger-entry, gen-jnl-post-line, gen-jnl-post-batch, journal-line, register, insert] technologies: [al] countries: [w1] -application-area: [finance] +application-area: [all] --- # Create financial ledger entries through the owning posting engine @@ -19,13 +19,15 @@ Use the owning document or journal posting workflow. For a normal persisted gene `"Gen. Jnl.-Post Line".RunWithCheck` is appropriate for a complete journal line inside a correctly owned posting lifecycle, but it does not invent a balancing account or document number, allocate numbering merely from `Posting No. Series`, or replace [batch document-balancing policy](preserve-journal-batch-document-balance.md). The line codeunit is stateful; its checked wrapper owns its start/continue/finish work. Normal batch posting owns its numbering and commits by default; do not imply these entry points are transaction-neutral. -Exclude temporary buffers and the standard engine's own insertion points. A checked parent may legitimately use `RunWithoutCheck`; do not replace it without inspecting that parent. This rule concerns standard financial ledgers, not a custom table merely named `Ledger Entry` or a supported, specifically reviewed migration/repair workflow. +Exclude temporary buffers and the standard engine's own insertion points. A checked parent may legitimately use `RunWithoutCheck`; do not replace it without inspecting that parent. This rule owns `G/L Entry`, `Cust. Ledger Entry`, `Vendor Ledger Entry`, their detailed customer/vendor entries, `VAT Entry`, and financial-posting/register records, not a custom table merely named `Ledger Entry` or a supported, specifically reviewed migration/repair workflow. + +`Item Ledger Entry`, `Value Entry`, Capacity/Warehouse entries, `Item Application Entry`, and other inventory-posting records are SCM concerns, not this rule's financial-ledger scope. That exclusion includes the financial-row leg of a single inventory-posting bypass when restoring the inventory workflow corrects the whole operation. Distinct, independently actionable financial defects remain in scope. See sample: [`post-ledger-entries-through-posting-codeunits.good.al`](post-ledger-entries-through-posting-codeunits.good.al). ## Anti Pattern -Create posted financial effects by directly inserting persistent `G/L Entry`, customer/vendor ledger, or VAT ledger rows outside their owning posting workflow. Resolve the actual record type and lifecycle. Balanced debit/credit values, copied dimensions, `Insert(true)`, and a lock around entry-number allocation do not turn raw inserts into a complete posting. +Create posted financial effects by directly inserting the Finance-owned records named above outside their owning posting workflow. Resolve the actual record type, operation, and lifecycle; do not match `*Ledger Entry` as a wildcard. Balanced debit/credit values, copied dimensions, `Insert(true)`, and a lock around entry-number allocation do not turn raw inserts into a complete posting. See sample: [`post-ledger-entries-through-posting-codeunits.bad.al`](post-ledger-entries-through-posting-codeunits.bad.al). diff --git a/microsoft/knowledge/finance/preserve-journal-batch-document-balance.bad.al b/microsoft/knowledge/finance/preserve-journal-batch-document-balance.bad.al index 40d4d43..1d5b5b3 100644 --- a/microsoft/knowledge/finance/preserve-journal-batch-document-balance.bad.al +++ b/microsoft/knowledge/finance/preserve-journal-batch-document-balance.bad.al @@ -6,6 +6,7 @@ codeunit 50107 "Post Journal Allocation" JournalTemplate: Record "Gen. Journal Template"; JournalBatch: Record "Gen. Journal Batch"; JournalLine: Record "Gen. Journal Line"; + LineToPost: Record "Gen. Journal Line"; PostLine: Codeunit "Gen. Jnl.-Post Line"; begin JournalTemplate.Get(TemplateName); @@ -24,11 +25,12 @@ codeunit 50107 "Post Journal Allocation" AddAllocationLine(JournalTemplate, BatchName, 20000, CreditAccount, PostingDate, 'ALLOC-B', -90); JournalLine.FindSet(); repeat - PostLine.RunWithCheck(JournalLine); + LineToPost := JournalLine; + PostLine.RunWithCheck(LineToPost); until JournalLine.Next() = 0; end; - local procedure AddAllocationLine(JournalTemplate: Record "Gen. Journal Template"; BatchName: Code[10]; LineNo: Integer; AccountNo: Code[20]; PostingDate: Date; DocumentNo: Code[20]; Amount: Decimal) + local procedure AddAllocationLine(JournalTemplate: Record "Gen. Journal Template"; BatchName: Code[10]; LineNo: Integer; AccountNo: Code[20]; PostingDate: Date; DocumentNo: Code[20]; LineAmount: Decimal) var JournalLine: Record "Gen. Journal Line"; begin @@ -42,7 +44,7 @@ codeunit 50107 "Post Journal Allocation" JournalLine.Validate("Account Type", JournalLine."Account Type"::"G/L Account"); JournalLine.Validate("Account No.", AccountNo); JournalLine.Validate("Gen. Posting Type", JournalLine."Gen. Posting Type"::" "); - JournalLine.Validate(Amount, Amount); + JournalLine.Validate(Amount, LineAmount); JournalLine.Insert(true); end; diff --git a/microsoft/knowledge/finance/preserve-journal-batch-document-balance.good.al b/microsoft/knowledge/finance/preserve-journal-batch-document-balance.good.al index 040269c..beff7f4 100644 --- a/microsoft/knowledge/finance/preserve-journal-batch-document-balance.good.al +++ b/microsoft/knowledge/finance/preserve-journal-batch-document-balance.good.al @@ -26,7 +26,7 @@ codeunit 50107 "Post Journal Allocation" PostBatch.Run(JournalLine); end; - local procedure AddAllocationLine(JournalTemplate: Record "Gen. Journal Template"; BatchName: Code[10]; LineNo: Integer; AccountNo: Code[20]; PostingDate: Date; DocumentNo: Code[20]; Amount: Decimal) + local procedure AddAllocationLine(JournalTemplate: Record "Gen. Journal Template"; BatchName: Code[10]; LineNo: Integer; AccountNo: Code[20]; PostingDate: Date; DocumentNo: Code[20]; LineAmount: Decimal) var JournalLine: Record "Gen. Journal Line"; begin @@ -40,7 +40,7 @@ codeunit 50107 "Post Journal Allocation" JournalLine.Validate("Account Type", JournalLine."Account Type"::"G/L Account"); JournalLine.Validate("Account No.", AccountNo); JournalLine.Validate("Gen. Posting Type", JournalLine."Gen. Posting Type"::" "); - JournalLine.Validate(Amount, Amount); + JournalLine.Validate(Amount, LineAmount); JournalLine.Insert(true); end; diff --git a/microsoft/knowledge/finance/preserve-journal-batch-document-balance.md b/microsoft/knowledge/finance/preserve-journal-batch-document-balance.md index 27751f9..b3079ac 100644 --- a/microsoft/knowledge/finance/preserve-journal-batch-document-balance.md +++ b/microsoft/knowledge/finance/preserve-journal-batch-document-balance.md @@ -4,7 +4,7 @@ domain: finance keywords: [force-doc-balance, gen-journal-template, gen-jnl-post-batch, runwithcheck, document-no, posting-date, balancing] technologies: [al] countries: [w1] -application-area: [finance] +application-area: [all] --- # Preserve the journal batch's document-balancing policy diff --git a/microsoft/knowledge/finance/reverse-transactions-by-transaction-number.md b/microsoft/knowledge/finance/reverse-transactions-by-transaction-number.md index d61f3a2..296d902 100644 --- a/microsoft/knowledge/finance/reverse-transactions-by-transaction-number.md +++ b/microsoft/knowledge/finance/reverse-transactions-by-transaction-number.md @@ -4,7 +4,7 @@ domain: finance keywords: [reversetransaction, reverseregister, transaction-no, entry-no, reversal-entry, g-l-register] technologies: [al] countries: [w1] -application-area: [finance] +application-area: [all] --- # Pass the transaction number, not a ledger-entry number, to ReverseTransaction diff --git a/microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.md b/microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.md index 7286b6b..0764f20 100644 --- a/microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.md +++ b/microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.md @@ -17,7 +17,7 @@ When a journal line or posting document inherits dimensions, its `Dimension Set For an intentional **complete** dimension transfer, copy the source set ID and synchronize the destination's projections through its supported validation or dimension-management routine. On `Gen. Journal Line`, `Validate("Dimension Set ID", SourceSetID)` updates the two shortcut fields. Do not assume another table has the same validation trigger. -When line-specific dimensions must survive a header change, use the appropriate set-combination or delta routine instead of blindly replacing the line's entire set. Reading or filtering a known global dimension is legitimate; it is not a claim to enumerate every dimension. This rule owns dimension transfer through existing journal/document posting records, not custom-table or master `Default Dimension` wiring, which belongs to data modeling. +When line-specific dimensions must survive a header change, use the appropriate set-combination or delta routine instead of blindly replacing the line's entire set. Reading or filtering a known global dimension is legitimate; it is not a claim to enumerate every dimension. This rule owns transfers through general-journal and financial-document posting records, not writes to Item, Value, Capacity, Warehouse, or inventory-application records owned by SCM. Generic custom-table or master `Default Dimension` wiring belongs to data modeling. See sample: [`write-dimensions-as-dimension-set-entries.good.al`](write-dimensions-as-dimension-set-entries.good.al). diff --git a/microsoft/skills/review/al-finance-review.md b/microsoft/skills/review/al-finance-review.md index 228086d..06090b1 100644 --- a/microsoft/skills/review/al-finance-review.md +++ b/microsoft/skills/review/al-finance-review.md @@ -15,12 +15,13 @@ application-area: [all] # AL Finance review Reviews the `finance` knowledge domain. This is a leaf action skill composed by -`al-code-review`; it invokes no other skills. Application-area discovery is -broad enough for posting-linked dimensions, but source relevance is narrow. +`al-code-review`; it invokes no other skills. Application-area metadata does +not gate Finance coverage; resolved source records and operations do. ## Source -Use READ's **Bounded retrieval for review skills** workflow with +Apply the source-surface gate in Relevance before retrieving knowledge. +If it passes, use READ's **Bounded retrieval for review skills** workflow with `-Domain finance`. Consume every catalog page across enabled layers, preserving each exact path and applicability metadata. Do not select a top-k catalog or deduplicate by basename. Open complete bodies only for exact Worklist paths, @@ -41,10 +42,20 @@ changed executable behavior involving at least one of these surfaces: - General-journal construction or posting, journal-batch processing, or an event subscriber whose resolved publisher is in the financial posting path. -- Writes or correction/application/reversal calls involving standard financial - ledger records, detailed customer/vendor entries, VAT entries, or G/L registers. +- Writes or correction/application/reversal calls involving `G/L Entry`, + `Cust. Ledger Entry`, `Vendor Ledger Entry`, `Detailed Cust. Ledg. Entry`, + `Detailed Vendor Ledg. Entry`, `VAT Entry`, or financial-posting/G/L-register + records. - Dimension transfer or dimension-set mutation connected by visible data flow - to an existing journal, posting document, or financial ledger record. + to an existing general journal, financial posting document, or Finance-owned + ledger record. + +Exclude `Item Ledger Entry`, `Value Entry`, Capacity/Warehouse entries, +`Item Application Entry`, and other inventory-posting records owned by SCM. +Do not adopt their findings when the SCM skill is absent or disabled. For one +inventory-originated posting bypass, equivalent findings have one SCM primary +owner; distinct independent financial defects remain Finance. Classify the +operation and actual record, not an inventory/finance word in a module name. Return `not-applicable` when none is present. Imports, object names, comments, read-only ledger displays, generic `Amount`/`Date`/`Open` fields, and calls to @@ -61,7 +72,7 @@ maps to the changed behavior; generic financial vocabulary is not enough. The following deterministic cues must select their named articles even if keyword ranking would otherwise omit them: -- Persistent standard financial ledger inserts reached from extension posting +- Persistent Finance-owned ledger inserts reached from extension posting code — `post-ledger-entries-through-posting-codeunits`. - Persisted general-journal lines posted through a line-codeunit loop or custom aggregate check, with visible template/document/date balancing context — @@ -69,28 +80,30 @@ keyword ranking would otherwise omit them: - Imported net/tax/gross values mapped into `Gen. Journal Line.Amount`, with evidence of the VAT posting mode and posting-setup combination — `normal-vat-journal-amount-includes-vat`. -- Persisted original accounting-value changes, deletion of posted rows, or +- Persisted original Finance accounting-value changes, deletion of Finance rows, or fabricated reversal flags/links — `do-not-modify-or-delete-posted-ledger-entries`. -- Settlement/reopening code writing `Open`, closure fields, detailed - application amounts, or unapplication flags — +- Customer/vendor settlement/reopening code writing `Open`, closure fields, + detailed customer/vendor application amounts, or unapplication flags — `apply-ledger-entries-through-application-codeunits`. - A due-date change persisted on an existing customer/vendor ledger entry — `change-ledger-due-dates-through-entry-edit`. - A `Reversal Entry.ReverseTransaction` or `ReverseRegister` argument with visible ledger-entry, transaction, or register provenance — `reverse-transactions-by-transaction-number`. -- A complete posting-dimension transfer represented by shortcut/global fields - or a `Dimension Set ID` assignment — +- A complete Finance posting-dimension transfer represented by shortcut/global + fields or a `Dimension Set ID` assignment — `write-dimensions-as-dimension-set-entries`. - A dimension/value membership change on `Dimension Set Entry`, reached from - a journal/document/ledger set ID — `do-not-edit-shared-dimension-sets`. + a general-journal/financial-document/Finance-ledger set ID — + `do-not-edit-shared-dimension-sets`. These are retrieval cues, not findings. Use the selected articles' normative -exceptions to classify standard workflows, temporary records, operational -edits, and extension fields. Finance does not own generic custom-table/master -dimension wiring, number-series API migration, or general AL validation, -locking, transaction, and event-style advice. Do not add those concerns as -Finance agent findings. +exceptions and ownership boundaries to classify standard workflows, temporary +records, operational edits, and extension fields. Do not select a Finance +ledger rule from `*Ledger Entry` or `Insert`/`Modify` alone. Finance does not +own SCM records, generic custom-table/master dimension wiring, number-series +API migration, or general AL validation, locking, transaction, and event-style +advice. Do not add those concerns as Finance agent findings. Resolve actual normative conflicts across layers per READ and record suppressed candidates per DO. Keep every remaining exact path in a stable worklist. @@ -109,7 +122,10 @@ about callers. Use the most specific article for the correction: application-state, due-date, and shared-dimension findings must not also become generic posted-row findings -for the same change. Emit `major` for a demonstrated financial-correctness +for the same change. Do not emit an equivalent Finance finding for the +financial-row leg of one SCM-owned inventory posting bypass; evaluate a +distinct financial defect only when its corrective action is independent. +Emit `major` for a demonstrated financial-correctness violation and reserve `blocker` for directly evidenced destructive corruption under DO's severity rules. Applicability alone produces no finding.