Skip to content

[Bug][Subscription Billing] "Delete Invoiced Sales Orders" leaves orphaned Sales Subscription Lines behind - #10210

Open
Miljan Milosavljević (miljance) wants to merge 1 commit into
microsoft:mainfrom
miljance:SBDeleteInvoicedSalesOrders
Open

[Bug][Subscription Billing] "Delete Invoiced Sales Orders" leaves orphaned Sales Subscription Lines behind#10210
Miljan Milosavljević (miljance) wants to merge 1 commit into
microsoft:mainfrom
miljance:SBDeleteInvoicedSalesOrders

Conversation

@miljance

@miljance Miljan Milosavljević (miljance) commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

What & why

Reports 299 "Delete Invoiced Sales Orders" and 291 "Delete Invd Blnkt Sales Orders" remove sales
lines with Delete() and DeleteAll(), which default to RunTrigger = false. The OnDelete()
trigger of tableextension 8054 "Sales Line" therefore never fires, and the Sales Subscription Lines
of the deleted document survive as orphans pointing at a document number that no longer exists.
Users see the "Sales Subscription Lines" list keep growing with entries for orders nobody can open
any more, and nothing in the UI can clean them up — only a data fix can.

This hits Subscription Billing users especially often, because Subscription Items force
Qty. to Invoice = 0, so EverythingInvoiced is never true and Sales-Post does not auto-delete
the order. The order is left fully shipped and invoiced — exactly report 299's scope.

The issue reports report 299. Report 291 is included because it is the identical defect on the
blanket-order path: SalesBlanketOrderLine.DeleteAll() is equally trigger-less, and "Blanket Order"
is one of the document types in IsSalesDocumentTypeWithServiceCommitments(). It was confirmed by a
failing test before the fix, not assumed.

Changes:

  • Two event subscribers in codeunit 8069 "Sales Subscription Line Mgmt.", following the per-path
    pattern the app already uses for Sales-Post and Sales-Quote to Order. No Base Application
    change is needed; both reports already publish suitable integration events.
  • Report 299 hooks OnAfterDeleteSalesLinesLoop, not OnBeforeDeleteSalesHeader. The latter fires
    only when the header is deleted, which would leak the subscription lines on the branch where an
    unassigned Charge (Item) line keeps the order alive after its item lines were already removed.
  • The four trigger-less deletion paths (both reports, Sales-Post, Sales-Quote to Order) now share
    a new SalesHeader.DeleteSalesServiceCommitments() on tableextension 8053, which batch-deletes a
    whole document's subscription lines. It lives on the header because Document Type + No. is the
    header's primary key and all four call sites already hold a Sales Header.
  • That procedure deliberately does not filter on document type. Copy Document guards only the
    source line and stamps the target's document type unchecked, so subscription lines can legitimately
    exist on invoices and credit memos and must still be cleaned up on posting.
  • SalesLine.DeleteSalesServiceCommitment() and SalesHeader.DeleteSalesServiceCommitments() are
    public rather than internal, and carry XML documentation. This is intentional: customers already
    need to call the deletion logic from outside the app, so both are exposed as a supported entry point.

Linked work

Fixes #10175

How I validated this

  • I read the full diff and it contains only changes I intended.
  • 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.
  • I added or updated tests for the new behavior, or explained below why none are needed.

What I tested and the outcome

Built with alc.exe (CodeCop, UICop, PerTenantExtensionCop, src/rulesets/base.ruleset.json) and
published to a BC 29 container. The only warning is pre-existing AA0139 in an untouched file.

Three new tests in codeunit 139915 "Sales Service Commitment Test":

  • CheckDeleteSalesServiceCommitmentOnDeleteInvoicedSalesOrders — order with a Subscription Item,
    fully shipped and invoiced, still open; run report 299; order gone and no subscription line left.
  • CheckDeleteSalesServiceCommitmentOnDeleteInvdBlnktSalesOrders — blanket order with two lines,
    converted to an order that is then fully posted; run report 291; same assertions.
  • CheckDeleteSalesServiceCommitmentOnPostSalesInvoiceCopiedFromSalesOrder — invoice created from an
    order by Copy Document; posting it removes the copied subscription lines. This path had no coverage
    and is what proves the shared procedure must not filter on document type.

Both report tests carry [TransactionModel(TransactionModel::AutoCommit)] because reports 299 and 291
commit unconditionally.

Each test was confirmed red before the fix and green after. The fix was then mutation-tested by
rebuilding, republishing and re-running: removing either subscriber reddens only its own test;
weakening the shared filter to document-type-only reddens both new tests and the pre-existing
SalesServiceCommitmentMakeOrderFromQuote; dropping the second blanket-order line's cleanup reddens
the blanket test. Both new tests also assert that a second, untouched document keeps its subscription
lines, so an over-deleting filter cannot pass.

