Summary
When a CFDI declares two <cfdi:Traslado> entries with the same tax, factor type and rate, but with the rate written in different notations (TasaOCuota="0.16" vs "0.160000"), the parser normalizes the rate when building the index key. Both entries collapse into the same key and the second one silently overwrites the first, so the amount of the overwritten entry is lost.
This is not a malformed document: it is valid CFDI 4.0 and the SAT accepts and stamps it. The TotalImpuestosTrasladados attribute of the same node states the amount that should be there.
Reproduction
Minimal synthetic CFDI (all data invented — test RFCs, made-up amounts):
minimo.xml
<?xml version="1.0" encoding="UTF-8"?>
<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/4"
Version="4.0" Fecha="2026-01-01T00:00:00" SubTotal="100.00" Total="116.00"
Moneda="MXN" TipoDeComprobante="I" Exportacion="01" LugarExpedicion="00000"
Sello="SELLO_DE_PRUEBA" Certificado="CERT_DE_PRUEBA" NoCertificado="00000000000000000000"
Folio="1" Serie="TEST" FormaPago="01" MetodoPago="PUE">
<cfdi:Emisor Rfc="AAA010101AAA" Nombre="EMISOR DE PRUEBA" RegimenFiscal="601"/>
<cfdi:Receptor Rfc="BBB010101BBB" Nombre="RECEPTOR DE PRUEBA"
DomicilioFiscalReceptor="00000" RegimenFiscalReceptor="601" UsoCFDI="G03"/>
<cfdi:Conceptos>
<cfdi:Concepto ClaveProdServ="01010101" Cantidad="1" ClaveUnidad="H87"
Descripcion="A" ValorUnitario="40.00" Importe="40.00" ObjetoImp="02">
<cfdi:Impuestos><cfdi:Traslados>
<cfdi:Traslado Base="40.00" Impuesto="002" TipoFactor="Tasa" TasaOCuota="0.160000" Importe="6.40"/>
</cfdi:Traslados></cfdi:Impuestos>
</cfdi:Concepto>
<cfdi:Concepto ClaveProdServ="01010101" Cantidad="1" ClaveUnidad="H87"
Descripcion="B" ValorUnitario="60.00" Importe="60.00" ObjetoImp="02">
<cfdi:Impuestos><cfdi:Traslados>
<cfdi:Traslado Base="60.00" Impuesto="002" TipoFactor="Tasa" TasaOCuota="0.16" Importe="9.60"/>
</cfdi:Traslados></cfdi:Impuestos>
</cfdi:Concepto>
</cfdi:Conceptos>
<cfdi:Impuestos TotalImpuestosTrasladados="16.00">
<cfdi:Traslados>
<cfdi:Traslado Base="40.00" Impuesto="002" TipoFactor="Tasa" TasaOCuota="0.160000" Importe="6.40"/>
<cfdi:Traslado Base="60.00" Impuesto="002" TipoFactor="Tasa" TasaOCuota="0.16" Importe="9.60"/>
</cfdi:Traslados>
</cfdi:Impuestos>
</cfdi:Comprobante>
from satcfdi.cfdi import CFDI
c = CFDI.from_file("minimo.xml")
tr = c["Impuestos"]["Traslados"]
len(tr) # -> 1 (expected 2)
list(tr) # -> ['002|Tasa|0.160000']
tr['002|Tasa|0.160000']['Importe'] # -> 9.60 (the first Traslado is gone)
sum(v['Importe'] for v in tr.values()) # -> 9.60
c["Impuestos"]["TotalImpuestosTrasladados"] # -> 16.00
Expected: the sum of the parsed traslados equals TotalImpuestosTrasladados (16.00).
Actual: 9.60. The Base="40.00" / Importe="6.40" entry is dropped with no error and no warning.
Why it matters
The failure is silent: no exception, no warning, and the resulting object looks perfectly well-formed. A consumer summing Traslados gets a smaller tax figure and has no way of noticing unless it independently compares the sum against TotalImpuestosTrasladados.
We hit this in production on real stamped invoices while investigating what looked like a double-counting bug in our own parser. It was not ours — the amounts were being lost upstream, not duplicated. The XML above is a synthetic reduction of that case.
Suggested fix
When the normalized key already exists, accumulate Base and Importe instead of replacing the entry (or keep the traslados as a list and let consumers group them).
⚠️ One trap worth flagging, because it is the first fix that comes to mind and it makes things worse: do not deduplicate by (Base, Importe). In two of the real documents we measured, the two traslados carried identical Base and identical Importe and differed only in the rate notation — they are legitimately two separate entries. Deduplicating them would halve the tax instead of fixing it.
Comparing rates as Decimal (Decimal("0.16") == Decimal("0.160000")) is the right way to decide that two entries belong together; the values still have to be summed, never discarded.
Possibly related
#29 (Tasa 0.16 genera excepción "Unkown Impuesto" en REP) is a different failure, but it comes from the same place: the notation used for TasaOCuota in an otherwise valid document. Issuers do write both 0.16 and 0.160000, sometimes within the same comprobante.
Environment
satcfdi 4.9.8
- Python 3.12
- CFDI 4.0
Summary
When a CFDI declares two
<cfdi:Traslado>entries with the same tax, factor type and rate, but with the rate written in different notations (TasaOCuota="0.16"vs"0.160000"), the parser normalizes the rate when building the index key. Both entries collapse into the same key and the second one silently overwrites the first, so the amount of the overwritten entry is lost.This is not a malformed document: it is valid CFDI 4.0 and the SAT accepts and stamps it. The
TotalImpuestosTrasladadosattribute of the same node states the amount that should be there.Reproduction
Minimal synthetic CFDI (all data invented — test RFCs, made-up amounts):
minimo.xmlExpected: the sum of the parsed traslados equals
TotalImpuestosTrasladados(16.00).Actual: 9.60. The
Base="40.00" / Importe="6.40"entry is dropped with no error and no warning.Why it matters
The failure is silent: no exception, no warning, and the resulting object looks perfectly well-formed. A consumer summing
Trasladosgets a smaller tax figure and has no way of noticing unless it independently compares the sum againstTotalImpuestosTrasladados.We hit this in production on real stamped invoices while investigating what looked like a double-counting bug in our own parser. It was not ours — the amounts were being lost upstream, not duplicated. The XML above is a synthetic reduction of that case.
Suggested fix
When the normalized key already exists, accumulate
BaseandImporteinstead of replacing the entry (or keep the traslados as a list and let consumers group them).(Base, Importe). In two of the real documents we measured, the two traslados carried identicalBaseand identicalImporteand differed only in the rate notation — they are legitimately two separate entries. Deduplicating them would halve the tax instead of fixing it.Comparing rates as
Decimal(Decimal("0.16") == Decimal("0.160000")) is the right way to decide that two entries belong together; the values still have to be summed, never discarded.Possibly related
#29 (
Tasa 0.16 genera excepción "Unkown Impuesto" en REP) is a different failure, but it comes from the same place: the notation used forTasaOCuotain an otherwise valid document. Issuers do write both0.16and0.160000, sometimes within the same comprobante.Environment
satcfdi4.9.8