Bugs/concurrent posting for general ledger gl entry#9587
Conversation
…https://github.com/microsoft/BCApps into bugs/Concurrent-Posting-for-General-Ledger-GL-Entry
| GeneralLedgerSetup.Validate("Currency Symbol Position", GeneralLedgerSetup."Currency Symbol Position"::"Before Amount"); | ||
| GeneralLedgerSetup.Validate("Enable Data Check", DataCheck); | ||
| GeneralLedgerSetup.Validate("Acc. Receivables Category", AccReceivablesCategory); | ||
| GeneralledgerSetup."Use Concurrent Posting" := true; // to test concurrent posting for all countries |
There was a problem hiding this comment.
CreateGeneralLedgerSetup.Codeunit.al unconditionally forces "Use Concurrent Posting" := true for every country's demo-data company, with a comment reading "to test concurrent posting for all countries".
This reads as leftover diagnostic/test code rather than an intentional demo-data default: it silently changes the posting behavior of the standard Contoso demo company for every locale, with no gating on a test/debug context, and the comment itself signals it was added for local verification. (Separately, the statement references the variable via inconsistent casing, "GeneralledgerSetup" instead of "GeneralLedgerSetup" — this compiles because AL identifiers are case-insensitive, but it is inconsistent with the casing used on every other line in the same procedure.) If enabling concurrent posting in the demo dataset is intentional, the hardcoded value should be reviewed and the debug-style comment removed; if it was left in from local testing, it should be reverted before merge.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| field(21; "SIFT Bucket No."; Integer) | ||
| { | ||
| Caption = 'SIFT Bucket No.'; | ||
| ToolTip = 'Specifies an automatically generated number that is used by the system to enable better concurrency.'; | ||
| Editable = false; | ||
| } |
There was a problem hiding this comment.
New field 21 "SIFT Bucket No." on table 17 "G/L Entry" has no DataClassification property.
Its immediate neighbor, field 20 "Source Currency Code", explicitly sets DataClassification = SystemMetadata, but the new field omits the property entirely, so the AL compiler applies the ToBeClassified sentinel. This field is a purely system-generated technical value (the OnInsert trigger sets it to "G/L Register No." mod 5) and should be classified the same way as other technical fields on this table.
| field(21; "SIFT Bucket No."; Integer) | |
| { | |
| Caption = 'SIFT Bucket No.'; | |
| ToolTip = 'Specifies an automatically generated number that is used by the system to enable better concurrency.'; | |
| Editable = false; | |
| } | |
| field(21; "SIFT Bucket No."; Integer) | |
| { | |
| Caption = 'SIFT Bucket No.'; | |
| ToolTip = 'Specifies an automatically generated number that is used by the system to enable better concurrency.'; | |
| Editable = false; | |
| DataClassification = SystemMetadata; | |
| } |
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
Copilot PR ReviewIteration 5 · Outcome: completed Knowledge source: https://github.com/microsoft/BCQuality@186d8a131465475c79244d994acb872cd5c0d4bf Findings by domainFindings split into Knowledge-backed (cite a BCQuality article) and Agent (the agent's own judgement, no matching BCQuality rule).
Totals: 1 knowledge-backed · 1 agent findings. Orchestrator pre-filter (2 file(s) excluded)
Findings produced by the AL review agent v1.7.3. Reply 👎 on any inline comment to flag false positives. |
|
|
||
| trigger OnInsert() | ||
| begin | ||
| Rec."SIFT Bucket No." := Rec."G/L Register No." mod 5; |
There was a problem hiding this comment.
The new "SIFT Bucket No." field is computed as 'Rec."G/L Register No." mod 5' in OnInsert and is appended to Key2 (the SumIndexFields key used for G/L account/posting-date aggregation) specifically to reduce SIFT-index write contention.
Every G/L entry created within the same posting run shares the same G/L Register No., so every entry from a single large batch (mass recurring-journal posting, a big sales/purchase batch, etc.) lands in exactly the same bucket — the scenario most likely to generate heavy SIFT-index write contention in the first place gets no distribution benefit at all; only different, non-overlapping posting runs land in different buckets. The bucket key would more directly serve the stated 'better concurrency' goal if it were derived from something that varies within a single run (e.g. the G/L Entry's own Entry No.) rather than from the Register No. that is constant for the whole run. The underlying impact could be more than cosmetic for high-volume batch posting, but this is capped at minor since no BCQuality knowledge file specifically addresses SIFT bucket-key selection strategy today.
| Rec."SIFT Bucket No." := Rec."G/L Register No." mod 5; | |
| Rec."SIFT Bucket No." := Rec."Entry No." mod 5; |
Agent judgement — not directly backed by a BCQuality knowledge article.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
This PR replaces the LockTable()+GetLastEntry()/FindLast() pattern for G/L Entry and VAT Entry numbering with sequence-based GetNextEntryNo() calls whenever GeneralLedgerSetup.UseConcurrentPosting() is true, specifically to remove the serialization point that LockTable causes under concurrent posting.However, StartPosting still unconditionally does 'GLReg.LockTable(); if GLReg.FindLast() then GLReg."No." := GLReg."No." + 1 else GLReg."No." := 1;' to assign the next G/L Register number, in the very same procedure that was edited to branch on UseConcurrentPosting for VAT Entry numbering a few lines above. Because every posting run must go through this table lock to obtain its register number, all concurrent postings still serialize on the G/L Register table regardless of the new setting, which defeats the concurrency goal the rest of the change is trying to achieve for G/L Entry and VAT Entry. The real impact here would normally be major (it undermines the feature's stated purpose), but per agent-finding rules this is capped at minor since no BCQuality knowledge file backs it yet; the concern should be promoted to a knowledge-backed rule (or the register numbering path should be given the same sequence-based treatment) rather than accepted at this severity long-term. Agent judgement — not directly backed by a BCQuality knowledge article. Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why |
| "VAT Reporting Date" := GenJnlLine."VAT Reporting Date"; | ||
| end; | ||
|
|
||
| [InherentPermissions(PermissionObjectType::TableData, Database::"VAT Entry", 'r')] |
There was a problem hiding this comment.
The new VAT Entry.GetNextEntryNo() procedure is decorated with [InherentPermissions(PermissionObjectType::TableData, Database::"VAT Entry", 'r')], copied from the pre-existing GetLastEntryNo() immediately below it.
GetLastEntryNo() actually reads the VAT Entry table (via FindRecordManagement.GetLastEntryIntFieldValue) so the grant matches its body, but GetNextEntryNo() only calls SequenceNoMgt.GetNextSeqNo(DATABASE::"VAT Entry") — it never Gets, Finds, or otherwise reads a VAT Entry row. The inherent-permissions grant should match the procedure's actual work; here it grants table-data read access on VAT Entry that the procedure's body never exercises.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
This PR adds several new conditional code paths gated on GeneralLedgerSetup.UseConcurrentPosting() — sequence-based G/L Entry/VAT Entry numbering in InitNextEntryNo/StartPosting/ContinuePosting, the new GetNextGLEntryNoForPosting/GetNextVATEntryNoForPosting helpers, the PreviousEntryNo rollback path in UpdateGLEntryNo, and the new ValidateSequenceNo call for VAT Entry — none of which are exercised by any test file in this diff (no *.Test.al / *.Codeunit.al test file appears in the changed-file list).Given the change affects core posting entry-numbering logic across every localization layer, the absence of any accompanying automated test for the new UseConcurrentPosting = true branch is a real coverage gap for a change this central to posting correctness. Impact would ordinarily warrant more than a minor flag, but this is capped per agent-finding rules since no BCQuality knowledge file backs the specific gap. Agent judgement — not directly backed by a BCQuality knowledge article. Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why |
| end; | ||
|
|
||
| [InherentPermissions(PermissionObjectType::TableData, Database::"G/L Register", 'r')] | ||
| procedure GetNextEntryNo(): Integer |
There was a problem hiding this comment.
table 45 "G/L Register" now declares two procedures named GetNextRegisterNo in the same object: GetNextRegisterNo(UseLegacyPosting: Boolean): Integer (line 178) and GetNextRegisterNo(): Integer (line 187, the renamed former GetNextEntryNo).
AL does not support procedure overloading by parameter list, so this object will fail to compile as written; the boolean-taking procedure's own call to GetNextRegisterNo() on line 181 is also now ambiguous/self-referential with the wrong target. The real impact of this defect is build-breaking (blocker-level), not merely a style nit; severity is capped here only because no curated knowledge rule backs it directly, and it should be promoted to a compile-time check before it can gate.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| end; | ||
|
|
||
| [InherentPermissions(PermissionObjectType::TableData, Database::"G/L Register", 'r')] | ||
| procedure GetNextEntryNo(): Integer |
There was a problem hiding this comment.
The published procedure GetNextEntryNo() on table 45 "G/L Register" was renamed to GetNextRegisterNo() in place, with no [Obsolete] marker left behind.
Any dependent extension calling GLRegister.GetNextEntryNo() will fail to compile the moment it picks up this change, with no deprecation warning window. The in-repo caller in GenJnlPostLine.Codeunit.al was updated to the new name, but external callers were not, and cannot be.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| GeneralLedgerSetup.Validate("Currency Symbol Position", GeneralLedgerSetup."Currency Symbol Position"::"Before Amount"); | ||
| GeneralLedgerSetup.Validate("Enable Data Check", DataCheck); | ||
| GeneralLedgerSetup.Validate("Acc. Receivables Category", AccReceivablesCategory); | ||
| GeneralledgerSetup."Use Concurrent Posting" := true; // to test concurrent posting for all countries |
There was a problem hiding this comment.
CreateGeneralLedgerSetup.Codeunit.al now unconditionally sets "Use Concurrent Posting" := true for every Contoso demo company, in every localization, with the comment "to test concurrent posting for all countries".
The comment itself signals this is leftover test-enablement code rather than an intended product default; it silently changes G/L posting behavior for anyone who installs the Contoso demo dataset in any country. (Note: the statement also references GeneralledgerSetup, a different-cased spelling of the GeneralLedgerSetup variable; AL identifiers are case-insensitive so this is not a functional bug, but it reinforces that the line reads as an unreviewed, ad hoc addition.)
Suggested fix (apply manually — could not be anchored as a one-click suggestion):
GeneralLedgerSetup.Modify(true);Agent judgement — not directly backed by a BCQuality knowledge article.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
…Concurrent-Posting-for-General-Ledger-GL-Entry
| /// <returns>Integer: The next sequential register number.</returns> | ||
| [InherentPermissions(PermissionObjectType::TableData, Database::"G/L Register", 'r')] | ||
| procedure GetNextEntryNo(): Integer | ||
| procedure GetNextRegisterNo(): Integer |
There was a problem hiding this comment.
The public table procedure G/L Register.GetNextEntryNo() is renamed in place to GetNextRegisterNo() (in the APAC layer both the parameterless overload and the GetNextEntryNo(UseLegacyPosting: Boolean) overload are renamed) with no [Obsolete] marker left behind.
Any dependent extension calling GLRegister.GetNextEntryNo() (directly, or via a subscriber that resolves the call at compile time) will fail to compile against this version with no deprecation warning window, which is exactly the anti-pattern this article describes. Keep the old procedure name in place, forward it to the new implementation, and mark it [Obsolete('Use GetNextRegisterNo instead.', '<version>')] for at least one release before removing it.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
What & why
Linked work
Fixes AB#624164
How I validated this
What I tested and the outcome (required — be specific: scenarios, commands, screenshots for UI changes)
Risk & compatibility