mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-10-05 14:46:55 +01:00
knowledge(data-modeling): move Prices Including VAT article to Microsoft layer
data-modeling is a Microsoft-owned review domain consumed by microsoft/skills/review/al-data-modeling-review.md, and docs/contributing.md does not allow Community as a staging layer for such domains. Move the article and its .good.al/.bad.al samples to microsoft/knowledge/data-modeling/. Content is unchanged; the slug-based evaluation entry stays as is.
This commit is contained in:
parent
dda13d5150
commit
51e68cbc02
3 changed files with 0 additions and 0 deletions
|
|
@ -0,0 +1,33 @@
|
|||
// Demonstration only; independently authored, not copied from BaseApp.
|
||||
codeunit 50641 "Sales Doc. VAT Basis Bad"
|
||||
{
|
||||
procedure GetNetAndGrossTotals(SalesHeader: Record "Sales Header"; var NetTotal: Decimal; var GrossTotal: Decimal)
|
||||
var
|
||||
SalesLine: Record "Sales Line";
|
||||
begin
|
||||
SalesLine.SetRange("Document Type", SalesHeader."Document Type");
|
||||
SalesLine.SetRange("Document No.", SalesHeader."No.");
|
||||
if SalesLine.FindSet() then
|
||||
repeat
|
||||
// Wrong: "Line Amount" already includes VAT when the header has Prices Including VAT,
|
||||
// so NetTotal is gross and VAT is added a second time. Invoice discount is also ignored.
|
||||
NetTotal += SalesLine."Line Amount";
|
||||
GrossTotal += SalesLine."Line Amount" * (1 + SalesLine."VAT %" / 100);
|
||||
until SalesLine.Next() = 0;
|
||||
end;
|
||||
|
||||
procedure GetOutstandingNetAmount(SalesLine: Record "Sales Line"): Decimal
|
||||
begin
|
||||
// Wrong: despite its name, CalculateOutstandingAmountExclTax is based on "Line Amount"
|
||||
// and therefore includes VAT on a Prices Including VAT document.
|
||||
exit(SalesLine.CalculateOutstandingAmountExclTax());
|
||||
end;
|
||||
|
||||
procedure SetUnitPriceFromNetSourcePrice(var SalesLine: Record "Sales Line"; NetSourcePrice: Decimal)
|
||||
begin
|
||||
// Wrong: on a Prices Including VAT document this net price is read as a gross price,
|
||||
// so the net line amount drops to NetSourcePrice / (1 + "VAT %" / 100).
|
||||
SalesLine.Validate("Unit Price", NetSourcePrice);
|
||||
SalesLine.Modify(true);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
// Demonstration only; independently authored, not copied from BaseApp.
|
||||
codeunit 50640 "Sales Doc. VAT Basis Good"
|
||||
{
|
||||
procedure GetNetAndGrossTotals(SalesHeader: Record "Sales Header"; var NetTotal: Decimal; var GrossTotal: Decimal)
|
||||
var
|
||||
SalesLine: Record "Sales Line";
|
||||
begin
|
||||
SalesLine.SetRange("Document Type", SalesHeader."Document Type");
|
||||
SalesLine.SetRange("Document No.", SalesHeader."No.");
|
||||
// Amount is always net and "Amount Including VAT" always gross, after line and
|
||||
// invoice discounts, whatever the header's "Prices Including VAT" says.
|
||||
SalesLine.CalcSums(Amount, "Amount Including VAT");
|
||||
NetTotal := SalesLine.Amount;
|
||||
GrossTotal := SalesLine."Amount Including VAT";
|
||||
end;
|
||||
|
||||
procedure GetOutstandingNetAmount(SalesLine: Record "Sales Line"): Decimal
|
||||
var
|
||||
SalesHeader: Record "Sales Header";
|
||||
Currency: Record Currency;
|
||||
begin
|
||||
if SalesLine.Quantity = 0 then
|
||||
exit(0);
|
||||
SalesHeader.Get(SalesLine."Document Type", SalesLine."Document No.");
|
||||
Currency.Initialize(SalesHeader."Currency Code");
|
||||
// Amount is net after line and invoice discounts on every document, so the
|
||||
// uninvoiced share needs no VAT conversion.
|
||||
exit(Round(
|
||||
SalesLine.Amount * (SalesLine.Quantity - SalesLine."Quantity Invoiced") / SalesLine.Quantity,
|
||||
Currency."Amount Rounding Precision"));
|
||||
end;
|
||||
|
||||
procedure SetUnitPriceFromNetSourcePrice(var SalesLine: Record "Sales Line"; NetSourcePrice: Decimal)
|
||||
var
|
||||
SalesHeader: Record "Sales Header";
|
||||
Currency: Record Currency;
|
||||
UnitPrice: Decimal;
|
||||
begin
|
||||
SalesHeader.Get(SalesLine."Document Type", SalesLine."Document No.");
|
||||
// Scope of the conversion below: Normal VAT only; Full VAT and Sales Tax need their own handling.
|
||||
SalesLine.TestField("VAT Calculation Type", SalesLine."VAT Calculation Type"::"Normal VAT");
|
||||
Currency.Initialize(SalesHeader."Currency Code");
|
||||
|
||||
UnitPrice := NetSourcePrice;
|
||||
// "Unit Price" is gross on a Prices Including VAT document: convert the net source price into that basis.
|
||||
if SalesHeader."Prices Including VAT" then
|
||||
UnitPrice := Round(NetSourcePrice * (1 + SalesLine."VAT %" / 100), Currency."Unit-Amount Rounding Precision");
|
||||
|
||||
SalesLine.Validate("Unit Price", UnitPrice);
|
||||
SalesLine.Modify(true);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: data-modeling
|
||||
keywords: [prices-including-vat, unit-price, line-amount, direct-unit-cost, prepmt-line-amount, amount-including-vat, sales-line, purchase-line, service-line, net-gross]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Sales, purchase, and service line prices follow the header's Prices Including VAT
|
||||
|
||||
> Contributions welcome — open a PR to refine or extend this article.
|
||||
|
||||
## Description
|
||||
|
||||
Line prices are gross or net depending on the header's `Prices Including VAT`. When the flag is set, `Unit Price` (sales, service), `Direct Unit Cost` (purchase), `Line Amount`, `Line Discount Amount`, `Inv. Discount Amount`, and the prepayment fields `Prepmt. Line Amount`, `Prepmt. Amt. Inv.`, `Prepmt Amt to Deduct`, and `Prepmt Amt Deducted` all include VAT. When it is cleared they exclude it. Only `Amount`, `VAT Base Amount`, and `Prepayment Amount` (always net) and `Amount Including VAT`, `Prepmt. Amt. Incl. VAT`, and `Prepmt. Amount Inv. Incl. VAT` (always gross) keep a fixed basis. Code that mixes the two groups without looking at the header flag is wrong for every document whose customer or vendor uses the other setting.
|
||||
|
||||
## Best Practice
|
||||
|
||||
When code needs a known basis — exports, integrations, KPIs, custom totals, commission or margin calculations — read `Amount` for net and `Amount Including VAT` for gross. Both are already reduced by line and invoice discounts and are maintained by the line's VAT calculation, so no VAT arithmetic is needed.
|
||||
|
||||
When code combines fields, pair fields of the same group. BaseApp's `UpdatePrepmtAmounts` sets `Prepmt. Line Amount` from `Line Amount` minus `Inv. Discount Amount`. This is correct because all three follow the header flag. `Prepmt. Amt. Inv.` pairs with `Prepmt. Line Amount`; `Prepmt. Amount Inv. Incl. VAT` pairs only with other gross values.
|
||||
|
||||
When code writes a price from an external source whose basis is known (an EDI price list, an API payload, a web-shop order), get the document header and convert the source price into the header's basis before validating `Unit Price` or `Direct Unit Cost`, using the line's `VAT %` and the currency's `Unit-Amount Rounding Precision`. Alternatively, set `Prices Including VAT` on the header to match the source before the first line is created. Toggling it later on a sales header with priced lines asks the user to confirm a recalculation. Without a UI session, or when validation dialogs are hidden, BaseApp converts all line prices without asking.
|
||||
|
||||
The standard price calculation already converts a `Price List Line` whose `Price Includes VAT` differs from the document's setting, so a price it returns is in the document's basis and must not be converted a second time.
|
||||
|
||||
On purchase lines, `Unit Cost` and `Unit Cost (LCY)` are derived from `Direct Unit Cost` with VAT removed, so they stay net. Service lines follow the same rule for `Unit Price` and `Line Amount`: they share the caption switch and the `UpdateVATAmounts` split of the sales line.
|
||||
|
||||
See sample: [`document-line-prices-follow-prices-including-vat.good.al`](document-line-prices-follow-prices-including-vat.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Treating `Unit Price`, `Direct Unit Cost`, or `Line Amount` as net by default. Typical signals: summing `Line Amount` as a document's net total, computing VAT as `Line Amount * "VAT %" / 100`, putting a known net or gross external price straight into `Unit Price` or `Direct Unit Cost`, or comparing a line price with `Item."Unit Price"` or `Item."Last Direct Cost"` — all without reading the header's `Prices Including VAT`. For a gross-price customer at 19 % VAT, a net total built from `Line Amount` is 19 % too high, and a net price of 100 imported unconverted yields a net revenue of only 84.03.
|
||||
|
||||
Trusting a name instead of the source fields. The public procedure `CalculateOutstandingAmountExclTax` on `Sales Line` and `Purchase Line` returns `Line Amount` minus `Inv. Discount Amount` for the uninvoiced quantity, so its result includes VAT on a `Prices Including VAT` document despite its name. BaseApp only combines it with `Prepmt. Line Amount`, which has the same basis. Extension code that uses it as a net outstanding amount — compared with `Amount`, exported as net, or used to compute VAT — has this defect. The same applies to any variable or procedure named `ExclVAT`, `ExclTax`, or `Net` that is fed from a header-dependent field.
|
||||
|
||||
Do not report code that reads the header flag, uses only fixed-basis fields, or only combines fields of the same group (for example, prepayment amounts derived from `Line Amount`, or values copied between two lines of the same document).
|
||||
|
||||
See sample: [`document-line-prices-follow-prices-including-vat.bad.al`](document-line-prices-follow-prices-including-vat.bad.al).
|
||||
|
||||
## References
|
||||
|
||||
- [BCApps: Sales Header `Prices Including VAT` OnValidate recalculates line prices](https://github.com/microsoft/BCApps/blob/main/src/Layers/W1/BaseApp/Sales/Document/SalesHeader.Table.al).
|
||||
- [BCApps: Sales Line `UpdateVATAmounts`, `UpdatePrepmtAmounts`, and `CalculateOutstandingAmountExclTax`](https://github.com/microsoft/BCApps/blob/main/src/Layers/W1/BaseApp/Sales/Document/SalesLine.Table.al).
|
||||
- [BCApps: `Sales Line CaptionClass Mgmt` switches captions to Incl./Excl. VAT](https://github.com/microsoft/BCApps/blob/main/src/Layers/W1/BaseApp/Sales/Document/SalesLineCaptionClassMgmt.Codeunit.al).
|
||||
- [BCApps: Purchase Line `UpdateUnitCost` removes VAT from `Direct Unit Cost`](https://github.com/microsoft/BCApps/blob/main/src/Layers/W1/BaseApp/Purchases/Document/PurchaseLine.Table.al).
|
||||
- [BCApps: Service Line `GetCaptionClass` and `UpdateVATAmounts`](https://github.com/microsoft/BCApps/blob/main/src/Layers/W1/BaseApp/Service/Document/ServiceLine.Table.al).
|
||||
- [BCApps: `Price Calculation Buffer Mgt.` `ConvertAmountByTax`](https://github.com/microsoft/BCApps/blob/main/src/Layers/W1/BaseApp/Pricing/Calculation/PriceCalculationBufferMgt.Codeunit.al).
|
||||
Loading…
Add table
Add a link
Reference in a new issue