Announcing the launch of the Saudi Zatca-2 E-Invoicing app on FrappeCloud

@Husna

Errors:

XML-INVOICE-ERROR: XML Submitted using reporting API is not a simplified document

:wrench: 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

:pushpin: 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 :cross_mark:

:right_arrow: Difference: 0.01


:warning: Important Note (Critical)

This issue mainly occurs when using foreign currencies (e.g., USD) instead of the base currency (SAR).

:backhand_index_pointing_right: 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

:right_arrow: Therefore, this issue must be handled carefully for multi-currency invoices


:magnifying_glass_tilted_left: 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)

:cross_mark: This uses float rounding, which causes precision errors (especially in USD cases)


:warning: Important Observation

There is already a correct implementation in:

tax_data_with_template(…)

Which uses:

Decimal(…).quantize(Decimal(“0.01”), rounding=ROUND_HALF_UP)

:check_mark: This is compliant with ZATCA requirements
:check_mark: Prevents rounding mismatch
:check_mark: Works correctly even with foreign currencies


:police_car_light: 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(…)

:right_arrow: This means:

  • Most invoices (without Item Tax Template) use incorrect logic

  • This is especially problematic for USD invoices


:firecracker: Additional Issue

In “create_xml_final_part.py”, line-level rounding exists:

cbc:RoundingAmount = lineextensionamount + taxamount

:cross_mark: This field:

  • Is not required by ZATCA

  • May introduce inconsistencies in totals


:white_check_mark: Required Fix

  1. 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)


  1. Ensure Consistency Rule (ZATCA Requirement)

Always enforce:

TaxInclusiveAmount = TaxExclusiveAmount + TaxAmount
PayableAmount = TaxInclusiveAmount


  1. (Recommended) Remove RoundingAmount

cbc:RoundingAmount…</cbc:RoundingAmount>


  1. (Optional but Strongly Recommended)

Unify logic:

Avoid splitting logic

tax_data(…) :cross_mark:
tax_data_with_template(…) :white_check_mark:

Use a single Decimal-based implementation


:bullseye: Expected Result

After fix:

  • No rounding mismatch (0.01 issues gone)

  • Correct handling for USD and multi-currency invoices

  • Full compliance with ZATCA rules


:high_voltage: Summary

Issue| Status
Float rounding| :cross_mark: Incorrect
Decimal rounding| :white_check_mark: Correct
USD handling| :cross_mark: Currently problematic
Required action| :wrench: 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.