Skip to content

pyfepdf: KeyError en ProcesarPlantilla con items exentos o no gravados (iva_id 1 o 2) #157

Description

@alebarbieri132

Resumen

FEPDF.ProcesarPlantilla() lanza KeyError cuando un ítem del detalle tiene iva_id 1 (No Gravado) o 2 (Exento).

ivas_ds (pyfepdf.py:205) mapea sólo las alícuotas con porcentaje:

ivas_ds = {3: 0, 4: 10.5, 5: 21, 6: 27, 8: 5, 9: 2.5}

Los códigos 1 y 2 son valores válidos de la tabla FEParamGetTiposIva de AFIP, pero no tienen porcentaje asociado, así que no están en ese diccionario. El acceso de pyfepdf.py:1336 no los contempla:

imp_it_iva = Decimal(it['imp_iva'] or "0.00")
if not imp_it_iva and it['importe']:
    p = self.ivas_ds[int(it['iva_id'])]   # KeyError con iva_id 1 o 2

La rama existe para despejar el IVA contenido en el importe bruto de los ítems que no informan imp_iva. Un ítem exento entra igual, porque su imp_iva es 0 por definición, no por omisión — y ahí revienta.

Reproducción

Script autocontenido, sólo con la API pública. Se ejecuta desde el directorio de instalación de la librería:

import os, sys
os.chdir("/code/pyafipws")          # ajustar al directorio de instalación
sys.path.insert(0, "/code")

from pyafipws.pyfepdf import FEPDF

fepdf = FEPDF()
fepdf.CargarFormato("plantillas/factura.csv")
fepdf.CUIT = "20267565393"

fepdf.CrearFactura(
    concepto=1, tipo_doc=80, nro_doc="20267565393",
    tipo_cbte=1, punto_vta=1, cbte_nro=1,
    imp_total=221.00, imp_neto=100.00, imp_iva=21.00,
    imp_op_ex=100.00,
    fecha_cbte="20260811", cae="12345678901234", fch_venc_cae="20260821",
    nombre_cliente="Cliente Ejemplo",
)

# Ítem gravado al 21% con imp_iva informado: no entra a la rama del despeje.
fepdf.AgregarDetalleItem(None, None, "1", "Item gravado", 1, 7, 100.00, 0.00,
                         iva_id=5, imp_iva=21.00, importe=100.00, despacho="")

# Ítem EXENTO (iva_id=2), imp_iva=0 -> KeyError: 2
fepdf.AgregarDetalleItem(None, None, "2", "Item exento", 1, 7, 100.00, 0.00,
                         iva_id=2, imp_iva=0, importe=100.00, despacho="")

fepdf.CrearPlantilla(papel="A4", orientacion="portrait")
fepdf.ProcesarPlantilla(num_copias=1, lineas_max=24, qty_pos="izq")

print("EXCEPCION:", fepdf.Excepcion)
print("TRACEBACK:", fepdf.Traceback)

Salida:

EXCEPCION: KeyError: 2
TRACEBACK: Traceback (most recent call last):
  File ".../pyafipws/pyfepdf.py", line 1336, in ProcesarPlantilla
    p = self.ivas_ds[int(it['iva_id'])]
KeyError: 2

Con iva_id=1 (No Gravado) pasa lo mismo.

Condiciones de disparo, para acotar el alcance: el ítem informa iva_id 1 o 2, con imp_iva en 0 o ausente e importe distinto de cero, y la factura es A/M — o B con receptor consumidor final, que es cuando se cumple letra_fact != "B" or consumidor_final (pyfepdf.py:1359).

El síntoma que llega al usuario es otro

Esto es lo que hace el problema difícil de diagnosticar: el KeyError no se ve. Lo que se ve es un error de imagen, a varios pasos de distancia.

  1. ProcesarPlantilla atrapa la excepción en su except Exception (pyfepdf.py:1653) para imprimirla dentro del PDF, y deja la traza en <tempdir>/traceback.txt.
  2. Al abortar antes de tiempo, nunca llega a ejecutar f.set("AFIP_QR", self.archivo_qr) (pyfepdf.py:1648).
  3. El elemento AFIP_QR de las plantillas que vienen con la librería tiene el texto vacío en el CSV:
    'AFIP_QR';'I';11.10;261.10;36.10;286.10;None;0;0;0;0;0;0;'I';'';2
    
  4. El sanity check de CrearPlantilla (pyfepdf.py:969-975) intenta resolver las rutas relativas de las imágenes. Con texto vacío, os.path.join(self.InstallDir, "") da el directorio de instalación, que os.path.exists() reporta como existente por ser un directorio, así que lo acepta como ruta de imagen válida:
    if field["type"] == "I" and not os.path.exists(text):
        if os.path.exists(os.path.join(self.InstallDir, text)):
            field["text"] = os.path.join(self.InstallDir, text)
  5. fpdf.image() recibe un directorio. Como no tiene extensión, name.rfind('.') devuelve -1, y el guard if(not pos) no se dispara porque -1 es truthy. El nombre entero termina usándose como si fuera el tipo de imagen.

Resultado final:

RuntimeError: FPDF error: Unsupported image type: /ruta/de/instalacion/pyafipws/

Un error que no menciona ni el IVA, ni el ítem, ni la línea real. Dejo la cadena documentada porque probablemente explique reportes previos con ese mensaje.

Fix propuesto

Guardar el acceso al diccionario. Si el código de IVA no tiene alícuota asociada, no hay IVA contenido que despejar y el importe queda como está:

                                             imp_it_iva = Decimal(it['imp_iva'] or "0.00")
-                                            if not imp_it_iva and it['importe']:
+                                            if (not imp_it_iva and it['importe']
+                                                    and int(it['iva_id']) in self.ivas_ds):
                                                 p = self.ivas_ds[int(it['iva_id'])]
                                                 importe_it = Decimal(it['importe'])

Es el mismo criterio que la librería ya aplica sobre este campo en fmt_iva (pyfepdf.py:799), que tolera los códigos sin alícuota devolviendo "":

def fmt_iva(self, i):
    if int(i) in self.ivas_ds:
        ...
    else:
        return ""

Con ese cambio, el script de arriba genera el PDF correctamente.

Nota aparte: en las plantillas factura.csv, fca.csv y fcb.csv que vienen con la librería no existen los campos Item.IvaId ni Item.AlicuotaIva, y para una factura B el iva_liq acumulado en esa rama se sobrescribe después con el imp_iva de la cabecera (pyfepdf.py:1524). O sea que en esos casos el bloque no llega a aportar nada al PDF.

Versión

Reproducido en d595b07, que al momento de este reporte es la punta de main.

Con gusto mando un PR con el cambio si les parece bien el enfoque.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions