Skip to content

fix: reconcile PEPPOL monetary totals on sub-cent invoices (BR-CO-10/15/16)#47

Open
maasanto wants to merge 4 commits into
prilk-consulting:developfrom
maasanto:fix/peppol-2decimal-rounding-totals
Open

fix: reconcile PEPPOL monetary totals on sub-cent invoices (BR-CO-10/15/16)#47
maasanto wants to merge 4 commits into
prilk-consulting:developfrom
maasanto:fix/peppol-2decimal-rounding-totals

Conversation

@maasanto

@maasanto maasanto commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Root cause

A Sales Invoice whose amounts carry three decimals (e.g. a 607.025 line at 21% VAT) fails Peppol/EN16931 validation on submit, because each monetary field is rounded to 2 decimals on its own and the rounded parts no longer reconcile:

  • the line LineExtensionAmount (BT-131) used f"{x:.2f}", which bypasses frappe flt and can round the raw float differently from the flt-rounded document LineExtensionAmount (BT-106), so the two disagree on a single sub-cent line (BR-CO-10);
  • with several 3-decimal lines, the sum of the independently-rounded line amounts (sum-of-rounds) no longer equals the rounded document total (round-of-sum), so BT-106 = Σ BT-131 breaks even after both sides use flt (BR-CO-10);
  • TaxInclusiveAmount (BT-112) was taken straight from grand_total, but TaxExclusive + VAT, each rounded on its own, lands on a different cent (607.03 + 127.48 = 734.51 ≠ 734.50) (BR-CO-15).

EN16931 forces this tension: BR-DEC-09/23 cap BT-106 and BT-131 at 2 decimals (a 3-decimal amount is illegal in the XML), while BR-CO-10 requires the rounded values to still add up exactly.

Fix

  • reconcile the lines so they sum exactly to the document total: each line is rounded through flt to 2 decimals and the ≤1-cent-per-line rounding residual is distributed with the largest-remainder method, so Σ BT-131 = BT-106 holds for any number of lines at any precision (BR-CO-10). Each line moves by at most a cent — well inside PEPPOL-EN16931-R120's ±0.02 quantity×price tolerance — and the document totals stay authoritative (BT-109/BT-110/BT-112 are left untouched);
  • derive TaxInclusiveAmount as TaxExclusive + total VAT instead of reading grand_total, so it reconciles by construction (BR-CO-15);
  • emit PayableRoundingAmount (BT-114) for the ≤1-cent residual, keeping PayableAmount equal to the real amount due (BR-CO-16). Prepaid is measured against rounded_total so partially-paid invoices with a rounding adjustment also balance.

Plain 2-decimal invoices are unaffected: same totals, no line redistribution, no rounding line emitted.

Tests

test_generator.py drives _set_totals across the sub-cent case, rounded-up/down totals, partially-paid, fully-paid, the disabled-round-off fallback, credit notes, and UBL element ordering, asserting the BR-CO-15/16 identities each time. A second suite drives the per-line reconciliation across multi-line sub-cent invoices (two/three lines, mixed magnitudes, a credit note, a deduction line), asserting Σ BT-131 = BT-106 and that every line stays within R120's tolerance.

Validated end-to-end on a sandbox as well: invoices generated for all these scenarios at 3-decimal currency precision pass the bundled CEN-EN16931 + PEPPOL-EN16931 Schematron with no rounding or decimal-place violations.

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.

1 participant