
▲ 6 r/chileIT
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]
```
u/AccomplishedGift2987 — 1 day ago