[Bug]: [Subscription Billing] Payment discount on contract invoices comes from the customer, not from the contract's payment terms - #10182
Conversation
… 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>
Agentic PR Review - Round 1Recommendation: AcceptWhat this PR doesThis 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. SuggestionsNone. Risk assessment and necessityRisk: 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.
|
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 Codeand a matchingPmt. Discount Date, but kept the customer'sPayment 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 Documentscopies the contract onto the document withTransferFields, which assigns without validation, soPayment Discount %is never recalculated. The laterValidate("Document Date")does re-run the payment terms trigger, but that trigger only refreshes the discount whenUpdateDocumentDateisfalse— and validating a changed document date sets it totrue.Due DateandPmt. Discount Datesit outside that guard, which is why only the percentage was wrong.This change re-validates the contract's
Payment Terms Codeafter 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 Headerguards the refresh withif not UpdateDocumentDate and (xRec."Payment Terms Code" <> Rec."Payment Terms Code"), so afterTransferFieldsa plain re-validate is a no-op — the customer's code is assigned back first to make the trigger see a real change.Purchase Headerhas noxReccomparison, so the re-validate alone is enough.Linked work
Fixes #10095
How I validated this
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(codeunit139687), covering both partners in both directions — contract terms granting a discount the customer's do not, and the reverse:PaymentDiscountOnSalesInvoiceIsTakenFromCustomerContractPaymentTermsPaymentDiscountOnSalesInvoiceIsRemovedWhenCustomerContractPaymentTermsGrantNonePaymentDiscountOnPurchaseInvoiceIsTakenFromVendorContractPaymentTermsPaymentDiscountOnPurchaseInvoiceIsRemovedWhenVendorContractPaymentTermsGrantNoneThey create billing documents with a document date that differs from the posting date, then assert
Payment Terms Code,Payment Discount %andPmt. Discount Dateon the created document.Written first, and confirmed red for the intended reason before the fix:
Mutation testing, each mutant built, published and re-run:
<>→=)Payment Terms Codere-assignmentPurchase Headerhas noxReccomparison to defeatValidate("Document Date")Regression suites, all green on the committed build:
139687Recurring Billing Docs Test — 92 passed, 0 failed139688Recurring Billing Test — 46 passed, 0 failed139686Billing Correction Test — 13 passed, 0 failed148455Automated Billing Test — 5 passed, 0 failedAnalyzer output is unchanged: the remaining
AL0920,AW0006,AW0008andAA0139warnings are all pre-existing and in files this PR does not touch.Risk & compatibility
…FromContractprocedures inCreate Billing Documents; the per-customer and per-vendor grouping paths (CreateSalesHeaderForCustomerNo/CreatePurchaseHeaderForVendorNo) never applied contract payment terms and are unaffected.