Errors:
XML-INVOICE-ERROR: XML Submitted using reporting API is not a simplified document
ZATCA XML Rounding Issue – Required Fix
i FIXD IT AND TEST IT BY UPDATE THIS FILE TOU CAN FIND IT HERE
ZATCA XML Rounding Issue – Required Fix · Issue #213 · ERPGulf/zatca_erpgulf
Problem Description
We are facing a ZATCA rejection (BR-CO-15) due to a rounding mismatch in the generated XML.
Example:
-
TaxExclusiveAmount: 2337348.90
-
TaxAmount: 350602.34
-
Expected TaxInclusiveAmount: 2687951.24
-
Generated TaxInclusiveAmount: 2687951.23

Difference: 0.01
Important Note (Critical)
This issue mainly occurs when using foreign currencies (e.g., USD) instead of the base currency (SAR).
When the invoice currency is USD (or any foreign currency):
-
Floating-point precision issues become more likely
-
Conversion + tax calculation increases rounding errors
-
ZATCA validation becomes stricter on totals
Therefore, this issue must be handled carefully for multi-currency invoices
Root Cause (Code-Level)
The issue originates from the function:
tax_data(…)
Inside:
zatca_erpgulf/zatca_erpgulf/xml_tax_data.py
Current implementation uses:
round(total + tax, 2)
This uses float rounding, which causes precision errors (especially in USD cases)
Important Observation
There is already a correct implementation in:
tax_data_with_template(…)
Which uses:
Decimal(…).quantize(Decimal(“0.01”), rounding=ROUND_HALF_UP)
This is compliant with ZATCA requirements
Prevents rounding mismatch
Works correctly even with foreign currencies
Critical Issue
In “sign_invoice.py”, the routing logic is:
if not any_item_has_tax_template:
invoice = tax_data(…)
else:
invoice = tax_data_with_template(…)
This means:
-
Most invoices (without Item Tax Template) use incorrect logic
-
This is especially problematic for USD invoices
Additional Issue
In “create_xml_final_part.py”, line-level rounding exists:
cbc:RoundingAmount = lineextensionamount + taxamount
This field:
-
Is not required by ZATCA
-
May introduce inconsistencies in totals
Required Fix
- Fix “tax_data(…)”
Replace ALL float rounding logic with:
from decimal import Decimal, ROUND_HALF_UP
total_amount = Decimal(str(sales_invoice_doc.total))
discount_amount = Decimal(str(sales_invoice_doc.get(“discount_amount”, 0.0)))
tax_amount = Decimal(str(tax_amount_without_retention)).quantize(
Decimal(“0.01”), rounding=ROUND_HALF_UP
)
tax_inclusive_amount = (
abs(total_amount - discount_amount) + tax_amount
).quantize(Decimal(“0.01”), rounding=ROUND_HALF_UP)
cbc_taxinclusiveamount.text = str(tax_inclusive_amount)
cbc_payableamount.text = str(tax_inclusive_amount)
- Ensure Consistency Rule (ZATCA Requirement)
Always enforce:
TaxInclusiveAmount = TaxExclusiveAmount + TaxAmount
PayableAmount = TaxInclusiveAmount
- (Recommended) Remove RoundingAmount
cbc:RoundingAmount…</cbc:RoundingAmount>
- (Optional but Strongly Recommended)
Unify logic:
Avoid splitting logic
tax_data(…) ![]()
tax_data_with_template(…) ![]()
Use a single Decimal-based implementation
Expected Result
After fix:
-
No rounding mismatch (0.01 issues gone)
-
Correct handling for USD and multi-currency invoices
-
Full compliance with ZATCA rules
Summary
Issue| Status
Float rounding|
Incorrect
Decimal rounding|
Correct
USD handling|
Currently problematic
Required action|
Fix tax_data
Please implement the fix in “tax_data(…)” to ensure correct behavior for foreign currencies (USD) and full ZATCA compliance.
Please change the calculation logic so that “TaxInclusiveAmount” and “PayableAmount” are not derived from any precomputed original total.
Instead, calculate them strictly as:
TaxInclusiveAmount = TaxExclusiveAmount + TaxAmount
PayableAmount = TaxInclusiveAmount
Where:
-
“TaxExclusiveAmount” = amount before VAT
-
“TaxAmount” = the already calculated and rounded VAT amount
This avoids mismatches in foreign currency invoices such as USD and ensures that the final total is always built from the actual XML tax values.
Suggested logic:
tax_exclusive_amount = Decimal(str(abs(taxable_amount_1))).quantize(
Decimal(“0.01”), rounding=ROUND_HALF_UP
)
tax_amount_decimal = Decimal(str(abs(tax_amount_without_retention))).quantize(
Decimal(“0.01”), rounding=ROUND_HALF_UP
)
tax_inclusive_amount = (tax_exclusive_amount + tax_amount_decimal).quantize(
Decimal(“0.01”), rounding=ROUND_HALF_UP
)
cbc_taxinclusiveamount.text = str(tax_inclusive_amount)
cbc_payableamount.text = str(tax_inclusive_amount)
This way, the final amount is always mathematically consistent with the XML tax values and will avoid BR-CO-15 rounding mismatches.