Skip to content

knowledge(data-modeling): code that calculates a unit price rounds it to the currency's Unit-Amount Rounding Precision - #223

Open
Michael Dieringer (MichaelDieringer) wants to merge 1 commit into
microsoft:mainfrom
Curabis:community-contribution/round-calculated-unit-prices
Open

Michael Dieringer (MichaelDieringer) wants to merge 1 commit into
microsoft:mainfrom
Curabis:community-contribution/round-calculated-unit-prices

Conversation

@MichaelDieringer

Copy link
Copy Markdown
Contributor

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's Unit-Amount Rounding Precision, or General Ledger Setup's when the currency code is blank). Code that assigns or validates Sales Line."Unit Price" or Purchase Line."Direct Unit Cost" with a computed value stores every decimal. The field OnValidate only re-validates Line Discount %, and DecimalPlaces/AutoFormatType do not round a value set from code. The line amount is still rounded to Amount 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 / OnUpdateDirectUnitCostOnBeforeFindPrice that sets IsHandled := true, because it skips the calculation together with its rounding. This complements events/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 in Currency.Initialize + Round(..., "Unit-Amount Rounding Precision").
  • Best practice:
    • Round once, last, after every factor, to the precision of the document's currency.
    • Prefer adjusting the standard result in OnUpdateUnitPriceByFieldOnAfterFindPrice over replacing it.
  • Exclusions, each grounded in BCApps:
    • Values copied unchanged from an already-rounded field (CopyUnitPriceAndLineDiscountPct).
    • Prices taken unchanged from an authoritative external document (EDI order, vendor invoice with 4 decimals), including an exact minor-units / 100 representation change.
    • BaseApp's own CalcUnitPriceUsingUOMCoef, which rescales a posted invoice price for a credit memo without rounding.
    • Prices returned by the standard calculation.
  • Wiring:
    • A worklist cue plus tokens in al-data-modeling-review. The cue's trigger is the anti-pattern: an unrounded computed assignment, or a handled subscriber.
    • Registration in the data-modeling review-fixtures override.
  • Sibling: document-line-prices-follow-prices-including-vat covers the net/gross basis of the same fields. This article covers their precision.

Evidence

  • BCApps (links pinned to 837ef80):
    • PriceCalculationBufferMgt.Codeunit.al L109–129, and ConvertAmount → RoundPrice at L166.
    • SalesLine.Table.al:
      • field 22 OnValidate L933–950
      • UpdateUnitPriceByField L5328–5380 (event at L5356; Validate("Unit Price") at L5376)
      • line amount L5884
      • Unit Cost rounding L993–1003
      • CalcUnitPriceUsingUOMCoef L11043–11055
    • PurchaseLine.Table.al: field 22 L785–801, and UpdateDirectUnitCostByField L5242–5304.
    • SalesHeader.Table.al: the Prices Including VAT conversion rounds, L1054–1055.
    • No BCApps app subscribes to OnUpdateUnitPriceOnBeforeFindPrice.
  • Learn:
    • "Set up currencies": "The unit-amount rounding feature is used automatically every time you enter an item or resource number on a sales line." The broader statement that "all unit amounts in the currency are rounded" only describes that path.
    • "DecimalPlaces Property": "This setting is evaluated on text boxes and fields during validation."
  • Empirical, BC 28.5 (build 55594), Price Calculation V16, test codeunit run in a sandbox:
    • Standard calculation rounds to 0.01 and 0.001 (24.20382607 → 24.20; 38.89161157 → 38.892).
    • Validate("Unit Price", 49.7536) and direct assignment both store 49.7536, with Line Amount = 199.01 at Quantity 4.
    • A handled OnUpdateUnitPriceOnBeforeFindPrice subscriber stores 49.7536 / 199.01 although the currency's unit precision is 0.01.
    • A field with DecimalPlaces = 2 : 2 stores 49.7536 when set from code, via Validate or :=.
    • Entry through a TestPage stores 49.7536 too. The web client was not tested.
    • Control: 4 × 49.75 = 199.00.

Applies to: all BC versions.

Test plan

  • Samples compiled with AL compiler 30.0 against Base Application 28.5 symbols (0 diagnostics; negative control confirmed the compiler reports errors)
  • 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.ps1
  • Test-ReviewFixtures.ps1: 254 cases, including the -PrepareDirectory run

🤖 Generated with Claude Code

… 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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants