Skip to content

[Bug]: Regression in BC 28.2: posting an invoice for a customer/vendor with Application Method = "Apply to Oldest" applies the invoice to **other open invoices (same sign)**, closing them and accumulating their amounts onto the new invoice #9529

Description

@nikolajovicic

Affected version: Business Central 28.2+ (verified on application build 28.2.50931.51034 OnPrem W1 and 28.2.50931.52241 SaaS production; source inspection of the latest available build 28.3.52162.52641 confirms the defective code is unchanged and the issue is still present)
Works correctly in: 28.0 (verified on 28.0.46665.48549) and, per code inspection, all versions before the 28.2 change
Area: Base Application, Codeunit 12 "Gen. Jnl.-Post Line", procedures PrepareTempCustLedgEntry and PrepareTempVendLedgEntry
Severity suggestion: High / data-integrity — silently produces incorrect applications during normal posting; no error is raised; recovery requires manual Unapply of potentially thousands of entries

Summary

In 28.2 an early-exit optimization (SufficientEntriesFound) was added to the "weighing" loop in PrepareTempCustLedgEntry (and PrepareTempVendLedgEntry). The loop now stops summing open entries as soon as the partial sum offsets the new document, but the subsequent decision TempOldCustLedgEntry.SetRange(Positive, RemainingAmount < 0) still uses that now partial sum. Because the temporary buffer iterates in key order (Customer No., Applies-to ID, Open, Positive, Due Date) where Positive = false sorts first, credit memos/payments are summed first. Whenever their total exceeds the new invoice amount, the partial sum turns negative and the filter resolves to Positive = TRUE — so the new invoice is applied to older open invoices (same sign), closing them one by one in due-date order. After the filtered set is exhausted, FindNextOldCustLedgEntryToApply clears the filter and continues, consuming the remaining opposite-sign entries as well. The net effect: a single posted invoice can close every open entry of the account and accumulate the entire balance onto itself under one due date.

The trigger condition on 28.2 is mundane: customer (or vendor) with Application Method = "Apply to Oldest" + open credit memos/payments totaling more than the newly posted invoice. The account's overall balance can be perfectly normal (net debit).

Steps to reproduce (minimal, 4 journal postings, W1)

  1. Create a customer; set Application Method = Manual (so setup entries stay open).
  2. In a general journal post, with blank Applies-to fields, balancing to a G/L account:
    • Invoice F1 = 1,000 (posting date D1)
    • Invoice F2 = 1,000 (D2 > D1)
    • Invoice F3 = 1,000 (D3 > D2)
    • Credit Memo CM1 = −2,500 (D4)
      All four entries remain open. Net balance = +500 (debit).
  3. On the customer card set Application Method = Apply to Oldest.
  4. Post a new invoice F4 = 2,000 (D5, blank Applies-to). Note: CM1 (2,500) > F4 (2,000).

Expected result

F4 is applied against CM1 (the only opposite-sign entry): F4 closed, CM1 remaining −500, F1–F3 untouched. This is exactly what 28.0/28.1 do.

Actual result on 28.2

F1, F2 and F3 are closed by invoice F4 (invoice applied to invoices), applications continue into CM1, and F4 ends with an incorrect remaining amount. The receivable of the closed invoices is silently consolidated onto F4 with F4's due date; per-invoice due-date tracking is destroyed. No error or warning is raised.

Root cause (code diff 28.0 → 28.2)

Codeunit 12, PrepareTempCustLedgEntry (identical change in PrepareTempVendLedgEntry):

// 28.0:
repeat
    ...
    RemainingAmount += TempOldCustLedgEntry."Remaining Amount";
until TempOldCustLedgEntry.Next() = 0;
TempOldCustLedgEntry.SetRange(Positive, RemainingAmount < 0);

// 28.2 (new):
repeat
    ...
    RemainingAmount += TempOldCustLedgEntry."Remaining Amount";
    if (Cust."Application Method" = Cust."Application Method"::"Apply to Oldest") and
       (RemainingAmount * NewCVLedgEntryBuf."Remaining Amount" <= 0)
    then
        SufficientEntriesFound := true;
until (TempOldCustLedgEntry.Next() = 0) or SufficientEntriesFound;   // <-- early exit
TempOldCustLedgEntry.SetRange(Positive, RemainingAmount < 0);        // <-- decision on PARTIAL sum

Chain of events for the repro above (new invoice +2,000):

  1. Temp buffer iterates with Positive = false first → CM1 is summed first: RemainingAmount = 2,000 − 2,500 = −500.
  2. RemainingAmount * NewRemaining ≤ 0SufficientEntriesFound := true → loop exits before summing the invoices.
  3. SetRange(Positive, −500 < 0)SetRange(Positive, TRUE) → the applying set = the open invoices (same sign as the new document!).
  4. ApplyCustLedgEntry applies F4 to F1→F2→F3 (due-date order); when the positives are exhausted, FindNextOldCustLedgEntryToApply clears the Positive filter (line TempOldCustLedgEntry.SetRange(Positive); Find('-')) and, since the product check is negative, continues into CM1 as well.

On 28.0 the loop sums all entries → RemainingAmount = +500SetRange(Positive, FALSE) → only CM1 is applied (correctly, capped at 2,000).

Note there is also a pre-existing (much rarer) variant of the same defective decision on ≤28.1: when the account's full net balance has the opposite sign of the new document (e.g. open payments > open invoices, then posting an invoice), SetRange(Positive, RemainingAmount < 0) likewise selects same-sign entries. 28.2 merely made the defect reachable with everyday data.

Consistency argument

The same procedure forbids same-sign application on the manual path: for Applies-to Doc. No. it runs OldCustLedgEntry.TestField(Positive, not NewCVLedgEntryBuf.Positive). The automatic Apply-to-Oldest path lacks any such guard, so the system automatically performs an application that a user is explicitly prevented from making manually.

Verification matrix (automated AL tests, same test app on both containers)

Scenario (new invoice posted, blank Applies-to) 28.0.46665 28.2.50931
Open CMs > new invoice, net balance positive (repro above) ✅ pass fails — invoices closed by invoice
Same, vendor side ✅ pass ❌ fails
Net balance opposite to new document (payments > invoices) ❌ fails (pre-existing variant) ❌ fails
Payment spread oldest-first, prepayment exclusion, FCY, real order/credit-memo posting flows (10 scenarios) ✅ pass ✅ pass

Suggested fix

Either compute the full sum before the sign decision (restore 28.0 behavior), or — more robustly — restrict the applying set for automatic Apply-to-Oldest to entries of the opposite sign of the new document (mirroring the TestField(Positive, not ...) rule already enforced on the manual path), e.g. TempOldCustLedgEntry.SetRange(Positive, NewCVLedgEntryBuf."Remaining Amount" < 0).

Reporter

GoPro doo (Microsoft partner, Belgrade, Serbia) — delegated admin for the affected tenant. Contact: nikola.jovicic@gopro.rs.

Expected behavior

F4 is applied against CM1 (the only opposite-sign entry): F4 closed, CM1 remaining −500, F1–F3 untouched. This is exactly what 28.0/28.1 do.

Steps to reproduce

Steps to reproduce (minimal, 4 journal postings, W1)

  1. Create a customer; set Application Method = Manual (so setup entries stay open).
  2. In a general journal post, with blank Applies-to fields, balancing to a G/L account: - Invoice F1 = 1,000 (posting date D1)
    • Invoice F2 = 1,000 (D2 > D1)
    • Invoice F3 = 1,000 (D3 > D2)
    • Credit Memo CM1 = −2,500 (D4)
      All four entries remain open. Net balance = +500 (debit).
  3. On the customer card set Application Method = Apply to Oldest.
  4. Post a new invoice F4 = 2,000 (D5, blank Applies-to). Note: CM1 (2,500) > F4 (2,000).

Additional context

Attached AL test app "ApplyToOldest Repro" (app.json + codeunit 50100 "ApplyToOldest Repro", 4 tests) depends only on Microsoft test libraries (Library Assert, Tests-TestLibraries) — no third-party code whatsoever. Each test is the 4-posting scenario from "Steps to reproduce" (customer + vendor, both variants), asserting the standard-correct outcome.

Verified on a clean Cronus W1 OnPrem 28.2.50931.51034 container with NO third-party extensions installed (Microsoft test framework only):

Test 28.0.46665 (vanilla) 28.2.50931 (vanilla)
CustInvoiceWithLargeOpenCreditMemoMustNotCloseInvoices pass FAILExpected 1,000, Actual 0 (older invoice closed by the new invoice)
VendInvoiceWithLargeOpenCreditMemoMustNotCloseInvoices pass FAILExpected −1,000, Actual −500
CustInvoiceWithNetCreditBalanceMustNotCloseInvoices FAIL (pre-existing variant) FAIL
VendInvoiceWithNetDebitBalanceMustNotCloseInvoices FAIL (pre-existing variant) FAIL

Attachments: Repro app.zip with Repro Test (BaseApp only).al, `

Repro app.zip

I will provide a fix for a bug

  • I will provide a fix for a bug

Metadata

Metadata

Assignees

No one assigned

    Labels

    ApprovedThe issue is approvedFinanceGitHub request for Finance area

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions