Fix #7399: Strip VAT from Subscription Line base amount when Prices Including VAT is enabled#7780
Conversation
… Prices Including VAT is enabled When Prices Including VAT is toggled on a Sales Header, the Calculation Base Amount on Sales Subscription Lines was storing the VAT-inclusive unit price instead of the net price. Changes: - CalculateCalculationBaseAmountCustomer: call GetSalesHeader to read the Prices Including VAT flag and strip VAT from the calculated base amount using the formula Amount / (1 + VAT% / 100) when the flag is true. - GetSalesHeader: add override check using SalesHeaderOverride so that an in-memory Sales Header record can be injected before a Modify call (the OnAfterValidateEvent fires before the record is committed to the database). - SetSalesHeader: new procedure matching the SetSalesLine pattern, allowing callers to inject an in-memory Sales Header for accurate PIV reads. - SalesSubscriptionLineMgmt: add event subscriber on Sales Header OnAfterValidateEvent for Prices Including VAT to recalculate all Sales Subscription Lines when the flag is toggled.
pri-kise
left a comment
There was a problem hiding this comment.
Will you add some automatic tests?
| then begin | ||
| OutSalesHeader := SalesHeaderOverride; | ||
| if SalesHeaderOverride."Currency Code" = '' then | ||
| Currency.InitRoundingPrecision() |
There was a problem hiding this comment.
Not the Currency TestFields are missing in this case?
Is this on purpose?
| end; | ||
| end; | ||
| if LocalSalesHeader."Prices Including VAT" then | ||
| CalculatedBaseAmount := Round(CalculatedBaseAmount / (1 + SalesLine."VAT %" / 100), Currency."Unit-Amount Rounding Precision"); |
There was a problem hiding this comment.
Microsoft introduced the procedure GetVATPct in the SalesLine some versions ago.
https://github.com/microsoft/BusinessCentralApps/blob/020503c3e3530c3fe56890c4a409afdf22037b45/App/Layers/W1/BaseApp/Sales/Document/SalesLine.Table.al#L11028-L11036
I'm not sure why the did this, but this used anywhere in the Salesline table.
Therfore I would suggest to use it, too.
…de path
- Replace SalesLine."VAT %" with SalesLine.GetVATPct() to follow the
Microsoft extensibility pattern introduced in SalesLine.Table.al,
which raises OnAfterGetVATPct to allow extensions to override the rate.
- In GetSalesHeader(), the SalesHeaderOverride branch was missing the
same Currency.TestField calls present in the database-read branch.
Add SalesHeaderOverride.TestField("Currency Factor"),
Currency.TestField("Unit-Amount Rounding Precision"), and
Currency.TestField("Amount Rounding Precision") so both branches
are fully symmetric.
There was a problem hiding this comment.
Pull request overview
Fixes Subscription Billing’s Sales Subscription Line “Calculation Base Amount” so it reflects the correct net unit price when Prices Including VAT is enabled, and ensures existing subscription lines are recalculated when the header flag is toggled.
Changes:
- Strip VAT from calculated base amount when
Sales Header."Prices Including VAT"is true. - Add an in-memory
Sales Headeroverride path (SetSalesHeader+SalesHeaderOverride) so calculations during validation read the updated flag value. - Add a
Sales HeaderOnAfterValidateEventsubscriber forPrices Including VATto recalculate existing Sales Subscription Lines.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/Apps/W1/Subscription Billing/App/Sales Service Commitments/Tables/SalesSubscriptionLine.Table.al | Reads Prices Including VAT via GetSalesHeader, strips VAT from base amount, and supports injected Sales Header override via SetSalesHeader. |
| src/Apps/W1/Subscription Billing/App/Sales Service Commitments/Codeunits/SalesSubscriptionLineMgmt.Codeunit.al | Recalculates Sales Subscription Lines when Prices Including VAT is toggled on Sales Header. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| [EventSubscriber(ObjectType::Table, Database::"Sales Header", OnAfterValidateEvent, "Prices Including VAT", false, false)] | ||
| local procedure RecalculateSalesSubscriptionLinesOnAfterSalesHeaderPricesIncludingVATValidate(var Rec: Record "Sales Header"; var xRec: Record "Sales Header"; CurrFieldNo: Integer) | ||
| var | ||
| SalesLine: Record "Sales Line"; | ||
| SalesServiceCommitment: Record "Sales Subscription Line"; | ||
| begin | ||
| if Rec.IsTemporary() then | ||
| exit; | ||
| if Rec."Prices Including VAT" = xRec."Prices Including VAT" then | ||
| exit; | ||
| SalesLine.SetRange("Document Type", Rec."Document Type"); | ||
| SalesLine.SetRange("Document No.", Rec."No."); | ||
| if not SalesLine.FindSet() then | ||
| exit; | ||
| repeat | ||
| SalesServiceCommitment.FilterOnSalesLine(SalesLine); | ||
| if SalesServiceCommitment.FindSet() then | ||
| repeat | ||
| SalesServiceCommitment.SetSalesLine(SalesLine); | ||
| SalesServiceCommitment.SetSalesHeader(Rec); | ||
| SalesServiceCommitment.CalculateCalculationBaseAmount(); | ||
| until SalesServiceCommitment.Next() = 0; | ||
| until SalesLine.Next() = 0; | ||
| end; |
There was a problem hiding this comment.
New behavior (stripping VAT from calculation base amount when Prices Incl. VAT is enabled, and recalculation when toggling the flag) is not covered by automated tests. There is an existing Sales Service Commitment Test suite; adding a test that (1) creates a sales order with non-zero VAT%, Prices Incl. VAT=true and verifies the created Sales Subscription Line base amount is net, and (2) toggles Prices Incl. VAT and verifies base amount updates, would prevent regressions.
| if LocalSalesHeader."Prices Including VAT" then | ||
| CalculatedBaseAmount := Round(CalculatedBaseAmount / (1 + SalesLine.GetVATPct() / 100), Currency."Unit-Amount Rounding Precision"); |
There was a problem hiding this comment.
Prices Including VAT handling here uses SalesLine.GetVATPct() and a generic gross-to-net division. In this app, VAT% is explicitly treated as 0 for Reverse Charge VAT/Sales Tax (see existing VAT buffer logic), and Sales Tax requires SalesTaxCalculate.ReverseCalculateTax(...) to derive the net price. As-is, Sales Tax documents (and potentially reverse charge depending on what GetVATPct() returns) can keep a tax-inclusive base amount or be reverse-calculated incorrectly. Consider branching on SalesLine."VAT Calculation Type": for Sales Tax use ReverseCalculateTax with the sales header’s tax fields; for Reverse Charge VAT treat the effective % as 0 (no stripping).
| SalesLine.SetRange("Document Type", Rec."Document Type"); | ||
| SalesLine.SetRange("Document No.", Rec."No."); | ||
| if not SalesLine.FindSet() then | ||
| exit; | ||
| repeat | ||
| SalesServiceCommitment.FilterOnSalesLine(SalesLine); | ||
| if SalesServiceCommitment.FindSet() then | ||
| repeat | ||
| SalesServiceCommitment.SetSalesLine(SalesLine); | ||
| SalesServiceCommitment.SetSalesHeader(Rec); | ||
| SalesServiceCommitment.CalculateCalculationBaseAmount(); | ||
| until SalesServiceCommitment.Next() = 0; | ||
| until SalesLine.Next() = 0; |
There was a problem hiding this comment.
This subscriber recalculates subscription lines for every sales line on the document and recalculates vendor-partner subscription lines as well. Since Prices Including VAT only affects customer pricing, consider filtering to Partner::Customer (and possibly Process/other relevant filters) before FindSet() to avoid unnecessary queries/modifications, and/or iterating subscription lines via FilterOnDocument(Rec."Document Type", Rec."No.") to reduce work on documents with many non-subscription sales lines.
Three tests added to SalesServiceCommitmentTest (codeunit 139915) that
cover both bugs fixed in this PR:
Bug 1 - Wrong amount at creation:
- BaseAmountIsNetPriceWhenPricesIncludingVATEnabled: verifies that
when Prices Including VAT = true, Calculation Base Amount is
Round(UnitPrice / (1 + VAT% / 100)) not the gross unit price.
- BaseAmountEqualsUnitPriceWhenPricesExcludingVAT: regression guard
confirming that the standard (PIV = false) path is unaffected.
Bug 2 - No recalculation on PIV toggle:
- TogglingPricesIncludingVATRecalculatesBaseAmount: verifies that
validating Prices Including VAT on an existing Sales Header
recalculates all subscription line base amounts in both directions
(false to true, then back to false).
|
Addressing review comments (commits 9be59b7 + 2fc0d32): Re: Currency TestFields missing in override path else begin
SalesHeaderOverride.TestField("Currency Factor");
Currency.Get(SalesHeaderOverride."Currency Code");
Currency.TestField("Unit-Amount Rounding Precision");
Currency.TestField("Amount Rounding Precision");
end;Re: Use GetVATPct() instead of direct field access Automated tests
|
…ubscriber to Customer lines CalculateCalculationBaseAmountCustomer: restrict the gross-to-net division to VAT Calculation Type = Normal VAT. Reverse Charge VAT and Sales Tax are excluded: for Reverse Charge VAT the effective rate is already zero at the line level so no stripping is needed; for Sales Tax the simple division formula does not apply. RecalculateSalesSubscriptionLinesOnAfterSalesHeaderPricesIncludingVATValidate: replace the outer SalesLine loop with FilterOnDocument plus SetRange(Partner::Customer) before FindSet. Each subscription line is retrieved by SalesLine.Get to skip non-subscription lines and avoid unnecessary queries. Vendor-partner lines are not affected by Prices Including VAT and are now correctly excluded.
2fc0d32 to
44f8da6
Compare
Summary
Fixes #7399
When
Prices Including VATis enabled on a Sales Header, theCalculation Base Amounton Sales Subscription Lines was incorrectly storing the VAT-inclusive unit price instead of the net price. Additionally, toggling the flag on an existing document did not trigger recalculation of already-created subscription lines.Root Cause
Bug 1 - Wrong amount at line creation:
CalculateCalculationBaseAmountCustomercopiedSalesLine."Unit Price"directly intoCalculation Base Amountwithout checkingSalesHeader."Prices Including VAT". When the flag is true,SalesLine."Unit Price"is VAT-inclusive, so subscription lines stored a gross price.Bug 2 - No recalculation on PIV toggle:
UpdateSalesServiceCommitmentCalculationBaseAmountinSalesLine.TableExt.alexits early whenUnit Pricehas not changed. TogglingPrices Including VATon the Sales Header does not change the numeric value ofUnit Price(same number, different interpretation), so the early-exit fires and subscription lines are never updated.Changes
CalculateCalculationBaseAmountCustomer: callGetSalesHeaderto read thePrices Including VATflag and strip VAT from the calculated base amount using the formulaAmount / (1 + VAT% / 100)when the flag is true.GetSalesHeader: add an override path usingSalesHeaderOverrideso that an in-memorySales Headerrecord can be injected. This is required becauseOnAfterValidateEventfires before the record is committed to the database, so a directGetwould return the old value.SetSalesHeader: new procedure following the same pattern asSetSalesLine, allowing callers to inject an in-memorySales Headerfor accuratePrices Including VATreads.SalesSubscriptionLineMgmt: add event subscriber onSales Header OnAfterValidateEventforPrices Including VATto recalculate all Sales Subscription Lines when the flag is toggled.How to Test
Prices Including VAT= true.Calculation Base Amount= 100 (net price, VAT stripped).Prices Including VATto false on the Sales Order.Calculation Base Amountupdates to 121 (gross price, no stripping).