Repository navigation
knowledge(data-modeling): code that calculates a unit price rounds it to the currency's Unit-Amount Rounding Precision - #223
Conversation
… to the currency's Unit-Amount Rounding Precision Only BaseApp's own price computations round a unit price; Validate/assignment and DecimalPlaces do not. A subscriber that handles OnUpdateUnitPriceOnBeforeFindPrice / OnUpdateDirectUnitCostOnBeforeFindPrice skips RoundPrice, so Quantity x displayed price no longer equals Line Amount. Adds the article, a good/bad sample pair, a worklist cue in al-data-modeling-review, and the review-fixtures registration. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Jesper Schulz-Wedde (JesperSchulz)
left a comment
There was a problem hiding this comment.
Reviewed 885c5896da8fff3f6edeabc331a2d73ff9fa05fb. The unit-amount rounding mechanic and field-validation caveat are well supported by the cited Base Application paths. One merge-critical problem remains in the canonical good sample at round-calculated-unit-prices-to-unit-amount-precision.good.al:15-26.
The sample computes a net price from Item."Unit Cost" plus markup/UOM and currency conversion, then assigns it to SalesLine."Unit Price" and sets IsHandled := true without considering SalesHeader."Prices Including VAT". That bypasses the standard calculation's tax-basis conversion along with its rounding. For LCY, UOM=1, net cost 100, markup 1.35 and Normal VAT 20%, the sample stores 135 as the gross price on an including-VAT document instead of 162; subsequent standard validation derives net 112.50 rather than the intended 135. This conflicts with the existing document-line-prices-follow-prices-including-vat guidance and makes the supposedly clean fixture an underpricing example.
Please either explicitly restrict both companions to excluding-VAT headers (an executable guard or leave including-VAT cases to the standard handler), or convert the calculated net price to the header's basis under an explicit supported VAT-calculation scope before the single final Round. Keep the two companions matched so their intentional difference remains rounding, and keep VAT conversion before the final round. The article's core rounding rule does not need to change.
Summary
Business Central rounds a unit price only where its own code computes one. The main place is the standard price calculation (
Price Calculation Buffer Mgt.RoundPrice, using the currency'sUnit-Amount Rounding Precision, or General Ledger Setup's when the currency code is blank). Code that assigns or validatesSales Line."Unit Price"orPurchase Line."Direct Unit Cost"with a computed value stores every decimal. The fieldOnValidateonly re-validatesLine Discount %, andDecimalPlaces/AutoFormatTypedo not round a value set from code. The line amount is still rounded toAmount Rounding Precision. A stored 49.7536 shown as 49.75 therefore gives a line amount of 199.01 for 4 units, while the document implies 199.00. Nothing fails in BC. The mismatch shows up at the receiver: e-invoice validation, customs, or EDI.The widest gap is a subscriber to
OnUpdateUnitPriceOnBeforeFindPrice/OnUpdateDirectUnitCostOnBeforeFindPricethat setsIsHandled := true, because it skips the calculation together with its rounding. This complementsevents/do-not-bypass-critical-operations-with-ishandled, which looks at the same pattern from the publisher's side: a subscriber that takes over a calculation also takes over the guarantees that calculation gave.What this adds
microsoft/knowledge/data-modeling/round-calculated-unit-prices-to-unit-amount-precision.md, with a good/bad sample pair that differs only inCurrency.Initialize+Round(..., "Unit-Amount Rounding Precision").OnUpdateUnitPriceByFieldOnAfterFindPriceover replacing it.CopyUnitPriceAndLineDiscountPct).CalcUnitPriceUsingUOMCoef, which rescales a posted invoice price for a credit memo without rounding.al-data-modeling-review. The cue's trigger is the anti-pattern: an unrounded computed assignment, or a handled subscriber.document-line-prices-follow-prices-including-vatcovers the net/gross basis of the same fields. This article covers their precision.Evidence
837ef80):PriceCalculationBufferMgt.Codeunit.alL109–129, andConvertAmount→RoundPriceat L166.SalesLine.Table.al:OnValidateL933–950UpdateUnitPriceByFieldL5328–5380 (event at L5356;Validate("Unit Price")at L5376)Unit Costrounding L993–1003CalcUnitPriceUsingUOMCoefL11043–11055PurchaseLine.Table.al: field 22 L785–801, andUpdateDirectUnitCostByFieldL5242–5304.SalesHeader.Table.al: thePrices Including VATconversion rounds, L1054–1055.OnUpdateUnitPriceOnBeforeFindPrice.Validate("Unit Price", 49.7536)and direct assignment both store 49.7536, with Line Amount = 199.01 at Quantity 4.OnUpdateUnitPriceOnBeforeFindPricesubscriber stores 49.7536 / 199.01 although the currency's unit precision is 0.01.DecimalPlaces = 2 : 2stores 49.7536 when set from code, viaValidateor:=.Applies to: all BC versions.
Test plan
validate_frontmatter.py: 0 errors (2 warnings, both in files this PR doesn't touch)Test-KnowledgeIndex.ps1,Test-SkillIndex.ps1,Test-ReviewContract.ps1,Test-KnowledgeRetrieval.ps1Test-ReviewFixtures.ps1: 254 cases, including the-PrepareDirectoryrun🤖 Generated with Claude Code