Suites run green on the container: codeunit 139915 (69/69), 139687 "Recurring Billing Docs Test"
(88/88, covering the invoice and credit-memo posting paths that share the refactored procedure), and
139916 "Service Comm. Archive Test" (5/5).

Risk & compatibility

  • Public API surface. Two procedures on tableextensions 8053/8054 become part of the app's public
    contract, by design (see above). DeleteSalesServiceCommitment() was previously internal.
  • Refactor of existing paths. The Sales-Post and Sales-Quote to Order subscribers now route
    through the shared procedure. Behaviour is unchanged apart from the Sales-Post subscriber no longer
    needing SalesLine.FindFirst() to recover the document key, since the publisher passes the header.
  • No schema or upgrade impact, no permission or telemetry changes.
  • Archiving is unaffected — both reports archive the document before deleting it, so
    "Sales Subscription Line Archive" entries are written as before. Only the live table 8068 was leaking.
  • No new translatable strings: the only new labels are Locked = true test assertion messages.

…d-order deletion reports

Reports 299 "Delete Invoiced Sales Orders" and 291 "Delete Invd Blnkt Sales Orders"
remove sales lines with Delete() and DeleteAll(), which default to RunTrigger = false,
so the OnDelete() trigger of tableextension 8054 "Sales Line" never fires. The Sales
Subscription Lines of the deleted document survive as orphans pointing at a document
that no longer exists, and nothing in the UI can reach them.

Add an event subscriber for each report to codeunit 8069, following the per-path
pattern the app already uses for Sales-Post and Sales-Quote to Order. Report 299 hooks
OnAfterDeleteSalesLinesLoop rather than OnBeforeDeleteSalesHeader, so the subscription
lines are cleaned up on the branch where an unassigned Charge (Item) line keeps the
order alive after its item lines have been deleted.

Consolidate the four trigger-less deletion paths on a new
SalesHeader.DeleteSalesServiceCommitments() in tableextension 8053, which batch-deletes
the subscription lines of a whole document. It deliberately does not filter on document
type: Copy Document carries Sales Subscription Lines over to invoices and credit memos,
and those still have to be removed when the document is posted.

Both deletion procedures are public rather than internal. This is intentional -
customers already need to call the deletion logic from outside the app, so they are
exposed as a supported entry point and documented with XML comments.

Tests cover both reports and posting an invoice that carries subscription lines copied
from an order. The two report tests use TransactionModel::AutoCommit because both
reports commit unconditionally.

Fixes microsoft#10175

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added AL: Apps (W1) Add-on apps for W1 From Fork Pull request is coming from a fork Finance GitHub request for Finance area needs-approval Workflow runs require maintainer approval to start labels Aug 12, 2026
@alexei-dobriansky

Copy link
Copy Markdown
Contributor

Agentic PR Review - Round 1

Recommendation: Accept

What this PR does

This PR fixes orphaned Sales Subscription Lines when report 299 deletes invoiced sales orders and report 291 deletes invoiced blanket sales orders without running Sales Line delete triggers. It also routes the existing Sales-Post and Sales-Quote-to-Order cleanup through a shared Sales Header procedure.

The change matches the root cause: these paths delete lines with Delete() or DeleteAll(false), so the Sales Line tableextension OnDelete() cleanup does not run. I verified the BaseApp publishers and timing: report 299 raises OnAfterDeleteSalesLinesLoop after the line loop and before possible header deletion, report 291 raises OnBeforeDeleteSalesHeader after line DeleteAll, and Sales-Post passes the header before SalesLine.DeleteAll. The new tests cover the sales order report, the blanket order report, the copied invoice posting path, and that another document is not over-deleted.

Suggestions

None.

Risk assessment and necessity

Risk: This is a data cleanup path, so the main risk is deleting too many Sales Subscription Lines or missing one of the trigger-less delete flows. The shared procedure filters by document type and document number, and the tests include a second document guard, which keeps the cleanup scoped. The public procedure exposure is additive; there is no schema, upgrade, permission, or telemetry change.

Necessity: The issue is valid and important because the current batch job can leave live subscription records that point to a deleted document and need a data fix. The scope is right: it uses existing BaseApp events, keeps the cleanup in the Subscription Billing app, and adds focused regression tests for the affected paths.


[AI-PR-REVIEW] version=1 promptVersion=2 system=github pr=10210 round=1 by=alexei-dobriansky at=2026-08-13T08:12:08Z lastSha=0192e996b79d7ad69fb6258f4e0b7af45f4f2867 reviewKey=09ab93a8ab660eab85bbf271a88f273b3183133729a78eb29458d06cda5b731e suggestions=

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AL: Apps (W1) Add-on apps for W1 Finance GitHub request for Finance area From Fork Pull request is coming from a fork needs-approval Workflow runs require maintainer approval to start

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug][Subscription Billing] "Delete Invoiced Sales Orders" leaves orphaned Sales Subscription Lines behind

3 participants