What BR-CO-17 checks
BR-CO-17 is the multiplication inside each VAT breakdown group: VAT category tax amount (BT-117) = VAT category taxable amount (BT-116) x (VAT category rate (BT-119) / 100), rounded to two decimals. An EN 16931 invoice carries one breakdown group per category and rate; this rule makes each group internally consistent, so the buyer can recompute the VAT from the declared base and rate and get the declared amount.
Layer and formats
An en16931 core layer rule for every syntax and profile. It works hand in hand with BR-CO-14, which sums the groups into the invoice VAT total, and with the per-category families (BR-S-08 for standard rate, BR-AE, BR-E and friends) that police which lines feed which group.
Why it fails in practice
- Per-line VAT rounding. VAT is computed and rounded on every line, then summed into the group; the group's own base times rate differs by a cent from that sum.
- Float arithmetic. Binary floating point turns 19% of 105.55 into 20.054499999, and the wrong rounding direction surfaces exactly at the second decimal.
- Rate drift. The group declares 19% while the amounts were computed with 16% (or a temporary rate) somewhere upstream, so the multiplication cannot hold.
How to fix it
Compute VAT per category with decimal arithmetic: sum the category's net amounts into BT-116, multiply by the rate once, round once, and emit that as BT-117.
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="EUR">970.00</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="EUR">184.30</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>19</cbc:Percent>
<cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
In an InvoiceXML create request, leave invoice.vatBreakdowns out and the API builds the groups from the line VAT information server-side; supplied groups are validated so an inconsistency comes back as a finding, not as a broken document.
The raw rejection vs the InvoiceXML finding
The Schematron report:
<svrl:failed-assert id="BR-CO-17" flag="fatal"
location="/*:Invoice[namespace-uri()='...']/cac:TaxTotal[1]/cac:TaxSubtotal[1]">
<svrl:text>[BR-CO-17]-VAT category tax amount (BT-117) =
VAT category taxable amount (BT-116) x (VAT category rate
(BT-119) / 100), rounded to two decimals.</svrl:text>
</svrl:failed-assert>
The InvoiceXML finding for the same failure:
{
"rule": "BR-CO-17",
"layer": "en16931",
"line": null,
"message": "The VAT category tax amount must equal the taxable amount multiplied by the VAT rate, divided by 100. Check VAT calculations.",
"btCodes": ["BT-117", "BT-116", "BT-119"],
"fields": null,
"raw": "[BR-CO-17] EN16931: VAT category tax amount (BT-117) = VAT category taxable amount (BT-116) x (VAT category rate (BT-119) / 100), rounded to two decimals."
}
The message is ready for an end user and the BT codes point at the breakdown fields involved. No rule-code lookup tables in your integration, and localization stays a per-rule concern for clients that want it.
Check or generate documents with consistent VAT
Run a file through the UBL validator or the XRechnung validator to see the per-group findings. Generation via POST /v1/create/ubl or POST /v1/create/facturx computes each group with a single rounding step server-side. The category model is covered in EN 16931 explained.