Skip to content

[Bug][Subscription Billing] G/L Entry "Subscription Contract No." is only filled on deferral release entries, not at invoice posting #10355

Description

Describe the issue

General ledger entries that originate from a subscription contract invoice do not carry the contract number. The field "Subscription Contract No." on the G/L Entry stays blank for every entry created when the invoice is posted — both when the line posts to a deferral account and when it posts directly to the revenue or expense account. The number only appears on the separate entries that the "Contract Deferrals Release" batch creates later.

The practical consequence is that you cannot trace general ledger postings back to the contract they came from. Filtering or analysing the G/L by contract silently returns only the released deferral portion, so revenue that was posted directly (no deferrals) is invisible, and the deferral posting itself cannot be matched to its contract until it is released. Any reporting, reconciliation, or audit that relies on this field gives an incomplete picture.

This also affects data already in the database: entries posted before a fix will still have a blank contract number, so historical postings need to be backfilled as well.

Note that the posted Sales/Purchase Invoice and Credit Memo lines do get their "Subscription Contract No." filled correctly at posting time. Only the derived G/L entries are affected, which means the correct value can be recovered from source data that is known to be reliable.

Affected objects:

  • src/Apps/W1/Subscription Billing/App/Base/Table Extensions/GLEntry.TableExt.al
  • src/Apps/W1/Subscription Billing/App/Deferrals/Codeunits/CustomerDeferralsMngmt.Codeunit.al
  • src/Apps/W1/Subscription Billing/App/Deferrals/Codeunits/VendorDeferralsMngmt.Codeunit.al
  • src/Apps/W1/Subscription Billing/App/Deferrals/Reports/ContractDeferralsRelease.Report.al
  • src/Apps/W1/Subscription Billing/App/Base/Codeunits/UpgradeSubscriptionBilling.Codeunit.al

Expected behavior

G/L Entry."Subscription Contract No." should be populated on every G/L entry that stems from a subscription contract line — including the entries created at initial invoice posting, for both deferral-account postings and direct revenue/expense postings — not only on deferral-release entries.

Why it currently fails

G/L Entry."Subscription Contract No." (field 8000, added by tableextension 8067 in GLEntry.TableExt.al) is Editable = false and purely code-populated. The only code that ever sets it is:

  • CustomerDeferralsMngmt.Codeunit.al line 351, SetContractNo (subscriber on Gen. Jnl.-Post Line.OnBeforePostGenJnlLine) — exits unless the DeferralEntryNo global is non-zero and the journal line's Source Code equals SourceCodeSetup."Sub. Contr. Deferrals Release".
  • CustomerDeferralsMngmt.Codeunit.al line 366, TransferContractNoToGLEntry (subscriber on OnBeforeInsertGlobalGLEntry) — copies a non-blank value from the Gen. Journal Line onto the G/L Entry.
  • VendorDeferralsMngmt.Codeunit.al line 315 has the vendor counterpart of SetContractNo with the same two guards.

Both guards are only satisfied while ContractDeferralsRelease.Report.al (PostGenJnlLine) is running. During Sales-Post / Purch.-Post nothing sets Gen. Journal Line."Subscription Contract No.", so the value never reaches the G/L entry.

Scenario G/L Entry."Subscription Contract No."
Invoice posted, line has deferrals (posting to deferral account) blank
Invoice posted, line has no deferrals (direct revenue/expense posting) blank
Deferral released later (Contract Deferrals Release) populated

Proposed fix — posting time

Set Gen. Journal Line."Subscription Contract No." from the originating sales/purchase line during invoice posting, for lines where IsLineAttachedToBillingLine() is true, regardless of whether deferrals are created. TransferContractNoToGLEntry then already carries it onto the G/L entry.

Related risk worth addressing in the same change: TransferContractNoToGLEntry exists only in CustomerDeferralsMngmt and has no partner/source-type filter, so it incidentally also covers the vendor release flow. There is no vendor-side equivalent, which makes the vendor case fragile against any future refactor that assumes the customer subscriber is customer-only.

Proposed fix — data upgrade for historical entries

Fixing posting only prevents the problem going forward. Existing entries need to be backfilled by a new OnUpgradePerCompany upgrade-tag procedure in codeunit 8032 "Upgrade Subscription Billing", following the existing FillPostingGroupsInContractDeferrals pattern (FindSet(true) / Modify(false)), registered with a new tag in OnGetPerCompanyUpgradeTags.

Scenario A — deferral postings (anchor: Cust./Vend. Sub. Contract Deferral). The deferral record already carries everything needed: "Subscription Contract No." (2), "Document Type"/"Document No."/"Document Line No." (3/4/17), "Gen. Bus./Prod. Posting Group" (74/75) and "Dimension Set ID" (480). For every deferral record (not only released ones):

  1. GeneralPostingSetup.Get(Deferral."Gen. Bus. Posting Group", Deferral."Gen. Prod. Posting Group") → read "Cust./Vend. Sub. Contr. Def Account".
  2. Find the G/L Entry with matching Document Type/Document No., "G/L Account No." = that deferral account, "Dimension Set ID" = the deferral's, and "Subscription Contract No." = ''.
  3. If found and unambiguous, set the contract number.

This relies on the posting groups on the deferral record already being correct, which is exactly what the existing FillPostingGroupsInContractDeferrals tag guarantees — so the new procedure must run after that one in OnUpgradePerCompany.

Scenario B — direct postings (anchor: posted headers with "Recurring Billing" = true, field 8051 on Sales Invoice Header, Sales Cr.Memo Header, Purch. Inv. Header and Purch. Cr. Memo Hdr, which is only set by Subscription Billing):

  1. Loop posted headers where "Recurring Billing" = true.
  2. For each, loop lines with a non-blank "Subscription Contract No.".
  3. Skip lines that have a matching deferral record for that Document Type/No./Document Line No. — those are Scenario A.
  4. For the rest, resolve "Cust./Vend. Sub. Contract Account" from General Posting Setup using the line's own posting groups, find the matching blank G/L Entry by Document Type/No. + account + "Dimension Set ID", and set the contract number.

Ambiguity and error handling (the upgrade runs unattended, so it must never fail or guess):

  • Optionally also match "Source No." against the deferral's/line's Customer No. or Vendor No. as a cheap extra safety check.
  • If posting groups are empty, the posting-setup combination does not exist, or it yields no account, fall back to the related document line before skipping; if it still cannot be resolved, skip the record and log it via Session.LogMessage so it surfaces in Application Insights.
  • If a target G/L entry was already set to a different contract number earlier in the same run, that is a genuine collision (two contracts summarised into one G/L entry). Do not guess — leave it blank and log via Session.LogMessage.

The upgrade must never fail; every skipped record is telemetry, not an error.

Steps to reproduce

  1. Set up a customer subscription contract with a contract line whose General Posting Setup has both a "Cust. Sub. Contract Account" and a "Cust. Sub. Contr. Def Account".
  2. Create the billing proposal and post the contract invoice for a line that does create contract deferrals.
  3. Open the posted sales invoice, choose Find entries > G/L Entries, and add the column "Subscription Contract No." — it is blank on all entries, including the one on the deferral account.
  4. Repeat steps 2–3 for a contract line that does not create deferrals (posting straight to the revenue account). "Subscription Contract No." is blank there too.
  5. Run Contract Deferrals Release for a later period and look at the G/L entries it creates. Only these entries have "Subscription Contract No." filled.
  6. The same applies on the vendor side with a vendor subscription contract and a posted purchase invoice.

Additional context

The existing tests TestReleasingCustomerContractDeferrals in Test/Deferrals/CustomerDeferralsTest.Codeunit.al and its vendor equivalent only assert G/L Entry."Subscription Contract No." after the release report has run, never right after the initial invoice posting — which is why the gap was never caught.

I will provide a fix for a bug

  • I will provide a fix for a bug

Metadata

Metadata

Assignees

No one assigned

    Labels

    FinanceGitHub request for Finance area

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions