Restore the full-sum sign decision in Apply-to-Oldest application - #9881
Restore the full-sum sign decision in Apply-to-Oldest application#9881Franco111000 wants to merge 3 commits into
Conversation
…oft#9529) The 28.2 early exit in the weighing loops of codeunit 12 stopped the sum as soon as it offset the new document, but the subsequent SetRange(Positive, RemainingAmount < 0) decision needs the net of all open entries: it selects the ledger side that application may consume uncapped. With open opposite-sign entries exceeding a positive new document, the truncated sum flipped the decision and the new document consumed open same-sign entries uncapped. Removes the early exit in PrepareTempCustLedgEntry, PrepareTempVendLedgEntry and PrepareTempEmplLedgEntry across all layer copies, restoring the pre-28.2 weighing semantics, and adds regression tests.
Agentic PR Review - Round 1Recommendation: Request ChangesWhat this PR doesThis PR removes the The changed customer and vendor logic matches the root cause in GitHub issue #9529. The new tests cover the reported customer case, the vendor mirror, and a mixed-entry customer case. The employee procedure has the same changed decision and the same event-adjusted sum path, but there is no matching employee regression test in this PR. SuggestionsS1 - Add employee regression coverage Risk assessment and necessityRisk: The regression surface is finance posting for Apply-to-Oldest on accounts with mixed-sign open entries. The change is narrow and does not change public signatures or events, but it changes how much data the weighing loop reads and restores the pre-28.2 full scan. The customer and vendor tests reduce risk for the reported paths; the employee path remains untested. Necessity: The linked GitHub bug is detailed and important: the current behavior can silently apply documents to same-sign entries and damage remaining amounts and due-date tracking. There is no linked ADO
|
Mirrors the customer and vendor regression tests for the employee copy of the weighing loop: three reimbursement payments, a larger back-dated expense, then a new payment that must settle the expense only.
|
Thanks for the review! S1 addressed in 8012bb6: added |
Agentic PR Review - Round 2Recommendation: AcceptWhat this PR doesThe new commit adds the missing employee Apply-to-Oldest mixed-sign regression test in the W1 and CZ employee test codeunits. The test mirrors the customer and vendor regression shape from round 1: older payments, a larger back-dated expense, and a new payment that must close only the expense and leave the other payments open. This addresses the round-1 gap. The test exercises the employee path that changed in PrepareTempEmplLedgEntry, uses whole expected amounts, checks the setup state before the final posting, and verifies both the closed opposite-sign entry and the untouched same-sign entries after posting. Status of previous suggestions
New observations (commits since round 1)None - changes only addressed the previous suggestion. Risk assessment and necessityRisk: The new commit only adds tests, so it does not expand runtime behavior. The remaining PR risk is still the finance posting Apply-to-Oldest change reviewed in round 1, now covered for customer, vendor, and employee paths. Necessity: The linked GitHub issue describes an important data-integrity bug. There is still no linked ADO AB# work item in the PR body, so I could not read an ADO Bug or Slice; I reviewed from GitHub issue #9529 and the diff. The added employee test is necessary and closes the only round-1 blocker.
|
| then | ||
| SufficientEntriesFound := true; | ||
| until (TempOldVendLedgEntry.Next() = 0) or SufficientEntriesFound; | ||
| until TempOldVendLedgEntry.Next() = 0; |
There was a problem hiding this comment.
This will reintroduce the severe performance issue that is affecting customers who have many (tens of thousands of) open ledger entries for one or very few vendors, and use 'Apply to Oldest'.
We should try to keep the performance optimization and address the bug that it introduced.
There was a problem hiding this comment.
Franco111000 I opened an ICM for this regression, and I am working on a fix that will fix it and also keep the performance optimization of 'Apply-to-Oldest'.
There was a problem hiding this comment.
Franco111000 I made a new pull request #10227 in which I:
- copied your new regression tests from this PR
- made sure to fix it while keeping the performance optimization (take sign into consideration when looping and summing)
- added one more test to make sure the performance optimization is still applied.
…10227) <!-- Thanks for contributing to BCApps! A few things before you hit "Create pull request": - Your PR must link to an approved issue. New here? See CONTRIBUTING.md. - You must have built and run your change yourself. CI is a safety net, not a substitute. - If you used AI or an agent to write this PR, you are still the author. Read the diff, build it, and try it before requesting review. Contributing guide: https://github.com/microsoft/BCApps/blob/main/CONTRIBUTING.md Local dev environment: https://github.com/microsoft/BCApps/blob/main/LOCAL_DEV_ENV.md --> ## What & why Fix regression introduced by Apply-to-Oldest optimization. Detailed explanation of the regression is in microsoft#9529 ## Linked work <!-- Required: link an approved GitHub issue using "Fixes #<number>". Microsoft contributors: also link the ADO work item with "AB#<number>" if you have one. --> Fixes [AB#646691](https://dynamicssmb2.visualstudio.com/1fcb79e7-ab07-432a-a3c6-6cf5a88ba4a5/_workitems/edit/646691) and its GitHub counterpart microsoft#9529 ## How I validated this - [X] I read the full diff and it contains only changes I intended. - [X] I built the affected app(s) locally with no new analyzer warnings. - [ ] I ran the change in Business Central and confirmed it behaves as expected. - [X] I added or updated tests for the new behavior, or explained below why none are needed. **What I tested and the outcome** *(required — be specific: scenarios, commands, screenshots for UI changes)* Added new automated tests: - 4 tests that guard against the regression added by the partner, taken from this PR microsoft#9881 - - one extra tests that confirms the performance optimization works (that early exit is done when summing is sufficient) - <!-- Example: - Ran the new "Post and Send" action on a sales invoice in a fresh container; document posted and email queued (see screenshot). - New unit tests in MyFeatureTest.Codeunit.al pass locally; full module test suite green. - No tests added because change is comment-only / refactor with existing coverage. --> ## Risk & compatibility There is test coverage for both the regression and the performance optimization, so the risk is low. <!-- Anything reviewers should watch for: breaking changes, upgrade/data impact, permissions, telemetry, feature flags, follow-up work. Write "None" if there's nothing to call out. -->
…10227) <!-- Thanks for contributing to BCApps! A few things before you hit "Create pull request": - Your PR must link to an approved issue. New here? See CONTRIBUTING.md. - You must have built and run your change yourself. CI is a safety net, not a substitute. - If you used AI or an agent to write this PR, you are still the author. Read the diff, build it, and try it before requesting review. Contributing guide: https://github.com/microsoft/BCApps/blob/main/CONTRIBUTING.md Local dev environment: https://github.com/microsoft/BCApps/blob/main/LOCAL_DEV_ENV.md --> ## What & why Fix regression introduced by Apply-to-Oldest optimization. Detailed explanation of the regression is in microsoft#9529 ## Linked work <!-- Required: link an approved GitHub issue using "Fixes #<number>". Microsoft contributors: also link the ADO work item with "AB#<number>" if you have one. --> Fixes [AB#646691](https://dynamicssmb2.visualstudio.com/1fcb79e7-ab07-432a-a3c6-6cf5a88ba4a5/_workitems/edit/646691) and its GitHub counterpart microsoft#9529 ## How I validated this - [X] I read the full diff and it contains only changes I intended. - [X] I built the affected app(s) locally with no new analyzer warnings. - [ ] I ran the change in Business Central and confirmed it behaves as expected. - [X] I added or updated tests for the new behavior, or explained below why none are needed. **What I tested and the outcome** *(required — be specific: scenarios, commands, screenshots for UI changes)* Added new automated tests: - 4 tests that guard against the regression added by the partner, taken from this PR microsoft#9881 - - one extra tests that confirms the performance optimization works (that early exit is done when summing is sufficient) - <!-- Example: - Ran the new "Post and Send" action on a sales invoice in a fresh container; document posted and email queued (see screenshot). - New unit tests in MyFeatureTest.Codeunit.al pass locally; full module test suite green. - No tests added because change is comment-only / refactor with existing coverage. --> ## Risk & compatibility There is test coverage for both the regression and the performance optimization, so the risk is low. <!-- Anything reviewers should watch for: breaking changes, upgrade/data impact, permissions, telemetry, feature flags, follow-up work. Write "None" if there's nothing to call out. -->
What & why
Since 28.2, posting a document for a customer or vendor with Application Method = "Apply to Oldest" can apply the new document to open entries of the same sign, closing them and accumulating their amounts onto the new document (issue #9529). A posted invoice can end up with a Remaining Amount larger than its own Amount, and per-entry due-date tracking is destroyed. No error is raised.
Root cause. In codeunit 12, the weighing loops in
PrepareTempCustLedgEntry,PrepareTempVendLedgEntryandPrepareTempEmplLedgEntryfeed the sign decisionSetRange(Positive, RemainingAmount < 0). That decision needs the net of the new document plus all open entries: it selects the ledger side that the application loop may consume uncapped (FindAmtForApplnapplies the full remaining amount of each entry when the Positive filter is set, and caps withABSMinonce the filter is cleared). 28.2 added aSufficientEntriesFoundearly exit that stops the loop as soon as the running sum offsets the new document. Because the buffer key iteratesPositive = falseentries first, a positive new document (customer invoice or refund, vendor payment or refund) whose opposite-sign open total exceeds it trips the exit inside the negative block, before the positive entries pull the sum back. The truncated sum flips the decision, and the new document consumes old same-sign entries uncapped.Fix. Remove the early exit in all three procedures, restoring the pre-28.2 weighing semantics. The decision is a property of the whole open-entry set, so any early exit leaves it on a partial sum in some conditions: a payment discount can exceed a partially credited invoice's remaining amount, the stored
Positiveflag can disagree with the current remaining sign on overshot entries (which this very defect produces), and the employee loop raisesOnPrepareTempEmplLedgEntryOnBeforeUpdateRemainingAmount, which lets subscribers adjust the summed amount. The decision must see the full sum.Why not the opposite-sign filter suggested in the issue.
SetRange(Positive, NewCVLedgEntryBuf."Remaining Amount" < 0)fixes the reported case but breaks the invariant that the selected side must be fully absorbable: with an open invoice of 1,000, an open credit memo of 5,000 and a new invoice of 2,000, it consumes the credit memo uncapped and leaves the new invoice permanently open at a remaining amount of -2,000. The full-sum rule picks the side that the netting can exhaust without overshoot.Origin. The early exit entered through the NAV sync published in this repo as 30ce9d7 (
features/BCAppsMigration, "Sync application code from NAV repo (#109)") and reached main in the layer import 748fdaa (#8848). It shipped with one companion test,ApplyToOldestPartialPaymentAppliesToOldestFirst, which covers only the single-sign case, where the exit is harmless. This PR restores the three procedure bodies to their state at30ce9d7f71^byte for byte, in all 11 layer copies (W1, APAC, BE, CH, ES, FI, FR, IT, NA, NO, RU).Out of scope, called out for transparency. There is a rarer pre-existing variant on <= 28.1: when the account's full net balance opposes the new document, the full-sum decision also selects same-sign entries. That behavior is unchanged here (it is the overshoot-protection trade-off described above and predates 28.2). Tests 3 and 4 of the reporter's attached repro app target that variant and still fail after this fix, by design. The payment-discount and stale-
Positiveedges above are likewise pre-existing and out of scope.Credit to Nikola Jovicic (@nikolajovicic) for the precise root-cause analysis and the attached repro app; direction agreed in the issue thread.
Linked work
Fixes #9529
How I validated this
What I tested and the outcome
30ce9d7f71^, verified by direct comparison against that commit.ApplyCustLedgEntry,FindAmtForAppln,FindNextOldCustLedgEntryToApply) with concrete numbers for the repro scenario, the netting scenario and the vendor mirror, and locked in as tests:ApplyToOldestInvoiceDoesNotApplyToOtherInvoices(codeunit 134006, W1 + DACH copy): the reported defect. Under the current code the three older invoices are closed by the new invoice and the new invoice stays open above its own amount; with the fix the credit memo is closed, the new invoice is closed at 0, and only the oldest invoice absorbs the credit memo excess.ApplyToOldestPaymentNettingAgainstMixedEntries(codeunit 134006, W1 + DACH copy): pins the mixed-sign netting end state, which is identical before and after this change.ApplyToOldestPaymentDoesNotApplyToOtherPayments(codeunit 134004, W1 + CH + IT copies): the vendor mirror of the defect (vendor payments are the positive direction there).ApplyToOldestPartialPaymentAppliesToOldestFirst(single-sign) takes the same decision path before and after this change and stays green.Risk & compatibility
CalcFieldsper qualifying open entry during Apply-to-Oldest application on mixed-sign accounts. The weighing loop is entered whenever at least one open entry has the same sign as the new document; it is skipped when every open entry is opposite-sign. A correct optimization would need a decision that does not depend on the truncated sum; I am happy to explore that separately if there is interest.