Skip to content

[Bug]: [Subscription Billing] Payment discount on contract invoices comes from the customer, not from the contract's payment terms - #10182

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

Conversation

@miljance

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

Copy link
Copy Markdown
Contributor

What & why

When a Subscription Contract uses payment terms that differ from the ones on the customer or vendor, the invoice created from that contract showed the contract's Payment Terms Code and a matching Pmt. Discount Date, but kept the customer's Payment Discount %. A customer set up with terms granting no discount, whose contract was changed to terms granting 2%, was invoiced at 0% — and the mirror case gave away a discount the contract does not allow. The same happened on purchase invoices created from vendor contracts.

Create Billing Documents copies the contract onto the document with TransferFields, which assigns without validation, so Payment Discount % is never recalculated. The later Validate("Document Date") does re-run the payment terms trigger, but that trigger only refreshes the discount when UpdateDocumentDate is false — and validating a changed document date sets it to true. Due Date and Pmt. Discount Date sit outside that guard, which is why only the percentage was wrong.

This change re-validates the contract's Payment Terms Code after the document date is set, so the discount follows the contract exactly as if a user had entered those terms on the document by hand.

The two branches differ deliberately. Sales Header guards the refresh with if not UpdateDocumentDate and (xRec."Payment Terms Code" <> Rec."Payment Terms Code"), so after TransferFields a plain re-validate is a no-op — the customer's code is assigned back first to make the trigger see a real change. Purchase Header has no xRec comparison, so the re-validate alone is enough.

// CreateSalesHeaderFromContract
if SalesHeader."Payment Terms Code" <> OldSalesHeader."Payment Terms Code" then begin
    SalesHeader."Payment Terms Code" := OldSalesHeader."Payment Terms Code";
    SalesHeader.Validate("Payment Terms Code", CustomerContract."Payment Terms Code");
end;

// CreatePurchaseHeaderFromContract
if PurchaseHeader."Payment Terms Code" <> OldPurchaseHeader."Payment Terms Code" then
    PurchaseHeader.Validate("Payment Terms Code", VendorContract."Payment Terms Code");

Linked work

Fixes #10095

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 (required — be specific: scenarios, commands, screenshots for UI changes)

Built with alc.exe (CodeCop + UICop) and published to a BC 29.0.53247.0 container. All test runs below are against that container.

  • 4 new tests in Recurring Billing Docs Test (codeunit 139687), covering both partners in both directions — contract terms granting a discount the customer's do not, and the reverse:

    • PaymentDiscountOnSalesInvoiceIsTakenFromCustomerContractPaymentTerms
    • PaymentDiscountOnSalesInvoiceIsRemovedWhenCustomerContractPaymentTermsGrantNone
    • PaymentDiscountOnPurchaseInvoiceIsTakenFromVendorContractPaymentTerms
    • PaymentDiscountOnPurchaseInvoiceIsRemovedWhenVendorContractPaymentTermsGrantNone

    They create billing documents with a document date that differs from the posting date, then assert Payment Terms Code, Payment Discount % and Pmt. Discount Date on the created document.

  • Written first, and confirmed red for the intended reason before the fix:

    Payment Discount % must be equal to '2'  in Sales Header: Document Type=Invoice, No.=102304. Current value is '0'.
    Payment Discount % must be equal to '0'  in Sales Header: Document Type=Invoice, No.=102305. Current value is '3'.
    Payment Discount % must be equal to '2'  in Purchase Header: Document Type=Invoice, No.=107212. Current value is '0'.
    Payment Discount % must be equal to '0'  in Purchase Header: Document Type=Invoice, No.=107213. Current value is '3'.
    
  • Mutation testing, each mutant built, published and re-run:

    Mutant Result
    Invert the guard (<>=) Killed — all 4 tests red
    Validate the customer's / vendor's terms code instead of the contract's Killed — all 4 tests red
    Remove the Payment Terms Code re-assignment Killed on sales; survived on purchase — the line was removed there, since Purchase Header has no xRec comparison to defeat
    Move the block before Validate("Document Date") Survived — the discount does not depend on the document date, so the order is not load-bearing for this fix
  • Regression suites, all green on the committed build:

    • 139687 Recurring Billing Docs Test — 92 passed, 0 failed
    • 139688 Recurring Billing Test — 46 passed, 0 failed
    • 139686 Billing Correction Test — 13 passed, 0 failed
    • 148455 Automated Billing Test — 5 passed, 0 failed
  • Analyzer output is unchanged: the remaining AL0920, AW0006, AW0008 and AA0139 warnings are all pre-existing and in files this PR does not touch.

Risk & compatibility

  • Behavioural change on newly created documents. Sales and purchase invoices created from a contract whose payment terms differ from the partner's will now carry that terms code's discount percentage. This is the fix, but it does change the amount settled on those receivables and payables. Already-posted documents are untouched; only documents created after the upgrade differ.
  • No schema, permission, telemetry or upgrade-code impact. No public API surface changes.
  • The change is confined to the two …FromContract procedures in Create Billing Documents; the per-customer and per-vendor grouping paths (CreateSalesHeaderForCustomerNo / CreatePurchaseHeaderForVendorNo) never applied contract payment terms and are unaffected.

… billing documents

Invoices created from a Subscription Contract carried the contract's "Payment
Terms Code" but the customer's or vendor's "Payment Discount %". A customer set
up with terms that grant no discount, whose contract was changed to terms that
do, was invoiced with 0% - and the mirror case gave away a discount the contract
did not allow.

"Create Billing Documents" copies the contract onto the document with
TransferFields, which assigns without validation, so the discount is never
recalculated. The later Validate("Document Date") does re-run the payment terms
trigger, but that trigger only refreshes the discount when UpdateDocumentDate is
false, which validating a changed document date sets to true.

Re-validate the contract's "Payment Terms Code" after the document date is set.
Sales Header additionally guards on xRec <> Rec, so there the customer's code is
assigned back first to make the trigger see a real change; Purchase Header has no
such guard and needs only the re-validate.

Tests in "Recurring Billing Docs Test" cover both partners in both directions:
contract terms granting a discount the customer's do not, and the reverse. They
assert the resulting "Payment Discount %" and "Pmt. Discount Date" on the created
document.

Fixes microsoft#10095

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 subscription contract invoices so Payment Discount % follows the contract payment terms, not the customer or vendor payment terms. I verified the W1 Sales Header and Purchase Header payment terms validation logic: the sales side needs the temporary re-assignment because it checks xRec, while the purchase side only needs a re-validate after Document Date is set.

The fix is narrow and runs after the document date is validated, so the base payment terms trigger recalculates the discount with the same contract terms code that is already on the document. The new tests cover sales and purchase, both adding a contract discount and removing a partner discount, and they assert the terms code, discount percent, and discount date on the created documents.

Suggestions

None.

Risk assessment and necessity

Risk: this is a financial behavior change for new sales and purchase invoices created from subscription contracts where the contract payment terms differ from the partner. The scope is limited to document creation from contracts, with no schema, permission, public API, or event surface change.

Necessity: the change is needed because the current TransferFields path can persist the wrong payment discount on receivables and payables. The scope matches the reported bug and the tests cover the important mirror cases.


[AI-PR-REVIEW] version=1 promptVersion=2 system=github pr=10182 round=1 by=alexei-dobriansky at=2026-08-12T14:54:30.070Z lastSha=d12b43de0e3d063db783cb6422375e646325a7f3 reviewKey=905f1a4a50ab7f3783818944711110d000a1be02038677df971c87fcf8bae098 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] Payment discount on contract invoices comes from the customer, not from the contract's payment terms

3 participants