u/AccomplishedGift2987

Lo que aprendí parseando los 4 tipos de DTE del SII en Python

Lo que aprendí parseando los 4 tipos de DTE del SII en Python

Hace unos meses empecé a construir una herramienta en Python para validar
facturas y boletas electrónicas chilenas (DTE). Lo que creí que iba a ser
un extractor XML simple terminó siendo más entretenido de lo que esperaba.


---


**El SII no es uno solo**


Todos los DTE son XML, pero cada tipo tiene un esquema distinto:


- Factura afecta (33): emisor en `<RznSoc>`, receptor en `<RznSocRecep>`
- Boleta electrónica (39): emisor en `<RznSocEmisor>`, receptor puede no existir
- Boleta exenta (41): igual que 39 pero sin IVA
- Nota de crédito (61): tiene `<Referencia>` apuntando al documento que acredita


Mi extractor empezó soportando solo el 33. Cuando agregué 39 y 41, encontré
el primer bug:


    # Esto falla silenciosamente en boletas consumidor final:
    rut = doc.find(".//RUTRecep").text  # → AttributeError: 'NoneType'


La boleta al consumidor no tiene nodo `<RUTRecep>`. Hay que manejar la
ausencia y marcarla como "sin receptor".


---


**Validación de RUT con módulo-11: el edge case del consumidor final**


    def validar_rut(rut_str: str) -> bool:
        # El RUT "66666666-6" es válido por convención del SII
        # pero el módulo-11 puro lo rechaza
        if rut_str.replace(".", "").replace("-", "") == "666666666":
            return True
        # ... resto del algoritmo


Si validas RUT a secas con módulo-11, rechazas todas las boletas consumidor
final. El "66666666-6" es un código especial que el SII usa para ese caso
y hay que whitelistearlo explícitamente.


---


**Detección de duplicados en 4 capas**


El problema de duplicados es peor de lo que parece porque la misma factura
puede llegar como XML original, como PDF escaneado, o como foto de celular.
Terminé con 4 capas en orden de costo computacional:


1. Hash exacto del archivo (los más obvios, O(1))
2. Hash perceptual de imagen con imagehash (misma foto con brillo distinto)
3. Comparación de campos: folio + RUT emisor + monto → duplicado aunque el
   archivo sea diferente
4. Similitud difusa con threshold (para OCR imperfecto donde el monto puede
   variar ±1 por ruido)


Las capas baratas van primero para no ejecutar OCR innecesariamente.


---


**El bundle de PyInstaller con assets estáticos**


Detalle que me costó: con `noarchive=False` (default), los .py quedan
dentro de `base_library.zip`. Eso significa que `__file__` resuelve a un
path virtual que no existe en disco, y cualquier código que haga:


    Path(__file__).parent / "ui" / "styles.qss"


silenciosamente falla porque el path no existe en el sistema de archivos.
La solución es detectar el entorno frozen y usar `sys._MEIPASS`:


    if getattr(sys, "frozen", False):
        base = Path(sys._MEIPASS)
    else:
        base = Path(__file__).resolve().parent
    qss_path = base / "ui" / "styles.qss"


No vi esto documentado claramente en ningún lado, lo encontré después de
una hora mirando logs vacíos en producción.


---


**Stack completo**


- Python 3.11 + PySide6 (Qt6) — UI de escritorio
- SQLite + SQLAlchemy — todo local, sin servidor
- pdfplumber + Tesseract — extracción desde PDF e imagen
- PyInstaller — distribución como .exe sin requerir Python instalado


---


Si trabajan con DTE o han parseado el XML del SII para otro propósito,
me interesa saber qué otros quirks encontraron.


La herramienta ya está funcionando. Hice una demo gratuita para quien
quiera probarla: [vistoapp.cl]
```

https://preview.redd.it/pf9n9c93gc2h1.jpg?width=1281&format=pjpg&auto=webp&s=936c9df12ec4fd080a2cb90f8d0cfd33a8be0f0b

reddit.com