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):
GeneralPostingSetup.Get(Deferral."Gen. Bus. Posting Group", Deferral."Gen. Prod. Posting Group") → read "Cust./Vend. Sub. Contr. Def Account".
- 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." = ''.
- 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):
- Loop posted headers where "Recurring Billing" = true.
- For each, loop lines with a non-blank "Subscription Contract No.".
- Skip lines that have a matching deferral record for that Document Type/No./Document Line No. — those are Scenario A.
- 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
- 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".
- Create the billing proposal and post the contract invoice for a line that does create contract deferrals.
- 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.
- 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.
- 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.
- 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
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:
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 inGLEntry.TableExt.al) isEditable = falseand purely code-populated. The only code that ever sets it is:CustomerDeferralsMngmt.Codeunit.alline 351,SetContractNo(subscriber onGen. Jnl.-Post Line.OnBeforePostGenJnlLine) — exits unless theDeferralEntryNoglobal is non-zero and the journal line'sSource CodeequalsSourceCodeSetup."Sub. Contr. Deferrals Release".CustomerDeferralsMngmt.Codeunit.alline 366,TransferContractNoToGLEntry(subscriber onOnBeforeInsertGlobalGLEntry) — copies a non-blank value from the Gen. Journal Line onto the G/L Entry.VendorDeferralsMngmt.Codeunit.alline 315 has the vendor counterpart ofSetContractNowith the same two guards.Both guards are only satisfied while
ContractDeferralsRelease.Report.al(PostGenJnlLine) is running. During Sales-Post / Purch.-Post nothing setsGen. Journal Line."Subscription Contract No.", so the value never reaches the G/L entry.G/L Entry."Subscription Contract No."Proposed fix — posting time
Set
Gen. Journal Line."Subscription Contract No."from the originating sales/purchase line during invoice posting, for lines whereIsLineAttachedToBillingLine()is true, regardless of whether deferrals are created.TransferContractNoToGLEntrythen already carries it onto the G/L entry.Related risk worth addressing in the same change:
TransferContractNoToGLEntryexists only inCustomerDeferralsMngmtand 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
OnUpgradePerCompanyupgrade-tag procedure incodeunit 8032 "Upgrade Subscription Billing", following the existingFillPostingGroupsInContractDeferralspattern (FindSet(true)/Modify(false)), registered with a new tag inOnGetPerCompanyUpgradeTags.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):GeneralPostingSetup.Get(Deferral."Gen. Bus. Posting Group", Deferral."Gen. Prod. Posting Group")→ read "Cust./Vend. Sub. Contr. Def Account"."G/L Account No."= that deferral account,"Dimension Set ID"= the deferral's, and"Subscription Contract No."= ''.This relies on the posting groups on the deferral record already being correct, which is exactly what the existing
FillPostingGroupsInContractDeferralstag guarantees — so the new procedure must run after that one inOnUpgradePerCompany.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):
Ambiguity and error handling (the upgrade runs unattended, so it must never fail or guess):
"Source No."against the deferral's/line's Customer No. or Vendor No. as a cheap extra safety check.Session.LogMessageso it surfaces in Application Insights.Session.LogMessage.The upgrade must never fail; every skipped record is telemetry, not an error.
Steps to reproduce
Additional context
The existing tests
TestReleasingCustomerContractDeferralsinTest/Deferrals/CustomerDeferralsTest.Codeunit.aland its vendor equivalent only assertG/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